feat: memory dashboard first version

Rafael Uzarowski committed Jul 25, 2025 at 18:15 UTC 350d9bb7b97c20a950f8d2551e48bb7bef09d110
6 files changed +1620
python/api/memory_dashboard.py new
+128
@@ -0,0 +1,128 @@
1 +from python.helpers.api import ApiHandler, Request, Response
2 +from python.helpers.memory import Memory
3 +
4 +
5 +class MemoryDashboard(ApiHandler):
6 +
7 + async def process(self, input: dict, request: Request) -> dict | Response:
8 + try:
9 + # Get filter parameters
10 + area_filter = input.get("area", "") # Filter by memory area (MAIN, FRAGMENTS, SOLUTIONS, INSTRUMENTS)
11 + search_query = input.get("search", "") # Full-text search query
12 + limit = input.get("limit", 100) # Number of results to return
13 +
14 + # Get context and agent
15 + ctxid = input.get("context", "")
16 + context = self.get_context(ctxid)
17 +
18 + # Check if memory is already initialized to avoid triggering preload
19 + memory_subdir = context.agent0.config.memory_subdir or "default"
20 + if Memory.index.get(memory_subdir) is None:
21 + # Memory not initialized yet, return empty results
22 + return {
23 + "success": True,
24 + "memories": [],
25 + "total_count": 0,
26 + "knowledge_count": 0,
27 + "conversation_count": 0,
28 + "areas_count": {},
29 + "search_query": search_query,
30 + "area_filter": area_filter,
31 + "message": "Memory database not yet initialized. Use the agent first to initialize memory."
32 + }
33 +
34 + # Get already initialized memory instance (no initialization triggered)
35 + db = Memory(
36 + agent=context.agent0,
37 + db=Memory.index[memory_subdir],
38 + memory_subdir=memory_subdir,
39 + )
40 +
41 + memories = []
42 +
43 + if search_query:
44 + # If search query provided, use similarity search
45 + threshold = 0.6 # Lower threshold for broader search in dashboard
46 + filter_expr = f"area == '{area_filter}'" if area_filter else ""
47 +
48 + docs = await db.search_similarity_threshold(
49 + query=search_query,
50 + limit=limit,
51 + threshold=threshold,
52 + filter=filter_expr
53 + )
54 + memories = docs
55 + else:
56 + # If no search query, get all memories from specified area(s)
57 + all_docs = db.db.get_all_docs()
58 +
59 + for doc_id, doc in all_docs.items():
60 + # Apply area filter if specified
61 + if area_filter and doc.metadata.get("area", "") != area_filter:
62 + continue
63 +
64 + memories.append(doc)
65 +
66 + # Apply limit
67 + if len(memories) >= limit:
68 + break
69 +
70 + # Format memories for the dashboard
71 + formatted_memories = []
72 + for memory in memories:
73 + metadata = memory.metadata
74 +
75 + # Extract key information
76 + memory_data = {
77 + "id": metadata.get("id", "unknown"),
78 + "area": metadata.get("area", "unknown"),
79 + "timestamp": metadata.get("timestamp", "unknown"),
80 + "content_preview": memory.page_content[:200] + ("..." if len(memory.page_content) > 200 else ""),
81 + "content_full": memory.page_content,
82 + "knowledge_source": metadata.get("knowledge_source", False),
83 + "source_file": metadata.get("source_file", ""),
84 + "file_type": metadata.get("file_type", ""),
85 + "consolidation_action": metadata.get("consolidation_action", ""),
86 + "tags": metadata.get("tags", []),
87 + "metadata": metadata # Include full metadata for advanced users
88 + }
89 +
90 + formatted_memories.append(memory_data)
91 +
92 + # Sort by timestamp (newest first) - handle "unknown" timestamps
93 + def get_sort_key(memory):
94 + timestamp = memory["timestamp"]
95 + if timestamp == "unknown" or not timestamp:
96 + return "0000-00-00 00:00:00" # Put unknown timestamps at the end
97 + return timestamp
98 +
99 + formatted_memories.sort(key=get_sort_key, reverse=True)
100 +
101 + # Get summary statistics
102 + total_memories = len(formatted_memories)
103 + knowledge_count = sum(1 for m in formatted_memories if m["knowledge_source"])
104 + conversation_count = total_memories - knowledge_count
105 +
106 + areas_count: dict[str, int] = {}
107 + for memory in formatted_memories:
108 + area = memory["area"]
109 + areas_count[area] = areas_count.get(area, 0) + 1
110 +
111 + return {
112 + "success": True,
113 + "memories": formatted_memories,
114 + "total_count": total_memories,
115 + "knowledge_count": knowledge_count,
116 + "conversation_count": conversation_count,
117 + "areas_count": areas_count,
118 + "search_query": search_query,
119 + "area_filter": area_filter
120 + }
121 +
122 + except Exception as e:
123 + return {
124 + "success": False,
125 + "error": str(e),
126 + "memories": [],
127 + "total_count": 0
128 + }
python/api/memory_delete.py new
+55
@@ -0,0 +1,55 @@
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/settings.py
+10
@@ -638,6 +638,16 @@ def convert_out(settings: Settings) -> SettingsOutput:
638 }
639 )
640
641 + memory_fields.append(
642 + {
643 + "id": "memory_dashboard",
644 + "title": "Memory Dashboard",
645 + "description": "View and explore all stored memories in a table format with filtering and search capabilities.",
646 + "type": "button",
647 + "value": "Open Dashboard",
648 + }
649 + )
650 +
651 memory_fields.append(
652 {
653 "id": "memory_recall_enabled",
webui/components/settings/memory/memory-dashboard-store.js new
+254
@@ -0,0 +1,254 @@
1 +import { createStore } from "/js/AlpineStore.js";
2 +import { getContext } from "/index.js";
3 +import * as API from "/js/api.js";
4 +
5 +const model = {
6 + memories: [],
7 + loading: true,
8 + error: null,
9 +
10 + // Filter states
11 + areaFilter: "",
12 + searchQuery: "",
13 + currentPage: 1,
14 + itemsPerPage: 10,
15 +
16 + // Statistics
17 + totalCount: 0,
18 + knowledgeCount: 0,
19 + conversationCount: 0,
20 + areasCount: {},
21 +
22 + // UI state
23 + selectedMemory: null,
24 + showDetailModal: false,
25 + message: null, // For displaying initialization messages
26 +
27 + async initialize() {
28 + await this.loadMemories();
29 + },
30 +
31 + async loadMemories() {
32 + this.loading = true;
33 + this.error = null;
34 + this.message = null;
35 +
36 + try {
37 + const response = await API.callJsonApi("memory_dashboard", {
38 + context: getContext(),
39 + area: this.areaFilter,
40 + search: this.searchQuery,
41 + limit: 500 // Get more for client-side pagination
42 + });
43 +
44 + if (response.success) {
45 + this.memories = response.memories || []; // Already sorted by API
46 + this.totalCount = response.total_count || 0;
47 + this.knowledgeCount = response.knowledge_count || 0;
48 + this.conversationCount = response.conversation_count || 0;
49 + this.areasCount = response.areas_count || {};
50 + this.message = response.message || null; // Handle initialization messages
51 + this.currentPage = 1; // Reset to first page when loading new data
52 + } else {
53 + this.error = response.error || "Failed to load memories";
54 + this.memories = [];
55 + this.message = null;
56 + }
57 + } catch (error) {
58 + this.error = error.message || "Failed to load memories";
59 + this.memories = [];
60 + this.message = null;
61 + console.error("Memory dashboard error:", error);
62 + } finally {
63 + this.loading = false;
64 + }
65 + },
66 +
67 + async applyFilters() {
68 + await this.loadMemories();
69 + },
70 +
71 + clearFilters() {
72 + this.areaFilter = "";
73 + this.searchQuery = "";
74 + this.applyFilters();
75 + },
76 +
77 + // Pagination
78 + get totalPages() {
79 + return Math.ceil(this.memories.length / this.itemsPerPage);
80 + },
81 +
82 + get paginatedMemories() {
83 + const startIndex = (this.currentPage - 1) * this.itemsPerPage;
84 + const endIndex = startIndex + this.itemsPerPage;
85 + return this.memories.slice(startIndex, endIndex);
86 + },
87 +
88 + goToPage(page) {
89 + if (page >= 1 && page <= this.totalPages) {
90 + this.currentPage = page;
91 + }
92 + },
93 +
94 + nextPage() {
95 + if (this.currentPage < this.totalPages) {
96 + this.currentPage++;
97 + }
98 + },
99 +
100 + prevPage() {
101 + if (this.currentPage > 1) {
102 + this.currentPage--;
103 + }
104 + },
105 +
106 + // Memory details
107 + showMemoryDetails(memory) {
108 + this.selectedMemory = memory;
109 + this.showDetailModal = true;
110 + },
111 +
112 + closeDetailModal() {
113 + this.showDetailModal = false;
114 + this.selectedMemory = null;
115 + },
116 +
117 +
118 +
119 + // Utility methods
120 + formatTimestamp(timestamp, compact = false) {
121 + if (!timestamp || timestamp === "unknown") return "Unknown";
122 + try {
123 + // Parse timestamp - assume UTC if no timezone specified
124 + let date;
125 + if (timestamp.includes('T') || timestamp.includes('Z') || timestamp.includes('+')) {
126 + // ISO format with timezone info
127 + date = new Date(timestamp);
128 + } else {
129 + // Assume UTC for plain format "YYYY-MM-DD HH:MM:SS"
130 + date = new Date(timestamp + ' UTC');
131 + }
132 +
133 + // Convert to local time and format
134 + if (compact) {
135 + // Compact format for table view
136 + return date.toLocaleString(undefined, {
137 + month: '2-digit',
138 + day: '2-digit',
139 + hour: '2-digit',
140 + minute: '2-digit',
141 + hour12: false
142 + });
143 + } else {
144 + // Full format for detail view
145 + return date.toLocaleString(undefined, {
146 + year: 'numeric',
147 + month: '2-digit',
148 + day: '2-digit',
149 + hour: '2-digit',
150 + minute: '2-digit',
151 + second: '2-digit',
152 + hour12: false
153 + });
154 + }
155 + } catch {
156 + return timestamp;
157 + }
158 + },
159 +
160 + formatTags(tags) {
161 + if (!Array.isArray(tags) || tags.length === 0) return "";
162 + return tags.join(", ");
163 + },
164 +
165 + getAreaColor(area) {
166 + if (!area) return '#757575'; // Default color for undefined/null area
167 +
168 + const colors = {
169 + 'main': '#2196F3',
170 + 'fragments': '#4CAF50',
171 + 'solutions': '#FF9800',
172 + 'instruments': '#9C27B0'
173 + };
174 + return colors[area.toLowerCase()] || '#757575';
175 + },
176 +
177 + copyToClipboard(text) {
178 + navigator.clipboard.writeText(text).then(() => {
179 + // Could show a toast notification here
180 + console.log("Copied to clipboard");
181 + }).catch(err => {
182 + console.error("Failed to copy: ", err);
183 + });
184 + },
185 +
186 + async deleteMemory(memory) {
187 + // Confirm deletion
188 + const confirmMessage = `Are you sure you want to delete this memory?\n\nArea: ${memory.area}\nContent: ${memory.content_preview}`;
189 + if (!confirm(confirmMessage)) {
190 + return;
191 + }
192 +
193 + try {
194 + const response = await API.callJsonApi("memory_delete", {
195 + context: getContext(),
196 + memory_id: memory.id
197 + });
198 +
199 + if (response.success) {
200 + // Close detail modal if the deleted memory is currently shown
201 + if (this.selectedMemory && this.selectedMemory.id === memory.id) {
202 + this.closeDetailModal();
203 + }
204 +
205 + // Remove memory from local array
206 + this.memories = this.memories.filter(m => m.id !== memory.id);
207 + this.totalCount = this.memories.length;
208 +
209 + // Update statistics
210 + if (memory.knowledge_source) {
211 + this.knowledgeCount = Math.max(0, this.knowledgeCount - 1);
212 + } else {
213 + this.conversationCount = Math.max(0, this.conversationCount - 1);
214 + }
215 +
216 + // Update areas count
217 + if (this.areasCount[memory.area]) {
218 + this.areasCount[memory.area] = Math.max(0, this.areasCount[memory.area] - 1);
219 + if (this.areasCount[memory.area] === 0) {
220 + delete this.areasCount[memory.area];
221 + }
222 + }
223 +
224 + // Adjust current page if needed
225 + if (this.paginatedMemories.length === 0 && this.currentPage > 1) {
226 + this.currentPage--;
227 + }
228 +
229 + console.log("Memory deleted successfully");
230 + } else {
231 + alert("Failed to delete memory: " + (response.error || "Unknown error"));
232 + }
233 + } catch (error) {
234 + console.error("Delete memory error:", error);
235 + alert("Failed to delete memory: " + error.message);
236 + }
237 + },
238 +
239 + // Export functionality (optional)
240 + exportMemories() {
241 + const dataStr = JSON.stringify(this.memories, null, 2);
242 + const dataBlob = new Blob([dataStr], {type: 'application/json'});
243 + const url = URL.createObjectURL(dataBlob);
244 + const link = document.createElement('a');
245 + link.href = url;
246 + link.download = `memory-export-${new Date().toISOString().split('T')[0]}.json`;
247 + link.click();
248 + URL.revokeObjectURL(url);
249 + }
250 +};
251 +
252 +const store = createStore("memoryDashboardStore", model);
253 +
254 +export { store };
webui/components/settings/memory/memory-dashboard.html new
+1171
@@ -0,0 +1,1171 @@
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>
9 +
10 +<body>
11 + <div x-data>
12 + <template x-if="$store.memoryDashboardStore">
13 + <div x-init="$store.memoryDashboardStore.initialize()" class="memory-dashboard">
14 +
15 + <!-- Header with title and statistics -->
16 + <div class="dashboard-header">
17 + <h3>Memory Dashboard</h3>
18 + <div class="memory-stats" x-show="!$store.memoryDashboardStore.loading">
19 + <div class="stat-item">
20 + <span class="stat-label">Total:</span>
21 + <span class="stat-value" x-text="$store.memoryDashboardStore.totalCount"></span>
22 + </div>
23 + <div class="stat-item">
24 + <span class="stat-label">Knowledge:</span>
25 + <span class="stat-value" x-text="$store.memoryDashboardStore.knowledgeCount"></span>
26 + </div>
27 + <div class="stat-item">
28 + <span class="stat-label">Conversation:</span>
29 + <span class="stat-value" x-text="$store.memoryDashboardStore.conversationCount"></span>
30 + </div>
31 + </div>
32 + </div>
33 +
34 + <!-- Filters -->
35 + <div class="filters-section">
36 + <div class="filter-row">
37 + <div class="filter-group">
38 + <label for="area-filter">Area:</label>
39 + <select id="area-filter" x-model="$store.memoryDashboardStore.areaFilter">
40 + <option value="">All Areas</option>
41 + <option value="main">Main</option>
42 + <option value="fragments">Fragments</option>
43 + <option value="solutions">Solutions</option>
44 + <option value="instruments">Instruments</option>
45 + </select>
46 + </div>
47 +
48 + <div class="filter-group">
49 + <label for="search-input">Search:</label>
50 + <input type="text" id="search-input" x-model="$store.memoryDashboardStore.searchQuery"
51 + placeholder="Search memory content..." />
52 + </div>
53 +
54 + <div class="filter-actions">
55 + <button class="btn primary slim" @click="$store.memoryDashboardStore.applyFilters()"
56 + :disabled="$store.memoryDashboardStore.loading">
57 + Apply Filters
58 + </button>
59 + <button class="btn slim" @click="$store.memoryDashboardStore.clearFilters()">
60 + Clear
61 + </button>
62 + </div>
63 + </div>
64 + </div>
65 +
66 + <!-- Loading state -->
67 + <div x-show="$store.memoryDashboardStore.loading" class="loading-state">
68 + <div class="loading-spinner"></div>
69 + <span>Loading memories...</span>
70 + </div>
71 +
72 + <!-- Error state -->
73 + <div x-show="$store.memoryDashboardStore.error" class="error-state">
74 + <span x-text="$store.memoryDashboardStore.error"></span>
75 + </div>
76 +
77 + <!-- Memory table -->
78 + <div x-show="!$store.memoryDashboardStore.loading && !$store.memoryDashboardStore.error"
79 + class="memory-table-container">
80 +
81 + <!-- Pagination controls at top -->
82 + <div class="pagination-controls-top" x-show="$store.memoryDashboardStore.totalPages > 1">
83 + <div class="pagination-info-inline">
84 + <span>Page <span x-text="$store.memoryDashboardStore.currentPage"></span>
85 + of <span x-text="$store.memoryDashboardStore.totalPages"></span>
86 + (<span x-text="$store.memoryDashboardStore.memories.length"></span> total memories)
87 + </span>
88 + </div>
89 +
90 + <div class="pagination-controls">
91 + <button class="btn slim" @click="$store.memoryDashboardStore.prevPage()"
92 + :disabled="$store.memoryDashboardStore.currentPage === 1">
93 + Previous
94 + </button>
95 +
96 + <span class="page-numbers">
97 + <template x-for="page in Array.from({length: $store.memoryDashboardStore.totalPages}, (_, i) => i + 1)" :key="page">
98 + <button class="page-btn"
99 + @click="$store.memoryDashboardStore.goToPage(page)"
100 + :class="{'active': page === $store.memoryDashboardStore.currentPage}"
101 + x-text="page">
102 + </button>
103 + </template>
104 + </span>
105 +
106 + <button class="btn slim" @click="$store.memoryDashboardStore.nextPage()"
107 + :disabled="$store.memoryDashboardStore.currentPage === $store.memoryDashboardStore.totalPages">
108 + Next
109 + </button>
110 + </div>
111 + </div>
112 +
113 + <div class="table-wrapper">
114 + <table class="memory-table">
115 + <thead>
116 + <tr>
117 + <th class="col-area">Area</th>
118 + <th class="col-timestamp">Timestamp</th>
119 + <th class="col-source">Source</th>
120 + <th class="col-preview">Preview</th>
121 + <th class="col-actions">Actions</th>
122 + </tr>
123 + </thead>
124 + <tbody>
125 + <template x-for="memory in $store.memoryDashboardStore.paginatedMemories" :key="memory.id">
126 + <tr class="memory-row">
127 + <!-- Area with color indicator -->
128 + <td class="area-cell">
129 + <span class="area-badge"
130 + :style="`background-color: ${$store.memoryDashboardStore.getAreaColor(memory.area)}`"
131 + x-text="memory.area.toUpperCase()"></span>
132 + </td>
133 +
134 + <!-- Timestamp -->
135 + <td class="timestamp-cell">
136 + <span x-text="$store.memoryDashboardStore.formatTimestamp(memory.timestamp, true)"
137 + :title="$store.memoryDashboardStore.formatTimestamp(memory.timestamp, false)"></span>
138 + </td>
139 +
140 + <!-- Source -->
141 + <td class="source-cell">
142 + <template x-if="memory.knowledge_source">
143 + <div class="source-info">
144 + <span class="source-type knowledge">Knowledge</span>
145 + <span class="source-file" x-text="memory.source_file" x-show="memory.source_file"></span>
146 + </div>
147 + </template>
148 + <template x-if="!memory.knowledge_source">
149 + <span class="source-type conversation">Conversation</span>
150 + </template>
151 + </td>
152 +
153 + <!-- Content preview -->
154 + <td class="preview-cell">
155 + <div class="content-preview" x-text="memory.content_preview"></div>
156 + <div class="tags" x-show="memory.tags && memory.tags.length > 0">
157 + <template x-for="tag in memory.tags" :key="tag">
158 + <span class="tag" x-text="tag"></span>
159 + </template>
160 + </div>
161 + </td>
162 +
163 + <!-- Actions -->
164 + <td class="actions-cell">
165 + <button class="btn-action view" @click="$store.memoryDashboardStore.showMemoryDetails(memory)"
166 + title="View Details">
167 + <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
168 + <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
169 + <circle cx="12" cy="12" r="3"></circle>
170 + </svg>
171 + </button>
172 + <button class="btn-action copy" @click="$store.memoryDashboardStore.copyToClipboard(memory.content_full)"
173 + title="Copy Content">
174 + <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
175 + <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
176 + <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
177 + </svg>
178 + </button>
179 + <button class="btn-action delete" @click="$store.memoryDashboardStore.deleteMemory(memory)"
180 + title="Delete Memory">
181 + <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
182 + <polyline points="3,6 5,6 21,6"></polyline>
183 + <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>
184 + <line x1="10" y1="11" x2="10" y2="17"></line>
185 + <line x1="14" y1="11" x2="14" y2="17"></line>
186 + </svg>
187 + </button>
188 + </td>
189 + </tr>
190 + </template>
191 + </tbody>
192 + </table>
193 + </div>
194 +
195 + <!-- No memories message -->
196 + <div x-show="$store.memoryDashboardStore.memories.length === 0 && !$store.memoryDashboardStore.message" class="no-memories">
197 + No memories found matching the current filters.
198 + </div>
199 +
200 + <!-- Initialization message -->
201 + <div x-show="$store.memoryDashboardStore.message" class="init-message">
202 + <span x-text="$store.memoryDashboardStore.message"></span>
203 + </div>
204 +
205 +
206 +
207 + <!-- Export button -->
208 + <div class="export-section">
209 + <button class="btn slim" @click="$store.memoryDashboardStore.exportMemories()">
210 + Export Memories (JSON)
211 + </button>
212 + </div>
213 + </div>
214 +
215 + <!-- Enhanced Detail Modal -->
216 + <div x-show="$store.memoryDashboardStore.showDetailModal"
217 + x-transition:enter="modal-enter"
218 + x-transition:enter-start="modal-enter-start"
219 + x-transition:enter-end="modal-enter-end"
220 + x-transition:leave="modal-leave"
221 + x-transition:leave-start="modal-leave-start"
222 + x-transition:leave-end="modal-leave-end"
223 + class="modal-overlay"
224 + @click.self="$store.memoryDashboardStore.closeDetailModal()">
225 + <div class="modal-content memory-detail-modal" x-show="$store.memoryDashboardStore.selectedMemory" x-cloak>
226 +
227 + <!-- Modal Header with Memory Info -->
228 + <div class="modal-header-enhanced">
229 + <div class="header-left">
230 + <div class="memory-title">
231 + <span class="area-badge-large"
232 + :style="`background: linear-gradient(135deg, ${$store.memoryDashboardStore.getAreaColor($store.memoryDashboardStore.selectedMemory?.area)} 0%, ${$store.memoryDashboardStore.getAreaColor($store.memoryDashboardStore.selectedMemory?.area)}cc 100%)`"
233 + x-text="$store.memoryDashboardStore.selectedMemory?.area?.toUpperCase() || 'UNKNOWN'"></span>
234 + <h3>Memory Details</h3>
235 + </div>
236 + <div class="memory-meta-quick">
237 + <span class="timestamp-badge" x-text="$store.memoryDashboardStore.formatTimestamp($store.memoryDashboardStore.selectedMemory?.timestamp, true)"></span>
238 + <span class="source-badge"
239 + :class="$store.memoryDashboardStore.selectedMemory?.knowledge_source ? 'knowledge' : 'conversation'"
240 + x-text="$store.memoryDashboardStore.selectedMemory?.knowledge_source ? 'Knowledge' : 'Conversation'"></span>
241 + </div>
242 + </div>
243 + <div class="header-actions">
244 + <button class="btn-action-header copy" @click="$store.memoryDashboardStore.copyToClipboard($store.memoryDashboardStore.selectedMemory?.content_full)"
245 + title="Copy Full Content">
246 + <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
247 + <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
248 + <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
249 + </svg>
250 + </button>
251 + <button class="btn-action-header delete" @click="$store.memoryDashboardStore.deleteMemory($store.memoryDashboardStore.selectedMemory)"
252 + title="Delete Memory">
253 + <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
254 + <polyline points="3,6 5,6 21,6"></polyline>
255 + <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>
256 + <line x1="10" y1="11" x2="10" y2="17"></line>
257 + <line x1="14" y1="11" x2="14" y2="17"></line>
258 + </svg>
259 + </button>
260 + <button class="btn-close-enhanced" @click="$store.memoryDashboardStore.closeDetailModal()">
261 + <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
262 + <line x1="18" y1="6" x2="6" y2="18"></line>
263 + <line x1="6" y1="6" x2="18" y2="18"></line>
264 + </svg>
265 + </button>
266 + </div>
267 + </div>
268 +
269 + <!-- Modal Body with Enhanced Layout -->
270 + <div class="modal-body-enhanced">
271 +
272 + <!-- Content Section -->
273 + <div class="content-section-main">
274 + <div class="section-header">
275 + <h4>
276 + <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
277 + <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
278 + <polyline points="14,2 14,8 20,8"></polyline>
279 + </svg>
280 + Content
281 + </h4>
282 + </div>
283 + <div class="content-display" x-text="$store.memoryDashboardStore.selectedMemory?.content_full"></div>
284 + </div>
285 +
286 + <!-- Metadata Sidebar -->
287 + <div class="metadata-sidebar">
288 + <div class="section-header">
289 + <h4>
290 + <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
291 + <circle cx="12" cy="12" r="3"></circle>
292 + <path d="M12 1v6m0 6v6m11-7h-6m-6 0H1"></path>
293 + </svg>
294 + Metadata
295 + </h4>
296 + </div>
297 +
298 + <div class="metadata-grid">
299 + <div class="metadata-item">
300 + <label>Memory ID</label>
301 + <value class="monospace" x-text="$store.memoryDashboardStore.selectedMemory?.id"></value>
302 + </div>
303 +
304 + <div class="metadata-item">
305 + <label>Area</label>
306 + <value x-text="$store.memoryDashboardStore.selectedMemory?.area"></value>
307 + </div>
308 +
309 + <div class="metadata-item">
310 + <label>Created</label>
311 + <value x-text="$store.memoryDashboardStore.formatTimestamp($store.memoryDashboardStore.selectedMemory?.timestamp)"></value>
312 + </div>
313 +
314 + <div class="metadata-item">
315 + <label>Source Type</label>
316 + <value x-text="$store.memoryDashboardStore.selectedMemory?.knowledge_source ? 'Knowledge File' : 'Conversation'"></value>
317 + </div>
318 +
319 + <div class="metadata-item" x-show="$store.memoryDashboardStore.selectedMemory?.source_file">
320 + <label>Source File</label>
321 + <value class="monospace" x-text="$store.memoryDashboardStore.selectedMemory?.source_file"></value>
322 + </div>
323 +
324 + <div class="metadata-item" x-show="$store.memoryDashboardStore.selectedMemory?.file_type">
325 + <label>File Type</label>
326 + <value x-text="$store.memoryDashboardStore.selectedMemory?.file_type"></value>
327 + </div>
328 +
329 + <div class="metadata-item" x-show="$store.memoryDashboardStore.selectedMemory?.consolidation_action">
330 + <label>Consolidation</label>
331 + <value class="consolidation-badge" x-text="$store.memoryDashboardStore.selectedMemory?.consolidation_action"></value>
332 + </div>
333 +
334 + <div class="metadata-item" x-show="$store.memoryDashboardStore.selectedMemory?.tags && $store.memoryDashboardStore.selectedMemory?.tags.length > 0">
335 + <label>Tags</label>
336 + <div class="tags-display">
337 + <template x-for="tag in $store.memoryDashboardStore.selectedMemory?.tags" :key="tag">
338 + <span class="tag-pill" x-text="tag"></span>
339 + </template>
340 + </div>
341 + </div>
342 + </div>
343 + </div>
344 + </div>
345 + </div>
346 + </div>
347 +
348 + </div>
349 + </template>
350 + </div>
351 +
352 + <style>
353 + [x-cloak] {
354 + display: none !important;
355 + }
356 +
357 + .memory-dashboard {
358 + padding: 1rem;
359 + max-width: 100%;
360 + background: var(--color-background);
361 + color: var(--color-text);
362 + }
363 +
364 + .dashboard-header {
365 + display: flex;
366 + justify-content: space-between;
367 + align-items: center;
368 + margin-bottom: 1.5rem;
369 + padding-bottom: 1rem;
370 + border-bottom: 1px solid var(--color-border);
371 + }
372 +
373 + .memory-stats {
374 + display: flex;
375 + gap: 1rem;
376 + }
377 +
378 + .stat-item {
379 + display: flex;
380 + flex-direction: column;
381 + align-items: center;
382 + padding: 0.5rem;
383 + background: var(--color-panel);
384 + border: 1px solid var(--color-border);
385 + border-radius: 8px;
386 + min-width: 80px;
387 + }
388 +
389 + .stat-label {
390 + font-size: 0.8rem;
391 + color: var(--color-secondary);
392 + margin-bottom: 0.25rem;
393 + }
394 +
395 + .stat-value {
396 + font-size: 1.1rem;
397 + font-weight: bold;
398 + color: var(--color-primary);
399 + }
400 +
401 + .filters-section {
402 + margin-bottom: 1.5rem;
403 + padding: 1rem;
404 + background: var(--color-panel);
405 + border: 1px solid var(--color-border);
406 + border-radius: 8px;
407 + }
408 +
409 + .filter-row {
410 + display: flex;
411 + gap: 1rem;
412 + align-items: end;
413 + flex-wrap: wrap;
414 + }
415 +
416 + .filter-group {
417 + display: flex;
418 + flex-direction: column;
419 + gap: 0.25rem;
420 + }
421 +
422 + .filter-group label {
423 + font-size: 0.9rem;
424 + font-weight: 500;
425 + color: var(--color-text);
426 + }
427 +
428 + .filter-group input, .filter-group select {
429 + padding: 0.5rem;
430 + border: 1px solid var(--color-border);
431 + background: var(--color-background);
432 + color: var(--color-text);
433 + border-radius: 6px;
434 + font-size: 0.9rem;
435 + min-width: 200px;
436 + }
437 +
438 + .filter-group input:focus, .filter-group select:focus {
439 + outline: none;
440 + border-color: var(--color-primary);
441 + box-shadow: 0 0 0 2px rgba(115, 122, 129, 0.1);
442 + }
443 +
444 + .filter-actions {
445 + display: flex;
446 + gap: 0.5rem;
447 + }
448 +
449 + .loading-state, .error-state, .no-memories, .init-message {
450 + text-align: center;
451 + padding: 2rem;
452 + color: var(--color-secondary);
453 + background: var(--color-panel);
454 + border: 1px solid var(--color-border);
455 + border-radius: 8px;
456 + margin: 1rem 0;
457 + }
458 +
459 + .init-message {
460 + color: var(--color-primary);
461 + background: rgba(115, 122, 129, 0.1);
462 + border-color: var(--color-primary);
463 + }
464 +
465 + .error-state {
466 + color: var(--color-accent);
467 + background: rgba(207, 102, 121, 0.1);
468 + border-color: var(--color-accent);
469 + }
470 +
471 + .loading-spinner {
472 + width: 20px;
473 + height: 20px;
474 + border: 2px solid var(--color-border);
475 + border-left-color: var(--color-primary);
476 + border-radius: 50%;
477 + animation: spin 1s linear infinite;
478 + margin: 0 auto 0.5rem;
479 + }
480 +
481 + @keyframes spin {
482 + to { transform: rotate(360deg); }
483 + }
484 +
485 + .memory-table-container {
486 + background: var(--color-background);
487 + border: 1px solid var(--color-border);
488 + border-radius: 8px;
489 + overflow: hidden;
490 + margin-bottom: 1rem;
491 + }
492 +
493 + .table-wrapper {
494 + width: 100%;
495 + overflow-x: auto;
496 + }
497 +
498 + .memory-table {
499 + width: 100%;
500 + border-collapse: collapse;
501 + table-layout: fixed;
502 + }
503 +
504 + /* Fixed column widths for proper fit */
505 + .col-area { width: 15%; }
506 + .col-timestamp { width: 18%; }
507 + .col-source { width: 15%; }
508 + .col-preview { width: 30%; }
509 + .col-actions { width: 22%; }
510 +
511 + .memory-table th,
512 + .memory-table td {
513 + padding: 1rem 0.75rem;
514 + text-align: left;
515 + vertical-align: top;
516 + border-bottom: 1px solid var(--color-border);
517 + overflow: hidden;
518 + }
519 +
520 + /* Allow source column to wrap properly */
521 + .source-cell {
522 + overflow: visible;
523 + white-space: normal;
524 + word-wrap: break-word;
525 + }
526 +
527 + .memory-table th {
528 + background: var(--color-panel);
529 + font-weight: 600;
530 + color: var(--color-text);
531 + position: sticky;
532 + top: 0;
533 + z-index: 10;
534 + border-bottom: 2px solid var(--color-border);
535 + }
536 +
537 + .memory-table tbody tr {
538 + border-bottom: 1px solid var(--color-border);
539 + transition: background-color 0.2s ease;
540 + }
541 +
542 + .memory-table tbody tr:nth-child(even) {
543 + background: var(--color-panel);
544 + }
545 +
546 + .memory-table tbody tr:hover {
547 + background: rgba(255, 255, 255, 0.1);
548 + }
549 +
550 + .memory-table tbody tr:last-child {
551 + border-bottom: none;
552 + }
553 +
554 + .area-badge {
555 + padding: 0.25rem 0.5rem;
556 + border-radius: 12px;
557 + color: white;
558 + font-size: 0.75rem;
559 + font-weight: bold;
560 + text-transform: uppercase;
561 + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
562 + }
563 +
564 + .timestamp-cell {
565 + font-size: 0.85rem;
566 + color: var(--color-secondary);
567 + white-space: normal;
568 + word-break: break-word;
569 + font-family: monospace;
570 + line-height: 1.2;
571 + }
572 +
573 + .source-info {
574 + display: flex;
575 + flex-direction: column;
576 + gap: 0.25rem;
577 + align-items: flex-start;
578 + }
579 +
580 + .source-type {
581 + padding: 0.25rem 0.5rem;
582 + border-radius: 6px;
583 + font-size: 0.75rem;
584 + font-weight: 500;
585 + text-align: center;
586 + border: 1px solid;
587 + white-space: nowrap;
588 + flex-shrink: 0;
589 + }
590 +
591 + .source-type.knowledge {
592 + background: rgba(115, 122, 129, 0.1);
593 + color: var(--color-primary);
594 + border-color: var(--color-primary);
595 + }
596 +
597 + .source-type.conversation {
598 + background: rgba(101, 101, 101, 0.1);
599 + color: var(--color-secondary);
600 + border-color: var(--color-secondary);
601 + }
602 +
603 + .source-file {
604 + font-size: 0.7rem;
605 + color: var(--color-secondary);
606 + font-family: monospace;
607 + background: var(--color-panel);
608 + padding: 0.2rem 0.4rem;
609 + border-radius: 4px;
610 + border: 1px solid var(--color-border);
611 + word-break: break-all;
612 + white-space: normal;
613 + max-width: 100%;
614 + overflow-wrap: break-word;
615 + }
616 +
617 + .content-preview {
618 + line-height: 1.4;
619 + font-size: 0.9rem;
620 + word-wrap: break-word;
621 + overflow-wrap: break-word;
622 + max-width: 100%;
623 + overflow: hidden;
624 + text-overflow: ellipsis;
625 + }
626 +
627 + .tags {
628 + display: flex;
629 + gap: 0.25rem;
630 + margin-top: 0.5rem;
631 + flex-wrap: wrap;
632 + }
633 +
634 + .tag {
635 + background: var(--color-panel);
636 + border: 1px solid var(--color-border);
637 + padding: 0.2rem 0.4rem;
638 + border-radius: 6px;
639 + font-size: 0.7rem;
640 + color: var(--color-secondary);
641 + }
642 +
643 + .actions-cell {
644 + white-space: nowrap;
645 + text-align: center;
646 + min-width: 140px;
647 + padding: 0.5rem 0.25rem !important;
648 + }
649 +
650 + .btn-action {
651 + background: none;
652 + border: 1px solid var(--color-border);
653 + padding: 0.3rem;
654 + margin: 0 0.1rem;
655 + cursor: pointer;
656 + border-radius: 6px;
657 + color: var(--color-secondary);
658 + transition: all 0.2s ease;
659 + display: inline-flex;
660 + align-items: center;
661 + justify-content: center;
662 + min-width: 32px;
663 + height: 32px;
664 + }
665 +
666 + .btn-action:hover {
667 + background: rgba(0, 0, 0, 0.05);
668 + border-color: rgba(0, 0, 0, 0.2);
669 + color: var(--color-text);
670 + transform: translateY(-1px);
671 + }
672 +
673 + .btn-action.view:hover {
674 + background: rgba(33, 150, 243, 0.1);
675 + border-color: #2196F3;
676 + color: #2196F3;
677 + }
678 +
679 + .btn-action.copy:hover {
680 + background: rgba(76, 175, 80, 0.1);
681 + border-color: #4CAF50;
682 + color: #4CAF50;
683 + }
684 +
685 + .btn-action.delete:hover {
686 + background: rgba(244, 67, 54, 0.1);
687 + border-color: #f44336;
688 + color: #f44336;
689 + }
690 +
691 + .pagination-controls-top {
692 + display: flex;
693 + justify-content: space-between;
694 + align-items: center;
695 + margin-bottom: 1rem;
696 + padding: 1rem;
697 + background: var(--color-panel);
698 + border: 1px solid var(--color-border);
699 + border-radius: 8px;
700 + }
701 +
702 + .pagination-info-inline {
703 + color: var(--color-secondary);
704 + font-size: 0.9rem;
705 + }
706 +
707 + .pagination-controls {
708 + display: flex;
709 + justify-content: center;
710 + align-items: center;
711 + gap: 0.5rem;
712 + margin: 1rem 0;
713 + padding: 1rem;
714 + background: var(--color-panel);
715 + border: 1px solid var(--color-border);
716 + border-radius: 8px;
717 + }
718 +
719 + .page-numbers {
720 + display: flex;
721 + gap: 0.25rem;
722 + }
723 +
724 + .page-btn {
725 + background: var(--color-background);
726 + border: 1px solid var(--color-border);
727 + color: var(--color-text);
728 + padding: 0.5rem 0.75rem;
729 + cursor: pointer;
730 + border-radius: 6px;
731 + font-size: 0.9rem;
732 + transition: all 0.2s ease;
733 + }
734 +
735 + .page-btn:hover {
736 + background: rgba(255, 255, 255, 0.1);
737 + border-color: var(--color-primary);
738 + }
739 +
740 + .page-btn.active {
741 + background: var(--color-primary);
742 + color: white;
743 + border-color: var(--color-primary);
744 + }
745 +
746 + .export-section {
747 + text-align: center;
748 + margin-top: 1rem;
749 + padding: 1rem;
750 + background: var(--color-panel);
751 + border: 1px solid var(--color-border);
752 + border-radius: 8px;
753 + border-top: 1px solid var(--color-border);
754 + }
755 +
756 + /* Enhanced Modal Styles */
757 + .modal-overlay {
758 + position: fixed;
759 + top: 0;
760 + left: 0;
761 + right: 0;
762 + bottom: 0;
763 + background: rgba(0, 0, 0, 0.6);
764 + backdrop-filter: blur(4px);
765 + display: flex;
766 + align-items: flex-start;
767 + justify-content: center;
768 + z-index: 1000;
769 + overflow-y: auto;
770 + padding: 2rem 0;
771 + }
772 +
773 + .modal-enter {
774 + transition: all 0.3s ease;
775 + }
776 +
777 + .modal-enter-start {
778 + opacity: 0;
779 + }
780 +
781 + .modal-enter-end {
782 + opacity: 1;
783 + }
784 +
785 + .modal-leave {
786 + transition: all 0.2s ease;
787 + }
788 +
789 + .modal-leave-start {
790 + opacity: 1;
791 + }
792 +
793 + .modal-leave-end {
794 + opacity: 0;
795 + }
796 +
797 + .memory-detail-modal {
798 + background: var(--color-background);
799 + border: 1px solid var(--color-border);
800 + border-radius: 16px;
801 + width: 95%;
802 + max-width: 1200px;
803 + max-height: 90vh;
804 + min-height: 60vh;
805 + overflow: hidden;
806 + display: flex;
807 + flex-direction: column;
808 + box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
809 + animation: modalSlideIn 0.3s ease-out;
810 + margin: 0 auto;
811 + }
812 +
813 + @keyframes modalSlideIn {
814 + from {
815 + transform: translateY(20px) scale(0.95);
816 + opacity: 0;
817 + }
818 + to {
819 + transform: translateY(0) scale(1);
820 + opacity: 1;
821 + }
822 + }
823 +
824 + .modal-header-enhanced {
825 + background: var(--color-panel);
826 + padding: 1.5rem 2rem;
827 + border-bottom: 2px solid var(--color-border);
828 + display: flex;
829 + justify-content: space-between;
830 + align-items: flex-start;
831 + }
832 +
833 + .header-left {
834 + flex: 1;
835 + }
836 +
837 + .memory-title {
838 + display: flex;
839 + align-items: center;
840 + gap: 1rem;
841 + margin-bottom: 0.75rem;
842 + }
843 +
844 + .memory-title h3 {
845 + margin: 0;
846 + font-size: 1.5rem;
847 + font-weight: 600;
848 + color: var(--color-text);
849 + }
850 +
851 + .area-badge-large {
852 + padding: 0.5rem 1rem;
853 + border-radius: 20px;
854 + color: white;
855 + font-size: 0.75rem;
856 + font-weight: 700;
857 + text-transform: uppercase;
858 + letter-spacing: 0.5px;
859 + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
860 + }
861 +
862 + .memory-meta-quick {
863 + display: flex;
864 + gap: 0.75rem;
865 + align-items: center;
866 + flex-wrap: wrap;
867 + }
868 +
869 + .timestamp-badge {
870 + background: var(--color-panel);
871 + border: 1px solid var(--color-border);
872 + color: var(--color-secondary);
873 + padding: 0.25rem 0.75rem;
874 + border-radius: 12px;
875 + font-size: 0.8rem;
876 + font-weight: 500;
877 + }
878 +
879 + .source-badge {
880 + padding: 0.25rem 0.75rem;
881 + border-radius: 12px;
882 + font-size: 0.8rem;
883 + font-weight: 500;
884 + border: 1px solid;
885 + }
886 +
887 + .source-badge.knowledge {
888 + background: rgba(115, 122, 129, 0.1);
889 + color: var(--color-primary);
890 + border-color: var(--color-primary);
891 + }
892 +
893 + .source-badge.conversation {
894 + background: rgba(101, 101, 101, 0.1);
895 + color: var(--color-secondary);
896 + border-color: var(--color-secondary);
897 + }
898 +
899 + .header-actions {
900 + display: flex;
901 + gap: 0.5rem;
902 + align-items: flex-start;
903 + }
904 +
905 + .btn-action-header {
906 + background: var(--color-background);
907 + border: 1px solid var(--color-border);
908 + padding: 0.75rem;
909 + border-radius: 12px;
910 + cursor: pointer;
911 + color: var(--color-secondary);
912 + transition: all 0.2s ease;
913 + display: flex;
914 + align-items: center;
915 + justify-content: center;
916 + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
917 + }
918 +
919 + .btn-action-header:hover {
920 + background: rgba(255, 255, 255, 0.1);
921 + border-color: var(--color-primary);
922 + transform: translateY(-1px);
923 + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
924 + }
925 +
926 + .btn-action-header.copy:hover {
927 + background: rgba(76, 175, 80, 0.05);
928 + border-color: #4CAF50;
929 + color: #4CAF50;
930 + }
931 +
932 + .btn-action-header.delete:hover {
933 + background: rgba(244, 67, 54, 0.05);
934 + border-color: #f44336;
935 + color: #f44336;
936 + }
937 +
938 + .btn-close-enhanced {
939 + background: rgba(207, 102, 121, 0.1);
940 + border: 1px solid var(--color-accent);
941 + padding: 0.75rem;
942 + border-radius: 12px;
943 + cursor: pointer;
944 + color: var(--color-accent);
945 + transition: all 0.2s ease;
946 + display: flex;
947 + align-items: center;
948 + justify-content: center;
949 + }
950 +
951 + .btn-close-enhanced:hover {
952 + background: rgba(207, 102, 121, 0.15);
953 + border-color: var(--color-accent);
954 + transform: translateY(-1px);
955 + }
956 +
957 + .modal-body-enhanced {
958 + display: flex;
959 + flex: 1;
960 + overflow: hidden;
961 + min-height: 0;
962 + }
963 +
964 + .content-section-main {
965 + flex: 2;
966 + padding: 2rem;
967 + overflow-y: auto;
968 + background: var(--color-background);
969 + }
970 +
971 + .metadata-sidebar {
972 + flex: 1;
973 + min-width: 320px;
974 + background: var(--color-panel);
975 + border-left: 1px solid var(--color-border);
976 + padding: 2rem;
977 + overflow-y: auto;
978 + }
979 +
980 + .section-header {
981 + margin-bottom: 1.5rem;
982 + }
983 +
984 + .section-header h4 {
985 + margin: 0;
986 + font-size: 1.1rem;
987 + font-weight: 600;
988 + color: var(--color-text);
989 + display: flex;
990 + align-items: center;
991 + gap: 0.5rem;
992 + }
993 +
994 + .content-display {
995 + background: var(--color-background);
996 + border: 1px solid var(--color-border);
997 + border-radius: 12px;
998 + padding: 1.5rem;
999 + font-size: 0.95rem;
1000 + line-height: 1.6;
1001 + color: var(--color-text);
1002 + white-space: pre-wrap;
1003 + max-height: 60vh;
1004 + overflow-y: auto;
1005 + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
1006 + }
1007 +
1008 + .metadata-grid {
1009 + display: flex;
1010 + flex-direction: column;
1011 + gap: 1.25rem;
1012 + }
1013 +
1014 + .metadata-item {
1015 + display: flex;
1016 + flex-direction: column;
1017 + gap: 0.5rem;
1018 + }
1019 +
1020 + .metadata-item label {
1021 + font-size: 0.8rem;
1022 + font-weight: 600;
1023 + color: var(--color-secondary);
1024 + text-transform: uppercase;
1025 + letter-spacing: 0.5px;
1026 + }
1027 +
1028 + .metadata-item value {
1029 + font-size: 0.9rem;
1030 + color: var(--color-text);
1031 + background: var(--color-background);
1032 + padding: 0.75rem;
1033 + border-radius: 8px;
1034 + border: 1px solid var(--color-border);
1035 + word-break: break-all;
1036 + }
1037 +
1038 + .metadata-item value.monospace {
1039 + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
1040 + font-size: 0.85rem;
1041 + }
1042 +
1043 + .consolidation-badge {
1044 + background: var(--color-accent) !important;
1045 + color: white !important;
1046 + padding: 0.5rem 1rem !important;
1047 + border-radius: 20px !important;
1048 + font-weight: 600 !important;
1049 + text-transform: capitalize !important;
1050 + border: none !important;
1051 + }
1052 +
1053 + .tags-display {
1054 + display: flex;
1055 + flex-wrap: wrap;
1056 + gap: 0.5rem;
1057 + background: var(--color-background);
1058 + padding: 0.75rem;
1059 + border-radius: 8px;
1060 + border: 1px solid var(--color-border);
1061 + }
1062 +
1063 + .tag-pill {
1064 + background: var(--color-primary);
1065 + color: white;
1066 + padding: 0.25rem 0.75rem;
1067 + border-radius: 12px;
1068 + font-size: 0.75rem;
1069 + font-weight: 500;
1070 + }
1071 +
1072 + /* Responsive design */
1073 + @media (max-width: 768px) {
1074 + .modal-overlay {
1075 + padding: 1rem 0;
1076 + }
1077 +
1078 + .memory-dashboard {
1079 + padding: 0.5rem;
1080 + }
1081 +
1082 + .dashboard-header {
1083 + flex-direction: column;
1084 + align-items: flex-start;
1085 + gap: 1rem;
1086 + }
1087 +
1088 + .memory-stats {
1089 + width: 100%;
1090 + justify-content: space-between;
1091 + }
1092 +
1093 + .filter-row {
1094 + flex-direction: column;
1095 + align-items: stretch;
1096 + }
1097 +
1098 + .filter-group input, .filter-group select {
1099 + min-width: unset;
1100 + width: 100%;
1101 + }
1102 +
1103 + .memory-table th,
1104 + .memory-table td {
1105 + padding: 0.5rem 0.25rem;
1106 + font-size: 0.85rem;
1107 + }
1108 +
1109 + .actions-cell {
1110 + min-width: 120px;
1111 + }
1112 +
1113 + .col-area { width: 15%; }
1114 + .col-timestamp { width: 15%; }
1115 + .col-source { width: 25%; }
1116 + .col-preview { width: 25%; }
1117 + .col-actions { width: 20%; }
1118 +
1119 + .memory-detail-modal {
1120 + width: 98%;
1121 + max-height: 95vh;
1122 + min-height: 50vh;
1123 + margin: 0;
1124 + }
1125 +
1126 + .modal-body-enhanced {
1127 + flex-direction: column;
1128 + }
1129 +
1130 + .metadata-sidebar {
1131 + min-width: unset;
1132 + border-left: none;
1133 + border-top: 1px solid var(--color-border);
1134 + }
1135 +
1136 + .modal-header-enhanced {
1137 + padding: 1rem;
1138 + }
1139 +
1140 + .memory-title {
1141 + flex-direction: column;
1142 + align-items: flex-start;
1143 + gap: 0.5rem;
1144 + }
1145 +
1146 + .content-section-main,
1147 + .metadata-sidebar {
1148 + padding: 1rem;
1149 + }
1150 +
1151 + .pagination-controls-top {
1152 + flex-direction: column;
1153 + gap: 1rem;
1154 + align-items: stretch;
1155 + }
1156 +
1157 + .pagination-controls {
1158 + flex-wrap: wrap;
1159 + justify-content: center;
1160 + }
1161 +
1162 + .page-numbers {
1163 + flex-wrap: wrap;
1164 + justify-content: center;
1165 + }
1166 + }
1167 + </style>
1168 +
1169 +</body>
1170 +
1171 +</html>
webui/js/settings.js
+2
@@ -281,6 +281,8 @@ const settingsModalProxy = {
281 openModal("settings/external/a2a-connection.html");
282 } else if (field.id === "external_api_examples") {
283 openModal("settings/external/api-examples.html");
284 + } else if (field.id === "memory_dashboard") {
285 + openModal("settings/memory/memory-dashboard.html");
286 }
287 }
288 };