From 3e374447102828952286f25124c81f5bd2098d3d Mon Sep 17 00:00:00 2001 From: Admin Date: Tue, 29 Sep 2026 08:39:40 +0000 Subject: [PATCH] Add shared responsive media intelligence UI --- src/web.ts | 386 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 386 insertions(+) create mode 100644 src/web.ts diff --git a/src/web.ts b/src/web.ts new file mode 100644 index 0000000..5af9f27 --- /dev/null +++ b/src/web.ts @@ -0,0 +1,386 @@ +type AnyRow = Record; + +function esc(value: unknown) { + return String(value ?? "") + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} + +function nz(value: unknown) { + if (!value) return "—"; + try { + return new Intl.DateTimeFormat("en-NZ", { + timeZone: "Pacific/Auckland", + dateStyle: "medium", + timeStyle: "short" + }).format(new Date(String(value))); + } catch { + return String(value); + } +} + +function compact(value: unknown) { + const n = Number(value ?? 0); + return Number.isFinite(n) ? new Intl.NumberFormat("en-NZ").format(n) : String(value ?? 0); +} + +function initials(name: unknown) { + return String(name ?? "U") + .split(/\s+/) + .filter(Boolean) + .slice(0, 2) + .map((part) => part[0]?.toUpperCase() ?? "") + .join("") || "U"; +} + +function badge(label: unknown, tone = "neutral") { + return `${esc(label)}`; +} + +function statusTone(status: string) { + if (status === "develop") return "purple"; + if (status === "review" || status === "investigate") return "blue"; + if (status === "watch" || status === "researching") return "amber"; + if (status === "closed" || status === "ignore") return "neutral"; + return "green"; +} + +function evidenceTone(kind: unknown) { + if (kind === "source_document") return "green"; + if (kind === "source_payload") return "blue"; + return "neutral"; +} + +function shell(args: { + title: string; + active: "dashboard" | "stories" | "opportunities"; + identity: string; + body: string; +}) { + const nav = [ + ["dashboard", "/", "Desk"], + ["stories", "/stories", "Story Threads"], + ["opportunities", "/opportunities", "Opportunities"] + ] as const; + + return ` + + + + + +${esc(args.title)} — Media Intelligence + + + +
+MIMedia Intelligence + +
${esc(initials(args.identity))}
${esc(args.identity)}
Sign out
+
+
${args.body}
+
Link copied
+ +`; +} + +function graphSvg(story: AnyRow) { + const nodes = Array.isArray(story.nodes) ? story.nodes : []; + const edges = Array.isArray(story.edges) ? story.edges : []; + const width = 1120, height = 690, cx = width / 2, cy = height / 2 + 20, rx = 430, ry = 245; + const positions = new Map(); + nodes.forEach((node: AnyRow, i: number) => { + const angle = -Math.PI / 2 + (Math.PI * 2 * i) / Math.max(nodes.length, 1); + positions.set(node.id, {x:cx + Math.cos(angle)*rx, y:cy + Math.sin(angle)*ry}); + }); + + const edgeSvg = edges.map((edge: AnyRow) => { + const a = positions.get(edge.from_node_id), b = positions.get(edge.to_node_id); + if (!a || !b) return ""; + const mx=(a.x+b.x)/2, my=(a.y+b.y)/2; + return `${esc(edge.relationship)}`; + }).join(""); + + const nodeSvg = nodes.map((node: AnyRow) => { + const p = positions.get(node.id)!; + const raw = String(node.title ?? ""); + const label = raw.length > 39 ? `${raw.slice(0,36)}…` : raw; + return `${esc(node.title)} — ${esc(node.node_type)}${esc(node.node_type)}${esc(label)}`; + }).join(""); + + return `STORY THREAD${esc(String(story.title).slice(0,42))}${edgeSvg}${nodeSvg}`; +} + +function assertionState(node: AnyRow, story: AnyRow) { + const evidenceIds = new Set((story.source_audit?.sources ?? []).map((s: AnyRow) => s.node_id)); + const incoming = (story.edges ?? []).filter((e: AnyRow) => e.to_node_id === node.id && evidenceIds.has(e.from_node_id)); + const rels = new Set(incoming.map((e: AnyRow) => e.relationship)); + if ((rels.has("supports") && rels.has("contradicts")) || rels.has("qualifies")) return "qualified"; + if (rels.has("contradicts")) return "contradicted"; + if (rels.has("supports") || rels.has("actual_for") || rels.has("forecast_for")) return "supported"; + return "unsupported"; +} + +function sourceAuditPanel(story: AnyRow) { + const audit = story.source_audit ?? {robust:false,checks:{},counts:{}}; + const labels: Record = { + at_least_two_independent_sources:"Two independent source groups", + at_least_two_evidence_backed_nodes:"At least two substantive evidence records", + at_least_one_tier_one_source:"At least one Tier 1 source", + all_assertions_have_direct_evidence:"Assertions directly connected to evidence", + no_source_tag_only_direct_evidence_nodes:"No source-label-only evidence nodes" + }; + return `
+
${audit.robust ? "✓" : "!"}
+
${audit.robust ? "Sourcing gate passed" : "Sourcing gaps remain"}
+
${audit.robust ? "This thread meets the current source-diversity and evidence-linkage requirements." : "The thread stays in research until the missing evidence and independence checks are satisfied."}
+
+ ${badge(`${compact(audit.counts?.evidence_backed_nodes)} evidence-backed`, audit.counts?.evidence_backed_nodes >= 2 ? "green" : "amber")} + ${badge(`${compact(audit.counts?.distinct_independence_groups)} source groups`, audit.counts?.distinct_independence_groups >= 2 ? "green" : "amber")} + ${badge(`${compact(audit.counts?.unsupported_claims)} unsupported assertions`, audit.counts?.unsupported_claims ? "red" : "green")} +
+
${Object.entries(audit.checks ?? {}).map(([key,value]) => `
${esc(labels[key] ?? key.replace(/_/g," "))}
`).join("")}
+
+
`; +} + +function evidenceCard(node: AnyRow) { + const kind = node.evidence_kind ?? "discovery_metadata"; + const publisher = node.provenance_publisher ?? node.provenance_source_id ?? "Unknown publisher"; + const quality = kind === "source_document" ? "Source document" : kind === "source_payload" ? "Official source payload" : "Discovery metadata"; + const excerpt = String(node.evidence_excerpt ?? node.summary ?? "").trim(); + const sourceDate = node.snapshot_published_at ?? node.snapshot_retrieved_at ?? node.created_at; + return `
+
${badge(quality,evidenceTone(kind))}${node.provenance_authority_tier ? badge(`Tier ${node.provenance_authority_tier}`,"neutral") : ""}
${esc(nz(sourceDate))}
+

${esc(node.title)}

${esc(node.summary)}

+ ${excerpt ? `
${esc(excerpt.slice(0,1500))}${excerpt.length > 1500 ? "…" : ""}
` : ""} +
+
Publisher
${esc(publisher)}
+
Source
${esc(node.provenance_source_id ?? "—")}
+
Snapshot
${esc(node.snapshot_kind ?? "—")}${node.snapshot_id ? ` · ${esc(String(node.snapshot_id).slice(0,12))}` : ""}
+
Captured
${esc(nz(node.snapshot_retrieved_at))}
+
+ ${node.evidence_url ? `` : ""} +
`; +} + +export function renderStory(args: {story: AnyRow; identity: string}) { + const {story, identity} = args; + const nodes = Array.isArray(story.nodes) ? story.nodes : []; + const edges = Array.isArray(story.edges) ? story.edges : []; + const evidenceNodes = nodes.filter((n: AnyRow) => n.evidence_id); + const assertions = nodes.filter((n: AnyRow) => ["claim","counterclaim","forecast","trend","metric"].includes(n.node_type)); + const researchNodes = nodes.filter((n: AnyRow) => ["question","context","organisation","person","place"].includes(n.node_type)); + const byId = new Map(nodes.map((n: AnyRow) => [n.id,n])); + const counter = (story.research_checks ?? []).find((c:AnyRow)=>c.check_type==="counter_evidence"); + + const body = ` +
360° Story Thread
+
${badge(story.status,statusTone(story.status))}${story.source_audit?.robust ? badge("sourcing ready","green") : badge("sourcing gaps","amber")}${counter ? badge(`counter-evidence: ${counter.result}`,"blue") : badge("counter-evidence pending","amber")}
+

${esc(story.title)}

${esc(story.question)}
+
Created by ${esc(story.created_by_name)} · updated ${esc(nz(story.updated_at))}
+
All threads
+ +
${sourceAuditPanel(story)}
+ +
+

Editorial state

Human-controlled progression. Research gates apply before review/develop.

+
${["researching","review","develop","closed"].map((s) => `
`).join("")}
+

Counter-evidence pass

${counter ? `${esc(counter.result)} · ${esc((counter.sources_checked ?? []).join(", "))}` : "Required before editorial progression"}

+
${counter ? esc(counter.summary) : "No completed counter-evidence check is stored for this thread."}
+
+ +

Evidence network

${compact(nodes.length)} nodes · ${compact(edges.length)} relationships

+
evidenceclaimcounterclaimquestionforecast / trend
Tap a node to jump to its card
+
${graphSvg(story)}
Graph view is condensed on mobile. Use the evidence and assertion cards below for the full research trail.
+ +

Assertions

Evidence linkage, not a confidence score.

+
${assertions.length ? assertions.map((node: AnyRow) => { + const state = assertionState(node, story); + const incoming = edges.filter((e:AnyRow)=>e.to_node_id===node.id); + return `
${badge(node.node_type,"neutral")}${badge(state,state==="supported"?"green":state==="contradicted"?"red":state==="qualified"?"amber":"neutral")}

${esc(node.title)}

${esc(node.summary)}

${incoming.length ? `${incoming.length} incoming relationship${incoming.length===1?"":"s"}` : "No evidence relationship yet"}${node.confidence != null ? ` · confidence ${esc(node.confidence)}` : ""}
`; + }).join("") : `
No assertions in this thread yet.
`}
+ +

Evidence & provenance

Exact publisher, evidence class, snapshot and original source.

+
${evidenceNodes.length ? evidenceNodes.map(evidenceCard).join("") : `
No evidence nodes yet.
`}
+ + ${researchNodes.length ? `

Research context

Questions, context and entities that shape the investigation.

${researchNodes.map((n:AnyRow)=>`
${badge(n.node_type,"neutral")}

${esc(n.title)}

${esc(n.summary)}
Added by ${esc(n.created_by_name)}
`).join("")}
` : ""} + +

Relationships

Human-readable graph edges with rationale.

+
${edges.length ? edges.map((edge:AnyRow)=>{ + const from=byId.get(edge.from_node_id), to=byId.get(edge.to_node_id); + return `
FROM
${esc(from?.title ?? edge.from_node_id)}
${badge(edge.relationship,"neutral")}
TO
${esc(to?.title ?? edge.to_node_id)}
${esc(edge.reason)}
`; + }).join("") : `
No relationships stored.
`}
`; + + return shell({title:story.title,active:"stories",identity,body}); +} + +export function renderStories(args: {stories: AnyRow[]; identity: string}) { + const body = `
Research workspace

Story Threads

Evidence networks built around a reporting question, with explicit counter-evidence and sourcing gates before editorial review.

+ `; + return shell({title:"Story Threads",active:"stories",identity:args.identity,body}); +} + +export function renderOpportunity(args:{opportunity:AnyRow;identity:string}) { + const o=args.opportunity, evidence=Array.isArray(o.evidence)?o.evidence:[], notes=Array.isArray(o.notes)?o.notes:[]; + const body=`
Opportunity
${badge(o.status,statusTone(o.status))}${badge(`score ${o.score}`,"neutral")}

${esc(o.title)}

${esc(o.summary)}

${esc(o.source_id)} · ${esc(nz(o.published_at || o.detected_at))}
+

Triage

${esc((o.reasons??[]).join(" · "))}

${["watch","investigate","develop","ignore","closed"].map((s)=>`
`).join("")}
+

Why it surfaced

${esc((o.reasons??[]).join(" · ")||"No scoring reasons stored.")}
+

Stored evidence

${evidence.length} record${evidence.length===1?"":"s"}

${evidence.length?evidence.map((e:AnyRow)=>`
${badge(e.evidence_kind??"stored evidence",evidenceTone(e.evidence_kind))}
${esc(nz(e.retrieved_at))}
${esc(e.excerpt)}
`).join(""):`
No evidence stored.
`}
+

Notes

${notes.length?notes.map((n:AnyRow)=>`
${esc(nz(n.created_at))}
${esc(n.note)}
`).join(""):`
No notes yet.
`}
`; + return shell({title:o.title,active:"opportunities",identity:args.identity,body}); +} + +export function renderOpportunities(args:{opportunities:AnyRow[];identity:string;status?:string}) { + const states=["new","watch","investigate","develop","ignore","closed"]; + const body=`
Signal triage

Opportunities

Potential stories surfaced from monitored sources. Humans decide what to watch, investigate or develop.

+
Active${states.map((s)=>`${esc(s)}`).join("")}
+ `; + return shell({title:"Opportunities",active:"opportunities",identity:args.identity,body}); +} + +export function renderDashboard(args:{identity:string;counts:AnyRow;messages:AnyRow[];sources:AnyRow[];stories:AnyRow[]}) { + const c=args.counts??{}, sourceErrors=args.sources.filter((s)=>s.last_error), storyAttention=args.stories.filter((s)=>!s.source_audit?.robust && s.status!=="closed"); + const body=`
Newsroom desk

Media Intelligence

Public-source monitoring, evidence research and human editorial control in one shared workspace.

+
+
${compact(c.signals)}
stored signals
+
${compact(c.opportunities)}
active opportunities
+
${compact(c.new_opportunities)}
new to triage
+
${compact(c.story_threads)}
open Story Threads
+
${compact(c.unread_messages)}
agent updates
+
+
+ +

Source health

Current ingestion state.

+
${args.sources.map((s:AnyRow)=>`
${esc(s.name)}
${badge(s.source_class??s.source_type,"neutral")}${s.authority_tier?badge(`Tier ${s.authority_tier}`,"neutral"):""}
${compact(s.signal_count)} signals
${s.last_error?"error":esc(nz(s.last_synced_at))}
`).join("")}
+ ${sourceErrors.length?`
${sourceErrors.length} source issue${sourceErrors.length===1?"":"s"}
${sourceErrors.map((s)=>esc(s.name)).join(" · ")}
`:""} +
+
+

Agent Activity

Operational findings and source-monitor updates.

+
${args.messages.length?args.messages.map((m:AnyRow)=>`
${esc(m.title)}
${esc(m.body)}
${badge(m.severity,m.severity==="warning"?"amber":m.severity==="error"?"red":"blue")}${esc(m.actor_name)} via ${esc(m.actor_client_id)}
${esc(nz(m.created_at))}
`).join(""):`
No unread Agent Activity.
`}
`; + return shell({title:"Desk",active:"dashboard",identity:args.identity,body}); +}