ParthSadaria commited on
Commit
189656a
·
verified ·
1 Parent(s): b2ec9cb

Update playground.html

Browse files
Files changed (1) hide show
  1. playground.html +610 -1042
playground.html CHANGED
@@ -1,652 +1,299 @@
1
  <!DOCTYPE html>
2
  <html lang="en">
3
-
4
  <head>
5
  <meta charset="UTF-8">
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
- <title>Loki.AI Playground</title>
8
  <link rel="icon" type="image/x-icon" href="favicon.ico">
9
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@300;400;500;600&display=swap" rel="stylesheet">
10
- <script src="https://cdnjs.cloudflare.com/ajax/libs/animejs/3.2.1/anime.min.js"></script>
11
- <script src="https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.2.19/tailwind.min.js"></script>
12
  <style>
13
  :root {
14
- --bg-dark: #0a0a0f;
15
- --bg-darker: #040409;
16
- --bg-deepest: #020205;
17
- --primary-blue: #4a6cf7;
18
- --secondary-blue: #6678e3;
19
- --accent-color: #7e57c2;
20
- --text-light: #e0e0e8;
21
- --text-muted: #8a8a9b;
22
- --border-color: #1a1a2e;
23
- --hover-color: rgba(78, 108, 247, 0.1);
24
- --delete-red: #ff4d4d;
25
- --header-height: 60px;
26
- --input-height: 80px;
27
- }
28
-
29
- * {
30
- margin: 0;
31
- padding: 0;
32
- box-sizing: border-box;
33
- scrollbar-width: thin;
34
- scrollbar-color: var(--primary-blue) transparent;
35
- }
36
-
37
- *::-webkit-scrollbar {
38
- width: 8px;
39
- }
40
-
41
- *::-webkit-scrollbar-track {
42
- background: transparent;
43
- }
44
-
45
- *::-webkit-scrollbar-thumb {
46
- background-color: var(--primary-blue);
47
- border-radius: 20px;
48
- }
49
-
50
  body {
51
- font-family: 'Inter', sans-serif;
52
- background-color: var(--bg-dark);
53
- color: var(--text-light);
 
54
  display: flex;
55
  justify-content: center;
56
  align-items: center;
57
- min-height: 100vh;
58
- overflow: hidden;
59
- perspective: 2000px;
60
  }
61
-
 
 
 
 
 
 
 
62
  .chat-wrapper {
63
- position: relative;
64
- width: 100%;
65
- max-width: 1000px;
66
  height: 90vh;
67
- background-color: var(--bg-darker);
68
- border-radius: 24px;
 
69
  overflow: hidden;
70
- box-shadow: 0 20px 50px rgba(5, 5, 10, 0.7);
71
  display: flex;
72
  flex-direction: column;
73
- opacity: 1;
74
- transform: scale(1);
75
- transition: all 0.6s cubic-bezier(0.23, 1, 0.32, 1);
76
- border: 1px solid var(--border-color);
77
  }
78
-
79
  .chat-header {
80
- background-color: var(--bg-deepest);
81
- padding: 15px 20px;
82
  display: flex;
83
  justify-content: space-between;
84
  align-items: center;
85
- border-bottom: 1px solid var(--border-color);
86
- flex-shrink: 0;
87
- height: var(--header-height);
88
  }
89
-
90
- .chat-container {
91
- display: none;
92
- flex-direction: column;
93
- height: calc(100% - var(--header-height));
94
- position: relative;
95
- }
96
-
97
- .chat-messages {
98
- flex: 1;
99
- overflow-y: auto;
100
- padding: 20px;
101
- display: flex;
102
- flex-direction: column;
103
- gap: 16px;
104
- background-color: var(--bg-dark);
105
- font-family: 'JetBrains Mono', monospace;
106
- height: calc(100% - var(--input-height));
107
- margin-bottom: var(--input-height);
108
  }
109
-
110
- .chat-input {
111
- position: absolute;
112
- bottom: 0;
113
- left: 0;
114
- right: 0;
115
- background-color: var(--bg-darker);
116
- border-top: 1px solid var(--border-color);
117
- padding: 16px;
118
- height: var(--input-height);
119
  display: flex;
120
- gap: 12px;
121
- z-index: 10;
122
- align-items: center;
123
  }
124
-
125
- .chat-input-container {
126
- position: relative;
127
- flex: 1;
 
 
 
128
  display: flex;
129
  align-items: center;
 
130
  }
131
-
132
- .chat-input input {
133
- width: 100%;
134
- padding: 12px 16px;
135
- padding-right: 50px;
136
- background-color: rgba(30, 30, 50, 0.8);
137
- border: 1px solid var(--border-color);
138
- border-radius: 12px;
139
- color: var(--text-light);
140
- font-size: 14px;
141
- font-family: 'JetBrains Mono', monospace;
142
- outline: none;
143
- }
144
-
145
- .chat-input input:focus {
146
- border-color: var(--primary-blue);
147
- box-shadow: 0 0 0 3px rgba(74, 108, 247, 0.2);
148
- }
149
-
150
- .message {
151
- max-width: 80%;
152
- width: fit-content;
153
- padding: 12px 18px;
154
- border-radius: 12px;
155
- font-size: 14px;
156
- line-height: 1.5;
157
  position: relative;
158
- animation: fadeIn 0.4s forwards;
159
- margin-bottom: 8px;
160
- }
161
-
162
- .message.user {
163
- align-self: flex-end;
164
- background-color: var(--primary-blue);
165
- color: white;
166
  }
167
-
168
- .message.bot {
169
- align-self: flex-start;
170
- background-color: rgba(40, 40, 70, 0.8);
171
- color: var(--text-light);
172
- border: 1px solid var(--border-color);
 
 
173
  }
174
-
175
- .message::before {
176
- content: '';
177
  position: absolute;
178
- top: 0;
179
  left: 0;
180
  right: 0;
181
- bottom: 0;
182
- background: linear-gradient(to right, transparent, rgba(255, 255, 255, 0.1));
183
- opacity: 0;
184
- transition: opacity 0.3s ease;
 
 
 
185
  }
186
-
187
- .message:hover::before {
188
- opacity: 1;
 
 
189
  }
190
-
 
 
 
 
 
191
  .initial-input {
192
- flex: 1;
193
  display: flex;
194
  flex-direction: column;
195
- justify-content: center;
196
  align-items: center;
197
- padding: 24px;
198
- text-align: center;
199
- background: linear-gradient(145deg, var(--bg-dark), var(--bg-darker));
200
- }
201
-
202
- .initial-input h2 {
203
- font-size: 24px;
204
- margin-bottom: 16px;
205
- color: var(--text-light);
206
- font-weight: 600;
207
- font-family: 'JetBrains Mono', monospace;
208
  }
209
-
210
  .input-container {
211
  width: 100%;
212
- max-width: 400px;
213
  position: relative;
214
  }
215
-
216
- .initial-input input {
217
  width: 100%;
218
- padding: 16px 24px;
219
- background-color: rgba(30, 30, 50, 0.8);
220
- border: 1px solid var(--border-color);
221
- border-radius: 12px;
222
- color: var(--text-light);
223
- font-size: 16px;
224
- font-family: 'JetBrains Mono', monospace;
 
 
 
225
  outline: none;
226
- transition: all 0.3s ease;
227
  }
228
-
229
  .send-icon {
230
  position: absolute;
231
- right: 12px;
232
  top: 50%;
233
  transform: translateY(-50%);
234
- background-color: var(--primary-blue);
235
- border-radius: 8px;
236
- width: 40px;
237
- height: 40px;
238
- display: flex;
239
- justify-content: center;
240
- align-items: center;
241
  cursor: pointer;
242
- transition: all 0.3s ease;
243
- }
244
-
245
- .send-icon:hover {
246
- background-color: var(--accent-color);
247
- transform: translateY(-50%) scale(1.05);
248
- border-radius: 15px;
249
- }
250
-
251
- @keyframes fadeIn {
252
- from {
253
- opacity: 0;
254
- transform: translateY(20px);
255
- }
256
-
257
- to {
258
- opacity: 1;
259
- transform: translateY(0);
260
- }
261
  }
262
-
263
- /* Header actions */
264
- .header-actions {
265
- display: flex;
266
- align-items: center;
267
- gap: 10px;
268
- justify-content: center;
269
- }
270
-
271
- .model-select {
272
- background-color: black;
273
- color: var(--text-light);
274
- border: 1px solid var(--border-color);
275
- border-radius: 8px;
276
- padding: 8px 12px;
277
- font-family: 'JetBrains Mono', monospace;
278
- font-size: 14px;
279
- cursor: pointer;
280
- transition: all 0.3s ease;
281
- max-width: 240px;
282
- overflow-y: auto;
283
- -webkit-appearance: none;
284
- -moz-appearance: none;
285
- appearance: none;
286
- background-image: url("data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2214%22%20height%3D%2214%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22%23ffffff%22%20stroke-width%3D%222%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Cpolyline%20points%3D%226%209%2012%2015%2018%209%22%3E%3C%2Fpolyline%3E%3C%2Fsvg%3E");
287
- background-repeat: no-repeat;
288
- background-position: right 8px center;
289
- padding-right: 28px;
290
- }
291
-
292
- .model-select option {
293
- background-color: var(--bg-dark);
294
- color: var(--text-light);
295
- padding: 8px;
296
- }
297
-
298
- .clear-chat {
299
- background-color: var(--delete-red);
300
- color: white;
301
- border: none;
302
- border-radius: 8px;
303
- width: 36px;
304
- height: 36px;
305
- padding: 8px;
306
- display: flex;
307
- align-items: center;
308
- justify-content: center;
309
- cursor: pointer;
310
- transition: all 0.3s ease;
311
- }
312
-
313
- .clear-chat:hover {
314
- background-color: #ff6666;
315
- }
316
-
317
- .clear-chat svg {
318
- width: 18px;
319
- height: 18px;
320
- }
321
-
322
- .watermark {
323
- position: absolute;
324
- bottom: 10px;
325
- right: 15px;
326
- color: var(--text-muted);
327
- font-size: 10px;
328
- opacity: 0.5;
329
- transition: opacity 0.3s ease;
330
- z-index: 11;
331
- }
332
-
333
- .watermark:hover {
334
- opacity: 0.8;
335
- }
336
-
337
- /* Loading spinner */
338
- .loader {
339
  display: none;
340
- width: 24px;
341
- height: 24px;
342
- border: 3px solid rgba(255, 255, 255, 0.3);
343
- border-radius: 50%;
344
- border-top-color: var(--primary-blue);
345
- animation: spin 1s ease-in-out infinite;
346
  }
347
-
348
- @keyframes spin {
349
- to {
350
- transform: rotate(360deg);
351
- }
352
  }
353
-
354
- .github-link {
355
- display: flex;
356
- align-items: center;
357
- justify-content: center;
358
- width: 36px;
359
- height: 36px;
360
- z-index: 999;
361
  border-radius: 8px;
362
- background-color: transparent;
363
- border: 1px solid var(--border-color);
364
- transition: all 0.3s ease;
365
- color: var(--text-light);
366
- position: absolute;
367
- bottom: 0;
368
- left: 0;
369
- margin: 10px;
370
  }
371
-
372
- .github-link:hover {
373
- background-color: var(--hover-color);
374
- transform: translateY(-2px);
375
  }
376
-
377
- .github-link svg {
378
- width: 22px;
379
- height: 22px;
380
  }
381
-
382
- /* Enhanced select dropdown */
383
- .custom-select-wrapper {
384
- position: relative;
385
- min-width: 180px;
386
  }
387
-
388
- .custom-select {
389
  position: relative;
390
- background-color: rgba(20, 20, 40, 0.9);
391
- color: var(--text-light);
392
- border: 1px solid var(--border-color);
393
- border-radius: 8px;
394
- padding: 8px 35px 8px 12px;
395
- font-family: 'JetBrains Mono', monospace;
396
- font-size: 14px;
397
- cursor: pointer;
398
- transition: all 0.3s ease;
399
- text-overflow: ellipsis;
400
- overflow: hidden;
401
- white-space: nowrap;
402
- max-width: 240px;
403
- }
404
-
405
- .custom-select:after {
406
- content: '';
407
- position: absolute;
408
- right: 10px;
409
- top: 50%;
410
- transform: translateY(-50%);
411
- width: 0;
412
- height: 0;
413
- border-left: 5px solid transparent;
414
- border-right: 5px solid transparent;
415
- border-top: 5px solid var(--text-light);
416
- }
417
-
418
- .custom-select-options {
419
- position: absolute;
420
- top: 100%;
421
- left: 0;
422
- right: 0;
423
- background-color: rgba(15, 15, 30, 0.95);
424
- border: 1px solid var(--border-color);
425
- border-radius: 8px;
426
- margin-top: 4px;
427
- max-height: 300px;
428
- overflow-y: auto;
429
- z-index: 1000;
430
- display: none;
431
- backdrop-filter: blur(10px);
432
- box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
433
  }
434
-
435
- .custom-option {
436
- padding: 8px 12px;
437
- cursor: pointer;
438
- transition: all 0.2s ease;
439
- }
440
-
441
- .custom-option:hover {
442
- background-color: rgba(74, 108, 247, 0.2);
443
- }
444
-
445
- .model-group {
446
- padding: 5px 10px;
447
- font-size: 10px;
448
- color: var(--text-muted);
449
- text-transform: uppercase;
450
- letter-spacing: 1px;
451
- border-bottom: 1px solid rgba(255, 255, 255, 0.1);
452
- margin-top: 5px;
453
- }
454
-
455
- /* Dark mode toggle */
456
- .theme-toggle {
457
- background-color: transparent;
458
- border: 1px solid var(--border-color);
459
- border-radius: 8px;
460
- width: 36px;
461
- height: 36px;
462
  display: flex;
463
  align-items: center;
464
- justify-content: center;
465
- cursor: pointer;
466
- transition: all 0.3s ease;
467
- color: var(--text-light);
468
  }
469
-
470
- .theme-toggle:hover {
471
- background-color: var(--hover-color);
 
 
 
 
472
  }
473
-
474
- /* Mobile optimizations */
475
- @media screen and (max-width: 768px) {
476
- .chat-wrapper {
477
- height: 100vh;
478
- border-radius: 0;
479
- }
480
-
481
- .custom-select {
482
- font-size: 12px;
483
- max-width: 140px;
484
- }
485
-
486
- .chat-input {
487
- padding: 10px;
488
- }
489
  }
490
-
491
- /* Markdown styling */
492
- .bot pre {
493
- background-color: rgba(15, 15, 30, 0.6);
494
- padding: 10px;
495
- border-radius: 6px;
496
  overflow-x: auto;
497
- margin: 10px 0;
498
- border: 1px solid rgba(255, 255, 255, 0.1);
499
- }
500
-
501
- .bot code {
502
- font-family: 'JetBrains Mono', monospace;
503
- font-size: 12px;
504
  }
505
-
506
- .bot p {
507
- margin-bottom: 8px;
508
- }
509
-
510
- .bot ul, .bot ol {
511
- margin-left: 20px;
512
- margin-bottom: 8px;
513
  }
 
 
514
 
515
- /* Typing indicator */
516
- .typing-indicator {
517
- display: inline-flex;
518
- align-items: center;
519
- gap: 5px;
520
- padding: 5px 10px;
521
- }
522
-
523
- .typing-indicator span {
524
- width: 6px;
525
- height: 6px;
526
- background-color: var(--text-muted);
527
  border-radius: 50%;
528
- display: inline-block;
529
- animation: pulse 1.5s infinite ease-in-out;
530
  }
531
-
532
- .typing-indicator span:nth-child(2) {
533
- animation-delay: 0.2s;
534
  }
535
 
536
- .typing-indicator span:nth-child(3) {
537
- animation-delay: 0.4s;
 
 
 
 
 
538
  }
539
 
540
- @keyframes pulse {
541
- 0%, 100% {
542
- transform: scale(0.8);
543
- opacity: 0.5;
544
- }
545
- 50% {
546
- transform: scale(1.2);
547
- opacity: 1;
548
- }
549
  }
550
- /* markdown */
551
- /* Add this to your style section */
552
- .message pre {
553
- background-color: rgba(15, 15, 30, 0.6);
554
- padding: 10px;
555
- border-radius: 6px;
556
- overflow-x: auto;
557
- margin: 10px 0;
558
- border: 1px solid rgba(255, 255, 255, 0.1);
559
- }
560
-
561
- .message code {
562
- font-family: 'JetBrains Mono', monospace;
563
- font-size: 12px;
564
- background-color: rgba(30, 30, 50, 0.3);
565
- padding: 2px 4px;
566
- border-radius: 3px;
567
- }
568
-
569
- .message pre code {
570
- background-color: transparent;
571
- padding: 0;
572
- }
573
-
574
- .message blockquote {
575
- border-left: 3px solid var(--primary-blue);
576
- padding-left: 10px;
577
- margin: 10px 0;
578
- color: var(--text-muted);
579
- }
580
-
581
- .message h1, .message h2, .message h3 {
582
- margin: 15px 0 5px 0;
583
- }
584
-
585
- .message ul, .message ol {
586
- margin-left: 20px;
587
- margin-bottom: 8px;
588
- }
589
-
590
- .message a {
591
- color: var(--primary-blue);
592
- text-decoration: underline;
593
- }
594
-
595
- .message p {
596
- margin-bottom: 8px;
597
- }
598
-
599
- .message img {
600
- max-width: 100%;
601
- border-radius: 8px;
602
- margin: 10px 0;
603
- }
604
-
605
- .message table {
606
- border-collapse: collapse;
607
- width: 100%;
608
- margin: 15px 0;
609
- }
610
-
611
- .message th, .message td {
612
- border: 1px solid var(--border-color);
613
- padding: 8px;
614
- text-align: left;
615
- }
616
-
617
- .message th {
618
- background-color: rgba(15, 15, 30, 0.6);
619
- }
620
  </style>
621
  </head>
622
-
623
  <body>
624
  <a href="https://github.com/ParthSadaria" target="_blank" rel="noopener noreferrer" class="github-link" aria-label="Visit Parth Sadaria's GitHub profile">
625
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
626
  <path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"></path>
627
  </svg>
628
  </a>
 
629
  <div class="chat-wrapper">
630
  <div class="chat-header">
631
- <h2 style="font-family: 'JetBrains Mono', monospace;">LOKI.AI Playground</h2>
632
  <div class="header-actions">
633
  <div class="custom-select-wrapper">
634
  <div class="custom-select" id="modelSelectDisplay">Select Model</div>
635
  <div class="custom-select-options" id="modelOptions">
636
- <!-- Models will be loaded here -->
637
- <div class="loader" id="modelLoader" style="margin: 10px auto;"></div>
638
  </div>
639
  </div>
640
- <button id="clearChatButton" class="clear-chat">
641
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
642
- stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
643
  <polyline points="3 6 5 6 21 6"></polyline>
644
  <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
645
  <line x1="10" y1="11" x2="10" y2="17"></line>
646
  <line x1="14" y1="11" x2="14" y2="17"></line>
647
  </svg>
648
  </button>
649
- <button id="themeToggle" class="theme-toggle">
650
  <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
651
  <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
652
  </svg>
@@ -659,8 +306,7 @@
659
  <div class="input-container">
660
  <input type="text" id="initialChatInput" placeholder="What can I help you with today?">
661
  <div class="send-icon" id="initialSendIcon">
662
- <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
663
- stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
664
  <line x1="22" y1="2" x2="11" y2="13"></line>
665
  <polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
666
  </svg>
@@ -674,8 +320,7 @@
674
  <div class="chat-input-container">
675
  <input type="text" id="chatInput" placeholder="Type your message...">
676
  <div class="send-icon" id="sendButtonIcon">
677
- <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
678
- stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
679
  <line x1="22" y1="2" x2="11" y2="13"></line>
680
  <polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
681
  </svg>
@@ -683,545 +328,468 @@
683
  </div>
684
  </div>
685
  </div>
686
- <p style="display: block; text-align: center; color: grey; font-size: 9px; z-index: 99; padding-bottom:5px;">
687
  Models can make mistakes. Check important info.
688
  </p>
689
  </div>
690
  </div>
 
691
  <div class="watermark">
692
  Made with ❤️ by Parth Sadaria
693
  </div>
694
- <script>
695
- document.addEventListener('DOMContentLoaded', function() {
696
- const chatWrapper = document.querySelector('.chat-wrapper');
697
- const initialInput = document.querySelector('.initial-input');
698
- const chatContainer = document.getElementById('chatContainer');
699
- const initialChatInput = document.getElementById('initialChatInput');
700
- const initialSendIcon = document.getElementById('initialSendIcon');
701
- const chatMessages = document.getElementById('chatMessages');
702
- const chatInput = document.getElementById('chatInput');
703
- const sendButtonIcon = document.getElementById('sendButtonIcon');
704
- const sendLoader = document.getElementById('sendLoader');
705
- const clearChatButton = document.getElementById('clearChatButton');
706
- const modelSelectDisplay = document.getElementById('modelSelectDisplay');
707
- const modelOptions = document.getElementById('modelOptions');
708
- const modelLoader = document.getElementById('modelLoader');
709
- const themeToggle = document.getElementById('themeToggle');
710
-
711
- let currentStreamingMessage = null;
712
- let isStreamingInProgress = false;
713
- let conversationHistory = [{ role: "system", content: "You are a helpful AI assistant. Assist the user effectively." }];
714
- let selectedModel = '';
715
- let modelsList = [];
716
- let isDarkMode = true;
717
-
718
- // Fetch models from API
719
- async function fetchModels() {
720
- modelLoader.style.display = 'block';
721
- try {
722
- const response = await fetch('https://parthsadaria-lokiai.hf.space/models', {
723
- method: 'GET',
724
- headers: {
725
- 'Authorization': 'playground'
726
- }
727
- });
728
-
729
- if (response.ok) {
730
- const data = await response.json();
731
- modelsList = data;
732
- populateModels(data);
733
- } else {
734
- console.error('Failed to fetch models:', response.status);
735
- // Fallback to some default models
736
- const fallbackModels = [
737
- { id: "gpt-4o", object: "model" },
738
- { id: "gpt-3.5-turbo", object: "model" },
739
- { id: "claude-3-5-sonnet", object: "model" },
740
- { id: "claude-3-haiku", object: "model" },
741
- { id: "llama-3.1-70b", object: "model" }
742
- ];
743
- populateModels(fallbackModels);
744
- }
745
- } catch (error) {
746
- console.error('Error fetching models:', error);
747
- // Use the same fallback
748
- const fallbackModels = [
749
- { id: "gpt-4o", object: "model" },
750
- { id: "gpt-3.5-turbo", object: "model" },
751
- { id: "claude-3-5-sonnet", object: "model" },
752
- { id: "claude-3-haiku", object: "model" },
753
- { id: "llama-3.1-70b", object: "model" }
754
- ];
755
- populateModels(fallbackModels);
756
- } finally {
757
- modelLoader.style.display = 'none';
758
- }
759
- }
760
 
761
- // Organize and display models
762
- function populateModels(models) {
763
- // Group models by provider
764
- const providers = {};
765
- models.forEach(model => {
766
- const modelName = model.id;
767
- let provider = 'Other';
768
-
769
- if (modelName.includes('gpt')) provider = 'OpenAI';
770
- else if (modelName.includes('claude')) provider = 'Anthropic';
771
- else if (modelName.includes('llama')) provider = 'Meta';
772
- else if (modelName.includes('gemini')) provider = 'Google';
773
- else if (modelName.includes('mistral')) provider = 'Mistral';
774
- else if (modelName.includes('yi')) provider = 'Yi';
775
-
776
- if (!providers[provider]) providers[provider] = [];
777
- providers[provider].push(model);
778
- });
779
-
780
- // Clear existing options
781
- modelOptions.innerHTML = '';
782
-
783
- // Add special option for web search
784
- const webSearchOption = document.createElement('div');
785
- webSearchOption.className = 'custom-option';
786
- webSearchOption.textContent = 'SearchGPT (Web-access)';
787
- webSearchOption.dataset.value = 'searchgpt';
788
- modelOptions.appendChild(webSearchOption);
789
-
790
- // Add a separator
791
- const separator = document.createElement('div');
792
- separator.className = 'model-group';
793
- separator.textContent = 'AI Models';
794
- modelOptions.appendChild(separator);
795
-
796
- // Create and append provider groups and their models
797
- Object.keys(providers).sort().forEach(provider => {
798
- const providerGroup = document.createElement('div');
799
- providerGroup.className = 'model-group';
800
- providerGroup.textContent = provider;
801
- modelOptions.appendChild(providerGroup);
802
-
803
- providers[provider].sort((a, b) => a.id.localeCompare(b.id)).forEach(model => {
804
- const option = document.createElement('div');
805
- option.className = 'custom-option';
806
- option.textContent = model.id;
807
- option.dataset.value = model.id;
808
- modelOptions.appendChild(option);
809
- });
810
- });
811
 
812
- // Set default model if none selected yet
813
- if (!selectedModel && models.length > 0) {
814
- const preferredModels = ['gpt-4o', 'gpt-4', 'claude-3-5-sonnet', 'gpt-3.5-turbo'];
815
-
816
- // Find the first preferred model that exists in our list
817
- for (const preferred of preferredModels) {
818
- const match = models.find(m => m.id.includes(preferred));
819
- if (match) {
820
- selectedModel = match.id;
821
- modelSelectDisplay.textContent = selectedModel;
822
- break;
823
- }
824
- }
 
 
825
 
826
- // If no preferred model found, use the first one in the list
827
- if (!selectedModel) {
828
- selectedModel = models[0].id;
829
- modelSelectDisplay.textContent = selectedModel;
 
 
 
 
 
 
 
 
 
 
 
830
  }
831
- }
832
-
833
- // Add event listeners to options
834
- document.querySelectorAll('.custom-option').forEach(option => {
835
- option.addEventListener('click', function() {
836
- selectedModel = this.dataset.value;
837
- modelSelectDisplay.textContent = this.textContent;
838
- modelOptions.style.display = 'none';
839
- });
840
- });
841
- }
842
-
843
- function scrollToBottom() {
844
- chatMessages.scrollTop = chatMessages.scrollHeight;
845
- }
846
-
847
- function appendMessage(content, type = 'bot', isStreaming = false) {
848
- if (type === 'bot' && isStreaming) {
849
- if (!currentStreamingMessage) {
850
- currentStreamingMessage = document.createElement('div');
851
- currentStreamingMessage.className = `message ${type}`;
852
- chatMessages.appendChild(currentStreamingMessage);
853
-
854
- // Add to conversation history only at the start of streaming
855
- if (!isStreamingInProgress) {
856
- conversationHistory.push({
857
- role: 'assistant',
858
- content: ''
859
  });
860
- isStreamingInProgress = true;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
861
  }
862
- }
863
-
864
- // Replace the existing formatting code in the appendMessage function
865
- const formattedContent = content
866
- .replace(/```([\s\S]*?)```/g, (match, code) => `<pre><code>${code}</code></pre>`)
867
- .replace(/`([^`]+)`/g, '<code>$1</code>')
868
- .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
869
- .replace(/\*(.*?)\*/g, '<em>$1</em>')
870
- .replace(/~~(.*?)~~/g, '<del>$1</del>')
871
- .replace(/\[(.*?)\]\((.*?)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>')
872
- .replace(/^#{3}\s+(.*?)$/gm, '<h3>$1</h3>')
873
- .replace(/^#{2}\s+(.*?)$/gm, '<h2>$1</h2>')
874
- .replace(/^#{1}\s+(.*?)$/gm, '<h1>$1</h1>')
875
- .replace(/^\>\s+(.*?)$/gm, '<blockquote>$1</blockquote>')
876
- .replace(/^(\d+)\.\s+(.*?)$/gm, '<ol start="$1"><li>$2</li></ol>')
877
- .replace(/^[-*]\s+(.*?)$/gm, '<ul><li>$1</li></ul>')
878
- .replace(/\n/g, '<br>');
879
-
880
- currentStreamingMessage.innerHTML += formattedContent;
881
-
882
- // Update the last message in conversation history
883
- if (conversationHistory.length > 0) {
884
- conversationHistory[conversationHistory.length - 1].content += content;
885
- }
886
-
887
- scrollToBottom();
888
- } else {
889
- if (type === 'bot' && currentStreamingMessage) {
890
- isStreamingInProgress = false;
891
- currentStreamingMessage = null;
892
- } else {
893
- const messageBox = document.createElement('div');
894
- messageBox.className = `message ${type}`;
895
-
896
- // Apply markdown-like formatting
897
- const formattedContent = content
898
- .replace(/```([\s\S]*?)```/g, (match, code) => `<pre><code>${code}</code></pre>`)
899
- .replace(/(\d+)\.\s*/g, '<br>$1. ')
900
- .replace(/\n/g, '<br>')
901
- .replace(/\*\*(.*?)\*\*/g, (match, p1) => '<strong>' + p1 + '</strong>');
902
-
903
- messageBox.innerHTML = formattedContent;
904
- chatMessages.appendChild(messageBox);
905
-
906
- // Add to conversation history only for new messages
907
- if (type === 'user') {
908
- conversationHistory.push({
909
- role: 'user',
910
- content: content
911
- });
912
- } else if (type === 'bot' && !isStreamingInProgress) {
913
- conversationHistory.push({
914
- role: 'assistant',
915
- content: content
916
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
917
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
918
 
919
- scrollToBottom();
920
- }
921
- }
922
- }
923
-
924
- function clearChat() {
925
- chatMessages.innerHTML = '';
926
- conversationHistory = [{ role: "system", content: "You are a helpful AI assistant. Assist the user effectively." }];
927
- initialInput.style.display = 'flex';
928
- chatContainer.style.display = 'none';
929
- }
930
 
931
- async function sendInitialMessage() {
932
- const userMessage = initialChatInput.value.trim();
933
- if (!userMessage || !selectedModel) return;
934
-
935
- initialInput.style.display = 'none';
936
- chatContainer.style.display = 'flex';
937
-
938
- appendMessage(userMessage, 'user');
939
- initialChatInput.value = '';
940
- scrollToBottom();
941
-
942
- // Show the loader, hide the send icon
943
- sendLoader.style.display = 'block';
944
- sendButtonIcon.querySelector('svg').style.display = 'none';
945
-
946
- try {
947
- await callApi(userMessage, selectedModel);
948
- } catch (error) {
949
- appendMessage("Oops! Something went wrong. Please try again.", 'bot');
950
- console.error("API Error:", error);
951
- } finally {
952
- // Hide loader, show send icon
953
- sendLoader.style.display = 'none';
954
- sendButtonIcon.querySelector('svg').style.display = 'block';
955
- }
956
- }
957
 
958
- async function sendMessage() {
959
- const userMessage = chatInput.value.trim();
960
- if (!userMessage || !selectedModel) return;
961
-
962
- appendMessage(userMessage, 'user');
963
- chatInput.value = '';
964
-
965
- // Show the loader, hide the send icon
966
- sendLoader.style.display = 'block';
967
- sendButtonIcon.querySelector('svg').style.display = 'none';
968
-
969
- try {
970
- await callApi(userMessage, selectedModel);
971
- } catch (error) {
972
- appendMessage("Oops! Something went wrong. Please try again.", 'bot');
973
- console.error("API Error:", error);
974
- } finally {
975
- // Hide loader, show send icon
976
- sendLoader.style.display = 'none';
977
- sendButtonIcon.querySelector('svg').style.display = 'block';
978
- }
979
- }
980
 
981
- async function callApi(userMessage, model) {
982
- let typingIndicator = null;
983
-
984
- try {
985
- // Add typing indicator before any API call
986
- typingIndicator = document.createElement('div');
987
- typingIndicator.className = 'message bot typing-indicator';
988
- typingIndicator.innerHTML = '<span></span><span></span><span></span>';
989
- chatMessages.appendChild(typingIndicator);
990
- scrollToBottom();
991
 
992
- if (model === "searchgpt") {
993
- const url = `https://parthsadaria-lokiai.hf.space/searchgpt?q=${encodeURIComponent(userMessage)}&stream=true&systemprompt=You are **SearchGPT**, an AI with internet access. Reply directly and accurately to user requests. Mention Sources at end`;
 
 
 
 
 
 
 
 
 
994
 
995
- const response = await fetch(url, {
996
- headers: {
997
- 'Authorization': 'playground'
998
- }
999
- });
1000
 
1001
- if (response.ok) {
1002
- // Remove typing indicator
1003
- if (typingIndicator) {
1004
- typingIndicator.remove();
1005
- typingIndicator = null;
1006
- }
1007
-
1008
- const reader = response.body.getReader();
1009
- const decoder = new TextDecoder("utf-8");
1010
- let done = false;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1011
 
1012
- while (!done) {
1013
- const { value, done: streamDone } = await reader.read();
1014
- done = streamDone;
 
 
 
1015
 
1016
- if (value) {
1017
- const chunk = decoder.decode(value);
1018
- const cleanedChunk = chunk.trim();
 
 
1019
 
1020
- // Handle different API response formats
1021
- if (cleanedChunk.startsWith('data:')) {
1022
- const jsonChunks = cleanedChunk.split("data:").filter(Boolean);
 
 
 
 
1023
 
1024
- for (const jsonString of jsonChunks) {
1025
- try {
1026
- const cleanJson = jsonString.trim();
1027
- if (cleanJson === '[DONE]') continue;
1028
-
1029
- const jsonData = JSON.parse(cleanJson);
1030
- const content = jsonData.choices?.[0]?.message?.content ||
1031
- jsonData.choices?.[0]?.delta?.content || "";
1032
 
1033
- if (content) {
1034
- appendMessage(content, 'bot', true);
 
 
 
 
 
 
 
 
 
 
 
 
 
1035
  }
1036
- } catch (err) {
1037
- console.warn("Parsing error:", err, "for chunk:", jsonString);
1038
  }
1039
  }
1040
- } else {
1041
- // Handle plain text response
1042
- appendMessage(cleanedChunk, 'bot', true);
1043
  }
 
 
1044
  }
1045
- }
1046
- } else {
1047
- throw new Error(`API responded with status ${response.status}`);
1048
- }
1049
- } else {
1050
- const url = "https://parthsadaria-lokiai.hf.space/chat/completions";
1051
- const payload = {
1052
- model: model,
1053
- messages: [...conversationHistory],
1054
- stream: true
1055
- };
1056
-
1057
- const headers = {
1058
- "Content-Type": "application/json",
1059
- "Authorization": "playground"
1060
- };
1061
-
1062
- const response = await fetch(url, {
1063
- method: "POST",
1064
- headers: headers,
1065
- body: JSON.stringify(payload)
1066
- });
1067
-
1068
- if (response.ok) {
1069
- // Remove typing indicator
1070
- if (typingIndicator) {
1071
- typingIndicator.remove();
1072
- typingIndicator = null;
1073
- }
1074
-
1075
- const reader = response.body.getReader();
1076
- const decoder = new TextDecoder("utf-8");
1077
- let done = false;
1078
- let buffer = "";
1079
-
1080
- while (!done) {
1081
- const { value, done: streamDone } = await reader.read();
1082
- done = streamDone;
1083
 
1084
- if (value) {
1085
- const chunk = decoder.decode(value);
1086
- buffer += chunk;
 
 
1087
 
1088
- // Process complete data chunks
1089
- const lines = buffer.split('\n');
1090
- buffer = lines.pop() || ""; // Keep the last incomplete line in buffer
 
1091
 
1092
- for (const line of lines) {
1093
- if (line.trim() === '') continue;
1094
-
1095
- const cleanedLine = line.replace(/^data:\s*/, '').trim();
1096
- if (cleanedLine === '[DONE]') continue;
1097
-
1098
- try {
1099
- const jsonData = JSON.parse(cleanedLine);
1100
- const content = jsonData.choices?.[0]?.delta?.content || "";
1101
-
1102
- if (content) {
1103
- appendMessage(content, 'bot', true);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1104
  }
1105
- } catch (err) {
1106
- console.warn("Parsing error:", err, "for line:", cleanedLine);
1107
  }
1108
  }
 
 
1109
  }
1110
  }
1111
-
1112
- // Handle any remaining data in buffer
1113
- if (buffer.trim() !== '') {
1114
- try {
1115
- const cleanedBuffer = buffer.replace(/^data:\s*/, '').trim();
1116
- if (cleanedBuffer !== '[DONE]') {
1117
- const jsonData = JSON.parse(cleanedBuffer);
1118
- const content = jsonData.choices?.[0]?.delta?.content || "";
1119
-
1120
- if (content) {
1121
- appendMessage(content, 'bot', true);
1122
- }
1123
- }
1124
- } catch (err) {
1125
- console.warn("Parsing error for remaining buffer:", err);
1126
- }
1127
  }
1128
- } else {
1129
- throw new Error(`API responded with status ${response.status}`);
1130
  }
1131
- }
1132
-
1133
- // End streaming after successful completion
1134
- appendMessage("", 'bot', false);
1135
-
1136
- } catch (error) {
1137
- console.error("API call error:", error);
1138
- // Remove typing indicator if still present
1139
- if (typingIndicator) {
1140
- typingIndicator.remove();
1141
- }
1142
- throw error;
1143
- }
1144
- }
1145
-
1146
- // Toggle theme
1147
- function toggleTheme() {
1148
- isDarkMode = !isDarkMode;
1149
- document.body.classList.toggle('light-mode');
1150
-
1151
- // Update icon based on current theme
1152
- if (isDarkMode) {
1153
- themeToggle.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
1154
- <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
1155
- </svg>`;
1156
- } else {
1157
- themeToggle.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
1158
- <circle cx="12" cy="12" r="5"></circle>
1159
- <line x1="12" y1="1" x2="12" y2="3"></line>
1160
- <line x1="12" y1="21" x2="12" y2="23"></line>
1161
- <line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
1162
- <line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
1163
- <line x1="1" y1="12" x2="3" y2="12"></line>
1164
- <line x1="21" y1="12" x2="23" y2="12"></line>
1165
- <line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
1166
- <line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
1167
- </svg>`;
1168
- }
1169
- }
1170
-
1171
- // Event Listeners
1172
- initialSendIcon.addEventListener('click', sendInitialMessage);
1173
- initialChatInput.addEventListener('keypress', (event) => {
1174
- if (event.key === 'Enter') sendInitialMessage();
1175
- });
1176
-
1177
- sendButtonIcon.addEventListener('click', sendMessage);
1178
- chatInput.addEventListener('keypress', (event) => {
1179
- if (event.key === 'Enter') sendMessage();
1180
- });
1181
 
1182
- // Clear Chat Button Event Listener
1183
- clearChatButton.addEventListener('click', clearChat);
 
 
 
 
1184
 
1185
- // Model selector dropdown event listeners
1186
- modelSelectDisplay.addEventListener('click', function() {
1187
- if (modelOptions.style.display === 'block') {
1188
- modelOptions.style.display = 'none';
1189
- } else {
1190
- modelOptions.style.display = 'block';
1191
- }
1192
- });
1193
 
1194
- // Close dropdown when clicking outside
1195
- document.addEventListener('click', function(event) {
1196
- if (!event.target.closest('.custom-select-wrapper') && modelOptions.style.display === 'block') {
1197
- modelOptions.style.display = 'none';
1198
- }
1199
- });
1200
-
1201
- // Theme toggle event listener
1202
- themeToggle.addEventListener('click', toggleTheme);
1203
 
1204
- // Initialize the app
1205
- fetchModels();
 
 
 
 
1206
 
1207
- // Add an animation to the chat wrapper
1208
- if (typeof anime !== 'undefined') {
1209
- anime({
1210
- targets: '.chat-wrapper',
1211
- translateY: [50, 0],
1212
- opacity: [0, 1],
1213
- easing: 'easeOutExpo',
1214
- duration: 1000
1215
- });
1216
- }
1217
 
1218
- // Detect escape key to close model selector
1219
- document.addEventListener('keydown', function(event) {
1220
- if (event.key === 'Escape' && modelOptions.style.display === 'block') {
1221
- modelOptions.style.display = 'none';
1222
- }
1223
  });
1224
- });
1225
- </script>
1226
  </body>
1227
- </html>
 
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>LOKI.AI Playground</title>
7
  <link rel="icon" type="image/x-icon" href="favicon.ico">
8
+ <link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@300;400;500;600;700&family=Fira+Code:wght@300;400;500;600&display=swap" rel="stylesheet">
 
 
9
  <style>
10
  :root {
11
+ --bg-primary: #121212;
12
+ --bg-secondary: #1e1e1e;
13
+ --text-primary: #f5f5f5;
14
+ --text-secondary: #a0a0a0;
15
+ --accent: #6b46c1;
16
+ --accent-hover: #805ad5;
17
+ --border: #333333;
18
+ --message-user: #2d3748;
19
+ --message-bot: #1a202c;
20
+ }
21
+ body.light-mode {
22
+ --bg-primary: #f5f5f5;
23
+ --bg-secondary: #e5e5e5;
24
+ --text-primary: #1a1a1a;
25
+ --text-secondary: #4a4a4a;
26
+ --message-user: #e2e8f0;
27
+ --message-bot: #edf2f7;
28
+ }
29
+ * { margin: 0; padding: 0; box-sizing: border-box; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  body {
31
+ font-family: 'Montserrat', sans-serif;
32
+ background-color: var(--bg-primary);
33
+ color: var(--text-primary);
34
+ min-height: 100vh;
35
  display: flex;
36
  justify-content: center;
37
  align-items: center;
38
+ transition: background-color 0.3s ease;
 
 
39
  }
40
+ .github-link {
41
+ position: fixed;
42
+ top: 1rem;
43
+ right: 1rem;
44
+ z-index: 100;
45
+ color: var(--text-primary);
46
+ }
47
+ .github-link:hover { color: var(--accent); }
48
  .chat-wrapper {
49
+ width: 95%;
50
+ max-width: 900px;
 
51
  height: 90vh;
52
+ background-color: var(--bg-secondary);
53
+ border-radius: 12px;
54
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
55
  overflow: hidden;
 
56
  display: flex;
57
  flex-direction: column;
 
 
 
 
58
  }
 
59
  .chat-header {
 
 
60
  display: flex;
61
  justify-content: space-between;
62
  align-items: center;
63
+ padding: 1rem;
64
+ border-bottom: 1px solid var(--border);
 
65
  }
66
+ .chat-header h2 {
67
+ font-family: 'Fira Code', monospace;
68
+ font-weight: 600;
69
+ margin: 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
  }
71
+ .header-actions {
 
 
 
 
 
 
 
 
 
72
  display: flex;
73
+ gap: 0.75rem;
 
 
74
  }
75
+ button {
76
+ background: none;
77
+ border: none;
78
+ color: var(--text-primary);
79
+ cursor: pointer;
80
+ padding: 0.5rem;
81
+ border-radius: 6px;
82
  display: flex;
83
  align-items: center;
84
+ justify-content: center;
85
  }
86
+ button:hover { background-color: rgba(255, 255, 255, 0.1); }
87
+
88
+ .custom-select-wrapper {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
  position: relative;
90
+ min-width: 180px;
 
 
 
 
 
 
 
91
  }
92
+ .custom-select {
93
+ border: 1px solid var(--border);
94
+ padding: 0.5rem;
95
+ border-radius: 6px;
96
+ cursor: pointer;
97
+ white-space: nowrap;
98
+ overflow: hidden;
99
+ text-overflow: ellipsis;
100
  }
101
+ .custom-select-options {
102
+ display: none;
 
103
  position: absolute;
104
+ top: 100%;
105
  left: 0;
106
  right: 0;
107
+ max-height: 300px;
108
+ overflow-y: auto;
109
+ background-color: var(--bg-secondary);
110
+ border: 1px solid var(--border);
111
+ border-radius: 6px;
112
+ z-index: 100;
113
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
114
  }
115
+ .model-group {
116
+ padding: 0.5rem;
117
+ font-weight: 500;
118
+ color: var(--text-secondary);
119
+ border-bottom: 1px solid var(--border);
120
  }
121
+ .custom-option {
122
+ padding: 0.5rem;
123
+ cursor: pointer;
124
+ }
125
+ .custom-option:hover { background-color: rgba(255, 255, 255, 0.1); }
126
+
127
  .initial-input {
 
128
  display: flex;
129
  flex-direction: column;
 
130
  align-items: center;
131
+ justify-content: center;
132
+ flex: 1;
133
+ gap: 2rem;
134
+ padding: 2rem;
 
 
 
 
 
 
 
135
  }
 
136
  .input-container {
137
  width: 100%;
138
+ max-width: 600px;
139
  position: relative;
140
  }
141
+ .input-container input, .chat-input input {
 
142
  width: 100%;
143
+ padding: 1rem;
144
+ padding-right: 3rem;
145
+ background-color: var(--bg-primary);
146
+ color: var(--text-primary);
147
+ border: 1px solid var(--border);
148
+ border-radius: 6px;
149
+ font-size: 1rem;
150
+ font-family: 'Montserrat', sans-serif;
151
+ }
152
+ .input-container input:focus, .chat-input input:focus {
153
  outline: none;
154
+ border-color: var(--accent);
155
  }
 
156
  .send-icon {
157
  position: absolute;
158
+ right: 0.5rem;
159
  top: 50%;
160
  transform: translateY(-50%);
 
 
 
 
 
 
 
161
  cursor: pointer;
162
+ padding: 0.5rem;
163
+ color: var(--text-primary);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
164
  }
165
+ .send-icon:hover { color: var(--accent); }
166
+
167
+ .chat-container {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
168
  display: none;
169
+ flex-direction: column;
170
+ flex: 1;
171
+ overflow: hidden;
 
 
 
172
  }
173
+ .chat-messages {
174
+ flex: 1;
175
+ overflow-y: auto;
176
+ padding: 1rem;
177
+ scroll-behavior: smooth;
178
  }
179
+ .message {
180
+ margin-bottom: 1rem;
181
+ padding: 1rem;
 
 
 
 
 
182
  border-radius: 8px;
183
+ max-width: 80%;
184
+ word-wrap: break-word;
 
 
 
 
 
 
185
  }
186
+ .message.user {
187
+ background-color: var(--message-user);
188
+ margin-left: auto;
 
189
  }
190
+ .message.bot {
191
+ background-color: var(--message-bot);
192
+ margin-right: auto;
 
193
  }
194
+ .chat-input {
195
+ padding: 1rem;
196
+ border-top: 1px solid var(--border);
 
 
197
  }
198
+ .chat-input-container {
 
199
  position: relative;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
200
  }
201
+
202
+ .typing-indicator {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
203
  display: flex;
204
  align-items: center;
205
+ justify-content: flex-start;
206
+ gap: 0.25rem;
207
+ padding: 0.75rem;
 
208
  }
209
+ .typing-indicator span {
210
+ width: 8px;
211
+ height: 8px;
212
+ background-color: var(--text-secondary);
213
+ border-radius: 50%;
214
+ display: inline-block;
215
+ animation: typing 1.5s infinite ease-in-out;
216
  }
217
+ .typing-indicator span:nth-child(2) { animation-delay: 0.2s; }
218
+ .typing-indicator span:nth-child(3) { animation-delay: 0.4s; }
219
+
220
+ @keyframes typing {
221
+ 0%, 60%, 100% { transform: translateY(0); }
222
+ 30% { transform: translateY(-6px); }
 
 
 
 
 
 
 
 
 
 
223
  }
224
+
225
+ pre {
226
+ background-color: rgba(0, 0, 0, 0.2);
227
+ padding: 1rem;
228
+ border-radius: 4px;
 
229
  overflow-x: auto;
230
+ margin: 0.5rem 0;
 
 
 
 
 
 
231
  }
232
+ code {
233
+ font-family: 'Fira Code', monospace;
234
+ font-size: 0.9rem;
 
 
 
 
 
235
  }
236
+ a { color: var(--accent); text-decoration: none; }
237
+ a:hover { text-decoration: underline; }
238
 
239
+ .loader {
240
+ display: none;
241
+ width: 18px;
242
+ height: 18px;
243
+ border: 2px solid transparent;
244
+ border-top-color: var(--text-primary);
 
 
 
 
 
 
245
  border-radius: 50%;
246
+ animation: spin 1s linear infinite;
 
247
  }
248
+ @keyframes spin {
249
+ 0% { transform: rotate(0deg); }
250
+ 100% { transform: rotate(360deg); }
251
  }
252
 
253
+ .watermark {
254
+ position: fixed;
255
+ bottom: 0.5rem;
256
+ font-size: 0.8rem;
257
+ color: var(--text-secondary);
258
+ text-align: center;
259
+ width: 100%;
260
  }
261
 
262
+ @media (max-width: 768px) {
263
+ .chat-wrapper { width: 100%; height: 100vh; border-radius: 0; }
264
+ .chat-header h2 { font-size: 1.2rem; }
265
+ .message { max-width: 90%; }
266
+ .custom-select-wrapper { min-width: 150px; }
267
+ .github-link { top: 0.5rem; right: 0.5rem; }
 
 
 
268
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
269
  </style>
270
  </head>
 
271
  <body>
272
  <a href="https://github.com/ParthSadaria" target="_blank" rel="noopener noreferrer" class="github-link" aria-label="Visit Parth Sadaria's GitHub profile">
273
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
274
  <path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"></path>
275
  </svg>
276
  </a>
277
+
278
  <div class="chat-wrapper">
279
  <div class="chat-header">
280
+ <h2>LOKI.AI Playground</h2>
281
  <div class="header-actions">
282
  <div class="custom-select-wrapper">
283
  <div class="custom-select" id="modelSelectDisplay">Select Model</div>
284
  <div class="custom-select-options" id="modelOptions">
285
+ <div class="loader" id="modelLoader"></div>
 
286
  </div>
287
  </div>
288
+ <button id="clearChatButton" title="Clear Chat">
289
+ <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
 
290
  <polyline points="3 6 5 6 21 6"></polyline>
291
  <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
292
  <line x1="10" y1="11" x2="10" y2="17"></line>
293
  <line x1="14" y1="11" x2="14" y2="17"></line>
294
  </svg>
295
  </button>
296
+ <button id="themeToggle" title="Toggle Theme">
297
  <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
298
  <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
299
  </svg>
 
306
  <div class="input-container">
307
  <input type="text" id="initialChatInput" placeholder="What can I help you with today?">
308
  <div class="send-icon" id="initialSendIcon">
309
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
 
310
  <line x1="22" y1="2" x2="11" y2="13"></line>
311
  <polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
312
  </svg>
 
320
  <div class="chat-input-container">
321
  <input type="text" id="chatInput" placeholder="Type your message...">
322
  <div class="send-icon" id="sendButtonIcon">
323
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
 
324
  <line x1="22" y1="2" x2="11" y2="13"></line>
325
  <polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
326
  </svg>
 
328
  </div>
329
  </div>
330
  </div>
331
+ <p style="text-align:center; color:var(--text-secondary); font-size:9px; padding-bottom:5px;">
332
  Models can make mistakes. Check important info.
333
  </p>
334
  </div>
335
  </div>
336
+
337
  <div class="watermark">
338
  Made with ❤️ by Parth Sadaria
339
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
340
 
341
+ <script>
342
+ document.addEventListener('DOMContentLoaded', () => {
343
+ // DOM elements
344
+ const elements = {
345
+ chatWrapper: document.querySelector('.chat-wrapper'),
346
+ initialInput: document.querySelector('.initial-input'),
347
+ chatContainer: document.getElementById('chatContainer'),
348
+ initialChatInput: document.getElementById('initialChatInput'),
349
+ initialSendIcon: document.getElementById('initialSendIcon'),
350
+ chatMessages: document.getElementById('chatMessages'),
351
+ chatInput: document.getElementById('chatInput'),
352
+ sendButtonIcon: document.getElementById('sendButtonIcon'),
353
+ sendLoader: document.getElementById('sendLoader'),
354
+ clearChatButton: document.getElementById('clearChatButton'),
355
+ modelSelectDisplay: document.getElementById('modelSelectDisplay'),
356
+ modelOptions: document.getElementById('modelOptions'),
357
+ modelLoader: document.getElementById('modelLoader'),
358
+ themeToggle: document.getElementById('themeToggle')
359
+ };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
360
 
361
+ // State variables
362
+ let state = {
363
+ currentStreamingMessage: null,
364
+ isStreamingInProgress: false,
365
+ conversationHistory: [{ role: "system", content: "You are a helpful AI assistant. Assist the user effectively." }],
366
+ selectedModel: '',
367
+ modelsList: [],
368
+ isDarkMode: true
369
+ };
370
+
371
+ // Helper functions
372
+ const helpers = {
373
+ scrollToBottom: () => {
374
+ elements.chatMessages.scrollTop = elements.chatMessages.scrollHeight;
375
+ },
376
 
377
+ formatMessage: (content) => {
378
+ return content
379
+ .replace(/```([\s\S]*?)```/g, (match, code) => `<pre><code>${code}</code></pre>`)
380
+ .replace(/`([^`]+)`/g, '<code>$1</code>')
381
+ .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
382
+ .replace(/\*(.*?)\*/g, '<em>$1</em>')
383
+ .replace(/~~(.*?)~~/g, '<del>$1</del>')
384
+ .replace(/\[(.*?)\]\((.*?)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>')
385
+ .replace(/^#{3}\s+(.*?)$/gm, '<h3>$1</h3>')
386
+ .replace(/^#{2}\s+(.*?)$/gm, '<h2>$1</h2>')
387
+ .replace(/^#{1}\s+(.*?)$/gm, '<h1>$1</h1>')
388
+ .replace(/^\>\s+(.*?)$/gm, '<blockquote>$1</blockquote>')
389
+ .replace(/^(\d+)\.\s+(.*?)$/gm, '<ol start="$1"><li>$2</li></ol>')
390
+ .replace(/^[-*]\s+(.*?)$/gm, '<ul><li>$1</li></ul>')
391
+ .replace(/\n/g, '<br>');
392
  }
393
+ };
394
+
395
+ // Core functionality
396
+ const app = {
397
+ // Fetch models from API
398
+ async fetchModels() {
399
+ elements.modelLoader.style.display = 'block';
400
+ try {
401
+ const response = await fetch('https://parthsadaria-lokiai.hf.space/models', {
402
+ headers: { 'Authorization': 'playground' }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
403
  });
404
+
405
+ if (response.ok) {
406
+ state.modelsList = await response.json();
407
+ app.populateModels(state.modelsList);
408
+ } else {
409
+ console.error('Failed to fetch models:', response.status);
410
+ app.populateModels([
411
+ { id: "gpt-4o", object: "model" },
412
+ { id: "gpt-3.5-turbo", object: "model" },
413
+ { id: "claude-3-5-sonnet", object: "model" },
414
+ { id: "claude-3-haiku", object: "model" },
415
+ { id: "llama-3.1-70b", object: "model" }
416
+ ]);
417
+ }
418
+ } catch (error) {
419
+ console.error('Error fetching models:', error);
420
+ app.populateModels([
421
+ { id: "gpt-4o", object: "model" },
422
+ { id: "gpt-3.5-turbo", object: "model" },
423
+ { id: "claude-3-5-sonnet", object: "model" },
424
+ { id: "claude-3-haiku", object: "model" },
425
+ { id: "llama-3.1-70b", object: "model" }
426
+ ]);
427
+ } finally {
428
+ elements.modelLoader.style.display = 'none';
429
  }
430
+ },
431
+
432
+ // Organize and display models
433
+ populateModels(models) {
434
+ // Group models by provider
435
+ const providers = {};
436
+ models.forEach(model => {
437
+ const modelName = model.id;
438
+ let provider = 'Other';
439
+
440
+ if (modelName.includes('gpt')) provider = 'OpenAI';
441
+ else if (modelName.includes('claude')) provider = 'Anthropic';
442
+ else if (modelName.includes('llama')) provider = 'Meta';
443
+ else if (modelName.includes('gemini')) provider = 'Google';
444
+ else if (modelName.includes('mistral')) provider = 'Mistral';
445
+ else if (modelName.includes('yi')) provider = 'Yi';
446
+
447
+ if (!providers[provider]) providers[provider] = [];
448
+ providers[provider].push(model);
449
+ });
450
+
451
+ // Clear existing options
452
+ elements.modelOptions.innerHTML = '';
453
+
454
+ // Add special option for web search
455
+ const webSearchOption = document.createElement('div');
456
+ webSearchOption.className = 'custom-option';
457
+ webSearchOption.textContent = 'SearchGPT (Web-access)';
458
+ webSearchOption.dataset.value = 'searchgpt';
459
+ elements.modelOptions.appendChild(webSearchOption);
460
+
461
+ // Add a separator
462
+ const separator = document.createElement('div');
463
+ separator.className = 'model-group';
464
+ separator.textContent = 'AI Models';
465
+ elements.modelOptions.appendChild(separator);
466
+
467
+ // Create and append provider groups and their models
468
+ Object.keys(providers).sort().forEach(provider => {
469
+ const providerGroup = document.createElement('div');
470
+ providerGroup.className = 'model-group';
471
+ providerGroup.textContent = provider;
472
+ elements.modelOptions.appendChild(providerGroup);
473
+
474
+ providers[provider].sort((a, b) => a.id.localeCompare(b.id)).forEach(model => {
475
+ const option = document.createElement('div');
476
+ option.className = 'custom-option';
477
+ option.textContent = model.id;
478
+ option.dataset.value = model.id;
479
+ elements.modelOptions.appendChild(option);
 
 
 
 
480
  });
481
+ });
482
+
483
+ // Set default model
484
+ if (!state.selectedModel && models.length > 0) {
485
+ const preferredModels = ['gpt-4o', 'gpt-4', 'claude-3-5-sonnet', 'gpt-3.5-turbo'];
486
+
487
+ for (const preferred of preferredModels) {
488
+ const match = models.find(m => m.id.includes(preferred));
489
+ if (match) {
490
+ state.selectedModel = match.id;
491
+ elements.modelSelectDisplay.textContent = state.selectedModel;
492
+ break;
493
+ }
494
+ }
495
+
496
+ if (!state.selectedModel) {
497
+ state.selectedModel = models[0].id;
498
+ elements.modelSelectDisplay.textContent = state.selectedModel;
499
+ }
500
  }
501
+
502
+ // Add event listeners to options
503
+ document.querySelectorAll('.custom-option').forEach(option => {
504
+ option.addEventListener('click', function() {
505
+ state.selectedModel = this.dataset.value;
506
+ elements.modelSelectDisplay.textContent = this.textContent;
507
+ elements.modelOptions.style.display = 'none';
508
+ });
509
+ });
510
+ },
511
+
512
+ // Message handling
513
+ appendMessage(content, type = 'bot', isStreaming = false) {
514
+ if (type === 'bot' && isStreaming) {
515
+ if (!state.currentStreamingMessage) {
516
+ state.currentStreamingMessage = document.createElement('div');
517
+ state.currentStreamingMessage.className = `message ${type}`;
518
+ elements.chatMessages.appendChild(state.currentStreamingMessage);
519
+
520
+ if (!state.isStreamingInProgress) {
521
+ state.conversationHistory.push({
522
+ role: 'assistant',
523
+ content: ''
524
+ });
525
+ state.isStreamingInProgress = true;
526
+ }
527
+ }
528
 
529
+ state.currentStreamingMessage.innerHTML += helpers.formatMessage(content);
 
 
 
 
 
 
 
 
 
 
530
 
531
+ if (state.conversationHistory.length > 0) {
532
+ state.conversationHistory[state.conversationHistory.length - 1].content += content;
533
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
534
 
535
+ helpers.scrollToBottom();
536
+ } else {
537
+ if (type === 'bot' && state.currentStreamingMessage) {
538
+ state.isStreamingInProgress = false;
539
+ state.currentStreamingMessage = null;
540
+ } else {
541
+ const messageBox = document.createElement('div');
542
+ messageBox.className = `message ${type}`;
543
+ messageBox.innerHTML = helpers.formatMessage(content);
544
+ elements.chatMessages.appendChild(messageBox);
545
+
546
+ if (type === 'user') {
547
+ state.conversationHistory.push({
548
+ role: 'user',
549
+ content: content
550
+ });
551
+ } else if (type === 'bot' && !state.isStreamingInProgress) {
552
+ state.conversationHistory.push({
553
+ role: 'assistant',
554
+ content: content
555
+ });
556
+ }
557
 
558
+ helpers.scrollToBottom();
559
+ }
560
+ }
561
+ },
 
 
 
 
 
 
562
 
563
+ // Chat operations
564
+ clearChat() {
565
+ elements.chatMessages.innerHTML = '';
566
+ state.conversationHistory = [{ role: "system", content: "You are a helpful AI assistant. Assist the user effectively." }];
567
+ elements.initialInput.style.display = 'flex';
568
+ elements.chatContainer.style.display = 'none';
569
+ },
570
+
571
+ async sendInitialMessage() {
572
+ const userMessage = elements.initialChatInput.value.trim();
573
+ if (!userMessage || !state.selectedModel) return;
574
 
575
+ elements.initialInput.style.display = 'none';
576
+ elements.chatContainer.style.display = 'flex';
 
 
 
577
 
578
+ app.appendMessage(userMessage, 'user');
579
+ elements.initialChatInput.value = '';
580
+ helpers.scrollToBottom();
581
+
582
+ elements.sendLoader.style.display = 'block';
583
+ elements.sendButtonIcon.querySelector('svg').style.display = 'none';
584
+
585
+ try {
586
+ await app.callApi(userMessage, state.selectedModel);
587
+ } catch (error) {
588
+ app.appendMessage("Oops! Something went wrong. Please try again.", 'bot');
589
+ console.error("API Error:", error);
590
+ } finally {
591
+ elements.sendLoader.style.display = 'none';
592
+ elements.sendButtonIcon.querySelector('svg').style.display = 'block';
593
+ }
594
+ },
595
+
596
+ async sendMessage() {
597
+ const userMessage = elements.chatInput.value.trim();
598
+ if (!userMessage || !state.selectedModel) return;
599
+
600
+ app.appendMessage(userMessage, 'user');
601
+ elements.chatInput.value = '';
602
+
603
+ elements.sendLoader.style.display = 'block';
604
+ elements.sendButtonIcon.querySelector('svg').style.display = 'none';
605
+
606
+ try {
607
+ await app.callApi(userMessage, state.selectedModel);
608
+ } catch (error) {
609
+ app.appendMessage("Oops! Something went wrong. Please try again.", 'bot');
610
+ console.error("API Error:", error);
611
+ } finally {
612
+ elements.sendLoader.style.display = 'none';
613
+ elements.sendButtonIcon.querySelector('svg').style.display = 'block';
614
+ }
615
+ },
616
+
617
+ async callApi(userMessage, model) {
618
+ let typingIndicator = null;
619
+
620
+ try {
621
+ typingIndicator = document.createElement('div');
622
+ typingIndicator.className = 'message bot typing-indicator';
623
+ typingIndicator.innerHTML = '<span></span><span></span><span></span>';
624
+ elements.chatMessages.appendChild(typingIndicator);
625
+ helpers.scrollToBottom();
626
 
627
+ if (model === "searchgpt") {
628
+ const url = `https://parthsadaria-lokiai.hf.space/searchgpt?q=${encodeURIComponent(userMessage)}&stream=true&systemprompt=You are **SearchGPT**, an AI with internet access. Reply directly and accurately to user requests. Mention Sources at end`;
629
+
630
+ const response = await fetch(url, {
631
+ headers: { }
632
+ });
633
 
634
+ if (response.ok) {
635
+ if (typingIndicator) {
636
+ typingIndicator.remove();
637
+ typingIndicator = null;
638
+ }
639
 
640
+ const reader = response.body.getReader();
641
+ const decoder = new TextDecoder("utf-8");
642
+ let done = false;
643
+
644
+ while (!done) {
645
+ const { value, done: streamDone } = await reader.read();
646
+ done = streamDone;
647
 
648
+ if (value) {
649
+ const chunk = decoder.decode(value);
650
+ const cleanedChunk = chunk.trim();
651
+
652
+ if (cleanedChunk.startsWith('data:')) {
653
+ const jsonChunks = cleanedChunk.split("data:").filter(Boolean);
 
 
654
 
655
+ for (const jsonString of jsonChunks) {
656
+ try {
657
+ const cleanJson = jsonString.trim();
658
+ if (cleanJson === '[DONE]') continue;
659
+
660
+ const jsonData = JSON.parse(cleanJson);
661
+ const content = jsonData.choices?.[0]?.message?.content ||
662
+ jsonData.choices?.[0]?.delta?.content || "";
663
+
664
+ if (content) {
665
+ app.appendMessage(content, 'bot', true);
666
+ }
667
+ } catch (err) {
668
+ console.warn("Parsing error:", err);
669
+ }
670
  }
671
+ } else {
672
+ app.appendMessage(cleanedChunk, 'bot', true);
673
  }
674
  }
 
 
 
675
  }
676
+ } else {
677
+ throw new Error(`API responded with status ${response.status}`);
678
  }
679
+ } else {
680
+ const url = "https://parthsadaria-lokiai.hf.space/chat/completions";
681
+ const payload = {
682
+ model: model,
683
+ messages: [...state.conversationHistory],
684
+ stream: true
685
+ };
686
+
687
+ const response = await fetch(url, {
688
+ method: "POST",
689
+ headers: {
690
+ "Content-Type": "application/json"
691
+ },
692
+ body: JSON.stringify(payload)
693
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
694
 
695
+ if (response.ok) {
696
+ if (typingIndicator) {
697
+ typingIndicator.remove();
698
+ typingIndicator = null;
699
+ }
700
 
701
+ const reader = response.body.getReader();
702
+ const decoder = new TextDecoder("utf-8");
703
+ let done = false;
704
+ let buffer = "";
705
 
706
+ while (!done) {
707
+ const { value, done: streamDone } = await reader.read();
708
+ done = streamDone;
709
+ if (value) {
710
+ buffer += decoder.decode(value, { stream: true });
711
+
712
+ let boundary = buffer.indexOf('\n');
713
+ while (boundary !== -1) {
714
+ let chunk = buffer.slice(0, boundary);
715
+ buffer = buffer.slice(boundary + 1);
716
+
717
+ if (chunk.startsWith('data:')) {
718
+ const jsonChunks = chunk.split("data:").filter(Boolean);
719
+
720
+ for (const jsonString of jsonChunks) {
721
+ try {
722
+ const cleanJson = jsonString.trim();
723
+ if (cleanJson === '[DONE]') continue;
724
+
725
+ const jsonData = JSON.parse(cleanJson);
726
+ const content = jsonData.choices?.[0]?.message?.content ||
727
+ jsonData.choices?.[0]?.delta?.content || "";
728
+
729
+ if (content) {
730
+ app.appendMessage(content, 'bot', true);
731
+ }
732
+ } catch (err) {
733
+ console.warn("Parsing error:", err);
734
+ }
735
+ }
736
+ } else {
737
+ app.appendMessage(chunk, 'bot', true);
738
+ }
739
+
740
+ boundary = buffer.indexOf('\n');
741
  }
 
 
742
  }
743
  }
744
+ } else {
745
+ throw new Error(`API responded with status ${response.status}`);
746
  }
747
  }
748
+ } catch (error) {
749
+ if (typingIndicator) {
750
+ typingIndicator.remove();
 
 
 
 
 
 
 
 
 
 
 
 
 
751
  }
752
+ throw error;
 
753
  }
754
+ },
755
+
756
+ // Event listeners
757
+ setupEventListeners() {
758
+ elements.initialSendIcon.addEventListener('click', app.sendInitialMessage);
759
+ elements.initialChatInput.addEventListener('keydown', (event) => {
760
+ if (event.key === 'Enter') {
761
+ app.sendInitialMessage();
762
+ }
763
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
764
 
765
+ elements.sendButtonIcon.addEventListener('click', app.sendMessage);
766
+ elements.chatInput.addEventListener('keydown', (event) => {
767
+ if (event.key === 'Enter') {
768
+ app.sendMessage();
769
+ }
770
+ });
771
 
772
+ elements.clearChatButton.addEventListener('click', app.clearChat);
 
 
 
 
 
 
 
773
 
774
+ elements.modelSelectDisplay.addEventListener('click', () => {
775
+ elements.modelOptions.style.display = elements.modelOptions.style.display === 'block' ? 'none' : 'block';
776
+ });
 
 
 
 
 
 
777
 
778
+ elements.themeToggle.addEventListener('click', () => {
779
+ document.body.classList.toggle('light-mode', !state.isDarkMode);
780
+ state.isDarkMode = !state.isDarkMode;
781
+ });
782
+ }
783
+ };
784
 
785
+ // Initialize the app
786
+ const init = async () => {
787
+ await app.fetchModels();
788
+ app.setupEventListeners();
789
+ };
 
 
 
 
 
790
 
791
+ init();
 
 
 
 
792
  });
793
+ </script>
 
794
  </body>
795
+ </html>