From b84d2fcbb0f588e258846ed7820e7d5076c76785 Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Wed, 9 Jul 2025 20:05:06 +0300 Subject: [PATCH] UI: Refactoring --- ui-ngx/src/app/modules/common/modules-map.ts | 2 - .../check-connectivity-dialog.component.html | 0 .../check-connectivity-dialog.component.scss | 0 .../check-connectivity-dialog.component.ts | 4 +- .../home/components/home-components.module.ts | 3 + .../common/time-unit-input.component.html | 98 +++------ .../common/time-unit-input.component.ts | 29 ++- .../external/ai-config.component.html | 28 +-- .../rule-node/external/ai-config.component.ts | 36 ++-- .../ai-model/ai-model-table-config.resolve.ts | 11 +- .../ai-model-table-header.component.scss | 18 -- .../ai-model-table-header.component.ts | 2 +- .../ai-model/ai-model-dialog.component.html | 166 ++++++++------- .../ai-model/ai-model-dialog.component.ts | 54 ++--- .../models-list-autocomplete.component.ts | 4 +- .../src/app/shared/models/ai-model.models.ts | 201 ++++++++++++------ ui-ngx/src/app/shared/shared.module.ts | 3 - 17 files changed, 335 insertions(+), 324 deletions(-) rename ui-ngx/src/app/{shared => modules/home}/components/ai-model/check-connectivity-dialog.component.html (100%) rename ui-ngx/src/app/{shared => modules/home}/components/ai-model/check-connectivity-dialog.component.scss (100%) rename ui-ngx/src/app/{shared => modules/home}/components/ai-model/check-connectivity-dialog.component.ts (95%) delete mode 100644 ui-ngx/src/app/modules/home/pages/ai-model/ai-model-table-header.component.scss diff --git a/ui-ngx/src/app/modules/common/modules-map.ts b/ui-ngx/src/app/modules/common/modules-map.ts index 997af07462..60a26e4211 100644 --- a/ui-ngx/src/app/modules/common/modules-map.ts +++ b/ui-ngx/src/app/modules/common/modules-map.ts @@ -337,7 +337,6 @@ import * as AggregationTypeSelectComponent from '@shared/components/time/aggrega import * as AggregationOptionsConfigComponent from '@shared/components/time/aggregation/aggregation-options-config-panel.component'; import * as IntervalOptionsConfigPanelComponent from '@shared/components/time/interval-options-config-panel.component'; import * as AIModelDialogComponent from '@shared/components/ai-model/ai-model-dialog.component'; -import * as CheckConnectivityDialogComponent from '@shared/components/ai-model/check-connectivity-dialog.component'; import * as ModelsListAutocompleteComponent from '@shared/components/ai-model/models-list-autocomplete.component'; import { IModulesMap } from '@modules/common/modules-map.models'; @@ -536,7 +535,6 @@ class ModulesMap implements IModulesMap { '@shared/components/image/multiple-gallery-image-input.component': MultipleGalleryImageInputComponent, '@shared/components/popover.service': TbPopoverService, '@shared/components/ai-model/ai-model-dialog.component': AIModelDialogComponent, - '@shared/components/ai-model/check-connectivity-dialog.component': CheckConnectivityDialogComponent, '@shared/components/ai-model/models-list-autocomplete.component': ModelsListAutocompleteComponent, diff --git a/ui-ngx/src/app/shared/components/ai-model/check-connectivity-dialog.component.html b/ui-ngx/src/app/modules/home/components/ai-model/check-connectivity-dialog.component.html similarity index 100% rename from ui-ngx/src/app/shared/components/ai-model/check-connectivity-dialog.component.html rename to ui-ngx/src/app/modules/home/components/ai-model/check-connectivity-dialog.component.html diff --git a/ui-ngx/src/app/shared/components/ai-model/check-connectivity-dialog.component.scss b/ui-ngx/src/app/modules/home/components/ai-model/check-connectivity-dialog.component.scss similarity index 100% rename from ui-ngx/src/app/shared/components/ai-model/check-connectivity-dialog.component.scss rename to ui-ngx/src/app/modules/home/components/ai-model/check-connectivity-dialog.component.scss diff --git a/ui-ngx/src/app/shared/components/ai-model/check-connectivity-dialog.component.ts b/ui-ngx/src/app/modules/home/components/ai-model/check-connectivity-dialog.component.ts similarity index 95% rename from ui-ngx/src/app/shared/components/ai-model/check-connectivity-dialog.component.ts rename to ui-ngx/src/app/modules/home/components/ai-model/check-connectivity-dialog.component.ts index e7f0d8c174..3b6c96d426 100644 --- a/ui-ngx/src/app/shared/components/ai-model/check-connectivity-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/ai-model/check-connectivity-dialog.component.ts @@ -20,7 +20,7 @@ import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { Router } from '@angular/router'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; -import { AiModel, AiModelWithUserMsg } from '@shared/models/ai-model.models'; +import { AiModel, AiModelWithUserMsg, ModelType } from '@shared/models/ai-model.models'; import { AiModelService } from '@core/http/ai-model.service'; export interface AIModelDialogData { @@ -55,7 +55,7 @@ export class CheckConnectivityDialogComponent extends DialogComponent
- @if (outlined) { -
-
{{ labelText }}
-
- - - - warning - - - warning - - - warning - - - - - @for (timeUnit of timeUnits; track timeUnit) { - {{ timeUnitTranslations.get(timeUnit) | translate }} - } - - -
+ + @if (labelText && !inlineField) { + {{ labelText }} + } + +
+
- } @else { - - {{ labelText }} - -
- -
+ @if (inlineField) { + + warning + + } @else { - - {{ requiredText }} + + {{ hasError }} - - {{ minErrorText }} - - - {{ maxErrorText }} - -
- + } + + + @if (!inlineField) { rule-node-config.units - - @for (timeUnit of timeUnits; track timeUnit) { - {{ timeUnitTranslations.get(timeUnit) | translate }} - } - - - } + } + + @for (timeUnit of timeUnits; track timeUnit) { + {{ timeUnitTranslations.get(timeUnit) | translate }} + } + +
diff --git a/ui-ngx/src/app/modules/home/components/rule-node/common/time-unit-input.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/common/time-unit-input.component.ts index 2cd8e7d879..e31d9abf9e 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/common/time-unit-input.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/common/time-unit-input.component.ts @@ -30,7 +30,7 @@ import { isDefinedAndNotNull, isNumeric } from '@core/utils'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { coerceBoolean, coerceNumber } from '@shared/decorators/coercion'; import { DAY, HOUR, MINUTE, SECOND } from '@shared/models/time/time.models'; -import { SubscriptSizing } from '@angular/material/form-field'; +import { MatFormFieldAppearance, SubscriptSizing } from '@angular/material/form-field'; interface TimeUnitInputModel { time: number; @@ -55,9 +55,6 @@ export class TimeUnitInputComponent implements ControlValueAccessor, Validator, @Input() labelText: string; - @Input() - labelIconHintTooltipText: string; - @Input() @coerceBoolean() required: boolean; @@ -83,12 +80,11 @@ export class TimeUnitInputComponent implements ControlValueAccessor, Validator, subscriptSizing: SubscriptSizing = 'fixed'; @Input() - @coerceBoolean() - filterTimeUnitsByMaxTime = false; + appearance: MatFormFieldAppearance = 'fill'; @Input() @coerceBoolean() - outlined = false; + inlineField: boolean; timeUnits = Object.values(TimeUnit).filter(item => item !== TimeUnit.MILLISECONDS) as TimeUnit[]; @@ -115,12 +111,13 @@ export class TimeUnitInputComponent implements ControlValueAccessor, Validator, } ngOnInit() { - if (this.filterTimeUnitsByMaxTime && this.maxTime) { - if (this.maxTime < 60) { + if (this.maxTime) { + const maxTimeMs = this.maxTime * SECOND; + if (maxTimeMs < MINUTE) { this.timeUnits = this.timeUnits.filter(item => item !== TimeUnit.MINUTES && item !== TimeUnit.HOURS && item !== TimeUnit.DAYS); - } else if (this.maxTime < 3600) { + } else if (maxTimeMs < HOUR) { this.timeUnits = this.timeUnits.filter(item => item !== TimeUnit.HOURS && item !== TimeUnit.DAYS); - } else if (this.maxTime < 86400) { + } else if (maxTimeMs < DAY) { this.timeUnits = this.timeUnits.filter(item => item !== TimeUnit.DAYS); } } @@ -157,6 +154,16 @@ export class TimeUnitInputComponent implements ControlValueAccessor, Validator, }); } + get hasError(): string { + if (this.timeInputForm.get('time').hasError('required') && this.requiredText) { + return this.requiredText; + } else if (this.timeInputForm.get('time').hasError('min') && this.minErrorText) { + return this.minErrorText; + } else if (this.timeInputForm.get('time').hasError('max') && this.maxErrorText) { + return this.maxErrorText; + } + } + registerOnChange(fn: any) { this.propagateChange = fn; } diff --git a/ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.html index 8846858a8d..ef9dbe04a4 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.html @@ -96,19 +96,21 @@ rule-node-config.ai.advanced-settings
- - +
+
{{ 'rule-node-config.ai.timeout' | translate }}
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.ts index 283abb636c..3b7f0a92d6 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.ts @@ -20,15 +20,9 @@ import { RuleNodeConfiguration, RuleNodeConfigurationComponent } from '@shared/m import { EntityType } from '@shared/models/entity-type.models'; import { MatDialog } from '@angular/material/dialog'; import { AIModelDialogComponent, AIModelDialogData } from '@shared/components/ai-model/ai-model-dialog.component'; -import { AiModel, AiProvider } from '@shared/models/ai-model.models'; +import { AiModel, AiRuleNodeResponseFormatTypeOnlyText, ResponseFormat } from '@shared/models/ai-model.models'; import { deepTrim } from '@core/utils'; -enum ResponseFormat { - TEXT = 'TEXT', - JSON = 'JSON', - JSON_SCHEMA = 'JSON_SCHEMA' -} - @Component({ selector: 'tb-external-node-ai-config', templateUrl: './ai-config.component.html', @@ -53,14 +47,14 @@ export class AiConfigComponent extends RuleNodeConfigurationComponent { protected onConfigurationSet(configuration: RuleNodeConfiguration) { this.aiConfigForm = this.fb.group({ - modelSettingsId: [configuration ? configuration.modelSettingsId : null, [Validators.required]], - systemPrompt: [configuration ? configuration.systemPrompt : '', [Validators.maxLength(10000), Validators.pattern(/.*\S.*/)]], - userPrompt: [configuration ? configuration.userPrompt : '', [Validators.required, Validators.maxLength(10000), Validators.pattern(/.*\S.*/)]], + modelSettingsId: [configuration?.modelSettingsId ?? null, [Validators.required]], + systemPrompt: [configuration?.systemPrompt ?? '', [Validators.maxLength(10000), Validators.pattern(/.*\S.*/)]], + userPrompt: [configuration?.userPrompt ?? '', [Validators.required, Validators.maxLength(10000), Validators.pattern(/.*\S.*/)]], responseFormat: this.fb.group({ - type: [configuration ? configuration.responseFormat.type : ResponseFormat.JSON, []], - schema: [configuration ? configuration.responseFormat.schema : null, []], + type: [configuration?.responseFormat?.type ?? ResponseFormat.JSON, []], + schema: [configuration?.responseFormat?.schema ?? null, [Validators.required]], }), - timeoutSeconds: [configuration ? configuration.timeoutSeconds : 60, []] + timeoutSeconds: [configuration?.timeoutSeconds ?? 60, []] }); } @@ -69,15 +63,11 @@ export class AiConfigComponent extends RuleNodeConfigurationComponent { } protected updateValidators(emitEvent: boolean) { - const responseFormatType = this.aiConfigForm.get('responseFormat.type').value; - if (responseFormatType === ResponseFormat.JSON_SCHEMA) { - this.aiConfigForm.get('responseFormat.schema').setValidators([Validators.required]); - this.aiConfigForm.get('responseFormat.schema').enable(); + if (this.aiConfigForm.get('responseFormat.type').value === ResponseFormat.JSON_SCHEMA) { + this.aiConfigForm.get('responseFormat.schema').enable({emitEvent: false}); } else { - this.aiConfigForm.get('responseFormat.schema').setValidators([]); - this.aiConfigForm.get('responseFormat.schema').disable(); + this.aiConfigForm.get('responseFormat.schema').disable({emitEvent: false}); } - this.aiConfigForm.get('responseFormat.schema').updateValueAndValidity({emitEvent}); } protected prepareOutputConfig(configuration: RuleNodeConfiguration): RuleNodeConfiguration { @@ -86,9 +76,7 @@ export class AiConfigComponent extends RuleNodeConfigurationComponent { onEntityChange($event: AiModel) { if ($event) { - if ($event.configuration.provider === AiProvider.AMAZON_BEDROCK || - $event.configuration.provider === AiProvider.ANTHROPIC || - $event.configuration.provider === AiProvider.GITHUB_MODELS) { + if (AiRuleNodeResponseFormatTypeOnlyText.includes($event.configuration.provider)) { this.aiConfigForm.get('responseFormat.type').patchValue(ResponseFormat.TEXT, {emitEvent: false}); this.aiConfigForm.get('responseFormat.type').disable({emitEvent: false}); } @@ -97,7 +85,7 @@ export class AiConfigComponent extends RuleNodeConfigurationComponent { } } - createModelAi(formControl: string,) { + createModelAi(formControl: string) { this.dialog.open(AIModelDialogComponent, { disableClose: true, panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], diff --git a/ui-ngx/src/app/modules/home/pages/ai-model/ai-model-table-config.resolve.ts b/ui-ngx/src/app/modules/home/pages/ai-model/ai-model-table-config.resolve.ts index 6e9bde0b60..3f3ded2abb 100644 --- a/ui-ngx/src/app/modules/home/pages/ai-model/ai-model-table-config.resolve.ts +++ b/ui-ngx/src/app/modules/home/pages/ai-model/ai-model-table-config.resolve.ts @@ -79,6 +79,11 @@ export class AiModelsTableConfigResolver { this.config.entitiesFetchFunction = pageLink => this.aiModelService.getAiModels(pageLink); this.config.cellActionDescriptors = this.configureCellActions(); + + this.config.handleRowClick = ($event, model) => { + this.editModel(model); + return true; + }; } resolve(_route: ActivatedRouteSnapshot): EntityTableConfig { @@ -97,11 +102,7 @@ export class AiModelsTableConfigResolver { } private editModel(AIModel: AiModel): void { - this.addModel(AIModel, false).subscribe((res) => { - if (res) { - this.config.updateData(); - } - }); + this.addModel(AIModel, false).subscribe(); } private addModel(AIModel: AiModel, isAdd = false): Observable { diff --git a/ui-ngx/src/app/modules/home/pages/ai-model/ai-model-table-header.component.scss b/ui-ngx/src/app/modules/home/pages/ai-model/ai-model-table-header.component.scss deleted file mode 100644 index acb9e2dc87..0000000000 --- a/ui-ngx/src/app/modules/home/pages/ai-model/ai-model-table-header.component.scss +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Copyright © 2016-2025 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -:host{ - width: 100%; -} diff --git a/ui-ngx/src/app/modules/home/pages/ai-model/ai-model-table-header.component.ts b/ui-ngx/src/app/modules/home/pages/ai-model/ai-model-table-header.component.ts index 4fbc906b80..48889dc877 100644 --- a/ui-ngx/src/app/modules/home/pages/ai-model/ai-model-table-header.component.ts +++ b/ui-ngx/src/app/modules/home/pages/ai-model/ai-model-table-header.component.ts @@ -23,7 +23,7 @@ import { AiModel } from '@shared/models/ai-model.models'; @Component({ selector: 'tb-ai-model-table-header', templateUrl: './ai-model-table-header.component.html', - styleUrls: ['./ai-model-table-header.component.scss'] + styleUrls: [] }) export class AiModelTableHeaderComponent extends EntityTableHeaderComponent { diff --git a/ui-ngx/src/app/shared/components/ai-model/ai-model-dialog.component.html b/ui-ngx/src/app/shared/components/ai-model/ai-model-dialog.component.html index 97eeb79f02..2a5a34a2c9 100644 --- a/ui-ngx/src/app/shared/components/ai-model/ai-model-dialog.component.html +++ b/ui-ngx/src/app/shared/components/ai-model/ai-model-dialog.component.html @@ -56,7 +56,7 @@
- @if (provider === aiProvider.GITHUB_MODELS) { + @if (AiModelMap.get(provider).providerFieldsList.includes('personalAccessToken')) { ai-models.personal-access-token @@ -65,24 +65,29 @@ {{ 'ai-models.personal-access-token-required' | translate }} - } @else if (provider === aiProvider.GOOGLE_VERTEX_AI_GEMINI) { - + } + @if (AiModelMap.get(provider).providerFieldsList.includes('projectId')) { + ai-models.project-id {{ 'ai-models.project-id-required' | translate }} - + } + @if (AiModelMap.get(provider).providerFieldsList.includes('location')) { + ai-models.location {{ 'ai-models.location-required' | translate }} + } + @if (AiModelMap.get(provider).providerFieldsList.includes('serviceAccountKey')) { - } @else { - @if (provider === aiProvider.AZURE_OPENAI) { - - ai-models.endpoint - - - {{ 'ai-models.endpoint-required' | translate }} - - - - ai-models.service-version - - - } + } + @if (AiModelMap.get(provider).providerFieldsList.includes('endpoint')) { + + ai-models.endpoint + + + {{ 'ai-models.endpoint-required' | translate }} + + + } + @if (AiModelMap.get(provider).providerFieldsList.includes('serviceVersion')) { + + ai-models.service-version + + + } + @if (AiModelMap.get(provider).providerFieldsList.includes('apiKey')) { ai-models.api-key @@ -128,43 +136,46 @@ [errorText]="(provider === aiProvider.AZURE_OPENAI ? 'ai-models.deployment-name-required': 'ai-models.model-id-required') | translate"> -
-
- {{ 'ai-models.temperature' | translate }} -
- - - - warning - - -
-
-
- {{ 'ai-models.top-p' | translate }} + @if (AiModelMap.get(provider).modelFieldsList.includes('temperature')) { +
+
+ {{ 'ai-models.temperature' | translate }} +
+ + + + warning + +
- - - +
+ {{ 'ai-models.top-p' | translate }} +
+ + + - warning - - -
- - @if (provider === aiProvider.GOOGLE_AI_GEMINI || provider === aiProvider.GOOGLE_VERTEX_AI_GEMINI || provider === aiProvider.ANTHROPIC) { + class="tb-error"> + warning + + +
+ } + @if (AiModelMap.get(provider).modelFieldsList.includes('topK')) {
{{ 'ai-models.top-k' | translate }} @@ -183,45 +194,48 @@
} - - @if (provider !== aiProvider.ANTHROPIC && provider !== aiProvider.AMAZON_BEDROCK) { + @if (AiModelMap.get(provider).modelFieldsList.includes('presencePenalty')) {
{{ 'ai-models.presence-penalty' | translate }}
-
+ } + + @if (AiModelMap.get(provider).modelFieldsList.includes('frequencyPenalty')) {
{{ 'ai-models.frequency-penalty' | translate }}
-
} - -
-
- {{ 'ai-models.max-output-token' | translate }} + @if (AiModelMap.get(provider).modelFieldsList.includes('maxOutputTokens')) { +
+
+ {{ 'ai-models.max-output-token' | translate }} +
+ + + + warning + +
- - - - warning - - -
+ }
diff --git a/ui-ngx/src/app/shared/components/ai-model/ai-model-dialog.component.ts b/ui-ngx/src/app/shared/components/ai-model/ai-model-dialog.component.ts index 04087dcb79..d9ae3eaa69 100644 --- a/ui-ngx/src/app/shared/components/ai-model/ai-model-dialog.component.ts +++ b/ui-ngx/src/app/shared/components/ai-model/ai-model-dialog.component.ts @@ -25,9 +25,16 @@ import { StepperOrientation } from '@angular/cdk/stepper'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { EntityType } from '@shared/models/entity-type.models'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; -import { AiModel, AiProvider, AiProviderTranslations, AiProviderWithApiKey } from '@shared/models/ai-model.models'; +import { + AiModel, + AiModelMap, + AiProvider, + AiProviderTranslations, + ModelType, + ProviderFieldsAllList +} from '@shared/models/ai-model.models'; import { AiModelService } from '@core/http/ai-model.service'; -import { CheckConnectivityDialogComponent } from '@shared/components/ai-model/check-connectivity-dialog.component'; +import { CheckConnectivityDialogComponent } from '@home/components/ai-model/check-connectivity-dialog.component'; export interface AIModelDialogData { AIModel?: AiModel; @@ -53,7 +60,7 @@ export class AIModelDialogComponent extends DialogComponent { this.provider = provider; - // this.aiModelForms.get('configuration').reset({}); + this.aiModelForms.get('configuration.modelId').reset({}); this.aiModelForms.get('configuration.providerConfig').reset({}); this.updateValidation(provider); }) @@ -114,35 +121,10 @@ export class AIModelDialogComponent extends DialogComponent + this.aiModelForms.get('configuration.providerConfig') + .get(key)[AiModelMap.get(provider).providerFieldsList.includes(key) ? 'enable' : 'disable']() + ) } cancel(): void { diff --git a/ui-ngx/src/app/shared/components/ai-model/models-list-autocomplete.component.ts b/ui-ngx/src/app/shared/components/ai-model/models-list-autocomplete.component.ts index a5cbdb332d..3e3c0cbb66 100644 --- a/ui-ngx/src/app/shared/components/ai-model/models-list-autocomplete.component.ts +++ b/ui-ngx/src/app/shared/components/ai-model/models-list-autocomplete.component.ts @@ -99,7 +99,7 @@ export class ModelsListAutocompleteComponent implements ControlValueAccessor, On tap(value => this.updateView(value)), map(value => { const search = value ? value.toLowerCase() : ''; - const options = this.provider ? AiModelMap.get(this.provider) || [] : []; + const options = this.provider ? AiModelMap.get(this.provider).modelList || [] : []; return search ? options.filter(option => option.toLowerCase().includes(search)) : options; }) ); @@ -150,7 +150,7 @@ export class ModelsListAutocompleteComponent implements ControlValueAccessor, On } clear() { - this.selectionFormControl.patchValue(null, {emitEvent: false}); + this.selectionFormControl.patchValue(null, {emitEvent: true}); this.propagateChange(null); this.modelValue = null; setTimeout(() => { diff --git a/ui-ngx/src/app/shared/models/ai-model.models.ts b/ui-ngx/src/app/shared/models/ai-model.models.ts index cd844f57e6..1eb69d867e 100644 --- a/ui-ngx/src/app/shared/models/ai-model.models.ts +++ b/ui-ngx/src/app/shared/models/ai-model.models.ts @@ -19,7 +19,7 @@ import { HasTenantId } from '@shared/models/entity.models'; import { AiModelId } from '@shared/models/id/ai-model-id'; export interface AiModel extends Omit, 'label'>, HasTenantId, ExportableEntity { - modelType: string; + modelType: ModelType; configuration: { provider: AiProvider providerConfig: { @@ -30,10 +30,10 @@ export interface AiModel extends Omit, 'label'>, HasTenantId projectId?: string; location?: string; serviceAccountKey?: string; - serviceAccountKeyFileName?: string + fileName?: string }; modelId: string; - temperature?: number | null; + temperature?: number; topP?: number; topK?: number; frequencyPenalty?: number; @@ -42,6 +42,10 @@ export interface AiModel extends Omit, 'label'>, HasTenantId } } +export enum ModelType { + CHAT = 'CHAT' +} + export enum AiProvider { OPENAI = 'OPENAI', AZURE_OPENAI = 'AZURE_OPENAI', @@ -53,15 +57,6 @@ export enum AiProvider { GITHUB_MODELS = 'GITHUB_MODELS' } -export const AiProviderWithApiKey: AiProvider[] = [ - AiProvider.OPENAI, - AiProvider.AZURE_OPENAI, - AiProvider.GOOGLE_AI_GEMINI, - AiProvider.MISTRAL_AI, - AiProvider.ANTHROPIC, - AiProvider.AMAZON_BEDROCK -] - export const AiProviderTranslations = new Map( [ [AiProvider.OPENAI , 'ai-models.ai-providers.openai'], @@ -75,55 +70,130 @@ export const AiProviderTranslations = new Map( ] ); -export const AiModelMap = new Map( +export const ProviderFieldsAllList = [ + 'apiKey', + 'personalAccessToken', + 'projectId', + 'location', + 'serviceAccountKey', + 'fileName', + 'endpoint', + 'serviceVersion' +]; + +export const ModelFieldsAllList = ['temperature', 'topP', 'topK', 'frequencyPenalty', 'presencePenalty', 'maxOutputTokens']; + +export const AiModelMap = new Map([ [ - [AiProvider.OPENAI , [ - 'o4-mini', - 'o3-pro', - 'o3', - 'o3-mini', - 'o1', - 'gpt-4.1', - 'gpt-4.1-mini', - 'gpt-4.1-nano', - 'gpt-4o', - 'gpt-4o-mini' - ]], - [AiProvider.AZURE_OPENAI , []], - [AiProvider.GOOGLE_AI_GEMINI , [ - 'gemini-2.5-pro', - 'gemini-2.5-flash', - 'gemini-2.0-flash', - 'gemini-2.0-flash-lite', - ]], - [AiProvider.GOOGLE_VERTEX_AI_GEMINI , [ - 'gemini-2.5-pro', - 'gemini-2.5-flash', - 'gemini-2.0-flash', - 'gemini-2.0-flash-lite', - ]], - [AiProvider.MISTRAL_AI , [ - 'magistral-medium-latest', - 'magistral-small-latest', - 'mistral-large-latest', - 'mistral-medium-latest', - 'mistral-small-latest', - 'pixtral-large-latest', - 'ministral-8b-latest', - 'ministral-3b-latest', - 'open-mistral-nemo' - ]], - [AiProvider.ANTHROPIC , [ - 'claude-opus-4-0', - 'claude-sonnet-4-0', - 'claude-3-7-sonnet-latest', - 'claude-3-5-sonnet-latest', - 'claude-3-5-haiku-latest' - ]], - [AiProvider.AMAZON_BEDROCK , []], - [AiProvider.GITHUB_MODELS , []] - ] -); + AiProvider.OPENAI, + { + modelList: [ + 'o4-mini', + 'o3-pro', + 'o3', + 'o3-mini', + 'o1', + 'gpt-4.1', + 'gpt-4.1-mini', + 'gpt-4.1-nano', + 'gpt-4o', + 'gpt-4o-mini', + ], + providerFieldsList: ['apiKey'], + modelFieldsList: ['temperature', 'topP', 'frequencyPenalty', 'presencePenalty', 'maxOutputTokens'], + }, + ], + [ + AiProvider.AZURE_OPENAI, + { + modelList: [], + providerFieldsList: ['apiKey', 'endpoint', 'serviceVersion'], + modelFieldsList: ['temperature', 'topP', 'frequencyPenalty', 'presencePenalty', 'maxOutputTokens'], + }, + ], + [ + AiProvider.GOOGLE_AI_GEMINI, + { + modelList: [ + 'gemini-2.5-pro', + 'gemini-2.5-flash', + 'gemini-2.0-flash', + 'gemini-2.0-flash-lite', + ], + providerFieldsList: ['apiKey'], + modelFieldsList: ['temperature', 'topP', 'topK', 'frequencyPenalty', 'presencePenalty', 'maxOutputTokens'], + }, + ], + [ + AiProvider.GOOGLE_VERTEX_AI_GEMINI, + { + modelList: [ + 'gemini-2.5-pro', + 'gemini-2.5-flash', + 'gemini-2.0-flash', + 'gemini-2.0-flash-lite', + ], + providerFieldsList: ['projectId', 'location', 'serviceAccountKey', 'fileName'], + modelFieldsList: ['temperature', 'topP', 'topK', 'frequencyPenalty', 'presencePenalty', 'maxOutputTokens'], + }, + ], + [ + AiProvider.MISTRAL_AI, + { + modelList: [ + 'magistral-medium-latest', + 'magistral-small-latest', + 'mistral-large-latest', + 'mistral-medium-latest', + 'mistral-small-latest', + 'pixtral-large-latest', + 'ministral-8b-latest', + 'ministral-3b-latest', + 'open-mistral-nemo', + ], + providerFieldsList: ['apiKey'], + modelFieldsList: ['temperature', 'topP', 'frequencyPenalty', 'presencePenalty', 'maxOutputTokens'], + }, + ], + [ + AiProvider.ANTHROPIC, + { + modelList: [ + 'claude-opus-4-0', + 'claude-sonnet-4-0', + 'claude-3-7-sonnet-latest', + 'claude-3-5-sonnet-latest', + 'claude-3-5-haiku-latest', + ], + providerFieldsList: ['apiKey'], + modelFieldsList: ['temperature', 'topP', 'topK', 'maxOutputTokens'], + }, + ], + [ + AiProvider.AMAZON_BEDROCK, + { + modelList: [], + providerFieldsList: ['apiKey'], + modelFieldsList: ['temperature', 'topP', 'maxOutputTokens'], + }, + ], + [ + AiProvider.GITHUB_MODELS, + { + modelList: [], + providerFieldsList: ['personalAccessToken'], + modelFieldsList: ['temperature', 'topP', 'frequencyPenalty', 'presencePenalty', 'maxOutputTokens'], + }, + ], +]); + +export const AiRuleNodeResponseFormatTypeOnlyText: AiProvider[] = [AiProvider.AMAZON_BEDROCK, AiProvider.ANTHROPIC, AiProvider.GITHUB_MODELS]; + +export enum ResponseFormat { + TEXT = 'TEXT', + JSON = 'JSON', + JSON_SCHEMA = 'JSON_SCHEMA' +} export interface AiModelWithUserMsg { userMessage: { @@ -140,17 +210,14 @@ export interface AiModelWithUserMsg { projectId?: string; location?: string; serviceAccountKey?: string; - serviceAccountKeyFileName?: string + fileName?: string }; - // chatModelConfig: { - modelId: string; - maxRetries: number; - timeoutSeconds: number; - // } + modelId: string; + maxRetries: number; + timeoutSeconds: number; } } - export interface CheckConnectivityResult { status: string; errorDetails: string; diff --git a/ui-ngx/src/app/shared/shared.module.ts b/ui-ngx/src/app/shared/shared.module.ts index 8f3de3c26a..0a559361c8 100644 --- a/ui-ngx/src/app/shared/shared.module.ts +++ b/ui-ngx/src/app/shared/shared.module.ts @@ -230,7 +230,6 @@ import { DurationLeftPipe } from '@shared/pipe/duration-left.pipe'; import { MqttVersionSelectComponent } from '@shared/components/mqtt-version-select.component'; import { AIModelDialogComponent } from '@shared/components/ai-model/ai-model-dialog.component'; import { ModelsListAutocompleteComponent } from '@shared/components/ai-model/models-list-autocomplete.component'; -import { CheckConnectivityDialogComponent } from '@shared/components/ai-model/check-connectivity-dialog.component'; export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) { return markedOptionsService; @@ -448,7 +447,6 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) MqttVersionSelectComponent, AIModelDialogComponent, ModelsListAutocompleteComponent, - CheckConnectivityDialogComponent, ], imports: [ CommonModule, @@ -715,7 +713,6 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) MqttVersionSelectComponent, AIModelDialogComponent, ModelsListAutocompleteComponent, - CheckConnectivityDialogComponent, ] }) export class SharedModule { }