memorix 0.5.2 → 0.6.1
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 +18 -1
- package/dist/cli/index.js +483 -72
- package/dist/cli/index.js.map +1 -1
- package/dist/dashboard/static/app.js +143 -11
- package/dist/dashboard/static/index.html +5 -0
- package/dist/dashboard/static/style.css +197 -0
- package/dist/index.js +450 -70
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
|
@@ -42,6 +42,15 @@ const i18n = {
|
|
|
42
42
|
noObsTitle: 'No Observations',
|
|
43
43
|
noObsDesc: 'Use memorix_store to create observations',
|
|
44
44
|
untitled: 'Untitled',
|
|
45
|
+
exportData: 'Export',
|
|
46
|
+
deleteObs: 'Delete',
|
|
47
|
+
deleteConfirm: 'Delete observation #%id%?',
|
|
48
|
+
deleted: 'Deleted',
|
|
49
|
+
narrative: 'Narrative',
|
|
50
|
+
facts: 'Facts',
|
|
51
|
+
concepts: 'Concepts',
|
|
52
|
+
files: 'Files Modified',
|
|
53
|
+
clickToExpand: 'Click to expand',
|
|
45
54
|
|
|
46
55
|
// Retention
|
|
47
56
|
memoryRetention: 'Memory Retention',
|
|
@@ -102,6 +111,15 @@ const i18n = {
|
|
|
102
111
|
noObsTitle: '暂无观察记录',
|
|
103
112
|
noObsDesc: '使用 memorix_store 创建观察记录',
|
|
104
113
|
untitled: '无标题',
|
|
114
|
+
exportData: '导出',
|
|
115
|
+
deleteObs: '删除',
|
|
116
|
+
deleteConfirm: '确认删除观察 #%id%?',
|
|
117
|
+
deleted: '已删除',
|
|
118
|
+
narrative: '叙述',
|
|
119
|
+
facts: '事实',
|
|
120
|
+
concepts: '概念',
|
|
121
|
+
files: '相关文件',
|
|
122
|
+
clickToExpand: '点击展开',
|
|
105
123
|
|
|
106
124
|
// Retention
|
|
107
125
|
memoryRetention: '记忆衰减',
|
|
@@ -241,9 +259,15 @@ document.querySelectorAll('.nav-btn').forEach(btn => {
|
|
|
241
259
|
// API Client
|
|
242
260
|
// ============================================================
|
|
243
261
|
|
|
262
|
+
let selectedProject = ''; // empty = current project (default)
|
|
263
|
+
|
|
244
264
|
async function api(endpoint) {
|
|
245
265
|
try {
|
|
246
|
-
const
|
|
266
|
+
const sep = endpoint.includes('?') ? '&' : '?';
|
|
267
|
+
const url = selectedProject
|
|
268
|
+
? `/api/${endpoint}${sep}project=${encodeURIComponent(selectedProject)}`
|
|
269
|
+
: `/api/${endpoint}`;
|
|
270
|
+
const res = await fetch(url);
|
|
247
271
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
248
272
|
return await res.json();
|
|
249
273
|
} catch (err) {
|
|
@@ -252,6 +276,49 @@ async function api(endpoint) {
|
|
|
252
276
|
}
|
|
253
277
|
}
|
|
254
278
|
|
|
279
|
+
// ============================================================
|
|
280
|
+
// Project Switcher
|
|
281
|
+
// ============================================================
|
|
282
|
+
|
|
283
|
+
async function initProjectSwitcher() {
|
|
284
|
+
const select = document.getElementById('project-select');
|
|
285
|
+
if (!select) return;
|
|
286
|
+
|
|
287
|
+
// Fetch project list
|
|
288
|
+
try {
|
|
289
|
+
const res = await fetch('/api/projects');
|
|
290
|
+
const projects = await res.json();
|
|
291
|
+
if (!Array.isArray(projects) || projects.length === 0) {
|
|
292
|
+
select.innerHTML = '<option value="">No projects</option>';
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
select.innerHTML = '';
|
|
297
|
+
for (const p of projects) {
|
|
298
|
+
const opt = document.createElement('option');
|
|
299
|
+
opt.value = p.isCurrent ? '' : p.id;
|
|
300
|
+
opt.textContent = p.name + (p.isCurrent ? ' ●' : '');
|
|
301
|
+
opt.title = p.id;
|
|
302
|
+
if (p.isCurrent) opt.selected = true;
|
|
303
|
+
select.appendChild(opt);
|
|
304
|
+
}
|
|
305
|
+
} catch {
|
|
306
|
+
select.innerHTML = '<option value="">Error</option>';
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Switch handler
|
|
310
|
+
select.addEventListener('change', () => {
|
|
311
|
+
selectedProject = select.value;
|
|
312
|
+
// Clear all cached pages and reload current
|
|
313
|
+
Object.keys(loaded).forEach(k => delete loaded[k]);
|
|
314
|
+
loadPage(currentPage);
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
319
|
+
initProjectSwitcher();
|
|
320
|
+
});
|
|
321
|
+
|
|
255
322
|
// ============================================================
|
|
256
323
|
// Page Loaders
|
|
257
324
|
// ============================================================
|
|
@@ -711,9 +778,15 @@ async function loadObservations() {
|
|
|
711
778
|
const types = [...new Set(allObservations.map(o => o.type).filter(Boolean))];
|
|
712
779
|
|
|
713
780
|
container.innerHTML = `
|
|
714
|
-
<div class="page-header">
|
|
715
|
-
<
|
|
716
|
-
|
|
781
|
+
<div class="page-header" style="display:flex;align-items:center;justify-content:space-between;">
|
|
782
|
+
<div>
|
|
783
|
+
<h1 class="page-title">${t('observations')}</h1>
|
|
784
|
+
<p class="page-subtitle">${allObservations.length} ${t('observationsStored')}</p>
|
|
785
|
+
</div>
|
|
786
|
+
<button class="export-btn" id="btn-export" title="${t('exportData')}">
|
|
787
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M8 2v8M4 7l4 4 4-4M2 12v2h12v-2"/></svg>
|
|
788
|
+
${t('exportData')}
|
|
789
|
+
</button>
|
|
717
790
|
</div>
|
|
718
791
|
|
|
719
792
|
<div class="search-bar">
|
|
@@ -725,6 +798,12 @@ async function loadObservations() {
|
|
|
725
798
|
<div class="obs-grid" id="obs-list"></div>
|
|
726
799
|
`;
|
|
727
800
|
|
|
801
|
+
// Export handler
|
|
802
|
+
document.getElementById('btn-export').addEventListener('click', () => {
|
|
803
|
+
const sep = selectedProject ? `?project=${encodeURIComponent(selectedProject)}` : '';
|
|
804
|
+
window.open(`/api/export${sep}`, '_blank');
|
|
805
|
+
});
|
|
806
|
+
|
|
728
807
|
document.getElementById('obs-search').addEventListener('input', (e) => {
|
|
729
808
|
obsFilter = e.target.value.toLowerCase();
|
|
730
809
|
renderObsList();
|
|
@@ -773,25 +852,32 @@ function renderObsList() {
|
|
|
773
852
|
}
|
|
774
853
|
|
|
775
854
|
list.innerHTML = filtered.map(obs => `
|
|
776
|
-
<div class="obs-card">
|
|
777
|
-
<div class="obs-card-header">
|
|
855
|
+
<div class="obs-card" data-obs-id="${obs.id}">
|
|
856
|
+
<div class="obs-card-header" onclick="toggleObsDetail(${obs.id})">
|
|
778
857
|
<span class="obs-card-id">#${obs.id}</span>
|
|
779
858
|
<span class="type-badge" data-type="${obs.type || 'unknown'}">
|
|
780
859
|
${typeIcons[obs.type] || '❓'} ${obs.type || 'unknown'}
|
|
781
860
|
</span>
|
|
782
861
|
<span class="obs-card-title">${escapeHtml(obs.title || t('untitled'))}</span>
|
|
862
|
+
<span class="obs-expand-icon">▼</span>
|
|
783
863
|
</div>
|
|
784
864
|
<div class="obs-card-meta">
|
|
785
865
|
<span>📁 ${escapeHtml(obs.entityName || 'unknown')}</span>
|
|
786
866
|
${obs.createdAt ? `<span>🕐 ${formatTime(obs.createdAt)}</span>` : ''}
|
|
787
867
|
${obs.accessCount ? `<span>👁 ${obs.accessCount}</span>` : ''}
|
|
788
868
|
</div>
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
869
|
+
<div class="obs-detail" id="obs-detail-${obs.id}" style="display:none;">
|
|
870
|
+
${obs.narrative ? `<div class="obs-detail-section"><label>${t('narrative')}</label><div class="obs-card-narrative">${escapeHtml(obs.narrative)}</div></div>` : ''}
|
|
871
|
+
${obs.facts && obs.facts.length > 0 ? `<div class="obs-detail-section"><label>${t('facts')}</label><div class="obs-card-facts">${obs.facts.map(f => `<span class="fact-tag">${escapeHtml(f)}</span>`).join('')}</div></div>` : ''}
|
|
872
|
+
${obs.concepts && obs.concepts.length > 0 ? `<div class="obs-detail-section"><label>${t('concepts')}</label><div class="obs-card-facts">${obs.concepts.map(c => `<span class="fact-tag concept-tag">${escapeHtml(c)}</span>`).join('')}</div></div>` : ''}
|
|
873
|
+
${obs.filesModified && obs.filesModified.length > 0 ? `<div class="obs-detail-section"><label>${t('files')}</label><div class="obs-card-facts">${obs.filesModified.map(f => `<span class="fact-tag file-tag">${escapeHtml(f)}</span>`).join('')}</div></div>` : ''}
|
|
874
|
+
<div class="obs-detail-actions">
|
|
875
|
+
<button class="delete-btn" onclick="deleteObs(${obs.id}, event)">
|
|
876
|
+
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M2 4h12M5 4V3a1 1 0 011-1h4a1 1 0 011 1v1M6 7v5M10 7v5M3 4l1 9a1 1 0 001 1h6a1 1 0 001-1l1-9"/></svg>
|
|
877
|
+
${t('deleteObs')}
|
|
878
|
+
</button>
|
|
793
879
|
</div>
|
|
794
|
-
|
|
880
|
+
</div>
|
|
795
881
|
</div>
|
|
796
882
|
`).join('');
|
|
797
883
|
}
|
|
@@ -882,6 +968,52 @@ async function loadRetention() {
|
|
|
882
968
|
`;
|
|
883
969
|
}
|
|
884
970
|
|
|
971
|
+
// ============================================================
|
|
972
|
+
// Observation Interactions
|
|
973
|
+
// ============================================================
|
|
974
|
+
|
|
975
|
+
function toggleObsDetail(id) {
|
|
976
|
+
const detail = document.getElementById(`obs-detail-${id}`);
|
|
977
|
+
const card = detail?.closest('.obs-card');
|
|
978
|
+
if (!detail || !card) return;
|
|
979
|
+
|
|
980
|
+
const isOpen = detail.style.display !== 'none';
|
|
981
|
+
detail.style.display = isOpen ? 'none' : 'block';
|
|
982
|
+
card.classList.toggle('expanded', !isOpen);
|
|
983
|
+
|
|
984
|
+
// Rotate expand icon
|
|
985
|
+
const icon = card.querySelector('.obs-expand-icon');
|
|
986
|
+
if (icon) icon.style.transform = isOpen ? '' : 'rotate(180deg)';
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
async function deleteObs(id, event) {
|
|
990
|
+
event?.stopPropagation();
|
|
991
|
+
const msg = t('deleteConfirm').replace('%id%', id);
|
|
992
|
+
if (!confirm(msg)) return;
|
|
993
|
+
|
|
994
|
+
try {
|
|
995
|
+
const sep = selectedProject ? `?project=${encodeURIComponent(selectedProject)}` : '';
|
|
996
|
+
const res = await fetch(`/api/observations/${id}${sep}`, { method: 'DELETE' });
|
|
997
|
+
const data = await res.json();
|
|
998
|
+
if (data.ok) {
|
|
999
|
+
// Remove from local array and re-render
|
|
1000
|
+
allObservations = allObservations.filter(o => o.id !== id);
|
|
1001
|
+
renderObsList();
|
|
1002
|
+
// Update counter in header
|
|
1003
|
+
const subtitle = document.querySelector('#page-observations .page-subtitle');
|
|
1004
|
+
if (subtitle) subtitle.textContent = `${allObservations.length} ${t('observationsStored')}`;
|
|
1005
|
+
} else {
|
|
1006
|
+
alert(data.error || 'Delete failed');
|
|
1007
|
+
}
|
|
1008
|
+
} catch (err) {
|
|
1009
|
+
alert('Delete failed: ' + err.message);
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
// Make functions globally accessible for onclick handlers
|
|
1014
|
+
window.toggleObsDetail = toggleObsDetail;
|
|
1015
|
+
window.deleteObs = deleteObs;
|
|
1016
|
+
|
|
885
1017
|
// ============================================================
|
|
886
1018
|
// Utilities
|
|
887
1019
|
// ============================================================
|
|
@@ -18,6 +18,11 @@
|
|
|
18
18
|
<div class="sidebar-brand">
|
|
19
19
|
<img src="/logo.png" alt="Memorix" class="brand-logo" />
|
|
20
20
|
</div>
|
|
21
|
+
<div class="project-switcher">
|
|
22
|
+
<select id="project-select" title="Switch project / 切换项目">
|
|
23
|
+
<option value="">Loading...</option>
|
|
24
|
+
</select>
|
|
25
|
+
</div>
|
|
21
26
|
<div class="sidebar-nav">
|
|
22
27
|
<button class="nav-btn active" data-page="dashboard" title="Dashboard">
|
|
23
28
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
@@ -1045,4 +1045,201 @@ body {
|
|
|
1045
1045
|
.page {
|
|
1046
1046
|
padding: 16px;
|
|
1047
1047
|
}
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
/* ============================================================
|
|
1051
|
+
* Project Switcher
|
|
1052
|
+
* ============================================================ */
|
|
1053
|
+
|
|
1054
|
+
.project-switcher {
|
|
1055
|
+
padding: 4px 8px 8px;
|
|
1056
|
+
display: flex;
|
|
1057
|
+
justify-content: center;
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
.project-switcher select {
|
|
1061
|
+
width: 44px;
|
|
1062
|
+
height: 28px;
|
|
1063
|
+
background: var(--bg-card);
|
|
1064
|
+
color: var(--text-primary);
|
|
1065
|
+
border: 1px solid var(--border-subtle);
|
|
1066
|
+
border-radius: 6px;
|
|
1067
|
+
font-size: 10px;
|
|
1068
|
+
font-family: var(--font-mono);
|
|
1069
|
+
cursor: pointer;
|
|
1070
|
+
padding: 0 2px;
|
|
1071
|
+
text-align: center;
|
|
1072
|
+
-webkit-appearance: none;
|
|
1073
|
+
-moz-appearance: none;
|
|
1074
|
+
appearance: none;
|
|
1075
|
+
transition: var(--transition-fast);
|
|
1076
|
+
overflow: hidden;
|
|
1077
|
+
text-overflow: ellipsis;
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
.project-switcher select:hover {
|
|
1081
|
+
border-color: var(--accent-cyan);
|
|
1082
|
+
box-shadow: var(--glow-cyan);
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
.project-switcher select:focus {
|
|
1086
|
+
outline: none;
|
|
1087
|
+
border-color: var(--accent-cyan);
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
.project-switcher select option {
|
|
1091
|
+
background: var(--bg-surface);
|
|
1092
|
+
color: var(--text-primary);
|
|
1093
|
+
font-size: 12px;
|
|
1094
|
+
padding: 4px 8px;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
/* ============================================================
|
|
1098
|
+
* Export Button
|
|
1099
|
+
* ============================================================ */
|
|
1100
|
+
|
|
1101
|
+
.export-btn {
|
|
1102
|
+
display: flex;
|
|
1103
|
+
align-items: center;
|
|
1104
|
+
gap: 6px;
|
|
1105
|
+
padding: 8px 16px;
|
|
1106
|
+
background: var(--bg-card);
|
|
1107
|
+
border: 1px solid var(--border-subtle);
|
|
1108
|
+
border-radius: 8px;
|
|
1109
|
+
color: var(--text-secondary);
|
|
1110
|
+
font-size: 13px;
|
|
1111
|
+
font-family: var(--font-sans);
|
|
1112
|
+
cursor: pointer;
|
|
1113
|
+
transition: all 0.2s;
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
.export-btn:hover {
|
|
1117
|
+
background: var(--bg-card-hover);
|
|
1118
|
+
border-color: var(--accent-cyan);
|
|
1119
|
+
color: var(--accent-cyan);
|
|
1120
|
+
box-shadow: var(--glow-cyan);
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
/* ============================================================
|
|
1124
|
+
* Observation Card Enhancements
|
|
1125
|
+
* ============================================================ */
|
|
1126
|
+
|
|
1127
|
+
.obs-card {
|
|
1128
|
+
cursor: pointer;
|
|
1129
|
+
transition: all 0.25s ease;
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
.obs-card:hover {
|
|
1133
|
+
border-color: var(--border-medium);
|
|
1134
|
+
transform: translateY(-1px);
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
.obs-card.expanded {
|
|
1138
|
+
border-color: var(--accent-cyan);
|
|
1139
|
+
box-shadow: var(--glow-cyan);
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
.obs-card-header {
|
|
1143
|
+
display: flex;
|
|
1144
|
+
align-items: center;
|
|
1145
|
+
gap: 8px;
|
|
1146
|
+
position: relative;
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
.obs-expand-icon {
|
|
1150
|
+
margin-left: auto;
|
|
1151
|
+
font-size: 10px;
|
|
1152
|
+
color: var(--text-muted);
|
|
1153
|
+
transition: transform 0.25s ease;
|
|
1154
|
+
flex-shrink: 0;
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
.obs-card:hover .obs-expand-icon {
|
|
1158
|
+
color: var(--text-secondary);
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
/* Observation Detail Section */
|
|
1162
|
+
.obs-detail {
|
|
1163
|
+
margin-top: 12px;
|
|
1164
|
+
padding-top: 12px;
|
|
1165
|
+
border-top: 1px solid var(--border-subtle);
|
|
1166
|
+
animation: slideDown 0.2s ease;
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
@keyframes slideDown {
|
|
1170
|
+
from {
|
|
1171
|
+
opacity: 0;
|
|
1172
|
+
transform: translateY(-8px);
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
to {
|
|
1176
|
+
opacity: 1;
|
|
1177
|
+
transform: translateY(0);
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
.obs-detail-section {
|
|
1182
|
+
margin-bottom: 12px;
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
.obs-detail-section label {
|
|
1186
|
+
display: block;
|
|
1187
|
+
font-size: 11px;
|
|
1188
|
+
font-weight: 600;
|
|
1189
|
+
color: var(--text-muted);
|
|
1190
|
+
text-transform: uppercase;
|
|
1191
|
+
letter-spacing: 0.5px;
|
|
1192
|
+
margin-bottom: 6px;
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
.obs-detail-section .obs-card-narrative {
|
|
1196
|
+
-webkit-line-clamp: unset;
|
|
1197
|
+
white-space: pre-wrap;
|
|
1198
|
+
max-height: none;
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
/* Tag variants */
|
|
1202
|
+
.concept-tag {
|
|
1203
|
+
background: rgba(168, 85, 247, 0.1) !important;
|
|
1204
|
+
border-color: rgba(168, 85, 247, 0.2) !important;
|
|
1205
|
+
color: var(--accent-purple) !important;
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
.file-tag {
|
|
1209
|
+
background: rgba(59, 130, 246, 0.1) !important;
|
|
1210
|
+
border-color: rgba(59, 130, 246, 0.2) !important;
|
|
1211
|
+
color: var(--accent-blue) !important;
|
|
1212
|
+
font-family: var(--font-mono);
|
|
1213
|
+
font-size: 11px !important;
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
/* Delete Button */
|
|
1217
|
+
.obs-detail-actions {
|
|
1218
|
+
display: flex;
|
|
1219
|
+
justify-content: flex-end;
|
|
1220
|
+
margin-top: 12px;
|
|
1221
|
+
padding-top: 8px;
|
|
1222
|
+
border-top: 1px solid var(--border-subtle);
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
.delete-btn {
|
|
1226
|
+
display: flex;
|
|
1227
|
+
align-items: center;
|
|
1228
|
+
gap: 5px;
|
|
1229
|
+
padding: 6px 12px;
|
|
1230
|
+
background: transparent;
|
|
1231
|
+
border: 1px solid rgba(239, 68, 68, 0.2);
|
|
1232
|
+
border-radius: 6px;
|
|
1233
|
+
color: var(--accent-red);
|
|
1234
|
+
font-size: 12px;
|
|
1235
|
+
font-family: var(--font-sans);
|
|
1236
|
+
cursor: pointer;
|
|
1237
|
+
transition: all 0.2s;
|
|
1238
|
+
opacity: 0.7;
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
.delete-btn:hover {
|
|
1242
|
+
background: rgba(239, 68, 68, 0.1);
|
|
1243
|
+
border-color: var(--accent-red);
|
|
1244
|
+
opacity: 1;
|
|
1048
1245
|
}
|