@dmsdc-ai/aigentry-deliberation 0.0.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/LICENSE +21 -0
- package/README.md +94 -0
- package/browser-control-port.js +563 -0
- package/degradation-state-machine.js +206 -0
- package/index.js +3156 -0
- package/install.js +202 -0
- package/observer.js +483 -0
- package/package.json +65 -0
- package/public/index.html +1478 -0
- package/selectors/chatgpt-extension.json +21 -0
- package/selectors/chatgpt.json +20 -0
- package/selectors/claude-extension.json +21 -0
- package/selectors/claude.json +19 -0
- package/selectors/extension-providers.json +24 -0
- package/selectors/gemini-extension.json +21 -0
- package/selectors/gemini.json +19 -0
- package/selectors/role-presets.json +28 -0
- package/selectors/roles/critic.md +12 -0
- package/selectors/roles/free.md +1 -0
- package/selectors/roles/implementer.md +12 -0
- package/selectors/roles/mediator.md +12 -0
- package/selectors/roles/researcher.md +12 -0
- package/session-monitor-win.js +94 -0
- package/session-monitor.sh +316 -0
- package/skills/deliberation/SKILL.md +164 -0
- package/skills/deliberation-executor/SKILL.md +86 -0
|
@@ -0,0 +1,1478 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>π Deliberation Observer</title>
|
|
7
|
+
<style>
|
|
8
|
+
/* ββ Reset & Base βββββββββββββββββββββββββββββββββββββββ */
|
|
9
|
+
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
|
|
10
|
+
:root {
|
|
11
|
+
--bg: #0d1117;
|
|
12
|
+
--surface: #161b22;
|
|
13
|
+
--border: #30363d;
|
|
14
|
+
--border-h: #58a6ff;
|
|
15
|
+
--text: #c9d1d9;
|
|
16
|
+
--muted: #8b949e;
|
|
17
|
+
--blue: #58a6ff;
|
|
18
|
+
--green: #3fb950;
|
|
19
|
+
--red: #f85149;
|
|
20
|
+
--yellow: #d29922;
|
|
21
|
+
--purple: #d2a8ff;
|
|
22
|
+
}
|
|
23
|
+
body {
|
|
24
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'SF Pro Text', sans-serif;
|
|
25
|
+
background: var(--bg);
|
|
26
|
+
color: var(--text);
|
|
27
|
+
min-height: 100vh;
|
|
28
|
+
font-size: 14px;
|
|
29
|
+
line-height: 1.5;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/* ββ Animations βββββββββββββββββββββββββββββββββββββββββ */
|
|
33
|
+
@keyframes pulse { 0%,100% { transform: scale(1); opacity: 1; } 50% { transform: scale(1.15); opacity: 0.85; } }
|
|
34
|
+
@keyframes fadeSlideIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
|
|
35
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
36
|
+
@keyframes blink { 0%,100% { opacity: 1; } 50% { opacity: 0.4; } }
|
|
37
|
+
|
|
38
|
+
/* ββ Layout βββββββββββββββββββββββββββββββββββββββββββββ */
|
|
39
|
+
#app { display: flex; flex-direction: column; height: 100vh; }
|
|
40
|
+
header {
|
|
41
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
42
|
+
padding: 14px 20px;
|
|
43
|
+
border-bottom: 1px solid var(--border);
|
|
44
|
+
background: var(--surface);
|
|
45
|
+
position: sticky; top: 0; z-index: 100;
|
|
46
|
+
}
|
|
47
|
+
header h1 { font-size: 1.1em; font-weight: 600; color: var(--text); letter-spacing: -0.01em; }
|
|
48
|
+
.header-actions { display: flex; gap: 8px; align-items: center; }
|
|
49
|
+
.btn {
|
|
50
|
+
background: #21262d; border: 1px solid var(--border); color: var(--text);
|
|
51
|
+
padding: 5px 12px; border-radius: 6px; cursor: pointer; font-size: 0.82em;
|
|
52
|
+
transition: border-color 0.15s, background 0.15s;
|
|
53
|
+
}
|
|
54
|
+
.btn:hover { border-color: var(--blue); background: #1c2128; }
|
|
55
|
+
.btn.back { display: flex; align-items: center; gap: 6px; }
|
|
56
|
+
|
|
57
|
+
/* ββ Screen management ββββββββββββββββββββββββββββββββββ */
|
|
58
|
+
.screen { display: none; flex: 1; overflow: hidden; }
|
|
59
|
+
.screen.active { display: flex; flex-direction: column; }
|
|
60
|
+
|
|
61
|
+
/* βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
62
|
+
SCREEN 1: Session List
|
|
63
|
+
ββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
|
64
|
+
#screen-list { padding: 20px; overflow-y: auto; }
|
|
65
|
+
.grid {
|
|
66
|
+
display: grid;
|
|
67
|
+
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
|
68
|
+
gap: 14px;
|
|
69
|
+
max-width: 1200px;
|
|
70
|
+
margin: 0 auto;
|
|
71
|
+
}
|
|
72
|
+
.session-card {
|
|
73
|
+
background: var(--surface); border: 1px solid var(--border);
|
|
74
|
+
border-radius: 10px; padding: 18px; cursor: pointer;
|
|
75
|
+
transition: border-color 0.2s, transform 0.15s;
|
|
76
|
+
animation: fadeSlideIn 0.3s ease both;
|
|
77
|
+
}
|
|
78
|
+
.session-card:hover { border-color: var(--blue); transform: translateY(-1px); }
|
|
79
|
+
.session-card.active-border { border-color: var(--green); }
|
|
80
|
+
|
|
81
|
+
.card-header { display: flex; align-items: flex-start; justify-content: space-between; margin-bottom: 12px; gap: 8px; }
|
|
82
|
+
.card-topic { font-size: 0.95em; font-weight: 600; color: var(--text); flex: 1; }
|
|
83
|
+
|
|
84
|
+
/* Status badges */
|
|
85
|
+
.badge {
|
|
86
|
+
display: inline-flex; align-items: center; gap: 4px;
|
|
87
|
+
padding: 2px 8px; border-radius: 12px;
|
|
88
|
+
font-size: 0.72em; font-weight: 600; white-space: nowrap;
|
|
89
|
+
}
|
|
90
|
+
.badge.active { background: #23833022; color: var(--green); border: 1px solid #3fb95044; }
|
|
91
|
+
.badge.awaiting_synthesis { background: #1f6feb22; color: var(--blue); border: 1px solid #58a6ff44; }
|
|
92
|
+
.badge.synthesized { background: #30363d; color: var(--muted); border: 1px solid var(--border); }
|
|
93
|
+
|
|
94
|
+
/* Mini avatar row */
|
|
95
|
+
.avatar-row { display: flex; gap: 4px; flex-wrap: wrap; margin-bottom: 12px; }
|
|
96
|
+
.avatar { position: relative; display: inline-flex; flex-shrink: 0; }
|
|
97
|
+
.avatar svg { display: block; }
|
|
98
|
+
.avatar.idle svg { opacity: 0.45; }
|
|
99
|
+
.avatar.speaking svg { animation: pulse 1.4s ease-in-out infinite; }
|
|
100
|
+
|
|
101
|
+
/* Speech bubble */
|
|
102
|
+
.bubble {
|
|
103
|
+
position: absolute; bottom: calc(100% + 6px); left: 50%; transform: translateX(-50%);
|
|
104
|
+
background: #21262d; border: 1px solid var(--border);
|
|
105
|
+
border-radius: 6px; padding: 5px 8px;
|
|
106
|
+
font-size: 0.72em; color: var(--text); white-space: nowrap;
|
|
107
|
+
max-width: 200px; overflow: hidden; text-overflow: ellipsis;
|
|
108
|
+
pointer-events: none; z-index: 10;
|
|
109
|
+
opacity: 0; transition: opacity 0.15s;
|
|
110
|
+
}
|
|
111
|
+
.bubble::after {
|
|
112
|
+
content: ''; position: absolute; top: 100%; left: 50%; transform: translateX(-50%);
|
|
113
|
+
border: 5px solid transparent; border-top-color: var(--border);
|
|
114
|
+
}
|
|
115
|
+
.avatar.speaking .bubble,
|
|
116
|
+
.avatar:hover .bubble { opacity: 1; }
|
|
117
|
+
|
|
118
|
+
/* Progress bar */
|
|
119
|
+
.progress-wrap { margin-bottom: 10px; }
|
|
120
|
+
.progress-label { display: flex; justify-content: space-between; font-size: 0.78em; color: var(--muted); margin-bottom: 4px; }
|
|
121
|
+
.progress-track { height: 4px; background: var(--border); border-radius: 2px; overflow: hidden; }
|
|
122
|
+
.progress-fill { height: 100%; background: var(--blue); border-radius: 2px; transition: width 0.4s ease; }
|
|
123
|
+
.progress-fill.green { background: var(--green); }
|
|
124
|
+
|
|
125
|
+
/* Card meta */
|
|
126
|
+
.card-meta { display: flex; gap: 12px; flex-wrap: wrap; font-size: 0.78em; color: var(--muted); }
|
|
127
|
+
|
|
128
|
+
/* Empty / loading */
|
|
129
|
+
.empty { color: var(--muted); text-align: center; padding: 60px 20px; font-size: 0.95em; }
|
|
130
|
+
.spinner {
|
|
131
|
+
width: 20px; height: 20px; border: 2px solid var(--border);
|
|
132
|
+
border-top-color: var(--blue); border-radius: 50%;
|
|
133
|
+
animation: spin 0.8s linear infinite; margin: 0 auto 12px;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/* βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
137
|
+
SCREEN 2: Session Detail
|
|
138
|
+
ββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
|
139
|
+
#screen-detail { overflow: hidden; }
|
|
140
|
+
|
|
141
|
+
/* Speaker top bar */
|
|
142
|
+
.speaker-bar {
|
|
143
|
+
background: var(--surface); border-bottom: 1px solid var(--border);
|
|
144
|
+
padding: 12px 20px; display: flex; gap: 16px; overflow-x: auto;
|
|
145
|
+
scrollbar-width: none;
|
|
146
|
+
}
|
|
147
|
+
.speaker-bar::-webkit-scrollbar { display: none; }
|
|
148
|
+
.speaker-slot { display: flex; flex-direction: column; align-items: center; gap: 5px; flex-shrink: 0; }
|
|
149
|
+
.speaker-name { font-size: 0.7em; color: var(--muted); max-width: 60px; text-overflow: ellipsis; overflow: hidden; white-space: nowrap; }
|
|
150
|
+
.speaker-slot.current-speaker .speaker-name { color: var(--green); font-weight: 600; }
|
|
151
|
+
|
|
152
|
+
/* Detail body: log + sidebar */
|
|
153
|
+
.detail-body { display: flex; flex: 1; overflow: hidden; }
|
|
154
|
+
|
|
155
|
+
/* Chat log */
|
|
156
|
+
#chat-log {
|
|
157
|
+
flex: 1; overflow-y: auto; padding: 16px 20px;
|
|
158
|
+
display: flex; flex-direction: column; gap: 10px;
|
|
159
|
+
}
|
|
160
|
+
.log-entry {
|
|
161
|
+
display: flex; gap: 12px; align-items: flex-start;
|
|
162
|
+
animation: fadeSlideIn 0.35s ease both;
|
|
163
|
+
border-left: 3px solid transparent; padding-left: 10px; border-radius: 0 6px 6px 0;
|
|
164
|
+
background: var(--surface); padding: 12px; border-radius: 6px;
|
|
165
|
+
}
|
|
166
|
+
.entry-avatar { flex-shrink: 0; }
|
|
167
|
+
.entry-body { flex: 1; min-width: 0; }
|
|
168
|
+
.entry-header { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; flex-wrap: wrap; }
|
|
169
|
+
.entry-speaker { font-weight: 600; font-size: 0.88em; }
|
|
170
|
+
.entry-round { font-size: 0.72em; color: var(--muted); background: var(--border); padding: 1px 6px; border-radius: 10px; }
|
|
171
|
+
.entry-drift { font-size: 0.72em; color: var(--yellow); }
|
|
172
|
+
.entry-content { font-size: 0.88em; line-height: 1.6; color: var(--text); white-space: pre-wrap; word-break: break-word; }
|
|
173
|
+
|
|
174
|
+
/* Sidebar */
|
|
175
|
+
#sidebar {
|
|
176
|
+
width: 240px; flex-shrink: 0;
|
|
177
|
+
border-left: 1px solid var(--border);
|
|
178
|
+
overflow-y: auto; padding: 16px;
|
|
179
|
+
background: var(--surface);
|
|
180
|
+
display: flex; flex-direction: column; gap: 18px;
|
|
181
|
+
}
|
|
182
|
+
.sidebar-section h3 { font-size: 0.78em; font-weight: 600; color: var(--muted); text-transform: uppercase; letter-spacing: 0.08em; margin-bottom: 10px; }
|
|
183
|
+
|
|
184
|
+
/* Vote bars */
|
|
185
|
+
.vote-bar-wrap { display: flex; flex-direction: column; gap: 6px; }
|
|
186
|
+
.vote-row { display: flex; align-items: center; gap: 8px; }
|
|
187
|
+
.vote-label { font-size: 0.75em; color: var(--muted); width: 80px; }
|
|
188
|
+
.vote-track { flex: 1; height: 6px; background: var(--border); border-radius: 3px; overflow: hidden; }
|
|
189
|
+
.vote-fill { height: 100%; border-radius: 3px; transition: width 0.4s ease; }
|
|
190
|
+
.vote-fill.agree { background: var(--green); }
|
|
191
|
+
.vote-fill.disagree { background: var(--red); }
|
|
192
|
+
.vote-fill.conditional { background: var(--yellow); }
|
|
193
|
+
.vote-count { font-size: 0.75em; color: var(--muted); width: 20px; text-align: right; }
|
|
194
|
+
|
|
195
|
+
/* Degradation indicators */
|
|
196
|
+
.deg-row { display: flex; align-items: center; gap: 8px; font-size: 0.8em; }
|
|
197
|
+
.deg-dot { font-size: 1em; }
|
|
198
|
+
.deg-label { color: var(--muted); flex: 1; }
|
|
199
|
+
|
|
200
|
+
/* Role list */
|
|
201
|
+
.role-item { display: flex; align-items: center; gap: 6px; font-size: 0.8em; margin-bottom: 4px; }
|
|
202
|
+
.role-item .role-speaker { color: var(--text); }
|
|
203
|
+
.role-item .role-name { color: var(--muted); }
|
|
204
|
+
|
|
205
|
+
/* Connection warning */
|
|
206
|
+
.conn-warn {
|
|
207
|
+
background: #f8514918; border: 1px solid #f8514944;
|
|
208
|
+
color: var(--red); font-size: 0.8em; padding: 8px 12px;
|
|
209
|
+
border-radius: 6px; display: flex; align-items: center; gap: 8px;
|
|
210
|
+
}
|
|
211
|
+
.conn-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--red); animation: blink 1s infinite; }
|
|
212
|
+
|
|
213
|
+
/* βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
214
|
+
SCREEN 3: LLM Configuration
|
|
215
|
+
ββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
|
216
|
+
#screen-config { padding: 24px 20px; overflow-y: auto; }
|
|
217
|
+
|
|
218
|
+
.config-header {
|
|
219
|
+
max-width: 900px; margin: 0 auto 24px;
|
|
220
|
+
}
|
|
221
|
+
.config-header h2 {
|
|
222
|
+
font-size: 1.25em; font-weight: 700; color: var(--text); margin-bottom: 6px;
|
|
223
|
+
}
|
|
224
|
+
.config-header p {
|
|
225
|
+
font-size: 0.88em; color: var(--muted); margin-bottom: 12px;
|
|
226
|
+
}
|
|
227
|
+
.config-mode-badge {
|
|
228
|
+
display: inline-flex; align-items: center; gap: 6px;
|
|
229
|
+
background: #1f6feb22; border: 1px solid #58a6ff44;
|
|
230
|
+
color: var(--blue); font-size: 0.75em; font-weight: 600;
|
|
231
|
+
padding: 3px 10px; border-radius: 12px;
|
|
232
|
+
}
|
|
233
|
+
.config-mode-badge.auto { background: #23833022; border-color: #3fb95044; color: var(--green); }
|
|
234
|
+
|
|
235
|
+
/* LLM Grid */
|
|
236
|
+
.llm-grid {
|
|
237
|
+
display: grid;
|
|
238
|
+
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
|
239
|
+
gap: 12px;
|
|
240
|
+
max-width: 900px;
|
|
241
|
+
margin: 0 auto 28px;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
.llm-card {
|
|
245
|
+
background: var(--surface); border: 1px solid var(--border);
|
|
246
|
+
border-radius: 10px; padding: 16px;
|
|
247
|
+
display: flex; flex-direction: column; align-items: center; gap: 10px;
|
|
248
|
+
transition: border-color 0.2s, transform 0.15s, opacity 0.2s;
|
|
249
|
+
animation: fadeSlideIn 0.3s ease both;
|
|
250
|
+
cursor: pointer; user-select: none;
|
|
251
|
+
}
|
|
252
|
+
.llm-card:hover { border-color: var(--border-h); transform: translateY(-1px); }
|
|
253
|
+
.llm-card.enabled { border-color: var(--green); background: #23833010; }
|
|
254
|
+
.llm-card.disabled { opacity: 0.55; }
|
|
255
|
+
|
|
256
|
+
.llm-card-name {
|
|
257
|
+
font-size: 0.9em; font-weight: 600; color: var(--text); text-align: center;
|
|
258
|
+
}
|
|
259
|
+
.llm-card-cmd {
|
|
260
|
+
font-size: 0.75em; color: var(--muted); font-family: 'SF Mono', 'Fira Code', monospace;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/* Toggle switch */
|
|
264
|
+
.toggle-wrap { display: flex; align-items: center; gap: 8px; }
|
|
265
|
+
.toggle {
|
|
266
|
+
position: relative; display: inline-block; width: 36px; height: 20px;
|
|
267
|
+
}
|
|
268
|
+
.toggle input { opacity: 0; width: 0; height: 0; }
|
|
269
|
+
.toggle-track {
|
|
270
|
+
position: absolute; inset: 0;
|
|
271
|
+
background: var(--border); border-radius: 20px;
|
|
272
|
+
transition: background 0.2s;
|
|
273
|
+
cursor: pointer;
|
|
274
|
+
}
|
|
275
|
+
.toggle input:checked + .toggle-track { background: var(--green); }
|
|
276
|
+
.toggle-thumb {
|
|
277
|
+
position: absolute; top: 3px; left: 3px;
|
|
278
|
+
width: 14px; height: 14px;
|
|
279
|
+
background: white; border-radius: 50%;
|
|
280
|
+
transition: transform 0.2s;
|
|
281
|
+
pointer-events: none;
|
|
282
|
+
}
|
|
283
|
+
.toggle input:checked ~ .toggle-thumb { transform: translateX(16px); }
|
|
284
|
+
|
|
285
|
+
/* Config actions */
|
|
286
|
+
.config-actions {
|
|
287
|
+
display: flex; gap: 10px; align-items: center;
|
|
288
|
+
max-width: 900px; margin: 0 auto;
|
|
289
|
+
flex-wrap: wrap;
|
|
290
|
+
}
|
|
291
|
+
.btn-primary {
|
|
292
|
+
background: var(--blue); border: 1px solid #79b8ff44;
|
|
293
|
+
color: #0d1117; font-weight: 700;
|
|
294
|
+
padding: 7px 18px; border-radius: 6px; cursor: pointer; font-size: 0.85em;
|
|
295
|
+
transition: background 0.15s, transform 0.1s;
|
|
296
|
+
}
|
|
297
|
+
.btn-primary:hover { background: #79b8ff; transform: translateY(-1px); }
|
|
298
|
+
.btn-secondary {
|
|
299
|
+
background: #21262d; border: 1px solid var(--border);
|
|
300
|
+
color: var(--text); font-weight: 500;
|
|
301
|
+
padding: 7px 18px; border-radius: 6px; cursor: pointer; font-size: 0.85em;
|
|
302
|
+
transition: border-color 0.15s, background 0.15s;
|
|
303
|
+
}
|
|
304
|
+
.btn-secondary:hover { border-color: var(--muted); background: #1c2128; }
|
|
305
|
+
|
|
306
|
+
/* Gear button */
|
|
307
|
+
.btn-gear {
|
|
308
|
+
background: #21262d; border: 1px solid var(--border); color: var(--muted);
|
|
309
|
+
width: 30px; height: 30px; border-radius: 6px; cursor: pointer; font-size: 1em;
|
|
310
|
+
display: flex; align-items: center; justify-content: center;
|
|
311
|
+
transition: border-color 0.15s, color 0.15s, background 0.15s;
|
|
312
|
+
}
|
|
313
|
+
.btn-gear:hover { border-color: var(--blue); color: var(--blue); background: #1c2128; }
|
|
314
|
+
.btn-gear.active { border-color: var(--blue); color: var(--blue); background: #1f6feb22; }
|
|
315
|
+
|
|
316
|
+
/* Toast */
|
|
317
|
+
.toast {
|
|
318
|
+
position: fixed; top: 64px; left: 50%; transform: translateX(-50%);
|
|
319
|
+
z-index: 200; padding: 10px 20px; border-radius: 8px; font-size: 0.85em; font-weight: 600;
|
|
320
|
+
white-space: nowrap; pointer-events: none;
|
|
321
|
+
animation: toastIn 0.25s ease;
|
|
322
|
+
box-shadow: 0 4px 16px #00000066;
|
|
323
|
+
}
|
|
324
|
+
.toast.success { background: #238330cc; border: 1px solid var(--green); color: #fff; }
|
|
325
|
+
.toast.error { background: #b91c1c99; border: 1px solid var(--red); color: #fff; }
|
|
326
|
+
@keyframes toastIn { from { opacity: 0; top: 50px; } to { opacity: 1; top: 64px; } }
|
|
327
|
+
@keyframes toastOut { from { opacity: 1; } to { opacity: 0; } }
|
|
328
|
+
.toast.hiding { animation: toastOut 0.35s ease forwards; }
|
|
329
|
+
|
|
330
|
+
/* ββ Responsive βββββββββββββββββββββββββββββββββββββββββ */
|
|
331
|
+
@media (max-width: 640px) {
|
|
332
|
+
#sidebar { display: none; }
|
|
333
|
+
.grid { grid-template-columns: 1fr; }
|
|
334
|
+
.bubble { max-width: 140px; }
|
|
335
|
+
.llm-grid { grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); }
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/* βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
339
|
+
FEATURE 1: CLI Install Status Dots
|
|
340
|
+
ββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
|
341
|
+
.cli-status-dot {
|
|
342
|
+
width: 8px; height: 8px; border-radius: 50%;
|
|
343
|
+
display: inline-block; margin-left: 6px;
|
|
344
|
+
flex-shrink: 0;
|
|
345
|
+
}
|
|
346
|
+
.cli-status-dot.installed { background: var(--green); box-shadow: 0 0 4px var(--green); }
|
|
347
|
+
.cli-status-dot.not-installed { background: var(--red); }
|
|
348
|
+
.cli-status-dot.loading { background: var(--muted); }
|
|
349
|
+
|
|
350
|
+
/* βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
351
|
+
FEATURE 2: Browser LLM Tabs
|
|
352
|
+
ββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
|
353
|
+
.browser-tabs-section {
|
|
354
|
+
margin: 20px auto 0;
|
|
355
|
+
padding: 16px;
|
|
356
|
+
background: var(--surface); border: 1px solid var(--border);
|
|
357
|
+
border-radius: 10px; max-width: 900px;
|
|
358
|
+
}
|
|
359
|
+
.section-header {
|
|
360
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
361
|
+
cursor: pointer; user-select: none;
|
|
362
|
+
}
|
|
363
|
+
.section-header h3 { font-size: 0.9em; color: var(--text); margin: 0; font-weight: 600; }
|
|
364
|
+
.section-toggle { color: var(--muted); font-size: 0.8em; transition: transform 0.2s; display: inline-block; }
|
|
365
|
+
.section-toggle.open { transform: rotate(90deg); }
|
|
366
|
+
.browser-tabs-body { margin-top: 14px; }
|
|
367
|
+
.browser-tab-item {
|
|
368
|
+
display: flex; align-items: center; gap: 10px;
|
|
369
|
+
padding: 8px 12px; border-radius: 6px; background: var(--bg);
|
|
370
|
+
margin-bottom: 6px; font-size: 0.85em;
|
|
371
|
+
}
|
|
372
|
+
.browser-tab-item .tab-title { color: var(--text); flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
373
|
+
.browser-tab-item .tab-url { color: var(--muted); font-size: 0.8em; max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
374
|
+
.cdp-status { font-size: 0.8em; color: var(--muted); margin-bottom: 10px; }
|
|
375
|
+
.cdp-status.available { color: var(--green); }
|
|
376
|
+
.cdp-status.unavailable { color: var(--red); }
|
|
377
|
+
|
|
378
|
+
/* βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
379
|
+
FEATURE 3: Quick Start FAB + Modal
|
|
380
|
+
ββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
|
381
|
+
.fab {
|
|
382
|
+
position: fixed; bottom: 24px; right: 24px; z-index: 90;
|
|
383
|
+
width: 56px; height: 56px; border-radius: 50%;
|
|
384
|
+
background: var(--blue); color: white; border: none;
|
|
385
|
+
font-size: 1.4em; cursor: pointer; box-shadow: 0 4px 12px rgba(0,0,0,0.4);
|
|
386
|
+
transition: transform 0.15s, box-shadow 0.15s;
|
|
387
|
+
display: flex; align-items: center; justify-content: center;
|
|
388
|
+
}
|
|
389
|
+
.fab:hover { transform: scale(1.1); box-shadow: 0 6px 20px rgba(0,0,0,0.5); }
|
|
390
|
+
|
|
391
|
+
.modal-overlay {
|
|
392
|
+
position: fixed; inset: 0; z-index: 150;
|
|
393
|
+
background: rgba(0,0,0,0.6); display: flex;
|
|
394
|
+
align-items: center; justify-content: center;
|
|
395
|
+
}
|
|
396
|
+
.modal {
|
|
397
|
+
background: var(--surface); border: 1px solid var(--border);
|
|
398
|
+
border-radius: 12px; padding: 24px; width: 90%; max-width: 480px;
|
|
399
|
+
animation: fadeSlideIn 0.25s ease;
|
|
400
|
+
}
|
|
401
|
+
.modal h2 { font-size: 1.1em; margin-bottom: 18px; color: var(--text); }
|
|
402
|
+
.form-group { margin-bottom: 16px; }
|
|
403
|
+
.form-group label {
|
|
404
|
+
display: block; font-size: 0.8em; color: var(--muted);
|
|
405
|
+
margin-bottom: 6px; font-weight: 600;
|
|
406
|
+
text-transform: uppercase; letter-spacing: 0.05em;
|
|
407
|
+
}
|
|
408
|
+
.form-input {
|
|
409
|
+
width: 100%; padding: 10px 12px; background: var(--bg);
|
|
410
|
+
border: 1px solid var(--border); border-radius: 6px;
|
|
411
|
+
color: var(--text); font-size: 0.9em; outline: none;
|
|
412
|
+
transition: border-color 0.15s; font-family: inherit;
|
|
413
|
+
}
|
|
414
|
+
.form-input:focus { border-color: var(--blue); }
|
|
415
|
+
.form-input-sm { width: 80px; }
|
|
416
|
+
|
|
417
|
+
.speaker-chips { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
418
|
+
.speaker-chip {
|
|
419
|
+
display: flex; align-items: center; gap: 6px;
|
|
420
|
+
padding: 6px 12px; border-radius: 20px;
|
|
421
|
+
background: var(--bg); border: 1px solid var(--border);
|
|
422
|
+
cursor: pointer; font-size: 0.82em; color: var(--muted);
|
|
423
|
+
transition: all 0.15s; user-select: none;
|
|
424
|
+
}
|
|
425
|
+
.speaker-chip.selected {
|
|
426
|
+
border-color: var(--blue); background: #58a6ff18; color: var(--text);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
.modal-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 20px; }
|
|
430
|
+
|
|
431
|
+
/* βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
432
|
+
FEATURE 4: LLM Statistics Screen
|
|
433
|
+
ββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
|
434
|
+
#screen-stats { padding: 0; overflow: hidden; }
|
|
435
|
+
#screen-stats > div { flex: 1; }
|
|
436
|
+
|
|
437
|
+
.stats-summary {
|
|
438
|
+
display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
|
439
|
+
gap: 12px; margin-bottom: 24px;
|
|
440
|
+
}
|
|
441
|
+
.stat-card {
|
|
442
|
+
background: var(--surface); border: 1px solid var(--border);
|
|
443
|
+
border-radius: 10px; padding: 16px; text-align: center;
|
|
444
|
+
animation: fadeSlideIn 0.3s ease both;
|
|
445
|
+
}
|
|
446
|
+
.stat-value { font-size: 1.8em; font-weight: 700; color: var(--blue); }
|
|
447
|
+
.stat-label { font-size: 0.75em; color: var(--muted); text-transform: uppercase; letter-spacing: 0.08em; margin-top: 4px; }
|
|
448
|
+
|
|
449
|
+
.stats-speakers { display: flex; flex-direction: column; gap: 10px; }
|
|
450
|
+
.speaker-stat-row {
|
|
451
|
+
display: flex; align-items: center; gap: 14px;
|
|
452
|
+
background: var(--surface); border: 1px solid var(--border);
|
|
453
|
+
border-radius: 8px; padding: 14px 16px;
|
|
454
|
+
animation: fadeSlideIn 0.3s ease both;
|
|
455
|
+
}
|
|
456
|
+
.speaker-stat-info { flex: 1; }
|
|
457
|
+
.speaker-stat-name { font-weight: 600; font-size: 0.9em; color: var(--text); }
|
|
458
|
+
.speaker-stat-detail { font-size: 0.78em; color: var(--muted); margin-top: 2px; }
|
|
459
|
+
.speaker-stat-bar { width: 120px; }
|
|
460
|
+
.mini-bar { height: 6px; background: var(--border); border-radius: 3px; overflow: hidden; margin-top: 4px; }
|
|
461
|
+
.mini-bar-fill { height: 100%; border-radius: 3px; transition: width 0.4s; }
|
|
462
|
+
</style>
|
|
463
|
+
</head>
|
|
464
|
+
<body>
|
|
465
|
+
<div id="app">
|
|
466
|
+
<!-- ββ Header βββββββββββββββββββββββββββββββββββββββ -->
|
|
467
|
+
<header>
|
|
468
|
+
<h1 id="header-title">π Deliberation Observer</h1>
|
|
469
|
+
<div class="header-actions">
|
|
470
|
+
<button class="btn back" id="btn-back" style="display:none">
|
|
471
|
+
β Back
|
|
472
|
+
</button>
|
|
473
|
+
<div id="conn-status"></div>
|
|
474
|
+
<button class="btn-gear" id="btn-stats" title="LLM Statistics" onclick="showStats()">π</button>
|
|
475
|
+
<button class="btn-gear" id="btn-gear" title="LLM Configuration" onclick="showConfig()">β</button>
|
|
476
|
+
<button class="btn" id="btn-refresh" onclick="loadSessions()">β» Refresh</button>
|
|
477
|
+
</div>
|
|
478
|
+
</header>
|
|
479
|
+
|
|
480
|
+
<!-- ββ Screen 1: Session List βββββββββββββββββββββββ -->
|
|
481
|
+
<div id="screen-list" class="screen active">
|
|
482
|
+
<div class="grid" id="session-grid">
|
|
483
|
+
<div class="empty"><div class="spinner"></div>Loading sessionsβ¦</div>
|
|
484
|
+
</div>
|
|
485
|
+
</div>
|
|
486
|
+
|
|
487
|
+
<!-- ββ Screen 2: Session Detail βββββββββββββββββββββ -->
|
|
488
|
+
<div id="screen-detail" class="screen">
|
|
489
|
+
<!-- Speaker top bar -->
|
|
490
|
+
<div class="speaker-bar" id="speaker-bar"></div>
|
|
491
|
+
|
|
492
|
+
<!-- Body -->
|
|
493
|
+
<div class="detail-body">
|
|
494
|
+
<div id="chat-log"></div>
|
|
495
|
+
|
|
496
|
+
<!-- Sidebar -->
|
|
497
|
+
<div id="sidebar">
|
|
498
|
+
<!-- Round progress -->
|
|
499
|
+
<div class="sidebar-section" id="sb-round">
|
|
500
|
+
<h3>Round Progress</h3>
|
|
501
|
+
<div class="progress-label"><span id="sb-round-label">β</span></div>
|
|
502
|
+
<div class="progress-track"><div class="progress-fill" id="sb-round-fill" style="width:0%"></div></div>
|
|
503
|
+
</div>
|
|
504
|
+
|
|
505
|
+
<!-- Vote tally -->
|
|
506
|
+
<div class="sidebar-section" id="sb-votes">
|
|
507
|
+
<h3>Vote Tally</h3>
|
|
508
|
+
<div class="vote-bar-wrap">
|
|
509
|
+
<div class="vote-row">
|
|
510
|
+
<span class="vote-label">β
AGREE</span>
|
|
511
|
+
<div class="vote-track"><div class="vote-fill agree" id="vf-agree" style="width:0%"></div></div>
|
|
512
|
+
<span class="vote-count" id="vc-agree">0</span>
|
|
513
|
+
</div>
|
|
514
|
+
<div class="vote-row">
|
|
515
|
+
<span class="vote-label">β DISAGREE</span>
|
|
516
|
+
<div class="vote-track"><div class="vote-fill disagree" id="vf-disagree" style="width:0%"></div></div>
|
|
517
|
+
<span class="vote-count" id="vc-disagree">0</span>
|
|
518
|
+
</div>
|
|
519
|
+
<div class="vote-row">
|
|
520
|
+
<span class="vote-label">π‘ CONDITIONAL</span>
|
|
521
|
+
<div class="vote-track"><div class="vote-fill conditional" id="vf-conditional" style="width:0%"></div></div>
|
|
522
|
+
<span class="vote-count" id="vc-conditional">0</span>
|
|
523
|
+
</div>
|
|
524
|
+
</div>
|
|
525
|
+
</div>
|
|
526
|
+
|
|
527
|
+
<!-- Degradation -->
|
|
528
|
+
<div class="sidebar-section" id="sb-deg">
|
|
529
|
+
<h3>Degradation</h3>
|
|
530
|
+
<div id="deg-list"></div>
|
|
531
|
+
</div>
|
|
532
|
+
|
|
533
|
+
<!-- Ordering / Roles -->
|
|
534
|
+
<div class="sidebar-section" id="sb-roles">
|
|
535
|
+
<h3>Roles</h3>
|
|
536
|
+
<div id="roles-list"></div>
|
|
537
|
+
</div>
|
|
538
|
+
</div>
|
|
539
|
+
</div>
|
|
540
|
+
</div>
|
|
541
|
+
|
|
542
|
+
<!-- ββ Screen 3: LLM Configuration ββββββββββββββββββ -->
|
|
543
|
+
<div id="screen-config" class="screen">
|
|
544
|
+
<div class="config-header">
|
|
545
|
+
<h2>LLM Configuration</h2>
|
|
546
|
+
<p>ν λ‘ μ μ°Έμ¬ν LLMμ μ ννμΈμ / Select LLMs for deliberation</p>
|
|
547
|
+
<span class="config-mode-badge auto" id="config-mode-badge">Auto-detect</span>
|
|
548
|
+
</div>
|
|
549
|
+
|
|
550
|
+
<div class="llm-grid" id="llm-grid">
|
|
551
|
+
<!-- Cards injected by renderLlmGrid() -->
|
|
552
|
+
</div>
|
|
553
|
+
|
|
554
|
+
<div class="config-actions">
|
|
555
|
+
<button class="btn-primary" onclick="saveConfig()">Save</button>
|
|
556
|
+
<button class="btn-secondary" onclick="resetConfig()">Reset to Auto-detect</button>
|
|
557
|
+
</div>
|
|
558
|
+
|
|
559
|
+
<!-- Feature 2: Browser LLM Tabs -->
|
|
560
|
+
<div class="browser-tabs-section">
|
|
561
|
+
<div class="section-header" onclick="toggleBrowserTabs()">
|
|
562
|
+
<h3>π Browser LLM Tabs</h3>
|
|
563
|
+
<span class="section-toggle" id="browser-toggle">βΆ</span>
|
|
564
|
+
</div>
|
|
565
|
+
<div class="browser-tabs-body" id="browser-tabs-body" style="display:none">
|
|
566
|
+
<div id="browser-tabs-list"></div>
|
|
567
|
+
<button class="btn" onclick="loadBrowserTabs()" style="margin-top:8px">β» Refresh</button>
|
|
568
|
+
</div>
|
|
569
|
+
</div>
|
|
570
|
+
</div>
|
|
571
|
+
|
|
572
|
+
<!-- ββ Screen 4: LLM Statistics βββββββββββββββββββββ -->
|
|
573
|
+
<div id="screen-stats" class="screen">
|
|
574
|
+
<div style="padding:20px;overflow-y:auto;flex:1;max-width:900px;margin:0 auto;width:100%">
|
|
575
|
+
<div class="config-header">
|
|
576
|
+
<h2>π LLM Statistics</h2>
|
|
577
|
+
<p>ν λ‘ μ°Έμ¬ ν΅κ³ / Deliberation participation stats</p>
|
|
578
|
+
</div>
|
|
579
|
+
<div class="stats-summary" id="stats-summary"></div>
|
|
580
|
+
<div class="stats-speakers" id="stats-speakers"></div>
|
|
581
|
+
</div>
|
|
582
|
+
</div>
|
|
583
|
+
|
|
584
|
+
<!-- Feature 3: FAB -->
|
|
585
|
+
<button class="fab" id="fab-start" onclick="showQuickStart()" title="New Deliberation">+</button>
|
|
586
|
+
|
|
587
|
+
<!-- Feature 3: Quick Start Modal -->
|
|
588
|
+
<div class="modal-overlay" id="quick-start-modal" style="display:none" onclick="if(event.target===this)hideQuickStart()">
|
|
589
|
+
<div class="modal">
|
|
590
|
+
<h2>π New Deliberation</h2>
|
|
591
|
+
<div class="form-group">
|
|
592
|
+
<label>Topic</label>
|
|
593
|
+
<input type="text" id="qs-topic" placeholder="ν λ‘ μ£Όμ λ₯Ό μ
λ ₯νμΈμ" class="form-input">
|
|
594
|
+
</div>
|
|
595
|
+
<div class="form-group">
|
|
596
|
+
<label>Speakers</label>
|
|
597
|
+
<div class="speaker-chips" id="qs-speakers"></div>
|
|
598
|
+
</div>
|
|
599
|
+
<div class="form-group">
|
|
600
|
+
<label>Max Rounds</label>
|
|
601
|
+
<input type="number" id="qs-rounds" value="3" min="1" max="20" class="form-input form-input-sm">
|
|
602
|
+
</div>
|
|
603
|
+
<div class="modal-actions">
|
|
604
|
+
<button class="btn" onclick="hideQuickStart()">Cancel</button>
|
|
605
|
+
<button class="btn-primary" onclick="startDeliberation()">Start</button>
|
|
606
|
+
</div>
|
|
607
|
+
</div>
|
|
608
|
+
</div>
|
|
609
|
+
</div>
|
|
610
|
+
|
|
611
|
+
<script>
|
|
612
|
+
/* ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
613
|
+
DATA
|
|
614
|
+
ββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
|
615
|
+
let sessions = [];
|
|
616
|
+
let currentSession = null;
|
|
617
|
+
let currentStream = null;
|
|
618
|
+
let previousScreen = 'list'; // tracks where to go Back from config
|
|
619
|
+
|
|
620
|
+
/* ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
621
|
+
MASCOT SYSTEM
|
|
622
|
+
ββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
|
623
|
+
const MASCOTS = {
|
|
624
|
+
claude: { initial: "C", color: "#D4744E", shape: "circle" },
|
|
625
|
+
chatgpt: { initial: "G", color: "#10A37F", shape: "circle" },
|
|
626
|
+
gemini: { initial: "Gm", color: "#4285F4", shape: "diamond" },
|
|
627
|
+
codex: { initial: "Cx", color: "#171717", shape: "square" },
|
|
628
|
+
copilot: { initial: "Co", color: "#0078D4", shape: "hexagon" },
|
|
629
|
+
perplexity: { initial: "P", color: "#20B8CD", shape: "circle" },
|
|
630
|
+
deepseek: { initial: "D", color: "#4D6BFF", shape: "circle" },
|
|
631
|
+
qwen: { initial: "Q", color: "#6F42C1", shape: "circle" },
|
|
632
|
+
aigentry: { initial: "A", color: "#888888", shape: "star" },
|
|
633
|
+
};
|
|
634
|
+
|
|
635
|
+
/** Derive a stable HSL color from a speaker name string */
|
|
636
|
+
function hashColor(name) {
|
|
637
|
+
let h = 0;
|
|
638
|
+
for (let i = 0; i < name.length; i++) h = (h * 31 + name.charCodeAt(i)) & 0xFFFFFF;
|
|
639
|
+
return `hsl(${h % 360}, 55%, 55%)`;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/** Return mascot config (or generated fallback) for a speaker */
|
|
643
|
+
function getMascot(speaker) {
|
|
644
|
+
const key = (speaker || '').toLowerCase().split(/[-_\s]/)[0];
|
|
645
|
+
if (MASCOTS[key]) return MASCOTS[key];
|
|
646
|
+
return {
|
|
647
|
+
initial: speaker.charAt(0).toUpperCase(),
|
|
648
|
+
color: hashColor(speaker),
|
|
649
|
+
shape: 'circle',
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/** Render an SVG avatar. size in px, state: 'idle'|'speaking'|'done' */
|
|
654
|
+
function renderAvatar(speaker, state = 'done', size = 36) {
|
|
655
|
+
const m = getMascot(speaker);
|
|
656
|
+
const s = size;
|
|
657
|
+
const cx = s / 2;
|
|
658
|
+
const fs = s * 0.36;
|
|
659
|
+
const col = m.color;
|
|
660
|
+
const cls = `avatar ${state}`;
|
|
661
|
+
|
|
662
|
+
let shape = '';
|
|
663
|
+
if (m.shape === 'circle') {
|
|
664
|
+
shape = `<circle cx="${cx}" cy="${cx}" r="${cx - 1}" fill="${col}" />`;
|
|
665
|
+
} else if (m.shape === 'diamond') {
|
|
666
|
+
const d = cx - 1;
|
|
667
|
+
shape = `<polygon points="${cx},1 ${s-1},${cx} ${cx},${s-1} 1,${cx}" fill="${col}" />`;
|
|
668
|
+
} else if (m.shape === 'square') {
|
|
669
|
+
shape = `<rect x="1" y="1" width="${s-2}" height="${s-2}" rx="4" fill="${col}" />`;
|
|
670
|
+
} else if (m.shape === 'hexagon') {
|
|
671
|
+
const r = cx - 1, h = r * Math.sin(Math.PI / 3);
|
|
672
|
+
const pts = [
|
|
673
|
+
[cx, cx - r], [cx + h, cx - r/2], [cx + h, cx + r/2],
|
|
674
|
+
[cx, cx + r], [cx - h, cx + r/2], [cx - h, cx - r/2]
|
|
675
|
+
].map(p => p.join(',')).join(' ');
|
|
676
|
+
shape = `<polygon points="${pts}" fill="${col}" />`;
|
|
677
|
+
} else if (m.shape === 'star') {
|
|
678
|
+
const r1 = cx - 1, r2 = r1 * 0.45;
|
|
679
|
+
const pts = [];
|
|
680
|
+
for (let i = 0; i < 10; i++) {
|
|
681
|
+
const a = (Math.PI / 5) * i - Math.PI / 2;
|
|
682
|
+
const r = i % 2 === 0 ? r1 : r2;
|
|
683
|
+
pts.push([cx + r * Math.cos(a), cx + r * Math.sin(a)].join(','));
|
|
684
|
+
}
|
|
685
|
+
shape = `<polygon points="${pts.join(' ')}" fill="${col}" />`;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
return `<span class="${cls}" title="${escHtml(speaker)}">
|
|
689
|
+
<svg width="${s}" height="${s}" viewBox="0 0 ${s} ${s}" xmlns="http://www.w3.org/2000/svg">
|
|
690
|
+
${shape}
|
|
691
|
+
<text x="${cx}" y="${cx}" text-anchor="middle" dominant-baseline="central"
|
|
692
|
+
fill="white" font-size="${fs}" font-weight="700"
|
|
693
|
+
font-family="-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif">${escHtml(m.initial)}</text>
|
|
694
|
+
</svg>
|
|
695
|
+
</span>`;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/** Render a speech bubble tooltip for an avatar slot */
|
|
699
|
+
function renderSpeechBubble(text) {
|
|
700
|
+
if (!text) return '';
|
|
701
|
+
const preview = text.length > 50 ? text.substring(0, 50) + 'β¦' : text;
|
|
702
|
+
return `<span class="bubble">${escHtml(preview)}</span>`;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
/* ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
706
|
+
API
|
|
707
|
+
ββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
|
708
|
+
async function loadSessions() {
|
|
709
|
+
try {
|
|
710
|
+
const res = await fetch('/api/sessions');
|
|
711
|
+
sessions = await res.json();
|
|
712
|
+
renderSessionList(sessions);
|
|
713
|
+
} catch (e) {
|
|
714
|
+
document.getElementById('session-grid').innerHTML =
|
|
715
|
+
`<div class="empty">Connection failed: ${escHtml(e.message)}</div>`;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
function connectSSE(sessionId) {
|
|
720
|
+
if (currentStream) { currentStream.close(); currentStream = null; }
|
|
721
|
+
|
|
722
|
+
const es = new EventSource(`/api/sessions/${sessionId}/stream`);
|
|
723
|
+
currentStream = es;
|
|
724
|
+
|
|
725
|
+
es.addEventListener('connected', () => setConnStatus('connected'));
|
|
726
|
+
|
|
727
|
+
es.addEventListener('snapshot', (e) => {
|
|
728
|
+
const data = JSON.parse(e.data);
|
|
729
|
+
currentSession = data;
|
|
730
|
+
renderSessionDetail(data);
|
|
731
|
+
setConnStatus('connected');
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
es.addEventListener('turn', (e) => {
|
|
735
|
+
const entry = JSON.parse(e.data);
|
|
736
|
+
if (currentSession) {
|
|
737
|
+
currentSession.log = currentSession.log || [];
|
|
738
|
+
currentSession.log.push(entry);
|
|
739
|
+
}
|
|
740
|
+
addLogEntry(entry);
|
|
741
|
+
renderVoteTally(currentSession ? currentSession.log : []);
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
es.addEventListener('status', (e) => {
|
|
745
|
+
const data = JSON.parse(e.data);
|
|
746
|
+
if (currentSession) Object.assign(currentSession, data);
|
|
747
|
+
updateSpeakerBar(data.current_speaker, currentSession ? currentSession.log : []);
|
|
748
|
+
updateRoundProgress(data.current_round, data.max_rounds);
|
|
749
|
+
renderDegradation(data.degradation);
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
es.onerror = () => {
|
|
753
|
+
setConnStatus('disconnected');
|
|
754
|
+
setTimeout(() => {
|
|
755
|
+
if (currentSession) connectSSE(currentSession.id || currentSession.session_id);
|
|
756
|
+
}, 3000);
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
/* ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
761
|
+
RENDERING β Session List
|
|
762
|
+
ββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
|
763
|
+
function renderSessionList(list) {
|
|
764
|
+
const grid = document.getElementById('session-grid');
|
|
765
|
+
if (!list || list.length === 0) {
|
|
766
|
+
grid.innerHTML = '<div class="empty">No active sessions</div>';
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
grid.innerHTML = list.map(s => {
|
|
771
|
+
const pct = s.max_rounds > 0 ? Math.round((s.current_round / s.max_rounds) * 100) : 0;
|
|
772
|
+
const speakers = (s.speakers || []);
|
|
773
|
+
const avatarRow = speakers.slice(0, 8).map(sp =>
|
|
774
|
+
renderAvatar(sp, sp === s.current_speaker ? 'speaking' : 'done', 28)
|
|
775
|
+
).join('');
|
|
776
|
+
const created = s.created_at ? new Date(s.created_at).toLocaleTimeString() : '';
|
|
777
|
+
const borderCls = s.status === 'active' ? 'active-border' : '';
|
|
778
|
+
|
|
779
|
+
return `<div class="session-card ${borderCls}" onclick="showDetail('${escHtml(s.id || s.session_id)}')">
|
|
780
|
+
<div class="card-header">
|
|
781
|
+
<span class="card-topic">${escHtml(s.topic || 'Untitled')}</span>
|
|
782
|
+
<span class="badge ${escHtml(s.status || 'active')}">${escHtml(s.status || 'active')}</span>
|
|
783
|
+
</div>
|
|
784
|
+
<div class="avatar-row">${avatarRow}</div>
|
|
785
|
+
<div class="progress-wrap">
|
|
786
|
+
<div class="progress-label">
|
|
787
|
+
<span>Round ${s.current_round || 0} / ${s.max_rounds || '?'}</span>
|
|
788
|
+
<span>${pct}%</span>
|
|
789
|
+
</div>
|
|
790
|
+
<div class="progress-track">
|
|
791
|
+
<div class="progress-fill ${s.status === 'synthesized' ? 'green' : ''}" style="width:${pct}%"></div>
|
|
792
|
+
</div>
|
|
793
|
+
</div>
|
|
794
|
+
<div class="card-meta">
|
|
795
|
+
<span>π€ ${speakers.length} speaker${speakers.length !== 1 ? 's' : ''}</span>
|
|
796
|
+
<span>π¬ ${s.log_count || 0} entries</span>
|
|
797
|
+
${created ? `<span>π ${created}</span>` : ''}
|
|
798
|
+
</div>
|
|
799
|
+
</div>`;
|
|
800
|
+
}).join('');
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
/* ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
804
|
+
RENDERING β Session Detail
|
|
805
|
+
ββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
|
806
|
+
function renderSessionDetail(data) {
|
|
807
|
+
// Chat log
|
|
808
|
+
const log = document.getElementById('chat-log');
|
|
809
|
+
log.innerHTML = '';
|
|
810
|
+
(data.log || []).forEach(entry => addLogEntry(entry, false));
|
|
811
|
+
log.scrollTop = log.scrollHeight;
|
|
812
|
+
|
|
813
|
+
// Speaker bar
|
|
814
|
+
updateSpeakerBar(data.current_speaker, data.log || []);
|
|
815
|
+
|
|
816
|
+
// Round progress
|
|
817
|
+
updateRoundProgress(data.current_round, data.max_rounds);
|
|
818
|
+
|
|
819
|
+
// Vote tally
|
|
820
|
+
renderVoteTally(data.log || []);
|
|
821
|
+
|
|
822
|
+
// Degradation
|
|
823
|
+
renderDegradation(data.degradation);
|
|
824
|
+
|
|
825
|
+
// Roles
|
|
826
|
+
renderRoles(data.roles || data.role_assignments);
|
|
827
|
+
|
|
828
|
+
// Ordering badge
|
|
829
|
+
if (data.ordering_strategy) {
|
|
830
|
+
const h = document.querySelector('#sb-roles h3');
|
|
831
|
+
if (h) h.textContent = `Roles Β· ${data.ordering_strategy}`;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// Header title
|
|
835
|
+
document.getElementById('header-title').textContent =
|
|
836
|
+
'π ' + (data.topic || 'Session');
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
function updateSpeakerBar(currentSpeaker, log) {
|
|
840
|
+
const bar = document.getElementById('speaker-bar');
|
|
841
|
+
if (!currentSession) return;
|
|
842
|
+
const speakers = currentSession.speakers || [];
|
|
843
|
+
|
|
844
|
+
// Build last-response map
|
|
845
|
+
const lastMsg = {};
|
|
846
|
+
(log || []).forEach(e => { if (e.speaker) lastMsg[e.speaker] = e.content || ''; });
|
|
847
|
+
|
|
848
|
+
bar.innerHTML = speakers.map(sp => {
|
|
849
|
+
const isCurrent = sp === currentSpeaker;
|
|
850
|
+
const state = isCurrent ? 'speaking' : (lastMsg[sp] ? 'done' : 'idle');
|
|
851
|
+
const bubble = renderSpeechBubble(lastMsg[sp]);
|
|
852
|
+
return `<div class="speaker-slot ${isCurrent ? 'current-speaker' : ''}">
|
|
853
|
+
<div class="avatar ${state}" style="position:relative">
|
|
854
|
+
${renderAvatar(sp, state, 40).replace(/^<span[^>]*>/, '').replace(/<\/span>$/, '')}
|
|
855
|
+
${bubble}
|
|
856
|
+
</div>
|
|
857
|
+
<span class="speaker-name">${escHtml(sp)}</span>
|
|
858
|
+
</div>`;
|
|
859
|
+
}).join('');
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
function updateRoundProgress(current, max) {
|
|
863
|
+
const pct = max > 0 ? Math.round((current / max) * 100) : 0;
|
|
864
|
+
const lbl = document.getElementById('sb-round-label');
|
|
865
|
+
const fill = document.getElementById('sb-round-fill');
|
|
866
|
+
if (lbl) lbl.textContent = `Round ${current || 0} / ${max || '?'}`;
|
|
867
|
+
if (fill) fill.style.width = pct + '%';
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
/* ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
871
|
+
RENDERING β Chat log entry
|
|
872
|
+
ββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
|
873
|
+
function addLogEntry(entry, animate = true) {
|
|
874
|
+
const log = document.getElementById('chat-log');
|
|
875
|
+
const el = document.createElement('div');
|
|
876
|
+
el.className = 'log-entry';
|
|
877
|
+
if (!animate) el.style.animation = 'none';
|
|
878
|
+
|
|
879
|
+
const m = getMascot(entry.speaker || '');
|
|
880
|
+
const color = m.color;
|
|
881
|
+
el.style.borderLeft = `3px solid ${color}`;
|
|
882
|
+
|
|
883
|
+
const content = (entry.content || '');
|
|
884
|
+
const preview = content.length > 800 ? content.substring(0, 800) + '\n\n[truncatedβ¦]' : content;
|
|
885
|
+
|
|
886
|
+
el.innerHTML = `
|
|
887
|
+
<div class="entry-avatar">${renderAvatar(entry.speaker, 'done', 32)}</div>
|
|
888
|
+
<div class="entry-body">
|
|
889
|
+
<div class="entry-header">
|
|
890
|
+
<span class="entry-speaker" style="color:${color}">${escHtml(entry.speaker || '?')}</span>
|
|
891
|
+
${entry.round != null ? `<span class="entry-round">Round ${entry.round}</span>` : ''}
|
|
892
|
+
${entry.role_drift ? `<span class="entry-drift">β drift</span>` : ''}
|
|
893
|
+
</div>
|
|
894
|
+
<div class="entry-content">${escHtml(preview)}</div>
|
|
895
|
+
</div>`;
|
|
896
|
+
|
|
897
|
+
log.appendChild(el);
|
|
898
|
+
if (animate) el.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
/* ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
902
|
+
RENDERING β Vote tally
|
|
903
|
+
ββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
|
904
|
+
function renderVoteTally(log) {
|
|
905
|
+
const counts = { agree: 0, disagree: 0, conditional: 0 };
|
|
906
|
+
(log || []).forEach(entry => {
|
|
907
|
+
(entry.votes || []).forEach(v => {
|
|
908
|
+
const key = (v || '').toLowerCase();
|
|
909
|
+
if (key in counts) counts[key]++;
|
|
910
|
+
});
|
|
911
|
+
});
|
|
912
|
+
|
|
913
|
+
const total = counts.agree + counts.disagree + counts.conditional || 1;
|
|
914
|
+
['agree', 'disagree', 'conditional'].forEach(k => {
|
|
915
|
+
const fill = document.getElementById(`vf-${k}`);
|
|
916
|
+
const count = document.getElementById(`vc-${k}`);
|
|
917
|
+
if (fill) fill.style.width = Math.round((counts[k] / total) * 100) + '%';
|
|
918
|
+
if (count) count.textContent = counts[k];
|
|
919
|
+
});
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
/* ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
923
|
+
RENDERING β Degradation
|
|
924
|
+
ββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
|
925
|
+
function renderDegradation(deg) {
|
|
926
|
+
const list = document.getElementById('deg-list');
|
|
927
|
+
if (!list) return;
|
|
928
|
+
if (!deg || typeof deg !== 'object') { list.innerHTML = '<span style="color:var(--muted);font-size:0.8em">No data</span>'; return; }
|
|
929
|
+
|
|
930
|
+
const tiers = { ok: 'π’', warn: 'π‘', error: 'π΄', monitoring: 'π’', browser: 'π‘', terminal: 'π΄' };
|
|
931
|
+
list.innerHTML = Object.entries(deg).map(([key, val]) => {
|
|
932
|
+
const dot = tiers[val] || tiers[key] || 'βͺ';
|
|
933
|
+
return `<div class="deg-row"><span class="deg-dot">${dot}</span><span class="deg-label">${escHtml(key)}</span><span style="color:var(--muted);font-size:0.75em">${escHtml(String(val))}</span></div>`;
|
|
934
|
+
}).join('');
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
/* ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
938
|
+
RENDERING β Roles
|
|
939
|
+
ββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
|
940
|
+
function renderRoles(roles) {
|
|
941
|
+
const list = document.getElementById('roles-list');
|
|
942
|
+
if (!list) return;
|
|
943
|
+
if (!roles || typeof roles !== 'object') { list.innerHTML = '<span style="color:var(--muted);font-size:0.8em">No roles</span>'; return; }
|
|
944
|
+
|
|
945
|
+
list.innerHTML = Object.entries(roles).map(([sp, role]) => `
|
|
946
|
+
<div class="role-item">
|
|
947
|
+
${renderAvatar(sp, 'done', 20)}
|
|
948
|
+
<span class="role-speaker">${escHtml(sp)}</span>
|
|
949
|
+
<span class="role-name">${escHtml(role)}</span>
|
|
950
|
+
</div>`).join('');
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
/* ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
954
|
+
CONNECTION STATUS
|
|
955
|
+
ββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
|
956
|
+
function setConnStatus(state) {
|
|
957
|
+
const el = document.getElementById('conn-status');
|
|
958
|
+
if (state === 'connected') {
|
|
959
|
+
el.innerHTML = '';
|
|
960
|
+
} else {
|
|
961
|
+
el.innerHTML = `<div class="conn-warn"><span class="conn-dot"></span>Reconnectingβ¦</div>`;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
/* ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
966
|
+
NAVIGATION
|
|
967
|
+
ββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
|
968
|
+
function showList() {
|
|
969
|
+
if (currentStream) { currentStream.close(); currentStream = null; }
|
|
970
|
+
currentSession = null;
|
|
971
|
+
|
|
972
|
+
document.getElementById('screen-list').classList.add('active');
|
|
973
|
+
document.getElementById('screen-detail').classList.remove('active');
|
|
974
|
+
document.getElementById('screen-config').classList.remove('active');
|
|
975
|
+
document.getElementById('screen-stats').classList.remove('active');
|
|
976
|
+
document.getElementById('btn-back').style.display = 'none';
|
|
977
|
+
document.getElementById('btn-refresh').style.display = '';
|
|
978
|
+
document.getElementById('btn-gear').classList.remove('active');
|
|
979
|
+
document.getElementById('btn-stats').classList.remove('active');
|
|
980
|
+
document.getElementById('header-title').textContent = 'π Deliberation Observer';
|
|
981
|
+
document.getElementById('conn-status').innerHTML = '';
|
|
982
|
+
// Show FAB on list screen
|
|
983
|
+
const fab = document.getElementById('fab-start');
|
|
984
|
+
if (fab) fab.style.display = '';
|
|
985
|
+
previousScreen = 'list';
|
|
986
|
+
loadSessions();
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
function showDetail(sessionId) {
|
|
990
|
+
document.getElementById('screen-list').classList.remove('active');
|
|
991
|
+
document.getElementById('screen-detail').classList.add('active');
|
|
992
|
+
document.getElementById('screen-config').classList.remove('active');
|
|
993
|
+
document.getElementById('screen-stats').classList.remove('active');
|
|
994
|
+
document.getElementById('btn-back').style.display = '';
|
|
995
|
+
document.getElementById('btn-refresh').style.display = 'none';
|
|
996
|
+
document.getElementById('btn-gear').classList.remove('active');
|
|
997
|
+
document.getElementById('btn-stats').classList.remove('active');
|
|
998
|
+
document.getElementById('chat-log').innerHTML = '';
|
|
999
|
+
document.getElementById('speaker-bar').innerHTML = '';
|
|
1000
|
+
// Hide FAB on detail screen
|
|
1001
|
+
const fab = document.getElementById('fab-start');
|
|
1002
|
+
if (fab) fab.style.display = 'none';
|
|
1003
|
+
previousScreen = 'detail';
|
|
1004
|
+
|
|
1005
|
+
// Find session data for initial render while SSE loads
|
|
1006
|
+
const found = sessions.find(s => (s.id || s.session_id) === sessionId);
|
|
1007
|
+
if (found) {
|
|
1008
|
+
currentSession = found;
|
|
1009
|
+
document.getElementById('header-title').textContent = 'π ' + (found.topic || 'Session');
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
connectSSE(sessionId);
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
function showConfig() {
|
|
1016
|
+
// Toggle: if already on config, go back
|
|
1017
|
+
if (document.getElementById('screen-config').classList.contains('active')) {
|
|
1018
|
+
if (previousScreen === 'detail' && currentSession) {
|
|
1019
|
+
const id = currentSession.id || currentSession.session_id;
|
|
1020
|
+
showDetail(id);
|
|
1021
|
+
} else {
|
|
1022
|
+
showList();
|
|
1023
|
+
}
|
|
1024
|
+
return;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
// Remember where we came from
|
|
1028
|
+
if (document.getElementById('screen-detail').classList.contains('active')) {
|
|
1029
|
+
previousScreen = 'detail';
|
|
1030
|
+
} else {
|
|
1031
|
+
previousScreen = 'list';
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
document.getElementById('screen-list').classList.remove('active');
|
|
1035
|
+
document.getElementById('screen-detail').classList.remove('active');
|
|
1036
|
+
document.getElementById('screen-config').classList.add('active');
|
|
1037
|
+
document.getElementById('screen-stats').classList.remove('active');
|
|
1038
|
+
document.getElementById('btn-back').style.display = '';
|
|
1039
|
+
document.getElementById('btn-refresh').style.display = 'none';
|
|
1040
|
+
document.getElementById('btn-gear').classList.add('active');
|
|
1041
|
+
document.getElementById('btn-stats').classList.remove('active');
|
|
1042
|
+
document.getElementById('header-title').textContent = 'β LLM Configuration';
|
|
1043
|
+
document.getElementById('conn-status').innerHTML = '';
|
|
1044
|
+
// Hide FAB on config screen
|
|
1045
|
+
const fab = document.getElementById('fab-start');
|
|
1046
|
+
if (fab) fab.style.display = 'none';
|
|
1047
|
+
|
|
1048
|
+
loadConfig();
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
function showStats() {
|
|
1052
|
+
// Toggle: if already on stats, go back to list
|
|
1053
|
+
if (document.getElementById('screen-stats').classList.contains('active')) {
|
|
1054
|
+
showList();
|
|
1055
|
+
return;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
document.getElementById('screen-list').classList.remove('active');
|
|
1059
|
+
document.getElementById('screen-detail').classList.remove('active');
|
|
1060
|
+
document.getElementById('screen-config').classList.remove('active');
|
|
1061
|
+
document.getElementById('screen-stats').classList.add('active');
|
|
1062
|
+
document.getElementById('btn-back').style.display = '';
|
|
1063
|
+
document.getElementById('btn-refresh').style.display = 'none';
|
|
1064
|
+
document.getElementById('btn-gear').classList.remove('active');
|
|
1065
|
+
document.getElementById('btn-stats').classList.add('active');
|
|
1066
|
+
document.getElementById('header-title').textContent = 'π LLM Statistics';
|
|
1067
|
+
document.getElementById('conn-status').innerHTML = '';
|
|
1068
|
+
// Hide FAB on stats screen
|
|
1069
|
+
const fab = document.getElementById('fab-start');
|
|
1070
|
+
if (fab) fab.style.display = 'none';
|
|
1071
|
+
|
|
1072
|
+
previousScreen = 'list';
|
|
1073
|
+
loadStats();
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
// Override Back button behavior to respect all screens
|
|
1077
|
+
document.getElementById('btn-back').addEventListener('click', function(e) {
|
|
1078
|
+
e.stopPropagation();
|
|
1079
|
+
if (document.getElementById('screen-stats').classList.contains('active')) {
|
|
1080
|
+
showList();
|
|
1081
|
+
} else if (document.getElementById('screen-config').classList.contains('active')) {
|
|
1082
|
+
if (previousScreen === 'detail' && currentSession) {
|
|
1083
|
+
const id = currentSession.id || currentSession.session_id;
|
|
1084
|
+
showDetail(id);
|
|
1085
|
+
} else {
|
|
1086
|
+
showList();
|
|
1087
|
+
}
|
|
1088
|
+
} else {
|
|
1089
|
+
showList();
|
|
1090
|
+
}
|
|
1091
|
+
});
|
|
1092
|
+
|
|
1093
|
+
/* ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
1094
|
+
UTILITIES
|
|
1095
|
+
ββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
|
1096
|
+
function escHtml(s) {
|
|
1097
|
+
return String(s || '').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
/* ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
1101
|
+
LLM CONFIG β Master list
|
|
1102
|
+
ββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
|
1103
|
+
const LLM_LIST = [
|
|
1104
|
+
{ key: 'claude', name: 'Claude', label: 'Claude', cmd: 'claude' },
|
|
1105
|
+
{ key: 'codex', name: 'Codex', label: 'Codex', cmd: 'codex' },
|
|
1106
|
+
{ key: 'gemini', name: 'Gemini', label: 'Gemini', cmd: 'gemini' },
|
|
1107
|
+
{ key: 'qwen', name: 'Qwen', label: 'Qwen', cmd: 'qwen' },
|
|
1108
|
+
{ key: 'chatgpt', name: 'ChatGPT', label: 'ChatGPT', cmd: 'chatgpt' },
|
|
1109
|
+
{ key: 'aider', name: 'Aider', label: 'Aider', cmd: 'aider' },
|
|
1110
|
+
{ key: 'llm', name: 'LLM (Simon)', label: 'LLM', cmd: 'llm' },
|
|
1111
|
+
{ key: 'opencode', name: 'OpenCode', label: 'OpenCode', cmd: 'opencode' },
|
|
1112
|
+
{ key: 'cursor', name: 'Cursor', label: 'Cursor', cmd: 'cursor' },
|
|
1113
|
+
{ key: 'continue', name: 'Continue', label: 'Continue', cmd: 'continue' },
|
|
1114
|
+
];
|
|
1115
|
+
|
|
1116
|
+
// Extend MASCOTS with any missing entries
|
|
1117
|
+
Object.assign(MASCOTS, {
|
|
1118
|
+
aider: { initial: 'Ai', color: '#E44D26', shape: 'circle' },
|
|
1119
|
+
llm: { initial: 'L', color: '#5B6EAE', shape: 'square' },
|
|
1120
|
+
opencode: { initial: 'Oc', color: '#00C7B7', shape: 'hexagon' },
|
|
1121
|
+
cursor: { initial: 'Cr', color: '#6E6E8A', shape: 'circle' },
|
|
1122
|
+
continue: { initial: 'Co', color: '#7C3AED', shape: 'diamond' },
|
|
1123
|
+
});
|
|
1124
|
+
|
|
1125
|
+
let configState = []; // array of enabled LLM keys
|
|
1126
|
+
|
|
1127
|
+
/* ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
1128
|
+
LLM CONFIG β API
|
|
1129
|
+
ββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
|
1130
|
+
async function loadConfig() {
|
|
1131
|
+
try {
|
|
1132
|
+
const res = await fetch('/api/config');
|
|
1133
|
+
const data = await res.json();
|
|
1134
|
+
configState = Array.isArray(data.enabled_clis) ? data.enabled_clis : [];
|
|
1135
|
+
} catch (e) {
|
|
1136
|
+
configState = [];
|
|
1137
|
+
}
|
|
1138
|
+
renderLlmGrid();
|
|
1139
|
+
updateModeBadge();
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
async function saveConfig() {
|
|
1143
|
+
const enabled = LLM_LIST
|
|
1144
|
+
.filter(llm => {
|
|
1145
|
+
const el = document.getElementById(`toggle-${llm.key}`);
|
|
1146
|
+
return el && el.checked;
|
|
1147
|
+
})
|
|
1148
|
+
.map(llm => llm.key);
|
|
1149
|
+
|
|
1150
|
+
try {
|
|
1151
|
+
const res = await fetch('/api/config', {
|
|
1152
|
+
method: 'POST',
|
|
1153
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1154
|
+
body: JSON.stringify({ enabled_clis: enabled }),
|
|
1155
|
+
});
|
|
1156
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
1157
|
+
configState = enabled;
|
|
1158
|
+
updateModeBadge();
|
|
1159
|
+
showToast('Configuration saved', 'success');
|
|
1160
|
+
} catch (e) {
|
|
1161
|
+
showToast('Save failed: ' + e.message, 'error');
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
async function resetConfig() {
|
|
1166
|
+
try {
|
|
1167
|
+
const res = await fetch('/api/config', {
|
|
1168
|
+
method: 'POST',
|
|
1169
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1170
|
+
body: JSON.stringify({ enabled_clis: [] }),
|
|
1171
|
+
});
|
|
1172
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
1173
|
+
configState = [];
|
|
1174
|
+
renderLlmGrid();
|
|
1175
|
+
updateModeBadge();
|
|
1176
|
+
showToast('Reset to Auto-detect', 'success');
|
|
1177
|
+
} catch (e) {
|
|
1178
|
+
showToast('Reset failed: ' + e.message, 'error');
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
/* ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
1183
|
+
LLM CONFIG β Rendering
|
|
1184
|
+
ββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
|
1185
|
+
function renderLlmGrid() {
|
|
1186
|
+
const grid = document.getElementById('llm-grid');
|
|
1187
|
+
if (!grid) return;
|
|
1188
|
+
|
|
1189
|
+
grid.innerHTML = LLM_LIST.map((llm, i) => {
|
|
1190
|
+
const isEnabled = configState.includes(llm.key);
|
|
1191
|
+
const cardCls = isEnabled ? 'enabled' : 'disabled';
|
|
1192
|
+
const delay = i * 40;
|
|
1193
|
+
const avatar = renderAvatar(llm.key, 'done', 48);
|
|
1194
|
+
|
|
1195
|
+
return `<div class="llm-card ${cardCls}" id="card-${llm.key}"
|
|
1196
|
+
style="animation-delay:${delay}ms"
|
|
1197
|
+
onclick="toggleLlmCard('${llm.key}')">
|
|
1198
|
+
${avatar}
|
|
1199
|
+
<span class="llm-card-name">
|
|
1200
|
+
${escHtml(llm.name)}<span class="cli-status-dot loading" id="dot-${llm.key}" title="Checking..."></span>
|
|
1201
|
+
</span>
|
|
1202
|
+
<span class="llm-card-cmd">${escHtml(llm.cmd)}</span>
|
|
1203
|
+
<div class="toggle-wrap">
|
|
1204
|
+
<label class="toggle" onclick="event.stopPropagation()">
|
|
1205
|
+
<input type="checkbox" id="toggle-${llm.key}"
|
|
1206
|
+
${isEnabled ? 'checked' : ''}
|
|
1207
|
+
onchange="syncCardState('${llm.key}')">
|
|
1208
|
+
<span class="toggle-track"></span>
|
|
1209
|
+
<span class="toggle-thumb"></span>
|
|
1210
|
+
</label>
|
|
1211
|
+
</div>
|
|
1212
|
+
</div>`;
|
|
1213
|
+
}).join('');
|
|
1214
|
+
|
|
1215
|
+
loadCliStatus();
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
function toggleLlmCard(key) {
|
|
1219
|
+
const checkbox = document.getElementById(`toggle-${key}`);
|
|
1220
|
+
if (!checkbox) return;
|
|
1221
|
+
checkbox.checked = !checkbox.checked;
|
|
1222
|
+
syncCardState(key);
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
function syncCardState(key) {
|
|
1226
|
+
const checkbox = document.getElementById(`toggle-${key}`);
|
|
1227
|
+
const card = document.getElementById(`card-${key}`);
|
|
1228
|
+
if (!checkbox || !card) return;
|
|
1229
|
+
if (checkbox.checked) {
|
|
1230
|
+
card.classList.add('enabled');
|
|
1231
|
+
card.classList.remove('disabled');
|
|
1232
|
+
} else {
|
|
1233
|
+
card.classList.remove('enabled');
|
|
1234
|
+
card.classList.add('disabled');
|
|
1235
|
+
}
|
|
1236
|
+
updateModeBadge();
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
function updateModeBadge() {
|
|
1240
|
+
const badge = document.getElementById('config-mode-badge');
|
|
1241
|
+
if (!badge) return;
|
|
1242
|
+
const checked = LLM_LIST.filter(llm => {
|
|
1243
|
+
const el = document.getElementById(`toggle-${llm.key}`);
|
|
1244
|
+
return el && el.checked;
|
|
1245
|
+
});
|
|
1246
|
+
const isAuto = checked.length === 0;
|
|
1247
|
+
badge.textContent = isAuto ? 'Auto-detect' : `Custom (${checked.length} selected)`;
|
|
1248
|
+
badge.classList.toggle('auto', isAuto);
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
/* ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
1252
|
+
TOAST
|
|
1253
|
+
ββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
|
1254
|
+
let toastTimer = null;
|
|
1255
|
+
function showToast(message, type = 'success') {
|
|
1256
|
+
// Remove any existing toast
|
|
1257
|
+
const existing = document.getElementById('toast-el');
|
|
1258
|
+
if (existing) existing.remove();
|
|
1259
|
+
if (toastTimer) { clearTimeout(toastTimer); toastTimer = null; }
|
|
1260
|
+
|
|
1261
|
+
const toast = document.createElement('div');
|
|
1262
|
+
toast.id = 'toast-el';
|
|
1263
|
+
toast.className = `toast ${type}`;
|
|
1264
|
+
toast.textContent = (type === 'success' ? 'β ' : 'β ') + message;
|
|
1265
|
+
document.body.appendChild(toast);
|
|
1266
|
+
|
|
1267
|
+
toastTimer = setTimeout(() => {
|
|
1268
|
+
toast.classList.add('hiding');
|
|
1269
|
+
setTimeout(() => { if (toast.parentNode) toast.remove(); }, 380);
|
|
1270
|
+
}, 3000);
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
/* ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
1274
|
+
FEATURE 1: CLI Install Status
|
|
1275
|
+
ββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
|
1276
|
+
async function loadCliStatus() {
|
|
1277
|
+
try {
|
|
1278
|
+
const res = await fetch('/api/cli-status');
|
|
1279
|
+
const data = await res.json();
|
|
1280
|
+
const statusMap = {};
|
|
1281
|
+
(data.clis || []).forEach(c => { statusMap[c.name] = c.installed; });
|
|
1282
|
+
LLM_LIST.forEach(llm => {
|
|
1283
|
+
const dot = document.getElementById(`dot-${llm.key}`);
|
|
1284
|
+
if (!dot) return;
|
|
1285
|
+
const installed = statusMap[llm.key] === true;
|
|
1286
|
+
dot.className = 'cli-status-dot ' + (installed ? 'installed' : 'not-installed');
|
|
1287
|
+
dot.title = installed ? 'Installed' : 'Not installed';
|
|
1288
|
+
});
|
|
1289
|
+
} catch (e) {
|
|
1290
|
+
// On error, leave dots as loading (gray) β non-critical
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
/* ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
1295
|
+
FEATURE 2: Browser LLM Tabs
|
|
1296
|
+
ββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
|
1297
|
+
function toggleBrowserTabs() {
|
|
1298
|
+
const body = document.getElementById('browser-tabs-body');
|
|
1299
|
+
const toggle = document.getElementById('browser-toggle');
|
|
1300
|
+
const isOpen = body.style.display !== 'none';
|
|
1301
|
+
body.style.display = isOpen ? 'none' : 'block';
|
|
1302
|
+
toggle.classList.toggle('open', !isOpen);
|
|
1303
|
+
if (!isOpen) loadBrowserTabs();
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
async function loadBrowserTabs() {
|
|
1307
|
+
const list = document.getElementById('browser-tabs-list');
|
|
1308
|
+
list.innerHTML = '<div class="cdp-status">Scanning...</div>';
|
|
1309
|
+
try {
|
|
1310
|
+
const res = await fetch('/api/browser-tabs');
|
|
1311
|
+
const data = await res.json();
|
|
1312
|
+
if (!data.cdp_available) {
|
|
1313
|
+
list.innerHTML = `<div class="cdp-status unavailable">β ${escHtml(data.message || 'CDP not available')}</div>`;
|
|
1314
|
+
return;
|
|
1315
|
+
}
|
|
1316
|
+
if (!data.tabs || data.tabs.length === 0) {
|
|
1317
|
+
list.innerHTML = '<div class="cdp-status available">β CDP connected β No LLM tabs detected</div>';
|
|
1318
|
+
return;
|
|
1319
|
+
}
|
|
1320
|
+
list.innerHTML = `<div class="cdp-status available">β CDP connected β ${data.tabs.length} LLM tab(s)</div>` +
|
|
1321
|
+
data.tabs.map(t => `<div class="browser-tab-item">
|
|
1322
|
+
<span class="tab-title">${escHtml(t.title || '')}</span>
|
|
1323
|
+
<span class="tab-url">${escHtml(t.url || '')}</span>
|
|
1324
|
+
</div>`).join('');
|
|
1325
|
+
} catch (e) {
|
|
1326
|
+
list.innerHTML = `<div class="cdp-status unavailable">Error: ${escHtml(e.message)}</div>`;
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
/* ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
1331
|
+
FEATURE 3: Quick Start Deliberation
|
|
1332
|
+
ββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
|
1333
|
+
let quickStartSpeakers = new Set();
|
|
1334
|
+
|
|
1335
|
+
function showQuickStart() {
|
|
1336
|
+
document.getElementById('quick-start-modal').style.display = 'flex';
|
|
1337
|
+
// Pre-select from config (enabled CLIs), or default to first 3
|
|
1338
|
+
quickStartSpeakers = new Set(
|
|
1339
|
+
configState.length > 0
|
|
1340
|
+
? configState
|
|
1341
|
+
: LLM_LIST.slice(0, 3).map(l => l.key)
|
|
1342
|
+
);
|
|
1343
|
+
renderSpeakerChips();
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
function hideQuickStart() {
|
|
1347
|
+
document.getElementById('quick-start-modal').style.display = 'none';
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
function renderSpeakerChips() {
|
|
1351
|
+
const container = document.getElementById('qs-speakers');
|
|
1352
|
+
container.innerHTML = LLM_LIST.map(llm => {
|
|
1353
|
+
const sel = quickStartSpeakers.has(llm.key) ? 'selected' : '';
|
|
1354
|
+
return `<span class="speaker-chip ${sel}" onclick="toggleSpeakerChip('${llm.key}')">
|
|
1355
|
+
${renderAvatar(llm.key, 'done', 20)} ${escHtml(llm.label)}
|
|
1356
|
+
</span>`;
|
|
1357
|
+
}).join('');
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
function toggleSpeakerChip(key) {
|
|
1361
|
+
if (quickStartSpeakers.has(key)) quickStartSpeakers.delete(key);
|
|
1362
|
+
else quickStartSpeakers.add(key);
|
|
1363
|
+
renderSpeakerChips();
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
async function startDeliberation() {
|
|
1367
|
+
const topic = document.getElementById('qs-topic').value.trim();
|
|
1368
|
+
const rounds = parseInt(document.getElementById('qs-rounds').value) || 3;
|
|
1369
|
+
const speakers = [...quickStartSpeakers];
|
|
1370
|
+
if (!topic) { showToast('Please enter a topic', 'error'); return; }
|
|
1371
|
+
if (speakers.length < 2) { showToast('Select at least 2 speakers', 'error'); return; }
|
|
1372
|
+
try {
|
|
1373
|
+
const res = await fetch('/api/sessions/start', {
|
|
1374
|
+
method: 'POST',
|
|
1375
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1376
|
+
body: JSON.stringify({ topic, speakers, max_rounds: rounds }),
|
|
1377
|
+
});
|
|
1378
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
1379
|
+
const session = await res.json();
|
|
1380
|
+
hideQuickStart();
|
|
1381
|
+
showToast(`Deliberation started: ${topic}`, 'success');
|
|
1382
|
+
await loadSessions();
|
|
1383
|
+
showDetail(session.id || session.session_id);
|
|
1384
|
+
} catch (e) {
|
|
1385
|
+
showToast('Failed to start: ' + e.message, 'error');
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
// Close modal on Escape key
|
|
1390
|
+
document.addEventListener('keydown', function(e) {
|
|
1391
|
+
if (e.key === 'Escape') hideQuickStart();
|
|
1392
|
+
});
|
|
1393
|
+
|
|
1394
|
+
/* ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
1395
|
+
FEATURE 4: LLM Statistics Dashboard
|
|
1396
|
+
ββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
|
1397
|
+
async function loadStats() {
|
|
1398
|
+
const summary = document.getElementById('stats-summary');
|
|
1399
|
+
const speakers = document.getElementById('stats-speakers');
|
|
1400
|
+
summary.innerHTML = '<div class="empty"><div class="spinner"></div>Loadingβ¦</div>';
|
|
1401
|
+
speakers.innerHTML = '';
|
|
1402
|
+
try {
|
|
1403
|
+
const res = await fetch('/api/stats');
|
|
1404
|
+
const data = await res.json();
|
|
1405
|
+
renderStats(data);
|
|
1406
|
+
} catch (e) {
|
|
1407
|
+
summary.innerHTML = `<div class="empty">Failed to load stats: ${escHtml(e.message)}</div>`;
|
|
1408
|
+
speakers.innerHTML = '';
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
function renderStats(data) {
|
|
1413
|
+
const summary = document.getElementById('stats-summary');
|
|
1414
|
+
const container = document.getElementById('stats-speakers');
|
|
1415
|
+
|
|
1416
|
+
const speakerEntries = Object.entries(data.speakers || {});
|
|
1417
|
+
summary.innerHTML = `
|
|
1418
|
+
<div class="stat-card" style="animation-delay:0ms">
|
|
1419
|
+
<div class="stat-value">${data.total_sessions != null ? data.total_sessions : 'β'}</div>
|
|
1420
|
+
<div class="stat-label">Sessions</div>
|
|
1421
|
+
</div>
|
|
1422
|
+
<div class="stat-card" style="animation-delay:60ms">
|
|
1423
|
+
<div class="stat-value">${data.total_turns != null ? data.total_turns : 'β'}</div>
|
|
1424
|
+
<div class="stat-label">Total Turns</div>
|
|
1425
|
+
</div>
|
|
1426
|
+
<div class="stat-card" style="animation-delay:120ms">
|
|
1427
|
+
<div class="stat-value">${speakerEntries.length}</div>
|
|
1428
|
+
<div class="stat-label">Speakers</div>
|
|
1429
|
+
</div>
|
|
1430
|
+
`;
|
|
1431
|
+
|
|
1432
|
+
if (speakerEntries.length === 0) {
|
|
1433
|
+
container.innerHTML = '<div class="empty">No participation data yet</div>';
|
|
1434
|
+
return;
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
const sorted = speakerEntries.sort((a, b) => b[1].turns - a[1].turns);
|
|
1438
|
+
const maxTurns = sorted[0][1].turns || 1;
|
|
1439
|
+
|
|
1440
|
+
container.innerHTML = sorted.map(([name, s], i) => {
|
|
1441
|
+
const pct = Math.round(((s.turns || 0) / maxTurns) * 100);
|
|
1442
|
+
const m = getMascot(name);
|
|
1443
|
+
const votes = s.votes || {};
|
|
1444
|
+
const voteStr = [
|
|
1445
|
+
votes.agree > 0 ? `β
${votes.agree}` : '',
|
|
1446
|
+
votes.disagree > 0 ? `β${votes.disagree}` : '',
|
|
1447
|
+
votes.conditional > 0 ? `π‘${votes.conditional}` : '',
|
|
1448
|
+
].filter(Boolean).join(' ') || 'No votes';
|
|
1449
|
+
|
|
1450
|
+
return `<div class="speaker-stat-row" style="animation-delay:${i * 0.05}s">
|
|
1451
|
+
${renderAvatar(name, 'done', 40)}
|
|
1452
|
+
<div class="speaker-stat-info">
|
|
1453
|
+
<div class="speaker-stat-name">${escHtml(name)}</div>
|
|
1454
|
+
<div class="speaker-stat-detail">${s.turns || 0} turns Β· avg ${s.avg_length || 0} chars Β· ${voteStr}</div>
|
|
1455
|
+
</div>
|
|
1456
|
+
<div class="speaker-stat-bar">
|
|
1457
|
+
<div style="font-size:0.75em;color:var(--muted);text-align:right">${s.turns || 0}</div>
|
|
1458
|
+
<div class="mini-bar">
|
|
1459
|
+
<div class="mini-bar-fill" style="width:${pct}%;background:${escHtml(m.color)}"></div>
|
|
1460
|
+
</div>
|
|
1461
|
+
</div>
|
|
1462
|
+
</div>`;
|
|
1463
|
+
}).join('');
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
/* ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
1467
|
+
INIT
|
|
1468
|
+
ββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
|
1469
|
+
loadSessions();
|
|
1470
|
+
setInterval(() => {
|
|
1471
|
+
// Only auto-refresh the list when on the list screen
|
|
1472
|
+
if (document.getElementById('screen-list').classList.contains('active')) {
|
|
1473
|
+
loadSessions();
|
|
1474
|
+
}
|
|
1475
|
+
}, 5000);
|
|
1476
|
+
</script>
|
|
1477
|
+
</body>
|
|
1478
|
+
</html>
|