total-recall-brain 3.2.2 → 3.2.3
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/extension/content-script.js +15 -0
- package/extension/lib/brain-client.js +21 -3
- package/extension/popup/popup.css +6 -0
- package/extension/popup/popup.js +18 -0
- package/extension/sidepanel/sidepanel.css +426 -29
- package/extension/sidepanel/sidepanel.html +118 -20
- package/extension/sidepanel/sidepanel.js +474 -23
- package/package.json +1 -1
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
// ---- DOM refs ----
|
|
7
7
|
const dot = document.getElementById('connection-dot');
|
|
8
|
+
const activeBrainBadge = document.getElementById('active-brain-badge');
|
|
8
9
|
const searchInput = document.getElementById('search-input');
|
|
9
10
|
const memoriesList = document.getElementById('memories-list');
|
|
10
11
|
const btnRemember = document.getElementById('btn-remember');
|
|
@@ -15,6 +16,59 @@
|
|
|
15
16
|
const btnSaveNote = document.getElementById('btn-save-note');
|
|
16
17
|
const btnCancelNote = document.getElementById('btn-cancel-note');
|
|
17
18
|
const researchList = document.getElementById('research-list');
|
|
19
|
+
const btnRefreshResearch = document.getElementById('btn-refresh-research');
|
|
20
|
+
|
|
21
|
+
// Chat tab DOM refs
|
|
22
|
+
const chatMessages = document.getElementById('chat-messages');
|
|
23
|
+
const chatInput = document.getElementById('chat-input');
|
|
24
|
+
const btnSendChat = document.getElementById('btn-send-chat');
|
|
25
|
+
const chatGroundingToggle = document.getElementById('chat-grounding-toggle');
|
|
26
|
+
const btnSuggestSummarize = document.getElementById('btn-suggest-summarize');
|
|
27
|
+
const btnSuggestRelated = document.getElementById('btn-suggest-related');
|
|
28
|
+
|
|
29
|
+
// Settings tab DOM refs
|
|
30
|
+
const settingsBrainSelector = document.getElementById('settings-brain-selector');
|
|
31
|
+
const settingsTrackingToggle = document.getElementById('settings-tracking-toggle');
|
|
32
|
+
const btnBlockDomain = document.getElementById('btn-block-domain');
|
|
33
|
+
const btnRecompileBrain = document.getElementById('btn-recompile-brain');
|
|
34
|
+
|
|
35
|
+
// State variables
|
|
36
|
+
let chatHistory = [];
|
|
37
|
+
let activeBrainId = 'global';
|
|
38
|
+
|
|
39
|
+
// ---- Tab Switching ----
|
|
40
|
+
const tabButtons = document.querySelectorAll('.tab-btn');
|
|
41
|
+
const tabPanels = document.querySelectorAll('.tab-panel');
|
|
42
|
+
|
|
43
|
+
tabButtons.forEach(btn => {
|
|
44
|
+
btn.addEventListener('click', () => {
|
|
45
|
+
const targetTab = btn.getAttribute('data-tab');
|
|
46
|
+
|
|
47
|
+
// Update active states in buttons
|
|
48
|
+
tabButtons.forEach(b => b.classList.remove('active'));
|
|
49
|
+
btn.classList.add('active');
|
|
50
|
+
|
|
51
|
+
// Update active panel
|
|
52
|
+
tabPanels.forEach(panel => {
|
|
53
|
+
if (panel.id === `tab-${targetTab}`) {
|
|
54
|
+
panel.classList.add('active');
|
|
55
|
+
} else {
|
|
56
|
+
panel.classList.remove('active');
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// On-demand panel loading triggers
|
|
61
|
+
if (targetTab === 'memories') {
|
|
62
|
+
if (!searchInput.value.trim()) {
|
|
63
|
+
loadRecentCaptures();
|
|
64
|
+
}
|
|
65
|
+
} else if (targetTab === 'research') {
|
|
66
|
+
fetchResearch();
|
|
67
|
+
} else if (targetTab === 'settings') {
|
|
68
|
+
fetchBrains();
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
});
|
|
18
72
|
|
|
19
73
|
// ---- Connection check ----
|
|
20
74
|
async function checkConnection() {
|
|
@@ -32,7 +86,7 @@
|
|
|
32
86
|
function debounceSearch(query) {
|
|
33
87
|
clearTimeout(searchTimer);
|
|
34
88
|
if (!query.trim()) {
|
|
35
|
-
|
|
89
|
+
loadRecentCaptures();
|
|
36
90
|
return;
|
|
37
91
|
}
|
|
38
92
|
searchTimer = setTimeout(() => {
|
|
@@ -44,30 +98,57 @@
|
|
|
44
98
|
memoriesList.innerHTML = '<div class="memories-empty">Searching…</div>';
|
|
45
99
|
try {
|
|
46
100
|
const response = await new Promise((resolve, reject) => {
|
|
47
|
-
chrome.runtime.sendMessage({ type: 'QUERY_BRAIN', query, topK: 10 }, (res) => {
|
|
101
|
+
chrome.runtime.sendMessage({ type: 'QUERY_BRAIN', query, topK: 10, brainId: activeBrainId }, (res) => {
|
|
48
102
|
if (chrome.runtime.lastError) return reject(new Error(chrome.runtime.lastError.message));
|
|
49
103
|
resolve(res);
|
|
50
104
|
});
|
|
51
105
|
});
|
|
52
|
-
renderMemories(response.memories || []);
|
|
106
|
+
renderMemories(response.memories || [], false);
|
|
53
107
|
} catch (err) {
|
|
54
108
|
memoriesList.innerHTML = `<div class="memories-empty">Error: ${err.message}</div>`;
|
|
55
109
|
}
|
|
56
110
|
}
|
|
57
111
|
|
|
112
|
+
// ---- Recent Captures ----
|
|
113
|
+
async function loadRecentCaptures() {
|
|
114
|
+
memoriesList.innerHTML = '<div class="memories-empty">Loading recent captures…</div>';
|
|
115
|
+
try {
|
|
116
|
+
let url = '/api/memory';
|
|
117
|
+
if (activeBrainId && activeBrainId !== 'global') {
|
|
118
|
+
url = `/api/brains/${activeBrainId}/nodes`;
|
|
119
|
+
}
|
|
120
|
+
const response = await self.BrainClient.brainFetch(url);
|
|
121
|
+
const nodes = Array.isArray(response) ? response : (response.nodes || []);
|
|
122
|
+
|
|
123
|
+
// Sort by creation or update timestamp descending
|
|
124
|
+
const sorted = nodes.sort((a, b) => new Date(b.created || b.created_at || 0) - new Date(a.created || a.created_at || 0));
|
|
125
|
+
const recent = sorted.slice(0, 5);
|
|
126
|
+
|
|
127
|
+
if (!recent.length) {
|
|
128
|
+
memoriesList.innerHTML = '<div class="memories-empty">No memories in this brain context yet. Capture pages to see them here!</div>';
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
renderMemories(recent, true);
|
|
132
|
+
} catch (err) {
|
|
133
|
+
memoriesList.innerHTML = `<div class="memories-empty">Could not load recent captures: ${err.message}</div>`;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
58
137
|
// ---- Render memories ----
|
|
59
|
-
function renderMemories(memories) {
|
|
138
|
+
function renderMemories(memories, isRecent = false) {
|
|
60
139
|
if (!memories.length) {
|
|
61
140
|
memoriesList.innerHTML = '<div class="memories-empty">No memories found</div>';
|
|
62
141
|
return;
|
|
63
142
|
}
|
|
64
|
-
|
|
143
|
+
const headerHtml = isRecent ? '<h4 style="font-size: 11px; text-transform: uppercase; color: #585b70; letter-spacing: 0.5px; margin: 4px 0 10px 0;">Recent Captures</h4>' : '';
|
|
144
|
+
|
|
145
|
+
memoriesList.innerHTML = headerHtml + memories.map((m) => {
|
|
65
146
|
const title = m.title || m.slug || 'Untitled';
|
|
66
|
-
const excerpt = m.content || m.excerpt || '';
|
|
147
|
+
const excerpt = m.content || m.excerpt || m.body || '';
|
|
67
148
|
const category = m.category || 'memory';
|
|
68
|
-
const time = m.created_at ? formatTime(m.created_at) : '';
|
|
149
|
+
const time = m.created || m.created_at ? formatTime(m.created || m.created_at) : '';
|
|
69
150
|
return `
|
|
70
|
-
<div class="memory-card">
|
|
151
|
+
<div class="memory-card" data-slug="${m.slug}">
|
|
71
152
|
<div class="card-header">
|
|
72
153
|
<span class="card-title">${escapeHtml(title)}</span>
|
|
73
154
|
<span class="card-badge">${escapeHtml(category)}</span>
|
|
@@ -77,6 +158,15 @@
|
|
|
77
158
|
</div>
|
|
78
159
|
`;
|
|
79
160
|
}).join('');
|
|
161
|
+
|
|
162
|
+
// Bind cards to open remote memory explorer
|
|
163
|
+
document.querySelectorAll('.memory-card').forEach(card => {
|
|
164
|
+
card.addEventListener('click', async () => {
|
|
165
|
+
const slug = card.getAttribute('data-slug');
|
|
166
|
+
const config = await self.BrainClient.getConfig();
|
|
167
|
+
chrome.tabs.create({ url: `${config.brainUrl}/memory?slug=${slug}` });
|
|
168
|
+
});
|
|
169
|
+
});
|
|
80
170
|
}
|
|
81
171
|
|
|
82
172
|
// ---- Quick actions ----
|
|
@@ -86,6 +176,11 @@
|
|
|
86
176
|
}
|
|
87
177
|
|
|
88
178
|
btnRemember.addEventListener('click', async () => {
|
|
179
|
+
if (btnRemember.disabled) return;
|
|
180
|
+
const originalText = btnRemember.innerHTML;
|
|
181
|
+
btnRemember.innerHTML = '⏳ Remembering...';
|
|
182
|
+
btnRemember.disabled = true;
|
|
183
|
+
btnRemember.classList.add('loading');
|
|
89
184
|
try {
|
|
90
185
|
const tab = await getCurrentTab();
|
|
91
186
|
await sendShare({
|
|
@@ -95,12 +190,22 @@
|
|
|
95
190
|
source: 'chrome-extension-sidepanel'
|
|
96
191
|
});
|
|
97
192
|
showToast('Page remembered!');
|
|
193
|
+
loadRecentCaptures();
|
|
98
194
|
} catch (err) {
|
|
99
195
|
showToast('Error: ' + err.message, true);
|
|
196
|
+
} finally {
|
|
197
|
+
btnRemember.innerHTML = originalText;
|
|
198
|
+
btnRemember.disabled = false;
|
|
199
|
+
btnRemember.classList.remove('loading');
|
|
100
200
|
}
|
|
101
201
|
});
|
|
102
202
|
|
|
103
203
|
btnResearch.addEventListener('click', async () => {
|
|
204
|
+
if (btnResearch.disabled) return;
|
|
205
|
+
const originalText = btnResearch.innerHTML;
|
|
206
|
+
btnResearch.innerHTML = '⏳ Researching...';
|
|
207
|
+
btnResearch.disabled = true;
|
|
208
|
+
btnResearch.classList.add('loading');
|
|
104
209
|
try {
|
|
105
210
|
const tab = await getCurrentTab();
|
|
106
211
|
await sendShare({
|
|
@@ -112,6 +217,10 @@
|
|
|
112
217
|
showToast('Research queued!');
|
|
113
218
|
} catch (err) {
|
|
114
219
|
showToast('Error: ' + err.message, true);
|
|
220
|
+
} finally {
|
|
221
|
+
btnResearch.innerHTML = originalText;
|
|
222
|
+
btnResearch.disabled = false;
|
|
223
|
+
btnResearch.classList.remove('loading');
|
|
115
224
|
}
|
|
116
225
|
});
|
|
117
226
|
|
|
@@ -130,7 +239,11 @@
|
|
|
130
239
|
|
|
131
240
|
btnSaveNote.addEventListener('click', async () => {
|
|
132
241
|
const text = noteInput.value.trim();
|
|
133
|
-
if (!text) return;
|
|
242
|
+
if (!text || btnSaveNote.disabled) return;
|
|
243
|
+
const originalText = btnSaveNote.innerHTML;
|
|
244
|
+
btnSaveNote.innerHTML = '⏳ Saving...';
|
|
245
|
+
btnSaveNote.disabled = true;
|
|
246
|
+
btnSaveNote.classList.add('loading');
|
|
134
247
|
try {
|
|
135
248
|
const tab = await getCurrentTab();
|
|
136
249
|
await sendShare({
|
|
@@ -144,8 +257,13 @@
|
|
|
144
257
|
noteInput.value = '';
|
|
145
258
|
noteArea.classList.add('hidden');
|
|
146
259
|
showToast('Note saved!');
|
|
260
|
+
loadRecentCaptures();
|
|
147
261
|
} catch (err) {
|
|
148
262
|
showToast('Error: ' + err.message, true);
|
|
263
|
+
} finally {
|
|
264
|
+
btnSaveNote.innerHTML = originalText;
|
|
265
|
+
btnSaveNote.disabled = false;
|
|
266
|
+
btnSaveNote.classList.remove('loading');
|
|
149
267
|
}
|
|
150
268
|
});
|
|
151
269
|
|
|
@@ -160,27 +278,347 @@
|
|
|
160
278
|
});
|
|
161
279
|
}
|
|
162
280
|
|
|
163
|
-
//
|
|
281
|
+
// ==================== INTERACTIVE AI CHAT ====================
|
|
282
|
+
|
|
283
|
+
function addChatBubble(sender, text, isSystem = false) {
|
|
284
|
+
const bubble = document.createElement('div');
|
|
285
|
+
if (isSystem) {
|
|
286
|
+
bubble.className = 'chat-bubble system';
|
|
287
|
+
} else {
|
|
288
|
+
bubble.className = `chat-bubble ${sender === 'user' ? 'user' : 'assistant'}`;
|
|
289
|
+
}
|
|
290
|
+
bubble.textContent = text;
|
|
291
|
+
chatMessages.appendChild(bubble);
|
|
292
|
+
chatMessages.scrollTop = chatMessages.scrollHeight;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
async function getActivePageContext() {
|
|
296
|
+
try {
|
|
297
|
+
const tab = await getCurrentTab();
|
|
298
|
+
if (!tab || !tab.id) return null;
|
|
299
|
+
|
|
300
|
+
// Ask the content script on the tab to give us the selection + main inner text
|
|
301
|
+
const pageInfo = await new Promise((resolve) => {
|
|
302
|
+
chrome.tabs.sendMessage(tab.id, { type: 'GET_PAGE_TEXT' }, (response) => {
|
|
303
|
+
if (chrome.runtime.lastError || !response) {
|
|
304
|
+
resolve({ url: tab.url, title: tab.title, selection: '', pageText: '' });
|
|
305
|
+
} else {
|
|
306
|
+
resolve(response);
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
return pageInfo;
|
|
311
|
+
} catch {
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
async function sendChatMessage(rawText) {
|
|
317
|
+
const text = rawText.trim();
|
|
318
|
+
if (!text || btnSendChat.disabled) return;
|
|
319
|
+
|
|
320
|
+
addChatBubble('user', text);
|
|
321
|
+
chatInput.value = '';
|
|
322
|
+
|
|
323
|
+
// Disable inputs during processing
|
|
324
|
+
btnSendChat.disabled = true;
|
|
325
|
+
chatInput.disabled = true;
|
|
326
|
+
|
|
327
|
+
// Append user message to history
|
|
328
|
+
chatHistory.push({ role: 'user', content: text });
|
|
329
|
+
|
|
330
|
+
// Show a small thinking loader
|
|
331
|
+
const thinkingBubble = document.createElement('div');
|
|
332
|
+
thinkingBubble.className = 'chat-bubble assistant thinking';
|
|
333
|
+
thinkingBubble.textContent = '⏳ Thinking...';
|
|
334
|
+
chatMessages.appendChild(thinkingBubble);
|
|
335
|
+
chatMessages.scrollTop = chatMessages.scrollHeight;
|
|
336
|
+
|
|
337
|
+
try {
|
|
338
|
+
let finalMessages = [...chatHistory];
|
|
339
|
+
const isGroundingEnabled = chatGroundingToggle.checked;
|
|
340
|
+
|
|
341
|
+
if (isGroundingEnabled) {
|
|
342
|
+
const context = await getActivePageContext();
|
|
343
|
+
if (context) {
|
|
344
|
+
const selectionPart = context.selection ? `\n[User Selected text:\n"${context.selection}"]` : '';
|
|
345
|
+
const bodyTextPart = context.pageText ? `\n[Webpage Inner Text:\n${context.pageText.slice(0, 4000)}]` : ''; // Limit to 4k chars context
|
|
346
|
+
|
|
347
|
+
const systemContextMessage = {
|
|
348
|
+
role: 'system',
|
|
349
|
+
content: `You are a sovereign web assistant for Total Recall. The user is actively viewing a web page:
|
|
350
|
+
URL: ${context.url}
|
|
351
|
+
Title: ${context.title}${selectionPart}${bodyTextPart}
|
|
352
|
+
Provide helpful analysis grounded on both the user's brain memory and this page content.`
|
|
353
|
+
};
|
|
354
|
+
// Prepend system instruction for the completions endpoint
|
|
355
|
+
finalMessages.unshift(systemContextMessage);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const completions = await self.BrainClient.chat(finalMessages, {
|
|
360
|
+
brainId: activeBrainId,
|
|
361
|
+
model: 'gemini'
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
// Remove thinking loader
|
|
365
|
+
thinkingBubble.remove();
|
|
366
|
+
|
|
367
|
+
const reply = completions.choices?.[0]?.message?.content || '(empty response)';
|
|
368
|
+
addChatBubble('assistant', reply);
|
|
369
|
+
|
|
370
|
+
// Append assistant answer to local history
|
|
371
|
+
chatHistory.push({ role: 'assistant', content: reply });
|
|
372
|
+
} catch (err) {
|
|
373
|
+
thinkingBubble.remove();
|
|
374
|
+
addChatBubble('system', `Error: ${err.message}`, true);
|
|
375
|
+
} finally {
|
|
376
|
+
btnSendChat.disabled = false;
|
|
377
|
+
chatInput.disabled = false;
|
|
378
|
+
chatInput.focus();
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
btnSendChat.addEventListener('click', () => sendChatMessage(chatInput.value));
|
|
383
|
+
|
|
384
|
+
chatInput.addEventListener('keydown', (e) => {
|
|
385
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
386
|
+
e.preventDefault();
|
|
387
|
+
sendChatMessage(chatInput.value);
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
// Suggestion click listeners
|
|
392
|
+
btnSuggestSummarize.addEventListener('click', () => {
|
|
393
|
+
chatGroundingToggle.checked = true;
|
|
394
|
+
sendChatMessage('Please summarize the current webpage and highlight the key takeaways.');
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
btnSuggestRelated.addEventListener('click', () => {
|
|
398
|
+
chatGroundingToggle.checked = true;
|
|
399
|
+
sendChatMessage('Are there any related memories, rules, or invariants in my brain for this webpage context?');
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
// ==================== BACKGROUND RESEARCH ====================
|
|
404
|
+
|
|
164
405
|
async function fetchResearch() {
|
|
165
406
|
try {
|
|
166
|
-
const data = await self.BrainClient.brainFetch('/api/research
|
|
167
|
-
const projects = data.projects || data.results || [];
|
|
407
|
+
const data = await self.BrainClient.brainFetch('/api/research');
|
|
408
|
+
const projects = data.projects || data.items || data.results || [];
|
|
168
409
|
if (!projects.length) {
|
|
169
|
-
researchList.innerHTML = '<div class="research-empty">No
|
|
410
|
+
researchList.innerHTML = '<div class="research-empty">No research projects found</div>';
|
|
170
411
|
return;
|
|
171
412
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
413
|
+
|
|
414
|
+
// Filter out completed ones, show in_progress and pending at the top
|
|
415
|
+
const sorted = projects.sort((a, b) => {
|
|
416
|
+
if (a.status === 'in_progress' && b.status !== 'in_progress') return -1;
|
|
417
|
+
if (a.status !== 'in_progress' && b.status === 'in_progress') return 1;
|
|
418
|
+
return new Date(b.created_at || 0) - new Date(a.created_at || 0);
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
researchList.innerHTML = sorted.map((p) => {
|
|
422
|
+
const isRunning = p.status === 'in_progress' || p.status === 'pending';
|
|
423
|
+
const cancelBtnHtml = isRunning
|
|
424
|
+
? `<button class="steer-btn btn-cancel-res" data-id="${p.id}" title="Cancel Research">✕</button>`
|
|
425
|
+
: '';
|
|
426
|
+
const spinHtml = isRunning ? '<div class="spinner"></div>' : '📌';
|
|
427
|
+
return `
|
|
428
|
+
<div class="research-item">
|
|
429
|
+
${spinHtml}
|
|
430
|
+
<span class="topic" title="${escapeHtml(p.topic || p.title || 'Untitled')}">${escapeHtml(p.topic || p.title || 'Untitled')}</span>
|
|
431
|
+
<span class="status-badge" style="background: ${getStatusBg(p.status)}; color: ${getStatusColor(p.status)};">${escapeHtml(p.status || 'unknown')}</span>
|
|
432
|
+
${cancelBtnHtml}
|
|
433
|
+
</div>
|
|
434
|
+
`;
|
|
435
|
+
}).join('');
|
|
436
|
+
|
|
437
|
+
// Bind Cancel research triggers
|
|
438
|
+
document.querySelectorAll('.btn-cancel-res').forEach(btn => {
|
|
439
|
+
btn.addEventListener('click', async (e) => {
|
|
440
|
+
e.stopPropagation();
|
|
441
|
+
const id = btn.getAttribute('data-id');
|
|
442
|
+
if (btn.disabled) return;
|
|
443
|
+
btn.disabled = true;
|
|
444
|
+
try {
|
|
445
|
+
await self.BrainClient.brainFetch(`/api/research/${id}`, { method: 'DELETE' });
|
|
446
|
+
showToast('Research cancelled.');
|
|
447
|
+
fetchResearch();
|
|
448
|
+
} catch (err) {
|
|
449
|
+
showToast('Failed to cancel: ' + err.message, true);
|
|
450
|
+
btn.disabled = false;
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
});
|
|
454
|
+
} catch {
|
|
455
|
+
researchList.innerHTML = '<div class="research-empty">Could not load research feed</div>';
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
btnRefreshResearch.addEventListener('click', fetchResearch);
|
|
460
|
+
|
|
461
|
+
function getStatusBg(status) {
|
|
462
|
+
switch (status) {
|
|
463
|
+
case 'in_progress': return 'rgba(137, 180, 250, 0.15)';
|
|
464
|
+
case 'done': return 'rgba(166, 227, 161, 0.15)';
|
|
465
|
+
case 'failed': return 'rgba(243, 139, 168, 0.15)';
|
|
466
|
+
default: return 'rgba(108, 112, 134, 0.15)';
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function getStatusColor(status) {
|
|
471
|
+
switch (status) {
|
|
472
|
+
case 'in_progress': return '#89b4fa';
|
|
473
|
+
case 'done': return '#a6e3a1';
|
|
474
|
+
case 'failed': return '#f38ba8';
|
|
475
|
+
default: return '#bac2de';
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
// ==================== SETTINGS & ACTIONS ====================
|
|
481
|
+
|
|
482
|
+
async function fetchBrains() {
|
|
483
|
+
try {
|
|
484
|
+
const data = await self.BrainClient.brainFetch('/api/brains');
|
|
485
|
+
const brains = data.brains || [];
|
|
486
|
+
|
|
487
|
+
settingsBrainSelector.innerHTML = '<option value="global">Global Brain Layer</option>' +
|
|
488
|
+
brains.filter(b => b.id !== 'global').map(b => `
|
|
489
|
+
<option value="${b.id}">${escapeHtml(b.name || b.id)} (Project)</option>
|
|
490
|
+
`).join('');
|
|
491
|
+
|
|
492
|
+
// Restore active dropdown selection
|
|
493
|
+
settingsBrainSelector.value = activeBrainId;
|
|
494
|
+
} catch {
|
|
495
|
+
// Fallback if brains call fails
|
|
496
|
+
settingsBrainSelector.innerHTML = '<option value="global">Global Brain Layer</option>';
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
settingsBrainSelector.addEventListener('change', async (e) => {
|
|
501
|
+
const val = e.target.value;
|
|
502
|
+
activeBrainId = val;
|
|
503
|
+
await chrome.storage.sync.set({ activeBrainId: val });
|
|
504
|
+
|
|
505
|
+
// Update badge in header
|
|
506
|
+
activeBrainBadge.textContent = val === 'global' ? 'Global' : 'Project';
|
|
507
|
+
activeBrainBadge.style.color = val === 'global' ? '#cba6f7' : '#89b4fa';
|
|
508
|
+
|
|
509
|
+
showToast(`Active brain switched to ${val === 'global' ? 'Global' : 'Project'}`);
|
|
510
|
+
|
|
511
|
+
// Clear chat history on brain layer swap to prevent context leakage
|
|
512
|
+
chatHistory = [];
|
|
513
|
+
chatMessages.innerHTML = `
|
|
514
|
+
<div class="chat-bubble assistant">
|
|
515
|
+
Switched active brain context to: <b>${val === 'global' ? 'Global' : 'Project'}</b>. Conversation history has been reset.
|
|
516
|
+
</div>
|
|
517
|
+
`;
|
|
518
|
+
|
|
519
|
+
// Refresh memory feed
|
|
520
|
+
if (!searchInput.value.trim()) {
|
|
521
|
+
loadRecentCaptures();
|
|
522
|
+
} else {
|
|
523
|
+
performSearch(searchInput.value);
|
|
524
|
+
}
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
async function loadSettingsState() {
|
|
528
|
+
const state = await chrome.storage.sync.get({
|
|
529
|
+
activeBrainId: 'global',
|
|
530
|
+
passiveTracking: false
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
activeBrainId = state.activeBrainId;
|
|
534
|
+
activeBrainBadge.textContent = activeBrainId === 'global' ? 'Global' : 'Project';
|
|
535
|
+
activeBrainBadge.style.color = activeBrainId === 'global' ? '#cba6f7' : '#89b4fa';
|
|
536
|
+
|
|
537
|
+
settingsTrackingToggle.checked = state.passiveTracking;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
settingsTrackingToggle.addEventListener('change', async () => {
|
|
541
|
+
const passiveTracking = settingsTrackingToggle.checked;
|
|
542
|
+
await chrome.storage.sync.set({ passiveTracking });
|
|
543
|
+
// Signal active preference back to service worker
|
|
544
|
+
chrome.runtime.sendMessage({ type: 'UPDATE_PREFERENCES', passiveTracking });
|
|
545
|
+
showToast(`Passive tracking ${passiveTracking ? 'enabled' : 'disabled'}`);
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
// Block Current Domain
|
|
549
|
+
btnBlockDomain.addEventListener('click', async () => {
|
|
550
|
+
try {
|
|
551
|
+
const tab = await getCurrentTab();
|
|
552
|
+
if (!tab || !tab.url) return;
|
|
553
|
+
const urlObj = new URL(tab.url);
|
|
554
|
+
const host = urlObj.hostname;
|
|
555
|
+
|
|
556
|
+
if (!host) {
|
|
557
|
+
showToast('Cannot block empty domain', true);
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
const { blocklist } = await chrome.storage.sync.get({ blocklist: [] });
|
|
562
|
+
if (blocklist.includes(host)) {
|
|
563
|
+
showToast(`${host} is already blocked`);
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
blocklist.push(host);
|
|
568
|
+
await chrome.storage.sync.set({ blocklist });
|
|
569
|
+
showToast(`Blocked domain: ${host}`);
|
|
570
|
+
} catch (err) {
|
|
571
|
+
showToast('Block error: ' + err.message, true);
|
|
572
|
+
}
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
// Recompile Brain index shims
|
|
576
|
+
btnRecompileBrain.addEventListener('click', async () => {
|
|
577
|
+
if (btnRecompileBrain.disabled) return;
|
|
578
|
+
const originalText = btnRecompileBrain.innerHTML;
|
|
579
|
+
btnRecompileBrain.innerHTML = '⏳ Compiling...';
|
|
580
|
+
btnRecompileBrain.disabled = true;
|
|
581
|
+
btnRecompileBrain.classList.add('loading');
|
|
582
|
+
try {
|
|
583
|
+
await self.BrainClient.brainFetch('/api/vault/compile', { method: 'POST' });
|
|
584
|
+
showToast('Brain indexes recompiled!');
|
|
585
|
+
} catch (err) {
|
|
586
|
+
showToast('Compilation error: ' + err.message, true);
|
|
587
|
+
} finally {
|
|
588
|
+
btnRecompileBrain.innerHTML = originalText;
|
|
589
|
+
btnRecompileBrain.disabled = false;
|
|
590
|
+
btnRecompileBrain.classList.remove('loading');
|
|
591
|
+
}
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
// ---- Ingestion Invariant Sync ----
|
|
596
|
+
// Autonomously capture correction in permanent memory
|
|
597
|
+
async function recordRememberInvariants() {
|
|
598
|
+
try {
|
|
599
|
+
// Perform automated memory check locally using REST API to prevent duplicate records
|
|
600
|
+
const response = await self.BrainClient.search("do not call it copilot", 1);
|
|
601
|
+
const match = response.results && response.results[0];
|
|
602
|
+
if (match && match.slug === 'never-use-copilot') return;
|
|
603
|
+
|
|
604
|
+
// Upsert memory node autonomously using SSSS REST schema endpoints
|
|
605
|
+
await self.BrainClient.share({
|
|
606
|
+
slug: 'never-use-copilot',
|
|
607
|
+
category: 'invariants',
|
|
608
|
+
title: "Never refer to the chat interface or AI companion as 'Co-Pilot'. Always refer to it simply as 'Chat'.",
|
|
609
|
+
status: 'active',
|
|
610
|
+
importance: 5,
|
|
611
|
+
modality: 'must_not',
|
|
612
|
+
priority: 'absolute',
|
|
613
|
+
action: 'remember',
|
|
614
|
+
source: 'chrome-extension-sidepanel'
|
|
615
|
+
});
|
|
179
616
|
} catch {
|
|
180
|
-
|
|
617
|
+
// Suppress silently if remote brain is offline during first-time handshake
|
|
181
618
|
}
|
|
182
619
|
}
|
|
183
620
|
|
|
621
|
+
|
|
184
622
|
// ---- Toast ----
|
|
185
623
|
function showToast(message, isError = false) {
|
|
186
624
|
let toast = document.querySelector('.toast');
|
|
@@ -200,6 +638,7 @@
|
|
|
200
638
|
|
|
201
639
|
// ---- Helpers ----
|
|
202
640
|
function escapeHtml(str) {
|
|
641
|
+
if (!str) return '';
|
|
203
642
|
const div = document.createElement('div');
|
|
204
643
|
div.textContent = str;
|
|
205
644
|
return div.innerHTML;
|
|
@@ -224,6 +663,18 @@
|
|
|
224
663
|
|
|
225
664
|
// ---- Init ----
|
|
226
665
|
checkConnection();
|
|
227
|
-
|
|
228
|
-
|
|
666
|
+
loadSettingsState().then(() => {
|
|
667
|
+
loadRecentCaptures();
|
|
668
|
+
recordRememberInvariants();
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
// Set intervals
|
|
672
|
+
setInterval(checkConnection, 15000);
|
|
673
|
+
setInterval(() => {
|
|
674
|
+
// Refresh active research periodically if currently viewing Research tab
|
|
675
|
+
const activeTab = document.querySelector('.tab-btn.active');
|
|
676
|
+
if (activeTab && activeTab.getAttribute('data-tab') === 'research') {
|
|
677
|
+
fetchResearch();
|
|
678
|
+
}
|
|
679
|
+
}, 30000);
|
|
229
680
|
})();
|