feat(ui): enhance agent interface with modal viewer and improved icon picker
- Add AgentRunOutputViewer modal component for inline run preview - Implement IconPicker component with expanded icon selection - Refactor AgentRunsList to use modal instead of full-page navigation - Improve CreateAgent form layout with better grid structure - Update agent icon system to support wider range of icons - Enhance UI components with better animations and styling - Add scroll-area component for better content scrolling - Remove unused AgentRunView in favor of modal approach
This commit is contained in:
665
src/components/AgentRunOutputViewer.tsx
Normal file
665
src/components/AgentRunOutputViewer.tsx
Normal file
@@ -0,0 +1,665 @@
|
|||||||
|
import { useState, useEffect, useRef, useMemo } from 'react';
|
||||||
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
|
import {
|
||||||
|
X,
|
||||||
|
Maximize2,
|
||||||
|
Minimize2,
|
||||||
|
Copy,
|
||||||
|
RefreshCw,
|
||||||
|
RotateCcw,
|
||||||
|
ChevronDown,
|
||||||
|
Bot,
|
||||||
|
Clock,
|
||||||
|
Hash,
|
||||||
|
DollarSign,
|
||||||
|
ExternalLink
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Toast, ToastContainer } from '@/components/ui/toast';
|
||||||
|
import { Popover } from '@/components/ui/popover';
|
||||||
|
import { api, type AgentRunWithMetrics } from '@/lib/api';
|
||||||
|
import { useOutputCache } from '@/lib/outputCache';
|
||||||
|
import { listen, type UnlistenFn } from '@tauri-apps/api/event';
|
||||||
|
import { StreamMessage } from './StreamMessage';
|
||||||
|
import { ErrorBoundary } from './ErrorBoundary';
|
||||||
|
import { formatISOTimestamp } from '@/lib/date-utils';
|
||||||
|
import { AGENT_ICONS } from './CCAgents';
|
||||||
|
import type { ClaudeStreamMessage } from './AgentExecution';
|
||||||
|
|
||||||
|
interface AgentRunOutputViewerProps {
|
||||||
|
/**
|
||||||
|
* The agent run to display
|
||||||
|
*/
|
||||||
|
run: AgentRunWithMetrics;
|
||||||
|
/**
|
||||||
|
* Callback when the viewer is closed
|
||||||
|
*/
|
||||||
|
onClose: () => void;
|
||||||
|
/**
|
||||||
|
* Optional callback to open full view
|
||||||
|
*/
|
||||||
|
onOpenFullView?: () => void;
|
||||||
|
/**
|
||||||
|
* Optional className for styling
|
||||||
|
*/
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AgentRunOutputViewer - Modal component for viewing agent execution output
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* <AgentRunOutputViewer
|
||||||
|
* run={agentRun}
|
||||||
|
* onClose={() => setSelectedRun(null)}
|
||||||
|
* />
|
||||||
|
*/
|
||||||
|
export function AgentRunOutputViewer({
|
||||||
|
run,
|
||||||
|
onClose,
|
||||||
|
onOpenFullView,
|
||||||
|
className
|
||||||
|
}: AgentRunOutputViewerProps) {
|
||||||
|
const [messages, setMessages] = useState<ClaudeStreamMessage[]>([]);
|
||||||
|
const [rawJsonlOutput, setRawJsonlOutput] = useState<string[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||||
|
const [refreshing, setRefreshing] = useState(false);
|
||||||
|
const [toast, setToast] = useState<{ message: string; type: "success" | "error" } | null>(null);
|
||||||
|
const [copyPopoverOpen, setCopyPopoverOpen] = useState(false);
|
||||||
|
const [hasUserScrolled, setHasUserScrolled] = useState(false);
|
||||||
|
|
||||||
|
const scrollAreaRef = useRef<HTMLDivElement>(null);
|
||||||
|
const outputEndRef = useRef<HTMLDivElement>(null);
|
||||||
|
const fullscreenScrollRef = useRef<HTMLDivElement>(null);
|
||||||
|
const fullscreenMessagesEndRef = useRef<HTMLDivElement>(null);
|
||||||
|
const unlistenRefs = useRef<UnlistenFn[]>([]);
|
||||||
|
const { getCachedOutput, setCachedOutput } = useOutputCache();
|
||||||
|
|
||||||
|
// Auto-scroll logic
|
||||||
|
const isAtBottom = () => {
|
||||||
|
const container = isFullscreen ? fullscreenScrollRef.current : scrollAreaRef.current;
|
||||||
|
if (container) {
|
||||||
|
const { scrollTop, scrollHeight, clientHeight } = container;
|
||||||
|
const distanceFromBottom = scrollHeight - scrollTop - clientHeight;
|
||||||
|
return distanceFromBottom < 1;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const scrollToBottom = () => {
|
||||||
|
if (!hasUserScrolled) {
|
||||||
|
const endRef = isFullscreen ? fullscreenMessagesEndRef.current : outputEndRef.current;
|
||||||
|
if (endRef) {
|
||||||
|
endRef.scrollIntoView({ behavior: 'smooth' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Clean up listeners on unmount
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
unlistenRefs.current.forEach(unlisten => unlisten());
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Auto-scroll when messages change
|
||||||
|
useEffect(() => {
|
||||||
|
const shouldAutoScroll = !hasUserScrolled || isAtBottom();
|
||||||
|
if (shouldAutoScroll) {
|
||||||
|
scrollToBottom();
|
||||||
|
}
|
||||||
|
}, [messages, hasUserScrolled, isFullscreen]);
|
||||||
|
|
||||||
|
const loadOutput = async (skipCache = false) => {
|
||||||
|
if (!run.id) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check cache first if not skipping cache
|
||||||
|
if (!skipCache) {
|
||||||
|
const cached = getCachedOutput(run.id);
|
||||||
|
if (cached) {
|
||||||
|
const cachedJsonlLines = cached.output.split('\n').filter(line => line.trim());
|
||||||
|
setRawJsonlOutput(cachedJsonlLines);
|
||||||
|
setMessages(cached.messages);
|
||||||
|
// If cache is recent (less than 5 seconds old) and session isn't running, use cache only
|
||||||
|
if (Date.now() - cached.lastUpdated < 5000 && run.status !== 'running') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
const rawOutput = await api.getSessionOutput(run.id);
|
||||||
|
|
||||||
|
// Parse JSONL output into messages
|
||||||
|
const jsonlLines = rawOutput.split('\n').filter(line => line.trim());
|
||||||
|
setRawJsonlOutput(jsonlLines);
|
||||||
|
|
||||||
|
const parsedMessages: ClaudeStreamMessage[] = [];
|
||||||
|
for (const line of jsonlLines) {
|
||||||
|
try {
|
||||||
|
const message = JSON.parse(line) as ClaudeStreamMessage;
|
||||||
|
parsedMessages.push(message);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to parse message:", err, line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setMessages(parsedMessages);
|
||||||
|
|
||||||
|
// Update cache
|
||||||
|
setCachedOutput(run.id, {
|
||||||
|
output: rawOutput,
|
||||||
|
messages: parsedMessages,
|
||||||
|
lastUpdated: Date.now(),
|
||||||
|
status: run.status
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set up live event listeners for running sessions
|
||||||
|
if (run.status === 'running') {
|
||||||
|
setupLiveEventListeners();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await api.streamSessionOutput(run.id);
|
||||||
|
} catch (streamError) {
|
||||||
|
console.warn('Failed to start streaming, will poll instead:', streamError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load agent output:', error);
|
||||||
|
setToast({ message: 'Failed to load agent output', type: 'error' });
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const setupLiveEventListeners = async () => {
|
||||||
|
try {
|
||||||
|
// Clean up existing listeners
|
||||||
|
unlistenRefs.current.forEach(unlisten => unlisten());
|
||||||
|
unlistenRefs.current = [];
|
||||||
|
|
||||||
|
// Set up live event listeners
|
||||||
|
const outputUnlisten = await listen<string>("agent-output", (event) => {
|
||||||
|
try {
|
||||||
|
// Store raw JSONL
|
||||||
|
setRawJsonlOutput(prev => [...prev, event.payload]);
|
||||||
|
|
||||||
|
// Parse and display
|
||||||
|
const message = JSON.parse(event.payload) as ClaudeStreamMessage;
|
||||||
|
setMessages(prev => [...prev, message]);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to parse message:", err, event.payload);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const errorUnlisten = await listen<string>("agent-error", (event) => {
|
||||||
|
console.error("Agent error:", event.payload);
|
||||||
|
setToast({ message: event.payload, type: 'error' });
|
||||||
|
});
|
||||||
|
|
||||||
|
const completeUnlisten = await listen<boolean>("agent-complete", () => {
|
||||||
|
setToast({ message: 'Agent execution completed', type: 'success' });
|
||||||
|
});
|
||||||
|
|
||||||
|
unlistenRefs.current = [outputUnlisten, errorUnlisten, completeUnlisten];
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to set up live event listeners:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Copy functionality
|
||||||
|
const handleCopyAsJsonl = async () => {
|
||||||
|
const jsonl = rawJsonlOutput.join('\n');
|
||||||
|
await navigator.clipboard.writeText(jsonl);
|
||||||
|
setCopyPopoverOpen(false);
|
||||||
|
setToast({ message: 'Output copied as JSONL', type: 'success' });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCopyAsMarkdown = async () => {
|
||||||
|
let markdown = `# Agent Execution: ${run.agent_name}\n\n`;
|
||||||
|
markdown += `**Task:** ${run.task}\n`;
|
||||||
|
markdown += `**Model:** ${run.model === 'opus' ? 'Claude 4 Opus' : 'Claude 4 Sonnet'}\n`;
|
||||||
|
markdown += `**Date:** ${formatISOTimestamp(run.created_at)}\n`;
|
||||||
|
if (run.metrics?.duration_ms) markdown += `**Duration:** ${(run.metrics.duration_ms / 1000).toFixed(2)}s\n`;
|
||||||
|
if (run.metrics?.total_tokens) markdown += `**Total Tokens:** ${run.metrics.total_tokens}\n`;
|
||||||
|
if (run.metrics?.cost_usd) markdown += `**Cost:** $${run.metrics.cost_usd.toFixed(4)} USD\n`;
|
||||||
|
markdown += `\n---\n\n`;
|
||||||
|
|
||||||
|
for (const msg of messages) {
|
||||||
|
if (msg.type === "system" && msg.subtype === "init") {
|
||||||
|
markdown += `## System Initialization\n\n`;
|
||||||
|
markdown += `- Session ID: \`${msg.session_id || 'N/A'}\`\n`;
|
||||||
|
markdown += `- Model: \`${msg.model || 'default'}\`\n`;
|
||||||
|
if (msg.cwd) markdown += `- Working Directory: \`${msg.cwd}\`\n`;
|
||||||
|
if (msg.tools?.length) markdown += `- Tools: ${msg.tools.join(', ')}\n`;
|
||||||
|
markdown += `\n`;
|
||||||
|
} else if (msg.type === "assistant" && msg.message) {
|
||||||
|
markdown += `## Assistant\n\n`;
|
||||||
|
for (const content of msg.message.content || []) {
|
||||||
|
if (content.type === "text") {
|
||||||
|
markdown += `${content.text}\n\n`;
|
||||||
|
} else if (content.type === "tool_use") {
|
||||||
|
markdown += `### Tool: ${content.name}\n\n`;
|
||||||
|
markdown += `\`\`\`json\n${JSON.stringify(content.input, null, 2)}\n\`\`\`\n\n`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (msg.message.usage) {
|
||||||
|
markdown += `*Tokens: ${msg.message.usage.input_tokens} in, ${msg.message.usage.output_tokens} out*\n\n`;
|
||||||
|
}
|
||||||
|
} else if (msg.type === "user" && msg.message) {
|
||||||
|
markdown += `## User\n\n`;
|
||||||
|
for (const content of msg.message.content || []) {
|
||||||
|
if (content.type === "text") {
|
||||||
|
markdown += `${content.text}\n\n`;
|
||||||
|
} else if (content.type === "tool_result") {
|
||||||
|
markdown += `### Tool Result\n\n`;
|
||||||
|
markdown += `\`\`\`\n${content.content}\n\`\`\`\n\n`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (msg.type === "result") {
|
||||||
|
markdown += `## Execution Result\n\n`;
|
||||||
|
if (msg.result) {
|
||||||
|
markdown += `${msg.result}\n\n`;
|
||||||
|
}
|
||||||
|
if (msg.error) {
|
||||||
|
markdown += `**Error:** ${msg.error}\n\n`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await navigator.clipboard.writeText(markdown);
|
||||||
|
setCopyPopoverOpen(false);
|
||||||
|
setToast({ message: 'Output copied as Markdown', type: 'success' });
|
||||||
|
};
|
||||||
|
|
||||||
|
const refreshOutput = async () => {
|
||||||
|
setRefreshing(true);
|
||||||
|
await loadOutput(true); // Skip cache
|
||||||
|
setRefreshing(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleScroll = (e: React.UIEvent<HTMLDivElement>) => {
|
||||||
|
const target = e.currentTarget;
|
||||||
|
const { scrollTop, scrollHeight, clientHeight } = target;
|
||||||
|
const distanceFromBottom = scrollHeight - scrollTop - clientHeight;
|
||||||
|
setHasUserScrolled(distanceFromBottom > 50);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Load output on mount
|
||||||
|
useEffect(() => {
|
||||||
|
if (!run.id) return;
|
||||||
|
|
||||||
|
// Check cache immediately for instant display
|
||||||
|
const cached = getCachedOutput(run.id);
|
||||||
|
if (cached) {
|
||||||
|
const cachedJsonlLines = cached.output.split('\n').filter(line => line.trim());
|
||||||
|
setRawJsonlOutput(cachedJsonlLines);
|
||||||
|
setMessages(cached.messages);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then load fresh data
|
||||||
|
loadOutput();
|
||||||
|
}, [run.id]);
|
||||||
|
|
||||||
|
const displayableMessages = useMemo(() => {
|
||||||
|
return messages.filter((message) => {
|
||||||
|
if (message.isMeta && !message.leafUuid && !message.summary) return false;
|
||||||
|
|
||||||
|
if (message.type === "user" && message.message) {
|
||||||
|
if (message.isMeta) return false;
|
||||||
|
|
||||||
|
const msg = message.message;
|
||||||
|
if (!msg.content || (Array.isArray(msg.content) && msg.content.length === 0)) return false;
|
||||||
|
|
||||||
|
if (Array.isArray(msg.content)) {
|
||||||
|
let hasVisibleContent = false;
|
||||||
|
for (const content of msg.content) {
|
||||||
|
if (content.type === "text") { hasVisibleContent = true; break; }
|
||||||
|
if (content.type === "tool_result") {
|
||||||
|
// Check if this tool result will be displayed as a widget
|
||||||
|
let willBeSkipped = false;
|
||||||
|
if (content.tool_use_id) {
|
||||||
|
// Find the corresponding tool use
|
||||||
|
for (let i = messages.indexOf(message) - 1; i >= 0; i--) {
|
||||||
|
const prevMsg = messages[i];
|
||||||
|
if (prevMsg.type === 'assistant' && prevMsg.message?.content && Array.isArray(prevMsg.message.content)) {
|
||||||
|
const toolUse = prevMsg.message.content.find((c: any) => c.type === 'tool_use' && c.id === content.tool_use_id);
|
||||||
|
if (toolUse) {
|
||||||
|
const toolName = toolUse.name?.toLowerCase();
|
||||||
|
const toolsWithWidgets = ['task','edit','multiedit','todowrite','ls','read','glob','bash','write','grep'];
|
||||||
|
if (toolsWithWidgets.includes(toolName) || toolUse.name?.startsWith('mcp__')) {
|
||||||
|
willBeSkipped = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!willBeSkipped) { hasVisibleContent = true; break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!hasVisibleContent) return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}, [messages]);
|
||||||
|
|
||||||
|
const renderIcon = (iconName: string) => {
|
||||||
|
const Icon = AGENT_ICONS[iconName as keyof typeof AGENT_ICONS] || Bot;
|
||||||
|
return <Icon className="h-5 w-5" />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDuration = (ms?: number) => {
|
||||||
|
if (!ms) return "N/A";
|
||||||
|
const seconds = Math.floor(ms / 1000);
|
||||||
|
if (seconds < 60) return `${seconds}s`;
|
||||||
|
const minutes = Math.floor(seconds / 60);
|
||||||
|
const remainingSeconds = seconds % 60;
|
||||||
|
return `${minutes}m ${remainingSeconds}s`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatTokens = (tokens?: number) => {
|
||||||
|
if (!tokens) return "0";
|
||||||
|
if (tokens >= 1000) {
|
||||||
|
return `${(tokens / 1000).toFixed(1)}k`;
|
||||||
|
}
|
||||||
|
return tokens.toString();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
className="fixed inset-0 bg-background/80 backdrop-blur-sm z-40"
|
||||||
|
onClick={onClose}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
||||||
|
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, scale: 0.95, y: 20 }}
|
||||||
|
className={`fixed inset-x-4 top-[10%] bottom-[10%] z-50 max-w-4xl mx-auto ${className}`}
|
||||||
|
>
|
||||||
|
<Card className="h-full flex flex-col shadow-xl">
|
||||||
|
<CardHeader className="pb-3">
|
||||||
|
<div className="flex items-start justify-between gap-4">
|
||||||
|
<div className="flex items-start gap-3 flex-1 min-w-0">
|
||||||
|
<div className="mt-0.5">
|
||||||
|
{renderIcon(run.agent_icon)}
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<CardTitle className="text-lg flex items-center gap-2">
|
||||||
|
{run.agent_name}
|
||||||
|
{run.status === 'running' && (
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div>
|
||||||
|
<span className="text-xs text-green-600 font-medium">Running</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardTitle>
|
||||||
|
<p className="text-sm text-muted-foreground mt-1 truncate">
|
||||||
|
{run.task}
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center gap-3 text-xs text-muted-foreground mt-2">
|
||||||
|
<Badge variant="outline" className="text-xs">
|
||||||
|
{run.model === 'opus' ? 'Claude 4 Opus' : 'Claude 4 Sonnet'}
|
||||||
|
</Badge>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Clock className="h-3 w-3" />
|
||||||
|
<span>{formatISOTimestamp(run.created_at)}</span>
|
||||||
|
</div>
|
||||||
|
{run.metrics?.duration_ms && (
|
||||||
|
<span>{formatDuration(run.metrics.duration_ms)}</span>
|
||||||
|
)}
|
||||||
|
{run.metrics?.total_tokens && (
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Hash className="h-3 w-3" />
|
||||||
|
<span>{formatTokens(run.metrics.total_tokens)}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{run.metrics?.cost_usd && (
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<DollarSign className="h-3 w-3" />
|
||||||
|
<span>${run.metrics.cost_usd.toFixed(4)}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Popover
|
||||||
|
trigger={
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-8 px-2"
|
||||||
|
>
|
||||||
|
<Copy className="h-4 w-4 mr-1" />
|
||||||
|
Copy
|
||||||
|
<ChevronDown className="h-3 w-3 ml-1" />
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
content={
|
||||||
|
<div className="w-44 p-1">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="w-full justify-start"
|
||||||
|
onClick={handleCopyAsJsonl}
|
||||||
|
>
|
||||||
|
Copy as JSONL
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="w-full justify-start"
|
||||||
|
onClick={handleCopyAsMarkdown}
|
||||||
|
>
|
||||||
|
Copy as Markdown
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
open={copyPopoverOpen}
|
||||||
|
onOpenChange={setCopyPopoverOpen}
|
||||||
|
align="end"
|
||||||
|
/>
|
||||||
|
{onOpenFullView && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={onOpenFullView}
|
||||||
|
title="Open in full view"
|
||||||
|
className="h-8 px-2"
|
||||||
|
>
|
||||||
|
<ExternalLink className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setIsFullscreen(!isFullscreen)}
|
||||||
|
title={isFullscreen ? "Exit fullscreen" : "Enter fullscreen"}
|
||||||
|
className="h-8 px-2"
|
||||||
|
>
|
||||||
|
{isFullscreen ? (
|
||||||
|
<Minimize2 className="h-4 w-4" />
|
||||||
|
) : (
|
||||||
|
<Maximize2 className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={refreshOutput}
|
||||||
|
disabled={refreshing}
|
||||||
|
title="Refresh output"
|
||||||
|
className="h-8 px-2"
|
||||||
|
>
|
||||||
|
<RotateCcw className={`h-4 w-4 ${refreshing ? 'animate-spin' : ''}`} />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={onClose}
|
||||||
|
className="h-8 px-2"
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className={`${isFullscreen ? 'h-[calc(100vh-120px)]' : 'flex-1'} p-0 overflow-hidden`}>
|
||||||
|
{loading ? (
|
||||||
|
<div className="flex items-center justify-center h-full">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<RefreshCw className="h-4 w-4 animate-spin" />
|
||||||
|
<span>Loading output...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : messages.length === 0 ? (
|
||||||
|
<div className="flex items-center justify-center h-full text-muted-foreground">
|
||||||
|
<p>No output available yet</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
ref={scrollAreaRef}
|
||||||
|
className="h-full overflow-y-auto p-4 space-y-2"
|
||||||
|
onScroll={handleScroll}
|
||||||
|
>
|
||||||
|
<AnimatePresence>
|
||||||
|
{displayableMessages.map((message: ClaudeStreamMessage, index: number) => (
|
||||||
|
<motion.div
|
||||||
|
key={index}
|
||||||
|
initial={{ opacity: 0, y: 10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
>
|
||||||
|
<ErrorBoundary>
|
||||||
|
<StreamMessage message={message} streamMessages={messages} />
|
||||||
|
</ErrorBoundary>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</AnimatePresence>
|
||||||
|
<div ref={outputEndRef} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Fullscreen Modal */}
|
||||||
|
{isFullscreen && (
|
||||||
|
<div className="fixed inset-0 bg-background z-[60] flex flex-col">
|
||||||
|
<div className="flex items-center justify-between p-4 border-b">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
{renderIcon(run.agent_icon)}
|
||||||
|
<div>
|
||||||
|
<h3 className="font-semibold text-lg">{run.agent_name}</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">{run.task}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Popover
|
||||||
|
trigger={
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
<Copy className="h-4 w-4 mr-2" />
|
||||||
|
Copy Output
|
||||||
|
<ChevronDown className="h-3 w-3 ml-2" />
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
content={
|
||||||
|
<div className="w-44 p-1">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="w-full justify-start"
|
||||||
|
onClick={handleCopyAsJsonl}
|
||||||
|
>
|
||||||
|
Copy as JSONL
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="w-full justify-start"
|
||||||
|
onClick={handleCopyAsMarkdown}
|
||||||
|
>
|
||||||
|
Copy as Markdown
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
align="end"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={refreshOutput}
|
||||||
|
disabled={refreshing}
|
||||||
|
>
|
||||||
|
<RotateCcw className={`h-4 w-4 ${refreshing ? 'animate-spin' : ''}`} />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setIsFullscreen(false)}
|
||||||
|
>
|
||||||
|
<Minimize2 className="h-4 w-4 mr-2" />
|
||||||
|
Exit Fullscreen
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
ref={fullscreenScrollRef}
|
||||||
|
className="flex-1 overflow-y-auto p-6"
|
||||||
|
onScroll={handleScroll}
|
||||||
|
>
|
||||||
|
<div className="max-w-4xl mx-auto space-y-2">
|
||||||
|
{messages.length === 0 ? (
|
||||||
|
<div className="text-center text-muted-foreground py-8">
|
||||||
|
No output available yet
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<AnimatePresence>
|
||||||
|
{displayableMessages.map((message: ClaudeStreamMessage, index: number) => (
|
||||||
|
<motion.div
|
||||||
|
key={index}
|
||||||
|
initial={{ opacity: 0, y: 10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
>
|
||||||
|
<ErrorBoundary>
|
||||||
|
<StreamMessage message={message} streamMessages={messages} />
|
||||||
|
</ErrorBoundary>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</AnimatePresence>
|
||||||
|
<div ref={fullscreenMessagesEndRef} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Toast Notification */}
|
||||||
|
<ToastContainer>
|
||||||
|
{toast && (
|
||||||
|
<Toast
|
||||||
|
message={toast.message}
|
||||||
|
type={toast.type}
|
||||||
|
onDismiss={() => setToast(null)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</ToastContainer>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { motion } from "framer-motion";
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
import { Play, Clock, Hash, Bot } from "lucide-react";
|
import { Play, Clock, Hash, Bot } from "lucide-react";
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
@@ -8,6 +8,7 @@ import { cn } from "@/lib/utils";
|
|||||||
import { formatISOTimestamp } from "@/lib/date-utils";
|
import { formatISOTimestamp } from "@/lib/date-utils";
|
||||||
import type { AgentRunWithMetrics } from "@/lib/api";
|
import type { AgentRunWithMetrics } from "@/lib/api";
|
||||||
import { AGENT_ICONS } from "./CCAgents";
|
import { AGENT_ICONS } from "./CCAgents";
|
||||||
|
import { AgentRunOutputViewer } from "./AgentRunOutputViewer";
|
||||||
|
|
||||||
interface AgentRunsListProps {
|
interface AgentRunsListProps {
|
||||||
/**
|
/**
|
||||||
@@ -41,6 +42,7 @@ export const AgentRunsList: React.FC<AgentRunsListProps> = ({
|
|||||||
className,
|
className,
|
||||||
}) => {
|
}) => {
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
const [selectedRun, setSelectedRun] = useState<AgentRunWithMetrics | null>(null);
|
||||||
|
|
||||||
// Calculate pagination
|
// Calculate pagination
|
||||||
const totalPages = Math.ceil(runs.length / ITEMS_PER_PAGE);
|
const totalPages = Math.ceil(runs.length / ITEMS_PER_PAGE);
|
||||||
@@ -75,6 +77,16 @@ export const AgentRunsList: React.FC<AgentRunsListProps> = ({
|
|||||||
return tokens.toString();
|
return tokens.toString();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleRunClick = (run: AgentRunWithMetrics) => {
|
||||||
|
// If there's a callback, use it (for full-page navigation)
|
||||||
|
if (onRunClick) {
|
||||||
|
onRunClick(run);
|
||||||
|
} else {
|
||||||
|
// Otherwise, open in modal preview
|
||||||
|
setSelectedRun(run);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (runs.length === 0) {
|
if (runs.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className={cn("text-center py-8 text-muted-foreground", className)}>
|
<div className={cn("text-center py-8 text-muted-foreground", className)}>
|
||||||
@@ -83,92 +95,114 @@ export const AgentRunsList: React.FC<AgentRunsListProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn("space-y-4", className)}>
|
<>
|
||||||
<div className="space-y-2">
|
<div className={cn("space-y-2", className)}>
|
||||||
{currentRuns.map((run, index) => (
|
<AnimatePresence mode="popLayout">
|
||||||
<motion.div
|
{currentRuns.map((run, index) => (
|
||||||
key={run.id}
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 20 }}
|
key={run.id}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
transition={{
|
animate={{ opacity: 1, y: 0 }}
|
||||||
duration: 0.3,
|
exit={{ opacity: 0, y: -20 }}
|
||||||
delay: index * 0.05,
|
transition={{
|
||||||
ease: [0.4, 0, 0.2, 1],
|
duration: 0.3,
|
||||||
}}
|
delay: index * 0.05,
|
||||||
>
|
ease: [0.4, 0, 0.2, 1],
|
||||||
<Card
|
}}
|
||||||
className={cn(
|
|
||||||
"transition-all hover:shadow-md cursor-pointer",
|
|
||||||
onRunClick && "hover:shadow-lg hover:border-primary/50 active:scale-[0.99]"
|
|
||||||
)}
|
|
||||||
onClick={() => onRunClick?.(run)}
|
|
||||||
>
|
>
|
||||||
<CardContent className="p-3">
|
<Card
|
||||||
<div className="flex items-start justify-between gap-3">
|
className={cn(
|
||||||
<div className="flex items-start gap-3 flex-1 min-w-0">
|
"cursor-pointer transition-all hover:shadow-md hover:scale-[1.01] active:scale-[0.99]",
|
||||||
<div className="mt-0.5">
|
run.status === "running" && "border-green-500/50"
|
||||||
|
)}
|
||||||
|
onClick={() => handleRunClick(run)}
|
||||||
|
>
|
||||||
|
<CardContent className="p-3">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="flex-shrink-0">
|
||||||
{renderIcon(run.agent_icon)}
|
{renderIcon(run.agent_icon)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-w-0 space-y-1">
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex-1 min-w-0">
|
||||||
<p className="text-sm font-medium truncate">{run.task}</p>
|
<div className="flex items-center gap-2 mb-1">
|
||||||
<Badge variant="outline" className="text-xs">
|
<h4 className="text-sm font-medium truncate">
|
||||||
{run.model === 'opus' ? 'Claude 4 Opus' : 'Claude 4 Sonnet'}
|
{run.agent_name}
|
||||||
</Badge>
|
</h4>
|
||||||
</div>
|
{run.status === "running" && (
|
||||||
<div className="flex items-center gap-3 text-xs text-muted-foreground">
|
<div className="flex items-center gap-1">
|
||||||
<span className="truncate">by {run.agent_name}</span>
|
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div>
|
||||||
{run.completed_at && (
|
<span className="text-xs text-green-600 font-medium">Running</span>
|
||||||
<>
|
</div>
|
||||||
<span>•</span>
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<Clock className="h-3 w-3" />
|
|
||||||
<span>{formatDuration(run.metrics?.duration_ms)}</span>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{run.metrics?.total_tokens && (
|
|
||||||
<>
|
|
||||||
<span>•</span>
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<Hash className="h-3 w-3" />
|
|
||||||
<span>{formatTokens(run.metrics?.total_tokens)}</span>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{run.metrics?.cost_usd && (
|
|
||||||
<>
|
|
||||||
<span>•</span>
|
|
||||||
<span>${run.metrics?.cost_usd?.toFixed(4)}</span>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
{formatISOTimestamp(run.created_at)}
|
<p className="text-xs text-muted-foreground truncate mb-1">
|
||||||
|
{run.task}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-3 text-xs text-muted-foreground">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Clock className="h-3 w-3" />
|
||||||
|
<span>{formatISOTimestamp(run.created_at)}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{run.metrics?.duration_ms && (
|
||||||
|
<span>{formatDuration(run.metrics.duration_ms)}</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{run.metrics?.total_tokens && (
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Hash className="h-3 w-3" />
|
||||||
|
<span>{formatTokens(run.metrics.total_tokens)}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
<Badge
|
||||||
|
variant={
|
||||||
|
run.status === "completed" ? "default" :
|
||||||
|
run.status === "running" ? "secondary" :
|
||||||
|
run.status === "failed" ? "destructive" :
|
||||||
|
"outline"
|
||||||
|
}
|
||||||
|
className="text-xs"
|
||||||
|
>
|
||||||
|
{run.status === "completed" ? "Completed" :
|
||||||
|
run.status === "running" ? "Running" :
|
||||||
|
run.status === "failed" ? "Failed" :
|
||||||
|
"Pending"}
|
||||||
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{!run.completed_at && (
|
</CardContent>
|
||||||
<Badge variant="secondary" className="text-xs">
|
</Card>
|
||||||
Running
|
</motion.div>
|
||||||
</Badge>
|
))}
|
||||||
)}
|
</AnimatePresence>
|
||||||
</div>
|
|
||||||
</CardContent>
|
{/* Pagination */}
|
||||||
</Card>
|
{totalPages > 1 && (
|
||||||
</motion.div>
|
<div className="pt-2">
|
||||||
))}
|
<Pagination
|
||||||
|
currentPage={currentPage}
|
||||||
|
totalPages={totalPages}
|
||||||
|
onPageChange={setCurrentPage}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{totalPages > 1 && (
|
{/* Agent Run Output Viewer Modal */}
|
||||||
<Pagination
|
{selectedRun && (
|
||||||
currentPage={currentPage}
|
<AgentRunOutputViewer
|
||||||
totalPages={totalPages}
|
run={selectedRun}
|
||||||
onPageChange={setCurrentPage}
|
onClose={() => setSelectedRun(null)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
@@ -6,14 +6,6 @@ import {
|
|||||||
Trash2,
|
Trash2,
|
||||||
Play,
|
Play,
|
||||||
Bot,
|
Bot,
|
||||||
Brain,
|
|
||||||
Code,
|
|
||||||
Sparkles,
|
|
||||||
Zap,
|
|
||||||
Cpu,
|
|
||||||
Rocket,
|
|
||||||
Shield,
|
|
||||||
Terminal,
|
|
||||||
ArrowLeft,
|
ArrowLeft,
|
||||||
History,
|
History,
|
||||||
Download,
|
Download,
|
||||||
@@ -46,9 +38,9 @@ import { Toast, ToastContainer } from "@/components/ui/toast";
|
|||||||
import { CreateAgent } from "./CreateAgent";
|
import { CreateAgent } from "./CreateAgent";
|
||||||
import { AgentExecution } from "./AgentExecution";
|
import { AgentExecution } from "./AgentExecution";
|
||||||
import { AgentRunsList } from "./AgentRunsList";
|
import { AgentRunsList } from "./AgentRunsList";
|
||||||
import { AgentRunView } from "./AgentRunView";
|
|
||||||
import { RunningSessionsView } from "./RunningSessionsView";
|
import { RunningSessionsView } from "./RunningSessionsView";
|
||||||
import { GitHubAgentBrowser } from "./GitHubAgentBrowser";
|
import { GitHubAgentBrowser } from "./GitHubAgentBrowser";
|
||||||
|
import { ICON_MAP } from "./IconPicker";
|
||||||
|
|
||||||
interface CCAgentsProps {
|
interface CCAgentsProps {
|
||||||
/**
|
/**
|
||||||
@@ -61,18 +53,8 @@ interface CCAgentsProps {
|
|||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Available icons for agents
|
// Available icons for agents - now using all icons from IconPicker
|
||||||
export const AGENT_ICONS = {
|
export const AGENT_ICONS = ICON_MAP;
|
||||||
bot: Bot,
|
|
||||||
brain: Brain,
|
|
||||||
code: Code,
|
|
||||||
sparkles: Sparkles,
|
|
||||||
zap: Zap,
|
|
||||||
cpu: Cpu,
|
|
||||||
rocket: Rocket,
|
|
||||||
shield: Shield,
|
|
||||||
terminal: Terminal,
|
|
||||||
};
|
|
||||||
|
|
||||||
export type AgentIconName = keyof typeof AGENT_ICONS;
|
export type AgentIconName = keyof typeof AGENT_ICONS;
|
||||||
|
|
||||||
@@ -90,10 +72,10 @@ export const CCAgents: React.FC<CCAgentsProps> = ({ onBack, className }) => {
|
|||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [toast, setToast] = useState<{ message: string; type: "success" | "error" } | null>(null);
|
const [toast, setToast] = useState<{ message: string; type: "success" | "error" } | null>(null);
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const [view, setView] = useState<"list" | "create" | "edit" | "execute" | "viewRun">("list");
|
const [view, setView] = useState<"list" | "create" | "edit" | "execute">("list");
|
||||||
const [activeTab, setActiveTab] = useState<"agents" | "running">("agents");
|
const [activeTab, setActiveTab] = useState<"agents" | "running">("agents");
|
||||||
const [selectedAgent, setSelectedAgent] = useState<Agent | null>(null);
|
const [selectedAgent, setSelectedAgent] = useState<Agent | null>(null);
|
||||||
const [selectedRunId, setSelectedRunId] = useState<number | null>(null);
|
// const [selectedRunId, setSelectedRunId] = useState<number | null>(null);
|
||||||
const [showGitHubBrowser, setShowGitHubBrowser] = useState(false);
|
const [showGitHubBrowser, setShowGitHubBrowser] = useState(false);
|
||||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
||||||
const [agentToDelete, setAgentToDelete] = useState<Agent | null>(null);
|
const [agentToDelete, setAgentToDelete] = useState<Agent | null>(null);
|
||||||
@@ -195,12 +177,12 @@ export const CCAgents: React.FC<CCAgentsProps> = ({ onBack, className }) => {
|
|||||||
setToast({ message: "Agent updated successfully", type: "success" });
|
setToast({ message: "Agent updated successfully", type: "success" });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRunClick = (run: AgentRunWithMetrics) => {
|
// const handleRunClick = (run: AgentRunWithMetrics) => {
|
||||||
if (run.id) {
|
// if (run.id) {
|
||||||
setSelectedRunId(run.id);
|
// setSelectedRunId(run.id);
|
||||||
setView("viewRun");
|
// setView("viewRun");
|
||||||
}
|
// }
|
||||||
};
|
// };
|
||||||
|
|
||||||
const handleExecutionComplete = async () => {
|
const handleExecutionComplete = async () => {
|
||||||
// Reload runs when returning from execution
|
// Reload runs when returning from execution
|
||||||
@@ -270,7 +252,7 @@ export const CCAgents: React.FC<CCAgentsProps> = ({ onBack, className }) => {
|
|||||||
const paginatedAgents = agents.slice(startIndex, startIndex + AGENTS_PER_PAGE);
|
const paginatedAgents = agents.slice(startIndex, startIndex + AGENTS_PER_PAGE);
|
||||||
|
|
||||||
const renderIcon = (iconName: string) => {
|
const renderIcon = (iconName: string) => {
|
||||||
const Icon = AGENT_ICONS[iconName as AgentIconName] || Bot;
|
const Icon = AGENT_ICONS[iconName as AgentIconName] || AGENT_ICONS.bot;
|
||||||
return <Icon className="h-12 w-12" />;
|
return <Icon className="h-12 w-12" />;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -305,14 +287,7 @@ export const CCAgents: React.FC<CCAgentsProps> = ({ onBack, className }) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (view === "viewRun" && selectedRunId) {
|
// Removed viewRun case - now using modal preview in AgentRunsList
|
||||||
return (
|
|
||||||
<AgentRunView
|
|
||||||
runId={selectedRunId}
|
|
||||||
onBack={() => setView("list")}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn("flex flex-col h-full bg-background", className)}>
|
<div className={cn("flex flex-col h-full bg-background", className)}>
|
||||||
@@ -564,7 +539,9 @@ export const CCAgents: React.FC<CCAgentsProps> = ({ onBack, className }) => {
|
|||||||
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-primary"></div>
|
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-primary"></div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<AgentRunsList runs={runs} onRunClick={handleRunClick} />
|
<AgentRunsList
|
||||||
|
runs={runs}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import { ArrowLeft, Save, Loader2 } from "lucide-react";
|
import { ArrowLeft, Save, Loader2, ChevronDown } from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
@@ -8,8 +8,9 @@ import { Toast, ToastContainer } from "@/components/ui/toast";
|
|||||||
import { api, type Agent } from "@/lib/api";
|
import { api, type Agent } from "@/lib/api";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import MDEditor from "@uiw/react-md-editor";
|
import MDEditor from "@uiw/react-md-editor";
|
||||||
import { AGENT_ICONS, type AgentIconName } from "./CCAgents";
|
import { type AgentIconName } from "./CCAgents";
|
||||||
import { AgentSandboxSettings } from "./AgentSandboxSettings";
|
import { AgentSandboxSettings } from "./AgentSandboxSettings";
|
||||||
|
import { IconPicker, ICON_MAP } from "./IconPicker";
|
||||||
|
|
||||||
interface CreateAgentProps {
|
interface CreateAgentProps {
|
||||||
/**
|
/**
|
||||||
@@ -54,6 +55,7 @@ export const CreateAgent: React.FC<CreateAgentProps> = ({
|
|||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [toast, setToast] = useState<{ message: string; type: "success" | "error" } | null>(null);
|
const [toast, setToast] = useState<{ message: string; type: "success" | "error" } | null>(null);
|
||||||
|
const [showIconPicker, setShowIconPicker] = useState(false);
|
||||||
|
|
||||||
const isEditMode = !!agent;
|
const isEditMode = !!agent;
|
||||||
|
|
||||||
@@ -183,164 +185,172 @@ export const CreateAgent: React.FC<CreateAgentProps> = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Form */}
|
{/* Form */}
|
||||||
<div className="flex-1 p-4 overflow-y-auto">
|
<div className="flex-1 overflow-y-auto px-4 py-6">
|
||||||
<div className="space-y-6">
|
<motion.div
|
||||||
{/* Agent Name */}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
<div className="space-y-2">
|
animate={{ opacity: 1, y: 0 }}
|
||||||
<Label htmlFor="agent-name">Agent Name</Label>
|
transition={{ duration: 0.3, delay: 0.1 }}
|
||||||
<Input
|
className="space-y-6"
|
||||||
id="agent-name"
|
>
|
||||||
type="text"
|
{/* Basic Information */}
|
||||||
placeholder="e.g., Code Reviewer, Test Generator"
|
<div className="space-y-4">
|
||||||
value={name}
|
<div>
|
||||||
onChange={(e) => setName(e.target.value)}
|
<h3 className="text-sm font-medium mb-4">Basic Information</h3>
|
||||||
className="max-w-md"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Icon Picker */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label>Icon</Label>
|
|
||||||
<div className="grid grid-cols-3 sm:grid-cols-5 md:grid-cols-9 gap-2 max-w-2xl">
|
|
||||||
{(Object.keys(AGENT_ICONS) as AgentIconName[]).map((iconName) => {
|
|
||||||
const Icon = AGENT_ICONS[iconName];
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
key={iconName}
|
|
||||||
type="button"
|
|
||||||
onClick={() => setSelectedIcon(iconName)}
|
|
||||||
className={cn(
|
|
||||||
"p-3 rounded-lg border-2 transition-all hover:scale-105",
|
|
||||||
"flex items-center justify-center",
|
|
||||||
selectedIcon === iconName
|
|
||||||
? "border-primary bg-primary/10"
|
|
||||||
: "border-border hover:border-primary/50"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Icon className="h-6 w-6" />
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
{/* Name and Icon */}
|
||||||
{/* Model Selection */}
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>Model</Label>
|
<Label htmlFor="name">Agent Name</Label>
|
||||||
<div className="flex flex-col sm:flex-row gap-3">
|
<Input
|
||||||
<button
|
id="name"
|
||||||
type="button"
|
value={name}
|
||||||
onClick={() => setModel("sonnet")}
|
onChange={(e) => setName(e.target.value)}
|
||||||
className={cn(
|
placeholder="e.g., Code Assistant"
|
||||||
"flex-1 px-4 py-2.5 rounded-full border-2 font-medium transition-all",
|
required
|
||||||
"hover:scale-[1.02] active:scale-[0.98]",
|
/>
|
||||||
model === "sonnet"
|
</div>
|
||||||
? "border-primary bg-primary text-primary-foreground shadow-lg"
|
|
||||||
: "border-muted-foreground/30 hover:border-muted-foreground/50"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-center gap-2.5">
|
|
||||||
<div className={cn(
|
|
||||||
"w-4 h-4 rounded-full border-2 flex items-center justify-center flex-shrink-0",
|
|
||||||
model === "sonnet" ? "border-primary-foreground" : "border-current"
|
|
||||||
)}>
|
|
||||||
{model === "sonnet" && (
|
|
||||||
<div className="w-2 h-2 rounded-full bg-primary-foreground" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="text-left">
|
|
||||||
<div className="text-sm font-semibold">Claude 4 Sonnet</div>
|
|
||||||
<div className="text-xs opacity-80">Faster, efficient for most tasks</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
<div className="space-y-2">
|
||||||
type="button"
|
<Label>Agent Icon</Label>
|
||||||
onClick={() => setModel("opus")}
|
<div
|
||||||
className={cn(
|
onClick={() => setShowIconPicker(true)}
|
||||||
"flex-1 px-4 py-2.5 rounded-full border-2 font-medium transition-all",
|
className="h-10 px-3 py-2 bg-background border border-input rounded-md cursor-pointer hover:bg-accent hover:text-accent-foreground transition-colors flex items-center justify-between"
|
||||||
"hover:scale-[1.02] active:scale-[0.98]",
|
>
|
||||||
model === "opus"
|
<div className="flex items-center gap-2">
|
||||||
? "border-primary bg-primary text-primary-foreground shadow-lg"
|
{(() => {
|
||||||
: "border-muted-foreground/30 hover:border-muted-foreground/50"
|
const Icon = ICON_MAP[selectedIcon] || ICON_MAP.bot;
|
||||||
)}
|
return (
|
||||||
>
|
<>
|
||||||
<div className="flex items-center justify-center gap-2.5">
|
<Icon className="h-4 w-4" />
|
||||||
<div className={cn(
|
<span className="text-sm">{selectedIcon}</span>
|
||||||
"w-4 h-4 rounded-full border-2 flex items-center justify-center flex-shrink-0",
|
</>
|
||||||
model === "opus" ? "border-primary-foreground" : "border-current"
|
);
|
||||||
)}>
|
})()}
|
||||||
{model === "opus" && (
|
|
||||||
<div className="w-2 h-2 rounded-full bg-primary-foreground" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="text-left">
|
|
||||||
<div className="text-sm font-semibold">Claude 4 Opus</div>
|
|
||||||
<div className="text-xs opacity-80">More capable, better for complex tasks</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<ChevronDown className="h-4 w-4 text-muted-foreground" />
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Default Task */}
|
{/* Model Selection */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="default-task">Default Task (Optional)</Label>
|
<Label>Model</Label>
|
||||||
<Input
|
<div className="flex flex-col sm:flex-row gap-3">
|
||||||
id="default-task"
|
<button
|
||||||
type="text"
|
type="button"
|
||||||
placeholder="e.g., Review this code for security issues"
|
onClick={() => setModel("sonnet")}
|
||||||
value={defaultTask}
|
className={cn(
|
||||||
onChange={(e) => setDefaultTask(e.target.value)}
|
"flex-1 px-4 py-2.5 rounded-full border-2 font-medium transition-all",
|
||||||
className="max-w-md"
|
"hover:scale-[1.02] active:scale-[0.98]",
|
||||||
/>
|
model === "sonnet"
|
||||||
<p className="text-xs text-muted-foreground">
|
? "border-primary bg-primary text-primary-foreground shadow-lg"
|
||||||
This will be used as the default task placeholder when executing the agent
|
: "border-muted-foreground/30 hover:border-muted-foreground/50"
|
||||||
</p>
|
)}
|
||||||
</div>
|
>
|
||||||
|
<div className="flex items-center justify-center gap-2.5">
|
||||||
|
<div className={cn(
|
||||||
|
"w-4 h-4 rounded-full border-2 flex items-center justify-center flex-shrink-0",
|
||||||
|
model === "sonnet" ? "border-primary-foreground" : "border-current"
|
||||||
|
)}>
|
||||||
|
{model === "sonnet" && (
|
||||||
|
<div className="w-2 h-2 rounded-full bg-primary-foreground" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="text-left">
|
||||||
|
<div className="text-sm font-semibold">Claude 4 Sonnet</div>
|
||||||
|
<div className="text-xs opacity-80">Faster, efficient for most tasks</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setModel("opus")}
|
||||||
|
className={cn(
|
||||||
|
"flex-1 px-4 py-2.5 rounded-full border-2 font-medium transition-all",
|
||||||
|
"hover:scale-[1.02] active:scale-[0.98]",
|
||||||
|
model === "opus"
|
||||||
|
? "border-primary bg-primary text-primary-foreground shadow-lg"
|
||||||
|
: "border-muted-foreground/30 hover:border-muted-foreground/50"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-center gap-2.5">
|
||||||
|
<div className={cn(
|
||||||
|
"w-4 h-4 rounded-full border-2 flex items-center justify-center flex-shrink-0",
|
||||||
|
model === "opus" ? "border-primary-foreground" : "border-current"
|
||||||
|
)}>
|
||||||
|
{model === "opus" && (
|
||||||
|
<div className="w-2 h-2 rounded-full bg-primary-foreground" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="text-left">
|
||||||
|
<div className="text-sm font-semibold">Claude 4 Opus</div>
|
||||||
|
<div className="text-xs opacity-80">More capable, better for complex tasks</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Sandbox Settings */}
|
{/* Default Task */}
|
||||||
<AgentSandboxSettings
|
<div className="space-y-2">
|
||||||
agent={{
|
<Label htmlFor="default-task">Default Task (Optional)</Label>
|
||||||
id: agent?.id,
|
<Input
|
||||||
name,
|
id="default-task"
|
||||||
icon: selectedIcon,
|
type="text"
|
||||||
system_prompt: systemPrompt,
|
placeholder="e.g., Review this code for security issues"
|
||||||
default_task: defaultTask || undefined,
|
value={defaultTask}
|
||||||
model,
|
onChange={(e) => setDefaultTask(e.target.value)}
|
||||||
sandbox_enabled: sandboxEnabled,
|
className="max-w-md"
|
||||||
enable_file_read: enableFileRead,
|
|
||||||
enable_file_write: enableFileWrite,
|
|
||||||
enable_network: enableNetwork,
|
|
||||||
created_at: agent?.created_at || "",
|
|
||||||
updated_at: agent?.updated_at || ""
|
|
||||||
}}
|
|
||||||
onUpdate={(updates) => {
|
|
||||||
if ('sandbox_enabled' in updates) setSandboxEnabled(updates.sandbox_enabled!);
|
|
||||||
if ('enable_file_read' in updates) setEnableFileRead(updates.enable_file_read!);
|
|
||||||
if ('enable_file_write' in updates) setEnableFileWrite(updates.enable_file_write!);
|
|
||||||
if ('enable_network' in updates) setEnableNetwork(updates.enable_network!);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* System Prompt Editor */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label>System Prompt</Label>
|
|
||||||
<p className="text-xs text-muted-foreground mb-2">
|
|
||||||
Define the behavior and capabilities of your CC Agent
|
|
||||||
</p>
|
|
||||||
<div className="rounded-lg border border-border overflow-hidden shadow-sm" data-color-mode="dark">
|
|
||||||
<MDEditor
|
|
||||||
value={systemPrompt}
|
|
||||||
onChange={(val) => setSystemPrompt(val || "")}
|
|
||||||
preview="edit"
|
|
||||||
height={400}
|
|
||||||
visibleDragbar={false}
|
|
||||||
/>
|
/>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
This will be used as the default task placeholder when executing the agent
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Sandbox Settings */}
|
||||||
|
<AgentSandboxSettings
|
||||||
|
agent={{
|
||||||
|
id: agent?.id,
|
||||||
|
name,
|
||||||
|
icon: selectedIcon,
|
||||||
|
system_prompt: systemPrompt,
|
||||||
|
default_task: defaultTask || undefined,
|
||||||
|
model,
|
||||||
|
sandbox_enabled: sandboxEnabled,
|
||||||
|
enable_file_read: enableFileRead,
|
||||||
|
enable_file_write: enableFileWrite,
|
||||||
|
enable_network: enableNetwork,
|
||||||
|
created_at: agent?.created_at || "",
|
||||||
|
updated_at: agent?.updated_at || ""
|
||||||
|
}}
|
||||||
|
onUpdate={(updates) => {
|
||||||
|
if ('sandbox_enabled' in updates) setSandboxEnabled(updates.sandbox_enabled!);
|
||||||
|
if ('enable_file_read' in updates) setEnableFileRead(updates.enable_file_read!);
|
||||||
|
if ('enable_file_write' in updates) setEnableFileWrite(updates.enable_file_write!);
|
||||||
|
if ('enable_network' in updates) setEnableNetwork(updates.enable_network!);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* System Prompt Editor */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>System Prompt</Label>
|
||||||
|
<p className="text-xs text-muted-foreground mb-2">
|
||||||
|
Define the behavior and capabilities of your CC Agent
|
||||||
|
</p>
|
||||||
|
<div className="rounded-lg border border-border overflow-hidden shadow-sm" data-color-mode="dark">
|
||||||
|
<MDEditor
|
||||||
|
value={systemPrompt}
|
||||||
|
onChange={(val) => setSystemPrompt(val || "")}
|
||||||
|
preview="edit"
|
||||||
|
height={400}
|
||||||
|
visibleDragbar={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -354,6 +364,17 @@ export const CreateAgent: React.FC<CreateAgentProps> = ({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</ToastContainer>
|
</ToastContainer>
|
||||||
|
|
||||||
|
{/* Icon Picker Dialog */}
|
||||||
|
<IconPicker
|
||||||
|
value={selectedIcon}
|
||||||
|
onSelect={(iconName) => {
|
||||||
|
setSelectedIcon(iconName as AgentIconName);
|
||||||
|
setShowIconPicker(false);
|
||||||
|
}}
|
||||||
|
isOpen={showIconPicker}
|
||||||
|
onClose={() => setShowIconPicker(false)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
@@ -3,7 +3,6 @@ import { motion, AnimatePresence } from "framer-motion";
|
|||||||
import {
|
import {
|
||||||
Search,
|
Search,
|
||||||
Download,
|
Download,
|
||||||
X,
|
|
||||||
Loader2,
|
Loader2,
|
||||||
AlertCircle,
|
AlertCircle,
|
||||||
Eye,
|
Eye,
|
||||||
@@ -16,8 +15,10 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Card, CardContent, CardFooter } from "@/components/ui/card";
|
import { Card, CardContent, CardFooter } from "@/components/ui/card";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { api, type GitHubAgentFile, type AgentExport } from "@/lib/api";
|
import { api, type GitHubAgentFile, type AgentExport, type Agent } from "@/lib/api";
|
||||||
import { AGENT_ICONS, type AgentIconName } from "./CCAgents";
|
import { type AgentIconName } from "./CCAgents";
|
||||||
|
import { ICON_MAP } from "./IconPicker";
|
||||||
|
|
||||||
interface GitHubAgentBrowserProps {
|
interface GitHubAgentBrowserProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
@@ -42,14 +43,24 @@ export const GitHubAgentBrowser: React.FC<GitHubAgentBrowserProps> = ({
|
|||||||
const [searchQuery, setSearchQuery] = useState("");
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
const [selectedAgent, setSelectedAgent] = useState<AgentPreview | null>(null);
|
const [selectedAgent, setSelectedAgent] = useState<AgentPreview | null>(null);
|
||||||
const [importing, setImporting] = useState(false);
|
const [importing, setImporting] = useState(false);
|
||||||
const [importedAgents, setImportedAgents] = useState<Set<string>>(new Set());
|
const [existingAgents, setExistingAgents] = useState<Agent[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
fetchAgents();
|
fetchAgents();
|
||||||
|
fetchExistingAgents();
|
||||||
}
|
}
|
||||||
}, [isOpen]);
|
}, [isOpen]);
|
||||||
|
|
||||||
|
const fetchExistingAgents = async () => {
|
||||||
|
try {
|
||||||
|
const agents = await api.listAgents();
|
||||||
|
setExistingAgents(agents);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to fetch existing agents:", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const fetchAgents = async () => {
|
const fetchAgents = async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -91,6 +102,13 @@ export const GitHubAgentBrowser: React.FC<GitHubAgentBrowserProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isAgentImported = (fileName: string) => {
|
||||||
|
const agentName = getAgentDisplayName(fileName);
|
||||||
|
return existingAgents.some(agent =>
|
||||||
|
agent.name.toLowerCase() === agentName.toLowerCase()
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const handleImportAgent = async () => {
|
const handleImportAgent = async () => {
|
||||||
if (!selectedAgent?.file) return;
|
if (!selectedAgent?.file) return;
|
||||||
|
|
||||||
@@ -98,8 +116,8 @@ export const GitHubAgentBrowser: React.FC<GitHubAgentBrowserProps> = ({
|
|||||||
setImporting(true);
|
setImporting(true);
|
||||||
await api.importAgentFromGitHub(selectedAgent.file.download_url);
|
await api.importAgentFromGitHub(selectedAgent.file.download_url);
|
||||||
|
|
||||||
// Mark as imported
|
// Refresh existing agents list
|
||||||
setImportedAgents(prev => new Set(prev).add(selectedAgent.file.name));
|
await fetchExistingAgents();
|
||||||
|
|
||||||
// Close preview
|
// Close preview
|
||||||
setSelectedAgent(null);
|
setSelectedAgent(null);
|
||||||
@@ -126,7 +144,7 @@ export const GitHubAgentBrowser: React.FC<GitHubAgentBrowserProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const renderIcon = (iconName: string) => {
|
const renderIcon = (iconName: string) => {
|
||||||
const Icon = AGENT_ICONS[iconName as AgentIconName] || AGENT_ICONS.bot;
|
const Icon = ICON_MAP[iconName as AgentIconName] || ICON_MAP.bot;
|
||||||
return <Icon className="h-8 w-8" />;
|
return <Icon className="h-8 w-8" />;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -141,6 +159,25 @@ export const GitHubAgentBrowser: React.FC<GitHubAgentBrowserProps> = ({
|
|||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div className="flex-1 overflow-hidden flex flex-col">
|
<div className="flex-1 overflow-hidden flex flex-col">
|
||||||
|
{/* Repository Info */}
|
||||||
|
<div className="px-4 py-3 bg-muted/50 rounded-lg mb-4">
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Agents are fetched from{" "}
|
||||||
|
<a
|
||||||
|
href="https://github.com/getAsterisk/claudia/tree/main/cc_agents"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-primary hover:underline inline-flex items-center gap-1"
|
||||||
|
>
|
||||||
|
github.com/getAsterisk/claudia/cc_agents
|
||||||
|
<Globe className="h-3 w-3" />
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
|
You can contribute your custom agents to the repository!
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Search Bar */}
|
{/* Search Bar */}
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
@@ -190,11 +227,20 @@ export const GitHubAgentBrowser: React.FC<GitHubAgentBrowserProps> = ({
|
|||||||
onClick={() => handlePreviewAgent(agent)}>
|
onClick={() => handlePreviewAgent(agent)}>
|
||||||
<CardContent className="p-4">
|
<CardContent className="p-4">
|
||||||
<div className="flex items-start justify-between mb-3">
|
<div className="flex items-start justify-between mb-3">
|
||||||
<h3 className="text-sm font-semibold line-clamp-2">
|
<div className="flex items-center gap-3 flex-1">
|
||||||
{getAgentDisplayName(agent.name)}
|
<div className="p-2 rounded-lg bg-primary/10 text-primary flex-shrink-0">
|
||||||
</h3>
|
{/* Default to bot icon for now, will be loaded from preview */}
|
||||||
{importedAgents.has(agent.name) && (
|
{(() => {
|
||||||
<Badge variant="secondary" className="ml-2">
|
const Icon = ICON_MAP.bot;
|
||||||
|
return <Icon className="h-6 w-6" />;
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
<h3 className="text-sm font-semibold line-clamp-2">
|
||||||
|
{getAgentDisplayName(agent.name)}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
{isAgentImported(agent.name) && (
|
||||||
|
<Badge variant="secondary" className="ml-2 flex-shrink-0">
|
||||||
<Check className="h-3 w-3 mr-1" />
|
<Check className="h-3 w-3 mr-1" />
|
||||||
Imported
|
Imported
|
||||||
</Badge>
|
</Badge>
|
||||||
@@ -234,17 +280,7 @@ export const GitHubAgentBrowser: React.FC<GitHubAgentBrowserProps> = ({
|
|||||||
<Dialog open={!!selectedAgent} onOpenChange={() => setSelectedAgent(null)}>
|
<Dialog open={!!selectedAgent} onOpenChange={() => setSelectedAgent(null)}>
|
||||||
<DialogContent className="max-w-2xl max-h-[80vh] overflow-hidden flex flex-col">
|
<DialogContent className="max-w-2xl max-h-[80vh] overflow-hidden flex flex-col">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="flex items-center justify-between">
|
<DialogTitle>Agent Preview</DialogTitle>
|
||||||
<span>Agent Preview</span>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
onClick={() => setSelectedAgent(null)}
|
|
||||||
className="h-8 w-8"
|
|
||||||
>
|
|
||||||
<X className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</DialogTitle>
|
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div className="flex-1 overflow-y-auto">
|
<div className="flex-1 overflow-y-auto">
|
||||||
@@ -333,14 +369,14 @@ export const GitHubAgentBrowser: React.FC<GitHubAgentBrowserProps> = ({
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleImportAgent}
|
onClick={handleImportAgent}
|
||||||
disabled={importing || importedAgents.has(selectedAgent.file.name)}
|
disabled={importing || isAgentImported(selectedAgent.file.name)}
|
||||||
>
|
>
|
||||||
{importing ? (
|
{importing ? (
|
||||||
<>
|
<>
|
||||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||||
Importing...
|
Importing...
|
||||||
</>
|
</>
|
||||||
) : importedAgents.has(selectedAgent.file.name) ? (
|
) : isAgentImported(selectedAgent.file.name) ? (
|
||||||
<>
|
<>
|
||||||
<Check className="h-4 w-4 mr-2" />
|
<Check className="h-4 w-4 mr-2" />
|
||||||
Already Imported
|
Already Imported
|
||||||
|
463
src/components/IconPicker.tsx
Normal file
463
src/components/IconPicker.tsx
Normal file
@@ -0,0 +1,463 @@
|
|||||||
|
import React, { useState, useMemo } from "react";
|
||||||
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
|
import {
|
||||||
|
// Interface & Navigation
|
||||||
|
Home,
|
||||||
|
Menu,
|
||||||
|
Settings,
|
||||||
|
User,
|
||||||
|
Users,
|
||||||
|
LogOut,
|
||||||
|
Bell,
|
||||||
|
Bookmark,
|
||||||
|
Calendar,
|
||||||
|
Clock,
|
||||||
|
Eye,
|
||||||
|
EyeOff,
|
||||||
|
Hash,
|
||||||
|
Heart,
|
||||||
|
Info,
|
||||||
|
Link,
|
||||||
|
Lock,
|
||||||
|
Map,
|
||||||
|
MessageSquare,
|
||||||
|
Mic,
|
||||||
|
Music,
|
||||||
|
Paperclip,
|
||||||
|
Phone,
|
||||||
|
Pin,
|
||||||
|
Plus,
|
||||||
|
Save,
|
||||||
|
Share,
|
||||||
|
Star,
|
||||||
|
Tag,
|
||||||
|
Trash,
|
||||||
|
Upload,
|
||||||
|
Download,
|
||||||
|
Edit,
|
||||||
|
Copy,
|
||||||
|
// Development & Tech
|
||||||
|
Bot,
|
||||||
|
Brain,
|
||||||
|
Code,
|
||||||
|
Terminal,
|
||||||
|
Cpu,
|
||||||
|
Database,
|
||||||
|
GitBranch,
|
||||||
|
Github,
|
||||||
|
Globe,
|
||||||
|
HardDrive,
|
||||||
|
Laptop,
|
||||||
|
Monitor,
|
||||||
|
Server,
|
||||||
|
Wifi,
|
||||||
|
Cloud,
|
||||||
|
Command,
|
||||||
|
FileCode,
|
||||||
|
FileJson,
|
||||||
|
Folder,
|
||||||
|
FolderOpen,
|
||||||
|
Bug,
|
||||||
|
Coffee,
|
||||||
|
// Business & Finance
|
||||||
|
Briefcase,
|
||||||
|
Building,
|
||||||
|
CreditCard,
|
||||||
|
DollarSign,
|
||||||
|
TrendingUp,
|
||||||
|
TrendingDown,
|
||||||
|
BarChart,
|
||||||
|
PieChart,
|
||||||
|
Calculator,
|
||||||
|
Receipt,
|
||||||
|
Wallet,
|
||||||
|
// Creative & Design
|
||||||
|
Palette,
|
||||||
|
Brush,
|
||||||
|
Camera,
|
||||||
|
Film,
|
||||||
|
Image,
|
||||||
|
Layers,
|
||||||
|
Layout,
|
||||||
|
PenTool,
|
||||||
|
Scissors,
|
||||||
|
Type,
|
||||||
|
Zap,
|
||||||
|
Sparkles,
|
||||||
|
Wand2,
|
||||||
|
// Nature & Science
|
||||||
|
Beaker,
|
||||||
|
Atom,
|
||||||
|
Dna,
|
||||||
|
Flame,
|
||||||
|
Leaf,
|
||||||
|
Mountain,
|
||||||
|
Sun,
|
||||||
|
Moon,
|
||||||
|
CloudRain,
|
||||||
|
Snowflake,
|
||||||
|
TreePine,
|
||||||
|
Waves,
|
||||||
|
Wind,
|
||||||
|
// Gaming & Entertainment
|
||||||
|
Gamepad2,
|
||||||
|
Dice1,
|
||||||
|
Trophy,
|
||||||
|
Medal,
|
||||||
|
Crown,
|
||||||
|
Rocket,
|
||||||
|
Target,
|
||||||
|
Swords,
|
||||||
|
Shield,
|
||||||
|
// Communication
|
||||||
|
Mail,
|
||||||
|
Send,
|
||||||
|
MessageCircle,
|
||||||
|
Video,
|
||||||
|
Voicemail,
|
||||||
|
Radio,
|
||||||
|
Podcast,
|
||||||
|
Megaphone,
|
||||||
|
// Miscellaneous
|
||||||
|
Activity,
|
||||||
|
Anchor,
|
||||||
|
Award,
|
||||||
|
Battery,
|
||||||
|
Bluetooth,
|
||||||
|
Compass,
|
||||||
|
Crosshair,
|
||||||
|
Flag,
|
||||||
|
Flashlight,
|
||||||
|
Gift,
|
||||||
|
Headphones,
|
||||||
|
Key,
|
||||||
|
Lightbulb,
|
||||||
|
Package,
|
||||||
|
Puzzle,
|
||||||
|
Search as SearchIcon,
|
||||||
|
Smile,
|
||||||
|
ThumbsUp,
|
||||||
|
Umbrella,
|
||||||
|
Watch,
|
||||||
|
Wrench,
|
||||||
|
type LucideIcon,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Icon categories for better organization
|
||||||
|
*/
|
||||||
|
const ICON_CATEGORIES = {
|
||||||
|
"Interface & Navigation": [
|
||||||
|
{ name: "home", icon: Home },
|
||||||
|
{ name: "menu", icon: Menu },
|
||||||
|
{ name: "settings", icon: Settings },
|
||||||
|
{ name: "user", icon: User },
|
||||||
|
{ name: "users", icon: Users },
|
||||||
|
{ name: "log-out", icon: LogOut },
|
||||||
|
{ name: "bell", icon: Bell },
|
||||||
|
{ name: "bookmark", icon: Bookmark },
|
||||||
|
{ name: "calendar", icon: Calendar },
|
||||||
|
{ name: "clock", icon: Clock },
|
||||||
|
{ name: "eye", icon: Eye },
|
||||||
|
{ name: "eye-off", icon: EyeOff },
|
||||||
|
{ name: "hash", icon: Hash },
|
||||||
|
{ name: "heart", icon: Heart },
|
||||||
|
{ name: "info", icon: Info },
|
||||||
|
{ name: "link", icon: Link },
|
||||||
|
{ name: "lock", icon: Lock },
|
||||||
|
{ name: "map", icon: Map },
|
||||||
|
{ name: "message-square", icon: MessageSquare },
|
||||||
|
{ name: "mic", icon: Mic },
|
||||||
|
{ name: "music", icon: Music },
|
||||||
|
{ name: "paperclip", icon: Paperclip },
|
||||||
|
{ name: "phone", icon: Phone },
|
||||||
|
{ name: "pin", icon: Pin },
|
||||||
|
{ name: "plus", icon: Plus },
|
||||||
|
{ name: "save", icon: Save },
|
||||||
|
{ name: "share", icon: Share },
|
||||||
|
{ name: "star", icon: Star },
|
||||||
|
{ name: "tag", icon: Tag },
|
||||||
|
{ name: "trash", icon: Trash },
|
||||||
|
{ name: "upload", icon: Upload },
|
||||||
|
{ name: "download", icon: Download },
|
||||||
|
{ name: "edit", icon: Edit },
|
||||||
|
{ name: "copy", icon: Copy },
|
||||||
|
],
|
||||||
|
"Development & Tech": [
|
||||||
|
{ name: "bot", icon: Bot },
|
||||||
|
{ name: "brain", icon: Brain },
|
||||||
|
{ name: "code", icon: Code },
|
||||||
|
{ name: "terminal", icon: Terminal },
|
||||||
|
{ name: "cpu", icon: Cpu },
|
||||||
|
{ name: "database", icon: Database },
|
||||||
|
{ name: "git-branch", icon: GitBranch },
|
||||||
|
{ name: "github", icon: Github },
|
||||||
|
{ name: "globe", icon: Globe },
|
||||||
|
{ name: "hard-drive", icon: HardDrive },
|
||||||
|
{ name: "laptop", icon: Laptop },
|
||||||
|
{ name: "monitor", icon: Monitor },
|
||||||
|
{ name: "server", icon: Server },
|
||||||
|
{ name: "wifi", icon: Wifi },
|
||||||
|
{ name: "cloud", icon: Cloud },
|
||||||
|
{ name: "command", icon: Command },
|
||||||
|
{ name: "file-code", icon: FileCode },
|
||||||
|
{ name: "file-json", icon: FileJson },
|
||||||
|
{ name: "folder", icon: Folder },
|
||||||
|
{ name: "folder-open", icon: FolderOpen },
|
||||||
|
{ name: "bug", icon: Bug },
|
||||||
|
{ name: "coffee", icon: Coffee },
|
||||||
|
],
|
||||||
|
"Business & Finance": [
|
||||||
|
{ name: "briefcase", icon: Briefcase },
|
||||||
|
{ name: "building", icon: Building },
|
||||||
|
{ name: "credit-card", icon: CreditCard },
|
||||||
|
{ name: "dollar-sign", icon: DollarSign },
|
||||||
|
{ name: "trending-up", icon: TrendingUp },
|
||||||
|
{ name: "trending-down", icon: TrendingDown },
|
||||||
|
{ name: "bar-chart", icon: BarChart },
|
||||||
|
{ name: "pie-chart", icon: PieChart },
|
||||||
|
{ name: "calculator", icon: Calculator },
|
||||||
|
{ name: "receipt", icon: Receipt },
|
||||||
|
{ name: "wallet", icon: Wallet },
|
||||||
|
],
|
||||||
|
"Creative & Design": [
|
||||||
|
{ name: "palette", icon: Palette },
|
||||||
|
{ name: "brush", icon: Brush },
|
||||||
|
{ name: "camera", icon: Camera },
|
||||||
|
{ name: "film", icon: Film },
|
||||||
|
{ name: "image", icon: Image },
|
||||||
|
{ name: "layers", icon: Layers },
|
||||||
|
{ name: "layout", icon: Layout },
|
||||||
|
{ name: "pen-tool", icon: PenTool },
|
||||||
|
{ name: "scissors", icon: Scissors },
|
||||||
|
{ name: "type", icon: Type },
|
||||||
|
{ name: "zap", icon: Zap },
|
||||||
|
{ name: "sparkles", icon: Sparkles },
|
||||||
|
{ name: "wand-2", icon: Wand2 },
|
||||||
|
],
|
||||||
|
"Nature & Science": [
|
||||||
|
{ name: "beaker", icon: Beaker },
|
||||||
|
{ name: "atom", icon: Atom },
|
||||||
|
{ name: "dna", icon: Dna },
|
||||||
|
{ name: "flame", icon: Flame },
|
||||||
|
{ name: "leaf", icon: Leaf },
|
||||||
|
{ name: "mountain", icon: Mountain },
|
||||||
|
{ name: "sun", icon: Sun },
|
||||||
|
{ name: "moon", icon: Moon },
|
||||||
|
{ name: "cloud-rain", icon: CloudRain },
|
||||||
|
{ name: "snowflake", icon: Snowflake },
|
||||||
|
{ name: "tree-pine", icon: TreePine },
|
||||||
|
{ name: "waves", icon: Waves },
|
||||||
|
{ name: "wind", icon: Wind },
|
||||||
|
],
|
||||||
|
"Gaming & Entertainment": [
|
||||||
|
{ name: "gamepad-2", icon: Gamepad2 },
|
||||||
|
{ name: "dice-1", icon: Dice1 },
|
||||||
|
{ name: "trophy", icon: Trophy },
|
||||||
|
{ name: "medal", icon: Medal },
|
||||||
|
{ name: "crown", icon: Crown },
|
||||||
|
{ name: "rocket", icon: Rocket },
|
||||||
|
{ name: "target", icon: Target },
|
||||||
|
{ name: "swords", icon: Swords },
|
||||||
|
{ name: "shield", icon: Shield },
|
||||||
|
],
|
||||||
|
"Communication": [
|
||||||
|
{ name: "mail", icon: Mail },
|
||||||
|
{ name: "send", icon: Send },
|
||||||
|
{ name: "message-circle", icon: MessageCircle },
|
||||||
|
{ name: "video", icon: Video },
|
||||||
|
{ name: "voicemail", icon: Voicemail },
|
||||||
|
{ name: "radio", icon: Radio },
|
||||||
|
{ name: "podcast", icon: Podcast },
|
||||||
|
{ name: "megaphone", icon: Megaphone },
|
||||||
|
],
|
||||||
|
"Miscellaneous": [
|
||||||
|
{ name: "activity", icon: Activity },
|
||||||
|
{ name: "anchor", icon: Anchor },
|
||||||
|
{ name: "award", icon: Award },
|
||||||
|
{ name: "battery", icon: Battery },
|
||||||
|
{ name: "bluetooth", icon: Bluetooth },
|
||||||
|
{ name: "compass", icon: Compass },
|
||||||
|
{ name: "crosshair", icon: Crosshair },
|
||||||
|
{ name: "flag", icon: Flag },
|
||||||
|
{ name: "flashlight", icon: Flashlight },
|
||||||
|
{ name: "gift", icon: Gift },
|
||||||
|
{ name: "headphones", icon: Headphones },
|
||||||
|
{ name: "key", icon: Key },
|
||||||
|
{ name: "lightbulb", icon: Lightbulb },
|
||||||
|
{ name: "package", icon: Package },
|
||||||
|
{ name: "puzzle", icon: Puzzle },
|
||||||
|
{ name: "search", icon: SearchIcon },
|
||||||
|
{ name: "smile", icon: Smile },
|
||||||
|
{ name: "thumbs-up", icon: ThumbsUp },
|
||||||
|
{ name: "umbrella", icon: Umbrella },
|
||||||
|
{ name: "watch", icon: Watch },
|
||||||
|
{ name: "wrench", icon: Wrench },
|
||||||
|
],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
type IconCategory = typeof ICON_CATEGORIES[keyof typeof ICON_CATEGORIES];
|
||||||
|
type IconItem = IconCategory[number];
|
||||||
|
|
||||||
|
interface IconPickerProps {
|
||||||
|
/**
|
||||||
|
* Currently selected icon name
|
||||||
|
*/
|
||||||
|
value: string;
|
||||||
|
/**
|
||||||
|
* Callback when an icon is selected
|
||||||
|
*/
|
||||||
|
onSelect: (iconName: string) => void;
|
||||||
|
/**
|
||||||
|
* Whether the picker is open
|
||||||
|
*/
|
||||||
|
isOpen: boolean;
|
||||||
|
/**
|
||||||
|
* Callback to close the picker
|
||||||
|
*/
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Icon picker component with search and categories
|
||||||
|
* Similar to Notion's icon picker interface
|
||||||
|
*/
|
||||||
|
export const IconPicker: React.FC<IconPickerProps> = ({
|
||||||
|
value,
|
||||||
|
onSelect,
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
}) => {
|
||||||
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
const [hoveredIcon, setHoveredIcon] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// Filter icons based on search query
|
||||||
|
const filteredCategories = useMemo(() => {
|
||||||
|
if (!searchQuery.trim()) return ICON_CATEGORIES;
|
||||||
|
|
||||||
|
const query = searchQuery.toLowerCase();
|
||||||
|
const filtered: Record<string, IconItem[]> = {};
|
||||||
|
|
||||||
|
Object.entries(ICON_CATEGORIES).forEach(([category, icons]) => {
|
||||||
|
const matchingIcons = icons.filter(({ name }) =>
|
||||||
|
name.toLowerCase().includes(query)
|
||||||
|
);
|
||||||
|
if (matchingIcons.length > 0) {
|
||||||
|
filtered[category] = matchingIcons;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return filtered;
|
||||||
|
}, [searchQuery]);
|
||||||
|
|
||||||
|
// Get all icons for search
|
||||||
|
const allIcons = useMemo(() => {
|
||||||
|
return Object.values(ICON_CATEGORIES).flat();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleSelect = (iconName: string) => {
|
||||||
|
onSelect(iconName);
|
||||||
|
onClose();
|
||||||
|
setSearchQuery("");
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||||
|
<DialogContent className="max-w-2xl max-h-[80vh] p-0">
|
||||||
|
<DialogHeader className="px-6 py-4 border-b">
|
||||||
|
<DialogTitle>Choose an icon</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
{/* Search Bar */}
|
||||||
|
<div className="px-6 py-3 border-b">
|
||||||
|
<div className="relative">
|
||||||
|
<SearchIcon className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||||
|
<Input
|
||||||
|
placeholder="Search icons..."
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
className="pl-10"
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Icon Grid */}
|
||||||
|
<div className="h-[60vh] px-6 py-4 overflow-y-auto">
|
||||||
|
{Object.keys(filteredCategories).length === 0 ? (
|
||||||
|
<div className="flex flex-col items-center justify-center h-32 text-center">
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
No icons found for "{searchQuery}"
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<AnimatePresence mode="wait">
|
||||||
|
{Object.entries(filteredCategories).map(([category, icons]) => (
|
||||||
|
<motion.div
|
||||||
|
key={category}
|
||||||
|
initial={{ opacity: 0, y: 10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: -10 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
>
|
||||||
|
<h3 className="text-sm font-medium text-muted-foreground mb-3">
|
||||||
|
{category}
|
||||||
|
</h3>
|
||||||
|
<div className="grid grid-cols-10 gap-2">
|
||||||
|
{icons.map((item: IconItem) => {
|
||||||
|
const Icon = item.icon;
|
||||||
|
return (
|
||||||
|
<motion.button
|
||||||
|
key={item.name}
|
||||||
|
whileHover={{ scale: 1.1 }}
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
onClick={() => handleSelect(item.name)}
|
||||||
|
onMouseEnter={() => setHoveredIcon(item.name)}
|
||||||
|
onMouseLeave={() => setHoveredIcon(null)}
|
||||||
|
className={cn(
|
||||||
|
"p-2.5 rounded-lg transition-colors relative group",
|
||||||
|
"hover:bg-accent hover:text-accent-foreground",
|
||||||
|
value === item.name && "bg-primary/10 text-primary"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Icon className="h-5 w-5" />
|
||||||
|
{hoveredIcon === item.name && (
|
||||||
|
<div className="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 px-2 py-1 bg-popover text-popover-foreground text-xs rounded shadow-lg whitespace-nowrap z-10">
|
||||||
|
{item.name}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</motion.button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<div className="px-6 py-3 border-t bg-muted/50">
|
||||||
|
<p className="text-xs text-muted-foreground text-center">
|
||||||
|
Click an icon to select • {allIcons.length} icons available
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Export all available icon names for type safety
|
||||||
|
export const AVAILABLE_ICONS = Object.values(ICON_CATEGORIES)
|
||||||
|
.flat()
|
||||||
|
.map(({ name }) => name);
|
||||||
|
|
||||||
|
// Export icon map for easy access
|
||||||
|
export const ICON_MAP = Object.values(ICON_CATEGORIES)
|
||||||
|
.flat()
|
||||||
|
.reduce((acc, { name, icon }) => ({ ...acc, [name]: icon }), {} as Record<string, LucideIcon>);
|
@@ -1,4 +1,29 @@
|
|||||||
export * from "./AgentExecutionDemo";
|
export * from "./AgentExecutionDemo";
|
||||||
|
export * from "./AgentRunOutputViewer";
|
||||||
export * from "./StreamMessage";
|
export * from "./StreamMessage";
|
||||||
export * from "./ToolWidgets";
|
export * from "./ToolWidgets";
|
||||||
export * from "./NFOCredits";
|
export * from "./NFOCredits";
|
||||||
|
export * from "./UsageDashboard";
|
||||||
|
export * from "./WebviewPreview";
|
||||||
|
export * from "./ImagePreview";
|
||||||
|
export * from "./MCPManager";
|
||||||
|
export * from "./MCPServerList";
|
||||||
|
export * from "./MCPAddServer";
|
||||||
|
export * from "./MCPImportExport";
|
||||||
|
export * from "./ui/badge";
|
||||||
|
export * from "./ui/button";
|
||||||
|
export * from "./ui/card";
|
||||||
|
export * from "./ui/dialog";
|
||||||
|
export * from "./ui/dropdown-menu";
|
||||||
|
export * from "./ui/input";
|
||||||
|
export * from "./ui/label";
|
||||||
|
export * from "./ui/select";
|
||||||
|
export * from "./ui/switch";
|
||||||
|
export * from "./ui/tabs";
|
||||||
|
export * from "./ui/textarea";
|
||||||
|
export * from "./ui/toast";
|
||||||
|
export * from "./ui/tooltip";
|
||||||
|
export * from "./ui/popover";
|
||||||
|
export * from "./ui/pagination";
|
||||||
|
export * from "./ui/split-pane";
|
||||||
|
export * from "./ui/scroll-area";
|
@@ -4,7 +4,7 @@ import { cva, type VariantProps } from "class-variance-authority"
|
|||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const badgeVariants = cva(
|
const badgeVariants = cva(
|
||||||
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors",
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
|
@@ -6,7 +6,7 @@ import { cn } from "@/lib/utils";
|
|||||||
* Button variants configuration using class-variance-authority
|
* Button variants configuration using class-variance-authority
|
||||||
*/
|
*/
|
||||||
const buttonVariants = cva(
|
const buttonVariants = cva(
|
||||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
|
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors disabled:pointer-events-none disabled:opacity-50",
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
|
@@ -41,7 +41,7 @@ const DialogContent = React.forwardRef<
|
|||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
||||||
<X className="h-4 w-4" />
|
<X className="h-4 w-4" />
|
||||||
<span className="sr-only">Close</span>
|
<span className="sr-only">Close</span>
|
||||||
</DialogPrimitive.Close>
|
</DialogPrimitive.Close>
|
||||||
|
46
src/components/ui/scroll-area.tsx
Normal file
46
src/components/ui/scroll-area.tsx
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
interface ScrollAreaProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
|
/**
|
||||||
|
* Optional className for styling
|
||||||
|
*/
|
||||||
|
className?: string;
|
||||||
|
/**
|
||||||
|
* Children to render inside the scroll area
|
||||||
|
*/
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ScrollArea component for scrollable content with custom scrollbar styling
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* <ScrollArea className="h-[200px]">
|
||||||
|
* <div>Scrollable content here</div>
|
||||||
|
* </ScrollArea>
|
||||||
|
*/
|
||||||
|
export const ScrollArea = React.forwardRef<HTMLDivElement, ScrollAreaProps>(
|
||||||
|
({ className, children, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative overflow-auto",
|
||||||
|
// Custom scrollbar styling
|
||||||
|
"scrollbar-thin scrollbar-thumb-border scrollbar-track-transparent",
|
||||||
|
"[&::-webkit-scrollbar]:w-2",
|
||||||
|
"[&::-webkit-scrollbar-track]:bg-transparent",
|
||||||
|
"[&::-webkit-scrollbar-thumb]:bg-border [&::-webkit-scrollbar-thumb]:rounded-full",
|
||||||
|
"[&::-webkit-scrollbar-thumb:hover]:bg-border/80",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
ScrollArea.displayName = "ScrollArea";
|
@@ -16,7 +16,7 @@ const SelectTrigger = React.forwardRef<
|
|||||||
<SelectPrimitive.Trigger
|
<SelectPrimitive.Trigger
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex h-9 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
"flex h-9 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -114,7 +114,7 @@ const SelectItem = React.forwardRef<
|
|||||||
<SelectPrimitive.Item
|
<SelectPrimitive.Item
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
@@ -174,7 +174,6 @@ export const SplitPane: React.FC<SplitPaneProps> = ({
|
|||||||
"w-1 hover:w-2 transition-all duration-150",
|
"w-1 hover:w-2 transition-all duration-150",
|
||||||
"bg-border hover:bg-primary/50",
|
"bg-border hover:bg-primary/50",
|
||||||
"cursor-col-resize",
|
"cursor-col-resize",
|
||||||
"focus:outline-none focus:bg-primary focus:w-2",
|
|
||||||
isDragging && "bg-primary w-2"
|
isDragging && "bg-primary w-2"
|
||||||
)}
|
)}
|
||||||
onMouseDown={handleMouseDown}
|
onMouseDown={handleMouseDown}
|
||||||
|
@@ -30,7 +30,6 @@ const Switch = React.forwardRef<HTMLInputElement, SwitchProps>(
|
|||||||
onClick={() => onCheckedChange?.(!checked)}
|
onClick={() => onCheckedChange?.(!checked)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors",
|
"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors",
|
||||||
"focus-visible:outline-none focus-visible:ring-2",
|
|
||||||
"disabled:cursor-not-allowed disabled:opacity-50",
|
"disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
|
@@ -106,7 +106,6 @@ const TabsTrigger = React.forwardRef<
|
|||||||
onClick={() => onValueChange(value)}
|
onClick={() => onValueChange(value)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium transition-all",
|
"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium transition-all",
|
||||||
"focus-visible:outline-none focus-visible:ring-2",
|
|
||||||
"disabled:pointer-events-none disabled:opacity-50",
|
"disabled:pointer-events-none disabled:opacity-50",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
@@ -145,7 +144,7 @@ const TabsContent = React.forwardRef<
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
role="tabpanel"
|
role="tabpanel"
|
||||||
className={cn(
|
className={cn(
|
||||||
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
"mt-2",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
@@ -80,6 +80,49 @@ button:disabled,
|
|||||||
cursor: not-allowed !important;
|
cursor: not-allowed !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Remove all focus styles globally */
|
||||||
|
* {
|
||||||
|
outline: none !important;
|
||||||
|
outline-offset: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
*:focus,
|
||||||
|
*:focus-visible,
|
||||||
|
*:focus-within {
|
||||||
|
outline: none !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Specifically remove focus styles from form elements */
|
||||||
|
input:focus,
|
||||||
|
input:focus-visible,
|
||||||
|
textarea:focus,
|
||||||
|
textarea:focus-visible,
|
||||||
|
select:focus,
|
||||||
|
select:focus-visible,
|
||||||
|
button:focus,
|
||||||
|
button:focus-visible,
|
||||||
|
[role="button"]:focus,
|
||||||
|
[role="button"]:focus-visible,
|
||||||
|
[role="combobox"]:focus,
|
||||||
|
[role="combobox"]:focus-visible {
|
||||||
|
outline: none !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
border-color: var(--color-input) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Remove ring styles */
|
||||||
|
.ring-0,
|
||||||
|
.ring-1,
|
||||||
|
.ring-2,
|
||||||
|
.ring,
|
||||||
|
.ring-offset-0,
|
||||||
|
.ring-offset-1,
|
||||||
|
.ring-offset-2,
|
||||||
|
.ring-offset {
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
/* Custom utilities */
|
/* Custom utilities */
|
||||||
@utility animate-in {
|
@utility animate-in {
|
||||||
animation-name: enter;
|
animation-name: enter;
|
||||||
@@ -134,6 +177,7 @@ button:disabled,
|
|||||||
.w-md-editor.w-md-editor-focus {
|
.w-md-editor.w-md-editor-focus {
|
||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
border-color: var(--color-border) !important;
|
border-color: var(--color-border) !important;
|
||||||
|
outline: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.w-md-editor-toolbar {
|
.w-md-editor-toolbar {
|
||||||
|
Reference in New Issue
Block a user