Merge commit '9b2f675702' as 'ai-context/context-graph-demo'

This commit is contained in:
elpresidank 2026-04-05 21:08:35 -05:00
commit ecaf3489f1
54 changed files with 10078 additions and 0 deletions

View file

@ -0,0 +1,9 @@
// Main data hook - provides entities, relationships, and ontology
export { useGraphData } from "./useGraphData";
// Schema hook - for OWL ontology schema view
export { useOntologySchema } from "./useOntologySchema";
// Toast notifications
export { useToastStore, toast } from "./toastStore";
export type { Toast, ToastType } from "./toastStore";

View file

@ -0,0 +1,52 @@
import { create } from "zustand";
export type ToastType = "success" | "error" | "warning" | "info";
export interface Toast {
id: string;
type: ToastType;
message: string;
persistent?: boolean;
}
interface ToastStore {
toasts: Toast[];
addToast: (type: ToastType, message: string, persistent?: boolean) => void;
removeToast: (id: string) => void;
}
let toastId = 0;
export const useToastStore = create<ToastStore>((set) => ({
toasts: [],
addToast: (type, message, persistent = false) => {
const id = `toast-${++toastId}`;
set((state) => ({
toasts: [...state.toasts.slice(-3), { id, type, message, persistent }],
}));
// Auto-dismiss after 6 seconds unless explicitly persistent
if (!persistent) {
setTimeout(() => {
set((state) => ({
toasts: state.toasts.filter((t) => t.id !== id),
}));
}, 6000);
}
},
removeToast: (id) => {
set((state) => ({
toasts: state.toasts.filter((t) => t.id !== id),
}));
},
}));
// Helper functions for easy access outside React
export const toast = {
success: (message: string) => useToastStore.getState().addToast("success", message),
error: (message: string) => useToastStore.getState().addToast("error", message),
warning: (message: string) => useToastStore.getState().addToast("warning", message),
info: (message: string) => useToastStore.getState().addToast("info", message),
};

View file

@ -0,0 +1,243 @@
import { useState, useEffect, useMemo } from "react";
import { useSocket } from "@trustgraph/react-provider";
import type { Triple } from "@trustgraph/react-state";
import type { Entity, Relationship, DomainKey, OntologyType } from "../types";
import { COLLECTION } from "../config";
import { domainColors } from "../theme";
const RDF_TYPE = "http://www.w3.org/1999/02/22-rdf-syntax-ns#type";
const RDFS_LABEL = "http://www.w3.org/2000/01/rdf-schema#label";
const RDFS_COMMENT = "http://www.w3.org/2000/01/rdf-schema#comment";
const OWL_CLASS = "http://www.w3.org/2002/07/owl#Class";
const OWL_DATATYPE_PROPERTY = "http://www.w3.org/2002/07/owl#DatatypeProperty";
const OWL_OBJECT_PROPERTY = "http://www.w3.org/2002/07/owl#ObjectProperty";
// Helper to extract value from a Term
function getTermValue(term: { t: string; i?: string; v?: string }): string {
if (term.t === "i") return term.i || "";
if (term.t === "l") return term.v || "";
return "";
}
// Helper to create a short ID from a URI
function uriToId(uri: string): string {
const hashIndex = uri.lastIndexOf("#");
const slashIndex = uri.lastIndexOf("/");
const index = Math.max(hashIndex, slashIndex);
return index >= 0 ? uri.substring(index + 1) : uri;
}
// Helper to get icon for a class (placeholder for now)
function getClassIcon(_classUri: string): string {
return "●";
}
// Helper to extract predicate name from URI
function predicateToName(uri: string): string {
const hashIndex = uri.lastIndexOf("#");
const slashIndex = uri.lastIndexOf("/");
const index = Math.max(hashIndex, slashIndex);
const name = index >= 0 ? uri.substring(index + 1) : uri;
return name.replace(/([a-z])([A-Z])/g, "$1_$2").toLowerCase();
}
export function useGraphData(domain?: DomainKey) {
const socket = useSocket();
const [triples, setTriples] = useState<Triple[] | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [isError, setIsError] = useState(false);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
let cancelled = false;
(async () => {
try {
setIsLoading(true);
setIsError(false);
setError(null);
const api = socket.flow("default");
const result = await api.triplesQuery(
undefined, undefined, undefined,
10000, COLLECTION, "",
);
if (!cancelled) {
setTriples(result);
setIsLoading(false);
}
} catch (err) {
if (!cancelled) {
setIsError(true);
setError(err instanceof Error ? err : new Error(String(err)));
setIsLoading(false);
}
}
})();
return () => { cancelled = true; };
}, [socket]);
// Process all data from the query
const { entities, relationships, ontology, propertyLabels } = useMemo(() => {
if (isLoading || !triples) {
return { entities: [], relationships: [], ontology: undefined, propertyLabels: {} };
}
// First pass: collect all labels, comments, and find OWL classes and properties
const allLabels = new Map<string, string>();
const allComments = new Map<string, string>();
const owlClasses = new Set<string>();
const propertyUris = new Set<string>();
for (const triple of triples) {
const subjectUri = getTermValue(triple.s);
const predicate = getTermValue(triple.p);
const objectUri = getTermValue(triple.o);
if (predicate === RDFS_LABEL) {
allLabels.set(subjectUri, getTermValue(triple.o));
} else if (predicate === RDFS_COMMENT) {
allComments.set(subjectUri, getTermValue(triple.o));
} else if (predicate === RDF_TYPE) {
if (objectUri === OWL_CLASS) {
owlClasses.add(subjectUri);
} else if (objectUri === OWL_DATATYPE_PROPERTY || objectUri === OWL_OBJECT_PROPERTY) {
propertyUris.add(subjectUri);
}
}
}
// Build property labels map: local name -> label
const propertyLabels: Record<string, string> = {};
for (const propUri of propertyUris) {
const localName = uriToId(propUri);
const label = allLabels.get(propUri);
if (label) {
propertyLabels[localName] = label;
}
}
// Build class config dynamically from discovered OWL classes
const classConfig = new Map<string, { domain: DomainKey; color: string; glow: string; icon: string; label: string; description: string }>();
let colorIndex = 0;
for (const classUri of owlClasses) {
const localName = uriToId(classUri).toLowerCase();
const palette = domainColors[colorIndex % domainColors.length];
classConfig.set(classUri, {
domain: localName,
color: palette.color,
glow: palette.glow,
icon: getClassIcon(classUri),
label: allLabels.get(classUri) || uriToId(classUri),
description: allComments.get(classUri) || "",
});
colorIndex++;
}
// Second pass: find entities (instances of OWL classes) and their properties
const entityMap = new Map<string, Entity>();
const entityProps = new Map<string, Record<string, string | number>>();
// Collect entity properties first
for (const triple of triples) {
const subjectUri = getTermValue(triple.s);
const predicate = getTermValue(triple.p);
const value = getTermValue(triple.o);
// Skip schema-level predicates and URIs as values
if (predicate !== RDF_TYPE && predicate !== RDFS_LABEL && predicate !== RDFS_COMMENT &&
value && !value.startsWith("http")) {
if (!entityProps.has(subjectUri)) {
entityProps.set(subjectUri, {});
}
const propName = uriToId(predicate);
entityProps.get(subjectUri)![propName] = value;
}
}
// Find entities by type (instances of OWL classes)
for (const triple of triples) {
const subjectUri = getTermValue(triple.s);
const predicate = getTermValue(triple.p);
const objectUri = getTermValue(triple.o);
if (predicate === RDF_TYPE && classConfig.has(objectUri)) {
const config = classConfig.get(objectUri)!;
if (domain && config.domain !== domain) continue;
const entityId = uriToId(subjectUri);
entityMap.set(subjectUri, {
id: entityId,
uri: subjectUri,
label: allLabels.get(subjectUri) || entityId,
props: entityProps.get(subjectUri) || {},
domain: config.domain,
color: config.color,
glow: config.glow,
icon: config.icon,
});
}
}
// Find relationships: triples where both subject and object are known entities
const relationships: Relationship[] = [];
const entityUris = new Set(entityMap.keys());
for (const triple of triples) {
const subjectUri = getTermValue(triple.s);
const predicate = getTermValue(triple.p);
const objectUri = getTermValue(triple.o);
// Skip rdf:type and rdfs:label
if (predicate === RDF_TYPE || predicate === RDFS_LABEL) continue;
// If both subject and object are entities, it's a relationship
if (entityUris.has(subjectUri) && entityUris.has(objectUri)) {
const fromEntity = entityMap.get(subjectUri)!;
const toEntity = entityMap.get(objectUri)!;
relationships.push({
from: fromEntity.id,
to: toEntity.id,
predicate: predicateToName(predicate),
domain: [fromEntity.domain, toEntity.domain],
});
}
}
const entities = Array.from(entityMap.values());
// Build ontology metadata dynamically from discovered classes
const ontology: OntologyType = {};
for (const [, config] of classConfig) {
ontology[config.domain] = {
label: config.label,
color: config.color,
glow: config.glow,
icon: config.icon,
description: config.description,
properties: [],
subclasses: entities.filter(e => e.domain === config.domain).map(e => ({
id: e.id,
uri: e.uri,
label: e.label,
props: e.props,
})),
};
}
return { entities, relationships, ontology, propertyLabels };
}, [isLoading, triples, domain]);
return {
entities,
relationships,
ontology,
propertyLabels,
isLoading,
isError,
error,
};
}

View file

@ -0,0 +1,178 @@
import { useMemo } from "react";
import { useTriples } from "@trustgraph/react-state";
import { COLLECTION } from "../config";
const RDF_TYPE = "http://www.w3.org/1999/02/22-rdf-syntax-ns#type";
const RDFS_LABEL = "http://www.w3.org/2000/01/rdf-schema#label";
const RDFS_DOMAIN = "http://www.w3.org/2000/01/rdf-schema#domain";
const RDFS_RANGE = "http://www.w3.org/2000/01/rdf-schema#range";
const RDFS_COMMENT = "http://www.w3.org/2000/01/rdf-schema#comment";
const OWL_CLASS = "http://www.w3.org/2002/07/owl#Class";
const OWL_OBJECT_PROPERTY = "http://www.w3.org/2002/07/owl#ObjectProperty";
const OWL_DATATYPE_PROPERTY = "http://www.w3.org/2002/07/owl#DatatypeProperty";
// Helper to extract value from a Term
function getTermValue(term: { t: string; i?: string; v?: string }): string {
if (term.t === "i") return term.i || "";
if (term.t === "l") return term.v || "";
return "";
}
// Helper to get local name from URI
function getLocalName(uri: string): string {
const hashIndex = uri.lastIndexOf("#");
const slashIndex = uri.lastIndexOf("/");
const index = Math.max(hashIndex, slashIndex);
return index >= 0 ? uri.substring(index + 1) : uri;
}
export interface OntologyClass {
uri: string;
label: string;
comment?: string;
}
export interface OntologyProperty {
uri: string;
label: string;
domain?: string;
range?: string;
}
export interface OntologySchema {
classes: OntologyClass[];
objectProperties: OntologyProperty[];
datatypeProperties: OntologyProperty[];
// Sets for quick lookup
objectPropertyUris: Set<string>;
datatypePropertyUris: Set<string>;
}
export function useOntologySchema() {
// Query for classes
const classTriples = useTriples({
p: { t: "i", i: RDF_TYPE },
o: { t: "i", i: OWL_CLASS },
limit: 100,
collection: COLLECTION,
});
// Query for object properties
const objectPropertyTriples = useTriples({
p: { t: "i", i: RDF_TYPE },
o: { t: "i", i: OWL_OBJECT_PROPERTY },
limit: 100,
collection: COLLECTION,
});
// Query for datatype properties
const datatypePropertyTriples = useTriples({
p: { t: "i", i: RDF_TYPE },
o: { t: "i", i: OWL_DATATYPE_PROPERTY },
limit: 100,
collection: COLLECTION,
});
// Query for all triples to get labels, domains, ranges
const allTriples = useTriples({
limit: 1000,
collection: COLLECTION,
});
const isLoading = classTriples.isLoading || objectPropertyTriples.isLoading ||
datatypePropertyTriples.isLoading || allTriples.isLoading;
const isError = classTriples.isError || objectPropertyTriples.isError ||
datatypePropertyTriples.isError || allTriples.isError;
const error = classTriples.error || objectPropertyTriples.error ||
datatypePropertyTriples.error || allTriples.error;
const schema = useMemo((): OntologySchema | undefined => {
if (isLoading) return undefined;
// Build a map of URI -> metadata from all triples
const metadata = new Map<string, { label?: string; domain?: string; range?: string; comment?: string }>();
for (const triple of allTriples.triples || []) {
const subject = getTermValue(triple.s);
const predicate = getTermValue(triple.p);
const value = getTermValue(triple.o);
if (!metadata.has(subject)) {
metadata.set(subject, {});
}
const meta = metadata.get(subject)!;
if (predicate === RDFS_LABEL) {
meta.label = value;
} else if (predicate === RDFS_DOMAIN) {
meta.domain = value;
} else if (predicate === RDFS_RANGE) {
meta.range = value;
} else if (predicate === RDFS_COMMENT) {
meta.comment = value;
}
}
// Build classes list
const classes: OntologyClass[] = [];
for (const triple of classTriples.triples || []) {
const uri = getTermValue(triple.s);
const meta = metadata.get(uri) || {};
classes.push({
uri,
label: meta.label || getLocalName(uri),
comment: meta.comment,
});
}
// Build object properties list
const objectProperties: OntologyProperty[] = [];
const objectPropertyUris = new Set<string>();
for (const triple of objectPropertyTriples.triples || []) {
const uri = getTermValue(triple.s);
const meta = metadata.get(uri) || {};
objectPropertyUris.add(uri);
objectProperties.push({
uri,
label: meta.label || getLocalName(uri),
domain: meta.domain,
range: meta.range,
});
}
// Build datatype properties list
const datatypeProperties: OntologyProperty[] = [];
const datatypePropertyUris = new Set<string>();
for (const triple of datatypePropertyTriples.triples || []) {
const uri = getTermValue(triple.s);
const meta = metadata.get(uri) || {};
datatypePropertyUris.add(uri);
datatypeProperties.push({
uri,
label: meta.label || getLocalName(uri),
domain: meta.domain,
range: meta.range,
});
}
return {
classes,
objectProperties,
datatypeProperties,
objectPropertyUris,
datatypePropertyUris,
};
}, [
isLoading,
classTriples.triples,
objectPropertyTriples.triples,
datatypePropertyTriples.triples,
allTriples.triples,
]);
return {
schema,
isLoading,
isError,
error,
};
}