Compare commits

..

No commits in common. "75d19433be36b89de8de7b0b1eb7661094856df5" and "206c0d4e573ca7c137b0c06069de9477580bfb33" have entirely different histories.

11 changed files with 319 additions and 462 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 - maud vs hypertext vs minijinja
- try porting to hypertext::maud ! - try porting to hypertext::maud !
* Todos * Todos
** [-] Starter boilerplate ** Starter boilerplate
*** [X] Server: Axum *** [X] Server: Axum
**** [X] Typed routes via macro **** [X] Typed routes via macro
**** [X] Nested Router **** [X] Nested Router
@ -11,7 +11,7 @@
*** [X] "html tmpl": minijinja *** [X] "html tmpl": minijinja
- only index yaml for now, test in app wether inline or external templates feel better - only index yaml for now, test in app wether inline or external templates feel better
*** [ ] CSS Basic *** [ ] CSS Basic
- [X] UnoCSS - UnoCSS
- daisyUI? - daisyUI?
*** [ ] UI framework *** [ ] UI framework
- data* js basic - data* js basic
@ -19,28 +19,14 @@
*** [ ] Asset bundle testing *** [ ] Asset bundle testing
*** [ ] DB *** [ ] DB
- SqlX/SurrealDB? - SqlX/SurrealDB?
*** [ ] Build scripts ** Basic streaming chat
- [ ] 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 - build with inline html via maud
- add messaging foo?
*** Stream same token in loop *** Stream same token in loop
*** ??? *** ???
*** Finish *** Finish
** [ ] Basic Proxy Settings ** Basic Proxy Settings
- build with jinja templates - build with jinja templates
** [ ] Markdown streaming chat ** Markdown streaming chat
*** Moar Feats *** Moar Feats
**** Tauri webview/wry? **** Tauri webview/wry?
**** Jinja tmplts for models ?? **** Jinja tmplts for models ??

View file

@ -1,61 +0,0 @@
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,17 +1,103 @@
mod assets;
mod ui;
use std::sync::Once; use std::sync::Once;
use assets::AssetsController;
use axum::{ use axum::{
http::{StatusCode, Uri}, body::Body,
extract::State,
http::{header, Response, StatusCode, Uri},
response::IntoResponse, response::IntoResponse,
routing::get, routing::get,
}; };
use axum_controller::*;
use hypertext::{maud, GlobalAttributes, Renderable}; use hypertext::{maud, GlobalAttributes, Renderable};
use ui::UiController; use rust_embed::RustEmbed;
use crate::ui::html_elements; 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)
}
}
fn markup_404(uri: String) -> impl Renderable { fn markup_404(uri: String) -> impl Renderable {
maud! { maud! {
@ -49,6 +135,35 @@ async fn handle_405() -> impl IntoResponse {
) )
.into_response() .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() { pub fn initialize_logger() {
static INIT: Once = Once::new(); static INIT: Once = Once::new();
@ -82,7 +197,7 @@ async fn main() {
let router: axum::Router = axum::Router::new() let router: axum::Router = axum::Router::new()
.merge(UiController::into_router(app_state.clone())) .merge(UiController::into_router(app_state.clone()))
.merge(AssetsController::into_router(app_state.clone())) .merge(DistController::into_router(app_state.clone()))
.fallback_service(get(handle_404)) .fallback_service(get(handle_404))
.method_not_allowed_fallback(handle_405) .method_not_allowed_fallback(handle_405)
.with_state(app_state); .with_state(app_state);

View file

@ -1,24 +0,0 @@
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)
}
}
}
}

View file

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

View file

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

View file

@ -1,16 +1,16 @@
[toolchain] [toolchain]
channel = "nightly-2025-03-01" channel = "nightly-2025-03-01"
components = [ components = [
"cargo", "cargo",
"rust-analyzer", "rust-analyzer",
"rust-src", "rust-src",
"rustc-codegen-cranelift", "rustc-codegen-cranelift",
"rustc-dev", "rustc-dev",
"rustfmt", "rustfmt",
] ]
profile = "default" profile = "default"
targets = [ targets = [
"wasm32-unknown-unknown", "wasm32-unknown-unknown",
"x86_64-pc-windows-msvc", "x86_64-pc-windows-msvc",
"x86_64-unknown-linux-gnu", "x86_64-unknown-linux-gnu",
] ]

View file

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