176 lines
6.2 KiB
TypeScript
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>
|
|
);
|
|
}
|