- 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
3112 lines
86 KiB
Vue
3112 lines
86 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">
|
||
<!-- 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>
|