import { pipeline, TextStreamer, env } from 'https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.8.0';
// Configure environment
env.allowLocalModels = false;
class ChatApp {
constructor() {
this.generator = null;
this.conversationHistory = [
{ role: "system", content: "You are a helpful, friendly AI assistant. Provide clear, concise, and accurate responses." }
];
this.isGenerating = false;
this.elements = {
loadingScreen: document.getElementById('loadingScreen'),
chatInterface: document.getElementById('chatInterface'),
messagesContainer: document.getElementById('messagesContainer'),
userInput: document.getElementById('userInput'),
sendButton: document.getElementById('sendButton'),
loadingStatus: document.getElementById('loadingStatus'),
progressFill: document.getElementById('progressFill'),
progressText: document.getElementById('progressText')
};
this.init();
}
async init() {
try {
await this.loadModel();
this.setupEventListeners();
this.showChatInterface();
} catch (error) {
console.error('Initialization error:', error);
this.showError('Failed to initialize the application. Please refresh the page.');
}
}
async loadModel() {
this.updateLoadingStatus('Downloading model...', 0);
try {
this.generator = await pipeline(
"text-generation",
"onnx-community/Qwen2.5-0.5B-Instruct",
{
dtype: "q8",
device: "webgpu",
progress_callback: (progress) => {
if (progress.status === 'progress') {
const percent = Math.round((progress.loaded / progress.total) * 100);
this.updateLoadingStatus(
`Loading ${progress.file}...`,
percent
);
} else if (progress.status === 'done') {
this.updateLoadingStatus('Model loaded successfully!', 100);
}
}
}
);
} catch (error) {
console.log('WebGPU not available, falling back to WASM');
this.generator = await pipeline(
"text-generation",
"onnx-community/Qwen2.5-0.5B-Instruct",
{
dtype: "q8",
progress_callback: (progress) => {
if (progress.status === 'progress') {
const percent = Math.round((progress.loaded / progress.total) * 100);
this.updateLoadingStatus(
`Loading ${progress.file}...`,
percent
);
}
}
}
);
}
}
updateLoadingStatus(message, percent) {
this.elements.loadingStatus.textContent = message;
this.elements.progressFill.style.width = `${percent}%`;
this.elements.progressText.textContent = `${percent}%`;
}
showChatInterface() {
setTimeout(() => {
this.elements.loadingScreen.style.display = 'none';
this.elements.chatInterface.style.display = 'flex';
this.elements.userInput.focus();
}, 500);
}
setupEventListeners() {
// Send button click
this.elements.sendButton.addEventListener('click', () => this.handleSend());
// Enter key to send (Shift+Enter for new line)
this.elements.userInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
this.handleSend();
}
});
// Auto-resize textarea
this.elements.userInput.addEventListener('input', (e) => {
e.target.style.height = 'auto';
e.target.style.height = Math.min(e.target.scrollHeight, 120) + 'px';
this.elements.sendButton.disabled = !e.target.value.trim() || this.isGenerating;
});
// Suggestion buttons
document.querySelectorAll('.suggestion-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
const prompt = e.target.dataset.prompt;
this.elements.userInput.value = prompt;
this.elements.userInput.dispatchEvent(new Event('input'));
this.handleSend();
});
});
}
async handleSend() {
const message = this.elements.userInput.value.trim();
if (!message || this.isGenerating) return;
// Clear input
this.elements.userInput.value = '';
this.elements.userInput.style.height = 'auto';
this.elements.sendButton.disabled = true;
// Hide welcome message if present
const welcomeMsg = document.querySelector('.welcome-message');
if (welcomeMsg) welcomeMsg.remove();
// Add user message
this.addMessage(message, 'user');
// Add assistant message placeholder with typing indicator
const assistantMessageDiv = this.addMessage('', 'assistant', true);
const messageContent = assistantMessageDiv.querySelector('.message-content');
// Add to conversation history
this.conversationHistory.push({ role: "user", content: message });
this.isGenerating = true;
try {
let fullResponse = '';
// Create text streamer with callback
const streamer = new TextStreamer(this.generator.tokenizer, {
skip_prompt: true,
skip_special_tokens: true,
callback_function: (text) => {
fullResponse += text;
// Remove typing indicator if present
const typingIndicator = messageContent.querySelector('.typing-indicator');
if (typingIndicator) typingIndicator.remove();
// Update message content
messageContent.textContent = fullResponse;
this.scrollToBottom();
}
});
// Generate response
const output = await this.generator(this.conversationHistory, {
max_new_tokens: 512,
do_sample: false,
streamer: streamer,
});
// Get final response
const finalResponse = output[0].generated_text.at(-1).content;
// Update conversation history
this.conversationHistory.push({ role: "assistant", content: finalResponse });
// Ensure final text is displayed
messageContent.textContent = finalResponse;
} catch (error) {
console.error('Generation error:', error);
messageContent.textContent = 'Sorry, I encountered an error generating a response. Please try again.';
} finally {
this.isGenerating = false;
this.elements.sendButton.disabled = false;
this.elements.userInput.focus();
this.scrollToBottom();
}
}
addMessage(content, role, showTyping = false) {
const messageDiv = document.createElement('div');
messageDiv.className = `message ${role}-message`;
const avatar = document.createElement('div');
avatar.className = 'message-avatar';
avatar.textContent = role === 'user' ? '👤' : '🤖';
const messageContent = document.createElement('div');
messageContent.className = 'message-content';
if (showTyping) {
const typingIndicator = document.createElement('div');
typingIndicator.className = 'typing-indicator';
typingIndicator.innerHTML = '';
messageContent.appendChild(typingIndicator);
} else {
messageContent.textContent = content;
}
messageDiv.appendChild(avatar);
messageDiv.appendChild(messageContent);
this.elements.messagesContainer.appendChild(messageDiv);
this.scrollToBottom();
return messageDiv;
}
scrollToBottom() {
this.elements.messagesContainer.scrollTop = this.elements.messagesContainer.scrollHeight;
}
showError(message) {
this.elements.loadingStatus.textContent = message;
this.elements.loadingStatus.style.color = '#FF3B30';
}
}
// Initialize the app
new ChatApp();