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

View File

@@ -187,13 +187,7 @@ pub fn ChatPage() -> Element {
match chat_complete(sid.clone(), messages_json).await { match chat_complete(sid.clone(), messages_json).await {
Ok(response) => { Ok(response) => {
// Save assistant message // Save assistant message
match save_chat_message( match save_chat_message(sid, "assistant".to_string(), response).await {
sid,
"assistant".to_string(),
response,
)
.await
{
Ok(msg) => { Ok(msg) => {
messages.write().push(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 dioxus_sdk::storage::use_persistent;
use crate::components::{ArticleDetail, DashboardSidebar, NewsCardView, PageHeader}; use crate::components::{ArticleDetail, DashboardSidebar, NewsCardView, PageHeader};
use crate::infrastructure::chat::{create_chat_session, save_chat_message};
use crate::infrastructure::llm::FollowUpMessage; use crate::infrastructure::llm::FollowUpMessage;
use crate::models::NewsCard; use crate::models::NewsCard;
@@ -50,6 +51,8 @@ pub fn DashboardPage() -> Element {
let mut is_chatting = use_signal(|| false); let mut is_chatting = use_signal(|| false);
// Stores the article text context for the chat system message // Stores the article text context for the chat system message
let mut article_context = use_signal(String::new); 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) // Recent search history, persisted in localStorage (capped at MAX_RECENT_SEARCHES)
let mut recent_searches = let mut recent_searches =
@@ -310,6 +313,7 @@ pub fn DashboardPage() -> Element {
summary.set(None); summary.set(None);
chat_messages.set(Vec::new()); chat_messages.set(Vec::new());
article_context.set(String::new()); article_context.set(String::new());
news_session_id.set(None);
let oll_url = ollama_url.read().clone(); let oll_url = ollama_url.read().clone();
@@ -358,6 +362,7 @@ pub fn DashboardPage() -> Element {
selected_card.set(None); selected_card.set(None);
summary.set(None); summary.set(None);
chat_messages.set(Vec::new()); chat_messages.set(Vec::new());
news_session_id.set(None);
}, },
summary: summary.read().clone(), summary: summary.read().clone(),
is_summarizing: *is_summarizing.read(), is_summarizing: *is_summarizing.read(),
@@ -367,49 +372,110 @@ pub fn DashboardPage() -> Element {
let oll_url = ollama_url.read().clone(); let oll_url = ollama_url.read().clone();
let mdl = ollama_model.read().clone(); let mdl = ollama_model.read().clone();
let ctx = article_context.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 // Append user message to local chat
chat_messages chat_messages.write().push(FollowUpMessage {
role: "user".into(),
content: question.clone(),
});
// Build full message history for Ollama // Build full message history for Ollama
let system_msg = format!(
.write()
.push(FollowUpMessage {
role: "user".into(),
content: question,
});
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 \ "You are a helpful assistant. The user is reading \
a news article. Use the following context to answer \ a news article. Use the following context to answer \
their questions. Do NOT comment on the source, \ their questions. Do NOT comment on the source, \
dates, URLs, or formatting.\n\n{ctx}", dates, URLs, or formatting.\n\n{ctx}",
), );
}, let msgs = {
]; let history = chat_messages.read();
let mut all = vec![FollowUpMessage {
role: "system".into(),
content: system_msg.clone(),
}];
all.extend(history.iter().cloned()); all.extend(history.iter().cloned());
all all
}; };
spawn(async move { spawn(async move {
is_chatting.set(true); 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) => { Ok(reply) => {
chat_messages // Persist assistant message
.write() if !sid.is_empty() {
.push(FollowUpMessage { let _ = save_chat_message(
sid,
"assistant".to_string(),
reply.clone(),
)
.await;
}
chat_messages.write().push(FollowUpMessage {
role: "assistant".into(), role: "assistant".into(),
content: reply, content: reply,
}); });
} }
Err(e) => { Err(e) => {
tracing::error!("Chat failed: {e}"); tracing::error!("Chat failed: {e}");
chat_messages chat_messages.write().push(FollowUpMessage {
.write()
.push(FollowUpMessage {
role: "assistant".into(), role: "assistant".into(),
content: format!("Error: {e}"), content: format!("Error: {e}"),
}); });