memory dashboard polishing

frdel committed Sep 23, 2025 at 22:59 UTC c817e39186284469807bb8b614ef165311045391
6 files changed +368 -470
python/api/memory_dashboard.py
+66 -170
@@ -24,16 +24,11 @@ class MemoryDashboard(ApiHandler):
24 "success": False,
25 "error": f"Unknown action: {action}",
26 "memories": [],
27 - "total_count": 0
27 + "total_count": 0,
28 }
29
30 except Exception as e:
31 - return {
32 - "success": False,
33 - "error": str(e),
34 - "memories": [],
35 - "total_count": 0
36 - }
31 + return {"success": False, "error": str(e), "memories": [], "total_count": 0}
32
33 async def _delete_memory(self, input: dict) -> dict:
34 """Delete a memory by ID from the specified subdirectory."""
@@ -42,44 +37,25 @@ class MemoryDashboard(ApiHandler):
37 memory_id = input.get("memory_id")
38
39 if not memory_id:
45 - return {
46 - "success": False,
47 - "error": "Memory ID is required for deletion"
48 - }
40 + return {"success": False, "error": "Memory ID is required for deletion"}
41 +
42 + memory = await Memory.get_by_subdir(memory_subdir, preload_knowledge=False)
43
50 - # Check if memory database exists
51 - if Memory.index.get(memory_subdir) is None:
44 + rem = await memory.delete_documents_by_ids([memory_id])
45 +
46 + if len(rem) == 0:
47 return {
48 "success": False,
54 - "error": f"Memory database '{memory_subdir}' not initialized"
49 + "error": f"Memory with ID '{memory_id}' not found",
50 }
56 -
57 - # Get the MyFaiss database directly
58 - myFaiss_db = Memory.index[memory_subdir]
59 -
60 - # Delete the memory by ID (replicate logic from Memory.delete_documents_by_ids)
61 - rem_docs = await myFaiss_db.aget_by_ids([memory_id])
62 - if rem_docs:
63 - rem_ids = [doc.metadata["id"] for doc in rem_docs]
64 - await myFaiss_db.adelete(ids=rem_ids)
65 - # Persist changes to disk
66 - Memory._save_db_file(myFaiss_db, memory_subdir)
51 else:
52 return {
69 - "success": False,
70 - "error": f"Memory with ID '{memory_id}' not found"
53 + "success": True,
54 + "message": f"Memory {memory_id} deleted successfully",
55 }
56
73 - return {
74 - "success": True,
75 - "message": f"Memory {memory_id} deleted successfully"
76 - }
77 -
57 except Exception as e:
79 - return {
80 - "success": False,
81 - "error": f"Failed to delete memory: {str(e)}"
82 - }
58 + return {"success": False, "error": f"Failed to delete memory: {str(e)}"}
59
60 async def _bulk_delete_memories(self, input: dict) -> dict:
61 """Delete multiple memories by IDs from the specified subdirectory."""
@@ -90,101 +66,69 @@ class MemoryDashboard(ApiHandler):
66 if not memory_ids:
67 return {
68 "success": False,
93 - "error": "No memory IDs provided for bulk deletion"
69 + "error": "No memory IDs provided for bulk deletion",
70 }
71
72 if not isinstance(memory_ids, list):
73 return {
74 "success": False,
99 - "error": "Memory IDs must be provided as a list"
75 + "error": "Memory IDs must be provided as a list",
76 }
77
102 - # Check if memory database exists
103 - if Memory.index.get(memory_subdir) is None:
104 - return {
105 - "success": False,
106 - "error": f"Memory database '{memory_subdir}' not initialized"
107 - }
78 + # delete
79 + memory = await Memory.get_by_subdir(memory_subdir, preload_knowledge=False)
80 + rem = await memory.delete_documents_by_ids(memory_ids)
81
109 - # Get the MyFaiss database directly
110 - myFaiss_db = Memory.index[memory_subdir]
111 -
112 - # Delete memories in batch
113 - deleted_count = 0
114 - failed_ids = []
115 -
116 - for memory_id in memory_ids:
117 - try:
118 - # Get memory to check if it exists
119 - rem_docs = await myFaiss_db.aget_by_ids([memory_id])
120 - if rem_docs:
121 - rem_ids = [doc.metadata["id"] for doc in rem_docs]
122 - await myFaiss_db.adelete(ids=rem_ids)
123 - deleted_count += 1
124 - else:
125 - failed_ids.append(memory_id)
126 - except Exception:
127 - failed_ids.append(memory_id)
128 -
129 - # Persist changes to disk if any deletions were successful
130 - if deleted_count > 0:
131 - Memory._save_db_file(myFaiss_db, memory_subdir)
132 -
133 - if deleted_count == len(memory_ids):
82 + if len(rem) == len(memory_ids):
83 return {
84 "success": True,
136 - "message": f"Successfully deleted {deleted_count} memories"
85 + "message": f"Successfully deleted {len(memory_ids)} memories",
86 }
138 - elif deleted_count > 0:
87 + elif len(rem) > 0:
88 return {
89 "success": True,
141 - "message": f"Successfully deleted {deleted_count} memories. {len(failed_ids)} failed: {failed_ids[:5]}"
90 + "message": f"Successfully deleted {len(rem)} memories. {len(memory_ids) - len(rem)} failed.",
91 }
92 else:
93 return {
94 "success": False,
146 - "error": f"Failed to delete any memories. Not found: {failed_ids[:10]}"
95 + "error": f"Failed to delete any memories.",
96 }
97
98 except Exception as e:
99 return {
100 "success": False,
152 - "error": f"Failed to bulk delete memories: {str(e)}"
101 + "error": f"Failed to bulk delete memories: {str(e)}",
102 }
103
104 async def _get_current_memory_subdir(self, request: Request) -> dict:
105 """Get the current memory subdirectory from the active context."""
106 try:
107 # Try to get the context from the request
159 - context_id = getattr(request, 'context_id', None)
108 + context_id = getattr(request, "context_id", None)
109 if not context_id:
110 # Fallback to default if no context available
162 - return {
163 - "success": True,
164 - "memory_subdir": "default"
165 - }
111 + return {"success": True, "memory_subdir": "default"}
112
113 # Import AgentContext here to avoid circular imports
114 from agent import AgentContext
115
116 # Get the context and extract memory subdirectory
117 context = AgentContext.get(context_id)
172 - if context and hasattr(context, 'config') and hasattr(context.config, 'memory_subdir'):
118 + if (
119 + context
120 + and hasattr(context, "config")
121 + and hasattr(context.config, "memory_subdir")
122 + ):
123 memory_subdir = context.config.memory_subdir or "default"
174 - return {
175 - "success": True,
176 - "memory_subdir": memory_subdir
177 - }
124 + return {"success": True, "memory_subdir": memory_subdir}
125 else:
179 - return {
180 - "success": True,
181 - "memory_subdir": "default"
182 - }
126 + return {"success": True, "memory_subdir": "default"}
127
128 except Exception:
129 return {
130 "success": True, # Still success, just fallback to default
187 - "memory_subdir": "default"
131 + "memory_subdir": "default",
132 }
133
134 async def _get_memory_subdirs(self) -> dict:
@@ -197,15 +141,12 @@ class MemoryDashboard(ApiHandler):
141 if "default" not in subdirs:
142 subdirs.insert(0, "default")
143
200 - return {
201 - "success": True,
202 - "subdirs": subdirs
203 - }
144 + return {"success": True, "subdirs": subdirs}
145 except Exception as e:
146 return {
147 "success": False,
148 "error": f"Failed to get memory subdirectories: {str(e)}",
208 - "subdirs": ["default"]
149 + "subdirs": ["default"],
150 }
151
152 async def _search_memories(self, input: dict) -> dict:
@@ -216,111 +157,72 @@ class MemoryDashboard(ApiHandler):
157 area_filter = input.get("area", "") # Filter by memory area
158 search_query = input.get("search", "") # Full-text search query
159 limit = input.get("limit", 100) # Number of results to return
160 + threshold = input.get("threshold", 0.6) # Similarity threshold
161
220 - # Initialize memory if not already done
221 - if Memory.index.get(memory_subdir) is None:
222 - # Create default embeddings model config
223 - embeddings_config = ModelConfig(
224 - type=ModelType.EMBEDDING,
225 - provider="huggingface",
226 - name="sentence-transformers/all-MiniLM-L6-v2",
227 - ctx_length=512,
228 - limit_requests=0,
229 - limit_input=0,
230 - limit_output=0,
231 - vision=False,
232 - kwargs={}
233 - )
234 -
235 - # Initialize memory database (with log_item=None to prevent status blinking)
236 - db, created = Memory.initialize(
237 - log_item=None,
238 - model_config=embeddings_config,
239 - memory_subdir=memory_subdir,
240 - in_memory=False
241 - )
242 -
243 - # Store in the Memory index
244 - Memory.index[memory_subdir] = db
245 -
246 - # Get the MyFaiss database directly
247 - myFaiss_db = Memory.index[memory_subdir]
162 + memory = await Memory.get_by_subdir(memory_subdir, preload_knowledge=False)
163
164 memories = []
165
166 if search_query:
252 - # If search query provided, use similarity search
253 - threshold = 0.6 # Lower threshold for broader search in dashboard
254 - comparator = Memory._get_comparator(f"area == '{area_filter}'") if area_filter else None
255 -
256 - # Get ALL matching results, don't limit in query
257 - docs = await myFaiss_db.asearch(
258 - search_query,
259 - search_type="similarity_score_threshold",
260 - k=10000, # Get all matches up to reasonable max
261 - score_threshold=threshold,
262 - filter=comparator,
167 + docs = await memory.search_similarity_threshold(
168 + query=search_query,
169 + limit=limit,
170 + threshold=threshold,
171 + filter=f"area == '{area_filter}'" if area_filter else "",
172 )
173 memories = docs
174 else:
175 # If no search query, get all memories from specified area(s)
267 - all_docs = myFaiss_db.get_all_docs()
268 -
176 + all_docs = memory.db.get_all_docs()
177 for doc_id, doc in all_docs.items():
178 # Apply area filter if specified
179 if area_filter and doc.metadata.get("area", "") != area_filter:
180 continue
273 -
181 memories.append(doc)
182
183 + # sort by timestamp
184 + def get_sort_key(m):
185 + timestamp = m.metadata.get("timestamp", "0000-00-00 00:00:00")
186 + return timestamp
187 +
188 + memories.sort(key=get_sort_key, reverse=True)
189 +
190 + # Apply limit AFTER sorting to get the newest entries
191 + if limit and len(memories) > limit:
192 + memories = memories[:limit]
193 +
194 # Format memories for the dashboard
195 formatted_memories: list[dict] = []
278 - for memory in memories:
279 - metadata = memory.metadata
196 + for m in memories:
197 + metadata = m.metadata
198
199 # Extract key information
200 memory_data = {
201 "id": metadata.get("id", "unknown"),
202 "area": metadata.get("area", "unknown"),
203 "timestamp": metadata.get("timestamp", "unknown"),
286 - "content_preview": memory.page_content[:200] + ("..." if len(memory.page_content) > 200 else ""),
287 - "content_full": memory.page_content,
204 + "content_preview": m.page_content[:200]
205 + + ("..." if len(m.page_content) > 200 else ""),
206 + "content_full": m.page_content,
207 "knowledge_source": metadata.get("knowledge_source", False),
208 "source_file": metadata.get("source_file", ""),
209 "file_type": metadata.get("file_type", ""),
210 "consolidation_action": metadata.get("consolidation_action", ""),
211 "tags": metadata.get("tags", []),
293 - "metadata": metadata # Include full metadata for advanced users
212 + "metadata": metadata, # Include full metadata for advanced users
213 }
214
215 formatted_memories.append(memory_data)
216
298 - # Sort ALL results by timestamp (newest first) - handle "unknown" timestamps
299 - def get_sort_key(memory):
300 - timestamp = memory["timestamp"]
301 - if timestamp == "unknown" or not timestamp:
302 - return "0000-00-00 00:00:00" # Put unknown timestamps at the end
303 - return timestamp
304 -
305 - formatted_memories.sort(key=get_sort_key, reverse=True)
306 -
307 - # Apply limit AFTER sorting to get the newest entries
308 - if limit and len(formatted_memories) > limit:
309 - formatted_memories = formatted_memories[:limit]
310 -
217 # Get summary statistics
218 total_memories = len(formatted_memories)
313 - knowledge_count = sum(1 for m in formatted_memories if m["knowledge_source"])
219 + knowledge_count = sum(
220 + 1 for m in formatted_memories if m["knowledge_source"]
221 + )
222 conversation_count = total_memories - knowledge_count
223
224 # Get total count of all memories in database (unfiltered)
317 - all_docs = myFaiss_db.get_all_docs()
318 - total_db_count = len(all_docs)
319 -
320 - areas_count: dict[str, int] = {}
321 - for memory_dict in formatted_memories:
322 - area = memory_dict["area"]
323 - areas_count[area] = areas_count.get(area, 0) + 1
225 + total_db_count = len(memory.db.get_all_docs())
226
227 return {
228 "success": True,
@@ -329,16 +231,10 @@ class MemoryDashboard(ApiHandler):
231 "total_db_count": total_db_count,
232 "knowledge_count": knowledge_count,
233 "conversation_count": conversation_count,
332 - "areas_count": areas_count,
234 "search_query": search_query,
235 "area_filter": area_filter,
335 - "memory_subdir": memory_subdir
236 + "memory_subdir": memory_subdir,
237 }
238
239 except Exception as e:
339 - return {
340 - "success": False,
341 - "error": str(e),
342 - "memories": [],
343 - "total_count": 0
344 - }
240 + return {"success": False, "error": str(e), "memories": [], "total_count": 0}
python/api/memory_delete.py deleted
-55
@@ -1,55 +0,0 @@
1 -from python.helpers.api import ApiHandler, Request, Response
2 -from python.helpers.memory import Memory
3 -
4 -
5 -class MemoryDelete(ApiHandler):
6 -
7 - async def process(self, input: dict, request: Request) -> dict | Response:
8 - try:
9 - # Get memory ID to delete
10 - memory_id = input.get("memory_id", "")
11 - if not memory_id:
12 - return {
13 - "success": False,
14 - "error": "Memory ID is required"
15 - }
16 -
17 - # Get context and agent
18 - ctxid = input.get("context", "")
19 - context = self.get_context(ctxid)
20 -
21 - # Check if memory is initialized to avoid triggering preload
22 - memory_subdir = context.agent0.config.memory_subdir or "default"
23 - if Memory.index.get(memory_subdir) is None:
24 - return {
25 - "success": False,
26 - "error": "Memory database not initialized"
27 - }
28 -
29 - # Get already initialized memory instance (no initialization triggered)
30 - db = Memory(
31 - agent=context.agent0,
32 - db=Memory.index[memory_subdir],
33 - memory_subdir=memory_subdir,
34 - )
35 -
36 - # Delete the memory by ID
37 - deleted_docs = await db.delete_documents_by_ids([memory_id])
38 -
39 - if deleted_docs:
40 - return {
41 - "success": True,
42 - "message": f"Memory {memory_id} deleted successfully",
43 - "deleted_count": len(deleted_docs)
44 - }
45 - else:
46 - return {
47 - "success": False,
48 - "error": f"Memory {memory_id} not found or already deleted"
49 - }
50 -
51 - except Exception as e:
52 - return {
53 - "success": False,
54 - "error": str(e)
55 - }
python/helpers/memory.py
+21 -4
@@ -76,7 +76,7 @@ class Memory:
76 False,
77 )
78 Memory.index[memory_subdir] = db
79 - wrap = Memory(agent, db, memory_subdir=memory_subdir)
79 + wrap = Memory(db, memory_subdir=memory_subdir)
80 if agent.config.knowledge_subdirs:
81 await wrap.preload_knowledge(
82 log_item, agent.config.knowledge_subdirs, memory_subdir
@@ -84,11 +84,30 @@ class Memory:
84 return wrap
85 else:
86 return Memory(
87 - agent=agent,
87 db=Memory.index[memory_subdir],
88 memory_subdir=memory_subdir,
89 )
90
91 + @staticmethod
92 + async def get_by_subdir(memory_subdir: str, log_item: LogItem | None = None, preload_knowledge: bool = True):
93 + if not Memory.index.get(memory_subdir):
94 + import initialize
95 + agent_config = initialize.initialize_agent()
96 + model_config = agent_config.embeddings_model
97 + db, _created = Memory.initialize(
98 + log_item=log_item,
99 + model_config=model_config,
100 + memory_subdir=memory_subdir,
101 + in_memory=False,
102 + )
103 + wrap = Memory(db, memory_subdir=memory_subdir)
104 + if preload_knowledge and agent_config.knowledge_subdirs:
105 + await wrap.preload_knowledge(
106 + log_item, agent_config.knowledge_subdirs, memory_subdir
107 + )
108 + Memory.index[memory_subdir] = db
109 + return Memory(db=Memory.index[memory_subdir], memory_subdir=memory_subdir)
110 +
111 @staticmethod
112 async def reload(agent: Agent):
113 memory_subdir = agent.config.memory_subdir or "default"
@@ -212,11 +231,9 @@ class Memory:
231
232 def __init__(
233 self,
215 - agent: Agent,
234 db: MyFaiss,
235 memory_subdir: str,
236 ):
219 - self.agent = agent
237 self.db = db
238 self.memory_subdir = memory_subdir
239
webui/components/settings/memory/memory-dashboard-store.js
+5 -2
@@ -45,7 +45,11 @@ const memoryDashboardStore = {
45
46 // Polling
47 pollingInterval: null,
48 - pollingEnabled: true,
48 + pollingEnabled: false,
49 +
50 + init(){
51 + this.initialize();
52 + },
53
54 async initialize() {
55 // Reset state when opening (but keep directory from context)
@@ -175,7 +179,6 @@ const memoryDashboardStore = {
179 this.totalDbCount = response.total_db_count || 0;
180 this.knowledgeCount = response.knowledge_count || 0;
181 this.conversationCount = response.conversation_count || 0;
178 - this.areasCount = response.areas_count || {};
182
183 if (!silent) {
184 this.message = response.message || null;
webui/components/settings/memory/memory-dashboard.html
+268 -237
@@ -1,54 +1,34 @@
1 <html>
2 +
3 <head>
4 <title>Memory Dashboard</title>
5 <script type="module">
6 import { store } from "/components/settings/memory/memory-dashboard-store.js";
7 </script>
8 </head>
8 -<body>
9 -<div x-data>
10 - <template x-if="$store.memoryDashboardStore">
11 - <div x-init="
12 - $store.memoryDashboardStore.initialize();
13 -
14 - // Setup cleanup when component is removed from DOM
15 - const observer = new MutationObserver((mutations) => {
16 - mutations.forEach((mutation) => {
17 - if (mutation.type === 'childList') {
18 - mutation.removedNodes.forEach((node) => {
19 - if (node.contains && node.contains($el)) {
20 - // Dashboard is being removed, cleanup polling
21 - $store.memoryDashboardStore.cleanup();
22 - observer.disconnect();
23 - }
24 - });
25 - }
26 - });
27 - });
28 -
29 - // Watch for DOM changes
30 - observer.observe(document.body, {
31 - childList: true,
32 - subtree: true
33 - });
34 - " class="memory-dashboard">
35 -
9
10 +<body>
11 + <div x-data>
12 + <template x-if="$store.memoryDashboardStore">
13 + <div x-create="$store.memoryDashboardStore.searchMemories()"
14 + x-destroy="$store.memoryDashboardStore.cleanup()" class="memory-dashboard">
15
16 <!-- Search and Filters -->
17 <div class="filters-section">
18 <div class="filter-row">
19 <div class="filter-group">
20 <label for="memory-subdir-select">Memory Directory:</label>
43 - <select id="memory-subdir-select"
44 - x-model="$store.memoryDashboardStore.selectedMemorySubdir"
45 - @change="$store.memoryDashboardStore.onMemorySubdirChange()"
46 - :disabled="$store.memoryDashboardStore.loadingSubdirs">
21 + <select id="memory-subdir-select" x-model="$store.memoryDashboardStore.selectedMemorySubdir"
22 + @change="$store.memoryDashboardStore.onMemorySubdirChange()"
23 + :disabled="$store.memoryDashboardStore.loadingSubdirs">
24 <template x-for="subdir in $store.memoryDashboardStore.memorySubdirs" :key="subdir">
48 - <option :value="subdir" x-text="subdir"></option>
25 + <option :value="subdir" x-text="subdir"
26 + :selected="subdir === $store.memoryDashboardStore.selectedMemorySubdir">
27 + </option>
28 </template>
29 </select>
51 - <span x-show="$store.memoryDashboardStore.loadingSubdirs" class="loading-text">Loading...</span>
30 + <span x-show="$store.memoryDashboardStore.loadingSubdirs"
31 + class="loading-text">Loading...</span>
32 </div>
33
34 <div class="filter-group">
@@ -65,22 +45,22 @@
45 <div class="filter-group">
46 <label for="search-input">Search:</label>
47 <input type="text" id="search-input" x-model="$store.memoryDashboardStore.searchQuery"
68 - placeholder="Search memory content..."
69 - @keyup.enter="$store.memoryDashboardStore.searchMemories()" />
48 + placeholder="Search memory content..."
49 + @keyup.enter="$store.memoryDashboardStore.searchMemories()" />
50 </div>
51
52 <div class="filter-group">
53 <label for="limit-input">Limit:</label>
54 <input type="number" id="limit-input" x-model.number="$store.memoryDashboardStore.limit"
75 - min="10" max="1000" step="10"
76 - style="min-width: 100px;" />
55 + min="10" max="1000" step="10" style="min-width: 100px;" />
56 </div>
57
58 <div class="filter-actions">
59 <button class="btn primary slim" @click="$store.memoryDashboardStore.searchMemories()"
81 - :disabled="$store.memoryDashboardStore.loading || $store.memoryDashboardStore.loadingSubdirs">
60 + :disabled="$store.memoryDashboardStore.loading || $store.memoryDashboardStore.loadingSubdirs">
61 <span x-show="!$store.memoryDashboardStore.initializingMemory">Search</span>
83 - <span x-show="$store.memoryDashboardStore.initializingMemory">Initializing Memory...</span>
62 + <span x-show="$store.memoryDashboardStore.initializingMemory">Initializing
63 + Memory...</span>
64 </button>
65 <button class="btn slim" @click="$store.memoryDashboardStore.clearSearch()">
66 Clear
@@ -101,210 +81,226 @@
81 <span x-text="$store.memoryDashboardStore.error"></span>
82 </div>
83
104 - <!-- Memory table -->
105 - <div x-show="!$store.memoryDashboardStore.loading && !$store.memoryDashboardStore.error"
106 - class="memory-table-container">
107 -
108 - <!-- Combined stats and pagination header -->
109 - <div class="stats-pagination-header" x-show="!$store.memoryDashboardStore.loading">
110 - <!-- Statistics -->
111 - <div class="memory-stats-compact">
112 - <div class="stat-item">
113 - <span class="stat-label">DB Total:</span>
114 - <span class="stat-value" x-text="$store.memoryDashboardStore.totalDbCount"></span>
115 - </div>
116 - <div class="stat-item">
117 - <span class="stat-label">Filtered:</span>
118 - <span class="stat-value" x-text="$store.memoryDashboardStore.totalCount"></span>
119 - </div>
120 - <div class="stat-item">
121 - <span class="stat-label">Knowledge:</span>
122 - <span class="stat-value" x-text="$store.memoryDashboardStore.knowledgeCount"></span>
123 - </div>
124 - <div class="stat-item">
125 - <span class="stat-label">Conversation:</span>
126 - <span class="stat-value" x-text="$store.memoryDashboardStore.conversationCount"></span>
127 - </div>
84 + <!-- Memory table -->
85 + <div x-show="!$store.memoryDashboardStore.loading && !$store.memoryDashboardStore.error"
86 + class="memory-table-container">
87 +
88 + <!-- Combined stats and pagination header -->
89 + <div class="stats-pagination-header" x-show="!$store.memoryDashboardStore.loading">
90 + <!-- Statistics -->
91 + <div class="memory-stats-compact">
92 + <div class="stat-item">
93 + <span class="stat-label">DB Total:</span>
94 + <span class="stat-value" x-text="$store.memoryDashboardStore.totalDbCount"></span>
95 </div>
129 -
130 - <!-- Pagination controls -->
131 - <div class="pagination-controls-compact" x-show="$store.memoryDashboardStore.totalPages > 1">
132 - <div class="pagination-info-inline">
133 - <span>Page <span x-text="$store.memoryDashboardStore.currentPage"></span>
134 - of <span x-text="$store.memoryDashboardStore.totalPages"></span>
135 - </span>
136 - </div>
137 -
138 - <div class="pagination-controls">
139 - <button class="btn slim" @click="$store.memoryDashboardStore.prevPage()"
140 - :disabled="$store.memoryDashboardStore.currentPage === 1">
141 - Previous
142 - </button>
143 -
144 - <select class="page-select"
145 - x-model.number="$store.memoryDashboardStore.currentPage"
146 - @change="$store.memoryDashboardStore.goToPage($store.memoryDashboardStore.currentPage)">
147 - <template x-for="page in Array.from({length: $store.memoryDashboardStore.totalPages}, (_, i) => i + 1)" :key="page">
148 - <option :value="page" x-text="'Page ' + page"></option>
149 - </template>
150 - </select>
151 -
152 - <button class="btn slim" @click="$store.memoryDashboardStore.nextPage()"
153 - :disabled="$store.memoryDashboardStore.currentPage === $store.memoryDashboardStore.totalPages">
154 - Next
155 - </button>
156 - </div>
96 + <div class="stat-item">
97 + <span class="stat-label">Filtered:</span>
98 + <span class="stat-value" x-text="$store.memoryDashboardStore.totalCount"></span>
99 + </div>
100 + <div class="stat-item">
101 + <span class="stat-label">Knowledge:</span>
102 + <span class="stat-value" x-text="$store.memoryDashboardStore.knowledgeCount"></span>
103 + </div>
104 + <div class="stat-item">
105 + <span class="stat-label">Conversation:</span>
106 + <span class="stat-value" x-text="$store.memoryDashboardStore.conversationCount"></span>
107 </div>
108 </div>
109
160 - <!-- Mass action toolbar -->
161 - <div x-show="$store.memoryDashboardStore.selectedCount > 0" class="mass-action-toolbar">
162 - <div class="selection-info">
163 - <span x-text="$store.memoryDashboardStore.selectedCount"></span> memories selected
110 + <!-- Pagination controls -->
111 + <div class="pagination-controls-compact" x-show="$store.memoryDashboardStore.totalPages > 1">
112 + <div class="pagination-info-inline">
113 + <span>Page <span x-text="$store.memoryDashboardStore.currentPage"></span>
114 + of <span x-text="$store.memoryDashboardStore.totalPages"></span>
115 + </span>
116 </div>
117
166 - <div class="mass-actions">
167 - <button class="btn-mass copy" @click="$store.memoryDashboardStore.bulkCopyMemories()"
168 - title="Copy Selected Content">
169 - <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
170 - <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
171 - <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
172 - </svg>
173 - Copy
118 + <div class="pagination-controls">
119 + <button class="btn slim" @click="$store.memoryDashboardStore.prevPage()"
120 + :disabled="$store.memoryDashboardStore.currentPage === 1">
121 + Previous
122 </button>
123
176 - <button class="btn-mass export" @click="$store.memoryDashboardStore.bulkExportMemories()"
177 - title="Export Selected Memories">
178 - <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
179 - <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
180 - <polyline points="7,10 12,15 17,10"></polyline>
181 - <line x1="12" y1="15" x2="12" y2="3"></line>
182 - </svg>
183 - Export
124 + <select class="page-select" x-model.number="$store.memoryDashboardStore.currentPage"
125 + @change="$store.memoryDashboardStore.goToPage($store.memoryDashboardStore.currentPage)">
126 + <template
127 + x-for="page in Array.from({length: $store.memoryDashboardStore.totalPages}, (_, i) => i + 1)"
128 + :key="page">
129 + <option :value="page" x-text="'Page ' + page"></option>
130 + </template>
131 + </select>
132 +
133 + <button class="btn slim" @click="$store.memoryDashboardStore.nextPage()"
134 + :disabled="$store.memoryDashboardStore.currentPage === $store.memoryDashboardStore.totalPages">
135 + Next
136 </button>
137 + </div>
138 + </div>
139 + </div>
140
186 - <button class="btn-mass delete" @click="$store.memoryDashboardStore.bulkDeleteMemories()"
187 - title="Delete Selected Memories">
188 - <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
189 - <polyline points="3,6 5,6 21,6"></polyline>
190 - <path d="M19,6V20a2,2,0,0,1-2,2H7a2,2,0,0,1-2-2V6m3,0V4a2,2,0,0,1,2-2h4a2,2,0,0,1,2,2V6"></path>
191 - <line x1="10" y1="11" x2="10" y2="17"></line>
192 - <line x1="14" y1="11" x2="14" y2="17"></line>
193 - </svg>
194 - Delete
195 - </button>
141 + <!-- Mass action toolbar -->
142 + <div x-show="$store.memoryDashboardStore.selectedCount > 0" class="mass-action-toolbar">
143 + <div class="selection-info">
144 + <span x-text="$store.memoryDashboardStore.selectedCount"></span> memories selected
145 + </div>
146
197 - <button class="btn-mass clear" @click="$store.memoryDashboardStore.clearSelection()"
198 - title="Clear Selection">
199 - <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
200 - <line x1="18" y1="6" x2="6" y2="18"></line>
201 - <line x1="6" y1="6" x2="18" y2="18"></line>
202 - </svg>
203 - Clear
204 - </button>
205 - </div>
147 + <div class="mass-actions">
148 + <button class="btn-mass copy" @click="$store.memoryDashboardStore.bulkCopyMemories()"
149 + title="Copy Selected Content">
150 + <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"
151 + stroke-width="2">
152 + <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
153 + <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
154 + </svg>
155 + Copy
156 + </button>
157 +
158 + <button class="btn-mass export" @click="$store.memoryDashboardStore.bulkExportMemories()"
159 + title="Export Selected Memories">
160 + <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"
161 + stroke-width="2">
162 + <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
163 + <polyline points="7,10 12,15 17,10"></polyline>
164 + <line x1="12" y1="15" x2="12" y2="3"></line>
165 + </svg>
166 + Export
167 + </button>
168 +
169 + <button class="btn-mass delete" @click="$store.memoryDashboardStore.bulkDeleteMemories()"
170 + title="Delete Selected Memories">
171 + <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"
172 + stroke-width="2">
173 + <polyline points="3,6 5,6 21,6"></polyline>
174 + <path
175 + d="M19,6V20a2,2,0,0,1-2,2H7a2,2,0,0,1-2-2V6m3,0V4a2,2,0,0,1,2-2h4a2,2,0,0,1,2,2V6">
176 + </path>
177 + <line x1="10" y1="11" x2="10" y2="17"></line>
178 + <line x1="14" y1="11" x2="14" y2="17"></line>
179 + </svg>
180 + Delete
181 + </button>
182 +
183 + <button class="btn-mass clear" @click="$store.memoryDashboardStore.clearSelection()"
184 + title="Clear Selection">
185 + <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"
186 + stroke-width="2">
187 + <line x1="18" y1="6" x2="6" y2="18"></line>
188 + <line x1="6" y1="6" x2="18" y2="18"></line>
189 + </svg>
190 + Clear
191 + </button>
192 </div>
193 + </div>
194
208 - <div class="table-wrapper">
209 - <table class="memory-table">
210 - <thead>
211 - <tr>
212 - <th class="col-select">
213 - <input type="checkbox"
214 - :checked="$store.memoryDashboardStore.allSelected"
215 - :indeterminate="$store.memoryDashboardStore.someSelected && !$store.memoryDashboardStore.allSelected"
216 - @change="$store.memoryDashboardStore.toggleSelectAll()"
217 - title="Select/Deselect All" />
218 - </th>
219 - <th class="col-metadata">Metadata</th>
220 - <th class="col-preview">Preview</th>
221 - <th class="col-actions"></th>
222 - </tr>
223 - </thead>
224 - <tbody>
225 - <template x-for="memory in $store.memoryDashboardStore.paginatedMemories" :key="memory.id">
226 - <tr class="memory-row" :class="{'selected': memory.selected}"
227 - @click="$store.memoryDashboardStore.showMemoryDetails(memory)"
228 - style="cursor: pointer;">
229 - <!-- Selection checkbox -->
230 - <td class="select-cell" @click.stop>
231 - <input type="checkbox"
232 - x-model="memory.selected" />
233 - </td>
234 -
235 - <!-- Metadata (merged Area, Timestamp, Source) -->
236 - <td class="metadata-cell">
237 - <div class="metadata-info">
238 - <div class="metadata-row">
239 - <span class="area-badge"
240 - :style="`background-color: ${$store.memoryDashboardStore.getAreaColor(memory.area)}`"
241 - x-text="(memory.area || 'UNKNOWN').toUpperCase()"></span>
242 - </div>
243 - <div class="metadata-row metadata-timestamp">
244 - <span x-text="$store.memoryDashboardStore.formatTimestamp(memory.timestamp, true)"
245 - :title="$store.memoryDashboardStore.formatTimestamp(memory.timestamp, false)"></span>
195 + <div class="table-wrapper">
196 + <table class="memory-table">
197 + <thead>
198 + <tr>
199 + <th class="col-select">
200 + <input type="checkbox" :checked="$store.memoryDashboardStore.allSelected"
201 + :indeterminate="$store.memoryDashboardStore.someSelected && !$store.memoryDashboardStore.allSelected"
202 + @change="$store.memoryDashboardStore.toggleSelectAll()"
203 + title="Select/Deselect All" />
204 + </th>
205 + <th class="col-metadata">Metadata</th>
206 + <th class="col-preview">Preview</th>
207 + <th class="col-actions"></th>
208 + </tr>
209 + </thead>
210 + <tbody>
211 + <template x-for="memory in $store.memoryDashboardStore.paginatedMemories"
212 + :key="memory.id">
213 + <tr class="memory-row" :class="{'selected': memory.selected}"
214 + @click="$store.memoryDashboardStore.showMemoryDetails(memory)"
215 + style="cursor: pointer;">
216 + <!-- Selection checkbox -->
217 + <td class="select-cell" @click.stop>
218 + <input type="checkbox" x-model="memory.selected" />
219 + </td>
220 +
221 + <!-- Metadata (merged Area, Timestamp, Source) -->
222 + <td class="metadata-cell">
223 + <div class="metadata-info">
224 + <div class="metadata-row">
225 + <span class="area-badge"
226 + :style="`background-color: ${$store.memoryDashboardStore.getAreaColor(memory.area)}`"
227 + x-text="(memory.area || 'UNKNOWN').toUpperCase()"></span>
228 + </div>
229 + <div class="metadata-row metadata-timestamp">
230 + <span
231 + x-text="$store.memoryDashboardStore.formatTimestamp(memory.timestamp, true)"
232 + :title="$store.memoryDashboardStore.formatTimestamp(memory.timestamp, false)"></span>
233 + </div>
234 + <div class="metadata-row">
235 + <template x-if="memory.knowledge_source">
236 + <span class="source-type knowledge">Knowledge</span>
237 + </template>
238 + <template x-if="!memory.knowledge_source">
239 + <span class="source-type conversation">Conversation</span>
240 + </template>
241 + </div>
242 </div>
247 - <div class="metadata-row">
248 - <template x-if="memory.knowledge_source">
249 - <span class="source-type knowledge">Knowledge</span>
250 - </template>
251 - <template x-if="!memory.knowledge_source">
252 - <span class="source-type conversation">Conversation</span>
243 + </td>
244 +
245 + <!-- Content preview -->
246 + <td class="preview-cell">
247 + <div class="content-preview" x-text="memory.content_preview"></div>
248 + <div class="tags" x-show="memory.tags && memory.tags.length > 0">
249 + <template x-for="tag in memory.tags" :key="tag">
250 + <span class="tag" x-text="tag"></span>
251 </template>
252 </div>
255 - </div>
256 - </td>
257 -
258 - <!-- Content preview -->
259 - <td class="preview-cell">
260 - <div class="content-preview" x-text="memory.content_preview"></div>
261 - <div class="tags" x-show="memory.tags && memory.tags.length > 0">
262 - <template x-for="tag in memory.tags" :key="tag">
263 - <span class="tag" x-text="tag"></span>
264 - </template>
265 - </div>
266 - </td>
267 -
268 - <!-- Actions -->
269 - <td class="actions-cell" @click.stop>
270 - <div class="actions-wrapper">
271 - <button class="btn-action copy" @click="$store.memoryDashboardStore.copyToClipboard($store.memoryDashboardStore.formatMemoryForCopy(memory))"
272 - title="Copy Memory">
273 - <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
274 - <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
275 - <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
276 - </svg>
277 - </button>
253 + </td>
254
279 - <button class="btn-action delete" @click="$store.memoryDashboardStore.deleteMemory(memory)"
255 + <!-- Actions -->
256 + <td class="actions-cell" @click.stop>
257 + <div class="actions-wrapper">
258 + <button class="btn-action copy"
259 + @click="$store.memoryDashboardStore.copyToClipboard($store.memoryDashboardStore.formatMemoryForCopy(memory))"
260 + title="Copy Memory">
261 + <svg width="14" height="14" viewBox="0 0 24 24" fill="none"
262 + stroke="currentColor" stroke-width="2">
263 + <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
264 + <path
265 + d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1">
266 + </path>
267 + </svg>
268 + </button>
269 +
270 + <button class="btn-action delete"
271 + @click="$store.memoryDashboardStore.deleteMemory(memory)"
272 title="Delete Memory">
281 - <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
282 - <polyline points="3,6 5,6 21,6"></polyline>
283 - <path d="M19,6V20a2,2,0,0,1-2,2H7a2,2,0,0,1-2-2V6M8,6V4a2,2,0,0,1,2-2h4a2,2,0,0,1,2,2V6"></path>
284 - <line x1="10" y1="11" x2="10" y2="17"></line>
285 - <line x1="14" y1="11" x2="14" y2="17"></line>
286 - </svg>
287 - </button>
288 - </div>
289 - </td>
290 - </tr>
291 - </template>
292 - </tbody>
293 - </table>
273 + <svg width="14" height="14" viewBox="0 0 24 24" fill="none"
274 + stroke="currentColor" stroke-width="2">
275 + <polyline points="3,6 5,6 21,6"></polyline>
276 + <path
277 + d="M19,6V20a2,2,0,0,1-2,2H7a2,2,0,0,1-2-2V6M8,6V4a2,2,0,0,1,2-2h4a2,2,0,0,1,2,2V6">
278 + </path>
279 + <line x1="10" y1="11" x2="10" y2="17"></line>
280 + <line x1="14" y1="11" x2="14" y2="17"></line>
281 + </svg>
282 + </button>
283 + </div>
284 + </td>
285 + </tr>
286 + </template>
287 + </tbody>
288 + </table>
289
290
291
292 </div>
293
299 - <!-- No memories message -->
300 - <div x-show="$store.memoryDashboardStore.memories.length === 0 && !$store.memoryDashboardStore.message" class="no-memories">
301 - No memories found matching the current filters.
302 - </div>
294 + <!-- No memories message -->
295 + <div x-show="$store.memoryDashboardStore.memories.length === 0 && !$store.memoryDashboardStore.message"
296 + class="no-memories">
297 + No memories found matching the current filters.
298 + </div>
299
304 - <!-- Initialization message -->
305 - <div x-show="$store.memoryDashboardStore.message" class="init-message">
306 - <span x-text="$store.memoryDashboardStore.message"></span>
307 - </div>
300 + <!-- Initialization message -->
301 + <div x-show="$store.memoryDashboardStore.message" class="init-message">
302 + <span x-text="$store.memoryDashboardStore.message"></span>
303 + </div>
304
305
306
@@ -423,7 +419,8 @@
419 color: var(--color-text);
420 }
421
426 - .filter-group input, .filter-group select {
422 + .filter-group input,
423 + .filter-group select {
424 padding: 0.5rem;
425 border: 1px solid var(--color-border);
426 background: var(--color-background);
@@ -435,7 +432,8 @@
432 box-sizing: border-box;
433 }
434
438 - .filter-group input:focus, .filter-group select:focus {
435 + .filter-group input:focus,
436 + .filter-group select:focus {
437 outline: none;
438 border-color: var(--color-primary);
439 box-shadow: 0 0 0 2px rgba(115, 122, 129, 0.2);
@@ -467,7 +465,10 @@
465 font-style: italic;
466 }
467
470 - .loading-state, .error-state, .no-memories, .init-message {
468 + .loading-state,
469 + .error-state,
470 + .no-memories,
471 + .init-message {
472 text-align: center;
473 padding: 2rem;
474 color: var(--color-text);
@@ -518,7 +519,9 @@
519 }
520
521 @keyframes spin {
521 - to { transform: rotate(360deg); }
522 + to {
523 + transform: rotate(360deg);
524 + }
525 }
526
527 .memory-table-container {
@@ -541,10 +544,21 @@
544 }
545
546 /* Fixed column widths for proper fit */
544 - .col-select { width: 5%; }
545 - .col-metadata { width: 25%; }
546 - .col-preview { width: 60%; }
547 - .col-actions { width: 10%; }
547 + .col-select {
548 + width: 5%;
549 + }
550 +
551 + .col-metadata {
552 + width: 25%;
553 + }
554 +
555 + .col-preview {
556 + width: 60%;
557 + }
558 +
559 + .col-actions {
560 + width: 10%;
561 + }
562
563 .memory-table th,
564 .memory-table td {
@@ -587,7 +601,8 @@
601 }
602
603 /* Selection column styling */
590 - .select-cell, .col-select {
604 + .select-cell,
605 + .col-select {
606 text-align: center;
607 padding: 0.5rem !important;
608 }
@@ -680,6 +695,7 @@
695 opacity: 0;
696 transform: translateY(-10px);
697 }
698 +
699 to {
700 opacity: 1;
701 transform: translateY(0);
@@ -1003,6 +1019,7 @@
1019 transform: translateY(20px) scale(0.95);
1020 opacity: 0;
1021 }
1022 +
1023 to {
1024 transform: translateY(0) scale(1);
1025 opacity: 1;
@@ -1290,7 +1307,8 @@
1307 align-items: stretch;
1308 }
1309
1293 - .filter-group input, .filter-group select {
1310 + .filter-group input,
1311 + .filter-group select {
1312 min-width: unset;
1313 width: 100%;
1314 }
@@ -1306,10 +1324,21 @@
1324 width: 60px;
1325 }
1326
1309 - .col-select { width: 8%; }
1310 - .col-metadata { width: 27%; }
1311 - .col-preview { width: 55%; }
1312 - .col-actions { width: 10%; }
1327 + .col-select {
1328 + width: 8%;
1329 + }
1330 +
1331 + .col-metadata {
1332 + width: 27%;
1333 + }
1334 +
1335 + .col-preview {
1336 + width: 55%;
1337 + }
1338 +
1339 + .col-actions {
1340 + width: 10%;
1341 + }
1342
1343 .memory-detail-modal {
1344 width: 98%;
@@ -1382,7 +1411,8 @@
1411 align-items: center;
1412 }
1413
1385 - .timestamp-badge, .source-badge {
1414 + .timestamp-badge,
1415 + .source-badge {
1416 padding: 0.25rem 0.75rem;
1417 border-radius: 12px;
1418 font-size: 0.75rem;
@@ -1604,7 +1634,8 @@
1634 }
1635 </style>
1636
1607 - </template>
1637 + </template>
1638 </div>
1639 </body>
1610 -</html>
1640 +
1641 +</html>
\ No newline at end of file
webui/js/initFw.js
+8 -2
@@ -1,9 +1,9 @@
1 -import * as initializer from "./initializer.js"
1 +import * as initializer from "./initializer.js";
2 import * as _modals from "./modals.js";
3 import * as _components from "./components.js";
4
5 // initialize required elements
6 -await initializer.initialize()
6 +await initializer.initialize();
7
8 // import alpine library
9 await import("../vendor/alpine/alpine.min.js");
@@ -16,3 +16,9 @@ Alpine.directive(
16 cleanup(() => onDestroy());
17 }
18 );
19 +
20 +// add x-create directive to alpine
21 +Alpine.directive("create", (_el, { expression }, { evaluateLater }) => {
22 + const onCreate = evaluateLater(expression);
23 + onCreate();
24 +});