mirror of
https://github.com/n8n-io/n8n.git
synced 2026-05-30 16:26:59 +02:00
feat(core): Add new Chat hub providers (no-changelog) (#21946)
This commit is contained in:
parent
4ac0d11486
commit
7cd871b234
|
|
@ -11,7 +11,13 @@ import { Z } from 'zod-class';
|
|||
/**
|
||||
* Supported AI model providers
|
||||
*/
|
||||
export const chatHubLLMProviderSchema = z.enum(['openai', 'anthropic', 'google']);
|
||||
export const chatHubLLMProviderSchema = z.enum([
|
||||
'openai',
|
||||
'anthropic',
|
||||
'google',
|
||||
'azureOpenAi',
|
||||
'ollama',
|
||||
]);
|
||||
export type ChatHubLLMProvider = z.infer<typeof chatHubLLMProviderSchema>;
|
||||
|
||||
export const chatHubProviderSchema = z.enum([
|
||||
|
|
@ -32,6 +38,8 @@ export const PROVIDER_CREDENTIAL_TYPE_MAP: Record<
|
|||
openai: 'openAiApi',
|
||||
anthropic: 'anthropicApi',
|
||||
google: 'googlePalmApi',
|
||||
ollama: 'ollamaApi',
|
||||
azureOpenAi: 'azureOpenAiApi',
|
||||
};
|
||||
|
||||
export type ChatHubAgentTool = typeof JINA_AI_TOOL_NODE_TYPE | typeof SEAR_XNG_TOOL_NODE_TYPE;
|
||||
|
|
@ -54,6 +62,16 @@ const googleModelSchema = z.object({
|
|||
model: z.string(),
|
||||
});
|
||||
|
||||
const azureOpenAIModelSchema = z.object({
|
||||
provider: z.literal('azureOpenAi'),
|
||||
model: z.string(),
|
||||
});
|
||||
|
||||
const ollamaModelSchema = z.object({
|
||||
provider: z.literal('ollama'),
|
||||
model: z.string(),
|
||||
});
|
||||
|
||||
const n8nModelSchema = z.object({
|
||||
provider: z.literal('n8n'),
|
||||
workflowId: z.string(),
|
||||
|
|
@ -68,6 +86,8 @@ export const chatHubConversationModelSchema = z.discriminatedUnion('provider', [
|
|||
openAIModelSchema,
|
||||
anthropicModelSchema,
|
||||
googleModelSchema,
|
||||
azureOpenAIModelSchema,
|
||||
ollamaModelSchema,
|
||||
n8nModelSchema,
|
||||
chatAgentSchema,
|
||||
]);
|
||||
|
|
@ -75,7 +95,14 @@ export const chatHubConversationModelSchema = z.discriminatedUnion('provider', [
|
|||
export type ChatHubOpenAIModel = z.infer<typeof openAIModelSchema>;
|
||||
export type ChatHubAnthropicModel = z.infer<typeof anthropicModelSchema>;
|
||||
export type ChatHubGoogleModel = z.infer<typeof googleModelSchema>;
|
||||
export type ChatHubBaseLLMModel = ChatHubOpenAIModel | ChatHubAnthropicModel | ChatHubGoogleModel;
|
||||
export type ChatHubAzureOpenAIModel = z.infer<typeof azureOpenAIModelSchema>;
|
||||
export type ChatHubOllamaModel = z.infer<typeof ollamaModelSchema>;
|
||||
export type ChatHubBaseLLMModel =
|
||||
| ChatHubOpenAIModel
|
||||
| ChatHubAnthropicModel
|
||||
| ChatHubGoogleModel
|
||||
| ChatHubAzureOpenAIModel
|
||||
| ChatHubOllamaModel;
|
||||
|
||||
export type ChatHubN8nModel = z.infer<typeof n8nModelSchema>;
|
||||
export type ChatHubCustomAgentModel = z.infer<typeof chatAgentSchema>;
|
||||
|
|
@ -114,6 +141,8 @@ export const emptyChatModelsResponse: ChatModelsResponse = {
|
|||
openai: { models: [] },
|
||||
anthropic: { models: [] },
|
||||
google: { models: [] },
|
||||
azureOpenAi: { models: [] },
|
||||
ollama: { models: [] },
|
||||
n8n: { models: [] },
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
'custom-agent': { models: [] },
|
||||
|
|
|
|||
|
|
@ -448,6 +448,25 @@ export class ChatHubWorkflowService {
|
|||
options: {},
|
||||
},
|
||||
};
|
||||
case 'azureOpenAi':
|
||||
return {
|
||||
...common,
|
||||
parameters: {
|
||||
model: { __rl: true, mode: 'id', value: model },
|
||||
options: {},
|
||||
},
|
||||
};
|
||||
case 'ollama': {
|
||||
return {
|
||||
...common,
|
||||
parameters: {
|
||||
model: { __rl: true, mode: 'id', value: model },
|
||||
options: {},
|
||||
},
|
||||
};
|
||||
}
|
||||
default:
|
||||
throw new OperationalError('Unsupported model provider');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,14 @@ export const PROVIDER_NODE_TYPE_MAP: Record<ChatHubLLMProvider, INodeTypeNameVer
|
|||
name: '@n8n/n8n-nodes-langchain.lmChatGoogleGemini',
|
||||
version: 1.2,
|
||||
},
|
||||
ollama: {
|
||||
name: '@n8n/n8n-nodes-langchain.lmOllama',
|
||||
version: 1,
|
||||
},
|
||||
azureOpenAi: {
|
||||
name: '@n8n/n8n-nodes-langchain.lmChatAzureOpenAi',
|
||||
version: 1,
|
||||
},
|
||||
};
|
||||
|
||||
export const NODE_NAMES = {
|
||||
|
|
|
|||
|
|
@ -155,6 +155,10 @@ export class ChatHubService {
|
|||
return await this.fetchAnthropicModels(credentials, additionalData);
|
||||
case 'google':
|
||||
return await this.fetchGoogleModels(credentials, additionalData);
|
||||
case 'ollama':
|
||||
return await this.fetchOllamaModels(credentials, additionalData);
|
||||
case 'azureOpenAi':
|
||||
return await this.fetchAzureOpenAiModels(credentials, additionalData);
|
||||
case 'n8n':
|
||||
return await this.fetchAgentWorkflowsAsModels(user);
|
||||
case 'custom-agent':
|
||||
|
|
@ -281,6 +285,76 @@ export class ChatHubService {
|
|||
};
|
||||
}
|
||||
|
||||
private async fetchOllamaModels(
|
||||
credentials: INodeCredentials,
|
||||
additionalData: IWorkflowExecuteAdditionalData,
|
||||
): Promise<ChatModelsResponse['ollama']> {
|
||||
const results = await this.nodeParametersService.getOptionsViaLoadOptions(
|
||||
{
|
||||
// From Ollama Model node
|
||||
// https://github.com/n8n-io/n8n/blob/master/packages/%40n8n/nodes-langchain/nodes/llms/LMOllama/description.ts#L24
|
||||
routing: {
|
||||
request: {
|
||||
method: 'GET',
|
||||
url: '/api/tags',
|
||||
},
|
||||
output: {
|
||||
postReceive: [
|
||||
{
|
||||
type: 'rootProperty',
|
||||
properties: {
|
||||
property: 'models',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'setKeyValue',
|
||||
properties: {
|
||||
name: '={{$responseItem.name}}',
|
||||
value: '={{$responseItem.name}}',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'sort',
|
||||
properties: {
|
||||
key: 'name',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
additionalData,
|
||||
PROVIDER_NODE_TYPE_MAP.ollama,
|
||||
{},
|
||||
credentials,
|
||||
);
|
||||
|
||||
return {
|
||||
models: results.map((result) => ({
|
||||
name: String(result.value),
|
||||
description: result.description ?? null,
|
||||
model: {
|
||||
provider: 'ollama',
|
||||
model: String(result.value),
|
||||
},
|
||||
createdAt: null,
|
||||
updatedAt: null,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
private async fetchAzureOpenAiModels(
|
||||
_credentials: INodeCredentials,
|
||||
_additionalData: IWorkflowExecuteAdditionalData,
|
||||
): Promise<ChatModelsResponse['azureOpenAi']> {
|
||||
// Azure doesn't appear to offer a way to list available models via API.
|
||||
// If we add support for this in the future on the Azure OpenAI node we should copy that
|
||||
// implementation here too.
|
||||
return {
|
||||
models: [],
|
||||
};
|
||||
}
|
||||
|
||||
private async fetchAgentWorkflowsAsModels(user: User): Promise<ChatModelsResponse['n8n']> {
|
||||
const nodeTypes = [CHAT_TRIGGER_NODE_TYPE];
|
||||
const workflows = await this.workflowService.getWorkflowsWithNodesIncluded(
|
||||
|
|
|
|||
|
|
@ -137,6 +137,8 @@ export const maxContextWindowTokens: Record<ChatHubLLMProvider, Record<string, n
|
|||
'models/imagen-4.0-ultra-generate-preview-06-06': 480,
|
||||
'models/learnlm-2.0-flash-experimental': 0,
|
||||
},
|
||||
azureOpenAi: {},
|
||||
ollama: {},
|
||||
};
|
||||
|
||||
export const getMaxContextWindowTokens = (
|
||||
|
|
|
|||
|
|
@ -299,6 +299,8 @@
|
|||
"chat.window.session.resetSession": "Reset chat session",
|
||||
"chatHub.agent.customAgents": "Custom Agents",
|
||||
"chatHub.agent.newAgent": "New Agent",
|
||||
"chatHub.agent.configureCredentials": "Configure credentials",
|
||||
"chatHub.agent.addModel": "Add model",
|
||||
"chatHub.agent.editor.title.new": "New Agent",
|
||||
"chatHub.agent.editor.title.edit": "Edit Agent",
|
||||
"chatHub.agent.editor.name.label": "Name",
|
||||
|
|
@ -332,6 +334,15 @@
|
|||
"chatHub.tools.editor.selectedCount": "{count} tool selected | {count} tools selected",
|
||||
"chatHub.tools.editor.confirm": "Confirm",
|
||||
"chatHub.tools.editor.cancel": "Cancel",
|
||||
"chatHub.credentials.selector.title": "Select {provider} credential",
|
||||
"chatHub.credentials.selector.chooseOrCreate": "Choose or create a credential for {provider}",
|
||||
"chatHub.credentials.selector.createNew": "Create new",
|
||||
"chatHub.credentials.selector.confirm": "Select",
|
||||
"chatHub.credentials.selector.cancel": "Cancel",
|
||||
"chatHub.models.byIdSelector.title": "Choose {provider} model by ID",
|
||||
"chatHub.models.byIdSelector.choose": "Enter model identifier (e.g. \"gpt-4\")",
|
||||
"chatHub.models.byIdSelector.confirm": "Select",
|
||||
"chatHub.models.byIdSelector.cancel": "Cancel",
|
||||
"chatEmbed.infoTip.description": "Add chat to external applications using the n8n chat package.",
|
||||
"chatEmbed.infoTip.link": "More info",
|
||||
"chatEmbed.title": "Embed Chat in your website",
|
||||
|
|
|
|||
|
|
@ -704,7 +704,27 @@ export const useChatStore = defineStore(CHAT_STORE, () => {
|
|||
function getAgent(model: ChatHubConversationModel) {
|
||||
if (!agents.value) return;
|
||||
|
||||
return 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') {
|
||||
return;
|
||||
}
|
||||
|
||||
// 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,
|
||||
};
|
||||
}
|
||||
|
||||
return agent;
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue';
|
||||
import { N8nButton, N8nOption, N8nSelect, N8nText } from '@n8n/design-system';
|
||||
import { N8nButton, N8nHeading, N8nOption, N8nSelect, N8nText } from '@n8n/design-system';
|
||||
import Modal from '@/app/components/Modal.vue';
|
||||
import { useCredentialsStore } from '@/features/credentials/credentials.store';
|
||||
import type { ICredentialsResponse } from '@/features/credentials/credentials.types';
|
||||
|
|
@ -8,23 +8,25 @@ import { createEventBus } from '@n8n/utils/event-bus';
|
|||
import { type ChatHubLLMProvider, PROVIDER_CREDENTIAL_TYPE_MAP } from '@n8n/api-types';
|
||||
import { providerDisplayNames } from '@/features/ai/chatHub/constants';
|
||||
import CredentialIcon from '@/features/credentials/components/CredentialIcon.vue';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
|
||||
const props = defineProps<{
|
||||
provider: ChatHubLLMProvider;
|
||||
initialValue: string | null;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
select: [provider: ChatHubLLMProvider, credentialId: string];
|
||||
createNew: [provider: ChatHubLLMProvider];
|
||||
modalName: string;
|
||||
data: {
|
||||
provider: ChatHubLLMProvider;
|
||||
initialValue: string | null;
|
||||
onSelect: (provider: ChatHubLLMProvider, credentialId: string) => void;
|
||||
onCreateNew: (provider: ChatHubLLMProvider) => void;
|
||||
};
|
||||
}>();
|
||||
|
||||
const i18n = useI18n();
|
||||
const credentialsStore = useCredentialsStore();
|
||||
const modalBus = ref(createEventBus());
|
||||
const selectedCredentialId = ref<string | null>(props.initialValue);
|
||||
const selectedCredentialId = ref<string | null>(props.data.initialValue);
|
||||
|
||||
const availableCredentials = computed<ICredentialsResponse[]>(() => {
|
||||
return credentialsStore.getCredentialsByType(PROVIDER_CREDENTIAL_TYPE_MAP[props.provider]);
|
||||
return credentialsStore.getCredentialsByType(PROVIDER_CREDENTIAL_TYPE_MAP[props.data.provider]);
|
||||
});
|
||||
|
||||
function onCredentialSelect(credentialId: string) {
|
||||
|
|
@ -33,13 +35,13 @@ function onCredentialSelect(credentialId: string) {
|
|||
|
||||
function onConfirm() {
|
||||
if (selectedCredentialId.value) {
|
||||
emit('select', props.provider, selectedCredentialId.value);
|
||||
props.data.onSelect(props.data.provider, selectedCredentialId.value);
|
||||
modalBus.value.emit('close');
|
||||
}
|
||||
}
|
||||
|
||||
function onCreateNew() {
|
||||
emit('createNew', props.provider);
|
||||
props.data.onCreateNew(props.data.provider);
|
||||
modalBus.value.emit('close');
|
||||
}
|
||||
|
||||
|
|
@ -50,7 +52,7 @@ function onCancel() {
|
|||
|
||||
<template>
|
||||
<Modal
|
||||
name="chatCredentialSelector"
|
||||
:name="modalName"
|
||||
:event-bus="modalBus"
|
||||
width="50%"
|
||||
:center="true"
|
||||
|
|
@ -60,17 +62,31 @@ function onCancel() {
|
|||
<template #header>
|
||||
<div :class="$style.header">
|
||||
<CredentialIcon
|
||||
:credential-type-name="PROVIDER_CREDENTIAL_TYPE_MAP[provider]"
|
||||
:credential-type-name="PROVIDER_CREDENTIAL_TYPE_MAP[data.provider]"
|
||||
:size="24"
|
||||
:class="$style.icon"
|
||||
/>
|
||||
<h2 :class="$style.title">Select {{ providerDisplayNames[provider] }} Credential</h2>
|
||||
<N8nHeading size="medium" tag="h2" :class="$style.title">
|
||||
{{
|
||||
i18n.baseText('chatHub.credentials.selector.title', {
|
||||
interpolate: {
|
||||
provider: providerDisplayNames[data.provider],
|
||||
},
|
||||
})
|
||||
}}
|
||||
</N8nHeading>
|
||||
</div>
|
||||
</template>
|
||||
<template #content>
|
||||
<div :class="$style.content">
|
||||
<N8nText size="small" color="text-base">
|
||||
Choose an existing credential or create a new one
|
||||
{{
|
||||
i18n.baseText('chatHub.credentials.selector.chooseOrCreate', {
|
||||
interpolate: {
|
||||
provider: providerDisplayNames[data.provider],
|
||||
},
|
||||
})
|
||||
}}
|
||||
</N8nText>
|
||||
<N8nSelect
|
||||
:model-value="selectedCredentialId"
|
||||
|
|
@ -89,11 +105,15 @@ function onCancel() {
|
|||
</template>
|
||||
<template #footer>
|
||||
<div :class="$style.footer">
|
||||
<N8nButton type="secondary" @click="onCreateNew"> Create New </N8nButton>
|
||||
<N8nButton type="secondary" @click="onCreateNew">
|
||||
{{ i18n.baseText('chatHub.credentials.selector.createNew') }}
|
||||
</N8nButton>
|
||||
<div :class="$style.footerRight">
|
||||
<N8nButton type="tertiary" @click="onCancel"> Cancel </N8nButton>
|
||||
<N8nButton type="tertiary" @click="onCancel">
|
||||
{{ i18n.baseText('chatHub.credentials.selector.cancel') }}
|
||||
</N8nButton>
|
||||
<N8nButton type="primary" :disabled="!selectedCredentialId" @click="onConfirm">
|
||||
Select
|
||||
{{ i18n.baseText('chatHub.credentials.selector.confirm') }}
|
||||
</N8nButton>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -102,12 +122,6 @@ function onCancel() {
|
|||
</template>
|
||||
|
||||
<style lang="scss" module>
|
||||
.title {
|
||||
font-size: var(--font-size--lg);
|
||||
line-height: var(--line-height--md);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,130 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { N8nButton, N8nFormInput, N8nHeading, N8nText } from '@n8n/design-system';
|
||||
import Modal from '@/app/components/Modal.vue';
|
||||
import { createEventBus } from '@n8n/utils/event-bus';
|
||||
import { type ChatHubLLMProvider, PROVIDER_CREDENTIAL_TYPE_MAP } from '@n8n/api-types';
|
||||
import {
|
||||
CHAT_MODEL_BY_ID_SELECTOR_MODAL_KEY,
|
||||
providerDisplayNames,
|
||||
} from '@/features/ai/chatHub/constants';
|
||||
import CredentialIcon from '@/features/credentials/components/CredentialIcon.vue';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
|
||||
const props = defineProps<{
|
||||
modalName: string;
|
||||
data: {
|
||||
provider: ChatHubLLMProvider;
|
||||
initialValue: string | null;
|
||||
onSelect: (provider: ChatHubLLMProvider, modelId: string) => void;
|
||||
};
|
||||
}>();
|
||||
|
||||
const modalBus = ref(createEventBus());
|
||||
const modelId = ref<string | null>(props.data.initialValue);
|
||||
const inputRef = ref<InstanceType<typeof N8nFormInput> | null>(null);
|
||||
|
||||
const i18n = useI18n();
|
||||
|
||||
onMounted(() => {
|
||||
// With modals normal focusing via `props.focus-initially` on N8nFormInput does not work
|
||||
setTimeout(() => {
|
||||
inputRef.value?.inputRef?.select();
|
||||
inputRef.value?.inputRef?.focus();
|
||||
});
|
||||
});
|
||||
|
||||
function onConfirm() {
|
||||
if (modelId.value) {
|
||||
props.data.onSelect(props.data.provider, modelId.value);
|
||||
modalBus.value.emit('close');
|
||||
}
|
||||
}
|
||||
|
||||
function onCancel() {
|
||||
modalBus.value.emit('close');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal
|
||||
:name="CHAT_MODEL_BY_ID_SELECTOR_MODAL_KEY"
|
||||
:event-bus="modalBus"
|
||||
width="50%"
|
||||
:center="true"
|
||||
max-width="460px"
|
||||
min-height="250px"
|
||||
>
|
||||
<template #header>
|
||||
<div :class="$style.header">
|
||||
<CredentialIcon
|
||||
:credential-type-name="PROVIDER_CREDENTIAL_TYPE_MAP[data.provider]"
|
||||
:size="24"
|
||||
:class="$style.icon"
|
||||
/>
|
||||
<N8nHeading size="medium" tag="h2" :class="$style.title">
|
||||
{{
|
||||
i18n.baseText('chatHub.models.byIdSelector.title', {
|
||||
interpolate: {
|
||||
provider: providerDisplayNames[data.provider],
|
||||
},
|
||||
})
|
||||
}}
|
||||
</N8nHeading>
|
||||
</div>
|
||||
</template>
|
||||
<template #content>
|
||||
<div :class="$style.content">
|
||||
<N8nText size="small" color="text-base">
|
||||
{{ i18n.baseText('chatHub.models.byIdSelector.choose') }}
|
||||
</N8nText>
|
||||
<N8nFormInput
|
||||
ref="inputRef"
|
||||
v-model="modelId"
|
||||
name="model"
|
||||
label=""
|
||||
max-length="64"
|
||||
focus-initially
|
||||
@enter="onConfirm"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<div :class="$style.footer">
|
||||
<N8nButton type="tertiary" @click="onCancel">
|
||||
{{ i18n.baseText('chatHub.models.byIdSelector.cancel') }}
|
||||
</N8nButton>
|
||||
<N8nButton type="primary" :disabled="!modelId" @click="onConfirm">
|
||||
{{ i18n.baseText('chatHub.models.byIdSelector.confirm') }}
|
||||
</N8nButton>
|
||||
</div>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<style lang="scss" module>
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing--sm);
|
||||
padding: var(--spacing--sm) 0;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
gap: var(--spacing--2xs);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.icon {
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -13,13 +13,16 @@ import type {
|
|||
ChatModelDto,
|
||||
ChatModelsResponse,
|
||||
} from '@n8n/api-types';
|
||||
import { providerDisplayNames } from '@/features/ai/chatHub/constants';
|
||||
import {
|
||||
CHAT_CREDENTIAL_SELECTOR_MODAL_KEY,
|
||||
CHAT_MODEL_BY_ID_SELECTOR_MODAL_KEY,
|
||||
providerDisplayNames,
|
||||
} from '@/features/ai/chatHub/constants';
|
||||
import CredentialIcon from '@/features/credentials/components/CredentialIcon.vue';
|
||||
import { onClickOutside } from '@vueuse/core';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
|
||||
import type { CredentialsMap } from '../chat.types';
|
||||
import CredentialSelectorModal from './CredentialSelectorModal.vue';
|
||||
import { useUIStore } from '@/app/stores/ui.store';
|
||||
import { useCredentialsStore } from '@/features/credentials/credentials.store';
|
||||
import ChatAgentAvatar from '@/features/ai/chatHub/components/ChatAgentAvatar.vue';
|
||||
|
|
@ -55,10 +58,22 @@ function handleSelectCredentials(provider: ChatHubProvider, id: string) {
|
|||
emit('selectCredential', provider, id);
|
||||
}
|
||||
|
||||
function handleSelectModelById(provider: ChatHubLLMProvider, modelId: string) {
|
||||
emit('change', {
|
||||
model: {
|
||||
provider,
|
||||
model: modelId,
|
||||
},
|
||||
name: modelId,
|
||||
description: null,
|
||||
updatedAt: null,
|
||||
createdAt: null,
|
||||
});
|
||||
}
|
||||
|
||||
const i18n = useI18n();
|
||||
const agents = ref<ChatModelsResponse>(emptyChatModelsResponse);
|
||||
const dropdownRef = useTemplateRef('dropdownRef');
|
||||
const credentialSelectorProvider = ref<ChatHubLLMProvider | null>(null);
|
||||
const uiStore = useUIStore();
|
||||
const credentialsStore = useCredentialsStore();
|
||||
const telemetry = useTelemetry();
|
||||
|
|
@ -119,10 +134,16 @@ const menu = computed(() => {
|
|||
|
||||
const submenu = agentOptions.concat([
|
||||
...(agentOptions.length > 0 ? [{ isDivider: true as const, id: 'divider' }] : []),
|
||||
{
|
||||
id: `${provider}::add-model`,
|
||||
icon: 'plus',
|
||||
title: i18n.baseText('chatHub.agent.addModel'),
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
id: `${provider}::configure`,
|
||||
icon: 'settings',
|
||||
title: 'Configure credentials...',
|
||||
title: i18n.baseText('chatHub.agent.configureCredentials'),
|
||||
disabled: false,
|
||||
},
|
||||
]);
|
||||
|
|
@ -148,8 +169,26 @@ function openCredentialsSelectorOrCreate(provider: ChatHubLLMProvider) {
|
|||
return;
|
||||
}
|
||||
|
||||
credentialSelectorProvider.value = provider;
|
||||
uiStore.openModal('chatCredentialSelector');
|
||||
uiStore.openModalWithData({
|
||||
name: CHAT_CREDENTIAL_SELECTOR_MODAL_KEY,
|
||||
data: {
|
||||
provider,
|
||||
initialValue: credentials?.[provider] ?? null,
|
||||
onSelect: handleSelectCredentials,
|
||||
onCreateNew: handleCreateNewCredential,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function openModelByIdSelector(provider: ChatHubLLMProvider) {
|
||||
uiStore.openModalWithData({
|
||||
name: CHAT_MODEL_BY_ID_SELECTOR_MODAL_KEY,
|
||||
data: {
|
||||
provider,
|
||||
initialValue: null,
|
||||
onSelect: handleSelectModelById,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function onSelect(id: string) {
|
||||
|
|
@ -174,6 +213,15 @@ function onSelect(id: string) {
|
|||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
identifier === 'add-model' &&
|
||||
parsedModel.provider !== 'n8n' &&
|
||||
parsedModel.provider !== 'custom-agent'
|
||||
) {
|
||||
openModelByIdSelector(parsedModel.provider);
|
||||
return;
|
||||
}
|
||||
|
||||
const selected = agents.value[parsedModel.provider].models.find((a) =>
|
||||
isMatchedAgent(a, parsedModel),
|
||||
);
|
||||
|
|
@ -242,15 +290,6 @@ defineExpose({
|
|||
</template>
|
||||
|
||||
<N8nButton :class="$style.dropdownButton" type="secondary" text>
|
||||
<CredentialSelectorModal
|
||||
v-if="credentialSelectorProvider"
|
||||
:key="credentialSelectorProvider"
|
||||
:provider="credentialSelectorProvider"
|
||||
:initial-value="credentials?.[credentialSelectorProvider] ?? null"
|
||||
@select="handleSelectCredentials"
|
||||
@create-new="handleCreateNewCredential"
|
||||
/>
|
||||
|
||||
<ChatAgentAvatar
|
||||
v-if="selectedAgent"
|
||||
:agent="selectedAgent"
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ export const providerDisplayNames: Record<ChatHubProvider, string> = {
|
|||
openai: 'OpenAI',
|
||||
anthropic: 'Anthropic',
|
||||
google: 'Google',
|
||||
azureOpenAi: 'Azure OpenAI',
|
||||
ollama: 'Ollama',
|
||||
n8n: 'n8n',
|
||||
'custom-agent': 'Custom Agent',
|
||||
};
|
||||
|
|
@ -19,3 +21,5 @@ export const MOBILE_MEDIA_QUERY = '(max-width: 768px)';
|
|||
|
||||
export const TOOLS_SELECTOR_MODAL_KEY = 'toolsSelectorModal';
|
||||
export const AGENT_EDITOR_MODAL_KEY = 'agentEditorModal';
|
||||
export const CHAT_CREDENTIAL_SELECTOR_MODAL_KEY = 'chatCredentialSelectorModal';
|
||||
export const CHAT_MODEL_BY_ID_SELECTOR_MODAL_KEY = 'chatModelByIdSelectorModal';
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import {
|
|||
CHAT_AGENTS_VIEW,
|
||||
TOOLS_SELECTOR_MODAL_KEY,
|
||||
AGENT_EDITOR_MODAL_KEY,
|
||||
CHAT_CREDENTIAL_SELECTOR_MODAL_KEY,
|
||||
CHAT_MODEL_BY_ID_SELECTOR_MODAL_KEY,
|
||||
} from '@/features/ai/chatHub/constants';
|
||||
|
||||
const ChatSidebar = async () => await import('@/features/ai/chatHub/components/ChatSidebar.vue');
|
||||
|
|
@ -40,6 +42,31 @@ export const ChatModule: FrontendModuleDescription = {
|
|||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
key: CHAT_CREDENTIAL_SELECTOR_MODAL_KEY,
|
||||
component: async () => await import('./components/CredentialSelectorModal.vue'),
|
||||
initialState: {
|
||||
open: false,
|
||||
data: {
|
||||
provider: null,
|
||||
initialValue: null,
|
||||
onSelect: () => {},
|
||||
onCreateNew: () => {},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
key: CHAT_MODEL_BY_ID_SELECTOR_MODAL_KEY,
|
||||
component: async () => await import('./components/ModelByIdSelectorModal.vue'),
|
||||
initialState: {
|
||||
open: false,
|
||||
data: {
|
||||
provider: null,
|
||||
initialValue: null,
|
||||
onSelect: () => {},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
routes: [
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user