/* Tab: Lanzamiento Automatizado → Lista — hoja editable (aún sin backend: los datos no
se guardan, se pierden al recargar). Selector de canal (Todos + cada canal activo)
igual que en Producción · Kanban. Clic en una celda para rellenarla; al escribir el
título de la última fila se añade automáticamente una fila nueva debajo. */
const LAUNCH_SHEET_COLUMNS = ["Título", "Narración", "Estado", "Canal", "Coste", ""];
const LAUNCH_CELL_STYLE = {
width: "100%", border: "none", background: "transparent", outline: "none",
fontSize: 12.5, color: "var(--text)", padding: "4px 2px", fontFamily: "inherit",
};
// Icono de canal: logo si tiene, si no el avatar (como en el resto de la app), si no
// las iniciales.
function LaunchChannelIcon({ ch, size = 18 }) {
if (!ch) return null;
const src = ch.logo || ch.avatarUrl || null;
return (
{src ? : ch.initials}
);
}
function NarrationModal({ row, onClose, onSave }) {
const [text, setText] = React.useState(row.narration || "");
const [useOwn, setUseOwn] = React.useState(!!row.useOwnNarration);
const [maintainContext, setMaintainContext] = React.useState(!!row.maintainObjectContext);
return (
| {col} | ))}|||||
|---|---|---|---|---|---|
| updateRow(r.id, "title", e.target.value)} title={locked ? "El vídeo ya está generado con este título — no se puede editar" : undefined}/> |
{(() => {
if (isDraft) return —;
if (r.error) return (
{ e.stopPropagation(); setErrorPopoverRow(errorPopoverRow === r.id ? null : r.id); }}>
Error
{errorPopoverRow === r.id && (
|
{locked ? (
e.stopPropagation()} style={{
left: 0, top: 26, padding: 6, minWidth: 190,
display: "grid", gridTemplateColumns: "repeat(3, 1fr)", gap: 6,
}}>
{(channelAvatars[r.channelId] || []).map((av) => (
))}
)}
)}
|
{(() => {
const job = jobForRow(r);
const credits = job && job.creditCost ? job.creditCost : null;
if (credits == null) return "—";
return `$${(credits * 0.005).toFixed(2)}`;
})()}
|
{(() => {
const job = jobForRow(r);
// Si hay jobId pero el job ya no aparece en la lista del servidor,
// es que se borró (a mano o por la limpieza automática a las 48h) —
// no está "procesando", está huérfano: se trata igual que terminado
// para no dejar la fila bloqueada ni el borrado deshabilitado.
const orphaned = !!r.jobId && !job;
const finished = !!r.jobId && (orphaned || ["completed", "failed", "cancelled"].includes(job._rawStatus));
const midGeneration = !!r.jobId && !finished;
const canPlay = (!r.jobId && isRowReady(r)) || finished; // 1ª vez, o regenerar tras terminar
const busy = !!launching[r.id];
const downloadReady = !!(r.jobId && job && job._rawStatus === "completed");
// Remontar solo necesita que ya haya contenido generado (audio + imágenes/clips) —
// no hace falta que el montaje haya terminado bien, así que también se permite
// con el vídeo en 'failed' (p.ej. si el montaje se cortó o falló tras generar todo).
const reassembleReady = !!(r.jobId && job && ["completed", "failed"].includes(job._rawStatus));
return (
{menuForRow === r.id && (
e.stopPropagation()} style={{ right: 0, top: 32, minWidth: 200, padding: 4 }}>
{midGeneration && (
)}
)}
|