feat(tui): add usage overlay and sidebar for displaying usage statistics and tasks
feat(tui): implement status bar with connection and turn state indicators feat(tui): create workflow panel for agent status and progress visualization feat(web): introduce web frontend interface with static file serving feat(ws): add WebSocket interface for real-time communication and session management
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
[package]
|
||||
name = "zesdex-web"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
|
||||
# Web frontend interface — serves a browser-based UI.
|
||||
# The frontend assets (JS/HTML/CSS) are compiled separately and
|
||||
# served by the Axum-based API server or a dedicated HTTP server.
|
||||
[dependencies]
|
||||
zesdex-domain = { path = "../../domain" }
|
||||
zesdex-application = { path = "../../application" }
|
||||
zesdex-infrastructure = { path = "../../infrastructure" }
|
||||
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
chrono.workspace = true
|
||||
uuid.workspace = true
|
||||
anyhow.workspace = true
|
||||
tokio.workspace = true
|
||||
tracing.workspace = true
|
||||
axum.workspace = true
|
||||
tower.workspace = true
|
||||
include_dir.workspace = true
|
||||
mime_guess = "2"
|
||||
@@ -0,0 +1,111 @@
|
||||
//! Web frontend interface — serves the browser-based UI.
|
||||
//!
|
||||
//! The frontend assets (JS/HTML/CSS) are expected to be built into
|
||||
//! a `dist/` directory at compile time via `include_dir!`, or served
|
||||
//! from a path at runtime.
|
||||
//!
|
||||
//! ## Development
|
||||
//!
|
||||
//! During development, point `--web-dir` to the frontend dev server
|
||||
//! or build directory.
|
||||
|
||||
use axum::http::StatusCode;
|
||||
use axum::response::{Html, IntoResponse, Response};
|
||||
use axum::routing::get;
|
||||
use axum::Router;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use tokio::fs;
|
||||
use tracing::{info, warn};
|
||||
|
||||
/// Web server state.
|
||||
pub struct WebState {
|
||||
/// Directory from which to serve static files.
|
||||
pub static_dir: PathBuf,
|
||||
}
|
||||
|
||||
/// Build the web frontend router.
|
||||
pub fn build_router(state: Arc<WebState>) -> Router {
|
||||
Router::new()
|
||||
.route("/", get(index_handler))
|
||||
.route("/{*path}", get(static_handler))
|
||||
.with_state(state)
|
||||
}
|
||||
|
||||
/// Serve the index.html for root requests.
|
||||
async fn index_handler(
|
||||
axum::extract::State(state): axum::extract::State<Arc<WebState>>,
|
||||
) -> Result<Html<String>, StatusCode> {
|
||||
let index_path = state.static_dir.join("index.html");
|
||||
match fs::read_to_string(&index_path).await {
|
||||
Ok(html) => Ok(Html(html)),
|
||||
Err(_) => {
|
||||
// Return a minimal HTML page when no frontend is built
|
||||
Ok(Html(
|
||||
r#"<!DOCTYPE html>
|
||||
<html><head><title>Zesdex Web</title>
|
||||
<meta charset="utf-8">
|
||||
<style>body{font-family:sans-serif;padding:2em;background:#1a1b26;color:#c0caf5}
|
||||
h1{color:#7aa2f7}a{color:#bb9af7}</style></head>
|
||||
<body>
|
||||
<h1>Zesdex Web</h1>
|
||||
<p>Web interface is ready.</p>
|
||||
<p>To connect the frontend:</p>
|
||||
<ol>
|
||||
<li>Build the frontend: <code>cd apps/interfaces/web && npm install && npm run build</code></li>
|
||||
<li>Restart with <code>--web-dir apps/interfaces/web/dist</code></li>
|
||||
</ol>
|
||||
</body></html>"#.to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Serve static files from the configured directory.
|
||||
async fn static_handler(
|
||||
axum::extract::State(state): axum::extract::State<Arc<WebState>>,
|
||||
path: axum::extract::Path<String>,
|
||||
) -> Response {
|
||||
let file_path = state.static_dir.join(path.0);
|
||||
// Security: prevent directory traversal
|
||||
let canonical = match file_path.canonicalize() {
|
||||
Ok(p) => p,
|
||||
Err(_) => return (StatusCode::NOT_FOUND, "Not found").into_response(),
|
||||
};
|
||||
if !canonical.starts_with(&state.static_dir) {
|
||||
return (StatusCode::FORBIDDEN, "Forbidden").into_response();
|
||||
}
|
||||
|
||||
match fs::read(&canonical).await {
|
||||
Ok(data) => {
|
||||
let mime = mime_guess::from_path(&canonical).first_or_octet_stream();
|
||||
Response::builder()
|
||||
.status(200)
|
||||
.header("Content-Type", mime.to_string())
|
||||
.body(axum::body::Body::from(data))
|
||||
.unwrap()
|
||||
.into_response()
|
||||
}
|
||||
Err(_) => (StatusCode::NOT_FOUND, "Not found").into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Run the web frontend server.
|
||||
pub async fn run_server(port: u16, static_dir: Option<PathBuf>) -> anyhow::Result<()> {
|
||||
let dir = static_dir.unwrap_or_else(|| {
|
||||
let p = PathBuf::from("apps/interfaces/web/dist");
|
||||
if p.exists() {
|
||||
p
|
||||
} else {
|
||||
warn!("No static dir found at {:?}, using current dir", p);
|
||||
PathBuf::from(".")
|
||||
}
|
||||
});
|
||||
let state = Arc::new(WebState { static_dir: dir });
|
||||
let app = build_router(state);
|
||||
let addr = std::net::SocketAddr::from(([0, 0, 0, 0], port));
|
||||
info!("Web frontend server listening on http://{addr}");
|
||||
let listener = tokio::net::TcpListener::bind(addr).await?;
|
||||
axum::serve(listener, app).await?;
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user