mirror of
https://github.com/agent0ai/agent-zero.git
synced 2026-05-23 12:44:31 +00:00
Add user-configurable timezone and 12/24-hour preferences, then wire them through settings, runtime snapshots, scheduler payloads, wait handling, notifications, backups, memory, plugin metadata, and frontend formatters. Keep UTC as the boundary for absolute instants while serializing user-facing dates in the configured or browser-resolved timezone. Preserve scheduler wall-clock inputs in the selected timezone, propagate TZ into desktop/runtime process environments, and restart active desktop sessions when the runtime timezone changes. Cover the risky paths with timezone regression tests for settings normalization, auto and fixed timezone resolution, scheduler round-trips, memory timestamp conversion, and desktop timezone sync.
703 lines
20 KiB
JavaScript
703 lines
20 KiB
JavaScript
import { createStore } from "/js/AlpineStore.js";
|
|
import { getContext } from "/index.js";
|
|
import * as API from "/js/api.js";
|
|
import { openModal, closeModal } from "/js/modals.js";
|
|
import { store as notificationStore } from "/components/notifications/notification-store.js";
|
|
import {
|
|
getCurrentUserDateString,
|
|
getCurrentUserISOString,
|
|
getUserHour12,
|
|
getUserTimezone,
|
|
} from "/js/time-utils.js";
|
|
const MEMORY_DASHBOARD_API = "/plugins/_memory/memory_dashboard";
|
|
|
|
// Helper function for toasts
|
|
function justToast(text, type = "info", timeout = 5000) {
|
|
notificationStore.addFrontendToastOnly(type, text, "", timeout / 1000);
|
|
}
|
|
|
|
// Memory Dashboard Store
|
|
const memoryDashboardStore = {
|
|
// Data
|
|
memories: [],
|
|
currentPage: 1,
|
|
itemsPerPage: 10,
|
|
|
|
// State
|
|
loading: false,
|
|
loadingSubdirs: false,
|
|
initializingMemory: false,
|
|
error: null,
|
|
message: null,
|
|
|
|
// Memory subdirectories
|
|
memorySubdirs: [],
|
|
selectedMemorySubdir: "default",
|
|
memoryInitialized: {}, // Track which subdirs have been initialized
|
|
|
|
// Search and filters
|
|
searchQuery: "",
|
|
areaFilter: "",
|
|
threshold: parseFloat(
|
|
localStorage.getItem("memoryDashboard_threshold") || "0.6"
|
|
),
|
|
limit: parseInt(localStorage.getItem("memoryDashboard_limit") || "1000"),
|
|
|
|
// Stats
|
|
totalCount: 0,
|
|
totalDbCount: 0,
|
|
knowledgeCount: 0,
|
|
conversationCount: 0,
|
|
areasCount: {},
|
|
|
|
// Memory detail modal (standard modal approach)
|
|
detailMemory: null,
|
|
editMode: false,
|
|
editMemoryBackup: null,
|
|
|
|
// Polling
|
|
pollingInterval: null,
|
|
pollingEnabled: false,
|
|
|
|
async openModal() {
|
|
await openModal("/plugins/_memory/webui/memory-dashboard.html");
|
|
},
|
|
|
|
init() {
|
|
this.initialize();
|
|
},
|
|
|
|
async onOpen() {
|
|
await this.getCurrentMemorySubdir();
|
|
await this.loadMemorySubdirs();
|
|
await this.searchMemories();
|
|
// Start polling for live updates as soon as dashboard is open
|
|
this.startPolling();
|
|
},
|
|
|
|
async initialize() {
|
|
// Reset state when opening (but keep directory from context)
|
|
this.currentPage = 1;
|
|
this.searchQuery = "";
|
|
this.areaFilter = "";
|
|
|
|
// // Get current memory subdirectory from application context
|
|
// await this.getCurrentMemorySubdir();
|
|
|
|
// await this.loadMemorySubdirs();
|
|
|
|
// // Automatically search with selected subdirectory
|
|
// if (this.selectedMemorySubdir) {
|
|
// await this.searchMemories();
|
|
// }
|
|
|
|
// // Start polling for live updates as soon as dashboard is open
|
|
// this.startPolling();
|
|
},
|
|
|
|
async getCurrentMemorySubdir() {
|
|
try {
|
|
// Try to get current memory subdirectory from the backend
|
|
const response = await API.callJsonApi(MEMORY_DASHBOARD_API, {
|
|
action: "get_current_memory_subdir",
|
|
context_id: getContext(),
|
|
});
|
|
|
|
if (response.success && response.memory_subdir) {
|
|
this.selectedMemorySubdir = response.memory_subdir;
|
|
} else {
|
|
// Fallback to default
|
|
this.selectedMemorySubdir = "default";
|
|
}
|
|
} catch (error) {
|
|
console.error("Failed to get current memory subdirectory:", error);
|
|
this.selectedMemorySubdir = "default";
|
|
}
|
|
},
|
|
|
|
async loadMemorySubdirs() {
|
|
this.loadingSubdirs = true;
|
|
this.error = null;
|
|
|
|
try {
|
|
const response = await API.callJsonApi(MEMORY_DASHBOARD_API, {
|
|
action: "get_memory_subdirs",
|
|
});
|
|
|
|
if (response.success) {
|
|
let subdirs = response.subdirs || ["default"];
|
|
|
|
// Sort alphabetically but ensure "default" is always first
|
|
subdirs = subdirs.filter((dir) => dir !== "default").sort();
|
|
if (response.subdirs && response.subdirs.includes("default")) {
|
|
subdirs.unshift("default");
|
|
} else {
|
|
subdirs.unshift("default");
|
|
}
|
|
|
|
this.memorySubdirs = subdirs;
|
|
|
|
// Ensure the currently selected subdirectory exists in the list
|
|
if (!this.memorySubdirs.includes(this.selectedMemorySubdir)) {
|
|
this.selectedMemorySubdir = "default";
|
|
}
|
|
} else {
|
|
this.error = response.error || "Failed to load memory subdirectories";
|
|
this.memorySubdirs = ["default"];
|
|
this.selectedMemorySubdir = "default";
|
|
}
|
|
} catch (error) {
|
|
this.error = error.message || "Failed to load memory subdirectories";
|
|
this.memorySubdirs = ["default"];
|
|
// Only fallback to default if current selection is not available
|
|
if (!this.memorySubdirs.includes(this.selectedMemorySubdir)) {
|
|
this.selectedMemorySubdir = "default";
|
|
}
|
|
console.error("Memory subdirectory loading error:", error);
|
|
} finally {
|
|
this.loadingSubdirs = false;
|
|
}
|
|
},
|
|
|
|
async searchMemories(silent = false) {
|
|
// Save limit to localStorage for persistence
|
|
localStorage.setItem("memoryDashboard_limit", this.limit.toString());
|
|
localStorage.setItem(
|
|
"memoryDashboard_threshold",
|
|
this.threshold.toString()
|
|
);
|
|
|
|
if (!silent) {
|
|
this.loading = true;
|
|
this.error = null;
|
|
this.message = null;
|
|
|
|
// Check if this memory subdirectory needs initialization
|
|
if (!this.memoryInitialized[this.selectedMemorySubdir]) {
|
|
this.initializingMemory = true;
|
|
}
|
|
}
|
|
|
|
try {
|
|
const response = await API.callJsonApi(MEMORY_DASHBOARD_API, {
|
|
action: "search",
|
|
memory_subdir: this.selectedMemorySubdir,
|
|
area: this.areaFilter,
|
|
search: this.searchQuery,
|
|
limit: this.limit,
|
|
threshold: this.threshold,
|
|
});
|
|
|
|
if (response.success) {
|
|
// Preserve existing selections when updating memories during polling
|
|
const existingSelections = {};
|
|
if (silent && this.memories) {
|
|
// Build a map of existing selections by memory ID
|
|
this.memories.forEach((memory) => {
|
|
if (memory.selected) {
|
|
existingSelections[memory.id] = true;
|
|
}
|
|
});
|
|
}
|
|
|
|
// Add selected property to each memory item for mass selection
|
|
this.memories = (response.memories || []).map((memory) => ({
|
|
...memory,
|
|
selected: existingSelections[memory.id] || false,
|
|
}));
|
|
this.totalCount = response.total_count || 0;
|
|
this.totalDbCount = response.total_db_count || 0;
|
|
this.knowledgeCount = response.knowledge_count || 0;
|
|
this.conversationCount = response.conversation_count || 0;
|
|
|
|
if (!silent) {
|
|
this.message = response.message || null;
|
|
this.currentPage = 1; // Reset to first page when loading new data
|
|
} else {
|
|
// For silent updates, adjust current page if it exceeds available pages
|
|
if (this.currentPage > this.totalPages && this.totalPages > 0) {
|
|
this.currentPage = this.totalPages;
|
|
}
|
|
}
|
|
|
|
// Mark this subdirectory as initialized
|
|
this.memoryInitialized[this.selectedMemorySubdir] = true;
|
|
} else {
|
|
if (!silent) {
|
|
this.error = response.error || "Failed to search memories";
|
|
this.memories = [];
|
|
this.message = null;
|
|
} else {
|
|
// For silent updates, just log the error but don't break the UI
|
|
console.warn("Memory dashboard polling failed:", response.error);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
if (!silent) {
|
|
this.error = error.message || "Failed to search memories";
|
|
this.memories = [];
|
|
this.message = null;
|
|
console.error("Memory search error:", error);
|
|
} else {
|
|
// For silent updates, just log the error but don't break the UI
|
|
console.warn("Memory dashboard polling error:", error);
|
|
}
|
|
} finally {
|
|
if (!silent) {
|
|
this.loading = false;
|
|
this.initializingMemory = false;
|
|
}
|
|
}
|
|
},
|
|
|
|
async clearSearch() {
|
|
this.areaFilter = "";
|
|
this.searchQuery = "";
|
|
this.currentPage = 1;
|
|
|
|
// Immediately trigger a new search with cleared filters
|
|
await this.searchMemories();
|
|
},
|
|
|
|
async onMemorySubdirChange() {
|
|
// Clear current results when subdirectory changes
|
|
await this.clearSearch(); // Polling continues with new subdirectory
|
|
},
|
|
|
|
// Pagination
|
|
get totalPages() {
|
|
return Math.ceil(this.memories.length / this.itemsPerPage);
|
|
},
|
|
|
|
get paginatedMemories() {
|
|
const start = (this.currentPage - 1) * this.itemsPerPage;
|
|
const end = start + this.itemsPerPage;
|
|
return this.memories.slice(start, end);
|
|
},
|
|
|
|
goToPage(page) {
|
|
if (page >= 1 && page <= this.totalPages) {
|
|
this.currentPage = page;
|
|
}
|
|
},
|
|
|
|
nextPage() {
|
|
if (this.currentPage < this.totalPages) {
|
|
this.currentPage++;
|
|
}
|
|
},
|
|
|
|
prevPage() {
|
|
if (this.currentPage > 1) {
|
|
this.currentPage--;
|
|
}
|
|
},
|
|
|
|
// Mass selection
|
|
get selectedMemories() {
|
|
return this.memories.filter((memory) => memory.selected);
|
|
},
|
|
|
|
get selectedCount() {
|
|
return this.selectedMemories.length;
|
|
},
|
|
|
|
get allSelected() {
|
|
return (
|
|
this.memories.length > 0 &&
|
|
this.memories.every((memory) => memory.selected)
|
|
);
|
|
},
|
|
|
|
get someSelected() {
|
|
return this.memories.some((memory) => memory.selected);
|
|
},
|
|
|
|
toggleSelectAll() {
|
|
const shouldSelectAll = !this.allSelected;
|
|
this.memories.forEach((memory) => {
|
|
memory.selected = shouldSelectAll;
|
|
});
|
|
},
|
|
|
|
clearSelection() {
|
|
this.memories.forEach((memory) => {
|
|
memory.selected = false;
|
|
});
|
|
},
|
|
|
|
// Bulk operations
|
|
async bulkDeleteMemories() {
|
|
const selectedMemories = this.selectedMemories;
|
|
if (selectedMemories.length === 0) return;
|
|
|
|
try {
|
|
this.loading = true;
|
|
const response = await API.callJsonApi(MEMORY_DASHBOARD_API, {
|
|
action: "bulk_delete",
|
|
memory_subdir: this.selectedMemorySubdir,
|
|
memory_ids: selectedMemories.map((memory) => memory.id),
|
|
});
|
|
|
|
if (response.success) {
|
|
justToast(
|
|
`Successfully deleted ${selectedMemories.length} memories`,
|
|
"success"
|
|
);
|
|
|
|
// Let polling refresh the data instead of manual manipulation
|
|
// Trigger an immediate refresh to get updated state from backend
|
|
await this.searchMemories(true); // silent refresh
|
|
} else {
|
|
justToast(
|
|
response.error || "Failed to delete selected memories",
|
|
"error"
|
|
);
|
|
}
|
|
} catch (error) {
|
|
justToast(error.message || "Failed to delete selected memories", "error");
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
},
|
|
|
|
// Helper method to format a complete memory with all metadata
|
|
formatMemoryForCopy(memory) {
|
|
let formatted = `=== Memory ID: ${memory.id} ===
|
|
Area: ${memory.area}
|
|
Timestamp: ${this.formatTimestamp(memory.timestamp)}
|
|
Source: ${memory.knowledge_source ? "Knowledge" : "Conversation"}
|
|
${memory.source_file ? `File: ${memory.source_file}` : ""}
|
|
${
|
|
memory.tags && memory.tags.length > 0 ? `Tags: ${memory.tags.join(", ")}` : ""
|
|
}`;
|
|
|
|
// Add custom metadata if present
|
|
if (
|
|
memory.metadata &&
|
|
typeof memory.metadata === "object" &&
|
|
Object.keys(memory.metadata).length > 0
|
|
) {
|
|
formatted += "\n\nMetadata:";
|
|
for (const [key, value] of Object.entries(memory.metadata)) {
|
|
const displayValue =
|
|
typeof value === "object" ? JSON.stringify(value, null, 2) : value;
|
|
formatted += `\n${key}: ${displayValue}`;
|
|
}
|
|
}
|
|
|
|
formatted += `\n\nContent:
|
|
${memory.content_full}
|
|
|
|
`;
|
|
return formatted;
|
|
},
|
|
|
|
bulkCopyMemories() {
|
|
const selectedMemories = this.selectedMemories;
|
|
if (selectedMemories.length === 0) return;
|
|
|
|
const content = selectedMemories
|
|
.map((memory) => this.formatMemoryForCopy(memory))
|
|
.join("\n");
|
|
|
|
this.copyToClipboard(content, false);
|
|
justToast(
|
|
`Copied ${selectedMemories.length} memories with metadata to clipboard`,
|
|
"success"
|
|
);
|
|
},
|
|
|
|
bulkExportMemories() {
|
|
const selectedMemories = this.selectedMemories;
|
|
if (selectedMemories.length === 0) return;
|
|
|
|
const exportData = {
|
|
export_timestamp: getCurrentUserISOString(),
|
|
memory_subdir: this.selectedMemorySubdir,
|
|
total_memories: selectedMemories.length,
|
|
memories: selectedMemories.map((memory) => ({
|
|
id: memory.id,
|
|
area: memory.area,
|
|
timestamp: memory.timestamp,
|
|
content: memory.content_full,
|
|
tags: memory.tags || [],
|
|
knowledge_source: memory.knowledge_source,
|
|
source_file: memory.source_file || null,
|
|
metadata: memory.metadata || {},
|
|
})),
|
|
};
|
|
|
|
const jsonString = JSON.stringify(exportData, null, 2);
|
|
const blob = new Blob([jsonString], { type: "application/json" });
|
|
const url = URL.createObjectURL(blob);
|
|
|
|
const timestamp = getCurrentUserDateString();
|
|
const filename = `memories_${this.selectedMemorySubdir}_selected_${selectedMemories.length}_${timestamp}.json`;
|
|
|
|
const a = document.createElement("a");
|
|
a.href = url;
|
|
a.download = filename;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
URL.revokeObjectURL(url);
|
|
|
|
justToast(
|
|
`Exported ${selectedMemories.length} selected memories to ${filename}`,
|
|
"success"
|
|
);
|
|
},
|
|
|
|
// Memory detail modal (standard approach)
|
|
showMemoryDetails(memory) {
|
|
this.detailMemory = memory;
|
|
this.editMode = false;
|
|
this.editMemoryBackup = null;
|
|
// Use global modal system
|
|
openModal("/plugins/_memory/webui/memory-detail-modal.html");
|
|
},
|
|
|
|
closeMemoryDetails() {
|
|
this.detailMemory = null;
|
|
},
|
|
|
|
// Utilities
|
|
formatTimestamp(timestamp, compact = false) {
|
|
if (!timestamp || timestamp === "unknown") {
|
|
return "Unknown";
|
|
}
|
|
|
|
const date = new Date(timestamp);
|
|
if (isNaN(date.getTime())) {
|
|
return "Invalid Date";
|
|
}
|
|
|
|
if (compact) {
|
|
const hour12 = getUserHour12();
|
|
// For table display: MM/DD HH:mm
|
|
return new Intl.DateTimeFormat("en-US", {
|
|
month: "2-digit",
|
|
day: "2-digit",
|
|
hour12,
|
|
hour: hour12 ? "numeric" : "2-digit",
|
|
minute: "2-digit",
|
|
timeZone: getUserTimezone(),
|
|
}).format(date);
|
|
} else {
|
|
const hour12 = getUserHour12();
|
|
// For details: Full format
|
|
return new Intl.DateTimeFormat("en-US", {
|
|
year: "numeric",
|
|
month: "long",
|
|
day: "numeric",
|
|
hour12,
|
|
hour: hour12 ? "numeric" : "2-digit",
|
|
minute: "2-digit",
|
|
timeZone: getUserTimezone(),
|
|
}).format(date);
|
|
}
|
|
},
|
|
|
|
formatTags(tags) {
|
|
if (!Array.isArray(tags) || tags.length === 0) return "None";
|
|
return tags.join(", ");
|
|
},
|
|
|
|
getAreaColor(area) {
|
|
const colors = {
|
|
main: "#3b82f6",
|
|
fragments: "#10b981",
|
|
solutions: "#8b5cf6",
|
|
skills: "#f59e0b",
|
|
};
|
|
return colors[area] || "#6c757d";
|
|
},
|
|
|
|
copyToClipboard(text, toastSuccess = true) {
|
|
if (navigator.clipboard && window.isSecureContext) {
|
|
navigator.clipboard
|
|
.writeText(text)
|
|
.then(() => {
|
|
if(toastSuccess)
|
|
justToast("Copied to clipboard!", "success");
|
|
})
|
|
.catch((err) => {
|
|
console.error("Clipboard copy failed:", err);
|
|
this.fallbackCopyToClipboard(text, toastSuccess);
|
|
});
|
|
} else {
|
|
this.fallbackCopyToClipboard(text, toastSuccess);
|
|
}
|
|
},
|
|
|
|
fallbackCopyToClipboard(text, toastSuccess = true) {
|
|
const textArea = document.createElement("textarea");
|
|
textArea.value = text;
|
|
textArea.style.position = "fixed";
|
|
textArea.style.left = "-999999px";
|
|
textArea.style.top = "-999999px";
|
|
document.body.appendChild(textArea);
|
|
textArea.focus();
|
|
textArea.select();
|
|
try {
|
|
document.execCommand("copy");
|
|
if(toastSuccess)
|
|
justToast("Copied to clipboard!", "success");
|
|
} catch (err) {
|
|
console.error("Fallback clipboard copy failed:", err);
|
|
justToast("Failed to copy to clipboard", "error");
|
|
}
|
|
document.body.removeChild(textArea);
|
|
},
|
|
|
|
async deleteMemory(memory) {
|
|
try {
|
|
// Check if this is the memory currently being viewed in detail modal
|
|
const isViewingThisMemory =
|
|
this.detailMemory && this.detailMemory.id === memory.id;
|
|
|
|
const response = await API.callJsonApi(MEMORY_DASHBOARD_API, {
|
|
action: "delete",
|
|
memory_subdir: this.selectedMemorySubdir,
|
|
memory_id: memory.id,
|
|
});
|
|
|
|
if (response.success) {
|
|
justToast("Memory deleted successfully", "success");
|
|
|
|
// If we were viewing this memory in detail modal, close it
|
|
if (isViewingThisMemory) {
|
|
this.detailMemory = null;
|
|
closeModal(); // Close the detail modal
|
|
}
|
|
|
|
// Let polling refresh the data instead of manual manipulation
|
|
// Trigger an immediate refresh to get updated state from backend
|
|
await this.searchMemories(true); // silent refresh
|
|
} else {
|
|
justToast(`Failed to delete memory: ${response.error}`, "error");
|
|
}
|
|
} catch (error) {
|
|
console.error("Memory deletion error:", error);
|
|
justToast("Failed to delete memory", "error");
|
|
}
|
|
},
|
|
|
|
exportMemories() {
|
|
if (this.memories.length === 0) {
|
|
justToast("No memories to export", "warning");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const exportData = {
|
|
memory_subdir: this.selectedMemorySubdir,
|
|
export_timestamp: getCurrentUserISOString(),
|
|
total_memories: this.memories.length,
|
|
search_query: this.searchQuery,
|
|
area_filter: this.areaFilter,
|
|
memories: this.memories.map((memory) => ({
|
|
id: memory.id,
|
|
area: memory.area,
|
|
timestamp: memory.timestamp,
|
|
content: memory.content_full,
|
|
metadata: memory.metadata,
|
|
})),
|
|
};
|
|
|
|
const blob = new Blob([JSON.stringify(exportData, null, 2)], {
|
|
type: "application/json",
|
|
});
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement("a");
|
|
a.href = url;
|
|
a.download = `memory-export-${this.selectedMemorySubdir}-${
|
|
getCurrentUserDateString()
|
|
}.json`;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
URL.revokeObjectURL(url);
|
|
|
|
justToast("Memory export completed", "success");
|
|
} catch (error) {
|
|
console.error("Memory export error:", error);
|
|
justToast("Failed to export memories", "error");
|
|
}
|
|
},
|
|
|
|
startPolling() {
|
|
if (!this.pollingEnabled || this.pollingInterval) {
|
|
return; // Already polling or disabled
|
|
}
|
|
|
|
this.pollingInterval = setInterval(async () => {
|
|
// Silently refresh using existing search logic
|
|
await this.searchMemories(true); // silent = true
|
|
}, 2000); // Poll every 3 seconds - reasonable for active user interactions
|
|
},
|
|
|
|
stopPolling() {
|
|
if (this.pollingInterval) {
|
|
clearInterval(this.pollingInterval);
|
|
this.pollingInterval = null;
|
|
}
|
|
},
|
|
|
|
// Call this when the dialog/component is closed or destroyed
|
|
cleanup() {
|
|
this.stopPolling();
|
|
// Clear data without triggering a new search (component is being destroyed)
|
|
this.areaFilter = "";
|
|
this.searchQuery = "";
|
|
this.memories = [];
|
|
this.totalCount = 0;
|
|
this.totalDbCount = 0;
|
|
this.knowledgeCount = 0;
|
|
this.conversationCount = 0;
|
|
this.areasCount = {};
|
|
this.message = null;
|
|
this.currentPage = 1;
|
|
this.editMemoryBackup;
|
|
},
|
|
|
|
enableEditMode() {
|
|
this.editMode = true;
|
|
this.editMemoryBackup = JSON.stringify(this.detailMemory); // store backup
|
|
},
|
|
|
|
cancelEditMode() {
|
|
this.editMode = false;
|
|
this.detailMemory = JSON.parse(this.editMemoryBackup); // restore backup
|
|
},
|
|
|
|
async confirmEditMode() {
|
|
try {
|
|
|
|
const response = await API.callJsonApi(MEMORY_DASHBOARD_API, {
|
|
action: "update",
|
|
memory_subdir: this.selectedMemorySubdir,
|
|
original: JSON.parse(this.editMemoryBackup),
|
|
edited: this.detailMemory,
|
|
});
|
|
|
|
if(response.success){
|
|
justToast("Memory updated successfully", "success");
|
|
await this.searchMemories(true); // silent refresh
|
|
}else{
|
|
justToast(`Failed to update memory: ${response.error}`, "error");
|
|
}
|
|
|
|
this.editMode = false;
|
|
this.editMemoryBackup = null; // discard backup
|
|
} catch (error) {
|
|
console.error("Error confirming edit mode:", error);
|
|
justToast("Failed to save memory changes.", "error");
|
|
}
|
|
},
|
|
};
|
|
|
|
const store = createStore("memoryDashboardStore", memoryDashboardStore);
|
|
|
|
export { store };
|