"use client"; import { useState, useRef, useCallback } from "react"; import { Button } from "@/components/ui/button"; import { Sparkles, Wand2, Maximize2, Minimize2, ArrowRight, Languages, FileText, Bug, X, Loader2, Copy, Check, } from "lucide-react"; interface AiAssistantProps { /** 当前编辑器的纯文本内容 */ content: string; /** 将 AI 结果插入/替换到编辑器 */ onInsert: (text: string, mode: "replace" | "append") => void; } type Action = "polish" | "expand" | "shorten" | "continue" | "translate_en" | "translate_zh" | "summarize" | "fix_grammar"; const ACTIONS: { key: Action; label: string; icon: React.ReactNode; needSelection?: boolean }[] = [ { key: "polish", label: "润色", icon: }, { key: "expand", label: "扩写", icon: }, { key: "shorten", label: "精简", icon: }, { key: "continue", label: "续写", icon: }, { key: "fix_grammar", label: "纠错", icon: }, { key: "translate_en", label: "中→英", icon: }, { key: "translate_zh", label: "英→中", icon: }, { key: "summarize", label: "摘要", icon: }, ]; export default function AiAssistant({ content, onInsert }: AiAssistantProps) { const [open, setOpen] = useState(false); const [loading, setLoading] = useState(false); const [result, setResult] = useState(""); const [customPrompt, setCustomPrompt] = useState(""); const [copied, setCopied] = useState(false); const abortRef = useRef(null); const stopGeneration = useCallback(() => { abortRef.current?.abort(); abortRef.current = null; setLoading(false); }, []); async function runAction(action?: Action) { const text = content.replace(/<[^>]+>/g, "").trim(); if (!text && action !== "summarize") { setResult("请先输入文章内容"); setOpen(true); return; } setLoading(true); setResult(""); setOpen(true); abortRef.current = new AbortController(); try { const res = await fetch("/api/ai", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ prompt: customPrompt || undefined, selectedText: text || undefined, action, }), signal: abortRef.current.signal, }); if (!res.ok) { const err = await res.json(); setResult(`错误:${err.error || "请求失败"}`); setLoading(false); return; } // 读取 SSE 流 const reader = res.body?.getReader(); const decoder = new TextDecoder(); let accumulated = ""; if (reader) { while (true) { const { done, value } = await reader.read(); if (done) break; const chunk = decoder.decode(value, { stream: true }); // 解析 SSE 数据 for (const line of chunk.split("\n")) { if (line.startsWith("data: ")) { const data = line.slice(6); if (data === "[DONE]") continue; try { const parsed = JSON.parse(data); const delta = parsed.choices?.[0]?.delta?.content; if (delta) { accumulated += delta; setResult(accumulated); } } catch { // 忽略解析错误 } } } } } } catch (err) { if (err instanceof Error && err.name === "AbortError") { setResult((prev) => prev + "\n\n[已停止]"); } else { setResult(`错误:${err instanceof Error ? err.message : "未知错误"}`); } } finally { setLoading(false); abortRef.current = null; } } function handleCopy() { navigator.clipboard.writeText(result); setCopied(true); setTimeout(() => setCopied(false), 2000); } function handleInsert(mode: "replace" | "append") { onInsert(result, mode); setResult(""); setOpen(false); } return (
{/* 触发按钮 */} {open && (
{/* 关闭按钮 */}
选择操作或输入自定义指令
{/* 操作按钮组 */}
{ACTIONS.map((a) => ( ))}
{/* 自定义指令 */}
setCustomPrompt(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); runAction(); } }} placeholder="或输入自定义指令..." className="flex-1 h-8 px-3 rounded-lg border border-border bg-transparent font-sans text-xs text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring" disabled={loading} />
{/* 结果 */} {result && (
{result}
{loading && ( )}
{/* 操作按钮 */} {!loading && result && (
)}
)}
)}
); }