@aitytech/agentkits-memory 1.0.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +54 -5
- package/dist/better-sqlite3-backend.d.ts +192 -0
- package/dist/better-sqlite3-backend.d.ts.map +1 -0
- package/dist/better-sqlite3-backend.js +801 -0
- package/dist/better-sqlite3-backend.js.map +1 -0
- package/dist/cli/save.js +0 -0
- package/dist/cli/setup.d.ts +6 -2
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli/setup.js +289 -42
- package/dist/cli/setup.js.map +1 -1
- package/dist/cli/viewer.js +25 -56
- package/dist/cli/viewer.js.map +1 -1
- package/dist/cli/web-viewer.d.ts +2 -1
- package/dist/cli/web-viewer.d.ts.map +1 -1
- package/dist/cli/web-viewer.js +791 -141
- package/dist/cli/web-viewer.js.map +1 -1
- package/dist/embeddings/embedding-cache.d.ts +131 -0
- package/dist/embeddings/embedding-cache.d.ts.map +1 -0
- package/dist/embeddings/embedding-cache.js +217 -0
- package/dist/embeddings/embedding-cache.js.map +1 -0
- package/dist/embeddings/index.d.ts +11 -0
- package/dist/embeddings/index.d.ts.map +1 -0
- package/dist/embeddings/index.js +11 -0
- package/dist/embeddings/index.js.map +1 -0
- package/dist/embeddings/local-embeddings.d.ts +140 -0
- package/dist/embeddings/local-embeddings.d.ts.map +1 -0
- package/dist/embeddings/local-embeddings.js +293 -0
- package/dist/embeddings/local-embeddings.js.map +1 -0
- package/dist/hooks/context.d.ts +6 -1
- package/dist/hooks/context.d.ts.map +1 -1
- package/dist/hooks/context.js +12 -2
- package/dist/hooks/context.js.map +1 -1
- package/dist/hooks/observation.d.ts +6 -1
- package/dist/hooks/observation.d.ts.map +1 -1
- package/dist/hooks/observation.js +12 -2
- package/dist/hooks/observation.js.map +1 -1
- package/dist/hooks/service.d.ts +1 -6
- package/dist/hooks/service.d.ts.map +1 -1
- package/dist/hooks/service.js +33 -85
- package/dist/hooks/service.js.map +1 -1
- package/dist/hooks/session-init.d.ts +6 -1
- package/dist/hooks/session-init.d.ts.map +1 -1
- package/dist/hooks/session-init.js +12 -2
- package/dist/hooks/session-init.js.map +1 -1
- package/dist/hooks/summarize.d.ts +6 -1
- package/dist/hooks/summarize.d.ts.map +1 -1
- package/dist/hooks/summarize.js +12 -2
- package/dist/hooks/summarize.js.map +1 -1
- package/dist/index.d.ts +10 -17
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +172 -94
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.js +17 -3
- package/dist/mcp/server.js.map +1 -1
- package/dist/migration.js +3 -3
- package/dist/migration.js.map +1 -1
- package/dist/search/hybrid-search.d.ts +262 -0
- package/dist/search/hybrid-search.d.ts.map +1 -0
- package/dist/search/hybrid-search.js +688 -0
- package/dist/search/hybrid-search.js.map +1 -0
- package/dist/search/index.d.ts +13 -0
- package/dist/search/index.d.ts.map +1 -0
- package/dist/search/index.js +13 -0
- package/dist/search/index.js.map +1 -0
- package/dist/search/token-economics.d.ts +161 -0
- package/dist/search/token-economics.d.ts.map +1 -0
- package/dist/search/token-economics.js +239 -0
- package/dist/search/token-economics.js.map +1 -0
- package/dist/types.d.ts +0 -68
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +5 -3
- package/src/__tests__/better-sqlite3-backend.test.ts +1466 -0
- package/src/__tests__/cache-manager.test.ts +499 -0
- package/src/__tests__/embedding-integration.test.ts +481 -0
- package/src/__tests__/hnsw-index.test.ts +727 -0
- package/src/__tests__/index.test.ts +432 -0
- package/src/better-sqlite3-backend.ts +1000 -0
- package/src/cli/setup.ts +358 -47
- package/src/cli/viewer.ts +28 -63
- package/src/cli/web-viewer.ts +936 -182
- package/src/embeddings/__tests__/embedding-cache.test.ts +269 -0
- package/src/embeddings/__tests__/local-embeddings.test.ts +495 -0
- package/src/embeddings/embedding-cache.ts +318 -0
- package/src/embeddings/index.ts +20 -0
- package/src/embeddings/local-embeddings.ts +419 -0
- package/src/hooks/__tests__/handlers.test.ts +58 -17
- package/src/hooks/__tests__/integration.test.ts +77 -26
- package/src/hooks/context.ts +13 -2
- package/src/hooks/observation.ts +13 -2
- package/src/hooks/service.ts +39 -100
- package/src/hooks/session-init.ts +13 -2
- package/src/hooks/summarize.ts +13 -2
- package/src/index.ts +210 -116
- package/src/mcp/server.ts +20 -3
- package/src/search/__tests__/hybrid-search.test.ts +669 -0
- package/src/search/__tests__/token-economics.test.ts +276 -0
- package/src/search/hybrid-search.ts +968 -0
- package/src/search/index.ts +29 -0
- package/src/search/token-economics.ts +367 -0
- package/src/types.ts +0 -96
- package/src/__tests__/sqljs-backend.test.ts +0 -410
- package/src/migration.ts +0 -574
- package/src/sql.js.d.ts +0 -70
- package/src/sqljs-backend.ts +0 -789
package/dist/cli/web-viewer.js
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* AgentKits Memory Web Viewer
|
|
4
4
|
*
|
|
5
|
-
* Web-based viewer for memory database with
|
|
5
|
+
* Web-based viewer for memory database with hybrid search support.
|
|
6
|
+
* Uses ProjectMemoryService for vector + text search.
|
|
6
7
|
*
|
|
7
8
|
* Usage:
|
|
8
9
|
* npx agentkits-memory-web [--port=1905]
|
|
@@ -10,12 +11,13 @@
|
|
|
10
11
|
* @module @aitytech/agentkits-memory/cli/web-viewer
|
|
11
12
|
*/
|
|
12
13
|
import * as http from 'node:http';
|
|
13
|
-
import * as fs from 'node:fs';
|
|
14
14
|
import * as path from 'node:path';
|
|
15
|
-
import
|
|
16
|
-
import
|
|
15
|
+
import Database from 'better-sqlite3';
|
|
16
|
+
import { HybridSearchEngine, LocalEmbeddingsService } from '../index.js';
|
|
17
17
|
const args = process.argv.slice(2);
|
|
18
18
|
const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
19
|
+
// Embeddings service singleton
|
|
20
|
+
let _embeddingsService = null;
|
|
19
21
|
function parseArgs() {
|
|
20
22
|
const parsed = {};
|
|
21
23
|
for (const arg of args) {
|
|
@@ -30,105 +32,162 @@ const options = parseArgs();
|
|
|
30
32
|
const PORT = parseInt(options.port, 10) || 1905;
|
|
31
33
|
const dbDir = path.join(projectDir, '.claude/memory');
|
|
32
34
|
const dbPath = path.join(dbDir, 'memory.db');
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
// Ensure directory exists
|
|
46
|
-
if (!fs.existsSync(dbDir)) {
|
|
47
|
-
fs.mkdirSync(dbDir, { recursive: true });
|
|
48
|
-
}
|
|
49
|
-
let db;
|
|
50
|
-
if (fs.existsSync(dbPath)) {
|
|
51
|
-
const buffer = fs.readFileSync(dbPath);
|
|
52
|
-
db = new SQL.Database(new Uint8Array(buffer));
|
|
53
|
-
}
|
|
54
|
-
else {
|
|
55
|
-
db = new SQL.Database();
|
|
56
|
-
}
|
|
57
|
-
// Create table if not exists
|
|
58
|
-
db.run(`
|
|
59
|
-
CREATE TABLE IF NOT EXISTS memory_entries (
|
|
60
|
-
id TEXT PRIMARY KEY,
|
|
61
|
-
key TEXT NOT NULL,
|
|
62
|
-
content TEXT NOT NULL,
|
|
63
|
-
type TEXT DEFAULT 'semantic',
|
|
64
|
-
namespace TEXT DEFAULT 'general',
|
|
65
|
-
tags TEXT DEFAULT '[]',
|
|
66
|
-
metadata TEXT DEFAULT '{}',
|
|
67
|
-
embedding BLOB,
|
|
68
|
-
created_at INTEGER NOT NULL,
|
|
69
|
-
updated_at INTEGER NOT NULL,
|
|
70
|
-
accessed_at INTEGER,
|
|
71
|
-
access_count INTEGER DEFAULT 0,
|
|
72
|
-
importance REAL DEFAULT 0.5,
|
|
73
|
-
decay_rate REAL DEFAULT 0.1
|
|
74
|
-
)
|
|
75
|
-
`);
|
|
76
|
-
db.run(`CREATE INDEX IF NOT EXISTS idx_namespace ON memory_entries(namespace)`);
|
|
77
|
-
db.run(`CREATE INDEX IF NOT EXISTS idx_key ON memory_entries(key)`);
|
|
78
|
-
db.run(`CREATE INDEX IF NOT EXISTS idx_created ON memory_entries(created_at)`);
|
|
79
|
-
return db;
|
|
35
|
+
// Singleton database and search engine
|
|
36
|
+
let _searchEngine = null;
|
|
37
|
+
let _db = null;
|
|
38
|
+
/**
|
|
39
|
+
* Get direct database access
|
|
40
|
+
*/
|
|
41
|
+
function getDatabase() {
|
|
42
|
+
if (_db)
|
|
43
|
+
return _db;
|
|
44
|
+
_db = new Database(dbPath);
|
|
45
|
+
_db.pragma('journal_mode = WAL');
|
|
46
|
+
return _db;
|
|
80
47
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
48
|
+
/**
|
|
49
|
+
* Get or initialize embeddings service
|
|
50
|
+
*/
|
|
51
|
+
async function getEmbeddingsService() {
|
|
52
|
+
if (_embeddingsService)
|
|
53
|
+
return _embeddingsService;
|
|
54
|
+
_embeddingsService = new LocalEmbeddingsService({
|
|
55
|
+
cacheDir: path.join(dbDir, 'embeddings-cache'),
|
|
56
|
+
});
|
|
57
|
+
await _embeddingsService.initialize();
|
|
58
|
+
return _embeddingsService;
|
|
85
59
|
}
|
|
86
|
-
|
|
87
|
-
|
|
60
|
+
/**
|
|
61
|
+
* Get or initialize the HybridSearchEngine with embeddings
|
|
62
|
+
*/
|
|
63
|
+
async function getSearchEngine() {
|
|
64
|
+
if (_searchEngine)
|
|
65
|
+
return _searchEngine;
|
|
66
|
+
const db = getDatabase();
|
|
67
|
+
const embeddings = await getEmbeddingsService();
|
|
68
|
+
// Create embedding generator function
|
|
69
|
+
const embeddingGenerator = async (text) => {
|
|
70
|
+
const result = await embeddings.embed(text);
|
|
71
|
+
return result.embedding;
|
|
72
|
+
};
|
|
73
|
+
_searchEngine = new HybridSearchEngine(db, {}, embeddingGenerator);
|
|
74
|
+
await _searchEngine.initialize();
|
|
75
|
+
return _searchEngine;
|
|
88
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* Get database statistics using direct SQL (faster for stats queries)
|
|
79
|
+
*/
|
|
89
80
|
function getStats(db) {
|
|
90
|
-
const
|
|
91
|
-
const total =
|
|
92
|
-
const
|
|
81
|
+
const totalRow = db.prepare('SELECT COUNT(*) as count FROM memory_entries').get();
|
|
82
|
+
const total = totalRow?.count || 0;
|
|
83
|
+
const nsRows = db.prepare('SELECT namespace, COUNT(*) as count FROM memory_entries GROUP BY namespace').all();
|
|
93
84
|
const byNamespace = {};
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
byNamespace[row[0]] = row[1];
|
|
97
|
-
}
|
|
85
|
+
for (const row of nsRows) {
|
|
86
|
+
byNamespace[row.namespace] = row.count;
|
|
98
87
|
}
|
|
99
|
-
const
|
|
88
|
+
const typeRows = db.prepare('SELECT type, COUNT(*) as count FROM memory_entries GROUP BY type').all();
|
|
100
89
|
const byType = {};
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
90
|
+
for (const row of typeRows) {
|
|
91
|
+
byType[row.type] = row.count;
|
|
92
|
+
}
|
|
93
|
+
// Calculate token economics
|
|
94
|
+
const contentRow = db.prepare('SELECT SUM(LENGTH(content)) as total_chars, COUNT(*) as count FROM memory_entries').get();
|
|
95
|
+
const totalCharacters = contentRow?.total_chars || 0;
|
|
96
|
+
const entryCount = contentRow?.count || 0;
|
|
97
|
+
// Estimate tokens (~4 chars per token)
|
|
98
|
+
const totalTokens = Math.ceil(totalCharacters / 4);
|
|
99
|
+
const avgTokensPerEntry = entryCount > 0 ? Math.ceil(totalTokens / entryCount) : 0;
|
|
100
|
+
// Estimated savings: if you had to rediscover this info each time
|
|
101
|
+
// Assume 5x overhead for discovery vs recall
|
|
102
|
+
const estimatedSavings = totalTokens * 5;
|
|
103
|
+
return {
|
|
104
|
+
total,
|
|
105
|
+
byNamespace,
|
|
106
|
+
byType,
|
|
107
|
+
tokenEconomics: {
|
|
108
|
+
totalTokens,
|
|
109
|
+
avgTokensPerEntry,
|
|
110
|
+
totalCharacters,
|
|
111
|
+
estimatedSavings,
|
|
112
|
+
},
|
|
113
|
+
};
|
|
107
114
|
}
|
|
115
|
+
/**
|
|
116
|
+
* Get entries with optional search (standard listing)
|
|
117
|
+
*/
|
|
108
118
|
function getEntries(db, namespace, limit = 50, offset = 0, search) {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
119
|
+
// Standard query without search
|
|
120
|
+
if (!search || !search.trim()) {
|
|
121
|
+
let query = 'SELECT id, key, content, type, namespace, tags, embedding, created_at, updated_at FROM memory_entries';
|
|
122
|
+
const conditions = [];
|
|
123
|
+
const params = [];
|
|
124
|
+
if (namespace) {
|
|
125
|
+
conditions.push('namespace = ?');
|
|
126
|
+
params.push(namespace);
|
|
127
|
+
}
|
|
128
|
+
if (conditions.length > 0) {
|
|
129
|
+
query += ' WHERE ' + conditions.join(' AND ');
|
|
130
|
+
}
|
|
131
|
+
query += ' ORDER BY created_at DESC LIMIT ? OFFSET ?';
|
|
132
|
+
params.push(limit, offset);
|
|
133
|
+
const rows = db.prepare(query).all(...params);
|
|
134
|
+
return rows.map((row) => ({
|
|
135
|
+
id: row.id,
|
|
136
|
+
key: row.key,
|
|
137
|
+
content: row.content,
|
|
138
|
+
type: row.type,
|
|
139
|
+
namespace: row.namespace,
|
|
140
|
+
tags: JSON.parse(row.tags || '[]'),
|
|
141
|
+
created_at: row.created_at,
|
|
142
|
+
updated_at: row.updated_at,
|
|
143
|
+
hasEmbedding: !!(row.embedding && row.embedding.length > 0),
|
|
144
|
+
}));
|
|
145
|
+
}
|
|
146
|
+
// Use FTS5 search for better CJK support
|
|
147
|
+
const sanitizedSearch = search.trim().replace(/"/g, '""');
|
|
148
|
+
let ftsQuery = `
|
|
149
|
+
SELECT m.id, m.key, m.content, m.type, m.namespace, m.tags, m.embedding, m.created_at, m.updated_at
|
|
150
|
+
FROM memory_entries m
|
|
151
|
+
INNER JOIN memory_fts f ON m.id = f.id
|
|
152
|
+
WHERE memory_fts MATCH '"${sanitizedSearch}"'
|
|
153
|
+
`;
|
|
112
154
|
if (namespace) {
|
|
113
|
-
|
|
114
|
-
params.push(namespace);
|
|
155
|
+
ftsQuery += ` AND m.namespace = ?`;
|
|
115
156
|
}
|
|
116
|
-
|
|
157
|
+
ftsQuery += ` ORDER BY m.created_at DESC LIMIT ? OFFSET ?`;
|
|
158
|
+
try {
|
|
159
|
+
const params = namespace ? [namespace, limit, offset] : [limit, offset];
|
|
160
|
+
const rows = db.prepare(ftsQuery).all(...params);
|
|
161
|
+
return rows.map((row) => ({
|
|
162
|
+
id: row.id,
|
|
163
|
+
key: row.key,
|
|
164
|
+
content: row.content,
|
|
165
|
+
type: row.type,
|
|
166
|
+
namespace: row.namespace,
|
|
167
|
+
tags: JSON.parse(row.tags || '[]'),
|
|
168
|
+
created_at: row.created_at,
|
|
169
|
+
updated_at: row.updated_at,
|
|
170
|
+
hasEmbedding: !!(row.embedding && row.embedding.length > 0),
|
|
171
|
+
}));
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
// Fallback to LIKE if FTS fails
|
|
175
|
+
console.warn('[WebViewer] FTS search failed, falling back to LIKE');
|
|
176
|
+
let query = 'SELECT id, key, content, type, namespace, tags, embedding, created_at, updated_at FROM memory_entries';
|
|
177
|
+
const conditions = [];
|
|
178
|
+
const params = [];
|
|
179
|
+
if (namespace) {
|
|
180
|
+
conditions.push('namespace = ?');
|
|
181
|
+
params.push(namespace);
|
|
182
|
+
}
|
|
117
183
|
conditions.push('(content LIKE ? OR key LIKE ? OR tags LIKE ?)');
|
|
118
184
|
const searchPattern = `%${search}%`;
|
|
119
185
|
params.push(searchPattern, searchPattern, searchPattern);
|
|
120
|
-
}
|
|
121
|
-
if (conditions.length > 0) {
|
|
122
186
|
query += ' WHERE ' + conditions.join(' AND ');
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
stmt.bind(params);
|
|
128
|
-
const entries = [];
|
|
129
|
-
while (stmt.step()) {
|
|
130
|
-
const row = stmt.getAsObject();
|
|
131
|
-
entries.push({
|
|
187
|
+
query += ' ORDER BY created_at DESC LIMIT ? OFFSET ?';
|
|
188
|
+
params.push(limit, offset);
|
|
189
|
+
const rows = db.prepare(query).all(...params);
|
|
190
|
+
return rows.map((row) => ({
|
|
132
191
|
id: row.id,
|
|
133
192
|
key: row.key,
|
|
134
193
|
content: row.content,
|
|
@@ -137,9 +196,46 @@ function getEntries(db, namespace, limit = 50, offset = 0, search) {
|
|
|
137
196
|
tags: JSON.parse(row.tags || '[]'),
|
|
138
197
|
created_at: row.created_at,
|
|
139
198
|
updated_at: row.updated_at,
|
|
140
|
-
|
|
199
|
+
hasEmbedding: !!(row.embedding && row.embedding.length > 0),
|
|
200
|
+
}));
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Search entries using HybridSearchEngine
|
|
205
|
+
* Supports hybrid (text + vector), text-only, or vector-only search
|
|
206
|
+
*/
|
|
207
|
+
async function searchEntries(searchEngine, query, options = {}) {
|
|
208
|
+
const { type = 'hybrid', namespace, limit = 20 } = options;
|
|
209
|
+
// Use searchCompact for efficient search with scores
|
|
210
|
+
const results = await searchEngine.searchCompact(query, {
|
|
211
|
+
limit,
|
|
212
|
+
namespace,
|
|
213
|
+
includeKeyword: type === 'hybrid' || type === 'text',
|
|
214
|
+
includeSemantic: type === 'hybrid' || type === 'vector',
|
|
215
|
+
});
|
|
216
|
+
// Fetch full entries for the results
|
|
217
|
+
const db = getDatabase();
|
|
218
|
+
const entries = [];
|
|
219
|
+
for (const result of results) {
|
|
220
|
+
const row = db.prepare(`
|
|
221
|
+
SELECT id, key, content, type, namespace, tags, embedding, created_at, updated_at
|
|
222
|
+
FROM memory_entries WHERE id = ?
|
|
223
|
+
`).get(result.id);
|
|
224
|
+
if (row) {
|
|
225
|
+
entries.push({
|
|
226
|
+
id: row.id,
|
|
227
|
+
key: row.key,
|
|
228
|
+
content: row.content,
|
|
229
|
+
type: row.type,
|
|
230
|
+
namespace: row.namespace,
|
|
231
|
+
tags: JSON.parse(row.tags || '[]'),
|
|
232
|
+
created_at: row.created_at,
|
|
233
|
+
updated_at: row.updated_at,
|
|
234
|
+
score: result.score,
|
|
235
|
+
hasEmbedding: !!(row.embedding && row.embedding.length > 0),
|
|
236
|
+
});
|
|
237
|
+
}
|
|
141
238
|
}
|
|
142
|
-
stmt.free();
|
|
143
239
|
return entries;
|
|
144
240
|
}
|
|
145
241
|
function getHTML() {
|
|
@@ -149,6 +245,7 @@ function getHTML() {
|
|
|
149
245
|
<meta charset="UTF-8">
|
|
150
246
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
151
247
|
<title>AgentKits Memory Viewer</title>
|
|
248
|
+
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cdefs%3E%3ClinearGradient id='g' x1='0%25' y1='0%25' x2='100%25' y2='100%25'%3E%3Cstop offset='0%25' stop-color='%233B82F6'/%3E%3Cstop offset='100%25' stop-color='%238B5CF6'/%3E%3C/linearGradient%3E%3C/defs%3E%3Ccircle cx='12' cy='12' r='10' fill='url(%23g)'/%3E%3Cpath d='M10 14.17l-3.17-3.17-1.42 1.41L10 17l8-8-1.41-1.41z' fill='white'/%3E%3C/svg%3E">
|
|
152
249
|
<style>
|
|
153
250
|
:root {
|
|
154
251
|
--bg-primary: #0F172A;
|
|
@@ -259,6 +356,202 @@ function getHTML() {
|
|
|
259
356
|
fill: var(--text-muted);
|
|
260
357
|
}
|
|
261
358
|
|
|
359
|
+
.search-type-select {
|
|
360
|
+
padding: 12px 16px;
|
|
361
|
+
background: var(--bg-secondary);
|
|
362
|
+
border: 1px solid var(--border);
|
|
363
|
+
border-radius: 8px;
|
|
364
|
+
color: var(--text-primary);
|
|
365
|
+
font-size: 14px;
|
|
366
|
+
cursor: pointer;
|
|
367
|
+
min-width: 180px;
|
|
368
|
+
transition: border-color 0.2s;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
.search-type-select:focus {
|
|
372
|
+
outline: none;
|
|
373
|
+
border-color: var(--accent);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
.search-type-select:hover { border-color: var(--accent); }
|
|
377
|
+
|
|
378
|
+
.score-badge {
|
|
379
|
+
font-size: 11px;
|
|
380
|
+
padding: 3px 8px;
|
|
381
|
+
background: linear-gradient(135deg, rgba(59, 130, 246, 0.2), rgba(139, 92, 246, 0.2));
|
|
382
|
+
color: var(--accent);
|
|
383
|
+
border-radius: 4px;
|
|
384
|
+
font-weight: 600;
|
|
385
|
+
white-space: nowrap;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
.vector-badge {
|
|
389
|
+
display: inline-flex;
|
|
390
|
+
align-items: center;
|
|
391
|
+
font-size: 10px;
|
|
392
|
+
font-weight: 600;
|
|
393
|
+
padding: 2px 6px;
|
|
394
|
+
border-radius: 4px;
|
|
395
|
+
text-transform: uppercase;
|
|
396
|
+
letter-spacing: 0.5px;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
.vector-badge.has-vector {
|
|
400
|
+
background: rgba(34, 197, 94, 0.15);
|
|
401
|
+
color: #22C55E;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
.vector-badge.no-vector {
|
|
405
|
+
background: rgba(100, 116, 139, 0.1);
|
|
406
|
+
color: var(--text-muted);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
.embedding-stats {
|
|
410
|
+
display: flex;
|
|
411
|
+
gap: 16px;
|
|
412
|
+
margin-bottom: 20px;
|
|
413
|
+
padding: 16px;
|
|
414
|
+
background: var(--bg-card);
|
|
415
|
+
border-radius: 8px;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
.embedding-stat {
|
|
419
|
+
text-align: center;
|
|
420
|
+
flex: 1;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
.embedding-stat-value {
|
|
424
|
+
font-size: 24px;
|
|
425
|
+
font-weight: 700;
|
|
426
|
+
color: var(--text-primary);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
.embedding-stat-label {
|
|
430
|
+
font-size: 12px;
|
|
431
|
+
color: var(--text-muted);
|
|
432
|
+
text-transform: uppercase;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
.embedding-stat-value.success { color: var(--success); }
|
|
436
|
+
.embedding-stat-value.warning { color: var(--warning); }
|
|
437
|
+
|
|
438
|
+
.progress-bar {
|
|
439
|
+
height: 8px;
|
|
440
|
+
background: var(--bg-card);
|
|
441
|
+
border-radius: 4px;
|
|
442
|
+
overflow: hidden;
|
|
443
|
+
margin: 16px 0;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
.progress-bar-fill {
|
|
447
|
+
height: 100%;
|
|
448
|
+
background: linear-gradient(90deg, var(--accent), #8B5CF6);
|
|
449
|
+
border-radius: 4px;
|
|
450
|
+
transition: width 0.3s ease;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
.progress-text {
|
|
454
|
+
text-align: center;
|
|
455
|
+
font-size: 14px;
|
|
456
|
+
color: var(--text-secondary);
|
|
457
|
+
margin-top: 8px;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
.btn-icon {
|
|
461
|
+
padding: 10px;
|
|
462
|
+
min-width: auto;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
.embedding-section {
|
|
466
|
+
margin-top: 20px;
|
|
467
|
+
padding: 16px;
|
|
468
|
+
background: var(--bg-card);
|
|
469
|
+
border-radius: 8px;
|
|
470
|
+
border: 1px solid var(--border);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
.embedding-header {
|
|
474
|
+
display: flex;
|
|
475
|
+
align-items: center;
|
|
476
|
+
gap: 12px;
|
|
477
|
+
margin-bottom: 12px;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
.embedding-status {
|
|
481
|
+
display: inline-flex;
|
|
482
|
+
align-items: center;
|
|
483
|
+
gap: 6px;
|
|
484
|
+
font-size: 13px;
|
|
485
|
+
padding: 4px 10px;
|
|
486
|
+
border-radius: 6px;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
.embedding-status.has-embedding {
|
|
490
|
+
background: rgba(34, 197, 94, 0.15);
|
|
491
|
+
color: #22C55E;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
.embedding-status.no-embedding {
|
|
495
|
+
background: rgba(239, 68, 68, 0.15);
|
|
496
|
+
color: #EF4444;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
.embedding-status svg {
|
|
500
|
+
width: 14px;
|
|
501
|
+
height: 14px;
|
|
502
|
+
fill: currentColor;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
.embedding-dims {
|
|
506
|
+
font-size: 12px;
|
|
507
|
+
color: var(--text-muted);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
.embedding-viz {
|
|
511
|
+
display: flex;
|
|
512
|
+
align-items: flex-end;
|
|
513
|
+
gap: 2px;
|
|
514
|
+
height: 40px;
|
|
515
|
+
margin-top: 12px;
|
|
516
|
+
padding: 8px;
|
|
517
|
+
background: var(--bg-primary);
|
|
518
|
+
border-radius: 6px;
|
|
519
|
+
overflow: hidden;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
.embedding-bar {
|
|
523
|
+
flex: 1;
|
|
524
|
+
min-width: 4px;
|
|
525
|
+
border-radius: 2px 2px 0 0;
|
|
526
|
+
transition: height 0.3s ease;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
.embedding-bar.positive { background: linear-gradient(to top, #3B82F6, #60A5FA); }
|
|
530
|
+
.embedding-bar.negative { background: linear-gradient(to top, #8B5CF6, #A78BFA); }
|
|
531
|
+
|
|
532
|
+
.embedding-legend {
|
|
533
|
+
display: flex;
|
|
534
|
+
justify-content: space-between;
|
|
535
|
+
margin-top: 8px;
|
|
536
|
+
font-size: 11px;
|
|
537
|
+
color: var(--text-muted);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
.embedding-legend-item {
|
|
541
|
+
display: flex;
|
|
542
|
+
align-items: center;
|
|
543
|
+
gap: 4px;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
.legend-dot {
|
|
547
|
+
width: 8px;
|
|
548
|
+
height: 8px;
|
|
549
|
+
border-radius: 50%;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
.legend-dot.positive { background: #3B82F6; }
|
|
553
|
+
.legend-dot.negative { background: #8B5CF6; }
|
|
554
|
+
|
|
262
555
|
.entries-list { display: flex; flex-direction: column; gap: 12px; }
|
|
263
556
|
|
|
264
557
|
.entry-card {
|
|
@@ -284,7 +577,14 @@ function getHTML() {
|
|
|
284
577
|
gap: 12px;
|
|
285
578
|
}
|
|
286
579
|
|
|
287
|
-
.entry-key { font-weight: 600; font-size: 15px; color: var(--text-primary); word-break: break-word; }
|
|
580
|
+
.entry-key { font-weight: 600; font-size: 15px; color: var(--text-primary); word-break: break-word; flex: 1; }
|
|
581
|
+
|
|
582
|
+
.entry-badges {
|
|
583
|
+
display: flex;
|
|
584
|
+
align-items: center;
|
|
585
|
+
gap: 8px;
|
|
586
|
+
flex-shrink: 0;
|
|
587
|
+
}
|
|
288
588
|
|
|
289
589
|
.entry-namespace {
|
|
290
590
|
font-size: 12px;
|
|
@@ -567,6 +867,10 @@ function getHTML() {
|
|
|
567
867
|
<svg viewBox="0 0 24 24"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>
|
|
568
868
|
Add Memory
|
|
569
869
|
</button>
|
|
870
|
+
<button class="btn" onclick="openEmbeddingModal()">
|
|
871
|
+
<svg viewBox="0 0 24 24"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg>
|
|
872
|
+
Embeddings
|
|
873
|
+
</button>
|
|
570
874
|
<button class="btn" onclick="loadData()">
|
|
571
875
|
<svg viewBox="0 0 24 24"><path d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/></svg>
|
|
572
876
|
Refresh
|
|
@@ -582,6 +886,11 @@ function getHTML() {
|
|
|
582
886
|
<svg viewBox="0 0 24 24"><path d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/></svg>
|
|
583
887
|
<input type="text" id="search-input" placeholder="Search memories..." oninput="debounceSearch()">
|
|
584
888
|
</div>
|
|
889
|
+
<select id="search-type" class="search-type-select" onchange="debounceSearch()">
|
|
890
|
+
<option value="hybrid">Hybrid (Text + Semantic)</option>
|
|
891
|
+
<option value="text">Text Only (FTS5)</option>
|
|
892
|
+
<option value="vector">Semantic Only (Vector)</option>
|
|
893
|
+
</select>
|
|
585
894
|
</div>
|
|
586
895
|
|
|
587
896
|
<div id="entries-container" class="entries-list">
|
|
@@ -676,6 +985,57 @@ function getHTML() {
|
|
|
676
985
|
</div>
|
|
677
986
|
</div>
|
|
678
987
|
|
|
988
|
+
<!-- Embedding Management Modal -->
|
|
989
|
+
<div id="embedding-modal" class="modal-overlay" onclick="closeEmbeddingModal(event)">
|
|
990
|
+
<div class="modal" style="max-width: 500px;" onclick="event.stopPropagation()">
|
|
991
|
+
<div class="modal-header">
|
|
992
|
+
<span class="modal-title">Manage Embeddings</span>
|
|
993
|
+
<button class="modal-close" onclick="closeEmbeddingModal()">
|
|
994
|
+
<svg viewBox="0 0 24 24"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>
|
|
995
|
+
</button>
|
|
996
|
+
</div>
|
|
997
|
+
<div class="modal-body" id="embedding-modal-body">
|
|
998
|
+
<div class="embedding-stats" id="embedding-stats">
|
|
999
|
+
<div class="embedding-stat">
|
|
1000
|
+
<div class="embedding-stat-value" id="stat-total">-</div>
|
|
1001
|
+
<div class="embedding-stat-label">Total</div>
|
|
1002
|
+
</div>
|
|
1003
|
+
<div class="embedding-stat">
|
|
1004
|
+
<div class="embedding-stat-value success" id="stat-with">-</div>
|
|
1005
|
+
<div class="embedding-stat-label">With Embedding</div>
|
|
1006
|
+
</div>
|
|
1007
|
+
<div class="embedding-stat">
|
|
1008
|
+
<div class="embedding-stat-value warning" id="stat-without">-</div>
|
|
1009
|
+
<div class="embedding-stat-label">Without</div>
|
|
1010
|
+
</div>
|
|
1011
|
+
</div>
|
|
1012
|
+
<p style="font-size: 14px; color: var(--text-secondary); margin-bottom: 12px;">
|
|
1013
|
+
Vector embeddings enable semantic search - finding memories by meaning, not just keywords.
|
|
1014
|
+
</p>
|
|
1015
|
+
<p style="font-size: 12px; color: var(--text-muted); margin-bottom: 20px; padding: 10px; background: var(--bg-card); border-radius: 6px;">
|
|
1016
|
+
<strong>Model:</strong> multilingual-e5-small (100+ languages, optimized for retrieval)
|
|
1017
|
+
</p>
|
|
1018
|
+
<div id="embedding-progress" style="display: none;">
|
|
1019
|
+
<div class="progress-bar">
|
|
1020
|
+
<div class="progress-bar-fill" id="progress-fill" style="width: 0%"></div>
|
|
1021
|
+
</div>
|
|
1022
|
+
<div class="progress-text" id="progress-text">Processing...</div>
|
|
1023
|
+
</div>
|
|
1024
|
+
</div>
|
|
1025
|
+
<div class="modal-footer" id="embedding-modal-footer">
|
|
1026
|
+
<button class="btn" onclick="closeEmbeddingModal()">Close</button>
|
|
1027
|
+
<button class="btn btn-primary" id="btn-generate-missing" onclick="generateEmbeddings('missing')">
|
|
1028
|
+
<svg viewBox="0 0 24 24" style="width:16px;height:16px;fill:currentColor"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>
|
|
1029
|
+
Generate Missing
|
|
1030
|
+
</button>
|
|
1031
|
+
<button class="btn btn-success" id="btn-regenerate-all" onclick="generateEmbeddings('all')">
|
|
1032
|
+
<svg viewBox="0 0 24 24" style="width:16px;height:16px;fill:currentColor"><path d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/></svg>
|
|
1033
|
+
Re-generate All
|
|
1034
|
+
</button>
|
|
1035
|
+
</div>
|
|
1036
|
+
</div>
|
|
1037
|
+
</div>
|
|
1038
|
+
|
|
679
1039
|
<script>
|
|
680
1040
|
let currentNamespace = '';
|
|
681
1041
|
let currentSearch = '';
|
|
@@ -708,10 +1068,6 @@ function getHTML() {
|
|
|
708
1068
|
<div class="stat-label">Namespaces</div>
|
|
709
1069
|
<div class="stat-value">\${Object.keys(stats.byNamespace || {}).length}</div>
|
|
710
1070
|
</div>
|
|
711
|
-
<div class="stat-card">
|
|
712
|
-
<div class="stat-label">Types</div>
|
|
713
|
-
<div class="stat-value">\${Object.keys(stats.byType || {}).length}</div>
|
|
714
|
-
</div>
|
|
715
1071
|
\`;
|
|
716
1072
|
}
|
|
717
1073
|
|
|
@@ -730,13 +1086,32 @@ function getHTML() {
|
|
|
730
1086
|
const container = document.getElementById('entries-container');
|
|
731
1087
|
container.innerHTML = '<div class="loading"><div class="spinner"></div></div>';
|
|
732
1088
|
|
|
733
|
-
const params = new URLSearchParams({ limit: pageSize, offset: currentPage * pageSize });
|
|
734
|
-
if (currentNamespace) params.set('namespace', currentNamespace);
|
|
735
|
-
if (currentSearch) params.set('search', currentSearch);
|
|
736
|
-
|
|
737
1089
|
try {
|
|
738
|
-
|
|
739
|
-
|
|
1090
|
+
let entries;
|
|
1091
|
+
|
|
1092
|
+
if (currentSearch && currentSearch.trim()) {
|
|
1093
|
+
// Use hybrid search endpoint when searching
|
|
1094
|
+
const searchType = document.getElementById('search-type').value;
|
|
1095
|
+
const params = new URLSearchParams({
|
|
1096
|
+
q: currentSearch,
|
|
1097
|
+
type: searchType,
|
|
1098
|
+
limit: String(pageSize)
|
|
1099
|
+
});
|
|
1100
|
+
if (currentNamespace) params.set('namespace', currentNamespace);
|
|
1101
|
+
|
|
1102
|
+
const res = await fetch('/api/search?' + params);
|
|
1103
|
+
entries = await res.json();
|
|
1104
|
+
} else {
|
|
1105
|
+
// Use standard entries endpoint for listing
|
|
1106
|
+
const params = new URLSearchParams({
|
|
1107
|
+
limit: String(pageSize),
|
|
1108
|
+
offset: String(currentPage * pageSize)
|
|
1109
|
+
});
|
|
1110
|
+
if (currentNamespace) params.set('namespace', currentNamespace);
|
|
1111
|
+
|
|
1112
|
+
const res = await fetch('/api/entries?' + params);
|
|
1113
|
+
entries = await res.json();
|
|
1114
|
+
}
|
|
740
1115
|
|
|
741
1116
|
if (!Array.isArray(entries) || entries.length === 0) {
|
|
742
1117
|
container.innerHTML = \`
|
|
@@ -751,7 +1126,14 @@ function getHTML() {
|
|
|
751
1126
|
<div class="entry-card" onclick="showDetail('\${entry.id}')">
|
|
752
1127
|
<div class="entry-header">
|
|
753
1128
|
<span class="entry-key">\${escapeHtml(entry.key)}</span>
|
|
754
|
-
<
|
|
1129
|
+
<div class="entry-badges">
|
|
1130
|
+
\${entry.score !== undefined ? \`<span class="score-badge">\${(entry.score * 100).toFixed(1)}%</span>\` : ''}
|
|
1131
|
+
\${entry.hasEmbedding ?
|
|
1132
|
+
\`<span class="vector-badge has-vector" title="Vector embedding enabled">Vec</span>\` :
|
|
1133
|
+
\`<span class="vector-badge no-vector" title="No vector embedding">--</span>\`
|
|
1134
|
+
}
|
|
1135
|
+
<span class="entry-namespace">\${entry.namespace}</span>
|
|
1136
|
+
</div>
|
|
755
1137
|
</div>
|
|
756
1138
|
<div class="entry-content truncated">\${escapeHtml(entry.content)}</div>
|
|
757
1139
|
<div class="entry-footer">
|
|
@@ -800,6 +1182,59 @@ function getHTML() {
|
|
|
800
1182
|
function prevPage() { if (currentPage > 0) { currentPage--; loadEntries(); } }
|
|
801
1183
|
function nextPage() { currentPage++; loadEntries(); }
|
|
802
1184
|
|
|
1185
|
+
function renderEmbeddingViz(embedding) {
|
|
1186
|
+
if (!embedding || !embedding.hasEmbedding) {
|
|
1187
|
+
return \`
|
|
1188
|
+
<div class="embedding-section">
|
|
1189
|
+
<div class="embedding-header">
|
|
1190
|
+
<span class="embedding-status no-embedding">
|
|
1191
|
+
<svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/></svg>
|
|
1192
|
+
No Embedding
|
|
1193
|
+
</span>
|
|
1194
|
+
</div>
|
|
1195
|
+
<p style="font-size: 12px; color: var(--text-muted); margin: 0;">
|
|
1196
|
+
This entry doesn't have a vector embedding. Edit and save to generate one.
|
|
1197
|
+
</p>
|
|
1198
|
+
</div>
|
|
1199
|
+
\`;
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
const preview = embedding.preview || [];
|
|
1203
|
+
const maxVal = Math.max(...preview.map(Math.abs), 0.001);
|
|
1204
|
+
|
|
1205
|
+
const bars = preview.map(val => {
|
|
1206
|
+
const height = Math.abs(val) / maxVal * 100;
|
|
1207
|
+
const isPositive = val >= 0;
|
|
1208
|
+
return \`<div class="embedding-bar \${isPositive ? 'positive' : 'negative'}" style="height: \${height}%" title="\${val.toFixed(4)}"></div>\`;
|
|
1209
|
+
}).join('');
|
|
1210
|
+
|
|
1211
|
+
return \`
|
|
1212
|
+
<div class="embedding-section">
|
|
1213
|
+
<div class="embedding-header">
|
|
1214
|
+
<span class="embedding-status has-embedding">
|
|
1215
|
+
<svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/></svg>
|
|
1216
|
+
Vector Enabled
|
|
1217
|
+
</span>
|
|
1218
|
+
<span class="embedding-dims">\${embedding.dimensions}D</span>
|
|
1219
|
+
</div>
|
|
1220
|
+
<div class="embedding-viz">
|
|
1221
|
+
\${bars}
|
|
1222
|
+
</div>
|
|
1223
|
+
<div class="embedding-legend">
|
|
1224
|
+
<div class="embedding-legend-item">
|
|
1225
|
+
<span class="legend-dot positive"></span>
|
|
1226
|
+
Positive values
|
|
1227
|
+
</div>
|
|
1228
|
+
<div class="embedding-legend-item">
|
|
1229
|
+
<span class="legend-dot negative"></span>
|
|
1230
|
+
Negative values
|
|
1231
|
+
</div>
|
|
1232
|
+
<span>First 20 dimensions</span>
|
|
1233
|
+
</div>
|
|
1234
|
+
</div>
|
|
1235
|
+
\`;
|
|
1236
|
+
}
|
|
1237
|
+
|
|
803
1238
|
async function showDetail(id) {
|
|
804
1239
|
try {
|
|
805
1240
|
const res = await fetch('/api/entry/' + id);
|
|
@@ -830,6 +1265,7 @@ function getHTML() {
|
|
|
830
1265
|
<div class="detail-label">Created</div>
|
|
831
1266
|
<div class="detail-value">\${new Date(entry.created_at).toLocaleString()}</div>
|
|
832
1267
|
</div>
|
|
1268
|
+
\${renderEmbeddingViz(entry.embedding)}
|
|
833
1269
|
<div class="detail-actions">
|
|
834
1270
|
<button class="btn btn-primary" onclick="openEditModal('\${entry.id}')">
|
|
835
1271
|
<svg viewBox="0 0 24 24"><path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"/></svg>
|
|
@@ -951,6 +1387,85 @@ function getHTML() {
|
|
|
951
1387
|
}
|
|
952
1388
|
}
|
|
953
1389
|
|
|
1390
|
+
async function openEmbeddingModal() {
|
|
1391
|
+
document.getElementById('embedding-modal').classList.add('active');
|
|
1392
|
+
document.getElementById('embedding-progress').style.display = 'none';
|
|
1393
|
+
document.getElementById('embedding-modal-footer').style.display = 'flex';
|
|
1394
|
+
|
|
1395
|
+
// Load embedding stats
|
|
1396
|
+
try {
|
|
1397
|
+
const res = await fetch('/api/embeddings/stats');
|
|
1398
|
+
const embeddingStats = await res.json();
|
|
1399
|
+
document.getElementById('stat-total').textContent = embeddingStats.total;
|
|
1400
|
+
document.getElementById('stat-with').textContent = embeddingStats.withEmbedding;
|
|
1401
|
+
document.getElementById('stat-without').textContent = embeddingStats.withoutEmbedding;
|
|
1402
|
+
|
|
1403
|
+
// Disable buttons if nothing to do
|
|
1404
|
+
document.getElementById('btn-generate-missing').disabled = embeddingStats.withoutEmbedding === 0;
|
|
1405
|
+
document.getElementById('btn-regenerate-all').disabled = embeddingStats.total === 0;
|
|
1406
|
+
} catch (error) {
|
|
1407
|
+
console.error('Failed to load embedding stats:', error);
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
function closeEmbeddingModal(event) {
|
|
1412
|
+
if (!event || event.target.id === 'embedding-modal') {
|
|
1413
|
+
document.getElementById('embedding-modal').classList.remove('active');
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
async function generateEmbeddings(mode) {
|
|
1418
|
+
const progressEl = document.getElementById('embedding-progress');
|
|
1419
|
+
const progressFill = document.getElementById('progress-fill');
|
|
1420
|
+
const progressText = document.getElementById('progress-text');
|
|
1421
|
+
const footerEl = document.getElementById('embedding-modal-footer');
|
|
1422
|
+
|
|
1423
|
+
// Show progress, hide buttons
|
|
1424
|
+
progressEl.style.display = 'block';
|
|
1425
|
+
footerEl.style.display = 'none';
|
|
1426
|
+
progressFill.style.width = '0%';
|
|
1427
|
+
progressText.textContent = mode === 'missing' ? 'Generating missing embeddings...' : 'Re-generating all embeddings...';
|
|
1428
|
+
|
|
1429
|
+
// Animate progress bar while waiting
|
|
1430
|
+
let progress = 0;
|
|
1431
|
+
const interval = setInterval(() => {
|
|
1432
|
+
progress += Math.random() * 10;
|
|
1433
|
+
if (progress > 90) progress = 90;
|
|
1434
|
+
progressFill.style.width = progress + '%';
|
|
1435
|
+
}, 200);
|
|
1436
|
+
|
|
1437
|
+
try {
|
|
1438
|
+
const res = await fetch('/api/embeddings/generate', {
|
|
1439
|
+
method: 'POST',
|
|
1440
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1441
|
+
body: JSON.stringify({ mode }),
|
|
1442
|
+
});
|
|
1443
|
+
|
|
1444
|
+
clearInterval(interval);
|
|
1445
|
+
progressFill.style.width = '100%';
|
|
1446
|
+
|
|
1447
|
+
const result = await res.json();
|
|
1448
|
+
progressText.textContent = result.message || 'Done!';
|
|
1449
|
+
|
|
1450
|
+
setTimeout(() => {
|
|
1451
|
+
showToast(result.message || 'Embeddings generated', 'success');
|
|
1452
|
+
closeEmbeddingModal();
|
|
1453
|
+
loadData();
|
|
1454
|
+
}, 1000);
|
|
1455
|
+
} catch (error) {
|
|
1456
|
+
clearInterval(interval);
|
|
1457
|
+
progressFill.style.width = '0%';
|
|
1458
|
+
progressText.textContent = 'Failed to generate embeddings';
|
|
1459
|
+
showToast('Failed to generate embeddings', 'error');
|
|
1460
|
+
|
|
1461
|
+
// Re-show buttons after error
|
|
1462
|
+
setTimeout(() => {
|
|
1463
|
+
progressEl.style.display = 'none';
|
|
1464
|
+
footerEl.style.display = 'flex';
|
|
1465
|
+
}, 2000);
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
|
|
954
1469
|
function showToast(message, type = 'success') {
|
|
955
1470
|
const toast = document.createElement('div');
|
|
956
1471
|
toast.className = 'toast ' + type;
|
|
@@ -985,6 +1500,7 @@ function getHTML() {
|
|
|
985
1500
|
closeDetailModal();
|
|
986
1501
|
closeFormModal();
|
|
987
1502
|
closeDeleteModal();
|
|
1503
|
+
closeEmbeddingModal();
|
|
988
1504
|
}
|
|
989
1505
|
});
|
|
990
1506
|
|
|
@@ -1001,18 +1517,17 @@ async function readBody(req) {
|
|
|
1001
1517
|
req.on('error', reject);
|
|
1002
1518
|
});
|
|
1003
1519
|
}
|
|
1004
|
-
|
|
1520
|
+
function handleRequest(req, res) {
|
|
1005
1521
|
const url = new URL(req.url || '/', `http://localhost:${PORT}`);
|
|
1006
1522
|
const method = req.method || 'GET';
|
|
1007
1523
|
res.setHeader('Content-Type', 'application/json');
|
|
1008
1524
|
try {
|
|
1009
|
-
const db =
|
|
1525
|
+
const db = getDatabase();
|
|
1010
1526
|
// Serve HTML
|
|
1011
1527
|
if (url.pathname === '/' && method === 'GET') {
|
|
1012
1528
|
res.setHeader('Content-Type', 'text/html');
|
|
1013
1529
|
res.writeHead(200);
|
|
1014
1530
|
res.end(getHTML());
|
|
1015
|
-
db.close();
|
|
1016
1531
|
return;
|
|
1017
1532
|
}
|
|
1018
1533
|
// GET stats
|
|
@@ -1020,10 +1535,9 @@ async function handleRequest(req, res) {
|
|
|
1020
1535
|
const stats = getStats(db);
|
|
1021
1536
|
res.writeHead(200);
|
|
1022
1537
|
res.end(JSON.stringify(stats));
|
|
1023
|
-
db.close();
|
|
1024
1538
|
return;
|
|
1025
1539
|
}
|
|
1026
|
-
// GET entries
|
|
1540
|
+
// GET entries (standard listing with optional FTS search)
|
|
1027
1541
|
if (url.pathname === '/api/entries' && method === 'GET') {
|
|
1028
1542
|
const namespace = url.searchParams.get('namespace') || undefined;
|
|
1029
1543
|
const limit = parseInt(url.searchParams.get('limit') || '50', 10);
|
|
@@ -1032,30 +1546,74 @@ async function handleRequest(req, res) {
|
|
|
1032
1546
|
const entries = getEntries(db, namespace, limit, offset, search);
|
|
1033
1547
|
res.writeHead(200);
|
|
1034
1548
|
res.end(JSON.stringify(entries));
|
|
1035
|
-
db.close();
|
|
1036
1549
|
return;
|
|
1037
1550
|
}
|
|
1038
|
-
//
|
|
1551
|
+
// GET hybrid search (new endpoint with vector support)
|
|
1552
|
+
if (url.pathname === '/api/search' && method === 'GET') {
|
|
1553
|
+
const query = url.searchParams.get('q') || '';
|
|
1554
|
+
const searchType = (url.searchParams.get('type') || 'hybrid');
|
|
1555
|
+
const limit = parseInt(url.searchParams.get('limit') || '20', 10);
|
|
1556
|
+
const namespace = url.searchParams.get('namespace') || undefined;
|
|
1557
|
+
getSearchEngine()
|
|
1558
|
+
.then((searchEngine) => searchEntries(searchEngine, query, { type: searchType, namespace, limit }))
|
|
1559
|
+
.then((results) => {
|
|
1560
|
+
res.writeHead(200);
|
|
1561
|
+
res.end(JSON.stringify(results));
|
|
1562
|
+
})
|
|
1563
|
+
.catch((error) => {
|
|
1564
|
+
res.writeHead(500);
|
|
1565
|
+
res.end(JSON.stringify({ error: error instanceof Error ? error.message : 'Search failed' }));
|
|
1566
|
+
});
|
|
1567
|
+
return;
|
|
1568
|
+
}
|
|
1569
|
+
// POST create entry (direct DB for compatibility with existing schema)
|
|
1039
1570
|
if (url.pathname === '/api/entries' && method === 'POST') {
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1571
|
+
readBody(req)
|
|
1572
|
+
.then(async (body) => {
|
|
1573
|
+
const data = JSON.parse(body);
|
|
1574
|
+
const now = Date.now();
|
|
1575
|
+
const id = `mem_${now}_${Math.random().toString(36).slice(2, 10)}`;
|
|
1576
|
+
const tags = JSON.stringify(data.tags || []);
|
|
1577
|
+
// Generate embedding for the content
|
|
1578
|
+
let embeddingBuffer = null;
|
|
1579
|
+
try {
|
|
1580
|
+
const embeddingsService = await getEmbeddingsService();
|
|
1581
|
+
const result = await embeddingsService.embed(data.content);
|
|
1582
|
+
embeddingBuffer = Buffer.from(result.embedding.buffer);
|
|
1583
|
+
}
|
|
1584
|
+
catch (e) {
|
|
1585
|
+
console.warn('[WebViewer] Failed to generate embedding:', e);
|
|
1586
|
+
}
|
|
1587
|
+
db.prepare(`INSERT INTO memory_entries (id, key, content, type, namespace, tags, embedding, created_at, updated_at)
|
|
1588
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, data.key, data.content, data.type || 'semantic', data.namespace || 'general', tags, embeddingBuffer, now, now);
|
|
1589
|
+
res.writeHead(201);
|
|
1590
|
+
res.end(JSON.stringify({ id, success: true }));
|
|
1591
|
+
})
|
|
1592
|
+
.catch((error) => {
|
|
1593
|
+
res.writeHead(500);
|
|
1594
|
+
res.end(JSON.stringify({ error: error instanceof Error ? error.message : 'Internal error' }));
|
|
1595
|
+
});
|
|
1050
1596
|
return;
|
|
1051
1597
|
}
|
|
1052
1598
|
// GET single entry
|
|
1053
1599
|
if (url.pathname.startsWith('/api/entry/') && method === 'GET') {
|
|
1054
1600
|
const id = url.pathname.split('/').pop();
|
|
1055
|
-
const
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1601
|
+
const row = db.prepare('SELECT * FROM memory_entries WHERE id = ?').get(id);
|
|
1602
|
+
if (row) {
|
|
1603
|
+
// Extract embedding info for visualization
|
|
1604
|
+
let embeddingInfo = {
|
|
1605
|
+
hasEmbedding: false,
|
|
1606
|
+
};
|
|
1607
|
+
if (row.embedding && row.embedding.length > 0) {
|
|
1608
|
+
const embedding = new Float32Array(row.embedding.buffer.slice(row.embedding.byteOffset, row.embedding.byteOffset + row.embedding.byteLength));
|
|
1609
|
+
// Get first 20 values for preview visualization
|
|
1610
|
+
const preview = Array.from(embedding.slice(0, 20));
|
|
1611
|
+
embeddingInfo = {
|
|
1612
|
+
hasEmbedding: true,
|
|
1613
|
+
dimensions: embedding.length,
|
|
1614
|
+
preview,
|
|
1615
|
+
};
|
|
1616
|
+
}
|
|
1059
1617
|
res.writeHead(200);
|
|
1060
1618
|
res.end(JSON.stringify({
|
|
1061
1619
|
id: row.id,
|
|
@@ -1066,43 +1624,135 @@ async function handleRequest(req, res) {
|
|
|
1066
1624
|
tags: JSON.parse(row.tags || '[]'),
|
|
1067
1625
|
created_at: row.created_at,
|
|
1068
1626
|
updated_at: row.updated_at,
|
|
1627
|
+
embedding: embeddingInfo,
|
|
1069
1628
|
}));
|
|
1070
1629
|
}
|
|
1071
1630
|
else {
|
|
1072
1631
|
res.writeHead(404);
|
|
1073
1632
|
res.end(JSON.stringify({ error: 'Entry not found' }));
|
|
1074
1633
|
}
|
|
1075
|
-
stmt.free();
|
|
1076
|
-
db.close();
|
|
1077
1634
|
return;
|
|
1078
1635
|
}
|
|
1079
|
-
// PUT update entry
|
|
1636
|
+
// PUT update entry (direct DB for full field updates)
|
|
1080
1637
|
if (url.pathname.startsWith('/api/entry/') && method === 'PUT') {
|
|
1081
1638
|
const id = url.pathname.split('/').pop();
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1639
|
+
if (!id) {
|
|
1640
|
+
res.writeHead(400);
|
|
1641
|
+
res.end(JSON.stringify({ error: 'Missing entry ID' }));
|
|
1642
|
+
return;
|
|
1643
|
+
}
|
|
1644
|
+
readBody(req)
|
|
1645
|
+
.then(async (body) => {
|
|
1646
|
+
const data = JSON.parse(body);
|
|
1647
|
+
const now = Date.now();
|
|
1648
|
+
const tags = JSON.stringify(data.tags || []);
|
|
1649
|
+
// Generate embedding for the updated content
|
|
1650
|
+
let embeddingBuffer = null;
|
|
1651
|
+
try {
|
|
1652
|
+
const embeddingsService = await getEmbeddingsService();
|
|
1653
|
+
const result = await embeddingsService.embed(data.content);
|
|
1654
|
+
embeddingBuffer = Buffer.from(result.embedding.buffer);
|
|
1655
|
+
}
|
|
1656
|
+
catch (e) {
|
|
1657
|
+
console.warn('[WebViewer] Failed to generate embedding:', e);
|
|
1658
|
+
}
|
|
1659
|
+
// Update with embedding
|
|
1660
|
+
const result = db.prepare(`UPDATE memory_entries SET key = ?, content = ?, type = ?, namespace = ?, tags = ?, embedding = ?, updated_at = ?
|
|
1661
|
+
WHERE id = ?`).run(data.key, data.content, data.type, data.namespace, tags, embeddingBuffer, now, id);
|
|
1662
|
+
if (result.changes > 0) {
|
|
1663
|
+
res.writeHead(200);
|
|
1664
|
+
res.end(JSON.stringify({ success: true }));
|
|
1665
|
+
}
|
|
1666
|
+
else {
|
|
1667
|
+
res.writeHead(404);
|
|
1668
|
+
res.end(JSON.stringify({ error: 'Entry not found' }));
|
|
1669
|
+
}
|
|
1670
|
+
})
|
|
1671
|
+
.catch((error) => {
|
|
1672
|
+
res.writeHead(500);
|
|
1673
|
+
res.end(JSON.stringify({ error: error instanceof Error ? error.message : 'Internal error' }));
|
|
1674
|
+
});
|
|
1091
1675
|
return;
|
|
1092
1676
|
}
|
|
1093
|
-
// DELETE entry
|
|
1677
|
+
// DELETE entry (direct DB for compatibility)
|
|
1094
1678
|
if (url.pathname.startsWith('/api/entry/') && method === 'DELETE') {
|
|
1095
1679
|
const id = url.pathname.split('/').pop();
|
|
1096
|
-
|
|
1097
|
-
|
|
1680
|
+
if (!id) {
|
|
1681
|
+
res.writeHead(400);
|
|
1682
|
+
res.end(JSON.stringify({ error: 'Missing entry ID' }));
|
|
1683
|
+
return;
|
|
1684
|
+
}
|
|
1685
|
+
const result = db.prepare('DELETE FROM memory_entries WHERE id = ?').run(id);
|
|
1686
|
+
if (result.changes > 0) {
|
|
1687
|
+
res.writeHead(200);
|
|
1688
|
+
res.end(JSON.stringify({ success: true }));
|
|
1689
|
+
}
|
|
1690
|
+
else {
|
|
1691
|
+
res.writeHead(404);
|
|
1692
|
+
res.end(JSON.stringify({ error: 'Entry not found' }));
|
|
1693
|
+
}
|
|
1694
|
+
return;
|
|
1695
|
+
}
|
|
1696
|
+
// GET embedding stats
|
|
1697
|
+
if (url.pathname === '/api/embeddings/stats' && method === 'GET') {
|
|
1698
|
+
const totalRow = db.prepare('SELECT COUNT(*) as count FROM memory_entries').get();
|
|
1699
|
+
const withEmbeddingRow = db.prepare('SELECT COUNT(*) as count FROM memory_entries WHERE embedding IS NOT NULL AND LENGTH(embedding) > 0').get();
|
|
1098
1700
|
res.writeHead(200);
|
|
1099
|
-
res.end(JSON.stringify({
|
|
1100
|
-
|
|
1701
|
+
res.end(JSON.stringify({
|
|
1702
|
+
total: totalRow?.count || 0,
|
|
1703
|
+
withEmbedding: withEmbeddingRow?.count || 0,
|
|
1704
|
+
withoutEmbedding: (totalRow?.count || 0) - (withEmbeddingRow?.count || 0),
|
|
1705
|
+
}));
|
|
1706
|
+
return;
|
|
1707
|
+
}
|
|
1708
|
+
// POST batch generate embeddings
|
|
1709
|
+
if (url.pathname === '/api/embeddings/generate' && method === 'POST') {
|
|
1710
|
+
readBody(req)
|
|
1711
|
+
.then(async (body) => {
|
|
1712
|
+
const options = JSON.parse(body || '{}');
|
|
1713
|
+
const mode = options.mode || 'missing';
|
|
1714
|
+
// Get entries to process
|
|
1715
|
+
const query = mode === 'missing'
|
|
1716
|
+
? 'SELECT id, content FROM memory_entries WHERE embedding IS NULL OR LENGTH(embedding) = 0'
|
|
1717
|
+
: 'SELECT id, content FROM memory_entries';
|
|
1718
|
+
const entries = db.prepare(query).all();
|
|
1719
|
+
if (entries.length === 0) {
|
|
1720
|
+
res.writeHead(200);
|
|
1721
|
+
res.end(JSON.stringify({ processed: 0, success: 0, failed: 0, message: 'No entries to process' }));
|
|
1722
|
+
return;
|
|
1723
|
+
}
|
|
1724
|
+
const embeddingsService = await getEmbeddingsService();
|
|
1725
|
+
let success = 0;
|
|
1726
|
+
let failed = 0;
|
|
1727
|
+
const updateStmt = db.prepare('UPDATE memory_entries SET embedding = ?, updated_at = ? WHERE id = ?');
|
|
1728
|
+
for (const entry of entries) {
|
|
1729
|
+
try {
|
|
1730
|
+
const result = await embeddingsService.embed(entry.content);
|
|
1731
|
+
const embeddingBuffer = Buffer.from(result.embedding.buffer);
|
|
1732
|
+
updateStmt.run(embeddingBuffer, Date.now(), entry.id);
|
|
1733
|
+
success++;
|
|
1734
|
+
}
|
|
1735
|
+
catch (e) {
|
|
1736
|
+
console.warn(`[WebViewer] Failed to generate embedding for ${entry.id}:`, e);
|
|
1737
|
+
failed++;
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
res.writeHead(200);
|
|
1741
|
+
res.end(JSON.stringify({
|
|
1742
|
+
processed: entries.length,
|
|
1743
|
+
success,
|
|
1744
|
+
failed,
|
|
1745
|
+
message: `Generated embeddings for ${success} entries${failed > 0 ? `, ${failed} failed` : ''}`,
|
|
1746
|
+
}));
|
|
1747
|
+
})
|
|
1748
|
+
.catch((error) => {
|
|
1749
|
+
res.writeHead(500);
|
|
1750
|
+
res.end(JSON.stringify({ error: error instanceof Error ? error.message : 'Internal error' }));
|
|
1751
|
+
});
|
|
1101
1752
|
return;
|
|
1102
1753
|
}
|
|
1103
1754
|
res.writeHead(404);
|
|
1104
1755
|
res.end(JSON.stringify({ error: 'Not found' }));
|
|
1105
|
-
db.close();
|
|
1106
1756
|
}
|
|
1107
1757
|
catch (error) {
|
|
1108
1758
|
res.writeHead(500);
|