Add segment splitting and editing in script review
- Added ability to edit segment text directly in textarea - Added split segment button to divide segments at natural break points - Added delete segment button to remove unwanted segments - Split logic finds sentence/quote boundaries for natural splits - All segments maintain speaker assignments after split
This commit is contained in:
@@ -103,6 +103,49 @@ function App() {
|
||||
)
|
||||
}
|
||||
|
||||
const handleSegmentTextChange = (segmentId: string, newText: string) => {
|
||||
setScriptSegments(prev =>
|
||||
prev.map(segment =>
|
||||
segment.id === segmentId
|
||||
? { ...segment, text: newText }
|
||||
: segment
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
const handleSegmentSplit = (segmentId: string, splitPosition: number) => {
|
||||
setScriptSegments(prev => {
|
||||
const segmentIndex = prev.findIndex(s => s.id === segmentId)
|
||||
if (segmentIndex === -1) return prev
|
||||
|
||||
const segment = prev[segmentIndex]
|
||||
const firstPart = segment.text.substring(0, splitPosition).trim()
|
||||
const secondPart = segment.text.substring(splitPosition).trim()
|
||||
|
||||
if (!firstPart || !secondPart) return prev
|
||||
|
||||
// Create new segments
|
||||
const newSegments = [...prev]
|
||||
newSegments[segmentIndex] = {
|
||||
...segment,
|
||||
text: firstPart
|
||||
}
|
||||
|
||||
// Insert second part after the first
|
||||
newSegments.splice(segmentIndex + 1, 0, {
|
||||
id: `${segmentId}-split-${Date.now()}`,
|
||||
speaker: segment.speaker, // Keep same speaker by default
|
||||
text: secondPart
|
||||
})
|
||||
|
||||
return newSegments
|
||||
})
|
||||
}
|
||||
|
||||
const handleSegmentDelete = (segmentId: string) => {
|
||||
setScriptSegments(prev => prev.filter(segment => segment.id !== segmentId))
|
||||
}
|
||||
|
||||
const handleGenerate = async () => {
|
||||
setIsGenerating(true)
|
||||
setError(null)
|
||||
@@ -265,6 +308,9 @@ function App() {
|
||||
segments={scriptSegments}
|
||||
roles={roles}
|
||||
onSegmentChange={handleSegmentChange}
|
||||
onSegmentTextChange={handleSegmentTextChange}
|
||||
onSegmentSplit={handleSegmentSplit}
|
||||
onSegmentDelete={handleSegmentDelete}
|
||||
onGenerate={handleGenerate}
|
||||
onSkipReview={handleSkipReview}
|
||||
isGenerating={isGenerating}
|
||||
|
||||
@@ -4,6 +4,9 @@ interface ScriptReviewSectionProps {
|
||||
segments: ScriptSegment[];
|
||||
roles: Role[];
|
||||
onSegmentChange: (segmentId: string, newSpeaker: string) => void;
|
||||
onSegmentTextChange: (segmentId: string, newText: string) => void;
|
||||
onSegmentSplit: (segmentId: string, splitPosition: number) => void;
|
||||
onSegmentDelete: (segmentId: string) => void;
|
||||
onGenerate: () => void;
|
||||
onSkipReview: () => void;
|
||||
isGenerating: boolean;
|
||||
@@ -13,6 +16,9 @@ export default function ScriptReviewSection({
|
||||
segments,
|
||||
roles,
|
||||
onSegmentChange,
|
||||
onSegmentTextChange,
|
||||
onSegmentSplit,
|
||||
onSegmentDelete,
|
||||
onGenerate,
|
||||
onSkipReview,
|
||||
isGenerating,
|
||||
@@ -21,6 +27,26 @@ export default function ScriptReviewSection({
|
||||
onSegmentChange(segmentId, newSpeaker);
|
||||
};
|
||||
|
||||
const handleTextChange = (segmentId: string, newText: string) => {
|
||||
onSegmentTextChange(segmentId, newText);
|
||||
};
|
||||
|
||||
const handleSplit = (segmentId: string, text: string) => {
|
||||
// Find a good split point (after a sentence or quote)
|
||||
const quoteMatch = text.match(/^(.*?[.!?]"?\s*)(.*)/s);
|
||||
if (quoteMatch && quoteMatch[1].length > 0 && quoteMatch[2].length > 0) {
|
||||
onSegmentSplit(segmentId, quoteMatch[1].length);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fall back to middle split
|
||||
const midPoint = Math.floor(text.length / 2);
|
||||
const splitPoint = text.lastIndexOf(' ', midPoint);
|
||||
if (splitPoint > 0) {
|
||||
onSegmentSplit(segmentId, splitPoint + 1);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-5xl mx-auto">
|
||||
<div className="card-modern p-10 sm:p-12 space-y-10">
|
||||
@@ -66,8 +92,36 @@ export default function ScriptReviewSection({
|
||||
</div>
|
||||
|
||||
{/* Segment Text */}
|
||||
<div className="p-5 bg-white rounded-xl border-2 border-gray-200 text-gray-800 whitespace-pre-wrap leading-relaxed text-base">
|
||||
{segment.text}
|
||||
<textarea
|
||||
value={segment.text}
|
||||
onChange={(e) => handleTextChange(segment.id, e.target.value)}
|
||||
className="w-full p-5 bg-white rounded-xl border-2 border-gray-200 text-gray-800 leading-relaxed text-base focus:border-indigo-500 focus:ring-4 focus:ring-indigo-100 focus:outline-none resize-y min-h-[100px] font-sans"
|
||||
disabled={isGenerating}
|
||||
/>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex gap-2 mt-4">
|
||||
<button
|
||||
onClick={() => handleSplit(segment.id, segment.text)}
|
||||
disabled={isGenerating || segment.text.length < 20}
|
||||
className="px-4 py-2 text-sm font-semibold text-indigo-600 bg-indigo-50 rounded-lg hover:bg-indigo-100 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200 flex items-center gap-2"
|
||||
title="Split this segment into two parts"
|
||||
>
|
||||
<span>✂️</span>
|
||||
Split Segment
|
||||
</button>
|
||||
|
||||
{segments.length > 1 && (
|
||||
<button
|
||||
onClick={() => onSegmentDelete(segment.id)}
|
||||
disabled={isGenerating}
|
||||
className="px-4 py-2 text-sm font-semibold text-red-600 bg-red-50 rounded-lg hover:bg-red-100 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200 flex items-center gap-2"
|
||||
title="Delete this segment"
|
||||
>
|
||||
<span>🗑️</span>
|
||||
Delete
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user