lemonade-sdk 8.1.2__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,1035 +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
- const host = window.location.hostname || 'localhost';
373
- return `http://${host}:${port}`;
374
- }
375
-
376
- // Check if current model supports vision
377
- function isVisionModel(modelId) {
378
- const allModels = window.SERVER_MODELS || {};
379
- const modelData = allModels[modelId];
380
- if (modelData && modelData.labels && Array.isArray(modelData.labels)) {
381
- return modelData.labels.some(label => label.toLowerCase() === 'vision');
382
- }
383
- return false;
384
- }
385
-
386
- // Update attachment button state based on current model
387
- function updateAttachmentButtonState() {
388
- const currentModel = modelSelect.value;
389
- const isVision = isVisionModel(currentModel);
390
-
391
- if (isVision) {
392
- attachmentBtn.style.opacity = '1';
393
- attachmentBtn.style.cursor = 'pointer';
394
- attachmentBtn.title = 'Attach images';
395
- } else {
396
- attachmentBtn.style.opacity = '0.5';
397
- attachmentBtn.style.cursor = 'not-allowed';
398
- attachmentBtn.title = 'Image attachments not supported by this model';
399
- }
400
- }
401
-
402
- // Populate model dropdown from /api/v1/models endpoint
403
- async function loadModels() {
404
- try {
405
- const data = await httpJson(getServerBaseUrl() + '/api/v1/models');
406
- const select = document.getElementById('model-select');
407
- select.innerHTML = '';
408
- if (!data.data || !Array.isArray(data.data)) {
409
- select.innerHTML = '<option>No models found (malformed response)</option>';
410
- return;
411
- }
412
- if (data.data.length === 0) {
413
- select.innerHTML = '<option>No models available</option>';
414
- return;
415
- }
416
-
417
- // Filter out embedding models from chat interface
418
- const allModels = window.SERVER_MODELS || {};
419
- let filteredModels = [];
420
- let defaultIndex = 0;
421
-
422
- // Check if model is specified in URL parameters
423
- const urlModel = new URLSearchParams(window.location.search).get('model');
424
- let urlModelIndex = -1;
425
-
426
- data.data.forEach(function(model, index) {
427
- const modelId = model.id || model.name || model;
428
- const modelInfo = allModels[modelId] || {};
429
- const labels = modelInfo.labels || [];
430
-
431
- // Skip models with "embeddings" or "reranking" label
432
- if (labels.includes('embeddings') || labels.includes('reranking')) {
433
- return;
434
- }
435
-
436
- filteredModels.push(modelId);
437
- const opt = document.createElement('option');
438
- opt.value = modelId;
439
- opt.textContent = modelId;
440
-
441
- // Check if this model matches the URL parameter
442
- if (urlModel && modelId === urlModel) {
443
- urlModelIndex = filteredModels.length - 1;
444
- }
445
-
446
- // Default fallback for backwards compatibility
447
- if (modelId === 'Llama-3.2-1B-Instruct-Hybrid') {
448
- defaultIndex = filteredModels.length - 1;
449
- }
450
-
451
- select.appendChild(opt);
452
- });
453
-
454
- if (filteredModels.length === 0) {
455
- select.innerHTML = '<option>No chat models available</option>';
456
- return;
457
- }
458
-
459
- // Select the URL-specified model if found, otherwise use default
460
- if (urlModelIndex !== -1) {
461
- select.selectedIndex = urlModelIndex;
462
- console.log(`Selected model from URL parameter: ${urlModel}`);
463
- } else {
464
- select.selectedIndex = defaultIndex;
465
- if (urlModel) {
466
- console.warn(`Model '${urlModel}' specified in URL not found in available models`);
467
- }
468
- }
469
-
470
- // Update attachment button state after model is loaded
471
- updateAttachmentButtonState();
472
- } catch (e) {
473
- const select = document.getElementById('model-select');
474
- select.innerHTML = `<option>Error loading models: ${e.message}</option>`;
475
- console.error('Error loading models:', e);
476
- showErrorBanner(`Error loading models: ${e.message}`);
477
- }
478
- }
479
- loadModels();
480
-
481
- // Add model change handler to clear attachments if switching to non-vision model
482
- document.addEventListener('DOMContentLoaded', function() {
483
- const modelSelect = document.getElementById('model-select');
484
- if (modelSelect) {
485
- modelSelect.addEventListener('change', function() {
486
- const currentModel = this.value;
487
- updateAttachmentButtonState(); // Update button visual state
488
-
489
- if (attachedFiles.length > 0 && !isVisionModel(currentModel)) {
490
- if (confirm(`The selected model "${currentModel}" does not support images. Would you like to remove the attached images?`)) {
491
- clearAttachments();
492
- } else {
493
- // Find a vision model to switch back to
494
- const allModels = window.SERVER_MODELS || {};
495
- const visionModels = Array.from(this.options).filter(option =>
496
- isVisionModel(option.value)
497
- );
498
-
499
- if (visionModels.length > 0) {
500
- this.value = visionModels[0].value;
501
- updateAttachmentButtonState(); // Update button state again
502
- alert(`Switched back to "${visionModels[0].value}" which supports images.`);
503
- } else {
504
- alert('No vision models available. Images will be cleared.');
505
- clearAttachments();
506
- }
507
- }
508
- }
509
- });
510
- }
511
- });
512
-
513
- // Helper function to create model name with labels
514
- function createModelNameWithLabels(modelId, allModels) {
515
- // Create container for model name and labels
516
- const container = document.createElement('div');
517
- container.className = 'model-labels-container';
518
-
519
- // Add model name
520
- const nameSpan = document.createElement('span');
521
- nameSpan.textContent = modelId;
522
- container.appendChild(nameSpan);
523
-
524
- // Add labels if they exist
525
- const modelData = allModels[modelId];
526
- if (modelData && modelData.labels && Array.isArray(modelData.labels)) {
527
- modelData.labels.forEach(label => {
528
- const labelLower = label.toLowerCase();
529
-
530
- // Skip "hot" labels since they have their own section
531
- if (labelLower === 'hot') {
532
- return;
533
- }
534
-
535
- const labelSpan = document.createElement('span');
536
- let labelClass = 'other';
537
- if (labelLower === 'vision') {
538
- labelClass = 'vision';
539
- } else if (labelLower === 'embeddings') {
540
- labelClass = 'embeddings';
541
- } else if (labelLower === 'reasoning') {
542
- labelClass = 'reasoning';
543
- } else if (labelLower === 'reranking') {
544
- labelClass = 'reranking';
545
- } else if (labelLower === 'coding') {
546
- labelClass = 'coding';
547
- }
548
- labelSpan.className = `model-label ${labelClass}`;
549
- labelSpan.textContent = label;
550
- container.appendChild(labelSpan);
551
- });
552
- }
553
-
554
- return container;
555
- }
556
-
557
- // Helper function to render a model table section
558
- function renderModelTable(tbody, models, allModels, emptyMessage) {
559
- tbody.innerHTML = '';
560
- if (models.length === 0) {
561
- const tr = document.createElement('tr');
562
- const td = document.createElement('td');
563
- td.colSpan = 2;
564
- td.textContent = emptyMessage;
565
- td.style.textAlign = 'center';
566
- td.style.fontStyle = 'italic';
567
- td.style.color = '#666';
568
- td.style.padding = '1em';
569
- tr.appendChild(td);
570
- tbody.appendChild(tr);
571
- } else {
572
- models.forEach(mid => {
573
- const tr = document.createElement('tr');
574
- const tdName = document.createElement('td');
575
-
576
- tdName.appendChild(createModelNameWithLabels(mid, allModels));
577
- tdName.style.paddingRight = '1em';
578
- tdName.style.verticalAlign = 'middle';
579
- const tdBtn = document.createElement('td');
580
- tdBtn.style.width = '1%';
581
- tdBtn.style.verticalAlign = 'middle';
582
- const btn = document.createElement('button');
583
- btn.textContent = '+';
584
- btn.title = 'Install model';
585
- btn.onclick = async function() {
586
- btn.disabled = true;
587
- btn.textContent = 'Installing...';
588
- btn.classList.add('installing-btn');
589
- try {
590
- await httpRequest(getServerBaseUrl() + '/api/v1/pull', {
591
- method: 'POST',
592
- headers: { 'Content-Type': 'application/json' },
593
- body: JSON.stringify({ model_name: mid })
594
- });
595
- await refreshModelMgmtUI();
596
- await loadModels(); // update chat dropdown too
597
- } catch (e) {
598
- btn.textContent = 'Error';
599
- showErrorBanner(`Failed to install model: ${e.message}`);
600
- }
601
- };
602
- tdBtn.appendChild(btn);
603
- tr.appendChild(tdName);
604
- tr.appendChild(tdBtn);
605
- tbody.appendChild(tr);
606
- });
607
- }
608
- }
609
-
610
- // Model Management Tab Logic
611
- async function refreshModelMgmtUI() {
612
- // Get installed models from /api/v1/models
613
- let installed = [];
614
- try {
615
- const data = await httpJson(getServerBaseUrl() + '/api/v1/models');
616
- if (data.data && Array.isArray(data.data)) {
617
- installed = data.data.map(m => m.id || m.name || m);
618
- }
619
- } catch (e) {
620
- showErrorBanner(`Error loading models: ${e.message}`);
621
- }
622
- // All models from server_models.json (window.SERVER_MODELS)
623
- const allModels = window.SERVER_MODELS || {};
624
-
625
- // Separate hot models and regular suggested models not installed
626
- const hotModels = [];
627
- const regularSuggested = [];
628
-
629
- Object.keys(allModels).forEach(k => {
630
- if (allModels[k].suggested && !installed.includes(k)) {
631
- const modelData = allModels[k];
632
- const hasHotLabel = modelData.labels && modelData.labels.some(label =>
633
- label.toLowerCase() === 'hot'
634
- );
635
-
636
- if (hasHotLabel) {
637
- hotModels.push(k);
638
- } else {
639
- regularSuggested.push(k);
640
- }
641
- }
642
- });
643
- // Render installed models as a table (two columns, second is invisible)
644
- const installedTbody = document.getElementById('installed-models-tbody');
645
- installedTbody.innerHTML = '';
646
- installed.forEach(function(mid) {
647
- var tr = document.createElement('tr');
648
- var tdName = document.createElement('td');
649
-
650
- tdName.appendChild(createModelNameWithLabels(mid, allModels));
651
- tdName.style.paddingRight = '1em';
652
- tdName.style.verticalAlign = 'middle';
653
-
654
- var tdBtn = document.createElement('td');
655
- tdBtn.style.width = '1%';
656
- tdBtn.style.verticalAlign = 'middle';
657
- const btn = document.createElement('button');
658
- btn.textContent = '−';
659
- btn.title = 'Delete model';
660
- btn.style.cursor = 'pointer';
661
- btn.onclick = async function() {
662
- if (!confirm(`Are you sure you want to delete the model "${mid}"?`)) {
663
- return;
664
- }
665
- btn.disabled = true;
666
- btn.textContent = 'Deleting...';
667
- btn.style.backgroundColor = '#888';
668
- try {
669
- await httpRequest(getServerBaseUrl() + '/api/v1/delete', {
670
- method: 'POST',
671
- headers: { 'Content-Type': 'application/json' },
672
- body: JSON.stringify({ model_name: mid })
673
- });
674
- await refreshModelMgmtUI();
675
- await loadModels(); // update chat dropdown too
676
- } catch (e) {
677
- btn.textContent = 'Error';
678
- btn.disabled = false;
679
- showErrorBanner(`Failed to delete model: ${e.message}`);
680
- }
681
- };
682
- tdBtn.appendChild(btn);
683
-
684
- tr.appendChild(tdName);
685
- tr.appendChild(tdBtn);
686
- installedTbody.appendChild(tr);
687
- });
688
-
689
- // Render hot models and suggested models using the helper function
690
- const hotTbody = document.getElementById('hot-models-tbody');
691
- const suggestedTbody = document.getElementById('suggested-models-tbody');
692
-
693
- renderModelTable(hotTbody, hotModels, allModels, "Nice, you've already installed all these models!");
694
- renderModelTable(suggestedTbody, regularSuggested, allModels, "Nice, you've already installed all these models!");
695
- }
696
- // Initial load
697
- refreshModelMgmtUI();
698
- // Optionally, refresh when switching to the tab
699
- document.getElementById('tab-models').addEventListener('click', refreshModelMgmtUI);
700
-
701
- // Chat logic (streaming with OpenAI JS client placeholder)
702
- const chatHistory = document.getElementById('chat-history');
703
- const chatInput = document.getElementById('chat-input');
704
- const sendBtn = document.getElementById('send-btn');
705
- const attachmentBtn = document.getElementById('attachment-btn');
706
- const fileAttachment = document.getElementById('file-attachment');
707
- const attachmentsPreviewContainer = document.getElementById('attachments-preview-container');
708
- const attachmentsPreviewRow = document.getElementById('attachments-preview-row');
709
- const modelSelect = document.getElementById('model-select');
710
- let messages = [];
711
- let attachedFiles = [];
712
-
713
- attachmentBtn.onclick = () => {
714
- const currentModel = modelSelect.value;
715
- if (!isVisionModel(currentModel)) {
716
- alert(`The selected model "${currentModel}" does not support image inputs. Please select a model with "Vision" capabilities to attach images.`);
717
- return;
718
- }
719
- fileAttachment.click();
720
- };
721
-
722
- function clearAttachments() {
723
- attachedFiles = [];
724
- fileAttachment.value = '';
725
- updateInputPlaceholder();
726
- updateAttachmentPreviewVisibility();
727
- updateAttachmentPreviews();
728
- }
729
-
730
- function updateAttachmentPreviewVisibility() {
731
- if (attachedFiles.length > 0) {
732
- attachmentsPreviewContainer.classList.add('has-attachments');
733
- } else {
734
- attachmentsPreviewContainer.classList.remove('has-attachments');
735
- }
736
- }
737
-
738
- function updateAttachmentPreviews() {
739
- // Clear existing previews
740
- attachmentsPreviewRow.innerHTML = '';
741
-
742
- if (attachedFiles.length === 0) {
743
- return;
744
- }
745
-
746
- attachedFiles.forEach((file, index) => {
747
- // Skip non-image files (extra safety check)
748
- if (!file.type.startsWith('image/')) {
749
- console.warn(`Skipping non-image file in preview: ${file.name} (${file.type})`);
750
- return;
751
- }
752
-
753
- const previewDiv = document.createElement('div');
754
- previewDiv.className = 'attachment-preview';
755
-
756
- // Create thumbnail
757
- const thumbnail = document.createElement('img');
758
- thumbnail.className = 'attachment-thumbnail';
759
- thumbnail.alt = file.name;
760
-
761
- // Create filename display
762
- const filename = document.createElement('div');
763
- filename.className = 'attachment-filename';
764
- filename.textContent = file.name || `pasted-image-${index + 1}`;
765
- filename.title = file.name || `pasted-image-${index + 1}`;
766
-
767
- // Create remove button
768
- const removeBtn = document.createElement('button');
769
- removeBtn.className = 'attachment-remove-btn';
770
- removeBtn.innerHTML = '✕';
771
- removeBtn.title = 'Remove this image';
772
- removeBtn.onclick = () => removeAttachment(index);
773
-
774
- // Generate thumbnail for image
775
- const reader = new FileReader();
776
- reader.onload = (e) => {
777
- thumbnail.src = e.target.result;
778
- };
779
- reader.readAsDataURL(file);
780
-
781
- previewDiv.appendChild(thumbnail);
782
- previewDiv.appendChild(filename);
783
- previewDiv.appendChild(removeBtn);
784
- attachmentsPreviewRow.appendChild(previewDiv);
785
- });
786
- }
787
-
788
- function removeAttachment(index) {
789
- attachedFiles.splice(index, 1);
790
- updateInputPlaceholder();
791
- updateAttachmentPreviewVisibility();
792
- updateAttachmentPreviews();
793
- }
794
-
795
- fileAttachment.addEventListener('change', () => {
796
- if (fileAttachment.files.length > 0) {
797
- // Check if current model supports vision
798
- const currentModel = modelSelect.value;
799
- if (!isVisionModel(currentModel)) {
800
- alert(`The selected model "${currentModel}" does not support image inputs. Please select a model with "Vision" capabilities or choose a different model.`);
801
- fileAttachment.value = ''; // Clear the input
802
- return;
803
- }
804
-
805
- // Filter only image files
806
- const imageFiles = Array.from(fileAttachment.files).filter(file => {
807
- if (!file.type.startsWith('image/')) {
808
- console.warn(`Skipping non-image file: ${file.name} (${file.type})`);
809
- return false;
810
- }
811
- return true;
812
- });
813
-
814
- if (imageFiles.length === 0) {
815
- alert('Please select only image files (PNG, JPG, GIF, etc.)');
816
- fileAttachment.value = ''; // Clear the input
817
- return;
818
- }
819
-
820
- if (imageFiles.length !== fileAttachment.files.length) {
821
- alert(`${fileAttachment.files.length - imageFiles.length} non-image file(s) were skipped. Only image files are supported.`);
822
- }
823
-
824
- attachedFiles = imageFiles;
825
- updateInputPlaceholder();
826
- updateAttachmentPreviewVisibility();
827
- updateAttachmentPreviews();
828
- }
829
- });
830
-
831
- // Handle paste events for images
832
- chatInput.addEventListener('paste', async (e) => {
833
- e.preventDefault();
834
-
835
- const clipboardData = e.clipboardData || window.clipboardData;
836
- const items = clipboardData.items;
837
- let hasImage = false;
838
- let pastedText = '';
839
-
840
- // Check for text content first
841
- for (let item of items) {
842
- if (item.type === 'text/plain') {
843
- pastedText = clipboardData.getData('text/plain');
844
- }
845
- }
846
-
847
- // Check for images
848
- for (let item of items) {
849
- if (item.type.indexOf('image') !== -1) {
850
- hasImage = true;
851
- const file = item.getAsFile();
852
- if (file && file.type.startsWith('image/')) {
853
- // Check if current model supports vision before adding image
854
- const currentModel = modelSelect.value;
855
- if (!isVisionModel(currentModel)) {
856
- alert(`The selected model "${currentModel}" does not support image inputs. Please select a model with "Vision" capabilities to paste images.`);
857
- // Only paste text, skip the image
858
- if (pastedText) {
859
- chatInput.value = pastedText;
860
- }
861
- return;
862
- }
863
- // Add to attachedFiles array only if it's an image and model supports vision
864
- attachedFiles.push(file);
865
- } else if (file) {
866
- console.warn(`Skipping non-image pasted file: ${file.name || 'unknown'} (${file.type})`);
867
- }
868
- }
869
- }
870
-
871
- // Update input box content - only show text, images will be indicated separately
872
- if (pastedText) {
873
- chatInput.value = pastedText;
874
- }
875
-
876
- // Update placeholder to show attached images
877
- updateInputPlaceholder();
878
- updateAttachmentPreviewVisibility();
879
- updateAttachmentPreviews();
880
- });
881
-
882
- // Function to update input placeholder to show attached files
883
- function updateInputPlaceholder() {
884
- if (attachedFiles.length > 0) {
885
- chatInput.placeholder = `Type your message... (${attachedFiles.length} image${attachedFiles.length > 1 ? 's' : ''} attached)`;
886
- } else {
887
- chatInput.placeholder = 'Type your message...';
888
- }
889
- }
890
-
891
- // Add keyboard shortcut to clear attachments
892
- chatInput.addEventListener('keydown', function(e) {
893
- if (e.key === 'Escape' && attachedFiles.length > 0) {
894
- e.preventDefault();
895
- clearAttachments();
896
- } else if (e.key === 'Enter') {
897
- sendMessage();
898
- }
899
- });
900
-
901
- // Function to convert file to base64
902
- function fileToBase64(file) {
903
- return new Promise((resolve, reject) => {
904
- const reader = new FileReader();
905
- reader.readAsDataURL(file);
906
- reader.onload = () => resolve(reader.result.split(',')[1]); // Remove data:image/...;base64, prefix
907
- reader.onerror = error => reject(error);
908
- });
909
- }
910
-
911
- function appendMessage(role, text, isMarkdown = false) {
912
- const div = document.createElement('div');
913
- div.className = 'chat-message ' + role;
914
- // Add a bubble for iMessage style
915
- const bubble = document.createElement('div');
916
- bubble.className = 'chat-bubble ' + role;
917
-
918
- if (role === 'llm' && isMarkdown) {
919
- bubble.innerHTML = renderMarkdownWithThinkTokens(text);
920
- } else {
921
- bubble.textContent = text;
922
- }
923
-
924
- div.appendChild(bubble);
925
- chatHistory.appendChild(div);
926
- chatHistory.scrollTop = chatHistory.scrollHeight;
927
- return bubble; // Return the bubble element for streaming updates
928
- }
929
-
930
- function updateMessageContent(bubbleElement, text, isMarkdown = false) {
931
- if (isMarkdown) {
932
- bubbleElement.innerHTML = renderMarkdownWithThinkTokens(text);
933
- } else {
934
- bubbleElement.textContent = text;
935
- }
936
- }
937
-
938
- function renderMarkdownWithThinkTokens(text) {
939
- // Check if text contains opening think tag
940
- if (text.includes('<think>')) {
941
- if (text.includes('</think>')) {
942
- // Complete think block - handle as before
943
- const thinkMatch = text.match(/<think>(.*?)<\/think>/s);
944
- if (thinkMatch) {
945
- const thinkContent = thinkMatch[1].trim();
946
- const mainResponse = text.replace(/<think>.*?<\/think>/s, '').trim();
947
-
948
- // Create collapsible structure
949
- let html = '';
950
- if (thinkContent) {
951
- html += `
952
- <div class="think-tokens-container">
953
- <div class="think-tokens-header" onclick="toggleThinkTokens(this)">
954
- <span class="think-tokens-chevron">▼</span>
955
- <span class="think-tokens-label">Thinking...</span>
956
- </div>
957
- <div class="think-tokens-content">
958
- ${renderMarkdown(thinkContent)}
959
- </div>
960
- </div>
961
- `;
962
- }
963
- if (mainResponse) {
964
- html += `<div class="main-response">${renderMarkdown(mainResponse)}</div>`;
965
- }
966
- return html;
967
- }
968
- } else {
969
- // Partial think block - only opening tag found, still being generated
970
- const thinkMatch = text.match(/<think>(.*)/s);
971
- if (thinkMatch) {
972
- const thinkContent = thinkMatch[1];
973
- const beforeThink = text.substring(0, text.indexOf('<think>'));
974
-
975
- let html = '';
976
- if (beforeThink.trim()) {
977
- html += `<div class="main-response">${renderMarkdown(beforeThink)}</div>`;
978
- }
979
-
980
- html += `
981
- <div class="think-tokens-container">
982
- <div class="think-tokens-header" onclick="toggleThinkTokens(this)">
983
- <span class="think-tokens-chevron">▼</span>
984
- <span class="think-tokens-label">Thinking...</span>
985
- </div>
986
- <div class="think-tokens-content">
987
- ${renderMarkdown(thinkContent)}
988
- </div>
989
- </div>
990
- `;
991
-
992
- return html;
993
- }
994
- }
995
- }
996
-
997
- // Fallback to normal markdown rendering
998
- return renderMarkdown(text);
999
- }
1000
-
1001
- function toggleThinkTokens(header) {
1002
- const container = header.parentElement;
1003
- const content = container.querySelector('.think-tokens-content');
1004
- const chevron = header.querySelector('.think-tokens-chevron');
1005
-
1006
- if (content.style.display === 'none') {
1007
- content.style.display = 'block';
1008
- chevron.textContent = '▼';
1009
- container.classList.remove('collapsed');
1010
- } else {
1011
- content.style.display = 'none';
1012
- chevron.textContent = '▶';
1013
- container.classList.add('collapsed');
1014
- }
1015
- }
1016
-
1017
- async function sendMessage() {
1018
- const text = chatInput.value.trim();
1019
- if (!text && attachedFiles.length === 0) return;
1020
-
1021
- // Check if trying to send images to non-vision model
1022
- if (attachedFiles.length > 0) {
1023
- const currentModel = modelSelect.value;
1024
- if (!isVisionModel(currentModel)) {
1025
- 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.`);
1026
- return;
1027
- }
1028
- }
1029
-
1030
- // Create message content
1031
- let messageContent = [];
1032
-
1033
- // Add text if present
1034
- if (text) {
1035
- messageContent.push({
1036
- type: "text",
1037
- text: text
1038
- });
1039
- }
1040
-
1041
- // Add images if present
1042
- if (attachedFiles.length > 0) {
1043
- for (const file of attachedFiles) {
1044
- if (file.type.startsWith('image/')) {
1045
- try {
1046
- const base64 = await fileToBase64(file);
1047
- messageContent.push({
1048
- type: "image_url",
1049
- image_url: {
1050
- url: `data:${file.type};base64,${base64}`
1051
- }
1052
- });
1053
- } catch (error) {
1054
- console.error('Error converting image to base64:', error);
1055
- }
1056
- }
1057
- }
1058
- }
1059
-
1060
- // Display user message (show text and file names)
1061
- let displayText = text;
1062
- if (attachedFiles.length > 0) {
1063
- const fileNames = attachedFiles.map(f => f.name || 'pasted-image').join(', ');
1064
- displayText = displayText ? `${displayText}\n[Images: ${fileNames}]` : `[Images: ${fileNames}]`;
1065
- }
1066
-
1067
- appendMessage('user', displayText);
1068
-
1069
- // Add to messages array
1070
- const userMessage = {
1071
- role: 'user',
1072
- content: messageContent.length === 1 && messageContent[0].type === "text"
1073
- ? messageContent[0].text
1074
- : messageContent
1075
- };
1076
- messages.push(userMessage);
1077
-
1078
- // Clear input and attachments
1079
- chatInput.value = '';
1080
- attachedFiles = [];
1081
- fileAttachment.value = '';
1082
- updateInputPlaceholder(); // Reset placeholder
1083
- updateAttachmentPreviewVisibility(); // Hide preview container
1084
- updateAttachmentPreviews(); // Clear previews
1085
- sendBtn.disabled = true;
1086
-
1087
- // Streaming OpenAI completions (placeholder, adapt as needed)
1088
- let llmText = '';
1089
- const llmBubble = appendMessage('llm', '...');
1090
- try {
1091
- // Use the correct endpoint for chat completions
1092
- const payload = {
1093
- model: modelSelect.value,
1094
- messages: messages,
1095
- stream: true
1096
- };
1097
- const resp = await httpRequest(getServerBaseUrl() + '/api/v1/chat/completions', {
1098
- method: 'POST',
1099
- headers: { 'Content-Type': 'application/json' },
1100
- body: JSON.stringify(payload)
1101
- });
1102
- if (!resp.body) throw new Error('No stream');
1103
- const reader = resp.body.getReader();
1104
- let decoder = new TextDecoder();
1105
- llmBubble.textContent = '';
1106
- while (true) {
1107
- const { done, value } = await reader.read();
1108
- if (done) break;
1109
- const chunk = decoder.decode(value);
1110
- if (chunk.trim() === 'data: [DONE]' || chunk.trim() === '[DONE]') continue;
1111
-
1112
- // Handle Server-Sent Events format
1113
- const lines = chunk.split('\n');
1114
- for (const line of lines) {
1115
- if (line.startsWith('data: ')) {
1116
- const jsonStr = line.substring(6).trim();
1117
- if (jsonStr === '[DONE]') continue;
1118
-
1119
- try {
1120
- const parsed = JSON.parse(jsonStr);
1121
- if (parsed.choices && parsed.choices[0] && parsed.choices[0].delta && parsed.choices[0].delta.content) {
1122
- llmText += parsed.choices[0].delta.content;
1123
- updateMessageContent(llmBubble, llmText, true);
1124
- }
1125
- } catch (e) {
1126
- // Fallback to regex parsing if JSON parsing fails
1127
- const match = jsonStr.match(/"content"\s*:\s*"((?:\\.|[^"\\])*)"/);
1128
- if (match && match[1]) {
1129
- llmText += unescapeJsonString(match[1]);
1130
- updateMessageContent(llmBubble, llmText, true);
1131
- }
1132
- }
1133
- }
1134
- }
1135
- }
1136
- if (!llmText) throw new Error('No response');
1137
- messages.push({ role: 'assistant', content: llmText });
1138
- } catch (e) {
1139
- let detail = e.message;
1140
- try {
1141
- const errPayload = { ...payload, stream: false };
1142
- const errResp = await httpJson(getServerBaseUrl() + '/api/v1/chat/completions', {
1143
- method: 'POST',
1144
- headers: { 'Content-Type': 'application/json' },
1145
- body: JSON.stringify(errPayload)
1146
- });
1147
- if (errResp && errResp.detail) detail = errResp.detail;
1148
- } catch (_) {}
1149
- llmBubble.textContent = '[Error: ' + detail + ']';
1150
- showErrorBanner(`Chat error: ${detail}`);
1151
- }
1152
- sendBtn.disabled = false;
1153
- }
1154
- sendBtn.onclick = sendMessage;
1155
-
1156
- // Register & Install Model logic
1157
- const registerForm = document.getElementById('register-model-form');
1158
- const registerStatus = document.getElementById('register-model-status');
1159
- if (registerForm) {
1160
- registerForm.onsubmit = async function(e) {
1161
- e.preventDefault();
1162
- registerStatus.textContent = '';
1163
- let name = document.getElementById('register-model-name').value.trim();
1164
- // Always prepend 'user.' if not already present
1165
- if (!name.startsWith('user.')) {
1166
- name = 'user.' + name;
1167
- }
1168
- const checkpoint = document.getElementById('register-checkpoint').value.trim();
1169
- const recipe = document.getElementById('register-recipe').value;
1170
- const reasoning = document.getElementById('register-reasoning').checked;
1171
- const mmproj = document.getElementById('register-mmproj').value.trim();
1172
- if (!name || !recipe) { return; }
1173
- const payload = { model_name: name, recipe, reasoning };
1174
- if (checkpoint) payload.checkpoint = checkpoint;
1175
- if (mmproj) payload.mmproj = mmproj;
1176
- const btn = document.getElementById('register-submit');
1177
- btn.disabled = true;
1178
- btn.textContent = 'Installing...';
1179
- try {
1180
- await httpRequest(getServerBaseUrl() + '/api/v1/pull', {
1181
- method: 'POST',
1182
- headers: { 'Content-Type': 'application/json' },
1183
- body: JSON.stringify(payload)
1184
- });
1185
- registerStatus.textContent = 'Model installed!';
1186
- registerStatus.style.color = '#27ae60';
1187
- registerStatus.className = 'register-status success';
1188
- registerForm.reset();
1189
- await refreshModelMgmtUI();
1190
- await loadModels(); // update chat dropdown too
1191
- } catch (e) {
1192
- registerStatus.textContent = e.message + ' See the Lemonade Server log for details.';
1193
- registerStatus.style.color = '#dc3545';
1194
- registerStatus.className = 'register-status error';
1195
- showErrorBanner(`Model install failed: ${e.message}`);
1196
- }
1197
- btn.disabled = false;
1198
- btn.textContent = 'Install';
1199
- refreshModelMgmtUI();
1200
- };
1201
- }
1202
- </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>
1203
256
  </body>
1204
257
  </html>