test / index.html
lnxasd
Update index.html
800b780 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Local WebGPU Chat (Paste Mode)</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
.no-scrollbar::-webkit-scrollbar { display: none; }
.no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
.typing-cursor::after { content: '▋'; animation: blink 1s step-start infinite; }
@keyframes blink { 50% { opacity: 0; } }
/* Smooth fade for details */
details > summary { list-style: none; }
details > summary::-webkit-details-marker { display: none; }
</style>
</head>
<body class="bg-gray-900 text-gray-100 h-screen flex flex-col font-sans overflow-hidden">
<!-- Header -->
<header class="p-3 border-b border-gray-700 bg-gray-800 flex justify-between items-center z-10">
<div class="flex items-center gap-2">
<h1 class="text-lg font-bold bg-gradient-to-r from-blue-400 to-purple-500 bg-clip-text text-transparent">
Local .MD Chat
</h1>
<span class="text-[10px] bg-blue-900 text-blue-300 px-1.5 py-0.5 rounded border border-blue-700 tracking-wider">WEBGPU</span>
</div>
<div class="text-xs text-gray-400" id="statusLabel">Model: Not Loaded</div>
</header>
<!-- Context / Paste Area -->
<div class="bg-gray-800 border-b border-gray-700 shadow-lg z-10">
<details id="pasteDetails" open class="group">
<summary class="p-3 cursor-pointer text-sm font-medium text-blue-300 hover:text-white flex justify-between items-center select-none bg-gray-750">
<span>📄 Document Context (Paste Here)</span>
<span class="group-open:rotate-180 transition-transform duration-200"></span>
</summary>
<div class="p-3 pt-0">
<textarea id="pasteInput"
class="w-full h-32 bg-gray-900 border border-gray-600 rounded p-2 text-xs font-mono text-gray-300 focus:outline-none focus:border-blue-500 resize-y"
placeholder="Paste your Markdown or Text content here..."></textarea>
<div class="flex justify-between items-center mt-2">
<span id="charCount" class="text-xs text-gray-500">0 chars</span>
<button id="setContextBtn" class="bg-green-600 hover:bg-green-500 text-white text-xs px-4 py-1.5 rounded transition shadow-sm">
Set Context
</button>
</div>
</div>
</details>
</div>
<!-- Main Chat Area -->
<main id="chatContainer" class="flex-1 overflow-y-auto p-4 space-y-4 scroll-smooth pb-20">
<!-- Welcome Message -->
<div class="flex flex-col items-center justify-center h-full text-center text-gray-500 space-y-4" id="welcomeMsg">
<div class="p-4 bg-gray-800 rounded-full animate-pulse">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-8 h-8">
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 13.5l10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75z" />
</svg>
</div>
<div>
<h2 class="text-lg font-semibold text-gray-300">Initialize Engine</h2>
<p class="text-xs text-gray-500 max-w-xs mt-1 mx-auto">
Downloads ~1.2GB (Llama 3.2 1B). <br>Stored in browser cache for next time.
</p>
</div>
<button id="initBtn" class="bg-blue-600 hover:bg-blue-500 text-white px-6 py-2 rounded-full font-medium transition text-sm shadow-lg shadow-blue-900/50">
Load Model
</button>
<!-- Progress Bar -->
<div id="progressContainer" class="hidden w-64 space-y-2">
<div class="h-1.5 bg-gray-700 rounded-full overflow-hidden">
<div id="progressBar" class="h-full bg-blue-500 w-0 transition-all duration-300"></div>
</div>
<p id="progressText" class="text-[10px] text-gray-400 font-mono">Initializing...</p>
</div>
</div>
</main>
<!-- Input Area -->
<footer class="p-3 border-t border-gray-700 bg-gray-800">
<form id="chatForm" class="flex gap-2 max-w-5xl mx-auto">
<input type="text" id="userInput" disabled
class="flex-1 bg-gray-900 border border-gray-600 text-white rounded px-3 py-2 text-sm focus:outline-none focus:border-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
placeholder="Wait for model..." autocomplete="off">
<button type="submit" id="sendBtn" disabled
class="bg-blue-600 hover:bg-blue-500 disabled:bg-gray-700 disabled:text-gray-500 text-white px-4 py-2 rounded text-sm font-medium transition">
Send
</button>
</form>
</footer>
<!-- Logic -->
<script type="module">
import { CreateMLCEngine } from "https://esm.run/@mlc-ai/web-llm";
const SELECTED_MODEL = "Llama-3.2-1B-Instruct-q4f16_1-MLC";
let engine = null;
let messages = [];
let isGenerating = false;
// DOM Elements
const initBtn = document.getElementById('initBtn');
const progressContainer = document.getElementById('progressContainer');
const progressBar = document.getElementById('progressBar');
const progressText = document.getElementById('progressText');
const welcomeMsg = document.getElementById('welcomeMsg');
const chatContainer = document.getElementById('chatContainer');
const chatForm = document.getElementById('chatForm');
const userInput = document.getElementById('userInput');
const sendBtn = document.getElementById('sendBtn');
const statusLabel = document.getElementById('statusLabel');
// Paste Logic Elements
const pasteInput = document.getElementById('pasteInput');
const setContextBtn = document.getElementById('setContextBtn');
const pasteDetails = document.getElementById('pasteDetails');
const charCount = document.getElementById('charCount');
// Character counter
pasteInput.addEventListener('input', () => {
charCount.innerText = `${pasteInput.value.length} chars`;
});
// --- 1. Initialize WebLLM ---
initBtn.addEventListener('click', async () => {
initBtn.classList.add('hidden');
progressContainer.classList.remove('hidden');
try {
const initProgressCallback = (report) => {
progressText.innerText = report.text;
const match = report.text.match(/(\d+)%/);
if (match) progressBar.style.width = `${match[1]}%`;
if (report.text.includes("Finish")) progressBar.style.width = "100%";
};
engine = await CreateMLCEngine(
SELECTED_MODEL,
{ initProgressCallback: initProgressCallback }
);
welcomeMsg.remove();
statusLabel.innerText = "Model: Ready";
statusLabel.classList.add("text-green-400");
appendSystemMessage("System: Model loaded. Paste your text above and click 'Set Context'.");
} catch (error) {
progressText.innerText = "Error: " + error.message;
progressText.classList.add("text-red-400");
}
});
// --- 2. Handle Paste / Set Context ---
setContextBtn.addEventListener('click', () => {
if (!engine) {
alert("Please load the model first!");
return;
}
let text = pasteInput.value.trim();
if (!text) return;
// SAFETY: Truncate to prevent crash (~20k chars)
const LIMIT = 20000;
let truncated = false;
if (text.length > LIMIT) {
text = text.substring(0, LIMIT) + "\n...[TRUNCATED]...";
truncated = true;
}
// Set System Prompt
messages = [
{
role: "system",
content: `You are a helpful assistant. Answer questions based ONLY on the following text. If the answer is not in the text, say you don't know.\n\n--- CONTEXT START ---\n${text}\n--- CONTEXT END ---`
}
];
// UI Updates
chatContainer.innerHTML = ''; // Clear old chat
let msg = `Context updated (${text.length} chars).`;
if (truncated) msg += " ⚠️ Text was truncated to prevent crash.";
appendSystemMessage(msg);
// Auto-close the details panel to save space
pasteDetails.removeAttribute("open");
// Enable Chat
enableChat();
});
// --- 3. Chat Logic ---
chatForm.addEventListener('submit', async (e) => {
e.preventDefault();
const text = userInput.value.trim();
if (!text || isGenerating) return;
appendUserMessage(text);
messages.push({ role: "user", content: text });
userInput.value = '';
isGenerating = true;
disableInput();
const responseElement = appendAIMessage("");
let fullResponse = "";
try {
const chunks = await engine.chat.completions.create({
messages: messages,
temperature: 0.6,
stream: true,
});
for await (const chunk of chunks) {
const content = chunk.choices[0]?.delta?.content || "";
fullResponse += content;
responseElement.innerText = fullResponse;
chatContainer.scrollTop = chatContainer.scrollHeight;
}
messages.push({ role: "assistant", content: fullResponse });
} catch (err) {
responseElement.innerText += "\n[Error: Memory limit reached or GPU error]";
console.error(err);
} finally {
isGenerating = false;
enableInput();
userInput.focus();
}
});
// --- Helpers ---
function enableChat() {
userInput.disabled = false;
sendBtn.disabled = false;
userInput.placeholder = "Ask about the text...";
userInput.focus();
}
function disableInput() {
userInput.disabled = true;
sendBtn.disabled = true;
sendBtn.innerText = "...";
}
function enableInput() {
userInput.disabled = false;
sendBtn.disabled = false;
sendBtn.innerText = "Send";
}
function appendUserMessage(text) {
const div = document.createElement('div');
div.className = "flex justify-end";
div.innerHTML = `<div class="bg-blue-600 text-white px-4 py-2 rounded-2xl rounded-tr-sm max-w-[85%] shadow-md text-sm">${escapeHtml(text)}</div>`;
chatContainer.appendChild(div);
chatContainer.scrollTop = chatContainer.scrollHeight;
}
function appendAIMessage(text) {
const div = document.createElement('div');
div.className = "flex justify-start";
div.innerHTML = `<div class="bg-gray-700 text-gray-200 px-4 py-2 rounded-2xl rounded-tl-sm max-w-[85%] shadow-md text-sm whitespace-pre-wrap leading-relaxed typing-cursor"></div>`;
chatContainer.appendChild(div);
return div.firstElementChild;
}
function appendSystemMessage(text) {
const div = document.createElement('div');
div.className = "flex justify-center my-4";
div.innerHTML = `<span class="text-[10px] uppercase tracking-wide text-gray-500 bg-gray-800/50 border border-gray-700 px-3 py-1 rounded-full">${text}</span>`;
chatContainer.appendChild(div);
}
function escapeHtml(text) {
const map = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#039;' };
return text.replace(/[&<>"']/g, function(m) { return map[m]; });
}
</script>
</body>
</html>