import React from "react"; import { AIMessage, AIMessageChunk, BaseMessage, HumanMessage } from "@langchain/core/messages"; import { useParams, useNavigate } from "react-router-dom"; import { ChatManager } from "@/lib/chat/manager"; import { useLiveQuery } from "dexie-react-hooks"; import { mapStoredMessageToChatMessage } from "@langchain/core/messages"; import { ChatHistoryDB, DexieChatMemory } from "@/lib/chat/memory"; import { AutosizeTextarea } from "@/components/ui/autosize-textarea"; import { Button } from "@/components/ui/button"; import { Send, X, Check, Info, Paperclip, Upload, Link2, Loader2, Download, ClipboardCopy, RefreshCcw, Pencil } from "lucide-react"; import { IDocument } from "@/lib/document/types"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, DropdownMenuSeparator, } from "@/components/ui/dropdown-menu"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; import { Input as InputField } from "@/components/ui/input"; import { CHAT_MODELS } from "@/lib/config/types"; import { ConfigManager } from "@/lib/config/manager"; import { Badge } from "@/components/ui/badge"; import { DocumentManager } from "@/lib/document/manager"; import { toast } from "sonner"; import { isHumanMessage, isAIMessage } from "@langchain/core/messages"; import { ScrollArea } from "@/components/ui/scroll-area"; import ReactMarkdown from "react-markdown"; import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; import { oneDark } from "react-syntax-highlighter/dist/esm/styles/prism"; import remarkMath from "remark-math"; import rehypeKatex from "rehype-katex"; import remarkGfm from "remark-gfm"; import "katex/dist/katex.min.css"; // import { Components } from "react-markdown"; // import type { SyntaxHighlighterProps } from 'react-syntax-highlighter'; // import type { CodeProps } from 'react-markdown/lib/ast-to-react'; import { cn } from "@/lib/utils"; // Types interface MessageProps { message: BaseMessage; documentManager?: DocumentManager; setPreviewDocument?: (document: IDocument | null) => void; previewDocument?: IDocument | null; } interface MessagesProps { messages: BaseMessage[] | undefined; streamingHumanMessage: HumanMessage | null; streamingAIMessageChunks: AIMessageChunk[]; setPreviewDocument: (document: IDocument | null) => void; } interface InputProps { input: string; selectedModel: string; attachments: IDocument[]; enabledChatModels: string[] | undefined; isUrlInputOpen: boolean; urlInput: string; selectedModelName: string; isGenerating: boolean; onInputChange: (value: string) => void; onModelChange: (model: string) => void; onSendMessage: () => void; setPreviewDocument: (doc: IDocument | null) => void; setIsUrlInputOpen: (open: boolean) => void; setUrlInput: (url: string) => void; handleAttachmentFileUpload: (event: React.ChangeEvent) => void; handleAttachmentUrlUpload: () => void; handleAttachmentRemove: (docId: string) => void; } interface FilePreviewDialogProps { document: IDocument | null; onClose: () => void; } // Components const DocumentBadge = React.memo(({ document, onPreview, onRemove, removeable = true }: { document: IDocument; onPreview: () => void; onRemove: () => void; removeable?: boolean; }) => ( {document.name} {removeable && ( )} )); DocumentBadge.displayName = "DocumentBadge"; const DocumentBadgesScrollArea = React.memo(({ documents, onPreview, onRemove, removeable = true, maxHeight = "100px", className = "" }: { documents: IDocument[]; onPreview: (doc: IDocument) => void; onRemove: (docId: string) => void; removeable?: boolean; maxHeight?: string; className?: string; }) => (
{documents.map((document) => ( onPreview(document)} onRemove={() => onRemove(document.id)} removeable={removeable} /> ))}
)); DocumentBadgesScrollArea.displayName = "DocumentBadgesScrollArea"; const HumanMessageComponent = React.memo(({ message, setPreviewDocument, onEdit, onRegenerate, isEditing, onSave, onCancelEdit }: MessageProps & { onEdit?: () => void; onRegenerate?: () => void; isEditing?: boolean; onSave?: (content: string) => void; onCancelEdit?: () => void; }) => { const [editedContent, setEditedContent] = React.useState(String(message.content)); if (message.response_metadata?.documents?.length) { return (
setPreviewDocument?.(doc)} onRemove={() => {}} removeable={false} maxHeight="200px" />
); } return (
{isEditing ? (
setEditedContent(e.target.value)} className="bg-muted p-5 rounded-md" maxHeight={300} />
) : ( <>
{String(message.content)}
)}
); }); HumanMessageComponent.displayName = "HumanMessageComponent"; const AIMessageComponent = React.memo(({ message }: MessageProps) => { const handleCopy = React.useCallback(() => { const content = String(message.content); navigator.clipboard.writeText(content) .then(() => toast.success("Response copied to clipboard")) .catch(() => toast.error("Failed to copy response")); }, [message.content]); return (
{ navigator.clipboard.writeText(code); toast.success("Code copied to clipboard"); }; return match ? (
{language && ( {language} )}
{code}
) : ( {children} ); }, a(props) { return ; }, table(props) { return ; }, th(props) { return
; }, td(props) { return ; }, blockquote(props) { return
; }, ul(props) { return
    ; }, ol(props) { return
      ; }, }} > {String(message.content)}
      ); }); AIMessageComponent.displayName = "AIMessageComponent"; const Messages = React.memo(({ messages, streamingHumanMessage, streamingAIMessageChunks, setPreviewDocument, onEditMessage, onRegenerateMessage, editingMessageIndex, onSaveEdit, onCancelEdit, }: MessagesProps & { onEditMessage: (index: number) => void; onRegenerateMessage: (index: number) => void; editingMessageIndex: number | null; onSaveEdit: (content: string) => void; onCancelEdit: () => void; }) => { const { id } = useParams(); if (id === "new" || !messages) { return ; } return (
      {messages.map((message, index) => { if (isHumanMessage(message)) { return ( onEditMessage(index)} onRegenerate={() => onRegenerateMessage(index)} isEditing={editingMessageIndex === index} onSave={onSaveEdit} onCancelEdit={onCancelEdit} /> ); } if (isAIMessage(message)) { return ; } return null; })} {streamingHumanMessage && ( )} {streamingAIMessageChunks.length > 0 && ( chunk.content).join(""))} /> )}
      ); }); Messages.displayName = "Messages"; const FilePreviewDialog = React.memo(({ document: fileDoc, onClose }: FilePreviewDialogProps) => { const [content, setContent] = React.useState(null); const [isLoading, setIsLoading] = React.useState(true); const [error, setError] = React.useState(null); const documentManager = React.useMemo(() => DocumentManager.getInstance(), []); React.useEffect(() => { if (!fileDoc) return; const loadContent = async () => { try { setIsLoading(true); setError(null); const file = await documentManager.getDocument(fileDoc.id); if (fileDoc.type === "image") { const reader = new FileReader(); reader.onload = () => setContent(reader.result as string); reader.readAsDataURL(file); } else if (fileDoc.type === "pdf") { const url = URL.createObjectURL(file); setContent(url); return () => URL.revokeObjectURL(url); } else { const text = await file.text(); setContent(text); } } catch (err) { console.error(err); setError("Failed to load file content"); } finally { setIsLoading(false); } }; loadContent(); }, [fileDoc, documentManager]); const handleDownload = React.useCallback(async () => { if (!fileDoc) return; try { const file = await documentManager.getDocument(fileDoc.id); const url = URL.createObjectURL(file); const a = document.createElement('a'); a.href = url; a.download = fileDoc.name; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } catch (err) { console.error(err); toast.error("Failed to download file"); } }, [fileDoc, documentManager]); if (!fileDoc) return null; const renderContent = () => { if (isLoading) { return (
      ); } if (error) { return (
      {error}
      ); } if (content) { if (fileDoc.type === "image") { return (
      {fileDoc.name}
      ); } if (fileDoc.type === "pdf") { return