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

@@ -19,7 +19,8 @@
"resume": "Resume",
"stop": "Stop",
"restart": "Restart",
"viewLogs": "View Logs"
"viewLogs": "View Logs",
"inProgress": "Execution in progress"
},
"status": {
"pending": "Pending",
@@ -35,6 +36,7 @@
"edit": "Edit Node",
"delete": "Delete Node",
"status": "Node Status",
"statusCount": "Node Status ({completed}/{total})",
"result": "Result"
},
"actions": {
@@ -54,11 +56,14 @@
}
},
"monitor": {
"title": "Execution Monitor",
"title": "Monitor",
"logs": "Logs",
"timeline": "Timeline",
"variables": "Variables",
"realtime": "Real-time Updates"
"realtime": "Real-time Updates",
"waitingForLogs": "Waiting for logs...",
"clickExecuteToStart": "Click Execute to start",
"toggleMonitor": "Toggle execution monitor"
},
"notifications": {
"flowCreated": "Flow Created",
@@ -69,7 +74,23 @@
"flowDeleted": "Flow Deleted",
"deleteFailed": "Delete Failed",
"flowDuplicated": "Flow Duplicated",
"duplicateFailed": "Duplicate Failed"
"duplicateFailed": "Duplicate Failed",
"noFlow": "No Flow",
"createFlowFirst": "Create a flow first before saving",
"savedSuccessfully": "\"{name}\" saved successfully",
"couldNotSave": "Could not save the flow",
"saveError": "An error occurred while saving",
"loadedSuccessfully": "\"{name}\" loaded successfully",
"couldNotLoad": "Could not load the flow",
"confirmDelete": "Delete \"{name}\"? This cannot be undone.",
"deletedSuccessfully": "\"{name}\" deleted successfully",
"couldNotDelete": "Could not delete the flow",
"duplicatedSuccessfully": "\"{name}\" created",
"couldNotDuplicate": "Could not duplicate the flow",
"flowExported": "Flow exported as JSON file",
"noFlowToExport": "Create or load a flow first",
"executionFailed": "Execution Failed",
"couldNotExecute": "Could not start flow execution"
},
"templateLibrary": {
"title": "Template Library",
@@ -119,8 +140,11 @@
"new": "New",
"save": "Save",
"load": "Load",
"export": "Export",
"export": "Export Flow",
"templates": "Templates",
"importTemplate": "Import Template",
"runWorkflow": "Run Workflow",
"monitor": "Monitor",
"savedFlows": "Saved Flows ({count})",
"loading": "Loading...",
"noSavedFlows": "No saved flows",
@@ -136,6 +160,42 @@
"tipLabel": "Tip:",
"tip": "Connect nodes by dragging from output to input handles"
},
"leftSidebar": {
"workbench": "Workbench",
"expand": "Expand panel",
"collapse": "Collapse panel",
"tabTemplates": "Templates",
"tabNodes": "Nodes",
"tipLabel": "Tip:",
"dragOrDoubleClick": "Drag to canvas or double-click to add"
},
"nodeLibrary": {
"builtIn": "Built-in",
"custom": "Custom ({count})",
"newCustomNode": "New Custom Node",
"nodeName": "Node name",
"descriptionOptional": "Description (optional)",
"defaultInstructionOptional": "Default instruction (optional)",
"color": "Color",
"save": "Save",
"noCustomNodes": "No custom nodes yet. Click + to create.",
"deleteTemplate": "Delete template",
"createCustomNode": "Create custom node",
"promptTemplateLabel": "Prompt Template",
"promptTemplateDesc": "Natural language instruction for workflow step",
"slashCommandLabel": "Slash Command",
"slashCommandDesc": "Execute /workflow commands (main thread)",
"slashCommandAsyncLabel": "Slash Command (Async)",
"slashCommandAsyncDesc": "Execute /workflow commands (background)",
"newStepLabel": "New Step"
},
"inlineTemplates": {
"searchPlaceholder": "Search templates...",
"loadFailed": "Could not load template library. Please check API service.",
"noMatches": "No matching templates found",
"noTemplates": "No templates available",
"nodes": "nodes"
},
"variablePicker": {
"empty": "No variables available"
},
@@ -161,6 +221,38 @@
"close": "Close panel",
"selectNode": "Select a node to edit its properties",
"deleteNode": "Delete Node",
"nodeType": "prompt template",
"saveToLibrary": "Save to Node Library",
"templateName": "Template name",
"descriptionOptional": "Description (optional)",
"cancel": "Cancel",
"save": "Save",
"slashCommandsGroup": "Slash Commands",
"cliToolsGroup": "CLI Tools",
"classificationSection": "Classification",
"description": "Description",
"descriptionPlaceholder": "Node description...",
"tags": "Tags",
"addTag": "Add tag...",
"executionSection": "Execution Config",
"condition": "Condition",
"conditionPlaceholder": "e.g., {{prev.success}} === true",
"artifacts": "Artifacts",
"available": "Available:",
"variables": "Variables:",
"artifactsLabel": "Artifacts:",
"templateLabel": "Templates:",
"newTemplate": "New",
"createCustomTemplate": "Create Custom Template",
"templateNameLabel": "Template Name",
"templateContent": "Template Content",
"templateContentHint": "(Use $INPUT as input placeholder)",
"tagColor": "Tag Color",
"requiresInput": "Requires user input parameter",
"inputPrompt": "Input prompt",
"defaultValue": "Default value",
"saveTemplate": "Save Template",
"confirmDeleteTemplate": "Delete template \"{name}\"?",
"placeholders": {
"nodeLabel": "Node label",
"instruction": "e.g., Execute /workflow:plan for login feature\nor: Analyze code architecture\nor: Save {{analysis}} to ./output/result.json",

View File

@@ -19,7 +19,8 @@
"resume": "继续",
"stop": "停止",
"restart": "重新开始",
"viewLogs": "查看日志"
"viewLogs": "查看日志",
"inProgress": "正在执行中"
},
"status": {
"pending": "待处理",
@@ -35,6 +36,7 @@
"edit": "编辑节点",
"delete": "删除节点",
"status": "节点状态",
"statusCount": "节点状态 ({completed}/{total})",
"result": "结果"
},
"actions": {
@@ -54,11 +56,14 @@
}
},
"monitor": {
"title": "执行监控",
"title": "监控",
"logs": "日志",
"timeline": "时间线",
"variables": "变量",
"realtime": "实时更新"
"realtime": "实时更新",
"waitingForLogs": "等待日志...",
"clickExecuteToStart": "点击执行以开始",
"toggleMonitor": "切换执行监控"
},
"notifications": {
"flowCreated": "流程已创建",
@@ -69,7 +74,23 @@
"flowDeleted": "流程已删除",
"deleteFailed": "删除失败",
"flowDuplicated": "流程已复制",
"duplicateFailed": "复制失败"
"duplicateFailed": "复制失败",
"noFlow": "无流程",
"createFlowFirst": "请先创建流程再保存",
"savedSuccessfully": "\"{name}\" 保存成功",
"couldNotSave": "无法保存流程",
"saveError": "保存时发生错误",
"loadedSuccessfully": "\"{name}\" 加载成功",
"couldNotLoad": "无法加载流程",
"confirmDelete": "确定删除 \"{name}\"?此操作无法撤销。",
"deletedSuccessfully": "\"{name}\" 删除成功",
"couldNotDelete": "无法删除流程",
"duplicatedSuccessfully": "\"{name}\" 创建成功",
"couldNotDuplicate": "无法复制流程",
"flowExported": "流程已导出为 JSON 文件",
"noFlowToExport": "请先创建或加载流程",
"executionFailed": "执行失败",
"couldNotExecute": "无法启动流程执行"
},
"templateLibrary": {
"title": "模板库",
@@ -118,8 +139,11 @@
"new": "新建",
"save": "保存",
"load": "加载",
"export": "导出",
"export": "导出流程",
"templates": "模板",
"importTemplate": "导入模板",
"runWorkflow": "运行流程",
"monitor": "监控",
"savedFlows": "已保存的流程 ({count})",
"loading": "加载中...",
"noSavedFlows": "无已保存的流程",
@@ -135,6 +159,42 @@
"tipLabel": "提示:",
"tip": "通过从输出拖动到输入句柄来连接节点"
},
"leftSidebar": {
"workbench": "工作台",
"expand": "展开面板",
"collapse": "折叠面板",
"tabTemplates": "模板库",
"tabNodes": "节点库",
"tipLabel": "提示:",
"dragOrDoubleClick": "拖拽到画布或双击添加"
},
"nodeLibrary": {
"builtIn": "内置节点",
"custom": "自定义 ({count})",
"newCustomNode": "新建自定义节点",
"nodeName": "节点名称",
"descriptionOptional": "描述 (可选)",
"defaultInstructionOptional": "默认指令 (可选)",
"color": "颜色",
"save": "保存",
"noCustomNodes": "暂无自定义节点。点击 + 创建。",
"deleteTemplate": "删除模板",
"createCustomNode": "创建自定义节点",
"promptTemplateLabel": "提示模板",
"promptTemplateDesc": "自然语言工作流步骤指令",
"slashCommandLabel": "斜杠命令",
"slashCommandDesc": "执行 /workflow 命令 (主线程)",
"slashCommandAsyncLabel": "斜杠命令 (异步)",
"slashCommandAsyncDesc": "执行 /workflow 命令 (后台)",
"newStepLabel": "新步骤"
},
"inlineTemplates": {
"searchPlaceholder": "搜索模板...",
"loadFailed": "无法加载模板库,请确认 API 服务可用",
"noMatches": "未找到匹配的模板",
"noTemplates": "暂无可用模板",
"nodes": "个节点"
},
"variablePicker": {
"empty": "没有可用的变量"
},
@@ -160,6 +220,38 @@
"close": "关闭面板",
"selectNode": "选择节点以编辑其属性",
"deleteNode": "删除节点",
"nodeType": "提示模板",
"saveToLibrary": "保存到节点库",
"templateName": "模板名称",
"descriptionOptional": "描述 (可选)",
"cancel": "取消",
"save": "保存",
"slashCommandsGroup": "斜杠命令",
"cliToolsGroup": "CLI 工具",
"classificationSection": "分类信息",
"description": "描述",
"descriptionPlaceholder": "节点功能描述...",
"tags": "标签",
"addTag": "添加标签...",
"executionSection": "执行配置",
"condition": "条件",
"conditionPlaceholder": "例如: {{prev.success}} === true",
"artifacts": "产物",
"available": "可用:",
"variables": "变量:",
"artifactsLabel": "产物:",
"templateLabel": "模板:",
"newTemplate": "新建",
"createCustomTemplate": "创建自定义模板",
"templateNameLabel": "模板名称",
"templateContent": "模板内容",
"templateContentHint": "(使用 $INPUT 作为输入占位符)",
"tagColor": "标签颜色",
"requiresInput": "需要用户输入参数",
"inputPrompt": "输入提示",
"defaultValue": "默认值",
"saveTemplate": "保存模板",
"confirmDeleteTemplate": "确定删除模板 \"{name}\"",
"placeholders": {
"nodeLabel": "节点标签",
"instruction": "例如: 执行 /workflow:plan 用于登录功能\n或: 分析代码架构\n或: 将 {{analysis}} 保存到 ./output/result.json",

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>