# All files / src/components/knowledge KnowledgeDetailPanel.tsx

0% Statements 0/91
100% Branches 1/1
100% Functions 1/1
0% Lines 0/91

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>
      );
    }