forked from 77media/video-flow
170 lines
6.1 KiB
TypeScript
170 lines
6.1 KiB
TypeScript
import { useState, useRef } from "react";
|
||
import { motion } from "framer-motion";
|
||
import { Button } from "@/components/ui/button";
|
||
import { Sparkles, X, Plus, RefreshCw } from 'lucide-react';
|
||
import { cn } from "@/public/lib/utils";
|
||
import ContentEditable from 'react-contenteditable';
|
||
|
||
interface CharacterAttribute {
|
||
key: string;
|
||
label: string;
|
||
value: string;
|
||
type: 'text' | 'number' | 'select';
|
||
options?: string[];
|
||
}
|
||
|
||
interface CharacterEditorProps {
|
||
initialDescription?: string;
|
||
onDescriptionChange?: (description: string) => void;
|
||
onAttributesChange?: (attributes: CharacterAttribute[]) => void;
|
||
onReplaceCharacter?: (url: string) => void;
|
||
}
|
||
|
||
const mockParse = (text: string): CharacterAttribute[] => {
|
||
// 模拟结构化解析结果
|
||
return [
|
||
{ key: "age", label: "年龄", value: "20", type: "number" },
|
||
{ key: "gender", label: "性别", value: "女性", type: "select", options: ["男性", "女性", "其他"] },
|
||
{ key: "hair", label: "发型", value: "银白短发", type: "text" },
|
||
{ key: "race", label: "种族", value: "精灵", type: "text" },
|
||
{ key: "skin", label: "肤色", value: "白皙", type: "text" },
|
||
{ key: "build", label: "体型", value: "高挑", type: "text" },
|
||
{ key: "costume", label: "服装", value: "白色连衣裙", type: "text" },
|
||
];
|
||
};
|
||
|
||
|
||
export default function CharacterEditor({
|
||
initialDescription = "一个银白短发的精灵女性,大约20岁,肤色白皙,身材高挑,身着白色连衣裙",
|
||
onDescriptionChange,
|
||
onAttributesChange,
|
||
onReplaceCharacter,
|
||
}: CharacterEditorProps) {
|
||
const [inputText, setInputText] = useState(initialDescription);
|
||
const [isOptimizing, setIsOptimizing] = useState(false);
|
||
const [isRegenerating, setIsRegenerating] = useState(false);
|
||
const [customTags, setCustomTags] = useState<string[]>([]);
|
||
const [newTag, setNewTag] = useState("");
|
||
const attributesRef = useRef<CharacterAttribute[]>(mockParse(initialDescription));
|
||
const contentEditableRef = useRef<HTMLElement>(null);
|
||
|
||
const handleTextChange = (e: { target: { value: string } }) => {
|
||
// 移除 HTML 标签,保留换行
|
||
const value = e.target.value;
|
||
setInputText(value);
|
||
onDescriptionChange?.(value);
|
||
};
|
||
|
||
// 格式化文本为 HTML
|
||
const formatTextToHtml = (text: string) => {
|
||
return text
|
||
.split('\n')
|
||
.map(line => line || '<br>')
|
||
.join('<div>');
|
||
};
|
||
|
||
const handleSmartPolish = async () => {
|
||
setIsOptimizing(true);
|
||
try {
|
||
const polishedText = "一位拥有银白短发、白皙肌肤的高挑精灵少女,年龄约二十岁,气质神秘优雅。举手投足间散发着独特的精灵族气质,眼神中透露出智慧与沧桑。";
|
||
setInputText(polishedText);
|
||
attributesRef.current = mockParse(polishedText);
|
||
onDescriptionChange?.(polishedText);
|
||
onAttributesChange?.(attributesRef.current);
|
||
} finally {
|
||
setIsOptimizing(false);
|
||
}
|
||
};
|
||
|
||
const handleAttributeChange = (attr: CharacterAttribute, newValue: string) => {
|
||
// 移除 HTML 标签
|
||
newValue = newValue.replace(/<[^>]*>/g, '');
|
||
|
||
// 更新描述文本
|
||
let newText = inputText;
|
||
if (attr.type === "number" && attr.key === "age") {
|
||
newText = newText.replace(/\d+岁/, `${newValue}岁`);
|
||
} else {
|
||
newText = newText.replace(new RegExp(attr.value, 'g'), newValue);
|
||
}
|
||
|
||
// 更新属性值
|
||
const newAttr = { ...attr, value: newValue };
|
||
attributesRef.current = attributesRef.current.map(a =>
|
||
a.key === attr.key ? newAttr : a
|
||
);
|
||
|
||
setInputText(newText);
|
||
onDescriptionChange?.(newText);
|
||
onAttributesChange?.(attributesRef.current);
|
||
};
|
||
|
||
const handleRegenerate = () => {
|
||
setIsRegenerating(true);
|
||
setTimeout(() => {
|
||
onReplaceCharacter?.("https://c.huiying.video/images/0411ac7b-ab7e-4a17-ab4f-6880a28f8915.jpg");
|
||
setIsRegenerating(false);
|
||
}, 3000);
|
||
};
|
||
|
||
return (
|
||
<div className="space-y-2 border border-white/10 relative p-2 rounded-[0.5rem]">
|
||
{/* 自由输入区域 */}
|
||
<div className="relative">
|
||
<ContentEditable
|
||
innerRef={contentEditableRef}
|
||
html={formatTextToHtml(inputText)}
|
||
onChange={handleTextChange}
|
||
className="block w-full min-h-[120px] bg-white/5 backdrop-blur-md p-4 text-white/90
|
||
rounded-lg border-unset outline-none pb-12
|
||
whitespace-pre-wrap break-words"
|
||
placeholder="用自然语言描述角色,比如:一个身穿红袍的精灵女性..."
|
||
/>
|
||
|
||
{/* 智能润色按钮 */}
|
||
<motion.button
|
||
onClick={handleSmartPolish}
|
||
disabled={isOptimizing}
|
||
className="absolute bottom-3 right-3 flex items-center gap-1.5 px-3 py-1.5
|
||
bg-purple-500/10 hover:bg-purple-500/20 text-purple-500 rounded-full
|
||
transition-colors text-xs disabled:opacity-50"
|
||
whileHover={{ scale: 1.05 }}
|
||
whileTap={{ scale: 0.95 }}
|
||
>
|
||
<Sparkles className="w-3.5 h-3.5" />
|
||
<span>{isOptimizing ? "优化中..." : "智能优化"}</span>
|
||
</motion.button>
|
||
</div>
|
||
|
||
{/* 结构化属性标签 */}
|
||
<div className="flex flex-wrap gap-2">
|
||
{attributesRef.current.map((attr) => (
|
||
<motion.div
|
||
key={attr.key}
|
||
className="flex items-center gap-1.5 px-3 py-1.5 rounded-full
|
||
bg-white/5 hover:bg-white/10 transition-colors
|
||
border border-white/20 group"
|
||
whileHover={{ scale: 1.05 }}
|
||
whileTap={{ scale: 0.95 }}
|
||
>
|
||
<span className="text-sm text-white/90">{attr.label}:</span>
|
||
<ContentEditable
|
||
html={attr.value}
|
||
onChange={(e) => handleAttributeChange(attr, e.target.value)}
|
||
className="text-sm text-white/90 min-w-[1em] focus:outline-none
|
||
border-b border-transparent focus:border-white/30
|
||
hover:border-white/20"
|
||
onKeyDown={(e) => {
|
||
if (e.key === 'Enter') {
|
||
e.preventDefault();
|
||
(e.target as HTMLElement).blur();
|
||
}
|
||
}}
|
||
/>
|
||
</motion.div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|