Spaces:
Sleeping
Sleeping
| """ | |
| Strategic Sandbox v1.0 | |
| Interactive strategy simulation and evaluation tool | |
| """ | |
| import streamlit as st | |
| import json | |
| import os | |
| from pathlib import Path | |
| import pandas as pd | |
| import plotly.graph_objects as go | |
| import plotly.express as px | |
| import networkx as nx | |
| import matplotlib.pyplot as plt | |
| from openai import OpenAI | |
| from datetime import datetime | |
| from strategy_core import ( | |
| Strategy, Goal, Arena, Insight, Hypothesis, Move, Metric, | |
| SimulationEngine | |
| ) | |
| # Page configuration | |
| st.set_page_config( | |
| page_title="Strategic Sandbox", | |
| page_icon="π―", | |
| layout="wide", | |
| initial_sidebar_state="expanded" | |
| ) | |
| # Custom CSS for Heuristica aesthetic | |
| st.markdown(""" | |
| <style> | |
| .main-header { | |
| font-size: 2.5rem; | |
| font-weight: 600; | |
| color: #1e90ff; | |
| margin-bottom: 0.5rem; | |
| } | |
| .sub-header { | |
| font-size: 1.2rem; | |
| color: #666; | |
| margin-bottom: 2rem; | |
| } | |
| .metric-card { | |
| background-color: #f0f8ff; | |
| padding: 1rem; | |
| border-radius: 8px; | |
| border-left: 4px solid #1e90ff; | |
| } | |
| .stButton>button { | |
| background-color: #1e90ff; | |
| color: white; | |
| border-radius: 6px; | |
| border: none; | |
| padding: 0.5rem 1.5rem; | |
| font-weight: 500; | |
| } | |
| .stButton>button:hover { | |
| background-color: #187bcd; | |
| } | |
| </style> | |
| """, unsafe_allow_html=True) | |
| # Initialize session state | |
| def init_session_state(): | |
| """Initialize all session state variables""" | |
| if 'strategy' not in st.session_state: | |
| st.session_state.strategy = Strategy() | |
| if 'simulation_results' not in st.session_state: | |
| st.session_state.simulation_results = None | |
| if 'ai_feedback' not in st.session_state: | |
| st.session_state.ai_feedback = None | |
| if 'openai_api_key' not in st.session_state: | |
| st.session_state.openai_api_key = "" | |
| if 'current_page' not in st.session_state: | |
| st.session_state.current_page = "Dashboard" | |
| def save_api_key(): | |
| """Save API key to local file for persistence""" | |
| key_file = Path("data/.api_key") | |
| key_file.parent.mkdir(exist_ok=True) | |
| with open(key_file, 'w') as f: | |
| f.write(st.session_state.openai_api_key) | |
| def load_api_key(): | |
| """Load API key from local file""" | |
| key_file = Path("data/.api_key") | |
| if key_file.exists(): | |
| with open(key_file, 'r') as f: | |
| return f.read().strip() | |
| return "" | |
| def render_sidebar(): | |
| """Render sidebar with navigation and API key input""" | |
| with st.sidebar: | |
| st.markdown('<div class="main-header">π― Strategic Sandbox</div>', unsafe_allow_html=True) | |
| st.markdown("**v1.0** - Strategy Simulation Tool") | |
| st.markdown("---") | |
| # API Key input | |
| st.subheader("OpenAI Configuration") | |
| # Load saved key if available | |
| if not st.session_state.openai_api_key: | |
| st.session_state.openai_api_key = load_api_key() | |
| api_key = st.text_input( | |
| "API Key", | |
| value=st.session_state.openai_api_key, | |
| type="password", | |
| help="Your OpenAI API key will be saved locally" | |
| ) | |
| if api_key != st.session_state.openai_api_key: | |
| st.session_state.openai_api_key = api_key | |
| save_api_key() | |
| if api_key: | |
| st.success("API Key saved!") | |
| st.markdown("---") | |
| # Navigation | |
| st.subheader("Navigation") | |
| pages = ["Dashboard", "Strategy Builder", "Simulation", "AI Evaluator", "Report"] | |
| for page in pages: | |
| if st.button(page, key=f"nav_{page}", use_container_width=True): | |
| st.session_state.current_page = page | |
| st.rerun() | |
| st.markdown("---") | |
| # Quick actions | |
| st.subheader("Quick Actions") | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| if st.button("π Load Example", use_container_width=True): | |
| load_example_strategy() | |
| st.success("Example loaded!") | |
| st.rerun() | |
| with col2: | |
| if st.button("π Reset", use_container_width=True): | |
| st.session_state.strategy = Strategy() | |
| st.session_state.simulation_results = None | |
| st.session_state.ai_feedback = None | |
| st.success("Strategy reset!") | |
| st.rerun() | |
| def load_example_strategy(): | |
| """Load example strategy from JSON""" | |
| example_path = Path("data/example.json") | |
| if example_path.exists(): | |
| st.session_state.strategy = Strategy.from_json(str(example_path)) | |
| def render_dashboard(): | |
| """Render dashboard page""" | |
| st.markdown('<div class="main-header">Dashboard</div>', unsafe_allow_html=True) | |
| st.markdown('<div class="sub-header">Strategic overview and quick start</div>', unsafe_allow_html=True) | |
| # Quick stats | |
| col1, col2, col3, col4 = st.columns(4) | |
| strategy = st.session_state.strategy | |
| with col1: | |
| st.metric("Insights", len(strategy.insights)) | |
| with col2: | |
| st.metric("Hypotheses", len(strategy.hypotheses)) | |
| with col3: | |
| st.metric("Moves", len(strategy.moves)) | |
| with col4: | |
| st.metric("Metrics", len(strategy.metrics)) | |
| st.markdown("---") | |
| # Strategy overview | |
| if strategy.goal: | |
| st.subheader("Current Goal") | |
| st.info(f"**{strategy.goal.text}**\n\nTarget: {strategy.goal.target}{strategy.metrics[0].unit if strategy.metrics else ''} by {strategy.goal.horizon}") | |
| else: | |
| st.warning("No strategy defined yet. Go to **Strategy Builder** to start.") | |
| # Recent simulation results | |
| if st.session_state.simulation_results: | |
| st.subheader("Latest Simulation Results") | |
| results = st.session_state.simulation_results | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| st.metric("Total Impact Score", f"{results['total_impact']:.4f}") | |
| with col2: | |
| if results['move_scores']: | |
| top_move = results['move_scores'][0] | |
| st.metric("Top Move", top_move['text'], f"Score: {top_move['score']:.4f}") | |
| # Quick start guide | |
| st.markdown("---") | |
| st.subheader("Quick Start Guide") | |
| st.markdown(""" | |
| 1. **Strategy Builder**: Define your goal, insights, hypotheses, and moves | |
| 2. **Simulation**: Run scoring simulation to evaluate moves | |
| 3. **AI Evaluator**: Get AI feedback on strategy coherence | |
| 4. **Report**: Generate and export your strategy report | |
| """) | |
| def render_strategy_builder(): | |
| """Render strategy builder page with forms""" | |
| st.markdown('<div class="main-header">Strategy Builder</div>', unsafe_allow_html=True) | |
| st.markdown('<div class="sub-header">Define your strategy components</div>', unsafe_allow_html=True) | |
| strategy = st.session_state.strategy | |
| # Create tabs for different sections | |
| tab1, tab2, tab3, tab4, tab5 = st.tabs([ | |
| "π― Goal & Arena", | |
| "π‘ Insights", | |
| "π¬ Hypotheses", | |
| "π¬ Moves", | |
| "π Supporting Metrics" | |
| ]) | |
| with tab1: | |
| render_goal_arena_form(strategy) | |
| with tab2: | |
| render_insights_form(strategy) | |
| with tab3: | |
| render_hypotheses_form(strategy) | |
| with tab4: | |
| render_moves_form(strategy) | |
| with tab5: | |
| render_metrics_form(strategy) | |
| # Save/Export buttons | |
| st.markdown("---") | |
| col1, col2, col3 = st.columns([1, 1, 2]) | |
| with col1: | |
| if st.button("πΎ Save Strategy", use_container_width=True): | |
| save_strategy() | |
| with col2: | |
| if st.button("π₯ Export JSON", use_container_width=True): | |
| export_strategy_json() | |
| def render_goal_arena_form(strategy): | |
| """Render goal and arena input form""" | |
| st.subheader("Goal Definition") | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| goal_text = st.text_area( | |
| "Goal Statement", | |
| value=strategy.goal.text if strategy.goal else "", | |
| help="What do you want to achieve?" | |
| ) | |
| metric_id = st.text_input( | |
| "Metric ID", | |
| value=strategy.goal.metric if strategy.goal else "", | |
| help="e.g., share_premium, CR_online" | |
| ) | |
| horizon = st.text_input( | |
| "Time Horizon", | |
| value=strategy.goal.horizon if strategy.goal else "2026", | |
| help="When do you want to achieve this?" | |
| ) | |
| with col2: | |
| baseline = st.number_input( | |
| "Baseline Value", | |
| value=float(strategy.goal.baseline) if strategy.goal else 0.0, | |
| help="Current value" | |
| ) | |
| target = st.number_input( | |
| "Target Value", | |
| value=float(strategy.goal.target) if strategy.goal else 0.0, | |
| help="Desired value" | |
| ) | |
| unit = st.text_input( | |
| "Unit", | |
| value=strategy.goal.unit if strategy.goal else "%", | |
| help="Unit of measurement (e.g., %, pts, PLN)" | |
| ) | |
| if st.button("Save Goal"): | |
| strategy.goal = Goal( | |
| text=goal_text, | |
| metric=metric_id, | |
| baseline=baseline, | |
| target=target, | |
| horizon=horizon, | |
| unit=unit | |
| ) | |
| st.success("Goal saved!") | |
| st.markdown("---") | |
| st.subheader("Arena Definition") | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| market = st.text_input( | |
| "Market", | |
| value=strategy.arena.market if strategy.arena else "", | |
| help="e.g., PL, EU, US" | |
| ) | |
| category = st.text_input( | |
| "Category", | |
| value=strategy.arena.category if strategy.arena else "", | |
| help="e.g., pasta_premium, skincare" | |
| ) | |
| target_audience = st.text_area( | |
| "Target Audience", | |
| value=strategy.arena.target_audience if strategy.arena else "", | |
| help="Describe your target audience (demographics, psychographics, behaviors)" | |
| ) | |
| with col2: | |
| competitors_text = st.text_area( | |
| "Competitors (one per line)", | |
| value="\n".join(strategy.arena.competitors) if strategy.arena else "", | |
| help="List your main competitors" | |
| ) | |
| if st.button("Save Arena"): | |
| competitors = [c.strip() for c in competitors_text.split("\n") if c.strip()] | |
| strategy.arena = Arena( | |
| market=market, | |
| category=category, | |
| competitors=competitors, | |
| target_audience=target_audience | |
| ) | |
| st.success("Arena saved!") | |
| def render_insights_form(strategy): | |
| """Render insights input form""" | |
| st.subheader("Add Insight") | |
| col1, col2 = st.columns([2, 1]) | |
| with col1: | |
| insight_id = st.text_input("Insight ID", help="e.g., i1, i2") | |
| insight_text = st.text_area("Insight Statement", help="What did you learn?") | |
| with col2: | |
| evidence_text = st.text_area("Evidence Sources (one per line)", help="e.g., FGI_2024, Survey_Q1") | |
| if st.button("Add Insight"): | |
| if insight_id and insight_text: | |
| evidence = [e.strip() for e in evidence_text.split("\n") if e.strip()] | |
| strategy.insights.append(Insight( | |
| id=insight_id, | |
| text=insight_text, | |
| evidence=evidence | |
| )) | |
| st.success(f"Insight {insight_id} added!") | |
| st.rerun() | |
| # Display existing insights | |
| if strategy.insights: | |
| st.markdown("---") | |
| st.subheader("Current Insights") | |
| for i, insight in enumerate(strategy.insights): | |
| with st.expander(f"{insight.id}: {insight.text[:50]}..."): | |
| st.write(f"**Full text:** {insight.text}") | |
| st.write(f"**Evidence:** {', '.join(insight.evidence)}") | |
| if st.button(f"Remove {insight.id}", key=f"remove_insight_{i}"): | |
| strategy.insights.pop(i) | |
| st.rerun() | |
| def render_hypotheses_form(strategy): | |
| """Render hypotheses input form""" | |
| st.subheader("Add Hypothesis") | |
| col1, col2 = st.columns([2, 1]) | |
| with col1: | |
| hyp_id = st.text_input("Hypothesis ID", help="e.g., h1, h2") | |
| hyp_text = st.text_area("Hypothesis Statement", help="What do you believe will happen?") | |
| # Insight selector | |
| insight_ids = [i.id for i in strategy.insights] | |
| based_on = st.multiselect("Based on Insights", insight_ids) | |
| with col2: | |
| metric_id = st.text_input("Target Metric ID", help="Which metric will this affect?") | |
| expected_change = st.number_input("Expected Change", value=0.0, step=0.01, help="Expected change (e.g., 0.2 for +20%)") | |
| if st.button("Add Hypothesis"): | |
| if hyp_id and hyp_text: | |
| strategy.hypotheses.append(Hypothesis( | |
| id=hyp_id, | |
| text=hyp_text, | |
| based_on=based_on, | |
| metric=metric_id, | |
| expected_change=expected_change | |
| )) | |
| st.success(f"Hypothesis {hyp_id} added!") | |
| st.rerun() | |
| # Display existing hypotheses | |
| if strategy.hypotheses: | |
| st.markdown("---") | |
| st.subheader("Current Hypotheses") | |
| for i, hyp in enumerate(strategy.hypotheses): | |
| with st.expander(f"{hyp.id}: {hyp.text[:50]}..."): | |
| st.write(f"**Full text:** {hyp.text}") | |
| st.write(f"**Based on:** {', '.join(hyp.based_on)}") | |
| st.write(f"**Metric:** {hyp.metric} | **Expected change:** {hyp.expected_change}") | |
| if st.button(f"Remove {hyp.id}", key=f"remove_hyp_{i}"): | |
| strategy.hypotheses.pop(i) | |
| st.rerun() | |
| def render_moves_form(strategy): | |
| """Render moves input form""" | |
| st.subheader("Add Move") | |
| col1, col2 = st.columns([2, 1]) | |
| with col1: | |
| move_id = st.text_input("Move ID", help="e.g., m1, m2") | |
| move_text = st.text_area("Move Description", help="What action will you take?") | |
| # Hypothesis selector | |
| hyp_ids = [h.id for h in strategy.hypotheses] | |
| linked_hyp = st.selectbox("Linked Hypothesis", [""] + hyp_ids) | |
| with col2: | |
| impact = st.slider("Impact", 0.0, 1.0, 0.5, 0.05, help="Expected impact (0-1)") | |
| fit = st.slider("Fit", 0.0, 1.0, 0.8, 0.05, help="Fit with strategy (0-1)") | |
| risk = st.slider("Risk", 0.0, 1.0, 0.3, 0.05, help="Risk level (0-1)") | |
| cost = st.number_input("Cost", value=100000.0, step=10000.0, help="Cost in currency") | |
| if st.button("Add Move"): | |
| if move_id and move_text and linked_hyp: | |
| strategy.moves.append(Move( | |
| id=move_id, | |
| text=move_text, | |
| linked_hypothesis=linked_hyp, | |
| impact=impact, | |
| fit=fit, | |
| risk=risk, | |
| cost=cost | |
| )) | |
| st.success(f"Move {move_id} added!") | |
| st.rerun() | |
| # Display existing moves | |
| if strategy.moves: | |
| st.markdown("---") | |
| st.subheader("Current Moves") | |
| for i, move in enumerate(strategy.moves): | |
| with st.expander(f"{move.id}: {move.text[:50]}..."): | |
| st.write(f"**Full text:** {move.text}") | |
| st.write(f"**Linked hypothesis:** {move.linked_hypothesis}") | |
| col1, col2, col3, col4 = st.columns(4) | |
| col1.metric("Impact", f"{move.impact:.2f}") | |
| col2.metric("Fit", f"{move.fit:.2f}") | |
| col3.metric("Risk", f"{move.risk:.2f}") | |
| col4.metric("Cost", f"{move.cost:,.0f}") | |
| if st.button(f"Remove {move.id}", key=f"remove_move_{i}"): | |
| strategy.moves.pop(i) | |
| st.rerun() | |
| def render_metrics_form(strategy): | |
| """Render metrics input form""" | |
| st.subheader("Add Supporting Metric") | |
| st.info("π‘ Main goal metric is defined in Goal & Arena tab. Here you add supporting/leading indicator metrics that help track progress.") | |
| col1, col2 = st.columns([2, 1]) | |
| with col1: | |
| metric_id = st.text_input("Metric ID", help="e.g., CR_online, consideration") | |
| metric_text = st.text_input("Metric Name", help="Full name of the metric") | |
| with col2: | |
| baseline = st.number_input("Baseline", value=0.0, help="Current value") | |
| target = st.number_input("Target", value=0.0, help="Target value") | |
| unit = st.text_input("Unit", value="%", help="Unit of measurement") | |
| if st.button("Add Metric"): | |
| if metric_id and metric_text: | |
| strategy.metrics.append(Metric( | |
| id=metric_id, | |
| text=metric_text, | |
| baseline=baseline, | |
| target=target, | |
| unit=unit | |
| )) | |
| st.success(f"Metric {metric_id} added!") | |
| st.rerun() | |
| # Display existing metrics | |
| if strategy.metrics: | |
| st.markdown("---") | |
| st.subheader("Current Metrics") | |
| for i, metric in enumerate(strategy.metrics): | |
| with st.expander(f"{metric.id}: {metric.text}"): | |
| col1, col2, col3 = st.columns(3) | |
| col1.metric("Baseline", f"{metric.baseline}{metric.unit}") | |
| col2.metric("Target", f"{metric.target}{metric.unit}") | |
| col3.metric("Gap", f"{metric.target - metric.baseline}{metric.unit}") | |
| if st.button(f"Remove {metric.id}", key=f"remove_metric_{i}"): | |
| strategy.metrics.pop(i) | |
| st.rerun() | |
| def render_simulation(): | |
| """Render simulation page""" | |
| st.markdown('<div class="main-header">Simulation</div>', unsafe_allow_html=True) | |
| st.markdown('<div class="sub-header">Run strategy simulation and view results</div>', unsafe_allow_html=True) | |
| strategy = st.session_state.strategy | |
| # Check if strategy is ready | |
| if not strategy.moves: | |
| st.warning("No moves defined. Go to Strategy Builder to add moves.") | |
| return | |
| # Run simulation button | |
| if st.button("βΆοΈ Run Simulation", use_container_width=True): | |
| with st.spinner("Running simulation..."): | |
| results = SimulationEngine.simulate_strategy(strategy) | |
| st.session_state.simulation_results = results | |
| st.success("Simulation complete!") | |
| # Display results | |
| if st.session_state.simulation_results: | |
| results = st.session_state.simulation_results | |
| # Overall metrics | |
| st.subheader("Overall Results") | |
| col1, col2, col3 = st.columns(3) | |
| with col1: | |
| st.metric("Total Impact Score", f"{results['total_impact']:.4f}") | |
| with col2: | |
| st.metric("Number of Moves", len(results['move_scores'])) | |
| with col3: | |
| avg_risk = sum(m['risk'] for m in results['move_scores']) / len(results['move_scores']) | |
| st.metric("Average Risk", f"{avg_risk:.2f}") | |
| # Moves ranking | |
| st.markdown("---") | |
| st.subheader("Moves Ranking") | |
| df = SimulationEngine.create_results_dataframe(results) | |
| st.dataframe(df, use_container_width=True) | |
| # Visualizations | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| # Bar chart - scores | |
| fig_scores = go.Figure() | |
| fig_scores.add_trace(go.Bar( | |
| x=[m['id'] for m in results['move_scores']], | |
| y=[m['score'] for m in results['move_scores']], | |
| marker_color='#1e90ff', | |
| text=[f"{m['score']:.4f}" for m in results['move_scores']], | |
| textposition='auto' | |
| )) | |
| fig_scores.update_layout( | |
| title="Move Scores Ranking", | |
| xaxis_title="Move ID", | |
| yaxis_title="Score", | |
| height=400 | |
| ) | |
| st.plotly_chart(fig_scores, use_container_width=True) | |
| with col2: | |
| # Scatter plot - risk vs impact | |
| fig_scatter = go.Figure() | |
| for move in results['move_scores']: | |
| fig_scatter.add_trace(go.Scatter( | |
| x=[move['risk']], | |
| y=[move['impact']], | |
| mode='markers+text', | |
| name=move['id'], | |
| text=[move['id']], | |
| textposition="top center", | |
| marker=dict( | |
| size=move['score'] * 100, | |
| color=move['fit'], | |
| colorscale='Blues', | |
| showscale=True, | |
| colorbar=dict( | |
| title=dict( | |
| text="Fit", | |
| font=dict(size=14, color='white'), | |
| side='right' | |
| ), | |
| showticklabels=False, | |
| thickness=15, | |
| len=0.7 | |
| ) | |
| ) | |
| )) | |
| fig_scatter.update_layout( | |
| title={ | |
| 'text': "Risk vs Impact Analysis<br><sub>Bubble size = Score | Color intensity = Fit</sub>", | |
| 'x': 0.5, | |
| 'xanchor': 'center' | |
| }, | |
| xaxis_title="Risk β", | |
| yaxis_title="Impact β", | |
| height=400, | |
| showlegend=False | |
| ) | |
| st.plotly_chart(fig_scatter, use_container_width=True) | |
| st.caption("π‘ Larger bubbles = higher score | Darker blue = better fit with strategy") | |
| # Metric forecasts | |
| if results['metric_forecasts']: | |
| st.markdown("---") | |
| st.subheader("Metric Forecasts") | |
| for forecast in results['metric_forecasts']: | |
| # Highlight main goal metric | |
| icon = "π―" if forecast.get('is_main') else "π" | |
| title_prefix = "MAIN GOAL: " if forecast.get('is_main') else "" | |
| with st.expander(f"{icon} {title_prefix}{forecast['text']}", expanded=True): | |
| # Metric summary in columns | |
| col1, col2, col3 = st.columns(3) | |
| with col1: | |
| st.markdown(f"**Baseline:** {forecast['baseline']}{forecast['unit']}") | |
| with col2: | |
| st.markdown(f"**Forecast:** <span style='color: #1e90ff; font-weight: bold;'>{forecast['forecast']}{forecast['unit']}</span>", unsafe_allow_html=True) | |
| with col3: | |
| gap_color = 'red' if forecast['gap_to_target'] > 0 else 'green' | |
| st.markdown(f"**Gap to Target:** <span style='color: {gap_color}; font-weight: bold;'>{forecast['gap_to_target']:+.2f}{forecast['unit']}</span>", unsafe_allow_html=True) | |
| st.markdown("") # spacing | |
| # Show linked hypotheses | |
| if forecast.get('linked_hypotheses'): | |
| st.markdown(f"**Linked Hypotheses:** {', '.join(forecast['linked_hypotheses'])}") | |
| # Show contributing moves breakdown | |
| if forecast.get('linked_moves'): | |
| st.markdown("**Contributing Moves:**") | |
| if len(forecast['linked_moves']) == 0: | |
| st.info("β οΈ No moves linked to this metric") | |
| else: | |
| for move in forecast['linked_moves']: | |
| contribution = move['score'] * forecast['baseline'] | |
| st.markdown(f"- **{move['id']}**: {move['text'][:60]}... (score: {move['score']:.4f}, contribution: +{contribution:.2f}{forecast['unit']})") | |
| else: | |
| st.info("β οΈ No moves linked to this metric") | |
| # Recommendations | |
| if results['recommendations']: | |
| st.markdown("---") | |
| st.subheader("Recommendations") | |
| for rec in results['recommendations']: | |
| st.info(rec) | |
| # Strategy graph visualization | |
| st.markdown("---") | |
| st.subheader("Strategy Logic Flow") | |
| render_strategy_graph(strategy) | |
| def render_strategy_graph(strategy): | |
| """Render NetworkX graph of strategy logic""" | |
| G = nx.DiGraph() | |
| # Add nodes | |
| for insight in strategy.insights: | |
| G.add_node(insight.id, type='insight', label=insight.text[:30]) | |
| for hyp in strategy.hypotheses: | |
| G.add_node(hyp.id, type='hypothesis', label=hyp.text[:30]) | |
| for move in strategy.moves: | |
| G.add_node(move.id, type='move', label=move.text[:30]) | |
| for metric in strategy.metrics: | |
| G.add_node(metric.id, type='metric', label=metric.text[:30]) | |
| # Add edges | |
| for hyp in strategy.hypotheses: | |
| for insight_id in hyp.based_on: | |
| if G.has_node(insight_id): | |
| G.add_edge(insight_id, hyp.id) | |
| if G.has_node(hyp.metric): | |
| G.add_edge(hyp.id, hyp.metric) | |
| for move in strategy.moves: | |
| if G.has_node(move.linked_hypothesis): | |
| G.add_edge(move.linked_hypothesis, move.id) | |
| # Draw graph | |
| fig, ax = plt.subplots(figsize=(12, 8)) | |
| pos = nx.spring_layout(G, k=2, iterations=50, seed=42) | |
| # Color nodes by type | |
| node_colors = [] | |
| for node in G.nodes(): | |
| node_type = G.nodes[node].get('type', '') | |
| if node_type == 'insight': | |
| node_colors.append('#90EE90') | |
| elif node_type == 'hypothesis': | |
| node_colors.append('#FFD700') | |
| elif node_type == 'move': | |
| node_colors.append('#1e90ff') | |
| elif node_type == 'metric': | |
| node_colors.append('#FF6B6B') | |
| else: | |
| node_colors.append('#CCCCCC') | |
| nx.draw(G, pos, ax=ax, node_color=node_colors, node_size=2000, | |
| with_labels=True, font_size=8, font_weight='bold', | |
| arrows=True, arrowsize=20, edge_color='#999999', width=2) | |
| ax.set_title("Strategy Logic Flow\n(Green=Insight, Yellow=Hypothesis, Blue=Move, Red=Metric)", | |
| fontsize=12, fontweight='bold') | |
| st.pyplot(fig, clear_figure=True) | |
| def render_ai_evaluator(): | |
| """Render AI evaluator page""" | |
| st.markdown('<div class="main-header">AI Evaluator</div>', unsafe_allow_html=True) | |
| st.markdown('<div class="sub-header">Get brutally honest AI audit of your strategy</div>', unsafe_allow_html=True) | |
| strategy = st.session_state.strategy | |
| # Check API key | |
| if not st.session_state.openai_api_key: | |
| st.warning("Please configure your OpenAI API key in the sidebar.") | |
| return | |
| # Check if strategy is ready | |
| if not strategy.goal or not strategy.moves: | |
| st.warning("Strategy is incomplete. Please define at least a goal and some moves.") | |
| return | |
| # Display current strategy summary | |
| st.subheader("Strategy Summary") | |
| st.write(f"**Goal:** {strategy.goal.text}") | |
| st.write(f"**Insights:** {len(strategy.insights)}") | |
| st.write(f"**Hypotheses:** {len(strategy.hypotheses)}") | |
| st.write(f"**Moves:** {len(strategy.moves)}") | |
| st.markdown("---") | |
| # Ask AI button | |
| st.info("β οΈ This AI audit will be brutally honest - expect specific criticism, red flags, and concrete fixes (not polite suggestions).") | |
| if st.button("π€ Run Strategy Audit", use_container_width=True): | |
| with st.spinner("AI is auditing your strategy..."): | |
| feedback = get_ai_evaluation(strategy) | |
| st.session_state.ai_feedback = feedback | |
| st.rerun() | |
| # Display AI feedback | |
| if st.session_state.ai_feedback: | |
| st.markdown("---") | |
| st.subheader("π Strategy Audit Report") | |
| st.markdown(st.session_state.ai_feedback) | |
| def get_ai_evaluation(strategy: Strategy) -> str: | |
| """Call OpenAI API to evaluate strategy""" | |
| try: | |
| client = OpenAI(api_key=st.session_state.openai_api_key) | |
| # Prepare strategy data | |
| strategy_json = json.dumps(strategy.to_dict(), indent=2, ensure_ascii=False) | |
| # Create prompt | |
| prompt = f"""You are a ruthlessly analytical strategy evaluator. Your job is to find weaknesses, gaps, and risks - not to be polite. | |
| IMPORTANT: Detect the language of the strategy below and respond in THE SAME LANGUAGE. | |
| If the strategy is in Polish, respond in Polish. If in English, respond in English. | |
| Analyze this strategy with BRUTAL honesty: | |
| Strategy: | |
| ```json | |
| {strategy_json} | |
| ``` | |
| Your evaluation MUST include: | |
| ## 1. LOGICAL FLOW AUDIT (Score: X/10) | |
| - Map exact connections: Which insights feed which hypotheses? Which hypotheses feed which moves? | |
| - RED FLAGS: List any broken links (e.g., "Move m3 links to h2, but h2 doesn't target the main goal metric") | |
| - ORPHANS: Are there insights/hypotheses not connected to any moves? List them by ID. | |
| ## 2. HYPOTHESIS QUALITY CHECK | |
| For EACH hypothesis (h1, h2, h3, h4...): | |
| - Is expected_change realistic? (Compare to industry benchmarks if known) | |
| - Is the metric actually measurable? How would you track it? | |
| - RED FLAG if hypothesis is vague or unmeasurable | |
| ## 3. MOVE PORTFOLIO ANALYSIS | |
| - Cost concentration: What % of budget goes to top move? (Flag if >40%) | |
| - Risk profile: How many high-risk moves (risk >0.6)? | |
| - MISSING MOVES: What obvious actions are NOT here? (e.g., "No paid search strategy despite digital focus") | |
| - Prioritization: Using score formula, which moves should be CUT? Be specific with IDs. | |
| ## 4. GAP ANALYSIS - What's MISSING? | |
| - Competitive response: How will competitors react? No moves addressing this? | |
| - Measurement plan: How will you track these metrics in reality? | |
| - Budget realism: Total cost vs. expected revenue/impact - does math work? | |
| - Timeline: Are moves sequenced or all at once? Flag if unclear. | |
| ## 5. TOP 3 FATAL FLAWS | |
| List the 3 biggest risks that could KILL this strategy. Be specific: | |
| - "Move m6 costs 150k but has lowest score (X.XX) - CUT IT" | |
| - "No B2B distribution strategy despite targeting gyms/studios" | |
| - "Hypothesis h2 assumes 30% trial rate but category average is 8%" | |
| ## 6. ACTIONABLE FIXES (Prioritized) | |
| π΄ CRITICAL (do NOW): | |
| - Specific change with exact ID reference (e.g., "Change h3 expected_change from 0.45 to 0.25") | |
| π‘ IMPORTANT (before launch): | |
| - Concrete additions (e.g., "Add move m7: Partnership with Ε»abka for distribution") | |
| π’ NICE TO HAVE: | |
| - Optimizations | |
| NO GENERIC ADVICE like "consider research" or "might want to test". Give SPECIFIC actions with IDs, numbers, and rationale. | |
| Remember: RESPOND IN THE SAME LANGUAGE AS THE STRATEGY. Be direct, numerical, and reference specific IDs. | |
| """ | |
| # Call API | |
| response = client.chat.completions.create( | |
| model="gpt-4", | |
| messages=[ | |
| {"role": "system", "content": "You are a ruthlessly honest strategy auditor. Find flaws, gaps, and risks. Reference specific IDs and numbers. Always respond in the same language as the user's input."}, | |
| {"role": "user", "content": prompt} | |
| ], | |
| temperature=0.5, | |
| max_tokens=2500 | |
| ) | |
| return response.choices[0].message.content | |
| except Exception as e: | |
| return f"Error calling OpenAI API: {str(e)}\n\nPlease check your API key and try again." | |
| def render_report(): | |
| """Render report generation page""" | |
| st.markdown('<div class="main-header">Strategy Report</div>', unsafe_allow_html=True) | |
| st.markdown('<div class="sub-header">View and export your complete strategy</div>', unsafe_allow_html=True) | |
| strategy = st.session_state.strategy | |
| if not strategy.goal: | |
| st.warning("No strategy to report. Go to Strategy Builder first.") | |
| return | |
| # Generate report | |
| report = generate_report(strategy) | |
| # Display report | |
| st.markdown(report) | |
| # Export buttons | |
| st.markdown("---") | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| st.download_button( | |
| label="π Download as Markdown", | |
| data=report, | |
| file_name=f"strategy_report_{datetime.now().strftime('%Y%m%d_%H%M%S')}.md", | |
| mime="text/markdown", | |
| use_container_width=True | |
| ) | |
| with col2: | |
| json_data = json.dumps(strategy.to_dict(), indent=2, ensure_ascii=False) | |
| st.download_button( | |
| label="π₯ Download as JSON", | |
| data=json_data, | |
| file_name=f"strategy_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json", | |
| mime="application/json", | |
| use_container_width=True | |
| ) | |
| def generate_report(strategy: Strategy) -> str: | |
| """Generate markdown report from strategy""" | |
| report = f"""# Strategic Sandbox Report | |
| *Generated on {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}* | |
| --- | |
| ## π― Goal | |
| **{strategy.goal.text if strategy.goal else 'Not defined'}** | |
| """ | |
| if strategy.goal: | |
| report += f""" | |
| - **Metric:** {strategy.goal.metric} | |
| - **Baseline:** {strategy.goal.baseline}{strategy.goal.unit} | |
| - **Target:** {strategy.goal.target}{strategy.goal.unit} | |
| - **Horizon:** {strategy.goal.horizon} | |
| - **Unit:** {strategy.goal.unit} | |
| """ | |
| if strategy.arena: | |
| report += f""" | |
| --- | |
| ## πΊοΈ Arena | |
| - **Market:** {strategy.arena.market} | |
| - **Category:** {strategy.arena.category} | |
| - **Target Audience:** {strategy.arena.target_audience} | |
| - **Competitors:** {', '.join(strategy.arena.competitors)} | |
| """ | |
| if strategy.insights: | |
| report += "\n---\n\n## π‘ Insights\n\n" | |
| for insight in strategy.insights: | |
| report += f"### {insight.id}\n{insight.text}\n\n*Evidence: {', '.join(insight.evidence)}*\n\n" | |
| if strategy.hypotheses: | |
| report += "---\n\n## π¬ Hypotheses\n\n" | |
| for hyp in strategy.hypotheses: | |
| report += f"### {hyp.id}\n{hyp.text}\n\n" | |
| report += f"- Based on: {', '.join(hyp.based_on)}\n" | |
| report += f"- Metric: {hyp.metric}\n" | |
| report += f"- Expected change: {hyp.expected_change}\n\n" | |
| if strategy.moves: | |
| report += "---\n\n## π¬ Moves\n\n" | |
| for move in strategy.moves: | |
| report += f"### {move.id}: {move.text}\n\n" | |
| report += f"- Linked hypothesis: {move.linked_hypothesis}\n" | |
| report += f"- Impact: {move.impact} | Fit: {move.fit} | Risk: {move.risk}\n" | |
| report += f"- Cost: {move.cost:,.0f}\n\n" | |
| if strategy.metrics: | |
| report += "---\n\n## π Supporting Metrics\n\n" | |
| for metric in strategy.metrics: | |
| report += f"### {metric.id}: {metric.text}\n" | |
| report += f"- Baseline: {metric.baseline}{metric.unit}\n" | |
| report += f"- Target: {metric.target}{metric.unit}\n\n" | |
| # Add simulation results if available | |
| if st.session_state.simulation_results: | |
| results = st.session_state.simulation_results | |
| report += "---\n\n## π Simulation Results\n\n" | |
| report += f"**Total Impact Score:** {results['total_impact']:.4f}\n\n" | |
| report += "### Move Rankings\n\n" | |
| for move in results['move_scores']: | |
| report += f"- **{move['id']}**: Score {move['score']:.4f} (Impact: {move['impact']}, Risk: {move['risk']}, Cost: {move['cost']:,.0f})\n" | |
| # Add AI feedback if available | |
| if st.session_state.ai_feedback: | |
| report += "\n---\n\n## π€ AI Evaluation\n\n" | |
| report += st.session_state.ai_feedback | |
| report += "\n\n---\n\n*Generated with Strategic Sandbox v1.0*\n" | |
| report += "*π€ Generated with [Claude Code](https://claude.com/claude-code)*\n" | |
| return report | |
| def save_strategy(): | |
| """Save strategy to JSON file""" | |
| timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') | |
| filename = f"data/strategy_{timestamp}.json" | |
| st.session_state.strategy.to_json(filename) | |
| st.success(f"Strategy saved to {filename}") | |
| def export_strategy_json(): | |
| """Export strategy as downloadable JSON""" | |
| json_data = json.dumps(st.session_state.strategy.to_dict(), indent=2, ensure_ascii=False) | |
| st.download_button( | |
| label="Download JSON", | |
| data=json_data, | |
| file_name=f"strategy_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json", | |
| mime="application/json" | |
| ) | |
| # Main app | |
| def main(): | |
| init_session_state() | |
| render_sidebar() | |
| # Route to appropriate page | |
| page = st.session_state.current_page | |
| if page == "Dashboard": | |
| render_dashboard() | |
| elif page == "Strategy Builder": | |
| render_strategy_builder() | |
| elif page == "Simulation": | |
| render_simulation() | |
| elif page == "AI Evaluator": | |
| render_ai_evaluator() | |
| elif page == "Report": | |
| render_report() | |
| if __name__ == "__main__": | |
| main() | |