All files / src/components/knowledge KnowledgeDetailPanel.tsx

57.89% Statements 11/19
76.92% Branches 20/26
85.71% Functions 6/7
52.94% Lines 9/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                                                    36x 36x 87x                                     36x                                                   99x                                         54x 54x 54x                 5x                                        
/**
 * 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 {
    Iif (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>
  );
}