From af1096b3e995d075229cf52ada33c0a2ccb5e55f Mon Sep 17 00:00:00 2001 From: Suguru Inoue Date: Wed, 22 Oct 2025 12:44:01 +0200 Subject: [PATCH] feat(editor): Show callout when credentials are not set (no-changelog) (#20997) --- .../NavigationDropdown.vue | 5 + .../src/features/ai/chatHub/ChatView.vue | 125 +++++++++++------- .../src/features/ai/chatHub/chat.store.ts | 27 +--- .../components/ChatConversationHeader.vue | 46 ++----- .../ai/chatHub/components/ChatPrompt.vue | 58 +++++++- .../ai/chatHub/components/ChatStarter.vue | 13 +- .../ai/chatHub/components/ModelSelector.vue | 16 ++- 7 files changed, 161 insertions(+), 129 deletions(-) diff --git a/packages/frontend/@n8n/design-system/src/components/N8nNavigationDropdown/NavigationDropdown.vue b/packages/frontend/@n8n/design-system/src/components/N8nNavigationDropdown/NavigationDropdown.vue index d97f53f17e2..fc90af419a7 100644 --- a/packages/frontend/@n8n/design-system/src/components/N8nNavigationDropdown/NavigationDropdown.vue +++ b/packages/frontend/@n8n/design-system/src/components/N8nNavigationDropdown/NavigationDropdown.vue @@ -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, }); diff --git a/packages/frontend/editor-ui/src/features/ai/chatHub/ChatView.vue b/packages/frontend/editor-ui/src/features/ai/chatHub/ChatView.vue index 5c1079ea7ba..024075efa15 100644 --- a/packages/frontend/editor-ui/src/features/ai/chatHub/ChatView.vue +++ b/packages/frontend/editor-ui/src/features/ai/chatHub/ChatView.vue @@ -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(() => 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(); const didSubmitInCurrentSession = ref(false); +const initialization = ref({ credentialsFetched: false, modelsFetched: false }); +const credentialSelectorProvider = ref(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]); +}