This is page 6 of 10. Use http://codebase.md/hangwin/mcp-chrome?lines=true&page={x} to view the full context.
# Directory Structure
```
├── .gitattributes
├── .github
│ └── workflows
│ └── build-release.yml
├── .gitignore
├── .husky
│ ├── commit-msg
│ └── pre-commit
├── .prettierignore
├── .prettierrc.json
├── .vscode
│ └── extensions.json
├── app
│ ├── chrome-extension
│ │ ├── _locales
│ │ │ ├── de
│ │ │ │ └── messages.json
│ │ │ ├── en
│ │ │ │ └── messages.json
│ │ │ ├── ja
│ │ │ │ └── messages.json
│ │ │ ├── ko
│ │ │ │ └── messages.json
│ │ │ ├── zh_CN
│ │ │ │ └── messages.json
│ │ │ └── zh_TW
│ │ │ └── messages.json
│ │ ├── .env.example
│ │ ├── assets
│ │ │ └── vue.svg
│ │ ├── common
│ │ │ ├── constants.ts
│ │ │ ├── message-types.ts
│ │ │ └── tool-handler.ts
│ │ ├── entrypoints
│ │ │ ├── background
│ │ │ │ ├── index.ts
│ │ │ │ ├── native-host.ts
│ │ │ │ ├── semantic-similarity.ts
│ │ │ │ ├── storage-manager.ts
│ │ │ │ └── tools
│ │ │ │ ├── base-browser.ts
│ │ │ │ ├── browser
│ │ │ │ │ ├── bookmark.ts
│ │ │ │ │ ├── common.ts
│ │ │ │ │ ├── console.ts
│ │ │ │ │ ├── file-upload.ts
│ │ │ │ │ ├── history.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── inject-script.ts
│ │ │ │ │ ├── interaction.ts
│ │ │ │ │ ├── keyboard.ts
│ │ │ │ │ ├── network-capture-debugger.ts
│ │ │ │ │ ├── network-capture-web-request.ts
│ │ │ │ │ ├── network-request.ts
│ │ │ │ │ ├── screenshot.ts
│ │ │ │ │ ├── vector-search.ts
│ │ │ │ │ ├── web-fetcher.ts
│ │ │ │ │ └── window.ts
│ │ │ │ └── index.ts
│ │ │ ├── content.ts
│ │ │ ├── offscreen
│ │ │ │ ├── index.html
│ │ │ │ └── main.ts
│ │ │ └── popup
│ │ │ ├── App.vue
│ │ │ ├── components
│ │ │ │ ├── ConfirmDialog.vue
│ │ │ │ ├── icons
│ │ │ │ │ ├── BoltIcon.vue
│ │ │ │ │ ├── CheckIcon.vue
│ │ │ │ │ ├── DatabaseIcon.vue
│ │ │ │ │ ├── DocumentIcon.vue
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── TabIcon.vue
│ │ │ │ │ ├── TrashIcon.vue
│ │ │ │ │ └── VectorIcon.vue
│ │ │ │ ├── ModelCacheManagement.vue
│ │ │ │ └── ProgressIndicator.vue
│ │ │ ├── index.html
│ │ │ ├── main.ts
│ │ │ └── style.css
│ │ ├── eslint.config.js
│ │ ├── inject-scripts
│ │ │ ├── click-helper.js
│ │ │ ├── fill-helper.js
│ │ │ ├── inject-bridge.js
│ │ │ ├── interactive-elements-helper.js
│ │ │ ├── keyboard-helper.js
│ │ │ ├── network-helper.js
│ │ │ ├── screenshot-helper.js
│ │ │ └── web-fetcher-helper.js
│ │ ├── LICENSE
│ │ ├── package.json
│ │ ├── public
│ │ │ ├── icon
│ │ │ │ ├── 128.png
│ │ │ │ ├── 16.png
│ │ │ │ ├── 32.png
│ │ │ │ ├── 48.png
│ │ │ │ └── 96.png
│ │ │ ├── libs
│ │ │ │ └── ort.min.js
│ │ │ └── wxt.svg
│ │ ├── README.md
│ │ ├── tsconfig.json
│ │ ├── utils
│ │ │ ├── content-indexer.ts
│ │ │ ├── i18n.ts
│ │ │ ├── image-utils.ts
│ │ │ ├── lru-cache.ts
│ │ │ ├── model-cache-manager.ts
│ │ │ ├── offscreen-manager.ts
│ │ │ ├── semantic-similarity-engine.ts
│ │ │ ├── simd-math-engine.ts
│ │ │ ├── text-chunker.ts
│ │ │ └── vector-database.ts
│ │ ├── workers
│ │ │ ├── ort-wasm-simd-threaded.jsep.mjs
│ │ │ ├── ort-wasm-simd-threaded.jsep.wasm
│ │ │ ├── ort-wasm-simd-threaded.mjs
│ │ │ ├── ort-wasm-simd-threaded.wasm
│ │ │ ├── simd_math_bg.wasm
│ │ │ ├── simd_math.js
│ │ │ └── similarity.worker.js
│ │ └── wxt.config.ts
│ └── native-server
│ ├── debug.sh
│ ├── install.md
│ ├── jest.config.js
│ ├── package.json
│ ├── README.md
│ ├── src
│ │ ├── cli.ts
│ │ ├── constant
│ │ │ └── index.ts
│ │ ├── file-handler.ts
│ │ ├── index.ts
│ │ ├── mcp
│ │ │ ├── mcp-server-stdio.ts
│ │ │ ├── mcp-server.ts
│ │ │ ├── register-tools.ts
│ │ │ └── stdio-config.json
│ │ ├── native-messaging-host.ts
│ │ ├── scripts
│ │ │ ├── browser-config.ts
│ │ │ ├── build.ts
│ │ │ ├── constant.ts
│ │ │ ├── postinstall.ts
│ │ │ ├── register-dev.ts
│ │ │ ├── register.ts
│ │ │ ├── run_host.bat
│ │ │ ├── run_host.sh
│ │ │ └── utils.ts
│ │ ├── server
│ │ │ ├── index.ts
│ │ │ └── server.test.ts
│ │ └── util
│ │ └── logger.ts
│ └── tsconfig.json
├── commitlint.config.cjs
├── docs
│ ├── ARCHITECTURE_zh.md
│ ├── ARCHITECTURE.md
│ ├── CHANGELOG.md
│ ├── CONTRIBUTING_zh.md
│ ├── CONTRIBUTING.md
│ ├── TOOLS_zh.md
│ ├── TOOLS.md
│ ├── TROUBLESHOOTING_zh.md
│ ├── TROUBLESHOOTING.md
│ └── WINDOWS_INSTALL_zh.md
├── eslint.config.js
├── LICENSE
├── package.json
├── packages
│ ├── shared
│ │ ├── package.json
│ │ ├── src
│ │ │ ├── constants.ts
│ │ │ ├── index.ts
│ │ │ ├── tools.ts
│ │ │ └── types.ts
│ │ └── tsconfig.json
│ └── wasm-simd
│ ├── .gitignore
│ ├── BUILD.md
│ ├── Cargo.toml
│ ├── package.json
│ ├── README.md
│ └── src
│ └── lib.rs
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
├── prompt
│ ├── content-analize.md
│ ├── excalidraw-prompt.md
│ └── modify-web.md
├── README_zh.md
├── README.md
├── releases
│ ├── chrome-extension
│ │ └── latest
│ │ └── chrome-mcp-server-lastest.zip
│ └── README.md
└── test-inject-script.js
```
# Files
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/popup/App.vue:
--------------------------------------------------------------------------------
```vue
1 | <template>
2 | <div class="popup-container">
3 | <div class="header">
4 | <div class="header-content">
5 | <h1 class="header-title">Chrome MCP Server</h1>
6 | </div>
7 | </div>
8 | <div class="content">
9 | <div class="section">
10 | <h2 class="section-title">{{ getMessage('nativeServerConfigLabel') }}</h2>
11 | <div class="config-card">
12 | <div class="status-section">
13 | <div class="status-header">
14 | <p class="status-label">{{ getMessage('runningStatusLabel') }}</p>
15 | <button
16 | class="refresh-status-button"
17 | @click="refreshServerStatus"
18 | :title="getMessage('refreshStatusButton')"
19 | >
20 | 🔄
21 | </button>
22 | </div>
23 | <div class="status-info">
24 | <span :class="['status-dot', getStatusClass()]"></span>
25 | <span class="status-text">{{ getStatusText() }}</span>
26 | </div>
27 | <div v-if="serverStatus.lastUpdated" class="status-timestamp">
28 | {{ getMessage('lastUpdatedLabel') }}
29 | {{ new Date(serverStatus.lastUpdated).toLocaleTimeString() }}
30 | </div>
31 | </div>
32 |
33 | <div v-if="showMcpConfig" class="mcp-config-section">
34 | <div class="mcp-config-header">
35 | <p class="mcp-config-label">{{ getMessage('mcpServerConfigLabel') }}</p>
36 | <button class="copy-config-button" @click="copyMcpConfig">
37 | {{ copyButtonText }}
38 | </button>
39 | </div>
40 | <div class="mcp-config-content">
41 | <pre class="mcp-config-json">{{ mcpConfigJson }}</pre>
42 | </div>
43 | </div>
44 | <div class="port-section">
45 | <label for="port" class="port-label">{{ getMessage('connectionPortLabel') }}</label>
46 | <input
47 | type="text"
48 | id="port"
49 | :value="nativeServerPort"
50 | @input="updatePort"
51 | class="port-input"
52 | />
53 | </div>
54 |
55 | <button class="connect-button" :disabled="isConnecting" @click="testNativeConnection">
56 | <BoltIcon />
57 | <span>{{
58 | isConnecting
59 | ? getMessage('connectingStatus')
60 | : nativeConnectionStatus === 'connected'
61 | ? getMessage('disconnectButton')
62 | : getMessage('connectButton')
63 | }}</span>
64 | </button>
65 | </div>
66 | </div>
67 |
68 | <div class="section">
69 | <h2 class="section-title">{{ getMessage('semanticEngineLabel') }}</h2>
70 | <div class="semantic-engine-card">
71 | <div class="semantic-engine-status">
72 | <div class="status-info">
73 | <span :class="['status-dot', getSemanticEngineStatusClass()]"></span>
74 | <span class="status-text">{{ getSemanticEngineStatusText() }}</span>
75 | </div>
76 | <div v-if="semanticEngineLastUpdated" class="status-timestamp">
77 | {{ getMessage('lastUpdatedLabel') }}
78 | {{ new Date(semanticEngineLastUpdated).toLocaleTimeString() }}
79 | </div>
80 | </div>
81 |
82 | <ProgressIndicator
83 | v-if="isSemanticEngineInitializing"
84 | :visible="isSemanticEngineInitializing"
85 | :text="semanticEngineInitProgress"
86 | :showSpinner="true"
87 | />
88 |
89 | <button
90 | class="semantic-engine-button"
91 | :disabled="isSemanticEngineInitializing"
92 | @click="initializeSemanticEngine"
93 | >
94 | <BoltIcon />
95 | <span>{{ getSemanticEngineButtonText() }}</span>
96 | </button>
97 | </div>
98 | </div>
99 |
100 | <div class="section">
101 | <h2 class="section-title">{{ getMessage('embeddingModelLabel') }}</h2>
102 |
103 | <ProgressIndicator
104 | v-if="isModelSwitching || isModelDownloading"
105 | :visible="isModelSwitching || isModelDownloading"
106 | :text="getProgressText()"
107 | :showSpinner="true"
108 | />
109 | <div v-if="modelInitializationStatus === 'error'" class="error-card">
110 | <div class="error-content">
111 | <div class="error-icon">⚠️</div>
112 | <div class="error-details">
113 | <p class="error-title">{{ getMessage('semanticEngineInitFailedStatus') }}</p>
114 | <p class="error-message">{{
115 | modelErrorMessage || getMessage('semanticEngineInitFailedStatus')
116 | }}</p>
117 | <p class="error-suggestion">{{ getErrorTypeText() }}</p>
118 | </div>
119 | </div>
120 | <button
121 | class="retry-button"
122 | @click="retryModelInitialization"
123 | :disabled="isModelSwitching || isModelDownloading"
124 | >
125 | <span>🔄</span>
126 | <span>{{ getMessage('retryButton') }}</span>
127 | </button>
128 | </div>
129 |
130 | <div class="model-list">
131 | <div
132 | v-for="model in availableModels"
133 | :key="model.preset"
134 | :class="[
135 | 'model-card',
136 | {
137 | selected: currentModel === model.preset,
138 | disabled: isModelSwitching || isModelDownloading,
139 | },
140 | ]"
141 | @click="
142 | !isModelSwitching && !isModelDownloading && switchModel(model.preset as ModelPreset)
143 | "
144 | >
145 | <div class="model-header">
146 | <div class="model-info">
147 | <p class="model-name" :class="{ 'selected-text': currentModel === model.preset }">
148 | {{ model.preset }}
149 | </p>
150 | <p class="model-description">{{ getModelDescription(model) }}</p>
151 | </div>
152 | <div v-if="currentModel === model.preset" class="check-icon">
153 | <CheckIcon class="text-white" />
154 | </div>
155 | </div>
156 | <div class="model-tags">
157 | <span class="model-tag performance">{{ getPerformanceText(model.performance) }}</span>
158 | <span class="model-tag size">{{ model.size }}</span>
159 | <span class="model-tag dimension">{{ model.dimension }}D</span>
160 | </div>
161 | </div>
162 | </div>
163 | </div>
164 |
165 | <div class="section">
166 | <h2 class="section-title">{{ getMessage('indexDataManagementLabel') }}</h2>
167 | <div class="stats-grid">
168 | <div class="stats-card">
169 | <div class="stats-header">
170 | <p class="stats-label">{{ getMessage('indexedPagesLabel') }}</p>
171 | <span class="stats-icon violet">
172 | <DocumentIcon />
173 | </span>
174 | </div>
175 | <p class="stats-value">{{ storageStats?.indexedPages || 0 }}</p>
176 | </div>
177 |
178 | <div class="stats-card">
179 | <div class="stats-header">
180 | <p class="stats-label">{{ getMessage('indexSizeLabel') }}</p>
181 | <span class="stats-icon teal">
182 | <DatabaseIcon />
183 | </span>
184 | </div>
185 | <p class="stats-value">{{ formatIndexSize() }}</p>
186 | </div>
187 |
188 | <div class="stats-card">
189 | <div class="stats-header">
190 | <p class="stats-label">{{ getMessage('activeTabsLabel') }}</p>
191 | <span class="stats-icon blue">
192 | <TabIcon />
193 | </span>
194 | </div>
195 | <p class="stats-value">{{ getActiveTabsCount() }}</p>
196 | </div>
197 |
198 | <div class="stats-card">
199 | <div class="stats-header">
200 | <p class="stats-label">{{ getMessage('vectorDocumentsLabel') }}</p>
201 | <span class="stats-icon green">
202 | <VectorIcon />
203 | </span>
204 | </div>
205 | <p class="stats-value">{{ storageStats?.totalDocuments || 0 }}</p>
206 | </div>
207 | </div>
208 | <ProgressIndicator
209 | v-if="isClearingData && clearDataProgress"
210 | :visible="isClearingData"
211 | :text="clearDataProgress"
212 | :showSpinner="true"
213 | />
214 |
215 | <button
216 | class="danger-button"
217 | :disabled="isClearingData"
218 | @click="showClearConfirmation = true"
219 | >
220 | <TrashIcon />
221 | <span>{{ isClearingData ? getMessage('clearingStatus') : getMessage('clearAllDataButton') }}</span>
222 | </button>
223 | </div>
224 |
225 | <!-- Model Cache Management Section -->
226 | <ModelCacheManagement
227 | :cache-stats="cacheStats"
228 | :is-managing-cache="isManagingCache"
229 | @cleanup-cache="cleanupCache"
230 | @clear-all-cache="clearAllCache"
231 | />
232 | </div>
233 |
234 | <div class="footer">
235 | <p class="footer-text">chrome mcp server for ai</p>
236 | </div>
237 |
238 | <ConfirmDialog
239 | :visible="showClearConfirmation"
240 | :title="getMessage('confirmClearDataTitle')"
241 | :message="getMessage('clearDataWarningMessage')"
242 | :items="[
243 | getMessage('clearDataList1'),
244 | getMessage('clearDataList2'),
245 | getMessage('clearDataList3'),
246 | ]"
247 | :warning="getMessage('clearDataIrreversibleWarning')"
248 | icon="⚠️"
249 | :confirm-text="getMessage('confirmClearButton')"
250 | :cancel-text="getMessage('cancelButton')"
251 | :confirming-text="getMessage('clearingStatus')"
252 | :is-confirming="isClearingData"
253 | @confirm="confirmClearAllData"
254 | @cancel="hideClearDataConfirmation"
255 | />
256 | </div>
257 | </template>
258 |
259 | <script lang="ts" setup>
260 | import { ref, onMounted, onUnmounted, computed } from 'vue';
261 | import {
262 | PREDEFINED_MODELS,
263 | type ModelPreset,
264 | getModelInfo,
265 | getCacheStats,
266 | clearModelCache,
267 | cleanupModelCache,
268 | } from '@/utils/semantic-similarity-engine';
269 | import { BACKGROUND_MESSAGE_TYPES } from '@/common/message-types';
270 | import { getMessage } from '@/utils/i18n';
271 |
272 | import ConfirmDialog from './components/ConfirmDialog.vue';
273 | import ProgressIndicator from './components/ProgressIndicator.vue';
274 | import ModelCacheManagement from './components/ModelCacheManagement.vue';
275 | import {
276 | DocumentIcon,
277 | DatabaseIcon,
278 | BoltIcon,
279 | TrashIcon,
280 | CheckIcon,
281 | TabIcon,
282 | VectorIcon,
283 | } from './components/icons';
284 |
285 | const nativeConnectionStatus = ref<'unknown' | 'connected' | 'disconnected'>('unknown');
286 | const isConnecting = ref(false);
287 | const nativeServerPort = ref<number>(12306);
288 |
289 | const serverStatus = ref<{
290 | isRunning: boolean;
291 | port?: number;
292 | lastUpdated: number;
293 | }>({
294 | isRunning: false,
295 | lastUpdated: Date.now(),
296 | });
297 |
298 | const showMcpConfig = computed(() => {
299 | return nativeConnectionStatus.value === 'connected' && serverStatus.value.isRunning;
300 | });
301 |
302 | const copyButtonText = ref(getMessage('copyConfigButton'));
303 |
304 | const mcpConfigJson = computed(() => {
305 | const port = serverStatus.value.port || nativeServerPort.value;
306 | const config = {
307 | mcpServers: {
308 | 'streamable-mcp-server': {
309 | type: 'streamable-http',
310 | url: `http://127.0.0.1:${port}/mcp`,
311 | },
312 | },
313 | };
314 | return JSON.stringify(config, null, 2);
315 | });
316 |
317 | const currentModel = ref<ModelPreset | null>(null);
318 | const isModelSwitching = ref(false);
319 | const modelSwitchProgress = ref('');
320 |
321 | const modelDownloadProgress = ref<number>(0);
322 | const isModelDownloading = ref(false);
323 | const modelInitializationStatus = ref<'idle' | 'downloading' | 'initializing' | 'ready' | 'error'>(
324 | 'idle',
325 | );
326 | const modelErrorMessage = ref<string>('');
327 | const modelErrorType = ref<'network' | 'file' | 'unknown' | ''>('');
328 |
329 | const selectedVersion = ref<'quantized'>('quantized');
330 |
331 | const storageStats = ref<{
332 | indexedPages: number;
333 | totalDocuments: number;
334 | totalTabs: number;
335 | indexSize: number;
336 | isInitialized: boolean;
337 | } | null>(null);
338 | const isRefreshingStats = ref(false);
339 | const isClearingData = ref(false);
340 | const showClearConfirmation = ref(false);
341 | const clearDataProgress = ref('');
342 |
343 | const semanticEngineStatus = ref<'idle' | 'initializing' | 'ready' | 'error'>('idle');
344 | const isSemanticEngineInitializing = ref(false);
345 | const semanticEngineInitProgress = ref('');
346 | const semanticEngineLastUpdated = ref<number | null>(null);
347 |
348 | // Cache management
349 | const isManagingCache = ref(false);
350 | const cacheStats = ref<{
351 | totalSize: number;
352 | totalSizeMB: number;
353 | entryCount: number;
354 | entries: Array<{
355 | url: string;
356 | size: number;
357 | sizeMB: number;
358 | timestamp: number;
359 | age: string;
360 | expired: boolean;
361 | }>;
362 | } | null>(null);
363 |
364 | const availableModels = computed(() => {
365 | return Object.entries(PREDEFINED_MODELS).map(([key, value]) => ({
366 | preset: key as ModelPreset,
367 | ...value,
368 | }));
369 | });
370 |
371 | const getStatusClass = () => {
372 | if (nativeConnectionStatus.value === 'connected') {
373 | if (serverStatus.value.isRunning) {
374 | return 'bg-emerald-500';
375 | } else {
376 | return 'bg-yellow-500';
377 | }
378 | } else if (nativeConnectionStatus.value === 'disconnected') {
379 | return 'bg-red-500';
380 | } else {
381 | return 'bg-gray-500';
382 | }
383 | };
384 |
385 | const getStatusText = () => {
386 | if (nativeConnectionStatus.value === 'connected') {
387 | if (serverStatus.value.isRunning) {
388 | return getMessage('serviceRunningStatus', [(serverStatus.value.port || 'Unknown').toString()]);
389 | } else {
390 | return getMessage('connectedServiceNotStartedStatus');
391 | }
392 | } else if (nativeConnectionStatus.value === 'disconnected') {
393 | return getMessage('serviceNotConnectedStatus');
394 | } else {
395 | return getMessage('detectingStatus');
396 | }
397 | };
398 |
399 | const formatIndexSize = () => {
400 | if (!storageStats.value?.indexSize) return '0 MB';
401 | const sizeInMB = Math.round(storageStats.value.indexSize / (1024 * 1024));
402 | return `${sizeInMB} MB`;
403 | };
404 |
405 | const getModelDescription = (model: any) => {
406 | switch (model.preset) {
407 | case 'multilingual-e5-small':
408 | return getMessage('lightweightModelDescription');
409 | case 'multilingual-e5-base':
410 | return getMessage('betterThanSmallDescription');
411 | default:
412 | return getMessage('multilingualModelDescription');
413 | }
414 | };
415 |
416 | const getPerformanceText = (performance: string) => {
417 | switch (performance) {
418 | case 'fast':
419 | return getMessage('fastPerformance');
420 | case 'balanced':
421 | return getMessage('balancedPerformance');
422 | case 'accurate':
423 | return getMessage('accuratePerformance');
424 | default:
425 | return performance;
426 | }
427 | };
428 |
429 | const getSemanticEngineStatusText = () => {
430 | switch (semanticEngineStatus.value) {
431 | case 'ready':
432 | return getMessage('semanticEngineReadyStatus');
433 | case 'initializing':
434 | return getMessage('semanticEngineInitializingStatus');
435 | case 'error':
436 | return getMessage('semanticEngineInitFailedStatus');
437 | case 'idle':
438 | default:
439 | return getMessage('semanticEngineNotInitStatus');
440 | }
441 | };
442 |
443 | const getSemanticEngineStatusClass = () => {
444 | switch (semanticEngineStatus.value) {
445 | case 'ready':
446 | return 'bg-emerald-500';
447 | case 'initializing':
448 | return 'bg-yellow-500';
449 | case 'error':
450 | return 'bg-red-500';
451 | case 'idle':
452 | default:
453 | return 'bg-gray-500';
454 | }
455 | };
456 |
457 | const getActiveTabsCount = () => {
458 | return storageStats.value?.totalTabs || 0;
459 | };
460 |
461 | const getProgressText = () => {
462 | if (isModelDownloading.value) {
463 | return getMessage('downloadingModelStatus', [modelDownloadProgress.value.toString()]);
464 | } else if (isModelSwitching.value) {
465 | return modelSwitchProgress.value || getMessage('switchingModelStatus');
466 | }
467 | return '';
468 | };
469 |
470 | const getErrorTypeText = () => {
471 | switch (modelErrorType.value) {
472 | case 'network':
473 | return getMessage('networkErrorMessage');
474 | case 'file':
475 | return getMessage('modelCorruptedErrorMessage');
476 | case 'unknown':
477 | default:
478 | return getMessage('unknownErrorMessage');
479 | }
480 | };
481 |
482 | const getSemanticEngineButtonText = () => {
483 | switch (semanticEngineStatus.value) {
484 | case 'ready':
485 | return getMessage('reinitializeButton');
486 | case 'initializing':
487 | return getMessage('initializingStatus');
488 | case 'error':
489 | return getMessage('reinitializeButton');
490 | case 'idle':
491 | default:
492 | return getMessage('initSemanticEngineButton');
493 | }
494 | };
495 |
496 | const loadCacheStats = async () => {
497 | try {
498 | cacheStats.value = await getCacheStats();
499 | } catch (error) {
500 | console.error('Failed to get cache stats:', error);
501 | cacheStats.value = null;
502 | }
503 | };
504 |
505 | const cleanupCache = async () => {
506 | if (isManagingCache.value) return;
507 |
508 | isManagingCache.value = true;
509 | try {
510 | await cleanupModelCache();
511 | // Refresh cache stats
512 | await loadCacheStats();
513 | } catch (error) {
514 | console.error('Failed to cleanup cache:', error);
515 | } finally {
516 | isManagingCache.value = false;
517 | }
518 | };
519 |
520 | const clearAllCache = async () => {
521 | if (isManagingCache.value) return;
522 |
523 | isManagingCache.value = true;
524 | try {
525 | await clearModelCache();
526 | // Refresh cache stats
527 | await loadCacheStats();
528 | } catch (error) {
529 | console.error('Failed to clear cache:', error);
530 | } finally {
531 | isManagingCache.value = false;
532 | }
533 | };
534 |
535 | const saveSemanticEngineState = async () => {
536 | try {
537 | const semanticEngineState = {
538 | status: semanticEngineStatus.value,
539 | lastUpdated: semanticEngineLastUpdated.value,
540 | };
541 | // eslint-disable-next-line no-undef
542 | await chrome.storage.local.set({ semanticEngineState });
543 | } catch (error) {
544 | console.error('保存语义引擎状态失败:', error);
545 | }
546 | };
547 |
548 | const initializeSemanticEngine = async () => {
549 | if (isSemanticEngineInitializing.value) return;
550 |
551 | const isReinitialization = semanticEngineStatus.value === 'ready';
552 | console.log(
553 | `🚀 User triggered semantic engine ${isReinitialization ? 'reinitialization' : 'initialization'}`,
554 | );
555 |
556 | isSemanticEngineInitializing.value = true;
557 | semanticEngineStatus.value = 'initializing';
558 | semanticEngineInitProgress.value = isReinitialization
559 | ? getMessage('semanticEngineInitializingStatus')
560 | : getMessage('semanticEngineInitializingStatus');
561 | semanticEngineLastUpdated.value = Date.now();
562 |
563 | await saveSemanticEngineState();
564 |
565 | try {
566 | // eslint-disable-next-line no-undef
567 | chrome.runtime
568 | .sendMessage({
569 | type: BACKGROUND_MESSAGE_TYPES.INITIALIZE_SEMANTIC_ENGINE,
570 | })
571 | .catch((error) => {
572 | console.error('❌ Error sending semantic engine initialization request:', error);
573 | });
574 |
575 | startSemanticEngineStatusPolling();
576 |
577 | semanticEngineInitProgress.value = isReinitialization
578 | ? getMessage('processingStatus')
579 | : getMessage('processingStatus');
580 | } catch (error: any) {
581 | console.error('❌ Failed to send initialization request:', error);
582 | semanticEngineStatus.value = 'error';
583 | semanticEngineInitProgress.value = `Failed to send initialization request: ${error?.message || 'Unknown error'}`;
584 |
585 | await saveSemanticEngineState();
586 |
587 | setTimeout(() => {
588 | semanticEngineInitProgress.value = '';
589 | }, 5000);
590 |
591 | isSemanticEngineInitializing.value = false;
592 | semanticEngineLastUpdated.value = Date.now();
593 | await saveSemanticEngineState();
594 | }
595 | };
596 |
597 | const checkSemanticEngineStatus = async () => {
598 | try {
599 | // eslint-disable-next-line no-undef
600 | const response = await chrome.runtime.sendMessage({
601 | type: BACKGROUND_MESSAGE_TYPES.GET_MODEL_STATUS,
602 | });
603 |
604 | if (response && response.success && response.status) {
605 | const status = response.status;
606 |
607 | if (status.initializationStatus === 'ready') {
608 | semanticEngineStatus.value = 'ready';
609 | semanticEngineLastUpdated.value = Date.now();
610 | isSemanticEngineInitializing.value = false;
611 | semanticEngineInitProgress.value = getMessage('semanticEngineReadyStatus');
612 | await saveSemanticEngineState();
613 | stopSemanticEngineStatusPolling();
614 | setTimeout(() => {
615 | semanticEngineInitProgress.value = '';
616 | }, 2000);
617 | } else if (
618 | status.initializationStatus === 'downloading' ||
619 | status.initializationStatus === 'initializing'
620 | ) {
621 | semanticEngineStatus.value = 'initializing';
622 | isSemanticEngineInitializing.value = true;
623 | semanticEngineInitProgress.value = getMessage('semanticEngineInitializingStatus');
624 | semanticEngineLastUpdated.value = Date.now();
625 | await saveSemanticEngineState();
626 | } else if (status.initializationStatus === 'error') {
627 | semanticEngineStatus.value = 'error';
628 | semanticEngineLastUpdated.value = Date.now();
629 | isSemanticEngineInitializing.value = false;
630 | semanticEngineInitProgress.value = getMessage('semanticEngineInitFailedStatus');
631 | await saveSemanticEngineState();
632 | stopSemanticEngineStatusPolling();
633 | setTimeout(() => {
634 | semanticEngineInitProgress.value = '';
635 | }, 5000);
636 | } else {
637 | semanticEngineStatus.value = 'idle';
638 | isSemanticEngineInitializing.value = false;
639 | await saveSemanticEngineState();
640 | }
641 | } else {
642 | semanticEngineStatus.value = 'idle';
643 | isSemanticEngineInitializing.value = false;
644 | await saveSemanticEngineState();
645 | }
646 | } catch (error) {
647 | console.error('Popup: Failed to check semantic engine status:', error);
648 | semanticEngineStatus.value = 'idle';
649 | isSemanticEngineInitializing.value = false;
650 | await saveSemanticEngineState();
651 | }
652 | };
653 |
654 | const retryModelInitialization = async () => {
655 | if (!currentModel.value) return;
656 |
657 | console.log('🔄 Retrying model initialization...');
658 |
659 | modelErrorMessage.value = '';
660 | modelErrorType.value = '';
661 | modelInitializationStatus.value = 'downloading';
662 | modelDownloadProgress.value = 0;
663 | isModelDownloading.value = true;
664 | await switchModel(currentModel.value);
665 | };
666 |
667 | const updatePort = async (event: Event) => {
668 | const target = event.target as HTMLInputElement;
669 | const newPort = Number(target.value);
670 | nativeServerPort.value = newPort;
671 |
672 | await savePortPreference(newPort);
673 | };
674 |
675 | const checkNativeConnection = async () => {
676 | try {
677 | // eslint-disable-next-line no-undef
678 | const response = await chrome.runtime.sendMessage({ type: 'ping_native' });
679 | nativeConnectionStatus.value = response?.connected ? 'connected' : 'disconnected';
680 | } catch (error) {
681 | console.error('检测 Native 连接状态失败:', error);
682 | nativeConnectionStatus.value = 'disconnected';
683 | }
684 | };
685 |
686 | const checkServerStatus = async () => {
687 | try {
688 | // eslint-disable-next-line no-undef
689 | const response = await chrome.runtime.sendMessage({
690 | type: BACKGROUND_MESSAGE_TYPES.GET_SERVER_STATUS,
691 | });
692 | if (response?.success && response.serverStatus) {
693 | serverStatus.value = response.serverStatus;
694 | }
695 |
696 | if (response?.connected !== undefined) {
697 | nativeConnectionStatus.value = response.connected ? 'connected' : 'disconnected';
698 | }
699 | } catch (error) {
700 | console.error('检测服务器状态失败:', error);
701 | }
702 | };
703 |
704 | const refreshServerStatus = async () => {
705 | try {
706 | // eslint-disable-next-line no-undef
707 | const response = await chrome.runtime.sendMessage({
708 | type: BACKGROUND_MESSAGE_TYPES.REFRESH_SERVER_STATUS,
709 | });
710 | if (response?.success && response.serverStatus) {
711 | serverStatus.value = response.serverStatus;
712 | }
713 |
714 | if (response?.connected !== undefined) {
715 | nativeConnectionStatus.value = response.connected ? 'connected' : 'disconnected';
716 | }
717 | } catch (error) {
718 | console.error('刷新服务器状态失败:', error);
719 | }
720 | };
721 |
722 | const copyMcpConfig = async () => {
723 | try {
724 | await navigator.clipboard.writeText(mcpConfigJson.value);
725 | copyButtonText.value = '✅' + getMessage('configCopiedNotification');
726 |
727 | setTimeout(() => {
728 | copyButtonText.value = getMessage('copyConfigButton');
729 | }, 2000);
730 | } catch (error) {
731 | console.error('复制配置失败:', error);
732 | copyButtonText.value = '❌' + getMessage('networkErrorMessage');
733 |
734 | setTimeout(() => {
735 | copyButtonText.value = getMessage('copyConfigButton');
736 | }, 2000);
737 | }
738 | };
739 |
740 | const testNativeConnection = async () => {
741 | if (isConnecting.value) return;
742 | isConnecting.value = true;
743 | try {
744 | if (nativeConnectionStatus.value === 'connected') {
745 | // eslint-disable-next-line no-undef
746 | await chrome.runtime.sendMessage({ type: 'disconnect_native' });
747 | nativeConnectionStatus.value = 'disconnected';
748 | } else {
749 | console.log(`尝试连接到端口: ${nativeServerPort.value}`);
750 | // eslint-disable-next-line no-undef
751 | const response = await chrome.runtime.sendMessage({
752 | type: 'connectNative',
753 | port: nativeServerPort.value,
754 | });
755 | if (response && response.success) {
756 | nativeConnectionStatus.value = 'connected';
757 | console.log('连接成功:', response);
758 | await savePortPreference(nativeServerPort.value);
759 | } else {
760 | nativeConnectionStatus.value = 'disconnected';
761 | console.error('连接失败:', response);
762 | }
763 | }
764 | } catch (error) {
765 | console.error('测试连接失败:', error);
766 | nativeConnectionStatus.value = 'disconnected';
767 | } finally {
768 | isConnecting.value = false;
769 | }
770 | };
771 |
772 | const loadModelPreference = async () => {
773 | try {
774 | // eslint-disable-next-line no-undef
775 | const result = await chrome.storage.local.get([
776 | 'selectedModel',
777 | 'selectedVersion',
778 | 'modelState',
779 | 'semanticEngineState',
780 | ]);
781 |
782 | if (result.selectedModel) {
783 | const storedModel = result.selectedModel as string;
784 | console.log('📋 Stored model from storage:', storedModel);
785 |
786 | if (PREDEFINED_MODELS[storedModel as ModelPreset]) {
787 | currentModel.value = storedModel as ModelPreset;
788 | console.log(`✅ Loaded valid model: ${currentModel.value}`);
789 | } else {
790 | console.warn(
791 | `⚠️ Stored model "${storedModel}" not found in PREDEFINED_MODELS, using default`,
792 | );
793 | currentModel.value = 'multilingual-e5-small';
794 | await saveModelPreference(currentModel.value);
795 | }
796 | } else {
797 | console.log('⚠️ No model found in storage, using default');
798 | currentModel.value = 'multilingual-e5-small';
799 | await saveModelPreference(currentModel.value);
800 | }
801 |
802 | selectedVersion.value = 'quantized';
803 | console.log('✅ Using quantized version (fixed)');
804 |
805 | await saveVersionPreference('quantized');
806 |
807 | if (result.modelState) {
808 | const modelState = result.modelState;
809 |
810 | if (modelState.status === 'ready') {
811 | modelInitializationStatus.value = 'ready';
812 | modelDownloadProgress.value = modelState.downloadProgress || 100;
813 | isModelDownloading.value = false;
814 | } else {
815 | modelInitializationStatus.value = 'idle';
816 | modelDownloadProgress.value = 0;
817 | isModelDownloading.value = false;
818 |
819 | await saveModelState();
820 | }
821 | } else {
822 | modelInitializationStatus.value = 'idle';
823 | modelDownloadProgress.value = 0;
824 | isModelDownloading.value = false;
825 | }
826 |
827 | if (result.semanticEngineState) {
828 | const semanticState = result.semanticEngineState;
829 | if (semanticState.status === 'ready') {
830 | semanticEngineStatus.value = 'ready';
831 | semanticEngineLastUpdated.value = semanticState.lastUpdated || Date.now();
832 | } else if (semanticState.status === 'error') {
833 | semanticEngineStatus.value = 'error';
834 | semanticEngineLastUpdated.value = semanticState.lastUpdated || Date.now();
835 | } else {
836 | semanticEngineStatus.value = 'idle';
837 | }
838 | } else {
839 | semanticEngineStatus.value = 'idle';
840 | }
841 | } catch (error) {
842 | console.error('❌ 加载模型偏好失败:', error);
843 | }
844 | };
845 |
846 | const saveModelPreference = async (model: ModelPreset) => {
847 | try {
848 | // eslint-disable-next-line no-undef
849 | await chrome.storage.local.set({ selectedModel: model });
850 | } catch (error) {
851 | console.error('保存模型偏好失败:', error);
852 | }
853 | };
854 |
855 | const saveVersionPreference = async (version: 'full' | 'quantized' | 'compressed') => {
856 | try {
857 | // eslint-disable-next-line no-undef
858 | await chrome.storage.local.set({ selectedVersion: version });
859 | } catch (error) {
860 | console.error('保存版本偏好失败:', error);
861 | }
862 | };
863 |
864 | const savePortPreference = async (port: number) => {
865 | try {
866 | // eslint-disable-next-line no-undef
867 | await chrome.storage.local.set({ nativeServerPort: port });
868 | console.log(`端口偏好已保存: ${port}`);
869 | } catch (error) {
870 | console.error('保存端口偏好失败:', error);
871 | }
872 | };
873 |
874 | const loadPortPreference = async () => {
875 | try {
876 | // eslint-disable-next-line no-undef
877 | const result = await chrome.storage.local.get(['nativeServerPort']);
878 | if (result.nativeServerPort) {
879 | nativeServerPort.value = result.nativeServerPort;
880 | console.log(`端口偏好已加载: ${result.nativeServerPort}`);
881 | }
882 | } catch (error) {
883 | console.error('加载端口偏好失败:', error);
884 | }
885 | };
886 |
887 | const saveModelState = async () => {
888 | try {
889 | const modelState = {
890 | status: modelInitializationStatus.value,
891 | downloadProgress: modelDownloadProgress.value,
892 | isDownloading: isModelDownloading.value,
893 | lastUpdated: Date.now(),
894 | };
895 | // eslint-disable-next-line no-undef
896 | await chrome.storage.local.set({ modelState });
897 | } catch (error) {
898 | console.error('保存模型状态失败:', error);
899 | }
900 | };
901 |
902 | let statusMonitoringInterval: ReturnType<typeof setInterval> | null = null;
903 | let semanticEngineStatusPollingInterval: ReturnType<typeof setInterval> | null = null;
904 |
905 | const startModelStatusMonitoring = () => {
906 | if (statusMonitoringInterval) {
907 | clearInterval(statusMonitoringInterval);
908 | }
909 |
910 | statusMonitoringInterval = setInterval(async () => {
911 | try {
912 | // eslint-disable-next-line no-undef
913 | const response = await chrome.runtime.sendMessage({
914 | type: 'get_model_status',
915 | });
916 |
917 | if (response && response.success) {
918 | const status = response.status;
919 | modelInitializationStatus.value = status.initializationStatus || 'idle';
920 | modelDownloadProgress.value = status.downloadProgress || 0;
921 | isModelDownloading.value = status.isDownloading || false;
922 |
923 | if (status.initializationStatus === 'error') {
924 | modelErrorMessage.value = status.errorMessage || getMessage('modelFailedStatus');
925 | modelErrorType.value = status.errorType || 'unknown';
926 | } else {
927 | modelErrorMessage.value = '';
928 | modelErrorType.value = '';
929 | }
930 |
931 | await saveModelState();
932 |
933 | if (status.initializationStatus === 'ready' || status.initializationStatus === 'error') {
934 | stopModelStatusMonitoring();
935 | }
936 | }
937 | } catch (error) {
938 | console.error('获取模型状态失败:', error);
939 | }
940 | }, 1000);
941 | };
942 |
943 | const stopModelStatusMonitoring = () => {
944 | if (statusMonitoringInterval) {
945 | clearInterval(statusMonitoringInterval);
946 | statusMonitoringInterval = null;
947 | }
948 | };
949 |
950 | const startSemanticEngineStatusPolling = () => {
951 | if (semanticEngineStatusPollingInterval) {
952 | clearInterval(semanticEngineStatusPollingInterval);
953 | }
954 |
955 | semanticEngineStatusPollingInterval = setInterval(async () => {
956 | try {
957 | await checkSemanticEngineStatus();
958 | } catch (error) {
959 | console.error('Semantic engine status polling failed:', error);
960 | }
961 | }, 2000);
962 | };
963 |
964 | const stopSemanticEngineStatusPolling = () => {
965 | if (semanticEngineStatusPollingInterval) {
966 | clearInterval(semanticEngineStatusPollingInterval);
967 | semanticEngineStatusPollingInterval = null;
968 | }
969 | };
970 |
971 | const refreshStorageStats = async () => {
972 | if (isRefreshingStats.value) return;
973 |
974 | isRefreshingStats.value = true;
975 | try {
976 | console.log('🔄 Refreshing storage statistics...');
977 |
978 | // eslint-disable-next-line no-undef
979 | const response = await chrome.runtime.sendMessage({
980 | type: 'get_storage_stats',
981 | });
982 |
983 | if (response && response.success) {
984 | storageStats.value = {
985 | indexedPages: response.stats.indexedPages || 0,
986 | totalDocuments: response.stats.totalDocuments || 0,
987 | totalTabs: response.stats.totalTabs || 0,
988 | indexSize: response.stats.indexSize || 0,
989 | isInitialized: response.stats.isInitialized || false,
990 | };
991 | console.log('✅ Storage stats refreshed:', storageStats.value);
992 | } else {
993 | console.error('❌ Failed to get storage stats:', response?.error);
994 | storageStats.value = {
995 | indexedPages: 0,
996 | totalDocuments: 0,
997 | totalTabs: 0,
998 | indexSize: 0,
999 | isInitialized: false,
1000 | };
1001 | }
1002 | } catch (error) {
1003 | console.error('❌ Error refreshing storage stats:', error);
1004 | storageStats.value = {
1005 | indexedPages: 0,
1006 | totalDocuments: 0,
1007 | totalTabs: 0,
1008 | indexSize: 0,
1009 | isInitialized: false,
1010 | };
1011 | } finally {
1012 | isRefreshingStats.value = false;
1013 | }
1014 | };
1015 |
1016 | const hideClearDataConfirmation = () => {
1017 | showClearConfirmation.value = false;
1018 | };
1019 |
1020 | const confirmClearAllData = async () => {
1021 | if (isClearingData.value) return;
1022 |
1023 | isClearingData.value = true;
1024 | clearDataProgress.value = getMessage('clearingStatus');
1025 |
1026 | try {
1027 | console.log('🗑️ Starting to clear all data...');
1028 |
1029 | // eslint-disable-next-line no-undef
1030 | const response = await chrome.runtime.sendMessage({
1031 | type: 'clear_all_data',
1032 | });
1033 |
1034 | if (response && response.success) {
1035 | clearDataProgress.value = getMessage('dataClearedNotification');
1036 | console.log('✅ All data cleared successfully');
1037 |
1038 | await refreshStorageStats();
1039 |
1040 | setTimeout(() => {
1041 | clearDataProgress.value = '';
1042 | hideClearDataConfirmation();
1043 | }, 2000);
1044 | } else {
1045 | throw new Error(response?.error || 'Failed to clear data');
1046 | }
1047 | } catch (error: any) {
1048 | console.error('❌ Failed to clear all data:', error);
1049 | clearDataProgress.value = `Failed to clear data: ${error?.message || 'Unknown error'}`;
1050 |
1051 | setTimeout(() => {
1052 | clearDataProgress.value = '';
1053 | }, 5000);
1054 | } finally {
1055 | isClearingData.value = false;
1056 | }
1057 | };
1058 |
1059 | const switchModel = async (newModel: ModelPreset) => {
1060 | console.log(`🔄 switchModel called with newModel: ${newModel}`);
1061 |
1062 | if (isModelSwitching.value) {
1063 | console.log('⏸️ Model switch already in progress, skipping');
1064 | return;
1065 | }
1066 |
1067 | const isSameModel = newModel === currentModel.value;
1068 | const currentModelInfo = currentModel.value
1069 | ? getModelInfo(currentModel.value)
1070 | : getModelInfo('multilingual-e5-small');
1071 | const newModelInfo = getModelInfo(newModel);
1072 | const isDifferentDimension = currentModelInfo.dimension !== newModelInfo.dimension;
1073 |
1074 | console.log(`📊 Switch analysis:`);
1075 | console.log(` - Same model: ${isSameModel} (${currentModel.value} -> ${newModel})`);
1076 | console.log(
1077 | ` - Current dimension: ${currentModelInfo.dimension}, New dimension: ${newModelInfo.dimension}`,
1078 | );
1079 | console.log(` - Different dimension: ${isDifferentDimension}`);
1080 |
1081 | if (isSameModel && !isDifferentDimension) {
1082 | console.log('✅ Same model and dimension - no need to switch');
1083 | return;
1084 | }
1085 |
1086 | const switchReasons = [];
1087 | if (!isSameModel) switchReasons.push('different model');
1088 | if (isDifferentDimension) switchReasons.push('different dimension');
1089 |
1090 | console.log(`🚀 Switching model due to: ${switchReasons.join(', ')}`);
1091 | console.log(
1092 | `📋 Model: ${currentModel.value} (${currentModelInfo.dimension}D) -> ${newModel} (${newModelInfo.dimension}D)`,
1093 | );
1094 |
1095 | isModelSwitching.value = true;
1096 | modelSwitchProgress.value = getMessage('switchingModelStatus');
1097 |
1098 | modelInitializationStatus.value = 'downloading';
1099 | modelDownloadProgress.value = 0;
1100 | isModelDownloading.value = true;
1101 |
1102 | try {
1103 | await saveModelPreference(newModel);
1104 | await saveVersionPreference('quantized');
1105 | await saveModelState();
1106 |
1107 | modelSwitchProgress.value = getMessage('semanticEngineInitializingStatus');
1108 |
1109 | startModelStatusMonitoring();
1110 |
1111 | // eslint-disable-next-line no-undef
1112 | const response = await chrome.runtime.sendMessage({
1113 | type: 'switch_semantic_model',
1114 | modelPreset: newModel,
1115 | modelVersion: 'quantized',
1116 | modelDimension: newModelInfo.dimension,
1117 | previousDimension: currentModelInfo.dimension,
1118 | });
1119 |
1120 | if (response && response.success) {
1121 | currentModel.value = newModel;
1122 | modelSwitchProgress.value = getMessage('successNotification');
1123 | console.log(
1124 | '模型切换成功:',
1125 | newModel,
1126 | 'version: quantized',
1127 | 'dimension:',
1128 | newModelInfo.dimension,
1129 | );
1130 |
1131 | modelInitializationStatus.value = 'ready';
1132 | isModelDownloading.value = false;
1133 | await saveModelState();
1134 |
1135 | setTimeout(() => {
1136 | modelSwitchProgress.value = '';
1137 | }, 2000);
1138 | } else {
1139 | throw new Error(response?.error || 'Model switch failed');
1140 | }
1141 | } catch (error: any) {
1142 | console.error('模型切换失败:', error);
1143 | modelSwitchProgress.value = `Model switch failed: ${error?.message || 'Unknown error'}`;
1144 |
1145 | modelInitializationStatus.value = 'error';
1146 | isModelDownloading.value = false;
1147 |
1148 | const errorMessage = error?.message || '未知错误';
1149 | if (
1150 | errorMessage.includes('network') ||
1151 | errorMessage.includes('fetch') ||
1152 | errorMessage.includes('timeout')
1153 | ) {
1154 | modelErrorType.value = 'network';
1155 | modelErrorMessage.value = getMessage('networkErrorMessage');
1156 | } else if (
1157 | errorMessage.includes('corrupt') ||
1158 | errorMessage.includes('invalid') ||
1159 | errorMessage.includes('format')
1160 | ) {
1161 | modelErrorType.value = 'file';
1162 | modelErrorMessage.value = getMessage('modelCorruptedErrorMessage');
1163 | } else {
1164 | modelErrorType.value = 'unknown';
1165 | modelErrorMessage.value = errorMessage;
1166 | }
1167 |
1168 | await saveModelState();
1169 |
1170 | setTimeout(() => {
1171 | modelSwitchProgress.value = '';
1172 | }, 8000);
1173 | } finally {
1174 | isModelSwitching.value = false;
1175 | }
1176 | };
1177 |
1178 | const setupServerStatusListener = () => {
1179 | // eslint-disable-next-line no-undef
1180 | chrome.runtime.onMessage.addListener((message) => {
1181 | if (message.type === BACKGROUND_MESSAGE_TYPES.SERVER_STATUS_CHANGED && message.payload) {
1182 | serverStatus.value = message.payload;
1183 | console.log('Server status updated:', message.payload);
1184 | }
1185 | });
1186 | };
1187 |
1188 | onMounted(async () => {
1189 | await loadPortPreference();
1190 | await loadModelPreference();
1191 | await checkNativeConnection();
1192 | await checkServerStatus();
1193 | await refreshStorageStats();
1194 | await loadCacheStats();
1195 |
1196 | await checkSemanticEngineStatus();
1197 | setupServerStatusListener();
1198 | });
1199 |
1200 | onUnmounted(() => {
1201 | stopModelStatusMonitoring();
1202 | stopSemanticEngineStatusPolling();
1203 | });
1204 | </script>
1205 |
1206 | <style scoped>
1207 | .popup-container {
1208 | background: #f1f5f9;
1209 | border-radius: 24px;
1210 | box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
1211 | display: flex;
1212 | flex-direction: column;
1213 | overflow: hidden;
1214 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
1215 | }
1216 |
1217 | .header {
1218 | flex-shrink: 0;
1219 | padding-left: 20px;
1220 | }
1221 |
1222 | .header-content {
1223 | display: flex;
1224 | justify-content: space-between;
1225 | align-items: center;
1226 | }
1227 |
1228 | .header-title {
1229 | font-size: 24px;
1230 | font-weight: 700;
1231 | color: #1e293b;
1232 | margin: 0;
1233 | }
1234 |
1235 | .settings-button {
1236 | padding: 8px;
1237 | border-radius: 50%;
1238 | color: #64748b;
1239 | background: none;
1240 | border: none;
1241 | cursor: pointer;
1242 | transition: all 0.2s ease;
1243 | }
1244 |
1245 | .settings-button:hover {
1246 | background: #e2e8f0;
1247 | color: #1e293b;
1248 | }
1249 |
1250 | .content {
1251 | flex-grow: 1;
1252 | padding: 8px 24px;
1253 | overflow-y: auto;
1254 | scrollbar-width: none;
1255 | -ms-overflow-style: none;
1256 | }
1257 |
1258 | .content::-webkit-scrollbar {
1259 | display: none;
1260 | }
1261 | .status-card {
1262 | background: white;
1263 | border-radius: 16px;
1264 | box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
1265 | padding: 20px;
1266 | margin-bottom: 20px;
1267 | }
1268 |
1269 | .status-label {
1270 | font-size: 14px;
1271 | font-weight: 500;
1272 | color: #64748b;
1273 | margin-bottom: 8px;
1274 | }
1275 |
1276 | .status-info {
1277 | display: flex;
1278 | align-items: center;
1279 | gap: 8px;
1280 | }
1281 |
1282 | .status-dot {
1283 | height: 8px;
1284 | width: 8px;
1285 | border-radius: 50%;
1286 | }
1287 |
1288 | .status-dot.bg-emerald-500 {
1289 | background-color: #10b981;
1290 | }
1291 |
1292 | .status-dot.bg-red-500 {
1293 | background-color: #ef4444;
1294 | }
1295 |
1296 | .status-dot.bg-yellow-500 {
1297 | background-color: #eab308;
1298 | }
1299 |
1300 | .status-dot.bg-gray-500 {
1301 | background-color: #6b7280;
1302 | }
1303 |
1304 | .status-text {
1305 | font-size: 16px;
1306 | font-weight: 600;
1307 | color: #1e293b;
1308 | }
1309 |
1310 | .model-label {
1311 | font-size: 14px;
1312 | font-weight: 500;
1313 | color: #64748b;
1314 | margin-bottom: 4px;
1315 | }
1316 |
1317 | .model-name {
1318 | font-weight: 600;
1319 | color: #7c3aed;
1320 | }
1321 |
1322 | .stats-grid {
1323 | display: grid;
1324 | grid-template-columns: 1fr 1fr;
1325 | gap: 12px;
1326 | }
1327 | .stats-card {
1328 | background: white;
1329 | border-radius: 12px;
1330 | box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
1331 | padding: 16px;
1332 | }
1333 |
1334 | .stats-header {
1335 | display: flex;
1336 | align-items: center;
1337 | justify-content: space-between;
1338 | margin-bottom: 8px;
1339 | }
1340 |
1341 | .stats-label {
1342 | font-size: 14px;
1343 | font-weight: 500;
1344 | color: #64748b;
1345 | }
1346 |
1347 | .stats-icon {
1348 | padding: 8px;
1349 | border-radius: 8px;
1350 | }
1351 |
1352 | .stats-icon.violet {
1353 | background: #ede9fe;
1354 | color: #7c3aed;
1355 | }
1356 |
1357 | .stats-icon.teal {
1358 | background: #ccfbf1;
1359 | color: #0d9488;
1360 | }
1361 |
1362 | .stats-icon.blue {
1363 | background: #dbeafe;
1364 | color: #2563eb;
1365 | }
1366 |
1367 | .stats-icon.green {
1368 | background: #dcfce7;
1369 | color: #16a34a;
1370 | }
1371 |
1372 | .stats-value {
1373 | font-size: 30px;
1374 | font-weight: 700;
1375 | color: #0f172a;
1376 | margin: 0;
1377 | }
1378 |
1379 | .section {
1380 | margin-bottom: 24px;
1381 | }
1382 |
1383 | .secondary-button {
1384 | background: #f1f5f9;
1385 | color: #475569;
1386 | border: 1px solid #cbd5e1;
1387 | padding: 8px 16px;
1388 | border-radius: 8px;
1389 | font-size: 14px;
1390 | font-weight: 500;
1391 | cursor: pointer;
1392 | transition: all 0.2s ease;
1393 | display: flex;
1394 | align-items: center;
1395 | gap: 8px;
1396 | }
1397 |
1398 | .secondary-button:hover:not(:disabled) {
1399 | background: #e2e8f0;
1400 | border-color: #94a3b8;
1401 | }
1402 |
1403 | .secondary-button:disabled {
1404 | opacity: 0.5;
1405 | cursor: not-allowed;
1406 | }
1407 |
1408 | .primary-button {
1409 | background: #3b82f6;
1410 | color: white;
1411 | border: none;
1412 | padding: 8px 16px;
1413 | border-radius: 8px;
1414 | font-size: 14px;
1415 | font-weight: 500;
1416 | cursor: pointer;
1417 | transition: all 0.2s ease;
1418 | }
1419 |
1420 | .primary-button:hover {
1421 | background: #2563eb;
1422 | }
1423 |
1424 | .section-title {
1425 | font-size: 16px;
1426 | font-weight: 600;
1427 | color: #374151;
1428 | margin-bottom: 12px;
1429 | }
1430 | .current-model-card {
1431 | background: linear-gradient(135deg, #faf5ff, #f3e8ff);
1432 | border: 1px solid #e9d5ff;
1433 | border-radius: 12px;
1434 | padding: 16px;
1435 | margin-bottom: 16px;
1436 | }
1437 |
1438 | .current-model-header {
1439 | display: flex;
1440 | justify-content: space-between;
1441 | align-items: center;
1442 | margin-bottom: 8px;
1443 | }
1444 |
1445 | .current-model-label {
1446 | font-size: 14px;
1447 | font-weight: 500;
1448 | color: #64748b;
1449 | margin: 0;
1450 | }
1451 |
1452 | .current-model-badge {
1453 | background: #8b5cf6;
1454 | color: white;
1455 | font-size: 12px;
1456 | font-weight: 600;
1457 | padding: 4px 8px;
1458 | border-radius: 6px;
1459 | }
1460 |
1461 | .current-model-name {
1462 | font-size: 16px;
1463 | font-weight: 700;
1464 | color: #7c3aed;
1465 | margin: 0;
1466 | }
1467 |
1468 | .model-list {
1469 | display: flex;
1470 | flex-direction: column;
1471 | gap: 12px;
1472 | }
1473 |
1474 | .model-card {
1475 | background: white;
1476 | border-radius: 12px;
1477 | padding: 16px;
1478 | cursor: pointer;
1479 | border: 1px solid #e5e7eb;
1480 | transition: all 0.2s ease;
1481 | }
1482 |
1483 | .model-card:hover {
1484 | border-color: #8b5cf6;
1485 | }
1486 |
1487 | .model-card.selected {
1488 | border: 2px solid #8b5cf6;
1489 | background: #faf5ff;
1490 | }
1491 |
1492 | .model-card.disabled {
1493 | opacity: 0.5;
1494 | cursor: not-allowed;
1495 | pointer-events: none;
1496 | }
1497 |
1498 | .model-header {
1499 | display: flex;
1500 | justify-content: space-between;
1501 | align-items: flex-start;
1502 | }
1503 |
1504 | .model-info {
1505 | flex: 1;
1506 | }
1507 |
1508 | .model-name {
1509 | font-weight: 600;
1510 | color: #1e293b;
1511 | margin: 0 0 4px 0;
1512 | }
1513 |
1514 | .model-name.selected-text {
1515 | color: #7c3aed;
1516 | }
1517 |
1518 | .model-description {
1519 | font-size: 14px;
1520 | color: #64748b;
1521 | margin: 0;
1522 | }
1523 |
1524 | .check-icon {
1525 | width: 20px;
1526 | height: 20px;
1527 | flex-shrink: 0;
1528 | background: #8b5cf6;
1529 | border-radius: 50%;
1530 | display: flex;
1531 | align-items: center;
1532 | justify-content: center;
1533 | }
1534 |
1535 | .model-tags {
1536 | display: flex;
1537 | align-items: center;
1538 | gap: 8px;
1539 | margin-top: 16px;
1540 | }
1541 | .model-tag {
1542 | display: inline-flex;
1543 | align-items: center;
1544 | border-radius: 9999px;
1545 | padding: 4px 10px;
1546 | font-size: 12px;
1547 | font-weight: 500;
1548 | }
1549 |
1550 | .model-tag.performance {
1551 | background: #d1fae5;
1552 | color: #065f46;
1553 | }
1554 |
1555 | .model-tag.size {
1556 | background: #ddd6fe;
1557 | color: #5b21b6;
1558 | }
1559 |
1560 | .model-tag.dimension {
1561 | background: #e5e7eb;
1562 | color: #4b5563;
1563 | }
1564 |
1565 | .config-card {
1566 | background: white;
1567 | border-radius: 16px;
1568 | box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
1569 | padding: 20px;
1570 | display: flex;
1571 | flex-direction: column;
1572 | gap: 16px;
1573 | }
1574 | .semantic-engine-card {
1575 | background: white;
1576 | border-radius: 16px;
1577 | box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
1578 | padding: 20px;
1579 | display: flex;
1580 | flex-direction: column;
1581 | gap: 16px;
1582 | }
1583 |
1584 | .semantic-engine-status {
1585 | display: flex;
1586 | flex-direction: column;
1587 | gap: 8px;
1588 | }
1589 |
1590 | .semantic-engine-button {
1591 | width: 100%;
1592 | display: flex;
1593 | align-items: center;
1594 | justify-content: center;
1595 | gap: 8px;
1596 | background: #8b5cf6;
1597 | color: white;
1598 | font-weight: 600;
1599 | padding: 12px 16px;
1600 | border-radius: 8px;
1601 | border: none;
1602 | cursor: pointer;
1603 | transition: all 0.2s ease;
1604 | box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
1605 | }
1606 |
1607 | .semantic-engine-button:hover:not(:disabled) {
1608 | background: #7c3aed;
1609 | box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
1610 | }
1611 |
1612 | .semantic-engine-button:disabled {
1613 | opacity: 0.6;
1614 | cursor: not-allowed;
1615 | }
1616 |
1617 | .status-header {
1618 | display: flex;
1619 | justify-content: space-between;
1620 | align-items: center;
1621 | margin-bottom: 8px;
1622 | }
1623 |
1624 | .refresh-status-button {
1625 | background: none;
1626 | border: none;
1627 | cursor: pointer;
1628 | padding: 4px 8px;
1629 | border-radius: 6px;
1630 | font-size: 14px;
1631 | color: #64748b;
1632 | transition: all 0.2s ease;
1633 | }
1634 |
1635 | .refresh-status-button:hover {
1636 | background: #f1f5f9;
1637 | color: #374151;
1638 | }
1639 |
1640 | .status-timestamp {
1641 | font-size: 12px;
1642 | color: #9ca3af;
1643 | margin-top: 4px;
1644 | }
1645 |
1646 | .mcp-config-section {
1647 | border-top: 1px solid #f1f5f9;
1648 | }
1649 |
1650 | .mcp-config-header {
1651 | display: flex;
1652 | justify-content: space-between;
1653 | align-items: center;
1654 | margin-bottom: 8px;
1655 | }
1656 |
1657 | .mcp-config-label {
1658 | font-size: 14px;
1659 | font-weight: 500;
1660 | color: #64748b;
1661 | margin: 0;
1662 | }
1663 |
1664 | .copy-config-button {
1665 | background: none;
1666 | border: none;
1667 | cursor: pointer;
1668 | padding: 4px 8px;
1669 | border-radius: 6px;
1670 | font-size: 14px;
1671 | color: #64748b;
1672 | transition: all 0.2s ease;
1673 | display: flex;
1674 | align-items: center;
1675 | gap: 4px;
1676 | }
1677 |
1678 | .copy-config-button:hover {
1679 | background: #f1f5f9;
1680 | color: #374151;
1681 | }
1682 |
1683 | .mcp-config-content {
1684 | background: #f8fafc;
1685 | border: 1px solid #e2e8f0;
1686 | border-radius: 8px;
1687 | padding: 12px;
1688 | overflow-x: auto;
1689 | }
1690 |
1691 | .mcp-config-json {
1692 | font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
1693 | font-size: 12px;
1694 | line-height: 1.4;
1695 | color: #374151;
1696 | margin: 0;
1697 | white-space: pre;
1698 | overflow-x: auto;
1699 | }
1700 |
1701 | .port-section {
1702 | display: flex;
1703 | flex-direction: column;
1704 | gap: 8px;
1705 | }
1706 |
1707 | .port-label {
1708 | font-size: 14px;
1709 | font-weight: 500;
1710 | color: #64748b;
1711 | }
1712 |
1713 | .port-input {
1714 | display: block;
1715 | width: 100%;
1716 | border-radius: 8px;
1717 | border: 1px solid #d1d5db;
1718 | box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
1719 | padding: 12px;
1720 | font-size: 14px;
1721 | background: #f8fafc;
1722 | }
1723 |
1724 | .port-input:focus {
1725 | outline: none;
1726 | border-color: #8b5cf6;
1727 | box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.1);
1728 | }
1729 |
1730 | .connect-button {
1731 | width: 100%;
1732 | display: flex;
1733 | align-items: center;
1734 | justify-content: center;
1735 | gap: 8px;
1736 | background: #8b5cf6;
1737 | color: white;
1738 | font-weight: 600;
1739 | padding: 12px 16px;
1740 | border-radius: 8px;
1741 | border: none;
1742 | cursor: pointer;
1743 | transition: all 0.2s ease;
1744 | box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
1745 | }
1746 |
1747 | .connect-button:hover:not(:disabled) {
1748 | background: #7c3aed;
1749 | box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
1750 | }
1751 |
1752 | .connect-button:disabled {
1753 | opacity: 0.6;
1754 | cursor: not-allowed;
1755 | }
1756 | .error-card {
1757 | background: #fef2f2;
1758 | border: 1px solid #fecaca;
1759 | border-radius: 12px;
1760 | padding: 16px;
1761 | margin-bottom: 16px;
1762 | display: flex;
1763 | align-items: flex-start;
1764 | gap: 16px;
1765 | }
1766 |
1767 | .error-content {
1768 | flex: 1;
1769 | display: flex;
1770 | align-items: flex-start;
1771 | gap: 12px;
1772 | }
1773 |
1774 | .error-icon {
1775 | font-size: 20px;
1776 | flex-shrink: 0;
1777 | margin-top: 2px;
1778 | }
1779 |
1780 | .error-details {
1781 | flex: 1;
1782 | }
1783 |
1784 | .error-title {
1785 | font-size: 14px;
1786 | font-weight: 600;
1787 | color: #dc2626;
1788 | margin: 0 0 4px 0;
1789 | }
1790 |
1791 | .error-message {
1792 | font-size: 14px;
1793 | color: #991b1b;
1794 | margin: 0 0 8px 0;
1795 | font-weight: 500;
1796 | }
1797 |
1798 | .error-suggestion {
1799 | font-size: 13px;
1800 | color: #7f1d1d;
1801 | margin: 0;
1802 | line-height: 1.4;
1803 | }
1804 |
1805 | .retry-button {
1806 | display: flex;
1807 | align-items: center;
1808 | gap: 6px;
1809 | background: #dc2626;
1810 | color: white;
1811 | font-weight: 600;
1812 | padding: 8px 16px;
1813 | border-radius: 8px;
1814 | border: none;
1815 | cursor: pointer;
1816 | transition: all 0.2s ease;
1817 | font-size: 14px;
1818 | flex-shrink: 0;
1819 | }
1820 |
1821 | .retry-button:hover:not(:disabled) {
1822 | background: #b91c1c;
1823 | }
1824 |
1825 | .retry-button:disabled {
1826 | opacity: 0.6;
1827 | cursor: not-allowed;
1828 | }
1829 | .danger-button {
1830 | width: 100%;
1831 | display: flex;
1832 | align-items: center;
1833 | justify-content: center;
1834 | gap: 8px;
1835 | background: white;
1836 | border: 1px solid #d1d5db;
1837 | color: #374151;
1838 | font-weight: 600;
1839 | padding: 12px 16px;
1840 | border-radius: 8px;
1841 | cursor: pointer;
1842 | transition: all 0.2s ease;
1843 | margin-top: 16px;
1844 | }
1845 |
1846 | .danger-button:hover:not(:disabled) {
1847 | border-color: #ef4444;
1848 | color: #dc2626;
1849 | }
1850 |
1851 | .danger-button:disabled {
1852 | opacity: 0.6;
1853 | cursor: not-allowed;
1854 | }
1855 |
1856 | .icon-small {
1857 | width: 14px;
1858 | height: 14px;
1859 | }
1860 |
1861 | .icon-default {
1862 | width: 20px;
1863 | height: 20px;
1864 | }
1865 |
1866 | .icon-medium {
1867 | width: 24px;
1868 | height: 24px;
1869 | }
1870 | .footer {
1871 | padding: 16px;
1872 | margin-top: auto;
1873 | }
1874 |
1875 | .footer-text {
1876 | text-align: center;
1877 | font-size: 12px;
1878 | color: #94a3b8;
1879 | margin: 0;
1880 | }
1881 |
1882 | @media (max-width: 320px) {
1883 | .popup-container {
1884 | width: 100%;
1885 | height: 100vh;
1886 | border-radius: 0;
1887 | }
1888 |
1889 | .header {
1890 | padding: 24px 20px 12px;
1891 | }
1892 |
1893 | .content {
1894 | padding: 8px 20px;
1895 | }
1896 |
1897 | .stats-grid {
1898 | grid-template-columns: 1fr;
1899 | gap: 8px;
1900 | }
1901 |
1902 | .config-card {
1903 | padding: 16px;
1904 | gap: 12px;
1905 | }
1906 |
1907 | .current-model-card {
1908 | padding: 12px;
1909 | margin-bottom: 12px;
1910 | }
1911 |
1912 | .stats-card {
1913 | padding: 12px;
1914 | }
1915 |
1916 | .stats-value {
1917 | font-size: 24px;
1918 | }
1919 | }
1920 | </style>
1921 |
```
--------------------------------------------------------------------------------
/app/chrome-extension/utils/vector-database.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Vector database manager
3 | * Uses hnswlib-wasm for high-performance vector similarity search
4 | * Implements singleton pattern to avoid duplicate WASM module initialization
5 | */
6 |
7 | import { loadHnswlib } from 'hnswlib-wasm-static';
8 | import type { TextChunk } from './text-chunker';
9 |
10 | export interface VectorDocument {
11 | id: string;
12 | tabId: number;
13 | url: string;
14 | title: string;
15 | chunk: TextChunk;
16 | embedding: Float32Array;
17 | timestamp: number;
18 | }
19 |
20 | export interface SearchResult {
21 | document: VectorDocument;
22 | similarity: number;
23 | distance: number;
24 | }
25 |
26 | export interface VectorDatabaseConfig {
27 | dimension: number;
28 | maxElements: number;
29 | efConstruction: number;
30 | M: number;
31 | efSearch: number;
32 | indexFileName: string;
33 | enableAutoCleanup?: boolean;
34 | maxRetentionDays?: number;
35 | }
36 |
37 | let globalHnswlib: any = null;
38 | let globalHnswlibInitPromise: Promise<any> | null = null;
39 | let globalHnswlibInitialized = false;
40 |
41 | let syncInProgress = false;
42 | let pendingSyncPromise: Promise<void> | null = null;
43 |
44 | const DB_NAME = 'VectorDatabaseStorage';
45 | const DB_VERSION = 1;
46 | const STORE_NAME = 'documentMappings';
47 |
48 | /**
49 | * IndexedDB helper functions
50 | */
51 | class IndexedDBHelper {
52 | private static dbPromise: Promise<IDBDatabase> | null = null;
53 |
54 | static async getDB(): Promise<IDBDatabase> {
55 | if (!this.dbPromise) {
56 | this.dbPromise = new Promise((resolve, reject) => {
57 | const request = indexedDB.open(DB_NAME, DB_VERSION);
58 |
59 | request.onerror = () => reject(request.error);
60 | request.onsuccess = () => resolve(request.result);
61 |
62 | request.onupgradeneeded = (event) => {
63 | const db = (event.target as IDBOpenDBRequest).result;
64 |
65 | if (!db.objectStoreNames.contains(STORE_NAME)) {
66 | const store = db.createObjectStore(STORE_NAME, { keyPath: 'id' });
67 | store.createIndex('indexFileName', 'indexFileName', { unique: false });
68 | }
69 | };
70 | });
71 | }
72 | return this.dbPromise;
73 | }
74 |
75 | static async saveData(indexFileName: string, data: any): Promise<void> {
76 | const db = await this.getDB();
77 | const transaction = db.transaction([STORE_NAME], 'readwrite');
78 | const store = transaction.objectStore(STORE_NAME);
79 |
80 | await new Promise<void>((resolve, reject) => {
81 | const request = store.put({
82 | id: indexFileName,
83 | indexFileName,
84 | data,
85 | timestamp: Date.now(),
86 | });
87 |
88 | request.onsuccess = () => resolve();
89 | request.onerror = () => reject(request.error);
90 | });
91 | }
92 |
93 | static async loadData(indexFileName: string): Promise<any | null> {
94 | const db = await this.getDB();
95 | const transaction = db.transaction([STORE_NAME], 'readonly');
96 | const store = transaction.objectStore(STORE_NAME);
97 |
98 | return new Promise<any | null>((resolve, reject) => {
99 | const request = store.get(indexFileName);
100 |
101 | request.onsuccess = () => {
102 | const result = request.result;
103 | resolve(result ? result.data : null);
104 | };
105 | request.onerror = () => reject(request.error);
106 | });
107 | }
108 |
109 | static async deleteData(indexFileName: string): Promise<void> {
110 | const db = await this.getDB();
111 | const transaction = db.transaction([STORE_NAME], 'readwrite');
112 | const store = transaction.objectStore(STORE_NAME);
113 |
114 | await new Promise<void>((resolve, reject) => {
115 | const request = store.delete(indexFileName);
116 | request.onsuccess = () => resolve();
117 | request.onerror = () => reject(request.error);
118 | });
119 | }
120 |
121 | /**
122 | * Clear all IndexedDB data (for complete cleanup during model switching)
123 | */
124 | static async clearAllData(): Promise<void> {
125 | try {
126 | const db = await this.getDB();
127 | const transaction = db.transaction([STORE_NAME], 'readwrite');
128 | const store = transaction.objectStore(STORE_NAME);
129 |
130 | await new Promise<void>((resolve, reject) => {
131 | const request = store.clear();
132 | request.onsuccess = () => {
133 | console.log('IndexedDBHelper: All data cleared from IndexedDB');
134 | resolve();
135 | };
136 | request.onerror = () => reject(request.error);
137 | });
138 | } catch (error) {
139 | console.error('IndexedDBHelper: Failed to clear all data:', error);
140 | throw error;
141 | }
142 | }
143 |
144 | /**
145 | * Get all stored keys (for debugging)
146 | */
147 | static async getAllKeys(): Promise<string[]> {
148 | try {
149 | const db = await this.getDB();
150 | const transaction = db.transaction([STORE_NAME], 'readonly');
151 | const store = transaction.objectStore(STORE_NAME);
152 |
153 | return new Promise<string[]>((resolve, reject) => {
154 | const request = store.getAllKeys();
155 | request.onsuccess = () => resolve(request.result as string[]);
156 | request.onerror = () => reject(request.error);
157 | });
158 | } catch (error) {
159 | console.error('IndexedDBHelper: Failed to get all keys:', error);
160 | return [];
161 | }
162 | }
163 | }
164 |
165 | /**
166 | * Global hnswlib-wasm initialization function
167 | * Ensures initialization only once across the entire application
168 | */
169 | async function initializeGlobalHnswlib(): Promise<any> {
170 | if (globalHnswlibInitialized && globalHnswlib) {
171 | return globalHnswlib;
172 | }
173 |
174 | if (globalHnswlibInitPromise) {
175 | return globalHnswlibInitPromise;
176 | }
177 |
178 | globalHnswlibInitPromise = (async () => {
179 | try {
180 | console.log('VectorDatabase: Initializing global hnswlib-wasm instance...');
181 | globalHnswlib = await loadHnswlib();
182 | globalHnswlibInitialized = true;
183 | console.log('VectorDatabase: Global hnswlib-wasm instance initialized successfully');
184 | return globalHnswlib;
185 | } catch (error) {
186 | console.error('VectorDatabase: Failed to initialize global hnswlib-wasm:', error);
187 | globalHnswlibInitPromise = null;
188 | throw error;
189 | }
190 | })();
191 |
192 | return globalHnswlibInitPromise;
193 | }
194 |
195 | export class VectorDatabase {
196 | private index: any = null;
197 | private isInitialized = false;
198 | private isInitializing = false;
199 | private initPromise: Promise<void> | null = null;
200 |
201 | private documents = new Map<number, VectorDocument>();
202 | private tabDocuments = new Map<number, Set<number>>();
203 | private nextLabel = 0;
204 |
205 | private readonly config: VectorDatabaseConfig;
206 |
207 | constructor(config?: Partial<VectorDatabaseConfig>) {
208 | this.config = {
209 | dimension: 384,
210 | maxElements: 100000,
211 | efConstruction: 200,
212 | M: 48,
213 | efSearch: 50,
214 | indexFileName: 'tab_content_index.dat',
215 | enableAutoCleanup: true,
216 | maxRetentionDays: 30,
217 | ...config,
218 | };
219 |
220 | console.log('VectorDatabase: Initialized with config:', {
221 | dimension: this.config.dimension,
222 | efSearch: this.config.efSearch,
223 | M: this.config.M,
224 | efConstruction: this.config.efConstruction,
225 | enableAutoCleanup: this.config.enableAutoCleanup,
226 | maxRetentionDays: this.config.maxRetentionDays,
227 | });
228 | }
229 |
230 | /**
231 | * Initialize vector database
232 | */
233 | public async initialize(): Promise<void> {
234 | if (this.isInitialized) return;
235 | if (this.isInitializing && this.initPromise) return this.initPromise;
236 |
237 | this.isInitializing = true;
238 | this.initPromise = this._doInitialize().finally(() => {
239 | this.isInitializing = false;
240 | });
241 |
242 | return this.initPromise;
243 | }
244 |
245 | private async _doInitialize(): Promise<void> {
246 | try {
247 | console.log('VectorDatabase: Initializing...');
248 |
249 | const hnswlib = await initializeGlobalHnswlib();
250 |
251 | hnswlib.EmscriptenFileSystemManager.setDebugLogs(true);
252 |
253 | this.index = new hnswlib.HierarchicalNSW(
254 | 'cosine',
255 | this.config.dimension,
256 | this.config.indexFileName,
257 | );
258 |
259 | await this.syncFileSystem('read');
260 |
261 | const indexExists = hnswlib.EmscriptenFileSystemManager.checkFileExists(
262 | this.config.indexFileName,
263 | );
264 |
265 | if (indexExists) {
266 | console.log('VectorDatabase: Loading existing index...');
267 | try {
268 | await this.index.readIndex(this.config.indexFileName, this.config.maxElements);
269 | this.index.setEfSearch(this.config.efSearch);
270 |
271 | await this.loadDocumentMappings();
272 |
273 | if (this.documents.size > 0) {
274 | const maxLabel = Math.max(...Array.from(this.documents.keys()));
275 | this.nextLabel = maxLabel + 1;
276 | console.log(
277 | `VectorDatabase: Loaded existing index with ${this.documents.size} documents, next label: ${this.nextLabel}`,
278 | );
279 | } else {
280 | const indexCount = this.index.getCurrentCount();
281 | if (indexCount > 0) {
282 | console.warn(
283 | `VectorDatabase: Index has ${indexCount} vectors but no document mappings found. This may cause label mismatch.`,
284 | );
285 | this.nextLabel = indexCount;
286 | } else {
287 | this.nextLabel = 0;
288 | }
289 | console.log(
290 | `VectorDatabase: No document mappings found, starting with next label: ${this.nextLabel}`,
291 | );
292 | }
293 | } catch (loadError) {
294 | console.warn(
295 | 'VectorDatabase: Failed to load existing index, creating new one:',
296 | loadError,
297 | );
298 |
299 | this.index.initIndex(
300 | this.config.maxElements,
301 | this.config.M,
302 | this.config.efConstruction,
303 | 200,
304 | );
305 | this.index.setEfSearch(this.config.efSearch);
306 | this.nextLabel = 0;
307 | }
308 | } else {
309 | console.log('VectorDatabase: Creating new index...');
310 | this.index.initIndex(
311 | this.config.maxElements,
312 | this.config.M,
313 | this.config.efConstruction,
314 | 200,
315 | );
316 | this.index.setEfSearch(this.config.efSearch);
317 | this.nextLabel = 0;
318 | }
319 |
320 | this.isInitialized = true;
321 | console.log('VectorDatabase: Initialization completed successfully');
322 | } catch (error) {
323 | console.error('VectorDatabase: Initialization failed:', error);
324 | this.isInitialized = false;
325 | throw error;
326 | }
327 | }
328 |
329 | /**
330 | * Add document to vector database
331 | */
332 | public async addDocument(
333 | tabId: number,
334 | url: string,
335 | title: string,
336 | chunk: TextChunk,
337 | embedding: Float32Array,
338 | ): Promise<number> {
339 | if (!this.isInitialized) {
340 | await this.initialize();
341 | }
342 |
343 | const documentId = this.generateDocumentId(tabId, chunk.index);
344 | const document: VectorDocument = {
345 | id: documentId,
346 | tabId,
347 | url,
348 | title,
349 | chunk,
350 | embedding,
351 | timestamp: Date.now(),
352 | };
353 |
354 | try {
355 | // Validate vector data
356 | if (!embedding || embedding.length !== this.config.dimension) {
357 | const errorMsg = `Invalid embedding dimension: expected ${this.config.dimension}, got ${embedding?.length || 0}`;
358 | console.error('VectorDatabase: Dimension mismatch detected!', {
359 | expectedDimension: this.config.dimension,
360 | actualDimension: embedding?.length || 0,
361 | documentId,
362 | tabId,
363 | url,
364 | title: title.substring(0, 50) + '...',
365 | });
366 |
367 | // This might be caused by model switching, suggest reinitialization
368 | console.warn(
369 | 'VectorDatabase: This might be caused by model switching. Consider reinitializing the vector database with the correct dimension.',
370 | );
371 |
372 | throw new Error(errorMsg);
373 | }
374 |
375 | // Check if vector data contains invalid values
376 | for (let i = 0; i < embedding.length; i++) {
377 | if (!isFinite(embedding[i])) {
378 | throw new Error(`Invalid embedding value at index ${i}: ${embedding[i]}`);
379 | }
380 | }
381 |
382 | // Ensure we have a clean Float32Array
383 | let cleanEmbedding: Float32Array;
384 | if (embedding instanceof Float32Array) {
385 | cleanEmbedding = embedding;
386 | } else {
387 | cleanEmbedding = new Float32Array(embedding);
388 | }
389 |
390 | // Use current nextLabel as label
391 | const label = this.nextLabel++;
392 |
393 | console.log(
394 | `VectorDatabase: Adding document with label ${label}, embedding dimension: ${embedding.length}`,
395 | );
396 |
397 | // Add vector to index
398 | // According to hnswlib-wasm-static emscripten binding requirements, need to create VectorFloat type
399 | console.log(`VectorDatabase: 🔧 DEBUGGING - About to call addPoint with:`, {
400 | embeddingType: typeof cleanEmbedding,
401 | isFloat32Array: cleanEmbedding instanceof Float32Array,
402 | length: cleanEmbedding.length,
403 | firstFewValues: Array.from(cleanEmbedding.slice(0, 3)),
404 | label: label,
405 | replaceDeleted: false,
406 | });
407 |
408 | // Method 1: Try using VectorFloat constructor (if available)
409 | let vectorToAdd;
410 | try {
411 | // Check if VectorFloat constructor exists
412 | if (globalHnswlib && globalHnswlib.VectorFloat) {
413 | console.log('VectorDatabase: Using VectorFloat constructor');
414 | vectorToAdd = new globalHnswlib.VectorFloat();
415 | // Add elements to VectorFloat one by one
416 | for (let i = 0; i < cleanEmbedding.length; i++) {
417 | vectorToAdd.push_back(cleanEmbedding[i]);
418 | }
419 | } else {
420 | // Method 2: Use plain JS array (fallback)
421 | console.log('VectorDatabase: Using plain JS array as fallback');
422 | vectorToAdd = Array.from(cleanEmbedding);
423 | }
424 |
425 | // Call addPoint with constructed vector
426 | this.index.addPoint(vectorToAdd, label, false);
427 |
428 | // Clean up VectorFloat object (if manually created)
429 | if (vectorToAdd && typeof vectorToAdd.delete === 'function') {
430 | vectorToAdd.delete();
431 | }
432 | } catch (vectorError) {
433 | console.error(
434 | 'VectorDatabase: VectorFloat approach failed, trying alternatives:',
435 | vectorError,
436 | );
437 |
438 | // Method 3: Try passing Float32Array directly
439 | try {
440 | console.log('VectorDatabase: Trying Float32Array directly');
441 | this.index.addPoint(cleanEmbedding, label, false);
442 | } catch (float32Error) {
443 | console.error('VectorDatabase: Float32Array approach failed:', float32Error);
444 |
445 | // Method 4: Last resort - use spread operator
446 | console.log('VectorDatabase: Trying spread operator as last resort');
447 | this.index.addPoint([...cleanEmbedding], label, false);
448 | }
449 | }
450 | console.log(`VectorDatabase: ✅ Successfully added document with label ${label}`);
451 |
452 | // Store document mapping
453 | this.documents.set(label, document);
454 |
455 | // Update tab document mapping
456 | if (!this.tabDocuments.has(tabId)) {
457 | this.tabDocuments.set(tabId, new Set());
458 | }
459 | this.tabDocuments.get(tabId)!.add(label);
460 |
461 | // Save index and mappings
462 | await this.saveIndex();
463 | await this.saveDocumentMappings();
464 |
465 | // Check if auto cleanup is needed
466 | if (this.config.enableAutoCleanup) {
467 | await this.checkAndPerformAutoCleanup();
468 | }
469 |
470 | console.log(`VectorDatabase: Successfully added document ${documentId} with label ${label}`);
471 | return label;
472 | } catch (error) {
473 | console.error('VectorDatabase: Failed to add document:', error);
474 | console.error('VectorDatabase: Embedding info:', {
475 | type: typeof embedding,
476 | constructor: embedding?.constructor?.name,
477 | length: embedding?.length,
478 | isFloat32Array: embedding instanceof Float32Array,
479 | firstFewValues: embedding ? Array.from(embedding.slice(0, 5)) : null,
480 | });
481 | throw error;
482 | }
483 | }
484 |
485 | /**
486 | * Search similar documents
487 | */
488 | public async search(queryEmbedding: Float32Array, topK: number = 10): Promise<SearchResult[]> {
489 | if (!this.isInitialized) {
490 | await this.initialize();
491 | }
492 |
493 | try {
494 | // Validate query vector
495 | if (!queryEmbedding || queryEmbedding.length !== this.config.dimension) {
496 | throw new Error(
497 | `Invalid query embedding dimension: expected ${this.config.dimension}, got ${queryEmbedding?.length || 0}`,
498 | );
499 | }
500 |
501 | // Check if query vector contains invalid values
502 | for (let i = 0; i < queryEmbedding.length; i++) {
503 | if (!isFinite(queryEmbedding[i])) {
504 | throw new Error(`Invalid query embedding value at index ${i}: ${queryEmbedding[i]}`);
505 | }
506 | }
507 |
508 | console.log(
509 | `VectorDatabase: Searching with query embedding dimension: ${queryEmbedding.length}, topK: ${topK}`,
510 | );
511 |
512 | // Check if index is empty
513 | const currentCount = this.index.getCurrentCount();
514 | if (currentCount === 0) {
515 | console.log('VectorDatabase: Index is empty, returning no results');
516 | return [];
517 | }
518 |
519 | console.log(`VectorDatabase: Index contains ${currentCount} vectors`);
520 |
521 | // Check if document mapping and index are synchronized
522 | const mappingCount = this.documents.size;
523 | if (mappingCount === 0 && currentCount > 0) {
524 | console.warn(
525 | `VectorDatabase: Index has ${currentCount} vectors but document mapping is empty. Attempting to reload mappings...`,
526 | );
527 | await this.loadDocumentMappings();
528 |
529 | if (this.documents.size === 0) {
530 | console.error(
531 | 'VectorDatabase: Failed to load document mappings. Index and mappings are out of sync.',
532 | );
533 | return [];
534 | }
535 | console.log(
536 | `VectorDatabase: Successfully reloaded ${this.documents.size} document mappings`,
537 | );
538 | }
539 |
540 | // Process query vector according to hnswlib-wasm-static emscripten binding requirements
541 | let queryVector;
542 | let searchResult;
543 |
544 | try {
545 | // Method 1: Try using VectorFloat constructor (if available)
546 | if (globalHnswlib && globalHnswlib.VectorFloat) {
547 | console.log('VectorDatabase: Using VectorFloat for search query');
548 | queryVector = new globalHnswlib.VectorFloat();
549 | // Add elements to VectorFloat one by one
550 | for (let i = 0; i < queryEmbedding.length; i++) {
551 | queryVector.push_back(queryEmbedding[i]);
552 | }
553 | searchResult = this.index.searchKnn(queryVector, topK, undefined);
554 |
555 | // Clean up VectorFloat object
556 | if (queryVector && typeof queryVector.delete === 'function') {
557 | queryVector.delete();
558 | }
559 | } else {
560 | // Method 2: Use plain JS array (fallback)
561 | console.log('VectorDatabase: Using plain JS array for search query');
562 | const queryArray = Array.from(queryEmbedding);
563 | searchResult = this.index.searchKnn(queryArray, topK, undefined);
564 | }
565 | } catch (vectorError) {
566 | console.error(
567 | 'VectorDatabase: VectorFloat search failed, trying alternatives:',
568 | vectorError,
569 | );
570 |
571 | // Method 3: Try passing Float32Array directly
572 | try {
573 | console.log('VectorDatabase: Trying Float32Array directly for search');
574 | searchResult = this.index.searchKnn(queryEmbedding, topK, undefined);
575 | } catch (float32Error) {
576 | console.error('VectorDatabase: Float32Array search failed:', float32Error);
577 |
578 | // Method 4: Last resort - use spread operator
579 | console.log('VectorDatabase: Trying spread operator for search as last resort');
580 | searchResult = this.index.searchKnn([...queryEmbedding], topK, undefined);
581 | }
582 | }
583 |
584 | const results: SearchResult[] = [];
585 |
586 | console.log(`VectorDatabase: Processing ${searchResult.neighbors.length} search neighbors`);
587 | console.log(`VectorDatabase: Available documents in mapping: ${this.documents.size}`);
588 | console.log(`VectorDatabase: Index current count: ${this.index.getCurrentCount()}`);
589 |
590 | for (let i = 0; i < searchResult.neighbors.length; i++) {
591 | const label = searchResult.neighbors[i];
592 | const distance = searchResult.distances[i];
593 | const similarity = 1 - distance; // Convert cosine distance to similarity
594 |
595 | console.log(
596 | `VectorDatabase: Processing neighbor ${i}: label=${label}, distance=${distance}, similarity=${similarity}`,
597 | );
598 |
599 | // Find corresponding document by label
600 | const document = this.findDocumentByLabel(label);
601 | if (document) {
602 | console.log(`VectorDatabase: Found document for label ${label}: ${document.id}`);
603 | results.push({
604 | document,
605 | similarity,
606 | distance,
607 | });
608 | } else {
609 | console.warn(`VectorDatabase: No document found for label ${label}`);
610 |
611 | // Detailed debug information
612 | if (i < 5) {
613 | // Only show detailed info for first 5 neighbors to avoid log spam
614 | console.warn(
615 | `VectorDatabase: Available labels (first 20): ${Array.from(this.documents.keys()).slice(0, 20).join(', ')}`,
616 | );
617 | console.warn(`VectorDatabase: Total available labels: ${this.documents.size}`);
618 | console.warn(
619 | `VectorDatabase: Label type: ${typeof label}, Available label types: ${Array.from(
620 | this.documents.keys(),
621 | )
622 | .slice(0, 3)
623 | .map((k) => typeof k)
624 | .join(', ')}`,
625 | );
626 | }
627 | }
628 | }
629 |
630 | console.log(
631 | `VectorDatabase: Found ${results.length} search results out of ${searchResult.neighbors.length} neighbors`,
632 | );
633 |
634 | // If no results found but index has data, indicates label mismatch
635 | if (results.length === 0 && searchResult.neighbors.length > 0) {
636 | console.error(
637 | 'VectorDatabase: Label mismatch detected! Index has vectors but no matching documents found.',
638 | );
639 | console.error(
640 | 'VectorDatabase: This usually indicates the index and document mappings are out of sync.',
641 | );
642 | console.error('VectorDatabase: Consider rebuilding the index to fix this issue.');
643 |
644 | // Provide some diagnostic information
645 | const sampleLabels = searchResult.neighbors.slice(0, 5);
646 | const availableLabels = Array.from(this.documents.keys()).slice(0, 5);
647 | console.error('VectorDatabase: Sample search labels:', sampleLabels);
648 | console.error('VectorDatabase: Sample available labels:', availableLabels);
649 | }
650 |
651 | return results.sort((a, b) => b.similarity - a.similarity);
652 | } catch (error) {
653 | console.error('VectorDatabase: Search failed:', error);
654 | console.error('VectorDatabase: Query embedding info:', {
655 | type: typeof queryEmbedding,
656 | constructor: queryEmbedding?.constructor?.name,
657 | length: queryEmbedding?.length,
658 | isFloat32Array: queryEmbedding instanceof Float32Array,
659 | firstFewValues: queryEmbedding ? Array.from(queryEmbedding.slice(0, 5)) : null,
660 | });
661 | throw error;
662 | }
663 | }
664 |
665 | /**
666 | * Remove all documents for a tab
667 | */
668 | public async removeTabDocuments(tabId: number): Promise<void> {
669 | if (!this.isInitialized) {
670 | await this.initialize();
671 | }
672 |
673 | const documentLabels = this.tabDocuments.get(tabId);
674 | if (!documentLabels) {
675 | return;
676 | }
677 |
678 | try {
679 | // Remove documents from mapping (hnswlib-wasm doesn't support direct deletion, only mark as deleted)
680 | for (const label of documentLabels) {
681 | this.documents.delete(label);
682 | }
683 |
684 | // Clean up tab mapping
685 | this.tabDocuments.delete(tabId);
686 |
687 | // Save changes
688 | await this.saveDocumentMappings();
689 |
690 | console.log(`VectorDatabase: Removed ${documentLabels.size} documents for tab ${tabId}`);
691 | } catch (error) {
692 | console.error('VectorDatabase: Failed to remove tab documents:', error);
693 | throw error;
694 | }
695 | }
696 |
697 | /**
698 | * Get database statistics
699 | */
700 | public getStats(): {
701 | totalDocuments: number;
702 | totalTabs: number;
703 | indexSize: number;
704 | isInitialized: boolean;
705 | } {
706 | return {
707 | totalDocuments: this.documents.size,
708 | totalTabs: this.tabDocuments.size,
709 | indexSize: this.calculateStorageSize(),
710 | isInitialized: this.isInitialized,
711 | };
712 | }
713 |
714 | /**
715 | * Calculate actual storage size (bytes)
716 | */
717 | private calculateStorageSize(): number {
718 | let totalSize = 0;
719 |
720 | try {
721 | // 1. 计算文档映射的大小
722 | const documentsSize = this.calculateDocumentMappingsSize();
723 | totalSize += documentsSize;
724 |
725 | // 2. 计算向量数据的大小
726 | const vectorsSize = this.calculateVectorsSize();
727 | totalSize += vectorsSize;
728 |
729 | // 3. 估算索引结构的大小
730 | const indexStructureSize = this.calculateIndexStructureSize();
731 | totalSize += indexStructureSize;
732 |
733 | console.log(
734 | `VectorDatabase: Storage size breakdown - Documents: ${documentsSize}, Vectors: ${vectorsSize}, Index: ${indexStructureSize}, Total: ${totalSize} bytes`,
735 | );
736 | } catch (error) {
737 | console.warn('VectorDatabase: Failed to calculate storage size:', error);
738 | // 返回一个基于文档数量的估算值
739 | totalSize = this.documents.size * 1024; // 每个文档估算1KB
740 | }
741 |
742 | return totalSize;
743 | }
744 |
745 | /**
746 | * Calculate document mappings size
747 | */
748 | private calculateDocumentMappingsSize(): number {
749 | let size = 0;
750 |
751 | // Calculate documents Map size
752 | for (const [label, document] of this.documents.entries()) {
753 | // label (number): 8 bytes
754 | size += 8;
755 |
756 | // document object
757 | size += this.calculateObjectSize(document);
758 | }
759 |
760 | // Calculate tabDocuments Map size
761 | for (const [tabId, labels] of this.tabDocuments.entries()) {
762 | // tabId (number): 8 bytes
763 | size += 8;
764 |
765 | // Set of labels: 8 bytes per label + Set overhead
766 | size += labels.size * 8 + 32; // 32 bytes Set overhead
767 | }
768 |
769 | return size;
770 | }
771 |
772 | /**
773 | * Calculate vectors data size
774 | */
775 | private calculateVectorsSize(): number {
776 | const documentCount = this.documents.size;
777 | const dimension = this.config.dimension;
778 |
779 | // Each vector: dimension * 4 bytes (Float32)
780 | const vectorSize = dimension * 4;
781 |
782 | return documentCount * vectorSize;
783 | }
784 |
785 | /**
786 | * Estimate index structure size
787 | */
788 | private calculateIndexStructureSize(): number {
789 | const documentCount = this.documents.size;
790 |
791 | if (documentCount === 0) return 0;
792 |
793 | // HNSW index size estimation
794 | // Based on papers and actual testing, HNSW index size is about 20-40% of vector data
795 | const vectorsSize = this.calculateVectorsSize();
796 | const indexOverhead = Math.floor(vectorsSize * 0.3); // 30% overhead
797 |
798 | // Additional graph structure overhead
799 | const graphOverhead = documentCount * 64; // About 64 bytes graph structure overhead per node
800 |
801 | return indexOverhead + graphOverhead;
802 | }
803 |
804 | /**
805 | * Calculate object size (rough estimation)
806 | */
807 | private calculateObjectSize(obj: any): number {
808 | let size = 0;
809 |
810 | try {
811 | const jsonString = JSON.stringify(obj);
812 | // UTF-8 encoding, most characters 1 byte, Chinese etc 3 bytes, average 2 bytes
813 | size = jsonString.length * 2;
814 | } catch (error) {
815 | // If JSON serialization fails, use default estimation
816 | size = 512; // Default 512 bytes
817 | }
818 |
819 | return size;
820 | }
821 |
822 | /**
823 | * Clear entire database
824 | */
825 | public async clear(): Promise<void> {
826 | console.log('VectorDatabase: Starting complete database clear...');
827 |
828 | try {
829 | // Clear in-memory data structures
830 | this.documents.clear();
831 | this.tabDocuments.clear();
832 | this.nextLabel = 0;
833 |
834 | // Clear HNSW index file (in hnswlib-index database)
835 | if (this.isInitialized && this.index) {
836 | try {
837 | console.log('VectorDatabase: Clearing HNSW index file from IndexedDB...');
838 |
839 | // 1. First try to physically delete index file (using EmscriptenFileSystemManager)
840 | try {
841 | if (
842 | globalHnswlib &&
843 | globalHnswlib.EmscriptenFileSystemManager.checkFileExists(this.config.indexFileName)
844 | ) {
845 | console.log(
846 | `VectorDatabase: Deleting physical index file: ${this.config.indexFileName}`,
847 | );
848 | globalHnswlib.EmscriptenFileSystemManager.deleteFile(this.config.indexFileName);
849 | await this.syncFileSystem('write'); // Ensure deletion is synced to persistent storage
850 | console.log(
851 | `VectorDatabase: Physical index file ${this.config.indexFileName} deleted successfully`,
852 | );
853 | } else {
854 | console.log(
855 | `VectorDatabase: Physical index file ${this.config.indexFileName} does not exist or already deleted`,
856 | );
857 | }
858 | } catch (fileError) {
859 | console.warn(
860 | `VectorDatabase: Failed to delete physical index file ${this.config.indexFileName}:`,
861 | fileError,
862 | );
863 | // Continue with other cleanup operations, don't block the process
864 | }
865 |
866 | // 2. Delete index file from IndexedDB
867 | await this.index.deleteIndex(this.config.indexFileName);
868 | console.log('VectorDatabase: HNSW index file cleared from IndexedDB');
869 |
870 | // 3. Reinitialize empty index
871 | console.log('VectorDatabase: Reinitializing empty HNSW index...');
872 | this.index.initIndex(
873 | this.config.maxElements,
874 | this.config.M,
875 | this.config.efConstruction,
876 | 200,
877 | );
878 | this.index.setEfSearch(this.config.efSearch);
879 |
880 | // 4. Force save empty index
881 | await this.forceSaveIndex();
882 | } catch (indexError) {
883 | console.warn('VectorDatabase: Failed to clear HNSW index file:', indexError);
884 | // Continue with other cleanup operations
885 | }
886 | }
887 |
888 | // Clear document mappings from IndexedDB (in VectorDatabaseStorage database)
889 | try {
890 | console.log('VectorDatabase: Clearing document mappings from IndexedDB...');
891 | await IndexedDBHelper.deleteData(this.config.indexFileName);
892 | console.log('VectorDatabase: Document mappings cleared from IndexedDB');
893 | } catch (idbError) {
894 | console.warn(
895 | 'VectorDatabase: Failed to clear document mappings from IndexedDB, trying chrome.storage fallback:',
896 | idbError,
897 | );
898 |
899 | // Clear backup data from chrome.storage
900 | try {
901 | const storageKey = `hnswlib_document_mappings_${this.config.indexFileName}`;
902 | await chrome.storage.local.remove([storageKey]);
903 | console.log('VectorDatabase: Chrome storage fallback cleared');
904 | } catch (storageError) {
905 | console.warn('VectorDatabase: Failed to clear chrome.storage fallback:', storageError);
906 | }
907 | }
908 |
909 | // Save empty document mappings to ensure consistency
910 | await this.saveDocumentMappings();
911 |
912 | console.log('VectorDatabase: Complete database clear finished successfully');
913 | } catch (error) {
914 | console.error('VectorDatabase: Failed to clear database:', error);
915 | throw error;
916 | }
917 | }
918 |
919 | /**
920 | * Force save index and sync filesystem
921 | */
922 | private async forceSaveIndex(): Promise<void> {
923 | try {
924 | await this.index.writeIndex(this.config.indexFileName);
925 | await this.syncFileSystem('write'); // Force sync
926 | } catch (error) {
927 | console.error('VectorDatabase: Failed to force save index:', error);
928 | }
929 | }
930 |
931 | /**
932 | * Check and perform auto cleanup
933 | */
934 | private async checkAndPerformAutoCleanup(): Promise<void> {
935 | try {
936 | const currentCount = this.documents.size;
937 | const maxElements = this.config.maxElements;
938 |
939 | console.log(
940 | `VectorDatabase: Auto cleanup check - current: ${currentCount}, max: ${maxElements}`,
941 | );
942 |
943 | // Check if maximum element count is exceeded
944 | if (currentCount >= maxElements) {
945 | console.log('VectorDatabase: Document count reached limit, performing cleanup...');
946 | await this.performLRUCleanup(Math.floor(maxElements * 0.2)); // Clean up 20% of data
947 | }
948 |
949 | // Check if there's expired data
950 | if (this.config.maxRetentionDays && this.config.maxRetentionDays > 0) {
951 | await this.performTimeBasedCleanup();
952 | }
953 | } catch (error) {
954 | console.error('VectorDatabase: Auto cleanup failed:', error);
955 | }
956 | }
957 |
958 | /**
959 | * Perform LRU-based cleanup (delete oldest documents)
960 | */
961 | private async performLRUCleanup(cleanupCount: number): Promise<void> {
962 | try {
963 | console.log(
964 | `VectorDatabase: Starting LRU cleanup, removing ${cleanupCount} oldest documents`,
965 | );
966 |
967 | // Get all documents and sort by timestamp
968 | const allDocuments = Array.from(this.documents.entries());
969 | allDocuments.sort((a, b) => a[1].timestamp - b[1].timestamp);
970 |
971 | // Select documents to delete
972 | const documentsToDelete = allDocuments.slice(0, cleanupCount);
973 |
974 | for (const [label, _document] of documentsToDelete) {
975 | await this.removeDocumentByLabel(label);
976 | }
977 |
978 | // Save updated index and mappings
979 | await this.saveIndex();
980 | await this.saveDocumentMappings();
981 |
982 | console.log(
983 | `VectorDatabase: LRU cleanup completed, removed ${documentsToDelete.length} documents`,
984 | );
985 | } catch (error) {
986 | console.error('VectorDatabase: LRU cleanup failed:', error);
987 | }
988 | }
989 |
990 | /**
991 | * Perform time-based cleanup (delete expired documents)
992 | */
993 | private async performTimeBasedCleanup(): Promise<void> {
994 | try {
995 | const maxRetentionMs = this.config.maxRetentionDays! * 24 * 60 * 60 * 1000;
996 | const cutoffTime = Date.now() - maxRetentionMs;
997 |
998 | console.log(
999 | `VectorDatabase: Starting time-based cleanup, removing documents older than ${this.config.maxRetentionDays} days`,
1000 | );
1001 |
1002 | const documentsToDelete: number[] = [];
1003 |
1004 | for (const [label, document] of this.documents.entries()) {
1005 | if (document.timestamp < cutoffTime) {
1006 | documentsToDelete.push(label);
1007 | }
1008 | }
1009 |
1010 | for (const label of documentsToDelete) {
1011 | await this.removeDocumentByLabel(label);
1012 | }
1013 |
1014 | // Save updated index and mappings
1015 | if (documentsToDelete.length > 0) {
1016 | await this.saveIndex();
1017 | await this.saveDocumentMappings();
1018 | }
1019 |
1020 | console.log(
1021 | `VectorDatabase: Time-based cleanup completed, removed ${documentsToDelete.length} expired documents`,
1022 | );
1023 | } catch (error) {
1024 | console.error('VectorDatabase: Time-based cleanup failed:', error);
1025 | }
1026 | }
1027 |
1028 | /**
1029 | * Remove single document by label
1030 | */
1031 | private async removeDocumentByLabel(label: number): Promise<void> {
1032 | try {
1033 | const document = this.documents.get(label);
1034 | if (!document) {
1035 | console.warn(`VectorDatabase: Document with label ${label} not found`);
1036 | return;
1037 | }
1038 |
1039 | // Remove vector from HNSW index
1040 | if (this.index) {
1041 | try {
1042 | this.index.markDelete(label);
1043 | } catch (indexError) {
1044 | console.warn(
1045 | `VectorDatabase: Failed to mark delete in index for label ${label}:`,
1046 | indexError,
1047 | );
1048 | }
1049 | }
1050 |
1051 | // Remove from memory mapping
1052 | this.documents.delete(label);
1053 |
1054 | // Remove from tab mapping
1055 | const tabId = document.tabId;
1056 | if (this.tabDocuments.has(tabId)) {
1057 | this.tabDocuments.get(tabId)!.delete(label);
1058 | // If tab has no other documents, delete entire tab mapping
1059 | if (this.tabDocuments.get(tabId)!.size === 0) {
1060 | this.tabDocuments.delete(tabId);
1061 | }
1062 | }
1063 |
1064 | console.log(`VectorDatabase: Removed document with label ${label} from tab ${tabId}`);
1065 | } catch (error) {
1066 | console.error(`VectorDatabase: Failed to remove document with label ${label}:`, error);
1067 | }
1068 | }
1069 |
1070 | // 私有辅助方法
1071 |
1072 | private generateDocumentId(tabId: number, chunkIndex: number): string {
1073 | return `tab_${tabId}_chunk_${chunkIndex}_${Date.now()}`;
1074 | }
1075 |
1076 | private findDocumentByLabel(label: number): VectorDocument | null {
1077 | return this.documents.get(label) || null;
1078 | }
1079 |
1080 | private async syncFileSystem(direction: 'read' | 'write'): Promise<void> {
1081 | try {
1082 | if (!globalHnswlib) {
1083 | return;
1084 | }
1085 |
1086 | // If sync operation is already in progress, wait for it to complete
1087 | if (syncInProgress && pendingSyncPromise) {
1088 | console.log(`VectorDatabase: Sync already in progress, waiting...`);
1089 | await pendingSyncPromise;
1090 | return;
1091 | }
1092 |
1093 | // Mark sync start
1094 | syncInProgress = true;
1095 |
1096 | // Create sync Promise with timeout mechanism
1097 | pendingSyncPromise = new Promise<void>((resolve, reject) => {
1098 | const timeout = setTimeout(() => {
1099 | console.warn(`VectorDatabase: Filesystem sync (${direction}) timeout`);
1100 | syncInProgress = false;
1101 | pendingSyncPromise = null;
1102 | reject(new Error('Sync timeout'));
1103 | }, 5000); // 5 second timeout
1104 |
1105 | try {
1106 | globalHnswlib.EmscriptenFileSystemManager.syncFS(direction === 'read', () => {
1107 | clearTimeout(timeout);
1108 | console.log(`VectorDatabase: Filesystem sync (${direction}) completed`);
1109 | syncInProgress = false;
1110 | pendingSyncPromise = null;
1111 | resolve();
1112 | });
1113 | } catch (error) {
1114 | clearTimeout(timeout);
1115 | console.warn(`VectorDatabase: Failed to sync filesystem (${direction}):`, error);
1116 | syncInProgress = false;
1117 | pendingSyncPromise = null;
1118 | reject(error);
1119 | }
1120 | });
1121 |
1122 | await pendingSyncPromise;
1123 | } catch (error) {
1124 | console.warn(`VectorDatabase: Failed to sync filesystem (${direction}):`, error);
1125 | syncInProgress = false;
1126 | pendingSyncPromise = null;
1127 | }
1128 | }
1129 |
1130 | private async saveIndex(): Promise<void> {
1131 | try {
1132 | await this.index.writeIndex(this.config.indexFileName);
1133 | // Reduce sync frequency, only sync when necessary
1134 | if (this.documents.size % 10 === 0) {
1135 | // Sync every 10 documents
1136 | await this.syncFileSystem('write');
1137 | }
1138 | } catch (error) {
1139 | console.error('VectorDatabase: Failed to save index:', error);
1140 | }
1141 | }
1142 |
1143 | private async saveDocumentMappings(): Promise<void> {
1144 | try {
1145 | // Save document mappings to IndexedDB
1146 | const mappingData = {
1147 | documents: Array.from(this.documents.entries()),
1148 | tabDocuments: Array.from(this.tabDocuments.entries()).map(([tabId, labels]) => [
1149 | tabId,
1150 | Array.from(labels),
1151 | ]),
1152 | nextLabel: this.nextLabel,
1153 | };
1154 |
1155 | try {
1156 | // Use IndexedDB to save data, supports larger storage capacity
1157 | await IndexedDBHelper.saveData(this.config.indexFileName, mappingData);
1158 | console.log('VectorDatabase: Document mappings saved to IndexedDB');
1159 | } catch (idbError) {
1160 | console.warn(
1161 | 'VectorDatabase: Failed to save to IndexedDB, falling back to chrome.storage:',
1162 | idbError,
1163 | );
1164 |
1165 | // Fall back to chrome.storage.local
1166 | try {
1167 | const storageKey = `hnswlib_document_mappings_${this.config.indexFileName}`;
1168 | await chrome.storage.local.set({ [storageKey]: mappingData });
1169 | console.log('VectorDatabase: Document mappings saved to chrome.storage.local (fallback)');
1170 | } catch (storageError) {
1171 | console.error(
1172 | 'VectorDatabase: Failed to save to both IndexedDB and chrome.storage:',
1173 | storageError,
1174 | );
1175 | }
1176 | }
1177 | } catch (error) {
1178 | console.error('VectorDatabase: Failed to save document mappings:', error);
1179 | }
1180 | }
1181 |
1182 | public async loadDocumentMappings(): Promise<void> {
1183 | try {
1184 | // Load document mappings from IndexedDB
1185 | if (!globalHnswlib) {
1186 | return;
1187 | }
1188 |
1189 | let mappingData = null;
1190 |
1191 | try {
1192 | // First try to read from IndexedDB
1193 | mappingData = await IndexedDBHelper.loadData(this.config.indexFileName);
1194 | if (mappingData) {
1195 | console.log(`VectorDatabase: Loaded document mappings from IndexedDB`);
1196 | }
1197 | } catch (idbError) {
1198 | console.warn(
1199 | 'VectorDatabase: Failed to read from IndexedDB, trying chrome.storage:',
1200 | idbError,
1201 | );
1202 | }
1203 |
1204 | // If IndexedDB has no data, try reading from chrome.storage.local (backward compatibility)
1205 | if (!mappingData) {
1206 | try {
1207 | const storageKey = `hnswlib_document_mappings_${this.config.indexFileName}`;
1208 | const result = await chrome.storage.local.get([storageKey]);
1209 | mappingData = result[storageKey];
1210 | if (mappingData) {
1211 | console.log(
1212 | `VectorDatabase: Loaded document mappings from chrome.storage.local (fallback)`,
1213 | );
1214 |
1215 | // Migrate to IndexedDB
1216 | try {
1217 | await IndexedDBHelper.saveData(this.config.indexFileName, mappingData);
1218 | console.log('VectorDatabase: Migrated data from chrome.storage to IndexedDB');
1219 | } catch (migrationError) {
1220 | console.warn('VectorDatabase: Failed to migrate data to IndexedDB:', migrationError);
1221 | }
1222 | }
1223 | } catch (storageError) {
1224 | console.warn('VectorDatabase: Failed to read from chrome.storage.local:', storageError);
1225 | }
1226 | }
1227 |
1228 | if (mappingData) {
1229 | // Restore document mappings
1230 | this.documents.clear();
1231 | for (const [label, doc] of mappingData.documents) {
1232 | this.documents.set(label, doc);
1233 | }
1234 |
1235 | // Restore tab mappings
1236 | this.tabDocuments.clear();
1237 | for (const [tabId, labels] of mappingData.tabDocuments) {
1238 | this.tabDocuments.set(tabId, new Set(labels));
1239 | }
1240 |
1241 | // Restore nextLabel - use saved value or calculate max label + 1
1242 | if (mappingData.nextLabel !== undefined) {
1243 | this.nextLabel = mappingData.nextLabel;
1244 | } else if (this.documents.size > 0) {
1245 | // If no saved nextLabel, calculate max label + 1
1246 | const maxLabel = Math.max(...Array.from(this.documents.keys()));
1247 | this.nextLabel = maxLabel + 1;
1248 | } else {
1249 | this.nextLabel = 0;
1250 | }
1251 |
1252 | console.log(
1253 | `VectorDatabase: Loaded ${this.documents.size} document mappings, next label: ${this.nextLabel}`,
1254 | );
1255 | } else {
1256 | console.log('VectorDatabase: No existing document mappings found');
1257 | }
1258 | } catch (error) {
1259 | console.error('VectorDatabase: Failed to load document mappings:', error);
1260 | }
1261 | }
1262 | }
1263 |
1264 | // Global VectorDatabase singleton
1265 | let globalVectorDatabase: VectorDatabase | null = null;
1266 | let currentDimension: number | null = null;
1267 |
1268 | /**
1269 | * Get global VectorDatabase singleton instance
1270 | * If dimension changes, will recreate instance to ensure compatibility
1271 | */
1272 | export async function getGlobalVectorDatabase(
1273 | config?: Partial<VectorDatabaseConfig>,
1274 | ): Promise<VectorDatabase> {
1275 | const newDimension = config?.dimension || 384;
1276 |
1277 | // If dimension changes, need to recreate vector database
1278 | if (globalVectorDatabase && currentDimension !== null && currentDimension !== newDimension) {
1279 | console.log(
1280 | `VectorDatabase: Dimension changed from ${currentDimension} to ${newDimension}, recreating instance`,
1281 | );
1282 |
1283 | // Clean up old instance - this will clean up index files and document mappings
1284 | try {
1285 | await globalVectorDatabase.clear();
1286 | console.log('VectorDatabase: Successfully cleared old instance for dimension change');
1287 | } catch (error) {
1288 | console.warn('VectorDatabase: Error during cleanup:', error);
1289 | }
1290 |
1291 | globalVectorDatabase = null;
1292 | currentDimension = null;
1293 | }
1294 |
1295 | if (!globalVectorDatabase) {
1296 | globalVectorDatabase = new VectorDatabase(config);
1297 | currentDimension = newDimension;
1298 | console.log(
1299 | `VectorDatabase: Created global singleton instance with dimension ${currentDimension}`,
1300 | );
1301 | }
1302 |
1303 | return globalVectorDatabase;
1304 | }
1305 |
1306 | /**
1307 | * Synchronous version of getting global VectorDatabase instance (for backward compatibility)
1308 | * Note: If dimension change is needed, recommend using async version
1309 | */
1310 | export function getGlobalVectorDatabaseSync(
1311 | config?: Partial<VectorDatabaseConfig>,
1312 | ): VectorDatabase {
1313 | const newDimension = config?.dimension || 384;
1314 |
1315 | // If dimension changes, log warning but don't clean up (avoid race conditions)
1316 | if (globalVectorDatabase && currentDimension !== null && currentDimension !== newDimension) {
1317 | console.warn(
1318 | `VectorDatabase: Dimension mismatch detected (${currentDimension} vs ${newDimension}). Consider using async version for proper cleanup.`,
1319 | );
1320 | }
1321 |
1322 | if (!globalVectorDatabase) {
1323 | globalVectorDatabase = new VectorDatabase(config);
1324 | currentDimension = newDimension;
1325 | console.log(
1326 | `VectorDatabase: Created global singleton instance with dimension ${currentDimension}`,
1327 | );
1328 | }
1329 |
1330 | return globalVectorDatabase;
1331 | }
1332 |
1333 | /**
1334 | * Reset global VectorDatabase instance (mainly for testing or model switching)
1335 | */
1336 | export async function resetGlobalVectorDatabase(): Promise<void> {
1337 | console.log('VectorDatabase: Starting global instance reset...');
1338 |
1339 | if (globalVectorDatabase) {
1340 | try {
1341 | console.log('VectorDatabase: Clearing existing global instance...');
1342 | await globalVectorDatabase.clear();
1343 | console.log('VectorDatabase: Global instance cleared successfully');
1344 | } catch (error) {
1345 | console.warn('VectorDatabase: Failed to clear during reset:', error);
1346 | }
1347 | }
1348 |
1349 | // Additional cleanup: ensure all possible IndexedDB data is cleared
1350 | try {
1351 | console.log('VectorDatabase: Performing comprehensive IndexedDB cleanup...');
1352 |
1353 | // Clear all data in VectorDatabaseStorage database
1354 | await IndexedDBHelper.clearAllData();
1355 |
1356 | // Clear index files from hnswlib-index database
1357 | try {
1358 | console.log('VectorDatabase: Clearing HNSW index files from IndexedDB...');
1359 |
1360 | // Try to clean up possible existing index files
1361 | const possibleIndexFiles = ['tab_content_index.dat', 'content_index.dat', 'vector_index.dat'];
1362 |
1363 | // If global hnswlib instance exists, try to delete known index files
1364 | if (typeof globalHnswlib !== 'undefined' && globalHnswlib) {
1365 | for (const fileName of possibleIndexFiles) {
1366 | try {
1367 | // 1. First try to physically delete index file (using EmscriptenFileSystemManager)
1368 | try {
1369 | if (globalHnswlib.EmscriptenFileSystemManager.checkFileExists(fileName)) {
1370 | console.log(`VectorDatabase: Deleting physical index file: ${fileName}`);
1371 | globalHnswlib.EmscriptenFileSystemManager.deleteFile(fileName);
1372 | console.log(`VectorDatabase: Physical index file ${fileName} deleted successfully`);
1373 | }
1374 | } catch (fileError) {
1375 | console.log(
1376 | `VectorDatabase: Physical index file ${fileName} not found or failed to delete:`,
1377 | fileError,
1378 | );
1379 | }
1380 |
1381 | // 2. Delete index file from IndexedDB
1382 | const tempIndex = new globalHnswlib.HierarchicalNSW('cosine', 384);
1383 | await tempIndex.deleteIndex(fileName);
1384 | console.log(`VectorDatabase: Deleted IndexedDB index file: ${fileName}`);
1385 | } catch (deleteError) {
1386 | // File might not exist, this is normal
1387 | console.log(`VectorDatabase: Index file ${fileName} not found or already deleted`);
1388 | }
1389 | }
1390 |
1391 | // 3. Force sync filesystem to ensure deletion takes effect
1392 | try {
1393 | await new Promise<void>((resolve) => {
1394 | const timeout = setTimeout(() => {
1395 | console.warn('VectorDatabase: Filesystem sync timeout during cleanup');
1396 | resolve(); // Don't block the process
1397 | }, 3000);
1398 |
1399 | globalHnswlib.EmscriptenFileSystemManager.syncFS(false, () => {
1400 | clearTimeout(timeout);
1401 | console.log('VectorDatabase: Filesystem sync completed during cleanup');
1402 | resolve();
1403 | });
1404 | });
1405 | } catch (syncError) {
1406 | console.warn('VectorDatabase: Failed to sync filesystem during cleanup:', syncError);
1407 | }
1408 | }
1409 | } catch (hnswError) {
1410 | console.warn('VectorDatabase: Failed to clear HNSW index files:', hnswError);
1411 | }
1412 |
1413 | // Clear possible chrome.storage backup data (only clear vector database related data, preserve user preferences)
1414 | const possibleKeys = [
1415 | 'hnswlib_document_mappings_tab_content_index.dat',
1416 | 'hnswlib_document_mappings_content_index.dat',
1417 | 'hnswlib_document_mappings_vector_index.dat',
1418 | // Note: Don't clear selectedModel and selectedVersion, these are user preference settings
1419 | // Note: Don't clear modelState, this contains model state info and should be handled by model management logic
1420 | ];
1421 |
1422 | if (possibleKeys.length > 0) {
1423 | try {
1424 | await chrome.storage.local.remove(possibleKeys);
1425 | console.log('VectorDatabase: Chrome storage backup data cleared');
1426 | } catch (storageError) {
1427 | console.warn('VectorDatabase: Failed to clear chrome.storage backup:', storageError);
1428 | }
1429 | }
1430 |
1431 | console.log('VectorDatabase: Comprehensive cleanup completed');
1432 | } catch (cleanupError) {
1433 | console.warn('VectorDatabase: Comprehensive cleanup failed:', cleanupError);
1434 | }
1435 |
1436 | globalVectorDatabase = null;
1437 | currentDimension = null;
1438 | console.log('VectorDatabase: Global singleton instance reset completed');
1439 | }
1440 |
1441 | /**
1442 | * Specifically for data cleanup during model switching
1443 | * Clear all IndexedDB data, including HNSW index files and document mappings
1444 | */
1445 | export async function clearAllVectorData(): Promise<void> {
1446 | console.log('VectorDatabase: Starting comprehensive vector data cleanup for model switch...');
1447 |
1448 | try {
1449 | // 1. Clear global instance
1450 | if (globalVectorDatabase) {
1451 | try {
1452 | await globalVectorDatabase.clear();
1453 | } catch (error) {
1454 | console.warn('VectorDatabase: Failed to clear global instance:', error);
1455 | }
1456 | }
1457 |
1458 | // 2. Clear VectorDatabaseStorage database
1459 | try {
1460 | console.log('VectorDatabase: Clearing VectorDatabaseStorage database...');
1461 | await IndexedDBHelper.clearAllData();
1462 | } catch (error) {
1463 | console.warn('VectorDatabase: Failed to clear VectorDatabaseStorage:', error);
1464 | }
1465 |
1466 | // 3. Clear hnswlib-index database and physical files
1467 | try {
1468 | console.log('VectorDatabase: Clearing hnswlib-index database and physical files...');
1469 |
1470 | // 3.1 First try to physically delete index files (using EmscriptenFileSystemManager)
1471 | if (typeof globalHnswlib !== 'undefined' && globalHnswlib) {
1472 | const possibleIndexFiles = [
1473 | 'tab_content_index.dat',
1474 | 'content_index.dat',
1475 | 'vector_index.dat',
1476 | ];
1477 |
1478 | for (const fileName of possibleIndexFiles) {
1479 | try {
1480 | if (globalHnswlib.EmscriptenFileSystemManager.checkFileExists(fileName)) {
1481 | console.log(`VectorDatabase: Deleting physical index file: ${fileName}`);
1482 | globalHnswlib.EmscriptenFileSystemManager.deleteFile(fileName);
1483 | console.log(`VectorDatabase: Physical index file ${fileName} deleted successfully`);
1484 | }
1485 | } catch (fileError) {
1486 | console.log(
1487 | `VectorDatabase: Physical index file ${fileName} not found or failed to delete:`,
1488 | fileError,
1489 | );
1490 | }
1491 | }
1492 |
1493 | // Force sync filesystem
1494 | try {
1495 | await new Promise<void>((resolve) => {
1496 | const timeout = setTimeout(() => {
1497 | console.warn('VectorDatabase: Filesystem sync timeout during model switch cleanup');
1498 | resolve();
1499 | }, 3000);
1500 |
1501 | globalHnswlib.EmscriptenFileSystemManager.syncFS(false, () => {
1502 | clearTimeout(timeout);
1503 | console.log('VectorDatabase: Filesystem sync completed during model switch cleanup');
1504 | resolve();
1505 | });
1506 | });
1507 | } catch (syncError) {
1508 | console.warn(
1509 | 'VectorDatabase: Failed to sync filesystem during model switch cleanup:',
1510 | syncError,
1511 | );
1512 | }
1513 | }
1514 |
1515 | // 3.2 Delete entire hnswlib-index database
1516 | await new Promise<void>((resolve) => {
1517 | const deleteRequest = indexedDB.deleteDatabase('/hnswlib-index');
1518 | deleteRequest.onsuccess = () => {
1519 | console.log('VectorDatabase: Successfully deleted /hnswlib-index database');
1520 | resolve();
1521 | };
1522 | deleteRequest.onerror = () => {
1523 | console.warn(
1524 | 'VectorDatabase: Failed to delete /hnswlib-index database:',
1525 | deleteRequest.error,
1526 | );
1527 | resolve(); // Don't block the process
1528 | };
1529 | deleteRequest.onblocked = () => {
1530 | console.warn('VectorDatabase: Deletion of /hnswlib-index database was blocked');
1531 | resolve(); // Don't block the process
1532 | };
1533 | });
1534 | } catch (error) {
1535 | console.warn(
1536 | 'VectorDatabase: Failed to clear hnswlib-index database and physical files:',
1537 | error,
1538 | );
1539 | }
1540 |
1541 | // 4. Clear backup data from chrome.storage
1542 | try {
1543 | const storageKeys = [
1544 | 'hnswlib_document_mappings_tab_content_index.dat',
1545 | 'hnswlib_document_mappings_content_index.dat',
1546 | 'hnswlib_document_mappings_vector_index.dat',
1547 | ];
1548 | await chrome.storage.local.remove(storageKeys);
1549 | console.log('VectorDatabase: Chrome storage backup data cleared');
1550 | } catch (error) {
1551 | console.warn('VectorDatabase: Failed to clear chrome.storage backup:', error);
1552 | }
1553 |
1554 | // 5. Reset global state
1555 | globalVectorDatabase = null;
1556 | currentDimension = null;
1557 |
1558 | console.log('VectorDatabase: Comprehensive vector data cleanup completed successfully');
1559 | } catch (error) {
1560 | console.error('VectorDatabase: Comprehensive vector data cleanup failed:', error);
1561 | throw error;
1562 | }
1563 | }
1564 |
```