1921 lines
48 KiB
Vue
1921 lines
48 KiB
Vue
<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>
|