diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index fe2fae4..07ba16c 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -3,9 +3,11 @@ import { createPipeline, deleteGraphConfig, getGraphConfig, + getPipelineConversationMessages, getGraphDefaultConfig, getPipelineDefaultConfig, getMcpToolConfig, + listPipelineConversations, listMcpAvailableTools, listAvailableGraphs, listGraphConfigs, @@ -16,6 +18,8 @@ import { } from "./api/frontApis"; import { chooseActiveConfigItem, chooseDisplayItemsByPipeline } from "./activeConfigSelection"; import type { + ConversationListItem, + ConversationMessageItem, GraphConfigListItem, GraphConfigReadResponse, PipelineSpec, @@ -33,7 +37,7 @@ type EditableAgent = { llmName: string; }; -type ActiveTab = "agents" | "mcp"; +type ActiveTab = "agents" | "discussions" | "mcp"; type McpTransport = "streamable_http" | "sse" | "stdio"; type McpEntry = { id: string; @@ -373,6 +377,17 @@ function sanitizeConfigPath(path: string): string { return normalized; } +function formatDateTime(value?: string | null): string { + if (!value) { + return ""; + } + const timestamp = Date.parse(value); + if (Number.isNaN(timestamp)) { + return value; + } + return new Date(timestamp).toLocaleString(); +} + function toEditable( config: GraphConfigReadResponse, draft: boolean @@ -406,6 +421,10 @@ export default function App() { const [mcpToolKeys, setMcpToolKeys] = useState([]); const [mcpToolsByServer, setMcpToolsByServer] = useState>({}); const [mcpErrorsByServer, setMcpErrorsByServer] = useState>({}); + const [discussionConversations, setDiscussionConversations] = useState([]); + const [selectedConversationId, setSelectedConversationId] = useState(null); + const [discussionMessages, setDiscussionMessages] = useState([]); + const [discussionLoading, setDiscussionLoading] = useState(false); const [busy, setBusy] = useState(false); const configKeySet = useMemo( @@ -454,6 +473,8 @@ export default function App() { }); }, [editor, running]); const isEditorRunning = selectedRuns.length > 0; + const selectedPipelineId = editor?.pipelineId.trim() || ""; + const canViewDiscussions = !!selectedPipelineId && !editor?.isDraft; async function refreshConfigs(): Promise { const resp = await listGraphConfigs(); @@ -510,6 +531,82 @@ export default function App() { reloadMcpConfig().catch(() => undefined); }, [activeTab, mcpEntries.length]); + async function loadPipelineDiscussions( + pipelineId: string, + opts: { keepSelection?: boolean } = {} + ): Promise { + if (!pipelineId) { + setDiscussionConversations([]); + setSelectedConversationId(null); + setDiscussionMessages([]); + return; + } + setDiscussionLoading(true); + try { + const conversations = await listPipelineConversations(pipelineId); + setDiscussionConversations(conversations); + const nextSelected = opts.keepSelection + ? conversations.find((item) => item.conversation_id === selectedConversationId) + ? selectedConversationId + : null + : null; + const initialConversationId = nextSelected || conversations[0]?.conversation_id || null; + setSelectedConversationId(initialConversationId); + + if (initialConversationId) { + const messages = await getPipelineConversationMessages( + pipelineId, + initialConversationId + ); + setDiscussionMessages(messages); + } else { + setDiscussionMessages([]); + } + } catch (error) { + setStatusMessage((error as Error).message); + setDiscussionConversations([]); + setSelectedConversationId(null); + setDiscussionMessages([]); + } finally { + setDiscussionLoading(false); + } + } + + async function selectDiscussionConversation(conversationId: string): Promise { + if (!selectedPipelineId) { + return; + } + setSelectedConversationId(conversationId); + setDiscussionLoading(true); + try { + const messages = await getPipelineConversationMessages( + selectedPipelineId, + conversationId + ); + setDiscussionMessages(messages); + } catch (error) { + setStatusMessage((error as Error).message); + setDiscussionMessages([]); + } finally { + setDiscussionLoading(false); + } + } + + useEffect(() => { + if (activeTab !== "discussions") { + return; + } + if (!canViewDiscussions) { + setDiscussionConversations([]); + setSelectedConversationId(null); + setDiscussionMessages([]); + return; + } + loadPipelineDiscussions(selectedPipelineId, { keepSelection: true }).catch( + () => undefined + ); + }, [activeTab, canViewDiscussions, selectedPipelineId]); + async function selectExisting(item: GraphConfigListItem): Promise { const id = makeAgentKey(item.pipeline_id); setSelectedId(id); @@ -786,6 +883,23 @@ export default function App() { api_key: editor.apiKey.trim(), }); + let yamlSyncError = ""; + try { + await createPipeline({ + graph_id: editor.graphId, + pipeline_id: editor.pipelineId.trim(), + prompt_set_id: upsertResp.prompt_set_id, + tool_keys: editor.toolKeys, + api_key: editor.apiKey.trim(), + llm_name: editor.llmName || DEFAULT_LLM_NAME, + enabled: isEditorRunning, + }); + await refreshRunning(); + } catch (error) { + // Preserve the DB save result but surface why YAML/registry sync failed. + yamlSyncError = (error as Error).message; + } + await refreshConfigs(); const detail = await getPipelineDefaultConfig(upsertResp.pipeline_id); const saved = toEditable(detail, false); @@ -795,7 +909,11 @@ export default function App() { setEditor(saved); setSelectedId(saved.id); setDraftAgents((prev) => prev.filter((d) => d.id !== editor.id)); - setStatusMessage("Agent config saved."); + setStatusMessage( + yamlSyncError + ? `Agent config saved, but YAML sync failed: ${yamlSyncError}` + : "Agent config saved and YAML synced." + ); } catch (error) { setStatusMessage((error as Error).message); } finally { @@ -917,7 +1035,7 @@ export default function App() { ]; const graphArchImage = editor ? getGraphArchImage(editor.graphId) : null; - const showSidebar = activeTab === "agents"; + const showSidebar = activeTab !== "mcp"; return (
@@ -973,6 +1091,14 @@ export default function App() { > Agents +
+ ) : activeTab === "discussions" ? ( +
+
+

Agent Discussions

+
+ +
+
+ {!editor ? ( +

Select an agent from the left to view its discussions.

+ ) : editor.isDraft || !selectedPipelineId ? ( +

Save this agent first to start tracking discussion history.

+ ) : ( +
+
+ {discussionConversations.length === 0 ? ( +

No discussions found for this agent yet.

+ ) : ( + discussionConversations.map((conversation) => ( + + )) + )} +
+
+ {!selectedConversationId ? ( +

Select a discussion to inspect messages.

+ ) : discussionMessages.length === 0 ? ( +

No stored messages for this discussion.

+ ) : ( + discussionMessages.map((message) => ( +
+
+ {message.message_type} + + #{message.sequence_number} + {message.created_at + ? ` • ${formatDateTime(message.created_at)}` + : ""} + +
+
{message.content}
+
+ )) + )} +
+
+ )} +
) : (