""" Hardcoded Queries tab β runs QueryExecutor methods against Neo4j. Query definitions live in queries/hardcoded.py. Each entry declares which QueryExecutor method to call and what UI inputs to render. No code changes are needed here when adding new queries. Special input type "node_picker": Runs a vector search on the user's term for a given node_type, shows a scrollable list of matches, and on click loads the selected label and auto-triggers the main query. Required field in input spec: node_type : Neo4j label to search (e.g. "UNESCOconcept", "Breakthrough") Optional fields: top_k : number of candidates shown (default 7) """ from __future__ import annotations from collections import defaultdict import pandas as pd import streamlit as st from db.neo4j_client import get_neo4j_resources from db.vector_client import vector_search from queries.hardcoded import HARDCODED_QUERIES from queries.mappings import apply_mapping # Per-type defaults used by the node picker _PICKER_ICONS: dict[str, str] = { "UNESCOconcept": "π", "Breakthrough": "π‘", "Platform": "ποΈ", "Emerging topic": "π±", "SDGtarget": "π―", "SDGgoal": "π", "SDGindicator": "π", "OECDfield": "π¬", } _PICKER_THRESHOLDS: dict[str, float] = { "UNESCOconcept": 0.55, "Breakthrough": 0.60, "Platform": 0.55, "Emerging topic": 0.55, "SDGtarget": 0.55, "SDGgoal": 0.45, "SDGindicator": 0.50, "OECDfield": 0.50, } # --------------------------------------------------------------------------- # Node picker (vector search β scrollable results β click to select) # --------------------------------------------------------------------------- def _render_node_picker(inp: dict, query_id: str) -> str: """ Render a vector-search-backed node picker for any node_type. Returns the selected label string, or '' if nothing is selected yet. Side-effect: sets session_state[f"hq_{query_id}_auto_run"] = True on click. """ node_type = inp["node_type"] icon = _PICKER_ICONS.get(node_type, "β’") threshold = _PICKER_THRESHOLDS.get(node_type, 0.50) key_base = f"hq_{query_id}_{inp['key']}" selected_key = f"{key_base}_selected" results_key = f"{key_base}_results" selected = st.session_state.get(selected_key, "") # ββ Already selected β show badge + clear button ββββββββββββββββββββββ if selected: display = st.session_state.get(f"{selected_key}_display", selected) version = st.session_state.get(f"{selected_key}_version") version_suffix = f" ({version})" if version else "" col_sel, col_clr = st.columns([5, 1]) with col_sel: st.success(f"{icon} **{display}**{version_suffix}") with col_clr: if st.button("β Clear", key=f"{key_base}_clear"): st.session_state.pop(selected_key, None) st.session_state.pop(f"{selected_key}_display", None) st.session_state.pop(f"{selected_key}_version", None) st.session_state.pop(results_key, None) st.rerun() return selected # ββ Search input ββββββββββββββββββββββββββββββββββββββββββββββββββββββ search_term = st.text_input( inp.get("label", f"Search for a {node_type}"), placeholder=inp.get("placeholder", ""), key=f"{key_base}_search_input", ) if st.button("Search", key=f"{key_base}_search_btn", disabled=not search_term.strip()): mapped_term, was_mapped = apply_mapping(search_term) st.session_state[f"{key_base}_mapped"] = mapped_term if was_mapped else "" with st.spinner(f"Searching {node_type}β¦"): results, err = vector_search( mapped_term, top_k=inp.get("top_k", 7) * 15, threshold=threshold, node_label=node_type, ) if err: st.error(f"Vector search error: {err}") return "" # Deduplicate: prefer pref_label_en, fall back to original_text # Store (score, radar_version, node_id) per label value_prop = inp.get("value_prop") # if set, use this field as stored value display_node_id = inp.get("display_node_id", False) # show node_id as prefix in button/badge groups: dict[str, tuple[float, str | None, str | None]] = {} for r in results: lbl = r.get("pref_label_en") or r.get("original_text", "") if lbl: prev_score = groups.get(lbl, (0.0, None, None))[0] if r["score"] > prev_score: groups[lbl] = (r["score"], r.get("radar_version"), r.get("node_id")) top = sorted(groups.items(), key=lambda x: x[1][0], reverse=True)[: inp.get("top_k", 7)] st.session_state[results_key] = top # ββ Scrollable results ββββββββββββββββββββββββββββββββββββββββββββββββ value_prop = inp.get("value_prop") display_node_id = inp.get("display_node_id", False) mapped_label = st.session_state.get(f"{key_base}_mapped", "") if mapped_label: st.caption(f"Searched as: *{mapped_label}*") top = st.session_state.get(results_key) if top is not None: if top: with st.container(height=220): for i, (lbl, (score, version, node_id)) in enumerate(top): version_suffix = f" ({version})" if version else "" node_id_prefix = f"[{node_id}] " if display_node_id and node_id else "" stored_value = node_id if value_prop == "node_id" and node_id else lbl if st.button( f"{icon} {node_id_prefix}{lbl}{version_suffix} Β· {score:.3f}", key=f"{key_base}_pick_{i}", use_container_width=True, ): st.session_state[selected_key] = stored_value st.session_state[f"{selected_key}_display"] = f"[{node_id}] {lbl}" if display_node_id and node_id else lbl st.session_state[f"{selected_key}_version"] = version st.session_state[f"hq_{query_id}_auto_run"] = True st.rerun() else: st.warning(f"No {node_type} nodes found above threshold.") return "" # --------------------------------------------------------------------------- # Generic input collection # --------------------------------------------------------------------------- def _collect_inputs(query_def: dict) -> tuple[dict, bool]: """Render input widgets and return (collected_kwargs, all_required_filled).""" collected: dict = {} all_filled = True concept_inputs = [i for i in query_def["inputs"] if i["type"] == "node_picker"] text_inputs = [i for i in query_def["inputs"] if i["type"] == "text"] other_inputs = [i for i in query_def["inputs"] if i["type"] not in ("text", "node_picker")] # Node pickers (full width, vector-search backed) for inp in concept_inputs: val = _render_node_picker(inp, query_def["id"]) collected[inp["key"]] = val if not val: all_filled = False # Plain text inputs for inp in text_inputs: val = st.text_input( inp["label"], placeholder=inp.get("placeholder", ""), help=inp.get("help"), key=f'hq_{query_def["id"]}_{inp["key"]}', ) collected[inp["key"]] = val if not val.strip(): all_filled = False # Selects / checkboxes / numbers β rendered in a single row of columns if other_inputs: cols = st.columns(len(other_inputs)) for col, inp in zip(cols, other_inputs): with col: if inp["type"] == "select": opts = inp["options"] idx = opts.index(inp["default"]) if inp["default"] in opts else 0 opt_labels = inp.get("option_labels", {}) collected[inp["key"]] = st.selectbox( inp["label"], opts, index=idx, format_func=lambda v, _m=opt_labels: _m.get(v, str(v)), help=inp.get("help"), key=f'hq_{query_def["id"]}_{inp["key"]}', ) elif inp["type"] == "checkbox": collected[inp["key"]] = st.checkbox( inp["label"], value=inp["default"], help=inp.get("help"), key=f'hq_{query_def["id"]}_{inp["key"]}', ) elif inp["type"] == "number": collected[inp["key"]] = int(st.number_input( inp["label"], min_value=inp.get("min", 1), max_value=inp.get("max", 500), value=inp["default"], help=inp.get("help"), key=f'hq_{query_def["id"]}_{inp["key"]}', )) return collected, all_filled # --------------------------------------------------------------------------- # Result helpers # --------------------------------------------------------------------------- def _flatten_results(results: list[dict]) -> list[dict]: """Convert list values to comma-separated strings for dataframe display. Also folds radar_version into breakthrough_name where present.""" flat = [] for row in results: flat_row = { k: (", ".join(str(x) for x in v) if isinstance(v, list) else v) for k, v in row.items() } version = flat_row.pop("radar_version", None) if version is not None and "breakthrough_name" in flat_row: flat_row["breakthrough_name"] = f"{flat_row['breakthrough_name']} ({version})" flat.append(flat_row) return flat _SKOS_COLORS = { "IS_BROADER": "#3498DB", "IS_NARROWER": "#9B59B6", "IS_BROADER_CONCEPT": "#3498DB", "IS_NARROWER_CONCEPT": "#9B59B6", "IS_RELATED_CONCEPT": "#F39C12", } _REL_COLORS = {"ADVANCES": "#2ECC71", "REQUIRES": "#E74C3C"} def _rel_badge(rel: str, color: str) -> str: return (f'{rel}') def _node_span(label: str, icon: str, bold: bool = False) -> str: weight = "700" if bold else "400" return f'{icon} {label}' def _path_card(parts: list[str]) -> None: st.markdown( '
{query_def["method"]}