diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml
new file mode 100644
index 0000000..2360bcc
--- /dev/null
+++ b/.github/workflows/pages.yml
@@ -0,0 +1,38 @@
+name: pages
+
+on:
+ push:
+ branches: [main]
+ paths: ["groundcheck/**", "site/**", ".github/workflows/pages.yml"]
+ workflow_dispatch:
+
+permissions:
+ contents: read
+ pages: write
+ id-token: write
+
+concurrency:
+ group: pages
+ cancel-in-progress: true
+
+jobs:
+ deploy:
+ runs-on: ubuntu-latest
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
+ steps:
+ - uses: actions/checkout@v7
+ # The playground runs the package itself in the browser, so it ships the
+ # same source the command line does.
+ - name: Assemble the site
+ run: |
+ mkdir _site
+ cp site/* _site/
+ zip -qr _site/groundcheck.zip groundcheck -x '*/__pycache__/*'
+ - uses: actions/configure-pages@v6
+ - uses: actions/upload-pages-artifact@v5
+ with:
+ path: _site
+ - id: deployment
+ uses: actions/deploy-pages@v5
diff --git a/README.md b/README.md
index b8b91f1..7eeacdd 100644
--- a/README.md
+++ b/README.md
@@ -9,6 +9,10 @@ A **deterministic groundedness linter** for RAG and LLM answers. It reads an
answer and the sources it was supposed to be based on, and flags the claims that
aren't actually supported — above all, **fabricated numbers**.
+**[Try it in your browser](https://rootx22.github.io/groundcheck/)**: paste an
+answer and its sources and see what it flags. It runs groundcheck itself through
+Pyodide, so nothing you paste leaves the page and no model is called.
+
```console
$ groundcheck --answer answer.txt --source q3-report.txt --fail-on high
diff --git a/site/index.html b/site/index.html
new file mode 100644
index 0000000..8de7bd3
--- /dev/null
+++ b/site/index.html
@@ -0,0 +1,390 @@
+
+
+
+
+
+groundcheck: catch the numbers your RAG answer made up
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Paste an answer and the sources it was given. groundcheck flags figures that appear in no source, claims nothing supports, citations that point at the wrong source and negations that got flipped. It is deterministic: the same input always gets the same verdict, so it can gate a CI pipeline where a model acting as judge cannot.
+
Everything runs in this page. Nothing you paste is sent anywhere, and no model is called.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Loading Python in your browser. About 10 MB the first time, cached after that.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Sentence by sentence
+
Each claim, whether a source supports it, and how much of its wording the closest source shares. Connective sentences with nothing to check are left out.
+
+
+
+
+
+
+
+
+
diff --git a/site/og.png b/site/og.png
new file mode 100644
index 0000000..ec2ed45
Binary files /dev/null and b/site/og.png differ
diff --git a/site/playground.py b/site/playground.py
new file mode 100644
index 0000000..e5ae35c
--- /dev/null
+++ b/site/playground.py
@@ -0,0 +1,16 @@
+"""Bridge between the browser page and groundcheck, run inside Pyodide.
+
+Sources arrive as a JSON list so the page never builds Python objects itself,
+and the report goes back as the same dictionary the CLI prints with --json.
+"""
+
+from __future__ import annotations
+
+import json
+
+from groundcheck import check
+
+
+def analyse(answer: str, sources_json: str, threshold: float) -> str:
+ sources = [source for source in json.loads(sources_json) if source.strip()]
+ return json.dumps(check(answer, sources, support_threshold=threshold).to_dict())