// Front-end JavaScript - app.js console.log("app.js loaded") // Auto- fill and load session ID if exists (for now, just from global variable, can be extended to localStorage or URL param) window.onload = () => { if (window.session_id) { document.getElementById("session_display").value = window.session_id } } // ----------------------------------- // GLOBAL SPEECH RECOGNITION // ----------------------------------- const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition if (!SpeechRecognition) { alert("Speech Recognition not supported in this browser.") } else { window.recognition = new SpeechRecognition() recognition.lang = "en-US" recognition.continuous = true recognition.interimResults = true console.log("🎤 Speech recognition initialized") } // ----------------------------------- // FORCE VOICE LOADING - FIX FOR EDGE // Without this Edge sometimes has: // . Zero voices loaded // . Silent speech synthesis // ----------------------------------- window.speechSynthesis.onvoiceschanged = () => { const voices = speechSynthesis.getVoices() console.log("🔊 Voices loaded:", voices) } // ------------------------------- // GLOBAL STATE // ------------------------------- window.conversationStarted = false // Global memory store for agents window.agentMemory = {} // --------------------------------------------------------- // INITIAL TRIGGER - Fire (Start Conversation; Run Analysis) // --------------------------------------------------------- //async function runAnalysis(){ //async function fire() { window.fire = async function () { //const inputBox = document.getElementById("initial_input") const inputBox = document.getElementById("main_input") //const text = inputBox.value //console.log("🚀 Run Analysis:", text) // safety check to prevent silent errrors if element isn't found if (!inputBox) { console.error("❌ main_input not found") return } const text = inputBox.value if (!text) return // Create session once if (!window.session_id) { window.session_id = Math.random().toString(36).substring(7) // 🔥 Display it on UI document.getElementById("session_display").value = window.session_id } // Mark conversation started window.conversationStarted = true // UI changes switchToConversationMode() // Show user message appendMessage("user", text) // Show "Thinking..." showThinking() inputBox.value = "" // Disable Run Analysis permanently after first use //document.querySelector('button[onclick="runAnalysis()"]').disabled = true await sendToBackend(text) } // ----------------------------------------------------- // FUEL - FOLLOW-UP (Continue Conversation; Send button) // ----------------------------------------------------- //async function sendFollowup(){ //async function fuel() { window.fuel = async function () { //const inputBox = document.getElementById("followup_input") const inputBox = document.getElementById("main_input") //const text = inputBox.value if (!inputBox) { console.error("❌ main_input not found") return } const text = inputBox.value //console.log("📨 Follow-up:", text) if (!text) return // Show user message appendMessage("user", text) // Show "Thinking..." showThinking() inputBox.value = "" await sendToBackend(text) } // ----------------- // UI Mode Switcher // ----------------- function switchToConversationMode() { const inputBox = document.getElementById("main_input") // Change placeholder inputBox.placeholder = "Continue..." // Replace Fire → Fuel const fireBtn = document.getElementById("fire_btn") const fuelBtn = document.createElement("button") fuelBtn.innerText = "Fuel" fuelBtn.id = "fuel_btn" //fuelBtn.onclick = fuel fuelBtn.onclick = window.fuel fireBtn.replaceWith(fuelBtn) // Show End Session button document.getElementById("end_btn").style.display = "inline-block" } // -------------------------------------------------------------------------------------------------------- // LOAD SESSION - Allow loading previous session by ID (for now, no authentication, just input ID and load) // -------------------------------------------------------------------------------------------------------- //function loadSession() { async function loadSession() { const input = document.getElementById("session_input").value if (!input) return window.session_id = input const res = await fetch(`/session/${input}`) const data = await res.json() if (data.error) { alert("Session not found") return } // Set session display document.getElementById("session_display").value = input // Switch UI mode window.conversationStarted = true switchToConversationMode() // Clear chat - Restore chat visually const chatBox = document.getElementById("chat") chatBox.innerHTML = "" // Restore messages data.history.forEach(msg => { if (msg.role === "client") { appendMessage("user", msg.content) } else { updateThinking(msg.content, "jung") } }) console.log("♻️ Session loaded:", input) } // ----------------------------- // VOICE INPUT - SPEECH TO TEXT // ----------------------------- function startVoiceInput() { recognition.start() console.log("🎤 Listening...") let finalTranscript = "" recognition.onresult = function (event) { let interimTranscript = "" for (let i = event.resultIndex; i < event.results.length; ++i) { const transcript = event.results[i][0].transcript if (event.results[i].isFinal) { finalTranscript += transcript + " " } else { interimTranscript += transcript } } // Show live text while speaking document.getElementById("main_input").value = finalTranscript + interimTranscript } recognition.onerror = function (event) { console.error("Speech recognition error:", event.error) } // ----------------------------------- // AUTO SEND WHEN USER FINISHES // ----------------------------------- recognition.onend = function () { console.log("🛑 Voice input ended") const text = document.getElementById("main_input").value.trim() if (!text) return // AUTO SEND if (window.conversationStarted) { fuel() } else { fire() } } } // ------------------------------------------------ // AUDIO OUTPUT - SYSTEM RESPONSE - TEXT TO SPEECH // ------------------------------------------------ function speakText(text) { // ----------------------------------- // STOP MIC BEFORE SPEAKING // System must be disabled from inadvertent audio inputs, preventing self-feedback-loop // ----------------------------------- try { recognition.stop() } catch (e) { console.log("Recognition already stopped") } const speech = new SpeechSynthesisUtterance(text) speech.lang = "en-US" speech.rate = 0.95 // ----------------------------------- // LOAD AVAILABLE VOICES // ----------------------------------- let voices = window.speechSynthesis.getVoices() // Edge sometimes loads voices asynchronously if (voices.length > 0) { speech.voice = voices.find(v => v.lang.includes("en") ) || voices[0] } speech.volume = 1.0 speech.pitch = 1.0 // DEBUG console.log("🔊 Speaking:", text) speech.onstart = () => { console.log("🟢 Speech started") } speech.onend = () => { console.log("🔴 Speech ended") } speech.onerror = (e) => { console.error("Speech error:", e) } window.speechSynthesis.speak(speech) } // ---------------------------------------------------- // REPLACE "THINKING..." WITH RESPONSE // + SPEAK RESPONSE OUT LOUD // ---------------------------------------------------- function updateThinking(text, agent = "jung") { let thinking = document.getElementById("thinking") // Preserve line breaks visually const formatted = text.replace(/\n+/g, "
") // Convert internal agent id → display label const agentName = formatAgentName(agent) // If no "thinking" block exists, // create a fresh assistant message container if (!thinking) { const chatBox = document.getElementById("chat") thinking = document.createElement("div") thinking.className = "ai-msg" chatBox.appendChild(thinking) } // Render assistant message thinking.innerHTML = ` Agent ${agentName}:

${formatted} ` // Remove temporary "thinking" ID thinking.id = "" // --------------------------------------- // 🔊 SPEAK RESPONSE OUT LOUD // --------------------------------------- // Clean text for speech synthesis // (remove excessive whitespace/newlines) const speechText = text.replace(/\n+/g, " ").trim() // Prevent empty speech if (speechText.length > 0) { // Stop previous speech if still speaking window.speechSynthesis.cancel() // Speak new response speakText(speechText) } } // ----------------------------------------------------------- // BACKEND CALL (SHARED) - PASS AGENT FROM BACKEND TO FRONTEND // ----------------------------------------------------------- async function sendToBackend(text) { const API_URL = window.location.origin + "/chat" console.log("📡 Sending to backend:", text) try { const res = await fetch(API_URL, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ session_id: window.session_id, message: text }) }) const data = await res.json() //removeThinking() // Format text (single line breaks) //const formatted = data.content.replace(/\n+/g, "
") // Update thinking, prevents duplicate messages // Update UI + speak response //updateThinking(formatted) updateThinking(data.content, data.agent) // 🔥 HANDLE CHOICE TYPE if (data.type === "choice") { renderChoices(data.choices) } // 🔥 Store agent memory if present if (data.agent_log) { const agent = data.agent_log.agent window.agentMemory[agent] = data.agent_log.history updateAgentBox(agent, data.agent_log.history) } // 🧠 HANDLE EPISTEMIC (PASSIVE OBSERVER) if (data.witness_updates) { for (const agent in data.witness_updates) { const content = data.witness_updates[agent] // Epistemic is not conversational → single block updateAgentBox(agent, [ { role: "agent", text: content } ]) } } } catch (err) { console.error("❌ ERROR:", err) removeThinking() appendMessage("assistant", "Error: server not reachable") } } // ------------------------------- // NEXT AGENT SELECTION // ------------------------------- function renderChoices(choices) { const chatBox = document.getElementById("chat") const container = document.createElement("div") container.className = "choice-container" choices.forEach(choice => { const btn = document.createElement("button") btn.innerText = choice.label btn.className = "choice-btn" btn.onclick = () => { handleAgentSelection(choice.id) container.remove() // remove after click } container.appendChild(btn) }) chatBox.appendChild(container) chatBox.scrollTop = chatBox.scrollHeight } // ------------------------------- // HANDLE NEXT AGENT SELECTION // ------------------------------- function handleAgentSelection(agentId) { console.log("🧭 Selected agent:", agentId) // Send agent selection as special message sendToBackend(`__agent__:${agentId}`) showThinking() } // ------------------------------- // SHOW "THINKING..." // ------------------------------- function showThinking() { const chatBox = document.getElementById("chat") const thinking = document.createElement("div") thinking.id = "thinking" thinking.className = "ai-msg" thinking.innerHTML = ` Agent Jung:

Thinking... ` chatBox.appendChild(thinking) } // ------------------------------- // REPLACE "THINKING..." WITH RESPONSE // ------------------------------- function updateThinking(text, agent = "jung") { let thinking = document.getElementById("thinking") const formatted = text.replace(/\n+/g, "
") // Convert agent id → display name const agentName = formatAgentName(agent) if (!thinking) { const chatBox = document.getElementById("chat") thinking = document.createElement("div") thinking.className = "ai-msg" chatBox.appendChild(thinking) } thinking.innerHTML = ` Agent ${agentName}:

${formatted} ` thinking.id = "" } // ------------------------------- // REMOVE THINKING (fallback) // ------------------------------- function removeThinking() { const thinking = document.getElementById("thinking") if (thinking) thinking.remove() } // ------------------------------- // RENDER USER MESSAGE ONLY // ------------------------------- function appendMessage(role, text) { const chatBox = document.getElementById("chat") const msg = document.createElement("div") if (role === "user") { msg.innerHTML = ` Client:

${text} ` msg.className = "user-msg" } // ❗ IMPORTANT: // We DO NOT render assistant here anymore // Assistant is handled ONLY via updateThinking() chatBox.appendChild(msg) chatBox.scrollTop = chatBox.scrollHeight } // ------------------------------- // FORMAT AGENT NAME // ------------------------------- function formatAgentName(agent) { const map = { jung: "Jung", dream: "Dream", shadow: "Shadow", myth: "Myth", epistemic: "Epistemic", bpsy: "BPsy", jred: "JRed", synthesis: "Synthesis" } return map[agent] || agent } // ------------------------------- // UPDATE AGENT BOX // ------------------------------- function updateAgentBox(agent, history) { const box = document.getElementById(agent) if (!box) return let html = "" history.forEach(entry => { if (agent === "epistemic") { html += formatEpistemic(entry.text) } else { if (entry.role === "client") { html += `Client: ${entry.text}

` } else { html += `Agent: ${entry.text}

` } } }) box.innerHTML = html } // ----------------------------------------------------- // HELPER FOR FORMATTING EPISTEMIC INSIGHTS WITH HEADERS // ----------------------------------------------------- function formatEpistemic(text) { return text .replace("SUMMARY — CLIENT:", "Summary — Client") .replace("CROSS-ANALYSIS — CLIENT:", "Cross-Analysis — Client") .replace("SUMMARY — AGENT JUNG:", "Summary — Agent Jung") .replace("CROSS-ANALYSIS — AGENT JUNG:", "Cross-Analysis — Agent Jung") .replace(/\n/g, "
") } // ------------ // END SESSION // ------------ async function endSession() { console.log("🛑 Ending session") const sessionId = window.session_id await fetch(window.location.origin + "/chat", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ session_id: sessionId, message: "end session" }) }) // 🔥 POPUP MESSAGE if (sessionId) { alert(`Session ended.\n\nUse Session ID: ${sessionId} to resume later.`) } window.session_id = null window.conversationStarted = false document.getElementById("chat").innerHTML = "" document.getElementById("session_display").value = "" } // --------------------------------------------------------- // ENTER(SEND ACTION) & SHIFT+ENTER(INSERT NEW LINE) CONTROL // --------------------------------------------------------- window.addEventListener("DOMContentLoaded", () => { const input = document.getElementById("main_input") if (input) { input.addEventListener("keydown", function (e) { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault() if (window.conversationStarted) { fuel() } else { fire() } } }) } })