From a7d68971dd8d2027b3b3dac0f84f01f53916224e Mon Sep 17 00:00:00 2001 From: adish-rmr Date: Sun, 8 Feb 2026 19:50:34 +0100 Subject: [PATCH] add functions esposition and ingredients --- app.py | 15 ++- pages/exposition_page.py | 121 +++++++++++++++++++++++ pages/ingredients_page.py | 196 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 331 insertions(+), 1 deletion(-) create mode 100644 pages/exposition_page.py create mode 100644 pages/ingredients_page.py diff --git a/app.py b/app.py index 0ca1496..3e05826 100644 --- a/app.py +++ b/app.py @@ -108,6 +108,16 @@ def home(): with st.expander("📝 Registro degli aggiornamenti"): # Placeholder for future versions st.markdown(""" + ### v0.3 + *v0.3.0 - 2026-02-08 + - Aggiunta pagina per la gestione dei preset dei parametri di esposizione + * Permette di creare, modificare e salvare preset personalizzati per i calcoli di esposizione + * In futuro per calcolare il MoS bisognerà prima selezionare un preset di esposizione + - Aggiunta pagina per la creazione degli ingredienti a partire da un CAS: + * Facendo una ricerca in 'Ingredienti' e selezionando un CAS, è possibile cliccare su 'Ricerca' per generare un nuovo ingrediente nel database a partire da quel CAS. + * Gli ingredienti creati in questo modo hanno già i dati di base, tossicologici e quelli normativi da CosIng + * E' consigliato cercare nuovi ingredienti tramite questa nuova funzione + ### v0.2 *v0.2.1 - 2026-01-13* - Fix minore su ricerca CosIng @@ -139,13 +149,16 @@ home_page = st.Page(home, title="Home", icon="🏠", default=True) echa_page = st.Page("pages/echa.py", title="ECHA Database", icon="🧪") cosing_page = st.Page("pages/cosing.py", title="CosIng", icon="💄") dap_page = st.Page("pages/dap.py", title="DAP", icon="🧬") +exposition_page = st.Page("pages/exposition_page.py", title="Esposizione", icon="☀️") +ingredients_page = st.Page("pages/ingredients_page.py", title="Ingredienti", icon="📋") #pubchem_page = st.Page("pages/pubchem.py", title="PubChem", icon="🧬") #cir_page = st.Page("pages/cir.py", title="CIR", icon="📊") pg = st.navigation({ "Ricerca": [home_page], - "Database": [echa_page, cosing_page, dap_page] + "Calcolatore MoS": [exposition_page, ingredients_page], + "Ricerche Online": [echa_page, cosing_page, dap_page] }) pg.run() \ No newline at end of file diff --git a/pages/exposition_page.py b/pages/exposition_page.py new file mode 100644 index 0000000..30d9db6 --- /dev/null +++ b/pages/exposition_page.py @@ -0,0 +1,121 @@ +import streamlit as st +import requests +import pandas as pd + +API_BASE = "https://api.cosmoguard.it/api/v1/" + +EXPOSURE_ROUTES = ["Dermal", "Oral", "Inhalation", "Ocular"] + +RITENZIONE_PRESETS = { + "Leave-on (1.0)": 1.0, + "Rinse-off (0.01)": 0.01, + "Dentifricio (0.05)": 0.05, + "Collutorio (0.10)": 0.10, + "Tintura (0.10)": 0.10, +} + +st.set_page_config(page_title="Gestione Esposizione", layout="wide") +st.title("Gestione Preset Esposizione") + +tab_crea, tab_lista = st.tabs(["Crea Preset", "Preset Esistenti"]) + +# --- TAB CREAZIONE --- +with tab_crea: + st.subheader("Nuovo Preset di Esposizione") + + col1, col2 = st.columns(2) + + with col1: + preset_name = st.text_input("Nome Preset", placeholder="es. Crema viso leave-on") + tipo_prodotto = st.text_input("Tipo Prodotto", placeholder="es. Crema") + luogo_applicazione = st.text_input("Luogo Applicazione", placeholder="es. Viso") + + with col2: + sup_esposta = st.number_input("Superficie esposta (cm2)", min_value=1, max_value=17500, value=565) + freq_applicazione = st.number_input("Frequenza applicazione (al giorno)", min_value=1, value=1) + qta_giornaliera = st.number_input("Quantita giornaliera (g/die)", min_value=0.01, value=1.54, step=0.01, format="%.2f") + + st.markdown("---") + col3, col4, col5 = st.columns(3) + + with col3: + st.markdown("**Vie esposizione normali**") + esp_normali = st.multiselect("Normali", EXPOSURE_ROUTES, default=["Dermal"], key="esp_norm") + + with col4: + st.markdown("**Vie esposizione secondarie**") + esp_secondarie = st.multiselect("Secondarie", EXPOSURE_ROUTES, default=[], key="esp_sec") + + with col5: + st.markdown("**Vie esposizione nano**") + esp_nano = st.multiselect("Nano", EXPOSURE_ROUTES, default=["Dermal"], key="esp_nano") + + st.markdown("---") + ritenzione_label = st.selectbox("Fattore di ritenzione", list(RITENZIONE_PRESETS.keys())) + ritenzione = RITENZIONE_PRESETS[ritenzione_label] + + # Preview payload + payload = { + "preset_name": preset_name, + "tipo_prodotto": tipo_prodotto, + "luogo_applicazione": luogo_applicazione, + "esp_normali": esp_normali, + "esp_secondarie": esp_secondarie, + "esp_nano": esp_nano, + "sup_esposta": sup_esposta, + "freq_applicazione": freq_applicazione, + "qta_giornaliera": qta_giornaliera, + "ritenzione": ritenzione, + } + + with st.expander("Anteprima JSON"): + st.json(payload) + + if st.button("Crea Preset", type="primary", disabled=not preset_name): + try: + resp = requests.post(f"{API_BASE}/esposition/create", json=payload, timeout=10) + data = resp.json() + if data.get("success"): + st.success(f"Preset '{preset_name}' creato con successo (id: {data['data']['id_preset']})") + else: + st.error(f"Errore: {data.get('error', 'Sconosciuto')}") + except requests.ConnectionError: + st.error("Impossibile connettersi all'API. Verifica che il server sia attivo.") + except Exception as e: + st.error(f"Errore: {e}") + +# --- TAB LISTA --- +with tab_lista: + st.subheader("Preset Esistenti") + + if st.button("Aggiorna lista"): + st.rerun() + + try: + resp = requests.get(f"{API_BASE}/esposition/presets", timeout=10) + data = resp.json() + + if data.get("success") and data.get("data"): + st.info(f"Trovati {data['total']} preset") + + df = pd.DataFrame(data["data"]) + display_cols = [ + "preset_name", "tipo_prodotto", "luogo_applicazione", + "sup_esposta", "freq_applicazione", "qta_giornaliera", + "ritenzione", "esposizione_calcolata", "esposizione_relativa", + ] + existing = [c for c in display_cols if c in df.columns] + st.dataframe(df[existing], use_container_width=True) + + with st.expander("Dettaglio completo"): + for preset in data["data"]: + st.markdown(f"**{preset['preset_name']}**") + st.json(preset) + st.markdown("---") + else: + st.warning("Nessun preset trovato.") + + except requests.ConnectionError: + st.error("Impossibile connettersi all'API. Verifica che il server sia attivo.") + except Exception as e: + st.error(f"Errore nel caricamento: {e}") diff --git a/pages/ingredients_page.py b/pages/ingredients_page.py new file mode 100644 index 0000000..cb8c6a4 --- /dev/null +++ b/pages/ingredients_page.py @@ -0,0 +1,196 @@ +import streamlit as st +import requests +import pandas as pd + +API_BASE = "https://api.cosmoguard.it/api/v1/" + +st.set_page_config(page_title="Ricerca Ingredienti", layout="wide") +st.title("Ricerca Ingredienti per CAS") + +tab_ricerca, tab_risultato, tab_inventario = st.tabs(["Ricerca", "Risultato", "Inventario"]) + +# --- State --- +if "ingredient_data" not in st.session_state: + st.session_state.ingredient_data = None +if "ingredient_cas" not in st.session_state: + st.session_state.ingredient_cas = "" + +# --- TAB INVENTARIO --- +with tab_inventario: + st.subheader("Ingredienti gia acquisiti") + + if st.button("Aggiorna inventario"): + st.rerun() + + try: + resp = requests.get(f"{API_BASE}/ingredients/list", timeout=10) + result = resp.json() + + if result.get("success") and result.get("data"): + st.info(f"Trovati {result['total']} ingredienti nel database") + + df = pd.DataFrame(result["data"]) + df["dap"] = df["dap"].map({True: "OK", False: "-"}) + df["cosing"] = df["cosing"].map({True: "OK", False: "-"}) + df["tox"] = df["tox"].map({True: "OK", False: "-"}) + df = df.rename(columns={ + "cas": "CAS", + "dap": "DAP", + "cosing": "COSING", + "tox": "TOX", + "created_at": "Data Acquisizione", + }) + + display_cols = ["CAS", "DAP", "COSING", "TOX", "Data Acquisizione"] + existing = [c for c in display_cols if c in df.columns] + st.dataframe(df[existing], use_container_width=True, hide_index=True) + else: + st.warning("Nessun ingrediente trovato nel database.") + + except requests.ConnectionError: + st.error("Impossibile connettersi all'API. Verifica che il server sia attivo.") + except Exception as e: + st.error(f"Errore nel caricamento: {e}") + +# --- TAB RICERCA --- +with tab_ricerca: + st.subheader("Cerca un ingrediente") + + cas_input = st.text_input("CAS Number", placeholder="es. 64-17-5") + + if st.button("Cerca", type="primary", disabled=not cas_input): + with st.spinner(f"Ricerca in corso per {cas_input}..."): + try: + resp = requests.post( + f"{API_BASE}/ingredients/search", + json={"cas": cas_input}, + timeout=120, + ) + result = resp.json() + + if result.get("success") and result.get("data"): + st.session_state.ingredient_data = result["data"] + st.session_state.ingredient_cas = cas_input + st.success(f"Ingrediente {cas_input} trovato") + else: + st.session_state.ingredient_data = None + st.error(result.get("error", f"Nessun dato per CAS {cas_input}")) + + except requests.ConnectionError: + st.error("Impossibile connettersi all'API. Verifica che il server sia attivo.") + except Exception as e: + st.error(f"Errore: {e}") + +# --- TAB RISULTATO --- +with tab_risultato: + data = st.session_state.ingredient_data + + if data is None: + st.info("Effettua una ricerca per visualizzare i risultati.") + else: + cas = data.get("cas", "") + st.subheader(f"Ingrediente: {cas}") + + # --- Header con INCI e data --- + col_h1, col_h2 = st.columns(2) + with col_h1: + inci = data.get("inci") or [] + st.markdown(f"**INCI:** {', '.join(inci) if inci else 'N/A'}") + with col_h2: + st.markdown(f"**Data creazione:** {data.get('creation_date', 'N/A')}") + + st.markdown("---") + + # --- DAP Info --- + st.subheader("DAP Info") + dap = data.get("dap_info") + if dap: + col_d1, col_d2, col_d3 = st.columns(3) + with col_d1: + st.metric("Peso Molecolare (Da)", dap.get("molecular_weight", "N/A")) + st.metric("Log Pow", dap.get("log_pow", "N/A")) + with col_d2: + st.metric("TPSA", dap.get("tpsa", "N/A")) + st.metric("Punto Fusione (C)", dap.get("melting_point") or "N/A") + with col_d3: + dap_val = dap.get("dap_value", 0.5) + st.metric("DAP Value", f"{dap_val * 100:.0f}%") + st.metric("Alta Ionizzazione", dap.get("high_ionization") or "N/A") + else: + st.warning("Dati DAP non disponibili") + + st.markdown("---") + + # --- COSING Info --- + st.subheader("COSING Info") + cosing_list = data.get("cosing_info") + if cosing_list: + for i, cosing in enumerate(cosing_list): + if len(cosing_list) > 1: + st.markdown(f"**Voce {i + 1}**") + + col_c1, col_c2 = st.columns(2) + with col_c1: + names = cosing.get("common_names", []) + st.markdown(f"**Nomi comuni:** {', '.join(names) if names else 'N/A'}") + funcs = cosing.get("functionName", []) + if funcs: + st.markdown("**Funzioni:**") + for f in funcs: + st.markdown(f"- {f}") + + with col_c2: + annex = cosing.get("annex", []) + if annex: + st.markdown("**Annex/Restrizioni:**") + for a in annex: + st.markdown(f"- {a}") + else: + st.markdown("**Annex:** Nessuna restrizione") + + other = cosing.get("otherRestrictions", []) + if other: + st.markdown("**Altre restrizioni:**") + for o in other: + st.markdown(f"- {o}") + + restriction = cosing.get("cosmeticRestriction", "") + if restriction: + st.warning(f"Restrizione cosmetica: {restriction}") + + if i < len(cosing_list) - 1: + st.divider() + else: + st.warning("Dati COSING non disponibili") + + st.markdown("---") + + # --- Toxicity --- + st.subheader("Tossicologia") + tox = data.get("toxicity") + if tox: + # Best case highlight + best = tox.get("best_case") + if best: + st.success( + f"Miglior indicatore: **{best['indicator']}** = {best['value']} {best['unit']} " + f"({best['route']}) — Fattore: {tox.get('factor', 'N/A')}" + ) + + # Indicators table + indicators = tox.get("indicators", []) + if indicators: + df = pd.DataFrame(indicators) + col_order = ["indicator", "value", "unit", "route", "toxicity_type"] + existing = [c for c in col_order if c in df.columns] + df = df[existing] + df.columns = ["Indicatore", "Valore", "Unita", "Via", "Tipo"][:len(existing)] + st.dataframe(df, use_container_width=True, hide_index=True) + else: + st.warning("Dati tossicologici non disponibili") + + st.markdown("---") + + # --- JSON completo --- + with st.expander("JSON completo"): + st.json(data)