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
10 changes: 5 additions & 5 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,14 @@ <h1>Refract</h1>
<div id="filter-area"></div>
<div id="export-area"></div>
</div>
<div class="section-head">Timeline</div>
<div id="dashboard">
<div id="panel-timeline" class="panel"></div>
<div id="panel-diff" class="panel"></div>
<div id="panel-citations" class="panel"></div>
<div id="panel-certainty" class="panel"></div>
<div id="dashboard-side">
<div id="panel-diff" class="panel"></div>
<div id="panel-citations" class="panel"></div>
<div id="panel-certainty" class="panel"></div>
</div>
</div>
<div class="section-head">Changes</div>
<div id="dashboard-row2">
<div id="panel-disputes" class="panel"></div>
<div id="panel-wording" class="panel"></div>
Expand Down
24 changes: 15 additions & 9 deletions src/citations/SourceChangeTable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,13 @@ export class SourceChangeTable {
}

const table = document.createElement("table");
table.className = "data-table";
table.className = "data-table source-table";

// Detail is last so it takes the width the short columns leave. The
// revision id is on each row's tooltip rather than in a column of its own.
const thead = document.createElement("thead");
thead.innerHTML = `
<tr>
<th>Rev</th>
<th>Date</th>
<th>Type</th>
<th>Section</th>
Expand All @@ -58,14 +59,19 @@ export class SourceChangeTable {
const tbody = document.createElement("tbody");
for (const ch of changes) {
const tr = document.createElement("tr");
tr.title = `Revision r${ch.revisionId}`;
const ts = new Date(ch.timestamp);
tr.innerHTML = `
<td>r${ch.revisionId}</td>
<td>${ts.toLocaleDateString()}</td>
<td>${ch.changeType}</td>
<td>${ch.section}</td>
<td>${ch.detail}</td>
`;
// textContent, not innerHTML: section and detail come from the loaded file.
for (const text of [
ts.toLocaleDateString(),
ch.changeType,
ch.section,
ch.detail,
]) {
const td = document.createElement("td");
td.textContent = text;
tr.appendChild(td);
}
tbody.appendChild(tr);
}
table.appendChild(tbody);
Expand Down
52 changes: 34 additions & 18 deletions src/export/SchemaViewer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,39 +30,47 @@ export class SchemaViewer {
}

const keys = this.collectKeys();
const infos = new Map(keys.map((key) => [key, this.getTypeInfo(key)]));
const allPresent = [...infos.values()].every((i) => i.inEveryEvent);

const schema = document.createElement("div");
schema.className = "schema-viewer";

// Presence is stated once for the fields every event carries; only the
// fields some events lack show their share.
const headerEl = document.createElement("div");
headerEl.style.cssText =
"color:var(--text-dim);margin-bottom:0.5rem;font-weight:600;";
headerEl.textContent = `Schema (${this.events.length} events, ${keys.length} fields)`;
headerEl.textContent = `${this.events.length} events, ${keys.length} fields. ${
allPresent
? "Every field is present in every event."
: "Fields without a percentage are present in every event."
}`;
schema.appendChild(headerEl);

for (const key of keys) {
const row = document.createElement("div");
row.style.cssText = "display:flex;gap:0.5rem;padding:0.15rem 0;";
// One grid for every field, so name, type and presence line up across
// rows whether or not a row has a presence cell.
const fields = document.createElement("div");
fields.className = "schema-fields";

for (const [key, typeInfo] of infos) {
const keyName = document.createElement("span");
keyName.style.cssText = "color:var(--accent);min-width:200px;";
keyName.className = "schema-key";
keyName.textContent = key;
row.appendChild(keyName);
fields.appendChild(keyName);

const typeInfo = this.getTypeInfo(key);
const typeName = document.createElement("span");
typeName.style.cssText = "color:var(--text-dim);min-width:100px;";
typeName.textContent = typeInfo.type;
row.appendChild(typeName);
fields.appendChild(typeName);

const presence = document.createElement("span");
presence.style.cssText = "color:var(--text-dim);";
presence.textContent = `${typeInfo.presence}% present`;
row.appendChild(presence);

schema.appendChild(row);
if (!typeInfo.inEveryEvent) {
const presence = document.createElement("span");
presence.textContent = `${typeInfo.presence}% present`;
fields.appendChild(presence);
}
}

schema.appendChild(fields);
this.container.appendChild(schema);
}

Expand All @@ -78,7 +86,11 @@ export class SchemaViewer {
return keys;
}

private getTypeInfo(key: string): { type: string; presence: number } {
private getTypeInfo(key: string): {
type: string;
presence: number;
inEveryEvent: boolean;
} {
let present = 0;
const types = new Set<string>();

Expand All @@ -93,7 +105,11 @@ export class SchemaViewer {
}

const typeStr = Array.from(types).join(" | ");
const pct = Math.round((present / this.events.length) * 100);
return { type: typeStr, presence: pct };
const inEveryEvent = present === this.events.length;
// A field missing from some events never rounds up to 100%.
const pct = inEveryEvent
? 100
: Math.min(99, Math.round((present / this.events.length) * 100));
return { type: typeStr, presence: pct, inEveryEvent };
}
}
40 changes: 24 additions & 16 deletions src/export/TimelineExport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@ import type { EvidenceEvent } from "../types.js";

export type ExportFormat = "json" | "jsonl" | "csv";

const FORMATS: [ExportFormat, string][] = [
["json", "JSON"],
["jsonl", "JSONL"],
["csv", "CSV"],
];

export class TimelineExport {
private container: HTMLElement;
private events: EvidenceEvent[] = [];
Expand All @@ -18,24 +24,26 @@ export class TimelineExport {
private render(): void {
this.container.innerHTML = "";

const jsonBtn = document.createElement("button");
jsonBtn.className = "export-btn";
jsonBtn.textContent = "Export JSON";
jsonBtn.addEventListener("click", () => this.export("json"));

const jsonlBtn = document.createElement("button");
jsonlBtn.className = "export-btn";
jsonlBtn.textContent = "Export JSONL";
jsonlBtn.addEventListener("click", () => this.export("jsonl"));
// One export action with a format choice, instead of a button per format.
const format = document.createElement("select");
format.setAttribute("aria-label", "Export format");
for (const [value, label] of FORMATS) {
const option = document.createElement("option");
option.value = value;
option.textContent = label;
format.appendChild(option);
}

const csvBtn = document.createElement("button");
csvBtn.className = "export-btn";
csvBtn.textContent = "Export CSV";
csvBtn.addEventListener("click", () => this.export("csv"));
const exportBtn = document.createElement("button");
exportBtn.type = "button";
exportBtn.className = "export-btn";
exportBtn.textContent = "Export";
exportBtn.addEventListener("click", () =>
this.export(format.value as ExportFormat),
);

this.container.appendChild(jsonBtn);
this.container.appendChild(jsonlBtn);
this.container.appendChild(csvBtn);
this.container.appendChild(format);
this.container.appendChild(exportBtn);

const exportStatus = document.createElement("span");
exportStatus.id = "export-status";
Expand Down
3 changes: 2 additions & 1 deletion src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ let allEvents: EvidenceEvent[] = [];
const timelineView = new TimelineView({
container: document.getElementById("panel-timeline")!,
onSelectEvent: (event) => {
diffView.showDiff(event);
if (event) diffView.showDiff(event);
else diffView.clear();
},
});

Expand Down
Loading
Loading