"use client"; import { useRef, useEffect, useState } from "react"; import { useChat } from "@ai-sdk/react"; import { DefaultChatTransport } from "ai"; import { Streamdown } from "streamdown"; const STORAGE_KEY = "docs-chat-messages"; const transport = new DefaultChatTransport({ api: "/api/docs-chat" }); export function DocsChat() { const [open, setOpen] = useState(false); const [input, setInput] = useState(""); const messagesEndRef = useRef(null); const inputRef = useRef(null); const containerRef = useRef(null); const restoredRef = useRef(false); const { messages, sendMessage, status, setMessages } = useChat({ transport }); const isLoading = status === "streaming" || status === "submitted"; // Restore messages from sessionStorage on mount useEffect(() => { if (restoredRef.current) return; restoredRef.current = true; try { const stored = sessionStorage.getItem(STORAGE_KEY); if (stored) { const parsed = JSON.parse(stored); if (Array.isArray(parsed) && parsed.length > 0) { setMessages(parsed); } } } catch { // ignore parse errors } }, [setMessages]); // Save completed messages to sessionStorage useEffect(() => { if (!restoredRef.current) return; if (isLoading) return; if (messages.length === 0) { sessionStorage.removeItem(STORAGE_KEY); return; } try { sessionStorage.setItem(STORAGE_KEY, JSON.stringify(messages)); } catch { // ignore quota errors } }, [messages, isLoading]); // Auto-open when new messages arrive const prevMessageCount = useRef(0); useEffect(() => { if (messages.length > prevMessageCount.current) { setOpen(true); } prevMessageCount.current = messages.length; }, [messages.length]); // Scroll to bottom when messages change useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); }, [messages]); // Close message area when clicking outside useEffect(() => { if (!open) return; const handleClickOutside = (e: MouseEvent) => { if ( containerRef.current && !containerRef.current.contains(e.target as Node) ) { setOpen(false); } }; document.addEventListener("mousedown", handleClickOutside); return () => document.removeEventListener("mousedown", handleClickOutside); }, [open]); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if (!input.trim() || isLoading) return; sendMessage({ text: input }); setInput(""); }; const handleClear = () => { setMessages([]); sessionStorage.removeItem(STORAGE_KEY); setOpen(false); inputRef.current?.focus(); }; const getTextFromParts = ( parts: (typeof messages)[number]["parts"], ): string => { return parts .filter( (p): p is Extract => p.type === "text", ) .map((p) => p.text) .join(""); }; return (
{/* Messages panel */} {open && messages.length > 0 && (
json-render Docs
{messages.map((message) => { const text = getTextFromParts(message.parts); if (!text) return null; return (
{message.role === "assistant" ? (
{text}
) : (
{text}
)}
); })}
)} {/* Input bar */}
inputRef.current?.focus()} className="flex items-end gap-2 bg-background border border-border rounded-lg shadow-lg px-4 py-3 cursor-text" >