1use crate::agent::{AgentError, AgentResult, ProviderType};
11use crate::config::{load_agent_config, save_agent_config};
12use colored::Colorize;
13use std::io::{self, Write};
14use std::path::Path;
15
16const ROBOT: &str = "🤖";
17
18pub fn get_available_models(provider: ProviderType) -> Vec<(&'static str, &'static str)> {
20 match provider {
21 ProviderType::OpenAI => vec![
22 ("gpt-5.2", "GPT-5.2 - Latest reasoning model (Dec 2025)"),
23 ("gpt-5.2-mini", "GPT-5.2 Mini - Fast and affordable"),
24 ("gpt-4o", "GPT-4o - Multimodal workhorse"),
25 ("o1-preview", "o1-preview - Advanced reasoning"),
26 ],
27 ProviderType::Anthropic => vec![
28 ("claude-sonnet-4-20250514", "Claude 4 Sonnet - Latest (May 2025)"),
29 ("claude-3-5-sonnet-latest", "Claude 3.5 Sonnet - Previous gen"),
30 ("claude-3-opus-latest", "Claude 3 Opus - Most capable"),
31 ("claude-3-haiku-latest", "Claude 3 Haiku - Fast and cheap"),
32 ],
33 }
34}
35
36pub struct ChatSession {
38 pub provider: ProviderType,
39 pub model: String,
40 pub project_path: std::path::PathBuf,
41 pub history: Vec<(String, String)>, }
43
44impl ChatSession {
45 pub fn new(project_path: &Path, provider: ProviderType, model: Option<String>) -> Self {
46 let default_model = match provider {
47 ProviderType::OpenAI => "gpt-5.2".to_string(),
48 ProviderType::Anthropic => "claude-sonnet-4-20250514".to_string(),
49 };
50
51 Self {
52 provider,
53 model: model.unwrap_or(default_model),
54 project_path: project_path.to_path_buf(),
55 history: Vec::new(),
56 }
57 }
58
59 pub fn has_api_key(provider: ProviderType) -> bool {
61 let env_key = match provider {
63 ProviderType::OpenAI => std::env::var("OPENAI_API_KEY").ok(),
64 ProviderType::Anthropic => std::env::var("ANTHROPIC_API_KEY").ok(),
65 };
66
67 if env_key.is_some() {
68 return true;
69 }
70
71 let agent_config = load_agent_config();
73 match provider {
74 ProviderType::OpenAI => agent_config.openai_api_key.is_some(),
75 ProviderType::Anthropic => agent_config.anthropic_api_key.is_some(),
76 }
77 }
78
79 pub fn load_api_key_to_env(provider: ProviderType) {
81 let env_var = match provider {
82 ProviderType::OpenAI => "OPENAI_API_KEY",
83 ProviderType::Anthropic => "ANTHROPIC_API_KEY",
84 };
85
86 if std::env::var(env_var).is_ok() {
88 return;
89 }
90
91 let agent_config = load_agent_config();
93 let key = match provider {
94 ProviderType::OpenAI => agent_config.openai_api_key,
95 ProviderType::Anthropic => agent_config.anthropic_api_key,
96 };
97
98 if let Some(key) = key {
99 unsafe {
101 std::env::set_var(env_var, &key);
102 }
103 }
104 }
105
106 pub fn get_configured_providers() -> Vec<ProviderType> {
108 let mut providers = Vec::new();
109 if Self::has_api_key(ProviderType::OpenAI) {
110 providers.push(ProviderType::OpenAI);
111 }
112 if Self::has_api_key(ProviderType::Anthropic) {
113 providers.push(ProviderType::Anthropic);
114 }
115 providers
116 }
117
118 pub fn prompt_api_key(provider: ProviderType) -> AgentResult<String> {
120 let env_var = match provider {
121 ProviderType::OpenAI => "OPENAI_API_KEY",
122 ProviderType::Anthropic => "ANTHROPIC_API_KEY",
123 };
124
125 println!("\n{}", format!("🔑 No API key found for {}", provider).yellow());
126 println!("Please enter your {} API key:", provider);
127 print!("> ");
128 io::stdout().flush().unwrap();
129
130 let mut key = String::new();
131 io::stdin().read_line(&mut key).map_err(|e| AgentError::ToolError(e.to_string()))?;
132 let key = key.trim().to_string();
133
134 if key.is_empty() {
135 return Err(AgentError::MissingApiKey(env_var.to_string()));
136 }
137
138 unsafe {
141 std::env::set_var(env_var, &key);
142 }
143
144 let mut agent_config = load_agent_config();
146 match provider {
147 ProviderType::OpenAI => agent_config.openai_api_key = Some(key.clone()),
148 ProviderType::Anthropic => agent_config.anthropic_api_key = Some(key.clone()),
149 }
150
151 if let Err(e) = save_agent_config(&agent_config) {
152 eprintln!("{}", format!("Warning: Could not save config: {}", e).yellow());
153 } else {
154 println!("{}", "✓ API key saved to ~/.syncable.toml".green());
155 }
156
157 Ok(key)
158 }
159
160 pub fn handle_model_command(&mut self) -> AgentResult<()> {
162 let models = get_available_models(self.provider);
163
164 println!("\n{}", format!("📋 Available models for {}:", self.provider).cyan().bold());
165 println!();
166
167 for (i, (id, desc)) in models.iter().enumerate() {
168 let marker = if *id == self.model { "→ " } else { " " };
169 let num = format!("[{}]", i + 1);
170 println!(" {} {} {} - {}", marker, num.dimmed(), id.white().bold(), desc.dimmed());
171 }
172
173 println!();
174 println!("Enter number to select, or press Enter to keep current:");
175 print!("> ");
176 io::stdout().flush().unwrap();
177
178 let mut input = String::new();
179 io::stdin().read_line(&mut input).ok();
180 let input = input.trim();
181
182 if input.is_empty() {
183 println!("{}", format!("Keeping model: {}", self.model).dimmed());
184 return Ok(());
185 }
186
187 if let Ok(num) = input.parse::<usize>() {
188 if num >= 1 && num <= models.len() {
189 let (id, desc) = models[num - 1];
190 self.model = id.to_string();
191 println!("{}", format!("✓ Switched to {} - {}", id, desc).green());
192 } else {
193 println!("{}", "Invalid selection".red());
194 }
195 } else {
196 self.model = input.to_string();
198 println!("{}", format!("✓ Set model to: {}", input).green());
199 }
200
201 Ok(())
202 }
203
204 pub fn handle_provider_command(&mut self) -> AgentResult<()> {
206 let providers = [ProviderType::OpenAI, ProviderType::Anthropic];
207
208 println!("\n{}", "🔄 Available providers:".cyan().bold());
209 println!();
210
211 for (i, provider) in providers.iter().enumerate() {
212 let marker = if *provider == self.provider { "→ " } else { " " };
213 let has_key = if Self::has_api_key(*provider) {
214 "✓ API key configured".green()
215 } else {
216 "⚠ No API key".yellow()
217 };
218 let num = format!("[{}]", i + 1);
219 println!(" {} {} {} - {}", marker, num.dimmed(), provider.to_string().white().bold(), has_key);
220 }
221
222 println!();
223 println!("Enter number to select:");
224 print!("> ");
225 io::stdout().flush().unwrap();
226
227 let mut input = String::new();
228 io::stdin().read_line(&mut input).ok();
229 let input = input.trim();
230
231 if let Ok(num) = input.parse::<usize>() {
232 if num >= 1 && num <= providers.len() {
233 let new_provider = providers[num - 1];
234
235 if !Self::has_api_key(new_provider) {
237 Self::prompt_api_key(new_provider)?;
238 }
239
240 self.provider = new_provider;
241
242 let default_model = match new_provider {
244 ProviderType::OpenAI => "gpt-5.2",
245 ProviderType::Anthropic => "claude-sonnet-4-20250514",
246 };
247 self.model = default_model.to_string();
248
249 println!("{}", format!("✓ Switched to {} with model {}", new_provider, default_model).green());
250 } else {
251 println!("{}", "Invalid selection".red());
252 }
253 }
254
255 Ok(())
256 }
257
258 pub fn print_help() {
260 println!();
261 println!("{}", "📖 Available Commands:".cyan().bold());
262 println!();
263 println!(" {} - Select a different AI model", "/model".white().bold());
264 println!(" {} - Switch provider (OpenAI/Anthropic)", "/provider".white().bold());
265 println!(" {} - Clear conversation history", "/clear".white().bold());
266 println!(" {} - Show this help message", "/help".white().bold());
267 println!(" {} - Exit the chat", "/exit".white().bold());
268 println!();
269 println!("{}", "Just type your message and press Enter to chat!".dimmed());
270 println!();
271 }
272
273
274 pub fn print_logo() {
276 let purple = "\x1b[38;5;141m"; let orange = "\x1b[38;5;216m"; let pink = "\x1b[38;5;212m"; let magenta = "\x1b[38;5;207m"; let reset = "\x1b[0m";
287
288 println!();
289 println!(
290 "{} ███████╗{}{} ██╗ ██╗{}{}███╗ ██╗{}{} ██████╗{}{} █████╗ {}{}██████╗ {}{}██╗ {}{}███████╗{}",
291 purple, reset, purple, reset, orange, reset, orange, reset, pink, reset, pink, reset, magenta, reset, magenta, reset
292 );
293 println!(
294 "{} ██╔════╝{}{} ╚██╗ ██╔╝{}{}████╗ ██║{}{} ██╔════╝{}{} ██╔══██╗{}{}██╔══██╗{}{}██║ {}{}██╔════╝{}",
295 purple, reset, purple, reset, orange, reset, orange, reset, pink, reset, pink, reset, magenta, reset, magenta, reset
296 );
297 println!(
298 "{} ███████╗{}{} ╚████╔╝ {}{}██╔██╗ ██║{}{} ██║ {}{} ███████║{}{}██████╔╝{}{}██║ {}{}█████╗ {}",
299 purple, reset, purple, reset, orange, reset, orange, reset, pink, reset, pink, reset, magenta, reset, magenta, reset
300 );
301 println!(
302 "{} ╚════██║{}{} ╚██╔╝ {}{}██║╚██╗██║{}{} ██║ {}{} ██╔══██║{}{}██╔══██╗{}{}██║ {}{}██╔══╝ {}",
303 purple, reset, purple, reset, orange, reset, orange, reset, pink, reset, pink, reset, magenta, reset, magenta, reset
304 );
305 println!(
306 "{} ███████║{}{} ██║ {}{}██║ ╚████║{}{} ╚██████╗{}{} ██║ ██║{}{}██████╔╝{}{}███████╗{}{}███████╗{}",
307 purple, reset, purple, reset, orange, reset, orange, reset, pink, reset, pink, reset, magenta, reset, magenta, reset
308 );
309 println!(
310 "{} ╚══════╝{}{} ╚═╝ {}{}╚═╝ ╚═══╝{}{} ╚═════╝{}{} ╚═╝ ╚═╝{}{}╚═════╝ {}{}╚══════╝{}{}╚══════╝{}",
311 purple, reset, purple, reset, orange, reset, orange, reset, pink, reset, pink, reset, magenta, reset, magenta, reset
312 );
313 println!();
314 }
315
316 pub fn print_banner(&self) {
318 Self::print_logo();
320
321 println!(
323 " {} {} powered by {}: {}",
324 ROBOT,
325 "Syncable Agent".white().bold(),
326 self.provider.to_string().cyan(),
327 self.model.cyan()
328 );
329 println!(
330 " {}",
331 "Your AI-powered code analysis assistant".dimmed()
332 );
333 println!();
334 println!(
335 " {} Type your questions. Use {} to exit.\n",
336 "→".cyan(),
337 "exit".yellow().bold()
338 );
339 }
340
341
342 pub fn process_command(&mut self, input: &str) -> AgentResult<bool> {
344 let cmd = input.trim().to_lowercase();
345
346 match cmd.as_str() {
347 "/exit" | "/quit" | "/q" => {
348 println!("\n{}", "👋 Goodbye!".green());
349 return Ok(false);
350 }
351 "/help" | "/h" | "/?" => {
352 Self::print_help();
353 }
354 "/model" | "/m" => {
355 self.handle_model_command()?;
356 }
357 "/provider" | "/p" => {
358 self.handle_provider_command()?;
359 }
360 "/clear" | "/c" => {
361 self.history.clear();
362 println!("{}", "✓ Conversation history cleared".green());
363 }
364 _ => {
365 if cmd.starts_with('/') {
366 println!("{}", format!("Unknown command: {}. Type /help for available commands.", cmd).yellow());
367 }
368 }
369 }
370
371 Ok(true)
372 }
373
374 pub fn is_command(input: &str) -> bool {
376 input.trim().starts_with('/')
377 }
378
379 pub fn read_input(&self) -> io::Result<String> {
381 print!("{}", "You: ".green().bold());
382 io::stdout().flush()?;
383
384 let mut input = String::new();
385 io::stdin().read_line(&mut input)?;
386 Ok(input.trim().to_string())
387 }
388}