Spaces:
Runtime error
Runtime error
| import type React from "react"; | |
| import type { Message } from "@langchain/langgraph-sdk"; | |
| import { ScrollArea } from "@/components/ui/scroll-area"; | |
| import { Loader2, Copy, CopyCheck } from "lucide-react"; | |
| import { InputForm } from "@/components/InputForm"; | |
| import { Button } from "@/components/ui/button"; | |
| import { useState, ReactNode } from "react"; | |
| import ReactMarkdown from "react-markdown"; | |
| import { cn } from "@/lib/utils"; | |
| import { Badge } from "@/components/ui/badge"; | |
| import { | |
| ActivityTimeline, | |
| ProcessedEvent, | |
| } from "@/components/ActivityTimeline"; // Assuming ActivityTimeline is in the same dir or adjust path | |
| // Markdown component props type from former ReportView | |
| type MdComponentProps = { | |
| className?: string; | |
| children?: ReactNode; | |
| [key: string]: any; | |
| }; | |
| // Markdown components (from former ReportView.tsx) | |
| const mdComponents = { | |
| h1: ({ className, children, ...props }: MdComponentProps) => ( | |
| <h1 className={cn("text-2xl font-bold mt-4 mb-2", className)} {...props}> | |
| {children} | |
| </h1> | |
| ), | |
| h2: ({ className, children, ...props }: MdComponentProps) => ( | |
| <h2 className={cn("text-xl font-bold mt-3 mb-2", className)} {...props}> | |
| {children} | |
| </h2> | |
| ), | |
| h3: ({ className, children, ...props }: MdComponentProps) => ( | |
| <h3 className={cn("text-lg font-bold mt-3 mb-1", className)} {...props}> | |
| {children} | |
| </h3> | |
| ), | |
| p: ({ className, children, ...props }: MdComponentProps) => ( | |
| <p className={cn("mb-3 leading-7", className)} {...props}> | |
| {children} | |
| </p> | |
| ), | |
| a: ({ className, children, href, ...props }: MdComponentProps) => ( | |
| <Badge className="text-xs mx-0.5"> | |
| <a | |
| className={cn("text-blue-400 hover:text-blue-300 text-xs", className)} | |
| href={href} | |
| target="_blank" | |
| rel="noopener noreferrer" | |
| {...props} | |
| > | |
| {children} | |
| </a> | |
| </Badge> | |
| ), | |
| ul: ({ className, children, ...props }: MdComponentProps) => ( | |
| <ul className={cn("list-disc pl-6 mb-3", className)} {...props}> | |
| {children} | |
| </ul> | |
| ), | |
| ol: ({ className, children, ...props }: MdComponentProps) => ( | |
| <ol className={cn("list-decimal pl-6 mb-3", className)} {...props}> | |
| {children} | |
| </ol> | |
| ), | |
| li: ({ className, children, ...props }: MdComponentProps) => ( | |
| <li className={cn("mb-1", className)} {...props}> | |
| {children} | |
| </li> | |
| ), | |
| blockquote: ({ className, children, ...props }: MdComponentProps) => ( | |
| <blockquote | |
| className={cn( | |
| "border-l-4 border-neutral-600 pl-4 italic my-3 text-sm", | |
| className | |
| )} | |
| {...props} | |
| > | |
| {children} | |
| </blockquote> | |
| ), | |
| code: ({ className, children, ...props }: MdComponentProps) => ( | |
| <code | |
| className={cn( | |
| "bg-neutral-900 rounded px-1 py-0.5 font-mono text-xs", | |
| className | |
| )} | |
| {...props} | |
| > | |
| {children} | |
| </code> | |
| ), | |
| pre: ({ className, children, ...props }: MdComponentProps) => ( | |
| <pre | |
| className={cn( | |
| "bg-neutral-900 p-3 rounded-lg overflow-x-auto font-mono text-xs my-3", | |
| className | |
| )} | |
| {...props} | |
| > | |
| {children} | |
| </pre> | |
| ), | |
| hr: ({ className, ...props }: MdComponentProps) => ( | |
| <hr className={cn("border-neutral-600 my-4", className)} {...props} /> | |
| ), | |
| table: ({ className, children, ...props }: MdComponentProps) => ( | |
| <div className="my-3 overflow-x-auto"> | |
| <table className={cn("border-collapse w-full", className)} {...props}> | |
| {children} | |
| </table> | |
| </div> | |
| ), | |
| th: ({ className, children, ...props }: MdComponentProps) => ( | |
| <th | |
| className={cn( | |
| "border border-neutral-600 px-3 py-2 text-left font-bold", | |
| className | |
| )} | |
| {...props} | |
| > | |
| {children} | |
| </th> | |
| ), | |
| td: ({ className, children, ...props }: MdComponentProps) => ( | |
| <td | |
| className={cn("border border-neutral-600 px-3 py-2", className)} | |
| {...props} | |
| > | |
| {children} | |
| </td> | |
| ), | |
| }; | |
| // Props for HumanMessageBubble | |
| interface HumanMessageBubbleProps { | |
| message: Message; | |
| mdComponents: typeof mdComponents; | |
| } | |
| // HumanMessageBubble Component | |
| const HumanMessageBubble: React.FC<HumanMessageBubbleProps> = ({ | |
| message, | |
| mdComponents, | |
| }) => { | |
| return ( | |
| <div | |
| className={`text-white rounded-3xl break-words min-h-7 bg-neutral-700 max-w-[100%] sm:max-w-[90%] px-4 pt-3 rounded-br-lg`} | |
| > | |
| <ReactMarkdown components={mdComponents}> | |
| {typeof message.content === "string" | |
| ? message.content | |
| : JSON.stringify(message.content)} | |
| </ReactMarkdown> | |
| </div> | |
| ); | |
| }; | |
| // Props for AiMessageBubble | |
| interface AiMessageBubbleProps { | |
| message: Message; | |
| historicalActivity: ProcessedEvent[] | undefined; | |
| liveActivity: ProcessedEvent[] | undefined; | |
| isLastMessage: boolean; | |
| isOverallLoading: boolean; | |
| mdComponents: typeof mdComponents; | |
| handleCopy: (text: string, messageId: string) => void; | |
| copiedMessageId: string | null; | |
| } | |
| // AiMessageBubble Component | |
| const AiMessageBubble: React.FC<AiMessageBubbleProps> = ({ | |
| message, | |
| historicalActivity, | |
| liveActivity, | |
| isLastMessage, | |
| isOverallLoading, | |
| mdComponents, | |
| handleCopy, | |
| copiedMessageId, | |
| }) => { | |
| // Determine which activity events to show and if it's for a live loading message | |
| const activityForThisBubble = | |
| isLastMessage && isOverallLoading ? liveActivity : historicalActivity; | |
| const isLiveActivityForThisBubble = isLastMessage && isOverallLoading; | |
| return ( | |
| <div className={`relative break-words flex flex-col`}> | |
| {activityForThisBubble && activityForThisBubble.length > 0 && ( | |
| <div className="mb-3 border-b border-neutral-700 pb-3 text-xs"> | |
| <ActivityTimeline | |
| processedEvents={activityForThisBubble} | |
| isLoading={isLiveActivityForThisBubble} | |
| /> | |
| </div> | |
| )} | |
| <ReactMarkdown components={mdComponents}> | |
| {typeof message.content === "string" | |
| ? message.content | |
| : JSON.stringify(message.content)} | |
| </ReactMarkdown> | |
| <Button | |
| variant="default" | |
| className="cursor-pointer bg-neutral-700 border-neutral-600 text-neutral-300 self-end" | |
| onClick={() => | |
| handleCopy( | |
| typeof message.content === "string" | |
| ? message.content | |
| : JSON.stringify(message.content), | |
| message.id! | |
| ) | |
| } | |
| > | |
| {copiedMessageId === message.id ? "Copied" : "Copy"} | |
| {copiedMessageId === message.id ? <CopyCheck /> : <Copy />} | |
| </Button> | |
| </div> | |
| ); | |
| }; | |
| interface ChatMessagesViewProps { | |
| messages: Message[]; | |
| isLoading: boolean; | |
| scrollAreaRef: React.RefObject<HTMLDivElement | null>; | |
| onSubmit: (inputValue: string, effort: string, model: string) => void; | |
| onCancel: () => void; | |
| liveActivityEvents: ProcessedEvent[]; | |
| historicalActivities: Record<string, ProcessedEvent[]>; | |
| } | |
| export function ChatMessagesView({ | |
| messages, | |
| isLoading, | |
| scrollAreaRef, | |
| onSubmit, | |
| onCancel, | |
| liveActivityEvents, | |
| historicalActivities, | |
| }: ChatMessagesViewProps) { | |
| const [copiedMessageId, setCopiedMessageId] = useState<string | null>(null); | |
| const handleCopy = async (text: string, messageId: string) => { | |
| try { | |
| await navigator.clipboard.writeText(text); | |
| setCopiedMessageId(messageId); | |
| setTimeout(() => setCopiedMessageId(null), 2000); // Reset after 2 seconds | |
| } catch (err) { | |
| console.error("Failed to copy text: ", err); | |
| } | |
| }; | |
| return ( | |
| <div className="flex flex-col h-full"> | |
| <ScrollArea className="flex-grow" ref={scrollAreaRef}> | |
| <div className="p-4 md:p-6 space-y-2 max-w-4xl mx-auto pt-16"> | |
| {messages.map((message, index) => { | |
| const isLast = index === messages.length - 1; | |
| return ( | |
| <div key={message.id || `msg-${index}`} className="space-y-3"> | |
| <div | |
| className={`flex items-start gap-3 ${ | |
| message.type === "human" ? "justify-end" : "" | |
| }`} | |
| > | |
| {message.type === "human" ? ( | |
| <HumanMessageBubble | |
| message={message} | |
| mdComponents={mdComponents} | |
| /> | |
| ) : ( | |
| <AiMessageBubble | |
| message={message} | |
| historicalActivity={historicalActivities[message.id!]} | |
| liveActivity={liveActivityEvents} // Pass global live events | |
| isLastMessage={isLast} | |
| isOverallLoading={isLoading} // Pass global loading state | |
| mdComponents={mdComponents} | |
| handleCopy={handleCopy} | |
| copiedMessageId={copiedMessageId} | |
| /> | |
| )} | |
| </div> | |
| </div> | |
| ); | |
| })} | |
| {isLoading && | |
| (messages.length === 0 || | |
| messages[messages.length - 1].type === "human") && ( | |
| <div className="flex items-start gap-3 mt-3"> | |
| {" "} | |
| {/* AI message row structure */} | |
| <div className="relative group max-w-[85%] md:max-w-[80%] rounded-xl p-3 shadow-sm break-words bg-neutral-800 text-neutral-100 rounded-bl-none w-full min-h-[56px]"> | |
| {liveActivityEvents.length > 0 ? ( | |
| <div className="text-xs"> | |
| <ActivityTimeline | |
| processedEvents={liveActivityEvents} | |
| isLoading={true} | |
| /> | |
| </div> | |
| ) : ( | |
| <div className="flex items-center justify-start h-full"> | |
| <Loader2 className="h-5 w-5 animate-spin text-neutral-400 mr-2" /> | |
| <span>Processing...</span> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| </ScrollArea> | |
| <InputForm | |
| onSubmit={onSubmit} | |
| isLoading={isLoading} | |
| onCancel={onCancel} | |
| hasHistory={messages.length > 0} | |
| /> | |
| </div> | |
| ); | |
| } | |