diff --git a/ui/src/components/flow/DocumentBadges.tsx b/ui/src/components/flow/DocumentBadges.tsx new file mode 100644 index 0000000..549ec20 --- /dev/null +++ b/ui/src/components/flow/DocumentBadges.tsx @@ -0,0 +1,67 @@ +"use client"; + +import { Badge } from "@/components/ui/badge"; +import { listDocumentsApiV1KnowledgeBaseDocumentsGet } from "@/client/sdk.gen"; +import { useAuth } from "@/lib/auth"; +import { useCallback, useEffect, useState } from "react"; + +interface DocumentBadgesProps { + documentUuids: string[]; +} + +export const DocumentBadges = ({ documentUuids }: DocumentBadgesProps) => { + const { getAccessToken } = useAuth(); + const [documentNames, setDocumentNames] = useState>({}); + const [loading, setLoading] = useState(false); + + const fetchDocuments = useCallback(async () => { + if (documentUuids.length === 0) return; + + setLoading(true); + try { + const accessToken = await getAccessToken(); + const response = await listDocumentsApiV1KnowledgeBaseDocumentsGet({ + headers: { Authorization: `Bearer ${accessToken}` }, + query: { + limit: 100, + }, + }); + + if (response.data) { + const nameMap: Record = {}; + response.data.documents + .filter((doc) => documentUuids.includes(doc.document_uuid)) + .forEach((doc) => { + nameMap[doc.document_uuid] = doc.filename; + }); + setDocumentNames(nameMap); + } + } catch (error) { + console.error("Failed to fetch documents:", error); + } finally { + setLoading(false); + } + }, [documentUuids, getAccessToken]); + + useEffect(() => { + fetchDocuments(); + }, [fetchDocuments]); + + if (documentUuids.length === 0) { + return <>; + } + + if (loading) { + return Loading...; + } + + return ( + <> + {documentUuids.map((uuid) => ( + + {documentNames[uuid] || uuid} + + ))} + + ); +}; diff --git a/ui/src/components/flow/DocumentSelector.tsx b/ui/src/components/flow/DocumentSelector.tsx new file mode 100644 index 0000000..0f138fe --- /dev/null +++ b/ui/src/components/flow/DocumentSelector.tsx @@ -0,0 +1,181 @@ +"use client"; + +import { Button } from "@/components/ui/button"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Label } from "@/components/ui/label"; +import { listDocumentsApiV1KnowledgeBaseDocumentsGet } from "@/client/sdk.gen"; +import type { DocumentResponseSchema } from "@/client/types.gen"; +import { useAuth } from "@/lib/auth"; +import { FileText } from "lucide-react"; +import Link from "next/link"; +import { useCallback, useEffect, useState } from "react"; + +interface DocumentSelectorProps { + value: string[]; + onChange: (uuids: string[]) => void; + disabled?: boolean; + label?: string; + description?: string; + showLabel?: boolean; +} + +export const DocumentSelector = ({ + value, + onChange, + disabled = false, + label = "Knowledge Base Documents", + description = "Select documents that the agent can reference during conversations.", + showLabel = true, +}: DocumentSelectorProps) => { + const { getAccessToken } = useAuth(); + const [documents, setDocuments] = useState([]); + const [loading, setLoading] = useState(false); + + const fetchDocuments = useCallback(async () => { + setLoading(true); + try { + const accessToken = await getAccessToken(); + const response = await listDocumentsApiV1KnowledgeBaseDocumentsGet({ + headers: { Authorization: `Bearer ${accessToken}` }, + query: { + limit: 100, + }, + }); + + if (response.data) { + // Only show completed documents + const completedDocs = response.data.documents.filter( + (doc) => doc.processing_status === "completed" + ); + setDocuments(completedDocs); + } + } catch (error) { + console.error("Failed to fetch documents:", error); + } finally { + setLoading(false); + } + }, [getAccessToken]); + + useEffect(() => { + fetchDocuments(); + }, [fetchDocuments]); + + const handleToggle = (documentUuid: string, checked: boolean) => { + if (checked) { + onChange([...value, documentUuid]); + } else { + onChange(value.filter((uuid) => uuid !== documentUuid)); + } + }; + + const formatFileSize = (bytes: number): string => { + if (bytes === 0) return "0 Bytes"; + const k = 1024; + const sizes = ["Bytes", "KB", "MB", "GB"]; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return Math.round(bytes / Math.pow(k, i) * 100) / 100 + " " + sizes[i]; + }; + + if (loading) { + return ( +
+ {showLabel && ( + <> + + {description && ( + + )} + + )} +
+ Loading documents... +
+
+ ); + } + + if (documents.length === 0) { + return ( +
+ {showLabel && ( + <> + + {description && ( + + )} + + )} +
+
+ No documents available. Upload documents to the knowledge base first. +
+
+ + + +
+
+
+ ); + } + + return ( +
+ {showLabel && ( + <> + + {description && ( + + )} + + )} +
+
+ {documents.map((doc) => ( +
+ + handleToggle(doc.document_uuid, checked as boolean) + } + disabled={disabled} + /> +
+ +
+
+ ))} +
+
+
+ + {value.length} {value.length === 1 ? "document" : "documents"} selected + + + Manage Documents + +
+
+ ); +}; diff --git a/ui/src/components/flow/nodes/AgentNode.tsx b/ui/src/components/flow/nodes/AgentNode.tsx index b973636..d2116ca 100644 --- a/ui/src/components/flow/nodes/AgentNode.tsx +++ b/ui/src/components/flow/nodes/AgentNode.tsx @@ -1,8 +1,10 @@ import { NodeProps, NodeToolbar, Position } from "@xyflow/react"; -import { Edit, Headset, PlusIcon, Trash2Icon, Wrench } from "lucide-react"; +import { Edit, FileText, Headset, PlusIcon, Trash2Icon, Wrench } from "lucide-react"; import { memo, useEffect, useMemo, useState } from "react"; import { useWorkflow } from "@/app/workflow/[workflowId]/contexts/WorkflowContext"; +import { DocumentBadges } from "@/components/flow/DocumentBadges"; +import { DocumentSelector } from "@/components/flow/DocumentSelector"; import { ToolBadges } from "@/components/flow/ToolBadges"; import { ToolSelector } from "@/components/flow/ToolSelector"; import { ExtractionVariable, FlowNodeData } from "@/components/flow/types"; @@ -34,6 +36,8 @@ interface AgentNodeEditFormProps { setAddGlobalPrompt: (value: boolean) => void; toolUuids: string[]; setToolUuids: (value: string[]) => void; + documentUuids: string[]; + setDocumentUuids: (value: string[]) => void; } interface AgentNodeProps extends NodeProps { @@ -55,6 +59,7 @@ export const AgentNode = memo(({ data, selected, id }: AgentNodeProps) => { const [variables, setVariables] = useState(data.extraction_variables ?? []); const [addGlobalPrompt, setAddGlobalPrompt] = useState(data.add_global_prompt ?? true); const [toolUuids, setToolUuids] = useState(data.tool_uuids ?? []); + const [documentUuids, setDocumentUuids] = useState(data.document_uuids ?? []); // Compute if form has unsaved changes (only check prompt, name) const isDirty = useMemo(() => { @@ -75,6 +80,7 @@ export const AgentNode = memo(({ data, selected, id }: AgentNodeProps) => { extraction_variables: variables, add_global_prompt: addGlobalPrompt, tool_uuids: toolUuids.length > 0 ? toolUuids : undefined, + document_uuids: documentUuids.length > 0 ? documentUuids : undefined, }); setOpen(false); // Save the workflow after updating node data with a small delay to ensure state is updated @@ -94,6 +100,7 @@ export const AgentNode = memo(({ data, selected, id }: AgentNodeProps) => { setVariables(data.extraction_variables ?? []); setAddGlobalPrompt(data.add_global_prompt ?? true); setToolUuids(data.tool_uuids ?? []); + setDocumentUuids(data.document_uuids ?? []); } setOpen(newOpen); }; @@ -109,6 +116,7 @@ export const AgentNode = memo(({ data, selected, id }: AgentNodeProps) => { setVariables(data.extraction_variables ?? []); setAddGlobalPrompt(data.add_global_prompt ?? true); setToolUuids(data.tool_uuids ?? []); + setDocumentUuids(data.document_uuids ?? []); } }, [data, open]); @@ -139,6 +147,15 @@ export const AgentNode = memo(({ data, selected, id }: AgentNodeProps) => { )} + {data.document_uuids && data.document_uuids.length > 0 && ( +
+
+ + Documents: +
+ +
+ )} @@ -179,6 +196,8 @@ export const AgentNode = memo(({ data, selected, id }: AgentNodeProps) => { setAddGlobalPrompt={setAddGlobalPrompt} toolUuids={toolUuids} setToolUuids={setToolUuids} + documentUuids={documentUuids} + setDocumentUuids={setDocumentUuids} /> )} @@ -203,6 +222,8 @@ const AgentNodeEditForm = ({ setAddGlobalPrompt, toolUuids, setToolUuids, + documentUuids, + setDocumentUuids, }: AgentNodeEditFormProps) => { const handleVariableNameChange = (idx: number, value: string) => { const newVars = [...variables]; @@ -346,6 +367,15 @@ const AgentNodeEditForm = ({ description="Select tools that the agent can invoke during this conversation step." /> + + {/* Documents Section */} +
+ +
); }; diff --git a/ui/src/components/flow/nodes/StartCall.tsx b/ui/src/components/flow/nodes/StartCall.tsx index 865590c..4c920d3 100644 --- a/ui/src/components/flow/nodes/StartCall.tsx +++ b/ui/src/components/flow/nodes/StartCall.tsx @@ -1,8 +1,10 @@ import { NodeProps, NodeToolbar, Position } from "@xyflow/react"; -import { Edit, Play, PlusIcon, Trash2Icon, Wrench } from "lucide-react"; +import { Edit, FileText, Play, PlusIcon, Trash2Icon, Wrench } from "lucide-react"; import { memo, useEffect, useMemo, useState } from "react"; import { useWorkflow } from "@/app/workflow/[workflowId]/contexts/WorkflowContext"; +import { DocumentBadges } from "@/components/flow/DocumentBadges"; +import { DocumentSelector } from "@/components/flow/DocumentSelector"; import { ToolBadges } from "@/components/flow/ToolBadges"; import { ToolSelector } from "@/components/flow/ToolSelector"; import { ExtractionVariable, FlowNodeData } from "@/components/flow/types"; @@ -41,6 +43,8 @@ interface StartCallEditFormProps { setVariables: (vars: ExtractionVariable[]) => void; toolUuids: string[]; setToolUuids: (value: string[]) => void; + documentUuids: string[]; + setDocumentUuids: (value: string[]) => void; } interface StartCallNodeProps extends NodeProps { @@ -66,6 +70,7 @@ export const StartCall = memo(({ data, selected, id }: StartCallNodeProps) => { const [extractionPrompt, setExtractionPrompt] = useState(data.extraction_prompt ?? ""); const [variables, setVariables] = useState(data.extraction_variables ?? []); const [toolUuids, setToolUuids] = useState(data.tool_uuids ?? []); + const [documentUuids, setDocumentUuids] = useState(data.document_uuids ?? []); // Compute if form has unsaved changes (only check prompt, name) const isDirty = useMemo(() => { @@ -89,6 +94,7 @@ export const StartCall = memo(({ data, selected, id }: StartCallNodeProps) => { extraction_prompt: extractionPrompt, extraction_variables: variables, tool_uuids: toolUuids.length > 0 ? toolUuids : undefined, + document_uuids: documentUuids.length > 0 ? documentUuids : undefined, }); setOpen(false); // Save the workflow after updating node data with a small delay to ensure state is updated @@ -111,6 +117,7 @@ export const StartCall = memo(({ data, selected, id }: StartCallNodeProps) => { setExtractionPrompt(data.extraction_prompt ?? ""); setVariables(data.extraction_variables ?? []); setToolUuids(data.tool_uuids ?? []); + setDocumentUuids(data.document_uuids ?? []); } setOpen(newOpen); }; @@ -129,6 +136,7 @@ export const StartCall = memo(({ data, selected, id }: StartCallNodeProps) => { setExtractionPrompt(data.extraction_prompt ?? ""); setVariables(data.extraction_variables ?? []); setToolUuids(data.tool_uuids ?? []); + setDocumentUuids(data.document_uuids ?? []); } }, [data, open]); @@ -158,6 +166,15 @@ export const StartCall = memo(({ data, selected, id }: StartCallNodeProps) => { )} + {data.document_uuids && data.document_uuids.length > 0 && ( +
+
+ + Documents: +
+ +
+ )} @@ -199,6 +216,8 @@ export const StartCall = memo(({ data, selected, id }: StartCallNodeProps) => { setVariables={setVariables} toolUuids={toolUuids} setToolUuids={setToolUuids} + documentUuids={documentUuids} + setDocumentUuids={setDocumentUuids} /> )} @@ -229,6 +248,8 @@ const StartCallEditForm = ({ setVariables, toolUuids, setToolUuids, + documentUuids, + setDocumentUuids, }: StartCallEditFormProps) => { const handleVariableNameChange = (idx: number, value: string) => { const newVars = [...variables]; @@ -417,6 +438,15 @@ const StartCallEditForm = ({ description="Select tools that the agent can invoke during this conversation step." /> + + {/* Documents Section */} +
+ +
); }; diff --git a/ui/src/components/flow/types.ts b/ui/src/components/flow/types.ts index a04d64c..3768716 100644 --- a/ui/src/components/flow/types.ts +++ b/ui/src/components/flow/types.ts @@ -42,6 +42,8 @@ export type FlowNodeData = { }; // Tools - array of tool UUIDs that can be invoked by this node tool_uuids?: string[]; + // Documents - array of knowledge base document UUIDs that can be referenced by this node + document_uuids?: string[]; } export type FlowNode = {