UI: ref after review

This commit is contained in:
Artem Dzhereleiko 2025-07-10 13:32:39 +03:00
parent b84d2fcbb0
commit 64f0da3365
20 changed files with 88 additions and 259 deletions

View File

@ -794,6 +794,7 @@ const defaultUserMenuMap = new Map<Authority, MenuReference[]>([
{id: MenuId.home}, {id: MenuId.home},
{id: MenuId.alarms}, {id: MenuId.alarms},
{id: MenuId.dashboards}, {id: MenuId.dashboards},
{id: MenuId.ai_models},
{ {
id: MenuId.entities, id: MenuId.entities,
pages: [ pages: [
@ -852,7 +853,6 @@ const defaultUserMenuMap = new Map<Authority, MenuReference[]>([
{id: MenuId.notification_rules} {id: MenuId.notification_rules}
] ]
}, },
{id: MenuId.ai_models},
{ {
id: MenuId.mobile_center, id: MenuId.mobile_center,
pages: [ pages: [

View File

@ -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 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 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 IntervalOptionsConfigPanelComponent from '@shared/components/time/interval-options-config-panel.component';
import * as AIModelDialogComponent from '@shared/components/ai-model/ai-model-dialog.component'; import * as AIModelDialogComponent from '@home/components/ai-model/ai-model-dialog.component';
import * as ModelsListAutocompleteComponent from '@shared/components/ai-model/models-list-autocomplete.component';
import { IModulesMap } from '@modules/common/modules-map.models'; import { IModulesMap } from '@modules/common/modules-map.models';
import { Observable, of } from 'rxjs'; import { Observable, of } from 'rxjs';
@ -534,8 +533,6 @@ class ModulesMap implements IModulesMap {
'@shared/components/image/gallery-image-input.component': GalleryImageInputComponent, '@shared/components/image/gallery-image-input.component': GalleryImageInputComponent,
'@shared/components/image/multiple-gallery-image-input.component': MultipleGalleryImageInputComponent, '@shared/components/image/multiple-gallery-image-input.component': MultipleGalleryImageInputComponent,
'@shared/components/popover.service': TbPopoverService, '@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, '@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/dashboard-page/dashboard-image-dialog.component': DashboardImageDialogComponent,
'@home/components/widget/widget-container.component': WidgetContainerComponent, '@home/components/widget/widget-container.component': WidgetContainerComponent,
'@home/components/profile/queue/tenant-profile-queues.component': TenantProfileQueuesComponent, '@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> { init(): Observable<any> {

View File

@ -56,7 +56,7 @@
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<div formGroupName="providerConfig" class="tb-form-panel no-border no-padding"> <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-form-field class="mat-block flex-1" appearance="outline">
<mat-label translate>ai-models.personal-access-token</mat-label> <mat-label translate>ai-models.personal-access-token</mat-label>
<input type="password" required matInput formControlName="personalAccessToken"> <input type="password" required matInput formControlName="personalAccessToken">
@ -66,7 +66,7 @@
</mat-error> </mat-error>
</mat-form-field> </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-form-field class="mat-block flex-1" appearance="outline" subscriptSizing="dynamic">
<mat-label translate>ai-models.project-id</mat-label> <mat-label translate>ai-models.project-id</mat-label>
<input matInput required formControlName="projectId"> <input matInput required formControlName="projectId">
@ -75,7 +75,7 @@
</mat-error> </mat-error>
</mat-form-field> </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-form-field class="mat-block flex-1" appearance="outline" subscriptSizing="dynamic">
<mat-label translate>ai-models.location</mat-label> <mat-label translate>ai-models.location</mat-label>
<input matInput required formControlName="location"> <input matInput required formControlName="location">
@ -84,7 +84,7 @@
</mat-error> </mat-error>
</mat-form-field> </mat-form-field>
} }
@if (AiModelMap.get(provider).providerFieldsList.includes('serviceAccountKey')) { @if (providerFieldsList.includes('serviceAccountKey')) {
<tb-file-input formControlName="serviceAccountKey" <tb-file-input formControlName="serviceAccountKey"
[existingFileName]="aiModelForms.get('configuration').get('providerConfig').get('fileName').value" [existingFileName]="aiModelForms.get('configuration').get('providerConfig').get('fileName').value"
(fileNameChanged)="aiModelForms.get('configuration').get('providerConfig').get('fileName').setValue($event)" (fileNameChanged)="aiModelForms.get('configuration').get('providerConfig').get('fileName').setValue($event)"
@ -97,7 +97,7 @@
dropLabel="{{'ai-models.drop-file' | translate}}"> dropLabel="{{'ai-models.drop-file' | translate}}">
</tb-file-input> </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-form-field class="mat-block flex-1" appearance="outline" subscriptSizing="dynamic">
<mat-label translate>ai-models.endpoint</mat-label> <mat-label translate>ai-models.endpoint</mat-label>
<input required matInput formControlName="endpoint"> <input required matInput formControlName="endpoint">
@ -106,13 +106,13 @@
</mat-error> </mat-error>
</mat-form-field> </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-form-field class="mat-block flex-1" appearance="outline" subscriptSizing="dynamic">
<mat-label translate>ai-models.service-version</mat-label> <mat-label translate>ai-models.service-version</mat-label>
<input matInput formControlName="serviceVersion"> <input matInput formControlName="serviceVersion">
</mat-form-field> </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-form-field class="mat-block flex-1" appearance="outline">
<mat-label translate>ai-models.api-key</mat-label> <mat-label translate>ai-models.api-key</mat-label>
<input type="password" required matInput formControlName="apiKey"> <input type="password" required matInput formControlName="apiKey">
@ -129,14 +129,17 @@
<div class="tb-form-panel-title" translate>ai-models.configuration</div> <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 no-border no-padding">
<section class="tb-form-panel outlined no-border no-padding"> <section class="tb-form-panel outlined no-border no-padding">
<tb-models-list-autocomplete formControlName="modelId" <tb-string-autocomplete [fetchOptionsFn]="fetchOptions.bind(this)"
[provider]="provider" additionalClass="tb-suffix-show-on-hover"
appearance="outline"
panelWidth=""
required required
[label]="(provider === aiProvider.AZURE_OPENAI ? 'ai-models.deployment-name': 'ai-models.model-id') | translate" [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"> [errorText]="(provider === aiProvider.AZURE_OPENAI ? 'ai-models.deployment-name-required': 'ai-models.model-id-required') | translate"
</tb-models-list-autocomplete> formControlName="modelId">
</tb-string-autocomplete>
</section> </section>
@if (AiModelMap.get(provider).modelFieldsList.includes('temperature')) { @if (modelFieldsList.includes('temperature')) {
<div class="tb-form-row space-between"> <div class="tb-form-row space-between">
<div tb-hint-tooltip-icon="{{ 'ai-models.temperature-hint' | translate }}"> <div tb-hint-tooltip-icon="{{ 'ai-models.temperature-hint' | translate }}">
{{ 'ai-models.temperature' | translate }} {{ 'ai-models.temperature' | translate }}
@ -155,7 +158,7 @@
</mat-form-field> </mat-form-field>
</div> </div>
} }
@if (AiModelMap.get(provider).modelFieldsList.includes('topP')) { @if (modelFieldsList.includes('topP')) {
<div class="tb-form-row space-between"> <div class="tb-form-row space-between">
<div tb-hint-tooltip-icon="{{ 'ai-models.top-p-hint' | translate }}"> <div tb-hint-tooltip-icon="{{ 'ai-models.top-p-hint' | translate }}">
{{ 'ai-models.top-p' | translate }} {{ 'ai-models.top-p' | translate }}
@ -175,7 +178,7 @@
</mat-form-field> </mat-form-field>
</div> </div>
} }
@if (AiModelMap.get(provider).modelFieldsList.includes('topK')) { @if (modelFieldsList.includes('topK')) {
<div class="tb-form-row space-between"> <div class="tb-form-row space-between">
<div tb-hint-tooltip-icon="{{ 'ai-models.top-k-hint' | translate }}"> <div tb-hint-tooltip-icon="{{ 'ai-models.top-k-hint' | translate }}">
{{ 'ai-models.top-k' | translate }} {{ 'ai-models.top-k' | translate }}
@ -194,7 +197,7 @@
</mat-form-field> </mat-form-field>
</div> </div>
} }
@if (AiModelMap.get(provider).modelFieldsList.includes('presencePenalty')) { @if (modelFieldsList.includes('presencePenalty')) {
<div class="tb-form-row space-between"> <div class="tb-form-row space-between">
<div tb-hint-tooltip-icon="{{ 'ai-models.presence-penalty-hint' | translate }}"> <div tb-hint-tooltip-icon="{{ 'ai-models.presence-penalty-hint' | translate }}">
{{ 'ai-models.presence-penalty' | translate }} {{ 'ai-models.presence-penalty' | translate }}
@ -206,7 +209,7 @@
</div> </div>
} }
@if (AiModelMap.get(provider).modelFieldsList.includes('frequencyPenalty')) { @if (modelFieldsList.includes('frequencyPenalty')) {
<div class="tb-form-row space-between"> <div class="tb-form-row space-between">
<div tb-hint-tooltip-icon="{{ 'ai-models.frequency-penalty-hint' | translate }}"> <div tb-hint-tooltip-icon="{{ 'ai-models.frequency-penalty-hint' | translate }}">
{{ 'ai-models.frequency-penalty' | translate }} {{ 'ai-models.frequency-penalty' | translate }}
@ -217,7 +220,7 @@
</mat-form-field> </mat-form-field>
</div> </div>
} }
@if (AiModelMap.get(provider).modelFieldsList.includes('maxOutputTokens')) { @if (modelFieldsList.includes('maxOutputTokens')) {
<div class="tb-form-row space-between"> <div class="tb-form-row space-between">
<div tb-hint-tooltip-icon="{{ 'ai-models.max-output-token-hint' | translate }}"> <div tb-hint-tooltip-icon="{{ 'ai-models.max-output-token-hint' | translate }}">
{{ 'ai-models.max-output-token' | translate }} {{ 'ai-models.max-output-token' | translate }}

View File

@ -20,7 +20,7 @@ import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog'; 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 { StepperOrientation } from '@angular/cdk/stepper';
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { EntityType } from '@shared/models/entity-type.models'; import { EntityType } from '@shared/models/entity-type.models';
@ -35,6 +35,7 @@ import {
} from '@shared/models/ai-model.models'; } from '@shared/models/ai-model.models';
import { AiModelService } from '@core/http/ai-model.service'; import { AiModelService } from '@core/http/ai-model.service';
import { CheckConnectivityDialogComponent } from '@home/components/ai-model/check-connectivity-dialog.component'; import { CheckConnectivityDialogComponent } from '@home/components/ai-model/check-connectivity-dialog.component';
import { map } from 'rxjs/operators';
export interface AIModelDialogData { export interface AIModelDialogData {
AIModel?: AiModel; AIModel?: AiModel;
@ -112,7 +113,7 @@ export class AIModelDialogComponent extends DialogComponent<AIModelDialogCompone
takeUntilDestroyed() takeUntilDestroyed()
).subscribe((provider: AiProvider) => { ).subscribe((provider: AiProvider) => {
this.provider = provider; this.provider = provider;
this.aiModelForms.get('configuration.modelId').reset({}); this.aiModelForms.get('configuration.modelId').reset('');
this.aiModelForms.get('configuration.providerConfig').reset({}); this.aiModelForms.get('configuration.providerConfig').reset({});
this.updateValidation(provider); this.updateValidation(provider);
}) })
@ -120,11 +121,28 @@ export class AIModelDialogComponent extends DialogComponent<AIModelDialogCompone
this.updateValidation(this.provider); 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) { private updateValidation(provider: AiProvider) {
ProviderFieldsAllList.forEach(key => ProviderFieldsAllList.forEach(key => {
this.aiModelForms.get('configuration.providerConfig') if (AiModelMap.get(provider).providerFieldsList.includes(key)) {
.get(key)[AiModelMap.get(provider).providerFieldsList.includes(key) ? 'enable' : 'disable']() 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 { cancel(): void {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -19,7 +19,7 @@ import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms
import { RuleNodeConfiguration, RuleNodeConfigurationComponent } from '@shared/models/rule-node.models'; import { RuleNodeConfiguration, RuleNodeConfigurationComponent } from '@shared/models/rule-node.models';
import { EntityType } from '@shared/models/entity-type.models'; import { EntityType } from '@shared/models/entity-type.models';
import { MatDialog } from '@angular/material/dialog'; 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 { AiModel, AiRuleNodeResponseFormatTypeOnlyText, ResponseFormat } from '@shared/models/ai-model.models';
import { deepTrim } from '@core/utils'; import { deepTrim } from '@core/utils';
@ -47,7 +47,7 @@ export class AiConfigComponent extends RuleNodeConfigurationComponent {
protected onConfigurationSet(configuration: RuleNodeConfiguration) { protected onConfigurationSet(configuration: RuleNodeConfiguration) {
this.aiConfigForm = this.fb.group({ 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.*/)]], systemPrompt: [configuration?.systemPrompt ?? '', [Validators.maxLength(10000), Validators.pattern(/.*\S.*/)]],
userPrompt: [configuration?.userPrompt ?? '', [Validators.required, Validators.maxLength(10000), Validators.pattern(/.*\S.*/)]], userPrompt: [configuration?.userPrompt ?? '', [Validators.required, Validators.maxLength(10000), Validators.pattern(/.*\S.*/)]],
responseFormat: this.fb.group({ responseFormat: this.fb.group({

View File

@ -31,7 +31,7 @@ import { Observable } from 'rxjs';
import { AiModel, AiProviderTranslations } from '@shared/models/ai-model.models'; import { AiModel, AiProviderTranslations } from '@shared/models/ai-model.models';
import { AiModelService } from '@core/http/ai-model.service'; import { AiModelService } from '@core/http/ai-model.service';
import { AiModelTableHeaderComponent } from '@home/pages/ai-model/ai-model-table-header.component'; 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'; import { map } from 'rxjs/operators';
@Injectable() @Injectable()
@ -81,7 +81,7 @@ export class AiModelsTableConfigResolver {
this.config.cellActionDescriptors = this.configureCellActions(); this.config.cellActionDescriptors = this.configureCellActions();
this.config.handleRowClick = ($event, model) => { this.config.handleRowClick = ($event, model) => {
this.editModel(model); this.editModel($event, model);
return true; return true;
}; };
} }
@ -96,12 +96,13 @@ export class AiModelsTableConfigResolver {
name: this.translate.instant('action.edit'), name: this.translate.instant('action.edit'),
icon: 'edit', icon: 'edit',
isEnabled: () => true, 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(); this.addModel(AIModel, false).subscribe();
} }

View File

@ -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>

View File

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

View File

@ -32,6 +32,13 @@
mat-button *ngIf="!readonly && !disabled" class="tidy" (click)="minifyJSON()"> mat-button *ngIf="!readonly && !disabled" class="tidy" (click)="minifyJSON()">
{{'js-func.mini' | translate }} {{'js-func.mini' | translate }}
</button> </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" <button mat-icon-button class="tb-mat-32" (click)="fullscreen = !fullscreen"
matTooltip="{{(fullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}" matTooltip="{{(fullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}"
matTooltipPosition="above"> matTooltipPosition="above">

View File

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

View File

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

View File

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

View File

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

View File

@ -5447,6 +5447,7 @@
"response-text": "Text", "response-text": "Text",
"response-json": "JSON", "response-json": "JSON",
"response-json-schema": "JSON Schema", "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", "response-json-schema-required": "JSON Schema is required",
"advanced-settings": "Advanced settings", "advanced-settings": "Advanced settings",
"timeout": "Timeout", "timeout": "Timeout",