142 lines
4.7 KiB
TypeScript
142 lines
4.7 KiB
TypeScript
"use client";
|
|
|
|
import { useCallback, useEffect, useState } from "react";
|
|
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 (
|
|
<section className="surface-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} className="btn btn-primary" aria-label="Query entity">
|
|
Query
|
|
</button>
|
|
<button onClick={handleTimeline} className="btn" 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 “{entity}”
|
|
</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)" }}>{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>
|
|
)}
|
|
</section>
|
|
);
|
|
}
|