BrianIsaac commited on
Commit
f85e1e8
·
1 Parent(s): 5f56019

feat: implement P0 frontend UX improvements with tabbed interface

Browse files

Implemented comprehensive UX improvements across authentication, navigation,
and results presentation to enhance user experience and reduce UI clutter.

Navigation & Authentication:
- Add native Gradio sidebar navigation with authentication-based visibility
- Sidebar hidden on login page, shown after authentication or demo mode
- Remove hamburger menu in favour of sidebar built-in controls
- Add navigation buttons for new analysis, history, settings, and help

UI Consistency & Polish:
- Standardise button font sizes across all large buttons with CSS
- Add emoji to demo section heading for visual consistency
- Remove redundant "View History" button from landing page
- Remove "Portfolio Analysis Results" header and all tab headers

Results Page Redesign:
- Replace single-page results with 5-tab interface
- Tabs: Analysis Results, Dashboard, Tax Analysis, Stress Testing, History
- Auto-load history when History tab selected
- Remove redundant action buttons from results header

Historical Analysis:
- Add get_analysis_by_id() method to database.py with auth checks
- Implement row selection in history tables
- Display full analysis details in accordion on selection
- Support history viewing in both standalone page and embedded tab

Bug Fixes:
- Fix type conversion in visualization code (string to float)
- Resolve TypeError in optimization comparison charts
- Resolve TypeError in risk metrics dashboard

Files changed (3) hide show
  1. app.py +351 -159
  2. backend/database.py +35 -0
  3. backend/visualizations/plotly_charts.py +10 -0
app.py CHANGED
@@ -169,6 +169,7 @@ else:
169
 
170
  # Global state for visualisations
171
  LAST_ANALYSIS_STATE = None
 
172
  LAST_STRESS_TEST = None
173
 
174
  # Loading screen rotating messages with MCP phases and disclaimers
@@ -531,9 +532,7 @@ def format_analysis_result(final_state: AgentState, holdings: List[Dict[str, Any
531
  recs_md += "*No specific recommendations generated.*\n"
532
 
533
  # Final output
534
- output = f"""# Portfolio Analysis Results
535
-
536
- {holdings_md}
537
 
538
  {metrics_md}
539
 
@@ -1165,6 +1164,47 @@ def create_interface() -> gr.Blocks:
1165
  padding: 1.5rem 2rem;
1166
  }
1167
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1168
  /* Dark mode specific container adjustments - theme adaptive */
1169
  .dark .gradio-container {
1170
  background-color: var(--body-background-fill-dark);
@@ -1571,6 +1611,25 @@ def create_interface() -> gr.Blocks:
1571
  with gr.Column(scale=1):
1572
  logout_btn = gr.Button("Sign Out", variant="secondary", size="sm", visible=False)
1573
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1574
  # Authentication container
1575
  with gr.Group(visible=True, elem_id="auth-container") as login_container:
1576
  gr.Markdown("## 🔐 Sign In to Continue")
@@ -1618,12 +1677,13 @@ def create_interface() -> gr.Blocks:
1618
 
1619
  # Demo mode option
1620
  gr.Markdown("---")
1621
- gr.Markdown("### Or try without signing up")
1622
  demo_btn = gr.Button(
1623
- "🎯 Try Demo (1 free analysis per day)",
1624
  variant="secondary",
1625
  size="lg"
1626
  )
 
1627
  demo_message = gr.Markdown("")
1628
 
1629
  # Input Page with side-by-side layout (hidden until authenticated)
@@ -1661,7 +1721,12 @@ def create_interface() -> gr.Blocks:
1661
  info="Enable brutal honesty mode for portfolio critique (only works with Standard Analysis)"
1662
  )
1663
 
1664
- analyse_btn = gr.Button("Analyse Portfolio", variant="primary", size="sm", scale=1)
 
 
 
 
 
1665
 
1666
  # Examples integrated as simple buttons
1667
  gr.Markdown("### Example Portfolios", elem_classes="examples-header")
@@ -1776,147 +1841,158 @@ def create_interface() -> gr.Blocks:
1776
  """
1777
  )
1778
 
1779
- # Results Page (unified dashboard)
1780
  with gr.Group(visible=False) as results_page:
1781
- with gr.Row():
1782
- with gr.Column(scale=3):
1783
- gr.Markdown("### Analysis Results")
1784
- analysis_output = gr.Markdown("")
1785
-
1786
- # Performance Metrics Accordion (progressive disclosure)
1787
- with gr.Accordion("Performance Metrics & Reasoning", open=False):
1788
- performance_metrics_output = gr.Markdown("")
1789
-
1790
- with gr.Column(scale=1):
1791
- new_analysis_btn = gr.Button("New Analysis", variant="secondary")
1792
- view_history_btn_results = gr.Button("View History", variant="secondary")
1793
-
1794
- gr.Markdown("---")
1795
- gr.Markdown("### Interactive Dashboard")
1796
-
1797
- # Top row - Portfolio allocation + Risk metrics
1798
- with gr.Row(equal_height=True):
1799
- with gr.Column(scale=1, min_width=400):
1800
- allocation_plot = gr.Plot(label="Portfolio Allocation", container=True)
1801
- with gr.Column(scale=1, min_width=400):
1802
- risk_plot = gr.Plot(label="Risk Metrics Dashboard", container=True)
1803
-
1804
- # Middle row - Performance (full width)
1805
- with gr.Row():
1806
- performance_plot = gr.Plot(label="Historical Performance", container=True)
1807
-
1808
- # Bottom row - Correlation + Optimisation
1809
- with gr.Row(equal_height=True):
1810
- with gr.Column(scale=1, min_width=400):
1811
- correlation_plot = gr.Plot(label="Asset Correlation Matrix", container=True)
1812
- with gr.Column(scale=1, min_width=400):
1813
- optimization_plot = gr.Plot(label="Optimisation Methods Comparison", container=True)
1814
-
1815
- # Tax Impact Analysis Section (Enhancement #5)
1816
- gr.Markdown("---")
1817
- gr.Markdown("### Tax Impact Analysis")
1818
- gr.Markdown("Analyse tax implications and identify tax-loss harvesting opportunities.")
1819
-
1820
- with gr.Row():
1821
- with gr.Column(scale=1):
1822
- tax_filing_status = gr.Dropdown(
1823
- choices=[
1824
- ("Single", "single"),
1825
- ("Married Filing Jointly", "married_joint"),
1826
- ("Married Filing Separately", "married_separate"),
1827
- ("Head of Household", "head_of_household"),
1828
- ],
1829
- value="single",
1830
- label="Filing Status",
1831
- info="Your tax filing status"
1832
- )
1833
- tax_annual_income = gr.Slider(
1834
- minimum=0,
1835
- maximum=1000000,
1836
- value=75000,
1837
- step=5000,
1838
- label="Annual Income ($)",
1839
- info="Total taxable income"
1840
- )
1841
- tax_cost_basis_method = gr.Dropdown(
1842
- choices=[
1843
- ("First In, First Out (FIFO)", "fifo"),
1844
- ("Last In, First Out (LIFO)", "lifo"),
1845
- ("Highest In, First Out (HIFO)", "hifo"),
1846
- ("Average Cost", "average"),
1847
- ],
1848
- value="fifo",
1849
- label="Cost Basis Method",
1850
- info="Method for calculating gains/losses"
1851
- )
1852
- tax_calculate_btn = gr.Button("Calculate Tax Impact", variant="primary")
1853
-
1854
- with gr.Column(scale=2):
1855
- tax_analysis_output = gr.Markdown("")
1856
-
1857
- # Stress Testing Section
1858
- gr.Markdown("---")
1859
- gr.Markdown("### Portfolio Stress Testing")
1860
- gr.Markdown("Test your portfolio's resilience against historical crises and market scenarios.")
1861
-
1862
- with gr.Row():
1863
- with gr.Column(scale=1):
1864
- scenario_choices = [
1865
- ("Monte Carlo Simulation (10,000 paths)", "monte_carlo"),
1866
- ("All Scenarios Comparison", "all_scenarios"),
1867
- ("2008 Financial Crisis", "2008_financial_crisis"),
1868
- ("COVID-19 Pandemic (2020)", "covid_2020"),
1869
- ("Dot-com Bubble (2000-2002)", "dotcom_bubble"),
1870
- ("European Debt Crisis (2011)", "european_debt_2011"),
1871
- ("China Devaluation (2015)", "china_2015"),
1872
- ("Inflation Shock (2022)", "inflation_2022"),
1873
- ("Severe Recession (Hypothetical)", "severe_recession"),
1874
- ("Stagflation (Hypothetical)", "stagflation"),
1875
- ("Flash Crash (Hypothetical)", "flash_crash"),
1876
- ]
1877
- stress_scenario_dropdown = gr.Dropdown(
1878
- choices=scenario_choices,
1879
- value="monte_carlo",
1880
- label="Select Stress Scenario",
1881
- info="Choose a historical crisis or simulation method"
1882
- )
1883
-
1884
  with gr.Row():
1885
- stress_n_sims = gr.Slider(
1886
- minimum=1000,
1887
- maximum=50000,
1888
- value=10000,
1889
- step=1000,
1890
- label="Monte Carlo Simulations",
1891
- info="More simulations = more accuracy (slower)"
1892
- )
1893
- stress_horizon = gr.Slider(
1894
- minimum=30,
1895
- maximum=756,
1896
- value=252,
1897
- step=30,
1898
- label="Time Horizon (days)",
1899
- info="1 year = 252 trading days"
1900
- )
1901
 
1902
- stress_test_btn = gr.Button("Run Stress Test", variant="primary", size="lg")
 
 
 
 
 
1903
 
1904
- # Stress test results
1905
- with gr.Column():
1906
- stress_summary = gr.Markdown("", visible=True)
1907
 
1908
- with gr.Tabs():
1909
- with gr.Tab("Dashboard"):
1910
- stress_dashboard_plot = gr.Plot(label="Stress Test Dashboard")
1911
-
1912
- with gr.Tab("Monte Carlo Paths"):
1913
- stress_mc_plot = gr.Plot(label="Monte Carlo Simulation Paths")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1914
 
1915
- with gr.Tab("Scenario Comparison"):
1916
- stress_scenario_plot = gr.Plot(label="All Scenarios Comparison")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1917
 
1918
- with gr.Tab("Drawdown Analysis"):
1919
- stress_drawdown_plot = gr.Plot(label="Maximum Drawdown Distribution")
1920
 
1921
  # History Page (Enhancement #4 - Historical Analysis Storage)
1922
  with gr.Group(visible=False) as history_page:
@@ -1930,11 +2006,12 @@ def create_interface() -> gr.Blocks:
1930
  back_to_input_btn = gr.Button("New Analysis", variant="secondary")
1931
 
1932
  history_table = gr.Dataframe(
1933
- headers=["Date", "Holdings", "Risk Tolerance", "AI Synthesis Preview"],
1934
- datatype=["str", "str", "str", "str"],
1935
  interactive=False,
1936
  wrap=True,
1937
- elem_id="history-table"
 
1938
  )
1939
 
1940
  with gr.Accordion("Selected Analysis Details", open=False) as history_details:
@@ -1982,8 +2059,12 @@ def create_interface() -> gr.Blocks:
1982
  logger.info(f"Loaded {len(history)} analyses for user {user_id}")
1983
 
1984
  # Format history for dataframe
 
 
 
 
1985
  rows = []
1986
- for record in history:
1987
  # Format holdings as comma-separated tickers
1988
  holdings_str = ", ".join([h.get("ticker", "?") for h in record.get("holdings_snapshot", [])])
1989
 
@@ -1991,6 +2072,7 @@ def create_interface() -> gr.Blocks:
1991
  synthesis_preview = record.get("ai_synthesis", "")[:100] + "..."
1992
 
1993
  rows.append([
 
1994
  record.get("created_at", "").split("T")[0], # Date only
1995
  holdings_str,
1996
  record.get("portfolios", {}).get("risk_tolerance", "moderate"),
@@ -2006,6 +2088,79 @@ def create_interface() -> gr.Blocks:
2006
  """Synchronous wrapper for load_history."""
2007
  return asyncio.run(load_history(session_state))
2008
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2009
  def show_history_page():
2010
  """Navigate to history page."""
2011
  return {
@@ -2166,19 +2321,17 @@ Please try again with different parameters.
2166
  show_progress="full"
2167
  )
2168
 
2169
- new_analysis_btn.click(
2170
- show_input_page,
2171
- outputs=[input_page, results_page, history_page]
2172
- )
 
 
2173
 
2174
- # History page navigation
2175
- view_history_btn_results.click(
2176
- show_history_page,
2177
- outputs=[input_page, results_page, history_page]
2178
- ).then(
2179
- sync_load_history,
2180
  inputs=[session_state],
2181
- outputs=[history_table, history_details_output]
2182
  )
2183
 
2184
  back_to_input_btn.click(
@@ -2186,6 +2339,17 @@ Please try again with different parameters.
2186
  outputs=[input_page, results_page, history_page]
2187
  )
2188
 
 
 
 
 
 
 
 
 
 
 
 
2189
  # Tax calculator button (Enhancement #5)
2190
  tax_calculate_btn.click(
2191
  calculate_tax_impact,
@@ -2282,8 +2446,8 @@ Please try again with different parameters.
2282
  inputs=[login_email, login_password, session_state],
2283
  outputs=[session_state, login_message, login_container, input_page, user_info]
2284
  ).then(
2285
- lambda: gr.update(visible=True),
2286
- outputs=[logout_btn]
2287
  )
2288
 
2289
  signup_btn.click(
@@ -2314,6 +2478,9 @@ Please try again with different parameters.
2314
  handle_demo_mode,
2315
  inputs=[session_state],
2316
  outputs=[session_state, demo_message, login_container, input_page, user_info, logout_btn]
 
 
 
2317
  )
2318
 
2319
  logout_btn.click(
@@ -2321,8 +2488,33 @@ Please try again with different parameters.
2321
  inputs=[session_state],
2322
  outputs=[session_state, login_message, login_container, input_page, user_info]
2323
  ).then(
2324
- lambda: gr.update(visible=False),
2325
- outputs=[logout_btn]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2326
  )
2327
 
2328
  return demo
 
169
 
170
  # Global state for visualisations
171
  LAST_ANALYSIS_STATE = None
172
+ HISTORY_RECORDS = [] # Stores loaded history records for row selection
173
  LAST_STRESS_TEST = None
174
 
175
  # Loading screen rotating messages with MCP phases and disclaimers
 
532
  recs_md += "*No specific recommendations generated.*\n"
533
 
534
  # Final output
535
+ output = f"""{holdings_md}
 
 
536
 
537
  {metrics_md}
538
 
 
1164
  padding: 1.5rem 2rem;
1165
  }
1166
 
1167
+ /* Sidebar Styling - Works with gr.Sidebar */
1168
+ #main-sidebar {
1169
+ background: linear-gradient(135deg, #1e3a5f 0%, #2d5a7b 100%) !important;
1170
+ }
1171
+
1172
+ #main-sidebar .sidebar-header {
1173
+ color: white !important;
1174
+ border-bottom: 2px solid rgba(255,255,255,0.2);
1175
+ padding-bottom: 10px;
1176
+ margin-bottom: 20px;
1177
+ }
1178
+
1179
+ .nav-btn {
1180
+ width: 100%;
1181
+ margin-bottom: 10px;
1182
+ text-align: left !important;
1183
+ justify-content: flex-start !important;
1184
+ background: rgba(255,255,255,0.05) !important;
1185
+ border: 1px solid rgba(255,255,255,0.1) !important;
1186
+ color: white !important;
1187
+ }
1188
+
1189
+ .nav-btn:hover {
1190
+ background: rgba(255,255,255,0.15) !important;
1191
+ border-color: rgba(255,255,255,0.3) !important;
1192
+ }
1193
+
1194
+ .sidebar-header {
1195
+ color: white !important;
1196
+ border-bottom: 2px solid rgba(255,255,255,0.2);
1197
+ padding-bottom: 10px;
1198
+ margin-bottom: 20px;
1199
+ }
1200
+
1201
+ /* Ensure all large buttons have consistent font size */
1202
+ button.lg,
1203
+ .lg button {
1204
+ font-size: 1rem !important;
1205
+ font-weight: 600 !important;
1206
+ }
1207
+
1208
  /* Dark mode specific container adjustments - theme adaptive */
1209
  .dark .gradio-container {
1210
  background-color: var(--body-background-fill-dark);
 
1611
  with gr.Column(scale=1):
1612
  logout_btn = gr.Button("Sign Out", variant="secondary", size="sm", visible=False)
1613
 
1614
+ # Navigation sidebar using Gradio's native component
1615
+ with gr.Sidebar(position="left", open=False, visible=False, elem_id="main-sidebar") as sidebar:
1616
+ gr.Markdown("## Navigation", elem_classes="sidebar-header")
1617
+
1618
+ # Navigation options
1619
+ nav_new_analysis = gr.Button("🏠 New Analysis", variant="secondary", size="lg", elem_classes="nav-btn")
1620
+ nav_view_history = gr.Button("📜 View History", variant="secondary", size="lg", elem_classes="nav-btn")
1621
+ nav_settings = gr.Button("⚙️ Settings", variant="secondary", size="lg", elem_classes="nav-btn")
1622
+ nav_help = gr.Button("❓ Help & Documentation", variant="secondary", size="lg", elem_classes="nav-btn")
1623
+
1624
+ gr.Markdown("---")
1625
+
1626
+ # User section (when logged in)
1627
+ with gr.Group(visible=False) as sidebar_user_section:
1628
+ gr.Markdown("### Account")
1629
+ sidebar_user_email = gr.Markdown("")
1630
+ nav_profile_btn = gr.Button("👤 Profile", variant="secondary", size="sm")
1631
+ nav_signout_btn = gr.Button("🚪 Sign Out", variant="secondary", size="sm")
1632
+
1633
  # Authentication container
1634
  with gr.Group(visible=True, elem_id="auth-container") as login_container:
1635
  gr.Markdown("## 🔐 Sign In to Continue")
 
1677
 
1678
  # Demo mode option
1679
  gr.Markdown("---")
1680
+ gr.Markdown("## 🎯 Or Try Without Signing Up")
1681
  demo_btn = gr.Button(
1682
+ "Try Demo",
1683
  variant="secondary",
1684
  size="lg"
1685
  )
1686
+ gr.Markdown("*1 free analysis per day • No account required*", elem_classes="demo-subtitle")
1687
  demo_message = gr.Markdown("")
1688
 
1689
  # Input Page with side-by-side layout (hidden until authenticated)
 
1721
  info="Enable brutal honesty mode for portfolio critique (only works with Standard Analysis)"
1722
  )
1723
 
1724
+ # Action button
1725
+ analyse_btn = gr.Button(
1726
+ "Analyse Portfolio",
1727
+ variant="primary",
1728
+ size="lg"
1729
+ )
1730
 
1731
  # Examples integrated as simple buttons
1732
  gr.Markdown("### Example Portfolios", elem_classes="examples-header")
 
1841
  """
1842
  )
1843
 
1844
+ # Results Page (tabbed interface)
1845
  with gr.Group(visible=False) as results_page:
1846
+ # Main tabbed interface
1847
+ with gr.Tabs() as results_tabs:
1848
+ # Tab 1: Analysis Results
1849
+ with gr.Tab("📊 Analysis Results"):
1850
+ with gr.Column():
1851
+ analysis_output = gr.Markdown("")
1852
+
1853
+ # Performance Metrics Accordion (progressive disclosure)
1854
+ with gr.Accordion("Performance Metrics & Reasoning", open=False):
1855
+ performance_metrics_output = gr.Markdown("")
1856
+
1857
+ # Tab 2: Dashboard
1858
+ with gr.Tab("📈 Dashboard"):
1859
+ # Top row - Portfolio allocation + Risk metrics
1860
+ with gr.Row(equal_height=True):
1861
+ with gr.Column(scale=1, min_width=400):
1862
+ allocation_plot = gr.Plot(label="Portfolio Allocation", container=True)
1863
+ with gr.Column(scale=1, min_width=400):
1864
+ risk_plot = gr.Plot(label="Risk Metrics Dashboard", container=True)
1865
+
1866
+ # Middle row - Performance (full width)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1867
  with gr.Row():
1868
+ performance_plot = gr.Plot(label="Historical Performance", container=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1869
 
1870
+ # Bottom row - Correlation + Optimisation
1871
+ with gr.Row(equal_height=True):
1872
+ with gr.Column(scale=1, min_width=400):
1873
+ correlation_plot = gr.Plot(label="Asset Correlation Matrix", container=True)
1874
+ with gr.Column(scale=1, min_width=400):
1875
+ optimization_plot = gr.Plot(label="Optimisation Methods Comparison", container=True)
1876
 
1877
+ # Tab 3: Tax Analysis
1878
+ with gr.Tab("💰 Tax Analysis"):
1879
+ gr.Markdown("Analyse tax implications and identify tax-loss harvesting opportunities.")
1880
 
1881
+ with gr.Row():
1882
+ with gr.Column(scale=1):
1883
+ tax_filing_status = gr.Dropdown(
1884
+ choices=[
1885
+ ("Single", "single"),
1886
+ ("Married Filing Jointly", "married_joint"),
1887
+ ("Married Filing Separately", "married_separate"),
1888
+ ("Head of Household", "head_of_household"),
1889
+ ],
1890
+ value="single",
1891
+ label="Filing Status",
1892
+ info="Your tax filing status"
1893
+ )
1894
+ tax_annual_income = gr.Slider(
1895
+ minimum=0,
1896
+ maximum=1000000,
1897
+ value=75000,
1898
+ step=5000,
1899
+ label="Annual Income ($)",
1900
+ info="Total taxable income"
1901
+ )
1902
+ tax_cost_basis_method = gr.Dropdown(
1903
+ choices=[
1904
+ ("First In, First Out (FIFO)", "fifo"),
1905
+ ("Last In, First Out (LIFO)", "lifo"),
1906
+ ("Highest In, First Out (HIFO)", "hifo"),
1907
+ ("Average Cost", "average"),
1908
+ ],
1909
+ value="fifo",
1910
+ label="Cost Basis Method",
1911
+ info="Method for calculating gains/losses"
1912
+ )
1913
+ tax_calculate_btn = gr.Button("Calculate Tax Impact", variant="primary")
1914
+
1915
+ with gr.Column(scale=2):
1916
+ tax_analysis_output = gr.Markdown("")
1917
+
1918
+ # Tab 4: Stress Testing
1919
+ with gr.Tab("🎲 Stress Testing"):
1920
+ gr.Markdown("Test your portfolio's resilience against historical crises and market scenarios.")
1921
 
1922
+ with gr.Row():
1923
+ with gr.Column(scale=1):
1924
+ scenario_choices = [
1925
+ ("Monte Carlo Simulation (10,000 paths)", "monte_carlo"),
1926
+ ("All Scenarios Comparison", "all_scenarios"),
1927
+ ("2008 Financial Crisis", "2008_financial_crisis"),
1928
+ ("COVID-19 Pandemic (2020)", "covid_2020"),
1929
+ ("Dot-com Bubble (2000-2002)", "dotcom_bubble"),
1930
+ ("European Debt Crisis (2011)", "european_debt_2011"),
1931
+ ("China Devaluation (2015)", "china_2015"),
1932
+ ("Inflation Shock (2022)", "inflation_2022"),
1933
+ ("Severe Recession (Hypothetical)", "severe_recession"),
1934
+ ("Stagflation (Hypothetical)", "stagflation"),
1935
+ ("Flash Crash (Hypothetical)", "flash_crash"),
1936
+ ]
1937
+ stress_scenario_dropdown = gr.Dropdown(
1938
+ choices=scenario_choices,
1939
+ value="monte_carlo",
1940
+ label="Select Stress Scenario",
1941
+ info="Choose a historical crisis or simulation method"
1942
+ )
1943
+
1944
+ with gr.Row():
1945
+ stress_n_sims = gr.Slider(
1946
+ minimum=1000,
1947
+ maximum=50000,
1948
+ value=10000,
1949
+ step=1000,
1950
+ label="Monte Carlo Simulations",
1951
+ info="More simulations = more accuracy (slower)"
1952
+ )
1953
+ stress_horizon = gr.Slider(
1954
+ minimum=30,
1955
+ maximum=756,
1956
+ value=252,
1957
+ step=30,
1958
+ label="Time Horizon (days)",
1959
+ info="1 year = 252 trading days"
1960
+ )
1961
+
1962
+ stress_test_btn = gr.Button("Run Stress Test", variant="primary", size="lg")
1963
+
1964
+ # Stress test results
1965
+ with gr.Column():
1966
+ stress_summary = gr.Markdown("", visible=True)
1967
+
1968
+ with gr.Tabs():
1969
+ with gr.Tab("Dashboard"):
1970
+ stress_dashboard_plot = gr.Plot(label="Stress Test Dashboard")
1971
+
1972
+ with gr.Tab("Monte Carlo Paths"):
1973
+ stress_mc_plot = gr.Plot(label="Monte Carlo Simulation Paths")
1974
+
1975
+ with gr.Tab("Scenario Comparison"):
1976
+ stress_scenario_plot = gr.Plot(label="All Scenarios Comparison")
1977
+
1978
+ with gr.Tab("Drawdown Analysis"):
1979
+ stress_drawdown_plot = gr.Plot(label="Maximum Drawdown Distribution")
1980
+
1981
+ # Tab 5: History (embedded)
1982
+ with gr.Tab("📜 History"):
1983
+ gr.Markdown("View your previous portfolio analyses")
1984
+
1985
+ history_table_results = gr.Dataframe(
1986
+ headers=["ID", "Date", "Holdings", "Risk Tolerance", "AI Synthesis Preview"],
1987
+ datatype=["str", "str", "str", "str", "str"],
1988
+ interactive=False,
1989
+ wrap=True,
1990
+ elem_id="history-table-results",
1991
+ column_widths=["5%", "15%", "25%", "15%", "40%"]
1992
+ )
1993
 
1994
+ with gr.Accordion("Selected Analysis Details", open=False):
1995
+ history_details_output_results = gr.Markdown("")
1996
 
1997
  # History Page (Enhancement #4 - Historical Analysis Storage)
1998
  with gr.Group(visible=False) as history_page:
 
2006
  back_to_input_btn = gr.Button("New Analysis", variant="secondary")
2007
 
2008
  history_table = gr.Dataframe(
2009
+ headers=["ID", "Date", "Holdings", "Risk Tolerance", "AI Synthesis Preview"],
2010
+ datatype=["str", "str", "str", "str", "str"],
2011
  interactive=False,
2012
  wrap=True,
2013
+ elem_id="history-table",
2014
+ column_widths=["5%", "15%", "25%", "15%", "40%"]
2015
  )
2016
 
2017
  with gr.Accordion("Selected Analysis Details", open=False) as history_details:
 
2059
  logger.info(f"Loaded {len(history)} analyses for user {user_id}")
2060
 
2061
  # Format history for dataframe
2062
+ # Store analysis records globally for row selection
2063
+ global HISTORY_RECORDS
2064
+ HISTORY_RECORDS = history
2065
+
2066
  rows = []
2067
+ for i, record in enumerate(history):
2068
  # Format holdings as comma-separated tickers
2069
  holdings_str = ", ".join([h.get("ticker", "?") for h in record.get("holdings_snapshot", [])])
2070
 
 
2072
  synthesis_preview = record.get("ai_synthesis", "")[:100] + "..."
2073
 
2074
  rows.append([
2075
+ str(i), # Row index for selection
2076
  record.get("created_at", "").split("T")[0], # Date only
2077
  holdings_str,
2078
  record.get("portfolios", {}).get("risk_tolerance", "moderate"),
 
2088
  """Synchronous wrapper for load_history."""
2089
  return asyncio.run(load_history(session_state))
2090
 
2091
+ def view_historical_analysis(evt: gr.SelectData):
2092
+ """View full details of a selected historical analysis.
2093
+
2094
+ Args:
2095
+ evt: Gradio SelectData event containing row index
2096
+
2097
+ Returns:
2098
+ Markdown string with full analysis details
2099
+ """
2100
+ global HISTORY_RECORDS
2101
+
2102
+ try:
2103
+ # Get the row index from the selected row
2104
+ row_index = int(evt.value) # First column contains the index
2105
+
2106
+ if not HISTORY_RECORDS or row_index >= len(HISTORY_RECORDS):
2107
+ return "**Error**: Could not load analysis. Please refresh the history table."
2108
+
2109
+ record = HISTORY_RECORDS[row_index]
2110
+
2111
+ # Format detailed analysis view
2112
+ details = f"""# Analysis Details
2113
+
2114
+ **Date**: {record.get('created_at', 'Unknown').split('T')[0]}
2115
+ **Portfolio**: {record.get('portfolios', {}).get('name', 'Unnamed Portfolio')}
2116
+ **Risk Tolerance**: {record.get('portfolios', {}).get('risk_tolerance', 'moderate')}
2117
+
2118
+ ---
2119
+
2120
+ ## Holdings Snapshot
2121
+ """
2122
+ # Format holdings as table
2123
+ holdings = record.get('holdings_snapshot', [])
2124
+ if holdings:
2125
+ details += "\n| Ticker | Shares | Value |\n|--------|--------|-------|\n"
2126
+ for holding in holdings:
2127
+ details += f"| {holding.get('ticker', '?')} | {holding.get('shares', 0)} | ${holding.get('value', 0):,.2f} |\n"
2128
+ else:
2129
+ details += "\n*No holdings data available*\n"
2130
+
2131
+ details += f"""
2132
+
2133
+ ---
2134
+
2135
+ ## AI Analysis
2136
+
2137
+ {record.get('ai_synthesis', '*No analysis available*')}
2138
+
2139
+ ---
2140
+
2141
+ ## Recommendations
2142
+ """
2143
+ recommendations = record.get('recommendations', [])
2144
+ if recommendations:
2145
+ for i, rec in enumerate(recommendations, 1):
2146
+ details += f"\n{i}. {rec}"
2147
+ else:
2148
+ details += "\n*No recommendations available*"
2149
+
2150
+ details += f"""
2151
+
2152
+ ---
2153
+
2154
+ **Model**: {record.get('model_version', 'Unknown')}
2155
+ **Execution Time**: {record.get('execution_time_ms', 0):,.0f}ms
2156
+ """
2157
+
2158
+ return details
2159
+
2160
+ except Exception as e:
2161
+ logger.error(f"Failed to view historical analysis: {e}")
2162
+ return f"**Error**: Failed to load analysis details: {str(e)}"
2163
+
2164
  def show_history_page():
2165
  """Navigate to history page."""
2166
  return {
 
2321
  show_progress="full"
2322
  )
2323
 
2324
+ # Auto-load history when History tab is selected
2325
+ def load_history_if_selected(evt: gr.SelectData, session):
2326
+ """Load history only when History tab is selected."""
2327
+ if evt.index == 4: # History tab is index 4 (0-indexed: Analysis, Dashboard, Tax, Stress, History)
2328
+ return sync_load_history(session)
2329
+ return gr.skip(), gr.skip()
2330
 
2331
+ results_tabs.select(
2332
+ load_history_if_selected,
 
 
 
 
2333
  inputs=[session_state],
2334
+ outputs=[history_table_results, history_details_output_results]
2335
  )
2336
 
2337
  back_to_input_btn.click(
 
2339
  outputs=[input_page, results_page, history_page]
2340
  )
2341
 
2342
+ # History table selection handlers
2343
+ history_table.select(
2344
+ view_historical_analysis,
2345
+ outputs=[history_details_output]
2346
+ )
2347
+
2348
+ history_table_results.select(
2349
+ view_historical_analysis,
2350
+ outputs=[history_details_output_results]
2351
+ )
2352
+
2353
  # Tax calculator button (Enhancement #5)
2354
  tax_calculate_btn.click(
2355
  calculate_tax_impact,
 
2446
  inputs=[login_email, login_password, session_state],
2447
  outputs=[session_state, login_message, login_container, input_page, user_info]
2448
  ).then(
2449
+ lambda: (gr.update(visible=True), gr.update(visible=True)),
2450
+ outputs=[logout_btn, sidebar]
2451
  )
2452
 
2453
  signup_btn.click(
 
2478
  handle_demo_mode,
2479
  inputs=[session_state],
2480
  outputs=[session_state, demo_message, login_container, input_page, user_info, logout_btn]
2481
+ ).then(
2482
+ lambda: gr.update(visible=True),
2483
+ outputs=[sidebar]
2484
  )
2485
 
2486
  logout_btn.click(
 
2488
  inputs=[session_state],
2489
  outputs=[session_state, login_message, login_container, input_page, user_info]
2490
  ).then(
2491
+ lambda: (gr.update(visible=False), gr.update(visible=False)),
2492
+ outputs=[logout_btn, sidebar]
2493
+ )
2494
+
2495
+ # Navigation event handlers
2496
+ nav_new_analysis.click(
2497
+ show_input_page,
2498
+ outputs=[input_page, results_page, history_page]
2499
+ )
2500
+
2501
+ nav_view_history.click(
2502
+ show_history_page,
2503
+ outputs=[input_page, results_page, history_page]
2504
+ ).then(
2505
+ sync_load_history,
2506
+ inputs=[session_state],
2507
+ outputs=[history_table, history_details_output]
2508
+ )
2509
+
2510
+ # Sidebar sign out button (mirrors main logout button)
2511
+ nav_signout_btn.click(
2512
+ sync_logout,
2513
+ inputs=[session_state],
2514
+ outputs=[session_state, login_message, login_container, input_page, user_info]
2515
+ ).then(
2516
+ lambda: (gr.update(visible=False), gr.update(visible=False)),
2517
+ outputs=[logout_btn, sidebar]
2518
  )
2519
 
2520
  return demo
backend/database.py CHANGED
@@ -279,6 +279,41 @@ class Database:
279
  logger.error(f"Failed to get analysis history: {e}")
280
  return []
281
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
282
 
283
  # Global database instance
284
  db = Database()
 
279
  logger.error(f"Failed to get analysis history: {e}")
280
  return []
281
 
282
+ async def get_analysis_by_id(
283
+ self,
284
+ analysis_id: str,
285
+ user_id: str
286
+ ) -> Optional[Dict[str, Any]]:
287
+ """Get a specific analysis by ID with user authorisation check.
288
+
289
+ Args:
290
+ analysis_id: Analysis ID to retrieve
291
+ user_id: User ID for authorisation check
292
+
293
+ Returns:
294
+ Analysis data if found and authorised, None otherwise
295
+ """
296
+ if not self.is_connected():
297
+ return None
298
+
299
+ try:
300
+ # Join with portfolios to ensure user owns this analysis
301
+ result = self.client.table('portfolio_analyses') \
302
+ .select('*, portfolios!inner(user_id, risk_tolerance, name)') \
303
+ .eq('id', analysis_id) \
304
+ .eq('portfolios.user_id', user_id) \
305
+ .execute()
306
+
307
+ if result.data:
308
+ return result.data[0]
309
+ else:
310
+ logger.warning(f"Analysis {analysis_id} not found or not authorised for user {user_id}")
311
+ return None
312
+
313
+ except Exception as e:
314
+ logger.error(f"Failed to get analysis by ID: {e}")
315
+ return None
316
+
317
 
318
  # Global database instance
319
  db = Database()
backend/visualizations/plotly_charts.py CHANGED
@@ -115,6 +115,12 @@ def create_risk_metrics_dashboard(
115
  Returns:
116
  Plotly figure with 4 gauge charts
117
  """
 
 
 
 
 
 
118
  fig = make_subplots(
119
  rows=2, cols=2,
120
  specs=[
@@ -434,6 +440,10 @@ def create_optimization_comparison(
434
  sharpe = result.get('sharpe_ratio', 0)
435
  vol = result.get('volatility', 0)
436
 
 
 
 
 
437
  methods.append(method.replace('_', ' ').title())
438
  sharpe_ratios.append(sharpe)
439
  volatilities.append(vol * 100 if vol < 1 else vol) # Convert to percentage
 
115
  Returns:
116
  Plotly figure with 4 gauge charts
117
  """
118
+ # Convert to float in case values are strings from serialization
119
+ sharpe = float(sharpe) if sharpe else 0
120
+ var = float(var) if var else 0
121
+ cvar = float(cvar) if cvar else 0
122
+ volatility = float(volatility) if volatility else 0
123
+
124
  fig = make_subplots(
125
  rows=2, cols=2,
126
  specs=[
 
440
  sharpe = result.get('sharpe_ratio', 0)
441
  vol = result.get('volatility', 0)
442
 
443
+ # Convert to float in case values are strings
444
+ sharpe = float(sharpe) if sharpe else 0
445
+ vol = float(vol) if vol else 0
446
+
447
  methods.append(method.replace('_', ' ').title())
448
  sharpe_ratios.append(sharpe)
449
  volatilities.append(vol * 100 if vol < 1 else vol) # Convert to percentage