// coder.js import * as webllm from "https://esm.run/@mlc-ai/web-llm"; // Ensure the script runs after the DOM is fully loaded document.addEventListener("DOMContentLoaded", () => { // Initialize the Young Coder section const coderMessages = [ { content: "You are Aged Guru, an intelligent assistant skilled in coding and software development. Provide insightful and comprehensive answers to complex programming questions.", role: "system" } ]; const coderAvailableModels = webllm.prebuiltAppConfig.model_list.map( (m) => m.model_id ); let coderSelectedModel = "Qwen2.5-Coder-1.5B-Instruct-q4f16_1-MLC"; // Default model function coderUpdateEngineInitProgressCallback(report) { console.log("Coder Initialize", report.progress); // Instead of updating a status span, log the progress logMessage(`Model Initialization Progress: ${report.text}`, "system"); } const coderEngine = new webllm.MLCEngine(); coderEngine.setInitProgressCallback(coderUpdateEngineInitProgressCallback); let coderIsGenerating = false; // Flag to prevent multiple generations async function coderInitializeWebLLMEngine() { logMessage("Model initialization started.", "system"); document.getElementById("coder-loading-spinner").classList.remove("hidden"); // Show spinner coderSelectedModel = document.getElementById("coder-model-selection").value; const config = { temperature: 0.7, // Adjusted for more precise answers top_p: 0.9 }; try { await coderEngine.reload(coderSelectedModel, config); document.getElementById("coder-selected-model").textContent = coderSelectedModel; document.getElementById("coder-start_button").disabled = false; document.getElementById("coder-text-input").disabled = false; // Enable text input after initialization document.getElementById("coder-submit-button").disabled = false; // Enable submit button after initialization document.getElementById("coder-speech-controls").disabled = false; // Enable speech controls after initialization document.getElementById("coder-configuration").classList.remove("hidden"); logMessage("Model initialized successfully.", "system"); } catch (error) { console.error("Error initializing the model:", error); alert("Failed to initialize the model. Please try again."); logMessage("Failed to initialize the model.", "error"); } finally { document.getElementById("coder-loading-spinner").classList.add("hidden"); // Hide spinner } } async function coderStreamingGenerating(messages, onUpdate, onFinish, onError) { if (coderIsGenerating) { console.warn("Coder Generation already in progress."); return; } coderIsGenerating = true; try { let curMessage = ""; const completion = await coderEngine.chat.completions.create({ stream: true, messages }); for await (const chunk of completion) { const curDelta = chunk.choices[0].delta.content; if (curDelta) { curMessage += curDelta; } onUpdate(curMessage); } const finalMessage = await coderEngine.getMessage(); console.log(`Coder Generated final message: ${finalMessage}`); // Debugging onFinish(finalMessage); logMessage("Response generated successfully.", "system"); } catch (err) { console.error(err); onError(err); logMessage("An error occurred during response generation.", "error"); } finally { coderIsGenerating = false; } } // Flag to track the last input method let coderLastInputWasVoice = false; function coderAppendMessage(message) { console.log(`Coder Appending message: ${message.content} (Role: ${message.role})`); // Debugging const coderChatBox = document.getElementById("coder-chat-box"); // Check if the assistant's message is already appended to avoid duplication if (message.role === "assistant") { const existingMessages = coderChatBox.querySelectorAll(".message"); const lastMessage = existingMessages[existingMessages.length - 1]; if (lastMessage && lastMessage.textContent === message.content) { console.warn("Duplicate assistant message detected in Coder section, skipping append."); // Only trigger TTS for assistant messages if the last input was via voice if (message.role === "assistant" && message.content !== "typing..." && coderLastInputWasVoice) { coderSpeak(message.content); } return; // Exit to avoid appending the same message twice } } const container = document.createElement("div"); container.classList.add("message-container"); const newMessage = document.createElement("div"); newMessage.classList.add("message"); newMessage.textContent = message.content; if (message.role === "user") { container.classList.add("user"); } else { container.classList.add("assistant"); } container.appendChild(newMessage); coderChatBox.appendChild(container); coderChatBox.scrollTop = coderChatBox.scrollHeight; // Only trigger TTS for assistant messages if the last input was via voice if (message.role === "assistant" && message.content !== "typing..." && coderLastInputWasVoice) { coderSpeak(message.content); } } function coderUpdateLastMessage(content) { const messageDoms = document.getElementById("coder-chat-box").querySelectorAll(".message"); const lastMessageDom = messageDoms[messageDoms.length - 1]; lastMessageDom.textContent = content; } function coderOnSpeechRecognized(transcript) { const input = transcript.trim(); const message = { content: input, role: "user" }; if (input.length === 0) { return; } coderLastInputWasVoice = true; // Set flag as voice input console.log(`Coder Voice input received: ${input}`); // Debugging document.getElementById("coder-start_button").disabled = true; document.getElementById("coder-submit-button").disabled = true; // Disable submit button during processing coderMessages.push(message); coderAppendMessage(message); logMessage(`User (Voice): ${input}`, "user"); // Append "typing..." placeholder const aiPlaceholder = { content: "typing...", role: "assistant" }; coderAppendMessage(aiPlaceholder); logMessage("CoderBot is typing...", "system"); const onFinishGenerating = (finalMessage) => { console.log(`Coder Finishing generation with message: ${finalMessage}`); // Debugging // Remove the "typing..." placeholder const coderChatBox = document.getElementById("coder-chat-box"); const lastMessageContainer = coderChatBox.lastElementChild; if (lastMessageContainer && lastMessageContainer.querySelector(".message").textContent === "typing...") { coderChatBox.removeChild(lastMessageContainer); } // Append the final message const aiMessage = { content: finalMessage, role: "assistant" }; coderAppendMessage(aiMessage); logMessage(`CoderBot: ${finalMessage}`, "assistant"); document.getElementById("coder-start_button").disabled = false; document.getElementById("coder-submit-button").disabled = false; // Re-enable submit button after processing coderEngine.runtimeStatsText().then((statsText) => { document.getElementById("coder-chat-stats").classList.remove("hidden"); document.getElementById("coder-chat-stats").textContent = statsText; logMessage(`Runtime Stats: ${statsText}`, "system"); }); }; coderStreamingGenerating( coderMessages, coderUpdateLastMessage, onFinishGenerating, (err) => { console.error(err); alert("An error occurred while generating the response. Please try again."); logMessage("Error during response generation.", "error"); document.getElementById("coder-start_button").disabled = false; document.getElementById("coder-submit-button").disabled = false; } ); } // Speech Recognition Code for Coder let coderRecognizing = false; let coderIgnore_onend; let coderFinal_transcript = ''; let coderRecognition; function coderStartButton(event) { if (coderRecognizing) { coderRecognition.stop(); return; } coderFinal_transcript = ''; coderRecognition.lang = 'en-US'; coderRecognition.start(); coderIgnore_onend = false; document.getElementById("coder-start_button").classList.add("mic-animate"); logMessage("Voice input started.", "system"); } if (!('webkitSpeechRecognition' in window)) { alert("Web Speech API is not supported by this browser."); logMessage("Web Speech API is not supported by this browser.", "error"); } else { coderRecognition = new webkitSpeechRecognition(); coderRecognition.continuous = false; // Non-continuous recognition coderRecognition.interimResults = false; // Get only final results coderRecognition.onstart = function() { coderRecognizing = true; logMessage("Speech recognition started.", "system"); }; coderRecognition.onerror = function(event) { if (event.error == 'no-speech') { document.getElementById("coder-start_button").classList.remove("mic-animate"); alert('No speech was detected in Coder section.'); logMessage("No speech detected.", "error"); coderIgnore_onend = true; } if (event.error == 'audio-capture') { document.getElementById("coder-start_button").classList.remove("mic-animate"); alert('No microphone was found in Coder section.'); logMessage("No microphone found.", "error"); coderIgnore_onend = true; } if (event.error == 'not-allowed') { alert('Permission to use microphone was denied in Coder section.'); logMessage("Microphone permission denied.", "error"); coderIgnore_onend = true; } }; coderRecognition.onend = function() { coderRecognizing = false; document.getElementById("coder-start_button").classList.remove("mic-animate"); logMessage("Speech recognition ended.", "system"); if (coderIgnore_onend) { return; } if (!coderFinal_transcript) { logMessage("No transcript captured.", "error"); return; } // Process the final transcript coderOnSpeechRecognized(coderFinal_transcript); }; coderRecognition.onresult = function(event) { for (let i = event.resultIndex; i < event.results.length; ++i) { if (event.results[i].isFinal) { coderFinal_transcript += event.results[i][0].transcript; } } coderFinal_transcript = coderFinal_transcript.trim(); logMessage(`Recognized Speech: ${coderFinal_transcript}`, "user"); }; } document.getElementById("coder-start_button").addEventListener("click", function(event) { coderStartButton(event); }); // Initialize Model Selection coderAvailableModels.forEach((modelId) => { const option = document.createElement("option"); option.value = modelId; option.textContent = modelId; document.getElementById("coder-model-selection").appendChild(option); }); document.getElementById("coder-model-selection").value = coderSelectedModel; // **Enable the Download Model button after models are loaded** document.getElementById("coder-download").disabled = false; document.getElementById("coder-download").addEventListener("click", function () { coderInitializeWebLLMEngine().then(() => { document.getElementById("coder-start_button").disabled = false; // Enable speech controls after model initialization document.getElementById("coder-speech-rate").disabled = false; document.getElementById("coder-speech-pitch").disabled = false; logMessage("Model download initiated.", "system"); }); }); document.getElementById("coder-clear-logs").addEventListener("click", function () { document.getElementById("coder-logs").innerHTML = ''; logMessage("Logs cleared.", "system"); }); // ===== TTS Integration ===== // Initialize Speech Synthesis let coderSpeech = new SpeechSynthesisUtterance(); coderSpeech.lang = "en"; let coderVoices = []; // Use addEventListener instead of directly assigning to onvoiceschanged window.speechSynthesis.addEventListener("voiceschanged", () => { coderVoices = window.speechSynthesis.getVoices(); coderPopulateVoices(); }); function coderPopulateVoices() { const voiceSelect = document.getElementById("coder-tools"); voiceSelect.innerHTML = ''; // Clear existing options coderVoices.forEach((voice, i) => { const option = new Option(voice.name, i); voiceSelect.appendChild(option); }); if (coderVoices.length > 0) { const savedVoice = localStorage.getItem("coderSelectedVoice"); if (savedVoice !== null && coderVoices[savedVoice]) { coderSpeech.voice = coderVoices[savedVoice]; voiceSelect.value = savedVoice; } else { coderSpeech.voice = coderVoices[0]; } } } // Voice Selection Event Listener document.getElementById("coder-tools").addEventListener("change", () => { const selectedVoiceIndex = document.getElementById("coder-tools").value; coderSpeech.voice = coderVoices[selectedVoiceIndex]; // Save to localStorage localStorage.setItem("coderSelectedVoice", selectedVoiceIndex); logMessage(`Voice changed to: ${coderVoices[selectedVoiceIndex].name}`, "system"); }); // Function to Speak Text with Voice Selection and Handling Large Texts function coderSpeak(text) { if (!window.speechSynthesis) { console.warn("Speech Synthesis not supported in this browser for Coder section."); logMessage("Speech Synthesis not supported in this browser.", "error"); return; } // Show spinner and enable Stop button document.getElementById("coder-loading-spinner").classList.remove("hidden"); document.getElementById("coder-stop_button").disabled = false; logMessage("TTS started.", "system"); // Retrieve the currently selected voice const selectedVoice = coderSpeech.voice; // Split the text into sentences to manage large texts const sentences = text.match(/[^\.!\?]+[\.!\?]+/g) || [text]; let utterancesCount = sentences.length; sentences.forEach(sentence => { const utterance = new SpeechSynthesisUtterance(sentence.trim()); // Assign the selected voice to the utterance if (selectedVoice) { utterance.voice = selectedVoice; } // Assign rate and pitch from sliders const rate = parseFloat(document.getElementById("coder-speech-rate").value); const pitch = parseFloat(document.getElementById("coder-speech-pitch").value); utterance.rate = rate; // Adjust the speaking rate (0.1 to 10) utterance.pitch = pitch; // Adjust the pitch (0 to 2) // Add event listeners for debugging or additional functionality utterance.onstart = () => { console.log("Speech started:", sentence); logMessage(`TTS started: ${sentence.trim()}`, "system"); }; utterance.onend = () => { console.log("Speech ended:", sentence); logMessage(`TTS ended: ${sentence.trim()}`, "system"); utterancesCount--; if (utterancesCount === 0) { // Hide spinner and disable Stop button when all utterances have been spoken document.getElementById("coder-loading-spinner").classList.add("hidden"); document.getElementById("coder-stop_button").disabled = true; logMessage("All TTS messages have been spoken.", "system"); } }; utterance.onerror = (e) => { console.error("Speech Synthesis Error:", e); alert("An error occurred during speech synthesis. Please try again."); logMessage("Speech synthesis encountered an error.", "error"); utterancesCount = 0; document.getElementById("coder-loading-spinner").classList.add("hidden"); document.getElementById("coder-stop_button").disabled = true; }; window.speechSynthesis.speak(utterance); }); } // ===== New: Stop Speech Functionality ===== /** * Stops any ongoing speech synthesis. */ function coderStopSpeech() { if (window.speechSynthesis.speaking) { window.speechSynthesis.cancel(); document.getElementById("coder-loading-spinner").classList.add("hidden"); document.getElementById("coder-stop_button").disabled = true; logMessage("Speech synthesis stopped by user.", "system"); } } // Event Listener for Stop Button document.getElementById("coder-stop_button").addEventListener("click", function () { coderStopSpeech(); }); // ===== New: Text Input Handling ===== // Function to Handle Text Submission function coderHandleTextSubmit() { const textInput = document.getElementById("coder-text-input"); const input = textInput.value.trim(); if (input.length === 0) { return; } textInput.value = ''; // Clear the input field const message = { content: input, role: "user" // Ensure this is correctly set }; console.log(`Coder Text input received: ${input}`); // Debugging logMessage(`User: ${input}`, "user"); coderLastInputWasVoice = false; // Set flag as text input document.getElementById("coder-submit-button").disabled = true; // Disable to prevent multiple submissions coderMessages.push(message); coderAppendMessage(message); // Append "typing..." placeholder const aiPlaceholder = { content: "typing...", role: "assistant" }; coderAppendMessage(aiPlaceholder); logMessage("CoderBot is typing...", "system"); const onFinishGenerating = (finalMessage) => { console.log(`Coder Finishing generation with message: ${finalMessage}`); // Debugging // Remove the "typing..." placeholder const coderChatBox = document.getElementById("coder-chat-box"); const lastMessageContainer = coderChatBox.lastElementChild; if (lastMessageContainer && lastMessageContainer.querySelector(".message").textContent === "typing...") { coderChatBox.removeChild(lastMessageContainer); } // Append the final message const aiMessage = { content: finalMessage, role: "assistant" }; coderAppendMessage(aiMessage); logMessage(`CoderBot: ${finalMessage}`, "assistant"); // Trigger TTS for assistant messages if required if (coderLastInputWasVoice) { coderSpeak(finalMessage); } document.getElementById("coder-submit-button").disabled = false; // Re-enable submit button after processing coderEngine.runtimeStatsText().then((statsText) => { document.getElementById("coder-chat-stats").classList.remove("hidden"); document.getElementById("coder-chat-stats").textContent = statsText; logMessage(`Runtime Stats: ${statsText}`, "system"); }); }; coderStreamingGenerating( coderMessages, coderUpdateLastMessage, onFinishGenerating, (err) => { console.error(err); alert("An error occurred while generating the response. Please try again."); logMessage("Error during response generation.", "error"); document.getElementById("coder-submit-button").disabled = false; } ); } // Event Listener for Submit Button document.getElementById("coder-submit-button").addEventListener("click", function () { coderHandleTextSubmit(); }); // Event Listener for Enter Key in Text Input document.getElementById("coder-text-input").addEventListener("keypress", function (e) { if (e.key === 'Enter') { coderHandleTextSubmit(); } }); // ===== Persisting User Preferences ===== // Load Preferences on Initialization window.addEventListener("load", () => { const savedVoice = localStorage.getItem("coderSelectedVoice"); if (savedVoice !== null && coderVoices[savedVoice]) { document.getElementById("coder-tools").value = savedVoice; coderSpeech.voice = coderVoices[savedVoice]; logMessage(`Loaded saved voice: ${coderVoices[savedVoice].name}`, "system"); } const savedRate = localStorage.getItem("coderSpeechRate"); if (savedRate !== null) { document.getElementById("coder-speech-rate").value = savedRate; coderSpeech.rate = parseFloat(savedRate); logMessage(`Loaded saved speech rate: ${savedRate}`, "system"); } const savedPitch = localStorage.getItem("coderSpeechPitch"); if (savedPitch !== null) { document.getElementById("coder-speech-pitch").value = savedPitch; coderSpeech.pitch = parseFloat(savedPitch); logMessage(`Loaded saved speech pitch: ${savedPitch}`, "system"); } }); // Save Speech Rate document.getElementById("coder-speech-rate").addEventListener("input", (e) => { const rate = e.target.value; coderSpeech.rate = parseFloat(rate); localStorage.setItem("coderSpeechRate", rate); logMessage(`Speech rate changed to: ${rate}`, "system"); }); // Save Speech Pitch document.getElementById("coder-speech-pitch").addEventListener("input", (e) => { const pitch = e.target.value; coderSpeech.pitch = parseFloat(pitch); localStorage.setItem("coderSpeechPitch", pitch); logMessage(`Speech pitch changed to: ${pitch}`, "system"); }); // ===== Logging Function ===== /** * Logs messages to the #coder-logs container. * @param {string} message - The message to log. * @param {string} type - The type of message: 'user', 'assistant', 'system', 'error'. */ function logMessage(message, type) { const coderLogs = document.getElementById("coder-logs"); const logEntry = document.createElement("div"); logEntry.classList.add("log-entry"); logEntry.textContent = `[${type.toUpperCase()}] ${message}`; // Style log entries based on type switch(type) { case 'user': logEntry.style.color = "#00796B"; break; case 'assistant': logEntry.style.color = "#004D40"; break; case 'system': logEntry.style.color = "#555555"; break; case 'error': logEntry.style.color = "#E53935"; break; default: logEntry.style.color = "#000000"; } coderLogs.appendChild(logEntry); coderLogs.scrollTop = coderLogs.scrollHeight; } // ===== TTS Integration Continued ===== // Optional: Global Listener to Detect When All Speech Has Finished window.speechSynthesis.addEventListener('end', () => { console.log("All coder speech has been spoken."); logMessage("All TTS messages have been spoken.", "system"); // Ensure Stop button is disabled after speech ends document.getElementById("coder-stop_button").disabled = true; }); });