199 lines
7.2 KiB
Rust
199 lines
7.2 KiB
Rust
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::<ChatMsg>::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<HelpChatHistoryMessage> = 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<HelpChatHistoryMessage> = 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("**", "<strong>")
|
|
.replace("\n\n", "<br><br>")
|
|
.replace("\n- ", "<br>- ")
|
|
.replace("`", "<code>")
|
|
} 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 }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|