mirror of
https://github.com/n8n-io/n8n
synced 2026-04-21 15:47:20 +00:00
feat(editor): Improve custom agent support in chat (no-changelog) (#21393)
This commit is contained in:
parent
956dd09fcc
commit
d5c275df03
11 changed files with 194 additions and 126 deletions
|
|
@ -2,7 +2,7 @@
|
|||
import { useChatStore } from '@/features/ai/chatHub/chat.store';
|
||||
import { useToast } from '@/composables/useToast';
|
||||
import { useMessage } from '@/composables/useMessage';
|
||||
import { MODAL_CONFIRM } from '@/constants';
|
||||
import { MODAL_CONFIRM, VIEWS } from '@/constants';
|
||||
import {
|
||||
N8nButton,
|
||||
N8nIcon,
|
||||
|
|
@ -18,12 +18,13 @@ import AgentEditorModal from '@/features/ai/chatHub/components/AgentEditorModal.
|
|||
import ChatAgentCard from '@/features/ai/chatHub/components/ChatAgentCard.vue';
|
||||
import { useChatCredentials } from '@/features/ai/chatHub/composables/useChatCredentials';
|
||||
import { useUsersStore } from '@/features/settings/users/users.store';
|
||||
import type { ChatHubConversationModel } from '@n8n/api-types';
|
||||
import { type ChatHubConversationModel } from '@n8n/api-types';
|
||||
import { filterAndSortAgents, stringifyModel } from '@/features/ai/chatHub/chat.utils';
|
||||
import type { ChatAgentFilter } from '@/features/ai/chatHub/chat.types';
|
||||
import { useChatHubSidebarState } from '@/features/ai/chatHub/composables/useChatHubSidebarState';
|
||||
import { useMediaQuery } from '@vueuse/core';
|
||||
import { MOBILE_MEDIA_QUERY } from '@/features/ai/chatHub/constants';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
const chatStore = useChatStore();
|
||||
const uiStore = useUIStore();
|
||||
|
|
@ -31,6 +32,7 @@ const toast = useToast();
|
|||
const message = useMessage();
|
||||
const usersStore = useUsersStore();
|
||||
const sidebar = useChatHubSidebarState();
|
||||
const router = useRouter();
|
||||
const isMobileDevice = useMediaQuery(MOBILE_MEDIA_QUERY);
|
||||
|
||||
const editingAgentId = ref<string | undefined>(undefined);
|
||||
|
|
@ -68,16 +70,26 @@ function handleCreateAgent() {
|
|||
}
|
||||
|
||||
async function handleEditAgent(model: ChatHubConversationModel) {
|
||||
if (model.provider !== 'custom-agent') {
|
||||
if (model.provider === 'n8n') {
|
||||
const routeData = router.resolve({
|
||||
name: VIEWS.WORKFLOW,
|
||||
params: {
|
||||
name: model.workflowId,
|
||||
},
|
||||
});
|
||||
|
||||
window.open(routeData.href, '_blank');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await chatStore.fetchCustomAgent(model.agentId);
|
||||
editingAgentId.value = model.agentId;
|
||||
uiStore.openModal('agentEditor');
|
||||
} catch (error) {
|
||||
toast.showError(error, 'Failed to load agent');
|
||||
if (model.provider === 'custom-agent') {
|
||||
try {
|
||||
await chatStore.fetchCustomAgent(model.agentId);
|
||||
editingAgentId.value = model.agentId;
|
||||
uiStore.openModal('agentEditor');
|
||||
} catch (error) {
|
||||
toast.showError(error, 'Failed to load agent');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<script setup lang="ts">
|
||||
import { useToast } from '@/composables/useToast';
|
||||
import { LOCAL_STORAGE_CHAT_HUB_SELECTED_MODEL } from '@/constants';
|
||||
import { LOCAL_STORAGE_CHAT_HUB_SELECTED_MODEL, VIEWS } from '@/constants';
|
||||
import { findOneFromModelsResponse, unflattenModel } from '@/features/ai/chatHub/chat.utils';
|
||||
import ChatConversationHeader from '@/features/ai/chatHub/components/ChatConversationHeader.vue';
|
||||
import ChatMessage from '@/features/ai/chatHub/components/ChatMessage.vue';
|
||||
|
|
@ -245,6 +245,17 @@ watch(
|
|||
{ immediate: true },
|
||||
);
|
||||
|
||||
// Reload models when credentials are updated
|
||||
watch(
|
||||
credentialsByProvider,
|
||||
(credentials) => {
|
||||
if (credentials) {
|
||||
void chatStore.fetchAgents(credentials);
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
function onSubmit(message: string) {
|
||||
if (
|
||||
!message.trim() ||
|
||||
|
|
@ -369,6 +380,12 @@ function openNewAgentCreator() {
|
|||
function closeAgentEditor() {
|
||||
editingAgentId.value = undefined;
|
||||
}
|
||||
|
||||
function handleOpenWorkflow(workflowId: string) {
|
||||
const routeData = router.resolve({ name: VIEWS.WORKFLOW, params: { name: workflowId } });
|
||||
|
||||
window.open(routeData.href, '_blank');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -385,10 +402,12 @@ function closeAgentEditor() {
|
|||
ref="headerRef"
|
||||
:selected-model="selectedModel ?? null"
|
||||
:credentials="credentialsByProvider"
|
||||
:ready-to-show-model-selector="chatStore.agentsReady"
|
||||
@select-model="handleSelectModel"
|
||||
@edit-custom-agent="handleEditAgent"
|
||||
@create-custom-agent="openNewAgentCreator"
|
||||
@select-credential="selectCredential"
|
||||
@open-workflow="handleOpenWorkflow"
|
||||
/>
|
||||
|
||||
<AgentEditorModal
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ import type {
|
|||
ChatStreamingState,
|
||||
} from './chat.types';
|
||||
import { retry } from '@n8n/utils/retry';
|
||||
import { isMatchedAgent } from './chat.utils';
|
||||
import { createAiMessageFromStreamingState, flattenModel } from './chat.utils';
|
||||
|
||||
export const useChatStore = defineStore(CHAT_STORE, () => {
|
||||
|
|
@ -675,15 +676,7 @@ export const useChatStore = defineStore(CHAT_STORE, () => {
|
|||
function getAgent(model: ChatHubConversationModel) {
|
||||
if (!agents.value) return;
|
||||
|
||||
return agents.value[model.provider].models.find((m) => {
|
||||
if (model.provider === 'n8n') {
|
||||
return m.model.provider === 'n8n' && m.model.workflowId === model.workflowId;
|
||||
} else if (model.provider === 'custom-agent') {
|
||||
return m.model.provider === 'custom-agent' && m.model.agentId === model.agentId;
|
||||
} else {
|
||||
return m.model.provider === model.provider && m.model.model === model.model;
|
||||
}
|
||||
});
|
||||
return agents.value[model.provider].models.find((agent) => isMatchedAgent(agent, model));
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -217,6 +217,18 @@ export function fromStringToModel(value: string): ChatHubConversationModel | und
|
|||
: { provider: parsedProvider, model: identifier };
|
||||
}
|
||||
|
||||
export function isMatchedAgent(agent: ChatModelDto, model: ChatHubConversationModel): boolean {
|
||||
if (model.provider === 'n8n') {
|
||||
return agent.model.provider === 'n8n' && agent.model.workflowId === model.workflowId;
|
||||
}
|
||||
|
||||
if (model.provider === 'custom-agent') {
|
||||
return agent.model.provider === 'custom-agent' && agent.model.agentId === model.agentId;
|
||||
}
|
||||
|
||||
return agent.model.provider === model.provider && agent.model.model === model.model;
|
||||
}
|
||||
|
||||
export function createAiMessageFromStreamingState(
|
||||
sessionId: ChatSessionId,
|
||||
messageId: ChatMessageId,
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ function resetForm() {
|
|||
|
||||
// Watch for modal opening
|
||||
watch(
|
||||
() => uiStore.isModalActiveById.agentEditor,
|
||||
() => uiStore.modalsById.agentEditor?.open,
|
||||
(isOpen) => {
|
||||
if (isOpen) {
|
||||
if (props.agentId) {
|
||||
|
|
|
|||
|
|
@ -19,15 +19,10 @@ defineProps<{
|
|||
:size="size === 'lg' ? 'xxlarge' : size === 'sm' ? 'large' : 'xlarge'"
|
||||
/>
|
||||
<N8nAvatar
|
||||
v-else-if="agent.model.provider === 'custom-agent'"
|
||||
v-else-if="agent.model.provider === 'custom-agent' || agent.model.provider === 'n8n'"
|
||||
:first-name="agent.name"
|
||||
:size="size === 'lg' ? 'medium' : size === 'sm' ? 'xxsmall' : 'xsmall'"
|
||||
/>
|
||||
<N8nIcon
|
||||
v-else-if="agent.model.provider === 'n8n'"
|
||||
icon="robot"
|
||||
:size="size === 'lg' ? 'xxlarge' : size === 'sm' ? 'large' : 'xlarge'"
|
||||
/>
|
||||
<CredentialIcon
|
||||
v-else
|
||||
:credential-type-name="PROVIDER_CREDENTIAL_TYPE_MAP[agent.model.provider]"
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ const emit = defineEmits<{
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="agent.model.provider === 'custom-agent'" :class="$style.actions">
|
||||
<div :class="$style.actions">
|
||||
<N8nIconButton
|
||||
icon="pen"
|
||||
type="tertiary"
|
||||
|
|
@ -49,6 +49,7 @@ const emit = defineEmits<{
|
|||
@click.prevent="emit('edit')"
|
||||
/>
|
||||
<N8nIconButton
|
||||
v-if="agent.model.provider === 'custom-agent'"
|
||||
icon="trash-2"
|
||||
type="tertiary"
|
||||
size="medium"
|
||||
|
|
|
|||
|
|
@ -8,9 +8,10 @@ import { N8nButton, N8nIconButton } from '@n8n/design-system';
|
|||
import { useTemplateRef } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
const { selectedModel, credentials } = defineProps<{
|
||||
const { selectedModel, credentials, readyToShowModelSelector } = defineProps<{
|
||||
selectedModel: ChatModelDto | null;
|
||||
credentials: CredentialsMap | null;
|
||||
readyToShowModelSelector: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
|
|
@ -19,6 +20,7 @@ const emit = defineEmits<{
|
|||
editCustomAgent: [agentId: string];
|
||||
createCustomAgent: [];
|
||||
selectCredential: [provider: ChatHubProvider, credentialId: string];
|
||||
openWorkflow: [workflowId: string];
|
||||
}>();
|
||||
|
||||
const sidebar = useChatHubSidebarState();
|
||||
|
|
@ -62,6 +64,7 @@ defineExpose({
|
|||
@click="onNewChat"
|
||||
/>
|
||||
<ModelSelector
|
||||
v-if="readyToShowModelSelector"
|
||||
ref="modelSelectorRef"
|
||||
:selectedAgent="selectedModel"
|
||||
:credentials="credentials"
|
||||
|
|
@ -77,10 +80,19 @@ defineExpose({
|
|||
:class="$style.editAgent"
|
||||
type="secondary"
|
||||
size="small"
|
||||
icon="cog"
|
||||
icon="settings"
|
||||
label="Edit Agent"
|
||||
@click="emit('editCustomAgent', selectedModel.model.agentId)"
|
||||
/>
|
||||
<N8nButton
|
||||
v-if="selectedModel?.model.provider === 'n8n'"
|
||||
:class="$style.editAgent"
|
||||
type="secondary"
|
||||
size="small"
|
||||
icon="settings"
|
||||
label="Open Workflow"
|
||||
@click="emit('openWorkflow', selectedModel.model.workflowId)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -178,12 +178,11 @@ onBeforeMount(() => {
|
|||
<ChatTypingIndicator v-if="isStreaming" :class="$style.typingIndicator" />
|
||||
<ChatMessageActions
|
||||
v-else
|
||||
:type="message.type"
|
||||
:just-copied="justCopied"
|
||||
:is-speech-synthesis-available="speech.isSupported.value"
|
||||
:is-speaking="speech.isPlaying.value"
|
||||
:class="$style.actions"
|
||||
:message-id="message.id"
|
||||
:message="message"
|
||||
:alternatives="message.alternatives"
|
||||
@copy="handleCopy"
|
||||
@edit="handleEdit"
|
||||
|
|
|
|||
|
|
@ -1,20 +1,22 @@
|
|||
<script setup lang="ts">
|
||||
import type { ChatHubMessageType, ChatMessageId } from '@n8n/api-types';
|
||||
import { N8nIconButton, N8nText, N8nTooltip } from '@n8n/design-system';
|
||||
import { VIEWS } from '@/constants';
|
||||
import type { ChatMessage } from '@/features/ai/chatHub/chat.types';
|
||||
import type { ChatMessageId } from '@n8n/api-types';
|
||||
import { N8nIconButton, N8nLink, N8nText, N8nTooltip } from '@n8n/design-system';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import { computed } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
const i18n = useI18n();
|
||||
const router = useRouter();
|
||||
|
||||
const { type, justCopied, messageId, alternatives, isSpeaking, isSpeechSynthesisAvailable } =
|
||||
defineProps<{
|
||||
type: ChatHubMessageType;
|
||||
justCopied: boolean;
|
||||
messageId: ChatMessageId;
|
||||
alternatives: ChatMessageId[];
|
||||
isSpeechSynthesisAvailable: boolean;
|
||||
isSpeaking: boolean;
|
||||
}>();
|
||||
const { justCopied, message, alternatives, isSpeaking, isSpeechSynthesisAvailable } = defineProps<{
|
||||
justCopied: boolean;
|
||||
message: ChatMessage;
|
||||
alternatives: ChatMessageId[];
|
||||
isSpeechSynthesisAvailable: boolean;
|
||||
isSpeaking: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
copy: [];
|
||||
|
|
@ -29,7 +31,17 @@ const copyTooltip = computed(() => {
|
|||
});
|
||||
|
||||
const currentAlternativeIndex = computed(() => {
|
||||
return alternatives.findIndex((id) => id === messageId);
|
||||
return alternatives.findIndex((id) => id === message.id);
|
||||
});
|
||||
|
||||
const executionUrl = computed(() => {
|
||||
if (message.type === 'ai' && message.provider === 'n8n' && message.executionId) {
|
||||
return router.resolve({
|
||||
name: VIEWS.EXECUTION_PREVIEW,
|
||||
params: { name: message.workflowId, executionId: message.executionId },
|
||||
}).href;
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
function handleCopy() {
|
||||
|
|
@ -62,7 +74,7 @@ function handleReadAloud() {
|
|||
<template #content>{{ copyTooltip }}</template>
|
||||
</N8nTooltip>
|
||||
<N8nTooltip
|
||||
v-if="isSpeechSynthesisAvailable && type === 'ai'"
|
||||
v-if="isSpeechSynthesisAvailable && message.type === 'ai'"
|
||||
placement="bottom"
|
||||
:show-after="300"
|
||||
>
|
||||
|
|
@ -79,7 +91,7 @@ function handleReadAloud() {
|
|||
<N8nIconButton icon="pen" type="tertiary" size="medium" text @click="handleEdit" />
|
||||
<template #content>Edit</template>
|
||||
</N8nTooltip>
|
||||
<N8nTooltip v-if="type === 'ai'" placement="bottom" :show-after="300">
|
||||
<N8nTooltip v-if="message.type === 'ai'" placement="bottom" :show-after="300">
|
||||
<N8nIconButton
|
||||
icon="refresh-cw"
|
||||
type="tertiary"
|
||||
|
|
@ -89,6 +101,15 @@ function handleReadAloud() {
|
|||
/>
|
||||
<template #content>Regenerate</template>
|
||||
</N8nTooltip>
|
||||
<N8nTooltip v-if="executionUrl && message.executionId" placement="bottom" :show-after="300">
|
||||
<N8nIconButton icon="info" type="tertiary" size="medium" text />
|
||||
<template #content>
|
||||
Execution ID:
|
||||
<N8nLink :to="executionUrl" :new-window="true">
|
||||
{{ message.executionId }}
|
||||
</N8nLink>
|
||||
</template>
|
||||
</N8nTooltip>
|
||||
<template v-if="alternatives.length > 1">
|
||||
<N8nIconButton
|
||||
icon="chevron-left"
|
||||
|
|
|
|||
|
|
@ -2,8 +2,17 @@
|
|||
import { computed, ref, useTemplateRef, watch } from 'vue';
|
||||
import { N8nNavigationDropdown, N8nIcon, N8nButton, N8nText, N8nAvatar } from '@n8n/design-system';
|
||||
import { type ComponentProps } from 'vue-component-type-helpers';
|
||||
import { PROVIDER_CREDENTIAL_TYPE_MAP, chatHubProviderSchema } from '@n8n/api-types';
|
||||
import type { ChatHubProvider, ChatHubLLMProvider, ChatModelDto } from '@n8n/api-types';
|
||||
import {
|
||||
PROVIDER_CREDENTIAL_TYPE_MAP,
|
||||
chatHubLLMProviderSchema,
|
||||
emptyChatModelsResponse,
|
||||
} from '@n8n/api-types';
|
||||
import type {
|
||||
ChatHubProvider,
|
||||
ChatHubLLMProvider,
|
||||
ChatModelDto,
|
||||
ChatModelsResponse,
|
||||
} from '@n8n/api-types';
|
||||
import { providerDisplayNames } from '@/features/ai/chatHub/constants';
|
||||
import CredentialIcon from '@/features/credentials/components/CredentialIcon.vue';
|
||||
import { onClickOutside } from '@vueuse/core';
|
||||
|
|
@ -13,9 +22,14 @@ import type { CredentialsMap } from '../chat.types';
|
|||
import CredentialSelectorModal from './CredentialSelectorModal.vue';
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
import { useCredentialsStore } from '@/features/credentials/credentials.store';
|
||||
import { useChatStore } from '@/features/ai/chatHub/chat.store';
|
||||
import ChatAgentAvatar from '@/features/ai/chatHub/components/ChatAgentAvatar.vue';
|
||||
import { fromStringToModel, stringifyModel } from '@/features/ai/chatHub/chat.utils';
|
||||
import {
|
||||
fromStringToModel,
|
||||
isMatchedAgent,
|
||||
stringifyModel,
|
||||
} from '@/features/ai/chatHub/chat.utils';
|
||||
import { fetchChatModelsApi } from '@/features/ai/chatHub/chat.api';
|
||||
import { useRootStore } from '@n8n/stores/useRootStore';
|
||||
|
||||
const NEW_AGENT_MENU_ID = 'agent::new';
|
||||
|
||||
|
|
@ -40,7 +54,7 @@ function handleSelectCredentials(provider: ChatHubProvider, id: string) {
|
|||
}
|
||||
|
||||
const i18n = useI18n();
|
||||
const chatStore = useChatStore();
|
||||
const agents = ref<ChatModelsResponse>(emptyChatModelsResponse);
|
||||
const dropdownRef = useTemplateRef('dropdownRef');
|
||||
const credentialSelectorProvider = ref<ChatHubLLMProvider | null>(null);
|
||||
const uiStore = useUIStore();
|
||||
|
|
@ -53,78 +67,71 @@ const credentialsName = computed(() =>
|
|||
);
|
||||
|
||||
const menu = computed(() => {
|
||||
const customAgents = chatStore.agents['custom-agent'].models;
|
||||
const customAgentOptions = customAgents.map<
|
||||
ComponentProps<typeof N8nNavigationDropdown>['menu'][number]
|
||||
>((agent) => ({
|
||||
id: stringifyModel(agent.model),
|
||||
title: agent.name,
|
||||
disabled: false,
|
||||
}));
|
||||
const menuItems: (typeof N8nNavigationDropdown)['menu'] = [];
|
||||
|
||||
const customAgentMenu: ComponentProps<typeof N8nNavigationDropdown>['menu'][number] = {
|
||||
id: 'custom-agents',
|
||||
title: i18n.baseText('chatHub.agent.customAgents'),
|
||||
icon: 'robot',
|
||||
iconSize: 'large',
|
||||
iconMargin: false,
|
||||
submenu: [
|
||||
...customAgentOptions,
|
||||
...(customAgentOptions.length > 0 ? [{ isDivider: true as const, id: 'divider' }] : []),
|
||||
if (includeCustomAgents) {
|
||||
const customAgents = [
|
||||
...agents.value['custom-agent'].models,
|
||||
...agents.value['n8n'].models,
|
||||
].map((agent) => ({
|
||||
id: stringifyModel(agent.model),
|
||||
title: agent.name,
|
||||
disabled: false,
|
||||
}));
|
||||
|
||||
menuItems.push({
|
||||
id: 'custom-agents',
|
||||
title: i18n.baseText('chatHub.agent.customAgents'),
|
||||
icon: 'robot',
|
||||
iconSize: 'large',
|
||||
iconMargin: false,
|
||||
submenu: [
|
||||
...customAgents,
|
||||
...(customAgents.length > 0 ? [{ isDivider: true as const, id: 'divider' }] : []),
|
||||
{
|
||||
id: NEW_AGENT_MENU_ID,
|
||||
icon: 'plus',
|
||||
title: i18n.baseText('chatHub.agent.newAgent'),
|
||||
disabled: false,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
for (const provider of chatHubLLMProviderSchema.options) {
|
||||
const theAgents = agents.value[provider].models;
|
||||
const error = agents.value[provider].error;
|
||||
const agentOptions =
|
||||
theAgents.length > 0
|
||||
? theAgents
|
||||
.filter((agent) => agent.model.provider !== 'custom-agent')
|
||||
.map<ComponentProps<typeof N8nNavigationDropdown>['menu'][number]>((agent) => ({
|
||||
id: stringifyModel(agent.model),
|
||||
title: agent.name,
|
||||
disabled: false,
|
||||
}))
|
||||
: error
|
||||
? [{ id: `${provider}::error`, value: null, disabled: true, title: error }]
|
||||
: [];
|
||||
|
||||
const submenu = agentOptions.concat([
|
||||
...(agentOptions.length > 0 ? [{ isDivider: true as const, id: 'divider' }] : []),
|
||||
{
|
||||
id: NEW_AGENT_MENU_ID,
|
||||
icon: 'plus',
|
||||
title: i18n.baseText('chatHub.agent.newAgent'),
|
||||
id: `${provider}::configure`,
|
||||
icon: 'settings',
|
||||
title: 'Configure credentials...',
|
||||
disabled: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
]);
|
||||
|
||||
const providerMenus = chatHubProviderSchema.options
|
||||
.filter(
|
||||
(provider) =>
|
||||
provider !== 'custom-agent' && (!includeCustomAgents ? provider !== 'n8n' : true),
|
||||
) // hide n8n agent for now
|
||||
.map((provider) => {
|
||||
const agents = chatStore.agents[provider].models;
|
||||
const error = chatStore.agents[provider].error;
|
||||
|
||||
const agentOptions =
|
||||
agents.length > 0
|
||||
? agents
|
||||
.filter((agent) => agent.model.provider !== 'custom-agent')
|
||||
.map<ComponentProps<typeof N8nNavigationDropdown>['menu'][number]>((agent) => ({
|
||||
id: stringifyModel(agent.model),
|
||||
title: agent.name,
|
||||
disabled: false,
|
||||
}))
|
||||
: error
|
||||
? [{ id: `${provider}::error`, value: null, disabled: true, title: error }]
|
||||
: [];
|
||||
|
||||
const submenu = agentOptions.concat([
|
||||
...(provider !== 'n8n' && agentOptions.length > 0
|
||||
? [{ isDivider: true as const, id: 'divider' }]
|
||||
: []),
|
||||
]);
|
||||
|
||||
if (provider !== 'n8n') {
|
||||
submenu.push({
|
||||
id: `${provider}::configure`,
|
||||
icon: 'settings',
|
||||
title: 'Configure credentials...',
|
||||
disabled: false,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
id: provider,
|
||||
title: providerDisplayNames[provider],
|
||||
submenu,
|
||||
};
|
||||
menuItems.push({
|
||||
id: provider,
|
||||
title: providerDisplayNames[provider],
|
||||
submenu,
|
||||
});
|
||||
}
|
||||
|
||||
return includeCustomAgents ? [customAgentMenu, ...providerMenus] : providerMenus;
|
||||
return menuItems;
|
||||
});
|
||||
|
||||
const selectedLabel = computed(() => selectedAgent?.name ?? 'Select model');
|
||||
|
|
@ -164,7 +171,9 @@ function onSelect(id: string) {
|
|||
return;
|
||||
}
|
||||
|
||||
const selected = chatStore.getAgent(parsedModel);
|
||||
const selected = agents.value[parsedModel.provider].models.find((a) =>
|
||||
isMatchedAgent(a, parsedModel),
|
||||
);
|
||||
|
||||
if (!selected) {
|
||||
return;
|
||||
|
|
@ -182,12 +191,12 @@ onClickOutside(
|
|||
() => dropdownRef.value?.close(),
|
||||
);
|
||||
|
||||
// Reload models when credentials are updated
|
||||
// Update agents when credentials are updated
|
||||
watch(
|
||||
() => credentials,
|
||||
(credentials) => {
|
||||
async (credentials) => {
|
||||
if (credentials) {
|
||||
void chatStore.fetchAgents(credentials);
|
||||
agents.value = await fetchChatModelsApi(useRootStore().restApiContext, { credentials });
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
|
|
@ -199,12 +208,7 @@ defineExpose({
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<N8nNavigationDropdown
|
||||
v-if="chatStore.agentsReady"
|
||||
ref="dropdownRef"
|
||||
:menu="menu"
|
||||
@select="onSelect"
|
||||
>
|
||||
<N8nNavigationDropdown ref="dropdownRef" :menu="menu" @select="onSelect">
|
||||
<template #item-icon="{ item }">
|
||||
<CredentialIcon
|
||||
v-if="item.id in PROVIDER_CREDENTIAL_TYPE_MAP"
|
||||
|
|
@ -213,7 +217,7 @@ defineExpose({
|
|||
:class="$style.menuIcon"
|
||||
/>
|
||||
<N8nAvatar
|
||||
v-else-if="item.id.startsWith('custom-agent::')"
|
||||
v-else-if="item.id.startsWith('n8n::') || item.id.startsWith('custom-agent::')"
|
||||
:class="$style.avatarIcon"
|
||||
:first-name="item.title"
|
||||
size="xsmall"
|
||||
|
|
|
|||
Loading…
Reference in a new issue