learning_ai_notes/web/src/components/KnowledgeGraphView.tsx

143 lines
4.7 KiB
TypeScript

"use client";
import { useCallback, useEffect, useState } from "react";
import { Button, Card } from "@/components/ui/Primitives";
import { queryEntity, getEntityTimeline, getKGContradictions, type PalaceKGTriple } from "@/lib/palace-client";
interface KnowledgeGraphViewProps {
wingId?: string;
}
export function KnowledgeGraphView({ wingId }: KnowledgeGraphViewProps) {
const [entity, setEntity] = useState("");
const [triples, setTriples] = useState<PalaceKGTriple[]>([]);
const [contradictions, setContradictions] = useState<Array<{ a: PalaceKGTriple; b: PalaceKGTriple }>>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const loadContradictions = useCallback(async () => {
try {
const result = await getKGContradictions(wingId);
setContradictions(result);
} catch {
// Ignore — contradictions are supplementary
}
}, [wingId]);
useEffect(() => {
void loadContradictions();
}, [loadContradictions]);
const handleQuery = async () => {
if (!entity.trim()) return;
try {
setLoading(true);
setError(null);
const result = await queryEntity(entity);
setTriples(result);
} catch {
setError("Query failed");
} finally {
setLoading(false);
}
};
const handleTimeline = async () => {
if (!entity.trim()) return;
try {
setLoading(true);
setError(null);
const result = await getEntityTimeline(entity);
setTriples(result);
} catch {
setError("Timeline query failed");
} finally {
setLoading(false);
}
};
return (
<Card style={{ padding: "var(--nl-space-5)", display: "grid", gap: "var(--nl-space-3)" }}>
<div style={{ fontWeight: 700, fontSize: "var(--nl-text-lg)" }}>Knowledge Graph</div>
<div style={{ display: "flex", gap: "var(--nl-space-2)" }}>
<input
type="text"
value={entity}
onChange={(e) => setEntity(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && handleQuery()}
placeholder="Query entity (e.g. React, Fastify)..."
aria-label="Query knowledge graph entity"
className="input"
style={{ flex: 1 }}
/>
<Button onClick={handleQuery} aria-label="Query entity">
Query
</Button>
<Button onClick={handleTimeline} variant="secondary" aria-label="Show timeline">
Timeline
</Button>
</div>
{error && <div style={{ color: "var(--nl-status-error)" }}>{error}</div>}
{loading && <div style={{ color: "var(--nl-text-secondary)" }}>Loading...</div>}
{!loading && triples.length === 0 && entity && (
<div style={{ color: "var(--nl-text-secondary)", textAlign: "center", padding: "var(--nl-space-4)" }}>
No triples found for &ldquo;{entity}&rdquo;
</div>
)}
<div style={{ display: "grid", gap: "var(--nl-space-2)" }}>
{triples.map((t) => (
<div
key={t.id}
style={{
padding: "var(--nl-space-3)",
borderRadius: "var(--nl-radius-md)",
border: "1px solid var(--nl-border-subtle)",
display: "flex",
alignItems: "center",
gap: "var(--nl-space-2)",
opacity: t.validTo ? 0.5 : 1,
}}
>
<span style={{ fontWeight: 600 }}>{t.subject}</span>
<span style={{ color: "var(--nl-accent-primary)" }}>{t.predicate}</span>
<span style={{ fontWeight: 600 }}>{t.object}</span>
<span style={{ marginLeft: "auto", color: "var(--nl-text-secondary)", fontSize: "0.75rem" }}>
{Math.round(t.confidence * 100)}%
{t.validTo && " (invalidated)"}
</span>
</div>
))}
</div>
{contradictions.length > 0 && (
<div style={{ marginTop: "var(--nl-space-3)" }}>
<div style={{ fontWeight: 600, color: "var(--nl-status-warning)", marginBottom: "var(--nl-space-2)" }}>
Contradictions ({contradictions.length})
</div>
{contradictions.map((c, i) => (
<div
key={i}
style={{
padding: "var(--nl-space-2)",
border: "1px solid var(--nl-status-warning)",
borderRadius: "var(--nl-radius-md)",
marginBottom: "var(--nl-space-2)",
fontSize: "0.875rem",
}}
>
<div>{c.a.subject} {c.a.predicate} <strong>{c.a.object}</strong></div>
<div style={{ color: "var(--nl-status-warning)" }}>
vs <strong>{c.b.object}</strong>
</div>
</div>
))}
</div>
)}
</Card>
);
}