cfalk43 commited on
Commit
1ddfd0a
·
verified ·
1 Parent(s): 272add3

Upload index.js with huggingface_hub

Browse files
Files changed (1) hide show
  1. index.js +425 -56
index.js CHANGED
@@ -1,76 +1,445 @@
1
- import { pipeline } from 'https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.7.6';
2
 
3
- // Reference the elements that we will need
4
- const status = document.getElementById('status');
5
- const fileUpload = document.getElementById('upload');
6
- const imageContainer = document.getElementById('container');
7
- const example = document.getElementById('example');
8
 
9
- const EXAMPLE_URL = 'https://huggingface.co/datasets/Xenova/transformers.js-docs/resolve/main/city-streets.jpg';
 
 
 
 
 
 
10
 
11
- // Create a new object detection pipeline
12
- status.textContent = 'Loading model...';
13
- const detector = await pipeline('object-detection', 'Xenova/detr-resnet-50');
14
- status.textContent = 'Ready';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
 
16
- example.addEventListener('click', (e) => {
17
- e.preventDefault();
18
- detect(EXAMPLE_URL);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  });
20
 
21
- fileUpload.addEventListener('change', function (e) {
22
- const file = e.target.files[0];
23
- if (!file) {
24
- return;
25
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
 
27
- const reader = new FileReader();
 
 
 
 
28
 
29
- // Set up a callback when the file is loaded
30
- reader.onload = e2 => detect(e2.target.result);
31
 
32
- reader.readAsDataURL(file);
 
 
 
33
  });
34
 
 
35
 
36
- // Detect objects in the image
37
- async function detect(img) {
38
- imageContainer.innerHTML = '';
39
- imageContainer.style.backgroundImage = `url(${img})`;
40
 
41
- status.textContent = 'Analysing...';
42
- const output = await detector(img, {
43
- threshold: 0.5,
44
- percentage: true,
45
- });
46
- status.textContent = '';
47
- output.forEach(renderBox);
48
  }
 
49
 
50
- // Render a bounding box and label on the image
51
- function renderBox({ box, label }) {
52
- const { xmax, xmin, ymax, ymin } = box;
53
 
54
- // Generate a random color for the box
55
- const color = '#' + Math.floor(Math.random() * 0xFFFFFF).toString(16).padStart(6, 0);
 
 
56
 
57
- // Draw the box
58
- const boxElement = document.createElement('div');
59
- boxElement.className = 'bounding-box';
60
- Object.assign(boxElement.style, {
61
- borderColor: color,
62
- left: 100 * xmin + '%',
63
- top: 100 * ymin + '%',
64
- width: 100 * (xmax - xmin) + '%',
65
- height: 100 * (ymax - ymin) + '%',
66
- })
67
 
68
- // Draw label
69
- const labelElement = document.createElement('span');
70
- labelElement.textContent = label;
71
- labelElement.className = 'bounding-box-label';
72
- labelElement.style.backgroundColor = color;
73
 
74
- boxElement.appendChild(labelElement);
75
- imageContainer.appendChild(boxElement);
 
 
 
76
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { pipeline, env } from 'https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.8.0';
2
 
3
+ // Configure environment
4
+ env.allowLocalModels = false;
 
 
 
5
 
6
+ // State
7
+ let generator = null;
8
+ let currentModel = null;
9
+ let isVisionModel = false;
10
+ let currentImage = null;
11
+ let isGenerating = false;
12
+ let conversationHistory = [];
13
 
14
+ // DOM Elements
15
+ const modelSelect = document.getElementById('model-select');
16
+ const loadModelBtn = document.getElementById('load-model-btn');
17
+ const loadingContainer = document.getElementById('loading-container');
18
+ const loadingStatus = document.getElementById('loading-status');
19
+ const progressBar = document.getElementById('progress-bar');
20
+ const progressText = document.getElementById('progress-text');
21
+ const chatContainer = document.getElementById('chat-container');
22
+ const chatMessages = document.getElementById('chat-messages');
23
+ const imageInput = document.getElementById('image-input');
24
+ const attachImageBtn = document.getElementById('attach-image-btn');
25
+ const imageUrlInput = document.getElementById('image-url-input');
26
+ const loadUrlBtn = document.getElementById('load-url-btn');
27
+ const imagePreviewContainer = document.getElementById('image-preview-container');
28
+ const imagePreview = document.getElementById('image-preview');
29
+ const removeImageBtn = document.getElementById('remove-image-btn');
30
+ const userInput = document.getElementById('user-input');
31
+ const sendBtn = document.getElementById('send-btn');
32
+ const errorContainer = document.getElementById('error-container');
33
+ const errorMessage = document.getElementById('error-message');
34
+ const dismissErrorBtn = document.getElementById('dismiss-error-btn');
35
 
36
+ // Settings
37
+ const maxTokensSlider = document.getElementById('max-tokens');
38
+ const maxTokensValue = document.getElementById('max-tokens-value');
39
+ const temperatureSlider = document.getElementById('temperature');
40
+ const temperatureValue = document.getElementById('temperature-value');
41
+ const topPSlider = document.getElementById('top-p');
42
+ const topPValue = document.getElementById('top-p-value');
43
+
44
+ // Vision model identifiers
45
+ const VISION_MODELS = ['SmolVLM', 'Fara', 'llava', 'vision'];
46
+
47
+ function isVisionModelSelected(modelId) {
48
+ return VISION_MODELS.some(vm => modelId.toLowerCase().includes(vm.toLowerCase()));
49
+ }
50
+
51
+ // Progress callback for model loading
52
+ function progressCallback(progress) {
53
+ if (progress.status === 'initiate') {
54
+ loadingStatus.textContent = `Loading ${progress.file || 'model'}...`;
55
+ } else if (progress.status === 'download') {
56
+ loadingStatus.textContent = `Downloading ${progress.file || 'model'}...`;
57
+ } else if (progress.status === 'progress') {
58
+ const percent = Math.round(progress.progress || 0);
59
+ progressBar.style.width = `${percent}%`;
60
+ progressText.textContent = `${percent}%`;
61
+ loadingStatus.textContent = `Downloading ${progress.file || 'model'}...`;
62
+ } else if (progress.status === 'done') {
63
+ loadingStatus.textContent = `Loaded ${progress.file || 'model'}`;
64
+ } else if (progress.status === 'ready') {
65
+ loadingStatus.textContent = 'Model ready!';
66
+ progressBar.style.width = '100%';
67
+ progressText.textContent = '100%';
68
+ }
69
+ }
70
+
71
+ // Load model
72
+ async function loadModel() {
73
+ const modelId = modelSelect.value;
74
+
75
+ if (currentModel === modelId && generator) {
76
+ showError('Model already loaded!');
77
+ return;
78
+ }
79
+
80
+ try {
81
+ loadModelBtn.disabled = true;
82
+ loadingContainer.classList.remove('hidden');
83
+ chatContainer.classList.add('hidden');
84
+ progressBar.style.width = '0%';
85
+ progressText.textContent = '0%';
86
+ loadingStatus.textContent = 'Initializing...';
87
+
88
+ isVisionModel = isVisionModelSelected(modelId);
89
+
90
+ // Clean up previous model
91
+ if (generator) {
92
+ generator = null;
93
+ }
94
+
95
+ // Determine device - try WebGPU first
96
+ let device = 'wasm';
97
+ if (navigator.gpu) {
98
+ try {
99
+ const adapter = await navigator.gpu.requestAdapter();
100
+ if (adapter) {
101
+ device = 'webgpu';
102
+ loadingStatus.textContent = 'Using WebGPU acceleration...';
103
+ }
104
+ } catch (e) {
105
+ console.log('WebGPU not available, falling back to WASM');
106
+ }
107
+ }
108
+
109
+ loadingStatus.textContent = `Loading model on ${device.toUpperCase()}...`;
110
+
111
+ // Create pipeline based on model type
112
+ if (isVisionModel) {
113
+ generator = await pipeline('image-text-to-text', modelId, {
114
+ device: device,
115
+ dtype: 'q4f16',
116
+ progress_callback: progressCallback,
117
+ });
118
+ } else {
119
+ generator = await pipeline('text-generation', modelId, {
120
+ device: device,
121
+ dtype: 'q4f16',
122
+ progress_callback: progressCallback,
123
+ });
124
+ }
125
+
126
+ currentModel = modelId;
127
+ conversationHistory = [];
128
+
129
+ // Update UI
130
+ loadingContainer.classList.add('hidden');
131
+ chatContainer.classList.remove('hidden');
132
+ sendBtn.disabled = false;
133
+
134
+ // Update attach button visibility
135
+ attachImageBtn.style.display = isVisionModel ? 'block' : 'none';
136
+ imageUrlInput.style.display = isVisionModel ? 'block' : 'none';
137
+ loadUrlBtn.style.display = isVisionModel ? 'block' : 'none';
138
+
139
+ // Clear chat and show ready message
140
+ chatMessages.innerHTML = `
141
+ <div class="welcome-message">
142
+ <p>✅ <strong>${modelId.split('/').pop()}</strong> loaded successfully!</p>
143
+ <p class="hint">${isVisionModel ? 'This is a vision model. You can attach images to your messages.' : 'This is a
144
+ text-only model for conversation.'}</p>
145
+ </div>
146
+ `;
147
+
148
+ } catch (error) {
149
+ console.error('Error loading model:', error);
150
+ showError(`Failed to load model: ${error.message}`);
151
+ loadingContainer.classList.add('hidden');
152
+ } finally {
153
+ loadModelBtn.disabled = false;
154
+ }
155
+ }
156
+
157
+ // Handle image upload
158
+ function handleImageUpload(file) {
159
+ if (!file) return;
160
+
161
+ const reader = new FileReader();
162
+ reader.onload = (e) => {
163
+ currentImage = e.target.result;
164
+ imagePreview.src = currentImage;
165
+ imagePreviewContainer.classList.remove('hidden');
166
+ };
167
+ reader.readAsDataURL(file);
168
+ }
169
+
170
+ // Load image from URL
171
+ async function loadImageFromUrl() {
172
+ const url = imageUrlInput.value.trim();
173
+ if (!url) return;
174
+
175
+ try {
176
+ currentImage = url;
177
+ imagePreview.src = url;
178
+ imagePreviewContainer.classList.remove('hidden');
179
+ imageUrlInput.value = '';
180
+ } catch (error) {
181
+ showError('Failed to load image from URL');
182
+ }
183
+ }
184
+
185
+ // Remove attached image
186
+ function removeImage() {
187
+ currentImage = null;
188
+ imagePreview.src = '';
189
+ imagePreviewContainer.classList.add('hidden');
190
+ imageInput.value = '';
191
+ }
192
+
193
+ // Add message to chat
194
+ function addMessage(role, content, imageUrl = null) {
195
+ const messageDiv = document.createElement('div');
196
+ messageDiv.className = `message ${role}`;
197
+
198
+ const label = document.createElement('div');
199
+ label.className = 'message-label';
200
+ label.textContent = role === 'user' ? 'You' : 'Assistant';
201
+
202
+ const contentDiv = document.createElement('div');
203
+ contentDiv.className = 'message-content';
204
+
205
+ if (imageUrl) {
206
+ const img = document.createElement('img');
207
+ img.src = imageUrl;
208
+ img.className = 'message-image';
209
+ contentDiv.appendChild(img);
210
+ }
211
+
212
+ const textSpan = document.createElement('span');
213
+ textSpan.textContent = content;
214
+ contentDiv.appendChild(textSpan);
215
+
216
+ messageDiv.appendChild(label);
217
+ messageDiv.appendChild(contentDiv);
218
+ chatMessages.appendChild(messageDiv);
219
+
220
+ chatMessages.scrollTop = chatMessages.scrollHeight;
221
+
222
+ return contentDiv;
223
+ }
224
+
225
+ // Add typing indicator
226
+ function addTypingIndicator() {
227
+ const messageDiv = document.createElement('div');
228
+ messageDiv.className = 'message assistant';
229
+ messageDiv.id = 'typing-indicator';
230
+
231
+ const label = document.createElement('div');
232
+ label.className = 'message-label';
233
+ label.textContent = 'Assistant';
234
+
235
+ const contentDiv = document.createElement('div');
236
+ contentDiv.className = 'message-content';
237
+
238
+ const typingDiv = document.createElement('div');
239
+ typingDiv.className = 'typing-indicator';
240
+ typingDiv.innerHTML = '<span></span><span></span><span></span>';
241
+
242
+ contentDiv.appendChild(typingDiv);
243
+ messageDiv.appendChild(label);
244
+ messageDiv.appendChild(contentDiv);
245
+ chatMessages.appendChild(messageDiv);
246
+
247
+ chatMessages.scrollTop = chatMessages.scrollHeight;
248
+ }
249
+
250
+ // Remove typing indicator
251
+ function removeTypingIndicator() {
252
+ const indicator = document.getElementById('typing-indicator');
253
+ if (indicator) {
254
+ indicator.remove();
255
+ }
256
+ }
257
+
258
+ // Send message
259
+ async function sendMessage() {
260
+ const text = userInput.value.trim();
261
+ if (!text || !generator || isGenerating) return;
262
+
263
+ isGenerating = true;
264
+ sendBtn.disabled = true;
265
+ userInput.disabled = true;
266
+
267
+ try {
268
+ // Add user message
269
+ addMessage('user', text, currentImage);
270
+
271
+ // Clear input
272
+ userInput.value = '';
273
+ const imageForMessage = currentImage;
274
+ removeImage();
275
+
276
+ // Show typing indicator
277
+ addTypingIndicator();
278
+
279
+ // Get generation settings
280
+ const maxTokens = parseInt(maxTokensSlider.value);
281
+ const temperature = parseFloat(temperatureSlider.value);
282
+ const topP = parseFloat(topPSlider.value);
283
+
284
+ let response;
285
+
286
+ if (isVisionModel && imageForMessage) {
287
+ // Vision model with image
288
+ const messages = [
289
+ {
290
+ role: 'user',
291
+ content: [
292
+ { type: 'image', image: imageForMessage },
293
+ { type: 'text', text: text }
294
+ ]
295
+ }
296
+ ];
297
+
298
+ const output = await generator(messages, {
299
+ max_new_tokens: maxTokens,
300
+ temperature: temperature,
301
+ top_p: topP,
302
+ do_sample: temperature > 0,
303
  });
304
 
305
+ response = output[0].generated_text.at(-1).content;
306
+
307
+ } else if (isVisionModel) {
308
+ // Vision model without image - text only
309
+ const messages = [
310
+ {
311
+ role: 'user',
312
+ content: [
313
+ { type: 'text', text: text }
314
+ ]
315
+ }
316
+ ];
317
+
318
+ const output = await generator(messages, {
319
+ max_new_tokens: maxTokens,
320
+ temperature: temperature,
321
+ top_p: topP,
322
+ do_sample: temperature > 0,
323
+ });
324
+
325
+ response = output[0].generated_text.at(-1).content;
326
+
327
+ } else {
328
+ // Text-only model
329
+ conversationHistory.push({ role: 'user', content: text });
330
+
331
+ const output = await generator(conversationHistory, {
332
+ max_new_tokens: maxTokens,
333
+ temperature: temperature,
334
+ top_p: topP,
335
+ do_sample: temperature > 0,
336
+ });
337
+
338
+ const generatedMessages = output[0].generated_text;
339
+ const assistantMessage = generatedMessages[generatedMessages.length - 1];
340
+ response = assistantMessage.content;
341
+
342
+ conversationHistory.push({ role: 'assistant', content: response });
343
+ }
344
+
345
+ // Remove typing indicator and add response
346
+ removeTypingIndicator();
347
+ addMessage('assistant', response);
348
+
349
+ } catch (error) {
350
+ console.error('Error generating response:', error);
351
+ removeTypingIndicator();
352
+ showError(`Generation error: ${error.message}`);
353
+ } finally {
354
+ isGenerating = false;
355
+ sendBtn.disabled = false;
356
+ userInput.disabled = false;
357
+ userInput.focus();
358
+ }
359
+ }
360
+
361
+ // Show error
362
+ function showError(message) {
363
+ errorMessage.textContent = message;
364
+ errorContainer.classList.remove('hidden');
365
+ }
366
+
367
+ // Hide error
368
+ function hideError() {
369
+ errorContainer.classList.add('hidden');
370
+ }
371
+
372
+ // Event Listeners
373
+ loadModelBtn.addEventListener('click', loadModel);
374
+
375
+ attachImageBtn.addEventListener('click', () => {
376
+ if (isVisionModel) {
377
+ imageInput.click();
378
+ }
379
+ });
380
 
381
+ imageInput.addEventListener('change', (e) => {
382
+ if (e.target.files.length > 0) {
383
+ handleImageUpload(e.target.files[0]);
384
+ }
385
+ });
386
 
387
+ loadUrlBtn.addEventListener('click', loadImageFromUrl);
 
388
 
389
+ imageUrlInput.addEventListener('keypress', (e) => {
390
+ if (e.key === 'Enter') {
391
+ loadImageFromUrl();
392
+ }
393
  });
394
 
395
+ removeImageBtn.addEventListener('click', removeImage);
396
 
397
+ sendBtn.addEventListener('click', sendMessage);
 
 
 
398
 
399
+ userInput.addEventListener('keypress', (e) => {
400
+ if (e.key === 'Enter' && !e.shiftKey) {
401
+ e.preventDefault();
402
+ sendMessage();
 
 
 
403
  }
404
+ });
405
 
406
+ dismissErrorBtn.addEventListener('click', hideError);
 
 
407
 
408
+ // Settings sliders
409
+ maxTokensSlider.addEventListener('input', () => {
410
+ maxTokensValue.textContent = maxTokensSlider.value;
411
+ });
412
 
413
+ temperatureSlider.addEventListener('input', () => {
414
+ temperatureValue.textContent = temperatureSlider.value;
415
+ });
 
 
 
 
 
 
 
416
 
417
+ topPSlider.addEventListener('input', () => {
418
+ topPValue.textContent = topPSlider.value;
419
+ });
 
 
420
 
421
+ // Drag and drop for images
422
+ chatContainer.addEventListener('dragover', (e) => {
423
+ if (isVisionModel) {
424
+ e.preventDefault();
425
+ e.dataTransfer.dropEffect = 'copy';
426
  }
427
+ });
428
+
429
+ chatContainer.addEventListener('drop', (e) => {
430
+ if (isVisionModel) {
431
+ e.preventDefault();
432
+ const files = e.dataTransfer.files;
433
+ if (files.length > 0 && files[0].type.startsWith('image/')) {
434
+ handleImageUpload(files[0]);
435
+ }
436
+ }
437
+ });
438
+
439
+ // Initialize
440
+ document.addEventListener('DOMContentLoaded', () => {
441
+ // Hide image controls initially
442
+ attachImageBtn.style.display = 'none';
443
+ imageUrlInput.style.display = 'none';
444
+ loadUrlBtn.style.display = 'none';
445
+ });