#
tokens: 36957/50000 2/120 files (page 6/10)
lines: on (toggle) GitHub
raw markdown copy reset
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 | 
```
Page 6/10FirstPrevNextLast