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 detail_tab: DetailTab,
60 pub detail_scroll: usize,
61 pub stats: Option<MemoryStats>,
62 pub graph: Option<GraphData>,
63 pub graph_sel: usize,
64 pub entity_data: Option<Vec<(String, EntityType, u32)>>,
65 pub temporal_data: Option<(Vec<Memory>, u8)>,
66 pub active_panel: usize, pub total_mems: u32,
68 pub db: Arc<Database>,
69}
70
71impl App {
72 pub fn new(db: Arc<Database>, _settings: Arc<Settings>) -> Self {
73 let project = Settings::infer_project();
74 Self {
75 project,
76 memories: Vec::new(),
77 sessions: Vec::new(),
78 selected: 0,
79 scroll: 0,
80 search: String::new(),
81 status_msg: None,
82 quit: false,
83 detail_tab: DetailTab::Content,
84 detail_scroll: 0,
85 stats: None,
86 graph: None,
87 graph_sel: 0,
88 entity_data: None,
89 temporal_data: None,
90 active_panel: 0,
91 total_mems: 0,
92 db,
93 }
94 }
95
96 pub fn load(&mut self) {
98 let s = self.db.memories();
99 if self.search.is_empty() {
100 self.memories = s
101 .list(&self.project, None, None, None, 500, 0)
102 .unwrap_or_default();
103 } else {
104 let q = SearchQuery {
105 text: self.search.clone(),
106 project: Some(self.project.clone()),
107 scope: None,
108 memory_type: None,
109 importance: None,
110 tags: vec![],
111 limit: 200,
112 include_snippet: false,
113 all_projects: false,
114 };
115 self.memories = s
116 .search(&q, &crate::store::search::SearchWeights::default(), None)
117 .unwrap_or_default()
118 .into_iter()
119 .map(|r| r.memory)
120 .collect();
121 }
122 self.selected = self.selected.min(self.memories.len().saturating_sub(1));
123 self.scroll = 0;
124 self.detail_scroll = 0;
125 self.stats = s.stats(&self.project).ok();
126 self.total_mems = self.stats.as_ref().map(|s| s.total_memories).unwrap_or(0);
127 self.sessions = self
128 .db
129 .sessions()
130 .list(&self.project, 50)
131 .unwrap_or_default();
132 }
133
134 pub fn down(&mut self) {
136 if !self.memories.is_empty() {
137 self.selected = (self.selected + 1).min(self.memories.len() - 1);
138 if self.selected >= self.scroll + 20 {
139 self.scroll += 1;
140 }
141 }
142 }
143 pub fn up(&mut self) {
144 self.selected = self.selected.saturating_sub(1);
145 if self.selected < self.scroll {
146 self.scroll = self.scroll.saturating_sub(1);
147 }
148 }
149 pub fn first(&mut self) {
150 self.selected = 0;
151 self.scroll = 0;
152 }
153 pub fn last(&mut self) {
154 if !self.memories.is_empty() {
155 self.selected = self.memories.len() - 1;
156 self.scroll = self.selected.saturating_sub(19);
157 }
158 }
159 pub fn pgdn(&mut self) {
160 if !self.memories.is_empty() {
161 self.selected = (self.selected + 20).min(self.memories.len() - 1);
162 if self.selected >= self.scroll + 20 {
163 self.scroll += 20;
164 }
165 }
166 }
167 pub fn pgup(&mut self) {
168 self.selected = self.selected.saturating_sub(20);
169 if self.selected < self.scroll {
170 self.scroll = self.scroll.saturating_sub(20);
171 }
172 }
173 pub fn sel(&self) -> Option<&Memory> {
174 self.memories.get(self.selected)
175 }
176
177 pub fn tab_next(&mut self) {
179 self.detail_tab = self.detail_tab.next();
180 self.detail_scroll = 0;
181 }
182 pub fn tab_prev(&mut self) {
183 self.detail_tab = self.detail_tab.prev();
184 self.detail_scroll = 0;
185 }
186 pub fn dscroll_down(&mut self) {
187 self.detail_scroll += 3;
188 }
189 pub fn dscroll_up(&mut self) {
190 self.detail_scroll = self.detail_scroll.saturating_sub(3);
191 }
192
193 pub fn delete_sel(&mut self) {
195 if let Some(m) = self.sel() {
196 self.db.memories().delete(m.id, false).ok();
197 self.status_msg = Some("🗑 Deleted".into());
198 self.load();
199 }
200 }
201 pub fn load_graph(&mut self) {
202 self.graph = self.db.memories().get_graph(&self.project).ok();
203 self.graph_sel = 0;
204 }
205 pub fn graph_next(&mut self) {
206 if let Some(ref d) = self.graph {
207 if !d.nodes.is_empty() {
208 self.graph_sel = (self.graph_sel + 1) % d.nodes.len();
209 }
210 }
211 }
212 pub fn graph_prev(&mut self) {
213 if let Some(ref d) = self.graph {
214 if !d.nodes.is_empty() {
215 self.graph_sel = self.graph_sel.checked_sub(1).unwrap_or(d.nodes.len() - 1);
216 }
217 }
218 }
219 pub fn load_entity(&mut self) {
220 self.entity_data = self.db.entities().frequent_entities(&self.project, 30).ok();
221 }
222 pub fn load_temporal(&mut self) {
223 self.temporal_data = Some((
224 self.db
225 .memories()
226 .list(&self.project, None, None, None, 500, 0)
227 .unwrap_or_default(),
228 0,
229 ));
230 }
231 pub fn temporal_cycle(&mut self) {
232 if let Some(ref mut td) = self.temporal_data {
233 td.1 = (td.1 + 1) % 3;
234 }
235 }
236}