All files / src/components/knowledge KnowledgeDetailPanel.tsx

0% Statements 0/19
0% Branches 0/27
0% Functions 0/7
0% Lines 0/17

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">
          &times;
        </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 &rarr;
            </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>
  );
}