forked from 77media/video-flow
264 lines
8.7 KiB
TypeScript
264 lines
8.7 KiB
TypeScript
import { useState, useRef } from "react";
|
||
import { motion } from "framer-motion";
|
||
import { Button } from "@/components/ui/button";
|
||
import { Sparkles, X, Plus, Clock, MapPin, Sun, Moon, Cloud, CloudRain, CloudSnow, CloudLightning, Palette, RefreshCw } from 'lucide-react';
|
||
import { cn } from "@/public/lib/utils";
|
||
import ContentEditable from 'react-contenteditable';
|
||
|
||
interface SceneAttribute {
|
||
key: string;
|
||
label: string;
|
||
value: string;
|
||
type: 'text' | 'number' | 'select';
|
||
options?: string[];
|
||
icon?: any;
|
||
}
|
||
|
||
interface SceneEnvironment {
|
||
time: {
|
||
period: '清晨' | '上午' | '中午' | '下午' | '傍晚' | '夜晚' | '深夜';
|
||
specific?: string;
|
||
};
|
||
location: {
|
||
main: string;
|
||
detail?: string;
|
||
};
|
||
weather: {
|
||
type: '晴天' | '多云' | '雨天' | '雪天' | '雷暴' | '阴天';
|
||
description?: string;
|
||
};
|
||
atmosphere: {
|
||
lighting: string;
|
||
mood: string;
|
||
};
|
||
}
|
||
|
||
interface SceneEditorProps {
|
||
initialDescription?: string;
|
||
onDescriptionChange?: (description: string) => void;
|
||
onAttributesChange?: (attributes: SceneAttribute[]) => void;
|
||
onEnvironmentChange?: (environment: SceneEnvironment) => void;
|
||
onReplaceScene?: (url: string) => void;
|
||
className?: string;
|
||
}
|
||
|
||
// 天气图标映射
|
||
const weatherIcons = {
|
||
'晴天': Sun,
|
||
'多云': Cloud,
|
||
'雨天': CloudRain,
|
||
'雪天': CloudSnow,
|
||
'雷暴': CloudLightning,
|
||
'阴天': Cloud
|
||
};
|
||
|
||
const mockParse = (text: string): { attributes: SceneAttribute[], environment: SceneEnvironment } => {
|
||
// 模拟结构化解析结果
|
||
const attributes: SceneAttribute[] = [];
|
||
|
||
const environment: SceneEnvironment = {
|
||
time: {
|
||
period: "下午",
|
||
specific: "午后2点左右"
|
||
},
|
||
location: {
|
||
main: "教室",
|
||
detail: "高中教室,靠窗的位置"
|
||
},
|
||
weather: {
|
||
type: "晴天",
|
||
description: "阳光明媚,微风轻拂"
|
||
},
|
||
atmosphere: {
|
||
lighting: "自然光线充足,温暖的阳光从窗户斜射入室内",
|
||
mood: "安静祥和,充满生机与活力"
|
||
}
|
||
};
|
||
|
||
// 添加环境属性到结构化标签
|
||
const environmentAttributes: SceneAttribute[] = [
|
||
{
|
||
key: "time",
|
||
label: "时间",
|
||
value: `${environment.time.period}${environment.time.specific ? `,${environment.time.specific}` : ''}`,
|
||
type: "text",
|
||
icon: Clock
|
||
},
|
||
{
|
||
key: "location",
|
||
label: "地点",
|
||
value: `${environment.location.main}${environment.location.detail ? `,${environment.location.detail}` : ''}`,
|
||
type: "text",
|
||
icon: MapPin
|
||
},
|
||
{
|
||
key: "weather",
|
||
label: "天气",
|
||
value: `${environment.weather.type}${environment.weather.description ? `,${environment.weather.description}` : ''}`,
|
||
type: "text",
|
||
icon: weatherIcons[environment.weather.type as keyof typeof weatherIcons]
|
||
},
|
||
{
|
||
key: "atmosphere",
|
||
label: "氛围",
|
||
value: `${environment.atmosphere.lighting},${environment.atmosphere.mood}`,
|
||
type: "text",
|
||
icon: Palette
|
||
}
|
||
];
|
||
|
||
return {
|
||
attributes: [...attributes, ...environmentAttributes],
|
||
environment
|
||
};
|
||
};
|
||
|
||
export default function SceneEditor({
|
||
initialDescription = "一个银白短发的精灵女性,大约20岁,肤色白皙,身材高挑,身着白色连衣裙。在教室里,阳光透过窗户洒在地板上,营造出温暖而安静的氛围。",
|
||
onDescriptionChange,
|
||
onAttributesChange,
|
||
onEnvironmentChange,
|
||
onReplaceScene,
|
||
className
|
||
}: SceneEditorProps) {
|
||
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 parseResult = useRef(mockParse(initialDescription));
|
||
const contentEditableRef = useRef<HTMLElement>(null);
|
||
|
||
const handleTextChange = (e: { target: { value: string } }) => {
|
||
// 移除 HTML 标签,保留换行
|
||
const value = e.target.value;
|
||
setInputText(value);
|
||
onDescriptionChange?.(value);
|
||
|
||
// 重新解析文本
|
||
const newParseResult = mockParse(value);
|
||
parseResult.current = newParseResult;
|
||
onAttributesChange?.(newParseResult.attributes);
|
||
onEnvironmentChange?.(newParseResult.environment);
|
||
};
|
||
|
||
// 格式化文本为 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);
|
||
const newParseResult = mockParse(polishedText);
|
||
parseResult.current = newParseResult;
|
||
onDescriptionChange?.(polishedText);
|
||
onAttributesChange?.(newParseResult.attributes);
|
||
onEnvironmentChange?.(newParseResult.environment);
|
||
} finally {
|
||
setIsOptimizing(false);
|
||
}
|
||
};
|
||
|
||
const handleAttributeChange = (attr: SceneAttribute, 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 };
|
||
const newAttributes = parseResult.current.attributes.map(a =>
|
||
a.key === attr.key ? newAttr : a
|
||
);
|
||
|
||
// 重新解析文本以更新环境信息
|
||
const newParseResult = mockParse(newText);
|
||
parseResult.current = newParseResult;
|
||
|
||
setInputText(newText);
|
||
onDescriptionChange?.(newText);
|
||
onAttributesChange?.(newAttributes);
|
||
onEnvironmentChange?.(newParseResult.environment);
|
||
};
|
||
|
||
const handleRegenerate = () => {
|
||
setIsRegenerating(true);
|
||
setTimeout(() => {
|
||
onReplaceScene?.("https://c.huiying.video/images/0411ac7b-ab7e-4a17-ab4f-6880a28f8915.jpg");
|
||
setIsRegenerating(false);
|
||
}, 3000);
|
||
};
|
||
|
||
return (
|
||
<div className={cn("space-y-2 border border-white/10 relative p-2 rounded-[0.5rem]", className)}>
|
||
{/* 自由输入区域 */}
|
||
<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">
|
||
{parseResult.current.attributes.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 }}
|
||
>
|
||
{attr.icon && <attr.icon className="w-3.5 h-3.5 text-white/70" />}
|
||
<span className="text-sm text-white/90 flex-shrink-0">{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>
|
||
);
|
||
}
|