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:
Rodrigo Rodriguez (Pragmatismo) 2025-11-15 19:08:26 -03:00
parent 3014822ace
commit 01e89c9358
50 changed files with 1286 additions and 3414 deletions

52
Cargo.lock generated
View file

@ -34,6 +34,29 @@ dependencies = [
"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]]
name = "actix-http"
version = "3.11.2"
@ -1330,6 +1353,7 @@ name = "botserver"
version = "6.0.8"
dependencies = [
"actix-cors",
"actix-files",
"actix-multipart",
"actix-web",
"actix-ws",
@ -3665,6 +3689,12 @@ dependencies = [
"pin-project-lite",
]
[[package]]
name = "http-range"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573"
[[package]]
name = "httparse"
version = "1.10.1"
@ -4775,6 +4805,16 @@ version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "minimal-lexical"
version = "0.2.1"
@ -8592,6 +8632,12 @@ dependencies = [
"unic-common",
]
[[package]]
name = "unicase"
version = "2.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539"
[[package]]
name = "unicode-bidi"
version = "0.3.18"
@ -8766,6 +8812,12 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "v_htmlescape"
version = "0.15.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e8257fbc510f0a46eb602c10215901938b5c2a7d5e70fc11483b1d3c9b5b18c"
[[package]]
name = "valuable"
version = "0.1.1"

View file

@ -46,6 +46,7 @@ desktop = ["dep:tauri", "dep:tauri-plugin-dialog", "dep:tauri-plugin-opener"]
[dependencies]
actix-cors = "0.7"
actix-files = "0.6.8"
actix-multipart = "0.7"
actix-web = "4.9"
actix-ws = "0.3"

View file

@ -29,7 +29,7 @@ impl DriveMonitor {
pub fn spawn(self: Arc<Self>) -> tokio::task::JoinHandle<()> {
tokio::spawn(async move {
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 {
tick.tick().await;
if let Err(e) = self.check_for_changes().await {
@ -44,7 +44,7 @@ impl DriveMonitor {
None => return Ok(()),
};
self.check_gbdialog_changes(client).await?;
// TODO: Remove self.check_gbot(client).await?;
self.check_gbot(client).await?;
Ok(())
}
async fn check_gbdialog_changes(&self, client: &Client) -> Result<(), Box<dyn Error + Send + Sync>> {

View file

@ -247,25 +247,25 @@ pub async fn start_llm_server(
// if n_moe != "0" {
// args.push_str(&format!(" --n-cpu-moe {}", n_moe));
// }
// if parallel != "1" {
// args.push_str(&format!(" --parallel {}", parallel));
// }
// if cont_batching == "true" {
// args.push_str(" --cont-batching");
// }
// if mlock == "true" {
// args.push_str(" --mlock");
// }
// if no_mmap == "true" {
// args.push_str(" --no-mmap");
// }
// if n_predict != "0" {
// args.push_str(&format!(" --n-predict {}", n_predict));
// }
// args.push_str(&format!(" --ctx-size {}", n_ctx_size));
if n_moe != "0" {
args.push_str(&format!(" --n-cpu-moe {}", n_moe));
}
if parallel != "1" {
args.push_str(&format!(" --parallel {}", parallel));
}
if cont_batching == "true" {
args.push_str(" --cont-batching");
}
if mlock == "true" {
args.push_str(" --mlock");
}
if no_mmap == "true" {
args.push_str(" --no-mmap");
}
if n_predict != "0" {
args.push_str(&format!(" --n-predict {}", n_predict));
}
args.push_str(&format!(" --ctx-size {}", n_ctx_size));
if cfg!(windows) {
let mut cmd = tokio::process::Command::new("cmd");

View file

@ -46,7 +46,7 @@ use crate::session::{create_session, get_session_history, get_sessions, start_se
use crate::shared::state::AppState;
use crate::shared::utils::create_conn;
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)]
pub enum BootstrapProgress {
StartingBootstrap,
@ -77,7 +77,29 @@ async fn main() -> std::io::Result<()> {
let (state_tx, state_rx) = tokio::sync::mpsc::channel::<Arc<AppState>>(1);
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 state_rx = Arc::new(tokio::sync::Mutex::new(state_rx));
let handle = std::thread::Builder::new()
@ -284,11 +306,11 @@ async fn main() -> std::io::Result<()> {
.wrap(Logger::default())
.wrap(Logger::new("HTTP REQUEST: %a %{User-Agent}i"))
.app_data(web::Data::from(app_state_clone))
.configure(web_server::configure_app)
.service(auth_handler)
.service(create_session)
.service(get_session_history)
.service(get_sessions)
.service(index)
.service(start_session)
.service(upload_file)
.service(voice_start)
@ -310,7 +332,6 @@ async fn main() -> std::io::Result<()> {
.service(save_draft)
.service(save_click);
}
app = app.service(static_files);
app = app.service(bot_index);
app
})

View file

@ -9,6 +9,8 @@ pub async fn run() -> Result<()> {
print_usage();
return Ok(());
}
use tracing::info;
fn print_usage(){info!("usage: botserver <command> [options]")}
let command = &args[1];
match command.as_str() {
"start" => {
@ -164,6 +166,3 @@ pub async fn run() -> Result<()> {
}
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");
}

View file

@ -156,6 +156,7 @@ impl PackageManager {
);
Ok(())
}
pub fn remove(&self, component_name: &str) -> Result<()> {
let component = self
.components

View file

@ -1,20 +1,11 @@
use actix_files::Files;
use actix_web::{HttpRequest, HttpResponse, Result};
use log::{debug, error, warn};
use std::fs;
use log::{debug, error};
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("/")]
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)),
Err(e) => {
error!("Failed to load index page: {}", e);
@ -22,11 +13,12 @@ async fn index() -> Result<HttpResponse> {
}
}
}
#[actix_web::get("/{botname}")]
async fn bot_index(req: HttpRequest) -> Result<HttpResponse> {
let botname = req.match_info().query("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)),
Err(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> {
let filename = req.match_info().query("filename");
let path = format!("web/html/{}", filename);
match fs::read(&path) {
Ok(content) => {
debug!(
"Static file {} loaded successfully, size: {} bytes",
filename,
content.len()
);
let content_type = match filename {
f if f.ends_with(".js") => "application/javascript",
f if f.ends_with(".riot") => "application/javascript",
f if f.ends_with(".html") => "application/javascript",
f if f.ends_with(".css") => "text/css",
f if f.ends_with(".png") => "image/png",
f if f.ends_with(".jpg") | f.ends_with(".jpeg") => "image/jpeg",
_ => "text/plain",
};
Ok(HttpResponse::Ok().content_type(content_type).body(content))
}
Err(e) => {
warn!("Static file not found: {} - {}", filename, e);
Ok(HttpResponse::NotFound().body("File not found"))
}
}
pub fn configure_app(cfg: &mut actix_web::web::ServiceConfig) {
let static_path = Path::new("/home/rodriguez/src/botserver/web/desktop");
// Serve all static files from desktop directory
cfg.service(
Files::new("/", static_path)
.index_file("index.html")
.prefer_utf8(true)
.use_last_modified(true)
.use_etag(true)
.show_files_listing()
);
// Serve all JS files
cfg.service(
Files::new("/js", static_path.join("js"))
.prefer_utf8(true)
.use_last_modified(true)
.use_etag(true)
);
// Serve all component directories
["drive", "tasks", "mail"].iter().for_each(|dir| {
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);
}

View 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>

View 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
View 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();
}
};
}

View file

@ -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';
}
}));
});

View file

@ -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>

View file

@ -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%;
}
}

View file

@ -1,64 +1,324 @@
/* Main app styles */
@import url('../shared/styles.css');
@import url('chat.css');
* { margin: 0; padding: 0; box-sizing: border-box; }
/* Navbar styles */
.navbar {
background: white;
color: #333;
padding: 0.5rem 1rem;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 1000;
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
background: #0f172a;
color: #e2e8f0;
height: 100vh;
overflow: hidden;
}
.nav-container {
/* Navbar */
nav {
background: #1e293b;
border-bottom: 2px solid #334155;
padding: 0 1rem;
display: flex;
align-items: center;
max-width: 1200px;
margin: 0 auto;
height: 60px;
gap: 0.5rem;
}
.nav-brand {
nav .logo {
font-size: 1.5rem;
font-weight: bold;
font-size: 1.2rem;
margin-right: 2rem;
color: #333;
background: linear-gradient(135deg, #3b82f6, #8b5cf6);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
margin-right: auto;
}
.nav-links {
display: flex;
list-style: none;
margin: 0;
padding: 0;
}
.nav-link {
color: #555;
nav a {
color: #94a3b8;
text-decoration: none;
padding: 0.5rem 1rem;
display: flex;
align-items: center;
padding: 0.75rem 1.25rem;
border-radius: 0.5rem;
transition: all 0.2s;
}
.nav-link:hover {
color: #000;
}
.nav-link.active {
color: #0066ff;
font-weight: 500;
}
.nav-link i {
margin-right: 0.5rem;
color: #666;
nav a:hover {
background: #334155;
color: #e2e8f0;
}
.nav-link.active i {
color: #0066ff;
nav a.active {
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;
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View 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())
);
}
};
}

View file

@ -1,585 +1,67 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<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 class="drive-layout" x-data="driveApp()" x-cloak>
<div class="panel drive-sidebar">
<div style="padding: 1rem; border-bottom: 1px solid #334155;">
<h3>General Bots Drive</h3>
</div>
<template x-for="item in navItems" :key="item.name">
<div class="nav-item"
:class="{ active: current === item.name }"
@click="current = item.name">
<span x-text="item.icon"></span>
<span x-text="item.name"></span>
</div>
</template>
</div>
<script>
function fileManager() {
return {
collapsed: false,
currentPath: '',
searchTerm: '',
filterType: 'all',
selectedFile: null,
expanded: { '': true, 'projects': true },
contextMenu: { show: false, x: 0, y: 0, file: null },
navLinks: [
{ title: 'My Drive', path: '', icon: '🏠' },
{ title: 'Shared', path: 'shared', icon: '👥' },
{ title: 'Starred', path: 'starred', icon: '⭐' },
{ title: 'Recent', path: 'recent', icon: '🕐' },
{ title: 'Trash', path: 'trash', icon: '🗑️' },
],
fileSystem: {
"": {
id: "root", name: "My Drive", path: "", is_dir: true,
children: ["projects", "documents", "media", "shared"]
},
"projects": {
id: "projects", name: "Projects", path: "projects", is_dir: true,
modified: "2025-01-15T10:30:00Z", starred: true, shared: false,
children: ["web-apps", "mobile-apps", "ai-research"]
},
"projects/web-apps": {
id: "web-apps", name: "Web Applications", path: "projects/web-apps", is_dir: true,
modified: "2025-01-14T16:45:00Z", starred: false, shared: true,
children: ["package.json", "README.md"]
},
"projects/web-apps/package.json": {
id: "package-json", name: "package.json", path: "projects/web-apps/package.json",
is_dir: false, size: 2048, type: "json", modified: "2025-01-13T14:20:00Z"
},
"projects/web-apps/README.md": {
id: "readme-md", name: "README.md", path: "projects/web-apps/README.md",
is_dir: false, size: 5120, type: "md", modified: "2025-01-12T09:30:00Z", shared: true
},
"documents": {
id: "documents", name: "Documents", path: "documents", is_dir: true,
modified: "2025-01-14T12:00:00Z",
children: ["Q1-Strategy.pdf", "Budget-2025.xlsx"]
},
"documents/Q1-Strategy.pdf": {
id: "q1-strategy", name: "Q1 Strategy.pdf", path: "documents/Q1-Strategy.pdf",
is_dir: false, size: 1048576, type: "pdf", modified: "2025-01-10T15:30:00Z", starred: true, shared: true
},
"documents/Budget-2025.xlsx": {
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>
<div class="panel drive-main">
<div style="padding: 1rem; border-bottom: 1px solid #334155;">
<h2 x-text="current"></h2>
<input type="text" x-model="search" placeholder="Search files..."
style="width: 100%; margin-top: 0.5rem; padding: 0.5rem; background: #0f172a; border: 1px solid #334155; border-radius: 0.375rem; color: #e2e8f0;">
</div>
<div class="file-list">
<template x-for="file in filteredFiles" :key="file.id">
<div class="file-item"
:class="{ selected: selectedFile?.id === file.id }"
@click="selectedFile = file">
<span class="file-icon" x-text="file.icon"></span>
<div style="flex: 1;">
<div style="font-weight: 600;" x-text="file.name"></div>
<div class="text-xs text-gray" x-text="file.date"></div>
</div>
<div class="text-sm text-gray" x-text="file.size"></div>
</div>
</template>
</div>
</div>
<div class="panel drive-details">
<template x-if="selectedFile">
<div style="padding: 2rem;">
<div style="text-align: center; margin-bottom: 2rem;">
<div style="font-size: 4rem; margin-bottom: 1rem;" x-text="selectedFile.icon"></div>
<h3 x-text="selectedFile.name"></h3>
<p class="text-sm text-gray" x-text="selectedFile.type"></p>
</div>
<div style="margin-bottom: 1rem;">
<div class="text-sm" style="margin-bottom: 0.5rem;">Size</div>
<div class="text-gray" x-text="selectedFile.size"></div>
</div>
<div style="margin-bottom: 1rem;">
<div class="text-sm" style="margin-bottom: 0.5rem;">Modified</div>
<div class="text-gray" x-text="selectedFile.date"></div>
</div>
<div style="display: flex; gap: 0.5rem; margin-top: 2rem;">
<button style="flex: 1; padding: 0.75rem; background: #3b82f6; color: white; border: none; border-radius: 0.375rem; cursor: pointer;">Download</button>
<button style="flex: 1; padding: 0.75rem; background: #10b981; color: white; border: none; border-radius: 0.375rem; cursor: pointer;">Share</button>
</div>
</div>
</template>
<template x-if="!selectedFile">
<div style="padding: 2rem; text-align: center; color: #64748b;">
<div style="font-size: 4rem; margin-bottom: 1rem;">📄</div>
<p>Select a file to view details</p>
</div>
</template>
</div>
</div>

View file

@ -1,35 +1,31 @@
<!doctype html>
<html lang="pt-br">
<html lang="en">
<head>
<meta charset="utf-8"/>
<title>General Bots</title>
<meta name="viewport" content="width=device-width,initial-scale=1.0"/>
<link rel="stylesheet" href="css/app.css" type="text/css">
<script src="js/lib/gsap.min.js"></script>
<script src="js/lib/marked.min.js"></script>
<script src="js/mock-data.js"></script>
<script src="js/auth.js"></script>
<meta charset="utf-8" />
<title>General Bots Desktop</title>
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<link rel="stylesheet" href="css/app.css" />
<script defer src="js/alpine.js"></script>
</head>
<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 class="chat-container">
<div class="connection-status connecting" id="connectionStatus"></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>
<div id="main-content">
<!-- Sections will be loaded dynamically -->
</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>
</html>

View file

@ -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);
}

View file

@ -1,56 +1,37 @@
class Layout {
static currentPage = 'chat';
const sections = {
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() {
this.setCurrentPage();
this.loadNavbar();
this.setupNavigation();
}
static setCurrentPage() {
const hash = window.location.hash.substring(1) || 'chat';
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();
}
});
try {
const html = await loadSectionHTML(sections[section]);
mainContent.innerHTML = html;
window.history.pushState({}, '', `#${section}`);
Alpine.initTree(mainContent);
} catch (err) {
console.error('Error loading section:', err);
mainContent.innerHTML = `<div class="error">Failed to load ${section} section</div>`;
}
}
// Initialize on load and also on navigation
Layout.init();
window.addEventListener('popstate', () => Layout.init());
// Handle initial load based on URL hash
window.addEventListener('DOMContentLoaded', () => {
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);
});

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -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" }
];

View file

@ -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
];

View file

@ -1,94 +1,64 @@
<!DOCTYPE html>
<html>
<head>
<title>Email Client</title>
<script src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js" defer></script>
<script src="./data.js"></script>
<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 class="mail-layout" x-data="mailApp()" x-cloak>
<div class="panel mail-sidebar">
<div style="padding: 1rem; border-bottom: 1px solid #334155;">
<button style="width: 100%; padding: 0.75rem; background: #3b82f6; color: white; border: none; border-radius: 0.5rem; cursor: pointer; font-weight: 600;">
✏ Compose
</button>
</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>
</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
View 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;
}
}
};
}

View file

@ -1,10 +0,0 @@
export function createMailStore() {
return {
selected: null,
setSelected(id) {
this.selected = id;
}
};
}
export const mailStore = createMailStore();

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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;
}

View file

@ -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>

View 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>

View file

@ -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);
}
});
});

View file

@ -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>

View 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;
}
};
}

View file

@ -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>

View file

@ -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>