mirror of
https://github.com/arkorty/B.Tech-Project-III.git
synced 2026-04-19 20:51:49 +00:00
init
This commit is contained in:
548
thirdeye/dashboard/app/knowledge-base/KnowledgeBrowser.tsx
Normal file
548
thirdeye/dashboard/app/knowledge-base/KnowledgeBrowser.tsx
Normal file
@@ -0,0 +1,548 @@
|
||||
"use client";
|
||||
import React, { useState, useEffect, useCallback, useRef } from "react";
|
||||
import {
|
||||
fetchGroups,
|
||||
fetchKnowledgeBrowse,
|
||||
Group,
|
||||
Signal,
|
||||
KnowledgeBrowseResponse,
|
||||
KnowledgeDayEntry,
|
||||
KnowledgeTopicSummary,
|
||||
parseMetaList,
|
||||
formatRelativeTime,
|
||||
} from "../lib/api";
|
||||
|
||||
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
const LENS_COLOR: Record<string, string> = {
|
||||
dev: "#00daf3",
|
||||
product: "#A78BFA",
|
||||
client: "#fbbf24",
|
||||
community: "#34d399",
|
||||
meet: "#f87171",
|
||||
jira: "#fb923c",
|
||||
};
|
||||
|
||||
const SEVERITY_STYLE: Record<string, { color: string; bg: string }> = {
|
||||
critical: { color: "#ff6f78", bg: "rgba(255,111,120,0.12)" },
|
||||
high: { color: "#ffb300", bg: "rgba(255,179,0,0.12)" },
|
||||
medium: { color: "#A78BFA", bg: "rgba(167,139,250,0.12)" },
|
||||
low: { color: "#6B7280", bg: "rgba(107,114,128,0.10)" },
|
||||
};
|
||||
|
||||
const TYPE_LABEL: Record<string, string> = {
|
||||
architecture_decision: "Architecture",
|
||||
tech_debt: "Tech Debt",
|
||||
knowledge_silo_evidence: "Knowledge Silo",
|
||||
recurring_bug: "Bug",
|
||||
stack_decision: "Stack",
|
||||
deployment_risk: "Deploy Risk",
|
||||
workaround: "Workaround",
|
||||
delivery_commitment: "Commitment",
|
||||
feature_request: "Feature",
|
||||
user_pain_point: "Pain Point",
|
||||
roadmap_drift: "Roadmap",
|
||||
priority_conflict: "Conflict",
|
||||
metric_mention: "Metric",
|
||||
user_quote: "Quote",
|
||||
competitor_intel: "Competitor",
|
||||
promise: "Promise",
|
||||
scope_creep: "Scope Creep",
|
||||
sentiment_signal: "Sentiment",
|
||||
unanswered_request: "Unanswered",
|
||||
satisfaction: "Satisfaction",
|
||||
escalation_risk: "Escalation",
|
||||
client_decision: "Client Decision",
|
||||
meet_decision: "Decision",
|
||||
meet_action_item: "Action Item",
|
||||
meet_blocker: "Blocker",
|
||||
meet_risk: "Risk",
|
||||
meet_summary: "Summary",
|
||||
};
|
||||
|
||||
function typeLabel(t: string) {
|
||||
return TYPE_LABEL[t] ?? t.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
||||
}
|
||||
|
||||
function fmtDate(iso: string): string {
|
||||
if (!iso || iso === "unknown") return "Unknown";
|
||||
const d = new Date(iso + (iso.includes("T") ? "" : "T00:00:00"));
|
||||
const today = new Date();
|
||||
const yesterday = new Date(today);
|
||||
yesterday.setDate(today.getDate() - 1);
|
||||
const opts: Intl.DateTimeFormatOptions = { month: "short", day: "numeric", year: "numeric" };
|
||||
if (d.toDateString() === today.toDateString()) return "Today · " + d.toLocaleDateString("en-US", { month: "short", day: "numeric" });
|
||||
if (d.toDateString() === yesterday.toDateString()) return "Yesterday · " + d.toLocaleDateString("en-US", { month: "short", day: "numeric" });
|
||||
return d.toLocaleDateString("en-US", opts);
|
||||
}
|
||||
|
||||
function fmtTime(iso: string): string {
|
||||
if (!iso) return "";
|
||||
try {
|
||||
return new Date(iso).toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit", hour12: true });
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Signal Card ─────────────────────────────────────────────────────────────
|
||||
|
||||
function SignalCard({ signal, expanded, onToggle }: {
|
||||
signal: Signal;
|
||||
expanded: boolean;
|
||||
onToggle: () => void;
|
||||
}) {
|
||||
const meta = signal.metadata;
|
||||
const lens = meta.lens || "dev";
|
||||
const lensColor = LENS_COLOR[lens] ?? "#A78BFA";
|
||||
const sev = SEVERITY_STYLE[meta.severity] ?? SEVERITY_STYLE.low;
|
||||
const entities = parseMetaList(meta.entities).slice(0, 3);
|
||||
const keywords = parseMetaList(meta.keywords).slice(0, 3);
|
||||
const summary = meta.summary || signal.document || "";
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={onToggle}
|
||||
className="w-full text-left rounded-xl border transition-all duration-200 group"
|
||||
style={{
|
||||
backgroundColor: expanded ? "rgba(167,139,250,0.06)" : "rgba(11,8,18,0.7)",
|
||||
borderColor: expanded ? "rgba(167,139,250,0.25)" : "rgba(167,139,250,0.08)",
|
||||
}}
|
||||
>
|
||||
{/* Top row */}
|
||||
<div className="flex items-center justify-between px-3 pt-2.5 pb-1 gap-2">
|
||||
<span
|
||||
className="text-[9px] font-mono font-bold px-2 py-0.5 rounded-md tracking-wider uppercase shrink-0"
|
||||
style={{ color: lensColor, backgroundColor: `${lensColor}18`, border: `1px solid ${lensColor}28` }}
|
||||
>
|
||||
{typeLabel(meta.type)}
|
||||
</span>
|
||||
<div className="flex items-center gap-1.5 shrink-0">
|
||||
<span
|
||||
className="text-[8px] font-mono font-semibold px-1.5 py-0.5 rounded-md uppercase tracking-wider"
|
||||
style={{ color: sev.color, backgroundColor: sev.bg }}
|
||||
>
|
||||
{meta.severity}
|
||||
</span>
|
||||
<span className="text-[8px] text-[#6B7280] font-mono">{fmtTime(meta.timestamp)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Summary */}
|
||||
<p
|
||||
className="px-3 pb-2 text-[11px] text-[#C4B5F4] leading-relaxed font-medium"
|
||||
style={{ display: "-webkit-box", WebkitLineClamp: expanded ? 999 : 2, WebkitBoxOrient: "vertical", overflow: "hidden" }}
|
||||
>
|
||||
{summary}
|
||||
</p>
|
||||
|
||||
{/* Entities + raw quote on expand */}
|
||||
{expanded && (
|
||||
<div className="px-3 pb-2.5 space-y-2">
|
||||
{meta.raw_quote && (
|
||||
<p className="text-[10px] text-[#8B7BB1] italic border-l-2 pl-2 leading-relaxed" style={{ borderColor: lensColor + "60" }}>
|
||||
"{meta.raw_quote}"
|
||||
</p>
|
||||
)}
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{entities.map((e, i) => (
|
||||
<span key={i} className="text-[9px] font-mono text-[#A78BFA] bg-[#2D1B4E]/50 px-1.5 py-0.5 rounded border border-[#A78BFA]/15">
|
||||
{e}
|
||||
</span>
|
||||
))}
|
||||
{keywords.map((k, i) => (
|
||||
<span key={i} className="text-[9px] font-mono text-[#6B7280] bg-[#1A1A2E]/50 px-1.5 py-0.5 rounded border border-white/5">
|
||||
#{k}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Entities preview (collapsed) */}
|
||||
{!expanded && entities.length > 0 && (
|
||||
<div className="px-3 pb-2.5 flex flex-wrap gap-1">
|
||||
{entities.map((e, i) => (
|
||||
<span key={i} className="text-[9px] font-mono text-[#8B7BB1]">{e}</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Day Section ─────────────────────────────────────────────────────────────
|
||||
|
||||
function DaySection({ entry, activeTopic }: { entry: KnowledgeDayEntry; activeTopic: string | null }) {
|
||||
const [expandedId, setExpandedId] = useState<string | null>(null);
|
||||
|
||||
const visibleSignals = activeTopic
|
||||
? entry.signals.filter((s) => {
|
||||
try {
|
||||
const kws: string[] = JSON.parse(s.metadata.keywords || "[]");
|
||||
return kws.map((k) => k.toLowerCase().trim()).includes(activeTopic.toLowerCase()) ||
|
||||
s.metadata.type.replace(/_/g, " ").toLowerCase() === activeTopic.toLowerCase();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
})
|
||||
: entry.signals;
|
||||
|
||||
if (visibleSignals.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="mb-4">
|
||||
{/* Day header */}
|
||||
<div className="flex items-center gap-2 mb-2 sticky top-0 z-10 py-1"
|
||||
style={{ background: "linear-gradient(to bottom, #09090B, transparent)" }}>
|
||||
<div className="h-px w-2 bg-[#A78BFA]/30" />
|
||||
<span className="text-[9px] font-mono font-bold text-[#A78BFA]/70 uppercase tracking-[0.2em] shrink-0">
|
||||
{fmtDate(entry.date)}
|
||||
</span>
|
||||
<div className="flex-1 h-px bg-[#A78BFA]/10" />
|
||||
<span className="text-[8px] font-mono text-[#6B7280] shrink-0">{visibleSignals.length} signals</span>
|
||||
</div>
|
||||
|
||||
{/* Topic chips for this day */}
|
||||
{entry.topics.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1 mb-2 px-0.5">
|
||||
{entry.topics.slice(0, 5).map((t, i) => (
|
||||
<span key={i} className="text-[8px] font-mono text-[#6B7280] bg-[#1A1A2E]/60 px-1.5 py-0.5 rounded-md border border-white/5">
|
||||
{t}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Signal cards */}
|
||||
<div className="space-y-1.5">
|
||||
{visibleSignals.map((sig) => (
|
||||
<SignalCard
|
||||
key={sig.id}
|
||||
signal={sig}
|
||||
expanded={expandedId === sig.id}
|
||||
onToggle={() => setExpandedId(expandedId === sig.id ? null : sig.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Topic Chips ─────────────────────────────────────────────────────────────
|
||||
|
||||
function TopicChips({
|
||||
topics,
|
||||
activeTopic,
|
||||
onSelect,
|
||||
}: {
|
||||
topics: KnowledgeTopicSummary[];
|
||||
activeTopic: string | null;
|
||||
onSelect: (t: string | null) => void;
|
||||
}) {
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
return (
|
||||
<div ref={scrollRef} className="flex gap-1.5 overflow-x-auto pb-1" style={{ scrollbarWidth: "none" }}>
|
||||
<button
|
||||
onClick={() => onSelect(null)}
|
||||
className="shrink-0 text-[9px] font-mono font-bold px-2.5 py-1 rounded-lg border transition-all uppercase tracking-wider"
|
||||
style={{
|
||||
color: activeTopic === null ? "#09090B" : "#A78BFA",
|
||||
backgroundColor: activeTopic === null ? "#A78BFA" : "rgba(167,139,250,0.1)",
|
||||
borderColor: activeTopic === null ? "#A78BFA" : "rgba(167,139,250,0.25)",
|
||||
}}
|
||||
>
|
||||
All
|
||||
</button>
|
||||
{topics.map((t) => (
|
||||
<button
|
||||
key={t.name}
|
||||
onClick={() => onSelect(activeTopic === t.name ? null : t.name)}
|
||||
className="shrink-0 flex items-center gap-1 text-[9px] font-mono px-2 py-1 rounded-lg border transition-all"
|
||||
style={{
|
||||
color: activeTopic === t.name ? "#09090B" : "#8B7BB1",
|
||||
backgroundColor: activeTopic === t.name ? "#A78BFA" : "rgba(167,139,250,0.06)",
|
||||
borderColor: activeTopic === t.name ? "#A78BFA" : "rgba(167,139,250,0.15)",
|
||||
}}
|
||||
>
|
||||
<span className="truncate max-w-[80px]">{t.name}</span>
|
||||
<span
|
||||
className="shrink-0 text-[8px] font-bold rounded-full w-4 h-4 flex items-center justify-center"
|
||||
style={{
|
||||
color: activeTopic === t.name ? "#09090B" : "#A78BFA",
|
||||
backgroundColor: activeTopic === t.name ? "rgba(0,0,0,0.2)" : "rgba(167,139,250,0.15)",
|
||||
}}
|
||||
>
|
||||
{t.signal_count > 99 ? "99+" : t.signal_count}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Main Component ───────────────────────────────────────────────────────────
|
||||
|
||||
export default function KnowledgeBrowser() {
|
||||
const [groups, setGroups] = useState<Group[]>([]);
|
||||
const [selectedGroup, setSelectedGroup] = useState<string>("");
|
||||
const [browseData, setBrowseData] = useState<KnowledgeBrowseResponse | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [activeTopic, setActiveTopic] = useState<string | null>(null);
|
||||
const [dateFrom, setDateFrom] = useState("");
|
||||
const [dateTo, setDateTo] = useState("");
|
||||
const timelineRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Load groups
|
||||
useEffect(() => {
|
||||
fetchGroups().then((grps) => {
|
||||
setGroups(grps);
|
||||
if (grps.length > 0) setSelectedGroup(grps[0].group_id);
|
||||
}).catch(() => {});
|
||||
}, []);
|
||||
|
||||
// Load browse data when group or date filters change
|
||||
const loadBrowse = useCallback(async (groupId: string, from: string, to: string) => {
|
||||
if (!groupId) return;
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setActiveTopic(null);
|
||||
try {
|
||||
const data = await fetchKnowledgeBrowse(groupId, {
|
||||
dateFrom: from || undefined,
|
||||
dateTo: to || undefined,
|
||||
});
|
||||
setBrowseData(data);
|
||||
} catch {
|
||||
setError("Backend unavailable — start the ThirdEye server to browse knowledge.");
|
||||
setBrowseData(null);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedGroup) loadBrowse(selectedGroup, dateFrom, dateTo);
|
||||
}, [selectedGroup, loadBrowse]);
|
||||
|
||||
const handleApplyDates = () => {
|
||||
if (selectedGroup) loadBrowse(selectedGroup, dateFrom, dateTo);
|
||||
};
|
||||
|
||||
const handleClearDates = () => {
|
||||
setDateFrom("");
|
||||
setDateTo("");
|
||||
if (selectedGroup) loadBrowse(selectedGroup, "", "");
|
||||
};
|
||||
|
||||
const selectedGroupData = groups.find((g) => g.group_id === selectedGroup);
|
||||
|
||||
// Count visible signals for active topic across all days
|
||||
const visibleCount = browseData
|
||||
? activeTopic
|
||||
? browseData.timeline.reduce((acc, day) => {
|
||||
const cnt = day.signals.filter((s) => {
|
||||
try {
|
||||
const kws: string[] = JSON.parse(s.metadata.keywords || "[]");
|
||||
return kws.map((k) => k.toLowerCase().trim()).includes(activeTopic.toLowerCase()) ||
|
||||
s.metadata.type.replace(/_/g, " ").toLowerCase() === activeTopic.toLowerCase();
|
||||
} catch { return false; }
|
||||
}).length;
|
||||
return acc + cnt;
|
||||
}, 0)
|
||||
: browseData.total_signals
|
||||
: 0;
|
||||
|
||||
return (
|
||||
<div className="w-full flex flex-col gap-3 animate-fade-in-right">
|
||||
|
||||
{/* ── Group Selector ──────────────────────────────────────────────── */}
|
||||
<div className="px-4 py-3 rounded-[1.25rem] bg-[#110D1A]/90 backdrop-blur-2xl border border-[#A78BFA]/10 shadow-[0_8px_32px_rgba(0,0,0,0.5)]">
|
||||
<p className="text-[9px] font-mono text-[#A78BFA]/60 uppercase tracking-widest mb-1.5">
|
||||
Knowledge Source
|
||||
</p>
|
||||
{groups.length === 0 ? (
|
||||
<p className="text-[11px] text-[#6B7280] font-mono">Loading groups…</p>
|
||||
) : (
|
||||
<select
|
||||
value={selectedGroup}
|
||||
onChange={(e) => setSelectedGroup(e.target.value)}
|
||||
className="w-full bg-transparent text-[#E9D9FF] text-[12px] font-semibold focus:outline-none cursor-pointer"
|
||||
style={{ appearance: "none" }}
|
||||
>
|
||||
{groups.map((g) => (
|
||||
<option key={g.group_id} value={g.group_id} style={{ backgroundColor: "#110D1A" }}>
|
||||
{g.group_name} — {g.signal_count} signals
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
{selectedGroupData && (
|
||||
<p className="text-[9px] font-mono text-[#6B7280] mt-1 uppercase tracking-wider">
|
||||
lens: {selectedGroupData.lens || "unknown"}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* ── Date Range Filter ───────────────────────────────────────────── */}
|
||||
<div className="px-4 py-3 rounded-[1.25rem] bg-[#110D1A]/90 backdrop-blur-2xl border border-[#A78BFA]/10 shadow-[0_8px_32px_rgba(0,0,0,0.5)]">
|
||||
<p className="text-[9px] font-mono text-[#A78BFA]/60 uppercase tracking-widest mb-2">
|
||||
Date Range
|
||||
</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="date"
|
||||
value={dateFrom}
|
||||
onChange={(e) => setDateFrom(e.target.value)}
|
||||
className="flex-1 bg-[#0C0814]/70 border border-[#A78BFA]/15 rounded-lg text-[10px] font-mono text-[#C4B5F4] px-2 py-1.5 focus:outline-none focus:border-[#A78BFA]/40"
|
||||
style={{ colorScheme: "dark" }}
|
||||
/>
|
||||
<span className="text-[9px] text-[#6B7280] font-mono shrink-0">→</span>
|
||||
<input
|
||||
type="date"
|
||||
value={dateTo}
|
||||
onChange={(e) => setDateTo(e.target.value)}
|
||||
className="flex-1 bg-[#0C0814]/70 border border-[#A78BFA]/15 rounded-lg text-[10px] font-mono text-[#C4B5F4] px-2 py-1.5 focus:outline-none focus:border-[#A78BFA]/40"
|
||||
style={{ colorScheme: "dark" }}
|
||||
/>
|
||||
<button
|
||||
onClick={handleApplyDates}
|
||||
className="shrink-0 material-symbols-outlined text-[18px] text-[#A78BFA]/60 hover:text-[#A78BFA] transition-colors"
|
||||
title="Apply date filter"
|
||||
>
|
||||
filter_alt
|
||||
</button>
|
||||
{(dateFrom || dateTo) && (
|
||||
<button
|
||||
onClick={handleClearDates}
|
||||
className="shrink-0 material-symbols-outlined text-[18px] text-[#6B7280] hover:text-[#ff6f78] transition-colors"
|
||||
title="Clear filter"
|
||||
>
|
||||
close
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Browser Panel ────────────────────────────────────────────────── */}
|
||||
<div className="rounded-[1.25rem] flex flex-col shadow-[0_16px_48px_rgba(0,0,0,0.6)] bg-[#110D1A]/95 backdrop-blur-2xl border border-[#A78BFA]/10 overflow-hidden">
|
||||
|
||||
{/* Header */}
|
||||
<div className="px-4 pt-4 pb-3 bg-[#1A132B]/30 border-b border-[#A78BFA]/8">
|
||||
<div className="flex items-center justify-between mb-0.5">
|
||||
<span className="text-[9px] font-mono bg-[#2D1B4E]/80 text-[#B79FFF] px-2.5 py-1 rounded-md tracking-[0.15em] border border-[#A78BFA]/20 uppercase">
|
||||
Knowledge Browser
|
||||
</span>
|
||||
{loading && (
|
||||
<span className="material-symbols-outlined animate-spin text-[16px] text-[#A78BFA]/50">autorenew</span>
|
||||
)}
|
||||
</div>
|
||||
{browseData && !loading && (
|
||||
<p className="text-[10px] text-[#8B7BB1] font-mono mt-1.5">
|
||||
<span className="text-[#00daf3] font-bold">{visibleCount}</span>
|
||||
{activeTopic ? ` signals · topic: "${activeTopic}"` : ` signals · ${browseData.topics.length} topics`}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Topic Chips */}
|
||||
{browseData && browseData.topics.length > 0 && (
|
||||
<div className="px-3 py-2.5 border-b border-[#A78BFA]/8 bg-[#0C0814]/30">
|
||||
<p className="text-[8px] font-mono text-[#6B7280] uppercase tracking-widest mb-1.5">Topics</p>
|
||||
<TopicChips
|
||||
topics={browseData.topics}
|
||||
activeTopic={activeTopic}
|
||||
onSelect={setActiveTopic}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Timeline */}
|
||||
<div
|
||||
ref={timelineRef}
|
||||
className="flex-1 overflow-y-auto px-3 py-3"
|
||||
style={{ maxHeight: "600px", scrollbarWidth: "none" }}
|
||||
>
|
||||
{/* Loading state */}
|
||||
{loading && (
|
||||
<div className="flex flex-col items-center justify-center py-10 gap-3">
|
||||
<span className="material-symbols-outlined animate-spin text-[32px] text-[#A78BFA]/40">autorenew</span>
|
||||
<p className="text-[10px] font-mono text-[#6B7280] uppercase tracking-widest">Loading knowledge…</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error state */}
|
||||
{!loading && error && (
|
||||
<div className="flex flex-col items-center justify-center py-8 gap-2 text-center">
|
||||
<span className="material-symbols-outlined text-[28px] text-[#ff6f78]/50">cloud_off</span>
|
||||
<p className="text-[10px] font-mono text-[#6B7280] leading-relaxed">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Empty state */}
|
||||
{!loading && !error && browseData && browseData.timeline.length === 0 && (
|
||||
<div className="flex flex-col items-center justify-center py-10 gap-2 text-center">
|
||||
<span className="material-symbols-outlined text-[32px] text-[#A78BFA]/30">library_books</span>
|
||||
<p className="text-[11px] font-mono text-[#6B7280]">No signals found</p>
|
||||
<p className="text-[9px] font-mono text-[#4B4860]">
|
||||
{dateFrom || dateTo ? "Try adjusting the date range" : "Send messages to populate this group"}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Timeline days */}
|
||||
{!loading && !error && browseData && browseData.timeline.map((entry) => (
|
||||
<DaySection key={entry.date} entry={entry} activeTopic={activeTopic} />
|
||||
))}
|
||||
|
||||
{/* No signals for active topic */}
|
||||
{!loading && !error && browseData && activeTopic && visibleCount === 0 && (
|
||||
<div className="flex flex-col items-center justify-center py-8 gap-2 text-center">
|
||||
<span className="material-symbols-outlined text-[28px] text-[#A78BFA]/30">search_off</span>
|
||||
<p className="text-[10px] font-mono text-[#6B7280]">No signals for topic "{activeTopic}"</p>
|
||||
<button
|
||||
onClick={() => setActiveTopic(null)}
|
||||
className="text-[9px] font-mono text-[#A78BFA] hover:text-white transition-colors underline underline-offset-2"
|
||||
>
|
||||
Clear filter
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer stats */}
|
||||
{browseData && !loading && (
|
||||
<div className="px-4 py-2.5 border-t border-[#A78BFA]/8 bg-[#0C0814]/40">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="text-center">
|
||||
<p className="text-[14px] font-mono font-bold text-[#00daf3]">{browseData.total_signals}</p>
|
||||
<p className="text-[7px] font-mono text-[#6B7280] uppercase tracking-wider">Signals</p>
|
||||
</div>
|
||||
<div className="w-px h-6 bg-[#A78BFA]/10" />
|
||||
<div className="text-center">
|
||||
<p className="text-[14px] font-mono font-bold text-[#A78BFA]">{browseData.topics.length}</p>
|
||||
<p className="text-[7px] font-mono text-[#6B7280] uppercase tracking-wider">Topics</p>
|
||||
</div>
|
||||
<div className="w-px h-6 bg-[#A78BFA]/10" />
|
||||
<div className="text-center">
|
||||
<p className="text-[14px] font-mono font-bold text-[#B79FFF]">{browseData.timeline.length}</p>
|
||||
<p className="text-[7px] font-mono text-[#6B7280] uppercase tracking-wider">Days</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => loadBrowse(selectedGroup, dateFrom, dateTo)}
|
||||
className="material-symbols-outlined text-[18px] text-[#A78BFA]/40 hover:text-[#A78BFA] transition-colors"
|
||||
title="Refresh"
|
||||
>
|
||||
refresh
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user