import { DynamicFormItemType, IDynamicFormItemSchema, IFileConfig, } from '@/app/infra/entities/form/dynamic'; import { Input } from '@/components/ui/input'; import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { Switch } from '@/components/ui/switch'; import { ControllerRenderProps } from 'react-hook-form'; import { Button } from '@/components/ui/button'; import { useEffect, useState } from 'react'; import { httpClient, systemInfo, userInfo } from '@/app/infra/http'; import { LLMModel, Bot, KnowledgeBase, EmbeddingModel, RerankModel, PluginTool, } from '@/app/infra/entities/api'; import { toast } from 'sonner'; import { useTranslation } from 'react-i18next'; import { extractI18nObject } from '@/i18n/I18nProvider'; import { Textarea } from '@/components/ui/textarea'; import { Card, CardContent } from '@/components/ui/card'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, } from '@/components/ui/dialog'; import { Checkbox } from '@/components/ui/checkbox'; import { Plus, X, Eye, Wrench, Trash2, Sparkles, Info, Settings, ChevronDown, } from 'lucide-react'; import { Tooltip, TooltipContent, TooltipTrigger, } from '@/components/ui/tooltip'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; import SettingsDialog, { SettingsSection, } from '@/app/home/components/settings-dialog/SettingsDialog'; import ToolResourceSelectors from '@/app/home/components/dynamic-form/ToolResourceSelectors'; import { LANGBOT_MODELS_PROVIDER_REQUESTER } from '@/app/home/components/models-dialog/types'; const EMPTY_SELECT_ITEM_VALUE = '__langbot_empty_select_item_value__'; function toSelectValue(value: unknown): string { return typeof value === 'string' ? value : ''; } function hasNonEmptyUuid( item: T, ): item is T & { uuid: string } { return typeof item.uuid === 'string' && item.uuid.length > 0; } export default function DynamicFormItemComponent({ config, field, formValues, onFileUploaded, setFormValue, systemContext, }: { config: IDynamicFormItemSchema; field: ControllerRenderProps; formValues?: Record; onFileUploaded?: (fileKey: string) => void; setFormValue?: (name: string, value: unknown) => void; systemContext?: Record; }) { const [llmModels, setLlmModels] = useState([]); const [embeddingModels, setEmbeddingModels] = useState([]); const [rerankModels, setRerankModels] = useState([]); const [knowledgeBases, setKnowledgeBases] = useState([]); const [bots, setBots] = useState([]); const [tools, setTools] = useState([]); const [uploading, setUploading] = useState(false); const [kbDialogOpen, setKbDialogOpen] = useState(false); const [tempSelectedKBIds, setTempSelectedKBIds] = useState([]); const [toolsDialogOpen, setToolsDialogOpen] = useState(false); const [tempSelectedToolNames, setTempSelectedToolNames] = useState( [], ); const { t } = useTranslation(); const [modelsDialogOpen, setModelsDialogOpen] = useState(false); const [settingsSection, setSettingsSection] = useState('models'); const fetchLlmModels = () => { httpClient .getProviderLLMModels() .then((resp) => { setLlmModels(resp.models); }) .catch((err) => { toast.error(t('models.getModelListError') + err.msg); }); }; const fetchEmbeddingModels = () => { httpClient .getProviderEmbeddingModels() .then((resp) => { setEmbeddingModels(resp.models); }) .catch((err) => { toast.error(t('embedding.getModelListError') + err.msg); }); }; const fetchRerankModels = () => { httpClient .getProviderRerankModels() .then((resp) => { setRerankModels(resp.models); }) .catch((err) => { toast.error('Failed to load rerank models: ' + err.msg); }); }; const handleModelsDialogChange = (open: boolean) => { setModelsDialogOpen(open); if (!open) { fetchLlmModels(); fetchEmbeddingModels(); fetchRerankModels(); } }; const handleFileUpload = async (file: File): Promise => { const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB if (file.size > MAX_FILE_SIZE) { toast.error(t('plugins.fileUpload.tooLarge')); return null; } try { setUploading(true); const response = await httpClient.uploadPluginConfigFile(file); toast.success(t('plugins.fileUpload.success')); // 通知父组件文件已上传 onFileUploaded?.(response.file_key); return { file_key: response.file_key, mimetype: file.type, }; } catch (error) { toast.error( t('plugins.fileUpload.failed') + ': ' + (error as Error).message, ); return null; } finally { setUploading(false); } }; // Whether to show Space login CTA in model selectors const showSpaceLoginCTA = !systemInfo.disable_models_service && userInfo?.account_type !== 'space'; const handleSpaceLogin = () => { try { const token = localStorage.getItem('token'); if (!token) { toast.error(t('common.error')); return; } const currentOrigin = window.location.origin; const redirectUri = `${currentOrigin}/auth/space/callback?mode=bind`; httpClient .getSpaceAuthorizeUrl(redirectUri, token) .then((response) => { window.location.href = response.authorize_url; }) .catch(() => { toast.error(t('common.spaceLoginFailed')); }); } catch { toast.error(t('common.spaceLoginFailed')); } }; useEffect(() => { if (config.type === DynamicFormItemType.LLM_MODEL_SELECTOR) { fetchLlmModels(); } }, [config.type]); useEffect(() => { if (config.type === DynamicFormItemType.EMBEDDING_MODEL_SELECTOR) { fetchEmbeddingModels(); } }, [config.type]); useEffect(() => { if (config.type === DynamicFormItemType.RERANK_MODEL_SELECTOR) { fetchRerankModels(); } }, [config.type]); useEffect(() => { if (config.type === DynamicFormItemType.MODEL_FALLBACK_SELECTOR) { fetchLlmModels(); } }, [config.type]); useEffect(() => { if ( config.type === DynamicFormItemType.KNOWLEDGE_BASE_SELECTOR || config.type === DynamicFormItemType.KNOWLEDGE_BASE_MULTI_SELECTOR ) { httpClient .getKnowledgeBases() .then((resp) => { setKnowledgeBases(resp.bases); }) .catch((err) => { toast.error(t('knowledge.getKnowledgeBaseListError') + err.msg); }); } }, [config.type]); useEffect(() => { if (config.type === DynamicFormItemType.BOT_SELECTOR) { httpClient .getBots() .then((resp) => { setBots(resp.bots); }) .catch((err) => { toast.error(t('bots.getBotListError') + err.msg); }); } }, [config.type]); useEffect(() => { if (config.type === DynamicFormItemType.TOOLS_SELECTOR) { httpClient .getTools() .then((resp) => { setTools(resp.tools); }) .catch((err) => { toast.error( t('tools.getToolListError', 'Failed to get tools: ') + err.msg, ); }); } }, [config.type]); const handleCompositePatch = (patch: Record) => { for (const [name, value] of Object.entries(patch)) { if (setFormValue) { setFormValue(name, value); } else if (name === field.name) { field.onChange(value); } } }; switch (config.type) { case DynamicFormItemType.INT: case DynamicFormItemType.FLOAT: return ( field.onChange(Number(e.target.value))} /> ); case DynamicFormItemType.STRING: if (config.options && config.options.length > 0) { return (
{config.options.map((option) => ( field.onChange(option.name)} >
{extractI18nObject(option.label)} {option.name}
))}
); } return ; case DynamicFormItemType.TEXT: return (