"use client" import { useEffect, useRef, useState } from "react" import { Send, Loader2, Copy, RefreshCw, Trash2, Plus, MessageSquare, X, Sparkles } from "lucide-react" import { toast } from "sonner" import { Button } from "@/components/ui/button" import { Textarea } from "@/components/ui/textarea" import { cn } from "@/lib/utils" import { useAISettings } from "./ai-settings-provider" import { Markdown } from "./markdown" import { requestOpenAICompatible } from "@/lib/ai/client" import { validateForRequest } from "@/lib/ai/settings" import { buildChatSystemPrompt } from "@/lib/ai/prompts" import { addMessage, clearMessages, createConversation, deleteLastAssistantMessage, fetchMessages, getLatestConversation, type AIConversation, } from "@/lib/ai/conversations" import type { ChatMessage } from "@/lib/ai/types" import type { KnowledgeItem } from "@/lib/types" type UIMessage = { role: "user" | "assistant"; content: string } export function AIChatPanel({ item }: { item: KnowledgeItem }) { const { settings } = useAISettings() const [conversation, setConversation] = useState(null) const [messages, setMessages] = useState([]) const [input, setInput] = useState("") const [loading, setLoading] = useState(false) const [booting, setBooting] = useState(true) const controllerRef = useRef(null) const scrollRef = useRef(null) // load latest conversation + messages on mount / item change useEffect(() => { let active = true setBooting(true) setMessages([]) setConversation(null) ;(async () => { try { const conv = await getLatestConversation(item.id) if (!active) return if (conv) { setConversation(conv) const rows = await fetchMessages(conv.id) if (!active) return setMessages(rows.filter((r) => r.role !== "system").map((r) => ({ role: r.role as "user" | "assistant", content: r.content }))) } } catch (e) { console.log("[v0] load conversation error:", e) } finally { if (active) setBooting(false) } })() return () => { active = false controllerRef.current?.abort() } }, [item.id]) useEffect(() => { scrollRef.current?.scrollTo({ top: scrollRef.current.scrollHeight, behavior: "smooth" }) }, [messages, loading]) async function ensureConversation(): Promise { if (conversation) return conversation const conv = await createConversation(item.id, item.title) setConversation(conv) return conv } async function runCompletion(history: UIMessage[]): Promise { const chatMessages: ChatMessage[] = history.map((m) => ({ role: m.role, content: m.content })) const ac = new AbortController() controllerRef.current = ac return requestOpenAICompatible({ baseUrl: settings.baseUrl, apiKey: settings.apiKey, model: settings.model, systemPrompt: buildChatSystemPrompt(item), messages: chatMessages, temperature: settings.temperature, maxTokens: settings.maxTokens, signal: ac.signal, }) } async function handleSend() { const err = validateForRequest(settings) if (err) { toast.error(err) return } const text = input.trim() if (!text || loading) return const userMsg: UIMessage = { role: "user", content: text } const next = [...messages, userMsg] setMessages(next) setInput("") setLoading(true) try { const conv = await ensureConversation() await addMessage(conv.id, "user", text) const reply = await runCompletion(next) setMessages((prev) => [...prev, { role: "assistant", content: reply }]) await addMessage(conv.id, "assistant", reply) } catch (e) { if ((e as Error).name === "AbortError") { toast.message("已取消回复") setMessages((prev) => prev.slice(0, -1)) // remove the user msg we optimistically added? keep it instead return } toast.error((e as Error).message) setMessages((prev) => [...prev, { role: "assistant", content: `[出错] ${(e as Error).message}` }]) } finally { setLoading(false) controllerRef.current = null } } async function handleRegenerate() { if (loading) return // find last user message const lastUserIdx = [...messages].reverse().findIndex((m) => m.role === "user") if (lastUserIdx === -1) return const historyEnd = messages.length - lastUserIdx // index just after last user message const history = messages.slice(0, historyEnd) setMessages(history) setLoading(true) try { const conv = await ensureConversation() await deleteLastAssistantMessage(conv.id) const reply = await runCompletion(history) setMessages((prev) => [...prev, { role: "assistant", content: reply }]) await addMessage(conv.id, "assistant", reply) } catch (e) { if ((e as Error).name === "AbortError") return toast.error((e as Error).message) } finally { setLoading(false) controllerRef.current = null } } async function handleNewConversation() { controllerRef.current?.abort() try { const conv = await createConversation(item.id, item.title) setConversation(conv) setMessages([]) toast.success("已新建会话") } catch (e) { toast.error("新建会话失败") console.log("[v0] new conversation error:", e) } } async function handleClear() { if (!conversation) { setMessages([]) return } try { await clearMessages(conversation.id) setMessages([]) toast.success("已清空当前会话") } catch (e) { toast.error("清空失败") console.log("[v0] clear error:", e) } } function copyMessage(content: string) { navigator.clipboard.writeText(content) toast.success("已复制") } const hasAssistant = messages.some((m) => m.role === "assistant") return (

AI 追问

{booting ? (
加载会话...
) : messages.length === 0 ? (
针对「{item.title}」向 AI 提问,开始复习对话
) : ( messages.map((m, i) => (
{m.role === "assistant" ? ( ) : (

{m.content}

)} {m.role === "assistant" && ( )}
)) )} {loading && (
AI 正在思考...
)}
{hasAssistant && !loading && (
)}