batuta/serve/banco/
handlers_ui.rs1use axum::{
8 extract::State,
9 http::{header, StatusCode},
10 response::{Html, IntoResponse},
11 Form,
12};
13use serde::Deserialize;
14
15use super::state::BancoState;
16
17pub async fn index_handler(State(state): State<BancoState>) -> Html<String> {
19 Html(render_chat_page(&state, None, None))
20}
21
22#[derive(Deserialize)]
24pub struct ChatFormData {
25 pub message: String,
26}
27
28pub async fn chat_form_handler(
30 State(state): State<BancoState>,
31 Form(form): Form<ChatFormData>,
32) -> Html<String> {
33 let prompt = form.message.trim().to_string();
34 if prompt.is_empty() {
35 return Html(render_chat_page(&state, None, None));
36 }
37
38 let request = super::types::BancoChatRequest {
40 model: None,
41 messages: vec![crate::serve::templates::ChatMessage::user(&prompt)],
42 max_tokens: 256,
43 temperature: 0.7,
44 top_p: 1.0,
45 stream: false,
46 conversation_id: None,
47 response_format: None,
48 rag: false,
49 rag_config: None,
50 attachments: vec![],
51 tools: None,
52 tool_choice: None,
53 };
54
55 let response = super::handlers_inference::try_inference(&state, &request)
56 .map(|(text, _, _)| text)
57 .unwrap_or_else(|| {
58 if state.model.is_loaded() {
59 "Model loaded but inference unavailable. Build with --features banco.".to_string()
60 } else {
61 "No model loaded. Use: POST /api/v1/models/load {\"model\": \"./model.gguf\"}"
62 .to_string()
63 }
64 });
65
66 Html(render_chat_page(&state, Some(&prompt), Some(&response)))
67}
68
69fn render_chat_page(state: &BancoState, prompt: Option<&str>, response: Option<&str>) -> String {
71 let model_status = if let Some(info) = state.model.info() {
72 format!("<span style='color:#4caf50'>●</span> {}", info.model_id)
73 } else {
74 "<span style='color:#f44336'>●</span> No model".to_string()
75 };
76
77 let messages_html = match (prompt, response) {
78 (Some(p), Some(r)) => format!(
79 "<div class='msg user'>{}</div><div class='msg assistant'>{}</div>",
80 html_escape(p),
81 html_escape(r)
82 ),
83 _ => "<div class='msg system'>Send a message to start chatting.</div>".to_string(),
84 };
85
86 format!(
87 r##"<!DOCTYPE html>
88<html lang="en">
89<head>
90<meta charset="utf-8">
91<meta name="viewport" content="width=device-width, initial-scale=1">
92<title>Banco — Local AI Workbench</title>
93<style>
94*{{margin:0;padding:0;box-sizing:border-box}}
95:root{{--bg:#1a1a2e;--surface:#16213e;--border:#0f3460;--accent:#e94560;--text:#e0e0e0;--dim:#888}}
96body{{font-family:-apple-system,system-ui,sans-serif;background:var(--bg);color:var(--text);height:100vh;display:flex;flex-direction:column}}
97header{{background:var(--surface);padding:12px 20px;display:flex;align-items:center;gap:12px;border-bottom:1px solid var(--border)}}
98header h1{{font-size:16px;font-weight:600;color:var(--accent)}}
99.model-info{{font-size:12px;color:var(--dim)}}
100.messages{{flex:1;overflow-y:auto;padding:20px;display:flex;flex-direction:column;gap:8px}}
101.msg{{padding:10px 14px;border-radius:8px;max-width:80%;line-height:1.5;white-space:pre-wrap;word-wrap:break-word}}
102.msg.user{{align-self:flex-end;background:var(--accent);color:white}}
103.msg.assistant{{align-self:flex-start;background:var(--surface);border:1px solid var(--border)}}
104.msg.system{{align-self:center;font-size:12px;color:var(--dim);font-style:italic}}
105.chat-form{{display:flex;gap:8px;padding:12px 20px;background:var(--surface);border-top:1px solid var(--border)}}
106.chat-form input[type=text]{{flex:1;padding:10px 14px;border:1px solid var(--border);border-radius:8px;background:var(--bg);color:var(--text);font-size:14px}}
107.chat-form button{{padding:10px 20px;background:var(--accent);color:white;border:none;border-radius:8px;cursor:pointer;font-size:14px}}
108.chat-form button:hover{{opacity:0.9}}
109footer{{padding:8px 20px;font-size:11px;color:var(--dim);background:var(--surface);border-top:1px solid var(--border);text-align:center}}
110footer a{{color:var(--accent);text-decoration:none}}
111</style>
112</head>
113<body>
114<header>
115 <h1>Banco</h1>
116 <span class="model-info">{model_status}</span>
117</header>
118<div class="messages">{messages_html}</div>
119<form class="chat-form" method="POST" action="/ui/chat">
120 <input name="message" type="text" placeholder="Type a message..." autocomplete="off" autofocus>
121 <button type="submit">Send</button>
122</form>
123<footer>
124 Zero-JS UI · API: <a href="/api/v1/system">/api/v1/system</a> ·
125 Chat: POST <a href="/api/v1/chat/completions">/api/v1/chat/completions</a>
126</footer>
127</body>
128</html>"##,
129 model_status = model_status,
130 messages_html = messages_html
131 )
132}
133
134fn html_escape(s: &str) -> String {
136 s.replace('&', "&").replace('<', "<").replace('>', ">").replace('"', """)
137}
138
139pub async fn assets_handler(
142 axum::extract::Path(path): axum::extract::Path<String>,
143) -> impl IntoResponse {
144 let _ = path;
145 (
146 StatusCode::NOT_FOUND,
147 [(header::CONTENT_TYPE, "text/plain")],
148 "Asset not found — UI is self-contained in index.html",
149 )
150}