eloukas commited on
Commit
f4fc491
·
verified ·
1 Parent(s): 2747e41

Update dashboard to include dialog viewer, root causes, keywords (#1)

Browse files

- Update dashboard to include dialog viewer, root causes, keywords (28d707bd073364022256f121f96981b75bf57964)

Files changed (1) hide show
  1. app.py +1280 -34
app.py CHANGED
@@ -1,5 +1,6 @@
1
  import base64
2
  import io
 
3
  import random
4
 
5
  import dash
@@ -11,8 +12,6 @@ from dash import Input, Output, State, callback, dcc, html
11
 
12
  # Initialize the Dash app
13
  app = dash.Dash(__name__, suppress_callback_exceptions=True)
14
- server = app.server
15
-
16
 
17
  # Define app layout
18
  app.layout = html.Div(
@@ -180,6 +179,37 @@ app.layout = html.Div(
180
  ],
181
  className="metrics-section",
182
  ),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
183
  # Added Tags section
184
  html.Div(
185
  [
@@ -191,16 +221,40 @@ app.layout = html.Div(
191
  id="important-tags",
192
  className="tags-container",
193
  ),
194
- ]
 
 
 
 
195
  ),
196
  ],
197
  className="details-section",
198
  ),
199
  html.Div(
200
  [
201
- html.H4(
202
- "Sample Dialogs (Summary)",
203
- className="subsection-header",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
204
  ),
205
  html.Div(
206
  id="sample-dialogs",
@@ -240,8 +294,111 @@ app.layout = html.Div(
240
  id="main-content",
241
  style={"display": "none"},
242
  ),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
243
  # Store the processed data
244
  dcc.Store(id="stored-data"),
 
 
 
 
245
  ],
246
  className="app-container",
247
  )
@@ -640,6 +797,139 @@ app.index_string = """
640
  font-weight: 500;
641
  }
642
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
643
  .no-selection-container {
644
  position: absolute;
645
  top: 0;
@@ -678,6 +968,18 @@ app.index_string = """
678
  background-color: #f8f9fa;
679
  }
680
 
 
 
 
 
 
 
 
 
 
 
 
 
681
 
682
  .topic-tag {
683
  padding: 0.375rem 0.75rem;
@@ -723,6 +1025,47 @@ app.index_string = """
723
  color: rgba(255, 255, 255, 0.9);
724
  }
725
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
726
  .no-tags-message {
727
  color: var(--muted-foreground);
728
  font-style: italic;
@@ -731,6 +1074,127 @@ app.index_string = """
731
  width: 100%;
732
  }
733
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
734
  /* Responsive adjustments */
735
  @media (max-width: 768px) {
736
  .dashboard-container {
@@ -794,6 +1258,36 @@ def process_upload(contents, filename):
794
  df = pd.read_csv(io.StringIO(decoded.decode("utf-8")))
795
  elif "xls" in filename.lower():
796
  df = pd.read_excel(io.BytesIO(decoded))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
797
  else:
798
  return (
799
  None,
@@ -858,6 +1352,9 @@ def process_upload(contents, filename):
858
  def analyze_topics(df):
859
  # Group by topic name and calculate metrics
860
  topic_stats = (
 
 
 
861
  df.groupby("deduplicated_topic_name")
862
  .agg(
863
  count=("id", "count"),
@@ -1105,22 +1602,24 @@ def update_bubble_chart(data, color_metric):
1105
  # DEBUG: Print sizes of bubbles in the first and second bins
1106
  bins = sorted(df["bin"].unique())
1107
  if len(bins) >= 1:
1108
- first_bin = bins[0]
1109
- print(f"DEBUG - First bin '{first_bin}' bubble sizes:")
1110
- first_bin_df = df[df["bin"] == first_bin]
1111
- for idx, row in first_bin_df.iterrows():
1112
- print(
1113
- f" Topic: {row['deduplicated_topic_name']}, Raw size: {row['count']}, Displayed size: {size_values[idx]}"
1114
- )
 
1115
 
1116
  if len(bins) >= 2:
1117
- second_bin = bins[1]
1118
- print(f"DEBUG - Second bin '{second_bin}' bubble sizes:")
1119
- second_bin_df = df[df["bin"] == second_bin]
1120
- for idx, row in second_bin_df.iterrows():
1121
- print(
1122
- f" Topic: {row['deduplicated_topic_name']}, Raw size: {row['count']}, Displayed size: {size_values[idx]}"
1123
- )
 
1124
 
1125
  # Determine color based on selected metric
1126
  if color_metric == "negative_rate":
@@ -1149,9 +1648,6 @@ def update_bubble_chart(data, color_metric):
1149
  # color_scale = "Portland"
1150
  color_scale = "Teal"
1151
 
1152
- # Set all text positions to bottom for consistent layout
1153
- text_positions = ["bottom center"] * len(df)
1154
-
1155
  # Create enhanced hover text that includes bin information
1156
  hover_text = [
1157
  f"Topic: {topic}<br>{size_title}: {raw:.1f}<br>{color_title}: {color:.1f}<br>Group: {bin_desc}"
@@ -1222,8 +1718,9 @@ def update_bubble_chart(data, color_metric):
1222
  showarrow=False,
1223
  textangle=0,
1224
  font=dict(
1225
- size=10,
1226
- # size=8,
 
1227
  color="var(--foreground)",
1228
  family="Arial, sans-serif",
1229
  weight="bold",
@@ -1335,19 +1832,40 @@ def update_bubble_chart(data, color_metric):
1335
  Output("topic-title", "children"),
1336
  Output("topic-metadata", "children"),
1337
  Output("topic-metrics", "children"),
 
 
1338
  Output("important-tags", "children"),
 
1339
  Output("sample-dialogs", "children"),
1340
  Output("no-topic-selected", "style"),
 
 
 
 
 
 
1341
  ],
1342
- [Input("bubble-chart", "hoverData"), Input("bubble-chart", "clickData")],
1343
  [State("stored-data", "data"), State("upload-data", "contents")],
1344
  )
1345
- def update_topic_details(hover_data, click_data, stored_data, file_contents):
 
 
1346
  # Determine which data to use (prioritize click over hover)
1347
  hover_info = hover_data or click_data
1348
 
1349
  if not hover_info or not stored_data or not file_contents:
1350
- return "", [], [], "", [], {"display": "flex"}
 
 
 
 
 
 
 
 
 
 
 
1351
 
1352
  # Extract topic name from the hover data
1353
  topic_name = hover_info["points"][0]["customdata"][0]
@@ -1364,9 +1882,11 @@ def update_topic_details(hover_data, click_data, stored_data, file_contents):
1364
  content_type
1365
  == "data:application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;base64"
1366
  ):
1367
- df_full = pd.read_excel(io.BytesIO(decoded))
1368
  else: # Assume CSV
1369
- df_full = pd.read_csv(io.StringIO(decoded.decode("utf-8")))
 
 
1370
 
1371
  # Filter to this topic
1372
  topic_conversations = df_full[df_full["deduplicated_topic_name"] == topic_name]
@@ -1380,8 +1900,20 @@ def update_topic_details(hover_data, click_data, stored_data, file_contents):
1380
  [
1381
  html.I(className="fas fa-comments metadata-icon"),
1382
  html.Span(f"{int(topic_data['count'])} dialogs"),
 
 
 
 
 
 
 
 
 
 
 
1383
  ],
1384
  className="metadata-item",
 
1385
  ),
1386
  ]
1387
 
@@ -1410,7 +1942,66 @@ def update_topic_details(hover_data, click_data, stored_data, file_contents):
1410
  ),
1411
  ]
1412
 
1413
- # New: Extract and process consolidated_tags with improved styling
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1414
  tags_list = []
1415
  for _, row in topic_conversations.iterrows():
1416
  tags_str = row.get("consolidated_tags", "")
@@ -1430,6 +2021,8 @@ def update_topic_details(hover_data, click_data, stored_data, file_contents):
1430
  TOP_K = 15
1431
  sorted_tags = sorted_tags[:TOP_K]
1432
 
 
 
1433
  if sorted_tags:
1434
  # Create beautifully styled tags with count indicators and consistent color
1435
  tags_output = html.Div(
@@ -1445,6 +2038,7 @@ def update_topic_details(hover_data, click_data, stored_data, file_contents):
1445
  ],
1446
  className="tags-container",
1447
  )
 
1448
  else:
1449
  tags_output = html.Div(
1450
  [
@@ -1475,13 +2069,37 @@ def update_topic_details(hover_data, click_data, stored_data, file_contents):
1475
  chat_id_tag = None
1476
  if "id" in row:
1477
  chat_id_tag = html.Span(
1478
- f"Chat ID: {row['id']}", className="dialog-tag tag-chat-id"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1479
  )
1480
 
1481
- # Compile all tags, including the new Chat ID tag if available
1482
  tags = [sentiment_tag, resolution_tag, urgency_tag]
1483
  if chat_id_tag:
1484
  tags.append(chat_id_tag)
 
 
1485
 
1486
  dialog_items.append(
1487
  html.Div(
@@ -1509,11 +2127,639 @@ def update_topic_details(hover_data, click_data, stored_data, file_contents):
1509
  title,
1510
  metadata_items,
1511
  metrics_boxes,
 
 
1512
  tags_output,
 
1513
  sample_dialogs,
1514
  {"display": "none"},
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1515
  )
1516
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1517
 
1518
  if __name__ == "__main__":
1519
- app.run_server(debug=False)
 
1
  import base64
2
  import io
3
+ import json
4
  import random
5
 
6
  import dash
 
12
 
13
  # Initialize the Dash app
14
  app = dash.Dash(__name__, suppress_callback_exceptions=True)
 
 
15
 
16
  # Define app layout
17
  app.layout = html.Div(
 
179
  ],
180
  className="metrics-section",
181
  ),
182
+ # Added Root Causes section
183
+ html.Div(
184
+ [
185
+ html.H4(
186
+ [
187
+ "Root Causes",
188
+ html.I(
189
+ className="fas fa-info-circle",
190
+ title="Root cause detection is experimental and may require manual review since it is generated by AI models. Root causes are only shown in clusters with identifiable root causes.",
191
+ # Added title for info icon
192
+ style={
193
+ "marginLeft": "0.2rem",
194
+ "color": "#6c757d", # General gray
195
+ "fontSize": "0.9rem",
196
+ "cursor": "pointer",
197
+ "verticalAlign": "middle",
198
+ },
199
+ ),
200
+ ],
201
+ className="subsection-header",
202
+ ),
203
+ html.Div(
204
+ id="root-causes",
205
+ className="root-causes-container",
206
+ ),
207
+ ],
208
+ id="root-causes-section",
209
+ style={
210
+ "display": "none"
211
+ }, # Initially hidden
212
+ ),
213
  # Added Tags section
214
  html.Div(
215
  [
 
221
  id="important-tags",
222
  className="tags-container",
223
  ),
224
+ ],
225
+ id="tags-section",
226
+ style={
227
+ "display": "none"
228
+ }, # Initially hidden
229
  ),
230
  ],
231
  className="details-section",
232
  ),
233
  html.Div(
234
  [
235
+ html.Div(
236
+ [
237
+ html.H4(
238
+ [
239
+ "Sample Dialogs (Summary)",
240
+ html.Button(
241
+ html.I(
242
+ className="fas fa-sync-alt"
243
+ ),
244
+ id="refresh-dialogs-btn",
245
+ className="refresh-button",
246
+ title="Refresh dialogs",
247
+ n_clicks=0,
248
+ ),
249
+ ],
250
+ className="subsection-header",
251
+ style={
252
+ "margin": "0",
253
+ "display": "flex",
254
+ "alignItems": "center",
255
+ },
256
+ ),
257
+ ],
258
  ),
259
  html.Div(
260
  id="sample-dialogs",
 
294
  id="main-content",
295
  style={"display": "none"},
296
  ),
297
+ # Conversation Modal
298
+ html.Div(
299
+ id="conversation-modal",
300
+ children=[
301
+ html.Div(
302
+ children=[
303
+ html.Div(
304
+ [
305
+ html.H3(
306
+ "Full Conversation",
307
+ style={"margin": "0", "flex": "1"},
308
+ ),
309
+ html.Button(
310
+ html.I(className="fas fa-times"),
311
+ id="close-modal-btn",
312
+ className="close-modal-btn",
313
+ title="Close",
314
+ ),
315
+ ],
316
+ className="modal-header",
317
+ ),
318
+ html.Div(
319
+ id="conversation-subheader",
320
+ className="conversation-subheader",
321
+ ),
322
+ html.Div(
323
+ id="conversation-content", className="conversation-content"
324
+ ),
325
+ ],
326
+ className="modal-content",
327
+ ),
328
+ ],
329
+ className="modal-overlay-conversation",
330
+ style={"display": "none"},
331
+ ),
332
+ # Dialogs Table Modal
333
+ html.Div(
334
+ id="dialogs-table-modal",
335
+ children=[
336
+ html.Div(
337
+ children=[
338
+ html.Div(
339
+ [
340
+ html.H3(
341
+ id="dialogs-modal-title",
342
+ style={"margin": "0", "flex": "1"},
343
+ ),
344
+ html.Button(
345
+ html.I(className="fas fa-times"),
346
+ id="close-dialogs-modal-btn",
347
+ className="close-modal-btn",
348
+ title="Close",
349
+ ),
350
+ ],
351
+ className="modal-header",
352
+ ),
353
+ html.Div(
354
+ id="dialogs-table-content",
355
+ className="dialogs-table-content",
356
+ ),
357
+ ],
358
+ className="modal-content-large",
359
+ ),
360
+ ],
361
+ className="modal-overlay",
362
+ style={"display": "none"},
363
+ ),
364
+ # Root Cause Dialogs Modal
365
+ html.Div(
366
+ id="root-cause-modal",
367
+ children=[
368
+ html.Div(
369
+ children=[
370
+ html.Div(
371
+ [
372
+ html.H3(
373
+ id="root-cause-modal-title",
374
+ style={"margin": "0", "flex": "1"},
375
+ ),
376
+ html.Button(
377
+ html.I(className="fas fa-times"),
378
+ id="close-root-cause-modal-btn",
379
+ className="close-modal-btn",
380
+ title="Close",
381
+ ),
382
+ ],
383
+ className="modal-header",
384
+ ),
385
+ html.Div(
386
+ id="root-cause-table-content",
387
+ className="dialogs-table-content",
388
+ ),
389
+ ],
390
+ className="modal-content-large",
391
+ ),
392
+ ],
393
+ className="modal-overlay",
394
+ style={"display": "none"},
395
+ ),
396
  # Store the processed data
397
  dcc.Store(id="stored-data"),
398
+ # Store the current selected topic for dialogs modal
399
+ dcc.Store(id="selected-topic-store"),
400
+ # Store the current selected root cause for root cause modal
401
+ dcc.Store(id="selected-root-cause-store"),
402
  ],
403
  className="app-container",
404
  )
 
797
  font-weight: 500;
798
  }
799
 
800
+ .tag-root-cause {
801
+ background-color: #8B4513;
802
+ color: hsl(0, 0%, 98%);
803
+ font-weight: 500;
804
+ }
805
+
806
+ .refresh-button {
807
+ background-color: hsl(210, 40%, 98%);
808
+ border: 1px solid hsl(214.3, 31.8%, 91.4%);
809
+ border-radius: 0.25rem;
810
+ padding: 0.25rem;
811
+ cursor: pointer;
812
+ color: hsl(222.2, 84%, 4.9%);
813
+ font-size: 0.75rem;
814
+ transition: all 0.15s ease-in-out;
815
+ display: flex;
816
+ align-items: center;
817
+ justify-content: center;
818
+ min-width: 1.5rem;
819
+ height: 1.5rem;
820
+ margin-left: 0.5rem;
821
+ }
822
+
823
+ .refresh-button:hover {
824
+ background-color: hsl(210, 40%, 96%);
825
+ border-color: hsl(214.3, 31.8%, 81.4%);
826
+ }
827
+
828
+ .refresh-button:active {
829
+ background-color: hsl(210, 40%, 94%);
830
+ transform: scale(0.98);
831
+ }
832
+
833
+ .modal-overlay {
834
+ position: fixed;
835
+ top: 0;
836
+ left: 0;
837
+ width: 100%;
838
+ height: 100%;
839
+ background-color: rgba(0, 0, 0, 0.5);
840
+ z-index: 1000;
841
+ display: flex;
842
+ align-items: center;
843
+ justify-content: center;
844
+ }
845
+
846
+ .modal-overlay-conversation {
847
+ position: fixed;
848
+ top: 0;
849
+ left: 0;
850
+ width: 100%;
851
+ height: 100%;
852
+ background-color: rgba(0, 0, 0, 0.7);
853
+ z-index: 1100;
854
+ display: flex;
855
+ align-items: center;
856
+ justify-content: center;
857
+ }
858
+
859
+ .modal-content {
860
+ background-color: white;
861
+ border-radius: 0.5rem;
862
+ box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
863
+ max-width: 80%;
864
+ max-height: 80%;
865
+ width: 600px;
866
+ display: flex;
867
+ flex-direction: column;
868
+ }
869
+
870
+ .modal-header {
871
+ display: flex;
872
+ align-items: center;
873
+ justify-content: space-between;
874
+ padding: 1rem;
875
+ border-bottom: 1px solid hsl(214.3, 31.8%, 91.4%);
876
+ }
877
+
878
+ .close-modal-btn {
879
+ background: none;
880
+ border: none;
881
+ cursor: pointer;
882
+ color: hsl(215.4, 16.3%, 46.9%);
883
+ font-size: 1.2rem;
884
+ padding: 0.5rem;
885
+ border-radius: 0.25rem;
886
+ transition: all 0.15s ease-in-out;
887
+ }
888
+
889
+ .close-modal-btn:hover {
890
+ background-color: hsl(210, 40%, 96%);
891
+ color: hsl(222.2, 84%, 4.9%);
892
+ }
893
+
894
+ .conversation-subheader {
895
+ padding: 0.75rem 1rem;
896
+ border-bottom: 1px solid hsl(214.3, 31.8%, 91.4%);
897
+ background-color: hsl(210, 40%, 98%);
898
+ font-size: 0.875rem;
899
+ color: hsl(215.4, 16.3%, 46.9%);
900
+ margin: 0 1rem;
901
+ border-radius: 0.25rem 0.25rem 0 0;
902
+ }
903
+
904
+ .conversation-content {
905
+ padding: 1rem;
906
+ overflow-y: auto;
907
+ max-height: 60vh;
908
+ white-space: pre-wrap;
909
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
910
+ font-size: 0.875rem;
911
+ line-height: 1.5;
912
+ color: hsl(222.2, 84%, 4.9%);
913
+ background-color: hsl(210, 40%, 98%);
914
+ border-radius: 0.25rem;
915
+ margin: 0 1rem 1rem 1rem;
916
+ }
917
+
918
+ .conversation-icon {
919
+ margin-left: 0.5rem;
920
+ cursor: pointer;
921
+ color: hsl(210, 40%, 98%);
922
+ font-size: 0.875rem;
923
+ padding: 0.25rem;
924
+ border-radius: 0.25rem;
925
+ transition: all 0.15s ease-in-out;
926
+ }
927
+
928
+ .conversation-icon:hover {
929
+ background-color: rgba(255, 255, 255, 0.2);
930
+ color: hsl(210, 40%, 98%);
931
+ }
932
+
933
  .no-selection-container {
934
  position: absolute;
935
  top: 0;
 
968
  background-color: #f8f9fa;
969
  }
970
 
971
+ /* Root Causes container */
972
+ .root-causes-container {
973
+ display: flex;
974
+ flex-wrap: wrap;
975
+ gap: 3px;
976
+ margin-top: 3px;
977
+ margin-bottom: 10px;
978
+ padding: 4px;
979
+ border-radius: 6px;
980
+ background-color: #f8f9fa;
981
+ }
982
+
983
 
984
  .topic-tag {
985
  padding: 0.375rem 0.75rem;
 
1025
  color: rgba(255, 255, 255, 0.9);
1026
  }
1027
 
1028
+ .root-cause-tag {
1029
+ padding: 3px 8px;
1030
+ border-radius: 12px;
1031
+ font-size: 0.7rem;
1032
+ display: inline-flex;
1033
+ align-items: center;
1034
+ box-shadow: 0 1px 2px rgba(0,0,0,0.08);
1035
+ transition: all 0.2s ease;
1036
+ font-weight: 500;
1037
+ margin: 2px 3px 2px 0;
1038
+ cursor: default;
1039
+ border: 1px solid rgba(0,0,0,0.06);
1040
+ background-color: #8b6f47; /* Muted brown/amber color for root causes */
1041
+ color: white;
1042
+ line-height: 1.2;
1043
+ }
1044
+
1045
+ .root-cause-tag:hover {
1046
+ transform: translateY(-1px);
1047
+ box-shadow: 0 3px 5px rgba(0,0,0,0.15);
1048
+ background-color: #7a5f3d; /* Slightly darker on hover */
1049
+ }
1050
+
1051
+ .root-cause-tag-icon {
1052
+ margin-right: 3px;
1053
+ font-size: 0.6rem;
1054
+ opacity: 0.8;
1055
+ color: rgba(255, 255, 255, 0.9);
1056
+ }
1057
+
1058
+ .root-cause-click-icon {
1059
+ transition: all 0.2s ease;
1060
+ color: rgba(255, 255, 255, 0.8);
1061
+ }
1062
+
1063
+ .root-cause-click-icon:hover {
1064
+ opacity: 1 !important;
1065
+ transform: scale(1.1);
1066
+ color: rgba(255, 255, 255, 1);
1067
+ }
1068
+
1069
  .no-tags-message {
1070
  color: var(--muted-foreground);
1071
  font-style: italic;
 
1074
  width: 100%;
1075
  }
1076
 
1077
+ .no-root-causes-message {
1078
+ color: var(--muted-foreground);
1079
+ font-style: italic;
1080
+ padding: 0.75rem;
1081
+ text-align: center;
1082
+ width: 100%;
1083
+ }
1084
+
1085
+ /* Show All Dialogs Button */
1086
+ .show-dialogs-btn {
1087
+ background-color: var(--primary);
1088
+ color: var(--primary-foreground);
1089
+ border: none;
1090
+ padding: 0.5rem 0.75rem;
1091
+ border-radius: var(--radius);
1092
+ font-size: 0.75rem;
1093
+ cursor: pointer;
1094
+ transition: all 0.2s ease;
1095
+ font-weight: 500;
1096
+ margin-left: 0.5rem;
1097
+ display: inline-flex;
1098
+ align-items: center;
1099
+ gap: 0.25rem;
1100
+ }
1101
+
1102
+ .show-dialogs-btn:hover {
1103
+ background-color: var(--primary);
1104
+ opacity: 0.9;
1105
+ transform: translateY(-1px);
1106
+ }
1107
+
1108
+ /* Dialogs Table Modal */
1109
+ .modal-content-large {
1110
+ background-color: white;
1111
+ border-radius: 0.5rem;
1112
+ box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
1113
+ max-width: 90%;
1114
+ max-height: 90%;
1115
+ width: 1200px;
1116
+ display: flex;
1117
+ flex-direction: column;
1118
+ }
1119
+
1120
+ .dialogs-table-content {
1121
+ padding: 1rem;
1122
+ overflow-y: auto;
1123
+ max-height: 70vh;
1124
+ background-color: hsl(210, 40%, 98%);
1125
+ border-radius: 0.25rem;
1126
+ margin: 0 1rem 1rem 1rem;
1127
+ }
1128
+
1129
+ .dialogs-table {
1130
+ width: 100%;
1131
+ border-collapse: collapse;
1132
+ background-color: white;
1133
+ border-radius: 0.5rem;
1134
+ overflow: hidden;
1135
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
1136
+ }
1137
+
1138
+ .dialogs-table th {
1139
+ background-color: var(--secondary);
1140
+ color: var(--secondary-foreground);
1141
+ padding: 0.75rem;
1142
+ text-align: left;
1143
+ font-weight: 600;
1144
+ font-size: 0.875rem;
1145
+ border-bottom: 1px solid var(--border);
1146
+ }
1147
+
1148
+ .dialogs-table td {
1149
+ padding: 0.75rem;
1150
+ border-bottom: 1px solid var(--border);
1151
+ font-size: 0.875rem;
1152
+ vertical-align: top;
1153
+ }
1154
+
1155
+ .dialogs-table tr:hover {
1156
+ background-color: var(--secondary);
1157
+ }
1158
+
1159
+ .dialog-summary-cell {
1160
+ max-width: 23.5rem;
1161
+ word-wrap: break-word;
1162
+ line-height: 1.4;
1163
+ }
1164
+
1165
+ .dialog-tags-cell {
1166
+ max-width: 200px;
1167
+ }
1168
+
1169
+ .dialog-tag-small {
1170
+ display: inline-block;
1171
+ padding: 0.125rem 0.375rem;
1172
+ margin: 0.125rem;
1173
+ border-radius: 0.25rem;
1174
+ font-size: 0.625rem;
1175
+ font-weight: 500;
1176
+ }
1177
+
1178
+ .open-chat-btn {
1179
+ background-color: var(--primary);
1180
+ color: var(--primary-foreground);
1181
+ border: none;
1182
+ padding: 0.375rem 0.5rem;
1183
+ border-radius: var(--radius);
1184
+ font-size: 0.75rem;
1185
+ cursor: pointer;
1186
+ transition: all 0.2s ease;
1187
+ font-weight: 500;
1188
+ display: inline-flex;
1189
+ align-items: center;
1190
+ gap: 0.25rem;
1191
+ }
1192
+
1193
+ .open-chat-btn:hover {
1194
+ opacity: 0.9;
1195
+ transform: translateY(-1px);
1196
+ }
1197
+
1198
  /* Responsive adjustments */
1199
  @media (max-width: 768px) {
1200
  .dashboard-container {
 
1258
  df = pd.read_csv(io.StringIO(decoded.decode("utf-8")))
1259
  elif "xls" in filename.lower():
1260
  df = pd.read_excel(io.BytesIO(decoded))
1261
+
1262
+ # DEBUG
1263
+ # --- Print unique root_cause_subcluster values for each deduplicated_topic_name ---
1264
+ if (
1265
+ "deduplicated_topic_name" in df.columns
1266
+ and "root_cause_subcluster" in df.columns
1267
+ ):
1268
+ print(
1269
+ "\n[INFO] Unique root_cause_subcluster values for each deduplicated_topic_name:"
1270
+ )
1271
+ for topic in df["deduplicated_topic_name"].unique():
1272
+ subclusters = (
1273
+ df[df["deduplicated_topic_name"] == topic]["root_cause_subcluster"]
1274
+ .dropna()
1275
+ .unique()
1276
+ )
1277
+ print(f"- {topic}:")
1278
+ for sub in subclusters:
1279
+ print(f" - {sub}")
1280
+ print()
1281
+ # --- End of DEBUG ---
1282
+
1283
+ # Hardcoded flag to exclude 'Unclustered' topics
1284
+ EXCLUDE_UNCLUSTERED = True
1285
+ if EXCLUDE_UNCLUSTERED and "deduplicated_topic_name" in df.columns:
1286
+ df = df[df["deduplicated_topic_name"] != "Unclustered"].copy()
1287
+ # If we strip leading and trailing `"` or `'` from the topic name here, then
1288
+ # we will have a problem with the deduplicated names, as they will not match the
1289
+ # original topic names in the dataset.
1290
+ # Better do it in the first script.
1291
  else:
1292
  return (
1293
  None,
 
1352
  def analyze_topics(df):
1353
  # Group by topic name and calculate metrics
1354
  topic_stats = (
1355
+ # IMPORTANT!
1356
+ # As deduplicated_topic_name, we have either the deduplicated names (if enabled by the process),
1357
+ # either the kmeans_reclustered name (where available) and the ClusterNames.
1358
  df.groupby("deduplicated_topic_name")
1359
  .agg(
1360
  count=("id", "count"),
 
1602
  # DEBUG: Print sizes of bubbles in the first and second bins
1603
  bins = sorted(df["bin"].unique())
1604
  if len(bins) >= 1:
1605
+ # first_bin = bins[0]
1606
+ # print(f"DEBUG - First bin '{first_bin}' bubble sizes:")
1607
+ # first_bin_df = df[df["bin"] == first_bin]
1608
+ # for idx, row in first_bin_df.iterrows():
1609
+ # print(
1610
+ # f" Topic: {row['deduplicated_topic_name']}, Raw size: {row['count']}, Displayed size: {size_values[idx]}"
1611
+ # )
1612
+ pass
1613
 
1614
  if len(bins) >= 2:
1615
+ # second_bin = bins[1]
1616
+ # print(f"DEBUG - Second bin '{second_bin}' bubble sizes:")
1617
+ # second_bin_df = df[df["bin"] == second_bin]
1618
+ # for idx, row in second_bin_df.iterrows():
1619
+ # print(
1620
+ # f" Topic: {row['deduplicated_topic_name']}, Raw size: {row['count']}, Displayed size: {size_values[idx]}"
1621
+ # )
1622
+ pass
1623
 
1624
  # Determine color based on selected metric
1625
  if color_metric == "negative_rate":
 
1648
  # color_scale = "Portland"
1649
  color_scale = "Teal"
1650
 
 
 
 
1651
  # Create enhanced hover text that includes bin information
1652
  hover_text = [
1653
  f"Topic: {topic}<br>{size_title}: {raw:.1f}<br>{color_title}: {color:.1f}<br>Group: {bin_desc}"
 
1718
  showarrow=False,
1719
  textangle=0,
1720
  font=dict(
1721
+ # size=10,
1722
+ # size=15,
1723
+ size=9,
1724
  color="var(--foreground)",
1725
  family="Arial, sans-serif",
1726
  weight="bold",
 
1832
  Output("topic-title", "children"),
1833
  Output("topic-metadata", "children"),
1834
  Output("topic-metrics", "children"),
1835
+ Output("root-causes", "children"),
1836
+ Output("root-causes-section", "style"),
1837
  Output("important-tags", "children"),
1838
+ Output("tags-section", "style"),
1839
  Output("sample-dialogs", "children"),
1840
  Output("no-topic-selected", "style"),
1841
+ Output("selected-topic-store", "data"),
1842
+ ],
1843
+ [
1844
+ Input("bubble-chart", "hoverData"),
1845
+ Input("bubble-chart", "clickData"),
1846
+ Input("refresh-dialogs-btn", "n_clicks"),
1847
  ],
 
1848
  [State("stored-data", "data"), State("upload-data", "contents")],
1849
  )
1850
+ def update_topic_details(
1851
+ hover_data, click_data, refresh_clicks, stored_data, file_contents
1852
+ ):
1853
  # Determine which data to use (prioritize click over hover)
1854
  hover_info = hover_data or click_data
1855
 
1856
  if not hover_info or not stored_data or not file_contents:
1857
+ return (
1858
+ "",
1859
+ [],
1860
+ [],
1861
+ "",
1862
+ {"display": "none"},
1863
+ "",
1864
+ {"display": "none"},
1865
+ [],
1866
+ {"display": "flex"},
1867
+ None,
1868
+ )
1869
 
1870
  # Extract topic name from the hover data
1871
  topic_name = hover_info["points"][0]["customdata"][0]
 
1882
  content_type
1883
  == "data:application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;base64"
1884
  ):
1885
+ df_full = pd.read_excel(io.BytesIO(decoded), dtype={"Root_Cause": str})
1886
  else: # Assume CSV
1887
+ df_full = pd.read_csv(
1888
+ io.StringIO(decoded.decode("utf-8")), dtype={"Root_Cause": str}
1889
+ )
1890
 
1891
  # Filter to this topic
1892
  topic_conversations = df_full[df_full["deduplicated_topic_name"] == topic_name]
 
1900
  [
1901
  html.I(className="fas fa-comments metadata-icon"),
1902
  html.Span(f"{int(topic_data['count'])} dialogs"),
1903
+ html.Button(
1904
+ [
1905
+ html.I(
1906
+ className="fas fa-table", style={"marginRight": "0.25rem"}
1907
+ ),
1908
+ "Show all dialogs inside",
1909
+ ],
1910
+ id="show-all-dialogs-btn",
1911
+ className="show-dialogs-btn",
1912
+ n_clicks=0,
1913
+ ),
1914
  ],
1915
  className="metadata-item",
1916
+ style={"display": "flex", "alignItems": "center", "width": "100%"},
1917
  ),
1918
  ]
1919
 
 
1942
  ),
1943
  ]
1944
 
1945
+ # Extract and process root causes
1946
+ root_causes_output = ""
1947
+ root_causes_section_style = {"display": "none"}
1948
+
1949
+ # Check if root_cause_subcluster column exists in the data
1950
+ if "root_cause_subcluster" in topic_conversations.columns:
1951
+ # Get unique root causes for this specific cluster
1952
+ root_causes = topic_conversations["root_cause_subcluster"].dropna().unique()
1953
+
1954
+ # Filter out common non-informative values including "Unclustered"
1955
+ filtered_root_causes = [
1956
+ rc
1957
+ for rc in root_causes
1958
+ if rc
1959
+ not in [
1960
+ "Sub-clustering disabled",
1961
+ "Not eligible for sub-clustering",
1962
+ "No valid root causes",
1963
+ "No Subcluster",
1964
+ "Unclustered",
1965
+ "",
1966
+ ]
1967
+ ]
1968
+
1969
+ # Debug: Print the unique root causes for this cluster
1970
+ print(f"\n[DEBUG] Root causes for cluster '{topic_name}':")
1971
+ print(f" All root causes: {list(root_causes)}")
1972
+ print(f" Filtered root causes: {filtered_root_causes}")
1973
+
1974
+ if filtered_root_causes:
1975
+ # Create beautifully styled root cause tags with clickable icons
1976
+ root_causes_output = html.Div(
1977
+ [
1978
+ html.Div(
1979
+ [
1980
+ html.I(
1981
+ className="fas fa-exclamation-triangle root-cause-tag-icon"
1982
+ ),
1983
+ html.Span(root_cause, style={"marginRight": "6px"}),
1984
+ html.I(
1985
+ className="fas fa-external-link-alt root-cause-click-icon",
1986
+ id={"type": "root-cause-icon", "index": root_cause},
1987
+ title="Click to see specific chats assigned with this root cause.",
1988
+ style={
1989
+ "cursor": "pointer",
1990
+ "fontSize": "0.55rem",
1991
+ "opacity": "0.8",
1992
+ },
1993
+ ),
1994
+ ],
1995
+ className="root-cause-tag",
1996
+ style={"display": "inline-flex", "alignItems": "center"},
1997
+ )
1998
+ for root_cause in filtered_root_causes
1999
+ ],
2000
+ className="root-causes-container",
2001
+ )
2002
+ root_causes_section_style = {"display": "block"}
2003
+
2004
+ # Extract and process consolidated_tags with improved styling
2005
  tags_list = []
2006
  for _, row in topic_conversations.iterrows():
2007
  tags_str = row.get("consolidated_tags", "")
 
2021
  TOP_K = 15
2022
  sorted_tags = sorted_tags[:TOP_K]
2023
 
2024
+ # Set tags section visibility and output
2025
+ tags_section_style = {"display": "none"}
2026
  if sorted_tags:
2027
  # Create beautifully styled tags with count indicators and consistent color
2028
  tags_output = html.Div(
 
2038
  ],
2039
  className="tags-container",
2040
  )
2041
+ tags_section_style = {"display": "block"}
2042
  else:
2043
  tags_output = html.Div(
2044
  [
 
2069
  chat_id_tag = None
2070
  if "id" in row:
2071
  chat_id_tag = html.Span(
2072
+ [
2073
+ f"Chat ID: {row['id']} ",
2074
+ html.I(
2075
+ className="fas fa-arrow-up-right-from-square conversation-icon",
2076
+ id={"type": "conversation-icon", "index": row["id"]},
2077
+ title="View full conversation",
2078
+ style={"marginLeft": "0.25rem"},
2079
+ ),
2080
+ ],
2081
+ className="dialog-tag tag-chat-id",
2082
+ style={"display": "inline-flex", "alignItems": "center"},
2083
+ )
2084
+
2085
+ # Add Root Cause tag if 'Root Cause' column exists
2086
+ root_cause_tag = None
2087
+ if (
2088
+ "Root_Cause" in row
2089
+ and pd.notna(row["Root_Cause"])
2090
+ and row["Root_Cause"] != "na"
2091
+ ):
2092
+ root_cause_tag = html.Span(
2093
+ f"Root Cause: {row['Root_Cause']}",
2094
+ className="dialog-tag tag-root-cause",
2095
  )
2096
 
2097
+ # Compile all tags, including the new Chat ID and Root Cause tags if available
2098
  tags = [sentiment_tag, resolution_tag, urgency_tag]
2099
  if chat_id_tag:
2100
  tags.append(chat_id_tag)
2101
+ if root_cause_tag:
2102
+ tags.append(root_cause_tag)
2103
 
2104
  dialog_items.append(
2105
  html.Div(
 
2127
  title,
2128
  metadata_items,
2129
  metrics_boxes,
2130
+ root_causes_output,
2131
+ root_causes_section_style,
2132
  tags_output,
2133
+ tags_section_style,
2134
  sample_dialogs,
2135
  {"display": "none"},
2136
+ {"topic_name": topic_name, "file_contents": file_contents},
2137
+ )
2138
+
2139
+
2140
+ # Callback to open modal when conversation icon is clicked
2141
+ @callback(
2142
+ [
2143
+ Output("conversation-modal", "style"),
2144
+ Output("conversation-content", "children"),
2145
+ Output("conversation-subheader", "children"),
2146
+ ],
2147
+ [Input({"type": "conversation-icon", "index": dash.dependencies.ALL}, "n_clicks")],
2148
+ [State("upload-data", "contents")],
2149
+ prevent_initial_call=True,
2150
+ )
2151
+ def open_conversation_modal(n_clicks_list, file_contents):
2152
+ # Check if any icon was clicked
2153
+ if not any(n_clicks_list) or not file_contents:
2154
+ return {"display": "none"}, "", ""
2155
+
2156
+ # Get which icon was clicked
2157
+ ctx = dash.callback_context
2158
+ if not ctx.triggered:
2159
+ return (
2160
+ {"display": "none"},
2161
+ "",
2162
+ "",
2163
+ ) # Extract the chat ID from the triggered input
2164
+ triggered_id = ctx.triggered[0]["prop_id"]
2165
+ chat_id = json.loads(triggered_id.split(".")[0])["index"]
2166
+
2167
+ # Get the full conversation from the uploaded file
2168
+ content_type, content_string = file_contents.split(",")
2169
+ decoded = base64.b64decode(content_string)
2170
+
2171
+ if (
2172
+ content_type
2173
+ == "data:application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;base64"
2174
+ ):
2175
+ df_full = pd.read_excel(io.BytesIO(decoded), dtype={"Root_Cause": str})
2176
+ else: # Assume CSV
2177
+ df_full = pd.read_csv(
2178
+ io.StringIO(decoded.decode("utf-8")), dtype={"Root_Cause": str}
2179
+ )
2180
+
2181
+ # Find the conversation with this chat ID
2182
+ conversation_row = df_full[df_full["id"] == chat_id]
2183
+ if len(conversation_row) == 0:
2184
+ conversation_text = "Conversation not found."
2185
+ subheader_content = f"Chat ID: {chat_id}"
2186
+ else:
2187
+ row = conversation_row.iloc[0]
2188
+ conversation_text = row.get("conversation", "No conversation data available.")
2189
+
2190
+ # Get cluster name if available
2191
+ cluster_name = row.get("deduplicated_topic_name", "Unknown cluster")
2192
+
2193
+ # Create subheader with both Chat ID and cluster name
2194
+ subheader_content = html.Div(
2195
+ [
2196
+ html.Span(
2197
+ f"Chat ID: {chat_id}",
2198
+ style={"fontWeight": "600", "marginRight": "1rem"},
2199
+ ),
2200
+ html.Span(
2201
+ f"Cluster: {cluster_name}",
2202
+ style={"color": "hsl(215.4, 16.3%, 46.9%)"},
2203
+ ),
2204
+ ]
2205
+ )
2206
+
2207
+ return {"display": "flex"}, conversation_text, subheader_content
2208
+
2209
+
2210
+ # Callback to close modal
2211
+ @callback(
2212
+ Output("conversation-modal", "style", allow_duplicate=True),
2213
+ [Input("close-modal-btn", "n_clicks")],
2214
+ prevent_initial_call=True,
2215
+ )
2216
+ def close_conversation_modal(n_clicks):
2217
+ if n_clicks:
2218
+ return {"display": "none"}
2219
+ return {"display": "none"}
2220
+
2221
+
2222
+ # Callback to open dialogs table modal when "Show all dialogs inside" button is clicked
2223
+ @callback(
2224
+ [
2225
+ Output("dialogs-table-modal", "style"),
2226
+ Output("dialogs-modal-title", "children"),
2227
+ Output("dialogs-table-content", "children"),
2228
+ ],
2229
+ [Input("show-all-dialogs-btn", "n_clicks")],
2230
+ [State("selected-topic-store", "data")],
2231
+ prevent_initial_call=True,
2232
+ )
2233
+ def open_dialogs_table_modal(n_clicks, selected_topic_data):
2234
+ if not n_clicks or not selected_topic_data:
2235
+ return {"display": "none"}, "", ""
2236
+
2237
+ topic_name = selected_topic_data["topic_name"]
2238
+ file_contents = selected_topic_data["file_contents"]
2239
+
2240
+ # Get the full data
2241
+ content_type, content_string = file_contents.split(",")
2242
+ decoded = base64.b64decode(content_string)
2243
+
2244
+ if (
2245
+ content_type
2246
+ == "data:application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;base64"
2247
+ ):
2248
+ df_full = pd.read_excel(io.BytesIO(decoded), dtype={"Root_Cause": str})
2249
+ else: # Assume CSV
2250
+ df_full = pd.read_csv(
2251
+ io.StringIO(decoded.decode("utf-8")), dtype={"Root_Cause": str}
2252
+ )
2253
+
2254
+ # Filter to this topic
2255
+ topic_conversations = df_full[df_full["deduplicated_topic_name"] == topic_name]
2256
+
2257
+ # Create the table
2258
+ table_rows = []
2259
+
2260
+ # Header row
2261
+ table_rows.append(
2262
+ html.Tr(
2263
+ [
2264
+ html.Th("Chat ID"),
2265
+ html.Th("Summary"),
2266
+ html.Th("Root Cause"),
2267
+ html.Th("Sentiment"),
2268
+ html.Th("Resolution"),
2269
+ html.Th("Urgency"),
2270
+ html.Th("Tags"),
2271
+ html.Th("Action"),
2272
+ ]
2273
+ )
2274
+ )
2275
+
2276
+ # Data rows
2277
+ for _, row in topic_conversations.iterrows():
2278
+ # Process tags
2279
+ tags_str = row.get("consolidated_tags", "")
2280
+ if pd.notna(tags_str):
2281
+ tags = [tag.strip() for tag in tags_str.split(",") if tag.strip()]
2282
+ tags_display = html.Div(
2283
+ [
2284
+ html.Span(
2285
+ tag,
2286
+ className="dialog-tag-small",
2287
+ style={"backgroundColor": "#6c757d", "color": "white"},
2288
+ )
2289
+ for tag in tags[:3] # Show only first 3 tags
2290
+ ]
2291
+ + (
2292
+ [
2293
+ html.Span(
2294
+ f"+{len(tags) - 3}",
2295
+ className="dialog-tag-small",
2296
+ style={"backgroundColor": "#6c757d", "color": "white"},
2297
+ )
2298
+ ]
2299
+ if len(tags) > 3
2300
+ else []
2301
+ ),
2302
+ className="dialog-tags-cell",
2303
+ )
2304
+ else:
2305
+ tags_display = html.Span(
2306
+ "No tags",
2307
+ style={"color": "var(--muted-foreground)", "fontStyle": "italic"},
2308
+ )
2309
+
2310
+ table_rows.append(
2311
+ html.Tr(
2312
+ [
2313
+ html.Td(
2314
+ row["id"],
2315
+ style={"fontFamily": "monospace", "fontSize": "0.8rem"},
2316
+ ),
2317
+ html.Td(
2318
+ row.get("Summary", "No summary"),
2319
+ className="dialog-summary-cell",
2320
+ ),
2321
+ html.Td(
2322
+ html.Span(
2323
+ str(row.get("Root_Cause", "Unknown")).capitalize()
2324
+ if not pd.isna(row.get("Root_Cause"))
2325
+ else "Unknown",
2326
+ className="dialog-tag-small",
2327
+ style={
2328
+ "backgroundColor": "#8B4513", # Brown color for root cause
2329
+ "color": "white",
2330
+ },
2331
+ )
2332
+ ),
2333
+ html.Td(
2334
+ html.Span( # if sentiment is negative, color it red, otherwise grey
2335
+ row.get("Sentiment", "Unknown").capitalize(),
2336
+ className="dialog-tag-small",
2337
+ style={
2338
+ "backgroundColor": "#dc3545"
2339
+ if row.get("Sentiment") == "negative"
2340
+ else "#6c757d",
2341
+ "color": "white",
2342
+ },
2343
+ )
2344
+ ),
2345
+ html.Td(
2346
+ html.Span( # if resolution is unresolved, color it red, otherwise grey
2347
+ row.get("Resolution", "Unknown").capitalize(),
2348
+ className="dialog-tag-small",
2349
+ style={
2350
+ "backgroundColor": "#dc3545"
2351
+ if row.get("Resolution") == "unresolved"
2352
+ else "#6c757d",
2353
+ "color": "white",
2354
+ },
2355
+ )
2356
+ ),
2357
+ html.Td(
2358
+ html.Span( # if urgency is urgent, color it red, otherwise grey
2359
+ row.get("Urgency", "Unknown").capitalize(),
2360
+ className="dialog-tag-small",
2361
+ style={
2362
+ "backgroundColor": "#dc3545"
2363
+ if row.get("Urgency") == "urgent"
2364
+ else "#6c757d",
2365
+ "color": "white",
2366
+ },
2367
+ )
2368
+ ),
2369
+ html.Td(tags_display),
2370
+ html.Td(
2371
+ html.Button(
2372
+ [
2373
+ html.I(
2374
+ className="fas fa-eye",
2375
+ style={"marginRight": "0.25rem"},
2376
+ ),
2377
+ "View chat session",
2378
+ ],
2379
+ id={"type": "open-chat-btn", "index": row["id"]},
2380
+ className="open-chat-btn",
2381
+ n_clicks=0,
2382
+ )
2383
+ ),
2384
+ ]
2385
+ )
2386
+ )
2387
+
2388
+ table = html.Table(table_rows, className="dialogs-table")
2389
+
2390
+ modal_title = (
2391
+ f"All dialogs in Topic: {topic_name} ({len(topic_conversations)} dialogs)"
2392
  )
2393
 
2394
+ return {"display": "flex"}, modal_title, table
2395
+
2396
+
2397
+ # Callback to close dialogs table modal
2398
+ @callback(
2399
+ Output("dialogs-table-modal", "style", allow_duplicate=True),
2400
+ [Input("close-dialogs-modal-btn", "n_clicks")],
2401
+ prevent_initial_call=True,
2402
+ )
2403
+ def close_dialogs_table_modal(n_clicks):
2404
+ if n_clicks:
2405
+ return {"display": "none"}
2406
+ return {"display": "none"}
2407
+
2408
+
2409
+ # Callback to open conversation modal from dialogs table
2410
+ @callback(
2411
+ [
2412
+ Output("conversation-modal", "style", allow_duplicate=True),
2413
+ Output("conversation-content", "children", allow_duplicate=True),
2414
+ Output("conversation-subheader", "children", allow_duplicate=True),
2415
+ ],
2416
+ [Input({"type": "open-chat-btn", "index": dash.dependencies.ALL}, "n_clicks")],
2417
+ [State("upload-data", "contents")],
2418
+ prevent_initial_call=True,
2419
+ )
2420
+ def open_conversation_from_table(n_clicks_list, file_contents):
2421
+ # Check if any button was clicked
2422
+ if not any(n_clicks_list) or not file_contents:
2423
+ return {"display": "none"}, "", ""
2424
+
2425
+ # Get which button was clicked
2426
+ ctx = dash.callback_context
2427
+ if not ctx.triggered:
2428
+ return {"display": "none"}, "", ""
2429
+
2430
+ # Extract the chat ID from the triggered input
2431
+ triggered_id = ctx.triggered[0]["prop_id"]
2432
+ chat_id = json.loads(triggered_id.split(".")[0])["index"]
2433
+
2434
+ # Debug: print the chat_id to understand its type and value
2435
+ print(f"DEBUG: Looking for chat_id: {chat_id} (type: {type(chat_id)})")
2436
+
2437
+ # Get the full conversation from the uploaded file
2438
+ content_type, content_string = file_contents.split(",")
2439
+ decoded = base64.b64decode(content_string)
2440
+
2441
+ if (
2442
+ content_type
2443
+ == "data:application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;base64"
2444
+ ):
2445
+ df_full = pd.read_excel(io.BytesIO(decoded), dtype={"Root_Cause": str})
2446
+ else: # Assume CSV
2447
+ df_full = pd.read_csv(
2448
+ io.StringIO(decoded.decode("utf-8")), dtype={"Root_Cause": str}
2449
+ )
2450
+
2451
+ # Debug: print some info about the dataframe
2452
+ print(f"DEBUG: DataFrame shape: {df_full.shape}")
2453
+ print(f"DEBUG: Available chat IDs (first 5): {df_full['id'].head().tolist()}")
2454
+ print(f"DEBUG: Chat ID types in df: {df_full['id'].dtype}")
2455
+
2456
+ # Try to match with different data type conversions
2457
+ conversation_row = df_full[df_full["id"] == chat_id]
2458
+
2459
+ # If not found, try converting types
2460
+ if len(conversation_row) == 0:
2461
+ # Try converting chat_id to string
2462
+ conversation_row = df_full[df_full["id"].astype(str) == str(chat_id)]
2463
+
2464
+ # If still not found, try converting df id to int
2465
+ if len(conversation_row) == 0:
2466
+ try:
2467
+ conversation_row = df_full[df_full["id"] == int(chat_id)]
2468
+ except (ValueError, TypeError):
2469
+ pass
2470
+
2471
+ if len(conversation_row) == 0:
2472
+ conversation_text = f"Conversation not found for Chat ID: {chat_id}. Available IDs: {df_full['id'].head(10).tolist()}"
2473
+ subheader_content = f"Chat ID: {chat_id} (Not Found)"
2474
+ else:
2475
+ conversation_row = conversation_row.iloc[0]
2476
+ conversation_text = conversation_row.get(
2477
+ "conversation",
2478
+ "No conversation available, oopsie.", # fix here the conversation status
2479
+ )
2480
+
2481
+ # Create subheader with metadata
2482
+ subheader_content = f"Chat ID: {chat_id} | Topic: {conversation_row.get('deduplicated_topic_name', 'Unknown')} | Sentiment: {conversation_row.get('Sentiment', 'Unknown')} | Resolution: {conversation_row.get('Resolution', 'Unknown')}"
2483
+
2484
+ return {"display": "flex"}, conversation_text, subheader_content
2485
+
2486
+
2487
+ # Callback to open root cause modal when root cause icon is clicked
2488
+ @callback(
2489
+ [
2490
+ Output("root-cause-modal", "style"),
2491
+ Output("root-cause-modal-title", "children"),
2492
+ Output("root-cause-table-content", "children"),
2493
+ ],
2494
+ [Input({"type": "root-cause-icon", "index": dash.dependencies.ALL}, "n_clicks")],
2495
+ [State("selected-topic-store", "data")],
2496
+ prevent_initial_call=True,
2497
+ )
2498
+ def open_root_cause_modal(n_clicks_list, selected_topic_data):
2499
+ # Check if any icon was clicked
2500
+ if not any(n_clicks_list) or not selected_topic_data:
2501
+ return {"display": "none"}, "", ""
2502
+
2503
+ # Get which icon was clicked
2504
+ ctx = dash.callback_context
2505
+ if not ctx.triggered:
2506
+ return {"display": "none"}, "", ""
2507
+
2508
+ triggered_id = ctx.triggered[0]["prop_id"]
2509
+ root_cause = json.loads(triggered_id.split(".")[0])["index"]
2510
+
2511
+ topic_name = selected_topic_data["topic_name"]
2512
+ file_contents = selected_topic_data["file_contents"]
2513
+
2514
+ # Get the full data
2515
+ content_type, content_string = file_contents.split(",")
2516
+ decoded = base64.b64decode(content_string)
2517
+
2518
+ if (
2519
+ content_type
2520
+ == "data:application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;base64"
2521
+ ):
2522
+ df_full = pd.read_excel(io.BytesIO(decoded), dtype={"Root_Cause": str})
2523
+ else: # Assume CSV
2524
+ df_full = pd.read_csv(
2525
+ io.StringIO(decoded.decode("utf-8")), dtype={"Root_Cause": str}
2526
+ )
2527
+
2528
+ # Filter to this topic and root cause
2529
+ filtered_conversations = df_full[
2530
+ (df_full["deduplicated_topic_name"] == topic_name)
2531
+ & (df_full["root_cause_subcluster"] == root_cause)
2532
+ ]
2533
+
2534
+ # Create the table
2535
+ table_rows = []
2536
+
2537
+ # Header row
2538
+ table_rows.append(
2539
+ html.Tr(
2540
+ [
2541
+ html.Th("Chat ID"),
2542
+ html.Th("Summary"),
2543
+ html.Th("Sentiment"),
2544
+ html.Th("Resolution"),
2545
+ html.Th("Urgency"),
2546
+ html.Th("Tags"),
2547
+ html.Th("Action"),
2548
+ ]
2549
+ )
2550
+ )
2551
+
2552
+ # Data rows
2553
+ for _, row in filtered_conversations.iterrows():
2554
+ # Process tags
2555
+ tags_str = row.get("consolidated_tags", "")
2556
+ if pd.notna(tags_str):
2557
+ tags = [tag.strip() for tag in tags_str.split(",") if tag.strip()]
2558
+ tags_display = html.Div(
2559
+ [
2560
+ html.Span(
2561
+ tag,
2562
+ className="dialog-tag-small",
2563
+ style={"backgroundColor": "#6c757d", "color": "white"},
2564
+ )
2565
+ for tag in tags[:3] # Show only first 3 tags
2566
+ ]
2567
+ + (
2568
+ [
2569
+ html.Span(
2570
+ f"+{len(tags) - 3}",
2571
+ className="dialog-tag-small",
2572
+ style={"backgroundColor": "#6c757d", "color": "white"},
2573
+ )
2574
+ ]
2575
+ if len(tags) > 3
2576
+ else []
2577
+ ),
2578
+ className="dialog-tags-cell",
2579
+ )
2580
+ else:
2581
+ tags_display = html.Span(
2582
+ "No tags",
2583
+ style={"color": "var(--muted-foreground)", "fontStyle": "italic"},
2584
+ )
2585
+
2586
+ table_rows.append(
2587
+ html.Tr(
2588
+ [
2589
+ html.Td(
2590
+ row["id"],
2591
+ style={"fontFamily": "monospace", "fontSize": "0.8rem"},
2592
+ ),
2593
+ html.Td(
2594
+ row.get("Summary", "No summary"),
2595
+ className="dialog-summary-cell",
2596
+ ),
2597
+ html.Td(
2598
+ html.Span(
2599
+ row.get("Sentiment", "Unknown").capitalize(),
2600
+ className="dialog-tag-small",
2601
+ style={
2602
+ "backgroundColor": "#dc3545"
2603
+ if row.get("Sentiment") == "negative"
2604
+ else "#6c757d",
2605
+ "color": "white",
2606
+ },
2607
+ )
2608
+ ),
2609
+ html.Td(
2610
+ html.Span(
2611
+ row.get("Resolution", "Unknown").capitalize(),
2612
+ className="dialog-tag-small",
2613
+ style={
2614
+ "backgroundColor": "#dc3545"
2615
+ if row.get("Resolution") == "unresolved"
2616
+ else "#6c757d",
2617
+ "color": "white",
2618
+ },
2619
+ )
2620
+ ),
2621
+ html.Td(
2622
+ html.Span(
2623
+ row.get("Urgency", "Unknown").capitalize(),
2624
+ className="dialog-tag-small",
2625
+ style={
2626
+ "backgroundColor": "#dc3545"
2627
+ if row.get("Urgency") == "urgent"
2628
+ else "#6c757d",
2629
+ "color": "white",
2630
+ },
2631
+ )
2632
+ ),
2633
+ html.Td(tags_display),
2634
+ html.Td(
2635
+ html.Button(
2636
+ [
2637
+ html.I(
2638
+ className="fas fa-eye",
2639
+ style={"marginRight": "0.25rem"},
2640
+ ),
2641
+ "View chat",
2642
+ ],
2643
+ id={"type": "open-chat-btn-rc", "index": row["id"]},
2644
+ className="open-chat-btn",
2645
+ n_clicks=0,
2646
+ )
2647
+ ),
2648
+ ]
2649
+ )
2650
+ )
2651
+
2652
+ table = html.Table(table_rows, className="dialogs-table")
2653
+
2654
+ modal_title = f"Dialogs with Root Cause: {root_cause} (Topic: {topic_name})"
2655
+ count_info = html.P(
2656
+ f"Found {len(filtered_conversations)} dialogs with this root cause",
2657
+ style={
2658
+ "margin": "0 0 1rem 0",
2659
+ "color": "var(--muted-foreground)",
2660
+ "fontSize": "0.875rem",
2661
+ },
2662
+ )
2663
+
2664
+ content = html.Div([count_info, table])
2665
+
2666
+ return {"display": "flex"}, modal_title, content
2667
+
2668
+
2669
+ # Callback to close root cause modal
2670
+ @callback(
2671
+ Output("root-cause-modal", "style", allow_duplicate=True),
2672
+ [Input("close-root-cause-modal-btn", "n_clicks")],
2673
+ prevent_initial_call=True,
2674
+ )
2675
+ def close_root_cause_modal(n_clicks):
2676
+ if n_clicks:
2677
+ return {"display": "none"}
2678
+ return {"display": "none"}
2679
+
2680
+
2681
+ # Callback to open conversation modal from root cause table
2682
+ @callback(
2683
+ [
2684
+ Output("conversation-modal", "style", allow_duplicate=True),
2685
+ Output("conversation-content", "children", allow_duplicate=True),
2686
+ Output("conversation-subheader", "children", allow_duplicate=True),
2687
+ ],
2688
+ [Input({"type": "open-chat-btn-rc", "index": dash.dependencies.ALL}, "n_clicks")],
2689
+ [State("upload-data", "contents")],
2690
+ prevent_initial_call=True,
2691
+ )
2692
+ def open_conversation_from_root_cause_table(n_clicks_list, file_contents):
2693
+ # Check if any button was clicked
2694
+ if not any(n_clicks_list) or not file_contents:
2695
+ return {"display": "none"}, "", ""
2696
+
2697
+ # Get which button was clicked
2698
+ ctx = dash.callback_context
2699
+ if not ctx.triggered:
2700
+ return {"display": "none"}, "", ""
2701
+
2702
+ triggered_id = ctx.triggered[0]["prop_id"]
2703
+ chat_id = json.loads(triggered_id.split(".")[0])["index"]
2704
+
2705
+ # Get the full conversation from the uploaded file
2706
+ content_type, content_string = file_contents.split(",")
2707
+ decoded = base64.b64decode(content_string)
2708
+
2709
+ if (
2710
+ content_type
2711
+ == "data:application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;base64"
2712
+ ):
2713
+ df_full = pd.read_excel(io.BytesIO(decoded), dtype={"Root_Cause": str})
2714
+ else: # Assume CSV
2715
+ df_full = pd.read_csv(
2716
+ io.StringIO(decoded.decode("utf-8")), dtype={"Root_Cause": str}
2717
+ )
2718
+
2719
+ # Find the conversation with this chat ID
2720
+ conversation_row = df_full[df_full["id"] == chat_id]
2721
+
2722
+ # If not found, try converting types
2723
+ if len(conversation_row) == 0:
2724
+ conversation_row = df_full[df_full["id"].astype(str) == str(chat_id)]
2725
+
2726
+ if len(conversation_row) == 0:
2727
+ try:
2728
+ conversation_row = df_full[df_full["id"] == int(chat_id)]
2729
+ except (ValueError, TypeError):
2730
+ pass
2731
+
2732
+ if len(conversation_row) == 0:
2733
+ conversation_text = f"Conversation not found for Chat ID: {chat_id}"
2734
+ subheader_content = f"Chat ID: {chat_id} (Not Found)"
2735
+ else:
2736
+ row = conversation_row.iloc[0]
2737
+ conversation_text = row.get("conversation", "No conversation data available.")
2738
+
2739
+ # Get additional metadata
2740
+ root_cause = row.get("root_cause_subcluster", "Unknown")
2741
+ cluster_name = row.get("deduplicated_topic_name", "Unknown cluster")
2742
+
2743
+ # Create subheader with metadata including root cause
2744
+ subheader_content = html.Div(
2745
+ [
2746
+ html.Span(
2747
+ f"Chat ID: {chat_id}",
2748
+ style={"fontWeight": "600", "marginRight": "1rem"},
2749
+ ),
2750
+ html.Span(
2751
+ f"Cluster: {cluster_name}",
2752
+ style={"color": "hsl(215.4, 16.3%, 46.9%)", "marginRight": "1rem"},
2753
+ ),
2754
+ html.Span(
2755
+ f"Root Cause: {root_cause}",
2756
+ style={"color": "#8b6f47", "fontWeight": "500"},
2757
+ ),
2758
+ ]
2759
+ )
2760
+
2761
+ return {"display": "flex"}, conversation_text, subheader_content
2762
+
2763
 
2764
  if __name__ == "__main__":
2765
+ app.run(debug=False)