Browse Source

UI: ref after review

pull/13371/head
Artem Dzhereleiko 12 months ago
parent
commit
64f0da3365
  1. 2
      ui-ngx/src/app/core/services/menu.models.ts
  2. 8
      ui-ngx/src/app/modules/common/modules-map.ts
  3. 41
      ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.html
  4. 0
      ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.scss
  5. 30
      ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.ts
  6. 9
      ui-ngx/src/app/modules/home/components/ai-model/check-connectivity-dialog.component.html
  7. 7
      ui-ngx/src/app/modules/home/components/ai-model/check-connectivity-dialog.component.scss
  8. 6
      ui-ngx/src/app/modules/home/components/ai-model/check-connectivity-dialog.component.ts
  9. 3
      ui-ngx/src/app/modules/home/components/home-components.module.ts
  10. 5
      ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.html
  11. 4
      ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.ts
  12. 9
      ui-ngx/src/app/modules/home/pages/ai-model/ai-model-table-config.resolve.ts
  13. 41
      ui-ngx/src/app/shared/components/ai-model/models-list-autocomplete.component.html
  14. 161
      ui-ngx/src/app/shared/components/ai-model/models-list-autocomplete.component.ts
  15. 7
      ui-ngx/src/app/shared/components/json-object-edit.component.html
  16. 2
      ui-ngx/src/app/shared/components/json-object-edit.component.ts
  17. 2
      ui-ngx/src/app/shared/components/string-autocomplete.component.html
  18. 3
      ui-ngx/src/app/shared/components/string-autocomplete.component.ts
  19. 6
      ui-ngx/src/app/shared/shared.module.ts
  20. 1
      ui-ngx/src/assets/locale/locale.constant-en_US.json

2
ui-ngx/src/app/core/services/menu.models.ts

@ -794,6 +794,7 @@ const defaultUserMenuMap = new Map<Authority, MenuReference[]>([
{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<Authority, MenuReference[]>([
{id: MenuId.notification_rules}
]
},
{id: MenuId.ai_models},
{
id: MenuId.mobile_center,
pages: [

8
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<any> {

41
ui-ngx/src/app/shared/components/ai-model/ai-model-dialog.component.html → ui-ngx/src/app/modules/home/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 (AiModelMap.get(provider).providerFieldsList.includes('personalAccessToken')) {
@if (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">
@ -66,7 +66,7 @@
</mat-error>
</mat-form-field>
}
@if (AiModelMap.get(provider).providerFieldsList.includes('projectId')) {
@if (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">
@ -75,7 +75,7 @@
</mat-error>
</mat-form-field>
}
@if (AiModelMap.get(provider).providerFieldsList.includes('location')) {
@if (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">
@ -84,7 +84,7 @@
</mat-error>
</mat-form-field>
}
@if (AiModelMap.get(provider).providerFieldsList.includes('serviceAccountKey')) {
@if (providerFieldsList.includes('serviceAccountKey')) {
<tb-file-input formControlName="serviceAccountKey"
[existingFileName]="aiModelForms.get('configuration').get('providerConfig').get('fileName').value"
(fileNameChanged)="aiModelForms.get('configuration').get('providerConfig').get('fileName').setValue($event)"
@ -97,7 +97,7 @@
dropLabel="{{'ai-models.drop-file' | translate}}">
</tb-file-input>
}
@if (AiModelMap.get(provider).providerFieldsList.includes('endpoint')) {
@if (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">
@ -106,13 +106,13 @@
</mat-error>
</mat-form-field>
}
@if (AiModelMap.get(provider).providerFieldsList.includes('serviceVersion')) {
@if (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')) {
@if (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">
@ -129,14 +129,17 @@
<div class="tb-form-panel-title" translate>ai-models.configuration</div>
<section class="tb-form-panel no-border no-padding">
<section class="tb-form-panel outlined no-border no-padding">
<tb-models-list-autocomplete formControlName="modelId"
[provider]="provider"
required
[label]="(provider === aiProvider.AZURE_OPENAI ? 'ai-models.deployment-name': 'ai-models.model-id') | translate"
[errorText]="(provider === aiProvider.AZURE_OPENAI ? 'ai-models.deployment-name-required': 'ai-models.model-id-required') | translate">
</tb-models-list-autocomplete>
<tb-string-autocomplete [fetchOptionsFn]="fetchOptions.bind(this)"
additionalClass="tb-suffix-show-on-hover"
appearance="outline"
panelWidth=""
required
[label]="(provider === aiProvider.AZURE_OPENAI ? 'ai-models.deployment-name': 'ai-models.model-id') | translate"
[errorText]="(provider === aiProvider.AZURE_OPENAI ? 'ai-models.deployment-name-required': 'ai-models.model-id-required') | translate"
formControlName="modelId">
</tb-string-autocomplete>
</section>
@if (AiModelMap.get(provider).modelFieldsList.includes('temperature')) {
@if (modelFieldsList.includes('temperature')) {
<div class="tb-form-row space-between">
<div tb-hint-tooltip-icon="{{ 'ai-models.temperature-hint' | translate }}">
{{ 'ai-models.temperature' | translate }}
@ -155,7 +158,7 @@
</mat-form-field>
</div>
}
@if (AiModelMap.get(provider).modelFieldsList.includes('topP')) {
@if (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 }}
@ -175,7 +178,7 @@
</mat-form-field>
</div>
}
@if (AiModelMap.get(provider).modelFieldsList.includes('topK')) {
@if (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 }}
@ -194,7 +197,7 @@
</mat-form-field>
</div>
}
@if (AiModelMap.get(provider).modelFieldsList.includes('presencePenalty')) {
@if (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 }}
@ -206,7 +209,7 @@
</div>
}
@if (AiModelMap.get(provider).modelFieldsList.includes('frequencyPenalty')) {
@if (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 }}
@ -217,7 +220,7 @@
</mat-form-field>
</div>
}
@if (AiModelMap.get(provider).modelFieldsList.includes('maxOutputTokens')) {
@if (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 }}

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

30
ui-ngx/src/app/shared/components/ai-model/ai-model-dialog.component.ts → 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<AIModelDialogCompone
takeUntilDestroyed()
).subscribe((provider: AiProvider) => {
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<AIModelDialogCompone
this.updateValidation(this.provider);
}
fetchOptions(searchText: string): Observable<Array<string>> {
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 {

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

@ -15,15 +15,15 @@
limitations under the License.
-->
<div class="flex h-6 flex-row">
<h2 class="connectivity-title" translate>ai-models.check-connectivity</h2>
<mat-toolbar class="transparent">
<h2 translate>ai-models.check-connectivity</h2>
<span class="flex-1"></span>
<button mat-icon-button
(click)="cancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</div>
</mat-toolbar>
<div mat-dialog-content>
<div class="flex h-full flex-1 flex-col items-center justify-center">
<mat-progress-spinner color="warn" mode="indeterminate"
@ -47,8 +47,7 @@
</div>
</div>
</div>
<div mat-dialog-actions>
<span class="flex-1"></span>
<div mat-dialog-actions class="justify-end">
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"

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

@ -21,11 +21,8 @@
max-height: 100vh;
display: grid;
.connectivity-title {
font-size: 18px;
font-weight: 500;
margin: 0;
padding-left: 16px;
.transparent {
background-color: transparent;
}
.connection-status {

6
ui-ngx/src/app/modules/home/components/ai-model/check-connectivity-dialog.component.ts

@ -71,7 +71,11 @@ export class CheckConnectivityDialogComponent extends DialogComponent<CheckConne
if (result.status === 'SUCCESS') {
this.showCheckSuccess = true;
} else {
this.checkErrMsg = JSON.parse(result.errorDetails);
try {
this.checkErrMsg = JSON.parse(result.errorDetails);
} catch (e) {
this.checkErrMsg = result.errorDetails;
}
}
},
error: err => this.checkErrMsg = err.error.message

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

@ -204,6 +204,7 @@ 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';
import { AIModelDialogComponent } from '@home/components/ai-model/ai-model-dialog.component';
@NgModule({
declarations:
@ -356,6 +357,7 @@ import { CheckConnectivityDialogComponent } from '@home/components/ai-model/chec
CalculatedFieldScriptTestDialogComponent,
CalculatedFieldTestArgumentsComponent,
CheckConnectivityDialogComponent,
AIModelDialogComponent,
],
imports: [
CommonModule,
@ -502,6 +504,7 @@ import { CheckConnectivityDialogComponent } from '@home/components/ai-model/chec
CalculatedFieldScriptTestDialogComponent,
CalculatedFieldTestArgumentsComponent,
CheckConnectivityDialogComponent,
AIModelDialogComponent,
],
providers: [
WidgetComponentService,

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

@ -29,8 +29,8 @@
labelText="ai-models.ai-model"
(entityChanged)="onEntityChange($event)"
[entityType]="entityType.AI_MODEL"
(createNew)="createModelAi('modelSettingsId')"
formControlName="modelSettingsId">
(createNew)="createModelAi('modelId')"
formControlName="modelId">
</tb-entity-autocomplete>
</section>
</section>
@ -84,6 +84,7 @@
@if (aiConfigForm.get('responseFormat.type').value === responseFormat.JSON_SCHEMA) {
<tb-json-object-edit
jsonRequired
iconHint="{{ 'rule-node-config.ai.response-json-schema-hint' | translate }}"
label="{{ 'rule-node-config.ai.response-json-schema' | translate }}"
formControlName="schema">
</tb-json-object-edit>

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

9
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();
}

41
ui-ngx/src/app/shared/components/ai-model/models-list-autocomplete.component.html

@ -1,41 +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.
-->
<mat-form-field [appearance]="appearance" [subscriptSizing]="subscriptSizing" style="width: 100%">
<mat-label *ngIf="label">{{label}}</mat-label>
<input matInput #nameInput [formControl]="selectionFormControl"
[placeholder]="placeholderText"
(focusin)="onFocus()"
[matAutocomplete]="optionsAutocomplete">
<button *ngIf="selectionFormControl.value && !disabled"
type="button"
class="tb-icon-24 mr-2"
matSuffix mat-icon-button aria-label="Clear"
(click)="clear()">
<mat-icon class="material-icons">close</mat-icon>
</button>
<mat-error *ngIf="selectionFormControl.hasError('required')">
{{errorText}}
</mat-error>
<mat-autocomplete
#optionsAutocomplete="matAutocomplete"
class="tb-autocomplete tb-options-input-autocomplete">
<mat-option *ngFor="let option of filteredOptions$ | async" [value]="option">
<span class="tb-option-name flex-1" [innerHTML]="option | highlight:searchText:true:'ig'"></span>
</mat-option>
</mat-autocomplete>
</mat-form-field>

161
ui-ngx/src/app/shared/components/ai-model/models-list-autocomplete.component.ts

@ -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<Array<string>>;
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);
}
}

7
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 }}
</button>
@if (iconHint) {
<button mat-icon-button class="tb-mat-32"
matTooltip="{{ iconHint }}"
matTooltipPosition="above">
<mat-icon class="material-icons">info</mat-icon>
</button>
}
<button mat-icon-button class="tb-mat-32" (click)="fullscreen = !fullscreen"
matTooltip="{{(fullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}"
matTooltipPosition="above">

2
ui-ngx/src/app/shared/components/json-object-edit.component.ts

@ -74,6 +74,8 @@ export class JsonObjectEditComponent implements OnInit, ControlValueAccessor, Va
@Input() label: string;
@Input() iconHint: string;
@Input() disabled: boolean;
@Input() fillHeight: boolean;

2
ui-ngx/src/app/shared/components/string-autocomplete.component.html

@ -43,7 +43,7 @@
<mat-autocomplete
#optionsAutocomplete="matAutocomplete"
class="tb-autocomplete tb-options-input-autocomplete"
panelWidth="fit-content">
[panelWidth]="panelWidth">
<mat-option *ngFor="let option of filteredOptions$ | async" [value]="option">
<span class="tb-option-name flex-1" [innerHTML]="option | highlight:searchText:true:'ig'"></span>
</mat-option>

3
ui-ngx/src/app/shared/components/string-autocomplete.component.ts

@ -76,6 +76,9 @@ export class StringAutocompleteComponent implements ControlValueAccessor, OnInit
@Input()
label: string;
@Input()
panelWidth: string = 'fit-content';
@Input()
tooltipClass = 'tb-error-tooltip';

6
ui-ngx/src/app/shared/shared.module.ts

@ -228,8 +228,6 @@ import { JsFuncModuleRowComponent } from '@shared/components/js-func-module-row.
import { EntityKeyAutocompleteComponent } from '@shared/components/entity/entity-key-autocomplete.component';
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';
export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) {
return markedOptionsService;
@ -445,8 +443,6 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
ScadaSymbolInputComponent,
EntityKeyAutocompleteComponent,
MqttVersionSelectComponent,
AIModelDialogComponent,
ModelsListAutocompleteComponent,
],
imports: [
CommonModule,
@ -711,8 +707,6 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
ScadaSymbolInputComponent,
EntityKeyAutocompleteComponent,
MqttVersionSelectComponent,
AIModelDialogComponent,
ModelsListAutocompleteComponent,
]
})
export class SharedModule { }

1
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -5447,6 +5447,7 @@
"response-text": "Text",
"response-json": "JSON",
"response-json-schema": "JSON Schema",
"response-json-schema-hint": "While any valid JSON Schema can be entered, this rule node only supports a limited subset of its features. See node documentation for details.",
"response-json-schema-required": "JSON Schema is required",
"advanced-settings": "Advanced settings",
"timeout": "Timeout",

Loading…
Cancel
Save