pretticlaw 0.1.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.
Files changed (158) hide show
  1. package/CONTRIBUTING.md +123 -0
  2. package/README.md +150 -0
  3. package/assets/logo.png +0 -0
  4. package/dist/agent/context.d.ts +22 -0
  5. package/dist/agent/context.js +85 -0
  6. package/dist/agent/loop.d.ts +63 -0
  7. package/dist/agent/loop.js +244 -0
  8. package/dist/agent/memory.d.ts +16 -0
  9. package/dist/agent/memory.js +98 -0
  10. package/dist/agent/skills.d.ts +18 -0
  11. package/dist/agent/skills.js +121 -0
  12. package/dist/agent/subagent.d.ts +30 -0
  13. package/dist/agent/subagent.js +92 -0
  14. package/dist/agent/tools/base.d.ts +10 -0
  15. package/dist/agent/tools/base.js +58 -0
  16. package/dist/agent/tools/cron.d.ts +43 -0
  17. package/dist/agent/tools/cron.js +83 -0
  18. package/dist/agent/tools/filesystem.d.ts +79 -0
  19. package/dist/agent/tools/filesystem.js +125 -0
  20. package/dist/agent/tools/message.d.ts +41 -0
  21. package/dist/agent/tools/message.js +55 -0
  22. package/dist/agent/tools/registry.d.ts +9 -0
  23. package/dist/agent/tools/registry.js +33 -0
  24. package/dist/agent/tools/shell.d.ts +26 -0
  25. package/dist/agent/tools/shell.js +78 -0
  26. package/dist/agent/tools/spawn.d.ts +27 -0
  27. package/dist/agent/tools/spawn.js +35 -0
  28. package/dist/agent/tools/web.d.ts +50 -0
  29. package/dist/agent/tools/web.js +119 -0
  30. package/dist/bus/async-queue.d.ts +7 -0
  31. package/dist/bus/async-queue.js +20 -0
  32. package/dist/bus/events.d.ts +19 -0
  33. package/dist/bus/events.js +3 -0
  34. package/dist/bus/queue.d.ts +12 -0
  35. package/dist/bus/queue.js +23 -0
  36. package/dist/channels/base.d.ts +22 -0
  37. package/dist/channels/base.js +35 -0
  38. package/dist/channels/discord.d.ts +24 -0
  39. package/dist/channels/discord.js +133 -0
  40. package/dist/channels/manager.d.ts +17 -0
  41. package/dist/channels/manager.js +67 -0
  42. package/dist/channels/stub.d.ts +10 -0
  43. package/dist/channels/stub.js +18 -0
  44. package/dist/channels/telegram.d.ts +20 -0
  45. package/dist/channels/telegram.js +93 -0
  46. package/dist/cli/commands.d.ts +2 -0
  47. package/dist/cli/commands.js +552 -0
  48. package/dist/config/loader.d.ts +5 -0
  49. package/dist/config/loader.js +55 -0
  50. package/dist/config/schema.d.ts +246 -0
  51. package/dist/config/schema.js +94 -0
  52. package/dist/cron/service.d.ts +33 -0
  53. package/dist/cron/service.js +195 -0
  54. package/dist/cron/types.d.ts +47 -0
  55. package/dist/cron/types.js +1 -0
  56. package/dist/dashboard/index.html +1567 -0
  57. package/dist/heartbeat/service.d.ts +21 -0
  58. package/dist/heartbeat/service.js +101 -0
  59. package/dist/index.d.ts +2 -0
  60. package/dist/index.js +5 -0
  61. package/dist/providers/base.d.ts +23 -0
  62. package/dist/providers/base.js +21 -0
  63. package/dist/providers/custom-provider.d.ts +16 -0
  64. package/dist/providers/custom-provider.js +49 -0
  65. package/dist/providers/litellm-provider.d.ts +19 -0
  66. package/dist/providers/litellm-provider.js +128 -0
  67. package/dist/providers/registry.d.ts +5 -0
  68. package/dist/providers/registry.js +45 -0
  69. package/dist/session/manager.d.ts +31 -0
  70. package/dist/session/manager.js +116 -0
  71. package/dist/skills/README.md +25 -0
  72. package/dist/skills/clawhub/SKILL.md +53 -0
  73. package/dist/skills/cron/SKILL.md +57 -0
  74. package/dist/skills/github/SKILL.md +48 -0
  75. package/dist/skills/memory/SKILL.md +31 -0
  76. package/dist/skills/skill-creator/SKILL.md +371 -0
  77. package/dist/skills/summarize/SKILL.md +67 -0
  78. package/dist/skills/tmux/SKILL.md +121 -0
  79. package/dist/skills/tmux/scripts/find-sessions.sh +112 -0
  80. package/dist/skills/tmux/scripts/wait-for-text.sh +83 -0
  81. package/dist/skills/weather/SKILL.md +49 -0
  82. package/dist/templates/AGENTS.md +23 -0
  83. package/dist/templates/HEARTBEAT.md +16 -0
  84. package/dist/templates/SOUL.md +21 -0
  85. package/dist/templates/TOOLS.md +15 -0
  86. package/dist/templates/USER.md +49 -0
  87. package/dist/templates/memory/MEMORY.md +23 -0
  88. package/dist/types.d.ts +4 -0
  89. package/dist/types.js +3 -0
  90. package/dist/utils/helpers.d.ts +5 -0
  91. package/dist/utils/helpers.js +53 -0
  92. package/dist/web/server.d.ts +15 -0
  93. package/dist/web/server.js +169 -0
  94. package/package.json +37 -0
  95. package/scripts/copy-assets.mjs +21 -0
  96. package/src/agent/context.ts +90 -0
  97. package/src/agent/loop.ts +291 -0
  98. package/src/agent/memory.ts +104 -0
  99. package/src/agent/skills.ts +121 -0
  100. package/src/agent/subagent.ts +96 -0
  101. package/src/agent/tools/base.ts +59 -0
  102. package/src/agent/tools/cron.ts +79 -0
  103. package/src/agent/tools/filesystem.ts +93 -0
  104. package/src/agent/tools/message.ts +57 -0
  105. package/src/agent/tools/registry.ts +36 -0
  106. package/src/agent/tools/shell.ts +69 -0
  107. package/src/agent/tools/spawn.ts +37 -0
  108. package/src/agent/tools/web.ts +108 -0
  109. package/src/bus/async-queue.ts +20 -0
  110. package/src/bus/events.ts +23 -0
  111. package/src/bus/queue.ts +31 -0
  112. package/src/channels/base.ts +36 -0
  113. package/src/channels/discord.ts +156 -0
  114. package/src/channels/manager.ts +70 -0
  115. package/src/channels/stub.ts +20 -0
  116. package/src/channels/telegram.ts +120 -0
  117. package/src/cli/commands.ts +581 -0
  118. package/src/config/loader.ts +58 -0
  119. package/src/config/schema.ts +144 -0
  120. package/src/cron/service.ts +190 -0
  121. package/src/cron/types.ts +36 -0
  122. package/src/dashboard/index.html +1567 -0
  123. package/src/heartbeat/service.ts +95 -0
  124. package/src/index.ts +6 -0
  125. package/src/providers/base.ts +43 -0
  126. package/src/providers/custom-provider.ts +46 -0
  127. package/src/providers/litellm-provider.ts +131 -0
  128. package/src/providers/registry.ts +48 -0
  129. package/src/session/manager.ts +129 -0
  130. package/src/skills/README.md +25 -0
  131. package/src/skills/clawhub/SKILL.md +53 -0
  132. package/src/skills/cron/SKILL.md +57 -0
  133. package/src/skills/github/SKILL.md +48 -0
  134. package/src/skills/memory/SKILL.md +31 -0
  135. package/src/skills/skill-creator/SKILL.md +371 -0
  136. package/src/skills/summarize/SKILL.md +67 -0
  137. package/src/skills/tmux/SKILL.md +121 -0
  138. package/src/skills/tmux/scripts/find-sessions.sh +112 -0
  139. package/src/skills/tmux/scripts/wait-for-text.sh +83 -0
  140. package/src/skills/weather/SKILL.md +49 -0
  141. package/src/templates/AGENTS.md +23 -0
  142. package/src/templates/HEARTBEAT.md +16 -0
  143. package/src/templates/SOUL.md +21 -0
  144. package/src/templates/TOOLS.md +15 -0
  145. package/src/templates/USER.md +49 -0
  146. package/src/templates/memory/MEMORY.md +23 -0
  147. package/src/types/prompts.d.ts +14 -0
  148. package/src/types/ws.d.ts +15 -0
  149. package/src/types.ts +5 -0
  150. package/src/utils/helpers.ts +55 -0
  151. package/src/web/server.ts +198 -0
  152. package/test/context.test.ts +27 -0
  153. package/test/cron-service.test.ts +31 -0
  154. package/test/message-tool.test.ts +10 -0
  155. package/test/providers.test.ts +43 -0
  156. package/test/tool-validation.test.ts +61 -0
  157. package/tsconfig.json +16 -0
  158. package/vitest.config.ts +8 -0
@@ -0,0 +1,1567 @@
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>Pretticlaw Dashboard</title>
7
+ <link href="https://fonts.googleapis.com/css2?family=Sora:wght@400;600;700&family=JetBrains+Mono:wght@400;600&display=swap" rel="stylesheet" />
8
+ <link href="https://cdn.jsdelivr.net/npm/lucide-static@0.344.0/font/lucide.min.css" rel="stylesheet" />
9
+ <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
10
+ <style>
11
+ /* ─── MARKDOWN IN CHAT ─── */
12
+ .message-bubble .markdown-body {
13
+ color: var(--text-primary);
14
+ font-size: 15px;
15
+ line-height: 1.7;
16
+ }
17
+ .message-bubble .markdown-body h1,
18
+ .message-bubble .markdown-body h2,
19
+ .message-bubble .markdown-body h3 {
20
+ color: var(--pink);
21
+ margin-top: 1.2em;
22
+ margin-bottom: 0.5em;
23
+ font-weight: 700;
24
+ }
25
+ .message-bubble .markdown-body h1 { font-size: 1.4em; }
26
+ .message-bubble .markdown-body h2 { font-size: 1.2em; }
27
+ .message-bubble .markdown-body h3 { font-size: 1.1em; }
28
+ .message-bubble .markdown-body ul,
29
+ .message-bubble .markdown-body ol {
30
+ margin: 0.5em 0 0.5em 1.2em;
31
+ }
32
+ .message-bubble .markdown-body li {
33
+ margin-bottom: 0.2em;
34
+ }
35
+ .message-bubble .markdown-body strong {
36
+ color: #fff;
37
+ }
38
+ .message-bubble .markdown-body code {
39
+ background: #23232a;
40
+ color: #ffb3d1;
41
+ border-radius: 4px;
42
+ padding: 2px 5px;
43
+ font-size: 0.97em;
44
+ font-family: 'JetBrains Mono', monospace;
45
+ }
46
+ .message-bubble .markdown-body pre {
47
+ background: #18181c;
48
+ color: #ffb3d1;
49
+ border-radius: 7px;
50
+ padding: 12px;
51
+ font-size: 0.97em;
52
+ font-family: 'JetBrains Mono', monospace;
53
+ overflow-x: auto;
54
+ margin: 1em 0;
55
+ }
56
+ .message-bubble .markdown-body blockquote {
57
+ border-left: 3px solid var(--pink);
58
+ background: #23232a;
59
+ color: #bdbdc7;
60
+ margin: 0.7em 0;
61
+ padding: 0.5em 1em;
62
+ border-radius: 6px;
63
+ }
64
+ .message-bubble .markdown-body table {
65
+ border-collapse: collapse;
66
+ margin: 1em 0;
67
+ width: 100%;
68
+ }
69
+ .message-bubble .markdown-body th,
70
+ .message-bubble .markdown-body td {
71
+ border: 1px solid #333;
72
+ padding: 6px 10px;
73
+ }
74
+ .message-bubble .markdown-body th {
75
+ background: #23232a;
76
+ color: var(--pink);
77
+ }
78
+ :root {
79
+ --bg-primary: #252527;
80
+ --bg-secondary: #272725;
81
+ --bg-tertiary: #2d2d2f;
82
+ --bg-input: #1e1e20;
83
+ --text-primary: #f3f3f6;
84
+ --text-secondary: #b9b9c5;
85
+ --pink: #ff5fa2;
86
+ --pink-dark: #e24f91;
87
+ --pink-glow: rgba(255,95,162,0.15);
88
+ --border: #3a3a3f;
89
+ --border-light: #4a4a50;
90
+ --user-bg: #3a2c3e;
91
+ --user-border: #5d4460;
92
+ --assistant-bg: #2a2a30;
93
+ --assistant-border: #3a3a3f;
94
+ --sidebar-width: 260px;
95
+ }
96
+
97
+ * { box-sizing: border-box; margin: 0; padding: 0; }
98
+ html, body, #root {
99
+ height: 100%;
100
+ background: var(--bg-primary);
101
+ color: var(--text-primary);
102
+ font-family: 'Sora', system-ui, sans-serif;
103
+ overflow: hidden;
104
+ }
105
+
106
+ .app {
107
+ display: flex;
108
+ height: 100%;
109
+ }
110
+
111
+ /* ─── SIDEBAR ─── */
112
+ .sidebar {
113
+ width: var(--sidebar-width);
114
+ min-width: var(--sidebar-width);
115
+ background: linear-gradient(180deg, #2b2a2d, #232326);
116
+ border-right: 1px solid var(--border);
117
+ display: flex;
118
+ flex-direction: column;
119
+ transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1), min-width 0.3s cubic-bezier(0.4, 0, 0.2, 1);
120
+ overflow: hidden;
121
+ flex-shrink: 0;
122
+ }
123
+
124
+ .sidebar.collapsed {
125
+ width: 0;
126
+ min-width: 0;
127
+ border-right: none;
128
+ }
129
+
130
+ .sidebar-header {
131
+ padding: 16px;
132
+ border-bottom: 1px solid var(--border);
133
+ display: flex;
134
+ align-items: center;
135
+ justify-content: space-between;
136
+ gap: 12px;
137
+ white-space: nowrap;
138
+ }
139
+
140
+ .sidebar-brand {
141
+ font-weight: 700;
142
+ font-size: 17px;
143
+ letter-spacing: -0.3px;
144
+ background: linear-gradient(135deg, var(--pink), #ff8ec4);
145
+ -webkit-background-clip: text;
146
+ -webkit-text-fill-color: transparent;
147
+ background-clip: text;
148
+ }
149
+
150
+ .toggle-btn {
151
+ background: var(--bg-tertiary);
152
+ border: 1px solid var(--border);
153
+ color: var(--text-secondary);
154
+ cursor: pointer;
155
+ width: 30px;
156
+ height: 30px;
157
+ display: flex;
158
+ align-items: center;
159
+ justify-content: center;
160
+ border-radius: 8px;
161
+ transition: all 0.2s;
162
+ font-size: 14px;
163
+ box-shadow: inset 0 1px 1px rgba(255,255,255,0.08);
164
+ flex-shrink: 0;
165
+ }
166
+
167
+ .toggle-btn:hover {
168
+ color: var(--text-primary);
169
+ background: var(--border);
170
+ }
171
+
172
+ .nav-tabs {
173
+ flex: 1;
174
+ overflow-y: auto;
175
+ padding: 10px 12px;
176
+ display: flex;
177
+ flex-direction: column;
178
+ gap: 4px;
179
+ }
180
+
181
+ .nav-tab {
182
+ text-align: left;
183
+ border: 1px solid transparent;
184
+ color: var(--text-secondary);
185
+ padding: 10px 14px;
186
+ border-radius: 10px;
187
+ background: transparent;
188
+ cursor: pointer;
189
+ transition: all 0.2s ease;
190
+ font-size: 14px;
191
+ font-weight: 500;
192
+ font-family: inherit;
193
+ white-space: nowrap;
194
+ display: flex;
195
+ align-items: center;
196
+ gap: 10px;
197
+ }
198
+
199
+ .nav-tab i {
200
+ font-size: 18px;
201
+ width: 20px;
202
+ text-align: center;
203
+ }
204
+
205
+ .nav-tab:hover {
206
+ background: rgba(255,255,255,0.04);
207
+ color: var(--text-primary);
208
+ }
209
+
210
+ .nav-tab.active {
211
+ background: linear-gradient(180deg, var(--pink), var(--pink-dark));
212
+ color: white;
213
+ border-color: #ea8ab7;
214
+ box-shadow: inset 0 2px 3px rgba(255,255,255,0.3), inset 0 -2px 4px rgba(0,0,0,0.3), 0 2px 8px var(--pink-glow);
215
+ }
216
+
217
+ /* ─── MAIN CONTENT ─── */
218
+ .main-container {
219
+ flex: 1;
220
+ display: flex;
221
+ flex-direction: column;
222
+ overflow: hidden;
223
+ min-width: 0;
224
+ }
225
+
226
+ .top-bar {
227
+ padding: 14px 20px;
228
+ border-bottom: 1px solid var(--border);
229
+ display: flex;
230
+ align-items: center;
231
+ gap: 14px;
232
+ background: var(--bg-secondary);
233
+ }
234
+
235
+ .sidebar-toggle {
236
+ background: var(--bg-tertiary);
237
+ border: 1px solid var(--border);
238
+ color: var(--text-primary);
239
+ cursor: pointer;
240
+ width: 36px;
241
+ height: 36px;
242
+ display: flex;
243
+ align-items: center;
244
+ justify-content: center;
245
+ border-radius: 10px;
246
+ transition: all 0.2s;
247
+ font-size: 16px;
248
+ box-shadow: inset 0 1px 2px rgba(255,255,255,0.1), inset 0 -1px 2px rgba(0,0,0,0.25);
249
+ }
250
+
251
+ .sidebar-toggle:hover {
252
+ background: var(--border);
253
+ box-shadow: inset 0 1px 2px rgba(255,255,255,0.15), inset 0 -1px 2px rgba(0,0,0,0.3);
254
+ }
255
+
256
+ .top-title {
257
+ font-size: 16px;
258
+ font-weight: 600;
259
+ color: var(--text-primary);
260
+ }
261
+
262
+ .content-area {
263
+ flex: 1;
264
+ overflow: hidden;
265
+ display: flex;
266
+ flex-direction: column;
267
+ }
268
+
269
+ /* ─── CHAT VIEW ─── */
270
+ .chat-view {
271
+ display: flex;
272
+ flex-direction: column;
273
+ height: 100%;
274
+ }
275
+
276
+ .chat-messages {
277
+ flex: 1;
278
+ overflow-y: auto;
279
+ overflow-x: hidden;
280
+ padding: 24px 20px;
281
+ display: flex;
282
+ flex-direction: column;
283
+ gap: 14px;
284
+ scroll-behavior: smooth;
285
+ }
286
+
287
+ .chat-messages::-webkit-scrollbar { width: 6px; }
288
+ .chat-messages::-webkit-scrollbar-track { background: transparent; }
289
+ .chat-messages::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
290
+ .chat-messages::-webkit-scrollbar-thumb:hover { background: #555; }
291
+
292
+ .message {
293
+ display: flex;
294
+ gap: 12px;
295
+ animation: msgIn 0.3s cubic-bezier(0.4, 0, 0.2, 1);
296
+ }
297
+
298
+ @keyframes msgIn {
299
+ from { opacity: 0; transform: translateY(10px); }
300
+ to { opacity: 1; transform: translateY(0); }
301
+ }
302
+
303
+ .message.user { justify-content: flex-end; }
304
+
305
+ .message-bubble {
306
+ max-width: 65%;
307
+ padding: 12px 16px;
308
+ border-radius: 16px;
309
+ font-size: 14.5px;
310
+ line-height: 1.6;
311
+ word-wrap: break-word;
312
+ border: 1px solid var(--border);
313
+ }
314
+
315
+ .message.user .message-bubble {
316
+ background: var(--user-bg);
317
+ border-color: var(--user-border);
318
+ border-bottom-right-radius: 4px;
319
+ }
320
+
321
+ .message.assistant .message-bubble {
322
+ background: var(--assistant-bg);
323
+ border-color: var(--assistant-border);
324
+ border-bottom-left-radius: 4px;
325
+ }
326
+
327
+ .message.progress .message-bubble {
328
+ background: transparent;
329
+ border: none;
330
+ font-size: 13px;
331
+ color: var(--text-secondary);
332
+ font-style: italic;
333
+ padding: 4px 0;
334
+ }
335
+
336
+ .tool-hint {
337
+ margin-top: 6px;
338
+ font-size: 12px;
339
+ color: var(--text-secondary);
340
+ font-style: italic;
341
+ opacity: 0.8;
342
+ }
343
+
344
+ /* ─── TOOL CALL COLLAPSIBLE ─── */
345
+ .tool-call-row {
346
+ animation: msgIn 0.3s cubic-bezier(0.4, 0, 0.2, 1);
347
+ margin: 2px 0;
348
+ }
349
+
350
+ .tool-call-header {
351
+ display: inline-flex;
352
+ align-items: center;
353
+ gap: 8px;
354
+ padding: 6px 14px;
355
+ border-radius: 8px;
356
+ background: rgba(255,95,162,0.08);
357
+ border: 1px solid rgba(255,95,162,0.15);
358
+ cursor: pointer;
359
+ transition: all 0.2s;
360
+ user-select: none;
361
+ font-size: 13px;
362
+ color: var(--pink);
363
+ font-weight: 500;
364
+ }
365
+
366
+ .tool-call-header:hover {
367
+ background: rgba(255,95,162,0.14);
368
+ border-color: rgba(255,95,162,0.3);
369
+ }
370
+
371
+ .tool-call-header i {
372
+ font-size: 14px;
373
+ transition: transform 0.2s;
374
+ }
375
+
376
+ .tool-call-header.expanded i {
377
+ transform: rotate(90deg);
378
+ }
379
+
380
+ .tool-call-body {
381
+ margin-top: 6px;
382
+ padding: 10px 14px;
383
+ background: var(--bg-input);
384
+ border: 1px solid var(--border);
385
+ border-radius: 10px;
386
+ font-size: 13px;
387
+ color: var(--text-secondary);
388
+ line-height: 1.5;
389
+ font-family: 'JetBrains Mono', monospace;
390
+ }
391
+
392
+ /* ─── STATUS INDICATORS ─── */
393
+ .status-grid {
394
+ display: grid;
395
+ grid-template-columns: 1fr 1fr;
396
+ gap: 16px;
397
+ }
398
+
399
+ .status-card {
400
+ background: var(--bg-tertiary);
401
+ border: 1px solid var(--border);
402
+ border-radius: 14px;
403
+ padding: 20px;
404
+ transition: border-color 0.2s;
405
+ }
406
+
407
+ .status-card:hover {
408
+ border-color: var(--border-light);
409
+ }
410
+
411
+ .status-card-header {
412
+ display: flex;
413
+ align-items: center;
414
+ gap: 10px;
415
+ margin-bottom: 12px;
416
+ }
417
+
418
+ .status-card-header i {
419
+ font-size: 20px;
420
+ color: var(--pink);
421
+ }
422
+
423
+ .status-card-title {
424
+ font-size: 13px;
425
+ text-transform: uppercase;
426
+ letter-spacing: 0.5px;
427
+ color: var(--text-secondary);
428
+ font-weight: 600;
429
+ }
430
+
431
+ .status-card-value {
432
+ font-size: 20px;
433
+ font-weight: 700;
434
+ color: var(--text-primary);
435
+ }
436
+
437
+ .status-indicator {
438
+ display: inline-flex;
439
+ align-items: center;
440
+ gap: 8px;
441
+ font-size: 15px;
442
+ font-weight: 600;
443
+ }
444
+
445
+ .status-dot {
446
+ width: 10px;
447
+ height: 10px;
448
+ border-radius: 50%;
449
+ flex-shrink: 0;
450
+ }
451
+
452
+ .status-dot.active {
453
+ background: #22c55e;
454
+ box-shadow: 0 0 8px rgba(34,197,94,0.5);
455
+ animation: pulse-green 2s ease-in-out infinite;
456
+ }
457
+
458
+ .status-dot.inactive {
459
+ background: #ef4444;
460
+ box-shadow: 0 0 6px rgba(239,68,68,0.3);
461
+ }
462
+
463
+ @keyframes pulse-green {
464
+ 0%, 100% { box-shadow: 0 0 4px rgba(34,197,94,0.3); }
465
+ 50% { box-shadow: 0 0 14px rgba(34,197,94,0.7); }
466
+ }
467
+
468
+ .status-banner {
469
+ background: var(--bg-tertiary);
470
+ border: 1px solid var(--border);
471
+ border-radius: 14px;
472
+ padding: 20px 24px;
473
+ margin-bottom: 20px;
474
+ display: flex;
475
+ align-items: center;
476
+ justify-content: space-between;
477
+ }
478
+
479
+ .status-banner-left {
480
+ display: flex;
481
+ align-items: center;
482
+ gap: 14px;
483
+ }
484
+
485
+ .status-banner-dot {
486
+ width: 14px;
487
+ height: 14px;
488
+ border-radius: 50%;
489
+ flex-shrink: 0;
490
+ }
491
+
492
+ .status-banner-dot.active {
493
+ background: #22c55e;
494
+ box-shadow: 0 0 10px rgba(34,197,94,0.6);
495
+ animation: pulse-green 2s ease-in-out infinite;
496
+ }
497
+
498
+ .status-banner-dot.inactive {
499
+ background: #ef4444;
500
+ box-shadow: 0 0 6px rgba(239,68,68,0.3);
501
+ }
502
+
503
+ .status-banner-text {
504
+ font-size: 16px;
505
+ font-weight: 600;
506
+ }
507
+
508
+ .status-banner-sub {
509
+ font-size: 13px;
510
+ color: var(--text-secondary);
511
+ margin-top: 2px;
512
+ }
513
+
514
+ /* ─── PRETTIFLOW LINK ─── */
515
+ .sidebar-footer {
516
+ padding: 12px 16px;
517
+ border-top: 1px solid var(--border);
518
+ white-space: nowrap;
519
+ }
520
+
521
+ .prettiflow-link {
522
+ display: flex;
523
+ align-items: center;
524
+ gap: 10px;
525
+ text-decoration: none;
526
+ color: var(--text-secondary);
527
+ padding: 8px 10px;
528
+ border-radius: 10px;
529
+ transition: all 0.2s;
530
+ font-size: 13px;
531
+ font-weight: 500;
532
+ }
533
+
534
+ .prettiflow-link:hover {
535
+ background: rgba(255,255,255,0.04);
536
+ color: var(--text-primary);
537
+ }
538
+
539
+ .prettiflow-link img {
540
+ width: 22px;
541
+ height: 22px;
542
+ border-radius: 6px;
543
+ }
544
+
545
+ /* ─── CRON INTERVAL ROW ─── */
546
+ .interval-row {
547
+ display: flex;
548
+ gap: 10px;
549
+ align-items: flex-end;
550
+ }
551
+
552
+ .interval-row .form-group {
553
+ margin-bottom: 0;
554
+ }
555
+
556
+ .interval-row .interval-value {
557
+ flex: 1;
558
+ }
559
+
560
+ .interval-row .interval-unit {
561
+ width: 140px;
562
+ flex-shrink: 0;
563
+ }
564
+
565
+ /* ─── INPUT AREA ─── */
566
+ .chat-input-area {
567
+ padding: 14px 20px 22px;
568
+ border-top: 1px solid var(--border);
569
+ background: var(--bg-secondary);
570
+ }
571
+
572
+ .input-container {
573
+ display: flex;
574
+ gap: 10px;
575
+ align-items: flex-end;
576
+ background: var(--bg-input);
577
+ border: 1px solid var(--border);
578
+ border-radius: 14px;
579
+ padding: 6px 6px 6px 16px;
580
+ transition: border-color 0.2s;
581
+ }
582
+
583
+ .input-container:focus-within {
584
+ border-color: var(--pink);
585
+ box-shadow: 0 0 0 2px var(--pink-glow);
586
+ }
587
+
588
+ .input-field {
589
+ flex: 1;
590
+ background: transparent;
591
+ border: none;
592
+ color: var(--text-primary);
593
+ padding: 8px 0;
594
+ font-family: 'Sora', system-ui, sans-serif;
595
+ font-size: 14.5px;
596
+ line-height: 1.5;
597
+ resize: none;
598
+ max-height: 120px;
599
+ outline: none;
600
+ }
601
+
602
+ .input-field::placeholder {
603
+ color: #666;
604
+ }
605
+
606
+ .send-btn {
607
+ background: linear-gradient(180deg, var(--pink), var(--pink-dark));
608
+ border: 1px solid #ea8ab7;
609
+ color: white;
610
+ padding: 10px 18px;
611
+ border-radius: 10px;
612
+ cursor: pointer;
613
+ font-weight: 600;
614
+ font-size: 14px;
615
+ font-family: inherit;
616
+ transition: all 0.15s ease;
617
+ box-shadow: inset 0 2px 3px rgba(255,255,255,0.35), inset 0 -2px 4px rgba(0,0,0,0.3);
618
+ }
619
+
620
+ .send-btn:hover:not(:disabled) {
621
+ filter: brightness(1.1);
622
+ transform: translateY(-1px);
623
+ box-shadow: inset 0 2px 3px rgba(255,255,255,0.4), inset 0 -2px 4px rgba(0,0,0,0.35), 0 4px 12px var(--pink-glow);
624
+ }
625
+
626
+ .send-btn:active:not(:disabled) {
627
+ transform: translateY(0);
628
+ box-shadow: inset 0 2px 5px rgba(0,0,0,0.4), inset 0 -1px 1px rgba(255,255,255,0.1);
629
+ }
630
+
631
+ .send-btn:disabled {
632
+ opacity: 0.5;
633
+ cursor: not-allowed;
634
+ }
635
+
636
+ /* ─── TYPING INDICATOR ─── */
637
+ .typing-dots { display: flex; gap: 5px; padding: 4px 0; align-items: center; }
638
+
639
+ .typing-dot {
640
+ width: 7px;
641
+ height: 7px;
642
+ border-radius: 50%;
643
+ background: var(--pink);
644
+ animation: bounce 1.4s infinite;
645
+ }
646
+
647
+ .typing-dot:nth-child(2) { animation-delay: 0.15s; }
648
+ .typing-dot:nth-child(3) { animation-delay: 0.3s; }
649
+
650
+ @keyframes bounce {
651
+ 0%, 60%, 100% { transform: translateY(0); opacity: 0.4; }
652
+ 30% { transform: translateY(-6px); opacity: 1; }
653
+ }
654
+
655
+ /* ─── PANELS ─── */
656
+ .panel {
657
+ padding: 24px;
658
+ overflow-y: auto;
659
+ height: 100%;
660
+ flex: 1;
661
+ }
662
+
663
+ .panel::-webkit-scrollbar { width: 6px; }
664
+ .panel::-webkit-scrollbar-track { background: transparent; }
665
+ .panel::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
666
+
667
+ .panel-title {
668
+ font-size: 22px;
669
+ font-weight: 700;
670
+ margin-bottom: 24px;
671
+ color: var(--text-primary);
672
+ }
673
+
674
+ .form-group {
675
+ margin-bottom: 18px;
676
+ }
677
+
678
+ .form-label {
679
+ display: block;
680
+ margin-bottom: 6px;
681
+ font-size: 13px;
682
+ font-weight: 600;
683
+ color: var(--text-secondary);
684
+ text-transform: uppercase;
685
+ letter-spacing: 0.5px;
686
+ }
687
+
688
+ .form-input {
689
+ width: 100%;
690
+ background: var(--bg-input);
691
+ border: 1px solid var(--border);
692
+ color: var(--text-primary);
693
+ padding: 10px 14px;
694
+ border-radius: 10px;
695
+ font-family: 'Sora', system-ui, sans-serif;
696
+ font-size: 14px;
697
+ transition: border-color 0.2s, box-shadow 0.2s;
698
+ outline: none;
699
+ }
700
+
701
+ .form-input:focus {
702
+ border-color: var(--pink);
703
+ box-shadow: 0 0 0 2px var(--pink-glow);
704
+ }
705
+
706
+ .form-input::placeholder { color: #555; }
707
+
708
+ /* ─── CUSTOM CHECKBOX ─── */
709
+ .checkbox-label {
710
+ display: inline-flex;
711
+ align-items: center;
712
+ gap: 10px;
713
+ cursor: pointer;
714
+ font-size: 14px;
715
+ color: var(--text-primary);
716
+ margin-bottom: 10px;
717
+ user-select: none;
718
+ }
719
+
720
+ .checkbox-label input[type="checkbox"] {
721
+ appearance: none;
722
+ -webkit-appearance: none;
723
+ width: 20px;
724
+ height: 20px;
725
+ border: 2px solid var(--border-light);
726
+ border-radius: 6px;
727
+ background: var(--bg-input);
728
+ cursor: pointer;
729
+ position: relative;
730
+ transition: all 0.2s;
731
+ flex-shrink: 0;
732
+ }
733
+
734
+ .checkbox-label input[type="checkbox"]:checked {
735
+ background: linear-gradient(180deg, var(--pink), var(--pink-dark));
736
+ border-color: var(--pink);
737
+ box-shadow: inset 0 1px 2px rgba(255,255,255,0.3), 0 0 8px var(--pink-glow);
738
+ }
739
+
740
+ .checkbox-label input[type="checkbox"]:checked::after {
741
+ content: '';
742
+ position: absolute;
743
+ left: 5px;
744
+ top: 2px;
745
+ width: 6px;
746
+ height: 10px;
747
+ border: solid white;
748
+ border-width: 0 2.5px 2.5px 0;
749
+ transform: rotate(45deg);
750
+ }
751
+
752
+ .checkbox-label input[type="checkbox"]:hover {
753
+ border-color: var(--pink);
754
+ }
755
+
756
+ /* ─── BUTTONS ─── */
757
+ .btn-primary {
758
+ background: linear-gradient(180deg, var(--pink), var(--pink-dark));
759
+ border: 1px solid #ea8ab7;
760
+ color: white;
761
+ padding: 10px 20px;
762
+ border-radius: 10px;
763
+ cursor: pointer;
764
+ font-weight: 600;
765
+ font-size: 14px;
766
+ font-family: inherit;
767
+ transition: all 0.15s ease;
768
+ box-shadow: inset 0 2px 3px rgba(255,255,255,0.35), inset 0 -2px 4px rgba(0,0,0,0.3);
769
+ }
770
+
771
+ .btn-primary:hover {
772
+ filter: brightness(1.1);
773
+ transform: translateY(-1px);
774
+ box-shadow: inset 0 2px 3px rgba(255,255,255,0.4), inset 0 -2px 4px rgba(0,0,0,0.35), 0 4px 12px var(--pink-glow);
775
+ }
776
+
777
+ .btn-primary:active {
778
+ transform: translateY(0);
779
+ box-shadow: inset 0 2px 5px rgba(0,0,0,0.4), inset 0 -1px 1px rgba(255,255,255,0.1);
780
+ }
781
+
782
+ .btn-secondary {
783
+ background: linear-gradient(180deg, #3a3a41, #313137);
784
+ border: 1px solid var(--border-light);
785
+ color: var(--text-primary);
786
+ padding: 10px 16px;
787
+ border-radius: 10px;
788
+ cursor: pointer;
789
+ font-weight: 600;
790
+ font-size: 14px;
791
+ font-family: inherit;
792
+ transition: all 0.15s ease;
793
+ box-shadow: inset 0 2px 2px rgba(255,255,255,0.12), inset 0 -2px 3px rgba(0,0,0,0.3);
794
+ }
795
+
796
+ .btn-secondary:hover {
797
+ background: linear-gradient(180deg, #444, #3a3a41);
798
+ border-color: var(--pink);
799
+ transform: translateY(-1px);
800
+ box-shadow: inset 0 2px 2px rgba(255,255,255,0.15), inset 0 -2px 3px rgba(0,0,0,0.3), 0 2px 8px rgba(0,0,0,0.2);
801
+ }
802
+
803
+ .btn-secondary:active {
804
+ transform: translateY(0);
805
+ box-shadow: inset 0 2px 4px rgba(0,0,0,0.35), inset 0 -1px 1px rgba(255,255,255,0.05);
806
+ }
807
+
808
+ .grid-2 {
809
+ display: grid;
810
+ grid-template-columns: 1fr 1fr;
811
+ gap: 20px;
812
+ }
813
+
814
+ .divider {
815
+ height: 1px;
816
+ background: var(--border);
817
+ margin: 20px 0;
818
+ }
819
+
820
+ .job-card {
821
+ background: var(--bg-tertiary);
822
+ border: 1px solid var(--border);
823
+ padding: 16px;
824
+ border-radius: 12px;
825
+ margin-bottom: 12px;
826
+ transition: border-color 0.2s;
827
+ }
828
+
829
+ .job-card:hover {
830
+ border-color: var(--border-light);
831
+ }
832
+
833
+ .job-name {
834
+ font-weight: 600;
835
+ color: var(--text-primary);
836
+ margin-bottom: 6px;
837
+ font-size: 15px;
838
+ }
839
+
840
+ .job-details {
841
+ font-size: 13px;
842
+ color: var(--text-secondary);
843
+ margin-bottom: 14px;
844
+ line-height: 1.6;
845
+ }
846
+
847
+ .btn-group {
848
+ display: flex;
849
+ gap: 8px;
850
+ }
851
+
852
+ .btn-group .btn-secondary {
853
+ flex: 1;
854
+ padding: 8px 12px;
855
+ font-size: 13px;
856
+ }
857
+
858
+ /* ─── SELECT ELEMENTS ─── */
859
+ select, select.form-input {
860
+ appearance: none;
861
+ -webkit-appearance: none;
862
+ background: var(--bg-input) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' fill='none' stroke='%23b9b9c5' stroke-width='2' stroke-linecap='round'%3E%3Cpath d='M1 1l5 5 5-5'/%3E%3C/svg%3E") no-repeat right 14px center;
863
+ border: 1px solid var(--border);
864
+ color: var(--text-primary);
865
+ padding: 10px 40px 10px 14px;
866
+ border-radius: 10px;
867
+ font-family: 'Sora', system-ui, sans-serif;
868
+ font-size: 14px;
869
+ cursor: pointer;
870
+ transition: border-color 0.2s, box-shadow 0.2s;
871
+ outline: none;
872
+ width: 100%;
873
+ }
874
+
875
+ select:focus, select.form-input:focus {
876
+ border-color: var(--pink);
877
+ box-shadow: 0 0 0 2px var(--pink-glow);
878
+ }
879
+
880
+ select option {
881
+ background: var(--bg-secondary);
882
+ color: var(--text-primary);
883
+ padding: 8px;
884
+ }
885
+
886
+ /* ─── RANGE SLIDER ─── */
887
+ input[type="range"] {
888
+ -webkit-appearance: none;
889
+ appearance: none;
890
+ width: 100%;
891
+ height: 6px;
892
+ background: var(--border);
893
+ border-radius: 3px;
894
+ outline: none;
895
+ border: none;
896
+ padding: 0;
897
+ margin: 8px 0;
898
+ }
899
+
900
+ input[type="range"]::-webkit-slider-thumb {
901
+ -webkit-appearance: none;
902
+ width: 20px;
903
+ height: 20px;
904
+ border-radius: 50%;
905
+ background: linear-gradient(180deg, var(--pink), var(--pink-dark));
906
+ cursor: pointer;
907
+ border: 2px solid #ea8ab7;
908
+ box-shadow: inset 0 1px 2px rgba(255,255,255,0.3), 0 2px 6px var(--pink-glow);
909
+ transition: transform 0.15s;
910
+ }
911
+
912
+ input[type="range"]::-webkit-slider-thumb:hover {
913
+ transform: scale(1.15);
914
+ }
915
+
916
+ input[type="range"]::-moz-range-thumb {
917
+ width: 20px;
918
+ height: 20px;
919
+ border-radius: 50%;
920
+ background: linear-gradient(180deg, var(--pink), var(--pink-dark));
921
+ cursor: pointer;
922
+ border: 2px solid #ea8ab7;
923
+ box-shadow: inset 0 1px 2px rgba(255,255,255,0.3), 0 2px 6px var(--pink-glow);
924
+ }
925
+
926
+ /* ─── NUMBER INPUT ARROWS ─── */
927
+ input[type="number"] {
928
+ -moz-appearance: textfield;
929
+ }
930
+
931
+ input[type="number"]::-webkit-inner-spin-button,
932
+ input[type="number"]::-webkit-outer-spin-button {
933
+ opacity: 1;
934
+ height: 28px;
935
+ }
936
+
937
+ /* ─── EMPTY STATE ─── */
938
+ .empty-state {
939
+ text-align: center;
940
+ color: var(--text-secondary);
941
+ padding: 48px 20px;
942
+ font-size: 15px;
943
+ line-height: 1.6;
944
+ }
945
+
946
+ /* ─── RESPONSIVENESS ─── */
947
+ @media (max-width: 768px) {
948
+ .sidebar {
949
+ position: fixed;
950
+ left: 0;
951
+ top: 0;
952
+ height: 100%;
953
+ z-index: 200;
954
+ box-shadow: 4px 0 16px rgba(0, 0, 0, 0.5);
955
+ }
956
+
957
+ .sidebar.collapsed {
958
+ width: 0;
959
+ min-width: 0;
960
+ }
961
+
962
+ .message-bubble {
963
+ max-width: 85%;
964
+ }
965
+
966
+ .grid-2 {
967
+ grid-template-columns: 1fr;
968
+ }
969
+ }
970
+ </style>
971
+ </head>
972
+ <body>
973
+ <div id="root"></div>
974
+ <script type="module">
975
+ import React, { useEffect, useRef, useState } from "https://esm.sh/react@18.3.1";
976
+ import { createRoot } from "https://esm.sh/react-dom@18.3.1/client";
977
+
978
+ const TABS = [
979
+ { id: "chat", label: "Chat", icon: "icon-message-square" },
980
+ { id: "channels", label: "Channels", icon: "icon-radio" },
981
+ { id: "settings", label: "Settings", icon: "icon-settings" },
982
+ { id: "cron", label: "Cron", icon: "icon-clock" },
983
+ { id: "status", label: "Status", icon: "icon-activity" },
984
+ ];
985
+
986
+ const MODEL_CHOICES = {
987
+ groq: ["openai/gpt-oss-120b","openai/gpt-oss-20b","meta-llama/llama-4-maverick-17b-128e-instruct","groq/compound","groq/compound-mini"],
988
+ openrouter: ["anthropic/claude-opus-4-1","anthropic/claude-sonnet-4","openai/gpt-4.1","google/gemini-2.5-pro"],
989
+ anthropic: ["anthropic/claude-opus-4-5","anthropic/claude-sonnet-4"],
990
+ openai: ["gpt-5.2","gpt-5.2-pro","gpt-5.3-codex","gpt-5.2-codex","gpt-5.1","gpt-5.1-codex","gpt-5 mini","gpt-5 nano","gpt-4.1","gpt-4.1 mini","gpt-4.1 nano","gpt-4o","gpt-4o mini"],
991
+ deepseek: ["deepseek/deepseek-chat","deepseek/deepseek-reasoner"],
992
+ gemini: ["gemini/gemini-2.5-pro","gemini/gemini-2.5-flash"],
993
+ moonshot: ["moonshot/kimi-k2.5"],
994
+ minimax: ["minimax/MiniMax-M2.1"],
995
+ dashscope: ["dashscope/qwen-max"],
996
+ zhipu: ["zai/glm-4.5"],
997
+ siliconflow: ["openai/deepseek-ai/DeepSeek-R1"],
998
+ volcengine: ["volcengine/deepseek-r1-250120"],
999
+ vllm: ["hosted_vllm/llama-3.1-8b-instruct"],
1000
+ custom: ["custom/model-name"],
1001
+ };
1002
+
1003
+ const PROVIDER_LIST = Object.keys(MODEL_CHOICES);
1004
+
1005
+ const API = {
1006
+ status: () => fetch("/api/status").then(r => r.json()),
1007
+ chat: (message) => fetch("/api/chat", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ message, session: "web:dashboard" }) }).then(r => r.json()),
1008
+ getConfig: () => fetch("/api/config").then(r => r.json()),
1009
+ saveConfig: (config) => fetch("/api/config", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(config) }).then(r => r.json()),
1010
+ cronList: () => fetch("/api/cron/jobs?all=1").then(r => r.json()),
1011
+ cronAdd: (payload) => fetch("/api/cron/jobs", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) }).then(r => r.json()),
1012
+ cronDel: (id) => fetch(`/api/cron/jobs/${id}`, { method: "DELETE" }).then(r => r.json()),
1013
+ cronEnable: (id, enabled) => fetch(`/api/cron/jobs/${id}/enable`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ enabled }) }).then(r => r.json()),
1014
+ cronRun: (id) => fetch(`/api/cron/jobs/${id}/run`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ force: true }) }).then(r => r.json()),
1015
+ };
1016
+
1017
+ function App() {
1018
+ const [currentTab, setCurrentTab] = useState("chat");
1019
+ const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
1020
+
1021
+ return React.createElement("div", { className: "app" },
1022
+ React.createElement("aside", { className: `sidebar ${sidebarCollapsed ? "collapsed" : ""}` },
1023
+ React.createElement("div", { className: "sidebar-header" },
1024
+ React.createElement("div", { className: "sidebar-brand" }, "Pretticlaw"),
1025
+ React.createElement("button", { className: "toggle-btn", onClick: () => setSidebarCollapsed(!sidebarCollapsed) }, "✕"),
1026
+ ),
1027
+ React.createElement("nav", { className: "nav-tabs" },
1028
+ TABS.map(tab =>
1029
+ React.createElement("button", {
1030
+ key: tab.id,
1031
+ className: `nav-tab ${currentTab === tab.id ? "active" : ""}`,
1032
+ onClick: () => setCurrentTab(tab.id),
1033
+ },
1034
+ React.createElement("i", { className: tab.icon }),
1035
+ ` ${tab.label}`,
1036
+ )
1037
+ ),
1038
+ ),
1039
+ React.createElement("div", { className: "sidebar-footer" },
1040
+ React.createElement("a", {
1041
+ className: "prettiflow-link",
1042
+ href: "https://prettiflow.tech",
1043
+ target: "_blank",
1044
+ rel: "noopener noreferrer",
1045
+ },
1046
+ React.createElement("img", { src: "https://prettiflow.tech/logo.png", alt: "PrettiFlow" }),
1047
+ "PrettiFlow",
1048
+ React.createElement("i", { className: "icon-external-link", style: { fontSize: "12px", marginLeft: "auto" } }),
1049
+ ),
1050
+ ),
1051
+ ),
1052
+ React.createElement("div", { className: "main-container" },
1053
+ React.createElement("div", { className: "top-bar" },
1054
+ React.createElement("button", { className: "sidebar-toggle", onClick: () => setSidebarCollapsed(!sidebarCollapsed) }, "☰"),
1055
+ React.createElement("div", { className: "top-title" }, TABS.find(t => t.id === currentTab)?.label || "Chat"),
1056
+ ),
1057
+ React.createElement("div", { className: "content-area" },
1058
+ currentTab === "chat" && React.createElement(ChatView),
1059
+ currentTab === "channels" && React.createElement(ChannelsView),
1060
+ currentTab === "settings" && React.createElement(SettingsView),
1061
+ currentTab === "cron" && React.createElement(CronView),
1062
+ currentTab === "status" && React.createElement(StatusView),
1063
+ ),
1064
+ ),
1065
+ );
1066
+ }
1067
+
1068
+ function ToolCallRow({ toolCalls }) {
1069
+ const [expanded, setExpanded] = useState(false);
1070
+ if (!toolCalls || !toolCalls.length) return null;
1071
+ return React.createElement("div", { className: "tool-call-row" },
1072
+ React.createElement("div", {
1073
+ className: `tool-call-header ${expanded ? "expanded" : ""}`,
1074
+ onClick: () => setExpanded(!expanded),
1075
+ },
1076
+ React.createElement("i", { className: "icon-chevron-right" }),
1077
+ React.createElement("i", { className: "icon-wrench", style: { fontSize: "13px" } }),
1078
+ toolCalls.length === 1
1079
+ ? `Tool: ${toolCalls[0].function?.name || toolCalls[0].name}`
1080
+ : `Tools: ${toolCalls.map(tc => tc.function?.name || tc.name).join(", ")}`
1081
+ ),
1082
+ expanded && React.createElement("div", { className: "tool-call-body" },
1083
+ toolCalls.map((tc, i) => {
1084
+ let args = tc.arguments;
1085
+ if (!args && tc.function && typeof tc.function.arguments === "string") {
1086
+ try {
1087
+ args = JSON.parse(tc.function.arguments);
1088
+ } catch { args = tc.function.arguments; }
1089
+ }
1090
+ return React.createElement("div", { key: i, style: { marginBottom: "10px" } },
1091
+ React.createElement("div", { style: { fontWeight: 600, color: "var(--pink)", marginBottom: 2 } }, `${i + 1}. ${tc.function?.name || tc.name}`),
1092
+ args && typeof args === "object"
1093
+ ? React.createElement("pre", { style: { background: "#18181c", color: "#ffb3d1", borderRadius: 7, padding: 8, fontSize: "13px", fontFamily: "'JetBrains Mono', monospace", margin: 0 } }, JSON.stringify(args, null, 2))
1094
+ : args
1095
+ ? React.createElement("pre", { style: { background: "#18181c", color: "#ffb3d1", borderRadius: 7, padding: 8, fontSize: "13px", fontFamily: "'JetBrains Mono', monospace", margin: 0 } }, String(args))
1096
+ : tc.input
1097
+ ? React.createElement("pre", { style: { background: "#18181c", color: "#ffb3d1", borderRadius: 7, padding: 8, fontSize: "13px", fontFamily: "'JetBrains Mono', monospace", margin: 0 } }, String(tc.input))
1098
+ : null
1099
+ );
1100
+ })
1101
+ ),
1102
+ );
1103
+ }
1104
+
1105
+
1106
+ function ChatView() {
1107
+ const [messages, setMessages] = useState([]);
1108
+ const [input, setInput] = useState("");
1109
+ const [loading, setLoading] = useState(false);
1110
+ const messagesEndRef = useRef(null);
1111
+
1112
+ useEffect(() => {
1113
+ fetch("/api/history")
1114
+ .then(r => r.json())
1115
+ .then(data => {
1116
+ const msgs = (data.messages || []).map(msg => {
1117
+ if (!msg || typeof msg.role !== "string") return null;
1118
+ return {
1119
+ ...msg,
1120
+ id: Math.random(),
1121
+ content: typeof msg.content === "string" ? msg.content : "",
1122
+ };
1123
+ }).filter(Boolean);
1124
+ setMessages(msgs);
1125
+ })
1126
+ .catch(() => {});
1127
+ }, []);
1128
+
1129
+ useEffect(() => {
1130
+ messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
1131
+ }, [messages, loading]);
1132
+
1133
+ const send = async () => {
1134
+ const text = input.trim();
1135
+ if (!text || loading) return;
1136
+
1137
+ const userMsg = { id: Math.random(), role: "user", content: text };
1138
+ setMessages(prev => [...prev, userMsg]);
1139
+ setInput("");
1140
+ setLoading(true);
1141
+
1142
+ try {
1143
+ const data = await API.chat(text);
1144
+ if (data.error) {
1145
+ setMessages(prev => [...prev, { id: Math.random(), role: "assistant", content: `Error: ${data.error}` }]);
1146
+ } else {
1147
+ if (data.response) {
1148
+ setMessages(prev => [...prev, { id: Math.random(), role: "assistant", content: data.response }]);
1149
+ }
1150
+ if (data.progress && Array.isArray(data.progress)) {
1151
+ data.progress.forEach(p => {
1152
+ setMessages(prev => [...prev, { id: Math.random(), role: "progress", content: p.content, tool_calls: p.tool_calls }]);
1153
+ });
1154
+ }
1155
+ }
1156
+ } catch (err) {
1157
+ setMessages(prev => [...prev, { id: Math.random(), role: "assistant", content: `Error: ${String(err)}` }]);
1158
+ } finally {
1159
+ setLoading(false);
1160
+ }
1161
+ };
1162
+
1163
+ const handleKeyDown = (e) => {
1164
+ if (e.key === "Enter" && !e.shiftKey) {
1165
+ e.preventDefault();
1166
+ send();
1167
+ }
1168
+ };
1169
+
1170
+ function renderMarkdown(md) {
1171
+ if (!md) return null;
1172
+ const html = window.marked.parse(md, { breaks: true });
1173
+ return React.createElement("div", {
1174
+ className: "markdown-body",
1175
+ dangerouslySetInnerHTML: { __html: html },
1176
+ });
1177
+ }
1178
+
1179
+ return React.createElement("div", { className: "chat-view" },
1180
+ React.createElement("div", { className: "chat-messages" },
1181
+ messages.length === 0 && React.createElement("div", { className: "empty-state" }, "Start a conversation..."),
1182
+ messages.map(msg => {
1183
+ const toolCalls = Array.isArray(msg.tool_calls) ? msg.tool_calls : [];
1184
+ if (toolCalls.length > 0 && !msg.content) {
1185
+ return React.createElement(ToolCallRow, { key: msg.id, toolCalls });
1186
+ }
1187
+ return React.createElement(React.Fragment, { key: msg.id },
1188
+ toolCalls.length > 0 && React.createElement(ToolCallRow, { toolCalls }),
1189
+ React.createElement("div", { className: `message ${msg.role}` },
1190
+ React.createElement("div", { className: "message-bubble" },
1191
+ msg.role === "assistant" || msg.role === "agent"
1192
+ ? renderMarkdown(msg.content)
1193
+ : msg.content
1194
+ ),
1195
+ ),
1196
+ );
1197
+ }),
1198
+ loading && React.createElement("div", { className: "message assistant" },
1199
+ React.createElement("div", { className: "message-bubble" },
1200
+ React.createElement("div", { className: "typing-dots" },
1201
+ React.createElement("div", { className: "typing-dot" }),
1202
+ React.createElement("div", { className: "typing-dot" }),
1203
+ React.createElement("div", { className: "typing-dot" }),
1204
+ ),
1205
+ ),
1206
+ ),
1207
+ React.createElement("div", { ref: messagesEndRef }),
1208
+ ),
1209
+ React.createElement("div", { className: "chat-input-area" },
1210
+ React.createElement("div", { className: "input-container" },
1211
+ React.createElement("textarea", {
1212
+ className: "input-field",
1213
+ value: input,
1214
+ onChange: e => setInput(e.target.value),
1215
+ onKeyDown: handleKeyDown,
1216
+ placeholder: "Ask anything... (Shift+Enter for new line)",
1217
+ rows: 1,
1218
+ }),
1219
+ React.createElement("button", { className: "send-btn", onClick: send, disabled: loading }, loading ? "..." : "Send"),
1220
+ ),
1221
+ ),
1222
+ );
1223
+ }
1224
+
1225
+ function ChannelsView() {
1226
+ const [config, setConfig] = useState(null);
1227
+ const [saving, setSaving] = useState(false);
1228
+
1229
+ useEffect(() => {
1230
+ API.getConfig().then(setConfig).catch(() => {});
1231
+ }, []);
1232
+
1233
+ if (!config) return React.createElement("div", { className: "panel" }, "Loading...");
1234
+
1235
+ const save = async () => {
1236
+ setSaving(true);
1237
+ await API.saveConfig(config).catch(() => {});
1238
+ setSaving(false);
1239
+ };
1240
+
1241
+ const c = config.channels;
1242
+
1243
+ return React.createElement("div", { className: "panel" },
1244
+ React.createElement("h2", { className: "panel-title" }, "Channels"),
1245
+ React.createElement("div", { className: "grid-2" },
1246
+ React.createElement("div",
1247
+ { className: "form-group" },
1248
+ React.createElement("label", { className: "checkbox-label" },
1249
+ React.createElement("input", {
1250
+ type: "checkbox",
1251
+ checked: c.telegram.enabled || false,
1252
+ onChange: e => { c.telegram.enabled = e.target.checked; setConfig({ ...config }); }
1253
+ }),
1254
+ "Telegram Enabled"
1255
+ ),
1256
+ React.createElement("input", {
1257
+ className: "form-input",
1258
+ value: c.telegram.token || "",
1259
+ onChange: e => { c.telegram.token = e.target.value; setConfig({ ...config }); },
1260
+ placeholder: "Telegram Bot Token"
1261
+ }),
1262
+ ),
1263
+ React.createElement("div",
1264
+ { className: "form-group" },
1265
+ React.createElement("label", { className: "checkbox-label" },
1266
+ React.createElement("input", {
1267
+ type: "checkbox",
1268
+ checked: c.discord.enabled || false,
1269
+ onChange: e => { c.discord.enabled = e.target.checked; setConfig({ ...config }); }
1270
+ }),
1271
+ "Discord Enabled"
1272
+ ),
1273
+ React.createElement("input", {
1274
+ className: "form-input",
1275
+ value: c.discord.token || "",
1276
+ onChange: e => { c.discord.token = e.target.value; setConfig({ ...config }); },
1277
+ placeholder: "Discord Bot Token"
1278
+ }),
1279
+ ),
1280
+ ),
1281
+ React.createElement("div", { className: "form-group" },
1282
+ React.createElement("button", { className: "btn-primary", onClick: save, disabled: saving }, saving ? "Saving..." : "Save Channels"),
1283
+ ),
1284
+ );
1285
+ }
1286
+
1287
+ function SettingsView() {
1288
+ const [config, setConfig] = useState(null);
1289
+ const [saving, setSaving] = useState(false);
1290
+
1291
+ useEffect(() => {
1292
+ API.getConfig().then(setConfig).catch(() => {});
1293
+ }, []);
1294
+
1295
+ if (!config) return React.createElement("div", { className: "panel" }, "Loading...");
1296
+
1297
+ const save = async () => {
1298
+ setSaving(true);
1299
+ await API.saveConfig(config).catch(() => {});
1300
+ setSaving(false);
1301
+ };
1302
+
1303
+ const d = config.agents.defaults;
1304
+ const currentProvider = d.provider || "openrouter";
1305
+ const models = MODEL_CHOICES[currentProvider] || [];
1306
+
1307
+ const onProviderChange = (e) => {
1308
+ const newProvider = e.target.value;
1309
+ d.provider = newProvider;
1310
+ const newModels = MODEL_CHOICES[newProvider] || [];
1311
+ if (newModels.length && !newModels.includes(d.model)) {
1312
+ d.model = newModels[0];
1313
+ }
1314
+ setConfig({ ...config });
1315
+ };
1316
+
1317
+ return React.createElement("div", { className: "panel" },
1318
+ React.createElement("h2", { className: "panel-title" }, "Settings"),
1319
+ React.createElement("div", { className: "grid-2" },
1320
+ React.createElement("div", { className: "form-group" },
1321
+ React.createElement("label", { className: "form-label" }, "Provider"),
1322
+ React.createElement("select", {
1323
+ className: "form-input",
1324
+ value: currentProvider,
1325
+ onChange: onProviderChange,
1326
+ },
1327
+ PROVIDER_LIST.map(p =>
1328
+ React.createElement("option", { key: p, value: p }, p.charAt(0).toUpperCase() + p.slice(1))
1329
+ ),
1330
+ ),
1331
+ ),
1332
+ React.createElement("div", { className: "form-group" },
1333
+ React.createElement("label", { className: "form-label" }, "Model"),
1334
+ React.createElement("select", {
1335
+ className: "form-input",
1336
+ value: d.model || "",
1337
+ onChange: e => { d.model = e.target.value; setConfig({ ...config }); },
1338
+ },
1339
+ models.map(m =>
1340
+ React.createElement("option", { key: m, value: m }, m)
1341
+ ),
1342
+ !models.includes(d.model) && d.model
1343
+ ? React.createElement("option", { key: d.model, value: d.model }, `${d.model} (custom)`)
1344
+ : null,
1345
+ ),
1346
+ ),
1347
+ ),
1348
+ React.createElement("div", { className: "grid-2" },
1349
+ React.createElement("div", { className: "form-group" },
1350
+ React.createElement("label", { className: "form-label" }, "Temperature"),
1351
+ React.createElement("input", {
1352
+ className: "form-input",
1353
+ type: "range",
1354
+ min: "0",
1355
+ max: "2",
1356
+ step: "0.1",
1357
+ value: d.temperature || 0,
1358
+ onChange: e => { d.temperature = Number(e.target.value); setConfig({ ...config }); },
1359
+ }),
1360
+ React.createElement("div", { style: { fontSize: "13px", color: "var(--text-secondary)", marginTop: "4px" } }, `Value: ${d.temperature ?? 0}`),
1361
+ ),
1362
+ React.createElement("div", { className: "form-group" },
1363
+ React.createElement("label", { className: "form-label" }, "Max Tokens"),
1364
+ React.createElement("input", {
1365
+ className: "form-input",
1366
+ type: "number",
1367
+ value: d.maxTokens || 0,
1368
+ onChange: e => { d.maxTokens = Number(e.target.value); setConfig({ ...config }); },
1369
+ }),
1370
+ ),
1371
+ ),
1372
+ React.createElement("div", { className: "form-group", style: { marginTop: "8px" } },
1373
+ React.createElement("button", { className: "btn-primary", onClick: save, disabled: saving }, saving ? "Saving..." : "Save Settings"),
1374
+ ),
1375
+ );
1376
+ }
1377
+
1378
+ function CronView() {
1379
+ const [jobs, setJobs] = useState([]);
1380
+ const [name, setName] = useState("");
1381
+ const [message, setMessage] = useState("");
1382
+ const [intervalValue, setIntervalValue] = useState("1");
1383
+ const [intervalUnit, setIntervalUnit] = useState("hours");
1384
+ const [loading, setLoading] = useState(false);
1385
+
1386
+ const UNIT_MULTIPLIERS = { seconds: 1, minutes: 60, hours: 3600, days: 86400 };
1387
+
1388
+ const loadJobs = () => API.cronList().then(setJobs).catch(() => {});
1389
+ useEffect(loadJobs, []);
1390
+
1391
+ const add = async () => {
1392
+ if (!name.trim() || !message.trim()) return;
1393
+ setLoading(true);
1394
+ const totalSeconds = Number(intervalValue) * (UNIT_MULTIPLIERS[intervalUnit] || 1);
1395
+ await API.cronAdd({ name: name.trim(), message: message.trim(), schedule: { kind: "every", everyMs: totalSeconds * 1000 } }).catch(() => {});
1396
+ setName("");
1397
+ setMessage("");
1398
+ setIntervalValue("1");
1399
+ setIntervalUnit("hours");
1400
+ setLoading(false);
1401
+ await loadJobs();
1402
+ };
1403
+
1404
+ const formatInterval = (job) => {
1405
+ if (job.schedule.kind !== "every") return job.schedule.kind;
1406
+ const totalSec = (job.schedule.everyMs || 0) / 1000;
1407
+ if (totalSec >= 86400) return `Every ${(totalSec / 86400).toFixed(totalSec % 86400 === 0 ? 0 : 1)} day${totalSec >= 172800 ? "s" : ""}`;
1408
+ if (totalSec >= 3600) return `Every ${(totalSec / 3600).toFixed(totalSec % 3600 === 0 ? 0 : 1)} hour${totalSec >= 7200 ? "s" : ""}`;
1409
+ if (totalSec >= 60) return `Every ${(totalSec / 60).toFixed(totalSec % 60 === 0 ? 0 : 1)} minute${totalSec >= 120 ? "s" : ""}`;
1410
+ return `Every ${totalSec} second${totalSec !== 1 ? "s" : ""}`;
1411
+ };
1412
+
1413
+ return React.createElement("div", { className: "panel" },
1414
+ React.createElement("h2", { className: "panel-title" }, "Cron Jobs"),
1415
+ React.createElement("div", { className: "form-group" },
1416
+ React.createElement("label", { className: "form-label" }, "Job Name"),
1417
+ React.createElement("input", {
1418
+ className: "form-input",
1419
+ value: name,
1420
+ onChange: e => setName(e.target.value),
1421
+ placeholder: "e.g., Daily Reminder"
1422
+ }),
1423
+ ),
1424
+ React.createElement("div", { className: "form-group" },
1425
+ React.createElement("label", { className: "form-label" }, "Message"),
1426
+ React.createElement("textarea", {
1427
+ className: "form-input",
1428
+ style: { resize: "vertical", minHeight: "80px" },
1429
+ value: message,
1430
+ onChange: e => setMessage(e.target.value),
1431
+ placeholder: "Cron job message"
1432
+ }),
1433
+ ),
1434
+ React.createElement("div", { className: "form-group" },
1435
+ React.createElement("label", { className: "form-label" }, "Run Every"),
1436
+ React.createElement("div", { className: "interval-row" },
1437
+ React.createElement("div", { className: "form-group interval-value" },
1438
+ React.createElement("input", {
1439
+ className: "form-input",
1440
+ type: "number",
1441
+ min: "1",
1442
+ value: intervalValue,
1443
+ onChange: e => setIntervalValue(e.target.value),
1444
+ }),
1445
+ ),
1446
+ React.createElement("div", { className: "form-group interval-unit" },
1447
+ React.createElement("select", {
1448
+ className: "form-input",
1449
+ value: intervalUnit,
1450
+ onChange: e => setIntervalUnit(e.target.value),
1451
+ },
1452
+ React.createElement("option", { value: "seconds" }, "Seconds"),
1453
+ React.createElement("option", { value: "minutes" }, "Minutes"),
1454
+ React.createElement("option", { value: "hours" }, "Hours"),
1455
+ React.createElement("option", { value: "days" }, "Days"),
1456
+ ),
1457
+ ),
1458
+ ),
1459
+ ),
1460
+ React.createElement("div", { className: "form-group" },
1461
+ React.createElement("button", { className: "btn-primary", onClick: add, disabled: loading }, loading ? "Adding..." : "Add Job"),
1462
+ ),
1463
+ React.createElement("div", { className: "divider" }),
1464
+ jobs.length === 0 && React.createElement("div", { className: "empty-state" }, "No scheduled jobs yet"),
1465
+ jobs.map(job =>
1466
+ React.createElement("div", { key: job.id, className: "job-card" },
1467
+ React.createElement("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: "8px" } },
1468
+ React.createElement("div", { className: "job-name" }, job.name),
1469
+ React.createElement("div", { className: "status-indicator" },
1470
+ React.createElement("div", { className: `status-dot ${job.enabled ? "active" : "inactive"}` }),
1471
+ React.createElement("span", { style: { fontSize: "12px", color: job.enabled ? "#22c55e" : "#ef4444" } }, job.enabled ? "Active" : "Disabled"),
1472
+ ),
1473
+ ),
1474
+ React.createElement("div", { className: "job-details" }, `${formatInterval(job)} • ID: ${job.id}`),
1475
+ React.createElement("div", { className: "btn-group" },
1476
+ React.createElement("button", {
1477
+ className: "btn-secondary",
1478
+ onClick: async () => { await API.cronRun(job.id).catch(() => {}); await loadJobs(); }
1479
+ }, "Run Now"),
1480
+ React.createElement("button", {
1481
+ className: "btn-secondary",
1482
+ onClick: async () => { await API.cronEnable(job.id, !job.enabled).catch(() => {}); await loadJobs(); }
1483
+ }, job.enabled ? "Disable" : "Enable"),
1484
+ React.createElement("button", {
1485
+ className: "btn-secondary",
1486
+ onClick: async () => { await API.cronDel(job.id).catch(() => {}); await loadJobs(); }
1487
+ }, "Delete"),
1488
+ ),
1489
+ )
1490
+ ),
1491
+ );
1492
+ }
1493
+
1494
+ function StatusView() {
1495
+ const [status, setStatus] = useState(null);
1496
+ const [uptime, setUptime] = useState(0);
1497
+
1498
+ useEffect(() => {
1499
+ API.status().then(setStatus).catch(() => {});
1500
+ const start = Date.now();
1501
+ const timer = setInterval(() => setUptime(Math.floor((Date.now() - start) / 1000)), 1000);
1502
+ return () => clearInterval(timer);
1503
+ }, []);
1504
+
1505
+ const formatUptime = (s) => {
1506
+ const h = Math.floor(s / 3600);
1507
+ const m = Math.floor((s % 3600) / 60);
1508
+ const sec = s % 60;
1509
+ return `${h}h ${m}m ${sec}s`;
1510
+ };
1511
+
1512
+ if (!status) return React.createElement("div", { className: "panel" }, "Loading...");
1513
+
1514
+ const isGatewayUp = !!status.provider;
1515
+ const heartbeatActive = !!(status.gateway?.heartbeat?.enabled);
1516
+
1517
+ return React.createElement("div", { className: "panel" },
1518
+ React.createElement("h2", { className: "panel-title" }, "System Status"),
1519
+ React.createElement("div", { className: "status-banner" },
1520
+ React.createElement("div", { className: "status-banner-left" },
1521
+ React.createElement("div", { className: `status-banner-dot ${isGatewayUp ? "active" : "inactive"}` }),
1522
+ React.createElement("div", null,
1523
+ React.createElement("div", { className: "status-banner-text" }, isGatewayUp ? "Gateway Running" : "Gateway Offline"),
1524
+ React.createElement("div", { className: "status-banner-sub" }, isGatewayUp ? `Uptime: ${formatUptime(uptime)}` : "Unable to reach gateway"),
1525
+ ),
1526
+ ),
1527
+ ),
1528
+ React.createElement("div", { className: "status-grid" },
1529
+ React.createElement("div", { className: "status-card" },
1530
+ React.createElement("div", { className: "status-card-header" },
1531
+ React.createElement("i", { className: "icon-cpu" }),
1532
+ React.createElement("div", { className: "status-card-title" }, "Provider"),
1533
+ ),
1534
+ React.createElement("div", { className: "status-card-value" }, status.provider || "—"),
1535
+ ),
1536
+ React.createElement("div", { className: "status-card" },
1537
+ React.createElement("div", { className: "status-card-header" },
1538
+ React.createElement("i", { className: "icon-brain" }),
1539
+ React.createElement("div", { className: "status-card-title" }, "Model"),
1540
+ ),
1541
+ React.createElement("div", { className: "status-card-value", style: { fontSize: "16px", wordBreak: "break-all" } }, status.model || "—"),
1542
+ ),
1543
+ React.createElement("div", { className: "status-card" },
1544
+ React.createElement("div", { className: "status-card-header" },
1545
+ React.createElement("i", { className: "icon-clock" }),
1546
+ React.createElement("div", { className: "status-card-title" }, "Cron Jobs"),
1547
+ ),
1548
+ React.createElement("div", { className: "status-card-value" }, String(status.cron?.jobs ?? 0)),
1549
+ ),
1550
+ React.createElement("div", { className: "status-card" },
1551
+ React.createElement("div", { className: "status-card-header" },
1552
+ React.createElement("i", { className: "icon-heart-pulse" }),
1553
+ React.createElement("div", { className: "status-card-title" }, "Heartbeat"),
1554
+ ),
1555
+ React.createElement("div", { className: "status-indicator" },
1556
+ React.createElement("div", { className: `status-dot ${heartbeatActive ? "active" : "inactive"}` }),
1557
+ React.createElement("span", { style: { color: heartbeatActive ? "#22c55e" : "#ef4444" } }, heartbeatActive ? "Active" : "Inactive"),
1558
+ ),
1559
+ ),
1560
+ ),
1561
+ );
1562
+ }
1563
+
1564
+ createRoot(document.getElementById("root")).render(React.createElement(App));
1565
+ </script>
1566
+ </body>
1567
+ </html>