"""Streamlit UI for the NL→SQL assistant. Modern light surface: white background, one calm indigo accent, soft shadows and rounded corners (Inter for text, JetBrains Mono for code). Controls live in a top bar — database, mode and the EN/RU language toggle; the sidebar keeps only the schema explorer, advanced retrieval knobs and clear-chat. Bilingual chrome (data questions stay in their natural language; the pipeline accepts both EN and RU). Run with: uv run streamlit run app/streamlit_app.py """ from __future__ import annotations import time from typing import Any, cast import streamlit as st from bootstrap import bootstrap, make_pipeline from components.output import render_output from components.schema_explorer import render_schema_explorer from components.show_working import render_show_working from components.welcome import render_welcome from i18n import t from nl_sql.agent.graph import PipelineRunResult, run_pipeline from samples import source_link_for from theme import inject_chrome def _render_lang_toggle(col_en: Any, col_ru: Any) -> None: """EN | RU joined pill in the top bar; the active segment inverts.""" lang = st.session_state.get("lang", "en") with col_en: if st.button( t("lang_en"), key="lang_en_btn", use_container_width=True, type="primary" if lang == "en" else "secondary", ): st.session_state.lang = "en" st.rerun() with col_ru: if st.button( t("lang_ru"), key="lang_ru_btn", use_container_width=True, type="primary" if lang == "ru" else "secondary", ): st.session_state.lang = "ru" st.rerun() def main() -> None: if "lang" not in st.session_state: st.session_state.lang = "en" st.set_page_config(page_title=t("page_title"), layout="wide") inject_chrome() try: registry, schema_index, sql_provider, explain_provider = bootstrap() except RuntimeError as exc: st.error(str(exc)) st.stop() st.session_state["_schema_index"] = schema_index db_ids = registry.ids() if not db_ids: st.error("No databases registered. Run scripts/download_data.py first.") st.stop() default_idx = ( db_ids.index("bird_california_schools") if "bird_california_schools" in db_ids else 0 ) # --- top bar (sticky): wordmark · database · mode · language with st.container(key="nl_topbar"): c_brand, c_db, c_mode, c_en, c_ru = st.columns( [2.4, 3, 3, 0.9, 0.9], vertical_alignment="center" ) with c_brand: st.markdown( "
NLSQL
", unsafe_allow_html=True, ) with c_db: db_id = st.selectbox( t("db_label"), db_ids, index=default_idx, label_visibility="collapsed" ) with c_mode: mode_opts = (t("mode_accurate"), t("mode_fast"), t("mode_debug")) mode_help = ( f"{t('mode_accurate')} — {t('mode_accurate_caption')}\n\n" f"{t('mode_fast')} — {t('mode_fast_caption')}\n\n" f"{t('mode_debug')} — {t('mode_debug_caption')}" ) mode = st.selectbox( t("mode_header"), mode_opts, index=0, label_visibility="collapsed", help=mode_help, ) _render_lang_toggle(c_en, c_ru) spec = registry.get(db_id) if mode == t("mode_fast"): fewshot_top_k = 0 verify_retry_on_empty = False else: fewshot_top_k = 3 verify_retry_on_empty = True # --- context line under the top bar ctx_parts = [f"{t('db_dialect')}: {spec.dialect}"] link = source_link_for(db_id) if link is not None: label, url = link ctx_parts.append(f"{t('db_source')}: {label}") if spec.description: ctx_parts.append(spec.description) st.markdown( f"
{' · '.join(ctx_parts)}
", unsafe_allow_html=True, ) # --- sidebar: schema explorer + advanced knobs + clear chat with st.sidebar: st.markdown( "
NLSQL
", unsafe_allow_html=True, ) render_schema_explorer(db_id) with st.expander(t("advanced_header"), expanded=False): schema_top_k = st.slider(t("schema_top_k"), 1, 10, 5) fk_hops = st.slider(t("fk_hops"), 0, 2, 1) table_budget = st.slider(t("table_budget"), 4, 20, 12) sort_schema_block = st.checkbox(t("sort_schema"), value=True) extended_sample_size = st.slider(t("sample_size"), 0, 8, 0) st.markdown("
", unsafe_allow_html=True) if st.button(t("clear_chat"), key="clear_chat_btn", use_container_width=True): st.session_state.messages = [] st.rerun() if "messages" not in st.session_state: st.session_state.messages = [] if not st.session_state.messages: render_welcome(db_id) for msg in st.session_state.messages: with st.chat_message(msg["role"]): if msg["role"] == "user": st.markdown(msg["content"]) else: _replay_assistant_turn(msg) typed = st.chat_input(t("ask_placeholder")) queued = st.session_state.pop("pending_question", None) question = queued or typed if not question: return st.session_state.messages.append({"role": "user", "content": question}) with st.chat_message("user"): st.markdown(question) pipeline = make_pipeline( registry, schema_index, sql_provider, explain_provider, schema_top_k=schema_top_k, fk_hops=fk_hops, table_budget=table_budget, sort_schema_block=sort_schema_block, extended_sample_size=extended_sample_size, fewshot_top_k=fewshot_top_k, verify_retry_on_empty=verify_retry_on_empty, ) with st.chat_message("assistant"): with st.spinner(t("spinner_generating")): t0 = time.perf_counter() try: result = run_pipeline( pipeline, question=question, db_id=db_id, dialect=spec.dialect, disable_repair=False, verify_retry_on_empty=verify_retry_on_empty, ) except Exception as exc: st.error(t("pipeline_crashed", kind=type(exc).__name__, msg=str(exc))) st.session_state.messages.append( {"role": "assistant", "error": str(exc), "question": question} ) return wall_ms = (time.perf_counter() - t0) * 1000 render_output(result.output_format, caption=result.caption) if result.sql: st.markdown(f"**{t('sql_label')}**") st.code(result.sql, language="sql") else: st.warning(t("no_sql")) st.caption(t("wall_model", wall=wall_ms, model=sql_provider.model)) render_show_working(result) st.session_state.messages.append( { "role": "assistant", "question": question, "result": result, "wall_ms": wall_ms, "model": sql_provider.model, } ) def _replay_assistant_turn(msg: dict[str, Any]) -> None: if msg.get("error"): st.error(t("pipeline_crashed", kind="prior", msg=msg["error"])) return result = cast(PipelineRunResult, msg["result"]) render_output(result.output_format, caption=result.caption) if result.sql: st.code(result.sql, language="sql") st.caption(t("wall_model", wall=msg.get("wall_ms", 0), model=msg.get("model", "?"))) render_show_working(result) if __name__ == "__main__": main()