'use client'; import { useCallback, useEffect, useRef, useState } from 'react'; import Link from 'next/link'; import Sidebar from '@/components/Sidebar'; import MessageContent from '@/components/MessageContent'; import { Sparkles, RefreshCw, Calendar } from 'lucide-react'; type Topic = { id: number; date: string; title: string; summary: string; message_count: number; group_count: number; }; type TopicMessage = { chatroom_id: string; chat_name: string; local_id: number; sender: string; content: string; time: string; timestamp: number; type: string; score: number; }; function localToday(): string { const d = new Date(); const y = d.getFullYear(); const m = String(d.getMonth() + 1).padStart(2, '0'); const day = String(d.getDate()).padStart(2, '0'); return `${y}-${m}-${day}`; } export default function TopicsPage() { const [date, setDate] = useState(() => localToday()); const [topics, setTopics] = useState([]); const [selected, setSelected] = useState(null); const [detail, setDetail] = useState<{ topic: Topic; messages: TopicMessage[] } | null>(null); const [busy, setBusy] = useState(false); const [info, setInfo] = useState(undefined); const autoBuildDates = useRef(new Set()); const reload = useCallback(async () => { try { const r = await fetch(`/api/topics?date=${date}`); const j = await r.json(); if (j.ok) setTopics(j.topics); } catch {} }, [date]); useEffect(() => { let cancelled = false; (async () => { try { const r = await fetch(`/api/topics?date=${date}`); const j = await r.json(); if (!cancelled && j.ok) setTopics(j.topics); } catch {} })(); return () => { cancelled = true; }; }, [date]); useEffect(() => { if (!selected) { return; } let cancelled = false; (async () => { const r = await fetch(`/api/topics/${selected}`); const j = await r.json(); if (!cancelled && j.ok) { setDetail({ topic: { id: j.id, date: j.date, title: j.title, summary: j.summary, message_count: j.message_count, group_count: j.group_count, }, messages: j.messages, }); } })(); return () => { cancelled = true; }; }, [selected]); const selectedDetail = selected ? detail : null; const build = useCallback(async () => { setBusy(true); setInfo('启动 Codex CLI 话题聚合…'); try { const r = await fetch('/api/topics/build', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ date }), }); if (!r.ok || !r.body) { setInfo('构建失败'); setBusy(false); return; } const reader = r.body.getReader(); const dec = new TextDecoder(); let buf = ''; while (true) { const { value, done } = await reader.read(); if (done) break; buf += dec.decode(value, { stream: true }); let nl; while ((nl = buf.indexOf('\n\n')) !== -1) { const chunk = buf.slice(0, nl).trim(); buf = buf.slice(nl + 2); if (!chunk.startsWith('data:')) continue; try { const evt = JSON.parse(chunk.slice(5).trim()); if (evt.type === 'start') { setInfo(`${date} · 开始构建话题…`); } else if (evt.type === 'load') { setInfo(evt.message ?? '加载当日消息…'); } else if (evt.type === 'llm' && evt.done !== undefined) { setInfo(evt.message ?? `Codex 聚合 ${evt.done}/${evt.total}`); } else if (evt.type === 'save' && evt.done !== undefined) { setInfo(`保存话题 ${evt.done}/${evt.total} · ${evt.message ?? ''}`); } else if (evt.type === 'finished' || evt.type === 'done') { setInfo(`完成 · ${evt.topics ?? evt.count ?? 0} 个话题`); } else if (evt.type === 'error') { setInfo('错误:' + evt.error); } else if (evt.message) { setInfo(evt.message); } } catch {} } } } catch (e) { setInfo('错误:' + (e instanceof Error ? e.message : 'unknown')); } finally { setBusy(false); reload(); } }, [date, reload]); useEffect(() => { if (busy || topics.length > 0 || autoBuildDates.current.has(date)) return; autoBuildDates.current.add(date); build(); }, [build, busy, date, topics.length]); return (
Cross-Group Topics
话题雷达 · 跨群聚合
{info ?? `${date} · ${topics.length} 个话题`}
setDate(e.target.value)} className="theme-date-input bg-transparent text-[12px] outline-none" />
{topics.length === 0 ? (
{busy ? '正在自动构建当日话题…' : '当日暂无可聚合话题'}
) : (
{topics.map((t) => ( ))}
)}
{!selectedDetail ? (
左侧选一个话题查看跨群讨论
) : (
{selectedDetail.topic.title}
{selectedDetail.topic.summary && (
{selectedDetail.topic.summary}
)}
消息:{selectedDetail.topic.message_count} 跨群:{selectedDetail.topic.group_count} 日期:{selectedDetail.topic.date}
{selectedDetail.messages.map((m) => (
{m.chat_name} {' · '} {m.sender} {m.time?.slice(11) ?? ''}
))}
)}
); }