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:
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,52 +372,113 @@ 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()
|
"You are a helpful assistant. The user is reading \
|
||||||
.push(FollowUpMessage {
|
a news article. Use the following context to answer \
|
||||||
role: "user".into(),
|
their questions. Do NOT comment on the source, \
|
||||||
content: question,
|
dates, URLs, or formatting.\n\n{ctx}",
|
||||||
});
|
);
|
||||||
let msgs = {
|
let msgs = {
|
||||||
let history = chat_messages.read();
|
let history = chat_messages.read();
|
||||||
let mut all = vec![
|
let mut all = vec![FollowUpMessage {
|
||||||
FollowUpMessage {
|
role: "system".into(),
|
||||||
role: "system".into(),
|
content: system_msg.clone(),
|
||||||
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}",
|
|
||||||
),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
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(
|
||||||
role: "assistant".into(),
|
sid,
|
||||||
content: reply,
|
"assistant".to_string(),
|
||||||
});
|
reply.clone(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
chat_messages.write().push(FollowUpMessage {
|
||||||
|
role: "assistant".into(),
|
||||||
|
content: reply,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::error!("Chat failed: {e}");
|
tracing::error!("Chat failed: {e}");
|
||||||
chat_messages
|
chat_messages.write().push(FollowUpMessage {
|
||||||
.write()
|
role: "assistant".into(),
|
||||||
.push(FollowUpMessage {
|
content: format!("Error: {e}"),
|
||||||
role: "assistant".into(),
|
});
|
||||||
content: format!("Error: {e}"),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is_chatting.set(false);
|
is_chatting.set(false);
|
||||||
|
|||||||
Reference in New Issue
Block a user