mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-07-08 21:56:06 +00:00
后端没修完版
This commit is contained in:
@@ -12,8 +12,25 @@ import {
|
||||
} from '@/components/ui/form';
|
||||
import DynamicFormItemComponent from '@/app/home/components/dynamic-form/DynamicFormItemComponent';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { extractI18nObject } from '@/i18n/I18nProvider';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import i18n from 'i18next';
|
||||
import { resolveI18nLabel, maybeTranslateKey } from '@/app/home/workflows/components/workflow-editor/workflow-i18n';
|
||||
|
||||
// Helper function to translate i18n key if the value is an i18n key string
|
||||
const translateIfKey = (value: string | undefined): string | undefined => {
|
||||
if (!value) return value;
|
||||
const translated = maybeTranslateKey(value);
|
||||
return translated || value;
|
||||
};
|
||||
|
||||
// Helper to extract i18n label and translate if it's an i18n key
|
||||
const extractAndTranslateI18n = (label: any): string => {
|
||||
if (!label) return '';
|
||||
if (typeof label === 'string') {
|
||||
return translateIfKey(label) || label;
|
||||
}
|
||||
return resolveI18nLabel(label) || '';
|
||||
};
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -34,6 +51,9 @@ function resolveShowIfValue(
|
||||
externalDependentValues?: Record<string, unknown>,
|
||||
systemContext?: Record<string, unknown>,
|
||||
): unknown {
|
||||
if (!field || typeof field !== 'string') {
|
||||
return undefined;
|
||||
}
|
||||
if (field.startsWith('__system.')) {
|
||||
const key = field.slice('__system.'.length);
|
||||
return systemContext?.[key];
|
||||
@@ -200,6 +220,38 @@ export default function DynamicFormComponent({
|
||||
// Default to a single empty system prompt entry
|
||||
return [{ role: 'system', content: '' }];
|
||||
}
|
||||
if (
|
||||
item.type === 'string' ||
|
||||
item.type === 'text' ||
|
||||
item.type === 'secret' ||
|
||||
item.type === 'select' ||
|
||||
item.type === 'llm-model-selector' ||
|
||||
item.type === 'embedding-model-selector' ||
|
||||
item.type === 'rerank-model-selector' ||
|
||||
item.type === 'knowledge-base-selector' ||
|
||||
item.type === 'bot-selector'
|
||||
) {
|
||||
return typeof value === 'string' ? value : '';
|
||||
}
|
||||
if (
|
||||
item.type === 'array[string]' ||
|
||||
item.type === 'knowledge-base-multi-selector' ||
|
||||
item.type === 'tools-selector'
|
||||
) {
|
||||
return Array.isArray(value)
|
||||
? value.filter((item): item is string => typeof item === 'string')
|
||||
: [];
|
||||
}
|
||||
if (item.type === 'boolean') {
|
||||
return typeof value === 'boolean' ? value : Boolean(value);
|
||||
}
|
||||
if (item.type === 'integer' || item.type === 'float') {
|
||||
return typeof value === 'number' && !Number.isNaN(value)
|
||||
? value
|
||||
: typeof item.default === 'number'
|
||||
? item.default
|
||||
: 0;
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
@@ -390,7 +442,7 @@ export default function DynamicFormComponent({
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-4 w-full overflow-x-hidden">
|
||||
{itemConfigList.map((config) => {
|
||||
if (config.show_if) {
|
||||
const dependValue = resolveShowIfValue(
|
||||
@@ -434,11 +486,13 @@ export default function DynamicFormComponent({
|
||||
|
||||
return (
|
||||
<WebhookUrlField
|
||||
key={config.id}
|
||||
label={extractI18nObject(config.label)}
|
||||
key={`${config.id}-${config.name}`}
|
||||
label={extractAndTranslateI18n(config.label)}
|
||||
description={
|
||||
config.description
|
||||
? extractI18nObject(config.description)
|
||||
? (typeof config.description === 'string'
|
||||
? (config.description.startsWith('workflows.') ? String(t(config.description)) : config.description)
|
||||
: extractAndTranslateI18n(config.description))
|
||||
: undefined
|
||||
}
|
||||
url={webhookUrl}
|
||||
@@ -451,24 +505,26 @@ export default function DynamicFormComponent({
|
||||
if (config.type === 'boolean') {
|
||||
return (
|
||||
<FormField
|
||||
key={config.id}
|
||||
key={`${config.id}-${config.name}`}
|
||||
control={form.control}
|
||||
name={config.name as keyof FormValues}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-row items-center justify-between rounded-lg border p-4 max-w-2xl',
|
||||
'flex flex-row items-center justify-between rounded-lg border p-4 w-full max-w-full overflow-hidden',
|
||||
isFieldDisabled && 'pointer-events-none opacity-60',
|
||||
)}
|
||||
>
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel className="text-base">
|
||||
{extractI18nObject(config.label)}
|
||||
{extractAndTranslateI18n(config.label)}
|
||||
</FormLabel>
|
||||
{config.description && (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{extractI18nObject(config.description)}
|
||||
{typeof config.description === 'string'
|
||||
? (config.description.startsWith('workflows.') ? String(t(config.description)) : translateIfKey(config.description))
|
||||
: extractAndTranslateI18n(config.description)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
@@ -489,13 +545,18 @@ export default function DynamicFormComponent({
|
||||
|
||||
return (
|
||||
<FormField
|
||||
key={config.id}
|
||||
key={`${config.id}-${config.name}`}
|
||||
control={form.control}
|
||||
name={config.name as keyof FormValues}
|
||||
render={({ field }) => (
|
||||
render={({ field }) => {
|
||||
// Use the i18n label from config.label (I18nObject), falling back to config.name
|
||||
const i18nLabel = config.label
|
||||
? extractAndTranslateI18n(config.label)
|
||||
: config.name;
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{extractI18nObject(config.label)}{' '}
|
||||
{i18nLabel}{' '}
|
||||
{config.required && <span className="text-red-500">*</span>}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
@@ -511,14 +572,20 @@ export default function DynamicFormComponent({
|
||||
/>
|
||||
</div>
|
||||
</FormControl>
|
||||
{config.description && (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{extractI18nObject(config.description)}
|
||||
</p>
|
||||
)}
|
||||
{config.description && (() => {
|
||||
const desc = config.description;
|
||||
if (typeof desc === 'string') {
|
||||
if (desc.startsWith('workflows.')) {
|
||||
return <p className="text-sm text-muted-foreground">{String(t(desc))}</p>;
|
||||
}
|
||||
return <p className="text-sm text-muted-foreground">{translateIfKey(desc) || desc}</p>;
|
||||
}
|
||||
return <p className="text-sm text-muted-foreground">{extractAndTranslateI18n(desc)}</p>;
|
||||
})()}
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
IDynamicFormItemSchema,
|
||||
IFileConfig,
|
||||
} from '@/app/infra/entities/form/dynamic';
|
||||
import type { I18nObject } from '@/app/infra/entities/common';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import {
|
||||
Select,
|
||||
@@ -28,7 +29,7 @@ import {
|
||||
} from '@/app/infra/entities/api';
|
||||
import { toast } from 'sonner';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { extractI18nObject } from '@/i18n/I18nProvider';
|
||||
import { resolveI18nLabel, maybeTranslateKey } from '@/app/home/workflows/components/workflow-editor/workflow-i18n';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import {
|
||||
@@ -43,6 +44,7 @@ import {
|
||||
Plus,
|
||||
X,
|
||||
Eye,
|
||||
EyeOff,
|
||||
Wrench,
|
||||
Trash2,
|
||||
Sparkles,
|
||||
@@ -63,6 +65,31 @@ import {
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import ModelsDialog from '@/app/home/components/models-dialog/ModelsDialog';
|
||||
|
||||
const resolveOptionLabel = (
|
||||
label: unknown,
|
||||
fallback: string,
|
||||
): string => {
|
||||
if (!label || typeof label !== 'object') return fallback;
|
||||
return resolveI18nLabel(label as Record<string, string> | I18nObject) || fallback;
|
||||
};
|
||||
|
||||
const getSelectedOptionLabel = (
|
||||
options: IDynamicFormItemSchema['options'],
|
||||
value: unknown,
|
||||
): string | null => {
|
||||
if (typeof value !== 'string' || !options?.length) return null;
|
||||
const matched = options.find((option) => option.name === value);
|
||||
if (!matched) return null;
|
||||
return resolveOptionLabel(matched.label, matched.name);
|
||||
};
|
||||
|
||||
const resolveModelLabel = (model: {
|
||||
name: string;
|
||||
display_name?: string;
|
||||
}): string => {
|
||||
return maybeTranslateKey(model.display_name || model.name) || model.display_name || model.name;
|
||||
};
|
||||
|
||||
export default function DynamicFormItemComponent({
|
||||
config,
|
||||
field,
|
||||
@@ -88,6 +115,7 @@ export default function DynamicFormItemComponent({
|
||||
);
|
||||
const { t } = useTranslation();
|
||||
const [modelsDialogOpen, setModelsDialogOpen] = useState(false);
|
||||
const [secretVisible, setSecretVisible] = useState(false);
|
||||
|
||||
const fetchLlmModels = () => {
|
||||
httpClient
|
||||
@@ -280,7 +308,7 @@ export default function DynamicFormItemComponent({
|
||||
onClick={() => field.onChange(option.name)}
|
||||
>
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<span>{extractI18nObject(option.label)}</span>
|
||||
<span>{resolveOptionLabel(option.label, option.name)}</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{option.name}
|
||||
</span>
|
||||
@@ -292,24 +320,65 @@ export default function DynamicFormItemComponent({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return <Input className="max-w-md" {...field} />;
|
||||
return <Input className="max-w-md" {...field} value={field.value ?? ''} />;
|
||||
|
||||
case DynamicFormItemType.SECRET:
|
||||
const secretValue = typeof field.value === 'string' ? field.value : '';
|
||||
const secretPlaceholder = resolveI18nLabel(config.label) || config.name;
|
||||
return (
|
||||
<div className="max-w-md flex items-center gap-1.5">
|
||||
<Input
|
||||
className="flex-1 transition-colors hover:bg-muted/60"
|
||||
type={secretVisible ? 'text' : 'password'}
|
||||
autoComplete="off"
|
||||
placeholder={secretPlaceholder}
|
||||
value={secretValue}
|
||||
onChange={(e) => field.onChange(e.target.value)}
|
||||
onBlur={field.onBlur}
|
||||
name={field.name}
|
||||
ref={field.ref}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="h-9 w-9 shrink-0 text-muted-foreground hover:bg-muted/60 hover:text-foreground"
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
onClick={() => setSecretVisible((prev) => !prev)}
|
||||
>
|
||||
{secretVisible ? <EyeOff className="size-4" /> : <Eye className="size-4" />}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
||||
case DynamicFormItemType.TEXT:
|
||||
return <Textarea {...field} className="min-h-[120px] max-w-2xl" />;
|
||||
// Ensure value is always a string to avoid [object Object] display
|
||||
const textValue = typeof field.value === 'string'
|
||||
? field.value
|
||||
: (field.value != null ? JSON.stringify(field.value, null, 2) : '');
|
||||
return (
|
||||
<Textarea
|
||||
{...field}
|
||||
value={textValue}
|
||||
onChange={(e) => field.onChange(e.target.value)}
|
||||
className="min-h-[120px] max-w-2xl"
|
||||
/>
|
||||
);
|
||||
|
||||
case DynamicFormItemType.BOOLEAN:
|
||||
return <Switch checked={field.value} onCheckedChange={field.onChange} />;
|
||||
return <Switch checked={!!field.value} onCheckedChange={field.onChange} />;
|
||||
|
||||
case DynamicFormItemType.STRING_ARRAY:
|
||||
const arrayValue = Array.isArray(field.value) ? field.value : [];
|
||||
return (
|
||||
<div className="space-y-2 max-w-md">
|
||||
{field.value.map((item: string, index: number) => (
|
||||
{arrayValue.map((item: string, index: number) => (
|
||||
<div key={index} className="flex gap-1.5 items-center">
|
||||
<Input
|
||||
className="flex-1"
|
||||
value={item}
|
||||
value={item ?? ''}
|
||||
onChange={(e) => {
|
||||
const newValue = [...field.value];
|
||||
const newValue = [...(Array.isArray(field.value) ? field.value : [])];
|
||||
newValue[index] = e.target.value;
|
||||
field.onChange(newValue);
|
||||
}}
|
||||
@@ -320,7 +389,7 @@ export default function DynamicFormItemComponent({
|
||||
size="icon"
|
||||
className="shrink-0 text-muted-foreground hover:text-destructive"
|
||||
onClick={() => {
|
||||
const newValue = field.value.filter(
|
||||
const newValue = (Array.isArray(field.value) ? field.value : []).filter(
|
||||
(_: string, i: number) => i !== index,
|
||||
);
|
||||
field.onChange(newValue);
|
||||
@@ -335,7 +404,7 @@ export default function DynamicFormItemComponent({
|
||||
variant="outline"
|
||||
className="w-full border-dashed text-muted-foreground hover:text-foreground"
|
||||
onClick={() => {
|
||||
field.onChange([...field.value, '']);
|
||||
field.onChange([...(Array.isArray(field.value) ? field.value : []), '']);
|
||||
}}
|
||||
>
|
||||
<Plus className="size-4 mr-1.5" />
|
||||
@@ -345,20 +414,24 @@ export default function DynamicFormItemComponent({
|
||||
);
|
||||
|
||||
case DynamicFormItemType.SELECT:
|
||||
const selectedOptionLabel = getSelectedOptionLabel(config.options, field.value);
|
||||
return (
|
||||
<Select value={field.value} onValueChange={field.onChange}>
|
||||
<SelectTrigger className="max-w-md bg-[#ffffff] dark:bg-[#2a2a2e]">
|
||||
<SelectValue placeholder={t('common.select')} />
|
||||
<Select
|
||||
value={typeof field.value === 'string' ? field.value : ''}
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
<SelectTrigger className="max-w-md bg-[#ffffff] dark:bg-[#2a2a2e] hover:bg-muted/60 transition-colors">
|
||||
{selectedOptionLabel ? (
|
||||
<span className="truncate">{selectedOptionLabel}</span>
|
||||
) : (
|
||||
<SelectValue placeholder={t('common.select')} />
|
||||
)}
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{config.options?.map((option) => (
|
||||
<SelectItem
|
||||
key={option.name}
|
||||
value={option.name}
|
||||
description={option.name}
|
||||
>
|
||||
{extractI18nObject(option.label)}
|
||||
<SelectItem key={option.name} value={option.name}>
|
||||
{resolveOptionLabel(option.label, option.name)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
@@ -411,18 +484,18 @@ export default function DynamicFormItemComponent({
|
||||
return (
|
||||
<div className="max-w-md flex items-center gap-1.5">
|
||||
<div className="flex-1">
|
||||
<Select value={field.value} onValueChange={field.onChange}>
|
||||
<Select value={field.value ?? ''} onValueChange={field.onChange}>
|
||||
<SelectTrigger className="bg-[#ffffff] dark:bg-[#2a2a2e]">
|
||||
<SelectValue placeholder={t('models.selectModel')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{Object.entries(groupedModels).map(([providerName, models]) => (
|
||||
<SelectGroup key={providerName}>
|
||||
<SelectGroup key={`llm-regular-${providerName}`}>
|
||||
<SelectLabel>{providerName}</SelectLabel>
|
||||
{models.map((model) => (
|
||||
<SelectItem key={model.uuid} value={model.uuid}>
|
||||
<span className="inline-flex items-center gap-1">
|
||||
{model.name}
|
||||
{resolveModelLabel(model)}
|
||||
{model.abilities?.includes('vision') && (
|
||||
<Eye className="h-3 w-3 text-muted-foreground" />
|
||||
)}
|
||||
@@ -464,12 +537,12 @@ export default function DynamicFormItemComponent({
|
||||
: previewModelNames
|
||||
)
|
||||
.slice(0, 3)
|
||||
.map((name) => (
|
||||
.map((name, index) => (
|
||||
<div
|
||||
key={name}
|
||||
key={`llm-preview-${name}-${index}`}
|
||||
className="relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm text-muted-foreground/60"
|
||||
>
|
||||
{name}
|
||||
{maybeTranslateKey(name) || name}
|
||||
</div>
|
||||
))}
|
||||
{/* Blurred remaining models with login overlay */}
|
||||
@@ -483,12 +556,12 @@ export default function DynamicFormItemComponent({
|
||||
: previewModelNames
|
||||
)
|
||||
.slice(3)
|
||||
.map((name) => (
|
||||
.map((name, index) => (
|
||||
<div
|
||||
key={name}
|
||||
key={`llm-preview-blur-${name}-${index}`}
|
||||
className="flex w-full items-center py-1.5 pl-8 pr-2 text-sm text-muted-foreground/40 blur-[2px]"
|
||||
>
|
||||
{name}
|
||||
{maybeTranslateKey(name) || name}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -516,7 +589,7 @@ export default function DynamicFormItemComponent({
|
||||
// User is logged into Space — show space models normally
|
||||
Object.entries(groupedSpaceModels).map(
|
||||
([providerName, models]) => (
|
||||
<SelectGroup key={providerName}>
|
||||
<SelectGroup key={`llm-space-${providerName}`}>
|
||||
<SelectLabel>
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<Sparkles className="h-3.5 w-3.5 text-purple-500" />
|
||||
@@ -526,7 +599,7 @@ export default function DynamicFormItemComponent({
|
||||
{models.map((model) => (
|
||||
<SelectItem key={model.uuid} value={model.uuid}>
|
||||
<span className="inline-flex items-center gap-1">
|
||||
{model.name}
|
||||
{resolveModelLabel(model)}
|
||||
{model.abilities?.includes('vision') && (
|
||||
<Eye className="h-3 w-3 text-muted-foreground" />
|
||||
)}
|
||||
@@ -578,18 +651,18 @@ export default function DynamicFormItemComponent({
|
||||
|
||||
return (
|
||||
<div className="max-w-md">
|
||||
<Select value={field.value} onValueChange={field.onChange}>
|
||||
<Select value={field.value ?? ''} onValueChange={field.onChange}>
|
||||
<SelectTrigger className="bg-[#ffffff] dark:bg-[#2a2a2e]">
|
||||
<SelectValue placeholder={t('knowledge.selectEmbeddingModel')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{Object.entries(groupedEmbeddingModels).map(
|
||||
([providerName, models]) => (
|
||||
<SelectGroup key={providerName}>
|
||||
<SelectGroup key={`embedding-${providerName}`}>
|
||||
<SelectLabel>{providerName}</SelectLabel>
|
||||
{models.map((model) => (
|
||||
<SelectItem key={model.uuid} value={model.uuid}>
|
||||
{model.name}
|
||||
{resolveModelLabel(model)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
@@ -624,11 +697,11 @@ export default function DynamicFormItemComponent({
|
||||
<SelectItem value="__none__">{t('common.none')}</SelectItem>
|
||||
{Object.entries(groupedRerankModels).map(
|
||||
([providerName, models]) => (
|
||||
<SelectGroup key={providerName}>
|
||||
<SelectGroup key={`rerank-${providerName}`}>
|
||||
<SelectLabel>{providerName}</SelectLabel>
|
||||
{models.map((model) => (
|
||||
<SelectItem key={model.uuid} value={model.uuid}>
|
||||
{model.name}
|
||||
{resolveModelLabel(model)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
@@ -712,19 +785,19 @@ export default function DynamicFormItemComponent({
|
||||
onChange: (val: string) => void,
|
||||
placeholder: string,
|
||||
) => (
|
||||
<Select value={value} onValueChange={onChange}>
|
||||
<Select value={value ?? ''} onValueChange={onChange}>
|
||||
<SelectTrigger className="bg-[#ffffff] dark:bg-[#2a2a2e]">
|
||||
<SelectValue placeholder={placeholder} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{Object.entries(groupedModelsForFallback).map(
|
||||
([providerName, models]) => (
|
||||
<SelectGroup key={providerName}>
|
||||
<SelectGroup key={`fallback-regular-${providerName}`}>
|
||||
<SelectLabel>{providerName}</SelectLabel>
|
||||
{models.map((model) => (
|
||||
<SelectItem key={model.uuid} value={model.uuid}>
|
||||
<span className="inline-flex items-center gap-1">
|
||||
{model.name}
|
||||
{resolveModelLabel(model)}
|
||||
{model.abilities?.includes('vision') && (
|
||||
<Eye className="h-3 w-3 text-muted-foreground" />
|
||||
)}
|
||||
@@ -767,12 +840,12 @@ export default function DynamicFormItemComponent({
|
||||
: fbPreviewModelNames
|
||||
)
|
||||
.slice(0, 3)
|
||||
.map((name) => (
|
||||
.map((name, index) => (
|
||||
<div
|
||||
key={name}
|
||||
key={`fallback-preview-${name}-${index}`}
|
||||
className="relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm text-muted-foreground/60"
|
||||
>
|
||||
{name}
|
||||
{maybeTranslateKey(name) || name}
|
||||
</div>
|
||||
))}
|
||||
{/* Blurred remaining models with login overlay */}
|
||||
@@ -786,12 +859,12 @@ export default function DynamicFormItemComponent({
|
||||
: fbPreviewModelNames
|
||||
)
|
||||
.slice(3)
|
||||
.map((name) => (
|
||||
.map((name, index) => (
|
||||
<div
|
||||
key={name}
|
||||
key={`fallback-preview-blur-${name}-${index}`}
|
||||
className="flex w-full items-center py-1.5 pl-8 pr-2 text-sm text-muted-foreground/40 blur-[2px]"
|
||||
>
|
||||
{name}
|
||||
{maybeTranslateKey(name) || name}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -819,7 +892,7 @@ export default function DynamicFormItemComponent({
|
||||
// User is logged into Space — show space models normally
|
||||
Object.entries(fbGroupedSpaceModels).map(
|
||||
([providerName, models]) => (
|
||||
<SelectGroup key={providerName}>
|
||||
<SelectGroup key={`fallback-space-${providerName}`}>
|
||||
<SelectLabel>
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<Sparkles className="h-3.5 w-3.5 text-purple-500" />
|
||||
@@ -829,7 +902,7 @@ export default function DynamicFormItemComponent({
|
||||
{models.map((model) => (
|
||||
<SelectItem key={model.uuid} value={model.uuid}>
|
||||
<span className="inline-flex items-center gap-1">
|
||||
{model.name}
|
||||
{resolveModelLabel(model)}
|
||||
{model.abilities?.includes('vision') && (
|
||||
<Eye className="h-3 w-3 text-muted-foreground" />
|
||||
)}
|
||||
@@ -990,7 +1063,7 @@ export default function DynamicFormItemComponent({
|
||||
const kbsByEngine = knowledgeBases.reduce(
|
||||
(acc, kb) => {
|
||||
const engineName = kb.knowledge_engine?.name
|
||||
? extractI18nObject(kb.knowledge_engine.name)
|
||||
? resolveI18nLabel(kb.knowledge_engine.name) || t('knowledge.unknownEngine')
|
||||
: t('knowledge.unknownEngine');
|
||||
if (!acc[engineName]) {
|
||||
acc[engineName] = [];
|
||||
@@ -1002,7 +1075,7 @@ export default function DynamicFormItemComponent({
|
||||
);
|
||||
|
||||
return (
|
||||
<Select value={field.value} onValueChange={field.onChange}>
|
||||
<Select value={field.value ?? '__none__'} onValueChange={field.onChange}>
|
||||
<SelectTrigger className="bg-[#ffffff] dark:bg-[#2a2a2e]">
|
||||
{field.value && field.value !== '__none__' ? (
|
||||
(() => {
|
||||
@@ -1053,7 +1126,7 @@ export default function DynamicFormItemComponent({
|
||||
const multiKbsByEngine = knowledgeBases.reduce(
|
||||
(acc, kb) => {
|
||||
const engineName = kb.knowledge_engine?.name
|
||||
? extractI18nObject(kb.knowledge_engine.name)
|
||||
? resolveI18nLabel(kb.knowledge_engine.name) || t('knowledge.unknownEngine')
|
||||
: t('knowledge.unknownEngine');
|
||||
if (!acc[engineName]) {
|
||||
acc[engineName] = [];
|
||||
@@ -1064,12 +1137,15 @@ export default function DynamicFormItemComponent({
|
||||
{} as Record<string, typeof knowledgeBases>,
|
||||
);
|
||||
|
||||
// Ensure field.value is always an array
|
||||
const safeValue = Array.isArray(field.value) ? field.value : [];
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="space-y-2">
|
||||
{field.value && field.value.length > 0 ? (
|
||||
{safeValue.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
{field.value.map((kbId: string) => {
|
||||
{safeValue.map((kbId: string) => {
|
||||
const currentKb = knowledgeBases.find(
|
||||
(base) => base.uuid === kbId,
|
||||
);
|
||||
@@ -1091,9 +1167,9 @@ export default function DynamicFormItemComponent({
|
||||
{currentKb.name}
|
||||
{currentKb.knowledge_engine?.name && (
|
||||
<span className="text-xs px-2 py-0.5 rounded-full bg-purple-100 text-purple-700 dark:bg-purple-900 dark:text-purple-300">
|
||||
{extractI18nObject(
|
||||
{resolveI18nLabel(
|
||||
currentKb.knowledge_engine.name,
|
||||
)}
|
||||
) || t('knowledge.unknownEngine')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -1109,7 +1185,7 @@ export default function DynamicFormItemComponent({
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => {
|
||||
const newValue = field.value.filter(
|
||||
const newValue = safeValue.filter(
|
||||
(id: string) => id !== kbId,
|
||||
);
|
||||
field.onChange(newValue);
|
||||
@@ -1133,7 +1209,7 @@ export default function DynamicFormItemComponent({
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setTempSelectedKBIds(field.value || []);
|
||||
setTempSelectedKBIds(safeValue);
|
||||
setKbDialogOpen(true);
|
||||
}}
|
||||
variant="outline"
|
||||
@@ -1220,7 +1296,7 @@ export default function DynamicFormItemComponent({
|
||||
|
||||
case DynamicFormItemType.BOT_SELECTOR:
|
||||
return (
|
||||
<Select value={field.value} onValueChange={field.onChange}>
|
||||
<Select value={field.value ?? ''} onValueChange={field.onChange}>
|
||||
<SelectTrigger className="bg-[#ffffff] dark:bg-[#2a2a2e]">
|
||||
<SelectValue placeholder={t('bots.selectBot')} />
|
||||
</SelectTrigger>
|
||||
@@ -1643,6 +1719,6 @@ export default function DynamicFormItemComponent({
|
||||
);
|
||||
|
||||
default:
|
||||
return <Input {...field} />;
|
||||
return <Input {...field} value={field.value ?? ''} />;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ export class DynamicFormItemConfig implements IDynamicFormItemSchema {
|
||||
label: I18nObject;
|
||||
required: boolean;
|
||||
type: DynamicFormItemType;
|
||||
description?: I18nObject;
|
||||
description?: I18nObject | string;
|
||||
options?: IDynamicFormItemOption[];
|
||||
show_if?: IShowIfCondition;
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
import { IDynamicFormItemSchema } from '@/app/infra/entities/form/dynamic';
|
||||
import DynamicFormItemComponent from '@/app/home/components/dynamic-form/DynamicFormItemComponent';
|
||||
import { extractI18nObject } from '@/i18n/I18nProvider';
|
||||
import { maybeTranslateKey } from '@/app/home/workflows/components/workflow-editor/workflow-i18n';
|
||||
|
||||
/**
|
||||
* N8n认证表单组件
|
||||
@@ -179,23 +180,34 @@ export default function N8nAuthFormComponent({
|
||||
key={config.id}
|
||||
control={form.control}
|
||||
name={config.name as keyof FormValues}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{extractI18nObject(config.label)}{' '}
|
||||
{config.required && <span className="text-red-500">*</span>}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<DynamicFormItemComponent config={config} field={field} />
|
||||
</FormControl>
|
||||
{config.description && (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{extractI18nObject(config.description)}
|
||||
</p>
|
||||
)}
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
render={({ field }) => {
|
||||
const labelText =
|
||||
typeof config.label === 'string'
|
||||
? maybeTranslateKey(config.label) || config.label
|
||||
: extractI18nObject(config.label);
|
||||
const descriptionText =
|
||||
typeof config.description === 'string'
|
||||
? maybeTranslateKey(config.description) || config.description
|
||||
: extractI18nObject(config.description);
|
||||
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{labelText || config.name}{' '}
|
||||
{config.required && <span className="text-red-500">*</span>}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<DynamicFormItemComponent config={config} field={field} />
|
||||
</FormControl>
|
||||
{descriptionText && (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{descriptionText}
|
||||
</p>
|
||||
)}
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -116,6 +116,7 @@ function compareVersions(v1: string, v2: string): boolean {
|
||||
const ENTITY_CATEGORY_IDS = [
|
||||
'bots',
|
||||
'pipelines',
|
||||
'workflows',
|
||||
'knowledge',
|
||||
'plugins',
|
||||
'mcp',
|
||||
@@ -126,6 +127,7 @@ type EntityCategoryId = (typeof ENTITY_CATEGORY_IDS)[number];
|
||||
const DETAIL_PAGE_CATEGORIES: EntityCategoryId[] = [
|
||||
'bots',
|
||||
'pipelines',
|
||||
'workflows',
|
||||
'knowledge',
|
||||
'plugins',
|
||||
'mcp',
|
||||
@@ -135,6 +137,7 @@ const DETAIL_PAGE_CATEGORIES: EntityCategoryId[] = [
|
||||
const CREATABLE_CATEGORIES: EntityCategoryId[] = [
|
||||
'bots',
|
||||
'pipelines',
|
||||
'workflows',
|
||||
'knowledge',
|
||||
'mcp',
|
||||
'plugins',
|
||||
@@ -144,6 +147,7 @@ const CREATABLE_CATEGORIES: EntityCategoryId[] = [
|
||||
const COLLAPSIBLE_ONLY_CATEGORIES: EntityCategoryId[] = [
|
||||
'bots',
|
||||
'pipelines',
|
||||
'workflows',
|
||||
'knowledge',
|
||||
'mcp',
|
||||
];
|
||||
@@ -155,10 +159,11 @@ function isEntityCategory(id: string): id is EntityCategoryId {
|
||||
// Map sidebar config IDs to SidebarDataContext keys
|
||||
const ENTITY_KEY_MAP: Record<
|
||||
EntityCategoryId,
|
||||
'bots' | 'pipelines' | 'knowledgeBases' | 'plugins' | 'mcpServers'
|
||||
'bots' | 'pipelines' | 'workflows' | 'knowledgeBases' | 'plugins' | 'mcpServers'
|
||||
> = {
|
||||
bots: 'bots',
|
||||
pipelines: 'pipelines',
|
||||
workflows: 'workflows',
|
||||
knowledge: 'knowledgeBases',
|
||||
plugins: 'plugins',
|
||||
mcp: 'mcpServers',
|
||||
@@ -168,6 +173,7 @@ const ENTITY_KEY_MAP: Record<
|
||||
const ENTITY_ROUTE_MAP: Record<EntityCategoryId, string> = {
|
||||
bots: '/home/bots',
|
||||
pipelines: '/home/pipelines',
|
||||
workflows: '/home/workflows',
|
||||
knowledge: '/home/knowledge',
|
||||
plugins: '/home/plugins',
|
||||
mcp: '/home/mcp',
|
||||
@@ -717,13 +723,13 @@ function NavItems({
|
||||
(isPlugin ? (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="p-1 rounded-sm text-sidebar-foreground/70 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground [@media(hover:hover)]:opacity-0 group-hover/category-header:opacity-100 transition-all"
|
||||
<div
|
||||
role="button"
|
||||
className="p-1 rounded-sm text-sidebar-foreground/70 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground [@media(hover:hover)]:opacity-0 group-hover/category-header:opacity-100 transition-all cursor-pointer"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<Plus className="size-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
{systemInfo.enable_marketplace && (
|
||||
@@ -760,25 +766,25 @@ function NavItems({
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className="p-1 rounded-sm text-sidebar-foreground/70 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground [@media(hover:hover)]:opacity-0 group-hover/category-header:opacity-100 transition-all"
|
||||
<div
|
||||
role="button"
|
||||
className="p-1 rounded-sm text-sidebar-foreground/70 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground [@media(hover:hover)]:opacity-0 group-hover/category-header:opacity-100 transition-all cursor-pointer"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
navigate(`${routePrefix}?id=new`);
|
||||
}}
|
||||
>
|
||||
<Plus className="size-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
<CollapsibleTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="p-1 rounded-sm hover:bg-sidebar-accent"
|
||||
<div
|
||||
role="button"
|
||||
className="p-1 rounded-sm hover:bg-sidebar-accent cursor-pointer"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<ChevronRight className="size-4 transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
|
||||
</button>
|
||||
</div>
|
||||
</CollapsibleTrigger>
|
||||
</div>
|
||||
</SidebarMenuButton>
|
||||
|
||||
@@ -35,11 +35,13 @@ export type PluginInstallAction = 'local' | 'github' | null;
|
||||
export interface SidebarDataContextValue {
|
||||
bots: SidebarEntityItem[];
|
||||
pipelines: SidebarEntityItem[];
|
||||
workflows: SidebarEntityItem[];
|
||||
knowledgeBases: SidebarEntityItem[];
|
||||
plugins: SidebarEntityItem[];
|
||||
mcpServers: SidebarEntityItem[];
|
||||
refreshBots: () => Promise<void>;
|
||||
refreshPipelines: () => Promise<void>;
|
||||
refreshWorkflows: () => Promise<void>;
|
||||
refreshKnowledgeBases: () => Promise<void>;
|
||||
refreshPlugins: () => Promise<void>;
|
||||
refreshMCPServers: () => Promise<void>;
|
||||
@@ -61,6 +63,7 @@ export function SidebarDataProvider({
|
||||
}) {
|
||||
const [bots, setBots] = useState<SidebarEntityItem[]>([]);
|
||||
const [pipelines, setPipelines] = useState<SidebarEntityItem[]>([]);
|
||||
const [workflows, setWorkflows] = useState<SidebarEntityItem[]>([]);
|
||||
const [knowledgeBases, setKnowledgeBases] = useState<SidebarEntityItem[]>([]);
|
||||
const [plugins, setPlugins] = useState<SidebarEntityItem[]>([]);
|
||||
const [mcpServers, setMCPServers] = useState<SidebarEntityItem[]>([]);
|
||||
@@ -103,6 +106,24 @@ export function SidebarDataProvider({
|
||||
}
|
||||
}, []);
|
||||
|
||||
const refreshWorkflows = useCallback(async () => {
|
||||
try {
|
||||
const resp = await httpClient.getWorkflows();
|
||||
setWorkflows(
|
||||
resp.workflows.map((w) => ({
|
||||
id: w.uuid || '',
|
||||
name: w.name,
|
||||
description: w.description,
|
||||
emoji: w.emoji,
|
||||
updatedAt: w.updated_at,
|
||||
enabled: w.is_enabled ?? true,
|
||||
})),
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch workflows for sidebar:', error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const refreshKnowledgeBases = useCallback(async () => {
|
||||
try {
|
||||
const resp = await httpClient.getKnowledgeBases();
|
||||
@@ -189,6 +210,7 @@ export function SidebarDataProvider({
|
||||
await Promise.all([
|
||||
refreshBots(),
|
||||
refreshPipelines(),
|
||||
refreshWorkflows(),
|
||||
refreshKnowledgeBases(),
|
||||
refreshPlugins(),
|
||||
refreshMCPServers(),
|
||||
@@ -196,6 +218,7 @@ export function SidebarDataProvider({
|
||||
}, [
|
||||
refreshBots,
|
||||
refreshPipelines,
|
||||
refreshWorkflows,
|
||||
refreshKnowledgeBases,
|
||||
refreshPlugins,
|
||||
refreshMCPServers,
|
||||
@@ -211,11 +234,13 @@ export function SidebarDataProvider({
|
||||
value={{
|
||||
bots,
|
||||
pipelines,
|
||||
workflows,
|
||||
knowledgeBases,
|
||||
plugins,
|
||||
mcpServers,
|
||||
refreshBots,
|
||||
refreshPipelines,
|
||||
refreshWorkflows,
|
||||
refreshKnowledgeBases,
|
||||
refreshPlugins,
|
||||
refreshMCPServers,
|
||||
|
||||
@@ -95,6 +95,27 @@ export const sidebarConfigList = [
|
||||
},
|
||||
section: 'home',
|
||||
}),
|
||||
new SidebarChildVO({
|
||||
id: 'workflows',
|
||||
name: t('workflows.title'),
|
||||
icon: (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
className="text-blue-500"
|
||||
>
|
||||
<path d="M2 3C2 2.44772 2.44772 2 3 2H7C7.55228 2 8 2.44772 8 3V7C8 7.55228 7.55228 8 7 8H5V11H11V9C11 8.44772 11.4477 8 12 8H21C21.5523 8 22 8.44772 22 9V13C22 13.5523 21.5523 14 21 14H12C11.4477 14 11 13.5523 11 13V12H5V17H11V15C11 14.4477 11.4477 14 12 14H21C21.5523 14 22 14.4477 22 15V19C22 19.5523 21.5523 20 21 20H12C11.4477 20 11 19.5523 11 19V18H4C3.44772 18 3 17.5523 3 17V8H3C2.44772 8 2 7.55228 2 7V3ZM4 4V6H6V4H4ZM13 10V12H20V10H13ZM13 16V18H20V16H13Z"></path>
|
||||
</svg>
|
||||
),
|
||||
route: '/home/workflows',
|
||||
description: t('workflows.description'),
|
||||
helpLink: {
|
||||
en_US: '',
|
||||
zh_Hans: '',
|
||||
},
|
||||
section: 'home',
|
||||
}),
|
||||
new SidebarChildVO({
|
||||
id: 'knowledge',
|
||||
name: t('knowledge.title'),
|
||||
|
||||
@@ -0,0 +1,235 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSidebarData, SidebarEntityItem } from '../home-sidebar/SidebarDataContext';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@/components/ui/popover';
|
||||
import { Check, ChevronsUpDown, GitBranch, Workflow } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
|
||||
export type BindingType = 'pipeline' | 'workflow';
|
||||
|
||||
export interface BindingValue {
|
||||
type: BindingType;
|
||||
id: string | null;
|
||||
}
|
||||
|
||||
interface UnifiedBindingSelectorProps {
|
||||
value: BindingValue;
|
||||
onChange: (value: BindingValue) => void;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function UnifiedBindingSelector({
|
||||
value,
|
||||
onChange,
|
||||
disabled = false,
|
||||
className,
|
||||
}: UnifiedBindingSelectorProps) {
|
||||
const { t } = useTranslation();
|
||||
const { pipelines, workflows, refreshPipelines, refreshWorkflows } = useSidebarData();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
refreshPipelines();
|
||||
refreshWorkflows();
|
||||
}, [refreshPipelines, refreshWorkflows]);
|
||||
|
||||
// Get current selection display
|
||||
const getSelectionDisplay = () => {
|
||||
if (!value.id) {
|
||||
return t('bots.selectBinding');
|
||||
}
|
||||
|
||||
if (value.type === 'pipeline') {
|
||||
const pipeline = pipelines.find((p) => p.id === value.id);
|
||||
return pipeline ? (
|
||||
<div className="flex items-center gap-2">
|
||||
{pipeline.emoji && <span>{pipeline.emoji}</span>}
|
||||
<span>{pipeline.name}</span>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
Pipeline
|
||||
</Badge>
|
||||
</div>
|
||||
) : (
|
||||
value.id
|
||||
);
|
||||
} else {
|
||||
const workflow = workflows.find((w) => w.id === value.id);
|
||||
return workflow ? (
|
||||
<div className="flex items-center gap-2">
|
||||
{workflow.emoji && <span>{workflow.emoji}</span>}
|
||||
<span>{workflow.name}</span>
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
Workflow
|
||||
</Badge>
|
||||
</div>
|
||||
) : (
|
||||
value.id
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle type change
|
||||
const handleTypeChange = (newType: BindingType) => {
|
||||
onChange({
|
||||
type: newType,
|
||||
id: null,
|
||||
});
|
||||
};
|
||||
|
||||
// Handle selection
|
||||
const handleSelect = (id: string, type: BindingType) => {
|
||||
onChange({
|
||||
type,
|
||||
id,
|
||||
});
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn('space-y-4', className)}>
|
||||
{/* Binding type selection */}
|
||||
<div className="space-y-2">
|
||||
<Label>{t('bots.bindingType')}</Label>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant={value.type === 'pipeline' ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => handleTypeChange('pipeline')}
|
||||
disabled={disabled}
|
||||
className="flex-1"
|
||||
>
|
||||
<GitBranch className="size-4 mr-1.5" />
|
||||
Pipeline
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant={value.type === 'workflow' ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => handleTypeChange('workflow')}
|
||||
disabled={disabled}
|
||||
className="flex-1"
|
||||
>
|
||||
<Workflow className="size-4 mr-1.5" />
|
||||
Workflow
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Entity selection */}
|
||||
<div className="space-y-2">
|
||||
<Label>
|
||||
{value.type === 'pipeline' ? t('bots.selectPipeline') : t('bots.selectWorkflow')}
|
||||
</Label>
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={open}
|
||||
className="w-full justify-between"
|
||||
disabled={disabled}
|
||||
>
|
||||
{getSelectionDisplay()}
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[400px] p-0" align="start">
|
||||
<ScrollArea className="h-[300px]">
|
||||
<div className="p-2 space-y-1">
|
||||
{value.type === 'pipeline' ? (
|
||||
pipelines.length === 0 ? (
|
||||
<div className="py-6 text-center text-sm text-muted-foreground">
|
||||
{t('bots.noPipelinesFound')}
|
||||
</div>
|
||||
) : (
|
||||
pipelines.map((pipeline) => (
|
||||
<Button
|
||||
key={pipeline.id}
|
||||
variant="ghost"
|
||||
className="w-full justify-start"
|
||||
onClick={() => handleSelect(pipeline.id, 'pipeline')}
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
'mr-2 h-4 w-4',
|
||||
value.id === pipeline.id ? 'opacity-100' : 'opacity-0'
|
||||
)}
|
||||
/>
|
||||
<div className="flex items-center gap-2 flex-1 text-left">
|
||||
{pipeline.emoji && <span>{pipeline.emoji}</span>}
|
||||
<span className="truncate">{pipeline.name}</span>
|
||||
</div>
|
||||
{pipeline.description && (
|
||||
<span className="text-xs text-muted-foreground truncate max-w-[120px]">
|
||||
{pipeline.description}
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
))
|
||||
)
|
||||
) : (
|
||||
workflows.length === 0 ? (
|
||||
<div className="py-6 text-center text-sm text-muted-foreground">
|
||||
{t('bots.noWorkflowsFound')}
|
||||
</div>
|
||||
) : (
|
||||
workflows.map((workflow) => (
|
||||
<Button
|
||||
key={workflow.id}
|
||||
variant="ghost"
|
||||
className="w-full justify-start"
|
||||
onClick={() => handleSelect(workflow.id, 'workflow')}
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
'mr-2 h-4 w-4',
|
||||
value.id === workflow.id ? 'opacity-100' : 'opacity-0'
|
||||
)}
|
||||
/>
|
||||
<div className="flex items-center gap-2 flex-1 text-left">
|
||||
{workflow.emoji && <span>{workflow.emoji}</span>}
|
||||
<span className="truncate">{workflow.name}</span>
|
||||
</div>
|
||||
{workflow.description && (
|
||||
<span className="text-xs text-muted-foreground truncate max-w-[120px]">
|
||||
{workflow.description}
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
))
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
{/* Helper text */}
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{value.type === 'pipeline'
|
||||
? t('bots.pipelineBindingHelp')
|
||||
: t('bots.workflowBindingHelp')}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export {
|
||||
default as UnifiedBindingSelector,
|
||||
type BindingType,
|
||||
type BindingValue,
|
||||
} from './UnifiedBindingSelector';
|
||||
Reference in New Issue
Block a user