diff --git a/src/main.rs b/src/main.rs index f6481bb9e..e112e3b83 100644 --- a/src/main.rs +++ b/src/main.rs @@ -134,16 +134,19 @@ async fn run_axum_server( let static_path = std::path::Path::new("./web/desktop"); let app = Router::new() - .route("/", get(crate::web_server::index)) - .merge(api_router) - .with_state(app_state.clone()) + // Static file services must come first to match before other routes .nest_service("/js", ServeDir::new(static_path.join("js"))) .nest_service("/css", ServeDir::new(static_path.join("css"))) .nest_service("/drive", ServeDir::new(static_path.join("drive"))) .nest_service("/chat", ServeDir::new(static_path.join("chat"))) .nest_service("/mail", ServeDir::new(static_path.join("mail"))) .nest_service("/tasks", ServeDir::new(static_path.join("tasks"))) - .fallback_service(ServeDir::new(static_path)) + // API routes + .merge(api_router) + .with_state(app_state.clone()) + // Root index route - only matches exact "/" + .route("/", get(crate::web_server::index)) + // Layers .layer(cors) .layer(TraceLayer::new_for_http()); diff --git a/web/desktop/chat/chat.css b/web/desktop/chat/chat.css index a1e170fae..635b3ad3f 100644 --- a/web/desktop/chat/chat.css +++ b/web/desktop/chat/chat.css @@ -196,7 +196,7 @@ body::before { } #messages { flex: 1; - overflow-y: auto; + overflow-y: scroll; overflow-x: hidden; padding: 20px 20px 140px; max-width: 680px; @@ -431,17 +431,21 @@ footer { color: var(--bg); font-size: 18px; cursor: pointer; - display: none; + display: flex; align-items: center; justify-content: center; transition: all 0.3s; z-index: 90; + opacity: 0; + pointer-events: none; + box-shadow: 0 2px 8px var(--shadow); } .scroll-to-bottom.visible { - display: flex; + opacity: 1; + pointer-events: auto; } .scroll-to-bottom:hover { - transform: scale(1.1) rotate(180deg); + transform: scale(1.15); } .warning-message { border-radius: 12px; @@ -612,7 +616,7 @@ footer { opacity: 0.7; text-decoration: underline; } -/* Claude-style scrollbar - thin, subtle, hover-only */ +/* Claude-style scrollbar - thin, subtle, always visible but more prominent on hover */ #messages::-webkit-scrollbar { width: 8px; } @@ -620,21 +624,24 @@ footer { background: transparent; } #messages::-webkit-scrollbar-thumb { - background: transparent; + background: rgba(128, 128, 128, 0.2); border-radius: 4px; transition: background 0.2s; } #messages:hover::-webkit-scrollbar-thumb { - background: rgba(128, 128, 128, 0.3); + background: rgba(128, 128, 128, 0.4); } #messages::-webkit-scrollbar-thumb:hover { - background: rgba(128, 128, 128, 0.5); + background: rgba(128, 128, 128, 0.6); +} +[data-theme="dark"] #messages::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.15); } [data-theme="dark"] #messages:hover::-webkit-scrollbar-thumb { - background: rgba(255, 255, 255, 0.2); + background: rgba(255, 255, 255, 0.25); } [data-theme="dark"] #messages::-webkit-scrollbar-thumb:hover { - background: rgba(255, 255, 255, 0.3); + background: rgba(255, 255, 255, 0.35); } /* Fallback for other elements */ diff --git a/web/desktop/chat/chat.js b/web/desktop/chat/chat.js index cb5ea500e..8861f0030 100644 --- a/web/desktop/chat/chat.js +++ b/web/desktop/chat/chat.js @@ -1,8 +1,22 @@ +// Singleton instance to prevent multiple initializations +let chatAppInstance = null; + function chatApp() { + // Return existing instance if already created + if (chatAppInstance) { + console.log("Returning existing chatApp instance"); + return chatAppInstance; + } + + console.log("Creating new chatApp instance"); + // Core state variables (shared via closure) let ws = null, pendingContextChange = null, - o; + o, + isConnecting = false, + isInitialized = false, + authPromise = null; ((currentSessionId = null), (currentUserId = null), (currentBotId = "default_bot"), @@ -159,6 +173,13 @@ function chatApp() { // Lifecycle / event handlers // ---------------------------------------------------------------------- init() { + // Prevent multiple initializations + if (isInitialized) { + console.log("Already initialized, skipping..."); + return; + } + isInitialized = true; + window.addEventListener("load", () => { // Assign DOM elements after the document is ready messagesDiv = document.getElementById("messages"); @@ -196,34 +217,35 @@ function chatApp() { // UI event listeners document.addEventListener("click", (e) => {}); - messagesDiv.addEventListener("scroll", () => { - const isAtBottom = - messagesDiv.scrollHeight - messagesDiv.scrollTop <= - messagesDiv.clientHeight + 100; - if (!isAtBottom) { - isUserScrolling = true; - scrollToBottomBtn.classList.add("visible"); - } else { - isUserScrolling = false; - scrollToBottomBtn.classList.remove("visible"); - } - }); + // Scroll detection + if (messagesDiv && scrollToBottomBtn) { + messagesDiv.addEventListener("scroll", () => { + const isAtBottom = + messagesDiv.scrollHeight - messagesDiv.scrollTop <= + messagesDiv.clientHeight + 100; + if (!isAtBottom) { + isUserScrolling = true; + scrollToBottomBtn.classList.add("visible"); + } else { + isUserScrolling = false; + scrollToBottomBtn.classList.remove("visible"); + } + }); - scrollToBottomBtn.addEventListener("click", () => { - this.scrollToBottom(); - }); + scrollToBottomBtn.addEventListener("click", () => { + this.scrollToBottom(); + }); + } sendBtn.onclick = () => this.sendMessage(); messageInputEl.addEventListener("keypress", (e) => { if (e.key === "Enter") this.sendMessage(); }); - window.addEventListener("focus", () => { - if (!ws || ws.readyState !== WebSocket.OPEN) { - this.connectWebSocket(); - } - }); - // Start authentication flow + // Don't auto-reconnect on focus in browser to prevent multiple connections + // Tauri doesn't fire focus events the same way + + // Initialize auth only once this.initializeAuth(); }); }, @@ -258,22 +280,48 @@ function chatApp() { }, async initializeAuth() { - try { - this.updateConnectionStatus("connecting"); - const p = window.location.pathname.split("/").filter((s) => s); - const b = p.length > 0 ? p[0] : "default"; - const r = await fetch( - `http://localhost:8080/api/auth?bot_name=${encodeURIComponent(b)}`, - ); - const a = await r.json(); - currentUserId = a.user_id; - currentSessionId = a.session_id; - this.connectWebSocket(); - } catch (e) { - console.error("Failed to initialize auth:", e); - this.updateConnectionStatus("disconnected"); - setTimeout(() => this.initializeAuth(), 3000); + // Return existing promise if auth is in progress + if (authPromise) { + console.log("Auth already in progress, waiting..."); + return authPromise; } + + // Already authenticated + if ( + currentSessionId && + currentUserId && + ws && + ws.readyState === WebSocket.OPEN + ) { + console.log("Already authenticated and connected"); + return; + } + + // Create auth promise to prevent concurrent calls + authPromise = (async () => { + try { + this.updateConnectionStatus("connecting"); + const p = window.location.pathname.split("/").filter((s) => s); + const b = p.length > 0 ? p[0] : "default"; + const r = await fetch( + `http://localhost:8080/api/auth?bot_name=${encodeURIComponent(b)}`, + ); + const a = await r.json(); + currentUserId = a.user_id; + currentSessionId = a.session_id; + console.log("Auth successful:", { currentUserId, currentSessionId }); + this.connectWebSocket(); + } catch (e) { + console.error("Failed to initialize auth:", e); + this.updateConnectionStatus("disconnected"); + authPromise = null; + setTimeout(() => this.initializeAuth(), 3000); + } finally { + authPromise = null; + } + })(); + + return authPromise; }, async loadSessions() { @@ -331,14 +379,37 @@ function chatApp() { }, connectWebSocket() { - if (ws) { + // Prevent multiple simultaneous connection attempts + if (isConnecting) { + console.log("Already connecting to WebSocket, skipping..."); + return; + } + if ( + ws && + (ws.readyState === WebSocket.OPEN || + ws.readyState === WebSocket.CONNECTING) + ) { + console.log("WebSocket already connected or connecting"); + return; + } + if (ws && ws.readyState !== WebSocket.CLOSED) { ws.close(); } clearTimeout(reconnectTimeout); + isConnecting = true; + const u = this.getWebSocketUrl(); + console.log("Connecting to WebSocket:", u); ws = new WebSocket(u); ws.onmessage = (e) => { const r = JSON.parse(e.data); + + // Filter out welcome/connection messages that aren't BotResponse + if (r.type === "connected" || !r.message_type) { + console.log("Ignoring non-message:", r); + return; + } + if (r.bot_id) { currentBotId = r.bot_id; } @@ -357,12 +428,14 @@ function chatApp() { }; ws.onopen = () => { console.log("Connected to WebSocket"); + isConnecting = false; this.updateConnectionStatus("connected"); reconnectAttempts = 0; hasReceivedInitialMessage = false; }; ws.onclose = (e) => { console.log("WebSocket disconnected:", e.code, e.reason); + isConnecting = false; this.updateConnectionStatus("disconnected"); if (isStreaming) { this.showContinueButton(); @@ -380,6 +453,7 @@ function chatApp() { }; ws.onerror = (e) => { console.error("WebSocket error:", e); + isConnecting = false; this.updateConnectionStatus("disconnected"); }; }, @@ -389,6 +463,12 @@ function chatApp() { isContextChange = false; return; } + + // Ignore messages without content + if (!r.content && r.is_complete !== true) { + return; + } + if (r.context_usage !== undefined) { this.updateContextUsage(r.context_usage); } @@ -401,7 +481,8 @@ function chatApp() { isStreaming = false; streamingMessageId = null; currentStreamingContent = ""; - } else { + } else if (r.content) { + // Only add message if there's actual content this.addMessage("assistant", r.content, false); } } else { @@ -842,11 +923,67 @@ function chatApp() { }, scrollToBottom() { - messagesDiv.scrollTop = messagesDiv.scrollHeight; - isUserScrolling = false; - scrollToBottomBtn.classList.remove("visible"); + if (messagesDiv) { + messagesDiv.scrollTop = messagesDiv.scrollHeight; + isUserScrolling = false; + if (scrollToBottomBtn) { + scrollToBottomBtn.classList.remove("visible"); + } + } }, }; + + const returnValue = { + init: init, + current: current, + search: search, + selectedChat: selectedChat, + navItems: navItems, + chats: chats, + get filteredChats() { + return chats.filter((chat) => + chat.name.toLowerCase().includes(search.toLowerCase()), + ); + }, + toggleSidebar: toggleSidebar, + toggleTheme: toggleTheme, + applyTheme: applyTheme, + updateContextUsage: updateContextUsage, + flashScreen: flashScreen, + updateConnectionStatus: updateConnectionStatus, + getWebSocketUrl: getWebSocketUrl, + initializeAuth: initializeAuth, + loadSessions: loadSessions, + createNewSession: createNewSession, + switchSession: switchSession, + connectWebSocket: connectWebSocket, + processMessageContent: processMessageContent, + handleEvent: handleEvent, + showThinkingIndicator: showThinkingIndicator, + hideThinkingIndicator: hideThinkingIndicator, + showWarning: showWarning, + showContinueButton: showContinueButton, + continueInterruptedResponse: continueInterruptedResponse, + addMessage: addMessage, + updateStreamingMessage: updateStreamingMessage, + finalizeStreamingMessage: finalizeStreamingMessage, + escapeHtml: escapeHtml, + clearSuggestions: clearSuggestions, + handleSuggestions: handleSuggestions, + setContext: setContext, + sendMessage: sendMessage, + toggleVoiceMode: toggleVoiceMode, + startVoiceSession: startVoiceSession, + stopVoiceSession: stopVoiceSession, + connectToVoiceRoom: connectToVoiceRoom, + startVoiceRecording: startVoiceRecording, + simulateVoiceTranscription: simulateVoiceTranscription, + scrollToBottom: scrollToBottom, + }; + + // Cache and return the singleton instance + chatAppInstance = returnValue; + return returnValue; } // Initialize the app diff --git a/web/desktop/index.html b/web/desktop/index.html index 00f171d4e..54aa8fcb0 100644 --- a/web/desktop/index.html +++ b/web/desktop/index.html @@ -1,43 +1,52 @@ +
+ +