spora 0.3.1 → 0.3.3

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 (83) hide show
  1. package/package.json +1 -1
  2. package/dist/account-creator-SETL5CGT.js +0 -498
  3. package/dist/account-creator-SETL5CGT.js.map +0 -1
  4. package/dist/chunk-FCAK5FYQ.js +0 -127
  5. package/dist/chunk-FCAK5FYQ.js.map +0 -1
  6. package/dist/chunk-GJFBWIW3.js +0 -622
  7. package/dist/chunk-GJFBWIW3.js.map +0 -1
  8. package/dist/chunk-HERI4RPY.js +0 -156
  9. package/dist/chunk-HERI4RPY.js.map +0 -1
  10. package/dist/chunk-J7J557HV.js +0 -47
  11. package/dist/chunk-J7J557HV.js.map +0 -1
  12. package/dist/chunk-JWMADEQO.js +0 -57
  13. package/dist/chunk-JWMADEQO.js.map +0 -1
  14. package/dist/chunk-LRKBNKMQ.js +0 -79
  15. package/dist/chunk-LRKBNKMQ.js.map +0 -1
  16. package/dist/chunk-NLWU5432.js +0 -32
  17. package/dist/chunk-NLWU5432.js.map +0 -1
  18. package/dist/chunk-POEDIDM6.js +0 -56
  19. package/dist/chunk-POEDIDM6.js.map +0 -1
  20. package/dist/chunk-Q7YS3AIK.js +0 -63
  21. package/dist/chunk-Q7YS3AIK.js.map +0 -1
  22. package/dist/chunk-QHFM2YW6.js +0 -159
  23. package/dist/chunk-QHFM2YW6.js.map +0 -1
  24. package/dist/chunk-R7PAD4OL.js +0 -44
  25. package/dist/chunk-R7PAD4OL.js.map +0 -1
  26. package/dist/chunk-RNVEWVDN.js +0 -129
  27. package/dist/chunk-RNVEWVDN.js.map +0 -1
  28. package/dist/chunk-SUFTVQME.js +0 -82
  29. package/dist/chunk-SUFTVQME.js.map +0 -1
  30. package/dist/chunk-SXMDYUK3.js +0 -80
  31. package/dist/chunk-SXMDYUK3.js.map +0 -1
  32. package/dist/chunk-TQWLKICD.js +0 -660
  33. package/dist/chunk-TQWLKICD.js.map +0 -1
  34. package/dist/chunk-YZ7RWJ6Z.js +0 -262
  35. package/dist/chunk-YZ7RWJ6Z.js.map +0 -1
  36. package/dist/cli.js +0 -654
  37. package/dist/cli.js.map +0 -1
  38. package/dist/client-23THPNVL.js +0 -382
  39. package/dist/client-23THPNVL.js.map +0 -1
  40. package/dist/client-NVI3ZD4G.js +0 -411
  41. package/dist/client-NVI3ZD4G.js.map +0 -1
  42. package/dist/colony-J4EZQI37.js +0 -229
  43. package/dist/colony-J4EZQI37.js.map +0 -1
  44. package/dist/config-QRBOL4NX.js +0 -14
  45. package/dist/config-QRBOL4NX.js.map +0 -1
  46. package/dist/crypto-ZVWJLD2J.js +0 -14
  47. package/dist/crypto-ZVWJLD2J.js.map +0 -1
  48. package/dist/decision-engine-WBD36PZI.js +0 -19
  49. package/dist/decision-engine-WBD36PZI.js.map +0 -1
  50. package/dist/goals-IM4AEHS4.js +0 -12
  51. package/dist/goals-IM4AEHS4.js.map +0 -1
  52. package/dist/heartbeat-TUV5IREO.js +0 -317
  53. package/dist/heartbeat-TUV5IREO.js.map +0 -1
  54. package/dist/identity-LN2R4KJU.js +0 -26
  55. package/dist/identity-LN2R4KJU.js.map +0 -1
  56. package/dist/image-search-SZVMGWLN.js +0 -45
  57. package/dist/image-search-SZVMGWLN.js.map +0 -1
  58. package/dist/init-FR5OTDRJ.js +0 -403
  59. package/dist/init-FR5OTDRJ.js.map +0 -1
  60. package/dist/llm-MHZG2VHU.js +0 -16
  61. package/dist/llm-MHZG2VHU.js.map +0 -1
  62. package/dist/mcp-server.js +0 -773
  63. package/dist/mcp-server.js.map +0 -1
  64. package/dist/memory-J6AYZ5Y2.js +0 -26
  65. package/dist/memory-J6AYZ5Y2.js.map +0 -1
  66. package/dist/memory-JMXU3UXR.js +0 -26
  67. package/dist/memory-JMXU3UXR.js.map +0 -1
  68. package/dist/paths-KXOWF2B2.js +0 -13
  69. package/dist/paths-KXOWF2B2.js.map +0 -1
  70. package/dist/performance-7G6R6ELJ.js +0 -18
  71. package/dist/performance-7G6R6ELJ.js.map +0 -1
  72. package/dist/prompt-builder-EQYCNP63.js +0 -28
  73. package/dist/prompt-builder-EQYCNP63.js.map +0 -1
  74. package/dist/queue-MLRTMJRE.js +0 -14
  75. package/dist/queue-MLRTMJRE.js.map +0 -1
  76. package/dist/strategy-TOVFBIZQ.js +0 -12
  77. package/dist/strategy-TOVFBIZQ.js.map +0 -1
  78. package/dist/web-chat/chat.html +0 -1343
  79. package/dist/web-chat/logo.png +0 -0
  80. package/dist/web-chat-RZKDQYJ4.js +0 -802
  81. package/dist/web-chat-RZKDQYJ4.js.map +0 -1
  82. package/dist/x-client-HUXCQOAW.js +0 -12
  83. package/dist/x-client-HUXCQOAW.js.map +0 -1
@@ -1,1343 +0,0 @@
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>Spora</title>
7
- <style>
8
- * {
9
- margin: 0;
10
- padding: 0;
11
- box-sizing: border-box;
12
- }
13
-
14
- body {
15
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
16
- background: #0a0a0a;
17
- color: #fff;
18
- height: 100vh;
19
- display: flex;
20
- flex-direction: column;
21
- }
22
-
23
- .top-bar {
24
- position: relative;
25
- padding: 0.625rem 1rem;
26
- display: flex;
27
- align-items: center;
28
- background: #0a0a0a;
29
- flex-shrink: 0;
30
- }
31
-
32
- .top-bar .logo-img {
33
- height: 20px;
34
- width: auto;
35
- opacity: 0.5;
36
- }
37
-
38
- /* Twitter DM-style profile header */
39
- .profile-header {
40
- padding: 0.5rem 1rem 1rem;
41
- display: flex;
42
- flex-direction: column;
43
- align-items: center;
44
- background: #0a0a0a;
45
- flex-shrink: 0;
46
- }
47
-
48
- .profile-avatar {
49
- width: 56px;
50
- height: 56px;
51
- border-radius: 50%;
52
- background: #1a1a1a;
53
- display: flex;
54
- align-items: center;
55
- justify-content: center;
56
- overflow: hidden;
57
- border: 2px solid rgba(255, 255, 255, 0.1);
58
- margin-bottom: 0.5rem;
59
- }
60
-
61
- .profile-avatar img {
62
- width: 100%;
63
- height: 100%;
64
- object-fit: cover;
65
- }
66
-
67
- .profile-avatar-letter {
68
- font-size: 1.25rem;
69
- font-weight: 700;
70
- color: #fff;
71
- }
72
-
73
- .profile-name {
74
- font-size: 0.9375rem;
75
- font-weight: 700;
76
- color: #fff;
77
- line-height: 1.2;
78
- }
79
-
80
- .profile-handle {
81
- font-size: 0.8125rem;
82
- color: rgba(255, 255, 255, 0.45);
83
- margin-top: 0.125rem;
84
- }
85
-
86
- .profile-meta {
87
- display: flex;
88
- align-items: center;
89
- gap: 0.625rem;
90
- margin-top: 0.375rem;
91
- }
92
-
93
- .profile-joined {
94
- font-size: 0.75rem;
95
- color: rgba(255, 255, 255, 0.35);
96
- }
97
-
98
- .profile-status {
99
- display: flex;
100
- align-items: center;
101
- gap: 0.3rem;
102
- }
103
-
104
- .status-dot {
105
- width: 6px;
106
- height: 6px;
107
- border-radius: 50%;
108
- background: #389e77;
109
- animation: pulse 2s infinite;
110
- }
111
-
112
- .status-label {
113
- font-size: 0.75rem;
114
- color: rgba(255, 255, 255, 0.35);
115
- }
116
-
117
- @keyframes pulse {
118
- 0%, 100% { opacity: 1; }
119
- 50% { opacity: 0.5; }
120
- }
121
-
122
- .chat-container {
123
- flex: 1;
124
- overflow-y: auto;
125
- padding: 1.25rem;
126
- display: flex;
127
- flex-direction: column;
128
- gap: 0.875rem;
129
- }
130
-
131
- .chat-container::-webkit-scrollbar {
132
- width: 4px;
133
- }
134
-
135
- .chat-container::-webkit-scrollbar-track {
136
- background: transparent;
137
- }
138
-
139
- .chat-container::-webkit-scrollbar-thumb {
140
- background: rgba(255, 255, 255, 0.1);
141
- border-radius: 4px;
142
- }
143
-
144
- .message {
145
- display: flex;
146
- gap: 0.625rem;
147
- animation: fadeIn 0.3s ease-in;
148
- }
149
-
150
- @keyframes fadeIn {
151
- from { opacity: 0; transform: translateY(8px); }
152
- to { opacity: 1; transform: translateY(0); }
153
- }
154
-
155
- .message.user {
156
- justify-content: flex-end;
157
- }
158
-
159
- .message.assistant {
160
- justify-content: flex-start;
161
- }
162
-
163
- .message-avatar {
164
- width: 32px;
165
- height: 32px;
166
- border-radius: 50%;
167
- background: #1a1a1a;
168
- display: flex;
169
- align-items: center;
170
- justify-content: center;
171
- flex-shrink: 0;
172
- overflow: hidden;
173
- border: 1px solid rgba(255, 255, 255, 0.08);
174
- }
175
-
176
- .message-avatar img {
177
- width: 100%;
178
- height: 100%;
179
- object-fit: cover;
180
- }
181
-
182
- .message.user .message-avatar {
183
- display: none;
184
- }
185
-
186
- .message-content {
187
- max-width: 80%;
188
- padding: 0.5rem 0.75rem;
189
- border-radius: 0.75rem;
190
- font-size: 0.8125rem;
191
- line-height: 1.45;
192
- white-space: pre-wrap;
193
- }
194
-
195
- .message.user .message-content {
196
- background: #389e77;
197
- color: #fff;
198
- border-bottom-right-radius: 0.25rem;
199
- }
200
-
201
- .message.assistant .message-content {
202
- background: #141414;
203
- border: 1px solid rgba(255, 255, 255, 0.06);
204
- border-bottom-left-radius: 0.25rem;
205
- color: rgba(255, 255, 255, 0.9);
206
- }
207
-
208
- .input-container {
209
- padding: 1rem 1.25rem;
210
- display: flex;
211
- gap: 0.5rem;
212
- background: #0a0a0a;
213
- }
214
-
215
- .input-box {
216
- flex: 1;
217
- background: #141414;
218
- border: 1px solid rgba(255, 255, 255, 0.08);
219
- border-radius: 0.625rem;
220
- padding: 0.625rem 0.875rem;
221
- color: #fff;
222
- font-size: 0.8125rem;
223
- font-family: inherit;
224
- outline: none;
225
- transition: border-color 0.2s;
226
- }
227
-
228
- .input-box:focus {
229
- border-color: #389e77;
230
- }
231
-
232
- .input-box::placeholder {
233
- color: rgba(255, 255, 255, 0.25);
234
- }
235
-
236
- .send-button {
237
- background: #389e77;
238
- color: #fff;
239
- border: none;
240
- padding: 0.625rem 1.125rem;
241
- border-radius: 0.625rem;
242
- font-weight: 600;
243
- font-size: 0.8125rem;
244
- cursor: pointer;
245
- transition: background 0.2s, transform 0.15s;
246
- }
247
-
248
- .send-button:hover:not(:disabled) {
249
- background: #4ab88a;
250
- transform: scale(1.02);
251
- }
252
-
253
- .send-button:disabled {
254
- opacity: 0.4;
255
- cursor: not-allowed;
256
- }
257
-
258
- .loading {
259
- display: flex;
260
- gap: 0.25rem;
261
- padding: 0.125rem 0;
262
- }
263
-
264
- .loading-dot {
265
- width: 5px;
266
- height: 5px;
267
- border-radius: 50%;
268
- background: rgba(255, 255, 255, 0.3);
269
- animation: bounce 1.4s infinite ease-in-out both;
270
- }
271
-
272
- .loading-dot:nth-child(1) { animation-delay: -0.32s; }
273
- .loading-dot:nth-child(2) { animation-delay: -0.16s; }
274
-
275
- @keyframes bounce {
276
- 0%, 80%, 100% { transform: scale(0); }
277
- 40% { transform: scale(1); }
278
- }
279
-
280
- /* X/Twitter-style tweet preview card */
281
- .tweet-card {
282
- background: #16181c;
283
- border: 1px solid rgba(255, 255, 255, 0.12);
284
- border-radius: 12px;
285
- padding: 12px;
286
- margin-top: 8px;
287
- max-width: 100%;
288
- cursor: pointer;
289
- transition: background 0.15s;
290
- }
291
-
292
- .tweet-card:hover {
293
- background: #1d1f23;
294
- }
295
-
296
- .tweet-card-header {
297
- display: flex;
298
- align-items: center;
299
- gap: 8px;
300
- margin-bottom: 6px;
301
- }
302
-
303
- .tweet-card-avatar {
304
- width: 32px;
305
- height: 32px;
306
- border-radius: 50%;
307
- background: #389e77;
308
- display: flex;
309
- align-items: center;
310
- justify-content: center;
311
- flex-shrink: 0;
312
- overflow: hidden;
313
- }
314
-
315
- .tweet-card-avatar img {
316
- width: 100%;
317
- height: 100%;
318
- object-fit: cover;
319
- }
320
-
321
- .tweet-card-avatar-letter {
322
- font-size: 0.75rem;
323
- font-weight: 700;
324
- color: #fff;
325
- }
326
-
327
- .tweet-card-names {
328
- display: flex;
329
- flex-direction: column;
330
- min-width: 0;
331
- }
332
-
333
- .tweet-card-name {
334
- font-size: 0.8125rem;
335
- font-weight: 700;
336
- color: #e7e9ea;
337
- line-height: 1.2;
338
- white-space: nowrap;
339
- overflow: hidden;
340
- text-overflow: ellipsis;
341
- }
342
-
343
- .tweet-card-handle {
344
- font-size: 0.75rem;
345
- color: #71767b;
346
- line-height: 1.2;
347
- }
348
-
349
- .tweet-card-badge {
350
- margin-left: auto;
351
- flex-shrink: 0;
352
- }
353
-
354
- .tweet-card-badge svg {
355
- width: 16px;
356
- height: 16px;
357
- }
358
-
359
- .tweet-card-text {
360
- font-size: 0.8125rem;
361
- color: #e7e9ea;
362
- line-height: 1.4;
363
- white-space: pre-wrap;
364
- word-break: break-word;
365
- }
366
-
367
- .tweet-card-footer {
368
- display: flex;
369
- align-items: center;
370
- justify-content: space-between;
371
- margin-top: 8px;
372
- padding-top: 6px;
373
- }
374
-
375
- .tweet-card-meta {
376
- font-size: 0.6875rem;
377
- color: #71767b;
378
- }
379
-
380
- .tweet-card-action-label {
381
- font-size: 0.6875rem;
382
- color: #389e77;
383
- font-weight: 600;
384
- text-transform: uppercase;
385
- letter-spacing: 0.03em;
386
- }
387
-
388
- .tweet-card-link {
389
- display: flex;
390
- align-items: center;
391
- gap: 4px;
392
- font-size: 0.6875rem;
393
- color: #1d9bf0;
394
- text-decoration: none;
395
- transition: opacity 0.15s;
396
- }
397
-
398
- .tweet-card-link:hover {
399
- opacity: 0.8;
400
- }
401
-
402
- .tweet-card-link svg {
403
- width: 12px;
404
- height: 12px;
405
- }
406
-
407
- /* Notification cards (like, retweet, follow) */
408
- .notif-card {
409
- display: flex;
410
- align-items: flex-start;
411
- gap: 10px;
412
- padding: 10px 12px;
413
- margin: 6px 0;
414
- background: rgba(255, 255, 255, 0.03);
415
- border-radius: 10px;
416
- border: 1px solid rgba(255, 255, 255, 0.06);
417
- }
418
-
419
- .notif-icon {
420
- flex-shrink: 0;
421
- width: 28px;
422
- height: 28px;
423
- display: flex;
424
- align-items: center;
425
- justify-content: center;
426
- border-radius: 50%;
427
- }
428
-
429
- .notif-icon.like {
430
- background: rgba(249, 24, 128, 0.12);
431
- color: #f91880;
432
- }
433
-
434
- .notif-icon.retweet {
435
- background: rgba(0, 186, 124, 0.12);
436
- color: #00ba7c;
437
- }
438
-
439
- .notif-icon.follow {
440
- background: rgba(29, 155, 240, 0.12);
441
- color: #1d9bf0;
442
- }
443
-
444
- .notif-icon svg {
445
- width: 14px;
446
- height: 14px;
447
- }
448
-
449
- .notif-body {
450
- flex: 1;
451
- min-width: 0;
452
- }
453
-
454
- .notif-label {
455
- font-size: 0.75rem;
456
- color: #71767b;
457
- margin-bottom: 2px;
458
- }
459
-
460
- .notif-label strong {
461
- color: #e7e9ea;
462
- font-weight: 600;
463
- }
464
-
465
- .notif-text {
466
- font-size: 0.8rem;
467
- color: rgba(255, 255, 255, 0.5);
468
- overflow: hidden;
469
- text-overflow: ellipsis;
470
- white-space: nowrap;
471
- max-width: 100%;
472
- }
473
-
474
- /* Intelligence bar */
475
- .intelligence-bar {
476
- position: absolute;
477
- left: 50%;
478
- transform: translateX(-50%);
479
- width: 40%;
480
- display: flex;
481
- flex-direction: column;
482
- align-items: center;
483
- gap: 3px;
484
- }
485
-
486
- .intelligence-label {
487
- font-size: 0.5625rem;
488
- color: rgba(255, 255, 255, 0.2);
489
- letter-spacing: 0.03em;
490
- white-space: nowrap;
491
- text-align: center;
492
- }
493
-
494
- .intelligence-track {
495
- width: 100%;
496
- height: 3px;
497
- background: rgba(255, 255, 255, 0.06);
498
- border-radius: 1.5px;
499
- overflow: hidden;
500
- }
501
-
502
- .intelligence-fill {
503
- height: 100%;
504
- width: 0%;
505
- border-radius: 1.5px;
506
- background: linear-gradient(90deg, #389e77 0%, #4fc3a0 50%, #7ee8c7 100%);
507
- transition: width 1.5s cubic-bezier(0.4, 0, 0.2, 1);
508
- }
509
-
510
- /* Heartbeat config button */
511
- .heartbeat-config {
512
- position: relative;
513
- margin-left: auto;
514
- }
515
-
516
- .heartbeat-btn {
517
- background: none;
518
- border: none;
519
- cursor: pointer;
520
- padding: 4px;
521
- display: flex;
522
- align-items: center;
523
- justify-content: center;
524
- opacity: 0.45;
525
- transition: opacity 0.2s;
526
- }
527
-
528
- .heartbeat-btn:hover {
529
- opacity: 0.8;
530
- }
531
-
532
- .heartbeat-btn svg {
533
- width: 18px;
534
- height: 18px;
535
- }
536
-
537
- .heartbeat-dropdown {
538
- display: none;
539
- position: absolute;
540
- top: 100%;
541
- right: 0;
542
- margin-top: 6px;
543
- background: #1a1a1a;
544
- border: 1px solid rgba(255, 255, 255, 0.12);
545
- border-radius: 10px;
546
- padding: 6px 0;
547
- min-width: 140px;
548
- z-index: 100;
549
- box-shadow: 0 8px 24px rgba(0,0,0,0.5);
550
- }
551
-
552
- .heartbeat-dropdown.open {
553
- display: block;
554
- animation: fadeIn 0.15s ease-out;
555
- }
556
-
557
- .heartbeat-dropdown-label {
558
- padding: 6px 14px;
559
- font-size: 0.6875rem;
560
- color: rgba(255, 255, 255, 0.35);
561
- text-transform: uppercase;
562
- letter-spacing: 0.05em;
563
- font-weight: 600;
564
- }
565
-
566
- .heartbeat-option {
567
- display: flex;
568
- align-items: center;
569
- justify-content: space-between;
570
- padding: 8px 14px;
571
- font-size: 0.8125rem;
572
- color: rgba(255, 255, 255, 0.75);
573
- cursor: pointer;
574
- transition: background 0.1s;
575
- }
576
-
577
- .heartbeat-option:hover {
578
- background: rgba(255, 255, 255, 0.06);
579
- }
580
-
581
- .heartbeat-option.active {
582
- color: #389e77;
583
- }
584
-
585
- .heartbeat-option.active::after {
586
- content: '';
587
- width: 6px;
588
- height: 6px;
589
- border-radius: 50%;
590
- background: #389e77;
591
- }
592
-
593
- /* Sleep indicator — styled like a chat message, lives inside chat container */
594
- .sleep-indicator {
595
- display: none;
596
- gap: 0.625rem;
597
- animation: fadeIn 0.4s ease-in;
598
- }
599
-
600
- .sleep-indicator.visible {
601
- display: flex;
602
- }
603
-
604
- .sleep-avatar {
605
- width: 32px;
606
- height: 32px;
607
- border-radius: 50%;
608
- background: #1a1a1a;
609
- display: flex;
610
- align-items: center;
611
- justify-content: center;
612
- flex-shrink: 0;
613
- overflow: hidden;
614
- border: 1px solid rgba(255, 255, 255, 0.08);
615
- }
616
-
617
- .sleep-avatar img {
618
- width: 100%;
619
- height: 100%;
620
- object-fit: cover;
621
- }
622
-
623
- .sleep-bubble {
624
- display: flex;
625
- align-items: center;
626
- gap: 8px;
627
- background: #141414;
628
- border: 1px solid rgba(255, 255, 255, 0.06);
629
- border-radius: 0.75rem;
630
- border-bottom-left-radius: 0.25rem;
631
- padding: 0.5rem 0.75rem;
632
- }
633
-
634
- .sleep-text {
635
- font-size: 0.8125rem;
636
- color: rgba(255, 255, 255, 0.4);
637
- white-space: nowrap;
638
- }
639
-
640
- .sleep-zzz {
641
- display: flex;
642
- align-items: baseline;
643
- gap: 1px;
644
- }
645
-
646
- .sleep-zzz span {
647
- display: inline-block;
648
- font-weight: 700;
649
- color: rgba(255, 255, 255, 0.25);
650
- animation: zzzFloat 2s ease-in-out infinite;
651
- }
652
-
653
- .sleep-zzz span:nth-child(1) {
654
- font-size: 0.5rem;
655
- animation-delay: 0s;
656
- }
657
- .sleep-zzz span:nth-child(2) {
658
- font-size: 0.65rem;
659
- animation-delay: 0.3s;
660
- }
661
- .sleep-zzz span:nth-child(3) {
662
- font-size: 0.8rem;
663
- animation-delay: 0.6s;
664
- }
665
-
666
- @keyframes zzzFloat {
667
- 0%, 100% { opacity: 0.3; transform: translateY(0); }
668
- 50% { opacity: 1; transform: translateY(-3px); }
669
- }
670
-
671
- .sleep-countdown {
672
- font-size: 0.6875rem;
673
- color: rgba(255, 255, 255, 0.2);
674
- font-variant-numeric: tabular-nums;
675
- margin-left: 2px;
676
- }
677
- </style>
678
- </head>
679
- <body>
680
- <div class="top-bar">
681
- <img class="logo-img" src="/logo.png" alt="Spora" />
682
- <div class="intelligence-bar" id="intelligenceBar">
683
- <span class="intelligence-label">Intelligence</span>
684
- <div class="intelligence-track">
685
- <div class="intelligence-fill" id="intelligenceFill"></div>
686
- </div>
687
- </div>
688
- <div class="heartbeat-config">
689
- <button class="heartbeat-btn" id="heartbeatBtn" title="Heartbeat interval">
690
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" color="#e05a5a">
691
- <path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z" fill="#e05a5a" stroke="#e05a5a"/>
692
- </svg>
693
- </button>
694
- <div class="heartbeat-dropdown" id="heartbeatDropdown">
695
- <div class="heartbeat-dropdown-label">Heartbeat interval</div>
696
- <div class="heartbeat-option" data-ms="300000">5 minutes</div>
697
- <div class="heartbeat-option" data-ms="1200000">20 minutes</div>
698
- <div class="heartbeat-option" data-ms="3600000">1 hour</div>
699
- <div class="heartbeat-option" data-ms="21600000">6 hours</div>
700
- </div>
701
- </div>
702
- </div>
703
- <div class="profile-header">
704
- <div class="profile-avatar" id="profileAvatar">
705
- <span class="profile-avatar-letter" id="profileAvatarLetter"></span>
706
- </div>
707
- <div class="profile-name" id="profileName">Your Spore</div>
708
- <div class="profile-handle" id="profileHandle"></div>
709
- <div class="profile-meta">
710
- <span class="profile-joined" id="profileJoined"></span>
711
- <div class="profile-status">
712
- <span class="status-dot"></span>
713
- <span class="status-label">Online</span>
714
- </div>
715
- </div>
716
- </div>
717
-
718
- <div class="chat-container" id="chatContainer">
719
- <div class="sleep-indicator" id="sleepIndicator">
720
- <div class="sleep-avatar" id="sleepAvatar"></div>
721
- <div class="sleep-bubble">
722
- <span class="sleep-text">Sleeping</span>
723
- <div class="sleep-zzz">
724
- <span>z</span><span>z</span><span>z</span>
725
- </div>
726
- <span class="sleep-countdown" id="sleepCountdown"></span>
727
- </div>
728
- </div>
729
- </div>
730
-
731
- <div class="input-container">
732
- <input
733
- type="text"
734
- class="input-box"
735
- id="messageInput"
736
- placeholder="Message your agent..."
737
- autocomplete="off"
738
- />
739
- <button class="send-button" id="sendButton">Send</button>
740
- </div>
741
-
742
- <script>
743
- const chatContainer = document.getElementById('chatContainer');
744
- const messageInput = document.getElementById('messageInput');
745
- const sendButton = document.getElementById('sendButton');
746
- const sleepIndicator = document.getElementById('sleepIndicator');
747
-
748
- let isLoading = false;
749
- let agentName = 'Your Spore';
750
- let agentPfp = null;
751
-
752
- function formatJoinedDate(isoString) {
753
- try {
754
- const date = new Date(isoString);
755
- const month = date.toLocaleString('en-US', { month: 'long' });
756
- const year = date.getFullYear();
757
- return `Joined ${month} ${year}`;
758
- } catch {
759
- return '';
760
- }
761
- }
762
-
763
- // Fetch agent identity and populate profile header
764
- async function init() {
765
- try {
766
- const response = await fetch('/api/identity');
767
- const data = await response.json();
768
- if (data.identity) {
769
- agentName = data.identity.name || 'Your Spore';
770
- agentPfp = data.identity.profileImage || null;
771
-
772
- // Update page title
773
- document.title = `${agentName} - Spora`;
774
-
775
- // Populate profile header
776
- document.getElementById('profileName').textContent = agentName;
777
-
778
- if (data.identity.handle) {
779
- document.getElementById('profileHandle').textContent = `@${data.identity.handle}`;
780
- }
781
-
782
- if (data.identity.createdAt) {
783
- document.getElementById('profileJoined').textContent = formatJoinedDate(data.identity.createdAt);
784
- }
785
-
786
- // Profile avatar
787
- const avatarEl = document.getElementById('profileAvatar');
788
- const letterEl = document.getElementById('profileAvatarLetter');
789
- if (agentPfp) {
790
- const img = document.createElement('img');
791
- img.src = agentPfp;
792
- img.alt = agentName;
793
- img.onerror = function() {
794
- this.remove();
795
- letterEl.textContent = agentName.charAt(0).toUpperCase();
796
- letterEl.style.display = '';
797
- };
798
- letterEl.style.display = 'none';
799
- avatarEl.appendChild(img);
800
- } else {
801
- letterEl.textContent = agentName.charAt(0).toUpperCase();
802
- avatarEl.style.background = '#389e77';
803
- }
804
- }
805
- } catch (error) {
806
- console.error('Failed to load identity:', error);
807
- }
808
-
809
- // Show last message from previous session, or default welcome
810
- try {
811
- const lastMsgRes = await fetch('/api/last-message');
812
- const lastMsgData = await lastMsgRes.json();
813
- if (lastMsgData.content) {
814
- addMessage('assistant', lastMsgData.content);
815
- } else {
816
- addMessage('assistant', `What's good — I'm ${agentName}, your autonomous agent on X. I'm always watching the timeline, engaging with people, and posting my thoughts. Talk to me if you want to steer the vibe.`);
817
- }
818
- } catch {
819
- addMessage('assistant', `What's good — I'm ${agentName}, your autonomous agent on X. I'm always watching the timeline, engaging with people, and posting my thoughts. Talk to me if you want to steer the vibe.`);
820
- }
821
-
822
- // Load any existing messages
823
- await loadMessages();
824
- }
825
-
826
- async function loadMessages() {
827
- try {
828
- const response = await fetch('/api/messages');
829
- const data = await response.json();
830
- if (data.messages && data.messages.length > 0) {
831
- // Clear welcome message if there are existing messages (keep sleep indicator)
832
- Array.from(chatContainer.children).forEach(child => {
833
- if (child !== sleepIndicator) child.remove();
834
- });
835
- data.messages.forEach(msg => {
836
- addMessage(msg.role, msg.content, false);
837
- });
838
- }
839
- } catch (error) {
840
- console.error('Failed to load messages:', error);
841
- }
842
- }
843
-
844
- function createTweetCard(tweetData) {
845
- const card = document.createElement('div');
846
- card.className = 'tweet-card';
847
-
848
- const actionLabels = { post: 'Posted', reply: 'Replied', schedule: 'Scheduled' };
849
- const actionLabel = actionLabels[tweetData.action] || 'Tweeted';
850
-
851
- // Build tweet URL if we have a tweetId and handle
852
- const handleText = document.getElementById('profileHandle')?.textContent?.replace('@', '') || '';
853
- const tweetUrl = tweetData.tweetId && handleText
854
- ? `https://x.com/${handleText}/status/${tweetData.tweetId}`
855
- : null;
856
-
857
- // Header: avatar + name + handle
858
- const header = document.createElement('div');
859
- header.className = 'tweet-card-header';
860
-
861
- const avatar = document.createElement('div');
862
- avatar.className = 'tweet-card-avatar';
863
- if (agentPfp) {
864
- avatar.innerHTML = `<img src="${agentPfp}" alt="${agentName}" />`;
865
- } else {
866
- avatar.innerHTML = `<span class="tweet-card-avatar-letter">${agentName.charAt(0).toUpperCase()}</span>`;
867
- }
868
- header.appendChild(avatar);
869
-
870
- const names = document.createElement('div');
871
- names.className = 'tweet-card-names';
872
- names.innerHTML = `
873
- <span class="tweet-card-name">${escapeHtml(agentName)}</span>
874
- <span class="tweet-card-handle">@${escapeHtml(handleText || 'agent')}</span>
875
- `;
876
- header.appendChild(names);
877
-
878
- // X logo badge
879
- const badge = document.createElement('div');
880
- badge.className = 'tweet-card-badge';
881
- badge.innerHTML = `<svg viewBox="0 0 24 24" fill="#71767b"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg>`;
882
- header.appendChild(badge);
883
-
884
- card.appendChild(header);
885
-
886
- // Tweet text
887
- const text = document.createElement('div');
888
- text.className = 'tweet-card-text';
889
- text.textContent = tweetData.content;
890
- card.appendChild(text);
891
-
892
- // Footer: action label + time + link
893
- const footer = document.createElement('div');
894
- footer.className = 'tweet-card-footer';
895
-
896
- const meta = document.createElement('div');
897
- meta.style.display = 'flex';
898
- meta.style.alignItems = 'center';
899
- meta.style.gap = '8px';
900
- meta.innerHTML = `
901
- <span class="tweet-card-action-label">${actionLabel}</span>
902
- <span class="tweet-card-meta">${new Date().toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' })}</span>
903
- `;
904
- footer.appendChild(meta);
905
-
906
- if (tweetUrl) {
907
- const link = document.createElement('a');
908
- link.className = 'tweet-card-link';
909
- link.href = tweetUrl;
910
- link.target = '_blank';
911
- link.rel = 'noopener noreferrer';
912
- link.innerHTML = `View on X <svg viewBox="0 0 24 24" fill="currentColor"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6M15 3h6v6M10 14L21 3"/></svg>`;
913
- footer.appendChild(link);
914
- }
915
-
916
- card.appendChild(footer);
917
-
918
- // Click whole card to open on X
919
- if (tweetUrl) {
920
- card.addEventListener('click', (e) => {
921
- if (e.target.closest('a')) return;
922
- window.open(tweetUrl, '_blank');
923
- });
924
- }
925
-
926
- return card;
927
- }
928
-
929
- function createNotifCard(type, data) {
930
- const card = document.createElement('div');
931
- card.className = 'notif-card';
932
-
933
- const icon = document.createElement('div');
934
- icon.className = `notif-icon ${type}`;
935
-
936
- if (type === 'like') {
937
- icon.innerHTML = '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>';
938
- } else if (type === 'retweet') {
939
- icon.innerHTML = '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M23.77 15.67a.749.749 0 0 0-1.06 0l-2.22 2.22V7.65a3.755 3.755 0 0 0-3.75-3.75h-5.85a.75.75 0 0 0 0 1.5h5.85a2.25 2.25 0 0 1 2.25 2.25v10.24l-2.22-2.22a.749.749 0 1 0-1.06 1.06l3.5 3.5a.749.749 0 0 0 1.06 0l3.5-3.5a.749.749 0 0 0 0-1.06zM.23 8.33a.749.749 0 0 0 1.06 0l2.22-2.22v10.24a3.755 3.755 0 0 0 3.75 3.75h5.85a.75.75 0 0 0 0-1.5H7.26a2.25 2.25 0 0 1-2.25-2.25V6.11l2.22 2.22a.749.749 0 1 0 1.06-1.06l-3.5-3.5a.749.749 0 0 0-1.06 0l-3.5 3.5a.749.749 0 0 0 0 1.06z"/></svg>';
940
- } else if (type === 'follow') {
941
- icon.innerHTML = '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M17.863 13.44c1.477 1.58 2.366 3.8 2.632 6.46l.11 1.1H3.395l.11-1.1c.266-2.66 1.155-4.88 2.632-6.46C7.627 11.85 9.648 11 12 11s4.373.85 5.863 2.44zM12 2C9.791 2 8 3.79 8 6s1.791 4 4 4 4-1.79 4-4-1.791-4-4-4z"/></svg>';
942
- }
943
-
944
- card.appendChild(icon);
945
-
946
- const body = document.createElement('div');
947
- body.className = 'notif-body';
948
-
949
- const label = document.createElement('div');
950
- label.className = 'notif-label';
951
-
952
- if (type === 'like') {
953
- label.innerHTML = 'Liked <strong>@' + escapeHtml(data.authorHandle) + '</strong>';
954
- } else if (type === 'retweet') {
955
- label.innerHTML = 'Retweeted <strong>@' + escapeHtml(data.authorHandle) + '</strong>';
956
- } else if (type === 'follow') {
957
- label.innerHTML = 'Followed <strong>@' + escapeHtml(data.handle) + '</strong>';
958
- }
959
-
960
- body.appendChild(label);
961
-
962
- if (data.text) {
963
- const text = document.createElement('div');
964
- text.className = 'notif-text';
965
- text.textContent = data.text.length > 80 ? data.text.slice(0, 80) + '...' : data.text;
966
- body.appendChild(text);
967
- }
968
-
969
- card.appendChild(body);
970
- return card;
971
- }
972
-
973
- function escapeHtml(text) {
974
- const div = document.createElement('div');
975
- div.textContent = text;
976
- return div.innerHTML;
977
- }
978
-
979
- function addMessage(role, content, animate = true) {
980
- const messageDiv = document.createElement('div');
981
- messageDiv.className = `message ${role}`;
982
- if (!animate) messageDiv.style.animation = 'none';
983
-
984
- if (role === 'assistant') {
985
- const avatar = document.createElement('div');
986
- avatar.className = 'message-avatar';
987
- if (agentPfp) {
988
- avatar.innerHTML = `<img src="${agentPfp}" alt="${agentName}" onerror="this.parentElement.textContent='${agentName.charAt(0).toUpperCase()}'" />`;
989
- } else {
990
- avatar.textContent = agentName.charAt(0).toUpperCase();
991
- avatar.style.background = '#389e77';
992
- avatar.style.color = '#fff';
993
- avatar.style.fontSize = '0.75rem';
994
- avatar.style.fontWeight = '700';
995
- }
996
- messageDiv.appendChild(avatar);
997
- }
998
-
999
- const contentDiv = document.createElement('div');
1000
- contentDiv.className = 'message-content';
1001
-
1002
- // Parse all special tags: <<TWEET:...>>, <<LIKE:...>>, <<RETWEET:...>>, <<FOLLOW:...>>
1003
- const tagPattern = /<<(TWEET|LIKE|RETWEET|FOLLOW):(.*?)>>/g;
1004
- const hasSpecialTags = tagPattern.test(content);
1005
-
1006
- if (hasSpecialTags) {
1007
- // Split content around all tags
1008
- const textParts = content.split(/<<(?:TWEET|LIKE|RETWEET|FOLLOW):.*?>>/g);
1009
- const tags = [...content.matchAll(/<<(TWEET|LIKE|RETWEET|FOLLOW):(.*?)>>/g)];
1010
-
1011
- // Add text before first tag
1012
- if (textParts[0]?.trim()) {
1013
- const textSpan = document.createElement('span');
1014
- textSpan.textContent = textParts[0].trim();
1015
- contentDiv.appendChild(textSpan);
1016
- }
1017
-
1018
- // Interleave tags and remaining text
1019
- tags.forEach((match, i) => {
1020
- try {
1021
- const tagType = match[1];
1022
- const data = JSON.parse(match[2]);
1023
-
1024
- if (tagType === 'TWEET') {
1025
- contentDiv.appendChild(createTweetCard(data));
1026
- } else if (tagType === 'LIKE') {
1027
- contentDiv.appendChild(createNotifCard('like', data));
1028
- } else if (tagType === 'RETWEET') {
1029
- contentDiv.appendChild(createNotifCard('retweet', data));
1030
- } else if (tagType === 'FOLLOW') {
1031
- contentDiv.appendChild(createNotifCard('follow', data));
1032
- }
1033
- } catch (e) {
1034
- // If JSON parse fails, skip silently
1035
- }
1036
-
1037
- // Text after this tag
1038
- if (textParts[i + 1]?.trim()) {
1039
- const textSpan = document.createElement('span');
1040
- textSpan.textContent = textParts[i + 1].trim();
1041
- contentDiv.appendChild(textSpan);
1042
- }
1043
- });
1044
- } else {
1045
- contentDiv.textContent = content;
1046
- }
1047
-
1048
- messageDiv.appendChild(contentDiv);
1049
-
1050
- // Insert before sleep indicator so it always stays at the bottom
1051
- chatContainer.insertBefore(messageDiv, sleepIndicator);
1052
- chatContainer.scrollTop = chatContainer.scrollHeight;
1053
- }
1054
-
1055
- function showLoading() {
1056
- const loadingDiv = document.createElement('div');
1057
- loadingDiv.className = 'message assistant';
1058
- loadingDiv.id = 'loadingMessage';
1059
-
1060
- const avatar = document.createElement('div');
1061
- avatar.className = 'message-avatar';
1062
- if (agentPfp) {
1063
- avatar.innerHTML = `<img src="${agentPfp}" alt="${agentName}" />`;
1064
- } else {
1065
- avatar.textContent = agentName.charAt(0).toUpperCase();
1066
- avatar.style.background = '#389e77';
1067
- avatar.style.color = '#fff';
1068
- avatar.style.fontSize = '0.75rem';
1069
- avatar.style.fontWeight = '700';
1070
- }
1071
-
1072
- const loadingContent = document.createElement('div');
1073
- loadingContent.className = 'message-content';
1074
- loadingContent.innerHTML = `
1075
- <div class="loading">
1076
- <div class="loading-dot"></div>
1077
- <div class="loading-dot"></div>
1078
- <div class="loading-dot"></div>
1079
- </div>
1080
- `;
1081
-
1082
- loadingDiv.appendChild(avatar);
1083
- loadingDiv.appendChild(loadingContent);
1084
-
1085
- chatContainer.insertBefore(loadingDiv, sleepIndicator);
1086
- chatContainer.scrollTop = chatContainer.scrollHeight;
1087
- }
1088
-
1089
- function hideLoading() {
1090
- const loadingDiv = document.getElementById('loadingMessage');
1091
- if (loadingDiv) loadingDiv.remove();
1092
- }
1093
-
1094
- async function sendMessage() {
1095
- const message = messageInput.value.trim();
1096
- if (!message || isLoading) return;
1097
-
1098
- isLoading = true;
1099
- sendButton.disabled = true;
1100
- messageInput.disabled = true;
1101
-
1102
- addMessage('user', message);
1103
- messageInput.value = '';
1104
-
1105
- showLoading();
1106
-
1107
- try {
1108
- const response = await fetch('/api/message', {
1109
- method: 'POST',
1110
- headers: { 'Content-Type': 'application/json' },
1111
- body: JSON.stringify({ message }),
1112
- });
1113
-
1114
- const data = await response.json();
1115
- hideLoading();
1116
-
1117
- if (data.response) {
1118
- addMessage('assistant', data.response);
1119
- } else if (data.error) {
1120
- addMessage('assistant', 'Something went wrong. Try again?');
1121
- }
1122
- // Advance poll timestamp so the poller doesn't re-show these messages
1123
- lastPollTimestamp = Date.now();
1124
- } catch (error) {
1125
- hideLoading();
1126
- addMessage('assistant', 'Couldn\'t connect to the server. Try again?');
1127
- } finally {
1128
- isLoading = false;
1129
- sendButton.disabled = false;
1130
- messageInput.disabled = false;
1131
- messageInput.focus();
1132
- }
1133
- }
1134
-
1135
- sendButton.addEventListener('click', sendMessage);
1136
- messageInput.addEventListener('keydown', (e) => {
1137
- if (e.key === 'Enter' && !e.shiftKey) {
1138
- e.preventDefault();
1139
- sendMessage();
1140
- }
1141
- });
1142
-
1143
- // Poll for new messages (heartbeat narration)
1144
- let lastPollTimestamp = Date.now();
1145
- let polling = false;
1146
-
1147
- async function pollNewMessages() {
1148
- if (polling || isLoading) return;
1149
- polling = true;
1150
- try {
1151
- const response = await fetch(`/api/messages?since=${lastPollTimestamp}`);
1152
- const data = await response.json();
1153
- if (data.messages && data.messages.length > 0) {
1154
- for (const msg of data.messages) {
1155
- // Only show messages we didn't create locally
1156
- addMessage(msg.role, msg.content);
1157
- if (msg.timestamp > lastPollTimestamp) {
1158
- lastPollTimestamp = msg.timestamp;
1159
- }
1160
- }
1161
- }
1162
- } catch (error) {
1163
- // Ignore poll errors
1164
- }
1165
- polling = false;
1166
- }
1167
-
1168
- // Poll every 3 seconds for heartbeat updates
1169
- setInterval(pollNewMessages, 3000);
1170
-
1171
- // --- Heartbeat config dropdown ---
1172
- const heartbeatBtn = document.getElementById('heartbeatBtn');
1173
- const heartbeatDropdown = document.getElementById('heartbeatDropdown');
1174
- let currentIntervalMs = 0;
1175
-
1176
- heartbeatBtn.addEventListener('click', (e) => {
1177
- e.stopPropagation();
1178
- heartbeatDropdown.classList.toggle('open');
1179
- });
1180
-
1181
- document.addEventListener('click', () => {
1182
- heartbeatDropdown.classList.remove('open');
1183
- });
1184
-
1185
- heartbeatDropdown.addEventListener('click', (e) => {
1186
- e.stopPropagation();
1187
- });
1188
-
1189
- function updateActiveOption() {
1190
- document.querySelectorAll('.heartbeat-option').forEach(opt => {
1191
- opt.classList.toggle('active', parseInt(opt.dataset.ms) === currentIntervalMs);
1192
- });
1193
- }
1194
-
1195
- document.querySelectorAll('.heartbeat-option').forEach(opt => {
1196
- opt.addEventListener('click', async () => {
1197
- const ms = parseInt(opt.dataset.ms);
1198
- try {
1199
- const res = await fetch('/api/config/heartbeat', {
1200
- method: 'POST',
1201
- headers: { 'Content-Type': 'application/json' },
1202
- body: JSON.stringify({ intervalMs: ms }),
1203
- });
1204
- const data = await res.json();
1205
- if (data.success) {
1206
- currentIntervalMs = ms;
1207
- updateActiveOption();
1208
- heartbeatDropdown.classList.remove('open');
1209
- }
1210
- } catch (err) {
1211
- console.error('Failed to update heartbeat:', err);
1212
- }
1213
- });
1214
- });
1215
-
1216
- // --- Sleep indicator with countdown ---
1217
- const sleepCountdown = document.getElementById('sleepCountdown');
1218
- const statusDot = document.querySelector('.status-dot');
1219
- const statusLabel = document.querySelector('.status-label');
1220
- let sleepTimer = null;
1221
-
1222
- function formatCountdown(ms) {
1223
- if (ms <= 0) return '';
1224
- const totalSec = Math.ceil(ms / 1000);
1225
- const h = Math.floor(totalSec / 3600);
1226
- const m = Math.floor((totalSec % 3600) / 60);
1227
- const s = totalSec % 60;
1228
- if (h > 0) return `${h}h ${m}m`;
1229
- if (m > 0) return `${m}m ${s}s`;
1230
- return `${s}s`;
1231
- }
1232
-
1233
- function startSleepCountdown(wakeAt) {
1234
- stopSleepCountdown();
1235
-
1236
- // Populate sleep avatar to match agent PFP
1237
- const sleepAvatarEl = document.getElementById('sleepAvatar');
1238
- if (sleepAvatarEl && !sleepAvatarEl.hasChildNodes()) {
1239
- if (agentPfp) {
1240
- sleepAvatarEl.innerHTML = `<img src="${agentPfp}" alt="${agentName}" />`;
1241
- } else {
1242
- sleepAvatarEl.textContent = agentName.charAt(0).toUpperCase();
1243
- sleepAvatarEl.style.background = '#389e77';
1244
- sleepAvatarEl.style.color = '#fff';
1245
- sleepAvatarEl.style.fontSize = '0.75rem';
1246
- sleepAvatarEl.style.fontWeight = '700';
1247
- }
1248
- }
1249
-
1250
- sleepIndicator.classList.add('visible');
1251
- chatContainer.scrollTop = chatContainer.scrollHeight;
1252
- statusDot.style.background = '#71767b';
1253
- statusDot.style.animation = 'none';
1254
- statusLabel.textContent = 'Sleeping';
1255
-
1256
- sleepTimer = setInterval(() => {
1257
- const remaining = wakeAt - Date.now();
1258
- if (remaining <= 0) {
1259
- stopSleepCountdown();
1260
- return;
1261
- }
1262
- sleepCountdown.textContent = formatCountdown(remaining);
1263
- }, 1000);
1264
- // Initial update
1265
- sleepCountdown.textContent = formatCountdown(wakeAt - Date.now());
1266
- }
1267
-
1268
- function stopSleepCountdown() {
1269
- if (sleepTimer) { clearInterval(sleepTimer); sleepTimer = null; }
1270
- sleepIndicator.classList.remove('visible');
1271
- statusDot.style.background = '#389e77';
1272
- statusDot.style.animation = 'pulse 2s infinite';
1273
- statusLabel.textContent = 'Online';
1274
- }
1275
-
1276
- // Poll sleep state every 2 seconds
1277
- let lastSleeping = false;
1278
- async function pollSleepState() {
1279
- try {
1280
- const res = await fetch('/api/sleep-state');
1281
- const state = await res.json();
1282
-
1283
- // Track current interval for dropdown
1284
- if (state.intervalMs && state.intervalMs !== currentIntervalMs) {
1285
- currentIntervalMs = state.intervalMs;
1286
- updateActiveOption();
1287
- }
1288
-
1289
- if (state.sleeping && state.wakeAt) {
1290
- if (!lastSleeping) {
1291
- startSleepCountdown(state.wakeAt);
1292
- }
1293
- lastSleeping = true;
1294
- } else {
1295
- if (lastSleeping) {
1296
- stopSleepCountdown();
1297
- }
1298
- lastSleeping = false;
1299
- }
1300
- } catch {
1301
- // Ignore poll errors
1302
- }
1303
- }
1304
-
1305
- setInterval(pollSleepState, 2000);
1306
- // Initial poll
1307
- pollSleepState();
1308
-
1309
- // --- Intelligence bar ---
1310
- const intelligenceFill = document.getElementById('intelligenceFill');
1311
-
1312
- function calculateIntelligence(stats) {
1313
- // Weighted raw score — learnings matter most, relationships next, interactions least
1314
- const raw = (stats.learnings * 3) + (stats.relationships * 2) + (stats.interactions * 0.2);
1315
- // Much slower curve: needs ~50 learnings + relationships to hit ~20%, hundreds to hit 50%
1316
- // A 1-hour bot with ~10 interactions should be around 3-8%
1317
- if (raw === 0) return 0;
1318
- const pct = Math.min(95, (Math.log(raw + 1) / Math.log(50000)) * 100);
1319
- return Math.round(pct * 10) / 10;
1320
- }
1321
-
1322
- async function pollMemoryStats() {
1323
- try {
1324
- const res = await fetch('/api/memory-stats');
1325
- const stats = await res.json();
1326
- const pct = calculateIntelligence(stats);
1327
- intelligenceFill.style.width = pct + '%';
1328
- } catch {
1329
- // Ignore poll errors
1330
- }
1331
- }
1332
-
1333
- // Poll every 10 seconds (memory doesn't change that fast)
1334
- setInterval(pollMemoryStats, 10000);
1335
- // Initial poll
1336
- pollMemoryStats();
1337
-
1338
- // Initialize
1339
- init();
1340
- messageInput.focus();
1341
- </script>
1342
- </body>
1343
- </html>