import { useEffect, useRef, useState } from "react"; import { BookOpen, Bot, Cpu, FileText, Menu, PanelLeftOpen, Paperclip, ScanText, SendHorizontal, Share2, User as UserIcon, X } from "lucide-react"; import type { Attachment, ChatMessage, Conversation, DriverModel, User } from "../lib/types.ts"; import { AssistantAvatar, Avatar, FlowerMark, IconButton, Picker, type PickerGroup, Spinner } from "./ui.tsx"; import { Markdown } from "./Markdown.tsx"; import { cn } from "../lib/cn.ts"; import { useFeatures } from "../lib/features.ts"; function MessageBubble({ msg, authorAvatar, authorName }: { msg: ChatMessage; authorAvatar?: string; authorName: string }) { const isUser = msg.role === "user"; return (
{isUser ? : }
{isUser ? : } {isUser ? "You" : "KisAssistant"}
{isUser ? (
{msg.content}
) : (
)}
); } export function Chat({ user, conv, messages, streaming, sending, models, model, onModelChange, onSend, onShare, sidebarCollapsed, onExpandSidebar, onOpenNav, attachments, attachError, ocrAvailable, onRemoveAttachment, onPickFile, }: { user: User; conv: Conversation | null; messages: ChatMessage[]; streaming: string; sending: boolean; models: DriverModel[]; model: string; onModelChange: (m: string) => void; onSend: (text: string) => void; onShare: () => void; sidebarCollapsed: boolean; onExpandSidebar: () => void; onOpenNav: () => void; attachments: Attachment[]; attachError: string | null; ocrAvailable: boolean; onRemoveAttachment: (id: string) => void; onPickFile: (kind: "ocr" | "text", file: File) => void; }) { const [draft, setDraft] = useState(""); const [attachOpen, setAttachOpen] = useState(false); const features = useFeatures(); const boxRef = useRef(null); const taRef = useRef(null); const imgRef = useRef(null); const txtRef = useRef(null); const modelGroups: PickerGroup[] = (() => { const byDriver = new Map(); for (const m of models) { const list = byDriver.get(m.driver) ?? []; list.push({ value: m.id, label: m.label, hint: m.id }); byDriver.set(m.driver, list); } return [...byDriver.entries()].map(([driver, options]) => ({ group: driver, options })); })(); useEffect(() => { const el = boxRef.current; if (el) el.scrollTop = el.scrollHeight; }, [messages, streaming]); function autoGrow() { const ta = taRef.current; if (ta) { ta.style.height = "auto"; ta.style.height = Math.min(ta.scrollHeight, 200) + "px"; } } function submit(e?: React.FormEvent) { e?.preventDefault(); const text = draft.trim(); if (!text || sending) return; setDraft(""); requestAnimationFrame(autoGrow); onSend(text); } return (
{sidebarCollapsed && ( )} {conv?.title || "New chat"} {conv?.topic && ( {conv.topic} )}
{conv && ( )}
{messages.length === 0 && !streaming && (

How can I help, {user.displayName.split(" ")[0]}?

Pick a model, then type below. Markdown is supported.

)} {messages.filter((m) => m.role !== "system").map((m, i) => ( ))} {streaming !== "" && (
KisAssistant
)} {sending && streaming === "" && (
Thinking…
)}
{attachError && (

{attachError}

)} {attachments.length > 0 && (
{attachments.map((a) => ( {a.kind === "ocr" ? : } {a.name} ))}
)}
{attachOpen && ( <>
setAttachOpen(false)} />

Uploads go through platform tools only. Images are sent to the model as reviewed text — never as raw images.

)}
{features.attachments && ( )} { const f = e.target.files?.[0]; if (f) onPickFile("ocr", f); e.target.value = ""; }} /> { const f = e.target.files?.[0]; if (f) onPickFile("text", f); e.target.value = ""; }} />