Skip to content
This repository was archived by the owner on Oct 13, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 8 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
# Refract UI

<p align="center">
<img src="docs/assets/refract-ui-demo.gif" alt="refract-ui demo animation" width="720">
</p>

Standalone visualization for the [Refract](https://github.com/refract-org/refract) observation engine.

Load any JSONL file of Refract observation events and explore what changed β€” timelines, diffs, citation graphs, disputes, and event-type breakdowns. No backend needed.
Load a JSONL file of Refract events to see them as a timeline with word-level diffs, citation and revert charts, talk page activity, and a list of the fields the events carry. Everything runs in the browser; there is no backend.

## Quick start

Expand All @@ -27,17 +23,17 @@ Then drag `bitcoin-events.jsonl` onto the Refract UI upload zone.

## What you'll see

**Timeline** β€” Every event in chronological order. Click any event to see the word-level diff of what changed. Filter by event type to narrow the view.
**Timeline** β€” Every event, in the order the file lists them. Click an event to see the word-level diff of what changed. Filter by event type to narrow the view.

**Citations** β€” Bar chart showing citation additions, removals, and replacements per revision, plus a sortable table of every citation change.
**Citations** β€” Bar chart of citation additions, removals, and replacements per revision, plus a table of every citation change.

**Certainty** β€” Confidence scores over time for model-interpreted events.
**Model confidence** β€” The confidence score of each event on the `model_interpretation` layer.

**Disputes** β€” Revert clusters, edit velocity per day, and talk page activity.
**Disputes** β€” Revert clusters, events per day (with reverts), and talk page activity.

**Language Changes** β€” Card grid of wording events (claims reworded, strengthened, softened, removed).
**Wording changes** β€” One card per sentence event (first seen, modified, removed, reintroduced), with the before and after text.

**Schema** β€” Every JSON key in the loaded data, with types and presence percentage.
**Event schema** β€” Every top-level field in the loaded events, with its types and the share of events that have it.

**Export** β€” Download filtered data as JSON, JSONL, or CSV.

Expand All @@ -46,7 +42,7 @@ Then drag `bitcoin-events.jsonl` onto the Refract UI upload zone.
- Vite + vanilla TypeScript
- No framework dependencies
- DOM-based rendering with CSS custom properties
- Works fully offline after first load
- Loads nothing from other origins; an uploaded file is read in the browser and never sent anywhere

---

Expand Down
9 changes: 3 additions & 6 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,14 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Refract β€” Observation UI</title>
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>πŸ”</text></svg>" />
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Syne:wght@400;500;600;700;800&family=Manrope:wght@300;400;500;600;700&family=Recursive:wght@400;500;600;700&display=swap" rel="stylesheet">
<title>Refract event viewer</title>
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'><style>path{fill:%230c0f1a}@media (prefers-color-scheme:dark){path{fill:%23e2e4ed}}</style><path d='M8 2 15 14H1z'/></svg>" />
</head>
<body>
<div id="app">
<header id="app-header">
<h1>Refract</h1>
<p class="subtitle">Observation Engine β€” Timeline Viewer</p>
<p class="subtitle">Event viewer for Refract JSONL exports</p>
</header>
<div id="toolbar">
<div id="import-area"></div>
Expand Down
2 changes: 1 addition & 1 deletion src/citations/CitationChurnChart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export class CitationChurnChart {

const title = document.createElement("div");
title.className = "panel-title";
title.textContent = "Citation Churn";
title.textContent = "Citation churn";
this.container.appendChild(title);

const churnData = this.computeChurn();
Expand Down
2 changes: 1 addition & 1 deletion src/citations/SourceChangeTable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export class SourceChangeTable {

const title = document.createElement("div");
title.className = "panel-title";
title.textContent = "Source Changes";
title.textContent = "Source changes";
this.container.appendChild(title);

const changes = this.computeChanges();
Expand Down
2 changes: 1 addition & 1 deletion src/disputes/EditVelocityChart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export class EditVelocityChart {

const title = document.createElement("div");
title.className = "panel-title";
title.textContent = "Edit Velocity";
title.textContent = "Events per day";
this.container.appendChild(title);

const buckets = this.computeBuckets();
Expand Down
2 changes: 1 addition & 1 deletion src/disputes/RevertClusterDisplay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export class RevertClusterDisplay {

const title = document.createElement("div");
title.className = "panel-title";
title.textContent = "Revert Clusters";
title.textContent = "Revert clusters";
this.container.appendChild(title);

const clusters = this.computeClusters();
Expand Down
2 changes: 1 addition & 1 deletion src/disputes/TalkPageTimeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export class TalkPageTimeline {

const title = document.createElement("div");
title.className = "panel-title";
title.textContent = "Talk Page Activity";
title.textContent = "Talk page activity";
this.container.appendChild(title);

const activities = this.computeActivities();
Expand Down
2 changes: 1 addition & 1 deletion src/export/SchemaViewer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export class SchemaViewer {

const title = document.createElement("div");
title.className = "panel-title";
title.textContent = "Event Schema Inspector";
title.textContent = "Event schema";
this.container.appendChild(title);

if (this.events.length === 0) {
Expand Down
2 changes: 1 addition & 1 deletion src/language/CertaintyTimeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export class CertaintyTimeline {

const title = document.createElement("div");
title.className = "panel-title";
title.textContent = "Certainty Over Time";
title.textContent = "Model confidence";
this.container.appendChild(title);

const points = this.computePoints();
Expand Down
2 changes: 1 addition & 1 deletion src/language/WordingDiffCard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export class WordingDiffCard {

const title = document.createElement("div");
title.className = "panel-title";
title.textContent = "Wording Changes";
title.textContent = "Wording changes";
this.container.appendChild(title);

const changes = this.computeChanges();
Expand Down
70 changes: 7 additions & 63 deletions src/styles/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,10 @@
--cyan: #22d3ee;
--radius: 10px;
--radius-sm: 6px;
--font: "Manrope", system-ui, sans-serif;
--font-heading: "Syne", system-ui, sans-serif;
--mono: "Recursive", monospace;
--font: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
--font-heading: var(--font);
--mono:
ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
}

html {
Expand All @@ -40,37 +41,17 @@ html {

body {
font-family: var(--font);
font-weight: 350;
background: var(--bg-deep);
color: var(--text);
line-height: 1.5;
-webkit-font-smoothing: antialiased;
min-height: 100vh;
}

/* ── Ambient Background ── */
body::before {
content: "";
position: fixed;
top: 0;
right: 0;
width: 50%;
height: 50%;
background: radial-gradient(
ellipse at top right,
rgba(124, 138, 255, 0.05),
transparent 60%
);
pointer-events: none;
z-index: 0;
}

#app {
max-width: 1440px;
margin: 0 auto;
padding: 1.25rem 1.5rem;
position: relative;
z-index: 1;
}

/* ── Header ── */
Expand All @@ -85,7 +66,7 @@ body::before {

#app-header h1 {
font-size: 1.4rem;
font-weight: 650;
font-weight: 600;
color: var(--accent);
letter-spacing: -0.02em;
}
Expand Down Expand Up @@ -128,46 +109,17 @@ body::before {
/* ── Panels ── */
.panel {
background: var(--bg-panel);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 1rem 1.25rem;
overflow: auto;
transition:
border-color 0.2s ease,
box-shadow 0.2s ease;
animation: panelFadeIn 0.35s ease-out both;
transition: border-color 0.2s ease;
}

.panel:hover {
border-color: var(--border-strong);
}

.panel:nth-child(1) {
animation-delay: 0ms;
}
.panel:nth-child(2) {
animation-delay: 60ms;
}
.panel:nth-child(3) {
animation-delay: 120ms;
}
.panel:nth-child(4) {
animation-delay: 180ms;
}

@keyframes panelFadeIn {
from {
opacity: 0;
transform: translateY(6px);
}
to {
opacity: 1;
transform: translateY(0);
}
}

.panel-title {
font-size: 0.7rem;
font-weight: 700;
Expand Down Expand Up @@ -357,7 +309,6 @@ select {
color: var(--text-dim);
margin-top: 0.15rem;
font-family: var(--mono);
font-variation-settings: "MONO" 1;
}

/* ── Diff Viewer ── */
Expand All @@ -384,7 +335,6 @@ select {

.diff-lines {
font-family: var(--mono);
font-variation-settings: "MONO" 1;
font-size: 0.78rem;
line-height: 1.6;
padding: 0.5rem 0.6rem;
Expand Down Expand Up @@ -476,14 +426,11 @@ select {
background: var(--accent);
transform: translate(-50%, -50%);
cursor: pointer;
transition:
transform 0.2s ease,
box-shadow 0.2s ease;
transition: transform 0.2s ease;
}

.certainty-dot:hover {
transform: translate(-50%, -50%) scale(1.5);
box-shadow: 0 0 12px rgba(124, 138, 255, 0.4);
}

.certainty-dot.high {
Expand Down Expand Up @@ -518,7 +465,6 @@ select {
padding: 0.5rem 0.6rem;
border-bottom: 1px solid var(--border);
font-family: var(--mono);
font-variation-settings: "MONO" 1;
font-size: 0.76rem;
}

Expand Down Expand Up @@ -559,7 +505,6 @@ select {

.card-diff {
font-family: var(--mono);
font-variation-settings: "MONO" 1;
font-size: 0.78rem;
margin-top: 0.4rem;
padding: 0.4rem;
Expand Down Expand Up @@ -628,7 +573,6 @@ select {
/* ── Schema Viewer ── */
.schema-viewer {
font-family: var(--mono);
font-variation-settings: "MONO" 1;
font-size: 0.76rem;
line-height: 1.6;
white-space: pre-wrap;
Expand Down
2 changes: 1 addition & 1 deletion src/timeline/DiffView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ export class DiffView {
this.container.innerHTML = "";
const title = document.createElement("div");
title.className = "panel-title";
title.textContent = "Diff Viewer";
title.textContent = "Diff";
this.container.appendChild(title);

const hint = document.createElement("div");
Expand Down
5 changes: 3 additions & 2 deletions src/timeline/TimelineView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export class TimelineView {

const title = document.createElement("div");
title.className = "panel-title";
title.textContent = "Revision Timeline";
title.textContent = "Revision timeline";
this.container.appendChild(title);

const count = document.createElement("div");
Expand Down Expand Up @@ -99,7 +99,8 @@ export class TimelineView {
}

function formatEventType(type: EventType): string {
return type.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
const words = type.replace(/_/g, " ");
return words.charAt(0).toUpperCase() + words.slice(1);
}

function summarizeEvent(event: EvidenceEvent): string {
Expand Down
Loading