feat(frontend): v2 rewrite — Vite + React + TS readonly SPA
Replaces the v1 CRA app (which targeted the obsolete Python Dialectic
backend) with a fresh Vite + React 18 + TypeScript scaffold that talks
to Dialectic.Backend Go v2.
Pages (all readonly — propose/signup/post are agent-only by design):
- / TopicList — filter by status, paginated
- /topics/:id TopicDetail — meta + camps + transcript
(polling every 8s)
- /topics/:id/verdict Verdict permalink (shareable)
- /agents/:id AgentActivity — admin diagnostics card
Stack:
- Vite 5 + React 18 + react-router-dom 6
- Pure ESM, NodeNext-style imports, .tsx
- Style: ~/STYLE.md tokens (IBM Plex Mono + Major Mono Display +
--acid #d8ff3e on --ink #080a0d, with subtle blueprint grid wash)
Auth:
- v1 dev-bypass only — VITE_OIDC_DEV_BYPASS auto-attaches
x-dev-bypass header. Real Keycloak OIDC redirect ships as v2.
- Admin endpoints (x-dialectic-admin-key) prompt on first visit
and store key in localStorage. Never baked into bundle. Never
sent to non-admin endpoints.
Backend pairing:
- Dialectic.Backend@0b16b52 adds GET /api/admin/agents/{id} for the
AgentActivity page. AgentActivity calls it via the admin-key
branch in api.ts.
Deploy:
- Multi-stage Dockerfile (node:22-alpine build → nginx:1.27-alpine
serve). nginx.conf reverse-proxies /api/ → dialectic-backend:8090
so the browser sees one origin (no CORS).
Reuses the existing hzhang/Dialectic.Frontend repo — old CRA contents
nuked in this commit. History preserved on master.
This commit is contained in:
78
src/pages/Verdict.tsx
Normal file
78
src/pages/Verdict.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Link, useParams } from 'react-router-dom';
|
||||
import { getTopic, getVerdict } from '../api';
|
||||
import type { TopicDetail, Verdict } from '../types';
|
||||
import { fmtTime } from '../util';
|
||||
|
||||
export function VerdictPage() {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const [topic, setTopic] = useState<TopicDetail | null>(null);
|
||||
const [verdict, setVerdict] = useState<Verdict | null>(null);
|
||||
const [err, setErr] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (!id) return;
|
||||
const ac = new AbortController();
|
||||
setLoading(true);
|
||||
Promise.all([getTopic(id, ac.signal), getVerdict(id, ac.signal)])
|
||||
.then(([t, v]) => {
|
||||
setTopic(t);
|
||||
setVerdict(v);
|
||||
setErr(null);
|
||||
})
|
||||
.catch((e: Error) => {
|
||||
if (e.name !== 'AbortError') setErr(e.message);
|
||||
})
|
||||
.finally(() => setLoading(false));
|
||||
return () => ac.abort();
|
||||
}, [id]);
|
||||
|
||||
if (!id) return <div className="err">missing topic id</div>;
|
||||
if (loading) return <div className="muted">loading…</div>;
|
||||
if (err) return <div className="err">{err}</div>;
|
||||
if (!topic) return <div className="err">topic not found</div>;
|
||||
|
||||
return (
|
||||
<div className="td">
|
||||
<div>
|
||||
<span className="eyebrow">verdict permalink</span>
|
||||
<h1>{topic.title}</h1>
|
||||
<p className="td-summary">{topic.summary}</p>
|
||||
<p className="muted" style={{ marginTop: 8 }}>
|
||||
schema: <code>{topic.verdict_schema_id}</code>
|
||||
{' · '}debate window: {fmtTime(topic.debate_start_at)} → {fmtTime(topic.debate_end_at)}
|
||||
{' · '}
|
||||
<Link to={`/topics/${topic.id}`}>back to topic</Link>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{!verdict ? (
|
||||
<div className="panel td-empty">
|
||||
no verdict yet — topic status is <span className={`pill pill-${topic.status}`}>{topic.status}</span>
|
||||
{topic.status === 'debating' && ', judge has not submitted'}
|
||||
{topic.status === 'cancelled' && ', topic was cancelled before a verdict could be reached'}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="panel">
|
||||
<div className="panel-header">verdict (structured)</div>
|
||||
<pre className="vd-json">{JSON.stringify(verdict.verdict, null, 2)}</pre>
|
||||
<div className="row muted" style={{ marginTop: 8, fontSize: '0.85em' }}>
|
||||
<span>judge: <Link to={`/agents/${verdict.judge_agent_id}`}>{verdict.judge_agent_id}</Link></span>
|
||||
<span className="spacer" />
|
||||
<span>produced: {fmtTime(verdict.produced_at)}</span>
|
||||
{(verdict.tokens_input + verdict.tokens_output) > 0 && (
|
||||
<span>tokens: {verdict.tokens_input} in / {verdict.tokens_output} out</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="panel">
|
||||
<div className="panel-header">rationale</div>
|
||||
<div className="vd-rationale">{verdict.rationale}</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user