Spaces:
Running
Running
| """Module for rendering the PortIQ Portfolio AI chat dialog assistant.""" | |
| import streamlit as st | |
| import os | |
| import json | |
| import pandas as pd | |
| def on_chat_date_change(): | |
| # Update chat_context_date to the selector's value | |
| st.session_state["chat_context_date"] = st.session_state["chat_context_date_selector"] | |
| # Clear chat history when date context changes | |
| st.session_state["chat_history"] = [] | |
| def on_chat_close(): | |
| st.session_state["show_chat"] = False | |
| def _inject_dialog_styles(): | |
| """Injects CSS scoped exclusively to the PortIQ chat dialog via a unique marker class.""" | |
| st.markdown( | |
| """ | |
| <style> | |
| /* ββ Force the Streamlit dialog wrapper to be centered & large ββ */ | |
| div[data-testid="stDialog"] { | |
| display: flex !important; | |
| align-items: center !important; | |
| justify-content: center !important; | |
| } | |
| /* ββ PortIQ Chat dialog inner panel ββ */ | |
| div[data-testid="stDialog"] > div[role="dialog"] { | |
| position: relative !important; | |
| top: unset !important; | |
| right: unset !important; | |
| left: unset !important; | |
| transform: none !important; | |
| width: min(820px, 92vw) !important; | |
| max-height: 90vh !important; | |
| margin: auto !important; | |
| background: linear-gradient(140deg, #0D1526 0%, #081018 100%) !important; | |
| border: 1px solid rgba(6, 182, 212, 0.35) !important; | |
| border-radius: 18px !important; | |
| box-shadow: 0 28px 72px rgba(0, 0, 0, 0.85), 0 0 0 1px rgba(6, 182, 212, 0.08) !important; | |
| padding: 0 !important; | |
| overflow: hidden !important; | |
| } | |
| /* ββ Dialog title header ββ */ | |
| div[data-testid="stDialog"] h2 { | |
| font-size: 1.15rem !important; | |
| font-weight: 700 !important; | |
| color: #06B6D4 !important; | |
| letter-spacing: -0.02em !important; | |
| margin: 0 !important; | |
| } | |
| /* ββ Close (Γ) button ββ */ | |
| div[data-testid="stDialog"] button[aria-label="Close"] { | |
| color: #64748B !important; | |
| background: transparent !important; | |
| border: none !important; | |
| } | |
| div[data-testid="stDialog"] button[aria-label="Close"]:hover { | |
| color: #06B6D4 !important; | |
| } | |
| /* ββ Scoped: Clear Chat button β cyan ghost style ββ | |
| Targets only buttons inside the unique tcr-chat-dlg marker container */ | |
| .tcr-chat-dlg .stButton > button { | |
| border: 1px solid rgba(6, 182, 212, 0.45) !important; | |
| color: #06B6D4 !important; | |
| background: transparent !important; | |
| border-radius: 7px !important; | |
| font-size: 0.82rem !important; | |
| padding: 4px 12px !important; | |
| transition: all 0.2s ease !important; | |
| } | |
| .tcr-chat-dlg .stButton > button:hover { | |
| background: rgba(6, 182, 212, 0.1) !important; | |
| border-color: #06B6D4 !important; | |
| } | |
| /* ββ Scoped: Selectbox inside dialog ββ */ | |
| .tcr-chat-dlg [data-testid="stSelectbox"] label { | |
| font-size: 0.78rem !important; | |
| color: #94A3B8 !important; | |
| font-weight: 500 !important; | |
| text-transform: uppercase !important; | |
| letter-spacing: 0.05em !important; | |
| } | |
| /* ββ Scoped: Divider inside dialog ββ */ | |
| .tcr-chat-dlg hr { | |
| border-color: rgba(6, 182, 212, 0.15) !important; | |
| margin: 6px 0 !important; | |
| } | |
| /* ββ Scoped: Chat input bar ββ */ | |
| .tcr-chat-dlg [data-testid="stChatInput"] textarea { | |
| background: #0F1F35 !important; | |
| border: 1px solid rgba(6, 182, 212, 0.25) !important; | |
| border-radius: 10px !important; | |
| color: #E2E8F0 !important; | |
| font-size: 0.9rem !important; | |
| } | |
| .tcr-chat-dlg [data-testid="stChatInput"] textarea:focus { | |
| border-color: rgba(6, 182, 212, 0.6) !important; | |
| box-shadow: 0 0 0 2px rgba(6, 182, 212, 0.12) !important; | |
| } | |
| /* ββ Scoped: Chat message area scrollbox ββ */ | |
| .tcr-chat-dlg [data-testid="stVerticalBlockBorderWrapper"] { | |
| border: 1px solid rgba(6, 182, 212, 0.12) !important; | |
| border-radius: 10px !important; | |
| background: rgba(6, 12, 24, 0.55) !important; | |
| } | |
| /* ββ Scoped: assistant message bubble ββ */ | |
| .tcr-chat-dlg [data-testid="stChatMessage"] { | |
| background: transparent !important; | |
| } | |
| </style> | |
| """, | |
| unsafe_allow_html=True | |
| ) | |
| def render_chat_assistant(api_key): | |
| """Renders the interactive chat assistant modal.""" | |
| from modules.api_caller import run_chat_assistant | |
| # Inject dialog-scoped styles | |
| _inject_dialog_styles() | |
| # Unique wrapper div β all scoped CSS is anchored to .tcr-chat-dlg | |
| st.markdown('<div class="tcr-chat-dlg">', unsafe_allow_html=True) | |
| # Auto-scroll helper: fires on mount via a hidden <img onload> | |
| st.markdown( | |
| """ | |
| <img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" | |
| onload=" | |
| (function() { | |
| function scrollTcrChat() { | |
| var dlg = document.querySelector('div[data-testid=\\'stDialog\\']'); | |
| if (!dlg) return; | |
| var divs = dlg.querySelectorAll('div'); | |
| divs.forEach(function(d) { | |
| var cs = window.getComputedStyle(d); | |
| if ((cs.overflowY==='auto'||cs.overflowY==='scroll') && d.scrollHeight > d.clientHeight) { | |
| d.scrollTop = d.scrollHeight; | |
| } | |
| }); | |
| } | |
| [0, 80, 200, 450, 900].forEach(function(t){ setTimeout(scrollTcrChat, t); }); | |
| })() | |
| " style="display:none;"/> | |
| """, | |
| unsafe_allow_html=True | |
| ) | |
| # ββ 1. Context Date Selection ββββββββββββββββββββββββββββββββββββββββββββββ | |
| using_db = False | |
| history_dates = [] | |
| try: | |
| from modules.db import get_available_briefing_dates, get_briefing_from_db | |
| history_dates = get_available_briefing_dates() | |
| using_db = True | |
| except Exception as e: | |
| print(f"[chat] DB dates fetch failed, falling back to files: {e}") | |
| if not using_db: | |
| hist_dir = "history" | |
| if os.path.exists(hist_dir): | |
| history_files = sorted( | |
| [f for f in os.listdir(hist_dir) if f.endswith(".json") and f != "performance_cache.json"], reverse=True | |
| ) | |
| history_dates = [f.replace(".json", "") for f in history_files] | |
| available_dates = [] | |
| if "report_data" in st.session_state: | |
| available_dates.append("Today") | |
| available_dates.extend(history_dates) | |
| if not available_dates: | |
| st.info("No briefing data available. Please upload portfolio data first.") | |
| st.markdown('</div>', unsafe_allow_html=True) | |
| return | |
| current_selected = st.session_state.get("chat_context_date", "Today") | |
| if current_selected not in available_dates: | |
| current_selected = available_dates[0] | |
| st.session_state["chat_context_date"] = current_selected | |
| st.selectbox( | |
| "Briefing Date Context", | |
| available_dates, | |
| index=available_dates.index(current_selected), | |
| key="chat_context_date_selector", | |
| on_change=on_chat_date_change, | |
| label_visibility="visible" | |
| ) | |
| selected_date = st.session_state.get("chat_context_date", "Today") | |
| # ββ Load data for selected date ββββββββββββββββββββββββββββββββββββββββββββ | |
| active_report_data = None | |
| active_portfolio_df = None | |
| if selected_date == "Today": | |
| active_report_data = st.session_state.get("report_data") | |
| active_portfolio_df = st.session_state.get("df_original") | |
| else: | |
| if using_db: | |
| try: | |
| active_report_data = get_briefing_from_db(selected_date) | |
| if active_report_data: | |
| active_portfolio_df = pd.DataFrame(active_report_data.get("_portfolio_snapshot", [])) | |
| except Exception as e: | |
| st.error(f"Error loading {selected_date} from DB: {e}") | |
| else: | |
| try: | |
| with open(os.path.join("history", f"{selected_date}.json"), "r", encoding="utf-8") as fp: | |
| active_report_data = json.load(fp) | |
| active_portfolio_df = pd.DataFrame(active_report_data.get("_portfolio_snapshot", [])) | |
| except Exception as e: | |
| st.error(f"Error loading {selected_date} from file: {e}") | |
| # ββ 2. Actions row βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| col_clear, col_status = st.columns([4, 6]) | |
| with col_clear: | |
| if st.button("ποΈ Clear Chat", key="btn_clear_chat_history", use_container_width=True): | |
| st.session_state["chat_history"] = [] | |
| st.rerun() | |
| with col_status: | |
| if active_report_data: | |
| st.markdown( | |
| f"<p style='text-align:right;font-size:0.75rem;color:#10B981;margin-top:8px;'>" | |
| f"π’ Context Loaded ({selected_date})</p>", | |
| unsafe_allow_html=True | |
| ) | |
| else: | |
| st.markdown( | |
| "<p style='text-align:right;font-size:0.75rem;color:#EF4444;margin-top:8px;'>" | |
| "π΄ No Data Loaded</p>", | |
| unsafe_allow_html=True | |
| ) | |
| st.divider() | |
| # ββ 3. Scrollable Chat History βββββββββββββββββββββββββββββββββββββββββββββ | |
| if "chat_history" not in st.session_state: | |
| st.session_state["chat_history"] = [] | |
| chat_container = st.container(height=420) | |
| with chat_container: | |
| if not st.session_state["chat_history"]: | |
| st.chat_message("assistant").write( | |
| f"Hi Ram! π I'm PortIQ. Ask me anything about your briefing and stock decisions for **{selected_date}**." | |
| ) | |
| for msg in st.session_state["chat_history"]: | |
| st.chat_message(msg["role"]).write(msg["content"]) | |
| # ββ 4. Chat Input ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| user_query = st.chat_input("Ask PortIQ AI...", key="chat_input_query") | |
| if user_query: | |
| st.session_state["chat_history"].append({"role": "user", "content": user_query}) | |
| with chat_container: | |
| st.chat_message("user").write(user_query) | |
| with chat_container: | |
| with st.spinner("PortIQ is analyzing..."): | |
| response = run_chat_assistant( | |
| query=user_query, | |
| chat_history=st.session_state["chat_history"][:-1], | |
| active_date=selected_date, | |
| active_report_data=active_report_data, | |
| active_portfolio_df=active_portfolio_df, | |
| api_key=api_key | |
| ) | |
| st.chat_message("assistant").write(response) | |
| st.session_state["chat_history"].append({"role": "assistant", "content": response}) | |
| st.rerun() | |
| st.markdown('</div>', unsafe_allow_html=True) | |