Compare commits
1 Commits
d1d2ae2fb1
...
7ad425ffc3
| Author | SHA1 | Date | |
|---|---|---|---|
| 7ad425ffc3 |
@@ -1,56 +1,46 @@
|
|||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { Link, useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import { getTopic, listArguments } from '../api';
|
import { getTopic, getVerdict, listArguments } from '../api';
|
||||||
import type { Argument, TopicDetail } from '../types';
|
import type { Argument, TopicDetail, Verdict } from '../types';
|
||||||
import { fmtTime, fmtRelative } from '../util';
|
import { fmtTime } from '../util';
|
||||||
|
|
||||||
// Polling interval for the live transcript. 8s strikes a balance — fast
|
// v0.3.1: completed-only view. Loads topic + arguments + verdict
|
||||||
// enough that a new argument appears within an attentive viewer's
|
// concurrently on mount. No polling — completed debates don't change.
|
||||||
// attention span, slow enough that idle tabs don't hammer the backend.
|
|
||||||
const POLL_MS = 8000;
|
|
||||||
|
|
||||||
export function TopicDetailPage() {
|
export function TopicDetailPage() {
|
||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>();
|
||||||
const [topic, setTopic] = useState<TopicDetail | null>(null);
|
const [topic, setTopic] = useState<TopicDetail | null>(null);
|
||||||
const [args, setArgs] = useState<Argument[]>([]);
|
const [args, setArgs] = useState<Argument[]>([]);
|
||||||
|
const [verdict, setVerdict] = useState<Verdict | null>(null);
|
||||||
const [err, setErr] = useState<string | null>(null);
|
const [err, setErr] = useState<string | null>(null);
|
||||||
const [lastTick, setLastTick] = useState<number>(0);
|
const [loading, setLoading] = useState(true);
|
||||||
const stopped = useRef(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
stopped.current = false;
|
|
||||||
const ac = new AbortController();
|
const ac = new AbortController();
|
||||||
|
setLoading(true);
|
||||||
async function tick() {
|
Promise.all([
|
||||||
try {
|
getTopic(id, ac.signal),
|
||||||
const [t, a] = await Promise.all([
|
listArguments(id, ac.signal),
|
||||||
getTopic(id!, ac.signal),
|
getVerdict(id, ac.signal),
|
||||||
listArguments(id!, ac.signal),
|
])
|
||||||
]);
|
.then(([t, a, v]) => {
|
||||||
if (stopped.current) return;
|
|
||||||
setTopic(t);
|
setTopic(t);
|
||||||
setArgs(a.arguments ?? []);
|
setArgs(a.arguments);
|
||||||
setLastTick(Date.now());
|
setVerdict(v);
|
||||||
setErr(null);
|
setErr(null);
|
||||||
} catch (e) {
|
})
|
||||||
const err = e as Error;
|
.catch((e: Error) => {
|
||||||
if (err.name !== 'AbortError') setErr(err.message);
|
if (e.name !== 'AbortError') setErr(e.message);
|
||||||
}
|
})
|
||||||
}
|
.finally(() => setLoading(false));
|
||||||
|
return () => ac.abort();
|
||||||
tick();
|
|
||||||
const h = setInterval(tick, POLL_MS);
|
|
||||||
return () => {
|
|
||||||
stopped.current = true;
|
|
||||||
ac.abort();
|
|
||||||
clearInterval(h);
|
|
||||||
};
|
|
||||||
}, [id]);
|
}, [id]);
|
||||||
|
|
||||||
if (!id) return <div className="err">missing topic id in url</div>;
|
if (!id) return <div className="err">missing topic id in url</div>;
|
||||||
|
if (loading) return <div className="muted">loading…</div>;
|
||||||
if (err && !topic) return <div className="err">{err}</div>;
|
if (err && !topic) return <div className="err">{err}</div>;
|
||||||
if (!topic) return <div className="muted">loading…</div>;
|
if (!topic) return <div className="err">topic not found</div>;
|
||||||
|
|
||||||
const proCamp = topic.camps?.find((c) => c.camp === 'pro');
|
const proCamp = topic.camps?.find((c) => c.camp === 'pro');
|
||||||
const conCamp = topic.camps?.find((c) => c.camp === 'con');
|
const conCamp = topic.camps?.find((c) => c.camp === 'con');
|
||||||
@@ -60,7 +50,7 @@ export function TopicDetailPage() {
|
|||||||
<div className="td">
|
<div className="td">
|
||||||
<div>
|
<div>
|
||||||
<span className="eyebrow">
|
<span className="eyebrow">
|
||||||
topic · <span className={`pill pill-${topic.status}`}>{topic.status}</span>
|
debate · <span className={`pill pill-${topic.status}`}>{topic.status}</span>
|
||||||
</span>
|
</span>
|
||||||
<h1>{topic.title}</h1>
|
<h1>{topic.title}</h1>
|
||||||
<p className="td-summary">{topic.summary}</p>
|
<p className="td-summary">{topic.summary}</p>
|
||||||
@@ -69,19 +59,13 @@ export function TopicDetailPage() {
|
|||||||
<div className="panel">
|
<div className="panel">
|
||||||
<div className="panel-header">metadata</div>
|
<div className="panel-header">metadata</div>
|
||||||
<div className="td-meta">
|
<div className="td-meta">
|
||||||
<div>
|
|
||||||
<div className="label">visibility</div>
|
|
||||||
<div className="value">{topic.visibility}</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
<div className="label">verdict schema</div>
|
<div className="label">verdict schema</div>
|
||||||
<div className="value">{topic.verdict_schema_id}</div>
|
<div className="value">{topic.verdict_schema_id}</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="label">signup window</div>
|
<div className="label">visibility</div>
|
||||||
<div className="value">
|
<div className="value">{topic.visibility}</div>
|
||||||
{fmtTime(topic.signup_open_at)} → {fmtTime(topic.signup_close_at)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="label">debate window</div>
|
<div className="label">debate window</div>
|
||||||
@@ -93,20 +77,12 @@ export function TopicDetailPage() {
|
|||||||
<div className="label">creator</div>
|
<div className="label">creator</div>
|
||||||
<div className="value">{topic.creator_user_id}</div>
|
<div className="value">{topic.creator_user_id}</div>
|
||||||
</div>
|
</div>
|
||||||
{topic.cancelled_reason && (
|
|
||||||
<div>
|
|
||||||
<div className="label">cancelled reason</div>
|
|
||||||
<div className="value err" style={{ padding: '4px 8px' }}>
|
|
||||||
{topic.cancelled_reason}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{topic.camps && topic.camps.length > 0 && (
|
{topic.camps && topic.camps.length > 0 && (
|
||||||
<div className="panel">
|
<div className="panel">
|
||||||
<div className="panel-header">camps (allocated)</div>
|
<div className="panel-header">camps</div>
|
||||||
<div className="td-camps">
|
<div className="td-camps">
|
||||||
<div className="td-camp">
|
<div className="td-camp">
|
||||||
<div className="td-camp-label camp-pro">pro</div>
|
<div className="td-camp-label camp-pro">pro</div>
|
||||||
@@ -131,15 +107,9 @@ export function TopicDetailPage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="panel">
|
<div className="panel">
|
||||||
<div className="panel-header row">
|
<div className="panel-header">transcript ({args.length})</div>
|
||||||
<span>transcript ({args.length})</span>
|
|
||||||
<span className="spacer" />
|
|
||||||
<span className="muted">
|
|
||||||
polling every {POLL_MS / 1000}s · last refresh {fmtRelative(new Date(lastTick).toISOString())}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{args.length === 0 ? (
|
{args.length === 0 ? (
|
||||||
<div className="td-empty">no arguments posted yet</div>
|
<div className="td-empty">no arguments were posted</div>
|
||||||
) : (
|
) : (
|
||||||
args.map((a) => (
|
args.map((a) => (
|
||||||
<div key={a.id} className={`td-arg td-arg-${a.camp}`}>
|
<div key={a.id} className={`td-arg td-arg-${a.camp}`}>
|
||||||
@@ -148,7 +118,7 @@ export function TopicDetailPage() {
|
|||||||
<span className="muted">{a.agent_id}</span>
|
<span className="muted">{a.agent_id}</span>
|
||||||
<span className="spacer" />
|
<span className="spacer" />
|
||||||
<span className="muted" title={fmtTime(a.posted_at)}>
|
<span className="muted" title={fmtTime(a.posted_at)}>
|
||||||
{fmtRelative(a.posted_at)}
|
{fmtTime(a.posted_at)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="td-arg-content">{a.content}</div>
|
<div className="td-arg-content">{a.content}</div>
|
||||||
@@ -157,14 +127,34 @@ export function TopicDetailPage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{topic.status === 'completed' && (
|
{verdict && (
|
||||||
<div className="panel">
|
<>
|
||||||
<div className="panel-header">verdict</div>
|
<div className="panel">
|
||||||
<Link to={`/topics/${topic.id}/verdict`}>view verdict + rationale →</Link>
|
<div className="panel-header">verdict</div>
|
||||||
</div>
|
<pre className="vd-json">{JSON.stringify(verdict.verdict, null, 2)}</pre>
|
||||||
|
<div className="row muted" style={{ marginTop: 8, fontSize: '0.85em' }}>
|
||||||
|
<span>judge: {verdict.judge_agent_id}</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>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{err && <div className="err">refresh error: {err}</div>}
|
{!verdict && topic.status === 'completed' && (
|
||||||
|
<div className="panel td-empty">
|
||||||
|
no verdict was recorded (judge may have failed to submit)
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,15 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { listTopics } from '../api';
|
import { listTopics } from '../api';
|
||||||
import type { Topic, TopicStatus } from '../types';
|
import type { Topic } from '../types';
|
||||||
import { fmtTime, fmtRelative } from '../util';
|
import { fmtTime, fmtRelative } from '../util';
|
||||||
|
|
||||||
const STATUS_OPTIONS: Array<TopicStatus | 'all'> = [
|
// v0.3.1: this list only shows COMPLETED debates. Topics in earlier
|
||||||
'all',
|
// lifecycle states (signup_open / signup_closed / debating) are agent
|
||||||
'signup_open',
|
// internals — human readers care about the finished record (full
|
||||||
'signup_closed',
|
// transcript + verdict). Status filtering UI removed.
|
||||||
'debating',
|
|
||||||
'completed',
|
|
||||||
'cancelled',
|
|
||||||
];
|
|
||||||
|
|
||||||
export function TopicListPage() {
|
export function TopicListPage() {
|
||||||
const [status, setStatus] = useState<TopicStatus | 'all'>('all');
|
|
||||||
const [topics, setTopics] = useState<Topic[]>([]);
|
const [topics, setTopics] = useState<Topic[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [err, setErr] = useState<string | null>(null);
|
const [err, setErr] = useState<string | null>(null);
|
||||||
@@ -23,61 +18,44 @@ export function TopicListPage() {
|
|||||||
const ac = new AbortController();
|
const ac = new AbortController();
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setErr(null);
|
setErr(null);
|
||||||
listTopics({
|
listTopics({ status: 'completed', limit: 100, signal: ac.signal })
|
||||||
status: status === 'all' ? undefined : status,
|
|
||||||
limit: 100,
|
|
||||||
signal: ac.signal,
|
|
||||||
})
|
|
||||||
.then((r) => setTopics(r.topics))
|
.then((r) => setTopics(r.topics))
|
||||||
.catch((e: Error) => {
|
.catch((e: Error) => {
|
||||||
if (e.name !== 'AbortError') setErr(e.message);
|
if (e.name !== 'AbortError') setErr(e.message);
|
||||||
})
|
})
|
||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
return () => ac.abort();
|
return () => ac.abort();
|
||||||
}, [status]);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<span className="eyebrow">debates</span>
|
<span className="eyebrow">archive</span>
|
||||||
<h1>topics</h1>
|
<h1>completed debates</h1>
|
||||||
<p className="muted" style={{ marginBottom: 20 }}>
|
<p className="muted" style={{ marginBottom: 20 }}>
|
||||||
Public + visible-to-you topics. Filter by lifecycle status. Click a row for the
|
Each entry is a finished debate — click for the full transcript + verdict.
|
||||||
live transcript.
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="tl-filters">
|
<div className="tl-filters">
|
||||||
<label>
|
|
||||||
status
|
|
||||||
<select
|
|
||||||
value={status}
|
|
||||||
onChange={(e) => setStatus(e.target.value as TopicStatus | 'all')}
|
|
||||||
>
|
|
||||||
{STATUS_OPTIONS.map((s) => (
|
|
||||||
<option key={s} value={s}>
|
|
||||||
{s}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
<span className="spacer" />
|
<span className="spacer" />
|
||||||
<span className="muted">
|
<span className="muted">
|
||||||
{loading ? 'loading…' : `${topics.length} ${topics.length === 1 ? 'topic' : 'topics'}`}
|
{loading
|
||||||
|
? 'loading…'
|
||||||
|
: `${topics.length} ${topics.length === 1 ? 'debate' : 'debates'}`}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{err && <div className="err">{err}</div>}
|
{err && <div className="err">{err}</div>}
|
||||||
|
|
||||||
{!err && !loading && topics.length === 0 && (
|
{!err && !loading && topics.length === 0 && (
|
||||||
<div className="td-empty">no topics match this filter</div>
|
<div className="td-empty">no completed debates yet</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{topics.length > 0 && (
|
{topics.length > 0 && (
|
||||||
<table className="tl-table">
|
<table className="tl-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th style={{ width: '60%' }}>topic</th>
|
<th style={{ width: '60%' }}>debate</th>
|
||||||
<th>status</th>
|
<th>schema</th>
|
||||||
<th>signup close</th>
|
|
||||||
<th>debate window</th>
|
<th>debate window</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -90,14 +68,12 @@ export function TopicListPage() {
|
|||||||
</Link>
|
</Link>
|
||||||
<div className="tl-row-summary">{t.summary}</div>
|
<div className="tl-row-summary">{t.summary}</div>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td className="tl-row-time">{t.verdict_schema_id}</td>
|
||||||
<span className={`pill pill-${t.status}`}>{t.status}</span>
|
<td
|
||||||
</td>
|
className="tl-row-time"
|
||||||
<td className="tl-row-time" title={fmtTime(t.signup_close_at)}>
|
title={`${fmtTime(t.debate_start_at)} → ${fmtTime(t.debate_end_at)}`}
|
||||||
{fmtRelative(t.signup_close_at)}
|
>
|
||||||
</td>
|
{fmtRelative(t.debate_end_at)}
|
||||||
<td className="tl-row-time" title={`${fmtTime(t.debate_start_at)} → ${fmtTime(t.debate_end_at)}`}>
|
|
||||||
{fmtTime(t.debate_start_at)} → {fmtTime(t.debate_end_at)}
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
|
|||||||
Reference in New Issue
Block a user