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}
)}
{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 !== "" && (
)}
{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.
>
)}
Wiki & docs
);
}