ramkumar-bindrix
feat: migrate to FastAPI + React stack with PostgreSQL backend
049861f
Raw
History Blame Contribute Delete
12 kB
"""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
)
@st.dialog("πŸ’¬ PortIQ Portfolio AI", width="large", on_dismiss=on_chat_close)
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)