opencode-mem 2.0.1 → 2.2.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 +54 -11
- package/dist/config.d.ts +5 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +62 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +18 -11
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +0 -4
- package/dist/services/ai/providers/anthropic-messages.d.ts.map +1 -1
- package/dist/services/ai/providers/anthropic-messages.js +11 -17
- package/dist/services/ai/providers/openai-chat-completion.d.ts.map +1 -1
- package/dist/services/ai/providers/openai-chat-completion.js +11 -17
- package/dist/services/ai/providers/openai-responses.d.ts.map +1 -1
- package/dist/services/ai/providers/openai-responses.js +11 -17
- package/dist/services/api-handlers.d.ts +4 -0
- package/dist/services/api-handlers.d.ts.map +1 -1
- package/dist/services/api-handlers.js +155 -1
- package/dist/services/auto-capture.js +2 -2
- package/dist/services/client.d.ts +1 -16
- package/dist/services/client.d.ts.map +1 -1
- package/dist/services/client.js +0 -35
- package/dist/services/context.d.ts +1 -7
- package/dist/services/context.d.ts.map +1 -1
- package/dist/services/context.js +6 -14
- package/dist/services/deduplication-service.d.ts.map +1 -1
- package/dist/services/deduplication-service.js +1 -3
- package/dist/services/logger.js +1 -1
- package/dist/services/user-memory-learning.d.ts.map +1 -1
- package/dist/services/user-memory-learning.js +122 -84
- 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/types.d.ts +51 -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 +22 -0
- package/dist/services/user-profile/user-profile-manager.d.ts.map +1 -0
- package/dist/services/user-profile/user-profile-manager.js +267 -0
- package/dist/services/web-server-worker.js +29 -1
- package/dist/types/index.d.ts +1 -3
- package/dist/types/index.d.ts.map +1 -1
- package/dist/web/app.js +249 -29
- package/dist/web/index.html +31 -5
- package/dist/web/styles.css +309 -0
- package/package.json +1 -1
package/dist/web/app.js
CHANGED
|
@@ -9,10 +9,12 @@ const state = {
|
|
|
9
9
|
totalItems: 0,
|
|
10
10
|
selectedTag: "",
|
|
11
11
|
currentScope: "project",
|
|
12
|
+
currentView: "project",
|
|
12
13
|
searchQuery: "",
|
|
13
14
|
isSearching: false,
|
|
14
15
|
selectedMemories: new Set(),
|
|
15
16
|
autoRefreshInterval: null,
|
|
17
|
+
userProfile: null,
|
|
16
18
|
};
|
|
17
19
|
|
|
18
20
|
marked.setOptions({
|
|
@@ -143,7 +145,7 @@ function renderPromptCard(prompt) {
|
|
|
143
145
|
<input type="checkbox" class="memory-checkbox" data-id="${prompt.id}" ${isSelected ? "checked" : ""} />
|
|
144
146
|
<i data-lucide="message-circle" class="icon"></i>
|
|
145
147
|
<span class="badge badge-prompt">USER PROMPT</span>
|
|
146
|
-
${isLinked ? '<span class="badge badge-linked"
|
|
148
|
+
${isLinked ? '<span class="badge badge-linked"><i data-lucide="link" class="icon-sm"></i> LINKED</span>' : ""}
|
|
147
149
|
<span class="prompt-date">${promptDate}</span>
|
|
148
150
|
</div>
|
|
149
151
|
<div class="prompt-actions">
|
|
@@ -156,7 +158,7 @@ function renderPromptCard(prompt) {
|
|
|
156
158
|
<div class="prompt-content">
|
|
157
159
|
${escapeHtml(prompt.content)}
|
|
158
160
|
</div>
|
|
159
|
-
${isLinked ? '<div class="link-indicator"
|
|
161
|
+
${isLinked ? '<div class="link-indicator"><i data-lucide="arrow-down" class="icon-sm"></i> Generated memory above <i data-lucide="arrow-up" class="icon-sm"></i></div>' : ""}
|
|
160
162
|
</div>
|
|
161
163
|
`;
|
|
162
164
|
}
|
|
@@ -202,7 +204,7 @@ function renderMemoryCard(memory) {
|
|
|
202
204
|
<input type="checkbox" class="memory-checkbox" data-id="${memory.id}" ${isSelected ? "checked" : ""} />
|
|
203
205
|
<span class="badge badge-${memory.scope}">${memory.scope}</span>
|
|
204
206
|
${memory.memoryType ? `<span class="badge badge-type">${memory.memoryType}</span>` : ""}
|
|
205
|
-
${isLinked ? '<span class="badge badge-linked"
|
|
207
|
+
${isLinked ? '<span class="badge badge-linked"><i data-lucide="link" class="icon-sm"></i> LINKED</span>' : ""}
|
|
206
208
|
${similarityHtml}
|
|
207
209
|
${isPinned ? '<span class="badge badge-pinned">PINNED</span>' : ""}
|
|
208
210
|
<span class="memory-display-name">${escapeHtml(displayInfo)}</span>
|
|
@@ -218,7 +220,7 @@ function renderMemoryCard(memory) {
|
|
|
218
220
|
</div>
|
|
219
221
|
</div>
|
|
220
222
|
<div class="memory-content markdown-content">${renderMarkdown(memory.content)}</div>
|
|
221
|
-
${isLinked ? '<div class="link-indicator"
|
|
223
|
+
${isLinked ? '<div class="link-indicator"><i data-lucide="arrow-up" class="icon-sm"></i> From prompt below <i data-lucide="arrow-down" class="icon-sm"></i></div>' : ""}
|
|
222
224
|
<div class="memory-footer">
|
|
223
225
|
${dateInfo}
|
|
224
226
|
<span>ID: ${memory.id}</span>
|
|
@@ -295,20 +297,6 @@ async function loadStats() {
|
|
|
295
297
|
}
|
|
296
298
|
}
|
|
297
299
|
|
|
298
|
-
function switchScope(scope) {
|
|
299
|
-
state.currentScope = scope;
|
|
300
|
-
state.currentPage = 1;
|
|
301
|
-
state.selectedTag = "";
|
|
302
|
-
|
|
303
|
-
document.querySelectorAll(".tab-btn").forEach((btn) => {
|
|
304
|
-
btn.classList.remove("active");
|
|
305
|
-
});
|
|
306
|
-
document.getElementById(`tab-${scope}`).classList.add("active");
|
|
307
|
-
|
|
308
|
-
populateTagDropdowns();
|
|
309
|
-
loadMemories();
|
|
310
|
-
}
|
|
311
|
-
|
|
312
300
|
async function addMemory(e) {
|
|
313
301
|
e.preventDefault();
|
|
314
302
|
|
|
@@ -512,16 +500,16 @@ function handleAddScopeChange() {
|
|
|
512
500
|
|
|
513
501
|
tagDropdown.innerHTML = '<option value="">Select tag</option>';
|
|
514
502
|
|
|
515
|
-
if (!scope) return;
|
|
503
|
+
if (!scope || scope !== "project") return;
|
|
516
504
|
|
|
517
|
-
const tags =
|
|
505
|
+
const tags = state.tags.project;
|
|
518
506
|
tags.forEach((tagInfo) => {
|
|
519
507
|
const displayText = tagInfo.displayName || tagInfo.tag;
|
|
520
508
|
const shortDisplay =
|
|
521
509
|
displayText.length > 50 ? displayText.substring(0, 50) + "..." : displayText;
|
|
522
510
|
const option = document.createElement("option");
|
|
523
511
|
option.value = tagInfo.tag;
|
|
524
|
-
option.textContent =
|
|
512
|
+
option.textContent = shortDisplay;
|
|
525
513
|
tagDropdown.appendChild(option);
|
|
526
514
|
});
|
|
527
515
|
}
|
|
@@ -551,12 +539,6 @@ function showRefreshIndicator(show) {
|
|
|
551
539
|
}
|
|
552
540
|
}
|
|
553
541
|
|
|
554
|
-
function escapeHtml(text) {
|
|
555
|
-
const div = document.createElement("div");
|
|
556
|
-
div.textContent = text;
|
|
557
|
-
return div.innerHTML;
|
|
558
|
-
}
|
|
559
|
-
|
|
560
542
|
function formatDate(isoString) {
|
|
561
543
|
const date = new Date(isoString);
|
|
562
544
|
return date.toLocaleString("en-US", {
|
|
@@ -722,9 +704,247 @@ async function runMigration(strategy) {
|
|
|
722
704
|
}
|
|
723
705
|
}
|
|
724
706
|
|
|
707
|
+
async function loadUserProfile() {
|
|
708
|
+
const result = await fetchAPI("/api/user-profile");
|
|
709
|
+
if (result.success) {
|
|
710
|
+
state.userProfile = result.data;
|
|
711
|
+
renderUserProfile();
|
|
712
|
+
} else {
|
|
713
|
+
showError(result.error || "Failed to load profile");
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
function renderUserProfile() {
|
|
718
|
+
const container = document.getElementById("profile-content");
|
|
719
|
+
const profile = state.userProfile;
|
|
720
|
+
|
|
721
|
+
if (!profile.exists) {
|
|
722
|
+
container.innerHTML = `
|
|
723
|
+
<div class="empty-state">
|
|
724
|
+
<i data-lucide="user-x" class="icon-large"></i>
|
|
725
|
+
<p>${profile.message}</p>
|
|
726
|
+
</div>
|
|
727
|
+
`;
|
|
728
|
+
lucide.createIcons();
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
const data = profile.profileData;
|
|
733
|
+
const preferences = data.preferences || [];
|
|
734
|
+
const patterns = data.patterns || [];
|
|
735
|
+
const workflows = data.workflows || [];
|
|
736
|
+
const skillLevel = data.skillLevel || {};
|
|
737
|
+
|
|
738
|
+
container.innerHTML = `
|
|
739
|
+
<div class="profile-header">
|
|
740
|
+
<div class="profile-info">
|
|
741
|
+
<h3>${profile.displayName || profile.userId}</h3>
|
|
742
|
+
<p class="profile-meta">
|
|
743
|
+
<span>Version: ${profile.version}</span>
|
|
744
|
+
<span class="separator">|</span>
|
|
745
|
+
<span>Analyzed: ${profile.totalPromptsAnalyzed} prompts</span>
|
|
746
|
+
<span class="separator">|</span>
|
|
747
|
+
<span>Updated: ${formatDate(profile.lastAnalyzedAt)}</span>
|
|
748
|
+
</p>
|
|
749
|
+
</div>
|
|
750
|
+
<button id="view-changelog-btn" class="btn-secondary">
|
|
751
|
+
<i data-lucide="history" class="icon"></i> Version History
|
|
752
|
+
</button>
|
|
753
|
+
</div>
|
|
754
|
+
|
|
755
|
+
<div class="profile-section">
|
|
756
|
+
<h4><i data-lucide="heart" class="icon"></i> Preferences (${preferences.length})</h4>
|
|
757
|
+
${
|
|
758
|
+
preferences.length === 0
|
|
759
|
+
? '<p class="empty-text">No preferences learned yet</p>'
|
|
760
|
+
: `
|
|
761
|
+
<div class="preferences-list">
|
|
762
|
+
${preferences
|
|
763
|
+
.map(
|
|
764
|
+
(p) => `
|
|
765
|
+
<div class="preference-item">
|
|
766
|
+
<div class="preference-header">
|
|
767
|
+
<span class="preference-name">${escapeHtml(p.description)}</span>
|
|
768
|
+
<span class="confidence-badge">${Math.round(p.confidence * 100)}%</span>
|
|
769
|
+
</div>
|
|
770
|
+
<div class="confidence-bar">
|
|
771
|
+
<div class="confidence-fill" style="width: ${p.confidence * 100}%"></div>
|
|
772
|
+
</div>
|
|
773
|
+
<p class="preference-evidence">${escapeHtml(Array.isArray(p.evidence) ? p.evidence.join(", ") : p.evidence)}</p>
|
|
774
|
+
<p class="preference-meta">Category: ${escapeHtml(p.category)}</p>
|
|
775
|
+
</div>
|
|
776
|
+
`
|
|
777
|
+
)
|
|
778
|
+
.join("")}
|
|
779
|
+
</div>
|
|
780
|
+
`
|
|
781
|
+
}
|
|
782
|
+
</div>
|
|
783
|
+
|
|
784
|
+
<div class="profile-section">
|
|
785
|
+
<h4><i data-lucide="activity" class="icon"></i> Patterns (${patterns.length})</h4>
|
|
786
|
+
${
|
|
787
|
+
patterns.length === 0
|
|
788
|
+
? '<p class="empty-text">No patterns detected yet</p>'
|
|
789
|
+
: `
|
|
790
|
+
<div class="patterns-list">
|
|
791
|
+
${patterns
|
|
792
|
+
.map(
|
|
793
|
+
(p) => `
|
|
794
|
+
<div class="pattern-item">
|
|
795
|
+
<div class="pattern-header">
|
|
796
|
+
<span class="pattern-name">${escapeHtml(p.description)}</span>
|
|
797
|
+
<span class="category-badge">${escapeHtml(p.category)}</span>
|
|
798
|
+
</div>
|
|
799
|
+
</div>
|
|
800
|
+
`
|
|
801
|
+
)
|
|
802
|
+
.join("")}
|
|
803
|
+
</div>
|
|
804
|
+
`
|
|
805
|
+
}
|
|
806
|
+
</div>
|
|
807
|
+
|
|
808
|
+
<div class="profile-section">
|
|
809
|
+
<h4><i data-lucide="workflow" class="icon"></i> Workflows (${workflows.length})</h4>
|
|
810
|
+
${
|
|
811
|
+
workflows.length === 0
|
|
812
|
+
? '<p class="empty-text">No workflows identified yet</p>'
|
|
813
|
+
: `
|
|
814
|
+
<div class="workflows-list">
|
|
815
|
+
${workflows
|
|
816
|
+
.map(
|
|
817
|
+
(w) => `
|
|
818
|
+
<div class="workflow-item">
|
|
819
|
+
<div class="workflow-header">
|
|
820
|
+
<span class="workflow-name">${escapeHtml(w.description)}</span>
|
|
821
|
+
</div>
|
|
822
|
+
<div class="workflow-steps">
|
|
823
|
+
${w.steps
|
|
824
|
+
.map(
|
|
825
|
+
(step) => `
|
|
826
|
+
<div class="workflow-step">
|
|
827
|
+
<span class="step-text">${escapeHtml(step)}</span>
|
|
828
|
+
</div>
|
|
829
|
+
`
|
|
830
|
+
)
|
|
831
|
+
.join("")}
|
|
832
|
+
</div>
|
|
833
|
+
</div>
|
|
834
|
+
`
|
|
835
|
+
)
|
|
836
|
+
.join("")}
|
|
837
|
+
</div>
|
|
838
|
+
`
|
|
839
|
+
}
|
|
840
|
+
</div>
|
|
841
|
+
|
|
842
|
+
<div class="profile-section">
|
|
843
|
+
<h4><i data-lucide="award" class="icon"></i> Skill Level</h4>
|
|
844
|
+
<div class="skill-level">
|
|
845
|
+
<div class="skill-item">
|
|
846
|
+
<span class="skill-label">Overall</span>
|
|
847
|
+
<span class="skill-value">${escapeHtml(skillLevel.overall || "unknown")}</span>
|
|
848
|
+
</div>
|
|
849
|
+
${Object.entries(skillLevel.domains || {})
|
|
850
|
+
.map(
|
|
851
|
+
([domain, level]) => `
|
|
852
|
+
<div class="skill-item">
|
|
853
|
+
<span class="skill-label">${escapeHtml(domain)}</span>
|
|
854
|
+
<span class="skill-value">${escapeHtml(level)}</span>
|
|
855
|
+
</div>
|
|
856
|
+
`
|
|
857
|
+
)
|
|
858
|
+
.join("")}
|
|
859
|
+
</div>
|
|
860
|
+
</div>
|
|
861
|
+
`;
|
|
862
|
+
|
|
863
|
+
document.getElementById("view-changelog-btn")?.addEventListener("click", showChangelog);
|
|
864
|
+
lucide.createIcons();
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
async function showChangelog() {
|
|
868
|
+
const modal = document.getElementById("changelog-modal");
|
|
869
|
+
const list = document.getElementById("changelog-list");
|
|
870
|
+
|
|
871
|
+
modal.classList.remove("hidden");
|
|
872
|
+
list.innerHTML = '<div class="loading">Loading changelog...</div>';
|
|
873
|
+
|
|
874
|
+
const result = await fetchAPI(
|
|
875
|
+
`/api/user-profile/changelog?profileId=${state.userProfile.id}&limit=10`
|
|
876
|
+
);
|
|
877
|
+
|
|
878
|
+
if (result.success && result.data.length > 0) {
|
|
879
|
+
list.innerHTML = result.data
|
|
880
|
+
.map(
|
|
881
|
+
(c) => `
|
|
882
|
+
<div class="changelog-item">
|
|
883
|
+
<div class="changelog-header">
|
|
884
|
+
<span class="changelog-version">v${c.version}</span>
|
|
885
|
+
<span class="changelog-type">${c.changeType}</span>
|
|
886
|
+
<span class="changelog-date">${formatDate(c.createdAt)}</span>
|
|
887
|
+
</div>
|
|
888
|
+
<p class="changelog-summary">${escapeHtml(c.changeSummary)}</p>
|
|
889
|
+
</div>
|
|
890
|
+
`
|
|
891
|
+
)
|
|
892
|
+
.join("");
|
|
893
|
+
} else {
|
|
894
|
+
list.innerHTML = '<div class="empty-state">No changelog available</div>';
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
async function refreshProfile() {
|
|
899
|
+
showToast("Refreshing profile...", "info");
|
|
900
|
+
const result = await fetchAPI("/api/user-profile/refresh", {
|
|
901
|
+
method: "POST",
|
|
902
|
+
headers: { "Content-Type": "application/json" },
|
|
903
|
+
body: JSON.stringify({}),
|
|
904
|
+
});
|
|
905
|
+
|
|
906
|
+
if (result.success) {
|
|
907
|
+
showToast(result.data.message, "success");
|
|
908
|
+
await loadUserProfile();
|
|
909
|
+
} else {
|
|
910
|
+
showToast(result.error || "Failed to refresh profile", "error");
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
function switchView(view) {
|
|
915
|
+
state.currentView = view;
|
|
916
|
+
|
|
917
|
+
document.querySelectorAll(".tab-btn").forEach((btn) => btn.classList.remove("active"));
|
|
918
|
+
|
|
919
|
+
if (view === "project") {
|
|
920
|
+
document.getElementById("tab-project").classList.add("active");
|
|
921
|
+
document.getElementById("project-section").classList.remove("hidden");
|
|
922
|
+
document.getElementById("profile-section").classList.add("hidden");
|
|
923
|
+
document.querySelector(".controls").classList.remove("hidden");
|
|
924
|
+
document.querySelector(".add-section").classList.remove("hidden");
|
|
925
|
+
} else if (view === "profile") {
|
|
926
|
+
document.getElementById("tab-profile").classList.add("active");
|
|
927
|
+
document.getElementById("project-section").classList.add("hidden");
|
|
928
|
+
document.getElementById("profile-section").classList.remove("hidden");
|
|
929
|
+
document.querySelector(".controls").classList.add("hidden");
|
|
930
|
+
document.querySelector(".add-section").classList.add("hidden");
|
|
931
|
+
loadUserProfile();
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
function escapeHtml(text) {
|
|
936
|
+
const div = document.createElement("div");
|
|
937
|
+
div.textContent = text;
|
|
938
|
+
return div.innerHTML;
|
|
939
|
+
}
|
|
940
|
+
|
|
725
941
|
document.addEventListener("DOMContentLoaded", async () => {
|
|
726
|
-
document.getElementById("tab-project").addEventListener("click", () =>
|
|
727
|
-
document.getElementById("tab-
|
|
942
|
+
document.getElementById("tab-project").addEventListener("click", () => switchView("project"));
|
|
943
|
+
document.getElementById("tab-profile").addEventListener("click", () => switchView("profile"));
|
|
944
|
+
document.getElementById("refresh-profile-btn")?.addEventListener("click", refreshProfile);
|
|
945
|
+
document.getElementById("changelog-close")?.addEventListener("click", () => {
|
|
946
|
+
document.getElementById("changelog-modal").classList.add("hidden");
|
|
947
|
+
});
|
|
728
948
|
|
|
729
949
|
document.getElementById("tag-filter").addEventListener("change", () => {
|
|
730
950
|
state.selectedTag = document.getElementById("tag-filter").value;
|
package/dist/web/index.html
CHANGED
|
@@ -38,11 +38,11 @@
|
|
|
38
38
|
<div class="scope-tabs">
|
|
39
39
|
<button id="tab-project" class="tab-btn active">
|
|
40
40
|
<i data-lucide="folder" class="icon"></i>
|
|
41
|
-
PROJECT
|
|
41
|
+
PROJECT MEMORIES
|
|
42
42
|
</button>
|
|
43
|
-
<button id="tab-
|
|
43
|
+
<button id="tab-profile" class="tab-btn">
|
|
44
44
|
<i data-lucide="user" class="icon"></i>
|
|
45
|
-
USER
|
|
45
|
+
USER PROFILE
|
|
46
46
|
</button>
|
|
47
47
|
</div>
|
|
48
48
|
|
|
@@ -99,7 +99,7 @@
|
|
|
99
99
|
</div>
|
|
100
100
|
</div>
|
|
101
101
|
|
|
102
|
-
<div class="memories-section">
|
|
102
|
+
<div class="memories-section" id="project-section">
|
|
103
103
|
<div class="section-header">
|
|
104
104
|
<h2 id="section-title">└─ PROJECT MEMORIES (0) ──</h2>
|
|
105
105
|
<div class="pagination" id="pagination-top">
|
|
@@ -128,6 +128,19 @@
|
|
|
128
128
|
</div>
|
|
129
129
|
</div>
|
|
130
130
|
|
|
131
|
+
<div class="profile-section hidden" id="profile-section">
|
|
132
|
+
<div class="section-header">
|
|
133
|
+
<h2>└─ USER PROFILE ──</h2>
|
|
134
|
+
<button id="refresh-profile-btn" class="btn-secondary">
|
|
135
|
+
<i data-lucide="refresh-cw" class="icon"></i> Refresh
|
|
136
|
+
</button>
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
<div id="profile-content" class="profile-content">
|
|
140
|
+
<div class="loading">Loading profile...</div>
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
|
|
131
144
|
<div class="add-section">
|
|
132
145
|
<h2>└─ ADD NEW MEMORY ──</h2>
|
|
133
146
|
<form id="add-form">
|
|
@@ -136,7 +149,6 @@
|
|
|
136
149
|
<label>Scope:</label>
|
|
137
150
|
<select id="add-scope" required>
|
|
138
151
|
<option value="">Select scope</option>
|
|
139
|
-
<option value="user">User</option>
|
|
140
152
|
<option value="project">Project</option>
|
|
141
153
|
</select>
|
|
142
154
|
</div>
|
|
@@ -201,6 +213,20 @@
|
|
|
201
213
|
|
|
202
214
|
<div id="toast" class="toast hidden"></div>
|
|
203
215
|
|
|
216
|
+
<div id="changelog-modal" class="modal hidden">
|
|
217
|
+
<div class="modal-content">
|
|
218
|
+
<div class="modal-header">
|
|
219
|
+
<h3>Profile Version History</h3>
|
|
220
|
+
<button class="modal-close" id="changelog-close">
|
|
221
|
+
<i data-lucide="x" class="icon"></i>
|
|
222
|
+
</button>
|
|
223
|
+
</div>
|
|
224
|
+
<div id="changelog-list" class="changelog-list">
|
|
225
|
+
<div class="loading">Loading changelog...</div>
|
|
226
|
+
</div>
|
|
227
|
+
</div>
|
|
228
|
+
</div>
|
|
229
|
+
|
|
204
230
|
<script src="/app.js"></script>
|
|
205
231
|
</body>
|
|
206
232
|
</html>
|