adk_server/
web_ui.rs

1use axum::{
2    body::Body,
3    extract::State,
4    http::{header, StatusCode, Uri},
5    response::IntoResponse,
6    Json,
7};
8use rust_embed::RustEmbed;
9use serde::Serialize;
10
11#[derive(RustEmbed)]
12#[folder = "assets/webui"]
13struct Assets;
14
15#[derive(Serialize)]
16pub struct RuntimeConfig {
17    #[serde(rename = "backendUrl")]
18    pub backend_url: String,
19}
20
21pub async fn serve_runtime_config(State(config): State<crate::ServerConfig>) -> impl IntoResponse {
22    // Use configured backend URL or default to relative "/api"
23    // Relative URLs work better as they adapt to the actual host/port
24    let backend_url = config.backend_url.unwrap_or_else(|| "/api".to_string());
25
26    Json(RuntimeConfig { backend_url })
27}
28
29pub async fn serve_ui_assets(uri: Uri) -> impl IntoResponse {
30    let mut path = uri.path().trim_start_matches("/ui/").to_string();
31
32    if path.is_empty() {
33        path = "index.html".to_string();
34    }
35
36    match Assets::get(&path) {
37        Some(content) => {
38            let mime = mime_guess::from_path(&path).first_or_octet_stream();
39            let mime_header = header::HeaderValue::from_str(mime.as_ref())
40                .unwrap_or_else(|_| header::HeaderValue::from_static("application/octet-stream"));
41            ([(header::CONTENT_TYPE, mime_header)], Body::from(content.data)).into_response()
42        }
43        None => {
44            // If file not found, serve index.html for SPA routing (if we were doing that),
45            // but for static assets, 404 is correct.
46            // However, Angular apps often use HTML5 pushState, so we might need to fallback to index.html
47            // for non-asset paths.
48            // Let's check if it looks like a file extension.
49            if path.contains('.') {
50                StatusCode::NOT_FOUND.into_response()
51            } else {
52                // Fallback to index.html
53                match Assets::get("index.html") {
54                    Some(content) => {
55                        let mime = mime_guess::from_path("index.html").first_or_octet_stream();
56                        let mime_header = header::HeaderValue::from_str(mime.as_ref())
57                            .unwrap_or_else(|_| header::HeaderValue::from_static("text/html"));
58                        ([(header::CONTENT_TYPE, mime_header)], Body::from(content.data))
59                            .into_response()
60                    }
61                    None => StatusCode::NOT_FOUND.into_response(),
62                }
63            }
64        }
65    }
66}
67
68pub async fn root_redirect() -> impl IntoResponse {
69    axum::response::Redirect::to("/ui/")
70}
71
72pub async fn serve_ui_index() -> impl IntoResponse {
73    match Assets::get("index.html") {
74        Some(content) => {
75            let mime_header = header::HeaderValue::from_static("text/html; charset=utf-8");
76            ([(header::CONTENT_TYPE, mime_header)], Body::from(content.data)).into_response()
77        }
78        None => StatusCode::NOT_FOUND.into_response(),
79    }
80}