Files
Claude-Code-Workflow/ccw/frontend/src/pages/orchestrator/InlineTemplatePanel.tsx
catlog22 248daa1d00 feat: 添加左侧面板和节点库组件,整合模板和节点功能
feat: 实现可折叠的模板面板,支持搜索和安装模板
feat: 更新流程工具栏,增加导入模板和模拟运行功能
feat: 增强属性面板,支持标签和产物管理
feat: 优化提示模板节点,增加执行状态和阶段显示
2026-02-06 11:56:48 +08:00

165 lines
5.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ========================================
// 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 (
<button
onClick={() => onInstall(template)}
disabled={isInstalling}
className={cn(
'w-full flex items-center gap-3 px-3 py-2.5 rounded-md text-left transition-colors',
'hover:bg-muted/60 active:bg-muted',
isInstalling && 'opacity-50 cursor-wait'
)}
>
<div className="flex-1 min-w-0">
<div className="text-sm font-medium text-foreground truncate">
{template.name}
</div>
<div className="flex items-center gap-2 mt-0.5">
<span className="text-xs text-muted-foreground flex items-center gap-1">
<GitBranch className="w-3 h-3" />
{template.nodeCount} nodes
</span>
{template.category && (
<Badge variant="secondary" className="text-[10px] px-1.5 py-0">
{template.category}
</Badge>
)}
</div>
</div>
{isInstalling ? (
<Loader2 className="w-4 h-4 animate-spin text-muted-foreground shrink-0" />
) : (
<Download className="w-4 h-4 text-muted-foreground shrink-0 opacity-0 group-hover:opacity-100" />
)}
</button>
);
}
// ========== 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<string | null>(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 (
<div className={cn('flex-1 flex flex-col overflow-hidden', className)}>
{/* Search */}
<div className="px-3 py-2">
<div className="relative">
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" />
<Input
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="搜索模板..."
className="pl-8 h-8 text-sm"
/>
</div>
</div>
{/* Template List */}
<div className="flex-1 overflow-y-auto px-1">
{isLoading ? (
<div className="flex items-center justify-center py-12">
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
</div>
) : error ? (
<div className="flex flex-col items-center justify-center py-12 text-muted-foreground px-4">
<FileText className="h-8 w-8 mb-2 opacity-50" />
<p className="text-xs text-center">
API
</p>
</div>
) : filteredTemplates.length === 0 ? (
<div className="flex flex-col items-center justify-center py-12 text-muted-foreground px-4">
<FileText className="h-8 w-8 mb-2 opacity-50" />
<p className="text-xs text-center">
{searchQuery ? '未找到匹配的模板' : '暂无可用模板'}
</p>
</div>
) : (
<div className="space-y-0.5">
{filteredTemplates.map((template) => (
<TemplateItem
key={template.id}
template={template}
onInstall={handleInstall}
isInstalling={installingId === template.id}
/>
))}
</div>
)}
</div>
</div>
);
}
export default InlineTemplatePanel;