"use client"; import React, { useEffect, useState, useCallback, useRef } from "react"; import Sidebar from "../components/Sidebar"; import TopBar from "../components/TopBar"; import { fetchTimeline, fetchGroups, raiseJiraTicket, TimelineSignal, Group, formatRelativeTime, getSeverityColor, getSignalIcon, parseMetaList, } from "../lib/api"; // ─── Helpers ────────────────────────────────────────────────────────────────── function formatDate(iso: string): string { if (!iso) return "—"; try { return new Date(iso).toLocaleString("en-GB", { day: "2-digit", month: "short", year: "numeric", hour: "2-digit", minute: "2-digit", }); } catch { return iso; } } function formatDateGroup(iso: string): string { if (!iso) return "Unknown Date"; try { return new Date(iso).toLocaleDateString("en-GB", { weekday: "long", day: "numeric", month: "long", year: "numeric", }); } catch { return iso.slice(0, 10); } } function getDay(iso: string): string { try { return new Date(iso).toISOString().slice(0, 10); } catch { return ""; } } const SEVERITY_OPTIONS = ["low", "medium", "high", "critical"]; const LENS_OPTIONS = ["dev", "product", "client", "community", "meet"]; const SIGNAL_TYPE_OPTIONS = [ "architecture_decision", "tech_debt", "blocker", "risk", "decision", "action_item", "feature_request", "promise", "scope_creep", "knowledge_gap", "sentiment_spike", "recurring_bug", "meet_decision", "meet_action_item", "meet_blocker", "meet_risk", "meet_open_q", ]; // ─── Signal Card ────────────────────────────────────────────────────────────── function SignalCard({ signal, onRaiseJira, raisingId, raisedIds, }: { signal: TimelineSignal; onRaiseJira: (sig: TimelineSignal) => void; raisingId: string | null; raisedIds: Set; }) { const meta = signal.metadata; const color = getSeverityColor(meta.severity); const icon = getSignalIcon(meta.type); const entities = parseMetaList(meta.entities); const keywords = parseMetaList(meta.keywords); const isRaised = raisedIds.has(signal.id); const isRaising = raisingId === signal.id; const raiseable = [ "tech_debt", "recurring_bug", "architecture_decision", "blocker", "risk", "feature_request", "priority_conflict", "promise", "scope_creep", "meet_action_item", "meet_blocker", "meet_risk", "meet_decision", "decision", "action_item", ].includes(meta.type); return (
{icon}
{meta.type.replace(/_/g, " ")} {meta.lens} {signal.group_name && ( {signal.group_name} )}

{meta.summary || signal.document}

{meta.raw_quote && meta.raw_quote !== meta.summary && meta.type !== "meet_chunk_raw" && (

"{meta.raw_quote.slice(0, 160)}"

)}
{formatRelativeTime(meta.timestamp)} {raiseable && !isRaised && ( )} {isRaised && ( check_circle Raised )}
{(entities.length > 0 || keywords.length > 0) && (
{entities.slice(0, 4).map((e, i) => ( {e} ))} {keywords.slice(0, 4).map((k, i) => ( {k} ))}
)}
); } // ─── Timeline View ───────────────────────────────────────────────────────────── function TimelineView({ signals, onRaiseJira, raisingId, raisedIds, }: { signals: TimelineSignal[]; onRaiseJira: (sig: TimelineSignal) => void; raisingId: string | null; raisedIds: Set; }) { // Group by day const byDay: Map = new Map(); for (const sig of signals) { const day = getDay(sig.metadata.timestamp); if (!byDay.has(day)) byDay.set(day, []); byDay.get(day)!.push(sig); } if (signals.length === 0) { return (
chat_bubble

No signals found

Signals appear as your Telegram groups generate activity

); } return (
{Array.from(byDay.entries()).map(([day, daySigs]) => (
{/* Day separator */}
calendar_today {formatDateGroup(day)} · {daySigs.length} signals
{daySigs.map((sig) => ( ))}
))}
); } // ─── Table View ──────────────────────────────────────────────────────────────── function TableView({ signals, sortKey, sortAsc, onSort, onRaiseJira, raisingId, raisedIds, }: { signals: TimelineSignal[]; sortKey: string; sortAsc: boolean; onSort: (key: string) => void; onRaiseJira: (sig: TimelineSignal) => void; raisingId: string | null; raisedIds: Set; }) { const SortIcon = ({ k }: { k: string }) => ( {sortKey !== k ? "unfold_more" : sortAsc ? "expand_less" : "expand_more"} ); if (signals.length === 0) { return (
table_rows

No signals

); } return (
{[ { label: "Type", key: "type" }, { label: "Summary", key: null }, { label: "Severity", key: "severity" }, { label: "Lens", key: "lens" }, { label: "Group", key: "group_name" }, { label: "Time", key: "timestamp" }, { label: "", key: null }, ].map(({ label, key }) => ( ))} {signals.map((sig) => { const meta = sig.metadata; const color = getSeverityColor(meta.severity); const isRaised = raisedIds.has(sig.id); const isRaising = raisingId === sig.id; const raiseable = [ "tech_debt", "recurring_bug", "architecture_decision", "blocker", "risk", "feature_request", "priority_conflict", "promise", "scope_creep", "meet_action_item", "meet_blocker", "meet_risk", "meet_decision", "decision", "action_item", ].includes(meta.type); return ( ); })}
onSort(key) : undefined} >
{label} {key && }
{meta.type.replace(/_/g, " ")}

{meta.summary || sig.document}

{meta.severity} {meta.lens} {sig.group_name || meta.group_id} {formatRelativeTime(meta.timestamp)} {raiseable && !isRaised && ( )} {isRaised && ( check_circle )}
); } // ─── Main Page ───────────────────────────────────────────────────────────────── export default function ChatsPage() { const [signals, setSignals] = useState([]); const [total, setTotal] = useState(0); const [truncated, setTruncated] = useState(false); const [groups, setGroups] = useState([]); const [loading, setLoading] = useState(true); const [viewMode, setViewMode] = useState<"timeline" | "table">("timeline"); const [raisingId, setRaisingId] = useState(null); const [raisedIds, setRaisedIds] = useState>(new Set()); const [raiseMsg, setRaiseMsg] = useState(""); // Filters const [filterGroup, setFilterGroup] = useState(""); const [filterSeverity, setFilterSeverity] = useState(""); const [filterLens, setFilterLens] = useState(""); const [filterType, setFilterType] = useState(""); const [dateFrom, setDateFrom] = useState(""); const [dateTo, setDateTo] = useState(""); const [search, setSearch] = useState(""); // Sort (table view) const [sortKey, setSortKey] = useState("timestamp"); const [sortAsc, setSortAsc] = useState(false); const load = useCallback(async () => { setLoading(true); try { const [result, grps] = await Promise.all([ fetchTimeline({ group_id: filterGroup || undefined, severity: filterSeverity || undefined, lens: filterLens || undefined, signal_type: filterType || undefined, date_from: dateFrom || undefined, date_to: dateTo || undefined, limit: 300, }), fetchGroups(), ]); setSignals(result.signals); setTotal(result.total); setTruncated(result.truncated); setGroups(grps); } catch (e) { console.error(e); } finally { setLoading(false); } }, [filterGroup, filterSeverity, filterLens, filterType, dateFrom, dateTo]); useEffect(() => { load(); }, [load]); const handleRaiseJira = async (sig: TimelineSignal) => { setRaisingId(sig.id); try { const result = await raiseJiraTicket(sig.id, sig.metadata.group_id); if (result.ok && result.key) { setRaisedIds((prev) => new Set([...prev, sig.id])); setRaiseMsg(`Ticket ${result.key} raised!`); setTimeout(() => setRaiseMsg(""), 4000); } else { setRaiseMsg(result.reason === "already_raised" ? "Already raised" : (result.error || "Failed to raise ticket")); setTimeout(() => setRaiseMsg(""), 4000); } } catch (e) { console.error(e); } finally { setRaisingId(null); } }; // Client-side search filter const filteredSignals = signals.filter((s) => { if (!search) return true; const q = search.toLowerCase(); const meta = s.metadata; return ( (meta.summary || "").toLowerCase().includes(q) || (meta.type || "").toLowerCase().includes(q) || (meta.raw_quote || "").toLowerCase().includes(q) || (s.group_name || "").toLowerCase().includes(q) ); }); // Sort for table view const sortedSignals = [...filteredSignals].sort((a, b) => { const metaA = a.metadata, metaB = b.metadata; let cmp = 0; if (sortKey === "timestamp") cmp = metaA.timestamp.localeCompare(metaB.timestamp); else if (sortKey === "severity") { const rank = { critical: 4, high: 3, medium: 2, low: 1 } as Record; cmp = (rank[metaA.severity] || 0) - (rank[metaB.severity] || 0); } else if (sortKey === "type") cmp = metaA.type.localeCompare(metaB.type); else if (sortKey === "lens") cmp = metaA.lens.localeCompare(metaB.lens); else if (sortKey === "group_name") cmp = (a.group_name || "").localeCompare(b.group_name || ""); return sortAsc ? cmp : -cmp; }); const toggleSort = (key: string) => { if (sortKey === key) setSortAsc((a) => !a); else { setSortKey(key); setSortAsc(false); } }; const exportCSV = () => { const header = ["Type", "Summary", "Severity", "Lens", "Group", "Timestamp"].join(","); const rows = filteredSignals.map((s) => { const m = s.metadata; return [ m.type, `"${(m.summary || "").replace(/"/g, '""').slice(0, 200)}"`, m.severity, m.lens, s.group_name || m.group_id, m.timestamp, ].join(","); }); const blob = new Blob([[header, ...rows].join("\n")], { type: "text/csv" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = "chat_signals.csv"; a.click(); URL.revokeObjectURL(url); }; const activeFilters = [filterGroup, filterSeverity, filterLens, filterType, dateFrom, dateTo, search].filter(Boolean).length; const criticalCount = signals.filter(s => s.metadata.severity === "critical").length; const highCount = signals.filter(s => s.metadata.severity === "high").length; const uniqueGroups = new Set(signals.map(s => s.metadata.group_id)).size; return (
{/* Header */}
chat

Chat History

Signal timeline · Cross-group intelligence

{raiseMsg && ( {raiseMsg} )}
{(["timeline", "table"] as const).map((mode) => ( ))}
{/* Stats */}
{[ { label: "Total Signals", value: total, icon: "sensors", color: "#A78BFA" }, { label: "Critical", value: criticalCount, icon: "error", color: "#EF4444" }, { label: "High Priority", value: highCount, icon: "warning", color: "#F97316" }, { label: "Active Groups", value: uniqueGroups, icon: "group", color: "#60A5FA" }, ].map((s) => (
{s.icon}

{s.value}

{s.label}

))}
{/* Filters */}
filter_alt Filters {activeFilters > 0 && ( {activeFilters} active )}
{activeFilters > 0 && ( )}
{/* Search */}
search setSearch(e.target.value)} className="bg-[#0C0C0E] border border-white/10 rounded-lg pl-8 pr-3 py-2 text-[12px] text-zinc-200 placeholder-zinc-600 focus:outline-none focus:border-[#A78BFA]/50 w-44" />
{/* Group */} {/* Severity */} {/* Lens */} {/* Signal type */} {/* Date range */}
From setDateFrom(e.target.value ? new Date(e.target.value).toISOString() : "")} className="bg-[#0C0C0E] border border-white/10 rounded-lg px-2 py-2 text-[12px] text-zinc-300 focus:outline-none focus:border-[#A78BFA]/50" /> To setDateTo(e.target.value ? new Date(e.target.value).toISOString() : "")} className="bg-[#0C0C0E] border border-white/10 rounded-lg px-2 py-2 text-[12px] text-zinc-300 focus:outline-none focus:border-[#A78BFA]/50" />
{/* Count line */}

Showing {filteredSignals.length} {total > filteredSignals.length && ` of ${total}`} signals {truncated && · Showing latest 300 — apply filters to narrow down}

{/* Content */} {loading ? (

Loading signals...

) : viewMode === "timeline" ? ( ) : (
)}
); }