let-them-talk 3.3.3 → 3.4.0

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 {
@@ -816,7 +854,8 @@
816
854
  .msg-input-bar {
817
855
  border-top: 1px solid var(--border);
818
856
  background: var(--surface);
819
- padding: 12px 20px;
857
+ background-image: var(--gradient-surface);
858
+ padding: 14px 20px;
820
859
  display: flex;
821
860
  gap: 8px;
822
861
  align-items: flex-end;
@@ -840,14 +879,15 @@
840
879
  background: var(--surface-2);
841
880
  color: var(--text);
842
881
  border: 1px solid var(--border);
843
- border-radius: 6px;
882
+ border-radius: 8px;
844
883
  padding: 7px 10px;
845
884
  font-size: 12px;
846
885
  cursor: pointer;
847
886
  outline: none;
887
+ transition: all 0.2s ease;
848
888
  }
849
889
 
850
- .input-target select:focus { border-color: var(--accent); }
890
+ .input-target select:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-dim); }
851
891
 
852
892
  .input-msg {
853
893
  flex: 1;
@@ -867,7 +907,7 @@
867
907
  background: var(--surface-2);
868
908
  color: var(--text);
869
909
  border: 1px solid var(--border);
870
- border-radius: 6px;
910
+ border-radius: 8px;
871
911
  padding: 7px 10px;
872
912
  font-size: 12px;
873
913
  font-family: inherit;
@@ -875,26 +915,30 @@
875
915
  height: 34px;
876
916
  max-height: 80px;
877
917
  outline: none;
918
+ transition: all 0.2s ease;
878
919
  }
879
920
 
880
- .input-msg textarea:focus { border-color: var(--accent); }
921
+ .input-msg textarea:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-dim); }
881
922
 
882
923
  .send-btn {
883
- background: var(--accent);
924
+ background: var(--gradient-accent);
884
925
  color: #fff;
885
926
  border: none;
886
- border-radius: 6px;
887
- padding: 7px 16px;
927
+ border-radius: 8px;
928
+ padding: 7px 20px;
888
929
  font-size: 12px;
889
930
  font-weight: 600;
890
931
  cursor: pointer;
891
932
  white-space: nowrap;
892
- transition: opacity 0.15s;
933
+ transition: all 0.2s ease;
893
934
  height: 34px;
935
+ box-shadow: 0 2px 8px var(--accent-glow);
936
+ letter-spacing: 0.02em;
894
937
  }
895
938
 
896
- .send-btn:hover { opacity: 0.85; }
897
- .send-btn:disabled { opacity: 0.4; cursor: not-allowed; }
939
+ .send-btn:hover { transform: translateY(-1px); box-shadow: 0 4px 16px var(--accent-glow); }
940
+ .send-btn:active { transform: translateY(0); }
941
+ .send-btn:disabled { opacity: 0.4; cursor: not-allowed; transform: none; box-shadow: none; }
898
942
 
899
943
  /* ===== EMPTY STATE ===== */
900
944
  .empty-state {
@@ -912,25 +956,27 @@
912
956
  .empty-sub { font-size: 12px; color: var(--text-muted); }
913
957
 
914
958
  /* ===== MESSAGE FLASH ===== */
915
- .message-new { animation: flashIn 0.6s ease-out; }
959
+ .message-new { animation: flashIn 0.8s ease-out; }
916
960
 
917
961
  @keyframes flashIn {
918
- from { background: var(--accent-dim); }
919
- to { background: transparent; }
962
+ 0% { background: var(--accent-dim); box-shadow: inset 0 0 20px var(--accent-glow); }
963
+ 100% { background: transparent; box-shadow: none; }
920
964
  }
921
965
 
922
966
  /* ===== SCROLLBAR ===== */
923
- ::-webkit-scrollbar { width: 6px; }
967
+ ::-webkit-scrollbar { width: 5px; }
924
968
  ::-webkit-scrollbar-track { background: transparent; }
925
- ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
926
- ::-webkit-scrollbar-thumb:hover { background: var(--text-muted); }
969
+ ::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.08); border-radius: 4px; }
970
+ ::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.15); }
971
+ [data-theme="light"] ::-webkit-scrollbar-thumb { background: rgba(0,0,0,0.1); }
972
+ [data-theme="light"] ::-webkit-scrollbar-thumb:hover { background: rgba(0,0,0,0.2); }
927
973
 
928
974
  /* ===== SCROLL TO BOTTOM ===== */
929
975
  .scroll-bottom {
930
976
  position: absolute;
931
977
  bottom: 80px;
932
978
  right: 24px;
933
- background: var(--accent);
979
+ background: var(--gradient-accent);
934
980
  color: #fff;
935
981
  border: none;
936
982
  border-radius: 50%;
@@ -941,13 +987,13 @@
941
987
  justify-content: center;
942
988
  cursor: pointer;
943
989
  font-size: 16px;
944
- box-shadow: 0 2px 8px rgba(0,0,0,0.4);
990
+ box-shadow: 0 4px 16px var(--accent-glow);
945
991
  z-index: 10;
946
- transition: opacity 0.2s;
992
+ transition: all 0.25s ease;
947
993
  }
948
994
 
949
995
  .scroll-bottom.visible { display: flex; }
950
- .scroll-bottom:hover { opacity: 0.85; }
996
+ .scroll-bottom:hover { transform: translateY(-2px); box-shadow: 0 6px 24px var(--accent-glow); }
951
997
 
952
998
  .scroll-bottom .new-count {
953
999
  position: absolute;
@@ -981,16 +1027,43 @@
981
1027
  background: var(--surface-2);
982
1028
  color: var(--text);
983
1029
  border: 1px solid var(--border);
984
- border-radius: 6px;
985
- padding: 6px 10px;
1030
+ border-radius: 8px;
1031
+ padding: 7px 12px;
986
1032
  font-size: 12px;
987
1033
  font-family: inherit;
988
1034
  outline: none;
1035
+ transition: all 0.2s ease;
989
1036
  }
990
1037
 
991
- .search-input:focus { border-color: var(--accent); }
1038
+ .search-input:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-dim); }
992
1039
  .search-input::placeholder { color: var(--text-muted); }
993
1040
 
1041
+ .compact-toggle {
1042
+ background: var(--surface-2);
1043
+ border: 1px solid var(--border);
1044
+ border-radius: 6px;
1045
+ color: var(--text-dim);
1046
+ font-size: 11px;
1047
+ padding: 4px 8px;
1048
+ cursor: pointer;
1049
+ white-space: nowrap;
1050
+ transition: background 0.15s, color 0.15s;
1051
+ }
1052
+ .compact-toggle:hover { background: var(--surface-3); color: var(--text); }
1053
+ .compact-toggle.active { background: var(--accent-dim); color: var(--accent); border-color: var(--accent); }
1054
+
1055
+ /* Compact mode styles */
1056
+ .messages-area.compact-mode { gap: 0; padding: 8px 12px; }
1057
+ .messages-area.compact-mode .message { padding: 3px 8px; gap: 6px; }
1058
+ .messages-area.compact-mode .msg-avatar,
1059
+ .messages-area.compact-mode .msg-avatar-img { display: none; }
1060
+ .messages-area.compact-mode .msg-header { margin-bottom: 0; display: inline; }
1061
+ .messages-area.compact-mode .msg-body { display: inline; }
1062
+ .messages-area.compact-mode .msg-from { font-size: 12px; }
1063
+ .messages-area.compact-mode .msg-time { font-size: 9px; }
1064
+ .messages-area.compact-mode .msg-content { display: inline; font-size: 12px; }
1065
+ .messages-area.compact-mode .msg-content p { display: inline; margin: 0; }
1066
+
994
1067
  .search-count {
995
1068
  font-size: 11px;
996
1069
  color: var(--text-muted);
@@ -1076,6 +1149,7 @@
1076
1149
  gap: 16px;
1077
1150
  font-size: 10px;
1078
1151
  color: var(--text-muted);
1152
+ letter-spacing: 0.02em;
1079
1153
  }
1080
1154
 
1081
1155
  .app-footer a { color: var(--text-dim); text-decoration: none; }
@@ -1177,17 +1251,18 @@
1177
1251
  .view-tab {
1178
1252
  flex: 1;
1179
1253
  text-align: center;
1180
- padding: 8px;
1254
+ padding: 10px 8px;
1181
1255
  font-size: 12px;
1182
1256
  font-weight: 600;
1183
- color: var(--text-dim);
1257
+ color: var(--text-muted);
1184
1258
  cursor: pointer;
1185
1259
  border-bottom: 2px solid transparent;
1186
- transition: all 0.15s;
1260
+ transition: all 0.2s ease;
1261
+ letter-spacing: 0.02em;
1187
1262
  }
1188
1263
 
1189
- .view-tab:hover { color: var(--text); }
1190
- .view-tab.active { color: var(--accent); border-bottom-color: var(--accent); }
1264
+ .view-tab:hover { color: var(--text); background: rgba(255,255,255,0.02); }
1265
+ .view-tab.active { color: var(--accent); border-bottom-color: var(--accent); background: var(--accent-dim); }
1191
1266
 
1192
1267
  /* ===== LAN BADGE ===== */
1193
1268
  .lan-badge {
@@ -1231,13 +1306,14 @@
1231
1306
 
1232
1307
  .phone-modal {
1233
1308
  background: var(--surface);
1234
- border: 1px solid var(--border);
1235
- border-radius: 12px;
1309
+ background-image: var(--gradient-surface);
1310
+ border: 1px solid var(--border-light);
1311
+ border-radius: 16px;
1236
1312
  padding: 28px;
1237
1313
  width: 360px;
1238
1314
  max-width: 90vw;
1239
1315
  text-align: center;
1240
- box-shadow: 0 16px 48px rgba(0,0,0,0.5);
1316
+ box-shadow: var(--shadow-lg), 0 0 60px rgba(0,0,0,0.3);
1241
1317
  position: relative;
1242
1318
  }
1243
1319
 
@@ -1411,14 +1487,19 @@
1411
1487
 
1412
1488
  .task-card {
1413
1489
  background: var(--surface-2);
1490
+ background-image: var(--gradient-surface);
1414
1491
  border: 1px solid var(--border);
1415
- border-radius: 6px;
1492
+ border-radius: 8px;
1416
1493
  padding: 10px;
1417
1494
  margin-bottom: 6px;
1418
- transition: border-color 0.15s;
1495
+ transition: all 0.2s ease;
1419
1496
  }
1420
1497
 
1421
- .task-card:hover { border-color: var(--border-light); }
1498
+ .task-card:hover { border-color: var(--border-light); transform: translateY(-1px); box-shadow: var(--shadow-sm); }
1499
+ .task-card[draggable="true"] { cursor: grab; }
1500
+ .task-card[draggable="true"]:active { cursor: grabbing; }
1501
+ .task-card.dragging { opacity: 0.4; transform: scale(0.95); }
1502
+ .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
1503
 
1423
1504
  .task-title {
1424
1505
  font-size: 13px;
@@ -1945,11 +2026,14 @@
1945
2026
  position: fixed;
1946
2027
  z-index: 300;
1947
2028
  background: var(--surface);
1948
- border: 1px solid var(--border);
1949
- border-radius: 10px;
1950
- padding: 16px;
2029
+ background-image: var(--gradient-surface);
2030
+ border: 1px solid var(--border-light);
2031
+ border-radius: 14px;
2032
+ padding: 18px;
1951
2033
  width: 260px;
1952
- box-shadow: 0 8px 24px rgba(0,0,0,0.4);
2034
+ box-shadow: var(--shadow-lg), 0 0 40px rgba(0,0,0,0.2);
2035
+ backdrop-filter: blur(8px);
2036
+ -webkit-backdrop-filter: blur(8px);
1953
2037
  }
1954
2038
 
1955
2039
  .profile-popup.open { display: block; }
@@ -2201,6 +2285,122 @@
2201
2285
 
2202
2286
  .launch-area.visible { display: block; }
2203
2287
 
2288
+ /* ===== STATS VIEW ===== */
2289
+ .stats-area {
2290
+ flex: 1;
2291
+ overflow-y: auto;
2292
+ padding: 20px;
2293
+ display: none;
2294
+ }
2295
+ .stats-area.visible { display: block; }
2296
+
2297
+ .stats-grid {
2298
+ display: grid;
2299
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
2300
+ gap: 12px;
2301
+ margin-bottom: 20px;
2302
+ }
2303
+
2304
+ .stat-card {
2305
+ background: var(--surface);
2306
+ border: 1px solid var(--border);
2307
+ border-radius: 8px;
2308
+ padding: 14px;
2309
+ }
2310
+ .stat-card-label {
2311
+ font-size: 10px;
2312
+ text-transform: uppercase;
2313
+ letter-spacing: 0.5px;
2314
+ color: var(--text-muted);
2315
+ margin-bottom: 4px;
2316
+ }
2317
+ .stat-card-value {
2318
+ font-size: 24px;
2319
+ font-weight: 700;
2320
+ color: var(--text);
2321
+ }
2322
+ .stat-card-sub {
2323
+ font-size: 11px;
2324
+ color: var(--text-dim);
2325
+ margin-top: 2px;
2326
+ }
2327
+
2328
+ .stats-section {
2329
+ background: var(--surface);
2330
+ border: 1px solid var(--border);
2331
+ border-radius: 8px;
2332
+ padding: 16px;
2333
+ margin-bottom: 16px;
2334
+ }
2335
+ .stats-section-title {
2336
+ font-size: 12px;
2337
+ font-weight: 700;
2338
+ text-transform: uppercase;
2339
+ letter-spacing: 0.5px;
2340
+ color: var(--text-dim);
2341
+ margin-bottom: 12px;
2342
+ }
2343
+
2344
+ .stats-bar-row {
2345
+ display: flex;
2346
+ align-items: center;
2347
+ gap: 8px;
2348
+ margin-bottom: 6px;
2349
+ }
2350
+ .stats-bar-label {
2351
+ font-size: 12px;
2352
+ color: var(--text);
2353
+ min-width: 80px;
2354
+ text-align: right;
2355
+ }
2356
+ .stats-bar-track {
2357
+ flex: 1;
2358
+ height: 18px;
2359
+ background: var(--surface-2);
2360
+ border-radius: 4px;
2361
+ overflow: hidden;
2362
+ }
2363
+ .stats-bar-fill {
2364
+ height: 100%;
2365
+ border-radius: 4px;
2366
+ transition: width 0.3s;
2367
+ display: flex;
2368
+ align-items: center;
2369
+ padding-left: 6px;
2370
+ font-size: 10px;
2371
+ font-weight: 600;
2372
+ color: #fff;
2373
+ white-space: nowrap;
2374
+ }
2375
+
2376
+ .stats-hour-chart {
2377
+ display: flex;
2378
+ align-items: flex-end;
2379
+ gap: 2px;
2380
+ height: 80px;
2381
+ }
2382
+ .stats-hour-bar {
2383
+ flex: 1;
2384
+ background: var(--accent);
2385
+ border-radius: 2px 2px 0 0;
2386
+ min-width: 4px;
2387
+ opacity: 0.7;
2388
+ transition: opacity 0.15s;
2389
+ position: relative;
2390
+ }
2391
+ .stats-hour-bar:hover { opacity: 1; }
2392
+ .stats-hour-labels {
2393
+ display: flex;
2394
+ gap: 2px;
2395
+ margin-top: 4px;
2396
+ }
2397
+ .stats-hour-labels span {
2398
+ flex: 1;
2399
+ text-align: center;
2400
+ font-size: 8px;
2401
+ color: var(--text-muted);
2402
+ }
2403
+
2204
2404
  .launch-panel {
2205
2405
  max-width: 560px;
2206
2406
  margin: 0 auto;
@@ -2459,6 +2659,7 @@
2459
2659
  <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
2660
  <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
2661
  <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>
2662
+ <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
2663
  </div>
2463
2664
  </div>
2464
2665
  <button class="btn btn-danger" onclick="doReset()">Reset</button>
@@ -2479,6 +2680,21 @@
2479
2680
  </div>
2480
2681
  </div>
2481
2682
 
2683
+ <!-- v3.4: EDIT MESSAGE MODAL -->
2684
+ <div class="phone-modal-overlay" id="edit-modal-overlay" onclick="if(event.target===this)closeEditModal()">
2685
+ <div class="phone-modal" style="width:500px;text-align:left">
2686
+ <button class="phone-modal-close" onclick="closeEditModal()">&times;</button>
2687
+ <h3>Edit Message</h3>
2688
+ <input type="hidden" id="edit-msg-id">
2689
+ <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>
2690
+ <div id="edit-history" style="max-height:150px;overflow-y:auto;margin-bottom:12px"></div>
2691
+ <div style="display:flex;gap:8px;justify-content:flex-end">
2692
+ <button class="btn" onclick="closeEditModal()">Cancel</button>
2693
+ <button class="btn btn-primary" onclick="saveEditMessage()" style="background:var(--accent);color:#fff;border-color:var(--accent)">Save Edit</button>
2694
+ </div>
2695
+ </div>
2696
+ </div>
2697
+
2482
2698
  <!-- APP LAYOUT -->
2483
2699
  <div class="app">
2484
2700
  <div class="sidebar-overlay" id="sidebar-overlay" onclick="toggleSidebar()"></div>
@@ -2566,11 +2782,13 @@
2566
2782
  <div class="view-tab" id="tab-workspaces" onclick="switchView('workspaces')">Workspaces</div>
2567
2783
  <div class="view-tab" id="tab-workflows" onclick="switchView('workflows')">Workflows</div>
2568
2784
  <div class="view-tab" id="tab-launch" onclick="switchView('launch')">Launch</div>
2785
+ <div class="view-tab" id="tab-stats" onclick="switchView('stats')">Stats</div>
2569
2786
  </div>
2570
2787
  <div class="branch-tabs" id="branch-tabs"></div>
2571
2788
  <div class="search-bar" id="search-bar">
2572
2789
  <input class="search-input" id="search-input" placeholder="Search messages... ( / )" oninput="onSearch()">
2573
2790
  <span class="search-count" id="search-count"></span>
2791
+ <button class="compact-toggle" id="compact-toggle" onclick="toggleCompactMode()" title="Toggle compact view">Compact</button>
2574
2792
  </div>
2575
2793
  <div class="pinned-section" id="pinned-section">
2576
2794
  <div class="pinned-header" onclick="togglePinnedSection()"><span>Pinned Messages</span><span class="pinned-toggle" id="pinned-toggle">Hide</span></div>
@@ -2581,6 +2799,7 @@
2581
2799
  <div class="workspaces-area" id="workspaces-area"></div>
2582
2800
  <div class="workflows-area" id="workflows-area"></div>
2583
2801
  <div class="launch-area" id="launch-area"></div>
2802
+ <div class="stats-area" id="stats-area"></div>
2584
2803
  <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
2804
  <div class="typing-bar" id="typing-bar"></div>
2586
2805
 
@@ -2602,7 +2821,7 @@
2602
2821
  </div>
2603
2822
  </div>
2604
2823
  <div class="app-footer">
2605
- <span>Let Them Talk v3.3.3</span>
2824
+ <span>Let Them Talk v3.4.0</span>
2606
2825
  </div>
2607
2826
  <div class="profile-popup" id="profile-popup" onclick="event.stopPropagation()">
2608
2827
  <div class="profile-popup-header">
@@ -3205,6 +3424,7 @@ function renderMessages(messages) {
3205
3424
  var badges = '';
3206
3425
  if (m.acked) badges += '<span class="badge badge-ack">ACK</span>';
3207
3426
  if (m.thread_id) badges += '<span class="badge badge-thread">Thread</span>';
3427
+ 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
3428
 
3209
3429
  var msgAvatarHtml = getMsgAvatar(m.from, color);
3210
3430
 
@@ -3330,6 +3550,28 @@ function scrollToBottom() {
3330
3550
  document.getElementById('new-msg-count').style.display = 'none';
3331
3551
  }
3332
3552
 
3553
+ // ==================== COMPACT MODE ====================
3554
+
3555
+ var compactMode = localStorage.getItem('compactMode') === 'true';
3556
+
3557
+ function initCompactMode() {
3558
+ var area = document.getElementById('messages');
3559
+ var btn = document.getElementById('compact-toggle');
3560
+ if (compactMode) {
3561
+ area.classList.add('compact-mode');
3562
+ btn.classList.add('active');
3563
+ }
3564
+ }
3565
+
3566
+ function toggleCompactMode() {
3567
+ compactMode = !compactMode;
3568
+ localStorage.setItem('compactMode', compactMode);
3569
+ var area = document.getElementById('messages');
3570
+ var btn = document.getElementById('compact-toggle');
3571
+ area.classList.toggle('compact-mode', compactMode);
3572
+ btn.classList.toggle('active', compactMode);
3573
+ }
3574
+
3333
3575
  // ==================== SEARCH ====================
3334
3576
 
3335
3577
  var searchQuery = '';
@@ -3425,16 +3667,106 @@ function buildMsgActions(msgId) {
3425
3667
  var starClass = isBookmarked ? ' active' : '';
3426
3668
  var pinClass = isPinned ? ' active' : '';
3427
3669
 
3670
+ var msg = cachedHistory.find(function(m) { return m.id === msgId; });
3671
+ var isDeletable = msg && (msg.from === 'dashboard' || msg.from === 'Dashboard' || msg.from === 'system' || msg.from === '__system__');
3672
+ var deleteBtn = isDeletable ? '<button class="msg-action-btn" onclick="deleteMessage(\'' + msgId + '\')" title="Delete" style="color:var(--red,#e74c3c)">&#x1f5d1;&#xFE0F;</button>' : '';
3673
+
3428
3674
  return '<div class="msg-actions">' +
3429
3675
  '<button class="msg-action-btn" onclick="toggleReactPicker(\'' + msgId + '\')" title="React">&#x1f600;</button>' +
3430
3676
  '<button class="msg-action-btn' + pinClass + '" onclick="togglePin(\'' + msgId + '\')" title="Pin">\ud83d\udccc</button>' +
3431
3677
  '<button class="msg-action-btn' + starClass + '" onclick="toggleBookmark(\'' + msgId + '\')" title="Bookmark">' + starChar + '</button>' +
3678
+ '<button class="msg-action-btn" onclick="copyMessage(\'' + msgId + '\', this)" title="Copy">&#x1f4cb;</button>' +
3679
+ '<button class="msg-action-btn" onclick="openEditMessage(\'' + msgId + '\')" title="Edit">&#x270F;&#xFE0F;</button>' +
3680
+ deleteBtn +
3432
3681
  '<div class="react-picker" id="react-' + msgId + '">' +
3433
3682
  REACTION_EMOJIS.map(function(e) { return '<button class="react-emoji" onclick="addReaction(\'' + msgId + '\',\'' + e + '\')">' + e + '</button>'; }).join('') +
3434
3683
  '</div>' +
3435
3684
  '</div>';
3436
3685
  }
3437
3686
 
3687
+ function copyMessage(msgId, btn) {
3688
+ var msg = cachedHistory.find(function(m) { return m.id === msgId; });
3689
+ if (!msg) return;
3690
+ navigator.clipboard.writeText(msg.content).then(function() {
3691
+ var orig = btn.innerHTML;
3692
+ btn.innerHTML = '&#x2705;';
3693
+ btn.title = 'Copied!';
3694
+ setTimeout(function() { btn.innerHTML = orig; btn.title = 'Copy'; }, 1500);
3695
+ });
3696
+ }
3697
+
3698
+ function deleteMessage(msgId) {
3699
+ var msg = cachedHistory.find(function(m) { return m.id === msgId; });
3700
+ if (!msg) return;
3701
+ var preview = msg.content.substring(0, 60) + (msg.content.length > 60 ? '...' : '');
3702
+ if (!confirm('Delete this message from ' + msg.from + '?\n\n"' + preview + '"')) return;
3703
+ var pq = activeProject ? '?project=' + encodeURIComponent(activeProject) : '';
3704
+ fetch('/api/message' + pq, {
3705
+ method: 'DELETE',
3706
+ headers: { 'Content-Type': 'application/json' },
3707
+ body: JSON.stringify({ id: msgId })
3708
+ }).then(function(r) { return r.json(); }).then(function(data) {
3709
+ if (data.error) {
3710
+ console.error('Delete failed: ' + data.error);
3711
+ } else {
3712
+ cachedHistory = cachedHistory.filter(function(m) { return m.id !== msgId; });
3713
+ lastMessageCount = 0;
3714
+ renderMessages(cachedHistory);
3715
+ }
3716
+ }).catch(function(err) {
3717
+ console.error('Delete failed: ' + err.message);
3718
+ });
3719
+ }
3720
+
3721
+ // ==================== v3.4: MESSAGE EDIT ====================
3722
+
3723
+ function openEditMessage(msgId) {
3724
+ var msg = cachedHistory.find(function(m) { return m.id === msgId; });
3725
+ if (!msg) return;
3726
+ var overlay = document.getElementById('edit-modal-overlay');
3727
+ document.getElementById('edit-msg-id').value = msgId;
3728
+ document.getElementById('edit-msg-content').value = msg.content;
3729
+ var historyEl = document.getElementById('edit-history');
3730
+ if (msg.edit_history && msg.edit_history.length > 0) {
3731
+ historyEl.innerHTML = '<div style="font-size:10px;color:var(--text-muted);margin-bottom:4px">Edit history (' + msg.edit_history.length + ' edits):</div>' +
3732
+ msg.edit_history.map(function(h) {
3733
+ return '<div style="font-size:11px;color:var(--text-dim);padding:4px 8px;background:var(--surface-2);border-radius:4px;margin-bottom:2px">' +
3734
+ '<span style="color:var(--text-muted)">' + new Date(h.edited_at).toLocaleTimeString() + '</span> ' +
3735
+ escapeHtml(h.content.substring(0, 100)) + (h.content.length > 100 ? '...' : '') + '</div>';
3736
+ }).join('');
3737
+ historyEl.style.display = 'block';
3738
+ } else {
3739
+ historyEl.style.display = 'none';
3740
+ }
3741
+ overlay.classList.add('open');
3742
+ document.getElementById('edit-msg-content').focus();
3743
+ }
3744
+
3745
+ function closeEditModal() {
3746
+ document.getElementById('edit-modal-overlay').classList.remove('open');
3747
+ }
3748
+
3749
+ function saveEditMessage() {
3750
+ var msgId = document.getElementById('edit-msg-id').value;
3751
+ var content = document.getElementById('edit-msg-content').value.trim();
3752
+ if (!content) return;
3753
+ var pq = activeProject ? '?project=' + encodeURIComponent(activeProject) : '';
3754
+ fetch('/api/message' + pq, {
3755
+ method: 'PUT',
3756
+ headers: { 'Content-Type': 'application/json' },
3757
+ body: JSON.stringify({ id: msgId, content: content })
3758
+ }).then(function(r) { return r.json(); }).then(function(data) {
3759
+ if (data.success) {
3760
+ closeEditModal();
3761
+ poll();
3762
+ } else {
3763
+ alert('Edit failed: ' + (data.error || 'Unknown error'));
3764
+ }
3765
+ }).catch(function(err) {
3766
+ alert('Edit failed: ' + err.message);
3767
+ });
3768
+ }
3769
+
3438
3770
  function buildReactionsHtml(msgId) {
3439
3771
  if (!reactions[msgId]) return '';
3440
3772
  var emojis = Object.keys(reactions[msgId]);
@@ -3512,6 +3844,8 @@ document.addEventListener('keydown', function(e) {
3512
3844
  if (e.key === '4') { switchView('workflows'); return; }
3513
3845
  // 5 — Launch tab
3514
3846
  if (e.key === '5') { switchView('launch'); return; }
3847
+ // 6 — Stats tab
3848
+ if (e.key === '6') { switchView('stats'); return; }
3515
3849
  });
3516
3850
 
3517
3851
  // ==================== RELATIVE TIMESTAMPS ====================
@@ -3531,7 +3865,7 @@ function relativeTime(ts) {
3531
3865
  // ==================== COPY TO CLIPBOARD ====================
3532
3866
 
3533
3867
  function copyText(el) {
3534
- var text = el.getAttribute('data-text').replace(/&quot;/g, '"');
3868
+ var text = el.getAttribute('data-text').replace(/&quot;/g, '"').replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&amp;/g, '&');
3535
3869
  navigator.clipboard.writeText(text).then(function() {
3536
3870
  el.classList.add('copied');
3537
3871
  el.querySelector('.copy-hint').textContent = 'copied!';
@@ -3740,6 +4074,21 @@ function exportConversation() {
3740
4074
  URL.revokeObjectURL(url);
3741
4075
  }
3742
4076
 
4077
+ function exportJSON() {
4078
+ if (!cachedHistory.length) return;
4079
+ var data = cachedHistory.map(function(m) {
4080
+ return { id: m.id, from: m.from, to: m.to, content: m.content, timestamp: m.timestamp, thread_id: m.thread_id || null };
4081
+ });
4082
+ var json = JSON.stringify(data, null, 2);
4083
+ var blob = new Blob([json], { type: 'application/json' });
4084
+ var url = URL.createObjectURL(blob);
4085
+ var a = document.createElement('a');
4086
+ a.href = url;
4087
+ a.download = 'conversation-' + new Date().toISOString().slice(0, 10) + '.json';
4088
+ a.click();
4089
+ URL.revokeObjectURL(url);
4090
+ }
4091
+
3743
4092
  // ==================== VIEW SWITCHING ====================
3744
4093
 
3745
4094
  var activeView = 'messages';
@@ -3751,16 +4100,19 @@ function switchView(view) {
3751
4100
  document.getElementById('tab-workspaces').classList.toggle('active', view === 'workspaces');
3752
4101
  document.getElementById('tab-workflows').classList.toggle('active', view === 'workflows');
3753
4102
  document.getElementById('tab-launch').classList.toggle('active', view === 'launch');
4103
+ document.getElementById('tab-stats').classList.toggle('active', view === 'stats');
3754
4104
  document.getElementById('messages').style.display = view === 'messages' ? 'flex' : 'none';
3755
4105
  document.getElementById('tasks-area').classList.toggle('visible', view === 'tasks');
3756
4106
  document.getElementById('workspaces-area').classList.toggle('visible', view === 'workspaces');
3757
4107
  document.getElementById('workflows-area').classList.toggle('visible', view === 'workflows');
3758
4108
  document.getElementById('launch-area').classList.toggle('visible', view === 'launch');
4109
+ document.getElementById('stats-area').classList.toggle('visible', view === 'stats');
3759
4110
  document.getElementById('search-bar').style.display = view === 'messages' ? 'flex' : 'none';
3760
4111
  if (view === 'tasks') fetchTasks();
3761
4112
  if (view === 'workspaces') fetchWorkspaces();
3762
4113
  if (view === 'workflows') fetchWorkflows();
3763
4114
  if (view === 'launch') renderLaunchPanel();
4115
+ if (view === 'stats') fetchStats();
3764
4116
  // Auto-close sidebar on mobile after view switch
3765
4117
  if (isMobile) closeSidebar();
3766
4118
  }
@@ -3824,7 +4176,7 @@ function renderTasks() {
3824
4176
  for (var c = 0; c < cols.length; c++) {
3825
4177
  var col = cols[c];
3826
4178
  var tasks = groups[col.key] || [];
3827
- html += '<div class="kanban-col">';
4179
+ html += '<div class="kanban-col" data-status="' + col.key + '" ondragover="onKanbanDragOver(event)" ondragleave="onKanbanDragLeave(event)" ondrop="onKanbanDrop(event)">';
3828
4180
  html += '<div class="kanban-title ' + col.key + '">' + col.label + '<span class="kanban-count">' + tasks.length + '</span></div>';
3829
4181
  for (var j = 0; j < tasks.length; j++) {
3830
4182
  html += buildTaskCard(tasks[j]);
@@ -3850,7 +4202,7 @@ function buildTaskCard(t) {
3850
4202
  '<option value="blocked"' + (t.status === 'blocked' ? ' selected' : '') + '>Blocked</option>' +
3851
4203
  '</select>';
3852
4204
 
3853
- return '<div class="task-card">' +
4205
+ return '<div class="task-card" draggable="true" data-task-id="' + t.id + '" ondragstart="onTaskDragStart(event)" ondragend="onTaskDragEnd(event)">' +
3854
4206
  '<div class="task-title">' + escapeHtml(t.title || 'Untitled') + '</div>' +
3855
4207
  (t.description ? '<div class="task-desc">' + escapeHtml(t.description) + '</div>' : '') +
3856
4208
  '<div class="task-footer">' +
@@ -3871,6 +4223,135 @@ function updateTaskStatus(taskId, newStatus) {
3871
4223
  }).catch(function(e) { console.error('Task update failed:', e); });
3872
4224
  }
3873
4225
 
4226
+ // ==================== KANBAN DRAG-AND-DROP ====================
4227
+
4228
+ var draggedTaskId = null;
4229
+
4230
+ function onTaskDragStart(e) {
4231
+ draggedTaskId = e.target.getAttribute('data-task-id');
4232
+ e.dataTransfer.effectAllowed = 'move';
4233
+ e.dataTransfer.setData('text/plain', draggedTaskId);
4234
+ e.target.classList.add('dragging');
4235
+ }
4236
+
4237
+ function onTaskDragEnd(e) {
4238
+ e.target.classList.remove('dragging');
4239
+ draggedTaskId = null;
4240
+ var cols = document.querySelectorAll('.kanban-col');
4241
+ for (var i = 0; i < cols.length; i++) cols[i].classList.remove('drag-over');
4242
+ }
4243
+
4244
+ function onKanbanDragOver(e) {
4245
+ e.preventDefault();
4246
+ e.dataTransfer.dropEffect = 'move';
4247
+ var col = e.currentTarget;
4248
+ if (!col.classList.contains('drag-over')) col.classList.add('drag-over');
4249
+ }
4250
+
4251
+ function onKanbanDragLeave(e) {
4252
+ if (!e.currentTarget.contains(e.relatedTarget)) {
4253
+ e.currentTarget.classList.remove('drag-over');
4254
+ }
4255
+ }
4256
+
4257
+ function onKanbanDrop(e) {
4258
+ e.preventDefault();
4259
+ var col = e.currentTarget;
4260
+ col.classList.remove('drag-over');
4261
+ var taskId = e.dataTransfer.getData('text/plain');
4262
+ var newStatus = col.getAttribute('data-status');
4263
+ if (taskId && newStatus) {
4264
+ updateTaskStatus(taskId, newStatus);
4265
+ }
4266
+ }
4267
+
4268
+ // ==================== STATS VIEW ====================
4269
+
4270
+ var AGENT_COLORS = ['#58a6ff', '#f78166', '#7ee787', '#d2a8ff', '#ffa657', '#ff7b72', '#79c0ff', '#56d364'];
4271
+
4272
+ function fetchStats() {
4273
+ var pq = activeProject ? '?project=' + encodeURIComponent(activeProject) : '';
4274
+ fetch('/api/stats' + pq).then(function(r) { return r.json(); }).then(function(data) {
4275
+ renderStats(data);
4276
+ }).catch(function(e) { console.error('Stats fetch failed:', e); });
4277
+ }
4278
+
4279
+ function renderStats(data) {
4280
+ var el = document.getElementById('stats-area');
4281
+ if (!data || !data.total_messages) {
4282
+ el.innerHTML = '<div class="tasks-empty">No messages yet. Stats will appear once agents start talking.</div>';
4283
+ return;
4284
+ }
4285
+
4286
+ var agentNames = Object.keys(data.agents);
4287
+ var maxMessages = 0;
4288
+ for (var i = 0; i < agentNames.length; i++) {
4289
+ if (data.agents[agentNames[i]].messages > maxMessages) maxMessages = data.agents[agentNames[i]].messages;
4290
+ }
4291
+
4292
+ // Overview cards
4293
+ var html = '<div class="stats-grid">';
4294
+ html += '<div class="stat-card"><div class="stat-card-label">Total Messages</div><div class="stat-card-value">' + data.total_messages + '</div></div>';
4295
+ 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>';
4296
+ 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>';
4297
+ html += '<div class="stat-card"><div class="stat-card-label">Active Agents</div><div class="stat-card-value">' + agentNames.length + '</div></div>';
4298
+ html += '</div>';
4299
+
4300
+ // Per-agent message bars
4301
+ html += '<div class="stats-section"><div class="stats-section-title">Messages per Agent</div>';
4302
+ for (var j = 0; j < agentNames.length; j++) {
4303
+ var name = agentNames[j];
4304
+ var agent = data.agents[name];
4305
+ var pct = maxMessages ? Math.round((agent.messages / maxMessages) * 100) : 0;
4306
+ var color = AGENT_COLORS[j % AGENT_COLORS.length];
4307
+ html += '<div class="stats-bar-row">';
4308
+ html += '<div class="stats-bar-label">' + escapeHtml(name) + '</div>';
4309
+ html += '<div class="stats-bar-track"><div class="stats-bar-fill" style="width:' + pct + '%;background:' + color + '">' + agent.messages + '</div></div>';
4310
+ html += '</div>';
4311
+ }
4312
+ html += '</div>';
4313
+
4314
+ // Per-agent response times
4315
+ var hasResponseTimes = false;
4316
+ for (var k = 0; k < agentNames.length; k++) {
4317
+ if (data.agents[agentNames[k]].avg_response_ms) { hasResponseTimes = true; break; }
4318
+ }
4319
+ if (hasResponseTimes) {
4320
+ html += '<div class="stats-section"><div class="stats-section-title">Avg Response Time</div>';
4321
+ for (var l = 0; l < agentNames.length; l++) {
4322
+ var aName = agentNames[l];
4323
+ var aData = data.agents[aName];
4324
+ if (!aData.avg_response_ms) continue;
4325
+ var secs = (aData.avg_response_ms / 1000).toFixed(1);
4326
+ var maxBar = 60000;
4327
+ var rtPct = Math.min(Math.round((aData.avg_response_ms / maxBar) * 100), 100);
4328
+ var rtColor = AGENT_COLORS[l % AGENT_COLORS.length];
4329
+ html += '<div class="stats-bar-row">';
4330
+ html += '<div class="stats-bar-label">' + escapeHtml(aName) + '</div>';
4331
+ html += '<div class="stats-bar-track"><div class="stats-bar-fill" style="width:' + rtPct + '%;background:' + rtColor + '">' + secs + 's</div></div>';
4332
+ html += '</div>';
4333
+ }
4334
+ html += '</div>';
4335
+ }
4336
+
4337
+ // Hourly distribution chart
4338
+ var maxHour = Math.max.apply(null, data.hour_distribution);
4339
+ html += '<div class="stats-section"><div class="stats-section-title">Activity by Hour</div>';
4340
+ html += '<div class="stats-hour-chart">';
4341
+ for (var h = 0; h < 24; h++) {
4342
+ var hPct = maxHour ? Math.round((data.hour_distribution[h] / maxHour) * 100) : 0;
4343
+ html += '<div class="stats-hour-bar" style="height:' + Math.max(hPct, 2) + '%" title="' + h + ':00 — ' + data.hour_distribution[h] + ' msgs"></div>';
4344
+ }
4345
+ html += '</div>';
4346
+ html += '<div class="stats-hour-labels">';
4347
+ for (var hh = 0; hh < 24; hh++) {
4348
+ html += '<span>' + (hh % 6 === 0 ? hh + 'h' : '') + '</span>';
4349
+ }
4350
+ html += '</div></div>';
4351
+
4352
+ el.innerHTML = html;
4353
+ }
4354
+
3874
4355
  // ==================== EXPORT MENU ====================
3875
4356
 
3876
4357
  function toggleExportMenu() {
@@ -4292,6 +4773,7 @@ function poll() {
4292
4773
  if (activeView === 'tasks') fetchTasks();
4293
4774
  if (activeView === 'workspaces') fetchWorkspaces();
4294
4775
  if (activeView === 'workflows') fetchWorkflows();
4776
+ if (activeView === 'stats') fetchStats();
4295
4777
  }).catch(function(e) {
4296
4778
  console.error('Poll failed:', e);
4297
4779
  document.getElementById('conn-detail').textContent = ' ERR: ' + e.message;
@@ -4300,7 +4782,7 @@ function poll() {
4300
4782
 
4301
4783
  function doReset() {
4302
4784
  if (!confirm('Clear all messages, agents, and history?')) return;
4303
- fetch('/api/reset', { method: 'POST' }).then(function() {
4785
+ fetch('/api/reset' + projectParam(), { method: 'POST' }).then(function() {
4304
4786
  lastMessageCount = 0;
4305
4787
  activeThread = null;
4306
4788
  cachedHistory = [];
@@ -4862,10 +5344,6 @@ function renderLaunchPanel() {
4862
5344
  templateOpts += '<option value="' + escapeHtml(templates[i].name) + '">' + escapeHtml(templates[i].name) + ' — ' + escapeHtml(templates[i].description) + '</option>';
4863
5345
  }
4864
5346
 
4865
- var activeProject = '';
4866
- var projSelect = document.getElementById('project-selector');
4867
- if (projSelect && projSelect.value) activeProject = projSelect.value;
4868
-
4869
5347
  el.innerHTML =
4870
5348
  '<div class="launch-panel">' +
4871
5349
  '<h3>Launch Agent Terminal</h3>' +
@@ -4899,11 +5377,16 @@ function renderLaunchPanel() {
4899
5377
  '</div>' +
4900
5378
  '<div class="launch-result" id="launch-result"></div>' +
4901
5379
  '</div>';
5380
+ renderConversationTemplates(el);
4902
5381
  }).catch(function() {
4903
5382
  el.innerHTML = '<div class="launch-panel"><h3>Launch Agent Terminal</h3><p style="color:var(--text-dim)">Failed to load templates.</p></div>';
4904
5383
  });
4905
5384
  }
4906
5385
 
5386
+ // ==================== v3.4: CONVERSATION TEMPLATES ====================
5387
+ 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(){})}
5388
+ 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'})})}
5389
+
4907
5390
  function selectCli(cli) {
4908
5391
  selectedCli = cli;
4909
5392
  var btns = document.querySelectorAll('.cli-btn');
@@ -4980,6 +5463,27 @@ function copyLaunchPrompt() {
4980
5463
  // ==================== SSE + POLLING ====================
4981
5464
 
4982
5465
  var sseConnected = false;
5466
+ var sseRetryDelay = 1000;
5467
+ var sseRetryTimer = null;
5468
+ var ssePollFallback = null;
5469
+
5470
+ function setConnStatus(status) {
5471
+ var dot = document.querySelector('.conn-dot');
5472
+ var label = document.getElementById('conn-label');
5473
+ if (status === 'live') {
5474
+ dot.style.background = 'var(--green)';
5475
+ dot.style.animation = 'pulse 2s infinite';
5476
+ label.textContent = 'Live (SSE)';
5477
+ } else if (status === 'reconnecting') {
5478
+ dot.style.background = 'var(--yellow, #f0ad4e)';
5479
+ dot.style.animation = 'pulse 0.8s infinite';
5480
+ label.textContent = 'Reconnecting...';
5481
+ } else if (status === 'poll') {
5482
+ dot.style.background = 'var(--green)';
5483
+ dot.style.animation = 'pulse 2s infinite';
5484
+ label.textContent = 'Live (poll)';
5485
+ }
5486
+ }
4983
5487
 
4984
5488
  function initSSE() {
4985
5489
  try {
@@ -4991,20 +5495,29 @@ function initSSE() {
4991
5495
  };
4992
5496
  eventSource.onopen = function() {
4993
5497
  sseConnected = true;
4994
- document.getElementById('conn-label').textContent = 'Live (SSE)';
5498
+ sseRetryDelay = 1000;
5499
+ if (sseRetryTimer) { clearTimeout(sseRetryTimer); sseRetryTimer = null; }
5500
+ if (ssePollFallback) { clearInterval(ssePollFallback); ssePollFallback = null; }
5501
+ setConnStatus('live');
4995
5502
  };
4996
5503
  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);
5504
+ sseConnected = false;
5505
+ eventSource.close();
5506
+ console.warn('SSE disconnected, retrying in ' + sseRetryDelay + 'ms');
5507
+ setConnStatus('reconnecting');
5508
+ if (!ssePollFallback) {
5509
+ ssePollFallback = setInterval(poll, POLL_INTERVAL);
5003
5510
  }
5511
+ sseRetryTimer = setTimeout(function() {
5512
+ initSSE();
5513
+ }, sseRetryDelay);
5514
+ sseRetryDelay = Math.min(sseRetryDelay * 2, 30000);
5004
5515
  };
5005
5516
  } catch (e) {
5006
- // SSE not supported — use polling
5007
- setInterval(poll, POLL_INTERVAL);
5517
+ setConnStatus('poll');
5518
+ if (!ssePollFallback) {
5519
+ ssePollFallback = setInterval(poll, POLL_INTERVAL);
5520
+ }
5008
5521
  }
5009
5522
  }
5010
5523
 
@@ -5020,6 +5533,9 @@ function initSSE() {
5020
5533
  }
5021
5534
  })();
5022
5535
 
5536
+ // Init UI preferences
5537
+ initCompactMode();
5538
+
5023
5539
  // Load projects first, then poll (so auto-select works before first data fetch)
5024
5540
  loadProjects().then(function() {
5025
5541
  console.log('[LTT] init: projects loaded, starting poll + SSE');