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:

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:

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, Wikimedia User-Agent policy, and ElevenLabs streaming text-to-speech.

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.

S
Description
Quixos Workspace Template resource
Readme
257 KiB
Languages
TypeScript 98%
Nix 2%