Initial commit

master
Tymon 1 year ago
commit f95673b312

1
.gitignore vendored

@ -0,0 +1 @@
/target

2766
Cargo.lock generated

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>
}
}