hangwin--mcp-chrome
746 行
17 KiB
Vue
746 行
17 KiB
Vue
<template>
|
|
<div class="local-model-page">
|
|
<!-- 返回按钮 -->
|
|
<div class="page-header">
|
|
<button class="back-button" @click="$emit('back')" title="返回首页">
|
|
<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="M15 19l-7-7 7-7" />
|
|
</svg>
|
|
<span>返回</span>
|
|
</button>
|
|
<h2 class="page-title">本地模型</h2>
|
|
</div>
|
|
|
|
<div class="page-content">
|
|
<!-- 语义引擎 -->
|
|
<div class="section">
|
|
<h3 class="section-title">{{ getMessage('semanticEngineLabel') }}</h3>
|
|
<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="primary-action-button"
|
|
:disabled="isSemanticEngineInitializing"
|
|
@click="$emit('initializeSemanticEngine')"
|
|
>
|
|
<BoltIcon />
|
|
<span>{{ getSemanticEngineButtonText() }}</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Embedding模型选择 -->
|
|
<div class="section">
|
|
<h3 class="section-title">{{ getMessage('embeddingModelLabel') }}</h3>
|
|
|
|
<ProgressIndicator
|
|
v-if="isModelSwitching || isModelDownloading"
|
|
:visible="isModelSwitching || isModelDownloading"
|
|
:text="progressText"
|
|
: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">{{ errorTypeText }}</p>
|
|
</div>
|
|
</div>
|
|
<button
|
|
class="retry-button"
|
|
@click="$emit('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 && $emit('switchModel', model.preset)"
|
|
>
|
|
<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">
|
|
<h3 class="section-title">{{ getMessage('indexDataManagementLabel') }}</h3>
|
|
<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">{{ storageStats?.totalTabs || 0 }}</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-action-button"
|
|
:disabled="isClearingData"
|
|
@click="$emit('showClearConfirmation')"
|
|
>
|
|
<TrashIcon />
|
|
<span>{{
|
|
isClearingData ? getMessage('clearingStatus') : getMessage('clearAllDataButton')
|
|
}}</span>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- 模型缓存管理 -->
|
|
<ModelCacheManagement
|
|
:cache-stats="cacheStats"
|
|
:is-managing-cache="isManagingCache"
|
|
@cleanup-cache="$emit('cleanupCache')"
|
|
@clear-all-cache="$emit('clearAllCache')"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script lang="ts" setup>
|
|
import { computed } from 'vue';
|
|
import { getMessage } from '@/utils/i18n';
|
|
import ProgressIndicator from './ProgressIndicator.vue';
|
|
import ModelCacheManagement from './ModelCacheManagement.vue';
|
|
import {
|
|
DocumentIcon,
|
|
DatabaseIcon,
|
|
BoltIcon,
|
|
TrashIcon,
|
|
CheckIcon,
|
|
TabIcon,
|
|
VectorIcon,
|
|
} from './icons';
|
|
|
|
interface Props {
|
|
// 语义引擎
|
|
semanticEngineStatus: 'idle' | 'initializing' | 'ready' | 'error';
|
|
isSemanticEngineInitializing: boolean;
|
|
semanticEngineInitProgress: string;
|
|
semanticEngineLastUpdated: number | null;
|
|
// 模型
|
|
availableModels: Array<{
|
|
preset: string;
|
|
performance: string;
|
|
size: string;
|
|
dimension: number;
|
|
}>;
|
|
currentModel: string | null;
|
|
isModelSwitching: boolean;
|
|
isModelDownloading: boolean;
|
|
modelDownloadProgress: number;
|
|
modelInitializationStatus: string;
|
|
modelErrorMessage: string;
|
|
modelErrorType: string;
|
|
// 存储统计
|
|
storageStats: {
|
|
indexedPages: number;
|
|
totalDocuments: number;
|
|
totalTabs: number;
|
|
indexSize: number;
|
|
isInitialized: boolean;
|
|
} | null;
|
|
isClearingData: boolean;
|
|
clearDataProgress: string;
|
|
// 缓存
|
|
cacheStats: any;
|
|
isManagingCache: boolean;
|
|
}
|
|
|
|
const props = defineProps<Props>();
|
|
|
|
defineEmits<{
|
|
(e: 'back'): void;
|
|
(e: 'initializeSemanticEngine'): void;
|
|
(e: 'switchModel', preset: string): void;
|
|
(e: 'retryModelInitialization'): void;
|
|
(e: 'showClearConfirmation'): void;
|
|
(e: 'cleanupCache'): void;
|
|
(e: 'clearAllCache'): void;
|
|
}>();
|
|
|
|
// 计算属性
|
|
const getSemanticEngineStatusClass = () => {
|
|
switch (props.semanticEngineStatus) {
|
|
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 getSemanticEngineStatusText = () => {
|
|
switch (props.semanticEngineStatus) {
|
|
case 'ready':
|
|
return getMessage('semanticEngineReadyStatus');
|
|
case 'initializing':
|
|
return getMessage('semanticEngineInitializingStatus');
|
|
case 'error':
|
|
return getMessage('semanticEngineInitFailedStatus');
|
|
case 'idle':
|
|
default:
|
|
return getMessage('semanticEngineNotInitStatus');
|
|
}
|
|
};
|
|
|
|
const getSemanticEngineButtonText = () => {
|
|
switch (props.semanticEngineStatus) {
|
|
case 'ready':
|
|
return getMessage('reinitializeButton');
|
|
case 'initializing':
|
|
return getMessage('initializingStatus');
|
|
case 'error':
|
|
return getMessage('reinitializeButton');
|
|
case 'idle':
|
|
default:
|
|
return getMessage('initSemanticEngineButton');
|
|
}
|
|
};
|
|
|
|
const progressText = computed(() => {
|
|
if (props.isModelDownloading) {
|
|
return getMessage('downloadingModelStatus', [props.modelDownloadProgress.toString()]);
|
|
} else if (props.isModelSwitching) {
|
|
return getMessage('switchingModelStatus');
|
|
}
|
|
return '';
|
|
});
|
|
|
|
const errorTypeText = computed(() => {
|
|
switch (props.modelErrorType) {
|
|
case 'network':
|
|
return getMessage('networkErrorMessage');
|
|
case 'file':
|
|
return getMessage('modelCorruptedErrorMessage');
|
|
case 'unknown':
|
|
default:
|
|
return getMessage('unknownErrorMessage');
|
|
}
|
|
});
|
|
|
|
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 formatIndexSize = () => {
|
|
if (!props.storageStats?.indexSize) return '0 MB';
|
|
const sizeInMB = Math.round(props.storageStats.indexSize / (1024 * 1024));
|
|
return `${sizeInMB} MB`;
|
|
};
|
|
</script>
|
|
|
|
<style scoped>
|
|
.local-model-page {
|
|
display: flex;
|
|
flex-direction: column;
|
|
height: 100%;
|
|
}
|
|
|
|
.page-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
padding: 16px 20px;
|
|
border-bottom: 1px solid var(--ac-border, #e7e5e4);
|
|
background: var(--ac-surface, #ffffff);
|
|
}
|
|
|
|
.back-button {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
padding: 8px 12px;
|
|
background: var(--ac-surface-muted, #f2f0eb);
|
|
border: none;
|
|
border-radius: var(--ac-radius-button, 8px);
|
|
color: var(--ac-text-muted, #6e6e6e);
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
cursor: pointer;
|
|
transition: all var(--ac-motion-fast, 120ms) ease;
|
|
}
|
|
|
|
.back-button:hover {
|
|
background: var(--ac-hover-bg, #f5f5f4);
|
|
color: var(--ac-text, #1a1a1a);
|
|
}
|
|
|
|
.page-title {
|
|
font-size: 18px;
|
|
font-weight: 600;
|
|
color: var(--ac-text, #1a1a1a);
|
|
margin: 0;
|
|
}
|
|
|
|
.page-content {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
padding: 16px 20px;
|
|
}
|
|
|
|
.section {
|
|
margin-bottom: 24px;
|
|
}
|
|
|
|
.section-title {
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
color: var(--ac-text, #374151);
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.semantic-engine-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: 12px;
|
|
}
|
|
|
|
.semantic-engine-status {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 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-yellow-500 {
|
|
background-color: #eab308;
|
|
}
|
|
.status-dot.bg-red-500 {
|
|
background-color: #ef4444;
|
|
}
|
|
.status-dot.bg-gray-500 {
|
|
background-color: #6b7280;
|
|
}
|
|
|
|
.status-text {
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
color: var(--ac-text, #1a1a1a);
|
|
}
|
|
|
|
.status-timestamp {
|
|
font-size: 12px;
|
|
color: var(--ac-text-subtle, #9ca3af);
|
|
}
|
|
|
|
.primary-action-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;
|
|
}
|
|
|
|
.primary-action-button:hover:not(:disabled) {
|
|
background: var(--ac-accent-hover, #c4664a);
|
|
}
|
|
|
|
.primary-action-button:disabled {
|
|
opacity: 0.6;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.danger-action-button {
|
|
width: 100%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 8px;
|
|
background: var(--ac-surface, white);
|
|
border: 1px solid var(--ac-border, #d1d5db);
|
|
color: var(--ac-text, #374151);
|
|
font-weight: 600;
|
|
padding: 12px 16px;
|
|
border-radius: var(--ac-radius-button, 8px);
|
|
cursor: pointer;
|
|
transition: all var(--ac-motion-fast, 120ms) ease;
|
|
margin-top: 12px;
|
|
}
|
|
|
|
.danger-action-button:hover:not(:disabled) {
|
|
border-color: #ef4444;
|
|
color: #dc2626;
|
|
}
|
|
|
|
.danger-action-button:disabled {
|
|
opacity: 0.6;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
/* 模型列表 */
|
|
.model-list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 12px;
|
|
}
|
|
|
|
.model-card {
|
|
background: var(--ac-surface, white);
|
|
border-radius: var(--ac-radius-card, 12px);
|
|
padding: 16px;
|
|
cursor: pointer;
|
|
border: 1px solid var(--ac-border, #e5e7eb);
|
|
transition: all var(--ac-motion-fast, 120ms) ease;
|
|
}
|
|
|
|
.model-card:hover {
|
|
border-color: var(--ac-accent, #d97757);
|
|
}
|
|
|
|
.model-card.selected {
|
|
border: 2px solid var(--ac-accent, #d97757);
|
|
background: var(--ac-accent-subtle, rgba(217, 119, 87, 0.08));
|
|
}
|
|
|
|
.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: var(--ac-text, #1e293b);
|
|
margin: 0 0 4px 0;
|
|
}
|
|
|
|
.model-name.selected-text {
|
|
color: var(--ac-accent, #d97757);
|
|
}
|
|
|
|
.model-description {
|
|
font-size: 14px;
|
|
color: var(--ac-text-muted, #64748b);
|
|
margin: 0;
|
|
}
|
|
|
|
.check-icon {
|
|
width: 20px;
|
|
height: 20px;
|
|
flex-shrink: 0;
|
|
background: var(--ac-accent, #d97757);
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.model-tags {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
margin-top: 12px;
|
|
}
|
|
|
|
.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: var(--ac-accent-subtle, #ddd6fe);
|
|
color: var(--ac-accent, #5b21b6);
|
|
}
|
|
|
|
.model-tag.dimension {
|
|
background: var(--ac-surface-muted, #e5e7eb);
|
|
color: var(--ac-text-muted, #4b5563);
|
|
}
|
|
|
|
/* 统计网格 */
|
|
.stats-grid {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 12px;
|
|
}
|
|
|
|
.stats-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;
|
|
}
|
|
|
|
.stats-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.stats-label {
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
color: var(--ac-text-muted, #64748b);
|
|
margin: 0;
|
|
}
|
|
|
|
.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: 24px;
|
|
font-weight: 700;
|
|
color: var(--ac-text, #0f172a);
|
|
margin: 0;
|
|
}
|
|
|
|
/* 错误卡片 */
|
|
.error-card {
|
|
background: #fef2f2;
|
|
border: 1px solid #fecaca;
|
|
border-radius: var(--ac-radius-card, 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;
|
|
}
|
|
|
|
.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 var(--ac-motion-fast, 120ms) ease;
|
|
font-size: 14px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.retry-button:hover:not(:disabled) {
|
|
background: #b91c1c;
|
|
}
|
|
|
|
.retry-button:disabled {
|
|
opacity: 0.6;
|
|
cursor: not-allowed;
|
|
}
|
|
</style>
|