feat: 更新模型选择组件和聊天组件的输入逻辑
This commit is contained in:
parent
3525e357d3
commit
21b16c7c06
@ -25,6 +25,7 @@
|
||||
- [ ] rename database table name, such as skills -> agent_skills, subagents -> agent_subagents, mcp, tool_call, 等等
|
||||
- [x] department 的 id 也不能使用那个索引的 id 来使用了,应该是一个独立的 dept_id,需要确认
|
||||
- [ ] allow user config skill, and envs
|
||||
- [ ] 添加 Skill 的权限设计
|
||||
- [ ] add model retry times to agent context config
|
||||
- [ ] add user envs when load sandbox
|
||||
- [ ] Config spacy model 的 load
|
||||
|
||||
@ -7,7 +7,7 @@ from fastapi import APIRouter, Depends, HTTPException
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from server.utils.auth_middleware import get_admin_user, get_db
|
||||
from server.utils.auth_middleware import get_admin_user, get_db, get_required_user
|
||||
from yuxi.services.model_provider_service import (
|
||||
check_credential_status,
|
||||
create_provider_config,
|
||||
@ -201,7 +201,7 @@ async def refresh_model_cache(
|
||||
@model_providers.get("/models/v2")
|
||||
async def get_v2_models(
|
||||
model_type: str = "chat",
|
||||
current_user: User = Depends(get_admin_user),
|
||||
_current_user: User = Depends(get_required_user),
|
||||
):
|
||||
"""获取 v2 格式的模型列表,按 provider 分组。
|
||||
|
||||
|
||||
@ -108,7 +108,7 @@ export const modelProviderApi = {
|
||||
},
|
||||
|
||||
getV2Models: async (modelType = 'chat') => {
|
||||
return apiAdminGet(`/api/system/model-providers/models/v2?model_type=${modelType}`)
|
||||
return apiGet(`/api/system/model-providers/models/v2?model_type=${modelType}`)
|
||||
},
|
||||
|
||||
getCacheStatus: async () => {
|
||||
|
||||
@ -120,6 +120,9 @@
|
||||
<template #actions-left-extra>
|
||||
<slot name="input-actions-left" :has-active-thread="!!currentChatId"></slot>
|
||||
</template>
|
||||
<template #actions-right-extra>
|
||||
<slot name="input-actions-right" :has-active-thread="!!currentChatId"></slot>
|
||||
</template>
|
||||
</AgentInputArea>
|
||||
|
||||
<div class="bottom-actions" v-if="conversations.length > 0">
|
||||
@ -205,7 +208,8 @@ import AgentPanel from '@/components/AgentPanel.vue'
|
||||
// ==================== PROPS & EMITS ====================
|
||||
const props = defineProps({
|
||||
agentId: { type: String, default: '' },
|
||||
singleMode: { type: Boolean, default: true }
|
||||
singleMode: { type: Boolean, default: true },
|
||||
sendDisabled: { type: Boolean, default: false }
|
||||
})
|
||||
const emit = defineEmits(['thread-change'])
|
||||
|
||||
@ -581,7 +585,9 @@ const isReplyLoading = computed(() => {
|
||||
})
|
||||
const isSendButtonDisabled = computed(() => {
|
||||
return (
|
||||
sendCooldownActive.value || ((!userInput.value || !currentAgent.value) && !isProcessing.value)
|
||||
sendCooldownActive.value ||
|
||||
(props.sendDisabled && !isProcessing.value) ||
|
||||
((!userInput.value || !currentAgent.value) && !isProcessing.value)
|
||||
)
|
||||
})
|
||||
|
||||
@ -1259,7 +1265,13 @@ const selectThreadFromRoute = async (threadId) => {
|
||||
const handleSendMessage = async ({ image } = {}) => {
|
||||
const text = userInput.value.trim()
|
||||
const imageContent = image?.imageContent || null
|
||||
if ((!text && !image) || !currentAgent.value || isProcessing.value || sendCooldownActive.value)
|
||||
if (
|
||||
(!text && !image) ||
|
||||
!currentAgent.value ||
|
||||
isProcessing.value ||
|
||||
sendCooldownActive.value ||
|
||||
props.sendDisabled
|
||||
)
|
||||
return
|
||||
|
||||
// 发送后进入短暂冷却,防止连续触发停止
|
||||
@ -1457,6 +1469,7 @@ const handleSendOrStop = async (payload) => {
|
||||
return
|
||||
}
|
||||
}
|
||||
if (props.sendDisabled) return
|
||||
await handleSendMessage(payload)
|
||||
}
|
||||
|
||||
|
||||
@ -77,6 +77,7 @@
|
||||
</template>
|
||||
<template #actions-left>
|
||||
<div class="input-actions-left">
|
||||
<slot name="actions-left-extra"></slot>
|
||||
<a-popover
|
||||
v-if="showTodoEntry"
|
||||
v-model:open="todoPopoverOpen"
|
||||
@ -133,7 +134,7 @@
|
||||
</template>
|
||||
<template #actions-right>
|
||||
<div class="input-actions-right">
|
||||
<slot name="actions-left-extra"></slot>
|
||||
<slot name="actions-right-extra"></slot>
|
||||
</div>
|
||||
</template>
|
||||
</MessageInputComponent>
|
||||
@ -401,7 +402,7 @@ const getTodoStatusLabel = (status) => {
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 8px;
|
||||
// height: 28px;
|
||||
height: 30px;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
color: var(--gray-600);
|
||||
|
||||
@ -218,7 +218,7 @@
|
||||
size="small"
|
||||
type="text"
|
||||
danger
|
||||
v-if="!isBuildActive"
|
||||
v-if="graphBuildStatus?.locked && !isBuildActive"
|
||||
@click="confirmResetGraph"
|
||||
>重置</a-button
|
||||
>
|
||||
@ -378,7 +378,7 @@ const extractorTypeOptions = [
|
||||
{
|
||||
value: 'spacy',
|
||||
label: 'spaCy',
|
||||
description: '使用本地 NER 模型抽取实体',
|
||||
description: '【Beta】使用本地 NER 模型抽取实体',
|
||||
icon: ScanText
|
||||
}
|
||||
]
|
||||
|
||||
@ -1,18 +1,16 @@
|
||||
<template>
|
||||
<a-dropdown trigger="click" :open="dropdownOpen" @open-change="handleOpenChange">
|
||||
<div class="model-select" :class="modelSelectClasses" @click.prevent>
|
||||
<a-dropdown trigger="click" :open="dropdownOpen" :disabled="props.disabled" @open-change="handleOpenChange">
|
||||
<div class="model-select" :class="modelSelectClasses" @click.prevent.stop @mousedown.stop>
|
||||
<div class="model-select-content">
|
||||
<div class="model-info">
|
||||
<a-tooltip :title="displayModelTooltip" placement="right">
|
||||
<span class="model-text text"> {{ displayModelText }} </span>
|
||||
</a-tooltip>
|
||||
<span class="model-text text" :title="displayModelTitle">{{ displayModelText }}</span>
|
||||
</div>
|
||||
<div class="model-status-controls">
|
||||
<div v-if="resolvedSize !== 'nano'" class="model-status-controls">
|
||||
<span
|
||||
v-if="state.currentModelStatus"
|
||||
class="model-status-indicator"
|
||||
:class="state.currentModelStatus.status"
|
||||
:title="getCurrentModelStatusTooltip()"
|
||||
:title="getCurrentModelStatusTitle()"
|
||||
>
|
||||
{{ modelStatusIcon }}
|
||||
</span>
|
||||
@ -21,7 +19,7 @@
|
||||
type="text"
|
||||
:loading="state.checkingStatus"
|
||||
@click.stop="checkCurrentModelStatus"
|
||||
:disabled="state.checkingStatus"
|
||||
:disabled="props.disabled || state.checkingStatus"
|
||||
class="status-check-button"
|
||||
>
|
||||
{{ state.checkingStatus ? '检查中...' : '检查' }}
|
||||
@ -34,16 +32,15 @@
|
||||
<div class="model-search">
|
||||
<a-input v-model:value="modelSearchKeyword" placeholder="搜索模型" allow-clear @keydown.stop>
|
||||
<template #suffix>
|
||||
<a-tooltip title="刷新缓存">
|
||||
<button
|
||||
:disabled="state.refreshingCache"
|
||||
class="cache-refresh-button"
|
||||
@mousedown.prevent.stop
|
||||
@click.stop="refreshCache"
|
||||
>
|
||||
<RefreshCw :size="13" :class="{ spin: state.refreshingCache }" />
|
||||
</button>
|
||||
</a-tooltip>
|
||||
<button
|
||||
:disabled="props.disabled || state.refreshingCache"
|
||||
:title="state.refreshingCache ? '刷新中...' : '刷新缓存'"
|
||||
class="cache-refresh-button"
|
||||
@mousedown.prevent.stop
|
||||
@click.stop="refreshCache"
|
||||
>
|
||||
<RefreshCw :size="13" :class="{ spin: state.refreshingCache }" />
|
||||
</button>
|
||||
</template>
|
||||
</a-input>
|
||||
</div>
|
||||
@ -91,7 +88,11 @@ const props = defineProps({
|
||||
size: {
|
||||
type: String,
|
||||
default: 'small',
|
||||
validator: (value) => ['small', 'middle', 'large'].includes(value)
|
||||
validator: (value) => ['nano', 'small', 'middle', 'large'].includes(value)
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
displayName: {
|
||||
type: String,
|
||||
@ -151,13 +152,17 @@ const fetchV2Models = async () => {
|
||||
|
||||
// 下拉展开时触发实时刷新(仅在打开瞬间触发,关闭时忽略)
|
||||
const handleOpenChange = (open) => {
|
||||
if (props.disabled) {
|
||||
dropdownOpen.value = false
|
||||
return
|
||||
}
|
||||
dropdownOpen.value = open
|
||||
if (open) fetchV2Models()
|
||||
}
|
||||
|
||||
// 强制刷新缓存
|
||||
const refreshCache = async () => {
|
||||
if (state.refreshingCache) return
|
||||
if (props.disabled || state.refreshingCache) return
|
||||
state.refreshingCache = true
|
||||
try {
|
||||
await modelProviderApi.refreshModelCache()
|
||||
@ -180,8 +185,10 @@ const state = reactive({
|
||||
|
||||
const resolvedSize = computed(() => props.size || 'small')
|
||||
const modelSelectClasses = computed(() => ({
|
||||
'model-select--nano': resolvedSize.value === 'nano',
|
||||
'model-select--middle': resolvedSize.value === 'middle',
|
||||
'model-select--large': resolvedSize.value === 'large'
|
||||
'model-select--large': resolvedSize.value === 'large',
|
||||
'model-select--disabled': props.disabled
|
||||
}))
|
||||
const buttonSize = computed(() => {
|
||||
if (resolvedSize.value === 'large') return 'large'
|
||||
@ -206,10 +213,11 @@ const displayModelText = computed(() => {
|
||||
return spec
|
||||
})
|
||||
|
||||
const displayModelTooltip = computed(() => props.model_spec || props.placeholder)
|
||||
const displayModelTitle = computed(() => props.model_spec || props.placeholder)
|
||||
|
||||
// 检查当前模型状态
|
||||
const checkCurrentModelStatus = async () => {
|
||||
if (props.disabled) return
|
||||
const spec = props.model_spec
|
||||
if (!spec) return
|
||||
|
||||
@ -238,7 +246,7 @@ const modelStatusIcon = computed(() => {
|
||||
return '○'
|
||||
})
|
||||
|
||||
const getCurrentModelStatusTooltip = () => {
|
||||
const getCurrentModelStatusTitle = () => {
|
||||
const status = state.currentModelStatus
|
||||
if (!status) return '状态未知'
|
||||
|
||||
@ -253,6 +261,7 @@ const getCurrentModelStatusTooltip = () => {
|
||||
|
||||
// 选择 v2 模型的方法
|
||||
const handleSelectV2Model = (spec) => {
|
||||
if (props.disabled) return
|
||||
emit('select-model', spec)
|
||||
dropdownOpen.value = false
|
||||
}
|
||||
@ -282,6 +291,34 @@ const handleSelectV2Model = (spec) => {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.model-select--nano {
|
||||
max-width: 100%;
|
||||
height: 30px;
|
||||
padding: 6px 8px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
line-height: 1;
|
||||
color: var(--gray-600);
|
||||
background: transparent;
|
||||
transition: all 0.2s ease;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.model-select--nano:hover {
|
||||
color: var(--gray-900);
|
||||
background: var(--gray-50);
|
||||
}
|
||||
|
||||
.model-select--nano .model-text {
|
||||
color: currentColor;
|
||||
}
|
||||
|
||||
.model-select--disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.model-dropdown {
|
||||
min-width: 280px;
|
||||
max-width: 420px;
|
||||
@ -302,5 +339,24 @@ const handleSelectV2Model = (spec) => {
|
||||
overflow-y: auto;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.ant-dropdown-menu-item,
|
||||
.ant-dropdown-menu-submenu-title {
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.ant-dropdown-menu-item:hover,
|
||||
.ant-dropdown-menu-submenu-title:hover,
|
||||
.ant-dropdown-menu-item-active,
|
||||
.ant-dropdown-menu-submenu-title-active {
|
||||
background: var(--gray-50);
|
||||
}
|
||||
|
||||
.ant-dropdown-menu-item-selected,
|
||||
.ant-dropdown-menu-item-selected:hover,
|
||||
.ant-dropdown-menu-item-selected.ant-dropdown-menu-item-active {
|
||||
color: var(--gray-1000);
|
||||
background: var(--main-50);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -6,9 +6,22 @@
|
||||
<AgentChatComponent
|
||||
ref="chatComponentRef"
|
||||
:single-mode="false"
|
||||
:send-disabled="isSavingInputModel"
|
||||
@thread-change="handleThreadChange"
|
||||
>
|
||||
<template #input-actions-left="{ hasActiveThread }">
|
||||
<template #input-actions-left>
|
||||
<div v-if="showInputModelSelector" class="input-model-selector">
|
||||
<ModelSelectorComponent
|
||||
:model_spec="currentInputModelSpec"
|
||||
:disabled="isInputModelSelectorDisabled"
|
||||
size="nano"
|
||||
display-name="mini"
|
||||
@select-model="handleInputModelChange"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #input-actions-right="{ hasActiveThread }">
|
||||
<a-dropdown
|
||||
v-if="selectedAgentId"
|
||||
v-model:open="agentDropdownOpen"
|
||||
@ -146,6 +159,7 @@ import { message } from 'ant-design-vue'
|
||||
import { Settings2, Ellipsis, ChevronDown, Check, FolderKanban } from 'lucide-vue-next'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import AgentChatComponent from '@/components/AgentChatComponent.vue'
|
||||
import ModelSelectorComponent from '@/components/ModelSelectorComponent.vue'
|
||||
import FeedbackModalComponent from '@/components/dashboard/FeedbackModalComponent.vue'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { isBuiltinAgent, useAgentStore } from '@/stores/agent'
|
||||
@ -169,9 +183,11 @@ const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
// 从 agentStore 中获取响应式状态
|
||||
const { agents, selectedAgentId, isLoadingConfig } = storeToRefs(agentStore)
|
||||
const { agents, selectedAgentId, selectedAgent, agentConfig, configurableItems, isLoadingConfig } =
|
||||
storeToRefs(agentStore)
|
||||
|
||||
const syncingRouteThread = ref(false)
|
||||
const isSavingInputModel = ref(false)
|
||||
|
||||
const getRouteThreadId = () => {
|
||||
const value = route.params.thread_id
|
||||
@ -246,6 +262,42 @@ const currentAgentLabel = computed(() => {
|
||||
|
||||
const currentAgentIcon = computed(() => currentAgentOption.value?.icon || defaultAgentIcon)
|
||||
|
||||
const inputModelKey = computed(() => {
|
||||
if (configurableItems.value?.model?.kind === 'llm') return 'model'
|
||||
return (
|
||||
Object.entries(configurableItems.value || {}).find(
|
||||
([key, item]) => key !== 'subagents_model' && item?.kind === 'llm'
|
||||
)?.[0] || ''
|
||||
)
|
||||
})
|
||||
|
||||
const currentInputModelSpec = computed(() => {
|
||||
const key = inputModelKey.value
|
||||
return key ? agentConfig.value?.[key] || '' : ''
|
||||
})
|
||||
|
||||
const showInputModelSelector = computed(() => Boolean(selectedAgentId.value && inputModelKey.value))
|
||||
const isInputModelSelectorDisabled = computed(
|
||||
() => isLoadingConfig.value || isSavingInputModel.value || !selectedAgent.value?.can_manage
|
||||
)
|
||||
|
||||
const handleInputModelChange = async (spec) => {
|
||||
const key = inputModelKey.value
|
||||
if (!key || typeof spec !== 'string' || !spec || spec === currentInputModelSpec.value) return
|
||||
if (isInputModelSelectorDisabled.value) return
|
||||
|
||||
const previousSpec = currentInputModelSpec.value
|
||||
isSavingInputModel.value = true
|
||||
try {
|
||||
agentStore.updateAgentConfig({ [key]: spec })
|
||||
await agentStore.saveAgentConfig()
|
||||
} catch {
|
||||
agentStore.updateAgentConfig({ [key]: previousSpec })
|
||||
} finally {
|
||||
isSavingInputModel.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const agentDropdownOpen = ref(false)
|
||||
|
||||
const handleAgentSwitch = async (agentId, hasActiveThread) => {
|
||||
@ -375,6 +427,14 @@ const handleFeedback = () => {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.input-model-selector {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 0;
|
||||
max-width: min(168px, calc(100vw - 160px));
|
||||
}
|
||||
|
||||
.config-dropdown-trigger {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@ -391,6 +451,7 @@ const handleFeedback = () => {
|
||||
.config-dropdown-agent-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 3px;
|
||||
flex-shrink: 0;
|
||||
object-fit: contain;
|
||||
}
|
||||
@ -585,6 +646,7 @@ const handleFeedback = () => {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
object-fit: contain;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.config-dropdown-overlay .config-dropdown-item-badge {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user