danielle2003 commited on
Commit
a99b6e3
·
verified ·
1 Parent(s): a02f6e4

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +345 -416
app.py CHANGED
@@ -34,14 +34,12 @@ html_code="""
34
  <html lang="en">
35
  <head>
36
  <meta charset="UTF-8">
37
- <meta
38
- name="viewport" content="width=device-width, initial-scale=1.0">
39
  <title>LLM Studio Enhanced</title>
40
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
41
  <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
42
  <style>
43
  /* General Styles */
44
-
45
  :root {
46
  --bg-color-dark: #0D1117;
47
  --bg-color-medium: #161B22;
@@ -56,7 +54,6 @@ name="viewport" content="width=device-width, initial-scale=1.0">
56
  --groq-color: #4CAF50;
57
  --container-radius: 16px;
58
  }
59
-
60
  body {
61
  height: 100vh;
62
  width: 100%;
@@ -66,7 +63,6 @@ name="viewport" content="width=device-width, initial-scale=1.0">
66
  font-family: 'time new roman';
67
  color: var(--text-color-primary);
68
  }
69
-
70
  .studio-container {
71
  width: 100%;
72
  height: 100vh;
@@ -78,7 +74,6 @@ name="viewport" content="width=device-width, initial-scale=1.0">
78
  display: flex;
79
  box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
80
  }
81
-
82
  /* MODIFIED: Final Polished Collapsible Sidebar */
83
  .sidebar {
84
  width: 240px; /* Fixed width */
@@ -101,7 +96,6 @@ name="viewport" content="width=device-width, initial-scale=1.0">
101
  padding: 16px;
102
  min-width: 208px;
103
  }
104
-
105
  .sidebar-header {
106
  display: flex;
107
  align-items: center;
@@ -118,7 +112,6 @@ name="viewport" content="width=device-width, initial-scale=1.0">
118
  min-width: 36px;
119
  text-align: center;
120
  }
121
-
122
  .new-chat-btn {
123
  background: linear-gradient(135deg, #58a6ff, #3a8dff);
124
  border: none;
@@ -140,7 +133,6 @@ name="viewport" content="width=device-width, initial-scale=1.0">
140
  transform: translateY(-2px);
141
  box-shadow: 0 6px 20px rgba(88, 166, 255, 0.3);
142
  }
143
-
144
  .nav-menu .nav-item {
145
  display: flex;
146
  align-items: center;
@@ -153,31 +145,25 @@ name="viewport" content="width=device-width, initial-scale=1.0">
153
  font-weight: 500;
154
  color: var(--text-color-primary);
155
  }
156
-
157
- .nav-menu .nav-item:hover { background-color: var(--bg-color-medium);
158
- }
159
  .nav-menu .nav-item.active {
160
  background-color: var(--accent-color);
161
  color: var(--bg-color-dark);
162
  font-weight: 600;
163
  box-shadow: 0 0 15px var(--accent-glow);
164
  }
165
- .nav-menu .nav-item.active .material-icons { color: var(--bg-color-dark);
166
- }
167
  .nav-menu .nav-item .material-icons {
168
  color: var(--text-color-secondary);
169
  transition: color 0.3s ease;
170
  min-width: 24px;
171
  }
172
- .nav-menu .nav-item:hover .material-icons { color: var(--text-color-primary);
173
- }
174
-
175
  .item-text {
176
  opacity: 1; /* Always visible */
177
  width: auto; /* Always auto width */
178
  white-space: nowrap;
179
  }
180
-
181
  .history-section {
182
  margin-top: 24px;
183
  padding-top: 24px;
@@ -224,6 +210,9 @@ name="viewport" content="width=device-width, initial-scale=1.0">
224
  text-overflow: ellipsis;
225
  margin-left: 16px;
226
  }
 
 
 
227
  .history-item .history-title-input {
228
  width: 100%;
229
  background: var(--bg-color-dark);
@@ -249,7 +238,6 @@ name="viewport" content="width=device-width, initial-scale=1.0">
249
  .history-icon:hover {
250
  color: var(--accent-color);
251
  }
252
-
253
  .api-key-section {
254
  padding-top: 24px;
255
  margin-top: 24px;
@@ -290,7 +278,6 @@ name="viewport" content="width=device-width, initial-scale=1.0">
290
  border-color: var(--error-color) !important;
291
  box-shadow: 0 0 10px rgba(248, 81, 73, 0.5) !important;
292
  }
293
-
294
  .main-content {
295
  flex-grow: 1;
296
  display: flex;
@@ -298,14 +285,12 @@ name="viewport" content="width=device-width, initial-scale=1.0">
298
  overflow: hidden;
299
  background-image: radial-gradient(circle at 50% 0%, rgba(255, 255, 255, 0.05), transparent 40%);
300
  }
301
-
302
  .content-window {
303
  flex-grow: 1;
304
  display: flex;
305
  flex-direction: column;
306
  overflow: hidden;
307
  }
308
-
309
  .prompt-view {
310
  display: none;
311
  }
@@ -313,7 +298,6 @@ name="viewport" content="width=device-width, initial-scale=1.0">
313
  display: flex;
314
  flex-direction: column;
315
  }
316
-
317
  #home-screen {
318
  justify-content: center;
319
  align-items: center;
@@ -321,23 +305,32 @@ name="viewport" content="width=device-width, initial-scale=1.0">
321
  padding: 40px;
322
  display: none;
323
  }
324
- #home-screen.active { display: flex;
325
- }
326
- #home-screen .logo { font-size: 60px; color: var(--accent-color);
327
- }
328
- #home-screen h1 { font-size: 32px; margin: 16px 0 8px; color: var(--text-color-primary);
329
- }
330
- #home-screen p { color: var(--text-color-secondary); max-width: 450px; line-height: 1.6;
331
- }
332
-
333
  .chat-history-display {
334
  flex-grow: 1;
 
 
 
 
 
 
 
 
 
 
335
  padding: 24px;
336
  overflow-y: auto;
337
  display: flex;
338
  flex-direction: column;
339
  gap: 16px;
 
340
  }
 
341
  .chat-message-container {
342
  display: flex;
343
  flex-direction: column;
@@ -345,16 +338,12 @@ name="viewport" content="width=device-width, initial-scale=1.0">
345
  }
346
  .chat-message {
347
  display: flex;
348
- gap: 16px;
349
  align-items: flex-start;
350
  }
351
- .chat-message.user { justify-content: flex-end;
352
- }
353
- .chat-message-container.user { align-items: flex-end;
354
- }
355
- .chat-message-container.model { align-items: flex-start;
356
- }
357
-
358
  .chat-avatar {
359
  width: 36px;
360
  height: 36px;
@@ -366,15 +355,10 @@ name="viewport" content="width=device-width, initial-scale=1.0">
366
  font-weight: 600;
367
  border: 1px solid var(--border-color);
368
  }
369
- .chat-message.user .chat-avatar { background-color: var(--bg-color-medium);
370
- }
371
- .chat-message.model .chat-avatar { background-color: var(--accent-color);
372
- }
373
- .chat-message.user .chat-avatar .material-icons { color: var(--text-color-primary);
374
- }
375
- .chat-message.model .chat-avatar .material-icons { color: var(--bg-color-dark); font-size: 20px;
376
- }
377
-
378
  .chat-bubble {
379
  padding: 12px 18px;
380
  border-radius: 20px;
@@ -386,7 +370,6 @@ name="viewport" content="width=device-width, initial-scale=1.0">
386
  border: 1px solid transparent;
387
  position: relative;
388
  }
389
-
390
  .copy-icon-bubble {
391
  position: absolute;
392
  top: 8px;
@@ -406,7 +389,6 @@ name="viewport" content="width=device-width, initial-scale=1.0">
406
  .copy-icon-bubble:hover {
407
  color: var(--accent-color);
408
  }
409
-
410
  .model-response-header {
411
  font-size: 10px;
412
  font-weight: 500;
@@ -415,13 +397,12 @@ name="viewport" content="width=device-width, initial-scale=1.0">
415
  padding-bottom: 4px;
416
  margin-top:-50px;
417
  display: flex;
418
- gap: 12px;
419
  align-items: center;
420
  opacity: 0.7;
421
  border-bottom: 1px solid var(--border-color);
422
  }
423
- .model-response-header .material-icons { font-size: 12px;
424
- }
425
  .chat-message.user .chat-bubble {
426
  background: linear-gradient(135deg, #58a6ff, #3a8dff);
427
  color: #ffffff;
@@ -438,7 +419,6 @@ name="viewport" content="width=device-width, initial-scale=1.0">
438
  border-color: var(--error-color);
439
  color: var(--error-color);
440
  }
441
-
442
  .chat-bubble pre {
443
  background: var(--bg-color-dark) !important;
444
  padding: 12px;
@@ -454,7 +434,6 @@ name="viewport" content="width=device-width, initial-scale=1.0">
454
  border-radius: 8px;
455
  margin-top: 8px;
456
  }
457
-
458
  .prompt-actions-bar {
459
  display: flex;
460
  align-items: center;
@@ -478,7 +457,6 @@ name="viewport" content="width=device-width, initial-scale=1.0">
478
  .prompt-actions-bar .action-icon .material-icons {
479
  font-size: 18px;
480
  }
481
-
482
  .prompt-area {
483
  padding: 16px 24px;
484
  border-top: 1px solid var(--border-color);
@@ -500,7 +478,6 @@ name="viewport" content="width=device-width, initial-scale=1.0">
500
  cursor: pointer;
501
  transition: all 0.2s;
502
  font-size: 12px;
503
- font-family: 'Times New Roman', Times, serif;
504
  }
505
  .prompt-example-btn:hover {
506
  border-color: var(--accent-color);
@@ -518,7 +495,7 @@ name="viewport" content="width=device-width, initial-scale=1.0">
518
  border-radius: 8px;
519
  padding: 12px 16px;
520
  color: var(--text-color-primary);
521
- font-family: 'Times New Roman', Times, serif;
522
  font-size: 16px;
523
  resize: none;
524
  height: 50px;
@@ -529,7 +506,6 @@ name="viewport" content="width=device-width, initial-scale=1.0">
529
  border-color: var(--accent-color);
530
  box-shadow: 0 0 8px var(--accent-glow);
531
  }
532
-
533
  .image-uploader {
534
  width: 50px;
535
  height: 50px;
@@ -544,15 +520,10 @@ name="viewport" content="width=device-width, initial-scale=1.0">
544
  flex-shrink: 0;
545
  overflow: hidden;
546
  }
547
- .image-uploader:hover { border-color: var(--accent-color);
548
- }
549
- .image-uploader .material-icons { font-size: 24px; color: var(--text-color-secondary); transition: opacity 0.2s;
550
- }
551
- .image-uploader.loading .spinner { display: block;
552
- }
553
- .image-uploader.loading .material-icons, .image-uploader.has-image .material-icons { display: none;
554
- }
555
-
556
  .spinner {
557
  display: none;
558
  width: 24px;
@@ -568,11 +539,8 @@ name="viewport" content="width=device-width, initial-scale=1.0">
568
  height: 100%;
569
  object-fit: cover;
570
  }
571
- .image-uploader.has-image .image-preview { display: block;
572
- }
573
- @keyframes spin { to { transform: rotate(360deg);
574
- } }
575
-
576
  .right-panel {
577
  width: 250px;
578
  padding: 24px;
@@ -591,8 +559,7 @@ name="viewport" content="width=device-width, initial-scale=1.0">
591
  font-weight: 500;
592
  margin-bottom: 16px;
593
  }
594
- .right-panel ul { list-style: none;
595
- padding: 0; margin: 0; }
596
  .model-item {
597
  padding: 12px;
598
  border-radius: 6px;
@@ -608,8 +575,7 @@ name="viewport" content="width=device-width, initial-scale=1.0">
608
  position: relative;
609
  padding-left: 30px;
610
  }
611
- .model-item:hover { background-color: var(--bg-color-medium);
612
- }
613
  .model-item.active {
614
  background-color: var(--accent-color);
615
  color: #ffffff;
@@ -625,14 +591,12 @@ name="viewport" content="width=device-width, initial-scale=1.0">
625
  color: var(--groq-color);
626
  font-size: 14px;
627
  }
628
-
629
  .parameters-section {
630
  margin-top: 24px;
631
  padding-top: 24px;
632
  border-top: 1px solid var(--border-color);
633
  }
634
- .parameter-control { margin-bottom: 16px;
635
- }
636
  .parameter-control label {
637
  display: flex;
638
  justify-content: space-between;
@@ -662,7 +626,6 @@ name="viewport" content="width=device-width, initial-scale=1.0">
662
  border-radius: 50%;
663
  box-shadow: 0 0 8px var(--accent-glow);
664
  }
665
-
666
  .action-button {
667
  background-color: var(--accent-color);
668
  border: none;
@@ -678,9 +641,6 @@ name="viewport" content="width=device-width, initial-scale=1.0">
678
  height: 50px;
679
  flex-shrink: 0;
680
  }
681
- .response-content{
682
- margin-top:-40px;
683
- }
684
  .action-button:hover {
685
  background-color: #4a9eff;
686
  box-shadow: 0 0 15px var(--accent-glow);
@@ -697,38 +657,26 @@ name="viewport" content="width=device-width, initial-scale=1.0">
697
  background-color: #d9443b;
698
  box-shadow: 0 0 15px rgba(248, 81, 73, 0.5);
699
  }
700
-
701
  @media (max-width: 1100px) {
702
- .studio-container { flex-direction: column;
703
- height: auto; max-height: none; }
704
  .sidebar {
705
  width: 100%;
706
  border-right: none; border-bottom: 1px solid var(--border-color);
707
  flex-direction: row; align-items: center; justify-content: space-between; padding: 16px;
708
  overflow: visible;
709
  }
710
- .sidebar:hover { width: 100%;
711
- }
712
- .sidebar-header { border: none; margin: 0; padding: 0;
713
- }
714
- .nav-menu { display: flex; gap: 8px;
715
- }
716
- .nav-menu .nav-item { flex-direction: column; padding: 8px;
717
- gap: 4px; font-size: 12px; }
718
- .right-panel { width: 100%;
719
- border-left: none; border-top: 1px solid var(--border-color); }
720
  }
721
  @media (max-width: 768px) {
722
- body { padding: 0;
723
- }
724
- .studio-container { border-radius: 0; min-height: 100vh;
725
- }
726
- .sidebar { flex-direction: column; align-items: stretch;
727
- }
728
- .nav-menu { justify-content: center;
729
- }
730
  }
731
-
732
  </style>
733
  </head>
734
  <body>
@@ -737,337 +685,272 @@ name="viewport" content="width=device-width, initial-scale=1.0">
737
  <div class="sidebar-content">
738
  <div class="sidebar-header">
739
  <span class="material-icons logo">auto_awesome</span><h1 class="item-text">LLM Studio</h1>
740
-
741
  </div>
742
  <button class="new-chat-btn" id="new-chat-btn"><span class="material-icons">add_circle</span><span class="item-text">New Chat</span></button>
743
  <nav class="nav-menu">
744
  <div class="nav-item active" data-target="text-generation"><span class="material-icons">text_fields</span><span class="item-text">Text Generation</span></div>
745
  <div class="nav-item" data-target="image-to-text"><span class="material-icons">image</span><span class="item-text">Image to Text</span></div>
746
-
747
  <div class="nav-item" data-target="text-classification"><span class="material-icons">label</span><span class="item-text">Text Classification</span></div>
748
  </nav>
749
  <div class="history-section">
750
  <h3 class="history-title item-text">History</h3>
751
-
752
  <div class="chat-history-list" id="chat-history-list">
753
  </div>
754
  </div>
755
  </div>
756
  </aside>
757
-
758
  <main class="main-content">
759
  <div id="home-screen" class="content-window active">
760
-
761
  <span class="material-icons logo">auto_awesome</span>
762
  <h1>Welcome to LLM Studio</h1>
763
- <p>Select a mode from the left, or click "New Chat" to begin a new conversation.
764
- Your chat history will be saved here for this session.</p>
765
  </div>
766
-
767
  <div id="chat-view" class="content-window" style="display:none;">
768
  <div class="chat-history-display" id="chat-history-display"></div>
769
  <div id="prompt-container">
770
-
771
  <div id="text-generation" class="prompt-view active">
772
-
773
  <div class="prompt-area">
774
  <div class="prompt-examples" id="text-generation-examples"></div>
775
-
776
- <div class="input-wrapper">
777
-
778
  <textarea id="text-generation-prompt" placeholder="Enter your prompt here..."></textarea>
779
  <button class="action-button" data-target="text-generation"><span class="material-icons">send</span></button>
780
-
781
- </div>
782
-
783
  </div>
784
  </div>
785
-
786
  <div id="image-to-text" class="prompt-view">
787
-
788
  <div class="prompt-area">
789
-
790
  <div class="prompt-examples" id="image-to-text-examples"></div>
791
  <div class="input-wrapper">
792
-
793
- <div id="image-uploader" class="image-uploader">
794
-
795
- <input type="file" id="image-upload-input" accept="image/*" style="display: none;">
796
  <span class="material-icons">add_photo_alternate</span>
797
-
798
-
799
- <div class="spinner"></div>
800
  <img class="image-preview" id="image-preview" src="" alt="Image Preview"/>
801
  </div>
802
-
803
-
804
  <textarea id="image-to-text-prompt" placeholder="Optionally, add instructions..."></textarea>
805
  <button class="action-button" data-target="image-to-text" disabled><span class="material-icons">send</span></button>
806
- </div>
807
-
808
-
809
- </div>
810
  </div>
811
-
812
  <div id="text-classification" class="prompt-view">
813
-
814
- <div class="prompt-area">
815
-
816
  <div class="prompt-examples" id="text-classification-examples"></div>
817
  <div class="input-wrapper">
818
-
819
- <textarea id="text-classification-prompt" placeholder="Enter text to classify..."></textarea>
820
-
821
- <button class="action-button" data-target="text-classification"><span class="material-icons">send</span></button>
822
  </div>
823
- </div>
824
-
825
- </div>
826
  </div>
827
  </div>
828
  </main>
829
-
830
  <aside class="right-panel">
831
  <div>
832
-
833
  <p class="panel-header">MODELS</p>
834
-
835
  <ul id="model-list">
836
  </ul>
837
  </div>
838
  <div class="parameters-section">
839
-
840
  <p class="panel-header">PARAMETERS</p>
841
-
842
- <div class="parameter-control">
843
  <label for="temperature"><span>Temperature</span><span id="temperature-value">0.9</span></label>
844
  <input type="range" id="temperature" min="0" max="1" step="0.1" value="0.9">
845
- </div>
846
-
847
  <div class="parameter-control">
848
-
849
  <label for="top-p"><span>Top-P</span><span id="top-p-value">1.0</span></label>
850
  <input type="range" id="top-p" min="0" max="1" step="0.1" value="1.0">
851
  </div>
852
  </div>
853
-
854
- <div
855
- class="api-key-section">
856
-
857
  <div class="api-key-group">
858
  <label for="groq-token">Groq API Key</label>
859
- <input type="password" id="groq-token" placeholder="gsk_...">
860
  </div>
861
-
862
- <div class="api-key-group">
863
-
864
  <label for="hf-token">Hugging Face Token</label>
865
- <input type="password" id="hf-token" placeholder="hf_...">
866
  </div>
867
-
868
- </div>
869
  </aside>
870
  </div>
871
 
872
  <script>
873
  document.addEventListener('DOMContentLoaded', () => {
874
-
875
- const navItems = document.querySelectorAll('.nav-item');
876
- const promptViews = document.querySelectorAll('.prompt-view');
877
  const modelList = document.getElementById('model-list');
878
  const newChatBtn = document.getElementById('new-chat-btn');
879
  const chatHistoryList = document.getElementById('chat-history-list');
880
  const chatHistoryDisplay = document.getElementById('chat-history-display');
881
- const homeScreen = document.getElementById('home-screen');
882
  const chatView = document.getElementById('chat-view');
883
  const tempSlider = document.getElementById('temperature');
884
  const tempValue = document.getElementById('temperature-value');
885
  const topPSlider = document.getElementById('top-p');
886
- const topPValue = document.getElementById('top-p-value');
887
  const hfTokenInput = document.getElementById('hf-token');
888
  const groqTokenInput = document.getElementById('groq-token');
889
  const imageUploader = document.getElementById('image-uploader');
890
  const imagePreview = document.getElementById('image-preview');
891
- let activeMode = 'text-generation';
892
  let activeModelInfo = {};
893
  let temperature = 0.9;
894
  let topP = 1.0;
895
  let uploadedImageBase64 = null;
896
- let uploadedImageDataUrl = null;
897
  let chatSessions = {};
898
  let activeChatId = null;
899
  let editingMessageId = null;
900
  let currentRequestController = null;
901
- let currentTypingInterval = null;
902
-
903
  const taskConfig = {
904
  'text-generation': {
905
  examples: [
906
- { title: "Explain quantum computing", prompt: "Explain quantum computing in simple terms."
907
- },
908
- { title: "Python factorial function", prompt: "Write a Python function that calculates the factorial of a number."
909
- },
910
- { title: "Email to boss", prompt: "Write a short, professional email to my boss requesting a meeting next week."
911
- }
912
  ],
913
  models: [
914
  { id: "llama3-8b-8192", name: "Llama3 8B (Groq)", url: "https://api.groq.com/openai/v1/chat/completions", type: "chat", isGroq: true },
915
  { id: "qwen/qwen3-32b", name: "qwen/qwen3-32b (Groq)", url: "https://api.groq.com/openai/v1/chat/completions", type: "chat", isGroq: true },
916
- { id:
917
-
918
- "gemma2-9b-it", name: "gemma2-9b-it (Groq)", url: "https://api.groq.com/openai/v1/chat/completions", type: "chat", isGroq: true },
919
  { id: "deepseek-ai/DeepSeek-V3", name: "DeepSeek-V3 (HF)", url: "https://router.huggingface.co/nebius/v1/chat/completions", type: "chat" },
920
  ]
921
  },
922
  'image-to-text': {
923
  examples: [
924
-
925
- { title: "Describe scene", prompt:
926
- "Describe this scene in detail."
927
- },
928
- { title: "Identify objects", prompt: "List all the objects you can identify."
929
- },
930
- { title: "Suggest a caption", prompt: "Suggest a creative social media caption."
931
- }
932
  ],
933
  models: [
934
  { id: "meta-llama/llama-4-scout-17b-16e-instruct", name: "meta-llama/llama-4-scout (Groq)", url: "https://api.groq.com/openai/v1/chat/completions", type: "image-to-text",isGroq: true },
935
  { id: "meta-llama/llama-4-maverick-17b-128e-instruct", name: "meta-llama/llama-4-maverick (Groq)", url: "https://api.groq.com/openai/v1/chat/completions", type: "image-to-text",isGroq: true }
936
  ]
937
-
938
-
939
  },
940
  'text-classification': {
941
  examples: [
942
- { title: "Sentiment analysis", prompt: "I'm so frustrated with customer service!"
943
- },
944
- { title: "Topic identification", prompt: "The new legislation aims to reduce carbon emissions."
945
- },
946
- { title: "Spam detection", prompt: "CLICK HERE to claim your prize!"
947
- }
948
  ],
949
  models: [
950
  { id: "distilbert-base-uncased-finetuned-sst-2-english", name: "DistilBERT SST-2", url: "https://router.huggingface.co/hf-inference/models/distilbert/distilbert-base-uncased-finetuned-sst-2-english", type: "text-classification" },
951
  { id: "SamLowe/roberta-base-go_emotions", name: "RoBERTa GoEmotions", url: "https://api-inference.huggingface.co/models/SamLowe/roberta-base-go_emotions", type: "text-classification" },
952
- { id: "nlptown/bert-base-multilingual-uncased-sentiment", name: "BERT Multilingual", url:
953
-
954
- "https://api-inference.huggingface.co/models/nlptown/bert-base-multilingual-uncased-sentiment", type: "text-classification" }
955
  ]
956
  }
957
  };
958
- function init() {
 
959
  populatePromptExamples();
960
  setupListeners();
961
  switchMode(activeMode);
962
  updateAllSendButtonStates();
963
- }
964
 
965
  function populatePromptExamples() {
966
  for (const mode in taskConfig) {
967
  const containerId = `${mode}-examples`;
968
- const container = document.getElementById(containerId);
969
  if(container) {
970
  container.innerHTML = '';
971
- taskConfig[mode].examples.forEach(ex => {
972
  const btn = document.createElement('button');
973
  btn.className = 'prompt-example-btn';
974
  btn.textContent = ex.title;
975
  btn.onclick = () => {
976
-
977
-
978
  const promptTextarea = document.getElementById(`${mode}-prompt`);
979
  promptTextarea.value = ex.prompt;
980
  promptTextarea.focus();
981
-
982
- autoGrowTextarea(promptTextarea);
983
  updateSendButtonState(mode);
984
-
985
  };
986
  container.appendChild(btn);
987
  });
988
- }
989
  }
990
  }
991
 
992
  function setupListeners() {
993
  navItems.forEach(item => item.addEventListener('click', () => switchMode(item.dataset.target)));
994
- newChatBtn.addEventListener('click', startNewChat);
995
-
996
  tempSlider.addEventListener('input', (e) => {
997
  temperature = parseFloat(e.target.value);
998
  tempValue.textContent = temperature.toFixed(1);
999
  });
1000
- topPSlider.addEventListener('input', (e) => {
1001
  topP = parseFloat(e.target.value);
1002
  topPValue.textContent = topP.toFixed(1);
1003
  });
1004
- document.querySelectorAll('textarea').forEach(textarea => {
1005
  textarea.addEventListener('input', () => {
1006
  autoGrowTextarea(textarea);
1007
  const mode = textarea.id.replace('-prompt', '');
1008
  updateSendButtonState(mode);
1009
  });
1010
  });
1011
- [hfTokenInput, groqTokenInput].forEach(input => {
1012
  input.addEventListener('input', () => input.classList.remove('input-error'));
1013
  });
1014
- setupImageUploader();
1015
  setupActionButtons();
1016
  }
1017
 
1018
  function updateSendButtonState(mode) {
1019
  if (mode === 'image-to-text') return;
1020
- const promptTextarea = document.getElementById(`${mode}-prompt`);
1021
  const sendButton = document.querySelector(`#${mode} .action-button`);
1022
  if (promptTextarea && sendButton) {
1023
  sendButton.disabled = promptTextarea.value.trim() === '';
1024
- }
1025
  }
 
1026
  function updateAllSendButtonStates() {
1027
  ['text-generation', 'text-classification'].forEach(updateSendButtonState);
1028
- }
1029
 
1030
  function setupImageUploader() {
1031
  const uploaderInput = document.getElementById('image-upload-input');
1032
- imageUploader.addEventListener('click', () => uploaderInput.click());
1033
  imageUploader.addEventListener('dragover', (e) => { e.preventDefault(); imageUploader.style.borderColor = 'var(--accent-color)'; });
1034
- imageUploader.addEventListener('dragleave', (e) => { imageUploader.style.borderColor = 'var(--border-color)'; });
1035
  imageUploader.addEventListener('drop', (e) => {
1036
  e.preventDefault();
1037
  imageUploader.style.borderColor = 'var(--border-color)';
1038
  if (e.dataTransfer.files.length > 0) handleImageFile(e.dataTransfer.files[0]);
1039
  });
1040
- uploaderInput.addEventListener('change', (e) => {
1041
  if (e.target.files.length > 0) handleImageFile(e.target.files[0]);
1042
  });
1043
- }
1044
 
1045
  function autoGrowTextarea(element) {
1046
  element.style.height = 'auto';
1047
- element.style.height = (element.scrollHeight) + 'px';
1048
  }
1049
 
1050
  function switchMode(targetMode) {
1051
  activeMode = targetMode;
1052
- navItems.forEach(i => i.classList.toggle('active', i.dataset.target === targetMode));
1053
  promptViews.forEach(v => {
1054
  v.classList.toggle('active', v.id === targetMode)
1055
  v.style.display = v.id === targetMode ? 'flex' : 'none';
1056
  });
1057
- renderModelList(targetMode);
1058
 
1059
  if (activeChatId) {
1060
  chatSessions[activeChatId].mode = targetMode;
1061
- }
1062
  }
1063
 
1064
  function renderModelList(mode) {
1065
  modelList.innerHTML = '';
1066
- const models = taskConfig[mode].models;
1067
  if (!models || models.length === 0) return;
1068
 
1069
  setActiveModel(models[0]);
1070
- models.forEach(model => {
1071
  const item = document.createElement('li');
1072
  item.className = 'model-item';
1073
  item.textContent = model.name;
@@ -1075,34 +958,30 @@ models.forEach(model => {
1075
  item.dataset.modelId = model.id;
1076
 
1077
  if (model.isGroq) {
1078
-
1079
-
1080
- item.classList.add('groq-model');
1081
  }
1082
  if (model.id === activeModelInfo.id) {
1083
  item.classList.add('active');
1084
  }
1085
 
1086
- item.onclick
1087
- = () => {
1088
-
1089
- setActiveModel(model);
1090
  document.querySelectorAll('.model-item').forEach(i => i.classList.remove('active'));
1091
  item.classList.add('active');
1092
  };
1093
 
1094
  modelList.appendChild(item);
1095
-
1096
- });
1097
  }
1098
 
1099
  function setActiveModel(model) {
1100
  activeModelInfo = model;
1101
- }
1102
 
 
1103
  function startNewChat() {
1104
  editingMessageId = null;
1105
- const newChatId = `chat_${Date.now()}`;
1106
  activeChatId = newChatId;
1107
 
1108
  chatSessions[newChatId] = {
@@ -1111,7 +990,17 @@ const newChatId = `chat_${Date.now()}`;
1111
  mode: activeMode,
1112
  messages: []
1113
  };
1114
- chatHistoryDisplay.innerHTML = '';
 
 
 
 
 
 
 
 
 
 
1115
  renderChatHistory();
1116
  homeScreen.classList.remove('active');
1117
  homeScreen.style.display = 'none';
@@ -1119,18 +1008,35 @@ chatHistoryDisplay.innerHTML = '';
1119
  switchMode(activeMode);
1120
  resetImageUploader();
1121
  updateAllSendButtonStates();
1122
- }
1123
 
 
1124
  function loadChat(chatId) {
1125
  editingMessageId = null;
1126
- activeChatId = chatId;
1127
  const chat = chatSessions[chatId];
1128
 
1129
- chatHistoryDisplay.innerHTML = '';
1130
- chat.messages.forEach(msg => {
1131
- displayMessage(msg);
1132
- });
1133
- homeScreen.classList.remove('active');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1134
  homeScreen.style.display = 'none';
1135
  chatView.style.display = 'flex';
1136
  switchMode(chat.mode);
@@ -1139,58 +1045,59 @@ homeScreen.classList.remove('active');
1139
 
1140
  function renderChatHistory() {
1141
  chatHistoryList.innerHTML = '';
1142
- Object.values(chatSessions).reverse().forEach(chat => {
1143
  const item = document.createElement('div');
1144
  item.className = 'history-item';
1145
  item.dataset.chatId = chat.id;
1146
 
1147
  item.innerHTML = `
1148
  <span class="material-icons">chat_bubble</span>
1149
-
1150
- <span class="history-title-text item-text">${chat.title}</span>
1151
  <div class="history-actions">
1152
  <span class="material-icons history-icon" onclick="editChatTitle('${chat.id}', event)">edit</span>
1153
  <span class="material-icons history-icon" onclick="deleteChat('${chat.id}', event)">delete</span>
1154
  </div>
1155
  `;
1156
-
1157
-
1158
  if(chat.id === activeChatId) {
1159
  item.classList.add('active');
1160
-
1161
- }
1162
  item.addEventListener('click', (e) => {
1163
  if (e.target.classList.contains('history-icon')) return;
1164
-
1165
- loadChat(chat.id)
1166
  });
1167
- chatHistoryList.appendChild(item);
1168
  });
1169
  }
1170
 
1171
  window.deleteChat = (chatId, event) => {
1172
  event.stopPropagation();
1173
- if (confirm(`Are you sure you want to delete this chat?`)) {
1174
  delete chatSessions[chatId];
1175
- if (activeChatId === chatId) {
 
 
 
 
 
1176
  activeChatId = null;
1177
- chatView.style.display = 'none';
1178
  homeScreen.style.display = 'flex';
1179
  homeScreen.classList.add('active');
1180
  }
1181
  renderChatHistory();
1182
- }
1183
  };
1184
 
1185
  window.editChatTitle = (chatId, event) => {
1186
  event.stopPropagation();
1187
- const item = event.target.closest('.history-item');
1188
  const titleSpan = item.querySelector('.history-title-text');
1189
  const currentTitle = titleSpan.textContent;
1190
 
1191
  const input = document.createElement('input');
1192
  input.type = 'text';
1193
- input.className = 'history-title-input';
1194
  input.value = currentTitle;
1195
 
1196
  titleSpan.replaceWith(input);
@@ -1198,119 +1105,132 @@ input.className = 'history-title-input';
1198
 
1199
  const saveTitle = () => {
1200
  const newTitle = input.value.trim();
1201
- if (newTitle && newTitle !== currentTitle) {
1202
  chatSessions[chatId].title = newTitle;
1203
- }
1204
  renderChatHistory();
1205
  };
1206
 
1207
  input.addEventListener('blur', saveTitle);
1208
- input.addEventListener('keydown', (e) => {
1209
  if (e.key === 'Enter') saveTitle();
1210
  else if (e.key === 'Escape') renderChatHistory();
1211
  });
1212
- };
1213
 
1214
  function handleImageFile(file) {
1215
  if (!file.type.startsWith('image/')) return;
1216
 
1217
  imageUploader.classList.remove('has-image');
1218
  imageUploader.classList.add('loading');
1219
- const reader = new FileReader();
1220
  reader.onload = (e) => {
1221
  uploadedImageDataUrl = e.target.result;
1222
- uploadedImageBase64 = uploadedImageDataUrl.split(',')[1];
1223
 
1224
  imagePreview.src = uploadedImageDataUrl;
1225
  imageUploader.classList.remove('loading');
1226
  imageUploader.classList.add('has-image');
1227
  document.querySelector('#image-to-text .action-button').disabled = false;
1228
- }
1229
  reader.readAsDataURL(file);
1230
  }
1231
 
1232
  function resetImageUploader() {
1233
  imageUploader.classList.remove('has-image', 'loading');
1234
- imagePreview.src = '';
1235
  uploadedImageDataUrl = null;
1236
  uploadedImageBase64 = null;
1237
  document.querySelector('#image-to-text .action-button').disabled = true;
1238
- }
1239
 
1240
  function addMessageToSession(sender, content, options = {}) {
1241
  if (!activeChatId) return null;
1242
- const messageId = `msg_${Date.now()}`;
1243
  const message = {
1244
  id: messageId,
1245
  sender,
1246
  content,
1247
- isHtml: options.isHtml ||
1248
- false,
1249
- isError: options.isError ||
1250
- false,
1251
  };
1252
  chatSessions[activeChatId].messages.push(message);
1253
  return messageId;
1254
- }
1255
 
1256
- function displayMessage(message, isTyping = false) {
 
1257
  const messageWrapper = document.createElement('div');
1258
- messageWrapper.className = `chat-message-container ${message.sender}`;
1259
- messageWrapper.id = message.id;
1260
 
1261
  const messageDiv = document.createElement('div');
1262
- messageDiv.className = `chat-message ${message.sender}`;
1263
- if(message.isError) { messageDiv.classList.add('error');
1264
- }
1265
-
1266
- const avatar = `<div class="chat-avatar"><span class="material-icons">${message.sender === 'user' ? 'person' : 'auto_awesome'}</span></div>`;
1267
- const bubble = document.createElement('div');
1268
- bubble.className = 'chat-bubble';
1269
  if (message.isHtml) {
1270
  bubble.innerHTML = message.content;
1271
- } else {
1272
  bubble.textContent = message.content;
1273
- }
1274
 
1275
  if (message.sender === 'model' && !isTyping && !message.isError) {
1276
  bubble.innerHTML += `<span class="material-icons copy-icon-bubble" onclick="copyResponse(event)" title="Copy response">content_copy</span>`;
1277
- }
1278
 
1279
  if (message.sender === 'user') {
1280
  messageDiv.innerHTML = bubble.outerHTML + avatar;
1281
- } else {
1282
  messageDiv.innerHTML = avatar + bubble.outerHTML;
1283
- }
1284
 
1285
  messageWrapper.appendChild(messageDiv);
1286
- if (message.sender === 'user' && !isTyping) {
 
 
1287
  const actionBar = document.createElement('div');
1288
- actionBar.className = 'prompt-actions-bar';
1289
  actionBar.innerHTML = `
1290
  <span class="action-icon" onclick="editMessage('${message.id}')" title="Edit prompt">
1291
  <span class="material-icons">edit</span>
1292
  </span>
1293
- <span class="action-icon" onclick="resendMessage('${message.id}')" title="Resend prompt">
1294
-
1295
- <span class="material-icons">replay</span>
1296
- </span>
1297
  `;
1298
- messageWrapper.appendChild(actionBar);
1299
  }
 
 
1300
 
1301
- chatHistoryDisplay.appendChild(messageWrapper);
1302
- chatHistoryDisplay.scrollTop = chatHistoryDisplay.scrollHeight;
 
 
 
 
 
 
 
 
 
 
 
1303
  return messageWrapper;
1304
- }
1305
 
1306
  function streamResponse(element, text, mode) {
1307
  let i = 0;
1308
- element.innerHTML = '';
1309
 
1310
  currentRequestController = new AbortController();
1311
  const signal = currentRequestController.signal;
1312
  toggleSendStopButton(true, true);
1313
- currentTypingInterval = setInterval(() => {
 
 
 
1314
  if (signal.aborted) {
1315
  clearInterval(currentTypingInterval);
1316
  currentTypingInterval = null;
@@ -1319,16 +1239,16 @@ currentTypingInterval = setInterval(() => {
1319
  if (mode === 'image-to-text') {
1320
  resetImageUploader();
1321
  }
1322
- return;
1323
  }
1324
 
1325
  if (i < text.length) {
1326
  element.innerHTML += text.charAt(i);
1327
  i++;
1328
- chatHistoryDisplay.scrollTop = chatHistoryDisplay.scrollHeight;
1329
- }
1330
-
1331
- else {
1332
  clearInterval(currentTypingInterval);
1333
  currentTypingInterval = null;
1334
  toggleSendStopButton(false);
@@ -1337,72 +1257,68 @@ currentTypingInterval = setInterval(() => {
1337
  }
1338
  }
1339
  }, 10);
1340
- }
1341
 
1342
  window.editMessage = (messageId) => {
1343
  if (!messageId) return;
1344
- const chat = chatSessions[activeChatId];
1345
  if (!chat) return;
1346
  const message = chat.messages.find(m => m.id === messageId);
1347
  if (!message) return;
1348
- const promptTextarea = document.getElementById(`${activeMode}-prompt`);
1349
 
1350
  const tempDiv = document.createElement('div');
1351
  tempDiv.innerHTML = message.content;
1352
  promptTextarea.value = tempDiv.textContent || "";
1353
  promptTextarea.focus();
1354
  updateSendButtonState(activeMode);
1355
- editingMessageId = messageId;
1356
  };
1357
 
1358
- // MODIFIED: Resend now redisplays the prompt first
1359
  window.resendMessage = (messageId) => {
1360
  if (!messageId) return;
1361
- const chat = chatSessions[activeChatId];
1362
  if (!chat) return;
1363
  const message = chat.messages.find(m => m.id === messageId);
1364
  if (!message) return;
1365
- // Immediately display the user message again
1366
  displayMessage(message);
1367
 
1368
  const tempDiv = document.createElement('div');
1369
- tempDiv.innerHTML = message.content;
1370
  handleAction(null, tempDiv.textContent || "");
1371
  };
1372
 
1373
  window.copyResponse = (event) => {
1374
  const icon = event.target;
1375
- const bubble = icon.closest('.chat-bubble');
1376
  if (!bubble) return;
1377
 
1378
  const contentClone = bubble.cloneNode(true);
1379
  contentClone.querySelector('.copy-icon-bubble').remove();
1380
  const textToCopy = contentClone.querySelector('.response-content')?.textContent || contentClone.textContent;
1381
- navigator.clipboard.writeText(textToCopy).then(() => {
1382
  icon.textContent = 'check';
1383
  setTimeout(() => { icon.textContent = 'content_copy'; }, 1500);
1384
  }).catch(err => console.error('Failed to copy: ', err));
1385
- };
1386
 
1387
  function createModelResponseHeader() {
1388
  return `
1389
  <div class="model-response-header">
1390
  <span class="material-icons">memory</span>
1391
  <span>${activeModelInfo.name}</span> &nbsp;
1392
- | &nbsp;
1393
- <span>Temp: ${temperature.toFixed(1)}</span> &nbsp; | &nbsp;
1394
  <span>Top-P: ${topP.toFixed(1)}</span>
1395
  </div>
1396
  `;
1397
- }
1398
 
1399
  function setupActionButtons() {
1400
  document.querySelectorAll('.action-button').forEach(btn => {
1401
  btn.onclick = handleAction;
1402
  });
1403
- }
1404
 
1405
- // MODIFIED: Final stop button logic
1406
  function toggleSendStopButton(isGenerating, isTyping = false) {
1407
  document.querySelectorAll('.action-button').forEach(btn => {
1408
  const currentTextarea = document.getElementById(`${btn.dataset.target}-prompt`);
@@ -1410,183 +1326,196 @@ navigator.clipboard.writeText(textToCopy).then(() => {
1410
  if (isGenerating || isTyping) {
1411
  btn.innerHTML = `<span class="material-icons">stop_circle</span>`;
1412
  btn.classList.add('stop-generating');
1413
-
1414
- btn.disabled = false;
1415
  if(currentTextarea) currentTextarea.disabled = true;
1416
 
1417
  btn.onclick = () => {
1418
  if (currentRequestController) currentRequestController.abort();
1419
  if(currentTypingInterval) clearInterval(currentTypingInterval);
1420
-
1421
- toggleSendStopButton(false); // Revert button state on click
1422
  };
1423
  } else {
1424
  btn.innerHTML = `<span class="material-icons">send</span>`;
1425
  btn.classList.remove('stop-generating');
1426
- if(currentTextarea) currentTextarea.disabled = false;
1427
  updateSendButtonState(btn.dataset.target);
1428
  btn.onclick = handleAction;
1429
  }
1430
  });
1431
- }
 
 
 
 
 
 
 
 
 
 
 
 
1432
 
1433
  async function handleAction(e, overridePrompt = null) {
1434
  if(e) e.preventDefault();
1435
- const targetMode = e ? e.target.closest('.action-button').dataset.target : activeMode;
1436
 
1437
- if (!activeChatId) { startNewChat();
1438
- }
 
1439
 
1440
  const isGroqModel = activeModelInfo.isGroq;
1441
  const apiKeyInput = isGroqModel ? groqTokenInput : hfTokenInput;
1442
- const apiKey = apiKeyInput.value.trim();
1443
  const apiKeyName = isGroqModel ? "Groq API Key" : "Hugging Face API Token";
1444
- if (!apiKey) {
 
1445
  const errorMsg = { id: `err_${Date.now()}`, sender: 'model', content: `Please enter your ${apiKeyName}.`, isError: true };
1446
- displayMessage(errorMsg);
1447
  apiKeyInput.classList.add('input-error');
1448
  return;
1449
  }
1450
 
1451
  const promptTextarea = document.getElementById(`${targetMode}-prompt`);
1452
- let prompt = overridePrompt !== null ?
1453
- overridePrompt : promptTextarea?.value.trim() || '';
1454
- if (targetMode === 'image-to-text' && !uploadedImageBase64 && !editingMessageId) {
1455
  const errorMsg = { id: `err_${Date.now()}`, sender: 'model', content: 'Please upload an image first.', isError: true };
1456
- displayMessage(errorMsg);
1457
  return;
1458
  }
1459
  if (!prompt && targetMode !== 'image-to-text' && overridePrompt === null) return;
1460
- let userMessageContent = prompt;
 
1461
  if (targetMode === 'image-to-text') {
1462
- userMessageContent = `<img src="${uploadedImageDataUrl}" alt="Uploaded preview" style="max-height: 150px; border-radius: 8px; margin-bottom: 8px;">${prompt ?
1463
- `<br>${prompt}`: ''}`;
1464
  }
1465
 
1466
- // MODIFIED: Avoid re-displaying prompt on resend
1467
  if (overridePrompt === null) {
1468
  let userMessageId;
1469
- if (editingMessageId) {
1470
  const messageToUpdate = chatSessions[activeChatId].messages.find(m => m.id === editingMessageId);
1471
- if (messageToUpdate) {
1472
  messageToUpdate.content = userMessageContent;
1473
- document.getElementById(editingMessageId).querySelector('.chat-bubble').innerHTML = userMessageContent;
 
 
 
 
1474
  userMessageId = editingMessageId;
1475
  }
1476
  } else {
1477
  userMessageId = addMessageToSession('user', userMessageContent, { isHtml: true });
1478
- const userMessage = chatSessions[activeChatId].messages.find(m => m.id === userMessageId);
1479
  displayMessage(userMessage);
1480
  }
1481
  }
1482
 
1483
  if (chatSessions[activeChatId].messages.length <= 2 && prompt && !editingMessageId) {
1484
  chatSessions[activeChatId].title = prompt.substring(0, 25) + (prompt.length > 25 ? '...' : '');
1485
- renderChatHistory();
1486
  }
1487
 
1488
- if(promptTextarea) { promptTextarea.value = ''; autoGrowTextarea(promptTextarea);
1489
- }
1490
  updateAllSendButtonStates();
1491
 
1492
  currentRequestController = new AbortController();
1493
  toggleSendStopButton(true);
1494
- const typingMsg = { id: `typing_${Date.now()}`, sender: 'model', content: '...' };
1495
  const typingIndicator = displayMessage(typingMsg, true);
1496
 
1497
  let requestBody;
1498
- let headers = { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' };
1499
- if (targetMode === 'image-to-text') {
1500
- if (isGroqModel) {
1501
- const payload = {
1502
- model: activeModelInfo.id,
1503
- messages: [ { role: "user", content: [ { type: "text", text: prompt }, { type: "image_url", image_url: { url: uploadedImageDataUrl } } ] } ],
1504
-
1505
- temperature, max_tokens: 1024, stream: false
1506
- };
1507
- requestBody = JSON.stringify(payload);
 
 
 
 
 
1508
  } else {
1509
- const res = await fetch(uploadedImageDataUrl, { signal: currentRequestController.signal });
1510
- requestBody = await res.blob();
1511
- headers['Content-Type'] = requestBody.type;
1512
- }
1513
- } else {
1514
  let payload;
1515
- if (isGroqModel) {
1516
  payload = { model: activeModelInfo.id, messages: [{"role": "user", "content": prompt}], temperature, top_p: topP, max_tokens: 1024, stream: false };
1517
- } else if (targetMode === 'text-generation') {
1518
  payload = { model: activeModelInfo.id, messages: [{"role": "user", "content": prompt}], temperature, top_p: topP };
1519
- } else {
1520
  payload = { inputs: prompt };
1521
- }
1522
  requestBody = JSON.stringify(payload);
1523
- }
1524
 
1525
  try {
1526
  const response = await fetch(activeModelInfo.url, { method: 'POST', headers: headers, body: requestBody, signal: currentRequestController.signal });
1527
- const resultText = await response.text();
1528
 
1529
  if (!response.ok) {
1530
  try {
1531
  const errorJson = JSON.parse(resultText);
1532
- throw new Error(errorJson.error?.message || errorJson.error || resultText);
1533
- } catch(e) { throw new Error(resultText);
1534
- }
1535
  }
1536
 
1537
  let responseContent = '';
1538
- let responseHtml = false;
1539
  const finalResponse = JSON.parse(resultText);
1540
 
1541
  switch (targetMode) {
1542
  case 'text-generation':
1543
- responseContent = finalResponse.choices?.[0]?.message?.content ||
1544
- '[No content]';
1545
  break;
1546
  case 'image-to-text':
1547
  if (isGroqModel) {
1548
- responseContent = finalResponse.choices?.[0]?.message?.content ||
1549
- '[No image caption]';
1550
  } else {
1551
- responseContent = finalResponse?.[0]?.generated_text ||
1552
- '[No image caption]';
1553
  }
1554
  break;
1555
- case 'text-classification':
1556
- responseContent = `<pre style="background:var(--bg-color-dark); padding: 12px;margin-top:-20px; border-radius: 8px;">${JSON.stringify(finalResponse, null, 2)}</pre>`;
1557
- responseHtml = true;
1558
  break;
1559
  }
1560
  const responseHeader = createModelResponseHeader();
1561
- const fullResponseHtml = responseHeader + `<div class="response-content"></div>`;
1562
 
1563
- typingIndicator.remove();
 
1564
  const modelMessageId = addMessageToSession('model', fullResponseHtml, { isHtml: true });
1565
- const finalMessage = chatSessions[activeChatId].messages.find(m => m.id === modelMessageId);
1566
  const finalMessageEl = displayMessage(finalMessage);
1567
 
1568
- const responseContentEl = finalMessageEl.querySelector('.response-content');
1569
- if (responseHtml) {
1570
- responseContentEl.innerHTML = responseContent;
1571
- toggleSendStopButton(false);
1572
- } else {
1573
- streamResponse(responseContentEl, responseContent, targetMode);
1574
- }
 
 
1575
  } catch (error) {
1576
- let errorMessage = `Error: ${error.message}`;
1577
- if (error.name === 'AbortError') {
1578
- errorMessage = 'Response generation stopped by user.';
1579
- }
1580
- typingIndicator.querySelector('.chat-bubble').textContent = errorMessage;
1581
- typingIndicator.classList.add('error');
1582
- addMessageToSession('model', errorMessage, { isError: true });
 
 
1583
  toggleSendStopButton(false);
1584
  } finally {
1585
- if (targetMode === 'image-to-text') { resetImageUploader();
1586
- }
1587
  editingMessageId = null;
1588
  currentRequestController = null;
1589
- }
1590
  }
1591
 
1592
  init();
 
34
  <html lang="en">
35
  <head>
36
  <meta charset="UTF-8">
37
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
 
38
  <title>LLM Studio Enhanced</title>
39
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
40
  <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
41
  <style>
42
  /* General Styles */
 
43
  :root {
44
  --bg-color-dark: #0D1117;
45
  --bg-color-medium: #161B22;
 
54
  --groq-color: #4CAF50;
55
  --container-radius: 16px;
56
  }
 
57
  body {
58
  height: 100vh;
59
  width: 100%;
 
63
  font-family: 'time new roman';
64
  color: var(--text-color-primary);
65
  }
 
66
  .studio-container {
67
  width: 100%;
68
  height: 100vh;
 
74
  display: flex;
75
  box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
76
  }
 
77
  /* MODIFIED: Final Polished Collapsible Sidebar */
78
  .sidebar {
79
  width: 240px; /* Fixed width */
 
96
  padding: 16px;
97
  min-width: 208px;
98
  }
 
99
  .sidebar-header {
100
  display: flex;
101
  align-items: center;
 
112
  min-width: 36px;
113
  text-align: center;
114
  }
 
115
  .new-chat-btn {
116
  background: linear-gradient(135deg, #58a6ff, #3a8dff);
117
  border: none;
 
133
  transform: translateY(-2px);
134
  box-shadow: 0 6px 20px rgba(88, 166, 255, 0.3);
135
  }
 
136
  .nav-menu .nav-item {
137
  display: flex;
138
  align-items: center;
 
145
  font-weight: 500;
146
  color: var(--text-color-primary);
147
  }
148
+ .nav-menu .nav-item:hover { background-color: var(--bg-color-medium); }
 
 
149
  .nav-menu .nav-item.active {
150
  background-color: var(--accent-color);
151
  color: var(--bg-color-dark);
152
  font-weight: 600;
153
  box-shadow: 0 0 15px var(--accent-glow);
154
  }
155
+ .nav-menu .nav-item.active .material-icons { color: var(--bg-color-dark); }
 
156
  .nav-menu .nav-item .material-icons {
157
  color: var(--text-color-secondary);
158
  transition: color 0.3s ease;
159
  min-width: 24px;
160
  }
161
+ .nav-menu .nav-item:hover .material-icons { color: var(--text-color-primary); }
 
 
162
  .item-text {
163
  opacity: 1; /* Always visible */
164
  width: auto; /* Always auto width */
165
  white-space: nowrap;
166
  }
 
167
  .history-section {
168
  margin-top: 24px;
169
  padding-top: 24px;
 
210
  text-overflow: ellipsis;
211
  margin-left: 16px;
212
  }
213
+ .response-content{
214
+ margin-top:-40px;
215
+ }
216
  .history-item .history-title-input {
217
  width: 100%;
218
  background: var(--bg-color-dark);
 
238
  .history-icon:hover {
239
  color: var(--accent-color);
240
  }
 
241
  .api-key-section {
242
  padding-top: 24px;
243
  margin-top: 24px;
 
278
  border-color: var(--error-color) !important;
279
  box-shadow: 0 0 10px rgba(248, 81, 73, 0.5) !important;
280
  }
 
281
  .main-content {
282
  flex-grow: 1;
283
  display: flex;
 
285
  overflow: hidden;
286
  background-image: radial-gradient(circle at 50% 0%, rgba(255, 255, 255, 0.05), transparent 40%);
287
  }
 
288
  .content-window {
289
  flex-grow: 1;
290
  display: flex;
291
  flex-direction: column;
292
  overflow: hidden;
293
  }
 
294
  .prompt-view {
295
  display: none;
296
  }
 
298
  display: flex;
299
  flex-direction: column;
300
  }
 
301
  #home-screen {
302
  justify-content: center;
303
  align-items: center;
 
305
  padding: 40px;
306
  display: none;
307
  }
308
+ #home-screen.active { display: flex; }
309
+ #home-screen .logo { font-size: 60px; color: var(--accent-color); }
310
+ #home-screen h1 { font-size: 32px; margin: 16px 0 8px; color: var(--text-color-primary); }
311
+ #home-screen p { color: var(--text-color-secondary); max-width: 450px; line-height: 1.6; }
312
+
313
+ /* MODIFIED: chat-history-display is now a simple container */
 
 
 
314
  .chat-history-display {
315
  flex-grow: 1;
316
+ overflow: hidden;
317
+ position: relative;
318
+ }
319
+ /* ADDED: New class for individual chat session containers */
320
+ .chat-session-container {
321
+ position: absolute;
322
+ top: 0;
323
+ left: 0;
324
+ width: 100%;
325
+ height: 100%;
326
  padding: 24px;
327
  overflow-y: auto;
328
  display: flex;
329
  flex-direction: column;
330
  gap: 16px;
331
+ box-sizing: border-box;
332
  }
333
+
334
  .chat-message-container {
335
  display: flex;
336
  flex-direction: column;
 
338
  }
339
  .chat-message {
340
  display: flex;
341
+ gap: 6px;
342
  align-items: flex-start;
343
  }
344
+ .chat-message.user { justify-content: flex-end; }
345
+ .chat-message-container.user { align-items: flex-end; }
346
+ .chat-message-container.model { align-items: flex-start; }
 
 
 
 
347
  .chat-avatar {
348
  width: 36px;
349
  height: 36px;
 
355
  font-weight: 600;
356
  border: 1px solid var(--border-color);
357
  }
358
+ .chat-message.user .chat-avatar { background-color: var(--bg-color-medium); }
359
+ .chat-message.model .chat-avatar { background-color: var(--accent-color); }
360
+ .chat-message.user .chat-avatar .material-icons { color: var(--text-color-primary); }
361
+ .chat-message.model .chat-avatar .material-icons { color: var(--bg-color-dark); font-size: 20px; }
 
 
 
 
 
362
  .chat-bubble {
363
  padding: 12px 18px;
364
  border-radius: 20px;
 
370
  border: 1px solid transparent;
371
  position: relative;
372
  }
 
373
  .copy-icon-bubble {
374
  position: absolute;
375
  top: 8px;
 
389
  .copy-icon-bubble:hover {
390
  color: var(--accent-color);
391
  }
 
392
  .model-response-header {
393
  font-size: 10px;
394
  font-weight: 500;
 
397
  padding-bottom: 4px;
398
  margin-top:-50px;
399
  display: flex;
400
+ gap: 2px;
401
  align-items: center;
402
  opacity: 0.7;
403
  border-bottom: 1px solid var(--border-color);
404
  }
405
+ .model-response-header .material-icons { font-size: 12px; }
 
406
  .chat-message.user .chat-bubble {
407
  background: linear-gradient(135deg, #58a6ff, #3a8dff);
408
  color: #ffffff;
 
419
  border-color: var(--error-color);
420
  color: var(--error-color);
421
  }
 
422
  .chat-bubble pre {
423
  background: var(--bg-color-dark) !important;
424
  padding: 12px;
 
434
  border-radius: 8px;
435
  margin-top: 8px;
436
  }
 
437
  .prompt-actions-bar {
438
  display: flex;
439
  align-items: center;
 
457
  .prompt-actions-bar .action-icon .material-icons {
458
  font-size: 18px;
459
  }
 
460
  .prompt-area {
461
  padding: 16px 24px;
462
  border-top: 1px solid var(--border-color);
 
478
  cursor: pointer;
479
  transition: all 0.2s;
480
  font-size: 12px;
 
481
  }
482
  .prompt-example-btn:hover {
483
  border-color: var(--accent-color);
 
495
  border-radius: 8px;
496
  padding: 12px 16px;
497
  color: var(--text-color-primary);
498
+ font-family: 'Inter', sans-serif;
499
  font-size: 16px;
500
  resize: none;
501
  height: 50px;
 
506
  border-color: var(--accent-color);
507
  box-shadow: 0 0 8px var(--accent-glow);
508
  }
 
509
  .image-uploader {
510
  width: 50px;
511
  height: 50px;
 
520
  flex-shrink: 0;
521
  overflow: hidden;
522
  }
523
+ .image-uploader:hover { border-color: var(--accent-color); }
524
+ .image-uploader .material-icons { font-size: 24px; color: var(--text-color-secondary); transition: opacity 0.2s; }
525
+ .image-uploader.loading .spinner { display: block; }
526
+ .image-uploader.loading .material-icons, .image-uploader.has-image .material-icons { display: none; }
 
 
 
 
 
527
  .spinner {
528
  display: none;
529
  width: 24px;
 
539
  height: 100%;
540
  object-fit: cover;
541
  }
542
+ .image-uploader.has-image .image-preview { display: block; }
543
+ @keyframes spin { to { transform: rotate(360deg); } }
 
 
 
544
  .right-panel {
545
  width: 250px;
546
  padding: 24px;
 
559
  font-weight: 500;
560
  margin-bottom: 16px;
561
  }
562
+ .right-panel ul { list-style: none; padding: 0; margin: 0; }
 
563
  .model-item {
564
  padding: 12px;
565
  border-radius: 6px;
 
575
  position: relative;
576
  padding-left: 30px;
577
  }
578
+ .model-item:hover { background-color: var(--bg-color-medium); }
 
579
  .model-item.active {
580
  background-color: var(--accent-color);
581
  color: #ffffff;
 
591
  color: var(--groq-color);
592
  font-size: 14px;
593
  }
 
594
  .parameters-section {
595
  margin-top: 24px;
596
  padding-top: 24px;
597
  border-top: 1px solid var(--border-color);
598
  }
599
+ .parameter-control { margin-bottom: 16px; }
 
600
  .parameter-control label {
601
  display: flex;
602
  justify-content: space-between;
 
626
  border-radius: 50%;
627
  box-shadow: 0 0 8px var(--accent-glow);
628
  }
 
629
  .action-button {
630
  background-color: var(--accent-color);
631
  border: none;
 
641
  height: 50px;
642
  flex-shrink: 0;
643
  }
 
 
 
644
  .action-button:hover {
645
  background-color: #4a9eff;
646
  box-shadow: 0 0 15px var(--accent-glow);
 
657
  background-color: #d9443b;
658
  box-shadow: 0 0 15px rgba(248, 81, 73, 0.5);
659
  }
 
660
  @media (max-width: 1100px) {
661
+ .studio-container { flex-direction: column; height: auto; max-height: none; }
 
662
  .sidebar {
663
  width: 100%;
664
  border-right: none; border-bottom: 1px solid var(--border-color);
665
  flex-direction: row; align-items: center; justify-content: space-between; padding: 16px;
666
  overflow: visible;
667
  }
668
+ .sidebar:hover { width: 100%; }
669
+ .sidebar-header { border: none; margin: 0; padding: 0; }
670
+ .nav-menu { display: flex; gap: 8px; }
671
+ .nav-menu .nav-item { flex-direction: column; padding: 8px; gap: 4px; font-size: 12px; }
672
+ .right-panel { width: 100%; border-left: none; border-top: 1px solid var(--border-color); }
 
 
 
 
 
673
  }
674
  @media (max-width: 768px) {
675
+ body { padding: 0; }
676
+ .studio-container { border-radius: 0; min-height: 100vh; }
677
+ .sidebar { flex-direction: column; align-items: stretch; }
678
+ .nav-menu { justify-content: center; }
 
 
 
 
679
  }
 
680
  </style>
681
  </head>
682
  <body>
 
685
  <div class="sidebar-content">
686
  <div class="sidebar-header">
687
  <span class="material-icons logo">auto_awesome</span><h1 class="item-text">LLM Studio</h1>
 
688
  </div>
689
  <button class="new-chat-btn" id="new-chat-btn"><span class="material-icons">add_circle</span><span class="item-text">New Chat</span></button>
690
  <nav class="nav-menu">
691
  <div class="nav-item active" data-target="text-generation"><span class="material-icons">text_fields</span><span class="item-text">Text Generation</span></div>
692
  <div class="nav-item" data-target="image-to-text"><span class="material-icons">image</span><span class="item-text">Image to Text</span></div>
 
693
  <div class="nav-item" data-target="text-classification"><span class="material-icons">label</span><span class="item-text">Text Classification</span></div>
694
  </nav>
695
  <div class="history-section">
696
  <h3 class="history-title item-text">History</h3>
 
697
  <div class="chat-history-list" id="chat-history-list">
698
  </div>
699
  </div>
700
  </div>
701
  </aside>
 
702
  <main class="main-content">
703
  <div id="home-screen" class="content-window active">
 
704
  <span class="material-icons logo">auto_awesome</span>
705
  <h1>Welcome to LLM Studio</h1>
706
+ <p>Select a mode from the left, or click "New Chat" to begin a new conversation. Your chat history will be saved here for this session.</p>
 
707
  </div>
 
708
  <div id="chat-view" class="content-window" style="display:none;">
709
  <div class="chat-history-display" id="chat-history-display"></div>
710
  <div id="prompt-container">
 
711
  <div id="text-generation" class="prompt-view active">
 
712
  <div class="prompt-area">
713
  <div class="prompt-examples" id="text-generation-examples"></div>
714
+ <div class="input-wrapper">
 
 
715
  <textarea id="text-generation-prompt" placeholder="Enter your prompt here..."></textarea>
716
  <button class="action-button" data-target="text-generation"><span class="material-icons">send</span></button>
717
+ </div>
 
 
718
  </div>
719
  </div>
 
720
  <div id="image-to-text" class="prompt-view">
 
721
  <div class="prompt-area">
 
722
  <div class="prompt-examples" id="image-to-text-examples"></div>
723
  <div class="input-wrapper">
724
+ <div id="image-uploader" class="image-uploader">
725
+ <input type="file" id="image-upload-input" accept="image/*" style="display: none;">
 
 
726
  <span class="material-icons">add_photo_alternate</span>
727
+ <div class="spinner"></div>
 
 
728
  <img class="image-preview" id="image-preview" src="" alt="Image Preview"/>
729
  </div>
 
 
730
  <textarea id="image-to-text-prompt" placeholder="Optionally, add instructions..."></textarea>
731
  <button class="action-button" data-target="image-to-text" disabled><span class="material-icons">send</span></button>
732
+ </div>
733
+ </div>
 
 
734
  </div>
 
735
  <div id="text-classification" class="prompt-view">
736
+ <div class="prompt-area">
 
 
737
  <div class="prompt-examples" id="text-classification-examples"></div>
738
  <div class="input-wrapper">
739
+ <textarea id="text-classification-prompt" placeholder="Enter text to classify..."></textarea>
740
+ <button class="action-button" data-target="text-classification"><span class="material-icons">send</span></button>
 
 
741
  </div>
742
+ </div>
743
+ </div>
 
744
  </div>
745
  </div>
746
  </main>
 
747
  <aside class="right-panel">
748
  <div>
 
749
  <p class="panel-header">MODELS</p>
 
750
  <ul id="model-list">
751
  </ul>
752
  </div>
753
  <div class="parameters-section">
 
754
  <p class="panel-header">PARAMETERS</p>
755
+ <div class="parameter-control">
 
756
  <label for="temperature"><span>Temperature</span><span id="temperature-value">0.9</span></label>
757
  <input type="range" id="temperature" min="0" max="1" step="0.1" value="0.9">
758
+ </div>
 
759
  <div class="parameter-control">
 
760
  <label for="top-p"><span>Top-P</span><span id="top-p-value">1.0</span></label>
761
  <input type="range" id="top-p" min="0" max="1" step="0.1" value="1.0">
762
  </div>
763
  </div>
764
+ <div class="api-key-section">
 
 
 
765
  <div class="api-key-group">
766
  <label for="groq-token">Groq API Key</label>
767
+ <input type="password" id="groq-token" placeholder="gsk_...">
768
  </div>
769
+ <div class="api-key-group">
 
 
770
  <label for="hf-token">Hugging Face Token</label>
771
+ <input type="password" id="hf-token" placeholder="hf_...">
772
  </div>
773
+ </div>
 
774
  </aside>
775
  </div>
776
 
777
  <script>
778
  document.addEventListener('DOMContentLoaded', () => {
779
+ const navItems = document.querySelectorAll('.nav-item');
780
+ const promptViews = document.querySelectorAll('.prompt-view');
 
781
  const modelList = document.getElementById('model-list');
782
  const newChatBtn = document.getElementById('new-chat-btn');
783
  const chatHistoryList = document.getElementById('chat-history-list');
784
  const chatHistoryDisplay = document.getElementById('chat-history-display');
785
+ const homeScreen = document.getElementById('home-screen');
786
  const chatView = document.getElementById('chat-view');
787
  const tempSlider = document.getElementById('temperature');
788
  const tempValue = document.getElementById('temperature-value');
789
  const topPSlider = document.getElementById('top-p');
790
+ const topPValue = document.getElementById('top-p-value');
791
  const hfTokenInput = document.getElementById('hf-token');
792
  const groqTokenInput = document.getElementById('groq-token');
793
  const imageUploader = document.getElementById('image-uploader');
794
  const imagePreview = document.getElementById('image-preview');
795
+ let activeMode = 'text-generation';
796
  let activeModelInfo = {};
797
  let temperature = 0.9;
798
  let topP = 1.0;
799
  let uploadedImageBase64 = null;
800
+ let uploadedImageDataUrl = null;
801
  let chatSessions = {};
802
  let activeChatId = null;
803
  let editingMessageId = null;
804
  let currentRequestController = null;
805
+ let currentTypingInterval = null;
 
806
  const taskConfig = {
807
  'text-generation': {
808
  examples: [
809
+ { title: "Explain quantum computing", prompt: "Explain quantum computing in simple terms." },
810
+ { title: "Python factorial function", prompt: "Write a Python function that calculates the factorial of a number." },
811
+ { title: "Email to boss", prompt: "Write a short, professional email to my boss requesting a meeting next week." }
 
 
 
812
  ],
813
  models: [
814
  { id: "llama3-8b-8192", name: "Llama3 8B (Groq)", url: "https://api.groq.com/openai/v1/chat/completions", type: "chat", isGroq: true },
815
  { id: "qwen/qwen3-32b", name: "qwen/qwen3-32b (Groq)", url: "https://api.groq.com/openai/v1/chat/completions", type: "chat", isGroq: true },
816
+ { id: "gemma2-9b-it", name: "gemma2-9b-it (Groq)", url: "https://api.groq.com/openai/v1/chat/completions", type: "chat", isGroq: true },
 
 
817
  { id: "deepseek-ai/DeepSeek-V3", name: "DeepSeek-V3 (HF)", url: "https://router.huggingface.co/nebius/v1/chat/completions", type: "chat" },
818
  ]
819
  },
820
  'image-to-text': {
821
  examples: [
822
+ { title: "Describe scene", prompt: "Describe this scene in detail." },
823
+ { title: "Identify objects", prompt: "List all the objects you can identify." },
824
+ { title: "Suggest a caption", prompt: "Suggest a creative social media caption." }
 
 
 
 
 
825
  ],
826
  models: [
827
  { id: "meta-llama/llama-4-scout-17b-16e-instruct", name: "meta-llama/llama-4-scout (Groq)", url: "https://api.groq.com/openai/v1/chat/completions", type: "image-to-text",isGroq: true },
828
  { id: "meta-llama/llama-4-maverick-17b-128e-instruct", name: "meta-llama/llama-4-maverick (Groq)", url: "https://api.groq.com/openai/v1/chat/completions", type: "image-to-text",isGroq: true }
829
  ]
 
 
830
  },
831
  'text-classification': {
832
  examples: [
833
+ { title: "Sentiment analysis", prompt: "I'm so frustrated with customer service!" },
834
+ { title: "Topic identification", prompt: "The new legislation aims to reduce carbon emissions." },
835
+ { title: "Spam detection", prompt: "CLICK HERE to claim your prize!" }
 
 
 
836
  ],
837
  models: [
838
  { id: "distilbert-base-uncased-finetuned-sst-2-english", name: "DistilBERT SST-2", url: "https://router.huggingface.co/hf-inference/models/distilbert/distilbert-base-uncased-finetuned-sst-2-english", type: "text-classification" },
839
  { id: "SamLowe/roberta-base-go_emotions", name: "RoBERTa GoEmotions", url: "https://api-inference.huggingface.co/models/SamLowe/roberta-base-go_emotions", type: "text-classification" },
840
+ { id: "nlptown/bert-base-multilingual-uncased-sentiment", name: "BERT Multilingual", url: "https://api-inference.huggingface.co/models/nlptown/bert-base-multilingual-uncased-sentiment", type: "text-classification" }
 
 
841
  ]
842
  }
843
  };
844
+
845
+ function init() {
846
  populatePromptExamples();
847
  setupListeners();
848
  switchMode(activeMode);
849
  updateAllSendButtonStates();
850
+ }
851
 
852
  function populatePromptExamples() {
853
  for (const mode in taskConfig) {
854
  const containerId = `${mode}-examples`;
855
+ const container = document.getElementById(containerId);
856
  if(container) {
857
  container.innerHTML = '';
858
+ taskConfig[mode].examples.forEach(ex => {
859
  const btn = document.createElement('button');
860
  btn.className = 'prompt-example-btn';
861
  btn.textContent = ex.title;
862
  btn.onclick = () => {
 
 
863
  const promptTextarea = document.getElementById(`${mode}-prompt`);
864
  promptTextarea.value = ex.prompt;
865
  promptTextarea.focus();
866
+ autoGrowTextarea(promptTextarea);
 
867
  updateSendButtonState(mode);
 
868
  };
869
  container.appendChild(btn);
870
  });
871
+ }
872
  }
873
  }
874
 
875
  function setupListeners() {
876
  navItems.forEach(item => item.addEventListener('click', () => switchMode(item.dataset.target)));
877
+ newChatBtn.addEventListener('click', startNewChat);
 
878
  tempSlider.addEventListener('input', (e) => {
879
  temperature = parseFloat(e.target.value);
880
  tempValue.textContent = temperature.toFixed(1);
881
  });
882
+ topPSlider.addEventListener('input', (e) => {
883
  topP = parseFloat(e.target.value);
884
  topPValue.textContent = topP.toFixed(1);
885
  });
886
+ document.querySelectorAll('textarea').forEach(textarea => {
887
  textarea.addEventListener('input', () => {
888
  autoGrowTextarea(textarea);
889
  const mode = textarea.id.replace('-prompt', '');
890
  updateSendButtonState(mode);
891
  });
892
  });
893
+ [hfTokenInput, groqTokenInput].forEach(input => {
894
  input.addEventListener('input', () => input.classList.remove('input-error'));
895
  });
896
+ setupImageUploader();
897
  setupActionButtons();
898
  }
899
 
900
  function updateSendButtonState(mode) {
901
  if (mode === 'image-to-text') return;
902
+ const promptTextarea = document.getElementById(`${mode}-prompt`);
903
  const sendButton = document.querySelector(`#${mode} .action-button`);
904
  if (promptTextarea && sendButton) {
905
  sendButton.disabled = promptTextarea.value.trim() === '';
906
+ }
907
  }
908
+
909
  function updateAllSendButtonStates() {
910
  ['text-generation', 'text-classification'].forEach(updateSendButtonState);
911
+ }
912
 
913
  function setupImageUploader() {
914
  const uploaderInput = document.getElementById('image-upload-input');
915
+ imageUploader.addEventListener('click', () => uploaderInput.click());
916
  imageUploader.addEventListener('dragover', (e) => { e.preventDefault(); imageUploader.style.borderColor = 'var(--accent-color)'; });
917
+ imageUploader.addEventListener('dragleave', (e) => { imageUploader.style.borderColor = 'var(--border-color)'; });
918
  imageUploader.addEventListener('drop', (e) => {
919
  e.preventDefault();
920
  imageUploader.style.borderColor = 'var(--border-color)';
921
  if (e.dataTransfer.files.length > 0) handleImageFile(e.dataTransfer.files[0]);
922
  });
923
+ uploaderInput.addEventListener('change', (e) => {
924
  if (e.target.files.length > 0) handleImageFile(e.target.files[0]);
925
  });
926
+ }
927
 
928
  function autoGrowTextarea(element) {
929
  element.style.height = 'auto';
930
+ element.style.height = (element.scrollHeight) + 'px';
931
  }
932
 
933
  function switchMode(targetMode) {
934
  activeMode = targetMode;
935
+ navItems.forEach(i => i.classList.toggle('active', i.dataset.target === targetMode));
936
  promptViews.forEach(v => {
937
  v.classList.toggle('active', v.id === targetMode)
938
  v.style.display = v.id === targetMode ? 'flex' : 'none';
939
  });
940
+ renderModelList(targetMode);
941
 
942
  if (activeChatId) {
943
  chatSessions[activeChatId].mode = targetMode;
944
+ }
945
  }
946
 
947
  function renderModelList(mode) {
948
  modelList.innerHTML = '';
949
+ const models = taskConfig[mode].models;
950
  if (!models || models.length === 0) return;
951
 
952
  setActiveModel(models[0]);
953
+ models.forEach(model => {
954
  const item = document.createElement('li');
955
  item.className = 'model-item';
956
  item.textContent = model.name;
 
958
  item.dataset.modelId = model.id;
959
 
960
  if (model.isGroq) {
961
+ item.classList.add('groq-model');
 
 
962
  }
963
  if (model.id === activeModelInfo.id) {
964
  item.classList.add('active');
965
  }
966
 
967
+ item.onclick = () => {
968
+ setActiveModel(model);
 
 
969
  document.querySelectorAll('.model-item').forEach(i => i.classList.remove('active'));
970
  item.classList.add('active');
971
  };
972
 
973
  modelList.appendChild(item);
974
+ });
 
975
  }
976
 
977
  function setActiveModel(model) {
978
  activeModelInfo = model;
979
+ }
980
 
981
+ // MODIFIED: startNewChat now creates a new session container and hides others
982
  function startNewChat() {
983
  editingMessageId = null;
984
+ const newChatId = `chat_${Date.now()}`;
985
  activeChatId = newChatId;
986
 
987
  chatSessions[newChatId] = {
 
990
  mode: activeMode,
991
  messages: []
992
  };
993
+
994
+ // Hide all other session containers
995
+ document.querySelectorAll('.chat-session-container').forEach(c => c.style.display = 'none');
996
+
997
+ // Create and append a new container for this chat
998
+ const newSessionContainer = document.createElement('div');
999
+ newSessionContainer.className = 'chat-session-container';
1000
+ newSessionContainer.id = `session-container-${newChatId}`;
1001
+ newSessionContainer.style.display = 'flex'; // Make it visible
1002
+ chatHistoryDisplay.appendChild(newSessionContainer);
1003
+
1004
  renderChatHistory();
1005
  homeScreen.classList.remove('active');
1006
  homeScreen.style.display = 'none';
 
1008
  switchMode(activeMode);
1009
  resetImageUploader();
1010
  updateAllSendButtonStates();
1011
+ }
1012
 
1013
+ // MODIFIED: loadChat now shows/hides containers instead of clearing the display
1014
  function loadChat(chatId) {
1015
  editingMessageId = null;
1016
+ activeChatId = chatId;
1017
  const chat = chatSessions[chatId];
1018
 
1019
+ // Hide all session containers
1020
+ document.querySelectorAll('.chat-session-container').forEach(c => c.style.display = 'none');
1021
+
1022
+ let targetContainer = document.getElementById(`session-container-${chatId}`);
1023
+
1024
+ // If the container for this chat doesn't exist, create and populate it
1025
+ if (!targetContainer) {
1026
+ targetContainer = document.createElement('div');
1027
+ targetContainer.className = 'chat-session-container';
1028
+ targetContainer.id = `session-container-${chatId}`;
1029
+ chat.messages.forEach(msg => {
1030
+ const messageEl = createMessageElement(msg); // Use helper to create element
1031
+ if (messageEl) targetContainer.appendChild(messageEl);
1032
+ });
1033
+ chatHistoryDisplay.appendChild(targetContainer);
1034
+ }
1035
+
1036
+ // Show the target container
1037
+ targetContainer.style.display = 'flex';
1038
+
1039
+ homeScreen.classList.remove('active');
1040
  homeScreen.style.display = 'none';
1041
  chatView.style.display = 'flex';
1042
  switchMode(chat.mode);
 
1045
 
1046
  function renderChatHistory() {
1047
  chatHistoryList.innerHTML = '';
1048
+ Object.values(chatSessions).reverse().forEach(chat => {
1049
  const item = document.createElement('div');
1050
  item.className = 'history-item';
1051
  item.dataset.chatId = chat.id;
1052
 
1053
  item.innerHTML = `
1054
  <span class="material-icons">chat_bubble</span>
1055
+ <span class="history-title-text item-text">${chat.title}</span>
 
1056
  <div class="history-actions">
1057
  <span class="material-icons history-icon" onclick="editChatTitle('${chat.id}', event)">edit</span>
1058
  <span class="material-icons history-icon" onclick="deleteChat('${chat.id}', event)">delete</span>
1059
  </div>
1060
  `;
1061
+
 
1062
  if(chat.id === activeChatId) {
1063
  item.classList.add('active');
1064
+ }
 
1065
  item.addEventListener('click', (e) => {
1066
  if (e.target.classList.contains('history-icon')) return;
1067
+ loadChat(chat.id)
 
1068
  });
1069
+ chatHistoryList.appendChild(item);
1070
  });
1071
  }
1072
 
1073
  window.deleteChat = (chatId, event) => {
1074
  event.stopPropagation();
1075
+ if (confirm(`Are you sure you want to delete this chat?`)) {
1076
  delete chatSessions[chatId];
1077
+
1078
+ // Also remove the chat container from the DOM
1079
+ const chatContainer = document.getElementById(`session-container-${chatId}`);
1080
+ if (chatContainer) chatContainer.remove();
1081
+
1082
+ if (activeChatId === chatId) {
1083
  activeChatId = null;
1084
+ chatView.style.display = 'none';
1085
  homeScreen.style.display = 'flex';
1086
  homeScreen.classList.add('active');
1087
  }
1088
  renderChatHistory();
1089
+ }
1090
  };
1091
 
1092
  window.editChatTitle = (chatId, event) => {
1093
  event.stopPropagation();
1094
+ const item = event.target.closest('.history-item');
1095
  const titleSpan = item.querySelector('.history-title-text');
1096
  const currentTitle = titleSpan.textContent;
1097
 
1098
  const input = document.createElement('input');
1099
  input.type = 'text';
1100
+ input.className = 'history-title-input';
1101
  input.value = currentTitle;
1102
 
1103
  titleSpan.replaceWith(input);
 
1105
 
1106
  const saveTitle = () => {
1107
  const newTitle = input.value.trim();
1108
+ if (newTitle && newTitle !== currentTitle) {
1109
  chatSessions[chatId].title = newTitle;
1110
+ }
1111
  renderChatHistory();
1112
  };
1113
 
1114
  input.addEventListener('blur', saveTitle);
1115
+ input.addEventListener('keydown', (e) => {
1116
  if (e.key === 'Enter') saveTitle();
1117
  else if (e.key === 'Escape') renderChatHistory();
1118
  });
1119
+ };
1120
 
1121
  function handleImageFile(file) {
1122
  if (!file.type.startsWith('image/')) return;
1123
 
1124
  imageUploader.classList.remove('has-image');
1125
  imageUploader.classList.add('loading');
1126
+ const reader = new FileReader();
1127
  reader.onload = (e) => {
1128
  uploadedImageDataUrl = e.target.result;
1129
+ uploadedImageBase64 = uploadedImageDataUrl.split(',')[1];
1130
 
1131
  imagePreview.src = uploadedImageDataUrl;
1132
  imageUploader.classList.remove('loading');
1133
  imageUploader.classList.add('has-image');
1134
  document.querySelector('#image-to-text .action-button').disabled = false;
1135
+ }
1136
  reader.readAsDataURL(file);
1137
  }
1138
 
1139
  function resetImageUploader() {
1140
  imageUploader.classList.remove('has-image', 'loading');
1141
+ imagePreview.src = '';
1142
  uploadedImageDataUrl = null;
1143
  uploadedImageBase64 = null;
1144
  document.querySelector('#image-to-text .action-button').disabled = true;
1145
+ }
1146
 
1147
  function addMessageToSession(sender, content, options = {}) {
1148
  if (!activeChatId) return null;
1149
+ const messageId = `msg_${Date.now()}`;
1150
  const message = {
1151
  id: messageId,
1152
  sender,
1153
  content,
1154
+ isHtml: options.isHtml || false,
1155
+ isError: options.isError || false,
 
 
1156
  };
1157
  chatSessions[activeChatId].messages.push(message);
1158
  return messageId;
1159
+ }
1160
 
1161
+ // ADDED: Refactored function to create a message element without displaying it
1162
+ function createMessageElement(message, isTyping = false) {
1163
  const messageWrapper = document.createElement('div');
1164
+ messageWrapper.className = `chat-message-container ${message.sender}`;
1165
+ messageWrapper.id = message.id;
1166
 
1167
  const messageDiv = document.createElement('div');
1168
+ messageDiv.className = `chat-message ${message.sender}`;
1169
+ if(message.isError) { messageDiv.classList.add('error'); }
1170
+
1171
+ const avatar = `<div class="chat-avatar"><span class="material-icons">${message.sender === 'user' ? 'person' : 'auto_awesome'}</span></div>`;
1172
+
1173
+ const bubble = document.createElement('div');
1174
+ bubble.className = 'chat-bubble';
1175
  if (message.isHtml) {
1176
  bubble.innerHTML = message.content;
1177
+ } else {
1178
  bubble.textContent = message.content;
1179
+ }
1180
 
1181
  if (message.sender === 'model' && !isTyping && !message.isError) {
1182
  bubble.innerHTML += `<span class="material-icons copy-icon-bubble" onclick="copyResponse(event)" title="Copy response">content_copy</span>`;
1183
+ }
1184
 
1185
  if (message.sender === 'user') {
1186
  messageDiv.innerHTML = bubble.outerHTML + avatar;
1187
+ } else {
1188
  messageDiv.innerHTML = avatar + bubble.outerHTML;
1189
+ }
1190
 
1191
  messageWrapper.appendChild(messageDiv);
1192
+
1193
+ // Add edit/resend bar for user messages
1194
+ if (message.sender === 'user' && !isTyping && chatSessions[activeChatId]?.mode !== 'image-to-text') {
1195
  const actionBar = document.createElement('div');
1196
+ actionBar.className = 'prompt-actions-bar';
1197
  actionBar.innerHTML = `
1198
  <span class="action-icon" onclick="editMessage('${message.id}')" title="Edit prompt">
1199
  <span class="material-icons">edit</span>
1200
  </span>
 
 
 
 
1201
  `;
1202
+ messageWrapper.appendChild(actionBar);
1203
  }
1204
+ return messageWrapper;
1205
+ }
1206
 
1207
+ // MODIFIED: displayMessage now finds the active container and appends the element
1208
+ function displayMessage(message, isTyping = false) {
1209
+ const activeSessionContainer = document.getElementById(`session-container-${activeChatId}`);
1210
+ if (!activeSessionContainer) {
1211
+ console.error('Active session container not found!');
1212
+ return null;
1213
+ }
1214
+
1215
+ const messageWrapper = createMessageElement(message, isTyping);
1216
+ if (messageWrapper) {
1217
+ activeSessionContainer.appendChild(messageWrapper);
1218
+ activeSessionContainer.scrollTop = activeSessionContainer.scrollHeight;
1219
+ }
1220
  return messageWrapper;
1221
+ }
1222
 
1223
  function streamResponse(element, text, mode) {
1224
  let i = 0;
1225
+ element.innerHTML = '';
1226
 
1227
  currentRequestController = new AbortController();
1228
  const signal = currentRequestController.signal;
1229
  toggleSendStopButton(true, true);
1230
+
1231
+ const activeSessionContainer = document.getElementById(`session-container-${activeChatId}`);
1232
+
1233
+ currentTypingInterval = setInterval(() => {
1234
  if (signal.aborted) {
1235
  clearInterval(currentTypingInterval);
1236
  currentTypingInterval = null;
 
1239
  if (mode === 'image-to-text') {
1240
  resetImageUploader();
1241
  }
1242
+ return;
1243
  }
1244
 
1245
  if (i < text.length) {
1246
  element.innerHTML += text.charAt(i);
1247
  i++;
1248
+ if (activeSessionContainer) {
1249
+ activeSessionContainer.scrollTop = activeSessionContainer.scrollHeight;
1250
+ }
1251
+ } else {
1252
  clearInterval(currentTypingInterval);
1253
  currentTypingInterval = null;
1254
  toggleSendStopButton(false);
 
1257
  }
1258
  }
1259
  }, 10);
1260
+ }
1261
 
1262
  window.editMessage = (messageId) => {
1263
  if (!messageId) return;
1264
+ const chat = chatSessions[activeChatId];
1265
  if (!chat) return;
1266
  const message = chat.messages.find(m => m.id === messageId);
1267
  if (!message) return;
1268
+ const promptTextarea = document.getElementById(`${activeMode}-prompt`);
1269
 
1270
  const tempDiv = document.createElement('div');
1271
  tempDiv.innerHTML = message.content;
1272
  promptTextarea.value = tempDiv.textContent || "";
1273
  promptTextarea.focus();
1274
  updateSendButtonState(activeMode);
1275
+ editingMessageId = messageId;
1276
  };
1277
 
 
1278
  window.resendMessage = (messageId) => {
1279
  if (!messageId) return;
1280
+ const chat = chatSessions[activeChatId];
1281
  if (!chat) return;
1282
  const message = chat.messages.find(m => m.id === messageId);
1283
  if (!message) return;
 
1284
  displayMessage(message);
1285
 
1286
  const tempDiv = document.createElement('div');
1287
+ tempDiv.innerHTML = message.content;
1288
  handleAction(null, tempDiv.textContent || "");
1289
  };
1290
 
1291
  window.copyResponse = (event) => {
1292
  const icon = event.target;
1293
+ const bubble = icon.closest('.chat-bubble');
1294
  if (!bubble) return;
1295
 
1296
  const contentClone = bubble.cloneNode(true);
1297
  contentClone.querySelector('.copy-icon-bubble').remove();
1298
  const textToCopy = contentClone.querySelector('.response-content')?.textContent || contentClone.textContent;
1299
+ navigator.clipboard.writeText(textToCopy).then(() => {
1300
  icon.textContent = 'check';
1301
  setTimeout(() => { icon.textContent = 'content_copy'; }, 1500);
1302
  }).catch(err => console.error('Failed to copy: ', err));
1303
+ };
1304
 
1305
  function createModelResponseHeader() {
1306
  return `
1307
  <div class="model-response-header">
1308
  <span class="material-icons">memory</span>
1309
  <span>${activeModelInfo.name}</span> &nbsp;
1310
+ <span>| &nbsp; Temp: ${temperature.toFixed(1)}</span> &nbsp; | &nbsp;
 
1311
  <span>Top-P: ${topP.toFixed(1)}</span>
1312
  </div>
1313
  `;
1314
+ }
1315
 
1316
  function setupActionButtons() {
1317
  document.querySelectorAll('.action-button').forEach(btn => {
1318
  btn.onclick = handleAction;
1319
  });
1320
+ }
1321
 
 
1322
  function toggleSendStopButton(isGenerating, isTyping = false) {
1323
  document.querySelectorAll('.action-button').forEach(btn => {
1324
  const currentTextarea = document.getElementById(`${btn.dataset.target}-prompt`);
 
1326
  if (isGenerating || isTyping) {
1327
  btn.innerHTML = `<span class="material-icons">stop_circle</span>`;
1328
  btn.classList.add('stop-generating');
1329
+ btn.disabled = false;
 
1330
  if(currentTextarea) currentTextarea.disabled = true;
1331
 
1332
  btn.onclick = () => {
1333
  if (currentRequestController) currentRequestController.abort();
1334
  if(currentTypingInterval) clearInterval(currentTypingInterval);
1335
+ toggleSendStopButton(false);
 
1336
  };
1337
  } else {
1338
  btn.innerHTML = `<span class="material-icons">send</span>`;
1339
  btn.classList.remove('stop-generating');
1340
+ if(currentTextarea) currentTextarea.disabled = false;
1341
  updateSendButtonState(btn.dataset.target);
1342
  btn.onclick = handleAction;
1343
  }
1344
  });
1345
+
1346
+ const activeSessionContainer = document.getElementById(`session-container-${activeChatId}`);
1347
+ if (!activeSessionContainer) return;
1348
+
1349
+ const userMessageContainers = activeSessionContainer.querySelectorAll('.chat-message-container.user');
1350
+ if (userMessageContainers.length > 0) {
1351
+ const lastUserMessage = userMessageContainers[userMessageContainers.length - 1];
1352
+ const actionBar = lastUserMessage.querySelector('.prompt-actions-bar');
1353
+ if (actionBar) {
1354
+ actionBar.style.display = (isGenerating || isTyping) ? 'none' : 'flex';
1355
+ }
1356
+ }
1357
+ }
1358
 
1359
  async function handleAction(e, overridePrompt = null) {
1360
  if(e) e.preventDefault();
1361
+ const targetMode = e ? e.target.closest('.action-button').dataset.target : activeMode;
1362
 
1363
+ if (!activeChatId) {
1364
+ startNewChat();
1365
+ }
1366
 
1367
  const isGroqModel = activeModelInfo.isGroq;
1368
  const apiKeyInput = isGroqModel ? groqTokenInput : hfTokenInput;
1369
+ const apiKey = apiKeyInput.value.trim();
1370
  const apiKeyName = isGroqModel ? "Groq API Key" : "Hugging Face API Token";
1371
+
1372
+ if (!apiKey) {
1373
  const errorMsg = { id: `err_${Date.now()}`, sender: 'model', content: `Please enter your ${apiKeyName}.`, isError: true };
1374
+ displayMessage(errorMsg);
1375
  apiKeyInput.classList.add('input-error');
1376
  return;
1377
  }
1378
 
1379
  const promptTextarea = document.getElementById(`${targetMode}-prompt`);
1380
+ let prompt = overridePrompt !== null ? overridePrompt : promptTextarea?.value.trim() || '';
1381
+
1382
+ if (targetMode === 'image-to-text' && !uploadedImageBase64 && !editingMessageId) {
1383
  const errorMsg = { id: `err_${Date.now()}`, sender: 'model', content: 'Please upload an image first.', isError: true };
1384
+ displayMessage(errorMsg);
1385
  return;
1386
  }
1387
  if (!prompt && targetMode !== 'image-to-text' && overridePrompt === null) return;
1388
+
1389
+ let userMessageContent = prompt;
1390
  if (targetMode === 'image-to-text') {
1391
+ userMessageContent = `<img src="${uploadedImageDataUrl}" alt="Uploaded preview" style="max-height: 150px; border-radius: 8px; margin-bottom: 8px;">${prompt ? `<br>${prompt}`: ''}`;
 
1392
  }
1393
 
 
1394
  if (overridePrompt === null) {
1395
  let userMessageId;
1396
+ if (editingMessageId) {
1397
  const messageToUpdate = chatSessions[activeChatId].messages.find(m => m.id === editingMessageId);
1398
+ if (messageToUpdate) {
1399
  messageToUpdate.content = userMessageContent;
1400
+ // Find the message in the DOM and update it
1401
+ const messageElement = document.getElementById(editingMessageId);
1402
+ if(messageElement) {
1403
+ messageElement.querySelector('.chat-bubble').innerHTML = userMessageContent;
1404
+ }
1405
  userMessageId = editingMessageId;
1406
  }
1407
  } else {
1408
  userMessageId = addMessageToSession('user', userMessageContent, { isHtml: true });
1409
+ const userMessage = chatSessions[activeChatId].messages.find(m => m.id === userMessageId);
1410
  displayMessage(userMessage);
1411
  }
1412
  }
1413
 
1414
  if (chatSessions[activeChatId].messages.length <= 2 && prompt && !editingMessageId) {
1415
  chatSessions[activeChatId].title = prompt.substring(0, 25) + (prompt.length > 25 ? '...' : '');
1416
+ renderChatHistory();
1417
  }
1418
 
1419
+ if(promptTextarea) { promptTextarea.value = ''; autoGrowTextarea(promptTextarea); }
 
1420
  updateAllSendButtonStates();
1421
 
1422
  currentRequestController = new AbortController();
1423
  toggleSendStopButton(true);
1424
+ const typingMsg = { id: `typing_${Date.now()}`, sender: 'model', content: '...' };
1425
  const typingIndicator = displayMessage(typingMsg, true);
1426
 
1427
  let requestBody;
1428
+ let headers = { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' };
1429
+
1430
+ if (targetMode === 'image-to-text') {
1431
+ if (isGroqModel) {
1432
+ const payload = {
1433
+ model: activeModelInfo.id,
1434
+ messages: [ { role: "user", content: [ { type: "text", text: prompt }, { type: "image_url", image_url: { url: uploadedImageDataUrl } } ] } ],
1435
+ temperature, max_tokens: 1024, stream: false
1436
+ };
1437
+ requestBody = JSON.stringify(payload);
1438
+ } else {
1439
+ const res = await fetch(uploadedImageDataUrl, { signal: currentRequestController.signal });
1440
+ requestBody = await res.blob();
1441
+ headers['Content-Type'] = requestBody.type;
1442
+ }
1443
  } else {
 
 
 
 
 
1444
  let payload;
1445
+ if (isGroqModel) {
1446
  payload = { model: activeModelInfo.id, messages: [{"role": "user", "content": prompt}], temperature, top_p: topP, max_tokens: 1024, stream: false };
1447
+ } else if (targetMode === 'text-generation') {
1448
  payload = { model: activeModelInfo.id, messages: [{"role": "user", "content": prompt}], temperature, top_p: topP };
1449
+ } else {
1450
  payload = { inputs: prompt };
1451
+ }
1452
  requestBody = JSON.stringify(payload);
1453
+ }
1454
 
1455
  try {
1456
  const response = await fetch(activeModelInfo.url, { method: 'POST', headers: headers, body: requestBody, signal: currentRequestController.signal });
1457
+ const resultText = await response.text();
1458
 
1459
  if (!response.ok) {
1460
  try {
1461
  const errorJson = JSON.parse(resultText);
1462
+ throw new Error(errorJson.error?.message || errorJson.error || resultText);
1463
+ } catch(e) { throw new Error(resultText); }
 
1464
  }
1465
 
1466
  let responseContent = '';
1467
+ let responseHtml = false;
1468
  const finalResponse = JSON.parse(resultText);
1469
 
1470
  switch (targetMode) {
1471
  case 'text-generation':
1472
+ responseContent = finalResponse.choices?.[0]?.message?.content || '[No content]';
 
1473
  break;
1474
  case 'image-to-text':
1475
  if (isGroqModel) {
1476
+ responseContent = finalResponse.choices?.[0]?.message?.content || '[No image caption]';
 
1477
  } else {
1478
+ responseContent = finalResponse?.[0]?.generated_text || '[No image caption]';
 
1479
  }
1480
  break;
1481
+ case 'text-classification':
1482
+ responseContent = `<pre style="background:var(--bg-color-dark); padding: 12px; border-radius: 8px;">${JSON.stringify(finalResponse, null, 2)}</pre>`;
1483
+ responseHtml = true;
1484
  break;
1485
  }
1486
  const responseHeader = createModelResponseHeader();
1487
+ const fullResponseHtml = responseHeader + `<div class="response-content"></div>`;
1488
 
1489
+ if (typingIndicator) typingIndicator.remove();
1490
+
1491
  const modelMessageId = addMessageToSession('model', fullResponseHtml, { isHtml: true });
1492
+ const finalMessage = chatSessions[activeChatId].messages.find(m => m.id === modelMessageId);
1493
  const finalMessageEl = displayMessage(finalMessage);
1494
 
1495
+ if (finalMessageEl) {
1496
+ const responseContentEl = finalMessageEl.querySelector('.response-content');
1497
+ if (responseHtml) {
1498
+ responseContentEl.innerHTML = responseContent;
1499
+ toggleSendStopButton(false);
1500
+ } else {
1501
+ streamResponse(responseContentEl, responseContent, targetMode);
1502
+ }
1503
+ }
1504
  } catch (error) {
1505
+ if (typingIndicator) {
1506
+ let errorMessage = `Error: ${error.message}`;
1507
+ if (error.name === 'AbortError') {
1508
+ errorMessage = 'Response generation stopped by user.';
1509
+ }
1510
+ typingIndicator.querySelector('.chat-bubble').textContent = errorMessage;
1511
+ typingIndicator.classList.add('error');
1512
+ addMessageToSession('model', errorMessage, { isError: true });
1513
+ }
1514
  toggleSendStopButton(false);
1515
  } finally {
 
 
1516
  editingMessageId = null;
1517
  currentRequestController = null;
1518
+ }
1519
  }
1520
 
1521
  init();