diff --git a/search_app.py b/search_app.py index eb056f0..fb30dfa 100644 --- a/search_app.py +++ b/search_app.py @@ -745,7 +745,7 @@ def create_app(config: AppConfig = CONFIG) -> Flask: } def build_full_graph_payload( - max_nodes: int, *, highlight_id: Optional[str] = None + max_nodes: Optional[int], *, highlight_id: Optional[str] = None ) -> Dict[str, Any]: """ Attempt to render the entire reference graph by gathering every video that @@ -776,7 +776,8 @@ def create_app(config: AppConfig = CONFIG) -> Flask: nodes: Dict[str, Dict[str, Any]] = {} links: List[Dict[str, Any]] = [] link_seen: Set[Tuple[str, str, str]] = set() - batch_size = min(500, max(50, max_nodes * 2)) + node_limit = max_nodes if max_nodes and max_nodes > 0 else None + batch_size = 500 if node_limit is None else min(500, max(50, node_limit * 2)) truncated = False def ensure_node(node_id: Optional[str], doc: Optional[Dict[str, Any]] = None) -> bool: @@ -799,7 +800,7 @@ def create_app(config: AppConfig = CONFIG) -> Flask: if not existing.get("date") and doc.get("date"): existing["date"] = doc.get("date") return True - if len(nodes) >= max_nodes: + if node_limit is not None and len(nodes) >= node_limit: return False channel_name = None channel_id = None @@ -836,7 +837,7 @@ def create_app(config: AppConfig = CONFIG) -> Flask: if not hits: break for hit in hits: - if len(nodes) >= max_nodes: + if node_limit is not None and len(nodes) >= node_limit: truncated = True stop_fetch = True break @@ -996,14 +997,17 @@ def create_app(config: AppConfig = CONFIG) -> Flask: depth = 1 depth = max(0, min(depth, 3)) - try: - max_nodes = int(request.args.get("max_nodes", "200")) - except ValueError: - max_nodes = 200 - max_nodes = max(10, min(max_nodes, 800 if full_graph else 400)) + if full_graph: + max_nodes = None + else: + try: + max_nodes = int(request.args.get("max_nodes", "200")) + except ValueError: + max_nodes = 200 + max_nodes = max(10, min(max_nodes, 400)) if full_graph: - payload = build_full_graph_payload(max_nodes, highlight_id=video_id or None) + payload = build_full_graph_payload(None, highlight_id=video_id or None) else: payload = build_graph_payload(video_id, depth, max_nodes) if not payload["nodes"]: @@ -1011,7 +1015,9 @@ def create_app(config: AppConfig = CONFIG) -> Flask: jsonify({"error": f"Video '{video_id}' was not found in the index."}), 404, ) - payload["meta"]["max_nodes"] = max_nodes + payload["meta"]["max_nodes"] = ( + len(payload["nodes"]) if full_graph else max_nodes + ) return jsonify(payload) @app.route("/api/years") diff --git a/static/graph.js b/static/graph.js index c817c11..daf1c24 100644 --- a/static/graph.js +++ b/static/graph.js @@ -135,6 +135,7 @@ let currentMaxNodes = sanitizeMaxNodes(maxNodesInput.value); let currentSimulation = null; let currentFullGraph = false; + let previousMaxNodesValue = maxNodesInput ? maxNodesInput.value : "200"; function setStatus(message, isError = false) { if (!statusEl) return; @@ -165,6 +166,18 @@ if (depthInput) { depthInput.disabled = enabled; } + if (maxNodesInput) { + if (enabled) { + previousMaxNodesValue = maxNodesInput.value || previousMaxNodesValue || "200"; + maxNodesInput.value = "0"; + maxNodesInput.disabled = true; + } else { + if (maxNodesInput.disabled) { + maxNodesInput.value = previousMaxNodesValue || "200"; + } + maxNodesInput.disabled = false; + } + } if (videoInput) { if (enabled) { videoInput.removeAttribute("required"); @@ -181,10 +194,11 @@ } if (fullGraphMode) { params.set("full_graph", "1"); + params.set("max_nodes", "0"); } else { params.set("depth", String(depth)); + params.set("max_nodes", String(maxNodes)); } - params.set("max_nodes", String(maxNodes)); const response = await fetch(`/api/graph?${params.toString()}`); if (!response.ok) { const errorPayload = await response.json().catch(() => ({})); @@ -445,8 +459,8 @@ setStatus("Please enter a video ID.", true); return; } - const safeDepth = wantsFull ? 0 : sanitizeDepth(depth); - const safeMaxNodes = sanitizeMaxNodes(maxNodes); + const safeDepth = wantsFull ? currentDepth || 1 : sanitizeDepth(depth); + const safeMaxNodes = wantsFull ? 0 : sanitizeMaxNodes(maxNodes); if (updateInputs) { videoInput.value = sanitizedId; @@ -630,11 +644,12 @@ if (fullGraphMode) { next.searchParams.set("full_graph", "1"); next.searchParams.delete("depth"); + next.searchParams.set("max_nodes", "0"); } else { next.searchParams.set("depth", String(depth)); next.searchParams.delete("full_graph"); + next.searchParams.set("max_nodes", String(maxNodes)); } - next.searchParams.set("max_nodes", String(maxNodes)); if (labelSize && labelSize !== "normal") { next.searchParams.set("label_size", labelSize); } else { @@ -647,7 +662,11 @@ const params = new URLSearchParams(window.location.search); const videoId = sanitizeId(params.get("video_id")); const depth = sanitizeDepth(params.get("depth") || ""); - const maxNodes = sanitizeMaxNodes(params.get("max_nodes") || ""); + const rawMaxNodes = params.get("max_nodes"); + let maxNodes = sanitizeMaxNodes(rawMaxNodes || ""); + if (rawMaxNodes && rawMaxNodes.trim() === "0") { + maxNodes = 0; + } const labelSizeParam = params.get("label_size"); const fullGraphParam = params.get("full_graph"); const viewFull = @@ -656,7 +675,7 @@ videoInput.value = videoId; } depthInput.value = String(depth); - maxNodesInput.value = String(maxNodes); + maxNodesInput.value = String(viewFull ? 0 : maxNodes); if (fullGraphToggle) { fullGraphToggle.checked = !!viewFull; }