/
Chaizee
/
ZenithCode_Incident-LLM-analytics
Обзор
Документация
Войти
/
Chaizee
/
ZenithCode_Incident-LLM-analytics
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
app.py
659 строк
32 KB
Chaizee
fix: fix speed
12 июн 2026, 16:28
12 июн 2026, 16:28
b2745cc
Код
Авторство
О чём код?
from __future__ import annotations import base64 import html from io import BytesIO from pathlib import Path from textwrap import dedent from typing import Any import streamlit as st from loguru import logger from config import APP_ICON, APP_TITLE, ROOT from src.data_polars import load_excel_polars_with_stats from src.memory_guard import free_memory from src.pipeline_hybrid import PipelineResult, run_hybrid_pipeline from src.device_utils import gpu_runtime_summary from src.runtime_deps import gpu_runtime_notice, runtime_deps_error_message from src.ui_results import render_results_page REGION_NAME = "Омская область" ASSETS = ROOT / "assets" LOGOS = ASSETS / "logos" FONTS = ASSETS / "fonts" ILLUSTRATIONS = ASSETS / "illustrations" STATE: dict[str, Any] = { "file_bytes": None, "filename": None, "preview_total_rows": 0, "settings": {"region": REGION_NAME}, "result": None, "ui_theme": "Светлая", "sidebar_open": False, } def main() -> None: st.set_page_config(page_title=APP_TITLE, page_icon=APP_ICON, layout="wide", initial_sidebar_state="collapsed") deps_msg = runtime_deps_error_message() if deps_msg: st.error(deps_msg) st.stop() gpu_notice = gpu_runtime_notice() if gpu_notice: st.info(gpu_notice) _init_state() _render_styles() if st.session_state.get("sidebar_open", False): side_col, main_col = st.columns([0.26, 0.74], gap="large") with side_col: _side_panel() with main_col: _render_main_content() else: _render_main_content() def _render_main_content() -> None: _render_header(show_menu=not st.session_state.get("sidebar_open", False)) _page_workflow() def _reset_session_data() -> None: keep_theme = st.session_state.get("ui_theme", "Светлая") keep_sidebar = st.session_state.get("sidebar_open", False) for key, value in STATE.items(): st.session_state[key] = value.copy() if isinstance(value, dict) else value st.session_state.ui_theme = keep_theme st.session_state.sidebar_open = keep_sidebar for extra in ( "semantic_index", "semantic_index_path", "_search_backend", ): st.session_state.pop(extra, None) free_memory(deep=True) def _init_state() -> None: if st.session_state.pop("_reset_data", False): _reset_session_data() for key, value in STATE.items(): if key not in st.session_state: st.session_state[key] = value.copy() if isinstance(value, dict) else value def _b64(path: Path) -> str: try: return base64.b64encode(path.read_bytes()).decode("ascii") except FileNotFoundError: return "" def _img_src(path: Path, mime: str) -> str: encoded = _b64(path) return f"data:{mime};base64,{encoded}" if encoded else "" def _font_face(name: str, filename: str, weight: int) -> str: path = FONTS / filename if not path.exists(): path = ROOT / filename encoded = _b64(path) if not encoded: return "" ext = "opentype" if path.suffix.lower() == ".otf" else "truetype" return ( f"@font-face{{font-family:'{name}';src:url(data:font/{ext};base64,{encoded}) format('{ext}');" f"font-weight:{weight};font-style:normal;font-display:swap;}}" ) def _render_styles() -> None: is_dark = st.session_state.get("ui_theme") == "Тёмная" font_css = "\n".join( [ _font_face("CoFo Kak", "cofo-kak-black.otf", 900), _font_face("Avenir Next Cyr", "avenirnextcyr-light.ttf", 300), _font_face("Avenir Next Cyr", "avenirnextcyr-regular.ttf", 400), _font_face("Avenir Next Cyr", "avenirnextcyr-demi.ttf", 600), ] ) if is_dark: palette = { "bg": "#000501", "bg2": "#06120E", "surface": "#0C1814", "surface2": "#142620", "text": "#EAF7F4", "muted": "#99D19C", "line": "rgba(173,225,229,.12)", "line2": "rgba(121,199,197,.28)", "accent": "#73AB84", "accent2": "#79C7C5", "soft": "rgba(115,171,132,.18)", "shadow": "0 12px 32px rgba(0,5,1,.35)", "logo_bg": "rgba(12,24,20,.92)", "logo_shadow": "0 0 0 1px rgba(121,199,197,.18)", "app_bg": "linear-gradient(180deg, #000501 0%, #0A1A15 55%, #0C1814 100%)", } else: palette = { "bg": "#F2FAF9", "bg2": "#E4F5F3", "surface": "#FFFFFF", "surface2": "#EAF7F5", "text": "#000501", "muted": "#3F5F56", "line": "rgba(115,171,132,.28)", "line2": "rgba(121,199,197,.42)", "accent": "#73AB84", "accent2": "#79C7C5", "soft": "rgba(173,225,229,.45)", "shadow": "0 10px 28px rgba(0,5,1,.06)", "logo_bg": "rgba(255,255,255,.78)", "logo_shadow": "0 0 0 1px rgba(115,171,132,.16)", "app_bg": "linear-gradient(165deg, #ADE1E5 0%, #E8F7F5 32%, #F8FDFC 68%, #FFFFFF 100%)", } st.markdown( f""" <style> {font_css} :root {{ --sr-bg: {palette['bg']}; --sr-bg-2: {palette['bg2']}; --sr-surface: {palette['surface']}; --sr-surface-2: {palette['surface2']}; --sr-text: {palette['text']}; --sr-muted: {palette['muted']}; --sr-line: {palette['line']}; --sr-line-2: {palette['line2']}; --sr-accent: {palette['accent']}; --sr-accent-2: {palette['accent2']}; --sr-soft: {palette['soft']}; --sr-shadow: {palette['shadow']}; --sr-radius: 18px; --sr-logo-bg: {palette['logo_bg']}; --sr-logo-shadow: {palette['logo_shadow']}; }} * {{ box-sizing: border-box; }} .stApp {{ background: {palette['app_bg']}; color: var(--sr-text); font-family: 'Avenir Next Cyr', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; }} [data-testid="stAppViewContainer"], [data-testid="stHeader"], [data-testid="stToolbar"] {{ background: transparent; }} .block-container {{ max-width: 1600px; padding: 0.35rem 1.5rem 3rem 1.5rem; }} [data-testid="stAppViewContainer"] .main .block-container {{ padding-top: 0.35rem; }} .stApp h1, .stApp h2, .stApp h3, .stApp h4, .stApp p, .stApp li, .stApp label, [data-testid="stMarkdownContainer"], [data-testid="stMarkdownContainer"] * {{ font-family: 'Avenir Next Cyr', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; color: var(--sr-text); }} .sr-title {{ font-family: 'CoFo Kak', Impact, 'Arial Black', sans-serif !important; font-weight: 900 !important; font-size: clamp(3.5rem, 6.2vw, 6.4rem); line-height: .9; letter-spacing: .03em; color: var(--sr-text); margin: 0; text-transform: uppercase; max-width: 100%; display: inline-flex; flex-direction: column; align-items: flex-start; gap: 0; }} .sr-title, .sr-title * {{ font-family: 'CoFo Kak', Impact, 'Arial Black', sans-serif !important; font-weight: 900 !important; color: var(--sr-text) !important; }} .sr-title-line {{ display: block; white-space: nowrap; font-family: 'CoFo Kak', Impact, 'Arial Black', sans-serif !important; font-weight: 900 !important; }} .stApp h2 {{ font-size: 1.35rem; font-weight: 600; margin: 0 0 .8rem; }} .stApp h3 {{ font-size: 1.08rem; font-weight: 600; margin: 0 0 .6rem; }} .sr-topbar {{ display:flex; align-items:center; justify-content:space-between; gap: 1rem; margin-bottom: 0.85rem; min-height: 0; }} .sr-brands {{ display:flex; align-items:center; gap: .75rem; min-width:0; padding: 0; background: transparent; box-shadow: none; border-radius: 0; }} .sr-gov-logo {{height:34px; max-width:220px; width:auto; object-fit:contain; display:block;}} .sr-signal-logo {{height:52px; max-width:260px; width:auto; object-fit:contain; display:block;}} .sr-divider {{height:28px; width:1px; background:var(--sr-line-2); flex:0 0 1px;}} .sr-context {{ color:var(--sr-muted) !important; font-size:.94rem; white-space:nowrap; }} .sr-side-card {{ position: sticky; top: .75rem; }} .sr-menu-icon {{ width:42px; height:42px; border-radius:14px; background:var(--sr-soft); display:flex; align-items:center; justify-content:center; color:var(--sr-accent); font-size:1.25rem; font-weight:600; margin-bottom:.25rem; }} .sr-side-title {{ color: var(--sr-text); font-weight: 600; font-size: 1rem; margin: 0 0 .45rem; }} .sr-status-line {{ color: var(--sr-muted); font-size: .92rem; line-height: 1.45; margin: .35rem 0; overflow-wrap: anywhere; }} .sr-hero {{ position:relative; overflow:hidden; padding: 0.35rem 0 2rem 0; margin-bottom: 0.75rem; min-height: 320px; border-bottom: 1px solid var(--sr-line); }} .sr-hero-city {{ position:absolute; right:0; bottom:0; width:min(52vw, 650px); max-width:100%; opacity: 1; pointer-events:none; }} .sr-hero-content {{ position:relative; z-index:1; max-width: 1100px; padding-top: .25rem; }} .sr-eyebrow {{ color:var(--sr-accent) !important; text-transform:uppercase; letter-spacing:.18em; font-size:.82rem; font-weight:600; margin-bottom: 1.25rem; }} .sr-subtitle {{ color:var(--sr-accent) !important; font-size:1.42rem; line-height:1.35; font-weight:600; margin: 1.4rem 0 1.1rem 0; }} .sr-description {{ color:var(--sr-muted) !important; max-width:820px; line-height:1.82; font-size:1.06rem; margin:0; }} .sr-stepper {{ display:grid; grid-template-columns: repeat(3, 1fr); gap:.75rem; margin: 1.25rem 0 1.15rem; }} .sr-step {{ display:flex; align-items:center; gap:.75rem; padding:.72rem .85rem; border:1px solid var(--sr-line); border-radius: 14px; background: var(--sr-surface); min-height: 56px; }} .sr-step.active {{ background: var(--sr-soft); border-color: rgba(115,171,132,.45); }} .sr-step-num {{ width:32px; height:32px; border-radius:50%; display:flex; align-items:center; justify-content:center; font-weight:600; border:1px solid var(--sr-line-2); color:var(--sr-muted); flex:0 0 32px; }} .sr-step.active .sr-step-num {{ background:var(--sr-accent); border-color:var(--sr-accent); color: .sr-step-label {{ color:var(--sr-text); font-size:.96rem; font-weight:600; }} .sr-grid {{ display:grid; grid-template-columns:minmax(0,2.2fr) minmax(280px,.95fr); gap:1rem; align-items:stretch; }} .sr-cards3 {{ display:grid; grid-template-columns: repeat(3, minmax(0,1fr)); gap:1.15rem; margin-top:1.15rem; }} .sr-card {{ background: var(--sr-surface); border:1px solid var(--sr-line); border-radius: var(--sr-radius); padding:1.4rem; box-shadow:var(--sr-shadow); }} .sr-card.soft {{ background: var(--sr-bg-2); }} .sr-card-title {{ font-size:1.1rem; font-weight:600; margin-bottom:.65rem; color:var(--sr-text); }} .sr-card-text {{ color:var(--sr-muted) !important; font-size:.94rem; line-height:1.55; margin:0; }} .sr-upload-zone {{ border:1.5px dashed rgba(115,171,132,.55); border-radius:16px; background:var(--sr-bg-2); min-height: 168px; display:flex; align-items:center; justify-content:center; text-align:center; padding:1.2rem; margin: .75rem 0 1rem; }} .sr-upload-icon {{font-size:2.6rem; color:var(--sr-accent); line-height:1; margin-bottom:.45rem;}} .sr-required-list {{ display:grid; gap:.54rem; margin-top:.6rem; }} .sr-check {{display:flex; align-items:flex-start; gap:.55rem; color:var(--sr-muted); font-size:.95rem;}} .sr-check span:first-child {{color:var(--sr-accent); font-weight:600;}} .sr-info {{ margin-top:.7rem; padding-top:.7rem; border-top:1px solid var(--sr-line); color:var(--sr-muted); font-size:.85rem; line-height:1.45; }} .sr-mini-table-wrap {{ width:100%; overflow-x:auto; border:1px solid var(--sr-line); border-radius:12px; margin-top:.7rem; }} .sr-mini-table {{ border-collapse:collapse; min-width:860px; width:100%; font-size:.82rem; }} .sr-mini-table th, .sr-mini-table td {{ border-right:1px solid var(--sr-line); padding:.62rem .7rem; white-space:nowrap; color:var(--sr-muted); }} .sr-mini-table th {{ background:var(--sr-surface-2); color:var(--sr-text); font-weight:600; }} .sr-link {{ color:var(--sr-accent) !important; font-weight:600; margin-top:.8rem; display:inline-block; }} .sr-feature-icon {{ width:42px; height:42px; border-radius:14px; background:var(--sr-soft); display:flex; align-items:center; justify-content:center; color:var(--sr-accent); font-size:1.3rem; margin-bottom:.75rem; }} .sr-panel-title {{ font-size:1.25rem; font-weight:600; margin-bottom:.5rem; }} .sr-callout {{ border-left:3px solid var(--sr-accent); background:var(--sr-soft); border-radius:0 12px 12px 0; padding:.9rem 1rem; color:var(--sr-muted); line-height:1.55; margin:.75rem 0 1rem; }} .sr-section-label {{ color: var(--sr-text); font-size: 1.25rem; font-weight: 600; margin: 1.5rem 0 .85rem; }} .sr-summary-card {{ display: flex; gap: 1rem; background: var(--sr-surface); border: 1px solid var(--sr-line); border-radius: var(--sr-radius); padding: 1.25rem 1.35rem; line-height: 1.7; white-space: pre-wrap; }} .sr-summary-icon {{ font-size: 1.5rem; flex: 0 0 auto; }} .sr-summary-body {{ color: var(--sr-muted); font-size: .96rem; }} section[data-testid="stSidebar"] {{ display: none !important; }} [data-testid="stSidebar"] h1, [data-testid="stSidebar"] h2, [data-testid="stSidebar"] h3, [data-testid="stSidebar"] p, [data-testid="stSidebar"] label, [data-testid="stSidebar"] span {{ color: var(--sr-text); }} [data-testid="stSidebar"] .stRadio > div {{ gap:.25rem; }} [data-testid="stSidebar"] [role="radiogroup"] label {{ padding:.65rem .7rem; border-radius:12px; margin:.1rem 0; }} [data-testid="stSidebar"] [role="radiogroup"] label:has(input:checked) {{ background: var(--sr-soft); border-left:3px solid var(--sr-accent); }} .stButton > button, .stDownloadButton > button, button[data-testid="baseButton-secondary"], button[data-testid="baseButton-primary"] {{ border-radius:14px !important; min-height:46px !important; font-weight:600 !important; box-shadow:none !important; border:1px solid var(--sr-line-2) !important; transition: .16s ease !important; background: var(--sr-surface) !important; color: var(--sr-text) !important; }} button[data-testid="baseButton-primary"] {{ background: var(--sr-accent) !important; color: }} .stButton > button:disabled, .stDownloadButton > button:disabled, button:disabled {{ background: var(--sr-surface-2) !important; color: var(--sr-muted) !important; opacity: 1 !important; }} .stButton > button:disabled *, .stDownloadButton > button:disabled *, button:disabled * {{ color: var(--sr-muted) !important; opacity: 1 !important; }} .stButton > button:hover, .stDownloadButton > button:hover {{ transform: translateY(-1px); border-color:var(--sr-accent) !important; color: var(--sr-text) !important; }} button[kind="primary"], button[data-testid="baseButton-primary"] {{ background: var(--sr-accent) !important; color: button[kind="primary"] p, button[data-testid="baseButton-primary"] p {{ color: button:disabled, button[data-testid="baseButton-primary"]:disabled, button[data-testid="baseButton-secondary"]:disabled {{ background: var(--sr-surface-2) !important; border-color: var(--sr-line-2) !important; color: var(--sr-muted) !important; opacity: 1 !important; }} button:disabled p, button:disabled span, button:disabled div {{ color: var(--sr-muted) !important; }} .sr-stepper-gap {{ height: 1.25rem; }} div[data-testid="column"] .stButton button[kind="secondary"] {{ background: var(--sr-surface) !important; color: var(--sr-text) !important; }} div[data-testid="column"] .stButton button[kind="primary"], div[data-testid="column"] button[data-testid="baseButton-primary"] {{ background: var(--sr-accent) !important; color: border-color: var(--sr-accent) !important; }} div[data-testid="column"] .stButton button[kind="primary"] p, div[data-testid="column"] button[data-testid="baseButton-primary"] p {{ color: div[data-testid="stTabs"] button[data-baseweb="tab"][aria-selected="true"] {{ color: var(--sr-accent) !important; border-bottom-color: var(--sr-accent) !important; }} div[data-testid="stTabs"] button[data-baseweb="tab"]:hover {{ color: var(--sr-accent-2) !important; }} .stFileUploader section {{ background: var(--sr-bg-2) !important; border:1px solid var(--sr-line-2) !important; border-radius:14px !important; padding:.75rem !important; min-height:52px !important; }} .stFileUploader [data-testid="stFileUploaderDropzoneInstructions"], .stFileUploader small, .stFileUploader svg, [data-testid="stTooltipIcon"] {{ display:none !important; }} .stFileUploader button {{ font-size:0 !important; min-height:38px !important; padding:.5rem 1rem !important; background:var(--sr-surface) !important; color:var(--sr-text) !important; }} .stFileUploader button * {{ display:none !important; }} .stFileUploader button::after {{ content:'Выбрать файл'; font-size:.92rem; font-family:'Avenir Next Cyr', system-ui, sans-serif; color:var(--sr-text); }} div[data-baseweb="select"] > div, .stTextInput input, textarea, input {{ background-color:var(--sr-surface) !important; color:var(--sr-text) !important; border-color:var(--sr-line-2) !important; border-radius:12px !important; }} [data-testid="stMetric"] {{ background: var(--sr-surface); border:1px solid var(--sr-line); border-radius:16px; padding:.9rem 1rem; box-shadow:none; }} [data-testid="stMetricValue"] {{ color:var(--sr-accent); font-weight:600; }} [data-testid="stDataFrame"], [data-testid="stTable"] {{ border:1px solid var(--sr-line); border-radius:16px; overflow:hidden; }} .stAlert, div[data-testid="stExpander"] {{ border-radius:16px; }} header[data-testid="stHeader"] {{ height: 0 !important; min-height: 0 !important; background: transparent !important; pointer-events: none !important; }} .stDeployButton, [data-testid="stToolbar"] {{ display:none !important; }} div[data-testid="stHorizontalBlock"]:has(.sr-sidebar-marker) {{ align-items: flex-start; }} div[data-testid="stHorizontalBlock"]:has(.sr-sidebar-marker) > div[data-testid="column"]:first-child {{ padding-top: 0 !important; animation: sr-drawer-in 0.34s cubic-bezier(0.22, 1, 0.36, 1) both; }} div[data-testid="stHorizontalBlock"]:has(.sr-sidebar-marker) > div[data-testid="column"]:last-child {{ animation: sr-content-shift 0.34s cubic-bezier(0.22, 1, 0.36, 1) both; }} div[data-testid="stHorizontalBlock"]:has(.sr-sidebar-marker) > div[data-testid="column"]:first-child [data-testid="stVerticalBlock"] {{ gap: .45rem; }} div[data-testid="stHorizontalBlock"]:has(.sr-sidebar-marker) > div[data-testid="column"]:first-child [data-testid="stVerticalBlockBorderWrapper"] {{ position: sticky; top: .35rem; background: var(--sr-surface); border-color: var(--sr-line); border-radius:18px; box-shadow: var(--sr-shadow); padding-top: .15rem; transition: box-shadow 0.3s ease, transform 0.3s ease; }} @keyframes sr-drawer-in {{ from {{ opacity: 0; transform: translateX(-20px); }} to {{ opacity: 1; transform: translateX(0); }} }} @keyframes sr-content-shift {{ from {{ opacity: 0.88; transform: translateX(12px); }} to {{ opacity: 1; transform: translateX(0); }} }} div[data-testid="stHorizontalBlock"]:has(.sr-header-strip) {{ align-items: center !important; margin-bottom: 0.15rem; }} div[data-testid="stHorizontalBlock"]:has(.sr-header-strip) > div[data-testid="column"]:first-child .stButton button {{ min-height: 40px !important; padding: 0.45rem 0.7rem !important; font-size: 0.9rem !important; }} .sr-wide-section {{ width:100%; margin-top: 1.25rem; }} .sr-dataframe-wide [data-testid="stDataFrame"] {{ width: 100% !important; }} .sr-dataframe-wide [data-testid="stDataFrame"] > div {{ width: 100% !important; }} .stMarkdown a[href^="#"], a.anchor-link, a.header-link, .stHeading a, [data-testid="stMarkdownContainer"] a[href^="#"] {{ display:none !important; }} .sr-title a, .sr-title svg {{ display:none !important; }} .material-symbols-rounded, .material-icons, [class*="material-symbols"], [class*="material-icons"] {{ font-size:0 !important; width:0 !important; overflow:hidden !important; }} @media (max-width: 980px) {{ section[data-testid="stSidebar"] {{ z-index: 100000 !important; }} .block-container {{ padding: 1rem 1rem 2rem 1rem; }} .sr-side-card {{ position: relative; top: 0; margin-bottom: 1rem; }} .sr-drawer-spacer, .sr-collapsed-caption {{ display:none !important; }} .sr-grid, .sr-cards3, .sr-stepper {{ grid-template-columns:1fr; }} .sr-hero {{ min-height:auto; padding: .6rem 0 1.6rem 0; }} .sr-hero-city {{ position:relative; width:100%; margin-top:1.1rem; opacity:.82; }} .sr-topbar {{ align-items:flex-start; flex-direction:column; }} .sr-title {{ font-size: clamp(2.8rem, 11vw, 4.2rem); }} }} @media (max-width: 480px) {{ .block-container {{ padding-left: 8px; padding-right: 8px; padding-top: 8px; }} .sr-side-card {{ border-radius: 14px; }} .sr-card {{ padding: 1rem; border-radius: 14px; }} .sr-title {{ font-size: clamp(2.1rem, 13vw, 3rem); letter-spacing:.03em; }} .sr-subtitle {{ font-size:1.05rem; }} .sr-description {{ font-size:.94rem; }} .sr-gov-logo {{height:38px; max-width:240px;}} .sr-signal-logo {{height:62px; max-width:250px;}} .sr-context {{font-size:.78rem; white-space:normal;}} }} </style> """, unsafe_allow_html=True, ) def _side_panel() -> None: st.markdown('<span class="sr-sidebar-marker"></span>', unsafe_allow_html=True) with st.container(border=True): st.markdown('<div class="sr-side-card">', unsafe_allow_html=True) st.markdown('<div class="sr-menu-icon">☰</div>', unsafe_allow_html=True) st.markdown('<div class="sr-side-title">Статус</div>', unsafe_allow_html=True) current_file = html.escape(str(st.session_state.filename or "не загружен")) analysis_status = "завершён" if st.session_state.result else "не запущен" st.markdown(f'<div class="sr-status-line"><b>Файл:</b> {current_file}</div>', unsafe_allow_html=True) st.markdown(f'<div class="sr-status-line"><b>Анализ:</b> {analysis_status}</div>', unsafe_allow_html=True) st.divider() st.markdown('<div class="sr-side-title">Внешний вид</div>', unsafe_allow_html=True) st.radio("Тема интерфейса", ["Светлая", "Тёмная"], key="ui_theme", horizontal=False, label_visibility="collapsed") if st.button("Скрыть меню", key="hide_sidebar", use_container_width=True): st.session_state.sidebar_open = False st.rerun() if st.button("Очистить данные", use_container_width=True): st.session_state["_reset_data"] = True st.rerun() st.markdown('</div>', unsafe_allow_html=True) def _render_topbar_html() -> str: gov = _img_src(LOGOS / "government_omsk.png", "image/png") signal = _img_src(LOGOS / "signal_region.svg", "image/svg+xml") return dedent( f""" <div class="sr-topbar"> <div class="sr-brands"> {'<img class="sr-gov-logo" src="' + gov + '" alt="Правительство Омской области">' if gov else '<b>Правительство Омской области</b>'} <div class="sr-divider"></div> {'<img class="sr-signal-logo" src="' + signal + '" alt="Сигнал региона">' if signal else '<b>Сигнал региона</b>'} </div> <div class="sr-context">Аналитика обращений граждан · Омская область</div> </div> """ ).strip() def _render_header(*, show_menu: bool = False) -> None: city = _img_src(ILLUSTRATIONS / "omsk_line.svg", "image/svg+xml") if show_menu: st.markdown('<span class="sr-header-strip"></span>', unsafe_allow_html=True) menu_col, bar_col = st.columns([0.1, 0.9], gap="small", vertical_alignment="center") with menu_col: if st.button("☰ Меню", key="open_sidebar", use_container_width=True): st.session_state.sidebar_open = True st.rerun() with bar_col: st.markdown(_render_topbar_html(), unsafe_allow_html=True) else: st.markdown(_render_topbar_html(), unsafe_allow_html=True) st.markdown( dedent( f""" <section class="sr-hero"> <div class="sr-hero-content"> <div class="sr-eyebrow">Единый аналитический контур для обращений граждан</div> <div class="sr-title"><span class="sr-title-line">СИГНАЛ</span><span class="sr-title-line">РЕГИОНА</span></div> <div class="sr-subtitle">Платформа анализа обращений граждан</div> <p class="sr-description"> Сервис предназначен для автоматизированной обработки массива обращений граждан до 400 000 записей. Он выделяет обращения, содержащие проблему, определяет уровень критичности, группирует данные по муниципалитетам и населённым пунктам и формирует аналитический отчёт. </p> </div> {'<img class="sr-hero-city" src="' + city + '" alt="">' if city else ''} </section> """ ).strip(), unsafe_allow_html=True, ) def _page_workflow() -> None: st.markdown( """ <div class="sr-card"> <div class="sr-panel-title">Загрузка и анализ реестра обращений</div> <p class="sr-card-text">Загрузите реестр в формате Excel и запустите анализ. Результаты появятся на этой же странице.</p> </div> """, unsafe_allow_html=True, ) uploaded = st.file_uploader( "Файл реестра обращений", type=["xlsx", "xls"], key="incident_file_uploader", ) read_click = st.button( "Проверить файл", type="secondary", use_container_width=True, disabled=uploaded is None, ) if uploaded and read_click: try: raw = uploaded.read() loaded = load_excel_polars_with_stats(BytesIO(raw)) st.session_state.file_bytes = raw st.session_state.filename = uploaded.name st.session_state.preview_total_rows = loaded.loaded_rows st.session_state.preview_raw_rows = loaded.raw_rows st.session_state.preview_skipped_text = loaded.skipped_text st.session_state.preview_skipped_municipality = loaded.skipped_municipality st.session_state.result = None for key in ( "semantic_index", "semantic_index_path", "last_citizen_response", "map_district_picker_Все", ): st.session_state.pop(key, None) free_memory() except Exception as exc: st.error(f"Ошибка чтения файла: {exc}") file_ready = bool(st.session_state.get("file_bytes")) result: PipelineResult | None = st.session_state.get("result") if file_ready and not result: filename = html.escape(str(st.session_state.filename or "файл")) total_rows = int(st.session_state.get("preview_total_rows", 0)) raw_rows = int(st.session_state.get("preview_raw_rows", total_rows)) skipped_text = int(st.session_state.get("preview_skipped_text", 0)) skipped_muni = int(st.session_state.get("preview_skipped_municipality", 0)) msg = ( f"Файл загружен: {filename}. " f"В реестре {raw_rows:,} строк, к анализу {total_rows:,}." ).replace(",", " ") if skipped_text or skipped_muni: msg += ( f" Пропущено при чтении: без текста — {skipped_text:,}, " f"«Омская область, другое» — {skipped_muni:,}." ).replace(",", " ") st.success(msg) bar = st.progress(0.0) status = st.empty() def on_progress(frac: float, msg: str) -> None: bar.progress(min(1.0, frac)) status.info(msg) info = gpu_runtime_summary() st.caption( f"Режим: {info['gpu_label']} · batch {info['embed_batch_size']} · " f"чанк {info['classify_chunk']} · потоки CPU: {info['cpu_threads']} · LLM включена" ) if st.button("Запустить анализ", type="primary", use_container_width=True): try: free_memory(deep=True) analysis_result = run_hybrid_pipeline( BytesIO(st.session_state.file_bytes), region_name=REGION_NAME, progress=on_progress, ) st.session_state.result = analysis_result bar.progress(1.0) problems = analysis_result.analytics.get("problems_count", 0) if problems == 0: status.warning( "Анализ завершён, но проблемные обращения не найдены. " "Проверьте состав файла." ) else: status.success( f"Готово: найдено {problems:,} проблемных обращений.".replace(",", " ") ) st.rerun() except Exception as exc: logger.exception("pipeline") status.error(f"Ошибка анализа: {exc}") if result: st.divider() st.markdown("### Результаты анализа") render_results_page(result, region_name=REGION_NAME) if __name__ == "__main__": main()