Video_AdGenesis_App / frontend /src /components /SegmentPromptsViewer.tsx
sushilideaclan01's picture
Enhance prompt validation and safety features
82a1419
import { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import type { VeoSegment } from '@/types';
import { ChevronDownIcon, CopyIcon, CheckIcon } from './Icons';
interface SegmentPromptsViewerProps {
segments: VeoSegment[];
accentColor: 'coral' | 'electric';
}
export const SegmentPromptsViewer: React.FC<SegmentPromptsViewerProps> = ({
segments,
accentColor
}) => {
const [isOpen, setIsOpen] = useState(false);
const [expandedSegments, setExpandedSegments] = useState<Set<number>>(new Set());
const [copiedIndex, setCopiedIndex] = useState<number | null>(null);
const toggleSegment = (index: number) => {
const newExpanded = new Set(expandedSegments);
if (newExpanded.has(index)) {
newExpanded.delete(index);
} else {
newExpanded.add(index);
}
setExpandedSegments(newExpanded);
};
const expandAll = () => {
setExpandedSegments(new Set(segments.map((_, i) => i)));
};
const collapseAll = () => {
setExpandedSegments(new Set());
};
const copySegment = (segment: VeoSegment, index: number) => {
const formatted = JSON.stringify(segment, null, 2);
navigator.clipboard.writeText(formatted);
setCopiedIndex(index);
setTimeout(() => setCopiedIndex(null), 2000);
};
const copyAllSegments = () => {
const formatted = JSON.stringify({ segments }, null, 2);
navigator.clipboard.writeText(formatted);
setCopiedIndex(-1);
setTimeout(() => setCopiedIndex(null), 2000);
};
if (segments.length === 0) return null;
return (
<div className="mb-8">
<button
onClick={() => setIsOpen(!isOpen)}
className={`
w-full card border-2 transition-colors
${accentColor === 'coral'
? 'border-coral-500/30 hover:border-coral-500/50'
: 'border-electric-500/30 hover:border-electric-500/50'
}
`}
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className={`p-2 rounded-lg ${accentColor === 'coral' ? 'bg-coral-500/20' : 'bg-electric-500/20'}`}>
<svg className={`w-5 h-5 ${accentColor === 'coral' ? 'text-coral-400' : 'text-electric-400'}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
</div>
<div className="text-left">
<h3 className="font-bold text-void-100">View Segment Prompts</h3>
<p className="text-sm text-void-400">
{segments.length} detailed AI-generated prompts
</p>
</div>
</div>
<motion.div
animate={{ rotate: isOpen ? 180 : 0 }}
transition={{ duration: 0.2 }}
>
<ChevronDownIcon size={24} className="text-void-400" />
</motion.div>
</div>
</button>
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
transition={{ duration: 0.3 }}
className="overflow-hidden"
>
<div className="mt-4 space-y-4">
{/* Controls */}
<div className="flex items-center justify-between gap-3 flex-wrap">
<div className="flex gap-2">
<button
onClick={expandAll}
className="btn-secondary-sm"
>
Expand All
</button>
<button
onClick={collapseAll}
className="btn-secondary-sm"
>
Collapse All
</button>
</div>
<button
onClick={copyAllSegments}
className={`
flex items-center gap-2 px-3 py-1.5 rounded-lg text-sm font-medium transition-colors
${accentColor === 'coral'
? 'bg-coral-500/10 text-coral-400 hover:bg-coral-500/20'
: 'bg-electric-500/10 text-electric-400 hover:bg-electric-500/20'
}
`}
>
{copiedIndex === -1 ? (
<>
<CheckIcon size={16} />
Copied All!
</>
) : (
<>
<CopyIcon size={16} />
Copy All JSON
</>
)}
</button>
</div>
{/* Segment Cards */}
<div className="space-y-3">
{segments.map((segment, index) => {
const isExpanded = expandedSegments.has(index);
return (
<div
key={index}
className="card border border-void-700 hover:border-void-600 transition-colors"
>
{/* Segment Header */}
<div className="flex items-start justify-between gap-4">
<button
onClick={() => toggleSegment(index)}
className="flex-1 text-left"
>
<div className="flex items-center gap-3 mb-2">
<span className={`
px-2.5 py-0.5 rounded-full text-xs font-bold
${accentColor === 'coral'
? 'bg-coral-500/20 text-coral-400'
: 'bg-electric-500/20 text-electric-400'
}
`}>
Segment {index + 1}
</span>
<motion.div
animate={{ rotate: isExpanded ? 180 : 0 }}
transition={{ duration: 0.2 }}
>
<ChevronDownIcon size={16} className="text-void-400" />
</motion.div>
</div>
<p className="text-sm text-void-300 line-clamp-2">
{segment.action_timeline?.dialogue || 'No dialogue'}
</p>
</button>
<button
onClick={() => copySegment(segment, index)}
className="p-2 rounded-lg hover:bg-void-800 transition-colors text-void-400 hover:text-void-200"
title="Copy segment JSON"
>
{copiedIndex === index ? (
<CheckIcon size={16} className="text-green-400" />
) : (
<CopyIcon size={16} />
)}
</button>
</div>
{/* Expanded Content */}
<AnimatePresence>
{isExpanded && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
className="overflow-hidden"
>
<div className="mt-4 pt-4 border-t border-void-700 space-y-4 text-sm">
{/* Dialogue */}
<div>
<h4 className="font-semibold text-void-200 mb-2">Dialogue</h4>
<p className="text-void-400 bg-void-900/50 p-3 rounded-lg">
"{segment.action_timeline?.dialogue}"
</p>
</div>
{/* Character Description */}
<div>
<h4 className="font-semibold text-void-200 mb-2">Character</h4>
<div className="space-y-2">
<div>
<span className="text-xs text-void-500 uppercase tracking-wide">Current State:</span>
<p className="text-void-400 bg-void-900/50 p-2 rounded mt-1 text-xs leading-relaxed">
{segment.character_description?.current_state}
</p>
</div>
<div>
<span className="text-xs text-void-500 uppercase tracking-wide">Voice Matching:</span>
<p className="text-void-400 bg-void-900/50 p-2 rounded mt-1 text-xs leading-relaxed">
{segment.character_description?.voice_matching}
</p>
</div>
</div>
</div>
{/* Scene Continuity */}
<div>
<h4 className="font-semibold text-void-200 mb-2">Scene</h4>
<div className="space-y-2">
<div>
<span className="text-xs text-void-500 uppercase tracking-wide">Environment:</span>
<p className="text-void-400 bg-void-900/50 p-2 rounded mt-1 text-xs leading-relaxed">
{segment.scene_continuity?.environment}
</p>
</div>
<div>
<span className="text-xs text-void-500 uppercase tracking-wide">Camera:</span>
<p className="text-void-400 bg-void-900/50 p-2 rounded mt-1 text-xs leading-relaxed">
{segment.scene_continuity?.camera_position} • {segment.scene_continuity?.camera_movement}
</p>
</div>
<div>
<span className="text-xs text-void-500 uppercase tracking-wide">Lighting:</span>
<p className="text-void-400 bg-void-900/50 p-2 rounded mt-1 text-xs leading-relaxed">
{segment.scene_continuity?.lighting_state}
</p>
</div>
</div>
</div>
{/* Synchronized Actions */}
<div>
<h4 className="font-semibold text-void-200 mb-2">Timeline</h4>
<div className="space-y-1.5">
{Object.entries(segment.action_timeline?.synchronized_actions || {}).map(([time, action]) => (
<div key={time} className="flex gap-3">
<span className={`
text-xs font-mono px-2 py-1 rounded
${accentColor === 'coral'
? 'bg-coral-500/10 text-coral-400'
: 'bg-electric-500/10 text-electric-400'
}
`}>
{time}
</span>
<p className="text-void-400 text-xs flex-1">
{action}
</p>
</div>
))}
</div>
</div>
{/* Segment Info */}
<div className="pt-3 border-t border-void-800">
<div className="grid grid-cols-2 gap-3 text-xs">
<div>
<span className="text-void-500">Duration:</span>
<span className="text-void-300 ml-2 font-medium">
{segment.segment_info?.duration}
</span>
</div>
<div>
<span className="text-void-500">Location:</span>
<span className="text-void-300 ml-2 font-medium">
{segment.segment_info?.location}
</span>
</div>
</div>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
);
})}
</div>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
);
};