mirror of
https://github.com/n8n-io/n8n.git
synced 2026-09-24 23:22:38 +08:00
perf: Improve chat UI time-to-interactive (no-changelog) (#22408)
This commit is contained in:
@@ -274,6 +274,7 @@ export class ChatHubSendMessageRequest extends Z.class({
|
||||
),
|
||||
tools: z.array(INodeSchema),
|
||||
attachments: z.array(chatAttachmentSchema),
|
||||
agentName: z.string(),
|
||||
}) {}
|
||||
|
||||
export class ChatHubRegenerateMessageRequest extends Z.class({
|
||||
@@ -301,10 +302,12 @@ export class ChatHubEditMessageRequest extends Z.class({
|
||||
export class ChatHubUpdateConversationRequest extends Z.class({
|
||||
title: z.string().optional(),
|
||||
credentialId: z.string().max(36).optional(),
|
||||
provider: chatHubProviderSchema.optional(),
|
||||
model: z.string().max(64).optional(),
|
||||
workflowId: z.string().max(36).optional(),
|
||||
agentId: z.string().uuid().optional(),
|
||||
agent: z
|
||||
.object({
|
||||
model: chatHubConversationModelSchema,
|
||||
name: z.string(),
|
||||
})
|
||||
.optional(),
|
||||
tools: z.array(INodeSchema).optional(),
|
||||
}) {}
|
||||
|
||||
@@ -324,7 +327,7 @@ export interface ChatHubSessionDto {
|
||||
model: string | null;
|
||||
workflowId: string | null;
|
||||
agentId: string | null;
|
||||
agentName: string | null;
|
||||
agentName: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
tools: INode[];
|
||||
|
||||
@@ -97,10 +97,9 @@ export class ChatHubSession extends WithTimestamps {
|
||||
@Column({ type: 'varchar', length: 36, nullable: true })
|
||||
agentId: string | null;
|
||||
|
||||
/*
|
||||
* Cached name of the custom agent to use (if applicable).
|
||||
* In case agent gets deleted
|
||||
* Only set when provider is 'custom-agent'.
|
||||
/**
|
||||
* Cached display name of the agent/model.
|
||||
* Used for all providers (LLM providers, custom agents, and n8n workflows).
|
||||
*/
|
||||
@Column({ type: 'varchar', length: 128, nullable: true })
|
||||
agentName: string | null;
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { INodeTypeNameVersion } from 'n8n-workflow';
|
||||
export const CONVERSATION_TITLE_GENERATION_PROMPT = `Generate a concise, descriptive title for this conversation based on the user's message.
|
||||
|
||||
Requirements:
|
||||
- 3 to 5 words
|
||||
- 2 to 5 words
|
||||
- Use normal sentence case (not title case)
|
||||
- No quotation marks
|
||||
- Only output the title, nothing else
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
ChatHubN8nModel,
|
||||
ChatHubCustomAgentModel,
|
||||
emptyChatModelsResponse,
|
||||
type ChatHubUpdateConversationRequest,
|
||||
} from '@n8n/api-types';
|
||||
import { Logger } from '@n8n/backend-common';
|
||||
import { ExecutionRepository, IExecutionResponse, User, WorkflowRepository } from '@n8n/db';
|
||||
@@ -68,6 +69,7 @@ import { getBase } from '@/workflow-execute-additional-data';
|
||||
import { WorkflowExecutionService } from '@/workflows/workflow-execution.service';
|
||||
import { WorkflowFinderService } from '@/workflows/workflow-finder.service';
|
||||
import { WorkflowService } from '@/workflows/workflow.service';
|
||||
import type { ChatHubSession } from './chat-hub-session.entity';
|
||||
|
||||
@Service()
|
||||
export class ChatHubService {
|
||||
@@ -1002,7 +1004,15 @@ export class ChatHubService {
|
||||
try {
|
||||
const result = await this.messageRepository.manager.transaction(async (trx) => {
|
||||
let session = await this.getChatSession(user, sessionId, trx);
|
||||
session ??= await this.createChatSession(user, sessionId, model, credentialId, tools, trx);
|
||||
session ??= await this.createChatSession(
|
||||
user,
|
||||
sessionId,
|
||||
model,
|
||||
credentialId,
|
||||
tools,
|
||||
payload.agentName,
|
||||
trx,
|
||||
);
|
||||
|
||||
await this.ensurePreviousMessage(previousMessageId, sessionId, trx);
|
||||
const messages = Object.fromEntries((session.messages ?? []).map((m) => [m.id, m]));
|
||||
@@ -1974,44 +1984,10 @@ export class ChatHubService {
|
||||
model: ChatHubConversationModel,
|
||||
credentialId: string | null,
|
||||
tools: INode[],
|
||||
agentName: string,
|
||||
trx?: EntityManager,
|
||||
) {
|
||||
let agentName: string | undefined = undefined;
|
||||
|
||||
if (model.provider === 'custom-agent') {
|
||||
// Find the agent to get its name
|
||||
const agent = await this.chatHubAgentService.getAgentById(model.agentId, user.id);
|
||||
if (!agent) {
|
||||
throw new BadRequestError('Agent not found for chat session initialization');
|
||||
}
|
||||
agentName = agent.name;
|
||||
}
|
||||
|
||||
if (model.provider === 'n8n') {
|
||||
// Find the workflow to get its name
|
||||
const workflow = await this.workflowFinderService.findWorkflowForUser(
|
||||
model.workflowId,
|
||||
user,
|
||||
['workflow:read'],
|
||||
{ includeTags: false, includeParentFolder: false },
|
||||
);
|
||||
|
||||
if (!workflow) {
|
||||
throw new BadRequestError('Workflow not found for chat session initialization');
|
||||
}
|
||||
|
||||
const chatTrigger = workflow.nodes?.find((node) => node.type === CHAT_TRIGGER_NODE_TYPE);
|
||||
if (!chatTrigger) {
|
||||
throw new BadRequestError(
|
||||
'Chat trigger not found in workflow for chat session initialization',
|
||||
);
|
||||
}
|
||||
agentName =
|
||||
typeof chatTrigger.parameters.agentName === 'string' &&
|
||||
chatTrigger.parameters.agentName.length > 0
|
||||
? chatTrigger.parameters.agentName
|
||||
: workflow.name;
|
||||
}
|
||||
await this.ensureValidModel(user, model);
|
||||
|
||||
return await this.sessionRepository.createChatSession(
|
||||
{
|
||||
@@ -2065,7 +2041,7 @@ export class ChatHubService {
|
||||
model: session.model,
|
||||
workflowId: session.workflowId,
|
||||
agentId: session.agentId,
|
||||
agentName: session.agentName,
|
||||
agentName: session.agentName ?? '',
|
||||
createdAt: session.createdAt.toISOString(),
|
||||
updatedAt: session.updatedAt.toISOString(),
|
||||
tools: session.tools,
|
||||
@@ -2097,7 +2073,7 @@ export class ChatHubService {
|
||||
model: session.model,
|
||||
workflowId: session.workflowId,
|
||||
agentId: session.agentId,
|
||||
agentName: session.agentName,
|
||||
agentName: session.agentName ?? '',
|
||||
createdAt: session.createdAt.toISOString(),
|
||||
updatedAt: session.updatedAt.toISOString(),
|
||||
tools: session.tools,
|
||||
@@ -2184,15 +2160,7 @@ export class ChatHubService {
|
||||
async updateSession(
|
||||
user: User,
|
||||
sessionId: ChatSessionId,
|
||||
updates: {
|
||||
title?: string;
|
||||
credentialId?: string | null;
|
||||
provider?: ChatHubProvider;
|
||||
model?: string | null;
|
||||
workflowId?: string | null;
|
||||
agentId?: string | null;
|
||||
agentName?: string | null;
|
||||
},
|
||||
updates: ChatHubUpdateConversationRequest,
|
||||
) {
|
||||
const session = await this.sessionRepository.getOneById(sessionId, user.id);
|
||||
|
||||
@@ -2200,62 +2168,34 @@ export class ChatHubService {
|
||||
throw new NotFoundError('Session not found');
|
||||
}
|
||||
|
||||
if (updates.workflowId) {
|
||||
// Validate the workflow exists and is accessible
|
||||
const workflow = await this.workflowFinderService.findWorkflowForUser(
|
||||
updates.workflowId,
|
||||
user,
|
||||
['workflow:read'],
|
||||
{ includeTags: false, includeParentFolder: false },
|
||||
);
|
||||
// Prepare the actual updates to be sent to the repository
|
||||
const sessionUpdates: Partial<ChatHubSession> = {};
|
||||
|
||||
if (!workflow) {
|
||||
throw new BadRequestError('Workflow not found');
|
||||
if (updates.agent) {
|
||||
const model = updates.agent.model;
|
||||
|
||||
await this.ensureValidModel(user, model);
|
||||
|
||||
sessionUpdates.agentName = updates.agent.name;
|
||||
sessionUpdates.provider = model.provider;
|
||||
sessionUpdates.model = null;
|
||||
sessionUpdates.credentialId = null;
|
||||
sessionUpdates.agentId = null;
|
||||
sessionUpdates.workflowId = null;
|
||||
|
||||
if (updates.agent.model.provider === 'n8n') {
|
||||
sessionUpdates.workflowId = updates.agent.model.workflowId;
|
||||
} else if (updates.agent.model.provider === 'custom-agent') {
|
||||
sessionUpdates.agentId = updates.agent.model.agentId;
|
||||
} else {
|
||||
sessionUpdates.model = updates.agent.model.model;
|
||||
}
|
||||
|
||||
const chatTriggers = workflow.nodes.filter((node) => node.type === CHAT_TRIGGER_NODE_TYPE);
|
||||
|
||||
if (chatTriggers.length !== 1) {
|
||||
throw new BadRequestError('Workflow must have exactly one chat trigger');
|
||||
}
|
||||
|
||||
const chatTrigger = chatTriggers[0];
|
||||
|
||||
updates.agentName =
|
||||
typeof chatTrigger.parameters.agentName === 'string' &&
|
||||
chatTrigger.parameters.agentName.length > 0
|
||||
? chatTrigger.parameters.agentName
|
||||
: workflow.name;
|
||||
}
|
||||
|
||||
if (updates.agentId) {
|
||||
// Validate the agent exists and is accessible
|
||||
const agent = await this.chatHubAgentService.getAgentById(updates.agentId, user.id);
|
||||
if (updates.title !== undefined) sessionUpdates.title = updates.title;
|
||||
if (updates.credentialId !== undefined) sessionUpdates.credentialId = updates.credentialId;
|
||||
|
||||
if (!agent) {
|
||||
throw new BadRequestError('Agent not found');
|
||||
}
|
||||
|
||||
updates.agentName = agent.name;
|
||||
}
|
||||
|
||||
if (updates.provider === 'n8n') {
|
||||
// n8n provider only stores workflowId
|
||||
updates.model = null;
|
||||
updates.credentialId = null;
|
||||
updates.agentId = null;
|
||||
} else if (updates.provider === 'custom-agent') {
|
||||
// custom-agent provider only stores agentId & Agent name
|
||||
updates.model = null;
|
||||
updates.credentialId = null;
|
||||
updates.workflowId = null;
|
||||
} else if (updates.provider) {
|
||||
updates.workflowId = null;
|
||||
updates.agentId = null;
|
||||
updates.agentName = null;
|
||||
}
|
||||
|
||||
return await this.sessionRepository.updateChatSession(sessionId, updates);
|
||||
return await this.sessionRepository.updateChatSession(sessionId, sessionUpdates);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2271,4 +2211,35 @@ export class ChatHubService {
|
||||
await this.chatHubAttachmentService.deleteAllBySessionId(sessionId);
|
||||
await this.sessionRepository.deleteChatHubSession(sessionId);
|
||||
}
|
||||
|
||||
private async ensureValidModel(user: User, model: ChatHubConversationModel) {
|
||||
if (model.provider === 'custom-agent') {
|
||||
// Find the agent to get its name
|
||||
const agent = await this.chatHubAgentService.getAgentById(model.agentId, user.id);
|
||||
if (!agent) {
|
||||
throw new BadRequestError('Agent not found for chat session initialization');
|
||||
}
|
||||
}
|
||||
|
||||
if (model.provider === 'n8n') {
|
||||
// Find the workflow to get its name
|
||||
const workflow = await this.workflowFinderService.findWorkflowForUser(
|
||||
model.workflowId,
|
||||
user,
|
||||
['workflow:read'],
|
||||
{ includeTags: false, includeParentFolder: false },
|
||||
);
|
||||
|
||||
if (!workflow) {
|
||||
throw new BadRequestError('Workflow not found for chat session initialization');
|
||||
}
|
||||
|
||||
const chatTrigger = workflow.nodes?.find((node) => node.type === CHAT_TRIGGER_NODE_TYPE);
|
||||
if (!chatTrigger) {
|
||||
throw new BadRequestError(
|
||||
'Chat trigger not found in workflow for chat session initialization',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ export interface HumanMessagePayload extends BaseMessagePayload {
|
||||
previousMessageId: ChatMessageId | null;
|
||||
attachments: ChatAttachment[];
|
||||
tools: INode[];
|
||||
agentName: string;
|
||||
}
|
||||
export interface RegenerateMessagePayload extends BaseMessagePayload {
|
||||
retryId: ChatMessageId;
|
||||
|
||||
@@ -22,7 +22,6 @@ import {
|
||||
} from '@/features/ai/chatHub/constants';
|
||||
import { useUsersStore } from '@/features/settings/users/users.store';
|
||||
import {
|
||||
chatHubConversationModelSchema,
|
||||
type ChatHubLLMProvider,
|
||||
PROVIDER_CREDENTIAL_TYPE_MAP,
|
||||
type ChatHubConversationModel,
|
||||
@@ -43,6 +42,10 @@ import { useChatCredentials } from '@/features/ai/chatHub/composables/useChatCre
|
||||
import ChatLayout from '@/features/ai/chatHub/components/ChatLayout.vue';
|
||||
import { INodesSchema, type INode } from 'n8n-workflow';
|
||||
import { useFileDrop } from '@/features/ai/chatHub/composables/useFileDrop';
|
||||
import {
|
||||
type ChatHubConversationModelWithCachedDisplayName,
|
||||
chatHubConversationModelWithCachedDisplayNameSchema,
|
||||
} from '@/features/ai/chatHub/chat.types';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
|
||||
const router = useRouter();
|
||||
@@ -65,12 +68,9 @@ const isNewSession = computed(() => sessionId.value !== route.params.id);
|
||||
const scrollableRef = useTemplateRef('scrollable');
|
||||
const scrollContainerRef = computed(() => scrollableRef.value?.parentElement ?? null);
|
||||
const currentConversation = computed(() =>
|
||||
sessionId.value
|
||||
? chatStore.sessions.find((session) => session.id === sessionId.value)
|
||||
: undefined,
|
||||
sessionId.value ? chatStore.sessions.byId[sessionId.value] : undefined,
|
||||
);
|
||||
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(() => isLlmProvider(selectedModel.value?.model.provider));
|
||||
@@ -80,7 +80,7 @@ const { arrivedState, measure } = useScroll(scrollContainerRef, {
|
||||
offset: { bottom: 100 },
|
||||
});
|
||||
|
||||
const defaultModel = useLocalStorage<ChatHubConversationModel | null>(
|
||||
const defaultModel = useLocalStorage<ChatHubConversationModelWithCachedDisplayName | null>(
|
||||
LOCAL_STORAGE_CHAT_HUB_SELECTED_MODEL(usersStore.currentUserId ?? 'anonymous'),
|
||||
null,
|
||||
{
|
||||
@@ -89,7 +89,7 @@ const defaultModel = useLocalStorage<ChatHubConversationModel | null>(
|
||||
serializer: {
|
||||
read: (value) => {
|
||||
try {
|
||||
return chatHubConversationModelSchema.parse(JSON.parse(value));
|
||||
return chatHubConversationModelWithCachedDisplayNameSchema.parse(JSON.parse(value));
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
@@ -99,6 +99,10 @@ const defaultModel = useLocalStorage<ChatHubConversationModel | null>(
|
||||
},
|
||||
);
|
||||
|
||||
const defaultModelName = computed(() =>
|
||||
defaultModel.value ? chatStore.getAgent(defaultModel.value).name : undefined,
|
||||
);
|
||||
|
||||
const defaultTools = useLocalStorage<INode[] | null>(
|
||||
LOCAL_STORAGE_CHAT_HUB_SELECTED_TOOLS(usersStore.currentUserId ?? 'anonymous'),
|
||||
null,
|
||||
@@ -154,25 +158,32 @@ const modelFromQuery = computed<ChatModelDto | null>(() => {
|
||||
});
|
||||
|
||||
const selectedModel = computed<ChatModelDto | null>(() => {
|
||||
if (!chatStore.agentsReady) {
|
||||
return null;
|
||||
if (!isNewSession.value) {
|
||||
const model = currentConversation.value ? unflattenModel(currentConversation.value) : null;
|
||||
|
||||
if (!model) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return chatStore.getAgent(
|
||||
model,
|
||||
(currentConversation.value?.agentName || currentConversation.value?.model) ?? undefined,
|
||||
);
|
||||
}
|
||||
|
||||
if (modelFromQuery.value) {
|
||||
return modelFromQuery.value;
|
||||
}
|
||||
|
||||
if (currentConversation.value?.provider) {
|
||||
const model = unflattenModel(currentConversation.value);
|
||||
|
||||
return model ? chatStore.getAgent(model) : null;
|
||||
}
|
||||
|
||||
if (chatStore.streaming?.sessionId === sessionId.value) {
|
||||
return chatStore.getAgent(chatStore.streaming.model);
|
||||
return chatStore.getAgent(chatStore.streaming.model, chatStore.streaming.agentName);
|
||||
}
|
||||
|
||||
return defaultModel.value ? chatStore.getAgent(defaultModel.value) : null;
|
||||
if (!defaultModel.value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return chatStore.getAgent(defaultModel.value, defaultModel.value.cachedDisplayName);
|
||||
});
|
||||
|
||||
const { credentialsByProvider, selectCredential } = useChatCredentials(
|
||||
@@ -207,6 +218,21 @@ const credentialsForSelectedProvider = computed<ChatHubSendMessageRequest['crede
|
||||
},
|
||||
);
|
||||
const isMissingSelectedCredential = computed(() => !credentialsForSelectedProvider.value);
|
||||
const issue = computed<null | 'missingCredentials' | 'missingAgent'>(() => {
|
||||
if (!chatStore.agentsReady) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!selectedModel.value) {
|
||||
return 'missingAgent';
|
||||
}
|
||||
|
||||
if (isMissingSelectedCredential.value) {
|
||||
return 'missingCredentials';
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
const editingMessageId = ref<string>();
|
||||
const didSubmitInCurrentSession = ref(false);
|
||||
@@ -235,9 +261,9 @@ function scrollToMessage(messageId: ChatMessageId) {
|
||||
|
||||
// Scroll to the bottom when a new message is added
|
||||
watch(
|
||||
[readyToShowMessages, () => chatMessages.value[chatMessages.value.length - 1]?.id],
|
||||
([ready, lastMessageId]) => {
|
||||
if (!ready || !lastMessageId) {
|
||||
() => chatMessages.value[chatMessages.value.length - 1]?.id,
|
||||
(lastMessageId) => {
|
||||
if (!lastMessageId) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -271,7 +297,7 @@ watch(
|
||||
const model = findOneFromModelsResponse(models) ?? null;
|
||||
|
||||
if (model) {
|
||||
void handleSelectModel(model);
|
||||
void handleSelectAgent(model);
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
@@ -322,6 +348,17 @@ watch(
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
// Keep cached display name up-to-date
|
||||
watch(
|
||||
defaultModelName,
|
||||
(name) => {
|
||||
if (defaultModel.value && name) {
|
||||
defaultModel.value = { ...defaultModel.value, cachedDisplayName: name };
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
function onSubmit(message: string, attachments: File[]) {
|
||||
if (
|
||||
!message.trim() ||
|
||||
@@ -342,6 +379,7 @@ function onSubmit(message: string, attachments: File[]) {
|
||||
credentialsForSelectedProvider.value,
|
||||
canSelectTools.value ? selectedTools.value : [],
|
||||
attachments,
|
||||
selectedModel.value.name,
|
||||
);
|
||||
|
||||
inputRef.value?.setText('');
|
||||
@@ -406,18 +444,24 @@ function handleRegenerateMessage(message: ChatHubMessageDto) {
|
||||
);
|
||||
}
|
||||
|
||||
async function handleSelectModel(selection: ChatModelDto) {
|
||||
async function handleSelectModel(selection: ChatHubConversationModel, displayName?: string) {
|
||||
const agentName = displayName ?? chatStore.getAgent(selection)?.name ?? '';
|
||||
|
||||
if (currentConversation.value) {
|
||||
try {
|
||||
await chatStore.updateSessionModel(sessionId.value, selection.model);
|
||||
await chatStore.updateSessionModel(sessionId.value, selection, agentName);
|
||||
} catch (error) {
|
||||
toast.showError(error, 'Could not update selected model');
|
||||
}
|
||||
} else {
|
||||
defaultModel.value = selection.model;
|
||||
defaultModel.value = { ...selection, cachedDisplayName: agentName };
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSelectAgent(selection: ChatModelDto) {
|
||||
await handleSelectModel(selection.model, selection.name);
|
||||
}
|
||||
|
||||
function handleSwitchAlternative(messageId: string) {
|
||||
shouldSkipNextScrollTrigger.value = true;
|
||||
chatStore.switchAlternative(sessionId.value, messageId);
|
||||
@@ -501,8 +545,8 @@ function onFilesDropped(files: File[]) {
|
||||
ref="headerRef"
|
||||
:selected-model="selectedModel"
|
||||
:credentials="credentialsByProvider"
|
||||
:ready-to-show-model-selector="chatStore.agentsReady"
|
||||
@select-model="handleSelectModel"
|
||||
:ready-to-show-model-selector="isNewSession || !!currentConversation"
|
||||
@select-model="handleSelectAgent"
|
||||
@edit-custom-agent="handleEditAgent"
|
||||
@create-custom-agent="openNewAgentCreator"
|
||||
@select-credential="selectCredential"
|
||||
@@ -510,7 +554,6 @@ function onFilesDropped(files: File[]) {
|
||||
/>
|
||||
|
||||
<N8nScrollArea
|
||||
v-if="readyToShowMessages"
|
||||
type="scroll"
|
||||
:enable-vertical-scroll="true"
|
||||
:enable-horizontal-scroll="false"
|
||||
@@ -532,6 +575,7 @@ function onFilesDropped(files: File[]) {
|
||||
:compact="isMobileDevice"
|
||||
:is-editing="editingMessageId === message.id"
|
||||
:is-streaming="message.status === 'running'"
|
||||
:cached-agent-display-name="selectedModel?.name ?? null"
|
||||
:min-height="
|
||||
didSubmitInCurrentSession &&
|
||||
message.type === 'ai' &&
|
||||
@@ -567,6 +611,7 @@ function onFilesDropped(files: File[]) {
|
||||
:is-tools-selectable="canSelectTools"
|
||||
:is-missing-credentials="isMissingSelectedCredential"
|
||||
:is-new-session="isNewSession"
|
||||
:issue="issue"
|
||||
@submit="onSubmit"
|
||||
@stop="onStop"
|
||||
@select-model="handleConfigureModel"
|
||||
|
||||
@@ -38,7 +38,6 @@ import {
|
||||
type EnrichedStructuredChunk,
|
||||
type ChatHubMessageStatus,
|
||||
type ChatModelDto,
|
||||
type ChatHubConversationsResponse,
|
||||
} from '@n8n/api-types';
|
||||
import type {
|
||||
CredentialsMap,
|
||||
@@ -66,7 +65,12 @@ export const useChatStore = defineStore(CHAT_STORE, () => {
|
||||
const telemetry = useTelemetry();
|
||||
|
||||
const agents = ref<ChatModelsResponse>();
|
||||
const sessions = ref<ChatHubConversationsResponse>();
|
||||
const sessions = ref<{
|
||||
byId: Partial<Record<string, ChatHubSessionDto>>;
|
||||
ids: string[] | null;
|
||||
hasMore: boolean;
|
||||
nextCursor: string | null;
|
||||
}>({ byId: {}, ids: null, hasMore: false, nextCursor: null });
|
||||
const sessionsLoadingMore = ref(false);
|
||||
|
||||
const streaming = ref<ChatStreamingState>();
|
||||
@@ -291,7 +295,12 @@ export const useChatStore = defineStore(CHAT_STORE, () => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!reset && sessions.value && !sessions.value.hasMore && sessions.value.data.length > 0) {
|
||||
if (
|
||||
!reset &&
|
||||
sessions.value &&
|
||||
!sessions.value.hasMore &&
|
||||
(sessions.value.ids ?? []).length > 0
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -306,10 +315,17 @@ export const useChatStore = defineStore(CHAT_STORE, () => {
|
||||
new Promise((resolve) => setTimeout(resolve, 500)),
|
||||
]);
|
||||
|
||||
sessions.value = {
|
||||
...response,
|
||||
data: [...(reset ? [] : (sessions.value?.data ?? [])), ...response.data],
|
||||
};
|
||||
if (reset || sessions.value.ids === null) {
|
||||
sessions.value.ids = [];
|
||||
}
|
||||
|
||||
sessions.value.hasMore = response.hasMore;
|
||||
sessions.value.nextCursor = response.nextCursor;
|
||||
|
||||
for (const session of response.data) {
|
||||
sessions.value.ids.push(session.id);
|
||||
sessions.value.byId[session.id] = session;
|
||||
}
|
||||
} finally {
|
||||
sessionsLoadingMore.value = false;
|
||||
}
|
||||
@@ -322,7 +338,7 @@ export const useChatStore = defineStore(CHAT_STORE, () => {
|
||||
}
|
||||
|
||||
async function fetchMessages(sessionId: string) {
|
||||
const { conversation } = await fetchMessagesApi(rootStore.restApiContext, sessionId);
|
||||
const { conversation, session } = await fetchMessagesApi(rootStore.restApiContext, sessionId);
|
||||
|
||||
const messages = linkMessages(Object.values(conversation.messages));
|
||||
|
||||
@@ -334,6 +350,7 @@ export const useChatStore = defineStore(CHAT_STORE, () => {
|
||||
messages,
|
||||
activeMessageChain: computeActiveChain(messages, latestMessage?.id ?? null),
|
||||
});
|
||||
sessions.value.byId[sessionId] = session;
|
||||
}
|
||||
|
||||
function onBeginMessage() {
|
||||
@@ -348,17 +365,6 @@ export const useChatStore = defineStore(CHAT_STORE, () => {
|
||||
);
|
||||
|
||||
addMessage(streaming.value.sessionId, message);
|
||||
|
||||
if (sessions.value?.data.some((session) => session.id === streaming.value?.sessionId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
sessions.value = {
|
||||
hasMore: false,
|
||||
nextCursor: null,
|
||||
...sessions.value,
|
||||
data: [...(sessions.value?.data ?? []), createSessionFromStreamingState(streaming.value)],
|
||||
};
|
||||
}
|
||||
|
||||
function ensureMessage(sessionId: ChatSessionId, messageId: ChatMessageId): ChatMessage {
|
||||
@@ -475,6 +481,7 @@ export const useChatStore = defineStore(CHAT_STORE, () => {
|
||||
credentials: ChatHubSendMessageRequest['credentials'],
|
||||
tools: INode[],
|
||||
files: File[] = [],
|
||||
agentName: string,
|
||||
) {
|
||||
const messageId = uuidv4();
|
||||
const conversation = ensureConversation(sessionId);
|
||||
@@ -517,8 +524,17 @@ export const useChatStore = defineStore(CHAT_STORE, () => {
|
||||
model,
|
||||
retryOfMessageId: null,
|
||||
tools,
|
||||
agentName,
|
||||
};
|
||||
|
||||
if (!sessions.value.byId[sessionId]) {
|
||||
sessions.value.byId[sessionId] = createSessionFromStreamingState(streaming.value);
|
||||
if (!sessions.value.ids) {
|
||||
sessions.value.ids = [];
|
||||
}
|
||||
sessions.value.ids.unshift(sessionId);
|
||||
}
|
||||
|
||||
sendMessageApi(
|
||||
rootStore.restApiContext,
|
||||
{
|
||||
@@ -530,6 +546,7 @@ export const useChatStore = defineStore(CHAT_STORE, () => {
|
||||
previousMessageId,
|
||||
tools,
|
||||
attachments,
|
||||
agentName,
|
||||
},
|
||||
onStreamMessage,
|
||||
onStreamDone,
|
||||
@@ -588,6 +605,7 @@ export const useChatStore = defineStore(CHAT_STORE, () => {
|
||||
model,
|
||||
retryOfMessageId: null,
|
||||
tools: [],
|
||||
agentName: sessions.value.byId[sessionId]?.agentName ?? '',
|
||||
};
|
||||
|
||||
editMessageApi(
|
||||
@@ -625,6 +643,7 @@ export const useChatStore = defineStore(CHAT_STORE, () => {
|
||||
model,
|
||||
retryOfMessageId: retryId,
|
||||
tools: [],
|
||||
agentName: sessions.value.byId[sessionId]?.agentName ?? '',
|
||||
};
|
||||
|
||||
regenerateMessageApi(
|
||||
@@ -652,22 +671,18 @@ export const useChatStore = defineStore(CHAT_STORE, () => {
|
||||
}
|
||||
|
||||
function updateSession(sessionId: ChatSessionId, toUpdate: Partial<ChatHubSessionDto>) {
|
||||
if (!sessions.value) {
|
||||
return;
|
||||
}
|
||||
const session = sessions.value.byId[sessionId];
|
||||
|
||||
sessions.value.data = sessions.value.data?.map((session) =>
|
||||
session.id === sessionId
|
||||
? {
|
||||
...session,
|
||||
...toUpdate,
|
||||
}
|
||||
: session,
|
||||
);
|
||||
if (session) {
|
||||
sessions.value.byId[sessionId] = {
|
||||
...session,
|
||||
...toUpdate,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function updateToolsInSession(sessionId: ChatSessionId, tools: INode[]) {
|
||||
const session = sessions.value?.data?.find((s) => s.id === sessionId);
|
||||
const session = sessions.value?.byId[sessionId];
|
||||
if (!session) {
|
||||
throw new Error(`Session with ID ${sessionId} not found`);
|
||||
}
|
||||
@@ -685,20 +700,22 @@ export const useChatStore = defineStore(CHAT_STORE, () => {
|
||||
updateSession(sessionId, updated.session);
|
||||
}
|
||||
|
||||
async function updateSessionModel(sessionId: ChatSessionId, model: ChatHubConversationModel) {
|
||||
await updateConversationApi(rootStore.restApiContext, sessionId, model);
|
||||
updateSession(sessionId, model);
|
||||
async function updateSessionModel(
|
||||
sessionId: ChatSessionId,
|
||||
model: ChatHubConversationModel,
|
||||
agentName: string,
|
||||
) {
|
||||
await updateConversationApi(rootStore.restApiContext, sessionId, {
|
||||
agent: { model, name: agentName },
|
||||
});
|
||||
updateSession(sessionId, { ...model, agentName });
|
||||
}
|
||||
|
||||
async function deleteSession(sessionId: ChatSessionId) {
|
||||
await deleteConversationApi(rootStore.restApiContext, sessionId);
|
||||
|
||||
if (sessions.value) {
|
||||
sessions.value = {
|
||||
...sessions.value,
|
||||
data: sessions.value.data?.filter((session) => session.id !== sessionId),
|
||||
};
|
||||
}
|
||||
delete sessions.value.byId[sessionId];
|
||||
sessions.value.ids = sessions.value.ids?.filter((id) => id !== sessionId) ?? null;
|
||||
}
|
||||
|
||||
function switchAlternative(sessionId: ChatSessionId, messageId: ChatMessageId) {
|
||||
@@ -781,33 +798,23 @@ export const useChatStore = defineStore(CHAT_STORE, () => {
|
||||
await fetchAgents(credentials);
|
||||
}
|
||||
|
||||
function getAgent(model: ChatHubConversationModel) {
|
||||
if (!agents.value) return null;
|
||||
|
||||
const agent = agents.value[model.provider]?.models.find((agent) =>
|
||||
function getAgent(model: ChatHubConversationModel, fallbackName: string = ''): ChatModelDto {
|
||||
const agent = agents.value?.[model.provider]?.models.find((agent) =>
|
||||
isMatchedAgent(agent, model),
|
||||
);
|
||||
|
||||
if (!agent) {
|
||||
if (!isLlmProviderModel(model)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Allow custom models chosen by ID even if they are not in the fetched list
|
||||
return {
|
||||
model: {
|
||||
provider: model.provider,
|
||||
model: model.model,
|
||||
},
|
||||
name: model.model,
|
||||
description: null,
|
||||
createdAt: null,
|
||||
updatedAt: null,
|
||||
allowFileUploads: true,
|
||||
};
|
||||
if (agent) {
|
||||
return agent;
|
||||
}
|
||||
|
||||
return agent;
|
||||
return {
|
||||
model,
|
||||
name: fallbackName,
|
||||
description: null,
|
||||
createdAt: null,
|
||||
updatedAt: null,
|
||||
allowFileUploads: true,
|
||||
};
|
||||
}
|
||||
|
||||
async function fetchAllChatSettings() {
|
||||
@@ -862,8 +869,8 @@ export const useChatStore = defineStore(CHAT_STORE, () => {
|
||||
/**
|
||||
* conversations
|
||||
*/
|
||||
sessions: computed(() => sessions.value?.data ?? []),
|
||||
sessionsReady: computed(() => sessions.value !== undefined),
|
||||
sessions,
|
||||
sessionsReady: computed(() => sessions.value.ids !== null),
|
||||
sessionsLoading: computed(() => sessionsLoadingMore.value),
|
||||
fetchSessions,
|
||||
fetchMoreSessions,
|
||||
|
||||
@@ -8,9 +8,11 @@ import {
|
||||
type ChatHubConversationModel,
|
||||
type EnrichedStructuredChunk,
|
||||
type ChatHubProvider,
|
||||
chatHubConversationModelSchema,
|
||||
} from '@n8n/api-types';
|
||||
import type { INode } from 'n8n-workflow';
|
||||
import { z } from 'zod';
|
||||
import { isLlmProviderModel } from './chat.utils';
|
||||
|
||||
export interface UserMessage {
|
||||
id: string;
|
||||
@@ -80,6 +82,7 @@ export interface ChatStreamingState extends Partial<EnrichedStructuredChunk['met
|
||||
model: ChatHubConversationModel;
|
||||
retryOfMessageId: ChatMessageId | null;
|
||||
tools: INode[];
|
||||
agentName: string;
|
||||
}
|
||||
|
||||
export interface FlattenedModel {
|
||||
@@ -88,3 +91,14 @@ export interface FlattenedModel {
|
||||
workflowId: string | null;
|
||||
agentId: string | null;
|
||||
}
|
||||
|
||||
export const chatHubConversationModelWithCachedDisplayNameSchema = chatHubConversationModelSchema
|
||||
.and(z.object({ cachedDisplayName: z.string().optional() }))
|
||||
.transform((value) => ({
|
||||
...value,
|
||||
cachedDisplayName: value.cachedDisplayName || (isLlmProviderModel(value) ? value.model : ''),
|
||||
}));
|
||||
|
||||
export type ChatHubConversationModelWithCachedDisplayName = z.infer<
|
||||
typeof chatHubConversationModelWithCachedDisplayNameSchema
|
||||
>;
|
||||
|
||||
@@ -340,7 +340,7 @@ export function createSessionFromStreamingState(streaming: ChatStreamingState):
|
||||
ownerId: '',
|
||||
lastMessageAt: new Date().toISOString(),
|
||||
credentialId: null,
|
||||
agentName: null,
|
||||
agentName: streaming.agentName,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
tools: streaming.tools,
|
||||
|
||||
@@ -46,7 +46,7 @@ const credentialIdForSelectedModelProvider = computed(
|
||||
() => selectedModel.value && agentMergedCredentials.value[selectedModel.value.provider],
|
||||
);
|
||||
const selectedAgent = computed(
|
||||
() => selectedModel.value && chatStore.getAgent(selectedModel.value),
|
||||
() => selectedModel.value && chatStore.getAgent(selectedModel.value, selectedModel.value.model),
|
||||
);
|
||||
|
||||
const isEditMode = computed(() => !!props.data.agentId);
|
||||
@@ -270,6 +270,7 @@ function onSelectTools(newTools: INode[]) {
|
||||
:include-custom-agents="false"
|
||||
:credentials="agentMergedCredentials"
|
||||
:disabled="isLoadingAgent"
|
||||
warn-missing-credentials
|
||||
@change="onModelChange"
|
||||
@select-credential="onCredentialSelected"
|
||||
/>
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import CredentialIcon from '@/features/credentials/components/CredentialIcon.vue';
|
||||
import { useCredentialsStore } from '@/features/credentials/credentials.store';
|
||||
import { type ChatModelDto, PROVIDER_CREDENTIAL_TYPE_MAP } from '@n8n/api-types';
|
||||
import { N8nAvatar, N8nIcon, N8nTooltip } from '@n8n/design-system';
|
||||
import { computed } from 'vue';
|
||||
|
||||
defineProps<{
|
||||
agent: ChatModelDto | null;
|
||||
size: 'sm' | 'md' | 'lg';
|
||||
tooltip?: boolean;
|
||||
}>();
|
||||
|
||||
const credentialsStore = useCredentialsStore();
|
||||
const isCredentialsIconReady = computed(() => credentialsStore.allCredentialTypes.length > 0);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -26,6 +31,7 @@ defineProps<{
|
||||
/>
|
||||
<CredentialIcon
|
||||
v-else
|
||||
:class="[$style.credentialsIcon, { [$style.isReady]: isCredentialsIconReady }]"
|
||||
:credential-type-name="PROVIDER_CREDENTIAL_TYPE_MAP[agent.model.provider]"
|
||||
:size="size === 'sm' ? 16 : size === 'lg' ? 40 : 20"
|
||||
/>
|
||||
@@ -36,4 +42,12 @@ defineProps<{
|
||||
.avatar.md {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
.credentialsIcon {
|
||||
visibility: hidden;
|
||||
|
||||
&.isReady {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -20,16 +20,18 @@ import { useRootStore } from '@n8n/stores/useRootStore';
|
||||
import { useDeviceSupport } from '@n8n/composables/useDeviceSupport';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
|
||||
const { message, compact, isEditing, isStreaming, minHeight } = defineProps<{
|
||||
message: ChatMessage;
|
||||
compact: boolean;
|
||||
isEditing: boolean;
|
||||
isStreaming: boolean;
|
||||
/**
|
||||
* minHeight allows scrolling agent's response to the top while it is being generated
|
||||
*/
|
||||
minHeight?: number;
|
||||
}>();
|
||||
const { message, compact, isEditing, isStreaming, minHeight, cachedAgentDisplayName } =
|
||||
defineProps<{
|
||||
message: ChatMessage;
|
||||
compact: boolean;
|
||||
isEditing: boolean;
|
||||
isStreaming: boolean;
|
||||
cachedAgentDisplayName: string | null;
|
||||
/**
|
||||
* minHeight allows scrolling agent's response to the top while it is being generated
|
||||
*/
|
||||
minHeight?: number;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
startEdit: [];
|
||||
@@ -60,7 +62,11 @@ const speech = useSpeechSynthesis(messageContent, {
|
||||
const agent = computed<ChatModelDto | null>(() => {
|
||||
const model = unflattenModel(message);
|
||||
|
||||
return model ? chatStore.getAgent(model) : null;
|
||||
if (!model) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return chatStore.getAgent(model, cachedAgentDisplayName ?? undefined);
|
||||
});
|
||||
|
||||
const attachments = computed(() =>
|
||||
|
||||
@@ -12,11 +12,11 @@ import { isLlmProviderModel } from '@/features/ai/chatHub/chat.utils';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import { I18nT } from 'vue-i18n';
|
||||
|
||||
const { selectedModel, selectedTools, isMissingCredentials } = defineProps<{
|
||||
const { selectedModel, selectedTools, issue } = defineProps<{
|
||||
isResponding: boolean;
|
||||
isNewSession: boolean;
|
||||
isToolsSelectable: boolean;
|
||||
isMissingCredentials: boolean;
|
||||
issue: 'missingCredentials' | 'missingAgent' | null;
|
||||
selectedModel: ChatModelDto | null;
|
||||
selectedTools: INode[] | null;
|
||||
}>();
|
||||
@@ -179,7 +179,7 @@ defineExpose({
|
||||
<template>
|
||||
<form :class="$style.prompt" @submit.prevent="handleSubmitForm">
|
||||
<div :class="$style.inputWrap">
|
||||
<N8nText v-if="!selectedModel" :class="$style.callout">
|
||||
<N8nText v-if="issue === 'missingAgent'" :class="$style.callout">
|
||||
<I18nT
|
||||
:keypath="
|
||||
isNewSession
|
||||
@@ -200,7 +200,7 @@ defineExpose({
|
||||
</template>
|
||||
</I18nT>
|
||||
</N8nText>
|
||||
<N8nText v-else-if="isMissingCredentials && llmProvider" :class="$style.callout">
|
||||
<N8nText v-else-if="issue === 'missingCredentials' && llmProvider" :class="$style.callout">
|
||||
<I18nT
|
||||
:keypath="
|
||||
isNewSession
|
||||
@@ -252,7 +252,7 @@ defineExpose({
|
||||
autocomplete="off"
|
||||
:autosize="{ minRows: 1, maxRows: 6 }"
|
||||
autofocus
|
||||
:disabled="isMissingCredentials || !selectedModel"
|
||||
:disabled="!!issue"
|
||||
@keydown="handleKeydownTextarea"
|
||||
/>
|
||||
|
||||
@@ -261,7 +261,7 @@ defineExpose({
|
||||
<ToolsSelector
|
||||
:class="$style.toolsButton"
|
||||
:selected="selectedTools ?? []"
|
||||
:disabled="isMissingCredentials || !selectedModel || isResponding"
|
||||
:disabled="!!issue || isResponding"
|
||||
transparent-bg
|
||||
@select="onSelectTools"
|
||||
/>
|
||||
@@ -272,7 +272,7 @@ defineExpose({
|
||||
native-type="button"
|
||||
type="secondary"
|
||||
:title="i18n.baseText('chatHub.chat.prompt.button.attach')"
|
||||
:disabled="isMissingCredentials || isResponding"
|
||||
:disabled="!!issue || isResponding"
|
||||
icon="paperclip"
|
||||
icon-size="large"
|
||||
text
|
||||
@@ -287,7 +287,7 @@ defineExpose({
|
||||
: i18n.baseText('chatHub.chat.prompt.button.voiceInput')
|
||||
"
|
||||
type="secondary"
|
||||
:disabled="isMissingCredentials || !selectedModel || isResponding"
|
||||
:disabled="!!issue || isResponding"
|
||||
:icon="speechInput.isListening.value ? 'square' : 'mic'"
|
||||
:class="{ [$style.recording]: speechInput.isListening.value }"
|
||||
icon-size="large"
|
||||
@@ -296,7 +296,7 @@ defineExpose({
|
||||
<N8nIconButton
|
||||
v-if="!isResponding"
|
||||
native-type="submit"
|
||||
:disabled="isMissingCredentials || !selectedModel || !message.trim()"
|
||||
:disabled="!!issue || !message.trim()"
|
||||
:title="i18n.baseText('chatHub.chat.prompt.button.send')"
|
||||
icon="arrow-up"
|
||||
icon-size="large"
|
||||
|
||||
+1
-13
@@ -37,19 +37,7 @@ const agent = computed<ChatModelDto | null>(() => {
|
||||
return null;
|
||||
}
|
||||
|
||||
const agent = chatStore.getAgent(model);
|
||||
|
||||
if (agent) {
|
||||
return agent;
|
||||
}
|
||||
|
||||
return {
|
||||
model,
|
||||
name: session.agentName || '',
|
||||
description: null,
|
||||
createdAt: null,
|
||||
updatedAt: null,
|
||||
};
|
||||
return chatStore.getAgent(model, session.agentName);
|
||||
});
|
||||
|
||||
const dropdownItems = computed<Array<ActionDropdownItem<SessionAction>>>(() => [
|
||||
|
||||
+14
-1
@@ -17,6 +17,7 @@ import { useIntersectionObserver } from '@vueuse/core';
|
||||
import ChatSessionMenuItem from './ChatSessionMenuItem.vue';
|
||||
import SkeletonMenuItem from './SkeletonMenuItem.vue';
|
||||
import { useTelemetry } from '@/app/composables/useTelemetry';
|
||||
import { type ChatHubSessionDto } from '@n8n/api-types';
|
||||
import { useCredentialsStore } from '@/features/credentials/credentials.store';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
|
||||
@@ -43,7 +44,19 @@ const currentSessionId = computed(() =>
|
||||
typeof route.params.id === 'string' ? route.params.id : undefined,
|
||||
);
|
||||
|
||||
const groupedConversations = computed(() => groupConversationsByDate(chatStore.sessions));
|
||||
const groupedConversations = computed(() =>
|
||||
groupConversationsByDate(
|
||||
(chatStore.sessions.ids ?? []).reduce<ChatHubSessionDto[]>((acc, id) => {
|
||||
const session = chatStore.sessions.byId[id];
|
||||
|
||||
if (session) {
|
||||
acc.push(session);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []),
|
||||
),
|
||||
);
|
||||
|
||||
function handleStartRename(sessionId: string) {
|
||||
renamingSessionId.value = sessionId;
|
||||
|
||||
@@ -37,6 +37,7 @@ import { fetchChatModelsApi } from '@/features/ai/chatHub/chat.api';
|
||||
import { useRootStore } from '@n8n/stores/useRootStore';
|
||||
import { useTelemetry } from '@/app/composables/useTelemetry';
|
||||
import { useSettingsStore } from '@/app/stores/settings.store';
|
||||
import { truncateBeforeLast } from '@n8n/utils';
|
||||
|
||||
const NEW_AGENT_MENU_ID = 'agent::new';
|
||||
|
||||
@@ -45,11 +46,13 @@ const {
|
||||
includeCustomAgents = true,
|
||||
credentials,
|
||||
text,
|
||||
warnMissingCredentials = false,
|
||||
} = defineProps<{
|
||||
selectedAgent: ChatModelDto | null;
|
||||
includeCustomAgents?: boolean;
|
||||
credentials: CredentialsMap | null;
|
||||
text?: boolean;
|
||||
warnMissingCredentials?: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -92,6 +95,7 @@ const credentialsName = computed(() =>
|
||||
const isCredentialsRequired = computed(() => isLlmProviderModel(selectedAgent?.model));
|
||||
const isCredentialsMissing = computed(
|
||||
() =>
|
||||
warnMissingCredentials &&
|
||||
isCredentialsRequired.value &&
|
||||
selectedAgent?.model.provider &&
|
||||
!credentials?.[selectedAgent?.model.provider],
|
||||
@@ -343,10 +347,10 @@ defineExpose({
|
||||
/>
|
||||
<div :class="$style.selected">
|
||||
<div>
|
||||
{{ selectedLabel }}
|
||||
{{ truncateBeforeLast(selectedLabel, 30) }}
|
||||
</div>
|
||||
<N8nText v-if="credentialsName" size="xsmall" color="text-light">
|
||||
{{ credentialsName }}
|
||||
{{ truncateBeforeLast(credentialsName, 30) }}
|
||||
</N8nText>
|
||||
<N8nText v-else-if="isCredentialsMissing" size="xsmall" color="danger">
|
||||
<N8nIcon
|
||||
@@ -382,13 +386,6 @@ defineExpose({
|
||||
flex-direction: column;
|
||||
align-items: start;
|
||||
gap: var(--spacing--4xs);
|
||||
max-width: 200px;
|
||||
|
||||
& > div {
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
|
||||
Reference in New Issue
Block a user