1use crate::{
4 action::{Action, QueryExplainedPayload, QueryMode, SourceRef, StatusType},
5 commands::SlashCommand,
6 handlers::{FileOperations, GraphRAGHandler},
7 query_history::{QueryEntry, QueryHistory},
8 theme::Theme,
9 tui::{Event, Tui},
10 ui::{HelpOverlay, InfoPanel, QueryInput, RawResultsViewer, ResultsViewer, StatusBar},
11 workspace::{WorkspaceManager, WorkspaceMetadata},
12};
13use chrono::Utc;
14use color_eyre::eyre::Result;
15use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
16use ratatui::layout::{Constraint, Direction, Layout};
17use std::{path::PathBuf, time::Instant};
18use tokio::sync::mpsc::{self, UnboundedReceiver, UnboundedSender};
19
20pub struct App {
22 should_quit: bool,
24 tui: Tui,
26 graphrag: GraphRAGHandler,
28 action_tx: UnboundedSender<Action>,
30 action_rx: UnboundedReceiver<Action>,
32 query_input: QueryInput,
34 results_viewer: ResultsViewer,
36 raw_results_viewer: RawResultsViewer,
38 info_panel: InfoPanel,
40 status_bar: StatusBar,
42 help_overlay: HelpOverlay,
44 query_history: QueryHistory,
46 #[allow(dead_code)]
48 workspace_manager: WorkspaceManager,
49 #[allow(dead_code)]
51 workspace_metadata: Option<WorkspaceMetadata>,
52 config_path: Option<PathBuf>,
54 query_mode: QueryMode,
56 focused_pane: u8,
58 #[allow(dead_code)]
60 theme: Theme,
61}
62
63impl App {
64 pub fn new(config_path: Option<PathBuf>, _workspace: Option<String>) -> Result<Self> {
66 let (action_tx, action_rx) = mpsc::unbounded_channel();
67
68 let workspace_manager = WorkspaceManager::new()?;
69
70 Ok(Self {
71 should_quit: false,
72 tui: Tui::new()?,
73 graphrag: GraphRAGHandler::new(),
74 action_tx,
75 action_rx,
76 query_input: QueryInput::new(),
77 results_viewer: ResultsViewer::new(),
78 raw_results_viewer: RawResultsViewer::new(),
79 info_panel: InfoPanel::new(),
80 status_bar: StatusBar::new(),
81 help_overlay: HelpOverlay::new(),
82 query_history: QueryHistory::new(),
83 workspace_manager,
84 workspace_metadata: None,
85 config_path,
86 query_mode: QueryMode::default(),
87 focused_pane: 0,
88 theme: Theme::default(),
89 })
90 }
91
92 fn set_focus(&mut self, pane: u8) {
95 self.focused_pane = pane;
96 self.query_input.set_focused(pane == 0);
97 self.results_viewer.set_focused(pane == 1);
98 self.raw_results_viewer.set_focused(pane == 2);
99 self.info_panel.set_focused(pane == 3);
100 }
101
102 pub async fn run(&mut self) -> Result<()> {
104 self.tui.enter()?;
106
107 if let Some(ref config_path) = self.config_path.clone() {
109 self.action_tx
110 .send(Action::LoadConfig(config_path.clone()))?;
111 } else {
112 self.action_tx.send(Action::SetStatus(
113 StatusType::Warning,
114 "No config loaded. Use /config <file> to load configuration".to_string(),
115 ))?;
116 }
117
118 while !self.should_quit {
120 if let Some(event) = self.tui.next().await {
122 self.handle_event(event).await?;
123 }
124
125 while let Ok(action) = self.action_rx.try_recv() {
127 self.update(action).await?;
128
129 if self.should_quit {
130 break;
131 }
132 }
133
134 let query_input = &mut self.query_input;
136 let results_viewer = &mut self.results_viewer;
137 let raw_results_viewer = &mut self.raw_results_viewer;
138 let info_panel = &mut self.info_panel;
139 let status_bar = &mut self.status_bar;
140 let help_overlay = &mut self.help_overlay;
141
142 self.tui.terminal.draw(|f| {
143 use crate::ui::components::Component;
144
145 let main_chunks = Layout::default()
147 .direction(Direction::Vertical)
148 .constraints([
149 Constraint::Length(3), Constraint::Min(0), Constraint::Length(3), ])
153 .split(f.area());
154
155 let content_chunks = Layout::default()
157 .direction(Direction::Horizontal)
158 .constraints([
159 Constraint::Percentage(70), Constraint::Percentage(30), ])
162 .split(main_chunks[1]);
163
164 let left_chunks = Layout::default()
166 .direction(Direction::Vertical)
167 .constraints([
168 Constraint::Percentage(60), Constraint::Percentage(40), ])
171 .split(content_chunks[0]);
172
173 query_input.render(f, main_chunks[0]);
175 results_viewer.render(f, left_chunks[0]);
176 raw_results_viewer.render(f, left_chunks[1]);
177 info_panel.render(f, content_chunks[1]);
178 status_bar.render(f, main_chunks[2]);
179
180 if help_overlay.is_visible() {
182 help_overlay.render(f, f.area());
183 }
184 })?;
185 }
186
187 self.tui.exit()?;
189
190 Ok(())
191 }
192
193 async fn handle_event(&mut self, event: Event) -> Result<()> {
195 match event {
196 Event::Crossterm(crossterm_event) => {
197 if let crossterm::event::Event::Key(key) = crossterm_event {
198 self.handle_key_event(key)?;
199 }
200 },
201 Event::Tick => {
202 },
204 Event::Render => {
205 },
207 Event::Resize(w, h) => {
208 self.action_tx.send(Action::Resize(w, h))?;
209 },
210 }
211
212 Ok(())
213 }
214
215 fn handle_key_event(&mut self, key: KeyEvent) -> Result<()> {
217 if self.help_overlay.is_visible() {
219 if matches!(key.code, KeyCode::Char('?') | KeyCode::Esc) {
220 self.action_tx.send(Action::ToggleHelp)?;
221 }
222 return Ok(());
223 }
224
225 match (key.code, key.modifiers) {
227 (KeyCode::Char('c'), KeyModifiers::CONTROL) => {
229 self.action_tx.send(Action::Quit)?;
230 return Ok(());
231 },
232 (KeyCode::Char('?'), _) | (KeyCode::Char('h'), KeyModifiers::CONTROL) => {
235 self.action_tx.send(Action::ToggleHelp)?;
236 return Ok(());
237 },
238 (KeyCode::Char('n'), KeyModifiers::CONTROL) => {
242 if self.focused_pane == 3 {
243 self.action_tx.send(Action::NextTab)?;
245 } else {
246 self.action_tx.send(Action::NextPane)?;
247 }
248 return Ok(());
249 },
250 (KeyCode::Char('p'), KeyModifiers::CONTROL) => {
252 self.action_tx.send(Action::PreviousPane)?;
253 return Ok(());
254 },
255 (KeyCode::Char('1'), KeyModifiers::CONTROL) => {
257 self.set_focus(0);
258 self.action_tx.send(Action::FocusQueryInput)?;
259 return Ok(());
260 },
261 (KeyCode::Char('2'), KeyModifiers::CONTROL) => {
262 self.set_focus(1);
263 self.action_tx.send(Action::FocusResultsViewer)?;
264 return Ok(());
265 },
266 (KeyCode::Char('3'), KeyModifiers::CONTROL) => {
267 self.set_focus(2);
268 self.action_tx.send(Action::FocusRawResultsViewer)?;
269 return Ok(());
270 },
271 (KeyCode::Char('4'), KeyModifiers::CONTROL) => {
272 self.set_focus(3);
273 self.action_tx.send(Action::FocusInfoPanel)?;
274 return Ok(());
275 },
276 (KeyCode::Esc, KeyModifiers::NONE) => {
278 self.set_focus(0);
279 self.action_tx.send(Action::FocusQueryInput)?;
280 return Ok(());
281 },
282 (KeyCode::Down, KeyModifiers::NONE) if self.focused_pane != 0 => {
284 self.action_tx.send(Action::ScrollDown)?;
285 return Ok(());
286 },
287 (KeyCode::Up, KeyModifiers::NONE) if self.focused_pane != 0 => {
288 self.action_tx.send(Action::ScrollUp)?;
289 return Ok(());
290 },
291 (KeyCode::Down, KeyModifiers::ALT) => {
293 self.action_tx.send(Action::ScrollDown)?;
294 return Ok(());
295 },
296 (KeyCode::Up, KeyModifiers::ALT) => {
297 self.action_tx.send(Action::ScrollUp)?;
298 return Ok(());
299 },
300 _ => {},
301 }
302
303 if let Some(action) = self.query_input.handle_key(key) {
305 self.action_tx.send(action)?;
306 return Ok(());
307 }
308
309 match (key.code, key.modifiers) {
311 (KeyCode::Char('j'), KeyModifiers::NONE) => {
313 self.action_tx.send(Action::ScrollDown)?;
314 },
315 (KeyCode::Char('k'), KeyModifiers::NONE) => {
316 self.action_tx.send(Action::ScrollUp)?;
317 },
318 (KeyCode::PageDown, KeyModifiers::NONE)
320 | (KeyCode::Char('d'), KeyModifiers::CONTROL) => {
321 self.action_tx.send(Action::ScrollPageDown)?;
322 },
323 (KeyCode::PageUp, KeyModifiers::NONE) | (KeyCode::Char('u'), KeyModifiers::CONTROL) => {
324 self.action_tx.send(Action::ScrollPageUp)?;
325 },
326 (KeyCode::Home, KeyModifiers::NONE) => {
328 self.action_tx.send(Action::ScrollToTop)?;
329 },
330 (KeyCode::End, KeyModifiers::NONE) => {
331 self.action_tx.send(Action::ScrollToBottom)?;
332 },
333 _ => {},
334 }
335
336 Ok(())
337 }
338
339 async fn update(&mut self, action: Action) -> Result<()> {
341 use crate::ui::components::Component;
342
343 self.query_input.handle_action(&action);
345 self.results_viewer.handle_action(&action);
346 self.raw_results_viewer.handle_action(&action);
347 self.info_panel.handle_action(&action);
348 self.status_bar.handle_action(&action);
349 self.help_overlay.handle_action(&action);
350
351 match action {
353 Action::Quit => {
354 self.should_quit = true;
355 },
356 Action::LoadConfig(path) => {
357 self.handle_load_config(path).await?;
358 },
359 Action::ExecuteQuery(query) => {
360 match self.query_mode {
362 QueryMode::Ask => self.handle_execute_query(query).await?,
363 QueryMode::Explain => self.handle_execute_explained_query(query).await?,
364 QueryMode::Reason => self.handle_execute_reason_query(query).await?,
365 }
366 },
367 Action::ExecuteExplainedQuery(query) => {
368 self.handle_execute_explained_query(query).await?;
369 },
370 Action::ExecuteReasonQuery(query) => {
371 self.handle_execute_reason_query(query).await?;
372 },
373 Action::SetQueryMode(mode) => {
374 self.query_mode = mode;
375 },
377 Action::NextPane => {
378 let next = (self.focused_pane + 1) % 4;
379 self.set_focus(next);
380 },
381 Action::PreviousPane => {
382 let prev = (self.focused_pane + 3) % 4;
383 self.set_focus(prev);
384 },
385 Action::ExecuteSlashCommand(cmd) => {
386 self.handle_slash_command(cmd).await?;
387 },
388 _ => {},
390 }
391
392 Ok(())
393 }
394
395 async fn handle_load_config(&mut self, path: PathBuf) -> Result<()> {
397 self.action_tx.send(Action::StartProgress(
398 "Loading configuration...".to_string(),
399 ))?;
400
401 match crate::config::load_config(&path).await {
402 Ok(config) => {
403 self.graphrag.initialize(config).await?;
404 self.config_path = Some(path.clone());
405
406 self.action_tx.send(Action::StopProgress)?;
407 self.action_tx.send(Action::ConfigLoaded(format!(
408 "Configuration loaded from {}",
409 path.display()
410 )))?;
411 self.action_tx.send(Action::SetStatus(
412 StatusType::Success,
413 "Configuration loaded successfully".to_string(),
414 ))?;
415
416 self.update_stats().await;
418 },
419 Err(e) => {
420 self.action_tx.send(Action::StopProgress)?;
421 self.action_tx.send(Action::ConfigLoadError(format!(
422 "Failed to load config: {}",
423 e
424 )))?;
425 self.action_tx.send(Action::SetStatus(
426 StatusType::Error,
427 format!("Config load failed: {}", e),
428 ))?;
429 },
430 }
431
432 self.set_focus(0);
433 Ok(())
434 }
435
436 async fn handle_execute_query(&mut self, query: String) -> Result<()> {
438 if !self.graphrag.is_initialized().await {
439 self.action_tx.send(Action::SetStatus(
440 StatusType::Error,
441 "GraphRAG not initialized. Load a config first with /config".to_string(),
442 ))?;
443 return Ok(());
444 }
445
446 self.results_viewer.set_content(vec![
448 "🤖 Generating Answer...".to_string(),
449 "━".repeat(50),
450 String::new(),
451 format!("Query: {}", query),
452 String::new(),
453 "⟳ Searching knowledge graph...".to_string(),
454 "⟳ Processing results with LLM...".to_string(),
455 String::new(),
456 "Please wait...".to_string(),
457 ]);
458
459 self.action_tx
460 .send(Action::StartProgress(format!("Executing query: {}", query)))?;
461
462 let start = Instant::now();
463
464 match self.graphrag.query_with_raw(&query).await {
465 Ok((answer, raw_results)) => {
466 let duration = start.elapsed();
467
468 let entry = QueryEntry {
470 query: query.clone(),
471 timestamp: Utc::now(),
472 duration_ms: duration.as_millis(),
473 results_count: raw_results.len(),
474 results_preview: vec![answer[..answer.len().min(200)].to_string()],
475 };
476
477 self.query_history.add_entry(entry.clone());
478
479 self.info_panel
481 .add_query(entry.query, entry.duration_ms, entry.results_count);
482
483 let mut raw_display = vec![
485 format!("🔍 Raw Search Results ({})", raw_results.len()),
486 "━".repeat(50),
487 String::new(),
488 ];
489 raw_display.extend(raw_results);
490 self.raw_results_viewer.set_content(raw_display);
491
492 self.action_tx.send(Action::StopProgress)?;
493 self.action_tx.send(Action::QuerySuccess(answer))?;
494 self.action_tx.send(Action::SetStatus(
495 StatusType::Success,
496 format!("Query completed in {}ms", duration.as_millis()),
497 ))?;
498 },
499 Err(e) => {
500 self.action_tx.send(Action::StopProgress)?;
501 self.action_tx
502 .send(Action::QueryError(format!("Query failed: {}", e)))?;
503 self.action_tx.send(Action::SetStatus(
504 StatusType::Error,
505 format!("Query error: {}", e),
506 ))?;
507 },
508 }
509
510 Ok(())
511 }
512
513 async fn handle_execute_explained_query(&mut self, query: String) -> Result<()> {
515 if !self.graphrag.is_initialized().await {
516 self.action_tx.send(Action::SetStatus(
517 StatusType::Error,
518 "GraphRAG not initialized. Load a config first with /config".to_string(),
519 ))?;
520 return Ok(());
521 }
522
523 self.results_viewer.set_content(vec![
524 "## Generating Answer (EXPLAIN mode)…".to_string(),
525 String::new(),
526 format!("**Query:** {}", query),
527 String::new(),
528 "- Searching knowledge graph…".to_string(),
529 "- Computing confidence and source references…".to_string(),
530 ]);
531
532 self.action_tx
533 .send(Action::StartProgress(format!("EXPLAIN query: {}", query)))?;
534
535 let start = Instant::now();
536
537 match self.graphrag.query_explained(&query).await {
538 Ok(result) => {
539 let duration = start.elapsed();
540
541 let entry = QueryEntry {
542 query: query.clone(),
543 timestamp: Utc::now(),
544 duration_ms: duration.as_millis(),
545 results_count: result.sources.len(),
546 results_preview: vec![result.answer[..result.answer.len().min(200)].to_string()],
547 };
548 self.query_history.add_entry(entry.clone());
549 self.info_panel
550 .add_query(entry.query, entry.duration_ms, entry.results_count);
551
552 let mut raw_display = vec![
554 format!(
555 "Sources ({}) | Confidence: {:.0}%",
556 result.sources.len(),
557 result.confidence * 100.0
558 ),
559 "━".repeat(50),
560 String::new(),
561 ];
562 for (i, src) in result.sources.iter().enumerate() {
563 raw_display.push(format!(
564 "{}. [score: {:.2}] {}",
565 i + 1,
566 src.relevance_score,
567 src.id
568 ));
569 let excerpt = &src.excerpt[..src.excerpt.len().min(120)];
570 raw_display.push(format!(" {}", excerpt));
571 raw_display.push(String::new());
572 }
573 self.raw_results_viewer.set_content(raw_display);
574
575 let payload = QueryExplainedPayload {
577 answer: result.answer.clone(),
578 confidence: result.confidence,
579 sources: result
580 .sources
581 .iter()
582 .map(|s| SourceRef {
583 id: s.id.clone(),
584 excerpt: s.excerpt.clone(),
585 relevance_score: s.relevance_score,
586 })
587 .collect(),
588 };
589
590 self.action_tx.send(Action::StopProgress)?;
591 self.action_tx
592 .send(Action::QueryExplainedSuccess(Box::new(payload)))?;
593 self.action_tx.send(Action::SetStatus(
594 StatusType::Success,
595 format!(
596 "EXPLAIN query done in {}ms | confidence: {:.0}%",
597 duration.as_millis(),
598 result.confidence * 100.0
599 ),
600 ))?;
601 },
602 Err(e) => {
603 self.action_tx.send(Action::StopProgress)?;
604 self.action_tx
605 .send(Action::QueryError(format!("Query failed: {}", e)))?;
606 self.action_tx.send(Action::SetStatus(
607 StatusType::Error,
608 format!("Query error: {}", e),
609 ))?;
610 },
611 }
612
613 self.set_focus(0);
614 Ok(())
615 }
616
617 async fn handle_execute_reason_query(&mut self, query: String) -> Result<()> {
619 if !self.graphrag.is_initialized().await {
620 self.action_tx.send(Action::SetStatus(
621 StatusType::Error,
622 "GraphRAG not initialized. Load a config first with /config".to_string(),
623 ))?;
624 return Ok(());
625 }
626
627 self.results_viewer.set_content(vec![
628 "## Generating Answer (REASON mode)…".to_string(),
629 String::new(),
630 format!("**Query:** {}", query),
631 String::new(),
632 "- Decomposing query into sub-questions…".to_string(),
633 "- Gathering context for each sub-question…".to_string(),
634 "- Synthesizing comprehensive answer…".to_string(),
635 ]);
636
637 self.action_tx
638 .send(Action::StartProgress(format!("REASON query: {}", query)))?;
639
640 let start = Instant::now();
641
642 match self.graphrag.query_with_reasoning(&query).await {
643 Ok(answer) => {
644 let duration = start.elapsed();
645
646 let entry = QueryEntry {
647 query: query.clone(),
648 timestamp: Utc::now(),
649 duration_ms: duration.as_millis(),
650 results_count: 0,
651 results_preview: vec![answer[..answer.len().min(200)].to_string()],
652 };
653 self.query_history.add_entry(entry.clone());
654 self.info_panel
655 .add_query(entry.query, entry.duration_ms, entry.results_count);
656
657 self.raw_results_viewer.set_content(vec![
658 "REASON mode — query decomposition active".to_string(),
659 "━".repeat(50),
660 String::new(),
661 "Sub-queries were generated and answered individually.".to_string(),
662 "The result above synthesizes all sub-answers.".to_string(),
663 ]);
664
665 self.action_tx.send(Action::StopProgress)?;
666 self.action_tx.send(Action::QuerySuccess(answer))?;
667 self.action_tx.send(Action::SetStatus(
668 StatusType::Success,
669 format!("REASON query done in {}ms", duration.as_millis()),
670 ))?;
671 },
672 Err(e) => {
673 self.action_tx.send(Action::StopProgress)?;
674 self.action_tx
675 .send(Action::QueryError(format!("Query failed: {}", e)))?;
676 self.action_tx.send(Action::SetStatus(
677 StatusType::Error,
678 format!("Query error: {}", e),
679 ))?;
680 },
681 }
682
683 self.set_focus(0);
684 Ok(())
685 }
686
687 async fn handle_slash_command(&mut self, cmd_str: String) -> Result<()> {
689 match SlashCommand::parse(&cmd_str) {
690 Ok(cmd) => match cmd {
691 SlashCommand::Config(path) => {
692 let expanded = FileOperations::expand_tilde(&path);
693 self.action_tx.send(Action::LoadConfig(expanded))?;
694 },
695 SlashCommand::Load(path, rebuild) => {
696 self.handle_load_document_with_rebuild(path, rebuild)
697 .await?;
698 },
699 SlashCommand::Clear => {
700 self.handle_clear_graph().await?;
701 },
702 SlashCommand::Rebuild => {
703 self.handle_rebuild_graph().await?;
704 },
705 SlashCommand::Stats => {
706 self.handle_show_stats().await?;
707 },
708 SlashCommand::Entities(filter) => {
709 self.handle_list_entities(filter).await?;
710 },
711 SlashCommand::Workspace(name) => {
712 self.handle_load_workspace(name).await?;
713 },
714 SlashCommand::WorkspaceList => {
715 self.handle_list_workspaces().await?;
716 },
717 SlashCommand::WorkspaceSave(name) => {
718 self.handle_save_workspace(name).await?;
719 },
720 SlashCommand::WorkspaceDelete(name) => {
721 self.handle_delete_workspace(name).await?;
722 },
723 SlashCommand::Reason(query) => {
724 self.handle_execute_reason_query(query).await?;
725 },
726 SlashCommand::Mode(mode_str) => {
727 let mode = match mode_str.as_str() {
728 "ask" => QueryMode::Ask,
729 "explain" => QueryMode::Explain,
730 "reason" => QueryMode::Reason,
731 other => {
732 self.action_tx.send(Action::SetStatus(
733 StatusType::Error,
734 format!("Unknown mode '{}'. Use: ask | explain | reason", other),
735 ))?;
736 return Ok(());
737 },
738 };
739 self.query_mode = mode;
740 self.action_tx.send(Action::SetQueryMode(mode))?;
741 self.action_tx.send(Action::SetStatus(
742 StatusType::Success,
743 format!("Query mode set to: {}", mode.label()),
744 ))?;
745 },
746 SlashCommand::ConfigShow => {
747 self.handle_config_show().await?;
748 },
749 SlashCommand::Export(path) => {
750 self.handle_export(path).await?;
751 },
752 SlashCommand::Help => {
753 let help_text = SlashCommand::help_text();
754 self.results_viewer
755 .set_content(help_text.lines().map(|s| s.to_string()).collect());
756 },
757 },
758 Err(e) => {
759 self.action_tx.send(Action::SetStatus(
760 StatusType::Error,
761 format!("Command error: {}", e),
762 ))?;
763 },
764 }
765
766 self.set_focus(0);
768
769 Ok(())
770 }
771
772 async fn handle_load_document_with_rebuild(
774 &mut self,
775 path: PathBuf,
776 rebuild: bool,
777 ) -> Result<()> {
778 if !self.graphrag.is_initialized().await {
779 self.action_tx.send(Action::SetStatus(
780 StatusType::Error,
781 "GraphRAG not initialized. Load a config first".to_string(),
782 ))?;
783 return Ok(());
784 }
785
786 let expanded = FileOperations::expand_tilde(&path);
787
788 let progress_msg = if rebuild {
789 format!("Loading document (rebuild): {}", expanded.display())
790 } else {
791 format!("Loading document: {}", expanded.display())
792 };
793
794 self.action_tx.send(Action::StartProgress(progress_msg))?;
795
796 match self
797 .graphrag
798 .load_document_with_options(&expanded, rebuild)
799 .await
800 {
801 Ok(message) => {
802 self.action_tx.send(Action::StopProgress)?;
803 self.action_tx
804 .send(Action::DocumentLoaded(message.clone()))?;
805 self.action_tx
806 .send(Action::SetStatus(StatusType::Success, message))?;
807
808 self.update_stats().await;
809 },
810 Err(e) => {
811 self.action_tx.send(Action::StopProgress)?;
812 self.action_tx.send(Action::SetStatus(
813 StatusType::Error,
814 format!("Failed to load document: {}", e),
815 ))?;
816 },
817 }
818
819 Ok(())
820 }
821
822 async fn handle_clear_graph(&mut self) -> Result<()> {
824 if !self.graphrag.is_initialized().await {
825 self.action_tx.send(Action::SetStatus(
826 StatusType::Error,
827 "GraphRAG not initialized. Load a config first".to_string(),
828 ))?;
829 return Ok(());
830 }
831
832 self.action_tx.send(Action::StartProgress(
833 "Clearing knowledge graph...".to_string(),
834 ))?;
835
836 match self.graphrag.clear_graph().await {
837 Ok(message) => {
838 self.action_tx.send(Action::StopProgress)?;
839 self.action_tx
840 .send(Action::SetStatus(StatusType::Success, message.clone()))?;
841
842 self.results_viewer.set_content(vec![
844 "✓ Knowledge Graph Cleared".to_string(),
845 "━".repeat(50),
846 String::new(),
847 message,
848 String::new(),
849 "The knowledge graph has been cleared.".to_string(),
850 "All entities and relationships have been removed.".to_string(),
851 "Documents and chunks are preserved.".to_string(),
852 String::new(),
853 "Use /rebuild to rebuild from loaded documents.".to_string(),
854 "Or use /load <file> to load a new document.".to_string(),
855 ]);
856
857 self.update_stats().await;
858 },
859 Err(e) => {
860 self.action_tx.send(Action::StopProgress)?;
861 self.action_tx.send(Action::SetStatus(
862 StatusType::Error,
863 format!("Failed to clear graph: {}", e),
864 ))?;
865 },
866 }
867
868 Ok(())
869 }
870
871 async fn handle_rebuild_graph(&mut self) -> Result<()> {
873 if !self.graphrag.is_initialized().await {
874 self.action_tx.send(Action::SetStatus(
875 StatusType::Error,
876 "GraphRAG not initialized. Load a config first".to_string(),
877 ))?;
878 return Ok(());
879 }
880
881 self.action_tx.send(Action::StartProgress(
882 "Rebuilding knowledge graph...".to_string(),
883 ))?;
884
885 match self.graphrag.rebuild_graph().await {
886 Ok(message) => {
887 self.action_tx.send(Action::StopProgress)?;
888 self.action_tx
889 .send(Action::SetStatus(StatusType::Success, message.clone()))?;
890
891 self.results_viewer.set_content(vec![
893 "✓ Knowledge Graph Rebuilt".to_string(),
894 "━".repeat(50),
895 String::new(),
896 message,
897 String::new(),
898 "The knowledge graph has been rebuilt from loaded documents.".to_string(),
899 "All entities and relationships have been re-extracted.".to_string(),
900 String::new(),
901 "You can now query the updated graph.".to_string(),
902 ]);
903
904 self.update_stats().await;
905 },
906 Err(e) => {
907 self.action_tx.send(Action::StopProgress)?;
908 self.action_tx.send(Action::SetStatus(
909 StatusType::Error,
910 format!("Failed to rebuild graph: {}", e),
911 ))?;
912 },
913 }
914
915 Ok(())
916 }
917
918 async fn handle_show_stats(&mut self) -> Result<()> {
920 if let Some(stats) = self.graphrag.get_stats().await {
921 let lines = vec![
922 "📊 Knowledge Graph Statistics".to_string(),
923 "━".repeat(50),
924 format!("Entities: {}", stats.entities),
925 format!("Relationships: {}", stats.relationships),
926 format!("Documents: {}", stats.documents),
927 format!("Chunks: {}", stats.chunks),
928 String::new(),
929 format!("Total Queries: {}", self.query_history.total_queries()),
930 ];
931
932 self.results_viewer.set_content(lines);
933 self.action_tx.send(Action::SetStatus(
934 StatusType::Info,
935 "Stats displayed".to_string(),
936 ))?;
937 } else {
938 self.action_tx.send(Action::SetStatus(
939 StatusType::Warning,
940 "No graph loaded yet".to_string(),
941 ))?;
942 }
943
944 Ok(())
945 }
946
947 async fn handle_list_entities(&mut self, filter: Option<String>) -> Result<()> {
949 match self.graphrag.get_entities(filter.as_deref()).await {
950 Ok(entities) => {
951 let mut lines = vec![
952 format!(
953 "🔍 Entities{}",
954 if filter.is_some() {
955 format!(" (filtered by '{}')", filter.unwrap())
956 } else {
957 String::new()
958 }
959 ),
960 "━".repeat(50),
961 ];
962
963 if entities.is_empty() {
964 lines.push("No entities found.".to_string());
965 } else {
966 for (i, entity) in entities.iter().take(50).enumerate() {
967 lines.push(format!(
968 "{}. {} ({})",
969 i + 1,
970 entity.name,
971 entity.entity_type
972 ));
973 }
974
975 if entities.len() > 50 {
976 lines.push(String::new());
977 lines.push(format!("... and {} more entities", entities.len() - 50));
978 }
979 }
980
981 self.results_viewer.set_content(lines);
982 self.action_tx.send(Action::SetStatus(
983 StatusType::Info,
984 format!("Found {} entities", entities.len()),
985 ))?;
986 },
987 Err(e) => {
988 self.action_tx.send(Action::SetStatus(
989 StatusType::Error,
990 format!("Failed to list entities: {}", e),
991 ))?;
992 },
993 }
994
995 Ok(())
996 }
997
998 async fn handle_load_workspace(&mut self, name: String) -> Result<()> {
1000 if !self.graphrag.is_initialized().await {
1001 self.action_tx.send(Action::SetStatus(
1002 StatusType::Error,
1003 "GraphRAG not initialized. Load a config first".to_string(),
1004 ))?;
1005 return Ok(());
1006 }
1007
1008 self.action_tx.send(Action::StartProgress(format!(
1009 "Loading workspace '{}'...",
1010 name
1011 )))?;
1012
1013 let workspace_dir = dirs::data_dir()
1015 .map(|p| p.join("graphrag").join("workspaces"))
1016 .unwrap_or_else(|| std::path::PathBuf::from("./workspaces"));
1017
1018 match self
1019 .graphrag
1020 .load_workspace(workspace_dir.to_str().unwrap(), &name)
1021 .await
1022 {
1023 Ok(message) => {
1024 self.action_tx.send(Action::StopProgress)?;
1025
1026 self.results_viewer.set_content(vec![
1028 "✓ Workspace Loaded".to_string(),
1029 "━".repeat(50),
1030 String::new(),
1031 message,
1032 String::new(),
1033 "The workspace has been loaded successfully.".to_string(),
1034 "You can now query the loaded graph.".to_string(),
1035 ]);
1036
1037 self.action_tx.send(Action::SetStatus(
1038 StatusType::Success,
1039 format!("Workspace '{}' loaded", name),
1040 ))?;
1041
1042 self.update_stats().await;
1043 },
1044 Err(e) => {
1045 self.action_tx.send(Action::StopProgress)?;
1046 self.action_tx.send(Action::SetStatus(
1047 StatusType::Error,
1048 format!("Failed to load workspace: {}", e),
1049 ))?;
1050 },
1051 }
1052
1053 Ok(())
1054 }
1055
1056 async fn handle_list_workspaces(&mut self) -> Result<()> {
1058 let workspace_dir = dirs::data_dir()
1059 .map(|p| p.join("graphrag").join("workspaces"))
1060 .unwrap_or_else(|| std::path::PathBuf::from("./workspaces"));
1061
1062 match self
1063 .graphrag
1064 .list_workspaces(workspace_dir.to_str().unwrap())
1065 .await
1066 {
1067 Ok(list_output) => {
1068 self.results_viewer
1069 .set_content(list_output.lines().map(|s| s.to_string()).collect());
1070 self.action_tx.send(Action::SetStatus(
1071 StatusType::Info,
1072 "Workspace list displayed".to_string(),
1073 ))?;
1074 },
1075 Err(e) => {
1076 self.action_tx.send(Action::SetStatus(
1077 StatusType::Error,
1078 format!("Failed to list workspaces: {}", e),
1079 ))?;
1080 },
1081 }
1082
1083 Ok(())
1084 }
1085
1086 async fn handle_save_workspace(&mut self, name: String) -> Result<()> {
1088 if !self.graphrag.is_initialized().await {
1089 self.action_tx.send(Action::SetStatus(
1090 StatusType::Error,
1091 "GraphRAG not initialized. Load a config first".to_string(),
1092 ))?;
1093 return Ok(());
1094 }
1095
1096 self.action_tx.send(Action::StartProgress(format!(
1097 "Saving workspace '{}'...",
1098 name
1099 )))?;
1100
1101 let workspace_dir = dirs::data_dir()
1102 .map(|p| p.join("graphrag").join("workspaces"))
1103 .unwrap_or_else(|| std::path::PathBuf::from("./workspaces"));
1104
1105 match self
1106 .graphrag
1107 .save_workspace(workspace_dir.to_str().unwrap(), &name)
1108 .await
1109 {
1110 Ok(message) => {
1111 self.action_tx.send(Action::StopProgress)?;
1112
1113 self.results_viewer.set_content(vec![
1115 "✓ Workspace Saved".to_string(),
1116 "━".repeat(50),
1117 String::new(),
1118 message,
1119 String::new(),
1120 format!("Workspace location: {}", workspace_dir.display()),
1121 String::new(),
1122 "You can load this workspace later with:".to_string(),
1123 format!(" /workspace {}", name),
1124 ]);
1125
1126 self.action_tx.send(Action::SetStatus(
1127 StatusType::Success,
1128 format!("Workspace '{}' saved", name),
1129 ))?;
1130 },
1131 Err(e) => {
1132 self.action_tx.send(Action::StopProgress)?;
1133 self.action_tx.send(Action::SetStatus(
1134 StatusType::Error,
1135 format!("Failed to save workspace: {}", e),
1136 ))?;
1137 },
1138 }
1139
1140 Ok(())
1141 }
1142
1143 async fn handle_delete_workspace(&mut self, name: String) -> Result<()> {
1145 self.action_tx.send(Action::StartProgress(format!(
1146 "Deleting workspace '{}'...",
1147 name
1148 )))?;
1149
1150 let workspace_dir = dirs::data_dir()
1151 .map(|p| p.join("graphrag").join("workspaces"))
1152 .unwrap_or_else(|| std::path::PathBuf::from("./workspaces"));
1153
1154 match self
1155 .graphrag
1156 .delete_workspace(workspace_dir.to_str().unwrap(), &name)
1157 .await
1158 {
1159 Ok(message) => {
1160 self.action_tx.send(Action::StopProgress)?;
1161
1162 self.results_viewer.set_content(vec![
1164 "✓ Workspace Deleted".to_string(),
1165 "━".repeat(50),
1166 String::new(),
1167 message,
1168 String::new(),
1169 "The workspace has been permanently deleted.".to_string(),
1170 ]);
1171
1172 self.action_tx.send(Action::SetStatus(
1173 StatusType::Success,
1174 format!("Workspace '{}' deleted", name),
1175 ))?;
1176 },
1177 Err(e) => {
1178 self.action_tx.send(Action::StopProgress)?;
1179 self.action_tx.send(Action::SetStatus(
1180 StatusType::Error,
1181 format!("Failed to delete workspace: {}", e),
1182 ))?;
1183 },
1184 }
1185
1186 Ok(())
1187 }
1188
1189 async fn handle_config_show(&mut self) -> Result<()> {
1191 if let Some(ref path) = self.config_path.clone() {
1192 match tokio::fs::read_to_string(path).await {
1193 Ok(content) => {
1194 let mut lines = vec![
1195 format!("# Config: {}", path.display()),
1196 String::new(),
1197 "```".to_string(),
1198 ];
1199 lines.extend(content.lines().map(|l| l.to_string()));
1200 lines.push("```".to_string());
1201 self.results_viewer.set_content(lines);
1202 self.action_tx.send(Action::SetStatus(
1203 StatusType::Info,
1204 format!("Showing config: {}", path.display()),
1205 ))?;
1206 },
1207 Err(e) => {
1208 self.action_tx.send(Action::SetStatus(
1209 StatusType::Error,
1210 format!("Cannot read config file: {}", e),
1211 ))?;
1212 },
1213 }
1214 } else {
1215 self.action_tx.send(Action::SetStatus(
1216 StatusType::Warning,
1217 "No config loaded. Use /config <file> first.".to_string(),
1218 ))?;
1219 }
1220 Ok(())
1221 }
1222
1223 async fn handle_export(&mut self, path: PathBuf) -> Result<()> {
1225 let entries = self.query_history.last_n(1000);
1226 if entries.is_empty() {
1227 self.action_tx.send(Action::SetStatus(
1228 StatusType::Warning,
1229 "No query history to export.".to_string(),
1230 ))?;
1231 return Ok(());
1232 }
1233
1234 let mut md = String::from("# GraphRAG Query History\n\n");
1235 for (i, entry) in entries.iter().enumerate() {
1236 md.push_str(&format!("## Query {}\n\n", i + 1));
1237 md.push_str(&format!("**Q:** {}\n\n", entry.query));
1238 if !entry.results_preview.is_empty() {
1239 md.push_str(&format!("**A:** {}\n\n", entry.results_preview[0]));
1240 }
1241 md.push_str(&format!(
1242 "*{}ms · {} sources*\n\n---\n\n",
1243 entry.duration_ms, entry.results_count
1244 ));
1245 }
1246
1247 let expanded = FileOperations::expand_tilde(&path);
1248 match tokio::fs::write(&expanded, md.as_bytes()).await {
1249 Ok(()) => {
1250 let msg = format!(
1251 "Exported {} queries to {}",
1252 entries.len(),
1253 expanded.display()
1254 );
1255 self.results_viewer.set_content(vec![
1256 "## Export Complete".to_string(),
1257 String::new(),
1258 msg.clone(),
1259 ]);
1260 self.action_tx
1261 .send(Action::SetStatus(StatusType::Success, msg))?;
1262 },
1263 Err(e) => {
1264 self.action_tx.send(Action::SetStatus(
1265 StatusType::Error,
1266 format!("Export failed: {}", e),
1267 ))?;
1268 },
1269 }
1270 Ok(())
1271 }
1272
1273 async fn update_stats(&mut self) {
1275 if let Some(stats) = self.graphrag.get_stats().await {
1276 self.info_panel.set_stats(stats);
1277 }
1278 }
1279}