Compare commits

...

2 commits

Author SHA1 Message Date
75d19433be
Remove panics 2025-03-18 00:55:37 +01:00
4861b479a7
Modularize darm test a bit 2025-03-18 00:55:28 +01:00
11 changed files with 462 additions and 319 deletions

431
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -2,7 +2,7 @@
- maud vs hypertext vs minijinja
- try porting to hypertext::maud !
* Todos
** Starter boilerplate
** [-] Starter boilerplate
*** [X] Server: Axum
**** [X] Typed routes via macro
**** [X] Nested Router
@ -11,7 +11,7 @@
*** [X] "html tmpl": minijinja
- only index yaml for now, test in app wether inline or external templates feel better
*** [ ] CSS Basic
- UnoCSS
- [X] UnoCSS
- daisyUI?
*** [ ] UI framework
- data* js basic
@ -19,14 +19,28 @@
*** [ ] Asset bundle testing
*** [ ] DB
- SqlX/SurrealDB?
** Basic streaming chat
*** [ ] Build scripts
- [ ] remove npm ?
- [ ] add fish or cargo make scripts ?
- [ ] features:
- [ ] make dev
- make js?
- "bundle"/download data* ?
- make css
- make rust app
- [ ] make watch-dev
- [ ] make prod
** [X] Chat mockup
** [X] Streaming chat with data*
** [ ] Basic streaming chat
- build with inline html via maud
- add messaging foo?
*** Stream same token in loop
*** ???
*** Finish
** Basic Proxy Settings
** [ ] Basic Proxy Settings
- build with jinja templates
** Markdown streaming chat
** [ ] Markdown streaming chat
*** Moar Feats
**** Tauri webview/wry?
**** Jinja tmplts for models ??

61
darm_test/src/assets.rs Normal file
View file

@ -0,0 +1,61 @@
use axum::{
body::Body,
extract::State,
http::{header, Response, StatusCode},
response::IntoResponse,
};
use axum_controller::*;
use hypertext::{maud, GlobalAttributes, Renderable};
use rust_embed::RustEmbed;
use crate::{ui::html_elements, AppState};
pub struct AxumEmbedAsset<T>(pub T);
impl<T> IntoResponse for AxumEmbedAsset<T>
where
T: Into<String>,
{
fn into_response(self) -> Response<Body> {
let path = self.0.into();
tracing::debug!(?path);
#[derive(RustEmbed)]
#[folder = "public/"]
struct EmbedAsset;
fn markup_404(uri: String) -> impl Renderable {
maud! {
h1 { "404" }
p { (uri) " Not Found" }
@for i in 0..5 {
div .{"m-" (i)} { (i) }
}
}
}
match EmbedAsset::get(path.as_str()) {
Some(content) => {
let mime = mime_guess::from_path(path).first_or_octet_stream();
([(header::CONTENT_TYPE, mime.as_ref())], content.data).into_response()
}
None => (
StatusCode::NOT_FOUND,
axum::response::Html(markup_404(path).render()),
)
.into_response(),
}
}
}
pub struct AssetsController;
#[controller(state=AppState, path="/dist")]
impl AssetsController {
#[route(GET "/*path")]
async fn static_handler(path: String, _: State<AppState>) -> impl IntoResponse {
let path = path.trim_start_matches('/').to_string();
AxumEmbedAsset(path)
}
}

View file

@ -1,103 +1,17 @@
mod assets;
mod ui;
use std::sync::Once;
use assets::AssetsController;
use axum::{
body::Body,
extract::State,
http::{header, Response, StatusCode, Uri},
http::{StatusCode, Uri},
response::IntoResponse,
routing::get,
};
use axum_controller::*;
use hypertext::{maud, GlobalAttributes, Renderable};
use rust_embed::RustEmbed;
use ui::UiController;
mod html_elements {
use hypertext::elements;
pub use hypertext::html_elements::*;
elements! {
bla {
blub
}
my_element {
my_attribute
}
}
}
struct UiController {}
#[controller(
state = AppState
)]
impl UiController {
#[route(GET "/")]
async fn index(State(_): State<AppState>) -> impl IntoResponse {
maud! {
html lang="en" {
head {
meta charset="UTF-8";
meta name="viewport" content="width=device-width, initial-scale=1.0";
title {
"LLM Chat App"
}
script type="module" src="/dist/datastar.min.js" {}
link rel="stylesheet" href="/dist/styles.min.css";
link rel="icon" href="/dist/favicon.ico";
}
body class="bg-gray-100" {
div class="container mx-auto p-4" {
h1 class="text-2xl font-bold mb-4" {
"LLM Chat App"
}
div class="bg-white p-6 rounded-lg shadow-md" {
div id="chat" class="mb-4" {
// Chat messages will appear here
}
form id="chat-form" {
textarea id="user-input" class="w-full p-2 border rounded-lg mb-2" placeholder="Type your message..." {}
button type="submit" class="bg-blue-500 text-white p-2 rounded-lg" {
"Send"
}
}
}
}
script {
(hypertext::Raw("
console.log(\"asd\");
document.getElementById('chat-form').addEventListener('submit', function(event) {
event.preventDefault();
const userInput = document.getElementById('user-input').value;
const chatContainer = document.getElementById('chat');
chatContainer.innerHTML += `<div class='mb-2'><strong>You:</strong> ${userInput}</div>`;
document.getElementById('user-input').value = '';
console.log(\"asd\");
// Mock response from LLM
setTimeout(() => {
chatContainer.innerHTML += `<div class='mb-2'><strong>LLM:</strong> This is a mock response.</div>`;
}, 1000);
});
"))
}
}
}
}
.render()
}
}
struct DistController;
#[controller(state=AppState, path="/dist")]
impl DistController {
#[route(GET "/*path")]
async fn static_handler(path: String, _: State<AppState>) -> impl IntoResponse {
let path = path.trim_start_matches('/').to_string();
StaticFile(path)
}
}
use crate::ui::html_elements;
fn markup_404(uri: String) -> impl Renderable {
maud! {
@ -135,35 +49,6 @@ async fn handle_405() -> impl IntoResponse {
)
.into_response()
}
pub struct StaticFile<T>(pub T);
impl<T> IntoResponse for StaticFile<T>
where
T: Into<String>,
{
fn into_response(self) -> Response<Body> {
let path = self.0.into();
tracing::debug!(?path);
#[derive(RustEmbed)]
#[folder = "public/"]
struct Asset;
match Asset::get(path.as_str()) {
Some(content) => {
let mime = mime_guess::from_path(path).first_or_octet_stream();
([(header::CONTENT_TYPE, mime.as_ref())], content.data).into_response()
}
None => (
StatusCode::NOT_FOUND,
axum::response::Html(markup_404(path).render()),
)
.into_response(),
}
}
}
pub fn initialize_logger() {
static INIT: Once = Once::new();
@ -197,7 +82,7 @@ async fn main() {
let router: axum::Router = axum::Router::new()
.merge(UiController::into_router(app_state.clone()))
.merge(DistController::into_router(app_state.clone()))
.merge(AssetsController::into_router(app_state.clone()))
.fallback_service(get(handle_404))
.method_not_allowed_fallback(handle_405)
.with_state(app_state);

View file

@ -0,0 +1,24 @@
use hypertext::{maud, GlobalAttributes, Renderable};
use crate::ui::html_elements;
pub fn main_page(body: impl Renderable) -> impl Renderable {
maud! {
html lang="en" {
head {
meta charset="UTF-8";
meta name="viewport" content="width=device-width, initial-scale=1.0";
title {
"LLM Chat App"
}
script type="module" src="/dist/datastar.min.js" {}
link rel="stylesheet" href="/dist/styles.min.css";
link rel="icon" href="/dist/favicon.ico";
}
body class="bg-gray-100" {
(body)
}
}
}
}

69
darm_test/src/ui/mod.rs Normal file
View file

@ -0,0 +1,69 @@
mod components;
use axum::{extract::State, response::IntoResponse};
use axum_controller::*;
use hypertext::{maud, GlobalAttributes, Renderable};
use crate::{ui::components::*, AppState};
pub mod html_elements {
use hypertext::elements;
pub use hypertext::html_elements::*;
elements! {
bla {
blub
}
my_element {
my_attribute
}
}
}
pub struct UiController {}
#[controller(
state = AppState
)]
impl UiController {
#[route(GET "/")]
async fn index(State(_): State<AppState>) -> impl IntoResponse {
main_page(maud! {
div class="container mx-auto p-4" {
h1 class="text-2xl font-bold mb-4" {
"LLM Chat App"
}
div class="bg-white p-6 rounded-lg shadow-md" {
div id="chat" class="mb-4" {
// Chat messages will appear here
}
form id="chat-form" {
textarea id="user-input" class="w-full p-2 border rounded-lg mb-2" placeholder="Type your message..." {}
button type="submit" class="bg-blue-500 text-white p-2 rounded-lg" {
"Send"
}
}
}
}
script {
(hypertext::Raw("
console.log(\"asd\");
document.getElementById('chat-form').addEventListener('submit', function(event) {
event.preventDefault();
const userInput = document.getElementById('user-input').value;
const chatContainer = document.getElementById('chat');
chatContainer.innerHTML += `<div class='mb-2'><strong>You:</strong> ${userInput}</div>`;
document.getElementById('user-input').value = '';
console.log(\"asd\");
// Mock response from LLM
setTimeout(() => {
chatContainer.innerHTML += `<div class='mb-2'><strong>LLM:</strong> This is a mock response.</div>`;
}, 1000);
});
"))
}
}
)
.render()
}
}

View file

@ -98,7 +98,7 @@ async fn main() -> anyhow::Result<()> {
let proxy_man_fut = async move {
use llama_proxy_man::{config::AppConfig, start_server};
let config = AppConfig::default_figment();
start_server(config).await;
start_server(config).await?;
Ok::<(), anyhow::Error>(())
};

View file

@ -51,7 +51,7 @@ pub fn axum_router(spec: &ModelSpec, state: AppState) -> Router {
}
/// Starts an inference server for each model defined in the config.
pub async fn start_server(config: AppConfig) {
pub async fn start_server(config: AppConfig) -> anyhow::Result<()> {
let state = AppState::from_config(config.clone());
let mut handles = Vec::new();
@ -59,17 +59,17 @@ pub async fn start_server(config: AppConfig) {
let state = state.clone();
let spec = spec.clone();
let handle = tokio::spawn(async move {
let handle: tokio::task::JoinHandle<anyhow::Result<()>> = tokio::spawn(async move {
let app = axum_router(&spec, state);
let addr = SocketAddr::from(([0, 0, 0, 0], spec.port));
tracing::info!(msg = "Listening", ?spec);
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
axum::serve(listener, app.into_make_service())
.await
.unwrap();
let listener = tokio::net::TcpListener::bind(&addr).await?;
axum::serve(listener, app.into_make_service()).await?;
Ok(())
});
handles.push(handle);
}
futures::future::join_all(handles).await;
let _ = futures::future::try_join_all(handles).await?;
Ok(())
}

View file

@ -1,10 +1,13 @@
use anyhow;
use llama_proxy_man::{config::AppConfig, logging, start_server};
#[tokio::main]
async fn main() {
async fn main() -> anyhow::Result<()> {
logging::initialize_logger();
let config = AppConfig::default_from_pwd_yml();
start_server(config).await;
start_server(config).await?;
Ok(())
}

View file

@ -1,5 +1,5 @@
edition = "2021"
edition = "2024"
max_width = 100
tab_spaces = 4