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 (
),
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 (