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! { } } #[component] pub fn App() -> impl IntoView { // Provides context that manages stylesheets, titles, meta tags, etc. provide_meta_context(); view! { // sets the document title // 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(()) }