1use crate::models::{extract_response_text, 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": 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 model_state: None,
121 time_spent_waiting: None,
122 });
123 }
124 }
125 _ => {}
126 }
127 }
128
129 ChatSession {
130 version: 3,
131 session_id: Some(generic.id),
132 creation_date: generic.created_at.unwrap_or(now),
133 last_message_date: generic.updated_at.unwrap_or(now),
134 is_imported: true,
135 initial_location: "imported".to_string(),
136 custom_title: generic.title,
137 requester_username: Some("user".to_string()),
138 requester_avatar_icon_uri: None,
139 responder_username: generic.provider,
140 responder_avatar_icon_uri: None,
141 requests,
142 }
143 }
144}
145
146pub fn session_to_markdown(session: &ChatSession) -> String {
148 let mut md = String::new();
149
150 md.push_str(&format!("# {}\n\n", session.title()));
152
153 if let Some(id) = &session.session_id {
154 md.push_str(&format!("Session ID: `{}`\n\n", id));
155 }
156
157 md.push_str(&format!(
158 "Created: {}\n",
159 format_timestamp(session.creation_date)
160 ));
161 md.push_str(&format!(
162 "Last Updated: {}\n\n",
163 format_timestamp(session.last_message_date)
164 ));
165
166 md.push_str("---\n\n");
167
168 for (i, request) in session.requests.iter().enumerate() {
170 if let Some(msg) = &request.message {
172 if let Some(text) = &msg.text {
173 md.push_str(&format!("## User ({})\n\n", i + 1));
174 md.push_str(text);
175 md.push_str("\n\n");
176 }
177 }
178
179 if let Some(response) = &request.response {
181 if let Some(text) = extract_response_text(response) {
182 let model = request.model_id.as_deref().unwrap_or("Assistant");
183 md.push_str(&format!("## {} ({})\n\n", model, i + 1));
184 md.push_str(&text);
185 md.push_str("\n\n");
186 }
187 }
188
189 md.push_str("---\n\n");
190 }
191
192 md
193}
194
195pub fn markdown_to_session(markdown: &str, title: Option<String>) -> ChatSession {
197 let now = chrono::Utc::now().timestamp_millis();
198 let session_id = uuid::Uuid::new_v4().to_string();
199
200 let mut requests = Vec::new();
202 let mut current_user: Option<String> = None;
203 let mut current_assistant: Option<String> = None;
204 let mut in_user = false;
205 let mut in_assistant = false;
206 let mut content = String::new();
207
208 for line in markdown.lines() {
209 if line.starts_with("## User") {
210 if let Some(user) = current_user.take() {
212 requests.push(create_request(
213 user,
214 current_assistant.take().unwrap_or_default(),
215 now,
216 None,
217 ));
218 }
219 in_user = true;
220 in_assistant = false;
221 content.clear();
222 } else if line.starts_with("## ") && !line.starts_with("## User") {
223 if in_user {
225 current_user = Some(content.trim().to_string());
226 }
227 in_user = false;
228 in_assistant = true;
229 content.clear();
230 } else if line == "---" {
231 if in_assistant {
232 current_assistant = Some(content.trim().to_string());
233 }
234 if let Some(user) = current_user.take() {
236 requests.push(create_request(
237 user,
238 current_assistant.take().unwrap_or_default(),
239 now,
240 None,
241 ));
242 }
243 in_user = false;
244 in_assistant = false;
245 content.clear();
246 } else {
247 content.push_str(line);
248 content.push('\n');
249 }
250 }
251
252 if in_user {
254 current_user = Some(content.trim().to_string());
255 } else if in_assistant {
256 current_assistant = Some(content.trim().to_string());
257 }
258 if let Some(user) = current_user.take() {
259 requests.push(create_request(
260 user,
261 current_assistant.take().unwrap_or_default(),
262 now,
263 None,
264 ));
265 }
266
267 ChatSession {
268 version: 3,
269 session_id: Some(session_id),
270 creation_date: now,
271 last_message_date: now,
272 is_imported: true,
273 initial_location: "markdown".to_string(),
274 custom_title: title,
275 requester_username: Some("user".to_string()),
276 requester_avatar_icon_uri: None,
277 responder_username: Some("Imported".to_string()),
278 responder_avatar_icon_uri: None,
279 requests,
280 }
281}
282
283fn create_request(
285 user_text: String,
286 assistant_text: String,
287 timestamp: i64,
288 model: Option<String>,
289) -> ChatRequest {
290 ChatRequest {
291 timestamp: Some(timestamp),
292 message: Some(ChatMessage {
293 text: Some(user_text),
294 parts: None,
295 }),
296 response: Some(serde_json::json!(
297 [{"value": assistant_text}]
298 )),
299 variable_data: None,
300 request_id: Some(uuid::Uuid::new_v4().to_string()),
301 response_id: Some(uuid::Uuid::new_v4().to_string()),
302 model_id: model,
303 agent: None,
304 result: None,
305 followups: None,
306 is_canceled: Some(false),
307 content_references: None,
308 code_citations: None,
309 response_markdown_info: None,
310 source_session: None,
311 model_state: None,
312 time_spent_waiting: None,
313 }
314}
315
316fn _extract_response_text_legacy(response: &serde_json::Value) -> Option<String> {
320 extract_response_text(response)
321}
322
323fn format_timestamp(timestamp: i64) -> String {
325 use chrono::{TimeZone, Utc};
326
327 if timestamp == 0 {
328 return "Unknown".to_string();
329 }
330
331 let dt = Utc.timestamp_millis_opt(timestamp);
332 match dt {
333 chrono::LocalResult::Single(dt) => dt.format("%Y-%m-%d %H:%M:%S").to_string(),
334 _ => "Invalid".to_string(),
335 }
336}
337
338#[cfg(test)]
339mod tests {
340 use super::*;
341
342 #[test]
343 fn test_session_to_markdown() {
344 let session = ChatSession {
345 version: 3,
346 session_id: Some("test-123".to_string()),
347 creation_date: 1700000000000,
348 last_message_date: 1700000000000,
349 is_imported: false,
350 initial_location: "panel".to_string(),
351 custom_title: Some("Test Session".to_string()),
352 requester_username: Some("user".to_string()),
353 requester_avatar_icon_uri: None,
354 responder_username: Some("assistant".to_string()),
355 responder_avatar_icon_uri: None,
356 requests: vec![ChatRequest {
357 timestamp: Some(1700000000000),
358 message: Some(ChatMessage {
359 text: Some("Hello".to_string()),
360 parts: None,
361 }),
362 response: Some(serde_json::json!({
363 "value": [{"value": "Hi there!"}]
364 })),
365 variable_data: None,
366 request_id: None,
367 response_id: None,
368 model_id: Some("gpt-4".to_string()),
369 agent: None,
370 result: None,
371 followups: None,
372 is_canceled: None,
373 content_references: None,
374 code_citations: None,
375 response_markdown_info: None,
376 source_session: None,
377 model_state: None,
378 time_spent_waiting: None,
379 }],
380 };
381
382 let md = session_to_markdown(&session);
383 assert!(md.contains("# Test Session"));
384 assert!(md.contains("Hello"));
385 assert!(md.contains("Hi there!"));
386 }
387
388 #[test]
389 fn test_generic_session_conversion() {
390 let session = ChatSession {
391 version: 3,
392 session_id: Some("test-123".to_string()),
393 creation_date: 1700000000000,
394 last_message_date: 1700000000000,
395 is_imported: false,
396 initial_location: "panel".to_string(),
397 custom_title: Some("Test".to_string()),
398 requester_username: None,
399 requester_avatar_icon_uri: None,
400 responder_username: Some("Copilot".to_string()),
401 responder_avatar_icon_uri: None,
402 requests: vec![],
403 };
404
405 let generic: GenericSession = session.clone().into();
406 assert_eq!(generic.id, "test-123");
407 assert_eq!(generic.title, Some("Test".to_string()));
408
409 let back: ChatSession = generic.into();
410 assert_eq!(back.session_id, Some("test-123".to_string()));
411 }
412}