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 () => {
|
const handleGenerate = async () => {
|
||||||
setIsGenerating(true)
|
setIsGenerating(true)
|
||||||
setError(null)
|
setError(null)
|
||||||
@@ -265,6 +308,9 @@ function App() {
|
|||||||
segments={scriptSegments}
|
segments={scriptSegments}
|
||||||
roles={roles}
|
roles={roles}
|
||||||
onSegmentChange={handleSegmentChange}
|
onSegmentChange={handleSegmentChange}
|
||||||
|
onSegmentTextChange={handleSegmentTextChange}
|
||||||
|
onSegmentSplit={handleSegmentSplit}
|
||||||
|
onSegmentDelete={handleSegmentDelete}
|
||||||
onGenerate={handleGenerate}
|
onGenerate={handleGenerate}
|
||||||
onSkipReview={handleSkipReview}
|
onSkipReview={handleSkipReview}
|
||||||
isGenerating={isGenerating}
|
isGenerating={isGenerating}
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ interface ScriptReviewSectionProps {
|
|||||||
segments: ScriptSegment[];
|
segments: ScriptSegment[];
|
||||||
roles: Role[];
|
roles: Role[];
|
||||||
onSegmentChange: (segmentId: string, newSpeaker: string) => void;
|
onSegmentChange: (segmentId: string, newSpeaker: string) => void;
|
||||||
|
onSegmentTextChange: (segmentId: string, newText: string) => void;
|
||||||
|
onSegmentSplit: (segmentId: string, splitPosition: number) => void;
|
||||||
|
onSegmentDelete: (segmentId: string) => void;
|
||||||
onGenerate: () => void;
|
onGenerate: () => void;
|
||||||
onSkipReview: () => void;
|
onSkipReview: () => void;
|
||||||
isGenerating: boolean;
|
isGenerating: boolean;
|
||||||
@@ -13,6 +16,9 @@ export default function ScriptReviewSection({
|
|||||||
segments,
|
segments,
|
||||||
roles,
|
roles,
|
||||||
onSegmentChange,
|
onSegmentChange,
|
||||||
|
onSegmentTextChange,
|
||||||
|
onSegmentSplit,
|
||||||
|
onSegmentDelete,
|
||||||
onGenerate,
|
onGenerate,
|
||||||
onSkipReview,
|
onSkipReview,
|
||||||
isGenerating,
|
isGenerating,
|
||||||
@@ -21,6 +27,26 @@ export default function ScriptReviewSection({
|
|||||||
onSegmentChange(segmentId, newSpeaker);
|
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 (
|
return (
|
||||||
<div className="max-w-5xl mx-auto">
|
<div className="max-w-5xl mx-auto">
|
||||||
<div className="card-modern p-10 sm:p-12 space-y-10">
|
<div className="card-modern p-10 sm:p-12 space-y-10">
|
||||||
@@ -66,8 +92,36 @@ export default function ScriptReviewSection({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Segment Text */}
|
{/* 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">
|
<textarea
|
||||||
{segment.text}
|
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>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
Reference in New Issue
Block a user