|
<!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(); |
|
}); |
|
|
|
|
|
window.addEventListener('load', () => { |
|
modelSelect.dispatchEvent(new Event('change')); |
|
}); |
|
</script> |
|
</body> |
|
</html> |
|
|