From 2b4a8a9df826a88b6cd4a91cd1d454b431ccb197 Mon Sep 17 00:00:00 2001 From: geoffsee <> Date: Sun, 31 Aug 2025 18:18:56 -0400 Subject: [PATCH] chat-ui not functional yet but builds --- .gitignore | 1 + Cargo.lock | 77 +-- Cargo.toml | 9 +- crates/{leptos-app => chat-ui}/.gitignore | 0 crates/{leptos-app => chat-ui}/Cargo.toml | 44 +- crates/chat-ui/LICENSE | 24 + crates/chat-ui/README.md | 2 + .../end2end/.gitignore | 0 .../end2end/package.json | 0 .../end2end/playwright.config.ts | 0 .../end2end/tests/example.spec.ts | 0 .../end2end/tsconfig.json | 0 .../public/favicon.ico | Bin crates/chat-ui/src/app.rs | 104 ++++ crates/chat-ui/src/lib.rs | 9 + crates/chat-ui/src/main.rs | 29 + .../{leptos-app => chat-ui}/style/main.scss | 0 crates/leptos-app/.cargo/config.toml | 3 - crates/leptos-app/Dockerfile | 21 - crates/leptos-app/src/app.rs | 520 ------------------ crates/leptos-app/src/lib.rs | 42 -- crates/leptos-app/src/main.rs | 38 -- crates/predict-otron-9000/Cargo.toml | 6 +- crates/predict-otron-9000/src/main.rs | 53 +- scripts/build_ui.sh | 14 + scripts/run.sh | 17 + scripts/run_server.sh | 7 - 27 files changed, 278 insertions(+), 742 deletions(-) rename crates/{leptos-app => chat-ui}/.gitignore (100%) rename crates/{leptos-app => chat-ui}/Cargo.toml (78%) create mode 100644 crates/chat-ui/LICENSE create mode 100644 crates/chat-ui/README.md rename crates/{leptos-app => chat-ui}/end2end/.gitignore (100%) rename crates/{leptos-app => chat-ui}/end2end/package.json (100%) rename crates/{leptos-app => chat-ui}/end2end/playwright.config.ts (100%) rename crates/{leptos-app => chat-ui}/end2end/tests/example.spec.ts (100%) rename crates/{leptos-app => chat-ui}/end2end/tsconfig.json (100%) rename crates/{leptos-app => chat-ui}/public/favicon.ico (100%) create mode 100644 crates/chat-ui/src/app.rs create mode 100644 crates/chat-ui/src/lib.rs create mode 100644 crates/chat-ui/src/main.rs rename crates/{leptos-app => chat-ui}/style/main.scss (100%) delete mode 100644 crates/leptos-app/.cargo/config.toml delete mode 100644 crates/leptos-app/Dockerfile delete mode 100644 crates/leptos-app/src/app.rs delete mode 100644 crates/leptos-app/src/lib.rs delete mode 100644 crates/leptos-app/src/main.rs create mode 100755 scripts/build_ui.sh create mode 100755 scripts/run.sh delete mode 100755 scripts/run_server.sh diff --git a/.gitignore b/.gitignore index c55b385..4896b72 100644 --- a/.gitignore +++ b/.gitignore @@ -78,3 +78,4 @@ venv/ /scripts/cli !/scripts/cli.ts /**/.*.bun-build +/AGENTS.md diff --git a/Cargo.lock b/Cargo.lock index 9486d27..5cbc50a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -247,30 +247,6 @@ dependencies = [ "syn 2.0.106", ] -[[package]] -name = "async-openai-wasm" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee2d154d8b82bf6a5786a9bb77f9daa5053318564d15f1c01ad2536e613a0055" -dependencies = [ - "async-openai-macros", - "base64 0.22.1", - "bytes", - "derive_builder", - "eventsource-stream", - "futures", - "getrandom 0.3.3", - "pin-project", - "rand 0.9.2", - "reqwest", - "reqwest-eventsource", - "secrecy", - "serde", - "serde_json", - "thiserror 2.0.15", - "tracing", -] - [[package]] name = "async-trait" version = "0.1.89" @@ -845,6 +821,20 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chat-ui" +version = "0.1.0" +dependencies = [ + "axum", + "console_error_panic_hook", + "leptos", + "leptos_axum", + "leptos_meta", + "leptos_router", + "tokio", + "wasm-bindgen", +] + [[package]] name = "clang-sys" version = "1.8.1" @@ -2939,28 +2929,6 @@ dependencies = [ "web-sys", ] -[[package]] -name = "leptos-app" -version = "0.1.2" -dependencies = [ - "async-openai-wasm", - "axum", - "console_error_panic_hook", - "either", - "futures-util", - "js-sys", - "leptos", - "leptos_axum", - "leptos_meta", - "leptos_router", - "serde", - "serde_json", - "tokio", - "uuid", - "wasm-bindgen", - "web-sys", -] - [[package]] name = "leptos_axum" version = "0.8.6" @@ -4086,9 +4054,10 @@ name = "predict-otron-9000" version = "0.1.2" dependencies = [ "axum", + "chat-ui", "embeddings-engine", "inference-engine", - "leptos-app", + "log", "mime_guess", "reqwest", "rust-embed", @@ -4839,6 +4808,7 @@ version = "8.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "025908b8682a26ba8d12f6f2d66b987584a4a87bc024abc5bbc12553a8cd178a" dependencies = [ + "axum", "rust-embed-impl", "rust-embed-utils", "walkdir", @@ -6463,22 +6433,9 @@ checksum = "f33196643e165781c20a5ead5582283a7dacbb87855d867fbc2df3f81eddc1be" dependencies = [ "getrandom 0.3.3", "js-sys", - "rand 0.9.2", - "uuid-macro-internal", "wasm-bindgen", ] -[[package]] -name = "uuid-macro-internal" -version = "1.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22b7ad00068276db5fea436dba78daa7891b8d60db76e4f51cbdefbdecdab97e" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - [[package]] name = "v_frame" version = "0.3.9" diff --git a/Cargo.toml b/Cargo.toml index f762dfd..9c50e08 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,11 +3,11 @@ members = [ "crates/predict-otron-9000", "crates/inference-engine", "crates/embeddings-engine", - "crates/leptos-app", "crates/helm-chart-tool", "crates/llama-runner", "crates/gemma-runner", - "crates/cli" + "crates/cli", + "crates/chat-ui" ] default-members = ["crates/predict-otron-9000"] resolver = "2" @@ -42,8 +42,3 @@ overflow-checks = true opt-level = 3 debug = true lto = "thin" - -[[workspace.metadata.leptos]] -# project name -bin-package = "leptos-app" -lib-package = "leptos-app" diff --git a/crates/leptos-app/.gitignore b/crates/chat-ui/.gitignore similarity index 100% rename from crates/leptos-app/.gitignore rename to crates/chat-ui/.gitignore diff --git a/crates/leptos-app/Cargo.toml b/crates/chat-ui/Cargo.toml similarity index 78% rename from crates/leptos-app/Cargo.toml rename to crates/chat-ui/Cargo.toml index b9bc214..4958398 100644 --- a/crates/leptos-app/Cargo.toml +++ b/crates/chat-ui/Cargo.toml @@ -1,6 +1,6 @@ [package] -name = "leptos-app" -version.workspace = true +name = "chat-ui" +version = "0.1.0" edition = "2021" [lib] @@ -16,44 +16,11 @@ leptos_meta = { version = "0.8.0" } tokio = { version = "1", features = ["rt-multi-thread"], optional = true } wasm-bindgen = { version = "=0.2.100", optional = true } -# Chat interface dependencies -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" -async-openai-wasm = { version = "0.29", default-features = false } -futures-util = "0.3" -js-sys = { version = "0.3", optional = true } -either = { version = "1.9", features = ["serde"] } - -web-sys = { version = "0.3", optional = true, features = [ - "console", - "Window", - "Document", - "Element", - "HtmlElement", - "HtmlInputElement", - "HtmlSelectElement", - "HtmlTextAreaElement", - "Event", - "EventTarget", - "KeyboardEvent", -] } - -[dependencies.uuid] -version = "1.0" -features = [ - "v4", - "fast-rng", - "macro-diagnostics", - "js", -] - [features] hydrate = [ "leptos/hydrate", "dep:console_error_panic_hook", "dep:wasm-bindgen", - "dep:js-sys", - "dep:web-sys", ] ssr = [ "dep:axum", @@ -73,8 +40,9 @@ codegen-units = 1 panic = "abort" [package.metadata.leptos] +name = "chat-ui" # The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name -output-name = "leptos-app" +output-name = "chat-ui" # The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup. site-root = "target/site" @@ -84,7 +52,7 @@ site-root = "target/site" site-pkg-dir = "pkg" # [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to //app.css -style-file = "style/main.scss" +style-file = "./style/main.scss" # Assets source dir. All files found here will be copied and synchronized to site-root. # The assets-dir cannot have a sub directory with the same name/path as site-pkg-dir. # @@ -132,4 +100,4 @@ lib-default-features = false # The profile to use for the lib target when compiling for release # # Optional. Defaults to "release". -lib-profile-release = "wasm-release" +lib-profile-release = "release" diff --git a/crates/chat-ui/LICENSE b/crates/chat-ui/LICENSE new file mode 100644 index 0000000..fdddb29 --- /dev/null +++ b/crates/chat-ui/LICENSE @@ -0,0 +1,24 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to diff --git a/crates/chat-ui/README.md b/crates/chat-ui/README.md new file mode 100644 index 0000000..63181d1 --- /dev/null +++ b/crates/chat-ui/README.md @@ -0,0 +1,2 @@ +# chat-ui +This is served by the predict-otron-9000 server. This needs to be built before the server. \ No newline at end of file diff --git a/crates/leptos-app/end2end/.gitignore b/crates/chat-ui/end2end/.gitignore similarity index 100% rename from crates/leptos-app/end2end/.gitignore rename to crates/chat-ui/end2end/.gitignore diff --git a/crates/leptos-app/end2end/package.json b/crates/chat-ui/end2end/package.json similarity index 100% rename from crates/leptos-app/end2end/package.json rename to crates/chat-ui/end2end/package.json diff --git a/crates/leptos-app/end2end/playwright.config.ts b/crates/chat-ui/end2end/playwright.config.ts similarity index 100% rename from crates/leptos-app/end2end/playwright.config.ts rename to crates/chat-ui/end2end/playwright.config.ts diff --git a/crates/leptos-app/end2end/tests/example.spec.ts b/crates/chat-ui/end2end/tests/example.spec.ts similarity index 100% rename from crates/leptos-app/end2end/tests/example.spec.ts rename to crates/chat-ui/end2end/tests/example.spec.ts diff --git a/crates/leptos-app/end2end/tsconfig.json b/crates/chat-ui/end2end/tsconfig.json similarity index 100% rename from crates/leptos-app/end2end/tsconfig.json rename to crates/chat-ui/end2end/tsconfig.json diff --git a/crates/leptos-app/public/favicon.ico b/crates/chat-ui/public/favicon.ico similarity index 100% rename from crates/leptos-app/public/favicon.ico rename to crates/chat-ui/public/favicon.ico diff --git a/crates/chat-ui/src/app.rs b/crates/chat-ui/src/app.rs new file mode 100644 index 0000000..63abee9 --- /dev/null +++ b/crates/chat-ui/src/app.rs @@ -0,0 +1,104 @@ +#[cfg(feature = "ssr")] +use axum::Router; +#[cfg(feature = "ssr")] +use leptos::prelude::LeptosOptions; +#[cfg(feature = "ssr")] +use leptos_axum::{generate_route_list, LeptosRoutes}; + +pub struct AppConfig { + pub config: ConfFile, + pub address: String, +} + +impl Default for AppConfig { + fn default() -> Self { + + let conf = get_configuration(Some(concat!(env!("CARGO_MANIFEST_DIR"), "/Cargo.toml"))) + .expect("failed to read config"); + + let addr = conf.leptos_options.site_addr; + + AppConfig { + config: conf, // or whichever field/string representation you need + address: addr.to_string(), + } + } +} + +/// Build the Axum router for this app, including routes, fallback, and state. +/// Call this from another crate (or your bin) when running the server. +#[cfg(feature = "ssr")] +pub fn create_router(leptos_options: LeptosOptions) -> Router { + // Generate the list of routes in your Leptos App + let routes = generate_route_list(App); + + Router::new() + .leptos_routes(&leptos_options, routes, { + let leptos_options = leptos_options.clone(); + move || shell(leptos_options.clone()) + }) + .fallback(leptos_axum::file_and_error_handler(shell)) + .with_state(leptos_options) +} + +use leptos::prelude::*; +use leptos_meta::{provide_meta_context, MetaTags, Stylesheet, Title}; +use leptos_router::{ + components::{Route, Router, Routes}, + StaticSegment, +}; + +pub fn shell(options: LeptosOptions) -> impl IntoView { + view! { + + + + + + + + + + + + + + } +} + +#[component] +pub fn App() -> impl IntoView { + // Provides context that manages stylesheets, titles, meta tags, etc. + provide_meta_context(); + + view! { + // injects a stylesheet into the document + // id=leptos means cargo-leptos will hot-reload this stylesheet + + + // sets the document title + + + // content for this welcome page + <Router> + <main> + <Routes fallback=|| "Page not found.".into_view()> + <Route path=StaticSegment("") view=HomePage/> + </Routes> + </main> + </Router> + } +} + +/// Renders the home page of your application. +#[component] +fn HomePage() -> impl IntoView { + // Creates a reactive value to update the button + let count = RwSignal::new(0); + let on_click = move |_| *count.write() += 1; + + view! { + <h1>"Welcome to Leptos!"</h1> + <button on:click=on_click>"Click Me: " {count}</button> + } +} diff --git a/crates/chat-ui/src/lib.rs b/crates/chat-ui/src/lib.rs new file mode 100644 index 0000000..151489d --- /dev/null +++ b/crates/chat-ui/src/lib.rs @@ -0,0 +1,9 @@ +pub mod app; + +#[cfg(feature = "hydrate")] +#[wasm_bindgen::prelude::wasm_bindgen] +pub fn hydrate() { + use crate::app::*; + console_error_panic_hook::set_once(); + leptos::mount::hydrate_body(App); +} diff --git a/crates/chat-ui/src/main.rs b/crates/chat-ui/src/main.rs new file mode 100644 index 0000000..0fac0d9 --- /dev/null +++ b/crates/chat-ui/src/main.rs @@ -0,0 +1,29 @@ + + + +#[cfg(feature = "ssr")] +#[tokio::main] +async fn main() { + use axum::Router; + use leptos::logging::log; + use leptos::prelude::*; + use leptos_axum::{generate_route_list, LeptosRoutes}; + use chat_ui::app::*; + + let conf = get_configuration(None).expect("failed to read config"); + let addr = conf.leptos_options.site_addr; + + // Build the app router with your extracted function + let app: Router = create_router(conf.leptos_options); + + log!("listening on http://{}", &addr); + let listener = tokio::net::TcpListener::bind(&addr).await.unwrap(); + axum::serve(listener, app.into_make_service()) + .await + .unwrap(); +} + +#[cfg(not(feature = "ssr"))] +pub fn main() { + // no client-side main function +} \ No newline at end of file diff --git a/crates/leptos-app/style/main.scss b/crates/chat-ui/style/main.scss similarity index 100% rename from crates/leptos-app/style/main.scss rename to crates/chat-ui/style/main.scss diff --git a/crates/leptos-app/.cargo/config.toml b/crates/leptos-app/.cargo/config.toml deleted file mode 100644 index 628502f..0000000 --- a/crates/leptos-app/.cargo/config.toml +++ /dev/null @@ -1,3 +0,0 @@ -# Ensure getrandom works on wasm32-unknown-unknown without needing manual RUSTFLAGS -[target.wasm32-unknown-unknown] -rustflags = ["--cfg", "getrandom_backend=\"wasm_js\""] diff --git a/crates/leptos-app/Dockerfile b/crates/leptos-app/Dockerfile deleted file mode 100644 index 5c2b22c..0000000 --- a/crates/leptos-app/Dockerfile +++ /dev/null @@ -1,21 +0,0 @@ -# Build stage -FROM rust:1-alpine AS builder - -# Install build dependencies -RUN apk add --no-cache npm nodejs musl-dev pkgconfig openssl-dev git curl bash - -RUN curl -L --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh | bash - -WORKDIR /app - -# Copy manifest first (cache deps) -COPY . . - -# Install cargo-leptos -RUN cargo binstall cargo-leptos - -# Build release artifacts -RUN cargo leptos build --release - -EXPOSE 8788 -CMD ["cargo", "leptos", "serve", "--release"] \ No newline at end of file diff --git a/crates/leptos-app/src/app.rs b/crates/leptos-app/src/app.rs deleted file mode 100644 index 0c946ca..0000000 --- a/crates/leptos-app/src/app.rs +++ /dev/null @@ -1,520 +0,0 @@ -use leptos::prelude::*; -use leptos_meta::{provide_meta_context, MetaTags, Stylesheet, Title}; -use leptos_router::{ - components::{Route, Router, Routes}, - StaticSegment, -}; - -#[cfg(feature = "hydrate")] -use async_openai_wasm::config::OpenAIConfig; -#[cfg(feature = "hydrate")] -use async_openai_wasm::types::{FinishReason, Role}; -#[cfg(feature = "hydrate")] -use async_openai_wasm::{ - types::{ - ChatCompletionRequestAssistantMessageArgs, ChatCompletionRequestSystemMessageArgs, - ChatCompletionRequestUserMessageArgs, CreateChatCompletionRequestArgs, - Model as OpenAIModel, - }, - Client, -}; -#[cfg(feature = "hydrate")] -use futures_util::StreamExt; -#[cfg(feature = "hydrate")] -use js_sys::Date; -#[cfg(feature = "hydrate")] -use leptos::task::spawn_local; -#[cfg(feature = "hydrate")] -use serde::{Deserialize, Serialize}; -#[cfg(feature = "hydrate")] -use std::collections::VecDeque; -#[cfg(feature = "hydrate")] -use uuid::Uuid; -#[cfg(feature = "hydrate")] -use web_sys::{HtmlInputElement, KeyboardEvent, SubmitEvent}; - -#[cfg(feature = "hydrate")] -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Message { - pub id: String, - pub role: String, - pub content: String, - pub timestamp: f64, -} - -#[cfg(feature = "hydrate")] -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct MessageContent( - pub either::Either<String, Vec<std::collections::HashMap<String, MessageInnerContent>>>, -); - -#[cfg(feature = "hydrate")] -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct MessageInnerContent( - pub either::Either<String, std::collections::HashMap<String, String>>, -); - -#[cfg(feature = "hydrate")] -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ChatMessage { - pub role: String, - pub content: Option<MessageContent>, - pub name: Option<String>, -} - -#[cfg(feature = "hydrate")] -const DEFAULT_MODEL: &str = "default"; - -#[cfg(feature = "hydrate")] -async fn fetch_available_models() -> Result<Vec<OpenAIModel>, String> { - leptos::logging::log!( - "[DEBUG_LOG] fetch_available_models: Starting model fetch from http://localhost:8080/v1" - ); - - let config = OpenAIConfig::new().with_api_base("http://localhost:8080/v1".to_string()); - let client = Client::with_config(config); - - match client.models().list().await { - Ok(response) => { - let model_count = response.data.len(); - leptos::logging::log!( - "[DEBUG_LOG] fetch_available_models: Successfully fetched {} models", - model_count - ); - - if model_count > 0 { - let model_names: Vec<String> = response.data.iter().map(|m| m.id.clone()).collect(); - leptos::logging::log!( - "[DEBUG_LOG] fetch_available_models: Available models: {:?}", - model_names - ); - } else { - leptos::logging::log!( - "[DEBUG_LOG] fetch_available_models: No models returned by server" - ); - } - - Ok(response.data) - } - Err(e) => { - leptos::logging::log!( - "[DEBUG_LOG] fetch_available_models: Failed to fetch models: {:?}", - e - ); - Err(format!("Failed to fetch models: {}", e)) - } - } -} - -pub fn shell(options: LeptosOptions) -> impl IntoView { - view! { - <!DOCTYPE html> - <html lang="en"> - <head> - <meta charset="utf-8"/> - <meta name="viewport" content="width=device-width, initial-scale=1"/> - <AutoReload options=options.clone() /> - <HydrationScripts options/> - <MetaTags/> - </head> - <body> - <App/> - </body> - </html> - } -} - -#[component] -pub fn App() -> impl IntoView { - // Provides context that manages stylesheets, titles, meta tags, etc. - provide_meta_context(); - - view! { - // injects a stylesheet into the document <head> - // id=leptos means cargo-leptos will hot-reload this stylesheet - <Stylesheet id="leptos" href="/pkg/leptos-app.css"/> - - // sets the document title - <Title text="Chat Interface"/> - - // content for this chat interface - <Router> - <main> - <Routes fallback=|| "Page not found.".into_view()> - <Route path=StaticSegment("") view=ChatInterface/> - </Routes> - </main> - </Router> - } -} - -/// Renders the home page of your application. -#[component] -fn HomePage() -> impl IntoView { - // Creates a reactive value to update the button - let count = RwSignal::new(0); - let on_click = move |_| *count.write() += 1; - - view! { - <h1>"Welcome to Leptos!"</h1> - <button on:click=on_click>"Click Me: " {count}</button> - } -} - -/// Renders the chat interface -#[component] -fn ChatInterface() -> impl IntoView { - #[cfg(feature = "hydrate")] - { - ChatInterfaceImpl() - } - - #[cfg(not(feature = "hydrate"))] - { - view! { - <div class="chat-container"> - <h1>"Chat Interface"</h1> - <p>"Loading chat interface..."</p> - </div> - } - } -} - -#[cfg(feature = "hydrate")] -#[component] -fn ChatInterfaceImpl() -> impl IntoView { - let (messages, set_messages) = RwSignal::new(VecDeque::<Message>::new()).split(); - let (input_value, set_input_value) = RwSignal::new(String::new()).split(); - let (is_loading, set_is_loading) = RwSignal::new(false).split(); - let (available_models, set_available_models) = RwSignal::new(Vec::<OpenAIModel>::new()).split(); - let (selected_model, set_selected_model) = RwSignal::new(DEFAULT_MODEL.to_string()).split(); - let (models_loading, set_models_loading) = RwSignal::new(false).split(); - - // Fetch models on component initialization - Effect::new(move |_| { - spawn_local(async move { - set_models_loading.set(true); - match fetch_available_models().await { - Ok(models) => { - set_available_models.set(models); - set_models_loading.set(false); - } - Err(e) => { - leptos::logging::log!("Failed to fetch models: {}", e); - set_available_models.set(vec![]); - set_models_loading.set(false); - } - } - }); - }); - - let send_message = Action::new_unsync(move |content: &String| { - let content = content.clone(); - async move { - if content.trim().is_empty() { - leptos::logging::log!("[DEBUG_LOG] send_message: Empty content, skipping"); - return; - } - - leptos::logging::log!("[DEBUG_LOG] send_message: Starting message send process"); - set_is_loading.set(true); - - // Add user message to chat - let user_message = Message { - id: Uuid::new_v4().to_string(), - role: "user".to_string(), - content: content.clone(), - timestamp: Date::now(), - }; - - set_messages.update(|msgs| msgs.push_back(user_message.clone())); - set_input_value.set(String::new()); - - let mut chat_messages = Vec::new(); - - // Add system message - let system_message = ChatCompletionRequestSystemMessageArgs::default() - .content("You are a helpful assistant.") - .build() - .expect("failed to build system message"); - chat_messages.push(system_message.into()); - - // Add history messages - let history_count = messages.get_untracked().len(); - for msg in messages.get_untracked().iter() { - match msg.role.as_str() { - "user" => { - let message = ChatCompletionRequestUserMessageArgs::default() - .content(msg.content.clone()) - .build() - .expect("failed to build user message"); - chat_messages.push(message.into()); - } - "assistant" => { - let message = ChatCompletionRequestAssistantMessageArgs::default() - .content(msg.content.clone()) - .build() - .expect("failed to build assistant message"); - chat_messages.push(message.into()); - } - _ => {} - } - } - - // Add current user message - let message = ChatCompletionRequestUserMessageArgs::default() - .content(user_message.content.clone()) - .build() - .expect("failed to build user message"); - chat_messages.push(message.into()); - - let current_model = selected_model.get_untracked(); - let total_messages = chat_messages.len(); - - leptos::logging::log!("[DEBUG_LOG] send_message: Preparing request - model: '{}', history_count: {}, total_messages: {}", - current_model, history_count, total_messages); - - let request = CreateChatCompletionRequestArgs::default() - .model(current_model.as_str()) - .max_tokens(512u32) - .messages(chat_messages) - .stream(true) - .build() - .expect("failed to build request"); - - // Send request - let config = OpenAIConfig::new().with_api_base("http://localhost:8080/v1".to_string()); - let client = Client::with_config(config); - - leptos::logging::log!("[DEBUG_LOG] send_message: Sending request to http://localhost:8080/v1 with model: '{}'", current_model); - - match client.chat().create_stream(request).await { - Ok(mut stream) => { - leptos::logging::log!("[DEBUG_LOG] send_message: Successfully created stream"); - - let mut assistant_created = false; - let mut content_appended = false; - let mut chunks_received = 0; - - while let Some(next) = stream.next().await { - match next { - Ok(chunk) => { - chunks_received += 1; - if let Some(choice) = chunk.choices.get(0) { - if !assistant_created { - if let Some(role) = &choice.delta.role { - if role == &Role::Assistant { - assistant_created = true; - let assistant_id = Uuid::new_v4().to_string(); - set_messages.update(|msgs| { - msgs.push_back(Message { - id: assistant_id, - role: "assistant".to_string(), - content: String::new(), - timestamp: Date::now(), - }); - }); - } - } - } - - if let Some(content) = &choice.delta.content { - if !content.is_empty() { - if !assistant_created { - assistant_created = true; - let assistant_id = Uuid::new_v4().to_string(); - set_messages.update(|msgs| { - msgs.push_back(Message { - id: assistant_id, - role: "assistant".to_string(), - content: String::new(), - timestamp: Date::now(), - }); - }); - } - content_appended = true; - set_messages.update(|msgs| { - if let Some(last) = msgs.back_mut() { - if last.role == "assistant" { - last.content.push_str(content); - last.timestamp = Date::now(); - } - } - }); - } - } - - if let Some(reason) = &choice.finish_reason { - if reason == &FinishReason::Stop { - leptos::logging::log!("[DEBUG_LOG] send_message: Received finish_reason=stop after {} chunks", chunks_received); - break; - } - } - } - } - Err(e) => { - leptos::logging::log!( - "[DEBUG_LOG] send_message: Stream error after {} chunks: {:?}", - chunks_received, - e - ); - set_messages.update(|msgs| { - msgs.push_back(Message { - id: Uuid::new_v4().to_string(), - role: "system".to_string(), - content: format!("Stream error: {}", e), - timestamp: Date::now(), - }); - }); - break; - } - } - } - - if assistant_created && !content_appended { - set_messages.update(|msgs| { - let should_pop = msgs - .back() - .map(|m| m.role == "assistant" && m.content.is_empty()) - .unwrap_or(false); - if should_pop { - msgs.pop_back(); - } - }); - } - - leptos::logging::log!("[DEBUG_LOG] send_message: Stream completed successfully, received {} chunks", chunks_received); - } - Err(e) => { - leptos::logging::log!( - "[DEBUG_LOG] send_message: Request failed with error: {:?}", - e - ); - let error_message = Message { - id: Uuid::new_v4().to_string(), - role: "system".to_string(), - content: format!("Error: Request failed - {}", e), - timestamp: Date::now(), - }; - set_messages.update(|msgs| msgs.push_back(error_message)); - } - } - - set_is_loading.set(false); - } - }); - - let on_input = move |ev| { - let input = event_target::<HtmlInputElement>(&ev); - set_input_value.set(input.value()); - }; - - let on_submit = move |ev: SubmitEvent| { - ev.prevent_default(); - let content = input_value.get(); - send_message.dispatch(content); - }; - - let on_keypress = move |ev: KeyboardEvent| { - if ev.key() == "Enter" && !ev.shift_key() { - ev.prevent_default(); - let content = input_value.get(); - send_message.dispatch(content); - } - }; - - let on_model_change = move |ev| { - let select = event_target::<web_sys::HtmlSelectElement>(&ev); - set_selected_model.set(select.value()); - }; - - let messages_list = move || { - messages - .get() - .into_iter() - .map(|message| { - let role_class = match message.role.as_str() { - "user" => "user-message", - "assistant" => "assistant-message", - _ => "system-message", - }; - - view! { - <div class=format!("message {}", role_class)> - <div class="message-role">{message.role}</div> - <div class="message-content">{message.content}</div> - </div> - } - }) - .collect::<Vec<_>>() - }; - - let loading_indicator = move || { - is_loading.get().then(|| { - view! { - <div class="message assistant-message"> - <div class="message-role">"assistant"</div> - <div class="message-content">"Thinking..."</div> - </div> - } - }) - }; - - view! { - <div class="chat-container"> - <h1>"Chat Interface"</h1> - <div class="model-selector"> - <label for="model-select">"Model: "</label> - <select - id="model-select" - on:change=on_model_change - prop:value=selected_model - prop:disabled=models_loading - > - {move || { - if models_loading.get() { - vec![view! { - <option value={String::from("")} selected=false>{String::from("Loading models...")}</option> - }] - } else { - let models = available_models.get(); - if models.is_empty() { - vec![view! { - <option value={String::from("default")} selected=true>{String::from("default")}</option> - }] - } else { - models.into_iter().map(|model| { - view! { - <option value=model.id.clone() selected={model.id == DEFAULT_MODEL}>{model.id.clone()}</option> - } - }).collect::<Vec<_>>() - } - } - }} - </select> - </div> - <div class="messages-container"> - {messages_list} - {loading_indicator} - </div> - <form class="input-form" on:submit=on_submit> - <input - type="text" - class="message-input" - placeholder="Type your message here..." - prop:value=input_value - on:input=on_input - on:keypress=on_keypress - prop:disabled=is_loading - /> - <button - type="submit" - class="send-button" - prop:disabled=move || is_loading.get() || input_value.get().trim().is_empty() - > - "Send" - </button> - </form> - </div> - } -} diff --git a/crates/leptos-app/src/lib.rs b/crates/leptos-app/src/lib.rs deleted file mode 100644 index c743a9b..0000000 --- a/crates/leptos-app/src/lib.rs +++ /dev/null @@ -1,42 +0,0 @@ -use std::path::PathBuf; - -pub mod app; - -#[cfg(feature = "hydrate")] -#[wasm_bindgen::prelude::wasm_bindgen] -pub fn hydrate() { - use crate::app::*; - console_error_panic_hook::set_once(); - leptos::mount::hydrate_body(App); -} - -#[cfg(feature = "ssr")] -pub fn create_leptos_router() -> axum::Router { - use crate::app::*; - use axum::Router; - use leptos::prelude::*; - use leptos_axum::{generate_route_list, LeptosRoutes}; - - - // Build an absolute path to THIS crate's Cargo.toml - let mut cargo_toml = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - cargo_toml.push("Cargo.toml"); - - let conf = get_configuration(Some( - cargo_toml.to_str().expect("valid utf-8 path to Cargo.toml"), - )) - .expect("load leptos config"); - - let conf = get_configuration(Some(cargo_toml.to_str().unwrap())).unwrap(); - let leptos_options = conf.leptos_options; - // Generate the list of routes in your Leptos App - let routes = generate_route_list(App); - - Router::new() - .leptos_routes(&leptos_options, routes, { - let leptos_options = leptos_options.clone(); - move || shell(leptos_options.clone()) - }) - .fallback(leptos_axum::file_and_error_handler(shell)) - .with_state(leptos_options) -} diff --git a/crates/leptos-app/src/main.rs b/crates/leptos-app/src/main.rs deleted file mode 100644 index df5c444..0000000 --- a/crates/leptos-app/src/main.rs +++ /dev/null @@ -1,38 +0,0 @@ -#[cfg(feature = "ssr")] -#[tokio::main] -async fn main() { - use axum::Router; - use leptos::logging::log; - use leptos::prelude::*; - use leptos_app::app::*; - use leptos_axum::{generate_route_list, LeptosRoutes}; - - let conf = get_configuration(None).unwrap(); - let addr = conf.leptos_options.site_addr; - let leptos_options = conf.leptos_options; - // Generate the list of routes in your Leptos App - let routes = generate_route_list(App); - - let app = Router::new() - .leptos_routes(&leptos_options, routes, { - let leptos_options = leptos_options.clone(); - move || shell(leptos_options.clone()) - }) - .fallback(leptos_axum::file_and_error_handler(shell)) - .with_state(leptos_options); - - // run our app with hyper - // `axum::Server` is a re-export of `hyper::Server` - log!("listening on http://{}", &addr); - let listener = tokio::net::TcpListener::bind(&addr).await.unwrap(); - axum::serve(listener, app.into_make_service()) - .await - .unwrap(); -} - -#[cfg(not(feature = "ssr"))] -pub fn main() { - // no client-side main function - // unless we want this to work with e.g., Trunk for pure client-side testing - // see lib.rs for hydration function instead -} diff --git a/crates/predict-otron-9000/Cargo.toml b/crates/predict-otron-9000/Cargo.toml index 7e3691f..b3dabb0 100644 --- a/crates/predict-otron-9000/Cargo.toml +++ b/crates/predict-otron-9000/Cargo.toml @@ -19,7 +19,7 @@ tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } uuid = { version = "1.7.0", features = ["v4"] } reqwest = { version = "0.12", features = ["json"] } -rust-embed = { version = "8.7.2", features = ["include-exclude"] } +rust-embed = { version = "8.7.2", features = ["include-exclude", "axum"] } # Dependencies for embeddings functionality embeddings-engine = { path = "../embeddings-engine" } @@ -28,9 +28,11 @@ embeddings-engine = { path = "../embeddings-engine" } inference-engine = { path = "../inference-engine" } # Dependencies for leptos web app -leptos-app = { path = "../leptos-app", features = ["ssr"] } +#leptos-app = { path = "../leptos-app", features = ["ssr"] } +chat-ui = { path = "../chat-ui", features = ["ssr", "hydrate"], optional = false } mime_guess = "2.0.5" +log = "0.4.27" [package.metadata.compose] diff --git a/crates/predict-otron-9000/src/main.rs b/crates/predict-otron-9000/src/main.rs index 0dfda47..2af994b 100644 --- a/crates/predict-otron-9000/src/main.rs +++ b/crates/predict-otron-9000/src/main.rs @@ -6,7 +6,7 @@ mod standalone_mode; use crate::standalone_mode::create_standalone_router; use axum::response::IntoResponse; use axum::routing::get; -use axum::{Router, http::Uri, response::Html, serve}; +use axum::{Router, http::Uri, response::Html, serve, ServiceExt}; use config::ServerConfig; use ha_mode::create_ha_router; use inference_engine::AppState; @@ -14,11 +14,52 @@ use middleware::{MetricsLayer, MetricsLoggerFuture, MetricsStore}; use rust_embed::Embed; use std::env; use std::path::Component::ParentDir; +use axum::handler::Handler; +use axum::http::header; +use mime_guess::from_path; use tokio::net::TcpListener; +use tower::MakeService; use tower_http::classify::ServerErrorsFailureClass::StatusCode; use tower_http::cors::{Any, CorsLayer}; use tower_http::trace::TraceLayer; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; +use axum::http::{StatusCode as AxumStatusCode }; +use log::info; + +#[derive(Embed)] +#[folder = "../../target/site"] +#[include = "*.js"] +#[include = "*.wasm"] +#[include = "*.css"] +#[include = "*.ico"] +struct Asset; + + +async fn static_handler(uri: Uri) -> axum::response::Response { + // Strip the leading `/` + let path = uri.path().trim_start_matches('/'); + + tracing::info!("Static file: {}", &path); + + // If root is requested, serve index.html + let path = if path.is_empty() { "index.html" } else { path }; + + match Asset::get(path) { + Some(content) => { + let body = content.data.into_owned(); + let mime = from_path(path).first_or_octet_stream(); + + ( + [(header::CONTENT_TYPE, mime.as_ref())], + body, + ) + .into_response() + } + None => (AxumStatusCode::NOT_FOUND, "404 Not Found").into_response(), + } +} + + #[tokio::main] async fn main() { @@ -77,14 +118,18 @@ async fn main() { // Create metrics layer let metrics_layer = MetricsLayer::new(metrics_store); + let leptos_config = chat_ui::app::AppConfig::default(); + // Create the leptos router for the web frontend - let leptos_router = leptos_app::create_leptos_router(); + let leptos_router = chat_ui::app::create_router(leptos_config.config.leptos_options); + // Merge the service router with base routes and add middleware layers let app = Router::new() + .route("/pkg/{*path}", get(static_handler)) .route("/health", get(|| async { "ok" })) .merge(service_router) - .merge(leptos_router) // Add leptos web frontend routes + .merge(leptos_router) .layer(metrics_layer) // Add metrics tracking .layer(cors) .layer(TraceLayer::new_for_http()); @@ -110,7 +155,7 @@ async fn main() { tracing::info!(" POST /v1/embeddings - Text embeddings API"); tracing::info!(" POST /v1/chat/completions - Chat completions API"); - serve(listener, app).await.unwrap(); + serve(listener, app.into_make_service()).await.unwrap(); } fn log_config(config: ServerConfig) { diff --git a/scripts/build_ui.sh b/scripts/build_ui.sh new file mode 100755 index 0000000..d939644 --- /dev/null +++ b/scripts/build_ui.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env sh + +# Resolve the project root (script_dir/..) +PROJECT_ROOT="$(cd "$(dirname "$0")/.." && pwd)" + +# Move into the chat-ui crate +cd "$PROJECT_ROOT/crates/chat-ui" || exit 1 + +# Build with cargo leptos +cargo leptos build --release + +# Move the wasm file, keeping paths relative to the project root +mv "$PROJECT_ROOT/target/site/pkg/chat-ui.wasm" \ + "$PROJECT_ROOT/target/site/pkg/chat-ui_bg.wasm" \ No newline at end of file diff --git a/scripts/run.sh b/scripts/run.sh new file mode 100755 index 0000000..4e26ee9 --- /dev/null +++ b/scripts/run.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +set -e + +# Resolve the project root (script_dir/..) +PROJECT_ROOT="$(cd "$(dirname "$0")/.." && pwd)" + +# todo, conditionally run this only when those files change +"$PROJECT_ROOT/scripts/build_ui.sh" + +# build the frontend first +# Start the unified predict-otron-9000 server on port 8080 +export SERVER_PORT=${SERVER_PORT:-8080} +export RUST_LOG=${RUST_LOG:-info} + +cd "$PROJECT_ROOT" || exit 1 +cargo run --bin predict-otron-9000 --release \ No newline at end of file diff --git a/scripts/run_server.sh b/scripts/run_server.sh deleted file mode 100755 index 9e3f2f7..0000000 --- a/scripts/run_server.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash - -# Start the unified predict-otron-9000 server on port 8080 -export SERVER_PORT=${SERVER_PORT:-8080} -export RUST_LOG=${RUST_LOG:-info} - -cargo run --bin predict-otron-9000 --release \ No newline at end of file