From 64f0da33655ae8d7569b140649bfafff92b9530a Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Thu, 10 Jul 2025 13:32:39 +0300 Subject: [PATCH] UI: ref after review --- ui-ngx/src/app/core/services/menu.models.ts | 2 +- ui-ngx/src/app/modules/common/modules-map.ts | 8 +- .../ai-model/ai-model-dialog.component.html | 41 ++--- .../ai-model/ai-model-dialog.component.scss | 0 .../ai-model/ai-model-dialog.component.ts | 30 +++- .../check-connectivity-dialog.component.html | 9 +- .../check-connectivity-dialog.component.scss | 7 +- .../check-connectivity-dialog.component.ts | 6 +- .../home/components/home-components.module.ts | 3 + .../external/ai-config.component.html | 5 +- .../rule-node/external/ai-config.component.ts | 4 +- .../ai-model/ai-model-table-config.resolve.ts | 9 +- .../models-list-autocomplete.component.html | 41 ----- .../models-list-autocomplete.component.ts | 161 ------------------ .../json-object-edit.component.html | 7 + .../components/json-object-edit.component.ts | 2 + .../string-autocomplete.component.html | 2 +- .../string-autocomplete.component.ts | 3 + ui-ngx/src/app/shared/shared.module.ts | 6 - .../assets/locale/locale.constant-en_US.json | 1 + 20 files changed, 88 insertions(+), 259 deletions(-) rename ui-ngx/src/app/{shared => modules/home}/components/ai-model/ai-model-dialog.component.html (88%) rename ui-ngx/src/app/{shared => modules/home}/components/ai-model/ai-model-dialog.component.scss (100%) rename ui-ngx/src/app/{shared => modules/home}/components/ai-model/ai-model-dialog.component.ts (86%) delete mode 100644 ui-ngx/src/app/shared/components/ai-model/models-list-autocomplete.component.html delete mode 100644 ui-ngx/src/app/shared/components/ai-model/models-list-autocomplete.component.ts diff --git a/ui-ngx/src/app/core/services/menu.models.ts b/ui-ngx/src/app/core/services/menu.models.ts index dc568c148e..b660b0ab0d 100644 --- a/ui-ngx/src/app/core/services/menu.models.ts +++ b/ui-ngx/src/app/core/services/menu.models.ts @@ -794,6 +794,7 @@ const defaultUserMenuMap = new Map([ {id: MenuId.home}, {id: MenuId.alarms}, {id: MenuId.dashboards}, + {id: MenuId.ai_models}, { id: MenuId.entities, pages: [ @@ -852,7 +853,6 @@ const defaultUserMenuMap = new Map([ {id: MenuId.notification_rules} ] }, - {id: MenuId.ai_models}, { id: MenuId.mobile_center, pages: [ diff --git a/ui-ngx/src/app/modules/common/modules-map.ts b/ui-ngx/src/app/modules/common/modules-map.ts index 60a26e4211..0890b9e623 100644 --- a/ui-ngx/src/app/modules/common/modules-map.ts +++ b/ui-ngx/src/app/modules/common/modules-map.ts @@ -336,8 +336,7 @@ import * as DatapointsLimitComponent from '@shared/components/time/datapoints-li import * as AggregationTypeSelectComponent from '@shared/components/time/aggregation/aggregation-type-select.component'; 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 ModelsListAutocompleteComponent from '@shared/components/ai-model/models-list-autocomplete.component'; +import * as AIModelDialogComponent from '@home/components/ai-model/ai-model-dialog.component'; import { IModulesMap } from '@modules/common/modules-map.models'; import { Observable, of } from 'rxjs'; @@ -534,8 +533,6 @@ class ModulesMap implements IModulesMap { '@shared/components/image/gallery-image-input.component': GalleryImageInputComponent, '@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/models-list-autocomplete.component': ModelsListAutocompleteComponent, '@home/components/alarm/alarm-filter-config.component': AlarmFilterConfigComponent, @@ -672,7 +669,8 @@ class ModulesMap implements IModulesMap { '@home/components/dashboard-page/dashboard-image-dialog.component': DashboardImageDialogComponent, '@home/components/widget/widget-container.component': WidgetContainerComponent, '@home/components/profile/queue/tenant-profile-queues.component': TenantProfileQueuesComponent, - '@home/components/queue/queue-form.component': QueueFormComponent + '@home/components/queue/queue-form.component': QueueFormComponent, + '@home/components/ai-model/ai-model-dialog.component': AIModelDialogComponent, }; init(): Observable { diff --git a/ui-ngx/src/app/shared/components/ai-model/ai-model-dialog.component.html b/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.html similarity index 88% rename from ui-ngx/src/app/shared/components/ai-model/ai-model-dialog.component.html rename to ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.html index 2a5a34a2c9..4201c5a418 100644 --- a/ui-ngx/src/app/shared/components/ai-model/ai-model-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.html @@ -56,7 +56,7 @@
- @if (AiModelMap.get(provider).providerFieldsList.includes('personalAccessToken')) { + @if (providerFieldsList.includes('personalAccessToken')) { ai-models.personal-access-token @@ -66,7 +66,7 @@ } - @if (AiModelMap.get(provider).providerFieldsList.includes('projectId')) { + @if (providerFieldsList.includes('projectId')) { ai-models.project-id @@ -75,7 +75,7 @@ } - @if (AiModelMap.get(provider).providerFieldsList.includes('location')) { + @if (providerFieldsList.includes('location')) { ai-models.location @@ -84,7 +84,7 @@ } - @if (AiModelMap.get(provider).providerFieldsList.includes('serviceAccountKey')) { + @if (providerFieldsList.includes('serviceAccountKey')) { } - @if (AiModelMap.get(provider).providerFieldsList.includes('endpoint')) { + @if (providerFieldsList.includes('endpoint')) { ai-models.endpoint @@ -106,13 +106,13 @@ } - @if (AiModelMap.get(provider).providerFieldsList.includes('serviceVersion')) { + @if (providerFieldsList.includes('serviceVersion')) { ai-models.service-version } - @if (AiModelMap.get(provider).providerFieldsList.includes('apiKey')) { + @if (providerFieldsList.includes('apiKey')) { ai-models.api-key @@ -129,14 +129,17 @@
ai-models.configuration
- - + +
- @if (AiModelMap.get(provider).modelFieldsList.includes('temperature')) { + @if (modelFieldsList.includes('temperature')) {
{{ 'ai-models.temperature' | translate }} @@ -155,7 +158,7 @@
} - @if (AiModelMap.get(provider).modelFieldsList.includes('topP')) { + @if (modelFieldsList.includes('topP')) {
{{ 'ai-models.top-p' | translate }} @@ -175,7 +178,7 @@
} - @if (AiModelMap.get(provider).modelFieldsList.includes('topK')) { + @if (modelFieldsList.includes('topK')) {
{{ 'ai-models.top-k' | translate }} @@ -194,7 +197,7 @@
} - @if (AiModelMap.get(provider).modelFieldsList.includes('presencePenalty')) { + @if (modelFieldsList.includes('presencePenalty')) {
{{ 'ai-models.presence-penalty' | translate }} @@ -206,7 +209,7 @@
} - @if (AiModelMap.get(provider).modelFieldsList.includes('frequencyPenalty')) { + @if (modelFieldsList.includes('frequencyPenalty')) {
{{ 'ai-models.frequency-penalty' | translate }} @@ -217,7 +220,7 @@
} - @if (AiModelMap.get(provider).modelFieldsList.includes('maxOutputTokens')) { + @if (modelFieldsList.includes('maxOutputTokens')) {
{{ 'ai-models.max-output-token' | translate }} diff --git a/ui-ngx/src/app/shared/components/ai-model/ai-model-dialog.component.scss b/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.scss similarity index 100% rename from ui-ngx/src/app/shared/components/ai-model/ai-model-dialog.component.scss rename to ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.scss diff --git a/ui-ngx/src/app/shared/components/ai-model/ai-model-dialog.component.ts b/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.ts similarity index 86% rename from ui-ngx/src/app/shared/components/ai-model/ai-model-dialog.component.ts rename to ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.ts index d9ae3eaa69..a5fca8d122 100644 --- a/ui-ngx/src/app/shared/components/ai-model/ai-model-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/ai-model/ai-model-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, MatDialog, MatDialogRef } from '@angular/material/dialog'; -import { Observable } from 'rxjs'; +import { Observable, of } from 'rxjs'; import { StepperOrientation } from '@angular/cdk/stepper'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { EntityType } from '@shared/models/entity-type.models'; @@ -35,6 +35,7 @@ import { } from '@shared/models/ai-model.models'; import { AiModelService } from '@core/http/ai-model.service'; import { CheckConnectivityDialogComponent } from '@home/components/ai-model/check-connectivity-dialog.component'; +import { map } from 'rxjs/operators'; export interface AIModelDialogData { AIModel?: AiModel; @@ -112,7 +113,7 @@ export class AIModelDialogComponent extends DialogComponent { this.provider = provider; - this.aiModelForms.get('configuration.modelId').reset({}); + this.aiModelForms.get('configuration.modelId').reset(''); this.aiModelForms.get('configuration.providerConfig').reset({}); this.updateValidation(provider); }) @@ -120,11 +121,28 @@ export class AIModelDialogComponent extends DialogComponent> { + const search = searchText ? searchText?.toLowerCase() : ''; + return of(this.provider ? AiModelMap.get(this.provider).modelList || [] : []).pipe( + map(name => name?.filter(option => option.toLowerCase().includes(search))), + ); + } + private updateValidation(provider: AiProvider) { - ProviderFieldsAllList.forEach(key => - this.aiModelForms.get('configuration.providerConfig') - .get(key)[AiModelMap.get(provider).providerFieldsList.includes(key) ? 'enable' : 'disable']() - ) + ProviderFieldsAllList.forEach(key => { + if (AiModelMap.get(provider).providerFieldsList.includes(key)) { + this.aiModelForms.get('configuration.providerConfig').get(key).enable(); + } else { + this.aiModelForms.get('configuration.providerConfig').get(key).disable(); + } + }) + } + + get providerFieldsList(): string[] { + return AiModelMap.get(this.provider).providerFieldsList; + } + get modelFieldsList(): string[] { + return AiModelMap.get(this.provider).modelFieldsList; } cancel(): void { diff --git a/ui-ngx/src/app/modules/home/components/ai-model/check-connectivity-dialog.component.html b/ui-ngx/src/app/modules/home/components/ai-model/check-connectivity-dialog.component.html index 9ead874275..afc881b10b 100644 --- a/ui-ngx/src/app/modules/home/components/ai-model/check-connectivity-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/ai-model/check-connectivity-dialog.component.html @@ -15,15 +15,15 @@ limitations under the License. --> -
-

ai-models.check-connectivity

+ +

ai-models.check-connectivity

-
+
-
- +
@@ -84,6 +84,7 @@ @if (aiConfigForm.get('responseFormat.type').value === responseFormat.JSON_SCHEMA) { 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 3b7f0a92d6..3c9d8bf919 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 @@ -19,7 +19,7 @@ import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms import { RuleNodeConfiguration, RuleNodeConfigurationComponent } from '@shared/models/rule-node.models'; 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 { AIModelDialogComponent, AIModelDialogData } from '@home/components/ai-model/ai-model-dialog.component'; import { AiModel, AiRuleNodeResponseFormatTypeOnlyText, ResponseFormat } from '@shared/models/ai-model.models'; import { deepTrim } from '@core/utils'; @@ -47,7 +47,7 @@ export class AiConfigComponent extends RuleNodeConfigurationComponent { protected onConfigurationSet(configuration: RuleNodeConfiguration) { this.aiConfigForm = this.fb.group({ - modelSettingsId: [configuration?.modelSettingsId ?? null, [Validators.required]], + modelId: [configuration?.modelId ?? 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({ 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 3f3ded2abb..03db209ada 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 @@ -31,7 +31,7 @@ import { Observable } from 'rxjs'; import { AiModel, AiProviderTranslations } from '@shared/models/ai-model.models'; import { AiModelService } from '@core/http/ai-model.service'; import { AiModelTableHeaderComponent } from '@home/pages/ai-model/ai-model-table-header.component'; -import { AIModelDialogComponent, AIModelDialogData } from '@shared/components/ai-model/ai-model-dialog.component'; +import { AIModelDialogComponent, AIModelDialogData } from '@home/components/ai-model/ai-model-dialog.component'; import { map } from 'rxjs/operators'; @Injectable() @@ -81,7 +81,7 @@ export class AiModelsTableConfigResolver { this.config.cellActionDescriptors = this.configureCellActions(); this.config.handleRowClick = ($event, model) => { - this.editModel(model); + this.editModel($event, model); return true; }; } @@ -96,12 +96,13 @@ export class AiModelsTableConfigResolver { name: this.translate.instant('action.edit'), icon: 'edit', isEnabled: () => true, - onAction: ($event, entity) => this.editModel(entity) + onAction: ($event, entity) => this.editModel($event, entity) } ]; } - private editModel(AIModel: AiModel): void { + private editModel($event, AIModel: AiModel): void { + $event?.stopPropagation(); this.addModel(AIModel, false).subscribe(); } diff --git a/ui-ngx/src/app/shared/components/ai-model/models-list-autocomplete.component.html b/ui-ngx/src/app/shared/components/ai-model/models-list-autocomplete.component.html deleted file mode 100644 index d204013805..0000000000 --- a/ui-ngx/src/app/shared/components/ai-model/models-list-autocomplete.component.html +++ /dev/null @@ -1,41 +0,0 @@ - - - {{label}} - - - - {{errorText}} - - - - - - - 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 deleted file mode 100644 index 3e3c0cbb66..0000000000 --- a/ui-ngx/src/app/shared/components/ai-model/models-list-autocomplete.component.ts +++ /dev/null @@ -1,161 +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. -/// - -import { Component, ElementRef, forwardRef, Input, OnChanges, OnInit, SimpleChanges, ViewChild } from '@angular/core'; -import { ControlValueAccessor, FormBuilder, FormControl, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; -import { Observable } from 'rxjs'; -import { map, startWith, tap } from 'rxjs/operators'; -import { TranslateService } from '@ngx-translate/core'; -import { coerceBoolean } from '@shared/decorators/coercion'; -import { MatFormFieldAppearance, SubscriptSizing } from '@angular/material/form-field'; -import { AiModelMap, AiProvider } from '@shared/models/ai-model.models'; - -@Component({ - selector: 'tb-models-list-autocomplete', - templateUrl: './models-list-autocomplete.component.html', - styleUrls: [], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => ModelsListAutocompleteComponent), - multi: true - } - ] -}) -export class ModelsListAutocompleteComponent implements ControlValueAccessor, OnInit, OnChanges { - - @ViewChild('nameInput', {static: true}) nameInput: ElementRef; - - @Input() - disabled: boolean; - - @Input() - @coerceBoolean() - required = false; - - @Input() - provider: AiProvider; - - @Input() - placeholderText: string = this.translate.instant('widget-config.set'); - - @Input() - subscriptSizing: SubscriptSizing = 'dynamic'; - - @Input() - appearance: MatFormFieldAppearance = 'outline'; - - @Input() - label: string; - - @Input() - errorText: string; - - selectionFormControl: FormControl; - modelValue: string | null; - - filteredOptions$: Observable>; - - searchText = ''; - - private dirty = false; - - private propagateChange = (_val: any) => {}; - - constructor(private fb: FormBuilder, - private translate: TranslateService) { - } - - ngOnInit() { - this.selectionFormControl = this.fb.control('', this.required ? [Validators.required] : []); - this.setupFilteredOptions(); - } - - ngOnChanges(changes: SimpleChanges) { - if (changes.provider && !changes.provider.isFirstChange()) { - this.setupFilteredOptions(); - this.selectionFormControl.setValue(null, {emitEvent: false}); - this.modelValue = null; - this.propagateChange(null); - } - } - - private setupFilteredOptions() { - this.filteredOptions$ = this.selectionFormControl.valueChanges.pipe( - startWith(''), - tap(value => this.updateView(value)), - map(value => { - const search = value ? value.toLowerCase() : ''; - const options = this.provider ? AiModelMap.get(this.provider).modelList || [] : []; - return search ? options.filter(option => option.toLowerCase().includes(search)) : options; - }) - ); - } - - writeValue(option?: string): void { - this.searchText = ''; - this.modelValue = option ? option : null; - - if (option) { - this.selectionFormControl.patchValue(option, { emitEvent: false }); - this.dirty = true; - } else { - this.selectionFormControl.patchValue(null, { emitEvent: false }); - this.dirty = true; - } - } - - onFocus() { - if (this.dirty) { - this.selectionFormControl.updateValueAndValidity({onlySelf: true, emitEvent: true}); - this.dirty = false; - } - } - - updateView(value: string) { - this.searchText = value ? value : ''; - if (this.modelValue !== value && value) { - this.modelValue = value; - this.propagateChange(this.modelValue); - } - } - - registerOnChange(fn: any): void { - this.propagateChange = fn; - } - - registerOnTouched(fn: any): void { - } - - setDisabledState(isDisabled: boolean): void { - this.disabled = isDisabled; - if (this.disabled) { - this.selectionFormControl.disable({emitEvent: false}); - } else { - this.selectionFormControl.enable({emitEvent: false}); - } - } - - clear() { - this.selectionFormControl.patchValue(null, {emitEvent: true}); - this.propagateChange(null); - this.modelValue = null; - setTimeout(() => { - this.nameInput.nativeElement.blur(); - this.nameInput.nativeElement.focus(); - }, 0); - } -} diff --git a/ui-ngx/src/app/shared/components/json-object-edit.component.html b/ui-ngx/src/app/shared/components/json-object-edit.component.html index 70967d3483..608efd8ad4 100644 --- a/ui-ngx/src/app/shared/components/json-object-edit.component.html +++ b/ui-ngx/src/app/shared/components/json-object-edit.component.html @@ -32,6 +32,13 @@ mat-button *ngIf="!readonly && !disabled" class="tidy" (click)="minifyJSON()"> {{'js-func.mini' | translate }} + @if (iconHint) { + + }