spora 0.1.18 → 0.2.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 (36) hide show
  1. package/dist/{chunk-ERTBXYOP.js → chunk-3FBYOHQR.js} +4 -4
  2. package/dist/{chunk-BUDIGP7I.js → chunk-3WPIVG27.js} +6 -6
  3. package/dist/{chunk-KVJ7QDP7.js → chunk-CMD2AGVW.js} +5 -5
  4. package/dist/{chunk-NFDZ47AG.js → chunk-MOCLA2KK.js} +4 -4
  5. package/dist/{chunk-7AHCICNB.js → chunk-V4BSNSFM.js} +2 -2
  6. package/dist/cli.js +45 -28
  7. package/dist/cli.js.map +1 -1
  8. package/dist/{client-UIVNUOML.js → client-FULMJH3S.js} +6 -6
  9. package/dist/{client-E2HXMCWG.js → client-RII4ST5D.js} +6 -6
  10. package/dist/{colony-JHNO77G3.js → colony-CBUE2M3P.js} +3 -3
  11. package/dist/{heartbeat-7NE3QFGK.js → heartbeat-KT5AFMGU.js} +11 -11
  12. package/dist/{init-SZYTEU67.js → init-NMWV64VQ.js} +13 -5
  13. package/dist/init-NMWV64VQ.js.map +1 -0
  14. package/dist/{llm-RDNC5Y3G.js → llm-OH2Z4PSN.js} +3 -3
  15. package/dist/mcp-server.js +22 -22
  16. package/dist/{prompt-builder-BWINRCXP.js → prompt-builder-TH576BVM.js} +4 -4
  17. package/dist/{queue-FMUMULCD.js → queue-5SDR3MF2.js} +3 -3
  18. package/dist/web-chat/chat.html +396 -0
  19. package/dist/web-chat-OQZCZSZO.js +162 -0
  20. package/dist/web-chat-OQZCZSZO.js.map +1 -0
  21. package/dist/{x-client-HM2LMXS3.js → x-client-T762KJFE.js} +3 -3
  22. package/package.json +1 -1
  23. package/dist/init-SZYTEU67.js.map +0 -1
  24. /package/dist/{chunk-ERTBXYOP.js.map → chunk-3FBYOHQR.js.map} +0 -0
  25. /package/dist/{chunk-BUDIGP7I.js.map → chunk-3WPIVG27.js.map} +0 -0
  26. /package/dist/{chunk-KVJ7QDP7.js.map → chunk-CMD2AGVW.js.map} +0 -0
  27. /package/dist/{chunk-NFDZ47AG.js.map → chunk-MOCLA2KK.js.map} +0 -0
  28. /package/dist/{chunk-7AHCICNB.js.map → chunk-V4BSNSFM.js.map} +0 -0
  29. /package/dist/{client-UIVNUOML.js.map → client-FULMJH3S.js.map} +0 -0
  30. /package/dist/{client-E2HXMCWG.js.map → client-RII4ST5D.js.map} +0 -0
  31. /package/dist/{colony-JHNO77G3.js.map → colony-CBUE2M3P.js.map} +0 -0
  32. /package/dist/{heartbeat-7NE3QFGK.js.map → heartbeat-KT5AFMGU.js.map} +0 -0
  33. /package/dist/{llm-RDNC5Y3G.js.map → llm-OH2Z4PSN.js.map} +0 -0
  34. /package/dist/{prompt-builder-BWINRCXP.js.map → prompt-builder-TH576BVM.js.map} +0 -0
  35. /package/dist/{queue-FMUMULCD.js.map → queue-5SDR3MF2.js.map} +0 -0
  36. /package/dist/{x-client-HM2LMXS3.js.map → x-client-T762KJFE.js.map} +0 -0
@@ -6,13 +6,13 @@ import {
6
6
  loadRelationships,
7
7
  updateRelationship
8
8
  } from "./chunk-EBO4F5NU.js";
9
+ import {
10
+ loadConfig
11
+ } from "./chunk-YEKHNTQO.js";
9
12
  import {
10
13
  logger,
11
14
  setLogLevel
12
15
  } from "./chunk-KELPENM3.js";
13
- import {
14
- loadConfig
15
- } from "./chunk-YEKHNTQO.js";
16
16
  import {
17
17
  FRAMEWORKS,
18
18
  GOAL_PRESETS,
@@ -388,7 +388,7 @@ Identity saved. Your Spore is alive. Use spora_get_identity to read the full doc
388
388
  },
389
389
  async ({ content }) => {
390
390
  try {
391
- const { getXClient } = await import("./x-client-HM2LMXS3.js");
391
+ const { getXClient } = await import("./x-client-T762KJFE.js");
392
392
  const client = await getXClient();
393
393
  const result = await client.postTweet(content);
394
394
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
@@ -406,7 +406,7 @@ Identity saved. Your Spore is alive. Use spora_get_identity to read the full doc
406
406
  },
407
407
  async ({ tweetId, content }) => {
408
408
  try {
409
- const { getXClient } = await import("./x-client-HM2LMXS3.js");
409
+ const { getXClient } = await import("./x-client-T762KJFE.js");
410
410
  const client = await getXClient();
411
411
  const result = await client.replyToTweet(tweetId, content);
412
412
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
@@ -421,7 +421,7 @@ Identity saved. Your Spore is alive. Use spora_get_identity to read the full doc
421
421
  { tweetId: z.string().describe("The ID of the tweet to like") },
422
422
  async ({ tweetId }) => {
423
423
  try {
424
- const { getXClient } = await import("./x-client-HM2LMXS3.js");
424
+ const { getXClient } = await import("./x-client-T762KJFE.js");
425
425
  const client = await getXClient();
426
426
  const result = await client.likeTweet(tweetId);
427
427
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
@@ -436,7 +436,7 @@ Identity saved. Your Spore is alive. Use spora_get_identity to read the full doc
436
436
  { tweetId: z.string().describe("The ID of the tweet to retweet") },
437
437
  async ({ tweetId }) => {
438
438
  try {
439
- const { getXClient } = await import("./x-client-HM2LMXS3.js");
439
+ const { getXClient } = await import("./x-client-T762KJFE.js");
440
440
  const client = await getXClient();
441
441
  const result = await client.retweet(tweetId);
442
442
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
@@ -451,7 +451,7 @@ Identity saved. Your Spore is alive. Use spora_get_identity to read the full doc
451
451
  { userId: z.string().describe("The user ID to follow") },
452
452
  async ({ userId }) => {
453
453
  try {
454
- const { getXClient } = await import("./x-client-HM2LMXS3.js");
454
+ const { getXClient } = await import("./x-client-T762KJFE.js");
455
455
  const client = await getXClient();
456
456
  const result = await client.followUser(userId);
457
457
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
@@ -466,7 +466,7 @@ Identity saved. Your Spore is alive. Use spora_get_identity to read the full doc
466
466
  { userId: z.string().describe("The user ID to unfollow") },
467
467
  async ({ userId }) => {
468
468
  try {
469
- const { getXClient } = await import("./x-client-HM2LMXS3.js");
469
+ const { getXClient } = await import("./x-client-T762KJFE.js");
470
470
  const client = await getXClient();
471
471
  const result = await client.unfollowUser(userId);
472
472
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
@@ -481,7 +481,7 @@ Identity saved. Your Spore is alive. Use spora_get_identity to read the full doc
481
481
  { count: z.number().optional().describe("Number of tweets to fetch (default 20)") },
482
482
  async ({ count }) => {
483
483
  try {
484
- const { getXClient } = await import("./x-client-HM2LMXS3.js");
484
+ const { getXClient } = await import("./x-client-T762KJFE.js");
485
485
  const client = await getXClient();
486
486
  const result = await client.getTimeline({ count: count ?? 20 });
487
487
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
@@ -496,7 +496,7 @@ Identity saved. Your Spore is alive. Use spora_get_identity to read the full doc
496
496
  { count: z.number().optional().describe("Number of mentions to fetch (default 20)") },
497
497
  async ({ count }) => {
498
498
  try {
499
- const { getXClient } = await import("./x-client-HM2LMXS3.js");
499
+ const { getXClient } = await import("./x-client-T762KJFE.js");
500
500
  const client = await getXClient();
501
501
  const result = await client.getMentions({ count: count ?? 20 });
502
502
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
@@ -514,7 +514,7 @@ Identity saved. Your Spore is alive. Use spora_get_identity to read the full doc
514
514
  },
515
515
  async ({ query, count }) => {
516
516
  try {
517
- const { getXClient } = await import("./x-client-HM2LMXS3.js");
517
+ const { getXClient } = await import("./x-client-T762KJFE.js");
518
518
  const client = await getXClient();
519
519
  const result = await client.searchTweets(query, { count: count ?? 20 });
520
520
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
@@ -529,7 +529,7 @@ Identity saved. Your Spore is alive. Use spora_get_identity to read the full doc
529
529
  { handle: z.string().describe("X handle (without @)") },
530
530
  async ({ handle }) => {
531
531
  try {
532
- const { getXClient } = await import("./x-client-HM2LMXS3.js");
532
+ const { getXClient } = await import("./x-client-T762KJFE.js");
533
533
  const client = await getXClient();
534
534
  const result = await client.getProfile(handle);
535
535
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
@@ -547,7 +547,7 @@ Identity saved. Your Spore is alive. Use spora_get_identity to read the full doc
547
547
  },
548
548
  async ({ content, scheduledFor }) => {
549
549
  try {
550
- const { addToQueue } = await import("./queue-FMUMULCD.js");
550
+ const { addToQueue } = await import("./queue-5SDR3MF2.js");
551
551
  const entry = addToQueue(content, scheduledFor);
552
552
  return {
553
553
  content: [
@@ -565,7 +565,7 @@ Identity saved. Your Spore is alive. Use spora_get_identity to read the full doc
565
565
  {},
566
566
  async () => {
567
567
  try {
568
- const { flushQueue } = await import("./queue-FMUMULCD.js");
568
+ const { flushQueue } = await import("./queue-5SDR3MF2.js");
569
569
  const results = await flushQueue();
570
570
  return {
571
571
  content: [
@@ -586,7 +586,7 @@ Identity saved. Your Spore is alive. Use spora_get_identity to read the full doc
586
586
  { message: z.string().optional().describe("Optional message to post to the Colony community") },
587
587
  async ({ message }) => {
588
588
  try {
589
- const { colonyCheckin } = await import("./colony-JHNO77G3.js");
589
+ const { colonyCheckin } = await import("./colony-CBUE2M3P.js");
590
590
  const result = await colonyCheckin(message);
591
591
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
592
592
  } catch (error) {
@@ -600,7 +600,7 @@ Identity saved. Your Spore is alive. Use spora_get_identity to read the full doc
600
600
  {},
601
601
  async () => {
602
602
  try {
603
- const { getColonyMemory } = await import("./colony-JHNO77G3.js");
603
+ const { getColonyMemory } = await import("./colony-CBUE2M3P.js");
604
604
  const memory = getColonyMemory();
605
605
  return { content: [{ type: "text", text: JSON.stringify(memory, null, 2) }] };
606
606
  } catch (error) {
@@ -614,7 +614,7 @@ Identity saved. Your Spore is alive. Use spora_get_identity to read the full doc
614
614
  {},
615
615
  async () => {
616
616
  try {
617
- const { getActivePlans } = await import("./colony-JHNO77G3.js");
617
+ const { getActivePlans } = await import("./colony-CBUE2M3P.js");
618
618
  const plans = getActivePlans();
619
619
  return {
620
620
  content: [
@@ -637,7 +637,7 @@ Identity saved. Your Spore is alive. Use spora_get_identity to read the full doc
637
637
  },
638
638
  async ({ description }) => {
639
639
  try {
640
- const { proposePlan } = await import("./colony-JHNO77G3.js");
640
+ const { proposePlan } = await import("./colony-CBUE2M3P.js");
641
641
  const result = await proposePlan(description);
642
642
  if (result.success) {
643
643
  return {
@@ -660,7 +660,7 @@ Identity saved. Your Spore is alive. Use spora_get_identity to read the full doc
660
660
  },
661
661
  async ({ planId }) => {
662
662
  try {
663
- const { joinPlan } = await import("./colony-JHNO77G3.js");
663
+ const { joinPlan } = await import("./colony-CBUE2M3P.js");
664
664
  const result = await joinPlan(planId);
665
665
  if (result.success) {
666
666
  return { content: [{ type: "text", text: "Joined the plan! Go execute it." }] };
@@ -679,7 +679,7 @@ Identity saved. Your Spore is alive. Use spora_get_identity to read the full doc
679
679
  },
680
680
  async ({ status }) => {
681
681
  try {
682
- const { postStatus } = await import("./colony-JHNO77G3.js");
682
+ const { postStatus } = await import("./colony-CBUE2M3P.js");
683
683
  const result = await postStatus(status);
684
684
  if (result.success) {
685
685
  return { content: [{ type: "text", text: "Status posted to the Colony." }] };
@@ -696,7 +696,7 @@ Identity saved. Your Spore is alive. Use spora_get_identity to read the full doc
696
696
  {},
697
697
  async () => {
698
698
  try {
699
- const { getTodaysActivity } = await import("./colony-JHNO77G3.js");
699
+ const { getTodaysActivity } = await import("./colony-CBUE2M3P.js");
700
700
  const activity = getTodaysActivity();
701
701
  return {
702
702
  content: [
@@ -2,11 +2,11 @@ import {
2
2
  buildChatPrompt,
3
3
  buildHeartbeatUserMessage,
4
4
  buildSystemPrompt
5
- } from "./chunk-7AHCICNB.js";
6
- import "./chunk-NFDZ47AG.js";
5
+ } from "./chunk-V4BSNSFM.js";
6
+ import "./chunk-MOCLA2KK.js";
7
7
  import "./chunk-EBO4F5NU.js";
8
- import "./chunk-KELPENM3.js";
9
8
  import "./chunk-YEKHNTQO.js";
9
+ import "./chunk-KELPENM3.js";
10
10
  import "./chunk-AIEXQCQS.js";
11
11
  import "./chunk-53YLFYJF.js";
12
12
  export {
@@ -14,4 +14,4 @@ export {
14
14
  buildHeartbeatUserMessage,
15
15
  buildSystemPrompt
16
16
  };
17
- //# sourceMappingURL=prompt-builder-BWINRCXP.js.map
17
+ //# sourceMappingURL=prompt-builder-TH576BVM.js.map
@@ -2,13 +2,13 @@ import {
2
2
  addToQueue,
3
3
  flushQueue,
4
4
  showQueue
5
- } from "./chunk-KVJ7QDP7.js";
6
- import "./chunk-KELPENM3.js";
5
+ } from "./chunk-CMD2AGVW.js";
7
6
  import "./chunk-YEKHNTQO.js";
7
+ import "./chunk-KELPENM3.js";
8
8
  import "./chunk-53YLFYJF.js";
9
9
  export {
10
10
  addToQueue,
11
11
  flushQueue,
12
12
  showQueue
13
13
  };
14
- //# sourceMappingURL=queue-FMUMULCD.js.map
14
+ //# sourceMappingURL=queue-5SDR3MF2.js.map
@@ -0,0 +1,396 @@
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 Chat</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
+ .header {
24
+ padding: 1rem 2rem;
25
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
26
+ display: flex;
27
+ align-items: center;
28
+ gap: 1rem;
29
+ }
30
+
31
+ .logo {
32
+ font-size: 1.5rem;
33
+ font-weight: bold;
34
+ background: linear-gradient(135deg, #f97316, #ec4899);
35
+ -webkit-background-clip: text;
36
+ -webkit-text-fill-color: transparent;
37
+ background-clip: text;
38
+ }
39
+
40
+ .status {
41
+ margin-left: auto;
42
+ display: flex;
43
+ align-items: center;
44
+ gap: 0.5rem;
45
+ font-size: 0.875rem;
46
+ color: rgba(255, 255, 255, 0.6);
47
+ }
48
+
49
+ .status-dot {
50
+ width: 8px;
51
+ height: 8px;
52
+ border-radius: 50%;
53
+ background: #10b981;
54
+ animation: pulse 2s infinite;
55
+ }
56
+
57
+ @keyframes pulse {
58
+ 0%, 100% { opacity: 1; }
59
+ 50% { opacity: 0.5; }
60
+ }
61
+
62
+ .chat-container {
63
+ flex: 1;
64
+ overflow-y: auto;
65
+ padding: 2rem;
66
+ display: flex;
67
+ flex-direction: column;
68
+ gap: 1rem;
69
+ }
70
+
71
+ .message {
72
+ display: flex;
73
+ gap: 0.75rem;
74
+ animation: fadeIn 0.3s ease-in;
75
+ }
76
+
77
+ @keyframes fadeIn {
78
+ from { opacity: 0; transform: translateY(10px); }
79
+ to { opacity: 1; transform: translateY(0); }
80
+ }
81
+
82
+ .message.user {
83
+ justify-content: flex-end;
84
+ }
85
+
86
+ .message.assistant {
87
+ justify-content: flex-start;
88
+ }
89
+
90
+ .message-avatar {
91
+ width: 40px;
92
+ height: 40px;
93
+ border-radius: 50%;
94
+ background: #000;
95
+ display: flex;
96
+ align-items: center;
97
+ justify-content: center;
98
+ flex-shrink: 0;
99
+ overflow: hidden;
100
+ }
101
+
102
+ .message-avatar img {
103
+ width: 100%;
104
+ height: 100%;
105
+ object-fit: cover;
106
+ }
107
+
108
+ .message.user .message-avatar {
109
+ order: 2;
110
+ }
111
+
112
+ .message-content {
113
+ max-width: 70%;
114
+ padding: 0.75rem 1.25rem;
115
+ border-radius: 1rem;
116
+ font-size: 0.875rem;
117
+ line-height: 1.5;
118
+ white-space: pre-wrap;
119
+ }
120
+
121
+ .message.user .message-content {
122
+ background: linear-gradient(135deg, #f97316, #ec4899);
123
+ color: #fff;
124
+ border-bottom-right-radius: 0.25rem;
125
+ }
126
+
127
+ .message.assistant .message-content {
128
+ background: #1a1a1a;
129
+ border: 1px solid rgba(255, 255, 255, 0.1);
130
+ border-bottom-left-radius: 0.25rem;
131
+ }
132
+
133
+ .input-container {
134
+ padding: 1.5rem 2rem;
135
+ border-top: 1px solid rgba(255, 255, 255, 0.1);
136
+ display: flex;
137
+ gap: 0.75rem;
138
+ }
139
+
140
+ .input-box {
141
+ flex: 1;
142
+ background: #1a1a1a;
143
+ border: 1px solid rgba(255, 255, 255, 0.1);
144
+ border-radius: 0.75rem;
145
+ padding: 0.75rem 1rem;
146
+ color: #fff;
147
+ font-size: 0.875rem;
148
+ font-family: inherit;
149
+ outline: none;
150
+ transition: border-color 0.2s;
151
+ }
152
+
153
+ .input-box:focus {
154
+ border-color: #f97316;
155
+ }
156
+
157
+ .send-button {
158
+ background: linear-gradient(135deg, #f97316, #ec4899);
159
+ color: #fff;
160
+ border: none;
161
+ padding: 0.75rem 1.5rem;
162
+ border-radius: 0.75rem;
163
+ font-weight: 600;
164
+ font-size: 0.875rem;
165
+ cursor: pointer;
166
+ transition: transform 0.2s, box-shadow 0.2s;
167
+ }
168
+
169
+ .send-button:hover:not(:disabled) {
170
+ transform: scale(1.05);
171
+ box-shadow: 0 10px 25px rgba(249, 115, 22, 0.3);
172
+ }
173
+
174
+ .send-button:disabled {
175
+ opacity: 0.5;
176
+ cursor: not-allowed;
177
+ }
178
+
179
+ .loading {
180
+ display: flex;
181
+ gap: 0.25rem;
182
+ padding: 0.75rem 1.25rem;
183
+ }
184
+
185
+ .loading-dot {
186
+ width: 8px;
187
+ height: 8px;
188
+ border-radius: 50%;
189
+ background: rgba(255, 255, 255, 0.4);
190
+ animation: bounce 1.4s infinite ease-in-out both;
191
+ }
192
+
193
+ .loading-dot:nth-child(1) { animation-delay: -0.32s; }
194
+ .loading-dot:nth-child(2) { animation-delay: -0.16s; }
195
+
196
+ @keyframes bounce {
197
+ 0%, 80%, 100% { transform: scale(0); }
198
+ 40% { transform: scale(1); }
199
+ }
200
+
201
+ .empty-state {
202
+ flex: 1;
203
+ display: flex;
204
+ flex-direction: column;
205
+ align-items: center;
206
+ justify-content: center;
207
+ text-align: center;
208
+ gap: 1rem;
209
+ color: rgba(255, 255, 255, 0.4);
210
+ }
211
+
212
+ .empty-state-icon {
213
+ font-size: 4rem;
214
+ }
215
+
216
+ .empty-state-text {
217
+ font-size: 1.125rem;
218
+ font-weight: 600;
219
+ }
220
+
221
+ .empty-state-subtext {
222
+ font-size: 0.875rem;
223
+ }
224
+ </style>
225
+ </head>
226
+ <body>
227
+ <div class="header">
228
+ <div class="logo">SPORA</div>
229
+ <div class="status">
230
+ <span class="status-dot"></span>
231
+ <span>Connected</span>
232
+ </div>
233
+ </div>
234
+
235
+ <div class="chat-container" id="chatContainer">
236
+ <div class="empty-state">
237
+ <div class="empty-state-icon">💬</div>
238
+ <div class="empty-state-text">Chat with your Spore</div>
239
+ <div class="empty-state-subtext">Start a conversation below</div>
240
+ </div>
241
+ </div>
242
+
243
+ <div class="input-container">
244
+ <input
245
+ type="text"
246
+ class="input-box"
247
+ id="messageInput"
248
+ placeholder="Type your message..."
249
+ autocomplete="off"
250
+ />
251
+ <button class="send-button" id="sendButton">Send</button>
252
+ </div>
253
+
254
+ <script>
255
+ const chatContainer = document.getElementById('chatContainer');
256
+ const messageInput = document.getElementById('messageInput');
257
+ const sendButton = document.getElementById('sendButton');
258
+
259
+ let isLoading = false;
260
+
261
+ // Load existing messages on startup
262
+ async function loadMessages() {
263
+ try {
264
+ const response = await fetch('/api/messages');
265
+ const data = await response.json();
266
+ if (data.messages && data.messages.length > 0) {
267
+ chatContainer.innerHTML = '';
268
+ data.messages.forEach(msg => {
269
+ addMessage(msg.role, msg.content, false);
270
+ });
271
+ }
272
+ } catch (error) {
273
+ console.error('Failed to load messages:', error);
274
+ }
275
+ }
276
+
277
+ function addMessage(role, content, animate = true) {
278
+ // Remove empty state if it exists
279
+ const emptyState = chatContainer.querySelector('.empty-state');
280
+ if (emptyState) {
281
+ emptyState.remove();
282
+ }
283
+
284
+ const messageDiv = document.createElement('div');
285
+ messageDiv.className = `message ${role}`;
286
+
287
+ const avatar = document.createElement('div');
288
+ avatar.className = 'message-avatar';
289
+ if (role === 'assistant') {
290
+ avatar.innerHTML = '<img src="/spore.png" alt="Spore" onerror="this.style.display=\'none\'" />';
291
+ } else {
292
+ avatar.textContent = '👤';
293
+ }
294
+
295
+ const contentDiv = document.createElement('div');
296
+ contentDiv.className = 'message-content';
297
+ contentDiv.textContent = content;
298
+
299
+ messageDiv.appendChild(avatar);
300
+ messageDiv.appendChild(contentDiv);
301
+
302
+ chatContainer.appendChild(messageDiv);
303
+ chatContainer.scrollTop = chatContainer.scrollHeight;
304
+ }
305
+
306
+ function showLoading() {
307
+ const loadingDiv = document.createElement('div');
308
+ loadingDiv.className = 'message assistant';
309
+ loadingDiv.id = 'loadingMessage';
310
+
311
+ const avatar = document.createElement('div');
312
+ avatar.className = 'message-avatar';
313
+ avatar.innerHTML = '<img src="/spore.png" alt="Spore" onerror="this.style.display=\'none\'" />';
314
+
315
+ const loadingContent = document.createElement('div');
316
+ loadingContent.className = 'message-content';
317
+ loadingContent.innerHTML = `
318
+ <div class="loading">
319
+ <div class="loading-dot"></div>
320
+ <div class="loading-dot"></div>
321
+ <div class="loading-dot"></div>
322
+ </div>
323
+ `;
324
+
325
+ loadingDiv.appendChild(avatar);
326
+ loadingDiv.appendChild(loadingContent);
327
+
328
+ chatContainer.appendChild(loadingDiv);
329
+ chatContainer.scrollTop = chatContainer.scrollHeight;
330
+ }
331
+
332
+ function hideLoading() {
333
+ const loadingDiv = document.getElementById('loadingMessage');
334
+ if (loadingDiv) {
335
+ loadingDiv.remove();
336
+ }
337
+ }
338
+
339
+ async function sendMessage() {
340
+ const message = messageInput.value.trim();
341
+ if (!message || isLoading) return;
342
+
343
+ isLoading = true;
344
+ sendButton.disabled = true;
345
+ messageInput.disabled = true;
346
+
347
+ // Add user message
348
+ addMessage('user', message);
349
+ messageInput.value = '';
350
+
351
+ // Show loading
352
+ showLoading();
353
+
354
+ try {
355
+ const response = await fetch('/api/message', {
356
+ method: 'POST',
357
+ headers: { 'Content-Type': 'application/json' },
358
+ body: JSON.stringify({ message }),
359
+ });
360
+
361
+ const data = await response.json();
362
+
363
+ hideLoading();
364
+
365
+ if (data.response) {
366
+ addMessage('assistant', data.response);
367
+ } else if (data.error) {
368
+ addMessage('assistant', 'Sorry, I encountered an error. Please try again.');
369
+ }
370
+ } catch (error) {
371
+ hideLoading();
372
+ addMessage('assistant', 'Sorry, I couldn\'t connect to the server. Please try again.');
373
+ } finally {
374
+ isLoading = false;
375
+ sendButton.disabled = false;
376
+ messageInput.disabled = false;
377
+ messageInput.focus();
378
+ }
379
+ }
380
+
381
+ sendButton.addEventListener('click', sendMessage);
382
+ messageInput.addEventListener('keydown', (e) => {
383
+ if (e.key === 'Enter' && !e.shiftKey) {
384
+ e.preventDefault();
385
+ sendMessage();
386
+ }
387
+ });
388
+
389
+ // Load messages on startup
390
+ loadMessages();
391
+
392
+ // Focus input
393
+ messageInput.focus();
394
+ </script>
395
+ </body>
396
+ </html>