UI: ref after review
This commit is contained in:
parent
b84d2fcbb0
commit
64f0da3365
@ -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: [
|
||||
|
||||
@ -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> {
|
||||
|
||||
@ -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 }}
|
||||
@ -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 {
|
||||
@ -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)"
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
|
||||
@ -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>
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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">
|
||||
|
||||
@ -74,6 +74,8 @@ export class JsonObjectEditComponent implements OnInit, ControlValueAccessor, Va
|
||||
|
||||
@Input() label: string;
|
||||
|
||||
@Input() iconHint: string;
|
||||
|
||||
@Input() disabled: boolean;
|
||||
|
||||
@Input() fillHeight: boolean;
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -76,6 +76,9 @@ export class StringAutocompleteComponent implements ControlValueAccessor, OnInit
|
||||
@Input()
|
||||
label: string;
|
||||
|
||||
@Input()
|
||||
panelWidth: string = 'fit-content';
|
||||
|
||||
@Input()
|
||||
tooltipClass = 'tb-error-tooltip';
|
||||
|
||||
|
||||
@ -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 { }
|
||||
|
||||
@ -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…
x
Reference in New Issue
Block a user