Guest User

Frontend

a guest
May 15th, 2025
13
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 19.87 KB | None | 0 0
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <title>Voice Chat with Chrome Speech API, Ollama, and Kokoro TTS</title>
  7. <link rel="icon" href="data:,"> <!-- Empty favicon to prevent 404 error -->
  8. <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
  9. <style>
  10. :root {
  11. --background-color: #121212;
  12. --card-color: #1e1e1e;
  13. --text-color: #e0e0e0;
  14. --accent-color: #2c7be5;
  15. --secondary-color: #404040;
  16. --highlight-color: #60EFFF;
  17. --success-color: #28a745;
  18. --danger-color: #dc3545;
  19. }
  20.  
  21. html, body {
  22. font-family: Arial, sans-serif;
  23. margin: 0;
  24. padding: 0;
  25. height: 100%;
  26. background-color: var(--background-color);
  27. color: var(--text-color);
  28. overflow: hidden;
  29. }
  30.  
  31. #container {
  32. display: flex;
  33. flex-direction: column;
  34. height: 100vh;
  35. max-width: 1200px;
  36. margin: 0 auto;
  37. padding: 20px;
  38. box-sizing: border-box;
  39. }
  40.  
  41. .header {
  42. display: flex;
  43. justify-content: space-between;
  44. align-items: center;
  45. margin-bottom: 20px;
  46. }
  47.  
  48. h1 {
  49. margin: 0;
  50. color: var(--highlight-color);
  51. font-size: 24px;
  52. }
  53.  
  54. .selector-group {
  55. display: flex;
  56. gap: 10px;
  57. align-items: center;
  58. }
  59.  
  60. .selector-group select {
  61. flex: 1;
  62. padding: 8px 12px;
  63. background-color: var(--card-color);
  64. color: var(--text-color);
  65. border: 1px solid var(--secondary-color);
  66. border-radius: 6px;
  67. cursor: pointer;
  68. }
  69.  
  70. #settings-btn {
  71. background: none;
  72. border: none;
  73. color: var(--text-color);
  74. font-size: 20px;
  75. cursor: pointer;
  76. padding: 8px;
  77. transition: color 0.2s;
  78. }
  79.  
  80. #settings-btn:hover {
  81. color: var(--highlight-color);
  82. }
  83.  
  84. #listening-info {
  85. background-color: rgba(44, 123, 229, 0.1);
  86. padding: 10px 15px;
  87. border-radius: 6px;
  88. margin-bottom: 15px;
  89. border-left: 4px solid var(--accent-color);
  90. font-size: 14px;
  91. }
  92.  
  93. .chat-area {
  94. flex: 1;
  95. display: flex;
  96. position: relative;
  97. margin-bottom: 20px;
  98. min-height: 300px;
  99. overflow: hidden;
  100. border-radius: 8px;
  101. border: 1px solid var(--secondary-color);
  102. background-color: rgba(30, 30, 30, 0.5);
  103. }
  104.  
  105. #output {
  106. position: absolute;
  107. top: 0;
  108. left: 0;
  109. right: 0;
  110. bottom: 0;
  111. padding: 20px;
  112. overflow-y: auto;
  113. background-color: transparent;
  114. z-index: 3;
  115. color: var(--text-color);
  116. text-shadow: 0 0 10px rgba(0, 0, 0, 0.8);
  117. font-size: 18px;
  118. line-height: 1.6;
  119. }
  120.  
  121. #text-input {
  122. width: 100%;
  123. padding: 15px;
  124. background-color: var(--card-color);
  125. color: var(--text-color);
  126. border: 1px solid var(--secondary-color);
  127. border-radius: 6px;
  128. min-height: 80px;
  129. resize: vertical;
  130. margin-bottom: 15px;
  131. box-sizing: border-box;
  132. }
  133.  
  134. .button-group {
  135. display: flex;
  136. gap: 10px;
  137. }
  138.  
  139. .btn {
  140. padding: 12px 24px;
  141. background-color: var(--accent-color);
  142. color: var(--text-color);
  143. border: none;
  144. border-radius: 6px;
  145. cursor: pointer;
  146. font-size: 16px;
  147. transition: background-color 0.2s;
  148. flex: 1;
  149. }
  150.  
  151. .btn:hover {
  152. background-color: #1a68d1;
  153. }
  154.  
  155. #voice-btn.recording {
  156. background-color: var(--success-color);
  157. }
  158.  
  159. #voice-btn:disabled, #send-btn:disabled {
  160. background-color: var(--secondary-color);
  161. cursor: not-allowed;
  162. }
  163.  
  164. #status {
  165. margin-top: 10px;
  166. font-size: 14px;
  167. color: #929292;
  168. }
  169.  
  170. #audio-output {
  171. margin-top: 15px;
  172. width: 100%;
  173. }
  174.  
  175. /* Modal styling */
  176. .modal {
  177. display: none;
  178. position: fixed;
  179. z-index: 10;
  180. left: 0;
  181. top: 0;
  182. width: 100%;
  183. height: 100%;
  184. background-color: rgba(0, 0, 0, 0.7);
  185. }
  186.  
  187. .modal-content {
  188. background-color: var(--card-color);
  189. margin: 10% auto;
  190. padding: 20px;
  191. border: 1px solid var(--secondary-color);
  192. border-radius: 8px;
  193. width: 70%;
  194. max-width: 600px;
  195. color: var(--text-color);
  196. }
  197.  
  198. .modal-header {
  199. display: flex;
  200. justify-content: space-between;
  201. align-items: center;
  202. margin-bottom: 20px;
  203. }
  204.  
  205. .close {
  206. color: var(--text-color);
  207. font-size: 24px;
  208. font-weight: bold;
  209. cursor: pointer;
  210. }
  211.  
  212. .close:hover {
  213. color: var(--highlight-color);
  214. }
  215.  
  216. .modal textarea {
  217. width: 100%;
  218. padding: 12px;
  219. height: 200px;
  220. margin-bottom: 15px;
  221. background-color: var(--background-color);
  222. color: var(--text-color);
  223. border: 1px solid var(--secondary-color);
  224. border-radius: 6px;
  225. resize: vertical;
  226. }
  227.  
  228. @media (max-width: 768px) {
  229. #container {
  230. padding: 10px;
  231. }
  232.  
  233. .header {
  234. flex-direction: column;
  235. align-items: flex-start;
  236. }
  237.  
  238. .selector-group {
  239. width: 100%;
  240. margin-top: 10px;
  241. }
  242.  
  243. .modal-content {
  244. width: 90%;
  245. margin: 20% auto;
  246. }
  247. }
  248. </style>
  249. </head>
  250. <body>
  251. <div id="container">
  252. <div class="header">
  253. <h1>Voice & Text Chat</h1>
  254.  
  255. <div class="selector-group">
  256. <select id="model-selector">
  257. {% for model in models %}
  258. <option value="{{ model }}">{{ model }}</option>
  259. {% endfor %}
  260. </select>
  261.  
  262. <select id="kroko-language-selector">
  263. <option value="auto">Auto-detect</option>
  264. <option value="en" selected>English</option>
  265. <option value="fr">French</option>
  266. <option value="de">German</option>
  267. </select>
  268.  
  269. <button id="settings-btn" title="Settings"><i class="fas fa-cog"></i></button>
  270. </div>
  271. </div>
  272.  
  273. <p id="listening-info">Voice recognition is always active. Speak clearly to interrupt at any time.</p>
  274.  
  275. <div class="chat-area">
  276. <div id="output">Welcome! Start speaking or type a message to begin.</div>
  277. </div>
  278.  
  279. <textarea id="text-input" placeholder="Type your message or use voice input"></textarea>
  280.  
  281. <div class="button-group">
  282. <button id="voice-btn" class="btn">Voice Status: Listening</button>
  283. <button id="send-btn" class="btn">Send Text</button>
  284. </div>
  285.  
  286. <div id="status">Ready</div>
  287. <audio id="audio-output" controls></audio>
  288. </div>
  289.  
  290. <!-- Settings Modal -->
  291. <div id="settings-modal" class="modal">
  292. <div class="modal-content">
  293. <div class="modal-header">
  294. <h2>System Prompt Settings</h2>
  295. <span class="close">&times;</span>
  296. </div>
  297. <p>Set a custom system prompt for the AI model:</p>
  298. <textarea id="system-prompt" placeholder="Enter a system prompt that defines how the AI should behave...">You are a helpful, friendly AI assistant.</textarea>
  299. <button id="save-settings" class="btn">Save Settings</button>
  300. </div>
  301. </div>
  302.  
  303. <script>
  304. // DOM Elements
  305. const voiceBtn = document.getElementById('voice-btn');
  306. const sendBtn = document.getElementById('send-btn');
  307. const textInput = document.getElementById('text-input');
  308. const outputDiv = document.getElementById('output');
  309. const audioOutput = document.getElementById('audio-output');
  310. const modelSelector = document.getElementById('model-selector');
  311. const languageSelector = document.getElementById('kroko-language-selector');
  312. const statusEl = document.getElementById('status');
  313. const settingsBtn = document.getElementById('settings-btn');
  314. const settingsModal = document.getElementById('settings-modal');
  315. const closeModal = document.querySelector('.close');
  316. const systemPrompt = document.getElementById('system-prompt');
  317. const saveSettings = document.getElementById('save-settings');
  318.  
  319. // State variables
  320. let isListening = false;
  321. let recognition = null;
  322. let isProcessing = false;
  323. let currentAudio = null;
  324. let continuousMode = true; // Always listen for speech
  325. let userSystemPrompt = "You are a helpful, friendly AI assistant.";
  326.  
  327. // Map language codes for speech recognition
  328. const languageMap = {
  329. 'en': 'en-US',
  330. 'fr': 'fr-FR',
  331. 'de': 'de-DE',
  332. 'auto': ''
  333. };
  334.  
  335. // Initialize Web Speech API
  336. function initSpeechRecognition() {
  337. // Check if browser supports speech recognition
  338. window.SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
  339.  
  340. if (!window.SpeechRecognition) {
  341. updateStatus("Speech recognition not supported in this browser. Please use Chrome.");
  342. voiceBtn.disabled = true;
  343. return false;
  344. }
  345.  
  346. // Create speech recognition instance
  347. recognition = new SpeechRecognition();
  348.  
  349. // Configure recognition
  350. recognition.continuous = true; // Enable continuous listening
  351. recognition.interimResults = true;
  352.  
  353. // Set language based on selector
  354. const lang = languageSelector.value;
  355. recognition.lang = languageMap[lang] || 'en-US';
  356.  
  357. // Handle results
  358. recognition.onresult = (event) => {
  359. let finalTranscript = '';
  360. let interimTranscript = '';
  361.  
  362. for (let i = event.resultIndex; i < event.results.length; i++) {
  363. const transcript = event.results[i][0].transcript;
  364.  
  365. if (event.results[i].isFinal) {
  366. finalTranscript += transcript;
  367. } else {
  368. interimTranscript += transcript;
  369. }
  370. }
  371.  
  372. // If we have a final transcript and we're not currently processing
  373. if (finalTranscript && !isProcessing) {
  374. textInput.value = finalTranscript;
  375.  
  376. // Stop any playing audio
  377. if (currentAudio && !currentAudio.paused) {
  378. currentAudio.pause();
  379. currentAudio = null;
  380. }
  381.  
  382. // Process the new input
  383. sendMessage();
  384. } else if (finalTranscript && isProcessing) {
  385. // If we're processing, this is an interruption
  386. textInput.value = finalTranscript;
  387.  
  388. // Stop any playing audio
  389. if (currentAudio && !currentAudio.paused) {
  390. currentAudio.pause();
  391. currentAudio = null;
  392. }
  393.  
  394. // Cancel current processing and handle new input
  395. handleInterruption(finalTranscript);
  396. } else if (interimTranscript) {
  397. // Just update the text input for interim results
  398. textInput.value = interimTranscript;
  399. }
  400. };
  401.  
  402. // Handle end of speech recognition service
  403. recognition.onend = () => {
  404. console.log("Recognition ended");
  405.  
  406. // If we're in continuous mode and not deliberately stopped, restart
  407. if (continuousMode && isListening) {
  408. try {
  409. recognition.start();
  410. console.log("Restarted continuous listening");
  411. } catch (error) {
  412. console.error("Error restarting recognition:", error);
  413. isListening = false;
  414. voiceBtn.textContent = "Start Voice Input";
  415. voiceBtn.classList.remove("recording");
  416. }
  417. } else {
  418. isListening = false;
  419. voiceBtn.textContent = "Start Voice Input";
  420. voiceBtn.classList.remove("recording");
  421. }
  422. };
  423.  
  424. // Handle errors
  425. recognition.onerror = (event) => {
  426. console.error("Recognition error:", event.error);
  427. updateStatus(`Error: ${event.error}`);
  428.  
  429. // If it's a network error or other temporary issue, try to restart
  430. if (event.error === 'network' || event.error === 'aborted') {
  431. setTimeout(() => {
  432. if (continuousMode && isListening) {
  433. try {
  434. recognition.start();
  435. console.log("Restarted after error");
  436. } catch (e) {
  437. isListening = false;
  438. voiceBtn.textContent = "Start Voice Input";
  439. voiceBtn.classList.remove("recording");
  440. }
  441. }
  442. }, 1000);
  443. } else {
  444. isListening = false;
  445. voiceBtn.textContent = "Start Voice Input";
  446. voiceBtn.classList.remove("recording");
  447. }
  448. };
  449.  
  450. return true;
  451. }
  452.  
  453. // Handle user interruption
  454. function handleInterruption(newText) {
  455. console.log("Handling interruption with: ", newText);
  456.  
  457. // Cancel any ongoing request if possible
  458. isProcessing = false;
  459.  
  460. // Process the new text
  461. sendMessage();
  462. }
  463.  
  464. // Start listening for speech
  465. function startListening() {
  466. if (!recognition) {
  467. if (!initSpeechRecognition()) {
  468. return;
  469. }
  470. }
  471.  
  472. try {
  473. // Update language setting
  474. const lang = languageSelector.value;
  475. recognition.lang = languageMap[lang] || 'en-US';
  476.  
  477. recognition.start();
  478. isListening = true;
  479. voiceBtn.textContent = "Voice Status: Listening";
  480. voiceBtn.classList.add("recording");
  481. updateStatus("Listening for speech...");
  482. } catch (error) {
  483. console.error("Error starting recognition:", error);
  484. updateStatus(`Error: ${error.message}`);
  485. }
  486. }
  487.  
  488. // Stop listening
  489. function stopListening() {
  490. if (!isListening) return;
  491.  
  492. try {
  493. recognition.stop();
  494. isListening = false;
  495. voiceBtn.textContent = "Voice Status: Paused";
  496. voiceBtn.classList.remove("recording");
  497. updateStatus("Ready");
  498. } catch (error) {
  499. console.error("Error stopping recognition:", error);
  500. }
  501. }
  502.  
  503. // Toggle listening mode
  504. function toggleListening() {
  505. if (isListening) {
  506. // If we're in continuous mode, just update the button text
  507. // but don't actually stop listening
  508. if (continuousMode) {
  509. isListening = false;
  510. voiceBtn.textContent = "Voice Status: Paused";
  511. voiceBtn.classList.remove("recording");
  512. updateStatus("Voice input paused (still listening in background)");
  513. } else {
  514. stopListening();
  515. }
  516. } else {
  517. startListening();
  518. }
  519. }
  520.  
  521. // Send message to the server for processing
  522. async function sendMessage() {
  523. const text = textInput.value.trim();
  524. if (!text) return;
  525.  
  526. // Disable send button during processing (but keep voice active for interruptions)
  527. sendBtn.disabled = true;
  528. textInput.disabled = true;
  529. isProcessing = true;
  530.  
  531. updateStatus("Processing message...");
  532. outputDiv.textContent = "Thinking...";
  533.  
  534. try {
  535. // Send the text to the server
  536. const response = await fetch('/process_text', {
  537. method: 'POST',
  538. headers: { 'Content-Type': 'application/json' },
  539. body: JSON.stringify({
  540. text: text,
  541. model: modelSelector.value,
  542. language: languageSelector.value,
  543. system_prompt: userSystemPrompt // Include the custom system prompt
  544. })
  545. });
  546.  
  547. // If processing was interrupted, don't continue
  548. if (!isProcessing) {
  549. console.log("Processing was interrupted, ignoring response");
  550. return;
  551. }
  552.  
  553. if (!response.ok) {
  554. throw new Error(`Server returned ${response.status}: ${response.statusText}`);
  555. }
  556.  
  557. const data = await response.json();
  558.  
  559. // If processing was interrupted, don't continue
  560. if (!isProcessing) {
  561. console.log("Processing was interrupted, ignoring response");
  562. return;
  563. }
  564.  
  565. if (data.error) {
  566. throw new Error(data.error);
  567. }
  568.  
  569. // Display the response
  570. outputDiv.textContent = data.text;
  571.  
  572. // Play audio if available and not interrupted
  573. if (data.audio && isProcessing) {
  574. // Set up new audio source
  575. audioOutput.src = `data:audio/wav;base64,${data.audio}`;
  576. currentAudio = audioOutput;
  577.  
  578. // Play the audio
  579. audioOutput.play().catch(err => {
  580. console.error("Error playing audio:", err);
  581. });
  582. }
  583.  
  584. // Clear input for next message
  585. textInput.value = "";
  586.  
  587. } catch (error) {
  588. console.error("Request error:", error);
  589. outputDiv.textContent = `Error: ${error.message}`;
  590. } finally {
  591. // Re-enable inputs
  592. sendBtn.disabled = false;
  593. textInput.disabled = false;
  594. isProcessing = false;
  595. updateStatus("Ready");
  596. }
  597. }
  598.  
  599. // Update status message
  600. function updateStatus(message) {
  601. statusEl.textContent = message;
  602. }
  603.  
  604. // Settings modal functions
  605. function openSettingsModal() {
  606. settingsModal.style.display = "block";
  607. systemPrompt.value = userSystemPrompt;
  608. }
  609.  
  610. function closeSettingsModal() {
  611. settingsModal.style.display = "none";
  612. }
  613.  
  614. function saveSystemPrompt() {
  615. userSystemPrompt = systemPrompt.value.trim();
  616. if (!userSystemPrompt) {
  617. userSystemPrompt = "You are a helpful, friendly AI assistant.";
  618. }
  619. closeSettingsModal();
  620. saveToLocalStorage();
  621. }
  622.  
  623. // Event listeners
  624. voiceBtn.addEventListener('click', toggleListening);
  625. sendBtn.addEventListener('click', sendMessage);
  626.  
  627. // Settings modal events
  628. settingsBtn.addEventListener('click', openSettingsModal);
  629. closeModal.addEventListener('click', closeSettingsModal);
  630. saveSettings.addEventListener('click', saveSystemPrompt);
  631.  
  632. // Close modal if clicked outside the content
  633. window.addEventListener('click', (event) => {
  634. if (event.target === settingsModal) {
  635. closeSettingsModal();
  636. }
  637. });
  638.  
  639. // Allow pressing Enter to send a message
  640. textInput.addEventListener('keydown', (event) => {
  641. if (event.key === 'Enter' && !event.shiftKey) {
  642. event.preventDefault();
  643. sendMessage();
  644. }
  645. });
  646.  
  647. // Update language when selection changes
  648. languageSelector.addEventListener('change', () => {
  649. if (recognition) {
  650. const lang = languageSelector.value;
  651. recognition.lang = languageMap[lang] || 'en-US';
  652. }
  653. });
  654.  
  655. // Handle stopping audio playback on interruption
  656. audioOutput.addEventListener('error', () => {
  657. console.log("Audio playback error");
  658. currentAudio = null;
  659. });
  660.  
  661. // Load saved system prompt from localStorage if available
  662. function loadSettings() {
  663. const savedPrompt = localStorage.getItem('systemPrompt');
  664. if (savedPrompt) {
  665. userSystemPrompt = savedPrompt;
  666. systemPrompt.value = savedPrompt;
  667. }
  668. }
  669.  
  670. // Save system prompt to localStorage
  671. function saveToLocalStorage() {
  672. localStorage.setItem('systemPrompt', userSystemPrompt);
  673. }
  674.  
  675. // Initialize the app
  676. document.addEventListener('DOMContentLoaded', () => {
  677. loadSettings();
  678. initSpeechRecognition();
  679. // Start listening immediately in continuous mode
  680. if (continuousMode) {
  681. startListening();
  682. }
  683. });
  684. </script>
  685. </body>
  686. </html>
Add Comment
Please, Sign In to add comment