mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-11 02:33:51 +08:00
feat: 实现可折叠的模板面板,支持搜索和安装模板 feat: 更新流程工具栏,增加导入模板和模拟运行功能 feat: 增强属性面板,支持标签和产物管理 feat: 优化提示模板节点,增加执行状态和阶段显示
165 lines
5.6 KiB
TypeScript
165 lines
5.6 KiB
TypeScript
// ========================================
|
||
// 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;
|