mirror of
https://github.com/n8n-io/n8n.git
synced 2026-05-31 00:37:10 +02:00
fix: Chat UI feedback 2 (no-changelog) (#22109)
This commit is contained in:
parent
6f2709608f
commit
a8ccc9f53e
|
|
@ -135,7 +135,7 @@ watch(
|
|||
behaviors
|
||||
</N8nText>
|
||||
</div>
|
||||
<N8nButton icon="plus" type="primary" size="large" @click="handleCreateAgent">
|
||||
<N8nButton icon="plus" type="primary" size="medium" @click="handleCreateAgent">
|
||||
New Agent
|
||||
</N8nButton>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,11 @@ import {
|
|||
LOCAL_STORAGE_CHAT_HUB_SELECTED_TOOLS,
|
||||
VIEWS,
|
||||
} from '@/app/constants';
|
||||
import { findOneFromModelsResponse, unflattenModel } from '@/features/ai/chatHub/chat.utils';
|
||||
import {
|
||||
findOneFromModelsResponse,
|
||||
isLlmProvider,
|
||||
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';
|
||||
import ChatPrompt from '@/features/ai/chatHub/components/ChatPrompt.vue';
|
||||
|
|
@ -67,11 +71,7 @@ const currentConversationTitle = computed(() => currentConversation.value?.title
|
|||
const readyToShowMessages = computed(() => chatStore.agentsReady);
|
||||
|
||||
// TODO: This also depends on the model, not all base LLM models support tools.
|
||||
const canSelectTools = computed(
|
||||
() =>
|
||||
selectedModel.value?.model.provider !== 'custom-agent' &&
|
||||
selectedModel.value?.model.provider !== 'n8n',
|
||||
);
|
||||
const canSelectTools = computed(() => isLlmProvider(selectedModel.value?.model.provider));
|
||||
|
||||
const { arrivedState, measure } = useScroll(scrollContainerRef, {
|
||||
throttle: 100,
|
||||
|
|
@ -186,7 +186,7 @@ const credentialsForSelectedProvider = computed<ChatHubSendMessageRequest['crede
|
|||
return null;
|
||||
}
|
||||
|
||||
if (provider === 'custom-agent' || provider === 'n8n') {
|
||||
if (!isLlmProvider(provider)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
|
|
@ -331,6 +331,7 @@ function onSubmit(message: string, attachments: File[]) {
|
|||
}
|
||||
|
||||
didSubmitInCurrentSession.value = true;
|
||||
editingMessageId.value = undefined;
|
||||
|
||||
void chatStore.sendMessage(
|
||||
sessionId.value,
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ import type {
|
|||
} from './chat.types';
|
||||
import { retry } from '@n8n/utils/retry';
|
||||
import { convertFileToChatAttachment } from '@/app/utils/fileUtils';
|
||||
import { buildUiMessages, isMatchedAgent } from './chat.utils';
|
||||
import { buildUiMessages, isLlmProviderModel, isMatchedAgent } from './chat.utils';
|
||||
import { createAiMessageFromStreamingState, flattenModel } from './chat.utils';
|
||||
import { useToast } from '@/app/composables/useToast';
|
||||
import { useTelemetry } from '@/app/composables/useTelemetry';
|
||||
|
|
@ -260,6 +260,7 @@ export const useChatStore = defineStore(CHAT_STORE, () => {
|
|||
sessionId: ChatSessionId,
|
||||
messageId: ChatMessageId,
|
||||
status: ChatHubMessageStatus,
|
||||
content?: string,
|
||||
) {
|
||||
const conversation = ensureConversation(sessionId);
|
||||
const message = conversation.messages[messageId];
|
||||
|
|
@ -268,6 +269,9 @@ export const useChatStore = defineStore(CHAT_STORE, () => {
|
|||
}
|
||||
|
||||
message.status = status;
|
||||
if (content) {
|
||||
message.content = content;
|
||||
}
|
||||
message.updatedAt = new Date().toISOString();
|
||||
}
|
||||
|
||||
|
|
@ -419,8 +423,7 @@ export const useChatStore = defineStore(CHAT_STORE, () => {
|
|||
return;
|
||||
}
|
||||
|
||||
updateMessage(sessionId, chunk.metadata.messageId, 'error');
|
||||
onChunk(message.content ?? '');
|
||||
updateMessage(sessionId, chunk.metadata.messageId, 'error', chunk.content);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
@ -498,7 +501,7 @@ export const useChatStore = defineStore(CHAT_STORE, () => {
|
|||
name: 'User',
|
||||
content: message,
|
||||
provider: null,
|
||||
model: model.provider === 'n8n' || model.provider === 'custom-agent' ? null : model.model,
|
||||
model: isLlmProviderModel(model) ? model.model : null,
|
||||
workflowId: null,
|
||||
executionId: null,
|
||||
agentId: null,
|
||||
|
|
@ -785,10 +788,12 @@ export const useChatStore = defineStore(CHAT_STORE, () => {
|
|||
function getAgent(model: ChatHubConversationModel) {
|
||||
if (!agents.value) return null;
|
||||
|
||||
const agent = agents.value[model.provider].models.find((agent) => isMatchedAgent(agent, model));
|
||||
const agent = agents.value[model.provider]?.models.find((agent) =>
|
||||
isMatchedAgent(agent, model),
|
||||
);
|
||||
|
||||
if (!agent) {
|
||||
if (model.provider === 'custom-agent' || model.provider === 'n8n') {
|
||||
if (!isLlmProviderModel(model)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ import {
|
|||
type ChatModelDto,
|
||||
type ChatSessionId,
|
||||
type ChatMessageId,
|
||||
type ChatHubProvider,
|
||||
type ChatHubLLMProvider,
|
||||
} from '@n8n/api-types';
|
||||
import type {
|
||||
ChatMessage,
|
||||
|
|
@ -320,3 +322,13 @@ export function buildUiMessages(
|
|||
|
||||
return messagesToShow;
|
||||
}
|
||||
|
||||
export function isLlmProvider(provider?: ChatHubProvider): provider is ChatHubLLMProvider {
|
||||
return provider !== 'n8n' && provider !== 'custom-agent';
|
||||
}
|
||||
|
||||
export function isLlmProviderModel(
|
||||
model?: ChatHubConversationModel,
|
||||
): model is ChatHubConversationModel & { provider: ChatHubLLMProvider } {
|
||||
return isLlmProvider(model?.provider);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import { computed, onMounted, ref } from 'vue';
|
|||
import type { CredentialsMap } from '../chat.types';
|
||||
import type { INode } from 'n8n-workflow';
|
||||
import ToolsSelector from './ToolsSelector.vue';
|
||||
import { isLlmProviderModel } from '@/features/ai/chatHub/chat.utils';
|
||||
|
||||
const props = defineProps<{
|
||||
modalName: string;
|
||||
|
|
@ -109,8 +110,7 @@ async function onSave() {
|
|||
|
||||
const model = 'model' in selectedModel.value ? selectedModel.value.model : undefined;
|
||||
|
||||
assert(model);
|
||||
assert(model.provider !== 'n8n' && model.provider !== 'custom-agent');
|
||||
assert(isLlmProviderModel(model));
|
||||
|
||||
const credentialId = agentMergedCredentials.value[model.provider];
|
||||
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ defineExpose({
|
|||
ref="modelSelectorRef"
|
||||
:selected-agent="selectedModel"
|
||||
:credentials="credentials"
|
||||
text
|
||||
@change="onModelChange"
|
||||
@create-custom-agent="emit('createCustomAgent')"
|
||||
@select-credential="
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { N8nButton, N8nIcon, N8nInput } from '@n8n/design-system';
|
|||
import { useSpeechSynthesis } from '@vueuse/core';
|
||||
import type MarkdownIt from 'markdown-it';
|
||||
import markdownLink from 'markdown-it-link-attributes';
|
||||
import { computed, nextTick, onBeforeMount, ref, useTemplateRef, watch } from 'vue';
|
||||
import { computed, onBeforeMount, ref, useTemplateRef, watch } from 'vue';
|
||||
import VueMarkdown from 'vue-markdown-render';
|
||||
import type { ChatMessage } from '../chat.types';
|
||||
import ChatMessageActions from './ChatMessageActions.vue';
|
||||
|
|
@ -17,6 +17,7 @@ import { useChatStore } from '@/features/ai/chatHub/chat.store';
|
|||
import ChatFile from '@n8n/chat/components/ChatFile.vue';
|
||||
import { buildChatAttachmentUrl } from '@/features/ai/chatHub/chat.api';
|
||||
import { useRootStore } from '@n8n/stores/useRootStore';
|
||||
import { useDeviceSupport } from '@n8n/composables/useDeviceSupport';
|
||||
|
||||
const { message, compact, isEditing, isStreaming, minHeight } = defineProps<{
|
||||
message: ChatMessage;
|
||||
|
|
@ -40,6 +41,7 @@ const emit = defineEmits<{
|
|||
const clipboard = useClipboard();
|
||||
const chatStore = useChatStore();
|
||||
const rootStore = useRootStore();
|
||||
const { isCtrlKeyPressed } = useDeviceSupport();
|
||||
|
||||
const editedText = ref('');
|
||||
const textareaRef = useTemplateRef('textarea');
|
||||
|
|
@ -96,6 +98,15 @@ function handleConfirmEdit() {
|
|||
emit('update', { ...message, content: editedText.value });
|
||||
}
|
||||
|
||||
function handleKeydownTextarea(e: KeyboardEvent) {
|
||||
const trimmed = editedText.value.trim();
|
||||
|
||||
if (e.key === 'Enter' && isCtrlKeyPressed(e) && !e.isComposing && trimmed) {
|
||||
e.preventDefault();
|
||||
handleConfirmEdit();
|
||||
}
|
||||
}
|
||||
|
||||
function handleRegenerate() {
|
||||
emit('regenerate', message);
|
||||
}
|
||||
|
|
@ -126,18 +137,24 @@ const linksNewTabPlugin = (vueMarkdownItInstance: MarkdownIt) => {
|
|||
// Watch for isEditing prop changes to initialize edit mode
|
||||
watch(
|
||||
() => isEditing,
|
||||
async (editing) => {
|
||||
if (editing) {
|
||||
editedText.value = message.content;
|
||||
await nextTick();
|
||||
textareaRef.value?.focus();
|
||||
} else {
|
||||
editedText.value = '';
|
||||
}
|
||||
(editing) => {
|
||||
editedText.value = editing ? message.content : '';
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
watch(
|
||||
textareaRef,
|
||||
async (textarea) => {
|
||||
if (textarea) {
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
textarea.focus();
|
||||
textarea.$el.scrollIntoView({ block: 'nearest' });
|
||||
}
|
||||
},
|
||||
{ immediate: true, flush: 'post' },
|
||||
);
|
||||
|
||||
onBeforeMount(() => {
|
||||
speech.stop();
|
||||
});
|
||||
|
|
@ -168,6 +185,7 @@ onBeforeMount(() => {
|
|||
type="textarea"
|
||||
:autosize="{ minRows: 3, maxRows: 20 }"
|
||||
:class="$style.textarea"
|
||||
@keydown="handleKeydownTextarea"
|
||||
/>
|
||||
<div :class="$style.editActions">
|
||||
<N8nButton type="secondary" size="small" @click="handleCancelEdit"> Cancel </N8nButton>
|
||||
|
|
@ -181,7 +199,7 @@ onBeforeMount(() => {
|
|||
</N8nButton>
|
||||
</div>
|
||||
</div>
|
||||
<template v-else>
|
||||
<div v-else>
|
||||
<div :class="[$style.chatMessage, { [$style.errorMessage]: message.status === 'error' }]">
|
||||
<div v-if="attachments.length > 0" :class="$style.attachments">
|
||||
<ChatFile
|
||||
|
|
@ -192,7 +210,9 @@ onBeforeMount(() => {
|
|||
:href="attachment.downloadUrl"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="message.type === 'human'">{{ message.content }}</div>
|
||||
<VueMarkdown
|
||||
v-else
|
||||
:key="forceReRenderKey"
|
||||
:class="[$style.chatMessageMarkdown, 'chat-message-markdown']"
|
||||
:source="
|
||||
|
|
@ -219,7 +239,7 @@ onBeforeMount(() => {
|
|||
@read-aloud="handleReadAloud"
|
||||
@switchAlternative="handleSwitchAlternative"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -267,11 +287,14 @@ onBeforeMount(() => {
|
|||
gap: var(--spacing--2xs);
|
||||
position: relative;
|
||||
max-width: fit-content;
|
||||
overflow-wrap: break-word;
|
||||
|
||||
.user & {
|
||||
padding: var(--spacing--3xs) var(--spacing--sm);
|
||||
padding: var(--spacing--2xs) var(--spacing--sm);
|
||||
border-radius: var(--radius--xl);
|
||||
background-color: var(--color--background);
|
||||
white-space-collapse: preserve-breaks;
|
||||
line-height: 1.8;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -389,6 +412,10 @@ onBeforeMount(() => {
|
|||
gap: var(--spacing--2xs);
|
||||
}
|
||||
|
||||
.textarea {
|
||||
scroll-margin-block: var(--spacing--sm);
|
||||
}
|
||||
|
||||
.textarea textarea {
|
||||
font-family: inherit;
|
||||
background-color: var(--color--background--light-3);
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { useSpeechRecognition } from '@vueuse/core';
|
|||
import type { INode } from 'n8n-workflow';
|
||||
import { computed, ref, useTemplateRef, watch } from 'vue';
|
||||
import ToolsSelector from './ToolsSelector.vue';
|
||||
import { isLlmProviderModel } from '@/features/ai/chatHub/chat.utils';
|
||||
|
||||
const { selectedModel, selectedTools, isMissingCredentials } = defineProps<{
|
||||
isResponding: boolean;
|
||||
|
|
@ -29,6 +30,7 @@ const emit = defineEmits<{
|
|||
const inputRef = useTemplateRef<HTMLElement>('inputRef');
|
||||
const fileInputRef = useTemplateRef<HTMLInputElement>('fileInputRef');
|
||||
const message = ref('');
|
||||
const committedSpokenMessage = ref('');
|
||||
const attachments = ref<File[]>([]);
|
||||
|
||||
const toast = useToast();
|
||||
|
|
@ -44,12 +46,12 @@ const placeholder = computed(() =>
|
|||
);
|
||||
|
||||
const llmProvider = computed<ChatHubLLMProvider | undefined>(() =>
|
||||
selectedModel?.model.provider === 'n8n' || selectedModel?.model.provider === 'custom-agent'
|
||||
? undefined
|
||||
: selectedModel?.model.provider,
|
||||
isLlmProviderModel(selectedModel?.model) ? selectedModel?.model.provider : undefined,
|
||||
);
|
||||
|
||||
function onMic() {
|
||||
committedSpokenMessage.value = message.value;
|
||||
|
||||
if (speechInput.isListening.value) {
|
||||
speechInput.stop();
|
||||
} else {
|
||||
|
|
@ -97,6 +99,7 @@ function handleSubmitForm() {
|
|||
speechInput.stop();
|
||||
emit('submit', trimmed, attachments.value);
|
||||
message.value = '';
|
||||
committedSpokenMessage.value = '';
|
||||
attachments.value = [];
|
||||
}
|
||||
}
|
||||
|
|
@ -109,6 +112,7 @@ function handleKeydownTextarea(e: KeyboardEvent) {
|
|||
speechInput.stop();
|
||||
emit('submit', trimmed, attachments.value);
|
||||
message.value = '';
|
||||
committedSpokenMessage.value = '';
|
||||
attachments.value = [];
|
||||
}
|
||||
}
|
||||
|
|
@ -118,11 +122,19 @@ function handleClickInputWrapper() {
|
|||
}
|
||||
|
||||
watch(speechInput.result, (spoken) => {
|
||||
if (spoken) {
|
||||
message.value = spoken;
|
||||
}
|
||||
message.value = committedSpokenMessage.value + ' ' + spoken.trimStart();
|
||||
});
|
||||
|
||||
watch(
|
||||
speechInput.isFinal,
|
||||
(final) => {
|
||||
if (final) {
|
||||
committedSpokenMessage.value = message.value;
|
||||
}
|
||||
},
|
||||
{ flush: 'post' },
|
||||
);
|
||||
|
||||
watch(speechInput.error, (event) => {
|
||||
if (event?.error === 'not-allowed') {
|
||||
toast.showError(
|
||||
|
|
@ -172,12 +184,12 @@ defineExpose({
|
|||
<N8nText v-else-if="isMissingCredentials && llmProvider" :class="$style.callout">
|
||||
<template v-if="isNewSession">
|
||||
Please
|
||||
<a href="" @click.prevent="emit('setCredentials', llmProvider)"> set credentials </a>
|
||||
<a href="" @click.prevent="emit('setCredentials', llmProvider)">set credentials</a>
|
||||
for {{ providerDisplayNames[llmProvider] }} to start a conversation
|
||||
</template>
|
||||
<template v-else>
|
||||
Please
|
||||
<a href="" @click.prevent="emit('setCredentials', llmProvider)"> set credentials </a>
|
||||
<a href="" @click.prevent="emit('setCredentials', llmProvider)">set credentials</a>
|
||||
for {{ providerDisplayNames[llmProvider] }} to continue the conversation
|
||||
</template>
|
||||
</N8nText>
|
||||
|
|
@ -217,8 +229,10 @@ defineExpose({
|
|||
<div :class="$style.footer">
|
||||
<div v-if="isToolsSelectable" :class="$style.tools">
|
||||
<ToolsSelector
|
||||
:class="$style.toolsButton"
|
||||
:selected="selectedTools ?? []"
|
||||
:disabled="isMissingCredentials || !selectedModel || isResponding"
|
||||
transparent-bg
|
||||
@select="onSelectTools"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -315,6 +329,7 @@ defineExpose({
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing--sm);
|
||||
transition: border-color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
|
||||
|
||||
&:focus-within,
|
||||
&:hover {
|
||||
|
|
@ -343,21 +358,8 @@ defineExpose({
|
|||
}
|
||||
|
||||
.toolsButton {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing--2xs);
|
||||
padding: var(--spacing--3xs) var(--spacing--xs);
|
||||
color: var(--color--text);
|
||||
cursor: pointer;
|
||||
|
||||
border-radius: var(--radius);
|
||||
border: var(--border);
|
||||
background: var(--color--background--light-3);
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
/* maintain the same height with other buttons regardless of selected tools */
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.iconStack {
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ function handleKeyDown(e: KeyboardEvent) {
|
|||
return;
|
||||
}
|
||||
|
||||
if (e.key === 'Enter') {
|
||||
if (e.key === 'Enter' && !e.isComposing) {
|
||||
handleBlur();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,7 +33,12 @@ defineSlots<{
|
|||
<div :class="[$style.menuItem, { [$style.active]: active }]">
|
||||
<slot v-if="$slots.default" />
|
||||
<template v-else>
|
||||
<RouterLink :to="to" :class="$style.menuItemLink" @click="emit('click', $event)">
|
||||
<RouterLink
|
||||
:to="to"
|
||||
:class="$style.menuItemLink"
|
||||
:title="label"
|
||||
@click="emit('click', $event)"
|
||||
>
|
||||
<slot name="icon">
|
||||
<N8nIcon v-if="icon" size="large" :icon="icon" />
|
||||
</slot>
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ import ChatAgentAvatar from '@/features/ai/chatHub/components/ChatAgentAvatar.vu
|
|||
import {
|
||||
flattenModel,
|
||||
fromStringToModel,
|
||||
isLlmProviderModel,
|
||||
isMatchedAgent,
|
||||
stringifyModel,
|
||||
} from '@/features/ai/chatHub/chat.utils';
|
||||
|
|
@ -43,10 +44,12 @@ const {
|
|||
selectedAgent,
|
||||
includeCustomAgents = true,
|
||||
credentials,
|
||||
text,
|
||||
} = defineProps<{
|
||||
selectedAgent: ChatModelDto | null;
|
||||
includeCustomAgents?: boolean;
|
||||
credentials: CredentialsMap | null;
|
||||
text?: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
|
|
@ -86,12 +89,7 @@ const credentialsName = computed(() =>
|
|||
? credentialsStore.getCredentialById(credentials?.[selectedAgent.model.provider] ?? '')?.name
|
||||
: undefined,
|
||||
);
|
||||
const isCredentialsRequired = computed(
|
||||
() =>
|
||||
selectedAgent &&
|
||||
selectedAgent.model.provider !== 'n8n' &&
|
||||
selectedAgent.model.provider !== 'custom-agent',
|
||||
);
|
||||
const isCredentialsRequired = computed(() => isLlmProviderModel(selectedAgent?.model));
|
||||
|
||||
const menu = computed(() => {
|
||||
const menuItems: (typeof N8nNavigationDropdown)['menu'] = [];
|
||||
|
|
@ -153,7 +151,6 @@ const menu = computed(() => {
|
|||
const agentOptions =
|
||||
theAgents.length > 0
|
||||
? theAgents
|
||||
.filter((agent) => agent.model.provider !== 'custom-agent')
|
||||
.filter(
|
||||
(agent) =>
|
||||
agent.model.provider === 'n8n' ||
|
||||
|
|
@ -250,20 +247,12 @@ function onSelect(id: string) {
|
|||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
identifier === 'configure' &&
|
||||
parsedModel.provider !== 'n8n' &&
|
||||
parsedModel.provider !== 'custom-agent'
|
||||
) {
|
||||
if (identifier === 'configure' && isLlmProviderModel(parsedModel)) {
|
||||
openCredentialsSelectorOrCreate(parsedModel.provider);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
identifier === 'add-model' &&
|
||||
parsedModel.provider !== 'n8n' &&
|
||||
parsedModel.provider !== 'custom-agent'
|
||||
) {
|
||||
if (identifier === 'add-model' && isLlmProviderModel(parsedModel)) {
|
||||
openModelByIdSelector(parsedModel.provider);
|
||||
return;
|
||||
}
|
||||
|
|
@ -335,7 +324,7 @@ defineExpose({
|
|||
/>
|
||||
</template>
|
||||
|
||||
<N8nButton :class="$style.dropdownButton" type="secondary" text>
|
||||
<N8nButton :class="$style.dropdownButton" type="secondary" :text="text">
|
||||
<ChatAgentAvatar
|
||||
v-if="selectedAgent"
|
||||
:agent="selectedAgent"
|
||||
|
|
|
|||
|
|
@ -7,9 +7,10 @@ import type { INode } from 'n8n-workflow';
|
|||
import { computed, onMounted } from 'vue';
|
||||
import { TOOLS_SELECTOR_MODAL_KEY } from '../constants';
|
||||
|
||||
const { selected } = defineProps<{
|
||||
const { selected, transparentBg = false } = defineProps<{
|
||||
disabled: boolean;
|
||||
selected: INode[];
|
||||
transparentBg?: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
|
|
@ -51,7 +52,8 @@ const onClick = () => {
|
|||
<template>
|
||||
<N8nButton
|
||||
type="secondary"
|
||||
:class="$style.toolsButton"
|
||||
native-type="button"
|
||||
:class="[$style.toolsButton, { [$style.transparentBg]: transparentBg }]"
|
||||
:disabled="disabled"
|
||||
aria-label="Select tools"
|
||||
:icon="toolCount > 0 ? undefined : 'plus'"
|
||||
|
|
@ -77,8 +79,10 @@ const onClick = () => {
|
|||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing--2xs);
|
||||
padding: var(--spacing--4xs) var(--spacing--2xs);
|
||||
background-color: transparent;
|
||||
|
||||
&.transparentBg {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.iconStack {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import {
|
|||
} from '@n8n/api-types';
|
||||
import { useLocalStorage } from '@vueuse/core';
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { isLlmProvider } from '../chat.utils';
|
||||
|
||||
/**
|
||||
* Composable for managing chat credentials including auto-selection and user selection.
|
||||
|
|
@ -37,10 +38,14 @@ export function useChatCredentials(userId: string) {
|
|||
},
|
||||
);
|
||||
|
||||
const isCredentialsReady = computed(
|
||||
() => isInitialized.value || credentialsStore.allCredentials.length > 0,
|
||||
);
|
||||
|
||||
const autoSelectCredentials = computed<CredentialsMap>(() =>
|
||||
Object.fromEntries(
|
||||
chatHubProviderSchema.options.map((provider) => {
|
||||
if (provider === 'n8n' || provider === 'custom-agent') {
|
||||
if (!isLlmProvider(provider)) {
|
||||
return [provider, null];
|
||||
}
|
||||
|
||||
|
|
@ -73,7 +78,7 @@ export function useChatCredentials(userId: string) {
|
|||
);
|
||||
|
||||
const credentialsByProvider = computed<CredentialsMap | null>(() =>
|
||||
isInitialized.value
|
||||
isCredentialsReady.value
|
||||
? {
|
||||
...autoSelectCredentials.value,
|
||||
...selectedCredentials.value,
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user