Introduce chatApp singleton and guarded init
- Implement singleton pattern for chatApp to prevent multiple instances - Gate initialization with isInitialized to skip repeated init calls - Add guards for auth and WebSocket connections to avoid overlaps - Filter non-message payloads and only render content messages - Improve scroll-to-bottom button visibility and interaction - Update scrollbar styling, including dark theme rules
This commit is contained in:
parent
cfbff7f35c
commit
9f1d74b101
4 changed files with 248 additions and 92 deletions
11
src/main.rs
11
src/main.rs
|
|
@ -134,16 +134,19 @@ async fn run_axum_server(
|
||||||
let static_path = std::path::Path::new("./web/desktop");
|
let static_path = std::path::Path::new("./web/desktop");
|
||||||
|
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
.route("/", get(crate::web_server::index))
|
// Static file services must come first to match before other routes
|
||||||
.merge(api_router)
|
|
||||||
.with_state(app_state.clone())
|
|
||||||
.nest_service("/js", ServeDir::new(static_path.join("js")))
|
.nest_service("/js", ServeDir::new(static_path.join("js")))
|
||||||
.nest_service("/css", ServeDir::new(static_path.join("css")))
|
.nest_service("/css", ServeDir::new(static_path.join("css")))
|
||||||
.nest_service("/drive", ServeDir::new(static_path.join("drive")))
|
.nest_service("/drive", ServeDir::new(static_path.join("drive")))
|
||||||
.nest_service("/chat", ServeDir::new(static_path.join("chat")))
|
.nest_service("/chat", ServeDir::new(static_path.join("chat")))
|
||||||
.nest_service("/mail", ServeDir::new(static_path.join("mail")))
|
.nest_service("/mail", ServeDir::new(static_path.join("mail")))
|
||||||
.nest_service("/tasks", ServeDir::new(static_path.join("tasks")))
|
.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(cors)
|
||||||
.layer(TraceLayer::new_for_http());
|
.layer(TraceLayer::new_for_http());
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -196,7 +196,7 @@ body::before {
|
||||||
}
|
}
|
||||||
#messages {
|
#messages {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: scroll;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
padding: 20px 20px 140px;
|
padding: 20px 20px 140px;
|
||||||
max-width: 680px;
|
max-width: 680px;
|
||||||
|
|
@ -431,17 +431,21 @@ footer {
|
||||||
color: var(--bg);
|
color: var(--bg);
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: none;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
transition: all 0.3s;
|
transition: all 0.3s;
|
||||||
z-index: 90;
|
z-index: 90;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
box-shadow: 0 2px 8px var(--shadow);
|
||||||
}
|
}
|
||||||
.scroll-to-bottom.visible {
|
.scroll-to-bottom.visible {
|
||||||
display: flex;
|
opacity: 1;
|
||||||
|
pointer-events: auto;
|
||||||
}
|
}
|
||||||
.scroll-to-bottom:hover {
|
.scroll-to-bottom:hover {
|
||||||
transform: scale(1.1) rotate(180deg);
|
transform: scale(1.15);
|
||||||
}
|
}
|
||||||
.warning-message {
|
.warning-message {
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
|
|
@ -612,7 +616,7 @@ footer {
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
text-decoration: underline;
|
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 {
|
#messages::-webkit-scrollbar {
|
||||||
width: 8px;
|
width: 8px;
|
||||||
}
|
}
|
||||||
|
|
@ -620,21 +624,24 @@ footer {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
#messages::-webkit-scrollbar-thumb {
|
#messages::-webkit-scrollbar-thumb {
|
||||||
background: transparent;
|
background: rgba(128, 128, 128, 0.2);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
transition: background 0.2s;
|
transition: background 0.2s;
|
||||||
}
|
}
|
||||||
#messages:hover::-webkit-scrollbar-thumb {
|
#messages:hover::-webkit-scrollbar-thumb {
|
||||||
background: rgba(128, 128, 128, 0.3);
|
background: rgba(128, 128, 128, 0.4);
|
||||||
}
|
}
|
||||||
#messages::-webkit-scrollbar-thumb:hover {
|
#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 {
|
[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 {
|
[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 */
|
/* Fallback for other elements */
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,22 @@
|
||||||
|
// Singleton instance to prevent multiple initializations
|
||||||
|
let chatAppInstance = null;
|
||||||
|
|
||||||
function chatApp() {
|
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)
|
// Core state variables (shared via closure)
|
||||||
let ws = null,
|
let ws = null,
|
||||||
pendingContextChange = null,
|
pendingContextChange = null,
|
||||||
o;
|
o,
|
||||||
|
isConnecting = false,
|
||||||
|
isInitialized = false,
|
||||||
|
authPromise = null;
|
||||||
((currentSessionId = null),
|
((currentSessionId = null),
|
||||||
(currentUserId = null),
|
(currentUserId = null),
|
||||||
(currentBotId = "default_bot"),
|
(currentBotId = "default_bot"),
|
||||||
|
|
@ -159,6 +173,13 @@ function chatApp() {
|
||||||
// Lifecycle / event handlers
|
// Lifecycle / event handlers
|
||||||
// ----------------------------------------------------------------------
|
// ----------------------------------------------------------------------
|
||||||
init() {
|
init() {
|
||||||
|
// Prevent multiple initializations
|
||||||
|
if (isInitialized) {
|
||||||
|
console.log("Already initialized, skipping...");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
isInitialized = true;
|
||||||
|
|
||||||
window.addEventListener("load", () => {
|
window.addEventListener("load", () => {
|
||||||
// Assign DOM elements after the document is ready
|
// Assign DOM elements after the document is ready
|
||||||
messagesDiv = document.getElementById("messages");
|
messagesDiv = document.getElementById("messages");
|
||||||
|
|
@ -196,34 +217,35 @@ function chatApp() {
|
||||||
// UI event listeners
|
// UI event listeners
|
||||||
document.addEventListener("click", (e) => {});
|
document.addEventListener("click", (e) => {});
|
||||||
|
|
||||||
messagesDiv.addEventListener("scroll", () => {
|
// Scroll detection
|
||||||
const isAtBottom =
|
if (messagesDiv && scrollToBottomBtn) {
|
||||||
messagesDiv.scrollHeight - messagesDiv.scrollTop <=
|
messagesDiv.addEventListener("scroll", () => {
|
||||||
messagesDiv.clientHeight + 100;
|
const isAtBottom =
|
||||||
if (!isAtBottom) {
|
messagesDiv.scrollHeight - messagesDiv.scrollTop <=
|
||||||
isUserScrolling = true;
|
messagesDiv.clientHeight + 100;
|
||||||
scrollToBottomBtn.classList.add("visible");
|
if (!isAtBottom) {
|
||||||
} else {
|
isUserScrolling = true;
|
||||||
isUserScrolling = false;
|
scrollToBottomBtn.classList.add("visible");
|
||||||
scrollToBottomBtn.classList.remove("visible");
|
} else {
|
||||||
}
|
isUserScrolling = false;
|
||||||
});
|
scrollToBottomBtn.classList.remove("visible");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
scrollToBottomBtn.addEventListener("click", () => {
|
scrollToBottomBtn.addEventListener("click", () => {
|
||||||
this.scrollToBottom();
|
this.scrollToBottom();
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
sendBtn.onclick = () => this.sendMessage();
|
sendBtn.onclick = () => this.sendMessage();
|
||||||
messageInputEl.addEventListener("keypress", (e) => {
|
messageInputEl.addEventListener("keypress", (e) => {
|
||||||
if (e.key === "Enter") this.sendMessage();
|
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();
|
this.initializeAuth();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
@ -258,22 +280,48 @@ function chatApp() {
|
||||||
},
|
},
|
||||||
|
|
||||||
async initializeAuth() {
|
async initializeAuth() {
|
||||||
try {
|
// Return existing promise if auth is in progress
|
||||||
this.updateConnectionStatus("connecting");
|
if (authPromise) {
|
||||||
const p = window.location.pathname.split("/").filter((s) => s);
|
console.log("Auth already in progress, waiting...");
|
||||||
const b = p.length > 0 ? p[0] : "default";
|
return authPromise;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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() {
|
async loadSessions() {
|
||||||
|
|
@ -331,14 +379,37 @@ function chatApp() {
|
||||||
},
|
},
|
||||||
|
|
||||||
connectWebSocket() {
|
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();
|
ws.close();
|
||||||
}
|
}
|
||||||
clearTimeout(reconnectTimeout);
|
clearTimeout(reconnectTimeout);
|
||||||
|
isConnecting = true;
|
||||||
|
|
||||||
const u = this.getWebSocketUrl();
|
const u = this.getWebSocketUrl();
|
||||||
|
console.log("Connecting to WebSocket:", u);
|
||||||
ws = new WebSocket(u);
|
ws = new WebSocket(u);
|
||||||
ws.onmessage = (e) => {
|
ws.onmessage = (e) => {
|
||||||
const r = JSON.parse(e.data);
|
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) {
|
if (r.bot_id) {
|
||||||
currentBotId = r.bot_id;
|
currentBotId = r.bot_id;
|
||||||
}
|
}
|
||||||
|
|
@ -357,12 +428,14 @@ function chatApp() {
|
||||||
};
|
};
|
||||||
ws.onopen = () => {
|
ws.onopen = () => {
|
||||||
console.log("Connected to WebSocket");
|
console.log("Connected to WebSocket");
|
||||||
|
isConnecting = false;
|
||||||
this.updateConnectionStatus("connected");
|
this.updateConnectionStatus("connected");
|
||||||
reconnectAttempts = 0;
|
reconnectAttempts = 0;
|
||||||
hasReceivedInitialMessage = false;
|
hasReceivedInitialMessage = false;
|
||||||
};
|
};
|
||||||
ws.onclose = (e) => {
|
ws.onclose = (e) => {
|
||||||
console.log("WebSocket disconnected:", e.code, e.reason);
|
console.log("WebSocket disconnected:", e.code, e.reason);
|
||||||
|
isConnecting = false;
|
||||||
this.updateConnectionStatus("disconnected");
|
this.updateConnectionStatus("disconnected");
|
||||||
if (isStreaming) {
|
if (isStreaming) {
|
||||||
this.showContinueButton();
|
this.showContinueButton();
|
||||||
|
|
@ -380,6 +453,7 @@ function chatApp() {
|
||||||
};
|
};
|
||||||
ws.onerror = (e) => {
|
ws.onerror = (e) => {
|
||||||
console.error("WebSocket error:", e);
|
console.error("WebSocket error:", e);
|
||||||
|
isConnecting = false;
|
||||||
this.updateConnectionStatus("disconnected");
|
this.updateConnectionStatus("disconnected");
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
@ -389,6 +463,12 @@ function chatApp() {
|
||||||
isContextChange = false;
|
isContextChange = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ignore messages without content
|
||||||
|
if (!r.content && r.is_complete !== true) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (r.context_usage !== undefined) {
|
if (r.context_usage !== undefined) {
|
||||||
this.updateContextUsage(r.context_usage);
|
this.updateContextUsage(r.context_usage);
|
||||||
}
|
}
|
||||||
|
|
@ -401,7 +481,8 @@ function chatApp() {
|
||||||
isStreaming = false;
|
isStreaming = false;
|
||||||
streamingMessageId = null;
|
streamingMessageId = null;
|
||||||
currentStreamingContent = "";
|
currentStreamingContent = "";
|
||||||
} else {
|
} else if (r.content) {
|
||||||
|
// Only add message if there's actual content
|
||||||
this.addMessage("assistant", r.content, false);
|
this.addMessage("assistant", r.content, false);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -842,11 +923,67 @@ function chatApp() {
|
||||||
},
|
},
|
||||||
|
|
||||||
scrollToBottom() {
|
scrollToBottom() {
|
||||||
messagesDiv.scrollTop = messagesDiv.scrollHeight;
|
if (messagesDiv) {
|
||||||
isUserScrolling = false;
|
messagesDiv.scrollTop = messagesDiv.scrollHeight;
|
||||||
scrollToBottomBtn.classList.remove("visible");
|
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
|
// Initialize the app
|
||||||
|
|
|
||||||
|
|
@ -1,43 +1,52 @@
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<title>General Bots</title>
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
||||||
|
<link rel="stylesheet" href="css/app.css" />
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/livekit-client/dist/livekit-client.umd.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||||
|
|
||||||
<head>
|
<script defer src="js/alpine.js"></script>
|
||||||
<meta charset="utf-8" />
|
</head>
|
||||||
<title>General Bots</title>
|
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
|
||||||
<link rel="stylesheet" href="css/app.css" />
|
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/livekit-client/dist/livekit-client.umd.min.js"></script>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
|
||||||
|
|
||||||
<script defer src="js/alpine.js"></script>
|
<body>
|
||||||
</head>
|
<nav x-data="{ current: 'chat' }">
|
||||||
|
<div class="logo">⚡ General Bots</div>
|
||||||
|
<a
|
||||||
|
href="#chat"
|
||||||
|
@click.prevent="current = 'chat'; window.switchSection('chat')"
|
||||||
|
:class="{ active: current === 'chat' }"
|
||||||
|
>💬 Chat</a
|
||||||
|
>
|
||||||
|
|
||||||
<body>
|
<a
|
||||||
<nav x-data="{ current: 'chat' }">
|
href="#drive"
|
||||||
<div class="logo">⚡ General Bots</div>
|
@click.prevent="current = 'drive'; window.switchSection('drive')"
|
||||||
<a href="#chat" @click.prevent="current = 'chat'; window.switchSection('chat')"
|
:class="{ active: current === 'drive' }"
|
||||||
:class="{ active: current === 'chat' }">💬 Chat</a>
|
>📁 Drive</a
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="#tasks"
|
||||||
|
@click.prevent="current = 'tasks'; window.switchSection('tasks')"
|
||||||
|
:class="{ active: current === 'tasks' }"
|
||||||
|
>✓ Tasks</a
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="#mail"
|
||||||
|
@click.prevent="current = 'mail'; window.switchSection('mail')"
|
||||||
|
:class="{ active: current === 'mail' }"
|
||||||
|
>✉ Mail</a
|
||||||
|
>
|
||||||
|
</nav>
|
||||||
|
|
||||||
<a href="#drive" @click.prevent="current = 'drive'; window.switchSection('drive')"
|
<div id="main-content">
|
||||||
:class="{ active: current === 'drive' }">📁 Drive</a>
|
<!-- Sections will be loaded dynamically -->
|
||||||
<a href="#tasks" @click.prevent="current = 'tasks'; window.switchSection('tasks')"
|
</div>
|
||||||
:class="{ active: current === 'tasks' }">✓ Tasks</a>
|
|
||||||
<a href="#mail" @click.prevent="current = 'mail'; window.switchSection('mail')"
|
|
||||||
:class="{ active: current === 'mail' }">✉ Mail</a>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<div id="main-content">
|
|
||||||
<!-- Sections will be loaded dynamically -->
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Load Module Scripts -->
|
|
||||||
<script src="js/layout.js"></script>
|
|
||||||
<script src="chat/chat.js"></script>
|
|
||||||
<script src="drive/drive.js"></script>
|
|
||||||
<script src="tasks/tasks.js"></script>
|
|
||||||
<script src="mail/mail.js"></script>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
|
|
||||||
|
<!-- Load Layout Script - module scripts are loaded dynamically -->
|
||||||
|
<script src="js/layout.js"></script>
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
Loading…
Add table
Reference in a new issue