Spaces:
Running
Running
| 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> | |
| ); | |
| }; | |