refactor(web): consolidate routing and expose auth handler
- Add `*.log` to `.gitignore` to exclude log files from version control. - Change `auth_handler` to `pub` in `src/auth/mod.rs` to make the endpoint publicly accessible. - Remove unused `bot_index` import and route; replace direct service registration with `web_server::configure_app` in `src/main.rs`. - Refactor `src/web_server/mod.rs`: - Remove the `bot_index` handler. - Introduce `serve_html` helper for loading HTML pages. - Simplify static file serving by configuring separate routes for JS and CSS assets. - Centralize all route and static file configuration in `configure_app`. - Clean up related imports and improve error handling for missing pages.
This commit is contained in:
parent
737f934d68
commit
f9a1e3a8c0
7 changed files with 772 additions and 774 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,3 +1,4 @@
|
||||||
|
*.log
|
||||||
target*
|
target*
|
||||||
.env
|
.env
|
||||||
*.env
|
*.env
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ impl AuthService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#[actix_web::get("/api/auth")]
|
#[actix_web::get("/api/auth")]
|
||||||
async fn auth_handler(
|
pub async fn auth_handler(
|
||||||
_req: HttpRequest,
|
_req: HttpRequest,
|
||||||
data: web::Data<AppState>,
|
data: web::Data<AppState>,
|
||||||
web::Query(params): web::Query<HashMap<String, String>>,
|
web::Query(params): web::Query<HashMap<String, String>>,
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,6 @@ use crate::session::{create_session, get_session_history, get_sessions, start_se
|
||||||
use crate::shared::state::AppState;
|
use crate::shared::state::AppState;
|
||||||
use crate::shared::utils::create_conn;
|
use crate::shared::utils::create_conn;
|
||||||
use crate::shared::utils::create_s3_operator;
|
use crate::shared::utils::create_s3_operator;
|
||||||
use crate::web_server::{bot_index};
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum BootstrapProgress {
|
pub enum BootstrapProgress {
|
||||||
StartingBootstrap,
|
StartingBootstrap,
|
||||||
|
|
@ -306,7 +305,6 @@ async fn main() -> std::io::Result<()> {
|
||||||
.wrap(Logger::default())
|
.wrap(Logger::default())
|
||||||
.wrap(Logger::new("HTTP REQUEST: %a %{User-Agent}i"))
|
.wrap(Logger::new("HTTP REQUEST: %a %{User-Agent}i"))
|
||||||
.app_data(web::Data::from(app_state_clone))
|
.app_data(web::Data::from(app_state_clone))
|
||||||
.configure(web_server::configure_app)
|
|
||||||
.service(auth_handler)
|
.service(auth_handler)
|
||||||
.service(create_session)
|
.service(create_session)
|
||||||
.service(get_session_history)
|
.service(get_session_history)
|
||||||
|
|
@ -332,7 +330,8 @@ async fn main() -> std::io::Result<()> {
|
||||||
.service(save_draft)
|
.service(save_draft)
|
||||||
.service(save_click);
|
.service(save_click);
|
||||||
}
|
}
|
||||||
app = app.service(bot_index);
|
app = app.configure(web_server::configure_app);
|
||||||
|
|
||||||
app
|
app
|
||||||
})
|
})
|
||||||
.workers(worker_count)
|
.workers(worker_count)
|
||||||
|
|
|
||||||
|
|
@ -14,15 +14,12 @@ async fn index() -> Result<HttpResponse> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix_web::get("/{botname}")]
|
async fn serve_html(path: &str) -> Result<HttpResponse> {
|
||||||
async fn bot_index(req: HttpRequest) -> Result<HttpResponse> {
|
match fs::read_to_string(format!("web/desktop/{}", path)) {
|
||||||
let botname = req.match_info().query("botname");
|
|
||||||
debug!("Serving bot interface for: {}", botname);
|
|
||||||
match fs::read_to_string("web/desktop/index.html") {
|
|
||||||
Ok(html) => Ok(HttpResponse::Ok().content_type("text/html").body(html)),
|
Ok(html) => Ok(HttpResponse::Ok().content_type("text/html").body(html)),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Failed to load index page for bot {}: {}", botname, e);
|
error!("Failed to load page {}: {}", path, e);
|
||||||
Ok(HttpResponse::InternalServerError().body("Failed to load index page"))
|
Ok(HttpResponse::InternalServerError().body("Failed to load page"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -30,16 +27,6 @@ async fn bot_index(req: HttpRequest) -> Result<HttpResponse> {
|
||||||
pub fn configure_app(cfg: &mut actix_web::web::ServiceConfig) {
|
pub fn configure_app(cfg: &mut actix_web::web::ServiceConfig) {
|
||||||
let static_path = Path::new("./web/desktop");
|
let static_path = Path::new("./web/desktop");
|
||||||
|
|
||||||
// Serve all static files from desktop directory
|
|
||||||
cfg.service(
|
|
||||||
Files::new("/", static_path)
|
|
||||||
.index_file("index.html")
|
|
||||||
.prefer_utf8(true)
|
|
||||||
.use_last_modified(true)
|
|
||||||
.use_etag(true)
|
|
||||||
.show_files_listing()
|
|
||||||
);
|
|
||||||
|
|
||||||
// Serve all JS files
|
// Serve all JS files
|
||||||
cfg.service(
|
cfg.service(
|
||||||
Files::new("/js", static_path.join("js"))
|
Files::new("/js", static_path.join("js"))
|
||||||
|
|
@ -48,17 +35,42 @@ pub fn configure_app(cfg: &mut actix_web::web::ServiceConfig) {
|
||||||
.use_etag(true)
|
.use_etag(true)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Serve all component directories
|
// Serve CSS files
|
||||||
["drive", "tasks", "mail"].iter().for_each(|dir| {
|
|
||||||
cfg.service(
|
cfg.service(
|
||||||
Files::new(&format!("/{}", dir), static_path.join(dir))
|
Files::new("/css", static_path.join("css"))
|
||||||
.prefer_utf8(true)
|
.prefer_utf8(true)
|
||||||
.use_last_modified(true)
|
.use_last_modified(true)
|
||||||
.use_etag(true)
|
.use_etag(true)
|
||||||
);
|
);
|
||||||
});
|
|
||||||
|
|
||||||
// Serve index routes
|
cfg.service(
|
||||||
|
Files::new("/drive", static_path.join("drive"))
|
||||||
|
.prefer_utf8(true)
|
||||||
|
.use_last_modified(true)
|
||||||
|
.use_etag(true)
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
cfg.service(
|
||||||
|
Files::new("/chat", static_path.join("chat"))
|
||||||
|
.prefer_utf8(true)
|
||||||
|
.use_last_modified(true)
|
||||||
|
.use_etag(true)
|
||||||
|
);
|
||||||
|
|
||||||
|
cfg.service(
|
||||||
|
Files::new("/mail", static_path.join("mail"))
|
||||||
|
.prefer_utf8(true)
|
||||||
|
.use_last_modified(true)
|
||||||
|
.use_etag(true)
|
||||||
|
);
|
||||||
|
|
||||||
|
cfg.service(
|
||||||
|
Files::new("/tasks", static_path.join("tasks"))
|
||||||
|
.prefer_utf8(true)
|
||||||
|
.use_last_modified(true)
|
||||||
|
.use_etag(true)
|
||||||
|
);
|
||||||
cfg.service(index);
|
cfg.service(index);
|
||||||
cfg.service(bot_index);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,101 +1,7 @@
|
||||||
<!doctype html>
|
|
||||||
<html lang="pt-br">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8"/>
|
|
||||||
<title>General Bots Chat</title>
|
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1.0"/>
|
|
||||||
<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>
|
|
||||||
<link rel="stylesheet" href="chat.css">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="connection-status connecting" id="connectionStatus"></div>
|
|
||||||
<div class="flash-overlay" id="flashOverlay"></div>
|
|
||||||
<div class="float-menu">
|
|
||||||
<div class="float-logo" id="floatLogo" title="Menu"></div>
|
|
||||||
<div class="menu-button" id="themeBtn" title="Theme">⚙</div>
|
|
||||||
</div>
|
|
||||||
<div class="sidebar" id="sidebar">
|
|
||||||
<div class="sidebar-header">
|
|
||||||
<div class="sidebar-logo"></div>
|
|
||||||
<div class="sidebar-title" id="sidebarTitle">General Bots Chat</div>
|
|
||||||
</div>
|
|
||||||
<button class="sidebar-button" id="voiceToggle" onclick="toggleVoiceMode()">🎤 Voice Mode</button>
|
|
||||||
<div class="history-section">
|
|
||||||
<div class="history-title">History</div>
|
|
||||||
<div id="history"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Chat layout analogous to Drive layout -->
|
|
||||||
<div class="chat-layout" x-data="chatApp()" x-cloak>
|
<div class="chat-layout" x-data="chatApp()" x-cloak>
|
||||||
<div class="panel chat-sidebar">
|
|
||||||
<div style="padding: 1rem; border-bottom: 1px solid #334155;">
|
|
||||||
<h3>General Bots Chat</h3>
|
|
||||||
</div>
|
|
||||||
<template x-for="item in navItems" :key="item.name">
|
|
||||||
<div class="nav-item"
|
|
||||||
:class="{ active: current === item.name }"
|
|
||||||
@click="current = item.name">
|
|
||||||
<span x-text="item.icon"></span>
|
|
||||||
<span x-text="item.name"></span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="panel chat-main">
|
<div id="connectionStatus" class="connection-status disconnected"></div>
|
||||||
<div style="padding: 1rem; border-bottom: 1px solid #334155;">
|
<main id="messages"></main>
|
||||||
<h2 x-text="current"></h2>
|
|
||||||
<input type="text" x-model="search" placeholder="Search chats..."
|
|
||||||
style="width: 100%; margin-top: 0.5rem; padding: 0.5rem; background: #0f172a; border: 1px solid #334155; border-radius: 0.375rem; color: #e2e8f0;">
|
|
||||||
</div>
|
|
||||||
<div class="chat-list">
|
|
||||||
<template x-for="chat in filteredChats" :key="chat.id">
|
|
||||||
<div class="chat-item"
|
|
||||||
:class="{ selected: selectedChat?.id === chat.id }"
|
|
||||||
@click="selectedChat = chat">
|
|
||||||
<span class="chat-icon" x-text="chat.icon"></span>
|
|
||||||
<div style="flex: 1;">
|
|
||||||
<div style="font-weight: 600;" x-text="chat.name"></div>
|
|
||||||
<div class="text-xs text-gray" x-text="chat.lastMessage"></div>
|
|
||||||
</div>
|
|
||||||
<div class="text-sm text-gray" x-text="chat.time"></div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="panel chat-details">
|
|
||||||
<template x-if="selectedChat">
|
|
||||||
<div style="padding: 2rem;">
|
|
||||||
<div style="text-align: center; margin-bottom: 2rem;">
|
|
||||||
<div style="font-size: 4rem; margin-bottom: 1rem;" x-text="selectedChat.icon"></div>
|
|
||||||
<h3 x-text="selectedChat.name"></h3>
|
|
||||||
<p class="text-sm text-gray" x-text="selectedChat.status"></p>
|
|
||||||
</div>
|
|
||||||
<div style="margin-bottom: 1rem;">
|
|
||||||
<div class="text-sm" style="margin-bottom: 0.5rem;">Last Message</div>
|
|
||||||
<div class="text-gray" x-text="selectedChat.lastMessage"></div>
|
|
||||||
</div>
|
|
||||||
<div style="margin-bottom: 1rem;">
|
|
||||||
<div class="text-sm" style="margin-bottom: 0.5rem;">Time</div>
|
|
||||||
<div class="text-gray" x-text="selectedChat.time"></div>
|
|
||||||
</div>
|
|
||||||
<div style="display: flex; gap: 0.5rem; margin-top: 2rem;">
|
|
||||||
<button style="flex: 1; padding: 0.75rem; background: #3b82f6; color: white; border: none; border-radius: 0.375rem; cursor: pointer;">Reply</button>
|
|
||||||
<button style="flex: 1; padding: 0.75rem; background: #10b981; color: white; border: none; border-radius: 0.375rem; cursor: pointer;">Close</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template x-if="!selectedChat">
|
|
||||||
<div style="padding: 2rem; text-align: center; color: #64748b;">
|
|
||||||
<div style="font-size: 4rem; margin-bottom: 1rem;">💬</div>
|
|
||||||
<p>Select a chat to view details</p>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
<div class="suggestions-container" id="suggestions"></div>
|
<div class="suggestions-container" id="suggestions"></div>
|
||||||
|
|
@ -105,7 +11,13 @@
|
||||||
<button id="sendBtn" title="Send">↑</button>
|
<button id="sendBtn" title="Send">↑</button>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
<button class="scroll-to-bottom" id="scrollToBottom">↓</button>
|
||||||
|
<div class="context-indicator" id="contextIndicator">
|
||||||
|
<div>Context</div>
|
||||||
|
<div id="contextPercentage">0%</div>
|
||||||
|
<div class="context-progress">
|
||||||
|
<div class="context-progress-bar" id="contextProgressBar" style="width:0%"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script src="chat.js"></script>
|
</div>
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
@ -1,19 +1,51 @@
|
||||||
function chatApp() {
|
function chatApp() {
|
||||||
|
|
||||||
|
// Core state variables (shared via closure)
|
||||||
|
let ws = null,
|
||||||
|
currentSessionId = null,
|
||||||
|
currentUserId = null,
|
||||||
|
currentBotId = "default_bot",
|
||||||
|
isStreaming = false,
|
||||||
|
voiceRoom = null,
|
||||||
|
isVoiceMode = false,
|
||||||
|
mediaRecorder = null,
|
||||||
|
audioChunks = [],
|
||||||
|
streamingMessageId = null,
|
||||||
|
isThinking = false,
|
||||||
|
currentStreamingContent = "",
|
||||||
|
hasReceivedInitialMessage = false,
|
||||||
|
reconnectAttempts = 0,
|
||||||
|
reconnectTimeout = null,
|
||||||
|
thinkingTimeout = null,
|
||||||
|
currentTheme = 'auto',
|
||||||
|
themeColor1 = null,
|
||||||
|
themeColor2 = null,
|
||||||
|
customLogoUrl = null,
|
||||||
|
contextUsage = 0,
|
||||||
|
isUserScrolling = false,
|
||||||
|
autoScrollEnabled = true,
|
||||||
|
isContextChange = false;
|
||||||
|
|
||||||
|
const maxReconnectAttempts = 5;
|
||||||
|
|
||||||
|
// DOM references (cached for performance)
|
||||||
|
let messagesDiv, input, sendBtn, voiceBtn, connectionStatus, flashOverlay, suggestionsContainer, floatLogo, sidebar, themeBtn, scrollToBottomBtn, contextIndicator, contextPercentage, contextProgressBar, sidebarTitle;
|
||||||
|
|
||||||
|
marked.setOptions({ breaks: true, gfm: true });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// Current navigation section (e.g., All Chats, Direct, Group)
|
// ----------------------------------------------------------------------
|
||||||
|
// UI state (mirrors the structure used in driveApp)
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
current: 'All Chats',
|
current: 'All Chats',
|
||||||
// Search term for filtering chats
|
|
||||||
search: '',
|
search: '',
|
||||||
// Currently selected chat object
|
|
||||||
selectedChat: null,
|
selectedChat: null,
|
||||||
// Navigation items similar to the Drive UI
|
|
||||||
navItems: [
|
navItems: [
|
||||||
{ name: 'All Chats', icon: '💬' },
|
{ name: 'All Chats', icon: '💬' },
|
||||||
{ name: 'Direct', icon: '👤' },
|
{ name: 'Direct', icon: '👤' },
|
||||||
{ name: 'Groups', icon: '👥' },
|
{ name: 'Groups', icon: '👥' },
|
||||||
{ name: 'Archived', icon: '🗄' }
|
{ name: 'Archived', icon: '🗄' }
|
||||||
],
|
],
|
||||||
// Sample chat list – in a real app this would be fetched from a server
|
|
||||||
chats: [
|
chats: [
|
||||||
{ id: 1, name: 'General Bot Support', icon: '🤖', lastMessage: 'How can I help you?', time: '10:15 AM', status: 'Online' },
|
{ id: 1, name: 'General Bot Support', icon: '🤖', lastMessage: 'How can I help you?', time: '10:15 AM', status: 'Online' },
|
||||||
{ id: 2, name: 'Project Alpha', icon: '🚀', lastMessage: 'Launch scheduled for tomorrow.', time: 'Yesterday', status: 'Active' },
|
{ id: 2, name: 'Project Alpha', icon: '🚀', lastMessage: 'Launch scheduled for tomorrow.', time: 'Yesterday', status: 'Active' },
|
||||||
|
|
@ -21,44 +53,31 @@ function chatApp() {
|
||||||
{ id: 4, name: 'Random Chat', icon: '🎲', lastMessage: 'Did you see the game last night?', time: '5 hrs ago', status: 'Idle' },
|
{ id: 4, name: 'Random Chat', icon: '🎲', lastMessage: 'Did you see the game last night?', time: '5 hrs ago', status: 'Idle' },
|
||||||
{ id: 5, name: 'Support Ticket #1234', icon: '🛠️', lastMessage: 'Issue resolved, closing ticket.', time: '3 days ago', status: 'Closed' }
|
{ id: 5, name: 'Support Ticket #1234', icon: '🛠️', lastMessage: 'Issue resolved, closing ticket.', time: '3 days ago', status: 'Closed' }
|
||||||
],
|
],
|
||||||
// Computed property – filters chats based on the search term
|
|
||||||
get filteredChats() {
|
get filteredChats() {
|
||||||
return this.chats.filter(chat =>
|
return this.chats.filter(chat =>
|
||||||
chat.name.toLowerCase().includes(this.search.toLowerCase())
|
chat.name.toLowerCase().includes(this.search.toLowerCase())
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ----- Full application mechanics migrated from web/html/index.html ----- */
|
// ----------------------------------------------------------------------
|
||||||
|
// UI helpers (formerly standalone functions)
|
||||||
let ws=null,currentSessionId=null,currentUserId=null,currentBotId="default_bot",isStreaming=false,voiceRoom=null,isVoiceMode=false,mediaRecorder=null,audioChunks=[],streamingMessageId=null,isThinking=false,currentStreamingContent="",hasReceivedInitialMessage=false,reconnectAttempts=0,reconnectTimeout=null,thinkingTimeout=null,currentTheme='auto',themeColor1=null,themeColor2=null,customLogoUrl=null,contextUsage=0,isUserScrolling=false,autoScrollEnabled=true,isContextChange=false;
|
// ----------------------------------------------------------------------
|
||||||
const maxReconnectAttempts=5,messagesDiv=document.getElementById("messages"),input=document.getElementById("messageInput"),sendBtn=document.getElementById("sendBtn"),voiceBtn=document.getElementById("voiceBtn"),connectionStatus=document.getElementById("connectionStatus"),flashOverlay=document.getElementById("flashOverlay"),suggestionsContainer=document.getElementById("suggestions"),floatLogo=document.getElementById("floatLogo"),sidebar=document.getElementById("sidebar"),themeBtn=document.getElementById("themeBtn"),scrollToBottomBtn=document.getElementById("scrollToBottom"),contextIndicator=document.getElementById("contextIndicator"),contextPercentage=document.getElementById("contextPercentage"),contextProgressBar=document.getElementById("contextProgressBar"),sidebarTitle=document.getElementById("sidebarTitle");
|
toggleSidebar() {
|
||||||
marked.setOptions({breaks:true,gfm:true});
|
|
||||||
|
|
||||||
floatLogo.addEventListener('click',toggleSidebar);
|
|
||||||
|
|
||||||
function toggleSidebar(){
|
|
||||||
sidebar.classList.toggle('open');
|
sidebar.classList.toggle('open');
|
||||||
}
|
},
|
||||||
|
|
||||||
function toggleTheme(){
|
toggleTheme() {
|
||||||
const themes = ['auto', 'dark', 'light'];
|
const themes = ['auto', 'dark', 'light'];
|
||||||
const savedTheme = localStorage.getItem('gb-theme') || 'auto';
|
const savedTheme = localStorage.getItem('gb-theme') || 'auto';
|
||||||
const idx = themes.indexOf(savedTheme);
|
const idx = themes.indexOf(savedTheme);
|
||||||
const newTheme = themes[(idx + 1) % themes.length];
|
const newTheme = themes[(idx + 1) % themes.length];
|
||||||
localStorage.setItem('gb-theme', newTheme);
|
localStorage.setItem('gb-theme', newTheme);
|
||||||
currentTheme = newTheme;
|
currentTheme = newTheme;
|
||||||
applyTheme();
|
this.applyTheme();
|
||||||
updateThemeButton();
|
this.updateThemeButton();
|
||||||
}
|
},
|
||||||
|
|
||||||
function updateThemeButton(){
|
applyTheme() {
|
||||||
const icons={'auto':'⚙','dark':'🌙','light':'☀️'};
|
|
||||||
themeBtn.textContent=icons[currentTheme]||'⚙';
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyTheme(){
|
|
||||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
let theme = currentTheme;
|
let theme = currentTheme;
|
||||||
if (theme === 'auto') {
|
if (theme === 'auto') {
|
||||||
|
|
@ -73,30 +92,49 @@ function applyTheme(){
|
||||||
if (customLogoUrl) {
|
if (customLogoUrl) {
|
||||||
document.documentElement.style.setProperty('--logo-url', `url('${customLogoUrl}')`);
|
document.documentElement.style.setProperty('--logo-url', `url('${customLogoUrl}')`);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
window.addEventListener("load",function(){
|
// ----------------------------------------------------------------------
|
||||||
|
// Lifecycle / event handlers
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
init() {
|
||||||
|
document.addEventListener('ready', () => {
|
||||||
|
// Assign DOM elements after the document is ready
|
||||||
|
messagesDiv = document.getElementById("messages");
|
||||||
|
input = document.getElementById("messageInput");
|
||||||
|
sendBtn = document.getElementById("sendBtn");
|
||||||
|
voiceBtn = document.getElementById("voiceBtn");
|
||||||
|
connectionStatus = document.getElementById("connectionStatus");
|
||||||
|
flashOverlay = document.getElementById("flashOverlay");
|
||||||
|
suggestionsContainer = document.getElementById("suggestions");
|
||||||
|
floatLogo = document.getElementById("floatLogo");
|
||||||
|
sidebar = document.getElementById("sidebar");
|
||||||
|
themeBtn = document.getElementById("themeBtn");
|
||||||
|
scrollToBottomBtn = document.getElementById("scrollToBottom");
|
||||||
|
contextIndicator = document.getElementById("contextIndicator");
|
||||||
|
contextPercentage = document.getElementById("contextPercentage");
|
||||||
|
contextProgressBar = document.getElementById("contextProgressBar");
|
||||||
|
sidebarTitle = document.getElementById("sidebarTitle");
|
||||||
|
|
||||||
|
// Theme initialization and focus
|
||||||
const savedTheme = localStorage.getItem('gb-theme') || 'auto';
|
const savedTheme = localStorage.getItem('gb-theme') || 'auto';
|
||||||
currentTheme = savedTheme;
|
currentTheme = savedTheme;
|
||||||
applyTheme();
|
this.applyTheme();
|
||||||
updateThemeButton();
|
|
||||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
|
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
|
||||||
if (currentTheme === 'auto') {
|
if (currentTheme === 'auto') {
|
||||||
applyTheme();
|
this.applyTheme();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
input.focus();
|
input.focus();
|
||||||
});
|
|
||||||
|
|
||||||
themeBtn.addEventListener('click',toggleTheme);
|
// UI event listeners
|
||||||
|
document.addEventListener('click', (e) => {
|
||||||
document.addEventListener('click',function(e){
|
|
||||||
if (sidebar.classList.contains('open') && !sidebar.contains(e.target) && !floatLogo.contains(e.target)) {
|
if (sidebar.classList.contains('open') && !sidebar.contains(e.target) && !floatLogo.contains(e.target)) {
|
||||||
sidebar.classList.remove('open');
|
sidebar.classList.remove('open');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
messagesDiv.addEventListener('scroll',function(){
|
messagesDiv.addEventListener('scroll', () => {
|
||||||
const isAtBottom = messagesDiv.scrollHeight - messagesDiv.scrollTop <= messagesDiv.clientHeight + 100;
|
const isAtBottom = messagesDiv.scrollHeight - messagesDiv.scrollTop <= messagesDiv.clientHeight + 100;
|
||||||
if (!isAtBottom) {
|
if (!isAtBottom) {
|
||||||
isUserScrolling = true;
|
isUserScrolling = true;
|
||||||
|
|
@ -107,74 +145,95 @@ messagesDiv.addEventListener('scroll',function(){
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
scrollToBottomBtn.addEventListener('click',function(){
|
scrollToBottomBtn.addEventListener('click', () => {
|
||||||
scrollToBottom();
|
this.scrollToBottom();
|
||||||
});
|
});
|
||||||
|
|
||||||
function updateContextUsage(u){
|
sendBtn.onclick = () => this.sendMessage();
|
||||||
|
input.addEventListener("keypress", e => { if (e.key === "Enter") this.sendMessage(); });
|
||||||
|
window.addEventListener("focus", () => {
|
||||||
|
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
||||||
|
this.connectWebSocket();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start authentication flow
|
||||||
|
this.initializeAuth();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
updateContextUsage(u) {
|
||||||
contextUsage = u;
|
contextUsage = u;
|
||||||
const p = Math.min(100, Math.round(u * 100));
|
const p = Math.min(100, Math.round(u * 100));
|
||||||
contextPercentage.textContent = `${p}%`;
|
contextPercentage.textContent = `${p}%`;
|
||||||
contextProgressBar.style.width = `${p}%`;
|
contextProgressBar.style.width = `${p}%`;
|
||||||
contextIndicator.classList.remove('visible');
|
contextIndicator.classList.remove('visible');
|
||||||
}
|
},
|
||||||
|
|
||||||
function flashScreen(){
|
flashScreen() {
|
||||||
gsap.to(flashOverlay,{opacity:0.15,duration:0.1,onComplete:()=>{gsap.to(flashOverlay,{opacity:0,duration:0.2});}});
|
gsap.to(flashOverlay, { opacity: 0.15, duration: 0.1, onComplete: () => {
|
||||||
}
|
gsap.to(flashOverlay, { opacity: 0, duration: 0.2 });
|
||||||
|
} });
|
||||||
|
},
|
||||||
|
|
||||||
function updateConnectionStatus(s){
|
updateConnectionStatus(s) {
|
||||||
connectionStatus.className = `connection-status ${s}`;
|
connectionStatus.className = `connection-status ${s}`;
|
||||||
}
|
},
|
||||||
|
|
||||||
function getWebSocketUrl(){
|
getWebSocketUrl() {
|
||||||
const p = "ws:", s = currentSessionId || crypto.randomUUID(), u = currentUserId || crypto.randomUUID();
|
const p = "ws:", s = currentSessionId || crypto.randomUUID(), u = currentUserId || crypto.randomUUID();
|
||||||
return `${p}//localhost:8080/ws?session_id=${s}&user_id=${u}`;
|
return `${p}//localhost:8080/ws?session_id=${s}&user_id=${u}`;
|
||||||
}
|
},
|
||||||
|
|
||||||
async function initializeAuth(){
|
async initializeAuth() {
|
||||||
try {
|
try {
|
||||||
updateConnectionStatus("connecting");
|
this.updateConnectionStatus("connecting");
|
||||||
const p=window.location.pathname.split('/').filter(s=>s),b=p.length>0?p[0]:'default',r=await fetch(`http://localhost:8080/api/auth?bot_name=${encodeURIComponent(b)}`),a=await r.json();
|
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;
|
currentUserId = a.user_id;
|
||||||
currentSessionId = a.session_id;
|
currentSessionId = a.session_id;
|
||||||
connectWebSocket();
|
this.connectWebSocket();
|
||||||
loadSessions();
|
this.loadSessions();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to initialize auth:", e);
|
console.error("Failed to initialize auth:", e);
|
||||||
updateConnectionStatus("disconnected");
|
this.updateConnectionStatus("disconnected");
|
||||||
setTimeout(initializeAuth,3000);
|
setTimeout(() => this.initializeAuth(), 3000);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
async function loadSessions(){
|
async loadSessions() {
|
||||||
try {
|
try {
|
||||||
const r=await fetch("http://localhost:8080/api/sessions"),s=await r.json(),h=document.getElementById("history");
|
const r = await fetch("http://localhost:8080/api/sessions");
|
||||||
|
const s = await r.json();
|
||||||
|
const h = document.getElementById("history");
|
||||||
h.innerHTML = "";
|
h.innerHTML = "";
|
||||||
s.forEach(session => {
|
s.forEach(session => {
|
||||||
const item = document.createElement('div');
|
const item = document.createElement('div');
|
||||||
item.className = 'history-item';
|
item.className = 'history-item';
|
||||||
item.textContent = session.title || `Session ${session.session_id.substring(0, 8)}`;
|
item.textContent = session.title || `Session ${session.session_id.substring(0, 8)}`;
|
||||||
item.onclick=()=>switchSession(session.session_id);
|
item.onclick = () => this.switchSession(session.session_id);
|
||||||
h.appendChild(item);
|
h.appendChild(item);
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to load sessions:", e);
|
console.error("Failed to load sessions:", e);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
async function createNewSession(){
|
async createNewSession() {
|
||||||
try {
|
try {
|
||||||
const r=await fetch("http://localhost:8080/api/sessions",{method:"POST"}),s=await r.json();
|
const r = await fetch("http://localhost:8080/api/sessions", { method: "POST" });
|
||||||
|
const s = await r.json();
|
||||||
currentSessionId = s.session_id;
|
currentSessionId = s.session_id;
|
||||||
hasReceivedInitialMessage = false;
|
hasReceivedInitialMessage = false;
|
||||||
connectWebSocket();
|
this.connectWebSocket();
|
||||||
loadSessions();
|
this.loadSessions();
|
||||||
messagesDiv.innerHTML = "";
|
messagesDiv.innerHTML = "";
|
||||||
clearSuggestions();
|
this.clearSuggestions();
|
||||||
updateContextUsage(0);
|
this.updateContextUsage(0);
|
||||||
if (isVoiceMode) {
|
if (isVoiceMode) {
|
||||||
await stopVoiceSession();
|
await this.stopVoiceSession();
|
||||||
isVoiceMode = false;
|
isVoiceMode = false;
|
||||||
const v = document.getElementById("voiceToggle");
|
const v = document.getElementById("voiceToggle");
|
||||||
v.textContent = "🎤 Voice Mode";
|
v.textContent = "🎤 Voice Mode";
|
||||||
|
|
@ -183,135 +242,137 @@ async function createNewSession(){
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to create session:", e);
|
console.error("Failed to create session:", e);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
function switchSession(s){
|
switchSession(s) {
|
||||||
currentSessionId = s;
|
currentSessionId = s;
|
||||||
hasReceivedInitialMessage = false;
|
hasReceivedInitialMessage = false;
|
||||||
loadSessionHistory(s);
|
this.loadSessionHistory(s);
|
||||||
connectWebSocket();
|
this.connectWebSocket();
|
||||||
if (isVoiceMode) {
|
if (isVoiceMode) {
|
||||||
startVoiceSession();
|
this.startVoiceSession();
|
||||||
}
|
}
|
||||||
sidebar.classList.remove('open');
|
sidebar.classList.remove('open');
|
||||||
}
|
},
|
||||||
|
|
||||||
async function loadSessionHistory(s){
|
async loadSessionHistory(s) {
|
||||||
try {
|
try {
|
||||||
const r=await fetch("http://localhost:8080/api/sessions/"+s),h=await r.json(),m=document.getElementById("messages");
|
const r = await fetch(`http://localhost:8080/api/sessions/${s}`);
|
||||||
|
const h = await r.json();
|
||||||
|
const m = document.getElementById("messages");
|
||||||
m.innerHTML = "";
|
m.innerHTML = "";
|
||||||
if (h.length === 0) {
|
if (h.length === 0) {
|
||||||
updateContextUsage(0);
|
this.updateContextUsage(0);
|
||||||
} else {
|
} else {
|
||||||
h.forEach(([role, content]) => {
|
h.forEach(([role, content]) => {
|
||||||
addMessage(role,content,false);
|
this.addMessage(role, content, false);
|
||||||
});
|
});
|
||||||
updateContextUsage(h.length/20);
|
this.updateContextUsage(h.length / 20);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to load session history:", e);
|
console.error("Failed to load session history:", e);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
function connectWebSocket(){
|
connectWebSocket() {
|
||||||
if (ws) {
|
if (ws) {
|
||||||
ws.close();
|
ws.close();
|
||||||
}
|
}
|
||||||
clearTimeout(reconnectTimeout);
|
clearTimeout(reconnectTimeout);
|
||||||
const u=getWebSocketUrl();
|
const u = this.getWebSocketUrl();
|
||||||
ws = new WebSocket(u);
|
ws = new WebSocket(u);
|
||||||
ws.onmessage=function(e){
|
ws.onmessage = (e) => {
|
||||||
const r = JSON.parse(e.data);
|
const r = JSON.parse(e.data);
|
||||||
if (r.bot_id) {
|
if (r.bot_id) {
|
||||||
currentBotId = r.bot_id;
|
currentBotId = r.bot_id;
|
||||||
}
|
}
|
||||||
if (r.message_type === 2) {
|
if (r.message_type === 2) {
|
||||||
const d = JSON.parse(r.content);
|
const d = JSON.parse(r.content);
|
||||||
handleEvent(d.event,d.data);
|
this.handleEvent(d.event, d.data);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (r.message_type === 5) {
|
if (r.message_type === 5) {
|
||||||
isContextChange = true;
|
isContextChange = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
processMessageContent(r);
|
this.processMessageContent(r);
|
||||||
};
|
};
|
||||||
ws.onopen=function(){
|
ws.onopen = () => {
|
||||||
console.log("Connected to WebSocket");
|
console.log("Connected to WebSocket");
|
||||||
updateConnectionStatus("connected");
|
this.updateConnectionStatus("connected");
|
||||||
reconnectAttempts = 0;
|
reconnectAttempts = 0;
|
||||||
hasReceivedInitialMessage = false;
|
hasReceivedInitialMessage = false;
|
||||||
};
|
};
|
||||||
ws.onclose=function(e){
|
ws.onclose = (e) => {
|
||||||
console.log("WebSocket disconnected:", e.code, e.reason);
|
console.log("WebSocket disconnected:", e.code, e.reason);
|
||||||
updateConnectionStatus("disconnected");
|
this.updateConnectionStatus("disconnected");
|
||||||
if (isStreaming) {
|
if (isStreaming) {
|
||||||
showContinueButton();
|
this.showContinueButton();
|
||||||
}
|
}
|
||||||
if (reconnectAttempts < maxReconnectAttempts) {
|
if (reconnectAttempts < maxReconnectAttempts) {
|
||||||
reconnectAttempts++;
|
reconnectAttempts++;
|
||||||
const d = Math.min(1000 * reconnectAttempts, 10000);
|
const d = Math.min(1000 * reconnectAttempts, 10000);
|
||||||
reconnectTimeout = setTimeout(() => {
|
reconnectTimeout = setTimeout(() => {
|
||||||
updateConnectionStatus("connecting");
|
this.updateConnectionStatus("connecting");
|
||||||
connectWebSocket();
|
this.connectWebSocket();
|
||||||
}, d);
|
}, d);
|
||||||
} else {
|
} else {
|
||||||
updateConnectionStatus("disconnected");
|
this.updateConnectionStatus("disconnected");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
ws.onerror=function(e){
|
ws.onerror = (e) => {
|
||||||
console.error("WebSocket error:", e);
|
console.error("WebSocket error:", e);
|
||||||
updateConnectionStatus("disconnected");
|
this.updateConnectionStatus("disconnected");
|
||||||
};
|
};
|
||||||
}
|
},
|
||||||
|
|
||||||
function processMessageContent(r){
|
processMessageContent(r) {
|
||||||
if (isContextChange) {
|
if (isContextChange) {
|
||||||
isContextChange = false;
|
isContextChange = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (r.context_usage !== undefined) {
|
if (r.context_usage !== undefined) {
|
||||||
updateContextUsage(r.context_usage);
|
this.updateContextUsage(r.context_usage);
|
||||||
}
|
}
|
||||||
if (r.suggestions && r.suggestions.length > 0) {
|
if (r.suggestions && r.suggestions.length > 0) {
|
||||||
handleSuggestions(r.suggestions);
|
this.handleSuggestions(r.suggestions);
|
||||||
}
|
}
|
||||||
if (r.is_complete) {
|
if (r.is_complete) {
|
||||||
if (isStreaming) {
|
if (isStreaming) {
|
||||||
finalizeStreamingMessage();
|
this.finalizeStreamingMessage();
|
||||||
isStreaming = false;
|
isStreaming = false;
|
||||||
streamingMessageId = null;
|
streamingMessageId = null;
|
||||||
currentStreamingContent = "";
|
currentStreamingContent = "";
|
||||||
} else {
|
} else {
|
||||||
addMessage("assistant",r.content,false);
|
this.addMessage("assistant", r.content, false);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (!isStreaming) {
|
if (!isStreaming) {
|
||||||
isStreaming = true;
|
isStreaming = true;
|
||||||
streamingMessageId = "streaming-" + Date.now();
|
streamingMessageId = "streaming-" + Date.now();
|
||||||
currentStreamingContent = r.content || "";
|
currentStreamingContent = r.content || "";
|
||||||
addMessage("assistant",currentStreamingContent,true,streamingMessageId);
|
this.addMessage("assistant", currentStreamingContent, true, streamingMessageId);
|
||||||
} else {
|
} else {
|
||||||
currentStreamingContent += r.content || "";
|
currentStreamingContent += r.content || "";
|
||||||
updateStreamingMessage(currentStreamingContent);
|
this.updateStreamingMessage(currentStreamingContent);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
function handleEvent(t,d){
|
handleEvent(t, d) {
|
||||||
console.log("Event received:", t, d);
|
console.log("Event received:", t, d);
|
||||||
switch (t) {
|
switch (t) {
|
||||||
case "thinking_start":
|
case "thinking_start":
|
||||||
showThinkingIndicator();
|
this.showThinkingIndicator();
|
||||||
break;
|
break;
|
||||||
case "thinking_end":
|
case "thinking_end":
|
||||||
hideThinkingIndicator();
|
this.hideThinkingIndicator();
|
||||||
break;
|
break;
|
||||||
case "warn":
|
case "warn":
|
||||||
showWarning(d.message);
|
this.showWarning(d.message);
|
||||||
break;
|
break;
|
||||||
case "context_usage":
|
case "context_usage":
|
||||||
updateContextUsage(d.usage);
|
this.updateContextUsage(d.usage);
|
||||||
break;
|
break;
|
||||||
case "change_theme":
|
case "change_theme":
|
||||||
if (d.color1) themeColor1 = d.color1;
|
if (d.color1) themeColor1 = d.color1;
|
||||||
|
|
@ -321,12 +382,12 @@ function handleEvent(t,d){
|
||||||
if (d.logo_text) {
|
if (d.logo_text) {
|
||||||
sidebarTitle.textContent = d.logo_text;
|
sidebarTitle.textContent = d.logo_text;
|
||||||
}
|
}
|
||||||
applyTheme();
|
this.applyTheme();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
function showThinkingIndicator(){
|
showThinkingIndicator() {
|
||||||
if (isThinking) return;
|
if (isThinking) return;
|
||||||
const t = document.createElement("div");
|
const t = document.createElement("div");
|
||||||
t.id = "thinking-indicator";
|
t.id = "thinking-indicator";
|
||||||
|
|
@ -335,18 +396,18 @@ function showThinkingIndicator(){
|
||||||
messagesDiv.appendChild(t);
|
messagesDiv.appendChild(t);
|
||||||
gsap.to(t, { opacity: 1, y: 0, duration: .3, ease: "power2.out" });
|
gsap.to(t, { opacity: 1, y: 0, duration: .3, ease: "power2.out" });
|
||||||
if (!isUserScrolling) {
|
if (!isUserScrolling) {
|
||||||
scrollToBottom();
|
this.scrollToBottom();
|
||||||
}
|
}
|
||||||
thinkingTimeout = setTimeout(() => {
|
thinkingTimeout = setTimeout(() => {
|
||||||
if (isThinking) {
|
if (isThinking) {
|
||||||
hideThinkingIndicator();
|
this.hideThinkingIndicator();
|
||||||
showWarning("O servidor pode estar ocupado. A resposta está demorando demais.");
|
this.showWarning("O servidor pode estar ocupado. A resposta está demorando demais.");
|
||||||
}
|
}
|
||||||
}, 60000);
|
}, 60000);
|
||||||
isThinking = true;
|
isThinking = true;
|
||||||
}
|
},
|
||||||
|
|
||||||
function hideThinkingIndicator(){
|
hideThinkingIndicator() {
|
||||||
if (!isThinking) return;
|
if (!isThinking) return;
|
||||||
const t = document.getElementById("thinking-indicator");
|
const t = document.getElementById("thinking-indicator");
|
||||||
if (t) {
|
if (t) {
|
||||||
|
|
@ -357,55 +418,64 @@ function hideThinkingIndicator(){
|
||||||
thinkingTimeout = null;
|
thinkingTimeout = null;
|
||||||
}
|
}
|
||||||
isThinking = false;
|
isThinking = false;
|
||||||
}
|
},
|
||||||
|
|
||||||
function showWarning(m){
|
showWarning(m) {
|
||||||
const w = document.createElement("div");
|
const w = document.createElement("div");
|
||||||
w.className = "warning-message";
|
w.className = "warning-message";
|
||||||
w.innerHTML = `⚠️ ${m}`;
|
w.innerHTML = `⚠️ ${m}`;
|
||||||
messagesDiv.appendChild(w);
|
messagesDiv.appendChild(w);
|
||||||
gsap.from(w, { opacity: 0, y: 20, duration: .4, ease: "power2.out" });
|
gsap.from(w, { opacity: 0, y: 20, duration: .4, ease: "power2.out" });
|
||||||
if (!isUserScrolling) {
|
if (!isUserScrolling) {
|
||||||
scrollToBottom();
|
this.scrollToBottom();
|
||||||
}
|
}
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (w.parentNode) {
|
if (w.parentNode) {
|
||||||
gsap.to(w, { opacity: 0, duration: .3, onComplete: () => w.remove() });
|
gsap.to(w, { opacity: 0, duration: .3, onComplete: () => w.remove() });
|
||||||
}
|
}
|
||||||
}, 5000);
|
}, 5000);
|
||||||
}
|
},
|
||||||
|
|
||||||
function showContinueButton(){
|
showContinueButton() {
|
||||||
const c = document.createElement("div");
|
const c = document.createElement("div");
|
||||||
c.className = "message-container";
|
c.className = "message-container";
|
||||||
c.innerHTML=`<div class="assistant-message"><div class="assistant-avatar"></div><div class="assistant-message-content"><p>A conexão foi interrompida. Clique em "Continuar" para tentar recuperar a resposta.</p><button class="continue-button" onclick="continueInterruptedResponse()">Continuar</button></div></div>`;
|
c.innerHTML = `<div class="assistant-message"><div class="assistant-avatar"></div><div class="assistant-message-content"><p>A conexão foi interrompida. Clique em "Continuar" para tentar recuperar a resposta.</p><button class="continue-button" onclick="this.parentElement.parentElement.parentElement.remove();">Continuar</button></div></div>`;
|
||||||
messagesDiv.appendChild(c);
|
messagesDiv.appendChild(c);
|
||||||
gsap.to(c, { opacity: 1, y: 0, duration: .5, ease: "power2.out" });
|
gsap.to(c, { opacity: 1, y: 0, duration: .5, ease: "power2.out" });
|
||||||
if (!isUserScrolling) {
|
if (!isUserScrolling) {
|
||||||
scrollToBottom();
|
this.scrollToBottom();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
function continueInterruptedResponse(){
|
continueInterruptedResponse() {
|
||||||
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
||||||
connectWebSocket();
|
this.connectWebSocket();
|
||||||
}
|
}
|
||||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||||
const d={bot_id:"default_bot",user_id:currentUserId,session_id:currentSessionId,channel:"web",content:"continue",message_type:3,media_url:null,timestamp:new Date().toISOString()};
|
const d = {
|
||||||
|
bot_id: "default_bot",
|
||||||
|
user_id: currentUserId,
|
||||||
|
session_id: currentSessionId,
|
||||||
|
channel: "web",
|
||||||
|
content: "continue",
|
||||||
|
message_type: 3,
|
||||||
|
media_url: null,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
};
|
||||||
ws.send(JSON.stringify(d));
|
ws.send(JSON.stringify(d));
|
||||||
}
|
}
|
||||||
document.querySelectorAll(".continue-button").forEach(b => { b.parentElement.parentElement.parentElement.remove(); });
|
document.querySelectorAll(".continue-button").forEach(b => { b.parentElement.parentElement.parentElement.remove(); });
|
||||||
}
|
},
|
||||||
|
|
||||||
function addMessage(role,content,streaming=false,msgId=null){
|
addMessage(role, content, streaming = false, msgId = null) {
|
||||||
const m = document.createElement("div");
|
const m = document.createElement("div");
|
||||||
m.className = "message-container";
|
m.className = "message-container";
|
||||||
if (role === "user") {
|
if (role === "user") {
|
||||||
m.innerHTML=`<div class="user-message"><div class="user-message-content">${escapeHtml(content)}</div></div>`;
|
m.innerHTML = `<div class="user-message"><div class="user-message-content">${this.escapeHtml(content)}</div></div>`;
|
||||||
updateContextUsage(contextUsage+.05);
|
this.updateContextUsage(contextUsage + .05);
|
||||||
} else if (role === "assistant") {
|
} else if (role === "assistant") {
|
||||||
m.innerHTML = `<div class="assistant-message"><div class="assistant-avatar"></div><div class="assistant-message-content markdown-content" id="${msgId || ""}">${streaming ? "" : marked.parse(content)}</div></div>`;
|
m.innerHTML = `<div class="assistant-message"><div class="assistant-avatar"></div><div class="assistant-message-content markdown-content" id="${msgId || ""}">${streaming ? "" : marked.parse(content)}</div></div>`;
|
||||||
updateContextUsage(contextUsage+.03);
|
this.updateContextUsage(contextUsage + .03);
|
||||||
} else if (role === "voice") {
|
} else if (role === "voice") {
|
||||||
m.innerHTML = `<div class="assistant-message"><div class="assistant-avatar">🎤</div><div class="assistant-message-content">${content}</div></div>`;
|
m.innerHTML = `<div class="assistant-message"><div class="assistant-avatar">🎤</div><div class="assistant-message-content">${content}</div></div>`;
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -414,61 +484,58 @@ function addMessage(role,content,streaming=false,msgId=null){
|
||||||
messagesDiv.appendChild(m);
|
messagesDiv.appendChild(m);
|
||||||
gsap.to(m, { opacity: 1, y: 0, duration: .5, ease: "power2.out" });
|
gsap.to(m, { opacity: 1, y: 0, duration: .5, ease: "power2.out" });
|
||||||
if (!isUserScrolling) {
|
if (!isUserScrolling) {
|
||||||
scrollToBottom();
|
this.scrollToBottom();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
function updateStreamingMessage(c){
|
updateStreamingMessage(c) {
|
||||||
const m = document.getElementById(streamingMessageId);
|
const m = document.getElementById(streamingMessageId);
|
||||||
if (m) {
|
if (m) {
|
||||||
m.innerHTML = marked.parse(c);
|
m.innerHTML = marked.parse(c);
|
||||||
if (!isUserScrolling) {
|
if (!isUserScrolling) {
|
||||||
scrollToBottom();
|
this.scrollToBottom();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
function finalizeStreamingMessage(){
|
finalizeStreamingMessage() {
|
||||||
const m = document.getElementById(streamingMessageId);
|
const m = document.getElementById(streamingMessageId);
|
||||||
if (m) {
|
if (m) {
|
||||||
m.innerHTML = marked.parse(currentStreamingContent);
|
m.innerHTML = marked.parse(currentStreamingContent);
|
||||||
m.removeAttribute("id");
|
m.removeAttribute("id");
|
||||||
if (!isUserScrolling) {
|
if (!isUserScrolling) {
|
||||||
scrollToBottom();
|
this.scrollToBottom();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
function escapeHtml(t){
|
escapeHtml(t) {
|
||||||
const d = document.createElement("div");
|
const d = document.createElement("div");
|
||||||
d.textContent = t;
|
d.textContent = t;
|
||||||
return d.innerHTML;
|
return d.innerHTML;
|
||||||
}
|
},
|
||||||
|
|
||||||
function clearSuggestions(){
|
clearSuggestions() {
|
||||||
suggestionsContainer.innerHTML = '';
|
suggestionsContainer.innerHTML = '';
|
||||||
}
|
},
|
||||||
|
|
||||||
function handleSuggestions(s){
|
handleSuggestions(s) {
|
||||||
const uniqueSuggestions = s.filter((v, i, a) => i === a.findIndex(t => t.text === v.text && t.context === v.context));
|
const uniqueSuggestions = s.filter((v, i, a) => i === a.findIndex(t => t.text === v.text && t.context === v.context));
|
||||||
suggestionsContainer.innerHTML = '';
|
suggestionsContainer.innerHTML = '';
|
||||||
uniqueSuggestions.forEach(v => {
|
uniqueSuggestions.forEach(v => {
|
||||||
const b = document.createElement('button');
|
const b = document.createElement('button');
|
||||||
b.textContent = v.text;
|
b.textContent = v.text;
|
||||||
b.className = 'suggestion-button';
|
b.className = 'suggestion-button';
|
||||||
b.onclick=()=>{setContext(v.context);input.value='';};
|
b.onclick = () => { this.setContext(v.context); input.value = ''; };
|
||||||
suggestionsContainer.appendChild(b);
|
suggestionsContainer.appendChild(b);
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
|
|
||||||
let pendingContextChange=null;
|
async setContext(c) {
|
||||||
|
|
||||||
async function setContext(c){
|
|
||||||
try {
|
try {
|
||||||
const t = event?.target?.textContent || c;
|
const t = event?.target?.textContent || c;
|
||||||
addMessage("user",t);
|
this.addMessage("user", t);
|
||||||
const i=document.getElementById('messageInput');
|
input.value = '';
|
||||||
if(i){i.value='';}
|
|
||||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||||
pendingContextChange = new Promise(r => {
|
pendingContextChange = new Promise(r => {
|
||||||
const h = e => {
|
const h = e => {
|
||||||
|
|
@ -487,14 +554,14 @@ async function setContext(c){
|
||||||
if (x) { document.getElementById('contextPercentage').textContent = c; }
|
if (x) { document.getElementById('contextPercentage').textContent = c; }
|
||||||
} else {
|
} else {
|
||||||
console.warn("WebSocket não está conectado. Tentando reconectar...");
|
console.warn("WebSocket não está conectado. Tentando reconectar...");
|
||||||
connectWebSocket();
|
this.connectWebSocket();
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to set context:', err);
|
console.error('Failed to set context:', err);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
async function sendMessage(){
|
async sendMessage() {
|
||||||
if (pendingContextChange) {
|
if (pendingContextChange) {
|
||||||
await pendingContextChange;
|
await pendingContextChange;
|
||||||
pendingContextChange = null;
|
pendingContextChange = null;
|
||||||
|
|
@ -502,56 +569,62 @@ async function sendMessage(){
|
||||||
const m = input.value.trim();
|
const m = input.value.trim();
|
||||||
if (!m || !ws || ws.readyState !== WebSocket.OPEN) {
|
if (!m || !ws || ws.readyState !== WebSocket.OPEN) {
|
||||||
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
||||||
showWarning("Conexão não disponível. Tentando reconectar...");
|
this.showWarning("Conexão não disponível. Tentando reconectar...");
|
||||||
connectWebSocket();
|
this.connectWebSocket();
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (isThinking) {
|
if (isThinking) {
|
||||||
hideThinkingIndicator();
|
this.hideThinkingIndicator();
|
||||||
}
|
}
|
||||||
addMessage("user",m);
|
this.addMessage("user", m);
|
||||||
const d = { bot_id: currentBotId, user_id: currentUserId, session_id: currentSessionId, channel: "web", content: m, message_type: 1, media_url: null, timestamp: new Date().toISOString() };
|
const d = { bot_id: currentBotId, user_id: currentUserId, session_id: currentSessionId, channel: "web", content: m, message_type: 1, media_url: null, timestamp: new Date().toISOString() };
|
||||||
ws.send(JSON.stringify(d));
|
ws.send(JSON.stringify(d));
|
||||||
input.value = "";
|
input.value = "";
|
||||||
input.focus();
|
input.focus();
|
||||||
}
|
},
|
||||||
|
|
||||||
sendBtn.onclick=sendMessage;
|
async toggleVoiceMode() {
|
||||||
input.addEventListener("keypress",e=>{if(e.key==="Enter")sendMessage();});
|
|
||||||
|
|
||||||
async function toggleVoiceMode(){
|
|
||||||
isVoiceMode = !isVoiceMode;
|
isVoiceMode = !isVoiceMode;
|
||||||
const v = document.getElementById("voiceToggle");
|
const v = document.getElementById("voiceToggle");
|
||||||
if (isVoiceMode) {
|
if (isVoiceMode) {
|
||||||
v.textContent = "🔴 Stop Voice";
|
v.textContent = "🔴 Stop Voice";
|
||||||
v.classList.add("recording");
|
v.classList.add("recording");
|
||||||
await startVoiceSession();
|
await this.startVoiceSession();
|
||||||
} else {
|
} else {
|
||||||
v.textContent = "🎤 Voice Mode";
|
v.textContent = "🎤 Voice Mode";
|
||||||
v.classList.remove("recording");
|
v.classList.remove("recording");
|
||||||
await stopVoiceSession();
|
await this.stopVoiceSession();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
async function startVoiceSession(){
|
async startVoiceSession() {
|
||||||
if (!currentSessionId) return;
|
if (!currentSessionId) return;
|
||||||
try {
|
try {
|
||||||
const r=await fetch("http://localhost:8080/api/voice/start",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({session_id:currentSessionId,user_id:currentUserId})}),d=await r.json();
|
const r = await fetch("http://localhost:8080/api/voice/start", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ session_id: currentSessionId, user_id: currentUserId })
|
||||||
|
});
|
||||||
|
const d = await r.json();
|
||||||
if (d.token) {
|
if (d.token) {
|
||||||
await connectToVoiceRoom(d.token);
|
await this.connectToVoiceRoom(d.token);
|
||||||
startVoiceRecording();
|
this.startVoiceRecording();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to start voice session:", e);
|
console.error("Failed to start voice session:", e);
|
||||||
showWarning("Falha ao iniciar modo de voz");
|
this.showWarning("Falha ao iniciar modo de voz");
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
async function stopVoiceSession(){
|
async stopVoiceSession() {
|
||||||
if (!currentSessionId) return;
|
if (!currentSessionId) return;
|
||||||
try {
|
try {
|
||||||
await fetch("http://localhost:8080/api/voice/stop",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({session_id:currentSessionId})});
|
await fetch("http://localhost:8080/api/voice/stop", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ session_id: currentSessionId })
|
||||||
|
});
|
||||||
if (voiceRoom) {
|
if (voiceRoom) {
|
||||||
voiceRoom.disconnect();
|
voiceRoom.disconnect();
|
||||||
voiceRoom = null;
|
voiceRoom = null;
|
||||||
|
|
@ -562,11 +635,12 @@ async function stopVoiceSession(){
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to stop voice session:", e);
|
console.error("Failed to stop voice session:", e);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
async function connectToVoiceRoom(t){
|
async connectToVoiceRoom(t) {
|
||||||
try {
|
try {
|
||||||
const r=new LiveKitClient.Room(),p="ws:",u=`${p}//localhost:8080/voice`;
|
const r = new LiveKitClient.Room();
|
||||||
|
const p = "ws:", u = `${p}//localhost:8080/voice`;
|
||||||
await r.connect(u, t);
|
await r.connect(u, t);
|
||||||
voiceRoom = r;
|
voiceRoom = r;
|
||||||
r.on("dataReceived", d => {
|
r.on("dataReceived", d => {
|
||||||
|
|
@ -574,7 +648,7 @@ async function connectToVoiceRoom(t){
|
||||||
try {
|
try {
|
||||||
const j = JSON.parse(m);
|
const j = JSON.parse(m);
|
||||||
if (j.type === "voice_response") {
|
if (j.type === "voice_response") {
|
||||||
addMessage("assistant",j.text);
|
this.addMessage("assistant", j.text);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log("Voice data:", m);
|
console.log("Voice data:", m);
|
||||||
|
|
@ -586,11 +660,11 @@ async function connectToVoiceRoom(t){
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to connect to voice room:", e);
|
console.error("Failed to connect to voice room:", e);
|
||||||
showWarning("Falha na conexão de voz");
|
this.showWarning("Falha na conexão de voz");
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
function startVoiceRecording(){
|
startVoiceRecording() {
|
||||||
if (!navigator.mediaDevices) {
|
if (!navigator.mediaDevices) {
|
||||||
console.log("Media devices not supported");
|
console.log("Media devices not supported");
|
||||||
return;
|
return;
|
||||||
|
|
@ -599,42 +673,41 @@ function startVoiceRecording(){
|
||||||
mediaRecorder = new MediaRecorder(s);
|
mediaRecorder = new MediaRecorder(s);
|
||||||
audioChunks = [];
|
audioChunks = [];
|
||||||
mediaRecorder.ondataavailable = e => { audioChunks.push(e.data); };
|
mediaRecorder.ondataavailable = e => { audioChunks.push(e.data); };
|
||||||
mediaRecorder.onstop=()=>{const a=new Blob(audioChunks,{type:"audio/wav"});simulateVoiceTranscription();};
|
mediaRecorder.onstop = () => { const a = new Blob(audioChunks, { type: "audio/wav" }); this.simulateVoiceTranscription(); };
|
||||||
mediaRecorder.start();
|
mediaRecorder.start();
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (mediaRecorder && mediaRecorder.state === "recording") {
|
if (mediaRecorder && mediaRecorder.state === "recording") {
|
||||||
mediaRecorder.stop();
|
mediaRecorder.stop();
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (isVoiceMode) {
|
if (isVoiceMode) {
|
||||||
startVoiceRecording();
|
this.startVoiceRecording();
|
||||||
}
|
}
|
||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
}, 5000);
|
}, 5000);
|
||||||
}).catch(e => {
|
}).catch(e => {
|
||||||
console.error("Error accessing microphone:", e);
|
console.error("Error accessing microphone:", e);
|
||||||
showWarning("Erro ao acessar microfone");
|
this.showWarning("Erro ao acessar microfone");
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
|
|
||||||
function simulateVoiceTranscription(){
|
simulateVoiceTranscription() {
|
||||||
const p=["Olá, como posso ajudá-lo hoje?","Entendo o que você está dizendo","Esse é um ponto interessante","Deixe-me pensar sobre isso","Posso ajudá-lo com isso","O que você gostaria de saber?","Isso parece ótimo","Estou ouvindo sua voz"],r=p[Math.floor(Math.random()*p.length)];
|
const p = ["Olá, como posso ajudá-lo hoje?", "Entendo o que você está dizendo", "Esse é um ponto interessante", "Deixe-me pensar sobre isso", "Posso ajudá-lo com isso", "O que você gostaria de saber?", "Isso parece ótimo", "Estou ouvindo sua voz"];
|
||||||
|
const r = p[Math.floor(Math.random() * p.length)];
|
||||||
if (voiceRoom) {
|
if (voiceRoom) {
|
||||||
const m = { type: "voice_input", content: r, timestamp: new Date().toISOString() };
|
const m = { type: "voice_input", content: r, timestamp: new Date().toISOString() };
|
||||||
voiceRoom.localParticipant.publishData(new TextEncoder().encode(JSON.stringify(m)), LiveKitClient.DataPacketKind.RELIABLE);
|
voiceRoom.localParticipant.publishData(new TextEncoder().encode(JSON.stringify(m)), LiveKitClient.DataPacketKind.RELIABLE);
|
||||||
}
|
}
|
||||||
addMessage("voice",`🎤 ${r}`);
|
this.addMessage("voice", `🎤 ${r}`);
|
||||||
}
|
},
|
||||||
|
|
||||||
function scrollToBottom(){
|
scrollToBottom() {
|
||||||
messagesDiv.scrollTop = messagesDiv.scrollHeight;
|
messagesDiv.scrollTop = messagesDiv.scrollHeight;
|
||||||
isUserScrolling = false;
|
isUserScrolling = false;
|
||||||
scrollToBottomBtn.classList.remove('visible');
|
scrollToBottomBtn.classList.remove('visible');
|
||||||
}
|
}
|
||||||
|
};
|
||||||
window.addEventListener("load",initializeAuth);
|
|
||||||
window.addEventListener("focus",function(){
|
|
||||||
if(!ws||ws.readyState!==WebSocket.OPEN){
|
|
||||||
connectWebSocket();
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
// Initialize the app
|
||||||
|
chatApp().init();
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,18 @@
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<title>General Bots Desktop</title>
|
<title>General Bots</title>
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
||||||
<link rel="stylesheet" href="css/app.css" />
|
<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>
|
<script defer src="js/alpine.js"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<nav x-data="{ current: 'drive' }">
|
<nav x-data="{ current: 'drive' }">
|
||||||
<div class="logo">⚡ General Bots</div>
|
<div class="logo">⚡ General Bots</div>
|
||||||
|
|
@ -27,16 +33,11 @@
|
||||||
|
|
||||||
<!-- Load Module Scripts -->
|
<!-- Load Module Scripts -->
|
||||||
<script src="js/layout.js"></script>
|
<script src="js/layout.js"></script>
|
||||||
|
<script src="chat/chat.js"></script>
|
||||||
<script src="drive/drive.js"></script>
|
<script src="drive/drive.js"></script>
|
||||||
<script src="tasks/tasks.js"></script>
|
<script src="tasks/tasks.js"></script>
|
||||||
<script src="mail/mail.js"></script>
|
<script src="mail/mail.js"></script>
|
||||||
<script src="dashboard/dashboard.js"></script>
|
|
||||||
<script src="editor/editor.js"></script>
|
|
||||||
<script src="player/player.js"></script>
|
|
||||||
<script src="paper/paper.js"></script>
|
|
||||||
<script src="settings/settings.js"></script>
|
|
||||||
<script src="tables/tables.js"></script>
|
|
||||||
<script src="news/news.js"></script>
|
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
Loading…
Add table
Reference in a new issue