- 表单下拉框不再用 label 包裹,改为 div + aria-label,避免点击后 下拉面板瞬间关闭 - Provider 卡片新增「从上游获取模型」和「修改」入口,支持编辑 base URL、API Key 与 api_mode,凭据写入对应的环境变量 - 服务端抽出 provider-model-discovery 统一不同 api_mode 的模型发现, 旧的 custom_providers 与新的 providers 字典结构都能正确更新 - Agent Bridge 改为后台启动,HTTP 接口在 Python 运行时就绪前即可用; worker 连接被拒时按类型重试,不重放可能已产生副作用的请求 - 断线重连后恢复 profiles、models 与会话列表,健康检查改为自调度 的 setTimeout,离线时 2 秒后重试 - 批量删除确认文案改用 Popconfirm content,避免变成侧栏触发器文本
635 lines
20 KiB
Vue
635 lines
20 KiB
Vue
<script setup lang="ts">
|
||
import { ref, computed } from 'vue'
|
||
import { useMessage } from '@/composables/useMessage'
|
||
import { useDialog } from '@/composables/useDialog'
|
||
import type { AvailableModelGroup } from '@/api/hermes/system'
|
||
import { useModelsStore } from '@/stores/hermes/models'
|
||
import { useAppStore } from '@/stores/hermes/app'
|
||
import { useChatStore } from '@/stores/hermes/chat'
|
||
import ProviderEditModal from './ProviderEditModal.vue'
|
||
import { fetchProviderModels } from '@/api/hermes/system'
|
||
import { checkCopilotToken, disableCopilot } from '@/api/hermes/copilot-auth'
|
||
import { useI18n } from 'vue-i18n'
|
||
|
||
const props = defineProps<{ provider: AvailableModelGroup }>()
|
||
|
||
const { t } = useI18n()
|
||
const modelsStore = useModelsStore()
|
||
const appStore = useAppStore()
|
||
const chatStore = useChatStore()
|
||
const message = useMessage()
|
||
const dialog = useDialog()
|
||
|
||
const isCustomProviderKey = computed(() => props.provider.provider.startsWith('custom:'))
|
||
const isCustom = computed(() => !props.provider.builtin && isCustomProviderKey.value)
|
||
const isConfigBackedProvider = computed(() => isCustomProviderKey.value || (!props.provider.builtin && !!props.provider.provider_source))
|
||
const isCopilot = computed(() => props.provider.provider === 'copilot')
|
||
const displayName = computed(() => props.provider.label)
|
||
const deleting = ref(false)
|
||
const showEditModal = ref(false)
|
||
const fetchingUpstream = ref(false)
|
||
const canEditProvider = computed(() => isConfigBackedProvider.value || props.provider.credentials_editable || !!props.provider.base_url_env)
|
||
|
||
async function handleFetchUpstream() {
|
||
if (fetchingUpstream.value) return
|
||
fetchingUpstream.value = true
|
||
try {
|
||
const result = await fetchProviderModels({
|
||
provider: props.provider.provider,
|
||
label: props.provider.label,
|
||
base_url: props.provider.base_url,
|
||
api_key: props.provider.api_key,
|
||
update_cache: true,
|
||
api_mode: props.provider.api_mode,
|
||
freeOnly: props.provider.provider === 'openrouter',
|
||
})
|
||
await modelsStore.fetchProviders()
|
||
await appStore.reloadModels()
|
||
message.success(t('models.upstreamModelsFetched', { count: result.models.length }))
|
||
} catch (e: any) {
|
||
message.error(e?.message || t('models.upstreamModelsFailed'))
|
||
} finally {
|
||
fetchingUpstream.value = false
|
||
}
|
||
}
|
||
const destructiveActionLabel = computed(() => {
|
||
if (isConfigBackedProvider.value) return t('common.delete')
|
||
if (isCopilot.value) return t('models.disableProvider')
|
||
return t('models.clearProviderCredentials')
|
||
})
|
||
const destructiveActionTitle = computed(() => {
|
||
if (isConfigBackedProvider.value) return t('models.deleteProvider')
|
||
if (isCopilot.value) return t('models.disableProvider')
|
||
return t('models.clearProviderCredentials')
|
||
})
|
||
|
||
function destructiveConfirmContent(copilotMsg: string) {
|
||
if (isConfigBackedProvider.value) return t('models.deleteConfirm', { name: displayName.value })
|
||
if (isCopilot.value) {
|
||
const base = t('models.disableProviderConfirm', { name: displayName.value })
|
||
return copilotMsg ? `${base}\n\n${copilotMsg}` : base
|
||
}
|
||
return t('models.clearCredentialsConfirm', { name: displayName.value })
|
||
}
|
||
|
||
const showAliasListModal = ref(false)
|
||
const showAliasModal = ref(false)
|
||
const aliasProvider = ref('')
|
||
const aliasModel = ref('')
|
||
const aliasInput = ref('')
|
||
|
||
const showVisibilityModal = ref(false)
|
||
const visibilitySaving = ref(false)
|
||
const selectedVisibleModels = ref<string[]>([])
|
||
const defaultingProvider = ref(false)
|
||
const defaultingModel = ref<string | null>(null)
|
||
|
||
const sourceProvider = computed(() => modelsStore.allProviders.find(g => g.provider === props.provider.provider))
|
||
const allModels = computed(() => props.provider.available_models?.length ? props.provider.available_models : (sourceProvider.value?.models?.length ? sourceProvider.value.models : props.provider.models))
|
||
const visibilityRule = computed(() => appStore.getProviderVisibility(props.provider.provider))
|
||
const isFiltered = computed(() => visibilityRule.value.mode === 'include')
|
||
const visibleCountLabel = computed(() => `${props.provider.models.length}/${allModels.value.length}`)
|
||
const isDefaultProvider = computed(() => modelsStore.defaultProvider === props.provider.provider)
|
||
const defaultModelOptions = computed(() => props.provider.models.map(model => ({
|
||
label: modelDisplayName(model),
|
||
value: model,
|
||
})))
|
||
const defaultModelSelectValue = computed(() => isDefaultProvider.value ? modelsStore.defaultModel || null : null)
|
||
const previewModels = computed(() => {
|
||
const firstModels = props.provider.models.slice(0, 20)
|
||
const currentDefault = modelsStore.defaultModel
|
||
if (isDefaultProvider.value && currentDefault && props.provider.models.includes(currentDefault) && !firstModels.includes(currentDefault)) {
|
||
return [currentDefault, ...firstModels.slice(0, 19)]
|
||
}
|
||
return firstModels
|
||
})
|
||
const hiddenModelCount = computed(() => Math.max(props.provider.models.length - previewModels.value.length, 0))
|
||
|
||
function isDefaultModel(model: string) {
|
||
return isDefaultProvider.value && modelsStore.defaultModel === model
|
||
}
|
||
|
||
function modelAlias(model: string) {
|
||
return appStore.getModelAlias(model, props.provider.provider)
|
||
}
|
||
|
||
function modelDisplayName(model: string) {
|
||
return appStore.displayModelName(model, props.provider.provider)
|
||
}
|
||
|
||
function openAliasEditor(model: string) {
|
||
aliasProvider.value = props.provider.provider
|
||
aliasModel.value = model
|
||
aliasInput.value = appStore.getModelAlias(model, props.provider.provider)
|
||
showAliasModal.value = true
|
||
}
|
||
|
||
async function handleSetDefaultProvider() {
|
||
if (isDefaultProvider.value) return
|
||
|
||
defaultingProvider.value = true
|
||
try {
|
||
await modelsStore.setDefaultProvider(props.provider.provider)
|
||
message.success(t('models.defaultProviderUpdated'))
|
||
} catch (e: any) {
|
||
message.error(e?.message || t('models.defaultProviderUpdateFailed'))
|
||
} finally {
|
||
defaultingProvider.value = false
|
||
}
|
||
}
|
||
|
||
async function handleSetDefaultModel(model: string | null) {
|
||
if (!model) return
|
||
if (isDefaultModel(model)) return
|
||
|
||
defaultingModel.value = model
|
||
try {
|
||
await modelsStore.setDefaultModel(model, props.provider.provider)
|
||
message.success(t('models.defaultModelUpdated'))
|
||
} catch (e: any) {
|
||
message.error(e?.message || t('models.defaultModelUpdateFailed'))
|
||
} finally {
|
||
if (defaultingModel.value === model) defaultingModel.value = null
|
||
}
|
||
}
|
||
|
||
async function saveAlias() {
|
||
if (!aliasModel.value || !aliasProvider.value) return
|
||
try {
|
||
await appStore.setModelAlias(aliasModel.value, aliasProvider.value, aliasInput.value)
|
||
showAliasModal.value = false
|
||
} catch (e: any) {
|
||
message.error(e.message || t('models.aliasSaveFailed'))
|
||
}
|
||
}
|
||
|
||
async function clearAlias() {
|
||
aliasInput.value = ''
|
||
await saveAlias()
|
||
}
|
||
|
||
function openVisibilityModal() {
|
||
const rule = appStore.getProviderVisibility(props.provider.provider)
|
||
selectedVisibleModels.value = rule.mode === 'include' ? allModels.value.filter(m => rule.models.includes(m)) : [...allModels.value]
|
||
showVisibilityModal.value = true
|
||
}
|
||
|
||
async function handleVisibilitySave() {
|
||
if (selectedVisibleModels.value.length === 0) {
|
||
message.error(t('models.visibilitySelectOne'))
|
||
return
|
||
}
|
||
visibilitySaving.value = true
|
||
try {
|
||
const selected = selectedVisibleModels.value.filter(m => allModels.value.includes(m))
|
||
const mode = selected.length === allModels.value.length ? 'all' : 'include'
|
||
await appStore.setModelVisibility(props.provider.provider, { mode, models: selected })
|
||
await modelsStore.fetchProviders()
|
||
showVisibilityModal.value = false
|
||
message.success(t('models.visibilitySaved'))
|
||
} catch (e: any) {
|
||
message.error(e.message || t('models.visibilitySaveFailed'))
|
||
} finally {
|
||
visibilitySaving.value = false
|
||
}
|
||
}
|
||
|
||
function resetVisibility() {
|
||
selectedVisibleModels.value = [...allModels.value]
|
||
}
|
||
|
||
function clearVisibility() {
|
||
selectedVisibleModels.value = []
|
||
}
|
||
|
||
async function handleDelete() {
|
||
let copilotMsg = ''
|
||
if (isCopilot.value) {
|
||
// 提前查 source,让用户清楚移除会不会影响 VS Code/gh CLI 等其他工具的登录态
|
||
try {
|
||
const status = await checkCopilotToken()
|
||
if (status.source === 'env') copilotMsg = t('models.copilotDeleteHintEnv')
|
||
else if (status.source === 'gh-cli') copilotMsg = t('models.copilotDeleteHintGhCli')
|
||
else if (status.source === 'apps-json') copilotMsg = t('models.copilotDeleteHintAppsJson')
|
||
} catch { /* ignore — fall back to generic confirm copy */ }
|
||
}
|
||
dialog.warning({
|
||
title: destructiveActionTitle.value,
|
||
content: destructiveConfirmContent(copilotMsg),
|
||
positiveText: destructiveActionLabel.value,
|
||
negativeText: t('common.cancel'),
|
||
onPositiveClick: async () => {
|
||
deleting.value = true
|
||
try {
|
||
if (isCopilot.value) {
|
||
// Copilot 走显式 opt-in 模型:disable 把 enabled 置 false,
|
||
// 仅当 token 来自 ~/.hermes/.env 时才清掉,gh-cli / apps.json 不动。
|
||
await disableCopilot()
|
||
// 服务端会在默认模型属于 copilot 时清掉 model.default,这里再清理本地
|
||
// 会话级 model/provider,避免 Chat 页继续显示已下架的 copilot 模型。
|
||
chatStore.clearProviderFromSessions('copilot')
|
||
await modelsStore.fetchProviders()
|
||
} else {
|
||
await modelsStore.removeProvider(props.provider.provider, {
|
||
source: props.provider.provider_source,
|
||
providerKey: props.provider.provider_key,
|
||
})
|
||
}
|
||
// 删完之后若已没有默认模型,自动从剩余 provider 里挑一个,避免 chat 页
|
||
// "无默认模型"的尴尬态。与 hermes CLI `model` 子命令的隐含行为对齐。
|
||
if (!appStore.selectedModel && appStore.modelGroups.length > 0) {
|
||
const first = appStore.modelGroups.find(g => g.models.length > 0)
|
||
if (first) {
|
||
await appStore.switchModel(first.models[0], first.provider)
|
||
}
|
||
}
|
||
message.success(isConfigBackedProvider.value
|
||
? t('models.providerDeleted')
|
||
: isCopilot.value
|
||
? t('models.providerDisabled')
|
||
: t('models.providerCredentialsCleared'))
|
||
} catch (e: any) {
|
||
message.error(e.message)
|
||
} finally {
|
||
deleting.value = false
|
||
}
|
||
},
|
||
})
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<div class="cy-panel provider-card">
|
||
<div class="cy-panel-h">
|
||
<h3 class="provider-name">{{ displayName }}</h3>
|
||
<span class="cy-tag">{{ isCustom ? t('models.customType') : t('models.builtIn') }}</span>
|
||
<div class="cy-panel-actions card-header-actions">
|
||
<span v-if="isDefaultProvider" class="cy-tag acc">{{ t('models.currentDefault') }}</span>
|
||
<a-button
|
||
v-else
|
||
size="mini"
|
||
type="text"
|
||
:loading="defaultingProvider"
|
||
@click="handleSetDefaultProvider"
|
||
>
|
||
{{ t('models.setDefaultProvider') }}
|
||
</a-button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="cy-panel-b">
|
||
<div class="cy-kv">
|
||
<div class="cy-kr">
|
||
<span class="cy-kk">{{ t('models.provider') }}</span>
|
||
<span class="cy-kv-v"><code class="cy-code">{{ provider.provider }}</code></span>
|
||
</div>
|
||
<div class="cy-kr">
|
||
<span class="cy-kk">{{ t('models.baseUrl') }}</span>
|
||
<span class="cy-kv-v"><code class="cy-code">{{ provider.base_url }}</code></span>
|
||
</div>
|
||
<div class="cy-kr">
|
||
<span class="cy-kk">{{ t('models.models') }}</span>
|
||
<span class="cy-kv-v">{{ isFiltered ? visibleCountLabel : provider.models.length }} {{ t('models.count') }}</span>
|
||
</div>
|
||
<div class="cy-kr">
|
||
<span class="cy-kk">{{ t('models.defaultModel') }}</span>
|
||
<span class="cy-kv-v">
|
||
<a-select
|
||
class="default-model-select"
|
||
size="mini"
|
||
allow-search
|
||
:model-value="defaultModelSelectValue ?? undefined"
|
||
:options="defaultModelOptions"
|
||
:placeholder="t('models.selectModel')"
|
||
:disabled="provider.models.length === 0"
|
||
:loading="defaultingModel !== null"
|
||
@change="handleSetDefaultModel($event as string | null)"
|
||
/>
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="models-list">
|
||
<button
|
||
v-for="model in previewModels"
|
||
:key="model"
|
||
class="cy-tag model-tag"
|
||
:class="{ acc: isDefaultModel(model) }"
|
||
type="button"
|
||
:title="t('models.aliasTitleFor', { model })"
|
||
@click="openAliasEditor(model)"
|
||
>
|
||
<span class="model-tag-name">{{ modelDisplayName(model) }}</span>
|
||
<span v-if="isDefaultModel(model)" class="model-tag-default">{{ t('models.defaultShort') }}</span>
|
||
<span v-if="modelAlias(model)" class="model-tag-id">{{ model }}</span>
|
||
</button>
|
||
<span v-if="hiddenModelCount > 0" class="cy-tag model-tag-more">
|
||
+{{ hiddenModelCount }} {{ t('models.more') }}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="cy-panel-b card-actions">
|
||
<a-button size="mini" type="text" :loading="fetchingUpstream" :disabled="!provider.base_url || showEditModal" @click="handleFetchUpstream">{{ t('models.fetchUpstreamModels') }}</a-button>
|
||
<a-button v-if="canEditProvider" size="mini" type="text" :disabled="fetchingUpstream || deleting" @click="showEditModal = true">{{ t('common.edit') }}</a-button>
|
||
<a-button size="mini" type="text" @click="showAliasListModal = true">{{ t('models.aliasManage') }}</a-button>
|
||
<a-button size="mini" type="text" @click="openVisibilityModal">{{ t('models.manageVisibleModels') }}</a-button>
|
||
<a-button size="mini" type="text" status="danger" :loading="deleting" @click="handleDelete">{{ destructiveActionLabel }}</a-button>
|
||
</div>
|
||
|
||
<ProviderEditModal v-if="showEditModal" :provider="provider" @close="showEditModal = false" />
|
||
|
||
<a-modal
|
||
v-model:visible="showAliasListModal"
|
||
:footer="false"
|
||
:title="t('models.aliasManageFor', { provider: displayName })"
|
||
width="min(560px, calc(100vw - 32px))"
|
||
:mask-closable="true"
|
||
>
|
||
<div class="alias-list-hint">{{ t('models.aliasHint') }}</div>
|
||
<div class="alias-list">
|
||
<div v-for="model in provider.models" :key="model" class="alias-row">
|
||
<div class="alias-row-text">
|
||
<span class="alias-row-name">{{ modelDisplayName(model) }}</span>
|
||
<span v-if="isDefaultModel(model)" class="alias-row-default">{{ t('models.defaultShort') }}</span>
|
||
<code class="alias-row-id">{{ model }}</code>
|
||
</div>
|
||
<a-button size="mini" type="text" @click="openAliasEditor(model)">{{ t('models.aliasEdit') }}</a-button>
|
||
</div>
|
||
</div>
|
||
</a-modal>
|
||
|
||
<a-modal
|
||
v-model:visible="showAliasModal"
|
||
:title="aliasModel ? t('models.aliasTitleFor', { model: aliasModel }) : t('models.aliasTitle')"
|
||
width="min(420px, calc(100vw - 32px))"
|
||
:mask-closable="true"
|
||
>
|
||
<a-input
|
||
v-model="aliasInput"
|
||
:placeholder="t('models.aliasPlaceholder')"
|
||
allow-clear
|
||
@keydown.enter="saveAlias"
|
||
/>
|
||
<div v-if="aliasModel" class="model-alias-canonical">
|
||
{{ t('models.aliasCanonical', { model: aliasModel }) }}
|
||
</div>
|
||
<div class="model-alias-hint">{{ t('models.aliasHint') }}</div>
|
||
<template #footer>
|
||
<div class="model-alias-actions">
|
||
<a-button type="text" :disabled="!appStore.getModelAlias(aliasModel, aliasProvider)" @click="clearAlias">
|
||
{{ t('models.aliasUseOriginal') }}
|
||
</a-button>
|
||
<div class="model-alias-spacer" />
|
||
<a-button @click="showAliasModal = false">{{ t('common.cancel') }}</a-button>
|
||
<a-button type="primary" @click="saveAlias">{{ t('common.save') }}</a-button>
|
||
</div>
|
||
</template>
|
||
</a-modal>
|
||
|
||
<a-modal
|
||
v-model:visible="showVisibilityModal"
|
||
:footer="false"
|
||
:title="t('models.manageVisibleModelsFor', { name: displayName })"
|
||
width="min(560px, calc(100vw - 32px))"
|
||
:mask-closable="!visibilitySaving"
|
||
>
|
||
<p class="visibility-hint">{{ t('models.visibilityHint') }}</p>
|
||
<div class="visibility-count">
|
||
{{ selectedVisibleModels.length }}/{{ allModels.length }} {{ t('models.count') }}
|
||
</div>
|
||
<div class="visibility-list">
|
||
<a-checkbox-group v-model="selectedVisibleModels">
|
||
<a-checkbox
|
||
v-for="model in allModels"
|
||
:key="model"
|
||
:value="model"
|
||
class="visibility-model"
|
||
>
|
||
<code>{{ modelDisplayName(model) }}</code>
|
||
<code v-if="modelAlias(model)" class="visibility-model-id">{{ model }}</code>
|
||
</a-checkbox>
|
||
</a-checkbox-group>
|
||
</div>
|
||
<div class="visibility-actions">
|
||
<a-button size="small" type="text" :disabled="visibilitySaving" @click="resetVisibility">
|
||
{{ t('models.showAllModels') }}
|
||
</a-button>
|
||
<a-button size="small" type="text" :disabled="visibilitySaving" @click="clearVisibility">
|
||
{{ t('models.clearVisibleModels') }}
|
||
</a-button>
|
||
<div class="visibility-action-spacer" />
|
||
<a-button size="small" :disabled="visibilitySaving" @click="showVisibilityModal = false">
|
||
{{ t('common.cancel') }}
|
||
</a-button>
|
||
<a-button size="small" type="primary" :loading="visibilitySaving" @click="handleVisibilitySave">
|
||
{{ t('common.save') }}
|
||
</a-button>
|
||
</div>
|
||
</a-modal>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped lang="scss">
|
||
@use '@/styles/variables' as *;
|
||
|
||
.provider-card {
|
||
transition: border-color $transition-fast;
|
||
|
||
&:hover {
|
||
border-color: rgba(var(--accent-primary-rgb), 0.3);
|
||
}
|
||
}
|
||
|
||
.provider-name {
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
max-width: 52%;
|
||
}
|
||
|
||
.default-model-select {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
.models-list {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 4px 6px;
|
||
margin-top: 12px;
|
||
max-height: 100px;
|
||
overflow-y: auto;
|
||
align-content: flex-start;
|
||
}
|
||
|
||
.model-tag {
|
||
max-width: 260px;
|
||
height: auto;
|
||
min-height: 20px;
|
||
padding: 2px 6px;
|
||
cursor: pointer;
|
||
text-align: left;
|
||
|
||
&:hover {
|
||
color: $text-primary;
|
||
border-color: rgba(var(--accent-primary-rgb), 0.45);
|
||
}
|
||
}
|
||
|
||
.model-tag-more {
|
||
color: $accent-primary;
|
||
border-color: rgba(var(--accent-primary-rgb), 0.35);
|
||
background: var(--accent-soft);
|
||
}
|
||
|
||
.model-tag-name,
|
||
.model-tag-id {
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.model-tag-id {
|
||
color: $text-faint;
|
||
font-size: 9.5px;
|
||
}
|
||
|
||
.model-tag-default,
|
||
.alias-row-default {
|
||
color: $warning;
|
||
font-family: $font-ui;
|
||
font-size: 10px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.card-actions {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
padding: 9px 12px;
|
||
border-top: 1px solid $border-light;
|
||
}
|
||
|
||
.alias-list-hint,
|
||
.model-alias-hint {
|
||
color: $text-muted;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.alias-list-hint {
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.alias-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
max-height: 45vh;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.alias-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
padding: 8px;
|
||
border: 1px solid $border-light;
|
||
border-radius: $radius-sm;
|
||
}
|
||
|
||
.alias-row-text {
|
||
flex: 1;
|
||
min-width: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 2px;
|
||
}
|
||
|
||
.alias-row-name {
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
color: $text-primary;
|
||
font-family: $font-code;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.alias-row-id,
|
||
.model-alias-canonical {
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
color: $text-muted;
|
||
font-family: $font-code;
|
||
font-size: 11px;
|
||
}
|
||
|
||
.model-alias-canonical {
|
||
margin-top: 8px;
|
||
}
|
||
|
||
.model-alias-hint {
|
||
margin-top: 6px;
|
||
}
|
||
|
||
.model-alias-actions {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.model-alias-spacer {
|
||
flex: 1;
|
||
}
|
||
|
||
.visibility-hint {
|
||
margin: 0 0 10px;
|
||
color: $text-secondary;
|
||
font-size: 13px;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.visibility-count {
|
||
color: $text-muted;
|
||
font-size: 12px;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.visibility-list {
|
||
max-height: 360px;
|
||
overflow-y: auto;
|
||
border: 1px solid $border-light;
|
||
border-radius: $radius-sm;
|
||
padding: 8px;
|
||
}
|
||
|
||
.visibility-model {
|
||
display: flex;
|
||
width: 100%;
|
||
padding: 4px 2px;
|
||
|
||
code {
|
||
font-family: $font-code;
|
||
font-size: 12px;
|
||
color: $text-secondary;
|
||
}
|
||
}
|
||
|
||
.visibility-model-id {
|
||
margin-left: 6px;
|
||
color: $text-muted !important;
|
||
font-size: 11px !important;
|
||
}
|
||
|
||
.visibility-actions {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
margin-top: 14px;
|
||
}
|
||
|
||
.visibility-action-spacer {
|
||
flex: 1;
|
||
}
|
||
</style>
|