Marek4321 commited on
Commit
41801e2
Β·
verified Β·
1 Parent(s): a6973d8

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +1043 -0
app.py ADDED
@@ -0,0 +1,1043 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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()