Twan07 commited on
Commit
6eda9c9
·
verified ·
1 Parent(s): 9ff1fd4

Upload 3 files

Browse files
exocore-web/public/templates/chatheadai.html ADDED
@@ -0,0 +1,774 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <style>
2
+ #gemini-fab {
3
+ position: fixed;
4
+ bottom: 25px;
5
+ right: 25px;
6
+ width: 60px;
7
+ height: 60px;
8
+ background-color: #4285f4;
9
+ border-radius: 50%;
10
+ box-shadow: 0 6px 20px rgba(0, 0, 0, 0.25);
11
+ display: flex;
12
+ justify-content: center;
13
+ align-items: center;
14
+ cursor: pointer;
15
+ z-index: 1000;
16
+ transition:
17
+ transform 0.2s ease-in-out,
18
+ box-shadow 0.2s ease-in-out,
19
+ width 0.2s ease,
20
+ height 0.2s ease;
21
+ }
22
+
23
+ #gemini-fab:hover {
24
+ transform: scale(1.1);
25
+ box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3);
26
+ }
27
+
28
+ #gemini-fab img {
29
+ width: 38px;
30
+ height: 38px;
31
+ transition:
32
+ width 0.2s ease,
33
+ height 0.2s ease;
34
+ }
35
+
36
+ #gemini-chat-container {
37
+ position: fixed;
38
+ background-color: #ffffff;
39
+ border: 1px solid #dadce0;
40
+ border-radius: 12px;
41
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15);
42
+ display: none;
43
+ flex-direction: column;
44
+ overflow: hidden;
45
+ z-index: 999;
46
+ font-family: 'Roboto', 'Segoe UI', Arial, sans-serif;
47
+ transition:
48
+ width 0.3s ease,
49
+ height 0.3s ease,
50
+ bottom 0.3s ease,
51
+ right 0.3s ease,
52
+ left 0.3s ease;
53
+ width: 400px;
54
+ max-height: 600px;
55
+ bottom: 95px;
56
+ right: 25px;
57
+ }
58
+
59
+ #gemini-chat-container.visible {
60
+ display: flex;
61
+ animation: slideUpFadeIn 0.3s ease-out;
62
+ }
63
+
64
+ @keyframes slideUpFadeIn {
65
+ from {
66
+ opacity: 0;
67
+ transform: translateY(20px);
68
+ }
69
+ to {
70
+ opacity: 1;
71
+ transform: translateY(0);
72
+ }
73
+ }
74
+
75
+ .gemini-chat-header {
76
+ background-color: #f1f3f4;
77
+ padding: 12px 18px;
78
+ display: flex;
79
+ justify-content: space-between;
80
+ align-items: center;
81
+ border-bottom: 1px solid #e0e0e0;
82
+ flex-shrink: 0;
83
+ }
84
+ .gemini-chat-header h3 {
85
+ margin: 0;
86
+ font-size: 1.05rem;
87
+ color: #202124;
88
+ font-weight: 500;
89
+ }
90
+ .gemini-chat-header .close-chat-btn {
91
+ background: none;
92
+ border: none;
93
+ font-size: 1.5rem;
94
+ font-weight: 300;
95
+ line-height: 1;
96
+ cursor: pointer;
97
+ color: #5f6368;
98
+ padding: 0;
99
+ }
100
+ .gemini-chat-header .close-chat-btn:hover {
101
+ color: #202124;
102
+ }
103
+
104
+ #gemini-chat-log {
105
+ flex-grow: 1;
106
+ padding: 15px 18px;
107
+ overflow-y: auto;
108
+ display: flex;
109
+ flex-direction: column;
110
+ gap: 12px;
111
+ background-color: #f8f9fa;
112
+ }
113
+
114
+ .chat-message {
115
+ padding: 10px 15px;
116
+ padding-bottom: 35px; /* Space for the bottom copy button */
117
+ border-radius: 18px;
118
+ max-width: 85%;
119
+ line-height: 1.45;
120
+ font-size: 0.92rem;
121
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
122
+ position: relative;
123
+ }
124
+ .chat-message span {
125
+ white-space: pre-wrap;
126
+ word-wrap: break-word;
127
+ }
128
+
129
+ .chat-message pre {
130
+ background: #1e1e1e;
131
+ padding: 0.8em 1em;
132
+ overflow: auto;
133
+ border-radius: 5px;
134
+ color: #ccc;
135
+ margin: 0.5em 0;
136
+ font-size: 0.85em;
137
+ line-height: 1.5;
138
+ position: relative;
139
+ white-space: pre;
140
+ word-wrap: normal;
141
+ }
142
+ .chat-message pre code,
143
+ .chat-message pre code.hljs {
144
+ font-family: 'monospace';
145
+ display: block;
146
+ padding: 0;
147
+ }
148
+
149
+ .chat-message.user {
150
+ background-color: #d1eaff;
151
+ color: #004085;
152
+ align-self: flex-end;
153
+ border-bottom-right-radius: 6px;
154
+ }
155
+ .chat-message.ai {
156
+ background-color: #e9ecef;
157
+ color: #383d41;
158
+ align-self: flex-start;
159
+ border-bottom-left-radius: 6px;
160
+ }
161
+
162
+ .copy-code-btn {
163
+ position: absolute;
164
+ top: 8px;
165
+ right: 8px;
166
+ background-color: rgba(80, 80, 80, 0.7);
167
+ color: white;
168
+ border: none;
169
+ padding: 4px 8px;
170
+ font-size: 0.75em;
171
+ border-radius: 4px;
172
+ cursor: pointer;
173
+ font-family: sans-serif;
174
+ z-index: 1;
175
+ opacity: 0.7;
176
+ visibility: visible;
177
+ transition:
178
+ opacity 0.2s,
179
+ background-color 0.2s;
180
+ }
181
+
182
+ .copy-msg-btn {
183
+ position: absolute;
184
+ bottom: 6px;
185
+ right: 8px;
186
+ background-color: rgba(100, 100, 100, 0.6); /* Slightly different base */
187
+ color: white;
188
+ border: none;
189
+ border-radius: 50%;
190
+ cursor: pointer;
191
+ font-family: sans-serif;
192
+ z-index: 1;
193
+ opacity: 0.6;
194
+ visibility: visible;
195
+ transition:
196
+ opacity 0.2s,
197
+ background-color 0.2s;
198
+ display: inline-flex;
199
+ align-items: center;
200
+ justify-content: center;
201
+ width: 26px;
202
+ height: 26px;
203
+ padding: 0;
204
+ }
205
+ .copy-msg-btn svg {
206
+ width: 14px;
207
+ height: 14px;
208
+ fill: currentColor;
209
+ }
210
+
211
+ .copy-code-btn:hover {
212
+ background-color: rgba(100, 100, 100, 0.9);
213
+ opacity: 1;
214
+ }
215
+ .copy-msg-btn:hover {
216
+ background-color: rgba(80, 80, 80, 0.9); /* Darker on hover */
217
+ opacity: 1;
218
+ }
219
+
220
+ .copy-code-btn.copied,
221
+ .copy-msg-btn.copied {
222
+ background-color: #28a745;
223
+ opacity: 1;
224
+ }
225
+
226
+ #gemini-typing-indicator {
227
+ display: flex;
228
+ align-items: center;
229
+ padding: 8px 18px 4px 18px;
230
+ background-color: #f8f9fa;
231
+ height: 20px;
232
+ flex-shrink: 0;
233
+ }
234
+ #gemini-typing-indicator span {
235
+ height: 8px;
236
+ width: 8px;
237
+ background-color: #909090;
238
+ border-radius: 50%;
239
+ display: inline-block;
240
+ margin: 0 2px;
241
+ animation: geminiDotsBounce 1.3s infinite ease-in-out;
242
+ }
243
+ #gemini-typing-indicator span:nth-child(2) {
244
+ animation-delay: -1.1s;
245
+ }
246
+ #gemini-typing-indicator span:nth-child(3) {
247
+ animation-delay: -0.9s;
248
+ }
249
+ @keyframes geminiDotsBounce {
250
+ 0%,
251
+ 60%,
252
+ 100% {
253
+ transform: scale(0.4);
254
+ }
255
+ 30% {
256
+ transform: scale(1);
257
+ }
258
+ }
259
+
260
+ #gemini-chat-input-form {
261
+ display: flex;
262
+ padding: 12px 15px;
263
+ border-top: 1px solid #e0e0e0;
264
+ background-color: #ffffff;
265
+ align-items: center;
266
+ flex-shrink: 0;
267
+ }
268
+ #gemini-chat-input-form input[type='text'] {
269
+ flex-grow: 1;
270
+ padding: 12px 18px;
271
+ border: 1px solid #dfe1e5;
272
+ border-radius: 24px;
273
+ margin-right: 10px;
274
+ font-size: 0.95rem;
275
+ outline: none;
276
+ transition:
277
+ border-color 0.2s,
278
+ box-shadow 0.2s;
279
+ }
280
+ #gemini-chat-input-form input[type='text']:focus {
281
+ border-color: #4285f4;
282
+ box-shadow: 0 0 0 2px rgba(66, 133, 244, 0.2);
283
+ }
284
+ #gemini-chat-input-form input[type='text']:disabled {
285
+ background-color: #e9ecef;
286
+ cursor: not-allowed;
287
+ }
288
+ #gemini-chat-input-form button {
289
+ background-color: #4285f4;
290
+ color: white;
291
+ border: none;
292
+ border-radius: 50%;
293
+ width: 44px;
294
+ height: 44px;
295
+ padding: 0;
296
+ cursor: pointer;
297
+ font-size: 1.3rem;
298
+ display: flex;
299
+ align-items: center;
300
+ justify-content: center;
301
+ transition:
302
+ background-color 0.2s ease,
303
+ width 0.2s ease,
304
+ height 0.2s ease;
305
+ flex-shrink: 0;
306
+ }
307
+ #gemini-chat-input-form button:hover {
308
+ background-color: #3367d6;
309
+ }
310
+ #gemini-chat-input-form button:disabled {
311
+ background-color: #a1c6ff;
312
+ cursor: not-allowed;
313
+ }
314
+ #gemini-chat-input-form button svg {
315
+ width: 22px;
316
+ height: 22px;
317
+ fill: white;
318
+ transition:
319
+ width 0.2s ease,
320
+ height 0.2s ease;
321
+ }
322
+
323
+ @media (max-width: 991.98px) {
324
+ #gemini-chat-container {
325
+ width: 380px;
326
+ max-height: 550px;
327
+ }
328
+ }
329
+ @media (max-width: 767.98px) {
330
+ #gemini-fab {
331
+ width: 55px;
332
+ height: 55px;
333
+ bottom: 20px;
334
+ right: 20px;
335
+ }
336
+ #gemini-fab img {
337
+ width: 32px;
338
+ height: 32px;
339
+ }
340
+ #gemini-chat-container {
341
+ width: 360px;
342
+ max-height: 75vh;
343
+ bottom: 85px;
344
+ right: 20px;
345
+ }
346
+ }
347
+ @media (max-width: 575.98px) {
348
+ #gemini-fab {
349
+ width: 50px;
350
+ height: 50px;
351
+ bottom: 15px;
352
+ right: 15px;
353
+ }
354
+ #gemini-fab img {
355
+ width: 28px;
356
+ height: 28px;
357
+ }
358
+ #gemini-chat-container {
359
+ left: 10px;
360
+ right: 10px;
361
+ width: auto;
362
+ bottom: 75px;
363
+ max-height: calc(100vh - 95px);
364
+ }
365
+ .chat-message pre {
366
+ font-size: 0.8em;
367
+ padding: 0.6em 0.8em;
368
+ }
369
+ .copy-code-btn {
370
+ font-size: 0.7em;
371
+ padding: 3px 6px;
372
+ top: 5px;
373
+ right: 5px;
374
+ }
375
+ .copy-msg-btn {
376
+ width: 24px;
377
+ height: 24px;
378
+ bottom: 5px;
379
+ right: 5px;
380
+ }
381
+ .copy-msg-btn svg {
382
+ width: 12px;
383
+ height: 12px;
384
+ }
385
+ .gemini-chat-header {
386
+ padding: 10px 12px;
387
+ }
388
+ .gemini-chat-header h3 {
389
+ font-size: 0.98rem;
390
+ }
391
+ .gemini-chat-header .close-chat-btn {
392
+ font-size: 1.3rem;
393
+ }
394
+ #gemini-chat-log {
395
+ padding: 10px 12px;
396
+ gap: 8px;
397
+ }
398
+ .chat-message {
399
+ font-size: 0.88rem;
400
+ padding: 8px 12px;
401
+ padding-bottom: 30px;
402
+ max-width: calc(100% - 10px);
403
+ border-radius: 15px;
404
+ }
405
+ .chat-message.user {
406
+ border-bottom-right-radius: 4px;
407
+ }
408
+ .chat-message.ai {
409
+ border-bottom-left-radius: 4px;
410
+ }
411
+ #gemini-chat-input-form {
412
+ padding: 8px 10px;
413
+ }
414
+ #gemini-chat-input-form input[type='text'] {
415
+ padding: 10px 15px;
416
+ font-size: 0.9rem;
417
+ margin-right: 8px;
418
+ }
419
+ #gemini-chat-input-form button {
420
+ width: 40px;
421
+ height: 40px;
422
+ }
423
+ #gemini-chat-input-form button svg {
424
+ width: 18px;
425
+ height: 18px;
426
+ }
427
+ #gemini-typing-indicator {
428
+ padding: 6px 12px 2px 12px;
429
+ }
430
+ }
431
+ </style>
432
+
433
+ <div id="gemini-fab" title="Chat with Gemini">
434
+ <img
435
+ src="https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTYnHRL-tMaj6h2CYK5Yy4ixuXfuohG8g8J4g&s"
436
+ alt="Gemini AI Logo"
437
+ />
438
+ </div>
439
+
440
+ <div id="gemini-chat-container">
441
+ <div class="gemini-chat-header">
442
+ <h3>Gemini Assistant</h3>
443
+ <button class="close-chat-btn" aria-label="Close chat">&times;</button>
444
+ </div>
445
+ <div id="gemini-chat-log">
446
+ <div class="chat-message ai">Hello! How can I assist you today?</div>
447
+ </div>
448
+ <div id="gemini-typing-indicator" class="gemini-typing-indicator" style="display: none">
449
+ <span></span><span></span><span></span>
450
+ </div>
451
+ <form id="gemini-chat-input-form">
452
+ <input type="text" id="gemini-user-input" placeholder="Type a message..." autocomplete="off" />
453
+ <button type="submit" title="Send message">
454
+ <svg viewBox="0 0 24 24">
455
+ <path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"></path>
456
+ </svg>
457
+ </button>
458
+ </form>
459
+ </div>
460
+
461
+ <script>
462
+ (function () {
463
+ const fab = document.getElementById('gemini-fab');
464
+ const chatContainer = document.getElementById('gemini-chat-container');
465
+ const chatLog = document.getElementById('gemini-chat-log');
466
+ const inputForm = document.getElementById('gemini-chat-input-form');
467
+ const userInput = document.getElementById('gemini-user-input');
468
+ const sendButton = inputForm.querySelector('button[type="submit"]');
469
+ const closeChatBtn = chatContainer.querySelector('.close-chat-btn');
470
+ const typingIndicator = document.getElementById('gemini-typing-indicator');
471
+ const TYPEWRITER_SPEED = 0.3;
472
+ let currentConversationId = null;
473
+
474
+ const SVG_COPY_ICON = `
475
+ <svg xmlns="http://www.w3.org/2000/svg" height="1em" viewBox="0 0 24 24" width="1em" fill="currentColor">
476
+ <path d="M0 0h24v24H0z" fill="none"/>
477
+ <path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/>
478
+ </svg>`;
479
+ const SVG_CHECK_ICON = `
480
+ <svg xmlns="http://www.w3.org/2000/svg" height="1em" viewBox="0 0 24 24" width="1em" fill="currentColor">
481
+ <path d="M0 0h24v24H0z" fill="none"/>
482
+ <path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/>
483
+ </svg>`;
484
+
485
+ fab.addEventListener('click', () => {
486
+ chatContainer.classList.toggle('visible');
487
+ if (chatContainer.classList.contains('visible')) {
488
+ userInput.focus();
489
+ }
490
+ });
491
+
492
+ closeChatBtn.addEventListener('click', () => {
493
+ chatContainer.classList.remove('visible');
494
+ });
495
+
496
+ inputForm.addEventListener('submit', async function (event) {
497
+ event.preventDefault();
498
+ const messageText = userInput.value.trim();
499
+ if (messageText === '' || userInput.disabled) return;
500
+
501
+ userInput.disabled = true;
502
+ if (sendButton) sendButton.disabled = true;
503
+
504
+ await addMessageToLog(messageText, 'user');
505
+ userInput.value = '';
506
+ showTypingIndicator(true);
507
+ try {
508
+ let apiUrl;
509
+ const isFirstMessageInSession = currentConversationId === null;
510
+
511
+ if (isFirstMessageInSession) {
512
+ apiUrl = `https://api-improve-production.up.railway.app/gemini/chat?q=${encodeURIComponent(messageText)}`;
513
+ } else {
514
+ apiUrl = `https://api-improve-production.up.railway.app/gemini/chat?q=${encodeURIComponent(messageText)}&id=${currentConversationId}`;
515
+ }
516
+
517
+ const response = await fetch(apiUrl);
518
+ const data = await response.json();
519
+
520
+ if (!response.ok) {
521
+ const errorMessage = data?.error?.message || data?.message || `Error: ${response.status}`;
522
+ await addMessageToLog(`Sorry, I couldn't get a response. ${errorMessage}`, 'ai');
523
+ return;
524
+ }
525
+
526
+ const reply = data.reply;
527
+
528
+ if (isFirstMessageInSession && data.id) {
529
+ currentConversationId = data.id;
530
+ } else if (!isFirstMessageInSession && data.id && data.id !== currentConversationId) {
531
+ currentConversationId = data.id;
532
+ }
533
+
534
+ if (reply !== undefined && reply !== null) {
535
+ await addMessageToLog(reply, 'ai');
536
+ } else {
537
+ await addMessageToLog('Sorry, I received an empty or malformed reply.', 'ai');
538
+ }
539
+ } catch (error) {
540
+ console.error('API/Network Error:', error);
541
+ await addMessageToLog(
542
+ 'Oops! Something went wrong. Please check connection and try again.',
543
+ 'ai'
544
+ );
545
+ } finally {
546
+ showTypingIndicator(false);
547
+ userInput.disabled = false;
548
+ if (sendButton) sendButton.disabled = false;
549
+ userInput.focus();
550
+ }
551
+ });
552
+
553
+ function showTypingIndicator(show) {
554
+ typingIndicator.style.display = show ? 'flex' : 'none';
555
+ }
556
+
557
+ function escapeHTML(str) {
558
+ const p = document.createElement('p');
559
+ p.textContent = str;
560
+ return p.innerHTML;
561
+ }
562
+
563
+ function isScrolledToBottom(element) {
564
+ const threshold = 10;
565
+ return element.scrollHeight - element.scrollTop - element.clientHeight < threshold;
566
+ }
567
+
568
+ async function typeSegment(
569
+ targetElement,
570
+ textToType,
571
+ isCode,
572
+ codeElementForHighlight,
573
+ postTypeCallback
574
+ ) {
575
+ return new Promise((resolve) => {
576
+ let i = 0;
577
+ targetElement.textContent = '';
578
+ function typeChar() {
579
+ if (i < textToType.length) {
580
+ targetElement.textContent += textToType.charAt(i);
581
+ i++;
582
+ if (isScrolledToBottom(chatLog)) {
583
+ chatLog.scrollTop = chatLog.scrollHeight;
584
+ }
585
+ setTimeout(typeChar, TYPEWRITER_SPEED);
586
+ } else {
587
+ if (isCode && codeElementForHighlight && typeof hljs !== 'undefined') {
588
+ try {
589
+ hljs.highlightElement(codeElementForHighlight);
590
+ } catch (e) {
591
+ console.error('Highlight.js error:', e);
592
+ }
593
+ }
594
+ if (postTypeCallback) postTypeCallback();
595
+ resolve();
596
+ }
597
+ }
598
+ if (textToType.length === 0) {
599
+ if (postTypeCallback) postTypeCallback();
600
+ resolve();
601
+ return;
602
+ }
603
+ typeChar();
604
+ });
605
+ }
606
+
607
+ async function addMessageToLog(text, sender) {
608
+ const messageDiv = document.createElement('div');
609
+ messageDiv.classList.add('chat-message', sender);
610
+ if (sender === 'ai') {
611
+ messageDiv.style.opacity = 0;
612
+ }
613
+
614
+ const segmentsToProcess = [];
615
+ const codeBlockRegex = /```(\w*)\n([\s\S]*?)```/g;
616
+ let lastIndex = 0;
617
+ let match;
618
+
619
+ while ((match = codeBlockRegex.exec(text)) !== null) {
620
+ const textBefore = text.substring(lastIndex, match.index);
621
+ if (textBefore.trim()) {
622
+ segmentsToProcess.push({ type: 'text', content: textBefore });
623
+ }
624
+ const language = match[1].trim().toLowerCase();
625
+ const codeContent = match[2];
626
+ segmentsToProcess.push({ type: 'code', content: codeContent, lang: language });
627
+ lastIndex = codeBlockRegex.lastIndex;
628
+ }
629
+ const textAfter = text.substring(lastIndex);
630
+ if (textAfter.trim()) {
631
+ segmentsToProcess.push({ type: 'text', content: textAfter });
632
+ }
633
+
634
+ if (segmentsToProcess.length === 0 && text.trim()) {
635
+ segmentsToProcess.push({ type: 'text', content: text });
636
+ }
637
+
638
+ chatLog.appendChild(messageDiv);
639
+
640
+ if (sender === 'ai' && segmentsToProcess.length > 0) {
641
+ messageDiv.style.opacity = 1;
642
+ for (const segment of segmentsToProcess) {
643
+ const contentToProcess = segment.content.trim();
644
+ if (segment.type === 'text') {
645
+ const span = document.createElement('span');
646
+ messageDiv.appendChild(span);
647
+ await typeSegment(span, contentToProcess, false, null, () => {
648
+ span.innerHTML = escapeHTML(span.textContent);
649
+ });
650
+ } else if (segment.type === 'code') {
651
+ const preElement = document.createElement('pre');
652
+ const codeElement = document.createElement('code');
653
+ if (segment.lang) {
654
+ codeElement.className = `language-${segment.lang}`;
655
+ }
656
+ preElement.appendChild(codeElement);
657
+ const copyCodeBtn = document.createElement('button');
658
+ copyCodeBtn.className = 'copy-code-btn';
659
+ copyCodeBtn.textContent = 'Copy';
660
+ preElement.appendChild(copyCodeBtn);
661
+ copyCodeBtn.addEventListener('click', () => {
662
+ navigator.clipboard
663
+ .writeText(codeElement.textContent)
664
+ .then(() => {
665
+ copyCodeBtn.textContent = 'Copied!';
666
+ copyCodeBtn.classList.add('copied');
667
+ setTimeout(() => {
668
+ copyCodeBtn.textContent = 'Copy';
669
+ copyCodeBtn.classList.remove('copied');
670
+ }, 2000);
671
+ })
672
+ .catch((err) => {
673
+ console.error('Copy code failed', err);
674
+ copyCodeBtn.textContent = 'Error';
675
+ setTimeout(() => {
676
+ copyCodeBtn.textContent = 'Copy';
677
+ }, 2000);
678
+ });
679
+ });
680
+ messageDiv.appendChild(preElement);
681
+ await typeSegment(codeElement, contentToProcess, true, codeElement, null);
682
+ }
683
+ }
684
+ } else {
685
+ segmentsToProcess.forEach((segment) => {
686
+ const contentToRender = segment.content.trim();
687
+ if (segment.type === 'text') {
688
+ const span = document.createElement('span');
689
+ span.innerHTML = escapeHTML(contentToRender);
690
+ messageDiv.appendChild(span);
691
+ } else if (segment.type === 'code') {
692
+ const preElement = document.createElement('pre');
693
+ const codeElement = document.createElement('code');
694
+ if (segment.lang) {
695
+ codeElement.className = `language-${segment.lang}`;
696
+ }
697
+ codeElement.textContent = contentToRender;
698
+ preElement.appendChild(codeElement);
699
+ const copyCodeBtn = document.createElement('button');
700
+ copyCodeBtn.className = 'copy-code-btn';
701
+ copyCodeBtn.textContent = 'Copy';
702
+ preElement.appendChild(copyCodeBtn);
703
+ copyCodeBtn.addEventListener('click', () => {
704
+ navigator.clipboard
705
+ .writeText(codeElement.textContent)
706
+ .then(() => {
707
+ copyCodeBtn.textContent = 'Copied!';
708
+ copyCodeBtn.classList.add('copied');
709
+ setTimeout(() => {
710
+ copyCodeBtn.textContent = 'Copy';
711
+ copyCodeBtn.classList.remove('copied');
712
+ }, 2000);
713
+ })
714
+ .catch((err) => {
715
+ console.error('Copy code failed', err);
716
+ copyCodeBtn.textContent = 'Error';
717
+ setTimeout(() => {
718
+ copyCodeBtn.textContent = 'Copy';
719
+ }, 2000);
720
+ });
721
+ });
722
+ messageDiv.appendChild(preElement);
723
+ if (typeof hljs !== 'undefined') {
724
+ try {
725
+ hljs.highlightElement(codeElement);
726
+ } catch (e) {
727
+ console.error(e);
728
+ }
729
+ }
730
+ }
731
+ });
732
+ if (sender === 'ai') messageDiv.style.opacity = 1;
733
+ }
734
+
735
+ let canAddCopyMsgButton = true;
736
+ if (
737
+ messageDiv.childNodes.length === 1 &&
738
+ messageDiv.firstChild &&
739
+ messageDiv.firstChild.nodeName === 'PRE'
740
+ ) {
741
+ canAddCopyMsgButton = false;
742
+ }
743
+ if (text.trim() && canAddCopyMsgButton) {
744
+ const copyMsgButton = document.createElement('button');
745
+ copyMsgButton.className = 'copy-msg-btn';
746
+ copyMsgButton.title = 'Copy message';
747
+ copyMsgButton.innerHTML = SVG_COPY_ICON;
748
+ messageDiv.appendChild(copyMsgButton);
749
+ copyMsgButton.addEventListener('click', (e) => {
750
+ e.stopPropagation();
751
+ navigator.clipboard
752
+ .writeText(text)
753
+ .then(() => {
754
+ copyMsgButton.innerHTML = SVG_CHECK_ICON;
755
+ copyMsgButton.classList.add('copied');
756
+ setTimeout(() => {
757
+ copyMsgButton.innerHTML = SVG_COPY_ICON;
758
+ copyMsgButton.classList.remove('copied');
759
+ }, 2000);
760
+ })
761
+ .catch((err) => {
762
+ console.error('Copy msg failed', err);
763
+ copyMsgButton.innerHTML = SVG_COPY_ICON;
764
+ alert('Failed to copy message.');
765
+ });
766
+ });
767
+ }
768
+
769
+ if (isScrolledToBottom(chatLog) || sender === 'user') {
770
+ chatLog.scrollTop = chatLog.scrollHeight;
771
+ }
772
+ }
773
+ })();
774
+ </script>
exocore-web/public/templates/footer.html ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <style>
2
+ @keyframes drawLine {
3
+ 0% {
4
+ width: 0%;
5
+ }
6
+ 100% {
7
+ width: 100%;
8
+ }
9
+ }
10
+
11
+ @keyframes fadeInText {
12
+ 0% {
13
+ opacity: 0;
14
+ transform: translateY(10px);
15
+ }
16
+ 100% {
17
+ opacity: 1;
18
+ transform: translateY(0);
19
+ }
20
+ }
21
+
22
+ .exocore-footer {
23
+ background-color: #1a1a1a;
24
+ color: #9e9e9e;
25
+ padding: 1.5rem 1rem;
26
+ text-align: center;
27
+ position: relative;
28
+ overflow: hidden;
29
+ border-top: 1px solid #282828;
30
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
31
+ }
32
+
33
+ .exocore-footer::before {
34
+ content: '';
35
+ position: absolute;
36
+ top: 0;
37
+ left: 50%;
38
+ transform: translateX(-50%);
39
+ width: 0%;
40
+ height: 2px;
41
+ background-color: #00bcd4;
42
+ animation: drawLine 1s cubic-bezier(0.23, 1, 0.32, 1) 0.5s forwards;
43
+ box-shadow: 0 0 8px rgba(0, 188, 212, 0.5);
44
+ }
45
+
46
+ .footer-content {
47
+ font-size: 0.85rem;
48
+ opacity: 0;
49
+ animation: fadeInText 0.8s ease-out 1s forwards;
50
+ }
51
+
52
+ .footer-content .exocore-name {
53
+ color: #00bcd4;
54
+ font-weight: 500;
55
+ }
56
+ </style>
57
+
58
+ <footer class="exocore-footer">
59
+ <div class="footer-content">
60
+ <small>&copy; 2025 <span class="exocore-name">Exocore</span>. All rights reserved.</small>
61
+ </div>
62
+ </footer>
exocore-web/public/templates/header.html ADDED
@@ -0,0 +1,373 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <style>
2
+ @keyframes sweepIn {
3
+ 0% {
4
+ clip-path: polygon(0 0, 0% 0, 0% 100%, 0 100%);
5
+ }
6
+ 100% {
7
+ clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%);
8
+ }
9
+ }
10
+
11
+ @keyframes sweepOut {
12
+ 0% {
13
+ clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%);
14
+ }
15
+ 100% {
16
+ clip-path: polygon(0 0, 0% 0, 0% 100%, 0 100%);
17
+ }
18
+ }
19
+
20
+ @keyframes linkFadeIn {
21
+ from {
22
+ opacity: 0;
23
+ transform: translateX(-20px);
24
+ }
25
+ to {
26
+ opacity: 1;
27
+ transform: translateX(0);
28
+ }
29
+ }
30
+
31
+ body {
32
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
33
+ background-color: #121212;
34
+ margin: 0;
35
+ color: #e0e0e0;
36
+ }
37
+
38
+ header {
39
+ background: #1a1a1a;
40
+ color: #e0e0e0;
41
+ padding: 0.8rem 1.25rem;
42
+ display: flex;
43
+ align-items: center;
44
+ z-index: 1001;
45
+ position: relative;
46
+ border-bottom: 1px solid #282828;
47
+ }
48
+
49
+ .menu-btn-wrapper {
50
+ background: transparent;
51
+ border: none;
52
+ color: #00bcd4;
53
+ cursor: pointer;
54
+ padding: 0.5rem;
55
+ margin-right: 1.2rem;
56
+ display: flex;
57
+ flex-direction: column;
58
+ justify-content: center;
59
+ align-items: center;
60
+ width: 40px;
61
+ height: 40px;
62
+ z-index: 1003;
63
+ position: relative;
64
+ transition: transform 0.3s ease;
65
+ }
66
+ .menu-btn-wrapper:hover {
67
+ transform: rotate(15deg);
68
+ }
69
+
70
+ .menu-btn-bar {
71
+ display: block;
72
+ width: 24px;
73
+ height: 2.5px;
74
+ background-color: #00bcd4;
75
+ border-radius: 1px;
76
+ transition: all 0.4s cubic-bezier(0.23, 1, 0.32, 1);
77
+ margin: 2.5px 0;
78
+ }
79
+
80
+ .menu-btn-wrapper.open .menu-btn-bar:nth-child(1) {
81
+ transform: rotate(45deg) translate(4.5px, 4.5px) scaleX(1.2);
82
+ }
83
+ .menu-btn-wrapper.open .menu-btn-bar:nth-child(2) {
84
+ opacity: 0;
85
+ transform: scaleX(0);
86
+ }
87
+ .menu-btn-wrapper.open .menu-btn-bar:nth-child(3) {
88
+ transform: rotate(-45deg) translate(4.5px, -4.5px) scaleX(1.2);
89
+ }
90
+
91
+ nav {
92
+ position: fixed;
93
+ top: 0;
94
+ left: 0;
95
+ width: 270px;
96
+ height: 100vh;
97
+ background-color: #1e1e1e;
98
+ z-index: 1002;
99
+ box-shadow: 4px 0 15px rgba(0, 0, 0, 0.3);
100
+ display: flex;
101
+ flex-direction: column;
102
+ clip-path: polygon(0 0, 0% 0, 0% 100%, 0 100%);
103
+ visibility: hidden;
104
+ }
105
+
106
+ nav.open {
107
+ animation: sweepIn 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards;
108
+ visibility: visible;
109
+ transition: visibility 0s;
110
+ }
111
+ nav.closing {
112
+ animation: sweepOut 0.4s cubic-bezier(0.55, 0.055, 0.675, 0.19) forwards;
113
+ visibility: visible;
114
+ }
115
+
116
+ .sidebar-backdrop {
117
+ position: fixed;
118
+ top: 0;
119
+ left: 0;
120
+ width: 100vw;
121
+ height: 100vh;
122
+ background: rgba(0, 0, 0, 0.7);
123
+ z-index: 1000;
124
+ opacity: 0;
125
+ pointer-events: none;
126
+ transition: opacity 0.4s ease;
127
+ }
128
+
129
+ .sidebar-backdrop.open {
130
+ opacity: 1;
131
+ pointer-events: auto;
132
+ }
133
+
134
+ .nav-header {
135
+ display: flex;
136
+ justify-content: space-between;
137
+ align-items: center;
138
+ padding: 0.8rem 1rem 0.8rem 1.5rem;
139
+ background-color: #161616;
140
+ border-bottom: 1px solid #2a2a2a;
141
+ }
142
+
143
+ .nav-header-title {
144
+ font-size: 1.1rem;
145
+ font-weight: 600;
146
+ color: #00bcd4;
147
+ letter-spacing: 0.5px;
148
+ }
149
+
150
+ .close-btn-nav {
151
+ background: transparent;
152
+ border: none;
153
+ color: #888;
154
+ font-size: 1.6rem;
155
+ font-weight: bold;
156
+ cursor: pointer;
157
+ padding: 0.5rem;
158
+ transition:
159
+ color 0.2s ease-in-out,
160
+ transform 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55);
161
+ }
162
+
163
+ .close-btn-nav:hover {
164
+ color: #00bcd4;
165
+ transform: rotate(180deg) scale(1.1);
166
+ }
167
+
168
+ .nav-links {
169
+ flex: 1;
170
+ display: flex;
171
+ flex-direction: column;
172
+ padding: 0.75rem 0;
173
+ overflow-y: auto;
174
+ }
175
+
176
+ .nav-links a {
177
+ color: #b0b0b0;
178
+ text-decoration: none;
179
+ padding: 0.95rem 1.75rem;
180
+ display: flex;
181
+ align-items: center;
182
+ position: relative;
183
+ font-size: 0.95rem;
184
+ letter-spacing: 0.3px;
185
+ transition:
186
+ color 0.25s ease,
187
+ background-color 0.25s ease;
188
+ opacity: 0;
189
+ }
190
+
191
+ nav.open .nav-links a {
192
+ animation: linkFadeIn 0.3s ease-out forwards;
193
+ }
194
+ nav.open .nav-links a:nth-child(1) {
195
+ animation-delay: 0.15s;
196
+ }
197
+ nav.open .nav-links a:nth-child(2) {
198
+ animation-delay: 0.2s;
199
+ }
200
+ nav.open .nav-links a:nth-child(3) {
201
+ animation-delay: 0.25s;
202
+ }
203
+ nav.open .nav-links a:nth-child(4) {
204
+ animation-delay: 0.3s;
205
+ }
206
+ nav.open .nav-links a:nth-child(5) {
207
+ animation-delay: 0.35s;
208
+ }
209
+ nav.open .nav-links a:nth-child(6) {
210
+ animation-delay: 0.4s;
211
+ }
212
+
213
+
214
+ .nav-links a::before {
215
+ content: '';
216
+ position: absolute;
217
+ left: 0;
218
+ top: 0;
219
+ bottom: 0;
220
+ width: 0px;
221
+ background-color: #00bcd4;
222
+ transition: width 0.3s cubic-bezier(0.23, 1, 0.32, 1);
223
+ }
224
+
225
+ .nav-links a:hover {
226
+ color: #ffffff;
227
+ background-color: rgba(0, 188, 212, 0.1);
228
+ }
229
+ .nav-links a:hover::before,
230
+ .nav-links a.active::before {
231
+ width: 4px;
232
+ }
233
+
234
+ .nav-links a.active {
235
+ color: #ffffff;
236
+ font-weight: 500;
237
+ background-color: rgba(0, 188, 212, 0.15);
238
+ }
239
+ .nav-links a.active::before {
240
+ width: 4px;
241
+ }
242
+
243
+ h1.header-title {
244
+ margin: 0;
245
+ flex-grow: 1;
246
+ font-weight: 700;
247
+ font-size: 1.5rem;
248
+ color: #f0f0f0;
249
+ letter-spacing: 0.5px;
250
+ text-shadow: 0 0 5px rgba(0, 188, 212, 0.3);
251
+ }
252
+ </style>
253
+
254
+ <header>
255
+ <button class="menu-btn-wrapper" id="menuBtnWrapper" aria-label="Open menu" aria-expanded="false">
256
+ <span class="menu-btn-bar"></span>
257
+ <span class="menu-btn-bar"></span>
258
+ <span class="menu-btn-bar"></span>
259
+ </button>
260
+ <h1 class="header-title">Exocore Dashboard</h1>
261
+ </header>
262
+
263
+ <div class="sidebar-backdrop" id="sidebarBackdrop"></div>
264
+
265
+ <nav id="sideNav">
266
+ <div class="nav-header">
267
+ <span class="nav-header-title">EXOCORE</span>
268
+ <button class="close-btn-nav" id="closeBtnNav" aria-label="Close menu">×</button>
269
+ </div>
270
+ <div class="nav-links">
271
+ <a href="/private/server/exocore/web/public/dashboard">Dashboard</a>
272
+ <a href="/private/server/exocore/web/public/profile">Profile</a>
273
+ <a href="/private/server/exocore/web/public/manager">File Manager</a>
274
+ <a href="/private/server/exocore/web/public/console">Console</a>
275
+ <a href="/private/server/exocore/web/public/shell">Shell</a>
276
+ <a href="#" id="logoutLink">Logout</a>
277
+ </div>
278
+ </nav>
279
+
280
+ <script>
281
+ const menuBtnWrapper = document.getElementById('menuBtnWrapper');
282
+ const sideNav = document.getElementById('sideNav');
283
+ const closeBtnNav = document.getElementById('closeBtnNav');
284
+ const backdrop = document.getElementById('sidebarBackdrop');
285
+ const navLinks = document.querySelectorAll('.nav-links a');
286
+ const logoutLink = document.getElementById('logoutLink');
287
+ let isNavAnimating = false;
288
+
289
+ function openSidebar() {
290
+ if (isNavAnimating || sideNav.classList.contains('open')) return;
291
+ isNavAnimating = true;
292
+
293
+ sideNav.classList.remove('closing');
294
+ sideNav.classList.add('open');
295
+ backdrop.classList.add('open');
296
+ menuBtnWrapper.classList.add('open');
297
+ menuBtnWrapper.setAttribute('aria-expanded', 'true');
298
+
299
+ sideNav.addEventListener(
300
+ 'animationend',
301
+ () => {
302
+ isNavAnimating = false;
303
+ },
304
+ { once: true }
305
+ );
306
+ }
307
+
308
+ function closeSidebar() {
309
+ if (isNavAnimating || !sideNav.classList.contains('open')) return;
310
+ isNavAnimating = true;
311
+
312
+ sideNav.classList.remove('open');
313
+ sideNav.classList.add('closing');
314
+ backdrop.classList.remove('open');
315
+ menuBtnWrapper.classList.remove('open');
316
+ menuBtnWrapper.setAttribute('aria-expanded', 'false');
317
+
318
+ sideNav.addEventListener(
319
+ 'animationend',
320
+ () => {
321
+ sideNav.classList.remove('closing');
322
+ isNavAnimating = false;
323
+ },
324
+ { once: true }
325
+ );
326
+ }
327
+
328
+ function toggleSidebar() {
329
+ if (sideNav.classList.contains('open')) {
330
+ closeSidebar();
331
+ } else {
332
+ openSidebar();
333
+ }
334
+ }
335
+
336
+ menuBtnWrapper.addEventListener('click', toggleSidebar);
337
+ closeBtnNav.addEventListener('click', closeSidebar);
338
+ backdrop.addEventListener('click', closeSidebar);
339
+
340
+ const currentPath = window.location.pathname;
341
+ const currentPathSpan = document.getElementById('currentPathSpan');
342
+ if (currentPathSpan) {
343
+ currentPathSpan.textContent = currentPath;
344
+ }
345
+
346
+ navLinks.forEach((link) => {
347
+ if (link.id !== 'logoutLink' && link.getAttribute('href') === currentPath) {
348
+ link.classList.add('active');
349
+ }
350
+ });
351
+
352
+ if (logoutLink) {
353
+ logoutLink.addEventListener('click', async function(event) {
354
+ event.preventDefault();
355
+
356
+ try {
357
+ await fetch('/private/server/exocore/web/logout', {
358
+ method: 'POST',
359
+ headers: {
360
+ 'Content-Type': 'application/json',
361
+ },
362
+ });
363
+ } catch (error) {
364
+ console.error('Logout fetch error:', error);
365
+ } finally {
366
+ localStorage.removeItem('exocore-token');
367
+ localStorage.removeItem('exocore-cookies');
368
+ window.location.href = '/private/server/exocore/web/public/login';
369
+
370
+ }
371
+ });
372
+ }
373
+ </script>