Loading models into slash.
Browse files- package.json +1 -1
- src/app/boot-app.js +10 -1
- src/app/init-milkdown.js +24 -2
- src/app/model-list.js +346 -0
package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
{
|
| 2 |
"name": "localm",
|
| 3 |
-
"version": "1.1.
|
| 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: '
|
| 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 |
+
}
|