Skip to content

AI Completion, Agent & Chat

This area covers 27 features. Watch the walkthrough, then use the reference below — each feature links to the exact moment it appears (▶).

Who this is for: Reviewer / translation consultant. Each feature below lists the role/permission it requires.

Features

Single-cell AI completion (sparkle button) ▶ 00:00

As a translator, I want to fill a single empty cell using AI, so that I can quickly generate a first draft for difficult segments.

How it works. User clicks the sparkle button on an untranslated cell. The app fetches few-shot examples from validated cells, builds a prompt with project rules/brief/context, sends to configured LLM endpoint, streams response, and commits the generated text as an unvalidated cell. If LLM is unavailable or user not configured, button is disabled with tooltip.

WhoReviewer / translation consultant PermissionsValidate/unvalidate: reviewer(300)
Key files

src/lib/completion/completion-service.ts:482-533
src/hooks/useCompletion.ts:137-250
src/components/ProjectWorkspace.tsx:~completeSingle

Batch AI completion (translate all) ▶ 00:00

As a translator, I want to draft all untranslated cells in my current file/selection with AI, so that I can rapidly bootstrap translations for bulk review.

How it works. User clicks 'Run AI completions' → acknowledgement dialog appears (confirmation + checkbox that they understand changes are attributed to their account) → on confirm, cells translate sequentially. A progress banner shows done/total + stop button. Cells auto-commit as unvalidated. If batch exceeds 30 cells, it chunks into sequential batches with continuity via prior batch context. Stop button aborts in-flight request + halts new cells. Partial completions remain committed.

WhoReviewer / translation consultant PermissionsValidate/unvalidate: reviewer(300)
Key files

src/lib/completion/batch-completion.ts
src/components/CompletionBulkProgressBanner.tsx
src/hooks/useCompletion.ts:250-350

Paragraph-unit AI completion (discourse-aware) ▶ 00:00

As a translator, I want the AI to translate a paragraph as a coherent unit with discourse context, so that pronouns, tense, and connectives flow naturally across sentence boundaries.

How it works. When a paragraph is selected (≥2 cells), the translator can trigger completion. Instead of translating each cell in isolation, the LLM receives: the whole paragraph's source cells (tagged with stable <c id> tags), the committed preceding target context (discourse window left), following source (right), and few-shot examples. The LLM responds with matching <c id> tags; response is parsed back to per-cell text and committed. Missing/extra/duplicate tags surface as errors.

WhoProject lead (source steward) PermissionsSource content edits: project_lead(500)
Key files

src/lib/completion/completion-service.ts:328-440
src/lib/completion/paragraph-protocol.ts
src/hooks/useCompletion.ts:~paragraphCompletion

AI completion settings (project-level provider config) ▶ 00:14

As a project admin, I want to configure which LLM provider the project uses (Frontier hosted vs. custom OpenAI-compatible endpoint), so that my team translates with the provider that best fits our needs.

How it works. Project settings → LLM section expands. User can: (1) select provider (Frontier / Custom); (2) if custom: enter endpoint URL + API key + model name; (3) customize system prompt (or leave blank to use default); (4) adjust temperature/max-tokens. Settings are saved to project record. Per-device override (user Settings) always takes precedence over project config. Valid custom endpoints are normalized (trailing slashes stripped, /v1 or /chat/completions appended as needed). Endpoint is tested by fetching available models.

WhoTranslator (contributor) PermissionsApply AI proposal target.cell.commit: contributor(400); cell.validate proposal: reviewer(300)
Key files

src/hooks/useCompletionSettings.ts
src/lib/completion/completion-service.ts:450-470
src/components/ProjectSettings.tsx:~LLM section

Per-device LLM provider override (user Settings) ▶ 00:00

As a translator, I want to use my own API key on this device for all projects, overriding the project's default provider, so that I can control my LLM spend locally.

How it works. User Settings → enters endpoint + model + API key. All projects on this device now use this override config instead of their project-level settings. The override is stored in localStorage as JSON. Both completion-service.complete() and useCompletion honor this precedence. Override can be cleared by blanking the settings.

WhoTranslator (contributor) PermissionsApply AI proposal target.cell.commit: contributor(400); cell.validate proposal: reviewer(300)
Key files

src/lib/store/user-provider-override.ts
src/lib/completion/completion-service.ts:486-495
src/hooks/useCompletion.ts:118-121

Few-shot example selection (validated pairs + search-retrieved) ▶ 00:00

As the system, I want to compose the LLM prompt with high-quality examples from the project, so that the model imitates the team's terminology and style.

How it works. For each completion: (1) collectValidatedPairs extracts all cells with status='validated' and non-empty content; (2) if a source query is provided, pairs are ranked by token overlap (cheap relevance proxy); top K returned (default 20); (3) separate search retrieval fetches additional examples from the branching-search corpus; (4) validated pairs are prepended (strongest signal), search results follow; (5) examples with empty source or target are filtered out. Optional: useOnlyValidatedExamples flag skips search entirely, using only validated cells.

WhoTranslator (contributor) PermissionsApply AI proposal target.cell.commit: contributor(400); cell.validate proposal: reviewer(300)
Key files

src/lib/completion/completion-service.ts:15-88
src/hooks/useCompletion.ts:140-150

Translation brief injection (project intent) ▶ 00:00

As a project admin, I want the LLM to know the project's purpose and audience (brief), so that translations follow the project's intended register and style.

How it works. Project brief's L1 summary (if non-empty) is injected into the system prompt AFTER the base instructions and BEFORE rules. The block is labeled 'Translation brief (the project's purpose and standards — follow it):'. Empty or null summaries add nothing; the LLM sees no brief block.

WhoTranslator (contributor) PermissionsApply AI proposal target.cell.commit: contributor(400); cell.validate proposal: reviewer(300)
Key files

src/lib/completion/completion-service.ts:90-100
src/hooks/useCompletion.ts:~briefSummary

Translation rules injection (project terminology & constraints) ▶ 00:00

As a project admin, I want the LLM to follow our project's rules (terminology must-haves, forbidden phrases), so that translations are consistent with project decisions.

How it works. Project rules (source-requires-target, target-forbids, source-target-match types; enabled only) are rendered as a 'Project terminology and style rules (MUST follow):' block in the system prompt. Each rule becomes a line: 'When X appears, use Y' or 'Do NOT use X'. Only enabled rules are included. Disabled rules are skipped. Builtin algorithmic rules are not injected (no useful prompt text). Empty rules list → no block added.

WhoTranslator (contributor) PermissionsApply AI proposal target.cell.commit: contributor(400); cell.validate proposal: reviewer(300)
Key files

src/lib/completion/completion-service.ts:69-88
src/hooks/useCompletion.ts:~rules

Discourse context window (preceding + following) ▶ 00:00

As the system, I want to give the LLM preceding & following context so that it can resolve pronouns, maintain tense, and continue narrative flow.

How it works. For each completion: (1) preceding context: ≤N most recently committed cells from the document (TARGET language for discourse flow); if no target yet committed, source fallback shown as 'Preceding (source, not yet translated):' (no Source/Translation pair to avoid blank translation echo). (2) Following context: ≤M upcoming source cells (read-only, labeled so model doesn't translate them). Context cells with empty source/target are filtered. Preceding rendered just before live source; following after examples. Context window size controlled by draftContextSettings.

WhoProject lead (source steward) PermissionsSource content edits: project_lead(500)
Key files

src/lib/completion/draft-context.ts
src/lib/completion/completion-service.ts:202-208, 405-430
src/hooks/useCompletion.ts:~draftContext

Example format toggle (source-and-target vs. target-only) ▶ 00:00

As a project admin, I want to switch the completion mode to target-only so that low-resource projects see only reference translations, not source-target pairs.

How it works. Project settings → LLM section → fewShotExampleFormat dropdown (default 'source-and-target'). When set to 'target-only': examples in prompt render as 'Target: ...' only (no 'Source: ...' lines); system prompt adds a note explaining these are reference translations. Validated pairs still require non-empty targets; sources are omitted. Effect applies to single-cell, batch, and paragraph modes.

WhoTranslator (contributor) PermissionsApply AI proposal target.cell.commit: contributor(400); cell.validate proposal: reviewer(300)
Key files

src/lib/completion/completion-service.ts:154-182, 248-290
src/hooks/useCompletion.ts:~exampleFormat

Chat (multi-turn AI copilot) ▶ 00:00

As a translator, I want to ask the AI questions about source meaning, translation strategy, or the current passage, so that I can resolve ambiguities without leaving the editor.

How it works. Workspace → chat panel (right sheet). User types a question → presses Enter or clicks Send. Message is added to history with timestamp + id. If cell context is pinned, current cell (source + target) is included in system prompt. LLM replies (streamed, markdown-rendered). User can: retry (re-send failed), dismiss (remove failed), regenerate (remove last assistant reply + resend prior turn), copy, or insert-into-cell (if cell is focused; prompts if cell already has text). History persists in localStorage (per-project, ≤200 messages). Clear history removes all messages after confirmation.

WhoTranslator (contributor) PermissionsApply AI proposal target.cell.commit: contributor(400); cell.validate proposal: reviewer(300)
Key files

src/hooks/useChat.ts
src/lib/completion/chat-service.ts
src/components/ChatPanel.tsx
src/components/chat/ChatMessageList.tsx

Chat cell context toggle & pin ▶ 00:00

As a translator, I want to include or exclude the current cell from chat context, so that the AI knows what I'm working on when relevant but doesn't get distracted otherwise.

How it works. Chat header: cell-context pin toggle shows current cell (source/target), or 'No cell context'. Clicking toggles includeCellContext flag (persisted per project in localStorage). When ON and a cell is focused, the cell's source + target are injected into chat's system prompt as context. When OFF or no cell is focused, no context. File name shown when includeCellContext=true but no specific cell is focused (agent dock only).

WhoTranslator (contributor) PermissionsApply AI proposal target.cell.commit: contributor(400); cell.validate proposal: reviewer(300)
Key files

src/components/chat/ChatContextPin.tsx
src/hooks/useChat.ts:117-125
src/lib/completion/chat-service.ts:81-89

Chat message actions (copy, insert, regenerate) ▶ 00:00

As a translator, I want to copy AI replies, insert them into cells, or regenerate if the response wasn't good, so that I can use AI suggestions efficiently.

How it works. Hover over an assistant message (not streaming): toolbar appears with Copy, Insert-into-cell (if cell focused), Regenerate (on last assistant message only). Copy copies markdown to clipboard + shows 'Copied' feedback. Insert-into-cell converts markdown to plain text + commits to cell (with overwrite confirmation if cell already has text). Regenerate: removes the last assistant reply, re-sends the conversation up to the previous user turn (re-runs LLM with same history).

WhoTranslator (contributor) PermissionsApply AI proposal target.cell.commit: contributor(400); cell.validate proposal: reviewer(300)
Key files

src/components/chat/ChatMessageBubble.tsx:33-176
src/components/chat/ChatMessageList.tsx:82-91, 127

Chat error recovery (retry + dismiss) ▶ 00:00

As a translator, I want to retry failed chat sends or dismiss failed messages, so that transient errors don't clutter my conversation.

How it works. When a chat message fails to send: message stays in list with status='failed', muted styling, and a red error line showing the human-readable error message. User can click Retry (re-sends with same context, keeps message in place) or Dismiss (removes from list). Details disclosure shows raw error text. Error messages are mapped: 401/sign-in → 'Sign in to use AI chat'; 402/429/limit → 'AI limit reached'; network → 'Connection lost'; else → 'Something went wrong.'

WhoTranslator (contributor) PermissionsApply AI proposal target.cell.commit: contributor(400); cell.validate proposal: reviewer(300)
Key files

src/hooks/useChat.ts:63-75
src/components/chat/ChatMessageBubble.tsx:112-139

Translation agent (multi-step AI reasoning) ▶ 00:00

As a translator, I want to ask the agent to draft, check, or explain translations in context, and review its proposals before applying, so that I can benefit from AI reasoning without ceding control.

How it works. Agent dock mode (chat → agent toggle or from suggested action). User types a prompt → agent receives: message history (≤10 turns), project/cell context (if pinned), translator profile. Agent streams: assistant text (rendered as ChatMarkdown), code steps (sql/emit/docs/aquifer with collapsible results), proposals (cell edits, comments, validations), Aquifer publish proposals. Responses are purely informational until user clicks Apply on a proposal card. Apply enqueues real events (same outbox path as editor) authored by current user, with ai_suggestion/agent_run_id provenance fields.

WhoAny user (personal preference) PermissionsAny authenticated user — scoped to own account
Key files

src/components/agent/AgentDockView.tsx
src/lib/agent/agent-client.ts
src/lib/agent/protocol.ts
src/components/agent/AgentRunView.tsx

Agent proposal review & apply (cell edits, comments, validation) ▶ 00:07

As a translator, I want to review the agent's proposed changes (what text is being changed, rule violations, comments), then selectively apply them to my project.

How it works. Each proposal card shows: summary ('Draft 12 cells in MRK 4'), event count badge, per-event rows with: kind (Edit/Comment/Validate icon), reference (canonicalRef badge), before→after text (truncatable), rule violation badges (if any, from client-side lint re-run). Each proposal has Apply/Discard buttons. Apply enqueues events through existing outbox (author = current user; events carry ai_suggestion + agent_run_id from server). Discard marks card as discarded (grayed out, 'Discarded:' prefix). Unknown event kinds show raw JSON + disabled Apply ('not supported yet').

WhoReviewer / translation consultant PermissionsValidate/unvalidate: reviewer(300)
Key files

src/components/agent/ProposalCard.tsx
src/lib/agent/apply.ts
src/lib/agent/protocol.ts:26-42

Agent proposal rule lint (client-side validation) ▶ 00:00

As the system, I want to check agent proposals against project rules before apply, so that the translator sees rule violations in the review card without applying first.

How it works. ProposalCard re-runs project rules on every proposed cell text at render time (not at agent stage time). If a proposed text would violate a rule, a red badge appears on that event showing the infraction message. Uses the same rule engine (checkRulesForCell) as the editor health pass. Lint is deterministic — same rules + text always produces same infractions.

WhoTranslator (contributor) PermissionsApply AI proposal target.cell.commit: contributor(400); cell.validate proposal: reviewer(300)
Key files

src/components/agent/ProposalCard.tsx:212-222
src/lib/rules/rule-engine.ts

Agent proposal role-gated apply (permission floor) ▶ 00:00

As the system, I want to prevent users without sufficient permissions from applying certain agent changes, so that only the right roles can make certain edits.

How it works. Apply button is disabled with an inline reason if: (1) any proposed event kind is not supported yet (canApply returns { allowed: false, reason: 'Contains event kinds...' }); (2) user's role.level is below the floor for any event kind (e.g., reviewer can't validate/commit). canApply mirrors server role-floors.ts checks. Reason is shown in gray text below Apply button. Tooltip on disabled Apply shows the reason.

WhoReviewer / translation consultant PermissionsValidate/unvalidate: reviewer(300)
Key files

src/lib/agent/role-floors.ts
src/components/agent/ProposalCard.tsx:225-235

Aquifer publish proposal (Bible wiki answers) ▶ 00:00

As the agent, I want to suggest publishing an exegetical answer to the Bible Aquifer wiki when it answers a question, so that the team can share knowledge.

How it works. When agent generates an exegetical answer, an AquiferProposalCard appears. Card shows: question (heading), status badge (answered/undetermined), answer text (truncatable), citations (links to sources). Apply button POSTs to aquifer API (not outbox) with question/answer/status/citations. On success, published wiki URL is shown with link. Discard hides the proposal (grayed, 'Discarded:' prefix). Apply disabled if user not signed in.

WhoTranslator (contributor) PermissionsApply AI proposal target.cell.commit: contributor(400); cell.validate proposal: reviewer(300)
Key files

src/components/agent/AquiferProposalCard.tsx
src/lib/aquifer/client.ts:aquiferPublishAnswer

Backtranslation (literal reverse translation) ▶ 00:00

As a translator, I want a word-for-word back-translation of my target text to verify it conveys the intended source meaning without drift.

How it works. On a cell with translation: user can request backtranslation. LLM translates target text back to source language, preserving exact words/structure (even if unnatural). Prompt includes: system instructions (preserve structure, show translation literally), terminology hints (derived from project concepts matching source text), examples (if any pre-existing backtranslations in corpus). Result is shown in cell UI (probably as a collapsible section or linked view). No auto-commit; review only.

WhoProject lead (source steward) PermissionsSource content edits: project_lead(500)
Key files

src/lib/completion/backtranslation-service.ts
src/lib/terminology/types.ts

Frontier health monitoring (service availability) ▶ 00:00

As the system, I want to detect when Frontier AI is unavailable, so that I can disable the complete button and show the user a clear message.

How it works. useFrontierHealth hook polls Frontier's health endpoint (~5s interval). Returns { available: bool }. When unavailable, complete button shows 'Service temporarily unavailable' tooltip and is disabled (isAvailable=false). Note: isAvailable is separate from isConfigured (user may be configured but service is down). No preamble dialogs on transient failure — just a clear disable + message.

WhoTranslator (contributor) PermissionsApply AI proposal target.cell.commit: contributor(400); cell.validate proposal: reviewer(300)
Key files

src/lib/completion/frontier-health.ts
src/hooks/useCompletion.ts:122-135

LLM error handling (402 credits, 401 auth, network) ▶ 00:00

As the system, I want to surface LLM errors to the user with clear messages, so they can understand what went wrong and how to fix it.

How it works. LLM errors caught and mapped: 402 (Frontier credits exhausted) → 'Frontier AI limit reached: ...'; 401 (auth failed) → 'Sign in to use [service]'; 429 (rate limit) → 'AI limit reached'; network/fetch errors → 'Connection lost'; else → 'Completion failed: 500 ...' (raw status shown). Chat errors use mapChatError for human-readable copy + raw detail (shown in disclosure). Single-cell errors stored in errors map per cell. Batch errors stop the batch + mark run as failed.

WhoTranslator (contributor) PermissionsApply AI proposal target.cell.commit: contributor(400); cell.validate proposal: reviewer(300)
Key files

src/lib/completion/completion-service.ts:518-532
src/hooks/useChat.ts:63-75

Streaming response rendering (on-chunk callback) ▶ 00:00

As a translator, I want to see the LLM response appear word-by-word as it's generated, so that I can read along and stop if it's going wrong.

How it works. complete(options) takes optional onChunk callback. As SSE frames arrive from LLM, each delta is accumulated and onChunk(accumulatedText) is called. UI re-renders on each chunk (streaming preview). When stream ends (or stops), the accumulated text is what gets committed/returned. If stream is aborted (user clicks Stop), the AbortController.signal fires, read loop exits, partial text is discarded, and DOMException('AbortError') is thrown.

WhoTranslator (contributor) PermissionsApply AI proposal target.cell.commit: contributor(400); cell.validate proposal: reviewer(300)
Key files

src/lib/completion/completion-service.ts:527-606
src/hooks/useCompletion.ts:~onChunk

SSE frame parsing (streaming LLM responses) ▶ 00:00

As the system, I want to parse streaming LLM responses robustly, so that errors don't crash the translation.

How it works. LLM SSE stream is consumed line-by-line. Lines are buffered across arbitrary read() boundaries so mid-frame breaks don't lose data. Each 'data: {...}' line is JSON-parsed. Content delta extracted to onChunk. If parsing fails, a console.warn logs the bad frame (first 200 chars) and the loop continues. Stream errors (e.g., 'error': '...') are caught + thrown (bubble up to caller as Error). '[DONE]' or done flag ends stream gracefully.

WhoTranslator (contributor) PermissionsApply AI proposal target.cell.commit: contributor(400); cell.validate proposal: reviewer(300)
Key files

src/lib/completion/completion-service.ts:547-606
src/lib/agent/agent-client.ts:45-89

Abort/cancellation (streaming stop button) ▶ 00:00

As a translator, I want to stop an in-flight completion or chat message, so that I can abort if it's taking too long or producing nonsense.

How it works. While streaming, UI shows a Stop button (replaces Send). Clicking Stop: (1) for single-cell/chat: aborts the fetch via AbortController signal; (2) for batch: sets cancel flag + aborts current cell's fetch; next cell doesn't start (checked at loop iteration). In-flight streams exit immediately via cancelled reader. Partial text is discarded (not committed). User sees 'Stopped' or 'aborted' status message.

WhoTranslator (contributor) PermissionsApply AI proposal target.cell.commit: contributor(400); cell.validate proposal: reviewer(300)
Key files

src/lib/completion/batch-completion.ts:86-100
src/lib/completion/completion-service.ts:587-589
src/hooks/useCompletion.ts:~stop

Streaming to preview (immediate user feedback) ▶ 00:00

As a translator, I want to see the draft text appear in real-time in the cell, so that I can watch the AI work and read the emerging translation.

How it works. While AI is generating, cell shows preview text (not yet committed). Each onChunk callback updates the preview map in state. Cell renders preview instead of committed translation (visual diff: lighter styling, maybe a loading spinner). Once stream ends/succeeds, preview is cleared + real commit happens (unvalidated cell appears).

WhoReviewer / translation consultant PermissionsValidate/unvalidate: reviewer(300)
Key files

src/hooks/useCompletion.ts:109
src/components/EditorTable.tsx:~previews rendering

AI-completion acknowledgement dialog ▶ 00:00

As the system, I want the user to explicitly acknowledge that batch AI completions will be attributed to their account, so that AI contributions are transparent.

How it works. Before running 'Translate all' (batch completion), a ConfirmActionDialog appears with: title 'Run completions', description 'Generate translations for the next N untranslated cells...', unchecked checkbox 'I understand this change will be attributed to my account', Cancel button, and disabled 'Run AI completions' button. Only after checking the box does the button enable. Clicking 'Run' dismisses dialog and starts batch. Clicking Cancel closes without action.

WhoTranslator (contributor) PermissionsApply AI proposal target.cell.commit: contributor(400); cell.validate proposal: reviewer(300)
Key files

e2e/specs/editor/ai-completion-dialog.smoke.spec.ts
src/components/ConfirmActionDialog.tsx