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.ts
|
||||
/**/.*.bun-build
|
||||
/AGENTS.md
|
||||
|
77
Cargo.lock
generated
77
Cargo.lock
generated
@@ -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"
|
||||
|
@@ -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"
|
||||
|
@@ -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 <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.
|
||||
# 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"
|
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"] }
|
||||
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]
|
||||
|
@@ -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) {
|
||||
|
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