363 lines
20 KiB
TypeScript
363 lines
20 KiB
TypeScript
"use client";
|
||
|
||
import { useEffect, useCallback } from "react";
|
||
import { Loader2 } from "lucide-react";
|
||
import { useScriptExtraction } from "./script-extraction/useScriptExtraction";
|
||
|
||
interface ScriptExtractionModalProps {
|
||
isOpen: boolean;
|
||
onClose: () => void;
|
||
onApply?: (text: string) => void;
|
||
}
|
||
|
||
export default function ScriptExtractionModal({
|
||
isOpen,
|
||
onClose,
|
||
onApply,
|
||
}: ScriptExtractionModalProps) {
|
||
const {
|
||
isLoading,
|
||
script,
|
||
rewrittenScript,
|
||
error,
|
||
doRewrite,
|
||
step,
|
||
dragActive,
|
||
selectedFile,
|
||
activeTab,
|
||
inputUrl,
|
||
customPrompt,
|
||
showCustomPrompt,
|
||
setDoRewrite,
|
||
setActiveTab,
|
||
setInputUrl,
|
||
setCustomPrompt,
|
||
setShowCustomPrompt,
|
||
handleDrag,
|
||
handleDrop,
|
||
handleFileChange,
|
||
handleExtract,
|
||
copyToClipboard,
|
||
resetToConfig,
|
||
clearSelectedFile,
|
||
clearInputUrl,
|
||
} = useScriptExtraction({ isOpen });
|
||
|
||
// 快捷键:ESC 关闭,Enter 提交(仅在 config 步骤)
|
||
const canExtract = (activeTab === "file" && selectedFile) || (activeTab === "url" && inputUrl.trim());
|
||
|
||
const handleKeyDown = useCallback((e: KeyboardEvent) => {
|
||
if (e.key === "Escape") {
|
||
onClose();
|
||
} else if (e.key === "Enter" && !e.shiftKey && step === "config" && canExtract && !isLoading) {
|
||
e.preventDefault();
|
||
handleExtract();
|
||
}
|
||
}, [onClose, step, canExtract, isLoading, handleExtract]);
|
||
|
||
useEffect(() => {
|
||
if (!isOpen) return;
|
||
document.addEventListener("keydown", handleKeyDown);
|
||
return () => document.removeEventListener("keydown", handleKeyDown);
|
||
}, [isOpen, handleKeyDown]);
|
||
|
||
if (!isOpen) return null;
|
||
|
||
const handleApplyAndClose = (text: string) => {
|
||
onApply?.(text);
|
||
onClose();
|
||
};
|
||
|
||
const handleExtractNext = () => {
|
||
resetToConfig();
|
||
clearSelectedFile();
|
||
clearInputUrl();
|
||
};
|
||
|
||
return (
|
||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm p-4 animate-in fade-in duration-200">
|
||
<div className="bg-[#1a1a1a] border border-white/10 rounded-2xl w-full max-w-2xl max-h-[90vh] overflow-hidden flex flex-col shadow-2xl">
|
||
{/* Header */}
|
||
<div className="flex items-center justify-between p-4 border-b border-white/10 bg-white/5">
|
||
<h3 className="text-lg font-semibold text-white flex items-center gap-2">
|
||
📜 文案提取助手
|
||
</h3>
|
||
<button
|
||
onClick={onClose}
|
||
className="text-gray-400 hover:text-white transition-colors text-2xl leading-none"
|
||
>
|
||
×
|
||
</button>
|
||
</div>
|
||
|
||
{/* Content */}
|
||
<div className="flex-1 overflow-y-auto p-6">
|
||
{step === "config" && (
|
||
<div className="space-y-6">
|
||
{/* Tabs */}
|
||
<div className="flex p-1 bg-white/5 rounded-xl border border-white/10">
|
||
<button
|
||
onClick={() => setActiveTab("url")}
|
||
className={`flex-1 py-2 rounded-lg text-sm font-medium transition-all ${activeTab === "url"
|
||
? "bg-purple-600 text-white shadow-lg"
|
||
: "text-gray-400 hover:text-white hover:bg-white/5"
|
||
}`}
|
||
>
|
||
🔗 粘贴链接
|
||
</button>
|
||
<button
|
||
onClick={() => setActiveTab("file")}
|
||
className={`flex-1 py-2 rounded-lg text-sm font-medium transition-all ${activeTab === "file"
|
||
? "bg-purple-600 text-white shadow-lg"
|
||
: "text-gray-400 hover:text-white hover:bg-white/5"
|
||
}`}
|
||
>
|
||
📂 上传文件
|
||
</button>
|
||
</div>
|
||
|
||
{/* URL Input Area */}
|
||
{activeTab === "url" && (
|
||
<div className="space-y-2 py-4">
|
||
<div className="relative">
|
||
<input
|
||
type="text"
|
||
value={inputUrl}
|
||
onChange={(e) => setInputUrl(e.target.value)}
|
||
placeholder="请粘贴抖音、B站等主流平台视频链接..."
|
||
className="w-full bg-black/20 border border-white/10 rounded-xl px-4 py-4 text-white placeholder-gray-500 focus:outline-none focus:border-purple-500 transition-colors"
|
||
/>
|
||
{inputUrl && (
|
||
<button
|
||
onClick={clearInputUrl}
|
||
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-500 hover:text-white"
|
||
>
|
||
✕
|
||
</button>
|
||
)}
|
||
</div>
|
||
<p className="text-xs text-gray-500 pl-1">
|
||
支持抖音、B站、微博、小红书等主流平台视频链接
|
||
</p>
|
||
</div>
|
||
)}
|
||
|
||
{/* File Upload Area */}
|
||
{activeTab === "file" && (
|
||
<div
|
||
className={`border-2 border-dashed rounded-xl p-8 text-center transition-colors ${dragActive
|
||
? "border-purple-500 bg-purple-500/10"
|
||
: "border-white/10 hover:border-white/20"
|
||
}`}
|
||
onDragEnter={handleDrag}
|
||
onDragLeave={handleDrag}
|
||
onDragOver={handleDrag}
|
||
onDrop={handleDrop}
|
||
>
|
||
{selectedFile ? (
|
||
<div className="space-y-2">
|
||
<p className="text-white">{selectedFile.name}</p>
|
||
<p className="text-sm text-gray-400">
|
||
{(selectedFile.size / 1024 / 1024).toFixed(2)} MB
|
||
</p>
|
||
<button
|
||
onClick={clearSelectedFile}
|
||
className="text-xs text-purple-400 hover:text-purple-300"
|
||
>
|
||
重新选择
|
||
</button>
|
||
</div>
|
||
) : (
|
||
<div className="space-y-4">
|
||
<div className="text-4xl">📁</div>
|
||
<p className="text-gray-400">
|
||
拖放视频/音频文件到此处,或
|
||
<label className="text-purple-400 hover:text-purple-300 cursor-pointer">
|
||
点击选择
|
||
<input
|
||
type="file"
|
||
accept=".mp4,.mov,.avi,.mp3,.wav,.m4a"
|
||
onChange={handleFileChange}
|
||
className="hidden"
|
||
/>
|
||
</label>
|
||
</p>
|
||
<p className="text-xs text-gray-500">
|
||
支持 MP4, MOV, AVI, MP3, WAV, M4A
|
||
</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{/* Options */}
|
||
<div className="bg-white/5 rounded-xl border border-white/10 overflow-hidden">
|
||
<div className="flex items-center justify-between p-4">
|
||
<label className="flex items-center gap-2 cursor-pointer">
|
||
<input
|
||
type="checkbox"
|
||
checked={doRewrite}
|
||
onChange={(e) => setDoRewrite(e.target.checked)}
|
||
className="w-4 h-4 rounded bg-white/10 border-white/20 text-purple-500 focus:ring-purple-500"
|
||
/>
|
||
<span className="text-sm text-gray-300">
|
||
AI 智能改写
|
||
</span>
|
||
</label>
|
||
{doRewrite && (
|
||
<button
|
||
type="button"
|
||
onClick={() => setShowCustomPrompt(!showCustomPrompt)}
|
||
className="text-xs text-purple-400 hover:text-purple-300 transition-colors flex items-center gap-1"
|
||
>
|
||
自定义提示词 {showCustomPrompt ? "▲" : "▼"}
|
||
</button>
|
||
)}
|
||
</div>
|
||
{doRewrite && showCustomPrompt && (
|
||
<div className="px-4 pb-4 space-y-2">
|
||
<textarea
|
||
value={customPrompt}
|
||
onChange={(e) => setCustomPrompt(e.target.value)}
|
||
placeholder="输入自定义改写提示词..."
|
||
rows={3}
|
||
className="w-full bg-black/20 border border-white/10 rounded-lg px-3 py-2 text-sm text-white placeholder-gray-500 focus:outline-none focus:border-purple-500 transition-colors resize-none"
|
||
/>
|
||
<p className="text-xs text-gray-500">
|
||
留空则使用默认提示词
|
||
</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Error */}
|
||
{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>
|
||
)}
|
||
|
||
{/* Action Button */}
|
||
<div className="flex gap-3 pt-2">
|
||
<button
|
||
onClick={onClose}
|
||
className="flex-1 py-3 px-4 bg-white/10 hover:bg-white/20 text-white rounded-xl transition-colors"
|
||
>
|
||
取消
|
||
</button>
|
||
<button
|
||
onClick={handleExtract}
|
||
disabled={
|
||
(activeTab === "file" && !selectedFile) ||
|
||
(activeTab === "url" && !inputUrl.trim()) ||
|
||
isLoading
|
||
}
|
||
className="flex-1 py-3 px-4 bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-500 hover:to-pink-500 disabled:opacity-50 disabled:cursor-not-allowed text-white rounded-xl transition-all font-medium shadow-lg flex items-center justify-center gap-2"
|
||
>
|
||
{isLoading ? <Loader2 className="w-5 h-5 animate-spin" /> : null}
|
||
开始提取
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{step === "processing" && (
|
||
<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-purple-500/30 rounded-full"></div>
|
||
<div className="absolute inset-0 border-4 border-t-purple-500 rounded-full animate-spin"></div>
|
||
</div>
|
||
<h4 className="text-xl font-medium text-white mb-2">
|
||
正在处理中...
|
||
</h4>
|
||
<p className="text-sm text-gray-400 text-center max-w-sm px-4">
|
||
{activeTab === "url" && "正在下载视频..."}
|
||
<br />
|
||
{doRewrite
|
||
? "正在进行语音识别和 AI 智能改写..."
|
||
: "正在进行语音识别..."}
|
||
<br />
|
||
<span className="opacity-75">
|
||
大文件可能需要几分钟,请不要关闭窗口
|
||
</span>
|
||
</p>
|
||
</div>
|
||
)}
|
||
|
||
{step === "result" && (
|
||
<div className="space-y-6">
|
||
{rewrittenScript && (
|
||
<div className="space-y-2">
|
||
<div className="flex justify-between items-center">
|
||
<h4 className="font-semibold text-purple-300 flex items-center gap-2">
|
||
✨ AI 改写结果{" "}
|
||
<span className="text-xs font-normal text-purple-400/70">
|
||
(推荐)
|
||
</span>
|
||
</h4>
|
||
{onApply && (
|
||
<button
|
||
onClick={() => handleApplyAndClose(rewrittenScript)}
|
||
className="text-xs bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-500 hover:to-pink-500 text-white px-3 py-1.5 rounded-lg transition-colors flex items-center gap-1 shadow-sm"
|
||
>
|
||
📥 填入
|
||
</button>
|
||
)}
|
||
<button
|
||
onClick={() => copyToClipboard(rewrittenScript)}
|
||
className="text-xs bg-purple-600 hover:bg-purple-500 text-white px-3 py-1.5 rounded-lg transition-colors flex items-center gap-1"
|
||
>
|
||
📋 复制内容
|
||
</button>
|
||
</div>
|
||
<div className="bg-purple-900/10 border border-purple-500/20 rounded-xl p-4 max-h-60 overflow-y-auto hide-scrollbar">
|
||
<p className="text-gray-200 text-sm leading-relaxed whitespace-pre-wrap">
|
||
{rewrittenScript}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
<div className="space-y-2">
|
||
<div className="flex justify-between items-center">
|
||
<h4 className="font-semibold text-gray-400 flex items-center gap-2">
|
||
🎙️ 原始识别结果
|
||
</h4>
|
||
{onApply && (
|
||
<button
|
||
onClick={() => handleApplyAndClose(script)}
|
||
className="text-xs bg-white/10 hover:bg-white/20 text-white px-3 py-1.5 rounded-lg transition-colors flex items-center gap-1"
|
||
>
|
||
📥 填入
|
||
</button>
|
||
)}
|
||
<button
|
||
onClick={() => copyToClipboard(script)}
|
||
className="text-xs bg-white/10 hover:bg-white/20 text-white px-3 py-1.5 rounded-lg transition-colors"
|
||
>
|
||
复制
|
||
</button>
|
||
</div>
|
||
<div className="bg-white/5 border border-white/10 rounded-xl p-4 max-h-40 overflow-y-auto hide-scrollbar">
|
||
<p className="text-gray-400 text-sm leading-relaxed whitespace-pre-wrap">
|
||
{script}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex justify-center pt-4">
|
||
<button
|
||
onClick={handleExtractNext}
|
||
className="px-6 py-2 bg-white/10 hover:bg-white/20 text-white rounded-lg transition-colors"
|
||
>
|
||
提取下一个
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|