eloukas commited on
Commit
3ccc0de
Β·
1 Parent(s): 1b75011

Implement project structure and configuration for initial setup

Browse files
Files changed (1) hide show
  1. dashboard.py +0 -1519
dashboard.py DELETED
@@ -1,1519 +0,0 @@
1
- import base64
2
- import io
3
- import random
4
-
5
- import dash
6
- import numpy as np
7
- import pandas as pd
8
- import plotly.express as px
9
- import plotly.graph_objects as go
10
- 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(
19
- [
20
- # Header
21
- html.Div(
22
- [
23
- html.H1(
24
- "Sessions Observatory by helvia.ai πŸ”­πŸ“Š",
25
- className="app-header",
26
- ),
27
- html.P(
28
- "Upload a CSV/Excel file to visualize the chatbot's dialog topics.",
29
- className="app-description",
30
- ),
31
- ],
32
- className="header-container",
33
- ),
34
- # File Upload Component
35
- html.Div(
36
- [
37
- dcc.Upload(
38
- id="upload-data",
39
- children=html.Div(
40
- [
41
- html.Div("Drag and Drop", className="upload-text"),
42
- html.Div("or", className="upload-divider"),
43
- html.Div(
44
- html.Button("Select a File", className="upload-button")
45
- ),
46
- ],
47
- className="upload-content",
48
- ),
49
- style={
50
- "width": "100%",
51
- "height": "120px",
52
- "lineHeight": "60px",
53
- "borderWidth": "1px",
54
- "borderStyle": "dashed",
55
- "borderRadius": "0.5rem",
56
- "textAlign": "center",
57
- "margin": "10px 0",
58
- "backgroundColor": "hsl(210, 40%, 98%)",
59
- "borderColor": "hsl(214.3, 31.8%, 91.4%)",
60
- "cursor": "pointer",
61
- },
62
- multiple=False,
63
- ),
64
- # Status message with more padding and emphasis
65
- html.Div(
66
- id="upload-status",
67
- className="upload-status-message",
68
- style={"display": "none"}, # Initially hidden
69
- ),
70
- ],
71
- className="upload-container",
72
- ),
73
- # Main Content Area (hidden until file is uploaded)
74
- html.Div(
75
- [
76
- # Dashboard layout with flexible grid
77
- html.Div(
78
- [
79
- # Left side: Bubble chart
80
- html.Div(
81
- [
82
- html.H3(
83
- id="topic-distribution-header",
84
- children="Sessions Observatory",
85
- className="section-header",
86
- ),
87
- # dcc.Graph(id="bubble-chart", style={"height": "80vh"}),
88
- dcc.Graph(
89
- id="bubble-chart",
90
- style={"height": "calc(100% - 154px)"},
91
- ), # this does not work for some reason
92
- html.Div(
93
- [
94
- # Only keep Color by
95
- html.Div(
96
- [
97
- html.Div(
98
- html.Label(
99
- "Color by:",
100
- className="control-label",
101
- ),
102
- className="control-label-container",
103
- ),
104
- ],
105
- className="control-labels-row",
106
- ),
107
- # Only keep Color by options
108
- html.Div(
109
- [
110
- html.Div(
111
- dcc.RadioItems(
112
- id="color-metric",
113
- options=[
114
- {
115
- "label": "Sentiment",
116
- "value": "negative_rate",
117
- },
118
- {
119
- "label": "Resolution",
120
- "value": "unresolved_rate",
121
- },
122
- {
123
- "label": "Urgency",
124
- "value": "urgent_rate",
125
- },
126
- ],
127
- value="negative_rate",
128
- inline=True,
129
- className="radio-group",
130
- inputClassName="radio-input",
131
- labelClassName="radio-label",
132
- ),
133
- className="radio-container",
134
- ),
135
- ],
136
- className="control-options-row",
137
- ),
138
- ],
139
- className="chart-controls",
140
- ),
141
- ],
142
- className="chart-container",
143
- ),
144
- # Right side: Interactive sidebar with topic details
145
- html.Div(
146
- [
147
- html.Div(
148
- [
149
- html.H3(
150
- "Topic Details", className="section-header"
151
- ),
152
- html.Div(
153
- id="topic-title", className="topic-title"
154
- ),
155
- html.Div(
156
- [
157
- html.Div(
158
- [
159
- html.H4(
160
- "Metadata",
161
- className="subsection-header",
162
- ),
163
- html.Div(
164
- id="topic-metadata",
165
- className="metadata-container",
166
- ),
167
- ],
168
- className="metadata-section",
169
- ),
170
- html.Div(
171
- [
172
- html.H4(
173
- "Key Metrics",
174
- className="subsection-header",
175
- ),
176
- html.Div(
177
- id="topic-metrics",
178
- className="metrics-container",
179
- ),
180
- ],
181
- className="metrics-section",
182
- ),
183
- # Added Tags section
184
- html.Div(
185
- [
186
- html.H4(
187
- "Tags",
188
- className="subsection-header",
189
- ),
190
- 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",
207
- className="sample-dialogs-container",
208
- ),
209
- ],
210
- className="samples-section",
211
- ),
212
- ],
213
- className="topic-details-content",
214
- ),
215
- html.Div(
216
- id="no-topic-selected",
217
- children=[
218
- html.Div(
219
- [
220
- html.I(
221
- className="fas fa-info-circle info-icon"
222
- ),
223
- html.H3("No topic selected"),
224
- html.P(
225
- "Click or hover on a bubble to view topic details."
226
- ),
227
- ],
228
- className="no-selection-message",
229
- )
230
- ],
231
- className="no-selection-container",
232
- ),
233
- ],
234
- className="sidebar-container",
235
- ),
236
- ],
237
- className="dashboard-container",
238
- )
239
- ],
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
- )
248
-
249
- # Define CSS for the app
250
- app.index_string = """
251
- <!DOCTYPE html>
252
- <html>
253
- <head>
254
- {%metas%}
255
- <title>Sessions Observatory by helvia.ai πŸ”­πŸ“Š</title>
256
- {%favicon%}
257
- {%css%}
258
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
259
- <style>
260
- @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
261
-
262
- :root {
263
- --background: hsl(210, 20%, 95%);
264
- --foreground: hsl(222.2, 84%, 4.9%);
265
- --card: hsl(0, 0%, 100%);
266
- --card-foreground: hsl(222.2, 84%, 4.9%);
267
- --popover: hsl(0, 0%, 100%);
268
- --popover-foreground: hsl(222.2, 84%, 4.9%);
269
- --primary: hsl(222.2, 47.4%, 11.2%);
270
- --primary-foreground: hsl(210, 40%, 98%);
271
- --secondary: hsl(210, 40%, 96.1%);
272
- --secondary-foreground: hsl(222.2, 47.4%, 11.2%);
273
- --muted: hsl(210, 40%, 96.1%);
274
- --muted-foreground: hsl(215.4, 16.3%, 46.9%);
275
- --accent: hsl(210, 40%, 96.1%);
276
- --accent-foreground: hsl(222.2, 47.4%, 11.2%);
277
- --destructive: hsl(0, 84.2%, 60.2%);
278
- --destructive-foreground: hsl(210, 40%, 98%);
279
- --border: hsl(214.3, 31.8%, 91.4%);
280
- --input: hsl(214.3, 31.8%, 91.4%);
281
- --ring: hsl(222.2, 84%, 4.9%);
282
- --radius: 0.5rem;
283
- }
284
-
285
- * {
286
- margin: 0;
287
- padding: 0;
288
- box-sizing: border-box;
289
- font-family: 'Inter', sans-serif;
290
- }
291
-
292
- body {
293
- background-color: var(--background);
294
- color: var(--foreground);
295
- font-feature-settings: "rlig" 1, "calt" 1;
296
- }
297
-
298
- .app-container {
299
- max-width: 2500px;
300
- margin: 0 auto;
301
- padding: 1.5rem;
302
- background-color: var(--background);
303
- min-height: 100vh;
304
- display: flex;
305
- flex-direction: column;
306
- }
307
-
308
- .header-container {
309
- margin-bottom: 2rem;
310
- text-align: center;
311
- }
312
-
313
- .app-header {
314
- color: var(--foreground);
315
- margin-bottom: 0.75rem;
316
- font-weight: 600;
317
- font-size: 2rem;
318
- line-height: 1.2;
319
- }
320
-
321
- .app-description {
322
- color: var(--muted-foreground);
323
- font-size: 1rem;
324
- line-height: 1.5;
325
- }
326
-
327
- .upload-container {
328
- margin-bottom: 2rem;
329
- max-width: 800px;
330
- margin-left: auto;
331
- margin-right: auto;
332
- }
333
-
334
- .upload-content {
335
- display: flex;
336
- flex-direction: column;
337
- align-items: center;
338
- justify-content: center;
339
- height: 80%;
340
- padding: 1.5rem;
341
- position: relative;
342
- }
343
-
344
- .upload-text {
345
- font-size: 1rem;
346
- color: var(--primary);
347
- font-weight: 500;
348
- }
349
-
350
- .upload-divider {
351
- color: var(--muted-foreground);
352
- margin: 0.5rem 0;
353
- font-size: 0.875rem;
354
- }
355
-
356
- .upload-button {
357
- background-color: var(--primary);
358
- color: var(--primary-foreground);
359
- border: none;
360
- padding: 0.5rem 1rem;
361
- border-radius: var(--radius);
362
- font-size: 0.875rem;
363
- cursor: pointer;
364
- transition: opacity 0.2s;
365
- font-weight: 500;
366
- box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
367
- height: 2.5rem;
368
- }
369
-
370
- .upload-button:hover {
371
- opacity: 0.9;
372
- }
373
-
374
- /* Status message styling */
375
- .upload-status-message {
376
- margin-top: 1rem;
377
- padding: 0.75rem;
378
- font-weight: 500;
379
- text-align: center;
380
- border-radius: var(--radius);
381
- font-size: 0.875rem;
382
- transition: all 0.3s ease;
383
- background-color: var(--secondary);
384
- color: var(--secondary-foreground);
385
- }
386
-
387
- /* Chart controls styling */
388
- .chart-controls {
389
- margin-top: 1rem;
390
- display: flex;
391
- flex-direction: column;
392
- gap: 0.75rem;
393
- padding: 1rem;
394
- background-color: var(--card);
395
- border-radius: var(--radius);
396
- border: 1px solid var(--border);
397
- box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
398
- }
399
-
400
- .control-labels-row {
401
- display: flex;
402
- width: 100%;
403
- }
404
-
405
- .control-options-row {
406
- display: flex;
407
- width: 100%;
408
- }
409
-
410
- .control-label-container {
411
- padding: 0 0.5rem;
412
- text-align: left;
413
- }
414
-
415
- .control-label {
416
- font-weight: 500;
417
- color: var(--foreground);
418
- font-size: 0.875rem;
419
- line-height: 1.25rem;
420
- }
421
-
422
- .radio-container {
423
- padding: 0 0.5rem;
424
- width: 100%;
425
- }
426
-
427
- .radio-group {
428
- display: flex;
429
- gap: 1rem;
430
- }
431
-
432
- .radio-input {
433
- margin-right: 0.375rem;
434
- cursor: pointer;
435
- height: 1rem;
436
- width: 1rem;
437
- border-radius: 9999px;
438
- border: 1px solid var(--border);
439
- appearance: none;
440
- -webkit-appearance: none;
441
- background-color: var(--background);
442
- transition: border-color 0.2s;
443
- }
444
-
445
- .radio-input:checked {
446
- border-color: var(--primary);
447
- background-color: var(--primary);
448
- background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3ccircle cx='8' cy='8' r='3'/%3e%3c/svg%3e");
449
- background-size: 100% 100%;
450
- background-position: center;
451
- background-repeat: no-repeat;
452
- }
453
-
454
- .radio-label {
455
- font-weight: 400;
456
- color: var(--foreground);
457
- display: flex;
458
- align-items: center;
459
- cursor: pointer;
460
- font-size: 0.875rem;
461
- line-height: 1.25rem;
462
- }
463
-
464
- /* Dashboard container */
465
- .dashboard-container {
466
- display: flex;
467
- flex-wrap: wrap;
468
- gap: 1.5rem;
469
- flex: 1;
470
- height: 100%;
471
- }
472
-
473
- .chart-container {
474
- flex: 2.75;
475
- min-width: 400px;
476
- background: var(--card);
477
- border-radius: var(--radius);
478
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
479
- padding: 1rem;
480
- border: 0.75px solid var(--border);
481
- height: 100%;
482
- }
483
-
484
- .sidebar-container {
485
- flex: 1;
486
- min-width: 300px;
487
- background: var(--card);
488
- border-radius: var(--radius);
489
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
490
- padding: 1rem;
491
- position: relative;
492
- height: 100vh;
493
- overflow-y: auto;
494
- border: 1px solid var(--border);
495
- height: 100%;
496
- }
497
-
498
- .section-header {
499
- margin-bottom: 1rem;
500
- color: var(--foreground);
501
- border-bottom: 1px solid var(--border);
502
- padding-bottom: 0.75rem;
503
- font-weight: 600;
504
- font-size: 1.25rem;
505
- }
506
-
507
- .subsection-header {
508
- margin: 1rem 0 0.75rem;
509
- color: var(--foreground);
510
- font-size: 1rem;
511
- font-weight: 600;
512
- }
513
-
514
- .topic-title {
515
- font-size: 1.25rem;
516
- font-weight: 600;
517
- color: var(--foreground);
518
- margin-bottom: 1rem;
519
- padding: 0.5rem 0.75rem;
520
- background-color: var(--secondary);
521
- border-radius: var(--radius);
522
- }
523
-
524
- .metadata-container {
525
- display: flex;
526
- flex-wrap: wrap;
527
- gap: 0.75rem;
528
- margin-bottom: 1rem;
529
- }
530
-
531
- .metadata-item {
532
- background-color: var(--secondary);
533
- padding: 0.5rem 0.75rem;
534
- border-radius: var(--radius);
535
- font-size: 0.875rem;
536
- display: flex;
537
- align-items: center;
538
- color: var(--secondary-foreground);
539
- }
540
-
541
- .metadata-icon {
542
- margin-right: 0.5rem;
543
- color: var(--primary);
544
- }
545
-
546
- .metrics-container {
547
- display: flex;
548
- justify-content: space-between;
549
- gap: 0.75rem;
550
- margin-bottom: 0.75rem;
551
- }
552
-
553
- .metric-box {
554
- background-color: var(--card);
555
- border-radius: var(--radius);
556
- padding: 0.75rem;
557
- text-align: center;
558
- flex: 1;
559
- border: 1px solid var(--border);
560
- }
561
-
562
- .metric-box.negative {
563
- border-left: 3px solid var(--destructive);
564
- }
565
-
566
- .metric-box.unresolved {
567
- border-left: 3px solid hsl(47.9, 95.8%, 53.1%);
568
- }
569
-
570
- .metric-box.urgent {
571
- border-left: 3px solid hsl(217.2, 91.2%, 59.8%);
572
- }
573
-
574
- .metric-value {
575
- font-size: 1.5rem;
576
- font-weight: 600;
577
- margin-bottom: 0.25rem;
578
- color: var(--foreground);
579
- line-height: 1;
580
- }
581
-
582
- .metric-label {
583
- font-size: 0.75rem;
584
- color: var(--muted-foreground);
585
- }
586
-
587
- .sample-dialogs-container {
588
- margin-top: 0.75rem;
589
- }
590
-
591
- .dialog-item {
592
- background-color: var(--secondary);
593
- border-radius: var(--radius);
594
- padding: 1rem;
595
- margin-bottom: 0.75rem;
596
- border-left: 3px solid var(--primary);
597
- }
598
-
599
- .dialog-summary {
600
- font-size: 0.875rem;
601
- line-height: 1.5;
602
- margin-bottom: 0.5rem;
603
- color: var(--foreground);
604
- }
605
-
606
- .dialog-metadata {
607
- display: flex;
608
- flex-wrap: wrap;
609
- gap: 0.5rem;
610
- margin-top: 0.5rem;
611
- font-size: 0.75rem;
612
- }
613
-
614
- .dialog-tag {
615
- padding: 0.25rem 0.5rem;
616
- border-radius: var(--radius);
617
- font-size: 0.7rem;
618
- font-weight: 500;
619
- }
620
-
621
- .tag-sentiment {
622
- background-color: var(--destructive);
623
- color: var(--destructive-foreground);
624
- }
625
-
626
- .tag-resolution {
627
- background-color: hsl(47.9, 95.8%, 53.1%);
628
- color: hsl(222.2, 84%, 4.9%);
629
- }
630
-
631
- .tag-urgency {
632
- background-color: hsl(217.2, 91.2%, 59.8%);
633
- color: hsl(210, 40%, 98%);
634
- }
635
-
636
- .tag-chat-id {
637
- background-color: hsl(215.4, 16.3%, 46.9%);
638
- color: hsl(210, 40%, 98%);
639
- font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
640
- font-weight: 500;
641
- }
642
-
643
- .no-selection-container {
644
- position: absolute;
645
- top: 0;
646
- left: 0;
647
- right: 0;
648
- bottom: 0;
649
- display: flex;
650
- align-items: center;
651
- justify-content: center;
652
- background-color: hsla(0, 0%, 100%, 0.95);
653
- z-index: 10;
654
- border-radius: var(--radius);
655
- }
656
-
657
- .no-selection-message {
658
- text-align: center;
659
- color: var(--muted-foreground);
660
- padding: 1.5rem;
661
- }
662
-
663
- .info-icon {
664
- font-size: 2rem;
665
- margin-bottom: 0.75rem;
666
- color: var(--muted);
667
- }
668
-
669
- /* Tags container */
670
- .tags-container {
671
- display: flex;
672
- flex-wrap: wrap;
673
- gap: 8px;
674
- margin-top: 5px;
675
- margin-bottom: 15px;
676
- padding: 6px;
677
- border-radius: 8px;
678
- background-color: #f8f9fa;
679
- }
680
-
681
-
682
- .topic-tag {
683
- padding: 0.375rem 0.75rem;
684
- border-radius: var(--radius);
685
- font-size: 0.75rem;
686
- display: inline-flex;
687
- align-items: center;
688
- transition: all 0.2s ease;
689
- font-weight: 500;
690
- margin-bottom: 0.25rem;
691
- cursor: default;
692
- background-color: var(--muted);
693
- color: var(--muted-foreground);
694
- border: 1px solid var(--border);
695
- }
696
-
697
- .topic-tag {
698
- padding: 6px 12px;
699
- border-radius: 15px;
700
- font-size: 0.8rem;
701
- display: inline-flex;
702
- align-items: center;
703
- box-shadow: 0 1px 3px rgba(0,0,0,0.12);
704
- transition: all 0.2s ease;
705
- font-weight: 500;
706
- margin-bottom: 5px;
707
- cursor: default;
708
- border: 1px solid rgba(0,0,0,0.08);
709
- background-color: #6c757d; /* Consistent medium gray color */
710
- color: white;
711
- }
712
-
713
- .topic-tag:hover {
714
- transform: translateY(-1px);
715
- box-shadow: 0 3px 5px rgba(0,0,0,0.15);
716
- background-color: #5a6268; /* Slightly darker on hover */
717
- }
718
-
719
- .topic-tag-icon {
720
- margin-right: 5px;
721
- font-size: 0.7rem;
722
- opacity: 0.8;
723
- color: rgba(255, 255, 255, 0.9);
724
- }
725
-
726
- .no-tags-message {
727
- color: var(--muted-foreground);
728
- font-style: italic;
729
- padding: 0.75rem;
730
- text-align: center;
731
- width: 100%;
732
- }
733
-
734
- /* Responsive adjustments */
735
- @media (max-width: 768px) {
736
- .dashboard-container {
737
- flex-direction: column;
738
- }
739
- .chart-container, .sidebar-container {
740
- width: 100%;
741
- }
742
- .app-header {
743
- font-size: 1.5rem;
744
- }
745
- }
746
- </style>
747
- </head>
748
- <body>
749
- {%app_entry%}
750
- <footer>
751
- {%config%}
752
- {%scripts%}
753
- {%renderer%}
754
- </footer>
755
- </body>
756
- </html>
757
- """
758
-
759
-
760
- @callback(
761
- Output("topic-distribution-header", "children"),
762
- Input("stored-data", "data"),
763
- )
764
- def update_topic_distribution_header(data):
765
- if not data:
766
- return "Sessions Observatory" # Default when no data is available
767
-
768
- df = pd.DataFrame(data)
769
- total_dialogs = df["count"].sum() # Sum up the 'count' column
770
- return f"Sessions Observatory ({total_dialogs} dialogs)"
771
-
772
-
773
- # Define callback to process uploaded file
774
- @callback(
775
- [
776
- Output("stored-data", "data"),
777
- Output("upload-status", "children"),
778
- Output("upload-status", "style"), # Add style output for visibility
779
- Output("main-content", "style"),
780
- ],
781
- [Input("upload-data", "contents")],
782
- [State("upload-data", "filename")],
783
- )
784
- def process_upload(contents, filename):
785
- if contents is None:
786
- return None, "", {"display": "none"}, {"display": "none"} # Keep hidden
787
-
788
- try:
789
- # Parse uploaded file
790
- content_type, content_string = contents.split(",")
791
- decoded = base64.b64decode(content_string)
792
-
793
- if "csv" in filename.lower():
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,
800
- html.Div(
801
- [
802
- html.I(
803
- className="fas fa-exclamation-circle",
804
- style={"color": "var(--destructive)", "marginRight": "8px"},
805
- ),
806
- "Please upload a CSV or Excel file.",
807
- ],
808
- style={"color": "var(--destructive)"},
809
- ),
810
- {"display": "block"}, # Make visible after error
811
- {"display": "none"},
812
- )
813
-
814
- # Process the dataframe to get topic statistics
815
- topic_stats = analyze_topics(df)
816
-
817
- return (
818
- topic_stats.to_dict("records"),
819
- html.Div(
820
- [
821
- html.I(
822
- className="fas fa-check-circle",
823
- style={
824
- "color": "hsl(142.1, 76.2%, 36.3%)",
825
- "marginRight": "8px",
826
- },
827
- ),
828
- f'Successfully uploaded "{filename}"',
829
- ],
830
- style={"color": "hsl(142.1, 76.2%, 36.3%)"},
831
- ),
832
- {"display": "block"}, # maybe add the above line here too #TODO
833
- {
834
- "display": "block",
835
- "height": "calc(100vh - 40px)",
836
- }, # Make visible after successful upload
837
- )
838
-
839
- except Exception as e:
840
- return (
841
- None,
842
- html.Div(
843
- [
844
- html.I(
845
- className="fas fa-exclamation-triangle",
846
- style={"color": "var(--destructive)", "marginRight": "8px"},
847
- ),
848
- f"Error processing file: {str(e)}",
849
- ],
850
- style={"color": "var(--destructive)"},
851
- ),
852
- {"display": "block"}, # Make visible after error
853
- {"display": "none"},
854
- )
855
-
856
-
857
- # Function to analyze the topics and create statistics
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"),
864
- negative_count=("Sentiment", lambda x: (x == "negative").sum()),
865
- unresolved_count=("Resolution", lambda x: (x == "unresolved").sum()),
866
- urgent_count=("Urgency", lambda x: (x == "urgent").sum()),
867
- )
868
- .reset_index()
869
- )
870
-
871
- # Calculate rates
872
- topic_stats["negative_rate"] = (
873
- topic_stats["negative_count"] / topic_stats["count"] * 100
874
- ).round(1)
875
- topic_stats["unresolved_rate"] = (
876
- topic_stats["unresolved_count"] / topic_stats["count"] * 100
877
- ).round(1)
878
- topic_stats["urgent_rate"] = (
879
- topic_stats["urgent_count"] / topic_stats["count"] * 100
880
- ).round(1)
881
-
882
- # Apply binned layout
883
- topic_stats = apply_binned_layout(topic_stats)
884
-
885
- return topic_stats
886
-
887
-
888
- # New binned layout function
889
-
890
-
891
- def apply_binned_layout(df, padding=0, bin_config=None, max_items_per_row=6):
892
- """
893
- Apply a binned layout where bubbles are grouped into rows based on dialog count.
894
- Bubbles in each row will be centered horizontally.
895
-
896
- Args:
897
- df: DataFrame containing the topic data
898
- padding: Padding from edges as percentage
899
- bin_config: List of tuples defining bin ranges and descriptions.
900
- Example: [(300, None, "300+ dialogs"), (250, 299, "250-299 dialogs"), ...]
901
- max_items_per_row: Maximum number of items to display in a single row
902
-
903
- Returns:
904
- DataFrame with updated x, y positions
905
- """
906
- # Create a copy of the dataframe to avoid modifying the original
907
- df_sorted = df.copy()
908
-
909
- # Default bin configuration if none is provided
910
- # 8 rows x 6 bubbles is usually good
911
- if bin_config is None:
912
- bin_config = [
913
- (100, None, "100+ dialogs"),
914
- (50, 99, "50-99 dialogs"),
915
- (25, 49, "25-49 dialogs"),
916
- (9, 24, "9-24 dialogs"),
917
- (7, 8, "7-8 dialogs"),
918
- (5, 7, "5-6 dialogs"),
919
- (4, 4, "4 dialogs"),
920
- (0, 3, "0-3 dialogs"),
921
- ]
922
-
923
- # Generate bin descriptions and conditions dynamically
924
- bin_descriptions = {}
925
- conditions = []
926
- bin_values = []
927
-
928
- for i, (lower, upper, description) in enumerate(bin_config):
929
- bin_name = f"Bin {i + 1}"
930
- bin_descriptions[bin_name] = description
931
- bin_values.append(bin_name)
932
-
933
- if upper is None: # No upper limit
934
- conditions.append(df_sorted["count"] >= lower)
935
- else:
936
- conditions.append(
937
- (df_sorted["count"] >= lower) & (df_sorted["count"] <= upper)
938
- )
939
-
940
- # Apply the conditions to create the bin column
941
- df_sorted["bin"] = np.select(conditions, bin_values, default="Bin 8")
942
- df_sorted["bin_description"] = df_sorted["bin"].map(bin_descriptions)
943
-
944
- # Sort by bin (ascending to get Bin 1 first) and by count (descending) within each bin
945
- df_sorted = df_sorted.sort_values(by=["bin", "count"], ascending=[True, False])
946
-
947
- # Now split bins that have more than max_items_per_row items
948
- original_bins = df_sorted["bin"].unique()
949
- new_rows = []
950
- new_bin_descriptions = bin_descriptions.copy()
951
-
952
- for bin_name in original_bins:
953
- bin_mask = df_sorted["bin"] == bin_name
954
- bin_group = df_sorted[bin_mask]
955
- bin_size = len(bin_group)
956
-
957
- # If bin has more items than max_items_per_row, split it
958
- if bin_size > max_items_per_row:
959
- # Calculate how many sub-bins we need
960
- num_sub_bins = (bin_size + max_items_per_row - 1) // max_items_per_row
961
-
962
- # Calculate items per sub-bin (distribute evenly)
963
- items_per_sub_bin = [bin_size // num_sub_bins] * num_sub_bins
964
-
965
- # Distribute the remainder one by one to achieve balance
966
- remainder = bin_size % num_sub_bins
967
- for i in range(remainder):
968
- items_per_sub_bin[i] += 1
969
-
970
- # Original bin description
971
- original_description = bin_descriptions[bin_name]
972
-
973
- # Create new row entries and update bin assignments
974
- start_idx = 0
975
- for i in range(num_sub_bins):
976
- # Create new bin name with sub-bin index
977
- new_bin_name = f"{bin_name}_{i + 1}"
978
-
979
- # Create new bin description with sub-bin index
980
- new_description = f"{original_description} ({i + 1}/{num_sub_bins})"
981
- new_bin_descriptions[new_bin_name] = new_description
982
-
983
- # Get slice of dataframe for this sub-bin
984
- end_idx = start_idx + items_per_sub_bin[i]
985
- sub_bin_rows = bin_group.iloc[start_idx:end_idx].copy()
986
-
987
- # Update bin name and description
988
- sub_bin_rows["bin"] = new_bin_name
989
- sub_bin_rows["bin_description"] = new_description
990
-
991
- # Add to new rows
992
- new_rows.append(sub_bin_rows)
993
-
994
- # Update start index for next iteration
995
- start_idx = end_idx
996
-
997
- # Remove the original bin from df_sorted
998
- df_sorted = df_sorted[~bin_mask]
999
-
1000
- # Combine the original dataframe (with small bins) and the new split bins
1001
- if new_rows:
1002
- df_sorted = pd.concat([df_sorted] + new_rows)
1003
-
1004
- # Re-sort with the new bin names
1005
- df_sorted = df_sorted.sort_values(by=["bin", "count"], ascending=[True, False])
1006
-
1007
- # Calculate the vertical positions for each row (bin)
1008
- bins_with_topics = sorted(df_sorted["bin"].unique())
1009
- num_rows = len(bins_with_topics)
1010
-
1011
- available_height = 100 - (2 * padding)
1012
- row_height = available_height / num_rows
1013
-
1014
- # Calculate and assign y-positions (vertical positions)
1015
- row_positions = {}
1016
- for i, bin_name in enumerate(bins_with_topics):
1017
- # Calculate row position (centered within its allocated space)
1018
- row_pos = padding + i * row_height + (row_height / 2)
1019
- row_positions[bin_name] = row_pos
1020
-
1021
- df_sorted["y"] = df_sorted["bin"].map(row_positions)
1022
-
1023
- # Center the bubbles in each row horizontally
1024
- center_point = 50 # Middle of the chart (0-100 scale)
1025
- for bin_name in bins_with_topics:
1026
- # Get topics in this bin
1027
- bin_mask = df_sorted["bin"] == bin_name
1028
- num_topics_in_bin = bin_mask.sum()
1029
-
1030
- if num_topics_in_bin == 1:
1031
- # If there's only one bubble, place it in the center
1032
- df_sorted.loc[bin_mask, "x"] = center_point
1033
- else:
1034
- if num_topics_in_bin < max_items_per_row:
1035
- # For fewer bubbles, add a little bit of spacing between them
1036
- # Calculate the total width needed
1037
- total_width = (num_topics_in_bin - 1) * 17.5 # 10 units between bubbles
1038
- # Calculate starting position (to center the group)
1039
- start_pos = center_point - (total_width / 2)
1040
- # Assign positions
1041
- positions = [start_pos + (i * 17.5) for i in range(num_topics_in_bin)]
1042
- df_sorted.loc[bin_mask, "x"] = positions
1043
- else:
1044
- # For multiple bubbles, distribute them evenly around the center
1045
- # Calculate the total width needed
1046
- total_width = (num_topics_in_bin - 1) * 15 # 15 units between bubbles
1047
-
1048
- # Calculate starting position (to center the group)
1049
- start_pos = center_point - (total_width / 2)
1050
-
1051
- # Assign positions
1052
- positions = [start_pos + (i * 15) for i in range(num_topics_in_bin)]
1053
- df_sorted.loc[bin_mask, "x"] = positions
1054
-
1055
- # Add original rank for reference
1056
- df_sorted["size_rank"] = range(1, len(df_sorted) + 1)
1057
-
1058
- return df_sorted
1059
-
1060
-
1061
- # New function to update positions based on selected size metric
1062
- def update_bubble_positions(df: pd.DataFrame) -> pd.DataFrame:
1063
- # For the main chart, we always use the binned layout
1064
- return apply_binned_layout(df)
1065
-
1066
-
1067
- # Callback to update the bubble chart
1068
- @callback(
1069
- Output("bubble-chart", "figure"),
1070
- [
1071
- Input("stored-data", "data"),
1072
- Input("color-metric", "value"),
1073
- ],
1074
- )
1075
- def update_bubble_chart(data, color_metric):
1076
- if not data:
1077
- return go.Figure()
1078
-
1079
- df = pd.DataFrame(data)
1080
-
1081
- # Update positions using binned layout
1082
- df = update_bubble_positions(df)
1083
-
1084
- # Always use count for sizing
1085
- size_values = df["count"]
1086
- raw_sizes = df["count"]
1087
- size_title = "Dialog Count"
1088
-
1089
- # Apply log scaling to the size values for better visualization
1090
- # To make the smallest bubble bigger, increase the min_size value (currently 2.5).
1091
- min_size = 1 # Minimum bubble size
1092
- if size_values.max() > size_values.min():
1093
- # Log-scale the sizes
1094
- log_sizes = np.log1p(size_values)
1095
- # Scale to a reasonable range for visualization
1096
- # To make the biggest bubble smaller, reduce the multiplier (currently 50).
1097
- size_values = (
1098
- min_size
1099
- + (log_sizes - log_sizes.min()) / (log_sizes.max() - log_sizes.min()) * 50
1100
- )
1101
- else:
1102
- # If all values are the same, use a default size
1103
- size_values = np.ones(len(df)) * 12.5
1104
-
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":
1127
- color_values = df["negative_rate"]
1128
- # color_title = "Negative Sentiment (%)"
1129
- color_title = "Negativity (%)"
1130
- # color_scale = "RdBu" # no ice, RdBu - og is Reds - matter is good too
1131
- # color_scale = "Portland"
1132
- # color_scale = "RdYlGn_r"
1133
- # color_scale = "Teal"
1134
- color_scale = "Teal"
1135
-
1136
- elif color_metric == "unresolved_rate":
1137
- color_values = df["unresolved_rate"]
1138
- color_title = "Unresolved (%)"
1139
- # color_scale = "Burg" # og is YlOrRd
1140
- # color_scale = "Temps"
1141
- # color_scale = "Armyrose"
1142
- # color_scale = "YlOrRd"
1143
- color_scale = "Teal"
1144
- else:
1145
- color_values = df["urgent_rate"]
1146
- color_title = "Urgency (%)"
1147
- # color_scale = "Magenta" # og is Blues
1148
- # color_scale = "Tealrose"
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}"
1158
- for topic, raw, color, bin_desc in zip(
1159
- df["deduplicated_topic_name"],
1160
- raw_sizes,
1161
- color_values,
1162
- df["bin_description"],
1163
- )
1164
- ]
1165
-
1166
- # Create bubble chart
1167
- fig = px.scatter(
1168
- df,
1169
- x="x",
1170
- y="y",
1171
- size=size_values,
1172
- color=color_values,
1173
- # text="deduplicated_topic_name", # Remove text here
1174
- hover_name="deduplicated_topic_name",
1175
- hover_data={
1176
- "x": False,
1177
- "y": False,
1178
- "bin_description": True,
1179
- },
1180
- size_max=42.5, # Maximum size of the bubbles, change this to adjust the size
1181
- color_continuous_scale=color_scale,
1182
- custom_data=[
1183
- "deduplicated_topic_name",
1184
- "count",
1185
- "negative_rate",
1186
- "unresolved_rate",
1187
- "urgent_rate",
1188
- "bin_description",
1189
- ],
1190
- )
1191
-
1192
- # Update traces: Remove text related properties
1193
- fig.update_traces(
1194
- mode="markers", # Remove '+text'
1195
- marker=dict(sizemode="area", opacity=0.8, line=dict(width=1, color="white")),
1196
- hovertemplate="%{hovertext}<extra></extra>",
1197
- hovertext=hover_text,
1198
- )
1199
-
1200
- # Create annotations for the bubbles
1201
- annotations = []
1202
- for i, row in df.iterrows():
1203
- # Wrap text every 2 words
1204
- words = row["deduplicated_topic_name"].split()
1205
- wrapped_text = "<br>".join(
1206
- [" ".join(words[i : i + 4]) for i in range(0, len(words), 4)]
1207
- )
1208
-
1209
- # Calculate size for vertical offset (approximately based on the bubble size)
1210
- # Add vertical offset based on bubble size to place text below the bubble
1211
- marker_size = (
1212
- size_values[i] / 20 # type: ignore # FIXME: size_values[df.index.get_loc(i)] / 20
1213
- ) # Adjust this divisor as needed to get proper spacing
1214
-
1215
- annotations.append(
1216
- dict(
1217
- x=row["x"],
1218
- y=row["y"]
1219
- + 0.125 # Adding this so in a row with maximum bubbles, the left one does not overlap with the bin label
1220
- + marker_size, # Add vertical offset to position text below the bubble
1221
- text=wrapped_text,
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",
1230
- ),
1231
- xanchor="center",
1232
- yanchor="top", # Anchor to top of text box so it hangs below the bubble
1233
- bgcolor="rgba(255,255,255,0.7)", # Add semi-transparent background for better readability
1234
- bordercolor="rgba(0,0,0,0.1)", # Add a subtle border color
1235
- borderwidth=1,
1236
- borderpad=1,
1237
- # TODO: Radius for rounded corners
1238
- )
1239
- )
1240
-
1241
- # Add bin labels and separator lines
1242
- unique_bins = sorted(df["bin"].unique())
1243
- bin_y_positions = [
1244
- df[df["bin"] == bin_name]["y"].mean() for bin_name in unique_bins
1245
- ]
1246
-
1247
- # Dynamically extract bin descriptions
1248
- bin_descriptions = df.set_index("bin")["bin_description"].to_dict()
1249
-
1250
- for bin_name, bin_y in zip(unique_bins, bin_y_positions):
1251
- # Add horizontal line
1252
- fig.add_shape(
1253
- type="line",
1254
- x0=0,
1255
- y0=bin_y,
1256
- x1=100,
1257
- y1=bin_y,
1258
- line=dict(color="rgba(0,0,0,0.1)", width=1, dash="dot"),
1259
- layer="below",
1260
- )
1261
-
1262
- # Add subtle lines for each bin and bin labels
1263
- for bin_name, bin_y in zip(unique_bins, bin_y_positions):
1264
- # Add horizontal line
1265
- fig.add_shape(
1266
- type="line",
1267
- x0=0,
1268
- y0=bin_y,
1269
- x1=100,
1270
- y1=bin_y,
1271
- line=dict(color="rgba(0,0,0,0.1)", width=1, dash="dot"),
1272
- layer="below",
1273
- )
1274
-
1275
- # Add bin label annotation
1276
- annotations.append(
1277
- dict(
1278
- x=0, # Position the label on the left side
1279
- y=bin_y,
1280
- xref="x",
1281
- yref="y",
1282
- text=bin_descriptions[bin_name],
1283
- showarrow=False,
1284
- font=dict(size=8.25, color="var(--muted-foreground)"),
1285
- align="left",
1286
- xanchor="left",
1287
- yanchor="middle",
1288
- bgcolor="rgba(255,255,255,0.7)",
1289
- borderpad=1,
1290
- )
1291
- )
1292
-
1293
- fig.update_layout(
1294
- title=None,
1295
- xaxis=dict(
1296
- showgrid=False,
1297
- zeroline=False,
1298
- showticklabels=False,
1299
- title=None,
1300
- range=[0, 100],
1301
- ),
1302
- yaxis=dict(
1303
- showgrid=False,
1304
- zeroline=False,
1305
- showticklabels=False,
1306
- title=None,
1307
- range=[0, 100],
1308
- autorange="reversed", # Keep largest at top
1309
- ),
1310
- hovermode="closest",
1311
- margin=dict(l=0, r=0, t=10, b=10),
1312
- coloraxis_colorbar=dict(
1313
- title=color_title,
1314
- title_font=dict(size=9),
1315
- tickfont=dict(size=8),
1316
- thickness=10,
1317
- len=0.6,
1318
- yanchor="middle",
1319
- y=0.5,
1320
- xpad=0,
1321
- ),
1322
- legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1),
1323
- paper_bgcolor="rgba(0,0,0,0)",
1324
- plot_bgcolor="rgba(0,0,0,0)",
1325
- hoverlabel=dict(bgcolor="white", font_size=12, font_family="Inter"),
1326
- annotations=annotations, # Add bin labels as annotations
1327
- )
1328
-
1329
- return fig
1330
-
1331
-
1332
- # Update the update_topic_details callback to use grayscale colors for tags based on frequency
1333
- @callback(
1334
- [
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]
1354
-
1355
- # Get stored data for this topic
1356
- df_stored = pd.DataFrame(stored_data)
1357
- topic_data = df_stored[df_stored["deduplicated_topic_name"] == topic_name].iloc[0]
1358
-
1359
- # Get original data to sample conversations
1360
- content_type, content_string = file_contents.split(",")
1361
- decoded = base64.b64decode(content_string)
1362
-
1363
- if (
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]
1373
-
1374
- # Create the title
1375
- title = html.Div([html.Span(topic_name)])
1376
-
1377
- # Create metadata items
1378
- metadata_items = [
1379
- html.Div(
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
-
1388
- # Create metrics boxes
1389
- metrics_boxes = [
1390
- html.Div(
1391
- [
1392
- html.Div(f"{topic_data['negative_rate']}%", className="metric-value"),
1393
- html.Div("Negative Sentiment", className="metric-label"),
1394
- ],
1395
- className="metric-box negative",
1396
- ),
1397
- html.Div(
1398
- [
1399
- html.Div(f"{topic_data['unresolved_rate']}%", className="metric-value"),
1400
- html.Div("Unresolved", className="metric-label"),
1401
- ],
1402
- className="metric-box unresolved",
1403
- ),
1404
- html.Div(
1405
- [
1406
- html.Div(f"{topic_data['urgent_rate']}%", className="metric-value"),
1407
- html.Div("Urgent", className="metric-label"),
1408
- ],
1409
- className="metric-box urgent",
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", "")
1417
- if pd.notna(tags_str):
1418
- tags = [tag.strip() for tag in tags_str.split(",") if tag.strip()]
1419
- tags_list.extend(tags)
1420
-
1421
- # Count tag frequencies for better insight
1422
- tag_counts = {}
1423
- for tag in tags_list:
1424
- tag_counts[tag] = tag_counts.get(tag, 0) + 1
1425
-
1426
- # Sort by frequency (most common first) and then alphabetically for ties
1427
- sorted_tags = sorted(tag_counts.items(), key=lambda x: (-x[1], x[0]))
1428
-
1429
- # Keep only the top K tags
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(
1436
- [
1437
- html.Div(
1438
- [
1439
- html.I(className="fas fa-tag topic-tag-icon"),
1440
- html.Span(f"{tag} ({count})"),
1441
- ],
1442
- className="topic-tag",
1443
- )
1444
- for tag, count in sorted_tags
1445
- ],
1446
- className="tags-container",
1447
- )
1448
- else:
1449
- tags_output = html.Div(
1450
- [
1451
- html.I(className="fas fa-info-circle", style={"marginRight": "5px"}),
1452
- "No tags found for this topic",
1453
- ],
1454
- className="no-tags-message",
1455
- )
1456
-
1457
- # Sample up to 5 random dialogs
1458
- sample_size = min(5, len(topic_conversations))
1459
- if sample_size > 0:
1460
- sample_indices = random.sample(range(len(topic_conversations)), sample_size)
1461
- samples = topic_conversations.iloc[sample_indices]
1462
-
1463
- dialog_items = []
1464
- for _, row in samples.iterrows():
1465
- # Create dialog item with tags
1466
- sentiment_tag = html.Span(
1467
- row["Sentiment"], className="dialog-tag tag-sentiment"
1468
- )
1469
- resolution_tag = html.Span(
1470
- row["Resolution"], className="dialog-tag tag-resolution"
1471
- )
1472
- urgency_tag = html.Span(row["Urgency"], className="dialog-tag tag-urgency")
1473
-
1474
- # Add Chat ID tag if 'id' column exists
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(
1488
- [
1489
- html.Div(row["Summary"], className="dialog-summary"),
1490
- html.Div(
1491
- tags,
1492
- className="dialog-metadata",
1493
- ),
1494
- ],
1495
- className="dialog-item",
1496
- )
1497
- )
1498
-
1499
- sample_dialogs = dialog_items
1500
- else:
1501
- sample_dialogs = [
1502
- html.Div(
1503
- "No sample dialogs available for this topic.",
1504
- style={"color": "var(--muted-foreground)"},
1505
- )
1506
- ]
1507
-
1508
- return (
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)