1use axum::{
4 extract::{State, Path},
5 response::{IntoResponse, Response},
6 Json,
7};
8use axum::http::StatusCode;
9use serde::{Deserialize, Serialize};
10use serde_json::Value;
11use tracing::{info, error};
12
13use crate::shared_state::UnifiedAppState;
14
15#[derive(Debug, Serialize)]
17pub struct ConversationsResponse {
18 pub conversations: Vec<ConversationSummary>,
19}
20
21#[derive(Debug, Serialize)]
23pub struct ConversationSummary {
24 pub id: String,
25 pub title: String,
26 pub created_at: String,
27 pub last_accessed: String,
28 pub message_count: usize,
29 pub pinned: bool,
30}
31
32#[derive(Debug, Serialize)]
34pub struct ConversationDetailResponse {
35 pub id: String,
36 pub title: String,
37 pub messages: Vec<MessageResponse>,
38}
39
40#[derive(Debug, Serialize)]
42pub struct MessageResponse {
43 pub role: String,
44 pub content: String,
45}
46
47pub async fn get_conversations(
49 State(state): State<UnifiedAppState>,
50) -> Result<Json<ConversationsResponse>, Response> {
51 info!("Fetching all conversations");
52
53 let orchestrator_lock = state.context_orchestrator.read().await;
54
55 if let Some(ref orchestrator) = *orchestrator_lock {
56 match orchestrator.database().conversations.get_all_sessions() {
57 Ok(sessions) => {
58 let mut conversations = Vec::new();
59
60 for session in sessions {
61 let message_count = orchestrator.database().conversations
63 .get_session_message_count(&session.id)
64 .unwrap_or(0);
65
66 if let Some(ref title) = session.metadata.title {
68 conversations.push(ConversationSummary {
69 id: session.id.clone(),
70 title: title.clone(),
71 created_at: session.created_at.to_rfc3339(),
72 last_accessed: session.last_accessed.to_rfc3339(),
73 message_count,
74 pinned: session.metadata.pinned,
75 });
76 } else if message_count > 0 {
77 let first_message = orchestrator.database().conversations
79 .get_session_messages(&session.id, Some(1), Some(0))
80 .ok()
81 .and_then(|msgs| msgs.into_iter().find(|m| m.role == "user"));
82
83 let title = first_message
84 .map(|m| {
85 let content = m.content.chars().take(50).collect::<String>();
86 if m.content.len() > 50 {
87 format!("{}...", content)
88 } else {
89 content
90 }
91 })
92 .unwrap_or_else(|| format!("Chat {}", session.created_at.format("%b %d, %Y")));
93
94 conversations.push(ConversationSummary {
95 id: session.id.clone(),
96 title,
97 created_at: session.created_at.to_rfc3339(),
98 last_accessed: session.last_accessed.to_rfc3339(),
99 message_count,
100 pinned: session.metadata.pinned,
101 });
102 }
103 }
104
105 info!("Found {} conversations", conversations.len());
106 Ok(Json(ConversationsResponse { conversations }))
107 }
108 Err(e) => {
109 error!("Failed to fetch conversations: {}", e);
110 Err((StatusCode::INTERNAL_SERVER_ERROR, format!("Database error: {}", e)).into_response())
111 }
112 }
113 } else {
114 error!("Context orchestrator not initialized");
115 Err((StatusCode::SERVICE_UNAVAILABLE, "Memory system not available").into_response())
116 }
117}
118
119pub async fn get_conversation(
121 State(state): State<UnifiedAppState>,
122 Path(session_id): Path<String>,
123) -> Result<Json<ConversationDetailResponse>, Response> {
124 info!("Fetching conversation: {}", session_id);
125
126 let orchestrator_lock = state.context_orchestrator.read().await;
127
128 if let Some(ref orchestrator) = *orchestrator_lock {
129 let session = match orchestrator.database().conversations.get_session(&session_id) {
131 Ok(Some(s)) => s,
132 Ok(None) => {
133 return Err((StatusCode::NOT_FOUND, "Conversation not found").into_response());
134 }
135 Err(e) => {
136 error!("Failed to fetch session: {}", e);
137 return Err((StatusCode::INTERNAL_SERVER_ERROR, format!("Database error: {}", e)).into_response());
138 }
139 };
140
141 let messages = match orchestrator.database().conversations.get_session_messages(&session_id, None, None) {
143 Ok(msgs) => msgs.into_iter()
144 .map(|msg| MessageResponse {
145 role: msg.role,
146 content: msg.content,
147 })
148 .collect(),
149 Err(e) => {
150 error!("Failed to fetch messages: {}", e);
151 return Err((StatusCode::INTERNAL_SERVER_ERROR, format!("Database error: {}", e)).into_response());
152 }
153 };
154
155 Ok(Json(ConversationDetailResponse {
156 id: session.id,
157 title: session.metadata.title.unwrap_or_else(|| "New Chat".to_string()),
158 messages,
159 }))
160 } else {
161 error!("Context orchestrator not initialized");
162 Err((StatusCode::SERVICE_UNAVAILABLE, "Memory system not available").into_response())
163 }
164}
165
166#[derive(Debug, Deserialize)]
168pub struct UpdateTitleRequest {
169 pub title: String,
170}
171
172pub async fn update_conversation_title(
174 State(state): State<UnifiedAppState>,
175 Path(session_id): Path<String>,
176 Json(req): Json<UpdateTitleRequest>,
177) -> Result<Json<Value>, Response> {
178 info!("Updating title for conversation: {}", session_id);
179
180 if req.title.is_empty() {
181 return Err((StatusCode::BAD_REQUEST, "Title cannot be empty").into_response());
182 }
183
184 let orchestrator_lock = state.context_orchestrator.read().await;
185
186 if let Some(ref orchestrator) = *orchestrator_lock {
187 match orchestrator.database().conversations.update_session_title(&session_id, &req.title) {
188 Ok(_) => {
189 info!("Successfully updated title for conversation: {}", session_id);
190 Ok(Json(serde_json::json!({
191 "success": true,
192 "id": session_id,
193 "title": req.title
194 })))
195 }
196 Err(e) => {
197 error!("Failed to update conversation title for session {}: {}", session_id, e);
199 Err((StatusCode::INTERNAL_SERVER_ERROR, format!("Database error: {}", e)).into_response())
200 }
201 }
202 } else {
203 error!("Context orchestrator not initialized");
204 Err((StatusCode::SERVICE_UNAVAILABLE, "Memory system not available").into_response())
205 }
206}
207
208pub async fn delete_conversation(
212 State(state): State<UnifiedAppState>,
213 Path(session_id): Path<String>,
214) -> Result<Json<Value>, Response> {
215 info!("Deleting conversation: {}", session_id);
216
217 let orchestrator_lock = state.context_orchestrator.read().await;
218
219 if let Some(ref orchestrator) = *orchestrator_lock {
220 match orchestrator.database().conversations.delete_session(&session_id) {
221 Ok(deleted_count) => {
222 if deleted_count == 0 {
223 info!("Conversation not found for deletion: {}", session_id);
224 Err((StatusCode::NOT_FOUND, format!("Conversation not found: {}", session_id)).into_response())
225 } else {
226 info!("Successfully deleted conversation: {}", session_id);
227 Ok(Json(serde_json::json!({
228 "success": true,
229 "id": session_id
230 })))
231 }
232 }
233 Err(e) => {
234 error!("Failed to delete conversation: {}", e);
235 Err((StatusCode::INTERNAL_SERVER_ERROR, format!("Database error: {}", e)).into_response())
237 }
238 }
239 } else {
240 error!("Context orchestrator not initialized");
241 Err((StatusCode::SERVICE_UNAVAILABLE, "Memory system not available").into_response())
242 }
243}
244
245#[derive(Debug, Serialize)]
247pub struct ConversationsDbStatsResponse {
248 pub db_path: String,
250 pub db_size_bytes: u64,
252 pub db_size_human: String,
254 pub total_sessions: i64,
256 pub total_messages: i64,
258 pub total_kv_snapshots: i64,
260 pub total_embeddings: i64,
262 pub total_details: i64,
264 pub total_summaries: i64,
266 pub last_accessed: Option<String>,
268}
269
270fn format_bytes_local(bytes: u64) -> String {
271 const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB"];
272 if bytes == 0 { return "0 B".to_string(); }
273 let i = (bytes as f64).log(1024.0).floor() as usize;
274 let i = i.min(UNITS.len() - 1);
275 let val = bytes as f64 / 1024_f64.powi(i as i32);
276 format!("{:.1} {}", val, UNITS[i])
277}
278
279pub async fn get_conversations_db_stats(
281 State(state): State<UnifiedAppState>,
282) -> Result<Json<ConversationsDbStatsResponse>, Response> {
283 info!("Fetching conversations DB stats");
284
285 let orchestrator_lock = state.context_orchestrator.read().await;
286
287 let (
288 total_sessions,
289 total_messages,
290 total_kv_snapshots,
291 total_embeddings,
292 total_details,
293 total_summaries,
294 last_accessed,
295 ) = if let Some(ref orchestrator) = *orchestrator_lock {
296 let db = orchestrator.database();
297 let conn_result = db.conversations.get_conn_public();
298 match conn_result {
299 Ok(conn) => {
300 let count = |sql: &str| -> i64 {
301 conn.query_row(sql, [], |r| r.get::<_, i64>(0)).unwrap_or(0)
302 };
303 let sessions = count("SELECT COUNT(*) FROM sessions");
304 let messages = count("SELECT COUNT(*) FROM messages");
305 let snapshots = count("SELECT COUNT(*) FROM kv_snapshots");
306 let embeddings = count("SELECT COUNT(*) FROM embeddings");
307 let details = count("SELECT COUNT(*) FROM details");
308 let summaries = count("SELECT COUNT(*) FROM summaries");
309 let last: Option<String> = conn
310 .query_row(
311 "SELECT MAX(last_accessed) FROM sessions",
312 [],
313 |r| r.get(0),
314 )
315 .unwrap_or(None);
316 (sessions, messages, snapshots, embeddings, details, summaries, last)
317 }
318 Err(e) => {
319 error!("Failed to get DB connection for stats: {}", e);
320 (0, 0, 0, 0, 0, 0, None)
321 }
322 }
323 } else {
324 error!("Context orchestrator not initialized");
325 return Err((StatusCode::SERVICE_UNAVAILABLE, "Memory system not available").into_response());
326 };
327
328 let db_path = dirs::data_dir()
330 .unwrap_or_else(|| std::env::current_dir().unwrap_or_default())
331 .join("Aud.io")
332 .join("data")
333 .join("memory.db");
334
335 let db_size_bytes = std::fs::metadata(&db_path).map(|m| m.len()).unwrap_or(0);
336
337 Ok(Json(ConversationsDbStatsResponse {
338 db_path: db_path.to_string_lossy().to_string(),
339 db_size_bytes,
340 db_size_human: format_bytes_local(db_size_bytes),
341 total_sessions,
342 total_messages,
343 total_kv_snapshots,
344 total_embeddings,
345 total_details,
346 total_summaries,
347 last_accessed,
348 }))
349}
350
351#[derive(Debug, Deserialize)]
353pub struct UpdatePinnedRequest {
354 pub pinned: bool,
355}
356
357pub async fn update_conversation_pinned(
359 State(state): State<UnifiedAppState>,
360 Path(session_id): Path<String>,
361 Json(req): Json<UpdatePinnedRequest>,
362) -> Result<Json<Value>, Response> {
363 info!("Updating pinned status for conversation: {} to {}", session_id, req.pinned);
364
365 let orchestrator_lock = state.context_orchestrator.read().await;
366
367 if let Some(ref orchestrator) = *orchestrator_lock {
368 match orchestrator.database().conversations.update_session_pinned(&session_id, req.pinned) {
369 Ok(_) => {
370 info!("Successfully updated pinned status for conversation: {}", session_id);
371 Ok(Json(serde_json::json!({
372 "success": true,
373 "id": session_id,
374 "pinned": req.pinned
375 })))
376 }
377 Err(e) => {
378 let error_msg = e.to_string();
379 if error_msg.contains("not found") {
381 error!("Conversation not found: {}", session_id);
382 Err((StatusCode::NOT_FOUND, format!("Conversation not found: {}", session_id)).into_response())
383 } else {
384 error!("Failed to update conversation pinned status: {}", e);
385 Err((StatusCode::INTERNAL_SERVER_ERROR, format!("Database error: {}", e)).into_response())
386 }
387 }
388 }
389 } else {
390 error!("Context orchestrator not initialized");
391 Err((StatusCode::SERVICE_UNAVAILABLE, "Memory system not available").into_response())
392 }
393}