Last active
August 10, 2025 10:28
-
-
Save mihailik/86d363e8e0cc476c3a3f52e934b3e218 to your computer and use it in GitHub Desktop.
Hello World: LLM chat in browser
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>TinyLLM In-Browser Chat</title> | |
| <style> | |
| body { | |
| font-family: Arial, sans-serif; | |
| margin: 0; | |
| padding: 0; | |
| background-color: #f0f2f5; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| min-height: 100vh; | |
| color: #333; | |
| } | |
| #chat-container { | |
| background-color: #fff; | |
| border-radius: 10px; | |
| box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); | |
| width: 90%; | |
| max-width: 600px; | |
| display: flex; | |
| flex-direction: column; | |
| overflow: hidden; | |
| min-height: 80vh; | |
| max-height: 95vh; | |
| position: relative; | |
| } | |
| h1 { | |
| text-align: center; | |
| color: #4a4a4a; | |
| padding: 15px; | |
| margin: 0; | |
| border-bottom: 1px solid #eee; | |
| font-size: 1.5em; | |
| } | |
| .status-message { | |
| text-align: center; | |
| padding: 10px; | |
| background-color: #e0f7fa; | |
| color: #00796b; | |
| font-weight: bold; | |
| border-bottom: 1px solid #b2ebf2; | |
| } | |
| #chat-box { | |
| flex-grow: 1; | |
| padding: 20px; | |
| overflow-y: auto; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 10px; | |
| } | |
| .message-container { | |
| display: flex; | |
| flex-direction: column; | |
| max-width: 80%; | |
| } | |
| .message-container.user { | |
| align-self: flex-end; | |
| align-items: flex-end; | |
| } | |
| .message-container.assistant { | |
| align-self: flex-start; | |
| align-items: flex-start; | |
| } | |
| .message-bubble { | |
| padding: 10px 15px; | |
| border-radius: 20px; | |
| line-height: 1.4; | |
| word-wrap: break-word; | |
| white-space: pre-wrap; /* Preserve whitespace and line breaks */ | |
| } | |
| or: white; | |
| border-bottom-right-radius: 5px; | |
| } | |
| .message-container.assistant.message-bubble { | |
| background-color: #e9e9eb; | |
| color: #333; | |
| border-bottom-left-radius: 5px; | |
| } | |
| .chat-input-container { | |
| display: flex; | |
| padding: 15px; | |
| border-top: 1px solid #eee; | |
| background-color: #fff; | |
| gap: 10px; | |
| } | |
| #user-input { | |
| flex-grow: 1; | |
| padding: 10px 15px; | |
| border: 1px solid #ddd; | |
| border-radius: 20px; | |
| font-size: 1em; | |
| outline: none; | |
| } | |
| #user-input:focus { | |
| border-color: #007bff; | |
| } | |
| #send { | |
| padding: 10px 20px; | |
| background-color: #007bff; | |
| color: white; | |
| border: none; | |
| border-radius: 20px; | |
| cursor: pointer; | |
| font-size: 1em; | |
| transition: background-color 0.2s; | |
| } | |
| #send:hover:not(:disabled) { | |
| background-color: #0056b3; | |
| } | |
| #send:disabled { | |
| background-color: #a0c9ff; | |
| cursor: not-allowed; | |
| } | |
| .chat-stats { | |
| font-size: 0.8em; | |
| color: #666; | |
| text-align: right; | |
| padding: 5px 20px; | |
| background-color: #f9f9f9; | |
| border-top: 1px solid #eee; | |
| } | |
| .hidden { | |
| display: none!important; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="chat-container"> | |
| <h1>In-Browser LLM Chat</h1> | |
| <div id="download-status" class="status-message">Loading model...</div> | |
| <div id="chat-box"> | |
| </div> | |
| <div id="chat-stats" class="chat-stats hidden"></div> | |
| <div class="chat-input-container"> | |
| <input type="text" id="user-input" placeholder="Loading model..." disabled> | |
| <button id="send" disabled>Send</button> | |
| </div> | |
| </div> | |
| <script type="module"> | |
| import * as webllm from "https://esm.run/@mlc-ai/web-llm"; | |
| /*************** WebLLM Logic ***************/ | |
| const messages =; | |
| const chatBox = document.getElementById("chat-box"); | |
| const userInput = document.getElementById("user-input"); | |
| const sendButton = document.getElementById("send"); | |
| const downloadStatus = document.getElementById("download-status"); | |
| const chatStats = document.getElementById("chat-stats"); | |
| let currentAssistantMessageElement = null; // To update the streaming message | |
| // Callback function for initializing progress. | |
| function updateEngineInitProgressCallback(report) { | |
| console.log("initialize", report.progress, report.text); | |
| downloadStatus.textContent = report.text; | |
| } | |
| // Create engine instance. | |
| const engine = new webllm.MLCEngine(); | |
| engine.setInitProgressCallback(updateEngineInitProgressCallback); | |
| async function initializeWebLLMEngine() { | |
| downloadStatus.classList.remove("hidden"); | |
| const selectedModel = "TinyLlama-1.1B-Chat-v1.0"; // Recommended small model [1, 2] | |
| const config = { | |
| temperature: 0.7, // Controls randomness [3] | |
| top_p: 0.9, // Controls diversity [3] | |
| }; | |
| try { | |
| await engine.reload(selectedModel, config); [4] | |
| downloadStatus.textContent = "Model loaded. Ready to chat!"; | |
| sendButton.disabled = false; | |
| userInput.disabled = false; | |
| userInput.setAttribute("placeholder", "Type a message..."); | |
| console.log("WebLLM engine initialized successfully."); | |
| } catch (error) { | |
| downloadStatus.textContent = `Error loading model: ${error.message}`; | |
| console.error("Error initializing WebLLM engine:", error); | |
| } | |
| } | |
| /*************** UI Logic ***************/ | |
| // Helper function to append messages to the chat box | |
| function appendMessage(message, isStreaming = false) { | |
| const messageContainer = document.createElement("div"); | |
| messageContainer.classList.add("message-container", message.role); | |
| const messageBubble = document.createElement("div"); | |
| messageBubble.classList.add("message-bubble"); | |
| messageBubble.textContent = message.content; | |
| messageContainer.appendChild(messageBubble); | |
| chatBox.appendChild(messageContainer); | |
| chatBox.scrollTop = chatBox.scrollHeight; // Scroll to bottom | |
| if (isStreaming && message.role === "assistant") { | |
| currentAssistantMessageElement = messageBubble; | |
| } | |
| } | |
| // Helper function to update the content of the last assistant message (for streaming) | |
| function updateLastAssistantMessage(newContent) { | |
| if (currentAssistantMessageElement) { | |
| currentAssistantMessageElement.textContent = newContent; | |
| chatBox.scrollTop = chatBox.scrollHeight; // Scroll to bottom | |
| } | |
| } | |
| // Function to handle sending a message | |
| async function onMessageSend() { | |
| const input = userInput.value.trim(); | |
| if (input.length === 0) { | |
| return; | |
| } | |
| const userMessage = { content: input, role: "user" }; [5, 6, 3] | |
| messages.push(userMessage); | |
| appendMessage(userMessage); | |
| userInput.value = ""; | |
| sendButton.disabled = true; | |
| userInput.setAttribute("placeholder", "Generating response..."); [7] | |
| const aiMessagePlaceholder = { content: "typing...", role: "assistant" }; [7] | |
| appendMessage(aiMessagePlaceholder, true); // Mark as streaming message | |
| let fullAssistantResponse = ""; | |
| chatStats.classList.add("hidden"); // Hide stats during generation | |
| try { | |
| const completion = await engine.chat.completions.create({ | |
| messages: messages, | |
| stream: true[5, 6, 3] | |
| // Parameters like temperature/top_p can be set here or in engine.reload | |
| }); | |
| for await (const chunk of completion) { [5] | |
| const curDelta = chunk.choices?.?.delta.content; [5, 7] | |
| if (curDelta) { | |
| fullAssistantResponse += curDelta; | |
| updateLastAssistantMessage(fullAssistantResponse); | |
| } | |
| } | |
| const finalMessage = await engine.getMessage(); // Get final message from engine [7] | |
| messages.push({ content: finalMessage, role: "assistant" }); // Add final message to history | |
| updateLastAssistantMessage(finalMessage); // Ensure final update | |
| // Display performance stats | |
| const usageText = await engine.runtimeStatsText(); // Get detailed stats string [7] | |
| chatStats.classList.remove("hidden"); | |
| chatStats.textContent = usageText; | |
| } catch (error) { | |
| updateLastAssistantMessage(`Error: ${error.message}`); | |
| console.error("Error during LLM inference:", error); | |
| } finally { | |
| sendButton.disabled = false; | |
| userInput.disabled = false; | |
| userInput.setAttribute("placeholder", "Type a message..."); | |
| currentAssistantMessageElement = null; // Clear reference | |
| } | |
| } | |
| // Event Listeners | |
| sendButton.addEventListener("click", onMessageSend); | |
| userInput.addEventListener("keypress", (event) => { | |
| if (event.key === "Enter" &&!sendButton.disabled) { | |
| onMessageSend(); | |
| } | |
| }); | |
| // Initialize the WebLLM engine when the page loads | |
| document.addEventListener("DOMContentLoaded", initializeWebLLMEngine); | |
| </script> | |
| </body> | |
| </html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>TinyLLM In-Browser Chat</title> | |
| <style> | |
| body { | |
| font-family: Arial, sans-serif; | |
| margin: 0; | |
| padding: 0; | |
| background-color: #f0f2f5; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| min-height: 100vh; | |
| color: #333; | |
| } | |
| #chat-container { | |
| background-color: #fff; | |
| border-radius: 10px; | |
| box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); | |
| width: 90%; | |
| max-width: 600px; | |
| display: flex; | |
| flex-direction: column; | |
| overflow: hidden; | |
| min-height: 80vh; | |
| max-height: 95vh; | |
| position: relative; | |
| } | |
| h1 { | |
| text-align: center; | |
| color: #4a4a4a; | |
| padding: 15px; | |
| margin: 0; | |
| border-bottom: 1px solid #eee; | |
| font-size: 1.5em; | |
| } | |
| .status-message { | |
| text-align: center; | |
| padding: 10px; | |
| background-color: #e0f7fa; | |
| color: #00796b; | |
| font-weight: bold; | |
| border-bottom: 1px solid #b2ebf2; | |
| } | |
| #chat-box { | |
| flex-grow: 1; | |
| padding: 20px; | |
| overflow-y: auto; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 10px; | |
| } | |
| .message-container { | |
| display: flex; | |
| flex-direction: column; | |
| max-width: 80%; | |
| } | |
| .message-container.user { | |
| align-self: flex-end; | |
| align-items: flex-end; | |
| } | |
| .message-container.assistant { | |
| align-self: flex-start; | |
| align-items: flex-start; | |
| } | |
| .message-bubble { | |
| padding: 10px 15px; | |
| border-radius: 20px; | |
| line-height: 1.4; | |
| word-wrap: break-word; | |
| white-space: pre-wrap; /* Preserve whitespace and line breaks */ | |
| } | |
| .message-container.user.message-bubble { | |
| background-color: #007bff; | |
| color: white; | |
| border-bottom-right-radius: 5px; | |
| } | |
| .message-container.assistant.message-bubble { | |
| background-color: #e9e9eb; | |
| color: #333; | |
| border-bottom-left-radius: 5px; | |
| } | |
| .chat-input-container { | |
| display: flex; | |
| padding: 15px; | |
| border-top: 1px solid #eee; | |
| background-color: #fff; | |
| gap: 10px; | |
| } | |
| #user-input { | |
| flex-grow: 1; | |
| padding: 10px 15px; | |
| border: 1px solid #ddd; | |
| border-radius: 20px; | |
| font-size: 1em; | |
| outline: none; | |
| } | |
| #user-input:focus { | |
| border-color: #007bff; | |
| } | |
| #send { | |
| padding: 10px 20px; | |
| background-color: #007bff; | |
| color: white; | |
| border: none; | |
| border-radius: 20px; | |
| cursor: pointer; | |
| font-size: 1em; | |
| transition: background-color 0.2s; | |
| } | |
| #send:hover:not(:disabled) { | |
| background-color: #0056b3; | |
| } | |
| #send:disabled { | |
| background-color: #a0c9ff; | |
| cursor: not-allowed; | |
| } | |
| .chat-stats { | |
| font-size: 0.8em; | |
| color: #666; | |
| text-align: right; | |
| padding: 5px 20px; | |
| background-color: #f9f9f9; | |
| border-top: 1px solid #eee; | |
| } | |
| .hidden { | |
| display: none!important; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="chat-container"> | |
| <h1>In-Browser LLM Chat</h1> | |
| <div id="download-status" class="status-message">Loading model...</div> | |
| <div id="chat-box"> | |
| </div> | |
| <div id="chat-stats" class="chat-stats hidden"></div> | |
| <div class="chat-input-container"> | |
| <input type="text" id="user-input" placeholder="Loading model..." disabled> | |
| <button id="send" disabled>Send</button> | |
| </div> | |
| </div> | |
| <script type="module"> | |
| import * as webllm from "https://esm.run/@mlc-ai/web-llm"; | |
| /*************** WebLLM Logic ***************/ | |
| const messages = []; // Corrected: Initialized as an empty array | |
| const chatBox = document.getElementById("chat-box"); | |
| const userInput = document.getElementById("user-input"); | |
| const sendButton = document.getElementById("send"); | |
| const downloadStatus = document.getElementById("download-status"); | |
| const chatStats = document.getElementById("chat-stats"); | |
| let currentAssistantMessageElement = null; // To update the streaming message | |
| // Callback function for initializing progress. | |
| function updateEngineInitProgressCallback(report) { | |
| console.log("initialize", report.progress, report.text); | |
| downloadStatus.textContent = report.text; | |
| } | |
| // Create engine instance. | |
| const engine = new webllm.MLCEngine(); | |
| engine.setInitProgressCallback(updateEngineInitProgressCallback); | |
| async function initializeWebLLMEngine() { | |
| downloadStatus.classList.remove("hidden"); | |
| const selectedModel = "TinyLlama-1.1B-Chat-v1.0"; // Recommended small model [1, 2] | |
| const config = { | |
| temperature: 0.7, // Controls randomness [3] | |
| top_p: 0.9, // Controls diversity [3] | |
| }; | |
| try { | |
| await engine.reload(selectedModel, config); // [4] | |
| downloadStatus.textContent = "Model loaded. Ready to chat!"; | |
| sendButton.disabled = false; | |
| userInput.disabled = false; | |
| userInput.setAttribute("placeholder", "Type a message..."); | |
| console.log("WebLLM engine initialized successfully."); | |
| } catch (error) { | |
| downloadStatus.textContent = `Error loading model: ${error.message}`; | |
| console.error("Error initializing WebLLM engine:", error); | |
| } | |
| } | |
| /*************** UI Logic ***************/ | |
| // Helper function to append messages to the chat box | |
| function appendMessage(message, isStreaming = false) { | |
| const messageContainer = document.createElement("div"); | |
| messageContainer.classList.add("message-container", message.role); | |
| const messageBubble = document.createElement("div"); | |
| messageBubble.classList.add("message-bubble"); | |
| messageBubble.textContent = message.content; | |
| messageContainer.appendChild(messageBubble); | |
| chatBox.appendChild(messageContainer); | |
| chatBox.scrollTop = chatBox.scrollHeight; // Scroll to bottom | |
| if (isStreaming && message.role === "assistant") { | |
| currentAssistantMessageElement = messageBubble; | |
| } | |
| } | |
| // Helper function to update the content of the last assistant message (for streaming) | |
| function updateLastAssistantMessage(newContent) { | |
| if (currentAssistantMessageElement) { | |
| currentAssistantMessageElement.textContent = newContent; | |
| chatBox.scrollTop = chatBox.scrollHeight; // Scroll to bottom | |
| } | |
| } | |
| // Function to handle sending a message | |
| async function onMessageSend() { | |
| const input = userInput.value.trim(); | |
| if (input.length === 0) { | |
| return; | |
| } | |
| const userMessage = { content: input, role: "user" }; // [5, 6, 3] | |
| messages.push(userMessage); | |
| appendMessage(userMessage); | |
| userInput.value = ""; | |
| sendButton.disabled = true; | |
| userInput.setAttribute("placeholder", "Generating response..."); // [7] | |
| const aiMessagePlaceholder = { content: "typing...", role: "assistant" }; // [7] | |
| appendMessage(aiMessagePlaceholder, true); // Mark as streaming message | |
| let fullAssistantResponse = ""; | |
| chatStats.classList.add("hidden"); // Hide stats during generation | |
| try { | |
| const completion = await engine.chat.completions.create({ | |
| messages: messages, | |
| stream: true, // [5, 6, 3] | |
| // Parameters like temperature/top_p can be set here or in engine.reload | |
| }); | |
| for await (const chunk of completion) { // [5] | |
| const curDelta = chunk.choices?.[0]?.delta.content; // [5, 7] | |
| if (curDelta) { | |
| fullAssistantResponse += curDelta; | |
| updateLastAssistantMessage(fullAssistantResponse); | |
| } | |
| } | |
| const finalMessage = await engine.getMessage(); // Get final message from engine [7] | |
| messages.push({ content: finalMessage, role: "assistant" }); // Add final message to history | |
| updateLastAssistantMessage(finalMessage); // Ensure final update | |
| // Display performance stats | |
| const usageText = await engine.runtimeStatsText(); // Get detailed stats string [7] | |
| chatStats.classList.remove("hidden"); | |
| chatStats.textContent = usageText; | |
| } catch (error) { | |
| updateLastAssistantMessage(`Error: ${error.message}`); | |
| console.error("Error during LLM inference:", error); | |
| } finally { | |
| sendButton.disabled = false; | |
| userInput.disabled = false; | |
| userInput.setAttribute("placeholder", "Type a message..."); | |
| currentAssistantMessageElement = null; // Clear reference | |
| } | |
| } | |
| // Event Listeners | |
| sendButton.addEventListener("click", onMessageSend); | |
| userInput.addEventListener("keypress", (event) => { | |
| if (event.key === "Enter" && !sendButton.disabled) { | |
| onMessageSend(); | |
| } | |
| }); | |
| // Initialize the WebLLM engine when the page loads | |
| document.addEventListener("DOMContentLoaded", initializeWebLLMEngine); | |
| </script> | |
| </body> | |
| </html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>TinyLLM In-Browser Chat</title> | |
| <style> | |
| body { | |
| font-family: Arial, sans-serif; | |
| margin: 0; | |
| padding: 0; | |
| background-color: #f0f2f5; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| min-height: 100vh; | |
| color: #333; | |
| } | |
| #chat-container { | |
| background-color: #fff; | |
| border-radius: 10px; | |
| box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); | |
| width: 90%; | |
| max-width: 600px; | |
| display: flex; | |
| flex-direction: column; | |
| overflow: hidden; | |
| min-height: 80vh; | |
| max-height: 95vh; | |
| position: relative; | |
| } | |
| h1 { | |
| text-align: center; | |
| color: #4a4a4a; | |
| padding: 15px; | |
| margin: 0; | |
| border-bottom: 1px solid #eee; | |
| font-size: 1.5em; | |
| } | |
| .status-message { | |
| text-align: center; | |
| padding: 10px; | |
| background-color: #e0f7fa; | |
| color: #00796b; | |
| font-weight: bold; | |
| border-bottom: 1px solid #b2ebf2; | |
| } | |
| #chat-box { | |
| flex-grow: 1; | |
| padding: 20px; | |
| overflow-y: auto; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 10px; | |
| } | |
| .message-container { | |
| display: flex; | |
| flex-direction: column; | |
| max-width: 80%; | |
| } | |
| .message-container.user { | |
| align-self: flex-end; | |
| align-items: flex-end; | |
| } | |
| .message-container.assistant { | |
| align-self: flex-start; | |
| align-items: flex-start; | |
| } | |
| .message-container.system { | |
| align-self: center; /* Center system messages */ | |
| align-items: center; | |
| font-size: 0.85em; | |
| color: #666; | |
| text-align: center; | |
| padding: 5px 10px; | |
| border-radius: 10px; | |
| background-color: #f0f0f0; | |
| margin: 5px 0; | |
| } | |
| .message-bubble { | |
| padding: 10px 15px; | |
| border-radius: 20px; | |
| line-height: 1.4; | |
| word-wrap: break-word; | |
| white-space: pre-wrap; /* Preserve whitespace and line breaks */ | |
| } | |
| .message-container.user .message-bubble { | |
| background-color: #007bff; | |
| color: white; | |
| border-bottom-right-radius: 5px; | |
| } | |
| .message-container.assistant .message-bubble { | |
| background-color: #e9e9eb; | |
| color: #333; | |
| border-bottom-left-radius: 5px; | |
| } | |
| .chat-input-container { | |
| display: flex; | |
| padding: 15px; | |
| border-top: 1px solid #eee; | |
| background-color: #fff; | |
| gap: 10px; | |
| } | |
| #user-input { | |
| flex-grow: 1; | |
| padding: 10px 15px; | |
| border: 1px solid #ddd; | |
| border-radius: 20px; | |
| font-size: 1em; | |
| outline: none; | |
| } | |
| #user-input:focus { | |
| border-color: #007bff; | |
| } | |
| #send { | |
| padding: 10px 20px; | |
| background-color: #007bff; | |
| color: white; | |
| border: none; | |
| border-radius: 20px; | |
| cursor: pointer; | |
| font-size: 1em; | |
| transition: background-color 0.2s; | |
| } | |
| #send:hover:not(:disabled) { | |
| background-color: #0056b3; | |
| } | |
| #send:disabled { | |
| background-color: #a0c9ff; | |
| cursor: not-allowed; | |
| } | |
| .chat-stats { | |
| font-size: 0.8em; | |
| color: #666; | |
| text-align: right; | |
| padding: 5px 20px; | |
| background-color: #f9f9f9; | |
| border-top: 1px solid #eee; | |
| } | |
| .hidden { | |
| display: none!important; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="chat-container"> | |
| <h1>In-Browser LLM Chat</h1> | |
| <div id="download-status" class="status-message">Loading model...</div> | |
| <div id="chat-box"> | |
| </div> | |
| <div id="chat-stats" class="chat-stats hidden"></div> | |
| <div class="chat-input-container"> | |
| <input type="text" id="user-input" placeholder="Loading model..." disabled> | |
| <button id="send" disabled>Send</button> | |
| </div> | |
| </div> | |
| <script type="module"> | |
| import * as webllm from "https://esm.run/@mlc-ai/web-llm"; | |
| /*************** WebLLM Logic ***************/ | |
| const messages = [ | |
| // Initial system message to guide the LLM's behavior | |
| { role: "system", content: "You are a helpful and concise AI assistant. Keep your responses brief and to the point." } | |
| ]; | |
| const chatBox = document.getElementById("chat-box"); | |
| const userInput = document.getElementById("user-input"); | |
| const sendButton = document.getElementById("send"); | |
| const downloadStatus = document.getElementById("download-status"); | |
| const chatStats = document.getElementById("chat-stats"); | |
| let currentAssistantMessageElement = null; // To update the streaming message | |
| // Callback function for initializing progress. | |
| function updateEngineInitProgressCallback(report) { | |
| console.log("initialize", report.progress, report.text); | |
| downloadStatus.textContent = report.text; | |
| } | |
| // Create engine instance. | |
| const engine = new webllm.MLCEngine(); | |
| engine.setInitProgressCallback(updateEngineInitProgressCallback); | |
| async function initializeWebLLMEngine() { | |
| downloadStatus.classList.remove("hidden"); | |
| let selectedModel = null; | |
| // Define a preferred pattern for the model name (e.g., "TinyLlama", "Gemma", "Phi") | |
| const preferredModelPattern = "TinyLlama"; | |
| const availableModels = webllm.prebuiltAppConfig.model_list; | |
| // Try to find a model matching the preferred pattern and suitable quantization | |
| const suitableModels = availableModels.filter(m => | |
| m.model_id.toLowerCase().includes(preferredModelPattern.toLowerCase()) && | |
| (m.model_id.includes("q4f16_1") || m.model_id.includes("q4f32_1")) && | |
| m.model_id.includes("Instruct") // Prioritize instruction-tuned models for chat | |
| ); | |
| if (suitableModels.length > 0) { | |
| // Select the first suitable model found (you could add more logic to pick the smallest/best) | |
| selectedModel = suitableModels[0].model_id; | |
| console.log(`Found preferred model matching '${preferredModelPattern}': ${selectedModel}`); | |
| } else { | |
| // Fallback to a known tiny model if preferred not found, or any small instruct model | |
| const fallbackModels = [ | |
| "TinyLlama-1.1B-Chat-v1.0-q4f16_1-MLC", // Our original choice, if exact match needed | |
| "Qwen2.5-0.5B-Instruct-q4f16_1-MLC", // Very tiny option | |
| "gemma-2b-it-q4f16_1-MLC", // Another small, instruct-tuned option | |
| "Phi-3.5-mini-instruct-q4f16_1-MLC", // Microsoft's small model | |
| ]; | |
| for (const fbModelId of fallbackModels) { | |
| const foundFbModel = availableModels.find(m => m.model_id === fbModelId); | |
| if (foundFbModel) { | |
| selectedModel = foundFbModel.model_id; | |
| console.log(`Falling back to model: ${selectedModel}`); | |
| break; | |
| } | |
| } | |
| if (!selectedModel) { | |
| downloadStatus.textContent = "Error: No suitable small chat model found in WebLLM's configuration. Please check your browser console for `webllm.prebuiltAppConfig.model_list` for available models."; | |
| console.error("No suitable small chat model found in available models."); | |
| return; // Stop initialization if no model found | |
| } | |
| } | |
| // Display the initial system message in the chat box | |
| appendMessage({ role: "system", content: messages[0].content }); | |
| const config = { | |
| temperature: 0.7, // Controls randomness [3] | |
| top_p: 0.9, // Controls diversity [3] | |
| }; | |
| try { | |
| await engine.reload(selectedModel, config); // [4] | |
| downloadStatus.textContent = `Model '${selectedModel}' loaded. Ready to chat!`; | |
| sendButton.disabled = false; | |
| userInput.disabled = false; | |
| userInput.setAttribute("placeholder", "Type a message..."); | |
| console.log("WebLLM engine initialized successfully."); | |
| } catch (error) { | |
| downloadStatus.textContent = `Error loading model '${selectedModel}': ${error.message}`; | |
| console.error(`Error initializing WebLLM engine with ${selectedModel}:`, error); | |
| } | |
| } | |
| /*************** UI Logic ***************/ | |
| // Helper function to append messages to the chat box | |
| function appendMessage(message, isStreaming = false) { | |
| const messageContainer = document.createElement("div"); | |
| messageContainer.classList.add("message-container", message.role); | |
| // Only create a message bubble for user and assistant messages | |
| if (message.role === "user" || message.role === "assistant") { | |
| const messageBubble = document.createElement("div"); | |
| messageBubble.classList.add("message-bubble"); | |
| messageBubble.textContent = message.content; | |
| messageContainer.appendChild(messageBubble); | |
| } else { | |
| // For system messages, just set the text content directly on the container | |
| messageContainer.textContent = message.content; | |
| } | |
| chatBox.appendChild(messageContainer); | |
| chatBox.scrollTop = chatBox.scrollHeight; // Scroll to bottom | |
| if (isStreaming && message.role === "assistant") { | |
| currentAssistantMessageElement = messageContainer.querySelector(".message-bubble"); | |
| // If system message, currentAssistantMessageElement will be null, and that's fine. | |
| } | |
| } | |
| // Helper function to update the content of the last assistant message (for streaming) | |
| function updateLastAssistantMessage(newContent) { | |
| if (currentAssistantMessageElement) { | |
| currentAssistantMessageElement.textContent = newContent; | |
| chatBox.scrollTop = chatBox.scrollHeight; // Scroll to bottom | |
| } | |
| } | |
| // Function to handle sending a message | |
| async function onMessageSend() { | |
| const input = userInput.value.trim(); | |
| if (input.length === 0) { | |
| return; | |
| } | |
| const userMessage = { content: input, role: "user" }; // [5, 6, 3] | |
| messages.push(userMessage); | |
| appendMessage(userMessage); | |
| userInput.value = ""; | |
| sendButton.disabled = true; | |
| userInput.setAttribute("placeholder", "Generating response..."); // [7] | |
| const aiMessagePlaceholder = { content: "typing...", role: "assistant" }; // [7] | |
| appendMessage(aiMessagePlaceholder, true); // Mark as streaming message | |
| let fullAssistantResponse = ""; | |
| chatStats.classList.add("hidden"); // Hide stats during generation | |
| try { | |
| const completion = await engine.chat.completions.create({ | |
| messages: messages, | |
| stream: true, // [5, 6, 3] | |
| // Parameters like temperature/top_p can be set here or in engine.reload | |
| }); | |
| for await (const chunk of completion) { // [5] | |
| const curDelta = chunk.choices?.[0]?.delta.content; // [5, 7] | |
| if (curDelta) { | |
| fullAssistantResponse += curDelta; | |
| updateLastAssistantMessage(fullAssistantResponse); | |
| } | |
| } | |
| const finalMessage = await engine.getMessage(); // Get final message from engine [7] | |
| messages.push({ content: finalMessage, role: "assistant" }); // Add final message to history | |
| updateLastAssistantMessage(finalMessage); // Ensure final update | |
| // Display performance stats | |
| const usageText = await engine.runtimeStatsText(); // Get detailed stats string [7] | |
| chatStats.classList.remove("hidden"); | |
| chatStats.textContent = usageText; | |
| } catch (error) { | |
| updateLastAssistantMessage(`Error: ${error.message}`); | |
| console.error("Error during LLM inference:", error); | |
| } finally { | |
| sendButton.disabled = false; | |
| userInput.disabled = false; | |
| userInput.setAttribute("placeholder", "Type a message..."); | |
| currentAssistantMessageElement = null; // Clear reference | |
| } | |
| } | |
| // Event Listeners | |
| sendButton.addEventListener("click", onMessageSend); | |
| userInput.addEventListener("keypress", (event) => { | |
| if (event.key === "Enter" && !sendButton.disabled) { | |
| onMessageSend(); | |
| } | |
| }); | |
| // Initialize the WebLLM engine when the page loads | |
| document.addEventListener("DOMContentLoaded", initializeWebLLMEngine); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment