fleet-memory/hindsight-control-plane/src/components/entities-view.tsx
Nicolò Boschi 4f2833873c
feat: introduce mental models (#132)
* mental models

* DRAFT: refactor entity observations

* fix db patch

* agentic

* agentic

* reflect agent

* new style

* more

* fix ci

* fix

* fix
2026-01-16 11:16:41 +01:00

271 lines
9.4 KiB
TypeScript

"use client";
import { useState, useEffect } from "react";
import { client } from "@/lib/api";
import { useBank } from "@/lib/bank-context";
import { Button } from "@/components/ui/button";
import { ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from "lucide-react";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
interface Entity {
id: string;
canonical_name: string;
mention_count: number;
first_seen?: string;
last_seen?: string;
metadata?: Record<string, any>;
}
type EntityDetail = Entity;
const ITEMS_PER_PAGE = 50;
export function EntitiesView() {
const { currentBank } = useBank();
const [entities, setEntities] = useState<Entity[]>([]);
const [loading, setLoading] = useState(false);
const [selectedEntity, setSelectedEntity] = useState<EntityDetail | null>(null);
const [loadingDetail, setLoadingDetail] = useState(false);
// Pagination state
const [currentPage, setCurrentPage] = useState(1);
const [total, setTotal] = useState(0);
const totalPages = Math.ceil(total / ITEMS_PER_PAGE);
const offset = (currentPage - 1) * ITEMS_PER_PAGE;
const loadEntities = async (page: number = 1) => {
if (!currentBank) return;
setLoading(true);
try {
const pageOffset = (page - 1) * ITEMS_PER_PAGE;
const result = await client.listEntities({
bank_id: currentBank,
limit: ITEMS_PER_PAGE,
offset: pageOffset,
});
setEntities(result.items || []);
setTotal(result.total || 0);
} catch (error) {
console.error("Error loading entities:", error);
alert("Error loading entities: " + (error as Error).message);
} finally {
setLoading(false);
}
};
const loadEntityDetail = async (entityId: string) => {
if (!currentBank) return;
setLoadingDetail(true);
try {
const result: any = await client.getEntity(entityId, currentBank);
setSelectedEntity(result);
} catch (error) {
console.error("Error loading entity detail:", error);
alert("Error loading entity detail: " + (error as Error).message);
} finally {
setLoadingDetail(false);
}
};
// Handle page change
const handlePageChange = (newPage: number) => {
setCurrentPage(newPage);
loadEntities(newPage);
};
useEffect(() => {
if (currentBank) {
setCurrentPage(1);
loadEntities(1);
setSelectedEntity(null);
}
}, [currentBank]);
const formatDate = (dateStr?: string) => {
if (!dateStr) return "N/A";
return new Date(dateStr).toLocaleDateString();
};
return (
<div>
{/* Entity List */}
<div>
{loading ? (
<div className="flex items-center justify-center py-20">
<div className="text-center">
<div className="text-4xl mb-2">...</div>
<div className="text-sm text-muted-foreground">Loading entities...</div>
</div>
</div>
) : entities.length > 0 ? (
<>
<div className="mb-4 text-sm text-muted-foreground">
{total} {total === 1 ? "entity" : "entities"}
</div>
<div className="overflow-x-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Mentions</TableHead>
<TableHead>First Seen</TableHead>
<TableHead>Last Seen</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{entities.map((entity) => (
<TableRow
key={entity.id}
onClick={() => loadEntityDetail(entity.id)}
className={`cursor-pointer hover:bg-muted/50 ${
selectedEntity?.id === entity.id ? "bg-primary/10" : ""
}`}
>
<TableCell className="font-medium text-card-foreground">
{entity.canonical_name}
</TableCell>
<TableCell className="text-card-foreground">{entity.mention_count}</TableCell>
<TableCell className="text-card-foreground">
{formatDate(entity.first_seen)}
</TableCell>
<TableCell className="text-card-foreground">
{formatDate(entity.last_seen)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
{/* Pagination Controls */}
{totalPages > 1 && (
<div className="flex items-center justify-between mt-3 pt-3 border-t">
<div className="text-xs text-muted-foreground">
{offset + 1}-{Math.min(offset + ITEMS_PER_PAGE, total)} of {total}
</div>
<div className="flex items-center gap-1">
<Button
variant="outline"
size="sm"
onClick={() => handlePageChange(1)}
disabled={currentPage === 1 || loading}
className="h-7 w-7 p-0"
>
<ChevronsLeft className="h-3 w-3" />
</Button>
<Button
variant="outline"
size="sm"
onClick={() => handlePageChange(currentPage - 1)}
disabled={currentPage === 1 || loading}
className="h-7 w-7 p-0"
>
<ChevronLeft className="h-3 w-3" />
</Button>
<span className="text-xs px-2">
{currentPage} / {totalPages}
</span>
<Button
variant="outline"
size="sm"
onClick={() => handlePageChange(currentPage + 1)}
disabled={currentPage === totalPages || loading}
className="h-7 w-7 p-0"
>
<ChevronRight className="h-3 w-3" />
</Button>
<Button
variant="outline"
size="sm"
onClick={() => handlePageChange(totalPages)}
disabled={currentPage === totalPages || loading}
className="h-7 w-7 p-0"
>
<ChevronsRight className="h-3 w-3" />
</Button>
</div>
</div>
)}
</>
) : (
<div className="flex items-center justify-center py-20">
<div className="text-center">
<div className="text-4xl mb-2">...</div>
<div className="text-sm text-muted-foreground">No entities found</div>
<div className="text-xs text-muted-foreground mt-1">
Entities are extracted from facts when memories are added.
</div>
</div>
</div>
)}
</div>
{/* Entity Detail Panel - Fixed overlay */}
{selectedEntity && (
<div className="fixed right-0 top-0 h-screen w-[420px] bg-card border-l-2 border-primary shadow-2xl z-50 overflow-y-auto animate-in slide-in-from-right duration-300 ease-out">
<div className="p-5">
{/* Header */}
<div className="flex justify-between items-center mb-6 pb-4 border-b border-border">
<div>
<h3 className="text-xl font-bold text-card-foreground">
{selectedEntity.canonical_name}
</h3>
<p className="text-sm text-muted-foreground mt-1">Entity details</p>
</div>
<Button
variant="ghost"
size="sm"
onClick={() => setSelectedEntity(null)}
className="h-8 w-8 p-0"
>
<span className="text-lg">x</span>
</Button>
</div>
<div className="space-y-5">
{/* Entity Info */}
<div className="grid grid-cols-2 gap-4">
<div className="p-4 bg-muted/50 rounded-lg">
<div className="text-xs font-bold text-muted-foreground uppercase mb-2">
Mentions
</div>
<div className="text-lg font-semibold text-card-foreground">
{selectedEntity.mention_count}
</div>
</div>
<div className="p-4 bg-muted/50 rounded-lg">
<div className="text-xs font-bold text-muted-foreground uppercase mb-2">
First Seen
</div>
<div className="text-sm font-medium text-card-foreground">
{formatDate(selectedEntity.first_seen)}
</div>
</div>
</div>
{/* ID */}
<div className="p-4 bg-muted/50 rounded-lg">
<div className="text-xs font-bold text-muted-foreground uppercase mb-2">
Entity ID
</div>
<code className="text-xs font-mono break-all text-muted-foreground">
{selectedEntity.id}
</code>
</div>
</div>
</div>
</div>
)}
</div>
);
}