feat: add Chinese localization and new assets for CCW documentation

- Created LICENSE.txt for JavaScript assets including NProgress and React libraries.
- Added runtime JavaScript file for main functionality.
- Introduced new favicon and logo SVG assets for branding.
- Added comprehensive FAQ section in Chinese, covering CCW features, installation, workflows, AI model support, and troubleshooting.
This commit is contained in:
catlog22
2026-02-06 21:56:02 +08:00
parent 9b1655be9b
commit 6a5c17e42e
126 changed files with 3363 additions and 734 deletions

View File

@@ -4,6 +4,7 @@
// Right-side slide-out panel for real-time execution monitoring
import { useEffect, useRef, useCallback, useState } from 'react';
import { useIntl } from 'react-intl';
import {
Play,
Pause,
@@ -97,6 +98,7 @@ export function ExecutionMonitor({ className }: ExecutionMonitorProps) {
const logsEndRef = useRef<HTMLDivElement>(null);
const logsContainerRef = useRef<HTMLDivElement>(null);
const [isUserScrolling, setIsUserScrolling] = useState(false);
const { formatMessage } = useIntl();
// Execution store state
const currentExecution = useExecutionStore((state) => state.currentExecution);
@@ -215,7 +217,7 @@ export function ExecutionMonitor({ className }: ExecutionMonitorProps) {
return (
<div
className={cn(
'w-80 border-l border-border bg-card flex flex-col h-full',
'w-[50%] border-l border-border bg-card flex flex-col h-full',
'animate-in slide-in-from-right duration-300',
className
)}
@@ -224,12 +226,12 @@ export function ExecutionMonitor({ className }: ExecutionMonitorProps) {
<div className="flex items-center justify-between px-3 py-2 border-b border-border shrink-0">
<div className="flex items-center gap-2 min-w-0">
<Terminal className="h-4 w-4 text-muted-foreground shrink-0" />
<span className="text-sm font-medium truncate">Monitor</span>
<span className="text-sm font-medium truncate">{formatMessage({ id: 'orchestrator.monitor.title' })}</span>
{currentExecution && (
<Badge variant={getStatusBadgeVariant(currentExecution.status)} className="shrink-0">
<span className="flex items-center gap-1">
{getStatusIcon(currentExecution.status)}
{currentExecution.status}
{formatMessage({ id: `orchestrator.status.${currentExecution.status}` })}
</span>
</Badge>
)}
@@ -255,7 +257,7 @@ export function ExecutionMonitor({ className }: ExecutionMonitorProps) {
className="flex-1"
>
<Play className="h-4 w-4 mr-1" />
Execute
{formatMessage({ id: 'orchestrator.actions.execute' })}
</Button>
)}
@@ -290,7 +292,7 @@ export function ExecutionMonitor({ className }: ExecutionMonitorProps) {
className="flex-1"
>
<Play className="h-4 w-4 mr-1" />
Resume
{formatMessage({ id: 'orchestrator.execution.resume' })}
</Button>
<Button
size="sm"
@@ -325,7 +327,7 @@ export function ExecutionMonitor({ className }: ExecutionMonitorProps) {
{currentExecution && Object.keys(nodeStates).length > 0 && (
<div className="px-3 py-2 border-b border-border shrink-0">
<div className="text-xs font-medium text-muted-foreground mb-1.5">
Node Status ({completedNodes}/{totalNodes})
{formatMessage({ id: 'orchestrator.node.statusCount' }, { completed: completedNodes, total: totalNodes })}
</div>
<div className="space-y-1 max-h-32 overflow-y-auto">
{Object.entries(nodeStates).map(([nodeId, state]) => (
@@ -364,8 +366,8 @@ export function ExecutionMonitor({ className }: ExecutionMonitorProps) {
{logs.length === 0 ? (
<div className="flex items-center justify-center h-full text-muted-foreground text-center">
{currentExecution
? 'Waiting for logs...'
: 'Click Execute to start'}
? formatMessage({ id: 'orchestrator.monitor.waitingForLogs' })
: formatMessage({ id: 'orchestrator.monitor.clickExecuteToStart' })}
</div>
) : (
<div className="space-y-1">

View File

@@ -4,6 +4,7 @@
// React Flow canvas with minimap, controls, and background
import { useCallback, useRef, DragEvent } from 'react';
import { useIntl } from 'react-intl';
import {
ReactFlow,
MiniMap,
@@ -36,6 +37,7 @@ interface FlowCanvasProps {
function FlowCanvasInner({ className }: FlowCanvasProps) {
const reactFlowWrapper = useRef<HTMLDivElement>(null);
const { screenToFlowPosition } = useReactFlow();
const { formatMessage } = useIntl();
// Execution state - lock canvas during execution
const isExecuting = useExecutionStore(selectIsExecuting);
@@ -193,7 +195,7 @@ function FlowCanvasInner({ className }: FlowCanvasProps) {
{isExecuting && (
<div className="absolute top-3 left-1/2 -translate-x-1/2 z-10 px-3 py-1.5 bg-primary/90 text-primary-foreground rounded-full text-xs font-medium shadow-lg flex items-center gap-2">
<span className="h-2 w-2 rounded-full bg-primary-foreground animate-pulse" />
Execution in progress
{formatMessage({ id: 'orchestrator.execution.inProgress' })}
</div>
)}
</div>

View File

@@ -73,7 +73,7 @@ export function FlowToolbar({ className, onOpenTemplateLibrary }: FlowToolbarPro
// Handle save
const handleSave = useCallback(async () => {
if (!currentFlow) {
toast.error('No Flow', 'Create a flow first before saving');
toast.error(formatMessage({ id: 'orchestrator.notifications.noFlow' }), formatMessage({ id: 'orchestrator.notifications.createFlowFirst' }));
return;
}
@@ -90,12 +90,12 @@ export function FlowToolbar({ className, onOpenTemplateLibrary }: FlowToolbarPro
const saved = await saveFlow();
if (saved) {
toast.success('Flow Saved', `"${flowName || currentFlow.name}" saved successfully`);
toast.success(formatMessage({ id: 'orchestrator.notifications.flowSaved' }), formatMessage({ id: 'orchestrator.notifications.savedSuccessfully' }, { name: flowName || currentFlow.name }));
} else {
toast.error('Save Failed', 'Could not save the flow');
toast.error(formatMessage({ id: 'orchestrator.notifications.saveFailed' }), formatMessage({ id: 'orchestrator.notifications.couldNotSave' }));
}
} catch (err) {
toast.error('Save Error', 'An error occurred while saving');
toast.error(formatMessage({ id: 'orchestrator.notifications.saveFailed' }), formatMessage({ id: 'orchestrator.notifications.saveError' }));
} finally {
setIsSaving(false);
}
@@ -107,9 +107,9 @@ export function FlowToolbar({ className, onOpenTemplateLibrary }: FlowToolbarPro
const loaded = await loadFlow(flow.id);
if (loaded) {
setIsFlowListOpen(false);
toast.success('Flow Loaded', `"${flow.name}" loaded successfully`);
toast.success(formatMessage({ id: 'orchestrator.notifications.flowLoaded' }), formatMessage({ id: 'orchestrator.notifications.loadedSuccessfully' }, { name: flow.name }));
} else {
toast.error('Load Failed', 'Could not load the flow');
toast.error(formatMessage({ id: 'orchestrator.notifications.loadFailed' }), formatMessage({ id: 'orchestrator.notifications.couldNotLoad' }));
}
},
[loadFlow]
@@ -119,13 +119,13 @@ export function FlowToolbar({ className, onOpenTemplateLibrary }: FlowToolbarPro
const handleDelete = useCallback(
async (flow: Flow, e: React.MouseEvent) => {
e.stopPropagation();
if (!confirm(`Delete "${flow.name}"? This cannot be undone.`)) return;
if (!confirm(formatMessage({ id: 'orchestrator.notifications.confirmDelete' }, { name: flow.name }))) return;
const deleted = await deleteFlow(flow.id);
if (deleted) {
toast.success('Flow Deleted', `"${flow.name}" deleted successfully`);
toast.success(formatMessage({ id: 'orchestrator.notifications.flowDeleted' }), formatMessage({ id: 'orchestrator.notifications.deletedSuccessfully' }, { name: flow.name }));
} else {
toast.error('Delete Failed', 'Could not delete the flow');
toast.error(formatMessage({ id: 'orchestrator.notifications.deleteFailed' }), formatMessage({ id: 'orchestrator.notifications.couldNotDelete' }));
}
},
[deleteFlow]
@@ -137,9 +137,9 @@ export function FlowToolbar({ className, onOpenTemplateLibrary }: FlowToolbarPro
e.stopPropagation();
const duplicated = await duplicateFlow(flow.id);
if (duplicated) {
toast.success('Flow Duplicated', `"${duplicated.name}" created`);
toast.success(formatMessage({ id: 'orchestrator.notifications.flowDuplicated' }), formatMessage({ id: 'orchestrator.notifications.duplicatedSuccessfully' }, { name: duplicated.name }));
} else {
toast.error('Duplicate Failed', 'Could not duplicate the flow');
toast.error(formatMessage({ id: 'orchestrator.notifications.duplicateFailed' }), formatMessage({ id: 'orchestrator.notifications.couldNotDuplicate' }));
}
},
[duplicateFlow]
@@ -148,7 +148,7 @@ export function FlowToolbar({ className, onOpenTemplateLibrary }: FlowToolbarPro
// Handle export
const handleExport = useCallback(() => {
if (!currentFlow) {
toast.error('No Flow', 'Create or load a flow first');
toast.error(formatMessage({ id: 'orchestrator.notifications.noFlow' }), formatMessage({ id: 'orchestrator.notifications.noFlowToExport' }));
return;
}
@@ -172,7 +172,7 @@ export function FlowToolbar({ className, onOpenTemplateLibrary }: FlowToolbarPro
document.body.removeChild(a);
URL.revokeObjectURL(url);
toast.success('Flow Exported', 'Flow exported as JSON file');
toast.success(formatMessage({ id: 'orchestrator.notifications.flowExported' }), formatMessage({ id: 'orchestrator.notifications.flowExported' }));
}, [currentFlow]);
// Handle run workflow
@@ -185,7 +185,7 @@ export function FlowToolbar({ className, onOpenTemplateLibrary }: FlowToolbarPro
startExecution(result.execId, currentFlow.id);
} catch (error) {
console.error('Failed to execute flow:', error);
toast.error('Execution Failed', 'Could not start flow execution');
toast.error(formatMessage({ id: 'orchestrator.notifications.executionFailed' }), formatMessage({ id: 'orchestrator.notifications.couldNotExecute' }));
}
}, [currentFlow, executeFlow, startExecution, setMonitorPanelOpen]);
@@ -206,7 +206,7 @@ export function FlowToolbar({ className, onOpenTemplateLibrary }: FlowToolbarPro
className="max-w-[200px] h-8 text-sm"
/>
{isModified && (
<span className="text-xs text-amber-500 flex-shrink-0">Unsaved changes</span>
<span className="text-xs text-amber-500 flex-shrink-0">{formatMessage({ id: 'orchestrator.toolbar.unsavedChanges' })}</span>
)}
</div>
@@ -224,7 +224,7 @@ export function FlowToolbar({ className, onOpenTemplateLibrary }: FlowToolbarPro
) : (
<Save className="w-4 h-4 mr-1" />
)}
Save
{formatMessage({ id: 'orchestrator.toolbar.save' })}
</Button>
{/* Flow List Dropdown */}
@@ -235,7 +235,7 @@ export function FlowToolbar({ className, onOpenTemplateLibrary }: FlowToolbarPro
onClick={() => setIsFlowListOpen(!isFlowListOpen)}
>
<FolderOpen className="w-4 h-4 mr-1" />
Load
{formatMessage({ id: 'orchestrator.toolbar.load' })}
<ChevronDown className="w-3 h-3 ml-1" />
</Button>
@@ -251,7 +251,7 @@ export function FlowToolbar({ className, onOpenTemplateLibrary }: FlowToolbarPro
<div className="absolute top-full right-0 mt-1 w-72 bg-card border border-border rounded-lg shadow-lg z-50 overflow-hidden">
<div className="px-3 py-2 border-b border-border bg-muted/50">
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
Saved Flows ({flows.length})
{formatMessage({ id: 'orchestrator.toolbar.savedFlows' }, { count: flows.length })}
</span>
</div>
@@ -259,11 +259,11 @@ export function FlowToolbar({ className, onOpenTemplateLibrary }: FlowToolbarPro
{isLoadingFlows ? (
<div className="p-4 text-center text-muted-foreground">
<Loader2 className="w-5 h-5 animate-spin mx-auto mb-2" />
Loading...
{formatMessage({ id: 'orchestrator.toolbar.loading' })}
</div>
) : flows.length === 0 ? (
<div className="p-4 text-center text-muted-foreground">
No saved flows
{formatMessage({ id: 'orchestrator.toolbar.noSavedFlows' })}
</div>
) : (
flows.map((flow) => (
@@ -317,12 +317,12 @@ export function FlowToolbar({ className, onOpenTemplateLibrary }: FlowToolbarPro
{/* Import & Export Group */}
<Button variant="outline" size="sm" onClick={onOpenTemplateLibrary}>
<Library className="w-4 h-4 mr-1" />
Import Template
{formatMessage({ id: 'orchestrator.toolbar.importTemplate' })}
</Button>
<Button variant="outline" size="sm" onClick={handleExport} disabled={!currentFlow}>
<Download className="w-4 h-4 mr-1" />
Export Flow
{formatMessage({ id: 'orchestrator.toolbar.export' })}
</Button>
<div className="w-px h-6 bg-border" />
@@ -332,10 +332,10 @@ export function FlowToolbar({ className, onOpenTemplateLibrary }: FlowToolbarPro
variant={isMonitorPanelOpen ? 'secondary' : 'outline'}
size="sm"
onClick={handleToggleMonitor}
title="Toggle execution monitor"
title={formatMessage({ id: 'orchestrator.monitor.toggleMonitor' })}
>
<Activity className={cn('w-4 h-4 mr-1', (isExecuting || isPaused) && 'text-primary animate-pulse')} />
Monitor
{formatMessage({ id: 'orchestrator.toolbar.monitor' })}
</Button>
<Button
@@ -349,7 +349,7 @@ export function FlowToolbar({ className, onOpenTemplateLibrary }: FlowToolbarPro
) : (
<Play className="w-4 h-4 mr-1" />
)}
Run Workflow
{formatMessage({ id: 'orchestrator.toolbar.runWorkflow' })}
</Button>
</div>
</div>

View File

@@ -4,6 +4,7 @@
// Compact template list for the left sidebar, uses useTemplates hook
import { useState, useCallback, useMemo } from 'react';
import { useIntl } from 'react-intl';
import { Search, Loader2, FileText, Download, GitBranch } from 'lucide-react';
import { cn } from '@/lib/utils';
import { Input } from '@/components/ui/Input';
@@ -21,6 +22,8 @@ interface TemplateItemProps {
}
function TemplateItem({ template, onInstall, isInstalling }: TemplateItemProps) {
const { formatMessage } = useIntl();
return (
<button
onClick={() => onInstall(template)}
@@ -38,7 +41,7 @@ function TemplateItem({ template, onInstall, isInstalling }: TemplateItemProps)
<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
{template.nodeCount} {formatMessage({ id: 'orchestrator.inlineTemplates.nodes' })}
</span>
{template.category && (
<Badge variant="secondary" className="text-[10px] px-1.5 py-0">
@@ -68,6 +71,7 @@ interface InlineTemplatePanelProps {
* Clicking a template installs it as the current flow.
*/
export function InlineTemplatePanel({ className }: InlineTemplatePanelProps) {
const { formatMessage } = useIntl();
const [searchQuery, setSearchQuery] = useState('');
const [installingId, setInstallingId] = useState<string | null>(null);
@@ -118,7 +122,7 @@ export function InlineTemplatePanel({ className }: InlineTemplatePanelProps) {
<Input
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="搜索模板..."
placeholder={formatMessage({ id: 'orchestrator.inlineTemplates.searchPlaceholder' })}
className="pl-8 h-8 text-sm"
/>
</div>
@@ -134,14 +138,14 @@ export function InlineTemplatePanel({ className }: InlineTemplatePanelProps) {
<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
{formatMessage({ id: 'orchestrator.inlineTemplates.loadFailed' })}
</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 ? '未找到匹配的模板' : '暂无可用模板'}
{searchQuery ? formatMessage({ id: 'orchestrator.inlineTemplates.noMatches' }) : formatMessage({ id: 'orchestrator.inlineTemplates.noTemplates' })}
</p>
</div>
) : (

View File

@@ -3,6 +3,7 @@
// ========================================
// Container with tab switching between NodeLibrary and InlineTemplatePanel
import { useIntl } from 'react-intl';
import { ChevronRight, ChevronDown } from 'lucide-react';
import { cn } from '@/lib/utils';
import { Button } from '@/components/ui/Button';
@@ -12,9 +13,9 @@ 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' },
const TABS: Array<{ key: 'templates' | 'nodes'; labelKey: string }> = [
{ key: 'templates', labelKey: 'orchestrator.leftSidebar.tabTemplates' },
{ key: 'nodes', labelKey: 'orchestrator.leftSidebar.tabNodes' },
];
// ========== Main Component ==========
@@ -28,6 +29,7 @@ interface LeftSidebarProps {
* Renders either InlineTemplatePanel or NodeLibrary based on active tab.
*/
export function LeftSidebar({ className }: LeftSidebarProps) {
const { formatMessage } = useIntl();
const isPaletteOpen = useFlowStore((state) => state.isPaletteOpen);
const setIsPaletteOpen = useFlowStore((state) => state.setIsPaletteOpen);
const leftPanelTab = useFlowStore((state) => state.leftPanelTab);
@@ -41,7 +43,7 @@ export function LeftSidebar({ className }: LeftSidebarProps) {
variant="ghost"
size="icon"
onClick={() => setIsPaletteOpen(true)}
title="展开面板"
title={formatMessage({ id: 'orchestrator.leftSidebar.expand' })}
>
<ChevronRight className="w-4 h-4" />
</Button>
@@ -54,13 +56,13 @@ export function LeftSidebar({ className }: LeftSidebarProps) {
<div className={cn('w-72 bg-card border-r border-border flex flex-col', className)}>
{/* Header */}
<div className="flex items-center justify-between px-4 py-3 border-b border-border">
<h3 className="font-semibold text-foreground"></h3>
<h3 className="font-semibold text-foreground">{formatMessage({ id: 'orchestrator.leftSidebar.workbench' })}</h3>
<Button
variant="ghost"
size="icon"
className="h-6 w-6"
onClick={() => setIsPaletteOpen(false)}
title="折叠面板"
title={formatMessage({ id: 'orchestrator.leftSidebar.collapse' })}
>
<ChevronDown className="w-4 h-4" />
</Button>
@@ -80,7 +82,7 @@ export function LeftSidebar({ className }: LeftSidebarProps) {
: 'text-muted-foreground'
)}
>
{tab.label}
{formatMessage({ id: tab.labelKey })}
</button>
))}
</div>
@@ -95,7 +97,7 @@ export function LeftSidebar({ className }: LeftSidebarProps) {
{/* Footer */}
<div className="px-4 py-3 border-t border-border bg-muted/30">
<div className="text-xs text-muted-foreground">
<span className="font-medium">Tip:</span>
<span className="font-medium">{formatMessage({ id: 'orchestrator.leftSidebar.tipLabel' })}</span> {formatMessage({ id: 'orchestrator.leftSidebar.dragOrDoubleClick' })}
</div>
</div>
</div>

View File

@@ -5,13 +5,14 @@
// 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 { NODE_TYPE_CONFIGS, QUICK_TEMPLATES } from '@/types/flow';
import { QUICK_TEMPLATES } from '@/types/flow';
import type { QuickTemplate } from '@/types/flow';
// ========== Icon Mapping ==========
@@ -21,6 +22,23 @@ const TEMPLATE_ICONS: Record<string, React.ElementType> = {
'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 = [
@@ -86,7 +104,11 @@ function QuickTemplateCard({
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');
@@ -113,14 +135,14 @@ function QuickTemplateCard({
<Icon className="w-4 h-4" />
</div>
<div className="flex-1 min-w-0">
<div className="text-sm font-medium text-foreground">{template.label}</div>
<div className="text-xs text-muted-foreground truncate">{template.description}</div>
<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="Delete template"
title={formatMessage({ id: 'orchestrator.nodeLibrary.deleteTemplate' })}
>
<Trash2 className="w-4 h-4" />
</button>
@@ -135,7 +157,8 @@ function QuickTemplateCard({
* Basic empty prompt template card
*/
function BasicTemplateCard() {
const config = NODE_TYPE_CONFIGS['prompt-template'];
const { formatMessage } = useIntl();
const i18n = TEMPLATE_I18N['prompt-template'];
const onDragStart = (event: DragEvent<HTMLDivElement>) => {
event.dataTransfer.setData('application/reactflow-node-type', 'prompt-template');
@@ -162,8 +185,8 @@ function BasicTemplateCard() {
<Plus className="w-4 h-4" />
</div>
<div className="flex-1 min-w-0">
<div className="text-sm font-medium text-foreground">{config.label}</div>
<div className="text-xs text-muted-foreground truncate">{config.description}</div>
<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>
@@ -174,6 +197,7 @@ function BasicTemplateCard() {
* 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('');
@@ -204,7 +228,7 @@ function CreateTemplateForm({ onClose }: { onClose: () => void }) {
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">New Custom Node</span>
<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>
@@ -212,7 +236,7 @@ function CreateTemplateForm({ onClose }: { onClose: () => void }) {
<input
type="text"
placeholder="Node name"
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"
@@ -221,14 +245,14 @@ function CreateTemplateForm({ onClose }: { onClose: () => void }) {
<input
type="text"
placeholder="Description (optional)"
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="Default instruction (optional)"
placeholder={formatMessage({ id: 'orchestrator.nodeLibrary.defaultInstructionOptional' })}
value={instruction}
onChange={(e) => setInstruction(e.target.value)}
rows={2}
@@ -237,7 +261,7 @@ function CreateTemplateForm({ onClose }: { onClose: () => void }) {
{/* Color picker */}
<div>
<div className="text-xs text-muted-foreground mb-1.5">Color</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
@@ -266,7 +290,7 @@ function CreateTemplateForm({ onClose }: { onClose: () => void }) {
: 'bg-muted text-muted-foreground cursor-not-allowed',
)}
>
Save
{formatMessage({ id: 'orchestrator.nodeLibrary.save' })}
</button>
</div>
);
@@ -284,6 +308,7 @@ interface NodeLibraryProps {
* 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);
@@ -291,7 +316,7 @@ export function NodeLibrary({ className }: NodeLibraryProps) {
return (
<div className={cn('flex-1 overflow-y-auto p-4 space-y-4', className)}>
{/* Built-in templates */}
<TemplateCategory title="Built-in" defaultExpanded>
<TemplateCategory title={formatMessage({ id: 'orchestrator.nodeLibrary.builtIn' })} defaultExpanded>
<BasicTemplateCard />
{QUICK_TEMPLATES.map((template) => (
<QuickTemplateCard key={template.id} template={template} />
@@ -300,13 +325,13 @@ export function NodeLibrary({ className }: NodeLibraryProps) {
{/* Custom templates */}
<TemplateCategory
title={`Custom (${customTemplates.length})`}
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="Create custom node"
title={formatMessage({ id: 'orchestrator.nodeLibrary.createCustomNode' })}
>
<Plus className="w-3.5 h-3.5" />
</button>
@@ -322,7 +347,7 @@ export function NodeLibrary({ className }: NodeLibraryProps) {
))}
{customTemplates.length === 0 && !isCreating && (
<div className="text-xs text-muted-foreground text-center py-3">
No custom nodes yet. Click + to create.
{formatMessage({ id: 'orchestrator.nodeLibrary.noCustomNodes' })}
</div>
)}
</TemplateCategory>

View File

@@ -172,6 +172,7 @@ interface TemplateModalProps {
}
function TemplateModal({ isOpen, onClose, onSave }: TemplateModalProps) {
const { formatMessage } = useIntl();
const [label, setLabel] = useState('');
const [content, setContent] = useState('');
const [color, setColor] = useState<TemplateItem['color']>('slate');
@@ -216,7 +217,7 @@ function TemplateModal({ isOpen, onClose, onSave }: TemplateModalProps) {
{/* Modal */}
<div className="relative bg-card border border-border rounded-lg shadow-xl w-full max-w-md mx-4 p-4 space-y-4">
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold text-foreground"></h3>
<h3 className="text-lg font-semibold text-foreground">{formatMessage({ id: 'orchestrator.propertyPanel.createCustomTemplate' })}</h3>
<button onClick={onClose} className="text-muted-foreground hover:text-foreground">
<X className="w-5 h-5" />
</button>
@@ -224,7 +225,7 @@ function TemplateModal({ isOpen, onClose, onSave }: TemplateModalProps) {
{/* Template name */}
<div>
<label className="block text-sm font-medium text-foreground mb-1"></label>
<label className="block text-sm font-medium text-foreground mb-1">{formatMessage({ id: 'orchestrator.propertyPanel.templateNameLabel' })}</label>
<Input
value={label}
onChange={(e) => setLabel(e.target.value)}
@@ -236,8 +237,8 @@ function TemplateModal({ isOpen, onClose, onSave }: TemplateModalProps) {
{/* Template content */}
<div>
<label className="block text-sm font-medium text-foreground mb-1">
<span className="text-muted-foreground font-normal ml-1">(使 $INPUT )</span>
{formatMessage({ id: 'orchestrator.propertyPanel.templateContent' })}
<span className="text-muted-foreground font-normal ml-1">{formatMessage({ id: 'orchestrator.propertyPanel.templateContentHint' })}</span>
</label>
<textarea
value={content}
@@ -250,7 +251,7 @@ function TemplateModal({ isOpen, onClose, onSave }: TemplateModalProps) {
{/* Color selection */}
<div>
<label className="block text-sm font-medium text-foreground mb-1"></label>
<label className="block text-sm font-medium text-foreground mb-1">{formatMessage({ id: 'orchestrator.propertyPanel.tagColor' })}</label>
<div className="flex flex-wrap gap-2">
{COLOR_OPTIONS.map((opt) => (
<button
@@ -279,7 +280,7 @@ function TemplateModal({ isOpen, onClose, onSave }: TemplateModalProps) {
className="rounded border-border"
/>
<label htmlFor="has-input" className="text-sm text-foreground">
{formatMessage({ id: 'orchestrator.propertyPanel.requiresInput' })}
</label>
</div>
@@ -287,7 +288,7 @@ function TemplateModal({ isOpen, onClose, onSave }: TemplateModalProps) {
{hasInput && (
<div className="grid grid-cols-2 gap-2 pl-6">
<div>
<label className="block text-xs text-muted-foreground mb-1"></label>
<label className="block text-xs text-muted-foreground mb-1">{formatMessage({ id: 'orchestrator.propertyPanel.inputPrompt' })}</label>
<Input
value={inputLabel}
onChange={(e) => setInputLabel(e.target.value)}
@@ -296,7 +297,7 @@ function TemplateModal({ isOpen, onClose, onSave }: TemplateModalProps) {
/>
</div>
<div>
<label className="block text-xs text-muted-foreground mb-1"></label>
<label className="block text-xs text-muted-foreground mb-1">{formatMessage({ id: 'orchestrator.propertyPanel.defaultValue' })}</label>
<Input
value={inputDefault}
onChange={(e) => setInputDefault(e.target.value)}
@@ -310,7 +311,7 @@ function TemplateModal({ isOpen, onClose, onSave }: TemplateModalProps) {
{/* Actions */}
<div className="flex justify-end gap-2 pt-2">
<Button variant="outline" size="sm" onClick={onClose}>
{formatMessage({ id: 'orchestrator.propertyPanel.cancel' })}
</Button>
<Button
size="sm"
@@ -319,7 +320,7 @@ function TemplateModal({ isOpen, onClose, onSave }: TemplateModalProps) {
className="gap-1"
>
<Save className="w-4 h-4" />
{formatMessage({ id: 'orchestrator.propertyPanel.saveTemplate' })}
</Button>
</div>
</div>
@@ -399,6 +400,7 @@ function extractArtifacts(text: string): string[] {
* Tag-based instruction editor with inline variable tags
*/
function TagEditor({ value, onChange, placeholder, availableVariables, minHeight = 120 }: TagEditorProps) {
const { formatMessage } = useIntl();
const editorRef = useRef<HTMLDivElement>(null);
const [isFocused, setIsFocused] = useState(false);
const [newVarName, setNewVarName] = useState('');
@@ -629,7 +631,7 @@ function TagEditor({ value, onChange, placeholder, availableVariables, minHeight
{availableVariables.length > 0 && (
<>
<div className="w-px h-5 bg-border" />
<span className="text-xs text-muted-foreground">:</span>
<span className="text-xs text-muted-foreground">{formatMessage({ id: 'orchestrator.propertyPanel.available' })}</span>
{availableVariables.slice(0, 5).map((varName) => (
<button
key={varName}
@@ -647,7 +649,7 @@ function TagEditor({ value, onChange, placeholder, availableVariables, minHeight
{detectedVars.length > 0 && (
<>
<div className="w-px h-5 bg-border" />
<span className="text-xs text-muted-foreground">:</span>
<span className="text-xs text-muted-foreground">{formatMessage({ id: 'orchestrator.propertyPanel.variables' })}</span>
{detectedVars.map((varName) => {
const isValid = availableVariables.includes(varName) || varName.includes('.');
return (
@@ -672,7 +674,7 @@ function TagEditor({ value, onChange, placeholder, availableVariables, minHeight
{detectedArtifacts.length > 0 && (
<>
<div className="w-px h-5 bg-border" />
<span className="text-xs text-muted-foreground">:</span>
<span className="text-xs text-muted-foreground">{formatMessage({ id: 'orchestrator.propertyPanel.artifactsLabel' })}</span>
{detectedArtifacts.map((artName) => (
<span
key={artName}
@@ -689,7 +691,7 @@ function TagEditor({ value, onChange, placeholder, availableVariables, minHeight
<div className="space-y-2">
{/* Template buttons by category */}
<div className="flex flex-wrap items-center gap-2 p-2 rounded-md bg-muted/30 border border-border">
<span className="text-xs text-muted-foreground shrink-0">:</span>
<span className="text-xs text-muted-foreground shrink-0">{formatMessage({ id: 'orchestrator.propertyPanel.templateLabel' })}</span>
{allTemplates.map((template) => (
<div key={template.id} className="relative group">
@@ -719,7 +721,7 @@ function TagEditor({ value, onChange, placeholder, availableVariables, minHeight
type="button"
onClick={(e) => {
e.stopPropagation();
if (confirm(`确定删除模板 "${template.label}"`)) {
if (confirm(formatMessage({ id: 'orchestrator.propertyPanel.confirmDeleteTemplate' }, { name: template.label }))) {
handleDeleteTemplate(template.id);
}
}}
@@ -738,7 +740,7 @@ function TagEditor({ value, onChange, placeholder, availableVariables, minHeight
className="inline-flex items-center gap-1 px-2 py-1 rounded text-xs font-medium border border-dashed border-border text-muted-foreground hover:text-foreground hover:border-foreground transition-colors"
>
<Plus className="w-3 h-3" />
{formatMessage({ id: 'orchestrator.propertyPanel.newTemplate' })}
</button>
</div>
</div>
@@ -897,6 +899,7 @@ function CollapsibleSection({
// ========== Tags Input ==========
function TagsInput({ tags, onChange }: { tags: string[]; onChange: (tags: string[]) => void }) {
const { formatMessage } = useIntl();
const [input, setInput] = useState('');
const handleAdd = () => {
@@ -934,7 +937,7 @@ function TagsInput({ tags, onChange }: { tags: string[]; onChange: (tags: string
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="添加标签..."
placeholder={formatMessage({ id: 'orchestrator.propertyPanel.addTag' })}
className="h-7 text-xs"
/>
<Button variant="ghost" size="sm" onClick={handleAdd} disabled={!input.trim()} className="h-7 px-2">
@@ -1041,11 +1044,11 @@ function PromptTemplateProperties({ data, onChange }: PromptTemplatePropertiesPr
}}
className="w-full h-10 px-3 rounded-md border border-border bg-background text-foreground text-sm"
>
<optgroup label="Slash Commands">
<optgroup label={formatMessage({ id: 'orchestrator.propertyPanel.slashCommandsGroup' })}>
<option value="mainprocess">{formatMessage({ id: 'orchestrator.propertyPanel.options.modeMainprocess' })}</option>
<option value="async">{formatMessage({ id: 'orchestrator.propertyPanel.options.modeAsync' })}</option>
</optgroup>
<optgroup label="CLI Tools">
<optgroup label={formatMessage({ id: 'orchestrator.propertyPanel.cliToolsGroup' })}>
<option value="analysis">{formatMessage({ id: 'orchestrator.propertyPanel.options.modeAnalysis' })}</option>
<option value="write">{formatMessage({ id: 'orchestrator.propertyPanel.options.modeWrite' })}</option>
</optgroup>
@@ -1075,14 +1078,14 @@ function PromptTemplateProperties({ data, onChange }: PromptTemplatePropertiesPr
)}
{/* Classification Section */}
<CollapsibleSection title="分类信息" defaultExpanded={false}>
<CollapsibleSection title={formatMessage({ id: 'orchestrator.propertyPanel.classificationSection' })} defaultExpanded={false}>
{/* Description */}
<div>
<label className="block text-sm font-medium text-foreground mb-1"></label>
<label className="block text-sm font-medium text-foreground mb-1">{formatMessage({ id: 'orchestrator.propertyPanel.description' })}</label>
<textarea
value={data.description || ''}
onChange={(e) => onChange({ description: e.target.value })}
placeholder="节点功能描述..."
placeholder={formatMessage({ id: 'orchestrator.propertyPanel.descriptionPlaceholder' })}
rows={2}
className="w-full px-3 py-2 rounded-md border border-border bg-background text-foreground text-sm resize-none"
/>
@@ -1090,7 +1093,7 @@ function PromptTemplateProperties({ data, onChange }: PromptTemplatePropertiesPr
{/* Tags */}
<div>
<label className="block text-sm font-medium text-foreground mb-1"></label>
<label className="block text-sm font-medium text-foreground mb-1">{formatMessage({ id: 'orchestrator.propertyPanel.tags' })}</label>
<TagsInput
tags={data.tags || []}
onChange={(tags) => onChange({ tags })}
@@ -1099,21 +1102,21 @@ function PromptTemplateProperties({ data, onChange }: PromptTemplatePropertiesPr
</CollapsibleSection>
{/* Execution Section */}
<CollapsibleSection title="执行配置" defaultExpanded={false}>
<CollapsibleSection title={formatMessage({ id: 'orchestrator.propertyPanel.executionSection' })} defaultExpanded={false}>
{/* Condition */}
<div>
<label className="block text-sm font-medium text-foreground mb-1"></label>
<label className="block text-sm font-medium text-foreground mb-1">{formatMessage({ id: 'orchestrator.propertyPanel.condition' })}</label>
<Input
value={data.condition || ''}
onChange={(e) => onChange({ condition: e.target.value || undefined })}
placeholder="例如: {{prev.success}} === true"
placeholder={formatMessage({ id: 'orchestrator.propertyPanel.conditionPlaceholder' })}
className="font-mono text-sm"
/>
</div>
{/* Artifacts */}
<div>
<label className="block text-sm font-medium text-foreground mb-1"></label>
<label className="block text-sm font-medium text-foreground mb-1">{formatMessage({ id: 'orchestrator.propertyPanel.artifacts' })}</label>
<ArtifactsList
artifacts={data.artifacts || []}
onChange={(artifacts) => onChange({ artifacts })}
@@ -1138,6 +1141,7 @@ const SAVE_COLOR_OPTIONS = [
];
function SaveAsTemplateButton({ nodeId, nodeLabel }: { nodeId: string; nodeLabel: string }) {
const { formatMessage } = useIntl();
const [isOpen, setIsOpen] = useState(false);
const [name, setName] = useState('');
const [desc, setDesc] = useState('');
@@ -1175,7 +1179,7 @@ function SaveAsTemplateButton({ nodeId, nodeLabel }: { nodeId: string; nodeLabel
onClick={() => { setName(nodeLabel); setIsOpen(true); }}
>
<BookmarkPlus className="w-4 h-4 mr-2" />
Save to Node Library
{formatMessage({ id: 'orchestrator.propertyPanel.saveToLibrary' })}
</Button>
);
}
@@ -1185,14 +1189,14 @@ function SaveAsTemplateButton({ nodeId, nodeLabel }: { nodeId: string; nodeLabel
<Input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Template name"
placeholder={formatMessage({ id: 'orchestrator.propertyPanel.templateName' })}
className="h-8 text-sm"
autoFocus
/>
<Input
value={desc}
onChange={(e) => setDesc(e.target.value)}
placeholder="Description (optional)"
placeholder={formatMessage({ id: 'orchestrator.propertyPanel.descriptionOptional' })}
className="h-8 text-sm"
/>
<div className="flex flex-wrap gap-1">
@@ -1211,11 +1215,11 @@ function SaveAsTemplateButton({ nodeId, nodeLabel }: { nodeId: string; nodeLabel
</div>
<div className="flex gap-1">
<Button variant="outline" size="sm" className="flex-1" onClick={() => setIsOpen(false)}>
Cancel
{formatMessage({ id: 'orchestrator.propertyPanel.cancel' })}
</Button>
<Button size="sm" className="flex-1" onClick={handleSave} disabled={!name.trim()}>
<Save className="w-3.5 h-3.5 mr-1" />
Save
{formatMessage({ id: 'orchestrator.propertyPanel.save' })}
</Button>
</div>
</div>
@@ -1318,7 +1322,7 @@ export function PropertyPanel({ className }: PropertyPanelProps) {
{/* Node Type Badge */}
<div className="px-4 py-2 border-b border-border bg-muted/30">
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
prompt template
{formatMessage({ id: 'orchestrator.propertyPanel.nodeType' })}
</span>
</div>