feat(chat): integrate News namespace sessions from dashboard

Dashboard article follow-up chats now persist to MongoDB as News
namespace sessions, making them visible in the Chat page sidebar
under "News Chats".

On first follow-up message: creates a News session with the article
title/URL, saves the system context message, then persists each
user and assistant message. Subsequent messages in the same article
reuse the existing session ID.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Sharang Parnerkar
2026-02-20 19:43:35 +01:00
parent 1a244f8f3d
commit 0ca0366f3a
3 changed files with 101 additions and 45 deletions

View File

@@ -62,10 +62,7 @@ impl Database {
/// Raw BSON document collection for queries that need manual
/// `_id` → `String` conversion (avoids `ObjectId` deserialization issues).
pub fn raw_collection(
&self,
name: &str,
) -> Collection<mongodb::bson::Document> {
pub fn raw_collection(&self, name: &str) -> Collection<mongodb::bson::Document> {
self.inner.collection(name)
}
}

View File

@@ -187,13 +187,7 @@ pub fn ChatPage() -> Element {
match chat_complete(sid.clone(), messages_json).await {
Ok(response) => {
// Save assistant message
match save_chat_message(
sid,
"assistant".to_string(),
response,
)
.await
{
match save_chat_message(sid, "assistant".to_string(), response).await {
Ok(msg) => {
messages.write().push(msg);
}
@@ -258,4 +252,3 @@ pub fn ChatPage() -> Element {
}
}
}

View File

@@ -2,6 +2,7 @@ use dioxus::prelude::*;
use dioxus_sdk::storage::use_persistent;
use crate::components::{ArticleDetail, DashboardSidebar, NewsCardView, PageHeader};
use crate::infrastructure::chat::{create_chat_session, save_chat_message};
use crate::infrastructure::llm::FollowUpMessage;
use crate::models::NewsCard;
@@ -50,6 +51,8 @@ pub fn DashboardPage() -> Element {
let mut is_chatting = use_signal(|| false);
// Stores the article text context for the chat system message
let mut article_context = use_signal(String::new);
// MongoDB session ID for persisting News chat (created on first follow-up)
let mut news_session_id: Signal<Option<String>> = use_signal(|| None);
// Recent search history, persisted in localStorage (capped at MAX_RECENT_SEARCHES)
let mut recent_searches =
@@ -310,6 +313,7 @@ pub fn DashboardPage() -> Element {
summary.set(None);
chat_messages.set(Vec::new());
article_context.set(String::new());
news_session_id.set(None);
let oll_url = ollama_url.read().clone();
@@ -358,6 +362,7 @@ pub fn DashboardPage() -> Element {
selected_card.set(None);
summary.set(None);
chat_messages.set(Vec::new());
news_session_id.set(None);
},
summary: summary.read().clone(),
is_summarizing: *is_summarizing.read(),
@@ -367,52 +372,113 @@ pub fn DashboardPage() -> Element {
let oll_url = ollama_url.read().clone();
let mdl = ollama_model.read().clone();
let ctx = article_context.read().clone();
// Capture article info for News session creation
let card_title = selected_card
.read()
.as_ref()
.map(|c| c.title.clone())
.unwrap_or_default();
let card_url = selected_card
.read()
.as_ref()
.map(|c| c.url.clone())
.unwrap_or_default();
// Append user message to chat
chat_messages
// Append user message to local chat
chat_messages.write().push(FollowUpMessage {
role: "user".into(),
content: question.clone(),
});
// Build full message history for Ollama
.write()
.push(FollowUpMessage {
role: "user".into(),
content: question,
});
// Build full message history for Ollama
let system_msg = format!(
"You are a helpful assistant. The user is reading \
a news article. Use the following context to answer \
their questions. Do NOT comment on the source, \
dates, URLs, or formatting.\n\n{ctx}",
);
let msgs = {
let history = chat_messages.read();
let mut all = vec![
FollowUpMessage {
role: "system".into(),
content: format!(
"You are a helpful assistant. The user is reading \
a news article. Use the following context to answer \
their questions. Do NOT comment on the source, \
dates, URLs, or formatting.\n\n{ctx}",
),
},
];
let mut all = vec![FollowUpMessage {
role: "system".into(),
content: system_msg.clone(),
}];
all.extend(history.iter().cloned());
all
};
spawn(async move {
is_chatting.set(true);
match crate::infrastructure::llm::chat_followup(msgs, oll_url, mdl).await {
// Create News session on first follow-up message
let existing_sid = news_session_id.read().clone();
let sid = if let Some(id) = existing_sid {
id
} else {
match create_chat_session(
card_title,
"News".to_string(),
"ollama".to_string(),
mdl.clone(),
card_url,
)
.await
{
Ok(session) => {
let id = session.id.clone();
news_session_id.set(Some(id.clone()));
// Persist system context as first message
let _ = save_chat_message(
id.clone(),
"system".to_string(),
system_msg,
)
.await;
id
}
Err(e) => {
tracing::error!("Failed to create News session: {e}");
String::new()
}
}
};
// Persist user message
if !sid.is_empty() {
let _ = save_chat_message(
sid.clone(),
"user".to_string(),
question,
)
.await;
}
match crate::infrastructure::llm::chat_followup(
msgs, oll_url, mdl,
)
.await
{
Ok(reply) => {
chat_messages
.write()
.push(FollowUpMessage {
role: "assistant".into(),
content: reply,
});
// Persist assistant message
if !sid.is_empty() {
let _ = save_chat_message(
sid,
"assistant".to_string(),
reply.clone(),
)
.await;
}
chat_messages.write().push(FollowUpMessage {
role: "assistant".into(),
content: reply,
});
}
Err(e) => {
tracing::error!("Chat failed: {e}");
chat_messages
.write()
.push(FollowUpMessage {
role: "assistant".into(),
content: format!("Error: {e}"),
});
chat_messages.write().push(FollowUpMessage {
role: "assistant".into(),
content: format!("Error: {e}"),
});
}
}
is_chatting.set(false);