823 lines
22 KiB
Vue
823 lines
22 KiB
Vue
<script setup lang="ts">
|
||
import type { ModelAbilityItem } from '#/views/ai/model/modelUtils/model-ability';
|
||
|
||
import { computed, reactive, ref, watch } from 'vue';
|
||
|
||
import { EasyFlowFormModal } from '@easyflow/common-ui';
|
||
import { IconifyIcon } from '@easyflow/icons';
|
||
|
||
import {
|
||
ArrowDown,
|
||
ArrowUp,
|
||
InfoFilled,
|
||
Loading,
|
||
} from '@element-plus/icons-vue';
|
||
import {
|
||
ElForm,
|
||
ElFormItem,
|
||
ElIcon,
|
||
ElInput,
|
||
ElMessage,
|
||
ElOption,
|
||
ElSelect,
|
||
ElTooltip,
|
||
} from 'element-plus';
|
||
|
||
import { resolveModelCapabilities } from '#/api/ai/llm';
|
||
import { api } from '#/api/request';
|
||
import { $t } from '#/locales';
|
||
import {
|
||
getDefaultModelAbility,
|
||
handleTagClick as handleTagClickUtil,
|
||
syncTagSelectedStatus as syncTagSelectedStatusUtil,
|
||
} from '#/views/ai/model/modelUtils/model-ability';
|
||
import { resetModelAbility } from '#/views/ai/model/modelUtils/model-ability-utils';
|
||
|
||
type AgentHttpVersionPolicy = 'AUTO' | 'HTTP_1_1' | 'HTTP_2_PREFERRED';
|
||
type AgentSystemContentFormat = 'STRING' | 'TEXT_PARTS';
|
||
|
||
interface ModelOptions {
|
||
agentHttpVersionPolicy: AgentHttpVersionPolicy;
|
||
agentSystemContentFormat: AgentSystemContentFormat;
|
||
[key: string]: unknown;
|
||
}
|
||
|
||
interface FormData {
|
||
id?: string;
|
||
modelType: string;
|
||
title: string;
|
||
modelName: string;
|
||
groupName: string;
|
||
providerId: string;
|
||
invokeCode: string;
|
||
publishEnabled: boolean;
|
||
apiKey: string;
|
||
endpoint: string;
|
||
requestPath: string;
|
||
supportThinking: boolean | null;
|
||
supportTool: boolean | null;
|
||
supportImage: boolean | null;
|
||
supportAudio: boolean;
|
||
supportFree: boolean;
|
||
supportVideo: boolean;
|
||
supportImageB64Only: boolean;
|
||
supportToolMessage: boolean | null;
|
||
options: ModelOptions;
|
||
}
|
||
|
||
const props = defineProps({
|
||
providerId: {
|
||
type: String,
|
||
default: '',
|
||
},
|
||
});
|
||
|
||
const emit = defineEmits(['reload']);
|
||
const selectedProviderId = ref<string>(props.providerId ?? '');
|
||
|
||
watch(
|
||
() => props.providerId,
|
||
(newVal) => {
|
||
if (newVal) {
|
||
selectedProviderId.value = newVal;
|
||
}
|
||
},
|
||
{ immediate: true },
|
||
);
|
||
|
||
const formDataRef = ref();
|
||
const isAdd = ref(true);
|
||
const dialogVisible = ref(false);
|
||
const btnLoading = ref(false);
|
||
const showAdvanced = ref(false);
|
||
const capabilityLoading = ref(false);
|
||
const autoDetectedModelType = ref(false);
|
||
let capabilityRequestSequence = 0;
|
||
let manuallyEditedModelName = '';
|
||
|
||
const formData = reactive<FormData>({
|
||
modelType: '',
|
||
title: '',
|
||
modelName: '',
|
||
groupName: '',
|
||
providerId: '',
|
||
invokeCode: '',
|
||
publishEnabled: false,
|
||
apiKey: '',
|
||
endpoint: '',
|
||
requestPath: '',
|
||
supportThinking: null,
|
||
supportTool: null,
|
||
supportImage: null,
|
||
supportAudio: false,
|
||
supportFree: false,
|
||
supportVideo: false,
|
||
supportImageB64Only: false,
|
||
supportToolMessage: null,
|
||
options: {
|
||
agentHttpVersionPolicy: 'AUTO',
|
||
agentSystemContentFormat: 'STRING',
|
||
},
|
||
});
|
||
|
||
const agentHttpVersionOptions = [
|
||
{ label: '自动', value: 'AUTO' },
|
||
{ label: 'HTTP/1.1 兼容', value: 'HTTP_1_1' },
|
||
{ label: 'HTTP/2 优先', value: 'HTTP_2_PREFERRED' },
|
||
] as const;
|
||
|
||
const agentSystemContentFormatOptions = [
|
||
{ label: '字符串(默认)', value: 'STRING' },
|
||
{ label: '内容块数组', value: 'TEXT_PARTS' },
|
||
] as const;
|
||
|
||
const normalizeAgentHttpVersionPolicy = (
|
||
value?: unknown,
|
||
): AgentHttpVersionPolicy => {
|
||
if (value === 'HTTP_1_1' || value === 'HTTP_2_PREFERRED') {
|
||
return value;
|
||
}
|
||
return 'AUTO';
|
||
};
|
||
|
||
const normalizeAgentSystemContentFormat = (
|
||
value?: unknown,
|
||
): AgentSystemContentFormat => {
|
||
return value === 'TEXT_PARTS' ? 'TEXT_PARTS' : 'STRING';
|
||
};
|
||
|
||
const normalizeModelOptions = (options?: unknown): ModelOptions => {
|
||
const source =
|
||
options && typeof options === 'object' && !Array.isArray(options)
|
||
? (options as Record<string, unknown>)
|
||
: {};
|
||
return {
|
||
...source,
|
||
agentHttpVersionPolicy: normalizeAgentHttpVersionPolicy(
|
||
source.agentHttpVersionPolicy,
|
||
),
|
||
agentSystemContentFormat: normalizeAgentSystemContentFormat(
|
||
source.agentSystemContentFormat,
|
||
),
|
||
};
|
||
};
|
||
|
||
const modelAbility = ref<ModelAbilityItem[]>(getDefaultModelAbility());
|
||
const visibleModelAbility = computed(() => modelAbility.value);
|
||
type SelectableModelType = '' | 'embeddingModel' | 'rerankModel';
|
||
|
||
const selectedModelType = ref<SelectableModelType>('');
|
||
const modelTypeAbilityOptions = [
|
||
{
|
||
label: $t('llmProvider.embeddingModel'),
|
||
value: 'embeddingModel',
|
||
},
|
||
{
|
||
label: $t('llmProvider.rerankModel'),
|
||
value: 'rerankModel',
|
||
},
|
||
] as const;
|
||
const hasSpecialModelType = computed(() => Boolean(selectedModelType.value));
|
||
const abilityIconMap: Record<string, string> = {
|
||
embeddingModel: 'svg:knowledge',
|
||
rerankModel: 'svg:data-center',
|
||
thinking: 'svg:llm',
|
||
tool: 'svg:wrench',
|
||
image: 'mdi:image-outline',
|
||
};
|
||
|
||
const syncTagSelectedStatus = () => {
|
||
syncTagSelectedStatusUtil(modelAbility.value, formData);
|
||
};
|
||
|
||
const resetAbilitySelection = () => {
|
||
resetModelAbility(modelAbility.value);
|
||
formData.supportThinking = false;
|
||
formData.supportTool = false;
|
||
formData.supportImage = false;
|
||
formData.supportToolMessage = false;
|
||
syncTagSelectedStatus();
|
||
};
|
||
|
||
const markCapabilitiesAsManuallyEdited = () => {
|
||
manuallyEditedModelName = formData.modelName.trim();
|
||
capabilityRequestSequence += 1;
|
||
capabilityLoading.value = false;
|
||
};
|
||
|
||
const handleAbilityChipClick = (item: ModelAbilityItem) => {
|
||
if (hasSpecialModelType.value) {
|
||
return;
|
||
}
|
||
markCapabilitiesAsManuallyEdited();
|
||
handleTagClickUtil(item, formData);
|
||
if (item.field === 'supportTool') {
|
||
formData.supportToolMessage = formData.supportTool;
|
||
}
|
||
};
|
||
|
||
const handleModelNameInput = () => {
|
||
manuallyEditedModelName = '';
|
||
capabilityRequestSequence += 1;
|
||
capabilityLoading.value = false;
|
||
autoDetectedModelType.value = false;
|
||
selectedModelType.value = '';
|
||
formData.supportThinking = null;
|
||
formData.supportTool = null;
|
||
formData.supportImage = null;
|
||
formData.supportToolMessage = null;
|
||
syncTagSelectedStatus();
|
||
};
|
||
|
||
const handleModelTypeChipClick = (
|
||
modelType: Exclude<SelectableModelType, ''>,
|
||
) => {
|
||
const nextType = selectedModelType.value === modelType ? '' : modelType;
|
||
markCapabilitiesAsManuallyEdited();
|
||
autoDetectedModelType.value = false;
|
||
selectedModelType.value = nextType;
|
||
if (nextType) {
|
||
resetAbilitySelection();
|
||
} else {
|
||
formData.supportThinking = null;
|
||
formData.supportTool = null;
|
||
formData.supportImage = null;
|
||
formData.supportToolMessage = null;
|
||
syncTagSelectedStatus();
|
||
}
|
||
};
|
||
|
||
const getAbilityIcon = (value: string) => abilityIconMap[value] || 'svg:llm';
|
||
|
||
const resolveModelType = (): FormData['modelType'] => {
|
||
return selectedModelType.value || 'chatModel';
|
||
};
|
||
|
||
const normalizeSelectableModelType = (
|
||
modelType?: string,
|
||
): SelectableModelType => {
|
||
return modelType === 'embeddingModel' || modelType === 'rerankModel'
|
||
? modelType
|
||
: '';
|
||
};
|
||
|
||
const resetFormData = () => {
|
||
Object.assign(formData, {
|
||
id: '',
|
||
modelType: '',
|
||
title: '',
|
||
modelName: '',
|
||
groupName: '',
|
||
providerId: '',
|
||
invokeCode: '',
|
||
publishEnabled: false,
|
||
apiKey: '',
|
||
endpoint: '',
|
||
requestPath: '',
|
||
supportThinking: null,
|
||
supportTool: null,
|
||
supportAudio: false,
|
||
supportVideo: false,
|
||
supportImage: null,
|
||
supportImageB64Only: false,
|
||
supportFree: false,
|
||
supportToolMessage: null,
|
||
options: normalizeModelOptions(),
|
||
});
|
||
};
|
||
|
||
defineExpose({
|
||
openAddDialog(modelType?: string) {
|
||
isAdd.value = true;
|
||
formDataRef.value?.resetFields();
|
||
resetFormData();
|
||
showAdvanced.value = false;
|
||
autoDetectedModelType.value = false;
|
||
manuallyEditedModelName = '';
|
||
selectedModelType.value = normalizeSelectableModelType(modelType);
|
||
if (selectedModelType.value) {
|
||
resetAbilitySelection();
|
||
} else {
|
||
syncTagSelectedStatus();
|
||
}
|
||
dialogVisible.value = true;
|
||
},
|
||
|
||
openEditDialog(item: any) {
|
||
dialogVisible.value = true;
|
||
isAdd.value = false;
|
||
resetFormData();
|
||
showAdvanced.value = false;
|
||
autoDetectedModelType.value = false;
|
||
manuallyEditedModelName = '';
|
||
Object.assign(formData, {
|
||
id: item.id,
|
||
modelType: item.modelType || '',
|
||
title: item.title || '',
|
||
modelName: item.modelName || '',
|
||
groupName: item.groupName || '',
|
||
providerId: item.providerId || '',
|
||
invokeCode: item.invokeCode || '',
|
||
publishEnabled: item.publishEnabled || false,
|
||
endpoint: item.endpoint || '',
|
||
requestPath: item.requestPath || '',
|
||
apiKey: item.apiKey || '',
|
||
supportThinking: item.supportThinking ?? null,
|
||
supportAudio: item.supportAudio || false,
|
||
supportImage: item.supportImage ?? null,
|
||
supportImageB64Only: item.supportImageB64Only || false,
|
||
supportVideo: item.supportVideo || false,
|
||
supportTool: item.supportTool ?? null,
|
||
supportFree: item.supportFree || false,
|
||
supportToolMessage: item.supportToolMessage ?? null,
|
||
options: normalizeModelOptions(item.options),
|
||
});
|
||
selectedModelType.value = normalizeSelectableModelType(item.modelType);
|
||
if (selectedModelType.value) {
|
||
resetAbilitySelection();
|
||
} else {
|
||
syncTagSelectedStatus();
|
||
}
|
||
},
|
||
});
|
||
|
||
const closeDialog = () => {
|
||
capabilityRequestSequence += 1;
|
||
dialogVisible.value = false;
|
||
};
|
||
|
||
const detectModelCapabilities = async () => {
|
||
const modelName = formData.modelName.trim();
|
||
if (!modelName || manuallyEditedModelName === modelName) {
|
||
return;
|
||
}
|
||
|
||
const requestSequence = ++capabilityRequestSequence;
|
||
capabilityLoading.value = true;
|
||
try {
|
||
const providerId = isAdd.value
|
||
? selectedProviderId.value
|
||
: formData.providerId;
|
||
const res = await resolveModelCapabilities({ modelName, providerId });
|
||
if (
|
||
requestSequence !== capabilityRequestSequence ||
|
||
res.errorCode !== 0 ||
|
||
!res.data
|
||
) {
|
||
return;
|
||
}
|
||
|
||
const capability = res.data;
|
||
manuallyEditedModelName = '';
|
||
if (!capability.detected) {
|
||
if (autoDetectedModelType.value) {
|
||
selectedModelType.value = '';
|
||
}
|
||
autoDetectedModelType.value = false;
|
||
formData.supportThinking = null;
|
||
formData.supportTool = null;
|
||
formData.supportImage = null;
|
||
formData.supportToolMessage = null;
|
||
syncTagSelectedStatus();
|
||
return;
|
||
}
|
||
|
||
autoDetectedModelType.value = true;
|
||
formData.modelType = capability.modelType;
|
||
selectedModelType.value = normalizeSelectableModelType(
|
||
capability.modelType,
|
||
);
|
||
formData.supportThinking = capability.supportThinking ?? null;
|
||
formData.supportTool = capability.supportTool ?? null;
|
||
formData.supportImage = capability.supportImage ?? null;
|
||
formData.supportToolMessage = capability.supportTool ?? null;
|
||
syncTagSelectedStatus();
|
||
} catch {
|
||
// 自动识别失败不阻塞表单,保存时后端仍会再次解析能力。
|
||
} finally {
|
||
if (requestSequence === capabilityRequestSequence) {
|
||
capabilityLoading.value = false;
|
||
}
|
||
}
|
||
};
|
||
|
||
const rules = {
|
||
title: [{ required: true, message: $t('message.required'), trigger: 'blur' }],
|
||
modelName: [
|
||
{ required: true, message: $t('message.required'), trigger: 'blur' },
|
||
],
|
||
groupName: [
|
||
{ required: true, message: $t('message.required'), trigger: 'blur' },
|
||
],
|
||
};
|
||
|
||
const save = async () => {
|
||
btnLoading.value = true;
|
||
const modelType = resolveModelType();
|
||
|
||
try {
|
||
await formDataRef.value.validate();
|
||
const submitData = {
|
||
...formData,
|
||
modelType,
|
||
providerId: isAdd.value ? selectedProviderId.value : formData.providerId,
|
||
};
|
||
|
||
const url = isAdd.value ? '/api/v1/model/save' : '/api/v1/model/update';
|
||
const res = await api.post(url, submitData);
|
||
|
||
if (res.errorCode === 0) {
|
||
ElMessage.success(res.message);
|
||
emit('reload');
|
||
closeDialog();
|
||
} else {
|
||
ElMessage.error(res.message || $t('ui.actionMessage.operationFailed'));
|
||
}
|
||
} catch (error) {
|
||
if (!(error as any)?.fields) {
|
||
ElMessage.error($t('ui.actionMessage.operationFailed'));
|
||
}
|
||
} finally {
|
||
btnLoading.value = false;
|
||
}
|
||
};
|
||
</script>
|
||
|
||
<template>
|
||
<EasyFlowFormModal
|
||
v-model:open="dialogVisible"
|
||
:centered="true"
|
||
:closable="!btnLoading"
|
||
:title="isAdd ? '添加模型' : '编辑模型'"
|
||
:before-close="closeDialog"
|
||
width="640"
|
||
:confirm-loading="btnLoading"
|
||
:confirm-text="$t('button.save')"
|
||
:submitting="btnLoading"
|
||
@confirm="save"
|
||
>
|
||
<div class="model-modal">
|
||
<ElForm
|
||
ref="formDataRef"
|
||
:model="formData"
|
||
status-icon
|
||
:rules="rules"
|
||
label-position="top"
|
||
class="model-modal__form"
|
||
>
|
||
<div class="model-modal__section">
|
||
<ElFormItem prop="title" :label="$t('llm.title')">
|
||
<ElInput
|
||
v-model.trim="formData.title"
|
||
placeholder="例如:生产主模型"
|
||
/>
|
||
</ElFormItem>
|
||
<ElFormItem prop="modelName" :label="$t('llm.llmModel')">
|
||
<ElInput
|
||
v-model.trim="formData.modelName"
|
||
placeholder="例如:gpt-4.1 / glm-4.5 / qwen3:8b"
|
||
@blur="detectModelCapabilities"
|
||
@input="handleModelNameInput"
|
||
/>
|
||
</ElFormItem>
|
||
<ElFormItem prop="groupName" :label="$t('llm.groupName')">
|
||
<ElInput
|
||
v-model.trim="formData.groupName"
|
||
placeholder="例如:默认组"
|
||
/>
|
||
</ElFormItem>
|
||
|
||
<ElFormItem class="model-modal__ability-item">
|
||
<template #label>
|
||
<span class="model-modal__ability-label">
|
||
{{ $t('llm.ability') }}
|
||
<ElTooltip
|
||
:content="$t('llm.abilityChangeTip')"
|
||
effect="light"
|
||
placement="top"
|
||
>
|
||
<button
|
||
type="button"
|
||
class="model-modal__ability-info"
|
||
:aria-label="$t('llm.abilityChangeTip')"
|
||
>
|
||
<ElIcon><InfoFilled /></ElIcon>
|
||
</button>
|
||
</ElTooltip>
|
||
</span>
|
||
</template>
|
||
<div
|
||
class="model-modal__ability-panel"
|
||
:aria-busy="capabilityLoading"
|
||
>
|
||
<div class="model-modal__ability-toolbar">
|
||
<button
|
||
v-for="item in modelTypeAbilityOptions"
|
||
:key="item.value"
|
||
type="button"
|
||
class="model-modal__ability-chip is-interactive"
|
||
:class="[
|
||
`is-tone-${item.value}`,
|
||
{ 'is-active': selectedModelType === item.value },
|
||
]"
|
||
:aria-pressed="selectedModelType === item.value"
|
||
@click="handleModelTypeChipClick(item.value)"
|
||
>
|
||
<IconifyIcon
|
||
:icon="getAbilityIcon(item.value)"
|
||
class="model-modal__ability-icon"
|
||
/>
|
||
{{ item.label }}
|
||
</button>
|
||
<span
|
||
class="model-modal__ability-separator"
|
||
aria-hidden="true"
|
||
></span>
|
||
<button
|
||
v-for="item in visibleModelAbility"
|
||
:key="item.value"
|
||
type="button"
|
||
class="model-modal__ability-chip is-interactive"
|
||
:class="{
|
||
'is-active': item.selected,
|
||
'is-disabled': hasSpecialModelType,
|
||
[`is-tone-${item.value}`]: true,
|
||
}"
|
||
:aria-pressed="item.selected"
|
||
:disabled="hasSpecialModelType"
|
||
@click="handleAbilityChipClick(item)"
|
||
>
|
||
<IconifyIcon
|
||
:icon="getAbilityIcon(item.value)"
|
||
class="model-modal__ability-icon"
|
||
/>
|
||
{{ item.label }}
|
||
</button>
|
||
<ElIcon
|
||
v-if="capabilityLoading"
|
||
class="model-modal__ability-loading is-loading"
|
||
aria-label="正在识别模型能力"
|
||
>
|
||
<Loading />
|
||
</ElIcon>
|
||
</div>
|
||
</div>
|
||
</ElFormItem>
|
||
|
||
<div v-if="!hasSpecialModelType" class="model-modal__advanced">
|
||
<button
|
||
type="button"
|
||
class="model-modal__advanced-toggle"
|
||
:aria-expanded="showAdvanced"
|
||
@click="showAdvanced = !showAdvanced"
|
||
>
|
||
<span>高级设置</span>
|
||
<ElIcon>
|
||
<ArrowUp v-if="showAdvanced" />
|
||
<ArrowDown v-else />
|
||
</ElIcon>
|
||
</button>
|
||
|
||
<div v-if="showAdvanced" class="model-modal__advanced-body">
|
||
<ElFormItem label="Agent HTTP 传输">
|
||
<ElSelect
|
||
v-model="formData.options.agentHttpVersionPolicy"
|
||
aria-label="Agent HTTP 传输"
|
||
>
|
||
<ElOption
|
||
v-for="item in agentHttpVersionOptions"
|
||
:key="item.value"
|
||
:label="item.label"
|
||
:value="item.value"
|
||
/>
|
||
</ElSelect>
|
||
</ElFormItem>
|
||
<ElFormItem label="System 消息格式">
|
||
<ElSelect
|
||
v-model="formData.options.agentSystemContentFormat"
|
||
aria-label="System 消息格式"
|
||
>
|
||
<ElOption
|
||
v-for="item in agentSystemContentFormatOptions"
|
||
:key="item.value"
|
||
:label="item.label"
|
||
:value="item.value"
|
||
/>
|
||
</ElSelect>
|
||
</ElFormItem>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</ElForm>
|
||
</div>
|
||
</EasyFlowFormModal>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.model-modal {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 16px;
|
||
}
|
||
|
||
.model-modal__section {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
padding: 16px 18px;
|
||
background: hsl(var(--surface-panel) / 96%);
|
||
border: 1px solid hsl(var(--divider-faint) / 42%);
|
||
border-radius: 20px;
|
||
}
|
||
|
||
.model-modal__form {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
}
|
||
|
||
.model-modal__ability-item {
|
||
margin-top: 4px;
|
||
}
|
||
|
||
.model-modal__ability-label {
|
||
display: inline-flex;
|
||
gap: var(--space-1);
|
||
align-items: center;
|
||
}
|
||
|
||
.model-modal__ability-info {
|
||
display: inline-flex;
|
||
padding: 0;
|
||
color: hsl(var(--text-muted));
|
||
cursor: help;
|
||
background: transparent;
|
||
border: 0;
|
||
border-radius: var(--radius-control);
|
||
transition: color var(--motion-duration-base) var(--motion-ease-standard);
|
||
}
|
||
|
||
.model-modal__ability-info:hover,
|
||
.model-modal__ability-info:focus-visible {
|
||
color: hsl(var(--primary));
|
||
}
|
||
|
||
.model-modal__ability-info:focus-visible {
|
||
outline: 2px solid hsl(var(--primary) / 24%);
|
||
outline-offset: 2px;
|
||
}
|
||
|
||
.model-modal__ability-panel {
|
||
padding: 2px;
|
||
overflow: hidden;
|
||
border-radius: 18px;
|
||
}
|
||
|
||
.model-modal__ability-toolbar {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 10px;
|
||
align-items: center;
|
||
}
|
||
|
||
.model-modal__ability-separator {
|
||
align-self: stretch;
|
||
width: 1px;
|
||
min-height: 28px;
|
||
background: hsl(var(--divider-faint) / 62%);
|
||
border-radius: 999px;
|
||
}
|
||
|
||
.model-modal__ability-chip {
|
||
display: inline-flex;
|
||
gap: 8px;
|
||
align-items: center;
|
||
justify-content: center;
|
||
min-height: 40px;
|
||
padding: 9px 18px;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
line-height: 1;
|
||
color: hsl(var(--text-muted));
|
||
cursor: default;
|
||
background: hsl(var(--surface-contrast-soft) / 86%);
|
||
border: 1px solid transparent;
|
||
border-radius: 999px;
|
||
transition:
|
||
transform 0.2s ease,
|
||
border-color 0.2s ease,
|
||
color 0.2s ease,
|
||
background 0.2s ease,
|
||
box-shadow 0.2s ease;
|
||
}
|
||
|
||
.model-modal__ability-chip.is-interactive {
|
||
cursor: pointer;
|
||
}
|
||
|
||
.model-modal__ability-chip.is-interactive:hover,
|
||
.model-modal__ability-chip.is-interactive:focus-visible {
|
||
color: hsl(var(--text-strong));
|
||
box-shadow: 0 10px 18px -14px hsl(var(--foreground) / 28%);
|
||
transform: translateY(-1px);
|
||
}
|
||
|
||
.model-modal__ability-chip.is-active {
|
||
color: hsl(var(--text-strong));
|
||
background: hsl(var(--primary) / 10%);
|
||
border-color: hsl(var(--primary) / 18%);
|
||
box-shadow: inset 0 0 0 1px hsl(var(--primary) / 14%);
|
||
}
|
||
|
||
.model-modal__ability-chip.is-disabled {
|
||
cursor: default;
|
||
box-shadow: none;
|
||
opacity: 0.56;
|
||
transform: none;
|
||
}
|
||
|
||
.model-modal__ability-icon {
|
||
font-size: 15px;
|
||
opacity: 0.88;
|
||
}
|
||
|
||
.model-modal__ability-loading {
|
||
margin-inline-start: var(--space-1);
|
||
color: hsl(var(--text-muted));
|
||
}
|
||
|
||
.model-modal__ability-chip.is-active.is-tone-embeddingModel,
|
||
.model-modal__ability-chip.is-active.is-tone-thinking {
|
||
color: hsl(var(--primary));
|
||
background: hsl(var(--primary) / 10%);
|
||
border-color: hsl(var(--primary) / 18%);
|
||
box-shadow: inset 0 0 0 1px hsl(var(--primary) / 14%);
|
||
}
|
||
|
||
.model-modal__ability-chip.is-active.is-tone-rerankModel,
|
||
.model-modal__ability-chip.is-active.is-tone-tool {
|
||
color: hsl(var(--warning));
|
||
background: hsl(var(--warning) / 12%);
|
||
border-color: hsl(var(--warning) / 18%);
|
||
box-shadow: inset 0 0 0 1px hsl(var(--warning) / 14%);
|
||
}
|
||
|
||
.model-modal__ability-chip.is-active.is-tone-image {
|
||
color: hsl(var(--success));
|
||
background: hsl(var(--success) / 12%);
|
||
border-color: hsl(var(--success) / 18%);
|
||
box-shadow: inset 0 0 0 1px hsl(var(--success) / 14%);
|
||
}
|
||
|
||
.model-modal__advanced {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: var(--space-3);
|
||
}
|
||
|
||
.model-modal__advanced-toggle {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
width: 100%;
|
||
padding: var(--space-2) var(--space-3);
|
||
font-size: 13px;
|
||
color: hsl(var(--text-muted));
|
||
cursor: pointer;
|
||
background: hsl(var(--surface-contrast-soft) / 68%);
|
||
border: 1px solid hsl(var(--divider-faint) / 56%);
|
||
border-radius: var(--radius-control);
|
||
transition:
|
||
color var(--motion-duration-base) var(--motion-ease-standard),
|
||
border-color var(--motion-duration-base) var(--motion-ease-standard),
|
||
background var(--motion-duration-base) var(--motion-ease-standard);
|
||
}
|
||
|
||
.model-modal__advanced-toggle:hover,
|
||
.model-modal__advanced-toggle:focus-visible {
|
||
color: hsl(var(--text-strong));
|
||
background: hsl(var(--surface-contrast-soft));
|
||
border-color: hsl(var(--divider-faint));
|
||
}
|
||
|
||
.model-modal__advanced-body :deep(.el-form-item) {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.model-modal__advanced-body {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: var(--space-3);
|
||
}
|
||
|
||
.model-modal__advanced-body :deep(.el-select) {
|
||
width: 100%;
|
||
}
|
||
|
||
@media (max-width: 640px) {
|
||
.model-modal__ability-separator {
|
||
display: none;
|
||
}
|
||
}
|
||
</style>
|