lemonade-sdk 8.1.1__py3-none-any.whl → 8.1.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of lemonade-sdk might be problematic. Click here for more details.

@@ -31,14 +31,26 @@
31
31
  <main class="main">
32
32
  <div class="tab-container">
33
33
  <div class="tabs">
34
- <button class="tab active" id="tab-chat" onclick="showTab('chat')">LLM Chat</button>
35
- <button class="tab" id="tab-models" onclick="showTab('models')">Model Management</button>
34
+ <div class="tab-group">
35
+ <button class="tab active" id="tab-chat" onclick="showTab('chat')">LLM Chat</button>
36
+ <button class="tab" id="tab-model-settings" onclick="showTab('settings')">Model Settings</button>
37
+ <button class="tab" id="tab-models" onclick="showTab('models')">Model Management</button>
38
+ </div>
39
+
40
+ <!-- Model Status Indicator integrated into tab bar -->
41
+ <div class="model-status-indicator" id="model-status-indicator">
42
+ <div class="status-light" id="status-light"></div>
43
+ <span class="model-status-text" id="model-status-text">Loading...</span>
44
+ <button class="model-action-btn" id="model-unload-btn" style="display: none;" title="Unload model">⏏</button>
45
+ </div>
36
46
  </div>
37
47
  <div class="tab-content active" id="content-chat">
38
48
  <div class="chat-container">
39
49
  <div class="chat-history" id="chat-history"></div>
40
50
  <div class="chat-input-row">
41
- <select id="model-select"></select>
51
+ <select id="model-select" class="model-select">
52
+ <option value="">Pick a model</option>
53
+ </select>
42
54
  <div class="input-with-indicator">
43
55
  <input type="text" id="chat-input" placeholder="Type your message..." />
44
56
  </div>
@@ -91,76 +103,136 @@
91
103
  </div>
92
104
  </div>
93
105
  </div>
94
- <div class="tab-content" id="content-models"> <div class="model-mgmt-register-form collapsed"> <h3 class="model-mgmt-form-title" onclick="toggleAddModelForm()">
95
- Add a Model
96
- <span class="tooltip-icon" data-tooltip="Lemonade Server has a built-in set of suggested models, however you can use this form to add any compatible GGUF or ONNX model you like from Hugging Face.">ⓘ</span>
97
- </h3>
98
- <form id="register-model-form" autocomplete="off" class="form-content">
99
- <div class="register-form-row">
100
- <label class="register-label">
101
- Model Name
102
- <span class="tooltip-icon" data-tooltip="Enter a unique short name for your model. This is how the model will be referenced by Lemonade Server and connected apps. It will be prefixed with 'user.' to distinguish it from the built-in models.">ⓘ</span>
103
- </label>
104
- <div class="register-model-name-group">
105
- <span class="register-model-prefix styled-prefix">user.</span>
106
- <input type="text" id="register-model-name" name="model_name" placeholder="Gemma-3-12b-it-GGUF" required autocomplete="off">
106
+ <div class="tab-content" id="content-settings">
107
+ <div class="model-settings-container">
108
+ <div class="settings-form">
109
+ <div class="setting-row">
110
+ <label for="setting-temperature">Temperature:</label>
111
+ <input type="number" id="setting-temperature" min="0" max="2" step="0.1" placeholder="default" />
112
+ <span class="setting-description">Controls randomness in responses (0 = deterministic, 2 = very random)</span>
107
113
  </div>
114
+ <div class="setting-row">
115
+ <label for="setting-top-k">Top K:</label>
116
+ <input type="number" id="setting-top-k" min="1" max="100" step="1" placeholder="default" />
117
+ <span class="setting-description">Limits token selection to top K most likely tokens</span>
108
118
  </div>
109
- <div class="register-form-row">
110
- <label class="register-label">
111
- Checkpoint
112
- <span class="tooltip-icon" data-tooltip="Specify the model checkpoint path from Hugging Face (e.g., org-name/model-name:variant).">ⓘ</span>
113
- </label>
114
- <input type="text" id="register-checkpoint" name="checkpoint" placeholder="unsloth/gemma-3-12b-it-GGUF:Q4_0" class="register-textbox" autocomplete="off">
119
+ <div class="setting-row">
120
+ <label for="setting-top-p">Top P:</label>
121
+ <input type="number" id="setting-top-p" min="0" max="1" step="0.01" placeholder="default" />
122
+ <span class="setting-description">Nucleus sampling - considers tokens with cumulative probability up to P</span>
115
123
  </div>
116
- <div class="register-form-row">
117
- <label class="register-label">
118
- Recipe
119
- <span class="tooltip-icon" data-tooltip="Select the Lemonade recipe corresponding to the inference engine and device Lemonade Server should use for the model. Use llamacpp for GGUF models. For OGA/ONNX models, click the More Info button to learn about the oga-* recipes.">ⓘ</span>
120
- </label>
121
- <select id="register-recipe" name="recipe" required>
122
- <option value="llamacpp">llamacpp</option>
123
- <option value="oga-npu">oga-npu</option>
124
- <option value="oga-hybrid">oga-hybrid</option>
125
- <option value="oga-cpu">oga-cpu</option>
126
- </select>
127
- <a href="https://lemonade-server.ai/docs/lemonade_api/" target="_blank" class="register-doc-link">More info</a>
124
+ <div class="setting-row">
125
+ <label for="setting-repeat-penalty">Repeat Penalty:</label>
126
+ <input type="number" id="setting-repeat-penalty" min="0.5" max="2" step="0.05" placeholder="default" />
127
+ <span class="setting-description">Penalty for repeating tokens (1 = no penalty, >1 = less repetition)</span>
128
128
  </div>
129
- <div class="register-form-row register-form-row-tight">
130
- <label class="register-label">
131
- mmproj file
132
- <span class="tooltip-icon" data-tooltip="Specify an mmproj file from the same Hugging Face checkpoint as the model. This is used for multimodal models, such as VLMs. Leave empty if not needed.">ⓘ</span>
133
- </label>
134
- <input type="text" id="register-mmproj" name="mmproj" placeholder="(Optional) mmproj-F16.gguf" autocomplete="off">
135
- <label class="register-label reasoning-inline">
136
- <input type="checkbox" id="register-reasoning" name="reasoning">
137
- Reasoning
138
- <span class="tooltip-icon" data-tooltip="Enable to inform Lemonade Server that the model has reasoning capabilities that will use thinking tokens.">ⓘ</span>
139
- </label>
129
+ <div class="setting-actions">
130
+ <button id="reset-settings-btn" class="reset-btn">Reset to Defaults</button>
140
131
  </div>
141
- <div class="register-form-row register-form-row-tight">
142
- <button type="submit" id="register-submit">Install</button>
143
- <span id="register-model-status" class="register-status"></span> </div>
144
- </form>
132
+ </div>
145
133
  </div>
146
- <div class="model-mgmt-container">
147
- <div class="model-mgmt-pane">
148
- <h3>Installed Models</h3>
149
- <table class="model-table" id="installed-models-table">
150
- <colgroup><col style="width:100%"></colgroup>
151
- <tbody id="installed-models-tbody"></tbody>
152
- </table>
134
+ </div>
135
+ <div class="tab-content" id="content-models">
136
+ <div class="model-browser-container">
137
+ <div class="model-browser-sidebar">
138
+ <div class="model-category" data-category="hot">
139
+ <div class="category-header active" onclick="toggleCategory('hot')">
140
+ <span class="category-icon">🔥</span>
141
+ <span class="category-name">Hot Models</span>
142
+ </div>
143
+ <div class="category-content expanded" id="category-hot"></div>
144
+ </div>
145
+
146
+ <div class="model-category-section">
147
+ <div class="section-header">
148
+ <span class="section-icon">🔧</span>
149
+ <span class="section-name">By Recipe</span>
150
+ </div>
151
+ <div class="section-content">
152
+ <div class="subcategory" data-recipe="llamacpp" onclick="selectRecipe('llamacpp')">llama.cpp</div>
153
+ <div class="subcategory" data-recipe="oga-hybrid" onclick="selectRecipe('oga-hybrid')">OGA Hybrid</div>
154
+ <div class="subcategory" data-recipe="oga-npu" onclick="selectRecipe('oga-npu')">OGA NPU</div>
155
+ <div class="subcategory" data-recipe="oga-cpu" onclick="selectRecipe('oga-cpu')">OGA CPU</div>
156
+ </div>
157
+ </div>
158
+
159
+ <div class="model-category-section">
160
+ <div class="section-header">
161
+ <span class="section-icon">🏷️</span>
162
+ <span class="section-name">By Category</span>
163
+ </div>
164
+ <div class="section-content">
165
+ <div class="subcategory" data-label="coding" onclick="selectLabel('coding')">Coding</div>
166
+ <div class="subcategory" data-label="vision" onclick="selectLabel('vision')">Vision</div>
167
+ <div class="subcategory" data-label="reasoning" onclick="selectLabel('reasoning')">Reasoning</div>
168
+ <div class="subcategory" data-label="reranking" onclick="selectLabel('reranking')">Reranking</div>
169
+ <div class="subcategory" data-label="embeddings" onclick="selectLabel('embeddings')">Embeddings</div>
170
+ <div class="subcategory" data-label="custom" onclick="selectLabel('custom')">Custom</div>
171
+ </div>
172
+ </div>
173
+
174
+ <div class="model-category" data-category="add">
175
+ <div class="category-header" onclick="showAddModelForm()">
176
+ <span class="category-icon">➕</span>
177
+ <span class="category-name">Add a Model</span>
178
+ </div>
179
+ </div>
153
180
  </div>
154
- <div class="model-mgmt-pane">
155
- <h3>🔥 Hot Models</h3>
156
- <table class="model-table" id="hot-models-table">
157
- <tbody id="hot-models-tbody"></tbody>
158
- </table>
181
+
182
+ <div class="model-browser-main">
183
+ <div class="model-list" id="model-list"></div>
159
184
 
160
- <h3 style="margin-top: 2em;">Suggested Models</h3>
161
- <table class="model-table" id="suggested-models-table">
162
- <tbody id="suggested-models-tbody"></tbody>
163
- </table>
185
+ <!-- Add Model Form (hidden by default) -->
186
+ <div class="add-model-form-main" id="add-model-form-main" style="display: none;">
187
+ <form id="register-model-form" autocomplete="off" class="form-content">
188
+ <div class="register-form-row">
189
+ <label class="register-label">
190
+ Model Name
191
+ <span class="tooltip-icon" data-tooltip="Enter a unique short name for your model. This is how the model will be referenced by Lemonade Server and connected apps. It will be prefixed with 'user.' to distinguish it from the built-in models.">ⓘ</span>
192
+ </label>
193
+ <div class="register-model-name-group">
194
+ <span class="register-model-prefix styled-prefix">user.</span>
195
+ <input type="text" id="register-model-name" name="model_name" placeholder="Gemma-3-12b-it-GGUF" required autocomplete="off">
196
+ </div>
197
+ </div>
198
+ <div class="register-form-row">
199
+ <label class="register-label">
200
+ Checkpoint
201
+ <span class="tooltip-icon" data-tooltip="Specify the model checkpoint path from Hugging Face (e.g., org-name/model-name:variant).">ⓘ</span>
202
+ </label>
203
+ <input type="text" id="register-checkpoint" name="checkpoint" placeholder="unsloth/gemma-3-12b-it-GGUF:Q4_0" class="register-textbox" autocomplete="off">
204
+ </div>
205
+ <div class="register-form-row">
206
+ <label class="register-label">
207
+ Recipe
208
+ <span class="tooltip-icon" data-tooltip="Select the Lemonade recipe corresponding to the inference engine and device Lemonade Server should use for the model. Use llamacpp for GGUF models. For OGA/ONNX models, click the More Info button to learn about the oga-* recipes.">ⓘ</span>
209
+ </label>
210
+ <select id="register-recipe" name="recipe" required>
211
+ <option value="llamacpp">llamacpp</option>
212
+ <option value="oga-npu">oga-npu</option>
213
+ <option value="oga-hybrid">oga-hybrid</option>
214
+ <option value="oga-cpu">oga-cpu</option>
215
+ </select>
216
+ <a href="https://lemonade-server.ai/docs/lemonade_api/" target="_blank" class="register-doc-link">More info</a>
217
+ </div>
218
+ <div class="register-form-row register-form-row-tight">
219
+ <label class="register-label">
220
+ mmproj file
221
+ <span class="tooltip-icon" data-tooltip="Specify an mmproj file from the same Hugging Face checkpoint as the model. This is used for multimodal models, such as VLMs. Leave empty if not needed.">ⓘ</span>
222
+ </label>
223
+ <input type="text" id="register-mmproj" name="mmproj" placeholder="(Optional) mmproj-F16.gguf" autocomplete="off">
224
+ <label class="register-label reasoning-inline">
225
+ <input type="checkbox" id="register-reasoning" name="reasoning">
226
+ Reasoning
227
+ <span class="tooltip-icon" data-tooltip="Enable to inform Lemonade Server that the model has reasoning capabilities that will use thinking tokens.">ⓘ</span>
228
+ </label>
229
+ </div>
230
+ <div class="register-form-row register-form-row-tight">
231
+ <button type="submit" id="register-submit">Install</button>
232
+ <span id="register-model-status" class="register-status"></span>
233
+ </div>
234
+ </form>
235
+ </div>
164
236
  </div>
165
237
  </div>
166
238
  </div>
@@ -170,1034 +242,16 @@
170
242
  <div class="dad-joke">When life gives you LLMs, make an LLM aide.</div>
171
243
  <div class="copyright">Copyright 2025 AMD</div>
172
244
  </footer>
245
+
246
+ <!-- External libraries -->
173
247
  <script src="https://cdn.jsdelivr.net/npm/openai@4.21.0/dist/openai.min.js"></script>
174
248
  <script src="https://cdn.jsdelivr.net/npm/marked@9.1.0/marked.min.js"></script>
175
- <script src="https://polyfill.io/v3/polyfill.min.js?features=es6"></script>
176
249
  <script id="MathJax-script" async src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
177
- <script>
178
- // Configure MathJax
179
- window.MathJax = {
180
- tex: {
181
- inlineMath: [['\\(', '\\)'], ['$', '$']],
182
- displayMath: [['\\[', '\\]'], ['$$', '$$']],
183
- processEscapes: true,
184
- processEnvironments: true
185
- },
186
- options: {
187
- skipHtmlTags: ['script', 'noscript', 'style', 'textarea', 'pre']
188
- }
189
- };
190
- </script>
191
- <script>
192
- // Configure marked.js for safe HTML rendering
193
- marked.setOptions({
194
- breaks: true,
195
- gfm: true,
196
- sanitize: false,
197
- smartLists: true,
198
- smartypants: true
199
- });
200
-
201
- // Function to unescape JSON strings
202
- function unescapeJsonString(str) {
203
- try {
204
- return str.replace(/\\n/g, '\n')
205
- .replace(/\\t/g, '\t')
206
- .replace(/\\r/g, '\r')
207
- .replace(/\\"/g, '"')
208
- .replace(/\\\\/g, '\\');
209
- } catch (error) {
210
- console.error('Error unescaping string:', error);
211
- return str;
212
- }
213
- }
214
-
215
- // Function to safely render markdown with MathJax support
216
- function renderMarkdown(text) {
217
- try {
218
- const html = marked.parse(text);
219
- // Trigger MathJax to process the new content
220
- if (window.MathJax && window.MathJax.typesetPromise) {
221
- // Use a timeout to ensure DOM is updated before typesetting
222
- setTimeout(() => {
223
- window.MathJax.typesetPromise();
224
- }, 0);
225
- }
226
- return html;
227
- } catch (error) {
228
- console.error('Error rendering markdown:', error);
229
- return text; // fallback to plain text
230
- }
231
- }
232
-
233
- // Display an error message in the banner
234
- function showErrorBanner(msg) {
235
- const banner = document.getElementById('error-banner');
236
- if (!banner) return;
237
- const msgEl = document.getElementById('error-banner-msg');
238
- const fullMsg = msg + '\nCheck the Lemonade Server logs via the system tray app for more information.';
239
- if (msgEl) {
240
- msgEl.textContent = fullMsg;
241
- } else {
242
- banner.textContent = fullMsg;
243
- }
244
- banner.style.display = 'flex';
245
- }
246
-
247
- function hideErrorBanner() {
248
- const banner = document.getElementById('error-banner');
249
- if (banner) banner.style.display = 'none';
250
- }
251
-
252
- // Helper fetch wrappers that surface server error details
253
- async function httpRequest(url, options = {}) {
254
- const resp = await fetch(url, options);
255
- if (!resp.ok) {
256
- let detail = resp.statusText || 'Request failed';
257
- try {
258
- const contentType = resp.headers.get('content-type') || '';
259
- if (contentType.includes('application/json')) {
260
- const data = await resp.json();
261
- if (data && data.detail) detail = data.detail;
262
- } else {
263
- const text = await resp.text();
264
- if (text) detail = text.trim();
265
- }
266
- } catch (_) {}
267
- throw new Error(detail);
268
- }
269
- return resp;
270
- }
271
-
272
- async function httpJson(url, options = {}) {
273
- const resp = await httpRequest(url, options);
274
- return await resp.json();
275
- }
276
-
277
- // Tab switching logic
278
- function showTab(tab, updateHash = true) {
279
- document.getElementById('tab-chat').classList.remove('active');
280
- document.getElementById('tab-models').classList.remove('active');
281
- document.getElementById('content-chat').classList.remove('active');
282
- document.getElementById('content-models').classList.remove('active');
283
- if (tab === 'chat') {
284
- document.getElementById('tab-chat').classList.add('active');
285
- document.getElementById('content-chat').classList.add('active');
286
- if (updateHash) {
287
- window.location.hash = 'llm-chat';
288
- }
289
- } else {
290
- document.getElementById('tab-models').classList.add('active');
291
- document.getElementById('content-models').classList.add('active');
292
- if (updateHash) {
293
- window.location.hash = 'model-management';
294
- }
295
- }
296
- }
297
-
298
- // Handle hash changes for anchor navigation
299
- function handleHashChange() {
300
- const hash = window.location.hash.slice(1); // Remove the # symbol
301
- if (hash === 'llm-chat') {
302
- showTab('chat', false);
303
- } else if (hash === 'model-management') {
304
- showTab('models', false);
305
- }
306
- }
307
-
308
- // Initialize tab based on URL hash on page load
309
- function initializeTabFromHash() {
310
- const hash = window.location.hash.slice(1);
311
- if (hash === 'llm-chat') {
312
- showTab('chat', false);
313
- } else if (hash === 'model-management') {
314
- showTab('models', false);
315
- }
316
- // If no hash or unrecognized hash, keep default (chat tab is already active)
317
- }
318
-
319
- // Listen for hash changes
320
- window.addEventListener('hashchange', handleHashChange);
321
-
322
- // Initialize on page load
323
- document.addEventListener('DOMContentLoaded', initializeTabFromHash);
324
-
325
- // Toggle Add Model form
326
- function toggleAddModelForm() {
327
- const form = document.querySelector('.model-mgmt-register-form');
328
- form.classList.toggle('collapsed');
329
- }
330
-
331
- // Handle image load failures for app logos
332
- function handleImageFailure(img) {
333
- const logoItem = img.closest('.app-logo-item');
334
- if (logoItem) {
335
- logoItem.classList.add('image-failed');
336
- }
337
- }
338
-
339
- // Set up image error handlers when DOM is loaded
340
- document.addEventListener('DOMContentLoaded', function() {
341
- const logoImages = document.querySelectorAll('.app-logo-img');
342
- logoImages.forEach(function(img) {
343
- let imageLoaded = false;
344
-
345
- img.addEventListener('load', function() {
346
- imageLoaded = true;
347
- });
348
-
349
- img.addEventListener('error', function() {
350
- if (!imageLoaded) {
351
- handleImageFailure(this);
352
- }
353
- });
354
-
355
- // Also check if image is already broken (cached failure)
356
- if (img.complete && img.naturalWidth === 0) {
357
- handleImageFailure(img);
358
- }
359
-
360
- // Timeout fallback for slow connections (5 seconds)
361
- setTimeout(function() {
362
- if (!imageLoaded && !img.complete) {
363
- handleImageFailure(img);
364
- }
365
- }, 5000);
366
- });
367
- });
368
-
369
- // Helper to get server base URL
370
- function getServerBaseUrl() {
371
- const port = window.SERVER_PORT || 8000;
372
- return `http://localhost:${port}`;
373
- }
374
-
375
- // Check if current model supports vision
376
- function isVisionModel(modelId) {
377
- const allModels = window.SERVER_MODELS || {};
378
- const modelData = allModels[modelId];
379
- if (modelData && modelData.labels && Array.isArray(modelData.labels)) {
380
- return modelData.labels.some(label => label.toLowerCase() === 'vision');
381
- }
382
- return false;
383
- }
384
-
385
- // Update attachment button state based on current model
386
- function updateAttachmentButtonState() {
387
- const currentModel = modelSelect.value;
388
- const isVision = isVisionModel(currentModel);
389
-
390
- if (isVision) {
391
- attachmentBtn.style.opacity = '1';
392
- attachmentBtn.style.cursor = 'pointer';
393
- attachmentBtn.title = 'Attach images';
394
- } else {
395
- attachmentBtn.style.opacity = '0.5';
396
- attachmentBtn.style.cursor = 'not-allowed';
397
- attachmentBtn.title = 'Image attachments not supported by this model';
398
- }
399
- }
400
-
401
- // Populate model dropdown from /api/v1/models endpoint
402
- async function loadModels() {
403
- try {
404
- const data = await httpJson(getServerBaseUrl() + '/api/v1/models');
405
- const select = document.getElementById('model-select');
406
- select.innerHTML = '';
407
- if (!data.data || !Array.isArray(data.data)) {
408
- select.innerHTML = '<option>No models found (malformed response)</option>';
409
- return;
410
- }
411
- if (data.data.length === 0) {
412
- select.innerHTML = '<option>No models available</option>';
413
- return;
414
- }
415
-
416
- // Filter out embedding models from chat interface
417
- const allModels = window.SERVER_MODELS || {};
418
- let filteredModels = [];
419
- let defaultIndex = 0;
420
-
421
- // Check if model is specified in URL parameters
422
- const urlModel = new URLSearchParams(window.location.search).get('model');
423
- let urlModelIndex = -1;
424
-
425
- data.data.forEach(function(model, index) {
426
- const modelId = model.id || model.name || model;
427
- const modelInfo = allModels[modelId] || {};
428
- const labels = modelInfo.labels || [];
429
-
430
- // Skip models with "embeddings" or "reranking" label
431
- if (labels.includes('embeddings') || labels.includes('reranking')) {
432
- return;
433
- }
434
-
435
- filteredModels.push(modelId);
436
- const opt = document.createElement('option');
437
- opt.value = modelId;
438
- opt.textContent = modelId;
439
-
440
- // Check if this model matches the URL parameter
441
- if (urlModel && modelId === urlModel) {
442
- urlModelIndex = filteredModels.length - 1;
443
- }
444
-
445
- // Default fallback for backwards compatibility
446
- if (modelId === 'Llama-3.2-1B-Instruct-Hybrid') {
447
- defaultIndex = filteredModels.length - 1;
448
- }
449
-
450
- select.appendChild(opt);
451
- });
452
-
453
- if (filteredModels.length === 0) {
454
- select.innerHTML = '<option>No chat models available</option>';
455
- return;
456
- }
457
-
458
- // Select the URL-specified model if found, otherwise use default
459
- if (urlModelIndex !== -1) {
460
- select.selectedIndex = urlModelIndex;
461
- console.log(`Selected model from URL parameter: ${urlModel}`);
462
- } else {
463
- select.selectedIndex = defaultIndex;
464
- if (urlModel) {
465
- console.warn(`Model '${urlModel}' specified in URL not found in available models`);
466
- }
467
- }
468
-
469
- // Update attachment button state after model is loaded
470
- updateAttachmentButtonState();
471
- } catch (e) {
472
- const select = document.getElementById('model-select');
473
- select.innerHTML = `<option>Error loading models: ${e.message}</option>`;
474
- console.error('Error loading models:', e);
475
- showErrorBanner(`Error loading models: ${e.message}`);
476
- }
477
- }
478
- loadModels();
479
-
480
- // Add model change handler to clear attachments if switching to non-vision model
481
- document.addEventListener('DOMContentLoaded', function() {
482
- const modelSelect = document.getElementById('model-select');
483
- if (modelSelect) {
484
- modelSelect.addEventListener('change', function() {
485
- const currentModel = this.value;
486
- updateAttachmentButtonState(); // Update button visual state
487
-
488
- if (attachedFiles.length > 0 && !isVisionModel(currentModel)) {
489
- if (confirm(`The selected model "${currentModel}" does not support images. Would you like to remove the attached images?`)) {
490
- clearAttachments();
491
- } else {
492
- // Find a vision model to switch back to
493
- const allModels = window.SERVER_MODELS || {};
494
- const visionModels = Array.from(this.options).filter(option =>
495
- isVisionModel(option.value)
496
- );
497
-
498
- if (visionModels.length > 0) {
499
- this.value = visionModels[0].value;
500
- updateAttachmentButtonState(); // Update button state again
501
- alert(`Switched back to "${visionModels[0].value}" which supports images.`);
502
- } else {
503
- alert('No vision models available. Images will be cleared.');
504
- clearAttachments();
505
- }
506
- }
507
- }
508
- });
509
- }
510
- });
511
-
512
- // Helper function to create model name with labels
513
- function createModelNameWithLabels(modelId, allModels) {
514
- // Create container for model name and labels
515
- const container = document.createElement('div');
516
- container.className = 'model-labels-container';
517
-
518
- // Add model name
519
- const nameSpan = document.createElement('span');
520
- nameSpan.textContent = modelId;
521
- container.appendChild(nameSpan);
522
-
523
- // Add labels if they exist
524
- const modelData = allModels[modelId];
525
- if (modelData && modelData.labels && Array.isArray(modelData.labels)) {
526
- modelData.labels.forEach(label => {
527
- const labelLower = label.toLowerCase();
528
-
529
- // Skip "hot" labels since they have their own section
530
- if (labelLower === 'hot') {
531
- return;
532
- }
533
-
534
- const labelSpan = document.createElement('span');
535
- let labelClass = 'other';
536
- if (labelLower === 'vision') {
537
- labelClass = 'vision';
538
- } else if (labelLower === 'embeddings') {
539
- labelClass = 'embeddings';
540
- } else if (labelLower === 'reasoning') {
541
- labelClass = 'reasoning';
542
- } else if (labelLower === 'reranking') {
543
- labelClass = 'reranking';
544
- } else if (labelLower === 'coding') {
545
- labelClass = 'coding';
546
- }
547
- labelSpan.className = `model-label ${labelClass}`;
548
- labelSpan.textContent = label;
549
- container.appendChild(labelSpan);
550
- });
551
- }
552
-
553
- return container;
554
- }
555
-
556
- // Helper function to render a model table section
557
- function renderModelTable(tbody, models, allModels, emptyMessage) {
558
- tbody.innerHTML = '';
559
- if (models.length === 0) {
560
- const tr = document.createElement('tr');
561
- const td = document.createElement('td');
562
- td.colSpan = 2;
563
- td.textContent = emptyMessage;
564
- td.style.textAlign = 'center';
565
- td.style.fontStyle = 'italic';
566
- td.style.color = '#666';
567
- td.style.padding = '1em';
568
- tr.appendChild(td);
569
- tbody.appendChild(tr);
570
- } else {
571
- models.forEach(mid => {
572
- const tr = document.createElement('tr');
573
- const tdName = document.createElement('td');
574
-
575
- tdName.appendChild(createModelNameWithLabels(mid, allModels));
576
- tdName.style.paddingRight = '1em';
577
- tdName.style.verticalAlign = 'middle';
578
- const tdBtn = document.createElement('td');
579
- tdBtn.style.width = '1%';
580
- tdBtn.style.verticalAlign = 'middle';
581
- const btn = document.createElement('button');
582
- btn.textContent = '+';
583
- btn.title = 'Install model';
584
- btn.onclick = async function() {
585
- btn.disabled = true;
586
- btn.textContent = 'Installing...';
587
- btn.classList.add('installing-btn');
588
- try {
589
- await httpRequest(getServerBaseUrl() + '/api/v1/pull', {
590
- method: 'POST',
591
- headers: { 'Content-Type': 'application/json' },
592
- body: JSON.stringify({ model_name: mid })
593
- });
594
- await refreshModelMgmtUI();
595
- await loadModels(); // update chat dropdown too
596
- } catch (e) {
597
- btn.textContent = 'Error';
598
- showErrorBanner(`Failed to install model: ${e.message}`);
599
- }
600
- };
601
- tdBtn.appendChild(btn);
602
- tr.appendChild(tdName);
603
- tr.appendChild(tdBtn);
604
- tbody.appendChild(tr);
605
- });
606
- }
607
- }
608
-
609
- // Model Management Tab Logic
610
- async function refreshModelMgmtUI() {
611
- // Get installed models from /api/v1/models
612
- let installed = [];
613
- try {
614
- const data = await httpJson(getServerBaseUrl() + '/api/v1/models');
615
- if (data.data && Array.isArray(data.data)) {
616
- installed = data.data.map(m => m.id || m.name || m);
617
- }
618
- } catch (e) {
619
- showErrorBanner(`Error loading models: ${e.message}`);
620
- }
621
- // All models from server_models.json (window.SERVER_MODELS)
622
- const allModels = window.SERVER_MODELS || {};
623
-
624
- // Separate hot models and regular suggested models not installed
625
- const hotModels = [];
626
- const regularSuggested = [];
627
-
628
- Object.keys(allModels).forEach(k => {
629
- if (allModels[k].suggested && !installed.includes(k)) {
630
- const modelData = allModels[k];
631
- const hasHotLabel = modelData.labels && modelData.labels.some(label =>
632
- label.toLowerCase() === 'hot'
633
- );
634
-
635
- if (hasHotLabel) {
636
- hotModels.push(k);
637
- } else {
638
- regularSuggested.push(k);
639
- }
640
- }
641
- });
642
- // Render installed models as a table (two columns, second is invisible)
643
- const installedTbody = document.getElementById('installed-models-tbody');
644
- installedTbody.innerHTML = '';
645
- installed.forEach(function(mid) {
646
- var tr = document.createElement('tr');
647
- var tdName = document.createElement('td');
648
-
649
- tdName.appendChild(createModelNameWithLabels(mid, allModels));
650
- tdName.style.paddingRight = '1em';
651
- tdName.style.verticalAlign = 'middle';
652
-
653
- var tdBtn = document.createElement('td');
654
- tdBtn.style.width = '1%';
655
- tdBtn.style.verticalAlign = 'middle';
656
- const btn = document.createElement('button');
657
- btn.textContent = '−';
658
- btn.title = 'Delete model';
659
- btn.style.cursor = 'pointer';
660
- btn.onclick = async function() {
661
- if (!confirm(`Are you sure you want to delete the model "${mid}"?`)) {
662
- return;
663
- }
664
- btn.disabled = true;
665
- btn.textContent = 'Deleting...';
666
- btn.style.backgroundColor = '#888';
667
- try {
668
- await httpRequest(getServerBaseUrl() + '/api/v1/delete', {
669
- method: 'POST',
670
- headers: { 'Content-Type': 'application/json' },
671
- body: JSON.stringify({ model_name: mid })
672
- });
673
- await refreshModelMgmtUI();
674
- await loadModels(); // update chat dropdown too
675
- } catch (e) {
676
- btn.textContent = 'Error';
677
- btn.disabled = false;
678
- showErrorBanner(`Failed to delete model: ${e.message}`);
679
- }
680
- };
681
- tdBtn.appendChild(btn);
682
-
683
- tr.appendChild(tdName);
684
- tr.appendChild(tdBtn);
685
- installedTbody.appendChild(tr);
686
- });
687
-
688
- // Render hot models and suggested models using the helper function
689
- const hotTbody = document.getElementById('hot-models-tbody');
690
- const suggestedTbody = document.getElementById('suggested-models-tbody');
691
-
692
- renderModelTable(hotTbody, hotModels, allModels, "Nice, you've already installed all these models!");
693
- renderModelTable(suggestedTbody, regularSuggested, allModels, "Nice, you've already installed all these models!");
694
- }
695
- // Initial load
696
- refreshModelMgmtUI();
697
- // Optionally, refresh when switching to the tab
698
- document.getElementById('tab-models').addEventListener('click', refreshModelMgmtUI);
699
-
700
- // Chat logic (streaming with OpenAI JS client placeholder)
701
- const chatHistory = document.getElementById('chat-history');
702
- const chatInput = document.getElementById('chat-input');
703
- const sendBtn = document.getElementById('send-btn');
704
- const attachmentBtn = document.getElementById('attachment-btn');
705
- const fileAttachment = document.getElementById('file-attachment');
706
- const attachmentsPreviewContainer = document.getElementById('attachments-preview-container');
707
- const attachmentsPreviewRow = document.getElementById('attachments-preview-row');
708
- const modelSelect = document.getElementById('model-select');
709
- let messages = [];
710
- let attachedFiles = [];
711
-
712
- attachmentBtn.onclick = () => {
713
- const currentModel = modelSelect.value;
714
- if (!isVisionModel(currentModel)) {
715
- alert(`The selected model "${currentModel}" does not support image inputs. Please select a model with "Vision" capabilities to attach images.`);
716
- return;
717
- }
718
- fileAttachment.click();
719
- };
720
-
721
- function clearAttachments() {
722
- attachedFiles = [];
723
- fileAttachment.value = '';
724
- updateInputPlaceholder();
725
- updateAttachmentPreviewVisibility();
726
- updateAttachmentPreviews();
727
- }
728
-
729
- function updateAttachmentPreviewVisibility() {
730
- if (attachedFiles.length > 0) {
731
- attachmentsPreviewContainer.classList.add('has-attachments');
732
- } else {
733
- attachmentsPreviewContainer.classList.remove('has-attachments');
734
- }
735
- }
736
-
737
- function updateAttachmentPreviews() {
738
- // Clear existing previews
739
- attachmentsPreviewRow.innerHTML = '';
740
-
741
- if (attachedFiles.length === 0) {
742
- return;
743
- }
744
-
745
- attachedFiles.forEach((file, index) => {
746
- // Skip non-image files (extra safety check)
747
- if (!file.type.startsWith('image/')) {
748
- console.warn(`Skipping non-image file in preview: ${file.name} (${file.type})`);
749
- return;
750
- }
751
-
752
- const previewDiv = document.createElement('div');
753
- previewDiv.className = 'attachment-preview';
754
-
755
- // Create thumbnail
756
- const thumbnail = document.createElement('img');
757
- thumbnail.className = 'attachment-thumbnail';
758
- thumbnail.alt = file.name;
759
-
760
- // Create filename display
761
- const filename = document.createElement('div');
762
- filename.className = 'attachment-filename';
763
- filename.textContent = file.name || `pasted-image-${index + 1}`;
764
- filename.title = file.name || `pasted-image-${index + 1}`;
765
-
766
- // Create remove button
767
- const removeBtn = document.createElement('button');
768
- removeBtn.className = 'attachment-remove-btn';
769
- removeBtn.innerHTML = '✕';
770
- removeBtn.title = 'Remove this image';
771
- removeBtn.onclick = () => removeAttachment(index);
772
-
773
- // Generate thumbnail for image
774
- const reader = new FileReader();
775
- reader.onload = (e) => {
776
- thumbnail.src = e.target.result;
777
- };
778
- reader.readAsDataURL(file);
779
-
780
- previewDiv.appendChild(thumbnail);
781
- previewDiv.appendChild(filename);
782
- previewDiv.appendChild(removeBtn);
783
- attachmentsPreviewRow.appendChild(previewDiv);
784
- });
785
- }
786
-
787
- function removeAttachment(index) {
788
- attachedFiles.splice(index, 1);
789
- updateInputPlaceholder();
790
- updateAttachmentPreviewVisibility();
791
- updateAttachmentPreviews();
792
- }
793
-
794
- fileAttachment.addEventListener('change', () => {
795
- if (fileAttachment.files.length > 0) {
796
- // Check if current model supports vision
797
- const currentModel = modelSelect.value;
798
- if (!isVisionModel(currentModel)) {
799
- alert(`The selected model "${currentModel}" does not support image inputs. Please select a model with "Vision" capabilities or choose a different model.`);
800
- fileAttachment.value = ''; // Clear the input
801
- return;
802
- }
803
-
804
- // Filter only image files
805
- const imageFiles = Array.from(fileAttachment.files).filter(file => {
806
- if (!file.type.startsWith('image/')) {
807
- console.warn(`Skipping non-image file: ${file.name} (${file.type})`);
808
- return false;
809
- }
810
- return true;
811
- });
812
-
813
- if (imageFiles.length === 0) {
814
- alert('Please select only image files (PNG, JPG, GIF, etc.)');
815
- fileAttachment.value = ''; // Clear the input
816
- return;
817
- }
818
-
819
- if (imageFiles.length !== fileAttachment.files.length) {
820
- alert(`${fileAttachment.files.length - imageFiles.length} non-image file(s) were skipped. Only image files are supported.`);
821
- }
822
-
823
- attachedFiles = imageFiles;
824
- updateInputPlaceholder();
825
- updateAttachmentPreviewVisibility();
826
- updateAttachmentPreviews();
827
- }
828
- });
829
-
830
- // Handle paste events for images
831
- chatInput.addEventListener('paste', async (e) => {
832
- e.preventDefault();
833
-
834
- const clipboardData = e.clipboardData || window.clipboardData;
835
- const items = clipboardData.items;
836
- let hasImage = false;
837
- let pastedText = '';
838
-
839
- // Check for text content first
840
- for (let item of items) {
841
- if (item.type === 'text/plain') {
842
- pastedText = clipboardData.getData('text/plain');
843
- }
844
- }
845
-
846
- // Check for images
847
- for (let item of items) {
848
- if (item.type.indexOf('image') !== -1) {
849
- hasImage = true;
850
- const file = item.getAsFile();
851
- if (file && file.type.startsWith('image/')) {
852
- // Check if current model supports vision before adding image
853
- const currentModel = modelSelect.value;
854
- if (!isVisionModel(currentModel)) {
855
- alert(`The selected model "${currentModel}" does not support image inputs. Please select a model with "Vision" capabilities to paste images.`);
856
- // Only paste text, skip the image
857
- if (pastedText) {
858
- chatInput.value = pastedText;
859
- }
860
- return;
861
- }
862
- // Add to attachedFiles array only if it's an image and model supports vision
863
- attachedFiles.push(file);
864
- } else if (file) {
865
- console.warn(`Skipping non-image pasted file: ${file.name || 'unknown'} (${file.type})`);
866
- }
867
- }
868
- }
869
-
870
- // Update input box content - only show text, images will be indicated separately
871
- if (pastedText) {
872
- chatInput.value = pastedText;
873
- }
874
-
875
- // Update placeholder to show attached images
876
- updateInputPlaceholder();
877
- updateAttachmentPreviewVisibility();
878
- updateAttachmentPreviews();
879
- });
880
-
881
- // Function to update input placeholder to show attached files
882
- function updateInputPlaceholder() {
883
- if (attachedFiles.length > 0) {
884
- chatInput.placeholder = `Type your message... (${attachedFiles.length} image${attachedFiles.length > 1 ? 's' : ''} attached)`;
885
- } else {
886
- chatInput.placeholder = 'Type your message...';
887
- }
888
- }
889
-
890
- // Add keyboard shortcut to clear attachments
891
- chatInput.addEventListener('keydown', function(e) {
892
- if (e.key === 'Escape' && attachedFiles.length > 0) {
893
- e.preventDefault();
894
- clearAttachments();
895
- } else if (e.key === 'Enter') {
896
- sendMessage();
897
- }
898
- });
899
-
900
- // Function to convert file to base64
901
- function fileToBase64(file) {
902
- return new Promise((resolve, reject) => {
903
- const reader = new FileReader();
904
- reader.readAsDataURL(file);
905
- reader.onload = () => resolve(reader.result.split(',')[1]); // Remove data:image/...;base64, prefix
906
- reader.onerror = error => reject(error);
907
- });
908
- }
909
-
910
- function appendMessage(role, text, isMarkdown = false) {
911
- const div = document.createElement('div');
912
- div.className = 'chat-message ' + role;
913
- // Add a bubble for iMessage style
914
- const bubble = document.createElement('div');
915
- bubble.className = 'chat-bubble ' + role;
916
-
917
- if (role === 'llm' && isMarkdown) {
918
- bubble.innerHTML = renderMarkdownWithThinkTokens(text);
919
- } else {
920
- bubble.textContent = text;
921
- }
922
-
923
- div.appendChild(bubble);
924
- chatHistory.appendChild(div);
925
- chatHistory.scrollTop = chatHistory.scrollHeight;
926
- return bubble; // Return the bubble element for streaming updates
927
- }
928
-
929
- function updateMessageContent(bubbleElement, text, isMarkdown = false) {
930
- if (isMarkdown) {
931
- bubbleElement.innerHTML = renderMarkdownWithThinkTokens(text);
932
- } else {
933
- bubbleElement.textContent = text;
934
- }
935
- }
936
-
937
- function renderMarkdownWithThinkTokens(text) {
938
- // Check if text contains opening think tag
939
- if (text.includes('<think>')) {
940
- if (text.includes('</think>')) {
941
- // Complete think block - handle as before
942
- const thinkMatch = text.match(/<think>(.*?)<\/think>/s);
943
- if (thinkMatch) {
944
- const thinkContent = thinkMatch[1].trim();
945
- const mainResponse = text.replace(/<think>.*?<\/think>/s, '').trim();
946
-
947
- // Create collapsible structure
948
- let html = '';
949
- if (thinkContent) {
950
- html += `
951
- <div class="think-tokens-container">
952
- <div class="think-tokens-header" onclick="toggleThinkTokens(this)">
953
- <span class="think-tokens-chevron">▼</span>
954
- <span class="think-tokens-label">Thinking...</span>
955
- </div>
956
- <div class="think-tokens-content">
957
- ${renderMarkdown(thinkContent)}
958
- </div>
959
- </div>
960
- `;
961
- }
962
- if (mainResponse) {
963
- html += `<div class="main-response">${renderMarkdown(mainResponse)}</div>`;
964
- }
965
- return html;
966
- }
967
- } else {
968
- // Partial think block - only opening tag found, still being generated
969
- const thinkMatch = text.match(/<think>(.*)/s);
970
- if (thinkMatch) {
971
- const thinkContent = thinkMatch[1];
972
- const beforeThink = text.substring(0, text.indexOf('<think>'));
973
-
974
- let html = '';
975
- if (beforeThink.trim()) {
976
- html += `<div class="main-response">${renderMarkdown(beforeThink)}</div>`;
977
- }
978
-
979
- html += `
980
- <div class="think-tokens-container">
981
- <div class="think-tokens-header" onclick="toggleThinkTokens(this)">
982
- <span class="think-tokens-chevron">▼</span>
983
- <span class="think-tokens-label">Thinking...</span>
984
- </div>
985
- <div class="think-tokens-content">
986
- ${renderMarkdown(thinkContent)}
987
- </div>
988
- </div>
989
- `;
990
-
991
- return html;
992
- }
993
- }
994
- }
995
-
996
- // Fallback to normal markdown rendering
997
- return renderMarkdown(text);
998
- }
999
-
1000
- function toggleThinkTokens(header) {
1001
- const container = header.parentElement;
1002
- const content = container.querySelector('.think-tokens-content');
1003
- const chevron = header.querySelector('.think-tokens-chevron');
1004
-
1005
- if (content.style.display === 'none') {
1006
- content.style.display = 'block';
1007
- chevron.textContent = '▼';
1008
- container.classList.remove('collapsed');
1009
- } else {
1010
- content.style.display = 'none';
1011
- chevron.textContent = '▶';
1012
- container.classList.add('collapsed');
1013
- }
1014
- }
1015
-
1016
- async function sendMessage() {
1017
- const text = chatInput.value.trim();
1018
- if (!text && attachedFiles.length === 0) return;
1019
-
1020
- // Check if trying to send images to non-vision model
1021
- if (attachedFiles.length > 0) {
1022
- const currentModel = modelSelect.value;
1023
- if (!isVisionModel(currentModel)) {
1024
- alert(`Cannot send images to model "${currentModel}" as it does not support vision. Please select a model with "Vision" capabilities or remove the attached images.`);
1025
- return;
1026
- }
1027
- }
1028
-
1029
- // Create message content
1030
- let messageContent = [];
1031
-
1032
- // Add text if present
1033
- if (text) {
1034
- messageContent.push({
1035
- type: "text",
1036
- text: text
1037
- });
1038
- }
1039
-
1040
- // Add images if present
1041
- if (attachedFiles.length > 0) {
1042
- for (const file of attachedFiles) {
1043
- if (file.type.startsWith('image/')) {
1044
- try {
1045
- const base64 = await fileToBase64(file);
1046
- messageContent.push({
1047
- type: "image_url",
1048
- image_url: {
1049
- url: `data:${file.type};base64,${base64}`
1050
- }
1051
- });
1052
- } catch (error) {
1053
- console.error('Error converting image to base64:', error);
1054
- }
1055
- }
1056
- }
1057
- }
1058
-
1059
- // Display user message (show text and file names)
1060
- let displayText = text;
1061
- if (attachedFiles.length > 0) {
1062
- const fileNames = attachedFiles.map(f => f.name || 'pasted-image').join(', ');
1063
- displayText = displayText ? `${displayText}\n[Images: ${fileNames}]` : `[Images: ${fileNames}]`;
1064
- }
1065
-
1066
- appendMessage('user', displayText);
1067
-
1068
- // Add to messages array
1069
- const userMessage = {
1070
- role: 'user',
1071
- content: messageContent.length === 1 && messageContent[0].type === "text"
1072
- ? messageContent[0].text
1073
- : messageContent
1074
- };
1075
- messages.push(userMessage);
1076
-
1077
- // Clear input and attachments
1078
- chatInput.value = '';
1079
- attachedFiles = [];
1080
- fileAttachment.value = '';
1081
- updateInputPlaceholder(); // Reset placeholder
1082
- updateAttachmentPreviewVisibility(); // Hide preview container
1083
- updateAttachmentPreviews(); // Clear previews
1084
- sendBtn.disabled = true;
1085
-
1086
- // Streaming OpenAI completions (placeholder, adapt as needed)
1087
- let llmText = '';
1088
- const llmBubble = appendMessage('llm', '...');
1089
- try {
1090
- // Use the correct endpoint for chat completions
1091
- const payload = {
1092
- model: modelSelect.value,
1093
- messages: messages,
1094
- stream: true
1095
- };
1096
- const resp = await httpRequest(getServerBaseUrl() + '/api/v1/chat/completions', {
1097
- method: 'POST',
1098
- headers: { 'Content-Type': 'application/json' },
1099
- body: JSON.stringify(payload)
1100
- });
1101
- if (!resp.body) throw new Error('No stream');
1102
- const reader = resp.body.getReader();
1103
- let decoder = new TextDecoder();
1104
- llmBubble.textContent = '';
1105
- while (true) {
1106
- const { done, value } = await reader.read();
1107
- if (done) break;
1108
- const chunk = decoder.decode(value);
1109
- if (chunk.trim() === 'data: [DONE]' || chunk.trim() === '[DONE]') continue;
1110
-
1111
- // Handle Server-Sent Events format
1112
- const lines = chunk.split('\n');
1113
- for (const line of lines) {
1114
- if (line.startsWith('data: ')) {
1115
- const jsonStr = line.substring(6).trim();
1116
- if (jsonStr === '[DONE]') continue;
1117
-
1118
- try {
1119
- const parsed = JSON.parse(jsonStr);
1120
- if (parsed.choices && parsed.choices[0] && parsed.choices[0].delta && parsed.choices[0].delta.content) {
1121
- llmText += parsed.choices[0].delta.content;
1122
- updateMessageContent(llmBubble, llmText, true);
1123
- }
1124
- } catch (e) {
1125
- // Fallback to regex parsing if JSON parsing fails
1126
- const match = jsonStr.match(/"content"\s*:\s*"((?:\\.|[^"\\])*)"/);
1127
- if (match && match[1]) {
1128
- llmText += unescapeJsonString(match[1]);
1129
- updateMessageContent(llmBubble, llmText, true);
1130
- }
1131
- }
1132
- }
1133
- }
1134
- }
1135
- if (!llmText) throw new Error('No response');
1136
- messages.push({ role: 'assistant', content: llmText });
1137
- } catch (e) {
1138
- let detail = e.message;
1139
- try {
1140
- const errPayload = { ...payload, stream: false };
1141
- const errResp = await httpJson(getServerBaseUrl() + '/api/v1/chat/completions', {
1142
- method: 'POST',
1143
- headers: { 'Content-Type': 'application/json' },
1144
- body: JSON.stringify(errPayload)
1145
- });
1146
- if (errResp && errResp.detail) detail = errResp.detail;
1147
- } catch (_) {}
1148
- llmBubble.textContent = '[Error: ' + detail + ']';
1149
- showErrorBanner(`Chat error: ${detail}`);
1150
- }
1151
- sendBtn.disabled = false;
1152
- }
1153
- sendBtn.onclick = sendMessage;
1154
-
1155
- // Register & Install Model logic
1156
- const registerForm = document.getElementById('register-model-form');
1157
- const registerStatus = document.getElementById('register-model-status');
1158
- if (registerForm) {
1159
- registerForm.onsubmit = async function(e) {
1160
- e.preventDefault();
1161
- registerStatus.textContent = '';
1162
- let name = document.getElementById('register-model-name').value.trim();
1163
- // Always prepend 'user.' if not already present
1164
- if (!name.startsWith('user.')) {
1165
- name = 'user.' + name;
1166
- }
1167
- const checkpoint = document.getElementById('register-checkpoint').value.trim();
1168
- const recipe = document.getElementById('register-recipe').value;
1169
- const reasoning = document.getElementById('register-reasoning').checked;
1170
- const mmproj = document.getElementById('register-mmproj').value.trim();
1171
- if (!name || !recipe) { return; }
1172
- const payload = { model_name: name, recipe, reasoning };
1173
- if (checkpoint) payload.checkpoint = checkpoint;
1174
- if (mmproj) payload.mmproj = mmproj;
1175
- const btn = document.getElementById('register-submit');
1176
- btn.disabled = true;
1177
- btn.textContent = 'Installing...';
1178
- try {
1179
- await httpRequest(getServerBaseUrl() + '/api/v1/pull', {
1180
- method: 'POST',
1181
- headers: { 'Content-Type': 'application/json' },
1182
- body: JSON.stringify(payload)
1183
- });
1184
- registerStatus.textContent = 'Model installed!';
1185
- registerStatus.style.color = '#27ae60';
1186
- registerStatus.className = 'register-status success';
1187
- registerForm.reset();
1188
- await refreshModelMgmtUI();
1189
- await loadModels(); // update chat dropdown too
1190
- } catch (e) {
1191
- registerStatus.textContent = e.message + ' See the Lemonade Server log for details.';
1192
- registerStatus.style.color = '#dc3545';
1193
- registerStatus.className = 'register-status error';
1194
- showErrorBanner(`Model install failed: ${e.message}`);
1195
- }
1196
- btn.disabled = false;
1197
- btn.textContent = 'Install';
1198
- refreshModelMgmtUI();
1199
- };
1200
- }
1201
- </script>
250
+
251
+ <!-- Application JavaScript -->
252
+ <script src="/static/js/shared.js"></script>
253
+ <script src="/static/js/models.js"></script>
254
+ <script src="/static/js/model-settings.js"></script>
255
+ <script src="/static/js/chat.js"></script>
1202
256
  </body>
1203
257
  </html>