Marek4321 commited on
Commit
257fecc
Β·
verified Β·
1 Parent(s): 4baa1a5

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +1042 -1043
app.py CHANGED
@@ -1,1043 +1,1042 @@
1
- """
2
- Strategic Sandbox v1.0
3
- Interactive strategy simulation and evaluation tool
4
- """
5
-
6
- import streamlit as st
7
- import json
8
- import os
9
- from pathlib import Path
10
- import pandas as pd
11
- import plotly.graph_objects as go
12
- import plotly.express as px
13
- import networkx as nx
14
- import matplotlib.pyplot as plt
15
- from openai import OpenAI
16
- from datetime import datetime
17
-
18
- from strategy_core import (
19
- Strategy, Goal, Arena, Insight, Hypothesis, Move, Metric,
20
- SimulationEngine
21
- )
22
-
23
-
24
- # Page configuration
25
- st.set_page_config(
26
- page_title="Strategic Sandbox",
27
- page_icon="🎯",
28
- layout="wide",
29
- initial_sidebar_state="expanded"
30
- )
31
-
32
- # Custom CSS for Heuristica aesthetic
33
- st.markdown("""
34
- <style>
35
- .main-header {
36
- font-size: 2.5rem;
37
- font-weight: 600;
38
- color: #1e90ff;
39
- margin-bottom: 0.5rem;
40
- }
41
- .sub-header {
42
- font-size: 1.2rem;
43
- color: #666;
44
- margin-bottom: 2rem;
45
- }
46
- .metric-card {
47
- background-color: #f0f8ff;
48
- padding: 1rem;
49
- border-radius: 8px;
50
- border-left: 4px solid #1e90ff;
51
- }
52
- .stButton>button {
53
- background-color: #1e90ff;
54
- color: white;
55
- border-radius: 6px;
56
- border: none;
57
- padding: 0.5rem 1.5rem;
58
- font-weight: 500;
59
- }
60
- .stButton>button:hover {
61
- background-color: #187bcd;
62
- }
63
- </style>
64
- """, unsafe_allow_html=True)
65
-
66
-
67
- # Initialize session state
68
- def init_session_state():
69
- """Initialize all session state variables"""
70
- if 'strategy' not in st.session_state:
71
- st.session_state.strategy = Strategy()
72
- if 'simulation_results' not in st.session_state:
73
- st.session_state.simulation_results = None
74
- if 'ai_feedback' not in st.session_state:
75
- st.session_state.ai_feedback = None
76
- if 'openai_api_key' not in st.session_state:
77
- st.session_state.openai_api_key = ""
78
- if 'current_page' not in st.session_state:
79
- st.session_state.current_page = "Dashboard"
80
-
81
-
82
- def save_api_key():
83
- """Save API key to local file for persistence"""
84
- key_file = Path("data/.api_key")
85
- key_file.parent.mkdir(exist_ok=True)
86
- with open(key_file, 'w') as f:
87
- f.write(st.session_state.openai_api_key)
88
-
89
-
90
- def load_api_key():
91
- """Load API key from local file"""
92
- key_file = Path("data/.api_key")
93
- if key_file.exists():
94
- with open(key_file, 'r') as f:
95
- return f.read().strip()
96
- return ""
97
-
98
-
99
- def render_sidebar():
100
- """Render sidebar with navigation and API key input"""
101
- with st.sidebar:
102
- st.markdown('<div class="main-header">🎯 Strategic Sandbox</div>', unsafe_allow_html=True)
103
- st.markdown("**v1.0** - Strategy Simulation Tool")
104
- st.markdown("---")
105
-
106
- # API Key input
107
- st.subheader("OpenAI Configuration")
108
-
109
- # Load saved key if available
110
- if not st.session_state.openai_api_key:
111
- st.session_state.openai_api_key = load_api_key()
112
-
113
- api_key = st.text_input(
114
- "API Key",
115
- value=st.session_state.openai_api_key,
116
- type="password",
117
- help="Your OpenAI API key will be saved locally"
118
- )
119
-
120
- if api_key != st.session_state.openai_api_key:
121
- st.session_state.openai_api_key = api_key
122
- save_api_key()
123
- if api_key:
124
- st.success("API Key saved!")
125
-
126
- st.markdown("---")
127
-
128
- # Navigation
129
- st.subheader("Navigation")
130
- pages = ["Dashboard", "Strategy Builder", "Simulation", "AI Evaluator", "Report"]
131
-
132
- for page in pages:
133
- if st.button(page, key=f"nav_{page}", use_container_width=True):
134
- st.session_state.current_page = page
135
- st.rerun()
136
-
137
- st.markdown("---")
138
-
139
- # Quick actions
140
- st.subheader("Quick Actions")
141
-
142
- col1, col2 = st.columns(2)
143
- with col1:
144
- if st.button("πŸ“ Load Example", use_container_width=True):
145
- load_example_strategy()
146
- st.success("Example loaded!")
147
- st.rerun()
148
-
149
- with col2:
150
- if st.button("πŸ”„ Reset", use_container_width=True):
151
- st.session_state.strategy = Strategy()
152
- st.session_state.simulation_results = None
153
- st.session_state.ai_feedback = None
154
- st.success("Strategy reset!")
155
- st.rerun()
156
-
157
-
158
- def load_example_strategy():
159
- """Load example strategy from JSON"""
160
- example_path = Path("data/example.json")
161
- if example_path.exists():
162
- st.session_state.strategy = Strategy.from_json(str(example_path))
163
-
164
-
165
- def render_dashboard():
166
- """Render dashboard page"""
167
- st.markdown('<div class="main-header">Dashboard</div>', unsafe_allow_html=True)
168
- st.markdown('<div class="sub-header">Strategic overview and quick start</div>', unsafe_allow_html=True)
169
-
170
- # Quick stats
171
- col1, col2, col3, col4 = st.columns(4)
172
-
173
- strategy = st.session_state.strategy
174
-
175
- with col1:
176
- st.metric("Insights", len(strategy.insights))
177
- with col2:
178
- st.metric("Hypotheses", len(strategy.hypotheses))
179
- with col3:
180
- st.metric("Moves", len(strategy.moves))
181
- with col4:
182
- st.metric("Metrics", len(strategy.metrics))
183
-
184
- st.markdown("---")
185
-
186
- # Strategy overview
187
- if strategy.goal:
188
- st.subheader("Current Goal")
189
- st.info(f"**{strategy.goal.text}**\n\nTarget: {strategy.goal.target}{strategy.metrics[0].unit if strategy.metrics else ''} by {strategy.goal.horizon}")
190
- else:
191
- st.warning("No strategy defined yet. Go to **Strategy Builder** to start.")
192
-
193
- # Recent simulation results
194
- if st.session_state.simulation_results:
195
- st.subheader("Latest Simulation Results")
196
- results = st.session_state.simulation_results
197
-
198
- col1, col2 = st.columns(2)
199
- with col1:
200
- st.metric("Total Impact Score", f"{results['total_impact']:.4f}")
201
- with col2:
202
- if results['move_scores']:
203
- top_move = results['move_scores'][0]
204
- st.metric("Top Move", top_move['text'], f"Score: {top_move['score']:.4f}")
205
-
206
- # Quick start guide
207
- st.markdown("---")
208
- st.subheader("Quick Start Guide")
209
-
210
- st.markdown("""
211
- 1. **Strategy Builder**: Define your goal, insights, hypotheses, and moves
212
- 2. **Simulation**: Run scoring simulation to evaluate moves
213
- 3. **AI Evaluator**: Get AI feedback on strategy coherence
214
- 4. **Report**: Generate and export your strategy report
215
- """)
216
-
217
-
218
- def render_strategy_builder():
219
- """Render strategy builder page with forms"""
220
- st.markdown('<div class="main-header">Strategy Builder</div>', unsafe_allow_html=True)
221
- st.markdown('<div class="sub-header">Define your strategy components</div>', unsafe_allow_html=True)
222
-
223
- strategy = st.session_state.strategy
224
-
225
- # Create tabs for different sections
226
- tab1, tab2, tab3, tab4, tab5 = st.tabs([
227
- "🎯 Goal & Arena",
228
- "πŸ’‘ Insights",
229
- "πŸ”¬ Hypotheses",
230
- "🎬 Moves",
231
- "πŸ“Š Supporting Metrics"
232
- ])
233
-
234
- with tab1:
235
- render_goal_arena_form(strategy)
236
-
237
- with tab2:
238
- render_insights_form(strategy)
239
-
240
- with tab3:
241
- render_hypotheses_form(strategy)
242
-
243
- with tab4:
244
- render_moves_form(strategy)
245
-
246
- with tab5:
247
- render_metrics_form(strategy)
248
-
249
- # Save/Export buttons
250
- st.markdown("---")
251
- col1, col2, col3 = st.columns([1, 1, 2])
252
-
253
- with col1:
254
- if st.button("πŸ’Ύ Save Strategy", use_container_width=True):
255
- save_strategy()
256
-
257
- with col2:
258
- if st.button("πŸ“₯ Export JSON", use_container_width=True):
259
- export_strategy_json()
260
-
261
-
262
- def render_goal_arena_form(strategy):
263
- """Render goal and arena input form"""
264
- st.subheader("Goal Definition")
265
-
266
- col1, col2 = st.columns(2)
267
-
268
- with col1:
269
- goal_text = st.text_area(
270
- "Goal Statement",
271
- value=strategy.goal.text if strategy.goal else "",
272
- help="What do you want to achieve?"
273
- )
274
-
275
- metric_id = st.text_input(
276
- "Metric ID",
277
- value=strategy.goal.metric if strategy.goal else "",
278
- help="e.g., share_premium, CR_online"
279
- )
280
-
281
- horizon = st.text_input(
282
- "Time Horizon",
283
- value=strategy.goal.horizon if strategy.goal else "2026",
284
- help="When do you want to achieve this?"
285
- )
286
-
287
- with col2:
288
- baseline = st.number_input(
289
- "Baseline Value",
290
- value=float(strategy.goal.baseline) if strategy.goal else 0.0,
291
- help="Current value"
292
- )
293
-
294
- target = st.number_input(
295
- "Target Value",
296
- value=float(strategy.goal.target) if strategy.goal else 0.0,
297
- help="Desired value"
298
- )
299
-
300
- unit = st.text_input(
301
- "Unit",
302
- value=strategy.goal.unit if strategy.goal else "%",
303
- help="Unit of measurement (e.g., %, pts, PLN)"
304
- )
305
-
306
- if st.button("Save Goal"):
307
- strategy.goal = Goal(
308
- text=goal_text,
309
- metric=metric_id,
310
- baseline=baseline,
311
- target=target,
312
- horizon=horizon,
313
- unit=unit
314
- )
315
- st.success("Goal saved!")
316
-
317
- st.markdown("---")
318
- st.subheader("Arena Definition")
319
-
320
- col1, col2 = st.columns(2)
321
-
322
- with col1:
323
- market = st.text_input(
324
- "Market",
325
- value=strategy.arena.market if strategy.arena else "",
326
- help="e.g., PL, EU, US"
327
- )
328
-
329
- category = st.text_input(
330
- "Category",
331
- value=strategy.arena.category if strategy.arena else "",
332
- help="e.g., pasta_premium, skincare"
333
- )
334
-
335
- target_audience = st.text_area(
336
- "Target Audience",
337
- value=strategy.arena.target_audience if strategy.arena else "",
338
- help="Describe your target audience (demographics, psychographics, behaviors)"
339
- )
340
-
341
- with col2:
342
- competitors_text = st.text_area(
343
- "Competitors (one per line)",
344
- value="\n".join(strategy.arena.competitors) if strategy.arena else "",
345
- help="List your main competitors"
346
- )
347
-
348
- if st.button("Save Arena"):
349
- competitors = [c.strip() for c in competitors_text.split("\n") if c.strip()]
350
- strategy.arena = Arena(
351
- market=market,
352
- category=category,
353
- competitors=competitors,
354
- target_audience=target_audience
355
- )
356
- st.success("Arena saved!")
357
-
358
-
359
- def render_insights_form(strategy):
360
- """Render insights input form"""
361
- st.subheader("Add Insight")
362
-
363
- col1, col2 = st.columns([2, 1])
364
-
365
- with col1:
366
- insight_id = st.text_input("Insight ID", help="e.g., i1, i2")
367
- insight_text = st.text_area("Insight Statement", help="What did you learn?")
368
-
369
- with col2:
370
- evidence_text = st.text_area("Evidence Sources (one per line)", help="e.g., FGI_2024, Survey_Q1")
371
-
372
- if st.button("Add Insight"):
373
- if insight_id and insight_text:
374
- evidence = [e.strip() for e in evidence_text.split("\n") if e.strip()]
375
- strategy.insights.append(Insight(
376
- id=insight_id,
377
- text=insight_text,
378
- evidence=evidence
379
- ))
380
- st.success(f"Insight {insight_id} added!")
381
- st.rerun()
382
-
383
- # Display existing insights
384
- if strategy.insights:
385
- st.markdown("---")
386
- st.subheader("Current Insights")
387
- for i, insight in enumerate(strategy.insights):
388
- with st.expander(f"{insight.id}: {insight.text[:50]}..."):
389
- st.write(f"**Full text:** {insight.text}")
390
- st.write(f"**Evidence:** {', '.join(insight.evidence)}")
391
- if st.button(f"Remove {insight.id}", key=f"remove_insight_{i}"):
392
- strategy.insights.pop(i)
393
- st.rerun()
394
-
395
-
396
- def render_hypotheses_form(strategy):
397
- """Render hypotheses input form"""
398
- st.subheader("Add Hypothesis")
399
-
400
- col1, col2 = st.columns([2, 1])
401
-
402
- with col1:
403
- hyp_id = st.text_input("Hypothesis ID", help="e.g., h1, h2")
404
- hyp_text = st.text_area("Hypothesis Statement", help="What do you believe will happen?")
405
-
406
- # Insight selector
407
- insight_ids = [i.id for i in strategy.insights]
408
- based_on = st.multiselect("Based on Insights", insight_ids)
409
-
410
- with col2:
411
- metric_id = st.text_input("Target Metric ID", help="Which metric will this affect?")
412
- expected_change = st.number_input("Expected Change", value=0.0, step=0.01, help="Expected change (e.g., 0.2 for +20%)")
413
-
414
- if st.button("Add Hypothesis"):
415
- if hyp_id and hyp_text:
416
- strategy.hypotheses.append(Hypothesis(
417
- id=hyp_id,
418
- text=hyp_text,
419
- based_on=based_on,
420
- metric=metric_id,
421
- expected_change=expected_change
422
- ))
423
- st.success(f"Hypothesis {hyp_id} added!")
424
- st.rerun()
425
-
426
- # Display existing hypotheses
427
- if strategy.hypotheses:
428
- st.markdown("---")
429
- st.subheader("Current Hypotheses")
430
- for i, hyp in enumerate(strategy.hypotheses):
431
- with st.expander(f"{hyp.id}: {hyp.text[:50]}..."):
432
- st.write(f"**Full text:** {hyp.text}")
433
- st.write(f"**Based on:** {', '.join(hyp.based_on)}")
434
- st.write(f"**Metric:** {hyp.metric} | **Expected change:** {hyp.expected_change}")
435
- if st.button(f"Remove {hyp.id}", key=f"remove_hyp_{i}"):
436
- strategy.hypotheses.pop(i)
437
- st.rerun()
438
-
439
-
440
- def render_moves_form(strategy):
441
- """Render moves input form"""
442
- st.subheader("Add Move")
443
-
444
- col1, col2 = st.columns([2, 1])
445
-
446
- with col1:
447
- move_id = st.text_input("Move ID", help="e.g., m1, m2")
448
- move_text = st.text_area("Move Description", help="What action will you take?")
449
-
450
- # Hypothesis selector
451
- hyp_ids = [h.id for h in strategy.hypotheses]
452
- linked_hyp = st.selectbox("Linked Hypothesis", [""] + hyp_ids)
453
-
454
- with col2:
455
- impact = st.slider("Impact", 0.0, 1.0, 0.5, 0.05, help="Expected impact (0-1)")
456
- fit = st.slider("Fit", 0.0, 1.0, 0.8, 0.05, help="Fit with strategy (0-1)")
457
- risk = st.slider("Risk", 0.0, 1.0, 0.3, 0.05, help="Risk level (0-1)")
458
- cost = st.number_input("Cost", value=100000.0, step=10000.0, help="Cost in currency")
459
-
460
- if st.button("Add Move"):
461
- if move_id and move_text and linked_hyp:
462
- strategy.moves.append(Move(
463
- id=move_id,
464
- text=move_text,
465
- linked_hypothesis=linked_hyp,
466
- impact=impact,
467
- fit=fit,
468
- risk=risk,
469
- cost=cost
470
- ))
471
- st.success(f"Move {move_id} added!")
472
- st.rerun()
473
-
474
- # Display existing moves
475
- if strategy.moves:
476
- st.markdown("---")
477
- st.subheader("Current Moves")
478
- for i, move in enumerate(strategy.moves):
479
- with st.expander(f"{move.id}: {move.text[:50]}..."):
480
- st.write(f"**Full text:** {move.text}")
481
- st.write(f"**Linked hypothesis:** {move.linked_hypothesis}")
482
- col1, col2, col3, col4 = st.columns(4)
483
- col1.metric("Impact", f"{move.impact:.2f}")
484
- col2.metric("Fit", f"{move.fit:.2f}")
485
- col3.metric("Risk", f"{move.risk:.2f}")
486
- col4.metric("Cost", f"{move.cost:,.0f}")
487
- if st.button(f"Remove {move.id}", key=f"remove_move_{i}"):
488
- strategy.moves.pop(i)
489
- st.rerun()
490
-
491
-
492
- def render_metrics_form(strategy):
493
- """Render metrics input form"""
494
- st.subheader("Add Supporting Metric")
495
- st.info("πŸ’‘ Main goal metric is defined in Goal & Arena tab. Here you add supporting/leading indicator metrics that help track progress.")
496
-
497
- col1, col2 = st.columns([2, 1])
498
-
499
- with col1:
500
- metric_id = st.text_input("Metric ID", help="e.g., CR_online, consideration")
501
- metric_text = st.text_input("Metric Name", help="Full name of the metric")
502
-
503
- with col2:
504
- baseline = st.number_input("Baseline", value=0.0, help="Current value")
505
- target = st.number_input("Target", value=0.0, help="Target value")
506
- unit = st.text_input("Unit", value="%", help="Unit of measurement")
507
-
508
- if st.button("Add Metric"):
509
- if metric_id and metric_text:
510
- strategy.metrics.append(Metric(
511
- id=metric_id,
512
- text=metric_text,
513
- baseline=baseline,
514
- target=target,
515
- unit=unit
516
- ))
517
- st.success(f"Metric {metric_id} added!")
518
- st.rerun()
519
-
520
- # Display existing metrics
521
- if strategy.metrics:
522
- st.markdown("---")
523
- st.subheader("Current Metrics")
524
- for i, metric in enumerate(strategy.metrics):
525
- with st.expander(f"{metric.id}: {metric.text}"):
526
- col1, col2, col3 = st.columns(3)
527
- col1.metric("Baseline", f"{metric.baseline}{metric.unit}")
528
- col2.metric("Target", f"{metric.target}{metric.unit}")
529
- col3.metric("Gap", f"{metric.target - metric.baseline}{metric.unit}")
530
- if st.button(f"Remove {metric.id}", key=f"remove_metric_{i}"):
531
- strategy.metrics.pop(i)
532
- st.rerun()
533
-
534
-
535
- def render_simulation():
536
- """Render simulation page"""
537
- st.markdown('<div class="main-header">Simulation</div>', unsafe_allow_html=True)
538
- st.markdown('<div class="sub-header">Run strategy simulation and view results</div>', unsafe_allow_html=True)
539
-
540
- strategy = st.session_state.strategy
541
-
542
- # Check if strategy is ready
543
- if not strategy.moves:
544
- st.warning("No moves defined. Go to Strategy Builder to add moves.")
545
- return
546
-
547
- # Run simulation button
548
- if st.button("▢️ Run Simulation", use_container_width=True):
549
- with st.spinner("Running simulation..."):
550
- results = SimulationEngine.simulate_strategy(strategy)
551
- st.session_state.simulation_results = results
552
- st.success("Simulation complete!")
553
- st.rerun()
554
-
555
- # Display results
556
- if st.session_state.simulation_results:
557
- results = st.session_state.simulation_results
558
-
559
- # Overall metrics
560
- st.subheader("Overall Results")
561
- col1, col2, col3 = st.columns(3)
562
-
563
- with col1:
564
- st.metric("Total Impact Score", f"{results['total_impact']:.4f}")
565
- with col2:
566
- st.metric("Number of Moves", len(results['move_scores']))
567
- with col3:
568
- avg_risk = sum(m['risk'] for m in results['move_scores']) / len(results['move_scores'])
569
- st.metric("Average Risk", f"{avg_risk:.2f}")
570
-
571
- # Moves ranking
572
- st.markdown("---")
573
- st.subheader("Moves Ranking")
574
-
575
- df = SimulationEngine.create_results_dataframe(results)
576
- st.dataframe(df, use_container_width=True)
577
-
578
- # Visualizations
579
- col1, col2 = st.columns(2)
580
-
581
- with col1:
582
- # Bar chart - scores
583
- fig_scores = go.Figure()
584
- fig_scores.add_trace(go.Bar(
585
- x=[m['id'] for m in results['move_scores']],
586
- y=[m['score'] for m in results['move_scores']],
587
- marker_color='#1e90ff',
588
- text=[f"{m['score']:.4f}" for m in results['move_scores']],
589
- textposition='auto'
590
- ))
591
- fig_scores.update_layout(
592
- title="Move Scores Ranking",
593
- xaxis_title="Move ID",
594
- yaxis_title="Score",
595
- height=400
596
- )
597
- st.plotly_chart(fig_scores, use_container_width=True)
598
-
599
- with col2:
600
- # Scatter plot - risk vs impact
601
- fig_scatter = go.Figure()
602
-
603
- for move in results['move_scores']:
604
- fig_scatter.add_trace(go.Scatter(
605
- x=[move['risk']],
606
- y=[move['impact']],
607
- mode='markers+text',
608
- name=move['id'],
609
- text=[move['id']],
610
- textposition="top center",
611
- marker=dict(
612
- size=move['score'] * 100,
613
- color=move['fit'],
614
- colorscale='Blues',
615
- showscale=True,
616
- colorbar=dict(
617
- title=dict(
618
- text="Fit",
619
- font=dict(size=14, color='white'),
620
- side='right'
621
- ),
622
- showticklabels=False,
623
- thickness=15,
624
- len=0.7
625
- )
626
- )
627
- ))
628
-
629
- fig_scatter.update_layout(
630
- title={
631
- 'text': "Risk vs Impact Analysis<br><sub>Bubble size = Score | Color intensity = Fit</sub>",
632
- 'x': 0.5,
633
- 'xanchor': 'center'
634
- },
635
- xaxis_title="Risk β†’",
636
- yaxis_title="Impact β†’",
637
- height=400,
638
- showlegend=False
639
- )
640
- st.plotly_chart(fig_scatter, use_container_width=True)
641
- st.caption("πŸ’‘ Larger bubbles = higher score | Darker blue = better fit with strategy")
642
-
643
- # Metric forecasts
644
- if results['metric_forecasts']:
645
- st.markdown("---")
646
- st.subheader("Metric Forecasts")
647
-
648
- for forecast in results['metric_forecasts']:
649
- # Highlight main goal metric
650
- icon = "🎯" if forecast.get('is_main') else "πŸ“Š"
651
- title_prefix = "MAIN GOAL: " if forecast.get('is_main') else ""
652
-
653
- with st.expander(f"{icon} {title_prefix}{forecast['text']}", expanded=True):
654
- # Metric summary in columns
655
- col1, col2, col3 = st.columns(3)
656
-
657
- with col1:
658
- st.markdown(f"**Baseline:** {forecast['baseline']}{forecast['unit']}")
659
- with col2:
660
- st.markdown(f"**Forecast:** <span style='color: #1e90ff; font-weight: bold;'>{forecast['forecast']}{forecast['unit']}</span>", unsafe_allow_html=True)
661
- with col3:
662
- gap_color = 'red' if forecast['gap_to_target'] > 0 else 'green'
663
- 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)
664
-
665
- st.markdown("") # spacing
666
-
667
- # Show linked hypotheses
668
- if forecast.get('linked_hypotheses'):
669
- st.markdown(f"**Linked Hypotheses:** {', '.join(forecast['linked_hypotheses'])}")
670
-
671
- # Show contributing moves breakdown
672
- if forecast.get('linked_moves'):
673
- st.markdown("**Contributing Moves:**")
674
- if len(forecast['linked_moves']) == 0:
675
- st.info("⚠️ No moves linked to this metric")
676
- else:
677
- for move in forecast['linked_moves']:
678
- contribution = move['score'] * forecast['baseline']
679
- st.markdown(f"- **{move['id']}**: {move['text'][:60]}... (score: {move['score']:.4f}, contribution: +{contribution:.2f}{forecast['unit']})")
680
- else:
681
- st.info("⚠️ No moves linked to this metric")
682
-
683
- # Recommendations
684
- if results['recommendations']:
685
- st.markdown("---")
686
- st.subheader("Recommendations")
687
- for rec in results['recommendations']:
688
- st.info(rec)
689
-
690
- # Strategy graph visualization
691
- st.markdown("---")
692
- st.subheader("Strategy Logic Flow")
693
- render_strategy_graph(strategy)
694
-
695
-
696
- def render_strategy_graph(strategy):
697
- """Render NetworkX graph of strategy logic"""
698
- G = nx.DiGraph()
699
-
700
- # Add nodes
701
- for insight in strategy.insights:
702
- G.add_node(insight.id, type='insight', label=insight.text[:30])
703
-
704
- for hyp in strategy.hypotheses:
705
- G.add_node(hyp.id, type='hypothesis', label=hyp.text[:30])
706
-
707
- for move in strategy.moves:
708
- G.add_node(move.id, type='move', label=move.text[:30])
709
-
710
- for metric in strategy.metrics:
711
- G.add_node(metric.id, type='metric', label=metric.text[:30])
712
-
713
- # Add edges
714
- for hyp in strategy.hypotheses:
715
- for insight_id in hyp.based_on:
716
- if G.has_node(insight_id):
717
- G.add_edge(insight_id, hyp.id)
718
- if G.has_node(hyp.metric):
719
- G.add_edge(hyp.id, hyp.metric)
720
-
721
- for move in strategy.moves:
722
- if G.has_node(move.linked_hypothesis):
723
- G.add_edge(move.linked_hypothesis, move.id)
724
-
725
- # Draw graph
726
- fig, ax = plt.subplots(figsize=(12, 8))
727
- pos = nx.spring_layout(G, k=2, iterations=50)
728
-
729
- # Color nodes by type
730
- node_colors = []
731
- for node in G.nodes():
732
- node_type = G.nodes[node].get('type', '')
733
- if node_type == 'insight':
734
- node_colors.append('#90EE90')
735
- elif node_type == 'hypothesis':
736
- node_colors.append('#FFD700')
737
- elif node_type == 'move':
738
- node_colors.append('#1e90ff')
739
- elif node_type == 'metric':
740
- node_colors.append('#FF6B6B')
741
- else:
742
- node_colors.append('#CCCCCC')
743
-
744
- nx.draw(G, pos, ax=ax, node_color=node_colors, node_size=2000,
745
- with_labels=True, font_size=8, font_weight='bold',
746
- arrows=True, arrowsize=20, edge_color='#999999', width=2)
747
-
748
- ax.set_title("Strategy Logic Flow\n(Green=Insight, Yellow=Hypothesis, Blue=Move, Red=Metric)",
749
- fontsize=12, fontweight='bold')
750
-
751
- st.pyplot(fig)
752
-
753
-
754
- def render_ai_evaluator():
755
- """Render AI evaluator page"""
756
- st.markdown('<div class="main-header">AI Evaluator</div>', unsafe_allow_html=True)
757
- st.markdown('<div class="sub-header">Get brutally honest AI audit of your strategy</div>', unsafe_allow_html=True)
758
-
759
- strategy = st.session_state.strategy
760
-
761
- # Check API key
762
- if not st.session_state.openai_api_key:
763
- st.warning("Please configure your OpenAI API key in the sidebar.")
764
- return
765
-
766
- # Check if strategy is ready
767
- if not strategy.goal or not strategy.moves:
768
- st.warning("Strategy is incomplete. Please define at least a goal and some moves.")
769
- return
770
-
771
- # Display current strategy summary
772
- st.subheader("Strategy Summary")
773
- st.write(f"**Goal:** {strategy.goal.text}")
774
- st.write(f"**Insights:** {len(strategy.insights)}")
775
- st.write(f"**Hypotheses:** {len(strategy.hypotheses)}")
776
- st.write(f"**Moves:** {len(strategy.moves)}")
777
-
778
- st.markdown("---")
779
-
780
- # Ask AI button
781
- st.info("⚠️ This AI audit will be brutally honest - expect specific criticism, red flags, and concrete fixes (not polite suggestions).")
782
- if st.button("πŸ€– Run Strategy Audit", use_container_width=True):
783
- with st.spinner("AI is auditing your strategy..."):
784
- feedback = get_ai_evaluation(strategy)
785
- st.session_state.ai_feedback = feedback
786
- st.rerun()
787
-
788
- # Display AI feedback
789
- if st.session_state.ai_feedback:
790
- st.markdown("---")
791
- st.subheader("πŸ” Strategy Audit Report")
792
- st.markdown(st.session_state.ai_feedback)
793
-
794
-
795
- def get_ai_evaluation(strategy: Strategy) -> str:
796
- """Call OpenAI API to evaluate strategy"""
797
- try:
798
- client = OpenAI(api_key=st.session_state.openai_api_key)
799
-
800
- # Prepare strategy data
801
- strategy_json = json.dumps(strategy.to_dict(), indent=2, ensure_ascii=False)
802
-
803
- # Create prompt
804
- prompt = f"""You are a ruthlessly analytical strategy evaluator. Your job is to find weaknesses, gaps, and risks - not to be polite.
805
-
806
- IMPORTANT: Detect the language of the strategy below and respond in THE SAME LANGUAGE.
807
- If the strategy is in Polish, respond in Polish. If in English, respond in English.
808
-
809
- Analyze this strategy with BRUTAL honesty:
810
-
811
- Strategy:
812
- ```json
813
- {strategy_json}
814
- ```
815
-
816
- Your evaluation MUST include:
817
-
818
- ## 1. LOGICAL FLOW AUDIT (Score: X/10)
819
- - Map exact connections: Which insights feed which hypotheses? Which hypotheses feed which moves?
820
- - RED FLAGS: List any broken links (e.g., "Move m3 links to h2, but h2 doesn't target the main goal metric")
821
- - ORPHANS: Are there insights/hypotheses not connected to any moves? List them by ID.
822
-
823
- ## 2. HYPOTHESIS QUALITY CHECK
824
- For EACH hypothesis (h1, h2, h3, h4...):
825
- - Is expected_change realistic? (Compare to industry benchmarks if known)
826
- - Is the metric actually measurable? How would you track it?
827
- - RED FLAG if hypothesis is vague or unmeasurable
828
-
829
- ## 3. MOVE PORTFOLIO ANALYSIS
830
- - Cost concentration: What % of budget goes to top move? (Flag if >40%)
831
- - Risk profile: How many high-risk moves (risk >0.6)?
832
- - MISSING MOVES: What obvious actions are NOT here? (e.g., "No paid search strategy despite digital focus")
833
- - Prioritization: Using score formula, which moves should be CUT? Be specific with IDs.
834
-
835
- ## 4. GAP ANALYSIS - What's MISSING?
836
- - Competitive response: How will competitors react? No moves addressing this?
837
- - Measurement plan: How will you track these metrics in reality?
838
- - Budget realism: Total cost vs. expected revenue/impact - does math work?
839
- - Timeline: Are moves sequenced or all at once? Flag if unclear.
840
-
841
- ## 5. TOP 3 FATAL FLAWS
842
- List the 3 biggest risks that could KILL this strategy. Be specific:
843
- - "Move m6 costs 150k but has lowest score (X.XX) - CUT IT"
844
- - "No B2B distribution strategy despite targeting gyms/studios"
845
- - "Hypothesis h2 assumes 30% trial rate but category average is 8%"
846
-
847
- ## 6. ACTIONABLE FIXES (Prioritized)
848
- πŸ”΄ CRITICAL (do NOW):
849
- - Specific change with exact ID reference (e.g., "Change h3 expected_change from 0.45 to 0.25")
850
-
851
- 🟑 IMPORTANT (before launch):
852
- - Concrete additions (e.g., "Add move m7: Partnership with Ε»abka for distribution")
853
-
854
- 🟒 NICE TO HAVE:
855
- - Optimizations
856
-
857
- NO GENERIC ADVICE like "consider research" or "might want to test". Give SPECIFIC actions with IDs, numbers, and rationale.
858
-
859
- Remember: RESPOND IN THE SAME LANGUAGE AS THE STRATEGY. Be direct, numerical, and reference specific IDs.
860
- """
861
-
862
- # Call API
863
- response = client.chat.completions.create(
864
- model="gpt-4",
865
- messages=[
866
- {"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."},
867
- {"role": "user", "content": prompt}
868
- ],
869
- temperature=0.5,
870
- max_tokens=2500
871
- )
872
-
873
- return response.choices[0].message.content
874
-
875
- except Exception as e:
876
- return f"Error calling OpenAI API: {str(e)}\n\nPlease check your API key and try again."
877
-
878
-
879
- def render_report():
880
- """Render report generation page"""
881
- st.markdown('<div class="main-header">Strategy Report</div>', unsafe_allow_html=True)
882
- st.markdown('<div class="sub-header">View and export your complete strategy</div>', unsafe_allow_html=True)
883
-
884
- strategy = st.session_state.strategy
885
-
886
- if not strategy.goal:
887
- st.warning("No strategy to report. Go to Strategy Builder first.")
888
- return
889
-
890
- # Generate report
891
- report = generate_report(strategy)
892
-
893
- # Display report
894
- st.markdown(report)
895
-
896
- # Export buttons
897
- st.markdown("---")
898
- col1, col2 = st.columns(2)
899
-
900
- with col1:
901
- st.download_button(
902
- label="πŸ“„ Download as Markdown",
903
- data=report,
904
- file_name=f"strategy_report_{datetime.now().strftime('%Y%m%d_%H%M%S')}.md",
905
- mime="text/markdown",
906
- use_container_width=True
907
- )
908
-
909
- with col2:
910
- json_data = json.dumps(strategy.to_dict(), indent=2, ensure_ascii=False)
911
- st.download_button(
912
- label="πŸ“₯ Download as JSON",
913
- data=json_data,
914
- file_name=f"strategy_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json",
915
- mime="application/json",
916
- use_container_width=True
917
- )
918
-
919
-
920
- def generate_report(strategy: Strategy) -> str:
921
- """Generate markdown report from strategy"""
922
- report = f"""# Strategic Sandbox Report
923
- *Generated on {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}*
924
-
925
- ---
926
-
927
- ## 🎯 Goal
928
-
929
- **{strategy.goal.text if strategy.goal else 'Not defined'}**
930
-
931
- """
932
-
933
- if strategy.goal:
934
- report += f"""
935
- - **Metric:** {strategy.goal.metric}
936
- - **Baseline:** {strategy.goal.baseline}{strategy.goal.unit}
937
- - **Target:** {strategy.goal.target}{strategy.goal.unit}
938
- - **Horizon:** {strategy.goal.horizon}
939
- - **Unit:** {strategy.goal.unit}
940
- """
941
-
942
- if strategy.arena:
943
- report += f"""
944
- ---
945
-
946
- ## πŸ—ΊοΈ Arena
947
-
948
- - **Market:** {strategy.arena.market}
949
- - **Category:** {strategy.arena.category}
950
- - **Target Audience:** {strategy.arena.target_audience}
951
- - **Competitors:** {', '.join(strategy.arena.competitors)}
952
- """
953
-
954
- if strategy.insights:
955
- report += "\n---\n\n## πŸ’‘ Insights\n\n"
956
- for insight in strategy.insights:
957
- report += f"### {insight.id}\n{insight.text}\n\n*Evidence: {', '.join(insight.evidence)}*\n\n"
958
-
959
- if strategy.hypotheses:
960
- report += "---\n\n## πŸ”¬ Hypotheses\n\n"
961
- for hyp in strategy.hypotheses:
962
- report += f"### {hyp.id}\n{hyp.text}\n\n"
963
- report += f"- Based on: {', '.join(hyp.based_on)}\n"
964
- report += f"- Metric: {hyp.metric}\n"
965
- report += f"- Expected change: {hyp.expected_change}\n\n"
966
-
967
- if strategy.moves:
968
- report += "---\n\n## 🎬 Moves\n\n"
969
- for move in strategy.moves:
970
- report += f"### {move.id}: {move.text}\n\n"
971
- report += f"- Linked hypothesis: {move.linked_hypothesis}\n"
972
- report += f"- Impact: {move.impact} | Fit: {move.fit} | Risk: {move.risk}\n"
973
- report += f"- Cost: {move.cost:,.0f}\n\n"
974
-
975
- if strategy.metrics:
976
- report += "---\n\n## πŸ“Š Supporting Metrics\n\n"
977
- for metric in strategy.metrics:
978
- report += f"### {metric.id}: {metric.text}\n"
979
- report += f"- Baseline: {metric.baseline}{metric.unit}\n"
980
- report += f"- Target: {metric.target}{metric.unit}\n\n"
981
-
982
- # Add simulation results if available
983
- if st.session_state.simulation_results:
984
- results = st.session_state.simulation_results
985
- report += "---\n\n## πŸ“ˆ Simulation Results\n\n"
986
- report += f"**Total Impact Score:** {results['total_impact']:.4f}\n\n"
987
-
988
- report += "### Move Rankings\n\n"
989
- for move in results['move_scores']:
990
- report += f"- **{move['id']}**: Score {move['score']:.4f} (Impact: {move['impact']}, Risk: {move['risk']}, Cost: {move['cost']:,.0f})\n"
991
-
992
- # Add AI feedback if available
993
- if st.session_state.ai_feedback:
994
- report += "\n---\n\n## πŸ€– AI Evaluation\n\n"
995
- report += st.session_state.ai_feedback
996
-
997
- report += "\n\n---\n\n*Generated with Strategic Sandbox v1.0*\n"
998
- report += "*πŸ€– Generated with [Claude Code](https://claude.com/claude-code)*\n"
999
-
1000
- return report
1001
-
1002
-
1003
- def save_strategy():
1004
- """Save strategy to JSON file"""
1005
- timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
1006
- filename = f"data/strategy_{timestamp}.json"
1007
- st.session_state.strategy.to_json(filename)
1008
- st.success(f"Strategy saved to {filename}")
1009
-
1010
-
1011
- def export_strategy_json():
1012
- """Export strategy as downloadable JSON"""
1013
- json_data = json.dumps(st.session_state.strategy.to_dict(), indent=2, ensure_ascii=False)
1014
- st.download_button(
1015
- label="Download JSON",
1016
- data=json_data,
1017
- file_name=f"strategy_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json",
1018
- mime="application/json"
1019
- )
1020
-
1021
-
1022
- # Main app
1023
- def main():
1024
- init_session_state()
1025
- render_sidebar()
1026
-
1027
- # Route to appropriate page
1028
- page = st.session_state.current_page
1029
-
1030
- if page == "Dashboard":
1031
- render_dashboard()
1032
- elif page == "Strategy Builder":
1033
- render_strategy_builder()
1034
- elif page == "Simulation":
1035
- render_simulation()
1036
- elif page == "AI Evaluator":
1037
- render_ai_evaluator()
1038
- elif page == "Report":
1039
- render_report()
1040
-
1041
-
1042
- if __name__ == "__main__":
1043
- main()
 
1
+ """
2
+ Strategic Sandbox v1.0
3
+ Interactive strategy simulation and evaluation tool
4
+ """
5
+
6
+ import streamlit as st
7
+ import json
8
+ import os
9
+ from pathlib import Path
10
+ import pandas as pd
11
+ import plotly.graph_objects as go
12
+ import plotly.express as px
13
+ import networkx as nx
14
+ import matplotlib.pyplot as plt
15
+ from openai import OpenAI
16
+ from datetime import datetime
17
+
18
+ from strategy_core import (
19
+ Strategy, Goal, Arena, Insight, Hypothesis, Move, Metric,
20
+ SimulationEngine
21
+ )
22
+
23
+
24
+ # Page configuration
25
+ st.set_page_config(
26
+ page_title="Strategic Sandbox",
27
+ page_icon="🎯",
28
+ layout="wide",
29
+ initial_sidebar_state="expanded"
30
+ )
31
+
32
+ # Custom CSS for Heuristica aesthetic
33
+ st.markdown("""
34
+ <style>
35
+ .main-header {
36
+ font-size: 2.5rem;
37
+ font-weight: 600;
38
+ color: #1e90ff;
39
+ margin-bottom: 0.5rem;
40
+ }
41
+ .sub-header {
42
+ font-size: 1.2rem;
43
+ color: #666;
44
+ margin-bottom: 2rem;
45
+ }
46
+ .metric-card {
47
+ background-color: #f0f8ff;
48
+ padding: 1rem;
49
+ border-radius: 8px;
50
+ border-left: 4px solid #1e90ff;
51
+ }
52
+ .stButton>button {
53
+ background-color: #1e90ff;
54
+ color: white;
55
+ border-radius: 6px;
56
+ border: none;
57
+ padding: 0.5rem 1.5rem;
58
+ font-weight: 500;
59
+ }
60
+ .stButton>button:hover {
61
+ background-color: #187bcd;
62
+ }
63
+ </style>
64
+ """, unsafe_allow_html=True)
65
+
66
+
67
+ # Initialize session state
68
+ def init_session_state():
69
+ """Initialize all session state variables"""
70
+ if 'strategy' not in st.session_state:
71
+ st.session_state.strategy = Strategy()
72
+ if 'simulation_results' not in st.session_state:
73
+ st.session_state.simulation_results = None
74
+ if 'ai_feedback' not in st.session_state:
75
+ st.session_state.ai_feedback = None
76
+ if 'openai_api_key' not in st.session_state:
77
+ st.session_state.openai_api_key = ""
78
+ if 'current_page' not in st.session_state:
79
+ st.session_state.current_page = "Dashboard"
80
+
81
+
82
+ def save_api_key():
83
+ """Save API key to local file for persistence"""
84
+ key_file = Path("data/.api_key")
85
+ key_file.parent.mkdir(exist_ok=True)
86
+ with open(key_file, 'w') as f:
87
+ f.write(st.session_state.openai_api_key)
88
+
89
+
90
+ def load_api_key():
91
+ """Load API key from local file"""
92
+ key_file = Path("data/.api_key")
93
+ if key_file.exists():
94
+ with open(key_file, 'r') as f:
95
+ return f.read().strip()
96
+ return ""
97
+
98
+
99
+ def render_sidebar():
100
+ """Render sidebar with navigation and API key input"""
101
+ with st.sidebar:
102
+ st.markdown('<div class="main-header">🎯 Strategic Sandbox</div>', unsafe_allow_html=True)
103
+ st.markdown("**v1.0** - Strategy Simulation Tool")
104
+ st.markdown("---")
105
+
106
+ # API Key input
107
+ st.subheader("OpenAI Configuration")
108
+
109
+ # Load saved key if available
110
+ if not st.session_state.openai_api_key:
111
+ st.session_state.openai_api_key = load_api_key()
112
+
113
+ api_key = st.text_input(
114
+ "API Key",
115
+ value=st.session_state.openai_api_key,
116
+ type="password",
117
+ help="Your OpenAI API key will be saved locally"
118
+ )
119
+
120
+ if api_key != st.session_state.openai_api_key:
121
+ st.session_state.openai_api_key = api_key
122
+ save_api_key()
123
+ if api_key:
124
+ st.success("API Key saved!")
125
+
126
+ st.markdown("---")
127
+
128
+ # Navigation
129
+ st.subheader("Navigation")
130
+ pages = ["Dashboard", "Strategy Builder", "Simulation", "AI Evaluator", "Report"]
131
+
132
+ for page in pages:
133
+ if st.button(page, key=f"nav_{page}", use_container_width=True):
134
+ st.session_state.current_page = page
135
+ st.rerun()
136
+
137
+ st.markdown("---")
138
+
139
+ # Quick actions
140
+ st.subheader("Quick Actions")
141
+
142
+ col1, col2 = st.columns(2)
143
+ with col1:
144
+ if st.button("πŸ“ Load Example", use_container_width=True):
145
+ load_example_strategy()
146
+ st.success("Example loaded!")
147
+ st.rerun()
148
+
149
+ with col2:
150
+ if st.button("πŸ”„ Reset", use_container_width=True):
151
+ st.session_state.strategy = Strategy()
152
+ st.session_state.simulation_results = None
153
+ st.session_state.ai_feedback = None
154
+ st.success("Strategy reset!")
155
+ st.rerun()
156
+
157
+
158
+ def load_example_strategy():
159
+ """Load example strategy from JSON"""
160
+ example_path = Path("data/example.json")
161
+ if example_path.exists():
162
+ st.session_state.strategy = Strategy.from_json(str(example_path))
163
+
164
+
165
+ def render_dashboard():
166
+ """Render dashboard page"""
167
+ st.markdown('<div class="main-header">Dashboard</div>', unsafe_allow_html=True)
168
+ st.markdown('<div class="sub-header">Strategic overview and quick start</div>', unsafe_allow_html=True)
169
+
170
+ # Quick stats
171
+ col1, col2, col3, col4 = st.columns(4)
172
+
173
+ strategy = st.session_state.strategy
174
+
175
+ with col1:
176
+ st.metric("Insights", len(strategy.insights))
177
+ with col2:
178
+ st.metric("Hypotheses", len(strategy.hypotheses))
179
+ with col3:
180
+ st.metric("Moves", len(strategy.moves))
181
+ with col4:
182
+ st.metric("Metrics", len(strategy.metrics))
183
+
184
+ st.markdown("---")
185
+
186
+ # Strategy overview
187
+ if strategy.goal:
188
+ st.subheader("Current Goal")
189
+ st.info(f"**{strategy.goal.text}**\n\nTarget: {strategy.goal.target}{strategy.metrics[0].unit if strategy.metrics else ''} by {strategy.goal.horizon}")
190
+ else:
191
+ st.warning("No strategy defined yet. Go to **Strategy Builder** to start.")
192
+
193
+ # Recent simulation results
194
+ if st.session_state.simulation_results:
195
+ st.subheader("Latest Simulation Results")
196
+ results = st.session_state.simulation_results
197
+
198
+ col1, col2 = st.columns(2)
199
+ with col1:
200
+ st.metric("Total Impact Score", f"{results['total_impact']:.4f}")
201
+ with col2:
202
+ if results['move_scores']:
203
+ top_move = results['move_scores'][0]
204
+ st.metric("Top Move", top_move['text'], f"Score: {top_move['score']:.4f}")
205
+
206
+ # Quick start guide
207
+ st.markdown("---")
208
+ st.subheader("Quick Start Guide")
209
+
210
+ st.markdown("""
211
+ 1. **Strategy Builder**: Define your goal, insights, hypotheses, and moves
212
+ 2. **Simulation**: Run scoring simulation to evaluate moves
213
+ 3. **AI Evaluator**: Get AI feedback on strategy coherence
214
+ 4. **Report**: Generate and export your strategy report
215
+ """)
216
+
217
+
218
+ def render_strategy_builder():
219
+ """Render strategy builder page with forms"""
220
+ st.markdown('<div class="main-header">Strategy Builder</div>', unsafe_allow_html=True)
221
+ st.markdown('<div class="sub-header">Define your strategy components</div>', unsafe_allow_html=True)
222
+
223
+ strategy = st.session_state.strategy
224
+
225
+ # Create tabs for different sections
226
+ tab1, tab2, tab3, tab4, tab5 = st.tabs([
227
+ "🎯 Goal & Arena",
228
+ "πŸ’‘ Insights",
229
+ "πŸ”¬ Hypotheses",
230
+ "🎬 Moves",
231
+ "πŸ“Š Supporting Metrics"
232
+ ])
233
+
234
+ with tab1:
235
+ render_goal_arena_form(strategy)
236
+
237
+ with tab2:
238
+ render_insights_form(strategy)
239
+
240
+ with tab3:
241
+ render_hypotheses_form(strategy)
242
+
243
+ with tab4:
244
+ render_moves_form(strategy)
245
+
246
+ with tab5:
247
+ render_metrics_form(strategy)
248
+
249
+ # Save/Export buttons
250
+ st.markdown("---")
251
+ col1, col2, col3 = st.columns([1, 1, 2])
252
+
253
+ with col1:
254
+ if st.button("πŸ’Ύ Save Strategy", use_container_width=True):
255
+ save_strategy()
256
+
257
+ with col2:
258
+ if st.button("πŸ“₯ Export JSON", use_container_width=True):
259
+ export_strategy_json()
260
+
261
+
262
+ def render_goal_arena_form(strategy):
263
+ """Render goal and arena input form"""
264
+ st.subheader("Goal Definition")
265
+
266
+ col1, col2 = st.columns(2)
267
+
268
+ with col1:
269
+ goal_text = st.text_area(
270
+ "Goal Statement",
271
+ value=strategy.goal.text if strategy.goal else "",
272
+ help="What do you want to achieve?"
273
+ )
274
+
275
+ metric_id = st.text_input(
276
+ "Metric ID",
277
+ value=strategy.goal.metric if strategy.goal else "",
278
+ help="e.g., share_premium, CR_online"
279
+ )
280
+
281
+ horizon = st.text_input(
282
+ "Time Horizon",
283
+ value=strategy.goal.horizon if strategy.goal else "2026",
284
+ help="When do you want to achieve this?"
285
+ )
286
+
287
+ with col2:
288
+ baseline = st.number_input(
289
+ "Baseline Value",
290
+ value=float(strategy.goal.baseline) if strategy.goal else 0.0,
291
+ help="Current value"
292
+ )
293
+
294
+ target = st.number_input(
295
+ "Target Value",
296
+ value=float(strategy.goal.target) if strategy.goal else 0.0,
297
+ help="Desired value"
298
+ )
299
+
300
+ unit = st.text_input(
301
+ "Unit",
302
+ value=strategy.goal.unit if strategy.goal else "%",
303
+ help="Unit of measurement (e.g., %, pts, PLN)"
304
+ )
305
+
306
+ if st.button("Save Goal"):
307
+ strategy.goal = Goal(
308
+ text=goal_text,
309
+ metric=metric_id,
310
+ baseline=baseline,
311
+ target=target,
312
+ horizon=horizon,
313
+ unit=unit
314
+ )
315
+ st.success("Goal saved!")
316
+
317
+ st.markdown("---")
318
+ st.subheader("Arena Definition")
319
+
320
+ col1, col2 = st.columns(2)
321
+
322
+ with col1:
323
+ market = st.text_input(
324
+ "Market",
325
+ value=strategy.arena.market if strategy.arena else "",
326
+ help="e.g., PL, EU, US"
327
+ )
328
+
329
+ category = st.text_input(
330
+ "Category",
331
+ value=strategy.arena.category if strategy.arena else "",
332
+ help="e.g., pasta_premium, skincare"
333
+ )
334
+
335
+ target_audience = st.text_area(
336
+ "Target Audience",
337
+ value=strategy.arena.target_audience if strategy.arena else "",
338
+ help="Describe your target audience (demographics, psychographics, behaviors)"
339
+ )
340
+
341
+ with col2:
342
+ competitors_text = st.text_area(
343
+ "Competitors (one per line)",
344
+ value="\n".join(strategy.arena.competitors) if strategy.arena else "",
345
+ help="List your main competitors"
346
+ )
347
+
348
+ if st.button("Save Arena"):
349
+ competitors = [c.strip() for c in competitors_text.split("\n") if c.strip()]
350
+ strategy.arena = Arena(
351
+ market=market,
352
+ category=category,
353
+ competitors=competitors,
354
+ target_audience=target_audience
355
+ )
356
+ st.success("Arena saved!")
357
+
358
+
359
+ def render_insights_form(strategy):
360
+ """Render insights input form"""
361
+ st.subheader("Add Insight")
362
+
363
+ col1, col2 = st.columns([2, 1])
364
+
365
+ with col1:
366
+ insight_id = st.text_input("Insight ID", help="e.g., i1, i2")
367
+ insight_text = st.text_area("Insight Statement", help="What did you learn?")
368
+
369
+ with col2:
370
+ evidence_text = st.text_area("Evidence Sources (one per line)", help="e.g., FGI_2024, Survey_Q1")
371
+
372
+ if st.button("Add Insight"):
373
+ if insight_id and insight_text:
374
+ evidence = [e.strip() for e in evidence_text.split("\n") if e.strip()]
375
+ strategy.insights.append(Insight(
376
+ id=insight_id,
377
+ text=insight_text,
378
+ evidence=evidence
379
+ ))
380
+ st.success(f"Insight {insight_id} added!")
381
+ st.rerun()
382
+
383
+ # Display existing insights
384
+ if strategy.insights:
385
+ st.markdown("---")
386
+ st.subheader("Current Insights")
387
+ for i, insight in enumerate(strategy.insights):
388
+ with st.expander(f"{insight.id}: {insight.text[:50]}..."):
389
+ st.write(f"**Full text:** {insight.text}")
390
+ st.write(f"**Evidence:** {', '.join(insight.evidence)}")
391
+ if st.button(f"Remove {insight.id}", key=f"remove_insight_{i}"):
392
+ strategy.insights.pop(i)
393
+ st.rerun()
394
+
395
+
396
+ def render_hypotheses_form(strategy):
397
+ """Render hypotheses input form"""
398
+ st.subheader("Add Hypothesis")
399
+
400
+ col1, col2 = st.columns([2, 1])
401
+
402
+ with col1:
403
+ hyp_id = st.text_input("Hypothesis ID", help="e.g., h1, h2")
404
+ hyp_text = st.text_area("Hypothesis Statement", help="What do you believe will happen?")
405
+
406
+ # Insight selector
407
+ insight_ids = [i.id for i in strategy.insights]
408
+ based_on = st.multiselect("Based on Insights", insight_ids)
409
+
410
+ with col2:
411
+ metric_id = st.text_input("Target Metric ID", help="Which metric will this affect?")
412
+ expected_change = st.number_input("Expected Change", value=0.0, step=0.01, help="Expected change (e.g., 0.2 for +20%)")
413
+
414
+ if st.button("Add Hypothesis"):
415
+ if hyp_id and hyp_text:
416
+ strategy.hypotheses.append(Hypothesis(
417
+ id=hyp_id,
418
+ text=hyp_text,
419
+ based_on=based_on,
420
+ metric=metric_id,
421
+ expected_change=expected_change
422
+ ))
423
+ st.success(f"Hypothesis {hyp_id} added!")
424
+ st.rerun()
425
+
426
+ # Display existing hypotheses
427
+ if strategy.hypotheses:
428
+ st.markdown("---")
429
+ st.subheader("Current Hypotheses")
430
+ for i, hyp in enumerate(strategy.hypotheses):
431
+ with st.expander(f"{hyp.id}: {hyp.text[:50]}..."):
432
+ st.write(f"**Full text:** {hyp.text}")
433
+ st.write(f"**Based on:** {', '.join(hyp.based_on)}")
434
+ st.write(f"**Metric:** {hyp.metric} | **Expected change:** {hyp.expected_change}")
435
+ if st.button(f"Remove {hyp.id}", key=f"remove_hyp_{i}"):
436
+ strategy.hypotheses.pop(i)
437
+ st.rerun()
438
+
439
+
440
+ def render_moves_form(strategy):
441
+ """Render moves input form"""
442
+ st.subheader("Add Move")
443
+
444
+ col1, col2 = st.columns([2, 1])
445
+
446
+ with col1:
447
+ move_id = st.text_input("Move ID", help="e.g., m1, m2")
448
+ move_text = st.text_area("Move Description", help="What action will you take?")
449
+
450
+ # Hypothesis selector
451
+ hyp_ids = [h.id for h in strategy.hypotheses]
452
+ linked_hyp = st.selectbox("Linked Hypothesis", [""] + hyp_ids)
453
+
454
+ with col2:
455
+ impact = st.slider("Impact", 0.0, 1.0, 0.5, 0.05, help="Expected impact (0-1)")
456
+ fit = st.slider("Fit", 0.0, 1.0, 0.8, 0.05, help="Fit with strategy (0-1)")
457
+ risk = st.slider("Risk", 0.0, 1.0, 0.3, 0.05, help="Risk level (0-1)")
458
+ cost = st.number_input("Cost", value=100000.0, step=10000.0, help="Cost in currency")
459
+
460
+ if st.button("Add Move"):
461
+ if move_id and move_text and linked_hyp:
462
+ strategy.moves.append(Move(
463
+ id=move_id,
464
+ text=move_text,
465
+ linked_hypothesis=linked_hyp,
466
+ impact=impact,
467
+ fit=fit,
468
+ risk=risk,
469
+ cost=cost
470
+ ))
471
+ st.success(f"Move {move_id} added!")
472
+ st.rerun()
473
+
474
+ # Display existing moves
475
+ if strategy.moves:
476
+ st.markdown("---")
477
+ st.subheader("Current Moves")
478
+ for i, move in enumerate(strategy.moves):
479
+ with st.expander(f"{move.id}: {move.text[:50]}..."):
480
+ st.write(f"**Full text:** {move.text}")
481
+ st.write(f"**Linked hypothesis:** {move.linked_hypothesis}")
482
+ col1, col2, col3, col4 = st.columns(4)
483
+ col1.metric("Impact", f"{move.impact:.2f}")
484
+ col2.metric("Fit", f"{move.fit:.2f}")
485
+ col3.metric("Risk", f"{move.risk:.2f}")
486
+ col4.metric("Cost", f"{move.cost:,.0f}")
487
+ if st.button(f"Remove {move.id}", key=f"remove_move_{i}"):
488
+ strategy.moves.pop(i)
489
+ st.rerun()
490
+
491
+
492
+ def render_metrics_form(strategy):
493
+ """Render metrics input form"""
494
+ st.subheader("Add Supporting Metric")
495
+ st.info("πŸ’‘ Main goal metric is defined in Goal & Arena tab. Here you add supporting/leading indicator metrics that help track progress.")
496
+
497
+ col1, col2 = st.columns([2, 1])
498
+
499
+ with col1:
500
+ metric_id = st.text_input("Metric ID", help="e.g., CR_online, consideration")
501
+ metric_text = st.text_input("Metric Name", help="Full name of the metric")
502
+
503
+ with col2:
504
+ baseline = st.number_input("Baseline", value=0.0, help="Current value")
505
+ target = st.number_input("Target", value=0.0, help="Target value")
506
+ unit = st.text_input("Unit", value="%", help="Unit of measurement")
507
+
508
+ if st.button("Add Metric"):
509
+ if metric_id and metric_text:
510
+ strategy.metrics.append(Metric(
511
+ id=metric_id,
512
+ text=metric_text,
513
+ baseline=baseline,
514
+ target=target,
515
+ unit=unit
516
+ ))
517
+ st.success(f"Metric {metric_id} added!")
518
+ st.rerun()
519
+
520
+ # Display existing metrics
521
+ if strategy.metrics:
522
+ st.markdown("---")
523
+ st.subheader("Current Metrics")
524
+ for i, metric in enumerate(strategy.metrics):
525
+ with st.expander(f"{metric.id}: {metric.text}"):
526
+ col1, col2, col3 = st.columns(3)
527
+ col1.metric("Baseline", f"{metric.baseline}{metric.unit}")
528
+ col2.metric("Target", f"{metric.target}{metric.unit}")
529
+ col3.metric("Gap", f"{metric.target - metric.baseline}{metric.unit}")
530
+ if st.button(f"Remove {metric.id}", key=f"remove_metric_{i}"):
531
+ strategy.metrics.pop(i)
532
+ st.rerun()
533
+
534
+
535
+ def render_simulation():
536
+ """Render simulation page"""
537
+ st.markdown('<div class="main-header">Simulation</div>', unsafe_allow_html=True)
538
+ st.markdown('<div class="sub-header">Run strategy simulation and view results</div>', unsafe_allow_html=True)
539
+
540
+ strategy = st.session_state.strategy
541
+
542
+ # Check if strategy is ready
543
+ if not strategy.moves:
544
+ st.warning("No moves defined. Go to Strategy Builder to add moves.")
545
+ return
546
+
547
+ # Run simulation button
548
+ if st.button("▢️ Run Simulation", use_container_width=True):
549
+ with st.spinner("Running simulation..."):
550
+ results = SimulationEngine.simulate_strategy(strategy)
551
+ st.session_state.simulation_results = results
552
+ st.success("Simulation complete!")
553
+
554
+ # Display results
555
+ if st.session_state.simulation_results:
556
+ results = st.session_state.simulation_results
557
+
558
+ # Overall metrics
559
+ st.subheader("Overall Results")
560
+ col1, col2, col3 = st.columns(3)
561
+
562
+ with col1:
563
+ st.metric("Total Impact Score", f"{results['total_impact']:.4f}")
564
+ with col2:
565
+ st.metric("Number of Moves", len(results['move_scores']))
566
+ with col3:
567
+ avg_risk = sum(m['risk'] for m in results['move_scores']) / len(results['move_scores'])
568
+ st.metric("Average Risk", f"{avg_risk:.2f}")
569
+
570
+ # Moves ranking
571
+ st.markdown("---")
572
+ st.subheader("Moves Ranking")
573
+
574
+ df = SimulationEngine.create_results_dataframe(results)
575
+ st.dataframe(df, use_container_width=True)
576
+
577
+ # Visualizations
578
+ col1, col2 = st.columns(2)
579
+
580
+ with col1:
581
+ # Bar chart - scores
582
+ fig_scores = go.Figure()
583
+ fig_scores.add_trace(go.Bar(
584
+ x=[m['id'] for m in results['move_scores']],
585
+ y=[m['score'] for m in results['move_scores']],
586
+ marker_color='#1e90ff',
587
+ text=[f"{m['score']:.4f}" for m in results['move_scores']],
588
+ textposition='auto'
589
+ ))
590
+ fig_scores.update_layout(
591
+ title="Move Scores Ranking",
592
+ xaxis_title="Move ID",
593
+ yaxis_title="Score",
594
+ height=400
595
+ )
596
+ st.plotly_chart(fig_scores, use_container_width=True)
597
+
598
+ with col2:
599
+ # Scatter plot - risk vs impact
600
+ fig_scatter = go.Figure()
601
+
602
+ for move in results['move_scores']:
603
+ fig_scatter.add_trace(go.Scatter(
604
+ x=[move['risk']],
605
+ y=[move['impact']],
606
+ mode='markers+text',
607
+ name=move['id'],
608
+ text=[move['id']],
609
+ textposition="top center",
610
+ marker=dict(
611
+ size=move['score'] * 100,
612
+ color=move['fit'],
613
+ colorscale='Blues',
614
+ showscale=True,
615
+ colorbar=dict(
616
+ title=dict(
617
+ text="Fit",
618
+ font=dict(size=14, color='white'),
619
+ side='right'
620
+ ),
621
+ showticklabels=False,
622
+ thickness=15,
623
+ len=0.7
624
+ )
625
+ )
626
+ ))
627
+
628
+ fig_scatter.update_layout(
629
+ title={
630
+ 'text': "Risk vs Impact Analysis<br><sub>Bubble size = Score | Color intensity = Fit</sub>",
631
+ 'x': 0.5,
632
+ 'xanchor': 'center'
633
+ },
634
+ xaxis_title="Risk β†’",
635
+ yaxis_title="Impact β†’",
636
+ height=400,
637
+ showlegend=False
638
+ )
639
+ st.plotly_chart(fig_scatter, use_container_width=True)
640
+ st.caption("πŸ’‘ Larger bubbles = higher score | Darker blue = better fit with strategy")
641
+
642
+ # Metric forecasts
643
+ if results['metric_forecasts']:
644
+ st.markdown("---")
645
+ st.subheader("Metric Forecasts")
646
+
647
+ for forecast in results['metric_forecasts']:
648
+ # Highlight main goal metric
649
+ icon = "🎯" if forecast.get('is_main') else "πŸ“Š"
650
+ title_prefix = "MAIN GOAL: " if forecast.get('is_main') else ""
651
+
652
+ with st.expander(f"{icon} {title_prefix}{forecast['text']}", expanded=True):
653
+ # Metric summary in columns
654
+ col1, col2, col3 = st.columns(3)
655
+
656
+ with col1:
657
+ st.markdown(f"**Baseline:** {forecast['baseline']}{forecast['unit']}")
658
+ with col2:
659
+ st.markdown(f"**Forecast:** <span style='color: #1e90ff; font-weight: bold;'>{forecast['forecast']}{forecast['unit']}</span>", unsafe_allow_html=True)
660
+ with col3:
661
+ gap_color = 'red' if forecast['gap_to_target'] > 0 else 'green'
662
+ 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)
663
+
664
+ st.markdown("") # spacing
665
+
666
+ # Show linked hypotheses
667
+ if forecast.get('linked_hypotheses'):
668
+ st.markdown(f"**Linked Hypotheses:** {', '.join(forecast['linked_hypotheses'])}")
669
+
670
+ # Show contributing moves breakdown
671
+ if forecast.get('linked_moves'):
672
+ st.markdown("**Contributing Moves:**")
673
+ if len(forecast['linked_moves']) == 0:
674
+ st.info("⚠️ No moves linked to this metric")
675
+ else:
676
+ for move in forecast['linked_moves']:
677
+ contribution = move['score'] * forecast['baseline']
678
+ st.markdown(f"- **{move['id']}**: {move['text'][:60]}... (score: {move['score']:.4f}, contribution: +{contribution:.2f}{forecast['unit']})")
679
+ else:
680
+ st.info("⚠️ No moves linked to this metric")
681
+
682
+ # Recommendations
683
+ if results['recommendations']:
684
+ st.markdown("---")
685
+ st.subheader("Recommendations")
686
+ for rec in results['recommendations']:
687
+ st.info(rec)
688
+
689
+ # Strategy graph visualization
690
+ st.markdown("---")
691
+ st.subheader("Strategy Logic Flow")
692
+ render_strategy_graph(strategy)
693
+
694
+
695
+ def render_strategy_graph(strategy):
696
+ """Render NetworkX graph of strategy logic"""
697
+ G = nx.DiGraph()
698
+
699
+ # Add nodes
700
+ for insight in strategy.insights:
701
+ G.add_node(insight.id, type='insight', label=insight.text[:30])
702
+
703
+ for hyp in strategy.hypotheses:
704
+ G.add_node(hyp.id, type='hypothesis', label=hyp.text[:30])
705
+
706
+ for move in strategy.moves:
707
+ G.add_node(move.id, type='move', label=move.text[:30])
708
+
709
+ for metric in strategy.metrics:
710
+ G.add_node(metric.id, type='metric', label=metric.text[:30])
711
+
712
+ # Add edges
713
+ for hyp in strategy.hypotheses:
714
+ for insight_id in hyp.based_on:
715
+ if G.has_node(insight_id):
716
+ G.add_edge(insight_id, hyp.id)
717
+ if G.has_node(hyp.metric):
718
+ G.add_edge(hyp.id, hyp.metric)
719
+
720
+ for move in strategy.moves:
721
+ if G.has_node(move.linked_hypothesis):
722
+ G.add_edge(move.linked_hypothesis, move.id)
723
+
724
+ # Draw graph
725
+ fig, ax = plt.subplots(figsize=(12, 8))
726
+ pos = nx.spring_layout(G, k=2, iterations=50, seed=42)
727
+
728
+ # Color nodes by type
729
+ node_colors = []
730
+ for node in G.nodes():
731
+ node_type = G.nodes[node].get('type', '')
732
+ if node_type == 'insight':
733
+ node_colors.append('#90EE90')
734
+ elif node_type == 'hypothesis':
735
+ node_colors.append('#FFD700')
736
+ elif node_type == 'move':
737
+ node_colors.append('#1e90ff')
738
+ elif node_type == 'metric':
739
+ node_colors.append('#FF6B6B')
740
+ else:
741
+ node_colors.append('#CCCCCC')
742
+
743
+ nx.draw(G, pos, ax=ax, node_color=node_colors, node_size=2000,
744
+ with_labels=True, font_size=8, font_weight='bold',
745
+ arrows=True, arrowsize=20, edge_color='#999999', width=2)
746
+
747
+ ax.set_title("Strategy Logic Flow\n(Green=Insight, Yellow=Hypothesis, Blue=Move, Red=Metric)",
748
+ fontsize=12, fontweight='bold')
749
+
750
+ st.pyplot(fig, clear_figure=True)
751
+
752
+
753
+ def render_ai_evaluator():
754
+ """Render AI evaluator page"""
755
+ st.markdown('<div class="main-header">AI Evaluator</div>', unsafe_allow_html=True)
756
+ st.markdown('<div class="sub-header">Get brutally honest AI audit of your strategy</div>', unsafe_allow_html=True)
757
+
758
+ strategy = st.session_state.strategy
759
+
760
+ # Check API key
761
+ if not st.session_state.openai_api_key:
762
+ st.warning("Please configure your OpenAI API key in the sidebar.")
763
+ return
764
+
765
+ # Check if strategy is ready
766
+ if not strategy.goal or not strategy.moves:
767
+ st.warning("Strategy is incomplete. Please define at least a goal and some moves.")
768
+ return
769
+
770
+ # Display current strategy summary
771
+ st.subheader("Strategy Summary")
772
+ st.write(f"**Goal:** {strategy.goal.text}")
773
+ st.write(f"**Insights:** {len(strategy.insights)}")
774
+ st.write(f"**Hypotheses:** {len(strategy.hypotheses)}")
775
+ st.write(f"**Moves:** {len(strategy.moves)}")
776
+
777
+ st.markdown("---")
778
+
779
+ # Ask AI button
780
+ st.info("⚠️ This AI audit will be brutally honest - expect specific criticism, red flags, and concrete fixes (not polite suggestions).")
781
+ if st.button("πŸ€– Run Strategy Audit", use_container_width=True):
782
+ with st.spinner("AI is auditing your strategy..."):
783
+ feedback = get_ai_evaluation(strategy)
784
+ st.session_state.ai_feedback = feedback
785
+ st.rerun()
786
+
787
+ # Display AI feedback
788
+ if st.session_state.ai_feedback:
789
+ st.markdown("---")
790
+ st.subheader("πŸ” Strategy Audit Report")
791
+ st.markdown(st.session_state.ai_feedback)
792
+
793
+
794
+ def get_ai_evaluation(strategy: Strategy) -> str:
795
+ """Call OpenAI API to evaluate strategy"""
796
+ try:
797
+ client = OpenAI(api_key=st.session_state.openai_api_key)
798
+
799
+ # Prepare strategy data
800
+ strategy_json = json.dumps(strategy.to_dict(), indent=2, ensure_ascii=False)
801
+
802
+ # Create prompt
803
+ prompt = f"""You are a ruthlessly analytical strategy evaluator. Your job is to find weaknesses, gaps, and risks - not to be polite.
804
+
805
+ IMPORTANT: Detect the language of the strategy below and respond in THE SAME LANGUAGE.
806
+ If the strategy is in Polish, respond in Polish. If in English, respond in English.
807
+
808
+ Analyze this strategy with BRUTAL honesty:
809
+
810
+ Strategy:
811
+ ```json
812
+ {strategy_json}
813
+ ```
814
+
815
+ Your evaluation MUST include:
816
+
817
+ ## 1. LOGICAL FLOW AUDIT (Score: X/10)
818
+ - Map exact connections: Which insights feed which hypotheses? Which hypotheses feed which moves?
819
+ - RED FLAGS: List any broken links (e.g., "Move m3 links to h2, but h2 doesn't target the main goal metric")
820
+ - ORPHANS: Are there insights/hypotheses not connected to any moves? List them by ID.
821
+
822
+ ## 2. HYPOTHESIS QUALITY CHECK
823
+ For EACH hypothesis (h1, h2, h3, h4...):
824
+ - Is expected_change realistic? (Compare to industry benchmarks if known)
825
+ - Is the metric actually measurable? How would you track it?
826
+ - RED FLAG if hypothesis is vague or unmeasurable
827
+
828
+ ## 3. MOVE PORTFOLIO ANALYSIS
829
+ - Cost concentration: What % of budget goes to top move? (Flag if >40%)
830
+ - Risk profile: How many high-risk moves (risk >0.6)?
831
+ - MISSING MOVES: What obvious actions are NOT here? (e.g., "No paid search strategy despite digital focus")
832
+ - Prioritization: Using score formula, which moves should be CUT? Be specific with IDs.
833
+
834
+ ## 4. GAP ANALYSIS - What's MISSING?
835
+ - Competitive response: How will competitors react? No moves addressing this?
836
+ - Measurement plan: How will you track these metrics in reality?
837
+ - Budget realism: Total cost vs. expected revenue/impact - does math work?
838
+ - Timeline: Are moves sequenced or all at once? Flag if unclear.
839
+
840
+ ## 5. TOP 3 FATAL FLAWS
841
+ List the 3 biggest risks that could KILL this strategy. Be specific:
842
+ - "Move m6 costs 150k but has lowest score (X.XX) - CUT IT"
843
+ - "No B2B distribution strategy despite targeting gyms/studios"
844
+ - "Hypothesis h2 assumes 30% trial rate but category average is 8%"
845
+
846
+ ## 6. ACTIONABLE FIXES (Prioritized)
847
+ πŸ”΄ CRITICAL (do NOW):
848
+ - Specific change with exact ID reference (e.g., "Change h3 expected_change from 0.45 to 0.25")
849
+
850
+ 🟑 IMPORTANT (before launch):
851
+ - Concrete additions (e.g., "Add move m7: Partnership with Ε»abka for distribution")
852
+
853
+ 🟒 NICE TO HAVE:
854
+ - Optimizations
855
+
856
+ NO GENERIC ADVICE like "consider research" or "might want to test". Give SPECIFIC actions with IDs, numbers, and rationale.
857
+
858
+ Remember: RESPOND IN THE SAME LANGUAGE AS THE STRATEGY. Be direct, numerical, and reference specific IDs.
859
+ """
860
+
861
+ # Call API
862
+ response = client.chat.completions.create(
863
+ model="gpt-4",
864
+ messages=[
865
+ {"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."},
866
+ {"role": "user", "content": prompt}
867
+ ],
868
+ temperature=0.5,
869
+ max_tokens=2500
870
+ )
871
+
872
+ return response.choices[0].message.content
873
+
874
+ except Exception as e:
875
+ return f"Error calling OpenAI API: {str(e)}\n\nPlease check your API key and try again."
876
+
877
+
878
+ def render_report():
879
+ """Render report generation page"""
880
+ st.markdown('<div class="main-header">Strategy Report</div>', unsafe_allow_html=True)
881
+ st.markdown('<div class="sub-header">View and export your complete strategy</div>', unsafe_allow_html=True)
882
+
883
+ strategy = st.session_state.strategy
884
+
885
+ if not strategy.goal:
886
+ st.warning("No strategy to report. Go to Strategy Builder first.")
887
+ return
888
+
889
+ # Generate report
890
+ report = generate_report(strategy)
891
+
892
+ # Display report
893
+ st.markdown(report)
894
+
895
+ # Export buttons
896
+ st.markdown("---")
897
+ col1, col2 = st.columns(2)
898
+
899
+ with col1:
900
+ st.download_button(
901
+ label="πŸ“„ Download as Markdown",
902
+ data=report,
903
+ file_name=f"strategy_report_{datetime.now().strftime('%Y%m%d_%H%M%S')}.md",
904
+ mime="text/markdown",
905
+ use_container_width=True
906
+ )
907
+
908
+ with col2:
909
+ json_data = json.dumps(strategy.to_dict(), indent=2, ensure_ascii=False)
910
+ st.download_button(
911
+ label="πŸ“₯ Download as JSON",
912
+ data=json_data,
913
+ file_name=f"strategy_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json",
914
+ mime="application/json",
915
+ use_container_width=True
916
+ )
917
+
918
+
919
+ def generate_report(strategy: Strategy) -> str:
920
+ """Generate markdown report from strategy"""
921
+ report = f"""# Strategic Sandbox Report
922
+ *Generated on {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}*
923
+
924
+ ---
925
+
926
+ ## 🎯 Goal
927
+
928
+ **{strategy.goal.text if strategy.goal else 'Not defined'}**
929
+
930
+ """
931
+
932
+ if strategy.goal:
933
+ report += f"""
934
+ - **Metric:** {strategy.goal.metric}
935
+ - **Baseline:** {strategy.goal.baseline}{strategy.goal.unit}
936
+ - **Target:** {strategy.goal.target}{strategy.goal.unit}
937
+ - **Horizon:** {strategy.goal.horizon}
938
+ - **Unit:** {strategy.goal.unit}
939
+ """
940
+
941
+ if strategy.arena:
942
+ report += f"""
943
+ ---
944
+
945
+ ## πŸ—ΊοΈ Arena
946
+
947
+ - **Market:** {strategy.arena.market}
948
+ - **Category:** {strategy.arena.category}
949
+ - **Target Audience:** {strategy.arena.target_audience}
950
+ - **Competitors:** {', '.join(strategy.arena.competitors)}
951
+ """
952
+
953
+ if strategy.insights:
954
+ report += "\n---\n\n## πŸ’‘ Insights\n\n"
955
+ for insight in strategy.insights:
956
+ report += f"### {insight.id}\n{insight.text}\n\n*Evidence: {', '.join(insight.evidence)}*\n\n"
957
+
958
+ if strategy.hypotheses:
959
+ report += "---\n\n## πŸ”¬ Hypotheses\n\n"
960
+ for hyp in strategy.hypotheses:
961
+ report += f"### {hyp.id}\n{hyp.text}\n\n"
962
+ report += f"- Based on: {', '.join(hyp.based_on)}\n"
963
+ report += f"- Metric: {hyp.metric}\n"
964
+ report += f"- Expected change: {hyp.expected_change}\n\n"
965
+
966
+ if strategy.moves:
967
+ report += "---\n\n## 🎬 Moves\n\n"
968
+ for move in strategy.moves:
969
+ report += f"### {move.id}: {move.text}\n\n"
970
+ report += f"- Linked hypothesis: {move.linked_hypothesis}\n"
971
+ report += f"- Impact: {move.impact} | Fit: {move.fit} | Risk: {move.risk}\n"
972
+ report += f"- Cost: {move.cost:,.0f}\n\n"
973
+
974
+ if strategy.metrics:
975
+ report += "---\n\n## πŸ“Š Supporting Metrics\n\n"
976
+ for metric in strategy.metrics:
977
+ report += f"### {metric.id}: {metric.text}\n"
978
+ report += f"- Baseline: {metric.baseline}{metric.unit}\n"
979
+ report += f"- Target: {metric.target}{metric.unit}\n\n"
980
+
981
+ # Add simulation results if available
982
+ if st.session_state.simulation_results:
983
+ results = st.session_state.simulation_results
984
+ report += "---\n\n## πŸ“ˆ Simulation Results\n\n"
985
+ report += f"**Total Impact Score:** {results['total_impact']:.4f}\n\n"
986
+
987
+ report += "### Move Rankings\n\n"
988
+ for move in results['move_scores']:
989
+ report += f"- **{move['id']}**: Score {move['score']:.4f} (Impact: {move['impact']}, Risk: {move['risk']}, Cost: {move['cost']:,.0f})\n"
990
+
991
+ # Add AI feedback if available
992
+ if st.session_state.ai_feedback:
993
+ report += "\n---\n\n## πŸ€– AI Evaluation\n\n"
994
+ report += st.session_state.ai_feedback
995
+
996
+ report += "\n\n---\n\n*Generated with Strategic Sandbox v1.0*\n"
997
+ report += "*πŸ€– Generated with [Claude Code](https://claude.com/claude-code)*\n"
998
+
999
+ return report
1000
+
1001
+
1002
+ def save_strategy():
1003
+ """Save strategy to JSON file"""
1004
+ timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
1005
+ filename = f"data/strategy_{timestamp}.json"
1006
+ st.session_state.strategy.to_json(filename)
1007
+ st.success(f"Strategy saved to {filename}")
1008
+
1009
+
1010
+ def export_strategy_json():
1011
+ """Export strategy as downloadable JSON"""
1012
+ json_data = json.dumps(st.session_state.strategy.to_dict(), indent=2, ensure_ascii=False)
1013
+ st.download_button(
1014
+ label="Download JSON",
1015
+ data=json_data,
1016
+ file_name=f"strategy_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json",
1017
+ mime="application/json"
1018
+ )
1019
+
1020
+
1021
+ # Main app
1022
+ def main():
1023
+ init_session_state()
1024
+ render_sidebar()
1025
+
1026
+ # Route to appropriate page
1027
+ page = st.session_state.current_page
1028
+
1029
+ if page == "Dashboard":
1030
+ render_dashboard()
1031
+ elif page == "Strategy Builder":
1032
+ render_strategy_builder()
1033
+ elif page == "Simulation":
1034
+ render_simulation()
1035
+ elif page == "AI Evaluator":
1036
+ render_ai_evaluator()
1037
+ elif page == "Report":
1038
+ render_report()
1039
+
1040
+
1041
+ if __name__ == "__main__":
1042
+ main()