import { useState, useEffect, useRef } from "react"; import { GraphCanvasSVG as GraphCanvas, NodeDetailPanel, SectionLabel, Badge, LoadingState, SearchInput, MessageBubble } from "../components"; import { useGraphData } from "../state"; import { COLLECTION } from "../config"; import type { Entity } from "../types"; import { useChat, useConversation, useEmbeddings, useGraphEmbeddings } from "@trustgraph/react-state"; import { getLocalName } from "../utils"; import { palette, text, border, withGlow } from "../theme"; // Type for embedding result items interface EmbeddingResultItem { id: string; uri: string; label: string; color: string; icon: string; isEntity: boolean; } export function QueryView() { const [customInput, setCustomInput] = useState(""); const [queryForEmbeddings, setQueryForEmbeddings] = useState(undefined); const [selectedEntityId, setSelectedEntityId] = useState(null); const [selectedNode, setSelectedNode] = useState(null); const scrollRef = useRef(null); const { entities, relationships, ontology, propertyLabels, isLoading: graphLoading } = useGraphData(); const { submitMessage, isSubmitting } = useChat(); const messages = useConversation((state) => state.messages); const setChatMode = useConversation((state) => state.setChatMode); // Get embeddings for the query text - only fetch when we have a committed query const { embeddings, isLoading: embeddingsLoading } = useEmbeddings({ flow: "default", term: queryForEmbeddings || undefined, }); // Get graph entities from embeddings - only fetch when we have embeddings const hasEmbeddings = embeddings && embeddings.length > 0; const { graphEmbeddings, isLoading: graphEmbeddingsLoading } = useGraphEmbeddings({ vecs: hasEmbeddings ? embeddings : [[]], limit: hasEmbeddings ? 10 : 0, collection: COLLECTION, }); // Set chat mode to agent on mount useEffect(() => { setChatMode("agent"); }, [setChatMode]); // Auto-scroll to bottom when messages change useEffect(() => { scrollRef.current?.scrollIntoView({ behavior: "smooth" }); }, [messages]); const handleSubmit = (query: string) => { if (query.trim() && !isSubmitting) { const trimmedQuery = query.trim(); submitMessage({ input: trimmedQuery }); setQueryForEmbeddings(trimmedQuery); setSelectedEntityId(null); setSelectedNode(null); setCustomInput(""); } }; // Match graph embedding entities to our loaded entities for labels and highlighting // graphEmbeddings returns RDF terms: { t: "i", i: "http://..." } // Only show matched entities, deduplicated by URI const embeddingResults: EmbeddingResultItem[] = []; const seenUris = new Set(); for (const ge of (hasEmbeddings && graphEmbeddings || []) as { t: string; i?: string }[]) { const uri = ge.i; if (!uri || seenUris.has(uri)) continue; const entityId = getLocalName(uri); const found = entities.find(e => e.id === entityId || e.uri === uri); // Only include actual entities, not properties/concepts if (found) { seenUris.add(uri); embeddingResults.push({ id: entityId, uri, label: found.label, color: found.color, icon: found.icon, isEntity: true, }); } } // Auto-select first embedding result when results arrive useEffect(() => { if (embeddingResults.length > 0 && !selectedEntityId && !selectedNode) { setSelectedEntityId(embeddingResults[0].id); } }, [embeddingResults.length, selectedEntityId, selectedNode]); // Extract entity IDs for highlighting on graph // Priority: selectedNode (graph click) > selectedEntityId (button click) > all embedding results const highlightedEntities = (() => { const focusId = selectedNode?.id || selectedEntityId; if (!focusId) { return embeddingResults.map(e => e.id); } // Find all entities connected to the focused entity const connected = new Set([focusId]); for (const rel of relationships) { if (rel.from === focusId) { connected.add(rel.to); } else if (rel.to === focusId) { connected.add(rel.from); } } return Array.from(connected); })(); if (graphLoading || !ontology) { return ; } return (
{/* Query input area */}
AGENT QUERIES handleSubmit(customInput)} placeholder="Type your own question..." buttonText="Ask" isLoading={isSubmitting} buttonColor={palette.amber} />
{/* Related entities from graph embeddings */} {queryForEmbeddings && (
RELATED ENTITIES {(embeddingsLoading || graphEmbeddingsLoading) && loading...}
{embeddingResults.length === 0 && !embeddingsLoading && !graphEmbeddingsLoading && ( No related concepts found )} {embeddingResults.map((item) => { const isSelected = selectedEntityId === item.id; return ( { setSelectedEntityId(isSelected ? null : item.id); setSelectedNode(null); }} > {item.icon} {item.label} ); })}
)} {/* Response area */}
{messages.length === 0 ? (
Type your question to get started.
) : (
{messages.map((msg, idx) => ( ))} {isSubmitting && (
Processing...
)}
)}
{/* Graph visualization */}
{ setSelectedNode(selectedNode?.id === node.id ? null : node); setSelectedEntityId(null); }} activeFilter={null} />
{selectedNode && ( setSelectedNode(null)} onNodeSelect={(node) => { setSelectedNode(node); setSelectedEntityId(null); }} /> )}
); }