mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-14 02:42:04 +08:00
- Added Phase 6: Fix Discovery & Batching with intelligent grouping and batching of findings. - Added Phase 7: Fix Parallel Planning to launch planning agents for concurrent analysis and aggregation of partial plans. - Added Phase 8: Fix Execution for stage-based execution of fixes with conservative test verification. - Added Phase 9: Fix Completion to aggregate results, generate summary reports, and handle session completion. - Introduced new frontend components: ResizeHandle for draggable resizing of sidebar panels and useResizablePanel hook for managing panel sizes with localStorage persistence. - Added PowerShell script for checking TypeScript errors in source code, excluding test files.
357 lines
12 KiB
TypeScript
357 lines
12 KiB
TypeScript
// ========================================
|
|
// Node Library Component
|
|
// ========================================
|
|
// Displays built-in and custom node templates
|
|
// Supports creating, saving, and deleting custom templates with color selection
|
|
|
|
import { DragEvent, useState } from 'react';
|
|
import { useIntl } from 'react-intl';
|
|
import {
|
|
MessageSquare, ChevronDown, ChevronRight, GripVertical,
|
|
Terminal, Plus, Trash2, X,
|
|
} from 'lucide-react';
|
|
import { cn } from '@/lib/utils';
|
|
import { useFlowStore } from '@/stores';
|
|
import { QUICK_TEMPLATES } from '@/types/flow';
|
|
import type { QuickTemplate } from '@/types/flow';
|
|
|
|
// ========== Icon Mapping ==========
|
|
|
|
const TEMPLATE_ICONS: Record<string, React.ElementType> = {
|
|
'slash-command-main': Terminal,
|
|
'slash-command-async': Terminal,
|
|
};
|
|
|
|
// ========== I18n Key Mapping for Built-in Templates ==========
|
|
|
|
const TEMPLATE_I18N: Record<string, { labelKey: string; descKey: string }> = {
|
|
'prompt-template': {
|
|
labelKey: 'orchestrator.nodeLibrary.promptTemplateLabel',
|
|
descKey: 'orchestrator.nodeLibrary.promptTemplateDesc',
|
|
},
|
|
'slash-command-main': {
|
|
labelKey: 'orchestrator.nodeLibrary.slashCommandLabel',
|
|
descKey: 'orchestrator.nodeLibrary.slashCommandDesc',
|
|
},
|
|
'slash-command-async': {
|
|
labelKey: 'orchestrator.nodeLibrary.slashCommandAsyncLabel',
|
|
descKey: 'orchestrator.nodeLibrary.slashCommandAsyncDesc',
|
|
},
|
|
};
|
|
|
|
// ========== Color Palette for custom templates ==========
|
|
|
|
const COLOR_OPTIONS = [
|
|
{ value: 'bg-blue-500', label: 'Blue' },
|
|
{ value: 'bg-green-500', label: 'Green' },
|
|
{ value: 'bg-purple-500', label: 'Purple' },
|
|
{ value: 'bg-rose-500', label: 'Rose' },
|
|
{ value: 'bg-amber-500', label: 'Amber' },
|
|
{ value: 'bg-cyan-500', label: 'Cyan' },
|
|
{ value: 'bg-teal-500', label: 'Teal' },
|
|
{ value: 'bg-orange-500', label: 'Orange' },
|
|
{ value: 'bg-indigo-500', label: 'Indigo' },
|
|
{ value: 'bg-pink-500', label: 'Pink' },
|
|
];
|
|
|
|
// ========== Sub-Components ==========
|
|
|
|
/**
|
|
* Collapsible category section with optional action button
|
|
*/
|
|
function TemplateCategory({
|
|
title,
|
|
children,
|
|
defaultExpanded = true,
|
|
action,
|
|
}: {
|
|
title: string;
|
|
children: React.ReactNode;
|
|
defaultExpanded?: boolean;
|
|
action?: React.ReactNode;
|
|
}) {
|
|
const [isExpanded, setIsExpanded] = useState(defaultExpanded);
|
|
|
|
return (
|
|
<div>
|
|
<div className="flex items-center gap-1 mb-2">
|
|
<button
|
|
onClick={() => setIsExpanded(!isExpanded)}
|
|
className="flex items-center gap-2 flex-1 text-left text-xs font-semibold text-muted-foreground uppercase tracking-wider hover:text-foreground transition-colors"
|
|
>
|
|
{isExpanded ? (
|
|
<ChevronDown className="w-3 h-3" />
|
|
) : (
|
|
<ChevronRight className="w-3 h-3" />
|
|
)}
|
|
{title}
|
|
</button>
|
|
{action}
|
|
</div>
|
|
|
|
{isExpanded && <div className="space-y-2">{children}</div>}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Draggable card for a quick template
|
|
*/
|
|
function QuickTemplateCard({
|
|
template,
|
|
onDelete,
|
|
}: {
|
|
template: QuickTemplate;
|
|
onDelete?: () => void;
|
|
}) {
|
|
const { formatMessage } = useIntl();
|
|
const Icon = TEMPLATE_ICONS[template.id] || MessageSquare;
|
|
const i18n = TEMPLATE_I18N[template.id];
|
|
const displayLabel = i18n ? formatMessage({ id: i18n.labelKey }) : template.label;
|
|
const displayDesc = i18n ? formatMessage({ id: i18n.descKey }) : template.description;
|
|
|
|
const onDragStart = (event: DragEvent<HTMLDivElement>) => {
|
|
event.dataTransfer.setData('application/reactflow-node-type', 'prompt-template');
|
|
event.dataTransfer.setData('application/reactflow-template-id', template.id);
|
|
event.dataTransfer.effectAllowed = 'move';
|
|
};
|
|
|
|
const onDoubleClick = () => {
|
|
useFlowStore.getState().addNodeFromTemplate(template.id, { x: 250, y: 200 });
|
|
};
|
|
|
|
return (
|
|
<div
|
|
draggable
|
|
onDragStart={onDragStart}
|
|
onDoubleClick={onDoubleClick}
|
|
className={cn(
|
|
'group flex items-center gap-3 p-3 rounded-lg border bg-card cursor-grab transition-all',
|
|
'hover:shadow-md hover:scale-[1.02] active:cursor-grabbing active:scale-[0.98]',
|
|
)}
|
|
>
|
|
<div className={cn('p-2 rounded-md text-white shrink-0', template.color)}>
|
|
<Icon className="w-4 h-4" />
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<div className="text-sm font-medium text-foreground">{displayLabel}</div>
|
|
<div className="text-xs text-muted-foreground truncate">{displayDesc}</div>
|
|
</div>
|
|
{onDelete ? (
|
|
<button
|
|
onClick={(e) => { e.stopPropagation(); onDelete(); }}
|
|
className="w-4 h-4 text-muted-foreground opacity-0 group-hover:opacity-100 transition-opacity hover:text-destructive"
|
|
title={formatMessage({ id: 'orchestrator.nodeLibrary.deleteTemplate' })}
|
|
>
|
|
<Trash2 className="w-4 h-4" />
|
|
</button>
|
|
) : (
|
|
<GripVertical className="w-4 h-4 text-muted-foreground opacity-0 group-hover:opacity-100 transition-opacity" />
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Basic empty prompt template card
|
|
*/
|
|
function BasicTemplateCard() {
|
|
const { formatMessage } = useIntl();
|
|
const i18n = TEMPLATE_I18N['prompt-template'];
|
|
|
|
const onDragStart = (event: DragEvent<HTMLDivElement>) => {
|
|
event.dataTransfer.setData('application/reactflow-node-type', 'prompt-template');
|
|
event.dataTransfer.effectAllowed = 'move';
|
|
};
|
|
|
|
const onDoubleClick = () => {
|
|
useFlowStore.getState().addNode({ x: 250, y: 200 });
|
|
};
|
|
|
|
return (
|
|
<div
|
|
draggable
|
|
onDragStart={onDragStart}
|
|
onDoubleClick={onDoubleClick}
|
|
className={cn(
|
|
'group flex items-center gap-3 p-3 rounded-lg border bg-card cursor-grab transition-all',
|
|
'hover:shadow-md hover:scale-[1.02] active:cursor-grabbing active:scale-[0.98]',
|
|
'border-dashed border-muted-foreground/50 hover:border-primary',
|
|
)}
|
|
>
|
|
<div className="p-2 rounded-md text-white bg-blue-500 hover:bg-blue-600">
|
|
<Plus className="w-4 h-4" />
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<div className="text-sm font-medium text-foreground">{formatMessage({ id: i18n.labelKey })}</div>
|
|
<div className="text-xs text-muted-foreground truncate">{formatMessage({ id: i18n.descKey })}</div>
|
|
</div>
|
|
<GripVertical className="w-4 h-4 text-muted-foreground opacity-0 group-hover:opacity-100 transition-opacity" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Inline form for creating a new custom template
|
|
*/
|
|
function CreateTemplateForm({ onClose }: { onClose: () => void }) {
|
|
const { formatMessage } = useIntl();
|
|
const [label, setLabel] = useState('');
|
|
const [description, setDescription] = useState('');
|
|
const [instruction, setInstruction] = useState('');
|
|
const [color, setColor] = useState('bg-blue-500');
|
|
const addCustomTemplate = useFlowStore((s) => s.addCustomTemplate);
|
|
|
|
const handleSubmit = () => {
|
|
if (!label.trim()) return;
|
|
|
|
const template: QuickTemplate = {
|
|
id: `custom-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
label: label.trim(),
|
|
description: description.trim() || label.trim(),
|
|
icon: 'MessageSquare',
|
|
color,
|
|
category: 'command',
|
|
data: {
|
|
label: label.trim(),
|
|
instruction: instruction.trim(),
|
|
contextRefs: [],
|
|
},
|
|
};
|
|
|
|
addCustomTemplate(template);
|
|
onClose();
|
|
};
|
|
|
|
return (
|
|
<div className="p-3 rounded-lg border border-primary/50 bg-muted/50 space-y-3">
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-xs font-medium text-foreground">{formatMessage({ id: 'orchestrator.nodeLibrary.newCustomNode' })}</span>
|
|
<button onClick={onClose} className="text-muted-foreground hover:text-foreground">
|
|
<X className="w-3.5 h-3.5" />
|
|
</button>
|
|
</div>
|
|
|
|
<input
|
|
type="text"
|
|
placeholder={formatMessage({ id: 'orchestrator.nodeLibrary.nodeName' })}
|
|
value={label}
|
|
onChange={(e) => setLabel(e.target.value)}
|
|
className="w-full text-sm px-2 py-1.5 rounded border border-border bg-background text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-primary"
|
|
autoFocus
|
|
/>
|
|
|
|
<input
|
|
type="text"
|
|
placeholder={formatMessage({ id: 'orchestrator.nodeLibrary.descriptionOptional' })}
|
|
value={description}
|
|
onChange={(e) => setDescription(e.target.value)}
|
|
className="w-full text-sm px-2 py-1.5 rounded border border-border bg-background text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-primary"
|
|
/>
|
|
|
|
<textarea
|
|
placeholder={formatMessage({ id: 'orchestrator.nodeLibrary.defaultInstructionOptional' })}
|
|
value={instruction}
|
|
onChange={(e) => setInstruction(e.target.value)}
|
|
rows={2}
|
|
className="w-full text-sm px-2 py-1.5 rounded border border-border bg-background text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-primary resize-none"
|
|
/>
|
|
|
|
{/* Color picker */}
|
|
<div>
|
|
<div className="text-xs text-muted-foreground mb-1.5">{formatMessage({ id: 'orchestrator.nodeLibrary.color' })}</div>
|
|
<div className="flex flex-wrap gap-1.5">
|
|
{COLOR_OPTIONS.map((opt) => (
|
|
<button
|
|
key={opt.value}
|
|
onClick={() => setColor(opt.value)}
|
|
className={cn(
|
|
'w-6 h-6 rounded-full transition-all',
|
|
opt.value,
|
|
color === opt.value
|
|
? 'ring-2 ring-offset-2 ring-offset-background ring-primary scale-110'
|
|
: 'hover:scale-110',
|
|
)}
|
|
title={opt.label}
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<button
|
|
onClick={handleSubmit}
|
|
disabled={!label.trim()}
|
|
className={cn(
|
|
'w-full text-sm font-medium py-1.5 rounded transition-colors',
|
|
label.trim()
|
|
? 'bg-primary text-primary-foreground hover:bg-primary/90'
|
|
: 'bg-muted text-muted-foreground cursor-not-allowed',
|
|
)}
|
|
>
|
|
{formatMessage({ id: 'orchestrator.nodeLibrary.save' })}
|
|
</button>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// ========== Main Component ==========
|
|
|
|
interface NodeLibraryProps {
|
|
className?: string;
|
|
}
|
|
|
|
/**
|
|
* Node library panel displaying built-in and custom node templates.
|
|
* Built-in: Slash Command, Slash Command (Async), Prompt Template
|
|
* Custom: User-created templates persisted to localStorage
|
|
*/
|
|
export function NodeLibrary({ className }: NodeLibraryProps) {
|
|
const { formatMessage } = useIntl();
|
|
const [isCreating, setIsCreating] = useState(false);
|
|
const customTemplates = useFlowStore((s) => s.customTemplates);
|
|
const removeCustomTemplate = useFlowStore((s) => s.removeCustomTemplate);
|
|
|
|
return (
|
|
<div className={cn('flex-1 overflow-y-auto p-4 space-y-4', className)}>
|
|
{/* Built-in templates */}
|
|
<TemplateCategory title={formatMessage({ id: 'orchestrator.nodeLibrary.builtIn' })} defaultExpanded>
|
|
<BasicTemplateCard />
|
|
{QUICK_TEMPLATES.map((template) => (
|
|
<QuickTemplateCard key={template.id} template={template} />
|
|
))}
|
|
</TemplateCategory>
|
|
|
|
{/* Custom templates */}
|
|
<TemplateCategory
|
|
title={formatMessage({ id: 'orchestrator.nodeLibrary.custom' }, { count: customTemplates.length })}
|
|
defaultExpanded
|
|
action={
|
|
<button
|
|
onClick={() => setIsCreating(true)}
|
|
className="p-0.5 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors"
|
|
title={formatMessage({ id: 'orchestrator.nodeLibrary.createCustomNode' })}
|
|
>
|
|
<Plus className="w-3.5 h-3.5" />
|
|
</button>
|
|
}
|
|
>
|
|
{isCreating && <CreateTemplateForm onClose={() => setIsCreating(false)} />}
|
|
{customTemplates.map((template) => (
|
|
<QuickTemplateCard
|
|
key={template.id}
|
|
template={template}
|
|
onDelete={() => removeCustomTemplate(template.id)}
|
|
/>
|
|
))}
|
|
{customTemplates.length === 0 && !isCreating && (
|
|
<div className="text-xs text-muted-foreground text-center py-3">
|
|
{formatMessage({ id: 'orchestrator.nodeLibrary.noCustomNodes' })}
|
|
</div>
|
|
)}
|
|
</TemplateCategory>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default NodeLibrary;
|