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
+});