NEKReport/web_ui_v2.py

214 lines
8.5 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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"""
<style>
:root {{
--septem-app-bg: {colors["app_bg"]};
--septem-card-bg: {colors["card_bg"]};
--septem-text: {colors["text"]};
--septem-muted: {colors["muted"]};
--septem-border: {colors["border"]};
--septem-code-bg: {colors["code_bg"]};
--septem-hover: {colors["hover"]};
--septem-shadow: {colors["shadow"]};
}}
div[data-testid="stAppViewContainer"] {{ background: var(--septem-app-bg); }}
div[data-testid="stSidebar"] {{ background: var(--septem-card-bg); }}
</style>""", 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"""
<div style='border:1px solid var(--septem-border); border-radius:8px; padding:8px 12px;
margin-bottom:8px; background:var(--septem-card-bg); box-sizing:border-box;'>
<b>{html.escape(str(number))}. {html.escape(criterion)}</b><br>
<span style='color:var(--septem-muted);'>Ответ:</span> {html.escape(display)}
</div>"""
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'<div style="display:flex; gap:8px; margin-bottom:8px;">{left_card}{right_card}</div>',
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()