1use chrono::{DateTime, Utc};
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9#[derive(Debug, Clone)]
11pub struct Workspace {
12 pub hash: String,
14 pub project_path: Option<String>,
16 pub workspace_path: std::path::PathBuf,
18 pub chat_sessions_path: std::path::PathBuf,
20 pub chat_session_count: usize,
22 pub has_chat_sessions: bool,
24 #[allow(dead_code)]
26 pub last_modified: Option<DateTime<Utc>>,
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct WorkspaceJson {
32 pub folder: Option<String>,
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
37#[serde(rename_all = "camelCase")]
38pub struct ChatSession {
39 #[serde(default = "default_version")]
41 pub version: u32,
42
43 #[serde(default)]
45 pub session_id: Option<String>,
46
47 #[serde(default)]
49 pub creation_date: i64,
50
51 #[serde(default)]
53 pub last_message_date: i64,
54
55 #[serde(default)]
57 pub is_imported: bool,
58
59 #[serde(default = "default_location")]
61 pub initial_location: String,
62
63 #[serde(default)]
65 pub custom_title: Option<String>,
66
67 #[serde(default)]
69 pub requester_username: Option<String>,
70
71 #[serde(default)]
73 pub requester_avatar_icon_uri: Option<serde_json::Value>,
74
75 #[serde(default)]
77 pub responder_username: Option<String>,
78
79 #[serde(default)]
81 pub responder_avatar_icon_uri: Option<serde_json::Value>,
82
83 #[serde(default)]
85 pub requests: Vec<ChatRequest>,
86}
87
88impl ChatSession {
89 pub fn collect_all_text(&self) -> String {
91 self.requests
92 .iter()
93 .flat_map(|req| {
94 let mut texts = Vec::new();
95 if let Some(msg) = &req.message {
96 if let Some(text) = &msg.text {
97 texts.push(text.as_str());
98 }
99 }
100 if let Some(resp) = &req.response {
101 if let Some(result) = resp.get("result").and_then(|v| v.as_str()) {
102 texts.push(result);
103 }
104 }
105 texts
106 })
107 .collect::<Vec<_>>()
108 .join(" ")
109 }
110
111 pub fn user_messages(&self) -> Vec<&str> {
113 self.requests
114 .iter()
115 .filter_map(|req| req.message.as_ref().and_then(|m| m.text.as_deref()))
116 .collect()
117 }
118
119 pub fn assistant_responses(&self) -> Vec<String> {
121 self.requests
122 .iter()
123 .filter_map(|req| {
124 req.response.as_ref().and_then(|r| {
125 r.get("result")
126 .and_then(|v| v.as_str())
127 .map(|s| s.to_string())
128 })
129 })
130 .collect()
131 }
132}
133
134fn default_version() -> u32 {
135 3
136}
137
138fn default_location() -> String {
139 "panel".to_string()
140}
141
142#[derive(Debug, Clone, Serialize, Deserialize)]
144#[serde(rename_all = "camelCase")]
145pub struct ChatRequest {
146 #[serde(default)]
148 pub timestamp: Option<i64>,
149
150 #[serde(default)]
152 pub message: Option<ChatMessage>,
153
154 #[serde(default)]
156 pub response: Option<serde_json::Value>,
157
158 #[serde(default)]
160 pub variable_data: Option<serde_json::Value>,
161
162 #[serde(default)]
164 pub request_id: Option<String>,
165
166 #[serde(default)]
168 pub response_id: Option<String>,
169
170 #[serde(default)]
172 pub model_id: Option<String>,
173
174 #[serde(default)]
176 pub agent: Option<serde_json::Value>,
177
178 #[serde(default)]
180 pub result: Option<serde_json::Value>,
181
182 #[serde(default)]
184 pub followups: Option<Vec<serde_json::Value>>,
185
186 #[serde(default)]
188 pub is_canceled: Option<bool>,
189
190 #[serde(default)]
192 pub content_references: Option<Vec<serde_json::Value>>,
193
194 #[serde(default)]
196 pub code_citations: Option<Vec<serde_json::Value>>,
197
198 #[serde(default)]
200 pub response_markdown_info: Option<Vec<serde_json::Value>>,
201
202 #[serde(rename = "_sourceSession", skip_serializing_if = "Option::is_none")]
204 pub source_session: Option<String>,
205}
206
207#[derive(Debug, Clone, Serialize, Deserialize)]
209#[serde(rename_all = "camelCase")]
210pub struct ChatMessage {
211 #[serde(alias = "content")]
213 pub text: Option<String>,
214
215 #[serde(default)]
217 pub parts: Option<Vec<serde_json::Value>>,
218}
219
220impl ChatMessage {
221 pub fn get_text(&self) -> String {
223 self.text.clone().unwrap_or_default()
224 }
225}
226
227#[derive(Debug, Clone, Serialize, Deserialize)]
229#[serde(rename_all = "camelCase")]
230#[allow(dead_code)]
231pub struct ChatResponse {
232 #[serde(alias = "content")]
234 pub text: Option<String>,
235
236 #[serde(default)]
238 pub parts: Option<Vec<serde_json::Value>>,
239
240 #[serde(default)]
242 pub result: Option<serde_json::Value>,
243}
244
245#[derive(Debug, Clone, Serialize, Deserialize)]
247pub struct ChatSessionIndex {
248 #[serde(default = "default_index_version")]
250 pub version: u32,
251
252 #[serde(default)]
254 pub entries: HashMap<String, ChatSessionIndexEntry>,
255}
256
257fn default_index_version() -> u32 {
258 1
259}
260
261impl Default for ChatSessionIndex {
262 fn default() -> Self {
263 Self {
264 version: 1,
265 entries: HashMap::new(),
266 }
267 }
268}
269
270#[derive(Debug, Clone, Serialize, Deserialize)]
272#[serde(rename_all = "camelCase")]
273pub struct ChatSessionIndexEntry {
274 pub session_id: String,
276
277 pub title: String,
279
280 pub last_message_date: i64,
282
283 #[serde(default)]
285 pub is_imported: bool,
286
287 #[serde(default = "default_location")]
289 pub initial_location: String,
290
291 #[serde(default)]
293 pub is_empty: bool,
294}
295
296#[derive(Debug, Clone)]
298pub struct SessionWithPath {
299 pub path: std::path::PathBuf,
300 pub session: ChatSession,
301}
302
303impl SessionWithPath {
304 #[allow(dead_code)]
306 pub fn get_session_id(&self) -> String {
307 self.session.session_id.clone().unwrap_or_else(|| {
308 self.path
309 .file_stem()
310 .map(|s| s.to_string_lossy().to_string())
311 .unwrap_or_else(|| uuid::Uuid::new_v4().to_string())
312 })
313 }
314}
315
316impl ChatSession {
317 #[allow(dead_code)]
319 pub fn get_session_id(&self) -> String {
320 self.session_id
321 .clone()
322 .unwrap_or_else(|| "unknown".to_string())
323 }
324
325 pub fn title(&self) -> String {
327 if let Some(title) = &self.custom_title {
329 if !title.is_empty() {
330 return title.clone();
331 }
332 }
333
334 if let Some(first_req) = self.requests.first() {
336 if let Some(msg) = &first_req.message {
337 if let Some(text) = &msg.text {
338 let title: String = text.chars().take(50).collect();
340 if !title.is_empty() {
341 if title.len() < text.len() {
342 return format!("{}...", title);
343 }
344 return title;
345 }
346 }
347 }
348 }
349
350 "Untitled".to_string()
351 }
352
353 pub fn is_empty(&self) -> bool {
355 self.requests.is_empty()
356 }
357
358 pub fn request_count(&self) -> usize {
360 self.requests.len()
361 }
362
363 pub fn timestamp_range(&self) -> Option<(i64, i64)> {
365 if self.requests.is_empty() {
366 return None;
367 }
368
369 let timestamps: Vec<i64> = self.requests.iter().filter_map(|r| r.timestamp).collect();
370
371 if timestamps.is_empty() {
372 return None;
373 }
374
375 let min = *timestamps.iter().min().unwrap();
376 let max = *timestamps.iter().max().unwrap();
377 Some((min, max))
378 }
379}