diff --git a/ccw/docs-site/.gitignore b/ccw/docs-site/.gitignore new file mode 100644 index 00000000..b4a7d405 --- /dev/null +++ b/ccw/docs-site/.gitignore @@ -0,0 +1 @@ +.ace-tool/ diff --git a/ccw/frontend/src/pages/orchestrator/FlowToolbar.tsx b/ccw/frontend/src/pages/orchestrator/FlowToolbar.tsx index 710d0211..652403a4 100644 --- a/ccw/frontend/src/pages/orchestrator/FlowToolbar.tsx +++ b/ccw/frontend/src/pages/orchestrator/FlowToolbar.tsx @@ -1,12 +1,11 @@ // ======================================== // Flow Toolbar Component // ======================================== -// Toolbar for flow operations: New, Save, Load, Export +// Toolbar for flow operations: Save, Load, Import Template, Export, Simulate, Run import { useState, useCallback, useEffect } from 'react'; import { useIntl } from 'react-intl'; import { - Plus, Save, FolderOpen, Download, @@ -16,6 +15,8 @@ import { Loader2, ChevronDown, Library, + Play, + FlaskConical, } from 'lucide-react'; import { cn } from '@/lib/utils'; import { Button } from '@/components/ui/Button'; @@ -39,7 +40,6 @@ export function FlowToolbar({ className, onOpenTemplateLibrary }: FlowToolbarPro const isModified = useFlowStore((state) => state.isModified); const flows = useFlowStore((state) => state.flows); const isLoadingFlows = useFlowStore((state) => state.isLoadingFlows); - const createFlow = useFlowStore((state) => state.createFlow); const saveFlow = useFlowStore((state) => state.saveFlow); const loadFlow = useFlowStore((state) => state.loadFlow); const deleteFlow = useFlowStore((state) => state.deleteFlow); @@ -56,13 +56,6 @@ export function FlowToolbar({ className, onOpenTemplateLibrary }: FlowToolbarPro setFlowName(currentFlow?.name || ''); }, [currentFlow?.name]); - // Handle new flow - const handleNew = useCallback(() => { - const newFlow = createFlow('Untitled Flow', 'A new workflow'); - setFlowName(newFlow.name); - toast.success('Flow Created', 'New flow created successfully'); - }, [createFlow]); - // Handle save const handleSave = useCallback(async () => { if (!currentFlow) { @@ -186,11 +179,7 @@ export function FlowToolbar({ className, onOpenTemplateLibrary }: FlowToolbarPro {/* Action Buttons */}
- - + {/* Save & Load Group */} +
+ {/* Import & Export Group */} + +
+ + {/* Run Group */} + + +
); diff --git a/ccw/frontend/src/pages/orchestrator/InlineTemplatePanel.tsx b/ccw/frontend/src/pages/orchestrator/InlineTemplatePanel.tsx new file mode 100644 index 00000000..86eed3a7 --- /dev/null +++ b/ccw/frontend/src/pages/orchestrator/InlineTemplatePanel.tsx @@ -0,0 +1,164 @@ +// ======================================== +// Inline Template Panel Component +// ======================================== +// Compact template list for the left sidebar, uses useTemplates hook + +import { useState, useCallback, useMemo } from 'react'; +import { Search, Loader2, FileText, Download, GitBranch } from 'lucide-react'; +import { cn } from '@/lib/utils'; +import { Input } from '@/components/ui/Input'; +import { Badge } from '@/components/ui/Badge'; +import { useTemplates, useInstallTemplate } from '@/hooks/useTemplates'; +import { useFlowStore } from '@/stores'; +import type { FlowTemplate } from '@/types/execution'; + +// ========== Sub-Components ========== + +interface TemplateItemProps { + template: FlowTemplate; + onInstall: (template: FlowTemplate) => void; + isInstalling: boolean; +} + +function TemplateItem({ template, onInstall, isInstalling }: TemplateItemProps) { + return ( + + ); +} + +// ========== Main Component ========== + +interface InlineTemplatePanelProps { + className?: string; +} + +/** + * Compact template browser for the left sidebar. + * Loads templates via the useTemplates API hook and displays them in a searchable list. + * Clicking a template installs it as the current flow. + */ +export function InlineTemplatePanel({ className }: InlineTemplatePanelProps) { + const [searchQuery, setSearchQuery] = useState(''); + const [installingId, setInstallingId] = useState(null); + + const setCurrentFlow = useFlowStore((state) => state.setCurrentFlow); + + const { data, isLoading, error } = useTemplates(); + const installTemplate = useInstallTemplate(); + + // Filter templates by search query + const filteredTemplates = useMemo(() => { + if (!data?.templates) return []; + if (!searchQuery.trim()) return data.templates; + + const query = searchQuery.toLowerCase(); + return data.templates.filter( + (t) => + t.name.toLowerCase().includes(query) || + t.description?.toLowerCase().includes(query) || + t.category?.toLowerCase().includes(query) || + t.tags?.some((tag) => tag.toLowerCase().includes(query)) + ); + }, [data?.templates, searchQuery]); + + // Handle install - load template as current flow + const handleInstall = useCallback( + async (template: FlowTemplate) => { + setInstallingId(template.id); + try { + const result = await installTemplate.mutateAsync({ + templateId: template.id, + }); + setCurrentFlow(result.flow); + } catch (err) { + console.error('Failed to install template:', err); + } finally { + setInstallingId(null); + } + }, + [installTemplate, setCurrentFlow] + ); + + return ( +
+ {/* Search */} +
+
+ + setSearchQuery(e.target.value)} + placeholder="搜索模板..." + className="pl-8 h-8 text-sm" + /> +
+
+ + {/* Template List */} +
+ {isLoading ? ( +
+ +
+ ) : error ? ( +
+ +

+ 无法加载模板库,请确认 API 服务可用 +

+
+ ) : filteredTemplates.length === 0 ? ( +
+ +

+ {searchQuery ? '未找到匹配的模板' : '暂无可用模板'} +

+
+ ) : ( +
+ {filteredTemplates.map((template) => ( + + ))} +
+ )} +
+
+ ); +} + +export default InlineTemplatePanel; diff --git a/ccw/frontend/src/pages/orchestrator/LeftSidebar.tsx b/ccw/frontend/src/pages/orchestrator/LeftSidebar.tsx new file mode 100644 index 00000000..7502e0df --- /dev/null +++ b/ccw/frontend/src/pages/orchestrator/LeftSidebar.tsx @@ -0,0 +1,105 @@ +// ======================================== +// Left Sidebar Component +// ======================================== +// Container with tab switching between NodeLibrary and InlineTemplatePanel + +import { ChevronRight, ChevronDown } from 'lucide-react'; +import { cn } from '@/lib/utils'; +import { Button } from '@/components/ui/Button'; +import { useFlowStore } from '@/stores'; +import { NodeLibrary } from './NodeLibrary'; +import { InlineTemplatePanel } from './InlineTemplatePanel'; + +// ========== Tab Configuration ========== + +const TABS: Array<{ key: 'templates' | 'nodes'; label: string }> = [ + { key: 'templates', label: '\u6A21\u677F\u5E93' }, + { key: 'nodes', label: '\u8282\u70B9\u5E93' }, +]; + +// ========== Main Component ========== + +interface LeftSidebarProps { + className?: string; +} + +/** + * Left sidebar container with collapsible panel and tab switching. + * Renders either InlineTemplatePanel or NodeLibrary based on active tab. + */ +export function LeftSidebar({ className }: LeftSidebarProps) { + const isPaletteOpen = useFlowStore((state) => state.isPaletteOpen); + const setIsPaletteOpen = useFlowStore((state) => state.setIsPaletteOpen); + const leftPanelTab = useFlowStore((state) => state.leftPanelTab); + const setLeftPanelTab = useFlowStore((state) => state.setLeftPanelTab); + + // Collapsed state + if (!isPaletteOpen) { + return ( +
+ +
+ ); + } + + // Expanded state + return ( +
+ {/* Header */} +
+

工作台

+ +
+ + {/* Tab Bar */} +
+ {TABS.map((tab) => ( + + ))} +
+ + {/* Content */} + {leftPanelTab === 'templates' ? ( + + ) : ( + + )} + + {/* Footer */} +
+
+ Tip: 拖拽到画布或双击添加 +
+
+
+ ); +} + +export default LeftSidebar; diff --git a/ccw/frontend/src/pages/orchestrator/NodeLibrary.tsx b/ccw/frontend/src/pages/orchestrator/NodeLibrary.tsx new file mode 100644 index 00000000..2abf6997 --- /dev/null +++ b/ccw/frontend/src/pages/orchestrator/NodeLibrary.tsx @@ -0,0 +1,208 @@ +// ======================================== +// Node Library Component +// ======================================== +// Displays quick templates organized by category (phase / tool / command) +// Extracted from NodePalette for use inside LeftSidebar + +import { DragEvent, useState } from 'react'; +import { + MessageSquare, ChevronDown, ChevronRight, GripVertical, + Search, Code, Terminal, Plus, + FolderOpen, Database, ListTodo, Play, CheckCircle, + FolderSearch, GitMerge, ListChecks, +} from 'lucide-react'; +import { cn } from '@/lib/utils'; +import { useFlowStore } from '@/stores'; +import { NODE_TYPE_CONFIGS, QUICK_TEMPLATES } from '@/types/flow'; +import type { QuickTemplate } from '@/types/flow'; + +// ========== Icon Mapping ========== + +const TEMPLATE_ICONS: Record = { + // Command templates + 'slash-command-main': Terminal, + 'slash-command-async': Terminal, + analysis: Search, + implementation: Code, + // Phase templates + 'phase-session': FolderOpen, + 'phase-context': Database, + 'phase-plan': ListTodo, + 'phase-execute': Play, + 'phase-review': CheckCircle, + // Tool templates + 'tool-context-gather': FolderSearch, + 'tool-conflict-resolution': GitMerge, + 'tool-task-generate': ListChecks, +}; + +// ========== Category Configuration ========== + +const CATEGORY_CONFIG: Record = { + phase: { title: '\u9636\u6BB5\u8282\u70B9', defaultExpanded: true }, + tool: { title: '\u5DE5\u5177\u8282\u70B9', defaultExpanded: true }, + command: { title: '\u547D\u4EE4', defaultExpanded: false }, +}; + +const CATEGORY_ORDER: QuickTemplate['category'][] = ['phase', 'tool', 'command']; + +// ========== Sub-Components ========== + +/** + * Collapsible category section + */ +function TemplateCategory({ + title, + children, + defaultExpanded = true, +}: { + title: string; + children: React.ReactNode; + defaultExpanded?: boolean; +}) { + const [isExpanded, setIsExpanded] = useState(defaultExpanded); + + return ( +
+ + + {isExpanded &&
{children}
} +
+ ); +} + +/** + * Draggable card for a quick template + */ +function QuickTemplateCard({ + template, +}: { + template: QuickTemplate; +}) { + const Icon = TEMPLATE_ICONS[template.id] || MessageSquare; + + const onDragStart = (event: DragEvent) => { + event.dataTransfer.setData('application/reactflow-node-type', 'prompt-template'); + event.dataTransfer.setData('application/reactflow-template-id', template.id); + event.dataTransfer.effectAllowed = 'move'; + }; + + const onDoubleClick = () => { + const position = { x: 100 + Math.random() * 200, y: 100 + Math.random() * 200 }; + useFlowStore.getState().addNodeFromTemplate(template.id, position); + }; + + return ( +
+
+ +
+
+
{template.label}
+
{template.description}
+
+ +
+ ); +} + +/** + * Basic empty prompt template card + */ +function BasicTemplateCard() { + const config = NODE_TYPE_CONFIGS['prompt-template']; + + const onDragStart = (event: DragEvent) => { + event.dataTransfer.setData('application/reactflow-node-type', 'prompt-template'); + event.dataTransfer.effectAllowed = 'move'; + }; + + const onDoubleClick = () => { + const position = { x: 100 + Math.random() * 200, y: 100 + Math.random() * 200 }; + useFlowStore.getState().addNode(position); + }; + + return ( +
+
+ +
+
+
{config.label}
+
{config.description}
+
+ +
+ ); +} + +// ========== Main Component ========== + +interface NodeLibraryProps { + className?: string; +} + +/** + * Node library panel displaying quick templates grouped by category. + * Renders a scrollable list of template cards organized into collapsible sections. + * Used inside LeftSidebar - does not manage its own header/footer/collapse state. + */ +export function NodeLibrary({ className }: NodeLibraryProps) { + return ( +
+ {/* Basic / Empty Template */} + + + + + {/* Category groups in order: phase -> tool -> command */} + {CATEGORY_ORDER.map((category) => { + const config = CATEGORY_CONFIG[category]; + const templates = QUICK_TEMPLATES.filter((t) => t.category === category); + if (templates.length === 0) return null; + + return ( + + {templates.map((template) => ( + + ))} + + ); + })} +
+ ); +} + +export default NodeLibrary; diff --git a/ccw/frontend/src/pages/orchestrator/OrchestratorPage.tsx b/ccw/frontend/src/pages/orchestrator/OrchestratorPage.tsx index 35cd2303..a7283b17 100644 --- a/ccw/frontend/src/pages/orchestrator/OrchestratorPage.tsx +++ b/ccw/frontend/src/pages/orchestrator/OrchestratorPage.tsx @@ -6,7 +6,7 @@ import { useEffect, useState, useCallback } from 'react'; import { useFlowStore } from '@/stores'; import { FlowCanvas } from './FlowCanvas'; -import { NodePalette } from './NodePalette'; +import { LeftSidebar } from './LeftSidebar'; import { PropertyPanel } from './PropertyPanel'; import { FlowToolbar } from './FlowToolbar'; import { TemplateLibrary } from './TemplateLibrary'; @@ -32,8 +32,8 @@ export function OrchestratorPage() { {/* Main Content Area */}
- {/* Node Palette (Left) */} - + {/* Left Sidebar (Templates + Nodes) */} + {/* Flow Canvas (Center) */}
diff --git a/ccw/frontend/src/pages/orchestrator/PropertyPanel.tsx b/ccw/frontend/src/pages/orchestrator/PropertyPanel.tsx index 2963245f..5dfafc29 100644 --- a/ccw/frontend/src/pages/orchestrator/PropertyPanel.tsx +++ b/ccw/frontend/src/pages/orchestrator/PropertyPanel.tsx @@ -5,7 +5,7 @@ import { useCallback, useMemo, useState, useEffect, useRef, KeyboardEvent } from 'react'; import { useIntl } from 'react-intl'; -import { Settings, X, MessageSquare, Trash2, AlertCircle, CheckCircle2, Plus, Save } from 'lucide-react'; +import { Settings, X, MessageSquare, Trash2, AlertCircle, CheckCircle2, Plus, Save, Copy, ChevronDown, ChevronRight } from 'lucide-react'; import { cn } from '@/lib/utils'; import { Button } from '@/components/ui/Button'; import { Input } from '@/components/ui/Input'; @@ -786,6 +786,176 @@ function SlashCommandSection({ data, onChange, availableVariables }: SlashComman ); } +// ========== Collapsible Section ========== + +function CollapsibleSection({ + title, + defaultExpanded = false, + children, +}: { + title: string; + defaultExpanded?: boolean; + children: React.ReactNode; +}) { + const [isExpanded, setIsExpanded] = useState(defaultExpanded); + return ( +
+ + {isExpanded &&
{children}
} +
+ ); +} + +// ========== Tags Input ========== + +function TagsInput({ tags, onChange }: { tags: string[]; onChange: (tags: string[]) => void }) { + const [input, setInput] = useState(''); + + const handleAdd = () => { + if (input.trim() && !tags.includes(input.trim())) { + onChange([...tags, input.trim()]); + setInput(''); + } + }; + + const handleRemove = (tag: string) => { + onChange(tags.filter(t => t !== tag)); + }; + + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Enter') { + e.preventDefault(); + handleAdd(); + } + }; + + return ( +
+
+ {tags.map((tag) => ( + + {tag} + + + ))} +
+
+ setInput(e.target.value)} + onKeyDown={handleKeyDown} + placeholder="添加标签..." + className="h-7 text-xs" + /> + +
+
+ ); +} + +// ========== Artifacts List ========== + +function ArtifactsList({ artifacts, onChange }: { artifacts: string[]; onChange: (artifacts: string[]) => void }) { + const [input, setInput] = useState(''); + + const handleAdd = () => { + if (input.trim()) { + onChange([...artifacts, input.trim()]); + setInput(''); + } + }; + + const handleRemove = (index: number) => { + onChange(artifacts.filter((_, i) => i !== index)); + }; + + return ( +
+ {artifacts.map((artifact, i) => ( +
+ {'->'} + {artifact} + +
+ ))} +
+ setInput(e.target.value)} + onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); handleAdd(); } }} + placeholder="output-file.json" + className="h-7 text-xs font-mono" + /> + +
+
+ ); +} + +// ========== Script Preview ========== + +function ScriptPreview({ data }: { data: PromptTemplateNodeData }) { + const script = useMemo(() => { + // Slash command mode + if (data.slashCommand) { + const args = data.slashArgs ? ` ${data.slashArgs}` : ''; + return `/${data.slashCommand}${args}`; + } + + // CLI tool mode + if (data.tool && (data.mode === 'analysis' || data.mode === 'write')) { + const parts = ['ccw cli']; + parts.push(`--tool ${data.tool}`); + parts.push(`--mode ${data.mode}`); + if (data.instruction) { + const snippet = data.instruction.slice(0, 80).replace(/\n/g, ' '); + parts.push(`-p "${snippet}..."`); + } + return parts.join(' \\\n '); + } + + // Plain instruction + if (data.instruction) { + return `# ${data.instruction.slice(0, 100)}`; + } + + return '# 未配置命令'; + }, [data.slashCommand, data.slashArgs, data.tool, data.mode, data.instruction]); + + const handleCopy = useCallback(() => { + navigator.clipboard.writeText(script); + }, [script]); + + return ( +
+
+        {script}
+      
+ +
+ ); +} + // ========== Unified PromptTemplate Property Editor ========== interface PromptTemplatePropertiesProps { @@ -867,6 +1037,75 @@ function PromptTemplateProperties({ data, onChange }: PromptTemplatePropertiesPr />
)} + + {/* Classification Section */} + + {/* Description */} +
+ +