243 lines
9.2 KiB
TypeScript
243 lines
9.2 KiB
TypeScript
"use client";
|
||
|
||
import { BookOpen, Sparkles } from "lucide-react";
|
||
import { AppModal, AppModalHeader } from "@/shared/ui/AppModal";
|
||
import { useScriptLearning } from "./script-learning/useScriptLearning";
|
||
|
||
interface ScriptLearningModalProps {
|
||
isOpen: boolean;
|
||
onClose: () => void;
|
||
onApply?: (text: string) => void;
|
||
}
|
||
|
||
const WORD_COUNT_MIN = 80;
|
||
const WORD_COUNT_MAX = 1000;
|
||
|
||
export default function ScriptLearningModal({ isOpen, onClose, onApply }: ScriptLearningModalProps) {
|
||
const {
|
||
step,
|
||
inputUrl,
|
||
setInputUrl,
|
||
topics,
|
||
selectedTopic,
|
||
setSelectedTopic,
|
||
wordCount,
|
||
setWordCount,
|
||
generatedScript,
|
||
error,
|
||
analysisId,
|
||
handleAnalyze,
|
||
handleGenerate,
|
||
handleRegenerate,
|
||
backToInput,
|
||
backToTopics,
|
||
copyToClipboard,
|
||
} = useScriptLearning({ isOpen });
|
||
|
||
if (!isOpen) return null;
|
||
|
||
const wordCountNum = Number(wordCount);
|
||
const wordCountValid = Number.isInteger(wordCountNum)
|
||
&& wordCountNum >= WORD_COUNT_MIN
|
||
&& wordCountNum <= WORD_COUNT_MAX;
|
||
const canGenerate = !!analysisId && !!selectedTopic && wordCountValid;
|
||
|
||
const handleApplyAndClose = () => {
|
||
if (!generatedScript.trim()) return;
|
||
onApply?.(generatedScript);
|
||
onClose();
|
||
};
|
||
|
||
return (
|
||
<AppModal
|
||
isOpen={isOpen}
|
||
onClose={onClose}
|
||
panelClassName="w-full max-w-2xl max-h-[90vh] rounded-2xl border border-white/10 bg-[#171821]/95 shadow-[0_24px_80px_rgba(0,0,0,0.55)] overflow-hidden flex flex-col"
|
||
closeOnOverlay={false}
|
||
closeOnEsc={false}
|
||
>
|
||
<AppModalHeader
|
||
title="文案深度学习"
|
||
icon={<BookOpen className="h-5 w-5 text-cyan-300" />}
|
||
subtitle="分析博主近期选题风格并快速生成文案"
|
||
onClose={onClose}
|
||
/>
|
||
|
||
<div className="flex-1 overflow-y-auto p-6">
|
||
{step === "input" && (
|
||
<div className="space-y-5">
|
||
<div className="space-y-2">
|
||
<label className="text-sm text-gray-300">博主主页链接</label>
|
||
<input
|
||
type="text"
|
||
value={inputUrl}
|
||
onChange={(event) => setInputUrl(event.target.value)}
|
||
placeholder="请粘贴抖音或B站博主主页链接..."
|
||
className="w-full bg-black/20 border border-white/10 rounded-xl px-4 py-3 text-white placeholder-gray-500 focus:outline-none focus:border-cyan-500 transition-colors"
|
||
/>
|
||
<p className="text-xs text-gray-500">仅支持 https 链接,建议使用主页地址(非单条视频链接)</p>
|
||
</div>
|
||
|
||
{error && (
|
||
<div className="bg-red-500/10 border border-red-500/30 rounded-xl p-4">
|
||
<p className="text-red-400 text-sm">{error}</p>
|
||
</div>
|
||
)}
|
||
|
||
<div className="flex gap-3 pt-1">
|
||
<button
|
||
type="button"
|
||
onClick={() => setInputUrl("")}
|
||
className="flex-1 py-3 px-4 bg-white/10 hover:bg-white/20 text-white rounded-xl transition-colors"
|
||
>
|
||
清空
|
||
</button>
|
||
<button
|
||
type="button"
|
||
onClick={() => void handleAnalyze()}
|
||
disabled={!inputUrl.trim()}
|
||
className="flex-1 py-3 px-4 bg-gradient-to-r from-blue-600 to-cyan-600 hover:from-blue-500 hover:to-cyan-500 disabled:opacity-50 disabled:cursor-not-allowed text-white rounded-xl transition-all font-medium shadow-lg"
|
||
>
|
||
开始分析
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{(step === "analyzing" || step === "generating") && (
|
||
<div className="flex flex-col items-center justify-center py-20">
|
||
<div className="relative w-20 h-20 mb-6">
|
||
<div className="absolute inset-0 border-4 border-cyan-500/30 rounded-full" />
|
||
<div className="absolute inset-0 border-4 border-t-cyan-500 rounded-full animate-spin" />
|
||
</div>
|
||
<h4 className="text-xl font-medium text-white mb-2">
|
||
{step === "analyzing" ? "正在分析中..." : "正在生成中..."}
|
||
</h4>
|
||
</div>
|
||
)}
|
||
|
||
{step === "topics" && (
|
||
<div className="space-y-5">
|
||
<div className="bg-cyan-500/10 border border-cyan-500/30 rounded-xl p-3">
|
||
<p className="text-cyan-200 text-sm">已完成深度学习,请选择热门话题。</p>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<p className="text-sm text-gray-300">请选择一个话题</p>
|
||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
||
{topics.map((topic) => {
|
||
const active = selectedTopic === topic;
|
||
return (
|
||
<button
|
||
key={topic}
|
||
type="button"
|
||
onClick={() => setSelectedTopic(topic)}
|
||
className={`text-left rounded-lg border px-3 py-2.5 text-sm transition-colors ${
|
||
active
|
||
? "border-cyan-400 bg-cyan-500/20 text-cyan-100"
|
||
: "border-white/10 bg-white/5 text-gray-200 hover:border-white/20 hover:bg-white/10"
|
||
}`}
|
||
>
|
||
{topic}
|
||
</button>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<label className="text-sm text-gray-300">目标字数</label>
|
||
<input
|
||
type="number"
|
||
min={WORD_COUNT_MIN}
|
||
max={WORD_COUNT_MAX}
|
||
value={wordCount}
|
||
onChange={(event) => setWordCount(event.target.value)}
|
||
placeholder="请输入目标字数(80-1000),如 300"
|
||
className="w-full bg-black/20 border border-white/10 rounded-xl px-4 py-3 text-white placeholder-gray-500 focus:outline-none focus:border-cyan-500 transition-colors"
|
||
/>
|
||
</div>
|
||
|
||
{error && (
|
||
<div className="bg-red-500/10 border border-red-500/30 rounded-xl p-4">
|
||
<p className="text-red-400 text-sm">{error}</p>
|
||
</div>
|
||
)}
|
||
|
||
<div className="flex gap-3 pt-1">
|
||
<button
|
||
type="button"
|
||
onClick={backToInput}
|
||
className="flex-1 py-3 px-4 bg-white/10 hover:bg-white/20 text-white rounded-xl transition-colors"
|
||
>
|
||
返回
|
||
</button>
|
||
<button
|
||
type="button"
|
||
onClick={() => void handleGenerate()}
|
||
disabled={!canGenerate}
|
||
className="flex-1 py-3 px-4 bg-gradient-to-r from-blue-600 to-cyan-600 hover:from-blue-500 hover:to-cyan-500 disabled:opacity-50 disabled:cursor-not-allowed text-white rounded-xl transition-all font-medium shadow-lg"
|
||
>
|
||
生成文案
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{step === "result" && (
|
||
<div className="space-y-5">
|
||
<div className="flex justify-between items-center">
|
||
<h4 className="font-semibold text-cyan-200 flex items-center gap-2">
|
||
<Sparkles className="h-4 w-4" />
|
||
生成结果
|
||
</h4>
|
||
<span className="text-xs text-gray-400">{generatedScript.length} 字</span>
|
||
</div>
|
||
|
||
<div className="bg-white/5 border border-white/10 rounded-xl p-4 max-h-72 overflow-y-auto hide-scrollbar">
|
||
<p className="text-gray-200 text-sm leading-relaxed whitespace-pre-wrap">{generatedScript}</p>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2">
|
||
<button
|
||
type="button"
|
||
onClick={handleApplyAndClose}
|
||
className="py-2.5 px-3 bg-gradient-to-r from-blue-600 to-cyan-600 hover:from-blue-500 hover:to-cyan-500 text-white rounded-lg transition-colors text-sm"
|
||
>
|
||
填入文案
|
||
</button>
|
||
<button
|
||
type="button"
|
||
onClick={() => copyToClipboard(generatedScript)}
|
||
className="py-2.5 px-3 bg-white/10 hover:bg-white/20 text-white rounded-lg transition-colors text-sm"
|
||
>
|
||
复制
|
||
</button>
|
||
<button
|
||
type="button"
|
||
onClick={() => void handleRegenerate()}
|
||
className="py-2.5 px-3 bg-white/10 hover:bg-white/20 text-white rounded-lg transition-colors text-sm"
|
||
>
|
||
重新生成
|
||
</button>
|
||
<button
|
||
type="button"
|
||
onClick={backToTopics}
|
||
className="py-2.5 px-3 bg-white/10 hover:bg-white/20 text-white rounded-lg transition-colors text-sm"
|
||
>
|
||
换个话题
|
||
</button>
|
||
</div>
|
||
|
||
{error && (
|
||
<div className="bg-red-500/10 border border-red-500/30 rounded-xl p-4">
|
||
<p className="text-red-400 text-sm">{error}</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</AppModal>
|
||
);
|
||
}
|