gemini/mcp/adk_multiagent_mcp_app/static/websocketApp.js (316 lines of code) (raw):

/** * Copyright 2025 Google LLC * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 **/ /* global marked */ // Inform linting tools that 'marked' is a global variable /** * @module websocketApp * Handles the core WebSocket chat application logic, including connection, * message sending/receiving, UI updates, and reconnection attempts. */ // --- Configuration --- const MAX_RECONNECT_ATTEMPTS = 5; const RECONNECT_BASE_DELAY_MS = 5000; // 5 seconds const ROBOT_ICON_PATH = "robot1.png"; // Ensure this path is correct relative to HTML const THINKING_INDICATOR_ID = "thinking-indicator-wrapper"; // --- Module State --- let ws = null; // WebSocket instance let reconnectAttempts = 0; let messageForm, messageInput, messagesDiv, sendButton; // DOM elements let appInitialized = false; // Flag to prevent multiple initializations // --- Helper Functions --- /** Adds a system status message (e.g., connection status) to the chat UI. */ function addStatusMessage(text, typeClass) { if (!messagesDiv) { console.error("Cannot add status message: messagesDiv not found."); return; } try { const p = document.createElement("p"); p.classList.add("system-status-message"); const span = document.createElement("span"); span.className = typeClass; // e.g., "connection-open-text", "error-text" span.textContent = text; p.appendChild(span); messagesDiv.appendChild(p); messagesDiv.scrollTop = messagesDiv.scrollHeight; // Scroll down } catch (e) { console.error("Error adding status message:", e); } } /** Displays the 'Thinking...' indicator in the chat UI. */ function showThinkingIndicator() { hideThinkingIndicator(); // Clear any previous indicator if (!messagesDiv) { console.error("Cannot show thinking indicator: messagesDiv not found."); return; } const wrapper = document.createElement("div"); wrapper.id = THINKING_INDICATOR_ID; wrapper.classList.add("message-wrapper", "thinking"); const iconSpan = document.createElement("span"); iconSpan.classList.add("message-icon", "robot-icon"); const robotImg = document.createElement("img"); robotImg.src = ROBOT_ICON_PATH; robotImg.alt = "Agent icon"; iconSpan.appendChild(robotImg); const bubbleP = document.createElement("p"); bubbleP.classList.add("message-bubble", "thinking-bubble"); bubbleP.innerHTML = 'Thinking<span class="dots"><span>.</span><span>.</span><span>.</span></span>'; // Animated dots wrapper.appendChild(iconSpan); wrapper.appendChild(bubbleP); messagesDiv.appendChild(wrapper); messagesDiv.scrollTop = messagesDiv.scrollHeight; console.log("Showing thinking indicator."); } /** Removes the 'Thinking...' indicator from the chat UI. */ function hideThinkingIndicator() { const indicatorWrapper = document.getElementById(THINKING_INDICATOR_ID); if (indicatorWrapper) { indicatorWrapper.remove(); console.log("Hiding thinking indicator."); } } /** Adds a user or server message to the chat UI. */ function addMessageToUI(messageText, senderType) { if (!messagesDiv) { console.error("Cannot add message: messagesDiv not found."); return; } const wrapper = document.createElement("div"); wrapper.classList.add("message-wrapper", senderType); // 'user' or 'server' const iconSpan = document.createElement("span"); iconSpan.classList.add("message-icon"); const bubbleP = document.createElement("p"); bubbleP.classList.add("message-bubble"); if (senderType === "user") { iconSpan.classList.add("user-icon"); iconSpan.textContent = "👤"; bubbleP.classList.add("user-message"); bubbleP.textContent = messageText; // Display user messages as plain text } else { // Server message iconSpan.classList.add("robot-icon"); const robotImg = document.createElement("img"); robotImg.src = ROBOT_ICON_PATH; robotImg.alt = "Agent icon"; iconSpan.appendChild(robotImg); bubbleP.classList.add("server-message-block"); // Attempt to render server messages as Markdown try { if (typeof marked !== "undefined") { bubbleP.innerHTML = marked.parse(messageText); // Use marked library } else { console.warn( "Marked library not loaded, displaying raw server message.", ); bubbleP.textContent = messageText; // Fallback } } catch (e) { console.error("Error parsing server Markdown:", e); bubbleP.textContent = messageText; // Fallback on error addStatusMessage(`Markdown parsing error: ${e.message}`, "error-text"); } } wrapper.appendChild(iconSpan); wrapper.appendChild(bubbleP); messagesDiv.appendChild(wrapper); messagesDiv.scrollTop = messagesDiv.scrollHeight; // Scroll down } // --- WebSocket Event Handlers --- function handleWebSocketOpen(event) { console.log("WebSocket connection opened successfully:", event.target.url); reconnectAttempts = 0; // Reset counter if (sendButton) sendButton.disabled = false; addStatusMessage("Connection established", "connection-open-text"); addSubmitHandler(); // Ensure form submit handler is active } function handleWebSocketMessage(event) { hideThinkingIndicator(); try { const packet = JSON.parse(event.data); if (packet.turn_complete === true) { console.log("Turn complete signal received."); // Optionally re-enable input or other actions here return; } if (packet.message) { addMessageToUI(packet.message, "server"); } else { console.warn("Received packet without 'message' field:", packet); } } catch (parseError) { console.error( "Error parsing WebSocket message:", parseError, "Raw data:", event.data, ); addStatusMessage( `Error processing server message: ${parseError.message}`, "error-text", ); addMessageToUI(`Received non-JSON data: ${event.data}`, "server"); // Display raw data } } function handleWebSocketClose(event) { console.warn( `WebSocket connection closed. Code: ${event.code}, Reason: '${ event.reason || "No reason given" }', Was Clean: ${event.wasClean}`, ); hideThinkingIndicator(); if (sendButton) sendButton.disabled = true; removeSubmitHandler(); // Prevent sending on closed connection // Reconnection logic for unclean closures if (!event.wasClean && reconnectAttempts < MAX_RECONNECT_ATTEMPTS) { reconnectAttempts++; const reconnectDelay = Math.min( 30000, // Max 30s delay RECONNECT_BASE_DELAY_MS * Math.pow(2, reconnectAttempts - 1), // Exponential backoff ); addStatusMessage( `Connection closed. Attempting reconnect ${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS} in ${Math.round( reconnectDelay / 1000, )}s...`, "connection-closed-text", ); setTimeout(connectWebSocket, reconnectDelay); } else if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) { console.error("Max reconnection attempts reached."); addStatusMessage( "Connection lost permanently. Max reconnection attempts reached. Please reload the page.", "error-text", ); } else { // Clean closure addStatusMessage("Connection closed.", "connection-closed-text"); } } function handleWebSocketError(error) { console.error("WebSocket error occurred:", error); hideThinkingIndicator(); addStatusMessage( "WebSocket connection error. See browser console.", "error-text", ); // onclose will likely be called after this } /** Attaches all necessary event listeners to the WebSocket instance. */ function addWebSocketHandlers(webSocketInstance) { webSocketInstance.onopen = handleWebSocketOpen; webSocketInstance.onmessage = handleWebSocketMessage; webSocketInstance.onclose = handleWebSocketClose; webSocketInstance.onerror = handleWebSocketError; console.log("WebSocket event handlers attached for:", webSocketInstance.url); } // --- Form Submission --- /** Handles the message form submission. */ function submitMessageHandler(e) { e.preventDefault(); // Prevent page reload if (!ws || ws.readyState !== WebSocket.OPEN) { console.warn("Attempted send, but WebSocket is not open."); addStatusMessage( "Cannot send message - Connection not active.", "error-text", ); return false; } if (!messageInput) { console.error("Cannot send message: messageInput is missing."); return false; } const messageText = messageInput.value.trim(); if (messageText) { addMessageToUI(messageText, "user"); showThinkingIndicator(); try { console.log("Sending message:", messageText); ws.send(messageText); // Send raw text messageInput.value = ""; // Clear input messageInput.focus(); } catch (error) { console.error("Error sending message via WebSocket:", error); hideThinkingIndicator(); addStatusMessage( `Failed to send message: ${error.message}`, "error-text", ); // Add visual feedback to the failed user message const lastUserBubble = messagesDiv?.querySelector( ".message-wrapper.user:last-child .message-bubble", ); if (lastUserBubble) { const errorSpan = document.createElement("span"); errorSpan.textContent = " (Send Error)"; errorSpan.style.color = "var(--status-error, #dc3545)"; // Use CSS var or fallback errorSpan.style.fontSize = "0.8em"; lastUserBubble.appendChild(errorSpan); } } } else { console.log("Empty message submission ignored."); } return false; // Prevent default just in case } /** Attaches the submit event listener to the form. */ function addSubmitHandler() { if (messageForm && submitMessageHandler) { messageForm.removeEventListener("submit", submitMessageHandler); // Prevent duplicates messageForm.addEventListener("submit", submitMessageHandler); console.log("Submit handler assigned to form."); } else { console.error( "Cannot add submit handler: Message form or handler missing!", ); } } /** Removes the submit event listener from the form. */ function removeSubmitHandler() { if (messageForm && submitMessageHandler) { messageForm.removeEventListener("submit", submitMessageHandler); console.log("Submit handler removed from form."); } } // --- WebSocket Connection --- /** Establishes or re-establishes the WebSocket connection. */ function connectWebSocket() { // Generate session ID and URL only when connecting const sessionId = Math.random().toString(36).substring(2, 15); const wsProtocol = window.location.protocol === "https:" ? "wss://" : "ws://"; const wsUrl = wsProtocol + window.location.host + "/ws/" + sessionId; console.log( `Attempting WebSocket connect: ${wsUrl} (Attempt: ${reconnectAttempts + 1})`, ); try { // Ensure any previous connection is closed before creating a new one if (ws && ws.readyState !== WebSocket.CLOSED) { console.log("Closing existing WebSocket before reconnecting."); ws.close(1000, "Reconnecting"); // 1000 = Normal Closure } ws = new WebSocket(wsUrl); addWebSocketHandlers(ws); // Attach handlers to the new instance } catch (error) { console.error("Error creating WebSocket instance:", error); addStatusMessage( `Failed to initialize connection: ${error.message}`, "error-text", ); // Attempt retry if creation fails (similar to onclose logic) if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) { reconnectAttempts++; const reconnectDelay = Math.min( 30000, RECONNECT_BASE_DELAY_MS * Math.pow(2, reconnectAttempts - 1), ); addStatusMessage( `Retrying connection in ${Math.round(reconnectDelay / 1000)}s...`, "connection-closed-text", ); setTimeout(connectWebSocket, reconnectDelay); } else { addStatusMessage( "Failed to initialize connection after multiple attempts. Please reload.", "error-text", ); } } } // --- Initialization --- /** * Initializes the WebSocket application. Finds necessary DOM elements * and initiates the first WebSocket connection attempt. */ export function initWebSocketApp() { if (appInitialized) { console.warn("WebSocket app already initialized. Skipping."); return; } console.log("Initializing WebSocket application logic..."); // Find essential DOM elements messageForm = document.getElementById("message-form"); messageInput = document.getElementById("message"); messagesDiv = document.getElementById("messages"); sendButton = document.getElementById("send-button"); const appTab = document.getElementById("app-tab-content"); // Container for error messages // Critical check for UI elements if (!messageForm || !messageInput || !messagesDiv || !sendButton) { console.error("CRITICAL: One or more required app DOM elements not found!"); const errorMsg = "<p class='system-status-message'><span class='error-text'>Initialization Error: Required UI elements missing. App cannot start.</span></p>"; if (appTab) { const appContainer = appTab.querySelector(".app-container"); // Try to find inner container if (appContainer) appContainer.innerHTML = errorMsg; else appTab.innerHTML = errorMsg; // Fallback to replacing tab content } else { // Fallback if even the app tab is missing alert( "Initialization Error: App UI elements missing and app tab not found.", ); } return; // Stop initialization } console.log("App UI Elements successfully located."); if (sendButton) sendButton.disabled = true; // Disable send until connected appInitialized = true; // Mark as initialized connectWebSocket(); // Start the connection process console.log("WebSocket App module initialized and connection initiated."); }