forked from 77media/video-flow
314 lines
11 KiB
TypeScript
314 lines
11 KiB
TypeScript
import React, { useMemo } from 'react';
|
|
import { Heart } from 'lucide-react';
|
|
import { motion } from 'framer-motion';
|
|
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
|
|
import { TypewriterText } from './common/TypewriterText';
|
|
import { ContentCard } from './common/ContentCard';
|
|
import { SkeletonCard } from './common/SkeletonCard';
|
|
import { IconLoading } from './common/IconLoading';
|
|
|
|
interface ScriptContent {
|
|
acts?: Array<{
|
|
id: string;
|
|
stableId: string;
|
|
title: string;
|
|
desc: string;
|
|
beats: string[];
|
|
}>;
|
|
characters?: Array<{
|
|
id: string;
|
|
stableId: string;
|
|
name: string;
|
|
role: string;
|
|
arc: string;
|
|
desc: string;
|
|
color: string;
|
|
}>;
|
|
dialogue?: {
|
|
stableId: string;
|
|
rhythm: string;
|
|
style: string;
|
|
};
|
|
themes?: Array<{
|
|
id: string;
|
|
stableId: string;
|
|
theme: string;
|
|
desc: string;
|
|
depth: string;
|
|
}>;
|
|
dramaticLine?: {
|
|
stableId: string;
|
|
points: Array<{
|
|
id: string;
|
|
stableId: string;
|
|
title: string;
|
|
desc: string;
|
|
intensity: number; // 0-100 情感强度
|
|
}>;
|
|
};
|
|
}
|
|
|
|
interface ScriptwriterProps {
|
|
currentContent: ScriptContent;
|
|
isPlaying: boolean;
|
|
}
|
|
|
|
const CustomTooltip = ({ active, payload, label }: any) => {
|
|
if (active && payload && payload.length) {
|
|
return (
|
|
<div className="bg-black/80 backdrop-blur-sm p-2 rounded-lg border border-purple-500/30">
|
|
<p className="text-purple-300 text-xs font-medium">{payload[0].payload.title}</p>
|
|
<p className="text-white text-xs">{`Emotional intensity: ${payload[0].value}`}</p>
|
|
</div>
|
|
);
|
|
}
|
|
return null;
|
|
};
|
|
|
|
// Three-act Structure Component
|
|
const ThreeActStructure = React.memo(({ acts, isPlaying }: { acts?: ScriptContent['acts']; isPlaying: boolean }) => {
|
|
return (
|
|
<div className="bg-black/30 rounded-lg p-4">
|
|
<h3 className="text-white font-semibold mb-3 flex items-center space-x-2">
|
|
<span>Three-act structure</span>
|
|
<IconLoading icon={Heart} isActive={isPlaying} color="#8b5cf6" />
|
|
</h3>
|
|
<div className="space-y-3">
|
|
{acts && acts.length > 0 ? (
|
|
acts.map((act) => (
|
|
<ContentCard
|
|
key={act.stableId}
|
|
className="bg-purple-500/20 rounded-lg p-3 border border-purple-500/30"
|
|
>
|
|
<div className="text-purple-300 font-medium text-sm mb-2">
|
|
{act.title}
|
|
</div>
|
|
<div className="text-gray-300 text-xs leading-relaxed mb-2">
|
|
<TypewriterText text={act.desc} stableId={act.stableId} />
|
|
</div>
|
|
<div className="flex flex-wrap gap-1">
|
|
{act.beats?.map((beat, index) => (
|
|
<span key={index} className="text-xs bg-purple-500/30 px-2 py-1 rounded">
|
|
{beat}
|
|
</span>
|
|
))}
|
|
</div>
|
|
</ContentCard>
|
|
))
|
|
) : (
|
|
Array.from({length: 3}, (_, i) => (
|
|
<SkeletonCard key={i} className="bg-purple-500/20 rounded-lg p-3 border border-purple-500/30" />
|
|
))
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
});
|
|
|
|
// Character Arc Design Component
|
|
const CharacterArcDesign = React.memo(({ characters, isPlaying }: { characters?: ScriptContent['characters']; isPlaying: boolean }) => {
|
|
return (
|
|
<div className="bg-black/30 rounded-lg p-4">
|
|
<h3 className="text-white font-semibold mb-3 flex items-center space-x-2">
|
|
<span>Character arc design</span>
|
|
<IconLoading icon={Heart} isActive={isPlaying} color="#8b5cf6" />
|
|
</h3>
|
|
<div className="space-y-3">
|
|
{characters && characters.length > 0 ? (
|
|
characters.map((char) => (
|
|
<ContentCard
|
|
key={char.stableId}
|
|
className="bg-slate-700/50 rounded-lg p-3 border border-slate-600/50"
|
|
>
|
|
<div className="flex items-center space-x-2 mb-2">
|
|
<div
|
|
className="w-3 h-3 rounded-full"
|
|
style={{ backgroundColor: char.color }}
|
|
/>
|
|
<span className="text-white font-medium text-sm">{char.name}</span>
|
|
<span className="text-gray-400 text-xs">({char.role})</span>
|
|
</div>
|
|
<div className="text-xs mb-2" style={{ color: char.color }}>
|
|
{char.arc}
|
|
</div>
|
|
<div className="text-gray-300 text-xs leading-relaxed">
|
|
<TypewriterText text={char.desc} stableId={char.stableId} />
|
|
</div>
|
|
</ContentCard>
|
|
))
|
|
) : (
|
|
Array.from({length: 3}, (_, i) => (
|
|
<SkeletonCard key={i} className="bg-purple-500/20 rounded-lg p-3 border border-purple-500/30" />
|
|
))
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
});
|
|
|
|
// Dialogue Rhythm Component
|
|
const DialogueRhythm = React.memo(({ dialogue, isPlaying }: { dialogue?: ScriptContent['dialogue']; isPlaying: boolean }) => {
|
|
return (
|
|
<div className="bg-black/30 rounded-lg p-4">
|
|
<h3 className="text-white font-semibold mb-3 flex items-center space-x-2">
|
|
<span>Dialogue rhythm</span>
|
|
<IconLoading icon={Heart} isActive={isPlaying} color="#8b5cf6" />
|
|
</h3>
|
|
{dialogue ? (
|
|
<div className="space-y-3">
|
|
<div>
|
|
<div className="text-purple-300 text-sm font-medium mb-1">Rhythm control</div>
|
|
<div className="text-gray-300 text-xs">
|
|
<TypewriterText text={dialogue.rhythm} stableId={`${dialogue.stableId}-rhythm`} />
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<div className="text-purple-300 text-sm font-medium mb-1">Expression style</div>
|
|
<div className="text-gray-300 text-xs">
|
|
<TypewriterText text={dialogue.style} stableId={`${dialogue.stableId}-style`} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<SkeletonCard className="bg-purple-500/20 rounded-lg p-3 border border-purple-500/30" />
|
|
)}
|
|
</div>
|
|
);
|
|
});
|
|
|
|
// Theme Development Component
|
|
const ThemeDevelopment = React.memo(({ themes, isPlaying }: { themes?: ScriptContent['themes']; isPlaying: boolean }) => {
|
|
return (
|
|
<div className="bg-black/30 rounded-lg p-4">
|
|
<h3 className="text-white font-semibold mb-3 flex items-center space-x-2">
|
|
<span>Theme deepening process</span>
|
|
<IconLoading icon={Heart} isActive={isPlaying} color="#8b5cf6" />
|
|
</h3>
|
|
<div className="space-y-3">
|
|
{themes ? (
|
|
themes.map((theme) => (
|
|
<ContentCard
|
|
key={theme.stableId}
|
|
className="bg-purple-400/10 rounded p-3"
|
|
>
|
|
<div className="text-purple-200 text-sm font-medium mb-1">{theme.theme}</div>
|
|
<div className="text-gray-300 text-xs mb-2">
|
|
<TypewriterText text={theme.desc} stableId={`${theme.stableId}-desc`} />
|
|
</div>
|
|
<div className="text-gray-400 text-xs">
|
|
<TypewriterText text={theme.depth} stableId={`${theme.stableId}-depth`} />
|
|
</div>
|
|
</ContentCard>
|
|
))
|
|
) : (
|
|
Array.from({length: 2}, (_, i) => (
|
|
<SkeletonCard key={i} className="bg-purple-500/20 rounded-lg p-3 border border-purple-500/30" />
|
|
))
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
});
|
|
|
|
// Dramatic Line Component
|
|
const DramaticLine = React.memo(({ dramaticLine, isPlaying }: { dramaticLine?: ScriptContent['dramaticLine']; isPlaying: boolean }) => {
|
|
return (
|
|
<div className="bg-black/30 rounded-lg p-4">
|
|
<h3 className="text-white font-semibold mb-3 flex items-center space-x-2">
|
|
<span>Dramatic line</span>
|
|
<IconLoading icon={Heart} isActive={isPlaying} color="#8b5cf6" />
|
|
</h3>
|
|
{dramaticLine ? (
|
|
<div className="space-y-4">
|
|
<div className="h-48 bg-purple-500/10 rounded-lg p-2">
|
|
<ResponsiveContainer width="100%" height="100%">
|
|
<LineChart
|
|
data={dramaticLine.points || []}
|
|
margin={{ top: 10, right: 10, left: 0, bottom: 10 }}
|
|
>
|
|
<CartesianGrid strokeDasharray="3 3" stroke="#374151" />
|
|
<XAxis
|
|
dataKey="title"
|
|
tick={{ fill: '#9CA3AF', fontSize: 10 }}
|
|
stroke="#4B5563"
|
|
/>
|
|
<YAxis
|
|
domain={[0, 100]}
|
|
tick={{ fill: '#9CA3AF', fontSize: 10 }}
|
|
stroke="#4B5563"
|
|
label={{
|
|
value: 'Emotional intensity',
|
|
angle: -90,
|
|
position: 'insideLeft',
|
|
fill: '#9CA3AF',
|
|
fontSize: 12
|
|
}}
|
|
/>
|
|
<Tooltip content={<CustomTooltip />} />
|
|
<Line
|
|
type="monotone"
|
|
dataKey="intensity"
|
|
stroke="#8B5CF6"
|
|
strokeWidth={2}
|
|
dot={{
|
|
fill: '#8B5CF6',
|
|
stroke: '#C4B5FD',
|
|
strokeWidth: 2,
|
|
r: 4
|
|
}}
|
|
activeDot={{
|
|
fill: '#8B5CF6',
|
|
stroke: '#C4B5FD',
|
|
strokeWidth: 2,
|
|
r: 6
|
|
}}
|
|
/>
|
|
</LineChart>
|
|
</ResponsiveContainer>
|
|
</div>
|
|
<div className="space-y-2">
|
|
{dramaticLine.points?.map((point) => (
|
|
<ContentCard
|
|
key={point.stableId}
|
|
className="bg-purple-400/10 rounded p-2"
|
|
>
|
|
<div className="text-purple-200 text-sm font-medium mb-1">{point.title}</div>
|
|
<div className="text-gray-300 text-xs">
|
|
<TypewriterText text={point.desc} stableId={point.stableId} />
|
|
</div>
|
|
</ContentCard>
|
|
))}
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-4">
|
|
<SkeletonCard className="h-48 bg-purple-500/20 rounded-lg border border-purple-500/30" />
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
});
|
|
|
|
const Scriptwriter: React.FC<ScriptwriterProps> = React.memo(({ currentContent, isPlaying }) => {
|
|
return (
|
|
<div className="grid grid-cols-2 gap-4 h-full">
|
|
{/* 左侧:三幕结构和角色弧光 */}
|
|
<div className="space-y-4 overflow-y-auto">
|
|
<ThreeActStructure acts={currentContent.acts} isPlaying={isPlaying} />
|
|
<CharacterArcDesign characters={currentContent.characters} isPlaying={isPlaying} />
|
|
</div>
|
|
|
|
{/* 右侧:对白节奏和主题深化 */}
|
|
<div className="space-y-4 overflow-y-auto">
|
|
<DialogueRhythm dialogue={currentContent.dialogue} isPlaying={isPlaying} />
|
|
<ThemeDevelopment themes={currentContent.themes} isPlaying={isPlaying} />
|
|
<DramaticLine dramaticLine={currentContent.dramaticLine} isPlaying={isPlaying} />
|
|
</div>
|
|
</div>
|
|
);
|
|
});
|
|
|
|
Scriptwriter.displayName = 'Scriptwriter';
|
|
|
|
export default Scriptwriter;
|