1use crate::models::{ChatMessage, ChatRequest, ChatSession};
12use serde::{Deserialize, Serialize};
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct GenericMessage {
17 pub role: String,
18 pub content: String,
19 #[serde(default)]
20 pub timestamp: Option<i64>,
21 #[serde(default)]
22 pub model: Option<String>,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct GenericSession {
28 pub id: String,
29 pub title: Option<String>,
30 pub messages: Vec<GenericMessage>,
31 #[serde(default)]
32 pub created_at: Option<i64>,
33 #[serde(default)]
34 pub updated_at: Option<i64>,
35 #[serde(default)]
36 pub provider: Option<String>,
37 #[serde(default)]
38 pub model: Option<String>,
39}
40
41impl From<ChatSession> for GenericSession {
42 fn from(session: ChatSession) -> Self {
43 let mut messages = Vec::new();
44
45 for request in session.requests {
46 if let Some(msg) = &request.message {
48 if let Some(text) = &msg.text {
49 messages.push(GenericMessage {
50 role: "user".to_string(),
51 content: text.clone(),
52 timestamp: request.timestamp,
53 model: request.model_id.clone(),
54 });
55 }
56 }
57
58 if let Some(response) = &request.response {
60 if let Some(text) = extract_response_text(response) {
61 messages.push(GenericMessage {
62 role: "assistant".to_string(),
63 content: text,
64 timestamp: request.timestamp,
65 model: request.model_id.clone(),
66 });
67 }
68 }
69 }
70
71 GenericSession {
72 id: session
73 .session_id
74 .unwrap_or_else(|| uuid::Uuid::new_v4().to_string()),
75 title: session.custom_title,
76 messages,
77 created_at: Some(session.creation_date),
78 updated_at: Some(session.last_message_date),
79 provider: session.responder_username,
80 model: None,
81 }
82 }
83}
84
85impl From<GenericSession> for ChatSession {
86 fn from(generic: GenericSession) -> Self {
87 let now = chrono::Utc::now().timestamp_millis();
88
89 let mut requests = Vec::new();
90 let mut user_msg: Option<(String, Option<i64>, Option<String>)> = None;
91
92 for msg in generic.messages {
93 match msg.role.as_str() {
94 "user" => {
95 user_msg = Some((msg.content, msg.timestamp, msg.model));
96 }
97 "assistant" => {
98 if let Some((user_text, timestamp, model)) = user_msg.take() {
99 requests.push(ChatRequest {
100 timestamp: timestamp.or(Some(now)),
101 message: Some(ChatMessage {
102 text: Some(user_text),
103 parts: None,
104 }),
105 response: Some(serde_json::json!({
106 "value": [{"value": msg.content}]
107 })),
108 variable_data: None,
109 request_id: Some(uuid::Uuid::new_v4().to_string()),
110 response_id: Some(uuid::Uuid::new_v4().to_string()),
111 model_id: model.or(msg.model),
112 agent: None,
113 result: None,
114 followups: None,
115 is_canceled: Some(false),
116 content_references: None,
117 code_citations: None,
118 response_markdown_info: None,
119 source_session: None,
120 });
121 }
122 }
123 _ => {}
124 }
125 }
126
127 ChatSession {
128 version: 3,
129 session_id: Some(generic.id),
130 creation_date: generic.created_at.unwrap_or(now),
131 last_message_date: generic.updated_at.unwrap_or(now),
132 is_imported: true,
133 initial_location: "imported".to_string(),
134 custom_title: generic.title,
135 requester_username: Some("user".to_string()),
136 requester_avatar_icon_uri: None,
137 responder_username: generic.provider,
138 responder_avatar_icon_uri: None,
139 requests,
140 }
141 }
142}
143
144pub fn session_to_markdown(session: &ChatSession) -> String {
146 let mut md = String::new();
147
148 md.push_str(&format!("# {}\n\n", session.title()));
150
151 if let Some(id) = &session.session_id {
152 md.push_str(&format!("Session ID: `{}`\n\n", id));
153 }
154
155 md.push_str(&format!(
156 "Created: {}\n",
157 format_timestamp(session.creation_date)
158 ));
159 md.push_str(&format!(
160 "Last Updated: {}\n\n",
161 format_timestamp(session.last_message_date)
162 ));
163
164 md.push_str("---\n\n");
165
166 for (i, request) in session.requests.iter().enumerate() {
168 if let Some(msg) = &request.message {
170 if let Some(text) = &msg.text {
171 md.push_str(&format!("## User ({})\n\n", i + 1));
172 md.push_str(text);
173 md.push_str("\n\n");
174 }
175 }
176
177 if let Some(response) = &request.response {
179 if let Some(text) = extract_response_text(response) {
180 let model = request.model_id.as_deref().unwrap_or("Assistant");
181 md.push_str(&format!("## {} ({})\n\n", model, i + 1));
182 md.push_str(&text);
183 md.push_str("\n\n");
184 }
185 }
186
187 md.push_str("---\n\n");
188 }
189
190 md
191}
192
193pub fn markdown_to_session(markdown: &str, title: Option<String>) -> ChatSession {
195 let now = chrono::Utc::now().timestamp_millis();
196 let session_id = uuid::Uuid::new_v4().to_string();
197
198 let mut requests = Vec::new();
200 let mut current_user: Option<String> = None;
201 let mut current_assistant: Option<String> = None;
202 let mut in_user = false;
203 let mut in_assistant = false;
204 let mut content = String::new();
205
206 for line in markdown.lines() {
207 if line.starts_with("## User") {
208 if let Some(user) = current_user.take() {
210 requests.push(create_request(
211 user,
212 current_assistant.take().unwrap_or_default(),
213 now,
214 None,
215 ));
216 }
217 in_user = true;
218 in_assistant = false;
219 content.clear();
220 } else if line.starts_with("## ") && !line.starts_with("## User") {
221 if in_user {
223 current_user = Some(content.trim().to_string());
224 }
225 in_user = false;
226 in_assistant = true;
227 content.clear();
228 } else if line == "---" {
229 if in_assistant {
230 current_assistant = Some(content.trim().to_string());
231 }
232 if let Some(user) = current_user.take() {
234 requests.push(create_request(
235 user,
236 current_assistant.take().unwrap_or_default(),
237 now,
238 None,
239 ));
240 }
241 in_user = false;
242 in_assistant = false;
243 content.clear();
244 } else {
245 content.push_str(line);
246 content.push('\n');
247 }
248 }
249
250 if in_user {
252 current_user = Some(content.trim().to_string());
253 } else if in_assistant {
254 current_assistant = Some(content.trim().to_string());
255 }
256 if let Some(user) = current_user.take() {
257 requests.push(create_request(
258 user,
259 current_assistant.take().unwrap_or_default(),
260 now,
261 None,
262 ));
263 }
264
265 ChatSession {
266 version: 3,
267 session_id: Some(session_id),
268 creation_date: now,
269 last_message_date: now,
270 is_imported: true,
271 initial_location: "markdown".to_string(),
272 custom_title: title,
273 requester_username: Some("user".to_string()),
274 requester_avatar_icon_uri: None,
275 responder_username: Some("Imported".to_string()),
276 responder_avatar_icon_uri: None,
277 requests,
278 }
279}
280
281fn create_request(
283 user_text: String,
284 assistant_text: String,
285 timestamp: i64,
286 model: Option<String>,
287) -> ChatRequest {
288 ChatRequest {
289 timestamp: Some(timestamp),
290 message: Some(ChatMessage {
291 text: Some(user_text),
292 parts: None,
293 }),
294 response: Some(serde_json::json!({
295 "value": [{"value": assistant_text}]
296 })),
297 variable_data: None,
298 request_id: Some(uuid::Uuid::new_v4().to_string()),
299 response_id: Some(uuid::Uuid::new_v4().to_string()),
300 model_id: model,
301 agent: None,
302 result: None,
303 followups: None,
304 is_canceled: Some(false),
305 content_references: None,
306 code_citations: None,
307 response_markdown_info: None,
308 source_session: None,
309 }
310}
311
312fn extract_response_text(response: &serde_json::Value) -> Option<String> {
314 if let Some(text) = response.get("text").and_then(|v| v.as_str()) {
316 return Some(text.to_string());
317 }
318
319 if let Some(value) = response.get("value").and_then(|v| v.as_array()) {
321 let parts: Vec<String> = value
322 .iter()
323 .filter_map(|v| v.get("value").and_then(|v| v.as_str()))
324 .map(String::from)
325 .collect();
326 if !parts.is_empty() {
327 return Some(parts.join("\n"));
328 }
329 }
330
331 if let Some(content) = response.get("content").and_then(|v| v.as_str()) {
333 return Some(content.to_string());
334 }
335
336 None
337}
338
339fn format_timestamp(timestamp: i64) -> String {
341 use chrono::{TimeZone, Utc};
342
343 if timestamp == 0 {
344 return "Unknown".to_string();
345 }
346
347 let dt = Utc.timestamp_millis_opt(timestamp);
348 match dt {
349 chrono::LocalResult::Single(dt) => dt.format("%Y-%m-%d %H:%M:%S").to_string(),
350 _ => "Invalid".to_string(),
351 }
352}
353
354#[cfg(test)]
355mod tests {
356 use super::*;
357
358 #[test]
359 fn test_session_to_markdown() {
360 let session = ChatSession {
361 version: 3,
362 session_id: Some("test-123".to_string()),
363 creation_date: 1700000000000,
364 last_message_date: 1700000000000,
365 is_imported: false,
366 initial_location: "panel".to_string(),
367 custom_title: Some("Test Session".to_string()),
368 requester_username: Some("user".to_string()),
369 requester_avatar_icon_uri: None,
370 responder_username: Some("assistant".to_string()),
371 responder_avatar_icon_uri: None,
372 requests: vec![ChatRequest {
373 timestamp: Some(1700000000000),
374 message: Some(ChatMessage {
375 text: Some("Hello".to_string()),
376 parts: None,
377 }),
378 response: Some(serde_json::json!({
379 "value": [{"value": "Hi there!"}]
380 })),
381 variable_data: None,
382 request_id: None,
383 response_id: None,
384 model_id: Some("gpt-4".to_string()),
385 agent: None,
386 result: None,
387 followups: None,
388 is_canceled: None,
389 content_references: None,
390 code_citations: None,
391 response_markdown_info: None,
392 source_session: None,
393 }],
394 };
395
396 let md = session_to_markdown(&session);
397 assert!(md.contains("# Test Session"));
398 assert!(md.contains("Hello"));
399 assert!(md.contains("Hi there!"));
400 }
401
402 #[test]
403 fn test_generic_session_conversion() {
404 let session = ChatSession {
405 version: 3,
406 session_id: Some("test-123".to_string()),
407 creation_date: 1700000000000,
408 last_message_date: 1700000000000,
409 is_imported: false,
410 initial_location: "panel".to_string(),
411 custom_title: Some("Test".to_string()),
412 requester_username: None,
413 requester_avatar_icon_uri: None,
414 responder_username: Some("Copilot".to_string()),
415 responder_avatar_icon_uri: None,
416 requests: vec![],
417 };
418
419 let generic: GenericSession = session.clone().into();
420 assert_eq!(generic.id, "test-123");
421 assert_eq!(generic.title, Some("Test".to_string()));
422
423 let back: ChatSession = generic.into();
424 assert_eq!(back.session_id, Some("test-123".to_string()));
425 }
426}