Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 | /** * Slide-in detail panel for a selected knowledge graph node. * * @module */ import { useMemo, type JSX } from "react"; import type { GraphNode, NodeDetail } from "../../hooks/types.js"; import { taskUrl, sessionUrl } from "../../utils/navigation.js"; import { useAppNavigate } from "../../utils/navigation.js"; import styles from "./KnowledgeDetailPanel.module.scss"; interface KnowledgeDetailPanelProps { detail: NodeDetail; nodes: GraphNode[]; onClose: () => void; onSelectNode: (id: string) => void; } /** Slide-in panel showing full details for a selected knowledge node. */ export function KnowledgeDetailPanel({ detail, nodes, onClose, onSelectNode, }: KnowledgeDetailPanelProps): JSX.Element { const navigate = useAppNavigate(); const { node, edges } = detail; const nodeById = useMemo(() => new Map(nodes.map((n) => [n.id, n])), [nodes]); /** Navigate to the source entity for reference nodes. */ function handleViewInGrackle(): void { if (node.kind !== "reference" || !node.sourceId) { return; } switch (node.sourceType) { case "task": navigate(taskUrl(node.sourceId)); break; case "session": navigate(sessionUrl(node.sourceId)); break; default: break; } } return ( <div className={styles.panel} data-testid="knowledge-detail-panel"> <div className={styles.header}> <h3 className={styles.title}>{node.label}</h3> <button className={styles.closeButton} onClick={onClose} aria-label="Close"> × </button> </div> <div className={styles.body}> <div className={styles.badge}> {node.kind === "reference" ? `Reference (${node.sourceType})` : node.category} </div> {node.content && ( <div className={styles.section}> <div className={styles.sectionLabel}>Content</div> <p className={styles.content}>{node.content}</p> </div> )} {node.tags && node.tags.length > 0 && ( <div className={styles.section}> <div className={styles.sectionLabel}>Tags</div> <div className={styles.tags}> {node.tags.map((tag) => ( <span key={tag} className={styles.tag}> {tag} </span> ))} </div> </div> )} {node.kind === "reference" && node.sourceId && ( <div className={styles.section}> <button className={styles.viewLink} onClick={handleViewInGrackle}> View in Grackle → </button> </div> )} {edges.length > 0 && ( <div className={styles.section}> <div className={styles.sectionLabel}>Edges ({edges.length})</div> <ul className={styles.edgeList}> {edges.map((edge) => { const otherId: string = edge.fromId === node.id ? edge.toId : edge.fromId; const edgeKey: string = `${edge.fromId}:${edge.toId}:${edge.type}`; return ( <li key={edgeKey} className={styles.edgeItem} data-testid="edge-item"> <span className={styles.edgeType} data-testid="edge-type"> {edge.type} </span> <button className={styles.edgeNodeLink} data-testid="edge-node-link" onClick={() => { onSelectNode(otherId); }} > {nodeById.get(otherId)?.label ?? `${otherId.substring(0, 8)}...`} </button> </li> ); })} </ul> </div> )} <div className={styles.timestamps}> {node.createdAt && <div>Created: {new Date(node.createdAt).toLocaleDateString()}</div>} {node.updatedAt && <div>Updated: {new Date(node.updatedAt).toLocaleDateString()}</div>} </div> </div> </div> ); } |