@xiaoxiamimengfb/my-opencode-mem 2.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +155 -0
- package/dist/config.d.ts +58 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +411 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +427 -0
- package/dist/plugin.d.ts +5 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +4 -0
- package/dist/services/ai/ai-provider-factory.d.ts +8 -0
- package/dist/services/ai/ai-provider-factory.d.ts.map +1 -0
- package/dist/services/ai/ai-provider-factory.js +28 -0
- package/dist/services/ai/opencode-provider.d.ts +30 -0
- package/dist/services/ai/opencode-provider.d.ts.map +1 -0
- package/dist/services/ai/opencode-provider.js +332 -0
- package/dist/services/ai/provider-config.d.ts +17 -0
- package/dist/services/ai/provider-config.d.ts.map +1 -0
- package/dist/services/ai/provider-config.js +14 -0
- package/dist/services/ai/providers/anthropic-messages.d.ts +12 -0
- package/dist/services/ai/providers/anthropic-messages.d.ts.map +1 -0
- package/dist/services/ai/providers/anthropic-messages.js +184 -0
- package/dist/services/ai/providers/base-provider.d.ts +25 -0
- package/dist/services/ai/providers/base-provider.d.ts.map +1 -0
- package/dist/services/ai/providers/base-provider.js +23 -0
- package/dist/services/ai/providers/google-gemini.d.ts +16 -0
- package/dist/services/ai/providers/google-gemini.d.ts.map +1 -0
- package/dist/services/ai/providers/google-gemini.js +228 -0
- package/dist/services/ai/providers/openai-chat-completion.d.ts +13 -0
- package/dist/services/ai/providers/openai-chat-completion.d.ts.map +1 -0
- package/dist/services/ai/providers/openai-chat-completion.js +277 -0
- package/dist/services/ai/providers/openai-responses.d.ts +14 -0
- package/dist/services/ai/providers/openai-responses.d.ts.map +1 -0
- package/dist/services/ai/providers/openai-responses.js +182 -0
- package/dist/services/ai/session/ai-session-manager.d.ts +21 -0
- package/dist/services/ai/session/ai-session-manager.d.ts.map +1 -0
- package/dist/services/ai/session/ai-session-manager.js +166 -0
- package/dist/services/ai/session/session-types.d.ts +43 -0
- package/dist/services/ai/session/session-types.d.ts.map +1 -0
- package/dist/services/ai/session/session-types.js +1 -0
- package/dist/services/ai/tools/tool-schema.d.ts +41 -0
- package/dist/services/ai/tools/tool-schema.d.ts.map +1 -0
- package/dist/services/ai/tools/tool-schema.js +24 -0
- package/dist/services/ai/validators/user-profile-validator.d.ts +13 -0
- package/dist/services/ai/validators/user-profile-validator.d.ts.map +1 -0
- package/dist/services/ai/validators/user-profile-validator.js +111 -0
- package/dist/services/api-handlers.d.ts +164 -0
- package/dist/services/api-handlers.d.ts.map +1 -0
- package/dist/services/api-handlers.js +901 -0
- package/dist/services/auto-capture.d.ts +3 -0
- package/dist/services/auto-capture.d.ts.map +1 -0
- package/dist/services/auto-capture.js +306 -0
- package/dist/services/cleanup-service.d.ts +23 -0
- package/dist/services/cleanup-service.d.ts.map +1 -0
- package/dist/services/cleanup-service.js +102 -0
- package/dist/services/client.d.ts +118 -0
- package/dist/services/client.d.ts.map +1 -0
- package/dist/services/client.js +251 -0
- package/dist/services/context.d.ts +11 -0
- package/dist/services/context.d.ts.map +1 -0
- package/dist/services/context.js +24 -0
- package/dist/services/deduplication-service.d.ts +30 -0
- package/dist/services/deduplication-service.d.ts.map +1 -0
- package/dist/services/deduplication-service.js +124 -0
- package/dist/services/embedding.d.ts +15 -0
- package/dist/services/embedding.d.ts.map +1 -0
- package/dist/services/embedding.js +106 -0
- package/dist/services/jsonc.d.ts +7 -0
- package/dist/services/jsonc.d.ts.map +1 -0
- package/dist/services/jsonc.js +76 -0
- package/dist/services/language-detector.d.ts +3 -0
- package/dist/services/language-detector.d.ts.map +1 -0
- package/dist/services/language-detector.js +16 -0
- package/dist/services/logger.d.ts +2 -0
- package/dist/services/logger.d.ts.map +1 -0
- package/dist/services/logger.js +51 -0
- package/dist/services/migration-service.d.ts +42 -0
- package/dist/services/migration-service.d.ts.map +1 -0
- package/dist/services/migration-service.js +250 -0
- package/dist/services/privacy.d.ts +3 -0
- package/dist/services/privacy.d.ts.map +1 -0
- package/dist/services/privacy.js +7 -0
- package/dist/services/secret-resolver.d.ts +2 -0
- package/dist/services/secret-resolver.d.ts.map +1 -0
- package/dist/services/secret-resolver.js +55 -0
- package/dist/services/sqlite/connection-manager.d.ts +13 -0
- package/dist/services/sqlite/connection-manager.d.ts.map +1 -0
- package/dist/services/sqlite/connection-manager.js +74 -0
- package/dist/services/sqlite/shard-manager.d.ts +23 -0
- package/dist/services/sqlite/shard-manager.d.ts.map +1 -0
- package/dist/services/sqlite/shard-manager.js +288 -0
- package/dist/services/sqlite/sqlite-bootstrap.d.ts +2 -0
- package/dist/services/sqlite/sqlite-bootstrap.d.ts.map +1 -0
- package/dist/services/sqlite/sqlite-bootstrap.js +8 -0
- package/dist/services/sqlite/types.d.ts +42 -0
- package/dist/services/sqlite/types.d.ts.map +1 -0
- package/dist/services/sqlite/types.js +1 -0
- package/dist/services/sqlite/vector-search.d.ts +29 -0
- package/dist/services/sqlite/vector-search.d.ts.map +1 -0
- package/dist/services/sqlite/vector-search.js +268 -0
- package/dist/services/tags.d.ts +24 -0
- package/dist/services/tags.d.ts.map +1 -0
- package/dist/services/tags.js +146 -0
- package/dist/services/user-memory-learning.d.ts +3 -0
- package/dist/services/user-memory-learning.d.ts.map +1 -0
- package/dist/services/user-memory-learning.js +231 -0
- package/dist/services/user-profile/profile-context.d.ts +2 -0
- package/dist/services/user-profile/profile-context.d.ts.map +1 -0
- package/dist/services/user-profile/profile-context.js +40 -0
- package/dist/services/user-profile/profile-utils.d.ts +3 -0
- package/dist/services/user-profile/profile-utils.d.ts.map +1 -0
- package/dist/services/user-profile/profile-utils.js +45 -0
- package/dist/services/user-profile/types.d.ts +46 -0
- package/dist/services/user-profile/types.d.ts.map +1 -0
- package/dist/services/user-profile/types.js +1 -0
- package/dist/services/user-profile/user-profile-manager.d.ts +23 -0
- package/dist/services/user-profile/user-profile-manager.d.ts.map +1 -0
- package/dist/services/user-profile/user-profile-manager.js +292 -0
- package/dist/services/user-prompt/user-prompt-manager.d.ts +41 -0
- package/dist/services/user-prompt/user-prompt-manager.d.ts.map +1 -0
- package/dist/services/user-prompt/user-prompt-manager.js +192 -0
- package/dist/services/vector-backends/backend-factory.d.ts +3 -0
- package/dist/services/vector-backends/backend-factory.d.ts.map +1 -0
- package/dist/services/vector-backends/backend-factory.js +104 -0
- package/dist/services/vector-backends/exact-scan-backend.d.ts +39 -0
- package/dist/services/vector-backends/exact-scan-backend.d.ts.map +1 -0
- package/dist/services/vector-backends/exact-scan-backend.js +63 -0
- package/dist/services/vector-backends/types.d.ts +51 -0
- package/dist/services/vector-backends/types.d.ts.map +1 -0
- package/dist/services/vector-backends/types.js +1 -0
- package/dist/services/vector-backends/usearch-backend.d.ts +47 -0
- package/dist/services/vector-backends/usearch-backend.d.ts.map +1 -0
- package/dist/services/vector-backends/usearch-backend.js +174 -0
- package/dist/services/web-server-worker.d.ts +2 -0
- package/dist/services/web-server-worker.d.ts.map +1 -0
- package/dist/services/web-server-worker.js +283 -0
- package/dist/services/web-server.d.ts +31 -0
- package/dist/services/web-server.d.ts.map +1 -0
- package/dist/services/web-server.js +356 -0
- package/dist/types/index.d.ts +19 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +1 -0
- package/dist/web/app.d.ts +2 -0
- package/dist/web/app.d.ts.map +1 -0
- package/dist/web/app.js +1194 -0
- package/dist/web/favicon.ico +0 -0
- package/dist/web/i18n.d.ts +2 -0
- package/dist/web/i18n.d.ts.map +1 -0
- package/dist/web/i18n.js +265 -0
- package/dist/web/index.html +284 -0
- package/dist/web/styles.css +1631 -0
- package/package.json +71 -0
package/dist/web/app.js
ADDED
|
@@ -0,0 +1,1194 @@
|
|
|
1
|
+
const API_BASE = "";
|
|
2
|
+
|
|
3
|
+
const state = {
|
|
4
|
+
tags: { project: [] },
|
|
5
|
+
memories: [],
|
|
6
|
+
currentPage: 1,
|
|
7
|
+
pageSize: 20,
|
|
8
|
+
totalPages: 1,
|
|
9
|
+
totalItems: 0,
|
|
10
|
+
selectedTag: "",
|
|
11
|
+
currentView: "project",
|
|
12
|
+
searchQuery: "",
|
|
13
|
+
isSearching: false,
|
|
14
|
+
selectedMemories: new Set(),
|
|
15
|
+
autoRefreshInterval: null,
|
|
16
|
+
userProfile: null,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
marked.setOptions({
|
|
20
|
+
gfm: true,
|
|
21
|
+
breaks: true,
|
|
22
|
+
headerIds: false,
|
|
23
|
+
mangle: false,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
function renderMarkdown(markdown) {
|
|
27
|
+
const html = marked.parse(markdown);
|
|
28
|
+
return DOMPurify.sanitize(html);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function fetchAPI(endpoint, options = {}) {
|
|
32
|
+
try {
|
|
33
|
+
const controller = new AbortController();
|
|
34
|
+
const timeoutId = setTimeout(() => controller.abort(), 60000);
|
|
35
|
+
const response = await fetch(API_BASE + endpoint, {
|
|
36
|
+
...options,
|
|
37
|
+
signal: controller.signal,
|
|
38
|
+
});
|
|
39
|
+
clearTimeout(timeoutId);
|
|
40
|
+
const data = await response.json();
|
|
41
|
+
return data;
|
|
42
|
+
} catch (error) {
|
|
43
|
+
console.error("API Error:", error);
|
|
44
|
+
return { success: false, error: error.message };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function loadTags() {
|
|
49
|
+
const result = await fetchAPI("/api/tags");
|
|
50
|
+
if (result.success) {
|
|
51
|
+
state.tags = result.data;
|
|
52
|
+
populateTagDropdowns();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function populateTagDropdowns() {
|
|
57
|
+
const tagFilter = document.getElementById("tag-filter");
|
|
58
|
+
const addTag = document.getElementById("add-tag");
|
|
59
|
+
|
|
60
|
+
tagFilter.innerHTML = `<option value="">${t("opt-all-tags")}</option>`;
|
|
61
|
+
addTag.innerHTML = `<option value="">${t("opt-select-tag")}</option>`;
|
|
62
|
+
|
|
63
|
+
const scopeTags = state.tags.project;
|
|
64
|
+
|
|
65
|
+
scopeTags.forEach((tagInfo) => {
|
|
66
|
+
const displayText = tagInfo.displayName || tagInfo.tag;
|
|
67
|
+
const shortDisplay =
|
|
68
|
+
displayText.length > 50 ? displayText.substring(0, 50) + "..." : displayText;
|
|
69
|
+
|
|
70
|
+
const option1 = document.createElement("option");
|
|
71
|
+
option1.value = tagInfo.tag;
|
|
72
|
+
option1.textContent = shortDisplay;
|
|
73
|
+
tagFilter.appendChild(option1);
|
|
74
|
+
|
|
75
|
+
const option2 = document.createElement("option");
|
|
76
|
+
option2.value = tagInfo.tag;
|
|
77
|
+
option2.textContent = shortDisplay;
|
|
78
|
+
addTag.appendChild(option2);
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function renderMemories() {
|
|
83
|
+
const container = document.getElementById("memories-list");
|
|
84
|
+
|
|
85
|
+
if (state.memories.length === 0) {
|
|
86
|
+
container.innerHTML = `<div class="empty-state">${t("empty-memories")}</div>`;
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
container.innerHTML = groupMemories(state.memories)
|
|
91
|
+
.map((group) => {
|
|
92
|
+
if (group.isPair) {
|
|
93
|
+
return renderCombinedCard(group);
|
|
94
|
+
} else if (group.type === "prompt") {
|
|
95
|
+
return renderPromptCard(group.item);
|
|
96
|
+
} else {
|
|
97
|
+
return renderMemoryCard(group.item);
|
|
98
|
+
}
|
|
99
|
+
})
|
|
100
|
+
.join("");
|
|
101
|
+
|
|
102
|
+
document.querySelectorAll(".memory-checkbox").forEach((checkbox) => {
|
|
103
|
+
checkbox.addEventListener("change", handleCheckboxChange);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
lucide.createIcons();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function groupMemories(items) {
|
|
110
|
+
const map = new Map();
|
|
111
|
+
const pairs = [];
|
|
112
|
+
const processed = new Set();
|
|
113
|
+
|
|
114
|
+
items.forEach((item) => map.set(item.id, item));
|
|
115
|
+
|
|
116
|
+
items.forEach((item) => {
|
|
117
|
+
if (processed.has(item.id)) return;
|
|
118
|
+
|
|
119
|
+
if (item.type === "memory" && item.linkedPromptId && map.has(item.linkedPromptId)) {
|
|
120
|
+
const prompt = map.get(item.linkedPromptId);
|
|
121
|
+
pairs.push({ isPair: true, memory: item, prompt: prompt });
|
|
122
|
+
processed.add(item.id);
|
|
123
|
+
processed.add(prompt.id);
|
|
124
|
+
} else if (item.type === "prompt" && item.linkedMemoryId && map.has(item.linkedMemoryId)) {
|
|
125
|
+
const memory = map.get(item.linkedMemoryId);
|
|
126
|
+
pairs.push({ isPair: true, memory: memory, prompt: item });
|
|
127
|
+
processed.add(item.id);
|
|
128
|
+
processed.add(memory.id);
|
|
129
|
+
} else {
|
|
130
|
+
pairs.push({ isPair: false, type: item.type, item: item });
|
|
131
|
+
processed.add(item.id);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
return pairs.sort((a, b) => {
|
|
136
|
+
const timeA = a.isPair ? a.memory.createdAt : a.item.createdAt;
|
|
137
|
+
const timeB = b.isPair ? b.memory.createdAt : b.item.createdAt;
|
|
138
|
+
return new Date(timeB) - new Date(timeA);
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function renderCombinedCard(pair) {
|
|
143
|
+
const { memory, prompt } = pair;
|
|
144
|
+
const isSelected = state.selectedMemories.has(memory.id);
|
|
145
|
+
const isPinned = memory.isPinned || false;
|
|
146
|
+
const similarityHtml =
|
|
147
|
+
memory.similarity !== undefined
|
|
148
|
+
? `<span class="similarity-score">${Math.round(memory.similarity * 100)}%</span>`
|
|
149
|
+
: "";
|
|
150
|
+
|
|
151
|
+
const tagsHtml =
|
|
152
|
+
memory.tags && memory.tags.length > 0
|
|
153
|
+
? `<div class="tags-list">${memory.tags.map((t) => `<span class="tag-badge">${escapeHtml(t)}</span>`).join("")}</div>`
|
|
154
|
+
: "";
|
|
155
|
+
|
|
156
|
+
const pinButton = isPinned
|
|
157
|
+
? `<button class="btn-pin pinned" onclick="unpinMemory('${memory.id}')" title="Unpin"><i data-lucide="pin" class="icon icon-filled"></i></button>`
|
|
158
|
+
: `<button class="btn-pin" onclick="pinMemory('${memory.id}')" title="Pin"><i data-lucide="pin" class="icon"></i></button>`;
|
|
159
|
+
|
|
160
|
+
const createdDate = formatDate(memory.createdAt);
|
|
161
|
+
const updatedDate =
|
|
162
|
+
memory.updatedAt && memory.updatedAt !== memory.createdAt ? formatDate(memory.updatedAt) : null;
|
|
163
|
+
|
|
164
|
+
const dateInfo = updatedDate
|
|
165
|
+
? `<span>${t("date-created")} ${createdDate}</span><span>${t("date-updated")} ${updatedDate}</span>`
|
|
166
|
+
: `<span>${t("date-created")} ${createdDate}</span>`;
|
|
167
|
+
return `
|
|
168
|
+
<div class="combined-card ${isSelected ? "selected" : ""} ${isPinned ? "pinned" : ""}" data-id="${memory.id}">
|
|
169
|
+
<div class="combined-prompt-section">
|
|
170
|
+
<div class="combined-header">
|
|
171
|
+
<span class="badge badge-prompt">${t("badge-prompt")}</span>
|
|
172
|
+
<span class="prompt-date">${formatDate(prompt.createdAt)}</span>
|
|
173
|
+
</div>
|
|
174
|
+
<div class="prompt-content">${escapeHtml(prompt.content)}</div>
|
|
175
|
+
</div>
|
|
176
|
+
|
|
177
|
+
<div class="combined-divider">
|
|
178
|
+
<i data-lucide="arrow-down" class="divider-icon"></i>
|
|
179
|
+
</div>
|
|
180
|
+
|
|
181
|
+
<div class="combined-memory-section">
|
|
182
|
+
<div class="memory-header">
|
|
183
|
+
<div class="meta">
|
|
184
|
+
<input type="checkbox" class="memory-checkbox" data-id="${memory.id}" ${isSelected ? "checked" : ""} />
|
|
185
|
+
<span class="badge badge-memory">${t("badge-memory")}</span>
|
|
186
|
+
${memory.memoryType ? `<span class="badge badge-type">${memory.memoryType}</span>` : ""}
|
|
187
|
+
${similarityHtml}
|
|
188
|
+
${isPinned ? `<span class="badge badge-pinned">${t("badge-pinned")}</span>` : ""}
|
|
189
|
+
<span class="memory-display-name">${escapeHtml(memory.displayName || memory.id)}</span>
|
|
190
|
+
</div>
|
|
191
|
+
<div class="memory-actions">
|
|
192
|
+
${pinButton}
|
|
193
|
+
<button class="btn-edit" onclick="editMemory('${memory.id}')"><i data-lucide="edit-3" class="icon"></i></button>
|
|
194
|
+
<button class="btn-delete" onclick="deleteMemoryWithLink('${memory.id}', true)">
|
|
195
|
+
<i data-lucide="trash-2" class="icon"></i> ${t("btn-delete-pair")}
|
|
196
|
+
</button>
|
|
197
|
+
</div>
|
|
198
|
+
</div>
|
|
199
|
+
${tagsHtml}
|
|
200
|
+
<div class="memory-content markdown-content">${renderMarkdown(memory.content)}</div>
|
|
201
|
+
<div class="memory-footer">
|
|
202
|
+
${dateInfo}
|
|
203
|
+
<span>ID: ${memory.id}</span>
|
|
204
|
+
</div>
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
`;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function renderPromptCard(prompt) {
|
|
211
|
+
const isLinked = !!prompt.linkedMemoryId;
|
|
212
|
+
const isSelected = state.selectedMemories.has(prompt.id);
|
|
213
|
+
const promptDate = formatDate(prompt.createdAt);
|
|
214
|
+
|
|
215
|
+
return `
|
|
216
|
+
<div class="prompt-card ${isSelected ? "selected" : ""}" data-id="${prompt.id}">
|
|
217
|
+
<div class="prompt-header">
|
|
218
|
+
<div class="meta">
|
|
219
|
+
<input type="checkbox" class="memory-checkbox" data-id="${prompt.id}" ${isSelected ? "checked" : ""} />
|
|
220
|
+
<i data-lucide="message-circle" class="icon"></i>
|
|
221
|
+
<span class="badge badge-prompt">${t("badge-prompt")}</span>
|
|
222
|
+
${isLinked ? `<span class="badge badge-linked"><i data-lucide="link" class="icon-sm"></i> ${t("badge-linked")}</span>` : ""}
|
|
223
|
+
<span class="prompt-date">${promptDate}</span>
|
|
224
|
+
</div>
|
|
225
|
+
<div class="prompt-actions">
|
|
226
|
+
<button class="btn-delete" onclick="deletePromptWithLink('${prompt.id}', ${isLinked})">
|
|
227
|
+
<i data-lucide="trash-2" class="icon"></i>
|
|
228
|
+
${isLinked ? t("btn-delete-pair") : t("btn-delete")}
|
|
229
|
+
</button>
|
|
230
|
+
</div>
|
|
231
|
+
</div>
|
|
232
|
+
<div class="prompt-content">
|
|
233
|
+
${escapeHtml(prompt.content)}
|
|
234
|
+
</div>
|
|
235
|
+
${isLinked ? `<div class="link-indicator"><i data-lucide="arrow-down" class="icon-sm"></i> ${t("text-generated-above")} <i data-lucide="arrow-up" class="icon-sm"></i></div>` : ""}
|
|
236
|
+
</div>
|
|
237
|
+
`;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function renderMemoryCard(memory) {
|
|
241
|
+
const isSelected = state.selectedMemories.has(memory.id);
|
|
242
|
+
const isPinned = memory.isPinned || false;
|
|
243
|
+
const isLinked = !!memory.linkedPromptId;
|
|
244
|
+
const similarityHtml =
|
|
245
|
+
memory.similarity !== undefined
|
|
246
|
+
? `<span class="similarity-score">${memory.similarity}%</span>`
|
|
247
|
+
: "";
|
|
248
|
+
|
|
249
|
+
let displayInfo = memory.displayName || memory.id;
|
|
250
|
+
if (memory.projectPath) {
|
|
251
|
+
const pathParts = memory.projectPath
|
|
252
|
+
.replace(/\\/g, "/")
|
|
253
|
+
.split("/")
|
|
254
|
+
.filter((p) => p);
|
|
255
|
+
displayInfo = pathParts[pathParts.length - 1] || memory.projectPath;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
let subtitle = "";
|
|
259
|
+
if (memory.projectPath) {
|
|
260
|
+
subtitle = `<span class="memory-subtitle">${escapeHtml(memory.projectPath)}</span>`;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const pinButton = isPinned
|
|
264
|
+
? `<button class="btn-pin pinned" onclick="unpinMemory('${memory.id}')" title="Unpin"><i data-lucide="pin" class="icon icon-filled"></i></button>`
|
|
265
|
+
: `<button class="btn-pin" onclick="pinMemory('${memory.id}')" title="Pin"><i data-lucide="pin" class="icon"></i></button>`;
|
|
266
|
+
|
|
267
|
+
const createdDate = formatDate(memory.createdAt);
|
|
268
|
+
const updatedDate =
|
|
269
|
+
memory.updatedAt && memory.updatedAt !== memory.createdAt ? formatDate(memory.updatedAt) : null;
|
|
270
|
+
|
|
271
|
+
const dateInfo = updatedDate
|
|
272
|
+
? `<span>${t("date-created")} ${createdDate}</span><span>${t("date-updated")} ${updatedDate}</span>`
|
|
273
|
+
: `<span>${t("date-created")} ${createdDate}</span>`;
|
|
274
|
+
const tagsHtml =
|
|
275
|
+
memory.tags && memory.tags.length > 0
|
|
276
|
+
? `<div class="tags-list">${memory.tags.map((t) => `<span class="tag-badge">${escapeHtml(t)}</span>`).join("")}</div>`
|
|
277
|
+
: "";
|
|
278
|
+
|
|
279
|
+
return `
|
|
280
|
+
<div class="memory-card ${isSelected ? "selected" : ""} ${isPinned ? "pinned" : ""}" data-id="${memory.id}">
|
|
281
|
+
<div class="memory-header">
|
|
282
|
+
<div class="meta">
|
|
283
|
+
<input type="checkbox" class="memory-checkbox" data-id="${memory.id}" ${isSelected ? "checked" : ""} />
|
|
284
|
+
${memory.memoryType ? `<span class="badge badge-type">${memory.memoryType}</span>` : ""}
|
|
285
|
+
${isLinked ? `<span class="badge badge-linked"><i data-lucide="link" class="icon-sm"></i> ${t("badge-linked")}</span>` : ""}
|
|
286
|
+
${similarityHtml}
|
|
287
|
+
${isPinned ? `<span class="badge badge-pinned">${t("badge-pinned")}</span>` : ""}
|
|
288
|
+
<span class="memory-display-name">${escapeHtml(displayInfo)}</span>
|
|
289
|
+
${subtitle}
|
|
290
|
+
</div>
|
|
291
|
+
<div class="memory-actions">
|
|
292
|
+
${pinButton}
|
|
293
|
+
<button class="btn-edit" onclick="editMemory('${memory.id}')"><i data-lucide="edit-3" class="icon"></i></button>
|
|
294
|
+
<button class="btn-delete" onclick="deleteMemoryWithLink('${memory.id}', ${isLinked})">
|
|
295
|
+
<i data-lucide="trash-2" class="icon"></i>
|
|
296
|
+
${isLinked ? t("btn-delete-pair") : t("btn-delete")}
|
|
297
|
+
</button>
|
|
298
|
+
</div>
|
|
299
|
+
</div>
|
|
300
|
+
${tagsHtml}
|
|
301
|
+
<div class="memory-content markdown-content">${renderMarkdown(memory.content)}</div>
|
|
302
|
+
${isLinked ? `<div class="link-indicator"><i data-lucide="arrow-up" class="icon-sm"></i> ${t("text-from-below")} <i data-lucide="arrow-down" class="icon-sm"></i></div>` : ""}
|
|
303
|
+
<div class="memory-footer">
|
|
304
|
+
${dateInfo}
|
|
305
|
+
<span>ID: ${memory.id}</span>
|
|
306
|
+
</div>
|
|
307
|
+
</div>
|
|
308
|
+
`;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function handleCheckboxChange(e) {
|
|
312
|
+
const id = e.target.dataset.id;
|
|
313
|
+
if (e.target.checked) {
|
|
314
|
+
state.selectedMemories.add(id);
|
|
315
|
+
} else {
|
|
316
|
+
state.selectedMemories.delete(id);
|
|
317
|
+
}
|
|
318
|
+
updateBulkActions();
|
|
319
|
+
updateCardSelection(id, e.target.checked);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function updateCardSelection(id, selected) {
|
|
323
|
+
const card = document.querySelector(
|
|
324
|
+
`.memory-card[data-id="${id}"], .prompt-card[data-id="${id}"]`
|
|
325
|
+
);
|
|
326
|
+
if (card) {
|
|
327
|
+
if (selected) {
|
|
328
|
+
card.classList.add("selected");
|
|
329
|
+
} else {
|
|
330
|
+
card.classList.remove("selected");
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function updateBulkActions() {
|
|
336
|
+
const bulkActions = document.getElementById("bulk-actions");
|
|
337
|
+
const selectedCount = document.getElementById("selected-count");
|
|
338
|
+
|
|
339
|
+
if (state.selectedMemories.size > 0) {
|
|
340
|
+
bulkActions.classList.remove("hidden");
|
|
341
|
+
selectedCount.textContent = t("text-selected", { count: state.selectedMemories.size });
|
|
342
|
+
} else {
|
|
343
|
+
bulkActions.classList.add("hidden");
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function updatePagination() {
|
|
348
|
+
const pageInfo = t("text-page", { current: state.currentPage, total: state.totalPages });
|
|
349
|
+
document.getElementById("page-info-top").textContent = pageInfo;
|
|
350
|
+
document.getElementById("page-info-bottom").textContent = pageInfo;
|
|
351
|
+
const hasPrev = state.currentPage > 1;
|
|
352
|
+
const hasNext = state.currentPage < state.totalPages;
|
|
353
|
+
|
|
354
|
+
document.getElementById("prev-page-top").disabled = !hasPrev;
|
|
355
|
+
document.getElementById("next-page-top").disabled = !hasNext;
|
|
356
|
+
document.getElementById("prev-page-bottom").disabled = !hasPrev;
|
|
357
|
+
document.getElementById("next-page-bottom").disabled = !hasNext;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function updateSectionTitle() {
|
|
361
|
+
const title = state.isSearching
|
|
362
|
+
? `└─ SEARCH RESULTS (${state.totalItems}) ──`
|
|
363
|
+
: t("section-project", { count: state.totalItems });
|
|
364
|
+
document.getElementById("section-title").textContent = title;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
async function loadStats() {
|
|
368
|
+
const result = await fetchAPI("/api/stats");
|
|
369
|
+
if (result.success) {
|
|
370
|
+
document.getElementById("stats-total").textContent = t("text-total", {
|
|
371
|
+
count: result.data.total,
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
async function addMemory(e) {
|
|
377
|
+
e.preventDefault();
|
|
378
|
+
|
|
379
|
+
const content = document.getElementById("add-content").value.trim();
|
|
380
|
+
const containerTag = document.getElementById("add-tag").value;
|
|
381
|
+
const type = document.getElementById("add-type").value;
|
|
382
|
+
const tagsStr = document.getElementById("add-tags").value.trim();
|
|
383
|
+
const tags = tagsStr
|
|
384
|
+
? tagsStr
|
|
385
|
+
.split(",")
|
|
386
|
+
.map((t) => t.trim())
|
|
387
|
+
.filter((t) => t)
|
|
388
|
+
: [];
|
|
389
|
+
|
|
390
|
+
if (!content || !containerTag) {
|
|
391
|
+
showToast(t("toast-add-error"), "error");
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const result = await fetchAPI("/api/memories", {
|
|
396
|
+
method: "POST",
|
|
397
|
+
headers: { "Content-Type": "application/json" },
|
|
398
|
+
body: JSON.stringify({ content, containerTag, type: type || undefined, tags }),
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
if (result.success) {
|
|
402
|
+
showToast(t("toast-add-success"), "success");
|
|
403
|
+
document.getElementById("add-form").reset();
|
|
404
|
+
await loadMemories();
|
|
405
|
+
await loadStats();
|
|
406
|
+
} else {
|
|
407
|
+
showToast(result.error || t("toast-add-failed"), "error");
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
async function loadMemories() {
|
|
412
|
+
showRefreshIndicator(true);
|
|
413
|
+
|
|
414
|
+
let endpoint = `/api/memories?page=${state.currentPage}&pageSize=${state.pageSize}&includePrompts=true`;
|
|
415
|
+
|
|
416
|
+
if (state.isSearching) {
|
|
417
|
+
endpoint = `/api/search?q=${encodeURIComponent(state.searchQuery || "")}&page=${state.currentPage}&pageSize=${state.pageSize}`;
|
|
418
|
+
if (state.selectedTag) {
|
|
419
|
+
endpoint += `&tag=${encodeURIComponent(state.selectedTag)}`;
|
|
420
|
+
}
|
|
421
|
+
} else {
|
|
422
|
+
if (state.selectedTag) {
|
|
423
|
+
endpoint += `&tag=${encodeURIComponent(state.selectedTag)}`;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const result = await fetchAPI(endpoint);
|
|
428
|
+
|
|
429
|
+
showRefreshIndicator(false);
|
|
430
|
+
|
|
431
|
+
if (result.success) {
|
|
432
|
+
state.memories = result.data.items;
|
|
433
|
+
state.totalPages = result.data.totalPages;
|
|
434
|
+
state.totalItems = result.data.total;
|
|
435
|
+
state.currentPage = result.data.page;
|
|
436
|
+
|
|
437
|
+
renderMemories();
|
|
438
|
+
updatePagination();
|
|
439
|
+
updateSectionTitle();
|
|
440
|
+
} else {
|
|
441
|
+
showError(result.error || t("toast-update-failed"));
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
async function deleteMemoryWithLink(id, isLinked) {
|
|
446
|
+
const message = isLinked ? t("confirm-delete-pair") : t("confirm-delete");
|
|
447
|
+
if (!confirm(message)) return;
|
|
448
|
+
|
|
449
|
+
const result = await fetchAPI(`/api/memories/${id}?cascade=true`, {
|
|
450
|
+
method: "DELETE",
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
if (result.success) {
|
|
454
|
+
showToast(t("toast-delete-success"), "success");
|
|
455
|
+
|
|
456
|
+
state.selectedMemories.delete(id);
|
|
457
|
+
await loadMemories();
|
|
458
|
+
await loadStats();
|
|
459
|
+
} else {
|
|
460
|
+
showToast(result.error || t("toast-delete-failed"), "error");
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
async function deletePromptWithLink(id, isLinked) {
|
|
465
|
+
const message = isLinked ? t("confirm-delete-prompt") : t("confirm-delete");
|
|
466
|
+
if (!confirm(message)) return;
|
|
467
|
+
|
|
468
|
+
const result = await fetchAPI(`/api/prompts/${id}?cascade=true`, {
|
|
469
|
+
method: "DELETE",
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
if (result.success) {
|
|
473
|
+
showToast(t("toast-delete-success"), "success");
|
|
474
|
+
|
|
475
|
+
state.selectedMemories.delete(id);
|
|
476
|
+
await loadMemories();
|
|
477
|
+
await loadStats();
|
|
478
|
+
} else {
|
|
479
|
+
showToast(result.error || t("toast-delete-failed"), "error");
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
async function bulkDelete() {
|
|
484
|
+
if (state.selectedMemories.size === 0) return;
|
|
485
|
+
|
|
486
|
+
const message = t("confirm-bulk-delete", { count: state.selectedMemories.size });
|
|
487
|
+
if (!confirm(message)) return;
|
|
488
|
+
|
|
489
|
+
const ids = Array.from(state.selectedMemories);
|
|
490
|
+
|
|
491
|
+
const promptIds = ids.filter((id) => id.startsWith("prompt_"));
|
|
492
|
+
const memoryIds = ids.filter((id) => !id.startsWith("prompt_"));
|
|
493
|
+
|
|
494
|
+
let deletedCount = 0;
|
|
495
|
+
|
|
496
|
+
if (promptIds.length > 0) {
|
|
497
|
+
const result = await fetchAPI("/api/prompts/bulk-delete", {
|
|
498
|
+
method: "POST",
|
|
499
|
+
headers: { "Content-Type": "application/json" },
|
|
500
|
+
body: JSON.stringify({ ids: promptIds, cascade: true }),
|
|
501
|
+
});
|
|
502
|
+
if (result.success) deletedCount += result.data.deleted;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
if (memoryIds.length > 0) {
|
|
506
|
+
const result = await fetchAPI("/api/memories/bulk-delete", {
|
|
507
|
+
method: "POST",
|
|
508
|
+
headers: { "Content-Type": "application/json" },
|
|
509
|
+
body: JSON.stringify({ ids: memoryIds, cascade: true }),
|
|
510
|
+
});
|
|
511
|
+
if (result.success) deletedCount += result.data.deleted;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
showToast(t("toast-bulk-delete-success"), "success");
|
|
515
|
+
state.selectedMemories.clear();
|
|
516
|
+
await loadMemories();
|
|
517
|
+
await loadStats();
|
|
518
|
+
updateBulkActions();
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function deselectAll() {
|
|
522
|
+
state.selectedMemories.clear();
|
|
523
|
+
document.querySelectorAll(".memory-checkbox").forEach((cb) => (cb.checked = false));
|
|
524
|
+
document
|
|
525
|
+
.querySelectorAll(".memory-card, .prompt-card")
|
|
526
|
+
.forEach((card) => card.classList.remove("selected"));
|
|
527
|
+
updateBulkActions();
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function editMemory(id) {
|
|
531
|
+
const memory = state.memories.find((m) => m.id === id && m.type === "memory");
|
|
532
|
+
if (!memory) return;
|
|
533
|
+
|
|
534
|
+
document.getElementById("edit-id").value = memory.id;
|
|
535
|
+
document.getElementById("edit-content").value = memory.content;
|
|
536
|
+
|
|
537
|
+
document.getElementById("edit-modal").classList.remove("hidden");
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
async function saveEdit(e) {
|
|
541
|
+
e.preventDefault();
|
|
542
|
+
|
|
543
|
+
const id = document.getElementById("edit-id").value;
|
|
544
|
+
const content = document.getElementById("edit-content").value.trim();
|
|
545
|
+
|
|
546
|
+
if (!content) {
|
|
547
|
+
showToast(t("toast-add-error"), "error");
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
const result = await fetchAPI(`/api/memories/${id}`, {
|
|
552
|
+
method: "PUT",
|
|
553
|
+
headers: { "Content-Type": "application/json" },
|
|
554
|
+
body: JSON.stringify({ content }),
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
if (result.success) {
|
|
558
|
+
showToast(t("toast-update-success"), "success");
|
|
559
|
+
closeModal();
|
|
560
|
+
await loadMemories();
|
|
561
|
+
} else {
|
|
562
|
+
showToast(result.error || t("toast-update-failed"), "error");
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function closeModal() {
|
|
567
|
+
document.getElementById("edit-modal").classList.add("hidden");
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function performSearch() {
|
|
571
|
+
const query = document.getElementById("search-input").value.trim();
|
|
572
|
+
|
|
573
|
+
if (!query) {
|
|
574
|
+
clearSearch();
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
state.searchQuery = query;
|
|
579
|
+
state.isSearching = true;
|
|
580
|
+
state.currentPage = 1;
|
|
581
|
+
|
|
582
|
+
document.getElementById("clear-search-btn").classList.remove("hidden");
|
|
583
|
+
|
|
584
|
+
loadMemories();
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function clearSearch() {
|
|
588
|
+
state.searchQuery = "";
|
|
589
|
+
state.isSearching = false;
|
|
590
|
+
state.currentPage = 1;
|
|
591
|
+
|
|
592
|
+
document.getElementById("search-input").value = "";
|
|
593
|
+
document.getElementById("clear-search-btn").classList.add("hidden");
|
|
594
|
+
|
|
595
|
+
loadMemories();
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function changePage(delta) {
|
|
599
|
+
const newPage = state.currentPage + delta;
|
|
600
|
+
if (newPage < 1 || newPage > state.totalPages) return;
|
|
601
|
+
|
|
602
|
+
state.currentPage = newPage;
|
|
603
|
+
loadMemories();
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function showToast(message, type = "success") {
|
|
607
|
+
const toast = document.getElementById("toast");
|
|
608
|
+
toast.textContent = message;
|
|
609
|
+
toast.className = `toast ${type}`;
|
|
610
|
+
toast.classList.remove("hidden");
|
|
611
|
+
|
|
612
|
+
setTimeout(() => {
|
|
613
|
+
toast.classList.add("hidden");
|
|
614
|
+
}, 3000);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function showError(message) {
|
|
618
|
+
const container = document.getElementById("memories-list");
|
|
619
|
+
container.innerHTML = `<div class="error-state">Error: ${escapeHtml(message)}</div>`;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function showRefreshIndicator(show) {
|
|
623
|
+
const indicator = document.getElementById("refresh-indicator");
|
|
624
|
+
if (show) {
|
|
625
|
+
indicator.classList.remove("hidden");
|
|
626
|
+
} else {
|
|
627
|
+
indicator.classList.add("hidden");
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
function formatDate(isoString) {
|
|
632
|
+
const date = new Date(isoString);
|
|
633
|
+
const locale = getLanguage() === "zh" ? "zh-CN" : "en-US";
|
|
634
|
+
return date.toLocaleString(locale, {
|
|
635
|
+
year: "numeric",
|
|
636
|
+
month: "short",
|
|
637
|
+
day: "numeric",
|
|
638
|
+
hour: "2-digit",
|
|
639
|
+
minute: "2-digit",
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
async function pinMemory(id) {
|
|
644
|
+
const result = await fetchAPI(`/api/memories/${id}/pin`, { method: "POST" });
|
|
645
|
+
|
|
646
|
+
if (result.success) {
|
|
647
|
+
showToast(t("toast-update-success"), "success");
|
|
648
|
+
await loadMemories();
|
|
649
|
+
} else {
|
|
650
|
+
showToast(result.error || t("toast-update-failed"), "error");
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
async function unpinMemory(id) {
|
|
655
|
+
const result = await fetchAPI(`/api/memories/${id}/unpin`, { method: "POST" });
|
|
656
|
+
|
|
657
|
+
if (result.success) {
|
|
658
|
+
showToast(t("toast-update-success"), "success");
|
|
659
|
+
await loadMemories();
|
|
660
|
+
} else {
|
|
661
|
+
showToast(result.error || t("toast-update-failed"), "error");
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
async function runCleanup() {
|
|
666
|
+
if (!confirm(t("confirm-cleanup"))) return;
|
|
667
|
+
|
|
668
|
+
showToast(t("status-cleanup"), "info");
|
|
669
|
+
const result = await fetchAPI("/api/cleanup", { method: "POST" });
|
|
670
|
+
|
|
671
|
+
if (result.success) {
|
|
672
|
+
showToast(t("toast-cleanup-success"), "success");
|
|
673
|
+
await loadMemories();
|
|
674
|
+
await loadStats();
|
|
675
|
+
} else {
|
|
676
|
+
showToast(result.error || t("toast-cleanup-failed"), "error");
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
async function runDeduplication() {
|
|
681
|
+
if (!confirm(t("confirm-dedup"))) return;
|
|
682
|
+
|
|
683
|
+
showToast(t("status-dedup"), "info");
|
|
684
|
+
const result = await fetchAPI("/api/deduplicate", { method: "POST" });
|
|
685
|
+
|
|
686
|
+
if (result.success) {
|
|
687
|
+
showToast(t("toast-dedup-success"), "success");
|
|
688
|
+
await loadMemories();
|
|
689
|
+
await loadStats();
|
|
690
|
+
} else {
|
|
691
|
+
showToast(result.error || t("toast-dedup-failed"), "error");
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
function startAutoRefresh() {
|
|
696
|
+
if (state.autoRefreshInterval) {
|
|
697
|
+
clearInterval(state.autoRefreshInterval);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
state.autoRefreshInterval = setInterval(() => {
|
|
701
|
+
loadStats();
|
|
702
|
+
if (!state.isSearching) {
|
|
703
|
+
loadMemories();
|
|
704
|
+
}
|
|
705
|
+
}, 30000);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
async function checkMigrationStatus() {
|
|
709
|
+
const result = await fetchAPI("/api/migration/detect");
|
|
710
|
+
if (result.success && result.data.needsMigration) {
|
|
711
|
+
showMigrationWarning(result.data);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
const tagResult = await fetchAPI("/api/migration/tags/detect");
|
|
715
|
+
if (tagResult.success && tagResult.data.needsMigration) {
|
|
716
|
+
showTagMigrationModal(tagResult.data.count);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
function showTagMigrationModal(count) {
|
|
721
|
+
const overlay = document.getElementById("tag-migration-overlay");
|
|
722
|
+
const status = document.getElementById("tag-migration-status");
|
|
723
|
+
status.textContent = t("migration-found-tags", { count });
|
|
724
|
+
|
|
725
|
+
document.getElementById("start-tag-migration-btn").onclick = runTagMigration;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
async function runTagMigration() {
|
|
729
|
+
const actions = document.getElementById("tag-migration-actions");
|
|
730
|
+
const status = document.getElementById("tag-migration-status");
|
|
731
|
+
const progress = document.getElementById("tag-migration-progress");
|
|
732
|
+
|
|
733
|
+
actions.classList.add("hidden");
|
|
734
|
+
status.textContent = t("status-migration-init");
|
|
735
|
+
progress.style.width = "0%";
|
|
736
|
+
|
|
737
|
+
let totalProcessed = 0;
|
|
738
|
+
let hasMore = true;
|
|
739
|
+
let attempts = 0;
|
|
740
|
+
const maxAttempts = 1000;
|
|
741
|
+
|
|
742
|
+
while (hasMore && attempts < maxAttempts) {
|
|
743
|
+
attempts++;
|
|
744
|
+
const result = await fetchAPI("/api/migration/tags/run-batch", {
|
|
745
|
+
method: "POST",
|
|
746
|
+
headers: { "Content-Type": "application/json" },
|
|
747
|
+
body: JSON.stringify({ batchSize: 3 }),
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
if (!result.success) {
|
|
751
|
+
status.textContent = t("toast-migration-failed") + ": " + result.error;
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
totalProcessed = result.data.processed;
|
|
756
|
+
hasMore = result.data.hasMore;
|
|
757
|
+
const total = result.data.total;
|
|
758
|
+
const percent = total > 0 ? Math.round((totalProcessed / total) * 100) : 0;
|
|
759
|
+
|
|
760
|
+
progress.style.width = percent + "%";
|
|
761
|
+
status.textContent = t("status-migration-progress", { current: totalProcessed, total: total });
|
|
762
|
+
if (hasMore) {
|
|
763
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
if (attempts >= maxAttempts) {
|
|
768
|
+
status.textContent = t("migration-stopped");
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
progress.style.width = "100%";
|
|
773
|
+
status.textContent = t("toast-migration-success");
|
|
774
|
+
showToast(t("toast-migration-success"), "success");
|
|
775
|
+
setTimeout(() => {
|
|
776
|
+
document.getElementById("tag-migration-overlay").classList.add("hidden");
|
|
777
|
+
loadMemories();
|
|
778
|
+
loadStats();
|
|
779
|
+
}, 2000);
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
function showMigrationWarning(data) {
|
|
783
|
+
const section = document.getElementById("migration-section");
|
|
784
|
+
const message = document.getElementById("migration-message");
|
|
785
|
+
|
|
786
|
+
const shardInfo =
|
|
787
|
+
data.shardMismatches.length > 0
|
|
788
|
+
? t("migration-shards-mismatch", { count: data.shardMismatches.length })
|
|
789
|
+
: t("migration-dimension-mismatch");
|
|
790
|
+
|
|
791
|
+
message.textContent = t("migration-mismatch-details", {
|
|
792
|
+
configDimensions: data.configDimensions,
|
|
793
|
+
configModel: data.configModel,
|
|
794
|
+
shardInfo,
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
lucide.createIcons();
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
function toggleMigrationButtons() {
|
|
801
|
+
const checkbox = document.getElementById("migration-confirm-checkbox");
|
|
802
|
+
const freshBtn = document.getElementById("migration-fresh-btn");
|
|
803
|
+
const reembedBtn = document.getElementById("migration-reembed-btn");
|
|
804
|
+
|
|
805
|
+
freshBtn.disabled = !checkbox.checked;
|
|
806
|
+
reembedBtn.disabled = !checkbox.checked;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
async function runMigration(strategy) {
|
|
810
|
+
const checkbox = document.getElementById("migration-confirm-checkbox");
|
|
811
|
+
|
|
812
|
+
if (!checkbox.checked) {
|
|
813
|
+
showToast(t("toast-migration-failed"), "error");
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
const strategyName =
|
|
818
|
+
strategy === "fresh-start" ? "Fresh Start (Delete All)" : "Re-embed (Preserve Data)";
|
|
819
|
+
|
|
820
|
+
if (
|
|
821
|
+
!confirm(
|
|
822
|
+
`Run ${strategyName} migration?\n\nThis operation is IRREVERSIBLE and will:\n${strategy === "fresh-start" ? "- DELETE all existing memories\n- Remove all shards" : "- Re-embed all memories with new model\n- This may take several minutes"}\n\nContinue?`
|
|
823
|
+
)
|
|
824
|
+
) {
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
showToast(t("status-migration-init"), "info");
|
|
829
|
+
const result = await fetchAPI("/api/migration/run", {
|
|
830
|
+
method: "POST",
|
|
831
|
+
headers: { "Content-Type": "application/json" },
|
|
832
|
+
body: JSON.stringify({ strategy }),
|
|
833
|
+
});
|
|
834
|
+
|
|
835
|
+
if (result.success) {
|
|
836
|
+
const data = result.data;
|
|
837
|
+
let message = `Migration complete! `;
|
|
838
|
+
|
|
839
|
+
if (strategy === "fresh-start") {
|
|
840
|
+
message += `Deleted ${data.deletedShards} shard(s). Duration: ${(data.duration / 1000).toFixed(2)}s`;
|
|
841
|
+
} else {
|
|
842
|
+
message += `Re-embedded ${data.reEmbeddedMemories} memories. Duration: ${(data.duration / 1000).toFixed(2)}s`;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
showToast(t("toast-migration-success"), "success");
|
|
846
|
+
document.getElementById("migration-section").classList.add("hidden");
|
|
847
|
+
document.getElementById("migration-confirm-checkbox").checked = false;
|
|
848
|
+
|
|
849
|
+
await loadMemories();
|
|
850
|
+
await loadStats();
|
|
851
|
+
} else {
|
|
852
|
+
showToast(result.error || t("toast-migration-failed"), "error");
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
async function loadUserProfile() {
|
|
857
|
+
const result = await fetchAPI("/api/user-profile");
|
|
858
|
+
if (result.success) {
|
|
859
|
+
state.userProfile = result.data;
|
|
860
|
+
renderUserProfile();
|
|
861
|
+
} else {
|
|
862
|
+
showError(result.error || t("toast-update-failed"));
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
function renderUserProfile() {
|
|
867
|
+
const container = document.getElementById("profile-content");
|
|
868
|
+
const profile = state.userProfile;
|
|
869
|
+
|
|
870
|
+
if (!profile.exists) {
|
|
871
|
+
container.innerHTML = `
|
|
872
|
+
<div class="empty-state">
|
|
873
|
+
<i data-lucide="user-x" class="icon-large"></i>
|
|
874
|
+
<p>${profile.message}</p>
|
|
875
|
+
</div>
|
|
876
|
+
`;
|
|
877
|
+
lucide.createIcons();
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
let data = profile.profileData;
|
|
882
|
+
if (typeof data === "string") {
|
|
883
|
+
try {
|
|
884
|
+
data = JSON.parse(data);
|
|
885
|
+
} catch (e) {
|
|
886
|
+
console.error("Failed to parse profileData string", e);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
const parseField = (field) => {
|
|
891
|
+
if (!field) return [];
|
|
892
|
+
let result = field;
|
|
893
|
+
let lastResult = null;
|
|
894
|
+
while (typeof result === "string" && result !== lastResult) {
|
|
895
|
+
lastResult = result;
|
|
896
|
+
try {
|
|
897
|
+
result = JSON.parse(typeof jsonrepair === "function" ? jsonrepair(result) : result);
|
|
898
|
+
} catch {
|
|
899
|
+
break;
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
if (!Array.isArray(result)) return [];
|
|
903
|
+
const flattened = [];
|
|
904
|
+
const walk = (item) => {
|
|
905
|
+
if (Array.isArray(item)) item.forEach(walk);
|
|
906
|
+
else if (item && typeof item === "object") flattened.push(item);
|
|
907
|
+
};
|
|
908
|
+
walk(result);
|
|
909
|
+
return flattened;
|
|
910
|
+
};
|
|
911
|
+
|
|
912
|
+
const preferences = parseField(data.preferences);
|
|
913
|
+
const patterns = parseField(data.patterns);
|
|
914
|
+
const workflows = parseField(data.workflows);
|
|
915
|
+
|
|
916
|
+
container.innerHTML = `
|
|
917
|
+
<div class="profile-header">
|
|
918
|
+
<div class="profile-info">
|
|
919
|
+
<h3>${profile.displayName || profile.userId}</h3>
|
|
920
|
+
<div class="profile-stats">
|
|
921
|
+
<div class="stat-pill">
|
|
922
|
+
<span class="label">${t("profile-version")}</span>
|
|
923
|
+
<span class="value">${profile.version}</span>
|
|
924
|
+
</div>
|
|
925
|
+
<div class="stat-pill">
|
|
926
|
+
<span class="label">${t("profile-prompts")}</span>
|
|
927
|
+
<span class="value">${profile.totalPromptsAnalyzed}</span>
|
|
928
|
+
</div>
|
|
929
|
+
<div class="stat-pill">
|
|
930
|
+
<span class="label">${t("profile-updated")}</span>
|
|
931
|
+
<span class="value">${formatDate(profile.lastAnalyzedAt)}</span>
|
|
932
|
+
</div>
|
|
933
|
+
</div>
|
|
934
|
+
</div>
|
|
935
|
+
<button id="view-changelog-btn" class="btn-secondary compact">
|
|
936
|
+
<i data-lucide="history" class="icon"></i> History
|
|
937
|
+
</button>
|
|
938
|
+
</div>
|
|
939
|
+
|
|
940
|
+
<div class="dashboard-grid">
|
|
941
|
+
<div class="dashboard-section preferences-section">
|
|
942
|
+
<h4><i data-lucide="heart" class="icon"></i> ${t("profile-preferences")} <span class="count">${preferences.length}</span></h4>
|
|
943
|
+
${
|
|
944
|
+
preferences.length === 0
|
|
945
|
+
? `<p class="empty-text">${t("empty-preferences")}</p>`
|
|
946
|
+
: `
|
|
947
|
+
<div class="cards-grid">
|
|
948
|
+
${preferences
|
|
949
|
+
.sort((a, b) => (b.confidence || 0) - (a.confidence || 0))
|
|
950
|
+
.map(
|
|
951
|
+
(p) => `
|
|
952
|
+
<div class="compact-card preference-card">
|
|
953
|
+
<div class="card-top">
|
|
954
|
+
<span class="category-tag">${escapeHtml(p.category || "General")}</span>
|
|
955
|
+
<div class="confidence-ring" style="--p:${Math.round((p.confidence || 0) * 100)}">
|
|
956
|
+
<span>${Math.round((p.confidence || 0) * 100)}%</span>
|
|
957
|
+
</div>
|
|
958
|
+
</div>
|
|
959
|
+
<div class="card-body">
|
|
960
|
+
<p class="card-text">${escapeHtml(p.description || "")}</p>
|
|
961
|
+
</div>
|
|
962
|
+
${
|
|
963
|
+
p.evidence && p.evidence.length > 0
|
|
964
|
+
? `
|
|
965
|
+
<div class="card-footer">
|
|
966
|
+
<span class="evidence-toggle" title="${escapeHtml(Array.isArray(p.evidence) ? p.evidence.join("\n") : p.evidence)}">
|
|
967
|
+
<i data-lucide="info" class="icon-xs"></i> ${Array.isArray(p.evidence) ? p.evidence.length : 1} evidence
|
|
968
|
+
</span>
|
|
969
|
+
</div>`
|
|
970
|
+
: ""
|
|
971
|
+
}
|
|
972
|
+
</div>
|
|
973
|
+
`
|
|
974
|
+
)
|
|
975
|
+
.join("")}
|
|
976
|
+
</div>
|
|
977
|
+
`
|
|
978
|
+
}
|
|
979
|
+
</div>
|
|
980
|
+
|
|
981
|
+
<div class="dashboard-section patterns-section">
|
|
982
|
+
<h4><i data-lucide="activity" class="icon"></i> ${t("profile-patterns")} <span class="count">${patterns.length}</span></h4>
|
|
983
|
+
${
|
|
984
|
+
patterns.length === 0
|
|
985
|
+
? `<p class="empty-text">${t("empty-patterns")}</p>`
|
|
986
|
+
: `
|
|
987
|
+
<div class="cards-grid">
|
|
988
|
+
${patterns
|
|
989
|
+
.map(
|
|
990
|
+
(p) => `
|
|
991
|
+
<div class="compact-card pattern-card">
|
|
992
|
+
<div class="card-top">
|
|
993
|
+
<span class="category-tag">${escapeHtml(p.category || "General")}</span>
|
|
994
|
+
</div>
|
|
995
|
+
<div class="card-body">
|
|
996
|
+
<p class="card-text">${escapeHtml(p.description || "")}</p>
|
|
997
|
+
</div>
|
|
998
|
+
</div>
|
|
999
|
+
`
|
|
1000
|
+
)
|
|
1001
|
+
.join("")}
|
|
1002
|
+
</div>
|
|
1003
|
+
`
|
|
1004
|
+
}
|
|
1005
|
+
</div>
|
|
1006
|
+
|
|
1007
|
+
<div class="dashboard-section workflows-section full-width">
|
|
1008
|
+
<h4><i data-lucide="workflow" class="icon"></i> ${t("profile-workflows")} <span class="count">${workflows.length}</span></h4>
|
|
1009
|
+
${
|
|
1010
|
+
workflows.length === 0
|
|
1011
|
+
? `<p class="empty-text">${t("empty-workflows")}</p>`
|
|
1012
|
+
: `
|
|
1013
|
+
<div class="workflows-grid">
|
|
1014
|
+
${workflows
|
|
1015
|
+
.map(
|
|
1016
|
+
(w) => `
|
|
1017
|
+
<div class="workflow-row">
|
|
1018
|
+
<div class="workflow-title">${escapeHtml(w.description || "")}</div>
|
|
1019
|
+
<div class="workflow-steps-horizontal">
|
|
1020
|
+
${(w.steps || [])
|
|
1021
|
+
.map(
|
|
1022
|
+
(step, i) => `
|
|
1023
|
+
<div class="step-node">
|
|
1024
|
+
<span class="step-idx">${i + 1}</span>
|
|
1025
|
+
<span class="step-content">${escapeHtml(step)}</span>
|
|
1026
|
+
</div>
|
|
1027
|
+
${i < (w.steps || []).length - 1 ? '<i data-lucide="arrow-right" class="step-arrow"></i>' : ""}
|
|
1028
|
+
`
|
|
1029
|
+
)
|
|
1030
|
+
.join("")}
|
|
1031
|
+
</div>
|
|
1032
|
+
</div>
|
|
1033
|
+
`
|
|
1034
|
+
)
|
|
1035
|
+
.join("")}
|
|
1036
|
+
</div>
|
|
1037
|
+
`
|
|
1038
|
+
}
|
|
1039
|
+
</div>
|
|
1040
|
+
</div>
|
|
1041
|
+
`;
|
|
1042
|
+
|
|
1043
|
+
document.getElementById("view-changelog-btn")?.addEventListener("click", showChangelog);
|
|
1044
|
+
lucide.createIcons();
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
async function showChangelog() {
|
|
1048
|
+
const modal = document.getElementById("changelog-modal");
|
|
1049
|
+
const list = document.getElementById("changelog-list");
|
|
1050
|
+
|
|
1051
|
+
modal.classList.remove("hidden");
|
|
1052
|
+
list.innerHTML = `<div class="loading">${t("loading-changelog")}</div>`;
|
|
1053
|
+
const result = await fetchAPI(
|
|
1054
|
+
`/api/user-profile/changelog?profileId=${state.userProfile.id}&limit=10`
|
|
1055
|
+
);
|
|
1056
|
+
|
|
1057
|
+
if (result.success && result.data.length > 0) {
|
|
1058
|
+
list.innerHTML = result.data
|
|
1059
|
+
.map(
|
|
1060
|
+
(c) => `
|
|
1061
|
+
<div class="changelog-item">
|
|
1062
|
+
<div class="changelog-header">
|
|
1063
|
+
<span class="changelog-version">v${c.version}</span>
|
|
1064
|
+
<span class="changelog-type">${c.changeType}</span>
|
|
1065
|
+
<span class="changelog-date">${formatDate(c.createdAt)}</span>
|
|
1066
|
+
</div>
|
|
1067
|
+
<p class="changelog-summary">${escapeHtml(c.changeSummary)}</p>
|
|
1068
|
+
</div>
|
|
1069
|
+
`
|
|
1070
|
+
)
|
|
1071
|
+
.join("");
|
|
1072
|
+
} else {
|
|
1073
|
+
list.innerHTML = `<div class="empty-state">${t("empty-changelog")}</div>`;
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
async function refreshProfile() {
|
|
1078
|
+
showToast(t("loading-profile"), "info");
|
|
1079
|
+
const result = await fetchAPI("/api/user-profile/refresh", {
|
|
1080
|
+
method: "POST",
|
|
1081
|
+
headers: { "Content-Type": "application/json" },
|
|
1082
|
+
body: JSON.stringify({}),
|
|
1083
|
+
});
|
|
1084
|
+
|
|
1085
|
+
if (result.success) {
|
|
1086
|
+
showToast(result.data.message, "success");
|
|
1087
|
+
await loadUserProfile();
|
|
1088
|
+
} else {
|
|
1089
|
+
showToast(result.error || t("toast-update-failed"), "error");
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
function switchView(view) {
|
|
1094
|
+
state.currentView = view;
|
|
1095
|
+
|
|
1096
|
+
document.querySelectorAll(".tab-btn").forEach((btn) => btn.classList.remove("active"));
|
|
1097
|
+
|
|
1098
|
+
if (view === "project") {
|
|
1099
|
+
document.getElementById("tab-project").classList.add("active");
|
|
1100
|
+
document.getElementById("project-section").classList.remove("hidden");
|
|
1101
|
+
document.getElementById("profile-section").classList.add("hidden");
|
|
1102
|
+
document.querySelector(".controls").classList.remove("hidden");
|
|
1103
|
+
document.querySelector(".add-section").classList.remove("hidden");
|
|
1104
|
+
} else if (view === "profile") {
|
|
1105
|
+
document.getElementById("tab-profile").classList.add("active");
|
|
1106
|
+
document.getElementById("project-section").classList.add("hidden");
|
|
1107
|
+
document.getElementById("profile-section").classList.remove("hidden");
|
|
1108
|
+
document.querySelector(".controls").classList.add("hidden");
|
|
1109
|
+
document.querySelector(".add-section").classList.add("hidden");
|
|
1110
|
+
loadUserProfile();
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
function escapeHtml(text) {
|
|
1115
|
+
const div = document.createElement("div");
|
|
1116
|
+
div.textContent = text;
|
|
1117
|
+
return div.innerHTML;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
document.addEventListener("DOMContentLoaded", async () => {
|
|
1121
|
+
document.getElementById("tab-project").addEventListener("click", () => switchView("project"));
|
|
1122
|
+
document.getElementById("tab-profile").addEventListener("click", () => switchView("profile"));
|
|
1123
|
+
document.getElementById("refresh-profile-btn")?.addEventListener("click", refreshProfile);
|
|
1124
|
+
document.getElementById("changelog-close")?.addEventListener("click", () => {
|
|
1125
|
+
document.getElementById("changelog-modal").classList.add("hidden");
|
|
1126
|
+
});
|
|
1127
|
+
|
|
1128
|
+
document.getElementById("lang-toggle").addEventListener("click", () => {
|
|
1129
|
+
const newLang = getLanguage() === "en" ? "zh" : "en";
|
|
1130
|
+
setLanguage(newLang);
|
|
1131
|
+
document.getElementById("lang-toggle").textContent = newLang.toUpperCase();
|
|
1132
|
+
// Re-render dynamic content
|
|
1133
|
+
loadMemories();
|
|
1134
|
+
loadStats();
|
|
1135
|
+
if (state.currentView === "profile") loadUserProfile();
|
|
1136
|
+
});
|
|
1137
|
+
|
|
1138
|
+
document.getElementById("lang-toggle").textContent = getLanguage().toUpperCase();
|
|
1139
|
+
|
|
1140
|
+
document.getElementById("tag-filter").addEventListener("change", () => {
|
|
1141
|
+
state.selectedTag = document.getElementById("tag-filter").value;
|
|
1142
|
+
state.currentPage = 1;
|
|
1143
|
+
state.isSearching = false;
|
|
1144
|
+
state.searchQuery = "";
|
|
1145
|
+
document.getElementById("search-input").value = "";
|
|
1146
|
+
document.getElementById("clear-search-btn").classList.add("hidden");
|
|
1147
|
+
loadMemories();
|
|
1148
|
+
});
|
|
1149
|
+
|
|
1150
|
+
document.getElementById("search-btn").addEventListener("click", performSearch);
|
|
1151
|
+
document.getElementById("clear-search-btn").addEventListener("click", clearSearch);
|
|
1152
|
+
document.getElementById("search-input").addEventListener("keypress", (e) => {
|
|
1153
|
+
if (e.key === "Enter") performSearch();
|
|
1154
|
+
});
|
|
1155
|
+
|
|
1156
|
+
document.getElementById("add-form").addEventListener("submit", addMemory);
|
|
1157
|
+
document.getElementById("edit-form").addEventListener("submit", saveEdit);
|
|
1158
|
+
document.getElementById("modal-close").addEventListener("click", closeModal);
|
|
1159
|
+
document.getElementById("cancel-edit").addEventListener("click", closeModal);
|
|
1160
|
+
|
|
1161
|
+
document.getElementById("prev-page-top").addEventListener("click", () => changePage(-1));
|
|
1162
|
+
document.getElementById("next-page-top").addEventListener("click", () => changePage(1));
|
|
1163
|
+
document.getElementById("prev-page-bottom").addEventListener("click", () => changePage(-1));
|
|
1164
|
+
document.getElementById("next-page-bottom").addEventListener("click", () => changePage(1));
|
|
1165
|
+
|
|
1166
|
+
document.getElementById("bulk-delete-btn").addEventListener("click", bulkDelete);
|
|
1167
|
+
document.getElementById("deselect-all-btn").addEventListener("click", deselectAll);
|
|
1168
|
+
|
|
1169
|
+
document.getElementById("cleanup-btn").addEventListener("click", runCleanup);
|
|
1170
|
+
document.getElementById("deduplicate-btn").addEventListener("click", runDeduplication);
|
|
1171
|
+
|
|
1172
|
+
document
|
|
1173
|
+
.getElementById("migration-confirm-checkbox")
|
|
1174
|
+
.addEventListener("change", toggleMigrationButtons);
|
|
1175
|
+
document
|
|
1176
|
+
.getElementById("migration-fresh-btn")
|
|
1177
|
+
.addEventListener("click", () => runMigration("fresh-start"));
|
|
1178
|
+
document
|
|
1179
|
+
.getElementById("migration-reembed-btn")
|
|
1180
|
+
.addEventListener("click", () => runMigration("re-embed"));
|
|
1181
|
+
|
|
1182
|
+
document.getElementById("edit-modal").addEventListener("click", (e) => {
|
|
1183
|
+
if (e.target.id === "edit-modal") closeModal();
|
|
1184
|
+
});
|
|
1185
|
+
|
|
1186
|
+
await loadTags();
|
|
1187
|
+
await loadMemories();
|
|
1188
|
+
await loadStats();
|
|
1189
|
+
await checkMigrationStatus();
|
|
1190
|
+
|
|
1191
|
+
startAutoRefresh();
|
|
1192
|
+
|
|
1193
|
+
lucide.createIcons();
|
|
1194
|
+
});
|