import { Avatar } from "@/components/ui/avatar"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Button } from "@/components/ui/button"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; import { Message as MessageType, ResearchTree } from "@/lib/types"; import { Bot, Copy, MoreHorizontal, User2, ExternalLink, Loader2, SearchIcon, Terminal, ChevronDown, ChevronRight } from "lucide-react"; import React, { useState, useEffect, useMemo, useRef } from "react"; import ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; import { oneDark } from "react-syntax-highlighter/dist/cjs/styles/prism"; import { Badge } from "@/components/ui/badge"; import { Card, CardContent, CardHeader } from "@/components/ui/card"; // Component to display tool calls nicely const ToolCallDisplay = ({ toolCall }: { toolCall: any }) => { const getDisplayArgs = () => { if (!toolCall.args) return ""; if (typeof toolCall.args === "string") return toolCall.args; if (toolCall.args.query) return toolCall.args.query; if (toolCall.args.url) return toolCall.args.url; return JSON.stringify(toolCall.args); }; const args = getDisplayArgs(); return (
{toolCall.name} {args}
); }; // Function to extract all source URLs from the research tree const extractAllSources = (tree: ResearchTree | undefined): Array<{ text: string; url: string }> => { if (!tree) return []; // Start with an empty set to avoid duplicates const uniqueSources = new Set(); // Recursive function to gather all sources const collectSources = (node: ResearchTree) => { // Add all sources from the current node if (node.sources && Array.isArray(node.sources)) { node.sources.forEach((url) => uniqueSources.add(url)); } // Process all children recursively if (node.children && Array.isArray(node.children)) { node.children.forEach((child) => collectSources(child)); } }; // Start the collection process collectSources(tree); // Convert the set to an array of objects with text and url properties return Array.from(uniqueSources).map((url) => { // Try to extract a readable title from the URL let text = ""; try { const urlObj = new URL(url); // Remove 'www.' if present and take the hostname text = urlObj.hostname.replace(/^www\./, ""); // Add the pathname if it's not just "/" if (urlObj.pathname && urlObj.pathname !== "/") { // Format the pathname - keep it short and clean const path = urlObj.pathname.split("/").filter(Boolean); if (path.length > 0) { const lastPathSegment = path[path.length - 1] .replace(/[-_]/g, " ") // Replace dashes and underscores with spaces .replace(/\.html$|\.pdf$|\.php$/, ""); // Remove common extensions text = `${text} - ${lastPathSegment}`; } } } catch (e) { // If URL parsing fails, use the URL as is text = url; } return { text, url }; }); }; // SourceLinks component for displaying research sources in a scrollable container const SourceLinks = ({ links }: { links: Array<{ text: string; url: string }> }) => { if (!links || links.length === 0) return null; return (

Research Sources:

{links.map((link, index) => { // Extract domain for display let domain = ""; try { const urlObj = new URL(link.url); domain = urlObj.hostname.replace(/^www\./, ""); } catch (e) { domain = "Unknown source"; } return (
Copy link
); })}
); }; // ImageGallery component for handling images in a scrollable container const ImageGallery = ({ imageUrls }: { imageUrls: string[] }) => { const [loadedImages, setLoadedImages] = useState([]); // Lazy load images using Intersection Observer useEffect(() => { const observer = new IntersectionObserver( (entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { const img = entry.target as HTMLImageElement; const src = img.getAttribute("data-src"); if (src) { img.src = src; observer.unobserve(img); setLoadedImages((prev) => [...prev, src]); } } }); }, { rootMargin: "100px" } ); const imgPlaceholders = document.querySelectorAll(".lazy-image"); imgPlaceholders.forEach((img) => observer.observe(img)); return () => observer.disconnect(); }, [imageUrls]); if (!imageUrls || imageUrls.length === 0) return null; return (

Relevant Images:

{imageUrls.map((url, index) => (
{`Research { const target = e.target as HTMLImageElement; target.onerror = null; target.src = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100' height='100' viewBox='0 0 100 100'%3E%3Crect width='100' height='100' fill='%23f1f1f1'/%3E%3Ctext x='50%' y='50%' font-size='12' text-anchor='middle' alignment-baseline='middle' font-family='Arial, sans-serif'%3EImage failed to load%3C/text%3E%3C/svg%3E"; }} />
))}
); }; const MarkdownComponents: Record> = { h1: ({ children }) =>

{children}

, h2: ({ children }) =>

{children}

, h3: ({ children }) =>

{children}

, p: ({ children }) =>

{children}

, ul: ({ children }) =>
    {children}
, ol: ({ children }) =>
    {children}
, li: ({ children }) =>
  • {children}
  • , code: ({ node, inline, className, children, ...props }) => { const match = /language-(\w+)/.exec(className || ""); const language = match ? match[1] : ""; return Object.keys(node.properties).length === 0 ? ( {children} ) : ( {String(children).replace(/\n$/, "")} ); }, pre: ({ children }) =>
    {children}
    , a: ({ children, href }) => ( {children} ), blockquote: ({ children }) =>
    {children}
    , table: ({ children }) => (
    {children}
    ), thead: ({ children }) => {children}, tbody: ({ children }) => {children}, th: ({ children }) => {children}, tr: ({ children }) => {children}, td: ({ children }) => {children}, }; interface MessageProps { message: MessageType; isLoading?: boolean; connectionMode: "agent" | "workflow"; } const Message = ({ message, isLoading, connectionMode }: MessageProps) => { const isUser = message?.role === "user"; const [imageUrls, setImageUrls] = useState([]); const progressPercentage = message.progress || 0; const isProgressMessage = message.isProgress === true && connectionMode === "workflow"; const [isSearchMessage, setIsSearchMessage] = useState(message.content.startsWith("s_")); // Use useMemo to extract sources only once and only when they change const sourceLinks = useMemo(() => { // If this is the loading component, return empty sources if (isLoading) return []; // First, extract sources from the research_tree if available const researchSources = extractAllSources(message.research_tree); // If research_tree sources exist, use those if (researchSources.length > 0) { return researchSources; } // Otherwise, fall back to any links in the media object return message.media?.links || []; }, [message.research_tree, message.media?.links, isLoading]); // Extract image URLs from the message content or use the media object useEffect(() => { if (isLoading) { setImageUrls([]); return; } if (!isUser) { // Handle image URLs let urls: string[] = []; // First, check if there's a media object with images if (message.media?.images && message.media.images.length > 0) { urls = message.media.images; } setImageUrls(urls); } setIsSearchMessage(message.content.startsWith("s_")); }, [message.content, message.media, isUser, isLoading]); const copyToClipboard = () => { if (!isLoading) { navigator.clipboard.writeText(message.content); } }; return (
    {isUser ? "You" : "KNet"}
    {!isUser && !isLoading &&
    {new Date(message.timestamp).toLocaleTimeString()}
    } {(isLoading || isProgressMessage) && connectionMode === "workflow" &&
    Just now
    } {!isUser && !isLoading && !isProgressMessage && (
    Copy to clipboard Copy text Show sources View in visualizations
    )}
    {(isLoading || isProgressMessage) && connectionMode === "workflow" ? (
    {message.content}
    {isSearchMessage ? ( {message.content.slice(2)} ) : null} {progressPercentage}%
    ) : ( <> {message.content && message.content.trim() !== "" && ( {message.content} )} {message.tool_calls && message.tool_calls.length > 0 && (
    Tool Usage
    {message.tool_calls.map((toolCall, index) => ( ))}
    )} {sourceLinks.length > 0 && } {imageUrls.length > 0 && } )}
    ); }; export default Message;