Browse Source

UI: ref after review

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

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

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

8
ui-ngx/src/app/modules/common/modules-map.ts

@ -336,8 +336,7 @@ import * as DatapointsLimitComponent from '@shared/components/time/datapoints-li
import * as AggregationTypeSelectComponent from '@shared/components/time/aggregation/aggregation-type-select.component'; import * as 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> {

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

@ -56,7 +56,7 @@
</mat-select> </mat-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"
required appearance="outline"
[label]="(provider === aiProvider.AZURE_OPENAI ? 'ai-models.deployment-name': 'ai-models.model-id') | translate" panelWidth=""
[errorText]="(provider === aiProvider.AZURE_OPENAI ? 'ai-models.deployment-name-required': 'ai-models.model-id-required') | translate"> required
</tb-models-list-autocomplete> [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> </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 }}

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

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

@ -20,7 +20,7 @@ import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { 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 {

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

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

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

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

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

@ -71,7 +71,11 @@ export class CheckConnectivityDialogComponent extends DialogComponent<CheckConne
if (result.status === 'SUCCESS') { if (result.status === 'SUCCESS') {
this.showCheckSuccess = true; this.showCheckSuccess = true;
} else { } 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 error: err => this.checkErrMsg = err.error.message

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

@ -204,6 +204,7 @@ import {
CalculatedFieldTestArgumentsComponent 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,

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

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

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

@ -19,7 +19,7 @@ import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms
import { RuleNodeConfiguration, RuleNodeConfigurationComponent } from '@shared/models/rule-node.models'; import { 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({

9
ui-ngx/src/app/modules/home/pages/ai-model/ai-model-table-config.resolve.ts

@ -31,7 +31,7 @@ import { Observable } from 'rxjs';
import { AiModel, AiProviderTranslations } from '@shared/models/ai-model.models'; import { 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();
} }

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

@ -1,41 +0,0 @@
<!--
Copyright © 2016-2025 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<mat-form-field [appearance]="appearance" [subscriptSizing]="subscriptSizing" style="width: 100%">
<mat-label *ngIf="label">{{label}}</mat-label>
<input matInput #nameInput [formControl]="selectionFormControl"
[placeholder]="placeholderText"
(focusin)="onFocus()"
[matAutocomplete]="optionsAutocomplete">
<button *ngIf="selectionFormControl.value && !disabled"
type="button"
class="tb-icon-24 mr-2"
matSuffix mat-icon-button aria-label="Clear"
(click)="clear()">
<mat-icon class="material-icons">close</mat-icon>
</button>
<mat-error *ngIf="selectionFormControl.hasError('required')">
{{errorText}}
</mat-error>
<mat-autocomplete
#optionsAutocomplete="matAutocomplete"
class="tb-autocomplete tb-options-input-autocomplete">
<mat-option *ngFor="let option of filteredOptions$ | async" [value]="option">
<span class="tb-option-name flex-1" [innerHTML]="option | highlight:searchText:true:'ig'"></span>
</mat-option>
</mat-autocomplete>
</mat-form-field>

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

@ -1,161 +0,0 @@
///
/// Copyright © 2016-2025 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, ElementRef, forwardRef, Input, OnChanges, OnInit, SimpleChanges, ViewChild } from '@angular/core';
import { ControlValueAccessor, FormBuilder, FormControl, NG_VALUE_ACCESSOR, Validators } from '@angular/forms';
import { Observable } from 'rxjs';
import { map, startWith, tap } from 'rxjs/operators';
import { TranslateService } from '@ngx-translate/core';
import { coerceBoolean } from '@shared/decorators/coercion';
import { MatFormFieldAppearance, SubscriptSizing } from '@angular/material/form-field';
import { AiModelMap, AiProvider } from '@shared/models/ai-model.models';
@Component({
selector: 'tb-models-list-autocomplete',
templateUrl: './models-list-autocomplete.component.html',
styleUrls: [],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => ModelsListAutocompleteComponent),
multi: true
}
]
})
export class ModelsListAutocompleteComponent implements ControlValueAccessor, OnInit, OnChanges {
@ViewChild('nameInput', {static: true}) nameInput: ElementRef;
@Input()
disabled: boolean;
@Input()
@coerceBoolean()
required = false;
@Input()
provider: AiProvider;
@Input()
placeholderText: string = this.translate.instant('widget-config.set');
@Input()
subscriptSizing: SubscriptSizing = 'dynamic';
@Input()
appearance: MatFormFieldAppearance = 'outline';
@Input()
label: string;
@Input()
errorText: string;
selectionFormControl: FormControl;
modelValue: string | null;
filteredOptions$: Observable<Array<string>>;
searchText = '';
private dirty = false;
private propagateChange = (_val: any) => {};
constructor(private fb: FormBuilder,
private translate: TranslateService) {
}
ngOnInit() {
this.selectionFormControl = this.fb.control('', this.required ? [Validators.required] : []);
this.setupFilteredOptions();
}
ngOnChanges(changes: SimpleChanges) {
if (changes.provider && !changes.provider.isFirstChange()) {
this.setupFilteredOptions();
this.selectionFormControl.setValue(null, {emitEvent: false});
this.modelValue = null;
this.propagateChange(null);
}
}
private setupFilteredOptions() {
this.filteredOptions$ = this.selectionFormControl.valueChanges.pipe(
startWith(''),
tap(value => this.updateView(value)),
map(value => {
const search = value ? value.toLowerCase() : '';
const options = this.provider ? AiModelMap.get(this.provider).modelList || [] : [];
return search ? options.filter(option => option.toLowerCase().includes(search)) : options;
})
);
}
writeValue(option?: string): void {
this.searchText = '';
this.modelValue = option ? option : null;
if (option) {
this.selectionFormControl.patchValue(option, { emitEvent: false });
this.dirty = true;
} else {
this.selectionFormControl.patchValue(null, { emitEvent: false });
this.dirty = true;
}
}
onFocus() {
if (this.dirty) {
this.selectionFormControl.updateValueAndValidity({onlySelf: true, emitEvent: true});
this.dirty = false;
}
}
updateView(value: string) {
this.searchText = value ? value : '';
if (this.modelValue !== value && value) {
this.modelValue = value;
this.propagateChange(this.modelValue);
}
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.disabled) {
this.selectionFormControl.disable({emitEvent: false});
} else {
this.selectionFormControl.enable({emitEvent: false});
}
}
clear() {
this.selectionFormControl.patchValue(null, {emitEvent: true});
this.propagateChange(null);
this.modelValue = null;
setTimeout(() => {
this.nameInput.nativeElement.blur();
this.nameInput.nativeElement.focus();
}, 0);
}
}

7
ui-ngx/src/app/shared/components/json-object-edit.component.html

@ -32,6 +32,13 @@
mat-button *ngIf="!readonly && !disabled" class="tidy" (click)="minifyJSON()"> 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">

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

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

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

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

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

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

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

@ -228,8 +228,6 @@ import { JsFuncModuleRowComponent } from '@shared/components/js-func-module-row.
import { EntityKeyAutocompleteComponent } from '@shared/components/entity/entity-key-autocomplete.component'; import { 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 { }

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

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

Loading…
Cancel
Save