import isEqual from 'lodash/isEqual'; import React, { useCallback, useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useDispatch, useSelector } from 'react-redux'; import { useNavigate, useParams } from 'react-router-dom'; import userService from '../api/services/userService'; import ArrowLeft from '../assets/arrow-left.svg'; import SourceIcon from '../assets/source.svg'; import Dropdown from '../components/Dropdown'; import { FileUpload } from '../components/FileUpload'; import MultiSelectPopup, { OptionType } from '../components/MultiSelectPopup'; import Spinner from '../components/Spinner'; import AgentDetailsModal from '../modals/AgentDetailsModal'; import ConfirmationModal from '../modals/ConfirmationModal'; import { ActiveState, Doc, Prompt } from '../models/misc'; import { selectSelectedAgent, selectSourceDocs, selectToken, setSelectedAgent, } from '../preferences/preferenceSlice'; import PromptsModal from '../preferences/PromptsModal'; import Prompts from '../settings/Prompts'; import { UserToolType } from '../settings/types'; import AgentPreview from './AgentPreview'; import { Agent, ToolSummary } from './types'; const embeddingsName = import.meta.env.VITE_EMBEDDINGS_NAME || 'huggingface_sentence-transformers/all-mpnet-base-v2'; export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) { const { t } = useTranslation(); const navigate = useNavigate(); const dispatch = useDispatch(); const { agentId } = useParams(); const token = useSelector(selectToken); const sourceDocs = useSelector(selectSourceDocs); const selectedAgent = useSelector(selectSelectedAgent); const [effectiveMode, setEffectiveMode] = useState(mode); const [agent, setAgent] = useState({ id: agentId || '', name: '', description: '', image: '', source: '', sources: [], chunks: '2', retriever: 'classic', prompt_id: 'default', tools: [], agent_type: 'classic', status: '', json_schema: undefined, limited_token_mode: false, token_limit: undefined, limited_request_mode: false, request_limit: undefined, }); const [imageFile, setImageFile] = useState(null); const [prompts, setPrompts] = useState< { name: string; id: string; type: string }[] >([]); const [userTools, setUserTools] = useState([]); const [isSourcePopupOpen, setIsSourcePopupOpen] = useState(false); const [isToolsPopupOpen, setIsToolsPopupOpen] = useState(false); const [selectedSourceIds, setSelectedSourceIds] = useState< Set >(new Set()); const [selectedTools, setSelectedTools] = useState([]); const [deleteConfirmation, setDeleteConfirmation] = useState('INACTIVE'); const [agentDetails, setAgentDetails] = useState('INACTIVE'); const [addPromptModal, setAddPromptModal] = useState('INACTIVE'); const [hasChanges, setHasChanges] = useState(false); const [draftLoading, setDraftLoading] = useState(false); const [publishLoading, setPublishLoading] = useState(false); const [jsonSchemaText, setJsonSchemaText] = useState(''); const [jsonSchemaValid, setJsonSchemaValid] = useState(true); const [isAdvancedSectionExpanded, setIsAdvancedSectionExpanded] = useState(false); const initialAgentRef = useRef(null); const sourceAnchorButtonRef = useRef(null); const toolAnchorButtonRef = useRef(null); const modeConfig = { new: { heading: t('agents.form.headings.new'), buttonText: t('agents.form.buttons.publish'), showDelete: false, showSaveDraft: true, showLogs: false, showAccessDetails: false, trackChanges: false, }, edit: { heading: t('agents.form.headings.edit'), buttonText: t('agents.form.buttons.save'), showDelete: true, showSaveDraft: false, showLogs: true, showAccessDetails: true, trackChanges: true, }, draft: { heading: t('agents.form.headings.draft'), buttonText: t('agents.form.buttons.publish'), showDelete: true, showSaveDraft: true, showLogs: false, showAccessDetails: false, trackChanges: false, }, }; const chunks = ['0', '2', '4', '6', '8', '10']; const agentTypes = [ { label: t('agents.form.agentTypes.classic'), value: 'classic' }, { label: t('agents.form.agentTypes.react'), value: 'react' }, ]; const isPublishable = () => { const hasRequiredFields = agent.name && agent.description && agent.prompt_id && agent.agent_type; const isJsonSchemaValidOrEmpty = jsonSchemaText.trim() === '' || jsonSchemaValid; const hasSource = selectedSourceIds.size > 0; return hasRequiredFields && isJsonSchemaValidOrEmpty && hasSource; }; const isJsonSchemaInvalid = () => { return jsonSchemaText.trim() !== '' && !jsonSchemaValid; }; const handleUpload = useCallback((files: File[]) => { if (files && files.length > 0) { const file = files[0]; setImageFile(file); } }, []); const handleCancel = () => { if (selectedAgent) dispatch(setSelectedAgent(null)); navigate('/agents'); }; const handleDelete = async (agentId: string) => { const response = await userService.deleteAgent(agentId, token); if (!response.ok) throw new Error('Failed to delete agent'); navigate('/agents'); }; const handleSaveDraft = async () => { const formData = new FormData(); formData.append('name', agent.name); formData.append('description', agent.description); if (selectedSourceIds.size > 1) { const sourcesArray = Array.from(selectedSourceIds) .map((id) => { const sourceDoc = sourceDocs?.find( (source) => source.id === id || source.retriever === id || source.name === id, ); if (sourceDoc?.name === 'Default' && !sourceDoc?.id) { return 'default'; } return sourceDoc?.id || id; }) .filter(Boolean); formData.append('sources', JSON.stringify(sourcesArray)); formData.append('source', ''); } else if (selectedSourceIds.size === 1) { const singleSourceId = Array.from(selectedSourceIds)[0]; const sourceDoc = sourceDocs?.find( (source) => source.id === singleSourceId || source.retriever === singleSourceId || source.name === singleSourceId, ); let finalSourceId; if (sourceDoc?.name === 'Default' && !sourceDoc?.id) finalSourceId = 'default'; else finalSourceId = sourceDoc?.id || singleSourceId; formData.append('source', String(finalSourceId)); formData.append('sources', JSON.stringify([])); } else { formData.append('source', ''); formData.append('sources', JSON.stringify([])); } formData.append('chunks', agent.chunks); formData.append('retriever', agent.retriever); formData.append('prompt_id', agent.prompt_id); formData.append('agent_type', agent.agent_type); formData.append('status', 'draft'); if (agent.limited_token_mode && agent.token_limit) { formData.append('limited_token_mode', 'True'); formData.append('token_limit', agent.token_limit.toString()); } else { formData.append('limited_token_mode', 'False'); formData.append('token_limit', '0'); } if (agent.limited_request_mode && agent.request_limit) { formData.append('limited_request_mode', 'True'); formData.append('request_limit', agent.request_limit.toString()); } else { formData.append('limited_request_mode', 'False'); formData.append('request_limit', '0'); } if (imageFile) formData.append('image', imageFile); if (agent.tools && agent.tools.length > 0) formData.append('tools', JSON.stringify(agent.tools)); else formData.append('tools', '[]'); if (agent.json_schema) { formData.append('json_schema', JSON.stringify(agent.json_schema)); } try { setDraftLoading(true); const response = effectiveMode === 'new' ? await userService.createAgent(formData, token) : await userService.updateAgent(agent.id || '', formData, token); if (!response.ok) throw new Error('Failed to create agent draft'); const data = await response.json(); const updatedAgent = { ...agent, id: data.id || agent.id, image: data.image || agent.image, }; setAgent(updatedAgent); if (effectiveMode === 'new') setEffectiveMode('draft'); } catch (error) { console.error('Error saving draft:', error); throw new Error('Failed to save draft'); } finally { setDraftLoading(false); } }; const handlePublish = async () => { const formData = new FormData(); formData.append('name', agent.name); formData.append('description', agent.description); if (selectedSourceIds.size > 1) { const sourcesArray = Array.from(selectedSourceIds) .map((id) => { const sourceDoc = sourceDocs?.find( (source) => source.id === id || source.retriever === id || source.name === id, ); if (sourceDoc?.name === 'Default' && !sourceDoc?.id) { return 'default'; } return sourceDoc?.id || id; }) .filter(Boolean); formData.append('sources', JSON.stringify(sourcesArray)); formData.append('source', ''); } else if (selectedSourceIds.size === 1) { const singleSourceId = Array.from(selectedSourceIds)[0]; const sourceDoc = sourceDocs?.find( (source) => source.id === singleSourceId || source.retriever === singleSourceId || source.name === singleSourceId, ); let finalSourceId; if (sourceDoc?.name === 'Default' && !sourceDoc?.id) finalSourceId = 'default'; else finalSourceId = sourceDoc?.id || singleSourceId; formData.append('source', String(finalSourceId)); formData.append('sources', JSON.stringify([])); } else { formData.append('source', ''); formData.append('sources', JSON.stringify([])); } formData.append('chunks', agent.chunks); formData.append('retriever', agent.retriever); formData.append('prompt_id', agent.prompt_id); formData.append('agent_type', agent.agent_type); formData.append('status', 'published'); if (imageFile) formData.append('image', imageFile); if (agent.tools && agent.tools.length > 0) formData.append('tools', JSON.stringify(agent.tools)); else formData.append('tools', '[]'); if (agent.json_schema) { formData.append('json_schema', JSON.stringify(agent.json_schema)); } // Always send the limited mode fields if (agent.limited_token_mode && agent.token_limit) { formData.append('limited_token_mode', 'True'); formData.append('token_limit', agent.token_limit.toString()); } else { formData.append('limited_token_mode', 'False'); formData.append('token_limit', '0'); } if (agent.limited_request_mode && agent.request_limit) { formData.append('limited_request_mode', 'True'); formData.append('request_limit', agent.request_limit.toString()); } else { formData.append('limited_request_mode', 'False'); formData.append('request_limit', '0'); } try { setPublishLoading(true); const response = effectiveMode === 'new' ? await userService.createAgent(formData, token) : await userService.updateAgent(agent.id || '', formData, token); if (!response.ok) throw new Error('Failed to publish agent'); const data = await response.json(); const updatedAgent = { ...agent, id: data.id || agent.id, key: data.key || agent.key, status: 'published', image: data.image || agent.image, }; setAgent(updatedAgent); initialAgentRef.current = updatedAgent; if (effectiveMode === 'new' || effectiveMode === 'draft') { setEffectiveMode('edit'); setAgentDetails('ACTIVE'); } setImageFile(null); } catch (error) { console.error('Error publishing agent:', error); throw new Error('Failed to publish agent'); } finally { setPublishLoading(false); } }; const validateAndSetJsonSchema = (text: string) => { setJsonSchemaText(text); if (text.trim() === '') { setAgent({ ...agent, json_schema: undefined }); setJsonSchemaValid(true); return; } try { const parsed = JSON.parse(text); setAgent({ ...agent, json_schema: parsed }); setJsonSchemaValid(true); } catch (error) { setJsonSchemaValid(false); } }; useEffect(() => { const getTools = async () => { const response = await userService.getUserTools(token); if (!response.ok) throw new Error('Failed to fetch tools'); const data = await response.json(); const tools: OptionType[] = data.tools.map((tool: UserToolType) => ({ id: tool.id, label: tool.customName ? tool.customName : tool.displayName, icon: `/toolIcons/tool_${tool.name}.svg`, })); setUserTools(tools); }; const getPrompts = async () => { const response = await userService.getPrompts(token); if (!response.ok) { throw new Error('Failed to fetch prompts'); } const data = await response.json(); setPrompts(data); }; getTools(); getPrompts(); }, [token]); // Auto-select default source if none selected useEffect(() => { if (sourceDocs && sourceDocs.length > 0 && selectedSourceIds.size === 0) { const defaultSource = sourceDocs.find((s) => s.name === 'Default'); if (defaultSource) { setSelectedSourceIds( new Set([ defaultSource.id || defaultSource.retriever || defaultSource.name, ]), ); } else { setSelectedSourceIds( new Set([ sourceDocs[0].id || sourceDocs[0].retriever || sourceDocs[0].name, ]), ); } } }, [sourceDocs, selectedSourceIds.size]); useEffect(() => { if ((mode === 'edit' || mode === 'draft') && agentId) { const getAgent = async () => { const response = await userService.getAgent(agentId, token); if (!response.ok) { navigate('/agents'); throw new Error('Failed to fetch agent'); } const data = await response.json(); if (data.sources && data.sources.length > 0) { const mappedSources = data.sources.map((sourceId: string) => { if (sourceId === 'default') { const defaultSource = sourceDocs?.find( (source) => source.name === 'Default', ); return defaultSource?.retriever || 'classic'; } return sourceId; }); setSelectedSourceIds(new Set(mappedSources)); } else if (data.source) { if (data.source === 'default') { const defaultSource = sourceDocs?.find( (source) => source.name === 'Default', ); setSelectedSourceIds( new Set([defaultSource?.retriever || 'classic']), ); } else { setSelectedSourceIds(new Set([data.source])); } } else if (data.retriever) { setSelectedSourceIds(new Set([data.retriever])); } if (data.tool_details) setSelectedTools(data.tool_details); if (data.status === 'draft') setEffectiveMode('draft'); if (data.json_schema) { const jsonText = JSON.stringify(data.json_schema, null, 2); setJsonSchemaText(jsonText); setJsonSchemaValid(true); } setAgent(data); initialAgentRef.current = data; }; getAgent(); } }, [agentId, mode, token]); useEffect(() => { const selectedSources = Array.from(selectedSourceIds) .map((id) => sourceDocs?.find( (source) => source.id === id || source.retriever === id || source.name === id, ), ) .filter(Boolean); if (selectedSources.length > 0) { // Handle multiple sources if (selectedSources.length > 1) { // Multiple sources selected - store in sources array const sourceIds = selectedSources .map((source) => source?.id) .filter((id): id is string => Boolean(id)); setAgent((prev) => ({ ...prev, sources: sourceIds, source: '', // Clear single source for multiple sources retriever: '', })); } else { // Single source selected - maintain backward compatibility const selectedSource = selectedSources[0]; if (selectedSource?.model === embeddingsName) { if (selectedSource && 'id' in selectedSource) { setAgent((prev) => ({ ...prev, source: selectedSource?.id || 'default', sources: [], // Clear sources array for single source retriever: '', })); } else { setAgent((prev) => ({ ...prev, source: '', sources: [], // Clear sources array retriever: selectedSource?.retriever || 'classic', })); } } } } else { // No sources selected setAgent((prev) => ({ ...prev, source: '', sources: [], retriever: '', })); } }, [selectedSourceIds]); useEffect(() => { setAgent((prev) => ({ ...prev, tools: Array.from(selectedTools) .map((tool) => tool?.id) .filter((id): id is string => typeof id === 'string'), })); }, [selectedTools]); useEffect(() => { if (isPublishable()) dispatch(setSelectedAgent(agent)); if (!modeConfig[effectiveMode].trackChanges) { setHasChanges(true); return; } if (!initialAgentRef.current) { setHasChanges(false); return; } const initialJsonSchemaText = initialAgentRef.current.json_schema ? JSON.stringify(initialAgentRef.current.json_schema, null, 2) : ''; const isChanged = !isEqual(agent, initialAgentRef.current) || imageFile !== null || jsonSchemaText !== initialJsonSchemaText; setHasChanges(isChanged); }, [agent, dispatch, effectiveMode, imageFile, jsonSchemaText]); return (

{t('agents.backToAll')}

{modeConfig[effectiveMode].heading}

{modeConfig[effectiveMode].showDelete && agent.id && ( )} {modeConfig[effectiveMode].showSaveDraft && ( )} {modeConfig[effectiveMode].showAccessDetails && ( )} {modeConfig[effectiveMode].showAccessDetails && ( )}

{t('agents.form.sections.meta')}

setAgent({ ...agent, name: e.target.value })} />