1use super::commands;
4use crate::types::VisionResult;
5use std::path::PathBuf;
6
7#[derive(Default)]
8pub struct ReplState {
9 pub file_path: Option<PathBuf>,
10 pub json: bool,
11}
12
13impl ReplState {
14 pub fn new() -> Self {
15 Self::default()
16 }
17
18 pub fn dispatch(&mut self, line: &str) -> VisionResult<()> {
19 let parts: Vec<&str> = line.split_whitespace().collect();
20 if parts.is_empty() {
21 return Ok(());
22 }
23
24 let cmd = parts[0].trim_start_matches('/');
25 let args = &parts[1..];
26
27 match cmd {
28 "create" => self.cmd_create(args),
29 "load" => self.cmd_load(args),
30 "info" => self.cmd_info(),
31 "capture" => self.cmd_capture(args),
32 "query" => self.cmd_query(args),
33 "similar" => self.cmd_similar(args),
34 "compare" => self.cmd_compare(args),
35 "diff" => self.cmd_diff(args),
36 "health" => self.cmd_health(),
37 "link" => self.cmd_link(args),
38 "stats" => self.cmd_stats(),
39 "export" => self.cmd_export(args),
40 "json" => {
41 self.json = !self.json;
42 println!("JSON output: {}", if self.json { "on" } else { "off" });
43 Ok(())
44 }
45 "clear" => {
46 print!("\x1B[2J\x1B[1;1H");
47 Ok(())
48 }
49 "help" => {
50 self.print_help();
51 Ok(())
52 }
53 "exit" | "quit" => {
54 println!("Goodbye.");
55 std::process::exit(0);
56 }
57 _ => {
58 let suggestion = suggest_command(cmd);
59 if let Some(s) = suggestion {
60 println!("Unknown command '/{cmd}'. Did you mean '/{s}'?");
61 } else {
62 println!("Unknown command '/{cmd}'. Type /help for available commands.");
63 }
64 Ok(())
65 }
66 }
67 }
68
69 fn require_file(&self) -> VisionResult<&PathBuf> {
70 self.file_path.as_ref().ok_or_else(|| {
71 crate::types::VisionError::Io(std::io::Error::new(
72 std::io::ErrorKind::NotFound,
73 "No file loaded. Use /create <path> or /load <path> first.",
74 ))
75 })
76 }
77
78 fn cmd_create(&mut self, args: &[&str]) -> VisionResult<()> {
79 let path = args.first().ok_or_else(|| {
80 crate::types::VisionError::Io(std::io::Error::new(
81 std::io::ErrorKind::InvalidInput,
82 "Usage: /create <path> [dimension]",
83 ))
84 })?;
85 let dim: u32 = args.get(1).and_then(|s| s.parse().ok()).unwrap_or(512);
86 let p = PathBuf::from(path);
87 commands::cmd_create(&p, dim)?;
88 self.file_path = Some(p);
89 Ok(())
90 }
91
92 fn cmd_load(&mut self, args: &[&str]) -> VisionResult<()> {
93 let path = args.first().ok_or_else(|| {
94 crate::types::VisionError::Io(std::io::Error::new(
95 std::io::ErrorKind::InvalidInput,
96 "Usage: /load <path>",
97 ))
98 })?;
99 let p = PathBuf::from(path);
100 let store = crate::storage::AvisReader::read_from_file(&p)?;
101 println!("Loaded {} ({} observations)", p.display(), store.count());
102 self.file_path = Some(p);
103 Ok(())
104 }
105
106 fn cmd_info(&self) -> VisionResult<()> {
107 commands::cmd_info(self.require_file()?, self.json)
108 }
109
110 fn cmd_capture(&self, args: &[&str]) -> VisionResult<()> {
111 let p = self.require_file()?;
112 let source = args.first().ok_or_else(|| {
113 crate::types::VisionError::Io(std::io::Error::new(
114 std::io::ErrorKind::InvalidInput,
115 "Usage: /capture <image_path>",
116 ))
117 })?;
118 commands::cmd_capture(p, source, Vec::new(), None, None, self.json)
119 }
120
121 fn cmd_query(&self, args: &[&str]) -> VisionResult<()> {
122 let limit: usize = args.first().and_then(|s| s.parse().ok()).unwrap_or(20);
123 commands::cmd_query(self.require_file()?, None, None, limit, self.json)
124 }
125
126 fn cmd_similar(&self, args: &[&str]) -> VisionResult<()> {
127 let id: u64 = args.first().and_then(|s| s.parse().ok()).ok_or_else(|| {
128 crate::types::VisionError::Io(std::io::Error::new(
129 std::io::ErrorKind::InvalidInput,
130 "Usage: /similar <capture_id> [top_k]",
131 ))
132 })?;
133 let top_k: usize = args.get(1).and_then(|s| s.parse().ok()).unwrap_or(10);
134 commands::cmd_similar(self.require_file()?, id, top_k, 0.5, self.json)
135 }
136
137 fn cmd_compare(&self, args: &[&str]) -> VisionResult<()> {
138 if args.len() < 2 {
139 return Err(crate::types::VisionError::Io(std::io::Error::new(
140 std::io::ErrorKind::InvalidInput,
141 "Usage: /compare <id_a> <id_b>",
142 )));
143 }
144 let id_a: u64 = args[0].parse().map_err(|_| {
145 crate::types::VisionError::Io(std::io::Error::new(
146 std::io::ErrorKind::InvalidInput,
147 "Invalid capture ID",
148 ))
149 })?;
150 let id_b: u64 = args[1].parse().map_err(|_| {
151 crate::types::VisionError::Io(std::io::Error::new(
152 std::io::ErrorKind::InvalidInput,
153 "Invalid capture ID",
154 ))
155 })?;
156 commands::cmd_compare(self.require_file()?, id_a, id_b, self.json)
157 }
158
159 fn cmd_diff(&self, args: &[&str]) -> VisionResult<()> {
160 if args.len() < 2 {
161 return Err(crate::types::VisionError::Io(std::io::Error::new(
162 std::io::ErrorKind::InvalidInput,
163 "Usage: /diff <id_a> <id_b>",
164 )));
165 }
166 let id_a: u64 = args[0].parse().map_err(|_| {
167 crate::types::VisionError::Io(std::io::Error::new(
168 std::io::ErrorKind::InvalidInput,
169 "Invalid capture ID",
170 ))
171 })?;
172 let id_b: u64 = args[1].parse().map_err(|_| {
173 crate::types::VisionError::Io(std::io::Error::new(
174 std::io::ErrorKind::InvalidInput,
175 "Invalid capture ID",
176 ))
177 })?;
178 commands::cmd_diff(self.require_file()?, id_a, id_b, self.json)
179 }
180
181 fn cmd_health(&self) -> VisionResult<()> {
182 commands::cmd_health(self.require_file()?, 168, 0.45, 20, self.json)
183 }
184
185 fn cmd_link(&self, args: &[&str]) -> VisionResult<()> {
186 if args.len() < 2 {
187 return Err(crate::types::VisionError::Io(std::io::Error::new(
188 std::io::ErrorKind::InvalidInput,
189 "Usage: /link <capture_id> <memory_node_id>",
190 )));
191 }
192 let cid: u64 = args[0].parse().map_err(|_| {
193 crate::types::VisionError::Io(std::io::Error::new(
194 std::io::ErrorKind::InvalidInput,
195 "Invalid capture ID",
196 ))
197 })?;
198 let mid: u64 = args[1].parse().map_err(|_| {
199 crate::types::VisionError::Io(std::io::Error::new(
200 std::io::ErrorKind::InvalidInput,
201 "Invalid memory node ID",
202 ))
203 })?;
204 commands::cmd_link(self.require_file()?, cid, mid, self.json)
205 }
206
207 fn cmd_stats(&self) -> VisionResult<()> {
208 commands::cmd_stats(self.require_file()?, self.json)
209 }
210
211 fn cmd_export(&self, args: &[&str]) -> VisionResult<()> {
212 let pretty = args.iter().any(|a| *a == "--pretty" || *a == "-p");
213 commands::cmd_export(self.require_file()?, pretty)
214 }
215
216 fn print_help(&self) {
217 println!("Available commands:");
218 println!(" /create <path> [dim] Create a new .avis file");
219 println!(" /load <path> Load an existing .avis file");
220 println!(" /info Display file info");
221 println!(" /capture <image> Capture an image");
222 println!(" /query [limit] Search observations");
223 println!(" /similar <id> [top_k] Find similar captures");
224 println!(" /compare <id_a> <id_b> Compare two captures");
225 println!(" /diff <id_a> <id_b> Pixel-level diff");
226 println!(" /health Quality report");
227 println!(" /link <cap_id> <mem_id> Link to memory node");
228 println!(" /stats Aggregate statistics");
229 println!(" /export [--pretty] Export as JSON");
230 println!(" /json Toggle JSON output");
231 println!(" /clear Clear screen");
232 println!(" /help Show this message");
233 println!(" /exit Quit");
234 }
235}
236
237fn suggest_command(input: &str) -> Option<&'static str> {
238 const CMDS: &[&str] = &[
239 "create", "load", "info", "capture", "query", "similar", "compare", "diff", "health",
240 "link", "stats", "export", "json", "clear", "help", "exit",
241 ];
242 let mut best: Option<(&str, usize)> = None;
243 for &cmd in CMDS {
244 let dist = levenshtein(input, cmd);
245 if dist <= 2 && (best.is_none() || dist < best.unwrap().1) {
246 best = Some((cmd, dist));
247 }
248 }
249 best.map(|(s, _)| s)
250}
251
252fn levenshtein(a: &str, b: &str) -> usize {
253 let a: Vec<char> = a.chars().collect();
254 let b: Vec<char> = b.chars().collect();
255 let (m, n) = (a.len(), b.len());
256 let mut dp = vec![vec![0usize; n + 1]; m + 1];
257 for (i, row) in dp.iter_mut().enumerate().take(m + 1) {
258 row[0] = i;
259 }
260 for (j, val) in dp[0].iter_mut().enumerate().take(n + 1) {
261 *val = j;
262 }
263 for i in 1..=m {
264 for j in 1..=n {
265 let cost = if a[i - 1] == b[j - 1] { 0 } else { 1 };
266 dp[i][j] = (dp[i - 1][j] + 1)
267 .min(dp[i][j - 1] + 1)
268 .min(dp[i - 1][j - 1] + cost);
269 }
270 }
271 dp[m][n]
272}