Skip to content

Commit

Permalink
add some loading magic
Browse files Browse the repository at this point in the history
  • Loading branch information
lukaszKielar committed Apr 25, 2024
1 parent 53d787a commit 041a8f4
Show file tree
Hide file tree
Showing 9 changed files with 195 additions and 49 deletions.
3 changes: 2 additions & 1 deletion src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use leptos::*;
use leptos_meta::*;
use leptos_router::*;

use crate::frontend::views::{Home, NotFound};
use crate::frontend::views::{Chat, Home, NotFound};

#[component]
pub fn App() -> impl IntoView {
Expand All @@ -17,6 +17,7 @@ pub fn App() -> impl IntoView {
<main>
<Routes>
<Route path="/" view=Home/>
<Route path="/c/:id" view=Chat/>
<Route path="/*any" view=NotFound/>
</Routes>
</main>
Expand Down
7 changes: 3 additions & 4 deletions src/frontend/components/conversation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,11 @@ pub(crate) fn Conversation() -> impl IntoView {
let user_prompt_textarea = create_node_ref::<Textarea>();
const MIN_USER_PROMPT_TEXTAREA_HEIGHT: i32 = 24;
const MAX_USER_PROMPT_TEXTAREA_HEIGHT: i32 = 24;
let (user_prompt_textarea_style_height, user_prompt_textarea_style_height_set) =
let (user_prompt_textarea_style_height, set_user_prompt_textarea_style_height) =
create_signal(MIN_USER_PROMPT_TEXTAREA_HEIGHT);
// automatically resize textarea
create_effect(move |_| {
user_prompt_textarea_style_height_set(MIN_USER_PROMPT_TEXTAREA_HEIGHT);
set_user_prompt_textarea_style_height(MIN_USER_PROMPT_TEXTAREA_HEIGHT);
// reset if user_prompt() if empty
let textarea_scroll_height = if user_prompt() == "" {
MIN_USER_PROMPT_TEXTAREA_HEIGHT
Expand All @@ -76,9 +76,8 @@ pub(crate) fn Conversation() -> impl IntoView {
// so it's safe to unwrap NodeRef
user_prompt_textarea.get().unwrap().scroll_height()
};
logging::log!("scroll height: {textarea_scroll_height}");
let style_height = std::cmp::min(textarea_scroll_height, 200);
user_prompt_textarea_style_height_set(style_height)
set_user_prompt_textarea_style_height(style_height)
});

view! {
Expand Down
76 changes: 64 additions & 12 deletions src/frontend/components/sidebar.rs
Original file line number Diff line number Diff line change
@@ -1,33 +1,85 @@
use leptos::*;
use leptos_icons::Icon;

use crate::models;
use crate::server::api::get_conversations;

#[component]
fn ConversationSbar(name: MaybeSignal<String>) -> impl IntoView {
view! {
<div class="flex flex-col gap-2 pb-2 text-gray-100 text-sm">
<a class="flex p-3 items-center gap-3 relative rounded-md hover:bg-[#2A2B32] cursor-pointer break-all hover:pr-4 group">
<Icon icon=icondata::LuMessageSquare class="h-4 w-4"/>
<div class="flex-1 text-ellipsis align-middle h-6 overflow-hidden break-all relative">
{name.get()}
</div>
</a>
</div>
}
}

#[component]
fn ConversationLoadingSbar() -> impl IntoView {
let div_cls = "h-2 w-2 bg-white rounded-full animate-bounce";
view! {
<div class="flex flex-col gap-2 pb-2 text-gray-100 text-sm">
<div class="flex p-3 place-content-center gap-1 relative">
<span class="sr-only">"Loading..."</span>
<div class=format!("{div_cls} [animation-delay:-0.3s]")></div>
<div class=format!("{div_cls} [animation-delay:-0.15s]")></div>
<div class=format!("{div_cls}")></div>
</div>
</div>
}
}

#[component]
fn ConversationsSbar() -> impl IntoView {
let db_conversations = create_resource(|| (), |_| async { get_conversations().await.unwrap() });

let (conversations, set_conversations) = create_signal(Vec::<models::Conversation>::new());

view! {
<Transition fallback=move || {
view! {
<>
<ConversationLoadingSbar/>
</>
}
}>
{if let Some(conversations) = db_conversations.get() {
set_conversations(conversations);
}}
{conversations()
.into_iter()
.map(|c| {
view! { <ConversationSbar name=c.name.into()/> }
})
.collect_view()}
</Transition>
}
}

// TODO: I should probably accept Signal instead of loading all conversations every time
#[component]
pub(crate) fn Sidebar() -> impl IntoView {
view! {
<div class="scrollbar-trigger flex h-full w-full flex-1 items-start border-white/20">
<nav class="flex h-full flex-1 flex-col space-y-1 p-2">
<a class="flex py-3 px-3 items-center gap-3 rounded-md hover:bg-gray-500/10 transition-colors duration-200 text-white cursor-pointer text-sm mb-1 flex-shrink-0 border border-white/20">
<Icon icon=icondata::LuMessageSquarePlus class="h-4 w-4"/>
New chat
"New chat"
</a>
<div class="flex-col flex-1 overflow-y-auto border-b border-white/20">
<div class="flex flex-col gap-2 pb-2 text-gray-100 text-sm">
<a class="flex py-3 px-3 items-center gap-3 relative rounded-md hover:bg-[#2A2B32] cursor-pointer break-all hover:pr-4 group">
<Icon icon=icondata::LuMessageSquare class="h-4 w-4"/>
<div class="flex-1 text-ellipsis max-h-5 overflow-hidden break-all relative">
Conversation
<div class="absolute inset-y-0 right-0 w-8 z-10 bg-gradient-to-l from-gray-900 group-hover:from-[#2A2B32]"></div>
</div>
</a>
</div>
<ConversationsSbar/>
</div>
<a class="flex py-3 px-3 items-center gap-3 rounded-md hover:bg-gray-500/10 transition-colors duration-200 text-white cursor-pointer text-sm">
<Icon icon=icondata::LuTrash2 class="h-4 w-4"/>
Clear conversations
"Clear conversations"
</a>
<a class="flex py-3 px-3 items-center gap-3 rounded-md hover:bg-gray-500/10 transition-colors duration-200 text-white cursor-pointer text-sm">
<Icon icon=icondata::LuSettings class="h-4 w-4"/>
Settings
"Settings"
</a>
</nav>
</div>
Expand Down
17 changes: 17 additions & 0 deletions src/frontend/views/chat.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
use leptos::*;

use crate::frontend::components::{Conversation, Sidebar};

#[component]
pub(crate) fn Chat() -> impl IntoView {
view! {
<div class="overflow-hidden w-full h-screen relative flex">
<div class="dark hidden flex-shrink-0 bg-gray-900 md:flex md:w-[260px] md:flex-col">
<div class="flex h-full min-h-0 flex-col ">
<Sidebar/>
</div>
</div>
<Conversation/>
</div>
}
}
2 changes: 2 additions & 0 deletions src/frontend/views/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
mod chat;
mod home;
mod not_found;

pub(crate) use chat::Chat;
pub(crate) use home::Home;
pub(crate) use not_found::NotFound;
9 changes: 9 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ async fn main() {
use lokai::app::App;
use lokai::fileserv::file_and_error_handler;
use lokai::handlers::{leptos_routes_handler, server_fn_handler};
use lokai::server::db;
use lokai::state::AppState;
use sqlx::sqlite::SqlitePoolOptions;
use sqlx::SqlitePool;
Expand Down Expand Up @@ -40,6 +41,14 @@ async fn main() {
.await
.unwrap();
}
{
let db_pool = db_pool.clone();
let conversation =
lokai::models::Conversation::new(String::from("conversation 2 strasznie dluga nazwa"));
let _ = db::create_conversation(db_pool, conversation)
.await
.unwrap();
}

// Setting get_configuration(None) means we'll be using cargo-leptos's env values
// For deployment these variables are:
Expand Down
20 changes: 4 additions & 16 deletions src/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,30 +77,18 @@ impl Message {
}
}

// TODO: it should contain: id(uuid), messages (vec<Message>))
// Message should contain: id (uuid), persona (enum or string - human/assistant), text (string)
#[derive(Deserialize, Serialize, Debug, Clone)]
#[cfg_attr(feature = "ssr", derive(FromRow))]
pub struct Conversation {
pub id: Uuid,
// TODO: remove messages, they should be loaded separately by different endpoint
pub messages: Vec<Message>,
pub name: String,
}

impl Conversation {
// TODO: remove id argument, it can be automatically generated
pub fn new(id: Uuid) -> Self {
pub fn new(name: String) -> Self {
Self {
id,
messages: Vec::new(),
id: Uuid::new_v4(),
name,
}
}

pub fn push_message(&mut self, message: Message) {
self.messages.push(message)
}

pub fn iter(&self) -> impl Iterator<Item = &Message> + '_ {
self.messages.iter()
}
}
32 changes: 30 additions & 2 deletions src/server/api.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::models::Message;
use crate::models::{Conversation, Message};
use leptos::{server, ServerFnError};
use uuid::Uuid;

Expand Down Expand Up @@ -71,10 +71,38 @@ pub async fn get_conversation_messages(
let _ = tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;

let db_pool = use_context::<SqlitePool>().expect("SqlitePool not found");

let messages = db::get_conversation_messages(db_pool, conversation_id)
.await
.unwrap();

Ok(messages)
}

#[server(GetConversations, "/api")]
pub async fn get_conversations() -> Result<Vec<Conversation>, ServerFnError> {
use super::db;
use leptos::use_context;
use sqlx::SqlitePool;

// TODO: remove me, this is simulating slow loading
let _ = tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;

let db_pool = use_context::<SqlitePool>().expect("SqlitePool not found");
let conversations = db::get_conversations(db_pool).await.unwrap();

Ok(conversations)
}

#[server(CreateConversations, "/api")]
pub async fn create_conversation(conversation: Conversation) -> Result<(), ServerFnError> {
use super::db;
use leptos::use_context;
use sqlx::SqlitePool;

let db_pool = use_context::<SqlitePool>().expect("SqlitePool not found");
let _ = db::create_conversation(db_pool, conversation)
.await
.unwrap();

Ok(())
}
78 changes: 64 additions & 14 deletions src/server/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@ use leptos::logging;
use sqlx::SqlitePool;
use uuid::Uuid;

use crate::models::Message;
use crate::models::{Conversation, Message};

pub async fn get_conversation_messages(
db_pool: SqlitePool,
conversation_id: Uuid,
) -> Result<Vec<Message>, String> {
let rows: Vec<(Uuid, String, String)> = sqlx::query_as(
let messages: Vec<Message> = sqlx::query_as(
r#"
SELECT id, role, content
SELECT id, role, content, conversation_id
FROM messages
WHERE conversation_id = ?
"#,
Expand All @@ -21,16 +21,6 @@ WHERE conversation_id = ?
// TODO: handle errors
.unwrap();

let messages = rows
.into_iter()
.map(|(id, role, content)| Message {
id,
role,
content,
conversation_id,
})
.collect();

Ok(messages)
}

Expand All @@ -53,7 +43,67 @@ VALUES ( ?1, ?2, ?3, ?4 )
.unwrap()
.last_insert_rowid();

logging::log!("new message saved: {:?}", message);
logging::log!("new message saved: {:?}", message.id);

Ok(id)
}

pub async fn get_conversation(
db_pool: SqlitePool,
conversation_id: Uuid,
) -> Result<Option<Conversation>, String> {
let maybe_conversation: Option<Conversation> = sqlx::query_as(
r#"
SELECT id, name
FROM conversations
WHERE id = ?
"#,
)
.bind(conversation_id)
.fetch_optional(&db_pool)
.await
// TODO: handle errors
.unwrap();

Ok(maybe_conversation)
}

pub async fn get_conversations(db_pool: SqlitePool) -> Result<Vec<Conversation>, String> {
let conversations: Vec<Conversation> = sqlx::query_as(
r#"
SELECT id, name
FROM conversations
"#,
)
.fetch_all(&db_pool)
.await
// TODO: handle errors
.unwrap();

Ok(conversations)
}

// TODO: user proper error handling
pub async fn create_conversation(
db_pool: SqlitePool,
conversation: Conversation,
) -> Result<i64, String> {
logging::log!("saving conversation to db: {:?}", conversation.id);

let id = sqlx::query!(
r#"
INSERT INTO conversations ( id, name )
VALUES ( ?1, ?2 )
"#,
conversation.id,
conversation.name,
)
.execute(&db_pool)
.await
.unwrap()
.last_insert_rowid();

logging::log!("new conversation saved: {:?}", conversation.id);

Ok(id)
}

0 comments on commit 041a8f4

Please sign in to comment.