Spaces:
Running
Running
File size: 4,530 Bytes
94004a2 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 | import React, { useEffect, useState } from 'react';
import { Sparkles, X } from 'lucide-react';
import { toast } from 'react-hot-toast';
import { Dialog, Button, Textarea, Field, Badge } from '../ui';
import { apiPost } from '../api/client';
import './Misc.css';
/**
* DirectionDialog — Phase 4.2 per-segment direction editor.
*
* The user types a natural-language note ("urgent and surprised"). We call
* /tools/direction to preview the parsed taxonomy tokens + instruct prompt +
* rate bias — useful for seeing what the LLM/heuristic actually took away.
*
* On Save, the parent receives the raw direction string and persists it on
* the segment. The dub pipeline re-parses at send time so the saved text is
* always the canonical input.
*/
export default function DirectionDialog({ open, seg, onSave, onClose }) {
const [text, setText] = useState('');
const [preview, setPreview] = useState(null);
const [parsing, setParsing] = useState(false);
const [saving, setSaving] = useState(false);
useEffect(() => {
if (open) {
setText(seg?.direction || '');
setPreview(null);
}
}, [open, seg?.id, seg?.direction]);
const runPreview = async () => {
if (!text.trim()) { setPreview(null); return; }
setParsing(true);
try {
setPreview(await apiPost('/tools/direction', { text }));
} catch (e) {
toast.error(`Preview failed: ${e.message}`);
} finally {
setParsing(false);
}
};
const save = async () => {
setSaving(true);
try {
await onSave?.(text.trim());
onClose?.();
} finally { setSaving(false); }
};
if (!open) return null;
return (
<Dialog
open
onClose={onClose}
size="md"
title={<><Sparkles size={14} /> Direction for segment #{seg?.id?.slice?.(0, 6) || ''}</>}
footer={
<>
{seg?.direction && (
<Button
variant="ghost" size="sm"
onClick={() => { setText(''); }}
leading={<X size={11} />}
className="dir-clear-btn"
>
Clear
</Button>
)}
<Button variant="ghost" onClick={onClose}>Cancel</Button>
<Button variant="primary" onClick={save} loading={saving}>Save direction</Button>
</>
}
>
<p className="direction-dialog__desc">
Tell the pipeline how this line should feel. Plain English works — the system
maps your words onto a stable taxonomy (energy / emotion / pace / intimacy / formality),
then threads the taxonomy through Cinematic translation, TTS, and slot-fit.
</p>
<Field label="Direction" hint={
seg?.text ? <>Line: <em>"{seg.text.slice(0, 80)}{seg.text.length > 80 ? '…' : ''}"</em></> : null
}>
<Textarea
rows={3}
value={text}
onChange={e => setText(e.target.value)}
placeholder="e.g. urgent and surprised / warm, hopeful / whispered, intimate"
autoFocus
/>
</Field>
<div className="dir-preview-actions">
<Button
variant="subtle" size="sm"
onClick={runPreview}
loading={parsing}
disabled={!text.trim()}
>
Preview parse
</Button>
{preview && (
<Badge tone={preview.method === 'llm' ? 'violet' : 'neutral'} size="xs">
{preview.method}
</Badge>
)}
</div>
{preview && (
<div className="direction-dialog__preview">
<div>
<strong>TTS instruct:</strong> <code>{preview.instruct_prompt || '— (nothing parsed)'}</code>
</div>
<div>
<strong>Translate hint:</strong> <em>{preview.translate_hint || '—'}</em>
</div>
<div>
<strong>Rate bias:</strong> <code>{preview.rate_bias?.toFixed?.(2)}</code>
{preview.rate_bias > 1.05 && <> · <span className="dir-rate-up">speeds up</span></>}
{preview.rate_bias < 0.95 && <> · <span className="dir-rate-down">slows down</span></>}
</div>
{Object.keys(preview.tokens || {}).length > 0 && (
<details>
<summary>taxonomy tokens</summary>
<pre>{JSON.stringify(preview.tokens, null, 2)}</pre>
</details>
)}
{preview.error && (
<div className="dir-error">
{preview.error}
</div>
)}
</div>
)}
</Dialog>
);
}
|