Files
broswer-automation/app/chrome-extension/entrypoints/popup/App.vue
nasir@endelospay.com f4ca680532 feat: add PM2 configuration and deployment setup for remote server
- Add Makefile with convenient PM2 commands for development and production
- Add ecosystem.config.js with production/staging/dev environment configurations
- Add comprehensive PM2-GUIDE.md documentation for deployment
- Update package.json with PM2-specific npm scripts
- Fix Chrome extension URL handling to omit standard ports (443/80)
- Hide embedding model and semantic engine sections in popup UI
2025-08-22 00:21:10 +05:00

3112 lines
86 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">
<!-- Remote Server Status Section -->
<div class="section">
<h2 class="section-title">{{ getMessage('remoteServerConfigLabel') }}</h2>
<div class="config-card">
<div class="status-section">
<div class="status-header">
<p class="status-label">{{ getMessage('remoteServerStatusLabel') }}</p>
<button
class="refresh-status-button"
@click="refreshRemoteServerStatus"
:title="getMessage('refreshStatusButton')"
>
🔄
</button>
</div>
<div class="status-info">
<span :class="['status-dot', getRemoteServerStatusClass()]"></span>
<span class="status-text">{{ getRemoteServerStatusText() }}</span>
</div>
<div v-if="remoteServerStatus.lastUpdated" class="status-timestamp">
{{ getMessage('lastUpdatedLabel') }}
{{ new Date(remoteServerStatus.lastUpdated).toLocaleTimeString() }}
</div>
</div>
<div
v-if="showRemoteMcpConfig && remoteServerStatus.connected"
class="mcp-config-section"
style="display: none"
>
<!-- Streamable HTTP Configuration (Recommended) -->
<div class="config-option recommended">
<div class="mcp-config-header">
<div class="config-title-group">
<p class="mcp-config-label"
>{{ getMessage('remoteMcpServerConfigLabel') }} - Streamable HTTP (Direct
Connection)</p
>
<span class="recommended-badge">{{ getMessage('recommendedLabel') }}</span>
</div>
<div class="config-note">
<small
> Chrome extension connects directly to remote server (bypasses native
server)</small
>
</div>
<button class="copy-config-button" @click="copyRemoteStreamableConfig">
{{ copyRemoteStreamableButtonText }}
</button>
</div>
<div class="mcp-config-content">
<pre class="mcp-config-json">{{ remoteStreamableConfigJson }}</pre>
</div>
</div>
<!-- WebSocket Configuration (Alternative) -->
<div class="config-option alternative" style="display: none">
<div class="mcp-config-header">
<div class="config-title-group">
<p class="mcp-config-label"
>{{ getMessage('remoteMcpServerConfigLabel') }} - WebSocket</p
>
<span class="alternative-badge">{{ getMessage('alternativeLabel') }}</span>
</div>
<button class="copy-config-button" @click="copyRemoteWebSocketConfig">
{{ copyRemoteWebSocketButtonText }}
</button>
</div>
<div class="mcp-config-content">
<pre class="mcp-config-json">{{ remoteWebSocketConfigJson }}</pre>
</div>
</div>
</div>
<div class="remote-server-info" v-if="remoteServerStatus.connected" style="display: none">
<div class="server-endpoint">
<label class="endpoint-label">{{ getMessage('serverEndpointLabel') }}</label>
<span class="endpoint-value">{{ remoteServerConfig.serverUrl }}</span>
</div>
<div class="connection-stats">
<span class="stat-item">
<span class="stat-label">{{ getMessage('reconnectAttemptsLabel') }}:</span>
<span class="stat-value">{{ remoteServerStatus.reconnectAttempts || 0 }}</span>
</span>
<span class="stat-item">
<span class="stat-label">{{ getMessage('connectionTimeLabel') }}:</span>
<span class="stat-value">{{ getConnectionDuration() }}</span>
</span>
<span class="stat-item persistent-indicator">
<span class="stat-label">🔗 Persistent:</span>
<span class="stat-value persistent-badge">Active</span>
</span>
</div>
</div>
<!-- Connection Status Display -->
<div class="connection-status-display">
<div class="status-indicator">
<div :class="['status-icon', getRemoteServerStatusClass()]">
<div
v-if="isRemoteConnecting || remoteServerStatus.connecting"
class="loading-spinner"
></div>
<span v-else-if="remoteServerStatus.connected" class="status-symbol"></span>
<span v-else-if="remoteServerStatus.error" class="status-symbol"></span>
<span v-else class="status-symbol"></span>
</div>
<div class="status-details">
<div class="status-text-primary">{{ getRemoteServerStatusText() }}</div>
<div v-if="remoteServerStatus.error" class="status-error">
{{ remoteServerStatus.error }}
<div class="error-actions">
<button
class="retry-button"
@click="retryConnection"
:disabled="isRemoteConnecting || remoteServerStatus.connecting"
>
🔄 Retry
</button>
<button class="help-button" @click="showConnectionHelp"> Help </button>
</div>
</div>
<div
v-if="remoteServerStatus.connected && remoteServerStatus.connectionTime"
class="status-info"
>
Connected {{ formatConnectionTime(remoteServerStatus.connectionTime) }}
</div>
<div v-if="remoteServerStatus.connected" class="persistent-info">
🔗 Persistent connection - No timeout, stays connected indefinitely
</div>
<div v-if="remoteServerStatus.reconnectAttempts > 0" class="status-info">
Reconnect attempts: {{ remoteServerStatus.reconnectAttempts }}
</div>
<div v-if="currentUserId" class="user-id-info">
<span class="user-id-label">👤 User ID:</span>
<span class="user-id-value" :title="currentUserId">{{
formatUserId(currentUserId)
}}</span>
<button
class="copy-user-id-button"
@click="copyUserId"
:title="getMessage('copyUserIdButton')"
>
📋
</button>
</div>
<div v-if="showHelp" class="connection-help">
<div class="help-content">
<h4>Connection Troubleshooting:</h4>
<ul>
<li
>Ensure the remote server is running on
{{ remoteServerConfig.serverUrl }}</li
>
<li>Check if the server port is accessible and not blocked by firewall</li>
<li>Verify the server URL format (should start with ws:// or wss://)</li>
<li>Try refreshing the page and reconnecting</li>
</ul>
<button class="close-help-button" @click="showHelp = false">Close</button>
</div>
</div>
</div>
</div>
</div>
<!-- Connection Settings -->
<div class="connection-settings" style="display: none">
<div class="settings-header">
<span class="settings-title">Connection Settings</span>
<button
class="settings-toggle"
@click="showAdvancedSettings = !showAdvancedSettings"
:title="showAdvancedSettings ? 'Hide advanced settings' : 'Show advanced settings'"
>
{{ showAdvancedSettings ? '▼' : '▶' }}
</button>
</div>
<label class="setting-item">
<input type="checkbox" v-model="shouldAutoReconnect" class="setting-checkbox" />
<span class="setting-label"
>Auto-reconnect if connection is lost (only after manual connection)</span
>
</label>
<div v-if="showAdvancedSettings" class="advanced-settings">
<div class="setting-group">
<label class="setting-label-block">Server URL:</label>
<input
type="text"
v-model="remoteServerConfig.serverUrl"
class="setting-input"
:placeholder="DEFAULT_SERVER_URL"
@blur="saveConnectionSettings"
/>
</div>
<div class="setting-group">
<label class="setting-label-block">Reconnect Interval (ms):</label>
<input
type="number"
v-model.number="remoteServerConfig.reconnectInterval"
class="setting-input"
min="1000"
max="60000"
step="1000"
@blur="saveConnectionSettings"
/>
</div>
<div class="setting-group">
<label class="setting-label-block">Max Reconnect Attempts:</label>
<input
type="number"
v-model.number="remoteServerConfig.maxReconnectAttempts"
class="setting-input"
min="1"
max="50"
@blur="saveConnectionSettings"
/>
</div>
<div class="setting-actions">
<button class="reset-button" @click="resetConnectionSettings">
Reset to Defaults
</button>
</div>
</div>
</div>
<!-- Connection Control Buttons -->
<div class="connection-controls">
<button
class="connect-button"
:class="{
'connect-button--connected': remoteServerStatus.connected,
'connect-button--connecting': isRemoteConnecting || remoteServerStatus.connecting,
'connect-button--error': remoteServerStatus.error && !remoteServerStatus.connected,
}"
:disabled="isRemoteConnecting || remoteServerStatus.connecting"
@click="toggleRemoteConnection"
>
<BoltIcon />
<span>{{
isRemoteConnecting || remoteServerStatus.connecting
? getMessage('connectingStatus')
: remoteServerStatus.connected
? getMessage('disconnectButton')
: getMessage('connectButton')
}}</span>
</button>
<button
v-if="
!remoteServerStatus.connected &&
!isRemoteConnecting &&
!remoteServerStatus.connecting
"
class="restore-button"
@click="restorePreviousConnection"
:disabled="isRestoringConnection"
title="Restore previous connection if available"
>
<span v-if="isRestoringConnection">🔄</span>
<span v-else>🔗</span>
<span>{{ isRestoringConnection ? 'Restoring...' : 'Restore Previous' }}</span>
</button>
</div>
</div>
</div>
<!-- Browser Settings Section -->
<div class="section">
<h2 class="section-title">Browser Settings</h2>
<div class="config-card">
<div class="browser-settings">
<div class="settings-header">
<span class="settings-title">URL Opening Behavior</span>
<button
class="settings-toggle"
@click="showBrowserSettings = !showBrowserSettings"
:title="showBrowserSettings ? 'Hide browser settings' : 'Show browser settings'"
>
{{ showBrowserSettings ? '▼' : '▶' }}
</button>
</div>
<label class="setting-item">
<input
type="checkbox"
v-model="openUrlsInBackground"
class="setting-checkbox"
@change="saveBrowserSettings"
/>
<span class="setting-label">Open URLs in background pages (recommended)</span>
<span class="setting-description"
>URLs open in 1280x720 minimized windows for better automation</span
>
</label>
<div v-if="showBrowserSettings" class="advanced-settings">
<div class="setting-info">
<h4>Background Page Behavior:</h4>
<ul>
<li>URLs open in minimized windows that don't interrupt your workflow</li>
<li>Pages load in the background and can be accessed from the taskbar</li>
<li
>Useful for automation tasks where you don't need to see the page
immediately</li
>
<li>Individual tool calls can still override this setting</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<div class="section native-server-section" style="display: none">
<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" style="display: none">
<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" style="display: none">
<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 {
DEFAULT_CONNECTION_CONFIG,
DEFAULT_SERVER_URL,
REMOTE_SERVER_CONFIG,
} from '@/common/env-config';
import { getMessage } from '@/utils/i18n';
import { TOOL_NAMES } from 'chrome-mcp-shared';
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(),
});
// Remote Server State
const remoteServerStatus = ref<{
connected: boolean;
connecting: boolean;
lastUpdated: number;
reconnectAttempts: number;
connectionTime?: number;
error?: string;
}>({
connected: false,
connecting: false,
lastUpdated: Date.now(),
reconnectAttempts: 0,
});
const isRemoteConnecting = ref(false);
const isRestoringConnection = ref(false);
const showHelp = ref(false);
const showAdvancedSettings = ref(false);
const showBrowserSettings = ref(false);
const shouldAutoReconnect = ref(false); // Disable auto-reconnection by default
const openUrlsInBackground = ref(true); // Default to opening URLs in background windows
const remoteServerConfig = ref({
serverUrl: DEFAULT_CONNECTION_CONFIG.serverUrl,
reconnectInterval: DEFAULT_CONNECTION_CONFIG.reconnectInterval,
maxReconnectAttempts: DEFAULT_CONNECTION_CONFIG.maxReconnectAttempts,
});
// Configuration is now always based on environment variables
const showMcpConfig = computed(() => {
return nativeConnectionStatus.value === 'connected' && serverStatus.value.isRunning;
});
const showRemoteMcpConfig = computed(() => {
return remoteServerStatus.value.connected;
});
const copyButtonText = ref(getMessage('copyConfigButton'));
const copyRemoteStreamableButtonText = ref(getMessage('copyConfigButton'));
const copyRemoteWebSocketButtonText = ref(getMessage('copyConfigButton'));
const currentUserId = ref<string | null>(null);
const copyUserIdButtonText = ref('📋');
// Generate all available capabilities dynamically from TOOL_NAMES
const getAllCapabilities = () => {
const capabilities = Object.values(TOOL_NAMES.BROWSER);
// Add legacy compatibility names
const legacyCapabilities = [
'navigate_to_url',
'get_page_content',
'click_element',
'fill_input',
'take_screenshot',
'tab_management',
];
return [...capabilities, ...legacyCapabilities];
};
const mcpConfigJson = computed(() => {
const port = serverStatus.value.port || nativeServerPort.value;
const config = {
mcpServers: {
'streamable-mcp-server': {
type: 'streamable-http',
url: `http://${REMOTE_SERVER_CONFIG.HOST}:${port}/mcp`,
},
},
};
return JSON.stringify(config, null, 2);
});
// Streamable HTTP Configuration (Recommended)
const remoteStreamableConfigJson = computed(() => {
const httpUrl = remoteServerConfig.value.serverUrl
.replace('ws://', 'http://')
.replace('/chrome', '/mcp');
const config = {
mcpServers: {
'chrome-mcp-remote-server': {
type: 'streamableHttp',
url: httpUrl,
description:
'Remote Chrome MCP Server for browser automation (Streamable HTTP) - All Tools Available',
capabilities: getAllCapabilities(),
},
},
};
return JSON.stringify(config, null, 2);
});
// WebSocket Configuration (Alternative)
const remoteWebSocketConfigJson = computed(() => {
const serverUrl = remoteServerConfig.value.serverUrl.replace('/chrome', '/mcp');
const config = {
mcpServers: {
'chrome-mcp-remote-server-ws': {
type: 'websocket',
url: serverUrl,
description:
'Remote Chrome MCP Server for browser automation (WebSocket) - All Tools Available',
capabilities: getAllCapabilities(),
},
},
};
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);
}
};
// Remote Server Methods
const getRemoteServerStatusClass = () => {
if (remoteServerStatus.value.connected) {
return 'status-connected';
} else if (remoteServerStatus.value.connecting || isRemoteConnecting.value) {
return 'status-connecting';
} else if (remoteServerStatus.value.error) {
return 'status-error';
} else {
return 'status-disconnected';
}
};
const formatConnectionTime = (connectionTime: number) => {
const now = Date.now();
const diff = now - connectionTime;
const seconds = Math.floor(diff / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
if (hours > 0) {
return `${hours}h ${minutes % 60}m ago`;
} else if (minutes > 0) {
return `${minutes}m ${seconds % 60}s ago`;
} else {
return `${seconds}s ago`;
}
};
const getRemoteServerStatusText = () => {
if (remoteServerStatus.value.connected) {
return 'Connected (Persistent - No Timeout)';
} else if (remoteServerStatus.value.connecting || isRemoteConnecting.value) {
return getMessage('remoteServerConnectingStatus');
} else if (remoteServerStatus.value.error) {
return getDetailedErrorMessage(remoteServerStatus.value.error);
} else {
return 'Disconnected - Manual connection required';
}
};
const getDetailedErrorMessage = (error: string) => {
// Provide more specific error messages based on common connection issues
if (error.includes('timeout') || error.includes('Connection timeout')) {
return 'Connection timeout - Server may be offline';
} else if (error.includes('ECONNREFUSED') || error.includes('Connection refused')) {
return 'Connection refused - Check if server is running';
} else if (error.includes('ENOTFOUND') || error.includes('getaddrinfo ENOTFOUND')) {
return 'Server not found - Check server URL';
} else if (error.includes('ECONNRESET') || error.includes('Connection reset')) {
return 'Connection lost - Server disconnected unexpectedly';
} else if (error.includes('WebSocket')) {
return 'WebSocket error - Check server compatibility';
} else if (error.includes('Already connected')) {
return 'Already connected to server';
} else if (error.includes('Max reconnection attempts')) {
return 'Connection failed after multiple attempts';
} else {
return `Connection error: ${error}`;
}
};
const getConnectionDuration = () => {
if (!remoteServerStatus.value.connectionTime) return '0s';
const duration = Math.floor((Date.now() - remoteServerStatus.value.connectionTime) / 1000);
if (duration < 60) return `${duration}s`;
const minutes = Math.floor(duration / 60);
const seconds = duration % 60;
return `${minutes}m ${seconds}s`;
};
const copyRemoteStreamableConfig = async () => {
try {
await navigator.clipboard.writeText(remoteStreamableConfigJson.value);
copyRemoteStreamableButtonText.value = '✅ ' + getMessage('configCopiedNotification');
setTimeout(() => {
copyRemoteStreamableButtonText.value = getMessage('copyConfigButton');
}, 2000);
} catch (error) {
console.error('Failed to copy remote streamable config:', error);
copyRemoteStreamableButtonText.value = getMessage('copyFailedButton');
setTimeout(() => {
copyRemoteStreamableButtonText.value = getMessage('copyConfigButton');
}, 2000);
}
};
const copyRemoteWebSocketConfig = async () => {
try {
await navigator.clipboard.writeText(remoteWebSocketConfigJson.value);
copyRemoteWebSocketButtonText.value = '✅ ' + getMessage('configCopiedNotification');
setTimeout(() => {
copyRemoteWebSocketButtonText.value = getMessage('copyConfigButton');
}, 2000);
} catch (error) {
console.error('Failed to copy remote websocket config:', error);
copyRemoteWebSocketButtonText.value = getMessage('copyFailedButton');
setTimeout(() => {
copyRemoteWebSocketButtonText.value = getMessage('copyConfigButton');
}, 2000);
}
};
const refreshRemoteServerStatus = async () => {
try {
// eslint-disable-next-line no-undef
const response = await chrome.runtime.sendMessage({
type: 'getRemoteServerStatus',
});
if (response) {
remoteServerStatus.value = {
...remoteServerStatus.value,
connected: response.connected || false,
connecting: response.connecting || false,
reconnectAttempts: response.reconnectAttempts || 0,
connectionTime: response.connectionTime,
error: response.error,
lastUpdated: Date.now(),
};
}
} catch (error) {
console.error('Failed to get remote server status:', error);
remoteServerStatus.value.error = 'Failed to check status';
remoteServerStatus.value.lastUpdated = Date.now();
}
};
const toggleRemoteConnection = async () => {
if (isRemoteConnecting.value) return;
// Clear previous errors when attempting new connection
if (!remoteServerStatus.value.connected) {
remoteServerStatus.value.error = undefined;
}
isRemoteConnecting.value = true;
remoteServerStatus.value.connecting = true;
try {
if (remoteServerStatus.value.connected) {
// Disconnect
console.log('Disconnecting from remote server...');
// eslint-disable-next-line no-undef
const response = await chrome.runtime.sendMessage({ type: 'disconnectRemoteServer' });
if (response && response.success) {
remoteServerStatus.value.connected = false;
remoteServerStatus.value.connectionTime = undefined;
remoteServerStatus.value.error = undefined;
remoteServerStatus.value.reconnectAttempts = 0;
console.log('Successfully disconnected from remote server');
} else {
throw new Error(response?.error || 'Failed to disconnect');
}
} else {
// Connect
console.log('Connecting to remote server...', remoteServerConfig.value);
// eslint-disable-next-line no-undef
const response = await chrome.runtime.sendMessage({
type: 'connectRemoteServer',
config: remoteServerConfig.value,
});
if (response && response.success) {
remoteServerStatus.value.connected = true;
remoteServerStatus.value.connectionTime = Date.now();
remoteServerStatus.value.error = undefined;
remoteServerStatus.value.reconnectAttempts = 0;
console.log('Successfully connected to remote server');
} else {
const errorMessage = response?.error || 'Connection failed';
remoteServerStatus.value.error = errorMessage;
console.error('Failed to connect to remote server:', errorMessage);
// Provide recovery suggestions based on error type
if (errorMessage.includes('timeout')) {
console.log('Recovery suggestion: Check if the server is running and accessible');
} else if (errorMessage.includes('refused')) {
console.log('Recovery suggestion: Verify server URL and port configuration');
}
throw new Error(errorMessage);
}
}
remoteServerStatus.value.lastUpdated = Date.now();
} catch (error) {
console.error('Failed to toggle remote connection:', error);
const errorMessage = error instanceof Error ? error.message : 'Unknown connection error';
remoteServerStatus.value.error = errorMessage;
remoteServerStatus.value.connected = false;
remoteServerStatus.value.connectionTime = undefined;
remoteServerStatus.value.lastUpdated = Date.now();
} finally {
isRemoteConnecting.value = false;
remoteServerStatus.value.connecting = false;
}
};
const retryConnection = async () => {
console.log('Retrying connection...');
// Clear the error and attempt to connect again
remoteServerStatus.value.error = undefined;
await toggleRemoteConnection();
};
const restorePreviousConnection = async () => {
if (isRestoringConnection.value) return;
isRestoringConnection.value = true;
console.log('Attempting to restore previous connection...');
try {
// eslint-disable-next-line no-undef
const response = await chrome.runtime.sendMessage({
type: 'restoreRemoteConnection',
});
if (response && response.success) {
console.log('Previous connection restored successfully');
await refreshRemoteServerStatus();
} else {
console.log('No previous connection to restore or restoration failed:', response?.error);
// Show a brief message to user
remoteServerStatus.value.error = response?.error || 'No previous connection found';
setTimeout(() => {
if (remoteServerStatus.value.error === 'No previous connection found') {
remoteServerStatus.value.error = undefined;
}
}, 3000);
}
} catch (error) {
console.error('Failed to restore previous connection:', error);
remoteServerStatus.value.error = 'Failed to restore connection';
setTimeout(() => {
if (remoteServerStatus.value.error === 'Failed to restore connection') {
remoteServerStatus.value.error = undefined;
}
}, 3000);
} finally {
isRestoringConnection.value = false;
}
};
const showConnectionHelp = () => {
showHelp.value = !showHelp.value;
};
const saveConnectionSettings = async () => {
try {
// Save settings to Chrome storage
// eslint-disable-next-line no-undef
await chrome.storage.local.set({
remoteServerConfig: remoteServerConfig.value,
shouldAutoReconnect: shouldAutoReconnect.value,
});
console.log('Connection settings saved:', remoteServerConfig.value);
} catch (error) {
console.error('Failed to save connection settings:', error);
}
};
const resetConnectionSettings = async () => {
// Reset to environment-based defaults
remoteServerConfig.value = {
serverUrl: DEFAULT_CONNECTION_CONFIG.serverUrl,
reconnectInterval: DEFAULT_CONNECTION_CONFIG.reconnectInterval,
maxReconnectAttempts: DEFAULT_CONNECTION_CONFIG.maxReconnectAttempts,
};
shouldAutoReconnect.value = false;
showAdvancedSettings.value = false;
await saveConnectionSettings();
console.log('Connection settings reset to environment-based defaults');
};
// User ID Methods
const getCurrentUserId = async () => {
try {
// eslint-disable-next-line no-undef
const response = await chrome.runtime.sendMessage({ type: 'getCurrentUserId' });
if (response && response.success) {
currentUserId.value = response.userId;
} else {
currentUserId.value = null;
}
} catch (error) {
console.error('Failed to get current user ID:', error);
currentUserId.value = null;
}
};
const formatUserId = (userId: string) => {
// Show first 8 and last 8 characters with ... in between for long user IDs
if (userId.length > 20) {
return `${userId.substring(0, 8)}...${userId.substring(userId.length - 8)}`;
}
return userId;
};
const copyUserId = async () => {
if (!currentUserId.value) return;
try {
await navigator.clipboard.writeText(currentUserId.value);
copyUserIdButtonText.value = '✅';
setTimeout(() => {
copyUserIdButtonText.value = '📋';
}, 2000);
} catch (error) {
console.error('Failed to copy user ID:', error);
copyUserIdButtonText.value = '❌';
setTimeout(() => {
copyUserIdButtonText.value = '📋';
}, 2000);
}
};
const loadConnectionSettings = async () => {
try {
// eslint-disable-next-line no-undef
const result = await chrome.storage.local.get(['remoteServerConfig', 'shouldAutoReconnect']);
// Always start with environment-based defaults
const envBasedConfig = {
serverUrl: DEFAULT_CONNECTION_CONFIG.serverUrl,
reconnectInterval: DEFAULT_CONNECTION_CONFIG.reconnectInterval,
maxReconnectAttempts: DEFAULT_CONNECTION_CONFIG.maxReconnectAttempts,
};
if (result.remoteServerConfig) {
// Only override non-URL settings from storage, always use env for serverUrl
remoteServerConfig.value = {
...envBasedConfig,
reconnectInterval:
result.remoteServerConfig.reconnectInterval || envBasedConfig.reconnectInterval,
maxReconnectAttempts:
result.remoteServerConfig.maxReconnectAttempts || envBasedConfig.maxReconnectAttempts,
};
} else {
remoteServerConfig.value = envBasedConfig;
}
if (result.shouldAutoReconnect !== undefined) {
shouldAutoReconnect.value = result.shouldAutoReconnect;
}
console.log('Connection settings loaded (env-based):', remoteServerConfig.value);
} catch (error) {
console.error('Failed to load connection settings:', error);
}
};
const saveBrowserSettings = async () => {
try {
// eslint-disable-next-line no-undef
await chrome.storage.local.set({
openUrlsInBackground: openUrlsInBackground.value,
});
console.log('Browser settings saved:', { openUrlsInBackground: openUrlsInBackground.value });
} catch (error) {
console.error('Failed to save browser settings:', error);
}
};
const loadBrowserSettings = async () => {
try {
// eslint-disable-next-line no-undef
const result = await chrome.storage.local.get(['openUrlsInBackground']);
if (result.openUrlsInBackground !== undefined) {
openUrlsInBackground.value = result.openUrlsInBackground;
}
console.log('Browser settings loaded:', { openUrlsInBackground: openUrlsInBackground.value });
} catch (error) {
console.error('Failed to load browser settings:', error);
}
};
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);
}
if (message.type === 'remoteServerStatusUpdate') {
const previousConnected = remoteServerStatus.value.connected;
remoteServerStatus.value = {
...remoteServerStatus.value,
...message.payload,
lastUpdated: Date.now(),
};
// Log connection state changes
if (previousConnected !== remoteServerStatus.value.connected) {
if (remoteServerStatus.value.connected) {
console.log('✅ Remote server connected successfully');
// Reset error state on successful connection
remoteServerStatus.value.error = undefined;
remoteServerStatus.value.reconnectAttempts = 0;
// Refresh user ID when connected
getCurrentUserId();
} else {
console.log('❌ Remote server disconnected');
// Clear user ID when disconnected
currentUserId.value = null;
}
}
console.log('Remote server status updated:', message.payload);
}
});
};
let remoteServerStatusInterval: ReturnType<typeof setInterval> | null = null;
const startRemoteServerStatusMonitoring = () => {
if (remoteServerStatusInterval) {
clearInterval(remoteServerStatusInterval);
}
// Check remote server status every 5 seconds for status updates only
remoteServerStatusInterval = setInterval(async () => {
await refreshRemoteServerStatus();
// Only attempt automatic reconnection if explicitly enabled by user
// and only if the connection was manually established before
if (
shouldAutoReconnect.value &&
!remoteServerStatus.value.connected &&
!remoteServerStatus.value.connecting &&
!isRemoteConnecting.value &&
remoteServerStatus.value.connectionTime && // Only if previously connected
remoteServerStatus.value.reconnectAttempts < remoteServerConfig.value.maxReconnectAttempts
) {
console.log('Attempting automatic reconnection (user enabled)...');
await retryConnection();
}
}, 5000); // Increased interval to reduce resource usage
};
const stopRemoteServerStatusMonitoring = () => {
if (remoteServerStatusInterval) {
clearInterval(remoteServerStatusInterval);
remoteServerStatusInterval = null;
}
};
// Function to ensure environment variables are used for server URL
const ensureEnvironmentConfig = async () => {
try {
// Get current stored config
const result = await chrome.storage.local.get(['remoteServerConfig']);
if (
result.remoteServerConfig &&
result.remoteServerConfig.serverUrl !== DEFAULT_CONNECTION_CONFIG.serverUrl
) {
console.log('Updating stored server URL to match environment variables');
// Update stored config to use environment-based server URL
const updatedConfig = {
...result.remoteServerConfig,
serverUrl: DEFAULT_CONNECTION_CONFIG.serverUrl,
};
await chrome.storage.local.set({ remoteServerConfig: updatedConfig });
}
} catch (error) {
console.error('Failed to ensure environment config:', error);
}
};
onMounted(async () => {
await loadPortPreference();
await loadModelPreference();
await ensureEnvironmentConfig(); // Ensure environment variables are used
await loadConnectionSettings(); // Load connection settings from storage
await loadBrowserSettings(); // Load browser settings from storage
await checkNativeConnection();
await checkServerStatus();
await refreshStorageStats();
await loadCacheStats();
await checkSemanticEngineStatus();
setupServerStatusListener();
// Initialize remote server status
await refreshRemoteServerStatus();
startRemoteServerStatusMonitoring();
// Load current user ID
await getCurrentUserId();
});
onUnmounted(() => {
stopModelStatusMonitoring();
stopSemanticEngineStatusPolling();
stopRemoteServerStatusMonitoring();
});
</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;
}
/* Remote Server Specific Styles */
.remote-server-info {
margin-top: 16px;
padding: 12px;
background: #f8fafc;
border-radius: 8px;
border: 1px solid #e2e8f0;
}
/* User ID Display Styles */
.user-id-info {
display: flex;
align-items: center;
gap: 8px;
margin-top: 8px;
padding: 8px 12px;
background: #f0f9ff;
border: 1px solid #bae6fd;
border-radius: 6px;
font-size: 12px;
}
.user-id-label {
font-weight: 500;
color: #0369a1;
}
.user-id-value {
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
color: #1e40af;
background: #dbeafe;
padding: 2px 6px;
border-radius: 4px;
font-size: 11px;
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.copy-user-id-button {
background: none;
border: none;
cursor: pointer;
padding: 4px;
border-radius: 4px;
font-size: 12px;
transition: background-color 0.2s;
display: flex;
align-items: center;
justify-content: center;
min-width: 24px;
height: 24px;
}
.copy-user-id-button:hover {
background: #bae6fd;
}
.copy-user-id-button:active {
background: #93c5fd;
}
.server-endpoint {
display: flex;
flex-direction: column;
gap: 4px;
margin-bottom: 12px;
}
.endpoint-label {
font-size: 12px;
font-weight: 500;
color: #64748b;
}
.endpoint-value {
font-size: 14px;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
color: #1e293b;
background: #ffffff;
padding: 6px 8px;
border-radius: 4px;
border: 1px solid #d1d5db;
}
.connection-stats {
display: flex;
gap: 16px;
flex-wrap: wrap;
}
.stat-item {
display: flex;
gap: 4px;
font-size: 12px;
}
.stat-item.persistent-indicator {
align-items: center;
}
.stat-label {
color: #64748b;
font-weight: 500;
}
.persistent-badge {
background: linear-gradient(135deg, #10b981, #059669);
color: white;
padding: 2px 6px;
border-radius: 8px;
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
box-shadow: 0 1px 2px rgba(16, 185, 129, 0.2);
}
.persistent-info {
font-size: 11px;
color: #10b981;
font-weight: 500;
margin-top: 4px;
padding: 4px 8px;
background: rgba(16, 185, 129, 0.1);
border-radius: 6px;
border-left: 3px solid #10b981;
}
.stat-value {
color: #1e293b;
font-weight: 600;
}
.status-connected {
background-color: #10b981;
}
.status-connecting {
background-color: #eab308;
animation: pulse 2s infinite;
}
.status-disconnected {
background-color: #ef4444;
}
@keyframes pulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
.mcp-config-section {
border-top: 1px solid #f1f5f9;
}
.config-option {
margin-bottom: 16px;
border-radius: 8px;
border: 1px solid #e2e8f0;
padding: 12px;
}
.config-option.recommended {
border-color: #10b981;
background: linear-gradient(135deg, #f0fdf4 0%, #ecfdf5 100%);
}
.config-option.alternative {
border-color: #6b7280;
background: #f9fafb;
}
.mcp-config-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.config-title-group {
display: flex;
align-items: center;
gap: 8px;
}
.mcp-config-label {
font-size: 14px;
font-weight: 500;
color: #64748b;
margin: 0;
}
.recommended-badge {
background: #10b981;
color: white;
font-size: 11px;
font-weight: 600;
padding: 2px 8px;
border-radius: 12px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.alternative-badge {
background: #6b7280;
color: white;
font-size: 11px;
font-weight: 600;
padding: 2px 8px;
border-radius: 12px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.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;
}
/* Connection Controls Layout */
.connection-controls {
display: flex;
gap: 8px;
margin-top: 16px;
flex-direction: column;
}
/* Restore Button Styles */
.restore-button {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
background: #6b7280;
color: white;
font-weight: 500;
padding: 10px 16px;
border-radius: 6px;
border: none;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
font-size: 13px;
}
.restore-button:hover:not(:disabled) {
background: #4b5563;
box-shadow: 0 2px 4px -1px rgba(0, 0, 0, 0.1);
}
.restore-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;
}
}
/* Browser Settings Styles */
.browser-settings {
display: flex;
flex-direction: column;
gap: 12px;
}
.setting-description {
display: block;
font-size: 12px;
color: #64748b;
margin-top: 4px;
margin-left: 24px;
}
.setting-info {
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 12px;
}
.setting-info h4 {
margin: 0 0 8px 0;
font-size: 14px;
font-weight: 500;
color: #374151;
}
.setting-info ul {
margin: 0;
padding-left: 16px;
font-size: 12px;
color: #64748b;
line-height: 1.4;
}
.setting-info li {
margin-bottom: 4px;
}
.advanced-settings {
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid #e2e8f0;
}
.setting-group {
margin-bottom: 12px;
}
</style>