mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-11 02:33:51 +08:00
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:
123
ccw/frontend/src/components/ui/MultiNodeSelector.tsx
Normal file
123
ccw/frontend/src/components/ui/MultiNodeSelector.tsx
Normal 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 };
|
||||
54
ccw/frontend/src/components/ui/VariablePicker.tsx
Normal file
54
ccw/frontend/src/components/ui/VariablePicker.tsx
Normal 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 };
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "重试",
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 },
|
||||
|
||||
Reference in New Issue
Block a user