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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +717 -389
app.py CHANGED
@@ -18,6 +18,7 @@ st.markdown("""
18
  width: 99.5vw !important;
19
  border-radius: 5px ;
20
  overflow: hidden !important;
 
21
  margin-top:-15px;
22
 
23
  }
@@ -27,13 +28,14 @@ st.markdown("""
27
  margin-top:-15px;
28
  }
29
  </style>
30
- """, unsafe_allow_html=True)
31
  html_code="""
32
  <!DOCTYPE html>
33
  <html lang="en">
34
  <head>
35
  <meta charset="UTF-8">
36
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
 
37
  <title>LLM Studio Enhanced</title>
38
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
39
  <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
@@ -52,6 +54,7 @@ html_code="""
52
  --model-bubble-bg: #161B22;
53
  --error-color: #f85149;
54
  --groq-color: #4CAF50;
 
55
  }
56
 
57
  body {
@@ -64,29 +67,39 @@ html_code="""
64
  color: var(--text-color-primary);
65
  }
66
 
67
- /* Main Container */
68
  .studio-container {
69
  width: 100%;
70
  height: 100vh;
71
  background-color: rgba(22, 27, 34, 0.8);
72
  backdrop-filter: blur(10px);
73
- border-radius: 16px;
74
  border: 1px solid var(--border-color);
75
  overflow: hidden;
76
  display: flex;
77
  box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
78
  }
79
 
80
- /* Sidebar Navigation */
81
  .sidebar {
82
- width: 240px;
83
  background-color: rgba(13, 17, 23, 0.9);
84
- padding: 24px;
85
  border-right: 1px solid var(--border-color);
86
  flex-shrink: 0;
87
  display: flex;
88
  flex-direction: column;
89
- justify-content: space-between;
 
 
 
 
 
 
 
 
 
 
 
 
90
  }
91
 
92
  .sidebar-header {
@@ -96,32 +109,28 @@ html_code="""
96
  padding-bottom: 24px;
97
  border-bottom: 1px solid var(--border-color);
98
  margin-bottom: 24px;
 
99
  }
100
  .sidebar-header .logo {
101
  font-size: 28px;
102
  color: var(--accent-color);
103
  text-shadow: 0 0 10px var(--accent-glow);
 
 
104
  }
105
- .sidebar-header h1 {
106
- font-size: 20px;
107
- margin: 0;
108
- color:white !important;
109
- font-weight: 600;
110
- }
111
-
112
- /* UPDATED: New Chat Button Style & Position */
113
  .new-chat-btn {
114
  background: linear-gradient(135deg, #58a6ff, #3a8dff);
115
  border: none;
116
  color: #ffffff;
117
- padding: 12px 16px;
118
  border-radius: 8px;
119
  cursor: pointer;
120
  transition: all 0.3s ease;
121
  display: flex;
122
  align-items: center;
123
- justify-content: center;
124
- gap: 10px;
125
  font-size: 14px;
126
  font-weight: 600;
127
  margin-bottom: 24px;
@@ -144,26 +153,37 @@ html_code="""
144
  font-weight: 500;
145
  color: var(--text-color-primary);
146
  }
147
- .nav-menu .nav-item:hover { background-color: var(--bg-color-medium); }
 
 
148
  .nav-menu .nav-item.active {
149
  background-color: var(--accent-color);
150
  color: var(--bg-color-dark);
151
  font-weight: 600;
152
  box-shadow: 0 0 15px var(--accent-glow);
153
  }
154
- .nav-menu .nav-item.active .material-icons { color: var(--bg-color-dark); }
 
155
  .nav-menu .nav-item .material-icons {
156
  color: var(--text-color-secondary);
157
  transition: color 0.3s ease;
 
 
 
 
 
 
 
 
 
158
  }
159
- .nav-menu .nav-item:hover .material-icons { color: var(--text-color-primary); }
160
 
161
  .history-section {
162
  margin-top: 24px;
163
  padding-top: 24px;
164
  border-top: 1px solid var(--border-color);
165
- flex-grow: 1;
166
- overflow-y: auto;
167
  min-height: 100px;
168
  }
169
  .history-title {
@@ -173,9 +193,12 @@ html_code="""
173
  text-transform: uppercase;
174
  letter-spacing: 0.5px;
175
  margin-bottom: 12px;
 
176
  }
177
  .chat-history-list .history-item {
178
- padding: 10px;
 
 
179
  font-size: 14px;
180
  border-radius: 6px;
181
  cursor: pointer;
@@ -184,6 +207,7 @@ html_code="""
184
  text-overflow: ellipsis;
185
  margin-bottom: 4px;
186
  color: var(--text-color-secondary);
 
187
  }
188
  .chat-history-list .history-item:hover {
189
  background-color: var(--bg-color-medium);
@@ -194,13 +218,43 @@ html_code="""
194
  color: var(--text-color-primary);
195
  font-weight: 500;
196
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
197
 
198
- /* UPDATED: API Key Section Styling */
199
  .api-key-section {
200
- padding: 12px;
201
- border: 1px solid var(--border-color);
 
202
  border-radius: 8px;
203
- background-color: var(--bg-color-medium);
204
  }
205
  .api-key-group {
206
  margin-bottom: 12px;
@@ -232,14 +286,17 @@ html_code="""
232
  border-color: var(--accent-color);
233
  box-shadow: 0 0 8px var(--accent-glow);
234
  }
 
 
 
 
235
 
236
-
237
- /* Main Content Area */
238
  .main-content {
239
  flex-grow: 1;
240
  display: flex;
241
  flex-direction: column;
242
  overflow: hidden;
 
243
  }
244
 
245
  .content-window {
@@ -257,7 +314,6 @@ html_code="""
257
  flex-direction: column;
258
  }
259
 
260
- /* Home Screen */
261
  #home-screen {
262
  justify-content: center;
263
  align-items: center;
@@ -265,13 +321,15 @@ html_code="""
265
  padding: 40px;
266
  display: none;
267
  }
268
- #home-screen.active { display: flex; }
269
- #home-screen .logo { font-size: 60px; color: var(--accent-color); }
 
 
270
  #home-screen h1 { font-size: 32px; margin: 16px 0 8px; color: var(--text-color-primary);
271
- }
272
- #home-screen p { color: var(--text-color-secondary); max-width: 450px; line-height: 1.6; }
 
273
 
274
- /* Chat History */
275
  .chat-history-display {
276
  flex-grow: 1;
277
  padding: 24px;
@@ -280,13 +338,22 @@ html_code="""
280
  flex-direction: column;
281
  gap: 16px;
282
  }
 
 
 
 
 
283
  .chat-message {
284
  display: flex;
285
  gap: 16px;
286
  align-items: flex-start;
287
- position: relative; /* For action buttons */
288
  }
289
- .chat-message.user { justify-content: flex-end; }
 
 
 
 
 
290
 
291
  .chat-avatar {
292
  width: 36px;
@@ -299,33 +366,62 @@ html_code="""
299
  font-weight: 600;
300
  border: 1px solid var(--border-color);
301
  }
302
- .chat-message.user .chat-avatar { background-color: var(--bg-color-medium); }
303
- .chat-message.model .chat-avatar { background-color: var(--accent-color); }
304
- .chat-message.user .chat-avatar .material-icons { color: var(--text-color-primary); }
305
- .chat-message.model .chat-avatar .material-icons { color: var(--bg-color-dark); font-size: 20px; }
 
 
 
 
306
 
307
  .chat-bubble {
308
  padding: 12px 18px;
309
  border-radius: 20px;
310
- max-width: 80%;
311
  line-height: 1.6;
312
  word-wrap: break-word;
313
  white-space: pre-wrap;
314
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
315
  border: 1px solid transparent;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
316
  }
317
 
318
- /* UPDATED: Model Response Header & Bubble */
319
  .model-response-header {
320
- font-size: 11px;
 
321
  color: var(--text-color-secondary);
322
- margin-bottom: 8px;
 
 
323
  display: flex;
324
- gap: 8px;
325
  align-items: center;
326
  opacity: 0.7;
 
 
 
327
  }
328
- .model-response-header .material-icons { font-size: 14px; }
329
  .chat-message.user .chat-bubble {
330
  background: linear-gradient(135deg, #58a6ff, #3a8dff);
331
  color: #ffffff;
@@ -343,7 +439,6 @@ html_code="""
343
  color: var(--error-color);
344
  }
345
 
346
- /* Styling for code blocks inside bubbles */
347
  .chat-bubble pre {
348
  background: var(--bg-color-dark) !important;
349
  padding: 12px;
@@ -354,45 +449,36 @@ html_code="""
354
  font-size: 14px;
355
  margin: 8px 0 0 0;
356
  }
357
-
358
  .chat-bubble img {
359
  max-width: 100%;
360
  border-radius: 8px;
361
  margin-top: 8px;
362
  }
363
 
364
- /* ADDED: User Message Actions */
365
- .message-actions {
366
- position: absolute;
367
- top: 50%;
368
- left: -70px; /* Adjust as needed */
369
- transform: translateY(-50%);
370
  display: flex;
371
- gap: 8px;
372
- opacity: 0;
373
- transition: opacity 0.2s ease-in-out;
374
- }
375
- .chat-message.user:hover .message-actions {
376
- opacity: 1;
377
  }
378
- .action-icon {
379
  cursor: pointer;
380
  color: var(--text-color-secondary);
381
- background-color: var(--bg-color-medium);
382
- border-radius: 50%;
383
- padding: 4px;
384
  display: flex;
385
  align-items: center;
386
- justify-content: center;
 
387
  }
388
- .action-icon:hover {
389
  color: var(--accent-color);
390
  }
391
- .action-icon .material-icons {
392
  font-size: 18px;
393
  }
394
 
395
- /* Prompt Input Area */
396
  .prompt-area {
397
  padding: 16px 24px;
398
  border-top: 1px solid var(--border-color);
@@ -414,6 +500,7 @@ html_code="""
414
  cursor: pointer;
415
  transition: all 0.2s;
416
  font-size: 12px;
 
417
  }
418
  .prompt-example-btn:hover {
419
  border-color: var(--accent-color);
@@ -431,7 +518,7 @@ html_code="""
431
  border-radius: 8px;
432
  padding: 12px 16px;
433
  color: var(--text-color-primary);
434
- font-family: 'Inter', sans-serif;
435
  font-size: 16px;
436
  resize: none;
437
  height: 50px;
@@ -457,10 +544,14 @@ html_code="""
457
  flex-shrink: 0;
458
  overflow: hidden;
459
  }
460
- .image-uploader:hover { border-color: var(--accent-color); }
461
- .image-uploader .material-icons { font-size: 24px; color: var(--text-color-secondary); transition: opacity 0.2s; }
462
- .image-uploader.loading .spinner { display: block; }
463
- .image-uploader.loading .material-icons, .image-uploader.has-image .material-icons { display: none; }
 
 
 
 
464
 
465
  .spinner {
466
  display: none;
@@ -477,10 +568,11 @@ html_code="""
477
  height: 100%;
478
  object-fit: cover;
479
  }
480
- .image-uploader.has-image .image-preview { display: block; }
481
- @keyframes spin { to { transform: rotate(360deg); } }
 
 
482
 
483
- /* Models and Parameters Panel */
484
  .right-panel {
485
  width: 250px;
486
  padding: 24px;
@@ -489,6 +581,9 @@ html_code="""
489
  flex-shrink: 0;
490
  display: flex;
491
  flex-direction: column;
 
 
 
492
  }
493
  .panel-header {
494
  font-size: 14px;
@@ -496,7 +591,8 @@ html_code="""
496
  font-weight: 500;
497
  margin-bottom: 16px;
498
  }
499
- .right-panel ul { list-style: none; padding: 0; margin: 0; }
 
500
  .model-item {
501
  padding: 12px;
502
  border-radius: 6px;
@@ -512,7 +608,8 @@ html_code="""
512
  position: relative;
513
  padding-left: 30px;
514
  }
515
- .model-item:hover { background-color: var(--bg-color-medium); }
 
516
  .model-item.active {
517
  background-color: var(--accent-color);
518
  color: #ffffff;
@@ -534,7 +631,8 @@ html_code="""
534
  padding-top: 24px;
535
  border-top: 1px solid var(--border-color);
536
  }
537
- .parameter-control { margin-bottom: 16px; }
 
538
  .parameter-control label {
539
  display: flex;
540
  justify-content: space-between;
@@ -580,308 +678,396 @@ html_code="""
580
  height: 50px;
581
  flex-shrink: 0;
582
  }
 
 
 
583
  .action-button:hover {
584
  background-color: #4a9eff;
585
- box-shadow: 0 0 15px var(--accent-glow);
586
  }
587
  .action-button:disabled {
588
  background-color: var(--border-color);
589
  cursor: not-allowed;
590
  box-shadow: none;
591
  }
 
 
 
 
 
 
 
592
 
593
  @media (max-width: 1100px) {
594
- .studio-container { flex-direction: column; height: auto; max-height: none; }
 
595
  .sidebar {
596
- width: 100%; border-right: none; border-bottom: 1px solid var(--border-color);
 
597
  flex-direction: row; align-items: center; justify-content: space-between; padding: 16px;
 
 
 
 
 
 
 
598
  }
599
- .sidebar-header { border: none; margin: 0; padding: 0; }
600
- .nav-menu { display: flex; gap: 8px; }
601
- .nav-menu .nav-item { flex-direction: column; padding: 8px; gap: 4px; font-size: 12px; }
602
- .right-panel { width: 100%; border-left: none; border-top: 1px solid var(--border-color); }
603
  }
604
  @media (max-width: 768px) {
605
- body { padding: 0; }
606
- .studio-container { border-radius: 0; min-height: 100vh; }
607
- .sidebar { flex-direction: column; align-items: stretch; }
608
- .nav-menu { justify-content: center; }
 
 
 
 
609
  }
610
 
611
  </style>
612
  </head>
613
  <body>
614
  <div class="studio-container">
615
- <!-- Sidebar -->
616
  <aside class="sidebar">
617
- <div>
618
  <div class="sidebar-header">
619
- <span class="material-icons logo">auto_awesome</span><h1>LLM Studio</h1>
 
620
  </div>
621
- <button class="new-chat-btn" id="new-chat-btn"><span class="material-icons">add_circle</span>New Chat</button>
622
  <nav class="nav-menu">
623
- <div class="nav-item active" data-target="text-generation"><span class="material-icons">text_fields</span><span>Text Generation</span></div>
624
- <div class="nav-item" data-target="image-to-text"><span class="material-icons">image</span><span>Image to Text</span></div>
625
- <div class="nav-item" data-target="text-classification"><span class="material-icons">label</span><span>Text Classification</span></div>
 
626
  </nav>
627
  <div class="history-section">
628
- <h3 class="history-title">History</h3>
629
- <div class="chat-history-list" id="chat-history-list">
630
- <!-- Chat history items will be dynamically inserted here -->
631
  </div>
632
  </div>
633
  </div>
634
- <div class="api-key-section">
635
- <div class="api-key-group">
636
- <label for="groq-token">Groq API Key</label>
637
- <input type="password" id="groq-token" placeholder="gsk_...">
638
- </div>
639
- <div class="api-key-group">
640
- <label for="hf-token">Hugging Face Token</label>
641
- <input type="password" id="hf-token" placeholder="hf_...">
642
- </div>
643
- </div>
644
  </aside>
645
 
646
- <!-- Main Content -->
647
  <main class="main-content">
648
  <div id="home-screen" class="content-window active">
649
- <span class="material-icons logo">auto_awesome</span>
 
650
  <h1>Welcome to LLM Studio</h1>
651
- <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>
 
652
  </div>
653
 
654
  <div id="chat-view" class="content-window" style="display:none;">
655
  <div class="chat-history-display" id="chat-history-display"></div>
656
  <div id="prompt-container">
657
- <!-- Text Generation View -->
658
- <div id="text-generation" class="prompt-view active">
659
- <div class="prompt-area">
 
660
  <div class="prompt-examples" id="text-generation-examples"></div>
661
- <div class="input-wrapper">
662
- <textarea id="text-generation-prompt" placeholder="Enter your prompt here..."></textarea>
 
 
663
  <button class="action-button" data-target="text-generation"><span class="material-icons">send</span></button>
664
- </div>
665
- </div>
 
 
666
  </div>
667
 
668
- <!-- Image to Text View -->
669
  <div id="image-to-text" class="prompt-view">
670
- <div class="prompt-area">
671
- <div class="prompt-examples" id="image-to-text-examples"></div>
 
 
672
  <div class="input-wrapper">
673
- <div id="image-uploader" class="image-uploader">
674
- <input type="file" id="image-upload-input" accept="image/*" style="display: none;">
 
 
675
  <span class="material-icons">add_photo_alternate</span>
676
- <div class="spinner"></div>
 
 
677
  <img class="image-preview" id="image-preview" src="" alt="Image Preview"/>
678
  </div>
679
- <textarea id="image-to-text-prompt" placeholder="Optionally, add instructions..."></textarea>
 
 
680
  <button class="action-button" data-target="image-to-text" disabled><span class="material-icons">send</span></button>
681
  </div>
682
- </div>
 
 
683
  </div>
684
 
685
- <!-- Text Classification View -->
686
  <div id="text-classification" class="prompt-view">
687
- <div class="prompt-area">
688
- <div class="prompt-examples" id="text-classification-examples"></div>
 
 
689
  <div class="input-wrapper">
690
- <textarea id="text-classification-prompt" placeholder="Enter text to classify..."></textarea>
691
- <button class="action-button" data-target="text-classification"><span class="material-icons">send</span></button>
 
 
692
  </div>
693
  </div>
694
- </div>
 
695
  </div>
696
  </div>
697
  </main>
698
 
699
- <!-- Right Panel -->
700
  <aside class="right-panel">
701
  <div>
702
- <p class="panel-header">MODELS</p>
703
- <ul id="model-list">
704
- <!-- Models will be dynamically inserted here -->
 
705
  </ul>
706
  </div>
707
  <div class="parameters-section">
708
- <p class="panel-header">PARAMETERS</p>
709
- <div class="parameter-control">
 
 
710
  <label for="temperature"><span>Temperature</span><span id="temperature-value">0.9</span></label>
711
  <input type="range" id="temperature" min="0" max="1" step="0.1" value="0.9">
712
  </div>
 
713
  <div class="parameter-control">
714
- <label for="top-p"><span>Top-P</span><span id="top-p-value">1.0</span></label>
 
715
  <input type="range" id="top-p" min="0" max="1" step="0.1" value="1.0">
716
  </div>
717
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
718
  </aside>
719
  </div>
720
 
721
  <script>
722
  document.addEventListener('DOMContentLoaded', () => {
723
- // DOM Elements
724
- const navItems = document.querySelectorAll('.nav-item');
725
- const promptViews = document.querySelectorAll('.prompt-view');
726
  const modelList = document.getElementById('model-list');
727
- const actionButtons = document.querySelectorAll('.action-button');
728
  const newChatBtn = document.getElementById('new-chat-btn');
729
  const chatHistoryList = document.getElementById('chat-history-list');
730
  const chatHistoryDisplay = document.getElementById('chat-history-display');
731
- const homeScreen = document.getElementById('home-screen');
732
  const chatView = document.getElementById('chat-view');
733
  const tempSlider = document.getElementById('temperature');
734
  const tempValue = document.getElementById('temperature-value');
735
  const topPSlider = document.getElementById('top-p');
736
- const topPValue = document.getElementById('top-p-value');
737
  const hfTokenInput = document.getElementById('hf-token');
738
  const groqTokenInput = document.getElementById('groq-token');
739
  const imageUploader = document.getElementById('image-uploader');
740
  const imagePreview = document.getElementById('image-preview');
741
-
742
- // App State
743
- let activeMode = 'text-generation';
744
  let activeModelInfo = {};
745
  let temperature = 0.9;
746
  let topP = 1.0;
747
  let uploadedImageBase64 = null;
748
- let uploadedImageDataUrl = null;
749
  let chatSessions = {};
750
  let activeChatId = null;
751
- let editingMessageId = null; // To track which message is being edited
 
 
752
 
753
- // UPDATED: taskConfig with Groq models
754
  const taskConfig = {
755
  'text-generation': {
756
  examples: [
757
- { title: "Explain quantum computing", prompt: "Explain quantum computing in simple terms." },
758
- { title: "Python factorial function", prompt: "Write a Python function that calculates the factorial of a number." },
759
- { title: "Email to boss", prompt: "Write a short, professional email to my boss requesting a meeting next week." }
 
 
 
760
  ],
761
  models: [
762
  { id: "llama3-8b-8192", name: "Llama3 8B (Groq)", url: "https://api.groq.com/openai/v1/chat/completions", type: "chat", isGroq: true },
763
  { id: "qwen/qwen3-32b", name: "qwen/qwen3-32b (Groq)", url: "https://api.groq.com/openai/v1/chat/completions", type: "chat", isGroq: true },
764
- { id: "gemma2-9b-it", name: "gemma2-9b-it (Groq)", url: "https://api.groq.com/openai/v1/chat/completions", type: "chat", isGroq: true },
 
 
765
  { id: "deepseek-ai/DeepSeek-V3", name: "DeepSeek-V3 (HF)", url: "https://router.huggingface.co/nebius/v1/chat/completions", type: "chat" },
766
  ]
767
  },
768
  'image-to-text': {
769
  examples: [
770
- { title: "Describe scene", prompt: "Describe this scene in detail." },
771
- { title: "Identify objects", prompt: "List all the objects you can identify." },
772
- { title: "Suggest a caption", prompt: "Suggest a creative social media caption." }
 
 
 
 
 
773
  ],
774
  models: [
775
  { 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 },
776
  { 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 }
777
  ]
778
- },
 
 
779
  'text-classification': {
780
  examples: [
781
- { title: "Sentiment analysis", prompt: "I'm so frustrated with customer service!" },
782
- { title: "Topic identification", prompt: "The new legislation aims to reduce carbon emissions." },
783
- { title: "Spam detection", prompt: "CLICK HERE to claim your prize!" }
 
 
 
784
  ],
785
  models: [
786
  { 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" },
787
  { id: "SamLowe/roberta-base-go_emotions", name: "RoBERTa GoEmotions", url: "https://api-inference.huggingface.co/models/SamLowe/roberta-base-go_emotions", type: "text-classification" },
788
- { 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" }
 
 
789
  ]
790
  }
791
  };
792
-
793
- function init() {
794
  populatePromptExamples();
795
  setupListeners();
796
  switchMode(activeMode);
797
- }
 
798
 
799
  function populatePromptExamples() {
800
  for (const mode in taskConfig) {
801
  const containerId = `${mode}-examples`;
802
- const container = document.getElementById(containerId);
803
  if(container) {
804
  container.innerHTML = '';
805
- taskConfig[mode].examples.forEach(ex => {
806
  const btn = document.createElement('button');
807
  btn.className = 'prompt-example-btn';
808
  btn.textContent = ex.title;
809
  btn.onclick = () => {
810
- const promptTextarea = document.getElementById(`${mode}-prompt`);
 
 
811
  promptTextarea.value = ex.prompt;
812
  promptTextarea.focus();
813
- autoGrowTextarea(promptTextarea);
814
- };
 
 
 
815
  container.appendChild(btn);
816
  });
817
- }
818
  }
819
  }
820
 
821
  function setupListeners() {
822
  navItems.forEach(item => item.addEventListener('click', () => switchMode(item.dataset.target)));
823
- actionButtons.forEach(btn => btn.addEventListener('click', handleAction));
824
- newChatBtn.addEventListener('click', startNewChat);
825
 
826
  tempSlider.addEventListener('input', (e) => {
827
  temperature = parseFloat(e.target.value);
828
  tempValue.textContent = temperature.toFixed(1);
829
  });
830
- topPSlider.addEventListener('input', (e) => {
831
  topP = parseFloat(e.target.value);
832
  topPValue.textContent = topP.toFixed(1);
833
  });
834
-
835
- document.querySelectorAll('textarea').forEach(textarea => {
836
- textarea.addEventListener('input', () => autoGrowTextarea(textarea));
 
 
 
837
  });
838
-
839
- setupImageUploader();
 
 
 
840
  }
841
 
 
 
 
 
 
 
 
 
 
 
 
 
842
  function setupImageUploader() {
843
  const uploaderInput = document.getElementById('image-upload-input');
844
- imageUploader.addEventListener('click', () => uploaderInput.click());
845
  imageUploader.addEventListener('dragover', (e) => { e.preventDefault(); imageUploader.style.borderColor = 'var(--accent-color)'; });
846
- imageUploader.addEventListener('dragleave', (e) => { imageUploader.style.borderColor = 'var(--border-color)'; });
847
  imageUploader.addEventListener('drop', (e) => {
848
  e.preventDefault();
849
  imageUploader.style.borderColor = 'var(--border-color)';
850
  if (e.dataTransfer.files.length > 0) handleImageFile(e.dataTransfer.files[0]);
851
  });
852
- uploaderInput.addEventListener('change', (e) => {
853
  if (e.target.files.length > 0) handleImageFile(e.target.files[0]);
854
  });
855
- }
856
 
857
  function autoGrowTextarea(element) {
858
  element.style.height = 'auto';
859
- element.style.height = (element.scrollHeight) + 'px';
860
  }
861
 
862
  function switchMode(targetMode) {
863
  activeMode = targetMode;
864
- navItems.forEach(i => i.classList.toggle('active', i.dataset.target === targetMode));
865
  promptViews.forEach(v => {
866
  v.classList.toggle('active', v.id === targetMode)
867
  v.style.display = v.id === targetMode ? 'flex' : 'none';
868
  });
869
-
870
- renderModelList(targetMode);
871
 
872
  if (activeChatId) {
873
  chatSessions[activeChatId].mode = targetMode;
874
- }
875
  }
876
 
877
  function renderModelList(mode) {
878
  modelList.innerHTML = '';
879
- const models = taskConfig[mode].models;
880
  if (!models || models.length === 0) return;
881
 
882
  setActiveModel(models[0]);
883
-
884
- models.forEach(model => {
885
  const item = document.createElement('li');
886
  item.className = 'model-item';
887
  item.textContent = model.name;
@@ -889,29 +1075,34 @@ document.addEventListener('DOMContentLoaded', () => {
889
  item.dataset.modelId = model.id;
890
 
891
  if (model.isGroq) {
892
- item.classList.add('groq-model');
 
 
893
  }
894
  if (model.id === activeModelInfo.id) {
895
  item.classList.add('active');
896
  }
897
 
898
- item.onclick = () => {
899
- setActiveModel(model);
 
 
900
  document.querySelectorAll('.model-item').forEach(i => i.classList.remove('active'));
901
  item.classList.add('active');
902
  };
903
 
904
  modelList.appendChild(item);
905
- });
 
906
  }
907
 
908
  function setActiveModel(model) {
909
  activeModelInfo = model;
910
- }
911
 
912
  function startNewChat() {
913
  editingMessageId = null;
914
- const newChatId = `chat_${Date.now()}`;
915
  activeChatId = newChatId;
916
 
917
  chatSessions[newChatId] = {
@@ -920,28 +1111,26 @@ document.addEventListener('DOMContentLoaded', () => {
920
  mode: activeMode,
921
  messages: []
922
  };
923
-
924
- chatHistoryDisplay.innerHTML = '';
925
  renderChatHistory();
926
  homeScreen.classList.remove('active');
927
  homeScreen.style.display = 'none';
928
  chatView.style.display = 'flex';
929
  switchMode(activeMode);
930
  resetImageUploader();
931
- }
 
932
 
933
  function loadChat(chatId) {
934
  editingMessageId = null;
935
- activeChatId = chatId;
936
  const chat = chatSessions[chatId];
937
 
938
  chatHistoryDisplay.innerHTML = '';
939
-
940
- chat.messages.forEach(msg => {
941
- displayMessage(msg.id, msg.sender, msg.content, msg.isHtml, msg.isError);
942
  });
943
-
944
- homeScreen.classList.remove('active');
945
  homeScreen.style.display = 'none';
946
  chatView.style.display = 'flex';
947
  switchMode(chat.mode);
@@ -950,302 +1139,454 @@ document.addEventListener('DOMContentLoaded', () => {
950
 
951
  function renderChatHistory() {
952
  chatHistoryList.innerHTML = '';
953
- Object.values(chatSessions).reverse().forEach(chat => {
954
  const item = document.createElement('div');
955
  item.className = 'history-item';
956
- item.textContent = chat.title;
957
  item.dataset.chatId = chat.id;
 
 
 
 
 
 
 
 
 
 
 
 
958
  if(chat.id === activeChatId) {
959
  item.classList.add('active');
960
- }
961
- item.onclick = () => loadChat(chat.id);
962
- chatHistoryList.appendChild(item);
 
 
 
 
 
963
  });
964
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
965
 
966
  function handleImageFile(file) {
967
  if (!file.type.startsWith('image/')) return;
968
 
969
  imageUploader.classList.remove('has-image');
970
  imageUploader.classList.add('loading');
971
-
972
- const reader = new FileReader();
973
  reader.onload = (e) => {
974
  uploadedImageDataUrl = e.target.result;
975
- uploadedImageBase64 = uploadedImageDataUrl.split(',')[1];
976
 
977
  imagePreview.src = uploadedImageDataUrl;
978
  imageUploader.classList.remove('loading');
979
  imageUploader.classList.add('has-image');
980
  document.querySelector('#image-to-text .action-button').disabled = false;
981
- }
982
  reader.readAsDataURL(file);
983
  }
984
 
985
  function resetImageUploader() {
986
  imageUploader.classList.remove('has-image', 'loading');
987
- imagePreview.src = '';
988
  uploadedImageDataUrl = null;
989
  uploadedImageBase64 = null;
990
  document.querySelector('#image-to-text .action-button').disabled = true;
991
- }
992
 
993
- function addMessageToSession(sender, content, isHtml = false, isError = false) {
994
- if (!activeChatId) return;
995
- const messageId = `msg_${Date.now()}`;
996
- const message = { id: messageId, sender, content, isHtml, isError };
 
 
 
 
 
 
 
 
997
  chatSessions[activeChatId].messages.push(message);
998
  return messageId;
999
- }
 
 
 
 
 
1000
 
1001
- // UPDATED: displayMessage to include edit/resend and message IDs
1002
- function displayMessage(messageId, sender, content, isHtml = false, isError = false) {
1003
  const messageDiv = document.createElement('div');
1004
- messageDiv.className = `chat-message ${sender}`;
1005
- messageDiv.id = messageId;
1006
- if(isError) { messageDiv.classList.add('error'); }
1007
 
1008
- const avatar = `<div class="chat-avatar"><span class="material-icons">${sender === 'user' ? 'person' : 'auto_awesome'}</span></div>`;
1009
-
1010
- let messageContentHtml = '';
1011
- if (sender === 'user') {
1012
- // Add action buttons for user messages
1013
- const actions = `
1014
- <div class="message-actions">
1015
- <div class="action-icon" onclick="editMessage('${messageId}')" title="Edit"><span class="material-icons">edit</span></div>
1016
- <div class="action-icon" onclick="resendMessage('${messageId}')" title="Resend"><span class="material-icons">replay</span></div>
1017
- </div>`;
1018
- messageContentHtml += actions;
1019
- }
1020
-
1021
- const bubble = document.createElement('div');
1022
  bubble.className = 'chat-bubble';
1023
-
1024
- if (isHtml) {
1025
- bubble.innerHTML = content;
1026
- } else {
1027
- bubble.textContent = content;
1028
- }
1029
 
1030
- messageContentHtml += bubble.outerHTML;
 
 
1031
 
1032
- if (sender === 'user') {
1033
- messageDiv.innerHTML = messageContentHtml + avatar;
1034
- } else {
1035
- messageDiv.innerHTML = avatar + messageContentHtml;
1036
- }
1037
 
1038
- chatHistoryDisplay.appendChild(messageDiv);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1039
  chatHistoryDisplay.scrollTop = chatHistoryDisplay.scrollHeight;
1040
- return messageDiv;
1041
- }
1042
 
1043
- // ADDED: streamResponse for typing effect
1044
- function streamResponse(element, text, speed = 10) {
1045
  let i = 0;
1046
- element.innerHTML = ''; // Clear the '...'
1047
- const interval = setInterval(() => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1048
  if (i < text.length) {
1049
  element.innerHTML += text.charAt(i);
1050
  i++;
1051
  chatHistoryDisplay.scrollTop = chatHistoryDisplay.scrollHeight;
1052
- } else {
1053
- clearInterval(interval);
 
 
 
 
 
 
 
1054
  }
1055
- }, speed);
1056
- }
1057
 
1058
  window.editMessage = (messageId) => {
1059
- const chat = chatSessions[activeChatId];
 
1060
  if (!chat) return;
1061
  const message = chat.messages.find(m => m.id === messageId);
1062
  if (!message) return;
1063
-
1064
- const promptTextarea = document.getElementById(`${activeMode}-prompt`);
1065
- // We need to strip HTML for editing. A simple way for this app:
1066
  const tempDiv = document.createElement('div');
1067
  tempDiv.innerHTML = message.content;
1068
  promptTextarea.value = tempDiv.textContent || "";
1069
  promptTextarea.focus();
1070
- editingMessageId = messageId; // Set the message to be replaced
 
1071
  };
1072
-
 
1073
  window.resendMessage = (messageId) => {
1074
- const chat = chatSessions[activeChatId];
 
1075
  if (!chat) return;
1076
  const message = chat.messages.find(m => m.id === messageId);
1077
  if (!message) return;
1078
-
 
 
1079
  const tempDiv = document.createElement('div');
1080
- tempDiv.innerHTML = message.content;
1081
-
1082
- handleAction(null, tempDiv.textContent || ""); // Resend the original text content
1083
  };
1084
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1085
  function createModelResponseHeader() {
1086
  return `
1087
  <div class="model-response-header">
1088
  <span class="material-icons">memory</span>
1089
- <span>${activeModelInfo.name}</span> |
1090
- <span>Temp: ${temperature.toFixed(1)}</span> |
 
1091
  <span>Top-P: ${topP.toFixed(1)}</span>
1092
  </div>
1093
  `;
1094
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1095
 
1096
- // UPDATED: handleAction to support editing and resending
1097
  async function handleAction(e, overridePrompt = null) {
1098
- const targetMode = e ? e.currentTarget.dataset.target : activeMode;
1099
- if (activeMode !== targetMode) return;
1100
 
1101
- if (!activeChatId) { startNewChat(); }
 
1102
 
1103
  const isGroqModel = activeModelInfo.isGroq;
1104
- const apiKey = isGroqModel ? groqTokenInput.value.trim() : hfTokenInput.value.trim();
 
1105
  const apiKeyName = isGroqModel ? "Groq API Key" : "Hugging Face API Token";
1106
-
1107
- if (!apiKey) {
1108
- displayMessage(null, 'model', `Please enter your ${apiKeyName}.`, false, true);
 
1109
  return;
1110
  }
1111
 
1112
  const promptTextarea = document.getElementById(`${targetMode}-prompt`);
1113
- let prompt = overridePrompt !== null ? overridePrompt : promptTextarea?.value.trim() || '';
1114
-
1115
- if (targetMode === 'image-to-text' && !uploadedImageBase64 && !editingMessageId) {
1116
- displayMessage(null, 'model', 'Please upload an image first.', false, true); return;
1117
- }
1118
- if (!prompt && targetMode !== 'image-to-text') return;
1119
-
1120
- let userMessageContent = prompt;
 
1121
  if (targetMode === 'image-to-text') {
1122
- userMessageContent = `<img src="${uploadedImageDataUrl}" alt="Uploaded preview" style="max-height: 150px; border-radius: 8px; margin-bottom: 8px;">${prompt ? `<br>${prompt}`: ''}`;
 
1123
  }
1124
 
1125
- if (editingMessageId) {
1126
- // Find and update the existing message
1127
- const messageToUpdate = chatSessions[activeChatId].messages.find(m => m.id === editingMessageId);
1128
- if (messageToUpdate) {
1129
- messageToUpdate.content = userMessageContent;
1130
- document.getElementById(editingMessageId).querySelector('.chat-bubble').innerHTML = userMessageContent;
 
 
 
 
 
 
 
 
1131
  }
1132
- } else {
1133
- addMessageToSession('user', userMessageContent, true);
1134
- displayMessage(`msg_${Date.now()}`, 'user', userMessageContent, true);
1135
  }
1136
 
1137
  if (chatSessions[activeChatId].messages.length <= 2 && prompt && !editingMessageId) {
1138
  chatSessions[activeChatId].title = prompt.substring(0, 25) + (prompt.length > 25 ? '...' : '');
1139
- renderChatHistory();
1140
  }
1141
 
1142
- if(promptTextarea) { promptTextarea.value = ''; autoGrowTextarea(promptTextarea); }
 
 
1143
 
1144
- const typingIndicator = displayMessage(`msg_${Date.now()}`, 'model', '...');
 
 
 
1145
 
1146
  let requestBody;
1147
- let headers = { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' };
1148
-
1149
- if (targetMode === 'image-to-text') {
1150
  if (isGroqModel) {
1151
  const payload = {
1152
  model: activeModelInfo.id,
1153
- messages: [
1154
- {
1155
- role: "user",
1156
- content: [
1157
- {
1158
- type: "text",
1159
- text: prompt
1160
- },
1161
- {
1162
- type: "image_url",
1163
- image_url: { url: uploadedImageDataUrl }
1164
- }
1165
- ]
1166
- }
1167
- ],
1168
- temperature,
1169
- max_tokens: 1024,
1170
- stream: false
1171
  };
1172
- requestBody = JSON.stringify(payload);
1173
  } else {
1174
- // Fallback to HF blob-based logic
1175
- const res = await fetch(uploadedImageDataUrl);
1176
- requestBody = await res.blob();
1177
  headers['Content-Type'] = requestBody.type;
1178
  }
1179
  } else {
1180
  let payload;
1181
- if (isGroqModel) {
1182
  payload = { model: activeModelInfo.id, messages: [{"role": "user", "content": prompt}], temperature, top_p: topP, max_tokens: 1024, stream: false };
1183
- } else if (targetMode === 'text-generation') {
1184
  payload = { model: activeModelInfo.id, messages: [{"role": "user", "content": prompt}], temperature, top_p: topP };
1185
- } else { // text-classification
1186
  payload = { inputs: prompt };
1187
- }
1188
  requestBody = JSON.stringify(payload);
1189
- }
1190
 
1191
  try {
1192
- const response = await fetch(activeModelInfo.url, { method: 'POST', headers: headers, body: requestBody });
1193
- const resultText = await response.text();
1194
 
1195
  if (!response.ok) {
1196
  try {
1197
  const errorJson = JSON.parse(resultText);
1198
- throw new Error(errorJson.error?.message || errorJson.error || resultText);
1199
- } catch(e) { throw new Error(resultText); }
 
1200
  }
1201
 
1202
  let responseContent = '';
 
1203
  const finalResponse = JSON.parse(resultText);
1204
 
1205
  switch (targetMode) {
1206
  case 'text-generation':
1207
- responseContent = finalResponse.choices?.[0]?.message?.content || '[No content]';
 
1208
  break;
1209
-
1210
  case 'image-to-text':
1211
  if (isGroqModel) {
1212
- // Groq returns choices array, just like OpenAI
1213
- responseContent = finalResponse.choices?.[0]?.message?.content || '[No image caption]';
1214
  } else {
1215
- // Hugging Face image-to-text returns an array
1216
- responseContent = finalResponse?.[0]?.generated_text || '[No image caption]';
1217
  }
1218
  break;
1219
-
1220
- case 'text-classification':
1221
- responseContent = `<pre style="background:var(--bg-color-dark); padding: 12px; border-radius: 8px;">${JSON.stringify(finalResponse, null, 2)}</pre>`;
1222
- responseHtml = true;
1223
  break;
1224
  }
1225
  const responseHeader = createModelResponseHeader();
1226
- const fullResponseHtml = responseHeader + `<div class="response-content"></div>`;
1227
 
1228
- // Update typing indicator with the header and prepare for streaming
1229
- typingIndicator.querySelector('.chat-bubble').innerHTML = fullResponseHtml;
1230
- const responseContentEl = typingIndicator.querySelector('.response-content');
 
1231
 
1232
- if (targetMode === 'text-classification') {
1233
- responseContentEl.innerHTML = responseContent; // No streaming for formatted JSON
 
 
1234
  } else {
1235
- streamResponse(responseContentEl, responseContent);
1236
- }
1237
- addMessageToSession('model', fullResponseHtml, true);
1238
-
1239
-
1240
  } catch (error) {
1241
- const errorMessage = `Error: ${error.message}`;
 
 
 
1242
  typingIndicator.querySelector('.chat-bubble').textContent = errorMessage;
1243
  typingIndicator.classList.add('error');
1244
- addMessageToSession('model', errorMessage, false, true);
 
1245
  } finally {
1246
- if (targetMode === 'image-to-text') { resetImageUploader(); }
1247
- editingMessageId = null; // Clear editing state
1248
- }
 
 
1249
  }
1250
 
1251
  init();
@@ -1253,21 +1594,8 @@ document.addEventListener('DOMContentLoaded', () => {
1253
  </script>
1254
  </body>
1255
  </html>
1256
-
1257
-
1258
  """
1259
- # --- Option 1: Using st.components.v1.html (Recommended for more control) ---
1260
- # This allows more control over height and scrolling
1261
  components.html(
1262
  html_code,
1263
-
1264
- )
1265
-
1266
- # --- Option 2: Using st.markdown with an iframe (Simpler, but less control) ---
1267
- # st.markdown(
1268
- # f"""
1269
- # <iframe src="{REACT_APP_URL}" width="100%" height="800" style="border:none;"></iframe>
1270
- # """,
1271
- # unsafe_allow_html=True
1272
- # )
1273
-
 
18
  width: 99.5vw !important;
19
  border-radius: 5px ;
20
  overflow: hidden !important;
21
+
22
  margin-top:-15px;
23
 
24
  }
 
28
  margin-top:-15px;
29
  }
30
  </style>
31
+ """, unsafe_allow_html=True)
32
  html_code="""
33
  <!DOCTYPE html>
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">
 
54
  --model-bubble-bg: #161B22;
55
  --error-color: #f85149;
56
  --groq-color: #4CAF50;
57
+ --container-radius: 16px;
58
  }
59
 
60
  body {
 
67
  color: var(--text-color-primary);
68
  }
69
 
 
70
  .studio-container {
71
  width: 100%;
72
  height: 100vh;
73
  background-color: rgba(22, 27, 34, 0.8);
74
  backdrop-filter: blur(10px);
75
+ border-radius: var(--container-radius);
76
  border: 1px solid var(--border-color);
77
  overflow: hidden;
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 */
85
  background-color: rgba(13, 17, 23, 0.9);
 
86
  border-right: 1px solid var(--border-color);
87
  flex-shrink: 0;
88
  display: flex;
89
  flex-direction: column;
90
+ overflow: hidden;
91
+ transition: width 0.4s cubic-bezier(0.4, 0, 0.2, 1);
92
+ padding: 0;
93
+ box-shadow: 4px 0 20px -5px rgba(0,0,0,0.3), 1px 0 0 0 rgba(88, 166, 255, 0.1);
94
+ border-top-left-radius: var(--container-radius);
95
+ border-bottom-left-radius: var(--container-radius);
96
+ }
97
+ .sidebar-content {
98
+ height: 100%;
99
+ display: flex;
100
+ flex-direction: column;
101
+ padding: 16px;
102
+ min-width: 208px;
103
  }
104
 
105
  .sidebar-header {
 
109
  padding-bottom: 24px;
110
  border-bottom: 1px solid var(--border-color);
111
  margin-bottom: 24px;
112
+ flex-shrink: 0;
113
  }
114
  .sidebar-header .logo {
115
  font-size: 28px;
116
  color: var(--accent-color);
117
  text-shadow: 0 0 10px var(--accent-glow);
118
+ min-width: 36px;
119
+ text-align: center;
120
  }
121
+
 
 
 
 
 
 
 
122
  .new-chat-btn {
123
  background: linear-gradient(135deg, #58a6ff, #3a8dff);
124
  border: none;
125
  color: #ffffff;
126
+ padding: 12px;
127
  border-radius: 8px;
128
  cursor: pointer;
129
  transition: all 0.3s ease;
130
  display: flex;
131
  align-items: center;
132
+ justify-content: flex-start;
133
+ gap: 16px;
134
  font-size: 14px;
135
  font-weight: 600;
136
  margin-bottom: 24px;
 
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;
184
  border-top: 1px solid var(--border-color);
185
+ flex-grow: 1;
186
+ overflow-y: auto;
187
  min-height: 100px;
188
  }
189
  .history-title {
 
193
  text-transform: uppercase;
194
  letter-spacing: 0.5px;
195
  margin-bottom: 12px;
196
+ padding-left: 12px;
197
  }
198
  .chat-history-list .history-item {
199
+ display: flex;
200
+ align-items: center;
201
+ padding: 10px 12px;
202
  font-size: 14px;
203
  border-radius: 6px;
204
  cursor: pointer;
 
207
  text-overflow: ellipsis;
208
  margin-bottom: 4px;
209
  color: var(--text-color-secondary);
210
+ position: relative;
211
  }
212
  .chat-history-list .history-item:hover {
213
  background-color: var(--bg-color-medium);
 
218
  color: var(--text-color-primary);
219
  font-weight: 500;
220
  }
221
+ .history-item .history-title-text {
222
+ flex-grow: 1;
223
+ overflow: hidden;
224
+ text-overflow: ellipsis;
225
+ margin-left: 16px;
226
+ }
227
+ .history-item .history-title-input {
228
+ width: 100%;
229
+ background: var(--bg-color-dark);
230
+ border: 1px solid var(--accent-color);
231
+ border-radius: 4px;
232
+ color: var(--text-color-primary);
233
+ padding: 2px 4px;
234
+ }
235
+ .history-actions {
236
+ display: flex;
237
+ align-items: center;
238
+ gap: 8px;
239
+ margin-left: 8px;
240
+ }
241
+ .history-item:hover .history-actions {
242
+ display: flex;
243
+ }
244
+ .history-icon {
245
+ font-size: 16px;
246
+ cursor: pointer;
247
+ color: var(--text-color-secondary);
248
+ }
249
+ .history-icon:hover {
250
+ color: var(--accent-color);
251
+ }
252
 
 
253
  .api-key-section {
254
+ padding-top: 24px;
255
+ margin-top: 24px;
256
+ border-top: 1px solid var(--border-color);
257
  border-radius: 8px;
 
258
  }
259
  .api-key-group {
260
  margin-bottom: 12px;
 
286
  border-color: var(--accent-color);
287
  box-shadow: 0 0 8px var(--accent-glow);
288
  }
289
+ .input-error {
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;
297
  flex-direction: column;
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 {
 
314
  flex-direction: column;
315
  }
316
 
 
317
  #home-screen {
318
  justify-content: center;
319
  align-items: center;
 
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;
 
338
  flex-direction: column;
339
  gap: 16px;
340
  }
341
+ .chat-message-container {
342
+ display: flex;
343
+ flex-direction: column;
344
+ max-width: 85%;
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;
 
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;
381
+ max-width: 100%;
382
  line-height: 1.6;
383
  word-wrap: break-word;
384
  white-space: pre-wrap;
385
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
386
  border: 1px solid transparent;
387
+ position: relative;
388
+ }
389
+
390
+ .copy-icon-bubble {
391
+ position: absolute;
392
+ top: 8px;
393
+ right: 8px;
394
+ color: var(--text-color-secondary);
395
+ background-color: var(--bg-color-dark);
396
+ border-radius: 50%;
397
+ padding: 4px;
398
+ cursor: pointer;
399
+ opacity: 0;
400
+ transition: opacity 0.2s;
401
+ font-size: 18px;
402
+ }
403
+ .chat-bubble:hover .copy-icon-bubble {
404
+ opacity: 1;
405
+ }
406
+ .copy-icon-bubble:hover {
407
+ color: var(--accent-color);
408
  }
409
 
 
410
  .model-response-header {
411
+ font-size: 10px;
412
+ font-weight: 500;
413
  color: var(--text-color-secondary);
414
+ margin-bottom: 4px;
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;
 
439
  color: var(--error-color);
440
  }
441
 
 
442
  .chat-bubble pre {
443
  background: var(--bg-color-dark) !important;
444
  padding: 12px;
 
449
  font-size: 14px;
450
  margin: 8px 0 0 0;
451
  }
 
452
  .chat-bubble img {
453
  max-width: 100%;
454
  border-radius: 8px;
455
  margin-top: 8px;
456
  }
457
 
458
+ .prompt-actions-bar {
 
 
 
 
 
459
  display: flex;
460
+ align-items: center;
461
+ gap: 12px;
462
+ padding: 8px 0 0 0;
463
+ justify-content: flex-end;
464
+ margin-right: 52px;
 
465
  }
466
+ .prompt-actions-bar .action-icon {
467
  cursor: pointer;
468
  color: var(--text-color-secondary);
469
+ transition: color 0.2s;
 
 
470
  display: flex;
471
  align-items: center;
472
+ gap: 4px;
473
+ font-size: 13px;
474
  }
475
+ .prompt-actions-bar .action-icon:hover {
476
  color: var(--accent-color);
477
  }
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
  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
  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;
 
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;
 
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;
 
581
  flex-shrink: 0;
582
  display: flex;
583
  flex-direction: column;
584
+ box-shadow: -4px 0 20px -5px rgba(0,0,0,0.3), -1px 0 0 0 rgba(88, 166, 255, 0.1);
585
+ border-top-right-radius: var(--container-radius);
586
+ border-bottom-right-radius: var(--container-radius);
587
  }
588
  .panel-header {
589
  font-size: 14px;
 
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
  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;
 
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;
 
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);
687
  }
688
  .action-button:disabled {
689
  background-color: var(--border-color);
690
  cursor: not-allowed;
691
  box-shadow: none;
692
  }
693
+ .action-button.stop-generating {
694
+ background-color: var(--error-color);
695
+ }
696
+ .action-button.stop-generating:hover {
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>
735
  <div class="studio-container">
 
736
  <aside class="sidebar">
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
  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
  mode: activeMode,
1112
  messages: []
1113
  };
1114
+ chatHistoryDisplay.innerHTML = '';
 
1115
  renderChatHistory();
1116
  homeScreen.classList.remove('active');
1117
  homeScreen.style.display = 'none';
1118
  chatView.style.display = 'flex';
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
 
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);
1197
+ input.focus();
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;
1317
+ element.innerHTML += '... [Stopped]';
1318
+ toggleSendStopButton(false);
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);
1335
+ if (mode === 'image-to-text') {
1336
+ resetImageUploader();
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`);
1409
+
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();
 
1594
  </script>
1595
  </body>
1596
  </html>
 
 
1597
  """
1598
+
 
1599
  components.html(
1600
  html_code,
1601
+ )