1use serde::Deserialize;
13use std::collections::{HashMap, VecDeque};
14use std::fs;
15use std::path::Path;
16
17#[derive(Debug, Deserialize)]
20pub struct ChatGPTExport {
21 #[serde(default)]
22 pub conversations: Vec<Conversation>,
23}
24
25#[derive(Debug, Deserialize)]
26pub struct Conversation {
27 pub title: Option<String>,
28 pub create_time: Option<f64>,
29 pub update_time: Option<f64>,
30 pub mapping: Option<HashMap<String, MappingNode>>,
31 pub current_node: Option<String>,
32 #[serde(default)]
33 pub moderation_results: Vec<serde_json::Value>,
34 pub plugin_ids: Option<Vec<String>>,
35}
36
37#[derive(Debug, Deserialize, Clone)]
38pub struct MappingNode {
39 pub id: Option<String>,
40 pub message: Option<ChatMessageData>,
41 pub parent: Option<String>,
42 pub children: Option<Vec<String>>,
43}
44
45#[derive(Debug, Deserialize, Clone)]
46pub struct ChatMessageData {
47 pub id: Option<String>,
48 pub author: Option<Author>,
49 pub content: Option<MessageContent>,
50 pub create_time: Option<f64>,
51 pub metadata: Option<serde_json::Value>,
52}
53
54#[derive(Debug, Deserialize, Clone)]
55pub struct Author {
56 pub role: String,
57 pub name: Option<String>,
58 pub metadata: Option<serde_json::Value>,
59}
60
61#[derive(Debug, Deserialize, Clone)]
62pub struct MessageContent {
63 pub content_type: Option<String>,
64 pub parts: Option<Vec<serde_json::Value>>,
65 pub text: Option<String>,
66}
67
68#[derive(Debug, Clone)]
71pub struct ProcessedMessage {
72 pub conversation_title: String,
73 pub role: String, pub sender_name: String, pub text: String,
76 pub timestamp_ms: u64,
77 pub message_index: usize,
78 pub message_count: usize,
79}
80
81#[derive(Debug, Clone)]
82pub struct ImportResult {
83 pub conversations_found: usize,
84 pub total_messages: usize,
85 pub user_messages: usize,
86 pub ai_messages: usize,
87 pub total_size_bytes: usize,
88 pub errors: Vec<String>,
89 pub import_duration_ms: u64,
90}
91
92pub struct ChatGPTImporter {
95 pub persona_name: String,
96 pub user_name: String,
97}
98
99impl ChatGPTImporter {
100 pub fn new(persona: &str) -> Self {
101 Self {
102 persona_name: persona.to_string(),
103 user_name: "user".to_string(),
104 }
105 }
106
107 pub fn parse_export(&self, path: &str) -> Result<VecDeque<ProcessedMessage>, String> {
109 let content = fs::read_to_string(Path::new(path))
110 .map_err(|e| format!("Nem lehet olvasni a fájlt: {}", e))?;
111
112 let conversations: Vec<Conversation> = if content.trim().starts_with('[') {
114 serde_json::from_str(&content).map_err(|e| format!("JSON parse hiba (tömb): {}", e))?
115 } else {
116 let export: ChatGPTExport = serde_json::from_str(&content)
117 .map_err(|e| format!("JSON parse hiba (objektum): {}", e))?;
118 export.conversations
119 };
120
121 if conversations.is_empty() {
122 return Err("Nincs egy beszélgetés sem az exportban.".to_string());
123 }
124
125 let mut all_messages = VecDeque::new();
126
127 for conv in &conversations {
128 let title = conv.title.as_deref().unwrap_or("Untitled");
129 let mapping = match &conv.mapping {
130 Some(m) => m,
131 None => continue,
132 };
133
134 let current = match &conv.current_node {
136 Some(n) => n.clone(),
137 None => continue,
138 };
139
140 let mut path = Vec::new();
142 let mut node_id = current.clone();
143 loop {
144 path.push(node_id.clone());
145 match mapping.get(&node_id) {
146 Some(node) => match &node.parent {
147 Some(parent) if !parent.is_empty() && parent != &node_id => {
148 node_id = parent.clone();
149 }
150 _ => break,
151 },
152 None => break,
153 }
154 }
155 path.reverse();
156
157 let conv_messages: Vec<ProcessedMessage> = path
159 .iter()
160 .filter_map(|nid| {
161 let node = mapping.get(nid)?;
162 let msg = node.message.as_ref()?;
163 let author = msg.author.as_ref()?;
164 let role = author.role.clone();
165
166 if role != "user" && role != "assistant" {
168 if role != "system" {
170 return None;
171 }
172 }
173
174 let content = msg.content.as_ref()?;
175 let text = content
176 .parts
177 .as_ref()
178 .and_then(|parts| {
179 parts
180 .first()
181 .and_then(|p| p.as_str())
182 .map(|s| s.to_string())
183 })
184 .or_else(|| content.text.clone())
185 .unwrap_or_default();
186
187 if text.trim().is_empty() {
188 return None;
189 }
190
191 let ts = (msg.create_time.unwrap_or(0.0) * 1000.0) as u64;
192 let sender = if role == "assistant" {
193 self.persona_name.clone()
194 } else {
195 self.user_name.clone()
196 };
197
198 Some(ProcessedMessage {
199 conversation_title: title.to_string(),
200 role,
201 sender_name: sender,
202 text: text.trim().to_string(),
203 timestamp_ms: ts,
204 message_index: 0,
205 message_count: path.len(),
206 })
207 })
208 .collect();
209
210 for msg in conv_messages {
211 all_messages.push_back(msg);
212 }
213 }
214
215 Ok(all_messages)
216 }
217
218 pub fn import(&self, path: &str, microscope_binary: &str) -> ImportResult {
220 let start = std::time::Instant::now();
221 let mut errors = Vec::new();
222 let mut total_messages = 0;
223 let mut user_msgs = 0;
224 let mut ai_msgs = 0;
225
226 let messages = match self.parse_export(path) {
227 Ok(m) => m,
228 Err(e) => {
229 return ImportResult {
230 conversations_found: 0,
231 total_messages: 0,
232 user_messages: 0,
233 ai_messages: 0,
234 total_size_bytes: 0,
235 errors: vec![e],
236 import_duration_ms: start.elapsed().as_millis() as u64,
237 };
238 }
239 };
240
241 let mut conv_count = 0;
242 let mut last_title = String::new();
243
244 for msg in &messages {
245 if msg.conversation_title != last_title {
246 conv_count += 1;
247 last_title = msg.conversation_title.clone();
248 }
249
250 let layer = if msg.role == "assistant" {
252 "long_term"
253 } else {
254 "short_term"
255 };
256 let importance = if msg.role == "assistant" { 8 } else { 6 };
257 let store_text = format!(
258 "[{} | {}] {}: {}",
259 msg.conversation_title,
260 msg.sender_name,
261 if msg.role == "assistant" {
262 &self.persona_name
263 } else {
264 &self.user_name
265 },
266 msg.text
267 );
268
269 let cmd = format!(
270 "{} store -l {} -i {} \"{}\"",
271 microscope_binary,
272 layer,
273 importance,
274 store_text.replace('\"', "\\\"")
275 );
276
277 if std::process::Command::new("sh")
279 .arg("-c")
280 .arg(&cmd)
281 .output()
282 .is_err()
283 {
284 errors.push(format!(
285 "Hiba üzenet tárolásakor: {}",
286 msg.text.chars().take(50).collect::<String>()
287 ));
288 }
289
290 total_messages += 1;
291 if msg.role == "assistant" {
292 ai_msgs += 1;
293 } else {
294 user_msgs += 1;
295 }
296 }
297
298 let file_size = fs::metadata(path).map(|m| m.len() as usize).unwrap_or(0);
299
300 ImportResult {
301 conversations_found: conv_count,
302 total_messages,
303 user_messages: user_msgs,
304 ai_messages: ai_msgs,
305 total_size_bytes: file_size,
306 errors,
307 import_duration_ms: start.elapsed().as_millis() as u64,
308 }
309 }
310}