video-flow-b/components/ui/scene-editor.tsx
2025-07-30 16:15:25 +08:00

264 lines
8.7 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>
);
}