Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions crates/core/assets/new-chat.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
@import url('https://fonts.googleapis.com/css2?family=Lora:ital,wght@0,400;0,500;0,600;1,400;1,500;1,600&display=swap');


.new-chat-box {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
top: 20vh;
width: calc(100% - var(--sidebar-width));
left: var(--sidebar-width);
position: relative;
height: 40vh;
padding: var(--spacing-lg);
text-align: center;
/*background-color: #e2e1da;*/
transition: width var(--transition-normal), left var(--transition-normal);
}

.new-chat-box.sidebar-collapsed {
width: calc(100% - var(--sidebar-collapsed-width));
left: var(--sidebar-collapsed-width);
}

.new-chat-greeting {
color: #3d3d3a;
font-family: "Lora", serif;
font-weight: 400;
font-style: italic;
font-size: clamp(1rem, 4vw, 2.8rem);
letter-spacing: 0.02em;
}

.textarea-box-new-chat {
width: 80vh;
margin: 0 auto;
}

.textarea-wrapper-new-chat {
background-color: var(--color-white);
border: 0.5px solid var(--color-border);
border-radius: var(--radius-lg);
box-shadow: 0 0.25rem var(--spacing-xl) var(--color-shadow);
padding: 15px;
display: flex;
flex-direction: column;
align-items: stretch;
gap: 10px;
z-index: 10;
cursor: text;
transition: all var(--transition-fast);
box-sizing: border-box;
flex-shrink: 0;
}

.textarea-wrapper-new-chat textarea {
border: none;
font-family: var(--font-sans-serif);
font-size: 16px;
resize: none;
outline: none;
height: 30px;
background-color: transparent;
letter-spacing: -0.025em;
}
100 changes: 100 additions & 0 deletions crates/core/src/chat_service.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
use dioxus::logger::tracing::info;
use dioxus::prelude::*;
use std::pin::pin;
use futures::StreamExt;

use crate::chat_completions::comp_stream::test_stream_frontend;
use crate::chat_completions::comp_chunks_collector::extract_content_from_chunks;
use crate::openai;
use crate::shared_state::{Message, SharedState};


#[derive(Clone)]
pub struct SendMessageRequest {
pub content: String,
}

pub async fn message_service(mut rx: UnboundedReceiver<SendMessageRequest>) {
let sh = use_context::<SharedState>();

while let Some(request) = rx.next().await {
send_message_and_stream(sh.clone(), request.content).await;
}
}

async fn send_message_and_stream(sh: SharedState, message_content: String) {
if message_content.trim().is_empty() {
return;
}

let mut sh_messages = sh.messages.clone();
let mut sh_scrolled_to_bottom_element = sh.scrolled_to_bottom_element.clone();

let assistant_message_index;
{
let mut messages = sh_messages.write();
messages.push(Message {
role: "user".to_string(),
content: message_content,
});
info!("Adding user message");
sh_scrolled_to_bottom_element.set(false);

// Create a new assistant message with empty content
assistant_message_index = messages.len();
messages.push(Message {
role: "assistant".to_string(),
content: "".to_string(),
});
info!("Adding empty assistant message");
}

let model = match sh.active_model.read().clone() {
Some(model) => model.id,
None => {
info!("No active model selected");
return;
}
};

// Convert messages to OMessages for the API
let omessages = sh_messages.read().iter()
.filter(|m| m.role == "user" || m.role == "assistant")
.map(|m| openai::OMessage::new(
m.role.clone(),
m.content.clone(),
))
.collect::<Vec<_>>();
// todo: remove the last empty user message

info!("{:#?}", omessages);
info!("Starting streaming");

// Start streaming the response
match test_stream_frontend(omessages, model).await {
Ok(stream) => {
let mut received_chunks = Vec::new();

// Process each chunk as it arrives
let mut stream = pin!(stream);
while let Some(chunk) = stream.next().await {
received_chunks.push(chunk);
{
let mut messages = sh_messages.write();
if let Some(assistant_message) = messages.get_mut(assistant_message_index) {
// Update with all chunks received so far
assistant_message.content = extract_content_from_chunks(&received_chunks);
}
}
}
info!("Stream completed. Received {} chunks in total.", received_chunks.len());
},
Err(e) => {
info!("Failed to start stream: {}", e);
let mut messages = sh_messages.write();
if let Some(assistant_message) = messages.get_mut(assistant_message_index) {
assistant_message.content = format!("Error: {}", e);
}
}
}
}
26 changes: 20 additions & 6 deletions crates/core/src/components/InputBox.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,15 @@ use crate::shared_state::SharedState;
const INPUT_BOX_CSS: Asset = asset!("assets/input-box.css");



#[derive(Props, PartialEq, Clone)]
pub struct InputBoxProps {
#[props(default = false)]
pub for_new_chat: bool,
}

#[component]
pub fn InputBox() -> Element {
pub fn InputBox(props: InputBoxProps) -> Element {
let sh = use_context::<SharedState>();

let mut chat_input_value = use_signal(|| "".to_string());
Expand All @@ -19,22 +26,29 @@ pub fn InputBox() -> Element {
} else {
"sidebar-collapsed"
};

let (textarea_box_class, textarea_wrapper_class, textarea_placeholder) = if props.for_new_chat {
("textarea-box-new-chat", "textarea-wrapper-new-chat", "How can I help you?")
} else {
("textarea-box", "textarea-wrapper", "Type your message...")
};

rsx! {
document::Link { rel: "stylesheet", href: INPUT_BOX_CSS}

div {class: "textarea-box {textarea_box_sidebar_classname}",
div {class: "textarea-wrapper",
div {class: "{textarea_box_class} {textarea_box_sidebar_classname}",
div {class: "{textarea_wrapper_class}",
textarea {
id: "chat-input",
value: "{chat_input_value}", // Use string interpolation
value: "{chat_input_value}",
oninput: move |evt| chat_input_value.set(evt.data.value()),
placeholder: "Type your message..."
placeholder: "{textarea_placeholder}"
}
div {class: "sub-input-box-container",
ModelSelector {}
SendButton {
chat_input_value: chat_input_value.clone()
chat_input_value: chat_input_value.clone(),
for_new_chat: props.for_new_chat,
}
}
}
Expand Down
36 changes: 36 additions & 0 deletions crates/core/src/components/NewChat.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
use dioxus::prelude::*;
use crate::components::InputBox::InputBox;
use crate::shared_state::{ActiveView, SharedState};

const NEW_CHAT_CSS: Asset = asset!("assets/new-chat.css");


#[component]
pub fn NewChat() -> Element {
let sh = use_context::<SharedState>();
let is_hidden = *sh.active_view.read() != ActiveView::NewChat;
if is_hidden {
return rsx! { div { style: "display: none;" } };
}

let new_chat_box_sidebar_classname = if sh.side_bar_state.read().is_expanded {
""
} else {
"sidebar-collapsed"
};

rsx! {
document::Link { rel: "stylesheet", href: NEW_CHAT_CSS}

div { class: "new-chat-box {new_chat_box_sidebar_classname}",
h1 { class: "new-chat-greeting {new_chat_box_sidebar_classname}",
"Good Afternoon!"
}
div {
InputBox {
for_new_chat: true,
}
}
}
}
}
84 changes: 13 additions & 71 deletions crates/core/src/components/SendButton.rs
Original file line number Diff line number Diff line change
@@ -1,96 +1,38 @@
use std::pin::pin;
use dioxus::core_macro::{component, rsx, Props};
use dioxus::dioxus_core::Element;
use dioxus::hooks::use_context;
use dioxus::logger::tracing::info;
use dioxus::prelude::*;
use crate::chat_completions::comp_stream::{test_stream_frontend};
use crate::openai;
use crate::shared_state::{Message, SharedState};
use futures::StreamExt;
use crate::chat_completions::comp_chunks_collector::extract_content_from_chunks;

use crate::chat_service::SendMessageRequest;
use crate::shared_state::{ActiveView, SharedState};


#[derive(Props, PartialEq, Clone)]
pub struct SendButtonProps {
pub chat_input_value: Signal<String>,
#[props(default = false)]
pub for_new_chat: bool,
}

#[component]
pub fn SendButton(props: SendButtonProps) -> Element {
let mut sh = use_context::<SharedState>();

let mut chat_input_value = props.chat_input_value.clone();
let message_service = use_coroutine_handle::<SendMessageRequest>();

let button_on_click = move |_| {
let value = chat_input_value.read().clone();

if value.trim().is_empty() {
return;
}

let assistant_message_index;
{
let mut messages = sh.messages.write();
messages.push(
Message {
role: "user".to_string(),
content: value,
}
);
sh.scrolled_to_bottom_element.set(false);

// Create a new assistant message with empty content
assistant_message_index = messages.len();
messages.push(
Message {
role: "assistant".to_string(),
content: "".to_string(),
}
);
}

let model = sh.active_model.read().clone().unwrap().id;

// Convert messages to OMessages for the API
let omessages = sh.messages.read().iter()
.filter(|m| m.role == "user" || m.role == "assistant")
.map(|m| openai::OMessage::new(
m.role.clone(),
m.content.clone(),
))
.collect::<Vec<_>>();

// Start streaming the response
spawn(async move {
match test_stream_frontend(omessages, model).await {
Ok(stream) => {
let mut received_chunks = Vec::new();
chat_input_value.set("".to_string());

// Process each chunk as it arrives
let mut stream = pin!(stream);
while let Some(chunk) = stream.next().await {
received_chunks.push(chunk);
{
let mut messages = sh.messages.write();
if let Some(assistant_message) = messages.get_mut(assistant_message_index) {
// Update with all chunks received so far
assistant_message.content = extract_content_from_chunks(&received_chunks);
}
}
}
info!("Stream completed. Received {} chunks in total.", received_chunks.len());
},
Err(e) => {
info!("Failed to start stream: {}", e);
let mut messages = sh.messages.write();
if let Some(assistant_message) = messages.get_mut(assistant_message_index) {
assistant_message.content = format!("Error: {}", e);
}
}
}
message_service.send(SendMessageRequest {
content: value,
});

// Clear the input after sending
chat_input_value.set("".to_string());
sh.active_view.set(ActiveView::Chat);
};

rsx! {
Expand Down
1 change: 1 addition & 0 deletions crates/core/src/components/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ pub mod SendButton;
pub mod InputBox;
pub mod ChatBody;
pub mod SideBar;
pub mod NewChat;
Loading