Files
compliance-scanner-agent/compliance-dashboard/src/pages/chat.rs
Sharang Parnerkar 89c30a62dd
Some checks failed
CI / Security Audit (push) Has been cancelled
CI / Tests (push) Has been cancelled
CI / Clippy (push) Has been cancelled
CI / Format (push) Failing after 3s
Add RAG embedding and AI chat feature
Implement end-to-end RAG pipeline: AST-aware code chunking, LiteLLM
embedding generation, MongoDB vector storage with brute-force cosine
similarity fallback for self-hosted instances, and a chat API with
RAG-augmented responses. Add dedicated /chat/:repo_id dashboard page
with embedding build controls, message history, and source reference
cards.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 23:29:40 +01:00

233 lines
8.7 KiB
Rust

use dioxus::prelude::*;
use crate::components::page_header::PageHeader;
use crate::infrastructure::chat::{
fetch_embedding_status, send_chat_message, trigger_embedding_build, ChatHistoryMessage,
SourceRef,
};
/// A UI-level chat message
#[derive(Clone, Debug)]
struct UiChatMessage {
role: String,
content: String,
sources: Vec<SourceRef>,
}
#[component]
pub fn ChatPage(repo_id: String) -> Element {
let mut messages: Signal<Vec<UiChatMessage>> = use_signal(Vec::new);
let mut input_text = use_signal(String::new);
let mut loading = use_signal(|| false);
let mut building = use_signal(|| false);
let repo_id_for_status = repo_id.clone();
let mut embedding_status = use_resource(move || {
let rid = repo_id_for_status.clone();
async move { fetch_embedding_status(rid).await.ok() }
});
let has_embeddings = {
let status = embedding_status.read();
match &*status {
Some(Some(resp)) => resp
.data
.as_ref()
.map(|d| d.status == "completed")
.unwrap_or(false),
_ => false,
}
};
let embedding_status_text = {
let status = embedding_status.read();
match &*status {
Some(Some(resp)) => match &resp.data {
Some(d) => match d.status.as_str() {
"completed" => format!(
"Embeddings ready: {}/{} chunks",
d.embedded_chunks, d.total_chunks
),
"running" => format!(
"Building embeddings: {}/{}...",
d.embedded_chunks, d.total_chunks
),
"failed" => format!(
"Embedding build failed: {}",
d.error_message.as_deref().unwrap_or("unknown error")
),
s => format!("Status: {s}"),
},
None => "No embeddings built yet".to_string(),
},
Some(None) => "Failed to check embedding status".to_string(),
None => "Checking embedding status...".to_string(),
}
};
let repo_id_for_build = repo_id.clone();
let on_build = move |_| {
let rid = repo_id_for_build.clone();
building.set(true);
spawn(async move {
let _ = trigger_embedding_build(rid).await;
building.set(false);
embedding_status.restart();
});
};
let repo_id_for_send = repo_id.clone();
let mut do_send = move || {
let text = input_text.read().trim().to_string();
if text.is_empty() || *loading.read() {
return;
}
let rid = repo_id_for_send.clone();
let user_msg = text.clone();
// Add user message to UI
messages.write().push(UiChatMessage {
role: "user".to_string(),
content: user_msg.clone(),
sources: Vec::new(),
});
input_text.set(String::new());
loading.set(true);
spawn(async move {
// Build history from existing messages
let history: Vec<ChatHistoryMessage> = messages
.read()
.iter()
.filter(|m| m.role == "user" || m.role == "assistant")
.rev()
.skip(1) // skip the message we just added
.take(10) // limit history
.collect::<Vec<_>>()
.into_iter()
.rev()
.map(|m| ChatHistoryMessage {
role: m.role.clone(),
content: m.content.clone(),
})
.collect();
match send_chat_message(rid, user_msg, history).await {
Ok(resp) => {
messages.write().push(UiChatMessage {
role: "assistant".to_string(),
content: resp.data.message,
sources: resp.data.sources,
});
}
Err(e) => {
messages.write().push(UiChatMessage {
role: "assistant".to_string(),
content: format!("Error: {e}"),
sources: Vec::new(),
});
}
}
loading.set(false);
});
};
let mut do_send_click = do_send.clone();
rsx! {
PageHeader { title: "AI Chat" }
// Embedding status banner
div { class: "chat-embedding-banner",
span { "{embedding_status_text}" }
button {
class: "btn btn-sm",
disabled: *building.read(),
onclick: on_build,
if *building.read() { "Building..." } else { "Build Embeddings" }
}
}
div { class: "chat-container",
// Message list
div { class: "chat-messages",
if messages.read().is_empty() && !*loading.read() {
div { class: "chat-empty",
h3 { "Ask anything about your codebase" }
p { "Build embeddings first, then ask questions about functions, architecture, patterns, and more." }
}
}
for (i, msg) in messages.read().iter().enumerate() {
{
let class = if msg.role == "user" {
"chat-message chat-message-user"
} else {
"chat-message chat-message-assistant"
};
let content = msg.content.clone();
let sources = msg.sources.clone();
rsx! {
div { class: class, key: "{i}",
div { class: "chat-message-role",
if msg.role == "user" { "You" } else { "Assistant" }
}
div { class: "chat-message-content", "{content}" }
if !sources.is_empty() {
div { class: "chat-sources",
span { class: "chat-sources-label", "Sources:" }
for src in sources {
div { class: "chat-source-card",
div { class: "chat-source-header",
span { class: "chat-source-name",
"{src.qualified_name}"
}
span { class: "chat-source-location",
"{src.file_path}:{src.start_line}-{src.end_line}"
}
}
pre { class: "chat-source-snippet",
code { "{src.snippet}" }
}
}
}
}
}
}
}
}
}
if *loading.read() {
div { class: "chat-message chat-message-assistant",
div { class: "chat-message-role", "Assistant" }
div { class: "chat-message-content chat-typing", "Thinking..." }
}
}
}
// Input area
div { class: "chat-input-area",
textarea {
class: "chat-input",
placeholder: "Ask about your codebase...",
value: "{input_text}",
disabled: !has_embeddings,
oninput: move |e| input_text.set(e.value()),
onkeydown: move |e: Event<KeyboardData>| {
if e.key() == Key::Enter && !e.modifiers().shift() {
e.prevent_default();
do_send();
}
},
}
button {
class: "btn chat-send-btn",
disabled: *loading.read() || !has_embeddings,
onclick: move |_| do_send_click(),
"Send"
}
}
}
}
}