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}