feat: add actix-files dependency for file serving support
Added actix-files and its dependencies (http-range, mime_guess, unicase, v_htmlescape) to enable static file functionality in the botserver. This will allow serving static assets and files through the web server. The change includes all required transitive dependencies for proper file handling and MIME type detection.
This commit is contained in:
parent
3014822ace
commit
01e89c9358
50 changed files with 1286 additions and 3414 deletions
52
Cargo.lock
generated
52
Cargo.lock
generated
|
|
@ -34,6 +34,29 @@ dependencies = [
|
||||||
"smallvec",
|
"smallvec",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "actix-files"
|
||||||
|
version = "0.6.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6c0d87f10d70e2948ad40e8edea79c8e77c6c66e0250a4c1f09b690465199576"
|
||||||
|
dependencies = [
|
||||||
|
"actix-http",
|
||||||
|
"actix-service",
|
||||||
|
"actix-utils",
|
||||||
|
"actix-web",
|
||||||
|
"bitflags 2.10.0",
|
||||||
|
"bytes",
|
||||||
|
"derive_more 2.0.1",
|
||||||
|
"futures-core",
|
||||||
|
"http-range",
|
||||||
|
"log",
|
||||||
|
"mime",
|
||||||
|
"mime_guess",
|
||||||
|
"percent-encoding",
|
||||||
|
"pin-project-lite",
|
||||||
|
"v_htmlescape",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "actix-http"
|
name = "actix-http"
|
||||||
version = "3.11.2"
|
version = "3.11.2"
|
||||||
|
|
@ -1330,6 +1353,7 @@ name = "botserver"
|
||||||
version = "6.0.8"
|
version = "6.0.8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"actix-cors",
|
"actix-cors",
|
||||||
|
"actix-files",
|
||||||
"actix-multipart",
|
"actix-multipart",
|
||||||
"actix-web",
|
"actix-web",
|
||||||
"actix-ws",
|
"actix-ws",
|
||||||
|
|
@ -3665,6 +3689,12 @@ dependencies = [
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "http-range"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "httparse"
|
name = "httparse"
|
||||||
version = "1.10.1"
|
version = "1.10.1"
|
||||||
|
|
@ -4775,6 +4805,16 @@ version = "0.3.17"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mime_guess"
|
||||||
|
version = "2.0.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
|
||||||
|
dependencies = [
|
||||||
|
"mime",
|
||||||
|
"unicase",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "minimal-lexical"
|
name = "minimal-lexical"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
|
|
@ -8592,6 +8632,12 @@ dependencies = [
|
||||||
"unic-common",
|
"unic-common",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicase"
|
||||||
|
version = "2.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-bidi"
|
name = "unicode-bidi"
|
||||||
version = "0.3.18"
|
version = "0.3.18"
|
||||||
|
|
@ -8766,6 +8812,12 @@ dependencies = [
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "v_htmlescape"
|
||||||
|
version = "0.15.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4e8257fbc510f0a46eb602c10215901938b5c2a7d5e70fc11483b1d3c9b5b18c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "valuable"
|
name = "valuable"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@ desktop = ["dep:tauri", "dep:tauri-plugin-dialog", "dep:tauri-plugin-opener"]
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|
||||||
actix-cors = "0.7"
|
actix-cors = "0.7"
|
||||||
|
actix-files = "0.6.8"
|
||||||
actix-multipart = "0.7"
|
actix-multipart = "0.7"
|
||||||
actix-web = "4.9"
|
actix-web = "4.9"
|
||||||
actix-ws = "0.3"
|
actix-ws = "0.3"
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ impl DriveMonitor {
|
||||||
pub fn spawn(self: Arc<Self>) -> tokio::task::JoinHandle<()> {
|
pub fn spawn(self: Arc<Self>) -> tokio::task::JoinHandle<()> {
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
info!("Drive Monitor service started for bucket: {}", self.bucket_name);
|
info!("Drive Monitor service started for bucket: {}", self.bucket_name);
|
||||||
let mut tick = interval(Duration::from_secs(30));
|
let mut tick = interval(Duration::from_secs(90));
|
||||||
loop {
|
loop {
|
||||||
tick.tick().await;
|
tick.tick().await;
|
||||||
if let Err(e) = self.check_for_changes().await {
|
if let Err(e) = self.check_for_changes().await {
|
||||||
|
|
@ -44,7 +44,7 @@ impl DriveMonitor {
|
||||||
None => return Ok(()),
|
None => return Ok(()),
|
||||||
};
|
};
|
||||||
self.check_gbdialog_changes(client).await?;
|
self.check_gbdialog_changes(client).await?;
|
||||||
// TODO: Remove self.check_gbot(client).await?;
|
self.check_gbot(client).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
async fn check_gbdialog_changes(&self, client: &Client) -> Result<(), Box<dyn Error + Send + Sync>> {
|
async fn check_gbdialog_changes(&self, client: &Client) -> Result<(), Box<dyn Error + Send + Sync>> {
|
||||||
|
|
|
||||||
|
|
@ -247,25 +247,25 @@ pub async fn start_llm_server(
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// if n_moe != "0" {
|
if n_moe != "0" {
|
||||||
// args.push_str(&format!(" --n-cpu-moe {}", n_moe));
|
args.push_str(&format!(" --n-cpu-moe {}", n_moe));
|
||||||
// }
|
}
|
||||||
// if parallel != "1" {
|
if parallel != "1" {
|
||||||
// args.push_str(&format!(" --parallel {}", parallel));
|
args.push_str(&format!(" --parallel {}", parallel));
|
||||||
// }
|
}
|
||||||
// if cont_batching == "true" {
|
if cont_batching == "true" {
|
||||||
// args.push_str(" --cont-batching");
|
args.push_str(" --cont-batching");
|
||||||
// }
|
}
|
||||||
// if mlock == "true" {
|
if mlock == "true" {
|
||||||
// args.push_str(" --mlock");
|
args.push_str(" --mlock");
|
||||||
// }
|
}
|
||||||
// if no_mmap == "true" {
|
if no_mmap == "true" {
|
||||||
// args.push_str(" --no-mmap");
|
args.push_str(" --no-mmap");
|
||||||
// }
|
}
|
||||||
// if n_predict != "0" {
|
if n_predict != "0" {
|
||||||
// args.push_str(&format!(" --n-predict {}", n_predict));
|
args.push_str(&format!(" --n-predict {}", n_predict));
|
||||||
// }
|
}
|
||||||
// args.push_str(&format!(" --ctx-size {}", n_ctx_size));
|
args.push_str(&format!(" --ctx-size {}", n_ctx_size));
|
||||||
|
|
||||||
if cfg!(windows) {
|
if cfg!(windows) {
|
||||||
let mut cmd = tokio::process::Command::new("cmd");
|
let mut cmd = tokio::process::Command::new("cmd");
|
||||||
|
|
|
||||||
29
src/main.rs
29
src/main.rs
|
|
@ -46,7 +46,7 @@ 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, index, static_files};
|
use crate::web_server::{bot_index, index};
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum BootstrapProgress {
|
pub enum BootstrapProgress {
|
||||||
StartingBootstrap,
|
StartingBootstrap,
|
||||||
|
|
@ -77,7 +77,29 @@ async fn main() -> std::io::Result<()> {
|
||||||
let (state_tx, state_rx) = tokio::sync::mpsc::channel::<Arc<AppState>>(1);
|
let (state_tx, state_rx) = tokio::sync::mpsc::channel::<Arc<AppState>>(1);
|
||||||
let (http_tx, http_rx) = tokio::sync::oneshot::channel();
|
let (http_tx, http_rx) = tokio::sync::oneshot::channel();
|
||||||
|
|
||||||
let ui_handle = if !no_ui {
|
|
||||||
|
let args: Vec<String> = std::env::args().collect();
|
||||||
|
if args.len() > 1 {
|
||||||
|
let command = &args[1];
|
||||||
|
match command.as_str() {
|
||||||
|
"install" | "remove" | "list" | "status" | "start" | "stop" | "restart" | "--help"
|
||||||
|
| "-h" => match package_manager::cli::run().await {
|
||||||
|
Ok(_) => return Ok(()),
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("CLI error: {}", e);
|
||||||
|
return Err(std::io::Error::new(
|
||||||
|
std::io::ErrorKind::Other,
|
||||||
|
format!("CLI command failed: {}", e),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if !no_ui {
|
||||||
let progress_rx = Arc::new(tokio::sync::Mutex::new(progress_rx));
|
let progress_rx = Arc::new(tokio::sync::Mutex::new(progress_rx));
|
||||||
let state_rx = Arc::new(tokio::sync::Mutex::new(state_rx));
|
let state_rx = Arc::new(tokio::sync::Mutex::new(state_rx));
|
||||||
let handle = std::thread::Builder::new()
|
let handle = std::thread::Builder::new()
|
||||||
|
|
@ -284,11 +306,11 @@ 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)
|
||||||
.service(get_sessions)
|
.service(get_sessions)
|
||||||
.service(index)
|
|
||||||
.service(start_session)
|
.service(start_session)
|
||||||
.service(upload_file)
|
.service(upload_file)
|
||||||
.service(voice_start)
|
.service(voice_start)
|
||||||
|
|
@ -310,7 +332,6 @@ async fn main() -> std::io::Result<()> {
|
||||||
.service(save_draft)
|
.service(save_draft)
|
||||||
.service(save_click);
|
.service(save_click);
|
||||||
}
|
}
|
||||||
app = app.service(static_files);
|
|
||||||
app = app.service(bot_index);
|
app = app.service(bot_index);
|
||||||
app
|
app
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@ pub async fn run() -> Result<()> {
|
||||||
print_usage();
|
print_usage();
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
use tracing::info;
|
||||||
|
fn print_usage(){info!("usage: botserver <command> [options]")}
|
||||||
let command = &args[1];
|
let command = &args[1];
|
||||||
match command.as_str() {
|
match command.as_str() {
|
||||||
"start" => {
|
"start" => {
|
||||||
|
|
@ -164,6 +166,3 @@ pub async fn run() -> Result<()> {
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
fn print_usage() {
|
|
||||||
println!("BotServer Package Manager\n\nUSAGE:\n botserver <command> [options]\n\nCOMMANDS:\n start Start all installed components\n stop Stop all running components\n restart Restart all components\n install <component> Install component\n remove <component> Remove component\n list List all components\n status <component> Check component status\n\nOPTIONS:\n --container Use container mode (LXC)\n --tenant <name> Specify tenant (default: 'default')\n\nCOMPONENTS:\n Required: drive cache tables llm\n Optional: email proxy directory alm alm-ci dns webmail meeting table-editor doc-editor desktop devtools bot system vector-db host\n\nEXAMPLES:\n botserver start\n botserver stop\n botserver restart\n botserver install email\n botserver install email --container --tenant myorg\n botserver remove email\n botserver list");
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -156,6 +156,7 @@ impl PackageManager {
|
||||||
);
|
);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remove(&self, component_name: &str) -> Result<()> {
|
pub fn remove(&self, component_name: &str) -> Result<()> {
|
||||||
let component = self
|
let component = self
|
||||||
.components
|
.components
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,11 @@
|
||||||
|
use actix_files::Files;
|
||||||
use actix_web::{HttpRequest, HttpResponse, Result};
|
use actix_web::{HttpRequest, HttpResponse, Result};
|
||||||
use log::{debug, error, warn};
|
use log::{debug, error};
|
||||||
use std::fs;
|
use std::{fs, path::Path};
|
||||||
|
|
||||||
#[actix_web::get("/auth")]
|
|
||||||
async fn auth() -> Result<HttpResponse> {
|
|
||||||
match fs::read_to_string("web/desktop/auth/index.html") {
|
|
||||||
Ok(html) => Ok(HttpResponse::Ok().content_type("text/html").body(html)),
|
|
||||||
Err(e) => {
|
|
||||||
error!("Failed to load auth page: {}", e);
|
|
||||||
Ok(HttpResponse::InternalServerError().body("Failed to load auth page"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#[actix_web::get("/")]
|
#[actix_web::get("/")]
|
||||||
async fn index() -> Result<HttpResponse> {
|
async fn index() -> Result<HttpResponse> {
|
||||||
match fs::read_to_string("web/desktop/auth/index.html") {
|
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: {}", e);
|
error!("Failed to load index page: {}", e);
|
||||||
|
|
@ -22,11 +13,12 @@ async fn index() -> Result<HttpResponse> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix_web::get("/{botname}")]
|
#[actix_web::get("/{botname}")]
|
||||||
async fn bot_index(req: HttpRequest) -> Result<HttpResponse> {
|
async fn bot_index(req: HttpRequest) -> Result<HttpResponse> {
|
||||||
let botname = req.match_info().query("botname");
|
let botname = req.match_info().query("botname");
|
||||||
debug!("Serving bot interface for: {}", botname);
|
debug!("Serving bot interface for: {}", botname);
|
||||||
match fs::read_to_string("web/html/index.html") {
|
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 index page for bot {}: {}", botname, e);
|
||||||
|
|
@ -34,31 +26,39 @@ async fn bot_index(req: HttpRequest) -> Result<HttpResponse> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#[actix_web::get("/static/{filename:.*}")]
|
|
||||||
async fn static_files(req: HttpRequest) -> Result<HttpResponse> {
|
pub fn configure_app(cfg: &mut actix_web::web::ServiceConfig) {
|
||||||
let filename = req.match_info().query("filename");
|
let static_path = Path::new("/home/rodriguez/src/botserver/web/desktop");
|
||||||
let path = format!("web/html/{}", filename);
|
|
||||||
match fs::read(&path) {
|
// Serve all static files from desktop directory
|
||||||
Ok(content) => {
|
cfg.service(
|
||||||
debug!(
|
Files::new("/", static_path)
|
||||||
"Static file {} loaded successfully, size: {} bytes",
|
.index_file("index.html")
|
||||||
filename,
|
.prefer_utf8(true)
|
||||||
content.len()
|
.use_last_modified(true)
|
||||||
);
|
.use_etag(true)
|
||||||
let content_type = match filename {
|
.show_files_listing()
|
||||||
f if f.ends_with(".js") => "application/javascript",
|
);
|
||||||
f if f.ends_with(".riot") => "application/javascript",
|
|
||||||
f if f.ends_with(".html") => "application/javascript",
|
// Serve all JS files
|
||||||
f if f.ends_with(".css") => "text/css",
|
cfg.service(
|
||||||
f if f.ends_with(".png") => "image/png",
|
Files::new("/js", static_path.join("js"))
|
||||||
f if f.ends_with(".jpg") | f.ends_with(".jpeg") => "image/jpeg",
|
.prefer_utf8(true)
|
||||||
_ => "text/plain",
|
.use_last_modified(true)
|
||||||
};
|
.use_etag(true)
|
||||||
Ok(HttpResponse::Ok().content_type(content_type).body(content))
|
);
|
||||||
}
|
|
||||||
Err(e) => {
|
// Serve all component directories
|
||||||
warn!("Static file not found: {} - {}", filename, e);
|
["drive", "tasks", "mail"].iter().for_each(|dir| {
|
||||||
Ok(HttpResponse::NotFound().body("File not found"))
|
cfg.service(
|
||||||
}
|
Files::new(&format!("/{}", dir), static_path.join(dir))
|
||||||
}
|
.prefer_utf8(true)
|
||||||
|
.use_last_modified(true)
|
||||||
|
.use_etag(true)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Serve index routes
|
||||||
|
cfg.service(index);
|
||||||
|
cfg.service(bot_index);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
55
web/app/tablesv2/index.html
Normal file
55
web/app/tablesv2/index.html
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
<div class="app-container" x-data="tablesApp()" x-init="init()">
|
||||||
|
<div class="navbar-container">
|
||||||
|
<div x-html="(await fetch('./components/navbar.html')).text()"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content-container">
|
||||||
|
<div class="header">
|
||||||
|
<h1>📊 Tables</h1>
|
||||||
|
<div class="subtitle">Excel Clone - Celebrating Lotus 1-2-3 Legacy 🎉</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="resizable-container">
|
||||||
|
<div class="resizable-panel left" style="width: 30%">
|
||||||
|
<!-- Left panel content -->
|
||||||
|
</div>
|
||||||
|
<div class="resizable-handle"></div>
|
||||||
|
<div class="resizable-panel right" style="width: 70%">
|
||||||
|
<div class="spreadsheet-content">
|
||||||
|
<table>
|
||||||
|
<thead id="tableHead"></thead>
|
||||||
|
<tbody id="tableBody"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Formula bar -->
|
||||||
|
<div class="formula-bar">
|
||||||
|
<span id="cellRef">A1</span>
|
||||||
|
<input type="text"
|
||||||
|
id="formulaInput"
|
||||||
|
placeholder="Enter formula..."
|
||||||
|
@keypress.enter="updateCellValue($event.target.value)"
|
||||||
|
x-model="formulaInputValue">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Status bar -->
|
||||||
|
<div class="status-bar">
|
||||||
|
<span>Rows: <span id="rowCount" x-text="rows"></span></span>
|
||||||
|
<span>Columns: <span id="colCount" x-text="cols"></span></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toolbar -->
|
||||||
|
<div class="toolbar">
|
||||||
|
<button @click="addRow()">Add Row</button>
|
||||||
|
<button @click="addColumn()">Add Column</button>
|
||||||
|
<button @click="deleteRow()">Delete Row</button>
|
||||||
|
<button @click="deleteColumn()">Delete Column</button>
|
||||||
|
<button @click="sort()">Sort</button>
|
||||||
|
<button @click="sum()">Sum</button>
|
||||||
|
<button @click="average()">Average</button>
|
||||||
|
<button @click="exportData()">Export</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
85
web/app/tablesv2/tables.css
Normal file
85
web/app/tablesv2/tables.css
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
/* Tables Component Styles */
|
||||||
|
.app-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100vh;
|
||||||
|
background: var(--background);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-container {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
background: linear-gradient(135deg, var(--primary) 0%, var(--accent) 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 15px 20px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h1 {
|
||||||
|
font-size: 24px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header .subtitle {
|
||||||
|
font-size: 12px;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spreadsheet-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Spreadsheet specific styles */
|
||||||
|
table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
th, td {
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
padding: 8px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
background-color: #f2f2f2;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected {
|
||||||
|
background-color: #e6f2ff;
|
||||||
|
outline: 2px solid #4d90fe;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editing input {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resizable-container {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resizable-panel {
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resizable-handle {
|
||||||
|
width: 10px;
|
||||||
|
background: #f0f0f0;
|
||||||
|
cursor: col-resize;
|
||||||
|
}
|
||||||
293
web/app/tablesv2/tables.js
Normal file
293
web/app/tablesv2/tables.js
Normal file
|
|
@ -0,0 +1,293 @@
|
||||||
|
function tablesApp() {
|
||||||
|
return {
|
||||||
|
data: [],
|
||||||
|
selectedCell: null,
|
||||||
|
cols: 26,
|
||||||
|
rows: 100,
|
||||||
|
visibleRows: 30,
|
||||||
|
rowOffset: 0,
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.data = this.generateMockData(this.rows, this.cols);
|
||||||
|
this.setupEventListeners();
|
||||||
|
this.updateStats();
|
||||||
|
},
|
||||||
|
|
||||||
|
generateMockData(rows, cols) {
|
||||||
|
const data = [];
|
||||||
|
const products = ['Laptop', 'Mouse', 'Keyboard', 'Monitor', 'Headphones', 'Webcam', 'Desk', 'Chair'];
|
||||||
|
const regions = ['North', 'South', 'East', 'West'];
|
||||||
|
|
||||||
|
for (let i = 0; i < rows; i++) {
|
||||||
|
const row = {};
|
||||||
|
for (let j = 0; j < cols; j++) {
|
||||||
|
const col = this.getColumnName(j);
|
||||||
|
if (i === 0) {
|
||||||
|
if (j === 0) row[col] = 'Product';
|
||||||
|
else if (j === 1) row[col] = 'Region';
|
||||||
|
else if (j === 2) row[col] = 'Q1';
|
||||||
|
else if (j === 3) row[col] = 'Q2';
|
||||||
|
else if (j === 4) row[col] = 'Q3';
|
||||||
|
else if (j === 5) row[col] = 'Q4';
|
||||||
|
else if (j === 6) row[col] = 'Total';
|
||||||
|
else row[col] = `Col ${col}`;
|
||||||
|
} else {
|
||||||
|
if (j === 0) row[col] = products[i % products.length];
|
||||||
|
else if (j === 1) row[col] = regions[i % regions.length];
|
||||||
|
else if (j >= 2 && j <= 5) row[col] = Math.floor(Math.random() * 10000) + 1000;
|
||||||
|
else if (j === 6) {
|
||||||
|
row[col] = `=C${i+1}+D${i+1}+E${i+1}+F${i+1}`;
|
||||||
|
}
|
||||||
|
else row[col] = Math.random() > 0.5 ? Math.floor(Math.random() * 1000) : '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data.push(row);
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
|
||||||
|
getColumnName(index) {
|
||||||
|
let name = '';
|
||||||
|
while (index >= 0) {
|
||||||
|
name = String.fromCharCode(65 + (index % 26)) + name;
|
||||||
|
index = Math.floor(index / 26) - 1;
|
||||||
|
}
|
||||||
|
return name;
|
||||||
|
},
|
||||||
|
|
||||||
|
setupEventListeners() {
|
||||||
|
// Will be replaced with Alpine.js directives
|
||||||
|
},
|
||||||
|
|
||||||
|
selectCell(cell) {
|
||||||
|
if (this.selectedCell) {
|
||||||
|
this.selectedCell.classList.remove('selected');
|
||||||
|
}
|
||||||
|
this.selectedCell = cell;
|
||||||
|
cell.classList.add('selected');
|
||||||
|
|
||||||
|
const cellRef = cell.dataset.cell;
|
||||||
|
document.getElementById('cellRef').textContent = cellRef;
|
||||||
|
document.getElementById('selectedCell').textContent = cellRef;
|
||||||
|
|
||||||
|
const row = parseInt(cell.dataset.row);
|
||||||
|
const col = this.getColumnName(parseInt(cell.dataset.col));
|
||||||
|
const value = this.data[row][col] || '';
|
||||||
|
document.getElementById('formulaInput').value = value;
|
||||||
|
},
|
||||||
|
|
||||||
|
calculateCell(value, row, col) {
|
||||||
|
if (typeof value === 'string' && value.startsWith('=')) {
|
||||||
|
try {
|
||||||
|
const formula = value.substring(1).toUpperCase();
|
||||||
|
|
||||||
|
if (formula.includes('SUM')) {
|
||||||
|
const match = formula.match(/SUM\(([A-Z]+\d+):([A-Z]+\d+)\)/);
|
||||||
|
if (match) {
|
||||||
|
const sum = this.calculateRange(match[1], match[2], 'sum');
|
||||||
|
return sum.toFixed(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (formula.includes('AVERAGE')) {
|
||||||
|
const match = formula.match(/AVERAGE\(([A-Z]+\d+):([A-Z]+\d+)\)/);
|
||||||
|
if (match) {
|
||||||
|
const avg = this.calculateRange(match[1], match[2], 'avg');
|
||||||
|
return avg.toFixed(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let expression = formula;
|
||||||
|
const cellRefs = expression.match(/[A-Z]+\d+/g);
|
||||||
|
if (cellRefs) {
|
||||||
|
cellRefs.forEach(ref => {
|
||||||
|
const val = this.getCellValue(ref);
|
||||||
|
expression = expression.replace(ref, val);
|
||||||
|
});
|
||||||
|
return eval(expression).toFixed(2);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return '#ERROR';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
},
|
||||||
|
|
||||||
|
getCellValue(cellRef) {
|
||||||
|
const col = cellRef.match(/[A-Z]+/)[0];
|
||||||
|
const row = parseInt(cellRef.match(/\d+/)[0]) - 1;
|
||||||
|
const value = this.data[row][col];
|
||||||
|
|
||||||
|
if (typeof value === 'string' && value.startsWith('=')) {
|
||||||
|
return this.calculateCell(value, row, this.getColIndex(col));
|
||||||
|
}
|
||||||
|
return parseFloat(value) || 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
getColIndex(colName) {
|
||||||
|
let index = 0;
|
||||||
|
for (let i = 0; i < colName.length; i++) {
|
||||||
|
index = index * 26 + (colName.charCodeAt(i) - 64);
|
||||||
|
}
|
||||||
|
return index - 1;
|
||||||
|
},
|
||||||
|
|
||||||
|
calculateRange(start, end, operation) {
|
||||||
|
const startCol = start.match(/[A-Z]+/)[0];
|
||||||
|
const startRow = parseInt(start.match(/\d+/)[0]) - 1;
|
||||||
|
const endCol = end.match(/[A-Z]+/)[0];
|
||||||
|
const endRow = parseInt(end.match(/\d+/)[0]) - 1;
|
||||||
|
|
||||||
|
let values = [];
|
||||||
|
for (let r = startRow; r <= endRow; r++) {
|
||||||
|
for (let c = this.getColIndex(startCol); c <= this.getColIndex(endCol); c++) {
|
||||||
|
const col = this.getColumnName(c);
|
||||||
|
const val = parseFloat(this.data[r][col]) || 0;
|
||||||
|
values.push(val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (operation === 'sum') {
|
||||||
|
return values.reduce((a, b) => a + b, 0);
|
||||||
|
} else if (operation === 'avg') {
|
||||||
|
return values.reduce((a, b) => a + b, 0) / values.length;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
updateCellValue(value) {
|
||||||
|
if (!this.selectedCell) return;
|
||||||
|
|
||||||
|
const row = parseInt(this.selectedCell.dataset.row);
|
||||||
|
const col = this.getColumnName(parseInt(this.selectedCell.dataset.col));
|
||||||
|
|
||||||
|
this.data[row][col] = value;
|
||||||
|
this.renderTable();
|
||||||
|
|
||||||
|
const newCell = document.querySelector(`td[data-row="${row}"][data-col="${this.selectedCell.dataset.col}"]`);
|
||||||
|
if (newCell) this.selectCell(newCell);
|
||||||
|
},
|
||||||
|
|
||||||
|
renderTable() {
|
||||||
|
const thead = document.getElementById('tableHead');
|
||||||
|
const tbody = document.getElementById('tableBody');
|
||||||
|
|
||||||
|
let headerHTML = '<tr><th></th>';
|
||||||
|
for (let i = 0; i < this.cols; i++) {
|
||||||
|
headerHTML += `<th>${this.getColumnName(i)}</th>`;
|
||||||
|
}
|
||||||
|
headerHTML += '</tr>';
|
||||||
|
thead.innerHTML = headerHTML;
|
||||||
|
|
||||||
|
let bodyHTML = '';
|
||||||
|
const endRow = Math.min(this.rowOffset + this.visibleRows, this.rows);
|
||||||
|
|
||||||
|
for (let i = this.rowOffset; i < endRow; i++) {
|
||||||
|
bodyHTML += `<tr><th>${i + 1}</th>`;
|
||||||
|
for (let j = 0; j < this.cols; j++) {
|
||||||
|
const col = this.getColumnName(j);
|
||||||
|
const value = this.data[i][col] || '';
|
||||||
|
const displayValue = this.calculateCell(value, i, j);
|
||||||
|
bodyHTML += `<td @click="selectCell($el)"
|
||||||
|
data-row="${i}"
|
||||||
|
data-col="${j}"
|
||||||
|
data-cell="${col}${i+1}"
|
||||||
|
:class="{ 'selected': selectedCell === $el }">
|
||||||
|
${displayValue}
|
||||||
|
</td>`;
|
||||||
|
}
|
||||||
|
bodyHTML += '</tr>';
|
||||||
|
}
|
||||||
|
tbody.innerHTML = bodyHTML;
|
||||||
|
},
|
||||||
|
|
||||||
|
updateStats() {
|
||||||
|
document.getElementById('rowCount').textContent = this.rows;
|
||||||
|
document.getElementById('colCount').textContent = this.cols;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Toolbar actions
|
||||||
|
addRow() {
|
||||||
|
const newRow = {};
|
||||||
|
for (let i = 0; i < this.cols; i++) {
|
||||||
|
newRow[this.getColumnName(i)] = '';
|
||||||
|
}
|
||||||
|
this.data.push(newRow);
|
||||||
|
this.rows++;
|
||||||
|
this.renderTable();
|
||||||
|
this.updateStats();
|
||||||
|
},
|
||||||
|
|
||||||
|
addColumn() {
|
||||||
|
const newCol = this.getColumnName(this.cols);
|
||||||
|
this.data.forEach(row => row[newCol] = '');
|
||||||
|
this.cols++;
|
||||||
|
this.renderTable();
|
||||||
|
this.updateStats();
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteRow() {
|
||||||
|
if (this.selectedCell && this.rows > 1) {
|
||||||
|
const row = parseInt(this.selectedCell.dataset.row);
|
||||||
|
this.data.splice(row, 1);
|
||||||
|
this.rows--;
|
||||||
|
this.renderTable();
|
||||||
|
this.updateStats();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteColumn() {
|
||||||
|
if (this.selectedCell && this.cols > 1) {
|
||||||
|
const col = this.getColumnName(parseInt(this.selectedCell.dataset.col));
|
||||||
|
this.data.forEach(row => delete row[col]);
|
||||||
|
this.cols--;
|
||||||
|
this.renderTable();
|
||||||
|
this.updateStats();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
sort() {
|
||||||
|
if (this.selectedCell) {
|
||||||
|
const col = this.getColumnName(parseInt(this.selectedCell.dataset.col));
|
||||||
|
const header = this.data[0];
|
||||||
|
const dataRows = this.data.slice(1);
|
||||||
|
|
||||||
|
dataRows.sort((a, b) => {
|
||||||
|
const aVal = a[col] || '';
|
||||||
|
const bVal = b[col] || '';
|
||||||
|
return aVal.toString().localeCompare(bVal.toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
this.data = [header, ...dataRows];
|
||||||
|
this.renderTable();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
sum() {
|
||||||
|
if (this.selectedCell) {
|
||||||
|
const col = this.getColumnName(parseInt(this.selectedCell.dataset.col));
|
||||||
|
this.formulaInputValue = `=SUM(${col}2:${col}${this.rows})`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
average() {
|
||||||
|
if (this.selectedCell) {
|
||||||
|
const col = this.getColumnName(parseInt(this.selectedCell.dataset.col));
|
||||||
|
this.formulaInputValue = `=AVERAGE(${col}2:${col}${this.rows})`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
exportData() {
|
||||||
|
const csv = this.data.map(row => {
|
||||||
|
return Object.values(row).join(',');
|
||||||
|
}).join('\n');
|
||||||
|
|
||||||
|
const blob = new Blob([csv], { type: 'text/csv' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = 'tables_export.csv';
|
||||||
|
a.click();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -1,74 +0,0 @@
|
||||||
document.addEventListener('alpine:init', () => {
|
|
||||||
Alpine.data('auth', () => ({
|
|
||||||
email: '',
|
|
||||||
password: '',
|
|
||||||
rememberMe: false,
|
|
||||||
isLoading: false,
|
|
||||||
error: '',
|
|
||||||
|
|
||||||
async socialLogin(provider) {
|
|
||||||
this.isLoading = true;
|
|
||||||
this.error = '';
|
|
||||||
|
|
||||||
try {
|
|
||||||
// In a real implementation, this would redirect to the auth endpoint
|
|
||||||
const authUrl = `${this.getAuthEndpoint()}/oauth/v2/authorize?` +
|
|
||||||
`client_id=${this.getClientId()}&` +
|
|
||||||
`redirect_uri=${encodeURIComponent(window.location.origin)}&` +
|
|
||||||
`response_type=code&` +
|
|
||||||
`scope=openid profile email&` +
|
|
||||||
`provider=${provider}`;
|
|
||||||
|
|
||||||
window.location.href = authUrl;
|
|
||||||
} catch (err) {
|
|
||||||
this.error = 'Failed to initiate login';
|
|
||||||
console.error('Login error:', err);
|
|
||||||
} finally {
|
|
||||||
this.isLoading = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async emailLogin() {
|
|
||||||
this.isLoading = true;
|
|
||||||
this.error = '';
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch('/api/auth/login', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
email: this.email,
|
|
||||||
password: this.password,
|
|
||||||
rememberMe: this.rememberMe
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const errorData = await response.json();
|
|
||||||
throw new Error(errorData.message || 'Login failed');
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
localStorage.setItem('authToken', data.token);
|
|
||||||
window.location.href = '/tables.html';
|
|
||||||
} catch (err) {
|
|
||||||
this.error = err.message || 'Login failed. Please check your credentials.';
|
|
||||||
console.error('Login error:', err);
|
|
||||||
} finally {
|
|
||||||
this.isLoading = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
getAuthEndpoint() {
|
|
||||||
// In a real app, this would come from config
|
|
||||||
return 'https://auth.example.com';
|
|
||||||
},
|
|
||||||
|
|
||||||
getClientId() {
|
|
||||||
// In a real app, this would come from config
|
|
||||||
return 'general-bots-client';
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
|
|
@ -1,82 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>General Bots - Authentication</title>
|
|
||||||
<link rel="stylesheet" href="styles.css">
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js" defer></script>
|
|
||||||
<script src="app.js" defer></script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="auth-container" x-data="auth">
|
|
||||||
<div class="auth-left-panel">
|
|
||||||
<div class="auth-logo">
|
|
||||||
<h1>Welcome to General Bots</h1>
|
|
||||||
</div>
|
|
||||||
<div class="auth-quote">
|
|
||||||
<p>"Errar é Humano."</p>
|
|
||||||
<p>General Bots</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="auth-form-container">
|
|
||||||
<div class="auth-form-header">
|
|
||||||
<h2>Sign in to your account</h2>
|
|
||||||
<p>Choose your preferred login method</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div x-show="error" class="auth-error" x-text="error"></div>
|
|
||||||
|
|
||||||
<div class="auth-social-buttons">
|
|
||||||
<button class="auth-social-button google" @click="socialLogin('google')">
|
|
||||||
<span class="auth-social-icon">G</span>
|
|
||||||
Continue with Google
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button class="auth-social-button microsoft" @click="socialLogin('microsoft')">
|
|
||||||
<span class="auth-social-icon">M</span>
|
|
||||||
Continue with Microsoft
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button class="auth-social-button pragmatismo" @click="socialLogin('pragmatismo')">
|
|
||||||
<span class="auth-social-icon">P</span>
|
|
||||||
Continue with Pragmatismo
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="auth-divider">
|
|
||||||
<span>OR</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form @submit.prevent="emailLogin" class="auth-form">
|
|
||||||
<div class="auth-form-group">
|
|
||||||
<label for="email">Email</label>
|
|
||||||
<input id="email" type="email" x-model="email" placeholder="your@email.com" required>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="auth-form-group">
|
|
||||||
<label for="password">Password</label>
|
|
||||||
<input id="password" type="password" x-model="password" placeholder="••••••••" required>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="auth-form-options">
|
|
||||||
<div class="auth-remember-me">
|
|
||||||
<input type="checkbox" id="remember" x-model="rememberMe">
|
|
||||||
<label for="remember">Remember me</label>
|
|
||||||
</div>
|
|
||||||
<a href="#" class="auth-forgot-password">Forgot password?</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button type="submit" class="auth-submit-button" :disabled="isLoading">
|
|
||||||
<span x-text="isLoading ? 'Signing in...' : 'Sign in with Email'"></span>
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div class="auth-signup-link">
|
|
||||||
Don't have an account? <a href="#">Sign up</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
@ -1,249 +0,0 @@
|
||||||
:root {
|
|
||||||
--background: #1a1a2e;
|
|
||||||
--foreground: #ffffff;
|
|
||||||
--primary: #4f46e5;
|
|
||||||
--primary-foreground: #ffffff;
|
|
||||||
--secondary: #374151;
|
|
||||||
--secondary-foreground: #ffffff;
|
|
||||||
--muted: #4b5563;
|
|
||||||
--muted-foreground: #9ca3af;
|
|
||||||
--accent: #7c3aed;
|
|
||||||
--destructive: #ef4444;
|
|
||||||
--border: #374151;
|
|
||||||
--input: #1f2937;
|
|
||||||
--radius: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
* {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: 'Segoe UI', sans-serif;
|
|
||||||
background-color: var(--background);
|
|
||||||
color: var(--foreground);
|
|
||||||
min-height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-container {
|
|
||||||
display: flex;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 1200px;
|
|
||||||
background-color: var(--secondary);
|
|
||||||
border-radius: var(--radius);
|
|
||||||
overflow: hidden;
|
|
||||||
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-left-panel {
|
|
||||||
flex: 1;
|
|
||||||
padding: 4rem;
|
|
||||||
background: linear-gradient(135deg, var(--primary), var(--accent));
|
|
||||||
color: var(--primary-foreground);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-logo h1 {
|
|
||||||
font-size: 2rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-quote {
|
|
||||||
font-style: italic;
|
|
||||||
margin-top: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-quote p:last-child {
|
|
||||||
text-align: right;
|
|
||||||
margin-top: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-form-container {
|
|
||||||
flex: 1;
|
|
||||||
padding: 4rem;
|
|
||||||
max-width: 500px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-form-header {
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-form-header h2 {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-form-header p {
|
|
||||||
color: var(--muted-foreground);
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-error {
|
|
||||||
background-color: var(--destructive);
|
|
||||||
color: var(--primary-foreground);
|
|
||||||
padding: 0.75rem;
|
|
||||||
border-radius: var(--radius);
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-social-buttons {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
gap: 0.75rem;
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-social-button {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 0.75rem;
|
|
||||||
border-radius: var(--radius);
|
|
||||||
font-weight: 500;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s;
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
background-color: var(--input);
|
|
||||||
color: var(--foreground);
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-social-button:hover {
|
|
||||||
background-color: var(--muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-social-icon {
|
|
||||||
width: 1.25rem;
|
|
||||||
height: 1.25rem;
|
|
||||||
margin-right: 0.5rem;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-divider {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
margin: 1.5rem 0;
|
|
||||||
color: var(--muted-foreground);
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-divider::before,
|
|
||||||
.auth-divider::after {
|
|
||||||
content: "";
|
|
||||||
flex: 1;
|
|
||||||
border-bottom: 1px solid var(--border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-divider span {
|
|
||||||
padding: 0 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-form {
|
|
||||||
margin-top: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-form-group {
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-form-group label {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-form-group input {
|
|
||||||
width: 100%;
|
|
||||||
padding: 0.75rem;
|
|
||||||
border-radius: var(--radius);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
background-color: var(--input);
|
|
||||||
color: var(--foreground);
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-form-group input:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: var(--primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-form-options {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin: 1rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-remember-me {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-remember-me input {
|
|
||||||
margin-right: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-forgot-password {
|
|
||||||
color: var(--primary);
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-forgot-password:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-submit-button {
|
|
||||||
width: 100%;
|
|
||||||
padding: 0.75rem;
|
|
||||||
border-radius: var(--radius);
|
|
||||||
background-color: var(--primary);
|
|
||||||
color: var(--primary-foreground);
|
|
||||||
font-weight: 500;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-submit-button:hover {
|
|
||||||
background-color: var(--accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-submit-button:disabled {
|
|
||||||
opacity: 0.7;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-signup-link {
|
|
||||||
text-align: center;
|
|
||||||
margin: 1.5rem 0;
|
|
||||||
color: var(--muted-foreground);
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-signup-link a {
|
|
||||||
color: var(--primary);
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-signup-link a:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.auth-container {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-left-panel {
|
|
||||||
padding: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-form-container {
|
|
||||||
padding: 2rem;
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,64 +1,324 @@
|
||||||
/* Main app styles */
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
@import url('../shared/styles.css');
|
|
||||||
@import url('chat.css');
|
|
||||||
|
|
||||||
/* Navbar styles */
|
body {
|
||||||
.navbar {
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
||||||
background: white;
|
background: #0f172a;
|
||||||
color: #333;
|
color: #e2e8f0;
|
||||||
padding: 0.5rem 1rem;
|
height: 100vh;
|
||||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
overflow: hidden;
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
z-index: 1000;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-container {
|
/* Navbar */
|
||||||
|
nav {
|
||||||
|
background: #1e293b;
|
||||||
|
border-bottom: 2px solid #334155;
|
||||||
|
padding: 0 1rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
max-width: 1200px;
|
height: 60px;
|
||||||
margin: 0 auto;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-brand {
|
nav .logo {
|
||||||
|
font-size: 1.5rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: 1.2rem;
|
background: linear-gradient(135deg, #3b82f6, #8b5cf6);
|
||||||
margin-right: 2rem;
|
-webkit-background-clip: text;
|
||||||
color: #333;
|
-webkit-text-fill-color: transparent;
|
||||||
|
margin-right: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-links {
|
nav a {
|
||||||
display: flex;
|
color: #94a3b8;
|
||||||
list-style: none;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-link {
|
|
||||||
color: #555;
|
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
padding: 0.5rem 1rem;
|
padding: 0.75rem 1.25rem;
|
||||||
display: flex;
|
border-radius: 0.5rem;
|
||||||
align-items: center;
|
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
}
|
|
||||||
|
|
||||||
.nav-link:hover {
|
|
||||||
color: #000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-link.active {
|
|
||||||
color: #0066ff;
|
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-link i {
|
nav a:hover {
|
||||||
margin-right: 0.5rem;
|
background: #334155;
|
||||||
color: #666;
|
color: #e2e8f0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-link.active i {
|
nav a.active {
|
||||||
color: #0066ff;
|
background: #3b82f6;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Main Content */
|
||||||
|
#main-content {
|
||||||
|
height: calc(100vh - 60px);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-section {
|
||||||
|
display: none;
|
||||||
|
height: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-section.active {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Panel Styles */
|
||||||
|
.panel {
|
||||||
|
background: #1e293b;
|
||||||
|
border: 1px solid #334155;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Drive Styles */
|
||||||
|
.drive-layout {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 250px 1fr 300px;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 1rem;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drive-sidebar, .drive-details {
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drive-main {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
margin: 0.25rem 0.5rem;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item:hover {
|
||||||
|
background: #334155;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item.active {
|
||||||
|
background: #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-list {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-item {
|
||||||
|
padding: 1rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
border-bottom: 1px solid #334155;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-item:hover {
|
||||||
|
background: #334155;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-item.selected {
|
||||||
|
background: #1e40af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-icon {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tasks Styles */
|
||||||
|
.tasks-container {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-input {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-input input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 0.75rem;
|
||||||
|
background: #1e293b;
|
||||||
|
border: 1px solid #334155;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
color: #e2e8f0;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-input input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-input button {
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
background: #3b82f6;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-input button:hover {
|
||||||
|
background: #2563eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-list {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-item {
|
||||||
|
padding: 1rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
background: #1e293b;
|
||||||
|
border: 1px solid #334155;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-item.completed span {
|
||||||
|
text-decoration: line-through;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-item input[type="checkbox"] {
|
||||||
|
width: 1.25rem;
|
||||||
|
height: 1.25rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-item span {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-item button {
|
||||||
|
background: #ef4444;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-item button:hover {
|
||||||
|
background: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-filters {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-top: 2rem;
|
||||||
|
padding-top: 2rem;
|
||||||
|
border-top: 1px solid #334155;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-filters button {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
background: #334155;
|
||||||
|
color: #e2e8f0;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-filters button.active {
|
||||||
|
background: #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mail Styles */
|
||||||
|
.mail-layout {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 250px 350px 1fr;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 1rem;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mail-sidebar, .mail-list, .mail-content {
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mail-item {
|
||||||
|
padding: 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
border-bottom: 1px solid #334155;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mail-item:hover {
|
||||||
|
background: #334155;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mail-item.unread {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mail-item.selected {
|
||||||
|
background: #1e40af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mail-content-view {
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mail-header {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
border-bottom: 1px solid #334155;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mail-body {
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Buttons */
|
||||||
|
button {
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Utility */
|
||||||
|
h1, h2, h3 {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-sm {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-xs {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-gray {
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
[x-cloak] {
|
||||||
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,195 +0,0 @@
|
||||||
.chat-container {
|
|
||||||
margin-top: 60px; /* Account for navbar height */
|
|
||||||
height: calc(100vh - 60px);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
#messages {
|
|
||||||
flex: 1;
|
|
||||||
overflow-y: auto;
|
|
||||||
padding: 20px 20px 140px;
|
|
||||||
max-width: 680px;
|
|
||||||
margin: 0 auto;
|
|
||||||
width: 100%;
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-footer {
|
|
||||||
position: fixed;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
background: var(--bg);
|
|
||||||
border-top: 1px solid var(--border);
|
|
||||||
padding: 12px;
|
|
||||||
z-index: 100;
|
|
||||||
transition: all 0.3s;
|
|
||||||
backdrop-filter: blur(20px);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Message styles */
|
|
||||||
.message-container {
|
|
||||||
margin-bottom: 24px;
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(10px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-message {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-message-content {
|
|
||||||
background: var(--fg);
|
|
||||||
color: var(--bg);
|
|
||||||
border-radius: 18px;
|
|
||||||
padding: 12px 18px;
|
|
||||||
max-width: 80%;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.5;
|
|
||||||
box-shadow: 0 2px 8px var(--shadow);
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-message-content::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
background: var(--gradient-2);
|
|
||||||
opacity: 0.3;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.assistant-message {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.assistant-avatar {
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: var(--logo-url) center/contain no-repeat;
|
|
||||||
flex-shrink: 0;
|
|
||||||
margin-top: 2px;
|
|
||||||
filter: var(--logo-filter, none);
|
|
||||||
}
|
|
||||||
|
|
||||||
.assistant-message-content {
|
|
||||||
flex: 1;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.7;
|
|
||||||
background: var(--glass);
|
|
||||||
border-radius: 18px;
|
|
||||||
padding: 12px 18px;
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
box-shadow: 0 2px 8px var(--shadow);
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.assistant-message-content::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
background: var(--gradient-1);
|
|
||||||
opacity: 0.5;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Input and suggestions */
|
|
||||||
.suggestions-container {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 4px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
justify-content: center;
|
|
||||||
max-width: 680px;
|
|
||||||
margin: 0 auto 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.suggestion-button {
|
|
||||||
padding: 6px 12px;
|
|
||||||
border-radius: 12px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 400;
|
|
||||||
transition: all 0.2s;
|
|
||||||
background: var(--glass);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
color: var(--fg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.suggestion-button:hover {
|
|
||||||
background: var(--fg);
|
|
||||||
color: var(--bg);
|
|
||||||
transform: scale(1.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-container {
|
|
||||||
display: flex;
|
|
||||||
gap: 6px;
|
|
||||||
max-width: 680px;
|
|
||||||
margin: 0 auto;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
#messageInput {
|
|
||||||
flex: 1;
|
|
||||||
border-radius: 20px;
|
|
||||||
padding: 10px 16px;
|
|
||||||
font-size: 14px;
|
|
||||||
font-family: "Inter", sans-serif;
|
|
||||||
outline: none;
|
|
||||||
transition: all 0.3s;
|
|
||||||
background: var(--glass);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
color: var(--fg);
|
|
||||||
backdrop-filter: blur(10px);
|
|
||||||
}
|
|
||||||
|
|
||||||
#messageInput:focus {
|
|
||||||
border-color: var(--accent);
|
|
||||||
box-shadow: 0 0 0 3px rgba(0,102,255,0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
#messageInput::placeholder {
|
|
||||||
opacity: 0.3;
|
|
||||||
}
|
|
||||||
|
|
||||||
#sendBtn, #voiceBtn {
|
|
||||||
width: 36px;
|
|
||||||
height: 36px;
|
|
||||||
border-radius: 18px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s;
|
|
||||||
border: none;
|
|
||||||
background: var(--fg);
|
|
||||||
color: var(--bg);
|
|
||||||
font-size: 16px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#voiceBtn.recording {
|
|
||||||
animation: pulse 1.5s infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes pulse {
|
|
||||||
0%, 100% { opacity: 1; transform: scale(1) }
|
|
||||||
50% { opacity: 0.6; transform: scale(1.1) }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive adjustments */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
#messages {
|
|
||||||
padding: 20px 16px 140px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,114 +0,0 @@
|
||||||
:root {
|
|
||||||
/* Main theme */
|
|
||||||
--background: #ffffff;
|
|
||||||
--foreground: #000000;
|
|
||||||
--card: #f8f9fa;
|
|
||||||
--popover: #ffffff;
|
|
||||||
--primary: #2563eb;
|
|
||||||
--secondary: #f1f5f9;
|
|
||||||
--muted: #64748b;
|
|
||||||
--accent: #f59e0b;
|
|
||||||
--destructive: #ef4444;
|
|
||||||
--border: #e2e8f0;
|
|
||||||
--input: #e2e8f0;
|
|
||||||
--ring: #93c5fd;
|
|
||||||
--radius: 0.5rem;
|
|
||||||
--chart-1: #3b82f6;
|
|
||||||
--chart-2: #10b981;
|
|
||||||
--chart-3: #f59e0b;
|
|
||||||
--chart-4: #ef4444;
|
|
||||||
--chart-5: #8b5cf6;
|
|
||||||
|
|
||||||
/* File manager theme */
|
|
||||||
--bg-primary: #1a1a2e;
|
|
||||||
--bg-secondary: #16213e;
|
|
||||||
--bg-tertiary: #0f3460;
|
|
||||||
--text-primary: #e94560;
|
|
||||||
--text-secondary: #00d9ff;
|
|
||||||
--filemanager-border: #533483;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar {
|
|
||||||
background: var(--background);
|
|
||||||
padding: 1rem;
|
|
||||||
border-bottom: 1px solid var(--border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-menu-btn {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-links {
|
|
||||||
display: flex;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-links a {
|
|
||||||
color: var(--foreground);
|
|
||||||
text-decoration: none;
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
border-radius: var(--radius);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-links a:hover {
|
|
||||||
background: var(--secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer {
|
|
||||||
background: var(--background);
|
|
||||||
padding: 1rem;
|
|
||||||
border-top: 1px solid var(--border);
|
|
||||||
display: flex;
|
|
||||||
gap: 2rem;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.shortcut-group {
|
|
||||||
display: flex;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.shortcut-btn {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
padding: 0.5rem;
|
|
||||||
background: var(--card);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: var(--radius);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.shortcut-btn .key {
|
|
||||||
font-weight: bold;
|
|
||||||
color: var(--primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.mobile-menu-btn {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-links {
|
|
||||||
display: none;
|
|
||||||
flex-direction: column;
|
|
||||||
position: absolute;
|
|
||||||
background: var(--background);
|
|
||||||
width: 100%;
|
|
||||||
left: 0;
|
|
||||||
padding: 1rem;
|
|
||||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-links.hidden {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-links:not(.hidden) {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.shortcut-group {
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
29
web/desktop/drive/drive.js
Normal file
29
web/desktop/drive/drive.js
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
function driveApp() {
|
||||||
|
return {
|
||||||
|
current: 'All Files',
|
||||||
|
search: '',
|
||||||
|
selectedFile: null,
|
||||||
|
navItems: [
|
||||||
|
{ name: 'All Files', icon: '📁' },
|
||||||
|
{ name: 'Recent', icon: '🕐' },
|
||||||
|
{ name: 'Starred', icon: '⭐' },
|
||||||
|
{ name: 'Shared', icon: '👥' },
|
||||||
|
{ name: 'Trash', icon: '🗑' }
|
||||||
|
],
|
||||||
|
files: [
|
||||||
|
{ id: 1, name: 'Project Proposal.pdf', type: 'PDF', icon: '📄', size: '2.4 MB', date: 'Nov 10, 2025' },
|
||||||
|
{ id: 2, name: 'Design Assets', type: 'Folder', icon: '📁', size: '—', date: 'Nov 12, 2025' },
|
||||||
|
{ id: 3, name: 'Meeting Notes.docx', type: 'Document', icon: '📝', size: '156 KB', date: 'Nov 14, 2025' },
|
||||||
|
{ id: 4, name: 'Budget 2025.xlsx', type: 'Spreadsheet', icon: '📊', size: '892 KB', date: 'Nov 13, 2025' },
|
||||||
|
{ id: 5, name: 'Presentation.pptx', type: 'Presentation', icon: '📽', size: '5.2 MB', date: 'Nov 11, 2025' },
|
||||||
|
{ id: 6, name: 'team-photo.jpg', type: 'Image', icon: '🖼', size: '3.1 MB', date: 'Nov 9, 2025' },
|
||||||
|
{ id: 7, name: 'source-code.zip', type: 'Archive', icon: '📦', size: '12.8 MB', date: 'Nov 8, 2025' },
|
||||||
|
{ id: 8, name: 'video-demo.mp4', type: 'Video', icon: '🎬', size: '45.2 MB', date: 'Nov 7, 2025' }
|
||||||
|
],
|
||||||
|
get filteredFiles() {
|
||||||
|
return this.files.filter(file =>
|
||||||
|
file.name.toLowerCase().includes(this.search.toLowerCase())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -1,585 +1,67 @@
|
||||||
<!DOCTYPE html>
|
<div class="drive-layout" x-data="driveApp()" x-cloak>
|
||||||
<html lang="en">
|
<div class="panel drive-sidebar">
|
||||||
<head>
|
<div style="padding: 1rem; border-bottom: 1px solid #334155;">
|
||||||
<meta charset="UTF-8">
|
<h3>General Bots Drive</h3>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>XTree Gold File Manager</title>
|
|
||||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
|
||||||
<script src="https://cdn.tailwindcss.com"></script>
|
|
||||||
<style>
|
|
||||||
[x-cloak] { display: none !important; }
|
|
||||||
|
|
||||||
/* XTree Gold inspired theme */
|
|
||||||
:root {
|
|
||||||
--bg-primary: #1a1a2e;
|
|
||||||
--bg-secondary: #16213e;
|
|
||||||
--bg-tertiary: #0f3460;
|
|
||||||
--text-primary: #e94560;
|
|
||||||
--text-secondary: #00d9ff;
|
|
||||||
--border: #533483;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: 'Courier New', monospace;
|
|
||||||
background: var(--bg-primary);
|
|
||||||
color: var(--text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel {
|
|
||||||
border: 2px solid var(--border);
|
|
||||||
background: var(--bg-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tree-line {
|
|
||||||
color: var(--border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.selected {
|
|
||||||
background: var(--bg-tertiary);
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.shortcut-key {
|
|
||||||
display: inline-block;
|
|
||||||
min-width: 2rem;
|
|
||||||
padding: 0.25rem 0.5rem;
|
|
||||||
background: var(--bg-tertiary);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
text-align: center;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-icon {
|
|
||||||
display: inline-block;
|
|
||||||
width: 1.5rem;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.folder-icon::before { content: '📁'; }
|
|
||||||
.file-icon.pdf::before { content: '📄'; }
|
|
||||||
.file-icon.xlsx::before { content: '📊'; }
|
|
||||||
.file-icon.json::before { content: '{}'; }
|
|
||||||
.file-icon.md::before { content: '📝'; }
|
|
||||||
.file-icon.jpg::before, .file-icon.jpeg::before, .file-icon.png::before { content: '🖼️'; }
|
|
||||||
.file-icon.mp4::before { content: '🎬'; }
|
|
||||||
.file-icon.mp3::before { content: '🎵'; }
|
|
||||||
.file-icon.default::before { content: '📋'; }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body class="h-screen flex flex-col overflow-hidden" x-data="fileManager()" x-cloak>
|
|
||||||
<!-- Main Container -->
|
|
||||||
<div class="flex-1 flex overflow-hidden">
|
|
||||||
<!-- Left Sidebar - Folder Tree -->
|
|
||||||
<div class="panel w-64 flex flex-col overflow-hidden" :class="{ 'w-16': collapsed }">
|
|
||||||
<!-- Navigation Links -->
|
|
||||||
<div class="p-2 space-y-1">
|
|
||||||
<template x-for="link in navLinks" :key="link.path">
|
|
||||||
<button
|
|
||||||
@click="selectPath(link.path)"
|
|
||||||
:class="currentPath === link.path ? 'selected' : ''"
|
|
||||||
class="w-full px-3 py-2 text-left hover:bg-gray-700 rounded flex items-center gap-2"
|
|
||||||
>
|
|
||||||
<span x-text="link.icon" class="text-xl"></span>
|
|
||||||
<span x-show="!collapsed" x-text="link.title"></span>
|
|
||||||
</button>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="border-t border-gray-600 my-2"></div>
|
|
||||||
|
|
||||||
<!-- Folder Tree -->
|
|
||||||
<div class="flex-1 overflow-auto p-2" x-show="!collapsed">
|
|
||||||
<template x-for="item in rootFolders" :key="item.id">
|
|
||||||
<div>
|
|
||||||
<button
|
|
||||||
@click="toggleFolder(item.path); selectPath(item.path)"
|
|
||||||
:class="currentPath === item.path ? 'selected' : ''"
|
|
||||||
class="w-full px-2 py-1 text-left hover:bg-gray-700 rounded flex items-center gap-1 text-sm"
|
|
||||||
>
|
|
||||||
<span x-show="item.is_dir" x-text="expanded[item.path] ? '▼' : '▶'" class="w-4"></span>
|
|
||||||
<span class="folder-icon"></span>
|
|
||||||
<span x-text="item.name"></span>
|
|
||||||
<span x-show="item.starred" class="ml-auto text-yellow-400">★</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div x-show="expanded[item.path]" class="ml-4">
|
|
||||||
<template x-for="child in getChildren(item.path)" :key="child.id">
|
|
||||||
<button
|
|
||||||
@click="selectPath(child.path)"
|
|
||||||
:class="currentPath === child.path ? 'selected' : ''"
|
|
||||||
class="w-full px-2 py-1 text-left hover:bg-gray-700 rounded flex items-center gap-1 text-sm"
|
|
||||||
>
|
|
||||||
<span :class="child.is_dir ? 'folder-icon' : 'file-icon ' + (child.type || 'default')"></span>
|
|
||||||
<span x-text="child.name"></span>
|
|
||||||
</button>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Collapse Toggle -->
|
|
||||||
<button
|
|
||||||
@click="collapsed = !collapsed"
|
|
||||||
class="p-2 border-t border-gray-600 hover:bg-gray-700 text-center"
|
|
||||||
>
|
|
||||||
<span x-text="collapsed ? '▶' : '◀'"></span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Middle Panel - File List -->
|
|
||||||
<div class="panel flex-1 flex flex-col overflow-hidden mx-2">
|
|
||||||
<!-- Header -->
|
|
||||||
<div class="p-4 border-b border-gray-600">
|
|
||||||
<h1 class="text-2xl font-bold mb-2" x-text="currentItem?.name || 'My Drive'"></h1>
|
|
||||||
|
|
||||||
<!-- Search and Filter -->
|
|
||||||
<div class="flex gap-2">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
x-model="searchTerm"
|
|
||||||
placeholder="Search files (Ctrl+F)"
|
|
||||||
class="flex-1 px-3 py-2 bg-gray-800 border border-gray-600 rounded focus:outline-none focus:border-blue-500"
|
|
||||||
/>
|
|
||||||
<select
|
|
||||||
x-model="filterType"
|
|
||||||
class="px-3 py-2 bg-gray-800 border border-gray-600 rounded focus:outline-none"
|
|
||||||
>
|
|
||||||
<option value="all">All items</option>
|
|
||||||
<option value="folders">Folders</option>
|
|
||||||
<option value="files">Files</option>
|
|
||||||
<option value="starred">Starred</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- File List -->
|
|
||||||
<div class="flex-1 overflow-auto p-2">
|
|
||||||
<template x-for="file in filteredFiles" :key="file.id">
|
|
||||||
<button
|
|
||||||
@click="selectFile(file)"
|
|
||||||
@dblclick="openFile(file)"
|
|
||||||
@contextmenu.prevent="showContextMenu($event, file)"
|
|
||||||
:class="selectedFile?.id === file.id ? 'selected' : ''"
|
|
||||||
class="w-full p-3 text-left hover:bg-gray-700 rounded border-b border-gray-700 flex items-center gap-3"
|
|
||||||
>
|
|
||||||
<span :class="file.is_dir ? 'folder-icon' : 'file-icon ' + (file.type || 'default')"></span>
|
|
||||||
<div class="flex-1">
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<span x-text="file.name" class="font-semibold"></span>
|
|
||||||
<span x-show="file.starred" class="text-yellow-400 text-sm">★</span>
|
|
||||||
<span x-show="file.shared" class="text-blue-400 text-sm">👥</span>
|
|
||||||
</div>
|
|
||||||
<div class="text-xs text-gray-400">
|
|
||||||
<span x-text="file.is_dir ? 'Folder' : formatFileSize(file.size)"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="text-xs text-gray-400" x-text="formatDate(file.modified)"></div>
|
|
||||||
</button>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<div x-show="filteredFiles.length === 0" class="text-center text-gray-500 py-8">
|
|
||||||
No files found
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Right Panel - File Details -->
|
|
||||||
<div class="panel w-80 flex flex-col overflow-hidden">
|
|
||||||
<div class="p-2 border-b border-gray-600 flex gap-2">
|
|
||||||
<button @click="downloadFile()" :disabled="!selectedFile" class="px-3 py-1 bg-blue-600 hover:bg-blue-700 rounded disabled:opacity-50">
|
|
||||||
⬇ Download
|
|
||||||
</button>
|
|
||||||
<button @click="shareFile()" :disabled="!selectedFile" class="px-3 py-1 bg-green-600 hover:bg-green-700 rounded disabled:opacity-50">
|
|
||||||
🔗 Share
|
|
||||||
</button>
|
|
||||||
<button @click="toggleStar()" :disabled="!selectedFile" class="px-3 py-1 bg-yellow-600 hover:bg-yellow-700 rounded disabled:opacity-50">
|
|
||||||
★ Star
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex-1 overflow-auto p-4">
|
|
||||||
<template x-if="selectedFile">
|
|
||||||
<div>
|
|
||||||
<div class="flex items-start gap-4 mb-4">
|
|
||||||
<div class="p-3 bg-gray-700 rounded">
|
|
||||||
<span :class="selectedFile.is_dir ? 'folder-icon' : 'file-icon ' + (selectedFile.type || 'default')" class="text-3xl"></span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 class="font-bold text-lg" x-text="selectedFile.name"></h3>
|
|
||||||
<p class="text-sm text-gray-400" x-text="selectedFile.is_dir ? 'Folder' : (selectedFile.type?.toUpperCase() || 'File') + ' • ' + formatFileSize(selectedFile.size)"></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="space-y-3 text-sm">
|
|
||||||
<div>
|
|
||||||
<div class="font-semibold mb-1">Location</div>
|
|
||||||
<div class="text-gray-400" x-text="'/' + (selectedFile.path || '')"></div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div class="font-semibold mb-1">Modified</div>
|
|
||||||
<div class="text-gray-400" x-text="formatDateTime(selectedFile.modified)"></div>
|
|
||||||
</div>
|
|
||||||
<div x-show="!selectedFile.is_dir">
|
|
||||||
<div class="font-semibold mb-1">Size</div>
|
|
||||||
<div class="text-gray-400" x-text="formatFileSize(selectedFile.size)"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template x-if="!selectedFile">
|
|
||||||
<div class="text-center text-gray-500 py-8">
|
|
||||||
<div class="text-4xl mb-4">📄</div>
|
|
||||||
<div class="text-lg font-semibold">No file selected</div>
|
|
||||||
<div class="text-sm">Select a file to view details</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Footer - Status Bar with Keyboard Shortcuts -->
|
|
||||||
<div class="panel p-2 border-t-2 border-gray-600">
|
|
||||||
<div class="grid grid-cols-2 gap-2 text-xs">
|
|
||||||
<!-- Row 1 -->
|
|
||||||
<div class="flex flex-wrap gap-1">
|
|
||||||
<template x-for="shortcut in shortcuts[0]" :key="shortcut.key">
|
|
||||||
<button
|
|
||||||
@click="shortcut.action()"
|
|
||||||
class="shortcut-key hover:bg-gray-600"
|
|
||||||
:title="'Ctrl+' + shortcut.key"
|
|
||||||
>
|
|
||||||
<span x-text="shortcut.key"></span>
|
|
||||||
<span class="text-xs ml-1" x-text="shortcut.label"></span>
|
|
||||||
</button>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Row 2 -->
|
|
||||||
<div class="flex flex-wrap gap-1">
|
|
||||||
<template x-for="shortcut in shortcuts[1]" :key="shortcut.key">
|
|
||||||
<button
|
|
||||||
@click="shortcut.action()"
|
|
||||||
class="shortcut-key hover:bg-gray-600"
|
|
||||||
:title="'Ctrl+' + shortcut.key"
|
|
||||||
>
|
|
||||||
<span x-text="shortcut.key"></span>
|
|
||||||
<span class="text-xs ml-1" x-text="shortcut.label"></span>
|
|
||||||
</button>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Context Menu -->
|
|
||||||
<div
|
|
||||||
x-show="contextMenu.show"
|
|
||||||
@click.away="contextMenu.show = false"
|
|
||||||
:style="`top: ${contextMenu.y}px; left: ${contextMenu.x}px`"
|
|
||||||
class="fixed bg-gray-800 border border-gray-600 rounded shadow-lg z-50 py-1 min-w-48"
|
|
||||||
>
|
|
||||||
<template x-for="item in contextMenuItems" :key="item.label">
|
|
||||||
<button
|
|
||||||
@click="handleContextAction(item.action)"
|
|
||||||
class="w-full px-4 py-2 text-left hover:bg-gray-700 flex items-center gap-2"
|
|
||||||
>
|
|
||||||
<span x-text="item.icon"></span>
|
|
||||||
<span x-text="item.label"></span>
|
|
||||||
</button>
|
|
||||||
</template>
|
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
<script>
|
<div class="panel drive-main">
|
||||||
function fileManager() {
|
<div style="padding: 1rem; border-bottom: 1px solid #334155;">
|
||||||
return {
|
<h2 x-text="current"></h2>
|
||||||
collapsed: false,
|
<input type="text" x-model="search" placeholder="Search files..."
|
||||||
currentPath: '',
|
style="width: 100%; margin-top: 0.5rem; padding: 0.5rem; background: #0f172a; border: 1px solid #334155; border-radius: 0.375rem; color: #e2e8f0;">
|
||||||
searchTerm: '',
|
</div>
|
||||||
filterType: 'all',
|
<div class="file-list">
|
||||||
selectedFile: null,
|
<template x-for="file in filteredFiles" :key="file.id">
|
||||||
expanded: { '': true, 'projects': true },
|
<div class="file-item"
|
||||||
contextMenu: { show: false, x: 0, y: 0, file: null },
|
:class="{ selected: selectedFile?.id === file.id }"
|
||||||
|
@click="selectedFile = file">
|
||||||
navLinks: [
|
<span class="file-icon" x-text="file.icon"></span>
|
||||||
{ title: 'My Drive', path: '', icon: '🏠' },
|
<div style="flex: 1;">
|
||||||
{ title: 'Shared', path: 'shared', icon: '👥' },
|
<div style="font-weight: 600;" x-text="file.name"></div>
|
||||||
{ title: 'Starred', path: 'starred', icon: '⭐' },
|
<div class="text-xs text-gray" x-text="file.date"></div>
|
||||||
{ title: 'Recent', path: 'recent', icon: '🕐' },
|
</div>
|
||||||
{ title: 'Trash', path: 'trash', icon: '🗑️' },
|
<div class="text-sm text-gray" x-text="file.size"></div>
|
||||||
],
|
</div>
|
||||||
|
</template>
|
||||||
fileSystem: {
|
</div>
|
||||||
"": {
|
</div>
|
||||||
id: "root", name: "My Drive", path: "", is_dir: true,
|
|
||||||
children: ["projects", "documents", "media", "shared"]
|
<div class="panel drive-details">
|
||||||
},
|
<template x-if="selectedFile">
|
||||||
"projects": {
|
<div style="padding: 2rem;">
|
||||||
id: "projects", name: "Projects", path: "projects", is_dir: true,
|
<div style="text-align: center; margin-bottom: 2rem;">
|
||||||
modified: "2025-01-15T10:30:00Z", starred: true, shared: false,
|
<div style="font-size: 4rem; margin-bottom: 1rem;" x-text="selectedFile.icon"></div>
|
||||||
children: ["web-apps", "mobile-apps", "ai-research"]
|
<h3 x-text="selectedFile.name"></h3>
|
||||||
},
|
<p class="text-sm text-gray" x-text="selectedFile.type"></p>
|
||||||
"projects/web-apps": {
|
</div>
|
||||||
id: "web-apps", name: "Web Applications", path: "projects/web-apps", is_dir: true,
|
<div style="margin-bottom: 1rem;">
|
||||||
modified: "2025-01-14T16:45:00Z", starred: false, shared: true,
|
<div class="text-sm" style="margin-bottom: 0.5rem;">Size</div>
|
||||||
children: ["package.json", "README.md"]
|
<div class="text-gray" x-text="selectedFile.size"></div>
|
||||||
},
|
</div>
|
||||||
"projects/web-apps/package.json": {
|
<div style="margin-bottom: 1rem;">
|
||||||
id: "package-json", name: "package.json", path: "projects/web-apps/package.json",
|
<div class="text-sm" style="margin-bottom: 0.5rem;">Modified</div>
|
||||||
is_dir: false, size: 2048, type: "json", modified: "2025-01-13T14:20:00Z"
|
<div class="text-gray" x-text="selectedFile.date"></div>
|
||||||
},
|
</div>
|
||||||
"projects/web-apps/README.md": {
|
<div style="display: flex; gap: 0.5rem; margin-top: 2rem;">
|
||||||
id: "readme-md", name: "README.md", path: "projects/web-apps/README.md",
|
<button style="flex: 1; padding: 0.75rem; background: #3b82f6; color: white; border: none; border-radius: 0.375rem; cursor: pointer;">Download</button>
|
||||||
is_dir: false, size: 5120, type: "md", modified: "2025-01-12T09:30:00Z", shared: true
|
<button style="flex: 1; padding: 0.75rem; background: #10b981; color: white; border: none; border-radius: 0.375rem; cursor: pointer;">Share</button>
|
||||||
},
|
</div>
|
||||||
"documents": {
|
</div>
|
||||||
id: "documents", name: "Documents", path: "documents", is_dir: true,
|
</template>
|
||||||
modified: "2025-01-14T12:00:00Z",
|
<template x-if="!selectedFile">
|
||||||
children: ["Q1-Strategy.pdf", "Budget-2025.xlsx"]
|
<div style="padding: 2rem; text-align: center; color: #64748b;">
|
||||||
},
|
<div style="font-size: 4rem; margin-bottom: 1rem;">📄</div>
|
||||||
"documents/Q1-Strategy.pdf": {
|
<p>Select a file to view details</p>
|
||||||
id: "q1-strategy", name: "Q1 Strategy.pdf", path: "documents/Q1-Strategy.pdf",
|
</div>
|
||||||
is_dir: false, size: 1048576, type: "pdf", modified: "2025-01-10T15:30:00Z", starred: true, shared: true
|
</template>
|
||||||
},
|
</div>
|
||||||
"documents/Budget-2025.xlsx": {
|
</div>
|
||||||
id: "budget-xlsx", name: "Budget-2025.xlsx", path: "documents/Budget-2025.xlsx",
|
|
||||||
is_dir: false, size: 524288, type: "xlsx", modified: "2025-01-09T11:00:00Z"
|
|
||||||
},
|
|
||||||
"media": {
|
|
||||||
id: "media", name: "Media", path: "media", is_dir: true,
|
|
||||||
modified: "2025-01-13T18:45:00Z",
|
|
||||||
children: ["vacation-2024.jpg"]
|
|
||||||
},
|
|
||||||
"media/vacation-2024.jpg": {
|
|
||||||
id: "vacation-photo", name: "vacation-2024.jpg", path: "media/vacation-2024.jpg",
|
|
||||||
is_dir: false, size: 3145728, type: "jpg", modified: "2024-12-25T20:00:00Z", starred: true
|
|
||||||
},
|
|
||||||
"shared": {
|
|
||||||
id: "shared", name: "Shared", path: "shared", is_dir: true,
|
|
||||||
modified: "2025-01-12T11:20:00Z", shared: true,
|
|
||||||
children: []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
shortcuts: [
|
|
||||||
[
|
|
||||||
{ key: 'Q', label: 'Rename', action: () => this.renameFile() },
|
|
||||||
{ key: 'W', label: 'View', action: () => this.viewFile() },
|
|
||||||
{ key: 'E', label: 'Edit', action: () => this.editFile() },
|
|
||||||
{ key: 'R', label: 'Move', action: () => this.moveFile() },
|
|
||||||
{ key: 'T', label: 'MkDir', action: () => this.makeDirectory() },
|
|
||||||
{ key: 'Y', label: 'Delete', action: () => this.deleteFile() },
|
|
||||||
{ key: 'U', label: 'Copy', action: () => this.copyFile() },
|
|
||||||
{ key: 'I', label: 'Cut', action: () => this.cutFile() },
|
|
||||||
{ key: 'O', label: 'Paste', action: () => this.pasteFile() },
|
|
||||||
{ key: 'P', label: 'Duplicate', action: () => this.duplicateFile() },
|
|
||||||
],
|
|
||||||
[
|
|
||||||
{ key: 'A', label: 'Select', action: () => this.toggleSelect() },
|
|
||||||
{ key: 'S', label: 'Select All', action: () => this.selectAll() },
|
|
||||||
{ key: 'D', label: 'Deselect', action: () => this.deselectAll() },
|
|
||||||
{ key: 'G', label: 'Details', action: () => this.showDetails() },
|
|
||||||
{ key: 'H', label: 'History', action: () => this.showHistory() },
|
|
||||||
{ key: 'J', label: 'Share', action: () => this.shareFile() },
|
|
||||||
{ key: 'K', label: 'Star', action: () => this.toggleStar() },
|
|
||||||
{ key: 'L', label: 'Download', action: () => this.downloadFile() },
|
|
||||||
{ key: 'Z', label: 'Upload', action: () => this.uploadFile() },
|
|
||||||
{ key: 'X', label: 'Refresh', action: () => this.refresh() },
|
|
||||||
]
|
|
||||||
],
|
|
||||||
|
|
||||||
contextMenuItems: [
|
|
||||||
{ icon: '👁️', label: 'Open', action: 'open' },
|
|
||||||
{ icon: '⬇', label: 'Download', action: 'download' },
|
|
||||||
{ icon: '🔗', label: 'Share', action: 'share' },
|
|
||||||
{ icon: '⭐', label: 'Star/Unstar', action: 'star' },
|
|
||||||
{ icon: '📋', label: 'Copy', action: 'copy' },
|
|
||||||
{ icon: '✂️', label: 'Cut', action: 'cut' },
|
|
||||||
{ icon: '✏️', label: 'Rename', action: 'rename' },
|
|
||||||
{ icon: '🗑️', label: 'Delete', action: 'delete' },
|
|
||||||
],
|
|
||||||
|
|
||||||
get currentItem() {
|
|
||||||
return this.fileSystem[this.currentPath];
|
|
||||||
},
|
|
||||||
|
|
||||||
get rootFolders() {
|
|
||||||
const root = this.fileSystem[''];
|
|
||||||
if (!root || !root.children) return [];
|
|
||||||
return root.children.map(name => this.fileSystem[name]).filter(Boolean);
|
|
||||||
},
|
|
||||||
|
|
||||||
get filteredFiles() {
|
|
||||||
const current = this.currentItem;
|
|
||||||
if (!current || !current.is_dir || !current.children) return [];
|
|
||||||
|
|
||||||
let files = current.children
|
|
||||||
.map(childName => {
|
|
||||||
const path = this.currentPath ? `${this.currentPath}/${childName}` : childName;
|
|
||||||
return this.fileSystem[path];
|
|
||||||
})
|
|
||||||
.filter(Boolean);
|
|
||||||
|
|
||||||
if (this.searchTerm) {
|
|
||||||
files = files.filter(f =>
|
|
||||||
f.name.toLowerCase().includes(this.searchTerm.toLowerCase())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.filterType !== 'all') {
|
|
||||||
if (this.filterType === 'folders') files = files.filter(f => f.is_dir);
|
|
||||||
else if (this.filterType === 'files') files = files.filter(f => !f.is_dir);
|
|
||||||
else if (this.filterType === 'starred') files = files.filter(f => f.starred);
|
|
||||||
}
|
|
||||||
|
|
||||||
return files.sort((a, b) => {
|
|
||||||
if (a.is_dir && !b.is_dir) return -1;
|
|
||||||
if (!a.is_dir && b.is_dir) return 1;
|
|
||||||
return a.name.localeCompare(b.name);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
getChildren(path) {
|
|
||||||
const item = this.fileSystem[path];
|
|
||||||
if (!item || !item.children) return [];
|
|
||||||
return item.children.map(name => {
|
|
||||||
const childPath = path ? `${path}/${name}` : name;
|
|
||||||
return this.fileSystem[childPath];
|
|
||||||
}).filter(Boolean);
|
|
||||||
},
|
|
||||||
|
|
||||||
selectPath(path) {
|
|
||||||
this.currentPath = path;
|
|
||||||
this.selectedFile = null;
|
|
||||||
},
|
|
||||||
|
|
||||||
selectFile(file) {
|
|
||||||
this.selectedFile = file;
|
|
||||||
},
|
|
||||||
|
|
||||||
openFile(file) {
|
|
||||||
if (file.is_dir) {
|
|
||||||
this.currentPath = file.path;
|
|
||||||
this.expanded[file.path] = true;
|
|
||||||
} else {
|
|
||||||
console.log('Opening file:', file.name);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
toggleFolder(path) {
|
|
||||||
this.expanded[path] = !this.expanded[path];
|
|
||||||
},
|
|
||||||
|
|
||||||
showContextMenu(event, file) {
|
|
||||||
this.contextMenu = {
|
|
||||||
show: true,
|
|
||||||
x: event.clientX,
|
|
||||||
y: event.clientY,
|
|
||||||
file: file
|
|
||||||
};
|
|
||||||
this.selectedFile = file;
|
|
||||||
},
|
|
||||||
|
|
||||||
handleContextAction(action) {
|
|
||||||
console.log('Action:', action, 'File:', this.contextMenu.file);
|
|
||||||
this.contextMenu.show = false;
|
|
||||||
|
|
||||||
switch(action) {
|
|
||||||
case 'open': this.openFile(this.contextMenu.file); break;
|
|
||||||
case 'download': this.downloadFile(); break;
|
|
||||||
case 'share': this.shareFile(); break;
|
|
||||||
case 'star': this.toggleStar(); break;
|
|
||||||
case 'copy': this.copyFile(); break;
|
|
||||||
case 'cut': this.cutFile(); break;
|
|
||||||
case 'rename': this.renameFile(); break;
|
|
||||||
case 'delete': this.deleteFile(); break;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
formatFileSize(bytes) {
|
|
||||||
if (!bytes) return '';
|
|
||||||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
||||||
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
||||||
return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${sizes[i]}`;
|
|
||||||
},
|
|
||||||
|
|
||||||
formatDate(dateString) {
|
|
||||||
if (!dateString) return '';
|
|
||||||
const date = new Date(dateString);
|
|
||||||
const now = new Date();
|
|
||||||
const diff = now - date;
|
|
||||||
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
|
|
||||||
|
|
||||||
if (days === 0) return 'Today';
|
|
||||||
if (days === 1) return 'Yesterday';
|
|
||||||
if (days < 7) return `${days}d ago`;
|
|
||||||
return date.toLocaleDateString();
|
|
||||||
},
|
|
||||||
|
|
||||||
formatDateTime(dateString) {
|
|
||||||
if (!dateString) return '';
|
|
||||||
return new Date(dateString).toLocaleString();
|
|
||||||
},
|
|
||||||
|
|
||||||
// Action methods
|
|
||||||
renameFile() { console.log('Rename:', this.selectedFile?.name); },
|
|
||||||
viewFile() { console.log('View:', this.selectedFile?.name); },
|
|
||||||
editFile() { console.log('Edit:', this.selectedFile?.name); },
|
|
||||||
moveFile() { console.log('Move:', this.selectedFile?.name); },
|
|
||||||
makeDirectory() { console.log('Make Directory'); },
|
|
||||||
deleteFile() { console.log('Delete:', this.selectedFile?.name); },
|
|
||||||
copyFile() { console.log('Copy:', this.selectedFile?.name); },
|
|
||||||
cutFile() { console.log('Cut:', this.selectedFile?.name); },
|
|
||||||
pasteFile() { console.log('Paste'); },
|
|
||||||
duplicateFile() { console.log('Duplicate:', this.selectedFile?.name); },
|
|
||||||
toggleSelect() { console.log('Toggle Select'); },
|
|
||||||
selectAll() { console.log('Select All'); },
|
|
||||||
deselectAll() { this.selectedFile = null; },
|
|
||||||
showDetails() { console.log('Show Details'); },
|
|
||||||
showHistory() { console.log('Show History'); },
|
|
||||||
shareFile() { console.log('Share:', this.selectedFile?.name); },
|
|
||||||
toggleStar() {
|
|
||||||
if (this.selectedFile) {
|
|
||||||
this.selectedFile.starred = !this.selectedFile.starred;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
downloadFile() { console.log('Download:', this.selectedFile?.name); },
|
|
||||||
uploadFile() { console.log('Upload'); },
|
|
||||||
refresh() { console.log('Refresh'); },
|
|
||||||
|
|
||||||
init() {
|
|
||||||
// Keyboard shortcuts
|
|
||||||
document.addEventListener('keydown', (e) => {
|
|
||||||
if (e.ctrlKey || e.metaKey) {
|
|
||||||
const key = e.key.toUpperCase();
|
|
||||||
|
|
||||||
// Find and execute shortcut
|
|
||||||
for (const row of this.shortcuts) {
|
|
||||||
const shortcut = row.find(s => s.key === key);
|
|
||||||
if (shortcut) {
|
|
||||||
e.preventDefault();
|
|
||||||
shortcut.action();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Special shortcuts
|
|
||||||
if (key === 'F') {
|
|
||||||
e.preventDefault();
|
|
||||||
document.querySelector('input[placeholder*="Search"]')?.focus();
|
|
||||||
}
|
|
||||||
} else if (e.key === 'Delete' && this.selectedFile) {
|
|
||||||
e.preventDefault();
|
|
||||||
this.deleteFile();
|
|
||||||
} else if (e.key === 'F2' && this.selectedFile) {
|
|
||||||
e.preventDefault();
|
|
||||||
this.renameFile();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
|
||||||
|
|
@ -1,35 +1,31 @@
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="pt-br">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8"/>
|
<meta charset="utf-8" />
|
||||||
<title>General Bots</title>
|
<title>General Bots Desktop</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" type="text/css">
|
<link rel="stylesheet" href="css/app.css" />
|
||||||
<script src="js/lib/gsap.min.js"></script>
|
<script defer src="js/alpine.js"></script>
|
||||||
<script src="js/lib/marked.min.js"></script>
|
|
||||||
<script src="js/mock-data.js"></script>
|
|
||||||
<script src="js/auth.js"></script>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="navbar-container"></div>
|
<nav x-data="{ current: 'drive' }">
|
||||||
|
<div class="logo">⚡ General Bots</div>
|
||||||
|
<a href="#drive" @click.prevent="current = 'drive'; window.switchSection('drive')"
|
||||||
|
:class="{ active: current === 'drive' }">📁 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>
|
||||||
|
|
||||||
<!-- Main chat content -->
|
<div id="main-content">
|
||||||
<div class="chat-container">
|
<!-- Sections will be loaded dynamically -->
|
||||||
<div class="connection-status connecting" id="connectionStatus"></div>
|
</div>
|
||||||
<div class="flash-overlay" id="flashOverlay"></div>
|
|
||||||
|
|
||||||
<main id="messages"></main>
|
|
||||||
|
|
||||||
<footer class="chat-footer">
|
|
||||||
<div class="suggestions-container" id="suggestions"></div>
|
|
||||||
<div class="input-container">
|
|
||||||
<input id="messageInput" type="text" placeholder="Message..." autofocus/>
|
|
||||||
<button id="voiceBtn" title="Voice">🎤</button>
|
|
||||||
<button id="sendBtn" title="Send">↑</button>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="js/layout.js"></script>
|
<!-- Load Module Scripts -->
|
||||||
|
<script src="js/layout.js"></script>
|
||||||
|
<script src="drive/drive.js"></script>
|
||||||
|
<script src="tasks/tasks.js"></script>
|
||||||
|
<script src="mail/mail.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -1,61 +0,0 @@
|
||||||
// Handle authentication state
|
|
||||||
let currentUser = null;
|
|
||||||
let currentSession = null;
|
|
||||||
|
|
||||||
// Initialize auth with mock data
|
|
||||||
function initializeAuth() {
|
|
||||||
if (window.location.pathname.includes('auth')) {
|
|
||||||
return; // Don't initialize on auth pages
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for existing session
|
|
||||||
const sessionId = localStorage.getItem('sessionId');
|
|
||||||
if (sessionId) {
|
|
||||||
currentSession = mockSessions.find(s => s.id === sessionId) || mockSessions[0];
|
|
||||||
} else {
|
|
||||||
currentSession = mockSessions[0];
|
|
||||||
localStorage.setItem('sessionId', currentSession.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set current user
|
|
||||||
currentUser = mockUsers[0];
|
|
||||||
updateUserUI();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update UI based on auth state
|
|
||||||
function updateUserUI() {
|
|
||||||
const userAvatar = document.getElementById('userAvatar');
|
|
||||||
if (userAvatar && currentUser) {
|
|
||||||
userAvatar.textContent = currentUser.avatar;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle login
|
|
||||||
function handleLogin(email, password) {
|
|
||||||
// In a real app, this would call an API
|
|
||||||
currentUser = mockUsers.find(u => u.email === email) || mockUsers[0];
|
|
||||||
currentSession = mockSessions[0];
|
|
||||||
localStorage.setItem('sessionId', currentSession.id);
|
|
||||||
updateUserUI();
|
|
||||||
window.location.href = '/desktop/index.html';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle logout
|
|
||||||
function handleLogout() {
|
|
||||||
localStorage.removeItem('sessionId');
|
|
||||||
window.location.href = '/desktop/auth/login.html';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check auth state for protected routes
|
|
||||||
function checkAuth() {
|
|
||||||
if (!currentUser && !window.location.pathname.includes('auth')) {
|
|
||||||
window.location.href = '/desktop/auth/login.html';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize on page load
|
|
||||||
if (document.readyState === 'complete') {
|
|
||||||
initializeAuth();
|
|
||||||
} else {
|
|
||||||
window.addEventListener('load', initializeAuth);
|
|
||||||
}
|
|
||||||
|
|
@ -1,56 +1,37 @@
|
||||||
class Layout {
|
const sections = {
|
||||||
static currentPage = 'chat';
|
drive: 'drive/index.html',
|
||||||
|
tasks: 'tasks/index.html',
|
||||||
|
mail: 'mail/index.html'
|
||||||
|
};
|
||||||
|
|
||||||
|
async function loadSectionHTML(path) {
|
||||||
|
const response = await fetch(path);
|
||||||
|
if (!response.ok) throw new Error('Failed to load section');
|
||||||
|
return await response.text();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function switchSection(section) {
|
||||||
|
const mainContent = document.getElementById('main-content');
|
||||||
|
|
||||||
static init() {
|
try {
|
||||||
this.setCurrentPage();
|
const html = await loadSectionHTML(sections[section]);
|
||||||
this.loadNavbar();
|
mainContent.innerHTML = html;
|
||||||
this.setupNavigation();
|
window.history.pushState({}, '', `#${section}`);
|
||||||
}
|
Alpine.initTree(mainContent);
|
||||||
|
} catch (err) {
|
||||||
static setCurrentPage() {
|
console.error('Error loading section:', err);
|
||||||
const hash = window.location.hash.substring(1) || 'chat';
|
mainContent.innerHTML = `<div class="error">Failed to load ${section} section</div>`;
|
||||||
this.currentPage = hash;
|
|
||||||
this.updateContent();
|
|
||||||
}
|
|
||||||
|
|
||||||
static async loadNavbar() {
|
|
||||||
try {
|
|
||||||
const response = await fetch('shared/navbar.html');
|
|
||||||
const html = await response.text();
|
|
||||||
|
|
||||||
if (!document.querySelector('.navbar')) {
|
|
||||||
document.body.insertAdjacentHTML('afterbegin', html);
|
|
||||||
}
|
|
||||||
|
|
||||||
document.querySelectorAll('.nav-link').forEach(link => {
|
|
||||||
link.classList.toggle('active', link.dataset.target === this.currentPage);
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to load navbar:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static updateContent() {
|
|
||||||
// Add your content loading logic here
|
|
||||||
// For example: fetch(`pages/${this.currentPage}.html`)
|
|
||||||
// and update the main content area
|
|
||||||
}
|
|
||||||
|
|
||||||
static setupNavigation() {
|
|
||||||
document.addEventListener('click', (e) => {
|
|
||||||
const navLink = e.target.closest('.nav-link');
|
|
||||||
if (navLink) {
|
|
||||||
e.preventDefault();
|
|
||||||
const target = navLink.dataset.target;
|
|
||||||
window.location.hash = target;
|
|
||||||
this.currentPage = target;
|
|
||||||
this.loadNavbar();
|
|
||||||
this.updateContent();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize on load and also on navigation
|
// Handle initial load based on URL hash
|
||||||
Layout.init();
|
window.addEventListener('DOMContentLoaded', () => {
|
||||||
window.addEventListener('popstate', () => Layout.init());
|
const initialSection = window.location.hash.substring(1) || 'drive';
|
||||||
|
switchSection(initialSection);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle browser back/forward navigation
|
||||||
|
window.addEventListener('popstate', () => {
|
||||||
|
const section = window.location.hash.substring(1) || 'drive';
|
||||||
|
switchSection(section);
|
||||||
|
});
|
||||||
|
|
|
||||||
11
web/desktop/js/lib/gsap.min.js
vendored
11
web/desktop/js/lib/gsap.min.js
vendored
File diff suppressed because one or more lines are too long
8
web/desktop/js/lib/livekit-client.min.js
vendored
8
web/desktop/js/lib/livekit-client.min.js
vendored
File diff suppressed because one or more lines are too long
69
web/desktop/js/lib/marked.min.js
vendored
69
web/desktop/js/lib/marked.min.js
vendored
File diff suppressed because one or more lines are too long
|
|
@ -1,46 +0,0 @@
|
||||||
const mockUsers = [
|
|
||||||
{
|
|
||||||
id: 'user1',
|
|
||||||
name: 'John Doe',
|
|
||||||
email: 'john@example.com',
|
|
||||||
avatar: '👨'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'user2',
|
|
||||||
name: 'Jane Smith',
|
|
||||||
email: 'jane@example.com',
|
|
||||||
avatar: '👩'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const mockBots = [
|
|
||||||
{
|
|
||||||
id: 'default_bot',
|
|
||||||
name: 'General Bot',
|
|
||||||
description: 'Main assistant bot'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const mockSessions = [
|
|
||||||
{
|
|
||||||
id: 'session1',
|
|
||||||
title: 'First Chat',
|
|
||||||
created_at: new Date().toISOString()
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'session2',
|
|
||||||
title: 'Project Discussion',
|
|
||||||
created_at: new Date(Date.now() - 86400000).toISOString()
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const mockAuthResponse = {
|
|
||||||
user_id: mockUsers[0].id,
|
|
||||||
session_id: mockSessions[0].id
|
|
||||||
};
|
|
||||||
|
|
||||||
const mockSuggestions = [
|
|
||||||
{ text: "What can you do?", context: "capabilities" },
|
|
||||||
{ text: "Show my files", context: "drive" },
|
|
||||||
{ text: "Create a task", context: "tasks" }
|
|
||||||
];
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
export const mails = [
|
|
||||||
{
|
|
||||||
id: "6c84fb90-12c4-11e1-840d-7b25c5ee775a",
|
|
||||||
name: "William Smith",
|
|
||||||
email: "williamsmith@example.com",
|
|
||||||
subject: "Meeting Tomorrow",
|
|
||||||
text: "Hi, let's have a meeting tomorrow to discuss the project...",
|
|
||||||
date: "2023-10-22T09:00:00",
|
|
||||||
read: true,
|
|
||||||
labels: ["meeting", "work", "important"],
|
|
||||||
},
|
|
||||||
// Additional emails would go here
|
|
||||||
];
|
|
||||||
|
|
||||||
export const accounts = [
|
|
||||||
{
|
|
||||||
label: "Alicia Koch",
|
|
||||||
email: "alicia@example.com",
|
|
||||||
icon: "📧",
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
export const contacts = [
|
|
||||||
{
|
|
||||||
name: "Emma Johnson",
|
|
||||||
email: "emma.johnson@example.com",
|
|
||||||
},
|
|
||||||
// Additional contacts would go here
|
|
||||||
];
|
|
||||||
|
|
@ -1,94 +1,64 @@
|
||||||
<!DOCTYPE html>
|
<div class="mail-layout" x-data="mailApp()" x-cloak>
|
||||||
<html>
|
<div class="panel mail-sidebar">
|
||||||
<head>
|
<div style="padding: 1rem; border-bottom: 1px solid #334155;">
|
||||||
<title>Email Client</title>
|
<button style="width: 100%; padding: 0.75rem; background: #3b82f6; color: white; border: none; border-radius: 0.5rem; cursor: pointer; font-weight: 600;">
|
||||||
<script src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js" defer></script>
|
✏ Compose
|
||||||
<script src="./data.js"></script>
|
</button>
|
||||||
<script src="./store.js"></script>
|
|
||||||
<style>
|
|
||||||
[x-cloak] { display: none !important; }
|
|
||||||
:root {
|
|
||||||
--bg-primary: #1a1a2e;
|
|
||||||
--bg-secondary: #16213e;
|
|
||||||
--text-primary: #e94560;
|
|
||||||
--text-secondary: #00d9ff;
|
|
||||||
}
|
|
||||||
body {
|
|
||||||
font-family: 'Segoe UI', sans-serif;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
background: var(--bg-primary);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
.email-container {
|
|
||||||
display: flex;
|
|
||||||
height: 100vh;
|
|
||||||
}
|
|
||||||
.sidebar {
|
|
||||||
width: 250px;
|
|
||||||
background: var(--bg-secondary);
|
|
||||||
padding: 1rem;
|
|
||||||
}
|
|
||||||
.email-list {
|
|
||||||
flex: 1;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
.email-content {
|
|
||||||
flex: 2;
|
|
||||||
padding: 1rem;
|
|
||||||
}
|
|
||||||
.email-item {
|
|
||||||
padding: 1rem;
|
|
||||||
border-bottom: 1px solid #333;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.email-item:hover {
|
|
||||||
background: rgba(255,255,255,0.1);
|
|
||||||
}
|
|
||||||
.email-item.unread {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body x-data="{
|
|
||||||
mails,
|
|
||||||
accounts,
|
|
||||||
contacts,
|
|
||||||
...mailStore
|
|
||||||
}" x-cloak>
|
|
||||||
<div class="email-container">
|
|
||||||
<div class="sidebar">
|
|
||||||
<h2>Accounts</h2>
|
|
||||||
<div x-text="accounts[0].label"></div>
|
|
||||||
|
|
||||||
<h2>Folders</h2>
|
|
||||||
<div>Inbox</div>
|
|
||||||
<div>Sent</div>
|
|
||||||
<div>Drafts</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="email-list">
|
|
||||||
<template x-for="mail in mails" :key="mail.id">
|
|
||||||
<div
|
|
||||||
class="email-item"
|
|
||||||
:class="{ 'unread': !mail.read }"
|
|
||||||
@click="setSelected(mail.id)"
|
|
||||||
>
|
|
||||||
<div x-text="mail.name"></div>
|
|
||||||
<div x-text="mail.subject"></div>
|
|
||||||
<div x-text="mail.date"></div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="email-content">
|
|
||||||
<template x-if="selected">
|
|
||||||
<div>
|
|
||||||
<h2 x-text="mails.find(m => m.id === selected).subject"></h2>
|
|
||||||
<div x-text="mails.find(m => m.id === selected).text"></div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
</div>
|
||||||
|
<template x-for="folder in folders" :key="folder.name">
|
||||||
|
<div class="nav-item"
|
||||||
|
:class="{ active: currentFolder === folder.name }"
|
||||||
|
@click="currentFolder = folder.name">
|
||||||
|
<span x-text="folder.icon"></span>
|
||||||
|
<span x-text="folder.name"></span>
|
||||||
|
<span style="margin-left: auto; font-size: 0.875rem; color: #64748b;"
|
||||||
|
x-show="folder.count > 0"
|
||||||
|
x-text="folder.count"></span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
|
||||||
</html>
|
<div class="panel mail-list">
|
||||||
|
<div style="padding: 1rem; border-bottom: 1px solid #334155;">
|
||||||
|
<h3 x-text="currentFolder"></h3>
|
||||||
|
</div>
|
||||||
|
<template x-for="mail in filteredMails" :key="mail.id">
|
||||||
|
<div class="mail-item"
|
||||||
|
:class="{ unread: !mail.read, selected: selectedMail?.id === mail.id }"
|
||||||
|
@click="selectMail(mail)">
|
||||||
|
<div style="display: flex; justify-content: space-between; margin-bottom: 0.5rem;">
|
||||||
|
<span style="font-weight: 600;" x-text="mail.from"></span>
|
||||||
|
<span class="text-xs text-gray" x-text="mail.time"></span>
|
||||||
|
</div>
|
||||||
|
<div style="font-weight: 600; margin-bottom: 0.25rem;" x-text="mail.subject"></div>
|
||||||
|
<div class="text-sm text-gray" x-text="mail.preview"></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel mail-content">
|
||||||
|
<template x-if="selectedMail">
|
||||||
|
<div class="mail-content-view">
|
||||||
|
<div class="mail-header">
|
||||||
|
<h2 x-text="selectedMail.subject"></h2>
|
||||||
|
<div style="display: flex; align-items: center; gap: 1rem; margin-top: 1rem;">
|
||||||
|
<div>
|
||||||
|
<div style="font-weight: 600;" x-text="selectedMail.from"></div>
|
||||||
|
<div class="text-sm text-gray" x-text="'to: ' + selectedMail.to"></div>
|
||||||
|
</div>
|
||||||
|
<div style="margin-left: auto;" class="text-sm text-gray" x-text="selectedMail.date"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mail-body" x-html="selectedMail.body"></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template x-if="!selectedMail">
|
||||||
|
<div style="display: flex; align-items: center; justify-content: center; height: 100%; color: #64748b;">
|
||||||
|
<div style="text-align: center;">
|
||||||
|
<div style="font-size: 4rem; margin-bottom: 1rem;">✉</div>
|
||||||
|
<p>Select a message to read</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
|
||||||
78
web/desktop/mail/mail.js
Normal file
78
web/desktop/mail/mail.js
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
function mailApp() {
|
||||||
|
return {
|
||||||
|
currentFolder: 'Inbox',
|
||||||
|
selectedMail: null,
|
||||||
|
|
||||||
|
folders: [
|
||||||
|
{ name: 'Inbox', icon: '📥', count: 4 },
|
||||||
|
{ name: 'Sent', icon: '📤', count: 0 },
|
||||||
|
{ name: 'Drafts', icon: '📝', count: 2 },
|
||||||
|
{ name: 'Starred', icon: '⭐', count: 0 },
|
||||||
|
{ name: 'Trash', icon: '🗑', count: 0 }
|
||||||
|
],
|
||||||
|
|
||||||
|
mails: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
from: 'Sarah Johnson',
|
||||||
|
to: 'me@example.com',
|
||||||
|
subject: 'Q4 Project Update',
|
||||||
|
preview: 'Hi team, I wanted to share the latest updates on our Q4 projects...',
|
||||||
|
body: '<p>Hi team,</p><p>I wanted to share the latest updates on our Q4 projects. We\'ve made significant progress on the main deliverables and are on track to meet our goals.</p><p>Please review the attached documents and let me know if you have any questions.</p><p>Best regards,<br>Sarah</p>',
|
||||||
|
time: '10:30 AM',
|
||||||
|
date: 'Nov 15, 2025',
|
||||||
|
read: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
from: 'Mike Chen',
|
||||||
|
to: 'me@example.com',
|
||||||
|
subject: 'Meeting Tomorrow',
|
||||||
|
preview: 'Don\'t forget about our meeting tomorrow at 2 PM...',
|
||||||
|
body: '<p>Hi,</p><p>Don\'t forget about our meeting tomorrow at 2 PM to discuss the new features.</p><p>See you then!<br>Mike</p>',
|
||||||
|
time: '9:15 AM',
|
||||||
|
date: 'Nov 15, 2025',
|
||||||
|
read: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
from: 'Emma Wilson',
|
||||||
|
to: 'me@example.com',
|
||||||
|
subject: 'Design Review Complete',
|
||||||
|
preview: 'The design review for the new dashboard is complete...',
|
||||||
|
body: '<p>Hi,</p><p>The design review for the new dashboard is complete. Overall, the team is happy with the direction.</p><p>I\'ve made the requested changes and updated the Figma file.</p><p>Thanks,<br>Emma</p>',
|
||||||
|
time: 'Yesterday',
|
||||||
|
date: 'Nov 14, 2025',
|
||||||
|
read: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
from: 'David Lee',
|
||||||
|
to: 'me@example.com',
|
||||||
|
subject: 'Budget Approval Needed',
|
||||||
|
preview: 'Could you please review and approve the Q1 budget?',
|
||||||
|
body: '<p>Hi,</p><p>Could you please review and approve the Q1 budget when you get a chance?</p><p>It\'s attached to this email.</p><p>Thanks,<br>David</p>',
|
||||||
|
time: 'Yesterday',
|
||||||
|
date: 'Nov 14, 2025',
|
||||||
|
read: false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
get filteredMails() {
|
||||||
|
return this.mails;
|
||||||
|
},
|
||||||
|
|
||||||
|
selectMail(mail) {
|
||||||
|
this.selectedMail = mail;
|
||||||
|
mail.read = true;
|
||||||
|
this.updateFolderCounts();
|
||||||
|
},
|
||||||
|
|
||||||
|
updateFolderCounts() {
|
||||||
|
const inbox = this.folders.find(f => f.name === 'Inbox');
|
||||||
|
if (inbox) {
|
||||||
|
inbox.count = this.mails.filter(m => !m.read).length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
export function createMailStore() {
|
|
||||||
return {
|
|
||||||
selected: null,
|
|
||||||
setSelected(id) {
|
|
||||||
this.selected = id;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export const mailStore = createMailStore();
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
<div x-data="{
|
|
||||||
shortcuts: [
|
|
||||||
[
|
|
||||||
{ key: 'Q', label: 'Rename', action: () => console.log('Rename') },
|
|
||||||
{ key: 'W', label: 'View', action: () => console.log('View') },
|
|
||||||
{ key: 'E', label: 'Edit', action: () => console.log('Edit') },
|
|
||||||
{ key: 'I', label: 'Cut', action: () => console.log('Cut') },
|
|
||||||
{ key: 'O', label: 'Paste', action: () => console.log('Paste') },
|
|
||||||
{ key: 'P', label: 'Duplicate', action: () => console.log('Duplicate') }
|
|
||||||
],
|
|
||||||
[
|
|
||||||
{ key: 'K', label: 'Star', action: () => console.log('Star') },
|
|
||||||
{ key: 'L', label: 'Download', action: () => console.log('Download') },
|
|
||||||
{ key: 'Z', label: 'Upload', action: () => console.log('Upload') },
|
|
||||||
{ key: 'X', label: 'Refresh', action: () => console.log('Refresh') }
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}" class="footer">
|
|
||||||
<div class="shortcut-group" x-for="group in shortcuts">
|
|
||||||
<template x-for="shortcut in group">
|
|
||||||
<button @click="shortcut.action()" class="shortcut-btn">
|
|
||||||
<span x-text="shortcut.key" class="key"></span>
|
|
||||||
<span x-text="shortcut.label" class="label"></span>
|
|
||||||
</button>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
<nav class="navbar">
|
|
||||||
<div class="navbar-brand">
|
|
||||||
<img src="/icons/general-bots.svg" alt="Logo" class="logo">
|
|
||||||
<span>General Bots</span>
|
|
||||||
</div>
|
|
||||||
<div class="nav-links">
|
|
||||||
<a href="#chat" class="nav-link active" data-target="chat">
|
|
||||||
<i class="icon">💬</i> Chat
|
|
||||||
</a>
|
|
||||||
<a href="#drive" class="nav-link" data-target="drive">
|
|
||||||
<i class="icon">📁</i> Drive
|
|
||||||
</a>
|
|
||||||
<a href="#tables" class="nav-link" data-target="tables">
|
|
||||||
<i class="icon">📊</i> Tables
|
|
||||||
</a>
|
|
||||||
<a href="#tasks" class="nav-link" data-target="tasks">
|
|
||||||
<i class="icon">✅</i> Tasks
|
|
||||||
</a>
|
|
||||||
<a href="#mail" class="nav-link" data-target="mail">
|
|
||||||
<i class="icon">✉️</i> Mail
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="nav-user">
|
|
||||||
<div class="user-avatar" id="userAvatar">👤</div>
|
|
||||||
<div class="user-menu">
|
|
||||||
<a href="../auth/login.html" class="user-menu-item">Sign In</a>
|
|
||||||
<a href="../auth/register.html" class="user-menu-item">Register</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
|
|
@ -1,79 +0,0 @@
|
||||||
<div x-data="{
|
|
||||||
leftWidth: '30%',
|
|
||||||
rightWidth: '70%',
|
|
||||||
isDragging: false,
|
|
||||||
|
|
||||||
startDrag() {
|
|
||||||
this.isDragging = true;
|
|
||||||
document.body.style.cursor = 'col-resize';
|
|
||||||
document.addEventListener('mousemove', this.drag.bind(this));
|
|
||||||
document.addEventListener('mouseup', this.stopDrag.bind(this));
|
|
||||||
},
|
|
||||||
|
|
||||||
drag(e) {
|
|
||||||
if (!this.isDragging) return;
|
|
||||||
const container = this.$el.parentElement;
|
|
||||||
const containerRect = container.getBoundingClientRect();
|
|
||||||
const newLeftWidth = ((e.clientX - containerRect.left) / containerRect.width) * 100;
|
|
||||||
|
|
||||||
if (newLeftWidth > 20 && newLeftWidth < 80) {
|
|
||||||
this.leftWidth = `${newLeftWidth}%`;
|
|
||||||
this.rightWidth = `${100 - newLeftWidth}%`;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
stopDrag() {
|
|
||||||
this.isDragging = false;
|
|
||||||
document.body.style.cursor = '';
|
|
||||||
document.removeEventListener('mousemove', this.drag);
|
|
||||||
document.removeEventListener('mouseup', this.stopDrag);
|
|
||||||
}
|
|
||||||
}" class="resizable-container">
|
|
||||||
<div class="resizable-panel left" :style="{ width: leftWidth }">
|
|
||||||
<slot name="left"></slot>
|
|
||||||
</div>
|
|
||||||
<div class="resizable-handle" @mousedown="startDrag"></div>
|
|
||||||
<div class="resizable-panel right" :style="{ width: rightWidth }">
|
|
||||||
<slot name="right"></slot>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.resizable-container {
|
|
||||||
display: flex;
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.resizable-panel {
|
|
||||||
height: 100%;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.resizable-handle {
|
|
||||||
width: 8px;
|
|
||||||
background: var(--border);
|
|
||||||
cursor: col-resize;
|
|
||||||
transition: background 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.resizable-handle:hover {
|
|
||||||
background: var(--primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.resizable-container {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.resizable-panel {
|
|
||||||
width: 100% !important;
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.resizable-handle {
|
|
||||||
width: 100%;
|
|
||||||
height: 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,112 +0,0 @@
|
||||||
:root {
|
|
||||||
--navbar-height: 60px;
|
|
||||||
--primary-color: #0066ff;
|
|
||||||
--text-color: #333;
|
|
||||||
--bg-color: #fff;
|
|
||||||
--border-color: #e0e0e0;
|
|
||||||
--hover-bg: #f5f5f5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
height: var(--navbar-height);
|
|
||||||
padding: 0 20px;
|
|
||||||
background: var(--bg-color);
|
|
||||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
z-index: 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-brand {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
font-weight: bold;
|
|
||||||
color: var(--text-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
height: 30px;
|
|
||||||
width: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-links {
|
|
||||||
display: flex;
|
|
||||||
gap: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-link {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 5px;
|
|
||||||
text-decoration: none;
|
|
||||||
color: var(--text-color);
|
|
||||||
padding: 5px 10px;
|
|
||||||
border-radius: 4px;
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-link:hover {
|
|
||||||
background: var(--hover-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-link.active {
|
|
||||||
color: var(--primary-color);
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-user {
|
|
||||||
position: relative;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-avatar {
|
|
||||||
width: 36px;
|
|
||||||
height: 36px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: var(--hover-bg);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-menu {
|
|
||||||
position: absolute;
|
|
||||||
right: 0;
|
|
||||||
top: 100%;
|
|
||||||
background: var(--bg-color);
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
border-radius: 4px;
|
|
||||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|
||||||
padding: 5px 0;
|
|
||||||
min-width: 150px;
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-menu-item {
|
|
||||||
display: block;
|
|
||||||
padding: 8px 15px;
|
|
||||||
color: var(--text-color);
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-menu-item:hover {
|
|
||||||
background: var(--hover-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-user:hover .user-menu {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="dark"] {
|
|
||||||
--text-color: #fff;
|
|
||||||
--bg-color: #1a1a1a;
|
|
||||||
--border-color: #333;
|
|
||||||
--hover-bg: #333;
|
|
||||||
}
|
|
||||||
|
|
@ -1,427 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Tables - Excel Clone</title>
|
|
||||||
<link rel="stylesheet" href="./styles/theme.css">
|
|
||||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
|
||||||
<script src="./store.js"></script>
|
|
||||||
<style>
|
|
||||||
.app-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
height: 100vh;
|
|
||||||
background: var(--background);
|
|
||||||
}
|
|
||||||
|
|
||||||
.content-container {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
|
||||||
background: linear-gradient(135deg, var(--primary) 0%, var(--accent) 100%);
|
|
||||||
color: white;
|
|
||||||
padding: 15px 20px;
|
|
||||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.header h1 {
|
|
||||||
font-size: 24px;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header .subtitle {
|
|
||||||
font-size: 12px;
|
|
||||||
opacity: 0.9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.spreadsheet-content {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Existing spreadsheet styles remain unchanged */
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="app-container" x-data>
|
|
||||||
<div class="navbar-container">
|
|
||||||
<div x-html="(await fetch('./components/navbar.html')).text()"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="content-container">
|
|
||||||
<div class="header">
|
|
||||||
<h1>📊 Tables</h1>
|
|
||||||
<div class="subtitle">Excel Clone - Celebrating Lotus 1-2-3 Legacy 🎉</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="resizable-container">
|
|
||||||
<div class="resizable-panel left" style="width: 30%">
|
|
||||||
<!-- Left panel content -->
|
|
||||||
</div>
|
|
||||||
<div class="resizable-handle"></div>
|
|
||||||
<div class="resizable-panel right" style="width: 70%">
|
|
||||||
<div class="spreadsheet-content">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
class TablesApp {
|
|
||||||
constructor() {
|
|
||||||
this.data = this.generateMockData(100, 26);
|
|
||||||
this.selectedCell = null;
|
|
||||||
this.cols = 26;
|
|
||||||
this.rows = 100;
|
|
||||||
this.visibleRows = 30;
|
|
||||||
this.rowOffset = 0;
|
|
||||||
|
|
||||||
this.init();
|
|
||||||
}
|
|
||||||
|
|
||||||
generateMockData(rows, cols) {
|
|
||||||
const data = [];
|
|
||||||
const products = ['Laptop', 'Mouse', 'Keyboard', 'Monitor', 'Headphones', 'Webcam', 'Desk', 'Chair'];
|
|
||||||
const regions = ['North', 'South', 'East', 'West'];
|
|
||||||
|
|
||||||
for (let i = 0; i < rows; i++) {
|
|
||||||
const row = {};
|
|
||||||
for (let j = 0; j < cols; j++) {
|
|
||||||
const col = this.getColumnName(j);
|
|
||||||
if (i === 0) {
|
|
||||||
if (j === 0) row[col] = 'Product';
|
|
||||||
else if (j === 1) row[col] = 'Region';
|
|
||||||
else if (j === 2) row[col] = 'Q1';
|
|
||||||
else if (j === 3) row[col] = 'Q2';
|
|
||||||
else if (j === 4) row[col] = 'Q3';
|
|
||||||
else if (j === 5) row[col] = 'Q4';
|
|
||||||
else if (j === 6) row[col] = 'Total';
|
|
||||||
else row[col] = `Col ${col}`;
|
|
||||||
} else {
|
|
||||||
if (j === 0) row[col] = products[i % products.length];
|
|
||||||
else if (j === 1) row[col] = regions[i % regions.length];
|
|
||||||
else if (j >= 2 && j <= 5) row[col] = Math.floor(Math.random() * 10000) + 1000;
|
|
||||||
else if (j === 6) {
|
|
||||||
row[col] = `=C${i+1}+D${i+1}+E${i+1}+F${i+1}`;
|
|
||||||
}
|
|
||||||
else row[col] = Math.random() > 0.5 ? Math.floor(Math.random() * 1000) : '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
data.push(row);
|
|
||||||
}
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
getColumnName(index) {
|
|
||||||
let name = '';
|
|
||||||
while (index >= 0) {
|
|
||||||
name = String.fromCharCode(65 + (index % 26)) + name;
|
|
||||||
index = Math.floor(index / 26) - 1;
|
|
||||||
}
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
|
|
||||||
init() {
|
|
||||||
this.renderTable();
|
|
||||||
this.setupEventListeners();
|
|
||||||
this.updateStats();
|
|
||||||
}
|
|
||||||
|
|
||||||
renderTable() {
|
|
||||||
const thead = document.getElementById('tableHead');
|
|
||||||
const tbody = document.getElementById('tableBody');
|
|
||||||
|
|
||||||
let headerHTML = '<tr><th></th>';
|
|
||||||
for (let i = 0; i < this.cols; i++) {
|
|
||||||
headerHTML += `<th>${this.getColumnName(i)}</th>`;
|
|
||||||
}
|
|
||||||
headerHTML += '</tr>';
|
|
||||||
thead.innerHTML = headerHTML;
|
|
||||||
|
|
||||||
let bodyHTML = '';
|
|
||||||
const endRow = Math.min(this.rowOffset + this.visibleRows, this.rows);
|
|
||||||
|
|
||||||
for (let i = this.rowOffset; i < endRow; i++) {
|
|
||||||
bodyHTML += `<tr><th>${i + 1}</th>`;
|
|
||||||
for (let j = 0; j < this.cols; j++) {
|
|
||||||
const col = this.getColumnName(j);
|
|
||||||
const value = this.data[i][col] || '';
|
|
||||||
const displayValue = this.calculateCell(value, i, j);
|
|
||||||
bodyHTML += `<td data-row="${i}" data-col="${j}" data-cell="${col}${i+1}">${displayValue}</td>`;
|
|
||||||
}
|
|
||||||
bodyHTML += '</tr>';
|
|
||||||
}
|
|
||||||
tbody.innerHTML = bodyHTML;
|
|
||||||
}
|
|
||||||
|
|
||||||
calculateCell(value, row, col) {
|
|
||||||
if (typeof value === 'string' && value.startsWith('=')) {
|
|
||||||
try {
|
|
||||||
const formula = value.substring(1).toUpperCase();
|
|
||||||
|
|
||||||
if (formula.includes('SUM')) {
|
|
||||||
const match = formula.match(/SUM\(([A-Z]+\d+):([A-Z]+\d+)\)/);
|
|
||||||
if (match) {
|
|
||||||
const sum = this.calculateRange(match[1], match[2], 'sum');
|
|
||||||
return sum.toFixed(2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (formula.includes('AVERAGE')) {
|
|
||||||
const match = formula.match(/AVERAGE\(([A-Z]+\d+):([A-Z]+\d+)\)/);
|
|
||||||
if (match) {
|
|
||||||
const avg = this.calculateRange(match[1], match[2], 'avg');
|
|
||||||
return avg.toFixed(2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let expression = formula;
|
|
||||||
const cellRefs = expression.match(/[A-Z]+\d+/g);
|
|
||||||
if (cellRefs) {
|
|
||||||
cellRefs.forEach(ref => {
|
|
||||||
const val = this.getCellValue(ref);
|
|
||||||
expression = expression.replace(ref, val);
|
|
||||||
});
|
|
||||||
return eval(expression).toFixed(2);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
return '#ERROR';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
getCellValue(cellRef) {
|
|
||||||
const col = cellRef.match(/[A-Z]+/)[0];
|
|
||||||
const row = parseInt(cellRef.match(/\d+/)[0]) - 1;
|
|
||||||
const value = this.data[row][col];
|
|
||||||
|
|
||||||
if (typeof value === 'string' && value.startsWith('=')) {
|
|
||||||
return this.calculateCell(value, row, this.getColIndex(col));
|
|
||||||
}
|
|
||||||
return parseFloat(value) || 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
getColIndex(colName) {
|
|
||||||
let index = 0;
|
|
||||||
for (let i = 0; i < colName.length; i++) {
|
|
||||||
index = index * 26 + (colName.charCodeAt(i) - 64);
|
|
||||||
}
|
|
||||||
return index - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
calculateRange(start, end, operation) {
|
|
||||||
const startCol = start.match(/[A-Z]+/)[0];
|
|
||||||
const startRow = parseInt(start.match(/\d+/)[0]) - 1;
|
|
||||||
const endCol = end.match(/[A-Z]+/)[0];
|
|
||||||
const endRow = parseInt(end.match(/\d+/)[0]) - 1;
|
|
||||||
|
|
||||||
let values = [];
|
|
||||||
for (let r = startRow; r <= endRow; r++) {
|
|
||||||
for (let c = this.getColIndex(startCol); c <= this.getColIndex(endCol); c++) {
|
|
||||||
const col = this.getColumnName(c);
|
|
||||||
const val = parseFloat(this.data[r][col]) || 0;
|
|
||||||
values.push(val);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (operation === 'sum') {
|
|
||||||
return values.reduce((a, b) => a + b, 0);
|
|
||||||
} else if (operation === 'avg') {
|
|
||||||
return values.reduce((a, b) => a + b, 0) / values.length;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
setupEventListeners() {
|
|
||||||
const container = document.getElementById('spreadsheetContainer');
|
|
||||||
const formulaInput = document.getElementById('formulaInput');
|
|
||||||
|
|
||||||
container.addEventListener('scroll', () => {
|
|
||||||
const scrollPercentage = (container.scrollTop + container.clientHeight) / container.scrollHeight;
|
|
||||||
if (scrollPercentage > 0.8 && this.rowOffset + this.visibleRows < this.rows) {
|
|
||||||
this.rowOffset += 10;
|
|
||||||
this.renderTable();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('tableBody').addEventListener('click', (e) => {
|
|
||||||
if (e.target.tagName === 'TD') {
|
|
||||||
this.selectCell(e.target);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('tableBody').addEventListener('dblclick', (e) => {
|
|
||||||
if (e.target.tagName === 'TD') {
|
|
||||||
this.editCell(e.target);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
formulaInput.addEventListener('keypress', (e) => {
|
|
||||||
if (e.key === 'Enter' && this.selectedCell) {
|
|
||||||
this.updateCellValue(formulaInput.value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
selectCell(cell) {
|
|
||||||
if (this.selectedCell) {
|
|
||||||
this.selectedCell.classList.remove('selected');
|
|
||||||
}
|
|
||||||
this.selectedCell = cell;
|
|
||||||
cell.classList.add('selected');
|
|
||||||
|
|
||||||
const cellRef = cell.dataset.cell;
|
|
||||||
document.getElementById('cellRef').textContent = cellRef;
|
|
||||||
document.getElementById('selectedCell').textContent = cellRef;
|
|
||||||
|
|
||||||
const row = parseInt(cell.dataset.row);
|
|
||||||
const col = this.getColumnName(parseInt(cell.dataset.col));
|
|
||||||
const value = this.data[row][col] || '';
|
|
||||||
document.getElementById('formulaInput').value = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
editCell(cell) {
|
|
||||||
const row = parseInt(cell.dataset.row);
|
|
||||||
const col = this.getColumnName(parseInt(cell.dataset.col));
|
|
||||||
const value = this.data[row][col] || '';
|
|
||||||
|
|
||||||
cell.classList.add('editing');
|
|
||||||
cell.innerHTML = `<input type="text" class="cell-editor" value="${value}" />`;
|
|
||||||
const input = cell.querySelector('.cell-editor');
|
|
||||||
input.focus();
|
|
||||||
input.select();
|
|
||||||
|
|
||||||
input.addEventListener('blur', () => {
|
|
||||||
this.updateCellValue(input.value);
|
|
||||||
});
|
|
||||||
|
|
||||||
input.addEventListener('keypress', (e) => {
|
|
||||||
if (e.key === 'Enter') {
|
|
||||||
this.updateCellValue(input.value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
updateCellValue(value) {
|
|
||||||
if (!this.selectedCell) return;
|
|
||||||
|
|
||||||
const row = parseInt(this.selectedCell.dataset.row);
|
|
||||||
const col = this.getColumnName(parseInt(this.selectedCell.dataset.col));
|
|
||||||
|
|
||||||
this.data[row][col] = value;
|
|
||||||
this.renderTable();
|
|
||||||
|
|
||||||
const newCell = document.querySelector(`td[data-row="${row}"][data-col="${this.selectedCell.dataset.col}"]`);
|
|
||||||
if (newCell) this.selectCell(newCell);
|
|
||||||
}
|
|
||||||
|
|
||||||
bold() {
|
|
||||||
if (this.selectedCell) {
|
|
||||||
this.selectedCell.style.fontWeight = this.selectedCell.style.fontWeight === 'bold' ? 'normal' : 'bold';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
addRow() {
|
|
||||||
const newRow = {};
|
|
||||||
for (let i = 0; i < this.cols; i++) {
|
|
||||||
newRow[this.getColumnName(i)] = '';
|
|
||||||
}
|
|
||||||
this.data.push(newRow);
|
|
||||||
this.rows++;
|
|
||||||
this.renderTable();
|
|
||||||
this.updateStats();
|
|
||||||
}
|
|
||||||
|
|
||||||
addColumn() {
|
|
||||||
const newCol = this.getColumnName(this.cols);
|
|
||||||
this.data.forEach(row => row[newCol] = '');
|
|
||||||
this.cols++;
|
|
||||||
this.renderTable();
|
|
||||||
this.updateStats();
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteRow() {
|
|
||||||
if (this.selectedCell && this.rows > 1) {
|
|
||||||
const row = parseInt(this.selectedCell.dataset.row);
|
|
||||||
this.data.splice(row, 1);
|
|
||||||
this.rows--;
|
|
||||||
this.renderTable();
|
|
||||||
this.updateStats();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteColumn() {
|
|
||||||
if (this.selectedCell && this.cols > 1) {
|
|
||||||
const col = this.getColumnName(parseInt(this.selectedCell.dataset.col));
|
|
||||||
this.data.forEach(row => delete row[col]);
|
|
||||||
this.cols--;
|
|
||||||
this.renderTable();
|
|
||||||
this.updateStats();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sort() {
|
|
||||||
if (this.selectedCell) {
|
|
||||||
const col = this.getColumnName(parseInt(this.selectedCell.dataset.col));
|
|
||||||
const header = this.data[0];
|
|
||||||
const dataRows = this.data.slice(1);
|
|
||||||
|
|
||||||
dataRows.sort((a, b) => {
|
|
||||||
const aVal = a[col] || '';
|
|
||||||
const bVal = b[col] || '';
|
|
||||||
return aVal.toString().localeCompare(bVal.toString());
|
|
||||||
});
|
|
||||||
|
|
||||||
this.data = [header, ...dataRows];
|
|
||||||
this.renderTable();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sum() {
|
|
||||||
if (this.selectedCell) {
|
|
||||||
const col = this.getColumnName(parseInt(this.selectedCell.dataset.col));
|
|
||||||
document.getElementById('formulaInput').value = `=SUM(${col}2:${col}${this.rows})`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
average() {
|
|
||||||
if (this.selectedCell) {
|
|
||||||
const col = this.getColumnName(parseInt(this.selectedCell.dataset.col));
|
|
||||||
document.getElementById('formulaInput').value = `=AVERAGE(${col}2:${col}${this.rows})`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
clearCell() {
|
|
||||||
if (this.selectedCell) {
|
|
||||||
this.updateCellValue('');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
exportData() {
|
|
||||||
const csv = this.data.map(row => {
|
|
||||||
return Object.values(row).join(',');
|
|
||||||
}).join('\n');
|
|
||||||
|
|
||||||
const blob = new Blob([csv], { type: 'text/csv' });
|
|
||||||
const url = URL.createObjectURL(blob);
|
|
||||||
const a = document.createElement('a');
|
|
||||||
a.href = url;
|
|
||||||
a.download = 'tables_export.csv';
|
|
||||||
a.click();
|
|
||||||
}
|
|
||||||
|
|
||||||
updateStats() {
|
|
||||||
document.getElementById('rowCount').textContent = this.rows;
|
|
||||||
document.getElementById('colCount').textContent = this.cols;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const app = new TablesApp();
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
37
web/desktop/tasks/index.html
Normal file
37
web/desktop/tasks/index.html
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
<div class="tasks-container" x-data="tasksApp()" x-init="init()" x-cloak>
|
||||||
|
<h1>Tasks</h1>
|
||||||
|
<div class="task-input">
|
||||||
|
<input type="text"
|
||||||
|
x-model="newTask"
|
||||||
|
@keyup.enter="addTask()"
|
||||||
|
placeholder="Add a new task...">
|
||||||
|
<button @click="addTask()">Add Task</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul class="task-list">
|
||||||
|
<template x-for="task in filteredTasks" :key="task.id">
|
||||||
|
<li class="task-item" :class="{ completed: task.completed }">
|
||||||
|
<input type="checkbox"
|
||||||
|
:checked="task.completed"
|
||||||
|
@change="toggleTask(task.id)">
|
||||||
|
<span x-text="task.text"></span>
|
||||||
|
<button @click="deleteTask(task.id)">×</button>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="task-filters" x-show="tasks.length > 0">
|
||||||
|
<button :class="{ active: filter === 'all' }" @click="filter = 'all'">
|
||||||
|
All (<span x-text="tasks.length"></span>)
|
||||||
|
</button>
|
||||||
|
<button :class="{ active: filter === 'active' }" @click="filter = 'active'">
|
||||||
|
Active (<span x-text="activeTasks"></span>)
|
||||||
|
</button>
|
||||||
|
<button :class="{ active: filter === 'completed' }" @click="filter = 'completed'">
|
||||||
|
Completed (<span x-text="completedTasks"></span>)
|
||||||
|
</button>
|
||||||
|
<button @click="clearCompleted()" x-show="completedTasks > 0">
|
||||||
|
Clear Completed
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
document.addEventListener('alpine:init', () => {
|
|
||||||
Alpine.store('todo', {
|
|
||||||
title: 'Todo',
|
|
||||||
items: [],
|
|
||||||
nextId: 1,
|
|
||||||
|
|
||||||
addTodo(text) {
|
|
||||||
if (!text.trim()) return;
|
|
||||||
|
|
||||||
this.items.push({
|
|
||||||
id: this.nextId,
|
|
||||||
title: text.trim(),
|
|
||||||
done: false
|
|
||||||
});
|
|
||||||
this.nextId++;
|
|
||||||
},
|
|
||||||
|
|
||||||
toggleTodo(id) {
|
|
||||||
this.items = this.items.map(item =>
|
|
||||||
item.id === id ? { ...item, done: !item.done } : item
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
removeTodo(id) {
|
|
||||||
this.items = this.items.filter(item => item.id !== id);
|
|
||||||
},
|
|
||||||
|
|
||||||
clearCompleted() {
|
|
||||||
this.items = this.items.filter(item => !item.done);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,101 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>Todo App</title>
|
|
||||||
<script src="./store.js"></script>
|
|
||||||
<script defer src="js/cdn.min.js"></script>
|
|
||||||
<style>
|
|
||||||
.todo-container {
|
|
||||||
max-width: 500px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 20px;
|
|
||||||
background: #fff;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
|
||||||
}
|
|
||||||
.todo-controls {
|
|
||||||
display: flex;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
input[type="text"] {
|
|
||||||
flex: 1;
|
|
||||||
padding: 10px;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
button {
|
|
||||||
padding: 10px 15px;
|
|
||||||
margin-left: 10px;
|
|
||||||
background: #4CAF50;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.todo-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 10px 0;
|
|
||||||
border-bottom: 1px solid #eee;
|
|
||||||
}
|
|
||||||
.todo-list {
|
|
||||||
list-style: none;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0 0 20px;
|
|
||||||
}
|
|
||||||
.completed span {
|
|
||||||
text-decoration: line-through;
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
.delete-btn {
|
|
||||||
background: #f44336;
|
|
||||||
padding: 2px 6px;
|
|
||||||
border-radius: 50%;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div x-data class="todo-container">
|
|
||||||
<h3 x-text="$store.todo.title + ' App'"></h3>
|
|
||||||
|
|
||||||
<div class="todo-controls" x-data="{ text: '' }">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
x-model="text"
|
|
||||||
@keyup.enter="$store.todo.addTodo(text); text = ''"
|
|
||||||
placeholder="Add new todo..."
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
@click="$store.todo.addTodo(text); text = ''"
|
|
||||||
:disabled="!text.trim()"
|
|
||||||
>
|
|
||||||
Add
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ul class="todo-list">
|
|
||||||
<template x-for="item in $store.todo.items" :key="item.id">
|
|
||||||
<li :class="{ 'completed': item.done }">
|
|
||||||
<div class="todo-item">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
:checked="item.done"
|
|
||||||
@click="$store.todo.toggleTodo(item.id)"
|
|
||||||
/>
|
|
||||||
<span x-text="item.title"></span>
|
|
||||||
<button
|
|
||||||
class="delete-btn"
|
|
||||||
@click="$store.todo.removeTodo(item.id)"
|
|
||||||
>
|
|
||||||
×
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</template>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
77
web/desktop/tasks/tasks.js
Normal file
77
web/desktop/tasks/tasks.js
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
function tasksApp() {
|
||||||
|
return {
|
||||||
|
newTask: '',
|
||||||
|
filter: 'all',
|
||||||
|
tasks: [],
|
||||||
|
|
||||||
|
init() {
|
||||||
|
const saved = localStorage.getItem('tasks');
|
||||||
|
if (saved) {
|
||||||
|
try {
|
||||||
|
this.tasks = JSON.parse(saved);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to load tasks:', e);
|
||||||
|
this.tasks = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
addTask() {
|
||||||
|
if (this.newTask.trim() === '') return;
|
||||||
|
|
||||||
|
this.tasks.push({
|
||||||
|
id: Date.now(),
|
||||||
|
text: this.newTask.trim(),
|
||||||
|
completed: false,
|
||||||
|
createdAt: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
|
this.newTask = '';
|
||||||
|
this.save();
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleTask(id) {
|
||||||
|
const task = this.tasks.find(t => t.id === id);
|
||||||
|
if (task) {
|
||||||
|
task.completed = !task.completed;
|
||||||
|
this.save();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteTask(id) {
|
||||||
|
this.tasks = this.tasks.filter(t => t.id !== id);
|
||||||
|
this.save();
|
||||||
|
},
|
||||||
|
|
||||||
|
clearCompleted() {
|
||||||
|
this.tasks = this.tasks.filter(t => !t.completed);
|
||||||
|
this.save();
|
||||||
|
},
|
||||||
|
|
||||||
|
save() {
|
||||||
|
try {
|
||||||
|
localStorage.setItem('tasks', JSON.stringify(this.tasks));
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to save tasks:', e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
get filteredTasks() {
|
||||||
|
if (this.filter === 'active') {
|
||||||
|
return this.tasks.filter(t => !t.completed);
|
||||||
|
}
|
||||||
|
if (this.filter === 'completed') {
|
||||||
|
return this.tasks.filter(t => t.completed);
|
||||||
|
}
|
||||||
|
return this.tasks;
|
||||||
|
},
|
||||||
|
|
||||||
|
get activeTasks() {
|
||||||
|
return this.tasks.filter(t => !t.completed).length;
|
||||||
|
},
|
||||||
|
|
||||||
|
get completedTasks() {
|
||||||
|
return this.tasks.filter(t => t.completed).length;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -1,344 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>About - BotServer</title>
|
|
||||||
<style>
|
|
||||||
* {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
||||||
min-height: 100vh;
|
|
||||||
padding: 40px 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
max-width: 800px;
|
|
||||||
margin: 0 auto;
|
|
||||||
background: white;
|
|
||||||
border-radius: 16px;
|
|
||||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
||||||
color: white;
|
|
||||||
padding: 50px 40px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header h1 {
|
|
||||||
font-size: 48px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header .version {
|
|
||||||
font-size: 18px;
|
|
||||||
opacity: 0.9;
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
padding: 50px 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section {
|
|
||||||
margin-bottom: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section h2 {
|
|
||||||
color: #1f2937;
|
|
||||||
font-size: 28px;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
padding-bottom: 10px;
|
|
||||||
border-bottom: 3px solid #667eea;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section p {
|
|
||||||
color: #4b5563;
|
|
||||||
line-height: 1.8;
|
|
||||||
font-size: 16px;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section ul {
|
|
||||||
list-style: none;
|
|
||||||
padding-left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section ul li {
|
|
||||||
color: #4b5563;
|
|
||||||
line-height: 1.8;
|
|
||||||
font-size: 16px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
padding-left: 30px;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section ul li:before {
|
|
||||||
content: "→";
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
color: #667eea;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.maintainer-box {
|
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
||||||
color: white;
|
|
||||||
padding: 30px;
|
|
||||||
border-radius: 12px;
|
|
||||||
text-align: center;
|
|
||||||
margin: 30px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.maintainer-box h3 {
|
|
||||||
font-size: 24px;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.maintainer-box a {
|
|
||||||
color: white;
|
|
||||||
text-decoration: none;
|
|
||||||
font-size: 20px;
|
|
||||||
font-weight: 600;
|
|
||||||
border-bottom: 2px solid rgba(255, 255, 255, 0.5);
|
|
||||||
transition: border-color 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.maintainer-box a:hover {
|
|
||||||
border-bottom-color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.links {
|
|
||||||
display: flex;
|
|
||||||
gap: 20px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.link-card {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 200px;
|
|
||||||
background: #f9fafb;
|
|
||||||
padding: 25px;
|
|
||||||
border-radius: 12px;
|
|
||||||
text-align: center;
|
|
||||||
transition: transform 0.3s, box-shadow 0.3s;
|
|
||||||
border: 2px solid #e5e7eb;
|
|
||||||
}
|
|
||||||
|
|
||||||
.link-card:hover {
|
|
||||||
transform: translateY(-5px);
|
|
||||||
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
|
|
||||||
border-color: #667eea;
|
|
||||||
}
|
|
||||||
|
|
||||||
.link-card h4 {
|
|
||||||
color: #1f2937;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.link-card a {
|
|
||||||
color: #667eea;
|
|
||||||
text-decoration: none;
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.link-card a:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.features {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
|
||||||
gap: 20px;
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.feature-card {
|
|
||||||
background: #f9fafb;
|
|
||||||
padding: 20px;
|
|
||||||
border-radius: 8px;
|
|
||||||
border-left: 4px solid #667eea;
|
|
||||||
}
|
|
||||||
|
|
||||||
.feature-card h4 {
|
|
||||||
color: #1f2937;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.feature-card p {
|
|
||||||
color: #6b7280;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.6;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.back-link {
|
|
||||||
text-align: center;
|
|
||||||
padding: 30px;
|
|
||||||
background: #f9fafb;
|
|
||||||
}
|
|
||||||
|
|
||||||
.back-link a {
|
|
||||||
color: #667eea;
|
|
||||||
text-decoration: none;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.back-link a:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 6px 12px;
|
|
||||||
background: #667eea;
|
|
||||||
color: white;
|
|
||||||
border-radius: 20px;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 600;
|
|
||||||
margin: 5px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="container">
|
|
||||||
<div class="header">
|
|
||||||
<h1>BotServer</h1>
|
|
||||||
<p>Open-source conversational AI platform</p>
|
|
||||||
<div class="version">Version 6.0.5</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="content">
|
|
||||||
<div class="section">
|
|
||||||
<h2>About BotServer</h2>
|
|
||||||
<p>
|
|
||||||
BotServer is a comprehensive, open-source platform for building and deploying conversational AI bots.
|
|
||||||
It provides a complete ecosystem for creating intelligent chatbots with natural language processing,
|
|
||||||
knowledge management, and multi-channel deployment capabilities.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Built with performance and scalability in mind, BotServer leverages Rust's safety and efficiency
|
|
||||||
to deliver enterprise-grade bot infrastructure that can handle thousands of concurrent conversations.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="maintainer-box">
|
|
||||||
<h3>Main Maintainer</h3>
|
|
||||||
<a href="https://pragmatismo.com.br" target="_blank">Pragmatismo.com.br</a>
|
|
||||||
<p style="margin-top: 15px; font-size: 14px; opacity: 0.9;">
|
|
||||||
Professional consulting and implementation services available
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="section">
|
|
||||||
<h2>Open Source</h2>
|
|
||||||
<div class="links">
|
|
||||||
<div class="link-card">
|
|
||||||
<h4>GitHub Organization</h4>
|
|
||||||
<a href="https://github.com/GeneralBots" target="_blank">github.com/GeneralBots</a>
|
|
||||||
</div>
|
|
||||||
<div class="link-card">
|
|
||||||
<h4>Repository</h4>
|
|
||||||
<a href="https://github.com/GeneralBots/BotServer" target="_blank">BotServer Repository</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p style="margin-top: 20px;">
|
|
||||||
BotServer is licensed under AGPL-3.0, ensuring it remains free and open source.
|
|
||||||
Contributions, bug reports, and feature requests are welcome!
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="section">
|
|
||||||
<h2>Key Features</h2>
|
|
||||||
<div class="features">
|
|
||||||
<div class="feature-card">
|
|
||||||
<h4>🤖 AI-Powered</h4>
|
|
||||||
<p>Local LLM support with DeepSeek and embeddings for intelligent conversations</p>
|
|
||||||
</div>
|
|
||||||
<div class="feature-card">
|
|
||||||
<h4>📚 Knowledge Base</h4>
|
|
||||||
<p>Vector database integration for semantic search and context-aware responses</p>
|
|
||||||
</div>
|
|
||||||
<div class="feature-card">
|
|
||||||
<h4>🔌 Multi-Channel</h4>
|
|
||||||
<p>Deploy to web, WhatsApp, voice, and more with unified bot logic</p>
|
|
||||||
</div>
|
|
||||||
<div class="feature-card">
|
|
||||||
<h4>📦 Modular Architecture</h4>
|
|
||||||
<p>Install only what you need - email, proxy, meeting, and more</p>
|
|
||||||
</div>
|
|
||||||
<div class="feature-card">
|
|
||||||
<h4>🚀 High Performance</h4>
|
|
||||||
<p>Built with Rust for speed, safety, and efficient resource usage</p>
|
|
||||||
</div>
|
|
||||||
<div class="feature-card">
|
|
||||||
<h4>🔒 Secure by Default</h4>
|
|
||||||
<p>Authentication, encryption, and secure credential management</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="section">
|
|
||||||
<h2>Technology Stack</h2>
|
|
||||||
<div>
|
|
||||||
<span class="badge">Rust</span>
|
|
||||||
<span class="badge">Actix-Web</span>
|
|
||||||
<span class="badge">PostgreSQL</span>
|
|
||||||
<span class="badge">Redis</span>
|
|
||||||
<span class="badge">MinIO</span>
|
|
||||||
<span class="badge">Qdrant</span>
|
|
||||||
<span class="badge">LLama.cpp</span>
|
|
||||||
<span class="badge">WebSocket</span>
|
|
||||||
<span class="badge">Docker</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="section">
|
|
||||||
<h2>Community</h2>
|
|
||||||
<p>
|
|
||||||
BotServer is built and maintained by a dedicated community of developers passionate about
|
|
||||||
conversational AI and open source software. Special thanks to all contributors who have
|
|
||||||
helped make this project possible.
|
|
||||||
</p>
|
|
||||||
<ul>
|
|
||||||
<li>Join discussions and get support on GitHub</li>
|
|
||||||
<li>Report bugs and request features through Issues</li>
|
|
||||||
<li>Contribute code via Pull Requests</li>
|
|
||||||
<li>Share your bot creations with the community</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="section">
|
|
||||||
<h2>Getting Started</h2>
|
|
||||||
<p>
|
|
||||||
Download the source code, press F5 (or run with <code>cargo run</code>), and everything
|
|
||||||
will be automatically set up. BotServer includes sample bots to get you started immediately.
|
|
||||||
</p>
|
|
||||||
<p style="margin-top: 15px; padding: 15px; background: #fef3c7; border-left: 4px solid #f59e0b; border-radius: 4px;">
|
|
||||||
<strong>Quick Start:</strong> Clone the repository, ensure Rust is installed, and run
|
|
||||||
<code>cargo run</code>. The first launch will download and configure all required components.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="back-link">
|
|
||||||
<a href="/">← Back to BotServer</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
@ -1,445 +0,0 @@
|
||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>BotServer - Login</title>
|
|
||||||
<style>
|
|
||||||
* {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family:
|
|
||||||
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
|
||||||
Oxygen, Ubuntu, Cantarell, sans-serif;
|
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
||||||
min-height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
background: white;
|
|
||||||
border-radius: 16px;
|
|
||||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
|
||||||
width: 100%;
|
|
||||||
max-width: 400px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
||||||
color: white;
|
|
||||||
padding: 30px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header h1 {
|
|
||||||
font-size: 28px;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header p {
|
|
||||||
font-size: 14px;
|
|
||||||
opacity: 0.9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-container {
|
|
||||||
padding: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tabs {
|
|
||||||
display: flex;
|
|
||||||
margin-bottom: 30px;
|
|
||||||
border-bottom: 2px solid #e5e7eb;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab {
|
|
||||||
flex: 1;
|
|
||||||
padding: 12px;
|
|
||||||
text-align: center;
|
|
||||||
cursor: pointer;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #6b7280;
|
|
||||||
transition: all 0.3s;
|
|
||||||
border-bottom: 3px solid transparent;
|
|
||||||
margin-bottom: -2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab.active {
|
|
||||||
color: #667eea;
|
|
||||||
border-bottom-color: #667eea;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group label {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: #374151;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group input {
|
|
||||||
width: 100%;
|
|
||||||
padding: 12px 16px;
|
|
||||||
border: 2px solid #e5e7eb;
|
|
||||||
border-radius: 8px;
|
|
||||||
font-size: 14px;
|
|
||||||
transition: border-color 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group input:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: #667eea;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn {
|
|
||||||
width: 100%;
|
|
||||||
padding: 14px;
|
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 8px;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
transition:
|
|
||||||
transform 0.2s,
|
|
||||||
box-shadow 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn:hover {
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 10px 20px rgba(102, 126, 234, 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn:active {
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-section {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-section.active {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message {
|
|
||||||
padding: 12px;
|
|
||||||
border-radius: 8px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
font-size: 14px;
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message.error {
|
|
||||||
background-color: #fee2e2;
|
|
||||||
color: #991b1b;
|
|
||||||
border: 1px solid #fecaca;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message.success {
|
|
||||||
background-color: #d1fae5;
|
|
||||||
color: #065f46;
|
|
||||||
border: 1px solid #a7f3d0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer {
|
|
||||||
text-align: center;
|
|
||||||
padding: 20px;
|
|
||||||
color: #6b7280;
|
|
||||||
font-size: 12px;
|
|
||||||
background: #f9fafb;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer a {
|
|
||||||
color: #667eea;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer a:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.anonymous-link {
|
|
||||||
text-align: center;
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.anonymous-link a {
|
|
||||||
color: #667eea;
|
|
||||||
text-decoration: none;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.anonymous-link a:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="container">
|
|
||||||
<div class="header">
|
|
||||||
<h1>BotServer</h1>
|
|
||||||
<p>by Pragmatismo.com.br</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-container">
|
|
||||||
<div id="message" class="message"></div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
style="
|
|
||||||
background: #f0f9ff;
|
|
||||||
border-left: 4px solid #3b82f6;
|
|
||||||
padding: 15px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
border-radius: 8px;
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<p
|
|
||||||
style="
|
|
||||||
margin: 0;
|
|
||||||
color: #1e40af;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.6;
|
|
||||||
"
|
|
||||||
>
|
|
||||||
💡 <strong>Anonymous Sessions:</strong> Each visitor
|
|
||||||
automatically gets a unique session. Register to save
|
|
||||||
your conversations and access them across devices!
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="tabs">
|
|
||||||
<div class="tab active" onclick="switchTab('signin')">
|
|
||||||
Sign In
|
|
||||||
</div>
|
|
||||||
<div class="tab" onclick="switchTab('signup')">Sign Up</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Sign In Form -->
|
|
||||||
<div id="signin-form" class="form-section active">
|
|
||||||
<form onsubmit="handleSignIn(event)">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="signin-email">Email</label>
|
|
||||||
<input
|
|
||||||
type="email"
|
|
||||||
id="signin-email"
|
|
||||||
required
|
|
||||||
placeholder="your@email.com"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="signin-password">Password</label>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
id="signin-password"
|
|
||||||
required
|
|
||||||
placeholder="••••••••"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="btn">Sign In</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Sign Up Form -->
|
|
||||||
<div id="signup-form" class="form-section">
|
|
||||||
<form onsubmit="handleSignUp(event)">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="signup-name">Full Name</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="signup-name"
|
|
||||||
required
|
|
||||||
placeholder="John Doe"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="signup-email">Email</label>
|
|
||||||
<input
|
|
||||||
type="email"
|
|
||||||
id="signup-email"
|
|
||||||
required
|
|
||||||
placeholder="your@email.com"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="signup-password">Password</label>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
id="signup-password"
|
|
||||||
required
|
|
||||||
placeholder="••••••••"
|
|
||||||
minlength="6"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="signup-confirm">Confirm Password</label>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
id="signup-confirm"
|
|
||||||
required
|
|
||||||
placeholder="••••••••"
|
|
||||||
minlength="6"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="btn">Sign Up</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="anonymous-link">
|
|
||||||
<a href="/">← Continue with anonymous session</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="footer">
|
|
||||||
Maintained by
|
|
||||||
<a href="https://pragmatismo.com.br" target="_blank"
|
|
||||||
>Pragmatismo.com.br</a
|
|
||||||
><br />
|
|
||||||
Open source at
|
|
||||||
<a href="https://github.com/GeneralBots" target="_blank"
|
|
||||||
>github.com/GeneralBots</a
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
function switchTab(tab) {
|
|
||||||
// Update tabs
|
|
||||||
document
|
|
||||||
.querySelectorAll(".tab")
|
|
||||||
.forEach((t) => t.classList.remove("active"));
|
|
||||||
event.target.classList.add("active");
|
|
||||||
|
|
||||||
// Update forms
|
|
||||||
document
|
|
||||||
.querySelectorAll(".form-section")
|
|
||||||
.forEach((f) => f.classList.remove("active"));
|
|
||||||
document.getElementById(tab + "-form").classList.add("active");
|
|
||||||
|
|
||||||
// Clear message
|
|
||||||
hideMessage();
|
|
||||||
}
|
|
||||||
|
|
||||||
function showMessage(text, type) {
|
|
||||||
const messageEl = document.getElementById("message");
|
|
||||||
messageEl.textContent = text;
|
|
||||||
messageEl.className = "message " + type;
|
|
||||||
messageEl.style.display = "block";
|
|
||||||
}
|
|
||||||
|
|
||||||
function hideMessage() {
|
|
||||||
const messageEl = document.getElementById("message");
|
|
||||||
messageEl.style.display = "none";
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleSignIn(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
hideMessage();
|
|
||||||
|
|
||||||
const email = document.getElementById("signin-email").value;
|
|
||||||
const password =
|
|
||||||
document.getElementById("signin-password").value;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch("/api/auth/login", {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ email, password }),
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
showMessage(
|
|
||||||
"Login successful! Redirecting...",
|
|
||||||
"success",
|
|
||||||
);
|
|
||||||
localStorage.setItem("token", data.token);
|
|
||||||
setTimeout(() => {
|
|
||||||
window.location.href = "/";
|
|
||||||
}, 1000);
|
|
||||||
} else {
|
|
||||||
showMessage(
|
|
||||||
data.error || "Login failed. Please try again.",
|
|
||||||
"error",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
showMessage("Network error. Please try again.", "error");
|
|
||||||
console.error("Login error:", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleSignUp(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
hideMessage();
|
|
||||||
|
|
||||||
const name = document.getElementById("signup-name").value;
|
|
||||||
const email = document.getElementById("signup-email").value;
|
|
||||||
const password =
|
|
||||||
document.getElementById("signup-password").value;
|
|
||||||
const confirm = document.getElementById("signup-confirm").value;
|
|
||||||
|
|
||||||
if (password !== confirm) {
|
|
||||||
showMessage("Passwords do not match!", "error");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (password.length < 6) {
|
|
||||||
showMessage(
|
|
||||||
"Password must be at least 6 characters long.",
|
|
||||||
"error",
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch("/api/auth/register", {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ name, email, password }),
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
showMessage(
|
|
||||||
"Registration successful! Please sign in.",
|
|
||||||
"success",
|
|
||||||
);
|
|
||||||
setTimeout(() => {
|
|
||||||
switchTab("signin");
|
|
||||||
document.getElementById("signin-email").value =
|
|
||||||
email;
|
|
||||||
}, 1500);
|
|
||||||
} else {
|
|
||||||
showMessage(
|
|
||||||
data.error ||
|
|
||||||
"Registration failed. Please try again.",
|
|
||||||
"error",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
showMessage("Network error. Please try again.", "error");
|
|
||||||
console.error("Registration error:", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
Loading…
Add table
Reference in a new issue