Browse Source

UI: Refactoring

pull/13371/head
Artem Dzhereleiko 1 year ago
parent
commit
b84d2fcbb0
  1. 2
      ui-ngx/src/app/modules/common/modules-map.ts
  2. 0
      ui-ngx/src/app/modules/home/components/ai-model/check-connectivity-dialog.component.html
  3. 0
      ui-ngx/src/app/modules/home/components/ai-model/check-connectivity-dialog.component.scss
  4. 4
      ui-ngx/src/app/modules/home/components/ai-model/check-connectivity-dialog.component.ts
  5. 3
      ui-ngx/src/app/modules/home/components/home-components.module.ts
  6. 98
      ui-ngx/src/app/modules/home/components/rule-node/common/time-unit-input.component.html
  7. 29
      ui-ngx/src/app/modules/home/components/rule-node/common/time-unit-input.component.ts
  8. 28
      ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.html
  9. 36
      ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.ts
  10. 11
      ui-ngx/src/app/modules/home/pages/ai-model/ai-model-table-config.resolve.ts
  11. 18
      ui-ngx/src/app/modules/home/pages/ai-model/ai-model-table-header.component.scss
  12. 2
      ui-ngx/src/app/modules/home/pages/ai-model/ai-model-table-header.component.ts
  13. 166
      ui-ngx/src/app/shared/components/ai-model/ai-model-dialog.component.html
  14. 54
      ui-ngx/src/app/shared/components/ai-model/ai-model-dialog.component.ts
  15. 4
      ui-ngx/src/app/shared/components/ai-model/models-list-autocomplete.component.ts
  16. 201
      ui-ngx/src/app/shared/models/ai-model.models.ts
  17. 3
      ui-ngx/src/app/shared/shared.module.ts

2
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,

0
ui-ngx/src/app/shared/components/ai-model/check-connectivity-dialog.component.html → ui-ngx/src/app/modules/home/components/ai-model/check-connectivity-dialog.component.html

0
ui-ngx/src/app/shared/components/ai-model/check-connectivity-dialog.component.scss → ui-ngx/src/app/modules/home/components/ai-model/check-connectivity-dialog.component.scss

4
ui-ngx/src/app/shared/components/ai-model/check-connectivity-dialog.component.ts → 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<CheckConne
]
},
chatModelConfig: {
modelType: "CHAT",
modelType: ModelType.CHAT,
provider: this.data.AIModel.configuration.provider,
providerConfig: {...this.data.AIModel.configuration.providerConfig},
modelId: this.data.AIModel.configuration.modelId,

3
ui-ngx/src/app/modules/home/components/home-components.module.ts

@ -203,6 +203,7 @@ import {
import {
CalculatedFieldTestArgumentsComponent
} from '@home/components/calculated-fields/components/test-arguments/calculated-field-test-arguments.component';
import { CheckConnectivityDialogComponent } from '@home/components/ai-model/check-connectivity-dialog.component';
@NgModule({
declarations:
@ -354,6 +355,7 @@ import {
CalculatedFieldDebugDialogComponent,
CalculatedFieldScriptTestDialogComponent,
CalculatedFieldTestArgumentsComponent,
CheckConnectivityDialogComponent,
],
imports: [
CommonModule,
@ -499,6 +501,7 @@ import {
CalculatedFieldDebugDialogComponent,
CalculatedFieldScriptTestDialogComponent,
CalculatedFieldTestArgumentsComponent,
CheckConnectivityDialogComponent,
],
providers: [
WidgetComponentService,

98
ui-ngx/src/app/modules/home/components/rule-node/common/time-unit-input.component.html

@ -16,72 +16,42 @@
-->
<section [formGroup]="timeInputForm" class="flex gap-4">
@if (outlined) {
<div class="tb-form-row space-between flex-1 columns-xs">
<div translate tb-hint-tooltip-icon="{{labelIconHintTooltipText}}">{{ labelText }}</div>
<div class="flex flex-row items-center justify-start gap-2">
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="time" type="number" min="0" step="1"
placeholder="{{ 'widget-config.set' | translate }}">
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="requiredText"
*ngIf="timeInputForm.get('time').hasError('required') && requiredText"
class="tb-error">
warning
</mat-icon>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="minErrorText"
*ngIf="timeInputForm.get('time').hasError('min') && minErrorText"
class="tb-error">
warning
</mat-icon>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="maxErrorText"
*ngIf="timeInputForm.get('time').hasError('max') && maxErrorText"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
<mat-form-field appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="timeUnit">
@for (timeUnit of timeUnits; track timeUnit) {
<mat-option [value]="timeUnit">{{ timeUnitTranslations.get(timeUnit) | translate }}</mat-option>
}
</mat-select>
</mat-form-field>
</div>
<mat-form-field [class]="{'number': inlineField, 'max-w-66%': !inlineField, 'flex-full': !inlineField}"
[appearance]="inlineField ? 'outline' : appearance"
[subscriptSizing]="inlineField ? 'dynamic' : subscriptSizing">
@if (labelText && !inlineField) {
<mat-label>{{ labelText }}</mat-label>
}
<input type="number" min="0" step="1" matInput formControlName="time" placeholder="{{ 'widget-config.set' | translate }}">
<div matSuffix>
<ng-content select="[matSuffix]"></ng-content>
</div>
} @else {
<mat-form-field class="max-w-66% flex-full" subscriptSizing="dynamic">
<mat-label *ngIf="labelText">{{ labelText }}</mat-label>
<input type="number" min="0" step="1" matInput formControlName="time">
<div matSuffix>
<ng-content select="[matSuffix]"></ng-content>
</div>
@if (inlineField) {
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="hasError"
*ngIf="hasError"
class="tb-error">
warning
</mat-icon>
} @else {
<mat-hint *ngIf="subscriptSizing === 'fixed'"></mat-hint>
<mat-error *ngIf="timeInputForm.get('time').hasError('required') && requiredText">
{{ requiredText }}
<mat-error *ngIf="hasError">
{{ hasError }}
</mat-error>
<mat-error *ngIf="timeInputForm.get('time').hasError('min') && minErrorText">
{{ minErrorText }}
</mat-error>
<mat-error *ngIf="timeInputForm.get('time').hasError('max') && maxErrorText">
{{ maxErrorText }}
</mat-error>
</mat-form-field>
<mat-form-field class="h-fit max-w-33% flex-full" [subscriptSizing]="subscriptSizing">
}
</mat-form-field>
<mat-form-field [class.h-fit.max-w-33%.flex-full]="!inlineField"
[appearance]="inlineField ? 'outline' : appearance"
[subscriptSizing]="inlineField ? 'dynamic' : subscriptSizing">
@if (!inlineField) {
<mat-label translate>rule-node-config.units</mat-label>
<mat-select formControlName="timeUnit">
@for (timeUnit of timeUnits; track timeUnit) {
<mat-option [value]="timeUnit">{{ timeUnitTranslations.get(timeUnit) | translate }}</mat-option>
}
</mat-select>
</mat-form-field>
}
}
<mat-select formControlName="timeUnit">
@for (timeUnit of timeUnits; track timeUnit) {
<mat-option [value]="timeUnit">{{ timeUnitTranslations.get(timeUnit) | translate }}</mat-option>
}
</mat-select>
</mat-form-field>
</section>

29
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;
}

28
ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.html

@ -96,19 +96,21 @@
<mat-panel-title translate>rule-node-config.ai.advanced-settings</mat-panel-title>
</mat-expansion-panel-header>
<div class="tb-form-panel no-border" style="margin-top: 0">
<tb-time-unit-input
required
outlined
filterTimeUnitsByMaxTime
labelText="{{ 'rule-node-config.ai.timeout' | translate }}"
labelIconHintTooltipText="{{ 'rule-node-config.ai.timeout-hint' | translate }}"
requiredText="{{ 'rule-node-config.ai.timeout-required' | translate }}"
minErrorText="{{ 'rule-node-config.ai.timeout-validation' | translate }}"
maxErrorText="{{ 'rule-node-config.ai.timeout-validation' | translate }}"
[maxTime]="600"
[minTime]="1"
formControlName="timeoutSeconds">
</tb-time-unit-input>
<div class="tb-form-row space-between flex-1 columns-xs">
<div translate tb-hint-tooltip-icon="{{'rule-node-config.ai.timeout-hint' | translate}}">{{ 'rule-node-config.ai.timeout' | translate }}</div>
<div class="flex flex-row items-center justify-start gap-2">
<tb-time-unit-input
required
inlineField
requiredText="{{ 'rule-node-config.ai.timeout-required' | translate }}"
minErrorText="{{ 'rule-node-config.ai.timeout-validation' | translate }}"
maxErrorText="{{ 'rule-node-config.ai.timeout-validation' | translate }}"
[maxTime]="600"
[minTime]="1"
formControlName="timeoutSeconds">
</tb-time-unit-input>
</div>
</div>
</div>
</mat-expansion-panel>
</div>

36
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, AIModelDialogData, AiModel>(AIModelDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],

11
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<AiModel> {
@ -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<AiModel> {

18
ui-ngx/src/app/modules/home/pages/ai-model/ai-model-table-header.component.scss

@ -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%;
}

2
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<AiModel> {

166
ui-ngx/src/app/shared/components/ai-model/ai-model-dialog.component.html

@ -56,7 +56,7 @@
</mat-select>
</mat-form-field>
<div formGroupName="providerConfig" class="tb-form-panel no-border no-padding">
@if (provider === aiProvider.GITHUB_MODELS) {
@if (AiModelMap.get(provider).providerFieldsList.includes('personalAccessToken')) {
<mat-form-field class="mat-block flex-1" appearance="outline">
<mat-label translate>ai-models.personal-access-token</mat-label>
<input type="password" required matInput formControlName="personalAccessToken">
@ -65,24 +65,29 @@
{{ 'ai-models.personal-access-token-required' | translate }}
</mat-error>
</mat-form-field>
} @else if (provider === aiProvider.GOOGLE_VERTEX_AI_GEMINI) {
<mat-form-field class="mat-block flex-1" appearance="outline">
}
@if (AiModelMap.get(provider).providerFieldsList.includes('projectId')) {
<mat-form-field class="mat-block flex-1" appearance="outline" subscriptSizing="dynamic">
<mat-label translate>ai-models.project-id</mat-label>
<input matInput required formControlName="projectId">
<mat-error *ngIf="aiModelForms.get('configuration').get('providerConfig').get('projectId').hasError('required')">
{{ 'ai-models.project-id-required' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block flex-1" appearance="outline">
}
@if (AiModelMap.get(provider).providerFieldsList.includes('location')) {
<mat-form-field class="mat-block flex-1" appearance="outline" subscriptSizing="dynamic">
<mat-label translate>ai-models.location</mat-label>
<input matInput required formControlName="location">
<mat-error *ngIf="aiModelForms.get('configuration').get('providerConfig').get('location').hasError('required')">
{{ 'ai-models.location-required' | translate }}
</mat-error>
</mat-form-field>
}
@if (AiModelMap.get(provider).providerFieldsList.includes('serviceAccountKey')) {
<tb-file-input formControlName="serviceAccountKey"
[existingFileName]="aiModelForms.get('configuration').get('providerConfig').get('serviceAccountKeyFileName').value"
(fileNameChanged)="aiModelForms.get('configuration').get('providerConfig').get('serviceAccountKeyFileName').setValue($event)"
[existingFileName]="aiModelForms.get('configuration').get('providerConfig').get('fileName').value"
(fileNameChanged)="aiModelForms.get('configuration').get('providerConfig').get('fileName').setValue($event)"
required
requiredAsError
accept=".json,application/json"
@ -91,20 +96,23 @@
noFileText="ai-models.no-file"
dropLabel="{{'ai-models.drop-file' | translate}}">
</tb-file-input>
} @else {
@if (provider === aiProvider.AZURE_OPENAI) {
<mat-form-field class="mat-block flex-1" appearance="outline" subscriptSizing="dynamic">
<mat-label translate>ai-models.endpoint</mat-label>
<input required matInput formControlName="endpoint">
<mat-error *ngIf="aiModelForms.get('configuration').get('providerConfig').get('endpoint').hasError('required')">
{{ 'ai-models.endpoint-required' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block flex-1" appearance="outline" subscriptSizing="dynamic">
<mat-label translate>ai-models.service-version</mat-label>
<input matInput formControlName="serviceVersion">
</mat-form-field>
}
}
@if (AiModelMap.get(provider).providerFieldsList.includes('endpoint')) {
<mat-form-field class="mat-block flex-1" appearance="outline" subscriptSizing="dynamic">
<mat-label translate>ai-models.endpoint</mat-label>
<input required matInput formControlName="endpoint">
<mat-error *ngIf="aiModelForms.get('configuration').get('providerConfig').get('endpoint').hasError('required')">
{{ 'ai-models.endpoint-required' | translate }}
</mat-error>
</mat-form-field>
}
@if (AiModelMap.get(provider).providerFieldsList.includes('serviceVersion')) {
<mat-form-field class="mat-block flex-1" appearance="outline" subscriptSizing="dynamic">
<mat-label translate>ai-models.service-version</mat-label>
<input matInput formControlName="serviceVersion">
</mat-form-field>
}
@if (AiModelMap.get(provider).providerFieldsList.includes('apiKey')) {
<mat-form-field class="mat-block flex-1" appearance="outline">
<mat-label translate>ai-models.api-key</mat-label>
<input type="password" required matInput formControlName="apiKey">
@ -128,43 +136,46 @@
[errorText]="(provider === aiProvider.AZURE_OPENAI ? 'ai-models.deployment-name-required': 'ai-models.model-id-required') | translate">
</tb-models-list-autocomplete>
</section>
<div class="tb-form-row space-between">
<div tb-hint-tooltip-icon="{{ 'ai-models.temperature-hint' | translate }}">
{{ 'ai-models.temperature' | translate }}
</div>
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="temperature"
type="number" min="0" step="0.1" placeholder="{{ 'ai-models.set' | translate }}">
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'ai-models.temperature-min' | translate"
*ngIf="aiModelForms.get('configuration').get('temperature').hasError('min')"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
<div class="tb-form-row space-between">
<div tb-hint-tooltip-icon="{{ 'ai-models.top-p-hint' | translate }}">
{{ 'ai-models.top-p' | translate }}
@if (AiModelMap.get(provider).modelFieldsList.includes('temperature')) {
<div class="tb-form-row space-between">
<div tb-hint-tooltip-icon="{{ 'ai-models.temperature-hint' | translate }}">
{{ 'ai-models.temperature' | translate }}
</div>
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="temperature"
type="number" min="0" step="0.1" placeholder="{{ 'ai-models.set' | translate }}">
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'ai-models.temperature-min' | translate"
*ngIf="aiModelForms.get('configuration').get('temperature').hasError('min')"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="topP"
type="number" min="0" step="0.1" max="1" placeholder="{{ 'ai-models.set' | translate }}">
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'ai-models.top-p-min-max' | translate"
*ngIf="aiModelForms.get('configuration').get('topP').hasError('min') ||
}
@if (AiModelMap.get(provider).modelFieldsList.includes('topP')) {
<div class="tb-form-row space-between">
<div tb-hint-tooltip-icon="{{ 'ai-models.top-p-hint' | translate }}">
{{ 'ai-models.top-p' | translate }}
</div>
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="topP"
type="number" min="0" step="0.1" max="1" placeholder="{{ 'ai-models.set' | translate }}">
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'ai-models.top-p-min-max' | translate"
*ngIf="aiModelForms.get('configuration').get('topP').hasError('min') ||
aiModelForms.get('configuration').get('topP').hasError('max')"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
@if (provider === aiProvider.GOOGLE_AI_GEMINI || provider === aiProvider.GOOGLE_VERTEX_AI_GEMINI || provider === aiProvider.ANTHROPIC) {
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
}
@if (AiModelMap.get(provider).modelFieldsList.includes('topK')) {
<div class="tb-form-row space-between">
<div tb-hint-tooltip-icon="{{ 'ai-models.top-k-hint' | translate }}">
{{ 'ai-models.top-k' | translate }}
@ -183,45 +194,48 @@
</mat-form-field>
</div>
}
@if (provider !== aiProvider.ANTHROPIC && provider !== aiProvider.AMAZON_BEDROCK) {
@if (AiModelMap.get(provider).modelFieldsList.includes('presencePenalty')) {
<div class="tb-form-row space-between">
<div tb-hint-tooltip-icon="{{ 'ai-models.presence-penalty-hint' | translate }}">
{{ 'ai-models.presence-penalty' | translate }}
</div>
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="frequencyPenalty"
<input matInput formControlName="presencePenalty"
type="number" step="1" placeholder="{{ 'ai-models.set' | translate }}">
</mat-form-field>
</div>
}
@if (AiModelMap.get(provider).modelFieldsList.includes('frequencyPenalty')) {
<div class="tb-form-row space-between">
<div tb-hint-tooltip-icon="{{ 'ai-models.frequency-penalty-hint' | translate }}">
{{ 'ai-models.frequency-penalty' | translate }}
</div>
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="presencePenalty"
<input matInput formControlName="frequencyPenalty"
type="number" step="1" placeholder="{{ 'ai-models.set' | translate }}">
</mat-form-field>
</div>
}
<div class="tb-form-row space-between">
<div tb-hint-tooltip-icon="{{ 'ai-models.max-output-token-hint' | translate }}">
{{ 'ai-models.max-output-token' | translate }}
@if (AiModelMap.get(provider).modelFieldsList.includes('maxOutputTokens')) {
<div class="tb-form-row space-between">
<div tb-hint-tooltip-icon="{{ 'ai-models.max-output-token-hint' | translate }}">
{{ 'ai-models.max-output-token' | translate }}
</div>
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="maxOutputTokens"
type="number" min="1" step="1" placeholder="{{ 'ai-models.set' | translate }}">
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'ai-models.max-output-token-min' | translate"
*ngIf="aiModelForms.get('configuration').get('maxOutputTokens').hasError('min')"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="maxOutputTokens"
type="number" min="1" step="1" placeholder="{{ 'ai-models.set' | translate }}">
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'ai-models.max-output-token-min' | translate"
*ngIf="aiModelForms.get('configuration').get('maxOutputTokens').hasError('min')"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
}
</section>
</section>
</div>

54
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<AIModelDialogCompone
providerMap: AiProvider[] = Object.keys(AiProvider) as AiProvider[];
providerTranslationMap = AiProviderTranslations;
aiProviderWithApiKey: AiProvider[] = AiProviderWithApiKey;
AiModelMap = AiModelMap;
provider: AiProvider = AiProvider.OPENAI;
@ -78,7 +85,7 @@ export class AIModelDialogComponent extends DialogComponent<AIModelDialogCompone
this.aiModelForms = this.fb.group({
name: [this.data.AIModel ? this.data.AIModel.name : '', [Validators.required, Validators.maxLength(255)]],
modelType: ['CHAT'],
modelType: [ModelType.CHAT],
configuration: this.fb.group({
provider: [this.provider, [Validators.required]],
providerConfig: this.fb.group({
@ -89,11 +96,11 @@ export class AIModelDialogComponent extends DialogComponent<AIModelDialogCompone
projectId: [this.data.AIModel ? this.data.AIModel.configuration.providerConfig?.projectId : '', [Validators.required]],
location: [this.data.AIModel ? this.data.AIModel.configuration.providerConfig?.location : '', [Validators.required]],
serviceAccountKey: [this.data.AIModel ? this.data.AIModel.configuration.providerConfig?.serviceAccountKey : '', [Validators.required]],
serviceAccountKeyFileName: [this.data.AIModel ? this.data.AIModel.configuration.providerConfig?.serviceAccountKeyFileName : '', [Validators.required]],
fileName: [this.data.AIModel ? this.data.AIModel.configuration.providerConfig?.fileName : '', [Validators.required]],
}),
modelId: [this.data.AIModel ? this.data.AIModel.configuration?.modelId : '', [Validators.required]],
temperature: [this.data.AIModel ? this.data.AIModel.configuration?.temperature : null, [Validators.min(0)]],
topP: this.data.AIModel ? this.data.AIModel.configuration?.topP : [null, [Validators.min(0.1), Validators.max(1)]],
topP: [this.data.AIModel ? this.data.AIModel.configuration?.topP : null, [Validators.min(0.1), Validators.max(1)]],
topK: [this.data.AIModel ? this.data.AIModel.configuration?.topK : null, [Validators.min(0)]],
frequencyPenalty: [this.data.AIModel ? this.data.AIModel.configuration?.frequencyPenalty : null],
presencePenalty: [this.data.AIModel ? this.data.AIModel.configuration?.presencePenalty : null],
@ -105,7 +112,7 @@ export class AIModelDialogComponent extends DialogComponent<AIModelDialogCompone
takeUntilDestroyed()
).subscribe((provider: AiProvider) => {
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<AIModelDialogCompone
}
private updateValidation(provider: AiProvider) {
const providerConfig = this.aiModelForms.get('configuration.providerConfig');
if (this.aiProviderWithApiKey.includes(provider)) {
providerConfig.get('apiKey').enable();
} else {
providerConfig.get('apiKey').disable();
}
if (provider === AiProvider.GITHUB_MODELS) {
providerConfig.get('personalAccessToken').enable();
} else {
providerConfig.get('personalAccessToken').disable();
}
if (provider === AiProvider.GOOGLE_VERTEX_AI_GEMINI) {
providerConfig.get('projectId').enable();
providerConfig.get('location').enable();
providerConfig.get('serviceAccountKey').enable();
providerConfig.get('serviceAccountKeyFileName').enable();
} else {
providerConfig.get('projectId').disable();
providerConfig.get('location').disable();
providerConfig.get('serviceAccountKey').disable();
providerConfig.get('serviceAccountKeyFileName').disable();
}
if (provider === AiProvider.AZURE_OPENAI) {
providerConfig.get('endpoint').enable();
providerConfig.get('serviceVersion').enable();
} else {
providerConfig.get('endpoint').disable();
providerConfig.get('serviceVersion').disable();
}
ProviderFieldsAllList.forEach(key =>
this.aiModelForms.get('configuration.providerConfig')
.get(key)[AiModelMap.get(provider).providerFieldsList.includes(key) ? 'enable' : 'disable']()
)
}
cancel(): void {

4
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(() => {

201
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<BaseData<AiModelId>, 'label'>, HasTenantId, ExportableEntity<AiModelId> {
modelType: string;
modelType: ModelType;
configuration: {
provider: AiProvider
providerConfig: {
@ -30,10 +30,10 @@ export interface AiModel extends Omit<BaseData<AiModelId>, '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<BaseData<AiModelId>, '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, string>(
[
[AiProvider.OPENAI , 'ai-models.ai-providers.openai'],
@ -75,55 +70,130 @@ export const AiProviderTranslations = new Map<AiProvider, string>(
]
);
export const AiModelMap = new Map<AiProvider, string[]>(
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, { modelList: string[], providerFieldsList: string[], modelFieldsList: string[] }>([
[
[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;

3
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 { }

Loading…
Cancel
Save