All files / src/components/knowledge KnowledgeNav.tsx

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

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 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163                                                                                                                                                                                                                                                                                                                                     
/**
 * Knowledge graph sidebar navigation — search, workspace filter, and node list.
 *
 * Rendered inside the shared Sidebar component when the Knowledge tab is active.
 *
 * @module
 */
 
import { useState, useCallback, type FormEvent, type JSX } from "react";
import type { Workspace } from "../../hooks/types.js";
import type { GraphNode } from "../../hooks/types.js";
import styles from "./KnowledgeNav.module.scss";
 
/** Props for the KnowledgeNav sidebar component. */
export interface KnowledgeNavProps {
  /** Nodes currently in the graph. */
  nodes: GraphNode[];
  /** All workspaces for the filter dropdown. */
  workspaces: Workspace[];
  /** Whether a search or load operation is in progress. */
  loading: boolean;
  /** Active search query (non-empty means search is active). */
  searchQuery: string;
  /** Execute a semantic search. */
  onSearch: (query: string) => void;
  /** Clear search and reload recent nodes. */
  onClearSearch: () => void;
  /** Select a node by ID (e.g., open detail panel). */
  onSelectNode: (nodeId: string) => void;
  /** Filter by workspace (empty string means all workspaces). */
  onWorkspaceChange: (workspaceId: string) => void;
}
 
/** Sidebar content for the Knowledge tab. */
export function KnowledgeNav({
  nodes,
  workspaces,
  loading,
  searchQuery,
  onSearch,
  onClearSearch,
  onSelectNode,
  onWorkspaceChange,
}: KnowledgeNavProps): JSX.Element {
  const [searchInput, setSearchInput] = useState("");
 
  const handleSearch = useCallback(
    (e: FormEvent) => {
      e.preventDefault();
      if (searchInput.trim()) {
        onSearch(searchInput.trim());
      }
    },
    [searchInput, onSearch],
  );
 
  const handleClear = useCallback(() => {
    setSearchInput("");
    onClearSearch();
  }, [onClearSearch]);
 
  const handleNodeClick = useCallback(
    (nodeId: string) => {
      onSelectNode(nodeId);
    },
    [onSelectNode],
  );
 
  const handleWorkspaceChange = useCallback(
    (wsId: string) => {
      setSearchInput("");
      onWorkspaceChange(wsId);
    },
    [onWorkspaceChange],
  );
 
  return (
    <div className={styles.nav} data-testid="knowledge-nav">
      {/* Search */}
      <form className={styles.searchForm} onSubmit={handleSearch}>
        <input
          className={styles.searchInput}
          type="text"
          placeholder="Search..."
          value={searchInput}
          onChange={(e) => {
            setSearchInput(e.target.value);
          }}
          data-testid="knowledge-search-input"
          aria-label="Search knowledge nodes"
        />
        <button type="submit" className={styles.searchButton} disabled={loading}>
          Go
        </button>
      </form>
      {searchQuery && (
        <button type="button" className={styles.clearButton} onClick={handleClear}>
          Clear search
        </button>
      )}
 
      {/* Workspace filter */}
      <select
        className={styles.workspaceSelect}
        onChange={(e) => {
          handleWorkspaceChange(e.target.value);
        }}
        data-testid="knowledge-workspace-filter"
        aria-label="Filter by workspace"
      >
        <option value="">All workspaces</option>
        {workspaces.map((ws) => (
          <option key={ws.id} value={ws.id}>
            {ws.name}
          </option>
        ))}
      </select>
 
      {/* Node list */}
      <div className={styles.listHeader}>Nodes ({nodes.length})</div>
      <ul className={styles.nodeList}>
        {nodes.map((node: GraphNode) => (
          <li
            key={node.id}
            className={styles.nodeItem}
            onClick={() => {
              handleNodeClick(node.id);
            }}
            onKeyDown={(e) => {
              if (e.key === "Enter" || e.key === " ") {
                e.preventDefault();
                handleNodeClick(node.id);
              }
            }}
            role="button"
            tabIndex={0}
          >
            <span
              className={styles.indicator}
              style={{
                backgroundColor:
                  node.kind === "reference"
                    ? "#4A9EFF"
                    : node.category === "decision"
                      ? "#22C55E"
                      : node.category === "concept"
                        ? "#A855F7"
                        : node.category === "snippet"
                          ? "#6B7280"
                          : "#EAB308",
              }}
            />
            <span className={styles.label}>{node.label}</span>
            <span className={styles.badge}>
              {node.kind === "reference" ? node.sourceType : node.category}
            </span>
          </li>
        ))}
      </ul>
    </div>
  );
}