"use client"; import { useState, useRef, useCallback } from "react"; import { Button } from "@/components/ui/button"; import { Sparkles, Wand2, Maximize2, Minimize2, ArrowRight, Languages, FileText, Bug, Loader2, Copy, Check, Heading, Send, RefreshCw, } from "lucide-react"; interface AiAssistantProps { content: string; selectedText?: string; onInsert: (text: string, mode: "replace" | "append") => void; onGenerateExcerpt?: (text: string) => void; onGenerateTitle?: (text: string) => void; } type Action = "polish" | "expand" | "shorten" | "continue" | "translate_en" | "translate_zh" | "summarize" | "fix_grammar"; export default function AiAssistant({ content, selectedText, onInsert, onGenerateExcerpt, onGenerateTitle, }: AiAssistantProps) { const [loading, setLoading] = useState(false); const [result, setResult] = useState(""); const [resultLabel, setResultLabel] = useState(""); const [generateTarget, setGenerateTarget] = useState<"title" | "excerpt" | null>(null); const [customPrompt, setCustomPrompt] = useState(""); const [copied, setCopied] = useState(false); const abortRef = useRef(null); const stopGeneration = useCallback(() => { abortRef.current?.abort(); abortRef.current = null; setLoading(false); setGenerateTarget(null); }, []); function getEffectiveText(): string { if (selectedText?.trim()) return selectedText.trim(); return content.replace(/<[^>]+>/g, "").trim(); } const hasSelection = !!(selectedText?.trim()); async function runAction(action?: Action, label?: string) { const text = getEffectiveText(); if (!text && action !== "summarize") { setResult("请先输入文章内容或选中文字"); setResultLabel(""); return; } setLoading(true); setResult(""); setResultLabel(label || ""); setGenerateTarget(null); 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; } 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 }); 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 { /* ignore */ } } } } } } 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; setGenerateTarget(null); } } async function runGenerate(target: "title" | "excerpt") { const text = content.replace(/<[^>]+>/g, "").trim(); if (!text) { setResult("请先输入文章内容"); setResultLabel(""); return; } setLoading(true); setResult(""); setResultLabel(target === "title" ? "生成标题" : "生成摘要"); setGenerateTarget(target); abortRef.current = new AbortController(); const promptText = target === "title" ? `请为以下文章生成一个简洁的中文标题(不超过30字),直接输出标题,不要引号:\n\n${text}` : `请为以下文章写一段摘要(2-3句话,不超过200字),直接输出摘要:\n\n${text}`; try { const res = await fetch("/api/ai", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ prompt: promptText, selectedText: text }), signal: abortRef.current.signal, }); if (!res.ok) { const err = await res.json(); setResult(`错误:${err.error || "请求失败"}`); setLoading(false); setGenerateTarget(null); return; } 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 }); 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 { /* ignore */ } } } } } } 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; setGenerateTarget(null); } } function applyGenerated() { if (generateTarget === "title" && onGenerateTitle && result) { onGenerateTitle(result); setResult(""); setResultLabel(""); setGenerateTarget(null); } else if (generateTarget === "excerpt" && onGenerateExcerpt && result) { onGenerateExcerpt(result); setResult(""); setResultLabel(""); setGenerateTarget(null); } } function handleCopy() { navigator.clipboard.writeText(result); setCopied(true); setTimeout(() => setCopied(false), 2000); } function handleInsert(mode: "replace" | "append") { onInsert(result, mode); setResult(""); setResultLabel(""); } return (
{/* ── 标题栏 ── */}
AI 写作助手
{/* ── 选中提示 ── */} {hasSelection && (

📌 已选中 {selectedText.length} 字,AI 将只处理选中内容

)} {/* ── 文本处理 ── */}

文本处理

{[ { key: "polish" as Action, label: "润色", icon: }, { key: "expand" as Action, label: "扩写", icon: }, { key: "shorten" as Action, label: "精简", icon: }, { key: "continue" as Action, label: "续写", icon: }, { key: "fix_grammar" as Action, label: "纠错", icon: }, { key: "summarize" as Action, label: "摘要", icon: }, ].map((a) => ( ))}
{/* ── 翻译 ── */}

翻译

{[ { key: "translate_en" as Action, label: "中 → 英", icon: }, { key: "translate_zh" as Action, label: "英 → 中", icon: }, ].map((a) => ( ))}
{/* ── 智能生成 ── */}

智能生成

{/* ── 自定义指令 ── */}
setCustomPrompt(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); runAction(undefined, "自定义指令"); } }} 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/60 focus:outline-none focus:ring-1 focus:ring-ring" disabled={loading} />
{/* ── 结果区 ── */} {(result || loading) && (
{/* 分割线 */}
{loading ? "生成中..." : "结果"}
{/* 结果标签 */} {resultLabel && !loading && result && (

{resultLabel}

)} {/* 结果内容 */}
{loading && !result && (
AI 正在思考...
)} {result && (
{result} {loading && }
)} {loading && result && ( )}
{/* 操作按钮 */} {!loading && result && (
{generateTarget ? ( ) : (
)}
)}
)} {/* ── 空状态 ── */} {!result && !loading && (

选中文字可以局部处理
或直接点击上方按钮处理全文

)}
); }