265 lines
8.8 KiB
Rust

use std::any::Any;
use crate::error_template::{AppError, ErrorTemplate};
use leptos::{either::Either, ev::SubmitEvent, html, prelude::*};
use leptos_meta::*;
use leptos_router::{components::*, StaticSegment};
use message::Message;
use serde::{Deserialize, Serialize};
pub mod error_template;
#[cfg(feature = "ssr")]
pub mod state;
mod message;
pub fn shell(options: LeptosOptions) -> impl IntoView {
view! {
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<AutoReload options=options.clone() />
<HydrationScripts options />
<MetaTags />
</head>
<body>
<App />
</body>
</html>
}
}
#[component]
pub fn App() -> impl IntoView {
// Provides context that manages stylesheets, titles, meta tags, etc.
provide_meta_context();
view! {
<Stylesheet id="leptos" href="/pkg/lebusiness-client.css" />
// sets the document title
<Title text="client" />
// content for this welcome page
<Router>
<main class="">
<Routes fallback=|| {
let mut outside_errors = Errors::default();
outside_errors.insert_with_default_key(AppError::NotFound);
view! { <ErrorTemplate outside_errors /> }.into_view()
}>
<Route path=StaticSegment("") view=HomePage />
</Routes>
</main>
</Router>
}
}
fn smooth_scroll_to_bottom() {
if let Some(window) = web_sys::window() {
if let Some(document) = window.document() {
if let Some(body) = document.get_element_by_id("messages") {
body.set_scroll_top(body.scroll_height());
leptos::logging::log!("moving to top");
}
}
}
}
#[component]
pub fn HomePage() -> impl IntoView {
let conversation_id = uuid::Uuid::new_v4();
let send_message = ServerAction::<SendMessage>::new();
let messages = Resource::new(
move || (send_message.version().get()),
move |_| {
get_messages(GetMessagesRequest {
conversation_id: conversation_id.clone(),
})
},
);
let existing_messages = move || {
Suspend::new(async move {
messages.await.map(|messages| {
if messages.messages.is_empty() {
Either::Left(view! { <p>"No messages sent yet"</p> })
} else {
Either::Right(
messages
.messages
.iter()
.map(move |message| {
view! {
<div class=format!(
"flex {}",
if message.role == "assistant" {
"justify-start"
} else {
"justify-end"
},
)>
<div class=format!(
"max-w-[80%] rounded-sm px-4 py-3 {}",
if message.role == "assistant" {
"bg-white border border-gray-200"
} else {
"bg-blue-500 text-white"
},
)>{message.content.clone()}</div>
</div>
}
})
.collect::<Vec<_>>(),
)
}
})
})
};
// let (_, set_messages) = signal(
// message::get_messages()
// .into_iter()
// .enumerate()
// .map(|(index, value)| (index, ArcRwSignal::new(value)))
// .collect::<Vec<_>>(),
// );
let (input, set_input) = signal("".to_string());
let on_submit = move |ev: SubmitEvent| {
// stop the page from reloading!
ev.prevent_default();
leptos::logging::log!("sending request");
send_message.dispatch(SendMessage {
request: SendMessageRequest {
conversation_id: Some(conversation_id.clone()),
role: "user".into(),
content: input.get(),
},
});
set_input.set("".into());
// let messages_len = messages.get().len();
// let mut messages = set_messages.write();
// messages.push((
// messages_len,
// ArcRwSignal::new(Message {
// role: "user".into(),
// content: input.get().into(),
// }),
// ));
// request_animation_frame(move || {
// smooth_scroll_to_bottom();
// });
};
view! {
<div class="flex flex-col h-screen bg-gray-50">
<header class="flex items-center py-4 px-4 bg-white border-b border-gray-200">
<div class="flex justify-between items-center mx-auto w-full max-w-5xl">
<h1 class="text-xl font-semibold text-gray-800">Medical Assistant</h1>
<button class="flex gap-2 items-center py-2 px-4 text-sm text-gray-600 bg-white rounded-sm border border-gray-200 hover:bg-gray-50">
New Chat
</button>
</div>
</header>
<div class="overflow-y-auto flex-1 px-4" id="messages">
<div class="py-6 mx-auto space-y-6 max-w-5xl">
<Transition fallback=move || {
view! {
<div class="flex justify-start">
<div class="py-3 px-4 bg-white rounded-sm border border-gray-200 max-w-[80%]">
"Loading..."
</div>
</div>
}
}>
<ErrorBoundary fallback=|errors| {
view! { <ErrorTemplate errors /> }
}>{existing_messages}</ErrorBoundary>
</Transition>
<div />
</div>
</div>
<div class="py-4 px-4 bg-white border-t border-gray-200">
<form class="mx-auto max-w-5xl" on:submit=on_submit>
<div class="flex gap-4">
<input
type="text"
placeholder="Type your medical question here..."
class="flex-1 py-2 px-4 rounded-sm border border-gray-200 focus:border-transparent focus:ring-2 focus:ring-blue-500 focus:outline-none"
bind:value=(input, set_input)
/>
<button
type="submit"
class="flex gap-2 items-center py-2 px-4 text-white bg-blue-500 rounded-sm hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed"
>
Send
</button>
</div>
</form>
</div>
</div>
}
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct GetMessagesRequest {
conversation_id: uuid::Uuid,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct Messages {
pub messages: Vec<Message>,
}
#[server]
pub async fn get_messages(req: GetMessagesRequest) -> Result<Messages, ServerFnError> {
let messages: Vec<Message> = reqwest::get(format!(
"https://lebusiness-service.prod.kjuulh.app/api/messages?conversation_id={}",
req.conversation_id,
))
.await
.map_err(|e| ServerFnError::new(e.to_string()))?
.json()
.await
.map_err(|e| ServerFnError::new(e.to_string()))?;
Ok(Messages { messages })
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct SendMessageRequest {
conversation_id: Option<uuid::Uuid>,
role: String,
content: String,
}
#[server]
pub async fn send_message(request: SendMessageRequest) -> Result<(), ServerFnError> {
let client = reqwest::Client::new();
client
.post("https://lebusiness-service.prod.kjuulh.app/api/messages")
.json(&request)
.send()
.await
.map_err(|e| ServerFnError::new(e.to_string()))?;
Ok(())
}