1use std::sync::Arc;
2
3use chrono::{DateTime, Utc};
4
5use crate::config::settings::Settings;
6use crate::store::db::Database;
7use crate::store::entities::{EntitySearchResult, EntityType};
8use crate::store::memory::{GraphData, Memory, MemoryStats, ProjectSummary, SearchQuery, Session};
9
10#[derive(Debug, Clone, Copy, PartialEq)]
12pub enum DetailTab {
13 Content,
14 Structured,
15 Entities,
16 Temporal,
17 Relations,
18}
19impl DetailTab {
20 pub fn label(&self) -> &'static str {
21 match self {
22 Self::Content => "Content",
23 Self::Structured => "Fields",
24 Self::Entities => "Entities",
25 Self::Temporal => "Temporal",
26 Self::Relations => "Graph",
27 }
28 }
29 pub fn next(&self) -> Self {
30 match self {
31 Self::Content => Self::Structured,
32 Self::Structured => Self::Entities,
33 Self::Entities => Self::Temporal,
34 Self::Temporal => Self::Relations,
35 Self::Relations => Self::Content,
36 }
37 }
38 pub fn prev(&self) -> Self {
39 match self {
40 Self::Content => Self::Relations,
41 Self::Structured => Self::Content,
42 Self::Entities => Self::Structured,
43 Self::Temporal => Self::Entities,
44 Self::Relations => Self::Temporal,
45 }
46 }
47}
48
49pub struct App {
51 pub project: String,
52 pub memories: Vec<Memory>,
53 pub sessions: Vec<Session>,
54 pub selected: usize,
55 pub scroll: usize,
56 pub search: String,
57 pub status_msg: Option<String>,
58 pub quit: bool,
59 pub show_help: bool,
60 pub detail_tab: DetailTab,
61 pub detail_scroll: usize,
62 pub stats: Option<MemoryStats>,
63 pub graph: Option<GraphData>,
64 pub graph_sel: usize,
65 pub entity_data: Option<Vec<(String, EntityType, u32)>>,
66 pub temporal_data: Option<(Vec<Memory>, u8)>,
67 pub active_panel: usize, pub total_mems: u32,
69 pub show_projects: bool,
70 pub projects_list: Vec<crate::store::memory::ProjectSummary>,
71 pub db: Arc<Database>,
72}
73
74impl App {
75 pub fn new(db: Arc<Database>, _settings: Arc<Settings>) -> Self {
76 let project = Settings::infer_project();
78 Self {
79 project,
80 memories: Vec::new(),
81 sessions: Vec::new(),
82 selected: 0,
83 scroll: 0,
84 search: String::new(),
85 status_msg: None,
86 quit: false,
87 show_help: false,
88 detail_tab: DetailTab::Content,
89 detail_scroll: 0,
90 stats: None,
91 graph: None,
92 graph_sel: 0,
93 entity_data: None,
94 temporal_data: None,
95 active_panel: 0,
96 total_mems: 0,
97 show_projects: false,
98 projects_list: vec![],
99 db,
100 }
101 }
102
103 pub fn load(&mut self) {
105 let s = self.db.memories();
106 if self.search.is_empty() {
107 let mut all = Vec::new();
109 if let Ok(projects) = s.list_projects() {
110 for p in &projects {
111 if let Ok(pmems) = s.list(&p.name, None, None, None, 100, 0) {
112 all.extend(pmems);
113 }
114 }
115 }
116 all.sort_by(|a, b| b.created_at.cmp(&a.created_at));
117 all.truncate(500);
118 self.memories = all;
119 } else {
120 let q = SearchQuery {
121 text: self.search.clone(),
122 project: None,
123 scope: None,
124 memory_type: None,
125 importance: None,
126 tags: vec![],
127 limit: 200,
128 include_snippet: false,
129 all_projects: true,
130 };
131 self.memories = s
132 .search(&q, &crate::store::search::SearchWeights::default(), None)
133 .unwrap_or_default()
134 .into_iter()
135 .map(|r| r.memory)
136 .collect();
137 }
138 self.selected = self.selected.min(self.memories.len().saturating_sub(1));
139 self.scroll = 0;
140 self.detail_scroll = 0;
141 let mut total_mems = self.memories.len() as u32;
143 if let Ok(projects) = s.list_projects() {
144 total_mems = projects.iter().map(|p| p.memory_count).sum();
145 }
146 self.projects_list = s.list_projects().unwrap_or_default();
147 self.total_mems = total_mems;
148 self.sessions = self
149 .db
150 .sessions()
151 .list(&self.project, 50)
152 .unwrap_or_default();
153 }
154
155 pub fn down(&mut self) {
157 if !self.memories.is_empty() {
158 self.selected = (self.selected + 1).min(self.memories.len() - 1);
159 if self.selected >= self.scroll + 20 {
160 self.scroll += 1;
161 }
162 }
163 }
164 pub fn up(&mut self) {
165 self.selected = self.selected.saturating_sub(1);
166 if self.selected < self.scroll {
167 self.scroll = self.scroll.saturating_sub(1);
168 }
169 }
170 pub fn first(&mut self) {
171 self.selected = 0;
172 self.scroll = 0;
173 }
174 pub fn last(&mut self) {
175 if !self.memories.is_empty() {
176 self.selected = self.memories.len() - 1;
177 self.scroll = self.selected.saturating_sub(19);
178 }
179 }
180 pub fn pgdn(&mut self) {
181 if !self.memories.is_empty() {
182 self.selected = (self.selected + 20).min(self.memories.len() - 1);
183 if self.selected >= self.scroll + 20 {
184 self.scroll += 20;
185 }
186 }
187 }
188 pub fn pgup(&mut self) {
189 self.selected = self.selected.saturating_sub(20);
190 if self.selected < self.scroll {
191 self.scroll = self.scroll.saturating_sub(20);
192 }
193 }
194 pub fn sel(&self) -> Option<&Memory> {
195 self.memories.get(self.selected)
196 }
197
198 pub fn tab_next(&mut self) {
200 self.detail_tab = self.detail_tab.next();
201 self.detail_scroll = 0;
202 }
203 pub fn tab_prev(&mut self) {
204 self.detail_tab = self.detail_tab.prev();
205 self.detail_scroll = 0;
206 }
207 pub fn dscroll_down(&mut self) {
208 self.detail_scroll += 3;
209 }
210 pub fn dscroll_up(&mut self) {
211 self.detail_scroll = self.detail_scroll.saturating_sub(3);
212 }
213
214 pub fn delete_sel(&mut self) {
216 if let Some(m) = self.sel() {
217 self.db.memories().delete(m.id, false).ok();
218 self.status_msg = Some("🗑 Deleted".into());
219 self.load();
220 }
221 }
222 pub fn load_graph(&mut self) {
223 self.graph = self.db.memories().get_graph(&self.project).ok();
224 self.graph_sel = 0;
225 }
226 pub fn graph_next(&mut self) {
227 if let Some(ref d) = self.graph {
228 if !d.nodes.is_empty() {
229 self.graph_sel = (self.graph_sel + 1) % d.nodes.len();
230 }
231 }
232 }
233 pub fn graph_prev(&mut self) {
234 if let Some(ref d) = self.graph {
235 if !d.nodes.is_empty() {
236 self.graph_sel = self.graph_sel.checked_sub(1).unwrap_or(d.nodes.len() - 1);
237 }
238 }
239 }
240 pub fn load_entity(&mut self) {
241 self.entity_data = self.db.entities().frequent_entities(&self.project, 30).ok();
242 }
243 pub fn load_temporal(&mut self) {
244 self.temporal_data = Some((
245 self.db
246 .memories()
247 .list(&self.project, None, None, None, 500, 0)
248 .unwrap_or_default(),
249 0,
250 ));
251 }
252 pub fn temporal_cycle(&mut self) {
253 if let Some(ref mut td) = self.temporal_data {
254 td.1 = (td.1 + 1) % 3;
255 }
256 }
257}