Files
blog/write-server/src/components/AIPanel.tsx
T
2026-06-21 20:42:44 +08:00

176 lines
6.2 KiB
TypeScript

"use client";
import { useState } from "react";
import { Sparkles, Loader2, Check, Copy, Shrink, Zap, Wand2, ListFilter, FileText } from "lucide-react";
interface AIPanelProps {
// For text operations (polish/expand/shorten/fix)
selectedText: string;
onApplyText: (text: string) => void;
// For summary/tags generation
title: string;
content: string;
onApplyMeta?: (data: { summary?: string; tags?: string }) => void;
}
export default function AIPanel({ selectedText, onApplyText, title, content, onApplyMeta }: AIPanelProps) {
const [loading, setLoading] = useState<string | null>(null);
const [result, setResult] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const runAction = async (action: string) => {
setLoading(action);
setError(null);
setResult(null);
try {
const body: Record<string, string> = { action };
if (["polish", "expand", "shorten", "fix"].includes(action) && selectedText) {
body.text = selectedText;
} else if (["summary", "tags"].includes(action)) {
body.title = title;
body.content = content;
}
const res = await fetch("/api/ai", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
if (!res.ok) throw new Error((await res.json()).error || "AI error");
const data = await res.json();
setResult(data.result);
} catch (err: unknown) {
setError(err instanceof Error ? err.message : "请求失败");
} finally {
setLoading(null);
}
};
const apply = () => {
if (!result) return;
if (loading && ["summary", "tags"].includes(loading)) {
// For tags: parse and apply
if (loading === "tags" && onApplyMeta) {
onApplyMeta({ tags: result });
} else if (loading === "summary" && onApplyMeta) {
onApplyMeta({ summary: result });
}
} else {
onApplyText(result);
}
setResult(null);
};
const textActions = [
{ id: "polish", label: "润色", icon: Wand2, desc: "让文字更流畅" },
{ id: "expand", label: "扩写", icon: Zap, desc: "增加细节论述" },
{ id: "shorten", label: "缩写", icon: Shrink, desc: "精简核心信息" },
{ id: "fix", label: "纠错", icon: Check, desc: "修正错别字语法" },
];
const metaActions = [
{ id: "summary", label: "生成摘要", icon: FileText },
{ id: "tags", label: "提取标签", icon: ListFilter },
];
return (
<div>
<h3 className="text-[11px] font-medium text-muted uppercase tracking-wider mb-3">AI 助手</h3>
<div className="bg-card border border-line rounded-lg overflow-hidden">
{/* Text operations */}
<div className="p-3 border-b border-line/50">
<div className="text-[10px] font-medium text-muted/70 uppercase mb-2">文本处理</div>
<div className="space-y-1">
{textActions.map((a) => (
<button
key={a.id}
type="button"
disabled={!!loading}
onClick={() => runAction(a.id)}
className={`w-full flex items-center gap-2 px-2.5 py-1.5 text-[12px] rounded transition-colors ${
selectedText
? "text-text/70 hover:text-accent hover:bg-accent/4"
: "text-muted/30 cursor-not-allowed"
}`}
title={selectedText ? a.desc : "请先选中文字"}
>
{loading === a.id ? (
<Loader2 size={13} className="animate-spin shrink-0" />
) : (
<a.icon size={13} className="shrink-0" />
)}
<span>{a.label}</span>
</button>
))}
</div>
</div>
{/* Meta generation */}
<div className="p-3">
<div className="text-[10px] font-medium text-muted/70 uppercase mb-2">元信息生成</div>
<div className="space-y-1">
{metaActions.map((a) => (
<button
key={a.id}
type="button"
disabled={!!loading || !title}
onClick={() => runAction(a.id)}
className={`w-full flex items-center gap-2 px-2.5 py-1.5 text-[12px] rounded transition-colors ${
title
? "text-text/70 hover:text-accent hover:bg-accent/4"
: "text-muted/30 cursor-not-allowed"
}`}
>
{loading === a.id ? (
<Loader2 size={13} className="animate-spin shrink-0" />
) : (
<a.icon size={13} className="shrink-0" />
)}
<span>{a.label}</span>
</button>
))}
</div>
</div>
</div>
{/* Result panel */}
{(result || error) && (
<div className="mt-3 p-3 bg-card border border-line rounded-lg">
{error ? (
<div className="text-[12px] text-danger">{error}</div>
) : (
<>
<div className="text-[12px] text-text/80 leading-relaxed max-h-[200px] overflow-y-auto whitespace-pre-wrap">
{result}
</div>
<div className="flex items-center gap-2 mt-2">
<button
type="button"
onClick={apply}
className="flex items-center gap-1 px-2.5 py-1 text-[11px] font-medium rounded bg-accent text-white hover:bg-accent-hover transition-colors"
>
<Copy size={11} /> 应用
</button>
<button
type="button"
onClick={() => setResult(null)}
className="px-2.5 py-1 text-[11px] rounded border border-line text-muted hover:bg-surface transition-colors"
>
忽略
</button>
</div>
</>
)}
</div>
)}
{/* API key tip */}
{!selectedText && !title && (
<p className="mt-3 text-[10px] text-muted/50 leading-relaxed">
选中文字后可使用润色/扩写/缩写/纠错。填写标题后可生成摘要和标签。
</p>
)}
</div>
);
}