textmachine/frontend/src/showcase/About.tsx

119 lines
5.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 (
<div className={styles.card}>
<State book={book} run={run ?? null} />
{/* 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 && (
<Button look="primary" onPress={onStartRun}>
{text('run.action')}
</Button>
)}
<dl className={styles.about}>
{fields(detail, text).map(([name, value]) => (
<div className={styles.field} key={name}>
<dt className={styles.fieldName}>{name}</dt>
<dd className={styles.fieldValue} title={value}>
{value}
</dd>
</div>
))}
</dl>
</div>
);
}
/**
* 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 <Callout tone="quiet" caption={text('about.uploading')} />;
if (book.status === 'parsing') return <Callout tone="quiet" caption={text('about.parsing')} />;
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 (
<Callout tone="note" caption={text(reason.label)}>
{text(reason.advice)}
</Callout>
);
}
// 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 (
<Callout
tone="note"
caption={text(pausedReasonWords.describe(run?.paused_reason ?? null).label)}
/>
);
}
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;
}