Skip to main content

oxibonsai_runtime/
web_ui.rs

1//! Web UI module — serves a minimal HTML chat interface from the Axum server.
2//!
3//! # Endpoints
4//!
5//! | Method | Path | Description |
6//! |--------|------|-------------|
7//! | `GET` | `/ui` | Serves the embedded [`CHAT_UI_HTML`] page |
8//! | `GET` | `/ui/health` | Returns `{"status":"ok"}` |
9//!
10//! The HTML is embedded at compile time via [`include_str!`] from
11//! `assets/chat.html`.  No runtime file I/O is required.
12//!
13//! # Example
14//!
15//! ```rust,no_run
16//! use oxibonsai_runtime::web_ui::create_ui_router;
17//!
18//! let router = create_ui_router();
19//! // Merge into an existing Axum router:
20//! // let app = existing_router.merge(router);
21//! ```
22
23use axum::{
24    http::{header, StatusCode},
25    response::{IntoResponse, Response},
26    routing::get,
27    Router,
28};
29
30// ─── Embedded HTML ────────────────────────────────────────────────────────────
31
32/// The single-page chat UI, embedded from `assets/chat.html` at compile time.
33///
34/// This is a pure HTML/CSS/JS application with no external CDN dependencies.
35/// It communicates with the inference server via `POST /v1/chat/completions`.
36pub const CHAT_UI_HTML: &str = include_str!("../assets/chat.html");
37
38// ─── Handlers ─────────────────────────────────────────────────────────────────
39
40/// Serve the embedded chat UI as `text/html; charset=utf-8`.
41async fn serve_chat_ui() -> Response {
42    (
43        StatusCode::OK,
44        [(header::CONTENT_TYPE, "text/html; charset=utf-8")],
45        CHAT_UI_HTML,
46    )
47        .into_response()
48}
49
50/// Liveness probe for the UI sub-router.
51///
52/// Returns `200 OK` with body `{"status":"ok"}`.
53async fn ui_health() -> Response {
54    (
55        StatusCode::OK,
56        [(header::CONTENT_TYPE, "application/json")],
57        r#"{"status":"ok"}"#,
58    )
59        .into_response()
60}
61
62// ─── Router ───────────────────────────────────────────────────────────────────
63
64/// Build the UI sub-router.
65///
66/// Routes:
67/// - `GET /ui`        → `serve_chat_ui`
68/// - `GET /ui/health` → `ui_health`
69///
70/// Merge this into an existing [`axum::Router`] with
71/// [`Router::merge`](axum::Router::merge).
72pub fn create_ui_router() -> Router {
73    Router::new()
74        .route("/ui", get(serve_chat_ui))
75        .route("/ui/health", get(ui_health))
76}
77
78// ─── Unit tests ───────────────────────────────────────────────────────────────
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83
84    #[test]
85    fn chat_ui_html_is_nonempty() {
86        assert!(!CHAT_UI_HTML.is_empty(), "CHAT_UI_HTML must not be empty");
87    }
88
89    #[test]
90    fn chat_ui_html_contains_doctype() {
91        assert!(
92            CHAT_UI_HTML.contains("<!DOCTYPE html>"),
93            "HTML should start with DOCTYPE"
94        );
95    }
96
97    #[test]
98    fn chat_ui_html_contains_fetch() {
99        assert!(
100            CHAT_UI_HTML.contains("fetch"),
101            "HTML should use fetch() for API calls"
102        );
103    }
104
105    #[test]
106    fn create_ui_router_does_not_panic() {
107        let _router = create_ui_router();
108    }
109}