4runr-os 2.10.39 → 2.10.41

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/apps/gateway/dist/apps/gateway/src/index.js +14 -4
  2. package/apps/gateway/dist/apps/gateway/src/index.js.map +1 -1
  3. package/apps/gateway/dist/apps/gateway/src/metrics/monitoring-detail.d.ts +18 -0
  4. package/apps/gateway/dist/apps/gateway/src/metrics/monitoring-detail.d.ts.map +1 -0
  5. package/apps/gateway/dist/apps/gateway/src/metrics/monitoring-detail.js +117 -0
  6. package/apps/gateway/dist/apps/gateway/src/metrics/monitoring-detail.js.map +1 -0
  7. package/apps/gateway/dist/apps/gateway/src/middleware/log-capture.d.ts +2 -0
  8. package/apps/gateway/dist/apps/gateway/src/middleware/log-capture.d.ts.map +1 -0
  9. package/apps/gateway/dist/apps/gateway/src/middleware/log-capture.js +54 -0
  10. package/apps/gateway/dist/apps/gateway/src/middleware/log-capture.js.map +1 -0
  11. package/apps/gateway/dist/apps/gateway/src/routes/monitoring.d.ts +15 -0
  12. package/apps/gateway/dist/apps/gateway/src/routes/monitoring.d.ts.map +1 -0
  13. package/apps/gateway/dist/apps/gateway/src/routes/monitoring.js +164 -0
  14. package/apps/gateway/dist/apps/gateway/src/routes/monitoring.js.map +1 -0
  15. package/apps/gateway/package-lock.json +204 -353
  16. package/apps/gateway/src/index.ts +27 -8
  17. package/apps/gateway/src/metrics/monitoring-detail.ts +162 -0
  18. package/apps/gateway/src/middleware/log-capture.ts +70 -0
  19. package/apps/gateway/src/routes/monitoring.ts +298 -0
  20. package/dist/gateway-client.d.ts +2 -0
  21. package/dist/gateway-client.d.ts.map +1 -1
  22. package/dist/gateway-client.js +22 -0
  23. package/dist/gateway-client.js.map +1 -1
  24. package/dist/tui-handlers.js +498 -0
  25. package/dist/tui-handlers.js.map +1 -1
  26. package/mk3-tui/src/app/render_scheduler.rs +111 -112
  27. package/mk3-tui/src/app.rs +1078 -295
  28. package/mk3-tui/src/debug_log.rs +131 -124
  29. package/mk3-tui/src/io/mod.rs +63 -66
  30. package/mk3-tui/src/io/protocol.rs +14 -15
  31. package/mk3-tui/src/io/stdio.rs +31 -32
  32. package/mk3-tui/src/io/ws.rs +25 -32
  33. package/mk3-tui/src/main.rs +774 -212
  34. package/mk3-tui/src/monitoring/mod.rs +428 -0
  35. package/mk3-tui/src/screens/mod.rs +53 -39
  36. package/mk3-tui/src/storage/cache.rs +221 -224
  37. package/mk3-tui/src/storage/mod.rs +5 -6
  38. package/mk3-tui/src/ui/agent_builder.rs +1148 -922
  39. package/mk3-tui/src/ui/agent_list.rs +344 -295
  40. package/mk3-tui/src/ui/boot.rs +145 -148
  41. package/mk3-tui/src/ui/connection_portal.rs +121 -98
  42. package/mk3-tui/src/ui/help.rs +340 -284
  43. package/mk3-tui/src/ui/layout.rs +966 -803
  44. package/mk3-tui/src/ui/mod.rs +1 -1
  45. package/mk3-tui/src/ui/portal_monitoring.rs +1027 -147
  46. package/mk3-tui/src/ui/run_manager.rs +784 -764
  47. package/mk3-tui/src/ui/safe_viewport.rs +236 -235
  48. package/mk3-tui/src/ui/settings.rs +414 -362
  49. package/mk3-tui/src/ui/setup_portal.rs +158 -101
  50. package/mk3-tui/src/websocket.rs +315 -308
  51. package/package.json +2 -2
@@ -1,922 +1,1148 @@
1
- /// Agent Builder Screen
2
- /// Multi-step wizard for creating and editing agents
3
-
4
- use ratatui::prelude::*;
5
- use ratatui::widgets::{Block, Borders, Paragraph, Wrap};
6
- use crate::app::AppState;
7
-
8
- // === 4RUNR BRAND COLORS (matching layout.rs) ===
9
- const BRAND_PURPLE: Color = Color::Rgb(138, 43, 226);
10
- #[allow(dead_code)]
11
- const BRAND_VIOLET: Color = Color::Rgb(148, 103, 189);
12
- const CYBER_CYAN: Color = Color::Rgb(0, 255, 255);
13
- const NEON_GREEN: Color = Color::Rgb(57, 255, 20);
14
- const AMBER_WARN: Color = Color::Rgb(255, 191, 0);
15
- const TEXT_PRIMARY: Color = Color::Rgb(230, 230, 240);
16
- const TEXT_DIM: Color = Color::Rgb(100, 100, 120);
17
- const TEXT_MUTED: Color = Color::Rgb(60, 60, 80);
18
- const BG_PANEL: Color = Color::Rgb(18, 18, 25);
19
-
20
- // === MODEL INFORMATION ===
21
- struct ModelInfo {
22
- context_window: u32,
23
- max_output: u32,
24
- training_cutoff: &'static str,
25
- capabilities: Vec<&'static str>,
26
- }
27
-
28
- fn get_model_info(provider: &str, model: &str) -> ModelInfo {
29
- match (provider, model) {
30
- // OpenAI
31
- ("openai", "gpt-4") => ModelInfo {
32
- context_window: 8192,
33
- max_output: 4096,
34
- training_cutoff: "Sep 2021",
35
- capabilities: vec!["Text", "Code", "Analysis"],
36
- },
37
- ("openai", "gpt-4-turbo") => ModelInfo {
38
- context_window: 128000,
39
- max_output: 4096,
40
- training_cutoff: "Apr 2023",
41
- capabilities: vec!["Text", "Code", "Vision", "JSON"],
42
- },
43
- ("openai", "gpt-3.5-turbo") => ModelInfo {
44
- context_window: 16385,
45
- max_output: 4096,
46
- training_cutoff: "Sep 2021",
47
- capabilities: vec!["Text", "Code"],
48
- },
49
-
50
- // Anthropic
51
- ("anthropic", "claude-3-opus") => ModelInfo {
52
- context_window: 200000,
53
- max_output: 4096,
54
- training_cutoff: "Aug 2023",
55
- capabilities: vec!["Text", "Code", "Vision", "Analysis", "Long Context"],
56
- },
57
- ("anthropic", "claude-3-sonnet") => ModelInfo {
58
- context_window: 200000,
59
- max_output: 4096,
60
- training_cutoff: "Aug 2023",
61
- capabilities: vec!["Text", "Code", "Vision", "Balanced"],
62
- },
63
- ("anthropic", "claude-3-haiku") => ModelInfo {
64
- context_window: 200000,
65
- max_output: 4096,
66
- training_cutoff: "Aug 2023",
67
- capabilities: vec!["Text", "Code", "Fast", "Efficient"],
68
- },
69
-
70
- // Local models
71
- _ => ModelInfo {
72
- context_window: 4096,
73
- max_output: 2048,
74
- training_cutoff: "Varies",
75
- capabilities: vec!["Text", "Local", "Private"],
76
- },
77
- }
78
- }
79
-
80
- // === COST CALCULATOR ===
81
- /// Pricing data for different AI providers (per 1M tokens)
82
- struct ModelPricing {
83
- input_cost: f64, // USD per 1M input tokens
84
- output_cost: f64, // USD per 1M output tokens
85
- }
86
-
87
- fn get_model_pricing(provider: &str, model: &str) -> ModelPricing {
88
- match (provider, model) {
89
- // OpenAI
90
- ("openai", "gpt-4") => ModelPricing { input_cost: 30.0, output_cost: 60.0 },
91
- ("openai", "gpt-4-turbo") => ModelPricing { input_cost: 10.0, output_cost: 30.0 },
92
- ("openai", "gpt-3.5-turbo") => ModelPricing { input_cost: 0.5, output_cost: 1.5 },
93
-
94
- // Anthropic
95
- ("anthropic", "claude-3-opus") => ModelPricing { input_cost: 15.0, output_cost: 75.0 },
96
- ("anthropic", "claude-3-sonnet") => ModelPricing { input_cost: 3.0, output_cost: 15.0 },
97
- ("anthropic", "claude-3-haiku") => ModelPricing { input_cost: 0.25, output_cost: 1.25 },
98
-
99
- // Local models (free)
100
- _ => ModelPricing { input_cost: 0.0, output_cost: 0.0 },
101
- }
102
- }
103
-
104
- struct CostEstimate {
105
- cost_1k: f64,
106
- cost_10k: f64,
107
- cost_100k: f64,
108
- cost_1m: f64,
109
- is_free: bool,
110
- }
111
-
112
- fn calculate_cost(provider: &str, model: &str, _max_tokens: u32) -> CostEstimate {
113
- let pricing = get_model_pricing(provider, model);
114
- let is_free = pricing.input_cost == 0.0 && pricing.output_cost == 0.0;
115
-
116
- if is_free {
117
- return CostEstimate {
118
- cost_1k: 0.0,
119
- cost_10k: 0.0,
120
- cost_100k: 0.0,
121
- cost_1m: 0.0,
122
- is_free: true,
123
- };
124
- }
125
-
126
- // Assume 50/50 split between input and output tokens for estimation
127
- let avg_cost_per_1m = (pricing.input_cost + pricing.output_cost) / 2.0;
128
-
129
- CostEstimate {
130
- cost_1k: avg_cost_per_1m / 1000.0,
131
- cost_10k: avg_cost_per_1m / 100.0,
132
- cost_100k: avg_cost_per_1m / 10.0,
133
- cost_1m: avg_cost_per_1m,
134
- is_free: false,
135
- }
136
- }
137
-
138
- /// Agent Builder wizard state
139
- #[derive(Debug, Clone)]
140
- pub struct AgentBuilderState {
141
- pub current_step: usize, // 1-6
142
- pub focused_field: usize,
143
-
144
- // Step 1: Basic Info
145
- pub name: String,
146
- pub description: String,
147
-
148
- // Step 2: Model Selection
149
- pub use_local_model: bool,
150
- pub provider: String, // "openai", "anthropic", "ollama", etc.
151
- pub model: String, // "gpt-4", "claude-3", "llama2", etc.
152
- pub local_provider: String, // "ollama", "lm-studio", "custom"
153
- pub local_model: String,
154
- pub local_url: String,
155
-
156
- // Step 3: System Prompt
157
- pub system_prompt: String,
158
-
159
- // Step 4: Parameters
160
- pub temperature: f32,
161
- pub max_tokens: u32,
162
- pub top_p: f32,
163
- pub frequency_penalty: f32,
164
- pub presence_penalty: f32,
165
-
166
- // Step 5: Tools
167
- pub selected_tools: Vec<String>,
168
- pub available_tools: Vec<String>,
169
-
170
- // Validation
171
- pub validation_errors: Vec<String>,
172
- }
173
-
174
- impl Default for AgentBuilderState {
175
- fn default() -> Self {
176
- Self {
177
- current_step: 1,
178
- focused_field: 0,
179
- name: String::new(),
180
- description: String::new(),
181
- use_local_model: false,
182
- provider: "openai".to_string(),
183
- model: "gpt-4".to_string(),
184
- local_provider: "ollama".to_string(),
185
- local_model: "llama2".to_string(),
186
- local_url: "http://localhost:11434".to_string(),
187
- system_prompt: String::new(),
188
- temperature: 0.7,
189
- max_tokens: 2000,
190
- top_p: 1.0,
191
- frequency_penalty: 0.0,
192
- presence_penalty: 0.0,
193
- selected_tools: Vec::new(),
194
- available_tools: vec![
195
- "calculator".to_string(),
196
- "time".to_string(),
197
- "weather".to_string(),
198
- ],
199
- validation_errors: Vec::new(),
200
- }
201
- }
202
- }
203
-
204
- impl AgentBuilderState {
205
- /// Validate current step
206
- pub fn validate_step(&mut self) -> bool {
207
- self.validation_errors.clear();
208
-
209
- match self.current_step {
210
- 1 => {
211
- if self.name.trim().is_empty() {
212
- self.validation_errors.push("Name is required".to_string());
213
- }
214
- if self.name.len() > 50 {
215
- self.validation_errors.push("Name must be 50 characters or less".to_string());
216
- }
217
- }
218
- 2 => {
219
- if !self.use_local_model && self.model.is_empty() {
220
- self.validation_errors.push("Model is required".to_string());
221
- }
222
- if self.use_local_model && self.local_model.is_empty() {
223
- self.validation_errors.push("Local model is required".to_string());
224
- }
225
- }
226
- 3 => {
227
- // System prompt is optional
228
- }
229
- 4 => {
230
- if !(0.0..=2.0).contains(&self.temperature) {
231
- self.validation_errors.push("Temperature must be between 0.0 and 2.0".to_string());
232
- }
233
- if self.max_tokens < 1 || self.max_tokens > 100000 {
234
- self.validation_errors.push("Max tokens must be between 1 and 100000".to_string());
235
- }
236
- }
237
- 5 => {
238
- // Tools are optional
239
- }
240
- 6 => {
241
- // Review step - validate all
242
- if self.name.trim().is_empty() {
243
- self.validation_errors.push("Name is required".to_string());
244
- }
245
- }
246
- _ => {}
247
- }
248
-
249
- self.validation_errors.is_empty()
250
- }
251
-
252
- /// Move to next step
253
- pub fn next_step(&mut self) -> bool {
254
- if self.validate_step() && self.current_step < 6 {
255
- self.current_step += 1;
256
- self.focused_field = 0;
257
- true
258
- } else {
259
- false
260
- }
261
- }
262
-
263
- /// Move to previous step
264
- pub fn prev_step(&mut self) {
265
- if self.current_step > 1 {
266
- self.current_step -= 1;
267
- self.focused_field = 0;
268
- }
269
- }
270
-
271
- /// Move to next field within current step
272
- pub fn next_field(&mut self) {
273
- let max_field = self.get_max_field_for_step();
274
- if self.focused_field < max_field {
275
- self.focused_field += 1;
276
- }
277
- }
278
-
279
- /// Move to previous field within current step
280
- pub fn prev_field(&mut self) {
281
- if self.focused_field > 0 {
282
- self.focused_field -= 1;
283
- }
284
- }
285
-
286
- /// Get the maximum field index for the current step
287
- fn get_max_field_for_step(&self) -> usize {
288
- match self.current_step {
289
- 1 => 1, // Name, Description
290
- 2 => 4, // Provider, Model, Local Provider, Local Model, Local URL
291
- 3 => 0, // System Prompt (single field)
292
- 4 => 4, // Temperature, Max Tokens, Top P, Frequency Penalty, Presence Penalty
293
- 5 => self.available_tools.len().saturating_sub(1), // Tools list
294
- 6 => 0, // Review (no editable fields)
295
- _ => 0,
296
- }
297
- }
298
- }
299
-
300
- /// Render the Agent Builder screen
301
- pub fn render(f: &mut Frame, state: &AppState) {
302
- let area = f.size();
303
-
304
- // Get wizard state from AppState
305
- let wizard_state = &state.agent_builder;
306
-
307
- render_wizard(f, area, wizard_state);
308
- }
309
-
310
- fn render_wizard(f: &mut Frame, area: Rect, wizard: &AgentBuilderState) {
311
- use ratatui::layout::{Constraint, Direction, Layout};
312
-
313
- // Split area into content and footer
314
- let v_chunks = Layout::default()
315
- .direction(Direction::Vertical)
316
- .constraints([
317
- Constraint::Min(10), // Content
318
- Constraint::Length(3), // Navigation footer
319
- ])
320
- .split(area);
321
-
322
- // Split content into main wizard and cost calculator (if on step 2 or 4)
323
- let show_cost_calc = wizard.current_step == 2 || wizard.current_step == 4;
324
- let h_chunks = Layout::default()
325
- .direction(Direction::Horizontal)
326
- .constraints(if show_cost_calc {
327
- [
328
- Constraint::Percentage(65), // Wizard content
329
- Constraint::Percentage(35), // Cost calculator
330
- ].as_ref()
331
- } else {
332
- [
333
- Constraint::Percentage(100), // Full width
334
- Constraint::Length(0), // No cost calc
335
- ].as_ref()
336
- })
337
- .split(v_chunks[0]);
338
-
339
- // Progress bar for title
340
- let progress = wizard.current_step as f32 / 6.0;
341
- let filled = (progress * 20.0) as usize;
342
- let empty = 20 - filled;
343
- let progress_bar = format!("[{}{}]", "█".repeat(filled), "░".repeat(empty));
344
-
345
- // Main container with brand styling
346
- let block = Block::default()
347
- .title(format!(" 🤖 Agent Builder - Step {}/6 {} ", wizard.current_step, progress_bar))
348
- .title_style(Style::default().fg(CYBER_CYAN).add_modifier(Modifier::BOLD))
349
- .borders(Borders::ALL)
350
- .border_style(Style::default().fg(BRAND_PURPLE))
351
- .style(Style::default().bg(BG_PANEL));
352
-
353
- let inner = block.inner(h_chunks[0]);
354
- f.render_widget(block, h_chunks[0]);
355
-
356
- // Render current step
357
- match wizard.current_step {
358
- 1 => render_step_1_basic_info(f, inner, wizard),
359
- 2 => render_step_2_model_selection(f, inner, wizard),
360
- 3 => render_step_3_system_prompt(f, inner, wizard),
361
- 4 => render_step_4_parameters(f, inner, wizard),
362
- 5 => render_step_5_tools(f, inner, wizard),
363
- 6 => render_step_6_review(f, inner, wizard),
364
- _ => {}
365
- }
366
-
367
- // Render cost calculator if visible
368
- if show_cost_calc {
369
- render_cost_calculator(f, h_chunks[1], wizard);
370
- }
371
-
372
- // Render navigation footer
373
- render_navigation_footer(f, v_chunks[1], wizard);
374
- }
375
-
376
- fn render_cost_calculator(f: &mut Frame, area: Rect, wizard: &AgentBuilderState) {
377
- use ratatui::layout::{Constraint, Direction, Layout};
378
-
379
- // Split into cost and model info sections
380
- let chunks = Layout::default()
381
- .direction(Direction::Vertical)
382
- .constraints([
383
- Constraint::Percentage(60), // Cost info
384
- Constraint::Percentage(40), // Model info
385
- ])
386
- .split(area);
387
-
388
- let provider = if wizard.use_local_model {
389
- "local"
390
- } else {
391
- &wizard.provider
392
- };
393
- let model = if wizard.use_local_model {
394
- &wizard.local_model
395
- } else {
396
- &wizard.model
397
- };
398
-
399
- let cost = calculate_cost(provider, model, wizard.max_tokens);
400
- let model_info = get_model_info(provider, model);
401
-
402
- // === COST SECTION ===
403
- let cost_block = Block::default()
404
- .title(" 💰 Cost Estimator ")
405
- .borders(Borders::ALL)
406
- .border_style(Style::default().fg(AMBER_WARN))
407
- .style(Style::default().bg(BG_PANEL));
408
-
409
- let cost_inner = cost_block.inner(chunks[0]);
410
- f.render_widget(cost_block, chunks[0]);
411
-
412
- let text = if cost.is_free {
413
- vec![
414
- Line::from(""),
415
- Line::from(vec![
416
- Span::styled("✓ ", Style::default().fg(NEON_GREEN)),
417
- Span::styled("Local Model", Style::default().fg(NEON_GREEN).bold()),
418
- ]),
419
- Line::from(""),
420
- Line::from(vec![
421
- Span::styled("FREE", Style::default().fg(NEON_GREEN).bold().add_modifier(Modifier::UNDERLINED)),
422
- ]),
423
- Line::from(""),
424
- Line::from("No API costs!").style(Style::default().fg(TEXT_DIM)),
425
- Line::from("Self-hosted model").style(Style::default().fg(TEXT_DIM)),
426
- Line::from(""),
427
- Line::from("Benefits:").style(Style::default().fg(TEXT_PRIMARY).bold()),
428
- Line::from(" • No usage limits").style(Style::default().fg(TEXT_DIM)),
429
- Line::from(" • Full privacy").style(Style::default().fg(TEXT_DIM)),
430
- Line::from(" • No API keys").style(Style::default().fg(TEXT_DIM)),
431
- ]
432
- } else {
433
- let cost_color = if cost.cost_1k < 0.01 {
434
- NEON_GREEN
435
- } else if cost.cost_1k < 0.05 {
436
- AMBER_WARN
437
- } else {
438
- Color::Rgb(255, 69, 69)
439
- };
440
-
441
- vec![
442
- Line::from(""),
443
- Line::from(vec![
444
- Span::styled("Provider: ", Style::default().fg(TEXT_DIM)),
445
- Span::styled(provider, Style::default().fg(TEXT_PRIMARY).bold()),
446
- ]),
447
- Line::from(vec![
448
- Span::styled("Model: ", Style::default().fg(TEXT_DIM)),
449
- Span::styled(model, Style::default().fg(TEXT_PRIMARY)),
450
- ]),
451
- Line::from(""),
452
- Line::from("Estimated Cost:").style(Style::default().fg(TEXT_PRIMARY).bold()),
453
- Line::from(""),
454
- Line::from(vec![
455
- Span::styled(" 1K tokens: ", Style::default().fg(TEXT_DIM)),
456
- Span::styled(format!("${:.4}", cost.cost_1k), Style::default().fg(cost_color)),
457
- ]),
458
- Line::from(vec![
459
- Span::styled(" 10K tokens: ", Style::default().fg(TEXT_DIM)),
460
- Span::styled(format!("${:.3}", cost.cost_10k), Style::default().fg(cost_color)),
461
- ]),
462
- Line::from(vec![
463
- Span::styled(" 100K tokens: ", Style::default().fg(TEXT_DIM)),
464
- Span::styled(format!("${:.2}", cost.cost_100k), Style::default().fg(cost_color)),
465
- ]),
466
- Line::from(vec![
467
- Span::styled(" 1M tokens: ", Style::default().fg(TEXT_DIM)),
468
- Span::styled(format!("${:.2}", cost.cost_1m), Style::default().fg(cost_color)),
469
- ]),
470
- Line::from(""),
471
- Line::from(vec![
472
- Span::styled("Max tokens: ", Style::default().fg(TEXT_DIM)),
473
- Span::styled(format!("{}", wizard.max_tokens), Style::default().fg(CYBER_CYAN)),
474
- ]),
475
- ]
476
- };
477
-
478
- let cost_paragraph = Paragraph::new(text)
479
- .wrap(Wrap { trim: false })
480
- .alignment(Alignment::Left);
481
-
482
- f.render_widget(cost_paragraph, cost_inner);
483
-
484
- // === MODEL INFO SECTION ===
485
- let info_block = Block::default()
486
- .title(" 📊 Model Info ")
487
- .borders(Borders::ALL)
488
- .border_style(Style::default().fg(CYBER_CYAN))
489
- .style(Style::default().bg(BG_PANEL));
490
-
491
- let info_inner = info_block.inner(chunks[1]);
492
- f.render_widget(info_block, chunks[1]);
493
-
494
- let info_text = vec![
495
- Line::from(""),
496
- Line::from(vec![
497
- Span::styled("Context: ", Style::default().fg(TEXT_DIM)),
498
- Span::styled(format!("{} tokens", model_info.context_window), Style::default().fg(NEON_GREEN)),
499
- ]),
500
- Line::from(vec![
501
- Span::styled("Max Output: ", Style::default().fg(TEXT_DIM)),
502
- Span::styled(format!("{} tokens", model_info.max_output), Style::default().fg(TEXT_PRIMARY)),
503
- ]),
504
- Line::from(vec![
505
- Span::styled("Training: ", Style::default().fg(TEXT_DIM)),
506
- Span::styled(model_info.training_cutoff, Style::default().fg(TEXT_MUTED)),
507
- ]),
508
- Line::from(""),
509
- Line::from("Capabilities:").style(Style::default().fg(TEXT_PRIMARY).bold()),
510
- Line::from(
511
- model_info.capabilities
512
- .iter()
513
- .map(|cap| format!(" • {}", cap))
514
- .collect::<Vec<_>>()
515
- .join("\n")
516
- ).style(Style::default().fg(TEXT_DIM)),
517
- ];
518
-
519
- let info_paragraph = Paragraph::new(info_text)
520
- .wrap(Wrap { trim: false })
521
- .alignment(Alignment::Left);
522
-
523
- f.render_widget(info_paragraph, info_inner);
524
- }
525
-
526
- fn render_navigation_footer(f: &mut Frame, area: Rect, wizard: &AgentBuilderState) {
527
- let block = Block::default()
528
- .title(" ⌨️ Navigation ")
529
- .borders(Borders::ALL)
530
- .border_style(Style::default().fg(TEXT_DIM))
531
- .style(Style::default().bg(BG_PANEL));
532
-
533
- let inner = block.inner(area);
534
- f.render_widget(block, area);
535
-
536
- let can_go_back = wizard.current_step > 1;
537
- let is_final_step = wizard.current_step == 6;
538
-
539
- let nav_text = if is_final_step {
540
- Line::from(vec![
541
- Span::styled("←", Style::default().fg(if can_go_back { CYBER_CYAN } else { TEXT_MUTED }).bold()),
542
- Span::styled(" Back │ ", Style::default().fg(TEXT_DIM)),
543
- Span::styled("Tab/↑↓", Style::default().fg(CYBER_CYAN).bold()),
544
- Span::styled(" Navigate │ ", Style::default().fg(TEXT_DIM)),
545
- Span::styled("Enter", Style::default().fg(NEON_GREEN).bold()),
546
- Span::styled(" Create Agent │ ", Style::default().fg(TEXT_DIM)),
547
- Span::styled("ESC", Style::default().fg(Color::Rgb(255, 69, 69)).bold()),
548
- Span::styled(" Cancel", Style::default().fg(TEXT_DIM)),
549
- ])
550
- } else {
551
- Line::from(vec![
552
- Span::styled("", Style::default().fg(if can_go_back { CYBER_CYAN } else { TEXT_MUTED }).bold()),
553
- Span::styled(" Back │ ", Style::default().fg(TEXT_DIM)),
554
- Span::styled("Tab/↑↓", Style::default().fg(CYBER_CYAN).bold()),
555
- Span::styled(" Navigate │ ", Style::default().fg(TEXT_DIM)),
556
- Span::styled("→/Enter", Style::default().fg(NEON_GREEN).bold()),
557
- Span::styled(" Next │ ", Style::default().fg(TEXT_DIM)),
558
- Span::styled("ESC", Style::default().fg(Color::Rgb(255, 69, 69)).bold()),
559
- Span::styled(" Cancel", Style::default().fg(TEXT_DIM)),
560
- ])
561
- };
562
-
563
- let paragraph = Paragraph::new(nav_text)
564
- .style(Style::default().fg(TEXT_PRIMARY))
565
- .alignment(Alignment::Center);
566
-
567
- f.render_widget(paragraph, inner);
568
- }
569
-
570
- fn render_step_1_basic_info(f: &mut Frame, area: Rect, wizard: &AgentBuilderState) {
571
- let focused = wizard.focused_field;
572
-
573
- let text = vec![
574
- Line::from(""),
575
- Line::from("Step 1: Basic Information").style(Style::default().fg(BRAND_PURPLE).add_modifier(Modifier::BOLD)),
576
- Line::from(""),
577
- Line::from(vec![
578
- Span::styled(if focused == 0 { "▶ " } else { " " }, Style::default().fg(CYBER_CYAN)),
579
- Span::raw("Name: "),
580
- Span::styled(&wizard.name, Style::default().fg(NEON_GREEN)),
581
- Span::styled(if focused == 0 { "_" } else { "" }, Style::default().fg(CYBER_CYAN)),
582
- ]),
583
- Line::from(" (Required, max 50 characters)").style(Style::default().fg(TEXT_DIM)),
584
- Line::from(""),
585
- Line::from(vec![
586
- Span::styled(if focused == 1 { "▶ " } else { " " }, Style::default().fg(CYBER_CYAN)),
587
- Span::raw("Description: "),
588
- Span::styled(&wizard.description, Style::default().fg(NEON_GREEN)),
589
- Span::styled(if focused == 1 { "_" } else { "" }, Style::default().fg(CYBER_CYAN)),
590
- ]),
591
- Line::from(" (Optional, brief description of agent purpose)").style(Style::default().fg(TEXT_DIM)),
592
- Line::from(""),
593
- Line::from(""),
594
- Line::from("Navigation:").style(Style::default().fg(TEXT_MUTED)),
595
- Line::from(" Tab/Shift+Tab/↑↓ - Move between fields").style(Style::default().fg(TEXT_DIM)),
596
- Line::from(" Enter - Next step").style(Style::default().fg(TEXT_DIM)),
597
- Line::from(" ESC - Cancel and return to Main").style(Style::default().fg(TEXT_DIM)),
598
- ];
599
-
600
- // Show validation errors if any
601
- let mut all_lines = text;
602
- if !wizard.validation_errors.is_empty() {
603
- all_lines.push(Line::from(""));
604
- all_lines.push(Line::from("Validation Errors:").style(Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)));
605
- for error in &wizard.validation_errors {
606
- all_lines.push(Line::from(format!(" • {}", error)).style(Style::default().fg(Color::Red)));
607
- }
608
- }
609
-
610
- let paragraph = Paragraph::new(all_lines)
611
- .wrap(Wrap { trim: false })
612
- .alignment(Alignment::Left);
613
-
614
- f.render_widget(paragraph, area);
615
- }
616
-
617
- fn render_step_2_model_selection(f: &mut Frame, area: Rect, wizard: &AgentBuilderState) {
618
- let focused = wizard.focused_field;
619
- let model_type = if wizard.use_local_model { "Local" } else { "Remote" };
620
-
621
- let text = vec![
622
- Line::from(""),
623
- Line::from("Step 2: Model Selection").style(Style::default().fg(BRAND_PURPLE).add_modifier(Modifier::BOLD)),
624
- Line::from(""),
625
- Line::from(vec![
626
- Span::raw("Model Type: "),
627
- Span::styled(model_type, Style::default().fg(CYBER_CYAN)),
628
- ]),
629
- Line::from(" [ ] Remote (OpenAI, Anthropic, etc.)").style(Style::default().fg(TEXT_DIM)),
630
- Line::from(" [ ] Local (Ollama, LM Studio, etc.)").style(Style::default().fg(TEXT_DIM)),
631
- Line::from(""),
632
- Line::from("Remote Model:").style(Style::default().fg(TEXT_PRIMARY).add_modifier(Modifier::BOLD)),
633
- Line::from(vec![
634
- Span::styled(if focused == 0 { " ▶ " } else { " " }, Style::default().fg(CYBER_CYAN)),
635
- Span::raw("Provider: "),
636
- Span::styled(&wizard.provider, Style::default().fg(NEON_GREEN)),
637
- Span::styled(if focused == 0 { "_" } else { "" }, Style::default().fg(CYBER_CYAN)),
638
- ]),
639
- Line::from(vec![
640
- Span::styled(if focused == 1 { "" } else { " " }, Style::default().fg(CYBER_CYAN)),
641
- Span::raw("Model: "),
642
- Span::styled(&wizard.model, Style::default().fg(NEON_GREEN)),
643
- Span::styled(if focused == 1 { "_" } else { "" }, Style::default().fg(CYBER_CYAN)),
644
- ]),
645
- Line::from(""),
646
- Line::from("Local Model:").style(Style::default().fg(TEXT_PRIMARY).add_modifier(Modifier::BOLD)),
647
- Line::from(vec![
648
- Span::styled(if focused == 2 { " ▶ " } else { " " }, Style::default().fg(CYBER_CYAN)),
649
- Span::raw("Provider: "),
650
- Span::styled(&wizard.local_provider, Style::default().fg(NEON_GREEN)),
651
- Span::styled(if focused == 2 { "_" } else { "" }, Style::default().fg(CYBER_CYAN)),
652
- ]),
653
- Line::from(vec![
654
- Span::styled(if focused == 3 { " ▶ " } else { " " }, Style::default().fg(CYBER_CYAN)),
655
- Span::raw("Model: "),
656
- Span::styled(&wizard.local_model, Style::default().fg(NEON_GREEN)),
657
- Span::styled(if focused == 3 { "_" } else { "" }, Style::default().fg(CYBER_CYAN)),
658
- ]),
659
- Line::from(vec![
660
- Span::styled(if focused == 4 { "" } else { " " }, Style::default().fg(CYBER_CYAN)),
661
- Span::raw("URL: "),
662
- Span::styled(&wizard.local_url, Style::default().fg(NEON_GREEN)),
663
- Span::styled(if focused == 4 { "_" } else { "" }, Style::default().fg(CYBER_CYAN)),
664
- ]),
665
- Line::from(""),
666
- Line::from("Navigation:").style(Style::default().fg(TEXT_MUTED)),
667
- Line::from(" Enter - Next step | ESC - Cancel").style(Style::default().fg(TEXT_DIM)),
668
- ];
669
-
670
- let paragraph = Paragraph::new(text)
671
- .wrap(Wrap { trim: false })
672
- .alignment(Alignment::Left);
673
-
674
- f.render_widget(paragraph, area);
675
- }
676
-
677
- fn render_step_3_system_prompt(f: &mut Frame, area: Rect, wizard: &AgentBuilderState) {
678
- let prompt_preview = if wizard.system_prompt.is_empty() {
679
- "(type to add prompt)".to_string()
680
- } else {
681
- wizard.system_prompt.chars().take(200).collect::<String>()
682
- };
683
-
684
- let text = vec![
685
- Line::from(""),
686
- Line::from("Step 3: System Prompt").style(Style::default().fg(BRAND_PURPLE).add_modifier(Modifier::BOLD)),
687
- Line::from(""),
688
- Line::from("Define the agent's behavior and personality.").style(Style::default().fg(TEXT_PRIMARY)),
689
- Line::from(""),
690
- Line::from(vec![
691
- Span::styled("▶ ", Style::default().fg(CYBER_CYAN)),
692
- Span::raw("Current Prompt: "),
693
- Span::styled("_", Style::default().fg(CYBER_CYAN)),
694
- ]).style(Style::default().fg(TEXT_PRIMARY).add_modifier(Modifier::BOLD)),
695
- Line::from("┌────────────────────────────────────────────────┐").style(Style::default().fg(TEXT_MUTED)),
696
- Line::from(format!("│ {}│", prompt_preview)).style(Style::default().fg(NEON_GREEN)),
697
- Line::from("└────────────────────────────────────────────────┘").style(Style::default().fg(TEXT_MUTED)),
698
- Line::from(""),
699
- Line::from(format!("Characters: {}", wizard.system_prompt.len())).style(Style::default().fg(TEXT_DIM)),
700
- Line::from(""),
701
- Line::from("Examples:").style(Style::default().fg(TEXT_MUTED)),
702
- Line::from(" • You are a helpful coding assistant").style(Style::default().fg(TEXT_DIM)),
703
- Line::from(" • You are an expert data analyst").style(Style::default().fg(TEXT_DIM)),
704
- Line::from(" • You are a creative writing partner").style(Style::default().fg(TEXT_DIM)),
705
- Line::from(""),
706
- Line::from("Navigation:").style(Style::default().fg(TEXT_MUTED)),
707
- Line::from(" Enter - Next step | ESC - Cancel").style(Style::default().fg(TEXT_DIM)),
708
- ];
709
-
710
- let paragraph = Paragraph::new(text)
711
- .wrap(Wrap { trim: false })
712
- .alignment(Alignment::Left);
713
-
714
- f.render_widget(paragraph, area);
715
- }
716
-
717
- fn render_step_4_parameters(f: &mut Frame, area: Rect, wizard: &AgentBuilderState) {
718
- let focused = wizard.focused_field;
719
-
720
- let text = vec![
721
- Line::from(""),
722
- Line::from("Step 4: Parameters").style(Style::default().fg(BRAND_PURPLE).add_modifier(Modifier::BOLD)),
723
- Line::from(""),
724
- Line::from("Fine-tune the model's behavior.").style(Style::default().fg(TEXT_PRIMARY)),
725
- Line::from(""),
726
- Line::from(vec![
727
- Span::styled(if focused == 0 { " " } else { " " }, Style::default().fg(CYBER_CYAN)),
728
- Span::raw("Temperature: "),
729
- Span::styled(format!("{:.2}", wizard.temperature), Style::default().fg(NEON_GREEN)),
730
- Span::styled(if focused == 0 { "_" } else { "" }, Style::default().fg(CYBER_CYAN)),
731
- Span::raw(" (0.0 = focused, 2.0 = creative)"),
732
- ]).style(Style::default().fg(TEXT_PRIMARY)),
733
- Line::from(""),
734
- Line::from(vec![
735
- Span::styled(if focused == 1 { "▶ " } else { " " }, Style::default().fg(CYBER_CYAN)),
736
- Span::raw("Max Tokens: "),
737
- Span::styled(format!("{}", wizard.max_tokens), Style::default().fg(NEON_GREEN)),
738
- Span::styled(if focused == 1 { "_" } else { "" }, Style::default().fg(CYBER_CYAN)),
739
- Span::raw(" (1-100000)"),
740
- ]).style(Style::default().fg(TEXT_PRIMARY)),
741
- Line::from(""),
742
- Line::from(vec![
743
- Span::raw("Top P: "),
744
- Span::styled(format!("{:.2}", wizard.top_p), Style::default().fg(TEXT_DIM)),
745
- Span::raw(" (0.0-1.0, nucleus sampling)"),
746
- ]).style(Style::default().fg(TEXT_DIM)),
747
- Line::from(""),
748
- Line::from(vec![
749
- Span::raw("Frequency Penalty: "),
750
- Span::styled(format!("{:.2}", wizard.frequency_penalty), Style::default().fg(TEXT_DIM)),
751
- Span::raw(" (-2.0 to 2.0)"),
752
- ]).style(Style::default().fg(TEXT_DIM)),
753
- Line::from(""),
754
- Line::from(vec![
755
- Span::raw("Presence Penalty: "),
756
- Span::styled(format!("{:.2}", wizard.presence_penalty), Style::default().fg(TEXT_DIM)),
757
- Span::raw(" (-2.0 to 2.0)"),
758
- ]).style(Style::default().fg(TEXT_DIM)),
759
- Line::from(""),
760
- Line::from("Navigation:").style(Style::default().fg(TEXT_MUTED)),
761
- Line::from(" Enter - Next step | ESC - Cancel").style(Style::default().fg(TEXT_DIM)),
762
- ];
763
-
764
- let paragraph = Paragraph::new(text)
765
- .wrap(Wrap { trim: false })
766
- .alignment(Alignment::Left);
767
-
768
- f.render_widget(paragraph, area);
769
- }
770
-
771
- fn render_step_5_tools(f: &mut Frame, area: Rect, wizard: &AgentBuilderState) {
772
- let selected_count = wizard.selected_tools.len();
773
- let available_count = wizard.available_tools.len();
774
- let focused = wizard.focused_field;
775
-
776
- let mut text = vec![
777
- Line::from(""),
778
- Line::from("Step 5: Tools Selection").style(Style::default().fg(BRAND_PURPLE).add_modifier(Modifier::BOLD)),
779
- Line::from(""),
780
- Line::from(format!("Selected: {} / {} tools", selected_count, available_count)).style(Style::default().fg(CYBER_CYAN)),
781
- Line::from(""),
782
- Line::from("Available Tools:").style(Style::default().fg(TEXT_PRIMARY).add_modifier(Modifier::BOLD)),
783
- ];
784
-
785
- for (i, tool) in wizard.available_tools.iter().enumerate() {
786
- let is_selected = wizard.selected_tools.contains(tool);
787
- let checkbox = if is_selected { "[✓]" } else { "[ ]" };
788
- let is_focused = i == focused;
789
- let style = if is_selected {
790
- Style::default().fg(NEON_GREEN)
791
- } else if is_focused {
792
- Style::default().fg(CYBER_CYAN)
793
- } else {
794
- Style::default().fg(TEXT_PRIMARY)
795
- };
796
- text.push(Line::from(vec![
797
- Span::styled(if is_focused { "▶ " } else { " " }, Style::default().fg(CYBER_CYAN)),
798
- Span::styled(format!("{} ", checkbox), style),
799
- Span::styled(tool.clone(), style),
800
- ]));
801
- }
802
-
803
- text.extend(vec![
804
- Line::from(""),
805
- Line::from("Note: Use Space to toggle, ↑↓ to navigate").style(Style::default().fg(TEXT_DIM)),
806
- Line::from(""),
807
- Line::from("Navigation:").style(Style::default().fg(TEXT_MUTED)),
808
- Line::from(" Enter - Next step | ESC - Cancel").style(Style::default().fg(TEXT_DIM)),
809
- ]);
810
-
811
- let paragraph = Paragraph::new(text)
812
- .wrap(Wrap { trim: false })
813
- .alignment(Alignment::Left);
814
-
815
- f.render_widget(paragraph, area);
816
- }
817
-
818
- fn render_step_6_review(f: &mut Frame, area: Rect, wizard: &AgentBuilderState) {
819
- let provider = if wizard.use_local_model {
820
- "local"
821
- } else {
822
- &wizard.provider
823
- };
824
- let model = if wizard.use_local_model {
825
- &wizard.local_model
826
- } else {
827
- &wizard.model
828
- };
829
-
830
- let model_info_str = if wizard.use_local_model {
831
- format!("{} ({})", wizard.local_model, wizard.local_provider)
832
- } else {
833
- format!("{} ({})", wizard.model, wizard.provider)
834
- };
835
-
836
- let cost = calculate_cost(provider, model, wizard.max_tokens);
837
- let model_info = get_model_info(provider, model);
838
-
839
- let mut text = vec![
840
- Line::from(""),
841
- Line::from("Step 6: Review & Confirm").style(Style::default().fg(BRAND_PURPLE).add_modifier(Modifier::BOLD)),
842
- Line::from(""),
843
- Line::from("✓ Configuration Complete - Ready to Create").style(Style::default().fg(NEON_GREEN).bold()),
844
- Line::from(""),
845
- Line::from("═══════════════════════════════════════════════════").style(Style::default().fg(TEXT_MUTED)),
846
- Line::from(""),
847
- Line::from("Basic Info:").style(Style::default().fg(CYBER_CYAN).bold()),
848
- Line::from(vec![
849
- Span::styled(" Name: ", Style::default().fg(TEXT_DIM)),
850
- Span::styled(&wizard.name, Style::default().fg(NEON_GREEN).add_modifier(Modifier::BOLD)),
851
- ]),
852
- Line::from(vec![
853
- Span::styled(" Description: ", Style::default().fg(TEXT_DIM)),
854
- Span::styled(
855
- if wizard.description.is_empty() { "(none)" } else { wizard.description.as_str() },
856
- Style::default().fg(TEXT_PRIMARY)
857
- ),
858
- ]),
859
- Line::from(""),
860
- Line::from("Model Configuration:").style(Style::default().fg(CYBER_CYAN).bold()),
861
- Line::from(vec![
862
- Span::styled(" Model: ", Style::default().fg(TEXT_DIM)),
863
- Span::styled(&model_info_str, Style::default().fg(TEXT_PRIMARY).bold()),
864
- ]),
865
- Line::from(vec![
866
- Span::styled(" Context Window: ", Style::default().fg(TEXT_DIM)),
867
- Span::styled(format!("{} tokens", model_info.context_window), Style::default().fg(TEXT_PRIMARY)),
868
- ]),
869
- Line::from(vec![
870
- Span::styled(" Temperature: ", Style::default().fg(TEXT_DIM)),
871
- Span::styled(format!("{:.2}", wizard.temperature), Style::default().fg(TEXT_PRIMARY)),
872
- ]),
873
- Line::from(vec![
874
- Span::styled(" Max Tokens: ", Style::default().fg(TEXT_DIM)),
875
- Span::styled(format!("{}", wizard.max_tokens), Style::default().fg(TEXT_PRIMARY)),
876
- ]),
877
- Line::from(""),
878
- Line::from("Tools:").style(Style::default().fg(CYBER_CYAN).bold()),
879
- Line::from(vec![
880
- Span::styled(" ", Style::default()),
881
- Span::styled(
882
- if wizard.selected_tools.is_empty() {
883
- "None selected".to_string()
884
- } else {
885
- wizard.selected_tools.join(", ")
886
- },
887
- Style::default().fg(TEXT_PRIMARY)
888
- ),
889
- ]),
890
- ];
891
-
892
- // Add cost estimate
893
- if !cost.is_free {
894
- text.push(Line::from(""));
895
- text.push(Line::from("Estimated Cost:").style(Style::default().fg(AMBER_WARN).bold()));
896
- text.push(Line::from(vec![
897
- Span::styled(" Per 1K tokens: ", Style::default().fg(TEXT_DIM)),
898
- Span::styled(format!("${:.4}", cost.cost_1k), Style::default().fg(TEXT_PRIMARY)),
899
- ]));
900
- } else {
901
- text.push(Line::from(""));
902
- text.push(Line::from(vec![
903
- Span::styled(" ✓ ", Style::default().fg(NEON_GREEN)),
904
- Span::styled("FREE (Local Model)", Style::default().fg(NEON_GREEN).bold()),
905
- ]));
906
- }
907
-
908
- text.push(Line::from(""));
909
- text.push(Line::from("═══════════════════════════════════════════════════").style(Style::default().fg(TEXT_MUTED)));
910
- text.push(Line::from(""));
911
- text.push(Line::from(vec![
912
- Span::styled("Press ", Style::default().fg(TEXT_DIM)),
913
- Span::styled("Enter", Style::default().fg(NEON_GREEN).bold()),
914
- Span::styled(" to create this agent", Style::default().fg(TEXT_DIM)),
915
- ]));
916
-
917
- let paragraph = Paragraph::new(text)
918
- .wrap(Wrap { trim: false })
919
- .alignment(Alignment::Left);
920
-
921
- f.render_widget(paragraph, area);
922
- }
1
+ use crate::app::AppState;
2
+ /// Agent Builder Screen
3
+ /// Multi-step wizard for creating and editing agents
4
+ use ratatui::prelude::*;
5
+ use ratatui::widgets::{Block, Borders, Paragraph, Wrap};
6
+
7
+ // === 4RUNR BRAND COLORS (matching layout.rs) ===
8
+ const BRAND_PURPLE: Color = Color::Rgb(138, 43, 226);
9
+ #[allow(dead_code)]
10
+ const BRAND_VIOLET: Color = Color::Rgb(148, 103, 189);
11
+ const CYBER_CYAN: Color = Color::Rgb(0, 255, 255);
12
+ const NEON_GREEN: Color = Color::Rgb(57, 255, 20);
13
+ const AMBER_WARN: Color = Color::Rgb(255, 191, 0);
14
+ const TEXT_PRIMARY: Color = Color::Rgb(230, 230, 240);
15
+ const TEXT_DIM: Color = Color::Rgb(100, 100, 120);
16
+ const TEXT_MUTED: Color = Color::Rgb(60, 60, 80);
17
+ const BG_PANEL: Color = Color::Rgb(18, 18, 25);
18
+
19
+ // === MODEL INFORMATION ===
20
+ struct ModelInfo {
21
+ context_window: u32,
22
+ max_output: u32,
23
+ training_cutoff: &'static str,
24
+ capabilities: Vec<&'static str>,
25
+ }
26
+
27
+ fn get_model_info(provider: &str, model: &str) -> ModelInfo {
28
+ match (provider, model) {
29
+ // OpenAI
30
+ ("openai", "gpt-4") => ModelInfo {
31
+ context_window: 8192,
32
+ max_output: 4096,
33
+ training_cutoff: "Sep 2021",
34
+ capabilities: vec!["Text", "Code", "Analysis"],
35
+ },
36
+ ("openai", "gpt-4-turbo") => ModelInfo {
37
+ context_window: 128000,
38
+ max_output: 4096,
39
+ training_cutoff: "Apr 2023",
40
+ capabilities: vec!["Text", "Code", "Vision", "JSON"],
41
+ },
42
+ ("openai", "gpt-3.5-turbo") => ModelInfo {
43
+ context_window: 16385,
44
+ max_output: 4096,
45
+ training_cutoff: "Sep 2021",
46
+ capabilities: vec!["Text", "Code"],
47
+ },
48
+
49
+ // Anthropic
50
+ ("anthropic", "claude-3-opus") => ModelInfo {
51
+ context_window: 200000,
52
+ max_output: 4096,
53
+ training_cutoff: "Aug 2023",
54
+ capabilities: vec!["Text", "Code", "Vision", "Analysis", "Long Context"],
55
+ },
56
+ ("anthropic", "claude-3-sonnet") => ModelInfo {
57
+ context_window: 200000,
58
+ max_output: 4096,
59
+ training_cutoff: "Aug 2023",
60
+ capabilities: vec!["Text", "Code", "Vision", "Balanced"],
61
+ },
62
+ ("anthropic", "claude-3-haiku") => ModelInfo {
63
+ context_window: 200000,
64
+ max_output: 4096,
65
+ training_cutoff: "Aug 2023",
66
+ capabilities: vec!["Text", "Code", "Fast", "Efficient"],
67
+ },
68
+
69
+ // Local models
70
+ _ => ModelInfo {
71
+ context_window: 4096,
72
+ max_output: 2048,
73
+ training_cutoff: "Varies",
74
+ capabilities: vec!["Text", "Local", "Private"],
75
+ },
76
+ }
77
+ }
78
+
79
+ // === COST CALCULATOR ===
80
+ /// Pricing data for different AI providers (per 1M tokens)
81
+ struct ModelPricing {
82
+ input_cost: f64, // USD per 1M input tokens
83
+ output_cost: f64, // USD per 1M output tokens
84
+ }
85
+
86
+ fn get_model_pricing(provider: &str, model: &str) -> ModelPricing {
87
+ match (provider, model) {
88
+ // OpenAI
89
+ ("openai", "gpt-4") => ModelPricing {
90
+ input_cost: 30.0,
91
+ output_cost: 60.0,
92
+ },
93
+ ("openai", "gpt-4-turbo") => ModelPricing {
94
+ input_cost: 10.0,
95
+ output_cost: 30.0,
96
+ },
97
+ ("openai", "gpt-3.5-turbo") => ModelPricing {
98
+ input_cost: 0.5,
99
+ output_cost: 1.5,
100
+ },
101
+
102
+ // Anthropic
103
+ ("anthropic", "claude-3-opus") => ModelPricing {
104
+ input_cost: 15.0,
105
+ output_cost: 75.0,
106
+ },
107
+ ("anthropic", "claude-3-sonnet") => ModelPricing {
108
+ input_cost: 3.0,
109
+ output_cost: 15.0,
110
+ },
111
+ ("anthropic", "claude-3-haiku") => ModelPricing {
112
+ input_cost: 0.25,
113
+ output_cost: 1.25,
114
+ },
115
+
116
+ // Local models (free)
117
+ _ => ModelPricing {
118
+ input_cost: 0.0,
119
+ output_cost: 0.0,
120
+ },
121
+ }
122
+ }
123
+
124
+ struct CostEstimate {
125
+ cost_1k: f64,
126
+ cost_10k: f64,
127
+ cost_100k: f64,
128
+ cost_1m: f64,
129
+ is_free: bool,
130
+ }
131
+
132
+ fn calculate_cost(provider: &str, model: &str, _max_tokens: u32) -> CostEstimate {
133
+ let pricing = get_model_pricing(provider, model);
134
+ let is_free = pricing.input_cost == 0.0 && pricing.output_cost == 0.0;
135
+
136
+ if is_free {
137
+ return CostEstimate {
138
+ cost_1k: 0.0,
139
+ cost_10k: 0.0,
140
+ cost_100k: 0.0,
141
+ cost_1m: 0.0,
142
+ is_free: true,
143
+ };
144
+ }
145
+
146
+ // Assume 50/50 split between input and output tokens for estimation
147
+ let avg_cost_per_1m = (pricing.input_cost + pricing.output_cost) / 2.0;
148
+
149
+ CostEstimate {
150
+ cost_1k: avg_cost_per_1m / 1000.0,
151
+ cost_10k: avg_cost_per_1m / 100.0,
152
+ cost_100k: avg_cost_per_1m / 10.0,
153
+ cost_1m: avg_cost_per_1m,
154
+ is_free: false,
155
+ }
156
+ }
157
+
158
+ /// Agent Builder wizard state
159
+ #[derive(Debug, Clone)]
160
+ pub struct AgentBuilderState {
161
+ pub current_step: usize, // 1-6
162
+ pub focused_field: usize,
163
+
164
+ // Step 1: Basic Info
165
+ pub name: String,
166
+ pub description: String,
167
+
168
+ // Step 2: Model Selection
169
+ pub use_local_model: bool,
170
+ pub provider: String, // "openai", "anthropic", "ollama", etc.
171
+ pub model: String, // "gpt-4", "claude-3", "llama2", etc.
172
+ pub local_provider: String, // "ollama", "lm-studio", "custom"
173
+ pub local_model: String,
174
+ pub local_url: String,
175
+
176
+ // Step 3: System Prompt
177
+ pub system_prompt: String,
178
+
179
+ // Step 4: Parameters
180
+ pub temperature: f32,
181
+ pub max_tokens: u32,
182
+ pub top_p: f32,
183
+ pub frequency_penalty: f32,
184
+ pub presence_penalty: f32,
185
+
186
+ // Step 5: Tools
187
+ pub selected_tools: Vec<String>,
188
+ pub available_tools: Vec<String>,
189
+
190
+ // Validation
191
+ pub validation_errors: Vec<String>,
192
+ }
193
+
194
+ impl Default for AgentBuilderState {
195
+ fn default() -> Self {
196
+ Self {
197
+ current_step: 1,
198
+ focused_field: 0,
199
+ name: String::new(),
200
+ description: String::new(),
201
+ use_local_model: false,
202
+ provider: "openai".to_string(),
203
+ model: "gpt-4".to_string(),
204
+ local_provider: "ollama".to_string(),
205
+ local_model: "llama2".to_string(),
206
+ local_url: "http://localhost:11434".to_string(),
207
+ system_prompt: String::new(),
208
+ temperature: 0.7,
209
+ max_tokens: 2000,
210
+ top_p: 1.0,
211
+ frequency_penalty: 0.0,
212
+ presence_penalty: 0.0,
213
+ selected_tools: Vec::new(),
214
+ available_tools: vec![
215
+ "calculator".to_string(),
216
+ "time".to_string(),
217
+ "weather".to_string(),
218
+ ],
219
+ validation_errors: Vec::new(),
220
+ }
221
+ }
222
+ }
223
+
224
+ impl AgentBuilderState {
225
+ /// Validate current step
226
+ pub fn validate_step(&mut self) -> bool {
227
+ self.validation_errors.clear();
228
+
229
+ match self.current_step {
230
+ 1 => {
231
+ if self.name.trim().is_empty() {
232
+ self.validation_errors.push("Name is required".to_string());
233
+ }
234
+ if self.name.len() > 50 {
235
+ self.validation_errors
236
+ .push("Name must be 50 characters or less".to_string());
237
+ }
238
+ }
239
+ 2 => {
240
+ if !self.use_local_model && self.model.is_empty() {
241
+ self.validation_errors.push("Model is required".to_string());
242
+ }
243
+ if self.use_local_model && self.local_model.is_empty() {
244
+ self.validation_errors
245
+ .push("Local model is required".to_string());
246
+ }
247
+ }
248
+ 3 => {
249
+ // System prompt is optional
250
+ }
251
+ 4 => {
252
+ if !(0.0..=2.0).contains(&self.temperature) {
253
+ self.validation_errors
254
+ .push("Temperature must be between 0.0 and 2.0".to_string());
255
+ }
256
+ if self.max_tokens < 1 || self.max_tokens > 100000 {
257
+ self.validation_errors
258
+ .push("Max tokens must be between 1 and 100000".to_string());
259
+ }
260
+ }
261
+ 5 => {
262
+ // Tools are optional
263
+ }
264
+ 6 => {
265
+ // Review step - validate all
266
+ if self.name.trim().is_empty() {
267
+ self.validation_errors.push("Name is required".to_string());
268
+ }
269
+ }
270
+ _ => {}
271
+ }
272
+
273
+ self.validation_errors.is_empty()
274
+ }
275
+
276
+ /// Move to next step
277
+ pub fn next_step(&mut self) -> bool {
278
+ if self.validate_step() && self.current_step < 6 {
279
+ self.current_step += 1;
280
+ self.focused_field = 0;
281
+ true
282
+ } else {
283
+ false
284
+ }
285
+ }
286
+
287
+ /// Move to previous step
288
+ pub fn prev_step(&mut self) {
289
+ if self.current_step > 1 {
290
+ self.current_step -= 1;
291
+ self.focused_field = 0;
292
+ }
293
+ }
294
+
295
+ /// Move to next field within current step
296
+ pub fn next_field(&mut self) {
297
+ let max_field = self.get_max_field_for_step();
298
+ if self.focused_field < max_field {
299
+ self.focused_field += 1;
300
+ }
301
+ }
302
+
303
+ /// Move to previous field within current step
304
+ pub fn prev_field(&mut self) {
305
+ if self.focused_field > 0 {
306
+ self.focused_field -= 1;
307
+ }
308
+ }
309
+
310
+ /// Get the maximum field index for the current step
311
+ fn get_max_field_for_step(&self) -> usize {
312
+ match self.current_step {
313
+ 1 => 1, // Name, Description
314
+ 2 => 4, // Provider, Model, Local Provider, Local Model, Local URL
315
+ 3 => 0, // System Prompt (single field)
316
+ 4 => 4, // Temperature, Max Tokens, Top P, Frequency Penalty, Presence Penalty
317
+ 5 => self.available_tools.len().saturating_sub(1), // Tools list
318
+ 6 => 0, // Review (no editable fields)
319
+ _ => 0,
320
+ }
321
+ }
322
+ }
323
+
324
+ /// Render the Agent Builder screen
325
+ pub fn render(f: &mut Frame, state: &AppState) {
326
+ let area = f.size();
327
+
328
+ // Get wizard state from AppState
329
+ let wizard_state = &state.agent_builder;
330
+
331
+ render_wizard(f, area, wizard_state);
332
+ }
333
+
334
+ fn render_wizard(f: &mut Frame, area: Rect, wizard: &AgentBuilderState) {
335
+ use ratatui::layout::{Constraint, Direction, Layout};
336
+
337
+ // Split area into content and footer
338
+ let v_chunks = Layout::default()
339
+ .direction(Direction::Vertical)
340
+ .constraints([
341
+ Constraint::Min(10), // Content
342
+ Constraint::Length(3), // Navigation footer
343
+ ])
344
+ .split(area);
345
+
346
+ // Split content into main wizard and cost calculator (if on step 2 or 4)
347
+ let show_cost_calc = wizard.current_step == 2 || wizard.current_step == 4;
348
+ let h_chunks = Layout::default()
349
+ .direction(Direction::Horizontal)
350
+ .constraints(if show_cost_calc {
351
+ [
352
+ Constraint::Percentage(65), // Wizard content
353
+ Constraint::Percentage(35), // Cost calculator
354
+ ]
355
+ .as_ref()
356
+ } else {
357
+ [
358
+ Constraint::Percentage(100), // Full width
359
+ Constraint::Length(0), // No cost calc
360
+ ]
361
+ .as_ref()
362
+ })
363
+ .split(v_chunks[0]);
364
+
365
+ // Progress bar for title
366
+ let progress = wizard.current_step as f32 / 6.0;
367
+ let filled = (progress * 20.0) as usize;
368
+ let empty = 20 - filled;
369
+ let progress_bar = format!("[{}{}]", "█".repeat(filled), "░".repeat(empty));
370
+
371
+ // Main container with brand styling
372
+ let block = Block::default()
373
+ .title(format!(
374
+ " 🤖 Agent Builder - Step {}/6 {} ",
375
+ wizard.current_step, progress_bar
376
+ ))
377
+ .title_style(Style::default().fg(CYBER_CYAN).add_modifier(Modifier::BOLD))
378
+ .borders(Borders::ALL)
379
+ .border_style(Style::default().fg(BRAND_PURPLE))
380
+ .style(Style::default().bg(BG_PANEL));
381
+
382
+ let inner = block.inner(h_chunks[0]);
383
+ f.render_widget(block, h_chunks[0]);
384
+
385
+ // Render current step
386
+ match wizard.current_step {
387
+ 1 => render_step_1_basic_info(f, inner, wizard),
388
+ 2 => render_step_2_model_selection(f, inner, wizard),
389
+ 3 => render_step_3_system_prompt(f, inner, wizard),
390
+ 4 => render_step_4_parameters(f, inner, wizard),
391
+ 5 => render_step_5_tools(f, inner, wizard),
392
+ 6 => render_step_6_review(f, inner, wizard),
393
+ _ => {}
394
+ }
395
+
396
+ // Render cost calculator if visible
397
+ if show_cost_calc {
398
+ render_cost_calculator(f, h_chunks[1], wizard);
399
+ }
400
+
401
+ // Render navigation footer
402
+ render_navigation_footer(f, v_chunks[1], wizard);
403
+ }
404
+
405
+ fn render_cost_calculator(f: &mut Frame, area: Rect, wizard: &AgentBuilderState) {
406
+ use ratatui::layout::{Constraint, Direction, Layout};
407
+
408
+ // Split into cost and model info sections
409
+ let chunks = Layout::default()
410
+ .direction(Direction::Vertical)
411
+ .constraints([
412
+ Constraint::Percentage(60), // Cost info
413
+ Constraint::Percentage(40), // Model info
414
+ ])
415
+ .split(area);
416
+
417
+ let provider = if wizard.use_local_model {
418
+ "local"
419
+ } else {
420
+ &wizard.provider
421
+ };
422
+ let model = if wizard.use_local_model {
423
+ &wizard.local_model
424
+ } else {
425
+ &wizard.model
426
+ };
427
+
428
+ let cost = calculate_cost(provider, model, wizard.max_tokens);
429
+ let model_info = get_model_info(provider, model);
430
+
431
+ // === COST SECTION ===
432
+ let cost_block = Block::default()
433
+ .title(" 💰 Cost Estimator ")
434
+ .borders(Borders::ALL)
435
+ .border_style(Style::default().fg(AMBER_WARN))
436
+ .style(Style::default().bg(BG_PANEL));
437
+
438
+ let cost_inner = cost_block.inner(chunks[0]);
439
+ f.render_widget(cost_block, chunks[0]);
440
+
441
+ let text = if cost.is_free {
442
+ vec![
443
+ Line::from(""),
444
+ Line::from(vec![
445
+ Span::styled("✓ ", Style::default().fg(NEON_GREEN)),
446
+ Span::styled("Local Model", Style::default().fg(NEON_GREEN).bold()),
447
+ ]),
448
+ Line::from(""),
449
+ Line::from(vec![Span::styled(
450
+ "FREE",
451
+ Style::default()
452
+ .fg(NEON_GREEN)
453
+ .bold()
454
+ .add_modifier(Modifier::UNDERLINED),
455
+ )]),
456
+ Line::from(""),
457
+ Line::from("No API costs!").style(Style::default().fg(TEXT_DIM)),
458
+ Line::from("Self-hosted model").style(Style::default().fg(TEXT_DIM)),
459
+ Line::from(""),
460
+ Line::from("Benefits:").style(Style::default().fg(TEXT_PRIMARY).bold()),
461
+ Line::from(" • No usage limits").style(Style::default().fg(TEXT_DIM)),
462
+ Line::from(" • Full privacy").style(Style::default().fg(TEXT_DIM)),
463
+ Line::from(" No API keys").style(Style::default().fg(TEXT_DIM)),
464
+ ]
465
+ } else {
466
+ let cost_color = if cost.cost_1k < 0.01 {
467
+ NEON_GREEN
468
+ } else if cost.cost_1k < 0.05 {
469
+ AMBER_WARN
470
+ } else {
471
+ Color::Rgb(255, 69, 69)
472
+ };
473
+
474
+ vec![
475
+ Line::from(""),
476
+ Line::from(vec![
477
+ Span::styled("Provider: ", Style::default().fg(TEXT_DIM)),
478
+ Span::styled(provider, Style::default().fg(TEXT_PRIMARY).bold()),
479
+ ]),
480
+ Line::from(vec![
481
+ Span::styled("Model: ", Style::default().fg(TEXT_DIM)),
482
+ Span::styled(model, Style::default().fg(TEXT_PRIMARY)),
483
+ ]),
484
+ Line::from(""),
485
+ Line::from("Estimated Cost:").style(Style::default().fg(TEXT_PRIMARY).bold()),
486
+ Line::from(""),
487
+ Line::from(vec![
488
+ Span::styled(" 1K tokens: ", Style::default().fg(TEXT_DIM)),
489
+ Span::styled(
490
+ format!("${:.4}", cost.cost_1k),
491
+ Style::default().fg(cost_color),
492
+ ),
493
+ ]),
494
+ Line::from(vec![
495
+ Span::styled(" 10K tokens: ", Style::default().fg(TEXT_DIM)),
496
+ Span::styled(
497
+ format!("${:.3}", cost.cost_10k),
498
+ Style::default().fg(cost_color),
499
+ ),
500
+ ]),
501
+ Line::from(vec![
502
+ Span::styled(" 100K tokens: ", Style::default().fg(TEXT_DIM)),
503
+ Span::styled(
504
+ format!("${:.2}", cost.cost_100k),
505
+ Style::default().fg(cost_color),
506
+ ),
507
+ ]),
508
+ Line::from(vec![
509
+ Span::styled(" 1M tokens: ", Style::default().fg(TEXT_DIM)),
510
+ Span::styled(
511
+ format!("${:.2}", cost.cost_1m),
512
+ Style::default().fg(cost_color),
513
+ ),
514
+ ]),
515
+ Line::from(""),
516
+ Line::from(vec![
517
+ Span::styled("Max tokens: ", Style::default().fg(TEXT_DIM)),
518
+ Span::styled(
519
+ format!("{}", wizard.max_tokens),
520
+ Style::default().fg(CYBER_CYAN),
521
+ ),
522
+ ]),
523
+ ]
524
+ };
525
+
526
+ let cost_paragraph = Paragraph::new(text)
527
+ .wrap(Wrap { trim: false })
528
+ .alignment(Alignment::Left);
529
+
530
+ f.render_widget(cost_paragraph, cost_inner);
531
+
532
+ // === MODEL INFO SECTION ===
533
+ let info_block = Block::default()
534
+ .title(" 📊 Model Info ")
535
+ .borders(Borders::ALL)
536
+ .border_style(Style::default().fg(CYBER_CYAN))
537
+ .style(Style::default().bg(BG_PANEL));
538
+
539
+ let info_inner = info_block.inner(chunks[1]);
540
+ f.render_widget(info_block, chunks[1]);
541
+
542
+ let info_text = vec![
543
+ Line::from(""),
544
+ Line::from(vec![
545
+ Span::styled("Context: ", Style::default().fg(TEXT_DIM)),
546
+ Span::styled(
547
+ format!("{} tokens", model_info.context_window),
548
+ Style::default().fg(NEON_GREEN),
549
+ ),
550
+ ]),
551
+ Line::from(vec![
552
+ Span::styled("Max Output: ", Style::default().fg(TEXT_DIM)),
553
+ Span::styled(
554
+ format!("{} tokens", model_info.max_output),
555
+ Style::default().fg(TEXT_PRIMARY),
556
+ ),
557
+ ]),
558
+ Line::from(vec![
559
+ Span::styled("Training: ", Style::default().fg(TEXT_DIM)),
560
+ Span::styled(model_info.training_cutoff, Style::default().fg(TEXT_MUTED)),
561
+ ]),
562
+ Line::from(""),
563
+ Line::from("Capabilities:").style(Style::default().fg(TEXT_PRIMARY).bold()),
564
+ Line::from(
565
+ model_info
566
+ .capabilities
567
+ .iter()
568
+ .map(|cap| format!(" • {}", cap))
569
+ .collect::<Vec<_>>()
570
+ .join("\n"),
571
+ )
572
+ .style(Style::default().fg(TEXT_DIM)),
573
+ ];
574
+
575
+ let info_paragraph = Paragraph::new(info_text)
576
+ .wrap(Wrap { trim: false })
577
+ .alignment(Alignment::Left);
578
+
579
+ f.render_widget(info_paragraph, info_inner);
580
+ }
581
+
582
+ fn render_navigation_footer(f: &mut Frame, area: Rect, wizard: &AgentBuilderState) {
583
+ let block = Block::default()
584
+ .title(" ⌨️ Navigation ")
585
+ .borders(Borders::ALL)
586
+ .border_style(Style::default().fg(TEXT_DIM))
587
+ .style(Style::default().bg(BG_PANEL));
588
+
589
+ let inner = block.inner(area);
590
+ f.render_widget(block, area);
591
+
592
+ let can_go_back = wizard.current_step > 1;
593
+ let is_final_step = wizard.current_step == 6;
594
+
595
+ let nav_text = if is_final_step {
596
+ Line::from(vec![
597
+ Span::styled(
598
+ "←",
599
+ Style::default()
600
+ .fg(if can_go_back { CYBER_CYAN } else { TEXT_MUTED })
601
+ .bold(),
602
+ ),
603
+ Span::styled(" Back │ ", Style::default().fg(TEXT_DIM)),
604
+ Span::styled("Tab/↑↓", Style::default().fg(CYBER_CYAN).bold()),
605
+ Span::styled(" Navigate │ ", Style::default().fg(TEXT_DIM)),
606
+ Span::styled("Enter", Style::default().fg(NEON_GREEN).bold()),
607
+ Span::styled(" Create Agent │ ", Style::default().fg(TEXT_DIM)),
608
+ Span::styled("ESC", Style::default().fg(Color::Rgb(255, 69, 69)).bold()),
609
+ Span::styled(" Cancel", Style::default().fg(TEXT_DIM)),
610
+ ])
611
+ } else {
612
+ Line::from(vec![
613
+ Span::styled(
614
+ "←",
615
+ Style::default()
616
+ .fg(if can_go_back { CYBER_CYAN } else { TEXT_MUTED })
617
+ .bold(),
618
+ ),
619
+ Span::styled(" Back │ ", Style::default().fg(TEXT_DIM)),
620
+ Span::styled("Tab/↑↓", Style::default().fg(CYBER_CYAN).bold()),
621
+ Span::styled(" Navigate │ ", Style::default().fg(TEXT_DIM)),
622
+ Span::styled("→/Enter", Style::default().fg(NEON_GREEN).bold()),
623
+ Span::styled(" Next │ ", Style::default().fg(TEXT_DIM)),
624
+ Span::styled("ESC", Style::default().fg(Color::Rgb(255, 69, 69)).bold()),
625
+ Span::styled(" Cancel", Style::default().fg(TEXT_DIM)),
626
+ ])
627
+ };
628
+
629
+ let paragraph = Paragraph::new(nav_text)
630
+ .style(Style::default().fg(TEXT_PRIMARY))
631
+ .alignment(Alignment::Center);
632
+
633
+ f.render_widget(paragraph, inner);
634
+ }
635
+
636
+ fn render_step_1_basic_info(f: &mut Frame, area: Rect, wizard: &AgentBuilderState) {
637
+ let focused = wizard.focused_field;
638
+
639
+ let text = vec![
640
+ Line::from(""),
641
+ Line::from("Step 1: Basic Information").style(
642
+ Style::default()
643
+ .fg(BRAND_PURPLE)
644
+ .add_modifier(Modifier::BOLD),
645
+ ),
646
+ Line::from(""),
647
+ Line::from(vec![
648
+ Span::styled(
649
+ if focused == 0 { " " } else { " " },
650
+ Style::default().fg(CYBER_CYAN),
651
+ ),
652
+ Span::raw("Name: "),
653
+ Span::styled(&wizard.name, Style::default().fg(NEON_GREEN)),
654
+ Span::styled(
655
+ if focused == 0 { "_" } else { "" },
656
+ Style::default().fg(CYBER_CYAN),
657
+ ),
658
+ ]),
659
+ Line::from(" (Required, max 50 characters)").style(Style::default().fg(TEXT_DIM)),
660
+ Line::from(""),
661
+ Line::from(vec![
662
+ Span::styled(
663
+ if focused == 1 { "" } else { " " },
664
+ Style::default().fg(CYBER_CYAN),
665
+ ),
666
+ Span::raw("Description: "),
667
+ Span::styled(&wizard.description, Style::default().fg(NEON_GREEN)),
668
+ Span::styled(
669
+ if focused == 1 { "_" } else { "" },
670
+ Style::default().fg(CYBER_CYAN),
671
+ ),
672
+ ]),
673
+ Line::from(" (Optional, brief description of agent purpose)")
674
+ .style(Style::default().fg(TEXT_DIM)),
675
+ Line::from(""),
676
+ Line::from(""),
677
+ Line::from("Navigation:").style(Style::default().fg(TEXT_MUTED)),
678
+ Line::from(" Tab/Shift+Tab/↑↓ - Move between fields").style(Style::default().fg(TEXT_DIM)),
679
+ Line::from(" Enter - Next step").style(Style::default().fg(TEXT_DIM)),
680
+ Line::from(" ESC - Cancel and return to Main").style(Style::default().fg(TEXT_DIM)),
681
+ ];
682
+
683
+ // Show validation errors if any
684
+ let mut all_lines = text;
685
+ if !wizard.validation_errors.is_empty() {
686
+ all_lines.push(Line::from(""));
687
+ all_lines.push(
688
+ Line::from("Validation Errors:")
689
+ .style(Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)),
690
+ );
691
+ for error in &wizard.validation_errors {
692
+ all_lines
693
+ .push(Line::from(format!(" • {}", error)).style(Style::default().fg(Color::Red)));
694
+ }
695
+ }
696
+
697
+ let paragraph = Paragraph::new(all_lines)
698
+ .wrap(Wrap { trim: false })
699
+ .alignment(Alignment::Left);
700
+
701
+ f.render_widget(paragraph, area);
702
+ }
703
+
704
+ fn render_step_2_model_selection(f: &mut Frame, area: Rect, wizard: &AgentBuilderState) {
705
+ let focused = wizard.focused_field;
706
+ let model_type = if wizard.use_local_model {
707
+ "Local"
708
+ } else {
709
+ "Remote"
710
+ };
711
+
712
+ let text = vec![
713
+ Line::from(""),
714
+ Line::from("Step 2: Model Selection").style(
715
+ Style::default()
716
+ .fg(BRAND_PURPLE)
717
+ .add_modifier(Modifier::BOLD),
718
+ ),
719
+ Line::from(""),
720
+ Line::from(vec![
721
+ Span::raw("Model Type: "),
722
+ Span::styled(model_type, Style::default().fg(CYBER_CYAN)),
723
+ ]),
724
+ Line::from(" [ ] Remote (OpenAI, Anthropic, etc.)").style(Style::default().fg(TEXT_DIM)),
725
+ Line::from(" [ ] Local (Ollama, LM Studio, etc.)").style(Style::default().fg(TEXT_DIM)),
726
+ Line::from(""),
727
+ Line::from("Remote Model:").style(
728
+ Style::default()
729
+ .fg(TEXT_PRIMARY)
730
+ .add_modifier(Modifier::BOLD),
731
+ ),
732
+ Line::from(vec![
733
+ Span::styled(
734
+ if focused == 0 { " ▶ " } else { " " },
735
+ Style::default().fg(CYBER_CYAN),
736
+ ),
737
+ Span::raw("Provider: "),
738
+ Span::styled(&wizard.provider, Style::default().fg(NEON_GREEN)),
739
+ Span::styled(
740
+ if focused == 0 { "_" } else { "" },
741
+ Style::default().fg(CYBER_CYAN),
742
+ ),
743
+ ]),
744
+ Line::from(vec![
745
+ Span::styled(
746
+ if focused == 1 { " ▶ " } else { " " },
747
+ Style::default().fg(CYBER_CYAN),
748
+ ),
749
+ Span::raw("Model: "),
750
+ Span::styled(&wizard.model, Style::default().fg(NEON_GREEN)),
751
+ Span::styled(
752
+ if focused == 1 { "_" } else { "" },
753
+ Style::default().fg(CYBER_CYAN),
754
+ ),
755
+ ]),
756
+ Line::from(""),
757
+ Line::from("Local Model:").style(
758
+ Style::default()
759
+ .fg(TEXT_PRIMARY)
760
+ .add_modifier(Modifier::BOLD),
761
+ ),
762
+ Line::from(vec![
763
+ Span::styled(
764
+ if focused == 2 { " ▶ " } else { " " },
765
+ Style::default().fg(CYBER_CYAN),
766
+ ),
767
+ Span::raw("Provider: "),
768
+ Span::styled(&wizard.local_provider, Style::default().fg(NEON_GREEN)),
769
+ Span::styled(
770
+ if focused == 2 { "_" } else { "" },
771
+ Style::default().fg(CYBER_CYAN),
772
+ ),
773
+ ]),
774
+ Line::from(vec![
775
+ Span::styled(
776
+ if focused == 3 { " ▶ " } else { " " },
777
+ Style::default().fg(CYBER_CYAN),
778
+ ),
779
+ Span::raw("Model: "),
780
+ Span::styled(&wizard.local_model, Style::default().fg(NEON_GREEN)),
781
+ Span::styled(
782
+ if focused == 3 { "_" } else { "" },
783
+ Style::default().fg(CYBER_CYAN),
784
+ ),
785
+ ]),
786
+ Line::from(vec![
787
+ Span::styled(
788
+ if focused == 4 { " ▶ " } else { " " },
789
+ Style::default().fg(CYBER_CYAN),
790
+ ),
791
+ Span::raw("URL: "),
792
+ Span::styled(&wizard.local_url, Style::default().fg(NEON_GREEN)),
793
+ Span::styled(
794
+ if focused == 4 { "_" } else { "" },
795
+ Style::default().fg(CYBER_CYAN),
796
+ ),
797
+ ]),
798
+ Line::from(""),
799
+ Line::from("Navigation:").style(Style::default().fg(TEXT_MUTED)),
800
+ Line::from(" Enter - Next step | ESC - Cancel").style(Style::default().fg(TEXT_DIM)),
801
+ ];
802
+
803
+ let paragraph = Paragraph::new(text)
804
+ .wrap(Wrap { trim: false })
805
+ .alignment(Alignment::Left);
806
+
807
+ f.render_widget(paragraph, area);
808
+ }
809
+
810
+ fn render_step_3_system_prompt(f: &mut Frame, area: Rect, wizard: &AgentBuilderState) {
811
+ let prompt_preview = if wizard.system_prompt.is_empty() {
812
+ "(type to add prompt)".to_string()
813
+ } else {
814
+ wizard.system_prompt.chars().take(200).collect::<String>()
815
+ };
816
+
817
+ let text = vec![
818
+ Line::from(""),
819
+ Line::from("Step 3: System Prompt").style(
820
+ Style::default()
821
+ .fg(BRAND_PURPLE)
822
+ .add_modifier(Modifier::BOLD),
823
+ ),
824
+ Line::from(""),
825
+ Line::from("Define the agent's behavior and personality.")
826
+ .style(Style::default().fg(TEXT_PRIMARY)),
827
+ Line::from(""),
828
+ Line::from(vec![
829
+ Span::styled("▶ ", Style::default().fg(CYBER_CYAN)),
830
+ Span::raw("Current Prompt: "),
831
+ Span::styled("_", Style::default().fg(CYBER_CYAN)),
832
+ ])
833
+ .style(
834
+ Style::default()
835
+ .fg(TEXT_PRIMARY)
836
+ .add_modifier(Modifier::BOLD),
837
+ ),
838
+ Line::from("┌────────────────────────────────────────────────┐")
839
+ .style(Style::default().fg(TEXT_MUTED)),
840
+ Line::from(format!("│ {}│", prompt_preview)).style(Style::default().fg(NEON_GREEN)),
841
+ Line::from("└────────────────────────────────────────────────┘")
842
+ .style(Style::default().fg(TEXT_MUTED)),
843
+ Line::from(""),
844
+ Line::from(format!("Characters: {}", wizard.system_prompt.len()))
845
+ .style(Style::default().fg(TEXT_DIM)),
846
+ Line::from(""),
847
+ Line::from("Examples:").style(Style::default().fg(TEXT_MUTED)),
848
+ Line::from(" • You are a helpful coding assistant").style(Style::default().fg(TEXT_DIM)),
849
+ Line::from(" You are an expert data analyst").style(Style::default().fg(TEXT_DIM)),
850
+ Line::from(" • You are a creative writing partner").style(Style::default().fg(TEXT_DIM)),
851
+ Line::from(""),
852
+ Line::from("Navigation:").style(Style::default().fg(TEXT_MUTED)),
853
+ Line::from(" Enter - Next step | ESC - Cancel").style(Style::default().fg(TEXT_DIM)),
854
+ ];
855
+
856
+ let paragraph = Paragraph::new(text)
857
+ .wrap(Wrap { trim: false })
858
+ .alignment(Alignment::Left);
859
+
860
+ f.render_widget(paragraph, area);
861
+ }
862
+
863
+ fn render_step_4_parameters(f: &mut Frame, area: Rect, wizard: &AgentBuilderState) {
864
+ let focused = wizard.focused_field;
865
+
866
+ let text = vec![
867
+ Line::from(""),
868
+ Line::from("Step 4: Parameters").style(
869
+ Style::default()
870
+ .fg(BRAND_PURPLE)
871
+ .add_modifier(Modifier::BOLD),
872
+ ),
873
+ Line::from(""),
874
+ Line::from("Fine-tune the model's behavior.").style(Style::default().fg(TEXT_PRIMARY)),
875
+ Line::from(""),
876
+ Line::from(vec![
877
+ Span::styled(
878
+ if focused == 0 { "" } else { " " },
879
+ Style::default().fg(CYBER_CYAN),
880
+ ),
881
+ Span::raw("Temperature: "),
882
+ Span::styled(
883
+ format!("{:.2}", wizard.temperature),
884
+ Style::default().fg(NEON_GREEN),
885
+ ),
886
+ Span::styled(
887
+ if focused == 0 { "_" } else { "" },
888
+ Style::default().fg(CYBER_CYAN),
889
+ ),
890
+ Span::raw(" (0.0 = focused, 2.0 = creative)"),
891
+ ])
892
+ .style(Style::default().fg(TEXT_PRIMARY)),
893
+ Line::from(""),
894
+ Line::from(vec![
895
+ Span::styled(
896
+ if focused == 1 { "▶ " } else { " " },
897
+ Style::default().fg(CYBER_CYAN),
898
+ ),
899
+ Span::raw("Max Tokens: "),
900
+ Span::styled(
901
+ format!("{}", wizard.max_tokens),
902
+ Style::default().fg(NEON_GREEN),
903
+ ),
904
+ Span::styled(
905
+ if focused == 1 { "_" } else { "" },
906
+ Style::default().fg(CYBER_CYAN),
907
+ ),
908
+ Span::raw(" (1-100000)"),
909
+ ])
910
+ .style(Style::default().fg(TEXT_PRIMARY)),
911
+ Line::from(""),
912
+ Line::from(vec![
913
+ Span::raw("Top P: "),
914
+ Span::styled(
915
+ format!("{:.2}", wizard.top_p),
916
+ Style::default().fg(TEXT_DIM),
917
+ ),
918
+ Span::raw(" (0.0-1.0, nucleus sampling)"),
919
+ ])
920
+ .style(Style::default().fg(TEXT_DIM)),
921
+ Line::from(""),
922
+ Line::from(vec![
923
+ Span::raw("Frequency Penalty: "),
924
+ Span::styled(
925
+ format!("{:.2}", wizard.frequency_penalty),
926
+ Style::default().fg(TEXT_DIM),
927
+ ),
928
+ Span::raw(" (-2.0 to 2.0)"),
929
+ ])
930
+ .style(Style::default().fg(TEXT_DIM)),
931
+ Line::from(""),
932
+ Line::from(vec![
933
+ Span::raw("Presence Penalty: "),
934
+ Span::styled(
935
+ format!("{:.2}", wizard.presence_penalty),
936
+ Style::default().fg(TEXT_DIM),
937
+ ),
938
+ Span::raw(" (-2.0 to 2.0)"),
939
+ ])
940
+ .style(Style::default().fg(TEXT_DIM)),
941
+ Line::from(""),
942
+ Line::from("Navigation:").style(Style::default().fg(TEXT_MUTED)),
943
+ Line::from(" Enter - Next step | ESC - Cancel").style(Style::default().fg(TEXT_DIM)),
944
+ ];
945
+
946
+ let paragraph = Paragraph::new(text)
947
+ .wrap(Wrap { trim: false })
948
+ .alignment(Alignment::Left);
949
+
950
+ f.render_widget(paragraph, area);
951
+ }
952
+
953
+ fn render_step_5_tools(f: &mut Frame, area: Rect, wizard: &AgentBuilderState) {
954
+ let selected_count = wizard.selected_tools.len();
955
+ let available_count = wizard.available_tools.len();
956
+ let focused = wizard.focused_field;
957
+
958
+ let mut text = vec![
959
+ Line::from(""),
960
+ Line::from("Step 5: Tools Selection").style(
961
+ Style::default()
962
+ .fg(BRAND_PURPLE)
963
+ .add_modifier(Modifier::BOLD),
964
+ ),
965
+ Line::from(""),
966
+ Line::from(format!(
967
+ "Selected: {} / {} tools",
968
+ selected_count, available_count
969
+ ))
970
+ .style(Style::default().fg(CYBER_CYAN)),
971
+ Line::from(""),
972
+ Line::from("Available Tools:").style(
973
+ Style::default()
974
+ .fg(TEXT_PRIMARY)
975
+ .add_modifier(Modifier::BOLD),
976
+ ),
977
+ ];
978
+
979
+ for (i, tool) in wizard.available_tools.iter().enumerate() {
980
+ let is_selected = wizard.selected_tools.contains(tool);
981
+ let checkbox = if is_selected { "[✓]" } else { "[ ]" };
982
+ let is_focused = i == focused;
983
+ let style = if is_selected {
984
+ Style::default().fg(NEON_GREEN)
985
+ } else if is_focused {
986
+ Style::default().fg(CYBER_CYAN)
987
+ } else {
988
+ Style::default().fg(TEXT_PRIMARY)
989
+ };
990
+ text.push(Line::from(vec![
991
+ Span::styled(
992
+ if is_focused { "▶ " } else { " " },
993
+ Style::default().fg(CYBER_CYAN),
994
+ ),
995
+ Span::styled(format!("{} ", checkbox), style),
996
+ Span::styled(tool.clone(), style),
997
+ ]));
998
+ }
999
+
1000
+ text.extend(vec![
1001
+ Line::from(""),
1002
+ Line::from("Note: Use Space to toggle, ↑↓ to navigate")
1003
+ .style(Style::default().fg(TEXT_DIM)),
1004
+ Line::from(""),
1005
+ Line::from("Navigation:").style(Style::default().fg(TEXT_MUTED)),
1006
+ Line::from(" Enter - Next step | ESC - Cancel").style(Style::default().fg(TEXT_DIM)),
1007
+ ]);
1008
+
1009
+ let paragraph = Paragraph::new(text)
1010
+ .wrap(Wrap { trim: false })
1011
+ .alignment(Alignment::Left);
1012
+
1013
+ f.render_widget(paragraph, area);
1014
+ }
1015
+
1016
+ fn render_step_6_review(f: &mut Frame, area: Rect, wizard: &AgentBuilderState) {
1017
+ let provider = if wizard.use_local_model {
1018
+ "local"
1019
+ } else {
1020
+ &wizard.provider
1021
+ };
1022
+ let model = if wizard.use_local_model {
1023
+ &wizard.local_model
1024
+ } else {
1025
+ &wizard.model
1026
+ };
1027
+
1028
+ let model_info_str = if wizard.use_local_model {
1029
+ format!("{} ({})", wizard.local_model, wizard.local_provider)
1030
+ } else {
1031
+ format!("{} ({})", wizard.model, wizard.provider)
1032
+ };
1033
+
1034
+ let cost = calculate_cost(provider, model, wizard.max_tokens);
1035
+ let model_info = get_model_info(provider, model);
1036
+
1037
+ let mut text = vec![
1038
+ Line::from(""),
1039
+ Line::from("Step 6: Review & Confirm").style(
1040
+ Style::default()
1041
+ .fg(BRAND_PURPLE)
1042
+ .add_modifier(Modifier::BOLD),
1043
+ ),
1044
+ Line::from(""),
1045
+ Line::from("✓ Configuration Complete - Ready to Create")
1046
+ .style(Style::default().fg(NEON_GREEN).bold()),
1047
+ Line::from(""),
1048
+ Line::from("═══════════════════════════════════════════════════")
1049
+ .style(Style::default().fg(TEXT_MUTED)),
1050
+ Line::from(""),
1051
+ Line::from("Basic Info:").style(Style::default().fg(CYBER_CYAN).bold()),
1052
+ Line::from(vec![
1053
+ Span::styled(" Name: ", Style::default().fg(TEXT_DIM)),
1054
+ Span::styled(
1055
+ &wizard.name,
1056
+ Style::default().fg(NEON_GREEN).add_modifier(Modifier::BOLD),
1057
+ ),
1058
+ ]),
1059
+ Line::from(vec![
1060
+ Span::styled(" Description: ", Style::default().fg(TEXT_DIM)),
1061
+ Span::styled(
1062
+ if wizard.description.is_empty() {
1063
+ "(none)"
1064
+ } else {
1065
+ wizard.description.as_str()
1066
+ },
1067
+ Style::default().fg(TEXT_PRIMARY),
1068
+ ),
1069
+ ]),
1070
+ Line::from(""),
1071
+ Line::from("Model Configuration:").style(Style::default().fg(CYBER_CYAN).bold()),
1072
+ Line::from(vec![
1073
+ Span::styled(" Model: ", Style::default().fg(TEXT_DIM)),
1074
+ Span::styled(&model_info_str, Style::default().fg(TEXT_PRIMARY).bold()),
1075
+ ]),
1076
+ Line::from(vec![
1077
+ Span::styled(" Context Window: ", Style::default().fg(TEXT_DIM)),
1078
+ Span::styled(
1079
+ format!("{} tokens", model_info.context_window),
1080
+ Style::default().fg(TEXT_PRIMARY),
1081
+ ),
1082
+ ]),
1083
+ Line::from(vec![
1084
+ Span::styled(" Temperature: ", Style::default().fg(TEXT_DIM)),
1085
+ Span::styled(
1086
+ format!("{:.2}", wizard.temperature),
1087
+ Style::default().fg(TEXT_PRIMARY),
1088
+ ),
1089
+ ]),
1090
+ Line::from(vec![
1091
+ Span::styled(" Max Tokens: ", Style::default().fg(TEXT_DIM)),
1092
+ Span::styled(
1093
+ format!("{}", wizard.max_tokens),
1094
+ Style::default().fg(TEXT_PRIMARY),
1095
+ ),
1096
+ ]),
1097
+ Line::from(""),
1098
+ Line::from("Tools:").style(Style::default().fg(CYBER_CYAN).bold()),
1099
+ Line::from(vec![
1100
+ Span::styled(" ", Style::default()),
1101
+ Span::styled(
1102
+ if wizard.selected_tools.is_empty() {
1103
+ "None selected".to_string()
1104
+ } else {
1105
+ wizard.selected_tools.join(", ")
1106
+ },
1107
+ Style::default().fg(TEXT_PRIMARY),
1108
+ ),
1109
+ ]),
1110
+ ];
1111
+
1112
+ // Add cost estimate
1113
+ if !cost.is_free {
1114
+ text.push(Line::from(""));
1115
+ text.push(Line::from("Estimated Cost:").style(Style::default().fg(AMBER_WARN).bold()));
1116
+ text.push(Line::from(vec![
1117
+ Span::styled(" Per 1K tokens: ", Style::default().fg(TEXT_DIM)),
1118
+ Span::styled(
1119
+ format!("${:.4}", cost.cost_1k),
1120
+ Style::default().fg(TEXT_PRIMARY),
1121
+ ),
1122
+ ]));
1123
+ } else {
1124
+ text.push(Line::from(""));
1125
+ text.push(Line::from(vec![
1126
+ Span::styled(" ✓ ", Style::default().fg(NEON_GREEN)),
1127
+ Span::styled("FREE (Local Model)", Style::default().fg(NEON_GREEN).bold()),
1128
+ ]));
1129
+ }
1130
+
1131
+ text.push(Line::from(""));
1132
+ text.push(
1133
+ Line::from("═══════════════════════════════════════════════════")
1134
+ .style(Style::default().fg(TEXT_MUTED)),
1135
+ );
1136
+ text.push(Line::from(""));
1137
+ text.push(Line::from(vec![
1138
+ Span::styled("Press ", Style::default().fg(TEXT_DIM)),
1139
+ Span::styled("Enter", Style::default().fg(NEON_GREEN).bold()),
1140
+ Span::styled(" to create this agent", Style::default().fg(TEXT_DIM)),
1141
+ ]));
1142
+
1143
+ let paragraph = Paragraph::new(text)
1144
+ .wrap(Wrap { trim: false })
1145
+ .alignment(Alignment::Left);
1146
+
1147
+ f.render_widget(paragraph, area);
1148
+ }