Skip to content

Commit cd9ae47

Browse files
author
Raffaele Crafa
committed
fix: added a first version of pdf generation (still need to be adjusted)
1 parent 8141563 commit cd9ae47

File tree

4 files changed

+336
-9
lines changed

4 files changed

+336
-9
lines changed

app.py

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from src.clients import list_providers
2323
from src.core import ChatbotAgent, FinancialAdvisorAgent
2424
from src.models import FinancialProfile, Portfolio
25+
from src.ui.pdf_generator import PDFGenerator # <-- NUOVO IMPORT
2526
from src.ui.settings_page import show_settings_page
2627

2728
MONTECARLO_MIN_ASSET_VOLATILITY = float(
@@ -292,26 +293,32 @@ def initialize_financial_advisor(
292293
raise
293294

294295

296+
# TODO: vedere se eliminare
297+
# Funzione per smooth scroll della pagina
295298
def _scroll_to_bottom():
296299
"""
297300
Force scroll to bottom by targeting the specific Streamlit main container.
298301
"""
299302
js = """
300303
<script>
301304
function forceScroll() {
305+
// Cerca il contenitore scrollabile principale di Streamlit
306+
// I selettori coprono varie versioni di Streamlit
302307
var scrollable_div = window.parent.document.querySelector('section.main') ||
303308
window.parent.document.querySelector('.main') ||
304309
window.parent.document.querySelector('[data-testid="stMain"]');
305310
306311
if (scrollable_div) {
312+
// Forza lo scroll alla fine dell'altezza totale del contenuto
307313
scrollable_div.scrollTop = scrollable_div.scrollHeight;
308314
}
309315
}
310316
317+
// Esegui più volte per "vincere" contro il rendering dinamico di Plotly e i Toast
311318
setTimeout(forceScroll, 100);
312319
setTimeout(forceScroll, 500);
313320
setTimeout(forceScroll, 1000);
314-
setTimeout(forceScroll, 2000);
321+
setTimeout(forceScroll, 2000); // Un ultimo tentativo dopo 2 secondi per sicurezza
315322
</script>
316323
"""
317324
components.html(js, height=0, width=0)
@@ -1850,8 +1857,11 @@ def main():
18501857
with st.chat_message(message["role"]):
18511858
st.markdown(message["content"])
18521859

1860+
# --- INPUT SEMPRE PRESENTE ---
1861+
# Manteniamo la chat input renderizzata ma disabilitata se la conversazione è finita.
1862+
# Questo impedisce al browser di perdere il focus e "saltare" in alto.
18531863
prompt = st.chat_input(
1854-
"Assessment completed. See the results above.",
1864+
"Ask me about your finances...",
18551865
disabled=st.session_state.conversation_completed,
18561866
)
18571867

@@ -1864,6 +1874,9 @@ def main():
18641874
duration="long",
18651875
)
18661876

1877+
# TODO: vedere se elimanrla
1878+
_scroll_to_bottom()
1879+
18671880
logger.debug("Conversation is completed")
18681881

18691882
# Generate and display portfolio
@@ -1931,6 +1944,9 @@ def main():
19311944
"Your financial profile and PAC metrics have been extracted and analyzed."
19321945
)
19331946

1947+
# TODO: vedere se eliminare
1948+
_scroll_to_bottom()
1949+
19341950
# Financial Profile in an expanded section
19351951
with st.expander(
19361952
"📊 View Your Financial Profile & Summary", expanded=False
@@ -1943,12 +1959,44 @@ def main():
19431959
"- Click 'Clear Conversation' to start a new assessment or 'Change Provider' to start over"
19441960
)
19451961

1962+
# --- AGGIUNGO QUI IL PULSANTE DI ESPORTAZIONE PDF ---
1963+
1964+
if (
1965+
st.session_state.financial_profile
1966+
and st.session_state.generated_portfolio
1967+
):
1968+
# 1. Recupera la configurazione dell'Agente per il PDF
1969+
agent_config = chatbot_agent.get_config_summary()
1970+
1971+
# 2. Genera l'istanza del PDF
1972+
pdf = PDFGenerator(
1973+
agent_config=agent_config,
1974+
profile_data=st.session_state.financial_profile.model_dump(),
1975+
portfolio_data=st.session_state.generated_portfolio,
1976+
)
1977+
1978+
# 3. Genera il file in memoria
1979+
pdf_output_bytes = pdf.generate()
1980+
1981+
# 4. Mostra il pulsante di download Streamlit
1982+
st.download_button(
1983+
label="⬇️ Esporta Report Portfolio (PDF)",
1984+
data=pdf_output_bytes,
1985+
file_name="Report_Finanziario.pdf",
1986+
mime="application/pdf",
1987+
key="download_pdf_button",
1988+
type="primary",
1989+
)
1990+
1991+
# --- FINE PULSANTE DI ESPORTAZIONE PDF ---
1992+
1993+
# TODO: vedere se eliminare
19461994
_scroll_to_bottom()
19471995

19481996
else:
19491997
logger.debug("No financial profile available to display")
19501998
else:
1951-
# Handle user input ONLY if there is a prompt (i.e., not disabled)
1999+
# Gestiamo l'input dell'utente SOLO se c'è un prompt (quindi non è disabilitato)
19522000
if prompt:
19532001
logger.debug("User input received: %s", prompt[:100])
19542002

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ filetype==1.2.0
5454
Flask==3.1.1
5555
flatbuffers==25.9.23
5656
fonttools==4.58.2
57+
fpdf2==2.8.5
5758
frozendict==2.4.6
5859
frozenlist==1.8.0
5960
fsspec==2025.5.1

src/models/portfolio.py

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -97,12 +97,58 @@ class Portfolio(BaseModel):
9797
},
9898
)
9999

100+
# TODO: questa è la funzione originale: vedere se eliminarla
101+
# @model_validator(mode="after")
102+
# def validate_total_percentage(self):
103+
# """Validate that asset percentages sum to approximately 100."""
104+
# total = sum(asset.percentage for asset in self.assets)
105+
# if not (99 <= total <= 101):
106+
# raise ValueError(
107+
# f"Asset percentages must sum to 100%, got {total}% instead"
108+
# )
109+
# return self
110+
111+
# TODO: è stata aggiunta questa nuova funzione nel caso in cui non si arrivi al 100%
112+
# ma va modificata perché così come è ora accetta anche i casi in cui non ho il 100%
113+
# invece bisogna forzare il fatto che la somma deve essere il 100%
100114
@model_validator(mode="after")
101-
def validate_total_percentage(self):
102-
"""Validate that asset percentages sum to approximately 100."""
115+
def normalize_total_percentage(self):
116+
"""
117+
Auto-correct asset percentages to ensure they sum to exactly 100%.
118+
Instead of raising an error, specifically re-proportions the values.
119+
"""
120+
if not self.assets:
121+
return self
122+
103123
total = sum(asset.percentage for asset in self.assets)
104-
if not (99 <= total <= 101):
105-
raise ValueError(
106-
f"Asset percentages must sum to 100%, got {total}% instead"
107-
)
124+
125+
# Se il totale è 0, non possiamo normalizzare (evitiamo divisione per zero)
126+
if total == 0:
127+
# In questo caso estremo, assegniamo tutto al primo asset o lanciamo errore
128+
# Qui scegliamo di distribuire equamente per non rompere l'app
129+
share = 100.0 / len(self.assets)
130+
for asset in self.assets:
131+
asset.percentage = round(share, 2)
132+
return self
133+
134+
# Se la somma è già corretta (con tolleranza), non facciamo nulla
135+
if 99.0 <= total <= 101.0:
136+
return self
137+
138+
# LOGICA DI NORMALIZZAZIONE
139+
# Esempio: Se il totale è 60%, il fattore è 100/60 = 1.666...
140+
factor = 100.0 / total
141+
142+
for asset in self.assets:
143+
# Scaliamo ogni asset
144+
asset.percentage = round(asset.percentage * factor, 2)
145+
146+
# Controllo finale per arrotondamenti (es. somma 99.99 o 100.01)
147+
new_total = sum(asset.percentage for asset in self.assets)
148+
diff = 100.0 - new_total
149+
150+
if diff != 0:
151+
# Aggiungiamo/togliamo la piccola differenza al primo asset (spesso il più grande)
152+
self.assets[0].percentage = round(self.assets[0].percentage + diff, 2)
153+
108154
return self

0 commit comments

Comments
 (0)