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