diff --git a/src/api/mod.rs b/src/api/mod.rs index 50af288..f59d715 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1,3 +1,4 @@ +use serde::{Deserialize, Serialize}; use serde_json::Number; use crate::{api::openai::OpenAI, storage::Chat}; @@ -5,34 +6,43 @@ use crate::{api::openai::OpenAI, storage::Chat}; pub mod openai; /// The available providers -#[derive(PartialEq, Clone, Debug)] +#[derive(PartialEq, Clone, Debug, Deserialize, Serialize)] pub enum Provider { - OpenAI(OpenAI), + OpenAI { url: String, token: String }, } -impl AiSpec for Provider { - async fn send_message(&self, chat: &Chat) -> anyhow::Result { +impl Provider { + pub fn connect(&self) -> impl AiSpec { match self { - Provider::OpenAI(open_ai) => open_ai.send_message(chat).await, - } - } - - async fn fetch_models(&self) -> anyhow::Result> { - match self { - Provider::OpenAI(open_ai) => open_ai.fetch_models().await, - } - } - - async fn list_models(&self) -> Vec { - match self { - Provider::OpenAI(open_ai) => open_ai.list_models().await, + Provider::OpenAI { url, token } => OpenAI::new(url, token), } } } -#[derive(Debug)] +// impl AiSpec for Provider { +// async fn send_message(&self, chat: &Chat) -> anyhow::Result { +// match self { +// Provider::OpenAI(open_ai) => open_ai.send_message(chat).await, +// } +// } + +// async fn fetch_models(&self) -> anyhow::Result> { +// match self { +// Provider::OpenAI(open_ai) => open_ai.fetch_models().await, +// } +// } + +// async fn list_models(&self) -> Vec { +// match self { +// Provider::OpenAI(open_ai) => open_ai.list_models().await, +// } +// } +// } + +#[derive(PartialEq, Clone, Debug, Deserialize, Serialize)] pub struct Model { pub id: String, + pub name: String, pub created: Number, pub object: String, pub owned_by: String, diff --git a/src/api/openai/mod.rs b/src/api/openai/mod.rs index 17a4176..eacced4 100644 --- a/src/api/openai/mod.rs +++ b/src/api/openai/mod.rs @@ -33,10 +33,11 @@ struct OpenAIModel { impl From for Model { fn from(value: OpenAIModel) -> Self { Model { - id: value.id, + id: value.id.clone(), created: value.created, object: value.object, owned_by: value.owned_by, + name: value.id, } } } diff --git a/src/components/ai_chat.rs b/src/components/ai_chat.rs new file mode 100644 index 0000000..d077ae6 --- /dev/null +++ b/src/components/ai_chat.rs @@ -0,0 +1,149 @@ +use dioxus::prelude::*; + +use crate::{ + api::AiSpec, + storage::{self, MessageAuthor}, + SUBMIT_SVG, +}; + +#[component] +pub fn ai_chat() -> Element { + // let mut chat = use_context::>(); + // let mut chat = use_signal(|| chat); + let mut message = use_signal(|| "".to_string()); + let mut response_pending = use_signal(|| false); + let mut state = use_context::>(); + + let selected_chat = state.read().selected_chat; + let chat = state.read().chats.get_chat(selected_chat).unwrap().clone(); + + let mut submit = move || { + let message_clone = message.cloned(); + + if message_clone.is_empty() { + return; + } + + message.set("".to_string()); + response_pending.set(true); + + println!("Message recieved!"); + println!("{message_clone:?}"); + + // chat.write() + // .messages + // .push((MessageAuthor::User, message_clone.clone())); + + let mut chat = state + .read() + .chats + .get_chat(selected_chat) + .unwrap() + .to_owned(); + + chat.messages + .push((MessageAuthor::User, message_clone.clone())); + + state.write().chats.write_chat(selected_chat, chat.clone()); + + // let state = use_context::(); // TODO, move this to top level + spawn(async move { + let provider = state.read().providers.first().unwrap().to_owned(); + let res = provider.provider.connect().send_message(&chat).await; + + match res { + Ok(val) => { + println!("{val}"); + + chat.messages.push((MessageAuthor::Assistant, val)); + state.write().chats.write_chat(selected_chat, chat); + } + Err(err) => { + println!("ERROR {:?}", err); + } + } + + // println!("{:?}", res); + response_pending.set(false); + }); + }; + + let model = match chat.model_id.clone() { + Some(val) => format!("{}/{}", val.provider, val.model), + None => "Default".to_string(), + }; + + rsx! { + div { + id: "ai_chat", + div { + itemid: "info", + "{chat.summary} || model: {model}" + }, + div { + id: "chats", + for message in &chat.messages { + div { + class: "message", + span { + "{message.0:?}" + } + p { + "{message.1}" + } + } + + } + + }, + div { + id: "input", + form { + class: if response_pending() { + "animate" + } else { + "" + }, + + onsubmit: move | _event | { + submit(); + // println!("{:?}", x.data); + }, + + textarea { + placeholder: "Write a message...", + + value: "{message()}", + + oninput: move |e| message.set(e.value()), + + onkeydown: move |event| { + let data = &event.data; + + // todo! this might not work on numbpad + if (data.key() == Key::Enter) && !data.modifiers().contains(Modifiers::SHIFT) { + + event.prevent_default(); // prevent the enter from working + submit() + } + } + } + + div { + button { + id: "submit_button", + r#type: "submit", // This tells the browser this button triggers the form + // "Submit", + img { + src: SUBMIT_SVG, + alt: "Submit" + } + } + } + } + } + + + } + } +} diff --git a/src/components/chat_list.rs b/src/components/chat_list.rs new file mode 100644 index 0000000..de0e81f --- /dev/null +++ b/src/components/chat_list.rs @@ -0,0 +1,48 @@ +use dioxus::prelude::*; +use std::str::FromStr; + +use crate::storage; + +#[component] +pub fn chat_list() -> Element { + let mut state = use_context::>(); + let selected_chat = use_signal(|| state.read().selected_chat); + + let mut chats: Vec<(u64, storage::Chat)> = + state.read().chats.get_chats().clone().into_iter().collect(); + chats.sort_by(|(a, _), (b, _)| b.cmp(a)); + + rsx! { + div { + id: "sidebar", + // style: "width: 20vw", + for (id, chat) in chats { + label { + "{chat.summary}" + input { + r#type: "radio", + // id: "contactChoice2", + name: "chat_selection", + value: "{id}", + + checked: selected_chat.read().eq(&id), + + onchange: move | event | { + let value = event.data().value(); + match u64::from_str(&value) { + Ok(val) => { + state.write().selected_chat = val; + println!("Switched default chat to: {val}") + }, + Err(err) => { + println!("Error setting the default chat, couldn't convert {value} to u64. {err:?}") + } + } + // println!("{value} {:?}", event); + }, + } + } + } + } + } +} diff --git a/src/components/mod.rs b/src/components/mod.rs new file mode 100644 index 0000000..f710424 --- /dev/null +++ b/src/components/mod.rs @@ -0,0 +1,2 @@ +pub mod ai_chat; +pub mod chat_list; \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 720a9c2..486d3b7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,14 +1,13 @@ use dioxus::prelude::*; -use std::str::FromStr; use crate::{ api::{openai::OpenAI, AiSpec, Provider}, - storage::{Chat, Chats, MessageAuthor, Settings, State}, + storage::{Chat, Chats, ProviderConfig, Settings, State}, }; mod api; +mod components; mod storage; -// mod structs; const FAVICON: Asset = asset!("/assets/favicon.ico"); const MAIN_CSS: Asset = asset!("/assets/main.css"); @@ -28,229 +27,24 @@ fn main() { #[component] fn App() -> Element { - let mut chats = Chats::default(); - chats.write_chat(0u64, Chat::new(None, "what the hell", vec![])); - chats.write_chat(1u64, Chat::new(None, "what the hell2", vec![])); - chats.write_chat( - 2u64, - Chat::new("meow".to_string(), "what the hell3", vec![]), - ); - chats.write_chat( - 3u64, - Chat::new("meow".to_string(), "what the hell4", vec![]), - ); - - let state = use_signal(|| State { - chats, - providers: vec![Provider::OpenAI(OpenAI::new( - "http://localhost:8080/v1", - "token", - ))], - settings: Settings {}, - selected_chat: 0u64, - }); + let state = use_signal(|| State::load()); let _ = use_context_provider(|| state); - // let chat: &'static mut Chat = state.chats.unwrap(); spawn(async move { let state = use_context::>(); let provider = state.read().providers.first().unwrap().to_owned(); - let models = provider.fetch_models().await; + let models = provider.provider.connect().fetch_models().await; println!("{models:?}") }); - // let chat = state.read().chats.get_chat(0u64).unwrap().clone(); - rsx! { div { style: "display: flex; flex_direction: row;", document::Link { rel: "icon", href: FAVICON } document::Link { rel: "stylesheet", href: MAIN_CSS } - my_sidebar {} - ai_chat {} - } - } -} - -#[component] -pub fn my_sidebar() -> Element { - let mut state = use_context::>(); - let selected_chat = use_signal(|| state.read().selected_chat); - - let mut chats: Vec<(u64, Chat)> = state.read().chats.get_chats().clone().into_iter().collect(); - chats.sort_by(|(a, _), (b, _)| b.cmp(a)); - - rsx! { - div { - id: "sidebar", - // style: "width: 20vw", - for (id, chat) in chats { - label { - "{chat.summary}" - input { - r#type: "radio", - // id: "contactChoice2", - name: "chat_selection", - value: "{id}", - - checked: selected_chat.read().eq(&id), - - onchange: move | event | { - let value = event.data().value(); - match u64::from_str(&value) { - Ok(val) => { - state.write().selected_chat = val; - println!("Switched default chat to: {val}") - }, - Err(err) => { - println!("Error setting the default chat, couldn't convert {value} to u64. {err:?}") - } - } - // println!("{value} {:?}", event); - }, - } - } - } - } - } -} - -#[component] -pub fn ai_chat() -> Element { - // let mut chat = use_context::>(); - // let mut chat = use_signal(|| chat); - let mut message = use_signal(|| "".to_string()); - let mut response_pending = use_signal(|| false); - let mut state = use_context::>(); - - let selected_chat = state.read().selected_chat; - let chat = state.read().chats.get_chat(selected_chat).unwrap().clone(); - - let mut submit = move || { - let message_clone = message.cloned(); - - if message_clone.is_empty() { - return; - } - - message.set("".to_string()); - response_pending.set(true); - - println!("Message recieved!"); - println!("{message_clone:?}"); - - // chat.write() - // .messages - // .push((MessageAuthor::User, message_clone.clone())); - - let mut chat = state - .read() - .chats - .get_chat(selected_chat) - .unwrap() - .to_owned(); - - chat.messages - .push((MessageAuthor::User, message_clone.clone())); - - state.write().chats.write_chat(selected_chat, chat.clone()); - - // let state = use_context::(); // TODO, move this to top level - spawn(async move { - let provider = state.read().providers.first().unwrap().to_owned(); - let res = provider.send_message(&chat).await; - - match res { - Ok(val) => { - println!("{val}"); - - chat.messages.push((MessageAuthor::Assistant, val)); - state.write().chats.write_chat(selected_chat, chat); - } - Err(err) => { - println!("ERROR {:?}", err); - } - } - - // println!("{:?}", res); - response_pending.set(false); - }); - }; - - let model = chat.model.clone().unwrap_or_else(|| "Default".to_string()); - - rsx! { - div { - id: "ai_chat", - div { - itemid: "info", - "{chat.summary} || model: {model}" - }, - div { - id: "chats", - for message in &chat.messages { - div { - class: "message", - span { - "{message.0:?}" - } - p { - "{message.1}" - } - } - - } - - }, - div { - id: "input", - form { - class: if response_pending() { - "animate" - } else { - "" - }, - - onsubmit: move | _event | { - submit(); - // println!("{:?}", x.data); - }, - - textarea { - placeholder: "Write a message...", - - value: "{message()}", - - oninput: move |e| message.set(e.value()), - - onkeydown: move |event| { - let data = &event.data; - - // todo! this might not work on numbpad - if (data.key() == Key::Enter) && !data.modifiers().contains(Modifiers::SHIFT) { - - event.prevent_default(); // prevent the enter from working - submit() - } - } - } - - div { - button { - id: "submit_button", - r#type: "submit", // This tells the browser this button triggers the form - // "Submit", - img { - src: SUBMIT_SVG, - alt: "Submit" - } - } - } - } - } - - + components::chat_list::chat_list {} + components::ai_chat::ai_chat {} } } } diff --git a/src/storage/load.rs b/src/storage/load.rs new file mode 100644 index 0000000..7f5a34c --- /dev/null +++ b/src/storage/load.rs @@ -0,0 +1,28 @@ +use crate::{ + api::Provider, + storage::{Chat, Chats, ProviderConfig, Settings, State}, +}; + +impl State { + pub fn load() -> State { + let mut chats = Chats::default(); + chats.write_chat(0u64, Chat::new(None, "what the hell", vec![])); + chats.write_chat(1u64, Chat::new(None, "what the hell2", vec![])); + chats.write_chat(2u64, Chat::new(None, "what the hell3", vec![])); + chats.write_chat(3u64, Chat::new(None, "what the hell4", vec![])); + + State { + chats, + providers: vec![ProviderConfig { + id: 0, + provider: Provider::OpenAI { + url: "http://localhost:8080/v1".to_string(), + token: "".to_string(), + }, + models: vec![], + }], + settings: Settings {}, + selected_chat: 0u64, + } + } +} diff --git a/src/storage/mod.rs b/src/storage/mod.rs index 98f661e..2620cbb 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -1,16 +1,28 @@ use std::collections::HashMap; -use crate::api; +use serde::{Deserialize, Serialize}; -#[derive(PartialEq, Clone, Debug)] +use crate::api::{self, Model}; + +mod load; + +#[derive(PartialEq, Clone, Debug, Deserialize, Serialize)] pub struct State { - pub providers: Vec, - pub chats: Chats, pub settings: Settings, + pub providers: Vec, + pub chats: Chats, pub selected_chat: u64, } -#[derive(PartialEq, Clone, Debug, Default)] +#[derive(PartialEq, Clone, Debug, Deserialize, Serialize)] +pub struct ProviderConfig { + pub id: u64, + pub provider: api::Provider, + #[serde(skip)] + pub models: Vec, +} + +#[derive(PartialEq, Clone, Debug, Default, Deserialize, Serialize)] pub struct Chats(HashMap); impl Chats { @@ -27,25 +39,41 @@ impl Chats { } } -#[derive(PartialEq, Clone, Debug)] +#[derive(PartialEq, Clone, Debug, Deserialize, Serialize)] pub struct Settings {} // #[derive(Props, PartialEq, Clone)] -#[derive(PartialEq, Clone, Debug)] +#[derive(PartialEq, Clone, Debug, Deserialize, Serialize)] pub struct Chat { - pub model: Option, + pub model_id: Option, pub summary: String, pub messages: Vec<(MessageAuthor, String)>, } +/// An id for a model, contains both the provider id and the model string. +#[derive(PartialEq, Clone, Debug, Deserialize, Serialize)] +pub struct ModelId { + pub provider: u64, + pub model: String, +} + +impl ModelId { + pub fn new(provider: u64, model: impl Into) -> Self { + Self { + provider, + model: model.into(), + } + } +} + impl Chat { pub fn new( - model: impl Into>, + model: impl Into>, summary: impl Into, messages: Vec<(MessageAuthor, String)>, ) -> Self { Self { - model: model.into(), + model_id: model.into(), summary: summary.into(), messages, } @@ -53,7 +81,7 @@ impl Chat { } // #[derive(Props, PartialEq, Clone)] -#[derive(PartialEq, Clone, Debug)] +#[derive(PartialEq, Clone, Debug, Deserialize, Serialize)] pub enum MessageAuthor { System, Assistant,