This commit is contained in:
Typer_Body
2026-05-23 02:58:17 +08:00
parent 127198675e
commit 8ebfcd963a
13 changed files with 728 additions and 173 deletions

View File

@@ -68,6 +68,7 @@ import {
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import ModelsDialog from '@/app/home/components/models-dialog/ModelsDialog';
import PromptEditorComponent from '@/app/home/components/dynamic-form/PromptEditorComponent';
const resolveOptionLabel = (label: unknown, fallback: string): string => {
if (!label || typeof label !== 'object') return fallback;

View File

@@ -0,0 +1,131 @@
import { useState, useEffect } from 'react';
import { Button } from '@/components/ui/button';
import { Textarea } from '@/components/ui/textarea';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Plus, Trash2 } from 'lucide-react';
import { useTranslation } from 'react-i18next';
interface PromptEntry {
role: string;
content: string;
}
interface PromptEditorProps {
value: PromptEntry[];
onChange: (value: PromptEntry[]) => void;
}
const ROLE_OPTIONS = [
{ value: 'system', label: 'System' },
{ value: 'user', label: 'User' },
{ value: 'assistant', label: 'Assistant' },
];
export default function PromptEditorComponent({
value,
onChange,
}: PromptEditorProps) {
const { t } = useTranslation();
const [entries, setEntries] = useState<PromptEntry[]>(
Array.isArray(value) && value.length > 0
? value
: [{ role: 'system', content: '' }],
);
// Sync with external value changes
useEffect(() => {
if (Array.isArray(value) && value.length > 0) {
setEntries(value);
}
}, [value]);
const updateEntries = (newEntries: PromptEntry[]) => {
setEntries(newEntries);
onChange(newEntries);
};
const handleRoleChange = (index: number, role: string) => {
const newEntries = [...entries];
newEntries[index] = { ...newEntries[index], role };
updateEntries(newEntries);
};
const handleContentChange = (index: number, content: string) => {
const newEntries = [...entries];
newEntries[index] = { ...newEntries[index], content };
updateEntries(newEntries);
};
const handleAddEntry = () => {
updateEntries([...entries, { role: 'system', content: '' }]);
};
const handleRemoveEntry = (index: number) => {
if (entries.length <= 1) return;
const newEntries = entries.filter((_, i) => i !== index);
updateEntries(newEntries);
};
return (
<div className="space-y-3 w-full">
{entries.map((entry, index) => (
<div
key={index}
className="flex gap-2 items-start p-3 rounded-lg border bg-card"
>
<div className="w-32 flex-shrink-0">
<Select
value={entry.role}
onValueChange={(role) => handleRoleChange(index, role)}
>
<SelectTrigger className="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
{ROLE_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex-1">
<Textarea
value={entry.content}
onChange={(e) => handleContentChange(index, e.target.value)}
placeholder={t('workflows.promptContentPlaceholder', 'Enter prompt content...')}
className="min-h-[80px] resize-y"
rows={3}
/>
</div>
<Button
type="button"
variant="ghost"
size="icon"
className="shrink-0 text-muted-foreground hover:text-destructive mt-1"
onClick={() => handleRemoveEntry(index)}
disabled={entries.length <= 1}
>
<Trash2 className="size-4" />
</Button>
</div>
))}
<Button
type="button"
variant="outline"
className="w-full border-dashed text-muted-foreground hover:text-foreground"
onClick={handleAddEntry}
>
<Plus className="size-4 mr-1.5" />
{t('workflows.addPromptEntry', 'Add Prompt Entry')}
</Button>
</div>
);
}

View File

@@ -55,7 +55,6 @@ export default function WorkflowDetailContent({ id }: { id: string }) {
const [activeTab, setActiveTab] = useState('editor');
const [workflow, setWorkflow] = useState<Workflow | null>(null);
const [createStep, setCreateStep] = useState<'basic' | 'editor'>('basic');
const [basicInfo, setBasicInfo] = useState<{
name: string;
description: string;
@@ -63,7 +62,7 @@ export default function WorkflowDetailContent({ id }: { id: string }) {
}>({
name: '',
description: '',
emoji: '🔄',
emoji: '💼',
});
const fileInputRef = useRef<HTMLInputElement>(null);
const [isWebSocketConnected, setIsWebSocketConnected] = useState(false);
@@ -136,8 +135,8 @@ export default function WorkflowDetailContent({ id }: { id: string }) {
name: basicInfo.name || t('workflows.newWorkflow'),
description: basicInfo.description,
emoji: basicInfo.emoji,
nodes,
edges,
nodes: [],
edges: [],
});
refreshWorkflows();
navigate(`/home/workflows?id=${encodeURIComponent(resp.uuid)}`);
@@ -330,7 +329,7 @@ export default function WorkflowDetailContent({ id }: { id: string }) {
}, [workflow, refreshWorkflows, navigate, t]);
// ==================== Create Mode ====================
if (isCreateMode && createStep === 'basic') {
if (isCreateMode) {
return (
<div className="flex h-full flex-col">
<div className="flex items-center justify-between pb-4 shrink-0">
@@ -352,11 +351,8 @@ export default function WorkflowDetailContent({ id }: { id: string }) {
<Upload className="size-4 mr-1" />
{t('workflows.import')}
</Button>
<Button
onClick={() => setCreateStep('editor')}
disabled={!basicInfo.name.trim()}
>
{t('common.next')}
<Button onClick={handleSave} disabled={isSaving || !basicInfo.name.trim()}>
{isSaving ? t('common.saving') : t('common.create')}
</Button>
</div>
</div>
@@ -417,30 +413,6 @@ export default function WorkflowDetailContent({ id }: { id: string }) {
);
}
if (isCreateMode) {
return (
<div className="flex h-full flex-col">
<div className="flex items-center justify-between pb-4 shrink-0">
<h1 className="text-xl font-semibold">
{t('workflows.createWorkflow')}
</h1>
<div className="flex gap-2">
<Button variant="outline" onClick={() => setCreateStep('basic')}>
{t('common.back')}
</Button>
<Button onClick={handleSave} disabled={isSaving}>
{isSaving ? t('common.saving') : t('common.create')}
</Button>
</div>
</div>
<div className="flex-1 min-h-0">
<WorkflowEditorComponent />
</div>
</div>
);
}
// ==================== Edit Mode ====================
return (
<div className="flex h-full flex-col">

View File

@@ -457,6 +457,7 @@ function WorkflowEditorInner() {
},
}}
deleteKeyCode={null} // We handle delete manually
// proOptions={{ hideAttribution: true }} Fack React Flow , we will never give you money, stop asking me to pay for this amazing library that I use for free and contribute to open source.
>
<Background
gap={15}
@@ -696,3 +697,4 @@ export default function WorkflowEditorComponent() {
</ReactFlowProvider>
);
}