1use uuid::Uuid;
2use chrono::Utc;
3use serde_json::{json, Value};
4use std::fs;
5use std::path::Path;
6use std::io::{self, Write};
7
8use crate::frs::ast::{Thread, Message};
9use crate::frs::ast::ScriptItem;
10
11pub fn persist_frs(conversation: &Thread) -> String {
13 let fur_dir = Path::new(".fur");
14 if !fur_dir.exists() {
15 panic!("🚨 .fur directory not initialized. Run `fur new` at least once.");
16 }
17
18 let index_path = fur_dir.join("index.json");
20 let mut index_data: Value =
21 serde_json::from_str(&fs::read_to_string(&index_path).unwrap()).unwrap();
22
23 let mut overwrite = false;
24 let mut old_conversation_id: Option<String> = None;
25
26 if let Some(threads) = index_data["threads"].as_array() {
27 for tid in threads {
28 if let Some(tid_str) = tid.as_str() {
29 let tpath = fur_dir.join("threads").join(format!("{}.json", tid_str));
30 if let Ok(txt) = fs::read_to_string(&tpath) {
31 if let Ok(tjson) = serde_json::from_str::<Value>(&txt) {
32 if tjson["title"].as_str() == Some(&conversation.title) {
33 println!("⚠️ Thread with title \"{}\" already exists.", conversation.title);
35 print!("Overwrite? [Y/n]: ");
36 io::stdout().flush().unwrap();
37
38 let mut input = String::new();
39 io::stdin().read_line(&mut input).unwrap();
40 let response = input.trim().to_lowercase();
41
42 if response.is_empty() || response == "y" || response == "yes" {
43 overwrite = true;
44 old_conversation_id = Some(tid_str.to_string());
45 } else {
46 println!("🚫 Skipped importing conversation \"{}\".", conversation.title);
47 return tid_str.to_string();
48 }
49 }
50 }
51 }
52 }
53 }
54 }
55
56 if overwrite {
58 if let Some(tid) = &old_conversation_id {
59 delete_old_conversation(tid);
60 if let Some(arr) = index_data["threads"].as_array_mut() {
61 arr.retain(|v| v.as_str() != Some(tid));
62 }
63 }
64 }
65
66 let conversation_id = Uuid::new_v4().to_string();
68 let timestamp = Utc::now().to_rfc3339();
69
70 let root_ids = persist_level(
72 &conversation.items.iter().filter_map(|item| {
73 if let ScriptItem::Message(m) = item { Some(m) } else { None }
74 }).cloned().collect::<Vec<_>>(),
75 None
76 );
77
78
79 let conversation_json = json!({
80 "id": conversation_id,
81 "created_at": timestamp,
82 "title": conversation.title,
83 "tags": conversation.tags,
84 "messages": root_ids, });
86
87 let convo_path = fur_dir.join("threads").join(format!("{}.json", conversation_id));
88 fs::write(&convo_path, serde_json::to_string_pretty(&conversation_json).unwrap())
89 .expect("❌ Could not write conversation file");
90
91 index_data["threads"].as_array_mut().unwrap().push(conversation_id.clone().into());
93 index_data["active_thread"] = conversation_id.clone().into();
94 index_data["current_message"] = Value::Null;
95 if index_data["schema_version"].as_str() == Some("0.1") {
96 index_data["schema_version"] = Value::String("0.2".to_string());
97 }
98
99 fs::write(&index_path, serde_json::to_string_pretty(&index_data).unwrap()).unwrap();
100
101 println!("🌱 Imported conversation into .fur: {} — \"{}\"", &conversation_id[..8], conversation.title);
102 conversation_id
103}
104
105
106pub fn persist_ephemeral(conversation: &Thread) -> String {
109 let fur_dir = Path::new(".fur/tmp");
110 if !fur_dir.exists() {
111 fs::create_dir_all(fur_dir).expect("❌ Could not create .fur/tmp/");
112 }
113
114 let conversation_id = format!("ephemeral-{}", Uuid::new_v4().to_string());
115 let timestamp = Utc::now().to_rfc3339();
116
117 let root_ids = persist_level(
118 &conversation.items.iter().filter_map(|item| {
119 if let ScriptItem::Message(m) = item { Some(m) } else { None }
120 }).cloned().collect::<Vec<_>>(),
121 None
122 );
123
124 let conversation_json = json!({
125 "id": conversation_id,
126 "created_at": timestamp,
127 "title": conversation.title,
128 "tags": conversation.tags,
129 "messages": root_ids,
130 });
131
132 let convo_path = fur_dir.join(format!("{}.json", conversation_id));
133 fs::write(&convo_path, serde_json::to_string_pretty(&conversation_json).unwrap())
134 .expect("❌ Could not write ephemeral conversation file");
135
136 conversation_id
137}
138
139pub fn cleanup_ephemeral(conversation_id: &str) {
141 let fur_dir = Path::new(".fur/tmp");
142 let convo_path = fur_dir.join(format!("{}.json", conversation_id));
143 let _ = fs::remove_file(convo_path);
144 }
146
147
148
149fn delete_old_conversation(conversation_id: &str) {
151 let fur_dir = Path::new(".fur");
152 let convo_path = fur_dir.join("threads").join(format!("{}.json", conversation_id));
153
154 if let Ok(content) = fs::read_to_string(&convo_path) {
155 if let Ok(conversation_json) = serde_json::from_str::<Value>(&content) {
156 if let Some(msgs) = conversation_json["messages"].as_array() {
157 for m in msgs {
158 if let Some(mid) = m.as_str() {
159 delete_message_recursive(mid, fur_dir);
160 }
161 }
162 }
163 }
164 }
165
166 let _ = fs::remove_file(convo_path);
167}
168
169fn delete_message_recursive(msg_id: &str, fur_dir: &Path) {
171 let msg_path = fur_dir.join("messages").join(format!("{}.json", msg_id));
172 if let Ok(content) = fs::read_to_string(&msg_path) {
173 if let Ok(msg_json) = serde_json::from_str::<Value>(&content) {
174 if let Some(children) = msg_json["children"].as_array() {
176 for c in children {
177 if let Some(cid) = c.as_str() {
178 delete_message_recursive(cid, fur_dir);
179 }
180 }
181 }
182 if let Some(branches) = msg_json["branches"].as_array() {
184 for block in branches {
185 if let Some(arr) = block.as_array() {
186 for c in arr {
187 if let Some(cid) = c.as_str() {
188 delete_message_recursive(cid, fur_dir);
189 }
190 }
191 }
192 }
193 }
194 }
195 }
196 let _ = fs::remove_file(msg_path);
197}
198
199fn persist_level(msgs: &[Message], parent: Option<String>) -> Vec<String> {
202 let mut ids_at_this_level: Vec<String> = Vec::new();
203
204 for m in msgs {
205 let msg_id = Uuid::new_v4().to_string();
206
207 let mut branch_groups_ids: Vec<Vec<String>> = Vec::new();
208 let mut direct_children_ids: Vec<String> = Vec::new();
209
210 for branch_block in &m.branches {
211 let group_ids = persist_level(branch_block, Some(msg_id.clone()));
212 if !group_ids.is_empty() {
213 direct_children_ids.extend(group_ids.clone());
214 branch_groups_ids.push(group_ids);
215 }
216 }
217
218 let msg_json = json!({
219 "id": msg_id,
220 "avatar": m.avatar,
221 "name": m.avatar,
222 "text": m.text,
223 "markdown": m.file,
224 "attachment": m.attachment,
225 "parent": parent,
226 "children": direct_children_ids,
227 "branches": branch_groups_ids,
228 "timestamp": Utc::now().to_rfc3339(),
229 });
230
231 let path = Path::new(".fur/messages").join(format!("{}.json", msg_id));
232 fs::write(&path, serde_json::to_string_pretty(&msg_json).unwrap())
233 .expect("❌ Could not write message file");
234
235 ids_at_this_level.push(msg_id);
236 }
237
238 ids_at_this_level
239}