hangwin--mcp-chrome
2680 行
68 KiB
Vue
2680 行
68 KiB
Vue
<template>
|
|
<div class="popup-container agent-theme" :data-agent-theme="agentTheme">
|
|
<!-- 首页 -->
|
|
<div v-show="currentView === 'home'" class="home-view">
|
|
<div class="header">
|
|
<div class="header-content">
|
|
<h1 class="header-title">Chrome MCP Server</h1>
|
|
</div>
|
|
</div>
|
|
<div class="content">
|
|
<!-- 服务配置卡片 -->
|
|
<div class="section">
|
|
<h2 class="section-title">{{ getMessage('nativeServerConfigLabel') }}</h2>
|
|
<div class="config-card">
|
|
<div class="status-section">
|
|
<div class="status-header">
|
|
<p class="status-label">{{ getMessage('runningStatusLabel') }}</p>
|
|
<button
|
|
class="refresh-status-button"
|
|
@click="refreshServerStatus"
|
|
:title="getMessage('refreshStatusButton')"
|
|
>
|
|
<RefreshIcon className="icon-small" />
|
|
</button>
|
|
</div>
|
|
<div class="status-info">
|
|
<span :class="['status-dot', getStatusClass()]"></span>
|
|
<span class="status-text">{{ getStatusText() }}</span>
|
|
</div>
|
|
<div v-if="serverStatus.lastUpdated" class="status-timestamp">
|
|
{{ getMessage('lastUpdatedLabel') }}
|
|
{{ new Date(serverStatus.lastUpdated).toLocaleTimeString() }}
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="showMcpConfig" class="mcp-config-section">
|
|
<div class="mcp-config-header">
|
|
<p class="mcp-config-label">{{ getMessage('mcpServerConfigLabel') }}</p>
|
|
<button class="copy-config-button" @click="copyMcpConfig">
|
|
{{ copyButtonText }}
|
|
</button>
|
|
</div>
|
|
<div class="mcp-config-content">
|
|
<pre class="mcp-config-json">{{ mcpConfigJson }}</pre>
|
|
</div>
|
|
</div>
|
|
<div class="port-section">
|
|
<label for="port" class="port-label">{{ getMessage('connectionPortLabel') }}</label>
|
|
<input
|
|
type="text"
|
|
id="port"
|
|
:value="nativeServerPort"
|
|
@input="updatePort"
|
|
class="port-input"
|
|
/>
|
|
</div>
|
|
|
|
<button class="connect-button" :disabled="isConnecting" @click="testNativeConnection">
|
|
<BoltIcon />
|
|
<span>{{
|
|
isConnecting
|
|
? getMessage('connectingStatus')
|
|
: nativeConnectionStatus === 'connected'
|
|
? getMessage('disconnectButton')
|
|
: getMessage('connectButton')
|
|
}}</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 快捷工具卡片 -->
|
|
<div class="section">
|
|
<h2 class="section-title">快捷工具</h2>
|
|
<div class="rr-icon-buttons">
|
|
<button
|
|
class="rr-icon-btn rr-icon-btn-record rr-icon-btn-coming-soon has-tooltip"
|
|
@click="startRecording"
|
|
data-tooltip="录制功能开发中"
|
|
>
|
|
<RecordIcon :recording="false" />
|
|
</button>
|
|
<button
|
|
class="rr-icon-btn rr-icon-btn-stop rr-icon-btn-coming-soon has-tooltip"
|
|
@click="stopRecording"
|
|
data-tooltip="录制功能开发中"
|
|
>
|
|
<StopIcon />
|
|
</button>
|
|
<button
|
|
class="rr-icon-btn rr-icon-btn-edit has-tooltip"
|
|
@click="toggleWebEditor"
|
|
data-tooltip="开启页面编辑模式"
|
|
>
|
|
<EditIcon />
|
|
</button>
|
|
<button
|
|
class="rr-icon-btn rr-icon-btn-marker has-tooltip"
|
|
@click="toggleElementMarker"
|
|
data-tooltip="开启元素标注"
|
|
>
|
|
<MarkerIcon />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 管理入口卡片 -->
|
|
<div class="section">
|
|
<h2 class="section-title">管理入口</h2>
|
|
<div class="entry-card">
|
|
<button class="entry-item" @click="openAgentSidepanel">
|
|
<div class="entry-icon agent">
|
|
<svg
|
|
viewBox="0 0 24 24"
|
|
width="20"
|
|
height="20"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
>
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"
|
|
/>
|
|
</svg>
|
|
</div>
|
|
<div class="entry-content">
|
|
<span class="entry-title">智能助手</span>
|
|
<span class="entry-desc">AI Agent 对话与任务</span>
|
|
</div>
|
|
<svg
|
|
class="entry-arrow"
|
|
viewBox="0 0 24 24"
|
|
width="16"
|
|
height="16"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
>
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7" />
|
|
</svg>
|
|
</button>
|
|
<button class="entry-item entry-item-coming-soon" @click="openWorkflowSidepanel">
|
|
<div class="entry-icon workflow">
|
|
<WorkflowIcon />
|
|
</div>
|
|
<div class="entry-content">
|
|
<span class="entry-title">
|
|
工作流管理
|
|
<span class="coming-soon-badge">Coming Soon</span>
|
|
</span>
|
|
<span class="entry-desc">录制与回放自动化流程</span>
|
|
</div>
|
|
<svg
|
|
class="entry-arrow"
|
|
viewBox="0 0 24 24"
|
|
width="16"
|
|
height="16"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
>
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7" />
|
|
</svg>
|
|
</button>
|
|
<button class="entry-item" @click="openElementMarkerSidepanel">
|
|
<div class="entry-icon marker">
|
|
<svg
|
|
viewBox="0 0 24 24"
|
|
width="20"
|
|
height="20"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
>
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"
|
|
/>
|
|
</svg>
|
|
</div>
|
|
<div class="entry-content">
|
|
<span class="entry-title">元素标注管理</span>
|
|
<span class="entry-desc">管理页面元素标注</span>
|
|
</div>
|
|
<svg
|
|
class="entry-arrow"
|
|
viewBox="0 0 24 24"
|
|
width="16"
|
|
height="16"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
>
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7" />
|
|
</svg>
|
|
</button>
|
|
<button class="entry-item" @click="currentView = 'local-model'">
|
|
<div class="entry-icon model">
|
|
<svg
|
|
viewBox="0 0 24 24"
|
|
width="20"
|
|
height="20"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
>
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
|
|
/>
|
|
</svg>
|
|
</div>
|
|
<div class="entry-content">
|
|
<span class="entry-title">本地模型</span>
|
|
<span class="entry-desc">语义引擎与模型管理</span>
|
|
</div>
|
|
<svg
|
|
class="entry-arrow"
|
|
viewBox="0 0 24 24"
|
|
width="16"
|
|
height="16"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
>
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="footer">
|
|
<div class="footer-links">
|
|
<button class="footer-link" @click="openWelcomePage" title="View installation guide">
|
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="2"
|
|
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
/>
|
|
</svg>
|
|
Guide
|
|
</button>
|
|
<button class="footer-link" @click="openTroubleshooting" title="Troubleshooting">
|
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="2"
|
|
d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"
|
|
/>
|
|
</svg>
|
|
Docs
|
|
</button>
|
|
</div>
|
|
<p class="footer-text">chrome mcp server for ai</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 本地模型二级页面 -->
|
|
<LocalModelPage
|
|
v-show="currentView === 'local-model'"
|
|
:semantic-engine-status="semanticEngineStatus"
|
|
:is-semantic-engine-initializing="isSemanticEngineInitializing"
|
|
:semantic-engine-init-progress="semanticEngineInitProgress"
|
|
:semantic-engine-last-updated="semanticEngineLastUpdated"
|
|
:available-models="availableModels"
|
|
:current-model="currentModel"
|
|
:is-model-switching="isModelSwitching"
|
|
:is-model-downloading="isModelDownloading"
|
|
:model-download-progress="modelDownloadProgress"
|
|
:model-initialization-status="modelInitializationStatus"
|
|
:model-error-message="modelErrorMessage"
|
|
:model-error-type="modelErrorType"
|
|
:storage-stats="storageStats"
|
|
:is-clearing-data="isClearingData"
|
|
:clear-data-progress="clearDataProgress"
|
|
:cache-stats="cacheStats"
|
|
:is-managing-cache="isManagingCache"
|
|
@back="currentView = 'home'"
|
|
@initialize-semantic-engine="initializeSemanticEngine"
|
|
@switch-model="switchModel"
|
|
@retry-model-initialization="retryModelInitialization"
|
|
@show-clear-confirmation="showClearConfirmation = true"
|
|
@cleanup-cache="cleanupCache"
|
|
@clear-all-cache="clearAllCache"
|
|
/>
|
|
|
|
<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"
|
|
/>
|
|
|
|
<!-- 侧边栏承担工作流管理;编辑器在独立窗口中打开 -->
|
|
|
|
<!-- Coming Soon Toast -->
|
|
<Transition name="toast">
|
|
<div v-if="comingSoonToast.show" class="coming-soon-toast">
|
|
<svg
|
|
class="toast-icon"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
>
|
|
<circle cx="12" cy="12" r="10" />
|
|
<path d="M12 6v6l4 2" stroke-linecap="round" stroke-linejoin="round" />
|
|
</svg>
|
|
<span>{{ comingSoonToast.feature }} 功能开发中,敬请期待</span>
|
|
</div>
|
|
</Transition>
|
|
</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 { LINKS } from '@/common/constants';
|
|
import { getMessage } from '@/utils/i18n';
|
|
import { useAgentTheme, type AgentThemeId } from '../sidepanel/composables/useAgentTheme';
|
|
|
|
import ConfirmDialog from './components/ConfirmDialog.vue';
|
|
import ProgressIndicator from './components/ProgressIndicator.vue';
|
|
import ModelCacheManagement from './components/ModelCacheManagement.vue';
|
|
import LocalModelPage from './components/LocalModelPage.vue';
|
|
import {
|
|
DocumentIcon,
|
|
DatabaseIcon,
|
|
BoltIcon,
|
|
TrashIcon,
|
|
CheckIcon,
|
|
TabIcon,
|
|
VectorIcon,
|
|
RecordIcon,
|
|
StopIcon,
|
|
WorkflowIcon,
|
|
RefreshIcon,
|
|
EditIcon,
|
|
MarkerIcon,
|
|
} from './components/icons';
|
|
|
|
// AgentChat theme - 从preload中获取,保持与sidepanel一致
|
|
const { theme: agentTheme, initTheme } = useAgentTheme();
|
|
|
|
// 当前视图状态:首页 or 本地模型页
|
|
const currentView = ref<'home' | 'local-model'>('home');
|
|
|
|
// Coming Soon Toast
|
|
const comingSoonToast = ref<{ show: boolean; feature: string }>({ show: false, feature: '' });
|
|
|
|
function showComingSoonToast(feature: string) {
|
|
comingSoonToast.value = { show: true, feature };
|
|
setTimeout(() => {
|
|
comingSoonToast.value = { show: false, feature: '' };
|
|
}, 2000);
|
|
}
|
|
|
|
// Record & Replay state
|
|
const rrRecording = ref(false);
|
|
const rrFlows = ref<
|
|
Array<{ id: string; name: string; description?: string; meta?: any; variables?: any[] }>
|
|
>([]);
|
|
const rrOnlyBound = ref(false);
|
|
const rrSearch = ref('');
|
|
const currentTabUrl = ref<string>('');
|
|
const filteredRrFlows = computed(() => {
|
|
const base = rrOnlyBound.value ? rrFlows.value.filter(isFlowBoundToCurrent) : rrFlows.value;
|
|
const q = rrSearch.value.trim().toLowerCase();
|
|
if (!q) return base;
|
|
return base.filter((f: any) => {
|
|
const name = String(f.name || '').toLowerCase();
|
|
const domain = String(f?.meta?.domain || '').toLowerCase();
|
|
const tags = ((f?.meta?.tags || []) as any[]).join(',').toLowerCase();
|
|
return name.includes(q) || domain.includes(q) || tags.includes(q);
|
|
});
|
|
});
|
|
|
|
// Flow editor在独立窗口中打开;在popup不再展示繁杂列表
|
|
|
|
const loadFlows = async () => {
|
|
try {
|
|
const res = await chrome.runtime.sendMessage({ type: BACKGROUND_MESSAGE_TYPES.RR_LIST_FLOWS });
|
|
if (res && res.success) rrFlows.value = res.flows || [];
|
|
} catch (e) {
|
|
/* ignore */
|
|
}
|
|
};
|
|
|
|
function isFlowBoundToCurrent(flow: any) {
|
|
try {
|
|
const bindings = flow?.meta?.bindings || [];
|
|
if (!bindings.length) return false;
|
|
if (!currentTabUrl.value) return true;
|
|
const url = new URL(currentTabUrl.value);
|
|
return bindings.some((b: any) => {
|
|
if (b.type === 'domain') return url.hostname.includes(b.value);
|
|
if (b.type === 'path') return url.pathname.startsWith(b.value);
|
|
if (b.type === 'url') return (url.href || '').startsWith(b.value);
|
|
return false;
|
|
});
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// 运行记录与覆盖项在侧边栏页面查看
|
|
const startRecording = async () => {
|
|
// TODO: 录制回放功能开发中,暂时拦截
|
|
showComingSoonToast('录制回放');
|
|
return;
|
|
// if (rrRecording.value) return;
|
|
// try {
|
|
// const res = await chrome.runtime.sendMessage({
|
|
// type: BACKGROUND_MESSAGE_TYPES.RR_START_RECORDING,
|
|
// meta: { name: '新录制' },
|
|
// });
|
|
// rrRecording.value = !!(res && res.success);
|
|
// } catch (e) {
|
|
// console.error('开始录制失败:', e);
|
|
// rrRecording.value = false;
|
|
// }
|
|
};
|
|
|
|
const stopRecording = async () => {
|
|
// TODO: 录制回放功能开发中,暂时拦截
|
|
showComingSoonToast('录制回放');
|
|
return;
|
|
// if (!rrRecording.value) return;
|
|
// try {
|
|
// const res = await chrome.runtime.sendMessage({
|
|
// type: BACKGROUND_MESSAGE_TYPES.RR_STOP_RECORDING,
|
|
// });
|
|
// rrRecording.value = false;
|
|
// if (res && res.success) await loadFlows();
|
|
// } catch (e) {
|
|
// console.error('停止录制失败:', e);
|
|
// rrRecording.value = false;
|
|
// }
|
|
};
|
|
|
|
const runFlow = async (flowId: string) => {
|
|
try {
|
|
// load flow to get runOptions
|
|
let flow: any = null;
|
|
try {
|
|
const getRes = await chrome.runtime.sendMessage({
|
|
type: BACKGROUND_MESSAGE_TYPES.RR_GET_FLOW,
|
|
flowId,
|
|
});
|
|
if (getRes && getRes.success) flow = getRes.flow;
|
|
} catch {}
|
|
const runOptions = (flow && flow.meta && flow.meta.runOptions) || {};
|
|
// No per-run overrides in popup; sidepanel/editor manage advanced options
|
|
const ov: any = {};
|
|
const res = await chrome.runtime.sendMessage({
|
|
type: BACKGROUND_MESSAGE_TYPES.RR_RUN_FLOW,
|
|
flowId,
|
|
options: { ...runOptions, ...ov, returnLogs: true },
|
|
});
|
|
if (!(res && res.success)) {
|
|
console.warn('回放失败');
|
|
return;
|
|
}
|
|
// If failed, open builder and focus the failed node
|
|
try {
|
|
const result = res.result;
|
|
if (result && result.success === false) {
|
|
const logs = result.logs || [];
|
|
const failed = logs.find((l: any) => l.status === 'failed');
|
|
if (failed && failed.stepId) {
|
|
// 打开独立编辑窗口并定位失败节点
|
|
if (flow) openBuilderWindow(flow.id, String(failed.stepId));
|
|
}
|
|
} else if (result && result.success === true) {
|
|
// If run succeeded but selector fallback was used, suggest updating priorities
|
|
const logs = result.logs || [];
|
|
const fb = logs.find((l: any) => l.fallbackUsed && l.fallbackTo);
|
|
if (fb && flow) openBuilderWindow(flow.id, String(fb.stepId || ''));
|
|
}
|
|
} catch {}
|
|
} catch (e) {
|
|
console.error('回放失败:', e);
|
|
}
|
|
};
|
|
|
|
// 旧的“克隆/发布/定时/覆盖项”在侧边栏或编辑器中处理
|
|
|
|
const nativeConnectionStatus = ref<'unknown' | 'connected' | 'disconnected'>('unknown');
|
|
const isConnecting = ref(false);
|
|
const nativeServerPort = ref<number>(12306);
|
|
|
|
const serverStatus = ref<{
|
|
isRunning: boolean;
|
|
port?: number;
|
|
lastUpdated: number;
|
|
}>({
|
|
isRunning: false,
|
|
lastUpdated: Date.now(),
|
|
});
|
|
|
|
const showMcpConfig = computed(() => {
|
|
return nativeConnectionStatus.value === 'connected' && serverStatus.value.isRunning;
|
|
});
|
|
|
|
const copyButtonText = ref(getMessage('copyConfigButton'));
|
|
|
|
const mcpConfigJson = computed(() => {
|
|
const port = serverStatus.value.port || nativeServerPort.value;
|
|
const config = {
|
|
mcpServers: {
|
|
'streamable-mcp-server': {
|
|
type: 'streamable-http',
|
|
url: `http://127.0.0.1:${port}/mcp`,
|
|
},
|
|
},
|
|
};
|
|
return JSON.stringify(config, null, 2);
|
|
});
|
|
|
|
const currentModel = ref<ModelPreset | null>(null);
|
|
const isModelSwitching = ref(false);
|
|
const modelSwitchProgress = ref('');
|
|
|
|
const modelDownloadProgress = ref<number>(0);
|
|
const isModelDownloading = ref(false);
|
|
const modelInitializationStatus = ref<'idle' | 'downloading' | 'initializing' | 'ready' | 'error'>(
|
|
'idle',
|
|
);
|
|
const modelErrorMessage = ref<string>('');
|
|
const modelErrorType = ref<'network' | 'file' | 'unknown' | ''>('');
|
|
|
|
const selectedVersion = ref<'quantized'>('quantized');
|
|
|
|
const storageStats = ref<{
|
|
indexedPages: number;
|
|
totalDocuments: number;
|
|
totalTabs: number;
|
|
indexSize: number;
|
|
isInitialized: boolean;
|
|
} | null>(null);
|
|
const isRefreshingStats = ref(false);
|
|
const isClearingData = ref(false);
|
|
const showClearConfirmation = ref(false);
|
|
const clearDataProgress = ref('');
|
|
|
|
const semanticEngineStatus = ref<'idle' | 'initializing' | 'ready' | 'error'>('idle');
|
|
const isSemanticEngineInitializing = ref(false);
|
|
const semanticEngineInitProgress = ref('');
|
|
const semanticEngineLastUpdated = ref<number | null>(null);
|
|
|
|
// Cache management
|
|
const isManagingCache = ref(false);
|
|
const cacheStats = ref<{
|
|
totalSize: number;
|
|
totalSizeMB: number;
|
|
entryCount: number;
|
|
entries: Array<{
|
|
url: string;
|
|
size: number;
|
|
sizeMB: number;
|
|
timestamp: number;
|
|
age: string;
|
|
expired: boolean;
|
|
}>;
|
|
} | null>(null);
|
|
|
|
const availableModels = computed(() => {
|
|
return Object.entries(PREDEFINED_MODELS).map(([key, value]) => ({
|
|
preset: key as ModelPreset,
|
|
...value,
|
|
}));
|
|
});
|
|
|
|
const getStatusClass = () => {
|
|
if (nativeConnectionStatus.value === 'connected') {
|
|
if (serverStatus.value.isRunning) {
|
|
return 'bg-emerald-500';
|
|
} else {
|
|
return 'bg-yellow-500';
|
|
}
|
|
} else if (nativeConnectionStatus.value === 'disconnected') {
|
|
return 'bg-red-500';
|
|
} else {
|
|
return 'bg-gray-500';
|
|
}
|
|
};
|
|
|
|
// Open sidepanel and close popup
|
|
async function openSidepanelAndClose(tab: string) {
|
|
try {
|
|
const current = await chrome.windows.getCurrent();
|
|
if ((chrome.sidePanel as any)?.setOptions) {
|
|
await (chrome.sidePanel as any).setOptions({
|
|
path: `sidepanel.html?tab=${tab}`,
|
|
enabled: true,
|
|
});
|
|
}
|
|
if (chrome.sidePanel && (chrome.sidePanel as any).open) {
|
|
await (chrome.sidePanel as any).open({ windowId: current.id! });
|
|
}
|
|
// Close popup after opening sidepanel
|
|
window.close();
|
|
} catch (e) {
|
|
console.warn(`Failed to open sidepanel (${tab}):`, e);
|
|
}
|
|
}
|
|
|
|
// Open sidepanel from popup for workflow management
|
|
function openWorkflowSidepanel() {
|
|
// TODO: 工作流功能开发中,暂时拦截
|
|
showComingSoonToast('工作流管理');
|
|
// openSidepanelAndClose('workflows');
|
|
}
|
|
|
|
// Open sidepanel for element marker management
|
|
function openElementMarkerSidepanel() {
|
|
openSidepanelAndClose('element-markers');
|
|
}
|
|
|
|
// Open sidepanel for agent chat
|
|
function openAgentSidepanel() {
|
|
openSidepanelAndClose('agent-chat');
|
|
}
|
|
|
|
async function toggleWebEditor() {
|
|
try {
|
|
await chrome.runtime.sendMessage({ type: BACKGROUND_MESSAGE_TYPES.WEB_EDITOR_TOGGLE });
|
|
} catch (error) {
|
|
console.warn('切换网页编辑模式失败:', error);
|
|
}
|
|
}
|
|
|
|
async function toggleElementMarker() {
|
|
try {
|
|
// 获取当前活动tab
|
|
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
|
if (!tab?.id) {
|
|
console.warn('无法获取当前tab');
|
|
return;
|
|
}
|
|
|
|
// 向background发送消息,启动元素标注
|
|
await chrome.runtime.sendMessage({
|
|
type: BACKGROUND_MESSAGE_TYPES.ELEMENT_MARKER_START,
|
|
tabId: tab.id,
|
|
});
|
|
} catch (error) {
|
|
console.warn('开启元素标注失败:', error);
|
|
}
|
|
}
|
|
|
|
async function openWelcomePage() {
|
|
try {
|
|
await chrome.tabs.create({ url: chrome.runtime.getURL('welcome.html') });
|
|
} catch {
|
|
// ignore
|
|
}
|
|
}
|
|
|
|
async function openTroubleshooting() {
|
|
try {
|
|
await chrome.tabs.create({ url: LINKS.TROUBLESHOOTING });
|
|
} catch {
|
|
// ignore
|
|
}
|
|
}
|
|
|
|
function openBuilderWindow(flowId?: string, focusNodeId?: string) {
|
|
const url = new URL(chrome.runtime.getURL('builder.html'));
|
|
if (flowId) url.searchParams.set('flowId', flowId);
|
|
if (focusNodeId) url.searchParams.set('focus', focusNodeId);
|
|
chrome.windows.create({ url: url.toString(), type: 'popup', width: 1280, height: 800 });
|
|
}
|
|
|
|
const getStatusText = () => {
|
|
if (nativeConnectionStatus.value === 'connected') {
|
|
if (serverStatus.value.isRunning) {
|
|
return getMessage('serviceRunningStatus', [
|
|
(serverStatus.value.port || 'Unknown').toString(),
|
|
]);
|
|
} else {
|
|
return getMessage('connectedServiceNotStartedStatus');
|
|
}
|
|
} else if (nativeConnectionStatus.value === 'disconnected') {
|
|
return getMessage('serviceNotConnectedStatus');
|
|
} else {
|
|
return getMessage('detectingStatus');
|
|
}
|
|
};
|
|
|
|
const formatIndexSize = () => {
|
|
if (!storageStats.value?.indexSize) return '0 MB';
|
|
const sizeInMB = Math.round(storageStats.value.indexSize / (1024 * 1024));
|
|
return `${sizeInMB} MB`;
|
|
};
|
|
|
|
const getModelDescription = (model: any) => {
|
|
switch (model.preset) {
|
|
case 'multilingual-e5-small':
|
|
return getMessage('lightweightModelDescription');
|
|
case 'multilingual-e5-base':
|
|
return getMessage('betterThanSmallDescription');
|
|
default:
|
|
return getMessage('multilingualModelDescription');
|
|
}
|
|
};
|
|
|
|
const getPerformanceText = (performance: string) => {
|
|
switch (performance) {
|
|
case 'fast':
|
|
return getMessage('fastPerformance');
|
|
case 'balanced':
|
|
return getMessage('balancedPerformance');
|
|
case 'accurate':
|
|
return getMessage('accuratePerformance');
|
|
default:
|
|
return performance;
|
|
}
|
|
};
|
|
|
|
const getSemanticEngineStatusText = () => {
|
|
switch (semanticEngineStatus.value) {
|
|
case 'ready':
|
|
return getMessage('semanticEngineReadyStatus');
|
|
case 'initializing':
|
|
return getMessage('semanticEngineInitializingStatus');
|
|
case 'error':
|
|
return getMessage('semanticEngineInitFailedStatus');
|
|
case 'idle':
|
|
default:
|
|
return getMessage('semanticEngineNotInitStatus');
|
|
}
|
|
};
|
|
|
|
const getSemanticEngineStatusClass = () => {
|
|
switch (semanticEngineStatus.value) {
|
|
case 'ready':
|
|
return 'bg-emerald-500';
|
|
case 'initializing':
|
|
return 'bg-yellow-500';
|
|
case 'error':
|
|
return 'bg-red-500';
|
|
case 'idle':
|
|
default:
|
|
return 'bg-gray-500';
|
|
}
|
|
};
|
|
|
|
const getActiveTabsCount = () => {
|
|
return storageStats.value?.totalTabs || 0;
|
|
};
|
|
|
|
const getProgressText = () => {
|
|
if (isModelDownloading.value) {
|
|
return getMessage('downloadingModelStatus', [modelDownloadProgress.value.toString()]);
|
|
} else if (isModelSwitching.value) {
|
|
return modelSwitchProgress.value || getMessage('switchingModelStatus');
|
|
}
|
|
return '';
|
|
};
|
|
|
|
const getErrorTypeText = () => {
|
|
switch (modelErrorType.value) {
|
|
case 'network':
|
|
return getMessage('networkErrorMessage');
|
|
case 'file':
|
|
return getMessage('modelCorruptedErrorMessage');
|
|
case 'unknown':
|
|
default:
|
|
return getMessage('unknownErrorMessage');
|
|
}
|
|
};
|
|
|
|
const getSemanticEngineButtonText = () => {
|
|
switch (semanticEngineStatus.value) {
|
|
case 'ready':
|
|
return getMessage('reinitializeButton');
|
|
case 'initializing':
|
|
return getMessage('initializingStatus');
|
|
case 'error':
|
|
return getMessage('reinitializeButton');
|
|
case 'idle':
|
|
default:
|
|
return getMessage('initSemanticEngineButton');
|
|
}
|
|
};
|
|
|
|
const loadCacheStats = async () => {
|
|
try {
|
|
cacheStats.value = await getCacheStats();
|
|
} catch (error) {
|
|
console.error('Failed to get cache stats:', error);
|
|
cacheStats.value = null;
|
|
}
|
|
};
|
|
|
|
const cleanupCache = async () => {
|
|
if (isManagingCache.value) return;
|
|
|
|
isManagingCache.value = true;
|
|
try {
|
|
await cleanupModelCache();
|
|
// Refresh cache stats
|
|
await loadCacheStats();
|
|
} catch (error) {
|
|
console.error('Failed to cleanup cache:', error);
|
|
} finally {
|
|
isManagingCache.value = false;
|
|
}
|
|
};
|
|
|
|
const clearAllCache = async () => {
|
|
if (isManagingCache.value) return;
|
|
|
|
isManagingCache.value = true;
|
|
try {
|
|
await clearModelCache();
|
|
// Refresh cache stats
|
|
await loadCacheStats();
|
|
} catch (error) {
|
|
console.error('Failed to clear cache:', error);
|
|
} finally {
|
|
isManagingCache.value = false;
|
|
}
|
|
};
|
|
|
|
const saveSemanticEngineState = async () => {
|
|
try {
|
|
const semanticEngineState = {
|
|
status: semanticEngineStatus.value,
|
|
lastUpdated: semanticEngineLastUpdated.value,
|
|
};
|
|
// eslint-disable-next-line no-undef
|
|
await chrome.storage.local.set({ semanticEngineState });
|
|
} catch (error) {
|
|
console.error('保存语义引擎状态失败:', error);
|
|
}
|
|
};
|
|
|
|
const initializeSemanticEngine = async () => {
|
|
if (isSemanticEngineInitializing.value) return;
|
|
|
|
const isReinitialization = semanticEngineStatus.value === 'ready';
|
|
console.log(
|
|
`🚀 User triggered semantic engine ${isReinitialization ? 'reinitialization' : 'initialization'}`,
|
|
);
|
|
|
|
isSemanticEngineInitializing.value = true;
|
|
semanticEngineStatus.value = 'initializing';
|
|
semanticEngineInitProgress.value = isReinitialization
|
|
? getMessage('semanticEngineInitializingStatus')
|
|
: getMessage('semanticEngineInitializingStatus');
|
|
semanticEngineLastUpdated.value = Date.now();
|
|
|
|
await saveSemanticEngineState();
|
|
|
|
try {
|
|
// eslint-disable-next-line no-undef
|
|
chrome.runtime
|
|
.sendMessage({
|
|
type: BACKGROUND_MESSAGE_TYPES.INITIALIZE_SEMANTIC_ENGINE,
|
|
})
|
|
.catch((error) => {
|
|
console.error('❌ Error sending semantic engine initialization request:', error);
|
|
});
|
|
|
|
startSemanticEngineStatusPolling();
|
|
|
|
semanticEngineInitProgress.value = isReinitialization
|
|
? getMessage('processingStatus')
|
|
: getMessage('processingStatus');
|
|
} catch (error: any) {
|
|
console.error('❌ Failed to send initialization request:', error);
|
|
semanticEngineStatus.value = 'error';
|
|
semanticEngineInitProgress.value = `Failed to send initialization request: ${error?.message || 'Unknown error'}`;
|
|
|
|
await saveSemanticEngineState();
|
|
|
|
setTimeout(() => {
|
|
semanticEngineInitProgress.value = '';
|
|
}, 5000);
|
|
|
|
isSemanticEngineInitializing.value = false;
|
|
semanticEngineLastUpdated.value = Date.now();
|
|
await saveSemanticEngineState();
|
|
}
|
|
};
|
|
|
|
const checkSemanticEngineStatus = async () => {
|
|
try {
|
|
// eslint-disable-next-line no-undef
|
|
const response = await chrome.runtime.sendMessage({
|
|
type: BACKGROUND_MESSAGE_TYPES.GET_MODEL_STATUS,
|
|
});
|
|
|
|
if (response && response.success && response.status) {
|
|
const status = response.status;
|
|
|
|
if (status.initializationStatus === 'ready') {
|
|
semanticEngineStatus.value = 'ready';
|
|
semanticEngineLastUpdated.value = Date.now();
|
|
isSemanticEngineInitializing.value = false;
|
|
semanticEngineInitProgress.value = getMessage('semanticEngineReadyStatus');
|
|
await saveSemanticEngineState();
|
|
stopSemanticEngineStatusPolling();
|
|
setTimeout(() => {
|
|
semanticEngineInitProgress.value = '';
|
|
}, 2000);
|
|
} else if (
|
|
status.initializationStatus === 'downloading' ||
|
|
status.initializationStatus === 'initializing'
|
|
) {
|
|
semanticEngineStatus.value = 'initializing';
|
|
isSemanticEngineInitializing.value = true;
|
|
semanticEngineInitProgress.value = getMessage('semanticEngineInitializingStatus');
|
|
semanticEngineLastUpdated.value = Date.now();
|
|
await saveSemanticEngineState();
|
|
} else if (status.initializationStatus === 'error') {
|
|
semanticEngineStatus.value = 'error';
|
|
semanticEngineLastUpdated.value = Date.now();
|
|
isSemanticEngineInitializing.value = false;
|
|
semanticEngineInitProgress.value = getMessage('semanticEngineInitFailedStatus');
|
|
await saveSemanticEngineState();
|
|
stopSemanticEngineStatusPolling();
|
|
setTimeout(() => {
|
|
semanticEngineInitProgress.value = '';
|
|
}, 5000);
|
|
} else {
|
|
semanticEngineStatus.value = 'idle';
|
|
isSemanticEngineInitializing.value = false;
|
|
await saveSemanticEngineState();
|
|
}
|
|
} else {
|
|
semanticEngineStatus.value = 'idle';
|
|
isSemanticEngineInitializing.value = false;
|
|
await saveSemanticEngineState();
|
|
}
|
|
} catch (error) {
|
|
console.error('Popup: Failed to check semantic engine status:', error);
|
|
semanticEngineStatus.value = 'idle';
|
|
isSemanticEngineInitializing.value = false;
|
|
await saveSemanticEngineState();
|
|
}
|
|
};
|
|
|
|
const retryModelInitialization = async () => {
|
|
if (!currentModel.value) return;
|
|
|
|
console.log('🔄 Retrying model initialization...');
|
|
|
|
modelErrorMessage.value = '';
|
|
modelErrorType.value = '';
|
|
modelInitializationStatus.value = 'downloading';
|
|
modelDownloadProgress.value = 0;
|
|
isModelDownloading.value = true;
|
|
await switchModel(currentModel.value);
|
|
};
|
|
|
|
const updatePort = async (event: Event) => {
|
|
const target = event.target as HTMLInputElement;
|
|
const newPort = Number(target.value);
|
|
nativeServerPort.value = newPort;
|
|
|
|
await savePortPreference(newPort);
|
|
};
|
|
|
|
const checkNativeConnection = async () => {
|
|
try {
|
|
// eslint-disable-next-line no-undef
|
|
const response = await chrome.runtime.sendMessage({ type: 'ping_native' });
|
|
nativeConnectionStatus.value = response?.connected ? 'connected' : 'disconnected';
|
|
} catch (error) {
|
|
console.error('检测 Native 连接状态失败:', error);
|
|
nativeConnectionStatus.value = 'disconnected';
|
|
}
|
|
};
|
|
|
|
const checkServerStatus = async () => {
|
|
try {
|
|
// eslint-disable-next-line no-undef
|
|
const response = await chrome.runtime.sendMessage({
|
|
type: BACKGROUND_MESSAGE_TYPES.GET_SERVER_STATUS,
|
|
});
|
|
if (response?.success && response.serverStatus) {
|
|
serverStatus.value = response.serverStatus;
|
|
}
|
|
|
|
if (response?.connected !== undefined) {
|
|
nativeConnectionStatus.value = response.connected ? 'connected' : 'disconnected';
|
|
}
|
|
} catch (error) {
|
|
console.error('检测服务器状态失败:', error);
|
|
}
|
|
};
|
|
|
|
const refreshServerStatus = async () => {
|
|
try {
|
|
// eslint-disable-next-line no-undef
|
|
const response = await chrome.runtime.sendMessage({
|
|
type: BACKGROUND_MESSAGE_TYPES.REFRESH_SERVER_STATUS,
|
|
});
|
|
if (response?.success && response.serverStatus) {
|
|
serverStatus.value = response.serverStatus;
|
|
}
|
|
|
|
if (response?.connected !== undefined) {
|
|
nativeConnectionStatus.value = response.connected ? 'connected' : 'disconnected';
|
|
}
|
|
} catch (error) {
|
|
console.error('刷新服务器状态失败:', error);
|
|
}
|
|
};
|
|
|
|
const copyMcpConfig = async () => {
|
|
try {
|
|
await navigator.clipboard.writeText(mcpConfigJson.value);
|
|
copyButtonText.value = '✅' + getMessage('configCopiedNotification');
|
|
|
|
setTimeout(() => {
|
|
copyButtonText.value = getMessage('copyConfigButton');
|
|
}, 2000);
|
|
} catch (error) {
|
|
console.error('复制配置失败:', error);
|
|
copyButtonText.value = '❌' + getMessage('networkErrorMessage');
|
|
|
|
setTimeout(() => {
|
|
copyButtonText.value = getMessage('copyConfigButton');
|
|
}, 2000);
|
|
}
|
|
};
|
|
|
|
const testNativeConnection = async () => {
|
|
if (isConnecting.value) return;
|
|
isConnecting.value = true;
|
|
try {
|
|
if (nativeConnectionStatus.value === 'connected') {
|
|
// eslint-disable-next-line no-undef
|
|
await chrome.runtime.sendMessage({ type: 'disconnect_native' });
|
|
nativeConnectionStatus.value = 'disconnected';
|
|
} else {
|
|
console.log(`尝试连接到端口: ${nativeServerPort.value}`);
|
|
// eslint-disable-next-line no-undef
|
|
const response = await chrome.runtime.sendMessage({
|
|
type: 'connectNative',
|
|
port: nativeServerPort.value,
|
|
});
|
|
if (response && response.success) {
|
|
nativeConnectionStatus.value = 'connected';
|
|
console.log('连接成功:', response);
|
|
await savePortPreference(nativeServerPort.value);
|
|
} else {
|
|
nativeConnectionStatus.value = 'disconnected';
|
|
console.error('连接失败:', response);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('测试连接失败:', error);
|
|
nativeConnectionStatus.value = 'disconnected';
|
|
} finally {
|
|
isConnecting.value = false;
|
|
}
|
|
};
|
|
|
|
const loadModelPreference = async () => {
|
|
try {
|
|
// eslint-disable-next-line no-undef
|
|
const result = await chrome.storage.local.get([
|
|
'selectedModel',
|
|
'selectedVersion',
|
|
'modelState',
|
|
'semanticEngineState',
|
|
]);
|
|
|
|
if (result.selectedModel) {
|
|
const storedModel = result.selectedModel as string;
|
|
console.log('📋 Stored model from storage:', storedModel);
|
|
|
|
if (PREDEFINED_MODELS[storedModel as ModelPreset]) {
|
|
currentModel.value = storedModel as ModelPreset;
|
|
console.log(`✅ Loaded valid model: ${currentModel.value}`);
|
|
} else {
|
|
console.warn(
|
|
`⚠️ Stored model "${storedModel}" not found in PREDEFINED_MODELS, using default`,
|
|
);
|
|
currentModel.value = 'multilingual-e5-small';
|
|
await saveModelPreference(currentModel.value);
|
|
}
|
|
} else {
|
|
console.log('⚠️ No model found in storage, using default');
|
|
currentModel.value = 'multilingual-e5-small';
|
|
await saveModelPreference(currentModel.value);
|
|
}
|
|
|
|
selectedVersion.value = 'quantized';
|
|
console.log('✅ Using quantized version (fixed)');
|
|
|
|
await saveVersionPreference('quantized');
|
|
|
|
if (result.modelState) {
|
|
const modelState = result.modelState;
|
|
|
|
if (modelState.status === 'ready') {
|
|
modelInitializationStatus.value = 'ready';
|
|
modelDownloadProgress.value = modelState.downloadProgress || 100;
|
|
isModelDownloading.value = false;
|
|
} else {
|
|
modelInitializationStatus.value = 'idle';
|
|
modelDownloadProgress.value = 0;
|
|
isModelDownloading.value = false;
|
|
|
|
await saveModelState();
|
|
}
|
|
} else {
|
|
modelInitializationStatus.value = 'idle';
|
|
modelDownloadProgress.value = 0;
|
|
isModelDownloading.value = false;
|
|
}
|
|
|
|
if (result.semanticEngineState) {
|
|
const semanticState = result.semanticEngineState;
|
|
if (semanticState.status === 'ready') {
|
|
semanticEngineStatus.value = 'ready';
|
|
semanticEngineLastUpdated.value = semanticState.lastUpdated || Date.now();
|
|
} else if (semanticState.status === 'error') {
|
|
semanticEngineStatus.value = 'error';
|
|
semanticEngineLastUpdated.value = semanticState.lastUpdated || Date.now();
|
|
} else {
|
|
semanticEngineStatus.value = 'idle';
|
|
}
|
|
} else {
|
|
semanticEngineStatus.value = 'idle';
|
|
}
|
|
} catch (error) {
|
|
console.error('❌ 加载模型偏好失败:', error);
|
|
}
|
|
};
|
|
|
|
const saveModelPreference = async (model: ModelPreset) => {
|
|
try {
|
|
// eslint-disable-next-line no-undef
|
|
await chrome.storage.local.set({ selectedModel: model });
|
|
} catch (error) {
|
|
console.error('保存模型偏好失败:', error);
|
|
}
|
|
};
|
|
|
|
const saveVersionPreference = async (version: 'full' | 'quantized' | 'compressed') => {
|
|
try {
|
|
// eslint-disable-next-line no-undef
|
|
await chrome.storage.local.set({ selectedVersion: version });
|
|
} catch (error) {
|
|
console.error('保存版本偏好失败:', error);
|
|
}
|
|
};
|
|
|
|
const savePortPreference = async (port: number) => {
|
|
try {
|
|
// eslint-disable-next-line no-undef
|
|
await chrome.storage.local.set({ nativeServerPort: port });
|
|
console.log(`端口偏好已保存: ${port}`);
|
|
} catch (error) {
|
|
console.error('保存端口偏好失败:', error);
|
|
}
|
|
};
|
|
|
|
const loadPortPreference = async () => {
|
|
try {
|
|
// eslint-disable-next-line no-undef
|
|
const result = await chrome.storage.local.get(['nativeServerPort']);
|
|
if (result.nativeServerPort) {
|
|
nativeServerPort.value = result.nativeServerPort;
|
|
console.log(`端口偏好已加载: ${result.nativeServerPort}`);
|
|
}
|
|
} catch (error) {
|
|
console.error('加载端口偏好失败:', error);
|
|
}
|
|
};
|
|
|
|
const saveModelState = async () => {
|
|
try {
|
|
const modelState = {
|
|
status: modelInitializationStatus.value,
|
|
downloadProgress: modelDownloadProgress.value,
|
|
isDownloading: isModelDownloading.value,
|
|
lastUpdated: Date.now(),
|
|
};
|
|
// eslint-disable-next-line no-undef
|
|
await chrome.storage.local.set({ modelState });
|
|
} catch (error) {
|
|
console.error('保存模型状态失败:', error);
|
|
}
|
|
};
|
|
|
|
let statusMonitoringInterval: ReturnType<typeof setInterval> | null = null;
|
|
let semanticEngineStatusPollingInterval: ReturnType<typeof setInterval> | null = null;
|
|
|
|
const startModelStatusMonitoring = () => {
|
|
if (statusMonitoringInterval) {
|
|
clearInterval(statusMonitoringInterval);
|
|
}
|
|
|
|
statusMonitoringInterval = setInterval(async () => {
|
|
try {
|
|
// eslint-disable-next-line no-undef
|
|
const response = await chrome.runtime.sendMessage({
|
|
type: 'get_model_status',
|
|
});
|
|
|
|
if (response && response.success) {
|
|
const status = response.status;
|
|
modelInitializationStatus.value = status.initializationStatus || 'idle';
|
|
modelDownloadProgress.value = status.downloadProgress || 0;
|
|
isModelDownloading.value = status.isDownloading || false;
|
|
|
|
if (status.initializationStatus === 'error') {
|
|
modelErrorMessage.value = status.errorMessage || getMessage('modelFailedStatus');
|
|
modelErrorType.value = status.errorType || 'unknown';
|
|
} else {
|
|
modelErrorMessage.value = '';
|
|
modelErrorType.value = '';
|
|
}
|
|
|
|
await saveModelState();
|
|
|
|
if (status.initializationStatus === 'ready' || status.initializationStatus === 'error') {
|
|
stopModelStatusMonitoring();
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('获取模型状态失败:', error);
|
|
}
|
|
}, 1000);
|
|
};
|
|
|
|
const stopModelStatusMonitoring = () => {
|
|
if (statusMonitoringInterval) {
|
|
clearInterval(statusMonitoringInterval);
|
|
statusMonitoringInterval = null;
|
|
}
|
|
};
|
|
|
|
const startSemanticEngineStatusPolling = () => {
|
|
if (semanticEngineStatusPollingInterval) {
|
|
clearInterval(semanticEngineStatusPollingInterval);
|
|
}
|
|
|
|
semanticEngineStatusPollingInterval = setInterval(async () => {
|
|
try {
|
|
await checkSemanticEngineStatus();
|
|
} catch (error) {
|
|
console.error('Semantic engine status polling failed:', error);
|
|
}
|
|
}, 2000);
|
|
};
|
|
|
|
const stopSemanticEngineStatusPolling = () => {
|
|
if (semanticEngineStatusPollingInterval) {
|
|
clearInterval(semanticEngineStatusPollingInterval);
|
|
semanticEngineStatusPollingInterval = null;
|
|
}
|
|
};
|
|
|
|
const refreshStorageStats = async () => {
|
|
if (isRefreshingStats.value) return;
|
|
|
|
isRefreshingStats.value = true;
|
|
try {
|
|
console.log('🔄 Refreshing storage statistics...');
|
|
|
|
// eslint-disable-next-line no-undef
|
|
const response = await chrome.runtime.sendMessage({
|
|
type: 'get_storage_stats',
|
|
});
|
|
|
|
if (response && response.success) {
|
|
storageStats.value = {
|
|
indexedPages: response.stats.indexedPages || 0,
|
|
totalDocuments: response.stats.totalDocuments || 0,
|
|
totalTabs: response.stats.totalTabs || 0,
|
|
indexSize: response.stats.indexSize || 0,
|
|
isInitialized: response.stats.isInitialized || false,
|
|
};
|
|
console.log('✅ Storage stats refreshed:', storageStats.value);
|
|
} else {
|
|
console.error('❌ Failed to get storage stats:', response?.error);
|
|
storageStats.value = {
|
|
indexedPages: 0,
|
|
totalDocuments: 0,
|
|
totalTabs: 0,
|
|
indexSize: 0,
|
|
isInitialized: false,
|
|
};
|
|
}
|
|
} catch (error) {
|
|
console.error('❌ Error refreshing storage stats:', error);
|
|
storageStats.value = {
|
|
indexedPages: 0,
|
|
totalDocuments: 0,
|
|
totalTabs: 0,
|
|
indexSize: 0,
|
|
isInitialized: false,
|
|
};
|
|
} finally {
|
|
isRefreshingStats.value = false;
|
|
}
|
|
};
|
|
|
|
const hideClearDataConfirmation = () => {
|
|
showClearConfirmation.value = false;
|
|
};
|
|
|
|
const confirmClearAllData = async () => {
|
|
if (isClearingData.value) return;
|
|
|
|
isClearingData.value = true;
|
|
clearDataProgress.value = getMessage('clearingStatus');
|
|
|
|
try {
|
|
console.log('🗑️ Starting to clear all data...');
|
|
|
|
// eslint-disable-next-line no-undef
|
|
const response = await chrome.runtime.sendMessage({
|
|
type: 'clear_all_data',
|
|
});
|
|
|
|
if (response && response.success) {
|
|
clearDataProgress.value = getMessage('dataClearedNotification');
|
|
console.log('✅ All data cleared successfully');
|
|
|
|
await refreshStorageStats();
|
|
|
|
setTimeout(() => {
|
|
clearDataProgress.value = '';
|
|
hideClearDataConfirmation();
|
|
}, 2000);
|
|
} else {
|
|
throw new Error(response?.error || 'Failed to clear data');
|
|
}
|
|
} catch (error: any) {
|
|
console.error('❌ Failed to clear all data:', error);
|
|
clearDataProgress.value = `Failed to clear data: ${error?.message || 'Unknown error'}`;
|
|
|
|
setTimeout(() => {
|
|
clearDataProgress.value = '';
|
|
}, 5000);
|
|
} finally {
|
|
isClearingData.value = false;
|
|
}
|
|
};
|
|
|
|
const switchModel = async (newModel: ModelPreset) => {
|
|
console.log(`🔄 switchModel called with newModel: ${newModel}`);
|
|
|
|
if (isModelSwitching.value) {
|
|
console.log('⏸️ Model switch already in progress, skipping');
|
|
return;
|
|
}
|
|
|
|
const isSameModel = newModel === currentModel.value;
|
|
const currentModelInfo = currentModel.value
|
|
? getModelInfo(currentModel.value)
|
|
: getModelInfo('multilingual-e5-small');
|
|
const newModelInfo = getModelInfo(newModel);
|
|
const isDifferentDimension = currentModelInfo.dimension !== newModelInfo.dimension;
|
|
|
|
console.log(`📊 Switch analysis:`);
|
|
console.log(` - Same model: ${isSameModel} (${currentModel.value} -> ${newModel})`);
|
|
console.log(
|
|
` - Current dimension: ${currentModelInfo.dimension}, New dimension: ${newModelInfo.dimension}`,
|
|
);
|
|
console.log(` - Different dimension: ${isDifferentDimension}`);
|
|
|
|
if (isSameModel && !isDifferentDimension) {
|
|
console.log('✅ Same model and dimension - no need to switch');
|
|
return;
|
|
}
|
|
|
|
const switchReasons = [];
|
|
if (!isSameModel) switchReasons.push('different model');
|
|
if (isDifferentDimension) switchReasons.push('different dimension');
|
|
|
|
console.log(`🚀 Switching model due to: ${switchReasons.join(', ')}`);
|
|
console.log(
|
|
`📋 Model: ${currentModel.value} (${currentModelInfo.dimension}D) -> ${newModel} (${newModelInfo.dimension}D)`,
|
|
);
|
|
|
|
isModelSwitching.value = true;
|
|
modelSwitchProgress.value = getMessage('switchingModelStatus');
|
|
|
|
modelInitializationStatus.value = 'downloading';
|
|
modelDownloadProgress.value = 0;
|
|
isModelDownloading.value = true;
|
|
|
|
try {
|
|
await saveModelPreference(newModel);
|
|
await saveVersionPreference('quantized');
|
|
await saveModelState();
|
|
|
|
modelSwitchProgress.value = getMessage('semanticEngineInitializingStatus');
|
|
|
|
startModelStatusMonitoring();
|
|
|
|
// eslint-disable-next-line no-undef
|
|
const response = await chrome.runtime.sendMessage({
|
|
type: 'switch_semantic_model',
|
|
modelPreset: newModel,
|
|
modelVersion: 'quantized',
|
|
modelDimension: newModelInfo.dimension,
|
|
previousDimension: currentModelInfo.dimension,
|
|
});
|
|
|
|
if (response && response.success) {
|
|
currentModel.value = newModel;
|
|
modelSwitchProgress.value = getMessage('successNotification');
|
|
console.log(
|
|
'模型切换成功:',
|
|
newModel,
|
|
'version: quantized',
|
|
'dimension:',
|
|
newModelInfo.dimension,
|
|
);
|
|
|
|
modelInitializationStatus.value = 'ready';
|
|
isModelDownloading.value = false;
|
|
await saveModelState();
|
|
|
|
setTimeout(() => {
|
|
modelSwitchProgress.value = '';
|
|
}, 2000);
|
|
} else {
|
|
throw new Error(response?.error || 'Model switch failed');
|
|
}
|
|
} catch (error: any) {
|
|
console.error('模型切换失败:', error);
|
|
modelSwitchProgress.value = `Model switch failed: ${error?.message || 'Unknown error'}`;
|
|
|
|
modelInitializationStatus.value = 'error';
|
|
isModelDownloading.value = false;
|
|
|
|
const errorMessage = error?.message || '未知错误';
|
|
if (
|
|
errorMessage.includes('network') ||
|
|
errorMessage.includes('fetch') ||
|
|
errorMessage.includes('timeout')
|
|
) {
|
|
modelErrorType.value = 'network';
|
|
modelErrorMessage.value = getMessage('networkErrorMessage');
|
|
} else if (
|
|
errorMessage.includes('corrupt') ||
|
|
errorMessage.includes('invalid') ||
|
|
errorMessage.includes('format')
|
|
) {
|
|
modelErrorType.value = 'file';
|
|
modelErrorMessage.value = getMessage('modelCorruptedErrorMessage');
|
|
} else {
|
|
modelErrorType.value = 'unknown';
|
|
modelErrorMessage.value = errorMessage;
|
|
}
|
|
|
|
await saveModelState();
|
|
|
|
setTimeout(() => {
|
|
modelSwitchProgress.value = '';
|
|
}, 8000);
|
|
} finally {
|
|
isModelSwitching.value = false;
|
|
}
|
|
};
|
|
|
|
const setupServerStatusListener = () => {
|
|
// eslint-disable-next-line no-undef
|
|
const onMessage = (message: { type?: string; payload?: unknown }) => {
|
|
// Server status changes
|
|
if (message.type === BACKGROUND_MESSAGE_TYPES.SERVER_STATUS_CHANGED && message.payload) {
|
|
serverStatus.value = message.payload as any;
|
|
console.log('Server status updated:', message.payload);
|
|
}
|
|
// Flows changed - refresh list (IndexedDB-based notification)
|
|
if (message.type === BACKGROUND_MESSAGE_TYPES.RR_FLOWS_CHANGED) {
|
|
loadFlows();
|
|
}
|
|
};
|
|
chrome.runtime.onMessage.addListener(onMessage);
|
|
// Store reference for cleanup
|
|
(window as any).__rr_popup_onMessage = onMessage;
|
|
};
|
|
|
|
onMounted(async () => {
|
|
// 初始化主题
|
|
await initTheme();
|
|
await loadPortPreference();
|
|
await loadModelPreference();
|
|
await checkNativeConnection();
|
|
await checkServerStatus();
|
|
await refreshStorageStats();
|
|
await loadCacheStats();
|
|
await loadFlows();
|
|
try {
|
|
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
|
currentTabUrl.value = tab?.url || '';
|
|
} catch {}
|
|
|
|
await checkSemanticEngineStatus();
|
|
setupServerStatusListener();
|
|
// Auto-refresh workflows list when storage rr_flows changes
|
|
try {
|
|
const onChanged = (changes: any, area: string) => {
|
|
try {
|
|
if (area !== 'local') return;
|
|
if (Object.prototype.hasOwnProperty.call(changes || {}, 'rr_flows')) loadFlows();
|
|
} catch {}
|
|
};
|
|
chrome.storage.onChanged.addListener(onChanged);
|
|
(window as any).__rr_popup_onChanged = onChanged;
|
|
} catch {}
|
|
});
|
|
|
|
onUnmounted(() => {
|
|
stopModelStatusMonitoring();
|
|
stopSemanticEngineStatusPolling();
|
|
// Clean up runtime message listener
|
|
try {
|
|
const msgFn = (window as any).__rr_popup_onMessage;
|
|
if (msgFn && chrome?.runtime?.onMessage?.removeListener) {
|
|
chrome.runtime.onMessage.removeListener(msgFn);
|
|
}
|
|
} catch {}
|
|
// Clean up storage change listener (legacy fallback)
|
|
try {
|
|
const fn = (window as any).__rr_popup_onChanged;
|
|
if (fn && chrome?.storage?.onChanged?.removeListener) {
|
|
chrome.storage.onChanged.removeListener(fn);
|
|
}
|
|
} catch {}
|
|
});
|
|
</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: var(--ac-surface, white);
|
|
border-radius: var(--ac-radius-card, 12px);
|
|
box-shadow: var(--ac-shadow-card, 0 1px 3px rgba(0, 0, 0, 0.08));
|
|
padding: 16px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 14px;
|
|
}
|
|
.semantic-engine-card {
|
|
background: white;
|
|
border-radius: 16px;
|
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
|
padding: 20px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 16px;
|
|
}
|
|
|
|
.semantic-engine-status {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
}
|
|
|
|
.semantic-engine-button {
|
|
width: 100%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 8px;
|
|
background: #8b5cf6;
|
|
color: white;
|
|
font-weight: 600;
|
|
padding: 12px 16px;
|
|
border-radius: 8px;
|
|
border: none;
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
|
}
|
|
|
|
.semantic-engine-button:hover:not(:disabled) {
|
|
background: #7c3aed;
|
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
.semantic-engine-button:disabled {
|
|
opacity: 0.6;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.status-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.refresh-status-button {
|
|
background: none;
|
|
border: none;
|
|
cursor: pointer;
|
|
padding: 4px 8px;
|
|
border-radius: 6px;
|
|
font-size: 14px;
|
|
color: #64748b;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.refresh-status-button:hover {
|
|
background: #f1f5f9;
|
|
color: #374151;
|
|
}
|
|
|
|
.status-timestamp {
|
|
font-size: 12px;
|
|
color: #9ca3af;
|
|
margin-top: 4px;
|
|
}
|
|
|
|
.mcp-config-section {
|
|
border-top: 1px solid #f1f5f9;
|
|
}
|
|
|
|
.mcp-config-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.mcp-config-label {
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
color: #64748b;
|
|
margin: 0;
|
|
}
|
|
|
|
.copy-config-button {
|
|
background: none;
|
|
border: none;
|
|
cursor: pointer;
|
|
padding: 4px 8px;
|
|
border-radius: 6px;
|
|
font-size: 14px;
|
|
color: #64748b;
|
|
transition: all 0.2s ease;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
}
|
|
|
|
.copy-config-button:hover {
|
|
background: #f1f5f9;
|
|
color: #374151;
|
|
}
|
|
|
|
.mcp-config-content {
|
|
background: #f8fafc;
|
|
border: 1px solid #e2e8f0;
|
|
border-radius: 8px;
|
|
padding: 12px;
|
|
overflow-x: auto;
|
|
}
|
|
|
|
.mcp-config-json {
|
|
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
|
font-size: 12px;
|
|
line-height: 1.4;
|
|
color: #374151;
|
|
margin: 0;
|
|
white-space: pre;
|
|
overflow-x: auto;
|
|
}
|
|
|
|
.port-section {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
}
|
|
|
|
.port-label {
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
color: #64748b;
|
|
}
|
|
|
|
.port-input {
|
|
display: block;
|
|
width: 100%;
|
|
border-radius: 8px;
|
|
border: 1px solid #d1d5db;
|
|
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
|
padding: 12px;
|
|
font-size: 14px;
|
|
background: #f8fafc;
|
|
}
|
|
|
|
.port-input:focus {
|
|
outline: none;
|
|
border-color: var(--ac-accent, #d97757);
|
|
box-shadow: 0 0 0 3px var(--ac-accent-subtle, rgba(217, 119, 87, 0.12));
|
|
}
|
|
|
|
.connect-button {
|
|
width: 100%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 8px;
|
|
background: var(--ac-accent, #d97757);
|
|
color: var(--ac-accent-contrast, white);
|
|
font-weight: 600;
|
|
padding: 12px 16px;
|
|
border-radius: var(--ac-radius-button, 8px);
|
|
border: none;
|
|
cursor: pointer;
|
|
transition: all var(--ac-motion-fast, 120ms) ease;
|
|
box-shadow: var(--ac-shadow-card, 0 1px 3px rgba(0, 0, 0, 0.08));
|
|
}
|
|
|
|
.connect-button:hover:not(:disabled) {
|
|
background: var(--ac-accent-hover, #c4664a);
|
|
box-shadow: var(--ac-shadow-float, 0 4px 20px -2px rgba(0, 0, 0, 0.05));
|
|
}
|
|
|
|
.connect-button:disabled {
|
|
opacity: 0.5;
|
|
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 sizes - use :deep to apply to child components */
|
|
:deep(.icon-small) {
|
|
width: 16px;
|
|
height: 16px;
|
|
}
|
|
|
|
:deep(.icon-default) {
|
|
width: 20px;
|
|
height: 20px;
|
|
}
|
|
|
|
:deep(.icon-medium) {
|
|
width: 24px;
|
|
height: 24px;
|
|
}
|
|
.footer {
|
|
padding: 16px;
|
|
margin-top: auto;
|
|
}
|
|
|
|
.footer-links {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
justify-content: center;
|
|
gap: 16px;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.footer-link {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
background: none;
|
|
border: none;
|
|
color: #64748b;
|
|
font-size: 12px;
|
|
cursor: pointer;
|
|
padding: 4px 8px;
|
|
border-radius: 6px;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.footer-link:hover {
|
|
color: #8b5cf6;
|
|
background: #e2e8f0;
|
|
}
|
|
|
|
.footer-link svg {
|
|
width: 14px;
|
|
height: 14px;
|
|
}
|
|
|
|
.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;
|
|
}
|
|
|
|
.footer-links {
|
|
gap: 8px;
|
|
}
|
|
|
|
.rr-grid {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 12px;
|
|
}
|
|
.rr-controls {
|
|
display: flex;
|
|
gap: 8px;
|
|
}
|
|
.rr-list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
}
|
|
.rr-item {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 8px;
|
|
border: 1px solid #eee;
|
|
border-radius: 6px;
|
|
}
|
|
.rr-runoverrides {
|
|
margin-top: 6px;
|
|
border: 1px dashed #e5e7eb;
|
|
border-radius: 8px;
|
|
padding: 8px;
|
|
background: #f9fafb;
|
|
}
|
|
.rr-meta {
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
.rr-name {
|
|
font-weight: 600;
|
|
}
|
|
.rr-desc {
|
|
font-size: 12px;
|
|
color: #666;
|
|
}
|
|
.empty {
|
|
color: #888;
|
|
font-size: 13px;
|
|
}
|
|
|
|
.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;
|
|
}
|
|
}
|
|
|
|
/* 快捷工具icon按钮样式 */
|
|
.rr-icon-buttons {
|
|
display: flex;
|
|
gap: 12px;
|
|
justify-content: flex-start;
|
|
padding: 16px;
|
|
background: var(--ac-surface, white);
|
|
border-radius: var(--ac-radius-card, 12px);
|
|
box-shadow: var(--ac-shadow-card, 0 1px 3px rgba(0, 0, 0, 0.08));
|
|
}
|
|
|
|
.rr-icon-btn {
|
|
width: 48px;
|
|
height: 48px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: var(--ac-surface-muted, #f2f0eb);
|
|
border: none;
|
|
border-radius: var(--ac-radius-button, 8px);
|
|
color: var(--ac-text-muted, #6e6e6e);
|
|
cursor: pointer;
|
|
transition: all var(--ac-motion-fast, 120ms) ease;
|
|
}
|
|
|
|
.rr-icon-btn:hover:not(:disabled) {
|
|
transform: translateY(-2px);
|
|
box-shadow: var(--ac-shadow-float, 0 4px 20px -2px rgba(0, 0, 0, 0.05));
|
|
}
|
|
|
|
.rr-icon-btn:disabled {
|
|
opacity: 0.4;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.rr-icon-btn svg {
|
|
width: 24px;
|
|
height: 24px;
|
|
}
|
|
|
|
/* 录制按钮 - 红色 */
|
|
.rr-icon-btn-record {
|
|
background: rgba(239, 68, 68, 0.1);
|
|
color: #ef4444;
|
|
}
|
|
|
|
.rr-icon-btn-record:hover:not(:disabled) {
|
|
background: rgba(239, 68, 68, 0.2);
|
|
color: #dc2626;
|
|
}
|
|
|
|
/* 录制中状态 - 脉冲动画 */
|
|
.rr-icon-btn-recording {
|
|
animation: pulse-recording 1.5s ease-in-out infinite;
|
|
}
|
|
|
|
@keyframes pulse-recording {
|
|
0%,
|
|
100% {
|
|
box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.4);
|
|
}
|
|
50% {
|
|
box-shadow: 0 0 0 8px rgba(239, 68, 68, 0);
|
|
}
|
|
}
|
|
|
|
/* 停止按钮 - 深红色 */
|
|
.rr-icon-btn-stop {
|
|
background: rgba(185, 28, 28, 0.1);
|
|
color: #b91c1c;
|
|
}
|
|
|
|
.rr-icon-btn-stop:hover:not(:disabled) {
|
|
background: rgba(185, 28, 28, 0.2);
|
|
color: #991b1b;
|
|
}
|
|
|
|
/* 编辑按钮 - 蓝色 */
|
|
.rr-icon-btn-edit {
|
|
background: rgba(37, 99, 235, 0.1);
|
|
color: #2563eb;
|
|
}
|
|
|
|
.rr-icon-btn-edit:hover:not(:disabled) {
|
|
background: rgba(37, 99, 235, 0.2);
|
|
color: #1d4ed8;
|
|
}
|
|
|
|
/* 标注按钮 - 绿色 */
|
|
.rr-icon-btn-marker {
|
|
background: rgba(16, 185, 129, 0.1);
|
|
color: #10b981;
|
|
}
|
|
|
|
.rr-icon-btn-marker:hover:not(:disabled) {
|
|
background: rgba(16, 185, 129, 0.2);
|
|
color: #059669;
|
|
}
|
|
|
|
/* Coming Soon 按钮样式 */
|
|
.rr-icon-btn-coming-soon {
|
|
opacity: 0.5;
|
|
cursor: default !important;
|
|
}
|
|
|
|
.rr-icon-btn-coming-soon:hover {
|
|
transform: none !important;
|
|
box-shadow: none !important;
|
|
opacity: 0.6;
|
|
}
|
|
|
|
/* CSS Tooltip - instant display */
|
|
.has-tooltip {
|
|
position: relative;
|
|
}
|
|
|
|
.has-tooltip::after {
|
|
content: attr(data-tooltip);
|
|
position: absolute;
|
|
bottom: calc(100% + 6px);
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
padding: 6px 10px;
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
line-height: 1.3;
|
|
white-space: nowrap;
|
|
color: var(--ac-text-inverse, #ffffff);
|
|
background-color: var(--ac-text, #1a1a1a);
|
|
border-radius: var(--ac-radius-button, 8px);
|
|
opacity: 0;
|
|
visibility: hidden;
|
|
transition:
|
|
opacity 80ms ease,
|
|
visibility 80ms ease;
|
|
pointer-events: none;
|
|
z-index: 100;
|
|
}
|
|
|
|
.has-tooltip::before {
|
|
content: '';
|
|
position: absolute;
|
|
bottom: calc(100% + 2px);
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
border: 4px solid transparent;
|
|
border-top-color: var(--ac-text, #1a1a1a);
|
|
opacity: 0;
|
|
visibility: hidden;
|
|
transition:
|
|
opacity 80ms ease,
|
|
visibility 80ms ease;
|
|
pointer-events: none;
|
|
z-index: 100;
|
|
}
|
|
|
|
.has-tooltip:hover::after,
|
|
.has-tooltip:hover::before {
|
|
opacity: 1;
|
|
visibility: visible;
|
|
}
|
|
|
|
/* 首页视图 */
|
|
.home-view {
|
|
display: flex;
|
|
flex-direction: column;
|
|
height: 100%;
|
|
}
|
|
|
|
/* 管理入口卡片样式 */
|
|
.entry-card {
|
|
background: var(--ac-surface, white);
|
|
border-radius: var(--ac-radius-card, 12px);
|
|
box-shadow: var(--ac-shadow-card, 0 1px 3px rgba(0, 0, 0, 0.08));
|
|
overflow: hidden;
|
|
}
|
|
|
|
.entry-item {
|
|
width: 100%;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
padding: 14px 16px;
|
|
background: transparent;
|
|
border: none;
|
|
border-bottom: 1px solid var(--ac-border, #e7e5e4);
|
|
cursor: pointer;
|
|
transition: all var(--ac-motion-fast, 120ms) ease;
|
|
text-align: left;
|
|
}
|
|
|
|
.entry-item:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.entry-item:hover {
|
|
background: var(--ac-hover-bg, #f5f5f4);
|
|
}
|
|
|
|
.entry-icon {
|
|
width: 40px;
|
|
height: 40px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
border-radius: var(--ac-radius-button, 8px);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.entry-icon.agent {
|
|
background: rgba(217, 119, 87, 0.12);
|
|
color: var(--ac-accent, #d97757);
|
|
}
|
|
|
|
.entry-icon.workflow {
|
|
background: rgba(37, 99, 235, 0.12);
|
|
color: #2563eb;
|
|
}
|
|
|
|
.entry-icon.marker {
|
|
background: rgba(16, 185, 129, 0.12);
|
|
color: #10b981;
|
|
}
|
|
|
|
.entry-icon.model {
|
|
background: rgba(139, 92, 246, 0.12);
|
|
color: #8b5cf6;
|
|
}
|
|
|
|
.entry-content {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
|
|
.entry-title {
|
|
display: block;
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
color: var(--ac-text, #1a1a1a);
|
|
line-height: 1.3;
|
|
}
|
|
|
|
.entry-desc {
|
|
display: block;
|
|
font-size: 12px;
|
|
color: var(--ac-text-subtle, #a8a29e);
|
|
line-height: 1.3;
|
|
margin-top: 2px;
|
|
}
|
|
|
|
.entry-arrow {
|
|
color: var(--ac-text-subtle, #a8a29e);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
/* Coming Soon Badge */
|
|
.coming-soon-badge {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
margin-left: 6px;
|
|
padding: 2px 6px;
|
|
font-size: 9px;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
color: var(--ac-accent, #d97757);
|
|
background: rgba(217, 119, 87, 0.12);
|
|
border-radius: 4px;
|
|
vertical-align: middle;
|
|
}
|
|
|
|
.entry-item-coming-soon {
|
|
opacity: 0.7;
|
|
}
|
|
|
|
.entry-item-coming-soon:hover {
|
|
opacity: 0.85;
|
|
}
|
|
|
|
/* Coming Soon Toast */
|
|
.coming-soon-toast {
|
|
position: fixed;
|
|
bottom: 24px;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 12px 20px;
|
|
background: var(--ac-text, #1a1a1a);
|
|
color: var(--ac-text-inverse, #ffffff);
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
border-radius: var(--ac-radius-card, 12px);
|
|
box-shadow: var(--ac-shadow-float, 0 4px 20px -2px rgba(0, 0, 0, 0.15));
|
|
z-index: 1000;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.toast-icon {
|
|
width: 18px;
|
|
height: 18px;
|
|
flex-shrink: 0;
|
|
color: var(--ac-accent, #d97757);
|
|
}
|
|
|
|
/* Toast transition */
|
|
.toast-enter-active,
|
|
.toast-leave-active {
|
|
transition: all 0.25s ease;
|
|
}
|
|
|
|
.toast-enter-from,
|
|
.toast-leave-to {
|
|
opacity: 0;
|
|
transform: translateX(-50%) translateY(12px);
|
|
}
|
|
</style>
|