profileShare

rasmusjy / roundtable

Read-only snapshot

No repository description.

main default branch 181 files Expires Sep 13, 2026, 9:06 AM
history-list.tsx 4,791 bytes
1 'use client';
2
3 import { Coins, MessageSquare, RotateCcw, Search, Trash2 } from 'lucide-react';
4 import Link from 'next/link';
5 import { useRouter } from 'next/navigation';
6 import { useMemo, useState } from 'react';
7 import { Badge } from '@/components/ui/badge';
8 import { Button } from '@/components/ui/button';
9 import { Input } from '@/components/ui/input';
10 import { toast } from '@/components/ui/sonner';
11 import type { DebateSummary } from '@/db/repositories';
12 import { displayNameForModel } from '@/core/types';
13 import { formatRelativeTime, formatUsd, truncate } from '@/lib/utils';
14
15 export function HistoryList({ debates }: { debates: DebateSummary[] }) {
16 const router = useRouter();
17 const [query, setQuery] = useState('');
18 const [deleting, setDeleting] = useState<string | null>(null);
19
20 const filtered = useMemo(() => {
21 const q = query.trim().toLowerCase();
22 return q ? debates.filter((d) => d.question.toLowerCase().includes(q)) : debates;
23 }, [debates, query]);
24
25 const remove = async (id: string) => {
26 setDeleting(id);
27 try {
28 const res = await fetch(`/api/debates/${id}`, { method: 'DELETE' });
29 if (!res.ok) throw new Error('Delete request failed');
30 toast.success('Question deleted');
31 router.refresh();
32 } catch {
33 toast.error('Could not delete');
34 } finally {
35 setDeleting(null);
36 }
37 };
38
39 return (
40 <div className="space-y-4">
41 <div className="flex items-center gap-2 rounded-lg border px-3">
42 <Search className="h-4 w-4 text-muted-foreground" />
43 <Input
44 value={query}
45 onChange={(e) => setQuery(e.target.value)}
46 placeholder="Search your debates..."
47 className="h-10 border-0 shadow-none focus-visible:ring-0"
48 />
49 </div>
50
51 {filtered.length === 0 ? (
52 <div className="rounded-xl border border-dashed p-10 text-center text-sm text-muted-foreground">
53 {debates.length === 0
54 ? 'No questions yet. Ask one to see it here.'
55 : 'No questions match your search.'}
56 </div>
57 ) : (
58 <ul className="space-y-2">
59 {filtered.map((d) => (
60 <li
61 key={d.id}
62 className="group flex items-center gap-3 rounded-xl border bg-card p-4 transition-colors hover:border-primary/40"
63 >
64 <Link href={`/debate/${d.id}`} className="min-w-0 flex-1">
65 <div className="flex items-center gap-2">
66 <StatusDot status={d.status} />
67 <span className="truncate font-medium">{truncate(d.question, 110)}</span>
68 </div>
69 <div className="mt-1 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-muted-foreground">
70 <span className="flex items-center gap-1">
71 <MessageSquare className="h-3 w-3" /> {d.models.length} models ·{' '}
72 {d.roundsCompleted} rounds
73 </span>
74 <span className="flex items-center gap-1">
75 <Coins className="h-3 w-3" /> {formatUsd(d.totalCostUsd)}
76 </span>
77 <span>{formatRelativeTime(d.createdAt)}</span>
78 <span className="hidden truncate sm:inline">
79 {d.models.map(displayNameForModel).join(' · ')}
80 </span>
81 </div>
82 </Link>
83 {d.shareToken && <Badge variant="secondary">shared</Badge>}
84 <Button
85 variant="ghost"
86 size="icon"
87 className="transition-opacity sm:opacity-0 sm:group-focus-within:opacity-100 sm:group-hover:opacity-100"
88 asChild
89 aria-label="Ask this question again with the same models"
90 >
91 <Link href={`/debate?from=${d.id}`}>
92 <RotateCcw className="h-4 w-4" />
93 </Link>
94 </Button>
95 <Button
96 variant="ghost"
97 size="icon"
98 className="transition-opacity sm:opacity-0 sm:group-focus-within:opacity-100 sm:group-hover:opacity-100"
99 disabled={deleting !== null}
100 onClick={() => remove(d.id)}
101 aria-label="Delete question"
102 >
103 <Trash2 className="h-4 w-4" />
104 </Button>
105 </li>
106 ))}
107 </ul>
108 )}
109 </div>
110 );
111 }
112
113 function StatusDot({ status }: { status: DebateSummary['status'] }) {
114 const color =
115 status === 'completed'
116 ? 'bg-emerald-500'
117 : status === 'running'
118 ? 'bg-amber-500'
119 : status === 'failed'
120 ? 'bg-red-500'
121 : 'bg-muted-foreground';
122 return <span className={`h-2 w-2 shrink-0 rounded-full ${color}`} />;
123 }
124