mirror of
https://github.com/n8n-io/n8n.git
synced 2026-05-30 08:17:06 +02:00
feat(editor): Show callout when credentials are not set (no-changelog) (#20997)
This commit is contained in:
parent
7681383e73
commit
af1096b3e9
|
|
@ -45,6 +45,10 @@ defineSlots<{
|
|||
[key: `item.append.${string}`]: (props: { item: Item }) => unknown;
|
||||
}>();
|
||||
|
||||
const open = () => {
|
||||
menuRef.value?.open(ROOT_MENU_INDEX);
|
||||
};
|
||||
|
||||
const close = () => {
|
||||
menuRef.value?.close(ROOT_MENU_INDEX);
|
||||
};
|
||||
|
|
@ -61,6 +65,7 @@ const onClose = (index: string) => {
|
|||
};
|
||||
|
||||
defineExpose({
|
||||
open,
|
||||
close,
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -33,6 +33,8 @@ import { useRoute, useRouter } from 'vue-router';
|
|||
import { useChatStore } from './chat.store';
|
||||
import { credentialsMapSchema, type CredentialsMap } from './chat.types';
|
||||
import { useDocumentTitle } from '@/composables/useDocumentTitle';
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
import CredentialSelectorModal from '@/features/ai/chatHub/components/CredentialSelectorModal.vue';
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
|
@ -42,7 +44,9 @@ const credentialsStore = useCredentialsStore();
|
|||
const toast = useToast();
|
||||
const isMobileDevice = useMediaQuery(MOBILE_MEDIA_QUERY);
|
||||
const documentTitle = useDocumentTitle();
|
||||
const uiStore = useUIStore();
|
||||
|
||||
const headerRef = useTemplateRef('headerRef');
|
||||
const inputRef = useTemplateRef('inputRef');
|
||||
const sessionId = computed<string>(() =>
|
||||
typeof route.params.id === 'string' ? route.params.id : uuidv4(),
|
||||
|
|
@ -117,18 +121,17 @@ const mergedCredentials = computed(() => ({
|
|||
|
||||
const chatMessages = computed(() => chatStore.getActiveMessages(sessionId.value));
|
||||
const isNewChat = computed(() => route.name === CHAT_VIEW);
|
||||
const inputPlaceholder = computed(() => {
|
||||
if (!selectedModel.value) {
|
||||
return 'Select a model';
|
||||
}
|
||||
|
||||
const modelName = selectedModel.value.model;
|
||||
|
||||
return `Message ${modelName}`;
|
||||
});
|
||||
const credentialsId = computed(() =>
|
||||
selectedModel.value ? mergedCredentials.value[selectedModel.value.provider] : undefined,
|
||||
);
|
||||
|
||||
const editingMessageId = ref<string>();
|
||||
const didSubmitInCurrentSession = ref(false);
|
||||
const initialization = ref({ credentialsFetched: false, modelsFetched: false });
|
||||
const credentialSelectorProvider = ref<ChatHubProvider | null>(null);
|
||||
const isInitialized = computed(
|
||||
() => initialization.value.credentialsFetched && initialization.value.modelsFetched,
|
||||
);
|
||||
|
||||
function scrollToBottom(smooth: boolean) {
|
||||
scrollContainerRef.value?.scrollTo({
|
||||
|
|
@ -179,6 +182,8 @@ watch(
|
|||
if (selected === null) {
|
||||
selectedModel.value = findOneFromModelsResponse(models) ?? null;
|
||||
}
|
||||
|
||||
initialization.value.modelsFetched = true;
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
|
@ -215,32 +220,22 @@ onMounted(async () => {
|
|||
credentialsStore.fetchCredentialTypes(false),
|
||||
credentialsStore.fetchAllCredentials(),
|
||||
]);
|
||||
initialization.value.credentialsFetched = true;
|
||||
});
|
||||
|
||||
function onSubmit(message: string) {
|
||||
if (!message.trim() || chatStore.isResponding) {
|
||||
if (!message.trim() || chatStore.isResponding || !selectedModel.value || !credentialsId.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const credentialsId = selectedModel.value
|
||||
? mergedCredentials.value[selectedModel.value.provider]
|
||||
: undefined;
|
||||
|
||||
didSubmitInCurrentSession.value = true;
|
||||
|
||||
chatStore.sendMessage(
|
||||
sessionId.value,
|
||||
message,
|
||||
selectedModel.value,
|
||||
selectedModel.value && credentialsId
|
||||
? {
|
||||
[PROVIDER_CREDENTIAL_TYPE_MAP[selectedModel.value.provider]]: {
|
||||
id: credentialsId,
|
||||
name: '',
|
||||
},
|
||||
}
|
||||
: null,
|
||||
);
|
||||
chatStore.sendMessage(sessionId.value, message, selectedModel.value, {
|
||||
[PROVIDER_CREDENTIAL_TYPE_MAP[selectedModel.value.provider]]: {
|
||||
id: credentialsId.value,
|
||||
name: '',
|
||||
},
|
||||
});
|
||||
|
||||
inputRef.value?.setText('');
|
||||
|
||||
|
|
@ -263,21 +258,20 @@ function handleCancelEditMessage() {
|
|||
}
|
||||
|
||||
function handleEditMessage(message: ChatHubMessageDto) {
|
||||
if (chatStore.isResponding || !['human', 'ai'].includes(message.type) || !selectedModel.value) {
|
||||
if (
|
||||
chatStore.isResponding ||
|
||||
!['human', 'ai'].includes(message.type) ||
|
||||
!selectedModel.value ||
|
||||
!credentialsId.value
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const credentialsId = mergedCredentials.value[selectedModel.value.provider];
|
||||
const messageToEdit = message.revisionOfMessageId ?? message.id;
|
||||
|
||||
if (!credentialsId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const mesasgeToEdit = message.revisionOfMessageId ?? message.id;
|
||||
|
||||
chatStore.editMessage(sessionId.value, mesasgeToEdit, message.content, selectedModel.value, {
|
||||
chatStore.editMessage(sessionId.value, messageToEdit, message.content, selectedModel.value, {
|
||||
[PROVIDER_CREDENTIAL_TYPE_MAP[selectedModel.value.provider]]: {
|
||||
id: credentialsId,
|
||||
id: credentialsId.value,
|
||||
name: '',
|
||||
},
|
||||
});
|
||||
|
|
@ -285,13 +279,12 @@ function handleEditMessage(message: ChatHubMessageDto) {
|
|||
}
|
||||
|
||||
function handleRegenerateMessage(message: ChatHubMessageDto) {
|
||||
if (chatStore.isResponding || message.type !== 'ai' || !selectedModel.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const credentialsId = mergedCredentials.value[selectedModel.value.provider];
|
||||
|
||||
if (!credentialsId) {
|
||||
if (
|
||||
chatStore.isResponding ||
|
||||
message.type !== 'ai' ||
|
||||
!selectedModel.value ||
|
||||
!credentialsId.value
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -299,7 +292,7 @@ function handleRegenerateMessage(message: ChatHubMessageDto) {
|
|||
|
||||
chatStore.regenerateMessage(sessionId.value, messageToRetry, selectedModel.value, {
|
||||
[PROVIDER_CREDENTIAL_TYPE_MAP[selectedModel.value.provider]]: {
|
||||
id: credentialsId,
|
||||
id: credentialsId.value,
|
||||
name: '',
|
||||
},
|
||||
});
|
||||
|
|
@ -316,6 +309,27 @@ function handleSelectCredentials(provider: ChatHubProvider, credentialsId: strin
|
|||
function handleSwitchAlternative(messageId: string) {
|
||||
chatStore.switchAlternative(sessionId.value, messageId);
|
||||
}
|
||||
|
||||
function handleConfigureCredentials(provider: ChatHubProvider) {
|
||||
const credentialType = PROVIDER_CREDENTIAL_TYPE_MAP[provider];
|
||||
const existingCredentials = credentialsStore.getCredentialsByType(credentialType);
|
||||
|
||||
if (existingCredentials.length === 0) {
|
||||
uiStore.openNewCredential(credentialType);
|
||||
return;
|
||||
}
|
||||
|
||||
credentialSelectorProvider.value = provider;
|
||||
uiStore.openModal('chatCredentialSelector');
|
||||
}
|
||||
|
||||
function handleConfigureModel() {
|
||||
headerRef.value?.openModelSelector();
|
||||
}
|
||||
|
||||
function handleCreateNewCredential(provider: ChatHubProvider) {
|
||||
uiStore.openNewCredential(PROVIDER_CREDENTIAL_TYPE_MAP[provider]);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -329,13 +343,25 @@ function handleSwitchAlternative(messageId: string) {
|
|||
]"
|
||||
>
|
||||
<ChatConversationHeader
|
||||
v-if="isInitialized"
|
||||
ref="headerRef"
|
||||
:selected-model="selectedModel"
|
||||
:credentials="mergedCredentials"
|
||||
@select-model="handleSelectModel"
|
||||
@select-credentials="handleSelectCredentials"
|
||||
@set-credentials="handleConfigureCredentials"
|
||||
/>
|
||||
|
||||
<CredentialSelectorModal
|
||||
v-if="credentialSelectorProvider"
|
||||
:key="credentialSelectorProvider"
|
||||
:provider="credentialSelectorProvider"
|
||||
:initial-value="mergedCredentials[credentialSelectorProvider] ?? null"
|
||||
@select="handleSelectCredentials"
|
||||
@create-new="handleCreateNewCredential"
|
||||
/>
|
||||
|
||||
<N8nScrollArea
|
||||
v-if="isInitialized"
|
||||
type="scroll"
|
||||
:enable-vertical-scroll="true"
|
||||
:enable-horizontal-scroll="false"
|
||||
|
|
@ -380,13 +406,16 @@ function handleSwitchAlternative(messageId: string) {
|
|||
/>
|
||||
|
||||
<ChatPrompt
|
||||
v-if="isInitialized"
|
||||
ref="inputRef"
|
||||
:class="$style.prompt"
|
||||
:placeholder="inputPlaceholder"
|
||||
:is-responding="chatStore.isResponding"
|
||||
:disabled="chatStore.isResponding"
|
||||
:selected-model="selectedModel"
|
||||
:is-credentials-selected="!!credentialsId"
|
||||
@submit="onSubmit"
|
||||
@stop="onStop"
|
||||
@select-model="handleConfigureModel"
|
||||
@set-credentials="handleConfigureCredentials"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -334,8 +334,8 @@ export const useChatStore = defineStore(CHAT_STORE, () => {
|
|||
function sendMessage(
|
||||
sessionId: ChatSessionId,
|
||||
message: string,
|
||||
model: ChatHubConversationModel | null,
|
||||
credentials: ChatHubSendMessageRequest['credentials'] | null,
|
||||
model: ChatHubConversationModel,
|
||||
credentials: ChatHubSendMessageRequest['credentials'],
|
||||
) {
|
||||
const messageId = uuidv4();
|
||||
const replyId = uuidv4();
|
||||
|
|
@ -364,29 +364,6 @@ export const useChatStore = defineStore(CHAT_STORE, () => {
|
|||
alternatives: [],
|
||||
});
|
||||
|
||||
if (!model || !credentials) {
|
||||
addMessage(sessionId, {
|
||||
id: replyId,
|
||||
sessionId,
|
||||
type: 'ai',
|
||||
name: 'AI',
|
||||
content: '**ERROR:** Select a model to start a conversation.',
|
||||
provider: null,
|
||||
model: null,
|
||||
workflowId: null,
|
||||
executionId: null,
|
||||
status: 'error',
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
previousMessageId: messageId,
|
||||
retryOfMessageId: null,
|
||||
revisionOfMessageId: null,
|
||||
responses: [],
|
||||
alternatives: [],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
sendMessageApi(
|
||||
rootStore.restApiContext,
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,20 +1,17 @@
|
|||
<script setup lang="ts">
|
||||
import { useChatStore } from '@/features/ai/chatHub/chat.store';
|
||||
import type { CredentialsMap } from '@/features/ai/chatHub/chat.types';
|
||||
import CredentialSelectorModal from '@/features/ai/chatHub/components/CredentialSelectorModal.vue';
|
||||
import ModelSelector from '@/features/ai/chatHub/components/ModelSelector.vue';
|
||||
import { useChatHubSidebarState } from '@/features/ai/chatHub/composables/useChatHubSidebarState';
|
||||
import { CHAT_VIEW } from '@/features/ai/chatHub/constants';
|
||||
import { useCredentialsStore } from '@/features/credentials/credentials.store';
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
import {
|
||||
type ChatHubConversationModel,
|
||||
type ChatHubProvider,
|
||||
type ChatSessionId,
|
||||
PROVIDER_CREDENTIAL_TYPE_MAP,
|
||||
} from '@n8n/api-types';
|
||||
import { N8nIconButton } from '@n8n/design-system';
|
||||
import { computed, ref } from 'vue';
|
||||
import { computed, useTemplateRef } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
const { selectedModel, credentials } = defineProps<{
|
||||
|
|
@ -24,17 +21,15 @@ const { selectedModel, credentials } = defineProps<{
|
|||
|
||||
const emit = defineEmits<{
|
||||
selectModel: [ChatHubConversationModel];
|
||||
selectCredentials: [provider: ChatHubProvider, credentialsId: string];
|
||||
setCredentials: [provider: ChatHubProvider];
|
||||
renameConversation: [id: ChatSessionId, title: string];
|
||||
}>();
|
||||
|
||||
const sidebar = useChatHubSidebarState();
|
||||
const chatStore = useChatStore();
|
||||
const uiStore = useUIStore();
|
||||
const credentialsStore = useCredentialsStore();
|
||||
const router = useRouter();
|
||||
|
||||
const credentialSelectorProvider = ref<ChatHubProvider | null>(null);
|
||||
const modelSelectorRef = useTemplateRef('modelSelectorRef');
|
||||
|
||||
const credentialsName = computed(() =>
|
||||
selectedModel
|
||||
|
|
@ -46,32 +41,15 @@ function onModelChange(selection: ChatHubConversationModel) {
|
|||
emit('selectModel', selection);
|
||||
}
|
||||
|
||||
function onConfigure(provider: ChatHubProvider) {
|
||||
const credentialType = PROVIDER_CREDENTIAL_TYPE_MAP[provider];
|
||||
const existingCredentials = credentialsStore.getCredentialsByType(credentialType);
|
||||
|
||||
if (existingCredentials.length === 0) {
|
||||
uiStore.openNewCredential(credentialType);
|
||||
return;
|
||||
}
|
||||
|
||||
credentialSelectorProvider.value = provider;
|
||||
uiStore.openModal('chatCredentialSelector');
|
||||
}
|
||||
|
||||
function onNewChat() {
|
||||
sidebar.toggleOpen(false);
|
||||
|
||||
void router.push({ name: CHAT_VIEW, force: true });
|
||||
}
|
||||
|
||||
function onCredentialSelected(provider: ChatHubProvider, credentialsId: string) {
|
||||
emit('selectCredentials', provider, credentialsId);
|
||||
}
|
||||
|
||||
function onCreateNewCredential(provider: ChatHubProvider) {
|
||||
uiStore.openNewCredential(PROVIDER_CREDENTIAL_TYPE_MAP[provider]);
|
||||
}
|
||||
defineExpose({
|
||||
openModelSelector: () => modelSelectorRef.value?.open(),
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -95,20 +73,12 @@ function onCreateNewCredential(provider: ChatHubProvider) {
|
|||
@click="onNewChat"
|
||||
/>
|
||||
<ModelSelector
|
||||
ref="modelSelectorRef"
|
||||
:models="chatStore.models ?? null"
|
||||
:selected-model="selectedModel"
|
||||
:credentials-name="credentialsName"
|
||||
@change="onModelChange"
|
||||
@configure="onConfigure"
|
||||
/>
|
||||
|
||||
<CredentialSelectorModal
|
||||
v-if="credentialSelectorProvider"
|
||||
:key="credentialSelectorProvider"
|
||||
:provider="credentialSelectorProvider"
|
||||
:initial-value="credentials[credentialSelectorProvider] ?? null"
|
||||
@select="onCredentialSelected"
|
||||
@create-new="onCreateNewCredential"
|
||||
@configure="emit('setCredentials', $event)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,18 +1,23 @@
|
|||
<script setup lang="ts">
|
||||
import { useToast } from '@/composables/useToast';
|
||||
import { providerDisplayNames } from '@/features/ai/chatHub/constants';
|
||||
import type { ChatHubConversationModel, ChatHubProvider } from '@n8n/api-types';
|
||||
import { N8nIconButton, N8nInput } from '@n8n/design-system';
|
||||
import { useSpeechRecognition } from '@vueuse/core';
|
||||
import { computed } from 'vue';
|
||||
import { ref, useTemplateRef, watch } from 'vue';
|
||||
|
||||
const { disabled } = defineProps<{
|
||||
placeholder: string;
|
||||
disabled: boolean;
|
||||
const { selectedModel } = defineProps<{
|
||||
isResponding: boolean;
|
||||
selectedModel: ChatHubConversationModel | null;
|
||||
isCredentialsSelected: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
submit: [string];
|
||||
stop: [];
|
||||
selectModel: [];
|
||||
setCredentials: [ChatHubProvider];
|
||||
}>();
|
||||
|
||||
const inputRef = useTemplateRef('inputRef');
|
||||
|
|
@ -26,6 +31,16 @@ const speechInput = useSpeechRecognition({
|
|||
lang: navigator.language,
|
||||
});
|
||||
|
||||
const placeholder = computed(() => {
|
||||
if (!selectedModel) {
|
||||
return 'Select a model';
|
||||
}
|
||||
|
||||
const modelName = selectedModel.model;
|
||||
|
||||
return `Message ${modelName}`;
|
||||
});
|
||||
|
||||
function onMic() {
|
||||
if (speechInput.isListening.value) {
|
||||
speechInput.stop();
|
||||
|
|
@ -91,6 +106,17 @@ defineExpose({
|
|||
<template>
|
||||
<form :class="$style.prompt" @submit.prevent="handleSubmitForm">
|
||||
<div :class="$style.inputWrap">
|
||||
<div v-if="!selectedModel" :class="$style.callout">
|
||||
Please <a href="" @click.prevent="emit('selectModel')">select a model</a> to start a
|
||||
conversation
|
||||
</div>
|
||||
<div v-else-if="!isCredentialsSelected" :class="$style.callout">
|
||||
Please
|
||||
<a href="" @click.prevent="emit('setCredentials', selectedModel.provider)">
|
||||
set credentials
|
||||
</a>
|
||||
for {{ providerDisplayNames[selectedModel.provider] }} to start a conversation
|
||||
</div>
|
||||
<N8nInput
|
||||
ref="inputRef"
|
||||
v-model="message"
|
||||
|
|
@ -100,6 +126,7 @@ defineExpose({
|
|||
autocomplete="off"
|
||||
:autosize="{ minRows: 1, maxRows: 6 }"
|
||||
autofocus
|
||||
:disabled="!isCredentialsSelected || !selectedModel"
|
||||
@keydown="handleKeydownTextarea"
|
||||
/>
|
||||
|
||||
|
|
@ -109,7 +136,7 @@ defineExpose({
|
|||
native-type="button"
|
||||
type="secondary"
|
||||
title="Attach"
|
||||
:disabled="disabled"
|
||||
:disabled="!isCredentialsSelected || !selectedModel || isResponding"
|
||||
icon="paperclip"
|
||||
icon-size="large"
|
||||
text
|
||||
|
|
@ -120,7 +147,7 @@ defineExpose({
|
|||
native-type="button"
|
||||
:title="speechInput.isListening.value ? 'Stop recording' : 'Voice input'"
|
||||
type="secondary"
|
||||
:disabled="disabled"
|
||||
:disabled="!isCredentialsSelected || !selectedModel || isResponding"
|
||||
:icon="speechInput.isListening.value ? 'square' : 'mic'"
|
||||
:class="{ [$style.recording]: speechInput.isListening.value }"
|
||||
icon-size="large"
|
||||
|
|
@ -129,7 +156,7 @@ defineExpose({
|
|||
<N8nIconButton
|
||||
v-if="!isResponding"
|
||||
native-type="submit"
|
||||
:disabled="disabled || !message.trim()"
|
||||
:disabled="!isCredentialsSelected || !selectedModel || !message.trim()"
|
||||
title="Send"
|
||||
icon="arrow-up"
|
||||
icon-size="large"
|
||||
|
|
@ -157,9 +184,28 @@ defineExpose({
|
|||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.callout {
|
||||
color: var(--color--secondary);
|
||||
background-color: hsla(247, 49%, 53%, 0.1);
|
||||
padding: 16px 16px 32px;
|
||||
border-top-left-radius: 16px;
|
||||
border-top-right-radius: 16px;
|
||||
width: 100%;
|
||||
border: var(--border);
|
||||
border-color: var(--color--secondary);
|
||||
text-align: center;
|
||||
margin-bottom: -16px;
|
||||
|
||||
& a {
|
||||
text-decoration: underline;
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.input {
|
||||
& textarea {
|
||||
font: inherit;
|
||||
|
|
|
|||
|
|
@ -1,14 +1,11 @@
|
|||
<script setup lang="ts">
|
||||
import { useUsersStore } from '@/features/settings/users/users.store';
|
||||
import { useSettingsStore } from '@/stores/settings.store';
|
||||
import { N8nHeading } from '@n8n/design-system';
|
||||
import Logo from '@n8n/design-system/components/N8nLogo/Logo.vue';
|
||||
import { computed } from 'vue';
|
||||
|
||||
defineProps<{ isMobileDevice: boolean }>();
|
||||
|
||||
const userStore = useUsersStore();
|
||||
const settingsStore = useSettingsStore();
|
||||
|
||||
const greetings = computed(
|
||||
() => `Hello, ${userStore.currentUser?.firstName ?? userStore.currentUser?.fullName ?? 'User'}!`,
|
||||
|
|
@ -17,13 +14,9 @@ const greetings = computed(
|
|||
|
||||
<template>
|
||||
<div :class="[$style.starter, { [$style.isMobileDevice]: isMobileDevice }]">
|
||||
<Logo size="large" collapsed :release-channel="settingsStore.settings.releaseChannel" />
|
||||
|
||||
<div :class="$style.header">
|
||||
<N8nHeading tag="h2" bold size="xlarge">
|
||||
{{ greetings }}
|
||||
</N8nHeading>
|
||||
</div>
|
||||
<N8nHeading tag="h2" bold size="xlarge">
|
||||
{{ greetings }}
|
||||
</N8nHeading>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { computed, useTemplateRef } from 'vue';
|
||||
import { N8nNavigationDropdown, N8nIcon, N8nButton, N8nText } from '@n8n/design-system';
|
||||
import { type ComponentProps } from 'vue-component-type-helpers';
|
||||
import {
|
||||
|
|
@ -11,6 +11,7 @@ import {
|
|||
} from '@n8n/api-types';
|
||||
import { providerDisplayNames } from '@/features/ai/chatHub/constants';
|
||||
import CredentialIcon from '@/features/credentials/components/CredentialIcon.vue';
|
||||
import { onClickOutside } from '@vueuse/core';
|
||||
|
||||
const props = defineProps<{
|
||||
models: ChatModelsResponse | null;
|
||||
|
|
@ -23,6 +24,8 @@ const emit = defineEmits<{
|
|||
configure: [ChatHubProvider];
|
||||
}>();
|
||||
|
||||
const dropdownRef = useTemplateRef('dropdownRef');
|
||||
|
||||
const menu = computed(() =>
|
||||
chatHubProviderSchema.options.map((provider) => {
|
||||
const models = props.models?.[provider].models ?? [];
|
||||
|
|
@ -76,10 +79,19 @@ function onSelect(id: string) {
|
|||
|
||||
emit('change', { provider: parsedProvider, model, workflowId: null });
|
||||
}
|
||||
|
||||
onClickOutside(
|
||||
computed(() => dropdownRef.value?.$el),
|
||||
() => dropdownRef.value?.close(),
|
||||
);
|
||||
|
||||
defineExpose({
|
||||
open: () => dropdownRef.value?.open(),
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<N8nNavigationDropdown :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"
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user