let-them-talk 3.3.3 → 3.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dashboard.html CHANGED
@@ -5,55 +5,72 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>Let Them Talk</title>
7
7
  <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><rect rx='20' width='100' height='100' fill='%230d1117'/><path d='M20 30 Q20 20 30 20 H70 Q80 20 80 30 V55 Q80 65 70 65 H55 L40 80 V65 H30 Q20 65 20 55Z' fill='%2358a6ff'/><circle cx='38' cy='42' r='5' fill='%230d1117'/><circle cx='55' cy='42' r='5' fill='%230d1117'/></svg>">
8
+ <link rel="preconnect" href="https://fonts.googleapis.com">
9
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
8
11
  <style>
9
12
  * { margin: 0; padding: 0; box-sizing: border-box; }
10
13
 
11
14
  :root {
12
- --bg: #0d1117;
13
- --surface: #161b22;
14
- --surface-2: #21262d;
15
- --surface-3: #2d333b;
16
- --border: #30363d;
17
- --border-light: #3d444d;
18
- --text: #e6edf3;
19
- --text-dim: #8b949e;
20
- --text-muted: #656d76;
21
- --accent: #58a6ff;
22
- --accent-dim: rgba(88, 166, 255, 0.15);
23
- --green: #3fb950;
24
- --green-dim: rgba(63, 185, 80, 0.15);
25
- --red: #f85149;
26
- --red-dim: rgba(248, 81, 73, 0.15);
27
- --orange: #d29922;
28
- --orange-dim: rgba(210, 153, 34, 0.15);
29
- --purple: #bc8cff;
30
- --purple-dim: rgba(188, 140, 255, 0.15);
31
- --yellow: #e3b341;
15
+ --bg: #080b12;
16
+ --surface: #0f1318;
17
+ --surface-2: #161c24;
18
+ --surface-3: #1e2530;
19
+ --border: rgba(255, 255, 255, 0.06);
20
+ --border-light: rgba(255, 255, 255, 0.1);
21
+ --text: #f0f4f8;
22
+ --text-dim: #94a3b8;
23
+ --text-muted: #5a6578;
24
+ --accent: #6c8aff;
25
+ --accent-dim: rgba(108, 138, 255, 0.12);
26
+ --accent-glow: rgba(108, 138, 255, 0.25);
27
+ --green: #34d399;
28
+ --green-dim: rgba(52, 211, 153, 0.12);
29
+ --red: #f87171;
30
+ --red-dim: rgba(248, 113, 113, 0.12);
31
+ --orange: #fbbf24;
32
+ --orange-dim: rgba(251, 191, 36, 0.12);
33
+ --purple: #c084fc;
34
+ --purple-dim: rgba(192, 132, 252, 0.12);
35
+ --yellow: #fbbf24;
32
36
  --sidebar-w: 280px;
33
37
  --header-h: 56px;
38
+ --glow: 0 0 20px rgba(108, 138, 255, 0.08);
39
+ --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.3);
40
+ --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.4);
41
+ --shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.5);
42
+ --gradient-accent: linear-gradient(135deg, #6c8aff, #a78bfa);
43
+ --gradient-surface: linear-gradient(180deg, rgba(255,255,255,0.03) 0%, transparent 100%);
34
44
  }
35
45
 
36
46
  [data-theme="light"] {
37
- --bg: #f6f8fa;
47
+ --bg: #f0f2f5;
38
48
  --surface: #ffffff;
39
- --surface-2: #f0f2f5;
40
- --surface-3: #e4e7eb;
41
- --border: #d0d7de;
42
- --border-light: #bbc0c7;
43
- --text: #1f2328;
44
- --text-dim: #57606a;
45
- --text-muted: #8b949e;
46
- --accent: #0969da;
47
- --accent-dim: rgba(9, 105, 218, 0.1);
48
- --green: #1a7f37;
49
- --green-dim: rgba(26, 127, 55, 0.1);
50
- --red: #cf222e;
51
- --red-dim: rgba(207, 34, 46, 0.1);
52
- --orange: #9a6700;
53
- --orange-dim: rgba(154, 103, 0, 0.1);
54
- --purple: #8250df;
55
- --purple-dim: rgba(130, 80, 223, 0.1);
56
- --yellow: #9a6700;
49
+ --surface-2: #f7f8fa;
50
+ --surface-3: #eef0f4;
51
+ --border: rgba(0, 0, 0, 0.08);
52
+ --border-light: rgba(0, 0, 0, 0.12);
53
+ --text: #111827;
54
+ --text-dim: #4b5563;
55
+ --text-muted: #9ca3af;
56
+ --accent: #4f6eff;
57
+ --accent-dim: rgba(79, 110, 255, 0.08);
58
+ --accent-glow: rgba(79, 110, 255, 0.15);
59
+ --green: #059669;
60
+ --green-dim: rgba(5, 150, 105, 0.08);
61
+ --red: #dc2626;
62
+ --red-dim: rgba(220, 38, 38, 0.08);
63
+ --orange: #d97706;
64
+ --orange-dim: rgba(217, 119, 6, 0.08);
65
+ --purple: #7c3aed;
66
+ --purple-dim: rgba(124, 58, 237, 0.08);
67
+ --yellow: #d97706;
68
+ --glow: 0 0 20px rgba(79, 110, 255, 0.05);
69
+ --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.08);
70
+ --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.1);
71
+ --shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.12);
72
+ --gradient-accent: linear-gradient(135deg, #4f6eff, #7c3aed);
73
+ --gradient-surface: linear-gradient(180deg, rgba(0,0,0,0.01) 0%, transparent 100%);
57
74
  }
58
75
 
59
76
  .theme-toggle {
@@ -84,16 +101,20 @@
84
101
  }
85
102
 
86
103
  body {
87
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
104
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
88
105
  background: var(--bg);
89
106
  color: var(--text);
90
107
  min-height: 100vh;
91
108
  overflow: hidden;
109
+ -webkit-font-smoothing: antialiased;
110
+ -moz-osx-font-smoothing: grayscale;
92
111
  }
93
112
 
94
113
  /* ===== HEADER ===== */
95
114
  .header {
96
- background: var(--surface);
115
+ background: rgba(15, 19, 24, 0.8);
116
+ backdrop-filter: blur(16px) saturate(180%);
117
+ -webkit-backdrop-filter: blur(16px) saturate(180%);
97
118
  border-bottom: 1px solid var(--border);
98
119
  padding: 0 20px;
99
120
  height: var(--header-h);
@@ -107,6 +128,10 @@
107
128
  z-index: 100;
108
129
  }
109
130
 
131
+ [data-theme="light"] .header {
132
+ background: rgba(255, 255, 255, 0.85);
133
+ }
134
+
110
135
  .header-left {
111
136
  display: flex;
112
137
  align-items: center;
@@ -115,12 +140,13 @@
115
140
 
116
141
  .logo {
117
142
  font-size: 18px;
118
- font-weight: 700;
119
- background: linear-gradient(135deg, var(--accent), var(--purple));
143
+ font-weight: 800;
144
+ background: var(--gradient-accent);
120
145
  -webkit-background-clip: text;
121
146
  -webkit-text-fill-color: transparent;
122
147
  background-clip: text;
123
148
  white-space: nowrap;
149
+ letter-spacing: -0.3px;
124
150
  }
125
151
 
126
152
  .header-stats {
@@ -163,6 +189,7 @@
163
189
  height: 6px;
164
190
  border-radius: 50%;
165
191
  background: var(--green);
192
+ box-shadow: 0 0 6px var(--green);
166
193
  animation: pulse 2s infinite;
167
194
  }
168
195
 
@@ -176,19 +203,21 @@
176
203
  color: var(--text);
177
204
  border: 1px solid var(--border);
178
205
  padding: 6px 12px;
179
- border-radius: 6px;
206
+ border-radius: 8px;
180
207
  cursor: pointer;
181
208
  font-size: 12px;
182
209
  font-weight: 500;
183
- transition: all 0.15s;
210
+ transition: all 0.2s ease;
184
211
  white-space: nowrap;
212
+ letter-spacing: 0.01em;
185
213
  }
186
214
 
187
- .btn:hover { border-color: var(--border-light); background: var(--surface-3); }
215
+ .btn:hover { border-color: var(--border-light); background: var(--surface-3); transform: translateY(-1px); box-shadow: var(--shadow-sm); }
216
+ .btn:active { transform: translateY(0); }
188
217
  .btn-danger { color: var(--red); }
189
- .btn-danger:hover { background: var(--red-dim); border-color: var(--red); }
218
+ .btn-danger:hover { background: var(--red-dim); border-color: var(--red); box-shadow: 0 0 12px rgba(248, 113, 113, 0.15); }
190
219
  .btn-primary { color: var(--accent); }
191
- .btn-primary:hover { background: var(--accent-dim); border-color: var(--accent); }
220
+ .btn-primary:hover { background: var(--accent-dim); border-color: var(--accent); box-shadow: 0 0 12px var(--accent-glow); }
192
221
 
193
222
  .mobile-toggle {
194
223
  display: none;
@@ -212,6 +241,7 @@
212
241
  width: var(--sidebar-w);
213
242
  min-width: var(--sidebar-w);
214
243
  background: var(--surface);
244
+ background-image: var(--gradient-surface);
215
245
  border-right: 1px solid var(--border);
216
246
  display: flex;
217
247
  flex-direction: column;
@@ -256,13 +286,16 @@
256
286
  /* ===== AGENT CARDS ===== */
257
287
  .agent-card {
258
288
  background: var(--surface-2);
289
+ background-image: var(--gradient-surface);
259
290
  border: 1px solid var(--border);
260
- border-radius: 8px;
291
+ border-radius: 10px;
261
292
  padding: 10px 12px;
262
293
  margin-bottom: 6px;
263
- transition: all 0.15s;
294
+ transition: all 0.25s ease;
264
295
  }
265
296
 
297
+ .agent-card:hover { border-color: var(--border-light); box-shadow: var(--shadow-sm); }
298
+
266
299
  .agent-card.sleeping {
267
300
  border-color: var(--orange);
268
301
  border-left: 3px solid var(--orange);
@@ -360,7 +393,7 @@
360
393
  display: inline-block;
361
394
  }
362
395
 
363
- .listen-dot.on { background: var(--green); box-shadow: 0 0 4px var(--green); }
396
+ .listen-dot.on { background: var(--green); box-shadow: 0 0 8px var(--green); }
364
397
  .listen-dot.off { background: var(--red); }
365
398
 
366
399
  @keyframes pulseAlert {
@@ -428,15 +461,16 @@
428
461
  background: var(--surface-2);
429
462
  color: var(--text);
430
463
  border: 1px solid var(--border);
431
- border-radius: 6px;
464
+ border-radius: 8px;
432
465
  padding: 7px 10px;
433
466
  font-size: 12px;
434
467
  cursor: pointer;
435
468
  outline: none;
436
469
  margin-bottom: 6px;
470
+ transition: all 0.2s ease;
437
471
  }
438
472
 
439
- .project-select:focus { border-color: var(--accent); }
473
+ .project-select:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-dim); }
440
474
 
441
475
  .project-actions {
442
476
  display: flex;
@@ -530,12 +564,12 @@
530
564
  display: flex;
531
565
  gap: 10px;
532
566
  padding: 10px 14px;
533
- border-radius: 8px;
534
- transition: background 0.15s;
567
+ border-radius: 10px;
568
+ transition: all 0.2s ease;
569
+ position: relative;
535
570
  }
536
571
 
537
- .message { position: relative; }
538
- .message:hover { background: var(--surface); }
572
+ .message:hover { background: var(--surface); box-shadow: var(--glow); }
539
573
 
540
574
  /* ===== REACTIONS ===== */
541
575
  .msg-actions {
@@ -570,12 +604,14 @@
570
604
  top: -4px;
571
605
  right: 56px;
572
606
  background: var(--surface-2);
573
- border: 1px solid var(--border);
574
- border-radius: 8px;
607
+ backdrop-filter: blur(12px);
608
+ -webkit-backdrop-filter: blur(12px);
609
+ border: 1px solid var(--border-light);
610
+ border-radius: 10px;
575
611
  padding: 4px;
576
612
  gap: 2px;
577
613
  z-index: 20;
578
- box-shadow: 0 4px 12px rgba(0,0,0,0.3);
614
+ box-shadow: var(--shadow-lg);
579
615
  }
580
616
 
581
617
  .react-picker.open { display: flex; }
@@ -701,9 +737,10 @@
701
737
 
702
738
  .badge {
703
739
  font-size: 9px;
704
- padding: 1px 5px;
705
- border-radius: 8px;
740
+ padding: 2px 6px;
741
+ border-radius: 6px;
706
742
  font-weight: 600;
743
+ letter-spacing: 0.03em;
707
744
  }
708
745
 
709
746
  .badge-ack { background: var(--green-dim); color: var(--green); }
@@ -737,16 +774,17 @@
737
774
  }
738
775
 
739
776
  .msg-content pre {
740
- background: var(--surface-2);
777
+ background: var(--bg);
741
778
  border: 1px solid var(--border);
742
- border-radius: 6px;
743
- padding: 12px 14px;
744
- margin: 6px 0;
779
+ border-radius: 8px;
780
+ padding: 14px 16px;
781
+ margin: 8px 0;
745
782
  overflow-x: auto;
746
783
  font-size: 12px;
747
- line-height: 1.5;
784
+ line-height: 1.6;
748
785
  font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', monospace;
749
786
  position: relative;
787
+ box-shadow: inset 0 1px 3px rgba(0,0,0,0.2);
750
788
  }
751
789
 
752
790
  .msg-content pre code {
@@ -812,11 +850,33 @@
812
850
 
813
851
  .msg-content a:hover { text-decoration: underline; }
814
852
 
853
+ /* ===== READ RECEIPTS ===== */
854
+ .read-receipts {
855
+ display: flex;
856
+ gap: 2px;
857
+ margin-top: 4px;
858
+ align-items: center;
859
+ }
860
+ .read-receipt-dot {
861
+ width: 14px;
862
+ height: 14px;
863
+ border-radius: 50%;
864
+ background: var(--surface-3);
865
+ display: flex;
866
+ align-items: center;
867
+ justify-content: center;
868
+ font-size: 8px;
869
+ font-weight: 700;
870
+ color: var(--text-dim);
871
+ cursor: default;
872
+ }
873
+
815
874
  /* ===== MESSAGE INPUT ===== */
816
875
  .msg-input-bar {
817
876
  border-top: 1px solid var(--border);
818
877
  background: var(--surface);
819
- padding: 12px 20px;
878
+ background-image: var(--gradient-surface);
879
+ padding: 14px 20px;
820
880
  display: flex;
821
881
  gap: 8px;
822
882
  align-items: flex-end;
@@ -840,14 +900,15 @@
840
900
  background: var(--surface-2);
841
901
  color: var(--text);
842
902
  border: 1px solid var(--border);
843
- border-radius: 6px;
903
+ border-radius: 8px;
844
904
  padding: 7px 10px;
845
905
  font-size: 12px;
846
906
  cursor: pointer;
847
907
  outline: none;
908
+ transition: all 0.2s ease;
848
909
  }
849
910
 
850
- .input-target select:focus { border-color: var(--accent); }
911
+ .input-target select:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-dim); }
851
912
 
852
913
  .input-msg {
853
914
  flex: 1;
@@ -867,7 +928,7 @@
867
928
  background: var(--surface-2);
868
929
  color: var(--text);
869
930
  border: 1px solid var(--border);
870
- border-radius: 6px;
931
+ border-radius: 8px;
871
932
  padding: 7px 10px;
872
933
  font-size: 12px;
873
934
  font-family: inherit;
@@ -875,26 +936,30 @@
875
936
  height: 34px;
876
937
  max-height: 80px;
877
938
  outline: none;
939
+ transition: all 0.2s ease;
878
940
  }
879
941
 
880
- .input-msg textarea:focus { border-color: var(--accent); }
942
+ .input-msg textarea:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-dim); }
881
943
 
882
944
  .send-btn {
883
- background: var(--accent);
945
+ background: var(--gradient-accent);
884
946
  color: #fff;
885
947
  border: none;
886
- border-radius: 6px;
887
- padding: 7px 16px;
948
+ border-radius: 8px;
949
+ padding: 7px 20px;
888
950
  font-size: 12px;
889
951
  font-weight: 600;
890
952
  cursor: pointer;
891
953
  white-space: nowrap;
892
- transition: opacity 0.15s;
954
+ transition: all 0.2s ease;
893
955
  height: 34px;
956
+ box-shadow: 0 2px 8px var(--accent-glow);
957
+ letter-spacing: 0.02em;
894
958
  }
895
959
 
896
- .send-btn:hover { opacity: 0.85; }
897
- .send-btn:disabled { opacity: 0.4; cursor: not-allowed; }
960
+ .send-btn:hover { transform: translateY(-1px); box-shadow: 0 4px 16px var(--accent-glow); }
961
+ .send-btn:active { transform: translateY(0); }
962
+ .send-btn:disabled { opacity: 0.4; cursor: not-allowed; transform: none; box-shadow: none; }
898
963
 
899
964
  /* ===== EMPTY STATE ===== */
900
965
  .empty-state {
@@ -912,25 +977,27 @@
912
977
  .empty-sub { font-size: 12px; color: var(--text-muted); }
913
978
 
914
979
  /* ===== MESSAGE FLASH ===== */
915
- .message-new { animation: flashIn 0.6s ease-out; }
980
+ .message-new { animation: flashIn 0.8s ease-out; }
916
981
 
917
982
  @keyframes flashIn {
918
- from { background: var(--accent-dim); }
919
- to { background: transparent; }
983
+ 0% { background: var(--accent-dim); box-shadow: inset 0 0 20px var(--accent-glow); }
984
+ 100% { background: transparent; box-shadow: none; }
920
985
  }
921
986
 
922
987
  /* ===== SCROLLBAR ===== */
923
- ::-webkit-scrollbar { width: 6px; }
988
+ ::-webkit-scrollbar { width: 5px; }
924
989
  ::-webkit-scrollbar-track { background: transparent; }
925
- ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
926
- ::-webkit-scrollbar-thumb:hover { background: var(--text-muted); }
990
+ ::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.08); border-radius: 4px; }
991
+ ::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.15); }
992
+ [data-theme="light"] ::-webkit-scrollbar-thumb { background: rgba(0,0,0,0.1); }
993
+ [data-theme="light"] ::-webkit-scrollbar-thumb:hover { background: rgba(0,0,0,0.2); }
927
994
 
928
995
  /* ===== SCROLL TO BOTTOM ===== */
929
996
  .scroll-bottom {
930
997
  position: absolute;
931
998
  bottom: 80px;
932
999
  right: 24px;
933
- background: var(--accent);
1000
+ background: var(--gradient-accent);
934
1001
  color: #fff;
935
1002
  border: none;
936
1003
  border-radius: 50%;
@@ -941,13 +1008,13 @@
941
1008
  justify-content: center;
942
1009
  cursor: pointer;
943
1010
  font-size: 16px;
944
- box-shadow: 0 2px 8px rgba(0,0,0,0.4);
1011
+ box-shadow: 0 4px 16px var(--accent-glow);
945
1012
  z-index: 10;
946
- transition: opacity 0.2s;
1013
+ transition: all 0.25s ease;
947
1014
  }
948
1015
 
949
1016
  .scroll-bottom.visible { display: flex; }
950
- .scroll-bottom:hover { opacity: 0.85; }
1017
+ .scroll-bottom:hover { transform: translateY(-2px); box-shadow: 0 6px 24px var(--accent-glow); }
951
1018
 
952
1019
  .scroll-bottom .new-count {
953
1020
  position: absolute;
@@ -981,16 +1048,43 @@
981
1048
  background: var(--surface-2);
982
1049
  color: var(--text);
983
1050
  border: 1px solid var(--border);
984
- border-radius: 6px;
985
- padding: 6px 10px;
1051
+ border-radius: 8px;
1052
+ padding: 7px 12px;
986
1053
  font-size: 12px;
987
1054
  font-family: inherit;
988
1055
  outline: none;
1056
+ transition: all 0.2s ease;
989
1057
  }
990
1058
 
991
- .search-input:focus { border-color: var(--accent); }
1059
+ .search-input:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-dim); }
992
1060
  .search-input::placeholder { color: var(--text-muted); }
993
1061
 
1062
+ .compact-toggle {
1063
+ background: var(--surface-2);
1064
+ border: 1px solid var(--border);
1065
+ border-radius: 6px;
1066
+ color: var(--text-dim);
1067
+ font-size: 11px;
1068
+ padding: 4px 8px;
1069
+ cursor: pointer;
1070
+ white-space: nowrap;
1071
+ transition: background 0.15s, color 0.15s;
1072
+ }
1073
+ .compact-toggle:hover { background: var(--surface-3); color: var(--text); }
1074
+ .compact-toggle.active { background: var(--accent-dim); color: var(--accent); border-color: var(--accent); }
1075
+
1076
+ /* Compact mode styles */
1077
+ .messages-area.compact-mode { gap: 0; padding: 8px 12px; }
1078
+ .messages-area.compact-mode .message { padding: 3px 8px; gap: 6px; }
1079
+ .messages-area.compact-mode .msg-avatar,
1080
+ .messages-area.compact-mode .msg-avatar-img { display: none; }
1081
+ .messages-area.compact-mode .msg-header { margin-bottom: 0; display: inline; }
1082
+ .messages-area.compact-mode .msg-body { display: inline; }
1083
+ .messages-area.compact-mode .msg-from { font-size: 12px; }
1084
+ .messages-area.compact-mode .msg-time { font-size: 9px; }
1085
+ .messages-area.compact-mode .msg-content { display: inline; font-size: 12px; }
1086
+ .messages-area.compact-mode .msg-content p { display: inline; margin: 0; }
1087
+
994
1088
  .search-count {
995
1089
  font-size: 11px;
996
1090
  color: var(--text-muted);
@@ -1076,6 +1170,7 @@
1076
1170
  gap: 16px;
1077
1171
  font-size: 10px;
1078
1172
  color: var(--text-muted);
1173
+ letter-spacing: 0.02em;
1079
1174
  }
1080
1175
 
1081
1176
  .app-footer a { color: var(--text-dim); text-decoration: none; }
@@ -1177,17 +1272,18 @@
1177
1272
  .view-tab {
1178
1273
  flex: 1;
1179
1274
  text-align: center;
1180
- padding: 8px;
1275
+ padding: 10px 8px;
1181
1276
  font-size: 12px;
1182
1277
  font-weight: 600;
1183
- color: var(--text-dim);
1278
+ color: var(--text-muted);
1184
1279
  cursor: pointer;
1185
1280
  border-bottom: 2px solid transparent;
1186
- transition: all 0.15s;
1281
+ transition: all 0.2s ease;
1282
+ letter-spacing: 0.02em;
1187
1283
  }
1188
1284
 
1189
- .view-tab:hover { color: var(--text); }
1190
- .view-tab.active { color: var(--accent); border-bottom-color: var(--accent); }
1285
+ .view-tab:hover { color: var(--text); background: rgba(255,255,255,0.02); }
1286
+ .view-tab.active { color: var(--accent); border-bottom-color: var(--accent); background: var(--accent-dim); }
1191
1287
 
1192
1288
  /* ===== LAN BADGE ===== */
1193
1289
  .lan-badge {
@@ -1231,13 +1327,14 @@
1231
1327
 
1232
1328
  .phone-modal {
1233
1329
  background: var(--surface);
1234
- border: 1px solid var(--border);
1235
- border-radius: 12px;
1330
+ background-image: var(--gradient-surface);
1331
+ border: 1px solid var(--border-light);
1332
+ border-radius: 16px;
1236
1333
  padding: 28px;
1237
1334
  width: 360px;
1238
1335
  max-width: 90vw;
1239
1336
  text-align: center;
1240
- box-shadow: 0 16px 48px rgba(0,0,0,0.5);
1337
+ box-shadow: var(--shadow-lg), 0 0 60px rgba(0,0,0,0.3);
1241
1338
  position: relative;
1242
1339
  }
1243
1340
 
@@ -1411,14 +1508,19 @@
1411
1508
 
1412
1509
  .task-card {
1413
1510
  background: var(--surface-2);
1511
+ background-image: var(--gradient-surface);
1414
1512
  border: 1px solid var(--border);
1415
- border-radius: 6px;
1513
+ border-radius: 8px;
1416
1514
  padding: 10px;
1417
1515
  margin-bottom: 6px;
1418
- transition: border-color 0.15s;
1516
+ transition: all 0.2s ease;
1419
1517
  }
1420
1518
 
1421
- .task-card:hover { border-color: var(--border-light); }
1519
+ .task-card:hover { border-color: var(--border-light); transform: translateY(-1px); box-shadow: var(--shadow-sm); }
1520
+ .task-card[draggable="true"] { cursor: grab; }
1521
+ .task-card[draggable="true"]:active { cursor: grabbing; }
1522
+ .task-card.dragging { opacity: 0.4; transform: scale(0.95); }
1523
+ .kanban-col.drag-over { background: var(--surface-2); border-radius: 8px; outline: 2px dashed var(--accent); outline-offset: -2px; transition: background 0.15s, outline 0.15s; }
1422
1524
 
1423
1525
  .task-title {
1424
1526
  font-size: 13px;
@@ -1945,11 +2047,14 @@
1945
2047
  position: fixed;
1946
2048
  z-index: 300;
1947
2049
  background: var(--surface);
1948
- border: 1px solid var(--border);
1949
- border-radius: 10px;
1950
- padding: 16px;
2050
+ background-image: var(--gradient-surface);
2051
+ border: 1px solid var(--border-light);
2052
+ border-radius: 14px;
2053
+ padding: 18px;
1951
2054
  width: 260px;
1952
- box-shadow: 0 8px 24px rgba(0,0,0,0.4);
2055
+ box-shadow: var(--shadow-lg), 0 0 40px rgba(0,0,0,0.2);
2056
+ backdrop-filter: blur(8px);
2057
+ -webkit-backdrop-filter: blur(8px);
1953
2058
  }
1954
2059
 
1955
2060
  .profile-popup.open { display: block; }
@@ -2201,6 +2306,122 @@
2201
2306
 
2202
2307
  .launch-area.visible { display: block; }
2203
2308
 
2309
+ /* ===== STATS VIEW ===== */
2310
+ .stats-area {
2311
+ flex: 1;
2312
+ overflow-y: auto;
2313
+ padding: 20px;
2314
+ display: none;
2315
+ }
2316
+ .stats-area.visible { display: block; }
2317
+
2318
+ .stats-grid {
2319
+ display: grid;
2320
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
2321
+ gap: 12px;
2322
+ margin-bottom: 20px;
2323
+ }
2324
+
2325
+ .stat-card {
2326
+ background: var(--surface);
2327
+ border: 1px solid var(--border);
2328
+ border-radius: 8px;
2329
+ padding: 14px;
2330
+ }
2331
+ .stat-card-label {
2332
+ font-size: 10px;
2333
+ text-transform: uppercase;
2334
+ letter-spacing: 0.5px;
2335
+ color: var(--text-muted);
2336
+ margin-bottom: 4px;
2337
+ }
2338
+ .stat-card-value {
2339
+ font-size: 24px;
2340
+ font-weight: 700;
2341
+ color: var(--text);
2342
+ }
2343
+ .stat-card-sub {
2344
+ font-size: 11px;
2345
+ color: var(--text-dim);
2346
+ margin-top: 2px;
2347
+ }
2348
+
2349
+ .stats-section {
2350
+ background: var(--surface);
2351
+ border: 1px solid var(--border);
2352
+ border-radius: 8px;
2353
+ padding: 16px;
2354
+ margin-bottom: 16px;
2355
+ }
2356
+ .stats-section-title {
2357
+ font-size: 12px;
2358
+ font-weight: 700;
2359
+ text-transform: uppercase;
2360
+ letter-spacing: 0.5px;
2361
+ color: var(--text-dim);
2362
+ margin-bottom: 12px;
2363
+ }
2364
+
2365
+ .stats-bar-row {
2366
+ display: flex;
2367
+ align-items: center;
2368
+ gap: 8px;
2369
+ margin-bottom: 6px;
2370
+ }
2371
+ .stats-bar-label {
2372
+ font-size: 12px;
2373
+ color: var(--text);
2374
+ min-width: 80px;
2375
+ text-align: right;
2376
+ }
2377
+ .stats-bar-track {
2378
+ flex: 1;
2379
+ height: 18px;
2380
+ background: var(--surface-2);
2381
+ border-radius: 4px;
2382
+ overflow: hidden;
2383
+ }
2384
+ .stats-bar-fill {
2385
+ height: 100%;
2386
+ border-radius: 4px;
2387
+ transition: width 0.3s;
2388
+ display: flex;
2389
+ align-items: center;
2390
+ padding-left: 6px;
2391
+ font-size: 10px;
2392
+ font-weight: 600;
2393
+ color: #fff;
2394
+ white-space: nowrap;
2395
+ }
2396
+
2397
+ .stats-hour-chart {
2398
+ display: flex;
2399
+ align-items: flex-end;
2400
+ gap: 2px;
2401
+ height: 80px;
2402
+ }
2403
+ .stats-hour-bar {
2404
+ flex: 1;
2405
+ background: var(--accent);
2406
+ border-radius: 2px 2px 0 0;
2407
+ min-width: 4px;
2408
+ opacity: 0.7;
2409
+ transition: opacity 0.15s;
2410
+ position: relative;
2411
+ }
2412
+ .stats-hour-bar:hover { opacity: 1; }
2413
+ .stats-hour-labels {
2414
+ display: flex;
2415
+ gap: 2px;
2416
+ margin-top: 4px;
2417
+ }
2418
+ .stats-hour-labels span {
2419
+ flex: 1;
2420
+ text-align: center;
2421
+ font-size: 8px;
2422
+ color: var(--text-muted);
2423
+ }
2424
+
2204
2425
  .launch-panel {
2205
2426
  max-width: 560px;
2206
2427
  margin: 0 auto;
@@ -2459,6 +2680,7 @@
2459
2680
  <div id="export-menu" style="display:none;position:absolute;right:0;top:100%;margin-top:4px;background:var(--surface-2);border:1px solid var(--border);border-radius:6px;overflow:hidden;z-index:200;min-width:160px">
2460
2681
  <div style="padding:7px 12px;font-size:12px;cursor:pointer;transition:background 0.1s" onmouseover="this.style.background='var(--surface-3)'" onmouseout="this.style.background=''" onclick="exportShareableHTML();toggleExportMenu()">HTML (shareable)</div>
2461
2682
  <div style="padding:7px 12px;font-size:12px;cursor:pointer;transition:background 0.1s" onmouseover="this.style.background='var(--surface-3)'" onmouseout="this.style.background=''" onclick="exportConversation();toggleExportMenu()">Markdown (.md)</div>
2683
+ <div style="padding:7px 12px;font-size:12px;cursor:pointer;transition:background 0.1s" onmouseover="this.style.background='var(--surface-3)'" onmouseout="this.style.background=''" onclick="exportJSON();toggleExportMenu()">JSON (.json)</div>
2462
2684
  </div>
2463
2685
  </div>
2464
2686
  <button class="btn btn-danger" onclick="doReset()">Reset</button>
@@ -2479,6 +2701,21 @@
2479
2701
  </div>
2480
2702
  </div>
2481
2703
 
2704
+ <!-- v3.4: EDIT MESSAGE MODAL -->
2705
+ <div class="phone-modal-overlay" id="edit-modal-overlay" onclick="if(event.target===this)closeEditModal()">
2706
+ <div class="phone-modal" style="width:500px;text-align:left">
2707
+ <button class="phone-modal-close" onclick="closeEditModal()">&times;</button>
2708
+ <h3>Edit Message</h3>
2709
+ <input type="hidden" id="edit-msg-id">
2710
+ <textarea id="edit-msg-content" style="width:100%;min-height:120px;background:var(--surface-2);color:var(--text);border:1px solid var(--border);border-radius:8px;padding:12px;font-size:13px;font-family:inherit;resize:vertical;outline:none;margin:12px 0 8px"></textarea>
2711
+ <div id="edit-history" style="max-height:150px;overflow-y:auto;margin-bottom:12px"></div>
2712
+ <div style="display:flex;gap:8px;justify-content:flex-end">
2713
+ <button class="btn" onclick="closeEditModal()">Cancel</button>
2714
+ <button class="btn btn-primary" onclick="saveEditMessage()" style="background:var(--accent);color:#fff;border-color:var(--accent)">Save Edit</button>
2715
+ </div>
2716
+ </div>
2717
+ </div>
2718
+
2482
2719
  <!-- APP LAYOUT -->
2483
2720
  <div class="app">
2484
2721
  <div class="sidebar-overlay" id="sidebar-overlay" onclick="toggleSidebar()"></div>
@@ -2566,11 +2803,13 @@
2566
2803
  <div class="view-tab" id="tab-workspaces" onclick="switchView('workspaces')">Workspaces</div>
2567
2804
  <div class="view-tab" id="tab-workflows" onclick="switchView('workflows')">Workflows</div>
2568
2805
  <div class="view-tab" id="tab-launch" onclick="switchView('launch')">Launch</div>
2806
+ <div class="view-tab" id="tab-stats" onclick="switchView('stats')">Stats</div>
2569
2807
  </div>
2570
2808
  <div class="branch-tabs" id="branch-tabs"></div>
2571
2809
  <div class="search-bar" id="search-bar">
2572
2810
  <input class="search-input" id="search-input" placeholder="Search messages... ( / )" oninput="onSearch()">
2573
2811
  <span class="search-count" id="search-count"></span>
2812
+ <button class="compact-toggle" id="compact-toggle" onclick="toggleCompactMode()" title="Toggle compact view">Compact</button>
2574
2813
  </div>
2575
2814
  <div class="pinned-section" id="pinned-section">
2576
2815
  <div class="pinned-header" onclick="togglePinnedSection()"><span>Pinned Messages</span><span class="pinned-toggle" id="pinned-toggle">Hide</span></div>
@@ -2581,6 +2820,7 @@
2581
2820
  <div class="workspaces-area" id="workspaces-area"></div>
2582
2821
  <div class="workflows-area" id="workflows-area"></div>
2583
2822
  <div class="launch-area" id="launch-area"></div>
2823
+ <div class="stats-area" id="stats-area"></div>
2584
2824
  <button class="scroll-bottom" id="scroll-bottom" onclick="scrollToBottom()">&#x2193;<span class="new-count" id="new-msg-count" style="display:none">0</span></button>
2585
2825
  <div class="typing-bar" id="typing-bar"></div>
2586
2826
 
@@ -2602,7 +2842,7 @@
2602
2842
  </div>
2603
2843
  </div>
2604
2844
  <div class="app-footer">
2605
- <span>Let Them Talk v3.3.3</span>
2845
+ <span>Let Them Talk v3.4.1</span>
2606
2846
  </div>
2607
2847
  <div class="profile-popup" id="profile-popup" onclick="event.stopPropagation()">
2608
2848
  <div class="profile-popup-header">
@@ -3205,6 +3445,7 @@ function renderMessages(messages) {
3205
3445
  var badges = '';
3206
3446
  if (m.acked) badges += '<span class="badge badge-ack">ACK</span>';
3207
3447
  if (m.thread_id) badges += '<span class="badge badge-thread">Thread</span>';
3448
+ if (m.edited) badges += '<span class="badge" style="background:var(--orange-dim);color:var(--orange)" title="Edited ' + (m.edited_at ? new Date(m.edited_at).toLocaleString() : '') + '">edited</span>';
3208
3449
 
3209
3450
  var msgAvatarHtml = getMsgAvatar(m.from, color);
3210
3451
 
@@ -3228,6 +3469,7 @@ function renderMessages(messages) {
3228
3469
  '</div>' +
3229
3470
  '<div class="msg-content">' + renderMarkdown(m.content) + '</div>' +
3230
3471
  buildReactionsHtml(m.id) +
3472
+ buildReadReceipts(m.id) +
3231
3473
  '</div></div>';
3232
3474
  lastFrom = m.from;
3233
3475
  lastTo = m.to;
@@ -3249,6 +3491,7 @@ function renderMessages(messages) {
3249
3491
  '<div class="file-meta"><span class="file-icon">&#x1f4c4;</span>' + escapeHtml(fileName) + '<span class="file-size">' + fileSize + '</span></div>' +
3250
3492
  '<div class="msg-content">' + renderMarkdown(m.content) + '</div>' +
3251
3493
  buildReactionsHtml(m.id) +
3494
+ buildReadReceipts(m.id) +
3252
3495
  '</div></div>';
3253
3496
  lastFrom = m.from;
3254
3497
  lastTo = m.to;
@@ -3266,6 +3509,7 @@ function renderMessages(messages) {
3266
3509
  '</div>' +
3267
3510
  '<div class="msg-content">' + renderMarkdown(m.content) + '</div>' +
3268
3511
  buildReactionsHtml(m.id) +
3512
+ buildReadReceipts(m.id) +
3269
3513
  '</div></div>';
3270
3514
  lastFrom = m.from;
3271
3515
  lastTo = m.to;
@@ -3330,6 +3574,28 @@ function scrollToBottom() {
3330
3574
  document.getElementById('new-msg-count').style.display = 'none';
3331
3575
  }
3332
3576
 
3577
+ // ==================== COMPACT MODE ====================
3578
+
3579
+ var compactMode = localStorage.getItem('compactMode') === 'true';
3580
+
3581
+ function initCompactMode() {
3582
+ var area = document.getElementById('messages');
3583
+ var btn = document.getElementById('compact-toggle');
3584
+ if (compactMode) {
3585
+ area.classList.add('compact-mode');
3586
+ btn.classList.add('active');
3587
+ }
3588
+ }
3589
+
3590
+ function toggleCompactMode() {
3591
+ compactMode = !compactMode;
3592
+ localStorage.setItem('compactMode', compactMode);
3593
+ var area = document.getElementById('messages');
3594
+ var btn = document.getElementById('compact-toggle');
3595
+ area.classList.toggle('compact-mode', compactMode);
3596
+ btn.classList.toggle('active', compactMode);
3597
+ }
3598
+
3333
3599
  // ==================== SEARCH ====================
3334
3600
 
3335
3601
  var searchQuery = '';
@@ -3340,6 +3606,33 @@ function onSearch() {
3340
3606
  renderMessages(cachedHistory);
3341
3607
  }
3342
3608
 
3609
+ // ==================== READ RECEIPTS ====================
3610
+
3611
+ var cachedReadReceipts = {};
3612
+
3613
+ function fetchReadReceipts() {
3614
+ var pq = activeProject ? '?project=' + encodeURIComponent(activeProject) : '';
3615
+ fetch('/api/read-receipts' + pq).then(function(r) { return r.json(); }).then(function(data) {
3616
+ cachedReadReceipts = data || {};
3617
+ }).catch(function() {});
3618
+ }
3619
+
3620
+ function buildReadReceipts(msgId) {
3621
+ var receipts = cachedReadReceipts[msgId];
3622
+ if (!receipts) return '';
3623
+ var agents = Object.keys(receipts);
3624
+ if (!agents.length) return '';
3625
+ var html = '<div class="read-receipts">';
3626
+ for (var i = 0; i < agents.length; i++) {
3627
+ var agent = agents[i];
3628
+ var time = receipts[agent] ? new Date(receipts[agent]).toLocaleTimeString() : '';
3629
+ var color = getColor(agent);
3630
+ html += '<div class="read-receipt-dot" style="background:' + color + '" title="Read by ' + escapeHtml(agent) + (time ? ' at ' + time : '') + '">' + initial(agent) + '</div>';
3631
+ }
3632
+ html += '</div>';
3633
+ return html;
3634
+ }
3635
+
3343
3636
  // ==================== REACTIONS ====================
3344
3637
 
3345
3638
  var REACTION_EMOJIS = ['\ud83d\udc4d', '\u2705', '\u2764\ufe0f', '\ud83e\udd14', '\ud83d\udd25'];
@@ -3425,16 +3718,106 @@ function buildMsgActions(msgId) {
3425
3718
  var starClass = isBookmarked ? ' active' : '';
3426
3719
  var pinClass = isPinned ? ' active' : '';
3427
3720
 
3721
+ var msg = cachedHistory.find(function(m) { return m.id === msgId; });
3722
+ var isDeletable = msg && (msg.from === 'dashboard' || msg.from === 'Dashboard' || msg.from === 'system' || msg.from === '__system__');
3723
+ var deleteBtn = isDeletable ? '<button class="msg-action-btn" onclick="deleteMessage(\'' + msgId + '\')" title="Delete" style="color:var(--red,#e74c3c)">&#x1f5d1;&#xFE0F;</button>' : '';
3724
+
3428
3725
  return '<div class="msg-actions">' +
3429
3726
  '<button class="msg-action-btn" onclick="toggleReactPicker(\'' + msgId + '\')" title="React">&#x1f600;</button>' +
3430
3727
  '<button class="msg-action-btn' + pinClass + '" onclick="togglePin(\'' + msgId + '\')" title="Pin">\ud83d\udccc</button>' +
3431
3728
  '<button class="msg-action-btn' + starClass + '" onclick="toggleBookmark(\'' + msgId + '\')" title="Bookmark">' + starChar + '</button>' +
3729
+ '<button class="msg-action-btn" onclick="copyMessage(\'' + msgId + '\', this)" title="Copy">&#x1f4cb;</button>' +
3730
+ '<button class="msg-action-btn" onclick="openEditMessage(\'' + msgId + '\')" title="Edit">&#x270F;&#xFE0F;</button>' +
3731
+ deleteBtn +
3432
3732
  '<div class="react-picker" id="react-' + msgId + '">' +
3433
3733
  REACTION_EMOJIS.map(function(e) { return '<button class="react-emoji" onclick="addReaction(\'' + msgId + '\',\'' + e + '\')">' + e + '</button>'; }).join('') +
3434
3734
  '</div>' +
3435
3735
  '</div>';
3436
3736
  }
3437
3737
 
3738
+ function copyMessage(msgId, btn) {
3739
+ var msg = cachedHistory.find(function(m) { return m.id === msgId; });
3740
+ if (!msg) return;
3741
+ navigator.clipboard.writeText(msg.content).then(function() {
3742
+ var orig = btn.innerHTML;
3743
+ btn.innerHTML = '&#x2705;';
3744
+ btn.title = 'Copied!';
3745
+ setTimeout(function() { btn.innerHTML = orig; btn.title = 'Copy'; }, 1500);
3746
+ });
3747
+ }
3748
+
3749
+ function deleteMessage(msgId) {
3750
+ var msg = cachedHistory.find(function(m) { return m.id === msgId; });
3751
+ if (!msg) return;
3752
+ var preview = msg.content.substring(0, 60) + (msg.content.length > 60 ? '...' : '');
3753
+ if (!confirm('Delete this message from ' + msg.from + '?\n\n"' + preview + '"')) return;
3754
+ var pq = activeProject ? '?project=' + encodeURIComponent(activeProject) : '';
3755
+ fetch('/api/message' + pq, {
3756
+ method: 'DELETE',
3757
+ headers: { 'Content-Type': 'application/json' },
3758
+ body: JSON.stringify({ id: msgId })
3759
+ }).then(function(r) { return r.json(); }).then(function(data) {
3760
+ if (data.error) {
3761
+ console.error('Delete failed: ' + data.error);
3762
+ } else {
3763
+ cachedHistory = cachedHistory.filter(function(m) { return m.id !== msgId; });
3764
+ lastMessageCount = 0;
3765
+ renderMessages(cachedHistory);
3766
+ }
3767
+ }).catch(function(err) {
3768
+ console.error('Delete failed: ' + err.message);
3769
+ });
3770
+ }
3771
+
3772
+ // ==================== v3.4: MESSAGE EDIT ====================
3773
+
3774
+ function openEditMessage(msgId) {
3775
+ var msg = cachedHistory.find(function(m) { return m.id === msgId; });
3776
+ if (!msg) return;
3777
+ var overlay = document.getElementById('edit-modal-overlay');
3778
+ document.getElementById('edit-msg-id').value = msgId;
3779
+ document.getElementById('edit-msg-content').value = msg.content;
3780
+ var historyEl = document.getElementById('edit-history');
3781
+ if (msg.edit_history && msg.edit_history.length > 0) {
3782
+ historyEl.innerHTML = '<div style="font-size:10px;color:var(--text-muted);margin-bottom:4px">Edit history (' + msg.edit_history.length + ' edits):</div>' +
3783
+ msg.edit_history.map(function(h) {
3784
+ return '<div style="font-size:11px;color:var(--text-dim);padding:4px 8px;background:var(--surface-2);border-radius:4px;margin-bottom:2px">' +
3785
+ '<span style="color:var(--text-muted)">' + new Date(h.edited_at).toLocaleTimeString() + '</span> ' +
3786
+ escapeHtml(h.content.substring(0, 100)) + (h.content.length > 100 ? '...' : '') + '</div>';
3787
+ }).join('');
3788
+ historyEl.style.display = 'block';
3789
+ } else {
3790
+ historyEl.style.display = 'none';
3791
+ }
3792
+ overlay.classList.add('open');
3793
+ document.getElementById('edit-msg-content').focus();
3794
+ }
3795
+
3796
+ function closeEditModal() {
3797
+ document.getElementById('edit-modal-overlay').classList.remove('open');
3798
+ }
3799
+
3800
+ function saveEditMessage() {
3801
+ var msgId = document.getElementById('edit-msg-id').value;
3802
+ var content = document.getElementById('edit-msg-content').value.trim();
3803
+ if (!content) return;
3804
+ var pq = activeProject ? '?project=' + encodeURIComponent(activeProject) : '';
3805
+ fetch('/api/message' + pq, {
3806
+ method: 'PUT',
3807
+ headers: { 'Content-Type': 'application/json' },
3808
+ body: JSON.stringify({ id: msgId, content: content })
3809
+ }).then(function(r) { return r.json(); }).then(function(data) {
3810
+ if (data.success) {
3811
+ closeEditModal();
3812
+ poll();
3813
+ } else {
3814
+ alert('Edit failed: ' + (data.error || 'Unknown error'));
3815
+ }
3816
+ }).catch(function(err) {
3817
+ alert('Edit failed: ' + err.message);
3818
+ });
3819
+ }
3820
+
3438
3821
  function buildReactionsHtml(msgId) {
3439
3822
  if (!reactions[msgId]) return '';
3440
3823
  var emojis = Object.keys(reactions[msgId]);
@@ -3512,6 +3895,8 @@ document.addEventListener('keydown', function(e) {
3512
3895
  if (e.key === '4') { switchView('workflows'); return; }
3513
3896
  // 5 — Launch tab
3514
3897
  if (e.key === '5') { switchView('launch'); return; }
3898
+ // 6 — Stats tab
3899
+ if (e.key === '6') { switchView('stats'); return; }
3515
3900
  });
3516
3901
 
3517
3902
  // ==================== RELATIVE TIMESTAMPS ====================
@@ -3531,7 +3916,7 @@ function relativeTime(ts) {
3531
3916
  // ==================== COPY TO CLIPBOARD ====================
3532
3917
 
3533
3918
  function copyText(el) {
3534
- var text = el.getAttribute('data-text').replace(/&quot;/g, '"');
3919
+ var text = el.getAttribute('data-text').replace(/&quot;/g, '"').replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&amp;/g, '&');
3535
3920
  navigator.clipboard.writeText(text).then(function() {
3536
3921
  el.classList.add('copied');
3537
3922
  el.querySelector('.copy-hint').textContent = 'copied!';
@@ -3740,6 +4125,21 @@ function exportConversation() {
3740
4125
  URL.revokeObjectURL(url);
3741
4126
  }
3742
4127
 
4128
+ function exportJSON() {
4129
+ if (!cachedHistory.length) return;
4130
+ var data = cachedHistory.map(function(m) {
4131
+ return { id: m.id, from: m.from, to: m.to, content: m.content, timestamp: m.timestamp, thread_id: m.thread_id || null };
4132
+ });
4133
+ var json = JSON.stringify(data, null, 2);
4134
+ var blob = new Blob([json], { type: 'application/json' });
4135
+ var url = URL.createObjectURL(blob);
4136
+ var a = document.createElement('a');
4137
+ a.href = url;
4138
+ a.download = 'conversation-' + new Date().toISOString().slice(0, 10) + '.json';
4139
+ a.click();
4140
+ URL.revokeObjectURL(url);
4141
+ }
4142
+
3743
4143
  // ==================== VIEW SWITCHING ====================
3744
4144
 
3745
4145
  var activeView = 'messages';
@@ -3751,16 +4151,19 @@ function switchView(view) {
3751
4151
  document.getElementById('tab-workspaces').classList.toggle('active', view === 'workspaces');
3752
4152
  document.getElementById('tab-workflows').classList.toggle('active', view === 'workflows');
3753
4153
  document.getElementById('tab-launch').classList.toggle('active', view === 'launch');
4154
+ document.getElementById('tab-stats').classList.toggle('active', view === 'stats');
3754
4155
  document.getElementById('messages').style.display = view === 'messages' ? 'flex' : 'none';
3755
4156
  document.getElementById('tasks-area').classList.toggle('visible', view === 'tasks');
3756
4157
  document.getElementById('workspaces-area').classList.toggle('visible', view === 'workspaces');
3757
4158
  document.getElementById('workflows-area').classList.toggle('visible', view === 'workflows');
3758
4159
  document.getElementById('launch-area').classList.toggle('visible', view === 'launch');
4160
+ document.getElementById('stats-area').classList.toggle('visible', view === 'stats');
3759
4161
  document.getElementById('search-bar').style.display = view === 'messages' ? 'flex' : 'none';
3760
4162
  if (view === 'tasks') fetchTasks();
3761
4163
  if (view === 'workspaces') fetchWorkspaces();
3762
4164
  if (view === 'workflows') fetchWorkflows();
3763
4165
  if (view === 'launch') renderLaunchPanel();
4166
+ if (view === 'stats') fetchStats();
3764
4167
  // Auto-close sidebar on mobile after view switch
3765
4168
  if (isMobile) closeSidebar();
3766
4169
  }
@@ -3824,7 +4227,7 @@ function renderTasks() {
3824
4227
  for (var c = 0; c < cols.length; c++) {
3825
4228
  var col = cols[c];
3826
4229
  var tasks = groups[col.key] || [];
3827
- html += '<div class="kanban-col">';
4230
+ html += '<div class="kanban-col" data-status="' + col.key + '" ondragover="onKanbanDragOver(event)" ondragleave="onKanbanDragLeave(event)" ondrop="onKanbanDrop(event)">';
3828
4231
  html += '<div class="kanban-title ' + col.key + '">' + col.label + '<span class="kanban-count">' + tasks.length + '</span></div>';
3829
4232
  for (var j = 0; j < tasks.length; j++) {
3830
4233
  html += buildTaskCard(tasks[j]);
@@ -3850,7 +4253,7 @@ function buildTaskCard(t) {
3850
4253
  '<option value="blocked"' + (t.status === 'blocked' ? ' selected' : '') + '>Blocked</option>' +
3851
4254
  '</select>';
3852
4255
 
3853
- return '<div class="task-card">' +
4256
+ return '<div class="task-card" draggable="true" data-task-id="' + t.id + '" ondragstart="onTaskDragStart(event)" ondragend="onTaskDragEnd(event)">' +
3854
4257
  '<div class="task-title">' + escapeHtml(t.title || 'Untitled') + '</div>' +
3855
4258
  (t.description ? '<div class="task-desc">' + escapeHtml(t.description) + '</div>' : '') +
3856
4259
  '<div class="task-footer">' +
@@ -3871,6 +4274,135 @@ function updateTaskStatus(taskId, newStatus) {
3871
4274
  }).catch(function(e) { console.error('Task update failed:', e); });
3872
4275
  }
3873
4276
 
4277
+ // ==================== KANBAN DRAG-AND-DROP ====================
4278
+
4279
+ var draggedTaskId = null;
4280
+
4281
+ function onTaskDragStart(e) {
4282
+ draggedTaskId = e.target.getAttribute('data-task-id');
4283
+ e.dataTransfer.effectAllowed = 'move';
4284
+ e.dataTransfer.setData('text/plain', draggedTaskId);
4285
+ e.target.classList.add('dragging');
4286
+ }
4287
+
4288
+ function onTaskDragEnd(e) {
4289
+ e.target.classList.remove('dragging');
4290
+ draggedTaskId = null;
4291
+ var cols = document.querySelectorAll('.kanban-col');
4292
+ for (var i = 0; i < cols.length; i++) cols[i].classList.remove('drag-over');
4293
+ }
4294
+
4295
+ function onKanbanDragOver(e) {
4296
+ e.preventDefault();
4297
+ e.dataTransfer.dropEffect = 'move';
4298
+ var col = e.currentTarget;
4299
+ if (!col.classList.contains('drag-over')) col.classList.add('drag-over');
4300
+ }
4301
+
4302
+ function onKanbanDragLeave(e) {
4303
+ if (!e.currentTarget.contains(e.relatedTarget)) {
4304
+ e.currentTarget.classList.remove('drag-over');
4305
+ }
4306
+ }
4307
+
4308
+ function onKanbanDrop(e) {
4309
+ e.preventDefault();
4310
+ var col = e.currentTarget;
4311
+ col.classList.remove('drag-over');
4312
+ var taskId = e.dataTransfer.getData('text/plain');
4313
+ var newStatus = col.getAttribute('data-status');
4314
+ if (taskId && newStatus) {
4315
+ updateTaskStatus(taskId, newStatus);
4316
+ }
4317
+ }
4318
+
4319
+ // ==================== STATS VIEW ====================
4320
+
4321
+ var AGENT_COLORS = ['#58a6ff', '#f78166', '#7ee787', '#d2a8ff', '#ffa657', '#ff7b72', '#79c0ff', '#56d364'];
4322
+
4323
+ function fetchStats() {
4324
+ var pq = activeProject ? '?project=' + encodeURIComponent(activeProject) : '';
4325
+ fetch('/api/stats' + pq).then(function(r) { return r.json(); }).then(function(data) {
4326
+ renderStats(data);
4327
+ }).catch(function(e) { console.error('Stats fetch failed:', e); });
4328
+ }
4329
+
4330
+ function renderStats(data) {
4331
+ var el = document.getElementById('stats-area');
4332
+ if (!data || !data.total_messages) {
4333
+ el.innerHTML = '<div class="tasks-empty">No messages yet. Stats will appear once agents start talking.</div>';
4334
+ return;
4335
+ }
4336
+
4337
+ var agentNames = Object.keys(data.agents);
4338
+ var maxMessages = 0;
4339
+ for (var i = 0; i < agentNames.length; i++) {
4340
+ if (data.agents[agentNames[i]].messages > maxMessages) maxMessages = data.agents[agentNames[i]].messages;
4341
+ }
4342
+
4343
+ // Overview cards
4344
+ var html = '<div class="stats-grid">';
4345
+ html += '<div class="stat-card"><div class="stat-card-label">Total Messages</div><div class="stat-card-value">' + data.total_messages + '</div></div>';
4346
+ html += '<div class="stat-card"><div class="stat-card-label">Busiest Agent</div><div class="stat-card-value" style="font-size:18px">' + escapeHtml(data.busiest_agent || '-') + '</div></div>';
4347
+ html += '<div class="stat-card"><div class="stat-card-label">Velocity</div><div class="stat-card-value">' + data.velocity_per_min + '</div><div class="stat-card-sub">msgs/min (last 10m)</div></div>';
4348
+ html += '<div class="stat-card"><div class="stat-card-label">Active Agents</div><div class="stat-card-value">' + agentNames.length + '</div></div>';
4349
+ html += '</div>';
4350
+
4351
+ // Per-agent message bars
4352
+ html += '<div class="stats-section"><div class="stats-section-title">Messages per Agent</div>';
4353
+ for (var j = 0; j < agentNames.length; j++) {
4354
+ var name = agentNames[j];
4355
+ var agent = data.agents[name];
4356
+ var pct = maxMessages ? Math.round((agent.messages / maxMessages) * 100) : 0;
4357
+ var color = AGENT_COLORS[j % AGENT_COLORS.length];
4358
+ html += '<div class="stats-bar-row">';
4359
+ html += '<div class="stats-bar-label">' + escapeHtml(name) + '</div>';
4360
+ html += '<div class="stats-bar-track"><div class="stats-bar-fill" style="width:' + pct + '%;background:' + color + '">' + agent.messages + '</div></div>';
4361
+ html += '</div>';
4362
+ }
4363
+ html += '</div>';
4364
+
4365
+ // Per-agent response times
4366
+ var hasResponseTimes = false;
4367
+ for (var k = 0; k < agentNames.length; k++) {
4368
+ if (data.agents[agentNames[k]].avg_response_ms) { hasResponseTimes = true; break; }
4369
+ }
4370
+ if (hasResponseTimes) {
4371
+ html += '<div class="stats-section"><div class="stats-section-title">Avg Response Time</div>';
4372
+ for (var l = 0; l < agentNames.length; l++) {
4373
+ var aName = agentNames[l];
4374
+ var aData = data.agents[aName];
4375
+ if (!aData.avg_response_ms) continue;
4376
+ var secs = (aData.avg_response_ms / 1000).toFixed(1);
4377
+ var maxBar = 60000;
4378
+ var rtPct = Math.min(Math.round((aData.avg_response_ms / maxBar) * 100), 100);
4379
+ var rtColor = AGENT_COLORS[l % AGENT_COLORS.length];
4380
+ html += '<div class="stats-bar-row">';
4381
+ html += '<div class="stats-bar-label">' + escapeHtml(aName) + '</div>';
4382
+ html += '<div class="stats-bar-track"><div class="stats-bar-fill" style="width:' + rtPct + '%;background:' + rtColor + '">' + secs + 's</div></div>';
4383
+ html += '</div>';
4384
+ }
4385
+ html += '</div>';
4386
+ }
4387
+
4388
+ // Hourly distribution chart
4389
+ var maxHour = Math.max.apply(null, data.hour_distribution);
4390
+ html += '<div class="stats-section"><div class="stats-section-title">Activity by Hour</div>';
4391
+ html += '<div class="stats-hour-chart">';
4392
+ for (var h = 0; h < 24; h++) {
4393
+ var hPct = maxHour ? Math.round((data.hour_distribution[h] / maxHour) * 100) : 0;
4394
+ html += '<div class="stats-hour-bar" style="height:' + Math.max(hPct, 2) + '%" title="' + h + ':00 — ' + data.hour_distribution[h] + ' msgs"></div>';
4395
+ }
4396
+ html += '</div>';
4397
+ html += '<div class="stats-hour-labels">';
4398
+ for (var hh = 0; hh < 24; hh++) {
4399
+ html += '<span>' + (hh % 6 === 0 ? hh + 'h' : '') + '</span>';
4400
+ }
4401
+ html += '</div></div>';
4402
+
4403
+ el.innerHTML = html;
4404
+ }
4405
+
3874
4406
  // ==================== EXPORT MENU ====================
3875
4407
 
3876
4408
  function toggleExportMenu() {
@@ -4292,6 +4824,8 @@ function poll() {
4292
4824
  if (activeView === 'tasks') fetchTasks();
4293
4825
  if (activeView === 'workspaces') fetchWorkspaces();
4294
4826
  if (activeView === 'workflows') fetchWorkflows();
4827
+ if (activeView === 'stats') fetchStats();
4828
+ fetchReadReceipts();
4295
4829
  }).catch(function(e) {
4296
4830
  console.error('Poll failed:', e);
4297
4831
  document.getElementById('conn-detail').textContent = ' ERR: ' + e.message;
@@ -4300,7 +4834,7 @@ function poll() {
4300
4834
 
4301
4835
  function doReset() {
4302
4836
  if (!confirm('Clear all messages, agents, and history?')) return;
4303
- fetch('/api/reset', { method: 'POST' }).then(function() {
4837
+ fetch('/api/reset' + projectParam(), { method: 'POST' }).then(function() {
4304
4838
  lastMessageCount = 0;
4305
4839
  activeThread = null;
4306
4840
  cachedHistory = [];
@@ -4436,7 +4970,7 @@ function discoverProjects() {
4436
4970
  resultsEl.style.display = 'block';
4437
4971
  resultsEl.innerHTML = '<div style="font-size:11px;color:var(--text-muted);padding:4px;">Scanning...</div>';
4438
4972
 
4439
- fetch('/api/discover').then(function(r) { return r.json(); }).then(function(found) {
4973
+ fetch('/api/discover', { method: 'POST' }).then(function(r) { return r.json(); }).then(function(found) {
4440
4974
  if (!found.length) {
4441
4975
  resultsEl.innerHTML = '<div style="font-size:11px;color:var(--text-muted);padding:4px;">No new projects found (all discovered projects already added)</div>';
4442
4976
  setTimeout(function() { resultsEl.style.display = 'none'; }, 3000);
@@ -4862,10 +5396,6 @@ function renderLaunchPanel() {
4862
5396
  templateOpts += '<option value="' + escapeHtml(templates[i].name) + '">' + escapeHtml(templates[i].name) + ' — ' + escapeHtml(templates[i].description) + '</option>';
4863
5397
  }
4864
5398
 
4865
- var activeProject = '';
4866
- var projSelect = document.getElementById('project-selector');
4867
- if (projSelect && projSelect.value) activeProject = projSelect.value;
4868
-
4869
5399
  el.innerHTML =
4870
5400
  '<div class="launch-panel">' +
4871
5401
  '<h3>Launch Agent Terminal</h3>' +
@@ -4899,11 +5429,16 @@ function renderLaunchPanel() {
4899
5429
  '</div>' +
4900
5430
  '<div class="launch-result" id="launch-result"></div>' +
4901
5431
  '</div>';
5432
+ renderConversationTemplates(el);
4902
5433
  }).catch(function() {
4903
5434
  el.innerHTML = '<div class="launch-panel"><h3>Launch Agent Terminal</h3><p style="color:var(--text-dim)">Failed to load templates.</p></div>';
4904
5435
  });
4905
5436
  }
4906
5437
 
5438
+ // ==================== v3.4: CONVERSATION TEMPLATES ====================
5439
+ function renderConversationTemplates(p){fetch('/api/conversation-templates').then(function(r){return r.json()}).then(function(t){if(!t.length)return;var s=document.createElement('div');s.className='launch-panel';s.style.marginTop='20px';var h='<h3 style="margin-bottom:4px">Conversation Templates</h3><p style="font-size:12px;color:var(--text-dim);margin-bottom:16px">Pre-built multi-agent workflows. Click to see agent prompts.</p><div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:12px">';for(var i=0;i<t.length;i++){var c=t[i];var n=c.agents.map(function(a){return a.name}).join(', ');h+='<div style="background:var(--surface-2);border:1px solid var(--border);border-radius:8px;padding:14px;cursor:pointer;transition:all 0.15s" onclick="showConvTemplate(\''+escapeHtml(c.id)+'\')" onmouseover="this.style.borderColor=\'var(--accent)\'" onmouseout="this.style.borderColor=\'var(--border)\'"><div style="font-weight:600;font-size:13px;margin-bottom:4px">'+escapeHtml(c.name)+'</div><div style="font-size:11px;color:var(--text-dim);margin-bottom:8px">'+escapeHtml(c.description)+'</div><div style="font-size:10px;color:var(--text-muted)">Agents: '+escapeHtml(n)+'</div>'+(c.workflow?'<div style="font-size:10px;color:var(--purple);margin-top:4px">Workflow: '+escapeHtml(c.workflow.steps.join(' \u2192 '))+'</div>':'')+'</div>'}h+='</div><div id="conv-template-detail" style="margin-top:16px"></div>';s.innerHTML=h;p.appendChild(s)}).catch(function(){})}
5440
+ function showConvTemplate(tid){var pq=activeProject?'?project='+encodeURIComponent(activeProject):'';fetch('/api/conversation-templates/launch'+pq,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({template_id:tid})}).then(function(r){return r.json()}).then(function(d){if(d.error){alert(d.error);return}var el=document.getElementById('conv-template-detail');var h='<div style="background:var(--surface);border:1px solid var(--border);border-radius:8px;padding:16px"><div style="font-weight:700;font-size:15px;margin-bottom:4px">'+escapeHtml(d.template.name)+'</div><div style="font-size:12px;color:var(--text-dim);margin-bottom:12px">'+escapeHtml(d.template.description)+'</div>';if(d.template.workflow){h+='<div style="display:flex;gap:6px;align-items:center;margin-bottom:14px;flex-wrap:wrap">';var st=d.template.workflow.steps;for(var s=0;s<st.length;s++){if(s>0)h+='<span style="color:var(--text-muted);font-size:12px">\u2192</span>';h+='<span style="background:var(--purple-dim);color:var(--purple);padding:3px 10px;border-radius:12px;font-size:11px;font-weight:600">'+escapeHtml(st[s])+'</span>'}h+='</div>'}h+='<div style="font-weight:600;font-size:12px;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.5px;margin-bottom:8px">Agent Prompts (copy each into a separate terminal)</div>';for(var i=0;i<d.instructions.length;i++){var inst=d.instructions[i];h+='<div style="margin-bottom:10px"><div style="display:flex;align-items:center;gap:8px;margin-bottom:4px"><span style="font-weight:600;font-size:13px">'+escapeHtml(inst.agent_name)+'</span><span class="role-badge">'+escapeHtml(inst.role)+'</span></div><div class="copy-block" onclick="copyText(this)" data-text="'+inst.prompt.replace(/&/g,'&amp;').replace(/"/g,'&quot;').replace(/</g,'&lt;').replace(/>/g,'&gt;')+'"><span class="copy-hint">click to copy</span><span style="font-size:11px;color:var(--text-dim)">'+escapeHtml(inst.prompt.substring(0,200))+(inst.prompt.length>200?'...':'')+'</span></div></div>'}h+='</div>';el.innerHTML=h;el.scrollIntoView({behavior:'smooth',block:'nearest'})})}
5441
+
4907
5442
  function selectCli(cli) {
4908
5443
  selectedCli = cli;
4909
5444
  var btns = document.querySelectorAll('.cli-btn');
@@ -4980,6 +5515,27 @@ function copyLaunchPrompt() {
4980
5515
  // ==================== SSE + POLLING ====================
4981
5516
 
4982
5517
  var sseConnected = false;
5518
+ var sseRetryDelay = 1000;
5519
+ var sseRetryTimer = null;
5520
+ var ssePollFallback = null;
5521
+
5522
+ function setConnStatus(status) {
5523
+ var dot = document.querySelector('.conn-dot');
5524
+ var label = document.getElementById('conn-label');
5525
+ if (status === 'live') {
5526
+ dot.style.background = 'var(--green)';
5527
+ dot.style.animation = 'pulse 2s infinite';
5528
+ label.textContent = 'Live (SSE)';
5529
+ } else if (status === 'reconnecting') {
5530
+ dot.style.background = 'var(--yellow, #f0ad4e)';
5531
+ dot.style.animation = 'pulse 0.8s infinite';
5532
+ label.textContent = 'Reconnecting...';
5533
+ } else if (status === 'poll') {
5534
+ dot.style.background = 'var(--green)';
5535
+ dot.style.animation = 'pulse 2s infinite';
5536
+ label.textContent = 'Live (poll)';
5537
+ }
5538
+ }
4983
5539
 
4984
5540
  function initSSE() {
4985
5541
  try {
@@ -4991,20 +5547,29 @@ function initSSE() {
4991
5547
  };
4992
5548
  eventSource.onopen = function() {
4993
5549
  sseConnected = true;
4994
- document.getElementById('conn-label').textContent = 'Live (SSE)';
5550
+ sseRetryDelay = 1000;
5551
+ if (sseRetryTimer) { clearTimeout(sseRetryTimer); sseRetryTimer = null; }
5552
+ if (ssePollFallback) { clearInterval(ssePollFallback); ssePollFallback = null; }
5553
+ setConnStatus('live');
4995
5554
  };
4996
5555
  eventSource.onerror = function() {
4997
- if (sseConnected) {
4998
- sseConnected = false;
4999
- document.getElementById('conn-label').textContent = 'Live (poll)';
5000
- console.warn('SSE disconnected, falling back to polling');
5001
- eventSource.close();
5002
- setInterval(poll, POLL_INTERVAL);
5556
+ sseConnected = false;
5557
+ eventSource.close();
5558
+ console.warn('SSE disconnected, retrying in ' + sseRetryDelay + 'ms');
5559
+ setConnStatus('reconnecting');
5560
+ if (!ssePollFallback) {
5561
+ ssePollFallback = setInterval(poll, POLL_INTERVAL);
5003
5562
  }
5563
+ sseRetryTimer = setTimeout(function() {
5564
+ initSSE();
5565
+ }, sseRetryDelay);
5566
+ sseRetryDelay = Math.min(sseRetryDelay * 2, 30000);
5004
5567
  };
5005
5568
  } catch (e) {
5006
- // SSE not supported — use polling
5007
- setInterval(poll, POLL_INTERVAL);
5569
+ setConnStatus('poll');
5570
+ if (!ssePollFallback) {
5571
+ ssePollFallback = setInterval(poll, POLL_INTERVAL);
5572
+ }
5008
5573
  }
5009
5574
  }
5010
5575
 
@@ -5020,6 +5585,9 @@ function initSSE() {
5020
5585
  }
5021
5586
  })();
5022
5587
 
5588
+ // Init UI preferences
5589
+ initCompactMode();
5590
+
5023
5591
  // Load projects first, then poll (so auto-select works before first data fetch)
5024
5592
  loadProjects().then(function() {
5025
5593
  console.log('[LTT] init: projects loaded, starting poll + SSE');