Skip to content

Commit 248daa1

Browse files
author
catlog22
committed
feat: 添加左侧面板和节点库组件,整合模板和节点功能
feat: 实现可折叠的模板面板,支持搜索和安装模板 feat: 更新流程工具栏,增加导入模板和模拟运行功能 feat: 增强属性面板,支持标签和产物管理 feat: 优化提示模板节点,增加执行状态和阶段显示
1 parent c8f9bc7 commit 248daa1

9 files changed

Lines changed: 927 additions & 104 deletions

File tree

ccw/docs-site/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
.ace-tool/

ccw/frontend/src/pages/orchestrator/FlowToolbar.tsx

Lines changed: 23 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
// ========================================
22
// Flow Toolbar Component
33
// ========================================
4-
// Toolbar for flow operations: New, Save, Load, Export
4+
// Toolbar for flow operations: Save, Load, Import Template, Export, Simulate, Run
55

66
import { useState, useCallback, useEffect } from 'react';
77
import { useIntl } from 'react-intl';
88
import {
9-
Plus,
109
Save,
1110
FolderOpen,
1211
Download,
@@ -16,6 +15,8 @@ import {
1615
Loader2,
1716
ChevronDown,
1817
Library,
18+
Play,
19+
FlaskConical,
1920
} from 'lucide-react';
2021
import { cn } from '@/lib/utils';
2122
import { Button } from '@/components/ui/Button';
@@ -39,7 +40,6 @@ export function FlowToolbar({ className, onOpenTemplateLibrary }: FlowToolbarPro
3940
const isModified = useFlowStore((state) => state.isModified);
4041
const flows = useFlowStore((state) => state.flows);
4142
const isLoadingFlows = useFlowStore((state) => state.isLoadingFlows);
42-
const createFlow = useFlowStore((state) => state.createFlow);
4343
const saveFlow = useFlowStore((state) => state.saveFlow);
4444
const loadFlow = useFlowStore((state) => state.loadFlow);
4545
const deleteFlow = useFlowStore((state) => state.deleteFlow);
@@ -56,13 +56,6 @@ export function FlowToolbar({ className, onOpenTemplateLibrary }: FlowToolbarPro
5656
setFlowName(currentFlow?.name || '');
5757
}, [currentFlow?.name]);
5858

59-
// Handle new flow
60-
const handleNew = useCallback(() => {
61-
const newFlow = createFlow('Untitled Flow', 'A new workflow');
62-
setFlowName(newFlow.name);
63-
toast.success('Flow Created', 'New flow created successfully');
64-
}, [createFlow]);
65-
6659
// Handle save
6760
const handleSave = useCallback(async () => {
6861
if (!currentFlow) {
@@ -186,11 +179,7 @@ export function FlowToolbar({ className, onOpenTemplateLibrary }: FlowToolbarPro
186179

187180
{/* Action Buttons */}
188181
<div className="flex items-center gap-2">
189-
<Button variant="outline" size="sm" onClick={handleNew}>
190-
<Plus className="w-4 h-4 mr-1" />
191-
New
192-
</Button>
193-
182+
{/* Save & Load Group */}
194183
<Button
195184
variant="outline"
196185
size="sm"
@@ -290,17 +279,31 @@ export function FlowToolbar({ className, onOpenTemplateLibrary }: FlowToolbarPro
290279
)}
291280
</div>
292281

293-
<Button variant="outline" size="sm" onClick={handleExport} disabled={!currentFlow}>
294-
<Download className="w-4 h-4 mr-1" />
295-
Export
296-
</Button>
282+
<div className="w-px h-6 bg-border" />
297283

284+
{/* Import & Export Group */}
298285
<Button variant="outline" size="sm" onClick={onOpenTemplateLibrary}>
299286
<Library className="w-4 h-4 mr-1" />
300-
Templates
287+
Import Template
288+
</Button>
289+
290+
<Button variant="outline" size="sm" onClick={handleExport} disabled={!currentFlow}>
291+
<Download className="w-4 h-4 mr-1" />
292+
Export Flow
301293
</Button>
302294

303295
<div className="w-px h-6 bg-border" />
296+
297+
{/* Run Group */}
298+
<Button variant="outline" size="sm" disabled title="Coming soon">
299+
<FlaskConical className="w-4 h-4 mr-1" />
300+
Simulate
301+
</Button>
302+
303+
<Button variant="default" size="sm" disabled title="Coming soon">
304+
<Play className="w-4 h-4 mr-1" />
305+
Run Workflow
306+
</Button>
304307
</div>
305308
</div>
306309
);
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
// ========================================
2+
// Inline Template Panel Component
3+
// ========================================
4+
// Compact template list for the left sidebar, uses useTemplates hook
5+
6+
import { useState, useCallback, useMemo } from 'react';
7+
import { Search, Loader2, FileText, Download, GitBranch } from 'lucide-react';
8+
import { cn } from '@/lib/utils';
9+
import { Input } from '@/components/ui/Input';
10+
import { Badge } from '@/components/ui/Badge';
11+
import { useTemplates, useInstallTemplate } from '@/hooks/useTemplates';
12+
import { useFlowStore } from '@/stores';
13+
import type { FlowTemplate } from '@/types/execution';
14+
15+
// ========== Sub-Components ==========
16+
17+
interface TemplateItemProps {
18+
template: FlowTemplate;
19+
onInstall: (template: FlowTemplate) => void;
20+
isInstalling: boolean;
21+
}
22+
23+
function TemplateItem({ template, onInstall, isInstalling }: TemplateItemProps) {
24+
return (
25+
<button
26+
onClick={() => onInstall(template)}
27+
disabled={isInstalling}
28+
className={cn(
29+
'w-full flex items-center gap-3 px-3 py-2.5 rounded-md text-left transition-colors',
30+
'hover:bg-muted/60 active:bg-muted',
31+
isInstalling && 'opacity-50 cursor-wait'
32+
)}
33+
>
34+
<div className="flex-1 min-w-0">
35+
<div className="text-sm font-medium text-foreground truncate">
36+
{template.name}
37+
</div>
38+
<div className="flex items-center gap-2 mt-0.5">
39+
<span className="text-xs text-muted-foreground flex items-center gap-1">
40+
<GitBranch className="w-3 h-3" />
41+
{template.nodeCount} nodes
42+
</span>
43+
{template.category && (
44+
<Badge variant="secondary" className="text-[10px] px-1.5 py-0">
45+
{template.category}
46+
</Badge>
47+
)}
48+
</div>
49+
</div>
50+
{isInstalling ? (
51+
<Loader2 className="w-4 h-4 animate-spin text-muted-foreground shrink-0" />
52+
) : (
53+
<Download className="w-4 h-4 text-muted-foreground shrink-0 opacity-0 group-hover:opacity-100" />
54+
)}
55+
</button>
56+
);
57+
}
58+
59+
// ========== Main Component ==========
60+
61+
interface InlineTemplatePanelProps {
62+
className?: string;
63+
}
64+
65+
/**
66+
* Compact template browser for the left sidebar.
67+
* Loads templates via the useTemplates API hook and displays them in a searchable list.
68+
* Clicking a template installs it as the current flow.
69+
*/
70+
export function InlineTemplatePanel({ className }: InlineTemplatePanelProps) {
71+
const [searchQuery, setSearchQuery] = useState('');
72+
const [installingId, setInstallingId] = useState<string | null>(null);
73+
74+
const setCurrentFlow = useFlowStore((state) => state.setCurrentFlow);
75+
76+
const { data, isLoading, error } = useTemplates();
77+
const installTemplate = useInstallTemplate();
78+
79+
// Filter templates by search query
80+
const filteredTemplates = useMemo(() => {
81+
if (!data?.templates) return [];
82+
if (!searchQuery.trim()) return data.templates;
83+
84+
const query = searchQuery.toLowerCase();
85+
return data.templates.filter(
86+
(t) =>
87+
t.name.toLowerCase().includes(query) ||
88+
t.description?.toLowerCase().includes(query) ||
89+
t.category?.toLowerCase().includes(query) ||
90+
t.tags?.some((tag) => tag.toLowerCase().includes(query))
91+
);
92+
}, [data?.templates, searchQuery]);
93+
94+
// Handle install - load template as current flow
95+
const handleInstall = useCallback(
96+
async (template: FlowTemplate) => {
97+
setInstallingId(template.id);
98+
try {
99+
const result = await installTemplate.mutateAsync({
100+
templateId: template.id,
101+
});
102+
setCurrentFlow(result.flow);
103+
} catch (err) {
104+
console.error('Failed to install template:', err);
105+
} finally {
106+
setInstallingId(null);
107+
}
108+
},
109+
[installTemplate, setCurrentFlow]
110+
);
111+
112+
return (
113+
<div className={cn('flex-1 flex flex-col overflow-hidden', className)}>
114+
{/* Search */}
115+
<div className="px-3 py-2">
116+
<div className="relative">
117+
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" />
118+
<Input
119+
value={searchQuery}
120+
onChange={(e) => setSearchQuery(e.target.value)}
121+
placeholder="搜索模板..."
122+
className="pl-8 h-8 text-sm"
123+
/>
124+
</div>
125+
</div>
126+
127+
{/* Template List */}
128+
<div className="flex-1 overflow-y-auto px-1">
129+
{isLoading ? (
130+
<div className="flex items-center justify-center py-12">
131+
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
132+
</div>
133+
) : error ? (
134+
<div className="flex flex-col items-center justify-center py-12 text-muted-foreground px-4">
135+
<FileText className="h-8 w-8 mb-2 opacity-50" />
136+
<p className="text-xs text-center">
137+
无法加载模板库,请确认 API 服务可用
138+
</p>
139+
</div>
140+
) : filteredTemplates.length === 0 ? (
141+
<div className="flex flex-col items-center justify-center py-12 text-muted-foreground px-4">
142+
<FileText className="h-8 w-8 mb-2 opacity-50" />
143+
<p className="text-xs text-center">
144+
{searchQuery ? '未找到匹配的模板' : '暂无可用模板'}
145+
</p>
146+
</div>
147+
) : (
148+
<div className="space-y-0.5">
149+
{filteredTemplates.map((template) => (
150+
<TemplateItem
151+
key={template.id}
152+
template={template}
153+
onInstall={handleInstall}
154+
isInstalling={installingId === template.id}
155+
/>
156+
))}
157+
</div>
158+
)}
159+
</div>
160+
</div>
161+
);
162+
}
163+
164+
export default InlineTemplatePanel;
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
// ========================================
2+
// Left Sidebar Component
3+
// ========================================
4+
// Container with tab switching between NodeLibrary and InlineTemplatePanel
5+
6+
import { ChevronRight, ChevronDown } from 'lucide-react';
7+
import { cn } from '@/lib/utils';
8+
import { Button } from '@/components/ui/Button';
9+
import { useFlowStore } from '@/stores';
10+
import { NodeLibrary } from './NodeLibrary';
11+
import { InlineTemplatePanel } from './InlineTemplatePanel';
12+
13+
// ========== Tab Configuration ==========
14+
15+
const TABS: Array<{ key: 'templates' | 'nodes'; label: string }> = [
16+
{ key: 'templates', label: '\u6A21\u677F\u5E93' },
17+
{ key: 'nodes', label: '\u8282\u70B9\u5E93' },
18+
];
19+
20+
// ========== Main Component ==========
21+
22+
interface LeftSidebarProps {
23+
className?: string;
24+
}
25+
26+
/**
27+
* Left sidebar container with collapsible panel and tab switching.
28+
* Renders either InlineTemplatePanel or NodeLibrary based on active tab.
29+
*/
30+
export function LeftSidebar({ className }: LeftSidebarProps) {
31+
const isPaletteOpen = useFlowStore((state) => state.isPaletteOpen);
32+
const setIsPaletteOpen = useFlowStore((state) => state.setIsPaletteOpen);
33+
const leftPanelTab = useFlowStore((state) => state.leftPanelTab);
34+
const setLeftPanelTab = useFlowStore((state) => state.setLeftPanelTab);
35+
36+
// Collapsed state
37+
if (!isPaletteOpen) {
38+
return (
39+
<div className={cn('w-10 bg-card border-r border-border flex flex-col items-center py-4', className)}>
40+
<Button
41+
variant="ghost"
42+
size="icon"
43+
onClick={() => setIsPaletteOpen(true)}
44+
title="展开面板"
45+
>
46+
<ChevronRight className="w-4 h-4" />
47+
</Button>
48+
</div>
49+
);
50+
}
51+
52+
// Expanded state
53+
return (
54+
<div className={cn('w-72 bg-card border-r border-border flex flex-col', className)}>
55+
{/* Header */}
56+
<div className="flex items-center justify-between px-4 py-3 border-b border-border">
57+
<h3 className="font-semibold text-foreground">工作台</h3>
58+
<Button
59+
variant="ghost"
60+
size="icon"
61+
className="h-6 w-6"
62+
onClick={() => setIsPaletteOpen(false)}
63+
title="折叠面板"
64+
>
65+
<ChevronDown className="w-4 h-4" />
66+
</Button>
67+
</div>
68+
69+
{/* Tab Bar */}
70+
<div className="flex border-b border-border">
71+
{TABS.map((tab) => (
72+
<button
73+
key={tab.key}
74+
onClick={() => setLeftPanelTab(tab.key)}
75+
className={cn(
76+
'flex-1 px-3 py-2 text-sm font-medium text-center transition-colors',
77+
'hover:text-foreground',
78+
leftPanelTab === tab.key
79+
? 'text-foreground border-b-2 border-primary'
80+
: 'text-muted-foreground'
81+
)}
82+
>
83+
{tab.label}
84+
</button>
85+
))}
86+
</div>
87+
88+
{/* Content */}
89+
{leftPanelTab === 'templates' ? (
90+
<InlineTemplatePanel />
91+
) : (
92+
<NodeLibrary />
93+
)}
94+
95+
{/* Footer */}
96+
<div className="px-4 py-3 border-t border-border bg-muted/30">
97+
<div className="text-xs text-muted-foreground">
98+
<span className="font-medium">Tip:</span> 拖拽到画布或双击添加
99+
</div>
100+
</div>
101+
</div>
102+
);
103+
}
104+
105+
export default LeftSidebar;

0 commit comments

Comments
 (0)