mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-16 12:27:53 +08:00
feat(llm): add UI to configure embedding models from any provider (#79)
This commit is contained in:
parent
7fb47390af
commit
ce33c1c652
@ -22,6 +22,7 @@ import vip.mate.llm.model.ModelProviderEntity;
|
|||||||
import vip.mate.llm.service.ModelProviderService;
|
import vip.mate.llm.service.ModelProviderService;
|
||||||
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Embedding 模型工厂
|
* Embedding 模型工厂
|
||||||
@ -55,6 +56,14 @@ public class EmbeddingModelFactory {
|
|||||||
/** 构造 API 时共享的 retry template(用 Spring AI 默认) */
|
/** 构造 API 时共享的 retry template(用 Spring AI 默认) */
|
||||||
private static final RetryTemplate DEFAULT_RETRY = RetryUtils.DEFAULT_RETRY_TEMPLATE;
|
private static final RetryTemplate DEFAULT_RETRY = RetryUtils.DEFAULT_RETRY_TEMPLATE;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trailing "/v{digits}" segment in a base URL — the OpenAI-compatible
|
||||||
|
* convention (/v1 OpenAI, /v3 Volcano Ark, /v4 Zhipu). When the base URL
|
||||||
|
* already carries a version segment, the default "/v1/embeddings" path
|
||||||
|
* would build a broken URL like ".../api/paas/v4/v1/embeddings".
|
||||||
|
*/
|
||||||
|
private static final Pattern BASE_URL_VERSION_SUFFIX = Pattern.compile(".*/v\\d+$");
|
||||||
|
|
||||||
/** 按 modelConfig.id 缓存 EmbeddingModel,config 变更时调用 {@link #evict} 清除 */
|
/** 按 modelConfig.id 缓存 EmbeddingModel,config 变更时调用 {@link #evict} 清除 */
|
||||||
private final ConcurrentHashMap<Long, EmbeddingModel> cache = new ConcurrentHashMap<>();
|
private final ConcurrentHashMap<Long, EmbeddingModel> cache = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
@ -163,7 +172,7 @@ public class EmbeddingModelFactory {
|
|||||||
OpenAiApi api = OpenAiApi.builder()
|
OpenAiApi api = OpenAiApi.builder()
|
||||||
.baseUrl(baseUrl)
|
.baseUrl(baseUrl)
|
||||||
.apiKey(apiKey.trim())
|
.apiKey(apiKey.trim())
|
||||||
.embeddingsPath("/v1/embeddings")
|
.embeddingsPath(resolveEmbeddingsPath(baseUrl))
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
OpenAiEmbeddingOptions options = OpenAiEmbeddingOptions.builder()
|
OpenAiEmbeddingOptions options = OpenAiEmbeddingOptions.builder()
|
||||||
@ -181,4 +190,17 @@ public class EmbeddingModelFactory {
|
|||||||
if (u.endsWith("/v1")) u = u.substring(0, u.length() - 3);
|
if (u.endsWith("/v1")) u = u.substring(0, u.length() - 3);
|
||||||
return u;
|
return u;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve the embeddings path. Defaults to {@code /v1/embeddings}; when the
|
||||||
|
* base URL already ends with a version segment (Zhipu {@code /v4}, Volcano
|
||||||
|
* Ark {@code /v3}, …), drop the leading {@code /v1} so the request hits
|
||||||
|
* {@code <base>/embeddings} instead of a 404 at {@code <base>/v1/embeddings}.
|
||||||
|
*/
|
||||||
|
private String resolveEmbeddingsPath(String baseUrl) {
|
||||||
|
if (baseUrl != null && BASE_URL_VERSION_SUFFIX.matcher(baseUrl).matches()) {
|
||||||
|
return "/embeddings";
|
||||||
|
}
|
||||||
|
return "/v1/embeddings";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -658,6 +658,36 @@ export default {
|
|||||||
editTitle: 'Provider Settings',
|
editTitle: 'Provider Settings',
|
||||||
manageTitle: 'Manage Models',
|
manageTitle: 'Manage Models',
|
||||||
addModel: 'Add Model',
|
addModel: 'Add Model',
|
||||||
|
embedding: {
|
||||||
|
title: 'Embedding Models',
|
||||||
|
hint: 'Used for knowledge base semantic search; shares the provider API key with chat models',
|
||||||
|
loading: 'Loading...',
|
||||||
|
empty: 'No embedding models yet — add one with the form below.',
|
||||||
|
defaultBadge: 'Default',
|
||||||
|
builtinBadge: 'Built-in',
|
||||||
|
test: 'Test',
|
||||||
|
testing: 'Testing...',
|
||||||
|
testPassed: 'Passed · {dim} dimensions',
|
||||||
|
setDefault: 'Set as default',
|
||||||
|
delete: 'Delete',
|
||||||
|
deleteConfirm: 'Delete embedding model "{name}"?',
|
||||||
|
deleteFailed: 'Failed to delete',
|
||||||
|
deleted: 'Embedding model deleted',
|
||||||
|
addTitle: 'Add Embedding Model',
|
||||||
|
addHint: 'Pick a configured provider and enter an embedding model name it supports (e.g. text-embedding-3-small, bge-m3).',
|
||||||
|
provider: 'Provider',
|
||||||
|
selectProvider: 'Select a provider',
|
||||||
|
modelName: 'Model Name',
|
||||||
|
modelNamePlaceholder: 'e.g. text-embedding-3-small',
|
||||||
|
displayName: 'Display Name',
|
||||||
|
displayNamePlaceholder: 'Defaults to the model name',
|
||||||
|
add: 'Add',
|
||||||
|
adding: 'Adding...',
|
||||||
|
added: 'Embedding model added',
|
||||||
|
addFailed: 'Failed to add',
|
||||||
|
nameRequired: 'Select a provider and enter a model name',
|
||||||
|
noProviders: 'Enable and configure at least one provider above first.',
|
||||||
|
},
|
||||||
// Inline API Key on the provider card — primary entry point for the
|
// Inline API Key on the provider card — primary entry point for the
|
||||||
// 90% case (paste a key, hit save). Modal stays for advanced fields.
|
// 90% case (paste a key, hit save). Modal stays for advanced fields.
|
||||||
inlineApiKeyTitle: 'API Key',
|
inlineApiKeyTitle: 'API Key',
|
||||||
|
|||||||
@ -538,6 +538,36 @@ export default {
|
|||||||
editTitle: '提供商配置',
|
editTitle: '提供商配置',
|
||||||
manageTitle: '管理模型',
|
manageTitle: '管理模型',
|
||||||
addModel: '新增模型',
|
addModel: '新增模型',
|
||||||
|
embedding: {
|
||||||
|
title: 'Embedding 模型',
|
||||||
|
hint: '知识库语义检索使用,与 Chat 模型共享 Provider 的 API Key',
|
||||||
|
loading: '加载中...',
|
||||||
|
empty: '暂无 Embedding 模型,使用下方表单添加。',
|
||||||
|
defaultBadge: '默认',
|
||||||
|
builtinBadge: '内置',
|
||||||
|
test: '测试连通性',
|
||||||
|
testing: '测试中...',
|
||||||
|
testPassed: '测试通过 · 维度 {dim}',
|
||||||
|
setDefault: '设为系统默认',
|
||||||
|
delete: '删除',
|
||||||
|
deleteConfirm: '确认删除 Embedding 模型“{name}”?',
|
||||||
|
deleteFailed: '删除失败',
|
||||||
|
deleted: 'Embedding 模型已删除',
|
||||||
|
addTitle: '新增 Embedding 模型',
|
||||||
|
addHint: '选择一个已配置的提供商,填入它支持的 Embedding 模型名称(如 text-embedding-3-small、bge-m3)。',
|
||||||
|
provider: '提供商',
|
||||||
|
selectProvider: '请选择提供商',
|
||||||
|
modelName: '模型名称',
|
||||||
|
modelNamePlaceholder: '例如 text-embedding-3-small',
|
||||||
|
displayName: '显示名称',
|
||||||
|
displayNamePlaceholder: '留空则使用模型名称',
|
||||||
|
add: '添加',
|
||||||
|
adding: '添加中...',
|
||||||
|
added: 'Embedding 模型已添加',
|
||||||
|
addFailed: '添加失败',
|
||||||
|
nameRequired: '请选择提供商并填写模型名称',
|
||||||
|
noProviders: '请先在上方启用并配置至少一个提供商。',
|
||||||
|
},
|
||||||
// Inline API Key on the provider card — primary entry point for the
|
// Inline API Key on the provider card — primary entry point for the
|
||||||
// 90% case (paste a key, hit save). Modal stays for advanced fields.
|
// 90% case (paste a key, hit save). Modal stays for advanced fields.
|
||||||
inlineApiKeyTitle: 'API Key',
|
inlineApiKeyTitle: 'API Key',
|
||||||
|
|||||||
@ -9,60 +9,128 @@
|
|||||||
<line x1="14.83" y1="9.17" x2="19.07" y2="4.93"/>
|
<line x1="14.83" y1="9.17" x2="19.07" y2="4.93"/>
|
||||||
<line x1="4.93" y1="19.07" x2="9.17" y2="14.83"/>
|
<line x1="4.93" y1="19.07" x2="9.17" y2="14.83"/>
|
||||||
</svg>
|
</svg>
|
||||||
Embedding 模型
|
{{ t('settings.model.embedding.title') }}
|
||||||
<span class="group-hint">知识库语义检索使用,与 Chat 模型共享 Provider 的 API Key</span>
|
<span class="group-hint">{{ t('settings.model.embedding.hint') }}</span>
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<div v-if="loading" class="loading-state">加载中...</div>
|
<div v-if="loading" class="loading-state">{{ t('settings.model.embedding.loading') }}</div>
|
||||||
<div v-else-if="models.length === 0" class="empty-state">
|
|
||||||
暂无可用的 Embedding 模型。系统已预置 DashScope Text Embedding v3/v2,
|
|
||||||
请在"云端模型"下的 <strong>DashScope</strong> Provider 中配置 API Key。
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else class="embedding-grid">
|
<template v-else>
|
||||||
<div v-for="model in models" :key="model.id" class="embedding-card">
|
<div v-if="models.length === 0" class="empty-state">
|
||||||
<div class="embedding-card-header">
|
{{ t('settings.model.embedding.empty') }}
|
||||||
<div class="embedding-name">
|
</div>
|
||||||
{{ model.name }}
|
|
||||||
<span v-if="String(model.id) === defaultModelId" class="default-badge">默认</span>
|
<div v-else class="embedding-grid">
|
||||||
|
<div v-for="model in models" :key="model.id" class="embedding-card">
|
||||||
|
<div class="embedding-card-header">
|
||||||
|
<div class="embedding-name">
|
||||||
|
{{ model.name }}
|
||||||
|
<span v-if="String(model.id) === defaultModelId" class="default-badge">
|
||||||
|
{{ t('settings.model.embedding.defaultBadge') }}
|
||||||
|
</span>
|
||||||
|
<span v-if="model.builtin" class="builtin-badge">
|
||||||
|
{{ t('settings.model.embedding.builtinBadge') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span class="provider-badge">{{ model.provider }}</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="provider-badge">{{ model.provider }}</span>
|
<div class="embedding-model-id">{{ model.modelName }}</div>
|
||||||
</div>
|
<div v-if="model.description" class="embedding-desc">{{ model.description }}</div>
|
||||||
<div class="embedding-model-id">{{ model.modelName }}</div>
|
|
||||||
<div v-if="model.description" class="embedding-desc">{{ model.description }}</div>
|
|
||||||
|
|
||||||
<!-- 测试结果 -->
|
<!-- Connectivity test result -->
|
||||||
<div v-if="testResults[String(model.id)]" class="test-result" :class="testResults[String(model.id)].success ? 'success' : 'error'">
|
<div
|
||||||
<span v-if="testResults[String(model.id)].success">
|
v-if="testResults[String(model.id)]"
|
||||||
✓ 测试通过 · 维度 {{ testResults[String(model.id)].dimensions }}
|
class="test-result"
|
||||||
</span>
|
:class="testResults[String(model.id)].success ? 'success' : 'error'"
|
||||||
<span v-else>✗ {{ testResults[String(model.id)].message }}</span>
|
>
|
||||||
</div>
|
<span v-if="testResults[String(model.id)].success">
|
||||||
|
✓ {{ t('settings.model.embedding.testPassed', { dim: testResults[String(model.id)].dimensions }) }}
|
||||||
|
</span>
|
||||||
|
<span v-else>✗ {{ testResults[String(model.id)].message }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="embedding-actions">
|
<div class="embedding-actions">
|
||||||
<button
|
<button
|
||||||
class="card-btn test-btn"
|
class="card-btn test-btn"
|
||||||
:disabled="testingId === String(model.id)"
|
:disabled="testingId === String(model.id)"
|
||||||
@click="onTest(model)"
|
@click="onTest(model)"
|
||||||
>
|
>
|
||||||
{{ testingId === String(model.id) ? '测试中...' : '测试连通性' }}
|
{{ testingId === String(model.id) ? t('settings.model.embedding.testing') : t('settings.model.embedding.test') }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="String(model.id) !== defaultModelId"
|
v-if="String(model.id) !== defaultModelId"
|
||||||
class="card-btn"
|
class="card-btn"
|
||||||
@click="onSetDefault(model)"
|
@click="onSetDefault(model)"
|
||||||
>
|
>
|
||||||
设为系统默认
|
{{ t('settings.model.embedding.setDefault') }}
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="!model.builtin"
|
||||||
|
class="card-btn danger"
|
||||||
|
@click="onDelete(model)"
|
||||||
|
>
|
||||||
|
{{ t('settings.model.embedding.delete') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
<!-- Add embedding model -->
|
||||||
|
<div class="embedding-add-box">
|
||||||
|
<div class="add-box-title">{{ t('settings.model.embedding.addTitle') }}</div>
|
||||||
|
<div class="add-box-hint">{{ t('settings.model.embedding.addHint') }}</div>
|
||||||
|
|
||||||
|
<div v-if="providers.length === 0" class="no-providers">
|
||||||
|
{{ t('settings.model.embedding.noProviders') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<div class="form-grid">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">{{ t('settings.model.embedding.provider') }}</label>
|
||||||
|
<select v-model="form.providerId" class="form-input">
|
||||||
|
<option value="" disabled>{{ t('settings.model.embedding.selectProvider') }}</option>
|
||||||
|
<option v-for="p in providers" :key="p.id" :value="p.id">{{ p.name }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">{{ t('settings.model.embedding.modelName') }}</label>
|
||||||
|
<input
|
||||||
|
v-model.trim="form.modelName"
|
||||||
|
class="form-input"
|
||||||
|
:placeholder="t('settings.model.embedding.modelNamePlaceholder')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">{{ t('settings.model.embedding.displayName') }}</label>
|
||||||
|
<input
|
||||||
|
v-model.trim="form.displayName"
|
||||||
|
class="form-input"
|
||||||
|
:placeholder="t('settings.model.embedding.displayNamePlaceholder')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="add-actions">
|
||||||
|
<button
|
||||||
|
class="btn-primary"
|
||||||
|
:disabled="adding || !form.providerId || !form.modelName"
|
||||||
|
@click="onAdd"
|
||||||
|
>
|
||||||
|
{{ adding ? t('settings.model.embedding.adding') : t('settings.model.embedding.add') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, ref } from 'vue'
|
import { onMounted, reactive, ref } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
import { modelApi } from '@/api'
|
import { modelApi } from '@/api'
|
||||||
|
import { mcToast } from '@/composables/useMcToast'
|
||||||
|
import { mcConfirm } from '@/components/common/useConfirm'
|
||||||
|
|
||||||
interface EmbeddingModel {
|
interface EmbeddingModel {
|
||||||
id: string | number
|
id: string | number
|
||||||
@ -72,23 +140,41 @@ interface EmbeddingModel {
|
|||||||
description?: string
|
description?: string
|
||||||
enabled?: boolean
|
enabled?: boolean
|
||||||
isDefault?: boolean
|
isDefault?: boolean
|
||||||
|
builtin?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ProviderOption {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
const models = ref<EmbeddingModel[]>([])
|
const models = ref<EmbeddingModel[]>([])
|
||||||
|
const providers = ref<ProviderOption[]>([])
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const defaultModelId = ref<string>('')
|
const defaultModelId = ref<string>('')
|
||||||
const testingId = ref<string>('')
|
const testingId = ref<string>('')
|
||||||
|
const adding = ref(false)
|
||||||
const testResults = ref<Record<string, { success: boolean; dimensions?: number; message?: string }>>({})
|
const testResults = ref<Record<string, { success: boolean; dimensions?: number; message?: string }>>({})
|
||||||
|
|
||||||
|
const form = reactive({
|
||||||
|
providerId: '',
|
||||||
|
modelName: '',
|
||||||
|
displayName: '',
|
||||||
|
})
|
||||||
|
|
||||||
async function loadAll() {
|
async function loadAll() {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const [listRes, defaultRes] = await Promise.all([
|
const [listRes, defaultRes, providerRes] = await Promise.all([
|
||||||
modelApi.listByType('embedding'),
|
modelApi.listByType('embedding'),
|
||||||
modelApi.getDefaultEmbedding(),
|
modelApi.getDefaultEmbedding(),
|
||||||
|
modelApi.listProviders(),
|
||||||
])
|
])
|
||||||
models.value = (listRes.data as any[]) || []
|
models.value = (listRes.data as EmbeddingModel[]) || []
|
||||||
defaultModelId.value = String((defaultRes.data as any)?.defaultModelId || '')
|
defaultModelId.value = String((defaultRes.data as any)?.defaultModelId || '')
|
||||||
|
providers.value = ((providerRes.data as any[]) || []).map(p => ({ id: p.id, name: p.name }))
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error('[EmbeddingModels] Load failed:', e?.message)
|
console.error('[EmbeddingModels] Load failed:', e?.message)
|
||||||
} finally {
|
} finally {
|
||||||
@ -109,7 +195,7 @@ async function onTest(model: EmbeddingModel) {
|
|||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
testResults.value[String(model.id)] = {
|
testResults.value[String(model.id)] = {
|
||||||
success: false,
|
success: false,
|
||||||
message: e?.message || '请求失败',
|
message: e?.message || t('settings.model.embedding.addFailed'),
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
testingId.value = ''
|
testingId.value = ''
|
||||||
@ -121,7 +207,58 @@ async function onSetDefault(model: EmbeddingModel) {
|
|||||||
await modelApi.setDefaultEmbedding(model.id)
|
await modelApi.setDefaultEmbedding(model.id)
|
||||||
defaultModelId.value = String(model.id)
|
defaultModelId.value = String(model.id)
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error('[EmbeddingModels] Set default failed:', e?.message)
|
mcToast.error(e?.message || t('settings.model.embedding.addFailed'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onAdd() {
|
||||||
|
const modelName = form.modelName.trim()
|
||||||
|
if (!form.providerId || !modelName) {
|
||||||
|
mcToast.error(t('settings.model.embedding.nameRequired'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
adding.value = true
|
||||||
|
try {
|
||||||
|
await modelApi.create({
|
||||||
|
name: form.displayName.trim() || modelName,
|
||||||
|
provider: form.providerId,
|
||||||
|
modelName,
|
||||||
|
modelType: 'embedding',
|
||||||
|
description: '',
|
||||||
|
builtin: false,
|
||||||
|
isDefault: false,
|
||||||
|
enabled: true,
|
||||||
|
})
|
||||||
|
form.modelName = ''
|
||||||
|
form.displayName = ''
|
||||||
|
mcToast.success(t('settings.model.embedding.added'))
|
||||||
|
await loadAll()
|
||||||
|
} catch (e: any) {
|
||||||
|
mcToast.error(e?.message || t('settings.model.embedding.addFailed'))
|
||||||
|
} finally {
|
||||||
|
adding.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onDelete(model: EmbeddingModel) {
|
||||||
|
const ok = await mcConfirm({
|
||||||
|
title: t('common.confirm'),
|
||||||
|
message: t('settings.model.embedding.deleteConfirm', { name: model.name }),
|
||||||
|
confirmText: t('settings.model.embedding.delete'),
|
||||||
|
tone: 'danger',
|
||||||
|
})
|
||||||
|
if (!ok) return
|
||||||
|
try {
|
||||||
|
await modelApi.delete(model.id)
|
||||||
|
// The deleted model may still be referenced as the system default — clear
|
||||||
|
// it so wiki semantic search falls back instead of resolving a dead id.
|
||||||
|
if (String(model.id) === defaultModelId.value) {
|
||||||
|
await modelApi.setDefaultEmbedding('')
|
||||||
|
}
|
||||||
|
mcToast.success(t('settings.model.embedding.deleted'))
|
||||||
|
await loadAll()
|
||||||
|
} catch (e: any) {
|
||||||
|
mcToast.error(e?.message || t('settings.model.embedding.deleteFailed'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -162,7 +299,6 @@ defineExpose({ refresh: loadAll })
|
|||||||
background: var(--mc-bg-sunken);
|
background: var(--mc-bg-sunken);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
.empty-state strong { color: var(--mc-primary); }
|
|
||||||
|
|
||||||
.embedding-grid {
|
.embedding-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
@ -199,6 +335,14 @@ defineExpose({ refresh: loadAll })
|
|||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
.builtin-badge {
|
||||||
|
font-size: 10px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
background: var(--mc-bg-sunken);
|
||||||
|
color: var(--mc-text-tertiary);
|
||||||
|
border-radius: 4px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
.provider-badge {
|
.provider-badge {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
padding: 2px 8px;
|
padding: 2px 8px;
|
||||||
@ -251,4 +395,74 @@ defineExpose({ refresh: loadAll })
|
|||||||
color: var(--mc-primary);
|
color: var(--mc-primary);
|
||||||
border-color: var(--mc-primary);
|
border-color: var(--mc-primary);
|
||||||
}
|
}
|
||||||
|
.card-btn.danger {
|
||||||
|
background: var(--mc-danger-bg);
|
||||||
|
color: var(--mc-danger);
|
||||||
|
border-color: var(--mc-danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
.embedding-add-box {
|
||||||
|
margin-top: 16px;
|
||||||
|
padding: 16px;
|
||||||
|
border: 1px dashed var(--mc-border);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--mc-bg-sunken);
|
||||||
|
}
|
||||||
|
.add-box-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--mc-text-primary);
|
||||||
|
}
|
||||||
|
.add-box-hint {
|
||||||
|
margin-top: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--mc-text-tertiary);
|
||||||
|
}
|
||||||
|
.no-providers {
|
||||||
|
margin-top: 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--mc-text-secondary);
|
||||||
|
}
|
||||||
|
.form-grid {
|
||||||
|
margin-top: 12px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.form-label {
|
||||||
|
display: block;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--mc-text-secondary);
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
.form-input {
|
||||||
|
width: 100%;
|
||||||
|
border: 1px solid var(--mc-border);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 9px 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
background: var(--mc-bg-surface);
|
||||||
|
color: var(--mc-text-primary);
|
||||||
|
}
|
||||||
|
.form-input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--mc-primary);
|
||||||
|
box-shadow: 0 0 0 2px rgba(217, 119, 87, 0.1);
|
||||||
|
}
|
||||||
|
.add-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
.btn-primary {
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 9px 16px;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
background: var(--mc-primary);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.btn-primary:hover:not(:disabled) { background: var(--mc-primary-hover); }
|
||||||
|
.btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user