Danialebrat commited on
Commit
bd604e5
·
1 Parent(s): b5c659d

Fixing deployment issues:

Browse files

Transferring files from visualization directory to the root and modifying paths.

app.py CHANGED
@@ -1,26 +1,308 @@
1
  """
2
- HuggingFace Space Entry Point
3
- This file serves as the entry point for the HuggingFace Space,
4
- redirecting to the visualization app.
5
  """
6
 
 
7
  import sys
8
  from pathlib import Path
 
9
 
10
- # Get directories
11
- root_dir = Path(__file__).parent
12
- visualization_dir = root_dir / "visualization"
 
13
 
14
- # Add both directories to Python path
15
- sys.path.insert(0, str(root_dir))
16
- sys.path.insert(0, str(root_dir / "ai_messaging_system_v2"))
17
- sys.path.insert(0, str(visualization_dir))
18
 
19
- # Import and run the visualization app
20
- import importlib.util
 
21
 
22
- # Load the visualization app module
23
- app_path = visualization_dir / "app.py"
24
- spec = importlib.util.spec_from_file_location("visualization_app", app_path)
25
- visualization_app = importlib.util.module_from_spec(spec)
26
- spec.loader.exec_module(visualization_app)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  """
2
+ AI Messaging System - Visualization Tool
3
+ Main entry point with authentication and brand selection.
 
4
  """
5
 
6
+ import streamlit as st
7
  import sys
8
  from pathlib import Path
9
+ from dotenv import load_dotenv
10
 
11
+ # Load environment variables from .env file FIRST
12
+ env_path = Path(__file__).parent / '.env'
13
+ if env_path.exists():
14
+ load_dotenv(env_path)
15
 
16
+ # Add root directory to path (for ai_messaging_system_v2 imports)
17
+ sys.path.insert(0, str(Path(__file__).parent))
 
 
18
 
19
+ from utils.auth import verify_login, check_authentication, get_current_user, logout
20
+ from utils.theme import apply_theme, get_brand_emoji, get_brand_theme
21
+ from utils.data_loader import DataLoader
22
 
23
+ # Page configuration
24
+ st.set_page_config(
25
+ page_title="AI Messaging System - Visualization",
26
+ page_icon="🎵",
27
+ layout="wide",
28
+ initial_sidebar_state="expanded"
29
+ )
30
+
31
+ # Initialize session state
32
+ def init_session_state():
33
+ """Initialize session state variables."""
34
+ defaults = {
35
+ "authenticated": False,
36
+ "user_email": "",
37
+ "selected_brand": None,
38
+ "current_experiment_id": None,
39
+ }
40
+ for key, value in defaults.items():
41
+ if key not in st.session_state:
42
+ st.session_state[key] = value
43
+
44
+
45
+ init_session_state()
46
+
47
+ # Authentication check
48
+ if not check_authentication():
49
+ # Show login page
50
+ st.title("🔐 AI Messaging System - Login")
51
+
52
+ st.markdown("""
53
+ Welcome to the **AI Messaging System Visualization Tool**!
54
+
55
+ This tool enables you to:
56
+ - 🏗️ Build and configure message campaigns
57
+ - 👀 Visualize generated messages across all stages
58
+ - 📊 Analyze performance and provide feedback
59
+ - 🧪 Run A/B tests to compare different approaches
60
+
61
+ ---
62
+
63
+ **Access is restricted to authorized team members only.**
64
+ Please enter your credentials below.
65
+ """)
66
+
67
+ with st.form("login_form"):
68
+ email = st.text_input("📧 Email Address", placeholder="your.name@musora.com")
69
+ token = st.text_input("🔑 Access Token", type="password", placeholder="Enter your access token")
70
+ submit = st.form_submit_button("🚀 Login", use_container_width=True)
71
+
72
+ if submit:
73
+ if verify_login(email, token):
74
+ st.session_state.authenticated = True
75
+ st.session_state.user_email = email
76
+ st.success("✅ Login successful! Redirecting...")
77
+ st.rerun()
78
+ else:
79
+ st.error("❌ Invalid email or access token. Please try again.")
80
+
81
+ st.stop()
82
+
83
+ # User is authenticated - show main app
84
+ # Apply base theme initially
85
+ apply_theme("base")
86
+
87
+ # Sidebar - Brand Selection
88
+ with st.sidebar:
89
+ st.title("🎵 AI Messaging System")
90
+
91
+ st.markdown(f"**Logged in as:** {get_current_user()}")
92
+
93
+ if st.button("🚪 Logout", use_container_width=True):
94
+ logout()
95
+ st.rerun()
96
+
97
+ st.markdown("---")
98
+
99
+ # Brand Selection
100
+ st.subheader("🎨 Select Brand")
101
+
102
+ brands = ["drumeo", "pianote", "guitareo", "singeo"]
103
+ brand_labels = {
104
+ "drumeo": "🥁 Drumeo",
105
+ "pianote": "🎹 Pianote",
106
+ "guitareo": "🎸 Guitareo",
107
+ "singeo": "🎤 Singeo"
108
+ }
109
+
110
+ # Get current brand from session state if exists
111
+ current_brand = st.session_state.get("selected_brand", brands[0])
112
+
113
+ selected_brand = st.selectbox(
114
+ "Brand",
115
+ brands,
116
+ index=brands.index(current_brand) if current_brand in brands else 0,
117
+ format_func=lambda x: brand_labels[x],
118
+ key="brand_selector",
119
+ label_visibility="collapsed"
120
+ )
121
+
122
+ # Update session state with selected brand
123
+ st.session_state.selected_brand = selected_brand
124
+
125
+ # Apply brand theme
126
+ if selected_brand:
127
+ apply_theme(selected_brand)
128
+
129
+ st.markdown("---")
130
+
131
+ # Navigation info
132
+ st.subheader("📖 Navigation")
133
+ st.markdown("""
134
+ Use the pages in the sidebar to navigate:
135
+ - **Campaign Builder**: Create campaigns & A/B tests
136
+ - **Message Viewer**: Review messages
137
+ - **Analytics**: View current performance
138
+ - **Historical Analytics**: Track improvements
139
+ """)
140
+
141
+ # Main Content Area
142
+ if not selected_brand:
143
+ st.title("🎵 Welcome to AI Messaging System")
144
+
145
+ st.markdown("""
146
+ ### Get Started
147
+
148
+ **Please select a brand from the sidebar to begin.**
149
+
150
+ Once you've selected a brand, you can:
151
+
152
+ 1. **Build Campaigns** - Configure and generate personalized messages
153
+ 2. **View Messages** - Browse and provide feedback on generated messages
154
+ 3. **Run A/B Tests** - Compare different configurations side-by-side
155
+ 4. **Analyze Results** - Review performance metrics and insights
156
+ """)
157
+
158
+ # Show feature cards
159
+ col1, col2 = st.columns(2)
160
+
161
+ with col1:
162
+ st.markdown("""
163
+ #### 🏗️ Campaign Builder
164
+ - Configure multi-stage campaigns
165
+ - Built-in A/B testing mode
166
+ - Parallel experiment processing
167
+ - Automatic experiment archiving
168
+ - Save custom configurations
169
+ """)
170
+
171
+ st.markdown("""
172
+ #### 👀 Message Viewer
173
+ - View all generated messages
174
+ - Side-by-side A/B comparison
175
+ - User-centric or stage-centric views
176
+ - Search and filter messages
177
+ - Provide detailed feedback
178
+ """)
179
+
180
+ with col2:
181
+ st.markdown("""
182
+ #### 📊 Analytics Dashboard
183
+ - Real-time performance metrics
184
+ - A/B test winner determination
185
+ - Rejection reason analysis
186
+ - Stage-by-stage performance
187
+ - Export capabilities
188
+ """)
189
+
190
+ st.markdown("""
191
+ #### 📚 Historical Analytics
192
+ - Track all past experiments
193
+ - Rejection rate trends over time
194
+ - Compare historical A/B tests
195
+ - Export historical data
196
+ """)
197
+
198
+ else:
199
+ # Brand is selected - show brand-specific homepage
200
+ emoji = get_brand_emoji(selected_brand)
201
+ theme = get_brand_theme(selected_brand)
202
+
203
+ st.title(f"{emoji} {selected_brand.title()} - AI Messaging System")
204
+
205
+ st.markdown(f"""
206
+ ### Welcome to {selected_brand.title()} Message Generation!
207
+
208
+ You're all set to create personalized messages for {selected_brand.title()} users.
209
+ """)
210
+
211
+ # Quick stats
212
+ data_loader = DataLoader()
213
+
214
+ # Check if we have brand users
215
+ has_users = data_loader.has_brand_users(selected_brand)
216
+
217
+ # Check if we have generated messages
218
+ messages_df = data_loader.load_generated_messages()
219
+ has_messages = messages_df is not None and len(messages_df) > 0
220
+
221
+ st.markdown("---")
222
+
223
+ # Status cards
224
+ col1, col2, col3 = st.columns(3)
225
+
226
+ with col1:
227
+ st.markdown("### 👥 Available Users")
228
+ if has_users:
229
+ user_count = data_loader.get_brand_user_count(selected_brand)
230
+ st.metric("Users Available", user_count)
231
+ st.success(f"✅ {user_count} users ready")
232
+ else:
233
+ st.metric("Users Available", 0)
234
+ st.info("ℹ️ No users available")
235
+
236
+ with col2:
237
+ st.markdown("### 📨 Generated Messages")
238
+ if has_messages:
239
+ stats = data_loader.get_message_stats()
240
+ st.metric("Total Messages", stats['total_messages'])
241
+ st.success(f"✅ {stats['total_stages']} stages")
242
+ else:
243
+ st.metric("Total Messages", 0)
244
+ st.info("ℹ️ No messages generated yet")
245
+
246
+ with col3:
247
+ st.markdown("### 🎯 Quick Actions")
248
+ st.markdown("") # spacing
249
+ if st.button("🏗️ Build Campaign", use_container_width=True):
250
+ st.switch_page("pages/1_Campaign_Builder.py")
251
+ if has_messages:
252
+ if st.button("👀 View Messages", use_container_width=True):
253
+ st.switch_page("pages/2_Message_Viewer.py")
254
+
255
+ st.markdown("---")
256
+
257
+ # Getting started guide
258
+ st.markdown("### 🚀 Quick Start Guide")
259
+
260
+ st.markdown("""
261
+ #### Step 1: Build a Campaign
262
+ Navigate to **Campaign Builder** to:
263
+ - Toggle A/B testing mode on/off as needed
264
+ - Select number of users to experiment with (1-25)
265
+ - Configure campaign stages (or two experiments side-by-side)
266
+ - Set LLM models and instructions
267
+ - Generate personalized messages with automatic archiving
268
+
269
+ #### Step 2: Review Messages
270
+ Go to **Message Viewer** to:
271
+ - Browse all generated messages
272
+ - View A/B tests side-by-side automatically
273
+ - Switch between user-centric and stage-centric views
274
+ - Provide detailed feedback with rejection reasons
275
+ - Track message headers and content
276
+
277
+ #### Step 3: Analyze Performance
278
+ Check **Analytics Dashboard** for:
279
+ - Real-time approval and rejection rates
280
+ - A/B test comparisons with winner determination
281
+ - Common rejection reasons with visual charts
282
+ - Stage-by-stage performance insights
283
+ - Export analytics data
284
+
285
+ #### Step 4: Track Improvements
286
+ View **Historical Analytics** to:
287
+ - See all past experiments over time
288
+ - Identify rejection rate trends
289
+ - Compare historical A/B tests
290
+ - Export comprehensive historical data
291
+ """)
292
+
293
+ st.markdown("---")
294
+
295
+ # Tips
296
+ with st.expander("💡 Tips & Best Practices"):
297
+ st.markdown("""
298
+ - **User Selection**: Select 1-25 users for quick experimentation
299
+ - **Configuration Templates**: Start with default configurations and customize as needed
300
+ - **A/B Testing**: Enable A/B mode in Campaign Builder to compare two configurations in parallel
301
+ - **Feedback**: Reject poor messages with specific categories including the new 'Similar To Previous' option
302
+ - **Historical Tracking**: Check Historical Analytics regularly to track improvements over time
303
+ - **Stage Design**: Each stage should build upon previous stages with varied messaging approaches
304
+ - **Automatic Archiving**: Previous experiments are automatically archived when you start a new one
305
+ """)
306
+
307
+ st.markdown("---")
308
+ st.markdown("**Built with ❤️ for the Musora team**")
data/UI_users/drumeo_users.csv ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ USER_ID
2
+ 668594
3
+ 268920
4
+ 223559
5
+ 830729
6
+ 356828
7
+ 703749
8
+ 840761
9
+ 773120
10
+ 847327
11
+ 566765
12
+ 843750
13
+ 825806
14
+ 551310
15
+ 924846
16
+ 655113
17
+ 576751
18
+ 334867
19
+ 691463
20
+ 680767
21
+ 812160
22
+ 164216
23
+ 881978
24
+ 539894
25
+ 566222
26
+ 660036
27
+ 531101
28
+ 591381
29
+ 876151
30
+ 596577
31
+ 910512
32
+ 395956
33
+ 173257
34
+ 792254
35
+ 911568
36
+ 914399
37
+ 903638
38
+ 736051
39
+ 675660
40
+ 892433
41
+ 346547
42
+ 824636
43
+ 774712
44
+ 488145
45
+ 688315
46
+ 546326
47
+ 610623
48
+ 560107
49
+ 356255
50
+ 757824
51
+ 826319
52
+ 272269
53
+ 919799
54
+ 670740
55
+ 919348
56
+ 324493
57
+ 284107
58
+ 772112
59
+ 821339
60
+ 433101
61
+ 705894
62
+ 775898
63
+ 922718
64
+ 627434
65
+ 811134
66
+ 922432
67
+ 922696
68
+ 934924
69
+ 158386
70
+ 488116
71
+ 167704
72
+ 721421
73
+ 785019
74
+ 831724
75
+ 928899
76
+ 911985
77
+ 911041
78
+ 552685
79
+ 391892
80
+ 265614
81
+ 916134
82
+ 402573
83
+ 900149
84
+ 717562
85
+ 161665
86
+ 389376
87
+ 509974
88
+ 913237
89
+ 760712
90
+ 415475
91
+ 346652
92
+ 604378
93
+ 817870
94
+ 698502
95
+ 904044
96
+ 931916
97
+ 662869
98
+ 837947
99
+ 666152
100
+ 367181
101
+ 510852
data/UI_users/guitareo_users.csv ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ USER_ID
2
+ 157715
3
+ 911553
4
+ 156907
5
+ 694766
6
+ 914407
7
+ 158017
8
+ 157707
9
+ 931216
10
+ 157275
11
+ 157049
12
+ 762379
13
+ 922989
14
+ 894939
15
+ 156233
16
+ 445299
17
+ 893526
18
+ 502852
19
+ 412696
20
+ 524379
21
+ 934803
22
+ 533843
23
+ 918486
24
+ 326346
25
+ 825138
26
+ 510105
27
+ 836199
28
+ 903763
29
+ 847337
30
+ 920186
31
+ 367515
32
+ 464147
33
+ 876953
34
+ 820530
35
+ 483932
36
+ 804270
37
+ 652888
38
+ 611387
39
+ 901114
40
+ 161990
41
+ 858049
42
+ 504479
43
+ 793688
44
+ 596030
45
+ 856499
46
+ 830288
47
+ 905807
48
+ 737373
49
+ 801123
50
+ 936207
51
+ 161806
52
+ 844885
53
+ 156673
54
+ 156417
55
+ 166828
56
+ 157214
57
+ 163682
58
+ 157637
59
+ 815647
60
+ 445428
61
+ 848726
62
+ 345886
63
+ 645329
64
+ 908931
65
+ 157569
66
+ 900579
67
+ 161677
68
+ 898048
69
+ 429111
70
+ 902004
71
+ 161816
72
+ 676947
73
+ 160755
74
+ 921572
75
+ 453858
76
+ 548274
77
+ 922381
78
+ 825297
79
+ 720840
80
+ 449753
81
+ 504742
82
+ 157789
83
+ 162667
84
+ 920329
85
+ 157258
86
+ 739976
87
+ 641480
88
+ 157176
89
+ 565814
90
+ 870217
91
+ 574774
92
+ 420668
93
+ 344602
94
+ 918695
95
+ 174797
96
+ 270977
97
+ 903413
98
+ 157324
99
+ 158234
100
+ 156592
101
+ 491409
data/UI_users/pianote_users.csv ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ USER_ID
2
+ 901462
3
+ 663534
4
+ 925578
5
+ 808019
6
+ 727360
7
+ 907785
8
+ 903125
9
+ 358505
10
+ 903293
11
+ 926435
12
+ 577983
13
+ 640044
14
+ 871545
15
+ 689297
16
+ 839797
17
+ 838978
18
+ 894404
19
+ 388259
20
+ 742545
21
+ 875618
22
+ 906682
23
+ 156360
24
+ 615274
25
+ 496647
26
+ 837649
27
+ 819147
28
+ 762360
29
+ 667066
30
+ 922040
31
+ 165396
32
+ 491498
33
+ 643639
34
+ 388015
35
+ 876565
36
+ 391879
37
+ 451960
38
+ 508244
39
+ 426511
40
+ 401073
41
+ 869259
42
+ 522076
43
+ 631399
44
+ 428081
45
+ 867206
46
+ 815870
47
+ 458726
48
+ 547732
49
+ 554044
50
+ 364525
51
+ 825266
52
+ 844418
53
+ 818026
54
+ 751982
55
+ 356018
56
+ 345635
57
+ 427868
58
+ 586083
59
+ 166330
60
+ 825075
61
+ 343531
62
+ 843298
63
+ 558798
64
+ 352221
65
+ 558176
66
+ 813053
67
+ 386914
68
+ 154237
69
+ 362024
70
+ 855589
71
+ 807289
72
+ 307320
73
+ 623466
74
+ 825321
75
+ 605229
76
+ 348870
77
+ 388261
78
+ 519602
79
+ 153096
80
+ 586282
81
+ 716526
82
+ 401891
83
+ 874652
84
+ 834192
85
+ 732481
86
+ 508825
87
+ 859490
88
+ 519051
89
+ 482569
90
+ 518170
91
+ 541203
92
+ 421310
93
+ 550180
94
+ 406176
95
+ 520979
96
+ 593825
97
+ 549999
98
+ 483788
99
+ 841717
100
+ 703563
101
+ 364690
data/UI_users/singeo_users.csv ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ USER_ID
2
+ 397959
3
+ 807871
4
+ 532351
5
+ 344867
6
+ 491564
7
+ 687686
8
+ 912997
9
+ 757896
10
+ 475186
11
+ 559116
12
+ 476436
13
+ 430839
14
+ 929828
15
+ 546602
16
+ 660403
17
+ 809863
18
+ 930638
19
+ 308771
20
+ 156181
21
+ 832580
22
+ 642142
23
+ 484262
24
+ 672837
25
+ 723502
26
+ 735251
27
+ 440691
28
+ 676900
29
+ 891419
30
+ 912102
31
+ 605067
32
+ 727808
33
+ 449959
34
+ 295615
35
+ 934452
36
+ 932834
37
+ 631715
38
+ 933185
39
+ 247877
40
+ 550753
41
+ 841927
42
+ 795745
43
+ 555274
44
+ 679834
45
+ 675272
46
+ 911688
47
+ 689022
48
+ 414410
49
+ 502875
50
+ 649091
51
+ 540841
52
+ 616322
53
+ 761625
54
+ 534677
55
+ 830290
56
+ 870293
57
+ 510636
58
+ 932700
59
+ 533364
60
+ 540891
61
+ 584369
62
+ 739290
63
+ 914907
64
+ 934815
65
+ 760925
66
+ 411509
67
+ 420037
68
+ 566575
69
+ 667578
70
+ 483362
71
+ 787773
72
+ 164176
73
+ 876127
74
+ 344023
75
+ 634693
76
+ 365543
77
+ 160559
78
+ 474363
79
+ 675851
80
+ 934109
81
+ 563890
82
+ 875101
83
+ 768715
84
+ 405549
85
+ 509855
86
+ 615509
87
+ 800896
88
+ 901230
89
+ 444234
90
+ 152323
91
+ 610485
92
+ 628039
93
+ 854271
94
+ 503894
95
+ 339113
96
+ 441986
97
+ 721231
98
+ 486789
99
+ 807279
100
+ 749298
101
+ 923577
data/configs/.gitkeep ADDED
File without changes
data/configs/drumeo_re_engagement_test.json ADDED
@@ -0,0 +1,193 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "brand": "drumeo",
3
+ "campaign_type": "re_engagement",
4
+ "campaign_name": "UI-Test-Campaign-Re-engagement",
5
+ "campaign_instructions": "Keep messages encouraging and motivational. Focus on getting users excited about practicing.",
6
+ "1": {
7
+ "stage": 1,
8
+ "model": "gpt-5-nano",
9
+ "personalization": true,
10
+ "involve_recsys_result": true,
11
+ "recsys_contents": [
12
+ "workout",
13
+ "course",
14
+ "quick_tips"
15
+ ],
16
+ "specific_content_id": null,
17
+ "segment_info": "Students who haven't practiced and logged into the app after at least 3 days.",
18
+ "instructions": "",
19
+ "sample_examples": "Header: Your next lesson is waiting 👇 \n Message: Check it out now and improve your playing!",
20
+ "identifier_column": "user_id",
21
+ "platform": "push"
22
+ },
23
+ "2": {
24
+ "stage": 2,
25
+ "model": "gemini-2.5-flash-lite",
26
+ "personalization": true,
27
+ "involve_recsys_result": true,
28
+ "recsys_contents": [
29
+ "workout",
30
+ "course",
31
+ "quick_tips"
32
+ ],
33
+ "specific_content_id": null,
34
+ "segment_info": "Students who haven't practiced and logged into the app after at least 3 days.",
35
+ "instructions": "",
36
+ "sample_examples": "Header: It's a great day to play 🤩,\n Message: It's been a few days — warm up with a quick lesson!",
37
+ "identifier_column": "user_id",
38
+ "platform": "push"
39
+ },
40
+ "3": {
41
+ "stage": 3,
42
+ "model": "gemini-2.5-flash-lite",
43
+ "personalization": true,
44
+ "involve_recsys_result": true,
45
+ "recsys_contents": [
46
+ "workout",
47
+ "course",
48
+ "quick_tips"
49
+ ],
50
+ "specific_content_id": null,
51
+ "segment_info": "Students who haven't practiced and logged into the app after at least 3 days.",
52
+ "instructions": "",
53
+ "sample_examples": "Header: Practice makes progress 💪, \nMessage: You don't need to be perfect. But you do need practice to reach your goals!",
54
+ "identifier_column": "user_id",
55
+ "platform": "push"
56
+ },
57
+ "4": {
58
+ "stage": 4,
59
+ "model": "gemini-2.5-flash-lite",
60
+ "personalization": true,
61
+ "involve_recsys_result": true,
62
+ "recsys_contents": [
63
+ "workout",
64
+ "course",
65
+ "quick_tips"
66
+ ],
67
+ "specific_content_id": null,
68
+ "segment_info": "Students who haven't practiced and logged into the app after at least 3 days.",
69
+ "instructions": "",
70
+ "sample_examples": "Header: Never stop learning, \nMessage: Take a lesson today and get back on track!",
71
+ "identifier_column": "user_id",
72
+ "platform": "push"
73
+ },
74
+ "5": {
75
+ "stage": 5,
76
+ "model": "gemini-2.5-flash-lite",
77
+ "personalization": true,
78
+ "involve_recsys_result": true,
79
+ "recsys_contents": [
80
+ "workout",
81
+ "course",
82
+ "quick_tips"
83
+ ],
84
+ "specific_content_id": null,
85
+ "segment_info": "Students who haven't practiced and logged into the app after at least 3 days.",
86
+ "instructions": "",
87
+ "sample_examples": "Header: Get back on track ⏱️\nMessage: It's been two weeks since your last practice session. Take a lesson today!",
88
+ "identifier_column": "user_id",
89
+ "platform": "push"
90
+ },
91
+ "6": {
92
+ "stage": 6,
93
+ "model": "gemini-2.5-flash-lite",
94
+ "personalization": true,
95
+ "involve_recsys_result": true,
96
+ "recsys_contents": [
97
+ "workout",
98
+ "course",
99
+ "quick_tips"
100
+ ],
101
+ "specific_content_id": null,
102
+ "segment_info": "Students who haven't practiced and logged into the app after at least 3 days.",
103
+ "instructions": "",
104
+ "sample_examples": "Header: Keep on going!\nMessage: Get back to playing today. It only takes a few minutes!",
105
+ "identifier_column": "user_id",
106
+ "platform": "push"
107
+ },
108
+ "7": {
109
+ "stage": 7,
110
+ "model": "gemini-2.5-flash-lite",
111
+ "personalization": true,
112
+ "involve_recsys_result": true,
113
+ "recsys_contents": [
114
+ "workout",
115
+ "course",
116
+ "quick_tips"
117
+ ],
118
+ "specific_content_id": null,
119
+ "segment_info": "Students who haven't practiced and logged into the app after at least 3 days.",
120
+ "instructions": "",
121
+ "sample_examples": "Header: Ready to play? 🎸\nMessage: Let's get started. Time for a quick practice session!",
122
+ "identifier_column": "user_id",
123
+ "platform": "push"
124
+ },
125
+ "8": {
126
+ "stage": 8,
127
+ "model": "gemini-2.5-flash-lite",
128
+ "personalization": true,
129
+ "involve_recsys_result": true,
130
+ "recsys_contents": [
131
+ "workout",
132
+ "course",
133
+ "quick_tips"
134
+ ],
135
+ "specific_content_id": null,
136
+ "segment_info": "Students who haven't practiced and logged into the app after at least 3 days.",
137
+ "instructions": "",
138
+ "sample_examples": "Header: Your lesson's waiting. 📥\nMessage: We want to hear you play! Dive in today.",
139
+ "identifier_column": "user_id",
140
+ "platform": "push"
141
+ },
142
+ "9": {
143
+ "stage": 9,
144
+ "model": "gemini-2.5-flash-lite",
145
+ "personalization": true,
146
+ "involve_recsys_result": true,
147
+ "recsys_contents": [
148
+ "workout",
149
+ "course",
150
+ "quick_tips"
151
+ ],
152
+ "specific_content_id": null,
153
+ "segment_info": "Students who haven't practiced and logged into the app after at least 3 days.",
154
+ "instructions": "",
155
+ "sample_examples": "Header: Time for a comeback!\nMessage: We haven't seen you in 25 days. This will help get you back into the groove!",
156
+ "identifier_column": "user_id",
157
+ "platform": "push"
158
+ },
159
+ "10": {
160
+ "stage": 10,
161
+ "model": "gemini-2.5-flash-lite",
162
+ "personalization": true,
163
+ "involve_recsys_result": true,
164
+ "recsys_contents": [
165
+ "workout",
166
+ "course",
167
+ "quick_tips"
168
+ ],
169
+ "specific_content_id": null,
170
+ "segment_info": "Students who haven't practiced and logged into the app after at least 3 days.",
171
+ "instructions": "",
172
+ "sample_examples": "Header: Have you been practicing?\nMessage: You're very talented. We'd love to hear you play again!",
173
+ "identifier_column": "user_id",
174
+ "platform": "push"
175
+ },
176
+ "11": {
177
+ "stage": 11,
178
+ "model": "gemini-2.5-flash-lite",
179
+ "personalization": true,
180
+ "involve_recsys_result": true,
181
+ "recsys_contents": [
182
+ "workout",
183
+ "course",
184
+ "quick_tips"
185
+ ],
186
+ "specific_content_id": null,
187
+ "segment_info": "Students who haven't practiced and logged into the app after at least 3 days.",
188
+ "instructions": "",
189
+ "sample_examples": "Header: We Miss You 😔Message: All your lessons will just be here when you get back!",
190
+ "identifier_column": "user_id",
191
+ "platform": "push"
192
+ }
193
+ }
data/configs/guitareo_re_engagement_test.json ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "campaign_name": "UI-Test-Campaign-Re-engagement",
3
+ "brand": "guitareo",
4
+ "campaign_instructions": "Keep messages encouraging and motivational. Focus on getting users excited about practicing.",
5
+
6
+ "1": {
7
+ "identifier_column": "user_id",
8
+ "stage": 1,
9
+ "segment_info": "Students who haven't practiced and logged into the app after at least 3 days.",
10
+ "recsys_contents": ["workout", "course", "quick_tips"],
11
+ "involve_recsys_result": true,
12
+ "personalization": true,
13
+ "sample_examples": "Header: Your next lesson is waiting 👇 \n Message: Check it out now and improve your playing!",
14
+ "model": "gemini-2.5-flash-lite"
15
+ },
16
+ "2": {
17
+ "identifier_column": "user_id",
18
+ "stage": 2,
19
+ "segment_info": "Students who haven't practiced and logged into the app after at least 3 days.",
20
+ "recsys_contents": ["workout", "course", "quick_tips"],
21
+ "involve_recsys_result": true,
22
+ "personalization": true,
23
+ "sample_examples": "Header: It's a great day to play 🤩,\n Message: It's been a few days — warm up with a quick lesson!",
24
+ "model": "gemini-2.5-flash-lite"
25
+ },
26
+ "3": {
27
+ "identifier_column": "user_id",
28
+ "stage": 3,
29
+ "segment_info": "Students who haven't practiced and logged into the app after at least 3 days.",
30
+ "recsys_contents": ["workout", "course", "quick_tips"],
31
+ "involve_recsys_result": true,
32
+ "personalization": true,
33
+ "sample_examples": "Header: Practice makes progress 💪, \nMessage: You don't need to be perfect. But you do need practice to reach your goals!",
34
+ "model": "gemini-2.5-flash-lite"
35
+ },
36
+ "4": {
37
+ "identifier_column": "user_id",
38
+ "stage": 4,
39
+ "segment_info": "Students who haven't practiced and logged into the app after at least 3 days.",
40
+ "recsys_contents": ["workout", "course", "quick_tips"],
41
+ "involve_recsys_result": true,
42
+ "personalization": true,
43
+ "sample_examples": "Header: Never stop learning, \nMessage: Take a lesson today and get back on track!",
44
+ "model": "gemini-2.5-flash-lite"
45
+ },
46
+ "5": {
47
+ "identifier_column": "user_id",
48
+ "stage": 5,
49
+ "segment_info": "Students who haven't practiced and logged into the app after at least 3 days.",
50
+ "recsys_contents": ["workout", "course", "quick_tips"],
51
+ "involve_recsys_result": true,
52
+ "personalization": true,
53
+ "sample_examples": "Header: Get back on track ⏱️\nMessage: It's been two weeks since your last practice session. Take a lesson today!",
54
+ "model": "gemini-2.5-flash-lite"
55
+ },
56
+ "6": {
57
+ "identifier_column": "user_id",
58
+ "stage": 6,
59
+ "segment_info": "Students who haven't practiced and logged into the app after at least 3 days.",
60
+ "recsys_contents": ["workout", "course", "quick_tips"],
61
+ "involve_recsys_result": true,
62
+ "personalization": true,
63
+ "sample_examples": "Header: Keep on going!\nMessage: Get back to playing today. It only takes a few minutes!",
64
+ "model": "gemini-2.5-flash-lite"
65
+ },
66
+ "7": {
67
+ "identifier_column": "user_id",
68
+ "stage": 7,
69
+ "segment_info": "Students who haven't practiced and logged into the app after at least 3 days.",
70
+ "recsys_contents": ["workout", "course", "quick_tips"],
71
+ "involve_recsys_result": true,
72
+ "personalization": true,
73
+ "sample_examples": "Header: Ready to play? 🎸\nMessage: Let's get started. Time for a quick practice session!",
74
+ "model": "gemini-2.5-flash-lite"
75
+ },
76
+ "8": {
77
+ "identifier_column": "user_id",
78
+ "stage": 8,
79
+ "segment_info": "Students who haven't practiced and logged into the app after at least 3 days.",
80
+ "recsys_contents": ["workout", "course", "quick_tips"],
81
+ "involve_recsys_result": true,
82
+ "personalization": true,
83
+ "sample_examples": "Header: Your lesson's waiting. 📥\nMessage: We want to hear you play! Dive in today.",
84
+ "model": "gemini-2.5-flash-lite"
85
+ },
86
+ "9": {
87
+ "identifier_column": "user_id",
88
+ "stage": 9,
89
+ "segment_info": "Students who haven't practiced and logged into the app after at least 3 days.",
90
+ "recsys_contents": ["workout", "course", "quick_tips"],
91
+ "involve_recsys_result": true,
92
+ "personalization": true,
93
+ "sample_examples": "Header: Time for a comeback!\nMessage: We haven't seen you in 25 days. This will help get you back into the groove!",
94
+ "model": "gemini-2.5-flash-lite"
95
+ },
96
+ "10": {
97
+ "identifier_column": "user_id",
98
+ "stage": 10,
99
+ "segment_info": "Students who haven't practiced and logged into the app after at least 3 days.",
100
+ "recsys_contents": ["workout", "course", "quick_tips"],
101
+ "involve_recsys_result": true,
102
+ "personalization": true,
103
+ "sample_examples": "Header: Have you been practicing?\nMessage: You're very talented. We'd love to hear you play again!",
104
+ "model": "gemini-2.5-flash-lite"
105
+ },
106
+ "11": {
107
+ "identifier_column": "user_id",
108
+ "stage": 11,
109
+ "segment_info": "Students who haven't practiced and logged into the app after at least 3 days.",
110
+ "recsys_contents": ["workout", "course", "quick_tips"],
111
+ "involve_recsys_result": true,
112
+ "personalization": true,
113
+ "sample_examples": "Header: We Miss You 😔Message: All your lessons will just be here when you get back!",
114
+ "model": "gemini-2.5-flash-lite"
115
+ }
116
+ }
data/configs/pianote_re_engagement_test.json ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "campaign_name": "UI-Test-Campaign-Re-engagement",
3
+ "brand": "pianote",
4
+ "campaign_instructions": "Keep messages encouraging and motivational. Focus on getting users excited about practicing.",
5
+
6
+ "1": {
7
+ "identifier_column": "user_id",
8
+ "stage": 1,
9
+ "segment_info": "Students who haven't practiced and logged into the app after at least 3 days.",
10
+ "recsys_contents": ["workout", "course", "quick_tips"],
11
+ "involve_recsys_result": true,
12
+ "personalization": true,
13
+ "sample_examples": "Header: Your next lesson is waiting 👇 \n Message: Check it out now and improve your playing!",
14
+ "model": "gemini-2.5-flash-lite"
15
+ },
16
+ "2": {
17
+ "identifier_column": "user_id",
18
+ "stage": 2,
19
+ "segment_info": "Students who haven't practiced and logged into the app after at least 3 days.",
20
+ "recsys_contents": ["workout", "course", "quick_tips"],
21
+ "involve_recsys_result": true,
22
+ "personalization": true,
23
+ "sample_examples": "Header: It's a great day to play 🤩,\n Message: It's been a few days — warm up with a quick lesson!",
24
+ "model": "gemini-2.5-flash-lite"
25
+ },
26
+ "3": {
27
+ "identifier_column": "user_id",
28
+ "stage": 3,
29
+ "segment_info": "Students who haven't practiced and logged into the app after at least 3 days.",
30
+ "recsys_contents": ["workout", "course", "quick_tips"],
31
+ "involve_recsys_result": true,
32
+ "personalization": true,
33
+ "sample_examples": "Header: Practice makes progress 💪, \nMessage: You don't need to be perfect. But you do need practice to reach your goals!",
34
+ "model": "gemini-2.5-flash-lite"
35
+ },
36
+ "4": {
37
+ "identifier_column": "user_id",
38
+ "stage": 4,
39
+ "segment_info": "Students who haven't practiced and logged into the app after at least 3 days.",
40
+ "recsys_contents": ["workout", "course", "quick_tips"],
41
+ "involve_recsys_result": true,
42
+ "personalization": true,
43
+ "sample_examples": "Header: Never stop learning, \nMessage: Take a lesson today and get back on track!",
44
+ "model": "gemini-2.5-flash-lite"
45
+ },
46
+ "5": {
47
+ "identifier_column": "user_id",
48
+ "stage": 5,
49
+ "segment_info": "Students who haven't practiced and logged into the app after at least 3 days.",
50
+ "recsys_contents": ["workout", "course", "quick_tips"],
51
+ "involve_recsys_result": true,
52
+ "personalization": true,
53
+ "sample_examples": "Header: Get back on track ⏱️\nMessage: It's been two weeks since your last practice session. Take a lesson today!",
54
+ "model": "gemini-2.5-flash-lite"
55
+ },
56
+ "6": {
57
+ "identifier_column": "user_id",
58
+ "stage": 6,
59
+ "segment_info": "Students who haven't practiced and logged into the app after at least 3 days.",
60
+ "recsys_contents": ["workout", "course", "quick_tips"],
61
+ "involve_recsys_result": true,
62
+ "personalization": true,
63
+ "sample_examples": "Header: Keep on going!\nMessage: Get back to playing today. It only takes a few minutes!",
64
+ "model": "gemini-2.5-flash-lite"
65
+ },
66
+ "7": {
67
+ "identifier_column": "user_id",
68
+ "stage": 7,
69
+ "segment_info": "Students who haven't practiced and logged into the app after at least 3 days.",
70
+ "recsys_contents": ["workout", "course", "quick_tips"],
71
+ "involve_recsys_result": true,
72
+ "personalization": true,
73
+ "sample_examples": "Header: Ready to play? 🎸\nMessage: Let's get started. Time for a quick practice session!",
74
+ "model": "gemini-2.5-flash-lite"
75
+ },
76
+ "8": {
77
+ "identifier_column": "user_id",
78
+ "stage": 8,
79
+ "segment_info": "Students who haven't practiced and logged into the app after at least 3 days.",
80
+ "recsys_contents": ["workout", "course", "quick_tips"],
81
+ "involve_recsys_result": true,
82
+ "personalization": true,
83
+ "sample_examples": "Header: Your lesson's waiting. 📥\nMessage: We want to hear you play! Dive in today.",
84
+ "model": "gemini-2.5-flash-lite"
85
+ },
86
+ "9": {
87
+ "identifier_column": "user_id",
88
+ "stage": 9,
89
+ "segment_info": "Students who haven't practiced and logged into the app after at least 3 days.",
90
+ "recsys_contents": ["workout", "course", "quick_tips"],
91
+ "involve_recsys_result": true,
92
+ "personalization": true,
93
+ "sample_examples": "Header: Time for a comeback!\nMessage: We haven't seen you in 25 days. This will help get you back into the groove!",
94
+ "model": "gemini-2.5-flash-lite"
95
+ },
96
+ "10": {
97
+ "identifier_column": "user_id",
98
+ "stage": 10,
99
+ "segment_info": "Students who haven't practiced and logged into the app after at least 3 days.",
100
+ "recsys_contents": ["workout", "course", "quick_tips"],
101
+ "involve_recsys_result": true,
102
+ "personalization": true,
103
+ "sample_examples": "Header: Have you been practicing?\nMessage: You're very talented. We'd love to hear you play again!",
104
+ "model": "gemini-2.5-flash-lite"
105
+ },
106
+ "11": {
107
+ "identifier_column": "user_id",
108
+ "stage": 11,
109
+ "segment_info": "Students who haven't practiced and logged into the app after at least 3 days.",
110
+ "recsys_contents": ["workout", "course", "quick_tips"],
111
+ "involve_recsys_result": true,
112
+ "personalization": true,
113
+ "sample_examples": "Header: We Miss You 😔Message: All your lessons will just be here when you get back!",
114
+ "model": "gemini-2.5-flash-lite"
115
+ }
116
+ }
data/configs/singeo_re_engagement_test.json ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "campaign_name": "UI-Test-Campaign-Re-engagement",
3
+ "brand": "singeo",
4
+ "campaign_instructions": "Keep messages encouraging and motivational. Focus on getting users excited about practicing.",
5
+
6
+ "1": {
7
+ "identifier_column": "user_id",
8
+ "stage": 1,
9
+ "segment_info": "Students who haven't practiced and logged into the app after at least 3 days.",
10
+ "recsys_contents": ["workout", "course", "quick_tips"],
11
+ "involve_recsys_result": true,
12
+ "personalization": true,
13
+ "sample_examples": "Header: Your next lesson is waiting 👇 \n Message: Check it out now and improve your singing!",
14
+ "model": "gemini-2.5-flash-lite"
15
+ },
16
+ "2": {
17
+ "identifier_column": "user_id",
18
+ "stage": 2,
19
+ "segment_info": "Students who haven't practiced and logged into the app after at least 3 days.",
20
+ "recsys_contents": ["workout", "course", "quick_tips"],
21
+ "involve_recsys_result": true,
22
+ "personalization": true,
23
+ "sample_examples": "Header: It's a great day to sing 🤩,\n Message: It's been a few days — warm up with a quick lesson!",
24
+ "model": "gemini-2.5-flash-lite"
25
+ },
26
+ "3": {
27
+ "identifier_column": "user_id",
28
+ "stage": 3,
29
+ "segment_info": "Students who haven't practiced and logged into the app after at least 3 days.",
30
+ "recsys_contents": ["workout", "course", "quick_tips"],
31
+ "involve_recsys_result": true,
32
+ "personalization": true,
33
+ "sample_examples": "Header: Practice makes progress 💪, \nMessage: You don't need to be perfect. But you do need practice to reach your goals!",
34
+ "model": "gemini-2.5-flash-lite"
35
+ },
36
+ "4": {
37
+ "identifier_column": "user_id",
38
+ "stage": 4,
39
+ "segment_info": "Students who haven't practiced and logged into the app after at least 3 days.",
40
+ "recsys_contents": ["workout", "course", "quick_tips"],
41
+ "involve_recsys_result": true,
42
+ "personalization": true,
43
+ "sample_examples": "Header: Never stop learning, \nMessage: Take a lesson today and get back on track!",
44
+ "model": "gemini-2.5-flash-lite"
45
+ },
46
+ "5": {
47
+ "identifier_column": "user_id",
48
+ "stage": 5,
49
+ "segment_info": "Students who haven't practiced and logged into the app after at least 3 days.",
50
+ "recsys_contents": ["workout", "course", "quick_tips"],
51
+ "involve_recsys_result": true,
52
+ "personalization": true,
53
+ "sample_examples": "Header: Get back on track ⏱️\nMessage: It's been two weeks since your last practice session. Take a lesson today!",
54
+ "model": "gemini-2.5-flash-lite"
55
+ },
56
+ "6": {
57
+ "identifier_column": "user_id",
58
+ "stage": 6,
59
+ "segment_info": "Students who haven't practiced and logged into the app after at least 3 days.",
60
+ "recsys_contents": ["workout", "course", "quick_tips"],
61
+ "involve_recsys_result": true,
62
+ "personalization": true,
63
+ "sample_examples": "Header: Keep on going!\nMessage: Get back to singing today. It only takes a few minutes!",
64
+ "model": "gemini-2.5-flash-lite"
65
+ },
66
+ "7": {
67
+ "identifier_column": "user_id",
68
+ "stage": 7,
69
+ "segment_info": "Students who haven't practiced and logged into the app after at least 3 days.",
70
+ "recsys_contents": ["workout", "course", "quick_tips"],
71
+ "involve_recsys_result": true,
72
+ "personalization": true,
73
+ "sample_examples": "Header: Ready to play? 🎸\nMessage: Let's get started. Time for a quick practice session!",
74
+ "model": "gemini-2.5-flash-lite"
75
+ },
76
+ "8": {
77
+ "identifier_column": "user_id",
78
+ "stage": 8,
79
+ "segment_info": "Students who haven't practiced and logged into the app after at least 3 days.",
80
+ "recsys_contents": ["workout", "course", "quick_tips"],
81
+ "involve_recsys_result": true,
82
+ "personalization": true,
83
+ "sample_examples": "Header: Your lesson's waiting. 📥\nMessage: We want to hear you sing! Dive in today.",
84
+ "model": "gemini-2.5-flash-lite"
85
+ },
86
+ "9": {
87
+ "identifier_column": "user_id",
88
+ "stage": 9,
89
+ "segment_info": "Students who haven't practiced and logged into the app after at least 3 days.",
90
+ "recsys_contents": ["workout", "course", "quick_tips"],
91
+ "involve_recsys_result": true,
92
+ "personalization": true,
93
+ "sample_examples": "Header: Time for a comeback!\nMessage: We haven't seen you in 25 days. This will help get you back into the groove!",
94
+ "model": "gemini-2.5-flash-lite"
95
+ },
96
+ "10": {
97
+ "identifier_column": "user_id",
98
+ "stage": 10,
99
+ "segment_info": "Students who haven't practiced and logged into the app after at least 3 days.",
100
+ "recsys_contents": ["workout", "course", "quick_tips"],
101
+ "involve_recsys_result": true,
102
+ "personalization": true,
103
+ "sample_examples": "Header: Have you been practicing?\nMessage: You have a lovely voice. We'd love to hear it again!",
104
+ "model": "gemini-2.5-flash-lite"
105
+ },
106
+ "11": {
107
+ "identifier_column": "user_id",
108
+ "stage": 11,
109
+ "segment_info": "Students who haven't practiced and logged into the app after at least 3 days.",
110
+ "recsys_contents": ["workout", "course", "quick_tips"],
111
+ "involve_recsys_result": true,
112
+ "personalization": true,
113
+ "sample_examples": "Header: We Miss You 😔Message: All your lessons will just be here when you get back!",
114
+ "model": "gemini-2.5-flash-lite"
115
+ }
116
+ }
data/experiments/.gitkeep ADDED
File without changes
data/feedback/.gitkeep ADDED
File without changes
data/users/.gitkeep ADDED
File without changes
pages/1_Campaign_Builder.py CHANGED
@@ -1,24 +1,1043 @@
1
  """
2
- Campaign Builder Page - HuggingFace Space Wrapper
3
- Redirects to the actual page in visualization/pages/
4
  """
5
 
 
6
  import sys
7
  from pathlib import Path
 
 
 
 
8
 
9
- # Set up paths
10
- root_dir = Path(__file__).parent.parent
11
- visualization_dir = root_dir / "visualization"
12
 
13
- # Add directories to path
14
- sys.path.insert(0, str(root_dir))
15
- sys.path.insert(0, str(root_dir / "ai_messaging_system_v2"))
16
- sys.path.insert(0, str(visualization_dir))
17
 
18
- # Import and run the actual page
19
- import importlib.util
 
 
 
20
 
21
- page_path = visualization_dir / "pages" / "1_Campaign_Builder.py"
22
- spec = importlib.util.spec_from_file_location("campaign_builder", page_path)
23
- campaign_builder = importlib.util.module_from_spec(spec)
24
- spec.loader.exec_module(campaign_builder)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  """
2
+ Page 1: Campaign Builder
3
+ Configure and run message generation campaigns with A/B testing support.
4
  """
5
 
6
+ import streamlit as st
7
  import sys
8
  from pathlib import Path
9
+ from datetime import datetime
10
+ import json
11
+ import threading
12
+ import shutil
13
 
14
+ # Add root directory to path
15
+ sys.path.insert(0, str(Path(__file__).parent.parent))
 
16
 
17
+ from utils.auth import check_authentication
18
+ from utils.theme import apply_theme, get_brand_emoji
19
+ from utils.data_loader import DataLoader
20
+ from utils.config_manager import ConfigManager
21
 
22
+ from ai_messaging_system_v2.Messaging_system.Permes import Permes
23
+ from ai_messaging_system_v2.configs.config_loader import get_system_config
24
+ from snowflake.snowpark import Session
25
+ from dotenv import load_dotenv
26
+ import os
27
 
28
+ # Load environment variables
29
+ env_path = Path(__file__).parent.parent / '.env'
30
+ if env_path.exists():
31
+ load_dotenv(env_path)
32
+
33
+ # Page configuration
34
+ st.set_page_config(
35
+ page_title="Campaign Builder",
36
+ page_icon="🏗️",
37
+ layout="wide"
38
+ )
39
+
40
+ # Check authentication
41
+ if not check_authentication():
42
+ st.error("🔒 Please login first")
43
+ st.stop()
44
+
45
+ # Initialize utilities
46
+ data_loader = DataLoader()
47
+ config_manager = ConfigManager()
48
+
49
+ # Helper function to create Snowflake session
50
+ def create_snowflake_session() -> Session:
51
+ """Create a Snowflake session using environment variables."""
52
+ conn_params = {
53
+ "user": os.getenv("SNOWFLAKE_USER"),
54
+ "password": os.getenv("SNOWFLAKE_PASSWORD"),
55
+ "account": os.getenv("SNOWFLAKE_ACCOUNT"),
56
+ "role": os.getenv("SNOWFLAKE_ROLE"),
57
+ "database": os.getenv("SNOWFLAKE_DATABASE"),
58
+ "warehouse": os.getenv("SNOWFLAKE_WAREHOUSE"),
59
+ "schema": os.getenv("SNOWFLAKE_SCHEMA"),
60
+ }
61
+ return Session.builder.configs(conn_params).create()
62
+
63
+ # Check if brand is selected
64
+ if "selected_brand" not in st.session_state or not st.session_state.selected_brand:
65
+ st.error("⚠️ Please select a brand from the home page first")
66
+ st.stop()
67
+
68
+ brand = st.session_state.selected_brand
69
+ apply_theme(brand)
70
+
71
+ # Initialize session state
72
+ if "ab_testing_mode" not in st.session_state:
73
+ st.session_state.ab_testing_mode = False
74
+ if "campaign_config" not in st.session_state:
75
+ st.session_state.campaign_config = None
76
+ if "campaign_config_a" not in st.session_state:
77
+ st.session_state.campaign_config_a = None
78
+ if "campaign_config_b" not in st.session_state:
79
+ st.session_state.campaign_config_b = None
80
+ if "selected_user_count" not in st.session_state:
81
+ st.session_state.selected_user_count = 5
82
+ if "generation_complete" not in st.session_state:
83
+ st.session_state.generation_complete = False
84
+ if "show_next_steps" not in st.session_state:
85
+ st.session_state.show_next_steps = False
86
+
87
+ # Page Header
88
+ emoji = get_brand_emoji(brand)
89
+ st.title(f"🏗️ Campaign Builder - {emoji} {brand.title()}")
90
+ st.markdown("**Configure and generate personalized message campaigns**")
91
+
92
+ st.markdown("---")
93
+
94
+ # ============================================================================
95
+ # A/B TESTING TOGGLE
96
+ # ============================================================================
97
+ ab_col1, ab_col2 = st.columns([3, 1])
98
+
99
+ with ab_col1:
100
+ st.subheader("🧪 Experiment Mode")
101
+ st.markdown("Toggle A/B testing to run two experiments in parallel with different configurations.")
102
+
103
+ with ab_col2:
104
+ ab_testing_enabled = st.toggle(
105
+ "Enable A/B Testing",
106
+ value=st.session_state.ab_testing_mode,
107
+ help="Enable to run two experiments (A and B) in parallel"
108
+ )
109
+ st.session_state.ab_testing_mode = ab_testing_enabled
110
+
111
+ st.markdown("---")
112
+
113
+ # ============================================================================
114
+ # HELPER FUNCTIONS FOR CONFIGURATION SECTIONS
115
+ # ============================================================================
116
+
117
+ def render_configuration_section(prefix: str, col_container, initial_config=None):
118
+ """
119
+ Render a configuration section (used for both single mode and A/B mode).
120
+
121
+ Args:
122
+ prefix: Prefix for session state keys (e.g., "single", "a", "b")
123
+ col_container: Streamlit container/column to render in
124
+ initial_config: Initial configuration to load
125
+
126
+ Returns:
127
+ Complete configuration dictionary
128
+ """
129
+ with col_container:
130
+ # Configuration template selection
131
+ st.subheader("📋 Configuration Template")
132
+
133
+ # Get available configurations
134
+ all_configs = config_manager.get_all_configs(brand)
135
+ config_names = list(all_configs.keys())
136
+
137
+ if len(config_names) == 0:
138
+ st.warning("No configurations available. Please check your setup.")
139
+ return None
140
+
141
+ selected_config_name = st.selectbox(
142
+ "Template",
143
+ config_names,
144
+ index=0 if "re_engagement_test" in config_names else 0,
145
+ help="Select a configuration template to start with",
146
+ key=f"{prefix}_config_select"
147
+ )
148
+
149
+ # Load selected configuration
150
+ if selected_config_name == "re_engagement_test":
151
+ loaded_config = config_manager.load_default_config(brand)
152
+ else:
153
+ loaded_config = config_manager.load_custom_config(brand, selected_config_name)
154
+
155
+ if loaded_config:
156
+ st.success(f"✅ Loaded: **{selected_config_name}**")
157
+
158
+ # Show config preview
159
+ # with st.expander("📄 View Configuration Details"):
160
+ # st.json(loaded_config)
161
+
162
+ st.markdown("---")
163
+
164
+ # Campaign settings
165
+ st.subheader("⚙️ Campaign Settings")
166
+
167
+ campaign_name = st.text_input(
168
+ "Campaign Name",
169
+ value=loaded_config.get("campaign_name", f"UI-Campaign-{brand}"),
170
+ help="Name for this campaign",
171
+ key=f"{prefix}_campaign_name"
172
+ )
173
+
174
+ col1, col2 = st.columns(2)
175
+
176
+ with col1:
177
+ campaign_type = st.selectbox(
178
+ "Campaign Type",
179
+ ["re_engagement"],
180
+ help="Type of campaign",
181
+ key=f"{prefix}_campaign_type"
182
+ )
183
+
184
+ with col2:
185
+ num_stages = st.number_input(
186
+ "Number of Stages",
187
+ min_value=1,
188
+ max_value=11,
189
+ value=min(3, len([k for k in loaded_config.keys() if k.isdigit()])),
190
+ help="Number of message stages (1-11)",
191
+ key=f"{prefix}_num_stages"
192
+ )
193
+
194
+ campaign_instructions = st.text_area(
195
+ "Campaign-Wide Instructions (Optional)",
196
+ value=loaded_config.get("campaign_instructions", ""),
197
+ height=80,
198
+ help="Instructions applied to all stages",
199
+ key=f"{prefix}_campaign_instructions"
200
+ )
201
+
202
+ st.markdown("---")
203
+
204
+ # Stage configuration
205
+ st.subheader("📝 Stage Configuration")
206
+
207
+ # Load system config for model options
208
+ system_config = get_system_config()
209
+ available_models = system_config.get("openai_models", []) + system_config.get("google_models", [])
210
+
211
+ stages_config = {}
212
+
213
+ for stage_num in range(1, num_stages + 1):
214
+ with st.expander(f"Stage {stage_num} Configuration", expanded=(stage_num == 1)):
215
+ # Load existing stage config if available
216
+ existing_stage = loaded_config.get(str(stage_num), {})
217
+
218
+ col1, col2 = st.columns(2)
219
+
220
+ with col1:
221
+ model = st.selectbox(
222
+ "LLM Model",
223
+ available_models,
224
+ index=available_models.index(existing_stage.get("model", available_models[0])) if existing_stage.get("model") in available_models else 0,
225
+ key=f"{prefix}_model_{stage_num}",
226
+ help="Language model to use for generation"
227
+ )
228
+
229
+ personalization = st.checkbox(
230
+ "Enable Personalization",
231
+ value=existing_stage.get("personalization", True),
232
+ key=f"{prefix}_personalization_{stage_num}",
233
+ help="Personalize messages based on user profile"
234
+ )
235
+
236
+ involve_recsys = st.checkbox(
237
+ "Include Content Recommendation",
238
+ value=existing_stage.get("involve_recsys_result", True),
239
+ key=f"{prefix}_involve_recsys_{stage_num}",
240
+ help="Include content recommendations in messages"
241
+ )
242
+
243
+ with col2:
244
+ recsys_contents = st.multiselect(
245
+ "Recommendation Types",
246
+ ["workout", "course", "quick_tips", "song"],
247
+ default=existing_stage.get("recsys_contents", ["workout", "course"]),
248
+ key=f"{prefix}_recsys_contents_{stage_num}",
249
+ help="Types of content to recommend",
250
+ disabled=not involve_recsys
251
+ )
252
+
253
+ specific_content_id = st.number_input(
254
+ "Specific Content ID (Optional)",
255
+ min_value=0,
256
+ value=existing_stage.get("specific_content_id", 0) or 0,
257
+ key=f"{prefix}_specific_content_id_{stage_num}",
258
+ help="Force specific content for all users (leave 0 for AI recommendations)"
259
+ )
260
+ specific_content_id = specific_content_id if specific_content_id > 0 else None
261
+
262
+ segment_info = st.text_area(
263
+ "Segment Description",
264
+ value=existing_stage.get("segment_info", f"Users eligible for stage {stage_num}"),
265
+ key=f"{prefix}_segment_info_{stage_num}",
266
+ height=68,
267
+ help="Description of the user segment"
268
+ )
269
+
270
+ instructions = st.text_area(
271
+ "Stage-Specific Instructions (Optional)",
272
+ value=existing_stage.get("instructions", ""),
273
+ key=f"{prefix}_instructions_{stage_num}",
274
+ height=80,
275
+ help="Instructions specific to this stage"
276
+ )
277
+
278
+ sample_example = st.text_area(
279
+ "Example Messages",
280
+ value=existing_stage.get("sample_examples", "Header: Hi!\nMessage: Check this out!"),
281
+ key=f"{prefix}_sample_example_{stage_num}",
282
+ height=80,
283
+ help="Example messages for the LLM to learn from"
284
+ )
285
+
286
+ # Store stage configuration
287
+ stages_config[stage_num] = {
288
+ "stage": stage_num,
289
+ "model": model,
290
+ "personalization": personalization,
291
+ "involve_recsys_result": involve_recsys,
292
+ "recsys_contents": recsys_contents if involve_recsys else [],
293
+ "specific_content_id": specific_content_id,
294
+ "segment_info": segment_info,
295
+ "instructions": instructions,
296
+ "sample_examples": sample_example,
297
+ "identifier_column": "user_id",
298
+ "platform": "push"
299
+ }
300
+
301
+ # Create complete configuration
302
+ complete_config = config_manager.create_config_from_ui(
303
+ brand=brand,
304
+ campaign_name=campaign_name,
305
+ campaign_type=campaign_type,
306
+ num_stages=num_stages,
307
+ campaign_instructions=campaign_instructions,
308
+ stages_config=stages_config
309
+ )
310
+
311
+ # Validation
312
+ is_valid, error_msg = config_manager.validate_config(complete_config)
313
+
314
+ if not is_valid:
315
+ st.error(f"❌ Configuration Error: {error_msg}")
316
+ else:
317
+ st.success("✅ Configuration is valid")
318
+
319
+ return complete_config
320
+
321
+ def render_save_config_section(prefix: str, col_container, config):
322
+ """Render save configuration section."""
323
+ with col_container:
324
+ st.subheader("💾 Save Configuration")
325
+
326
+ with st.form(f"{prefix}_save_config_form"):
327
+ new_config_name = st.text_input(
328
+ "Configuration Name",
329
+ placeholder="my-custom-config",
330
+ help="Enter a name to save this configuration"
331
+ )
332
+
333
+ save_button = st.form_submit_button("💾 Save Configuration", use_container_width=True)
334
+
335
+ if save_button and new_config_name:
336
+ if config:
337
+ # Check if config already exists
338
+ if config_manager.config_exists(brand, new_config_name):
339
+ st.warning(f"⚠️ Configuration '{new_config_name}' already exists. It will be overwritten.")
340
+
341
+ # Save configuration
342
+ if config_manager.save_custom_config(brand, new_config_name, config):
343
+ st.success(f"✅ Configuration saved as '{new_config_name}'")
344
+ else:
345
+ st.error("❌ Failed to save configuration")
346
+ else:
347
+ st.error("❌ No configuration loaded")
348
+
349
+ # ============================================================================
350
+ # RENDER CONFIGURATION SECTIONS BASED ON MODE
351
+ # ============================================================================
352
+
353
+ if st.session_state.ab_testing_mode:
354
+ # A/B Testing Mode - Two Columns
355
+ st.header("🧪 A/B Testing Configuration")
356
+ st.info("Configure two different experiments (A and B) to run in parallel with the same users.")
357
+
358
+ col_a, col_b = st.columns(2)
359
+
360
+ # Initialize A/B configs with defaults if None
361
+ if st.session_state.campaign_config_a is None:
362
+ st.session_state.campaign_config_a = config_manager.load_default_config(brand)
363
+ if st.session_state.campaign_config_b is None:
364
+ st.session_state.campaign_config_b = config_manager.load_default_config(brand)
365
+
366
+ # Column A
367
+ with col_a:
368
+ st.markdown("### 🅰️ Experiment A")
369
+ with st.expander("Configuration A", expanded=True):
370
+ config_a = render_configuration_section("a", st.container(), st.session_state.campaign_config_a)
371
+ if config_a is not None:
372
+ st.session_state.campaign_config_a = config_a
373
+
374
+ with st.expander("Save Configuration A"):
375
+ if st.session_state.campaign_config_a:
376
+ render_save_config_section("a", st.container(), st.session_state.campaign_config_a)
377
+
378
+ # Column B
379
+ with col_b:
380
+ st.markdown("### 🅱️ Experiment B")
381
+ with st.expander("Configuration B", expanded=True):
382
+ config_b = render_configuration_section("b", st.container(), st.session_state.campaign_config_b)
383
+ if config_b is not None:
384
+ st.session_state.campaign_config_b = config_b
385
+
386
+ with st.expander("Save Configuration B"):
387
+ if st.session_state.campaign_config_b:
388
+ render_save_config_section("b", st.container(), st.session_state.campaign_config_b)
389
+
390
+ else:
391
+ # Single Mode
392
+ # Initialize config with default if None
393
+ if st.session_state.campaign_config is None:
394
+ st.session_state.campaign_config = config_manager.load_default_config(brand)
395
+
396
+ with st.expander("📋 Configuration", expanded=True):
397
+ config = render_configuration_section("single", st.container(), st.session_state.campaign_config)
398
+ if config is not None:
399
+ st.session_state.campaign_config = config
400
+
401
+ # Save section outside the main config expander
402
+ with st.expander("💾 Save Configuration"):
403
+ if st.session_state.campaign_config:
404
+ render_save_config_section("single", st.container(), st.session_state.campaign_config)
405
+
406
+ st.markdown("---")
407
+
408
+ # ============================================================================
409
+ # USER SELECTION SECTION
410
+ # ============================================================================
411
+
412
+ with st.expander("👥 User Selection", expanded=True):
413
+ st.subheader("👥 Select Number of Users")
414
+
415
+ # Check if brand users file exists
416
+ has_brand_users = data_loader.has_brand_users(brand)
417
+
418
+ if not has_brand_users:
419
+ st.error(f"❌ No user file found for {brand}. Please ensure {brand}_users.csv exists in visualization/data/UI_users/")
420
+ else:
421
+ # Get total available users
422
+ total_users = data_loader.get_brand_user_count(brand)
423
+
424
+ st.info(f"ℹ️ {total_users} users available for {brand}")
425
+
426
+ st.markdown("""
427
+ Choose how many users you want to experiment with. Users will be **randomly sampled** each time you generate messages.
428
+
429
+ For quick experimentation, we recommend **5-10 users**.
430
+ """)
431
+
432
+ col1, col2 = st.columns([2, 3])
433
+
434
+ with col1:
435
+ user_count = st.number_input(
436
+ "Number of Users",
437
+ min_value=1,
438
+ max_value=min(25, total_users),
439
+ value=st.session_state.selected_user_count,
440
+ help="Select 1-25 users for experimentation"
441
+ )
442
+
443
+ # Update session state
444
+ st.session_state.selected_user_count = user_count
445
+
446
+ with col2:
447
+ st.markdown("")
448
+ st.markdown("")
449
+ st.markdown(f"**Selected:** {user_count} user{'s' if user_count != 1 else ''}")
450
+ st.markdown(f"**Available:** {total_users} total users")
451
+
452
+ st.markdown("---")
453
+
454
+ # Preview sample button
455
+ if st.button("👀 Preview Random Sample", use_container_width=False):
456
+ sample_df = data_loader.sample_users_randomly(brand, user_count)
457
+ if sample_df is not None:
458
+ st.success(f"✅ Sampled {len(sample_df)} random users")
459
+ st.dataframe(sample_df, use_container_width=True)
460
+
461
+ st.markdown("---")
462
+
463
+ # ============================================================================
464
+ # GENERATION SECTION
465
+ # ============================================================================
466
+
467
+ def archive_existing_messages(campaign_name: str):
468
+ """Archive existing messages.csv to a timestamped file."""
469
+ ui_output_path = data_loader.get_ui_output_path()
470
+ messages_file = ui_output_path / "messages.csv"
471
+
472
+ if messages_file.exists():
473
+ timestamp = datetime.now().strftime('%Y%m%d_%H%M')
474
+ archive_name = f"messages_{campaign_name}_{timestamp}.csv"
475
+ archive_path = ui_output_path / archive_name
476
+
477
+ try:
478
+ shutil.copy2(messages_file, archive_path)
479
+ st.info(f"📦 Archived existing messages to: {archive_name}")
480
+ return True
481
+ except Exception as e:
482
+ st.warning(f"⚠️ Could not archive existing messages: {e}")
483
+ return False
484
+ return True
485
+
486
+ def generate_messages_for_experiment(
487
+ experiment_name: str,
488
+ config: dict,
489
+ sampled_users_df,
490
+ output_suffix: str = None,
491
+ progress_container=None
492
+ ):
493
+ """
494
+ Generate messages for a single experiment.
495
+
496
+ Args:
497
+ experiment_name: Name of the experiment (e.g., "A" or "B")
498
+ config: Configuration dictionary
499
+ sampled_users_df: DataFrame of sampled users
500
+ output_suffix: Optional suffix for output filename (e.g., "_a" or "_b")
501
+ progress_container: Streamlit container for progress updates
502
+
503
+ Returns:
504
+ tuple: (success, total_messages_generated)
505
+ """
506
+ if progress_container is None:
507
+ progress_container = st
508
+
509
+ with progress_container:
510
+ st.markdown(f"### 📊 Experiment {experiment_name} Progress")
511
+
512
+ system_config = get_system_config()
513
+
514
+ # Setup Permes for message generation
515
+ permes = Permes()
516
+ original_output_file = permes.UI_OUTPUT_FILE
517
+ ui_experiment_id = None
518
+
519
+ if output_suffix:
520
+ # Generate experiment ID for timestamping and naming
521
+ experiment_timestamp = datetime.now().strftime('%Y%m%d_%H%M')
522
+ ui_experiment_id = f"messages_{output_suffix}_{brand}_{experiment_timestamp}"
523
+ # Keep this for backward compatibility (though it won't be used anymore)
524
+ permes.UI_OUTPUT_FILE = f"{ui_experiment_id}.csv"
525
+ st.info(f"💾 Results will be saved to: {ui_experiment_id}.csv")
526
+
527
+ # Get campaign-level configurations
528
+ campaign_name = config.get("campaign_name", "UI-Campaign")
529
+ campaign_instructions = config.get("campaign_instructions", None)
530
+ num_stages = len([k for k in config.keys() if k.isdigit()])
531
+
532
+ # Generate messages for each stage
533
+ total_messages_generated = 0
534
+ generation_successful = True
535
+
536
+ for stage in range(1, num_stages + 1):
537
+ st.markdown(f"#### Stage {stage} of {num_stages}")
538
+
539
+ progress_bar = st.progress(0)
540
+ status_text = st.empty()
541
+
542
+ status_text.text(f"Generating messages for stage {stage}...")
543
+
544
+ session = None
545
+ try:
546
+ session = create_snowflake_session()
547
+ progress_bar.progress(10)
548
+
549
+ # Get stage configuration
550
+ stage_config = config.get(str(stage), {})
551
+
552
+ if not stage_config:
553
+ status_text.error(f"❌ Stage {stage}: Configuration not found")
554
+ generation_successful = False
555
+ if session:
556
+ try:
557
+ session.close()
558
+ except:
559
+ pass
560
+ continue
561
+
562
+ # Extract stage parameters
563
+ model = stage_config.get("model", "gemini-2.5-flash-lite")
564
+ segment_info = stage_config.get("segment_info", "")
565
+ recsys_contents = stage_config.get("recsys_contents", [])
566
+ involve_recsys_result = stage_config.get("involve_recsys_result", True)
567
+ personalization = stage_config.get("personalization", True)
568
+ sample_example = stage_config.get("sample_examples", "")
569
+ per_message_instructions = stage_config.get("instructions", None)
570
+ specific_content_id = stage_config.get("specific_content_id", None)
571
+
572
+ progress_bar.progress(20)
573
+
574
+ # Call Permes to generate messages
575
+ users_message = permes.create_personalize_messages(
576
+ session=session,
577
+ model=model,
578
+ users=sampled_users_df.copy(),
579
+ brand=brand,
580
+ config_file=system_config,
581
+ segment_info=segment_info,
582
+ involve_recsys_result=involve_recsys_result,
583
+ identifier_column="user_id",
584
+ recsys_contents=recsys_contents,
585
+ sample_example=sample_example,
586
+ campaign_name=campaign_name,
587
+ personalization=personalization,
588
+ stage=stage,
589
+ test_mode=False,
590
+ mode="ui",
591
+ campaign_instructions=campaign_instructions,
592
+ per_message_instructions=per_message_instructions,
593
+ specific_content_id=specific_content_id,
594
+ ui_experiment_id=ui_experiment_id
595
+ )
596
+
597
+ progress_bar.progress(100)
598
+
599
+ if users_message is not None and len(users_message) > 0:
600
+ total_messages_generated += len(users_message)
601
+ status_text.success(
602
+ f"✅ Stage {stage}: Generated {len(users_message)} messages"
603
+ )
604
+ else:
605
+ status_text.warning(f"⚠️ Stage {stage}: No messages generated")
606
+ generation_successful = False
607
+
608
+ except Exception as e:
609
+ progress_bar.progress(0)
610
+ status_text.error(f"❌ Stage {stage}: Error - {str(e)}")
611
+ generation_successful = False
612
+
613
+ finally:
614
+ if session:
615
+ try:
616
+ session.close()
617
+ except:
618
+ pass
619
+
620
+ # Restore original output file name
621
+ permes.UI_OUTPUT_FILE = original_output_file
622
+
623
+ return generation_successful, total_messages_generated
624
+
625
+ def generate_messages_threaded(
626
+ experiment_name: str,
627
+ config: dict,
628
+ sampled_users_df,
629
+ output_suffix: str,
630
+ brand: str,
631
+ experiment_timestamp: str = None
632
+ ):
633
+ """
634
+ Generate messages for threading (no UI updates).
635
+
636
+ Args:
637
+ experiment_name: Name of the experiment (e.g., "A" or "B")
638
+ config: Configuration dictionary
639
+ sampled_users_df: DataFrame of sampled users
640
+ output_suffix: Suffix for output filename (e.g., "a" or "b")
641
+ brand: Brand name
642
+ experiment_timestamp: Shared timestamp for A/B test pairs
643
+
644
+ Returns:
645
+ dict: Results with 'success', 'total_messages', 'error', 'stages_completed'
646
+ """
647
+ result = {
648
+ 'success': False,
649
+ 'total_messages': 0,
650
+ 'error': None,
651
+ 'stages_completed': 0,
652
+ 'stage_details': []
653
+ }
654
+
655
+ try:
656
+ system_config = get_system_config()
657
+
658
+ # Setup Permes for message generation
659
+ permes = Permes()
660
+ original_output_file = permes.UI_OUTPUT_FILE
661
+
662
+ # Use provided timestamp or generate new one
663
+ if experiment_timestamp is None:
664
+ experiment_timestamp = datetime.now().strftime('%Y%m%d_%H%M')
665
+
666
+ # Create experiment ID for UI mode
667
+ ui_experiment_id = f"messages_{output_suffix}_{brand}_{experiment_timestamp}"
668
+ # Keep this for backward compatibility (though it won't be used anymore)
669
+ permes.UI_OUTPUT_FILE = f"{ui_experiment_id}.csv"
670
+
671
+ # Get campaign-level configurations
672
+ campaign_name = config.get("campaign_name", "UI-Campaign")
673
+ campaign_instructions = config.get("campaign_instructions", None)
674
+ num_stages = len([k for k in config.keys() if k.isdigit()])
675
+
676
+ # Generate messages for each stage
677
+ for stage in range(1, num_stages + 1):
678
+ session = None
679
+ try:
680
+ session = create_snowflake_session()
681
+
682
+ # Get stage configuration
683
+ stage_config = config.get(str(stage), {})
684
+
685
+ if not stage_config:
686
+ result['stage_details'].append({
687
+ 'stage': stage,
688
+ 'status': 'error',
689
+ 'message': 'Configuration not found',
690
+ 'count': 0
691
+ })
692
+ continue
693
+
694
+ # Extract stage parameters
695
+ model = stage_config.get("model", "gemini-2.5-flash-lite")
696
+ segment_info = stage_config.get("segment_info", "")
697
+ recsys_contents = stage_config.get("recsys_contents", [])
698
+ involve_recsys_result = stage_config.get("involve_recsys_result", True)
699
+ personalization = stage_config.get("personalization", True)
700
+ sample_example = stage_config.get("sample_examples", "")
701
+ per_message_instructions = stage_config.get("instructions", None)
702
+ specific_content_id = stage_config.get("specific_content_id", None)
703
+
704
+ # Call Permes to generate messages
705
+ users_message = permes.create_personalize_messages(
706
+ session=session,
707
+ model=model,
708
+ users=sampled_users_df.copy(),
709
+ brand=brand,
710
+ config_file=system_config,
711
+ segment_info=segment_info,
712
+ involve_recsys_result=involve_recsys_result,
713
+ identifier_column="user_id",
714
+ recsys_contents=recsys_contents,
715
+ sample_example=sample_example,
716
+ campaign_name=campaign_name,
717
+ personalization=personalization,
718
+ stage=stage,
719
+ test_mode=False,
720
+ mode="ui",
721
+ campaign_instructions=campaign_instructions,
722
+ per_message_instructions=per_message_instructions,
723
+ specific_content_id=specific_content_id,
724
+ ui_experiment_id=ui_experiment_id
725
+ )
726
+
727
+ if users_message is not None and len(users_message) > 0:
728
+ result['total_messages'] += len(users_message)
729
+ result['stages_completed'] += 1
730
+ result['stage_details'].append({
731
+ 'stage': stage,
732
+ 'status': 'success',
733
+ 'message': f'Generated {len(users_message)} messages',
734
+ 'count': len(users_message)
735
+ })
736
+ else:
737
+ result['stage_details'].append({
738
+ 'stage': stage,
739
+ 'status': 'warning',
740
+ 'message': 'No messages generated',
741
+ 'count': 0
742
+ })
743
+
744
+ except Exception as e:
745
+ result['stage_details'].append({
746
+ 'stage': stage,
747
+ 'status': 'error',
748
+ 'message': str(e),
749
+ 'count': 0
750
+ })
751
+
752
+ finally:
753
+ if session:
754
+ try:
755
+ session.close()
756
+ except:
757
+ pass
758
+
759
+ # Restore original output file name
760
+ permes.UI_OUTPUT_FILE = original_output_file
761
+
762
+ # Mark as successful if at least one stage completed
763
+ result['success'] = result['stages_completed'] > 0
764
+
765
+ except Exception as e:
766
+ result['error'] = str(e)
767
+ result['success'] = False
768
+
769
+ return result
770
+
771
+ with st.expander("🚀 Generate Messages", expanded=True):
772
+ st.subheader("🚀 Generate Messages")
773
+
774
+ # Check prerequisites
775
+ if st.session_state.ab_testing_mode:
776
+ config_ready = (st.session_state.campaign_config_a is not None and
777
+ st.session_state.campaign_config_b is not None)
778
+ else:
779
+ config_ready = st.session_state.campaign_config is not None
780
+
781
+ users_ready = data_loader.has_brand_users(brand)
782
+ user_count_selected = st.session_state.get("selected_user_count", 5)
783
+
784
+ # Status checks
785
+ col1, col2, col3 = st.columns(3)
786
+
787
+ with col1:
788
+ if config_ready:
789
+ st.success("✅ Configuration ready")
790
+ else:
791
+ st.error("❌ Configuration not ready")
792
+
793
+ with col2:
794
+ if users_ready:
795
+ st.success(f"✅ {user_count_selected} users selected")
796
+ else:
797
+ st.error("❌ No users available")
798
+
799
+ with col3:
800
+ if st.session_state.ab_testing_mode:
801
+ st.metric("Mode", "A/B Testing")
802
+ else:
803
+ st.metric("Mode", "Single")
804
+
805
+ st.markdown("---")
806
+
807
+ if not config_ready:
808
+ st.info("👆 Please complete the configuration first")
809
+ elif not users_ready:
810
+ st.error("❌ No users available. Please check that user files exist.")
811
+ else:
812
+ # Generation settings
813
+ st.subheader("Generation Settings")
814
+
815
+ if st.session_state.ab_testing_mode:
816
+ num_stages_a = len([k for k in st.session_state.campaign_config_a.keys() if k.isdigit()])
817
+ num_stages_b = len([k for k in st.session_state.campaign_config_b.keys() if k.isdigit()])
818
+ st.info(f"ℹ️ Will generate messages for **{num_stages_a} stages (A)** and **{num_stages_b} stages (B)** in parallel")
819
+ else:
820
+ num_stages = len([k for k in st.session_state.campaign_config.keys() if k.isdigit()])
821
+ st.info(f"ℹ️ Will generate messages for **all {num_stages} stages** configured in the campaign")
822
+
823
+ # Clear previous results option
824
+ clear_previous = st.checkbox(
825
+ "Clear previous UI output before generating",
826
+ value=True,
827
+ help="Clear previous message results before generating new ones"
828
+ )
829
+
830
+ st.markdown("---")
831
+
832
+ # Generate button
833
+ if st.button("🚀 Start Generation", use_container_width=True, type="primary"):
834
+ # Reset next steps flag
835
+ st.session_state.show_next_steps = False
836
+ st.session_state.generation_complete = False
837
+
838
+ # Sample users randomly from the brand's user file
839
+ with st.spinner(f"Sampling {user_count_selected} random users from {brand}_users.csv..."):
840
+ sampled_users_df = data_loader.sample_users_randomly(brand, user_count_selected)
841
+
842
+ if sampled_users_df is None or len(sampled_users_df) == 0:
843
+ st.error("❌ Failed to sample users. Please check your user files.")
844
+ st.stop()
845
+
846
+ st.success(f"✅ Sampled {len(sampled_users_df)} users successfully")
847
+
848
+ # Archive existing messages before starting
849
+ if st.session_state.ab_testing_mode:
850
+ campaign_name_a = st.session_state.campaign_config_a.get("campaign_name", "Experiment_A")
851
+ archive_existing_messages(campaign_name_a)
852
+ else:
853
+ campaign_name = st.session_state.campaign_config.get("campaign_name", "Campaign")
854
+ archive_existing_messages(campaign_name)
855
+
856
+ # Clear previous output if requested
857
+ if clear_previous:
858
+ data_loader.clear_ui_output()
859
+
860
+ st.markdown("---")
861
+
862
+ if st.session_state.ab_testing_mode:
863
+ # A/B Testing Mode - Parallel Execution
864
+ st.markdown("## 🧪 Running A/B Tests in Parallel")
865
+ st.info("⏳ Generating messages for both experiments in parallel. This may take a few minutes...")
866
+
867
+ # Read configs from session state BEFORE starting threads
868
+ # (session state is not thread-safe when accessed from worker threads)
869
+ config_a = st.session_state.campaign_config_a
870
+ config_b = st.session_state.campaign_config_b
871
+
872
+ # Generate shared timestamp for both experiments (needed for A/B detection)
873
+ shared_timestamp = datetime.now().strftime('%Y%m%d_%H%M')
874
+
875
+ # Store results
876
+ results = {"a": None, "b": None}
877
+
878
+ def run_experiment_a(config, users_df, brand_name, timestamp):
879
+ results["a"] = generate_messages_threaded(
880
+ "A",
881
+ config,
882
+ users_df,
883
+ "a",
884
+ brand_name,
885
+ timestamp
886
+ )
887
+
888
+ def run_experiment_b(config, users_df, brand_name, timestamp):
889
+ results["b"] = generate_messages_threaded(
890
+ "B",
891
+ config,
892
+ users_df,
893
+ "b",
894
+ brand_name,
895
+ timestamp
896
+ )
897
+
898
+ # Start both threads with configs as arguments
899
+ thread_a = threading.Thread(target=run_experiment_a, args=(config_a, sampled_users_df, brand, shared_timestamp))
900
+ thread_b = threading.Thread(target=run_experiment_b, args=(config_b, sampled_users_df, brand, shared_timestamp))
901
+
902
+ thread_a.start()
903
+ thread_b.start()
904
+
905
+ # Show spinner while waiting
906
+ with st.spinner("Waiting for both experiments to complete..."):
907
+ thread_a.join()
908
+ thread_b.join()
909
+
910
+ st.markdown("---")
911
+
912
+ # Display results
913
+ st.markdown("## 📊 A/B Test Results")
914
+
915
+ col_a_result, col_b_result = st.columns(2)
916
+
917
+ # Experiment A Results
918
+ with col_a_result:
919
+ st.markdown("### 🅰️ Experiment A")
920
+ if results["a"]:
921
+ result_a = results["a"]
922
+ if result_a['error']:
923
+ st.error(f"❌ Error: {result_a['error']}")
924
+ elif result_a['success']:
925
+ st.success(f"✅ Generated {result_a['total_messages']} messages")
926
+ st.metric("Stages Completed", f"{result_a['stages_completed']}")
927
+
928
+ # Show stage details
929
+ with st.expander("View Stage Details"):
930
+ for stage_detail in result_a['stage_details']:
931
+ if stage_detail['status'] == 'success':
932
+ st.success(f"Stage {stage_detail['stage']}: {stage_detail['message']}")
933
+ elif stage_detail['status'] == 'warning':
934
+ st.warning(f"Stage {stage_detail['stage']}: {stage_detail['message']}")
935
+ elif stage_detail['status'] == 'error':
936
+ st.error(f"Stage {stage_detail['stage']}: {stage_detail['message']}")
937
+ else:
938
+ st.warning(f"⚠️ Generation completed with issues")
939
+ st.write(f"Messages: {result_a['total_messages']}")
940
+ else:
941
+ st.error("❌ No results available")
942
+
943
+ # Experiment B Results
944
+ with col_b_result:
945
+ st.markdown("### 🅱️ Experiment B")
946
+ if results["b"]:
947
+ result_b = results["b"]
948
+ if result_b['error']:
949
+ st.error(f"❌ Error: {result_b['error']}")
950
+ elif result_b['success']:
951
+ st.success(f"✅ Generated {result_b['total_messages']} messages")
952
+ st.metric("Stages Completed", f"{result_b['stages_completed']}")
953
+
954
+ # Show stage details
955
+ with st.expander("View Stage Details"):
956
+ for stage_detail in result_b['stage_details']:
957
+ if stage_detail['status'] == 'success':
958
+ st.success(f"Stage {stage_detail['stage']}: {stage_detail['message']}")
959
+ elif stage_detail['status'] == 'warning':
960
+ st.warning(f"Stage {stage_detail['stage']}: {stage_detail['message']}")
961
+ elif stage_detail['status'] == 'error':
962
+ st.error(f"Stage {stage_detail['stage']}: {stage_detail['message']}")
963
+ else:
964
+ st.warning(f"⚠️ Generation completed with issues")
965
+ st.write(f"Messages: {result_b['total_messages']}")
966
+ else:
967
+ st.error("❌ No results available")
968
+
969
+ # Create experiment record
970
+ experiment_id = f"{brand}_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
971
+ st.session_state.current_experiment_id = experiment_id
972
+
973
+ # Mark generation as complete
974
+ st.session_state.generation_complete = True
975
+ st.session_state.show_next_steps = True
976
+
977
+ else:
978
+ # Single Mode
979
+ success, total = generate_messages_for_experiment(
980
+ "Single",
981
+ st.session_state.campaign_config,
982
+ sampled_users_df,
983
+ output_suffix=None,
984
+ progress_container=st.container()
985
+ )
986
+
987
+ st.markdown("---")
988
+
989
+ if success:
990
+ st.success(f"✅ Generation complete! Generated {total} total messages.")
991
+ else:
992
+ st.warning("⚠️ Generation completed with some errors. Please check the output above.")
993
+
994
+ # Create experiment record
995
+ experiment_id = f"{brand}_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
996
+ st.session_state.current_experiment_id = experiment_id
997
+
998
+ # Mark generation as complete
999
+ st.session_state.generation_complete = True
1000
+ st.session_state.show_next_steps = True
1001
+
1002
+ # Show next steps outside the generation button block (persists across reruns)
1003
+ if st.session_state.get("show_next_steps", False):
1004
+ st.markdown("---")
1005
+ st.subheader("📋 Next Steps")
1006
+
1007
+ col1, col2 = st.columns(2)
1008
+
1009
+ with col1:
1010
+ if st.button("👀 View Messages", use_container_width=True, key="view_messages_after_gen"):
1011
+ st.switch_page("pages/2_Message_Viewer.py")
1012
+
1013
+ with col2:
1014
+ if st.button("📊 View Analytics", use_container_width=True, key="view_analytics_after_gen"):
1015
+ st.switch_page("pages/4_Analytics.py")
1016
+
1017
+ # Show existing messages if any
1018
+ existing_messages = data_loader.load_generated_messages()
1019
+
1020
+ if existing_messages is not None and len(existing_messages) > 0:
1021
+ st.markdown("---")
1022
+ st.subheader("📨 Existing Messages")
1023
+
1024
+ stats = data_loader.get_message_stats()
1025
+
1026
+ col1, col2, col3 = st.columns(3)
1027
+
1028
+ with col1:
1029
+ st.metric("Total Messages", stats['total_messages'])
1030
+
1031
+ with col2:
1032
+ st.metric("Total Users", stats['total_users'])
1033
+
1034
+ with col3:
1035
+ st.metric("Total Stages", stats['total_stages'])
1036
+
1037
+ if st.button("🗑️ Clear All Messages", use_container_width=False):
1038
+ data_loader.clear_ui_output()
1039
+ st.success("✅ All messages cleared")
1040
+ st.rerun()
1041
+
1042
+ st.markdown("---")
1043
+ st.markdown("**💡 Tip:** Use A/B testing to compare different configurations with the same users!")
pages/2_Message_Viewer.py CHANGED
@@ -1,24 +1,812 @@
1
  """
2
- Message Viewer Page - HuggingFace Space Wrapper
3
- Redirects to the actual page in visualization/pages/
 
4
  """
5
 
 
6
  import sys
7
  from pathlib import Path
 
 
 
 
8
 
9
- # Set up paths
10
- root_dir = Path(__file__).parent.parent
11
- visualization_dir = root_dir / "visualization"
12
 
13
- # Add directories to path
14
- sys.path.insert(0, str(root_dir))
15
- sys.path.insert(0, str(root_dir / "ai_messaging_system_v2"))
16
- sys.path.insert(0, str(visualization_dir))
17
 
18
- # Import and run the actual page
19
- import importlib.util
 
 
 
 
20
 
21
- page_path = visualization_dir / "pages" / "2_Message_Viewer.py"
22
- spec = importlib.util.spec_from_file_location("message_viewer", page_path)
23
- message_viewer = importlib.util.module_from_spec(spec)
24
- spec.loader.exec_module(message_viewer)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  """
2
+ Page 2: Message Viewer
3
+ Browse, search, and evaluate generated messages with feedback system.
4
+ Supports A/B testing mode for comparing two experiments side-by-side.
5
  """
6
 
7
+ import streamlit as st
8
  import sys
9
  from pathlib import Path
10
+ import json
11
+ import pandas as pd
12
+ import re
13
+ from datetime import datetime
14
 
15
+ # Add root directory to path
16
+ sys.path.insert(0, str(Path(__file__).parent.parent))
 
17
 
18
+ from utils.auth import check_authentication
19
+ from utils.theme import apply_theme, get_brand_emoji, create_badge
20
+ from utils.data_loader import DataLoader
21
+ from utils.feedback_manager import FeedbackManager
22
 
23
+ # Page configuration
24
+ st.set_page_config(
25
+ page_title="Message Viewer",
26
+ page_icon="👀",
27
+ layout="wide"
28
+ )
29
 
30
+ # Check authentication
31
+ if not check_authentication():
32
+ st.error("🔒 Please login first")
33
+ st.stop()
34
+
35
+ # Initialize utilities
36
+ data_loader = DataLoader()
37
+ feedback_manager = FeedbackManager()
38
+
39
+ # Check if brand is selected
40
+ if "selected_brand" not in st.session_state or not st.session_state.selected_brand:
41
+ st.error("⚠️ Please select a brand from the home page first")
42
+ st.stop()
43
+
44
+ brand = st.session_state.selected_brand
45
+ apply_theme(brand)
46
+
47
+ # Helper functions for A/B testing
48
+ def detect_ab_testing_files():
49
+ """
50
+ Detect if there are A/B testing files in the UI output directory.
51
+ Returns (is_ab_mode, messages_a_path, messages_b_path, timestamp)
52
+ """
53
+ ui_output_path = data_loader.get_ui_output_path()
54
+
55
+ # Check session state first
56
+ if st.session_state.get('ab_testing_mode', False):
57
+ # Look for files with pattern: messages_a_* and messages_b_*
58
+ a_files = list(ui_output_path.glob('messages_a_*.csv'))
59
+ b_files = list(ui_output_path.glob('messages_b_*.csv'))
60
+
61
+ if a_files and b_files:
62
+ # Find matching pairs by timestamp
63
+ for a_file in a_files:
64
+ # Extract timestamp from filename: messages_a_brand_variant_timestamp.csv
65
+ match_a = re.search(r'messages_a_.*_(\d{8}_\d{4})\.csv', a_file.name)
66
+ if match_a:
67
+ timestamp = match_a.group(1)
68
+ # Look for matching b file
69
+ for b_file in b_files:
70
+ if timestamp in b_file.name:
71
+ return True, a_file, b_file, timestamp
72
+
73
+ # Also detect if files exist even if session state not set
74
+ ui_output_path = data_loader.get_ui_output_path()
75
+ a_files = list(ui_output_path.glob('messages_a_*.csv'))
76
+ b_files = list(ui_output_path.glob('messages_b_*.csv'))
77
+
78
+ if a_files and b_files:
79
+ # Auto-detect matching pairs
80
+ for a_file in a_files:
81
+ match_a = re.search(r'messages_a_.*_(\d{8}_\d{4})\.csv', a_file.name)
82
+ if match_a:
83
+ timestamp = match_a.group(1)
84
+ for b_file in b_files:
85
+ if timestamp in b_file.name:
86
+ return True, a_file, b_file, timestamp
87
+
88
+ return False, None, None, None
89
+
90
+
91
+ def load_ab_test_messages(file_path):
92
+ """Load messages from A/B test file."""
93
+ try:
94
+ return pd.read_csv(file_path, encoding='utf-8-sig')
95
+ except Exception as e:
96
+ st.error(f"Error loading A/B test file: {e}")
97
+ return None
98
+
99
+
100
+ def extract_experiment_id_from_filename(file_path):
101
+ """Extract experiment ID from A/B test filename."""
102
+ # Pattern: messages_a_brand_variant_timestamp.csv or messages_b_brand_variant_timestamp.csv
103
+ # Use the full filename (without .csv) to ensure A and B experiments have different IDs
104
+ filename = file_path.stem # Removes .csv extension automatically
105
+ return filename
106
+
107
+
108
+ # Page Header
109
+ emoji = get_brand_emoji(brand)
110
+ st.title(f"👀 Message Viewer - {emoji} {brand.title()}")
111
+ st.markdown("**Browse and evaluate generated messages**")
112
+
113
+ # Detect A/B testing mode
114
+ is_ab_mode, messages_a_path, messages_b_path, ab_timestamp = detect_ab_testing_files()
115
+
116
+ if is_ab_mode:
117
+ st.info(f"🔬 **A/B Testing Mode Active** - Comparing two experiments side-by-side (Timestamp: {ab_timestamp})")
118
+
119
+ st.markdown("---")
120
+
121
+ # Load messages based on mode
122
+ if is_ab_mode:
123
+ # Load A/B test messages
124
+ messages_a_df = load_ab_test_messages(messages_a_path)
125
+ messages_b_df = load_ab_test_messages(messages_b_path)
126
+
127
+ if messages_a_df is None or len(messages_a_df) == 0 or messages_b_df is None or len(messages_b_df) == 0:
128
+ st.warning("⚠️ Error loading A/B test messages. Please check the files.")
129
+ if st.button("🏗️ Go to Campaign Builder"):
130
+ st.switch_page("pages/1_Campaign_Builder.py")
131
+ st.stop()
132
+
133
+ # Extract experiment IDs from filenames
134
+ experiment_a_id = extract_experiment_id_from_filename(messages_a_path)
135
+ experiment_b_id = extract_experiment_id_from_filename(messages_b_path)
136
+
137
+ messages_df = None # Not used in AB mode
138
+ else:
139
+ # Load regular messages
140
+ messages_df = data_loader.load_generated_messages()
141
+
142
+ if messages_df is None or len(messages_df) == 0:
143
+ st.warning("⚠️ No messages found. Please generate messages first in Campaign Builder.")
144
+ if st.button("🏗️ Go to Campaign Builder"):
145
+ st.switch_page("pages/1_Campaign_Builder.py")
146
+ st.stop()
147
+
148
+ # Get experiment ID from session state or create one
149
+ if "current_experiment_id" not in st.session_state or not st.session_state.current_experiment_id:
150
+ # Create experiment ID based on campaign name and timestamp
151
+ if 'campaign_name' in messages_df.columns and len(messages_df) > 0:
152
+ campaign_name = messages_df['campaign_name'].iloc[0]
153
+ experiment_id = campaign_name.replace(" ", "_")
154
+ else:
155
+ experiment_id = f"{brand}_experiment"
156
+
157
+ st.session_state.current_experiment_id = experiment_id
158
+
159
+ experiment_id = st.session_state.current_experiment_id
160
+ messages_a_df = None
161
+ messages_b_df = None
162
+ experiment_a_id = None
163
+ experiment_b_id = None
164
+
165
+ # Sidebar - Filters and Settings
166
+ with st.sidebar:
167
+ st.header("🔍 Filters & Settings")
168
+
169
+ # View mode
170
+ if is_ab_mode:
171
+ st.markdown("**View Mode (applies to both experiments)**")
172
+ view_mode = st.radio(
173
+ "View Mode",
174
+ ["User-Centric", "Stage-Centric"],
175
+ help="User-Centric: All stages for each user\nStage-Centric: All users for each stage"
176
+ )
177
+
178
+ st.markdown("---")
179
+
180
+ # Stage filter
181
+ if is_ab_mode:
182
+ # Get stages from both dataframes
183
+ stages_a = sorted(messages_a_df['stage'].unique()) if 'stage' in messages_a_df.columns else []
184
+ stages_b = sorted(messages_b_df['stage'].unique()) if 'stage' in messages_b_df.columns else []
185
+ available_stages = sorted(list(set(stages_a + stages_b)))
186
+ else:
187
+ available_stages = sorted(messages_df['stage'].unique()) if 'stage' in messages_df.columns else []
188
+
189
+ selected_stages = st.multiselect(
190
+ "Filter by Stage",
191
+ available_stages,
192
+ default=available_stages,
193
+ help="Select specific stages to view" + (" (applies to both experiments)" if is_ab_mode else "")
194
+ )
195
+
196
+ # Search
197
+ search_term = st.text_input(
198
+ "🔎 Search Messages",
199
+ placeholder="Enter keyword...",
200
+ help="Search for specific keywords in messages" + (" (applies to both experiments)" if is_ab_mode else "")
201
+ )
202
+
203
+ st.markdown("---")
204
+
205
+ # Display settings
206
+ st.subheader("⚙️ Display Settings")
207
+
208
+ show_feedback = st.checkbox(
209
+ "Show Feedback Options",
210
+ value=True,
211
+ help="Show reject button and feedback form"
212
+ )
213
+
214
+ messages_per_page = st.number_input(
215
+ "Messages per Page",
216
+ min_value=5,
217
+ max_value=100,
218
+ value=20,
219
+ help="Number of messages to display per page"
220
+ )
221
+
222
+ # Filter messages
223
+ if is_ab_mode:
224
+ # Filter both experiments
225
+ filtered_messages_a = messages_a_df.copy()
226
+ filtered_messages_b = messages_b_df.copy()
227
+
228
+ if selected_stages:
229
+ filtered_messages_a = filtered_messages_a[filtered_messages_a['stage'].isin(selected_stages)]
230
+ filtered_messages_b = filtered_messages_b[filtered_messages_b['stage'].isin(selected_stages)]
231
+
232
+ if search_term:
233
+ filtered_messages_a = data_loader.search_messages(filtered_messages_a, search_term)
234
+ filtered_messages_b = data_loader.search_messages(filtered_messages_b, search_term)
235
+
236
+ filtered_messages = None # Not used in AB mode
237
+ else:
238
+ filtered_messages = messages_df.copy()
239
+
240
+ if selected_stages:
241
+ filtered_messages = filtered_messages[filtered_messages['stage'].isin(selected_stages)]
242
+
243
+ if search_term:
244
+ filtered_messages = data_loader.search_messages(filtered_messages, search_term)
245
+
246
+ filtered_messages_a = None
247
+ filtered_messages_b = None
248
+
249
+ # Summary stats
250
+ st.subheader("📊 Summary")
251
+
252
+ if is_ab_mode:
253
+ # Show stats for both experiments
254
+ st.markdown("##### Experiment A vs Experiment B")
255
+
256
+ col1, col2, col3, col4 = st.columns(4)
257
+
258
+ with col1:
259
+ st.metric("Total Messages (A)", len(filtered_messages_a))
260
+ st.metric("Total Messages (B)", len(filtered_messages_b))
261
+
262
+ with col2:
263
+ unique_users_a = filtered_messages_a['user_id'].nunique() if 'user_id' in filtered_messages_a.columns else 0
264
+ unique_users_b = filtered_messages_b['user_id'].nunique() if 'user_id' in filtered_messages_b.columns else 0
265
+ st.metric("Unique Users (A)", unique_users_a)
266
+ st.metric("Unique Users (B)", unique_users_b)
267
+
268
+ with col3:
269
+ unique_stages_a = filtered_messages_a['stage'].nunique() if 'stage' in filtered_messages_a.columns else 0
270
+ unique_stages_b = filtered_messages_b['stage'].nunique() if 'stage' in filtered_messages_b.columns else 0
271
+ st.metric("Stages (A)", unique_stages_a)
272
+ st.metric("Stages (B)", unique_stages_b)
273
+
274
+ with col4:
275
+ # Load feedback stats for both experiments
276
+ feedback_a_df = feedback_manager.load_feedback(experiment_a_id)
277
+ feedback_b_df = feedback_manager.load_feedback(experiment_b_id)
278
+
279
+ reject_count_a = 0
280
+ if feedback_a_df is not None and len(feedback_a_df) > 0:
281
+ reject_count_a = len(feedback_a_df[feedback_a_df['feedback_type'] == 'reject'])
282
+
283
+ reject_count_b = 0
284
+ if feedback_b_df is not None and len(feedback_b_df) > 0:
285
+ reject_count_b = len(feedback_b_df[feedback_b_df['feedback_type'] == 'reject'])
286
+
287
+ st.metric("Rejected (A)", reject_count_a)
288
+ st.metric("Rejected (B)", reject_count_b)
289
+ else:
290
+ # Single experiment stats
291
+ col1, col2, col3, col4 = st.columns(4)
292
+
293
+ with col1:
294
+ st.metric("Total Messages", len(filtered_messages))
295
+
296
+ with col2:
297
+ unique_users = filtered_messages['user_id'].nunique() if 'user_id' in filtered_messages.columns else 0
298
+ st.metric("Unique Users", unique_users)
299
+
300
+ with col3:
301
+ unique_stages = filtered_messages['stage'].nunique() if 'stage' in filtered_messages.columns else 0
302
+ st.metric("Stages", unique_stages)
303
+
304
+ with col4:
305
+ # Load feedback stats
306
+ feedback_df = feedback_manager.load_feedback(experiment_id)
307
+ if feedback_df is not None and len(feedback_df) > 0:
308
+ reject_count = len(feedback_df[feedback_df['feedback_type'] == 'reject'])
309
+ st.metric("Rejected", reject_count)
310
+ else:
311
+ st.metric("Rejected", 0)
312
+
313
+ st.markdown("---")
314
+
315
+ # Helper function to parse message JSON
316
+ def parse_message_content(message_str):
317
+ """Parse message JSON string and extract content."""
318
+ try:
319
+ if isinstance(message_str, str):
320
+ message_data = json.loads(message_str)
321
+ elif isinstance(message_str, dict):
322
+ message_data = message_str
323
+ else:
324
+ return None
325
+
326
+ # Handle different message formats
327
+ if isinstance(message_data, dict):
328
+ # Check for stage-keyed format: {"1": {"header": ..., "message": ...}}
329
+ if any(k.isdigit() for k in message_data.keys()):
330
+ # Extract messages from all stages
331
+ messages = []
332
+ for key in sorted(message_data.keys(), key=lambda x: int(x) if x.isdigit() else 0):
333
+ if isinstance(message_data[key], dict):
334
+ messages.append(message_data[key])
335
+ return messages
336
+ else:
337
+ # Single message format
338
+ return [message_data]
339
+ elif isinstance(message_data, list):
340
+ return message_data
341
+
342
+ except (json.JSONDecodeError, TypeError):
343
+ return None
344
+
345
+ return None
346
+
347
+ # Helper function to parse and display recommendation
348
+ def parse_recommendation(rec_str):
349
+ """Parse recommendation JSON string and extract details."""
350
+ try:
351
+ if isinstance(rec_str, str):
352
+ rec_data = json.loads(rec_str)
353
+ elif isinstance(rec_str, dict):
354
+ rec_data = rec_str
355
+ else:
356
+ return None
357
+
358
+ if isinstance(rec_data, dict):
359
+ return rec_data
360
+
361
+ except (json.JSONDecodeError, TypeError):
362
+ return None
363
+
364
+ return None
365
+
366
+ def display_recommendation(recommendation):
367
+ """Display recommendation with thumbnail and link."""
368
+ if not recommendation:
369
+ return
370
+
371
+ rec_data = parse_recommendation(recommendation)
372
+
373
+ if rec_data:
374
+ # Extract recommendation details
375
+ title = rec_data.get('title', 'Recommended Content')
376
+ web_url = rec_data.get('web_url_path', '#')
377
+ thumbnail_url = rec_data.get('thumbnail_url', '')
378
+
379
+ # Create a clickable card with thumbnail
380
+ st.markdown("**📍 Recommended Content:**")
381
+
382
+ if thumbnail_url:
383
+ # Display thumbnail as clickable image
384
+ col1, col2 = st.columns([1, 2])
385
+
386
+ with col1:
387
+ st.image(thumbnail_url, use_container_width=True)
388
+
389
+ with col2:
390
+ st.markdown(f"**{title}**")
391
+ if web_url and web_url != '#':
392
+ st.markdown(f"[🔗 View Content]({web_url})")
393
+ else:
394
+ # No thumbnail, just show title and link
395
+ st.markdown(f"**{title}**")
396
+ if web_url and web_url != '#':
397
+ st.markdown(f"[🔗 View Content]({web_url})")
398
+
399
+
400
+ def display_user_centric_messages(filtered_df, exp_id, key_suffix, show_feedback_option, msgs_per_page):
401
+ """Display messages in user-centric view for a single experiment."""
402
+ if 'user_id' not in filtered_df.columns:
403
+ st.warning("No user_id column found in messages.")
404
+ return
405
+
406
+ unique_users = filtered_df['user_id'].unique()
407
+
408
+ # Pagination
409
+ total_users = len(unique_users)
410
+ users_per_page = msgs_per_page
411
+
412
+ if total_users > users_per_page:
413
+ page = st.number_input(
414
+ f"Page",
415
+ min_value=1,
416
+ max_value=(total_users // users_per_page) + 1,
417
+ value=1,
418
+ key=f"page_{key_suffix}"
419
+ )
420
+ start_idx = (page - 1) * users_per_page
421
+ end_idx = min(start_idx + users_per_page, total_users)
422
+ users_to_display = unique_users[start_idx:end_idx]
423
+
424
+ st.markdown(f"*Showing users {start_idx + 1}-{end_idx} of {total_users}*")
425
+ else:
426
+ users_to_display = unique_users
427
+
428
+ # Display each user
429
+ for user_id in users_to_display:
430
+ user_messages = filtered_df[filtered_df['user_id'] == user_id].sort_values('stage')
431
+
432
+ if len(user_messages) == 0:
433
+ continue
434
+
435
+ # Get user info from first message
436
+ first_msg = user_messages.iloc[0]
437
+ user_email = first_msg.get('email', 'N/A')
438
+ user_first_name = first_msg.get('first_name', 'N/A')
439
+
440
+ # User expander
441
+ with st.expander(f"👤 User {user_id} - {user_first_name} ({user_email})", expanded=False):
442
+ # User profile
443
+ st.markdown("##### 📋 User Profile")
444
+
445
+ profile_cols = st.columns(4)
446
+ profile_fields = ['first_name', 'country', 'instrument', 'subscription_status']
447
+
448
+ for idx, field in enumerate(profile_fields):
449
+ if field in first_msg:
450
+ profile_cols[idx % 4].markdown(f"**{field.replace('_', ' ').title()}:** {first_msg[field]}")
451
+
452
+ st.markdown("---")
453
+
454
+ # Display all stages for this user
455
+ st.markdown("##### 📨 Messages Across All Stages")
456
+
457
+ for _, row in user_messages.iterrows():
458
+ stage = row['stage']
459
+
460
+ # Parse message content
461
+ message_content = parse_message_content(row.get('message', ''))
462
+
463
+ if message_content:
464
+ # Find the message for this stage
465
+ stage_message = None
466
+ for msg in message_content:
467
+ if isinstance(msg, dict):
468
+ stage_message = msg
469
+ break
470
+
471
+ if stage_message:
472
+ # Create message card
473
+ st.markdown(f"**Stage {stage}**")
474
+
475
+ msg_col1, msg_col2 = st.columns([4, 1])
476
+
477
+ with msg_col1:
478
+ header = stage_message.get('header', 'No header')
479
+ message_text = stage_message.get('message', 'No message')
480
+
481
+ st.markdown(f"**📧 Header:** {header}")
482
+ st.markdown(f"**💬 Message:** {message_text}")
483
+
484
+ # Show recommendation if available
485
+ if 'thumbnail_url' in stage_message or 'web_url_path' in stage_message:
486
+ rec_data = {
487
+ 'title': stage_message.get('title', 'Recommended Content'),
488
+ 'web_url_path': stage_message.get('web_url_path', '#'),
489
+ 'thumbnail_url': stage_message.get('thumbnail_url', '')
490
+ }
491
+ display_recommendation(rec_data)
492
+ elif 'recommendation' in row and pd.notna(row['recommendation']):
493
+ display_recommendation(row['recommendation'])
494
+
495
+ with msg_col2:
496
+ # Feedback section
497
+ if show_feedback_option:
498
+ existing_feedback = feedback_manager.get_feedback_for_message(
499
+ exp_id, user_id, stage
500
+ )
501
+
502
+ if existing_feedback:
503
+ st.error("❌ Rejected")
504
+ if st.button(f"Undo", key=f"undo_{user_id}_{stage}_{key_suffix}"):
505
+ feedback_manager.delete_feedback(exp_id, user_id, stage)
506
+ st.rerun()
507
+ else:
508
+ if st.button("🚫 Reject", key=f"reject_{user_id}_{stage}_{key_suffix}"):
509
+ st.session_state[f"show_feedback_form_{user_id}_{stage}_{key_suffix}"] = True
510
+ st.rerun()
511
+
512
+ # Feedback form
513
+ if st.session_state.get(f"show_feedback_form_{user_id}_{stage}_{key_suffix}", False):
514
+ with st.form(f"feedback_form_{user_id}_{stage}_{key_suffix}"):
515
+ st.markdown("**Why is this message rejected?**")
516
+
517
+ rejection_reasons = feedback_manager.get_all_rejection_reasons()
518
+ selected_reason = st.selectbox(
519
+ "Rejection Reason",
520
+ list(rejection_reasons.keys()),
521
+ format_func=lambda x: rejection_reasons[x],
522
+ key=f"reason_{user_id}_{stage}_{key_suffix}"
523
+ )
524
+
525
+ rejection_text = st.text_area(
526
+ "Additional Comments (Optional)",
527
+ key=f"text_{user_id}_{stage}_{key_suffix}"
528
+ )
529
+
530
+ col1, col2 = st.columns(2)
531
+
532
+ with col1:
533
+ if st.form_submit_button("💾 Save", use_container_width=True):
534
+ feedback_manager.save_feedback(
535
+ experiment_id=exp_id,
536
+ user_id=user_id,
537
+ stage=stage,
538
+ feedback_type="reject",
539
+ rejection_reason=selected_reason,
540
+ rejection_text=rejection_text,
541
+ campaign_name=row.get('campaign_name'),
542
+ message_header=stage_message.get('header', 'N/A'),
543
+ message_body=stage_message.get('message', 'N/A')
544
+ )
545
+ st.session_state[f"show_feedback_form_{user_id}_{stage}_{key_suffix}"] = False
546
+ st.success("✅ Feedback saved")
547
+ st.rerun()
548
+
549
+ with col2:
550
+ if st.form_submit_button("❌ Cancel", use_container_width=True):
551
+ st.session_state[f"show_feedback_form_{user_id}_{stage}_{key_suffix}"] = False
552
+ st.rerun()
553
+
554
+ st.markdown("---")
555
+
556
+
557
+ def display_stage_centric_messages(filtered_df, exp_id, key_suffix, show_feedback_option, msgs_per_page, stages_to_display):
558
+ """Display messages in stage-centric view for a single experiment."""
559
+ # Group by stage
560
+ for stage in sorted(stages_to_display if stages_to_display else filtered_df['stage'].unique()):
561
+ stage_messages = filtered_df[filtered_df['stage'] == stage]
562
+
563
+ if len(stage_messages) == 0:
564
+ continue
565
+
566
+ with st.expander(f"📨 Stage {stage} - {len(stage_messages)} messages", expanded=(stage == 1)):
567
+ st.markdown(f"##### Stage {stage} Messages")
568
+
569
+ # Pagination for this stage
570
+ total_stage_messages = len(stage_messages)
571
+ stage_messages_per_page = msgs_per_page
572
+
573
+ if total_stage_messages > stage_messages_per_page:
574
+ page = st.number_input(
575
+ f"Page for Stage {stage}",
576
+ min_value=1,
577
+ max_value=(total_stage_messages // stage_messages_per_page) + 1,
578
+ value=1,
579
+ key=f"page_stage_{stage}_{key_suffix}"
580
+ )
581
+ start_idx = (page - 1) * stage_messages_per_page
582
+ end_idx = min(start_idx + stage_messages_per_page, total_stage_messages)
583
+ messages_to_display = stage_messages.iloc[start_idx:end_idx]
584
+
585
+ st.markdown(f"*Showing messages {start_idx + 1}-{end_idx} of {total_stage_messages}*")
586
+ else:
587
+ messages_to_display = stage_messages
588
+
589
+ # Display messages
590
+ for idx, row in messages_to_display.iterrows():
591
+ user_id = row.get('user_id', 'N/A')
592
+ user_email = row.get('email', 'N/A')
593
+ user_name = row.get('first_name', 'N/A')
594
+
595
+ # Parse message content
596
+ message_content = parse_message_content(row.get('message', ''))
597
+
598
+ if message_content:
599
+ stage_message = None
600
+ for msg in message_content:
601
+ if isinstance(msg, dict):
602
+ stage_message = msg
603
+ break
604
+
605
+ if stage_message:
606
+ # Message card
607
+ msg_col1, msg_col2 = st.columns([4, 1])
608
+
609
+ with msg_col1:
610
+ st.markdown(f"**User:** {user_name} ({user_email}) - ID: {user_id}")
611
+
612
+ header = stage_message.get('header', 'No header')
613
+ message_text = stage_message.get('message', 'No message')
614
+
615
+ st.markdown(f"**📧 Header:** {header}")
616
+ st.markdown(f"**💬 Message:** {message_text}")
617
+
618
+ # Show recommendation if available
619
+ if 'thumbnail_url' in stage_message or 'web_url_path' in stage_message:
620
+ rec_data = {
621
+ 'title': stage_message.get('title', 'Recommended Content'),
622
+ 'web_url_path': stage_message.get('web_url_path', '#'),
623
+ 'thumbnail_url': stage_message.get('thumbnail_url', '')
624
+ }
625
+ display_recommendation(rec_data)
626
+ elif 'recommendation' in row and pd.notna(row['recommendation']):
627
+ display_recommendation(row['recommendation'])
628
+
629
+ with msg_col2:
630
+ # Feedback section
631
+ if show_feedback_option:
632
+ existing_feedback = feedback_manager.get_feedback_for_message(
633
+ exp_id, user_id, stage
634
+ )
635
+
636
+ if existing_feedback:
637
+ st.error("❌ Rejected")
638
+ if st.button(f"Undo", key=f"undo_{user_id}_{stage}_{key_suffix}_sc"):
639
+ feedback_manager.delete_feedback(exp_id, user_id, stage)
640
+ st.rerun()
641
+ else:
642
+ if st.button("🚫 Reject", key=f"reject_{user_id}_{stage}_{key_suffix}_sc"):
643
+ st.session_state[f"show_feedback_form_{user_id}_{stage}_{key_suffix}_sc"] = True
644
+ st.rerun()
645
+
646
+ # Feedback form
647
+ if st.session_state.get(f"show_feedback_form_{user_id}_{stage}_{key_suffix}_sc", False):
648
+ with st.form(f"feedback_form_{user_id}_{stage}_{key_suffix}_sc"):
649
+ st.markdown("**Why is this message rejected?**")
650
+
651
+ rejection_reasons = feedback_manager.get_all_rejection_reasons()
652
+ selected_reason = st.selectbox(
653
+ "Rejection Reason",
654
+ list(rejection_reasons.keys()),
655
+ format_func=lambda x: rejection_reasons[x],
656
+ key=f"reason_{user_id}_{stage}_{key_suffix}_sc"
657
+ )
658
+
659
+ rejection_text = st.text_area(
660
+ "Additional Comments (Optional)",
661
+ key=f"text_{user_id}_{stage}_{key_suffix}_sc"
662
+ )
663
+
664
+ col1, col2 = st.columns(2)
665
+
666
+ with col1:
667
+ if st.form_submit_button("💾 Save", use_container_width=True):
668
+ feedback_manager.save_feedback(
669
+ experiment_id=exp_id,
670
+ user_id=user_id,
671
+ stage=stage,
672
+ feedback_type="reject",
673
+ rejection_reason=selected_reason,
674
+ rejection_text=rejection_text,
675
+ campaign_name=row.get('campaign_name'),
676
+ message_header=stage_message.get('header', 'N/A'),
677
+ message_body=stage_message.get('message', 'N/A')
678
+ )
679
+ st.session_state[f"show_feedback_form_{user_id}_{stage}_{key_suffix}_sc"] = False
680
+ st.success("✅ Feedback saved")
681
+ st.rerun()
682
+
683
+ with col2:
684
+ if st.form_submit_button("❌ Cancel", use_container_width=True):
685
+ st.session_state[f"show_feedback_form_{user_id}_{stage}_{key_suffix}_sc"] = False
686
+ st.rerun()
687
+
688
+ st.markdown("---")
689
+
690
+ # Display messages based on mode and view
691
+ if is_ab_mode:
692
+ # A/B Testing Mode - Show both experiments side by side
693
+ if view_mode == "User-Centric":
694
+ st.subheader("👤 User-Centric View")
695
+ st.markdown("*All stages for each user - comparing both experiments*")
696
+ else:
697
+ st.subheader("📊 Stage-Centric View")
698
+ st.markdown("*All users for each stage - comparing both experiments*")
699
+
700
+ # Create two columns for A/B comparison
701
+ col_a, col_b = st.columns(2)
702
+
703
+ with col_a:
704
+ st.markdown("### 🅰️ Experiment A")
705
+ if view_mode == "User-Centric":
706
+ display_user_centric_messages(
707
+ filtered_messages_a,
708
+ experiment_a_id,
709
+ "exp_a",
710
+ show_feedback,
711
+ messages_per_page
712
+ )
713
+ else:
714
+ display_stage_centric_messages(
715
+ filtered_messages_a,
716
+ experiment_a_id,
717
+ "exp_a",
718
+ show_feedback,
719
+ messages_per_page,
720
+ selected_stages
721
+ )
722
+
723
+ with col_b:
724
+ st.markdown("### 🅱️ Experiment B")
725
+ if view_mode == "User-Centric":
726
+ display_user_centric_messages(
727
+ filtered_messages_b,
728
+ experiment_b_id,
729
+ "exp_b",
730
+ show_feedback,
731
+ messages_per_page
732
+ )
733
+ else:
734
+ display_stage_centric_messages(
735
+ filtered_messages_b,
736
+ experiment_b_id,
737
+ "exp_b",
738
+ show_feedback,
739
+ messages_per_page,
740
+ selected_stages
741
+ )
742
+
743
+ else:
744
+ # Single Experiment Mode - Original behavior
745
+ if view_mode == "User-Centric":
746
+ st.subheader("👤 User-Centric View")
747
+ st.markdown("*All stages for each user*")
748
+ display_user_centric_messages(
749
+ filtered_messages,
750
+ experiment_id,
751
+ "single",
752
+ show_feedback,
753
+ messages_per_page
754
+ )
755
+ else:
756
+ st.subheader("📊 Stage-Centric View")
757
+ st.markdown("*All users for each stage*")
758
+ display_stage_centric_messages(
759
+ filtered_messages,
760
+ experiment_id,
761
+ "single",
762
+ show_feedback,
763
+ messages_per_page,
764
+ selected_stages
765
+ )
766
+
767
+ st.markdown("---")
768
+
769
+ # Quick actions
770
+ col1, col2, col3 = st.columns(3)
771
+
772
+ with col1:
773
+ if st.button("🏗️ Back to Campaign Builder", use_container_width=True):
774
+ st.switch_page("pages/1_Campaign_Builder.py")
775
+
776
+ with col2:
777
+ if st.button("📊 View Analytics", use_container_width=True):
778
+ st.switch_page("pages/4_Analytics.py")
779
+
780
+ with col3:
781
+ # Export messages
782
+ if st.button("💾 Export Messages", use_container_width=True):
783
+ if is_ab_mode:
784
+ # In AB mode, offer to export both experiments
785
+ st.markdown("**Export Options:**")
786
+ csv_a = filtered_messages_a.to_csv(index=False, encoding='utf-8')
787
+ csv_b = filtered_messages_b.to_csv(index=False, encoding='utf-8')
788
+
789
+ st.download_button(
790
+ label="⬇️ Download Experiment A CSV",
791
+ data=csv_a,
792
+ file_name=f"{brand}_messages_experiment_a_{ab_timestamp}.csv",
793
+ mime="text/csv",
794
+ use_container_width=True
795
+ )
796
+
797
+ st.download_button(
798
+ label="⬇️ Download Experiment B CSV",
799
+ data=csv_b,
800
+ file_name=f"{brand}_messages_experiment_b_{ab_timestamp}.csv",
801
+ mime="text/csv",
802
+ use_container_width=True
803
+ )
804
+ else:
805
+ csv = filtered_messages.to_csv(index=False, encoding='utf-8')
806
+ st.download_button(
807
+ label="⬇️ Download CSV",
808
+ data=csv,
809
+ file_name=f"{brand}_messages_{experiment_id}.csv",
810
+ mime="text/csv",
811
+ use_container_width=True
812
+ )
pages/4_Analytics.py CHANGED
@@ -1,24 +1,1070 @@
1
  """
2
- Analytics Page - HuggingFace Space Wrapper
3
- Redirects to the actual page in visualization/pages/
 
4
  """
5
 
 
6
  import sys
7
  from pathlib import Path
 
 
8
 
9
- # Set up paths
10
- root_dir = Path(__file__).parent.parent
11
- visualization_dir = root_dir / "visualization"
 
 
 
 
12
 
13
- # Add directories to path
14
- sys.path.insert(0, str(root_dir))
15
- sys.path.insert(0, str(root_dir / "ai_messaging_system_v2"))
16
- sys.path.insert(0, str(visualization_dir))
17
 
18
- # Import and run the actual page
19
- import importlib.util
 
 
20
 
21
- page_path = visualization_dir / "pages" / "4_Analytics.py"
22
- spec = importlib.util.spec_from_file_location("analytics", page_path)
23
- analytics = importlib.util.module_from_spec(spec)
24
- spec.loader.exec_module(analytics)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  """
2
+ Page 4: Analytics Dashboard
3
+ Visualize performance metrics and insights from generated messages and feedback.
4
+ Supports A/B testing mode for comparing two experiments side-by-side.
5
  """
6
 
7
+ import streamlit as st
8
  import sys
9
  from pathlib import Path
10
+ import pandas as pd
11
+ import re
12
 
13
+ # Try to import plotly with helpful error message
14
+ try:
15
+ import plotly.express as px
16
+ import plotly.graph_objects as go
17
+ except ModuleNotFoundError:
18
+ st.error("""
19
+ ⚠️ **Missing Dependency: plotly**
20
 
21
+ Plotly is not installed in the current Python environment.
 
 
 
22
 
23
+ **To fix this:**
24
+ 1. Make sure you're running Streamlit from the correct Python environment
25
+ 2. Install plotly: `pip install plotly>=5.17.0`
26
+ 3. Or install all requirements: `pip install -r visualization/requirements.txt`
27
 
28
+ **Current Python:** {}
29
+ """.format(sys.executable))
30
+ st.stop()
31
+
32
+ # Add root directory to path
33
+ sys.path.insert(0, str(Path(__file__).parent.parent))
34
+
35
+ from utils.auth import check_authentication
36
+ from utils.theme import apply_theme, get_brand_emoji, get_brand_theme
37
+ from utils.data_loader import DataLoader
38
+ from utils.feedback_manager import FeedbackManager
39
+
40
+ # Page configuration
41
+ st.set_page_config(
42
+ page_title="Analytics Dashboard",
43
+ page_icon="📊",
44
+ layout="wide"
45
+ )
46
+
47
+ # Check authentication
48
+ if not check_authentication():
49
+ st.error("🔒 Please login first")
50
+ st.stop()
51
+
52
+ # Initialize utilities
53
+ data_loader = DataLoader()
54
+ feedback_manager = FeedbackManager()
55
+
56
+ # Check if brand is selected
57
+ if "selected_brand" not in st.session_state or not st.session_state.selected_brand:
58
+ st.error("⚠️ Please select a brand from the home page first")
59
+ st.stop()
60
+
61
+ brand = st.session_state.selected_brand
62
+ apply_theme(brand)
63
+ theme = get_brand_theme(brand)
64
+
65
+ # Helper function to detect A/B testing files
66
+ def detect_ab_testing_files():
67
+ """
68
+ Detect if there are A/B testing files in the UI output directory.
69
+ Returns (is_ab_mode, messages_a_path, messages_b_path, timestamp)
70
+ """
71
+ ui_output_path = data_loader.get_ui_output_path()
72
+
73
+ # Check session state first
74
+ if st.session_state.get('ab_testing_mode', False):
75
+ # Look for files with pattern: messages_a_* and messages_b_*
76
+ a_files = list(ui_output_path.glob('messages_a_*.csv'))
77
+ b_files = list(ui_output_path.glob('messages_b_*.csv'))
78
+
79
+ if a_files and b_files:
80
+ # Find matching pairs by timestamp
81
+ for a_file in a_files:
82
+ # Extract timestamp from filename: messages_a_brand_variant_timestamp.csv
83
+ match_a = re.search(r'messages_a_.*_(\d{8}_\d{4})\.csv', a_file.name)
84
+ if match_a:
85
+ timestamp = match_a.group(1)
86
+ # Look for matching b file
87
+ for b_file in b_files:
88
+ if timestamp in b_file.name:
89
+ return True, a_file, b_file, timestamp
90
+
91
+ # Also detect if files exist even if session state not set
92
+ ui_output_path = data_loader.get_ui_output_path()
93
+ a_files = list(ui_output_path.glob('messages_a_*.csv'))
94
+ b_files = list(ui_output_path.glob('messages_b_*.csv'))
95
+
96
+ if a_files and b_files:
97
+ # Auto-detect matching pairs
98
+ for a_file in a_files:
99
+ match_a = re.search(r'messages_a_.*_(\d{8}_\d{4})\.csv', a_file.name)
100
+ if match_a:
101
+ timestamp = match_a.group(1)
102
+ for b_file in b_files:
103
+ if timestamp in b_file.name:
104
+ return True, a_file, b_file, timestamp
105
+
106
+ return False, None, None, None
107
+
108
+
109
+ def load_ab_test_messages(file_path):
110
+ """Load messages from A/B test file."""
111
+ try:
112
+ return pd.read_csv(file_path, encoding='utf-8-sig')
113
+ except Exception as e:
114
+ st.error(f"Error loading A/B test file: {e}")
115
+ return None
116
+
117
+
118
+ def extract_experiment_id_from_filename(file_path):
119
+ """Extract experiment ID from A/B test filename."""
120
+ # Pattern: messages_a_brand_variant_timestamp.csv or messages_b_brand_variant_timestamp.csv
121
+ # Use the full filename (without .csv) to ensure A and B experiments have different IDs
122
+ filename = file_path.stem # Removes .csv extension automatically
123
+ return filename
124
+
125
+
126
+ # Page Header
127
+ emoji = get_brand_emoji(brand)
128
+ st.title(f"📊 Analytics Dashboard - {emoji} {brand.title()}")
129
+ st.markdown("**Performance metrics and insights**")
130
+
131
+ # Detect A/B testing mode
132
+ is_ab_mode, messages_a_path, messages_b_path, ab_timestamp = detect_ab_testing_files()
133
+
134
+ if is_ab_mode:
135
+ st.success(f"🔬 **A/B Testing Mode - Comparing Experiments Side-by-Side** (Timestamp: {ab_timestamp})")
136
+
137
+ st.markdown("---")
138
+
139
+ # Load messages based on mode
140
+ if is_ab_mode:
141
+ # Load A/B test messages
142
+ messages_a_df = load_ab_test_messages(messages_a_path)
143
+ messages_b_df = load_ab_test_messages(messages_b_path)
144
+
145
+ if messages_a_df is None or len(messages_a_df) == 0 or messages_b_df is None or len(messages_b_df) == 0:
146
+ st.warning("⚠️ Error loading A/B test messages. Please check the files.")
147
+ if st.button("🏗️ Go to Campaign Builder"):
148
+ st.switch_page("pages/1_Campaign_Builder.py")
149
+ st.stop()
150
+
151
+ # Extract experiment IDs from filenames
152
+ experiment_a_id = extract_experiment_id_from_filename(messages_a_path)
153
+ experiment_b_id = extract_experiment_id_from_filename(messages_b_path)
154
+
155
+ messages_df = None # Not used in AB mode
156
+ else:
157
+ # Load regular messages
158
+ messages_df = data_loader.load_generated_messages()
159
+
160
+ if messages_df is None or len(messages_df) == 0:
161
+ st.warning("⚠️ No messages found. Please generate messages first in Campaign Builder.")
162
+ if st.button("🏗️ Go to Campaign Builder"):
163
+ st.switch_page("pages/1_Campaign_Builder.py")
164
+ st.stop()
165
+
166
+ # Get experiment ID
167
+ if "current_experiment_id" not in st.session_state or not st.session_state.current_experiment_id:
168
+ if 'campaign_name' in messages_df.columns and len(messages_df) > 0:
169
+ campaign_name = messages_df['campaign_name'].iloc[0]
170
+ experiment_id = campaign_name.replace(" ", "_")
171
+ else:
172
+ experiment_id = f"{brand}_experiment"
173
+
174
+ st.session_state.current_experiment_id = experiment_id
175
+
176
+ experiment_id = st.session_state.current_experiment_id
177
+
178
+ # Sidebar - Experiment Selection and Filters
179
+ with st.sidebar:
180
+ st.header("⚙️ Settings")
181
+
182
+ if not is_ab_mode:
183
+ # List available experiments (only in single mode)
184
+ experiment_files = list(data_loader.ui_output_path.glob("messages_*.csv"))
185
+ experiment_names = [f.stem for f in experiment_files]
186
+
187
+ if len(experiment_names) > 1:
188
+ selected_experiment = st.selectbox(
189
+ "Select Experiment",
190
+ experiment_names,
191
+ help="Choose an experiment to analyze"
192
+ )
193
+
194
+ if selected_experiment != experiment_id:
195
+ # Load different experiment
196
+ experiment_id = selected_experiment
197
+ st.session_state.current_experiment_id = experiment_id
198
+
199
+ # Reload messages and feedback
200
+ exp_file = data_loader.ui_output_path / f"{selected_experiment}.csv"
201
+ if exp_file.exists():
202
+ messages_df = pd.read_csv(exp_file, encoding='utf-8-sig')
203
+ st.rerun()
204
+
205
+ st.markdown("---")
206
+
207
+ # Filters
208
+ st.subheader("🔍 Filters")
209
+
210
+ # Stage filter
211
+ if is_ab_mode:
212
+ # Get stages from both dataframes
213
+ stages_a = sorted(messages_a_df['stage'].unique()) if 'stage' in messages_a_df.columns else []
214
+ stages_b = sorted(messages_b_df['stage'].unique()) if 'stage' in messages_b_df.columns else []
215
+ available_stages = sorted(list(set(stages_a + stages_b)))
216
+ else:
217
+ available_stages = sorted(messages_df['stage'].unique()) if 'stage' in messages_df.columns else []
218
+
219
+ if available_stages:
220
+ selected_stages = st.multiselect(
221
+ "Filter by Stage",
222
+ available_stages,
223
+ default=available_stages,
224
+ help="Select stages to include in analysis" + (" (applies to both experiments)" if is_ab_mode else "")
225
+ )
226
+ else:
227
+ selected_stages = []
228
+
229
+ # Filter messages by selected stages
230
+ if is_ab_mode:
231
+ # Filter both experiments
232
+ filtered_messages_a = messages_a_df.copy()
233
+ filtered_messages_b = messages_b_df.copy()
234
+
235
+ if selected_stages:
236
+ filtered_messages_a = filtered_messages_a[filtered_messages_a['stage'].isin(selected_stages)]
237
+ filtered_messages_b = filtered_messages_b[filtered_messages_b['stage'].isin(selected_stages)]
238
+
239
+ filtered_messages = None # Not used in AB mode
240
+
241
+ # Get feedback stats for both experiments
242
+ feedback_stats_a = feedback_manager.get_feedback_stats(experiment_a_id, total_messages=len(filtered_messages_a))
243
+ feedback_stats_b = feedback_manager.get_feedback_stats(experiment_b_id, total_messages=len(filtered_messages_b))
244
+ stage_feedback_stats_a = feedback_manager.get_stage_feedback_stats(experiment_a_id, messages_df=filtered_messages_a)
245
+ stage_feedback_stats_b = feedback_manager.get_stage_feedback_stats(experiment_b_id, messages_df=filtered_messages_b)
246
+ else:
247
+ if selected_stages and 'stage' in messages_df.columns:
248
+ filtered_messages = messages_df[messages_df['stage'].isin(selected_stages)]
249
+ else:
250
+ filtered_messages = messages_df
251
+
252
+ # Get feedback stats (pass total messages for accurate rejection rate)
253
+ feedback_stats = feedback_manager.get_feedback_stats(experiment_id, total_messages=len(filtered_messages))
254
+ stage_feedback_stats = feedback_manager.get_stage_feedback_stats(experiment_id, messages_df=filtered_messages)
255
+
256
+ # ============================================================================
257
+ # OVERALL METRICS
258
+ # ============================================================================
259
+ st.header("📈 Overall Performance")
260
+
261
+ if is_ab_mode:
262
+ # A/B Testing Mode - Show comparison side-by-side
263
+ st.markdown("##### Experiment A vs Experiment B")
264
+
265
+ col1, col2 = st.columns(2)
266
+
267
+ with col1:
268
+ st.markdown("### 🅰️ Experiment A")
269
+ sub_col1, sub_col2 = st.columns(2)
270
+
271
+ with sub_col1:
272
+ st.metric(
273
+ "Total Messages",
274
+ len(filtered_messages_a),
275
+ help="Total number of generated messages"
276
+ )
277
+ st.metric(
278
+ "Rejected Messages",
279
+ feedback_stats_a['total_rejects'],
280
+ f"{feedback_stats_a['reject_rate']:.1f}%"
281
+ )
282
+
283
+ with sub_col2:
284
+ st.metric(
285
+ "Total Feedback",
286
+ feedback_stats_a['total_feedback'],
287
+ help="Number of messages with feedback"
288
+ )
289
+ approved_a = len(filtered_messages_a) - feedback_stats_a['total_rejects']
290
+ approval_rate_a = (approved_a / len(filtered_messages_a) * 100) if len(filtered_messages_a) > 0 else 0
291
+ st.metric(
292
+ "Approved/OK",
293
+ approved_a,
294
+ f"{approval_rate_a:.1f}%"
295
+ )
296
+
297
+ with col2:
298
+ st.markdown("### 🅱️ Experiment B")
299
+ sub_col1, sub_col2 = st.columns(2)
300
+
301
+ with sub_col1:
302
+ st.metric(
303
+ "Total Messages",
304
+ len(filtered_messages_b),
305
+ help="Total number of generated messages"
306
+ )
307
+ st.metric(
308
+ "Rejected Messages",
309
+ feedback_stats_b['total_rejects'],
310
+ f"{feedback_stats_b['reject_rate']:.1f}%"
311
+ )
312
+
313
+ with sub_col2:
314
+ st.metric(
315
+ "Total Feedback",
316
+ feedback_stats_b['total_feedback'],
317
+ help="Number of messages with feedback"
318
+ )
319
+ approved_b = len(filtered_messages_b) - feedback_stats_b['total_rejects']
320
+ approval_rate_b = (approved_b / len(filtered_messages_b) * 100) if len(filtered_messages_b) > 0 else 0
321
+ st.metric(
322
+ "Approved/OK",
323
+ approved_b,
324
+ f"{approval_rate_b:.1f}%"
325
+ )
326
+
327
+ # Winner determination
328
+ st.markdown("---")
329
+ st.markdown("#### 🏆 Winner Determination")
330
+
331
+ reject_rate_diff = feedback_stats_a['reject_rate'] - feedback_stats_b['reject_rate']
332
+
333
+ col1, col2, col3 = st.columns([1, 2, 1])
334
+
335
+ with col2:
336
+ if reject_rate_diff < -1:
337
+ st.success(f"**🏆 Experiment A is performing better** by {abs(reject_rate_diff):.1f}% (lower rejection rate)")
338
+ elif reject_rate_diff > 1:
339
+ st.success(f"**🏆 Experiment B is performing better** by {abs(reject_rate_diff):.1f}% (lower rejection rate)")
340
+ else:
341
+ st.info("**➡️ Both experiments have similar performance** (difference < 1%)")
342
+
343
+ else:
344
+ # Single Experiment Mode - Original behavior
345
+ col1, col2, col3, col4 = st.columns(4)
346
+
347
+ with col1:
348
+ st.metric(
349
+ "Total Messages",
350
+ len(filtered_messages),
351
+ help="Total number of generated messages"
352
+ )
353
+
354
+ with col2:
355
+ st.metric(
356
+ "Total Feedback",
357
+ feedback_stats['total_feedback'],
358
+ help="Number of messages with feedback"
359
+ )
360
+
361
+ with col3:
362
+ st.metric(
363
+ "Rejected Messages",
364
+ feedback_stats['total_rejects'],
365
+ f"{feedback_stats['reject_rate']:.1f}%"
366
+ )
367
+
368
+ with col4:
369
+ approved = len(filtered_messages) - feedback_stats['total_rejects']
370
+ approval_rate = (approved / len(filtered_messages) * 100) if len(filtered_messages) > 0 else 0
371
+ st.metric(
372
+ "Approved/OK Messages",
373
+ approved,
374
+ f"{approval_rate:.1f}%"
375
+ )
376
+
377
+ st.markdown("---")
378
+
379
+ # ============================================================================
380
+ # STAGE-BY-STAGE PERFORMANCE
381
+ # ============================================================================
382
+ st.header("📊 Stage-by-Stage Performance")
383
+
384
+ if is_ab_mode:
385
+ # A/B Testing Mode - Show both experiments on same chart for easy comparison
386
+ if len(stage_feedback_stats_a) > 0 or len(stage_feedback_stats_b) > 0:
387
+ # Create comparison chart
388
+ fig = go.Figure()
389
+
390
+ if len(stage_feedback_stats_a) > 0:
391
+ fig.add_trace(go.Bar(
392
+ x=stage_feedback_stats_a['stage'],
393
+ y=stage_feedback_stats_a['reject_rate'],
394
+ name='Experiment A',
395
+ marker_color='#4A90E2',
396
+ text=stage_feedback_stats_a['reject_rate'].round(1),
397
+ textposition='auto',
398
+ ))
399
+
400
+ if len(stage_feedback_stats_b) > 0:
401
+ fig.add_trace(go.Bar(
402
+ x=stage_feedback_stats_b['stage'],
403
+ y=stage_feedback_stats_b['reject_rate'],
404
+ name='Experiment B',
405
+ marker_color='#E94B3C',
406
+ text=stage_feedback_stats_b['reject_rate'].round(1),
407
+ textposition='auto',
408
+ ))
409
+
410
+ max_reject_rate = 10
411
+ if len(stage_feedback_stats_a) > 0:
412
+ max_reject_rate = max(max_reject_rate, stage_feedback_stats_a['reject_rate'].max())
413
+ if len(stage_feedback_stats_b) > 0:
414
+ max_reject_rate = max(max_reject_rate, stage_feedback_stats_b['reject_rate'].max())
415
+
416
+ fig.update_layout(
417
+ title="Rejection Rate by Stage - A vs B Comparison",
418
+ xaxis_title="Stage",
419
+ yaxis_title="Rejection Rate (%)",
420
+ yaxis=dict(range=[0, max_reject_rate * 1.2]),
421
+ template="plotly_dark",
422
+ paper_bgcolor='rgba(0,0,0,0)',
423
+ plot_bgcolor='rgba(0,0,0,0)',
424
+ barmode='group'
425
+ )
426
+
427
+ st.plotly_chart(fig, use_container_width=True)
428
+
429
+ # Stage details tables side-by-side
430
+ with st.expander("📋 View Stage Details Comparison"):
431
+ col1, col2 = st.columns(2)
432
+
433
+ with col1:
434
+ st.markdown("**🅰️ Experiment A**")
435
+ if len(stage_feedback_stats_a) > 0:
436
+ st.dataframe(
437
+ stage_feedback_stats_a,
438
+ use_container_width=True,
439
+ column_config={
440
+ "stage": "Stage",
441
+ "total_messages": "Messages",
442
+ "total_feedback": "Feedback",
443
+ "rejects": "Rejected",
444
+ "reject_rate": st.column_config.NumberColumn(
445
+ "Reject Rate (%)",
446
+ format="%.1f%%"
447
+ )
448
+ }
449
+ )
450
+ else:
451
+ st.info("No feedback data for Experiment A")
452
+
453
+ with col2:
454
+ st.markdown("**🅱️ Experiment B**")
455
+ if len(stage_feedback_stats_b) > 0:
456
+ st.dataframe(
457
+ stage_feedback_stats_b,
458
+ use_container_width=True,
459
+ column_config={
460
+ "stage": "Stage",
461
+ "total_messages": "Messages",
462
+ "total_feedback": "Feedback",
463
+ "rejects": "Rejected",
464
+ "reject_rate": st.column_config.NumberColumn(
465
+ "Reject Rate (%)",
466
+ format="%.1f%%"
467
+ )
468
+ }
469
+ )
470
+ else:
471
+ st.info("No feedback data for Experiment B")
472
+ else:
473
+ st.info("ℹ️ No feedback data available yet. Provide feedback in the Message Viewer to see stage-by-stage analysis.")
474
+
475
+ else:
476
+ # Single Experiment Mode - Original behavior
477
+ if len(stage_feedback_stats) > 0:
478
+ # Create bar chart for stage performance
479
+ fig = go.Figure()
480
+
481
+ fig.add_trace(go.Bar(
482
+ x=stage_feedback_stats['stage'],
483
+ y=stage_feedback_stats['reject_rate'],
484
+ name='Rejection Rate (%)',
485
+ marker_color=theme['accent'],
486
+ text=stage_feedback_stats['reject_rate'].round(1),
487
+ textposition='auto',
488
+ ))
489
+
490
+ fig.update_layout(
491
+ title="Rejection Rate by Stage",
492
+ xaxis_title="Stage",
493
+ yaxis_title="Rejection Rate (%)",
494
+ yaxis=dict(range=[0, max(stage_feedback_stats['reject_rate'].max() * 1.2, 10)]),
495
+ template="plotly_dark",
496
+ paper_bgcolor='rgba(0,0,0,0)',
497
+ plot_bgcolor='rgba(0,0,0,0)',
498
+ )
499
+
500
+ st.plotly_chart(fig, use_container_width=True)
501
+
502
+ # Stage details table
503
+ with st.expander("📋 View Stage Details"):
504
+ st.dataframe(
505
+ stage_feedback_stats,
506
+ use_container_width=True,
507
+ column_config={
508
+ "stage": "Stage",
509
+ "total_messages": "Total Messages",
510
+ "total_feedback": "Total Feedback",
511
+ "rejects": "Rejected",
512
+ "reject_rate": st.column_config.NumberColumn(
513
+ "Rejection Rate (%)",
514
+ format="%.1f%%"
515
+ )
516
+ }
517
+ )
518
+ else:
519
+ st.info("ℹ️ No feedback data available yet. Provide feedback in the Message Viewer to see stage-by-stage analysis.")
520
+
521
+ st.markdown("---")
522
+
523
+ # ============================================================================
524
+ # REJECTION REASONS ANALYSIS
525
+ # ============================================================================
526
+ st.header("🔍 Rejection Reasons Analysis")
527
+
528
+ if is_ab_mode:
529
+ # A/B Testing Mode - Show side-by-side pie charts for both experiments
530
+ has_rejection_a = len(feedback_stats_a['rejection_reasons']) > 0
531
+ has_rejection_b = len(feedback_stats_b['rejection_reasons']) > 0
532
+
533
+ if has_rejection_a or has_rejection_b:
534
+ col1, col2 = st.columns(2)
535
+
536
+ with col1:
537
+ st.markdown("### 🅰️ Experiment A")
538
+ if has_rejection_a:
539
+ rejection_reasons_a_df = pd.DataFrame([
540
+ {"Reason": reason, "Count": count}
541
+ for reason, count in feedback_stats_a['rejection_reasons'].items()
542
+ ]).sort_values('Count', ascending=False)
543
+
544
+ # Pie chart for rejection reasons
545
+ fig_a = px.pie(
546
+ rejection_reasons_a_df,
547
+ names='Reason',
548
+ values='Count',
549
+ title="Distribution of Rejection Reasons",
550
+ color_discrete_sequence=px.colors.qualitative.Set3
551
+ )
552
+
553
+ fig_a.update_layout(
554
+ template="plotly_dark",
555
+ paper_bgcolor='rgba(0,0,0,0)',
556
+ plot_bgcolor='rgba(0,0,0,0)',
557
+ )
558
+
559
+ st.plotly_chart(fig_a, use_container_width=True)
560
+
561
+ # Most common issue
562
+ most_common_reason_a = rejection_reasons_a_df.iloc[0]
563
+ st.info(f"💡 **Most Common:** {most_common_reason_a['Reason']} ({most_common_reason_a['Count']})")
564
+ else:
565
+ st.info("No rejection feedback for Experiment A")
566
+
567
+ with col2:
568
+ st.markdown("### 🅱️ Experiment B")
569
+ if has_rejection_b:
570
+ rejection_reasons_b_df = pd.DataFrame([
571
+ {"Reason": reason, "Count": count}
572
+ for reason, count in feedback_stats_b['rejection_reasons'].items()
573
+ ]).sort_values('Count', ascending=False)
574
+
575
+ # Pie chart for rejection reasons
576
+ fig_b = px.pie(
577
+ rejection_reasons_b_df,
578
+ names='Reason',
579
+ values='Count',
580
+ title="Distribution of Rejection Reasons",
581
+ color_discrete_sequence=px.colors.qualitative.Pastel
582
+ )
583
+
584
+ fig_b.update_layout(
585
+ template="plotly_dark",
586
+ paper_bgcolor='rgba(0,0,0,0)',
587
+ plot_bgcolor='rgba(0,0,0,0)',
588
+ )
589
+
590
+ st.plotly_chart(fig_b, use_container_width=True)
591
+
592
+ # Most common issue
593
+ most_common_reason_b = rejection_reasons_b_df.iloc[0]
594
+ st.info(f"💡 **Most Common:** {most_common_reason_b['Reason']} ({most_common_reason_b['Count']})")
595
+ else:
596
+ st.info("No rejection feedback for Experiment B")
597
+
598
+ # Detailed rejection reasons tables side-by-side
599
+ with st.expander("📋 View Detailed Rejection Feedback"):
600
+ col1, col2 = st.columns(2)
601
+
602
+ with col1:
603
+ st.markdown("**🅰️ Experiment A**")
604
+ feedback_a_df = feedback_manager.load_feedback(experiment_a_id)
605
+ if feedback_a_df is not None and len(feedback_a_df) > 0:
606
+ reject_a_df = feedback_a_df[feedback_a_df['feedback_type'] == 'reject']
607
+
608
+ if len(reject_a_df) > 0:
609
+ display_a_df = reject_a_df[[
610
+ 'user_id', 'stage', 'rejection_reason', 'rejection_text', 'timestamp'
611
+ ]].copy()
612
+
613
+ display_a_df['rejection_reason'] = display_a_df['rejection_reason'].apply(
614
+ lambda x: feedback_manager.get_rejection_reason_label(x) if pd.notna(x) else 'N/A'
615
+ )
616
+
617
+ st.dataframe(display_a_df, use_container_width=True)
618
+ else:
619
+ st.info("No rejection feedback")
620
+ else:
621
+ st.info("No feedback data")
622
+
623
+ with col2:
624
+ st.markdown("**🅱️ Experiment B**")
625
+ feedback_b_df = feedback_manager.load_feedback(experiment_b_id)
626
+ if feedback_b_df is not None and len(feedback_b_df) > 0:
627
+ reject_b_df = feedback_b_df[feedback_b_df['feedback_type'] == 'reject']
628
+
629
+ if len(reject_b_df) > 0:
630
+ display_b_df = reject_b_df[[
631
+ 'user_id', 'stage', 'rejection_reason', 'rejection_text', 'timestamp'
632
+ ]].copy()
633
+
634
+ display_b_df['rejection_reason'] = display_b_df['rejection_reason'].apply(
635
+ lambda x: feedback_manager.get_rejection_reason_label(x) if pd.notna(x) else 'N/A'
636
+ )
637
+
638
+ st.dataframe(display_b_df, use_container_width=True)
639
+ else:
640
+ st.info("No rejection feedback")
641
+ else:
642
+ st.info("No feedback data")
643
+ else:
644
+ st.info("ℹ️ No rejection feedback available yet. Reject messages in the Message Viewer to see detailed analysis.")
645
+
646
+ else:
647
+ # Single Experiment Mode - Original behavior
648
+ if len(feedback_stats['rejection_reasons']) > 0:
649
+ # Create DataFrame for rejection reasons
650
+ rejection_reasons_df = pd.DataFrame([
651
+ {"Reason": reason, "Count": count}
652
+ for reason, count in feedback_stats['rejection_reasons'].items()
653
+ ]).sort_values('Count', ascending=False)
654
+
655
+ col1, col2 = st.columns([1, 1])
656
+
657
+ with col1:
658
+ # Pie chart for rejection reasons
659
+ fig = px.pie(
660
+ rejection_reasons_df,
661
+ names='Reason',
662
+ values='Count',
663
+ title="Distribution of Rejection Reasons",
664
+ color_discrete_sequence=px.colors.qualitative.Set3
665
+ )
666
+
667
+ fig.update_layout(
668
+ template="plotly_dark",
669
+ paper_bgcolor='rgba(0,0,0,0)',
670
+ plot_bgcolor='rgba(0,0,0,0)',
671
+ )
672
+
673
+ st.plotly_chart(fig, use_container_width=True)
674
+
675
+ with col2:
676
+ # Bar chart for rejection reasons
677
+ fig = px.bar(
678
+ rejection_reasons_df,
679
+ x='Reason',
680
+ y='Count',
681
+ title="Rejection Reasons Count",
682
+ color='Count',
683
+ color_continuous_scale='Reds'
684
+ )
685
+
686
+ fig.update_layout(
687
+ template="plotly_dark",
688
+ paper_bgcolor='rgba(0,0,0,0)',
689
+ plot_bgcolor='rgba(0,0,0,0)',
690
+ xaxis_tickangle=-45
691
+ )
692
+
693
+ st.plotly_chart(fig, use_container_width=True)
694
+
695
+ # Most common issue
696
+ most_common_reason = rejection_reasons_df.iloc[0]
697
+ st.info(f"💡 **Most Common Issue:** {most_common_reason['Reason']} ({most_common_reason['Count']} occurrences)")
698
+
699
+ # Detailed rejection reasons table
700
+ with st.expander("📋 View Detailed Rejection Feedback"):
701
+ feedback_df = feedback_manager.load_feedback(experiment_id)
702
+ if feedback_df is not None and len(feedback_df) > 0:
703
+ reject_df = feedback_df[feedback_df['feedback_type'] == 'reject']
704
+
705
+ if len(reject_df) > 0:
706
+ # Prepare display dataframe
707
+ display_df = reject_df[[
708
+ 'user_id', 'stage', 'rejection_reason', 'rejection_text', 'timestamp'
709
+ ]].copy()
710
+
711
+ # Map rejection reason keys to labels
712
+ display_df['rejection_reason'] = display_df['rejection_reason'].apply(
713
+ lambda x: feedback_manager.get_rejection_reason_label(x) if pd.notna(x) else 'N/A'
714
+ )
715
+
716
+ st.dataframe(display_df, use_container_width=True)
717
+ else:
718
+ st.info("ℹ️ No rejection feedback available yet. Reject messages in the Message Viewer to see detailed analysis.")
719
+
720
+ st.markdown("---")
721
+
722
+ # ============================================================================
723
+ # STATISTICAL COMPARISON (AB Testing Mode Only)
724
+ # ============================================================================
725
+ if is_ab_mode:
726
+ st.header("📊 Statistical Comparison")
727
+
728
+ # Get comparison stats
729
+ comparison = feedback_manager.compare_experiments(
730
+ experiment_a_id,
731
+ experiment_b_id,
732
+ total_messages_a=len(filtered_messages_a),
733
+ total_messages_b=len(filtered_messages_b)
734
+ )
735
+
736
+ col1, col2, col3 = st.columns(3)
737
+
738
+ with col1:
739
+ st.markdown("### 🅰️ Experiment A")
740
+ st.metric(
741
+ "Rejection Rate",
742
+ f"{comparison['experiment_a']['stats']['reject_rate']:.1f}%"
743
+ )
744
+ st.metric(
745
+ "Total Feedback",
746
+ comparison['experiment_a']['stats']['total_feedback']
747
+ )
748
+ st.metric(
749
+ "Total Messages",
750
+ len(filtered_messages_a)
751
+ )
752
+
753
+ with col2:
754
+ st.markdown("### 🅱️ Experiment B")
755
+ st.metric(
756
+ "Rejection Rate",
757
+ f"{comparison['experiment_b']['stats']['reject_rate']:.1f}%"
758
+ )
759
+ st.metric(
760
+ "Total Feedback",
761
+ comparison['experiment_b']['stats']['total_feedback']
762
+ )
763
+ st.metric(
764
+ "Total Messages",
765
+ len(filtered_messages_b)
766
+ )
767
+
768
+ with col3:
769
+ st.markdown("### 📊 Difference")
770
+ diff = comparison['difference']['reject_rate']
771
+
772
+ st.metric(
773
+ "Rejection Rate Δ",
774
+ f"{diff:.1f}%",
775
+ delta=f"{-diff:.1f}%" if diff != 0 else "0%",
776
+ delta_color="inverse"
777
+ )
778
+
779
+ feedback_diff = comparison['experiment_b']['stats']['total_feedback'] - comparison['experiment_a']['stats']['total_feedback']
780
+ st.metric(
781
+ "Feedback Δ",
782
+ feedback_diff,
783
+ delta=f"{feedback_diff}" if feedback_diff != 0 else "0"
784
+ )
785
+
786
+ st.markdown("---")
787
+
788
+ # ============================================================================
789
+ # COMPARATIVE ANALYSIS (if multiple experiments in non-AB mode)
790
+ # ============================================================================
791
+ if not is_ab_mode:
792
+ st.header("🧪 Comparative Analysis")
793
+
794
+ # Check for A/B test experiments
795
+ experiment_files = list(data_loader.ui_output_path.glob("messages_*_*.csv"))
796
+ ab_experiments = [f for f in experiment_files if '_a_' in f.stem.lower() or '_b_' in f.stem.lower()]
797
+
798
+ if len(ab_experiments) >= 2:
799
+ st.markdown("**A/B Test Experiments Detected**")
800
+
801
+ # Find matching A/B pairs
802
+ ab_pairs = {}
803
+ for exp_file in ab_experiments:
804
+ exp_name = exp_file.stem
805
+ if '_a_' in exp_name.lower():
806
+ base_name = exp_name.lower().replace('_a_', '_')
807
+ if base_name not in ab_pairs:
808
+ ab_pairs[base_name] = {}
809
+ ab_pairs[base_name]['A'] = exp_name
810
+ elif '_b_' in exp_name.lower():
811
+ base_name = exp_name.lower().replace('_b_', '_')
812
+ if base_name not in ab_pairs:
813
+ ab_pairs[base_name] = {}
814
+ ab_pairs[base_name]['B'] = exp_name
815
+
816
+ # Display A/B comparisons
817
+ for base_name, pair in ab_pairs.items():
818
+ if 'A' in pair and 'B' in pair:
819
+ with st.expander(f"📊 {base_name.replace('messages_', '').replace('_', ' ').title()}"):
820
+ exp_a_id = pair['A']
821
+ exp_b_id = pair['B']
822
+
823
+ # Load messages to get total counts
824
+ try:
825
+ exp_a_file = data_loader.ui_output_path / f"{exp_a_id}.csv"
826
+ exp_b_file = data_loader.ui_output_path / f"{exp_b_id}.csv"
827
+
828
+ messages_a_df = pd.read_csv(exp_a_file, encoding='utf-8-sig') if exp_a_file.exists() else None
829
+ messages_b_df = pd.read_csv(exp_b_file, encoding='utf-8-sig') if exp_b_file.exists() else None
830
+
831
+ total_messages_a = len(messages_a_df) if messages_a_df is not None else None
832
+ total_messages_b = len(messages_b_df) if messages_b_df is not None else None
833
+ except:
834
+ total_messages_a = None
835
+ total_messages_b = None
836
+
837
+ # Get comparison stats
838
+ comparison = feedback_manager.compare_experiments(
839
+ exp_a_id,
840
+ exp_b_id,
841
+ total_messages_a=total_messages_a,
842
+ total_messages_b=total_messages_b
843
+ )
844
+
845
+ col1, col2, col3 = st.columns(3)
846
+
847
+ with col1:
848
+ st.markdown("### 🅰️ Experiment A")
849
+ st.metric(
850
+ "Rejection Rate",
851
+ f"{comparison['experiment_a']['stats']['reject_rate']:.1f}%"
852
+ )
853
+ st.metric(
854
+ "Total Feedback",
855
+ comparison['experiment_a']['stats']['total_feedback']
856
+ )
857
+
858
+ with col2:
859
+ st.markdown("### 🅱️ Experiment B")
860
+ st.metric(
861
+ "Rejection Rate",
862
+ f"{comparison['experiment_b']['stats']['reject_rate']:.1f}%"
863
+ )
864
+ st.metric(
865
+ "Total Feedback",
866
+ comparison['experiment_b']['stats']['total_feedback']
867
+ )
868
+
869
+ with col3:
870
+ st.markdown("### 📈 Winner")
871
+ diff = comparison['difference']['reject_rate']
872
+
873
+ if diff < -1:
874
+ st.success("🏆 Experiment A")
875
+ st.metric("Better by", f"{abs(diff):.1f}%")
876
+ elif diff > 1:
877
+ st.success("🏆 Experiment B")
878
+ st.metric("Better by", f"{abs(diff):.1f}%")
879
+ else:
880
+ st.info("➡️ Similar Performance")
881
+
882
+ # Side-by-side rejection reasons comparison
883
+ st.markdown("#### Rejection Reasons Comparison")
884
+
885
+ col1, col2 = st.columns(2)
886
+
887
+ with col1:
888
+ reasons_a = comparison['experiment_a']['stats']['rejection_reasons']
889
+ if len(reasons_a) > 0:
890
+ df_a = pd.DataFrame([
891
+ {"Reason": k, "Count": v}
892
+ for k, v in reasons_a.items()
893
+ ])
894
+
895
+ fig_a = px.bar(
896
+ df_a,
897
+ x='Reason',
898
+ y='Count',
899
+ title="Experiment A - Rejection Reasons",
900
+ color='Count',
901
+ color_continuous_scale='Blues'
902
+ )
903
+
904
+ fig_a.update_layout(
905
+ template="plotly_dark",
906
+ paper_bgcolor='rgba(0,0,0,0)',
907
+ plot_bgcolor='rgba(0,0,0,0)',
908
+ xaxis_tickangle=-45,
909
+ height=300
910
+ )
911
+
912
+ st.plotly_chart(fig_a, use_container_width=True)
913
+ else:
914
+ st.info("No rejection feedback for Experiment A")
915
+
916
+ with col2:
917
+ reasons_b = comparison['experiment_b']['stats']['rejection_reasons']
918
+ if len(reasons_b) > 0:
919
+ df_b = pd.DataFrame([
920
+ {"Reason": k, "Count": v}
921
+ for k, v in reasons_b.items()
922
+ ])
923
+
924
+ fig_b = px.bar(
925
+ df_b,
926
+ x='Reason',
927
+ y='Count',
928
+ title="Experiment B - Rejection Reasons",
929
+ color='Count',
930
+ color_continuous_scale='Reds'
931
+ )
932
+
933
+ fig_b.update_layout(
934
+ template="plotly_dark",
935
+ paper_bgcolor='rgba(0,0,0,0)',
936
+ plot_bgcolor='rgba(0,0,0,0)',
937
+ xaxis_tickangle=-45,
938
+ height=300
939
+ )
940
+
941
+ st.plotly_chart(fig_b, use_container_width=True)
942
+ else:
943
+ st.info("No rejection feedback for Experiment B")
944
+ else:
945
+ st.info("ℹ️ Run A/B tests to see comparative analysis here")
946
+
947
+ st.markdown("---")
948
+
949
+ # Export options
950
+ st.header("💾 Export Data")
951
+
952
+ if is_ab_mode:
953
+ # A/B Testing Mode - Export both experiments
954
+ st.markdown("##### Export Experiment Data")
955
+
956
+ col1, col2 = st.columns(2)
957
+
958
+ with col1:
959
+ st.markdown("**🅰️ Experiment A**")
960
+
961
+ if st.button("📥 Export Messages A", use_container_width=True):
962
+ csv = filtered_messages_a.to_csv(index=False, encoding='utf-8')
963
+ st.download_button(
964
+ label="⬇️ Download Messages A CSV",
965
+ data=csv,
966
+ file_name=f"{brand}_{experiment_a_id}_messages.csv",
967
+ mime="text/csv",
968
+ use_container_width=True
969
+ )
970
+
971
+ feedback_a_df = feedback_manager.load_feedback(experiment_a_id)
972
+ if feedback_a_df is not None and len(feedback_a_df) > 0:
973
+ if st.button("📥 Export Feedback A", use_container_width=True):
974
+ csv = feedback_a_df.to_csv(index=False, encoding='utf-8')
975
+ st.download_button(
976
+ label="⬇️ Download Feedback A CSV",
977
+ data=csv,
978
+ file_name=f"{brand}_{experiment_a_id}_feedback.csv",
979
+ mime="text/csv",
980
+ use_container_width=True
981
+ )
982
+
983
+ if len(stage_feedback_stats_a) > 0:
984
+ if st.button("📥 Export Analytics A", use_container_width=True):
985
+ csv = stage_feedback_stats_a.to_csv(index=False, encoding='utf-8')
986
+ st.download_button(
987
+ label="⬇️ Download Analytics A CSV",
988
+ data=csv,
989
+ file_name=f"{brand}_{experiment_a_id}_analytics.csv",
990
+ mime="text/csv",
991
+ use_container_width=True
992
+ )
993
+
994
+ with col2:
995
+ st.markdown("**🅱️ Experiment B**")
996
+
997
+ if st.button("📥 Export Messages B", use_container_width=True):
998
+ csv = filtered_messages_b.to_csv(index=False, encoding='utf-8')
999
+ st.download_button(
1000
+ label="⬇️ Download Messages B CSV",
1001
+ data=csv,
1002
+ file_name=f"{brand}_{experiment_b_id}_messages.csv",
1003
+ mime="text/csv",
1004
+ use_container_width=True
1005
+ )
1006
+
1007
+ feedback_b_df = feedback_manager.load_feedback(experiment_b_id)
1008
+ if feedback_b_df is not None and len(feedback_b_df) > 0:
1009
+ if st.button("📥 Export Feedback B", use_container_width=True):
1010
+ csv = feedback_b_df.to_csv(index=False, encoding='utf-8')
1011
+ st.download_button(
1012
+ label="⬇️ Download Feedback B CSV",
1013
+ data=csv,
1014
+ file_name=f"{brand}_{experiment_b_id}_feedback.csv",
1015
+ mime="text/csv",
1016
+ use_container_width=True
1017
+ )
1018
+
1019
+ if len(stage_feedback_stats_b) > 0:
1020
+ if st.button("📥 Export Analytics B", use_container_width=True):
1021
+ csv = stage_feedback_stats_b.to_csv(index=False, encoding='utf-8')
1022
+ st.download_button(
1023
+ label="⬇️ Download Analytics B CSV",
1024
+ data=csv,
1025
+ file_name=f"{brand}_{experiment_b_id}_analytics.csv",
1026
+ mime="text/csv",
1027
+ use_container_width=True
1028
+ )
1029
+
1030
+ else:
1031
+ # Single Experiment Mode - Original behavior
1032
+ col1, col2, col3 = st.columns(3)
1033
+
1034
+ with col1:
1035
+ if st.button("📥 Export Messages", use_container_width=True):
1036
+ csv = filtered_messages.to_csv(index=False, encoding='utf-8')
1037
+ st.download_button(
1038
+ label="⬇️ Download Messages CSV",
1039
+ data=csv,
1040
+ file_name=f"{brand}_{experiment_id}_messages.csv",
1041
+ mime="text/csv",
1042
+ use_container_width=True
1043
+ )
1044
+
1045
+ with col2:
1046
+ feedback_df = feedback_manager.load_feedback(experiment_id)
1047
+ if feedback_df is not None and len(feedback_df) > 0:
1048
+ if st.button("📥 Export Feedback", use_container_width=True):
1049
+ csv = feedback_df.to_csv(index=False, encoding='utf-8')
1050
+ st.download_button(
1051
+ label="⬇️ Download Feedback CSV",
1052
+ data=csv,
1053
+ file_name=f"{brand}_{experiment_id}_feedback.csv",
1054
+ mime="text/csv",
1055
+ use_container_width=True
1056
+ )
1057
+
1058
+ with col3:
1059
+ if len(stage_feedback_stats) > 0:
1060
+ if st.button("📥 Export Analytics", use_container_width=True):
1061
+ csv = stage_feedback_stats.to_csv(index=False, encoding='utf-8')
1062
+ st.download_button(
1063
+ label="⬇️ Download Analytics CSV",
1064
+ data=csv,
1065
+ file_name=f"{brand}_{experiment_id}_analytics.csv",
1066
+ mime="text/csv",
1067
+ use_container_width=True
1068
+ )
1069
+
1070
+ st.markdown("---")
pages/5_Historical_Analytics.py CHANGED
@@ -1,24 +1,519 @@
1
  """
2
- Historical Analytics Page - HuggingFace Space Wrapper
3
- Redirects to the actual page in visualization/pages/
4
  """
5
 
 
6
  import sys
7
  from pathlib import Path
 
 
8
 
9
- # Set up paths
10
- root_dir = Path(__file__).parent.parent
11
- visualization_dir = root_dir / "visualization"
 
 
 
 
12
 
13
- # Add directories to path
14
- sys.path.insert(0, str(root_dir))
15
- sys.path.insert(0, str(root_dir / "ai_messaging_system_v2"))
16
- sys.path.insert(0, str(visualization_dir))
17
 
18
- # Import and run the actual page
19
- import importlib.util
 
 
20
 
21
- page_path = visualization_dir / "pages" / "5_Historical_Analytics.py"
22
- spec = importlib.util.spec_from_file_location("historical_analytics", page_path)
23
- historical_analytics = importlib.util.module_from_spec(spec)
24
- spec.loader.exec_module(historical_analytics)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  """
2
+ Page 5: Historical Analytics
3
+ View performance metrics and insights from all previous experiments.
4
  """
5
 
6
+ import streamlit as st
7
  import sys
8
  from pathlib import Path
9
+ import pandas as pd
10
+ from datetime import datetime
11
 
12
+ # Try to import plotly with helpful error message
13
+ try:
14
+ import plotly.express as px
15
+ import plotly.graph_objects as go
16
+ except ModuleNotFoundError:
17
+ st.error("""
18
+ ⚠️ **Missing Dependency: plotly**
19
 
20
+ Plotly is not installed in the current Python environment.
 
 
 
21
 
22
+ **To fix this:**
23
+ 1. Make sure you're running Streamlit from the correct Python environment
24
+ 2. Install plotly: `pip install plotly>=5.17.0`
25
+ 3. Or install all requirements: `pip install -r visualization/requirements.txt`
26
 
27
+ **Current Python:** {}
28
+ """.format(sys.executable))
29
+ st.stop()
30
+
31
+ # Add root directory to path
32
+ sys.path.insert(0, str(Path(__file__).parent.parent))
33
+
34
+ from utils.auth import check_authentication
35
+ from utils.theme import apply_theme, get_brand_emoji, get_brand_theme
36
+ from utils.data_loader import DataLoader
37
+ from utils.feedback_manager import FeedbackManager
38
+
39
+ # Page configuration
40
+ st.set_page_config(
41
+ page_title="Historical Analytics",
42
+ page_icon="📚",
43
+ layout="wide"
44
+ )
45
+
46
+ # Check authentication
47
+ if not check_authentication():
48
+ st.error("🔒 Please login first")
49
+ st.stop()
50
+
51
+ # Initialize utilities
52
+ data_loader = DataLoader()
53
+ feedback_manager = FeedbackManager()
54
+
55
+ # Check if brand is selected
56
+ if "selected_brand" not in st.session_state or not st.session_state.selected_brand:
57
+ st.error("⚠️ Please select a brand from the home page first")
58
+ st.stop()
59
+
60
+ brand = st.session_state.selected_brand
61
+ apply_theme(brand)
62
+ theme = get_brand_theme(brand)
63
+
64
+ # Page Header
65
+ emoji = get_brand_emoji(brand)
66
+ st.title(f"📚 Historical Analytics - {emoji} {brand.title()}")
67
+ st.markdown("**View performance metrics from all previous experiments**")
68
+
69
+ st.markdown("---")
70
+
71
+ # Load all experiment files
72
+ ui_output_path = data_loader.ui_output_path
73
+ experiment_files = list(ui_output_path.glob("messages_*.csv"))
74
+
75
+ # Filter out current messages.csv and A/B experiment files for separate handling
76
+ regular_experiments = [f for f in experiment_files if f.name != "messages.csv" and "_a_" not in f.name.lower() and "_b_" not in f.name.lower()]
77
+ ab_experiments = [f for f in experiment_files if "_a_" in f.name.lower() or "_b_" in f.name.lower()]
78
+
79
+ if len(regular_experiments) == 0 and len(ab_experiments) == 0:
80
+ st.info("ℹ️ No historical experiments found yet. Run experiments in Campaign Builder to see historical data here.")
81
+ st.stop()
82
+
83
+ # Sidebar - Filters
84
+ with st.sidebar:
85
+ st.header("🔍 Filters")
86
+
87
+ # Date range filter
88
+ st.subheader("Date Range")
89
+ show_all = st.checkbox("Show All Experiments", value=True)
90
+
91
+ if not show_all:
92
+ # Extract dates from filenames for filtering
93
+ all_dates = []
94
+ for f in regular_experiments + ab_experiments:
95
+ # Try to extract date from filename (format: messages_brand_YYYYMMDD_HHMM.csv)
96
+ try:
97
+ parts = f.stem.split('_')
98
+ if len(parts) >= 3:
99
+ date_str = parts[-2] # YYYYMMDD
100
+ date_obj = datetime.strptime(date_str, '%Y%m%d')
101
+ all_dates.append(date_obj.date())
102
+ except:
103
+ continue
104
+
105
+ if len(all_dates) > 0:
106
+ min_date = min(all_dates)
107
+ max_date = max(all_dates)
108
+
109
+ start_date = st.date_input("Start Date", min_date)
110
+ end_date = st.date_input("End Date", max_date)
111
+
112
+ st.markdown("---")
113
+
114
+ # Experiment type filter
115
+ st.subheader("Experiment Type")
116
+ show_regular = st.checkbox("Regular Experiments", value=True)
117
+ show_ab = st.checkbox("A/B Tests", value=True)
118
+
119
+ # ============================================================================
120
+ # OVERVIEW STATISTICS
121
+ # ============================================================================
122
+ st.header("📊 Overview Statistics")
123
+
124
+ col1, col2, col3, col4 = st.columns(4)
125
+
126
+ with col1:
127
+ st.metric("Total Experiments", len(regular_experiments) + (len(ab_experiments) // 2))
128
+
129
+ with col2:
130
+ st.metric("Regular Experiments", len(regular_experiments))
131
+
132
+ with col3:
133
+ st.metric("A/B Tests", len(ab_experiments) // 2)
134
+
135
+ with col4:
136
+ # Calculate total messages across all experiments
137
+ total_messages = 0
138
+ for f in regular_experiments + ab_experiments:
139
+ try:
140
+ df = pd.read_csv(f, encoding='utf-8-sig')
141
+ total_messages += len(df)
142
+ except:
143
+ continue
144
+ st.metric("Total Messages Generated", total_messages)
145
+
146
+ st.markdown("---")
147
+
148
+ # ============================================================================
149
+ # REGULAR EXPERIMENTS
150
+ # ============================================================================
151
+ if show_regular and len(regular_experiments) > 0:
152
+ st.header("📈 Regular Experiments")
153
+
154
+ # Create summary table
155
+ experiment_summary = []
156
+
157
+ for exp_file in sorted(regular_experiments, key=lambda x: x.stat().st_mtime, reverse=True):
158
+ try:
159
+ # Load experiment data
160
+ messages_df = pd.read_csv(exp_file, encoding='utf-8-sig')
161
+
162
+ # Extract experiment info
163
+ exp_id = exp_file.stem
164
+ exp_name = exp_file.stem.replace('messages_', '')
165
+
166
+ # Get timestamp from file
167
+ file_timestamp = datetime.fromtimestamp(exp_file.stat().st_mtime)
168
+
169
+ # Get feedback stats
170
+ feedback_stats = feedback_manager.get_feedback_stats(exp_id, total_messages=len(messages_df))
171
+
172
+ experiment_summary.append({
173
+ "Experiment": exp_name,
174
+ "Date": file_timestamp.strftime('%Y-%m-%d %H:%M'),
175
+ "Messages": len(messages_df),
176
+ "Users": messages_df['user_id'].nunique() if 'user_id' in messages_df.columns else 0,
177
+ "Stages": messages_df['stage'].nunique() if 'stage' in messages_df.columns else 0,
178
+ "Rejection Rate (%)": round(feedback_stats['reject_rate'], 1),
179
+ "Total Feedback": feedback_stats['total_feedback'],
180
+ "exp_id": exp_id,
181
+ "exp_file": exp_file
182
+ })
183
+ except Exception as e:
184
+ st.warning(f"⚠️ Error loading {exp_file.name}: {str(e)}")
185
+ continue
186
+
187
+ if len(experiment_summary) > 0:
188
+ summary_df = pd.DataFrame(experiment_summary)
189
+
190
+ # Display summary table
191
+ st.dataframe(
192
+ summary_df.drop(columns=['exp_id', 'exp_file']),
193
+ use_container_width=True,
194
+ hide_index=True
195
+ )
196
+
197
+ st.markdown("---")
198
+
199
+ # Rejection rate trend over time
200
+ st.subheader("📉 Rejection Rate Trend Over Time")
201
+
202
+ fig = go.Figure()
203
+
204
+ fig.add_trace(go.Scatter(
205
+ x=summary_df['Date'],
206
+ y=summary_df['Rejection Rate (%)'],
207
+ mode='lines+markers',
208
+ name='Rejection Rate',
209
+ line=dict(color=theme['accent'], width=3),
210
+ marker=dict(size=10)
211
+ ))
212
+
213
+ fig.update_layout(
214
+ xaxis_title="Experiment Date",
215
+ yaxis_title="Rejection Rate (%)",
216
+ template="plotly_dark",
217
+ paper_bgcolor='rgba(0,0,0,0)',
218
+ plot_bgcolor='rgba(0,0,0,0)',
219
+ hovermode='x unified'
220
+ )
221
+
222
+ st.plotly_chart(fig, use_container_width=True)
223
+
224
+ st.markdown("---")
225
+
226
+ # Detailed experiment view
227
+ st.subheader("🔍 Detailed Experiment View")
228
+
229
+ selected_exp = st.selectbox(
230
+ "Select Experiment to View Details",
231
+ summary_df['Experiment'].tolist(),
232
+ key="selected_regular_exp"
233
+ )
234
+
235
+ if selected_exp:
236
+ # Get selected experiment data
237
+ selected_row = summary_df[summary_df['Experiment'] == selected_exp].iloc[0]
238
+ exp_id = selected_row['exp_id']
239
+ exp_file = selected_row['exp_file']
240
+
241
+ # Load messages and feedback
242
+ messages_df = pd.read_csv(exp_file, encoding='utf-8-sig')
243
+ feedback_df = feedback_manager.load_feedback(exp_id)
244
+
245
+ col1, col2 = st.columns(2)
246
+
247
+ with col1:
248
+ st.markdown("### 📊 Experiment Metrics")
249
+ st.metric("Total Messages", len(messages_df))
250
+ st.metric("Unique Users", messages_df['user_id'].nunique() if 'user_id' in messages_df.columns else 0)
251
+ st.metric("Stages", messages_df['stage'].nunique() if 'stage' in messages_df.columns else 0)
252
+ st.metric("Rejection Rate", f"{selected_row['Rejection Rate (%)']}%")
253
+
254
+ with col2:
255
+ st.markdown("### 🔍 Rejection Reasons")
256
+ if feedback_df is not None and len(feedback_df) > 0:
257
+ reject_df = feedback_df[feedback_df['feedback_type'] == 'reject']
258
+
259
+ if len(reject_df) > 0:
260
+ # Get rejection reasons
261
+ reason_counts = reject_df['rejection_reason'].value_counts()
262
+
263
+ # Map to labels
264
+ reason_labels = {
265
+ feedback_manager.get_rejection_reason_label(k): v
266
+ for k, v in reason_counts.items()
267
+ if pd.notna(k)
268
+ }
269
+
270
+ # Create pie chart
271
+ if len(reason_labels) > 0:
272
+ fig = px.pie(
273
+ names=list(reason_labels.keys()),
274
+ values=list(reason_labels.values()),
275
+ color_discrete_sequence=px.colors.qualitative.Set3
276
+ )
277
+
278
+ fig.update_layout(
279
+ template="plotly_dark",
280
+ paper_bgcolor='rgba(0,0,0,0)',
281
+ plot_bgcolor='rgba(0,0,0,0)',
282
+ height=300
283
+ )
284
+
285
+ st.plotly_chart(fig, use_container_width=True)
286
+ else:
287
+ st.info("No rejection feedback for this experiment")
288
+ else:
289
+ st.info("No feedback data available")
290
+
291
+ # Stage-by-stage performance
292
+ st.markdown("### 📈 Stage-by-Stage Performance")
293
+ stage_stats = feedback_manager.get_stage_feedback_stats(exp_id, messages_df=messages_df)
294
+
295
+ if len(stage_stats) > 0:
296
+ fig = go.Figure()
297
+
298
+ fig.add_trace(go.Bar(
299
+ x=stage_stats['stage'],
300
+ y=stage_stats['reject_rate'],
301
+ name='Rejection Rate (%)',
302
+ marker_color=theme['accent'],
303
+ text=stage_stats['reject_rate'].round(1),
304
+ textposition='auto',
305
+ ))
306
+
307
+ fig.update_layout(
308
+ xaxis_title="Stage",
309
+ yaxis_title="Rejection Rate (%)",
310
+ template="plotly_dark",
311
+ paper_bgcolor='rgba(0,0,0,0)',
312
+ plot_bgcolor='rgba(0,0,0,0)',
313
+ )
314
+
315
+ st.plotly_chart(fig, use_container_width=True)
316
+ else:
317
+ st.info("No stage performance data available")
318
+
319
+ # Show rejected messages with headers
320
+ if feedback_df is not None and len(feedback_df) > 0:
321
+ reject_df = feedback_df[feedback_df['feedback_type'] == 'reject']
322
+
323
+ if len(reject_df) > 0 and 'message_header' in reject_df.columns:
324
+ st.markdown("### ❌ Rejected Messages")
325
+
326
+ with st.expander("View Rejected Messages"):
327
+ display_df = reject_df[[
328
+ 'user_id', 'stage', 'message_header', 'message_body',
329
+ 'rejection_reason', 'rejection_text'
330
+ ]].copy()
331
+
332
+ # Map rejection reason keys to labels
333
+ display_df['rejection_reason'] = display_df['rejection_reason'].apply(
334
+ lambda x: feedback_manager.get_rejection_reason_label(x) if pd.notna(x) else 'N/A'
335
+ )
336
+
337
+ st.dataframe(display_df, use_container_width=True, hide_index=True)
338
+
339
+ st.markdown("---")
340
+
341
+ # ============================================================================
342
+ # A/B TEST EXPERIMENTS
343
+ # ============================================================================
344
+ if show_ab and len(ab_experiments) > 0:
345
+ st.header("🧪 A/B Test Experiments")
346
+
347
+ # Group A/B experiments into pairs
348
+ ab_pairs = {}
349
+ for exp_file in ab_experiments:
350
+ exp_name = exp_file.stem
351
+ if '_a_' in exp_name.lower():
352
+ base_name = exp_name.lower().replace('messages_a_', '').replace('_a_', '_')
353
+ if base_name not in ab_pairs:
354
+ ab_pairs[base_name] = {}
355
+ ab_pairs[base_name]['A'] = exp_file
356
+ elif '_b_' in exp_name.lower():
357
+ base_name = exp_name.lower().replace('messages_b_', '').replace('_b_', '_')
358
+ if base_name not in ab_pairs:
359
+ ab_pairs[base_name] = {}
360
+ ab_pairs[base_name]['B'] = exp_file
361
+
362
+ # Display A/B test pairs
363
+ for base_name, pair in sorted(ab_pairs.items(), key=lambda x: x[0], reverse=True):
364
+ if 'A' in pair and 'B' in pair:
365
+ with st.expander(f"🧪 {base_name.replace('_', ' ').title()}", expanded=False):
366
+ exp_a_file = pair['A']
367
+ exp_b_file = pair['B']
368
+
369
+ exp_a_id = exp_a_file.stem
370
+ exp_b_id = exp_b_file.stem
371
+
372
+ try:
373
+ # Load both experiments
374
+ messages_a = pd.read_csv(exp_a_file, encoding='utf-8-sig')
375
+ messages_b = pd.read_csv(exp_b_file, encoding='utf-8-sig')
376
+
377
+ # Get feedback stats
378
+ stats_a = feedback_manager.get_feedback_stats(exp_a_id, total_messages=len(messages_a))
379
+ stats_b = feedback_manager.get_feedback_stats(exp_b_id, total_messages=len(messages_b))
380
+
381
+ # Comparison metrics
382
+ col1, col2, col3 = st.columns(3)
383
+
384
+ with col1:
385
+ st.markdown("### 🅰️ Experiment A")
386
+ st.metric("Messages", len(messages_a))
387
+ st.metric("Rejection Rate", f"{stats_a['reject_rate']:.1f}%")
388
+ st.metric("Total Feedback", stats_a['total_feedback'])
389
+
390
+ with col2:
391
+ st.markdown("### 🅱️ Experiment B")
392
+ st.metric("Messages", len(messages_b))
393
+ st.metric("Rejection Rate", f"{stats_b['reject_rate']:.1f}%")
394
+ st.metric("Total Feedback", stats_b['total_feedback'])
395
+
396
+ with col3:
397
+ st.markdown("### 🏆 Winner")
398
+ diff = stats_a['reject_rate'] - stats_b['reject_rate']
399
+
400
+ if diff < -1:
401
+ st.success("🏆 Experiment A")
402
+ st.metric("Better by", f"{abs(diff):.1f}%")
403
+ elif diff > 1:
404
+ st.success("🏆 Experiment B")
405
+ st.metric("Better by", f"{abs(diff):.1f}%")
406
+ else:
407
+ st.info("➡️ Similar Performance")
408
+
409
+ st.markdown("---")
410
+
411
+ # Side-by-side rejection reasons comparison
412
+ st.markdown("#### Rejection Reasons Comparison")
413
+
414
+ col1, col2 = st.columns(2)
415
+
416
+ with col1:
417
+ reasons_a = stats_a['rejection_reasons']
418
+ if len(reasons_a) > 0:
419
+ df_a = pd.DataFrame([
420
+ {"Reason": k, "Count": v}
421
+ for k, v in reasons_a.items()
422
+ ])
423
+
424
+ fig_a = px.bar(
425
+ df_a,
426
+ x='Reason',
427
+ y='Count',
428
+ title="Experiment A - Rejection Reasons",
429
+ color='Count',
430
+ color_continuous_scale='Blues'
431
+ )
432
+
433
+ fig_a.update_layout(
434
+ template="plotly_dark",
435
+ paper_bgcolor='rgba(0,0,0,0)',
436
+ plot_bgcolor='rgba(0,0,0,0)',
437
+ xaxis_tickangle=-45,
438
+ height=300
439
+ )
440
+
441
+ st.plotly_chart(fig_a, use_container_width=True)
442
+ else:
443
+ st.info("No rejection feedback for Experiment A")
444
+
445
+ with col2:
446
+ reasons_b = stats_b['rejection_reasons']
447
+ if len(reasons_b) > 0:
448
+ df_b = pd.DataFrame([
449
+ {"Reason": k, "Count": v}
450
+ for k, v in reasons_b.items()
451
+ ])
452
+
453
+ fig_b = px.bar(
454
+ df_b,
455
+ x='Reason',
456
+ y='Count',
457
+ title="Experiment B - Rejection Reasons",
458
+ color='Count',
459
+ color_continuous_scale='Reds'
460
+ )
461
+
462
+ fig_b.update_layout(
463
+ template="plotly_dark",
464
+ paper_bgcolor='rgba(0,0,0,0)',
465
+ plot_bgcolor='rgba(0,0,0,0)',
466
+ xaxis_tickangle=-45,
467
+ height=300
468
+ )
469
+
470
+ st.plotly_chart(fig_b, use_container_width=True)
471
+ else:
472
+ st.info("No rejection feedback for Experiment B")
473
+
474
+ except Exception as e:
475
+ st.error(f"Error loading A/B test data: {str(e)}")
476
+
477
+ st.markdown("---")
478
+
479
+ # Export options
480
+ st.header("💾 Export Historical Data")
481
+
482
+ col1, col2 = st.columns(2)
483
+
484
+ with col1:
485
+ if st.button("📥 Export All Experiments Summary", use_container_width=True):
486
+ if len(experiment_summary) > 0:
487
+ summary_export_df = pd.DataFrame(experiment_summary).drop(columns=['exp_id', 'exp_file'])
488
+ csv = summary_export_df.to_csv(index=False, encoding='utf-8')
489
+ st.download_button(
490
+ label="⬇️ Download Summary CSV",
491
+ data=csv,
492
+ file_name=f"{brand}_experiments_summary_{datetime.now().strftime('%Y%m%d')}.csv",
493
+ mime="text/csv",
494
+ use_container_width=True
495
+ )
496
+
497
+ with col2:
498
+ if st.button("📥 Export All Feedback", use_container_width=True):
499
+ # Combine all feedback files
500
+ all_feedback = []
501
+ for exp_file in regular_experiments + ab_experiments:
502
+ exp_id = exp_file.stem
503
+ feedback_df = feedback_manager.load_feedback(exp_id)
504
+ if feedback_df is not None and len(feedback_df) > 0:
505
+ all_feedback.append(feedback_df)
506
+
507
+ if len(all_feedback) > 0:
508
+ combined_feedback = pd.concat(all_feedback, ignore_index=True)
509
+ csv = combined_feedback.to_csv(index=False, encoding='utf-8')
510
+ st.download_button(
511
+ label="⬇️ Download All Feedback CSV",
512
+ data=csv,
513
+ file_name=f"{brand}_all_feedback_{datetime.now().strftime('%Y%m%d')}.csv",
514
+ mime="text/csv",
515
+ use_container_width=True
516
+ )
517
+
518
+ st.markdown("---")
519
+ st.markdown("**💡 Tip:** Use historical analytics to track improvements over time and identify patterns in rejection reasons!")
utils/__init__.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Utility modules for the AI Messaging System Visualization Tool.
3
+
4
+ Modules:
5
+ - auth: Authentication and access control
6
+ - data_loader: Data loading from Snowflake and CSV files
7
+ - config_manager: Configuration persistence and management
8
+ - theme: Brand-specific theming and styling
9
+ - feedback_manager: Feedback storage and retrieval
10
+ """
11
+
12
+ from .auth import verify_login, check_authentication, AUTHORIZED_EMAILS
13
+ from .theme import get_brand_theme, apply_theme, BRAND_COLORS
14
+ from .data_loader import DataLoader
15
+ from .config_manager import ConfigManager
16
+ from .feedback_manager import FeedbackManager
17
+
18
+ __all__ = [
19
+ 'verify_login',
20
+ 'check_authentication',
21
+ 'AUTHORIZED_EMAILS',
22
+ 'get_brand_theme',
23
+ 'apply_theme',
24
+ 'BRAND_COLORS',
25
+ 'DataLoader',
26
+ 'ConfigManager',
27
+ 'FeedbackManager',
28
+ ]
utils/auth.py ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Authentication module for the AI Messaging System Visualization Tool.
3
+
4
+ Handles user authentication and access control.
5
+ """
6
+
7
+ import os
8
+ import streamlit as st
9
+ from pathlib import Path
10
+ from dotenv import load_dotenv
11
+
12
+ # Load environment variables from .env file in visualization directory
13
+ env_path = Path(__file__).parent.parent / '.env'
14
+ if env_path.exists():
15
+ load_dotenv(env_path)
16
+ else:
17
+ # Try parent directory .env
18
+ parent_env_path = Path(__file__).parent.parent.parent / '.env'
19
+ if parent_env_path.exists():
20
+ load_dotenv(parent_env_path)
21
+
22
+ # Authorized emails - team members only
23
+ AUTHORIZED_EMAILS = {
24
+ "danial@musora.com",
25
+ "danial.ebrat@gmail.com",
26
+ "simon@musora.com",
27
+ "una@musora.com",
28
+ "mark@musora.com",
29
+ "gabriel@musora.com",
30
+ "nikki@musora.com"
31
+ }
32
+
33
+
34
+ def get_credential(key: str) -> str:
35
+ """
36
+ Get credential from environment variables.
37
+
38
+ Args:
39
+ key: Credential key
40
+
41
+ Returns:
42
+ str: Credential value
43
+ """
44
+ return os.getenv(key, "")
45
+
46
+
47
+ def get_valid_token() -> str:
48
+ """
49
+ Get the valid access token from environment.
50
+
51
+ Returns:
52
+ str: Valid access token
53
+ """
54
+ return get_credential("APP_TOKEN")
55
+
56
+
57
+ def verify_login(email: str, token: str) -> bool:
58
+ """
59
+ Verify user login credentials.
60
+
61
+ Args:
62
+ email: User email address
63
+ token: Access token
64
+
65
+ Returns:
66
+ bool: True if credentials are valid, False otherwise
67
+ """
68
+ valid_token = get_valid_token()
69
+ email_normalized = email.lower().strip()
70
+
71
+ return (email_normalized in AUTHORIZED_EMAILS) and (token == valid_token)
72
+
73
+
74
+ def check_authentication() -> bool:
75
+ """
76
+ Check if user is authenticated in current session.
77
+
78
+ Returns:
79
+ bool: True if authenticated, False otherwise
80
+ """
81
+ return st.session_state.get("authenticated", False)
82
+
83
+
84
+ def get_current_user() -> str:
85
+ """
86
+ Get the currently logged-in user's email.
87
+
88
+ Returns:
89
+ str: User email or empty string if not authenticated
90
+ """
91
+ return st.session_state.get("user_email", "")
92
+
93
+
94
+ def logout():
95
+ """
96
+ Log out the current user by clearing session state.
97
+ """
98
+ if "authenticated" in st.session_state:
99
+ del st.session_state["authenticated"]
100
+ if "user_email" in st.session_state:
101
+ del st.session_state["user_email"]
utils/config_manager.py ADDED
@@ -0,0 +1,313 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Configuration management module for the AI Messaging System Visualization Tool.
3
+
4
+ Handles saving, loading, and managing campaign configurations.
5
+ """
6
+
7
+ import json
8
+ from pathlib import Path
9
+ from typing import Dict, List, Optional
10
+ import streamlit as st
11
+
12
+
13
+ class ConfigManager:
14
+ """
15
+ Manages campaign configurations including loading defaults and persisting custom configs.
16
+ """
17
+
18
+ def __init__(self):
19
+ """Initialize ConfigManager."""
20
+ self.base_path = Path(__file__).parent.parent
21
+ self.configs_path = self.base_path / "data" / "configs"
22
+
23
+ # Create directory if it doesn't exist
24
+ self.configs_path.mkdir(parents=True, exist_ok=True)
25
+
26
+ def load_default_config(self, brand: str, campaign_type: str = "re_engagement") -> Dict:
27
+ """
28
+ Load default configuration from local JSON files.
29
+
30
+ Args:
31
+ brand: Brand name
32
+ campaign_type: Campaign type (default: re_engagement)
33
+
34
+ Returns:
35
+ dict: Campaign configuration
36
+ """
37
+ try:
38
+ # Load from local configs directory
39
+ filename = f"{brand}_{campaign_type}_test.json"
40
+ file_path = self.configs_path / filename
41
+
42
+ if not file_path.exists():
43
+ st.error(f"Default config file not found: {filename}")
44
+ return {}
45
+
46
+ with open(file_path, 'r', encoding='utf-8') as f:
47
+ config = json.load(f)
48
+
49
+ return config
50
+ except Exception as e:
51
+ st.error(f"Error loading default config: {e}")
52
+ return {}
53
+
54
+ def save_custom_config(self, brand: str, config_name: str, config: Dict) -> bool:
55
+ """
56
+ Save custom configuration to JSON file.
57
+
58
+ Args:
59
+ brand: Brand name
60
+ config_name: Configuration name
61
+ config: Configuration dictionary
62
+
63
+ Returns:
64
+ bool: True if saved successfully, False otherwise
65
+ """
66
+ try:
67
+ # Create filename
68
+ filename = f"{brand}_{config_name}.json"
69
+ file_path = self.configs_path / filename
70
+
71
+ # Save to JSON
72
+ with open(file_path, 'w', encoding='utf-8') as f:
73
+ json.dump(config, f, indent=2, ensure_ascii=False)
74
+
75
+ return True
76
+
77
+ except Exception as e:
78
+ st.error(f"Error saving config: {e}")
79
+ return False
80
+
81
+ def load_custom_config(self, brand: str, config_name: str) -> Optional[Dict]:
82
+ """
83
+ Load custom configuration from JSON file.
84
+
85
+ Args:
86
+ brand: Brand name
87
+ config_name: Configuration name
88
+
89
+ Returns:
90
+ dict or None: Configuration dictionary or None if not found
91
+ """
92
+ try:
93
+ filename = f"{brand}_{config_name}.json"
94
+ file_path = self.configs_path / filename
95
+
96
+ if not file_path.exists():
97
+ return None
98
+
99
+ with open(file_path, 'r', encoding='utf-8') as f:
100
+ config = json.load(f)
101
+
102
+ return config
103
+
104
+ except Exception as e:
105
+ st.error(f"Error loading config: {e}")
106
+ return None
107
+
108
+ def list_custom_configs(self, brand: str) -> List[str]:
109
+ """
110
+ List all custom configurations for a brand.
111
+
112
+ Args:
113
+ brand: Brand name
114
+
115
+ Returns:
116
+ list: List of configuration names
117
+ """
118
+ try:
119
+ # Find all JSON files for this brand
120
+ pattern = f"{brand}_*.json"
121
+ config_files = list(self.configs_path.glob(pattern))
122
+
123
+ # Extract config names (remove brand prefix and .json extension)
124
+ config_names = []
125
+ for file in config_files:
126
+ name = file.stem # filename without extension
127
+ # Remove brand prefix
128
+ config_name = name.replace(f"{brand}_", "", 1)
129
+ config_names.append(config_name)
130
+
131
+ return sorted(config_names)
132
+
133
+ except Exception as e:
134
+ st.error(f"Error listing configs: {e}")
135
+ return []
136
+
137
+ def delete_custom_config(self, brand: str, config_name: str) -> bool:
138
+ """
139
+ Delete a custom configuration.
140
+
141
+ Args:
142
+ brand: Brand name
143
+ config_name: Configuration name
144
+
145
+ Returns:
146
+ bool: True if deleted successfully, False otherwise
147
+ """
148
+ try:
149
+ filename = f"{brand}_{config_name}.json"
150
+ file_path = self.configs_path / filename
151
+
152
+ if file_path.exists():
153
+ file_path.unlink()
154
+ return True
155
+ return False
156
+
157
+ except Exception as e:
158
+ st.error(f"Error deleting config: {e}")
159
+ return False
160
+
161
+ def config_exists(self, brand: str, config_name: str) -> bool:
162
+ """
163
+ Check if a custom configuration exists.
164
+
165
+ Args:
166
+ brand: Brand name
167
+ config_name: Configuration name
168
+
169
+ Returns:
170
+ bool: True if exists, False otherwise
171
+ """
172
+ filename = f"{brand}_{config_name}.json"
173
+ file_path = self.configs_path / filename
174
+ return file_path.exists()
175
+
176
+ def get_all_configs(self, brand: str) -> Dict[str, Dict]:
177
+ """
178
+ Get all configurations (default + custom) for a brand.
179
+
180
+ Args:
181
+ brand: Brand name
182
+
183
+ Returns:
184
+ dict: Dictionary mapping config names to config dictionaries
185
+ """
186
+ configs = {}
187
+
188
+ # Add default config as "re_engagement_test"
189
+ default_config = self.load_default_config(brand)
190
+ if default_config:
191
+ configs["re_engagement_test"] = default_config
192
+
193
+ # Add custom configs
194
+ custom_names = self.list_custom_configs(brand)
195
+ for name in custom_names:
196
+ # Skip re_engagement_test if it's already in the list (avoid duplicates)
197
+ if name == "re_engagement_test":
198
+ continue
199
+ custom_config = self.load_custom_config(brand, name)
200
+ if custom_config:
201
+ configs[name] = custom_config
202
+
203
+ return configs
204
+
205
+ def create_config_from_ui(
206
+ self,
207
+ brand: str,
208
+ campaign_name: str,
209
+ campaign_type: str,
210
+ num_stages: int,
211
+ campaign_instructions: Optional[str],
212
+ stages_config: Dict[int, Dict]
213
+ ) -> Dict:
214
+ """
215
+ Create a configuration dictionary from UI inputs.
216
+
217
+ Args:
218
+ brand: Brand name
219
+ campaign_name: Campaign name
220
+ campaign_type: Campaign type
221
+ num_stages: Number of stages
222
+ campaign_instructions: Campaign-wide instructions (optional)
223
+ stages_config: Dictionary mapping stage numbers to stage configurations
224
+
225
+ Returns:
226
+ dict: Complete configuration dictionary
227
+ """
228
+ config = {
229
+ "brand": brand,
230
+ "campaign_type": campaign_type,
231
+ "campaign_name": campaign_name,
232
+ "campaign_instructions": campaign_instructions,
233
+ }
234
+
235
+ # Add stage configurations
236
+ for stage_num in range(1, num_stages + 1):
237
+ if stage_num in stages_config:
238
+ config[str(stage_num)] = stages_config[stage_num]
239
+
240
+ return config
241
+
242
+ def extract_stage_config(self, config: Dict, stage: int) -> Dict:
243
+ """
244
+ Extract configuration for a specific stage.
245
+
246
+ Args:
247
+ config: Complete configuration dictionary
248
+ stage: Stage number
249
+
250
+ Returns:
251
+ dict: Stage configuration
252
+ """
253
+ return config.get(str(stage), {})
254
+
255
+ def validate_config(self, config: Dict) -> tuple[bool, str]:
256
+ """
257
+ Validate configuration structure and required fields.
258
+
259
+ Args:
260
+ config: Configuration dictionary
261
+
262
+ Returns:
263
+ tuple: (is_valid, error_message)
264
+ """
265
+ required_fields = ["brand", "campaign_name"]
266
+
267
+ # Check required top-level fields
268
+ for field in required_fields:
269
+ if field not in config:
270
+ return False, f"Missing required field: {field}"
271
+
272
+ # Check if at least stage 1 exists
273
+ if "1" not in config:
274
+ return False, "Configuration must have at least stage 1"
275
+
276
+ # Validate stage 1 config
277
+ stage1 = config["1"]
278
+ required_stage_fields = ["stage", "model"]
279
+
280
+ for field in required_stage_fields:
281
+ if field not in stage1:
282
+ return False, f"Stage 1 missing required field: {field}"
283
+
284
+ return True, ""
285
+
286
+ def duplicate_config(self, brand: str, source_name: str, new_name: str) -> bool:
287
+ """
288
+ Duplicate an existing configuration with a new name.
289
+
290
+ Args:
291
+ brand: Brand name
292
+ source_name: Source configuration name
293
+ new_name: New configuration name
294
+
295
+ Returns:
296
+ bool: True if duplicated successfully, False otherwise
297
+ """
298
+ try:
299
+ # Load source config
300
+ if source_name == "re_engagement_test":
301
+ source_config = self.load_default_config(brand)
302
+ else:
303
+ source_config = self.load_custom_config(brand, source_name)
304
+
305
+ if source_config is None:
306
+ return False
307
+
308
+ # Save as new config
309
+ return self.save_custom_config(brand, new_name, source_config)
310
+
311
+ except Exception as e:
312
+ st.error(f"Error duplicating config: {e}")
313
+ return False
utils/data_loader.py ADDED
@@ -0,0 +1,230 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Data loading module for the AI Messaging System Visualization Tool.
3
+
4
+ Handles data loading from local CSV files only.
5
+ """
6
+
7
+ import sys
8
+ import pandas as pd
9
+ from pathlib import Path
10
+ from typing import Optional
11
+ import streamlit as st
12
+
13
+ # Add parent directory to path to import from ai_messaging_system_v2
14
+ sys.path.insert(0, str(Path(__file__).parent.parent.parent))
15
+
16
+ try:
17
+ from ai_messaging_system_v2.Messaging_system.Permes import Permes
18
+ except ImportError as e:
19
+ st.error(f"Error importing AI Messaging System modules: {e}")
20
+
21
+
22
+ class DataLoader:
23
+ """
24
+ Handles all data loading operations for the visualization tool.
25
+ """
26
+
27
+ def __init__(self):
28
+ """Initialize DataLoader."""
29
+ self.base_path = Path(__file__).parent.parent
30
+ self.ui_users_path = self.base_path / "data" / "UI_users"
31
+ self.ui_output_path = Path(__file__).parent.parent.parent / "ai_messaging_system_v2" / "Data" / "ui_output"
32
+
33
+ # Create directories if they don't exist
34
+ self.ui_users_path.mkdir(parents=True, exist_ok=True)
35
+ self.ui_output_path.mkdir(parents=True, exist_ok=True)
36
+
37
+ def load_brand_users(self, brand: str) -> Optional[pd.DataFrame]:
38
+ """
39
+ Load all users for a brand from UI_users directory.
40
+
41
+ Args:
42
+ brand: Brand name
43
+
44
+ Returns:
45
+ pd.DataFrame or None: All users for the brand or None if file doesn't exist
46
+ """
47
+ file_path = self.ui_users_path / f"{brand}_users.csv"
48
+
49
+ if not file_path.exists():
50
+ st.error(f"User file not found: {brand}_users.csv")
51
+ return None
52
+
53
+ try:
54
+ users_df = pd.read_csv(file_path, encoding='utf-8-sig')
55
+ return users_df
56
+ except Exception as e:
57
+ st.error(f"Error loading users from {brand}_users.csv: {e}")
58
+ return None
59
+
60
+ def sample_users_randomly(self, brand: str, sample_size: int) -> Optional[pd.DataFrame]:
61
+ """
62
+ Randomly sample users from the brand's user file.
63
+
64
+ Args:
65
+ brand: Brand name
66
+ sample_size: Number of users to sample (1-25)
67
+
68
+ Returns:
69
+ pd.DataFrame or None: Sampled users or None if error
70
+ """
71
+ try:
72
+ # Load all users for the brand
73
+ all_users = self.load_brand_users(brand)
74
+
75
+ if all_users is None or len(all_users) == 0:
76
+ st.error(f"No users available for {brand}")
77
+ return None
78
+
79
+ # Validate sample size
80
+ if sample_size < 1:
81
+ sample_size = 1
82
+ if sample_size > min(25, len(all_users)):
83
+ sample_size = min(25, len(all_users))
84
+
85
+ # Randomly sample
86
+ sampled_users = all_users.sample(n=sample_size, random_state=None)
87
+
88
+ return sampled_users
89
+
90
+ except Exception as e:
91
+ st.error(f"Error sampling users: {e}")
92
+ return None
93
+
94
+ def has_brand_users(self, brand: str) -> bool:
95
+ """
96
+ Check if user file exists for a brand.
97
+
98
+ Args:
99
+ brand: Brand name
100
+
101
+ Returns:
102
+ bool: True if file exists, False otherwise
103
+ """
104
+ file_path = self.ui_users_path / f"{brand}_users.csv"
105
+ return file_path.exists()
106
+
107
+ def get_brand_user_count(self, brand: str) -> int:
108
+ """
109
+ Get the total number of users available for a brand.
110
+
111
+ Args:
112
+ brand: Brand name
113
+
114
+ Returns:
115
+ int: Number of users available
116
+ """
117
+ users_df = self.load_brand_users(brand)
118
+ if users_df is not None:
119
+ return len(users_df)
120
+ return 0
121
+
122
+ def load_generated_messages(self) -> Optional[pd.DataFrame]:
123
+ """
124
+ Load generated messages from UI output folder.
125
+
126
+ Returns:
127
+ pd.DataFrame or None: Generated messages or None if file doesn't exist
128
+ """
129
+ messages_file = self.ui_output_path / "messages.csv"
130
+
131
+ if messages_file.exists():
132
+ return pd.read_csv(messages_file, encoding='utf-8-sig')
133
+ return None
134
+
135
+ def clear_ui_output(self):
136
+ """
137
+ Clear UI output files (messages and costs).
138
+ """
139
+ try:
140
+ Permes.clear_ui_output()
141
+ st.success("UI output cleared successfully!")
142
+ except Exception as e:
143
+ st.error(f"Error clearing UI output: {e}")
144
+
145
+ def get_ui_output_path(self) -> Path:
146
+ """
147
+ Get path to UI output directory.
148
+
149
+ Returns:
150
+ Path: UI output directory path
151
+ """
152
+ return self.ui_output_path
153
+
154
+
155
+ def get_message_stats(self) -> dict:
156
+ """
157
+ Get statistics about generated messages.
158
+
159
+ Returns:
160
+ dict: Statistics including total messages, stages, users, etc.
161
+ """
162
+ messages_df = self.load_generated_messages()
163
+
164
+ if messages_df is None or len(messages_df) == 0:
165
+ return {
166
+ "total_messages": 0,
167
+ "total_users": 0,
168
+ "total_stages": 0,
169
+ "campaigns": []
170
+ }
171
+
172
+ stats = {
173
+ "total_messages": len(messages_df),
174
+ "total_users": messages_df['user_id'].nunique() if 'user_id' in messages_df.columns else 0,
175
+ "total_stages": messages_df['stage'].nunique() if 'stage' in messages_df.columns else 0,
176
+ "campaigns": messages_df['campaign_name'].unique().tolist() if 'campaign_name' in messages_df.columns else []
177
+ }
178
+
179
+ return stats
180
+
181
+ def filter_messages_by_stage(self, messages_df: pd.DataFrame, stage: int) -> pd.DataFrame:
182
+ """
183
+ Filter messages by stage number.
184
+
185
+ Args:
186
+ messages_df: Messages DataFrame
187
+ stage: Stage number
188
+
189
+ Returns:
190
+ pd.DataFrame: Filtered messages
191
+ """
192
+ if 'stage' in messages_df.columns:
193
+ return messages_df[messages_df['stage'] == stage]
194
+ return pd.DataFrame()
195
+
196
+ def filter_messages_by_user(self, messages_df: pd.DataFrame, user_id: int) -> pd.DataFrame:
197
+ """
198
+ Filter messages by user ID.
199
+
200
+ Args:
201
+ messages_df: Messages DataFrame
202
+ user_id: User ID
203
+
204
+ Returns:
205
+ pd.DataFrame: Filtered messages
206
+ """
207
+ if 'user_id' in messages_df.columns:
208
+ return messages_df[messages_df['user_id'] == user_id]
209
+ return pd.DataFrame()
210
+
211
+ def search_messages(self, messages_df: pd.DataFrame, search_term: str) -> pd.DataFrame:
212
+ """
213
+ Search messages by keyword in message content.
214
+
215
+ Args:
216
+ messages_df: Messages DataFrame
217
+ search_term: Search keyword
218
+
219
+ Returns:
220
+ pd.DataFrame: Filtered messages containing the search term
221
+ """
222
+ if len(search_term) == 0:
223
+ return messages_df
224
+
225
+ # Search in message column (which contains JSON)
226
+ if 'message' in messages_df.columns:
227
+ mask = messages_df['message'].astype(str).str.contains(search_term, case=False, na=False)
228
+ return messages_df[mask]
229
+
230
+ return pd.DataFrame()
utils/feedback_manager.py ADDED
@@ -0,0 +1,414 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Feedback management module for the AI Messaging System Visualization Tool.
3
+
4
+ Handles storage and retrieval of user feedback on generated messages.
5
+ """
6
+
7
+ import pandas as pd
8
+ from pathlib import Path
9
+ from datetime import datetime
10
+ from typing import Optional, List, Dict
11
+ import streamlit as st
12
+
13
+
14
+ # Rejection reason categories
15
+ REJECTION_REASONS = {
16
+ "poor_header": "Poor Header",
17
+ "poor_body": "Poor Body/Content",
18
+ "grammar_issues": "Grammar Issues",
19
+ "emoji_problems": "Emoji Problems",
20
+ "recommendation_issues": "Recommendation Issues",
21
+ "wrong_information": "Wrong/Inaccurate Information",
22
+ "tone_issues": "Tone Issues",
23
+ "similarity": "Similar To Previous Header/Messages",
24
+ "other": "Other"
25
+ }
26
+
27
+
28
+ class FeedbackManager:
29
+ """
30
+ Manages feedback storage and retrieval for generated messages.
31
+ """
32
+
33
+ def __init__(self):
34
+ """Initialize FeedbackManager."""
35
+ self.base_path = Path(__file__).parent.parent
36
+ self.feedback_path = self.base_path / "data" / "feedback"
37
+
38
+ # Create directory if it doesn't exist
39
+ self.feedback_path.mkdir(parents=True, exist_ok=True)
40
+
41
+ def get_feedback_file_path(self, experiment_id: str) -> Path:
42
+ """
43
+ Get path to feedback file for an experiment.
44
+
45
+ Args:
46
+ experiment_id: Experiment ID
47
+
48
+ Returns:
49
+ Path: Feedback file path
50
+ """
51
+ return self.feedback_path / f"{experiment_id}_feedback.csv"
52
+
53
+ def save_feedback(
54
+ self,
55
+ experiment_id: str,
56
+ user_id: int,
57
+ stage: int,
58
+ feedback_type: str,
59
+ rejection_reason: Optional[str] = None,
60
+ rejection_text: Optional[str] = None,
61
+ campaign_name: Optional[str] = None,
62
+ message_header: Optional[str] = None,
63
+ message_body: Optional[str] = None
64
+ ) -> bool:
65
+ """
66
+ Save feedback for a message.
67
+
68
+ Args:
69
+ experiment_id: Experiment ID
70
+ user_id: User ID
71
+ stage: Stage number
72
+ feedback_type: 'reject' (we focus on rejects)
73
+ rejection_reason: Category of rejection (optional)
74
+ rejection_text: Additional text for rejection (optional)
75
+ campaign_name: Campaign name (optional)
76
+ message_header: Message header text (optional)
77
+ message_body: Message body text (optional)
78
+
79
+ Returns:
80
+ bool: True if saved successfully, False otherwise
81
+ """
82
+ try:
83
+ feedback_file = self.get_feedback_file_path(experiment_id)
84
+
85
+ # Create feedback record
86
+ feedback_record = {
87
+ "experiment_id": experiment_id,
88
+ "user_id": user_id,
89
+ "stage": stage,
90
+ "feedback_type": feedback_type,
91
+ "rejection_reason": rejection_reason,
92
+ "rejection_text": rejection_text,
93
+ "campaign_name": campaign_name,
94
+ "message_header": message_header,
95
+ "message_body": message_body,
96
+ "timestamp": datetime.now().isoformat()
97
+ }
98
+
99
+ # Convert to DataFrame
100
+ new_feedback = pd.DataFrame([feedback_record])
101
+
102
+ # Append to existing file or create new
103
+ if feedback_file.exists():
104
+ existing_feedback = pd.read_csv(feedback_file, encoding='utf-8-sig')
105
+
106
+ # Check if this exact feedback already exists
107
+ duplicate = existing_feedback[
108
+ (existing_feedback['user_id'] == user_id) &
109
+ (existing_feedback['stage'] == stage) &
110
+ (existing_feedback['experiment_id'] == experiment_id)
111
+ ]
112
+
113
+ if len(duplicate) > 0:
114
+ # Update existing feedback
115
+ existing_feedback.loc[
116
+ (existing_feedback['user_id'] == user_id) &
117
+ (existing_feedback['stage'] == stage) &
118
+ (existing_feedback['experiment_id'] == experiment_id),
119
+ ['feedback_type', 'rejection_reason', 'rejection_text', 'message_header', 'message_body', 'timestamp']
120
+ ] = [feedback_type, rejection_reason, rejection_text, message_header, message_body, datetime.now().isoformat()]
121
+
122
+ updated_feedback = existing_feedback
123
+ else:
124
+ # Append new feedback
125
+ updated_feedback = pd.concat([existing_feedback, new_feedback], ignore_index=True)
126
+ else:
127
+ updated_feedback = new_feedback
128
+
129
+ # Save to CSV
130
+ updated_feedback.to_csv(feedback_file, index=False, encoding='utf-8-sig')
131
+ return True
132
+
133
+ except Exception as e:
134
+ st.error(f"Error saving feedback: {e}")
135
+ return False
136
+
137
+ def load_feedback(self, experiment_id: str) -> Optional[pd.DataFrame]:
138
+ """
139
+ Load feedback for an experiment.
140
+
141
+ Args:
142
+ experiment_id: Experiment ID
143
+
144
+ Returns:
145
+ pd.DataFrame or None: Feedback DataFrame or None if not found
146
+ """
147
+ try:
148
+ feedback_file = self.get_feedback_file_path(experiment_id)
149
+
150
+ if not feedback_file.exists():
151
+ return pd.DataFrame(columns=[
152
+ 'experiment_id', 'user_id', 'stage', 'feedback_type',
153
+ 'rejection_reason', 'rejection_text', 'campaign_name',
154
+ 'message_header', 'message_body', 'timestamp'
155
+ ])
156
+
157
+ return pd.read_csv(feedback_file, encoding='utf-8-sig')
158
+
159
+ except Exception as e:
160
+ st.error(f"Error loading feedback: {e}")
161
+ return None
162
+
163
+ def get_feedback_for_message(
164
+ self,
165
+ experiment_id: str,
166
+ user_id: int,
167
+ stage: int
168
+ ) -> Optional[Dict]:
169
+ """
170
+ Get feedback for a specific message.
171
+
172
+ Args:
173
+ experiment_id: Experiment ID
174
+ user_id: User ID
175
+ stage: Stage number
176
+
177
+ Returns:
178
+ dict or None: Feedback record or None if not found
179
+ """
180
+ feedback_df = self.load_feedback(experiment_id)
181
+
182
+ if feedback_df is None or len(feedback_df) == 0:
183
+ return None
184
+
185
+ # Filter for specific message
186
+ message_feedback = feedback_df[
187
+ (feedback_df['user_id'] == user_id) &
188
+ (feedback_df['stage'] == stage) &
189
+ (feedback_df['experiment_id'] == experiment_id)
190
+ ]
191
+
192
+ if len(message_feedback) > 0:
193
+ return message_feedback.iloc[0].to_dict()
194
+
195
+ return None
196
+
197
+ def delete_feedback(
198
+ self,
199
+ experiment_id: str,
200
+ user_id: int,
201
+ stage: int
202
+ ) -> bool:
203
+ """
204
+ Delete feedback for a specific message.
205
+
206
+ Args:
207
+ experiment_id: Experiment ID
208
+ user_id: User ID
209
+ stage: Stage number
210
+
211
+ Returns:
212
+ bool: True if deleted successfully, False otherwise
213
+ """
214
+ try:
215
+ feedback_df = self.load_feedback(experiment_id)
216
+
217
+ if feedback_df is None or len(feedback_df) == 0:
218
+ return False
219
+
220
+ # Remove the feedback
221
+ updated_feedback = feedback_df[
222
+ ~((feedback_df['user_id'] == user_id) &
223
+ (feedback_df['stage'] == stage) &
224
+ (feedback_df['experiment_id'] == experiment_id))
225
+ ]
226
+
227
+ # Save updated feedback
228
+ feedback_file = self.get_feedback_file_path(experiment_id)
229
+ updated_feedback.to_csv(feedback_file, index=False, encoding='utf-8-sig')
230
+
231
+ return True
232
+
233
+ except Exception as e:
234
+ st.error(f"Error deleting feedback: {e}")
235
+ return False
236
+
237
+ def get_feedback_stats(self, experiment_id: str, total_messages: int = None) -> Dict:
238
+ """
239
+ Get feedback statistics for an experiment.
240
+
241
+ Args:
242
+ experiment_id: Experiment ID
243
+ total_messages: Total number of messages generated (optional, for accurate rejection rate)
244
+
245
+ Returns:
246
+ dict: Feedback statistics
247
+ """
248
+ feedback_df = self.load_feedback(experiment_id)
249
+
250
+ if feedback_df is None or len(feedback_df) == 0:
251
+ return {
252
+ "total_feedback": 0,
253
+ "total_rejects": 0,
254
+ "reject_rate": 0.0,
255
+ "rejection_reasons": {}
256
+ }
257
+
258
+ total_feedback = len(feedback_df)
259
+ total_rejects = len(feedback_df[feedback_df['feedback_type'] == 'reject'])
260
+
261
+ # Count rejection reasons
262
+ rejection_reasons = {}
263
+ if total_rejects > 0:
264
+ reason_counts = feedback_df[
265
+ feedback_df['feedback_type'] == 'reject'
266
+ ]['rejection_reason'].value_counts().to_dict()
267
+
268
+ rejection_reasons = {
269
+ REJECTION_REASONS.get(k, k): v
270
+ for k, v in reason_counts.items()
271
+ if k is not None and pd.notna(k)
272
+ }
273
+
274
+ # Calculate rejection rate based on total messages (not total feedback)
275
+ # Since users only provide feedback for rejected messages
276
+ if total_messages is not None and total_messages > 0:
277
+ reject_rate = (total_rejects / total_messages * 100)
278
+ else:
279
+ # Fallback to old calculation if total_messages not provided
280
+ reject_rate = (total_rejects / total_feedback * 100) if total_feedback > 0 else 0.0
281
+
282
+ stats = {
283
+ "total_feedback": total_feedback,
284
+ "total_rejects": total_rejects,
285
+ "reject_rate": reject_rate,
286
+ "rejection_reasons": rejection_reasons
287
+ }
288
+
289
+ return stats
290
+
291
+ def get_stage_feedback_stats(self, experiment_id: str, messages_df: pd.DataFrame = None) -> pd.DataFrame:
292
+ """
293
+ Get feedback statistics by stage.
294
+
295
+ Args:
296
+ experiment_id: Experiment ID
297
+ messages_df: DataFrame of all messages (optional, for accurate rejection rate per stage)
298
+
299
+ Returns:
300
+ pd.DataFrame: Stage-wise feedback statistics
301
+ """
302
+ feedback_df = self.load_feedback(experiment_id)
303
+
304
+ if feedback_df is None or len(feedback_df) == 0:
305
+ return pd.DataFrame(columns=['stage', 'total_messages', 'total_feedback', 'rejects', 'reject_rate'])
306
+
307
+ # Get total messages per stage from messages_df if provided
308
+ stage_message_counts = {}
309
+ if messages_df is not None and 'stage' in messages_df.columns:
310
+ stage_message_counts = messages_df.groupby('stage').size().to_dict()
311
+
312
+ # Group by stage
313
+ stage_stats = []
314
+ for stage in sorted(feedback_df['stage'].unique()):
315
+ stage_feedback = feedback_df[feedback_df['stage'] == stage]
316
+ total_feedback = len(stage_feedback)
317
+ rejects = len(stage_feedback[stage_feedback['feedback_type'] == 'reject'])
318
+
319
+ # Calculate rejection rate based on total messages for this stage
320
+ total_messages_for_stage = stage_message_counts.get(stage, total_feedback)
321
+ reject_rate = (rejects / total_messages_for_stage * 100) if total_messages_for_stage > 0 else 0.0
322
+
323
+ stage_stats.append({
324
+ 'stage': stage,
325
+ 'total_messages': total_messages_for_stage,
326
+ 'total_feedback': total_feedback,
327
+ 'rejects': rejects,
328
+ 'reject_rate': reject_rate
329
+ })
330
+
331
+ return pd.DataFrame(stage_stats)
332
+
333
+ def compare_experiments(
334
+ self,
335
+ experiment_id_a: str,
336
+ experiment_id_b: str,
337
+ total_messages_a: int = None,
338
+ total_messages_b: int = None
339
+ ) -> Dict:
340
+ """
341
+ Compare feedback statistics between two experiments.
342
+
343
+ Args:
344
+ experiment_id_a: First experiment ID
345
+ experiment_id_b: Second experiment ID
346
+ total_messages_a: Total messages in experiment A (optional, for accurate rejection rate)
347
+ total_messages_b: Total messages in experiment B (optional, for accurate rejection rate)
348
+
349
+ Returns:
350
+ dict: Comparison statistics
351
+ """
352
+ stats_a = self.get_feedback_stats(experiment_id_a, total_messages=total_messages_a)
353
+ stats_b = self.get_feedback_stats(experiment_id_b, total_messages=total_messages_b)
354
+
355
+ comparison = {
356
+ "experiment_a": {
357
+ "id": experiment_id_a,
358
+ "stats": stats_a
359
+ },
360
+ "experiment_b": {
361
+ "id": experiment_id_b,
362
+ "stats": stats_b
363
+ },
364
+ "difference": {
365
+ "reject_rate": stats_a['reject_rate'] - stats_b['reject_rate']
366
+ }
367
+ }
368
+
369
+ return comparison
370
+
371
+ def export_feedback(self, experiment_id: str, export_path: Path) -> bool:
372
+ """
373
+ Export feedback to CSV file.
374
+
375
+ Args:
376
+ experiment_id: Experiment ID
377
+ export_path: Export file path
378
+
379
+ Returns:
380
+ bool: True if exported successfully, False otherwise
381
+ """
382
+ try:
383
+ feedback_df = self.load_feedback(experiment_id)
384
+
385
+ if feedback_df is None:
386
+ return False
387
+
388
+ feedback_df.to_csv(export_path, index=False, encoding='utf-8-sig')
389
+ return True
390
+
391
+ except Exception as e:
392
+ st.error(f"Error exporting feedback: {e}")
393
+ return False
394
+
395
+ def get_rejection_reason_label(self, reason_key: str) -> str:
396
+ """
397
+ Get human-readable label for rejection reason key.
398
+
399
+ Args:
400
+ reason_key: Rejection reason key
401
+
402
+ Returns:
403
+ str: Human-readable label
404
+ """
405
+ return REJECTION_REASONS.get(reason_key, reason_key)
406
+
407
+ def get_all_rejection_reasons(self) -> Dict[str, str]:
408
+ """
409
+ Get all available rejection reasons.
410
+
411
+ Returns:
412
+ dict: Dictionary mapping keys to labels
413
+ """
414
+ return REJECTION_REASONS.copy()
utils/theme.py ADDED
@@ -0,0 +1,335 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Brand-specific theming module for the AI Messaging System Visualization Tool.
3
+
4
+ Provides dynamic color schemes and styling based on selected brand.
5
+ """
6
+
7
+ import streamlit as st
8
+ from typing import Dict, Tuple
9
+
10
+ # Brand color definitions (primary, secondary, accent)
11
+ BRAND_COLORS = {
12
+ "base": {
13
+ "primary": "#FFD700", # Gold
14
+ "secondary": "#2C2C2C", # Dark gray
15
+ "accent": "#1A1A1A", # Black
16
+ "text": "#FFFFFF", # White
17
+ "background": "#0D0D0D", # Very dark
18
+ "sidebar_bg": "#8B7500" # Darker gold for sidebar
19
+ },
20
+ "drumeo": {
21
+ "primary": "#5DADE2", # Light blue
22
+ "secondary": "#FFD700", # Gold
23
+ "accent": "#2874A6", # Darker blue
24
+ "text": "#FFFFFF", # White
25
+ "background": "#1C2833", # Dark blue-gray
26
+ "sidebar_bg": "#1A4D6B" # Darker blue for sidebar
27
+ },
28
+ "pianote": {
29
+ "primary": "#EC7063", # Light red
30
+ "secondary": "#FFD700", # Gold
31
+ "accent": "#C0392B", # Darker red
32
+ "text": "#FFFFFF", # White
33
+ "background": "#1C1C1C", # Dark
34
+ "sidebar_bg": "#8B2500" # Darker red for sidebar
35
+ },
36
+ "guitareo": {
37
+ "primary": "#58D68D", # Light green
38
+ "secondary": "#FFD700", # Gold
39
+ "accent": "#229954", # Darker green
40
+ "text": "#FFFFFF", # White
41
+ "background": "#1C2E1F", # Dark green-gray
42
+ "sidebar_bg": "#1B5E20" # Darker green for sidebar
43
+ },
44
+ "singeo": {
45
+ "primary": "#BB8FCE", # Light purple
46
+ "secondary": "#FFD700", # Gold
47
+ "accent": "#7D3C98", # Darker purple
48
+ "text": "#FFFFFF", # White
49
+ "background": "#1F1926", # Dark purple-gray
50
+ "sidebar_bg": "#4A148C" # Darker purple for sidebar
51
+ }
52
+ }
53
+
54
+ # Brand emojis
55
+ BRAND_EMOJIS = {
56
+ "drumeo": "🥁",
57
+ "pianote": "🎹",
58
+ "guitareo": "🎸",
59
+ "singeo": "🎤"
60
+ }
61
+
62
+
63
+ def get_brand_theme(brand: str) -> Dict[str, str]:
64
+ """
65
+ Get color theme for specified brand.
66
+
67
+ Args:
68
+ brand: Brand name (drumeo, pianote, guitareo, singeo)
69
+
70
+ Returns:
71
+ dict: Color theme dictionary
72
+ """
73
+ brand_lower = brand.lower() if brand else "base"
74
+ return BRAND_COLORS.get(brand_lower, BRAND_COLORS["base"])
75
+
76
+
77
+ def get_brand_emoji(brand: str) -> str:
78
+ """
79
+ Get emoji for specified brand.
80
+
81
+ Args:
82
+ brand: Brand name
83
+
84
+ Returns:
85
+ str: Brand emoji
86
+ """
87
+ brand_lower = brand.lower() if brand else ""
88
+ return BRAND_EMOJIS.get(brand_lower, "🎵")
89
+
90
+
91
+ def apply_theme(brand: str = None):
92
+ """
93
+ Apply brand-specific theme to Streamlit app.
94
+
95
+ Args:
96
+ brand: Brand name (optional, uses session state if not provided)
97
+ """
98
+ if brand is None:
99
+ brand = st.session_state.get("selected_brand", "base")
100
+
101
+ theme = get_brand_theme(brand)
102
+
103
+ # Custom CSS for brand theming
104
+ css = f"""
105
+ <style>
106
+ /* Global styles */
107
+ .stApp {{
108
+ background-color: {theme['background']};
109
+ }}
110
+
111
+ /* Text colors */
112
+ h1, h2, h3, h4, h5, h6 {{
113
+ color: {theme['primary']} !important;
114
+ }}
115
+
116
+ p, span, div {{
117
+ color: {theme['text']} !important;
118
+ }}
119
+
120
+ /* Button styles */
121
+ .stButton > button {{
122
+ background-color: {theme['primary']};
123
+ color: {theme['background']};
124
+ border: none;
125
+ border-radius: 8px;
126
+ padding: 0.5rem 1rem;
127
+ font-weight: 600;
128
+ transition: all 0.3s ease;
129
+ }}
130
+
131
+ .stButton > button:hover {{
132
+ background-color: {theme['secondary']};
133
+ transform: translateY(-2px);
134
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
135
+ }}
136
+
137
+ /* Download button */
138
+ .stDownloadButton > button {{
139
+ background-color: {theme['accent']};
140
+ color: {theme['text']};
141
+ border-radius: 8px;
142
+ font-weight: 600;
143
+ }}
144
+
145
+ /* Input fields */
146
+ .stTextInput > div > div > input,
147
+ .stTextArea > div > div > textarea,
148
+ .stNumberInput > div > div > input {{
149
+ background-color: {theme['secondary']};
150
+ color: {theme['text']};
151
+ border: 1px solid {theme['primary']};
152
+ border-radius: 5px;
153
+ }}
154
+
155
+ /* Select boxes */
156
+ .stSelectbox > div > div {{
157
+ background-color: {theme['secondary']};
158
+ color: {theme['text']};
159
+ border-radius: 5px;
160
+ }}
161
+
162
+ /* Multiselect */
163
+ .stMultiSelect > div > div {{
164
+ background-color: {theme['secondary']};
165
+ border-radius: 5px;
166
+ }}
167
+
168
+ /* Tabs */
169
+ .stTabs [data-baseweb="tab-list"] {{
170
+ gap: 8px;
171
+ }}
172
+
173
+ .stTabs [data-baseweb="tab"] {{
174
+ background-color: {theme['secondary']};
175
+ color: {theme['text']};
176
+ border-radius: 5px 5px 0 0;
177
+ padding: 10px 20px;
178
+ font-weight: 600;
179
+ }}
180
+
181
+ .stTabs [aria-selected="true"] {{
182
+ background-color: {theme['primary']};
183
+ color: {theme['background']};
184
+ }}
185
+
186
+ /* Progress bar */
187
+ .stProgress > div > div > div {{
188
+ background-color: {theme['primary']};
189
+ }}
190
+
191
+ /* Expanders */
192
+ .streamlit-expanderHeader {{
193
+ background-color: {theme['secondary']};
194
+ color: {theme['primary']};
195
+ border-radius: 5px;
196
+ font-weight: 600;
197
+ }}
198
+
199
+ /* Cards/Containers */
200
+ .element-container {{
201
+ background-color: transparent;
202
+ }}
203
+
204
+ /* Sidebar */
205
+ [data-testid="stSidebar"] {{
206
+ background-color: {theme['sidebar_bg']};
207
+ }}
208
+
209
+ [data-testid="stSidebar"] h1,
210
+ [data-testid="stSidebar"] h2,
211
+ [data-testid="stSidebar"] h3 {{
212
+ color: {theme['text']} !important;
213
+ }}
214
+
215
+ [data-testid="stSidebar"] p,
216
+ [data-testid="stSidebar"] span,
217
+ [data-testid="stSidebar"] div,
218
+ [data-testid="stSidebar"] label {{
219
+ color: {theme['text']} !important;
220
+ }}
221
+
222
+ /* Metrics */
223
+ [data-testid="stMetricValue"] {{
224
+ color: {theme['primary']} !important;
225
+ font-size: 2rem;
226
+ font-weight: bold;
227
+ }}
228
+
229
+ /* Divider */
230
+ hr {{
231
+ border-color: {theme['primary']};
232
+ opacity: 0.3;
233
+ }}
234
+
235
+ /* Checkbox */
236
+ .stCheckbox {{
237
+ color: {theme['text']};
238
+ }}
239
+
240
+ /* Radio */
241
+ .stRadio > div {{
242
+ color: {theme['text']};
243
+ }}
244
+
245
+ /* Success/Info/Warning/Error boxes */
246
+ .stSuccess {{
247
+ background-color: rgba(88, 214, 141, 0.1);
248
+ color: {theme['text']};
249
+ }}
250
+
251
+ .stInfo {{
252
+ background-color: rgba(93, 173, 226, 0.1);
253
+ color: {theme['text']};
254
+ }}
255
+
256
+ .stWarning {{
257
+ background-color: rgba(255, 215, 0, 0.1);
258
+ color: {theme['text']};
259
+ }}
260
+
261
+ .stError {{
262
+ background-color: rgba(236, 112, 99, 0.1);
263
+ color: {theme['text']};
264
+ }}
265
+
266
+ /* DataFrames */
267
+ .dataframe {{
268
+ border: 1px solid {theme['primary']};
269
+ }}
270
+
271
+ /* Custom card styling */
272
+ .message-card {{
273
+ background-color: {theme['secondary']};
274
+ border-left: 4px solid {theme['primary']};
275
+ border-radius: 8px;
276
+ padding: 1rem;
277
+ margin: 0.5rem 0;
278
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
279
+ }}
280
+
281
+ .message-card:hover {{
282
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
283
+ transform: translateX(4px);
284
+ transition: all 0.3s ease;
285
+ }}
286
+
287
+ /* Small text */
288
+ .small {{
289
+ font-size: 0.85rem;
290
+ opacity: 0.7;
291
+ }}
292
+
293
+ /* Badge styles */
294
+ .badge {{
295
+ display: inline-block;
296
+ padding: 0.25rem 0.5rem;
297
+ border-radius: 12px;
298
+ font-size: 0.75rem;
299
+ font-weight: 600;
300
+ background-color: {theme['accent']};
301
+ color: {theme['text']};
302
+ }}
303
+ </style>
304
+ """
305
+
306
+ st.markdown(css, unsafe_allow_html=True)
307
+
308
+
309
+ def create_card(content: str, key: str = None) -> None:
310
+ """
311
+ Create a styled card container.
312
+
313
+ Args:
314
+ content: HTML content to display in card
315
+ key: Optional unique key for the card
316
+ """
317
+ st.markdown(
318
+ f'<div class="message-card">{content}</div>',
319
+ unsafe_allow_html=True
320
+ )
321
+
322
+
323
+ def create_badge(text: str, color: str = None) -> str:
324
+ """
325
+ Create a styled badge element.
326
+
327
+ Args:
328
+ text: Badge text
329
+ color: Optional custom color
330
+
331
+ Returns:
332
+ str: HTML for badge
333
+ """
334
+ style = f'background-color: {color};' if color else ''
335
+ return f'<span class="badge" style="{style}">{text}</span>'
visualization/check_env.py DELETED
@@ -1,85 +0,0 @@
1
- """
2
- Environment Variable Check Script
3
- Verifies that all required environment variables are set correctly.
4
- """
5
-
6
- import os
7
- from pathlib import Path
8
- from dotenv import load_dotenv
9
-
10
- # Load environment variables
11
- env_path = Path(__file__).parent / '.env'
12
- if env_path.exists():
13
- load_dotenv(env_path)
14
- print(f"✅ Found .env file at: {env_path}")
15
- else:
16
- # Try parent directory
17
- parent_env_path = Path(__file__).parent.parent / '.env'
18
- if parent_env_path.exists():
19
- load_dotenv(parent_env_path)
20
- print(f"✅ Found .env file at: {parent_env_path}")
21
- else:
22
- print("❌ No .env file found!")
23
- print(f" Expected locations:")
24
- print(f" - {env_path}")
25
- print(f" - {parent_env_path}")
26
- print("\n💡 Create a .env file using .env.example as a template:")
27
- print(" cp .env.example .env")
28
- exit(1)
29
-
30
- print("\n" + "="*60)
31
- print("Environment Variables Check")
32
- print("="*60 + "\n")
33
-
34
- # Required variables
35
- required_vars = {
36
- "Snowflake": [
37
- "SNOWFLAKE_USER",
38
- "SNOWFLAKE_PASSWORD",
39
- "SNOWFLAKE_ACCOUNT",
40
- "SNOWFLAKE_ROLE",
41
- "SNOWFLAKE_DATABASE",
42
- "SNOWFLAKE_WAREHOUSE",
43
- "SNOWFLAKE_SCHEMA"
44
- ],
45
- "API Keys": [
46
- "OPENAI_API_KEY",
47
- "GOOGLE_API_KEY"
48
- ],
49
- "Application": [
50
- "APP_TOKEN"
51
- ]
52
- }
53
-
54
- all_good = True
55
-
56
- for category, vars_list in required_vars.items():
57
- print(f"\n{category}:")
58
- print("-" * 40)
59
-
60
- for var in vars_list:
61
- value = os.getenv(var)
62
- if value:
63
- # Mask sensitive values
64
- if len(value) > 10:
65
- masked = value[:4] + "..." + value[-4:]
66
- else:
67
- masked = "***"
68
- print(f" ✅ {var}: {masked}")
69
- else:
70
- print(f" ❌ {var}: NOT SET")
71
- all_good = False
72
-
73
- print("\n" + "="*60)
74
-
75
- if all_good:
76
- print("✅ All required environment variables are set!")
77
- print("\n🚀 You're ready to run the application:")
78
- print(" streamlit run app.py")
79
- else:
80
- print("❌ Some environment variables are missing!")
81
- print("\n💡 Please edit your .env file and set all required variables.")
82
- print(" Use .env.example as a reference.")
83
- exit(1)
84
-
85
- print("="*60 + "\n")