Your sources. Your knowledge. Your browser.
Live demo: research-bench.nachtigall.dev
A browser research workbench built for the Everlast Consulting test assignment: upload documents, ask questions with source references, inspect the cited passage, and turn a notebook into a summary, FAQ, study guide or a cited two-host podcast.
See assignment and scope for the original brief, chosen boundaries and a dated NotebookLM feature mapping.
Documents, chunking, embeddings, indexing and semantic search run locally. Generation uses your own Mistral La Plateforme key by default, with OpenRouter as an explicit alternative. There is no application backend.
Prototype: source references and local heuristics do not prove that generated
claims follow from the documents. Review generated content before relying on it.
The live demo can run a different version from this candidate; its build identity
is available at /_app/version.json.
Tested toolchain: Node.js 26.11.1, pnpm 11.28.5, Rust/Cargo 1.98.1,
Git, rustup and Python 3 (pinned Pdfium ZIP extraction). The package declares Node.js >=22.17; that lower bound is not a
promise that every version has been tested. Rust is pinned in rust-toolchain.toml.
The first setup needs network access to download public dependencies and pinned
model/runtime assets. A private repository requires authorized GitHub access
until the maintainer explicitly makes it public.
git clone https://github.com/VikingOwl91/research-bench.git
cd research-bench
pnpm install --frozen-lockfile
rustup target add wasm32-unknown-unknown
pnpm assets:prepare
pnpm wasm:build
pnpm dev
Open the URL printed by Vite. In Settings, select the API connection and
enter its key. Keys stay in sessionStorage, survive reload in that tab, and
are never included in the build, IndexedDB or repository. Browser session or
crash recovery can restore sessionStorage after closing a tab. Use Remove
saved key in Settings to explicitly remove it. Any script running on this
origin can access a saved key; the app therefore renders untrusted content as
text and uses a restrictive Content Security Policy. Local search works without
any key.
wasm:build installs the pinned wasm-bindgen CLI into the project's
.cache/tools when necessary. Native ONNX installation scripts are deliberately
disabled: inference uses ONNX Runtime Web in a browser Worker. TypeScript 6 is
pinned because SvelteKit currently requires its JavaScript compiler API.
Open Studio → Podcast. English is selected by default; German (experimental) is available explicitly. Optionally use Test voices before creating an episode. The short voice test uses fictional text and no documents; it makes two billable speech requests. Podcast creation makes one script request and up to 24 sequential speech requests. No request is retried automatically. The same selected connection and its session key are used for both writing and speech. An episode targets roughly 3–5 minutes; smaller source sets may produce shorter conversations. Both hosts explain, develop ideas and ask questions; their roles are tendencies rather than a fixed interview. Previously saved German episodes remain available when selecting German.
The pinned speech model is voxtral-mini-tts-2603 for Mistral direct and
mistralai/voxtral-mini-tts-2603 for OpenRouter. The presets are
gb_oliver_curious and gb_jane_neutral. Direct Mistral resolves their actual
voice IDs through the EU voices endpoint before generating a script. These are
British voice presets: German pronunciation and transitions require a listening
test; no native German quality is promised and no substitute voice is chosen.
The first owner listening test accepted English but found a strong accent in
German. The interface labels German speech as experimental.
Mistral returns lossless WAV. OpenRouter currently uses MP3 because its documented raw PCM option does not establish a reliable sample layout/rate. Each input is decoded locally and the contributions plus short pauses are encoded once into a mono 24 kHz, 64 kbit/s MP3 in a dedicated Worker. OpenRouter therefore involves an additional lossy encoding step. Files are never assembled by concatenating MP3 bytes. Timing follows decoded contribution lengths, including encoder priming/padding; it is contribution-level, not word alignment. There is no guarantee that separately spoken turns have identical tempo/intonation. New episodes use local per-contribution RMS gain adjustment, capped at 3× with a 0.92 peak ceiling; silence remains unchanged. This is a bounded gain adjustment, not a broadcast loudness certification.
The recommended player setting preserves pitch and uses 1.3× for German, and 1.2× for Oliver / 1× for Jane in English, following the first listening feedback. Manual speed selection overrides it. The MP3 download retains the original speech tempo; changing playback speed does not rewrite the downloaded file.
Completed speech segments are saved after each contribution. On cancellation or failure, Resume explicitly requests only the missing contributions using the frozen voices, language and provider. A replacement key for that provider is allowed; switching provider requires selecting the original provider to resume. Changed sources require a new episode. Removing/replacing a key, switching provider or changing notebooks aborts active processing. Already issued requests can still incur charges. An existing finished episode stays available until its replacement is successfully committed.
Web Locks prevent simultaneous renders of the same notebook/language across tabs. Closing a tab releases its lock; explicit resume claims a fresh persisted render identity. Every write also checks the notebook revision, data-wipe epoch, config signature and render identity. Browsers without Web Locks can play saved episodes but cannot render/resume. Script metadata, final MP3 and replacement cleanup are committed in one short IndexedDB transaction; network requests, decoding and encoding happen outside it. Audio blobs live in a separate store and load lazily, not in application snapshots. Deleting an episode, notebook or all local data also removes its audio. Old episodes remain labeled stale after source changes, and removed source references become unavailable.
The renderer enforces ten minutes of decoded audio, 128 MiB of accounted audio buffers and a 30-second encoding watchdog. These bounds are not measurements of the browser's entire process heap. The recorded fixture benchmark and the open real-voice acceptance gates are in the verification report.
System, Light and Dark appearance modes apply without reloading. System follows the browser preference; an explicit appearance choice is stored locally.
The interface defaults to English, regardless of the browser language. Use the header language selector to switch the entire interface to German. This choice is saved locally; switching still works in memory if preference storage is blocked. Dialogs, progress, errors, source controls and accessibility labels update live. Switching language does not interrupt indexing, generation or audio playback.
Chat and Studio text generation use the interface language captured when the operation starts, unless the question explicitly requests another language. Existing notebook names, source text and generated content are preserved. Podcast speech has its own language selector: English stays the default in either interface language, and German remains experimental because native German voices are unavailable. Source viewing displays extracted text with PDF page anchors, not overlays in the original PDF.
flowchart TD
subgraph Browser[Browser]
Upload[PDF / MD / TXT] --> Parser[Rust + Pdfium WASM: canonical pages]
Parser --> Chunks[Rust: chunking and UTF-16 source spans]
Chunks --> Embed[Transformers.js + multilingual-e5-small: local embeddings]
Embed --> IDB[IndexedDB: notebooks, sources, chunks and vectors]
Question[Question] --> Query[Local query embedding]
Query --> Retrieval[Rust: cosine search]
IDB --> Retrieval
Retrieval --> Context[Bounded context with citation IDs]
Context --> Adapter[Explicitly selected API adapter]
Answer[Validated structured answer] --> Cite[Citation → source text highlight]
Answer --> Saved[Local chat and Studio artifacts]
Script[Validated two-host script] --> Transcript[Cited contribution transcript]
Audio[Local decode + gain + MP3 Worker] --> AudioStore[IndexedDB: audio stored separately]
end
Adapter --> Mistral[Mistral EU regional endpoint]
Adapter --> Router[OpenRouter global endpoint]
Mistral --> Answer
Router --> Answer
Mistral --> Script
Router --> Script
Script --> Speech[Same selected connection: speech API]
Speech --> Audio
The Rust crate provides strict UTF-8 decoding, canonical per-page text, fixed
Unicode-scalar windows with overlap, exact half-open UTF-16 spans for browser
highlighting, and cosine retrieval over a row-major Float32Array. Window
defaults are 1,000 characters with 200 overlap, confined to each page.
pdfium-render binds a separately shipped, pinned Pdfium WASM module. The JS
bridge only initializes modules and transports results; PDF parsing remains in
the Rust/Pdfium path. Pdfium calls and ONNX inference run off the main thread.
Cancellation terminates the Worker, including synchronous parsing/inference.
There is one primary PDF parser; alternative parsers are not silently selected.
The default is quantized multilingual-e5-small, pinned to the ONNX revision
in the asset manifest. Queries use query:, passages use passage:, followed
by mean pooling and normalization. The 512-token limit includes prefixes and
special tokens; oversized windows are split again without losing source spans.
Long current questions are rejected rather than silently truncated. Previous
questions are included only within the remaining token budget.
The first import downloads approximately 160 MB of model, tokenizer and needed
WASM runtime assets from this app's origin. The complete deployment asset
set is larger because it includes runtime variants and license notices. Browser
caches can be evicted; their persistence is not guaranteed. Model and tokenizer
files, including their notices, use a content-addressed
/models/<asset-set-sha256>/multilingual-e5-small/ namespace with immutable HTTP
caching. Runtime/PDFium and legacy unversioned paths require revalidation. Remote model loading
is disabled. E5 was chosen for multilingual retrieval, browser-ready ONNX
weights and a bounded download, rather than model novelty.
Notebook and source IDs exist from the first import. A source becomes searchable only when its pages, chunks and vectors have been committed atomically. Parser/model/pipeline versions accompany the index; incompatible indices request reindexing. Each operation captures notebook/source revision and API configuration. Revision checks and abort-aware transactions prevent canceled or outdated jobs from saving results into a changed or deleted notebook.
Chat sends at most four conversation pairs from the current source revision. Source changes start a new active conversation context; old visible answers and Studio items remain labeled as referring to an older source state. Deleted citations are unavailable. API switches abort active generation; there is no automatic cross-provider or cross-region fallback.
Chat uses the top six local retrieval results. Both adapters request a JSON schema and normalize into one client-side validated answer type. Only citation IDs from the selected context can become clickable references. Request and validation bounds derive from the same schema. A section may cite up to the number of distinct transmitted passages; there is no fixed twelve-reference cap. Size violations produce diagnostics without revealing response contents. Validation proves source-ID membership and exact navigation, not whether a model's factual claim is truly entailed by the cited passage. Citations identify the supplied source; ResearchBench does not fact-check it against outside information. Documents are untrusted prompt data. A malicious source can still steer generated statements or include misleading/phishing text despite the system prompt and schema checks. Such output is displayed as escaped text, without automatic links, HTML or tool execution.
Before transmission, a sentence-level EN/DE/FR sanitizer replaces recognized instruction-like source sentences. Original documents and the retrieval index are unchanged. Before new Chat/Studio output is returned for display or persistence, a deterministic support gate compares selected numeric literals and paired quotations with the sanitized excerpts. Unmatched specifics produce a removal notice. Podcast turns receive the same check before new TTS, and paused drafts are rechecked before resume. These are lexical heuristics, not semantic proof. They can miss false conclusions using existing numbers, invented names, unsupported prose, single-significant-digit values and answer/script titles. They can also remove supported wording, including ambiguous number formatting or benign quoted instructions. Historical saved output is not retrospectively verified. See the current security limitations.
Studio generators use the whole canonical source text up to 24,000 characters, without repeated overlap. Larger notebooks use a round-robin selection of spread-out, non-overlapping passages across sources until the budget is exhausted. Storing 50 sources does not mean all 50 participate in an answer. The UI shows selected sources, text coverage and the selection limitation. This is not advertised as a complete summary of a large notebook.
| Connection | Endpoint | Default model |
|---|---|---|
| Mistral direct (default) | https://api.eu.mistral.ai/v1/chat/completions | mistral-small-2603 |
| OpenRouter (alternative) | https://openrouter.ai/api/v1/chat/completions | mistralai/mistral-small-2603 |
Speech uses /v1/audio/speech on the same provider origins. Direct voices are
listed at https://api.eu.mistral.ai/v2/audio/voices; OpenRouter voice support is
checked in its public model list. The speech adapter requests OpenRouter
provider: { zdr: true, data_collection: "deny" }. require_parameters is a
chat-routing field and is not sent to speech. Unsupported privacy/voice/model
combinations fail visibly instead of changing provider, region or privacy flags.
See Mistral speech
and OpenRouter speech.
Each connection has its own session key. Only the selected connection receives a
generation request. OpenRouter's adapter requests data_collection: "deny",
zdr: true and require_parameters: true. Those fields are never sent to
Mistral. Missing eligible endpoints cause a visible error; privacy requirements
are not relaxed automatically. Requests use no tools, web-search plugins, OCR
APIs, file-upload APIs or external embedding APIs.
Invalid keys, insufficient credits/access, unavailable regional models, rate limits, invalid JSON/citations, timeouts and cancellation have separate user-facing outcomes. Raw provider error bodies are not displayed or logged.
Use a funded key and the paid, pinned model for a live demo. A free-model quota or model switch is not part of the reliable demo path. No credits are bought or keys created by the setup scripts.
Authoritative sources (reviewed October 2026): Mistral regional inference, Mistral training controls, Mistral ZDR, OpenRouter routing, OpenRouter ZDR, OpenRouter Business and DPA.
The DSK orientation on RAG (October 2025) explains why RAG does not automatically settle data protection questions. Local storage, bounded disclosure, transparent provenance and deletion support a privacy-conscious design; lawful production use still depends on the actual documents, purpose, contracts and provider configuration.
The differentiator is the combination of zero server setup for the visitor, Rust/WASM provenance, multilingual local retrieval, explicit provider choice, bounded disclosure and cited learning artifacts. This repository does not claim to be the only product with that combination or that Rust alone improves retrieval.
pnpm verify
pnpm assets:prepare -- --verify
PLAYWRIGHT_BROWSERS_PATH=.cache/ms-playwright pnpm exec playwright install chromium firefox
pnpm test:e2e
pnpm test:security
Tests cover Unicode spans, retrieval math, schema/citation validation, provider request isolation, IndexedDB revisions and cancellation. The browser suite uses the actual Rust/Pdfium Worker and local E5 model, original DE/EN fixture PDFs, reload/notebook isolation, exact highlights, all three generators, bounded coverage, unsafe source rendering and deletion. API responses use route mocks with test-only keys; actual account entitlements and live generation require a manual test using the key entered in the UI.
The i18n tests cover English defaults, persisted/blocked preference storage, live dialogs and errors, language changes during indexing and generation, preserved source highlights and podcast playback, and the independently selected experimental German speech option in both browsers.
The reproducible checks, release gates and coverage limits are in the verification report.
Build from a clean committed checkout so the application and its source offer share the exact Git commit identity.
pnpm build
pnpm source:prepare --expected-head "$(git rev-parse HEAD)"
pnpm preview
Publish the entire build/ directory, including models, runtimes and notices,
to an HTTPS static host with SPA fallback to index.html. WASM must be served as
application/wasm, JavaScript modules with a JavaScript MIME type. Missing assets
under the app/model/runtime directories and dotfiles must return 404; extensionless
paths return the SPA shell, including mistyped paths. The fallback is not an
authorization or existence check. No runtime
environment secrets or proxy service are needed. CI verifies builds/tests and
does not push, deploy or publish a release.
Production uses a hashed SvelteKit CSP meta placed directly after the charset, before scripts, preloads and resource links. It authorizes the exact bootstrap hash, same-origin scripts/styles/fonts and same-origin module Workers. The HTTP CSP adds framing protection and resource restrictions; all applicable policies are enforced together. Neither policy can relax the other.
The effective policy denies objects, frames, base URLs and form navigation.
Connections are limited to this origin, https://api.eu.mistral.ai and
https://openrouter.ai; images may use same-origin/data URLs and audio may use
same-origin/blob URLs. Inline script attributes, arbitrary inline scripts,
JavaScript evaluation and blob Workers are denied. The only inline style
attribute exception is the verified SvelteKit accessibility-announcer hash.
Worker responses carry their own policy allowing same-origin imports and WASM
compilation, with same-origin connections and no nested Workers. Document WASM
compilation remains disabled while all application WASM runs in Workers.
Required HTTP headers include X-Frame-Options: DENY,
Strict-Transport-Security: max-age=31536000 on HTTPS,
X-Content-Type-Options: nosniff and Referrer-Policy: no-referrer.
HSTS does not include subdomains or preload. Versioned model assets and hashed
app assets use immutable caching; entry HTML and fixed runtime paths revalidate.
The hosting checker verifies the combined policies, bootstrap hashes, worker
policies and exact artifact bytes. Browser tests must also prove permitted
features work and forbidden actions are blocked under the delivered policies.
There is no external CSP-reporting endpoint. robots.txt and security.txt
are not included in this release.
pnpm release:prepare creates a content-addressed build snapshot;
pnpm release:verify <directory> checks its complete file inventory and hashes.
For public-origin checks, use pnpm hosting:check --url <https-origin> --release <directory>
and set RESEARCHBENCH_HOSTING_URL to that origin before running
pnpm test:hosting. API calls in the hosting browser suite are mocked.
The v0.1.0 candidate is prepared locally. GitHub remains private; push, merge, publication, signed tagging, a GitHub Release and deployment each require explicit maintainer authorization. See the release checklist. CI success is separate from live-provider acceptance and deployment verification.
The original assignment's 6–8 minute OBS/unlisted-YouTube demonstration outline and NIST inputs remain in the demo script and public corpus. Prepare and check all generated outputs before recording. No automatic retries or real-key calls are required by the test suite.
See THIRD_PARTY_NOTICES.md for redistribution notices.
Copyright (c) 2026 Christian Nachtigall and ResearchBench contributors. SPDX-License-Identifier: AGPL-3.0-or-later
ResearchBench's own application code and documentation are free software: you can redistribute them and/or modify them under the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. They are distributed without any warranty, including merchantability or fitness for a particular purpose. See LICENSE for the complete terms. This declaration applies only to code and documentation for which the project holds the necessary rights.
Existing MIT releases, including v0.0.1 and v0.0.2, retain their MIT terms;
the license change does not rewrite history or revoke those permissions.
Third-party packages, runtime/model assets, the public NIST demo inputs and
CC0 test fixtures keep their own terms. See
THIRD_PARTY_NOTICES.md.
The deployed footer offers an anonymous same-origin Source archive and
License for the build's actual commit, even while GitHub is private.
pnpm source:prepare --expected-head <full-commit-SHA> creates the archive from the clean committed tree; it
contains project/ and separate provenance/rebuild instructions, without Git
history, generated binaries, dependency directories or user data. The archive
contains the application source and pinned build/asset instructions; upstream
packages remain available from their pinned sources, and the LGPL encoder's
matching source is also served separately. Rebuilding an extracted archive
starts a new local Git history and receives a new build identity. It does not
fake the original commit or promise identical binary hashes. Compressed archive
bytes can vary with the compression toolchain; compare complete extracted member
paths, modes, hashes and provenance before interpreting a compressed hash mismatch
as a source change. See release verification. Retain published
source archives and license files across later activation and rollback so old
open tabs continue to offer their matching source.
Your sources. Your knowledge. Your browser.
Live demo: research-bench.nachtigall.dev
A browser research workbench built for the Everlast Consulting test assignment: upload documents, ask questions with source references, inspect the cited passage, and turn a notebook into a summary, FAQ, study guide or a cited two-host podcast.
See assignment and scope for the original brief, chosen boundaries and a dated NotebookLM feature mapping.
Documents, chunking, embeddings, indexing and semantic search run locally. Generation uses your own Mistral La Plateforme key by default, with OpenRouter as an explicit alternative. There is no application backend.
Prototype: source references and local heuristics do not prove that generated
claims follow from the documents. Review generated content before relying on it.
The live demo can run a different version from this candidate; its build identity
is available at /_app/version.json.
Tested toolchain: Node.js 26.11.1, pnpm 11.28.5, Rust/Cargo 1.98.1,
Git, rustup and Python 3 (pinned Pdfium ZIP extraction). The package declares Node.js >=22.17; that lower bound is not a
promise that every version has been tested. Rust is pinned in rust-toolchain.toml.
The first setup needs network access to download public dependencies and pinned
model/runtime assets. A private repository requires authorized GitHub access
until the maintainer explicitly makes it public.
git clone https://github.com/VikingOwl91/research-bench.git
cd research-bench
pnpm install --frozen-lockfile
rustup target add wasm32-unknown-unknown
pnpm assets:prepare
pnpm wasm:build
pnpm dev
Open the URL printed by Vite. In Settings, select the API connection and
enter its key. Keys stay in sessionStorage, survive reload in that tab, and
are never included in the build, IndexedDB or repository. Browser session or
crash recovery can restore sessionStorage after closing a tab. Use Remove
saved key in Settings to explicitly remove it. Any script running on this
origin can access a saved key; the app therefore renders untrusted content as
text and uses a restrictive Content Security Policy. Local search works without
any key.
wasm:build installs the pinned wasm-bindgen CLI into the project's
.cache/tools when necessary. Native ONNX installation scripts are deliberately
disabled: inference uses ONNX Runtime Web in a browser Worker. TypeScript 6 is
pinned because SvelteKit currently requires its JavaScript compiler API.
Open Studio → Podcast. English is selected by default; German (experimental) is available explicitly. Optionally use Test voices before creating an episode. The short voice test uses fictional text and no documents; it makes two billable speech requests. Podcast creation makes one script request and up to 24 sequential speech requests. No request is retried automatically. The same selected connection and its session key are used for both writing and speech. An episode targets roughly 3–5 minutes; smaller source sets may produce shorter conversations. Both hosts explain, develop ideas and ask questions; their roles are tendencies rather than a fixed interview. Previously saved German episodes remain available when selecting German.
The pinned speech model is voxtral-mini-tts-2603 for Mistral direct and
mistralai/voxtral-mini-tts-2603 for OpenRouter. The presets are
gb_oliver_curious and gb_jane_neutral. Direct Mistral resolves their actual
voice IDs through the EU voices endpoint before generating a script. These are
British voice presets: German pronunciation and transitions require a listening
test; no native German quality is promised and no substitute voice is chosen.
The first owner listening test accepted English but found a strong accent in
German. The interface labels German speech as experimental.
Mistral returns lossless WAV. OpenRouter currently uses MP3 because its documented raw PCM option does not establish a reliable sample layout/rate. Each input is decoded locally and the contributions plus short pauses are encoded once into a mono 24 kHz, 64 kbit/s MP3 in a dedicated Worker. OpenRouter therefore involves an additional lossy encoding step. Files are never assembled by concatenating MP3 bytes. Timing follows decoded contribution lengths, including encoder priming/padding; it is contribution-level, not word alignment. There is no guarantee that separately spoken turns have identical tempo/intonation. New episodes use local per-contribution RMS gain adjustment, capped at 3× with a 0.92 peak ceiling; silence remains unchanged. This is a bounded gain adjustment, not a broadcast loudness certification.
The recommended player setting preserves pitch and uses 1.3× for German, and 1.2× for Oliver / 1× for Jane in English, following the first listening feedback. Manual speed selection overrides it. The MP3 download retains the original speech tempo; changing playback speed does not rewrite the downloaded file.
Completed speech segments are saved after each contribution. On cancellation or failure, Resume explicitly requests only the missing contributions using the frozen voices, language and provider. A replacement key for that provider is allowed; switching provider requires selecting the original provider to resume. Changed sources require a new episode. Removing/replacing a key, switching provider or changing notebooks aborts active processing. Already issued requests can still incur charges. An existing finished episode stays available until its replacement is successfully committed.
Web Locks prevent simultaneous renders of the same notebook/language across tabs. Closing a tab releases its lock; explicit resume claims a fresh persisted render identity. Every write also checks the notebook revision, data-wipe epoch, config signature and render identity. Browsers without Web Locks can play saved episodes but cannot render/resume. Script metadata, final MP3 and replacement cleanup are committed in one short IndexedDB transaction; network requests, decoding and encoding happen outside it. Audio blobs live in a separate store and load lazily, not in application snapshots. Deleting an episode, notebook or all local data also removes its audio. Old episodes remain labeled stale after source changes, and removed source references become unavailable.
The renderer enforces ten minutes of decoded audio, 128 MiB of accounted audio buffers and a 30-second encoding watchdog. These bounds are not measurements of the browser's entire process heap. The recorded fixture benchmark and the open real-voice acceptance gates are in the verification report.
System, Light and Dark appearance modes apply without reloading. System follows the browser preference; an explicit appearance choice is stored locally.
The interface defaults to English, regardless of the browser language. Use the header language selector to switch the entire interface to German. This choice is saved locally; switching still works in memory if preference storage is blocked. Dialogs, progress, errors, source controls and accessibility labels update live. Switching language does not interrupt indexing, generation or audio playback.
Chat and Studio text generation use the interface language captured when the operation starts, unless the question explicitly requests another language. Existing notebook names, source text and generated content are preserved. Podcast speech has its own language selector: English stays the default in either interface language, and German remains experimental because native German voices are unavailable. Source viewing displays extracted text with PDF page anchors, not overlays in the original PDF.
flowchart TD
subgraph Browser[Browser]
Upload[PDF / MD / TXT] --> Parser[Rust + Pdfium WASM: canonical pages]
Parser --> Chunks[Rust: chunking and UTF-16 source spans]
Chunks --> Embed[Transformers.js + multilingual-e5-small: local embeddings]
Embed --> IDB[IndexedDB: notebooks, sources, chunks and vectors]
Question[Question] --> Query[Local query embedding]
Query --> Retrieval[Rust: cosine search]
IDB --> Retrieval
Retrieval --> Context[Bounded context with citation IDs]
Context --> Adapter[Explicitly selected API adapter]
Answer[Validated structured answer] --> Cite[Citation → source text highlight]
Answer --> Saved[Local chat and Studio artifacts]
Script[Validated two-host script] --> Transcript[Cited contribution transcript]
Audio[Local decode + gain + MP3 Worker] --> AudioStore[IndexedDB: audio stored separately]
end
Adapter --> Mistral[Mistral EU regional endpoint]
Adapter --> Router[OpenRouter global endpoint]
Mistral --> Answer
Router --> Answer
Mistral --> Script
Router --> Script
Script --> Speech[Same selected connection: speech API]
Speech --> Audio
The Rust crate provides strict UTF-8 decoding, canonical per-page text, fixed
Unicode-scalar windows with overlap, exact half-open UTF-16 spans for browser
highlighting, and cosine retrieval over a row-major Float32Array. Window
defaults are 1,000 characters with 200 overlap, confined to each page.
pdfium-render binds a separately shipped, pinned Pdfium WASM module. The JS
bridge only initializes modules and transports results; PDF parsing remains in
the Rust/Pdfium path. Pdfium calls and ONNX inference run off the main thread.
Cancellation terminates the Worker, including synchronous parsing/inference.
There is one primary PDF parser; alternative parsers are not silently selected.
The default is quantized multilingual-e5-small, pinned to the ONNX revision
in the asset manifest. Queries use query:, passages use passage:, followed
by mean pooling and normalization. The 512-token limit includes prefixes and
special tokens; oversized windows are split again without losing source spans.
Long current questions are rejected rather than silently truncated. Previous
questions are included only within the remaining token budget.
The first import downloads approximately 160 MB of model, tokenizer and needed
WASM runtime assets from this app's origin. The complete deployment asset
set is larger because it includes runtime variants and license notices. Browser
caches can be evicted; their persistence is not guaranteed. Model and tokenizer
files, including their notices, use a content-addressed
/models/<asset-set-sha256>/multilingual-e5-small/ namespace with immutable HTTP
caching. Runtime/PDFium and legacy unversioned paths require revalidation. Remote model loading
is disabled. E5 was chosen for multilingual retrieval, browser-ready ONNX
weights and a bounded download, rather than model novelty.
Notebook and source IDs exist from the first import. A source becomes searchable only when its pages, chunks and vectors have been committed atomically. Parser/model/pipeline versions accompany the index; incompatible indices request reindexing. Each operation captures notebook/source revision and API configuration. Revision checks and abort-aware transactions prevent canceled or outdated jobs from saving results into a changed or deleted notebook.
Chat sends at most four conversation pairs from the current source revision. Source changes start a new active conversation context; old visible answers and Studio items remain labeled as referring to an older source state. Deleted citations are unavailable. API switches abort active generation; there is no automatic cross-provider or cross-region fallback.
Chat uses the top six local retrieval results. Both adapters request a JSON schema and normalize into one client-side validated answer type. Only citation IDs from the selected context can become clickable references. Request and validation bounds derive from the same schema. A section may cite up to the number of distinct transmitted passages; there is no fixed twelve-reference cap. Size violations produce diagnostics without revealing response contents. Validation proves source-ID membership and exact navigation, not whether a model's factual claim is truly entailed by the cited passage. Citations identify the supplied source; ResearchBench does not fact-check it against outside information. Documents are untrusted prompt data. A malicious source can still steer generated statements or include misleading/phishing text despite the system prompt and schema checks. Such output is displayed as escaped text, without automatic links, HTML or tool execution.
Before transmission, a sentence-level EN/DE/FR sanitizer replaces recognized instruction-like source sentences. Original documents and the retrieval index are unchanged. Before new Chat/Studio output is returned for display or persistence, a deterministic support gate compares selected numeric literals and paired quotations with the sanitized excerpts. Unmatched specifics produce a removal notice. Podcast turns receive the same check before new TTS, and paused drafts are rechecked before resume. These are lexical heuristics, not semantic proof. They can miss false conclusions using existing numbers, invented names, unsupported prose, single-significant-digit values and answer/script titles. They can also remove supported wording, including ambiguous number formatting or benign quoted instructions. Historical saved output is not retrospectively verified. See the current security limitations.
Studio generators use the whole canonical source text up to 24,000 characters, without repeated overlap. Larger notebooks use a round-robin selection of spread-out, non-overlapping passages across sources until the budget is exhausted. Storing 50 sources does not mean all 50 participate in an answer. The UI shows selected sources, text coverage and the selection limitation. This is not advertised as a complete summary of a large notebook.
| Connection | Endpoint | Default model |
|---|---|---|
| Mistral direct (default) | https://api.eu.mistral.ai/v1/chat/completions | mistral-small-2603 |
| OpenRouter (alternative) | https://openrouter.ai/api/v1/chat/completions | mistralai/mistral-small-2603 |
Speech uses /v1/audio/speech on the same provider origins. Direct voices are
listed at https://api.eu.mistral.ai/v2/audio/voices; OpenRouter voice support is
checked in its public model list. The speech adapter requests OpenRouter
provider: { zdr: true, data_collection: "deny" }. require_parameters is a
chat-routing field and is not sent to speech. Unsupported privacy/voice/model
combinations fail visibly instead of changing provider, region or privacy flags.
See Mistral speech
and OpenRouter speech.
Each connection has its own session key. Only the selected connection receives a
generation request. OpenRouter's adapter requests data_collection: "deny",
zdr: true and require_parameters: true. Those fields are never sent to
Mistral. Missing eligible endpoints cause a visible error; privacy requirements
are not relaxed automatically. Requests use no tools, web-search plugins, OCR
APIs, file-upload APIs or external embedding APIs.
Invalid keys, insufficient credits/access, unavailable regional models, rate limits, invalid JSON/citations, timeouts and cancellation have separate user-facing outcomes. Raw provider error bodies are not displayed or logged.
Use a funded key and the paid, pinned model for a live demo. A free-model quota or model switch is not part of the reliable demo path. No credits are bought or keys created by the setup scripts.
Authoritative sources (reviewed October 2026): Mistral regional inference, Mistral training controls, Mistral ZDR, OpenRouter routing, OpenRouter ZDR, OpenRouter Business and DPA.
The DSK orientation on RAG (October 2025) explains why RAG does not automatically settle data protection questions. Local storage, bounded disclosure, transparent provenance and deletion support a privacy-conscious design; lawful production use still depends on the actual documents, purpose, contracts and provider configuration.
The differentiator is the combination of zero server setup for the visitor, Rust/WASM provenance, multilingual local retrieval, explicit provider choice, bounded disclosure and cited learning artifacts. This repository does not claim to be the only product with that combination or that Rust alone improves retrieval.
pnpm verify
pnpm assets:prepare -- --verify
PLAYWRIGHT_BROWSERS_PATH=.cache/ms-playwright pnpm exec playwright install chromium firefox
pnpm test:e2e
pnpm test:security
Tests cover Unicode spans, retrieval math, schema/citation validation, provider request isolation, IndexedDB revisions and cancellation. The browser suite uses the actual Rust/Pdfium Worker and local E5 model, original DE/EN fixture PDFs, reload/notebook isolation, exact highlights, all three generators, bounded coverage, unsafe source rendering and deletion. API responses use route mocks with test-only keys; actual account entitlements and live generation require a manual test using the key entered in the UI.
The i18n tests cover English defaults, persisted/blocked preference storage, live dialogs and errors, language changes during indexing and generation, preserved source highlights and podcast playback, and the independently selected experimental German speech option in both browsers.
The reproducible checks, release gates and coverage limits are in the verification report.
Build from a clean committed checkout so the application and its source offer share the exact Git commit identity.
pnpm build
pnpm source:prepare --expected-head "$(git rev-parse HEAD)"
pnpm preview
Publish the entire build/ directory, including models, runtimes and notices,
to an HTTPS static host with SPA fallback to index.html. WASM must be served as
application/wasm, JavaScript modules with a JavaScript MIME type. Missing assets
under the app/model/runtime directories and dotfiles must return 404; extensionless
paths return the SPA shell, including mistyped paths. The fallback is not an
authorization or existence check. No runtime
environment secrets or proxy service are needed. CI verifies builds/tests and
does not push, deploy or publish a release.
Production uses a hashed SvelteKit CSP meta placed directly after the charset, before scripts, preloads and resource links. It authorizes the exact bootstrap hash, same-origin scripts/styles/fonts and same-origin module Workers. The HTTP CSP adds framing protection and resource restrictions; all applicable policies are enforced together. Neither policy can relax the other.
The effective policy denies objects, frames, base URLs and form navigation.
Connections are limited to this origin, https://api.eu.mistral.ai and
https://openrouter.ai; images may use same-origin/data URLs and audio may use
same-origin/blob URLs. Inline script attributes, arbitrary inline scripts,
JavaScript evaluation and blob Workers are denied. The only inline style
attribute exception is the verified SvelteKit accessibility-announcer hash.
Worker responses carry their own policy allowing same-origin imports and WASM
compilation, with same-origin connections and no nested Workers. Document WASM
compilation remains disabled while all application WASM runs in Workers.
Required HTTP headers include X-Frame-Options: DENY,
Strict-Transport-Security: max-age=31536000 on HTTPS,
X-Content-Type-Options: nosniff and Referrer-Policy: no-referrer.
HSTS does not include subdomains or preload. Versioned model assets and hashed
app assets use immutable caching; entry HTML and fixed runtime paths revalidate.
The hosting checker verifies the combined policies, bootstrap hashes, worker
policies and exact artifact bytes. Browser tests must also prove permitted
features work and forbidden actions are blocked under the delivered policies.
There is no external CSP-reporting endpoint. robots.txt and security.txt
are not included in this release.
pnpm release:prepare creates a content-addressed build snapshot;
pnpm release:verify <directory> checks its complete file inventory and hashes.
For public-origin checks, use pnpm hosting:check --url <https-origin> --release <directory>
and set RESEARCHBENCH_HOSTING_URL to that origin before running
pnpm test:hosting. API calls in the hosting browser suite are mocked.
The v0.1.0 candidate is prepared locally. GitHub remains private; push, merge, publication, signed tagging, a GitHub Release and deployment each require explicit maintainer authorization. See the release checklist. CI success is separate from live-provider acceptance and deployment verification.
The original assignment's 6–8 minute OBS/unlisted-YouTube demonstration outline and NIST inputs remain in the demo script and public corpus. Prepare and check all generated outputs before recording. No automatic retries or real-key calls are required by the test suite.
See THIRD_PARTY_NOTICES.md for redistribution notices.
Copyright (c) 2026 Christian Nachtigall and ResearchBench contributors. SPDX-License-Identifier: AGPL-3.0-or-later
ResearchBench's own application code and documentation are free software: you can redistribute them and/or modify them under the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. They are distributed without any warranty, including merchantability or fitness for a particular purpose. See LICENSE for the complete terms. This declaration applies only to code and documentation for which the project holds the necessary rights.
Existing MIT releases, including v0.0.1 and v0.0.2, retain their MIT terms;
the license change does not rewrite history or revoke those permissions.
Third-party packages, runtime/model assets, the public NIST demo inputs and
CC0 test fixtures keep their own terms. See
THIRD_PARTY_NOTICES.md.
The deployed footer offers an anonymous same-origin Source archive and
License for the build's actual commit, even while GitHub is private.
pnpm source:prepare --expected-head <full-commit-SHA> creates the archive from the clean committed tree; it
contains project/ and separate provenance/rebuild instructions, without Git
history, generated binaries, dependency directories or user data. The archive
contains the application source and pinned build/asset instructions; upstream
packages remain available from their pinned sources, and the LGPL encoder's
matching source is also served separately. Rebuilding an extracted archive
starts a new local Git history and receives a new build identity. It does not
fake the original commit or promise identical binary hashes. Compressed archive
bytes can vary with the compression toolchain; compare complete extracted member
paths, modes, hashes and provenance before interpreting a compressed hash mismatch
as a source change. See release verification. Retain published
source archives and license files across later activation and rollback so old
open tabs continue to offer their matching source.