Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>Always Listening Voice Chat: Smarter Interruption</title>
- <link rel="icon" href="data:,">
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
- <style>
- :root {
- --background-color: #121212;
- --card-color: #1e1e1e;
- --text-color: #e0e0e0;
- --accent-color: #2c7be5;
- --secondary-color: #404040;
- --highlight-color: #60EFFF;
- --success-color: #28a745;
- --speaking-color: #ffc107;
- --processing-color: #fd7e14;
- --danger-color: #dc3545;
- }
- html, body {
- font-family: Arial, sans-serif; margin: 0; padding: 0; height: 100%;
- background-color: var(--background-color); color: var(--text-color); overflow: hidden;
- }
- #container {
- display: flex; flex-direction: column; height: 100vh; max-width: 1200px;
- margin: 0 auto; padding: 20px; box-sizing: border-box;
- }
- .header {
- display: flex; justify-content: space-between; align-items: center;
- margin-bottom: 20px; flex-wrap: wrap;
- }
- h1 {
- margin: 0; color: var(--highlight-color); font-size: 24px; white-space: nowrap;
- }
- .selector-group {
- display: flex; gap: 10px; align-items: center; flex-grow: 1; justify-content: flex-end;
- }
- .selector-group select {
- flex: 1; min-width: 150px; padding: 8px 12px; background-color: var(--card-color);
- color: var(--text-color); border: 1px solid var(--secondary-color); border-radius: 6px; cursor: pointer;
- }
- #settings-btn {
- background: none; border: none; color: var(--text-color); font-size: 20px;
- cursor: pointer; padding: 8px; transition: color 0.2s;
- }
- #settings-btn:hover { color: var(--highlight-color); }
- #listening-info {
- background-color: rgba(44, 123, 229, 0.1); padding: 10px 15px; border-radius: 6px;
- margin-bottom: 15px; border-left: 4px solid var(--accent-color); font-size: 14px;
- min-height: 1.5em;
- }
- .chat-area {
- flex: 1; display: flex; position: relative; margin-bottom: 20px; min-height: 250px;
- overflow: hidden; border-radius: 8px; border: 1px solid var(--secondary-color);
- background-color: rgba(30, 30, 30, 0.5);
- }
- #output {
- position: absolute; top: 0; left: 0; right: 0; bottom: 0; padding: 20px; overflow-y: auto;
- background-color: transparent; z-index: 3; color: var(--text-color); font-size: 18px; line-height: 1.6;
- }
- #output strong { font-weight: bold; }
- #output br { content: ""; display: block; margin-bottom: 0.5em; }
- #text-input {
- width: 100%; padding: 15px; background-color: var(--card-color); color: var(--text-color);
- border: 1px solid var(--secondary-color); border-radius: 6px; min-height: 70px;
- resize: vertical; margin-bottom: 15px; box-sizing: border-box;
- }
- .button-group { display: flex; gap: 10px; }
- .btn {
- padding: 12px 20px; background-color: var(--accent-color); color: white; border: none;
- border-radius: 6px; cursor: pointer; font-size: 16px; transition: background-color 0.2s, color 0.2s; flex: 1;
- }
- .btn:hover:not(:disabled) { background-color: #1a68d1; }
- #voice-btn.listening-mode { background-color: var(--success-color); }
- #voice-btn.speech-detected { background-color: var(--speaking-color); color: #121212; }
- #send-btn.processing, #voice-btn.processing-backend {
- background-color: var(--processing-color); color: #121212;
- }
- #voice-btn:disabled, #send-btn:disabled {
- background-color: var(--secondary-color); color: #777; cursor: not-allowed;
- }
- #status { margin-top: 10px; font-size: 14px; color: #929292; min-height: 20px; }
- #audio-output { margin-top: 15px; width: 100%; }
- .modal {
- display: none; position: fixed; z-index: 10; left: 0; top: 0;
- width: 100%; height: 100%; background-color: rgba(0,0,0,0.7);
- align-items: center; justify-content: center;
- }
- .modal-content {
- background-color: var(--card-color); padding: 25px;
- border: 1px solid var(--secondary-color); border-radius: 8px;
- width: 70%; max-width: 600px; color: var(--text-color);
- box-shadow: 0 5px 15px rgba(0,0,0,0.3);
- }
- .modal-header {
- display: flex; justify-content: space-between; align-items: center;
- margin-bottom: 20px; padding-bottom: 10px;
- border-bottom: 1px solid var(--secondary-color);
- }
- .modal-header h2 { margin: 0; color: var(--highlight-color); }
- .close { color: var(--text-color); font-size: 28px; font-weight: bold; cursor: pointer; }
- .close:hover { color: var(--highlight-color); }
- .modal textarea {
- width: calc(100% - 24px); padding: 12px; height: 150px; margin-bottom: 20px;
- background-color: var(--background-color); color: var(--text-color);
- border: 1px solid var(--secondary-color); border-radius: 6px; resize: vertical;
- }
- .modal .btn { width: 100%; padding: 12px; }
- @media (max-width: 768px) {
- #container { padding: 10px; }
- .header { flex-direction: column; align-items: stretch; }
- h1 { text-align: center; margin-bottom: 15px; }
- .selector-group { width: 100%; margin-top: 10px; flex-direction: column; }
- .selector-group select { width: 100%; margin-bottom: 10px; }
- #settings-btn { align-self: flex-end; }
- .modal-content { width: 90%; }
- .button-group { flex-direction: column; }
- .button-group .btn { margin-bottom: 10px; }
- .button-group .btn:last-child { margin-bottom: 0; }
- }
- </style>
- </head>
- <body>
- <div id="container">
- <div class="header">
- <h1>Always Listening Voice Chat</h1>
- <div class="selector-group">
- <select id="model-selector">
- {% if models %}
- {% for model in models %}
- <option value="{{ model }}">{{ model }}</option>
- {% endfor %}
- {% else %}
- <option value="llama2:latest">llama2 (fallback)</option>
- {% endif %}
- </select>
- <select id="kroko-language-selector">
- <option value="en" selected>English (TTS)</option>
- </select>
- <button id="settings-btn" title="Settings"><i class="fas fa-cog"></i></button>
- </div>
- </div>
- <p id="listening-info">Click "Start Always Listening" to begin.</p>
- <div class="chat-area">
- <div id="output">Welcome! AI is ready.</div>
- </div>
- <textarea id="text-input" placeholder="Transcribed speech will appear here. You can also type."></textarea>
- <div class="button-group">
- <button id="voice-btn" class="btn">Start Always Listening</button>
- <button id="send-btn" class="btn">Send Typed Text</button>
- </div>
- <div id="status">Ready</div>
- <audio id="audio-output" controls style="display: none;"></audio>
- </div>
- <div id="settings-modal" class="modal">
- <div class="modal-content">
- <div class="modal-header">
- <h2>System Prompt Settings</h2>
- <span class="close" onclick="closeSettingsModal()">×</span>
- </div>
- <p>Set a custom system prompt for the AI model (Ollama):</p>
- <textarea id="system-prompt-input" placeholder="e.g., You are a pirate captain."></textarea>
- <button id="save-settings-btn" class="btn">Save Settings</button>
- </div>
- </div>
- <script>
- const voiceBtn = document.getElementById('voice-btn');
- const sendBtn = document.getElementById('send-btn');
- const textInput = document.getElementById('text-input');
- const outputDiv = document.getElementById('output');
- const audioOutput = document.getElementById('audio-output');
- const modelSelector = document.getElementById('model-selector');
- const languageSelector = document.getElementById('kroko-language-selector');
- const statusEl = document.getElementById('status');
- const listeningInfoEl = document.getElementById('listening-info');
- const settingsBtn = document.getElementById('settings-btn');
- const settingsModal = document.getElementById('settings-modal');
- const systemPromptInputEl = document.getElementById('system-prompt-input');
- const saveSettingsBtn = document.getElementById('save-settings-btn');
- let appState = 'IDLE';
- let isAlwaysListeningModeActive = false;
- let isBackendProcessing = false;
- let currentTtsAudio = null;
- let userSystemPrompt = "You are a helpful, friendly AI assistant.";
- let audioContext;
- let mediaStream;
- let scriptProcessorNode;
- const TARGET_SAMPLE_RATE = 16000;
- let vadSpeechBuffer = [];
- let preSpeechBuffer = [];
- let silenceFramesCount = 0;
- let speechFramesCount = 0;
- // No longer need speechStreakForInterrupt with the new logic
- const VAD_ENERGY_THRESHOLD = 0.005;
- const VAD_SILENCE_BUFFERS_NEEDED = 5;
- const VAD_MIN_SPEECH_BUFFERS = 3;
- const VAD_PRE_SPEECH_BUFFER_COUNT = 2;
- function updateUIState(newState) {
- appState = newState;
- console.log("App State:", appState);
- voiceBtn.classList.remove("listening-mode", "speech-detected", "processing-backend");
- voiceBtn.disabled = false;
- sendBtn.disabled = false;
- if (isAlwaysListeningModeActive) {
- if (appState === 'LISTENING') {
- voiceBtn.classList.add("listening-mode");
- voiceBtn.textContent = "Stop Always Listening";
- listeningInfoEl.textContent = "Listening for you to speak...";
- } else if (appState === 'SPEAKING_DETECTED') {
- voiceBtn.classList.add("speech-detected");
- voiceBtn.textContent = "Stop Always Listening (Speaking...)";
- listeningInfoEl.textContent = "Speaking detected, keep going!";
- } else if (appState === 'PROCESSING_AUDIO' || (isBackendProcessing && appState !== 'PLAYING_TTS')) {
- voiceBtn.classList.add("processing-backend");
- voiceBtn.textContent = "Stop Always Listening (Processing...)";
- } else if (appState === 'PLAYING_TTS') {
- voiceBtn.classList.add("listening-mode");
- voiceBtn.textContent = "Stop Always Listening (AI Speaking)";
- listeningInfoEl.textContent = "AI responding... Listening for interruptions...";
- } else {
- voiceBtn.classList.add("listening-mode");
- voiceBtn.textContent = "Stop Always Listening";
- listeningInfoEl.textContent = "Listening for you to speak...";
- }
- } else {
- voiceBtn.textContent = "Start Always Listening";
- listeningInfoEl.textContent = "Click 'Start Always Listening' to begin.";
- }
- if (isBackendProcessing) {
- sendBtn.disabled = true;
- if (appState === 'PROCESSING_AUDIO' && isAlwaysListeningModeActive) {
- // Allow stopping always listening even when processing VAD audio.
- voiceBtn.disabled = false;
- } else if (!isAlwaysListeningModeActive){
- voiceBtn.disabled = true; // If not always listening, disable voice btn when typed text is processing
- }
- }
- }
- function startAlwaysListening() {
- if (isAlwaysListeningModeActive) return;
- navigator.mediaDevices.getUserMedia({ audio: true })
- .then(stream => {
- isAlwaysListeningModeActive = true;
- mediaStream = stream;
- audioContext = new (window.AudioContext || window.webkitAudioContext)({ sampleRate: TARGET_SAMPLE_RATE });
- const source = audioContext.createMediaStreamSource(stream);
- scriptProcessorNode = audioContext.createScriptProcessor(4096, 1, 1);
- vadSpeechBuffer = [];
- preSpeechBuffer = [];
- silenceFramesCount = 0;
- speechFramesCount = 0;
- scriptProcessorNode.onaudioprocess = (e) => {
- if (!isAlwaysListeningModeActive || (isBackendProcessing && appState !== 'PLAYING_TTS')) {
- // If backend is busy with a VAD-triggered request, VAD pauses to prevent overload,
- // UNLESS TTS is playing, in which case VAD needs to run to detect interruptions for completed utterances.
- // If TTS is playing AND backend becomes busy due to an interruption, this guard will then take effect.
- return;
- }
- const samples = e.inputBuffer.getChannelData(0);
- const currentAudioChunk = new Float32Array(samples);
- let energy = 0;
- for (let i = 0; i < currentAudioChunk.length; i++) energy += currentAudioChunk[i] * currentAudioChunk[i];
- energy = Math.sqrt(energy / currentAudioChunk.length);
- if (energy > VAD_ENERGY_THRESHOLD) {
- silenceFramesCount = 0;
- if (appState === 'LISTENING' || appState === 'PLAYING_TTS') {
- // If sound starts while listening or while TTS is playing,
- // we transition to SPEAKING_DETECTED. TTS is NOT interrupted here.
- updateUIState('SPEAKING_DETECTED');
- vadSpeechBuffer = [...preSpeechBuffer.slice(-VAD_PRE_SPEECH_BUFFER_COUNT)];
- vadSpeechBuffer.push(currentAudioChunk);
- speechFramesCount = vadSpeechBuffer.length; // Count actual frames buffered
- preSpeechBuffer = [];
- } else if (appState === 'SPEAKING_DETECTED') {
- vadSpeechBuffer.push(currentAudioChunk);
- speechFramesCount++;
- }
- } else { // Silence detected
- if (appState === 'SPEAKING_DETECTED') {
- vadSpeechBuffer.push(currentAudioChunk);
- silenceFramesCount++;
- if (silenceFramesCount >= VAD_SILENCE_BUFFERS_NEEDED) {
- if (speechFramesCount >= VAD_MIN_SPEECH_BUFFERS) {
- console.log("VAD: End of speech confirmed by silence.");
- // --- TTS INTERSECT POINT ---
- // Now that a full utterance is detected, check if TTS needs to be interrupted.
- if (currentTtsAudio && !currentTtsAudio.paused) {
- console.log("Interrupting TTS because a new complete user utterance is ready.");
- currentTtsAudio.pause();
- currentTtsAudio.src = ''; // Stop it fully
- }
- // --- END TTS INTERSECT POINT ---
- const audioToSend = concatenateFloat32Arrays(vadSpeechBuffer);
- processAndSendAudio(audioToSend);
- } else {
- console.log("VAD: Speech was too short, discarding.");
- if (isAlwaysListeningModeActive) updateUIState('LISTENING');
- }
- vadSpeechBuffer = [];
- speechFramesCount = 0;
- preSpeechBuffer = []; // Clear pre-buffer after an utterance attempt
- }
- } else { // Continuous silence, not in SPEAKING_DETECTED
- preSpeechBuffer.push(currentAudioChunk);
- if (preSpeechBuffer.length > VAD_PRE_SPEECH_BUFFER_COUNT) {
- preSpeechBuffer.shift();
- }
- // If TTS just finished and it's silent, ensure state is LISTENING
- if (isAlwaysListeningModeActive && appState === 'PLAYING_TTS' && (!currentTtsAudio || currentTtsAudio.paused)) {
- updateUIState('LISTENING');
- } else if (isAlwaysListeningModeActive && appState !== 'LISTENING' && !isBackendProcessing) {
- // If it's not any other active state, default to listening
- updateUIState('LISTENING');
- }
- }
- }
- };
- source.connect(scriptProcessorNode);
- scriptProcessorNode.connect(audioContext.destination);
- updateUIState('LISTENING');
- updateStatus("Always listening mode activated.");
- })
- .catch(error => {
- console.error("Error starting microphone capture:", error);
- updateStatus(`Mic Error: ${error.message}`);
- isAlwaysListeningModeActive = false;
- updateUIState('IDLE');
- });
- }
- function stopAlwaysListening() { /* ... (Keep this function exactly the same as previous version) ... */
- if (!isAlwaysListeningModeActive) return;
- isAlwaysListeningModeActive = false;
- if (mediaStream) {
- mediaStream.getTracks().forEach(track => track.stop());
- mediaStream = null;
- }
- if (scriptProcessorNode) {
- scriptProcessorNode.disconnect();
- scriptProcessorNode.onaudioprocess = null;
- scriptProcessorNode = null;
- }
- if (audioContext && audioContext.state !== 'closed') {
- audioContext.close().catch(e => console.error("Error closing audio context:", e));
- audioContext = null;
- }
- vadSpeechBuffer = []; preSpeechBuffer = [];
- silenceFramesCount = 0; speechFramesCount = 0;
- updateUIState('IDLE');
- updateStatus("Always listening mode deactivated.");
- }
- function toggleAlwaysListeningMode() { /* ... (Keep this function exactly the same as previous version) ... */
- if (isAlwaysListeningModeActive) {
- stopAlwaysListening();
- } else {
- startAlwaysListening();
- }
- }
- function processAndSendAudio(audioFloat32Array) { /* ... (Keep this function exactly the same as previous version) ... */
- if (audioFloat32Array.length === 0) {
- console.log("processAndSendAudio called with empty buffer.");
- if(isAlwaysListeningModeActive) updateUIState('LISTENING');
- return;
- }
- // appState will be set to PROCESSING_AUDIO within sendAudioToBackend
- updateStatus("Encoding audio for backend ASR...");
- listeningInfoEl.textContent = "Sending audio for transcription...";
- const wavBlob = encodeWAV(audioFloat32Array, TARGET_SAMPLE_RATE);
- sendAudioToBackend(wavBlob);
- }
- function concatenateFloat32Arrays(arrays) { /* ... (Keep this function exactly the same as previous version) ... */
- let totalLength = 0;
- for (const arr of arrays) { totalLength += arr.length; }
- const result = new Float32Array(totalLength);
- let offset = 0;
- for (const arr of arrays) { result.set(arr, offset); offset += arr.length; }
- return result;
- }
- function encodeWAV(samples, sampleRate) { /* ... (Keep this function exactly the same as previous version) ... */
- const buffer = new ArrayBuffer(44 + samples.length * 2);
- const view = new DataView(buffer);
- function writeString(view, offset, string) {
- for (let i = 0; i < string.length; i++) view.setUint8(offset + i, string.charCodeAt(i));
- }
- let offset = 0;
- writeString(view, offset, 'RIFF'); offset += 4;
- view.setUint32(offset, 36 + samples.length * 2, true); offset += 4;
- writeString(view, offset, 'WAVE'); offset += 4;
- writeString(view, offset, 'fmt '); offset += 4;
- view.setUint32(offset, 16, true); offset += 4;
- view.setUint16(offset, 1, true); offset += 2;
- view.setUint16(offset, 1, true); offset += 2;
- view.setUint32(offset, sampleRate, true); offset += 4;
- view.setUint32(offset, sampleRate * 1 * 2, true); offset += 4;
- view.setUint16(offset, 1 * 2, true); offset += 2;
- view.setUint16(offset, 16, true); offset += 2;
- writeString(view, offset, 'data'); offset += 4;
- view.setUint32(offset, samples.length * 2, true); offset += 4;
- for (let i = 0; i < samples.length; i++, offset += 2) {
- const s = Math.max(-1, Math.min(1, samples[i]));
- view.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true);
- }
- return new Blob([view], { type: 'audio/wav' });
- }
- async function sendAudioToBackend(audioBlob) { /* ... (Keep the version that prevents display of "Speech not clear") ... */
- if (isBackendProcessing && appState !== 'PROCESSING_AUDIO') {
- updateStatus("Still processing previous request...");
- if (isAlwaysListeningModeActive) updateUIState('LISTENING'); else updateUIState('IDLE');
- return;
- }
- isBackendProcessing = true;
- sendBtn.disabled = true;
- updateUIState('PROCESSING_AUDIO');
- const formData = new FormData();
- formData.append('audio_data', audioBlob, 'vad_audio.wav');
- formData.append('model', modelSelector.value);
- formData.append('system_prompt', userSystemPrompt);
- try {
- const response = await fetch('/process_voice_input', { method: 'POST', body: formData });
- if (!response.ok) {
- const errorData = await response.json().catch(() => ({ error: `Server error ${response.status}` }));
- throw new Error(errorData.error || `Server Error: ${response.status} ${response.statusText}`);
- }
- const data = await response.json();
- if (data.error && data.text !== "Could not understand audio or audio was silent.") {
- throw new Error(data.error);
- }
- if (data.transcribed_text && data.transcribed_text.trim() !== "") {
- textInput.value = data.transcribed_text;
- if (outputDiv.textContent.startsWith("Welcome!")) {
- outputDiv.innerHTML = `<strong>You (voice):</strong> ${data.transcribed_text}`;
- } else {
- outputDiv.innerHTML += `<br><br><strong>You (voice):</strong> ${data.transcribed_text}`;
- }
- if (data.text && data.text !== "Could not understand audio or audio was silent.") {
- outputDiv.innerHTML += `<br><strong>AI:</strong> ${data.text}`;
- } else {
- console.log("ASR produced no clear text for AI query or AI response was generic 'could not understand'.");
- }
- outputDiv.scrollTop = outputDiv.scrollHeight;
- if (data.audio) {
- updateUIState('PLAYING_TTS');
- audioOutput.src = `data:audio/wav;base64,${data.audio}`;
- audioOutput.style.display = 'block';
- currentTtsAudio = audioOutput;
- await audioOutput.play();
- // onended will handle state transition after play
- } else {
- updateStatus("AI response received (no audio).");
- if (isAlwaysListeningModeActive) updateUIState('LISTENING'); else updateUIState('IDLE');
- }
- } else {
- console.log("Backend ASR resulted in empty transcription. UI not updated for this voice attempt.");
- updateStatus("Couldn't quite catch that. Try speaking again.");
- listeningInfoEl.textContent = "Audio unclear or too short. Listening again...";
- }
- } catch (error) {
- console.error("Error sending audio or processing response:", error);
- outputDiv.innerHTML += `<br><strong style="color:var(--danger-color);">Error:</strong> ${error.message}`;
- updateStatus(`Error: ${error.message}. Check console.`);
- } finally {
- isBackendProcessing = false; // This is key
- // State transition after backend processing is done
- if (isAlwaysListeningModeActive) {
- // If TTS was playing, onended/onerror will transition to LISTENING.
- // If no TTS, or if TTS already finished before this finally, transition to LISTENING.
- if (appState !== 'PLAYING_TTS' || (currentTtsAudio && currentTtsAudio.paused)) {
- updateUIState('LISTENING');
- }
- } else {
- updateUIState('IDLE');
- }
- }
- }
- async function sendTypedText() { /* ... (Keep this function largely the same, ensure UI state updates are sensible) ... */
- const textToSend = textInput.value.trim();
- if (!textToSend || isBackendProcessing) {
- if(!textToSend) updateStatus("Input is empty.");
- return;
- }
- isBackendProcessing = true;
- sendBtn.disabled = true; sendBtn.classList.add("processing");
- let originalVoiceBtnText = voiceBtn.textContent;
- let originalVoiceBtnDisabled = voiceBtn.disabled;
- voiceBtn.disabled = true;
- if(isAlwaysListeningModeActive) voiceBtn.textContent = "Stop (Processing Typed...)";
- updateStatus("Sending typed text to AI...");
- if (outputDiv.textContent.startsWith("Welcome!")) outputDiv.innerHTML = `<strong>You:</strong> ${textToSend}`;
- else outputDiv.innerHTML += `<br><br><strong>You:</strong> ${textToSend}`;
- outputDiv.scrollTop = outputDiv.scrollHeight;
- try {
- const response = await fetch('/process_typed_text', {
- method: 'POST', headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ text: textToSend, model: modelSelector.value, system_prompt: userSystemPrompt })
- });
- if (!response.ok) {
- const errorData = await response.json().catch(() => ({ error: `Server error ${response.status}` }));
- throw new Error(errorData.error || `Server Error: ${response.status} ${response.statusText}`);
- }
- const data = await response.json();
- if (data.error) throw new Error(data.error);
- outputDiv.innerHTML += `<br><strong>AI:</strong> ${data.text}`;
- outputDiv.scrollTop = outputDiv.scrollHeight;
- if (data.audio) {
- // If always listening, TTS playing is a sub-state of it.
- // If not always listening, it goes to IDLE after TTS.
- updateUIState(isAlwaysListeningModeActive ? 'PLAYING_TTS' : 'IDLE');
- audioOutput.src = `data:audio/wav;base64,${data.audio}`;
- audioOutput.style.display = 'block';
- currentTtsAudio = audioOutput;
- await audioOutput.play();
- // onended will handle further state transitions
- } else {
- updateStatus("AI response received (no audio).");
- }
- } catch (error) {
- console.error("Error sending typed text:", error);
- outputDiv.innerHTML += `<br><strong style="color:var(--danger-color);">Error:</strong> ${error.message}`;
- updateStatus(`Error: ${error.message}. Check console.`);
- } finally {
- isBackendProcessing = false;
- sendBtn.disabled = false; sendBtn.classList.remove("processing");
- if (isAlwaysListeningModeActive) {
- // If TTS was playing, onended will transition to LISTENING.
- // Otherwise, ensure it's LISTENING.
- if (appState !== 'PLAYING_TTS' || (currentTtsAudio && currentTtsAudio.paused) ) {
- updateUIState('LISTENING');
- }
- } else {
- voiceBtn.disabled = originalVoiceBtnDisabled; // Restore its specific disabled state
- updateUIState('IDLE');
- }
- }
- }
- function updateStatus(message) {
- statusEl.textContent = message; console.log("Status Update:", message);
- }
- function openSettingsModal() {
- systemPromptInputEl.value = userSystemPrompt; settingsModal.style.display = "flex";
- }
- function closeSettingsModal() {
- settingsModal.style.display = "none";
- }
- function saveSystemPrompt() {
- userSystemPrompt = systemPromptInputEl.value.trim();
- if (!userSystemPrompt) userSystemPrompt = "You are a helpful, friendly AI assistant.";
- localStorage.setItem('ollamaSystemPrompt', userSystemPrompt);
- closeSettingsModal(); updateStatus("System prompt saved.");
- }
- voiceBtn.addEventListener('click', toggleAlwaysListeningMode);
- sendBtn.addEventListener('click', sendTypedText);
- settingsBtn.addEventListener('click', openSettingsModal);
- saveSettingsBtn.addEventListener('click', saveSystemPrompt);
- window.addEventListener('click', (event) => { if (event.target === settingsModal) closeSettingsModal(); });
- textInput.addEventListener('keydown', (event) => {
- if (event.key === 'Enter' && !event.shiftKey) { event.preventDefault(); sendTypedText(); }
- });
- audioOutput.onended = () => {
- updateStatus("Audio playback finished.");
- if (isAlwaysListeningModeActive) { // If mode is still active, go back to listening
- updateUIState('LISTENING');
- } else { // Otherwise, we are idle
- updateUIState('IDLE');
- }
- };
- audioOutput.onerror = (e) => {
- console.error("Error playing TTS audio:", e); updateStatus("Error playing audio.");
- if (isAlwaysListeningModeActive) {
- updateUIState('LISTENING');
- } else {
- updateUIState('IDLE');
- }
- };
- document.addEventListener('DOMContentLoaded', () => {
- const savedPrompt = localStorage.getItem('ollamaSystemPrompt');
- if (savedPrompt) userSystemPrompt = savedPrompt;
- if(systemPromptInputEl) systemPromptInputEl.value = userSystemPrompt;
- updateUIState('IDLE');
- updateStatus("Ready. Click 'Start Always Listening' or type message.");
- });
- </script>
- </body>
- </html>
Add Comment
Please, Sign In to add comment