import streamlit as st import requests import os import html import json API_URL = os.getenv("PUBLIC_API_URL", "https://nec.clients.septem.pro") + "/llm" NO_INFO_TEXT = "Информация отсутствует" IMAGE_EXTS = ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'tiff', 'webp'] @st.cache_data def load_layout(): path = os.path.join(os.path.dirname(__file__), "criteria_interface_layout.json") if not os.path.exists(path): return {} with open(path, "r", encoding="utf-8") as f: return json.load(f).get("by_shipping_type", {}) LAYOUT = load_layout() if 'session_id' not in st.session_state: st.session_state.session_id = None if 'current_report' not in st.session_state: st.session_state.current_report = None if 'report_loaded' not in st.session_state: st.session_state.report_loaded = False if 'ui_theme' not in st.session_state: st.session_state.ui_theme = "light" THEME_CONFIG = { "light": { "app_bg": "#f5f7fb", "card_bg": "#ffffff", "text": "#111827", "muted": "#6b7280", "border": "#d0d7de", "code_bg": "#f6f8fa", "hover": "#e9ecef", "shadow": "0 2px 4px rgba(0,0,0,0.05)" }, "dark": { "app_bg": "#0f172a", "card_bg": "#111827", "text": "#e5e7eb", "muted": "#94a3b8", "border": "#334155", "code_bg": "#1f2937", "hover": "#374151", "shadow": "0 2px 8px rgba(0,0,0,0.45)" }, } def apply_ui_theme(): colors = THEME_CONFIG[st.session_state.ui_theme] st.markdown(f""" """, unsafe_allow_html=True) def load_report(session_id: str): try: resp = requests.post(f"{API_URL}/generate-cargo-report", json={"session_id": session_id}) if resp.status_code == 200: st.session_state.current_report = resp.json() else: st.error(f"Ошибка загрузки отчёта: {resp.status_code}") except Exception as e: st.error(f"Не удалось соединиться с сервером: {e}") def criterion_card_html(number, criterion, value): if value is None: display = NO_INFO_TEXT elif isinstance(value, bool): display = "✅ Да" if value else "❌ Нет" else: display = str(value) return f"""
{html.escape(str(number))}. {html.escape(criterion)}
Ответ: {html.escape(display)}
""" def display_criteria_with_layout(report): criteria_results = report.get("criteria_results", []) if not criteria_results: st.info("Нет данных по критериям") return # Индексируем по номеру by_num = {item["number"]: item for item in criteria_results if "number" in item} shipping_type = report.get("shipping_type", "") layout_blocks = LAYOUT.get(shipping_type, {}).get("blocks", []) shown_numbers = set() # Рисуем блоки из layout for block in layout_blocks: if block["type"] == "pair": left = block["left"] right = block["right"] num_l = int(left["num"]) num_r = int(right["num"]) crit_l = by_num.get(num_l) crit_r = by_num.get(num_r) if crit_l and crit_r: left_card = criterion_card_html(num_l, crit_l["criterion"], crit_l["value"]) right_card = criterion_card_html(num_r, crit_r["criterion"], crit_r["value"]) st.markdown( f'
{left_card}{right_card}
', unsafe_allow_html=True ) shown_numbers.add(num_l) shown_numbers.add(num_r) elif block["type"] == "left_12": num = int(block["num"]) crit = by_num.get(num) if crit: st.markdown(criterion_card_html(num, crit["criterion"], crit["value"]), unsafe_allow_html=True) shown_numbers.add(num) # Оставшиеся критерии, не попавшие в layout remaining = [item for item in criteria_results if item["number"] not in shown_numbers] if remaining: st.markdown("---") for item in remaining: st.markdown(criterion_card_html(item["number"], item["criterion"], item["value"]), unsafe_allow_html=True) def display_all_attachments(report): sources = report.get('sources', []) if not sources: st.info("Нет писем с вложениями") return all_att = [] for email_idx, source in enumerate(sources): for att_idx, att in enumerate(source.get('attachments', [])): ext = att['filename'].split('.')[-1].lower() if '.' in att['filename'] else '' if ext in IMAGE_EXTS: continue all_att.append({ 'email_idx': email_idx, 'att_idx': att_idx, 'filename': att['filename'], 'subject': source.get('subject', ''), 'size': att.get('size', 0), }) if not all_att: st.info("Нет доступных вложений (изображения скрыты)") return for att in all_att: cols = st.columns([3,1,1]) cols[0].markdown(f"**📄 {att['filename']}**") cols[0].caption(f"Из письма: {att['subject']}") url = f"{API_URL}/attachments/{st.session_state.session_id}/{att['email_idx']}/{att['att_idx']}" cols[1].link_button("👁️ Открыть", url) cols[2].link_button("📥 Скачать", url) def main(): st.set_page_config(page_title="SEPTEM Cargo Analytics", page_icon="🚚", layout="wide") col1, col2 = st.columns([8, 2]) with col1: st.title("🚚 SEPTEM Cargo Analytics") with col2: theme_label = st.selectbox("Тема", ["Светлая", "Тёмная"], index=0 if st.session_state.ui_theme == "light" else 1) new_theme = "light" if theme_label == "Светлая" else "dark" if new_theme != st.session_state.ui_theme: st.session_state.ui_theme = new_theme st.rerun() apply_ui_theme() query_params = st.query_params session_id = query_params.get("session_id", [None]) if isinstance(session_id, list): session_id = session_id[0] if session_id else None if session_id and not st.session_state.report_loaded: st.session_state.session_id = session_id with st.spinner("Анализируем письма..."): load_report(session_id) st.session_state.report_loaded = True with st.sidebar: st.header("📋 Сессия") if st.session_state.session_id: st.success(f"ID: {st.session_state.session_id[:8]}...") if st.button("🔄 Обновить отчёт"): load_report(st.session_state.session_id) st.rerun() else: st.info("Нет активной сессии") if st.session_state.current_report: report = st.session_state.current_report st.metric("Писем", report.get('emails_count', 0)) if st.session_state.current_report: display_report(st.session_state.current_report) else: st.info("Отправьте письма через Outlook и нажмите «Анализировать», чтобы увидеть результат.") def display_report(report): tab1, tab2 = st.tabs(["📦 Обзор", "📎 Вложения"]) with tab1: st.subheader(f"Перевозка: {report.get('shipping_type', 'Не определён')}") display_criteria_with_layout(report) st.info("✉️ Генерация писем будет добавлена в следующей версии.") with tab2: display_all_attachments(report) if __name__ == "__main__": main()