Mount the review library
Connect source units, findings and native text selection to your own application.
On this page
A framework-independent JavaScript library for reviewing text on a local web page. Angular, plain HTML and same-origin iframe hosts use the same controller. It draws supplied analysis findings as solid underlines and human feedback as dotted underlines, while native text selection supplies new feedback anchors. The host supplies the analysis; source editing is outside the library API.
Build and consume#
From the Voice Studio workspace:
npm run build -w @voice/review
npm test -w @voice/reviewThe package exports ESM and TypeScript declarations through @voice/review.
dist/voice-review.js is a standalone browser bundle exposing window.VoiceReview;
it includes no Angular dependency or runtime package dependency. A host can copy
that file into its local development assets. The Studio serves it at
/library/voice-review.js; see examples/library-embed/index.html in the repo.
TypeScript and JavaScript IntelliSense#
The public ESM API carries declaration files and JSDoc for completion, callback
inference and hover help. Import types such as TextUnit, Finding,
SourceSpan and VoiceReviewController directly from @voice/review.
Plain JavaScript can use // @ts-check and JSDoc import('@voice/review')
types. Checked examples ship in examples/:
TypeScript, JavaScript,
and classic-script global.
For a normal browser script, use
/// <reference types="@voice/review/standalone" /> to add editor types;
load voice-review.js separately with a classic <script> tag. The reference
does not load runtime code. Use the main entry for ESM imports.
Run npm run test:types -w @voice/review to verify the declarations and editor
metadata. Typed Library integration contains
complete setup examples and describes the consumer checks.
Mount on existing HTML#
Assign explicit text-unit identifiers in development markup. Units should be
non-overlapping blocks or spans. Their concatenated DOM text must match the
corresponding TextUnit.text exactly, including whitespace. The adapter skips
script, style, template, hidden, aria-hidden="true" and data-voice-exclude regions.
Use data-voice-exclude for visible inline code that the text adapter excludes.
<main id="preview">
<p data-voice-unit="intro">Eine <strong>präzise</strong> Aussage.</p>
</main>import { getUnitText, mountVoiceReview } from '@voice/review';
const root = document.querySelector('#preview');
const text = getUnitText(root.querySelector('[data-voice-unit="intro"]'));
const controller = mountVoiceReview({
root,
units: [{
id: 'intro', text, kind: 'paragraph', language: 'de',
// For this isolated browser-only example these are text coordinates.
// A source adapter must supply the actual file range before source edits.
sourceSpan: { start: 0, end: text.length, encoding: 'utf16' }
}],
findings: [{
id: 'finding-1', ruleId: 'example.precision', category: 'wording',
severity: 'info', message: 'Was bedeutet präzise hier?',
explanation: 'Beispielbefund für die Einbindung, keine automatische Analyse.',
quote: 'präzise', unitId: 'intro', start: 5, end: 12,
suggestion: null, engine: 'example'
}],
feedback: [],
onSelect: selection => showFeedbackForm(selection),
onFindingSelect: finding => showFinding(finding),
onFeedbackSelect: feedback => showFeedback(feedback)
});
// After new analysis or a saved note:
controller.update({ findings: latestFindings, feedback: savedFeedback });
controller.selectFinding('finding-1');
console.log(controller.getDiagnostics());
// On route changes, component destruction or iframe replacement:
controller.dispose();showFeedbackForm, showFinding, showFeedback, latestFindings and
savedFeedback above are integration callbacks/data supplied by the host.
An already-mounted root is disposed automatically before a second mount, so
repeated initialization does not duplicate handlers or underline layers.
For the standalone bundle, load <script src="/library/voice-review.js"></script>
and call VoiceReview.mountVoiceReview(...) with the same options. No build
framework is required on the consuming page.
Durable feedback through the local backend#
The host pairs with the local Studio once and supplies the resulting token.
The client sends source-version and review-revision preconditions unchanged.
Saved feedback is written by the backend to project metadata sidecars; it is
not stored by this package in localStorage.
import { createReviewClient, ReviewApiError } from '@voice/review';
const api = createReviewClient({
baseUrl: '/api',
token: () => currentPairingToken
});
let detail = await api.getDocument(projectId, documentId);
/**
* @param {import('@voice/review').SelectionTarget} selection
* @param {string} comment
* @param {string} category
* @returns {Readonly<import('@voice/review').FeedbackInput>}
*/
function prepareFeedback(selection, comment, category) {
return Object.freeze({
...selection, comment, category,
expectedVersion: detail.version,
expectedReviewRevision: detail.reviewRevision,
requestId: crypto.randomUUID()
});
}
/** @param {Readonly<import('@voice/review').FeedbackInput>} input */
async function persistFeedback(input) {
try {
detail = await api.saveFeedback(projectId, documentId, input);
controller.update({ units: detail.units, findings: detail.findings, feedback: detail.feedback });
} catch (error) {
if (error instanceof ReviewApiError && error.status === 409) {
// Keep the user's draft, reload the document and ask them to review the
// anchor against the new source before submitting a new request.
showConflict(error.details);
return;
}
throw error;
}
}Call prepareFeedback once for a new or edited note, retain the returned payload,
then pass it to persistFeedback. After an uncertain network failure, retry
persistFeedback with that same payload, including its request ID and version
preconditions. After a 409 conflict, reload and review the anchor before preparing
a new payload. Pairing is performed by the host at POST /api/session/pair;
do not put tokens in page URLs, source code or reports.
The backend has no cross-origin API access. Host the preview on the Studio origin,
or configure the consuming app's local development proxy.
Browser and source coordinates#
All review offsets are UTF-16 code units, zero-based and half-open. This is
the coordinate system used by JavaScript string slices and browser text ranges.
codePointOffsetToUtf16(text, offset) explicitly converts offsets from a
Unicode-code-point analysis engine. Do not pass Voice Lint core spans directly
without converting their declared encoding.
createTextRange(element, start, end, expectedText) maps offsets across inline
markup without wrapping or replacing content. The optional expectedText
rejects stale text. selectionToTarget(root, units, selection) returns
{ unitId, quote, start, end } for a selection within one mapped unit; a selection
across multiple units or across excluded inline code is rejected rather than
silently shortened. Selections on either side of code use the correct offsets.
createTextRanges(...) returns per-text-node ranges for drawing across inline
markup without accidentally marking excluded content.
The library validates both unit text and exact quoted substrings before drawing
marks. getDiagnostics() reports missing/mismatched units and stale anchors.
Resolved notes and notes needing reattachment are not shown as current marks.
Overlapping findings and human notes receive separate underline lanes.
Category colors are structure blue, claims orange, wording purple and meta ochre.
The layer uses range geometry and pointer events pass through it. Clicking text selects its most precise annotation; native
links, buttons and editable controls retain their normal interaction. Supply an
accessible findings list in the host for keyboard navigation and annotations
inside links. selectFinding(id) highlights and scrolls to a selected finding.
For an iframe, mount after its load event using
root: iframe.contentDocument.body, and dispose on reload. Its document must
be same-origin. The Studio's imported preview uses a sandbox without scripts.
Arbitrary remote cross-origin pages and closed shadow roots cannot be inspected
by this adapter; they need an explicit integration/import route. A site's CSP
must permit the library asset and its inline annotation styles in development.
Live development websites#
Use the exported connectVoiceStudio({studioOrigin}) bridge to review a running
application in Studio while preserving its real CSS, JavaScript and native
routing. The target website explicitly integrates the library and retains its
own origin. See LIVE-BRIDGE.md for the exact origin/session
protocol, source mapping, iframe requirements and limitations.
Git and source reference
Source for this page: packages/review/README.md. Content hash and Git revision: Build record.
Additional references in the repository:
packages/review/examples/