fix: eview refinements for memory dashboard

Adressed: 1) When selecting memories using checkbox, it unchecks one second later automatically (Brave+Firefox) 2) Area filter does not work. When All is selecter, it shows me memories from fragments, solutions, etc., but when I select an area, no memories are shown. 3) Modal does not have a title, it shows a path 4) In light mode, buttons and some labels are almost invisible - we should not use custom colors anywhere, we should always use one of the color vars in the screenshot, those are automatically adjusted based on current theme. Also in light mode there's a lot of gray areas that blend, again, we should use our color vars. 5) The loading spinner is stretched into an oval 6) We should merge the first div (title and memory counts) with the third one (pagination) to save space. + we should also show the total number of memories in the DB (unfiltered, just the total count of docs) 7) Action buttons should stack vertically, we can remove the title to make it more narrow 8) We should not use emojis for icons, they break the design, it's used in the source badge (knowledge, conversation...)

Rafael Uzarowski committed Aug 26, 2025 at 17:44 UTC 28900232427383d0c344e78705787f371cba5f6b
5 files changed +606 -260
python/api/memory_dashboard.py
+12 -6
@@ -253,10 +253,11 @@ class MemoryDashboard(ApiHandler):
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",
259 - k=limit,
260 + k=10000, # Get all matches up to reasonable max
261 score_threshold=threshold,
262 filter=comparator,
263 )
@@ -272,10 +273,6 @@ class MemoryDashboard(ApiHandler):
273
274 memories.append(doc)
275
275 - # Apply limit
276 - if len(memories) >= limit:
277 - break
278 -
276 # Format memories for the dashboard
277 formatted_memories: list[dict] = []
278 for memory in memories:
@@ -298,7 +295,7 @@ class MemoryDashboard(ApiHandler):
295
296 formatted_memories.append(memory_data)
297
301 - # Sort by timestamp (newest first) - handle "unknown" timestamps
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:
@@ -307,11 +304,19 @@ class MemoryDashboard(ApiHandler):
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 +
311 # Get summary statistics
312 total_memories = len(formatted_memories)
313 knowledge_count = sum(1 for m in formatted_memories if m["knowledge_source"])
314 conversation_count = total_memories - knowledge_count
315
316 + # 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"]
@@ -321,6 +326,7 @@ class MemoryDashboard(ApiHandler):
326 "success": True,
327 "memories": formatted_memories,
328 "total_count": total_memories,
329 + "total_db_count": total_db_count,
330 "knowledge_count": knowledge_count,
331 "conversation_count": conversation_count,
332 "areas_count": areas_count,
webui/components/settings/memory/memory-dashboard-store.js
+42 -29
@@ -2,6 +2,12 @@ import { createStore } from "/js/AlpineStore.js";
2 import { getContext } from "/index.js";
3 import * as API from "/js/api.js";
4 import { openModal, closeModal } from "/js/modals.js";
5 +import { store as notificationStore } from "/components/notifications/notification-store.js";
6 +
7 +// Helper function for toasts
8 +function justToast(text, type = "info", timeout = 5000) {
9 + notificationStore.addFrontendToastOnly(type, text, "", timeout / 1000);
10 +}
11
12 // Memory Dashboard Store
13 const memoryDashboardStore = {
@@ -29,6 +35,7 @@ const memoryDashboardStore = {
35
36 // Stats
37 totalCount: 0,
38 + totalDbCount: 0,
39 knowledgeCount: 0,
40 conversationCount: 0,
41 areasCount: {},
@@ -147,13 +154,25 @@ const memoryDashboardStore = {
154 limit: this.limit
155 });
156
150 - if (response.success) {
157 + if (response.success) {
158 + // Preserve existing selections when updating memories during polling
159 + const existingSelections = {};
160 + if (silent && this.memories) {
161 + // Build a map of existing selections by memory ID
162 + this.memories.forEach(memory => {
163 + if (memory.selected) {
164 + existingSelections[memory.id] = true;
165 + }
166 + });
167 + }
168 +
169 // Add selected property to each memory item for mass selection
170 this.memories = (response.memories || []).map(memory => ({
171 ...memory,
154 - selected: memory.selected || false
172 + selected: existingSelections[memory.id] || false
173 }));
174 this.totalCount = response.total_count || 0;
175 + this.totalDbCount = response.total_db_count || 0;
176 this.knowledgeCount = response.knowledge_count || 0;
177 this.conversationCount = response.conversation_count || 0;
178 this.areasCount = response.areas_count || {};
@@ -290,16 +309,16 @@ const memoryDashboardStore = {
309 });
310
311 if (response.success) {
293 - this.showToast(`Successfully deleted ${selectedMemories.length} memories`, "success");
312 + justToast(`Successfully deleted ${selectedMemories.length} memories`, "success");
313
314 // Let polling refresh the data instead of manual manipulation
315 // Trigger an immediate refresh to get updated state from backend
316 await this.searchMemories(true); // silent refresh
317 } else {
299 - this.showToast(response.error || "Failed to delete selected memories", "error");
318 + justToast(response.error || "Failed to delete selected memories", "error");
319 }
320 } catch (error) {
302 - this.showToast(error.message || "Failed to delete selected memories", "error");
321 + justToast(error.message || "Failed to delete selected memories", "error");
322 } finally {
323 this.loading = false;
324 }
@@ -337,7 +356,7 @@ ${memory.content_full}
356 const content = selectedMemories.map(memory => this.formatMemoryForCopy(memory)).join('\n');
357
358 this.copyToClipboard(content);
340 - this.showToast(`Copied ${selectedMemories.length} memories with metadata to clipboard`, "success");
359 + justToast(`Copied ${selectedMemories.length} memories with metadata to clipboard`, "success");
360 },
361
362 bulkExportMemories() {
@@ -375,7 +394,7 @@ ${memory.content_full}
394 document.body.removeChild(a);
395 URL.revokeObjectURL(url);
396
378 - this.showToast(`Exported ${selectedMemories.length} selected memories to ${filename}`, "success");
397 + justToast(`Exported ${selectedMemories.length} selected memories to ${filename}`, "success");
398 },
399
400 // Memory detail modal (standard approach)
@@ -431,18 +450,18 @@ ${memory.content_full}
450
451 getAreaColor(area) {
452 const colors = {
434 - "MAIN": "#3b82f6", // blue
435 - "FRAGMENTS": "#10b981", // emerald
436 - "SOLUTIONS": "#8b5cf6", // violet
437 - "INSTRUMENTS": "#f59e0b" // amber
453 + "main": "#3b82f6",
454 + "fragments": "#10b981",
455 + "solutions": "#8b5cf6",
456 + "instruments": "#f59e0b"
457 };
439 - return colors[area] || "#6b7280"; // gray for unknown
458 + return colors[area] || "#6c757d";
459 },
460
461 copyToClipboard(text) {
462 if (navigator.clipboard && window.isSecureContext) {
463 navigator.clipboard.writeText(text).then(() => {
445 - this.showToast("Copied to clipboard!", "success");
464 + justToast("Copied to clipboard!", "success");
465 }).catch(err => {
466 console.error("Clipboard copy failed:", err);
467 this.fallbackCopyToClipboard(text);
@@ -463,10 +482,10 @@ ${memory.content_full}
482 textArea.select();
483 try {
484 document.execCommand('copy');
466 - this.showToast("Copied to clipboard!", "success");
485 + justToast("Copied to clipboard!", "success");
486 } catch (err) {
487 console.error("Fallback clipboard copy failed:", err);
469 - this.showToast("Failed to copy to clipboard", "error");
488 + justToast("Failed to copy to clipboard", "error");
489 }
490 document.body.removeChild(textArea);
491 },
@@ -487,7 +506,7 @@ ${memory.content_full}
506 });
507
508 if (response.success) {
490 - this.showToast("Memory deleted successfully", "success");
509 + justToast("Memory deleted successfully", "success");
510
511 // If we were viewing this memory in detail modal, close it
512 if (isViewingThisMemory) {
@@ -499,17 +518,17 @@ ${memory.content_full}
518 // Trigger an immediate refresh to get updated state from backend
519 await this.searchMemories(true); // silent refresh
520 } else {
502 - this.showToast(`Failed to delete memory: ${response.error}`, "error");
521 + justToast(`Failed to delete memory: ${response.error}`, "error");
522 }
523 } catch (error) {
524 console.error("Memory deletion error:", error);
506 - this.showToast("Failed to delete memory", "error");
525 + justToast("Failed to delete memory", "error");
526 }
527 },
528
529 exportMemories() {
530 if (this.memories.length === 0) {
512 - this.showToast("No memories to export", "warning");
531 + justToast("No memories to export", "warning");
532 return;
533 }
534
@@ -541,10 +560,10 @@ ${memory.content_full}
560 document.body.removeChild(a);
561 URL.revokeObjectURL(url);
562
544 - this.showToast("Memory export completed", "success");
563 + justToast("Memory export completed", "success");
564 } catch (error) {
565 console.error("Memory export error:", error);
547 - this.showToast("Failed to export memories", "error");
566 + justToast("Failed to export memories", "error");
567 }
568 },
569
@@ -574,6 +593,7 @@ ${memory.content_full}
593 this.searchQuery = "";
594 this.memories = [];
595 this.totalCount = 0;
596 + this.totalDbCount = 0;
597 this.knowledgeCount = 0;
598 this.conversationCount = 0;
599 this.areasCount = {};
@@ -581,14 +601,7 @@ ${memory.content_full}
601 this.currentPage = 1;
602 },
603
584 - showToast(message, type = "info") {
585 - // Use global toast function if available
586 - if (typeof toast === 'function') {
587 - toast(message, type);
588 - } else {
589 - console.log(`[${type.toUpperCase()}] ${message}`);
590 - }
591 - }
604 +
605 };
606
607 const store = createStore("memoryDashboardStore", memoryDashboardStore);
webui/components/settings/memory/memory-dashboard.html
+295 -216
@@ -1,29 +1,39 @@
1 -<script type="module">
2 - import { store } from "/components/settings/memory/memory-dashboard-store.js";
3 -</script>
4 -
1 +<html>
2 +<head>
3 + <title>Memory Dashboard</title>
4 + <script type="module">
5 + import { store } from "/components/settings/memory/memory-dashboard-store.js";
6 + </script>
7 +</head>
8 +<body>
9 <div x-data>
10 <template x-if="$store.memoryDashboardStore">
7 - <div x-init="$store.memoryDashboardStore.initialize()" class="memory-dashboard">
8 -
9 - <!-- Header with title and statistics -->
10 - <div class="dashboard-header">
11 - <h3>Memory Dashboard</h3>
12 - <div class="memory-stats" x-show="!$store.memoryDashboardStore.loading">
13 - <div class="stat-item">
14 - <span class="stat-label">Total:</span>
15 - <span class="stat-value" x-text="$store.memoryDashboardStore.totalCount"></span>
16 - </div>
17 - <div class="stat-item">
18 - <span class="stat-label">Knowledge:</span>
19 - <span class="stat-value" x-text="$store.memoryDashboardStore.knowledgeCount"></span>
20 - </div>
21 - <div class="stat-item">
22 - <span class="stat-label">Conversation:</span>
23 - <span class="stat-value" x-text="$store.memoryDashboardStore.conversationCount"></span>
24 - </div>
25 - </div>
26 - </div>
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 +
36 +
37
38 <!-- Search and Filters -->
39 <div class="filters-section">
@@ -45,10 +55,10 @@
55 <label for="area-filter">Area:</label>
56 <select id="area-filter" x-model="$store.memoryDashboardStore.areaFilter">
57 <option value="">All Areas</option>
48 - <option value="MAIN">Main</option>
49 - <option value="FRAGMENTS">Fragments</option>
50 - <option value="SOLUTIONS">Solutions</option>
51 - <option value="INSTRUMENTS">Instruments</option>
58 + <option value="main">Main</option>
59 + <option value="fragments">Fragments</option>
60 + <option value="solutions">Solutions</option>
61 + <option value="instruments">Instruments</option>
62 </select>
63 </div>
64
@@ -95,33 +105,55 @@
105 <div x-show="!$store.memoryDashboardStore.loading && !$store.memoryDashboardStore.error"
106 class="memory-table-container">
107
98 - <!-- Pagination controls at top -->
99 - <div class="pagination-controls-top" x-show="$store.memoryDashboardStore.totalPages > 1">
100 - <div class="pagination-info-inline">
101 - <span>Page <span x-text="$store.memoryDashboardStore.currentPage"></span>
102 - of <span x-text="$store.memoryDashboardStore.totalPages"></span>
103 - (<span x-text="$store.memoryDashboardStore.memories.length"></span> total memories)
104 - </span>
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>
128 </div>
129
107 - <div class="pagination-controls">
108 - <button class="btn slim" @click="$store.memoryDashboardStore.prevPage()"
109 - :disabled="$store.memoryDashboardStore.currentPage === 1">
110 - Previous
111 - </button>
112 -
113 - <select class="page-select"
114 - x-model.number="$store.memoryDashboardStore.currentPage"
115 - @change="$store.memoryDashboardStore.goToPage($store.memoryDashboardStore.currentPage)">
116 - <template x-for="page in Array.from({length: $store.memoryDashboardStore.totalPages}, (_, i) => i + 1)" :key="page">
117 - <option :value="page" x-text="'Page ' + page"></option>
118 - </template>
119 - </select>
120 -
121 - <button class="btn slim" @click="$store.memoryDashboardStore.nextPage()"
122 - :disabled="$store.memoryDashboardStore.currentPage === $store.memoryDashboardStore.totalPages">
123 - Next
124 - </button>
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>
157 </div>
158 </div>
159
@@ -186,7 +218,7 @@
218 </th>
219 <th class="col-metadata">Metadata</th>
220 <th class="col-preview">Preview</th>
189 - <th class="col-actions">Actions</th>
221 + <th class="col-actions"></th>
222 </tr>
223 </thead>
224 <tbody>
@@ -204,9 +236,9 @@
236 <td class="metadata-cell">
237 <div class="metadata-info">
238 <div class="metadata-row">
207 - <span class="area-badge"
208 - :style="`background-color: ${$store.memoryDashboardStore.getAreaColor(memory.area)}`"
209 - x-text="memory.area"></span>
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)"
@@ -214,10 +246,10 @@
246 </div>
247 <div class="metadata-row">
248 <template x-if="memory.knowledge_source">
217 - <span class="source-type knowledge">📚 Knowledge</span>
249 + <span class="source-type knowledge">Knowledge</span>
250 </template>
251 <template x-if="!memory.knowledge_source">
220 - <span class="source-type conversation">💬 Conversation</span>
252 + <span class="source-type conversation">Conversation</span>
253 </template>
254 </div>
255 </div>
@@ -235,58 +267,32 @@
267
268 <!-- Actions -->
269 <td class="actions-cell" @click.stop>
238 - <button class="btn-action copy" @click="$store.memoryDashboardStore.copyToClipboard($store.memoryDashboardStore.formatMemoryForCopy(memory))"
239 - title="Copy Memory">
240 - <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
241 - <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
242 - <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
243 - </svg>
244 - </button>
245 -
246 - <button class="btn-action delete" @click="$store.memoryDashboardStore.deleteMemory(memory)"
247 - title="Delete Memory">
248 - <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
249 - <polyline points="3,6 5,6 21,6"></polyline>
250 - <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>
251 - <line x1="10" y1="11" x2="10" y2="17"></line>
252 - <line x1="14" y1="11" x2="14" y2="17"></line>
253 - </svg>
254 - </button>
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>
278 +
279 + <button class="btn-action delete" @click="$store.memoryDashboardStore.deleteMemory(memory)"
280 + 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>
294
261 - <!-- Pagination controls at bottom -->
262 - <div class="pagination-controls-bottom" x-show="$store.memoryDashboardStore.totalPages > 1">
263 - <div class="pagination-info-inline">
264 - <span>Page <span x-text="$store.memoryDashboardStore.currentPage"></span>
265 - of <span x-text="$store.memoryDashboardStore.totalPages"></span>
266 - (<span x-text="$store.memoryDashboardStore.memories.length"></span> total memories)
267 - </span>
268 - </div>
295
270 - <div class="pagination-controls">
271 - <button class="btn slim" @click="$store.memoryDashboardStore.prevPage()"
272 - :disabled="$store.memoryDashboardStore.currentPage === 1">
273 - Previous
274 - </button>
275 -
276 - <select class="page-select"
277 - x-model.number="$store.memoryDashboardStore.currentPage"
278 - @change="$store.memoryDashboardStore.goToPage($store.memoryDashboardStore.currentPage)">
279 - <template x-for="page in Array.from({length: $store.memoryDashboardStore.totalPages}, (_, i) => i + 1)" :key="page">
280 - <option :value="page" x-text="'Page ' + page"></option>
281 - </template>
282 - </select>
283 -
284 - <button class="btn slim" @click="$store.memoryDashboardStore.nextPage()"
285 - :disabled="$store.memoryDashboardStore.currentPage === $store.memoryDashboardStore.totalPages">
286 - Next
287 - </button>
288 - </div>
289 - </div>
296
297 </div>
298
@@ -328,18 +334,38 @@
334 color: var(--color-text);
335 }
336
331 - .dashboard-header {
337 + .memory-dashboard h3 {
338 + color: var(--color-text);
339 + margin: 0;
340 + font-size: 1.5rem;
341 + font-weight: 600;
342 + }
343 +
344 +
345 +
346 + .stats-pagination-header {
347 display: flex;
348 justify-content: space-between;
349 align-items: center;
335 - margin-bottom: 1.5rem;
336 - padding-bottom: 1rem;
337 - border-bottom: 1px solid var(--color-border);
350 + padding: 1rem;
351 + background: var(--color-panel);
352 + border: 1px solid var(--color-border);
353 + border-radius: 8px;
354 + border-bottom: none;
355 + border-bottom-left-radius: 0;
356 + border-bottom-right-radius: 0;
357 }
358
340 - .memory-stats {
359 + .memory-stats-compact {
360 display: flex;
361 gap: 1rem;
362 + flex-wrap: wrap;
363 + }
364 +
365 + .pagination-controls-compact {
366 + display: flex;
367 + align-items: center;
368 + gap: 1rem;
369 }
370
371 .stat-item {
@@ -355,10 +381,15 @@
381
382 .stat-label {
383 font-size: 0.8rem;
358 - color: var(--color-secondary);
384 + color: var(--color-text);
385 + opacity: 0.7;
386 margin-bottom: 0.25rem;
387 }
388
389 + .light-mode .stat-label {
390 + opacity: 0.8;
391 + }
392 +
393 .stat-value {
394 font-size: 1.1rem;
395 font-weight: bold;
@@ -407,7 +438,13 @@
438 .filter-group input:focus, .filter-group select:focus {
439 outline: none;
440 border-color: var(--color-primary);
410 - box-shadow: 0 0 0 2px rgba(115, 122, 129, 0.1);
441 + box-shadow: 0 0 0 2px rgba(115, 122, 129, 0.2);
442 + background: var(--color-input-focus);
443 + }
444 +
445 + .light-mode .filter-group input:focus,
446 + .light-mode .filter-group select:focus {
447 + box-shadow: 0 0 0 2px rgba(56, 70, 83, 0.1);
448 }
449
450 .filter-actions {
@@ -425,14 +462,16 @@
462
463 .loading-text {
464 font-size: 0.85rem;
428 - color: var(--color-secondary);
465 + color: var(--color-text);
466 + opacity: 0.7;
467 font-style: italic;
468 }
469
470 .loading-state, .error-state, .no-memories, .init-message {
471 text-align: center;
472 padding: 2rem;
435 - color: var(--color-secondary);
473 + color: var(--color-text);
474 + opacity: 0.8;
475 background: var(--color-panel);
476 border: 1px solid var(--color-border);
477 border-radius: 8px;
@@ -441,24 +480,41 @@
480
481 .init-message {
482 color: var(--color-primary);
444 - background: rgba(115, 122, 129, 0.1);
483 + background: rgba(115, 122, 129, 0.2);
484 border-color: var(--color-primary);
485 }
486
487 + .light-mode .init-message {
488 + background: rgba(56, 70, 83, 0.1);
489 + }
490 +
491 .error-state {
492 color: var(--color-accent);
450 - background: rgba(207, 102, 121, 0.1);
493 + background: rgba(207, 102, 121, 0.2);
494 border-color: var(--color-accent);
495 }
496
497 + .light-mode .error-state {
498 + background: rgba(176, 0, 32, 0.1);
499 + }
500 +
501 .loading-spinner {
455 - width: 20px;
456 - height: 20px;
457 - border: 2px solid var(--color-border);
458 - border-left-color: var(--color-primary);
502 + width: 24px;
503 + height: 24px;
504 + min-width: 24px;
505 + min-height: 24px;
506 + max-width: 24px;
507 + max-height: 24px;
508 + border: 3px solid var(--color-border);
509 + border-top-color: var(--color-primary);
510 border-radius: 50%;
511 animation: spin 1s linear infinite;
512 margin: 0 auto 0.5rem;
513 + flex-shrink: 0;
514 + flex-grow: 0;
515 + display: inline-block;
516 + position: relative;
517 + box-sizing: border-box;
518 }
519
520 @keyframes spin {
@@ -487,8 +543,8 @@
543 /* Fixed column widths for proper fit */
544 .col-select { width: 5%; }
545 .col-metadata { width: 25%; }
490 - .col-preview { width: 55%; }
491 - .col-actions { width: 15%; }
546 + .col-preview { width: 60%; }
547 + .col-actions { width: 10%; }
548
549 .memory-table th,
550 .memory-table td {
@@ -544,12 +600,12 @@
600
601 /* Selected row styling */
602 .memory-row.selected {
547 - background: rgba(115, 122, 129, 0.1);
603 + background: rgba(115, 122, 129, 0.2);
604 border-left: 3px solid var(--color-primary);
605 }
606
607 .light-mode .memory-row.selected {
552 - background: rgba(115, 122, 129, 0.05);
608 + background: rgba(56, 70, 83, 0.1);
609 }
610
611 /* Mass action toolbar */
@@ -558,7 +614,7 @@
614 justify-content: space-between;
615 align-items: center;
616 padding: 1rem;
561 - background: rgba(115, 122, 129, 0.1);
617 + background: rgba(115, 122, 129, 0.2);
618 border: 1px solid var(--color-primary);
619 border-radius: 8px;
620 margin: 1rem 0;
@@ -566,7 +622,7 @@
622 }
623
624 .light-mode .mass-action-toolbar {
569 - background: rgba(115, 122, 129, 0.05);
625 + background: rgba(56, 70, 83, 0.1);
626 }
627
628 .selection-info {
@@ -596,27 +652,27 @@
652
653 .btn-mass:hover {
654 border-color: var(--color-primary);
599 - background: rgba(115, 122, 129, 0.1);
655 + background: var(--color-panel);
656 }
657
658 .btn-mass.copy:hover {
603 - border-color: #4caf50;
604 - color: #4caf50;
659 + border-color: var(--color-primary);
660 + color: var(--color-primary);
661 }
662
663 .btn-mass.export:hover {
608 - border-color: #2196f3;
609 - color: #2196f3;
664 + border-color: var(--color-primary);
665 + color: var(--color-primary);
666 }
667
668 .btn-mass.delete:hover {
613 - border-color: #f44336;
614 - color: #f44336;
669 + border-color: var(--color-accent);
670 + color: var(--color-accent);
671 }
672
673 .btn-mass.clear:hover {
618 - border-color: #ff9800;
619 - color: #ff9800;
674 + border-color: var(--color-primary);
675 + color: var(--color-primary);
676 }
677
678 @keyframes slideDown {
@@ -650,7 +706,7 @@
706 }
707
708 .memory-table tbody tr:hover {
653 - background: rgba(255, 255, 255, 0.1);
709 + background: var(--color-panel);
710 }
711
712 .memory-table tbody tr:last-child {
@@ -663,12 +719,19 @@
719 color: white;
720 font-size: 0.75rem;
721 font-weight: bold;
666 - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
722 + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
723 + border: 1px solid rgba(255, 255, 255, 0.1);
724 + }
725 +
726 + .light-mode .area-badge {
727 + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
728 + border: 1px solid rgba(0, 0, 0, 0.1);
729 }
730
731 .timestamp-cell {
732 font-size: 0.85rem;
671 - color: var(--color-secondary);
733 + color: var(--color-text);
734 + opacity: 0.7;
735 white-space: normal;
736 word-break: break-word;
737 font-family: monospace;
@@ -694,20 +757,33 @@
757 }
758
759 .source-type.knowledge {
697 - background: rgba(115, 122, 129, 0.1);
760 + background: rgba(115, 122, 129, 0.2);
761 color: var(--color-primary);
762 border-color: var(--color-primary);
763 }
764
765 + .light-mode .source-type.knowledge {
766 + background: rgba(56, 70, 83, 0.1);
767 + color: var(--color-primary);
768 + }
769 +
770 .source-type.conversation {
703 - background: rgba(101, 101, 101, 0.1);
704 - color: var(--color-secondary);
705 - border-color: var(--color-secondary);
771 + background: rgba(101, 101, 101, 0.2);
772 + color: var(--color-text);
773 + opacity: 0.8;
774 + border-color: var(--color-border);
775 + }
776 +
777 + .light-mode .source-type.conversation {
778 + background: rgba(232, 234, 246, 0.5);
779 + color: var(--color-text);
780 + opacity: 0.9;
781 }
782
783 .source-file {
784 font-size: 0.7rem;
710 - color: var(--color-secondary);
785 + color: var(--color-text);
786 + opacity: 0.7;
787 font-family: monospace;
788 background: var(--color-panel);
789 padding: 0.2rem 0.4rem;
@@ -743,14 +819,35 @@
819 padding: 0.2rem 0.4rem;
820 border-radius: 6px;
821 font-size: 0.7rem;
746 - color: var(--color-secondary);
822 + color: var(--color-text);
823 + opacity: 0.8;
824 }
825
826 .actions-cell {
750 - white-space: nowrap;
751 - text-align: center;
752 - min-width: 140px;
753 - padding: 0.5rem 0.25rem !important;
827 + min-width: 60px;
828 + width: 60px;
829 + padding: 0 !important;
830 + height: 60px;
831 + position: relative;
832 + }
833 +
834 + .actions-wrapper {
835 + position: absolute;
836 + top: 0;
837 + left: 0;
838 + right: 0;
839 + bottom: 0;
840 + display: flex;
841 + flex-direction: column;
842 + align-items: center;
843 + justify-content: center;
844 + }
845 +
846 + .actions-cell .btn-action {
847 + display: block;
848 + margin: 0.1rem 0;
849 + width: 28px;
850 + height: 28px;
851 }
852
853 .btn-action {
@@ -760,7 +857,8 @@
857 margin: 0 0.1rem;
858 cursor: pointer;
859 border-radius: 6px;
763 - color: var(--color-secondary);
860 + color: var(--color-text);
861 + opacity: 0.7;
862 transition: all 0.2s ease;
863 display: inline-flex;
864 align-items: center;
@@ -770,56 +868,43 @@
868 }
869
870 .btn-action:hover {
773 - background: rgba(0, 0, 0, 0.05);
774 - border-color: rgba(0, 0, 0, 0.2);
775 - color: var(--color-text);
871 + opacity: 1;
872 + background: var(--color-panel);
873 + border-color: var(--color-primary);
874 + color: var(--color-primary);
875 transform: translateY(-1px);
876 }
877
878 .btn-action.view:hover {
780 - background: rgba(33, 150, 243, 0.1);
781 - border-color: #2196F3;
782 - color: #2196F3;
879 + background: var(--color-panel);
880 + border-color: var(--color-primary);
881 + color: var(--color-primary);
882 }
883
884 .btn-action.copy:hover {
786 - background: rgba(76, 175, 80, 0.1);
787 - border-color: #4CAF50;
788 - color: #4CAF50;
885 + background: var(--color-panel);
886 + border-color: var(--color-primary);
887 + color: var(--color-primary);
888 }
889
890 .btn-action.delete:hover {
792 - background: rgba(244, 67, 54, 0.1);
793 - border-color: #f44336;
794 - color: #f44336;
795 - }
796 -
797 - .pagination-controls-top {
798 - display: flex;
799 - justify-content: space-between;
800 - align-items: center;
801 - margin-bottom: 1rem;
802 - padding: 1rem;
891 background: var(--color-panel);
804 - border: 1px solid var(--color-border);
805 - border-radius: 8px;
892 + border-color: var(--color-accent);
893 + color: var(--color-accent);
894 }
895
896 +
897 +
898 .pagination-info-inline {
809 - color: var(--color-secondary);
899 + color: var(--color-text);
900 + opacity: 0.7;
901 font-size: 0.9rem;
902 }
903
904 .pagination-controls {
905 display: flex;
815 - justify-content: center;
906 align-items: center;
907 gap: 0.5rem;
818 - margin: 1rem 0;
819 - padding: 1rem;
820 - background: var(--color-panel);
821 - border: 1px solid var(--color-border);
822 - border-radius: 8px;
908 }
909
910 .page-select {
@@ -836,22 +921,15 @@
921 .page-select:focus {
922 outline: none;
923 border-color: var(--color-primary);
839 - box-shadow: 0 0 0 2px rgba(115, 122, 129, 0.1);
924 + box-shadow: 0 0 0 2px rgba(115, 122, 129, 0.2);
925 + background: var(--color-input-focus);
926 }
927
842 - .pagination-controls-top, .pagination-controls-bottom {
843 - display: flex;
844 - justify-content: space-between;
845 - align-items: center;
846 - padding: 1rem;
847 - background: var(--color-panel);
848 - border-bottom: 1px solid var(--color-border);
928 + .light-mode .page-select:focus {
929 + box-shadow: 0 0 0 2px rgba(56, 70, 83, 0.1);
930 }
931
851 - .pagination-controls-bottom {
852 - border-top: 1px solid var(--color-border);
853 - border-bottom: none;
854 - }
932 +
933
934 .export-section {
935 text-align: center;
@@ -979,7 +1057,8 @@
1057 .timestamp-badge {
1058 background: var(--color-panel);
1059 border: 1px solid var(--color-border);
982 - color: var(--color-secondary);
1060 + color: var(--color-text);
1061 + opacity: 0.8;
1062 padding: 0.25rem 0.75rem;
1063 border-radius: 12px;
1064 font-size: 0.8rem;
@@ -1002,8 +1081,9 @@
1081
1082 .source-badge.conversation {
1083 background: rgba(101, 101, 101, 0.1);
1005 - color: var(--color-secondary);
1006 - border-color: var(--color-secondary);
1084 + color: var(--color-text);
1085 + opacity: 0.8;
1086 + border-color: var(--color-border);
1087 }
1088
1089 .header-actions {
@@ -1189,17 +1269,22 @@
1269 padding: 0.5rem;
1270 }
1271
1192 - .dashboard-header {
1272 + .stats-pagination-header {
1273 flex-direction: column;
1274 align-items: flex-start;
1275 gap: 1rem;
1276 }
1277
1198 - .memory-stats {
1278 + .memory-stats-compact {
1279 width: 100%;
1280 justify-content: space-between;
1281 }
1282
1283 + .pagination-controls-compact {
1284 + align-self: stretch;
1285 + justify-content: center;
1286 + }
1287 +
1288 .filter-row {
1289 flex-direction: column;
1290 align-items: stretch;
@@ -1217,14 +1302,14 @@
1302 }
1303
1304 .actions-cell {
1220 - min-width: 120px;
1305 + min-width: 60px;
1306 + width: 60px;
1307 }
1308
1223 - .col-area { width: 15%; }
1224 - .col-timestamp { width: 15%; }
1225 - .col-source { width: 25%; }
1226 - .col-preview { width: 25%; }
1227 - .col-actions { width: 20%; }
1309 + .col-select { width: 8%; }
1310 + .col-metadata { width: 27%; }
1311 + .col-preview { width: 55%; }
1312 + .col-actions { width: 10%; }
1313
1314 .memory-detail-modal {
1315 width: 98%;
@@ -1258,20 +1343,10 @@
1343 padding: 1rem;
1344 }
1345
1261 - .pagination-controls-top {
1262 - flex-direction: column;
1263 - gap: 1rem;
1264 - align-items: stretch;
1265 - }
1346
1267 - .pagination-controls {
1268 - flex-wrap: wrap;
1269 - justify-content: center;
1270 - }
1347
1272 - .page-numbers {
1348 + .pagination-controls {
1349 flex-wrap: wrap;
1274 - justify-content: center;
1350 }
1351 }
1352
@@ -1324,8 +1399,9 @@
1399
1400 .source-badge.conversation {
1401 background: rgba(101, 101, 101, 0.1);
1327 - color: var(--color-secondary);
1328 - border-color: var(--color-secondary);
1402 + color: var(--color-text);
1403 + opacity: 0.8;
1404 + border-color: var(--color-border);
1405 }
1406
1407 .header-actions {
@@ -1423,7 +1499,8 @@
1499
1500 .metadata-group h5 {
1501 margin: 0 0 0.75rem 0;
1426 - color: var(--color-secondary);
1502 + color: var(--color-text);
1503 + opacity: 0.7;
1504 font-size: 0.9rem;
1505 font-weight: 600;
1506 text-transform: uppercase;
@@ -1444,7 +1521,8 @@
1521 .metadata-label {
1522 font-size: 0.8rem;
1523 font-weight: 600;
1447 - color: var(--color-secondary);
1524 + color: var(--color-text);
1525 + opacity: 0.7;
1526 text-transform: uppercase;
1527 letter-spacing: 0.3px;
1528 }
@@ -1528,4 +1606,5 @@
1606
1607 </template>
1608 </div>
1531 -</div>
1609 +</body>
1610 +</html>
webui/components/settings/memory/memory-detail-modal.html
+256 -8
@@ -1,14 +1,261 @@
1 -<div x-data>
2 - <template x-if="$store.memoryDashboardStore && $store.memoryDashboardStore.detailMemory">
3 - <div>
1 +<html>
2 +
3 +<head>
4 + <title>Memory Details</title>
5 +</head>
6 +
7 +<style>
8 + /* Memory Detail Modal Styles */
9 + .modal-header-enhanced {
10 + display: flex;
11 + justify-content: space-between;
12 + align-items: flex-start;
13 + padding: 1.5rem;
14 + border-bottom: 2px solid var(--color-border);
15 + background: var(--color-panel);
16 + }
17 +
18 + .memory-title {
19 + display: flex;
20 + align-items: center;
21 + gap: 1rem;
22 + margin-bottom: 0.5rem;
23 + }
24 +
25 + .area-badge-large {
26 + color: white;
27 + padding: 0.5rem 1rem;
28 + border-radius: 20px;
29 + font-size: 0.8rem;
30 + font-weight: bold;
31 + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
32 + }
33 +
34 + .memory-meta-quick {
35 + display: flex;
36 + gap: 1rem;
37 + align-items: center;
38 + }
39 +
40 + .timestamp-badge, .source-badge {
41 + padding: 0.25rem 0.75rem;
42 + border-radius: 12px;
43 + font-size: 0.75rem;
44 + font-weight: 500;
45 + background: var(--color-panel);
46 + border: 1px solid var(--color-border);
47 + color: var(--color-text);
48 + opacity: 0.8;
49 + }
50 +
51 + .source-badge.knowledge {
52 + background: rgba(115, 122, 129, 0.1);
53 + color: var(--color-primary);
54 + border-color: var(--color-primary);
55 + opacity: 1;
56 + }
57 +
58 + .source-badge.conversation {
59 + background: rgba(101, 101, 101, 0.1);
60 + color: var(--color-text);
61 + opacity: 0.8;
62 + border-color: var(--color-border);
63 + }
64 +
65 + .header-actions {
66 + display: flex;
67 + gap: 0.5rem;
68 + }
69 +
70 + .btn-action-header {
71 + background: var(--color-background);
72 + border: 1px solid var(--color-border);
73 + color: var(--color-text);
74 + padding: 0.5rem;
75 + border-radius: 8px;
76 + cursor: pointer;
77 + transition: all 0.2s ease;
78 + }
79 +
80 + .btn-action-header:hover {
81 + border-color: var(--color-primary);
82 + background: var(--color-panel);
83 + }
84 +
85 + .modal-body-enhanced {
86 + display: flex;
87 + min-height: 60vh;
88 + max-height: 70vh;
89 + }
90 +
91 + .content-section-main {
92 + flex: 1;
93 + padding: 1.5rem;
94 + overflow-y: auto;
95 + }
96 +
97 + .content-block {
98 + margin-bottom: 2rem;
99 + }
100 +
101 + .content-block h4 {
102 + margin: 0 0 1rem 0;
103 + color: var(--color-text);
104 + font-size: 1.1rem;
105 + font-weight: 600;
106 + border-bottom: 1px solid var(--color-border);
107 + padding-bottom: 0.5rem;
108 + }
109 +
110 + .memory-content-display {
111 + background: var(--color-panel);
112 + border: 1px solid var(--color-border);
113 + border-radius: 8px;
114 + padding: 1.5rem;
115 + line-height: 1.6;
116 + color: var(--color-text);
117 + white-space: pre-wrap;
118 + word-break: break-word;
119 + max-height: 300px;
120 + overflow-y: auto;
121 + }
122 +
123 + .tags-display {
124 + display: flex;
125 + flex-wrap: wrap;
126 + gap: 0.5rem;
127 + }
128 +
129 + .tag-display {
130 + background: var(--color-primary);
131 + color: white;
132 + padding: 0.25rem 0.75rem;
133 + border-radius: 12px;
134 + font-size: 0.75rem;
135 + font-weight: 500;
136 + }
137 +
138 + .metadata-sidebar {
139 + min-width: 300px;
140 + max-width: 350px;
141 + background: var(--color-panel);
142 + border-left: 1px solid var(--color-border);
143 + padding: 1.5rem;
144 + overflow-y: auto;
145 + }
146 +
147 + .metadata-sidebar h4 {
148 + margin: 0 0 1rem 0;
149 + color: var(--color-text);
150 + font-size: 1.1rem;
151 + font-weight: 600;
152 + }
153 +
154 + .metadata-group {
155 + margin-bottom: 1.5rem;
156 + }
157 +
158 + .metadata-group h5 {
159 + margin: 0 0 0.75rem 0;
160 + color: var(--color-text);
161 + opacity: 0.7;
162 + font-size: 0.9rem;
163 + font-weight: 600;
164 + text-transform: uppercase;
165 + letter-spacing: 0.5px;
166 + }
167 +
168 + .metadata-item {
169 + display: flex;
170 + flex-direction: column;
171 + gap: 0.25rem;
172 + margin-bottom: 0.75rem;
173 + padding: 0.5rem;
174 + background: var(--color-background);
175 + border: 1px solid var(--color-border);
176 + border-radius: 6px;
177 + }
178 +
179 + .metadata-label {
180 + font-size: 0.8rem;
181 + font-weight: 600;
182 + color: var(--color-text);
183 + opacity: 0.7;
184 + text-transform: uppercase;
185 + letter-spacing: 0.3px;
186 + }
187 +
188 + .metadata-value {
189 + font-size: 0.9rem;
190 + color: var(--color-text);
191 + word-break: break-all;
192 + line-height: 1.4;
193 + }
194 +
195 + .source-file-display {
196 + font-family: monospace;
197 + background: var(--color-panel);
198 + padding: 0.5rem;
199 + border-radius: 4px;
200 + border: 1px solid var(--color-border);
201 + }
202 +
203 + .metadata-actions {
204 + display: flex;
205 + flex-direction: column;
206 + gap: 0.5rem;
207 + }
208 +
209 + .metadata-actions .btn {
210 + display: flex;
211 + align-items: center;
212 + gap: 0.5rem;
213 + justify-content: flex-start;
214 + font-size: 0.9rem;
215 + }
216 +
217 + /* Responsive design for modal */
218 + @media (max-width: 768px) {
219 + .modal-body-enhanced {
220 + flex-direction: column;
221 + max-height: 80vh;
222 + }
223 +
224 + .metadata-sidebar {
225 + min-width: unset;
226 + max-width: unset;
227 + border-left: none;
228 + border-top: 1px solid var(--color-border);
229 + }
230 +
231 + .modal-header-enhanced {
232 + padding: 1rem;
233 + }
234 +
235 + .memory-title {
236 + flex-direction: column;
237 + align-items: flex-start;
238 + gap: 0.5rem;
239 + }
240 +
241 + .content-section-main,
242 + .metadata-sidebar {
243 + padding: 1rem;
244 + }
245 + }
246 +</style>
247 +
248 +<body>
249 + <div x-data>
250 + <template x-if="$store.memoryDashboardStore && $store.memoryDashboardStore.detailMemory">
251 + <div>
252 <!-- Modal Header with Memory Info -->
253 <div class="modal-header-enhanced">
254 <div class="header-left">
255 <div class="memory-title">
256 <span class="area-badge-large"
9 - :style="`background: linear-gradient(135deg, ${$store.memoryDashboardStore.getAreaColor($store.memoryDashboardStore.detailMemory?.area)} 0%, ${$store.memoryDashboardStore.getAreaColor($store.memoryDashboardStore.detailMemory?.area)}cc 100%)`"
257 + :style="`background-color: ${$store.memoryDashboardStore.getAreaColor($store.memoryDashboardStore.detailMemory?.area)}`"
258 x-text="$store.memoryDashboardStore.detailMemory?.area?.toUpperCase() || 'UNKNOWN'"></span>
11 - <h3>Memory Details</h3>
259 </div>
260 <div class="memory-meta-quick">
261 <span class="timestamp-badge" x-text="$store.memoryDashboardStore.formatTimestamp($store.memoryDashboardStore.detailMemory?.timestamp, true)"></span>
@@ -34,7 +281,7 @@
281 <div class="content-section-main">
282 <div class="content-block">
283 <h4>Memory Content</h4>
37 - <div class="memory-content-display" x-html="$store.memoryDashboardStore.detailMemory?.content_full?.replace(/\n/g, '<br>')"></div>
284 + <div class="memory-content-display" x-text="$store.memoryDashboardStore.detailMemory?.content_full"></div>
285 </div>
286
287 <!-- Tags Section -->
@@ -133,5 +380,6 @@
380 </div>
381 </template>
382 </div>
136 -</template>
137 -</div>
383 +</body>
384 +
385 +</html>
webui/index.html
+1 -1
@@ -1606,7 +1606,7 @@
1606 <script>
1607 if ('serviceWorker' in navigator) {
1608 window.addEventListener('load', () => {
1609 - navigator.service-worker.register('js/sw.js').then(registration => {
1609 + navigator.serviceWorker.register('js/sw.js').then(registration => {
1610 console.log('SW registered: ', registration);
1611 }).catch(registrationError => {
1612 console.log('SW registration failed: ', registrationError);