Add multi-language logic exploration
This commit is contained in:
parent
9b4258c852
commit
9161889492
18 changed files with 1639 additions and 76 deletions
|
|
@ -39,10 +39,18 @@ header h1 { margin: 0; font-size: 17px; }
|
|||
.view-switch[data-mode="flow"]::before { transform: translateX(58px); }
|
||||
.view-switch[data-mode="web"]::before { transform: translateX(116px); }
|
||||
.view-switch[data-mode="logic"]::before { transform: translateX(174px); }
|
||||
.filter-presets { display: flex; flex-wrap: wrap; gap: 5px; }
|
||||
.filter-presets button {
|
||||
border: 1px solid var(--line); border-radius: 999px; padding: 4px 8px;
|
||||
background: #0b1724; color: var(--muted); font-size: 10px; font-weight: 700;
|
||||
}
|
||||
.view-switch button {
|
||||
min-height: 30px; border: 0; border-radius: 6px; padding: 4px 8px;
|
||||
background: transparent; color: var(--muted); font-size: 12px; font-weight: 700;
|
||||
}
|
||||
.filter-presets button:hover, .filter-presets button:focus-visible {
|
||||
border-color: var(--accent); color: var(--text); outline: none;
|
||||
}
|
||||
.view-switch button[aria-pressed="true"] { color: var(--text); }
|
||||
.view-switch button:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
|
||||
.stats { display: flex; flex: 0 0 auto; gap: 14px; color: var(--muted); }
|
||||
|
|
@ -71,6 +79,9 @@ aside { min-height: 0; overflow: hidden; padding: 16px; background: var(--panel)
|
|||
.panel-resizer.resizing::after { background: var(--accent); }
|
||||
.panel-resizer:focus-visible { outline: 1px solid var(--accent); outline-offset: -1px; }
|
||||
form { display: grid; flex: 0 0 auto; gap: 8px; }
|
||||
form > label, .filter-grid label {
|
||||
display: grid; gap: 6px; color: var(--muted); font-size: 11px; font-weight: 700;
|
||||
}
|
||||
input, select {
|
||||
width: 100%; border: 1px solid var(--line); border-radius: 8px;
|
||||
padding: 9px 10px; background: var(--panel-2); color: var(--text);
|
||||
|
|
@ -79,6 +90,9 @@ input:focus-visible, select:focus-visible {
|
|||
border-color: var(--accent); outline: 2px solid var(--accent); outline-offset: 1px;
|
||||
}
|
||||
.search-row { display: grid; grid-template-columns: 1fr auto; gap: 8px; }
|
||||
.filter-grid {
|
||||
display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 8px;
|
||||
}
|
||||
.button {
|
||||
border: 1px solid #277fa0; border-radius: 8px; padding: 8px 12px;
|
||||
background: #12384a; color: var(--text);
|
||||
|
|
@ -162,8 +176,16 @@ input:focus-visible, select:focus-visible {
|
|||
padding: 9px; background: var(--panel-2); color: var(--text);
|
||||
}
|
||||
.result:hover, .result:focus-visible { border-color: var(--accent); outline: none; }
|
||||
.result strong, .result span { display: block; overflow: hidden; text-overflow: ellipsis; }
|
||||
.result span { color: var(--muted); font-size: 12px; white-space: nowrap; }
|
||||
.result strong, .result span { display: block; overflow-wrap: anywhere; }
|
||||
.result > span { color: var(--muted); font-size: 11px; }
|
||||
.result-badges {
|
||||
display: flex !important; flex-wrap: wrap; gap: 4px; margin: 5px 0;
|
||||
}
|
||||
.result-badges small {
|
||||
border: 1px solid #31526d; border-radius: 999px; padding: 1px 6px;
|
||||
background: #0a1724; color: #b7c9da; font-size: 9px; font-weight: 750;
|
||||
letter-spacing: .04em; text-transform: uppercase;
|
||||
}
|
||||
.canvas { position: relative; min-width: 0; min-height: 0; overflow: hidden; }
|
||||
svg { width: 100%; height: 100%; background:
|
||||
radial-gradient(circle at 52% 46%, rgba(26, 65, 89, .58) 0, rgba(10, 28, 45, .46) 34%,
|
||||
|
|
@ -240,13 +262,24 @@ svg { width: 100%; height: 100%; background:
|
|||
.relationship-key-empty { margin: 2px 0; color: var(--muted); font-size: 11px; }
|
||||
.relationship-edge {
|
||||
fill: none; stroke-opacity: .74; stroke-width: 1.7;
|
||||
vector-effect: non-scaling-stroke;
|
||||
vector-effect: non-scaling-stroke; transition: opacity .16s, stroke-width .16s, filter .16s;
|
||||
}
|
||||
.edge-label {
|
||||
font-size: 9px; font-weight: 700; letter-spacing: .015em; pointer-events: none;
|
||||
paint-order: stroke; stroke: #07101a; stroke-width: 4px; stroke-linejoin: round;
|
||||
transition: opacity .16s;
|
||||
}
|
||||
.node { cursor: pointer; }
|
||||
.relationship-edge.trace-connected {
|
||||
stroke-opacity: 1; stroke-width: 3;
|
||||
filter: drop-shadow(0 0 7px currentcolor);
|
||||
}
|
||||
.relationship-edge.trace-muted, .edge-label.trace-muted { opacity: .13; }
|
||||
.edge-label.trace-connected { opacity: 1; font-size: 10px; }
|
||||
.node { cursor: pointer; transition: opacity .16s, filter .16s; }
|
||||
.node.trace-connected:not(.selected) {
|
||||
filter: drop-shadow(0 0 8px rgba(165, 243, 252, .28));
|
||||
}
|
||||
.node.trace-muted { opacity: .24; }
|
||||
.node:focus { outline: none; }
|
||||
.node .node-surface {
|
||||
stroke-width: 1.35; vector-effect: non-scaling-stroke;
|
||||
|
|
|
|||
|
|
@ -35,6 +35,34 @@
|
|||
</div>
|
||||
<label for="family">Family</label>
|
||||
<select id="family" name="family"><option value="">All families</option></select>
|
||||
<div class="filter-grid">
|
||||
<label>
|
||||
Node type
|
||||
<select id="kind" name="kind">
|
||||
<option value="">All node types</option>
|
||||
<option value="callable">Callable functions & methods</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Language
|
||||
<select id="language" name="language">
|
||||
<option value="">All languages</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<label for="capability">Capability</label>
|
||||
<select id="capability" name="capability">
|
||||
<option value="">All capabilities</option>
|
||||
<option value="logic">Logic available</option>
|
||||
<option value="source">Source available</option>
|
||||
</select>
|
||||
<div class="filter-presets" role="group" aria-label="Quick node filters">
|
||||
<button type="button" data-preset="logic">Logic-ready</button>
|
||||
<button type="button" data-preset="python">Python callables</button>
|
||||
<button type="button" data-preset="tests">Tests</button>
|
||||
<button type="button" data-preset="routes">Routes</button>
|
||||
<button type="button" data-preset="docs">Docs</button>
|
||||
</div>
|
||||
</form>
|
||||
<div class="results-context">
|
||||
<strong id="results-label">All nodes</strong>
|
||||
|
|
|
|||
|
|
@ -23,6 +23,35 @@ const state = {
|
|||
dialogDrag: null,
|
||||
leaseTimer: null,
|
||||
};
|
||||
const nodeKindOptions = Object.freeze([
|
||||
["function", "Function"],
|
||||
["method", "Method"],
|
||||
["class", "Class"],
|
||||
["module", "Module"],
|
||||
["package", "Package"],
|
||||
["route", "Route"],
|
||||
["command", "Command"],
|
||||
["service", "Service"],
|
||||
["plugin", "Plugin"],
|
||||
["test", "Test"],
|
||||
["table", "Table"],
|
||||
["view", "View"],
|
||||
["document", "Document"],
|
||||
["manual", "Manual"],
|
||||
["section", "Section"],
|
||||
]);
|
||||
const languageOptions = Object.freeze([
|
||||
["python", "Python"],
|
||||
["javascript", "JavaScript"],
|
||||
["typescript", "TypeScript"],
|
||||
["cpp", "C++"],
|
||||
["c", "C"],
|
||||
["csharp", "C#"],
|
||||
["rust", "Rust"],
|
||||
["java", "Java"],
|
||||
["go", "Go"],
|
||||
["sql", "SQL"],
|
||||
]);
|
||||
const relationStyles = Object.freeze({
|
||||
contains: {
|
||||
family: "Structure", color: "#60a5fa", dash: "", marker: "diamond-arrow",
|
||||
|
|
@ -183,8 +212,8 @@ const contributionStyles = Object.freeze({
|
|||
"logic-control": {
|
||||
label: "Control", section: "Loops & exception handling", color: "#c084fc", fill: "#302044",
|
||||
},
|
||||
"logic-merge": {
|
||||
label: "Merge", section: "Branch convergence", color: "#94a3b8", fill: "#252d39",
|
||||
"logic-convergence": {
|
||||
label: "Convergence", section: "Control paths reunite", color: "#94a3b8", fill: "#252d39",
|
||||
},
|
||||
"logic-terminal": {
|
||||
label: "Terminal", section: "Returns, raises & exits", color: "#fb7185", fill: "#41202a",
|
||||
|
|
@ -194,7 +223,7 @@ const contributionOrder = Object.freeze([
|
|||
"focus", "composition", "behavior", "dependency", "execution",
|
||||
"data", "evidence", "context", "related",
|
||||
"logic-entry", "logic-condition", "logic-action", "logic-control",
|
||||
"logic-merge", "logic-terminal",
|
||||
"logic-convergence", "logic-terminal",
|
||||
]);
|
||||
const compositionRelations = new Set(["contains", "defines", "defined_in"]);
|
||||
const behaviorRelations = new Set(["inherits", "implemented_by"]);
|
||||
|
|
@ -486,8 +515,31 @@ function selectNode(nodeId) {
|
|||
group.classList.toggle("selected", selected);
|
||||
group.setAttribute("aria-pressed", String(selected));
|
||||
}
|
||||
applyTraceHighlight(nodeId);
|
||||
return true;
|
||||
}
|
||||
function applyTraceHighlight(nodeId) {
|
||||
const graph = $("graph");
|
||||
const connected = new Set([nodeId]);
|
||||
for (const edge of graph.querySelectorAll(".relationship-edge")) {
|
||||
const direct = edge.dataset.sourceId === nodeId || edge.dataset.targetId === nodeId;
|
||||
edge.classList.toggle("trace-connected", direct);
|
||||
edge.classList.toggle("trace-muted", !direct);
|
||||
if (direct) {
|
||||
connected.add(edge.dataset.sourceId);
|
||||
connected.add(edge.dataset.targetId);
|
||||
}
|
||||
}
|
||||
for (const label of graph.querySelectorAll(".edge-label")) {
|
||||
const direct = label.dataset.sourceId === nodeId || label.dataset.targetId === nodeId;
|
||||
label.classList.toggle("trace-connected", direct);
|
||||
label.classList.toggle("trace-muted", !direct);
|
||||
}
|
||||
for (const group of graph.querySelectorAll(".node")) {
|
||||
group.classList.toggle("trace-connected", connected.has(group.dataset.nodeId));
|
||||
group.classList.toggle("trace-muted", !connected.has(group.dataset.nodeId));
|
||||
}
|
||||
}
|
||||
function centerSelectedNode() {
|
||||
const point = state.positions.get(state.selectedNode);
|
||||
if (!point) return false;
|
||||
|
|
@ -562,6 +614,39 @@ function renderOverview(data) {
|
|||
option.textContent = `${item.value} (${item.count})`;
|
||||
family.append(option);
|
||||
}
|
||||
const tagCounts = new Map((data.tags || []).map(
|
||||
(item) => [String(item.value), Number(item.count)],
|
||||
));
|
||||
const kind = $("kind");
|
||||
const callableCount = ["function", "method", "nested-function"]
|
||||
.reduce((total, tag) => total + (tagCounts.get(tag) || 0), 0);
|
||||
kind.options[1].textContent = `Callable functions & methods (${callableCount})`;
|
||||
for (const [value, label] of nodeKindOptions) {
|
||||
const count = tagCounts.get(value) || 0;
|
||||
if (!count) continue;
|
||||
const option = document.createElement("option");
|
||||
option.value = value;
|
||||
option.textContent = `${label} (${count})`;
|
||||
kind.append(option);
|
||||
}
|
||||
const language = $("language");
|
||||
for (const [value, label] of languageOptions) {
|
||||
const count = tagCounts.get(value) || 0;
|
||||
if (!count) continue;
|
||||
const option = document.createElement("option");
|
||||
option.value = value;
|
||||
option.textContent = `${label} (${count})`;
|
||||
language.append(option);
|
||||
}
|
||||
const capabilityCounts = new Map((data.capabilities || []).map(
|
||||
(item) => [String(item.value), Number(item.count)],
|
||||
));
|
||||
for (const option of $("capability").options) {
|
||||
const count = capabilityCounts.get(option.value);
|
||||
if (option.value && count !== undefined) {
|
||||
option.textContent = `${option.textContent} (${count})`;
|
||||
}
|
||||
}
|
||||
}
|
||||
function renderResults(items) {
|
||||
const results = $("results");
|
||||
|
|
@ -578,12 +663,26 @@ function renderResults(items) {
|
|||
button.type = "button";
|
||||
button.className = "result";
|
||||
const title = document.createElement("strong");
|
||||
title.textContent = item.title;
|
||||
title.textContent = nodeDisplayName(item);
|
||||
const badges = document.createElement("span");
|
||||
badges.className = "result-badges";
|
||||
const tags = new Set(item.tags || []);
|
||||
const kind = nodeKindLabel(item);
|
||||
const language = languageOptions.find(([value]) => tags.has(value))?.[1];
|
||||
for (const label of [kind, language]) {
|
||||
if (!label) continue;
|
||||
const badge = document.createElement("small");
|
||||
badge.textContent = label;
|
||||
badges.append(badge);
|
||||
}
|
||||
const id = document.createElement("span");
|
||||
id.textContent = item.node_id;
|
||||
id.textContent = item.source_anchor
|
||||
? `${item.source_path} · ${item.source_anchor}`
|
||||
: item.source_path;
|
||||
const family = document.createElement("span");
|
||||
family.textContent = `${item.family} · ${item.source_path}`;
|
||||
button.append(title, id, family);
|
||||
family.textContent = item.family;
|
||||
button.title = `${item.title}\n${item.node_id}`;
|
||||
button.append(title, badges, id, family);
|
||||
button.addEventListener("click", () => loadNode(item.node_id));
|
||||
results.append(button);
|
||||
}
|
||||
|
|
@ -843,7 +942,7 @@ function nodeContributionCategory(nodeId, data, topology) {
|
|||
if (["loop", "try", "except", "finally", "break", "continue"].includes(kind)) {
|
||||
return "logic-control";
|
||||
}
|
||||
if (kind === "merge") return "logic-merge";
|
||||
if (["merge", "convergence"].includes(kind)) return "logic-convergence";
|
||||
if (["return", "raise", "exit"].includes(kind)) return "logic-terminal";
|
||||
return "logic-action";
|
||||
}
|
||||
|
|
@ -946,11 +1045,65 @@ function layoutFlow(nodes, rootId, topology, sizes = nodeSizeMap(nodes, rootId))
|
|||
}
|
||||
return positions;
|
||||
}
|
||||
function layoutLogic(nodes, rootId, topology, sizes = nodeSizeMap(nodes, rootId)) {
|
||||
const positions = layoutFlow(nodes, rootId, topology, sizes);
|
||||
for (const [nodeId, point] of positions) {
|
||||
if (nodeId === rootId) continue;
|
||||
positions.set(nodeId, {...point, x: Math.abs(point.x)});
|
||||
function layoutLogic(nodes, rootId, topology, edges, sizes = nodeSizeMap(nodes, rootId)) {
|
||||
const layers = new Map();
|
||||
for (const node of nodes) {
|
||||
const hop = topology.get(node.node_id).hop;
|
||||
if (!layers.has(hop)) layers.set(hop, []);
|
||||
layers.get(hop).push(node);
|
||||
}
|
||||
const positions = new Map();
|
||||
const layerWidths = new Map([...layers].map(([hop, layer]) => [
|
||||
hop,
|
||||
Math.max(...layer.map((node) => sizes.get(node.node_id).width)),
|
||||
]));
|
||||
const layerX = new Map([[0, 0]]);
|
||||
for (const hop of [...layers.keys()].sort((a, b) => a - b).filter((value) => value > 0)) {
|
||||
const previous = layerX.get(hop - 1) || 0;
|
||||
layerX.set(
|
||||
hop,
|
||||
previous + (layerWidths.get(hop - 1) || 188) / 2
|
||||
+ (layerWidths.get(hop) || 188) / 2 + 190,
|
||||
);
|
||||
}
|
||||
const relationRank = new Map([
|
||||
["when_true", 0], ["case", 1], ["next", 2], ["when_false", 3],
|
||||
["exception", 4], ["loop", 5],
|
||||
]);
|
||||
for (const [hop, layer] of [...layers.entries()].sort((a, b) => a[0] - b[0])) {
|
||||
layer.sort((first, second) => {
|
||||
const firstIncoming = edges.filter((edge) => edge.target_id === first.node_id);
|
||||
const secondIncoming = edges.filter((edge) => edge.target_id === second.node_id);
|
||||
const parentY = (incoming) => {
|
||||
const points = incoming
|
||||
.map((edge) => positions.get(edge.source_id)?.y)
|
||||
.filter((value) => value !== undefined);
|
||||
return points.length
|
||||
? points.reduce((total, value) => total + value, 0) / points.length
|
||||
: 0;
|
||||
};
|
||||
const relation = (incoming) => Math.min(
|
||||
...incoming.map((edge) => relationRank.get(edge.relation) ?? 20),
|
||||
20,
|
||||
);
|
||||
return parentY(firstIncoming) - parentY(secondIncoming)
|
||||
|| relation(firstIncoming) - relation(secondIncoming)
|
||||
|| first.node_id.localeCompare(second.node_id);
|
||||
});
|
||||
const verticalGap = 96;
|
||||
const layerHeight = layer.reduce(
|
||||
(total, node) => total + sizes.get(node.node_id).height + verticalGap,
|
||||
-verticalGap,
|
||||
);
|
||||
let cursor = -layerHeight / 2;
|
||||
for (const node of layer) {
|
||||
const size = sizes.get(node.node_id);
|
||||
positions.set(node.node_id, {
|
||||
x: layerX.get(hop) || 0,
|
||||
y: cursor + size.height / 2,
|
||||
});
|
||||
cursor += size.height + verticalGap;
|
||||
}
|
||||
}
|
||||
return positions;
|
||||
}
|
||||
|
|
@ -1081,6 +1234,30 @@ function edgeEndpoints(source, target, sourceSize, targetSize) {
|
|||
y2: target.y - unitY * targetOffset,
|
||||
};
|
||||
}
|
||||
function logicEdgeGeometry(points, lane) {
|
||||
const {x1, y1, x2, y2} = points;
|
||||
const deltaX = x2 - x1;
|
||||
if (deltaX > 40) {
|
||||
const bend = Math.max(70, deltaX * .42);
|
||||
const controlY = lane * 14;
|
||||
return {
|
||||
path: `M ${x1} ${y1} C ${x1 + bend} ${y1 + controlY}, `
|
||||
+ `${x2 - bend} ${y2 + controlY}, ${x2} ${y2}`,
|
||||
label: {
|
||||
x: (x1 + x2) / 2,
|
||||
y: (y1 + y2) / 2 + controlY * .75 - 8,
|
||||
},
|
||||
};
|
||||
}
|
||||
const direction = lane % 2 === 0 ? -1 : 1;
|
||||
const archY = Math.min(y1, y2) + direction * (130 + Math.abs(lane) * 22);
|
||||
const reach = Math.max(90, Math.abs(deltaX) * .32);
|
||||
return {
|
||||
path: `M ${x1} ${y1} C ${x1 + reach} ${archY}, `
|
||||
+ `${x2 - reach} ${archY}, ${x2} ${y2}`,
|
||||
label: {x: (x1 + x2) / 2, y: archY - 8},
|
||||
};
|
||||
}
|
||||
function topologyRoleLabel(category) {
|
||||
const style = contributionStyles[category] || contributionStyles.related;
|
||||
return category === "focus" ? `${state.mode} focus` : `${style.label} contributor`;
|
||||
|
|
@ -1158,7 +1335,7 @@ function renderGraph(data, preserveSelection = false) {
|
|||
const categories = nodeCategoryMap(view, topology);
|
||||
const sizes = nodeSizeMap(view.nodes, view.root);
|
||||
const positions = state.mode === "logic"
|
||||
? layoutLogic(view.nodes, view.root, topology, sizes)
|
||||
? layoutLogic(view.nodes, view.root, topology, view.edges, sizes)
|
||||
: state.mode !== "nodes"
|
||||
? layoutFlow(view.nodes, view.root, topology, sizes)
|
||||
: layoutNodes(view.nodes, view.root, topology, sizes);
|
||||
|
|
@ -1173,6 +1350,11 @@ function renderGraph(data, preserveSelection = false) {
|
|||
}
|
||||
const edgeLayer = svgElement("g");
|
||||
const nodeLayer = svgElement("g");
|
||||
const outgoing = new Map();
|
||||
for (const edge of view.edges) {
|
||||
if (!outgoing.has(edge.source_id)) outgoing.set(edge.source_id, []);
|
||||
outgoing.get(edge.source_id).push(edge);
|
||||
}
|
||||
for (const edge of view.edges) {
|
||||
const source = positions.get(edge.source_id);
|
||||
const target = positions.get(edge.target_id);
|
||||
|
|
@ -1184,21 +1366,36 @@ function renderGraph(data, preserveSelection = false) {
|
|||
sizes.get(edge.source_id),
|
||||
sizes.get(edge.target_id),
|
||||
);
|
||||
const line = svgElement("line", {
|
||||
...points,
|
||||
const siblings = outgoing.get(edge.source_id);
|
||||
const lane = siblings.indexOf(edge) - (siblings.length - 1) / 2;
|
||||
const geometry = state.mode === "logic"
|
||||
? logicEdgeGeometry(points, lane)
|
||||
: {
|
||||
path: `M ${points.x1} ${points.y1} L ${points.x2} ${points.y2}`,
|
||||
label: {
|
||||
x: (points.x1 + points.x2) / 2,
|
||||
y: (points.y1 + points.y2) / 2 - 5,
|
||||
},
|
||||
};
|
||||
const line = svgElement("path", {
|
||||
d: geometry.path,
|
||||
class: "relationship-edge",
|
||||
stroke: style.color,
|
||||
"marker-end": `url(#${relationMarkerId(edge.relation)})`,
|
||||
"data-relation": edge.relation,
|
||||
"data-source-id": edge.source_id,
|
||||
"data-target-id": edge.target_id,
|
||||
});
|
||||
if (style.dash) line.setAttribute("stroke-dasharray", style.dash);
|
||||
edgeLayer.append(line);
|
||||
const label = svgElement("text", {
|
||||
x: (points.x1 + points.x2) / 2,
|
||||
y: (points.y1 + points.y2) / 2 - 5,
|
||||
x: geometry.label.x,
|
||||
y: geometry.label.y,
|
||||
class: "edge-label",
|
||||
fill: style.color,
|
||||
"text-anchor": "middle",
|
||||
"data-source-id": edge.source_id,
|
||||
"data-target-id": edge.target_id,
|
||||
});
|
||||
label.textContent = edge.label || relationLabel(edge.relation, edge.reversed);
|
||||
edgeLayer.append(label);
|
||||
|
|
@ -1291,6 +1488,7 @@ function renderGraph(data, preserveSelection = false) {
|
|||
nodeLayer.append(group);
|
||||
}
|
||||
svg.append(definitions, edgeLayer, nodeLayer);
|
||||
applyTraceHighlight(state.selectedNode);
|
||||
}
|
||||
function hideNode(nodeId) {
|
||||
if (!state.graph || nodeId === state.root) {
|
||||
|
|
@ -1512,16 +1710,25 @@ async function search() {
|
|||
const params = new URLSearchParams({
|
||||
q: $("search").value.trim(),
|
||||
family: $("family").value,
|
||||
kind: $("kind").value,
|
||||
language: $("language").value,
|
||||
capability: $("capability").value,
|
||||
limit: String(state.searchLimit),
|
||||
});
|
||||
const filtersActive = [
|
||||
$("search").value.trim(),
|
||||
$("family").value,
|
||||
$("kind").value,
|
||||
$("language").value,
|
||||
$("capability").value,
|
||||
].some(Boolean);
|
||||
try {
|
||||
setStatus("Searching validated index…");
|
||||
const data = await api(`search?${params}`);
|
||||
renderResults(data.results || []);
|
||||
setResultsContext(
|
||||
$("search").value.trim() || $("family").value
|
||||
? `${data.count} search results`
|
||||
: "All nodes",
|
||||
filtersActive ? `${data.count} filtered results` : "All nodes",
|
||||
filtersActive,
|
||||
);
|
||||
setStatus(`${data.count} matching node${data.count === 1 ? "" : "s"}`);
|
||||
} catch (error) {
|
||||
|
|
@ -1538,8 +1745,12 @@ async function filterByDescriptor(category, value) {
|
|||
closeNodeCard();
|
||||
setStatus(`Filtering ${category} ${value}…`);
|
||||
const data = await api(`filter?${params}`);
|
||||
$("search").value = "";
|
||||
$("family").value = category === "family" ? value : "";
|
||||
clearSearchFilters();
|
||||
if (category === "family") $("family").value = value;
|
||||
if (category === "tag") {
|
||||
if (nodeKindOptions.some(([tag]) => tag === value)) $("kind").value = value;
|
||||
if (languageOptions.some(([tag]) => tag === value)) $("language").value = value;
|
||||
}
|
||||
renderResults(data.results || []);
|
||||
setResultsContext(`${category}: ${value} (${data.total})`, true);
|
||||
const suffix = data.truncated ? ` · showing first ${data.count}` : "";
|
||||
|
|
@ -1548,6 +1759,32 @@ async function filterByDescriptor(category, value) {
|
|||
setStatus(error.message, true);
|
||||
}
|
||||
}
|
||||
function clearSearchFilters() {
|
||||
$("search").value = "";
|
||||
$("family").value = "";
|
||||
$("kind").value = "";
|
||||
$("language").value = "";
|
||||
$("capability").value = "";
|
||||
}
|
||||
function applyFilterPreset(preset) {
|
||||
clearSearchFilters();
|
||||
if (preset === "logic") {
|
||||
$("kind").value = "callable";
|
||||
$("capability").value = "logic";
|
||||
} else if (preset === "python") {
|
||||
$("kind").value = "callable";
|
||||
$("language").value = "python";
|
||||
} else if (preset === "tests") {
|
||||
$("kind").value = "test";
|
||||
} else if (preset === "routes") {
|
||||
$("kind").value = "route";
|
||||
} else if (preset === "docs") {
|
||||
$("kind").value = $("kind").querySelector('option[value="document"]')
|
||||
? "document"
|
||||
: "manual";
|
||||
}
|
||||
search();
|
||||
}
|
||||
async function loadNode(nodeId) {
|
||||
try {
|
||||
const showingFlow = state.mode === "flow";
|
||||
|
|
@ -1580,7 +1817,7 @@ async function loadNode(nodeId) {
|
|||
: showingLogic ? "control flow" : "neighborhood";
|
||||
const suffix = data.truncated ? " · truncated at the safety limit" : "";
|
||||
const status = showingLogic && !data.available
|
||||
? `No indexed Python logic is available for ${nodeId}`
|
||||
? `No indexed logic is available for ${nodeId}; use the Logic-ready filter`
|
||||
: `${data.nodes.length} nodes · ${data.edges.length} edges in ${scope}${suffix}`;
|
||||
setStatus(status, showingLogic && !data.available);
|
||||
history.replaceState(
|
||||
|
|
@ -1692,10 +1929,14 @@ function endDialogDrag(event) {
|
|||
state.dialogDrag = null;
|
||||
}
|
||||
$("search-form").addEventListener("submit", (event) => { event.preventDefault(); search(); });
|
||||
$("family").addEventListener("change", search);
|
||||
for (const id of ["family", "kind", "language", "capability"]) {
|
||||
$(id).addEventListener("change", search);
|
||||
}
|
||||
for (const button of document.querySelectorAll("[data-preset]")) {
|
||||
button.addEventListener("click", () => applyFilterPreset(button.dataset.preset));
|
||||
}
|
||||
$("clear-result-filter").addEventListener("click", () => {
|
||||
$("search").value = "";
|
||||
$("family").value = "";
|
||||
clearSearchFilters();
|
||||
search();
|
||||
});
|
||||
$("view-nodes").addEventListener("click", () => setViewMode("nodes"));
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue