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()