From 7ad425ffc3aba2381cbe8fbe0857d88001071e2d Mon Sep 17 00:00:00 2001 From: hzhang Date: Sun, 24 May 2026 03:36:38 +0100 Subject: [PATCH] feat(ui): completed-only view + inline verdict + no polling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two product simplifications per user feedback: 1. TopicList shows ONLY completed debates. Status filter dropdown removed — debates in earlier lifecycle states (signup / debating) are agent internals; human readers care about the finished record. API call hardcodes status=completed. Page title is now 'completed debates' + 'archive' eyebrow. 2. TopicDetail merges verdict inline. Loads topic + arguments + verdict concurrently on mount, renders transcript followed by structured verdict + judge rationale on the same page. Drops the 8-second polling loop (completed debates don't change). The /topics/:id/verdict route is kept as a shareable permalink for the verdict alone, but the in-page link to it from the detail view is gone since the same content is already there. Build delta: 179 → 178 KB / gzip 57.70 → 57.40 (-300 bytes; one less route imported on the critical path). --- src/pages/TopicDetail.tsx | 128 ++++++++++++++++++-------------------- src/pages/TopicList.tsx | 68 +++++++------------- 2 files changed, 81 insertions(+), 115 deletions(-) diff --git a/src/pages/TopicDetail.tsx b/src/pages/TopicDetail.tsx index 7f3b682..8b4122b 100644 --- a/src/pages/TopicDetail.tsx +++ b/src/pages/TopicDetail.tsx @@ -1,56 +1,46 @@ -import { useEffect, useRef, useState } from 'react'; -import { Link, useParams } from 'react-router-dom'; -import { getTopic, listArguments } from '../api'; -import type { Argument, TopicDetail } from '../types'; -import { fmtTime, fmtRelative } from '../util'; +import { useEffect, useState } from 'react'; +import { useParams } from 'react-router-dom'; +import { getTopic, getVerdict, listArguments } from '../api'; +import type { Argument, TopicDetail, Verdict } from '../types'; +import { fmtTime } from '../util'; -// Polling interval for the live transcript. 8s strikes a balance — fast -// enough that a new argument appears within an attentive viewer's -// attention span, slow enough that idle tabs don't hammer the backend. -const POLL_MS = 8000; +// v0.3.1: completed-only view. Loads topic + arguments + verdict +// concurrently on mount. No polling — completed debates don't change. export function TopicDetailPage() { const { id } = useParams<{ id: string }>(); const [topic, setTopic] = useState(null); const [args, setArgs] = useState([]); + const [verdict, setVerdict] = useState(null); const [err, setErr] = useState(null); - const [lastTick, setLastTick] = useState(0); - const stopped = useRef(false); + const [loading, setLoading] = useState(true); useEffect(() => { if (!id) return; - stopped.current = false; const ac = new AbortController(); - - async function tick() { - try { - const [t, a] = await Promise.all([ - getTopic(id!, ac.signal), - listArguments(id!, ac.signal), - ]); - if (stopped.current) return; + setLoading(true); + Promise.all([ + getTopic(id, ac.signal), + listArguments(id, ac.signal), + getVerdict(id, ac.signal), + ]) + .then(([t, a, v]) => { setTopic(t); - setArgs(a.arguments ?? []); - setLastTick(Date.now()); + setArgs(a.arguments); + setVerdict(v); setErr(null); - } catch (e) { - const err = e as Error; - if (err.name !== 'AbortError') setErr(err.message); - } - } - - tick(); - const h = setInterval(tick, POLL_MS); - return () => { - stopped.current = true; - ac.abort(); - clearInterval(h); - }; + }) + .catch((e: Error) => { + if (e.name !== 'AbortError') setErr(e.message); + }) + .finally(() => setLoading(false)); + return () => ac.abort(); }, [id]); if (!id) return
missing topic id in url
; + if (loading) return
loading…
; if (err && !topic) return
{err}
; - if (!topic) return
loading…
; + if (!topic) return
topic not found
; const proCamp = topic.camps?.find((c) => c.camp === 'pro'); const conCamp = topic.camps?.find((c) => c.camp === 'con'); @@ -60,7 +50,7 @@ export function TopicDetailPage() {
- topic · {topic.status} + debate · {topic.status}

{topic.title}

{topic.summary}

@@ -69,19 +59,13 @@ export function TopicDetailPage() {
metadata
-
-
visibility
-
{topic.visibility}
-
verdict schema
{topic.verdict_schema_id}
-
signup window
-
- {fmtTime(topic.signup_open_at)} → {fmtTime(topic.signup_close_at)} -
+
visibility
+
{topic.visibility}
debate window
@@ -93,20 +77,12 @@ export function TopicDetailPage() {
creator
{topic.creator_user_id}
- {topic.cancelled_reason && ( -
-
cancelled reason
-
- {topic.cancelled_reason} -
-
- )}
{topic.camps && topic.camps.length > 0 && (
-
camps (allocated)
+
camps
pro
@@ -131,15 +107,9 @@ export function TopicDetailPage() { )}
-
- transcript ({args.length}) - - - polling every {POLL_MS / 1000}s · last refresh {fmtRelative(new Date(lastTick).toISOString())} - -
+
transcript ({args.length})
{args.length === 0 ? ( -
no arguments posted yet
+
no arguments were posted
) : ( args.map((a) => (
@@ -148,7 +118,7 @@ export function TopicDetailPage() { {a.agent_id} - {fmtRelative(a.posted_at)} + {fmtTime(a.posted_at)}
{a.content}
@@ -157,14 +127,34 @@ export function TopicDetailPage() { )}
- {topic.status === 'completed' && ( -
-
verdict
- view verdict + rationale → -
+ {verdict && ( + <> +
+
verdict
+
{JSON.stringify(verdict.verdict, null, 2)}
+
+ judge: {verdict.judge_agent_id} + + produced: {fmtTime(verdict.produced_at)} + {verdict.tokens_input + verdict.tokens_output > 0 && ( + + tokens: {verdict.tokens_input} in / {verdict.tokens_output} out + + )} +
+
+
+
rationale
+
{verdict.rationale}
+
+ )} - {err &&
refresh error: {err}
} + {!verdict && topic.status === 'completed' && ( +
+ no verdict was recorded (judge may have failed to submit) +
+ )}
); } diff --git a/src/pages/TopicList.tsx b/src/pages/TopicList.tsx index 93ff8f0..e5e3230 100644 --- a/src/pages/TopicList.tsx +++ b/src/pages/TopicList.tsx @@ -1,20 +1,15 @@ import { useEffect, useState } from 'react'; import { Link } from 'react-router-dom'; import { listTopics } from '../api'; -import type { Topic, TopicStatus } from '../types'; +import type { Topic } from '../types'; import { fmtTime, fmtRelative } from '../util'; -const STATUS_OPTIONS: Array = [ - 'all', - 'signup_open', - 'signup_closed', - 'debating', - 'completed', - 'cancelled', -]; +// v0.3.1: this list only shows COMPLETED debates. Topics in earlier +// lifecycle states (signup_open / signup_closed / debating) are agent +// internals — human readers care about the finished record (full +// transcript + verdict). Status filtering UI removed. export function TopicListPage() { - const [status, setStatus] = useState('all'); const [topics, setTopics] = useState([]); const [loading, setLoading] = useState(true); const [err, setErr] = useState(null); @@ -23,61 +18,44 @@ export function TopicListPage() { const ac = new AbortController(); setLoading(true); setErr(null); - listTopics({ - status: status === 'all' ? undefined : status, - limit: 100, - signal: ac.signal, - }) + listTopics({ status: 'completed', limit: 100, signal: ac.signal }) .then((r) => setTopics(r.topics)) .catch((e: Error) => { if (e.name !== 'AbortError') setErr(e.message); }) .finally(() => setLoading(false)); return () => ac.abort(); - }, [status]); + }, []); return (
- debates -

topics

+ archive +

completed debates

- Public + visible-to-you topics. Filter by lifecycle status. Click a row for the - live transcript. + Each entry is a finished debate — click for the full transcript + verdict.

- - {loading ? 'loading…' : `${topics.length} ${topics.length === 1 ? 'topic' : 'topics'}`} + {loading + ? 'loading…' + : `${topics.length} ${topics.length === 1 ? 'debate' : 'debates'}`}
{err &&
{err}
} {!err && !loading && topics.length === 0 && ( -
no topics match this filter
+
no completed debates yet
)} {topics.length > 0 && ( - - - + + @@ -90,14 +68,12 @@ export function TopicListPage() {
{t.summary}
- - - + ))}
topicstatussignup closedebateschema debate window
- {t.status} - - {fmtRelative(t.signup_close_at)} - - {fmtTime(t.debate_start_at)} → {fmtTime(t.debate_end_at)} + {t.verdict_schema_id} + {fmtRelative(t.debate_end_at)}