import { useChatHandler } from "@/components/chat/chat-hooks/use-chat-handler" import { ChatbotUIContext } from "@/context/context" import { LLM_LIST } from "@/lib/models/llm/llm-list" import { cn } from "@/lib/utils" import { Tables } from "@/supabase/types" import { LLM, LLMID, MessageImage, ModelProvider } from "@/types" import { IconBolt, IconCaretDownFilled, IconCaretRightFilled, IconCircleFilled, IconFileText, IconMoodSmile, IconPencil } from "@tabler/icons-react" import Image from "next/image" import { FC, useContext, useEffect, useRef, useState } from "react" import { ModelIcon } from "../models/model-icon" import { Button } from "../ui/button" import { FileIcon } from "../ui/file-icon" import { FilePreview } from "../ui/file-preview" import { TextareaAutosize } from "../ui/textarea-autosize" import { WithTooltip } from "../ui/with-tooltip" import { MessageActions } from "./message-actions" import { MessageMarkdown } from "./message-markdown" const ICON_SIZE = 32 interface MessageProps { message: Tables<"messages"> fileItems: Tables<"file_items">[] isEditing: boolean isLast: boolean onStartEdit: (message: Tables<"messages">) => void onCancelEdit: () => void onSubmitEdit: (value: string, sequenceNumber: number) => void } export const Message: FC = ({ message, fileItems, isEditing, isLast, onStartEdit, onCancelEdit, onSubmitEdit }) => { const { assistants, profile, isGenerating, setIsGenerating, firstTokenReceived, availableLocalModels, availableOpenRouterModels, chatMessages, selectedAssistant, chatImages, assistantImages, toolInUse, files, models } = useContext(ChatbotUIContext) const { handleSendMessage } = useChatHandler() const editInputRef = useRef(null) const [isHovering, setIsHovering] = useState(false) const [editedMessage, setEditedMessage] = useState(message.content) const [showImagePreview, setShowImagePreview] = useState(false) const [selectedImage, setSelectedImage] = useState(null) const [showFileItemPreview, setShowFileItemPreview] = useState(false) const [selectedFileItem, setSelectedFileItem] = useState | null>(null) const [viewSources, setViewSources] = useState(false) const handleCopy = () => { if (navigator.clipboard) { navigator.clipboard.writeText(message.content) } else { const textArea = document.createElement("textarea") textArea.value = message.content document.body.appendChild(textArea) textArea.focus() textArea.select() document.execCommand("copy") document.body.removeChild(textArea) } } const handleSendEdit = () => { onSubmitEdit(editedMessage, message.sequence_number) onCancelEdit() } const handleKeyDown = (event: React.KeyboardEvent) => { if (isEditing && event.key === "Enter" && event.metaKey) { handleSendEdit() } } const handleRegenerate = async () => { setIsGenerating(true) await handleSendMessage( editedMessage || chatMessages[chatMessages.length - 2].message.content, chatMessages, true ) } const handleStartEdit = () => { onStartEdit(message) } useEffect(() => { setEditedMessage(message.content) if (isEditing && editInputRef.current) { const input = editInputRef.current input.focus() input.setSelectionRange(input.value.length, input.value.length) } }, [isEditing]) const MODEL_DATA = [ ...models.map(model => ({ modelId: model.model_id as LLMID, modelName: model.name, provider: "custom" as ModelProvider, hostedId: model.id, platformLink: "", imageInput: false })), ...LLM_LIST, ...availableLocalModels, ...availableOpenRouterModels ].find(llm => llm.modelId === message.model) as LLM const messageAssistantImage = assistantImages.find( image => image.assistantId === message.assistant_id )?.base64 const selectedAssistantImage = assistantImages.find( image => image.path === selectedAssistant?.image_path )?.base64 const modelDetails = LLM_LIST.find(model => model.modelId === message.model) const fileAccumulator: Record< string, { id: string name: string count: number type: string description: string } > = {} const fileSummary = fileItems.reduce((acc, fileItem) => { const parentFile = files.find(file => file.id === fileItem.file_id) if (parentFile) { if (!acc[parentFile.id]) { acc[parentFile.id] = { id: parentFile.id, name: parentFile.name, count: 1, type: parentFile.type, description: parentFile.description } } else { acc[parentFile.id].count += 1 } } return acc }, fileAccumulator) return (
setIsHovering(true)} onMouseLeave={() => setIsHovering(false)} onKeyDown={handleKeyDown} >
{message.role === "system" ? (
Prompt
) : (
{message.role === "assistant" ? ( messageAssistantImage ? ( assistant image ) : ( {MODEL_DATA?.modelName}
} trigger={ } /> ) ) : profile?.image_url ? ( user image ) : ( )}
{message.role === "assistant" ? message.assistant_id ? assistants.find( assistant => assistant.id === message.assistant_id )?.name : selectedAssistant ? selectedAssistant?.name : MODEL_DATA?.modelName : profile?.display_name ?? profile?.username}
)} {!firstTokenReceived && isGenerating && isLast && message.role === "assistant" ? ( <> {(() => { switch (toolInUse) { case "none": return ( ) case "retrieval": return (
Searching files...
) default: return (
Using {toolInUse}...
) } })()} ) : isEditing ? ( ) : ( )}
{fileItems.length > 0 && (
{!viewSources ? (
setViewSources(true)} > {fileItems.length} {fileItems.length > 1 ? " Sources " : " Source "} from {Object.keys(fileSummary).length}{" "} {Object.keys(fileSummary).length > 1 ? "Files" : "File"}{" "}
) : ( <>
setViewSources(false)} > {fileItems.length} {fileItems.length > 1 ? " Sources " : " Source "} from {Object.keys(fileSummary).length}{" "} {Object.keys(fileSummary).length > 1 ? "Files" : "File"}{" "}
{Object.values(fileSummary).map((file, index) => (
{file.name}
{fileItems .filter(fileItem => { const parentFile = files.find( parentFile => parentFile.id === fileItem.file_id ) return parentFile?.id === file.id }) .map((fileItem, index) => (
{ setSelectedFileItem(fileItem) setShowFileItemPreview(true) }} >
-{" "} {fileItem.content.substring(0, 200)}...
))}
))}
)}
)}
{message.image_paths.map((path, index) => { const item = chatImages.find(image => image.path === path) return ( message image { setSelectedImage({ messageId: message.id, path, base64: path.startsWith("data") ? path : item?.base64 || "", url: path.startsWith("data") ? "" : item?.url || "", file: null }) setShowImagePreview(true) }} loading="lazy" /> ) })}
{isEditing && (
)}
{showImagePreview && selectedImage && ( { setShowImagePreview(isOpen) setSelectedImage(null) }} /> )} {showFileItemPreview && selectedFileItem && ( { setShowFileItemPreview(isOpen) setSelectedFileItem(null) }} /> )} ) }