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
88fn default_version() -> u32 {
89 3
90}
91
92fn default_location() -> String {
93 "panel".to_string()
94}
95
96#[derive(Debug, Clone, Serialize, Deserialize)]
98#[serde(rename_all = "camelCase")]
99pub struct ChatRequest {
100 #[serde(default)]
102 pub timestamp: Option<i64>,
103
104 #[serde(default)]
106 pub message: Option<ChatMessage>,
107
108 #[serde(default)]
110 pub response: Option<serde_json::Value>,
111
112 #[serde(default)]
114 pub variable_data: Option<serde_json::Value>,
115
116 #[serde(default)]
118 pub request_id: Option<String>,
119
120 #[serde(default)]
122 pub response_id: Option<String>,
123
124 #[serde(default)]
126 pub model_id: Option<String>,
127
128 #[serde(default)]
130 pub agent: Option<serde_json::Value>,
131
132 #[serde(default)]
134 pub result: Option<serde_json::Value>,
135
136 #[serde(default)]
138 pub followups: Option<Vec<serde_json::Value>>,
139
140 #[serde(default)]
142 pub is_canceled: Option<bool>,
143
144 #[serde(default)]
146 pub content_references: Option<Vec<serde_json::Value>>,
147
148 #[serde(default)]
150 pub code_citations: Option<Vec<serde_json::Value>>,
151
152 #[serde(default)]
154 pub response_markdown_info: Option<Vec<serde_json::Value>>,
155
156 #[serde(rename = "_sourceSession", skip_serializing_if = "Option::is_none")]
158 pub source_session: Option<String>,
159}
160
161#[derive(Debug, Clone, Serialize, Deserialize)]
163#[serde(rename_all = "camelCase")]
164pub struct ChatMessage {
165 #[serde(alias = "content")]
167 pub text: Option<String>,
168
169 #[serde(default)]
171 pub parts: Option<Vec<serde_json::Value>>,
172}
173
174impl ChatMessage {
175 pub fn get_text(&self) -> String {
177 self.text.clone().unwrap_or_default()
178 }
179}
180
181#[derive(Debug, Clone, Serialize, Deserialize)]
183#[serde(rename_all = "camelCase")]
184#[allow(dead_code)]
185pub struct ChatResponse {
186 #[serde(alias = "content")]
188 pub text: Option<String>,
189
190 #[serde(default)]
192 pub parts: Option<Vec<serde_json::Value>>,
193
194 #[serde(default)]
196 pub result: Option<serde_json::Value>,
197}
198
199#[derive(Debug, Clone, Serialize, Deserialize)]
201pub struct ChatSessionIndex {
202 #[serde(default = "default_index_version")]
204 pub version: u32,
205
206 #[serde(default)]
208 pub entries: HashMap<String, ChatSessionIndexEntry>,
209}
210
211fn default_index_version() -> u32 {
212 1
213}
214
215impl Default for ChatSessionIndex {
216 fn default() -> Self {
217 Self {
218 version: 1,
219 entries: HashMap::new(),
220 }
221 }
222}
223
224#[derive(Debug, Clone, Serialize, Deserialize)]
226#[serde(rename_all = "camelCase")]
227pub struct ChatSessionIndexEntry {
228 pub session_id: String,
230
231 pub title: String,
233
234 pub last_message_date: i64,
236
237 #[serde(default)]
239 pub is_imported: bool,
240
241 #[serde(default = "default_location")]
243 pub initial_location: String,
244
245 #[serde(default)]
247 pub is_empty: bool,
248}
249
250#[derive(Debug, Clone)]
252pub struct SessionWithPath {
253 pub path: std::path::PathBuf,
254 pub session: ChatSession,
255}
256
257impl SessionWithPath {
258 #[allow(dead_code)]
260 pub fn get_session_id(&self) -> String {
261 self.session.session_id.clone().unwrap_or_else(|| {
262 self.path
263 .file_stem()
264 .map(|s| s.to_string_lossy().to_string())
265 .unwrap_or_else(|| uuid::Uuid::new_v4().to_string())
266 })
267 }
268}
269
270impl ChatSession {
271 #[allow(dead_code)]
273 pub fn get_session_id(&self) -> String {
274 self.session_id
275 .clone()
276 .unwrap_or_else(|| "unknown".to_string())
277 }
278
279 pub fn title(&self) -> String {
281 if let Some(title) = &self.custom_title {
283 if !title.is_empty() {
284 return title.clone();
285 }
286 }
287
288 if let Some(first_req) = self.requests.first() {
290 if let Some(msg) = &first_req.message {
291 if let Some(text) = &msg.text {
292 let title: String = text.chars().take(50).collect();
294 if !title.is_empty() {
295 if title.len() < text.len() {
296 return format!("{}...", title);
297 }
298 return title;
299 }
300 }
301 }
302 }
303
304 "Untitled".to_string()
305 }
306
307 pub fn is_empty(&self) -> bool {
309 self.requests.is_empty()
310 }
311
312 pub fn request_count(&self) -> usize {
314 self.requests.len()
315 }
316
317 pub fn timestamp_range(&self) -> Option<(i64, i64)> {
319 if self.requests.is_empty() {
320 return None;
321 }
322
323 let timestamps: Vec<i64> = self.requests.iter().filter_map(|r| r.timestamp).collect();
324
325 if timestamps.is_empty() {
326 return None;
327 }
328
329 let min = *timestamps.iter().min().unwrap();
330 let max = *timestamps.iter().max().unwrap();
331 Some((min, max))
332 }
333}