use dioxus::prelude::*; use dioxus_free_icons::icons::bs_icons::*; use dioxus_free_icons::Icon; use crate::infrastructure::help_chat::{send_help_chat_message, HelpChatHistoryMessage}; // ── Message model ──────────────────────────────────────────────────────────── #[derive(Clone, Debug)] struct ChatMsg { role: String, content: String, } // ── Component ──────────────────────────────────────────────────────────────── #[component] pub fn HelpChat() -> Element { let mut is_open = use_signal(|| false); let mut messages = use_signal(Vec::::new); let mut input_text = use_signal(String::new); let mut is_loading = use_signal(|| false); // Send message handler let on_send = move |_| { let text = input_text().trim().to_string(); if text.is_empty() || is_loading() { return; } // Push user message messages.write().push(ChatMsg { role: "user".into(), content: text.clone(), }); input_text.set(String::new()); is_loading.set(true); // Build history for API call (exclude last user message, it goes as `message`) let history: Vec = messages() .iter() .rev() .skip(1) // skip the user message we just added .rev() .map(|m| HelpChatHistoryMessage { role: m.role.clone(), content: m.content.clone(), }) .collect(); spawn(async move { match send_help_chat_message(text, history).await { Ok(resp) => { messages.write().push(ChatMsg { role: "assistant".into(), content: resp.data.message, }); } Err(e) => { messages.write().push(ChatMsg { role: "assistant".into(), content: format!("Error: {e}"), }); } } is_loading.set(false); }); }; // Key handler for Enter to send let on_keydown = move |e: KeyboardEvent| { if e.key() == Key::Enter && !e.modifiers().shift() { e.prevent_default(); let text = input_text().trim().to_string(); if text.is_empty() || is_loading() { return; } messages.write().push(ChatMsg { role: "user".into(), content: text.clone(), }); input_text.set(String::new()); is_loading.set(true); let history: Vec = messages() .iter() .rev() .skip(1) .rev() .map(|m| HelpChatHistoryMessage { role: m.role.clone(), content: m.content.clone(), }) .collect(); spawn(async move { match send_help_chat_message(text, history).await { Ok(resp) => { messages.write().push(ChatMsg { role: "assistant".into(), content: resp.data.message, }); } Err(e) => { messages.write().push(ChatMsg { role: "assistant".into(), content: format!("Error: {e}"), }); } } is_loading.set(false); }); } }; rsx! { // Floating toggle button if !is_open() { button { class: "help-chat-toggle", onclick: move |_| is_open.set(true), title: "Help", Icon { icon: BsQuestionCircle, width: 22, height: 22 } } } // Chat panel if is_open() { div { class: "help-chat-panel", // Header div { class: "help-chat-header", span { class: "help-chat-title", Icon { icon: BsRobot, width: 16, height: 16 } "Help Assistant" } button { class: "help-chat-close", onclick: move |_| is_open.set(false), Icon { icon: BsX, width: 18, height: 18 } } } // Messages area div { class: "help-chat-messages", if messages().is_empty() { div { class: "help-chat-empty", p { "Ask me anything about the Compliance Scanner." } p { class: "help-chat-hint", "e.g. \"How do I add a repository?\" or \"What is SBOM?\"" } } } for (i, msg) in messages().iter().enumerate() { div { key: "{i}", class: if msg.role == "user" { "help-msg help-msg-user" } else { "help-msg help-msg-assistant" }, div { class: "help-msg-content", dangerous_inner_html: if msg.role == "assistant" { // Basic markdown rendering: bold, code, newlines msg.content .replace("**", "") .replace("\n\n", "

") .replace("\n- ", "
- ") .replace("`", "") } else { msg.content.clone() } } } } if is_loading() { div { class: "help-msg help-msg-assistant", div { class: "help-msg-loading", "Thinking..." } } } } // Input area div { class: "help-chat-input", input { r#type: "text", placeholder: "Ask a question...", value: "{input_text}", disabled: is_loading(), oninput: move |e| input_text.set(e.value()), onkeydown: on_keydown, } button { class: "help-chat-send", disabled: is_loading() || input_text().trim().is_empty(), onclick: on_send, Icon { icon: BsSend, width: 14, height: 14 } } } } } } }