Files

399 lines
24 KiB
Markdown
Raw Permalink Normal View History

# Reading package
This package implements the Chinese `language-reader` workspace. Its independent
contracts are LanguageReader, ReadingReader, ReadingDocument, ReadingFeature, ReadingJob,
ReadingVocabulary, ReadingWord, and ReadingOwner. Baseline Studio, the workspace agent, and checked
source activation remain available. Reading entities expose AgentDocumentation;
read `guide` to find this resource and its extension points from an entity.
## Run the workspace
From the Quixos repository:
```sh
nix run .#camino-dev -- start --template language-reader --dir /home/timothy/qx-language-reader --agent
```
The command prints a local browser launch link. Reusing the directory resumes
the same workspace and its independent authoring workbench. A different
directory creates a fresh workspace from the Chinese template.
## Entities
LanguageReader owns documents, annotation features, vocabulary, and durable jobs.
Documents preserve their exact original text and source attribution. Their
Canonical sourceRevision is a content hash; variant revisions identify an immutable
adaptation. All ranges use original UTF-16 offsets in that document.
Creating a rewritten document retains the original document. Features contain
presentation preferences and a processor key; schemas and implementations belong
in code. Feature changes use expectedRevision to protect concurrent edits.
Inference is admitted through a persisted narrow Agent. Each scope request owns
a thread and request; managed graph edges retain those associations. Completion
subscriptions validate structured output and record results without an open
reader tab. Reading annotations default to Luna without reasoning and fast
mode. Cache keys include source revision, semantic configuration, scope, instructions,
output schema and actual context. Model routing/profile and scheduling preferences
do not invalidate completed semantic results. Results persist indefinitely until
explicit regeneration or deletion; attempt history is separate from the indexed
canonical successful result. Declarative model processors retain
results across unrelated runtime and presentation edits. Local processors and
prompt closures conservatively include the package source fingerprint. `ReadingFeature.processorRevision` exposes the checked processor identity
without copying its implementation into graph state. Old processor results are
hidden and old in-flight completions become stale after source activation.
Duplicate requests reuse the job, including a recorded
failure; explicit retries are bounded. Old feature results cannot replace newer
configuration. The document source is untrusted data, not developer instructions.
## Add an annotation
Edit `src/processors.ts` or import a new implementation there. A processor
declares its label, computation strategy, scope, and instructions. A local
processor returns source ranges directly. Section processors produce at most
180 ranges in one pass over a bounded section. Focus processors explain a word
or sentence and use neighboring text as context. Chinese initial features are
in `src/examples/chinese.ts`; French implementations are source examples in
`src/examples/french.ts`. Choosing another language is a workspace template or
source change, not an in-app preset switch.
Generation and reveal are independent. A single section-wide tagging pass can
prepare before reading. A feature requiring one model call per word must be
focused, cached, and lazy. Heavy hover requests should wait for deliberate dwell;
cached or local annotations reveal immediately. Never start a per-word background
fan-out to satisfy a precomputation setting. Leaving hover discards interest in
its result; the durable request may still finish and populate its cache.
Reader settings use a large modal with a reader selector, General settings,
annotator navigation, and breadcrumbs. A document attaches to a ReadingReader;
settings apply to all its attached documents. Cloning copies settings and creates
independent annotator objects. General settings include size, maximum width
(0 is unbounded), CSS font family/fallback list, and background.
Premades insert enabled copies of existing processors. New annotators start with
a visual type, optional title and type-specific color/font suggestions, then a
natural-language behavior request. There is no model/content chooser. The agent
creates checked code, generates a title if needed, chooses preparation strategy,
and supplies any implementation-specific configuration controls.
One processor can present a cached item through several rendering hooks. Declare
`Processor.renders`, for example:
```ts
renders: [
{ style: "replace", reveal: "always" },
{ style: "tooltip", reveal: "hover", content: "source" },
]
```
Each hook keeps the original source range and data source. A replacement's color
already controls its ink. A source tooltip slices the original French text
locally, with no extra inference. `annotation-display.ts` filters/decorates the
cached facts before all hooks; `render-hooks.ts` projects their appearances.
Separate annotator objects can also share facts through `cache.identity` and the
same semantic scope/options, as pinyin and hover pinyin do. Generation stays with
the processor; reveal belongs to each rendering hook.
Premades currently create one feature from code-owned catalog metadata. QX object
initialization can install several edges from supplied references, including their
reciprocal relationships. It does not clone neighboring objects. To instantiate
a processor/renderer bundle as separate graph objects, create all of them with
distinct creation keys in one checked transaction and pass references between
seeds. `addFrenchReader` and `addReader(copy)` are working examples. No second
construction mechanism is needed for multi-edge initialization.
Text content loads before annotation history. Preparation waits for that history
to prevent cache misses while it is still loading. Planning and display-mode
changes have checked JavaScript implementations and run against the local graph;
server validation remains the same. Measurement and segmentation caches survive
page and annotation changes. Package source changes do not invalidate declarative
model results unless instructions, schema, semantic options or actual context
change. Update the task version in `processor-identity.ts` when changing the model
request wire shape or shared task construction; the text planner is fingerprinted
automatically.
Styles are overhead, highlight, underline, color, tooltip, sidebar, replace, bold,
and italic. Hover must not reflow text: labels, replacement, bold, italic, font
changes, sizes and spacing use permanent or click activation instead. This also
applies to custom render hooks. Tooltip/sidebar use hover or click. The renderer
preserves canonical source mapping for replacement spans:
selection/copy returns source text. Fitting words and short labels move together;
oversized spans may wrap. Processing contours cover intervening rows and paragraph
gaps with straight connectors, without diagonal spikes. The embedded feature author makes actual checked
source changes, activates a successful candidate, and registers the new feature
through LanguageReader.addFeature. It has source and graph capabilities; normal
annotation inference has neither. Their prompts and capability settings are
captured by the existing Agent admission implementation.
## Check and activate
Inside an authored workbench, use `qx-workspace check` in each changed resource
and then `root/`. Activate its successful report with `qx-workspace activate`.
Inspect the selected identity and new feature afterward. Preserve unrelated
source and concurrent edits. Never report successful activation after merely
writing files. Failed compilation or activation must preserve the working app.
Renderer code and source-selection tests live in
LanguageReaderPresentation/src/canvas and tests/canvas.test.ts. Follow its
RENDERER_PROVENANCE.md when maintaining the recovered measured canvas approach.
Output schema changes require updating validation and consumer code together.
## Sources and read aloud
Import pasted text or UTF-8 text files, Chinese Wikipedia articles, or opening
excerpts from the existing Gutenberg catalog. Wikipedia imports retain their
source URL and CC BY-SA attribution; requests use TextExtracts, a descriptive
User-Agent, bounded results, caching, and timeouts. Difficulty rewriting creates
a new document, leaving the imported original intact.
Read-aloud settings store an ElevenLabs key and voice once in private workspace
state. The public graph returns configured status, never the key. Playback uses
the streaming text-to-speech endpoint for a selected passage, limited to 1,200
codepoints per request; it does not automatically narrate an entire book. The
network implementation is covered with mocked API responses; live paid audio
requires a configured key and voice.
Primary API references: [MediaWiki TextExtracts](https://www.mediawiki.org/wiki/Extension:TextExtracts),
[Wikimedia User-Agent policy](https://foundation.wikimedia.org/wiki/Policy:Wikimedia_Foundation_User-Agent_Policy),
and [ElevenLabs streaming text-to-speech](https://elevenlabs.io/docs/api-reference/text-to-speech/stream).
## Verification
`nix run .#language-reader-qualification` from the monorepo runs native graph
integration and real Chromium interaction tests. Package checks also run
processor/output validation, mocked network tests, canvas collision/Unicode
selection tests, and hover scheduling tests. The browser suite checks actual
CJK canvas metrics, pointer and keyboard copy, scrolling, bounded hover
admission/cache reuse, the visual editor, imports, and the embedded feature
conversation at desktop and narrow widths.
## Inline annotator configuration
Configuration belongs to annotator code and appears on its page in Reader settings.
`Processor.config` is a list of `ConfigField` controls, or a function returning that
list. Supported controls: `color`, `number`, `select`, `toggle`, `text`, with a key,
label, typed default, optional numeric bounds and select choices. The grammar
processor generates one color control per part of speech; names does the same for
entity types. Declare every option in code so the server can validate it.
`ReadingFeature.options` persists the JSON object. `configureOptions` checks
`expectedRevision`, validates options and increments `configRevision` atomically.
The UI edits a draft and saves all processor options together; appearance/reveal
controls save directly when the processor explicitly declares them. `addFeature` requires `options` (use `"{}"` for defaults) and optional `authoring` (use null for premades).
Concurrent edits reject cleanly. Changing display settings preserves raw cached results; only semantic inputs invalidate computation.
`Processor.prompt(config)` can include validated config in developer instructions.
Local processors receive config as their third argument.
The presentation `annotationDisplays` registry can return per-item `color`, `fontFamily`, or
`fontSize`, applied after strict model output validation. Model output stays the
four-field range/text/tag contract; rendering choices are owned by checked code.
Common options `fontFamily` and `fontSize` configure labels. Their style remains
`overhead`; `options.placement` chooses `"above"` (default) or `"below"`. A
`Processor.renders` hook can set `placement` independently. Both positions use
fixed source lanes, and display changes reuse semantic caches. Wide labels gain
faint alignment guides; hovering a label emphasizes its source without invoking
word-hover tooltips or processors. Different
fonts and sizes in the same annotation source stay on one row; each physical line
reserves its maximum ascent/descent and shares the resulting baseline.
The presentation renders declared fields automatically. For richer controls, add
a React component to `annotatorConfigViews[processor]` in
`LanguageReaderPresentation/src/annotator-config.tsx`; it receives `fields`,
`values`, `disabled`, and `change(key, value)`. This component edits the same draft
and validated options. Built annotators have no universal output-type editor.
`Processor.settings` explicitly opts into color, font, or timing controls;
`description` explains the behavior and `preset` defines a premade output.
The New annotator panel can reuse any installed processor type with its code-owned
controls, or describe a new type to the author. `LanguageReader.archiveType` takes
`{processor, archived}` and updates `archivedTypes`, hiding a type from the creation
library without changing its instances, implementation or caches. Restore it to
make it discoverable again. `ReadingFeature.remove` removes only an instance.
Creation choices are authoring instructions, not a universal runtime schema.
Set `addFeature.authoring` to the supplied `authoringJob` when registering a new
annotator; this links its original request and followups to that annotator.
`customize.feature` links a change request to an existing annotator.
`ReadingFeature.setEnabled` runs locally and preserves configuration revision
and cached annotations across toggles.
Do not bypass persistence or create per-word inference from a config component.
The French word-gloss and passage-translation processors declare `profile: "cerebras-light"`
to use GPT OSS 120B with low reasoning. Processor-owned profiles override the
workspace default and participate in cache identity. Cerebras must be connected.
The default inference profile is `quick-fast`: GPT-6 Luna without reasoning and
fast mode. `LanguageReader.inferenceProfile` is configurable through the graph and
captured for each request. Model-backed cache identity includes the effective
profile; local processors use `local`. Feature authoring remains on balanced.
Pending jobs mark the exact text segment with animated contours. This is
a rendering overlay, independent of layout, and respects reduced-motion settings.
Hover word feedback uses cached segmentation/layout immediately; heavyweight hover
work still waits for dwell and reuses durable results.
## Text processing and reading windows
`src/text-processing.ts` is the reusable source-indexed planner for words,
sentences, chunks and context. `ReadingDocument.plan` returns target ranges for
`kind: "focus"` or `"nearby"`; it never starts inference. The UI reports visible
and nearby source ranges, then uses the same admission/cache path for automatic
preparation, clicks, dwell hover and retries. Pages are the default view; the
Reading layout control also offers scrolling. Navigation persists mode and source
position independently of pixel layout. Selection and cache ranges always refer
to the original document, across pagination, font changes and reflow.
Defaults: focus targets at most 400 graphemes; context includes up to two sentences
before and after, capped at 1,600 graphemes. This also bounds unpunctuated paragraphs.
Automatic preparation uses stable chunks of at most 800 graphemes, preferring
sentence/word boundaries. Pages prepare the current page plus its neighbors;
scrolling prepares roughly one viewport above and below. Only two background
model jobs are admitted concurrently. Independent scopes from the same annotator
may use both slots; duplicate scopes reuse their existing requests. Unsent queued
scopes and admitted work both show processing outlines.
Moving away replaces unsent plans; admitted results finish and remain cached.
Local passes use the same chunks without model admission. Errors do not cause an
automatic retry loop. Focused interactions are independent of preparation slots.
Authors can override the defaults with `Processor.processing`, a partial policy
or `(config) => policy`. Available fields: `chunkChars`, `focusChars`,
`contextChars`, `beforeSentences`, `afterSentences`, `prefetchChunks`, and
`context: "nearby" | "document"`. For long-range references, a processor might
choose `{ context: "document", contextChars: 12000, chunkChars: 2000 }`.
Sizes have a generous 80,000-grapheme ceiling and radii a 20-unit ceiling.
These are author choices, not extra user settings. Large prompts/results still
obey transport and output bounds; choose batch outputs responsibly. Generation
rejects oversized targets so callers cannot accidentally bypass the planner.
`passageTranslation` demonstrates lazy article translation, producing per-sentence
sidebar notes in nearby chunks. Add it from Premade annotators. Adapt text creates
a linked lazy text variant for either translation or difficulty changes.
Hover feedback changes source ink immediately and never draws a word outline.
## Tooltip presentation
Features can use `style: "tooltip"` with `reveal: "hover"` or `"click"`.
Choose Tooltip in the annotation editor, then choose its reveal trigger. Existing
processors and generated/local content work unchanged; this is a presentation
choice. `always` is unsupported for tooltips. Hover inference retains dwell,
nearby preparation and cache behavior. Ready hover content opens immediately.
The source word remains highlighted by ink on hover, without changing layout.
`LanguageReaderPresentation/src/canvas/Tooltip.tsx` renders selectable React
content in a native manual popover (the browser top layer). It escapes canvas,
scroll-pane and component clipping while retaining component styles. Positioning
uses source glyph rectangles in viewport coordinates, flips above/below and clamps
to the viewport. Cards combine matching annotations and identify their sources.
Hover cards allow a brief pointer crossing to select/copy their text. This grace
period does not extend model hover intent. Click cards remain until dismissed or
the anchor leaves view. Escape, outside clicks and the close button dismiss them;
pagination, scroll and resize update or clear the anchor. No model work is owned
by the tooltip component and no HTML from model output is interpreted.
## Lazy text variants
ReadingDocument.rewrite creates a variant linked by canonical; it does not start
inference. adaptation stores immutable canonical ranges and completed outputs.
prepareVariant admits only the next section, bounded to 1400 graphemes, with
nearby context. Requests deduplicate and explicit retries are bounded to three.
Validated completions append in canonical order. Output length changes do not
change canonical section identities or invalidate ready prefix annotations.
The reader prepares ahead near the current page; ordinary annotators then work
on generated text. This path serves both difficulty adaptation and translation.
A failed section retains all completed text and offers an explicit retry. Closing
the UI does not discard an admitted request. Variants are separate documents and
inherit the source's reader, which can subsequently be changed.
## Embedded authoring
customize captures reader, requested configuration, and prose intent separately.
Jobs retain the reader link and a read-only configuration snapshot. Their threads
are collapsed beneath that reader's settings; opening one shows a React request
card rather than raw orchestration text, then ordinary follow-ups and questions.
ConversationTranscript separates rendering from its graph controller, and the
standard agent ThreadView also accepts renderItem for embedded request views.
Before follow-ups, the author must re-read current reader settings and feature
options; a request card records history and is not authoritative live state.
## Product language
Use concrete labels and direct copy. Avoid slogans, inspirational filler,
decorative all-caps branding, and boilerplate AI disclaimers. Explain empty states
with a useful next action. Caveats should identify a specific limitation briefly.
These directions also live in the default Workspace Agent prompt and harness.
## Semantic annotation caching
Raw model/local facts are cached separately from display preferences. The pure
cache.ts wrapper serializes processor identity, scope, generation and explicitly
selected semantic options. Source revision/range and model profile complete the
cache identity. Processor.cache.inputs opts into semantic fields; the default
selects non-color config fields unless computation:false. prompt(config) sees
only these semantic values. Changes to color, fonts, enabled state or display
filters must never admit another model request. Install custom filtering/styling
in LanguageReaderPresentation/src/annotation-display.ts, not model output.
Pinyin batches every word in stable 160-grapheme nearby chunks, with tone marks
and an estimated HSK level per token. The HSK slider filters cached facts locally.
Hover pronunciation uses the same computation identity, including sentence context,
so it does not request inference per word or duplicate the overhead pass. Two
background slots bound preparation; visible chunks precede neighboring chunks.
Context-sensitive annotation caches stay scoped to the canonical source/range;
only introduce bare-word reuse for a processor whose context-independence is
explicit. For ambiguous pronunciations/definitions retain context.
## Text library and generation
The library lists texts; readers are reusable configurations attached to them.
`ReadingDocument.archived` hides a text from the active collection without deleting
its source, variants, annotations or threads. Restoring it changes only that flag.
`LanguageReader.generateDocument` captures a reader and a bounded prose request,
creates a durable `ReadingJob` of kind `text`, and uses the quick-fast inference
agent. Only a validated `{title,text}` result creates and attaches the new text.
The UI can close while the request runs; completion survives independently.
`addFrenchReader` creates an independent reader with two click-only overhead
sources and a small French sample. `src/examples/french.ts` supplies per-word
contextual glosses for the selected sentence and a separate translation lane for
a few sentences. Focus planning bounds the source range and supplies neighboring
context. Clicking another passage replaces both visible scopes immediately.
Ready results remain cached; revisiting a passage reuses them. This scope-based
exclusivity belongs to click/focus annotations; ordinary word/sidebar annotations
continue to follow their source word. Hover tooltip dismissal clears only native
selection owned by the tooltip, so it does not intercept later canvas copying.
Model annotations echo `source` alongside `start`/`end`; it must equal the exact
original UTF-16 slice, including whitespace and punctuation. `text` remains the
rendered pronunciation, gloss or replacement. `validateAnnotations` collects all
indexed errors and strips the echo after validation. The job calls the shared
`AgentRequest.correct` operation for at most two same-thread corrections, staying
pending until valid or exhausted. Invalid results never enter the display cache.
Local processors still receive range/content checks without an echo. An author
may document a code-owned `verifySource:false` exception; `annotationSchemaFor`
then supplies the corresponding schema. This affects semantic cache identity.
The feature author uses `balanced-fast` (Sol, medium reasoning, fast mode).
Inference-based features must be tried on actual reader text or representative
sample text. Start with `cerebras-light` when connected; try Luna fast, then Sol
fast if the output needs a stronger model. Check returned ranges and content,
not just request completion. These are authoring choices, not reader settings.
## Reader extensions and vocabulary
Sidebar panels and word/selection context-menu actions are ordinary React reader
extensions, separate from annotators. `src/extensions.tsx` in the presentation
provides a shared `ReaderContext` and `ReaderAction`; `reader-extensions.tsx` is the
installation point. Actions can inspect the exact original range or cached annotation
tags. Context menus use the browser top layer and keyboard focus/dismissal. Drag
selection does not generate click annotations. Panels can include arbitrary UI.
`ReadingVocabulary` is a named graph list, shared through each reader’s optional
`vocabulary` edge (otherwise the library default). Its standalone editor supports
search, add/remove and rename. The SDK `ObjectPicker` presents checked paginated
lists and explicit selection, creation, preview and open-in-new-tab. Existing words
remain in their original storage; `ensureVocabulary` attaches unlisted entries to
the first default list through public contracts. Cloning a reader shares its list.
Settings show the current text alongside configuration. This preview projects
cached facts through current appearance/display options, never initiates inference.
Author new data models simply, and refactor to richer domain entities when needed
while preserving user values via supported transformations. Independently scoped UI
work can use bounded Codex subagents; join/check before activation.