214 lines
8.5 KiB
Python
214 lines
8.5 KiB
Python
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()
|