Files
Hankan/app/src/ui/tutor/GlossBlock.tsx
MechaCat02 ab9b75ff32 feat(tutor): look any word up — the underline, the popover, the word list
The reworked artifact made every Korean word on the lesson screen a way in
to the dictionary, in three tiers. This ports them.

1. The underline. Every Korean run in a message, a gloss or an exercise is
   a button whose underline says what the word is to him: amber new, blue
   learning, jade in review, faint when secure or merely explainable,
   dotted when nothing knows it. A form carries its dictionary word's state
   — 갔어 is 가다's — through the one resolver the gate uses
   (domain/words.ts). The artifact gave "in review" no colour at all.

2. The popover, beside the word: the meaning, "+ Add to deck" (the
   dictionary entry, so 먹었어 adds 먹다), "Ask 선생님" for a word he was
   shown without being taught, a search, the word list. English on an
   exercise's English side opens its Korean, from an index of the course's
   own material — never the frequency bands, whose thousands of glosses
   would bury the answer. An answer chip's tap belongs to the exercise, so
   a 420ms hold glosses it, or a tap with 힌트 on; the hold's click is
   swallowed before the exercise sees it. Pressing a word never takes focus
   from the answer being typed, and the popover no longer closes when the
   window resizes — on a phone that was the keyboard moving.

3. The word list gains + / ✓ on every row, search results included.

Every lookup, whichever tier, goes into the answer's "I had to look up"
and the peek tally — counted under the Korean looked at; the artifact
counted an English lookup under the English word.

A dictionary word added this way is studied: a card on an entry outside
the curated sources now counts as a deck word, in review and in "my
units". Before, it got a card that no review would ever show.

The word list glosses through the same resolver as the popover and the
gate, rather than the surface table alone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 21:59:48 +02:00

84 lines
2.5 KiB
TypeScript

/* The colour-coded sentence breakdown.
Each chunk is a column: 한글 on top, a micro-gloss underneath, and a role
carried by the underline. The optional fourth field highlights the
meaningful piece INSIDE the word — the particle, the tense marker, the
ending — so the grammatical morpheme reads distinctly from the stem. */
import { Fragment } from "react";
import type { GlossBlock as Block, GlossPart, GlossRole } from "@lib/blocks.js";
import { ROLE_STYLES, isRole, legendFor } from "./roles.js";
import { KoText } from "./lookup.js";
import "./gloss.css";
/** Wrap the LAST occurrence of the highlight, which is where a suffix sits. */
function withHighlight(ko: string, highlight: string) {
const at = highlight ? ko.lastIndexOf(highlight) : -1;
if (at < 0) return <KoText text={ko} />;
return (
<>
<KoText text={ko.slice(0, at)} />
<em>
<KoText text={highlight} />
</em>
<KoText text={ko.slice(at + highlight.length)} />
</>
);
}
function Word({ part }: { part: GlossPart }) {
const role: GlossRole = isRole(part.role) ? part.role : "N";
const style = ROLE_STYLES[role];
return (
<span
className="gw"
data-role={role}
data-underline={style.underline}
style={
{
"--role-color": `var(${style.color})`,
"--role-bg": `var(${style.bg})`,
} as React.CSSProperties
}
>
<span className="k ko">{withHighlight(part.ko, part.highlight)}</span>
{part.gloss && <span className="g">{part.gloss}</span>}
</span>
);
}
export function GlossBlocks({ blocks }: { blocks: Block[] }) {
const used = legendFor(
blocks.flatMap((b) => b.parts.map((p) => (isRole(p.role) ? p.role : "N"))),
);
return (
<div className="gloss-set">
{blocks.map((b, i) => (
<div className="gloss" key={i}>
<div className="gloss-line">
{b.parts.map((p, j) => (
<Word part={p} key={j} />
))}
</div>
{b.en && <div className="gloss-en">{b.en}</div>}
</div>
))}
{used.length > 0 && (
<div className="gloss-key">
{used.map((r) => (
<Fragment key={r}>
<span className="key-item">
<i style={{ background: `var(${ROLE_STYLES[r].color})` }} />
<span className="ko">{ROLE_STYLES[r].label}</span>
</span>
</Fragment>
))}
</div>
)}
</div>
);
}