mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-09 02:24:11 +08:00
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:
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
) : (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user