import { pausedReason as pausedReasonWords, rejectReason as rejectReasonWords } from '../api';
import type { Book, BookDetail, Run } from '../api';
import { useText, type Text } from '../i18n/text';
import { Button } from '../ui/Button';
import { Callout } from '../ui/Callout';
import { counted, date, languageName, number, statusOf, translatedPercent } from './format';
import { chapters } from './units';
import styles from './About.module.css';
/**
* Metadata of the book (remark 6), extended exactly up to the boundary of the contract. The
* boundary is a defence of the approaches, not a gap: there is no money on the screen (§4.8), and
* no models or pipeline stages either.
*/
export function About({ detail, onStartRun }: { detail: BookDetail; onStartRun?: () => void }) {
const text = useText();
const { book, run } = detail;
const startable = statusOf(book.status).startable === true;
return (
{/* The one action of the book card, and it stands where the card is: a book is translated
from the place that says what the book is. It is offered only where a run can begin —
which state that is, is decided by the vocabulary on the api seam, in one file. */}
{startable && onStartRun !== undefined && (
)}
{fields(detail, text).map(([name, value]) => (
{name}
{value}
))}
);
}
/**
* The states a book is IN rather than the fields it has: still arriving, being cut into sections,
* refused, halted. Each of them is a state the list of pairs below cannot say anything about, and
* each of them is what the person is waiting on.
*
* ⚠ Parsing carries NO number, and that is the contract's shape and not an omission: there is no
* counter of it anywhere on this surface, so a percentage would have to be invented (0.2.3).
*/
function State({ book, run }: { book: Book; run: Run | null }) {
const text = useText();
if (book.status === 'uploading')
return ;
if (book.status === 'parsing') return ;
if (book.status === 'rejected') {
// The machine reason is the platform's, the phrase and the advice are the product's — and the
// advice is the point: "the file is not one we can read" and "not us, not now" end in
// different next actions (0.2.3, PD-173).
const reason = rejectReasonWords.describe(book.reject_reason ?? null);
return (
{text(reason.advice)}
);
}
// A halt is stated where the action is, not only in the status strip: the strip says WHAT, the
// card is where the person looks for what to do about it. The phrase is the owner's (В-6), and
// resume is deliberately not offered — after a ceiling stop it does not move the run at all.
if (book.status === 'paused') {
return (
);
}
return null;
}
function fields(detail: BookDetail, text: Text): [string, string][] {
const { book, run } = detail;
const rows: [string, string][] = [
[text('about.title'), book.title],
[
text('about.languages'),
`${languageName(book.source_lang)} → ${languageName(book.target_lang)}`,
],
[text('about.genre'), book.genre ?? '—'],
[text('about.status'), text(statusOf(book.status).label)],
[text('about.translated'), `${String(translatedPercent(book.progress))}%`],
[text('about.chapters'), number(book.chapter_count)],
[text('about.blocks'), number(book.progress.draft.total)],
// Optional on the wire, so an absent size is a dash rather than a zero — the same shape the
// dates use for "not known".
[
text('about.characters'),
book.character_count === undefined ? '—' : number(book.character_count),
],
[text('about.notes'), number(book.note_count)],
[text('about.added'), date(book.added_at)],
];
if (run) {
rows.push(
[text('about.runStarted'), date(run.started_at)],
// `finished_at` comes both as `null` and as altogether absent: an unfinished run may have no
// field at all — both forms mean one and the same thing and are drawn as a dash.
[text('about.runFinished'), run.finished_at ? date(run.finished_at) : '—'],
// The ceiling and the stop at the signature are the person's own choice before the start,
// not the workings of the engine: showing them is honest and useful, he set them himself.
[text('about.ceiling'), counted(run.ceiling_chapters, chapters)],
[text('about.verifyBank'), run.verify_bank ? text('about.yes') : text('about.no')],
);
}
return rows;
}