import { useEffect, useMemo, useState } from "react"; import { createPipeline, deleteGraphConfig, getGraphConfig, getGraphDefaultConfig, getPipelineDefaultConfig, getMcpToolConfig, listAvailableGraphs, listGraphConfigs, listPipelines, stopPipeline, updateMcpToolConfig, upsertGraphConfig, } from "./api/frontApis"; import { chooseActiveConfigItem, chooseDisplayItemsByPipeline } from "./activeConfigSelection"; import type { GraphConfigListItem, GraphConfigReadResponse, PipelineSpec, } from "./types"; type EditableAgent = { id: string; isDraft: boolean; graphId: string; pipelineId: string; promptSetId?: string; toolKeys: string[]; prompts: Record; apiKey: string; llmName: string; }; type ActiveTab = "agents" | "mcp"; const DEFAULT_LLM_NAME = "qwen-plus"; const DEFAULT_API_KEY = ""; const GRAPH_ARCH_IMAGE_MODULES = import.meta.glob( "../assets/images/graph_arch/*.{png,jpg,jpeg,webp,gif}", { eager: true, import: "default" } ) as Record; const FALLBACK_PROMPTS_BY_GRAPH: Record> = { routing: { route_prompt: "", chat_prompt: "", tool_prompt: "", }, react: { sys_prompt: "", }, }; function makeAgentKey(pipelineId: string): string { return `pipeline::${pipelineId}`; } function parseToolCsv(value: string): string[] { const out: string[] = []; const seen = new Set(); for (const token of value.split(",")) { const trimmed = token.trim(); if (!trimmed || seen.has(trimmed)) { continue; } seen.add(trimmed); out.push(trimmed); } return out; } function maskSecretPreview(value: string): string { const trimmed = value.trim(); if (!trimmed) { return ""; } if (trimmed.length <= 10) { return trimmed; } return `${trimmed.slice(0, 5)}...${trimmed.slice(-5)}`; } function getGraphArchImage(graphId: string): string | null { const normalizedGraphId = graphId.trim().toLowerCase(); for (const [path, source] of Object.entries(GRAPH_ARCH_IMAGE_MODULES)) { const fileName = path.split("/").pop() || ""; const baseName = fileName.split(".")[0]?.toLowerCase() || ""; if (baseName === normalizedGraphId) { return source; } } return null; } function toEditable( config: GraphConfigReadResponse, draft: boolean ): EditableAgent { return { id: draft ? `draft-${Date.now()}-${Math.random().toString(36).slice(2, 6)}` : makeAgentKey(config.pipeline_id), isDraft: draft, graphId: config.graph_id || config.pipeline_id, pipelineId: config.pipeline_id, promptSetId: config.prompt_set_id, toolKeys: config.tool_keys || [], prompts: config.prompt_dict || {}, apiKey: config.api_key || DEFAULT_API_KEY, llmName: DEFAULT_LLM_NAME, }; } export default function App() { const [activeTab, setActiveTab] = useState("agents"); const [graphs, setGraphs] = useState([]); const [configItems, setConfigItems] = useState([]); const [running, setRunning] = useState([]); const [draftAgents, setDraftAgents] = useState([]); const [selectedId, setSelectedId] = useState(null); const [editor, setEditor] = useState(null); const [statusMessage, setStatusMessage] = useState(""); const [mcpConfigPath, setMcpConfigPath] = useState(""); const [mcpConfigRaw, setMcpConfigRaw] = useState(""); const [mcpToolKeys, setMcpToolKeys] = useState([]); const [busy, setBusy] = useState(false); const configKeySet = useMemo( () => new Set(configItems.map((x) => makeAgentKey(x.pipeline_id))), [configItems] ); const visibleConfigItems = useMemo( () => configItems.filter((item) => { // Hide the pre-seeded template entries (pipeline_id === graph_id, name "default") if ( item.name.toLowerCase() === "default" && item.graph_id && item.pipeline_id === item.graph_id ) { return false; } return true; }), [configItems] ); const displayConfigItems = useMemo( () => chooseDisplayItemsByPipeline(visibleConfigItems), [visibleConfigItems] ); const runningPipelineIdSet = useMemo(() => { const ids = new Set(); for (const run of running) { if (run.enabled) { ids.add(run.pipeline_id); } } return ids; }, [running]); const selectedRuns = useMemo(() => { const pipelineId = editor?.pipelineId.trim(); if (!pipelineId) { return []; } return running.filter((run) => { if (run.pipeline_id !== pipelineId) { return false; } return run.enabled; }); }, [editor, running]); const isEditorRunning = selectedRuns.length > 0; async function refreshConfigs(): Promise { const resp = await listGraphConfigs(); setConfigItems(resp.items); } async function refreshRunning(): Promise { const resp = await listPipelines(); setRunning(resp.items); } async function bootstrap(): Promise { setBusy(true); setStatusMessage("Loading graphs and agent configs..."); try { const [graphResp, configResp, runsResp] = await Promise.all([ listAvailableGraphs(), listGraphConfigs(), listPipelines(), ]); setGraphs(graphResp.available_graphs || []); setConfigItems(configResp.items || []); setRunning(runsResp.items || []); setStatusMessage(""); } catch (error) { setStatusMessage((error as Error).message); } finally { setBusy(false); } } useEffect(() => { bootstrap(); const timer = setInterval(() => { refreshRunning().catch(() => undefined); }, 5000); return () => clearInterval(timer); }, []); useEffect(() => { if (selectedId && !selectedId.startsWith("draft-") && !configKeySet.has(selectedId)) { setSelectedId(null); setEditor(null); } }, [selectedId, configKeySet]); useEffect(() => { if (activeTab !== "mcp") { return; } if (mcpConfigRaw) { return; } reloadMcpConfig().catch(() => undefined); }, [activeTab]); async function selectExisting(item: GraphConfigListItem): Promise { const id = makeAgentKey(item.pipeline_id); setSelectedId(id); setBusy(true); setStatusMessage("Loading agent details..."); try { let detail: GraphConfigReadResponse; try { detail = await getPipelineDefaultConfig(item.pipeline_id); } catch { const latest = await listGraphConfigs({ pipeline_id: item.pipeline_id }); const selected = chooseActiveConfigItem(latest.items || [], item.pipeline_id); if (!selected) { throw new Error(`No prompt set found for pipeline '${item.pipeline_id}'`); } detail = await getGraphConfig(item.pipeline_id, selected.prompt_set_id); } const editable = toEditable(detail, false); editable.id = id; editable.llmName = editor?.pipelineId === editable.pipelineId ? editor.llmName : DEFAULT_LLM_NAME; // apiKey is loaded from backend (persisted in DB) — don't override with default setEditor(editable); setStatusMessage(""); } catch (error) { setStatusMessage((error as Error).message); } finally { setBusy(false); } } async function addDraftAgent(): Promise { const graphId = graphs[0] || "routing"; setBusy(true); setStatusMessage("Preparing new agent draft..."); try { const defaults = await loadPromptDefaults(graphId); const editable = toEditable(defaults, true); editable.graphId = graphId; editable.pipelineId = ""; editable.promptSetId = undefined; editable.id = `draft-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`; setDraftAgents((prev) => [editable, ...prev]); setEditor(editable); setSelectedId(editable.id); setStatusMessage(""); } catch (error) { setStatusMessage((error as Error).message); } finally { setBusy(false); } } async function changeGraph(graphId: string): Promise { if (!editor) { return; } const targetEditorId = editor.id; setBusy(true); setStatusMessage("Loading default prompts for selected graph..."); try { const defaults = await loadPromptDefaults(graphId); setEditor((prev) => { if (!prev || prev.id !== targetEditorId) { // Selection changed while defaults were loading; do not mutate another agent. return prev; } const next: EditableAgent = { ...prev, graphId, prompts: { ...defaults.prompt_dict }, toolKeys: defaults.tool_keys || [], }; if (next.isDraft) { setDraftAgents((drafts) => drafts.map((draft) => (draft.id === next.id ? next : draft))); } return next; }); setStatusMessage(""); } catch (error) { setStatusMessage((error as Error).message); } finally { setBusy(false); } } function setEditorAndSyncDraft( updater: (prev: EditableAgent) => EditableAgent ): void { setEditor((prev) => { if (!prev) { return prev; } const next = updater(prev); if (next.isDraft) { setDraftAgents((drafts) => drafts.map((draft) => (draft.id === next.id ? next : draft))); } return next; }); } function updateEditor(key: K, value: EditableAgent[K]): void { setEditorAndSyncDraft((prev) => ({ ...prev, [key]: value })); } function updatePrompt(key: string, value: string): void { setEditorAndSyncDraft((prev) => ({ ...prev, prompts: { ...prev.prompts, [key]: value, }, })); } async function loadPromptDefaults(graphId: string): Promise { try { return await getGraphDefaultConfig(graphId); } catch { const fallbackPrompts = FALLBACK_PROMPTS_BY_GRAPH[graphId] || { sys_prompt: "" }; setStatusMessage( `No backend default config found for '${graphId}'. Using built-in fallback fields.` ); return { graph_id: graphId, pipeline_id: graphId, prompt_set_id: "default", tool_keys: [], prompt_dict: fallbackPrompts, api_key: "", }; } } async function reloadMcpConfig(): Promise { setBusy(true); setStatusMessage("Loading MCP config..."); try { const resp = await getMcpToolConfig(); setMcpConfigPath(resp.path || ""); setMcpConfigRaw(resp.raw_content || ""); setMcpToolKeys(resp.tool_keys || []); setStatusMessage("MCP config loaded."); } catch (error) { setStatusMessage((error as Error).message); } finally { setBusy(false); } } async function saveMcpConfig(): Promise { setBusy(true); setStatusMessage("Saving MCP config..."); try { const resp = await updateMcpToolConfig({ raw_content: mcpConfigRaw }); setMcpConfigPath(resp.path || ""); setMcpToolKeys(resp.tool_keys || []); setStatusMessage("MCP config saved."); } catch (error) { setStatusMessage((error as Error).message); } finally { setBusy(false); } } async function saveConfig(): Promise { if (!editor) { return; } const promptEntries = Object.entries(editor.prompts); if (!editor.pipelineId.trim()) { setStatusMessage("pipeline_id is required."); return; } if (!editor.graphId.trim()) { setStatusMessage("graph_id is required."); return; } if (promptEntries.length === 0) { setStatusMessage("At least one prompt field is required."); return; } if (promptEntries.some(([_, content]) => !content.trim())) { setStatusMessage("All prompt fields must be filled."); return; } setBusy(true); setStatusMessage("Saving agent config..."); try { let targetPromptSetId = editor.promptSetId; if (!targetPromptSetId) { try { const active = await getPipelineDefaultConfig(editor.pipelineId.trim()); targetPromptSetId = active.prompt_set_id; } catch { throw new Error( "No active prompt set for this pipeline. Create/activate one via backend first." ); } } const upsertResp = await upsertGraphConfig({ graph_id: editor.graphId, pipeline_id: editor.pipelineId.trim(), prompt_set_id: targetPromptSetId, tool_keys: editor.toolKeys, prompt_dict: editor.prompts, api_key: editor.apiKey.trim(), }); await refreshConfigs(); const detail = await getPipelineDefaultConfig(upsertResp.pipeline_id); const saved = toEditable(detail, false); saved.id = makeAgentKey(upsertResp.pipeline_id); // apiKey is loaded from backend (persisted in DB) — don't override saved.llmName = editor.llmName; setEditor(saved); setSelectedId(saved.id); setDraftAgents((prev) => prev.filter((d) => d.id !== editor.id)); setStatusMessage("Agent config saved."); } catch (error) { setStatusMessage((error as Error).message); } finally { setBusy(false); } } async function deleteSelected(): Promise { if (!editor) { return; } if (editor.isDraft || !editor.promptSetId) { setDraftAgents((prev) => prev.filter((d) => d.id !== editor.id)); setEditor(null); setSelectedId(null); setStatusMessage("Draft deleted."); return; } setBusy(true); setStatusMessage("Deleting agent config..."); try { await deleteGraphConfig(editor.pipelineId, editor.promptSetId); await refreshConfigs(); setEditor(null); setSelectedId(null); setStatusMessage("Agent deleted."); } catch (error) { setStatusMessage((error as Error).message); } finally { setBusy(false); } } async function runSelected(): Promise { if (!editor) { return; } if (!editor.promptSetId) { setStatusMessage("Save the agent first before running."); return; } if (!editor.pipelineId.trim()) { setStatusMessage("pipeline_id is required before run."); return; } if (!editor.apiKey.trim()) { setStatusMessage("api_key is required before run."); return; } setBusy(true); setStatusMessage("Registering agent runtime..."); try { const resp = await createPipeline({ graph_id: editor.graphId, pipeline_id: editor.pipelineId.trim(), prompt_set_id: editor.promptSetId, tool_keys: editor.toolKeys, api_key: editor.apiKey.trim(), llm_name: editor.llmName, enabled: true, }); await refreshRunning(); if (resp.reload_required) { setStatusMessage( `Agent registered, runtime reload pending. config_file=${resp.config_file}` ); } else { setStatusMessage( `Agent registered and runtime auto-reload is active. Ready to chat via app_id=${resp.pipeline_id}.` ); } } catch (error) { setStatusMessage((error as Error).message); } finally { setBusy(false); } } async function stopSelected(): Promise { if (!editor) { return; } const target = selectedRuns[0]; if (!target) { setStatusMessage("No running instance found for this agent."); return; } setBusy(true); setStatusMessage("Stopping agent..."); try { await stopPipeline(target.pipeline_id); await refreshRunning(); setStatusMessage("Agent stopped."); } catch (error) { setStatusMessage((error as Error).message); } finally { setBusy(false); } } const rows = [ ...draftAgents.map((d) => ({ id: d.id, label: d.pipelineId || "(new agent)", graphId: d.graphId, isRunning: d.pipelineId ? runningPipelineIdSet.has(d.pipelineId.trim()) : false, isDraft: true, })), ...displayConfigItems.map((item) => ({ id: makeAgentKey(item.pipeline_id), label: item.pipeline_id, graphId: item.graph_id || item.pipeline_id, isRunning: runningPipelineIdSet.has(item.pipeline_id), isDraft: false, })), ]; const graphArchImage = editor ? getGraphArchImage(editor.graphId) : null; const showSidebar = activeTab === "agents"; return (
{showSidebar ? ( ) : null}

Agent Manager

{statusMessage ?

{statusMessage}

: null} {activeTab === "agents" ? (
{!editor ? (

Select an agent from the left or create a new one.

) : (
{graphArchImage && (

Graph Architecture

{`${editor.graphId}
)}

Prompts

{Object.keys(editor.prompts).length === 0 ? (

No prompt keys returned from backend.

) : ( Object.entries(editor.prompts).map(([key, value]) => (