Files
2026-04-05 00:43:23 +05:30

243 lines
9.6 KiB
TypeScript

"use client";
import React, { useState, useEffect } from "react";
import "./knowledge.css";
import {
fetchGroups,
queryKnowledge,
Group,
fetchSignals,
Signal,
parseMetaList,
} from "../lib/api";
export default function EntityPanel() {
const [searchQuery, setSearchQuery] = useState("");
const [groups, setGroups] = useState<Group[]>([]);
const [selectedGroup, setSelectedGroup] = useState<string>("");
const [queryResult, setQueryResult] = useState<string | null>(null);
const [querying, setQuerying] = useState(false);
const [topSignals, setTopSignals] = useState<Signal[]>([]);
const [loadingSignals, setLoadingSignals] = useState(false);
useEffect(() => {
fetchGroups().then((grps) => {
setGroups(grps);
if (grps.length > 0) {
setSelectedGroup(grps[0].group_id);
loadSignals(grps[0].group_id);
}
});
}, []);
async function loadSignals(groupId: string) {
setLoadingSignals(true);
try {
const sigs = await fetchSignals(groupId);
setTopSignals(sigs.slice(0, 2));
} catch {
setTopSignals([]);
} finally {
setLoadingSignals(false);
}
}
const handleGroupChange = (groupId: string) => {
setSelectedGroup(groupId);
setQueryResult(null);
loadSignals(groupId);
};
const handleSearch = async () => {
if (!searchQuery.trim() || !selectedGroup) return;
setQuerying(true);
setQueryResult(null);
try {
const res = await queryKnowledge(selectedGroup, searchQuery);
setQueryResult(res.answer);
} catch {
setQueryResult("⚠ Backend unavailable or group has no signals yet.");
} finally {
setQuerying(false);
}
};
const selectedGroupData = groups.find((g) => g.group_id === selectedGroup);
const topEntities = topSignals
.flatMap((s) => parseMetaList(s.metadata.entities))
.filter(Boolean)
.slice(0, 4);
return (
<div className="w-full flex flex-col gap-3 animate-fade-in-right">
{/* Group Selector */}
{groups.length > 1 && (
<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-data text-[#A78BFA]/60 uppercase tracking-widest mb-2">
Select Group
</p>
<select
value={selectedGroup}
onChange={(e) => handleGroupChange(e.target.value)}
className="w-full bg-transparent text-[#E9D9FF] text-[12px] font-medium 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>
</div>
)}
{/* Search Header */}
<div className="px-4 py-3.5 rounded-[1.25rem] bg-[#110D1A]/90 backdrop-blur-2xl border border-[#A78BFA]/10 flex items-center gap-3 shadow-[0_8px_32px_rgba(0,0,0,0.5)]">
<button
onClick={handleSearch}
className="material-symbols-outlined text-[#A78BFA]/50 text-[20px] hover:text-[#A78BFA] transition-colors"
disabled={querying}
>
{querying ? "hourglass_empty" : "search"}
</button>
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && handleSearch()}
placeholder="Query knowledge base..."
className="flex-1 bg-transparent text-[#E9D9FF] placeholder-[#8B7BB1] focus:outline-none text-[13px] font-medium tracking-wide"
/>
<button
onClick={() => {
setSearchQuery("");
setQueryResult(null);
}}
className="material-symbols-outlined text-[#A78BFA]/50 text-[20px] hover:text-[#A78BFA] transition-colors"
>
close
</button>
</div>
<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="p-6 relative bg-[#1A132B]/30">
<div className="flex justify-between items-start mb-4">
<span className="text-[9px] font-mono-data bg-[#2D1B4E]/80 text-[#B79FFF] px-2.5 py-1 rounded-md tracking-[0.15em] border border-[#A78BFA]/20">
{selectedGroupData ? "ACTIVE_GROUP" : "NO_GROUP"}
</span>
<span className="material-symbols-outlined text-[#8B7BB1] text-[20px] cursor-pointer hover:text-white transition-colors">
close
</span>
</div>
<h2
className="text-[20px] font-bold text-white tracking-tight leading-none mb-1.5 shadow-sm"
style={{ fontFamily: "'Space Grotesk', sans-serif" }}
>
{selectedGroup ? selectedGroup.replace(/-/g, "_").toUpperCase() : "Select a group"}
</h2>
<p className="text-[11px] text-[#8B7BB1] font-mono-data opacity-90 tracking-wide">
{selectedGroupData
? `${selectedGroupData.signal_count} signals · lens: ${selectedGroupData.lens || "unknown"}`
: "No data"}
</p>
</div>
<div className="w-full h-[1px] bg-gradient-to-r from-transparent via-[#A78BFA]/10 to-transparent" />
{/* Query Result */}
{queryResult && (
<>
<div className="p-6 bg-[#0C0814]/60">
<h4 className="text-[9px] font-mono-data text-[#8B7BB1] uppercase tracking-[0.2em] mb-3 opacity-90">
Query Result
</h4>
<p className="text-[12px] text-[#E9D9FF] leading-relaxed">
{queryResult}
</p>
</div>
<div className="w-full h-[1px] bg-gradient-to-r from-transparent via-[#A78BFA]/10 to-transparent" />
</>
)}
{/* Content */}
<div className="p-6 space-y-7 relative">
{/* Primary Insights */}
<div>
<h4 className="text-[9px] font-mono-data text-[#8B7BB1] uppercase tracking-[0.2em] mb-2.5 opacity-90">
Signal Overview
</h4>
<div className="grid grid-cols-2 gap-3">
<div className="p-3.5 rounded-xl bg-[#0C0814]/80 border border-[#A78BFA]/5 shadow-inner">
<p className="text-[8px] font-mono-data text-[#8B7BB1] mb-1.5 uppercase tracking-[0.15em] opacity-80">
Signals
</p>
<p className="text-[16px] font-mono-data text-[#00daf3] font-bold tracking-tight drop-shadow-[0_0_8px_rgba(0,218,243,0.2)]">
{selectedGroupData?.signal_count ?? "—"}
</p>
</div>
<div className="p-3.5 rounded-xl bg-[#0C0814]/80 border border-[#A78BFA]/5 shadow-inner">
<p className="text-[8px] font-mono-data text-[#8B7BB1] mb-1.5 uppercase tracking-[0.15em] opacity-80">
Lens
</p>
<p className="text-[14px] font-mono-data text-[#E9D9FF] font-bold tracking-tight uppercase">
{selectedGroupData?.lens?.slice(0, 8) ?? "—"}
</p>
</div>
</div>
</div>
{/* Recent Entities */}
{topEntities.length > 0 && (
<div>
<h4 className="text-[9px] font-mono-data text-[#8B7BB1] uppercase tracking-[0.2em] mb-3 opacity-90">
Recent Entities
</h4>
<div className="flex flex-wrap gap-2">
{topEntities.map((entity, i) => (
<span
key={i}
className="text-[10px] font-mono-data text-[#B79FFF] bg-[#2D1B4E]/60 px-2.5 py-1 rounded-md border border-[#A78BFA]/20"
>
{entity}
</span>
))}
</div>
</div>
)}
{/* Loading state */}
{loadingSignals && (
<div className="flex items-center gap-2 text-zinc-600 text-[10px] font-mono-data">
<span className="material-symbols-outlined animate-spin text-sm">autorenew</span>
Loading signals...
</div>
)}
{/* Empty state */}
{!loadingSignals && selectedGroupData?.signal_count === 0 && (
<p className="text-[11px] text-zinc-600 font-mono-data">
No signals yet. Send messages to the monitored Telegram group to populate the knowledge base.
</p>
)}
</div>
{/* Footer Action */}
<div className="p-5 border-t border-[#A78BFA]/10 bg-[#0C0814]/40">
<button
onClick={handleSearch}
disabled={!searchQuery.trim() || querying}
className="w-full bg-[#1A102A] hover:bg-[#2D1B4E] border border-[#A78BFA]/20 text-[#B79FFF] hover:text-white font-mono-data text-[9.5px] font-bold tracking-[0.25em] py-3.5 rounded-xl transition-all uppercase flex items-center justify-center gap-3 group relative overflow-hidden btn-interactive shadow-lg disabled:opacity-40 disabled:cursor-not-allowed"
>
<span className="relative z-10">
{querying ? "Querying..." : "Execute Deep Query"}
</span>
<span className="material-symbols-outlined text-[16px] relative z-10 group-hover:scale-110 transition-transform text-[#00daf3]">
{querying ? "hourglass_empty" : "bolt"}
</span>
</button>
</div>
</div>
</div>
);
}