mihailik commited on
Commit
eeb4c01
·
1 Parent(s): 0282567

Loading models into slash.

Browse files
package.json CHANGED
@@ -1,6 +1,6 @@
1
  {
2
  "name": "localm",
3
- "version": "1.1.14",
4
  "description": "Chat application",
5
  "scripts": {
6
  "build": "esbuild src/index.js --target=es6 --bundle --sourcemap --outfile=./index.js --format=iife --external:fs --external:path --external:child_process --external:ws --external:katex/dist/katex.min.css",
 
1
  {
2
  "name": "localm",
3
+ "version": "1.1.15",
4
  "description": "Chat application",
5
  "scripts": {
6
  "build": "esbuild src/index.js --target=es6 --bundle --sourcemap --outfile=./index.js --format=iife --external:fs --external:path --external:child_process --external:ws --external:katex/dist/katex.min.css",
src/app/boot-app.js CHANGED
@@ -34,7 +34,16 @@ export async function bootApp() {
34
  } = await initMilkdown({
35
  chatLog,
36
  chatInput,
37
- inputPlugins: makeEnterPlugins({ workerConnection: worker })
 
 
 
 
 
 
 
 
 
38
  });
39
 
40
  chatLogEditor = chatLogEditorInstance;
 
34
  } = await initMilkdown({
35
  chatLog,
36
  chatInput,
37
+ inputPlugins: makeEnterPlugins({ workerConnection: worker }),
38
+ onSlashCommand: async (modelId) => {
39
+ try {
40
+ outputMessage(`Loading model: ${modelId}...`);
41
+ await worker.loadModel(modelId);
42
+ outputMessage(`Model ${modelId} loaded successfully!`);
43
+ } catch (error) {
44
+ outputMessage(`Error loading model ${modelId}: ${error.message}`);
45
+ }
46
+ }
47
  });
48
 
49
  chatLogEditor = chatLogEditorInstance;
src/app/init-milkdown.js CHANGED
@@ -10,6 +10,7 @@ import {
10
  import { Crepe } from '@milkdown/crepe';
11
  import { commonmark } from '@milkdown/kit/preset/commonmark';
12
  import { slashFactory } from "@milkdown/plugin-slash";
 
13
 
14
  import "@milkdown/crepe/theme/common/style.css";
15
  import "@milkdown/crepe/theme/frame.css";
@@ -37,6 +38,12 @@ export async function initMilkdown({
37
  if (chatLog) chatLog.innerHTML = '';
38
  if (chatInput) chatInput.innerHTML = '';
39
 
 
 
 
 
 
 
40
  // Create read-only editor in .chat-log
41
  const chatLogEditor = await Editor.make()
42
  .config((ctx) => {
@@ -85,11 +92,26 @@ export async function initMilkdown({
85
  taskList: null
86
  },
87
  advancedGroup: {
88
- label: 'Code',
89
  codeBlock: { label: 'Code', icon: '`' },
90
  image: null,
91
  table: null,
92
- math: null
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
93
  }
94
  }
95
  }
 
10
  import { Crepe } from '@milkdown/crepe';
11
  import { commonmark } from '@milkdown/kit/preset/commonmark';
12
  import { slashFactory } from "@milkdown/plugin-slash";
13
+ import { fetchBrowserModels } from './model-list.js';
14
 
15
  import "@milkdown/crepe/theme/common/style.css";
16
  import "@milkdown/crepe/theme/frame.css";
 
38
  if (chatLog) chatLog.innerHTML = '';
39
  if (chatInput) chatInput.innerHTML = '';
40
 
41
+ // Fetch available models for slash menu
42
+ console.log('Starting to fetch browser models...');
43
+ const availableModels = await fetchBrowserModels();
44
+ console.log(`Loaded ${availableModels.length} models for slash menu`);
45
+ console.log('Available models:', availableModels);
46
+
47
  // Create read-only editor in .chat-log
48
  const chatLogEditor = await Editor.make()
49
  .config((ctx) => {
 
92
  taskList: null
93
  },
94
  advancedGroup: {
95
+ label: 'Advanced',
96
  codeBlock: { label: 'Code', icon: '`' },
97
  image: null,
98
  table: null,
99
+ math: null,
100
+ // Add model commands to advanced group
101
+ ...Object.fromEntries(
102
+ availableModels.map(model => [
103
+ model.slashCommand,
104
+ {
105
+ label: `${model.name} (${model.size})`,
106
+ icon: '🤖',
107
+ command: () => {
108
+ if (onSlashCommand) {
109
+ onSlashCommand(model.id);
110
+ }
111
+ }
112
+ }
113
+ ])
114
+ )
115
  }
116
  }
117
  }
src/app/model-list.js ADDED
@@ -0,0 +1,346 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // @ts-check
2
+
3
+ /**
4
+ * @typedef {{
5
+ * id: string,
6
+ * name: string,
7
+ * vendor: string,
8
+ * size: string,
9
+ * slashCommand: string,
10
+ * description: string,
11
+ * downloads?: number,
12
+ * pipeline_tag?: string
13
+ * }} ModelInfo
14
+ */
15
+
16
+ /**
17
+ * Cache for fetched models to avoid repeated API calls
18
+ */
19
+ let modelCache = null;
20
+ let cacheTimestamp = 0;
21
+ const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes
22
+
23
+ /**
24
+ * Size thresholds for mobile capability (in billions of parameters)
25
+ */
26
+ const MOBILE_SIZE_THRESHOLD = 15; // Models under 15B are considered mobile-capable
27
+
28
+ /**
29
+ * Fetch models from Hugging Face Hub with transformers.js compatibility
30
+ * @returns {Promise<ModelInfo[]>}
31
+ */
32
+ export async function fetchBrowserModels() {
33
+ // Check cache first
34
+ const now = Date.now();
35
+ if (modelCache && (now - cacheTimestamp) < CACHE_DURATION) {
36
+ return modelCache;
37
+ }
38
+
39
+ try {
40
+ console.log('Fetching transformers.js compatible models from Hugging Face Hub...');
41
+
42
+ // Fetch models with transformers.js library tag, sorted by downloads
43
+ const response = await fetch(
44
+ 'https://huggingface.co/api/models?library=transformers.js&sort=downloads&direction=-1&limit=100'
45
+ );
46
+
47
+ if (!response.ok) {
48
+ throw new Error(`HTTP error! status: ${response.status}`);
49
+ }
50
+
51
+ const rawModels = await response.json();
52
+ console.log(`Found ${rawModels.length} transformers.js models`);
53
+
54
+ // Filter and process models
55
+ const processedModels = rawModels
56
+ .filter(isModelMobileCapable)
57
+ .map(processModelData)
58
+ .filter(Boolean) // Remove any null results
59
+ .slice(0, 20); // Limit to top 20 models
60
+
61
+ console.log(`Filtered to ${processedModels.length} mobile-capable models`);
62
+
63
+ // Cache the results
64
+ modelCache = processedModels;
65
+ cacheTimestamp = now;
66
+
67
+ return processedModels;
68
+ } catch (error) {
69
+ console.error('Failed to fetch models from Hugging Face Hub:', error);
70
+
71
+ // Return fallback models if API fails
72
+ return getFallbackModels();
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Check if a model is suitable for mobile/browser use
78
+ * @param {any} model - Raw model data from HF API
79
+ * @returns {boolean}
80
+ */
81
+ function isModelMobileCapable(model) {
82
+ // Skip if no model ID
83
+ if (!model.id) return false;
84
+
85
+ // Estimate model size from various indicators
86
+ const sizeEstimate = estimateModelSize(model);
87
+
88
+ // Skip models that are too large
89
+ if (sizeEstimate > MOBILE_SIZE_THRESHOLD) {
90
+ return false;
91
+ }
92
+
93
+ // Prefer models with certain pipeline tags that work well in browsers
94
+ const preferredTags = [
95
+ 'text-generation',
96
+ 'text2text-generation',
97
+ 'feature-extraction',
98
+ 'sentence-similarity',
99
+ 'fill-mask'
100
+ ];
101
+
102
+ const hasPreferredTag = !model.pipeline_tag || preferredTags.includes(model.pipeline_tag);
103
+
104
+ // Skip certain model types that are less suitable for general text generation
105
+ const excludePatterns = [
106
+ /whisper/i,
107
+ /vision/i,
108
+ /image/i,
109
+ /audio/i,
110
+ /translation/i,
111
+ /classification/i,
112
+ /embedding/i
113
+ ];
114
+
115
+ const isExcluded = excludePatterns.some(pattern => pattern.test(model.id));
116
+
117
+ return hasPreferredTag && !isExcluded;
118
+ }
119
+
120
+ /**
121
+ * Estimate model size in billions of parameters from various indicators
122
+ * @param {any} model - Raw model data from HF API
123
+ * @returns {number}
124
+ */
125
+ function estimateModelSize(model) {
126
+ const modelId = model.id.toLowerCase();
127
+
128
+ // Extract size from model name patterns
129
+ const sizePatterns = [
130
+ /(\d+\.?\d*)b\b/i, // "7b", "3.8b", etc.
131
+ /(\d+)m\b/i, // "125m" -> convert to billions
132
+ /(\d+)k\b/i // "125k" -> very small
133
+ ];
134
+
135
+ for (const pattern of sizePatterns) {
136
+ const match = modelId.match(pattern);
137
+ if (match) {
138
+ const size = parseFloat(match[1]);
139
+ if (pattern.source.includes('m\\b')) {
140
+ return size / 1000; // Convert millions to billions
141
+ } else if (pattern.source.includes('k\\b')) {
142
+ return size / 1000000; // Convert thousands to billions
143
+ } else {
144
+ return size; // Already in billions
145
+ }
146
+ }
147
+ }
148
+
149
+ // If no size found in name, make conservative estimates based on model family
150
+ if (modelId.includes('gpt2') || modelId.includes('distil')) return 0.2;
151
+ if (modelId.includes('phi-1') || modelId.includes('phi1')) return 1.3;
152
+ if (modelId.includes('phi-3') || modelId.includes('phi3')) return 3.8;
153
+ if (modelId.includes('mistral')) return 7;
154
+ if (modelId.includes('qwen') && modelId.includes('3b')) return 3;
155
+ if (modelId.includes('qwen') && modelId.includes('7b')) return 7;
156
+ if (modelId.includes('llama') && modelId.includes('7b')) return 7;
157
+ if (modelId.includes('llama') && modelId.includes('13b')) return 13;
158
+
159
+ // Default conservative estimate for unknown models
160
+ return 5;
161
+ }
162
+
163
+ /**
164
+ * Process raw model data into our ModelInfo format
165
+ * @param {any} model - Raw model data from HF API
166
+ * @returns {ModelInfo | null}
167
+ */
168
+ function processModelData(model) {
169
+ try {
170
+ const size = estimateModelSize(model);
171
+ const vendor = extractVendor(model.id);
172
+ const name = extractModelName(model.id);
173
+ const slashCommand = generateSlashCommand(model.id);
174
+
175
+ return {
176
+ id: model.id,
177
+ name,
178
+ vendor,
179
+ size: formatSize(size),
180
+ slashCommand,
181
+ description: `${formatSize(size)} parameter model from ${vendor}`,
182
+ downloads: model.downloads || 0,
183
+ pipeline_tag: model.pipeline_tag
184
+ };
185
+ } catch (error) {
186
+ console.warn(`Failed to process model ${model.id}:`, error);
187
+ return null;
188
+ }
189
+ }
190
+
191
+ /**
192
+ * Extract vendor/organization from model ID
193
+ * @param {string} modelId
194
+ * @returns {string}
195
+ */
196
+ function extractVendor(modelId) {
197
+ const parts = modelId.split('/');
198
+ if (parts.length > 1) {
199
+ const org = parts[0];
200
+ // Map known organizations to friendly names
201
+ const orgMap = {
202
+ 'microsoft': 'Microsoft',
203
+ 'mistralai': 'Mistral AI',
204
+ 'Qwen': 'Alibaba',
205
+ 'google': 'Google',
206
+ 'openai-community': 'OpenAI',
207
+ 'Xenova': 'Xenova',
208
+ 'meta-llama': 'Meta',
209
+ 'onnx-community': 'ONNX Community'
210
+ };
211
+ return orgMap[org] || org;
212
+ }
213
+ return 'Unknown';
214
+ }
215
+
216
+ /**
217
+ * Extract clean model name from full ID
218
+ * @param {string} modelId
219
+ * @returns {string}
220
+ */
221
+ function extractModelName(modelId) {
222
+ const parts = modelId.split('/');
223
+ const name = parts[parts.length - 1];
224
+
225
+ // Clean up common patterns
226
+ return name
227
+ .replace(/-ONNX$/, '')
228
+ .replace(/-onnx$/, '')
229
+ .replace(/-instruct$/, '')
230
+ .replace(/-chat$/, '')
231
+ .replace(/^Xenova-/, '')
232
+ .replace(/-/g, ' ')
233
+ .replace(/\b\w/g, l => l.toUpperCase()); // Title case
234
+ }
235
+
236
+ /**
237
+ * Generate a slash command from model ID
238
+ * @param {string} modelId
239
+ * @returns {string}
240
+ */
241
+ function generateSlashCommand(modelId) {
242
+ const name = (modelId.split('/').pop() || modelId).toLowerCase();
243
+
244
+ // Create short, memorable commands
245
+ if (name.includes('phi-3') || name.includes('phi3')) return 'phi3';
246
+ if (name.includes('phi-1') || name.includes('phi1')) return 'phi1';
247
+ if (name.includes('mistral')) return 'mistral';
248
+ if (name.includes('qwen') && name.includes('3b')) return 'qwen3b';
249
+ if (name.includes('qwen') && name.includes('7b')) return 'qwen7b';
250
+ if (name.includes('qwen')) return 'qwen';
251
+ if (name.includes('gpt2')) return 'gpt2';
252
+ if (name.includes('distilgpt2')) return 'distilgpt2';
253
+ if (name.includes('llama')) return 'llama';
254
+ if (name.includes('gemma')) return 'gemma';
255
+ if (name.includes('flan')) return 'flant5';
256
+
257
+ // Generate from first few characters of model name
258
+ const clean = name.replace(/[^a-z0-9]/g, '');
259
+ return clean.substring(0, 8);
260
+ }
261
+
262
+ /**
263
+ * Format size number for display
264
+ * @param {number} size
265
+ * @returns {string}
266
+ */
267
+ function formatSize(size) {
268
+ if (size < 1) {
269
+ return `${Math.round(size * 1000)}M`;
270
+ } else {
271
+ return `${size.toFixed(1)}B`;
272
+ }
273
+ }
274
+
275
+ /**
276
+ * Get fallback models if API fetch fails
277
+ * @returns {ModelInfo[]}
278
+ */
279
+ function getFallbackModels() {
280
+ return [
281
+ {
282
+ id: 'microsoft/Phi-3-mini-4k-instruct',
283
+ name: 'Phi-3 Mini',
284
+ vendor: 'Microsoft',
285
+ size: '3.8B',
286
+ slashCommand: 'phi3',
287
+ description: 'Exceptional performance-to-size ratio, strong in reasoning and math'
288
+ },
289
+ {
290
+ id: 'mistralai/Mistral-7B-v0.1',
291
+ name: 'Mistral 7B',
292
+ vendor: 'Mistral AI',
293
+ size: '7.3B',
294
+ slashCommand: 'mistral',
295
+ description: 'Highly efficient, outperforms larger models with innovative architecture'
296
+ },
297
+ {
298
+ id: 'Xenova/distilgpt2',
299
+ name: 'DistilGPT-2',
300
+ vendor: 'Xenova',
301
+ size: '82M',
302
+ slashCommand: 'distilgpt2',
303
+ description: 'Extremely fast and lightweight for quick prototyping'
304
+ },
305
+ {
306
+ id: 'openai-community/gpt2',
307
+ name: 'GPT-2',
308
+ vendor: 'OpenAI',
309
+ size: '124M',
310
+ slashCommand: 'gpt2',
311
+ description: 'Foundational model for reliable lightweight text generation'
312
+ }
313
+ ];
314
+ }
315
+
316
+ /**
317
+ * Get model info by slash command
318
+ * @param {string} command - The slash command (e.g., 'phi3')
319
+ * @param {ModelInfo[]} [models] - Optional pre-fetched models list
320
+ * @returns {Promise<ModelInfo | undefined>}
321
+ */
322
+ export async function getModelByCommand(command, models) {
323
+ const modelList = models || await fetchBrowserModels();
324
+ return modelList.find(model => model.slashCommand === command);
325
+ }
326
+
327
+ /**
328
+ * Get model info by ID
329
+ * @param {string} id - The model ID
330
+ * @param {ModelInfo[]} [models] - Optional pre-fetched models list
331
+ * @returns {Promise<ModelInfo | undefined>}
332
+ */
333
+ export async function getModelById(id, models) {
334
+ const modelList = models || await fetchBrowserModels();
335
+ return modelList.find(model => model.id === id);
336
+ }
337
+
338
+ /**
339
+ * Get all available slash commands
340
+ * @param {ModelInfo[]} [models] - Optional pre-fetched models list
341
+ * @returns {Promise<string[]>}
342
+ */
343
+ export async function getAllSlashCommands(models) {
344
+ const modelList = models || await fetchBrowserModels();
345
+ return modelList.map(model => model.slashCommand);
346
+ }