"use client"; import { useState, useRef, useEffect } from "react"; import { Send, User, Bot, Copy, FileText, X, Sparkles, Eraser } from "lucide-react"; import { cn } from "@/lib/utils"; import { useSettingsStore } from "@/lib/settings-store"; import { Editor } from "@tiptap/react"; import { marked } from "marked"; import { sanitizeHtml } from "@/lib/sanitize-html"; interface Message { id: string; role: "user" | "assistant" | "system"; content: string; } interface AIChatPanelProps { editor: Editor | null; isOpen: boolean; onClose: () => void; } export function AIChatPanel({ editor, isOpen, onClose }: AIChatPanelProps) { const { aiConfig } = useSettingsStore(); const [messages, setMessages] = useState([]); const [input, setInput] = useState(""); const [isLoading, setIsLoading] = useState(false); const [useContext, setUseContext] = useState(true); const messagesEndRef = useRef(null); const [tokenCount, setTokenCount] = useState(0); // ... (rest of the code) // Simple token estimation: ~4 chars per token for English, ~1 char per token for Chinese useEffect(() => { const text = messages.map(m => m.content).join("") + input; setTokenCount(Math.ceil(text.length * 0.7)); }, [messages, input]); // Scroll to bottom effect const scrollToBottom = () => { messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); }; useEffect(() => { scrollToBottom(); }, [messages]); const abortControllerRef = useRef(null); const handleStop = () => { if (abortControllerRef.current) { abortControllerRef.current.abort(); abortControllerRef.current = null; } setIsLoading(false); }; const handleSend = async () => { if (!input.trim() || isLoading) return; if (!aiConfig.apiKey) { setMessages(prev => [...prev, { id: Date.now().toString(), role: "system", content: "Error: No API Key configured. Please go to Settings > AI Config." }]); return; } const userMsg: Message = { id: Date.now().toString(), role: "user", content: input }; setMessages(prev => [...prev, userMsg]); setInput(""); setIsLoading(true); abortControllerRef.current = new AbortController(); const contextMsg: Message | null = (useContext && editor) ? { id: "context", role: "system", content: `Current Document Context:\n${editor.getText().slice(0, 4000)}...` } : null; const apiMessages = [ ...(contextMsg ? [contextMsg] : []), ...messages.filter(m => m.role !== "system"), userMsg ]; try { const res = await fetch("/api/ai/chat", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ messages: apiMessages.map(m => ({ role: m.role, content: m.content })), config: aiConfig }), signal: abortControllerRef.current.signal }); if (!res.ok) throw new Error(res.statusText); if (!res.body) throw new Error("No response body"); const reader = res.body.getReader(); const decoder = new TextDecoder(); const assistantMsg: Message = { id: (Date.now() + 1).toString(), role: "assistant", content: "" }; setMessages(prev => [...prev, assistantMsg]); while (true) { const { done, value } = await reader.read(); if (done) break; const chunk = decoder.decode(value); const lines = chunk.split("\n\n"); for (const line of lines) { if (line.startsWith("data: ")) { const data = line.slice(6); if (data === "[DONE]") break; try { const parsed = JSON.parse(data); const content = parsed.choices[0]?.delta?.content || ""; if (content) { assistantMsg.content += content; setMessages(prev => prev.map(m => m.id === assistantMsg.id ? { ...assistantMsg } : m)); } } catch (e) { console.error("Parse error", e); } } } } } catch (e: unknown) { const isAbortError = e instanceof Error && e.name === "AbortError"; if (isAbortError) { setMessages(prev => [...prev, { id: Date.now().toString(), role: "system", content: "Genertion stopped by user." }]); } else { setMessages(prev => [...prev, { id: Date.now().toString(), role: "system", content: `Error: ${e instanceof Error ? e.message : "Unknown error"}` }]); } } finally { setIsLoading(false); abortControllerRef.current = null; } }; const handleInsert = (content: string) => { if (editor) { editor.commands.insertContent(content); } }; if (!isOpen) return null; return (
{/* Header */}

AI 助手

{aiConfig.model || "gpt-3.5-turbo"}

Est. Tokens: {tokenCount}
{/* Messages */}
{messages.length === 0 && (

有什么我可以帮你的吗?

我可以协助写作、润色、摘要或回答问题。

)} {messages.map((msg) => (
{msg.role === "user" ? : }
{/* Markdown Rendering for Assistant */} {msg.role === "assistant" ? (
) : (

{msg.content}

)} {/* Assistant Actions */} {msg.role === "assistant" && !isLoading && (
)}
))}
{/* Input Area */}