diff --git a/apps/web/src/features/deep-agent-chat/components/ChatInterface.tsx b/apps/web/src/features/deep-agent-chat/components/ChatInterface.tsx index 50ff5616..33b33810 100644 --- a/apps/web/src/features/deep-agent-chat/components/ChatInterface.tsx +++ b/apps/web/src/features/deep-agent-chat/components/ChatInterface.tsx @@ -31,6 +31,8 @@ import { Assistant, Message } from "@langchain/langgraph-sdk"; import { extractStringFromMessageContent, isPreparingToCallTaskTool, + extractDocumentsFromMessage, + Document, } from "../utils"; import { v4 as uuidv4 } from "uuid"; import { useQueryState } from "nuqs"; @@ -166,6 +168,27 @@ export const ChatInterface = React.memo( agentId, ); + const sourceToDocumentsMap = useMemo(() => { + const documents = messages + .filter( + (message) => + message.type === "tool" && typeof message.content === "string", + ) + .map((message) => + extractDocumentsFromMessage(message.content as string), + ) + .flat(); + return documents.reduce( + (acc, document) => { + if (document.source) { + acc[document.source] = document; + } + return acc; + }, + {} as Record, + ); + }, [messages]); + useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); }, [messages]); @@ -444,6 +467,7 @@ export const ChatInterface = React.memo( selectedSubAgent={selectedSubAgent} onRestartFromAIMessage={handleRestartFromAIMessage} onRestartFromSubTask={handleRestartFromSubTask} + sourceToDocumentsMap={sourceToDocumentsMap} debugMode={debugMode} isLoading={isLoading} isLastMessage={index === processedMessages.length - 1} diff --git a/apps/web/src/features/deep-agent-chat/components/ChatMessage.tsx b/apps/web/src/features/deep-agent-chat/components/ChatMessage.tsx index 07a20208..4148ebe4 100644 --- a/apps/web/src/features/deep-agent-chat/components/ChatMessage.tsx +++ b/apps/web/src/features/deep-agent-chat/components/ChatMessage.tsx @@ -5,9 +5,14 @@ import { User, Bot } from "lucide-react"; import { SubAgentIndicator } from "./SubAgentIndicator"; import { ToolCallBox } from "./ToolCallBox"; import { MarkdownContent } from "./MarkdownContent"; +import { Citations } from "./Citations"; import type { SubAgent, ToolCall } from "../types"; import { Message } from "@langchain/langgraph-sdk"; -import { extractStringFromMessageContent } from "../utils"; +import { + extractStringFromMessageContent, + extractCitationUrls, + Document, +} from "../utils"; import { cn } from "@/lib/utils"; interface ChatMessageProps { @@ -18,6 +23,7 @@ interface ChatMessageProps { selectedSubAgent: SubAgent | null; onRestartFromAIMessage: (message: Message) => void; onRestartFromSubTask: (toolCallId: string) => void; + sourceToDocumentsMap: Record; debugMode?: boolean; isLastMessage?: boolean; isLoading?: boolean; @@ -32,6 +38,7 @@ export const ChatMessage = React.memo( selectedSubAgent, onRestartFromAIMessage, onRestartFromSubTask, + sourceToDocumentsMap, debugMode, isLastMessage, isLoading, @@ -75,6 +82,10 @@ export const ChatMessage = React.memo( } }, [selectedSubAgent, onSelectSubAgent, subAgentsString, subAgents]); + const citations = useMemo(() => { + return extractCitationUrls(messageContent); + }, [messageContent]); + return (
( {messageContent}

) : ( - + <> + + {citations.length > 0 && ( + + )} + )}
@@ -131,7 +150,7 @@ export const ChatMessage = React.memo(
)} {hasToolCalls && ( -
+
{toolCalls.map((toolCall: ToolCall) => { if (toolCall.name === "task") return null; return ( diff --git a/apps/web/src/features/deep-agent-chat/components/Citations.tsx b/apps/web/src/features/deep-agent-chat/components/Citations.tsx new file mode 100644 index 00000000..505e1ab6 --- /dev/null +++ b/apps/web/src/features/deep-agent-chat/components/Citations.tsx @@ -0,0 +1,89 @@ +"use client"; + +import React, { useMemo } from "react"; +import { ExternalLink } from "lucide-react"; +import { Document } from "../utils"; + +interface CitationsProps { + urls: string[]; + sourceToDocumentsMap: Record; +} + +export const Citations = React.memo( + ({ urls, sourceToDocumentsMap }) => { + if (urls.length === 0) return null; + + return ( +
+
+ Sources ({urls.length}) +
+
+ {urls.map((url, index) => ( + + ))} +
+
+ ); + }, +); + +Citations.displayName = "Citations"; + +const CHARACTER_LIMIT = 40; + +interface CitationProps { + url: string; + document: Document | null; +} +export const Citation = React.memo(({ url, document }) => { + const displayUrl = + url.length > CHARACTER_LIMIT + ? url.substring(0, CHARACTER_LIMIT) + "..." + : url; + + const favicon = useMemo(() => { + try { + const urlObj = new URL(url); + const domain = urlObj.hostname; + return `https://www.google.com/s2/favicons?domain=${domain}&sz=32`; + } catch { + return undefined; + } + }, [url]); + + return ( + + <> + {favicon ? ( + { + e.currentTarget.style.display = "none"; + e.currentTarget.nextElementSibling?.classList.remove("hidden"); + }} + /> + ) : ( + + )} + + {document?.title || displayUrl} + + + + ); +}); + +Citation.displayName = "Citation"; diff --git a/apps/web/src/features/deep-agent-chat/components/ToolCallBox.tsx b/apps/web/src/features/deep-agent-chat/components/ToolCallBox.tsx index 31792c7d..77535bcf 100644 --- a/apps/web/src/features/deep-agent-chat/components/ToolCallBox.tsx +++ b/apps/web/src/features/deep-agent-chat/components/ToolCallBox.tsx @@ -10,7 +10,16 @@ import { Loader, } from "lucide-react"; import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; import { ToolCall } from "../types"; +import { extractDocumentsFromMessage, Document } from "../utils"; +import { MarkdownContent } from "./MarkdownContent"; +import { Citation } from "./Citations"; interface ToolCallBoxProps { toolCall: ToolCall; @@ -75,6 +84,13 @@ export const ToolCallBox = React.memo(({ toolCall }) => { const hasContent = result || Object.keys(args).length > 0; + const documents = useMemo(() => { + if (result && typeof result === "string") { + return extractDocumentsFromMessage(result); + } + return []; + }, [result]); + return (
(({ toolCall }) => { > Result -
-                {typeof result === "string"
-                  ? result
-                  : JSON.stringify(result, null, 2)}
-              
+ {documents.length > 0 ? ( +
+ {documents.map((document, index) => ( + + ))} +
+ ) : ( +
+                  {typeof result === "string"
+                    ? result
+                    : JSON.stringify(result, null, 2)}
+                
+ )}
)}
@@ -203,3 +230,62 @@ export const ToolCallBox = React.memo(({ toolCall }) => { }); ToolCallBox.displayName = "ToolCallBox"; + +interface DocumentViewProps { + document: Document; +} + +const DocumentView = React.memo(({ document }) => { + const [isOpen, setIsOpen] = useState(false); + + return ( + <> +
setIsOpen(true)} + > +
+ {document.title || "Untitled Document"} +
+
+ {document.content + ? document.content.substring(0, 150) + "..." + : "No content"} +
+
+ + + + + + {document.title || "Document"} + + {document.source && ( +
+ +
+ )} +
+
+ {document.content ? ( + + ) : ( +

No content available

+ )} +
+
+
+ + ); +}); + +DocumentView.displayName = "DocumentView"; diff --git a/apps/web/src/features/deep-agent-chat/utils.ts b/apps/web/src/features/deep-agent-chat/utils.ts index 1ecaf6fa..02c19e0a 100644 --- a/apps/web/src/features/deep-agent-chat/utils.ts +++ b/apps/web/src/features/deep-agent-chat/utils.ts @@ -1,6 +1,12 @@ import { Deployment } from "@/types/deployment"; import { Message } from "@langchain/langgraph-sdk"; +export interface Document { + title: string | null; + content: string | null; + source: string | null; +} + export function extractStringFromMessageContent(message: Message): string { return typeof message.content === "string" ? message.content @@ -68,3 +74,25 @@ export function deploymentSupportsDeepAgents( ) { return deployment?.supportsDeepAgents ?? false; } + +export function extractCitationUrls(text: string): string[] { + return Array.from( + text.matchAll(/\[([^\]]*)\]\(([^)]*)\)/g), + (match) => match[2], + ).filter((url, index, self) => self.indexOf(url) === index); +} + +export function extractDocumentsFromMessage(content: string): Document[] { + try { + const toolResultContent = JSON.parse(content); + return toolResultContent["documents"].map((document: any) => { + return { + title: document.title, + content: document.page_content, + source: document.source, + } as Document; + }); + } catch { + return []; + } +}