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"
 | 
			
		||||
            <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">
 | 
			
		||||
            </tb-models-list-autocomplete>
 | 
			
		||||
                                    [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 {
 | 
			
		||||
            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