230 lines
7.3 KiB
TypeScript
230 lines
7.3 KiB
TypeScript
import { useState, useEffect, useCallback, useRef } from "react";
|
|
import api from "@/shared/api/axios";
|
|
import { ApiResponse, unwrap } from "@/shared/api/types";
|
|
import { toast } from "sonner";
|
|
|
|
export type ExtractionStep = "config" | "processing" | "result";
|
|
export type InputTab = "file" | "url";
|
|
|
|
const VALID_FILE_TYPES = [".mp4", ".mov", ".avi", ".mp3", ".wav", ".m4a"];
|
|
const CUSTOM_PROMPT_KEY = "vigent_rewriteCustomPrompt";
|
|
|
|
interface UseScriptExtractionOptions {
|
|
isOpen: boolean;
|
|
}
|
|
|
|
export const useScriptExtraction = ({ isOpen }: UseScriptExtractionOptions) => {
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const [script, setScript] = useState("");
|
|
const [rewrittenScript, setRewrittenScript] = useState("");
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [doRewrite, setDoRewrite] = useState(true);
|
|
const [step, setStep] = useState<ExtractionStep>("config");
|
|
const [dragActive, setDragActive] = useState(false);
|
|
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
|
const [activeTab, setActiveTab] = useState<InputTab>("url");
|
|
const [inputUrl, setInputUrl] = useState("");
|
|
const [customPrompt, setCustomPrompt] = useState(() => typeof window !== "undefined" ? localStorage.getItem(CUSTOM_PROMPT_KEY) || "" : "");
|
|
const [showCustomPrompt, setShowCustomPrompt] = useState(false);
|
|
|
|
// Debounced save customPrompt to localStorage
|
|
const debounceRef = useRef<ReturnType<typeof setTimeout>>(undefined);
|
|
useEffect(() => {
|
|
debounceRef.current = setTimeout(() => {
|
|
localStorage.setItem(CUSTOM_PROMPT_KEY, customPrompt);
|
|
}, 300);
|
|
return () => clearTimeout(debounceRef.current);
|
|
}, [customPrompt]);
|
|
|
|
// Reset state when modal opens (customPrompt is persistent, not reset)
|
|
useEffect(() => {
|
|
if (isOpen) {
|
|
setStep("config");
|
|
setScript("");
|
|
setRewrittenScript("");
|
|
setError(null);
|
|
setIsLoading(false);
|
|
setSelectedFile(null);
|
|
setInputUrl("");
|
|
setActiveTab("url");
|
|
}
|
|
}, [isOpen]);
|
|
|
|
const handleDrag = useCallback((e: React.DragEvent) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
if (e.type === "dragenter" || e.type === "dragover") {
|
|
setDragActive(true);
|
|
} else if (e.type === "dragleave") {
|
|
setDragActive(false);
|
|
}
|
|
}, []);
|
|
|
|
const handleFile = useCallback((file: File) => {
|
|
const ext = file.name.toLowerCase().slice(file.name.lastIndexOf("."));
|
|
if (!VALID_FILE_TYPES.includes(ext)) {
|
|
setError(`不支持的文件格式 ${ext},请上传视频或音频文件`);
|
|
return;
|
|
}
|
|
setSelectedFile(file);
|
|
setError(null);
|
|
}, []);
|
|
|
|
const handleDrop = useCallback(
|
|
(e: React.DragEvent) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
setDragActive(false);
|
|
if (e.dataTransfer.files?.[0]) {
|
|
handleFile(e.dataTransfer.files[0]);
|
|
}
|
|
},
|
|
[handleFile]
|
|
);
|
|
|
|
const handleFileChange = useCallback(
|
|
(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
if (e.target.files?.[0]) {
|
|
handleFile(e.target.files[0]);
|
|
}
|
|
},
|
|
[handleFile]
|
|
);
|
|
|
|
const handleExtract = useCallback(async () => {
|
|
if (activeTab === "file" && !selectedFile) {
|
|
setError("请先上传文件");
|
|
return;
|
|
}
|
|
if (activeTab === "url" && !inputUrl.trim()) {
|
|
setError("请先输入视频链接");
|
|
return;
|
|
}
|
|
|
|
setIsLoading(true);
|
|
setStep("processing");
|
|
setError(null);
|
|
|
|
try {
|
|
const formData = new FormData();
|
|
if (activeTab === "file" && selectedFile) {
|
|
formData.append("file", selectedFile);
|
|
} else if (activeTab === "url") {
|
|
formData.append("url", inputUrl.trim());
|
|
}
|
|
formData.append("rewrite", doRewrite ? "true" : "false");
|
|
if (doRewrite && customPrompt.trim()) {
|
|
formData.append("custom_prompt", customPrompt.trim());
|
|
}
|
|
|
|
const { data: res } = await api.post<
|
|
ApiResponse<{ original_script: string; rewritten_script?: string }>
|
|
>("/api/tools/extract-script", formData, {
|
|
headers: { "Content-Type": "multipart/form-data" },
|
|
timeout: 180000, // 3 minutes timeout
|
|
});
|
|
|
|
const payload = unwrap(res);
|
|
setScript(payload.original_script);
|
|
setRewrittenScript(payload.rewritten_script || "");
|
|
setStep("result");
|
|
} catch (err: unknown) {
|
|
console.error(err);
|
|
const axiosErr = err as {
|
|
response?: { data?: { message?: string } };
|
|
message?: string;
|
|
};
|
|
const msg =
|
|
axiosErr.response?.data?.message || axiosErr.message || "请求失败";
|
|
setError(msg);
|
|
setStep("config");
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
}, [activeTab, selectedFile, inputUrl, doRewrite, customPrompt]);
|
|
|
|
const copyToClipboard = useCallback((text: string) => {
|
|
if (navigator.clipboard && window.isSecureContext) {
|
|
navigator.clipboard
|
|
.writeText(text)
|
|
.then(() => {
|
|
toast.success("已复制到剪贴板");
|
|
})
|
|
.catch(() => {
|
|
fallbackCopyTextToClipboard(text);
|
|
});
|
|
} else {
|
|
fallbackCopyTextToClipboard(text);
|
|
}
|
|
}, []);
|
|
|
|
const fallbackCopyTextToClipboard = (text: string) => {
|
|
const textArea = document.createElement("textarea");
|
|
textArea.value = text;
|
|
textArea.style.top = "0";
|
|
textArea.style.left = "0";
|
|
textArea.style.position = "fixed";
|
|
textArea.style.opacity = "0";
|
|
|
|
document.body.appendChild(textArea);
|
|
textArea.focus();
|
|
textArea.select();
|
|
|
|
try {
|
|
const successful = document.execCommand("copy");
|
|
if (successful) {
|
|
toast.success("已复制到剪贴板");
|
|
} else {
|
|
toast.error("复制失败,请手动复制");
|
|
}
|
|
} catch {
|
|
toast.error("复制失败,请手动复制");
|
|
}
|
|
|
|
document.body.removeChild(textArea);
|
|
};
|
|
|
|
const resetToConfig = useCallback(() => {
|
|
setStep("config");
|
|
}, []);
|
|
|
|
const clearSelectedFile = useCallback(() => {
|
|
setSelectedFile(null);
|
|
}, []);
|
|
|
|
const clearInputUrl = useCallback(() => {
|
|
setInputUrl("");
|
|
}, []);
|
|
|
|
return {
|
|
// State
|
|
isLoading,
|
|
script,
|
|
rewrittenScript,
|
|
error,
|
|
doRewrite,
|
|
step,
|
|
dragActive,
|
|
selectedFile,
|
|
activeTab,
|
|
inputUrl,
|
|
customPrompt,
|
|
showCustomPrompt,
|
|
// Setters
|
|
setDoRewrite,
|
|
setActiveTab,
|
|
setInputUrl,
|
|
setCustomPrompt,
|
|
setShowCustomPrompt,
|
|
// Handlers
|
|
handleDrag,
|
|
handleDrop,
|
|
handleFileChange,
|
|
handleExtract,
|
|
copyToClipboard,
|
|
resetToConfig,
|
|
clearSelectedFile,
|
|
clearInputUrl,
|
|
};
|
|
};
|