mirror of
https://github.com/geoffsee/predict-otron-9001.git
synced 2025-09-08 22:46:44 +00:00
chat-ui not functional yet but builds
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -78,3 +78,4 @@ venv/
|
|||||||
/scripts/cli
|
/scripts/cli
|
||||||
!/scripts/cli.ts
|
!/scripts/cli.ts
|
||||||
/**/.*.bun-build
|
/**/.*.bun-build
|
||||||
|
/AGENTS.md
|
||||||
|
77
Cargo.lock
generated
77
Cargo.lock
generated
@@ -247,30 +247,6 @@ dependencies = [
|
|||||||
"syn 2.0.106",
|
"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]]
|
[[package]]
|
||||||
name = "async-trait"
|
name = "async-trait"
|
||||||
version = "0.1.89"
|
version = "0.1.89"
|
||||||
@@ -845,6 +821,20 @@ version = "0.2.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
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]]
|
[[package]]
|
||||||
name = "clang-sys"
|
name = "clang-sys"
|
||||||
version = "1.8.1"
|
version = "1.8.1"
|
||||||
@@ -2939,28 +2929,6 @@ dependencies = [
|
|||||||
"web-sys",
|
"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]]
|
[[package]]
|
||||||
name = "leptos_axum"
|
name = "leptos_axum"
|
||||||
version = "0.8.6"
|
version = "0.8.6"
|
||||||
@@ -4086,9 +4054,10 @@ name = "predict-otron-9000"
|
|||||||
version = "0.1.2"
|
version = "0.1.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"axum",
|
"axum",
|
||||||
|
"chat-ui",
|
||||||
"embeddings-engine",
|
"embeddings-engine",
|
||||||
"inference-engine",
|
"inference-engine",
|
||||||
"leptos-app",
|
"log",
|
||||||
"mime_guess",
|
"mime_guess",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"rust-embed",
|
"rust-embed",
|
||||||
@@ -4839,6 +4808,7 @@ version = "8.7.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "025908b8682a26ba8d12f6f2d66b987584a4a87bc024abc5bbc12553a8cd178a"
|
checksum = "025908b8682a26ba8d12f6f2d66b987584a4a87bc024abc5bbc12553a8cd178a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"axum",
|
||||||
"rust-embed-impl",
|
"rust-embed-impl",
|
||||||
"rust-embed-utils",
|
"rust-embed-utils",
|
||||||
"walkdir",
|
"walkdir",
|
||||||
@@ -6463,22 +6433,9 @@ checksum = "f33196643e165781c20a5ead5582283a7dacbb87855d867fbc2df3f81eddc1be"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"getrandom 0.3.3",
|
"getrandom 0.3.3",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"rand 0.9.2",
|
|
||||||
"uuid-macro-internal",
|
|
||||||
"wasm-bindgen",
|
"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]]
|
[[package]]
|
||||||
name = "v_frame"
|
name = "v_frame"
|
||||||
version = "0.3.9"
|
version = "0.3.9"
|
||||||
|
@@ -3,11 +3,11 @@ members = [
|
|||||||
"crates/predict-otron-9000",
|
"crates/predict-otron-9000",
|
||||||
"crates/inference-engine",
|
"crates/inference-engine",
|
||||||
"crates/embeddings-engine",
|
"crates/embeddings-engine",
|
||||||
"crates/leptos-app",
|
|
||||||
"crates/helm-chart-tool",
|
"crates/helm-chart-tool",
|
||||||
"crates/llama-runner",
|
"crates/llama-runner",
|
||||||
"crates/gemma-runner",
|
"crates/gemma-runner",
|
||||||
"crates/cli"
|
"crates/cli",
|
||||||
|
"crates/chat-ui"
|
||||||
]
|
]
|
||||||
default-members = ["crates/predict-otron-9000"]
|
default-members = ["crates/predict-otron-9000"]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
@@ -42,8 +42,3 @@ overflow-checks = true
|
|||||||
opt-level = 3
|
opt-level = 3
|
||||||
debug = true
|
debug = true
|
||||||
lto = "thin"
|
lto = "thin"
|
||||||
|
|
||||||
[[workspace.metadata.leptos]]
|
|
||||||
# project name
|
|
||||||
bin-package = "leptos-app"
|
|
||||||
lib-package = "leptos-app"
|
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "leptos-app"
|
name = "chat-ui"
|
||||||
version.workspace = true
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
@@ -16,44 +16,11 @@ leptos_meta = { version = "0.8.0" }
|
|||||||
tokio = { version = "1", features = ["rt-multi-thread"], optional = true }
|
tokio = { version = "1", features = ["rt-multi-thread"], optional = true }
|
||||||
wasm-bindgen = { version = "=0.2.100", 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]
|
[features]
|
||||||
hydrate = [
|
hydrate = [
|
||||||
"leptos/hydrate",
|
"leptos/hydrate",
|
||||||
"dep:console_error_panic_hook",
|
"dep:console_error_panic_hook",
|
||||||
"dep:wasm-bindgen",
|
"dep:wasm-bindgen",
|
||||||
"dep:js-sys",
|
|
||||||
"dep:web-sys",
|
|
||||||
]
|
]
|
||||||
ssr = [
|
ssr = [
|
||||||
"dep:axum",
|
"dep:axum",
|
||||||
@@ -73,8 +40,9 @@ codegen-units = 1
|
|||||||
panic = "abort"
|
panic = "abort"
|
||||||
|
|
||||||
[package.metadata.leptos]
|
[package.metadata.leptos]
|
||||||
|
name = "chat-ui"
|
||||||
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
|
# 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.
|
# 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"
|
site-root = "target/site"
|
||||||
@@ -84,7 +52,7 @@ site-root = "target/site"
|
|||||||
site-pkg-dir = "pkg"
|
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 <site-root>/<site-pkg>/app.css
|
# [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 <site-root>/<site-pkg>/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.
|
# 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.
|
# 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
|
# The profile to use for the lib target when compiling for release
|
||||||
#
|
#
|
||||||
# Optional. Defaults to "release".
|
# Optional. Defaults to "release".
|
||||||
lib-profile-release = "wasm-release"
|
lib-profile-release = "release"
|
24
crates/chat-ui/LICENSE
Normal file
24
crates/chat-ui/LICENSE
Normal file
@@ -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 <https://unlicense.org>
|
2
crates/chat-ui/README.md
Normal file
2
crates/chat-ui/README.md
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# chat-ui
|
||||||
|
This is served by the predict-otron-9000 server. This needs to be built before the server.
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
104
crates/chat-ui/src/app.rs
Normal file
104
crates/chat-ui/src/app.rs
Normal file
@@ -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! {
|
||||||
|
<!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/chat-ui.css"/>
|
||||||
|
|
||||||
|
// sets the document title
|
||||||
|
<Title text="Welcome to Leptos"/>
|
||||||
|
|
||||||
|
// 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>
|
||||||
|
}
|
||||||
|
}
|
9
crates/chat-ui/src/lib.rs
Normal file
9
crates/chat-ui/src/lib.rs
Normal file
@@ -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);
|
||||||
|
}
|
29
crates/chat-ui/src/main.rs
Normal file
29
crates/chat-ui/src/main.rs
Normal file
@@ -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
|
||||||
|
}
|
@@ -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\""]
|
|
@@ -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"]
|
|
@@ -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>
|
|
||||||
}
|
|
||||||
}
|
|
@@ -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)
|
|
||||||
}
|
|
@@ -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
|
|
||||||
}
|
|
@@ -19,7 +19,7 @@ tracing = "0.1"
|
|||||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
uuid = { version = "1.7.0", features = ["v4"] }
|
uuid = { version = "1.7.0", features = ["v4"] }
|
||||||
reqwest = { version = "0.12", features = ["json"] }
|
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
|
# Dependencies for embeddings functionality
|
||||||
embeddings-engine = { path = "../embeddings-engine" }
|
embeddings-engine = { path = "../embeddings-engine" }
|
||||||
@@ -28,9 +28,11 @@ embeddings-engine = { path = "../embeddings-engine" }
|
|||||||
inference-engine = { path = "../inference-engine" }
|
inference-engine = { path = "../inference-engine" }
|
||||||
|
|
||||||
# Dependencies for leptos web app
|
# 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"
|
mime_guess = "2.0.5"
|
||||||
|
log = "0.4.27"
|
||||||
|
|
||||||
|
|
||||||
[package.metadata.compose]
|
[package.metadata.compose]
|
||||||
|
@@ -6,7 +6,7 @@ mod standalone_mode;
|
|||||||
use crate::standalone_mode::create_standalone_router;
|
use crate::standalone_mode::create_standalone_router;
|
||||||
use axum::response::IntoResponse;
|
use axum::response::IntoResponse;
|
||||||
use axum::routing::get;
|
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 config::ServerConfig;
|
||||||
use ha_mode::create_ha_router;
|
use ha_mode::create_ha_router;
|
||||||
use inference_engine::AppState;
|
use inference_engine::AppState;
|
||||||
@@ -14,11 +14,52 @@ use middleware::{MetricsLayer, MetricsLoggerFuture, MetricsStore};
|
|||||||
use rust_embed::Embed;
|
use rust_embed::Embed;
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::path::Component::ParentDir;
|
use std::path::Component::ParentDir;
|
||||||
|
use axum::handler::Handler;
|
||||||
|
use axum::http::header;
|
||||||
|
use mime_guess::from_path;
|
||||||
use tokio::net::TcpListener;
|
use tokio::net::TcpListener;
|
||||||
|
use tower::MakeService;
|
||||||
use tower_http::classify::ServerErrorsFailureClass::StatusCode;
|
use tower_http::classify::ServerErrorsFailureClass::StatusCode;
|
||||||
use tower_http::cors::{Any, CorsLayer};
|
use tower_http::cors::{Any, CorsLayer};
|
||||||
use tower_http::trace::TraceLayer;
|
use tower_http::trace::TraceLayer;
|
||||||
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
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]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
@@ -77,14 +118,18 @@ async fn main() {
|
|||||||
// Create metrics layer
|
// Create metrics layer
|
||||||
let metrics_layer = MetricsLayer::new(metrics_store);
|
let metrics_layer = MetricsLayer::new(metrics_store);
|
||||||
|
|
||||||
|
let leptos_config = chat_ui::app::AppConfig::default();
|
||||||
|
|
||||||
// Create the leptos router for the web frontend
|
// 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
|
// Merge the service router with base routes and add middleware layers
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
|
.route("/pkg/{*path}", get(static_handler))
|
||||||
.route("/health", get(|| async { "ok" }))
|
.route("/health", get(|| async { "ok" }))
|
||||||
.merge(service_router)
|
.merge(service_router)
|
||||||
.merge(leptos_router) // Add leptos web frontend routes
|
.merge(leptos_router)
|
||||||
.layer(metrics_layer) // Add metrics tracking
|
.layer(metrics_layer) // Add metrics tracking
|
||||||
.layer(cors)
|
.layer(cors)
|
||||||
.layer(TraceLayer::new_for_http());
|
.layer(TraceLayer::new_for_http());
|
||||||
@@ -110,7 +155,7 @@ async fn main() {
|
|||||||
tracing::info!(" POST /v1/embeddings - Text embeddings API");
|
tracing::info!(" POST /v1/embeddings - Text embeddings API");
|
||||||
tracing::info!(" POST /v1/chat/completions - Chat completions 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) {
|
fn log_config(config: ServerConfig) {
|
||||||
|
14
scripts/build_ui.sh
Executable file
14
scripts/build_ui.sh
Executable file
@@ -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"
|
17
scripts/run.sh
Executable file
17
scripts/run.sh
Executable file
@@ -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
|
@@ -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
|
|
Reference in New Issue
Block a user