Files
broswer-automation/app/chrome-extension/entrypoints/popup/App.vue
nasir@endelospay.com d97cad1736 first commit
2025-08-12 02:54:17 +05:00

1921 lines
48 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="popup-container">
<div class="header">
<div class="header-content">
<h1 class="header-title">Chrome MCP Server</h1>
</div>
</div>
<div class="content">
<div class="section">
<h2 class="section-title">{{ getMessage('nativeServerConfigLabel') }}</h2>
<div class="config-card">
<div class="status-section">
<div class="status-header">
<p class="status-label">{{ getMessage('runningStatusLabel') }}</p>
<button
class="refresh-status-button"
@click="refreshServerStatus"
:title="getMessage('refreshStatusButton')"
>
🔄
</button>
</div>
<div class="status-info">
<span :class="['status-dot', getStatusClass()]"></span>
<span class="status-text">{{ getStatusText() }}</span>
</div>
<div v-if="serverStatus.lastUpdated" class="status-timestamp">
{{ getMessage('lastUpdatedLabel') }}
{{ new Date(serverStatus.lastUpdated).toLocaleTimeString() }}
</div>
</div>
<div v-if="showMcpConfig" class="mcp-config-section">
<div class="mcp-config-header">
<p class="mcp-config-label">{{ getMessage('mcpServerConfigLabel') }}</p>
<button class="copy-config-button" @click="copyMcpConfig">
{{ copyButtonText }}
</button>
</div>
<div class="mcp-config-content">
<pre class="mcp-config-json">{{ mcpConfigJson }}</pre>
</div>
</div>
<div class="port-section">
<label for="port" class="port-label">{{ getMessage('connectionPortLabel') }}</label>
<input
type="text"
id="port"
:value="nativeServerPort"
@input="updatePort"
class="port-input"
/>
</div>
<button class="connect-button" :disabled="isConnecting" @click="testNativeConnection">
<BoltIcon />
<span>{{
isConnecting
? getMessage('connectingStatus')
: nativeConnectionStatus === 'connected'
? getMessage('disconnectButton')
: getMessage('connectButton')
}}</span>
</button>
</div>
</div>
<div class="section">
<h2 class="section-title">{{ getMessage('semanticEngineLabel') }}</h2>
<div class="semantic-engine-card">
<div class="semantic-engine-status">
<div class="status-info">
<span :class="['status-dot', getSemanticEngineStatusClass()]"></span>
<span class="status-text">{{ getSemanticEngineStatusText() }}</span>
</div>
<div v-if="semanticEngineLastUpdated" class="status-timestamp">
{{ getMessage('lastUpdatedLabel') }}
{{ new Date(semanticEngineLastUpdated).toLocaleTimeString() }}
</div>
</div>
<ProgressIndicator
v-if="isSemanticEngineInitializing"
:visible="isSemanticEngineInitializing"
:text="semanticEngineInitProgress"
:showSpinner="true"
/>
<button
class="semantic-engine-button"
:disabled="isSemanticEngineInitializing"
@click="initializeSemanticEngine"
>
<BoltIcon />
<span>{{ getSemanticEngineButtonText() }}</span>
</button>
</div>
</div>
<div class="section">
<h2 class="section-title">{{ getMessage('embeddingModelLabel') }}</h2>
<ProgressIndicator
v-if="isModelSwitching || isModelDownloading"
:visible="isModelSwitching || isModelDownloading"
:text="getProgressText()"
:showSpinner="true"
/>
<div v-if="modelInitializationStatus === 'error'" class="error-card">
<div class="error-content">
<div class="error-icon"></div>
<div class="error-details">
<p class="error-title">{{ getMessage('semanticEngineInitFailedStatus') }}</p>
<p class="error-message">{{
modelErrorMessage || getMessage('semanticEngineInitFailedStatus')
}}</p>
<p class="error-suggestion">{{ getErrorTypeText() }}</p>
</div>
</div>
<button
class="retry-button"
@click="retryModelInitialization"
:disabled="isModelSwitching || isModelDownloading"
>
<span>🔄</span>
<span>{{ getMessage('retryButton') }}</span>
</button>
</div>
<div class="model-list">
<div
v-for="model in availableModels"
:key="model.preset"
:class="[
'model-card',
{
selected: currentModel === model.preset,
disabled: isModelSwitching || isModelDownloading,
},
]"
@click="
!isModelSwitching && !isModelDownloading && switchModel(model.preset as ModelPreset)
"
>
<div class="model-header">
<div class="model-info">
<p class="model-name" :class="{ 'selected-text': currentModel === model.preset }">
{{ model.preset }}
</p>
<p class="model-description">{{ getModelDescription(model) }}</p>
</div>
<div v-if="currentModel === model.preset" class="check-icon">
<CheckIcon class="text-white" />
</div>
</div>
<div class="model-tags">
<span class="model-tag performance">{{ getPerformanceText(model.performance) }}</span>
<span class="model-tag size">{{ model.size }}</span>
<span class="model-tag dimension">{{ model.dimension }}D</span>
</div>
</div>
</div>
</div>
<div class="section">
<h2 class="section-title">{{ getMessage('indexDataManagementLabel') }}</h2>
<div class="stats-grid">
<div class="stats-card">
<div class="stats-header">
<p class="stats-label">{{ getMessage('indexedPagesLabel') }}</p>
<span class="stats-icon violet">
<DocumentIcon />
</span>
</div>
<p class="stats-value">{{ storageStats?.indexedPages || 0 }}</p>
</div>
<div class="stats-card">
<div class="stats-header">
<p class="stats-label">{{ getMessage('indexSizeLabel') }}</p>
<span class="stats-icon teal">
<DatabaseIcon />
</span>
</div>
<p class="stats-value">{{ formatIndexSize() }}</p>
</div>
<div class="stats-card">
<div class="stats-header">
<p class="stats-label">{{ getMessage('activeTabsLabel') }}</p>
<span class="stats-icon blue">
<TabIcon />
</span>
</div>
<p class="stats-value">{{ getActiveTabsCount() }}</p>
</div>
<div class="stats-card">
<div class="stats-header">
<p class="stats-label">{{ getMessage('vectorDocumentsLabel') }}</p>
<span class="stats-icon green">
<VectorIcon />
</span>
</div>
<p class="stats-value">{{ storageStats?.totalDocuments || 0 }}</p>
</div>
</div>
<ProgressIndicator
v-if="isClearingData && clearDataProgress"
:visible="isClearingData"
:text="clearDataProgress"
:showSpinner="true"
/>
<button
class="danger-button"
:disabled="isClearingData"
@click="showClearConfirmation = true"
>
<TrashIcon />
<span>{{ isClearingData ? getMessage('clearingStatus') : getMessage('clearAllDataButton') }}</span>
</button>
</div>
<!-- Model Cache Management Section -->
<ModelCacheManagement
:cache-stats="cacheStats"
:is-managing-cache="isManagingCache"
@cleanup-cache="cleanupCache"
@clear-all-cache="clearAllCache"
/>
</div>
<div class="footer">
<p class="footer-text">chrome mcp server for ai</p>
</div>
<ConfirmDialog
:visible="showClearConfirmation"
:title="getMessage('confirmClearDataTitle')"
:message="getMessage('clearDataWarningMessage')"
:items="[
getMessage('clearDataList1'),
getMessage('clearDataList2'),
getMessage('clearDataList3'),
]"
:warning="getMessage('clearDataIrreversibleWarning')"
icon="⚠️"
:confirm-text="getMessage('confirmClearButton')"
:cancel-text="getMessage('cancelButton')"
:confirming-text="getMessage('clearingStatus')"
:is-confirming="isClearingData"
@confirm="confirmClearAllData"
@cancel="hideClearDataConfirmation"
/>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted, onUnmounted, computed } from 'vue';
import {
PREDEFINED_MODELS,
type ModelPreset,
getModelInfo,
getCacheStats,
clearModelCache,
cleanupModelCache,
} from '@/utils/semantic-similarity-engine';
import { BACKGROUND_MESSAGE_TYPES } from '@/common/message-types';
import { getMessage } from '@/utils/i18n';
import ConfirmDialog from './components/ConfirmDialog.vue';
import ProgressIndicator from './components/ProgressIndicator.vue';
import ModelCacheManagement from './components/ModelCacheManagement.vue';
import {
DocumentIcon,
DatabaseIcon,
BoltIcon,
TrashIcon,
CheckIcon,
TabIcon,
VectorIcon,
} from './components/icons';
const nativeConnectionStatus = ref<'unknown' | 'connected' | 'disconnected'>('unknown');
const isConnecting = ref(false);
const nativeServerPort = ref<number>(12306);
const serverStatus = ref<{
isRunning: boolean;
port?: number;
lastUpdated: number;
}>({
isRunning: false,
lastUpdated: Date.now(),
});
const showMcpConfig = computed(() => {
return nativeConnectionStatus.value === 'connected' && serverStatus.value.isRunning;
});
const copyButtonText = ref(getMessage('copyConfigButton'));
const mcpConfigJson = computed(() => {
const port = serverStatus.value.port || nativeServerPort.value;
const config = {
mcpServers: {
'streamable-mcp-server': {
type: 'streamable-http',
url: `http://127.0.0.1:${port}/mcp`,
},
},
};
return JSON.stringify(config, null, 2);
});
const currentModel = ref<ModelPreset | null>(null);
const isModelSwitching = ref(false);
const modelSwitchProgress = ref('');
const modelDownloadProgress = ref<number>(0);
const isModelDownloading = ref(false);
const modelInitializationStatus = ref<'idle' | 'downloading' | 'initializing' | 'ready' | 'error'>(
'idle',
);
const modelErrorMessage = ref<string>('');
const modelErrorType = ref<'network' | 'file' | 'unknown' | ''>('');
const selectedVersion = ref<'quantized'>('quantized');
const storageStats = ref<{
indexedPages: number;
totalDocuments: number;
totalTabs: number;
indexSize: number;
isInitialized: boolean;
} | null>(null);
const isRefreshingStats = ref(false);
const isClearingData = ref(false);
const showClearConfirmation = ref(false);
const clearDataProgress = ref('');
const semanticEngineStatus = ref<'idle' | 'initializing' | 'ready' | 'error'>('idle');
const isSemanticEngineInitializing = ref(false);
const semanticEngineInitProgress = ref('');
const semanticEngineLastUpdated = ref<number | null>(null);
// Cache management
const isManagingCache = ref(false);
const cacheStats = ref<{
totalSize: number;
totalSizeMB: number;
entryCount: number;
entries: Array<{
url: string;
size: number;
sizeMB: number;
timestamp: number;
age: string;
expired: boolean;
}>;
} | null>(null);
const availableModels = computed(() => {
return Object.entries(PREDEFINED_MODELS).map(([key, value]) => ({
preset: key as ModelPreset,
...value,
}));
});
const getStatusClass = () => {
if (nativeConnectionStatus.value === 'connected') {
if (serverStatus.value.isRunning) {
return 'bg-emerald-500';
} else {
return 'bg-yellow-500';
}
} else if (nativeConnectionStatus.value === 'disconnected') {
return 'bg-red-500';
} else {
return 'bg-gray-500';
}
};
const getStatusText = () => {
if (nativeConnectionStatus.value === 'connected') {
if (serverStatus.value.isRunning) {
return getMessage('serviceRunningStatus', [(serverStatus.value.port || 'Unknown').toString()]);
} else {
return getMessage('connectedServiceNotStartedStatus');
}
} else if (nativeConnectionStatus.value === 'disconnected') {
return getMessage('serviceNotConnectedStatus');
} else {
return getMessage('detectingStatus');
}
};
const formatIndexSize = () => {
if (!storageStats.value?.indexSize) return '0 MB';
const sizeInMB = Math.round(storageStats.value.indexSize / (1024 * 1024));
return `${sizeInMB} MB`;
};
const getModelDescription = (model: any) => {
switch (model.preset) {
case 'multilingual-e5-small':
return getMessage('lightweightModelDescription');
case 'multilingual-e5-base':
return getMessage('betterThanSmallDescription');
default:
return getMessage('multilingualModelDescription');
}
};
const getPerformanceText = (performance: string) => {
switch (performance) {
case 'fast':
return getMessage('fastPerformance');
case 'balanced':
return getMessage('balancedPerformance');
case 'accurate':
return getMessage('accuratePerformance');
default:
return performance;
}
};
const getSemanticEngineStatusText = () => {
switch (semanticEngineStatus.value) {
case 'ready':
return getMessage('semanticEngineReadyStatus');
case 'initializing':
return getMessage('semanticEngineInitializingStatus');
case 'error':
return getMessage('semanticEngineInitFailedStatus');
case 'idle':
default:
return getMessage('semanticEngineNotInitStatus');
}
};
const getSemanticEngineStatusClass = () => {
switch (semanticEngineStatus.value) {
case 'ready':
return 'bg-emerald-500';
case 'initializing':
return 'bg-yellow-500';
case 'error':
return 'bg-red-500';
case 'idle':
default:
return 'bg-gray-500';
}
};
const getActiveTabsCount = () => {
return storageStats.value?.totalTabs || 0;
};
const getProgressText = () => {
if (isModelDownloading.value) {
return getMessage('downloadingModelStatus', [modelDownloadProgress.value.toString()]);
} else if (isModelSwitching.value) {
return modelSwitchProgress.value || getMessage('switchingModelStatus');
}
return '';
};
const getErrorTypeText = () => {
switch (modelErrorType.value) {
case 'network':
return getMessage('networkErrorMessage');
case 'file':
return getMessage('modelCorruptedErrorMessage');
case 'unknown':
default:
return getMessage('unknownErrorMessage');
}
};
const getSemanticEngineButtonText = () => {
switch (semanticEngineStatus.value) {
case 'ready':
return getMessage('reinitializeButton');
case 'initializing':
return getMessage('initializingStatus');
case 'error':
return getMessage('reinitializeButton');
case 'idle':
default:
return getMessage('initSemanticEngineButton');
}
};
const loadCacheStats = async () => {
try {
cacheStats.value = await getCacheStats();
} catch (error) {
console.error('Failed to get cache stats:', error);
cacheStats.value = null;
}
};
const cleanupCache = async () => {
if (isManagingCache.value) return;
isManagingCache.value = true;
try {
await cleanupModelCache();
// Refresh cache stats
await loadCacheStats();
} catch (error) {
console.error('Failed to cleanup cache:', error);
} finally {
isManagingCache.value = false;
}
};
const clearAllCache = async () => {
if (isManagingCache.value) return;
isManagingCache.value = true;
try {
await clearModelCache();
// Refresh cache stats
await loadCacheStats();
} catch (error) {
console.error('Failed to clear cache:', error);
} finally {
isManagingCache.value = false;
}
};
const saveSemanticEngineState = async () => {
try {
const semanticEngineState = {
status: semanticEngineStatus.value,
lastUpdated: semanticEngineLastUpdated.value,
};
// eslint-disable-next-line no-undef
await chrome.storage.local.set({ semanticEngineState });
} catch (error) {
console.error('保存语义引擎状态失败:', error);
}
};
const initializeSemanticEngine = async () => {
if (isSemanticEngineInitializing.value) return;
const isReinitialization = semanticEngineStatus.value === 'ready';
console.log(
`🚀 User triggered semantic engine ${isReinitialization ? 'reinitialization' : 'initialization'}`,
);
isSemanticEngineInitializing.value = true;
semanticEngineStatus.value = 'initializing';
semanticEngineInitProgress.value = isReinitialization
? getMessage('semanticEngineInitializingStatus')
: getMessage('semanticEngineInitializingStatus');
semanticEngineLastUpdated.value = Date.now();
await saveSemanticEngineState();
try {
// eslint-disable-next-line no-undef
chrome.runtime
.sendMessage({
type: BACKGROUND_MESSAGE_TYPES.INITIALIZE_SEMANTIC_ENGINE,
})
.catch((error) => {
console.error('❌ Error sending semantic engine initialization request:', error);
});
startSemanticEngineStatusPolling();
semanticEngineInitProgress.value = isReinitialization
? getMessage('processingStatus')
: getMessage('processingStatus');
} catch (error: any) {
console.error('❌ Failed to send initialization request:', error);
semanticEngineStatus.value = 'error';
semanticEngineInitProgress.value = `Failed to send initialization request: ${error?.message || 'Unknown error'}`;
await saveSemanticEngineState();
setTimeout(() => {
semanticEngineInitProgress.value = '';
}, 5000);
isSemanticEngineInitializing.value = false;
semanticEngineLastUpdated.value = Date.now();
await saveSemanticEngineState();
}
};
const checkSemanticEngineStatus = async () => {
try {
// eslint-disable-next-line no-undef
const response = await chrome.runtime.sendMessage({
type: BACKGROUND_MESSAGE_TYPES.GET_MODEL_STATUS,
});
if (response && response.success && response.status) {
const status = response.status;
if (status.initializationStatus === 'ready') {
semanticEngineStatus.value = 'ready';
semanticEngineLastUpdated.value = Date.now();
isSemanticEngineInitializing.value = false;
semanticEngineInitProgress.value = getMessage('semanticEngineReadyStatus');
await saveSemanticEngineState();
stopSemanticEngineStatusPolling();
setTimeout(() => {
semanticEngineInitProgress.value = '';
}, 2000);
} else if (
status.initializationStatus === 'downloading' ||
status.initializationStatus === 'initializing'
) {
semanticEngineStatus.value = 'initializing';
isSemanticEngineInitializing.value = true;
semanticEngineInitProgress.value = getMessage('semanticEngineInitializingStatus');
semanticEngineLastUpdated.value = Date.now();
await saveSemanticEngineState();
} else if (status.initializationStatus === 'error') {
semanticEngineStatus.value = 'error';
semanticEngineLastUpdated.value = Date.now();
isSemanticEngineInitializing.value = false;
semanticEngineInitProgress.value = getMessage('semanticEngineInitFailedStatus');
await saveSemanticEngineState();
stopSemanticEngineStatusPolling();
setTimeout(() => {
semanticEngineInitProgress.value = '';
}, 5000);
} else {
semanticEngineStatus.value = 'idle';
isSemanticEngineInitializing.value = false;
await saveSemanticEngineState();
}
} else {
semanticEngineStatus.value = 'idle';
isSemanticEngineInitializing.value = false;
await saveSemanticEngineState();
}
} catch (error) {
console.error('Popup: Failed to check semantic engine status:', error);
semanticEngineStatus.value = 'idle';
isSemanticEngineInitializing.value = false;
await saveSemanticEngineState();
}
};
const retryModelInitialization = async () => {
if (!currentModel.value) return;
console.log('🔄 Retrying model initialization...');
modelErrorMessage.value = '';
modelErrorType.value = '';
modelInitializationStatus.value = 'downloading';
modelDownloadProgress.value = 0;
isModelDownloading.value = true;
await switchModel(currentModel.value);
};
const updatePort = async (event: Event) => {
const target = event.target as HTMLInputElement;
const newPort = Number(target.value);
nativeServerPort.value = newPort;
await savePortPreference(newPort);
};
const checkNativeConnection = async () => {
try {
// eslint-disable-next-line no-undef
const response = await chrome.runtime.sendMessage({ type: 'ping_native' });
nativeConnectionStatus.value = response?.connected ? 'connected' : 'disconnected';
} catch (error) {
console.error('检测 Native 连接状态失败:', error);
nativeConnectionStatus.value = 'disconnected';
}
};
const checkServerStatus = async () => {
try {
// eslint-disable-next-line no-undef
const response = await chrome.runtime.sendMessage({
type: BACKGROUND_MESSAGE_TYPES.GET_SERVER_STATUS,
});
if (response?.success && response.serverStatus) {
serverStatus.value = response.serverStatus;
}
if (response?.connected !== undefined) {
nativeConnectionStatus.value = response.connected ? 'connected' : 'disconnected';
}
} catch (error) {
console.error('检测服务器状态失败:', error);
}
};
const refreshServerStatus = async () => {
try {
// eslint-disable-next-line no-undef
const response = await chrome.runtime.sendMessage({
type: BACKGROUND_MESSAGE_TYPES.REFRESH_SERVER_STATUS,
});
if (response?.success && response.serverStatus) {
serverStatus.value = response.serverStatus;
}
if (response?.connected !== undefined) {
nativeConnectionStatus.value = response.connected ? 'connected' : 'disconnected';
}
} catch (error) {
console.error('刷新服务器状态失败:', error);
}
};
const copyMcpConfig = async () => {
try {
await navigator.clipboard.writeText(mcpConfigJson.value);
copyButtonText.value = '✅' + getMessage('configCopiedNotification');
setTimeout(() => {
copyButtonText.value = getMessage('copyConfigButton');
}, 2000);
} catch (error) {
console.error('复制配置失败:', error);
copyButtonText.value = '❌' + getMessage('networkErrorMessage');
setTimeout(() => {
copyButtonText.value = getMessage('copyConfigButton');
}, 2000);
}
};
const testNativeConnection = async () => {
if (isConnecting.value) return;
isConnecting.value = true;
try {
if (nativeConnectionStatus.value === 'connected') {
// eslint-disable-next-line no-undef
await chrome.runtime.sendMessage({ type: 'disconnect_native' });
nativeConnectionStatus.value = 'disconnected';
} else {
console.log(`尝试连接到端口: ${nativeServerPort.value}`);
// eslint-disable-next-line no-undef
const response = await chrome.runtime.sendMessage({
type: 'connectNative',
port: nativeServerPort.value,
});
if (response && response.success) {
nativeConnectionStatus.value = 'connected';
console.log('连接成功:', response);
await savePortPreference(nativeServerPort.value);
} else {
nativeConnectionStatus.value = 'disconnected';
console.error('连接失败:', response);
}
}
} catch (error) {
console.error('测试连接失败:', error);
nativeConnectionStatus.value = 'disconnected';
} finally {
isConnecting.value = false;
}
};
const loadModelPreference = async () => {
try {
// eslint-disable-next-line no-undef
const result = await chrome.storage.local.get([
'selectedModel',
'selectedVersion',
'modelState',
'semanticEngineState',
]);
if (result.selectedModel) {
const storedModel = result.selectedModel as string;
console.log('📋 Stored model from storage:', storedModel);
if (PREDEFINED_MODELS[storedModel as ModelPreset]) {
currentModel.value = storedModel as ModelPreset;
console.log(`✅ Loaded valid model: ${currentModel.value}`);
} else {
console.warn(
`⚠️ Stored model "${storedModel}" not found in PREDEFINED_MODELS, using default`,
);
currentModel.value = 'multilingual-e5-small';
await saveModelPreference(currentModel.value);
}
} else {
console.log('⚠️ No model found in storage, using default');
currentModel.value = 'multilingual-e5-small';
await saveModelPreference(currentModel.value);
}
selectedVersion.value = 'quantized';
console.log('✅ Using quantized version (fixed)');
await saveVersionPreference('quantized');
if (result.modelState) {
const modelState = result.modelState;
if (modelState.status === 'ready') {
modelInitializationStatus.value = 'ready';
modelDownloadProgress.value = modelState.downloadProgress || 100;
isModelDownloading.value = false;
} else {
modelInitializationStatus.value = 'idle';
modelDownloadProgress.value = 0;
isModelDownloading.value = false;
await saveModelState();
}
} else {
modelInitializationStatus.value = 'idle';
modelDownloadProgress.value = 0;
isModelDownloading.value = false;
}
if (result.semanticEngineState) {
const semanticState = result.semanticEngineState;
if (semanticState.status === 'ready') {
semanticEngineStatus.value = 'ready';
semanticEngineLastUpdated.value = semanticState.lastUpdated || Date.now();
} else if (semanticState.status === 'error') {
semanticEngineStatus.value = 'error';
semanticEngineLastUpdated.value = semanticState.lastUpdated || Date.now();
} else {
semanticEngineStatus.value = 'idle';
}
} else {
semanticEngineStatus.value = 'idle';
}
} catch (error) {
console.error('❌ 加载模型偏好失败:', error);
}
};
const saveModelPreference = async (model: ModelPreset) => {
try {
// eslint-disable-next-line no-undef
await chrome.storage.local.set({ selectedModel: model });
} catch (error) {
console.error('保存模型偏好失败:', error);
}
};
const saveVersionPreference = async (version: 'full' | 'quantized' | 'compressed') => {
try {
// eslint-disable-next-line no-undef
await chrome.storage.local.set({ selectedVersion: version });
} catch (error) {
console.error('保存版本偏好失败:', error);
}
};
const savePortPreference = async (port: number) => {
try {
// eslint-disable-next-line no-undef
await chrome.storage.local.set({ nativeServerPort: port });
console.log(`端口偏好已保存: ${port}`);
} catch (error) {
console.error('保存端口偏好失败:', error);
}
};
const loadPortPreference = async () => {
try {
// eslint-disable-next-line no-undef
const result = await chrome.storage.local.get(['nativeServerPort']);
if (result.nativeServerPort) {
nativeServerPort.value = result.nativeServerPort;
console.log(`端口偏好已加载: ${result.nativeServerPort}`);
}
} catch (error) {
console.error('加载端口偏好失败:', error);
}
};
const saveModelState = async () => {
try {
const modelState = {
status: modelInitializationStatus.value,
downloadProgress: modelDownloadProgress.value,
isDownloading: isModelDownloading.value,
lastUpdated: Date.now(),
};
// eslint-disable-next-line no-undef
await chrome.storage.local.set({ modelState });
} catch (error) {
console.error('保存模型状态失败:', error);
}
};
let statusMonitoringInterval: ReturnType<typeof setInterval> | null = null;
let semanticEngineStatusPollingInterval: ReturnType<typeof setInterval> | null = null;
const startModelStatusMonitoring = () => {
if (statusMonitoringInterval) {
clearInterval(statusMonitoringInterval);
}
statusMonitoringInterval = setInterval(async () => {
try {
// eslint-disable-next-line no-undef
const response = await chrome.runtime.sendMessage({
type: 'get_model_status',
});
if (response && response.success) {
const status = response.status;
modelInitializationStatus.value = status.initializationStatus || 'idle';
modelDownloadProgress.value = status.downloadProgress || 0;
isModelDownloading.value = status.isDownloading || false;
if (status.initializationStatus === 'error') {
modelErrorMessage.value = status.errorMessage || getMessage('modelFailedStatus');
modelErrorType.value = status.errorType || 'unknown';
} else {
modelErrorMessage.value = '';
modelErrorType.value = '';
}
await saveModelState();
if (status.initializationStatus === 'ready' || status.initializationStatus === 'error') {
stopModelStatusMonitoring();
}
}
} catch (error) {
console.error('获取模型状态失败:', error);
}
}, 1000);
};
const stopModelStatusMonitoring = () => {
if (statusMonitoringInterval) {
clearInterval(statusMonitoringInterval);
statusMonitoringInterval = null;
}
};
const startSemanticEngineStatusPolling = () => {
if (semanticEngineStatusPollingInterval) {
clearInterval(semanticEngineStatusPollingInterval);
}
semanticEngineStatusPollingInterval = setInterval(async () => {
try {
await checkSemanticEngineStatus();
} catch (error) {
console.error('Semantic engine status polling failed:', error);
}
}, 2000);
};
const stopSemanticEngineStatusPolling = () => {
if (semanticEngineStatusPollingInterval) {
clearInterval(semanticEngineStatusPollingInterval);
semanticEngineStatusPollingInterval = null;
}
};
const refreshStorageStats = async () => {
if (isRefreshingStats.value) return;
isRefreshingStats.value = true;
try {
console.log('🔄 Refreshing storage statistics...');
// eslint-disable-next-line no-undef
const response = await chrome.runtime.sendMessage({
type: 'get_storage_stats',
});
if (response && response.success) {
storageStats.value = {
indexedPages: response.stats.indexedPages || 0,
totalDocuments: response.stats.totalDocuments || 0,
totalTabs: response.stats.totalTabs || 0,
indexSize: response.stats.indexSize || 0,
isInitialized: response.stats.isInitialized || false,
};
console.log('✅ Storage stats refreshed:', storageStats.value);
} else {
console.error('❌ Failed to get storage stats:', response?.error);
storageStats.value = {
indexedPages: 0,
totalDocuments: 0,
totalTabs: 0,
indexSize: 0,
isInitialized: false,
};
}
} catch (error) {
console.error('❌ Error refreshing storage stats:', error);
storageStats.value = {
indexedPages: 0,
totalDocuments: 0,
totalTabs: 0,
indexSize: 0,
isInitialized: false,
};
} finally {
isRefreshingStats.value = false;
}
};
const hideClearDataConfirmation = () => {
showClearConfirmation.value = false;
};
const confirmClearAllData = async () => {
if (isClearingData.value) return;
isClearingData.value = true;
clearDataProgress.value = getMessage('clearingStatus');
try {
console.log('🗑️ Starting to clear all data...');
// eslint-disable-next-line no-undef
const response = await chrome.runtime.sendMessage({
type: 'clear_all_data',
});
if (response && response.success) {
clearDataProgress.value = getMessage('dataClearedNotification');
console.log('✅ All data cleared successfully');
await refreshStorageStats();
setTimeout(() => {
clearDataProgress.value = '';
hideClearDataConfirmation();
}, 2000);
} else {
throw new Error(response?.error || 'Failed to clear data');
}
} catch (error: any) {
console.error('❌ Failed to clear all data:', error);
clearDataProgress.value = `Failed to clear data: ${error?.message || 'Unknown error'}`;
setTimeout(() => {
clearDataProgress.value = '';
}, 5000);
} finally {
isClearingData.value = false;
}
};
const switchModel = async (newModel: ModelPreset) => {
console.log(`🔄 switchModel called with newModel: ${newModel}`);
if (isModelSwitching.value) {
console.log('⏸️ Model switch already in progress, skipping');
return;
}
const isSameModel = newModel === currentModel.value;
const currentModelInfo = currentModel.value
? getModelInfo(currentModel.value)
: getModelInfo('multilingual-e5-small');
const newModelInfo = getModelInfo(newModel);
const isDifferentDimension = currentModelInfo.dimension !== newModelInfo.dimension;
console.log(`📊 Switch analysis:`);
console.log(` - Same model: ${isSameModel} (${currentModel.value} -> ${newModel})`);
console.log(
` - Current dimension: ${currentModelInfo.dimension}, New dimension: ${newModelInfo.dimension}`,
);
console.log(` - Different dimension: ${isDifferentDimension}`);
if (isSameModel && !isDifferentDimension) {
console.log('✅ Same model and dimension - no need to switch');
return;
}
const switchReasons = [];
if (!isSameModel) switchReasons.push('different model');
if (isDifferentDimension) switchReasons.push('different dimension');
console.log(`🚀 Switching model due to: ${switchReasons.join(', ')}`);
console.log(
`📋 Model: ${currentModel.value} (${currentModelInfo.dimension}D) -> ${newModel} (${newModelInfo.dimension}D)`,
);
isModelSwitching.value = true;
modelSwitchProgress.value = getMessage('switchingModelStatus');
modelInitializationStatus.value = 'downloading';
modelDownloadProgress.value = 0;
isModelDownloading.value = true;
try {
await saveModelPreference(newModel);
await saveVersionPreference('quantized');
await saveModelState();
modelSwitchProgress.value = getMessage('semanticEngineInitializingStatus');
startModelStatusMonitoring();
// eslint-disable-next-line no-undef
const response = await chrome.runtime.sendMessage({
type: 'switch_semantic_model',
modelPreset: newModel,
modelVersion: 'quantized',
modelDimension: newModelInfo.dimension,
previousDimension: currentModelInfo.dimension,
});
if (response && response.success) {
currentModel.value = newModel;
modelSwitchProgress.value = getMessage('successNotification');
console.log(
'模型切换成功:',
newModel,
'version: quantized',
'dimension:',
newModelInfo.dimension,
);
modelInitializationStatus.value = 'ready';
isModelDownloading.value = false;
await saveModelState();
setTimeout(() => {
modelSwitchProgress.value = '';
}, 2000);
} else {
throw new Error(response?.error || 'Model switch failed');
}
} catch (error: any) {
console.error('模型切换失败:', error);
modelSwitchProgress.value = `Model switch failed: ${error?.message || 'Unknown error'}`;
modelInitializationStatus.value = 'error';
isModelDownloading.value = false;
const errorMessage = error?.message || '未知错误';
if (
errorMessage.includes('network') ||
errorMessage.includes('fetch') ||
errorMessage.includes('timeout')
) {
modelErrorType.value = 'network';
modelErrorMessage.value = getMessage('networkErrorMessage');
} else if (
errorMessage.includes('corrupt') ||
errorMessage.includes('invalid') ||
errorMessage.includes('format')
) {
modelErrorType.value = 'file';
modelErrorMessage.value = getMessage('modelCorruptedErrorMessage');
} else {
modelErrorType.value = 'unknown';
modelErrorMessage.value = errorMessage;
}
await saveModelState();
setTimeout(() => {
modelSwitchProgress.value = '';
}, 8000);
} finally {
isModelSwitching.value = false;
}
};
const setupServerStatusListener = () => {
// eslint-disable-next-line no-undef
chrome.runtime.onMessage.addListener((message) => {
if (message.type === BACKGROUND_MESSAGE_TYPES.SERVER_STATUS_CHANGED && message.payload) {
serverStatus.value = message.payload;
console.log('Server status updated:', message.payload);
}
});
};
onMounted(async () => {
await loadPortPreference();
await loadModelPreference();
await checkNativeConnection();
await checkServerStatus();
await refreshStorageStats();
await loadCacheStats();
await checkSemanticEngineStatus();
setupServerStatusListener();
});
onUnmounted(() => {
stopModelStatusMonitoring();
stopSemanticEngineStatusPolling();
});
</script>
<style scoped>
.popup-container {
background: #f1f5f9;
border-radius: 24px;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
display: flex;
flex-direction: column;
overflow: hidden;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.header {
flex-shrink: 0;
padding-left: 20px;
}
.header-content {
display: flex;
justify-content: space-between;
align-items: center;
}
.header-title {
font-size: 24px;
font-weight: 700;
color: #1e293b;
margin: 0;
}
.settings-button {
padding: 8px;
border-radius: 50%;
color: #64748b;
background: none;
border: none;
cursor: pointer;
transition: all 0.2s ease;
}
.settings-button:hover {
background: #e2e8f0;
color: #1e293b;
}
.content {
flex-grow: 1;
padding: 8px 24px;
overflow-y: auto;
scrollbar-width: none;
-ms-overflow-style: none;
}
.content::-webkit-scrollbar {
display: none;
}
.status-card {
background: white;
border-radius: 16px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
padding: 20px;
margin-bottom: 20px;
}
.status-label {
font-size: 14px;
font-weight: 500;
color: #64748b;
margin-bottom: 8px;
}
.status-info {
display: flex;
align-items: center;
gap: 8px;
}
.status-dot {
height: 8px;
width: 8px;
border-radius: 50%;
}
.status-dot.bg-emerald-500 {
background-color: #10b981;
}
.status-dot.bg-red-500 {
background-color: #ef4444;
}
.status-dot.bg-yellow-500 {
background-color: #eab308;
}
.status-dot.bg-gray-500 {
background-color: #6b7280;
}
.status-text {
font-size: 16px;
font-weight: 600;
color: #1e293b;
}
.model-label {
font-size: 14px;
font-weight: 500;
color: #64748b;
margin-bottom: 4px;
}
.model-name {
font-weight: 600;
color: #7c3aed;
}
.stats-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.stats-card {
background: white;
border-radius: 12px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
padding: 16px;
}
.stats-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
}
.stats-label {
font-size: 14px;
font-weight: 500;
color: #64748b;
}
.stats-icon {
padding: 8px;
border-radius: 8px;
}
.stats-icon.violet {
background: #ede9fe;
color: #7c3aed;
}
.stats-icon.teal {
background: #ccfbf1;
color: #0d9488;
}
.stats-icon.blue {
background: #dbeafe;
color: #2563eb;
}
.stats-icon.green {
background: #dcfce7;
color: #16a34a;
}
.stats-value {
font-size: 30px;
font-weight: 700;
color: #0f172a;
margin: 0;
}
.section {
margin-bottom: 24px;
}
.secondary-button {
background: #f1f5f9;
color: #475569;
border: 1px solid #cbd5e1;
padding: 8px 16px;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
gap: 8px;
}
.secondary-button:hover:not(:disabled) {
background: #e2e8f0;
border-color: #94a3b8;
}
.secondary-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.primary-button {
background: #3b82f6;
color: white;
border: none;
padding: 8px 16px;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
.primary-button:hover {
background: #2563eb;
}
.section-title {
font-size: 16px;
font-weight: 600;
color: #374151;
margin-bottom: 12px;
}
.current-model-card {
background: linear-gradient(135deg, #faf5ff, #f3e8ff);
border: 1px solid #e9d5ff;
border-radius: 12px;
padding: 16px;
margin-bottom: 16px;
}
.current-model-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.current-model-label {
font-size: 14px;
font-weight: 500;
color: #64748b;
margin: 0;
}
.current-model-badge {
background: #8b5cf6;
color: white;
font-size: 12px;
font-weight: 600;
padding: 4px 8px;
border-radius: 6px;
}
.current-model-name {
font-size: 16px;
font-weight: 700;
color: #7c3aed;
margin: 0;
}
.model-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.model-card {
background: white;
border-radius: 12px;
padding: 16px;
cursor: pointer;
border: 1px solid #e5e7eb;
transition: all 0.2s ease;
}
.model-card:hover {
border-color: #8b5cf6;
}
.model-card.selected {
border: 2px solid #8b5cf6;
background: #faf5ff;
}
.model-card.disabled {
opacity: 0.5;
cursor: not-allowed;
pointer-events: none;
}
.model-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.model-info {
flex: 1;
}
.model-name {
font-weight: 600;
color: #1e293b;
margin: 0 0 4px 0;
}
.model-name.selected-text {
color: #7c3aed;
}
.model-description {
font-size: 14px;
color: #64748b;
margin: 0;
}
.check-icon {
width: 20px;
height: 20px;
flex-shrink: 0;
background: #8b5cf6;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.model-tags {
display: flex;
align-items: center;
gap: 8px;
margin-top: 16px;
}
.model-tag {
display: inline-flex;
align-items: center;
border-radius: 9999px;
padding: 4px 10px;
font-size: 12px;
font-weight: 500;
}
.model-tag.performance {
background: #d1fae5;
color: #065f46;
}
.model-tag.size {
background: #ddd6fe;
color: #5b21b6;
}
.model-tag.dimension {
background: #e5e7eb;
color: #4b5563;
}
.config-card {
background: white;
border-radius: 16px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
padding: 20px;
display: flex;
flex-direction: column;
gap: 16px;
}
.semantic-engine-card {
background: white;
border-radius: 16px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
padding: 20px;
display: flex;
flex-direction: column;
gap: 16px;
}
.semantic-engine-status {
display: flex;
flex-direction: column;
gap: 8px;
}
.semantic-engine-button {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
background: #8b5cf6;
color: white;
font-weight: 600;
padding: 12px 16px;
border-radius: 8px;
border: none;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
}
.semantic-engine-button:hover:not(:disabled) {
background: #7c3aed;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
.semantic-engine-button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.status-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.refresh-status-button {
background: none;
border: none;
cursor: pointer;
padding: 4px 8px;
border-radius: 6px;
font-size: 14px;
color: #64748b;
transition: all 0.2s ease;
}
.refresh-status-button:hover {
background: #f1f5f9;
color: #374151;
}
.status-timestamp {
font-size: 12px;
color: #9ca3af;
margin-top: 4px;
}
.mcp-config-section {
border-top: 1px solid #f1f5f9;
}
.mcp-config-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.mcp-config-label {
font-size: 14px;
font-weight: 500;
color: #64748b;
margin: 0;
}
.copy-config-button {
background: none;
border: none;
cursor: pointer;
padding: 4px 8px;
border-radius: 6px;
font-size: 14px;
color: #64748b;
transition: all 0.2s ease;
display: flex;
align-items: center;
gap: 4px;
}
.copy-config-button:hover {
background: #f1f5f9;
color: #374151;
}
.mcp-config-content {
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 12px;
overflow-x: auto;
}
.mcp-config-json {
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 12px;
line-height: 1.4;
color: #374151;
margin: 0;
white-space: pre;
overflow-x: auto;
}
.port-section {
display: flex;
flex-direction: column;
gap: 8px;
}
.port-label {
font-size: 14px;
font-weight: 500;
color: #64748b;
}
.port-input {
display: block;
width: 100%;
border-radius: 8px;
border: 1px solid #d1d5db;
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
padding: 12px;
font-size: 14px;
background: #f8fafc;
}
.port-input:focus {
outline: none;
border-color: #8b5cf6;
box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.1);
}
.connect-button {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
background: #8b5cf6;
color: white;
font-weight: 600;
padding: 12px 16px;
border-radius: 8px;
border: none;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
}
.connect-button:hover:not(:disabled) {
background: #7c3aed;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
.connect-button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.error-card {
background: #fef2f2;
border: 1px solid #fecaca;
border-radius: 12px;
padding: 16px;
margin-bottom: 16px;
display: flex;
align-items: flex-start;
gap: 16px;
}
.error-content {
flex: 1;
display: flex;
align-items: flex-start;
gap: 12px;
}
.error-icon {
font-size: 20px;
flex-shrink: 0;
margin-top: 2px;
}
.error-details {
flex: 1;
}
.error-title {
font-size: 14px;
font-weight: 600;
color: #dc2626;
margin: 0 0 4px 0;
}
.error-message {
font-size: 14px;
color: #991b1b;
margin: 0 0 8px 0;
font-weight: 500;
}
.error-suggestion {
font-size: 13px;
color: #7f1d1d;
margin: 0;
line-height: 1.4;
}
.retry-button {
display: flex;
align-items: center;
gap: 6px;
background: #dc2626;
color: white;
font-weight: 600;
padding: 8px 16px;
border-radius: 8px;
border: none;
cursor: pointer;
transition: all 0.2s ease;
font-size: 14px;
flex-shrink: 0;
}
.retry-button:hover:not(:disabled) {
background: #b91c1c;
}
.retry-button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.danger-button {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
background: white;
border: 1px solid #d1d5db;
color: #374151;
font-weight: 600;
padding: 12px 16px;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
margin-top: 16px;
}
.danger-button:hover:not(:disabled) {
border-color: #ef4444;
color: #dc2626;
}
.danger-button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.icon-small {
width: 14px;
height: 14px;
}
.icon-default {
width: 20px;
height: 20px;
}
.icon-medium {
width: 24px;
height: 24px;
}
.footer {
padding: 16px;
margin-top: auto;
}
.footer-text {
text-align: center;
font-size: 12px;
color: #94a3b8;
margin: 0;
}
@media (max-width: 320px) {
.popup-container {
width: 100%;
height: 100vh;
border-radius: 0;
}
.header {
padding: 24px 20px 12px;
}
.content {
padding: 8px 20px;
}
.stats-grid {
grid-template-columns: 1fr;
gap: 8px;
}
.config-card {
padding: 16px;
gap: 12px;
}
.current-model-card {
padding: 12px;
margin-bottom: 12px;
}
.stats-card {
padding: 12px;
}
.stats-value {
font-size: 24px;
}
}
</style>