← All projects

Literature Review Tool

VikeReactTypeScriptFastifySQLiteLLMs

A solo-built, full-stack platform for running systematic literature reviews — the kind of evidence-synthesis work researchers normally do by hand across a spreadsheet, a reference manager, and months of screening. It ingests RIS bibliographic exports, uses LLMs to help triage which studies belong in a review, and carries that review all the way through PRISMA reporting. Currently in active development.

Why this exists

A proper systematic review means screening hundreds or thousands of abstracts against inclusion/exclusion criteria, resolving duplicates across databases, tracking two independent reviewers’ decisions, and producing a PRISMA flow diagram at the end that can withstand scrutiny. It’s slow, repetitive, and exactly the kind of first-pass triage an LLM can meaningfully speed up — provided every decision it makes is explainable and reproducible, not a black box. That constraint shaped almost every technical decision below.

Ingesting and deduplicating records

RIS files are parsed with @citation-js/core and its RIS plugin, normalizing each record into a consistent shape (title, abstract, authors, year, journal, DOI, keywords) regardless of which database exported it. Every record gets a content hash: if a DOI is present it’s normalized and MD5-hashed; if not, the hash falls back to a normalized title+abstract composite — either way, records pulled from different databases for the same study collapse to the same fingerprint.

Exact-DOI duplicates surface automatically. For everything else there’s a fuzzy-match endpoint that scores candidate pairs on Levenshtein-based title similarity plus author-list overlap, so near-duplicates with slightly different formatting still get flagged for review. Merging two records isn’t just a delete — it reassigns screenings, full-text screenings, data extractions, notes, risk-of-bias assessments, tags, and any attached PDF from the duplicate onto the surviving record across every table before removing it, so nothing silently disappears from the review.

AI-assisted screening

Screening is built around one core idea: a criterion-by-criterion structured judgment, not a single yes/no. For each record, every inclusion and exclusion criterion (or a PICO-derived default when a review hasn’t defined custom criteria yet) is evaluated in one combined LLM call, which returns a JSON array with one verdict per criterion — yes / no / unsure — each with a rationale and a verbatim quoted snippet from the abstract. The record’s overall decision is then derived deterministically from those per-criterion verdicts, not asked for directly: any triggered exclusion criterion excludes the record; all inclusion criteria met includes it; anything else falls to unsure for a human to resolve.

The prompting deliberately differs by stage. At abstract screening, the model is instructed to default to unsure whenever the abstract doesn’t explicitly confirm or deny a criterion — missing information is never grounds for exclusion, since an abstract alone often can’t rule a study out. At full-text screening, the instruction flips: the model has the whole paper, so it’s told to make the definitive call the text supports rather than hedge. A handful of worked examples (on an unrelated topic, to avoid biasing toward any one review’s subject matter) calibrate three failure modes seen in practice: missing an exception clause inside an exclusion criterion, being needlessly conservative when an abstract is actually clear, and overusing unsure when a criterion simply isn’t addressed either way.

Every decision carries provenance: prompt hash, exact prompt text, temperature/top-p/seed (fixed at 0/1/42 for reproducibility), the git commit the decision was made under, model, and token usage. An “AI log” view lets you pull up the exact prompt behind any historical decision — useful both for debugging and for justifying a review’s methodology later.

Talking to the model, reliably

The screening layer supports Ollama (local or Ollama Cloud), Anthropic, and OpenAI behind one interface — a model string like anthropic:claude-opus-4-8 or openai:gpt-5.5 routes straight to that provider’s SDK, while anything else goes through Ollama, local or cloud depending on whether OLLAMA_HOST is set. Every screener-model dropdown in the UI gets all three providers for free from that one dispatch point.

Ollama Cloud calls get exponential backoff with jitter on rate limits and timeouts, and the abort timer stays armed until the response body is fully read — not just until headers arrive — since a slow-streaming reasoning model can otherwise hang past the nominal timeout with no protection. LLMs also don’t reliably emit valid JSON at scale: control characters end up inside quoted strings, one object in a fifty-record batch gets malformed. Response parsing runs through three fallback tiers — strict JSON parse, then control-character repair, then per-object regex recovery — so a single bad object degrades to an unsure for that one record instead of failing the whole batch.

Beyond screening

Screening is the core, but the tool follows a review through to publication-ready output: a live PRISMA flow diagram computed directly from the screening tables, a PRISMA checklist, GRADE certainty-of-evidence assessment, a risk-of-bias tool, forest plots, narrative synthesis and evidence-table views, and exports to DOCX/XLSX for the write-up itself. Inter-rater reliability is built in too — Cohen’s Kappa is computed pairwise between any two AI screeners and between human and AI decisions, with agreement percentages and per-category distributions, so you can actually see how much a given model agrees with a human reviewer rather than assuming it.

Stack

Vike + React 19 + TypeScript on the frontend, Fastify on the backend, SQLite (better-sqlite3) for storage, WebSockets for live screening progress, and Playwright for full-text PDF retrieval. LLM access spans the Ollama, Anthropic, and OpenAI SDKs behind the routing layer described above.