mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-11 00:06:04 +00:00
new node
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user