mirror of
https://github.com/arkorty/B.Tech-Project-III.git
synced 2026-04-19 12:41:48 +00:00
549 lines
22 KiB
TypeScript
549 lines
22 KiB
TypeScript
"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>
|
|
);
|
|
}
|