localm / attempts /chat3.html
mihailik's picture
Moving to a proper app.
811916b
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Mobile Chat App with Transformers.js</title>
<style>
body {
margin: 0;
font-family: sans-serif;
display: flex;
flex-direction: column;
height: 100vh;
background: #f5f5f5;
}
#model-select {
padding: 10px;
font-size: 1em;
width: 100%;
box-sizing: border-box;
}
#chat {
flex: 1;
overflow-y: auto;
padding: 10px;
background: #fff;
}
.message {
margin: 10px 0;
padding: 10px;
border-radius: 5px;
max-width: 80%;
word-wrap: break-word;
}
.user {
background: #d1e7dd;
align-self: flex-end;
}
.bot {
background: #f8d7da;
align-self: flex-start;
}
#input-area {
display: flex;
padding: 10px;
background: #eee;
}
#user-input {
flex: 1;
padding: 10px;
font-size: 1em;
}
#send-btn {
padding: 10px;
font-size: 1em;
}
#status {
text-align: center;
padding: 5px;
font-size: 0.9em;
color: #555;
}
</style>
</head>
<body>
<select id="model-select">
<option value="Xenova/distilgpt2">Xenova/distilgpt2</option>
<option value="Xenova/phi-3-mini-4k-instruct">Xenova/phi-3-mini-4k-instruct</option>
<option value="Xenova/t5-small">Xenova/t5-small</option>
<option value="Xenova/gemma-2b-it">Xenova/gemma-2b-it</option>
<option value="Xenova/llama-3-8b-instruct">Xenova/llama-3-8b-instruct</option>
<option value="Xenova/Mistral-7B-Instruct-v0.2">Xenova/Mistral-7B-Instruct-v0.2</option>
</select>
<div id="status">Select a model to begin</div>
<div id="chat"></div>
<div id="input-area">
<input type="text" id="user-input" placeholder="Type a message..." />
<button id="send-btn">Send</button>
</div>
<script type="module">
import { pipeline } from 'https://cdn.jsdelivr.net/npm/@xenova/transformers@3.7.1/dist/transformers.min.js';
let worker = new Worker(URL.createObjectURL(new Blob([`
importScripts('https://cdn.jsdelivr.net/npm/@xenova/transformers@3.7.1/dist/transformers.min.js');
let chatPipeline = null;
let currentModel = null;
self.onmessage = async (e) => {
const { type, data } = e.data;
try {
if (type === 'load') {
self.postMessage({ type: 'status', data: 'Loading model...' });
chatPipeline = await window.pipeline('text-generation', data.model, { progress_callback: (x) => self.postMessage({ type: 'status', data: 'Progress: ' + x }) });
currentModel = data.model;
self.postMessage({ type: 'status', data: 'Model loaded: ' + currentModel });
} else if (type === 'generate') {
if (!chatPipeline) throw new Error('Model not loaded');
self.postMessage({ type: 'status', data: 'Generating response...' });
const output = await chatPipeline(data.text, { max_new_tokens: 100 });
self.postMessage({ type: 'response', data: output[0].generated_text });
self.postMessage({ type: 'status', data: 'Ready' });
}
} catch (err) {
self.postMessage({ type: 'error', data: err.stack });
}
};
self.onerror = (e) => {
self.postMessage({ type: 'error', data: e.message + '\\n' + e.filename + ':' + e.lineno });
};
`], { type: 'application/javascript' })));
const chat = document.getElementById('chat');
const input = document.getElementById('user-input');
const sendBtn = document.getElementById('send-btn');
const modelSelect = document.getElementById('model-select');
const status = document.getElementById('status');
let isBusy = false;
function appendMessage(text, sender) {
const msg = document.createElement('div');
msg.className = 'message ' + sender;
msg.textContent = text;
chat.appendChild(msg);
chat.scrollTop = chat.scrollHeight;
}
function setStatus(text) {
status.textContent = text;
}
function handleError(err) {
appendMessage('Error: ' + err, 'bot');
setStatus('Error occurred');
}
window.onerror = (msg, src, line, col, err) => {
handleError(err?.stack || msg);
};
worker.onmessage = (e) => {
const { type, data } = e.data;
if (type === 'response') {
appendMessage(data, 'bot');
isBusy = false;
} else if (type === 'status') {
setStatus(data);
} else if (type === 'error') {
handleError(data);
isBusy = false;
}
};
modelSelect.addEventListener('change', () => {
const model = modelSelect.value;
isBusy = true;
worker.postMessage({ type: 'load', data: { model } });
});
sendBtn.addEventListener('click', () => {
if (isBusy) return;
const text = input.value.trim();
if (!text) return;
appendMessage(text, 'user');
input.value = '';
isBusy = true;
worker.postMessage({ type: 'generate', data: { text } });
});
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') sendBtn.click();
});
// Auto-load default model
window.addEventListener('load', () => {
modelSelect.dispatchEvent(new Event('change'));
});
</script>
</body>
</html>