refactor: remove lite-plan-c workflow and update orchestrator UI components

- Deleted the lite-plan-c workflow file to streamline the planning process.
- Updated orchestrator localization files to include new variable picker and multi-node selector messages.
- Modified PropertyPanel to support new execution modes and added output variable input fields.
- Enhanced SlashCommandNode to reflect changes in execution modes.
- Introduced MultiNodeSelector component for better node selection management.
- Added VariablePicker component for selecting variables with improved UX.
This commit is contained in:
catlog22
2026-02-03 21:24:34 +08:00
parent 9bb50a13fa
commit a806d70d9b
13 changed files with 233 additions and 3084 deletions

View File

@@ -0,0 +1,123 @@
import * as React from "react";
import { useIntl } from "react-intl";
import { Check, X } from "lucide-react";
import { cn } from "@/lib/utils";
export interface NodeOption {
id: string;
label: string;
type?: string;
}
export interface MultiNodeSelectorProps {
availableNodes: NodeOption[];
selectedNodes: string[];
onChange: (selectedIds: string[]) => void;
placeholder?: string;
emptyMessage?: string;
className?: string;
}
const MultiNodeSelector = React.forwardRef<HTMLDivElement, MultiNodeSelectorProps>(
({ availableNodes, selectedNodes, onChange, placeholder, emptyMessage, className }, ref) => {
const { formatMessage } = useIntl();
const isSelected = (nodeId: string) => selectedNodes.includes(nodeId);
const toggleNode = (nodeId: string) => {
if (isSelected(nodeId)) {
onChange(selectedNodes.filter((id) => id !== nodeId));
} else {
onChange([...selectedNodes, nodeId]);
}
};
const clearSelection = () => {
onChange([]);
};
return (
<div ref={ref} className={cn("space-y-2", className)}>
{/* Selected tags */}
{selectedNodes.length > 0 && (
<div className="flex flex-wrap gap-2 p-2 rounded-md border border-border bg-muted/30">
{selectedNodes.map((nodeId) => {
const node = availableNodes.find((n) => n.id === nodeId);
return (
<span
key={nodeId}
className="inline-flex items-center gap-1 px-2 py-1 rounded-md bg-primary text-primary-foreground text-xs"
>
<span>{node?.label || nodeId}</span>
<button
type="button"
onClick={() => toggleNode(nodeId)}
className="hover:bg-primary-foreground/20 rounded p-0.5"
>
<X className="w-3 h-3" />
</button>
</span>
);
})}
<button
type="button"
onClick={clearSelection}
className="text-xs text-muted-foreground hover:text-foreground underline"
>
{formatMessage({ id: 'orchestrator.multiNodeSelector.clear' })}
</button>
</div>
)}
{/* Available nodes list */}
<div className="border border-border rounded-md bg-background max-h-48 overflow-y-auto">
{availableNodes.length === 0 ? (
<div className="p-4 text-sm text-muted-foreground text-center">
{emptyMessage || formatMessage({ id: 'orchestrator.multiNodeSelector.empty' })}
</div>
) : (
<div className="p-1">
{availableNodes.map((node) => (
<div
key={node.id}
onClick={() => toggleNode(node.id)}
className={cn(
"flex items-center gap-2 p-2 rounded cursor-pointer transition-colors",
"hover:bg-muted",
isSelected(node.id) && "bg-muted"
)}
>
<div
className={cn(
"w-4 h-4 rounded border flex items-center justify-center",
isSelected(node.id)
? "bg-primary border-primary"
: "border-border"
)}
>
{isSelected(node.id) && (
<Check className="w-3 h-3 text-primary-foreground" />
)}
</div>
<div className="flex-1 min-w-0">
<div className="text-sm font-medium text-foreground truncate">
{node.label}
</div>
{node.type && (
<div className="text-xs text-muted-foreground">
{node.type}
</div>
)}
</div>
</div>
))}
</div>
)}
</div>
</div>
);
}
);
MultiNodeSelector.displayName = "MultiNodeSelector";
export { MultiNodeSelector };

View File

@@ -0,0 +1,54 @@
import * as React from "react";
import { useIntl } from "react-intl";
import { cn } from "@/lib/utils";
export interface VariablePickerProps extends Omit<React.SelectHTMLAttributes<HTMLSelectElement>, 'onChange'> {
options: string[];
value?: string;
onChange?: (value: string) => void;
placeholder?: string;
emptyMessage?: string;
}
const VariablePicker = React.forwardRef<HTMLSelectElement, VariablePickerProps>(
({ className, options, value, onChange, placeholder, emptyMessage, ...props }, ref) => {
const { formatMessage } = useIntl();
const handleChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
onChange?.(e.target.value);
};
return (
<select
ref={ref}
value={value || ''}
onChange={handleChange}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
{placeholder && (
<option value="">
{placeholder}
</option>
)}
{options.length === 0 ? (
<option value="" disabled>
{emptyMessage || formatMessage({ id: 'orchestrator.variablePicker.empty' })}
</option>
) : (
options.map((option) => (
<option key={option} value={option}>
{option}
</option>
))
)}
</select>
);
}
);
VariablePicker.displayName = "VariablePicker";
export { VariablePicker };

View File

@@ -136,6 +136,13 @@
"tipLabel": "Tip:",
"tip": "Connect nodes by dragging from output to input handles"
},
"variablePicker": {
"empty": "No variables available"
},
"multiNodeSelector": {
"empty": "No nodes available",
"clear": "Clear all"
},
"propertyPanel": {
"title": "Properties",
"open": "Open properties panel",
@@ -175,8 +182,8 @@
"failFast": "Fail fast (stop all branches on first error)"
},
"options": {
"modeAnalysis": "Analysis (Read-only)",
"modeWrite": "Write (Modify files)",
"modeMainprocess": "Main Process",
"modeAsync": "Async",
"errorStop": "Stop execution",
"errorContinue": "Continue",
"errorRetry": "Retry",

View File

@@ -135,6 +135,13 @@
"tipLabel": "提示:",
"tip": "通过从输出拖动到输入句柄来连接节点"
},
"variablePicker": {
"empty": "没有可用的变量"
},
"multiNodeSelector": {
"empty": "没有可用的节点",
"clear": "清除全部"
},
"propertyPanel": {
"title": "属性",
"open": "打开属性面板",
@@ -174,8 +181,8 @@
"failFast": "快速失败 (首次错误时停止所有分支)"
},
"options": {
"modeAnalysis": "分析 (只读)",
"modeWrite": "写入 (修改文件)",
"modeMainprocess": "主进程",
"modeAsync": "异步",
"errorStop": "停止执行",
"errorContinue": "继续",
"errorRetry": "重试",

View File

@@ -74,16 +74,16 @@ function SlashCommandProperties({
<div>
<label className="block text-sm font-medium text-foreground mb-1">{formatMessage({ id: 'orchestrator.propertyPanel.labels.executionMode' })}</label>
<select
value={data.execution?.mode || 'analysis'}
value={data.execution?.mode || 'mainprocess'}
onChange={(e) =>
onChange({
execution: { ...data.execution, mode: e.target.value as 'analysis' | 'write' },
execution: { ...data.execution, mode: e.target.value as 'mainprocess' | 'async' },
})
}
className="w-full h-10 px-3 rounded-md border border-border bg-background text-foreground text-sm"
>
<option value="analysis">{formatMessage({ id: 'orchestrator.propertyPanel.options.modeAnalysis' })}</option>
<option value="write">{formatMessage({ id: 'orchestrator.propertyPanel.options.modeWrite' })}</option>
<option value="mainprocess">{formatMessage({ id: 'orchestrator.propertyPanel.options.modeMainprocess' })}</option>
<option value="async">{formatMessage({ id: 'orchestrator.propertyPanel.options.modeAsync' })}</option>
</select>
</div>
@@ -109,7 +109,7 @@ function SlashCommandProperties({
onChange({
execution: {
...data.execution,
mode: data.execution?.mode || 'analysis',
mode: data.execution?.mode || 'mainprocess',
timeout: e.target.value ? parseInt(e.target.value) : undefined,
},
})
@@ -117,6 +117,15 @@ function SlashCommandProperties({
placeholder={formatMessage({ id: 'orchestrator.propertyPanel.placeholders.timeout' })}
/>
</div>
<div>
<label className="block text-sm font-medium text-foreground mb-1">{formatMessage({ id: 'orchestrator.propertyPanel.labels.outputVariable' })}</label>
<Input
value={data.outputVariable || ''}
onChange={(e) => onChange({ outputVariable: e.target.value })}
placeholder={formatMessage({ id: 'orchestrator.propertyPanel.placeholders.variableName' })}
/>
</div>
</div>
);
}
@@ -270,6 +279,15 @@ function ConditionalProperties({
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-foreground mb-1">{formatMessage({ id: 'orchestrator.propertyPanel.labels.outputVariable' })}</label>
<Input
value={data.outputVariable || ''}
onChange={(e) => onChange({ outputVariable: e.target.value })}
placeholder={formatMessage({ id: 'orchestrator.propertyPanel.placeholders.variableName' })}
/>
</div>
</div>
);
}
@@ -334,6 +352,15 @@ function ParallelProperties({
{formatMessage({ id: 'orchestrator.propertyPanel.labels.failFast' })}
</label>
</div>
<div>
<label className="block text-sm font-medium text-foreground mb-1">{formatMessage({ id: 'orchestrator.propertyPanel.labels.outputVariable' })}</label>
<Input
value={data.outputVariable || ''}
onChange={(e) => onChange({ outputVariable: e.target.value })}
placeholder={formatMessage({ id: 'orchestrator.propertyPanel.placeholders.variableName' })}
/>
</div>
</div>
);
}

View File

@@ -17,12 +17,12 @@ interface SlashCommandNodeProps {
// Mode badge styling
const MODE_STYLES = {
analysis: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400',
write: 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400',
mainprocess: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400',
async: 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400',
};
export const SlashCommandNode = memo(({ data, selected }: SlashCommandNodeProps) => {
const executionMode = data.execution?.mode || 'analysis';
const executionMode = data.execution?.mode || 'mainprocess';
return (
<NodeWrapper

View File

@@ -18,6 +18,7 @@ interface BaseNodeData {
executionStatus?: ExecutionStatus;
executionError?: string;
executionResult?: unknown;
outputVariable?: string;
[key: string]: unknown;
}
@@ -26,7 +27,7 @@ export interface SlashCommandNodeData extends BaseNodeData {
command: string;
args?: string;
execution: {
mode: 'analysis' | 'write';
mode: 'mainprocess' | 'async';
timeout?: number;
};
contextHint?: string;
@@ -191,7 +192,7 @@ export const NODE_TYPE_CONFIGS: Record<FlowNodeType, NodeTypeConfig> = {
label: 'New Command',
command: '',
args: '',
execution: { mode: 'analysis' },
execution: { mode: 'mainprocess' },
onError: 'stop',
} as SlashCommandNodeData,
handles: { inputs: 1, outputs: 1 },