| |
| |
| |
|
|
|
|
| class WebLLMEngine {
|
| constructor(options = {}) {
|
| this.status = 'OFFLINE';
|
| this.model = options.model || 'TinyLlama-1.1B-Chat-v0.4-q4f16_1-1k';
|
| this.temperature = options.temperature || 0.2;
|
| this.topP = options.topP || 0.9;
|
| this.maxTokens = options.maxTokens || 256;
|
|
|
| this.engine = null;
|
| this.isLoading = false;
|
| this.isGenerating = false;
|
| this.abortController = null;
|
|
|
| this.supportedModels = [
|
| { id: 'TinyLlama-1.1B-Chat-v0.4-q4f16_1-1k', name: 'TinyLlama 1.1B' },
|
| { id: 'Mistral-7B-Instruct-v0.2-q4f16_1-MLC', name: 'Mistral 7B' },
|
| { id: 'Llama-2-7b-chat-hf-q4f32_1-MLC', name: 'Llama 2 7B' },
|
| ];
|
|
|
| this.conversationHistory = [];
|
| this.maxHistoryLength = 10;
|
| }
|
|
|
| |
| |
|
|
| async initialize() {
|
| if (this.status === 'READY' || this.isLoading) {
|
| return;
|
| }
|
|
|
| this.isLoading = true;
|
| this.status = 'LOADING';
|
| this.emit('statusChanged', 'LOADING');
|
|
|
| try {
|
|
|
| if (typeof window.webllm === 'undefined') {
|
| throw new Error('WebLLM not loaded. Include @mlc-ai/web-llm script.');
|
| }
|
|
|
|
|
| const hasWebGPU = !!(navigator.gpu);
|
| console.log(`WebGPU available: ${hasWebGPU}`);
|
|
|
|
|
| const webllm = window.webllm;
|
| this.engine = new webllm.Engine({
|
| model: this.model,
|
| useWebGPU: hasWebGPU,
|
| maxSequenceLength: 4096,
|
| });
|
|
|
| await this.engine.forward('');
|
|
|
| this.status = 'READY';
|
| this.isLoading = false;
|
| this.emit('statusChanged', 'READY');
|
| console.log(`WebLLM engine initialized: ${this.model}`);
|
| } catch (error) {
|
| this.status = 'ERROR';
|
| this.isLoading = false;
|
| this.emit('statusChanged', 'ERROR');
|
| this.emit('error', error.message);
|
| console.error('WebLLM initialization failed:', error);
|
| throw error;
|
| }
|
| }
|
|
|
| |
| |
|
|
| async generateResponse(userMessage, systemPrompt = '') {
|
| if (this.status !== 'READY' || !this.engine) {
|
| throw new Error('Engine not ready');
|
| }
|
|
|
| if (this.isGenerating) {
|
| throw new Error('Generation already in progress');
|
| }
|
|
|
| this.isGenerating = true;
|
| this.status = 'GENERATING';
|
| this.emit('statusChanged', 'GENERATING');
|
| this.emit('generationStart');
|
|
|
| this.abortController = new AbortController();
|
| let fullResponse = '';
|
|
|
| try {
|
|
|
| const messages = this.buildConversation(userMessage, systemPrompt);
|
|
|
|
|
| const generator = await this.engine.generate(messages, {
|
| temperature: this.temperature,
|
| top_p: this.topP,
|
| max_new_tokens: this.maxTokens,
|
| });
|
|
|
| for await (const token of generator) {
|
| if (this.abortController.signal.aborted) {
|
| break;
|
| }
|
|
|
| fullResponse += token;
|
| this.emit('token', token);
|
| }
|
|
|
|
|
| this.addToHistory({
|
| role: 'user',
|
| content: userMessage,
|
| });
|
|
|
| this.addToHistory({
|
| role: 'assistant',
|
| content: fullResponse,
|
| });
|
|
|
| this.status = 'READY';
|
| this.emit('statusChanged', 'READY');
|
| this.emit('generationComplete', fullResponse);
|
|
|
| return fullResponse;
|
| } catch (error) {
|
| if (error.name !== 'AbortError') {
|
| this.status = 'ERROR';
|
| this.emit('statusChanged', 'ERROR');
|
| this.emit('error', error.message);
|
| console.error('Generation failed:', error);
|
| throw error;
|
| }
|
| } finally {
|
| this.isGenerating = false;
|
| }
|
| }
|
|
|
| |
| |
|
|
| buildConversation(userMessage, systemPrompt) {
|
| const messages = [];
|
|
|
| if (systemPrompt) {
|
| messages.push({
|
| role: 'system',
|
| content: systemPrompt,
|
| });
|
| }
|
|
|
|
|
| for (const msg of this.conversationHistory.slice(-this.maxHistoryLength)) {
|
| messages.push(msg);
|
| }
|
|
|
|
|
| messages.push({
|
| role: 'user',
|
| content: userMessage,
|
| });
|
|
|
| return messages;
|
| }
|
|
|
| |
| |
|
|
| addToHistory(message) {
|
| this.conversationHistory.push(message);
|
|
|
|
|
| if (this.conversationHistory.length > this.maxHistoryLength * 2) {
|
| this.conversationHistory = this.conversationHistory.slice(-this.maxHistoryLength);
|
| }
|
| }
|
|
|
| |
| |
|
|
| stopGeneration() {
|
| if (this.abortController) {
|
| this.abortController.abort();
|
| this.isGenerating = false;
|
| this.status = 'READY';
|
| this.emit('statusChanged', 'READY');
|
| this.emit('generationStopped');
|
| }
|
| }
|
|
|
| |
| |
|
|
| clearHistory() {
|
| this.conversationHistory = [];
|
| this.emit('historyCleared');
|
| }
|
|
|
| |
| |
|
|
| setTemperature(temp) {
|
| this.temperature = Math.max(0, Math.min(2, temp));
|
| }
|
|
|
| |
| |
|
|
| setTopP(p) {
|
| this.topP = Math.max(0, Math.min(1, p));
|
| }
|
|
|
| |
| |
|
|
| setMaxTokens(tokens) {
|
| this.maxTokens = Math.max(1, Math.min(2048, tokens));
|
| }
|
|
|
| |
| |
|
|
| getSettings() {
|
| return {
|
| model: this.model,
|
| temperature: this.temperature,
|
| topP: this.topP,
|
| maxTokens: this.maxTokens,
|
| status: this.status,
|
| };
|
| }
|
|
|
| |
| |
|
|
| getSupportedModels() {
|
| return this.supportedModels;
|
| }
|
|
|
| |
| |
|
|
| async switchModel(modelId) {
|
| if (this.isGenerating) {
|
| throw new Error('Cannot switch model while generating');
|
| }
|
|
|
| const supported = this.supportedModels.some(m => m.id === modelId);
|
| if (!supported) {
|
| throw new Error(`Model not supported: ${modelId}`);
|
| }
|
|
|
| this.model = modelId;
|
| this.status = 'OFFLINE';
|
|
|
|
|
| this.engine = null;
|
| this.emit('statusChanged', 'OFFLINE');
|
| }
|
|
|
| |
| |
|
|
| listeners = {};
|
|
|
| on(event, callback) {
|
| if (!this.listeners[event]) {
|
| this.listeners[event] = [];
|
| }
|
| this.listeners[event].push(callback);
|
| }
|
|
|
| off(event, callback) {
|
| if (this.listeners[event]) {
|
| this.listeners[event] = this.listeners[event].filter(cb => cb !== callback);
|
| }
|
| }
|
|
|
| emit(event, data) {
|
| if (this.listeners[event]) {
|
| for (const callback of this.listeners[event]) {
|
| try {
|
| callback(data);
|
| } catch (error) {
|
| console.error(`Error in listener for ${event}:`, error);
|
| }
|
| }
|
| }
|
| }
|
|
|
| |
| |
|
|
| static hasWebGPU() {
|
| return !!navigator.gpu;
|
| }
|
|
|
| |
| |
|
|
| static hasWebLLM() {
|
| return typeof window.webllm !== 'undefined';
|
| }
|
|
|
| |
| |
|
|
| estimateModelSize(modelId) {
|
| const sizes = {
|
| 'TinyLlama-1.1B-Chat-v0.4-q4f16_1-1k': '600MB',
|
| 'Mistral-7B-Instruct-v0.2-q4f16_1-MLC': '4GB',
|
| 'Llama-2-7b-chat-hf-q4f32_1-MLC': '8GB',
|
| };
|
| return sizes[modelId] || 'Unknown';
|
| }
|
| }
|
|
|
|
|
| if (typeof module !== 'undefined' && module.exports) {
|
| module.exports = WebLLMEngine;
|
| }
|
|
|