gg / index.html
kimhyunwoo's picture
Update index.html
fa541aa verified
raw
history blame
24.9 kB
<!DOCTYPE html>
<html lang="en"> {/* Language set to English */}
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>AI Assistant (Gemma 3 1B)</title> {/* English Title */}
<style>
/* Google Fonts (Using a common English font) */
@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap');
/* CSS Variables (Neutral theme) */
:root {
--primary-color: #007bff; /* Standard blue */
--secondary-color: #6c757d; /* Gray */
--text-color: #212529;
--bg-color: #f8f9fa;
--user-msg-bg: #e7f5ff; /* Light blue */
--user-msg-text: #004085;
--bot-msg-bg: #ffffff;
--bot-msg-border: #dee2e6;
--system-msg-color: #6c757d;
--border-color: #dee2e6;
--input-bg: #ffffff;
--input-border: #ced4da;
--button-bg: var(--primary-color);
--button-hover-bg: #0056b3;
--button-disabled-bg: #adb5bd;
--scrollbar-thumb: var(--primary-color);
--scrollbar-track: #e9ecef;
--header-bg: #ffffff;
--header-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
--container-shadow: 0 4px 15px rgba(0, 0, 0, 0.07);
}
/* Reset and Base Styles */
* { box-sizing: border-box; margin: 0; padding: 0; }
html { height: 100%; }
body {
font-family: 'Roboto', sans-serif; /* Changed Font */
display: flex; flex-direction: column; align-items: center; justify-content: center;
min-height: 100vh; background-color: var(--bg-color); color: var(--text-color);
padding: 5px; overscroll-behavior: none;
}
/* Chat Container */
#chat-container {
width: 100%; max-width: 600px; height: calc(100vh - 10px); max-height: 800px;
background-color: #ffffff; border-radius: 12px; /* Less rounded */
box-shadow: var(--container-shadow); display: flex; flex-direction: column;
overflow: hidden; border: 1px solid var(--border-color);
}
/* Header */
h1 {
text-align: center; color: var(--primary-color); padding: 15px;
background-color: var(--header-bg); border-bottom: 1px solid var(--border-color);
font-size: 1.2em; font-weight: 500; flex-shrink: 0; box-shadow: var(--header-shadow);
position: relative; z-index: 10;
}
/* Chatbox Area */
#chatbox {
flex-grow: 1; overflow-y: auto; padding: 15px; display: flex; flex-direction: column;
gap: 12px; scrollbar-width: thin; scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track);
background-color: var(--bg-color); /* Match body background */
}
#chatbox::-webkit-scrollbar { width: 6px; }
#chatbox::-webkit-scrollbar-track { background: var(--scrollbar-track); border-radius: 3px; }
#chatbox::-webkit-scrollbar-thumb { background-color: var(--scrollbar-thumb); border-radius: 3px; }
/* Message Bubbles */
#messages div {
padding: 10px 15px; border-radius: 16px; max-width: 85%; word-wrap: break-word;
line-height: 1.5; font-size: 1em; box-shadow: 0 1px 2px rgba(0,0,0,0.05);
position: relative; animation: fadeIn 0.25s ease-out;
}
@keyframes fadeIn { from { opacity: 0; transform: translateY(5px); } to { opacity: 1; transform: translateY(0); } }
.user-message {
background: var(--user-msg-bg); color: var(--user-msg-text); align-self: flex-end;
border-bottom-right-radius: 4px; margin-left: auto;
}
.bot-message {
background-color: var(--bot-msg-bg); border: 1px solid var(--bot-msg-border); align-self: flex-start;
border-bottom-left-radius: 4px; margin-right: auto;
}
.bot-message a { color: var(--primary-color); text-decoration: none; }
.bot-message a:hover { text-decoration: underline; }
.system-message {
font-style: italic; color: var(--system-msg-color); text-align: center; font-size: 0.85em;
background-color: transparent; box-shadow: none; align-self: center; max-width: 100%;
padding: 5px 0; animation: none;
}
/* Loading & Status Indicators */
.status-indicator {
text-align: center; padding: 8px 0; color: var(--system-msg-color); font-size: 0.9em;
height: 24px; display: flex; align-items: center; justify-content: center; gap: 8px;
flex-shrink: 0; background-color: var(--bg-color);
}
#loading span.spinner {
display: inline-block; width: 14px; height: 14px; border: 2px solid var(--primary-color);
border-bottom-color: transparent; border-radius: 50%; animation: spin 1s linear infinite; vertical-align: middle;
}
@keyframes spin { to { transform: rotate(360deg); } }
/* Input Area */
#input-area {
display: flex; padding: 10px 12px; border-top: 1px solid var(--border-color);
background-color: var(--header-bg); align-items: center; gap: 8px; flex-shrink: 0;
}
#userInput {
flex-grow: 1; padding: 10px 15px; border: 1px solid var(--input-border);
border-radius: 20px; outline: none; font-size: 1em; font-family: 'Roboto', sans-serif;
background-color: var(--input-bg); transition: border-color 0.2s ease;
min-height: 42px; resize: none; overflow-y: auto;
}
#userInput:focus { border-color: var(--primary-color); }
/* Buttons */
.control-button {
padding: 0; border: none; border-radius: 50%; cursor: pointer;
background-color: var(--button-bg); color: white; width: 42px; height: 42px;
font-size: 1.3em; display: flex; align-items: center; justify-content: center;
flex-shrink: 0; transition: background-color 0.2s ease, transform 0.1s ease;
box-shadow: 0 1px 2px rgba(0,0,0,0.08);
}
.control-button:hover:not(:disabled) { background-color: var(--button-hover-bg); transform: translateY(-1px); }
.control-button:active:not(:disabled) { transform: scale(0.95); }
.control-button:disabled { background-color: var(--button-disabled-bg); cursor: not-allowed; transform: none; box-shadow: none; }
#toggleSpeakerButton.muted { background-color: #aaa; }
/* Responsive Design */
@media (max-width: 600px) {
body { padding: 0; }
#chat-container { width: 100%; height: 100vh; max-height: none; border-radius: 0; border: none; box-shadow: none; }
h1 { font-size: 1.1em; padding: 12px; }
#chatbox { padding: 12px 8px; gap: 10px; }
#messages div { max-width: 90%; font-size: 0.95em; padding: 9px 14px;}
#input-area { padding: 8px; gap: 5px; }
#userInput { padding: 9px 14px; min-height: 40px; }
.control-button { width: 40px; height: 40px; font-size: 1.2em; }
}
</style>
<!-- Import map -->
<script type="importmap">
{ "imports": { "@xenova/transformers": "https://cdn.jsdelivr.net/npm/@xenova/[email protected]" } }
</script>
</head>
<body>
<div id="chat-container">
<h1 id="chatbot-name">AI Assistant</h1> {/* English Header */}
<div id="loading" class="status-indicator" style="display: none;"></div>
<div id="speech-status" class="status-indicator" style="display: none;"></div>
<div id="chatbox">
<div id="messages">
<!-- Chat messages will appear here -->
</div>
</div>
<div id="input-area">
<textarea id="userInput" placeholder="How can I help you today?" rows="1" disabled></textarea> {/* English Placeholder */}
<button id="speechButton" class="control-button" title="Speak message" disabled>🎀</button> {/* English Title */}
<button id="toggleSpeakerButton" class="control-button" title="Toggle AI speech output" disabled>πŸ”Š</button> {/* English Title */}
<button id="sendButton" class="control-button" title="Send message" disabled>➀</button> {/* English Title */}
</div>
</div>
<script type="module">
import { pipeline, env } from '@xenova/transformers';
// --- Configuration ---
const MODEL_NAME = 'onnx-community/gemma-3-1b-it-ONNX-GQA'; // Still using this model
const TASK = 'text-generation';
// No dtype specified to avoid previous loading error
// ONNX Runtime & WebGPU config
env.allowLocalModels = false;
env.useBrowserCache = true;
env.backends.onnx.executionProviders = ['webgpu', 'wasm'];
console.log('Using Execution Providers:', env.backends.onnx.executionProviders);
// --- DOM Elements ---
const chatbox = document.getElementById('messages');
const userInput = document.getElementById('userInput');
const sendButton = document.getElementById('sendButton');
const loadingIndicator = document.getElementById('loading');
const chatbotNameElement = document.getElementById('chatbot-name');
const speechButton = document.getElementById('speechButton');
const toggleSpeakerButton = document.getElementById('toggleSpeakerButton');
const speechStatus = document.getElementById('speech-status');
// --- State Management (English) ---
let generator = null;
let conversationHistory = [];
let botState = {
botName: "AI Assistant", // English Name
userName: "User", // English Default Name
botSettings: { useSpeechOutput: true }
};
const stateKey = 'generalBotState_gemma3_1b_en_v1'; // New key for English version
const historyKey = 'generalBotHistory_gemma3_1b_en_v1';
// --- Web Speech API (English) ---
let recognition = null;
let synthesis = window.speechSynthesis;
let targetVoice = null; // Target English voice
let isListening = false;
// --- Initialization ---
window.addEventListener('load', async () => {
loadState();
chatbotNameElement.textContent = botState.botName;
updateSpeakerButtonUI();
initializeSpeechAPI(); // Initialize Speech API for English
await initializeModel(); // Attempt to load the model
setupInputAutosize();
setTimeout(loadVoices, 500); // Load voices (will look for English)
});
// --- State Persistence ---
function loadState() {
const savedState = localStorage.getItem(stateKey);
if (savedState) {
try {
const loadedState = JSON.parse(savedState);
botState = {
...botState, ...loadedState,
botSettings: { ...botState.botSettings, ...(loadedState.botSettings || {}) },
};
} catch (e) { console.error("Failed to parse state:", e); }
}
const savedHistory = localStorage.getItem(historyKey);
if (savedHistory) {
try { conversationHistory = JSON.parse(savedHistory); displayHistory(); }
catch (e) { console.error("Failed to parse history:", e); conversationHistory = []; }
}
}
function saveState() {
localStorage.setItem(stateKey, JSON.stringify(botState));
localStorage.setItem(historyKey, JSON.stringify(conversationHistory));
}
function displayHistory() {
chatbox.innerHTML = '';
conversationHistory.forEach(msg => displayMessage(msg.sender, msg.text, false));
}
// --- UI Update Functions ---
function displayMessage(sender, text, animate = true) {
const messageDiv = document.createElement('div');
const messageClass = sender === 'user' ? 'user-message' : sender === 'bot' ? 'bot-message' : 'system-message';
messageDiv.classList.add(messageClass);
if (!animate) messageDiv.style.animation = 'none';
text = text.replace(/</g, "<").replace(/>/g, ">");
text = text.replace(/\[(.*?)\]\((.*?)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>');
text = text.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>').replace(/\*(.*?)\*/g, '<em>$1</em>');
text = text.replace(/\n/g, '<br>');
messageDiv.innerHTML = text;
chatbox.appendChild(messageDiv);
chatbox.scrollTo({ top: chatbox.scrollHeight, behavior: animate ? 'smooth' : 'auto' });
}
function setLoading(isLoading, message = "AI thinking...") { // English message
loadingIndicator.style.display = isLoading ? 'flex' : 'none';
loadingIndicator.innerHTML = isLoading ? `<span class="spinner"></span> ${message}` : '';
const disableButtons = isLoading || !generator;
userInput.disabled = disableButtons;
sendButton.disabled = disableButtons || userInput.value.trim() === '';
speechButton.disabled = disableButtons || isListening || !recognition;
toggleSpeakerButton.disabled = disableButtons || !synthesis;
}
function updateSpeakerButtonUI() {
toggleSpeakerButton.textContent = botState.botSettings.useSpeechOutput ? 'πŸ”Š' : 'πŸ”‡';
toggleSpeakerButton.title = botState.botSettings.useSpeechOutput ? 'Turn off AI speech' : 'Turn on AI speech'; // English title
toggleSpeakerButton.classList.toggle('muted', !botState.botSettings.useSpeechOutput);
}
function showSpeechStatus(message) {
speechStatus.textContent = message;
speechStatus.style.display = message ? 'flex' : 'none';
if (message) loadingIndicator.style.display = 'none';
}
function setupInputAutosize() {
userInput.addEventListener('input', () => {
userInput.style.height = 'auto';
userInput.style.height = userInput.scrollHeight + 'px';
sendButton.disabled = userInput.value.trim() === '' || !generator || loadingIndicator.style.display === 'flex';
});
}
// --- Model & AI Logic ---
async function initializeModel() {
setLoading(true, "Connecting to AI model..."); // English message
// **CRITICAL WARNING:** The following model loading might still fail due to potential incompatibility.
displayMessage('system', `[NOTICE] Attempting to load ${MODEL_NAME}... This might take a while.`, false);
displayMessage('system', `[WARNING] If loading fails with 'TypeError', the model ${MODEL_NAME} might be incompatible with this library version. Consider trying 'Xenova/gemma-2b-it'.`, false);
try {
generator = await pipeline(TASK, MODEL_NAME, {
// No dtype option
progress_callback: (progress) => {
const msg = `[Loading: ${progress.status}] ${progress.file ? progress.file.split('/').pop() : ''} (${Math.round(progress.progress || 0)}%)`;
setLoading(true, msg);
},
});
displayMessage('system', "[NOTICE] AI Model ready! How can I assist you?", false); // English message
} catch (error) {
console.error("Model loading failed:", error);
displayMessage('system', `[ERROR] Failed to load AI model: ${error.message}. Please refresh or try a different model.`, false); // English message
setLoading(false);
loadingIndicator.textContent = 'Model Load Failed';
return;
} finally {
setLoading(false);
userInput.disabled = !generator;
sendButton.disabled = userInput.value.trim() === '' || !generator;
speechButton.disabled = !generator || !recognition;
toggleSpeakerButton.disabled = !generator || !synthesis;
if (generator) { userInput.focus(); }
}
}
// Build prompt for English conversation
function buildPrompt() {
const historyLimit = 6;
const recentHistory = conversationHistory.slice(-historyLimit);
// Using Gemma Instruct format for English
let prompt = "<start_of_turn>system\n";
prompt += `You are '${botState.botName}', a helpful AI assistant. Answer the user's questions clearly and concisely in English.\n<end_of_turn>\n`;
recentHistory.forEach(msg => {
const role = msg.sender === 'user' ? 'user' : 'model';
prompt += `<start_of_turn>${role}\n${msg.text}\n<end_of_turn>\n`;
});
prompt += "<start_of_turn>model\n"; // Model response starts here
console.log("Generated English Prompt:", prompt);
return prompt;
}
// Cleanup response (English focus)
function cleanupResponse(responseText, prompt) {
let cleaned = responseText;
if (cleaned.startsWith(prompt)) { cleaned = cleaned.substring(prompt.length); }
else { cleaned = cleaned.replace(/^model\n?/, '').trim(); }
cleaned = cleaned.replace(/<end_of_turn>/g, '').trim();
cleaned = cleaned.replace(/<start_of_turn>/g, '').trim();
cleaned = cleaned.replace(/^['"]/, '').replace(/['"]$/, '');
if (!cleaned || cleaned.length < 2) {
console.warn("Generated reply seems empty:", cleaned);
const fallbacks = [ "Sorry, I didn't quite understand. Could you please rephrase?", "Hmm, I'm not sure how to respond to that. Can you try asking differently?", "Is there something else I can help you with?" ]; // English fallbacks
return fallbacks[Math.floor(Math.random() * fallbacks.length)];
}
return cleaned;
}
// --- Main Interaction Logic ---
async function handleUserMessage() {
const userText = userInput.value.trim();
if (!userText || !generator || loadingIndicator.style.display === 'flex') return;
userInput.value = ''; userInput.style.height = 'auto';
sendButton.disabled = true;
displayMessage('user', userText);
conversationHistory.push({ sender: 'user', text: userText });
setLoading(true);
const prompt = buildPrompt(); // Get English prompt
try {
const outputs = await generator(prompt, {
max_new_tokens: 300,
temperature: 0.7,
repetition_penalty: 1.1,
top_k: 50,
top_p: 0.9,
do_sample: true,
});
const rawResponse = Array.isArray(outputs) ? outputs[0].generated_text : outputs.generated_text;
const replyText = cleanupResponse(rawResponse, prompt);
console.log("Cleaned English Output:", replyText);
displayMessage('bot', replyText);
conversationHistory.push({ sender: 'bot', text: replyText });
if (botState.botSettings.useSpeechOutput && synthesis && targetVoice) {
speakText(replyText); // Speak English response
}
saveState();
} catch (error) {
console.error("AI response generation error:", error);
displayMessage('system', `[ERROR] Failed to generate response: ${error.message}`); // English error
const errorReply = "Sorry, I encountered an error while generating the response. Please try again later."; // English error reply
displayMessage('bot', errorReply);
conversationHistory.push({ sender: 'bot', text: errorReply });
} finally {
setLoading(false);
userInput.focus();
}
}
// --- Speech API Functions (English) ---
function initializeSpeechAPI() {
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
if (SpeechRecognition) {
recognition = new SpeechRecognition();
recognition.lang = 'en-US'; // Set to English
recognition.continuous = false; recognition.interimResults = false;
recognition.onstart = () => { isListening = true; speechButton.disabled = true; speechButton.textContent = 'πŸ‘‚'; showSpeechStatus('Listening...'); }; // English status
recognition.onresult = (event) => { userInput.value = event.results[0][0].transcript; userInput.dispatchEvent(new Event('input')); handleUserMessage(); };
recognition.onerror = (event) => { console.error("Speech error:", event.error); showSpeechStatus(`Speech recognition error (${event.error})`); setTimeout(() => showSpeechStatus(''), 3000); }; // English status
recognition.onend = () => { isListening = false; speechButton.disabled = !generator; speechButton.textContent = '🎀'; if (speechStatus.textContent === 'Listening...') showSpeechStatus(''); };
speechButton.disabled = false;
} else { console.warn("Speech Recognition not supported."); speechButton.style.display = 'none'; }
if (!synthesis) { console.warn("Speech Synthesis not supported."); toggleSpeakerButton.style.display = 'none'; }
else {
toggleSpeakerButton.addEventListener('click', () => { botState.botSettings.useSpeechOutput = !botState.botSettings.useSpeechOutput; updateSpeakerButtonUI(); saveState(); if (!botState.botSettings.useSpeechOutput) synthesis.cancel(); });
toggleSpeakerButton.disabled = false;
}
}
function loadVoices() {
if (!synthesis) return;
let voices = synthesis.getVoices();
if (voices.length === 0) {
synthesis.onvoiceschanged = () => { voices = synthesis.getVoices(); findAndSetVoice(voices); };
} else { findAndSetVoice(voices); }
}
// Find English voice
function findAndSetVoice(voices) {
// Prioritize US English voices
targetVoice = voices.find(v => v.lang === 'en-US');
// Fallback to any English voice
if (!targetVoice) targetVoice = voices.find(v => v.lang.startsWith('en-'));
if (targetVoice) {
console.log("Using English voice:", targetVoice.name, targetVoice.lang);
} else {
console.warn("No suitable English voice found. Speech output might use default voice.");
displayMessage('system', "[NOTICE] No English voice found. Speech output may use the default system voice.", false); // English message
}
}
// Speak English text
function speakText(text) {
if (!synthesis || !botState.botSettings.useSpeechOutput) return;
synthesis.cancel();
const utterance = new SpeechSynthesisUtterance(text);
if (targetVoice) {
utterance.voice = targetVoice;
utterance.lang = targetVoice.lang; // Use the found voice's language
} else {
utterance.lang = 'en-US'; // Default to US English if no voice found
}
utterance.rate = 1.0; utterance.pitch = 1.0;
synthesis.speak(utterance);
}
// --- Event Listeners ---
sendButton.addEventListener('click', handleUserMessage);
userInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleUserMessage(); }
});
speechButton.addEventListener('click', () => {
if (recognition && !isListening && generator) {
try { recognition.start(); }
catch (error) { console.error("Rec start fail:", error); showSpeechStatus(`Failed to start recognition`); setTimeout(() => showSpeechStatus(''), 2000); isListening = false; speechButton.disabled = !generator; speechButton.textContent = '🎀';}
}
});
</script>
</body>
</html>