100 lines
3.9 KiB
JavaScript
100 lines
3.9 KiB
JavaScript
import React, { useEffect, useState } from "react";
|
|
import { api, subscribeState } from "./api.js";
|
|
import { StatusChip, ProgressBar, Spinner } from "./ui.jsx";
|
|
import Chapters from "./Chapters.jsx";
|
|
import AnalysisEditor from "./AnalysisEditor.jsx";
|
|
import CastEditor from "./CastEditor.jsx";
|
|
import PronunciationEditor from "./PronunciationEditor.jsx";
|
|
|
|
const STAGES = [
|
|
{ key: "analyze", label: "Analyse", action: (s) => api.analyze(s), hint: "Découpe le texte, détecte les locuteurs et le casting." },
|
|
{ key: "cast", label: "Casting", action: (s) => api.castAuto(s), hint: "Attribue une voix à chaque personnage." },
|
|
{ key: "pronounce", label: "Prononciations", action: (s) => api.pronounce(s), hint: "Repère les mots à risque de mauvaise prononciation." },
|
|
];
|
|
|
|
export default function BookView({ slug, onBack }) {
|
|
const [data, setData] = useState(null);
|
|
const [state, setState] = useState(null);
|
|
const [tab, setTab] = useState("chapters");
|
|
|
|
useEffect(() => {
|
|
api.getBook(slug).then((d) => { setData(d); setState(d.state); });
|
|
const unsub = subscribeState(slug, setState);
|
|
return unsub;
|
|
}, [slug]);
|
|
|
|
if (!data) return <p className="text-ink-muted"><Spinner /> chargement…</p>;
|
|
const { book } = data;
|
|
const st = state || data.state;
|
|
const busy = !!st.active_stage;
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<button onClick={onBack} className="text-sm text-ink-muted hover:text-ink-text">← Bibliothèque</button>
|
|
|
|
<div className="flex gap-5">
|
|
{book.cover_file && (
|
|
<img src={api.coverUrl(slug)} alt="" className="h-44 rounded-md border border-ink-edge object-cover" />
|
|
)}
|
|
<div className="flex-1">
|
|
<h1 className="font-serif text-2xl">{book.title}</h1>
|
|
<p className="text-ink-muted">{book.author}</p>
|
|
<p className="mt-1 text-sm text-ink-muted">{book.chapters.filter((c) => c.render).length} chapitres à narrer</p>
|
|
|
|
{busy && (
|
|
<div className="mt-4 max-w-md space-y-1">
|
|
<div className="flex justify-between text-xs text-ink-accent">
|
|
<span>{st.active_detail || st.active_stage}</span>
|
|
<span>{Math.round((st.active_progress || 0) * 100)}%</span>
|
|
</div>
|
|
<ProgressBar value={st.active_progress} />
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Pipeline */}
|
|
<div className="grid grid-cols-1 gap-3 sm:grid-cols-3">
|
|
{STAGES.map((stage) => {
|
|
const status = st.stages?.[stage.key] || "pending";
|
|
return (
|
|
<div key={stage.key} className="card p-4">
|
|
<div className="flex items-center justify-between">
|
|
<span className="font-medium">{stage.label}</span>
|
|
<StatusChip status={status} />
|
|
</div>
|
|
<p className="mt-1 text-xs text-ink-muted">{stage.hint}</p>
|
|
<button className="btn-ghost mt-3" disabled={busy}
|
|
onClick={() => stage.action(slug)}>
|
|
{status === "done" ? "Relancer" : "Lancer"}
|
|
</button>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
{/* Onglets */}
|
|
<div className="flex gap-1 border-b border-ink-edge">
|
|
{[
|
|
["chapters", "Chapitres"],
|
|
["analysis", "Analyse"],
|
|
["cast", "Casting"],
|
|
["pron", "Prononciation"],
|
|
].map(([key, label]) => (
|
|
<button key={key} onClick={() => setTab(key)}
|
|
className={`px-4 py-2 text-sm ${tab === key
|
|
? "border-b-2 border-ink-accent text-ink-text"
|
|
: "text-ink-muted hover:text-ink-text"}`}>
|
|
{label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
{tab === "chapters" && <Chapters slug={slug} book={book} state={st} busy={busy} />}
|
|
{tab === "analysis" && <AnalysisEditor slug={slug} book={book} state={st} />}
|
|
{tab === "cast" && <CastEditor slug={slug} busy={busy} />}
|
|
{tab === "pron" && <PronunciationEditor slug={slug} />}
|
|
</div>
|
|
);
|
|
}
|