@pcircle/memesh 4.1.2 → 4.1.4
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/.mcp.json +1 -3
- package/README.de.md +12 -1
- package/README.es.md +14 -2
- package/README.fr.md +14 -2
- package/README.ja.md +12 -1
- package/README.ko.md +12 -1
- package/README.md +17 -2
- package/README.pt.md +14 -2
- package/README.th.md +14 -2
- package/README.vi.md +14 -2
- package/README.zh-CN.md +12 -1
- package/README.zh-TW.md +12 -1
- package/dashboard/dist/index.html +13 -4
- package/dist/cli/view-live.d.ts +2 -0
- package/dist/cli/view-live.d.ts.map +1 -0
- package/dist/cli/view-live.js +2177 -0
- package/dist/cli/view-live.js.map +1 -0
- package/dist/cli/view.d.ts +0 -1
- package/dist/cli/view.d.ts.map +1 -1
- package/dist/cli/view.js +0 -2169
- package/dist/cli/view.js.map +1 -1
- package/dist/core/analytics.d.ts +23 -0
- package/dist/core/analytics.d.ts.map +1 -1
- package/dist/core/analytics.js +79 -0
- package/dist/core/analytics.js.map +1 -1
- package/dist/core/config.d.ts +9 -4
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +16 -9
- package/dist/core/config.js.map +1 -1
- package/dist/core/consolidator.d.ts.map +1 -1
- package/dist/core/consolidator.js +1 -0
- package/dist/core/consolidator.js.map +1 -1
- package/dist/core/demo.d.ts +9 -0
- package/dist/core/demo.d.ts.map +1 -0
- package/dist/core/demo.js +79 -0
- package/dist/core/demo.js.map +1 -0
- package/dist/core/doctor.d.ts +2 -1
- package/dist/core/doctor.d.ts.map +1 -1
- package/dist/core/doctor.js +198 -8
- package/dist/core/doctor.js.map +1 -1
- package/dist/core/dreamer.d.ts +65 -0
- package/dist/core/dreamer.d.ts.map +1 -0
- package/dist/core/dreamer.js +477 -0
- package/dist/core/dreamer.js.map +1 -0
- package/dist/core/embedder.d.ts.map +1 -1
- package/dist/core/embedder.js +9 -5
- package/dist/core/embedder.js.map +1 -1
- package/dist/core/extractor.d.ts.map +1 -1
- package/dist/core/extractor.js +44 -22
- package/dist/core/extractor.js.map +1 -1
- package/dist/core/graph.d.ts +1 -0
- package/dist/core/graph.d.ts.map +1 -1
- package/dist/core/graph.js +8 -2
- package/dist/core/graph.js.map +1 -1
- package/dist/core/install-hooks.d.ts +41 -0
- package/dist/core/install-hooks.d.ts.map +1 -0
- package/dist/core/install-hooks.js +181 -0
- package/dist/core/install-hooks.js.map +1 -0
- package/dist/core/install-id.d.ts +9 -0
- package/dist/core/install-id.d.ts.map +1 -0
- package/dist/core/install-id.js +51 -0
- package/dist/core/install-id.js.map +1 -0
- package/dist/core/lesson-engine.d.ts +1 -5
- package/dist/core/lesson-engine.d.ts.map +1 -1
- package/dist/core/lesson-engine.js +13 -13
- package/dist/core/lesson-engine.js.map +1 -1
- package/dist/core/llm-validator.d.ts +16 -0
- package/dist/core/llm-validator.d.ts.map +1 -0
- package/dist/core/llm-validator.js +182 -0
- package/dist/core/llm-validator.js.map +1 -0
- package/dist/core/operations.d.ts.map +1 -1
- package/dist/core/operations.js +6 -2
- package/dist/core/operations.js.map +1 -1
- package/dist/core/projects.d.ts +14 -0
- package/dist/core/projects.d.ts.map +1 -0
- package/dist/core/projects.js +70 -0
- package/dist/core/projects.js.map +1 -0
- package/dist/core/query-expander.js.map +1 -1
- package/dist/core/scoring.d.ts +0 -4
- package/dist/core/scoring.d.ts.map +1 -1
- package/dist/core/scoring.js +3 -10
- package/dist/core/scoring.js.map +1 -1
- package/dist/core/serializer.d.ts.map +1 -1
- package/dist/core/serializer.js +2 -0
- package/dist/core/serializer.js.map +1 -1
- package/dist/core/signal-scorer.d.ts +9 -0
- package/dist/core/signal-scorer.d.ts.map +1 -0
- package/dist/core/signal-scorer.js +70 -0
- package/dist/core/signal-scorer.js.map +1 -0
- package/dist/core/types.d.ts +2 -30
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/updater.d.ts +16 -0
- package/dist/core/updater.d.ts.map +1 -1
- package/dist/core/updater.js +85 -0
- package/dist/core/updater.js.map +1 -1
- package/dist/core/version-check.d.ts +2 -1
- package/dist/core/version-check.d.ts.map +1 -1
- package/dist/core/version-check.js +154 -30
- package/dist/core/version-check.js.map +1 -1
- package/dist/db.d.ts +7 -0
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +91 -0
- package/dist/db.js.map +1 -1
- package/dist/knowledge-graph.d.ts +7 -3
- package/dist/knowledge-graph.d.ts.map +1 -1
- package/dist/knowledge-graph.js +43 -18
- package/dist/knowledge-graph.js.map +1 -1
- package/dist/skills-manifest.json +23 -18
- package/dist/storage/conflicts.d.ts +4 -1
- package/dist/storage/conflicts.d.ts.map +1 -1
- package/dist/storage/conflicts.js +5 -2
- package/dist/storage/conflicts.js.map +1 -1
- package/dist/transports/cli/cli.js +393 -37
- package/dist/transports/cli/cli.js.map +1 -1
- package/dist/transports/http/server.d.ts.map +1 -1
- package/dist/transports/http/server.js +167 -13
- package/dist/transports/http/server.js.map +1 -1
- package/dist/transports/schemas.d.ts +2 -2
- package/hooks/hooks.json +12 -0
- package/package.json +16 -6
- package/plugin.json +1 -1
- package/scripts/hooks/_shared.js +257 -5
- package/scripts/hooks/pre-compact.js +19 -9
- package/scripts/hooks/session-start.js +364 -15
- package/scripts/hooks/session-summary.js +131 -29
- package/scripts/hooks/user-prompt-intent.js +159 -0
- package/skills/memesh/SKILL.md +9 -5
- package/skills/memesh-review/SKILL.md +24 -0
|
@@ -0,0 +1,2177 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
|
5
|
+
const bundledD3 = fs
|
|
6
|
+
.readFileSync(path.join(moduleDir, 'assets', 'd3.v7.min.js'), 'utf8')
|
|
7
|
+
.replace(/<\/script/gi, '<\\/script');
|
|
8
|
+
export function generateLiveDashboardHtml() {
|
|
9
|
+
const CSS = `
|
|
10
|
+
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap');
|
|
11
|
+
|
|
12
|
+
:root {
|
|
13
|
+
--bg-primary: #09090b;
|
|
14
|
+
--bg-secondary: #111113;
|
|
15
|
+
--bg-card: rgba(17,17,19,0.8);
|
|
16
|
+
--bg-hover: rgba(39,39,42,0.5);
|
|
17
|
+
--border: rgba(39,39,42,0.8);
|
|
18
|
+
--border-subtle: rgba(39,39,42,0.4);
|
|
19
|
+
--text-primary: #fafafa;
|
|
20
|
+
--text-secondary: #a1a1aa;
|
|
21
|
+
--text-muted: #71717a;
|
|
22
|
+
--accent: #3b82f6;
|
|
23
|
+
--accent-glow: rgba(59,130,246,0.15);
|
|
24
|
+
--accent-hover: #60a5fa;
|
|
25
|
+
--danger: #ef4444;
|
|
26
|
+
--success: #22c55e;
|
|
27
|
+
--warning: #f59e0b;
|
|
28
|
+
--radius: 12px;
|
|
29
|
+
--radius-sm: 8px;
|
|
30
|
+
--radius-xs: 6px;
|
|
31
|
+
--font-sans: 'Inter', system-ui, -apple-system, sans-serif;
|
|
32
|
+
--font-mono: 'JetBrains Mono', ui-monospace, monospace;
|
|
33
|
+
--shadow-sm: 0 1px 2px rgba(0,0,0,0.3);
|
|
34
|
+
--shadow-md: 0 4px 12px rgba(0,0,0,0.4);
|
|
35
|
+
--shadow-lg: 0 8px 32px rgba(0,0,0,0.5);
|
|
36
|
+
--shadow-glow: 0 0 20px var(--accent-glow);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
40
|
+
|
|
41
|
+
body {
|
|
42
|
+
font-family: var(--font-sans);
|
|
43
|
+
background: var(--bg-primary);
|
|
44
|
+
color: var(--text-primary);
|
|
45
|
+
line-height: 1.6;
|
|
46
|
+
-webkit-font-smoothing: antialiased;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/* Light mode overrides */
|
|
50
|
+
body.light {
|
|
51
|
+
--bg-primary: #fafafa;
|
|
52
|
+
--bg-secondary: #ffffff;
|
|
53
|
+
--bg-card: rgba(255,255,255,0.9);
|
|
54
|
+
--bg-hover: rgba(0,0,0,0.04);
|
|
55
|
+
--border: rgba(0,0,0,0.08);
|
|
56
|
+
--border-subtle: rgba(0,0,0,0.04);
|
|
57
|
+
--text-primary: #09090b;
|
|
58
|
+
--text-secondary: #52525b;
|
|
59
|
+
--text-muted: #a1a1aa;
|
|
60
|
+
--shadow-sm: 0 1px 2px rgba(0,0,0,0.05);
|
|
61
|
+
--shadow-md: 0 4px 12px rgba(0,0,0,0.08);
|
|
62
|
+
--shadow-lg: 0 8px 32px rgba(0,0,0,0.12);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/* Header */
|
|
66
|
+
.header {
|
|
67
|
+
background: var(--bg-secondary);
|
|
68
|
+
border-bottom: 1px solid var(--border);
|
|
69
|
+
padding: 16px 24px;
|
|
70
|
+
display: flex;
|
|
71
|
+
align-items: center;
|
|
72
|
+
justify-content: space-between;
|
|
73
|
+
backdrop-filter: blur(12px);
|
|
74
|
+
position: sticky;
|
|
75
|
+
top: 0;
|
|
76
|
+
z-index: 100;
|
|
77
|
+
}
|
|
78
|
+
.header h1 {
|
|
79
|
+
font-size: 17px;
|
|
80
|
+
font-weight: 700;
|
|
81
|
+
letter-spacing: -0.02em;
|
|
82
|
+
color: var(--text-primary);
|
|
83
|
+
}
|
|
84
|
+
.subtitle {
|
|
85
|
+
font-size: 11px;
|
|
86
|
+
color: var(--text-muted);
|
|
87
|
+
font-weight: 400;
|
|
88
|
+
letter-spacing: 0.02em;
|
|
89
|
+
}
|
|
90
|
+
.version {
|
|
91
|
+
font-family: var(--font-mono);
|
|
92
|
+
font-size: 11px;
|
|
93
|
+
color: var(--text-muted);
|
|
94
|
+
background: var(--bg-hover);
|
|
95
|
+
padding: 2px 8px;
|
|
96
|
+
border-radius: var(--radius-xs);
|
|
97
|
+
}
|
|
98
|
+
.header-right { display: flex; align-items: center; gap: 12px; }
|
|
99
|
+
.header .meta { display: flex; align-items: center; gap: 12px; }
|
|
100
|
+
|
|
101
|
+
.dot {
|
|
102
|
+
width: 7px; height: 7px; border-radius: 50%;
|
|
103
|
+
display: inline-block; margin-right: 6px;
|
|
104
|
+
background: var(--text-muted);
|
|
105
|
+
}
|
|
106
|
+
.dot.connected { background: var(--success); box-shadow: 0 0 6px rgba(34,197,94,0.4); }
|
|
107
|
+
.dot.error { background: var(--danger); }
|
|
108
|
+
|
|
109
|
+
/* Navigation */
|
|
110
|
+
.nav {
|
|
111
|
+
display: flex;
|
|
112
|
+
background: var(--bg-secondary);
|
|
113
|
+
border-bottom: 1px solid var(--border);
|
|
114
|
+
padding: 0 24px;
|
|
115
|
+
gap: 2px;
|
|
116
|
+
overflow-x: auto;
|
|
117
|
+
scrollbar-width: none;
|
|
118
|
+
}
|
|
119
|
+
.nav::-webkit-scrollbar { display: none; }
|
|
120
|
+
.nav button {
|
|
121
|
+
padding: 12px 16px;
|
|
122
|
+
border: none;
|
|
123
|
+
background: none;
|
|
124
|
+
cursor: pointer;
|
|
125
|
+
font-size: 13px;
|
|
126
|
+
font-weight: 500;
|
|
127
|
+
color: var(--text-muted);
|
|
128
|
+
border-bottom: 2px solid transparent;
|
|
129
|
+
transition: all 0.2s ease;
|
|
130
|
+
white-space: nowrap;
|
|
131
|
+
font-family: var(--font-sans);
|
|
132
|
+
}
|
|
133
|
+
.nav button:hover { color: var(--text-secondary); background: var(--bg-hover); }
|
|
134
|
+
.nav button.active { color: var(--text-primary); border-bottom-color: var(--accent); }
|
|
135
|
+
|
|
136
|
+
/* Content */
|
|
137
|
+
.content { max-width: 1280px; margin: 0 auto; padding: 24px; }
|
|
138
|
+
|
|
139
|
+
/* Tab panels */
|
|
140
|
+
.tab-content { display: none; }
|
|
141
|
+
.tab-content.active { display: block; }
|
|
142
|
+
|
|
143
|
+
/* Cards */
|
|
144
|
+
.card {
|
|
145
|
+
background: var(--bg-card);
|
|
146
|
+
border: 1px solid var(--border);
|
|
147
|
+
border-radius: var(--radius);
|
|
148
|
+
padding: 20px;
|
|
149
|
+
margin-bottom: 16px;
|
|
150
|
+
backdrop-filter: blur(8px);
|
|
151
|
+
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
|
152
|
+
}
|
|
153
|
+
.card:hover { border-color: rgba(59,130,246,0.2); box-shadow: var(--shadow-glow); }
|
|
154
|
+
.card h2 { font-size: 15px; font-weight: 600; margin-bottom: 16px; color: var(--text-primary); letter-spacing: -0.01em; }
|
|
155
|
+
|
|
156
|
+
/* Forms */
|
|
157
|
+
input, button, textarea { font-family: var(--font-sans); font-size: 14px; }
|
|
158
|
+
.search-row { display: flex; gap: 8px; margin-bottom: 20px; }
|
|
159
|
+
.search-input {
|
|
160
|
+
flex: 1;
|
|
161
|
+
padding: 10px 14px;
|
|
162
|
+
background: var(--bg-primary);
|
|
163
|
+
border: 1px solid var(--border);
|
|
164
|
+
border-radius: var(--radius-sm);
|
|
165
|
+
color: var(--text-primary);
|
|
166
|
+
font-size: 14px;
|
|
167
|
+
font-family: var(--font-sans);
|
|
168
|
+
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
|
169
|
+
outline: none;
|
|
170
|
+
}
|
|
171
|
+
.search-input:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-glow); }
|
|
172
|
+
.search-input::placeholder { color: var(--text-muted); }
|
|
173
|
+
|
|
174
|
+
/* Buttons */
|
|
175
|
+
.btn {
|
|
176
|
+
padding: 8px 16px;
|
|
177
|
+
border: 1px solid var(--border);
|
|
178
|
+
border-radius: var(--radius-sm);
|
|
179
|
+
cursor: pointer;
|
|
180
|
+
font-size: 13px;
|
|
181
|
+
font-weight: 500;
|
|
182
|
+
font-family: var(--font-sans);
|
|
183
|
+
transition: all 0.15s ease;
|
|
184
|
+
background: var(--bg-secondary);
|
|
185
|
+
color: var(--text-primary);
|
|
186
|
+
}
|
|
187
|
+
.btn:hover { background: var(--bg-hover); border-color: var(--text-muted); }
|
|
188
|
+
.btn-primary { background: var(--accent); border-color: var(--accent); color: white; }
|
|
189
|
+
.btn-primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
|
|
190
|
+
.btn-secondary { background: var(--bg-secondary); color: var(--text-secondary); border-color: var(--border); }
|
|
191
|
+
.btn-secondary:hover { background: var(--bg-hover); color: var(--text-primary); }
|
|
192
|
+
.btn-danger { border-color: var(--danger); color: var(--danger); background: transparent; }
|
|
193
|
+
.btn-danger:hover { background: rgba(239,68,68,0.1); }
|
|
194
|
+
.btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
195
|
+
.btn-sm { padding: 6px 12px; font-size: 12px; }
|
|
196
|
+
|
|
197
|
+
/* Tables */
|
|
198
|
+
.table-wrap { overflow-x: auto; border-radius: var(--radius); border: 1px solid var(--border); -webkit-overflow-scrolling: touch; }
|
|
199
|
+
table { width: 100%; border-collapse: collapse; min-width: 500px; table-layout: auto; }
|
|
200
|
+
th {
|
|
201
|
+
text-align: left;
|
|
202
|
+
padding: 10px 14px;
|
|
203
|
+
font-size: 11px;
|
|
204
|
+
font-weight: 600;
|
|
205
|
+
color: var(--text-muted);
|
|
206
|
+
text-transform: uppercase;
|
|
207
|
+
letter-spacing: 0.05em;
|
|
208
|
+
background: var(--bg-secondary);
|
|
209
|
+
border-bottom: 1px solid var(--border);
|
|
210
|
+
}
|
|
211
|
+
td {
|
|
212
|
+
padding: 10px 14px;
|
|
213
|
+
border-bottom: 1px solid var(--border-subtle);
|
|
214
|
+
font-size: 13px;
|
|
215
|
+
color: var(--text-secondary);
|
|
216
|
+
word-break: break-word;
|
|
217
|
+
}
|
|
218
|
+
tbody tr { transition: background 0.15s ease; }
|
|
219
|
+
tbody tr:hover { background: var(--bg-hover); }
|
|
220
|
+
tbody tr:last-child td { border-bottom: none; }
|
|
221
|
+
|
|
222
|
+
/* Badges */
|
|
223
|
+
.badge { display: inline-block; padding: 2px 10px; border-radius: 999px; font-size: 11px; font-weight: 500; letter-spacing: 0.02em; }
|
|
224
|
+
.badge-active { background: rgba(34,197,94,0.12); color: #4ade80; }
|
|
225
|
+
.badge-archived { background: rgba(239,68,68,0.12); color: #f87171; }
|
|
226
|
+
.badge-type { background: var(--accent-glow); color: var(--accent-hover); }
|
|
227
|
+
|
|
228
|
+
/* Entity name */
|
|
229
|
+
.entity-name { font-weight: 500; color: var(--text-primary); }
|
|
230
|
+
.entity-obs { color: var(--text-muted); font-size: 12px; font-family: var(--font-mono); }
|
|
231
|
+
|
|
232
|
+
/* Tag pills */
|
|
233
|
+
.tag-pill {
|
|
234
|
+
display: inline-block; padding: 2px 8px; margin: 2px;
|
|
235
|
+
border-radius: 999px; font-size: 11px;
|
|
236
|
+
font-family: var(--font-mono);
|
|
237
|
+
background: var(--bg-hover);
|
|
238
|
+
color: var(--text-secondary);
|
|
239
|
+
border: 1px solid var(--border-subtle);
|
|
240
|
+
cursor: default;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/* Placeholder */
|
|
244
|
+
.placeholder { text-align: center; padding: 60px 20px; color: var(--text-muted); font-size: 14px; }
|
|
245
|
+
.placeholder .icon { font-size: 32px; margin-bottom: 12px; display: block; }
|
|
246
|
+
|
|
247
|
+
/* Result / error box */
|
|
248
|
+
.result-box { background: var(--bg-secondary); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 14px; margin-top: 12px; font-size: 13px; font-family: var(--font-mono); white-space: pre-wrap; max-height: 400px; overflow: auto; color: var(--text-secondary); }
|
|
249
|
+
.result-box.error { background: rgba(239,68,68,0.08); border-color: rgba(239,68,68,0.2); color: #f87171; }
|
|
250
|
+
|
|
251
|
+
/* Loading spinner */
|
|
252
|
+
.loading { display: inline-block; width: 16px; height: 16px; border: 2px solid var(--border); border-top-color: var(--accent); border-radius: 50%; animation: spin 0.7s linear infinite; vertical-align: middle; }
|
|
253
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
254
|
+
|
|
255
|
+
/* Settings */
|
|
256
|
+
.settings-section { margin-bottom: 28px; }
|
|
257
|
+
.settings-section h3 { font-size: 13px; font-weight: 600; color: var(--text-muted); margin-bottom: 12px; padding-bottom: 8px; border-bottom: 1px solid var(--border); text-transform: uppercase; letter-spacing: 0.05em; }
|
|
258
|
+
.form-group { margin-bottom: 14px; }
|
|
259
|
+
.form-label { display: block; font-size: 13px; font-weight: 500; color: var(--text-primary); margin-bottom: 5px; }
|
|
260
|
+
.form-hint { font-size: 12px; color: var(--text-muted); margin-top: 4px; }
|
|
261
|
+
.provider-radio-group { display: flex; gap: 8px; flex-wrap: wrap; }
|
|
262
|
+
.provider-radio { display: flex; align-items: center; gap: 6px; padding: 8px 14px; border: 1px solid var(--border); border-radius: var(--radius-sm); cursor: pointer; transition: border-color 0.15s; color: var(--text-secondary); font-size: 13px; }
|
|
263
|
+
.provider-radio:hover { border-color: var(--accent); color: var(--text-primary); }
|
|
264
|
+
.provider-radio.selected { border-color: var(--accent); background: var(--accent-glow); color: var(--accent-hover); }
|
|
265
|
+
.provider-radio input[type=radio] { accent-color: var(--accent); }
|
|
266
|
+
.cap-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 10px; margin-bottom: 16px; }
|
|
267
|
+
.cap-item { background: var(--bg-secondary); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 10px 12px; font-size: 12px; }
|
|
268
|
+
.cap-item .cap-label { color: var(--text-muted); margin-bottom: 2px; }
|
|
269
|
+
.cap-item .cap-value { font-weight: 600; color: var(--text-primary); font-family: var(--font-mono); }
|
|
270
|
+
.status-dot { display: inline-block; width: 7px; height: 7px; border-radius: 50%; margin-right: 5px; }
|
|
271
|
+
.status-dot.ok { background: var(--success); box-shadow: 0 0 5px rgba(34,197,94,0.4); }
|
|
272
|
+
.status-dot.warn { background: var(--warning); }
|
|
273
|
+
|
|
274
|
+
/* Welcome wizard modal */
|
|
275
|
+
.wizard-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.7); display: flex; align-items: center; justify-content: center; z-index: 1000; backdrop-filter: blur(4px); }
|
|
276
|
+
.wizard-overlay.hidden { display: none; }
|
|
277
|
+
.wizard-modal { background: var(--bg-secondary); border: 1px solid var(--border); border-radius: var(--radius); padding: 32px; max-width: 520px; width: 90%; box-shadow: var(--shadow-lg); }
|
|
278
|
+
.wizard-modal h2 { font-size: 22px; font-weight: 700; margin-bottom: 8px; color: var(--text-primary); letter-spacing: -0.02em; }
|
|
279
|
+
.wizard-modal .subtitle { color: var(--text-secondary); font-size: 14px; margin-bottom: 24px; line-height: 1.6; }
|
|
280
|
+
.wizard-steps { display: flex; gap: 6px; margin-bottom: 28px; }
|
|
281
|
+
.wizard-step-dot { flex: 1; height: 3px; background: var(--border); border-radius: 2px; transition: background 0.2s; }
|
|
282
|
+
.wizard-step-dot.active { background: var(--accent); }
|
|
283
|
+
.wizard-step-dot.done { background: var(--success); }
|
|
284
|
+
.wizard-actions { display: flex; gap: 10px; justify-content: flex-end; margin-top: 24px; }
|
|
285
|
+
|
|
286
|
+
/* Analytics */
|
|
287
|
+
.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 12px; margin-bottom: 20px; }
|
|
288
|
+
.stat-card { background: var(--bg-card); border: 1px solid var(--border); border-radius: var(--radius); padding: 20px; text-align: center; backdrop-filter: blur(8px); }
|
|
289
|
+
.stat-card .stat-value { font-size: 32px; font-weight: 700; color: var(--accent); font-family: var(--font-mono); line-height: 1.2; }
|
|
290
|
+
.stat-card .stat-label { font-size: 11px; color: var(--text-muted); margin-top: 4px; text-transform: uppercase; letter-spacing: 0.05em; }
|
|
291
|
+
.bar-chart { margin-bottom: 20px; }
|
|
292
|
+
.bar-row { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; font-size: 13px; }
|
|
293
|
+
.bar-label { width: 140px; flex-shrink: 0; color: var(--text-secondary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
294
|
+
.bar-track { flex: 1; background: var(--bg-hover); border-radius: 4px; height: 8px; overflow: hidden; }
|
|
295
|
+
.bar-fill { height: 100%; background: var(--accent); border-radius: 4px; transition: width 0.3s ease; opacity: 0.8; }
|
|
296
|
+
.bar-count { width: 36px; text-align: right; color: var(--text-muted); flex-shrink: 0; font-family: var(--font-mono); font-size: 12px; }
|
|
297
|
+
.tag-cloud { display: flex; flex-wrap: wrap; gap: 8px; }
|
|
298
|
+
|
|
299
|
+
/* Timeline */
|
|
300
|
+
.chain { display: flex; align-items: center; flex-wrap: wrap; gap: 0; margin-bottom: 16px; padding: 16px; background: var(--bg-secondary); border: 1px solid var(--border); border-radius: var(--radius); }
|
|
301
|
+
.chain-node { background: var(--bg-card); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 8px 14px; font-size: 13px; position: relative; transition: border-color 0.15s; }
|
|
302
|
+
.chain-node:hover { border-color: var(--accent); }
|
|
303
|
+
.chain-node.archived { opacity: 0.4; }
|
|
304
|
+
.chain-node .cn-name { font-weight: 600; color: var(--text-primary); }
|
|
305
|
+
.chain-node .cn-type { font-size: 11px; color: var(--text-muted); font-family: var(--font-mono); }
|
|
306
|
+
.chain-arrow { font-size: 16px; color: var(--accent); padding: 0 8px; flex-shrink: 0; opacity: 0.6; }
|
|
307
|
+
|
|
308
|
+
/* Graph */
|
|
309
|
+
.graph-svg-wrap { background: var(--bg-card); border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; position: relative; }
|
|
310
|
+
.graph-svg-wrap svg { width: 100%; height: 520px; display: block; }
|
|
311
|
+
.graph-controls { display: flex; gap: 8px; margin-bottom: 12px; align-items: center; font-size: 12px; color: var(--text-muted); }
|
|
312
|
+
|
|
313
|
+
/* Graph tooltip */
|
|
314
|
+
.graph-tooltip {
|
|
315
|
+
position: absolute;
|
|
316
|
+
background: var(--bg-card);
|
|
317
|
+
border: 1px solid var(--border);
|
|
318
|
+
border-radius: var(--radius-sm);
|
|
319
|
+
padding: 12px 16px;
|
|
320
|
+
font-size: 12px;
|
|
321
|
+
pointer-events: none;
|
|
322
|
+
backdrop-filter: blur(12px);
|
|
323
|
+
box-shadow: var(--shadow-lg);
|
|
324
|
+
max-width: 280px;
|
|
325
|
+
z-index: 10;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/* Manage */
|
|
329
|
+
.manage-action-cell { display: flex; gap: 6px; flex-wrap: wrap; }
|
|
330
|
+
|
|
331
|
+
/* Feedback widget */
|
|
332
|
+
#feedback-btn {
|
|
333
|
+
position: fixed; bottom: 24px; right: 24px;
|
|
334
|
+
background: var(--accent); color: white;
|
|
335
|
+
border: none; padding: 10px 16px;
|
|
336
|
+
border-radius: 999px; cursor: pointer;
|
|
337
|
+
font-size: 13px; font-weight: 500;
|
|
338
|
+
box-shadow: var(--shadow-md), var(--shadow-glow);
|
|
339
|
+
transition: all 0.2s ease; z-index: 50;
|
|
340
|
+
font-family: var(--font-sans);
|
|
341
|
+
}
|
|
342
|
+
#feedback-btn:hover { transform: translateY(-2px); box-shadow: var(--shadow-lg), 0 0 30px var(--accent-glow); }
|
|
343
|
+
#feedback-panel {
|
|
344
|
+
position: fixed; bottom: 72px; right: 24px;
|
|
345
|
+
width: 340px; background: var(--bg-secondary);
|
|
346
|
+
border: 1px solid var(--border); border-radius: var(--radius);
|
|
347
|
+
padding: 20px; box-shadow: var(--shadow-lg);
|
|
348
|
+
z-index: 51; backdrop-filter: blur(16px); display: none;
|
|
349
|
+
}
|
|
350
|
+
#feedback-panel.open { display: block; }
|
|
351
|
+
#feedback-panel h3 { font-size: 14px; font-weight: 600; margin-bottom: 14px; color: var(--text-primary); }
|
|
352
|
+
.fb-radio-group { display: flex; gap: 8px; margin-bottom: 12px; }
|
|
353
|
+
.fb-radio { display: flex; align-items: center; gap: 5px; padding: 5px 10px; border: 1px solid var(--border); border-radius: var(--radius-xs); cursor: pointer; font-size: 12px; color: var(--text-secondary); }
|
|
354
|
+
.fb-radio.selected { border-color: var(--accent); background: var(--accent-glow); color: var(--accent-hover); }
|
|
355
|
+
.fb-radio input { accent-color: var(--accent); }
|
|
356
|
+
#fb-desc { width: 100%; height: 80px; padding: 8px 10px; border: 1px solid var(--border); border-radius: var(--radius-xs); font-size: 13px; resize: vertical; outline: none; margin-bottom: 10px; background: var(--bg-primary); color: var(--text-primary); font-family: var(--font-sans); }
|
|
357
|
+
#fb-desc:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-glow); }
|
|
358
|
+
.fb-sys-row { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--text-muted); margin-bottom: 12px; }
|
|
359
|
+
|
|
360
|
+
/* Highlight */
|
|
361
|
+
mark { background: rgba(59,130,246,0.2); color: inherit; padding: 1px 3px; border-radius: 3px; }
|
|
362
|
+
|
|
363
|
+
/* Hidden */
|
|
364
|
+
.hidden { display: none !important; }
|
|
365
|
+
|
|
366
|
+
/* Scrollbar */
|
|
367
|
+
::-webkit-scrollbar { width: 6px; height: 6px; }
|
|
368
|
+
::-webkit-scrollbar-track { background: transparent; }
|
|
369
|
+
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
|
|
370
|
+
::-webkit-scrollbar-thumb:hover { background: var(--text-muted); }
|
|
371
|
+
|
|
372
|
+
/* Responsive */
|
|
373
|
+
@media (max-width: 768px) {
|
|
374
|
+
.content { padding: 16px; }
|
|
375
|
+
.nav button { padding: 10px 12px; font-size: 12px; }
|
|
376
|
+
.header h1 { font-size: 15px; }
|
|
377
|
+
.search-row { flex-direction: column; }
|
|
378
|
+
.stats-grid { grid-template-columns: 1fr 1fr; }
|
|
379
|
+
#feedback-panel { width: calc(100vw - 48px); }
|
|
380
|
+
#feedback-btn { bottom: 16px; right: 16px; }
|
|
381
|
+
}
|
|
382
|
+
@media (max-width: 480px) {
|
|
383
|
+
.nav { gap: 0; }
|
|
384
|
+
.nav button { flex: 1; font-size: 11px; padding: 10px 6px; }
|
|
385
|
+
.stats-grid { grid-template-columns: 1fr; }
|
|
386
|
+
.header { padding: 12px 16px; }
|
|
387
|
+
}
|
|
388
|
+
`.trim();
|
|
389
|
+
const SCRIPT = `
|
|
390
|
+
(function () {
|
|
391
|
+
'use strict';
|
|
392
|
+
|
|
393
|
+
var _currentVersion = '';
|
|
394
|
+
var _currentSearchLevel = '';
|
|
395
|
+
|
|
396
|
+
// ---- API ----
|
|
397
|
+
async function apiCall(method, path, body) {
|
|
398
|
+
var controller = new AbortController();
|
|
399
|
+
var timeout = setTimeout(function() { controller.abort(); }, 10000);
|
|
400
|
+
try {
|
|
401
|
+
var opts = { method: method, headers: { 'Content-Type': 'application/json' }, signal: controller.signal };
|
|
402
|
+
if (body !== undefined) opts.body = JSON.stringify(body);
|
|
403
|
+
var res = await fetch(path, opts);
|
|
404
|
+
clearTimeout(timeout);
|
|
405
|
+
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
406
|
+
return await res.json();
|
|
407
|
+
} catch (err) {
|
|
408
|
+
clearTimeout(timeout);
|
|
409
|
+
if (err.name === 'AbortError') throw new Error('Request timed out (10s)');
|
|
410
|
+
throw err;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// ---- Theme (dark default, light class toggles light mode) ----
|
|
415
|
+
(function initTheme() {
|
|
416
|
+
var saved = localStorage.getItem('memesh-theme');
|
|
417
|
+
if (saved === 'light') { document.body.classList.add('light'); }
|
|
418
|
+
var themeBtn = document.getElementById('theme-btn');
|
|
419
|
+
if (themeBtn) {
|
|
420
|
+
themeBtn.textContent = document.body.classList.contains('light') ? '\\ud83c\\udf19' : '\\u2600\\ufe0f';
|
|
421
|
+
themeBtn.addEventListener('click', function () {
|
|
422
|
+
document.body.classList.toggle('light');
|
|
423
|
+
var isLight = document.body.classList.contains('light');
|
|
424
|
+
themeBtn.textContent = isLight ? '\\ud83c\\udf19' : '\\u2600\\ufe0f';
|
|
425
|
+
localStorage.setItem('memesh-theme', isLight ? 'light' : 'dark');
|
|
426
|
+
try { apiCall('POST', '/v1/config', { theme: isLight ? 'light' : 'dark' }); } catch (_) {}
|
|
427
|
+
// Update graph label colors if graph has been rendered
|
|
428
|
+
var newLabelColor = isLight ? '#09090b' : '#fafafa';
|
|
429
|
+
var graphWrap = document.getElementById('graph-svg-wrap');
|
|
430
|
+
if (graphWrap) {
|
|
431
|
+
graphWrap.querySelectorAll('svg g text:not([font-size="9"])').forEach(function(el) {
|
|
432
|
+
el.setAttribute('fill', newLabelColor);
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
})();
|
|
438
|
+
|
|
439
|
+
// ---- Tab switching ----
|
|
440
|
+
document.getElementById('nav').addEventListener('click', function (e) {
|
|
441
|
+
var btn = e.target.closest('button[data-tab]');
|
|
442
|
+
if (!btn) return;
|
|
443
|
+
var tab = btn.dataset.tab;
|
|
444
|
+
document.querySelectorAll('.nav button').forEach(function (b) { b.classList.remove('active'); });
|
|
445
|
+
btn.classList.add('active');
|
|
446
|
+
document.querySelectorAll('.tab-content').forEach(function (el) { el.classList.remove('active'); });
|
|
447
|
+
document.getElementById('tab-' + tab).classList.add('active');
|
|
448
|
+
if (tab === 'browse') loadBrowse();
|
|
449
|
+
if (tab === 'graph') loadGraph();
|
|
450
|
+
if (tab === 'analytics') loadAnalytics();
|
|
451
|
+
if (tab === 'timeline') loadTimeline();
|
|
452
|
+
if (tab === 'manage') loadManage();
|
|
453
|
+
if (tab === 'settings') loadSettings();
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
// ---- Health check ----
|
|
457
|
+
async function checkHealth() {
|
|
458
|
+
var indicator = document.getElementById('health-indicator');
|
|
459
|
+
var versionLabel = document.getElementById('version-label');
|
|
460
|
+
try {
|
|
461
|
+
var data = await apiCall('GET', '/v1/health');
|
|
462
|
+
if (!data.success) throw new Error(data.error || 'API error');
|
|
463
|
+
_currentVersion = data.data.version || '';
|
|
464
|
+
var dot = document.createElement('span');
|
|
465
|
+
dot.className = 'dot';
|
|
466
|
+
indicator.textContent = '';
|
|
467
|
+
indicator.appendChild(dot);
|
|
468
|
+
indicator.appendChild(document.createTextNode('Connected'));
|
|
469
|
+
versionLabel.textContent = 'v' + data.data.version + ' \u00b7 ' + data.data.entity_count + ' entities';
|
|
470
|
+
} catch (_err) {
|
|
471
|
+
var dot2 = document.createElement('span');
|
|
472
|
+
dot2.className = 'dot error';
|
|
473
|
+
indicator.textContent = '';
|
|
474
|
+
indicator.appendChild(dot2);
|
|
475
|
+
indicator.appendChild(document.createTextNode('Disconnected'));
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// ---- Shared: build entity table from array using only DOM APIs ----
|
|
480
|
+
// Redesigned: shows Time (locale) | Memory Preview | Source/Type | Status | Tags
|
|
481
|
+
// Entity name is secondary — first observation preview is the primary content
|
|
482
|
+
function buildEntityTable(entities, highlightTerm) {
|
|
483
|
+
var table = document.createElement('table');
|
|
484
|
+
var thead = document.createElement('thead');
|
|
485
|
+
var hrow = document.createElement('tr');
|
|
486
|
+
['Time', 'Memory', 'Type', 'Status', 'Tags'].forEach(function (h) {
|
|
487
|
+
var th = document.createElement('th');
|
|
488
|
+
th.textContent = h;
|
|
489
|
+
if (h === 'Memory') th.style.width = '45%';
|
|
490
|
+
if (h === 'Time') th.style.width = '140px';
|
|
491
|
+
hrow.appendChild(th);
|
|
492
|
+
});
|
|
493
|
+
thead.appendChild(hrow);
|
|
494
|
+
table.appendChild(thead);
|
|
495
|
+
var tbody = document.createElement('tbody');
|
|
496
|
+
entities.forEach(function (e) {
|
|
497
|
+
var status = e.archived ? 'archived' : (e.status || 'active');
|
|
498
|
+
var tr = document.createElement('tr');
|
|
499
|
+
if (status === 'archived') tr.style.opacity = '0.5';
|
|
500
|
+
|
|
501
|
+
// Time column — locale-formatted timestamp
|
|
502
|
+
var tdTime = document.createElement('td');
|
|
503
|
+
tdTime.style.cssText = 'font-family:var(--font-mono);font-size:11px;color:var(--text-muted);white-space:nowrap;';
|
|
504
|
+
try {
|
|
505
|
+
var d = new Date(e.created_at);
|
|
506
|
+
tdTime.textContent = d.toLocaleDateString(undefined, { month: 'short', day: 'numeric' }) + ' ' + d.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' });
|
|
507
|
+
} catch (_) {
|
|
508
|
+
tdTime.textContent = e.created_at || '—';
|
|
509
|
+
}
|
|
510
|
+
tdTime.title = e.created_at || '';
|
|
511
|
+
tr.appendChild(tdTime);
|
|
512
|
+
|
|
513
|
+
// Memory column — first observation as preview, entity name as secondary
|
|
514
|
+
var tdMemory = document.createElement('td');
|
|
515
|
+
var memoryContent = document.createElement('div');
|
|
516
|
+
|
|
517
|
+
// Primary: first observation (the actual useful content)
|
|
518
|
+
var preview = document.createElement('div');
|
|
519
|
+
preview.style.cssText = 'font-size:13px;line-height:1.5;margin-bottom:2px;';
|
|
520
|
+
var firstObs = (e.observations && e.observations.length > 0) ? e.observations[0] : '(no observations)';
|
|
521
|
+
var previewText = firstObs.length > 120 ? firstObs.slice(0, 120) + '\\u2026' : firstObs;
|
|
522
|
+
if (highlightTerm) {
|
|
523
|
+
highlightText(preview, previewText, highlightTerm);
|
|
524
|
+
} else {
|
|
525
|
+
preview.textContent = previewText;
|
|
526
|
+
}
|
|
527
|
+
memoryContent.appendChild(preview);
|
|
528
|
+
|
|
529
|
+
// Secondary: entity name (technical identifier) + observation count
|
|
530
|
+
var meta = document.createElement('div');
|
|
531
|
+
meta.style.cssText = 'font-size:11px;color:var(--text-muted);font-family:var(--font-mono);';
|
|
532
|
+
var obsCount = e.observations ? e.observations.length : 0;
|
|
533
|
+
meta.textContent = e.name + (obsCount > 1 ? ' \\u00b7 ' + obsCount + ' observations' : '');
|
|
534
|
+
memoryContent.appendChild(meta);
|
|
535
|
+
|
|
536
|
+
tdMemory.appendChild(memoryContent);
|
|
537
|
+
tr.appendChild(tdMemory);
|
|
538
|
+
|
|
539
|
+
// Type badge
|
|
540
|
+
var tdType = document.createElement('td');
|
|
541
|
+
var typeBadge = document.createElement('span');
|
|
542
|
+
typeBadge.className = 'badge badge-type';
|
|
543
|
+
typeBadge.textContent = e.type;
|
|
544
|
+
tdType.appendChild(typeBadge);
|
|
545
|
+
tr.appendChild(tdType);
|
|
546
|
+
|
|
547
|
+
// Status badge
|
|
548
|
+
var tdStatus = document.createElement('td');
|
|
549
|
+
var statusBadge = document.createElement('span');
|
|
550
|
+
statusBadge.className = 'badge badge-' + (status === 'archived' ? 'archived' : 'active');
|
|
551
|
+
statusBadge.textContent = status;
|
|
552
|
+
tdStatus.appendChild(statusBadge);
|
|
553
|
+
tr.appendChild(tdStatus);
|
|
554
|
+
|
|
555
|
+
// Tags
|
|
556
|
+
var tdTags = document.createElement('td');
|
|
557
|
+
if (e.tags && e.tags.length > 0) {
|
|
558
|
+
e.tags.slice(0, 3).forEach(function (t) {
|
|
559
|
+
var pill = document.createElement('span');
|
|
560
|
+
pill.className = 'tag-pill';
|
|
561
|
+
pill.textContent = t;
|
|
562
|
+
tdTags.appendChild(pill);
|
|
563
|
+
});
|
|
564
|
+
if (e.tags.length > 3) {
|
|
565
|
+
var more = document.createElement('span');
|
|
566
|
+
more.className = 'tag-pill';
|
|
567
|
+
more.style.opacity = '0.6';
|
|
568
|
+
more.textContent = '+' + (e.tags.length - 3);
|
|
569
|
+
tdTags.appendChild(more);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
tr.appendChild(tdTags);
|
|
573
|
+
|
|
574
|
+
tbody.appendChild(tr);
|
|
575
|
+
});
|
|
576
|
+
table.appendChild(tbody);
|
|
577
|
+
var wrap = document.createElement('div');
|
|
578
|
+
wrap.className = 'table-wrap';
|
|
579
|
+
wrap.appendChild(table);
|
|
580
|
+
return wrap;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function showError(container, msg) {
|
|
584
|
+
container.textContent = '';
|
|
585
|
+
var box = document.createElement('div');
|
|
586
|
+
box.className = 'result-box error';
|
|
587
|
+
box.textContent = msg;
|
|
588
|
+
container.appendChild(box);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function showPlaceholder(container, text) {
|
|
592
|
+
container.textContent = '';
|
|
593
|
+
var ph = document.createElement('div');
|
|
594
|
+
ph.className = 'placeholder';
|
|
595
|
+
ph.textContent = text;
|
|
596
|
+
container.appendChild(ph);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function showSpinner(container) {
|
|
600
|
+
container.textContent = '';
|
|
601
|
+
var wrap = document.createElement('div');
|
|
602
|
+
wrap.className = 'placeholder';
|
|
603
|
+
var sp = document.createElement('div');
|
|
604
|
+
sp.className = 'loading';
|
|
605
|
+
wrap.appendChild(sp);
|
|
606
|
+
container.appendChild(wrap);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// ---- Search term highlighting (XSS-safe via createElement/textContent) ----
|
|
610
|
+
function highlightText(container, text, term) {
|
|
611
|
+
if (!term) { container.textContent = text; return; }
|
|
612
|
+
var lower = text.toLowerCase();
|
|
613
|
+
var lowerTerm = term.toLowerCase();
|
|
614
|
+
var termLen = lowerTerm.length;
|
|
615
|
+
if (termLen === 0) { container.textContent = text; return; }
|
|
616
|
+
var pos = 0;
|
|
617
|
+
while (pos < text.length) {
|
|
618
|
+
var idx = lower.indexOf(lowerTerm, pos);
|
|
619
|
+
if (idx === -1) {
|
|
620
|
+
container.appendChild(document.createTextNode(text.slice(pos)));
|
|
621
|
+
break;
|
|
622
|
+
}
|
|
623
|
+
if (idx > pos) {
|
|
624
|
+
container.appendChild(document.createTextNode(text.slice(pos, idx)));
|
|
625
|
+
}
|
|
626
|
+
var mark = document.createElement('mark');
|
|
627
|
+
mark.textContent = text.slice(idx, idx + termLen);
|
|
628
|
+
container.appendChild(mark);
|
|
629
|
+
pos = idx + termLen;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// ---- Search tab ----
|
|
634
|
+
var searchInput = document.getElementById('search-query');
|
|
635
|
+
var searchBtn = document.getElementById('search-btn');
|
|
636
|
+
var searchResults = document.getElementById('search-results');
|
|
637
|
+
|
|
638
|
+
async function doSearch() {
|
|
639
|
+
var q = searchInput.value.trim();
|
|
640
|
+
if (!q) return;
|
|
641
|
+
showSpinner(searchResults);
|
|
642
|
+
try {
|
|
643
|
+
var data = await apiCall('POST', '/v1/recall', { query: q, limit: 20 });
|
|
644
|
+
searchResults.textContent = '';
|
|
645
|
+
if (!data.success) { showError(searchResults, data.error || 'Unknown error'); return; }
|
|
646
|
+
var entities = Array.isArray(data.data) ? data.data : (data.data.entities || []);
|
|
647
|
+
if (entities.length === 0) { showPlaceholder(searchResults, 'No results for "' + q + '"'); return; }
|
|
648
|
+
searchResults.appendChild(buildEntityTable(entities, q));
|
|
649
|
+
} catch (err) {
|
|
650
|
+
showError(searchResults, err.message);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
searchBtn.addEventListener('click', doSearch);
|
|
655
|
+
searchInput.addEventListener('keydown', function (e) { if (e.key === 'Enter') doSearch(); });
|
|
656
|
+
|
|
657
|
+
// ---- Browse tab ----
|
|
658
|
+
var allEntities = [];
|
|
659
|
+
var browseFilter = document.getElementById('browse-filter');
|
|
660
|
+
var browseWrap = document.getElementById('browse-table-wrap');
|
|
661
|
+
|
|
662
|
+
async function loadBrowse() {
|
|
663
|
+
showSpinner(browseWrap);
|
|
664
|
+
try {
|
|
665
|
+
var data = await apiCall('GET', '/v1/entities?limit=200');
|
|
666
|
+
if (!data.success) throw new Error(data.error || 'Failed to load entities');
|
|
667
|
+
allEntities = data.data || [];
|
|
668
|
+
renderBrowseTable(browseFilter.value);
|
|
669
|
+
} catch (err) {
|
|
670
|
+
showError(browseWrap, err.message);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
function renderBrowseTable(filter) {
|
|
675
|
+
var f = (filter || '').toLowerCase();
|
|
676
|
+
var rows = allEntities.filter(function (e) {
|
|
677
|
+
return !f || e.name.toLowerCase().includes(f) || e.type.toLowerCase().includes(f);
|
|
678
|
+
});
|
|
679
|
+
browseWrap.textContent = '';
|
|
680
|
+
if (rows.length === 0) { showPlaceholder(browseWrap, 'No entities found'); return; }
|
|
681
|
+
browseWrap.appendChild(buildEntityTable(rows, filter || ''));
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
browseFilter.addEventListener('input', function () { renderBrowseTable(this.value); });
|
|
685
|
+
document.getElementById('browse-refresh').addEventListener('click', loadBrowse);
|
|
686
|
+
|
|
687
|
+
// ---- Graph tab ----
|
|
688
|
+
var graphLoaded = false;
|
|
689
|
+
|
|
690
|
+
async function loadGraph() {
|
|
691
|
+
if (graphLoaded) return;
|
|
692
|
+
graphLoaded = true;
|
|
693
|
+
var wrap = document.getElementById('graph-svg-wrap');
|
|
694
|
+
showSpinner(wrap);
|
|
695
|
+
try {
|
|
696
|
+
var data = await apiCall('GET', '/v1/graph');
|
|
697
|
+
if (!data.success) throw new Error(data.error || 'Failed to load graph');
|
|
698
|
+
renderGraph(data.data.entities || [], data.data.relations || [], wrap);
|
|
699
|
+
} catch (err) {
|
|
700
|
+
showError(wrap, err.message);
|
|
701
|
+
graphLoaded = false;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
function renderGraph(entities, relations, container) {
|
|
706
|
+
container.textContent = '';
|
|
707
|
+
|
|
708
|
+
if (entities.length === 0) {
|
|
709
|
+
showPlaceholder(container, 'No entities yet. Start remembering to build your graph.');
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
var svgNS = 'http://www.w3.org/2000/svg';
|
|
714
|
+
var svg = document.createElementNS(svgNS, 'svg');
|
|
715
|
+
svg.setAttribute('width', '100%');
|
|
716
|
+
svg.setAttribute('height', '520');
|
|
717
|
+
container.appendChild(svg);
|
|
718
|
+
|
|
719
|
+
var width = container.clientWidth || 900;
|
|
720
|
+
var height = 520;
|
|
721
|
+
|
|
722
|
+
// Only show top connected nodes to keep graph readable
|
|
723
|
+
var MAX_NODES = 60;
|
|
724
|
+
var degreeMap = {};
|
|
725
|
+
relations.forEach(function (r) {
|
|
726
|
+
degreeMap[r.from] = (degreeMap[r.from] || 0) + 1;
|
|
727
|
+
degreeMap[r.to] = (degreeMap[r.to] || 0) + 1;
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
var topNames;
|
|
731
|
+
if (Object.keys(degreeMap).length > 0) {
|
|
732
|
+
topNames = new Set(
|
|
733
|
+
Object.entries(degreeMap)
|
|
734
|
+
.sort(function (a, b) { return b[1] - a[1]; })
|
|
735
|
+
.slice(0, MAX_NODES)
|
|
736
|
+
.map(function (e) { return e[0]; })
|
|
737
|
+
);
|
|
738
|
+
} else {
|
|
739
|
+
topNames = new Set(entities.slice(0, MAX_NODES).map(function (e) { return e.name; }));
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
var graphEntities = entities.filter(function (e) { return topNames.has(e.name); });
|
|
743
|
+
if (graphEntities.length === 0) graphEntities = entities.slice(0, MAX_NODES);
|
|
744
|
+
|
|
745
|
+
var colorPalette = ['#3b82f6','#8b5cf6','#10b981','#f59e0b','#ef4444','#06b6d4','#ec4899','#84cc16','#f97316','#6366f1'];
|
|
746
|
+
var typeColorMap = {};
|
|
747
|
+
var colorIdx = 0;
|
|
748
|
+
graphEntities.forEach(function (e) {
|
|
749
|
+
if (!typeColorMap[e.type]) {
|
|
750
|
+
typeColorMap[e.type] = colorPalette[colorIdx % colorPalette.length];
|
|
751
|
+
colorIdx++;
|
|
752
|
+
}
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
// Build nodes and links
|
|
756
|
+
var nodeMap = {};
|
|
757
|
+
var nodes = graphEntities.map(function (e, i) {
|
|
758
|
+
var firstObs = (e.observations && e.observations.length > 0) ? e.observations[0] : null;
|
|
759
|
+
var n = { id: e.name, type: e.type, status: e.status, obs: e.observations ? e.observations.length : 0, firstObs: firstObs, x: 0, y: 0, vx: 0, vy: 0, fx: null, fy: null };
|
|
760
|
+
nodeMap[e.name] = n;
|
|
761
|
+
return n;
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
var links = relations
|
|
765
|
+
.filter(function (r) { return nodeMap[r.from] && nodeMap[r.to]; })
|
|
766
|
+
.map(function (r) { return { source: r.from, target: r.to, type: r.type }; });
|
|
767
|
+
|
|
768
|
+
// Force simulation using pure JS (no D3 needed for basic layout)
|
|
769
|
+
// Initialize positions in a circle
|
|
770
|
+
nodes.forEach(function (n, i) {
|
|
771
|
+
var angle = (2 * Math.PI * i) / nodes.length;
|
|
772
|
+
var r = Math.min(width, height) * 0.35;
|
|
773
|
+
n.x = width / 2 + r * Math.cos(angle);
|
|
774
|
+
n.y = height / 2 + r * Math.sin(angle);
|
|
775
|
+
});
|
|
776
|
+
|
|
777
|
+
// Create SVG elements using D3 (bundled)
|
|
778
|
+
var d3svg = d3.select(svg);
|
|
779
|
+
d3svg.attr('viewBox', [0, 0, width, height]);
|
|
780
|
+
|
|
781
|
+
// Defs: arrow marker + glow filter + grid pattern
|
|
782
|
+
var defs = d3svg.append('defs');
|
|
783
|
+
|
|
784
|
+
// Arrow marker
|
|
785
|
+
defs.append('marker')
|
|
786
|
+
.attr('id', 'arrow')
|
|
787
|
+
.attr('viewBox', '0 0 10 10').attr('refX', 20).attr('refY', 5)
|
|
788
|
+
.attr('markerWidth', 6).attr('markerHeight', 6).attr('orient', 'auto')
|
|
789
|
+
.append('path').attr('d', 'M 0 0 L 10 5 L 0 10 z').attr('fill', 'rgba(59,130,246,0.4)');
|
|
790
|
+
|
|
791
|
+
// Glow filter
|
|
792
|
+
var filter = defs.append('filter').attr('id', 'node-glow').attr('x', '-50%').attr('y', '-50%').attr('width', '200%').attr('height', '200%');
|
|
793
|
+
filter.append('feGaussianBlur').attr('stdDeviation', '3').attr('result', 'coloredBlur');
|
|
794
|
+
var feMerge = filter.append('feMerge');
|
|
795
|
+
feMerge.append('feMergeNode').attr('in', 'coloredBlur');
|
|
796
|
+
feMerge.append('feMergeNode').attr('in', 'SourceGraphic');
|
|
797
|
+
|
|
798
|
+
// Grid pattern background
|
|
799
|
+
var gridSize = 32;
|
|
800
|
+
var gridPattern = defs.append('pattern')
|
|
801
|
+
.attr('id', 'grid').attr('width', gridSize).attr('height', gridSize)
|
|
802
|
+
.attr('patternUnits', 'userSpaceOnUse');
|
|
803
|
+
gridPattern.append('path')
|
|
804
|
+
.attr('d', 'M ' + gridSize + ' 0 L 0 0 0 ' + gridSize)
|
|
805
|
+
.attr('fill', 'none').attr('stroke', 'rgba(39,39,42,0.4)').attr('stroke-width', '0.5');
|
|
806
|
+
|
|
807
|
+
// Background rect with grid
|
|
808
|
+
d3svg.append('rect')
|
|
809
|
+
.attr('width', width).attr('height', height)
|
|
810
|
+
.attr('fill', 'url(#grid)');
|
|
811
|
+
|
|
812
|
+
var g = d3svg.append('g');
|
|
813
|
+
|
|
814
|
+
d3svg.call(d3.zoom().scaleExtent([0.2, 5]).on('zoom', function (event) {
|
|
815
|
+
g.attr('transform', event.transform);
|
|
816
|
+
}));
|
|
817
|
+
|
|
818
|
+
var linkSel = g.append('g')
|
|
819
|
+
.selectAll('line').data(links).join('line')
|
|
820
|
+
.attr('stroke', 'rgba(59,130,246,0.25)').attr('stroke-width', 1.5)
|
|
821
|
+
.attr('marker-end', 'url(#arrow)');
|
|
822
|
+
|
|
823
|
+
var linkLabel = g.append('g')
|
|
824
|
+
.selectAll('text').data(links).join('text')
|
|
825
|
+
.attr('font-size', 9).attr('fill', 'rgba(113,113,122,0.8)').attr('text-anchor', 'middle')
|
|
826
|
+
.attr('font-family', 'JetBrains Mono, ui-monospace, monospace')
|
|
827
|
+
.text(function (d) { return d.type; });
|
|
828
|
+
|
|
829
|
+
var nodeSel = g.append('g')
|
|
830
|
+
.selectAll('circle').data(nodes).join('circle')
|
|
831
|
+
.attr('r', function (d) { return Math.max(6, Math.min(20, 6 + d.obs * 1.5)); })
|
|
832
|
+
.attr('fill', function (d) { return typeColorMap[d.type] || '#3b82f6'; })
|
|
833
|
+
.attr('stroke', 'rgba(9,9,11,0.8)').attr('stroke-width', 1.5)
|
|
834
|
+
.attr('opacity', function (d) { return d.status === 'archived' ? 0.3 : 0.9; })
|
|
835
|
+
.attr('filter', function (d) { return d.status === 'archived' ? null : 'url(#node-glow)'; })
|
|
836
|
+
.style('cursor', 'pointer')
|
|
837
|
+
.call(d3.drag()
|
|
838
|
+
.on('start', function (event, d) { if (!event.active) sim.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; })
|
|
839
|
+
.on('drag', function (event, d) { d.fx = event.x; d.fy = event.y; })
|
|
840
|
+
.on('end', function (event, d) { if (!event.active) sim.alphaTarget(0); d.fx = null; d.fy = null; }));
|
|
841
|
+
|
|
842
|
+
// Hover pulse effect
|
|
843
|
+
nodeSel.on('mouseenter', function () {
|
|
844
|
+
d3.select(this).attr('stroke', '#3b82f6').attr('stroke-width', 2.5);
|
|
845
|
+
}).on('mouseleave', function () {
|
|
846
|
+
d3.select(this).attr('stroke', 'rgba(9,9,11,0.8)').attr('stroke-width', 1.5);
|
|
847
|
+
});
|
|
848
|
+
|
|
849
|
+
var labelColor = document.body.classList.contains('light') ? '#09090b' : '#fafafa';
|
|
850
|
+
var labelSel = g.append('g')
|
|
851
|
+
.selectAll('text').data(nodes).join('text')
|
|
852
|
+
.attr('font-size', 11).attr('fill', labelColor).attr('dy', 4)
|
|
853
|
+
.attr('font-family', 'Inter, system-ui, sans-serif').attr('font-weight', '500')
|
|
854
|
+
.text(function (d) {
|
|
855
|
+
var label = d.firstObs ? d.firstObs : d.id;
|
|
856
|
+
return label.length > 25 ? label.slice(0, 25) + '\u2026' : label;
|
|
857
|
+
});
|
|
858
|
+
|
|
859
|
+
// Tooltip
|
|
860
|
+
var ttEl = document.getElementById('live-tooltip');
|
|
861
|
+
nodeSel.on('mouseover', function (event, d) {
|
|
862
|
+
ttEl.style.display = 'block';
|
|
863
|
+
var obsPreview = d.firstObs ? (d.firstObs.length > 80 ? d.firstObs.slice(0, 80) + '\u2026' : d.firstObs) : '(no observations)';
|
|
864
|
+
ttEl.querySelector('.tt-name').textContent = obsPreview;
|
|
865
|
+
ttEl.querySelector('.tt-type').textContent = d.type + (d.status === 'archived' ? ' \u2014 archived' : '');
|
|
866
|
+
ttEl.querySelector('.tt-obs').textContent = d.id + ' \u00b7 ' + d.obs + ' observation' + (d.obs !== 1 ? 's' : '');
|
|
867
|
+
}).on('mousemove', function (event) {
|
|
868
|
+
ttEl.style.left = (event.pageX + 14) + 'px';
|
|
869
|
+
ttEl.style.top = (event.pageY - 10) + 'px';
|
|
870
|
+
}).on('mouseout', function () { ttEl.style.display = 'none'; });
|
|
871
|
+
|
|
872
|
+
var sim = d3.forceSimulation(nodes)
|
|
873
|
+
.force('link', d3.forceLink(links).id(function (d) { return d.id; }).distance(150))
|
|
874
|
+
.force('charge', d3.forceManyBody().strength(-400))
|
|
875
|
+
.force('center', d3.forceCenter(width / 2, height / 2))
|
|
876
|
+
.force('collision', d3.forceCollide().radius(22));
|
|
877
|
+
|
|
878
|
+
sim.on('tick', function () {
|
|
879
|
+
linkSel
|
|
880
|
+
.attr('x1', function (d) { return d.source.x; }).attr('y1', function (d) { return d.source.y; })
|
|
881
|
+
.attr('x2', function (d) { return d.target.x; }).attr('y2', function (d) { return d.target.y; });
|
|
882
|
+
linkLabel
|
|
883
|
+
.attr('x', function (d) { return (d.source.x + d.target.x) / 2; })
|
|
884
|
+
.attr('y', function (d) { return (d.source.y + d.target.y) / 2; });
|
|
885
|
+
nodeSel.attr('cx', function (d) { return d.x; }).attr('cy', function (d) { return d.y; });
|
|
886
|
+
labelSel.attr('x', function (d) { return d.x + 10; }).attr('y', function (d) { return d.y; });
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
// Legend
|
|
890
|
+
var legend = document.createElement('div');
|
|
891
|
+
legend.style.cssText = 'display:flex;flex-wrap:wrap;gap:10px;margin-top:12px;font-size:12px;padding:0 2px;font-family:Inter,system-ui,sans-serif;';
|
|
892
|
+
Object.entries(typeColorMap).forEach(function (pair) {
|
|
893
|
+
var item = document.createElement('span');
|
|
894
|
+
item.style.cssText = 'display:flex;align-items:center;gap:5px;color:var(--text-secondary,#a1a1aa);';
|
|
895
|
+
var dot = document.createElement('span');
|
|
896
|
+
dot.style.cssText = 'width:8px;height:8px;border-radius:50%;display:inline-block;background:' + pair[1] + ';box-shadow:0 0 4px ' + pair[1] + '66;';
|
|
897
|
+
var lbl = document.createElement('span');
|
|
898
|
+
lbl.textContent = pair[0];
|
|
899
|
+
item.appendChild(dot);
|
|
900
|
+
item.appendChild(lbl);
|
|
901
|
+
legend.appendChild(item);
|
|
902
|
+
});
|
|
903
|
+
container.appendChild(legend);
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
// ---- Analytics tab ----
|
|
907
|
+
var analyticsLoaded = false;
|
|
908
|
+
|
|
909
|
+
async function loadAnalytics() {
|
|
910
|
+
if (analyticsLoaded) return;
|
|
911
|
+
analyticsLoaded = true;
|
|
912
|
+
var container = document.getElementById('analytics-body');
|
|
913
|
+
showSpinner(container);
|
|
914
|
+
try {
|
|
915
|
+
var statsRes = await apiCall('GET', '/v1/stats');
|
|
916
|
+
if (!statsRes.success) throw new Error(statsRes.error || 'Failed to load stats');
|
|
917
|
+
// Also fetch entities for actionable insight calculations
|
|
918
|
+
var entitiesRes = await apiCall('GET', '/v1/entities?limit=500&status=all');
|
|
919
|
+
var allEntitiesForAnalytics = (entitiesRes.success && entitiesRes.data) ? entitiesRes.data : [];
|
|
920
|
+
renderAnalytics(statsRes.data, allEntitiesForAnalytics, container);
|
|
921
|
+
} catch (err) {
|
|
922
|
+
showError(container, err.message);
|
|
923
|
+
analyticsLoaded = false;
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
function renderAnalytics(stats, entities, container) {
|
|
928
|
+
container.textContent = '';
|
|
929
|
+
|
|
930
|
+
// Stats grid
|
|
931
|
+
var grid = document.createElement('div');
|
|
932
|
+
grid.className = 'stats-grid';
|
|
933
|
+
[
|
|
934
|
+
{ label: 'Total Memories', value: stats.totalEntities },
|
|
935
|
+
{ label: 'Knowledge Facts', value: stats.totalObservations },
|
|
936
|
+
{ label: 'Connections', value: stats.totalRelations },
|
|
937
|
+
{ label: 'Topics', value: stats.totalTags },
|
|
938
|
+
].forEach(function (c) {
|
|
939
|
+
var card = document.createElement('div');
|
|
940
|
+
card.className = 'stat-card';
|
|
941
|
+
var val = document.createElement('div');
|
|
942
|
+
val.className = 'stat-value';
|
|
943
|
+
val.textContent = String(c.value);
|
|
944
|
+
var lbl = document.createElement('div');
|
|
945
|
+
lbl.className = 'stat-label';
|
|
946
|
+
lbl.textContent = c.label;
|
|
947
|
+
card.appendChild(val);
|
|
948
|
+
card.appendChild(lbl);
|
|
949
|
+
grid.appendChild(card);
|
|
950
|
+
});
|
|
951
|
+
container.appendChild(grid);
|
|
952
|
+
|
|
953
|
+
// Status distribution
|
|
954
|
+
if (stats.statusDistribution && stats.statusDistribution.length > 0) {
|
|
955
|
+
var statusTitle = document.createElement('h3');
|
|
956
|
+
statusTitle.style.cssText = 'font-size:11px;font-weight:600;margin-bottom:10px;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.05em;';
|
|
957
|
+
statusTitle.textContent = 'Status Distribution';
|
|
958
|
+
container.appendChild(statusTitle);
|
|
959
|
+
var statusGrid = document.createElement('div');
|
|
960
|
+
statusGrid.className = 'stats-grid';
|
|
961
|
+
stats.statusDistribution.forEach(function (s) {
|
|
962
|
+
var card = document.createElement('div');
|
|
963
|
+
card.className = 'stat-card';
|
|
964
|
+
var val = document.createElement('div');
|
|
965
|
+
val.className = 'stat-value';
|
|
966
|
+
val.textContent = String(s.count);
|
|
967
|
+
var lbl = document.createElement('div');
|
|
968
|
+
lbl.className = 'stat-label';
|
|
969
|
+
lbl.textContent = s.status || 'active';
|
|
970
|
+
card.appendChild(val);
|
|
971
|
+
card.appendChild(lbl);
|
|
972
|
+
statusGrid.appendChild(card);
|
|
973
|
+
});
|
|
974
|
+
container.appendChild(statusGrid);
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
// Actionable insights section
|
|
978
|
+
if (entities && entities.length > 0) {
|
|
979
|
+
var insightsTitle = document.createElement('h3');
|
|
980
|
+
insightsTitle.style.cssText = 'font-size:11px;font-weight:600;margin:16px 0 10px;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.05em;';
|
|
981
|
+
insightsTitle.textContent = 'Recent Activity';
|
|
982
|
+
container.appendChild(insightsTitle);
|
|
983
|
+
|
|
984
|
+
var now = Date.now();
|
|
985
|
+
var sevenDays = 7 * 24 * 60 * 60 * 1000;
|
|
986
|
+
var thirtyDays = 30 * 24 * 60 * 60 * 1000;
|
|
987
|
+
|
|
988
|
+
// Count this week
|
|
989
|
+
var thisWeek = entities.filter(function (e) {
|
|
990
|
+
try { return (now - new Date(e.created_at).getTime()) < sevenDays; } catch (_) { return false; }
|
|
991
|
+
}).length;
|
|
992
|
+
|
|
993
|
+
// Stale: not accessed in 30+ days (or never accessed)
|
|
994
|
+
var stale = entities.filter(function (e) {
|
|
995
|
+
if (!e.last_accessed_at) return true;
|
|
996
|
+
try { return (now - new Date(e.last_accessed_at).getTime()) > thirtyDays; } catch (_) { return true; }
|
|
997
|
+
}).length;
|
|
998
|
+
|
|
999
|
+
// Archived count
|
|
1000
|
+
var archived = entities.filter(function (e) {
|
|
1001
|
+
return e.status === 'archived' || e.archived === true;
|
|
1002
|
+
}).length;
|
|
1003
|
+
|
|
1004
|
+
// Most recalled (top 1)
|
|
1005
|
+
var topRecalled = entities
|
|
1006
|
+
.filter(function (e) { return e.access_count && e.access_count > 0; })
|
|
1007
|
+
.sort(function (a, b) { return (b.access_count || 0) - (a.access_count || 0); })
|
|
1008
|
+
.slice(0, 1);
|
|
1009
|
+
|
|
1010
|
+
var insightCards = [
|
|
1011
|
+
{ icon: '\ud83d\udcdd', label: 'This Week', value: thisWeek + ' new memories' },
|
|
1012
|
+
{ icon: '\ud83d\udca4', label: 'Stale Memories', value: stale + ' not accessed in 30+ days' },
|
|
1013
|
+
{ icon: '\ud83d\udce6', label: 'Archived', value: archived + ' memories' },
|
|
1014
|
+
];
|
|
1015
|
+
if (topRecalled.length > 0) {
|
|
1016
|
+
var tr = topRecalled[0];
|
|
1017
|
+
var trPreview = (tr.observations && tr.observations[0])
|
|
1018
|
+
? (tr.observations[0].length > 40 ? tr.observations[0].slice(0, 40) + '\u2026' : tr.observations[0])
|
|
1019
|
+
: tr.name;
|
|
1020
|
+
insightCards.unshift({ icon: '\ud83d\udd25', label: 'Most Recalled', value: '\u201c' + trPreview + '\u201d (' + tr.access_count + 'x)' });
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
var insightGrid = document.createElement('div');
|
|
1024
|
+
insightGrid.style.cssText = 'display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:10px;margin-bottom:20px;';
|
|
1025
|
+
insightCards.forEach(function (ic) {
|
|
1026
|
+
var card = document.createElement('div');
|
|
1027
|
+
card.style.cssText = 'background:var(--bg-secondary);border:1px solid var(--border);border-radius:var(--radius-sm);padding:14px 16px;font-size:13px;';
|
|
1028
|
+
var iconRow = document.createElement('div');
|
|
1029
|
+
iconRow.style.cssText = 'font-size:18px;margin-bottom:6px;';
|
|
1030
|
+
iconRow.textContent = ic.icon;
|
|
1031
|
+
var labelEl = document.createElement('div');
|
|
1032
|
+
labelEl.style.cssText = 'font-size:11px;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.04em;margin-bottom:2px;';
|
|
1033
|
+
labelEl.textContent = ic.label;
|
|
1034
|
+
var valueEl = document.createElement('div');
|
|
1035
|
+
valueEl.style.cssText = 'color:var(--text-primary);font-weight:500;font-size:13px;';
|
|
1036
|
+
valueEl.textContent = ic.value;
|
|
1037
|
+
card.appendChild(iconRow);
|
|
1038
|
+
card.appendChild(labelEl);
|
|
1039
|
+
card.appendChild(valueEl);
|
|
1040
|
+
insightGrid.appendChild(card);
|
|
1041
|
+
});
|
|
1042
|
+
container.appendChild(insightGrid);
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
// Type distribution bar chart
|
|
1046
|
+
if (stats.typeDistribution && stats.typeDistribution.length > 0) {
|
|
1047
|
+
var typeTitle = document.createElement('h3');
|
|
1048
|
+
typeTitle.style.cssText = 'font-size:11px;font-weight:600;margin:16px 0 10px;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.05em;';
|
|
1049
|
+
typeTitle.textContent = 'Memory Types';
|
|
1050
|
+
container.appendChild(typeTitle);
|
|
1051
|
+
|
|
1052
|
+
var maxCount = stats.typeDistribution[0].count || 1;
|
|
1053
|
+
var barChart = document.createElement('div');
|
|
1054
|
+
barChart.className = 'bar-chart';
|
|
1055
|
+
stats.typeDistribution.forEach(function (t) {
|
|
1056
|
+
var row = document.createElement('div');
|
|
1057
|
+
row.className = 'bar-row';
|
|
1058
|
+
var lbl = document.createElement('div');
|
|
1059
|
+
lbl.className = 'bar-label';
|
|
1060
|
+
lbl.textContent = t.type;
|
|
1061
|
+
lbl.title = t.type;
|
|
1062
|
+
var track = document.createElement('div');
|
|
1063
|
+
track.className = 'bar-track';
|
|
1064
|
+
var fill = document.createElement('div');
|
|
1065
|
+
fill.className = 'bar-fill';
|
|
1066
|
+
fill.style.width = Math.round((t.count / maxCount) * 100) + '%';
|
|
1067
|
+
track.appendChild(fill);
|
|
1068
|
+
var cnt = document.createElement('div');
|
|
1069
|
+
cnt.className = 'bar-count';
|
|
1070
|
+
cnt.textContent = String(t.count);
|
|
1071
|
+
row.appendChild(lbl);
|
|
1072
|
+
row.appendChild(track);
|
|
1073
|
+
row.appendChild(cnt);
|
|
1074
|
+
barChart.appendChild(row);
|
|
1075
|
+
});
|
|
1076
|
+
container.appendChild(barChart);
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
// Tag cloud
|
|
1080
|
+
if (stats.tagDistribution && stats.tagDistribution.length > 0) {
|
|
1081
|
+
var tagTitle = document.createElement('h3');
|
|
1082
|
+
tagTitle.style.cssText = 'font-size:11px;font-weight:600;margin:16px 0 10px;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.05em;';
|
|
1083
|
+
tagTitle.textContent = 'Topics';
|
|
1084
|
+
container.appendChild(tagTitle);
|
|
1085
|
+
|
|
1086
|
+
var maxTagCount = stats.tagDistribution[0].count || 1;
|
|
1087
|
+
var tagCloud = document.createElement('div');
|
|
1088
|
+
tagCloud.className = 'tag-cloud';
|
|
1089
|
+
stats.tagDistribution.forEach(function (t) {
|
|
1090
|
+
var pill = document.createElement('span');
|
|
1091
|
+
pill.className = 'tag-pill';
|
|
1092
|
+
var scale = 0.8 + (t.count / maxTagCount) * 0.9;
|
|
1093
|
+
pill.style.fontSize = Math.round(scale * 13) + 'px';
|
|
1094
|
+
pill.title = t.count + ' entities';
|
|
1095
|
+
pill.textContent = t.tag;
|
|
1096
|
+
tagCloud.appendChild(pill);
|
|
1097
|
+
});
|
|
1098
|
+
container.appendChild(tagCloud);
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
if (!stats.typeDistribution || stats.typeDistribution.length === 0) {
|
|
1102
|
+
showPlaceholder(container, 'No data yet. Start adding entities.');
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
// ---- Timeline tab ----
|
|
1107
|
+
var timelineLoaded = false;
|
|
1108
|
+
|
|
1109
|
+
async function loadTimeline() {
|
|
1110
|
+
if (timelineLoaded) return;
|
|
1111
|
+
timelineLoaded = true;
|
|
1112
|
+
var container = document.getElementById('timeline-body');
|
|
1113
|
+
showSpinner(container);
|
|
1114
|
+
try {
|
|
1115
|
+
var data = await apiCall('GET', '/v1/graph');
|
|
1116
|
+
if (!data.success) throw new Error(data.error || 'Failed to load graph');
|
|
1117
|
+
renderTimeline(data.data.entities || [], data.data.relations || [], container);
|
|
1118
|
+
} catch (err) {
|
|
1119
|
+
showError(container, err.message);
|
|
1120
|
+
timelineLoaded = false;
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
function renderTimeline(entities, relations, container) {
|
|
1125
|
+
container.textContent = '';
|
|
1126
|
+
|
|
1127
|
+
// Build entity map
|
|
1128
|
+
var entityMap = {};
|
|
1129
|
+
entities.forEach(function (e) { entityMap[e.name] = e; });
|
|
1130
|
+
|
|
1131
|
+
// Find supersedes relations
|
|
1132
|
+
var supersedes = relations.filter(function (r) { return r.type === 'supersedes'; });
|
|
1133
|
+
|
|
1134
|
+
if (supersedes.length === 0) {
|
|
1135
|
+
showPlaceholder(container, 'No evolution chains yet. Use "supersedes" relations to track knowledge evolution.');
|
|
1136
|
+
return;
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
// Build chains: find roots (nodes that are not a "to" in any supersedes)
|
|
1140
|
+
var hasIncoming = new Set(supersedes.map(function (r) { return r.to; }));
|
|
1141
|
+
var roots = [...new Set(supersedes.map(function (r) { return r.from; }))].filter(function (name) {
|
|
1142
|
+
return !hasIncoming.has(name);
|
|
1143
|
+
});
|
|
1144
|
+
|
|
1145
|
+
// If no pure roots, just use all froms
|
|
1146
|
+
if (roots.length === 0) {
|
|
1147
|
+
roots = [...new Set(supersedes.map(function (r) { return r.from; }))];
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
// Build adjacency: from -> [to, ...]
|
|
1151
|
+
var nextMap = {};
|
|
1152
|
+
supersedes.forEach(function (r) {
|
|
1153
|
+
if (!nextMap[r.from]) nextMap[r.from] = [];
|
|
1154
|
+
nextMap[r.from].push(r.to);
|
|
1155
|
+
});
|
|
1156
|
+
|
|
1157
|
+
// Walk each chain
|
|
1158
|
+
var visited = new Set();
|
|
1159
|
+
var chainCount = 0;
|
|
1160
|
+
|
|
1161
|
+
roots.forEach(function (root) {
|
|
1162
|
+
if (visited.has(root)) return;
|
|
1163
|
+
var chain = [];
|
|
1164
|
+
var cursor = root;
|
|
1165
|
+
var safety = 0;
|
|
1166
|
+
while (cursor && !visited.has(cursor) && safety < 50) {
|
|
1167
|
+
visited.add(cursor);
|
|
1168
|
+
chain.push(cursor);
|
|
1169
|
+
var nexts = nextMap[cursor];
|
|
1170
|
+
cursor = nexts && nexts[0];
|
|
1171
|
+
safety++;
|
|
1172
|
+
}
|
|
1173
|
+
if (chain.length < 2) return;
|
|
1174
|
+
chainCount++;
|
|
1175
|
+
|
|
1176
|
+
var chainEl = document.createElement('div');
|
|
1177
|
+
chainEl.className = 'chain';
|
|
1178
|
+
|
|
1179
|
+
chain.forEach(function (name, idx) {
|
|
1180
|
+
if (idx > 0) {
|
|
1181
|
+
var arrow = document.createElement('span');
|
|
1182
|
+
arrow.className = 'chain-arrow';
|
|
1183
|
+
arrow.textContent = '\\u2192';
|
|
1184
|
+
chainEl.appendChild(arrow);
|
|
1185
|
+
}
|
|
1186
|
+
var node = document.createElement('div');
|
|
1187
|
+
var ent = entityMap[name];
|
|
1188
|
+
node.className = 'chain-node' + (ent && ent.status === 'archived' ? ' archived' : '');
|
|
1189
|
+
var nameEl = document.createElement('div');
|
|
1190
|
+
nameEl.className = 'cn-name';
|
|
1191
|
+
// Show first observation as primary content, fall back to entity name
|
|
1192
|
+
var obsContent = (ent && ent.observations && ent.observations[0])
|
|
1193
|
+
? ent.observations[0]
|
|
1194
|
+
: name;
|
|
1195
|
+
nameEl.textContent = obsContent.length > 60 ? obsContent.slice(0, 60) + '\u2026' : obsContent;
|
|
1196
|
+
nameEl.title = obsContent; // full text on hover
|
|
1197
|
+
var typeEl = document.createElement('div');
|
|
1198
|
+
typeEl.className = 'cn-type';
|
|
1199
|
+
typeEl.textContent = (ent ? ent.type : '') + (ent ? ' \u00b7 ' : '');
|
|
1200
|
+
// Add timestamp
|
|
1201
|
+
if (ent && ent.created_at) {
|
|
1202
|
+
try {
|
|
1203
|
+
var tDate = new Date(ent.created_at);
|
|
1204
|
+
var timeSpan = document.createElement('span');
|
|
1205
|
+
timeSpan.style.cssText = 'font-size:11px;color:var(--text-muted);font-family:var(--font-mono);';
|
|
1206
|
+
timeSpan.textContent = tDate.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' });
|
|
1207
|
+
typeEl.textContent = (ent ? ent.type + ' \u00b7 ' : '');
|
|
1208
|
+
typeEl.appendChild(timeSpan);
|
|
1209
|
+
} catch (_) {}
|
|
1210
|
+
}
|
|
1211
|
+
// Add entity name as secondary metadata
|
|
1212
|
+
var entityNameEl = document.createElement('div');
|
|
1213
|
+
entityNameEl.style.cssText = 'font-size:10px;color:var(--text-muted);font-family:var(--font-mono);opacity:0.7;margin-top:2px;';
|
|
1214
|
+
entityNameEl.textContent = name;
|
|
1215
|
+
node.appendChild(nameEl);
|
|
1216
|
+
node.appendChild(typeEl);
|
|
1217
|
+
node.appendChild(entityNameEl);
|
|
1218
|
+
chainEl.appendChild(node);
|
|
1219
|
+
});
|
|
1220
|
+
|
|
1221
|
+
container.appendChild(chainEl);
|
|
1222
|
+
});
|
|
1223
|
+
|
|
1224
|
+
if (chainCount === 0) {
|
|
1225
|
+
showPlaceholder(container, 'No multi-step evolution chains found.');
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
// ---- Manage tab ----
|
|
1230
|
+
var allManageEntities = [];
|
|
1231
|
+
var manageFilter = '';
|
|
1232
|
+
|
|
1233
|
+
async function loadManage() {
|
|
1234
|
+
var tableWrap = document.getElementById('manage-table-wrap');
|
|
1235
|
+
if (!tableWrap) return;
|
|
1236
|
+
showSpinner(tableWrap);
|
|
1237
|
+
try {
|
|
1238
|
+
var data = await apiCall('GET', '/v1/entities?limit=500&status=all');
|
|
1239
|
+
if (!data.success) throw new Error(data.error || 'Failed to load');
|
|
1240
|
+
allManageEntities = data.data || [];
|
|
1241
|
+
renderManageTable();
|
|
1242
|
+
} catch (err) {
|
|
1243
|
+
showError(tableWrap, err.message);
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
function renderManageTable() {
|
|
1248
|
+
var filterInput = document.getElementById('manage-filter');
|
|
1249
|
+
var f = (filterInput ? filterInput.value : manageFilter).toLowerCase();
|
|
1250
|
+
var rows = allManageEntities.filter(function (e) {
|
|
1251
|
+
return !f || e.name.toLowerCase().includes(f) || e.type.toLowerCase().includes(f);
|
|
1252
|
+
});
|
|
1253
|
+
|
|
1254
|
+
var tableWrap = document.getElementById('manage-table-wrap');
|
|
1255
|
+
if (!tableWrap) return;
|
|
1256
|
+
tableWrap.textContent = '';
|
|
1257
|
+
|
|
1258
|
+
if (rows.length === 0) {
|
|
1259
|
+
showPlaceholder(tableWrap, 'No entities found');
|
|
1260
|
+
return;
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
var table = document.createElement('table');
|
|
1264
|
+
var thead = document.createElement('thead');
|
|
1265
|
+
var hrow = document.createElement('tr');
|
|
1266
|
+
[['Time', '140px'], ['Memory', '40%'], ['Type', ''], ['Status', ''], ['Tags', ''], ['Actions', '']].forEach(function (hDef) {
|
|
1267
|
+
var th = document.createElement('th');
|
|
1268
|
+
th.textContent = hDef[0];
|
|
1269
|
+
if (hDef[1]) th.style.width = hDef[1];
|
|
1270
|
+
hrow.appendChild(th);
|
|
1271
|
+
});
|
|
1272
|
+
thead.appendChild(hrow);
|
|
1273
|
+
table.appendChild(thead);
|
|
1274
|
+
|
|
1275
|
+
var tbody = document.createElement('tbody');
|
|
1276
|
+
rows.forEach(function (e) {
|
|
1277
|
+
var status = e.status || 'active';
|
|
1278
|
+
var tr = document.createElement('tr');
|
|
1279
|
+
if (status === 'archived') tr.style.opacity = '0.6';
|
|
1280
|
+
|
|
1281
|
+
// Time column
|
|
1282
|
+
var tdTime = document.createElement('td');
|
|
1283
|
+
tdTime.style.cssText = 'font-family:var(--font-mono);font-size:11px;color:var(--text-muted);white-space:nowrap;';
|
|
1284
|
+
try {
|
|
1285
|
+
var d = new Date(e.created_at);
|
|
1286
|
+
tdTime.textContent = d.toLocaleDateString(undefined, { month: 'short', day: 'numeric' }) + ' ' + d.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' });
|
|
1287
|
+
} catch (_) {
|
|
1288
|
+
tdTime.textContent = e.created_at || '\u2014';
|
|
1289
|
+
}
|
|
1290
|
+
tdTime.title = e.created_at || '';
|
|
1291
|
+
tr.appendChild(tdTime);
|
|
1292
|
+
|
|
1293
|
+
// Memory column — first observation as primary, entity name as metadata
|
|
1294
|
+
var tdMemory = document.createElement('td');
|
|
1295
|
+
var memContent = document.createElement('div');
|
|
1296
|
+
var preview = document.createElement('div');
|
|
1297
|
+
preview.style.cssText = 'font-size:13px;line-height:1.5;margin-bottom:2px;';
|
|
1298
|
+
var firstObs = (e.observations && e.observations.length > 0) ? e.observations[0] : '(no observations)';
|
|
1299
|
+
preview.textContent = firstObs.length > 100 ? firstObs.slice(0, 100) + '\u2026' : firstObs;
|
|
1300
|
+
memContent.appendChild(preview);
|
|
1301
|
+
var meta = document.createElement('div');
|
|
1302
|
+
meta.style.cssText = 'font-size:11px;color:var(--text-muted);font-family:var(--font-mono);';
|
|
1303
|
+
var obsCount = e.observations ? e.observations.length : 0;
|
|
1304
|
+
meta.textContent = e.name + (obsCount > 1 ? ' \u00b7 ' + obsCount + ' observations' : '');
|
|
1305
|
+
memContent.appendChild(meta);
|
|
1306
|
+
tdMemory.appendChild(memContent);
|
|
1307
|
+
tr.appendChild(tdMemory);
|
|
1308
|
+
|
|
1309
|
+
var tdType = document.createElement('td');
|
|
1310
|
+
var typeBadge = document.createElement('span');
|
|
1311
|
+
typeBadge.className = 'badge badge-type';
|
|
1312
|
+
typeBadge.textContent = e.type;
|
|
1313
|
+
tdType.appendChild(typeBadge);
|
|
1314
|
+
tr.appendChild(tdType);
|
|
1315
|
+
|
|
1316
|
+
var tdStatus = document.createElement('td');
|
|
1317
|
+
var sb = document.createElement('span');
|
|
1318
|
+
sb.className = 'badge badge-' + (status === 'archived' ? 'archived' : 'active');
|
|
1319
|
+
sb.textContent = status;
|
|
1320
|
+
tdStatus.appendChild(sb);
|
|
1321
|
+
tr.appendChild(tdStatus);
|
|
1322
|
+
|
|
1323
|
+
var tdTags = document.createElement('td');
|
|
1324
|
+
if (e.tags && e.tags.length > 0) {
|
|
1325
|
+
e.tags.slice(0, 3).forEach(function (t) {
|
|
1326
|
+
var pill = document.createElement('span');
|
|
1327
|
+
pill.className = 'tag-pill';
|
|
1328
|
+
pill.textContent = t;
|
|
1329
|
+
tdTags.appendChild(pill);
|
|
1330
|
+
});
|
|
1331
|
+
if (e.tags.length > 3) {
|
|
1332
|
+
var moreTag = document.createElement('span');
|
|
1333
|
+
moreTag.className = 'tag-pill';
|
|
1334
|
+
moreTag.style.opacity = '0.6';
|
|
1335
|
+
moreTag.textContent = '+' + (e.tags.length - 3);
|
|
1336
|
+
tdTags.appendChild(moreTag);
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
tr.appendChild(tdTags);
|
|
1340
|
+
|
|
1341
|
+
var tdActions = document.createElement('td');
|
|
1342
|
+
var actCell = document.createElement('div');
|
|
1343
|
+
actCell.className = 'manage-action-cell';
|
|
1344
|
+
|
|
1345
|
+
if (status === 'archived') {
|
|
1346
|
+
var restoreBtn = document.createElement('button');
|
|
1347
|
+
restoreBtn.className = 'btn btn-sm btn-secondary';
|
|
1348
|
+
restoreBtn.textContent = 'Restore';
|
|
1349
|
+
restoreBtn.addEventListener('click', function () {
|
|
1350
|
+
restoreBtn.disabled = true;
|
|
1351
|
+
restoreBtn.textContent = '...';
|
|
1352
|
+
apiCall('POST', '/v1/remember', { name: e.name, type: e.type, observations: [], tags: [] })
|
|
1353
|
+
.then(function () { loadManage(); })
|
|
1354
|
+
.catch(function (err) {
|
|
1355
|
+
restoreBtn.disabled = false;
|
|
1356
|
+
restoreBtn.textContent = 'Restore';
|
|
1357
|
+
var errEl = document.createElement('div');
|
|
1358
|
+
errEl.style.cssText = 'color:var(--danger);padding:8px 12px;background:rgba(239,68,68,0.08);border:1px solid rgba(239,68,68,0.2);border-radius:var(--radius-xs);margin-top:8px;font-size:13px;';
|
|
1359
|
+
errEl.textContent = 'Error: ' + err.message;
|
|
1360
|
+
actCell.appendChild(errEl);
|
|
1361
|
+
setTimeout(function() { errEl.remove(); }, 5000);
|
|
1362
|
+
});
|
|
1363
|
+
});
|
|
1364
|
+
actCell.appendChild(restoreBtn);
|
|
1365
|
+
} else {
|
|
1366
|
+
var archiveBtn = document.createElement('button');
|
|
1367
|
+
archiveBtn.className = 'btn btn-sm btn-danger';
|
|
1368
|
+
archiveBtn.textContent = 'Archive';
|
|
1369
|
+
archiveBtn.addEventListener('click', function () {
|
|
1370
|
+
if (!confirm('Archive entity "' + e.name + '"? It will be hidden but not deleted.')) return;
|
|
1371
|
+
archiveBtn.disabled = true;
|
|
1372
|
+
archiveBtn.textContent = '...';
|
|
1373
|
+
apiCall('POST', '/v1/forget', { name: e.name })
|
|
1374
|
+
.then(function () { loadManage(); })
|
|
1375
|
+
.catch(function (err) {
|
|
1376
|
+
archiveBtn.disabled = false;
|
|
1377
|
+
archiveBtn.textContent = 'Archive';
|
|
1378
|
+
var errEl = document.createElement('div');
|
|
1379
|
+
errEl.style.cssText = 'color:var(--danger);padding:8px 12px;background:rgba(239,68,68,0.08);border:1px solid rgba(239,68,68,0.2);border-radius:var(--radius-xs);margin-top:8px;font-size:13px;';
|
|
1380
|
+
errEl.textContent = 'Error: ' + err.message;
|
|
1381
|
+
actCell.appendChild(errEl);
|
|
1382
|
+
setTimeout(function() { errEl.remove(); }, 5000);
|
|
1383
|
+
});
|
|
1384
|
+
});
|
|
1385
|
+
actCell.appendChild(archiveBtn);
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
// Remove observation button (only if entity has observations)
|
|
1389
|
+
if (e.observations && e.observations.length > 0) {
|
|
1390
|
+
var rmObsBtn = document.createElement('button');
|
|
1391
|
+
rmObsBtn.className = 'btn btn-sm btn-secondary';
|
|
1392
|
+
rmObsBtn.textContent = 'Remove obs';
|
|
1393
|
+
rmObsBtn.addEventListener('click', function () {
|
|
1394
|
+
var modal = document.createElement('div');
|
|
1395
|
+
modal.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.7);display:flex;align-items:center;justify-content:center;z-index:1000;backdrop-filter:blur(4px);';
|
|
1396
|
+
var box = document.createElement('div');
|
|
1397
|
+
box.className = 'card';
|
|
1398
|
+
box.style.cssText = 'max-width:500px;width:90%;max-height:80vh;overflow-y:auto;';
|
|
1399
|
+
var title = document.createElement('h3');
|
|
1400
|
+
title.textContent = 'Remove Observation';
|
|
1401
|
+
title.style.cssText = 'margin-bottom:12px;font-size:14px;font-weight:600;color:var(--text-primary);';
|
|
1402
|
+
box.appendChild(title);
|
|
1403
|
+
e.observations.forEach(function(obsText) {
|
|
1404
|
+
var label = document.createElement('label');
|
|
1405
|
+
label.style.cssText = 'display:block;padding:8px 10px;margin:4px 0;border:1px solid var(--border);border-radius:var(--radius-xs);cursor:pointer;font-size:13px;color:var(--text-secondary);transition:border-color 0.15s;';
|
|
1406
|
+
var radio = document.createElement('input');
|
|
1407
|
+
radio.type = 'radio';
|
|
1408
|
+
radio.name = 'obs-select-' + e.name;
|
|
1409
|
+
radio.value = obsText;
|
|
1410
|
+
radio.style.marginRight = '8px';
|
|
1411
|
+
radio.style.accentColor = 'var(--accent)';
|
|
1412
|
+
label.appendChild(radio);
|
|
1413
|
+
label.appendChild(document.createTextNode(obsText.length > 120 ? obsText.slice(0, 120) + '...' : obsText));
|
|
1414
|
+
box.appendChild(label);
|
|
1415
|
+
});
|
|
1416
|
+
var modalErrEl = document.createElement('div');
|
|
1417
|
+
modalErrEl.style.cssText = 'color:var(--danger);font-size:13px;min-height:18px;margin-top:8px;';
|
|
1418
|
+
box.appendChild(modalErrEl);
|
|
1419
|
+
var btnRow = document.createElement('div');
|
|
1420
|
+
btnRow.style.cssText = 'display:flex;gap:8px;margin-top:12px;justify-content:flex-end;';
|
|
1421
|
+
var cancelBtn = document.createElement('button');
|
|
1422
|
+
cancelBtn.className = 'btn btn-secondary';
|
|
1423
|
+
cancelBtn.textContent = 'Cancel';
|
|
1424
|
+
cancelBtn.addEventListener('click', function() { modal.remove(); });
|
|
1425
|
+
btnRow.appendChild(cancelBtn);
|
|
1426
|
+
var removeBtn = document.createElement('button');
|
|
1427
|
+
removeBtn.className = 'btn btn-danger';
|
|
1428
|
+
removeBtn.textContent = 'Remove Selected';
|
|
1429
|
+
removeBtn.addEventListener('click', function() {
|
|
1430
|
+
var selected = box.querySelector('input[name="obs-select-' + e.name + '"]:checked');
|
|
1431
|
+
if (!selected) { modalErrEl.textContent = 'Select an observation first.'; return; }
|
|
1432
|
+
removeBtn.disabled = true;
|
|
1433
|
+
removeBtn.textContent = '...';
|
|
1434
|
+
apiCall('POST', '/v1/forget', { name: e.name, observation: selected.value })
|
|
1435
|
+
.then(function() { modal.remove(); loadManage(); })
|
|
1436
|
+
.catch(function(err) {
|
|
1437
|
+
removeBtn.disabled = false;
|
|
1438
|
+
removeBtn.textContent = 'Remove Selected';
|
|
1439
|
+
modalErrEl.textContent = 'Error: ' + err.message;
|
|
1440
|
+
});
|
|
1441
|
+
});
|
|
1442
|
+
btnRow.appendChild(removeBtn);
|
|
1443
|
+
box.appendChild(btnRow);
|
|
1444
|
+
modal.appendChild(box);
|
|
1445
|
+
modal.addEventListener('click', function(ev) { if (ev.target === modal) modal.remove(); });
|
|
1446
|
+
document.body.appendChild(modal);
|
|
1447
|
+
});
|
|
1448
|
+
actCell.appendChild(rmObsBtn);
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
tdActions.appendChild(actCell);
|
|
1452
|
+
tr.appendChild(tdActions);
|
|
1453
|
+
tbody.appendChild(tr);
|
|
1454
|
+
});
|
|
1455
|
+
|
|
1456
|
+
table.appendChild(tbody);
|
|
1457
|
+
var manageTableOuter = document.createElement('div');
|
|
1458
|
+
manageTableOuter.className = 'table-wrap';
|
|
1459
|
+
manageTableOuter.appendChild(table);
|
|
1460
|
+
tableWrap.appendChild(manageTableOuter);
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
document.getElementById('manage-filter').addEventListener('input', function () { renderManageTable(); });
|
|
1464
|
+
document.getElementById('manage-refresh').addEventListener('click', loadManage);
|
|
1465
|
+
|
|
1466
|
+
// ---- Feedback widget ----
|
|
1467
|
+
(function initFeedback() {
|
|
1468
|
+
var btn = document.getElementById('feedback-btn');
|
|
1469
|
+
var panel = document.getElementById('feedback-panel');
|
|
1470
|
+
if (!btn || !panel) return;
|
|
1471
|
+
|
|
1472
|
+
var fbType = 'bug';
|
|
1473
|
+
|
|
1474
|
+
btn.addEventListener('click', function () {
|
|
1475
|
+
panel.classList.toggle('open');
|
|
1476
|
+
});
|
|
1477
|
+
|
|
1478
|
+
// Radio group selection
|
|
1479
|
+
panel.querySelectorAll('.fb-radio input').forEach(function (radio) {
|
|
1480
|
+
radio.addEventListener('change', function () {
|
|
1481
|
+
panel.querySelectorAll('.fb-radio').forEach(function (el) { el.classList.remove('selected'); });
|
|
1482
|
+
radio.parentElement.classList.add('selected');
|
|
1483
|
+
fbType = radio.value;
|
|
1484
|
+
});
|
|
1485
|
+
});
|
|
1486
|
+
|
|
1487
|
+
var submitBtn = document.getElementById('fb-submit');
|
|
1488
|
+
if (submitBtn) {
|
|
1489
|
+
submitBtn.addEventListener('click', function () {
|
|
1490
|
+
var desc = document.getElementById('fb-desc').value.trim();
|
|
1491
|
+
if (!desc) { document.getElementById('fb-desc').focus(); return; }
|
|
1492
|
+
var includeSys = document.getElementById('fb-sys').checked;
|
|
1493
|
+
var labels = 'feedback,from-dashboard,' + fbType;
|
|
1494
|
+
var title = encodeURIComponent('[' + fbType + '] ' + desc.slice(0, 50));
|
|
1495
|
+
var bodyText = '## Description\\n' + desc;
|
|
1496
|
+
if (includeSys) {
|
|
1497
|
+
bodyText += '\\n\\n## System Info\\nVersion: ' + _currentVersion + '\\nSearch Level: ' + _currentSearchLevel;
|
|
1498
|
+
}
|
|
1499
|
+
var body = encodeURIComponent(bodyText);
|
|
1500
|
+
window.open('https://github.com/PCIRCLE-AI/memesh-llm-memory/issues/new?title=' + title + '&body=' + body + '&labels=' + labels, '_blank');
|
|
1501
|
+
panel.classList.remove('open');
|
|
1502
|
+
document.getElementById('fb-desc').value = '';
|
|
1503
|
+
});
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
// Close panel on outside click
|
|
1507
|
+
document.addEventListener('click', function (e) {
|
|
1508
|
+
if (!panel.contains(e.target) && e.target !== btn) {
|
|
1509
|
+
panel.classList.remove('open');
|
|
1510
|
+
}
|
|
1511
|
+
});
|
|
1512
|
+
})();
|
|
1513
|
+
|
|
1514
|
+
// ---- Settings tab ----
|
|
1515
|
+
var settingsLoaded = false;
|
|
1516
|
+
|
|
1517
|
+
async function loadSettings() {
|
|
1518
|
+
if (settingsLoaded) return;
|
|
1519
|
+
settingsLoaded = false; // allow re-render on each visit to reflect saved state
|
|
1520
|
+
var body = document.getElementById('settings-body');
|
|
1521
|
+
body.textContent = '';
|
|
1522
|
+
|
|
1523
|
+
var configRes;
|
|
1524
|
+
try {
|
|
1525
|
+
configRes = await apiCall('GET', '/v1/config');
|
|
1526
|
+
} catch (err) {
|
|
1527
|
+
showError(body, 'Failed to load config: ' + err.message);
|
|
1528
|
+
return;
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
var currentConfig = (configRes.success && configRes.data && configRes.data.config) || {};
|
|
1532
|
+
var caps = (configRes.success && configRes.data && configRes.data.capabilities) || {};
|
|
1533
|
+
|
|
1534
|
+
// --- Capabilities section ---
|
|
1535
|
+
var capSection = document.createElement('div');
|
|
1536
|
+
capSection.className = 'settings-section';
|
|
1537
|
+
var capH3 = document.createElement('h3');
|
|
1538
|
+
capH3.textContent = 'Current Capabilities';
|
|
1539
|
+
capSection.appendChild(capH3);
|
|
1540
|
+
|
|
1541
|
+
var capGrid = document.createElement('div');
|
|
1542
|
+
capGrid.className = 'cap-grid';
|
|
1543
|
+
|
|
1544
|
+
function addCap(label, value, good) {
|
|
1545
|
+
var item = document.createElement('div');
|
|
1546
|
+
item.className = 'cap-item';
|
|
1547
|
+
var lbl = document.createElement('div');
|
|
1548
|
+
lbl.className = 'cap-label';
|
|
1549
|
+
lbl.textContent = label;
|
|
1550
|
+
var val = document.createElement('div');
|
|
1551
|
+
val.className = 'cap-value';
|
|
1552
|
+
if (good !== undefined) {
|
|
1553
|
+
var dot = document.createElement('span');
|
|
1554
|
+
dot.className = 'status-dot ' + (good ? 'ok' : 'warn');
|
|
1555
|
+
val.appendChild(dot);
|
|
1556
|
+
}
|
|
1557
|
+
val.appendChild(document.createTextNode(String(value)));
|
|
1558
|
+
item.appendChild(lbl);
|
|
1559
|
+
item.appendChild(val);
|
|
1560
|
+
capGrid.appendChild(item);
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
var searchLevel = caps.searchLevel !== undefined ? caps.searchLevel : '?';
|
|
1564
|
+
_currentSearchLevel = String(searchLevel);
|
|
1565
|
+
addCap('Search Level', searchLevel === 1 ? 'Smart Mode' : 'Core FTS5', searchLevel === 1);
|
|
1566
|
+
addCap('Embeddings', caps.embeddings || 'tfidf', caps.embeddings && caps.embeddings !== 'tfidf');
|
|
1567
|
+
var llmProvider = (caps.llm && caps.llm.provider) ? caps.llm.provider : 'None';
|
|
1568
|
+
addCap('LLM Provider', llmProvider, !!caps.llm);
|
|
1569
|
+
var llmModel = (caps.llm && caps.llm.model) ? caps.llm.model : '\u2014';
|
|
1570
|
+
addCap('LLM Model', llmModel, !!caps.llm);
|
|
1571
|
+
|
|
1572
|
+
capSection.appendChild(capGrid);
|
|
1573
|
+
body.appendChild(capSection);
|
|
1574
|
+
|
|
1575
|
+
// --- LLM Provider section ---
|
|
1576
|
+
var llmSection = document.createElement('div');
|
|
1577
|
+
llmSection.className = 'settings-section';
|
|
1578
|
+
var llmH3 = document.createElement('h3');
|
|
1579
|
+
llmH3.textContent = 'LLM Provider';
|
|
1580
|
+
llmSection.appendChild(llmH3);
|
|
1581
|
+
|
|
1582
|
+
var providerGroup = document.createElement('div');
|
|
1583
|
+
providerGroup.className = 'form-group';
|
|
1584
|
+
var providerLabel = document.createElement('label');
|
|
1585
|
+
providerLabel.className = 'form-label';
|
|
1586
|
+
providerLabel.textContent = 'Provider';
|
|
1587
|
+
providerGroup.appendChild(providerLabel);
|
|
1588
|
+
|
|
1589
|
+
var radioGroup = document.createElement('div');
|
|
1590
|
+
radioGroup.className = 'provider-radio-group';
|
|
1591
|
+
var currentProvider = (currentConfig.llm && currentConfig.llm.provider) || '';
|
|
1592
|
+
|
|
1593
|
+
['anthropic', 'openai', 'ollama'].forEach(function (p) {
|
|
1594
|
+
var wrapper = document.createElement('label');
|
|
1595
|
+
wrapper.className = 'provider-radio' + (currentProvider === p ? ' selected' : '');
|
|
1596
|
+
|
|
1597
|
+
var radio = document.createElement('input');
|
|
1598
|
+
radio.type = 'radio';
|
|
1599
|
+
radio.name = 'llm-provider';
|
|
1600
|
+
radio.value = p;
|
|
1601
|
+
if (currentProvider === p) radio.checked = true;
|
|
1602
|
+
|
|
1603
|
+
radio.addEventListener('change', function () {
|
|
1604
|
+
document.querySelectorAll('.provider-radio').forEach(function (el) { el.classList.remove('selected'); });
|
|
1605
|
+
wrapper.classList.add('selected');
|
|
1606
|
+
apiKeyGroup.style.display = p === 'ollama' ? 'none' : 'block';
|
|
1607
|
+
modelGroup.style.display = 'block';
|
|
1608
|
+
});
|
|
1609
|
+
|
|
1610
|
+
wrapper.appendChild(radio);
|
|
1611
|
+
wrapper.appendChild(document.createTextNode(p.charAt(0).toUpperCase() + p.slice(1)));
|
|
1612
|
+
radioGroup.appendChild(wrapper);
|
|
1613
|
+
});
|
|
1614
|
+
|
|
1615
|
+
providerGroup.appendChild(radioGroup);
|
|
1616
|
+
llmSection.appendChild(providerGroup);
|
|
1617
|
+
|
|
1618
|
+
// API Key input
|
|
1619
|
+
var apiKeyGroup = document.createElement('div');
|
|
1620
|
+
apiKeyGroup.className = 'form-group';
|
|
1621
|
+
apiKeyGroup.style.display = currentProvider && currentProvider !== 'ollama' ? 'block' : (currentProvider === 'ollama' ? 'none' : 'none');
|
|
1622
|
+
|
|
1623
|
+
var apiKeyLabel = document.createElement('label');
|
|
1624
|
+
apiKeyLabel.className = 'form-label';
|
|
1625
|
+
apiKeyLabel.textContent = 'API Key';
|
|
1626
|
+
apiKeyGroup.appendChild(apiKeyLabel);
|
|
1627
|
+
|
|
1628
|
+
var apiKeyInput = document.createElement('input');
|
|
1629
|
+
apiKeyInput.type = 'password';
|
|
1630
|
+
apiKeyInput.className = 'search-input';
|
|
1631
|
+
apiKeyInput.placeholder = 'sk-ant-api03-\u2026 or sk-\u2026';
|
|
1632
|
+
apiKeyInput.style.width = '100%';
|
|
1633
|
+
apiKeyInput.autocomplete = 'off';
|
|
1634
|
+
apiKeyGroup.appendChild(apiKeyInput);
|
|
1635
|
+
|
|
1636
|
+
var apiKeyHint = document.createElement('div');
|
|
1637
|
+
apiKeyHint.className = 'form-hint';
|
|
1638
|
+
apiKeyHint.textContent = 'Leave blank to keep existing key. Key is stored in ~/.memesh/config.json (mode 600).';
|
|
1639
|
+
apiKeyGroup.appendChild(apiKeyHint);
|
|
1640
|
+
llmSection.appendChild(apiKeyGroup);
|
|
1641
|
+
|
|
1642
|
+
// Model input
|
|
1643
|
+
var modelGroup = document.createElement('div');
|
|
1644
|
+
modelGroup.className = 'form-group';
|
|
1645
|
+
modelGroup.style.display = currentProvider ? 'block' : 'none';
|
|
1646
|
+
|
|
1647
|
+
var modelLabel = document.createElement('label');
|
|
1648
|
+
modelLabel.className = 'form-label';
|
|
1649
|
+
modelLabel.textContent = 'Model (optional)';
|
|
1650
|
+
modelGroup.appendChild(modelLabel);
|
|
1651
|
+
|
|
1652
|
+
var modelInput = document.createElement('input');
|
|
1653
|
+
modelInput.type = 'text';
|
|
1654
|
+
modelInput.className = 'search-input';
|
|
1655
|
+
modelInput.placeholder = 'e.g. claude-haiku-4-5, gpt-4o-mini, llama3.2';
|
|
1656
|
+
modelInput.style.width = '100%';
|
|
1657
|
+
if (currentConfig.llm && currentConfig.llm.model) modelInput.value = currentConfig.llm.model;
|
|
1658
|
+
modelGroup.appendChild(modelInput);
|
|
1659
|
+
llmSection.appendChild(modelGroup);
|
|
1660
|
+
|
|
1661
|
+
// Save button row
|
|
1662
|
+
var saveRow = document.createElement('div');
|
|
1663
|
+
saveRow.style.display = 'flex';
|
|
1664
|
+
saveRow.style.gap = '10px';
|
|
1665
|
+
saveRow.style.alignItems = 'center';
|
|
1666
|
+
saveRow.style.marginTop = '4px';
|
|
1667
|
+
|
|
1668
|
+
var saveBtn = document.createElement('button');
|
|
1669
|
+
saveBtn.className = 'btn btn-primary btn-sm';
|
|
1670
|
+
saveBtn.textContent = 'Save';
|
|
1671
|
+
|
|
1672
|
+
var saveMsg = document.createElement('span');
|
|
1673
|
+
saveMsg.style.fontSize = '13px';
|
|
1674
|
+
saveMsg.style.color = 'var(--success)';
|
|
1675
|
+
|
|
1676
|
+
saveBtn.addEventListener('click', async function () {
|
|
1677
|
+
var selectedProvider = document.querySelector('input[name=llm-provider]:checked');
|
|
1678
|
+
if (!selectedProvider) { saveMsg.style.color = 'var(--danger)'; saveMsg.textContent = 'Select a provider first.'; return; }
|
|
1679
|
+
var prov = selectedProvider.value;
|
|
1680
|
+
var llmUpdate = { provider: prov };
|
|
1681
|
+
if (modelInput.value.trim()) llmUpdate.model = modelInput.value.trim();
|
|
1682
|
+
if (apiKeyInput.value.trim()) llmUpdate.apiKey = apiKeyInput.value.trim();
|
|
1683
|
+
|
|
1684
|
+
saveBtn.disabled = true;
|
|
1685
|
+
saveBtn.textContent = 'Saving\u2026';
|
|
1686
|
+
try {
|
|
1687
|
+
var res = await apiCall('POST', '/v1/config', { llm: llmUpdate });
|
|
1688
|
+
if (!res.success) throw new Error(res.error || 'Save failed');
|
|
1689
|
+
saveMsg.style.color = 'var(--success)';
|
|
1690
|
+
saveMsg.textContent = 'Saved! Restart the server to apply LLM changes.';
|
|
1691
|
+
apiKeyInput.value = '';
|
|
1692
|
+
settingsLoaded = false; // force re-render on next visit
|
|
1693
|
+
} catch (err) {
|
|
1694
|
+
saveMsg.style.color = 'var(--danger)';
|
|
1695
|
+
saveMsg.textContent = 'Error: ' + err.message;
|
|
1696
|
+
} finally {
|
|
1697
|
+
saveBtn.disabled = false;
|
|
1698
|
+
saveBtn.textContent = 'Save';
|
|
1699
|
+
}
|
|
1700
|
+
});
|
|
1701
|
+
|
|
1702
|
+
saveRow.appendChild(saveBtn);
|
|
1703
|
+
saveRow.appendChild(saveMsg);
|
|
1704
|
+
llmSection.appendChild(saveRow);
|
|
1705
|
+
body.appendChild(llmSection);
|
|
1706
|
+
|
|
1707
|
+
// --- General settings section ---
|
|
1708
|
+
var genSection = document.createElement('div');
|
|
1709
|
+
genSection.className = 'settings-section';
|
|
1710
|
+
var genH3 = document.createElement('h3');
|
|
1711
|
+
genH3.textContent = 'General';
|
|
1712
|
+
genSection.appendChild(genH3);
|
|
1713
|
+
|
|
1714
|
+
// Auto-capture toggle
|
|
1715
|
+
var autoGroup = document.createElement('div');
|
|
1716
|
+
autoGroup.className = 'form-group';
|
|
1717
|
+
autoGroup.style.display = 'flex';
|
|
1718
|
+
autoGroup.style.alignItems = 'center';
|
|
1719
|
+
autoGroup.style.gap = '10px';
|
|
1720
|
+
|
|
1721
|
+
var autoCheck = document.createElement('input');
|
|
1722
|
+
autoCheck.type = 'checkbox';
|
|
1723
|
+
autoCheck.id = 'auto-capture';
|
|
1724
|
+
autoCheck.style.accentColor = 'var(--accent)';
|
|
1725
|
+
autoCheck.checked = currentConfig.autoCapture !== false;
|
|
1726
|
+
|
|
1727
|
+
var autoLabel = document.createElement('label');
|
|
1728
|
+
autoLabel.htmlFor = 'auto-capture';
|
|
1729
|
+
autoLabel.style.fontSize = '13px';
|
|
1730
|
+
autoLabel.style.fontWeight = '600';
|
|
1731
|
+
autoLabel.style.cursor = 'pointer';
|
|
1732
|
+
autoLabel.textContent = 'Auto-capture (session-start hook)';
|
|
1733
|
+
|
|
1734
|
+
autoGroup.appendChild(autoCheck);
|
|
1735
|
+
autoGroup.appendChild(autoLabel);
|
|
1736
|
+
genSection.appendChild(autoGroup);
|
|
1737
|
+
|
|
1738
|
+
var autoHint = document.createElement('div');
|
|
1739
|
+
autoHint.className = 'form-hint';
|
|
1740
|
+
autoHint.style.marginLeft = '28px';
|
|
1741
|
+
autoHint.textContent = 'Automatically recall project memories at the start of each Claude Code session.';
|
|
1742
|
+
genSection.appendChild(autoHint);
|
|
1743
|
+
|
|
1744
|
+
var genSaveRow = document.createElement('div');
|
|
1745
|
+
genSaveRow.style.marginTop = '12px';
|
|
1746
|
+
|
|
1747
|
+
var genSaveBtn = document.createElement('button');
|
|
1748
|
+
genSaveBtn.className = 'btn btn-primary btn-sm';
|
|
1749
|
+
genSaveBtn.textContent = 'Save';
|
|
1750
|
+
|
|
1751
|
+
var genMsg = document.createElement('span');
|
|
1752
|
+
genMsg.style.fontSize = '13px';
|
|
1753
|
+
genMsg.style.marginLeft = '10px';
|
|
1754
|
+
genMsg.style.color = 'var(--success)';
|
|
1755
|
+
|
|
1756
|
+
genSaveBtn.addEventListener('click', async function () {
|
|
1757
|
+
genSaveBtn.disabled = true;
|
|
1758
|
+
genSaveBtn.textContent = 'Saving\u2026';
|
|
1759
|
+
try {
|
|
1760
|
+
var res = await apiCall('POST', '/v1/config', { autoCapture: autoCheck.checked });
|
|
1761
|
+
if (!res.success) throw new Error(res.error || 'Save failed');
|
|
1762
|
+
genMsg.style.color = 'var(--success)';
|
|
1763
|
+
genMsg.textContent = 'Saved!';
|
|
1764
|
+
setTimeout(function () { genMsg.textContent = ''; }, 3000);
|
|
1765
|
+
} catch (err) {
|
|
1766
|
+
genMsg.style.color = 'var(--danger)';
|
|
1767
|
+
genMsg.textContent = 'Error: ' + err.message;
|
|
1768
|
+
} finally {
|
|
1769
|
+
genSaveBtn.disabled = false;
|
|
1770
|
+
genSaveBtn.textContent = 'Save';
|
|
1771
|
+
}
|
|
1772
|
+
});
|
|
1773
|
+
|
|
1774
|
+
genSaveRow.appendChild(genSaveBtn);
|
|
1775
|
+
genSaveRow.appendChild(genMsg);
|
|
1776
|
+
genSection.appendChild(genSaveRow);
|
|
1777
|
+
body.appendChild(genSection);
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
// ---- Welcome Wizard ----
|
|
1781
|
+
var wizardStep = 0;
|
|
1782
|
+
var wizardProvider = '';
|
|
1783
|
+
var wizardApiKey = '';
|
|
1784
|
+
var wizardModel = '';
|
|
1785
|
+
|
|
1786
|
+
function updateWizardStepDots(totalSteps) {
|
|
1787
|
+
var dotsEl = document.getElementById('wizard-steps');
|
|
1788
|
+
dotsEl.textContent = '';
|
|
1789
|
+
for (var i = 0; i < totalSteps; i++) {
|
|
1790
|
+
var dot = document.createElement('div');
|
|
1791
|
+
dot.className = 'wizard-step-dot' + (i < wizardStep ? ' done' : (i === wizardStep ? ' active' : ''));
|
|
1792
|
+
dotsEl.appendChild(dot);
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
function renderWizardStep() {
|
|
1797
|
+
var content = document.getElementById('wizard-content');
|
|
1798
|
+
var actions = document.getElementById('wizard-actions');
|
|
1799
|
+
content.textContent = '';
|
|
1800
|
+
actions.textContent = '';
|
|
1801
|
+
updateWizardStepDots(3);
|
|
1802
|
+
|
|
1803
|
+
if (wizardStep === 0) {
|
|
1804
|
+
var title = document.createElement('h2');
|
|
1805
|
+
title.textContent = 'Welcome to MeMesh';
|
|
1806
|
+
content.appendChild(title);
|
|
1807
|
+
|
|
1808
|
+
var sub = document.createElement('p');
|
|
1809
|
+
sub.className = 'subtitle';
|
|
1810
|
+
sub.textContent = 'MeMesh gives Claude a persistent knowledge graph. Set up an LLM provider to unlock Smart Mode \u2014 semantic search, conflict detection, and knowledge evolution.';
|
|
1811
|
+
content.appendChild(sub);
|
|
1812
|
+
|
|
1813
|
+
var capList = document.createElement('ul');
|
|
1814
|
+
capList.style.listStyle = 'none';
|
|
1815
|
+
capList.style.marginBottom = '8px';
|
|
1816
|
+
[
|
|
1817
|
+
['Core FTS5 search \u2014 always available', true],
|
|
1818
|
+
['Smart Mode (LLM) \u2014 requires API key', false],
|
|
1819
|
+
['Auto-capture on session start', true],
|
|
1820
|
+
].forEach(function (item) {
|
|
1821
|
+
var li = document.createElement('li');
|
|
1822
|
+
li.style.cssText = 'display:flex;align-items:center;gap:8px;padding:6px 0;font-size:14px;';
|
|
1823
|
+
var dot = document.createElement('span');
|
|
1824
|
+
dot.className = 'status-dot ' + (item[1] ? 'ok' : 'warn');
|
|
1825
|
+
li.appendChild(dot);
|
|
1826
|
+
li.appendChild(document.createTextNode(String(item[0])));
|
|
1827
|
+
capList.appendChild(li);
|
|
1828
|
+
});
|
|
1829
|
+
content.appendChild(capList);
|
|
1830
|
+
|
|
1831
|
+
var skipBtn = document.createElement('button');
|
|
1832
|
+
skipBtn.className = 'btn btn-secondary';
|
|
1833
|
+
skipBtn.textContent = 'Skip for now';
|
|
1834
|
+
skipBtn.addEventListener('click', closeWizard);
|
|
1835
|
+
|
|
1836
|
+
var nextBtn = document.createElement('button');
|
|
1837
|
+
nextBtn.className = 'btn btn-primary';
|
|
1838
|
+
nextBtn.textContent = 'Set up Smart Mode \u2192';
|
|
1839
|
+
nextBtn.addEventListener('click', function () { wizardStep = 1; renderWizardStep(); });
|
|
1840
|
+
|
|
1841
|
+
actions.appendChild(skipBtn);
|
|
1842
|
+
actions.appendChild(nextBtn);
|
|
1843
|
+
|
|
1844
|
+
} else if (wizardStep === 1) {
|
|
1845
|
+
var title2 = document.createElement('h2');
|
|
1846
|
+
title2.textContent = 'Choose a Provider';
|
|
1847
|
+
content.appendChild(title2);
|
|
1848
|
+
|
|
1849
|
+
var sub2 = document.createElement('p');
|
|
1850
|
+
sub2.className = 'subtitle';
|
|
1851
|
+
sub2.textContent = 'Select your LLM provider and enter your API key. Keys are stored locally in ~/.memesh/config.json (mode 600).';
|
|
1852
|
+
content.appendChild(sub2);
|
|
1853
|
+
|
|
1854
|
+
// Provider radios
|
|
1855
|
+
var radioGroupW = document.createElement('div');
|
|
1856
|
+
radioGroupW.className = 'provider-radio-group';
|
|
1857
|
+
radioGroupW.style.marginBottom = '16px';
|
|
1858
|
+
|
|
1859
|
+
['anthropic', 'openai', 'ollama'].forEach(function (p) {
|
|
1860
|
+
var wrapper = document.createElement('label');
|
|
1861
|
+
wrapper.className = 'provider-radio' + (wizardProvider === p ? ' selected' : '');
|
|
1862
|
+
|
|
1863
|
+
var radio = document.createElement('input');
|
|
1864
|
+
radio.type = 'radio';
|
|
1865
|
+
radio.name = 'wizard-provider';
|
|
1866
|
+
radio.value = p;
|
|
1867
|
+
if (wizardProvider === p) radio.checked = true;
|
|
1868
|
+
|
|
1869
|
+
radio.addEventListener('change', function () {
|
|
1870
|
+
document.querySelectorAll('.provider-radio').forEach(function (el) { el.classList.remove('selected'); });
|
|
1871
|
+
wrapper.classList.add('selected');
|
|
1872
|
+
wizardProvider = p;
|
|
1873
|
+
keyInput.style.display = p === 'ollama' ? 'none' : 'block';
|
|
1874
|
+
keyLabel.style.display = p === 'ollama' ? 'none' : 'block';
|
|
1875
|
+
});
|
|
1876
|
+
|
|
1877
|
+
wrapper.appendChild(radio);
|
|
1878
|
+
wrapper.appendChild(document.createTextNode(p.charAt(0).toUpperCase() + p.slice(1)));
|
|
1879
|
+
radioGroupW.appendChild(wrapper);
|
|
1880
|
+
});
|
|
1881
|
+
content.appendChild(radioGroupW);
|
|
1882
|
+
|
|
1883
|
+
// API Key
|
|
1884
|
+
var keyLabel = document.createElement('label');
|
|
1885
|
+
keyLabel.className = 'form-label';
|
|
1886
|
+
keyLabel.textContent = 'API Key';
|
|
1887
|
+
keyLabel.style.display = wizardProvider && wizardProvider !== 'ollama' ? 'block' : 'none';
|
|
1888
|
+
content.appendChild(keyLabel);
|
|
1889
|
+
|
|
1890
|
+
var keyInput = document.createElement('input');
|
|
1891
|
+
keyInput.type = 'password';
|
|
1892
|
+
keyInput.className = 'search-input';
|
|
1893
|
+
keyInput.placeholder = 'Paste your API key\u2026';
|
|
1894
|
+
keyInput.style.width = '100%';
|
|
1895
|
+
keyInput.style.marginBottom = '12px';
|
|
1896
|
+
keyInput.style.display = wizardProvider && wizardProvider !== 'ollama' ? 'block' : 'none';
|
|
1897
|
+
keyInput.value = wizardApiKey;
|
|
1898
|
+
keyInput.addEventListener('input', function () { wizardApiKey = keyInput.value; });
|
|
1899
|
+
content.appendChild(keyInput);
|
|
1900
|
+
|
|
1901
|
+
// Error message
|
|
1902
|
+
var errMsg = document.createElement('div');
|
|
1903
|
+
errMsg.style.cssText = 'font-size:13px;color:var(--danger);min-height:18px;margin-bottom:8px;';
|
|
1904
|
+
content.appendChild(errMsg);
|
|
1905
|
+
|
|
1906
|
+
var backBtn = document.createElement('button');
|
|
1907
|
+
backBtn.className = 'btn btn-secondary';
|
|
1908
|
+
backBtn.textContent = '\u2190 Back';
|
|
1909
|
+
backBtn.addEventListener('click', function () { wizardStep = 0; renderWizardStep(); });
|
|
1910
|
+
|
|
1911
|
+
var testBtn = document.createElement('button');
|
|
1912
|
+
testBtn.className = 'btn btn-secondary';
|
|
1913
|
+
testBtn.textContent = 'Test Connection';
|
|
1914
|
+
testBtn.addEventListener('click', async function () {
|
|
1915
|
+
if (!wizardProvider) { errMsg.textContent = 'Select a provider.'; return; }
|
|
1916
|
+
if (wizardProvider !== 'ollama' && !wizardApiKey.trim()) { errMsg.textContent = 'Enter an API key.'; return; }
|
|
1917
|
+
testBtn.disabled = true;
|
|
1918
|
+
testBtn.textContent = 'Testing\u2026';
|
|
1919
|
+
errMsg.textContent = '';
|
|
1920
|
+
try {
|
|
1921
|
+
// Save to config and check capabilities
|
|
1922
|
+
var payload = { llm: { provider: wizardProvider } };
|
|
1923
|
+
if (wizardApiKey.trim()) payload.llm.apiKey = wizardApiKey.trim();
|
|
1924
|
+
var res = await apiCall('POST', '/v1/config', payload);
|
|
1925
|
+
if (!res.success) throw new Error(res.error || 'Config save failed');
|
|
1926
|
+
errMsg.style.color = 'var(--success)';
|
|
1927
|
+
errMsg.textContent = 'Connection config saved!';
|
|
1928
|
+
} catch (err) {
|
|
1929
|
+
errMsg.style.color = 'var(--danger)';
|
|
1930
|
+
errMsg.textContent = 'Error: ' + err.message;
|
|
1931
|
+
} finally {
|
|
1932
|
+
testBtn.disabled = false;
|
|
1933
|
+
testBtn.textContent = 'Test Connection';
|
|
1934
|
+
}
|
|
1935
|
+
});
|
|
1936
|
+
|
|
1937
|
+
var nextBtn2 = document.createElement('button');
|
|
1938
|
+
nextBtn2.className = 'btn btn-primary';
|
|
1939
|
+
nextBtn2.textContent = 'Next \u2192';
|
|
1940
|
+
nextBtn2.addEventListener('click', async function () {
|
|
1941
|
+
if (!wizardProvider) { errMsg.textContent = 'Select a provider.'; return; }
|
|
1942
|
+
nextBtn2.disabled = true;
|
|
1943
|
+
nextBtn2.textContent = 'Saving\u2026';
|
|
1944
|
+
try {
|
|
1945
|
+
var payload2 = { llm: { provider: wizardProvider }, setupCompleted: true };
|
|
1946
|
+
if (wizardApiKey.trim()) payload2.llm.apiKey = wizardApiKey.trim();
|
|
1947
|
+
var res2 = await apiCall('POST', '/v1/config', payload2);
|
|
1948
|
+
if (!res2.success) throw new Error(res2.error || 'Save failed');
|
|
1949
|
+
wizardStep = 2;
|
|
1950
|
+
renderWizardStep();
|
|
1951
|
+
} catch (err) {
|
|
1952
|
+
errMsg.style.color = 'var(--danger)';
|
|
1953
|
+
errMsg.textContent = 'Error: ' + err.message;
|
|
1954
|
+
nextBtn2.disabled = false;
|
|
1955
|
+
nextBtn2.textContent = 'Next \u2192';
|
|
1956
|
+
}
|
|
1957
|
+
});
|
|
1958
|
+
|
|
1959
|
+
actions.appendChild(backBtn);
|
|
1960
|
+
actions.appendChild(testBtn);
|
|
1961
|
+
actions.appendChild(nextBtn2);
|
|
1962
|
+
|
|
1963
|
+
} else if (wizardStep === 2) {
|
|
1964
|
+
var title3 = document.createElement('h2');
|
|
1965
|
+
title3.textContent = "You're all set!";
|
|
1966
|
+
content.appendChild(title3);
|
|
1967
|
+
|
|
1968
|
+
var sub3 = document.createElement('p');
|
|
1969
|
+
sub3.className = 'subtitle';
|
|
1970
|
+
sub3.textContent = 'MeMesh Smart Mode is configured. Restart the server for LLM changes to take effect. Start using the Search and Browse tabs to explore your knowledge graph.';
|
|
1971
|
+
content.appendChild(sub3);
|
|
1972
|
+
|
|
1973
|
+
var tipBox = document.createElement('div');
|
|
1974
|
+
tipBox.style.cssText = 'background:var(--accent-glow);border:1px solid rgba(59,130,246,0.2);border-radius:var(--radius-sm);padding:14px 16px;font-size:13px;line-height:1.6;color:var(--text-secondary);';
|
|
1975
|
+
var tipLines = [
|
|
1976
|
+
'\u2022 Use the Search tab to find anything across all your memories.',
|
|
1977
|
+
'\u2022 Browse tab shows all entities with filter controls.',
|
|
1978
|
+
'\u2022 Settings tab lets you change your provider anytime.',
|
|
1979
|
+
];
|
|
1980
|
+
tipLines.forEach(function (line) {
|
|
1981
|
+
var p = document.createElement('p');
|
|
1982
|
+
p.textContent = line;
|
|
1983
|
+
tipBox.appendChild(p);
|
|
1984
|
+
});
|
|
1985
|
+
content.appendChild(tipBox);
|
|
1986
|
+
|
|
1987
|
+
var doneBtn = document.createElement('button');
|
|
1988
|
+
doneBtn.className = 'btn btn-primary';
|
|
1989
|
+
doneBtn.textContent = 'Go to Dashboard';
|
|
1990
|
+
doneBtn.addEventListener('click', closeWizard);
|
|
1991
|
+
actions.appendChild(doneBtn);
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1995
|
+
function showWelcomeWizard() {
|
|
1996
|
+
wizardStep = 0;
|
|
1997
|
+
document.getElementById('wizard-overlay').classList.remove('hidden');
|
|
1998
|
+
renderWizardStep();
|
|
1999
|
+
}
|
|
2000
|
+
|
|
2001
|
+
function closeWizard() {
|
|
2002
|
+
document.getElementById('wizard-overlay').classList.add('hidden');
|
|
2003
|
+
// Mark setup completed so wizard doesn't reappear
|
|
2004
|
+
apiCall('POST', '/v1/config', { setupCompleted: true });
|
|
2005
|
+
}
|
|
2006
|
+
|
|
2007
|
+
// Overlay click does NOT close wizard — user must use Skip or complete setup
|
|
2008
|
+
document.getElementById('wizard-overlay').addEventListener('click', function (e) {
|
|
2009
|
+
if (e.target === this) {
|
|
2010
|
+
// Intentionally do nothing — prevent accidental dismissal
|
|
2011
|
+
}
|
|
2012
|
+
});
|
|
2013
|
+
|
|
2014
|
+
// ---- Init ----
|
|
2015
|
+
checkHealth();
|
|
2016
|
+
loadBrowse();
|
|
2017
|
+
|
|
2018
|
+
// Check if first-run wizard should appear
|
|
2019
|
+
(async function checkFirstRun() {
|
|
2020
|
+
try {
|
|
2021
|
+
var res = await apiCall('GET', '/v1/config');
|
|
2022
|
+
if (res.success && res.data && res.data.config && !res.data.config.setupCompleted) {
|
|
2023
|
+
showWelcomeWizard();
|
|
2024
|
+
}
|
|
2025
|
+
} catch (_err) {
|
|
2026
|
+
// Silently ignore — wizard is optional
|
|
2027
|
+
}
|
|
2028
|
+
})();
|
|
2029
|
+
})();
|
|
2030
|
+
`.trim();
|
|
2031
|
+
return `<!DOCTYPE html>
|
|
2032
|
+
<html lang="en">
|
|
2033
|
+
<head>
|
|
2034
|
+
<meta charset="UTF-8">
|
|
2035
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
2036
|
+
<title>MeMesh LLM Memory — Dashboard</title>
|
|
2037
|
+
<style>${CSS}</style>
|
|
2038
|
+
<!-- bundled d3.js -->
|
|
2039
|
+
<script>
|
|
2040
|
+
${bundledD3}
|
|
2041
|
+
<\/script>
|
|
2042
|
+
</head>
|
|
2043
|
+
<body>
|
|
2044
|
+
|
|
2045
|
+
<div class="header">
|
|
2046
|
+
<div>
|
|
2047
|
+
<h1 style="margin:0;line-height:1.2">MeMesh LLM Memory</h1>
|
|
2048
|
+
<span class="subtitle">powered by pcircle.ai</span>
|
|
2049
|
+
</div>
|
|
2050
|
+
<div class="header-right">
|
|
2051
|
+
<div class="meta">
|
|
2052
|
+
<span id="health-indicator"><span class="dot"></span>Connecting\u2026</span>
|
|
2053
|
+
<span id="version-label" class="version"></span>
|
|
2054
|
+
</div>
|
|
2055
|
+
<button id="theme-btn" title="Toggle theme" style="background:none;border:1px solid var(--border);border-radius:var(--radius-xs);padding:6px 10px;cursor:pointer;color:var(--text-secondary);font-size:14px;transition:border-color 0.2s ease;">\u2600\ufe0f</button>
|
|
2056
|
+
</div>
|
|
2057
|
+
</div>
|
|
2058
|
+
|
|
2059
|
+
<nav class="nav" id="nav">
|
|
2060
|
+
<button class="active" data-tab="search">Search</button>
|
|
2061
|
+
<button data-tab="browse">Browse</button>
|
|
2062
|
+
<button data-tab="graph">Graph</button>
|
|
2063
|
+
<button data-tab="analytics">Analytics</button>
|
|
2064
|
+
<button data-tab="timeline">Timeline</button>
|
|
2065
|
+
<button data-tab="manage">Manage</button>
|
|
2066
|
+
<button data-tab="settings">Settings</button>
|
|
2067
|
+
</nav>
|
|
2068
|
+
|
|
2069
|
+
<div class="content">
|
|
2070
|
+
|
|
2071
|
+
<div class="tab-content active" id="tab-search">
|
|
2072
|
+
<div class="card">
|
|
2073
|
+
<h2>Search Knowledge</h2>
|
|
2074
|
+
<div class="search-row">
|
|
2075
|
+
<input class="search-input" id="search-query" type="text" placeholder="Search your memories\u2026 (e.g., \u201cauth\u201d, \u201cdatabase\u201d, \u201cbug fix\u201d)" />
|
|
2076
|
+
<button class="btn btn-primary" id="search-btn">Search</button>
|
|
2077
|
+
</div>
|
|
2078
|
+
<div id="search-results"></div>
|
|
2079
|
+
</div>
|
|
2080
|
+
</div>
|
|
2081
|
+
|
|
2082
|
+
<div class="tab-content" id="tab-browse">
|
|
2083
|
+
<div class="card">
|
|
2084
|
+
<h2>All Memories</h2>
|
|
2085
|
+
<div class="search-row">
|
|
2086
|
+
<input class="search-input" id="browse-filter" type="text" placeholder="Filter by name or type\u2026" />
|
|
2087
|
+
<button class="btn btn-secondary" id="browse-refresh">Refresh</button>
|
|
2088
|
+
</div>
|
|
2089
|
+
<div id="browse-table-wrap"></div>
|
|
2090
|
+
</div>
|
|
2091
|
+
</div>
|
|
2092
|
+
|
|
2093
|
+
<div class="tab-content" id="tab-graph">
|
|
2094
|
+
<div class="card">
|
|
2095
|
+
<h2>Knowledge Graph</h2>
|
|
2096
|
+
<div class="graph-controls">
|
|
2097
|
+
<span>Scroll to zoom \u00b7 Drag nodes \u00b7 Pan with mouse</span>
|
|
2098
|
+
</div>
|
|
2099
|
+
<div class="graph-svg-wrap" id="graph-svg-wrap"></div>
|
|
2100
|
+
</div>
|
|
2101
|
+
</div>
|
|
2102
|
+
|
|
2103
|
+
<div class="tab-content" id="tab-analytics">
|
|
2104
|
+
<div class="card">
|
|
2105
|
+
<h2>Analytics</h2>
|
|
2106
|
+
<div id="analytics-body"></div>
|
|
2107
|
+
</div>
|
|
2108
|
+
</div>
|
|
2109
|
+
|
|
2110
|
+
<div class="tab-content" id="tab-timeline">
|
|
2111
|
+
<div class="card">
|
|
2112
|
+
<h2>Evolution Timeline</h2>
|
|
2113
|
+
<p style="font-size:13px;color:var(--text-muted);margin-bottom:16px;">Entities connected by \u201csupersedes\u201d relations, showing knowledge evolution chains.</p>
|
|
2114
|
+
<div id="timeline-body"></div>
|
|
2115
|
+
</div>
|
|
2116
|
+
</div>
|
|
2117
|
+
|
|
2118
|
+
<div class="tab-content" id="tab-manage">
|
|
2119
|
+
<div class="card">
|
|
2120
|
+
<h2>Manage Memories</h2>
|
|
2121
|
+
<div class="search-row">
|
|
2122
|
+
<input class="search-input" id="manage-filter" type="text" placeholder="Filter by name or type\u2026" />
|
|
2123
|
+
<button class="btn btn-secondary" id="manage-refresh">Refresh</button>
|
|
2124
|
+
</div>
|
|
2125
|
+
<div id="manage-body">
|
|
2126
|
+
<div id="manage-table-wrap"></div>
|
|
2127
|
+
</div>
|
|
2128
|
+
</div>
|
|
2129
|
+
</div>
|
|
2130
|
+
|
|
2131
|
+
<div class="tab-content" id="tab-settings">
|
|
2132
|
+
<div class="card" id="settings-card">
|
|
2133
|
+
<h2>Settings</h2>
|
|
2134
|
+
<div id="settings-body"></div>
|
|
2135
|
+
</div>
|
|
2136
|
+
</div>
|
|
2137
|
+
|
|
2138
|
+
</div>
|
|
2139
|
+
|
|
2140
|
+
<!-- Welcome Wizard Modal -->
|
|
2141
|
+
<div class="wizard-overlay hidden" id="wizard-overlay">
|
|
2142
|
+
<div class="wizard-modal">
|
|
2143
|
+
<div class="wizard-steps" id="wizard-steps"></div>
|
|
2144
|
+
<div id="wizard-content"></div>
|
|
2145
|
+
<div class="wizard-actions" id="wizard-actions"></div>
|
|
2146
|
+
</div>
|
|
2147
|
+
</div>
|
|
2148
|
+
|
|
2149
|
+
<!-- Graph tooltip -->
|
|
2150
|
+
<div id="live-tooltip" class="graph-tooltip" style="display:none;">
|
|
2151
|
+
<div class="tt-name" style="font-weight:600;color:var(--text-primary);font-size:13px;margin-bottom:3px;"></div>
|
|
2152
|
+
<div class="tt-type" style="color:var(--accent-hover);font-size:11px;font-family:var(--font-mono);margin-bottom:4px;"></div>
|
|
2153
|
+
<div class="tt-obs" style="color:var(--text-muted);font-size:12px;"></div>
|
|
2154
|
+
</div>
|
|
2155
|
+
|
|
2156
|
+
<!-- Feedback widget -->
|
|
2157
|
+
<button id="feedback-btn">💬 Feedback</button>
|
|
2158
|
+
<div id="feedback-panel">
|
|
2159
|
+
<h3>Send Feedback</h3>
|
|
2160
|
+
<div class="fb-radio-group">
|
|
2161
|
+
<label class="fb-radio selected"><input type="radio" name="fb-type" value="bug" checked /> Bug</label>
|
|
2162
|
+
<label class="fb-radio"><input type="radio" name="fb-type" value="feature" /> Feature</label>
|
|
2163
|
+
<label class="fb-radio"><input type="radio" name="fb-type" value="question" /> Question</label>
|
|
2164
|
+
</div>
|
|
2165
|
+
<textarea id="fb-desc" placeholder="Describe your feedback\u2026"></textarea>
|
|
2166
|
+
<div class="fb-sys-row">
|
|
2167
|
+
<input type="checkbox" id="fb-sys" checked style="accent-color:var(--accent);" />
|
|
2168
|
+
<label for="fb-sys">Include system info</label>
|
|
2169
|
+
</div>
|
|
2170
|
+
<button class="btn btn-primary" id="fb-submit" style="width:100%;">Open GitHub Issue</button>
|
|
2171
|
+
</div>
|
|
2172
|
+
|
|
2173
|
+
<script>${SCRIPT}<\/script>
|
|
2174
|
+
</body>
|
|
2175
|
+
</html>`;
|
|
2176
|
+
}
|
|
2177
|
+
//# sourceMappingURL=view-live.js.map
|