Skip to content

Commit

Permalink
create prompt component
Browse files Browse the repository at this point in the history
  • Loading branch information
lukaszKielar committed Apr 26, 2024
1 parent ec9d9bf commit 6e3b2c7
Show file tree
Hide file tree
Showing 3 changed files with 156 additions and 135 deletions.
1 change: 1 addition & 0 deletions src/frontend/components/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ mod prompt;
mod sidebar;

pub(crate) use message::Messages;
pub(crate) use prompt::Prompt;
pub(crate) use sidebar::Sidebar;
149 changes: 146 additions & 3 deletions src/frontend/components/prompt.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,149 @@
use leptos::*;
use icondata;
use leptos::ev::{Event, KeyboardEvent, MouseEvent};
use leptos::{html::Textarea, *};
use leptos_icons::Icon;
use uuid::Uuid;

use crate::models;
use crate::server::api::AskAssistant;

#[component]
fn PromptSubmitButton<F>(on_click: F, button_disabled: ReadSignal<bool>) -> impl IntoView
where
F: Fn(MouseEvent) + 'static,
{
view! {
<button
class="absolute p-1 rounded-md bottom-1.5 md:bottom-2.5 bg-transparent disabled:bg-gray-500 right-1 md:right-2 disabled:opacity-40"
on:click=on_click
disabled=button_disabled()
>
<Icon icon=icondata::LuSend class="h-4 w-4 mr-1 text-white "/>
</button>
}
}

#[component]
pub(crate) fn Prompt() -> impl IntoView {
todo!()
fn PromptTextArea<FIn, FKdn>(
on_input: FIn,
on_keydown: FKdn,
#[prop(into)] user_prompt: Signal<String>,
#[prop(default = 24)] min_height: i32,
#[prop(default = 216)] max_height: i32,
) -> impl IntoView
where
FIn: Fn(Event) + 'static,
FKdn: Fn(KeyboardEvent) + 'static,
{
let textarea = create_node_ref::<Textarea>();
let (dynamic_height, set_dynamic_height) = create_signal(min_height);
// automatically resize textarea
create_effect(move |_| {
set_dynamic_height(min_height);
// reset if user_prompt() if empty
let scroll_height = if user_prompt() == "" {
min_height
} else {
// SAFETY: effect is triggered by user input into Textarea, so element has been already loaded,
// so it's safe to unwrap NodeRef
textarea.get().unwrap().scroll_height()
};
let style_height = std::cmp::min(scroll_height, max_height);
set_dynamic_height(style_height)
});

view! {
<textarea
on:input=on_input
on:keydown=on_keydown
type="text"
prop:value=user_prompt
node_ref=textarea
tab_index=0
placeholder="Message LokAI..."
class=move || {
format!(
"m-0 w-full resize-none border-0 bg-transparent p-0 pr-7 focus:ring-0 focus-visible:ring-0 dark:bg-transparent pl-2 h-[{min_height}px] max-h-[{max_height}px] overflow-y-auto",
)
}

style:height=move || { format!("{}px", dynamic_height()) }
></textarea>
}
}

#[component]
pub(crate) fn Prompt(
#[prop(default = 24)] min_textarea_height: i32,
#[prop(default = 216)] max_textarea_height: i32,
conversation_id: MaybeSignal<Uuid>,
set_messages: WriteSignal<Vec<models::Message>>,
) -> impl IntoView {
let (user_prompt, set_user_prompt) = create_signal(String::new());
let (button_disabled, set_button_disabled) = create_signal(true);

let send_user_prompt = create_server_action::<AskAssistant>();
let assistant_response_value = send_user_prompt.value();
create_effect(move |_| {
if let Some(response) = assistant_response_value.get() {
let assistant_response = response.unwrap();
set_messages.update(|msgs| msgs.push(assistant_response));
}
});

create_effect(move |_| {
if user_prompt().len() == 0 || send_user_prompt.pending().get() {
set_button_disabled(true)
} else {
set_button_disabled(false)
};
});

let on_click = move |ev: MouseEvent| {
ev.prevent_default();
let user_message = models::Message::user(user_prompt(), conversation_id());
let user_message_clone = user_message.clone();
if user_message.content != "" {
set_messages.update(|msgs| msgs.push(user_message_clone));
send_user_prompt.dispatch(AskAssistant { user_message });
set_user_prompt("".to_string());
}
};

let on_input = move |ev| set_user_prompt(event_target_value(&ev));
let on_keydown = move |ev: KeyboardEvent| {
if ev.key() == "Enter" && !ev.shift_key() {
ev.prevent_default();
let user_message = models::Message::user(user_prompt(), conversation_id());
let user_message_clone = user_message.clone();
if user_message.content != "" {
set_messages.update(|msgs| msgs.push(user_message_clone));
send_user_prompt.dispatch(AskAssistant { user_message });
set_user_prompt("".to_string());
}
}
};

view! {
<div class="absolute bottom-0 left-0 w-full border-t md:border-t-0 dark:border-white/20 md:border-transparent md:dark:border-transparent md:bg-vert-light-gradient bg-white dark:bg-gray-800 md:!bg-transparent dark:md:bg-vert-dark-gradient pt-2">
<form class="stretch mx-2 flex flex-row gap-3 last:mb-2 md:mx-4 md:last:mb-6 lg:mx-auto lg:max-w-2xl xl:max-w-3xl">

<div class="relative flex flex-col h-full flex-1 items-stretch md:flex-col">
<div class="flex flex-col w-full py-2 flex-grow md:py-3 md:pl-4 relative border border-black/10 bg-white dark:border-gray-900/50 dark:text-white dark:bg-gray-700 rounded-md shadow-[0_0_10px_rgba(0,0,0,0.10)] dark:shadow-[0_0_15px_rgba(0,0,0,0.10)]">
<PromptTextArea
on_input=on_input
on_keydown=on_keydown
min_height=min_textarea_height
max_height=max_textarea_height
user_prompt=user_prompt
/>
<PromptSubmitButton on_click=on_click button_disabled=button_disabled/>
</div>
</div>
</form>
<div class="px-3 pt-2 pb-3 text-center text-xs text-black/50 dark:text-white/50 md:px-4 md:pt-3 md:pb-6">
<span>"Enjoy your self-hosted LokAI!"</span>
</div>
</div>
}
}
141 changes: 9 additions & 132 deletions src/frontend/views/chat.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,11 @@
use icondata;
use leptos::{
html::{Div, Textarea},
*,
};
use leptos_icons::Icon;
use leptos::{html::Div, *};
use leptos_router::use_params_map;
use uuid::Uuid;

use crate::{
app::MessagesContext,
frontend::components::Messages,
models,
server::api::{get_conversation_messages, AskAssistant},
MODEL,
};
use crate::app::MessagesContext;
use crate::frontend::components::{Messages, Prompt};
use crate::server::api::get_conversation_messages;
use crate::MODEL;

#[component]
pub(crate) fn Chat() -> impl IntoView {
Expand All @@ -39,28 +31,6 @@ pub(crate) fn Chat() -> impl IntoView {
set_messages,
} = use_context::<MessagesContext>().unwrap();

let (user_prompt, set_user_prompt) = create_signal(String::new());

let send_user_prompt = create_server_action::<AskAssistant>();
let assistant_response_value = send_user_prompt.value();

let (button_disabled, set_button_disabled) = create_signal(true);

create_effect(move |_| {
if user_prompt().len() == 0 || send_user_prompt.pending().get() {
set_button_disabled(true)
} else {
set_button_disabled(false)
};
});

create_effect(move |_| {
if let Some(response) = assistant_response_value.get() {
let assistant_response = response.unwrap();
set_messages.update(|msgs| msgs.push(assistant_response));
}
});

let bottom_of_chat_div = create_node_ref::<Div>();
create_effect(move |_| {
let _ = messages();
Expand All @@ -71,26 +41,6 @@ pub(crate) fn Chat() -> 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, set_user_prompt_textarea_style_height) =
create_signal(MIN_USER_PROMPT_TEXTAREA_HEIGHT);
// automatically resize textarea
create_effect(move |_| {
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
} else {
// SAFETY: effect is triggered by user input into Textarea, so element has been already loaded,
// so it's safe to unwrap NodeRef
user_prompt_textarea.get().unwrap().scroll_height()
};
let style_height = std::cmp::min(textarea_scroll_height, 200);
set_user_prompt_textarea_style_height(style_height)
});

view! {
<div class="flex max-w-full flex-1 flex-col">
<div class="relative h-full w-full transition-width flex flex-col overflow-hidden items-stretch flex-1">
Expand Down Expand Up @@ -119,83 +69,10 @@ pub(crate) fn Chat() -> impl IntoView {
</div>
</div>
</div>
<div class="absolute bottom-0 left-0 w-full border-t md:border-t-0 dark:border-white/20 md:border-transparent md:dark:border-transparent md:bg-vert-light-gradient bg-white dark:bg-gray-800 md:!bg-transparent dark:md:bg-vert-dark-gradient pt-2">
<form class="stretch mx-2 flex flex-row gap-3 last:mb-2 md:mx-4 md:last:mb-6 lg:mx-auto lg:max-w-2xl xl:max-w-3xl">

<div class="relative flex flex-col h-full flex-1 items-stretch md:flex-col">
// {errorMessage ? (
// <div class="mb-2 md:mb-0">
// <div class="h-full flex ml-1 md:w-full md:m-auto md:mb-2 gap-0 md:gap-2 justify-center">
// <span class="text-red-500 text-sm">{errorMessage}</span>
// </div>
// </div>
// ) : null}
<div class="flex flex-col w-full py-2 flex-grow md:py-3 md:pl-4 relative border border-black/10 bg-white dark:border-gray-900/50 dark:text-white dark:bg-gray-700 rounded-md shadow-[0_0_10px_rgba(0,0,0,0.10)] dark:shadow-[0_0_15px_rgba(0,0,0,0.10)]">
<textarea
on:input=move |ev| {
set_user_prompt(event_target_value(&ev));
}

on:keydown=move |ev| {
if ev.key() == "Enter" && !ev.shift_key() {
ev.prevent_default();
let user_message = models::Message::user(
user_prompt(),
conversation_id(),
);
let user_message_clone = user_message.clone();
if user_message.content != "" {
set_messages.update(|msgs| msgs.push(user_message_clone));
send_user_prompt.dispatch(AskAssistant { user_message });
set_user_prompt("".to_string());
}
}
}

type="text"
prop:value=user_prompt
node_ref=user_prompt_textarea
tab_index=0
placeholder="Message LokAI..."
class=move || {
format!(
"m-0 w-full resize-none border-0 bg-transparent p-0 pr-7 focus:ring-0 focus-visible:ring-0 dark:bg-transparent pl-2 h-[{MIN_USER_PROMPT_TEXTAREA_HEIGHT}px] max-h-[{MAX_USER_PROMPT_TEXTAREA_HEIGHT}px] overflow-y-auto",
)
}

style:height=move || {
format!("{}px", user_prompt_textarea_style_height())
}
>
</textarea>
<button
class="absolute p-1 rounded-md bottom-1.5 md:bottom-2.5 bg-transparent disabled:bg-gray-500 right-1 md:right-2 disabled:opacity-40"
on:click=move |ev| {
ev.prevent_default();
let user_message = models::Message::user(
user_prompt(),
conversation_id(),
);
let user_message_clone = user_message.clone();
if user_message.content != "" {
set_messages.update(|msgs| msgs.push(user_message_clone));
send_user_prompt.dispatch(AskAssistant { user_message });
set_user_prompt("".to_string());
}
}

disabled=button_disabled
>

<Icon icon=icondata::LuSend class="h-4 w-4 mr-1 text-white "/>
</button>
</div>
</div>
</form>
<div class="px-3 pt-2 pb-3 text-center text-xs text-black/50 dark:text-white/50 md:px-4 md:pt-3 md:pb-6">
<span>"Enjoy your self-hosted LokAI!"</span>
</div>
</div>
<Prompt
conversation_id=MaybeSignal::derive(conversation_id)
set_messages=set_messages
/>
</div>
</div>
}
Expand Down

0 comments on commit 6e3b2c7

Please sign in to comment.