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
142fn default_response_state() -> u8 {
144 1
145}
146
147#[derive(Debug, Clone, Serialize, Deserialize)]
149#[serde(rename_all = "camelCase")]
150pub struct ChatRequest {
151 #[serde(default)]
153 pub timestamp: Option<i64>,
154
155 #[serde(default)]
157 pub message: Option<ChatMessage>,
158
159 #[serde(default)]
161 pub response: Option<serde_json::Value>,
162
163 #[serde(default)]
165 pub variable_data: Option<serde_json::Value>,
166
167 #[serde(default)]
169 pub request_id: Option<String>,
170
171 #[serde(default)]
173 pub response_id: Option<String>,
174
175 #[serde(default)]
177 pub model_id: Option<String>,
178
179 #[serde(default)]
181 pub agent: Option<serde_json::Value>,
182
183 #[serde(default)]
185 pub result: Option<serde_json::Value>,
186
187 #[serde(default)]
189 pub followups: Option<Vec<serde_json::Value>>,
190
191 #[serde(default)]
193 pub is_canceled: Option<bool>,
194
195 #[serde(default)]
197 pub content_references: Option<Vec<serde_json::Value>>,
198
199 #[serde(default)]
201 pub code_citations: Option<Vec<serde_json::Value>>,
202
203 #[serde(default)]
205 pub response_markdown_info: Option<Vec<serde_json::Value>>,
206
207 #[serde(rename = "_sourceSession", skip_serializing_if = "Option::is_none")]
209 pub source_session: Option<String>,
210}
211
212#[derive(Debug, Clone, Serialize, Deserialize)]
214#[serde(rename_all = "camelCase")]
215pub struct ChatMessage {
216 #[serde(alias = "content")]
218 pub text: Option<String>,
219
220 #[serde(default)]
222 pub parts: Option<Vec<serde_json::Value>>,
223}
224
225impl ChatMessage {
226 pub fn get_text(&self) -> String {
228 self.text.clone().unwrap_or_default()
229 }
230}
231
232#[derive(Debug, Clone, Serialize, Deserialize)]
234#[serde(rename_all = "camelCase")]
235#[allow(dead_code)]
236pub struct ChatResponse {
237 #[serde(alias = "content")]
239 pub text: Option<String>,
240
241 #[serde(default)]
243 pub parts: Option<Vec<serde_json::Value>>,
244
245 #[serde(default)]
247 pub result: Option<serde_json::Value>,
248}
249
250#[derive(Debug, Clone, Serialize, Deserialize)]
252pub struct ChatSessionIndex {
253 #[serde(default = "default_index_version")]
255 pub version: u32,
256
257 #[serde(default)]
259 pub entries: HashMap<String, ChatSessionIndexEntry>,
260}
261
262fn default_index_version() -> u32 {
263 1
264}
265
266impl Default for ChatSessionIndex {
267 fn default() -> Self {
268 Self {
269 version: 1,
270 entries: HashMap::new(),
271 }
272 }
273}
274
275#[derive(Debug, Clone, Serialize, Deserialize)]
278#[serde(rename_all = "camelCase")]
279pub struct ChatSessionTiming {
280 #[serde(default, alias = "startTime")]
283 pub created: i64,
284
285 #[serde(default, skip_serializing_if = "Option::is_none")]
287 pub last_request_started: Option<i64>,
288
289 #[serde(default, skip_serializing_if = "Option::is_none", alias = "endTime")]
292 pub last_request_ended: Option<i64>,
293}
294
295#[derive(Debug, Clone, Serialize, Deserialize)]
297#[serde(rename_all = "camelCase")]
298pub struct ChatSessionIndexEntry {
299 pub session_id: String,
301
302 pub title: String,
304
305 pub last_message_date: i64,
307
308 #[serde(default)]
310 pub timing: Option<ChatSessionTiming>,
311
312 #[serde(default = "default_response_state")]
314 pub last_response_state: u8,
315
316 #[serde(default = "default_location")]
318 pub initial_location: String,
319
320 #[serde(default)]
322 pub is_empty: bool,
323
324 #[serde(default, skip_serializing_if = "Option::is_none")]
326 pub is_imported: Option<bool>,
327
328 #[serde(default, skip_serializing_if = "Option::is_none")]
330 pub has_pending_edits: Option<bool>,
331
332 #[serde(default, skip_serializing_if = "Option::is_none")]
334 pub is_external: Option<bool>,
335}
336
337#[derive(Debug, Clone)]
339pub struct SessionWithPath {
340 pub path: std::path::PathBuf,
341 pub session: ChatSession,
342}
343
344impl SessionWithPath {
345 #[allow(dead_code)]
347 pub fn get_session_id(&self) -> String {
348 self.session.session_id.clone().unwrap_or_else(|| {
349 self.path
350 .file_stem()
351 .map(|s| s.to_string_lossy().to_string())
352 .unwrap_or_else(|| uuid::Uuid::new_v4().to_string())
353 })
354 }
355}
356
357impl ChatSession {
358 #[allow(dead_code)]
360 pub fn get_session_id(&self) -> String {
361 self.session_id
362 .clone()
363 .unwrap_or_else(|| "unknown".to_string())
364 }
365
366 pub fn title(&self) -> String {
368 if let Some(title) = &self.custom_title {
370 if !title.is_empty() {
371 return title.clone();
372 }
373 }
374
375 if let Some(first_req) = self.requests.first() {
377 if let Some(msg) = &first_req.message {
378 if let Some(text) = &msg.text {
379 let title: String = text.chars().take(50).collect();
381 if !title.is_empty() {
382 if title.len() < text.len() {
383 return format!("{}...", title);
384 }
385 return title;
386 }
387 }
388 }
389 }
390
391 "Untitled".to_string()
392 }
393
394 pub fn is_empty(&self) -> bool {
396 self.requests.is_empty()
397 }
398
399 pub fn request_count(&self) -> usize {
401 self.requests.len()
402 }
403
404 pub fn timestamp_range(&self) -> Option<(i64, i64)> {
406 if self.requests.is_empty() {
407 return None;
408 }
409
410 let timestamps: Vec<i64> = self.requests.iter().filter_map(|r| r.timestamp).collect();
411
412 if timestamps.is_empty() {
413 return None;
414 }
415
416 let min = *timestamps.iter().min().unwrap();
417 let max = *timestamps.iter().max().unwrap();
418 Some((min, max))
419 }
420}