@awareness-sdk/local 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/awareness-local.mjs +489 -0
- package/package.json +31 -0
- package/src/api.mjs +122 -0
- package/src/core/cloud-sync.mjs +970 -0
- package/src/core/config.mjs +303 -0
- package/src/core/embedder.mjs +239 -0
- package/src/core/index.mjs +34 -0
- package/src/core/indexer.mjs +726 -0
- package/src/core/knowledge-extractor.mjs +629 -0
- package/src/core/memory-store.mjs +665 -0
- package/src/core/search.mjs +633 -0
- package/src/daemon.mjs +1720 -0
- package/src/mcp-server.mjs +335 -0
- package/src/spec/awareness-spec.json +393 -0
- package/src/web/index.html +1015 -0
|
@@ -0,0 +1,1015 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>Awareness Local</title>
|
|
7
|
+
<style>
|
|
8
|
+
/* ======================================================================
|
|
9
|
+
Awareness Local — Dark Developer Dashboard
|
|
10
|
+
Single-file SPA, zero external dependencies
|
|
11
|
+
====================================================================== */
|
|
12
|
+
|
|
13
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
14
|
+
|
|
15
|
+
:root {
|
|
16
|
+
/* Awareness brand colors — matching frontend/app/globals.css dark mode */
|
|
17
|
+
--bg-primary: #121212;
|
|
18
|
+
--bg-secondary: #1a1a1a;
|
|
19
|
+
--bg-tertiary: #242424;
|
|
20
|
+
--bg-hover: #292929;
|
|
21
|
+
--border: #2e2e2e;
|
|
22
|
+
--border-active: #3b82f6;
|
|
23
|
+
--text-primary: #ebebeb;
|
|
24
|
+
--text-secondary: #94a3b8;
|
|
25
|
+
--text-muted: #64748b;
|
|
26
|
+
--accent: #3b82f6; /* Awareness primary blue */
|
|
27
|
+
--accent-hover: #60a5fa;
|
|
28
|
+
--accent-glow: #0ea5e9; /* Logo deep blue */
|
|
29
|
+
--green: #22c55e;
|
|
30
|
+
--green-bg: rgba(34,197,94,0.12);
|
|
31
|
+
--yellow: #eab308;
|
|
32
|
+
--yellow-bg: rgba(234,179,8,0.12);
|
|
33
|
+
--red: #ef4444;
|
|
34
|
+
--red-bg: rgba(239,68,68,0.12);
|
|
35
|
+
--blue: #3b82f6;
|
|
36
|
+
--blue-bg: rgba(59,130,246,0.12);
|
|
37
|
+
--orange: #f97316;
|
|
38
|
+
--orange-bg: rgba(249,115,22,0.12);
|
|
39
|
+
--radius: 12px; /* Matching frontend border-radius */
|
|
40
|
+
--radius-sm: 6px;
|
|
41
|
+
--transition: 150ms ease;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
html { font-size: 14px; }
|
|
45
|
+
body {
|
|
46
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
47
|
+
background: var(--bg-primary);
|
|
48
|
+
color: var(--text-primary);
|
|
49
|
+
line-height: 1.6;
|
|
50
|
+
min-height: 100vh;
|
|
51
|
+
display: flex;
|
|
52
|
+
flex-direction: column;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/* ---- Layout ---- */
|
|
56
|
+
.app { display: flex; flex-direction: column; min-height: 100vh; }
|
|
57
|
+
.header {
|
|
58
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
59
|
+
padding: 12px 24px; border-bottom: 1px solid var(--border);
|
|
60
|
+
background: var(--bg-secondary);
|
|
61
|
+
}
|
|
62
|
+
.header h1 { font-size: 1.15rem; font-weight: 600; display: flex; align-items: center; gap: 8px; }
|
|
63
|
+
.header .logo-svg { width: 28px; height: 28px; flex-shrink: 0; vertical-align: middle; margin-right: 4px; }
|
|
64
|
+
.header-right { display: flex; align-items: center; gap: 12px; }
|
|
65
|
+
.daemon-badge {
|
|
66
|
+
display: inline-flex; align-items: center; gap: 6px;
|
|
67
|
+
font-size: 0.8rem; color: var(--text-secondary); background: var(--bg-tertiary);
|
|
68
|
+
padding: 4px 10px; border-radius: 999px; border: 1px solid var(--border);
|
|
69
|
+
}
|
|
70
|
+
.daemon-badge .dot { width: 7px; height: 7px; border-radius: 50%; }
|
|
71
|
+
.daemon-badge .dot.ok { background: var(--green); box-shadow: 0 0 6px var(--green); }
|
|
72
|
+
.daemon-badge .dot.err { background: var(--red); }
|
|
73
|
+
|
|
74
|
+
.main { flex: 1; display: flex; flex-direction: column; }
|
|
75
|
+
|
|
76
|
+
/* ---- Tab Navigation ---- */
|
|
77
|
+
.tabs {
|
|
78
|
+
display: flex; gap: 0; border-bottom: 1px solid var(--border);
|
|
79
|
+
background: var(--bg-secondary); padding: 0 24px; overflow-x: auto;
|
|
80
|
+
}
|
|
81
|
+
.tab-btn {
|
|
82
|
+
background: none; border: none; border-bottom: 2px solid transparent;
|
|
83
|
+
color: var(--text-secondary); font-size: 0.9rem; padding: 12px 16px;
|
|
84
|
+
cursor: pointer; white-space: nowrap; transition: color var(--transition), border-color var(--transition);
|
|
85
|
+
}
|
|
86
|
+
.tab-btn:hover { color: var(--text-primary); }
|
|
87
|
+
.tab-btn.active { color: var(--accent); border-bottom-color: var(--accent); font-weight: 500; }
|
|
88
|
+
|
|
89
|
+
/* ---- Content Area ---- */
|
|
90
|
+
.content { flex: 1; padding: 24px; overflow-y: auto; }
|
|
91
|
+
.panel { display: none; }
|
|
92
|
+
.panel.active { display: block; }
|
|
93
|
+
|
|
94
|
+
/* ---- Search Bar ---- */
|
|
95
|
+
.search-bar {
|
|
96
|
+
display: flex; gap: 8px; margin-bottom: 20px;
|
|
97
|
+
}
|
|
98
|
+
.search-bar input {
|
|
99
|
+
flex: 1; background: var(--bg-tertiary); border: 1px solid var(--border);
|
|
100
|
+
border-radius: var(--radius); padding: 10px 14px; color: var(--text-primary);
|
|
101
|
+
font-size: 0.9rem; outline: none; transition: border-color var(--transition);
|
|
102
|
+
}
|
|
103
|
+
.search-bar input::placeholder { color: var(--text-muted); }
|
|
104
|
+
.search-bar input:focus { border-color: var(--accent); }
|
|
105
|
+
.search-bar button {
|
|
106
|
+
background: var(--accent); color: #fff; border: none; border-radius: var(--radius);
|
|
107
|
+
padding: 10px 20px; font-size: 0.9rem; cursor: pointer; font-weight: 500;
|
|
108
|
+
transition: background var(--transition);
|
|
109
|
+
}
|
|
110
|
+
.search-bar button:hover { background: var(--accent-hover); }
|
|
111
|
+
|
|
112
|
+
/* ---- Memory List ---- */
|
|
113
|
+
.memory-list { display: flex; flex-direction: column; gap: 8px; }
|
|
114
|
+
.memory-item {
|
|
115
|
+
background: var(--bg-secondary); border: 1px solid var(--border);
|
|
116
|
+
border-radius: var(--radius); padding: 14px 18px; cursor: pointer;
|
|
117
|
+
transition: border-color var(--transition), background var(--transition);
|
|
118
|
+
}
|
|
119
|
+
.memory-item:hover { border-color: var(--border-active); background: var(--bg-hover); }
|
|
120
|
+
.memory-item-header { display: flex; align-items: center; gap: 10px; margin-bottom: 6px; flex-wrap: wrap; }
|
|
121
|
+
.memory-item-title { font-weight: 500; font-size: 0.95rem; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
122
|
+
.memory-item-date { font-size: 0.78rem; color: var(--text-muted); white-space: nowrap; }
|
|
123
|
+
.memory-item-meta { display: flex; gap: 6px; flex-wrap: wrap; }
|
|
124
|
+
.memory-item-content {
|
|
125
|
+
display: none; margin-top: 12px; padding-top: 12px;
|
|
126
|
+
border-top: 1px solid var(--border); font-size: 0.88rem;
|
|
127
|
+
color: var(--text-secondary); white-space: pre-wrap; word-break: break-word;
|
|
128
|
+
max-height: 400px; overflow-y: auto;
|
|
129
|
+
}
|
|
130
|
+
.memory-item.expanded .memory-item-content { display: block; }
|
|
131
|
+
|
|
132
|
+
/* ---- Badges ---- */
|
|
133
|
+
.badge {
|
|
134
|
+
display: inline-block; font-size: 0.72rem; padding: 2px 8px;
|
|
135
|
+
border-radius: 999px; font-weight: 500; white-space: nowrap;
|
|
136
|
+
}
|
|
137
|
+
.badge-type { background: var(--blue-bg); color: var(--blue); }
|
|
138
|
+
.badge-tag { background: var(--bg-tertiary); color: var(--text-secondary); border: 1px solid var(--border); }
|
|
139
|
+
.badge-decision { background: var(--accent); color: #fff; }
|
|
140
|
+
.badge-problem_solution { background: var(--green); color: #fff; }
|
|
141
|
+
.badge-workflow { background: var(--blue); color: #fff; }
|
|
142
|
+
.badge-key_point { background: var(--yellow); color: #000; }
|
|
143
|
+
.badge-pitfall { background: var(--red); color: #fff; }
|
|
144
|
+
.badge-insight { background: var(--orange); color: #fff; }
|
|
145
|
+
.badge-confidence { background: var(--bg-tertiary); color: var(--text-secondary); border: 1px solid var(--border); }
|
|
146
|
+
.badge-confidence.high { border-color: var(--green); color: var(--green); }
|
|
147
|
+
.badge-confidence.mid { border-color: var(--yellow); color: var(--yellow); }
|
|
148
|
+
.badge-confidence.low { border-color: var(--red); color: var(--red); }
|
|
149
|
+
|
|
150
|
+
/* ---- Knowledge Cards Grid ---- */
|
|
151
|
+
.category-section { margin-bottom: 28px; }
|
|
152
|
+
.category-title { font-size: 0.85rem; font-weight: 600; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 12px; }
|
|
153
|
+
.cards-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 12px; }
|
|
154
|
+
.kc-card {
|
|
155
|
+
background: var(--bg-secondary); border: 1px solid var(--border);
|
|
156
|
+
border-radius: var(--radius); padding: 16px; transition: border-color var(--transition);
|
|
157
|
+
}
|
|
158
|
+
.kc-card:hover { border-color: var(--border-active); }
|
|
159
|
+
.kc-card-title { font-weight: 500; margin-bottom: 8px; font-size: 0.92rem; }
|
|
160
|
+
.kc-card-summary { font-size: 0.85rem; color: var(--text-secondary); margin-bottom: 10px; line-height: 1.5; }
|
|
161
|
+
.kc-card-footer { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
|
|
162
|
+
|
|
163
|
+
/* ---- Tasks ---- */
|
|
164
|
+
.task-list { display: flex; flex-direction: column; gap: 8px; }
|
|
165
|
+
.task-item {
|
|
166
|
+
background: var(--bg-secondary); border: 1px solid var(--border);
|
|
167
|
+
border-radius: var(--radius); padding: 14px 18px;
|
|
168
|
+
display: flex; align-items: center; gap: 14px;
|
|
169
|
+
transition: border-color var(--transition);
|
|
170
|
+
}
|
|
171
|
+
.task-item:hover { border-color: var(--border-active); }
|
|
172
|
+
.priority-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
|
|
173
|
+
.priority-dot.high { background: var(--red); box-shadow: 0 0 6px var(--red); }
|
|
174
|
+
.priority-dot.medium { background: var(--yellow); }
|
|
175
|
+
.priority-dot.low { background: var(--green); }
|
|
176
|
+
.task-info { flex: 1; min-width: 0; }
|
|
177
|
+
.task-title { font-weight: 500; font-size: 0.92rem; }
|
|
178
|
+
.task-desc { font-size: 0.82rem; color: var(--text-secondary); margin-top: 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
179
|
+
.task-status-select {
|
|
180
|
+
background: var(--bg-tertiary); color: var(--text-primary);
|
|
181
|
+
border: 1px solid var(--border); border-radius: var(--radius-sm);
|
|
182
|
+
padding: 6px 10px; font-size: 0.82rem; cursor: pointer; outline: none;
|
|
183
|
+
}
|
|
184
|
+
.task-status-select:focus { border-color: var(--accent); }
|
|
185
|
+
|
|
186
|
+
/* ---- Sync Panel ---- */
|
|
187
|
+
.sync-status-box {
|
|
188
|
+
background: var(--bg-secondary); border: 1px solid var(--border);
|
|
189
|
+
border-radius: var(--radius); padding: 24px; margin-bottom: 20px;
|
|
190
|
+
display: flex; align-items: center; gap: 16px;
|
|
191
|
+
}
|
|
192
|
+
.sync-icon { width: 48px; height: 48px; border-radius: 12px; display: flex; align-items: center; justify-content: center; font-size: 22px; }
|
|
193
|
+
.sync-icon.connected { background: var(--green-bg); }
|
|
194
|
+
.sync-icon.disconnected { background: var(--bg-tertiary); }
|
|
195
|
+
.sync-info h3 { font-size: 1rem; font-weight: 600; margin-bottom: 4px; }
|
|
196
|
+
.sync-info p { font-size: 0.85rem; color: var(--text-secondary); }
|
|
197
|
+
.connect-btn {
|
|
198
|
+
margin-left: auto; background: var(--accent); color: #fff; border: none;
|
|
199
|
+
border-radius: var(--radius); padding: 10px 20px; font-size: 0.9rem;
|
|
200
|
+
cursor: pointer; font-weight: 500; transition: background var(--transition);
|
|
201
|
+
}
|
|
202
|
+
.connect-btn:hover { background: var(--accent-hover); }
|
|
203
|
+
.connect-instructions {
|
|
204
|
+
display: none; background: var(--bg-secondary); border: 1px solid var(--border);
|
|
205
|
+
border-radius: var(--radius); padding: 20px; margin-top: 16px;
|
|
206
|
+
}
|
|
207
|
+
.connect-instructions.visible { display: block; }
|
|
208
|
+
.connect-instructions h4 { margin-bottom: 12px; }
|
|
209
|
+
.connect-instructions ol { margin-left: 20px; color: var(--text-secondary); font-size: 0.88rem; }
|
|
210
|
+
.connect-instructions ol li { margin-bottom: 8px; }
|
|
211
|
+
.connect-instructions code {
|
|
212
|
+
background: var(--bg-primary); padding: 2px 6px; border-radius: var(--radius-sm);
|
|
213
|
+
font-family: "SF Mono", Monaco, "Cascadia Code", monospace; font-size: 0.84rem;
|
|
214
|
+
}
|
|
215
|
+
.sync-history { margin-top: 20px; }
|
|
216
|
+
.sync-history h4 { font-size: 0.9rem; font-weight: 600; margin-bottom: 12px; color: var(--text-secondary); }
|
|
217
|
+
.sync-history-empty { font-size: 0.85rem; color: var(--text-muted); padding: 16px; text-align: center; }
|
|
218
|
+
|
|
219
|
+
/* ---- Settings ---- */
|
|
220
|
+
.settings-section { margin-bottom: 28px; }
|
|
221
|
+
.settings-section h3 { font-size: 0.95rem; font-weight: 600; margin-bottom: 14px; }
|
|
222
|
+
.setting-row {
|
|
223
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
224
|
+
padding: 12px 0; border-bottom: 1px solid var(--border);
|
|
225
|
+
}
|
|
226
|
+
.setting-row:last-child { border-bottom: none; }
|
|
227
|
+
.setting-label { font-size: 0.9rem; }
|
|
228
|
+
.setting-label small { display: block; color: var(--text-muted); font-size: 0.78rem; margin-top: 2px; }
|
|
229
|
+
.setting-value { font-size: 0.88rem; color: var(--text-secondary); font-family: "SF Mono", Monaco, monospace; }
|
|
230
|
+
.toggle {
|
|
231
|
+
width: 44px; height: 24px; border-radius: 12px; background: var(--bg-tertiary);
|
|
232
|
+
border: 1px solid var(--border); position: relative; cursor: pointer;
|
|
233
|
+
transition: background var(--transition), border-color var(--transition);
|
|
234
|
+
}
|
|
235
|
+
.toggle.on { background: var(--accent); border-color: var(--accent); }
|
|
236
|
+
.toggle .knob {
|
|
237
|
+
width: 18px; height: 18px; border-radius: 50%; background: #fff;
|
|
238
|
+
position: absolute; top: 2px; left: 2px; transition: left var(--transition);
|
|
239
|
+
}
|
|
240
|
+
.toggle.on .knob { left: 22px; }
|
|
241
|
+
|
|
242
|
+
/* ---- Stats Bar ---- */
|
|
243
|
+
.stats-bar {
|
|
244
|
+
display: flex; align-items: center; gap: 16px; padding: 10px 24px;
|
|
245
|
+
background: var(--bg-secondary); border-top: 1px solid var(--border);
|
|
246
|
+
font-size: 0.78rem; color: var(--text-muted); flex-wrap: wrap;
|
|
247
|
+
}
|
|
248
|
+
.stats-bar .stat { display: flex; align-items: center; gap: 4px; }
|
|
249
|
+
.stats-bar .sep { color: var(--border); }
|
|
250
|
+
|
|
251
|
+
/* ---- Empty State ---- */
|
|
252
|
+
.empty-state {
|
|
253
|
+
text-align: center; padding: 60px 20px; color: var(--text-muted);
|
|
254
|
+
}
|
|
255
|
+
.empty-state .icon { font-size: 2.5rem; margin-bottom: 12px; opacity: 0.5; }
|
|
256
|
+
.empty-state p { font-size: 0.92rem; }
|
|
257
|
+
|
|
258
|
+
/* ---- Loading ---- */
|
|
259
|
+
.loading { text-align: center; padding: 40px; color: var(--text-muted); }
|
|
260
|
+
.spinner {
|
|
261
|
+
display: inline-block; width: 24px; height: 24px; border: 2px solid var(--border);
|
|
262
|
+
border-top-color: var(--accent); border-radius: 50%;
|
|
263
|
+
animation: spin 0.8s linear infinite; margin-bottom: 8px;
|
|
264
|
+
}
|
|
265
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
266
|
+
|
|
267
|
+
/* ---- Responsive ---- */
|
|
268
|
+
@media (max-width: 640px) {
|
|
269
|
+
.header { padding: 10px 16px; }
|
|
270
|
+
.tabs { padding: 0 12px; }
|
|
271
|
+
.content { padding: 16px; }
|
|
272
|
+
.stats-bar { padding: 8px 16px; gap: 10px; }
|
|
273
|
+
.cards-grid { grid-template-columns: 1fr; }
|
|
274
|
+
.sync-status-box { flex-direction: column; text-align: center; }
|
|
275
|
+
.connect-btn { margin-left: 0; width: 100%; }
|
|
276
|
+
}
|
|
277
|
+
</style>
|
|
278
|
+
</head>
|
|
279
|
+
<body>
|
|
280
|
+
|
|
281
|
+
<div class="app" id="app">
|
|
282
|
+
<!-- Header -->
|
|
283
|
+
<header class="header">
|
|
284
|
+
<h1><svg class="logo-svg" width="28" height="28" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient id="os" x1="30" y1="50" x2="490" y2="500" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#E0F2FE"/><stop offset=".35" stop-color="#7DD3FC"/><stop offset=".7" stop-color="#38BDF8"/><stop offset="1" stop-color="#0EA5E9"/></linearGradient><linearGradient id="is" x1="120" y1="80" x2="300" y2="260" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#F0F9FF"/><stop offset=".4" stop-color="#BAE6FD"/><stop offset="1" stop-color="#38BDF8"/></linearGradient><radialGradient id="df" cx="200" cy="168" r="20" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#F0F9FF"/><stop offset=".5" stop-color="#7DD3FC"/><stop offset="1" stop-color="#0EA5E9"/></radialGradient></defs><circle cx="262" cy="275" r="218" stroke="url(#os)" stroke-width="16" fill="none"/><circle cx="198" cy="168" r="68" stroke="url(#is)" stroke-width="13" fill="none"/><circle cx="198" cy="168" r="15" fill="url(#df)"/></svg> Awareness Local</h1>
|
|
285
|
+
<div class="header-right">
|
|
286
|
+
<div class="daemon-badge">
|
|
287
|
+
<span class="dot" id="daemon-dot"></span>
|
|
288
|
+
<span id="daemon-label">Checking...</span>
|
|
289
|
+
</div>
|
|
290
|
+
</div>
|
|
291
|
+
</header>
|
|
292
|
+
|
|
293
|
+
<!-- Tabs -->
|
|
294
|
+
<nav class="tabs" id="tabs">
|
|
295
|
+
<button class="tab-btn active" data-tab="memories">All Memories</button>
|
|
296
|
+
<button class="tab-btn" data-tab="knowledge">Knowledge Cards</button>
|
|
297
|
+
<button class="tab-btn" data-tab="tasks">Tasks</button>
|
|
298
|
+
<button class="tab-btn" data-tab="sync">Sync</button>
|
|
299
|
+
<button class="tab-btn" data-tab="settings">Settings</button>
|
|
300
|
+
</nav>
|
|
301
|
+
|
|
302
|
+
<!-- Main Content -->
|
|
303
|
+
<div class="main">
|
|
304
|
+
<div class="content">
|
|
305
|
+
|
|
306
|
+
<!-- Memories Panel -->
|
|
307
|
+
<div class="panel active" id="panel-memories">
|
|
308
|
+
<div class="search-bar">
|
|
309
|
+
<input type="text" id="memory-search" placeholder="Search memories..." />
|
|
310
|
+
<button onclick="searchMemories()">Search</button>
|
|
311
|
+
</div>
|
|
312
|
+
<div id="memory-list" class="memory-list">
|
|
313
|
+
<div class="loading"><div class="spinner"></div><p>Loading memories...</p></div>
|
|
314
|
+
</div>
|
|
315
|
+
</div>
|
|
316
|
+
|
|
317
|
+
<!-- Knowledge Panel -->
|
|
318
|
+
<div class="panel" id="panel-knowledge">
|
|
319
|
+
<div id="knowledge-list">
|
|
320
|
+
<div class="loading"><div class="spinner"></div><p>Loading knowledge cards...</p></div>
|
|
321
|
+
</div>
|
|
322
|
+
</div>
|
|
323
|
+
|
|
324
|
+
<!-- Tasks Panel -->
|
|
325
|
+
<div class="panel" id="panel-tasks">
|
|
326
|
+
<div class="search-bar">
|
|
327
|
+
<select id="task-filter" onchange="loadTasks()" style="background:var(--bg-tertiary);color:var(--text-primary);border:1px solid var(--border);border-radius:var(--radius);padding:10px 14px;font-size:0.9rem;outline:none;">
|
|
328
|
+
<option value="">All Tasks</option>
|
|
329
|
+
<option value="open">Open</option>
|
|
330
|
+
<option value="in_progress">In Progress</option>
|
|
331
|
+
<option value="done">Done</option>
|
|
332
|
+
</select>
|
|
333
|
+
</div>
|
|
334
|
+
<div id="task-list" class="task-list">
|
|
335
|
+
<div class="loading"><div class="spinner"></div><p>Loading tasks...</p></div>
|
|
336
|
+
</div>
|
|
337
|
+
</div>
|
|
338
|
+
|
|
339
|
+
<!-- Sync Panel -->
|
|
340
|
+
<div class="panel" id="panel-sync">
|
|
341
|
+
<div class="sync-status-box" id="sync-status-box">
|
|
342
|
+
<div class="sync-icon disconnected" id="sync-icon">☁</div>
|
|
343
|
+
<div class="sync-info">
|
|
344
|
+
<h3 id="sync-title">Checking sync status...</h3>
|
|
345
|
+
<p id="sync-desc">Loading...</p>
|
|
346
|
+
</div>
|
|
347
|
+
<button class="connect-btn" id="connect-btn" onclick="handleCloudConnect()">Connect to Cloud</button>
|
|
348
|
+
</div>
|
|
349
|
+
<div class="connect-instructions" id="connect-instructions">
|
|
350
|
+
<div id="auth-idle">
|
|
351
|
+
<p style="margin:0 0 12px 0;color:#94a3b8">One click to connect. No manual setup needed.</p>
|
|
352
|
+
<button class="connect-btn" style="width:100%" onclick="startDeviceAuth()">Start Connection</button>
|
|
353
|
+
</div>
|
|
354
|
+
<div id="auth-pending" style="display:none;text-align:center">
|
|
355
|
+
<p style="color:#94a3b8;margin:0 0 8px">Open this link in your browser and enter the code:</p>
|
|
356
|
+
<div style="font-size:2em;font-weight:700;letter-spacing:4px;color:#6366f1;margin:12px 0" id="auth-user-code">---</div>
|
|
357
|
+
<a id="auth-verify-link" href="#" target="_blank" style="color:#818cf8;word-break:break-all">Loading...</a>
|
|
358
|
+
<p style="color:#64748b;margin:12px 0 0;font-size:0.85em">Waiting for authorization... <span class="spinner" style="display:inline-block;width:14px;height:14px;vertical-align:middle"></span></p>
|
|
359
|
+
</div>
|
|
360
|
+
<div id="auth-select-memory" style="display:none">
|
|
361
|
+
<p style="color:#94a3b8;margin:0 0 8px">Select a memory to sync with:</p>
|
|
362
|
+
<select id="memory-select" style="width:100%;padding:8px;background:#1e293b;color:#e2e8f0;border:1px solid #334155;border-radius:6px;margin-bottom:12px"></select>
|
|
363
|
+
<button class="connect-btn" style="width:100%" onclick="confirmMemorySelection()">Connect</button>
|
|
364
|
+
</div>
|
|
365
|
+
<div id="auth-success" style="display:none;text-align:center">
|
|
366
|
+
<div style="font-size:2em;margin:8px 0">✓</div>
|
|
367
|
+
<p style="color:#4ade80;margin:0">Connected! Sync will start automatically.</p>
|
|
368
|
+
</div>
|
|
369
|
+
<div id="auth-error" style="display:none;text-align:center">
|
|
370
|
+
<p style="color:#f87171;margin:0" id="auth-error-msg">Connection failed.</p>
|
|
371
|
+
<button class="connect-btn" style="margin-top:12px" onclick="startDeviceAuth()">Retry</button>
|
|
372
|
+
</div>
|
|
373
|
+
</div>
|
|
374
|
+
<div class="sync-history" id="sync-history">
|
|
375
|
+
<h4>Sync History</h4>
|
|
376
|
+
<div class="sync-history-empty">No sync history yet. Connect to cloud to enable sync.</div>
|
|
377
|
+
</div>
|
|
378
|
+
</div>
|
|
379
|
+
|
|
380
|
+
<!-- Settings Panel -->
|
|
381
|
+
<div class="panel" id="panel-settings">
|
|
382
|
+
<div id="settings-content">
|
|
383
|
+
<div class="loading"><div class="spinner"></div><p>Loading settings...</p></div>
|
|
384
|
+
</div>
|
|
385
|
+
</div>
|
|
386
|
+
|
|
387
|
+
</div>
|
|
388
|
+
</div>
|
|
389
|
+
|
|
390
|
+
<!-- Stats Bar -->
|
|
391
|
+
<footer class="stats-bar" id="stats-bar">
|
|
392
|
+
<span class="stat" id="stat-memories">-- memories</span>
|
|
393
|
+
<span class="sep">·</span>
|
|
394
|
+
<span class="stat" id="stat-cards">-- cards</span>
|
|
395
|
+
<span class="sep">·</span>
|
|
396
|
+
<span class="stat" id="stat-tasks">-- tasks</span>
|
|
397
|
+
<span class="sep">·</span>
|
|
398
|
+
<span class="stat" id="stat-daemon">Daemon: checking</span>
|
|
399
|
+
<span class="sep">·</span>
|
|
400
|
+
<span class="stat" id="stat-cloud">Cloud: checking</span>
|
|
401
|
+
</footer>
|
|
402
|
+
</div>
|
|
403
|
+
|
|
404
|
+
<script>
|
|
405
|
+
/* ======================================================================
|
|
406
|
+
Awareness Local Dashboard — JavaScript Controller
|
|
407
|
+
====================================================================== */
|
|
408
|
+
|
|
409
|
+
const API = '/api/v1';
|
|
410
|
+
let currentTab = 'memories';
|
|
411
|
+
let memoriesData = [];
|
|
412
|
+
let knowledgeData = [];
|
|
413
|
+
let tasksData = [];
|
|
414
|
+
let configData = {};
|
|
415
|
+
let syncData = {};
|
|
416
|
+
let statsData = { totalMemories: 0, totalKnowledge: 0, totalTasks: 0, totalSessions: 0 };
|
|
417
|
+
|
|
418
|
+
// -------------------------------------------------------------------
|
|
419
|
+
// API Helpers
|
|
420
|
+
// -------------------------------------------------------------------
|
|
421
|
+
|
|
422
|
+
async function api(path, options) {
|
|
423
|
+
try {
|
|
424
|
+
const resp = await fetch(API + path, options);
|
|
425
|
+
if (!resp.ok) throw new Error('HTTP ' + resp.status);
|
|
426
|
+
return await resp.json();
|
|
427
|
+
} catch (err) {
|
|
428
|
+
console.error('[dashboard] API error:', path, err);
|
|
429
|
+
return null;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function formatDate(iso) {
|
|
434
|
+
if (!iso) return '';
|
|
435
|
+
try {
|
|
436
|
+
const d = new Date(iso);
|
|
437
|
+
const now = new Date();
|
|
438
|
+
const diffMs = now - d;
|
|
439
|
+
const diffMin = Math.floor(diffMs / 60000);
|
|
440
|
+
const diffHr = Math.floor(diffMs / 3600000);
|
|
441
|
+
const diffDay = Math.floor(diffMs / 86400000);
|
|
442
|
+
if (diffMin < 1) return 'just now';
|
|
443
|
+
if (diffMin < 60) return diffMin + 'm ago';
|
|
444
|
+
if (diffHr < 24) return diffHr + 'h ago';
|
|
445
|
+
if (diffDay < 7) return diffDay + 'd ago';
|
|
446
|
+
return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: d.getFullYear() !== now.getFullYear() ? 'numeric' : undefined });
|
|
447
|
+
} catch { return iso; }
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function escapeHtml(str) {
|
|
451
|
+
if (!str) return '';
|
|
452
|
+
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function parseTags(tags) {
|
|
456
|
+
if (Array.isArray(tags)) return tags;
|
|
457
|
+
if (typeof tags === 'string') {
|
|
458
|
+
try { const p = JSON.parse(tags); if (Array.isArray(p)) return p; } catch {}
|
|
459
|
+
}
|
|
460
|
+
return [];
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function confidenceClass(c) {
|
|
464
|
+
if (c >= 0.8) return 'high';
|
|
465
|
+
if (c >= 0.5) return 'mid';
|
|
466
|
+
return 'low';
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// -------------------------------------------------------------------
|
|
470
|
+
// Tab Navigation
|
|
471
|
+
// -------------------------------------------------------------------
|
|
472
|
+
|
|
473
|
+
document.getElementById('tabs').addEventListener('click', function(e) {
|
|
474
|
+
if (!e.target.classList.contains('tab-btn')) return;
|
|
475
|
+
var tab = e.target.getAttribute('data-tab');
|
|
476
|
+
if (tab === currentTab) return;
|
|
477
|
+
|
|
478
|
+
document.querySelectorAll('.tab-btn').forEach(function(b) { b.classList.remove('active'); });
|
|
479
|
+
document.querySelectorAll('.panel').forEach(function(p) { p.classList.remove('active'); });
|
|
480
|
+
e.target.classList.add('active');
|
|
481
|
+
document.getElementById('panel-' + tab).classList.add('active');
|
|
482
|
+
currentTab = tab;
|
|
483
|
+
|
|
484
|
+
// Lazy load on tab switch
|
|
485
|
+
if (tab === 'memories' && memoriesData.length === 0) loadMemories();
|
|
486
|
+
if (tab === 'knowledge' && knowledgeData.length === 0) loadKnowledge();
|
|
487
|
+
if (tab === 'tasks' && tasksData.length === 0) loadTasks();
|
|
488
|
+
if (tab === 'sync') loadSync();
|
|
489
|
+
if (tab === 'settings') loadSettings();
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
// -------------------------------------------------------------------
|
|
493
|
+
// Memories
|
|
494
|
+
// -------------------------------------------------------------------
|
|
495
|
+
|
|
496
|
+
async function loadMemories() {
|
|
497
|
+
var container = document.getElementById('memory-list');
|
|
498
|
+
container.innerHTML = '<div class="loading"><div class="spinner"></div><p>Loading memories...</p></div>';
|
|
499
|
+
|
|
500
|
+
var data = await api('/memories?limit=50&offset=0');
|
|
501
|
+
if (!data) {
|
|
502
|
+
container.innerHTML = '<div class="empty-state"><div class="icon">🗒</div><p>Could not load memories. Is the daemon running?</p></div>';
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
memoriesData = data.items || [];
|
|
507
|
+
renderMemories(memoriesData);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
async function searchMemories() {
|
|
511
|
+
var q = document.getElementById('memory-search').value.trim();
|
|
512
|
+
if (!q) { loadMemories(); return; }
|
|
513
|
+
|
|
514
|
+
var container = document.getElementById('memory-list');
|
|
515
|
+
container.innerHTML = '<div class="loading"><div class="spinner"></div><p>Searching...</p></div>';
|
|
516
|
+
|
|
517
|
+
var data = await api('/memories/search?q=' + encodeURIComponent(q));
|
|
518
|
+
if (!data) {
|
|
519
|
+
container.innerHTML = '<div class="empty-state"><p>Search failed.</p></div>';
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
renderMemories(data.items || []);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function renderMemories(items) {
|
|
527
|
+
var container = document.getElementById('memory-list');
|
|
528
|
+
if (items.length === 0) {
|
|
529
|
+
container.innerHTML = '<div class="empty-state"><div class="icon">🗒</div><p>No memories yet. Start a conversation with your AI agent to create memories.</p></div>';
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
var html = '';
|
|
534
|
+
for (var i = 0; i < items.length; i++) {
|
|
535
|
+
var m = items[i];
|
|
536
|
+
var tags = parseTags(m.tags);
|
|
537
|
+
var tagsHtml = tags.map(function(t) { return '<span class="badge badge-tag">' + escapeHtml(t) + '</span>'; }).join('');
|
|
538
|
+
// Title: prefer explicit title, then extract first sentence from content, lastly ID
|
|
539
|
+
var rawContent = m.content || m.fts_content || '';
|
|
540
|
+
var title = m.title;
|
|
541
|
+
if (!title || title === 'null') {
|
|
542
|
+
var firstSentence = rawContent.split(/[.\n!?。!?]/)[0].trim();
|
|
543
|
+
title = firstSentence.length > 80 ? firstSentence.substring(0, 77) + '...' : firstSentence;
|
|
544
|
+
}
|
|
545
|
+
title = escapeHtml(title || 'Untitled');
|
|
546
|
+
var contentPreview = escapeHtml(rawContent).substring(0, 500);
|
|
547
|
+
|
|
548
|
+
html += '<div class="memory-item" onclick="this.classList.toggle(\'expanded\')">'
|
|
549
|
+
+ '<div class="memory-item-header">'
|
|
550
|
+
+ '<span class="memory-item-title">' + title + '</span>'
|
|
551
|
+
+ '<span class="memory-item-date">' + formatDate(m.created_at) + '</span>'
|
|
552
|
+
+ '</div>'
|
|
553
|
+
+ '<div class="memory-item-meta">'
|
|
554
|
+
+ '<span class="badge badge-type">' + escapeHtml(m.type || 'memory') + '</span>'
|
|
555
|
+
+ tagsHtml
|
|
556
|
+
+ '</div>'
|
|
557
|
+
+ '<div class="memory-item-content">' + contentPreview + '</div>'
|
|
558
|
+
+ '</div>';
|
|
559
|
+
}
|
|
560
|
+
container.innerHTML = html;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// Search on Enter key
|
|
564
|
+
document.getElementById('memory-search').addEventListener('keydown', function(e) {
|
|
565
|
+
if (e.key === 'Enter') searchMemories();
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
// -------------------------------------------------------------------
|
|
569
|
+
// Knowledge Cards
|
|
570
|
+
// -------------------------------------------------------------------
|
|
571
|
+
|
|
572
|
+
async function loadKnowledge() {
|
|
573
|
+
var container = document.getElementById('knowledge-list');
|
|
574
|
+
container.innerHTML = '<div class="loading"><div class="spinner"></div><p>Loading knowledge cards...</p></div>';
|
|
575
|
+
|
|
576
|
+
var data = await api('/knowledge');
|
|
577
|
+
if (!data) {
|
|
578
|
+
container.innerHTML = '<div class="empty-state"><div class="icon">📚</div><p>Could not load knowledge cards.</p></div>';
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
knowledgeData = data.items || [];
|
|
583
|
+
renderKnowledge(knowledgeData);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function renderKnowledge(items) {
|
|
587
|
+
var container = document.getElementById('knowledge-list');
|
|
588
|
+
if (items.length === 0) {
|
|
589
|
+
container.innerHTML = '<div class="empty-state"><div class="icon">📚</div><p>No knowledge cards yet. Cards are extracted automatically from your memories.</p></div>';
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// Group by category
|
|
594
|
+
var groups = {};
|
|
595
|
+
for (var i = 0; i < items.length; i++) {
|
|
596
|
+
var cat = items[i].category || 'other';
|
|
597
|
+
if (!groups[cat]) groups[cat] = [];
|
|
598
|
+
groups[cat].push(items[i]);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
var categoryOrder = ['decision', 'problem_solution', 'workflow', 'key_point', 'pitfall', 'insight',
|
|
602
|
+
'personal_preference', 'important_detail', 'plan_intention', 'risk', 'other'];
|
|
603
|
+
|
|
604
|
+
var html = '';
|
|
605
|
+
for (var ci = 0; ci < categoryOrder.length; ci++) {
|
|
606
|
+
var catKey = categoryOrder[ci];
|
|
607
|
+
if (!groups[catKey]) continue;
|
|
608
|
+
var catItems = groups[catKey];
|
|
609
|
+
var catLabel = catKey.replace(/_/g, ' ').replace(/\b\w/g, function(c) { return c.toUpperCase(); });
|
|
610
|
+
|
|
611
|
+
html += '<div class="category-section">';
|
|
612
|
+
html += '<div class="category-title">' + escapeHtml(catLabel) + ' (' + catItems.length + ')</div>';
|
|
613
|
+
html += '<div class="cards-grid">';
|
|
614
|
+
|
|
615
|
+
for (var j = 0; j < catItems.length; j++) {
|
|
616
|
+
var c = catItems[j];
|
|
617
|
+
var tags = parseTags(c.tags);
|
|
618
|
+
var tagsHtml = tags.map(function(t) { return '<span class="badge badge-tag">' + escapeHtml(t) + '</span>'; }).join('');
|
|
619
|
+
var conf = c.confidence != null ? c.confidence : 0.8;
|
|
620
|
+
var confPct = Math.round(conf * 100);
|
|
621
|
+
|
|
622
|
+
html += '<div class="kc-card">'
|
|
623
|
+
+ '<div class="kc-card-title">' + escapeHtml(c.title || 'Untitled') + '</div>'
|
|
624
|
+
+ '<div class="kc-card-summary">' + escapeHtml(c.summary || '') + '</div>'
|
|
625
|
+
+ '<div class="kc-card-footer">'
|
|
626
|
+
+ '<span class="badge badge-' + escapeHtml(catKey) + '">' + escapeHtml(catLabel) + '</span>'
|
|
627
|
+
+ '<span class="badge badge-confidence ' + confidenceClass(conf) + '">' + confPct + '%</span>'
|
|
628
|
+
+ tagsHtml
|
|
629
|
+
+ '</div></div>';
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
html += '</div></div>';
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// Remaining categories not in order
|
|
636
|
+
var allCats = Object.keys(groups);
|
|
637
|
+
for (var k = 0; k < allCats.length; k++) {
|
|
638
|
+
if (categoryOrder.indexOf(allCats[k]) === -1) {
|
|
639
|
+
var catItems2 = groups[allCats[k]];
|
|
640
|
+
var catLabel2 = allCats[k].replace(/_/g, ' ').replace(/\b\w/g, function(c) { return c.toUpperCase(); });
|
|
641
|
+
html += '<div class="category-section"><div class="category-title">' + escapeHtml(catLabel2) + ' (' + catItems2.length + ')</div><div class="cards-grid">';
|
|
642
|
+
for (var l = 0; l < catItems2.length; l++) {
|
|
643
|
+
var c2 = catItems2[l];
|
|
644
|
+
html += '<div class="kc-card"><div class="kc-card-title">' + escapeHtml(c2.title || 'Untitled') + '</div>'
|
|
645
|
+
+ '<div class="kc-card-summary">' + escapeHtml(c2.summary || '') + '</div></div>';
|
|
646
|
+
}
|
|
647
|
+
html += '</div></div>';
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
container.innerHTML = html;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// -------------------------------------------------------------------
|
|
655
|
+
// Tasks
|
|
656
|
+
// -------------------------------------------------------------------
|
|
657
|
+
|
|
658
|
+
async function loadTasks() {
|
|
659
|
+
var container = document.getElementById('task-list');
|
|
660
|
+
container.innerHTML = '<div class="loading"><div class="spinner"></div><p>Loading tasks...</p></div>';
|
|
661
|
+
|
|
662
|
+
var filter = document.getElementById('task-filter').value;
|
|
663
|
+
var url = '/tasks';
|
|
664
|
+
if (filter) url += '?status=' + encodeURIComponent(filter);
|
|
665
|
+
|
|
666
|
+
var data = await api(url);
|
|
667
|
+
if (!data) {
|
|
668
|
+
container.innerHTML = '<div class="empty-state"><div class="icon">☑</div><p>Could not load tasks.</p></div>';
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
tasksData = data.items || [];
|
|
673
|
+
renderTasks(tasksData);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
function renderTasks(items) {
|
|
677
|
+
var container = document.getElementById('task-list');
|
|
678
|
+
if (items.length === 0) {
|
|
679
|
+
container.innerHTML = '<div class="empty-state"><div class="icon">☑</div><p>No tasks found. Tasks are extracted from your memories automatically.</p></div>';
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
var html = '';
|
|
684
|
+
for (var i = 0; i < items.length; i++) {
|
|
685
|
+
var t = items[i];
|
|
686
|
+
var priority = t.priority || 'medium';
|
|
687
|
+
|
|
688
|
+
html += '<div class="task-item">'
|
|
689
|
+
+ '<div class="priority-dot ' + escapeHtml(priority) + '" title="Priority: ' + escapeHtml(priority) + '"></div>'
|
|
690
|
+
+ '<div class="task-info">'
|
|
691
|
+
+ '<div class="task-title">' + escapeHtml(t.title || 'Untitled') + '</div>'
|
|
692
|
+
+ (t.description ? '<div class="task-desc">' + escapeHtml(t.description) + '</div>' : '')
|
|
693
|
+
+ '</div>'
|
|
694
|
+
+ '<select class="task-status-select" data-task-id="' + escapeHtml(t.id) + '" onchange="updateTaskStatus(this)">'
|
|
695
|
+
+ '<option value="open"' + (t.status === 'open' ? ' selected' : '') + '>Open</option>'
|
|
696
|
+
+ '<option value="in_progress"' + (t.status === 'in_progress' ? ' selected' : '') + '>In Progress</option>'
|
|
697
|
+
+ '<option value="done"' + (t.status === 'done' ? ' selected' : '') + '>Done</option>'
|
|
698
|
+
+ '</select>'
|
|
699
|
+
+ '</div>';
|
|
700
|
+
}
|
|
701
|
+
container.innerHTML = html;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
async function updateTaskStatus(select) {
|
|
705
|
+
var taskId = select.getAttribute('data-task-id');
|
|
706
|
+
var newStatus = select.value;
|
|
707
|
+
|
|
708
|
+
var result = await api('/tasks/' + encodeURIComponent(taskId), {
|
|
709
|
+
method: 'PUT',
|
|
710
|
+
headers: { 'Content-Type': 'application/json' },
|
|
711
|
+
body: JSON.stringify({ status: newStatus })
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
if (!result || result.error) {
|
|
715
|
+
alert('Failed to update task status');
|
|
716
|
+
loadTasks();
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// Refresh stats
|
|
721
|
+
loadStats();
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// -------------------------------------------------------------------
|
|
725
|
+
// Sync
|
|
726
|
+
// -------------------------------------------------------------------
|
|
727
|
+
|
|
728
|
+
async function loadSync() {
|
|
729
|
+
var data = await api('/sync/status');
|
|
730
|
+
var iconEl = document.getElementById('sync-icon');
|
|
731
|
+
var titleEl = document.getElementById('sync-title');
|
|
732
|
+
var descEl = document.getElementById('sync-desc');
|
|
733
|
+
var btnEl = document.getElementById('connect-btn');
|
|
734
|
+
|
|
735
|
+
if (!data) {
|
|
736
|
+
titleEl.textContent = 'Unable to check sync status';
|
|
737
|
+
descEl.textContent = 'Make sure the daemon is running.';
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
syncData = data;
|
|
742
|
+
|
|
743
|
+
if (data.cloud_enabled) {
|
|
744
|
+
iconEl.className = 'sync-icon connected';
|
|
745
|
+
iconEl.innerHTML = '⚡';
|
|
746
|
+
titleEl.textContent = 'Connected to Cloud';
|
|
747
|
+
descEl.textContent = 'Memory ID: ' + (data.memory_id || 'N/A');
|
|
748
|
+
if (data.last_push_at) descEl.textContent += ' | Last sync: ' + formatDate(data.last_push_at);
|
|
749
|
+
btnEl.textContent = 'Disconnect';
|
|
750
|
+
} else {
|
|
751
|
+
iconEl.className = 'sync-icon disconnected';
|
|
752
|
+
iconEl.innerHTML = '☁';
|
|
753
|
+
titleEl.textContent = 'Not Connected to Cloud';
|
|
754
|
+
descEl.textContent = 'Memories are stored locally only. Connect to cloud for backup and cross-device sync.';
|
|
755
|
+
btnEl.textContent = 'Connect to Cloud';
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
var _authApiKey = null;
|
|
760
|
+
|
|
761
|
+
function handleCloudConnect() {
|
|
762
|
+
if (syncData && syncData.cloud_enabled) {
|
|
763
|
+
// Disconnect
|
|
764
|
+
if (confirm('Disconnect from cloud? Local memories will remain.')) {
|
|
765
|
+
api('/cloud/disconnect', { method: 'POST' }).then(function() { loadSync(); });
|
|
766
|
+
}
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
var el = document.getElementById('connect-instructions');
|
|
770
|
+
el.classList.toggle('visible');
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
async function startDeviceAuth() {
|
|
774
|
+
document.getElementById('auth-idle').style.display = 'none';
|
|
775
|
+
document.getElementById('auth-pending').style.display = 'block';
|
|
776
|
+
document.getElementById('auth-error').style.display = 'none';
|
|
777
|
+
|
|
778
|
+
try {
|
|
779
|
+
var data = await api('/cloud/auth/start', { method: 'POST' });
|
|
780
|
+
if (!data || data.error || !data.user_code) {
|
|
781
|
+
throw new Error(data?.error || 'Failed to start device auth. Check your internet connection.');
|
|
782
|
+
}
|
|
783
|
+
document.getElementById('auth-user-code').textContent = data.user_code;
|
|
784
|
+
var link = document.getElementById('auth-verify-link');
|
|
785
|
+
link.href = data.verification_uri + '?code=' + data.user_code;
|
|
786
|
+
link.textContent = data.verification_uri;
|
|
787
|
+
|
|
788
|
+
// Open browser automatically
|
|
789
|
+
window.open(link.href, '_blank');
|
|
790
|
+
|
|
791
|
+
// Poll for authorization
|
|
792
|
+
var result = await api('/cloud/auth/poll', {
|
|
793
|
+
method: 'POST',
|
|
794
|
+
body: JSON.stringify({ device_code: data.device_code, interval: data.interval || 5 })
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
if (result.api_key) {
|
|
798
|
+
_authApiKey = result.api_key;
|
|
799
|
+
// Load memories for selection
|
|
800
|
+
document.getElementById('auth-pending').style.display = 'none';
|
|
801
|
+
document.getElementById('auth-select-memory').style.display = 'block';
|
|
802
|
+
|
|
803
|
+
var memories = await api('/cloud/memories?api_key=' + encodeURIComponent(result.api_key));
|
|
804
|
+
var select = document.getElementById('memory-select');
|
|
805
|
+
select.innerHTML = '';
|
|
806
|
+
(memories || []).forEach(function(m) {
|
|
807
|
+
var opt = document.createElement('option');
|
|
808
|
+
opt.value = m.id;
|
|
809
|
+
opt.textContent = m.name || m.id;
|
|
810
|
+
select.appendChild(opt);
|
|
811
|
+
});
|
|
812
|
+
if (!memories || memories.length === 0) {
|
|
813
|
+
select.innerHTML = '<option value="">No memories found — one will be created</option>';
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
} catch (err) {
|
|
817
|
+
document.getElementById('auth-pending').style.display = 'none';
|
|
818
|
+
document.getElementById('auth-error').style.display = 'block';
|
|
819
|
+
document.getElementById('auth-error-msg').textContent = err.message || 'Connection failed';
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
async function confirmMemorySelection() {
|
|
824
|
+
var memoryId = document.getElementById('memory-select').value;
|
|
825
|
+
try {
|
|
826
|
+
await api('/cloud/connect', {
|
|
827
|
+
method: 'POST',
|
|
828
|
+
body: JSON.stringify({ api_key: _authApiKey, memory_id: memoryId })
|
|
829
|
+
});
|
|
830
|
+
document.getElementById('auth-select-memory').style.display = 'none';
|
|
831
|
+
document.getElementById('auth-success').style.display = 'block';
|
|
832
|
+
setTimeout(function() { loadSync(); }, 2000);
|
|
833
|
+
} catch (err) {
|
|
834
|
+
document.getElementById('auth-select-memory').style.display = 'none';
|
|
835
|
+
document.getElementById('auth-error').style.display = 'block';
|
|
836
|
+
document.getElementById('auth-error-msg').textContent = err.message || 'Failed to save config';
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
// -------------------------------------------------------------------
|
|
841
|
+
// Settings
|
|
842
|
+
// -------------------------------------------------------------------
|
|
843
|
+
|
|
844
|
+
async function loadSettings() {
|
|
845
|
+
var container = document.getElementById('settings-content');
|
|
846
|
+
var data = await api('/config');
|
|
847
|
+
if (!data) {
|
|
848
|
+
container.innerHTML = '<div class="empty-state"><p>Could not load settings.</p></div>';
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
configData = data;
|
|
853
|
+
var daemon = data.daemon || {};
|
|
854
|
+
var device = data.device || {};
|
|
855
|
+
var cloud = data.cloud || {};
|
|
856
|
+
var embedding = data.embedding || {};
|
|
857
|
+
var gitSync = data.git_sync || {};
|
|
858
|
+
|
|
859
|
+
var html = '';
|
|
860
|
+
|
|
861
|
+
// Daemon section
|
|
862
|
+
html += '<div class="settings-section"><h3>Daemon</h3>';
|
|
863
|
+
html += settingRow('Port', daemon.port || 37800);
|
|
864
|
+
html += settingRow('Log Level', daemon.log_level || 'info');
|
|
865
|
+
html += settingRow('Auto Start', daemon.auto_start ? 'Yes' : 'No');
|
|
866
|
+
html += '</div>';
|
|
867
|
+
|
|
868
|
+
// Device section
|
|
869
|
+
html += '<div class="settings-section"><h3>Device</h3>';
|
|
870
|
+
html += settingRow('Device ID', device.id || 'N/A');
|
|
871
|
+
html += settingRow('Device Name', device.name || 'N/A');
|
|
872
|
+
html += '</div>';
|
|
873
|
+
|
|
874
|
+
// Embedding section
|
|
875
|
+
html += '<div class="settings-section"><h3>Embedding</h3>';
|
|
876
|
+
html += settingRowToggle('Language', 'embedding_language',
|
|
877
|
+
embedding.language === 'multilingual',
|
|
878
|
+
'English', 'Multilingual',
|
|
879
|
+
'Switch between English-optimized and multilingual embedding model');
|
|
880
|
+
html += settingRow('Model', embedding.model_id || 'auto-detect');
|
|
881
|
+
html += '</div>';
|
|
882
|
+
|
|
883
|
+
// Cloud section
|
|
884
|
+
html += '<div class="settings-section"><h3>Cloud Sync</h3>';
|
|
885
|
+
html += settingRow('Status', cloud.enabled ? 'Connected' : 'Not connected');
|
|
886
|
+
html += settingRow('API Base', cloud.api_base || 'N/A');
|
|
887
|
+
html += settingRow('Memory ID', cloud.memory_id || 'N/A');
|
|
888
|
+
html += settingRow('Auto Sync', cloud.auto_sync ? 'Yes' : 'No');
|
|
889
|
+
html += settingRow('Sync Interval', (cloud.sync_interval_min || 5) + ' min');
|
|
890
|
+
html += '</div>';
|
|
891
|
+
|
|
892
|
+
// Git section
|
|
893
|
+
html += '<div class="settings-section"><h3>Git Sync</h3>';
|
|
894
|
+
html += settingRowToggle('Auto Commit', 'git_auto_commit',
|
|
895
|
+
gitSync.auto_commit === true,
|
|
896
|
+
'Off', 'On',
|
|
897
|
+
'Automatically commit .awareness/ changes to git');
|
|
898
|
+
html += settingRow('Branch', gitSync.branch || 'current branch');
|
|
899
|
+
html += '</div>';
|
|
900
|
+
|
|
901
|
+
container.innerHTML = html;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
function settingRow(label, value) {
|
|
905
|
+
return '<div class="setting-row"><div class="setting-label">' + escapeHtml(label) + '</div>'
|
|
906
|
+
+ '<div class="setting-value">' + escapeHtml(String(value)) + '</div></div>';
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
function settingRowToggle(label, id, isOn, offLabel, onLabel, desc) {
|
|
910
|
+
return '<div class="setting-row"><div class="setting-label">' + escapeHtml(label)
|
|
911
|
+
+ (desc ? '<small>' + escapeHtml(desc) + '</small>' : '') + '</div>'
|
|
912
|
+
+ '<div style="display:flex;align-items:center;gap:8px;">'
|
|
913
|
+
+ '<span style="font-size:0.82rem;color:var(--text-muted)">' + escapeHtml(isOn ? onLabel : offLabel) + '</span>'
|
|
914
|
+
+ '<div class="toggle' + (isOn ? ' on' : '') + '" data-setting="' + escapeHtml(id) + '" onclick="toggleSetting(this)">'
|
|
915
|
+
+ '<div class="knob"></div></div></div></div>';
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
async function toggleSetting(el) {
|
|
919
|
+
var settingKey = el.getAttribute('data-setting');
|
|
920
|
+
var isOn = el.classList.contains('on');
|
|
921
|
+
var newVal = !isOn;
|
|
922
|
+
|
|
923
|
+
var payload = {};
|
|
924
|
+
if (settingKey === 'embedding_language') {
|
|
925
|
+
payload = { embedding: { language: newVal ? 'multilingual' : 'english' } };
|
|
926
|
+
} else if (settingKey === 'git_auto_commit') {
|
|
927
|
+
payload = { git_sync: { auto_commit: newVal } };
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
var result = await api('/config', {
|
|
931
|
+
method: 'PUT',
|
|
932
|
+
headers: { 'Content-Type': 'application/json' },
|
|
933
|
+
body: JSON.stringify(payload)
|
|
934
|
+
});
|
|
935
|
+
|
|
936
|
+
if (result && !result.error) {
|
|
937
|
+
el.classList.toggle('on');
|
|
938
|
+
// Update label next to toggle
|
|
939
|
+
var labelSpan = el.previousElementSibling;
|
|
940
|
+
if (settingKey === 'embedding_language') {
|
|
941
|
+
labelSpan.textContent = newVal ? 'Multilingual' : 'English';
|
|
942
|
+
} else if (settingKey === 'git_auto_commit') {
|
|
943
|
+
labelSpan.textContent = newVal ? 'On' : 'Off';
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
// -------------------------------------------------------------------
|
|
949
|
+
// Stats Bar
|
|
950
|
+
// -------------------------------------------------------------------
|
|
951
|
+
|
|
952
|
+
async function loadStats() {
|
|
953
|
+
var data = await api('/stats');
|
|
954
|
+
if (!data) return;
|
|
955
|
+
statsData = data;
|
|
956
|
+
|
|
957
|
+
document.getElementById('stat-memories').textContent = (data.totalMemories || 0) + ' memories';
|
|
958
|
+
document.getElementById('stat-cards').textContent = (data.totalKnowledge || 0) + ' cards';
|
|
959
|
+
document.getElementById('stat-tasks').textContent = (data.totalTasks || 0) + ' tasks';
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
async function checkDaemon() {
|
|
963
|
+
var dot = document.getElementById('daemon-dot');
|
|
964
|
+
var label = document.getElementById('daemon-label');
|
|
965
|
+
var statDaemon = document.getElementById('stat-daemon');
|
|
966
|
+
|
|
967
|
+
try {
|
|
968
|
+
var resp = await fetch('/healthz');
|
|
969
|
+
if (resp.ok) {
|
|
970
|
+
var d = await resp.json();
|
|
971
|
+
dot.className = 'dot ok';
|
|
972
|
+
label.textContent = 'Running (' + (d.uptime || 0) + 's)';
|
|
973
|
+
statDaemon.textContent = 'Daemon: running';
|
|
974
|
+
|
|
975
|
+
// Cloud status
|
|
976
|
+
var statCloud = document.getElementById('stat-cloud');
|
|
977
|
+
var cfg = await api('/config');
|
|
978
|
+
if (cfg && cfg.cloud && cfg.cloud.enabled) {
|
|
979
|
+
statCloud.textContent = 'Cloud: synced';
|
|
980
|
+
} else {
|
|
981
|
+
statCloud.textContent = 'Cloud: not synced';
|
|
982
|
+
}
|
|
983
|
+
} else {
|
|
984
|
+
throw new Error();
|
|
985
|
+
}
|
|
986
|
+
} catch (e) {
|
|
987
|
+
dot.className = 'dot err';
|
|
988
|
+
label.textContent = 'Offline';
|
|
989
|
+
document.getElementById('stat-daemon').textContent = 'Daemon: offline';
|
|
990
|
+
document.getElementById('stat-cloud').textContent = 'Cloud: N/A';
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
// -------------------------------------------------------------------
|
|
995
|
+
// Init
|
|
996
|
+
// -------------------------------------------------------------------
|
|
997
|
+
|
|
998
|
+
async function init() {
|
|
999
|
+
await Promise.all([
|
|
1000
|
+
checkDaemon(),
|
|
1001
|
+
loadStats(),
|
|
1002
|
+
loadMemories()
|
|
1003
|
+
]);
|
|
1004
|
+
|
|
1005
|
+
// Auto-refresh stats every 30 seconds
|
|
1006
|
+
setInterval(function() {
|
|
1007
|
+
loadStats();
|
|
1008
|
+
checkDaemon();
|
|
1009
|
+
}, 30000);
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
init();
|
|
1013
|
+
</script>
|
|
1014
|
+
</body>
|
|
1015
|
+
</html>
|