Initial commit
commit
f95673b312
@ -0,0 +1 @@
|
|||||||
|
/target
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,3 @@
|
|||||||
|
[workspace]
|
||||||
|
resolver = "2"
|
||||||
|
members = ["crates/*"]
|
@ -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 <http://unlicense.org/>
|
@ -0,0 +1,4 @@
|
|||||||
|
# aigui-web
|
||||||
|
|
||||||
|
Janky port of ai-gui to the web.
|
||||||
|
Made to test out leptos.
|
@ -0,0 +1 @@
|
|||||||
|
config.ron
|
@ -0,0 +1,18 @@
|
|||||||
|
[package]
|
||||||
|
name = "backend"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
axum = "0.6.16"
|
||||||
|
serde_json = "1.0.96"
|
||||||
|
tokio = { version = "1.27.0", features = ["full"] }
|
||||||
|
tracing = "0.1.37"
|
||||||
|
tracing-subscriber = "0.3.17"
|
||||||
|
ai_core = { git = "https://git.ruthenic.com/xTymon/ai-gui", rev = "a847557914" }
|
||||||
|
ron = "0.8.0"
|
||||||
|
once_cell = "1.17.1"
|
||||||
|
serde = { version = "1.0.160", features = ["derive"] }
|
||||||
|
anyhow = "1.0.70"
|
@ -0,0 +1,27 @@
|
|||||||
|
(
|
||||||
|
app: App(
|
||||||
|
// Path to directory to save history files in. If set to `None` then history is disabled.
|
||||||
|
history_dir: Some("./history")
|
||||||
|
),
|
||||||
|
ai: [
|
||||||
|
AI(
|
||||||
|
// Name of the AI shown to the UI
|
||||||
|
name: "Example",
|
||||||
|
// Generation endpoint
|
||||||
|
url: "https://example.com/generate",
|
||||||
|
// Request parameters, example usage below
|
||||||
|
params: {
|
||||||
|
// Simple string
|
||||||
|
"model": String("example"),
|
||||||
|
// Prompt type, allows for appening a string to the end of the prompt. Useful for stuff like `<|endofprompt|>`.
|
||||||
|
"prompt": Prompt(Some("<|endofprompt|>")),
|
||||||
|
// Temperature type
|
||||||
|
"temperature": Temperature,
|
||||||
|
// Max tokens type
|
||||||
|
"max_tokens": MaxTokens
|
||||||
|
},
|
||||||
|
// JSON objects to get from response
|
||||||
|
retriever: [ "output" ]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
@ -0,0 +1,34 @@
|
|||||||
|
mod routes;
|
||||||
|
mod utils;
|
||||||
|
|
||||||
|
use std::{fs, net::SocketAddr};
|
||||||
|
|
||||||
|
use ai_core::config::Config;
|
||||||
|
use axum::{
|
||||||
|
routing::{get, post},
|
||||||
|
Router,
|
||||||
|
};
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
|
||||||
|
pub static CONFIG: Lazy<Config> = Lazy::new(|| {
|
||||||
|
let config = fs::read_to_string("./config.ron").expect("Failed to read config.ron");
|
||||||
|
|
||||||
|
ron::from_str(&config).expect("Failed to parse config")
|
||||||
|
});
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
tracing_subscriber::fmt::init();
|
||||||
|
|
||||||
|
let addr: SocketAddr = "0.0.0.0:3000".parse().unwrap();
|
||||||
|
let app = Router::new()
|
||||||
|
.route("/generate", post(routes::generate))
|
||||||
|
.route("/ais", get(routes::get_ais));
|
||||||
|
|
||||||
|
tracing::info!("Listening on {}", addr);
|
||||||
|
|
||||||
|
axum::Server::bind(&addr)
|
||||||
|
.serve(app.into_make_service())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
use ai_core::ai::{generate as ai_generate, Params};
|
||||||
|
use anyhow::Context;
|
||||||
|
use axum::Json;
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
use crate::{utils::AppError, CONFIG};
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct GenerationPayload {
|
||||||
|
ai: String,
|
||||||
|
params: Params,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn generate(Json(payload): Json<GenerationPayload>) -> Result<String, AppError> {
|
||||||
|
let ai = CONFIG
|
||||||
|
.ai
|
||||||
|
.iter()
|
||||||
|
.filter(|ai| ai.name == payload.ai)
|
||||||
|
.next()
|
||||||
|
.context(format!("Failed to get AI with name {}!", payload.ai))?
|
||||||
|
.clone();
|
||||||
|
|
||||||
|
let res = ai_generate(ai, payload.params).await?;
|
||||||
|
|
||||||
|
Ok(res.trim().to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_ais() -> Result<Json<serde_json::Value>, AppError> {
|
||||||
|
let ais: Vec<String> = CONFIG.ai.iter().map(|ai| ai.name.clone()).collect();
|
||||||
|
|
||||||
|
Ok(Json(serde_json::to_value(ais)?))
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
// mostly taken from https://github.com/tokio-rs/axum/blob/main/examples/anyhow-error-response/src/main.rs
|
||||||
|
|
||||||
|
use axum::{
|
||||||
|
http::StatusCode,
|
||||||
|
response::{IntoResponse, Response},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct AppError(anyhow::Error);
|
||||||
|
|
||||||
|
impl IntoResponse for AppError {
|
||||||
|
fn into_response(self) -> Response {
|
||||||
|
(StatusCode::INTERNAL_SERVER_ERROR, format!("{}", self.0)).into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<E> From<E> for AppError
|
||||||
|
where
|
||||||
|
E: Into<anyhow::Error>,
|
||||||
|
{
|
||||||
|
fn from(err: E) -> Self {
|
||||||
|
Self(err.into())
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
dist/
|
@ -0,0 +1,14 @@
|
|||||||
|
[package]
|
||||||
|
name = "gui_web"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
ai_core = { git = "https://git.ruthenic.com/xTymon/ai-gui", rev = "a847557914" }
|
||||||
|
leptos = "0.2.5"
|
||||||
|
tracing = "0.1.37"
|
||||||
|
tracing-wasm = "0.2.1"
|
||||||
|
gloo-net = "0.2.6"
|
||||||
|
serde_json = "1.0.96"
|
@ -0,0 +1,3 @@
|
|||||||
|
[[proxy]]
|
||||||
|
rewrite = "/api/"
|
||||||
|
backend = "http://localhost:3000/"
|
@ -0,0 +1,9 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width" />
|
||||||
|
<link data-trunk rel="scss" href="./index.scss"/>
|
||||||
|
</head>
|
||||||
|
<body></body>
|
||||||
|
</html>
|
@ -0,0 +1,89 @@
|
|||||||
|
// this css is kinda jank
|
||||||
|
|
||||||
|
@import url("https://fonts.googleapis.com/css2?family=IBM+Plex+Sans&display=swap");
|
||||||
|
|
||||||
|
* {
|
||||||
|
font-family: "IBM Plex Sans";
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
height: calc(100vh - 10px);
|
||||||
|
background-color: #232428;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
height: 100%;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
padding: 2.5px 5px 2.5px 5px;
|
||||||
|
|
||||||
|
button {
|
||||||
|
background-color: #313338;
|
||||||
|
border: 1px solid #dbdee1;
|
||||||
|
border-radius: 2.5px;
|
||||||
|
color: #dbdee1;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
border-color: #87929b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h5 {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
margin-left: 10px;
|
||||||
|
margin-right: 5px;
|
||||||
|
color: #dbdee1;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
background-color: #313338;
|
||||||
|
border: 1px solid #dbdee1;
|
||||||
|
border-radius: 2.5px;
|
||||||
|
color: #dbdee1;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
border-color: #87929b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
background-color: #313338;
|
||||||
|
border: 1px solid #dbdee1;
|
||||||
|
border-radius: 2.5px;
|
||||||
|
color: #dbdee1;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
border-color: #87929b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.io {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
@media (max-width: 500px) {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
margin: 5px;
|
||||||
|
border-radius: 2.5px;
|
||||||
|
resize: none;
|
||||||
|
background-color: #313338;
|
||||||
|
border-color: #dbdee1;
|
||||||
|
color: #dbdee1;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
mod ui;
|
||||||
|
|
||||||
|
use leptos::*;
|
||||||
|
use ui::*;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
tracing_wasm::set_as_global_default();
|
||||||
|
|
||||||
|
mount_to_body(|cx| view! { cx, <GenerationUI /> })
|
||||||
|
}
|
@ -0,0 +1,121 @@
|
|||||||
|
use ai_core::ai::Params;
|
||||||
|
use gloo_net::http::Request;
|
||||||
|
use leptos::{
|
||||||
|
html::{Input, Option_, Select, Textarea},
|
||||||
|
*,
|
||||||
|
};
|
||||||
|
use serde_json::{json, Value};
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn GenerationUI(cx: Scope) -> impl IntoView {
|
||||||
|
let input: NodeRef<Textarea> = create_node_ref(cx);
|
||||||
|
let output: NodeRef<Textarea> = create_node_ref(cx);
|
||||||
|
|
||||||
|
let ai_selector: NodeRef<Select> = create_node_ref(cx);
|
||||||
|
let temp: NodeRef<Input> = create_node_ref(cx);
|
||||||
|
let max_tokens: NodeRef<Input> = create_node_ref(cx);
|
||||||
|
|
||||||
|
let generate_action = create_action(cx, move |_: &()| async move {
|
||||||
|
// these unwraps should never fail as the elements will always exist at this point
|
||||||
|
let prompt = input.get().unwrap().value();
|
||||||
|
let output = output.get().unwrap();
|
||||||
|
|
||||||
|
let temperature = temp.get().unwrap().value_as_number();
|
||||||
|
let max_tokens = max_tokens.get().unwrap().value_as_number();
|
||||||
|
|
||||||
|
let ai_selector = ai_selector.get().unwrap().value();
|
||||||
|
|
||||||
|
output.set_value("Generating...");
|
||||||
|
|
||||||
|
let res = Request::post("/api/generate")
|
||||||
|
.json(&json!({
|
||||||
|
"ai": ai_selector,
|
||||||
|
"params": Params {
|
||||||
|
prompt,
|
||||||
|
max_tokens: max_tokens as _,
|
||||||
|
temperature: temperature as _
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.unwrap()
|
||||||
|
.send()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
match res {
|
||||||
|
Ok(res) => {
|
||||||
|
if res.status() != 200 {
|
||||||
|
output.set_value("Failed to generate!");
|
||||||
|
tracing::error!("Status code not 200");
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let res = res.text().await.unwrap(); // should be safe to unwrap here?
|
||||||
|
|
||||||
|
output.set_value(&res);
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
output.set_value("Failed to generate!");
|
||||||
|
tracing::error!("{}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let fill_selector_action = create_action(cx, move |_: &()| async move {
|
||||||
|
let res = Request::get("/api/ais").send().await;
|
||||||
|
|
||||||
|
match res {
|
||||||
|
Ok(res) => {
|
||||||
|
if res.status() != 200 {
|
||||||
|
tracing::error!("Status code not 200 for AI selector fetch");
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// these should never fail in theory
|
||||||
|
let res: Value = res.json().await.unwrap();
|
||||||
|
let res = res.as_array().unwrap().to_vec();
|
||||||
|
|
||||||
|
for ai in res {
|
||||||
|
let ai = ai.as_str().unwrap(); // should never fail
|
||||||
|
let option = &Option_::default();
|
||||||
|
|
||||||
|
option.set_value(ai);
|
||||||
|
option.set_label(ai);
|
||||||
|
|
||||||
|
ai_selector.get().unwrap().append_child(&option).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
tracing::error!("Failed to get AIs for selector! {}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
fill_selector_action.dispatch(());
|
||||||
|
|
||||||
|
view! { cx,
|
||||||
|
<main>
|
||||||
|
<div class="bar">
|
||||||
|
<button on:click=move |_| {
|
||||||
|
generate_action.dispatch(());
|
||||||
|
}>
|
||||||
|
"Generate"
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<h5>"AI"</h5>
|
||||||
|
<select node_ref=ai_selector />
|
||||||
|
|
||||||
|
<h5>"Temperature"</h5>
|
||||||
|
<input type="number" min="0" max="2" step="0.1" value="1" node_ref=temp />
|
||||||
|
|
||||||
|
<h5>"Max Tokens"</h5>
|
||||||
|
<input type="number" min="0" max="2000" step="10" value="1000" node_ref=max_tokens />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="io">
|
||||||
|
<textarea node_ref=input />
|
||||||
|
<textarea readonly="true" node_ref=output />
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
}
|
||||||
|
}
|
Reference in new issue