UI: Fixed bugs change translate and add bedrock provider config settings

This commit is contained in:
Artem Dzhereleiko 2025-07-14 10:52:06 +03:00
parent 2a8c6a3d2a
commit 315fef0fc8
8 changed files with 91 additions and 38 deletions

View File

@ -35,7 +35,7 @@
<mat-label translate>ai-models.name</mat-label>
<input matInput required formControlName="name">
<mat-hint></mat-hint>
<mat-error *ngIf="aiModelForms.get('name').hasError('required')">
<mat-error *ngIf="aiModelForms.get('name').hasError('required') || aiModelForms.get('name').hasError('pattern')">
{{ 'ai-models.name-required' | translate }}
</mat-error>
<mat-error *ngIf="aiModelForms.get('name').hasError('maxlength')">
@ -49,7 +49,7 @@
<section class="tb-form-panel outlined no-border no-padding">
<mat-form-field class="mat-block flex-1" appearance="outline">
<mat-label translate>ai-models.ai-provider</mat-label>
<mat-select required formControlName="provider">
<mat-select formControlName="provider">
<mat-option *ngFor="let provider of providerMap" [value]="provider">
{{providerTranslationMap.get(provider) | translate}}
</mat-option>
@ -122,6 +122,34 @@
</mat-error>
</mat-form-field>
}
@if (providerFieldsList.includes('region')) {
<mat-form-field class="mat-block flex-1" appearance="outline" subscriptSizing="dynamic">
<mat-label translate>ai-models.region</mat-label>
<input required matInput formControlName="region">
<mat-error *ngIf="aiModelForms.get('configuration').get('providerConfig').get('region').hasError('required')">
{{ 'ai-models.region-required' | translate }}
</mat-error>
</mat-form-field>
}
@if (providerFieldsList.includes('accessKeyId')) {
<mat-form-field class="mat-block flex-1" appearance="outline" subscriptSizing="dynamic">
<mat-label translate>ai-models.access-key-id</mat-label>
<input required matInput formControlName="accessKeyId">
<mat-error *ngIf="aiModelForms.get('configuration').get('providerConfig').get('accessKeyId').hasError('required')">
{{ 'ai-models.access-key-id-required' | translate }}
</mat-error>
</mat-form-field>
}
@if (providerFieldsList.includes('secretAccessKey')) {
<mat-form-field class="mat-block flex-1" appearance="outline">
<mat-label translate>ai-models.secret-access-key</mat-label>
<input type="password" required matInput formControlName="secretAccessKey">
<tb-toggle-password matSuffix></tb-toggle-password>
<mat-error *ngIf="aiModelForms.get('configuration').get('providerConfig').get('secretAccessKey').hasError('required')">
{{ 'ai-models.secret-access-key-required' | translate }}
</mat-error>
</mat-form-field>
}
</div>
</section>
</section>
@ -222,8 +250,8 @@
}
@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 }}
<div tb-hint-tooltip-icon="{{ 'ai-models.max-output-tokens-hint' | translate }}">
{{ 'ai-models.max-output-tokens' | translate }}
</div>
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="maxOutputTokens"
@ -231,7 +259,7 @@
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'ai-models.max-output-token-min' | translate"
[matTooltip]="'ai-models.max-output-tokens-min' | translate"
*ngIf="aiModelForms.get('configuration').get('maxOutputTokens').hasError('min')"
class="tb-error">
warning

View File

@ -83,10 +83,10 @@ export class AIModelDialogComponent extends DialogComponent<AIModelDialogCompone
this.provider = this.data.AIModel ? this.data.AIModel.configuration.provider : AiProvider.OPENAI;
this.aiModelForms = this.fb.group({
name: [this.data.AIModel ? this.data.AIModel.name : '', [Validators.required, Validators.maxLength(255)]],
name: [this.data.AIModel ? this.data.AIModel.name : '', [Validators.required, Validators.maxLength(255), Validators.pattern(/.*\S.*/)]],
modelType: [ModelType.CHAT],
configuration: this.fb.group({
provider: [this.provider, [Validators.required]],
provider: [this.provider, []],
providerConfig: this.fb.group({
apiKey: [this.data.AIModel ? this.data.AIModel.configuration.providerConfig?.apiKey : '', [Validators.required]],
personalAccessToken: [this.data.AIModel ? this.data.AIModel.configuration.providerConfig?.personalAccessToken : '', [Validators.required]],
@ -96,6 +96,9 @@ export class AIModelDialogComponent extends DialogComponent<AIModelDialogCompone
location: [this.data.AIModel ? this.data.AIModel.configuration.providerConfig?.location : '', [Validators.required]],
serviceAccountKey: [this.data.AIModel ? this.data.AIModel.configuration.providerConfig?.serviceAccountKey : '', [Validators.required]],
fileName: [this.data.AIModel ? this.data.AIModel.configuration.providerConfig?.fileName : '', [Validators.required]],
region: [this.data.AIModel ? this.data.AIModel.configuration.providerConfig?.region : '', [Validators.required]],
accessKeyId: [this.data.AIModel ? this.data.AIModel.configuration.providerConfig?.accessKeyId : '', [Validators.required]],
secretAccessKey: [this.data.AIModel ? this.data.AIModel.configuration.providerConfig?.secretAccessKey : '', [Validators.required]],
}),
modelId: [this.data.AIModel ? this.data.AIModel.configuration?.modelId : '', [Validators.required]],
temperature: [this.data.AIModel ? this.data.AIModel.configuration?.temperature : null, [Validators.min(0)]],

View File

@ -26,7 +26,7 @@
useFullEntityId
required
appearance="outline"
labelText="ai-models.ai-model"
labelText="rule-node-config.ai.model"
(entityChanged)="onEntityChange($event)"
[entityType]="entityType.AI_MODEL"
(createNew)="createModelAi('modelId')"
@ -76,10 +76,10 @@
<div class="tb-form-panel stroked" formGroupName="responseFormat">
<div class="flex flex-row items-center justify-between xs:flex-col xs:items-start xs:gap-3">
<div class="tb-form-panel-title" tb-hint-tooltip-icon="{{ 'rule-node-config.ai.response-format-hint' | translate }}">
<div class="tb-form-panel-title" tb-hint-tooltip-icon="{{ getResponseFormatHint }}">
{{ 'rule-node-config.ai.response-format' | translate }}
</div>
<tb-toggle-select formControlName="type">
<tb-toggle-select formControlName="type" [disabled]="disabledResponseFormatType">
<tb-toggle-option [value]="responseFormat.TEXT">{{ 'rule-node-config.ai.response-text' | translate }}</tb-toggle-option>
<tb-toggle-option [value]="responseFormat.JSON">{{ 'rule-node-config.ai.response-json' | translate }}</tb-toggle-option>
<tb-toggle-option [value]="responseFormat.JSON_SCHEMA">{{ 'rule-node-config.ai.response-json-schema' | translate }}</tb-toggle-option>
@ -107,7 +107,7 @@
</mat-expansion-panel-header>
<div class="tb-form-panel no-border no-padding-top" style="margin-top: 0">
<div class="tb-form-row space-between flex-1 columns-xs">
<div translate tb-hint-tooltip-icon="{{'rule-node-config.ai.timeout-hint' | translate}}">{{ 'rule-node-config.ai.timeout' | translate }}</div>
<div tb-hint-tooltip-icon="{{'rule-node-config.ai.timeout-hint' | translate}}">{{ 'rule-node-config.ai.timeout' | translate }}</div>
<div class="flex flex-row items-center justify-start gap-2">
<tb-time-unit-input
required

View File

@ -22,6 +22,7 @@ import { MatDialog } from '@angular/material/dialog';
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';
import { TranslateService } from '@ngx-translate/core';
@Component({
selector: 'tb-external-node-ai-config',
@ -36,7 +37,10 @@ export class AiConfigComponent extends RuleNodeConfigurationComponent {
responseFormat = ResponseFormat;
disabledResponseFormatType: boolean;
constructor(private fb: UntypedFormBuilder,
private translate: TranslateService,
private dialog: MatDialog) {
super();
}
@ -72,20 +76,29 @@ export class AiConfigComponent extends RuleNodeConfigurationComponent {
}
protected prepareOutputConfig(configuration: RuleNodeConfiguration): RuleNodeConfiguration {
if (!this.aiConfigForm.get('systemPrompt').value) {
delete configuration.systemPrompt;
}
return deepTrim(configuration);
}
onEntityChange($event: AiModel) {
if ($event) {
if (AiRuleNodeResponseFormatTypeOnlyText.includes($event.configuration.provider)) {
this.aiConfigForm.get('responseFormat.type').patchValue(ResponseFormat.TEXT, {emitEvent: false});
this.aiConfigForm.get('responseFormat.type').disable({emitEvent: false});
if (this.aiConfigForm.get('responseFormat.type').value !== ResponseFormat.TEXT) {
this.aiConfigForm.get('responseFormat.type').patchValue(ResponseFormat.TEXT, {emitEvent: true});
}
this.disabledResponseFormatType = true;
}
} else {
this.aiConfigForm.get('responseFormat.type').enable({emitEvent: false});
this.disabledResponseFormatType = false;
}
}
get getResponseFormatHint() {
return this.translate.instant(`rule-node-config.ai.response-format-hint-${this.aiConfigForm.get('responseFormat.type').value}`);
}
createModelAi(formControl: string) {
this.dialog.open<AIModelDialogComponent, AIModelDialogData, AiModel>(AIModelDialogComponent, {
disableClose: true,

View File

@ -62,10 +62,10 @@ export class AiModelsTableConfigResolver {
this.config.columns.push(
new DateEntityTableColumn<AiModel>('createdTime', 'common.created-time', this.datePipe, '170px'),
new EntityTableColumn<AiModel>('name', 'ai-models.name', '33%'),
new EntityTableColumn<AiModel>('provider', 'ai-models.ai-provider', '33%',
new EntityTableColumn<AiModel>('provider', 'ai-models.provider', '33%',
entity => this.translate.instant(AiProviderTranslations.get(entity.configuration.provider))
),
new EntityTableColumn<AiModel>('aiModel', 'ai-models.ai-model', '33%',
new EntityTableColumn<AiModel>('aiModel', 'ai-models.model', '33%',
entity => entity.configuration.modelId, () => ({}), false
)
)
@ -104,7 +104,7 @@ export class AiModelsTableConfigResolver {
private editModel($event, AIModel: AiModel): void {
$event?.stopPropagation();
this.addModel(AIModel, false).subscribe();
this.addModel(AIModel, false).subscribe(res => res ? this.config.updateData() : null);
}
private addModel(AIModel: AiModel, isAdd = false): Observable<AiModel> {
@ -115,13 +115,6 @@ export class AiModelsTableConfigResolver {
isAdd,
AIModel
}
}).afterClosed().pipe(map(res => {
if (res) {
this.config.updateData();
return res;
} else {
return null;
}
}));
}).afterClosed();
}
}

View File

@ -29,7 +29,7 @@
(click)="clear()">
<mat-icon class="material-icons">close</mat-icon>
</button>
<mat-icon *ngIf="selectionFormControl.hasError('required') && !showInlineError"
<mat-icon *ngIf="selectionFormControl.hasError('required') && selectionFormControl.touched && !showInlineError"
matIconSuffix
[matTooltip]="errorText"
matTooltipPosition="above"

View File

@ -30,7 +30,10 @@ export interface AiModel extends Omit<BaseData<AiModelId>, 'label'>, HasTenantId
projectId?: string;
location?: string;
serviceAccountKey?: string;
fileName?: string
fileName?: string;
region?: string;
accessKeyId?: string;
secretAccessKey?: string;
};
modelId: string;
temperature?: number;
@ -78,7 +81,10 @@ export const ProviderFieldsAllList = [
'serviceAccountKey',
'fileName',
'endpoint',
'serviceVersion'
'serviceVersion',
'region',
'accessKeyId',
'secretAccessKey'
];
export const ModelFieldsAllList = ['temperature', 'topP', 'topK', 'frequencyPenalty', 'presencePenalty', 'maxOutputTokens'];
@ -173,7 +179,7 @@ export const AiModelMap = new Map<AiProvider, { modelList: string[], providerFie
AiProvider.AMAZON_BEDROCK,
{
modelList: [],
providerFieldsList: ['apiKey'],
providerFieldsList: ['region', 'accessKeyId', 'secretAccessKey'],
modelFieldsList: ['temperature', 'topP', 'maxOutputTokens'],
},
],

View File

@ -1092,6 +1092,7 @@
"ai-models": {
"ai-models": "AI models",
"ai-model": "AI model",
"model": "Model",
"name": "Name",
"ai-provider": "AI provider",
"no-found": "No AI models found",
@ -1113,7 +1114,7 @@
"github-models": "GitHub Models"
},
"name-required": "Name is required.",
"name-max-length": "Name should be less than 256 characters.",
"name-max-length": "Name must be 255 characters or less.",
"provider": "Provider",
"api-key": "API key",
"api-key-required": "API key is required.",
@ -1133,22 +1134,28 @@
"deployment-name": "Deployment name",
"deployment-name-required": "Deployment name is required",
"set": "Set",
"region": "Region",
"region-required": "Region is required.",
"access-key-id": "Access key ID",
"access-key-id-required": "Access key ID is required.",
"secret-access-key": "Secret access key",
"secret-access-key-required": "Secret access key is required.",
"temperature": "Temperature",
"temperature-hint": "Adjusts the level of randomness in the model's output. Higher values increase randomness, while lower values decrease it.",
"temperature-min": "Min value is 0.",
"temperature-min": "Must be 0 or greater.",
"top-p": "Top P",
"top-p-hint": "Creates a pool of the most probable tokens for the model to choose from. Higher values create a larger and more diverse pool, while lower values create a smaller one.",
"top-p-min-max": "Min value is more than 0, max value is 1.",
"top-p-min-max": "Must be greater than 0 and up to 1.",
"top-k": "Top K",
"top-k-hint": "Restricts the model's choices to a fixed set of the \"K\" most likely tokens.",
"top-k-min": "Min value is 0.",
"top-k-min": "Must be 0 or greater.",
"presence-penalty": "Presence penalty",
"presence-penalty-hint": "Applies a fixed penalty to the likelihood of a token if it has already appeared in the text.",
"frequency-penalty": "Frequency penalty",
"frequency-penalty-hint": "Applies a penalty to a token's likelihood that increases based on its frequency in the text.",
"max-output-token": "Maximum output token",
"max-output-token-min": "Min value is more than 0.",
"max-output-token-hint": "Sets the maximum number of tokens that the \nmodel can generate in a single response.",
"max-output-tokens": "Maximum output tokens",
"max-output-tokens-min": "Must be greater than 0.",
"max-output-tokens-hint": "Sets the maximum number of tokens that the \nmodel can generate in a single response.",
"endpoint": "Endpoint",
"endpoint-required": "Endpoint id required.",
"service-version": "Service version",
@ -5432,6 +5439,7 @@
},
"ai": {
"ai-model": "AI model",
"model": "Model",
"ai-model-hint": "Select the pre-configured AI model to process requests sent by this rule node, or use \"Create new\" to configure a new one.",
"prompt-settings": "Prompt settings",
"prompt-settings-hint": "Configure the instructions for the AI. The optional system prompt sets the AI's general role and constraints, while the user prompt defines the specific task to perform. Both fields also support rule node templates to include dynamic data (e.g., use $[*] to access the entire message body or ${*} to access all metadata).",
@ -5443,14 +5451,16 @@
"user-prompt-max-length": "User prompt must be 10000 characters or less.",
"user-prompt-blank": "User prompt must not be blank.",
"response-format": "Response format",
"response-format-hint": "The model is required to generate a JSON that matches the specific structure and data types defined in the provided schema. If the output is not a valid JSON object, it will be automatically wrapped within a JSON object under the \"response\" key.",
"response-text": "Text",
"response-json": "JSON",
"response-json-schema": "JSON Schema",
"response-format-hint-TEXT": "Allows the model to generate arbitrary text, which may or may not be a valid JSON object. If the output is not a valid JSON object, it will be automatically wrapped within a JSON object under the \"response\" key.",
"response-format-hint-JSON": "The model is required to generate a response that is a valid JSON. If the output is not a valid JSON object, it will be automatically wrapped within a JSON object under the \"response\" key.",
"response-format-hint-JSON_SCHEMA": "The model is required to generate a JSON that matches the specific structure and data types defined in the provided schema. If the output is not a valid JSON object, it will be automatically wrapped within a JSON object under the \"response\" key.",
"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",
"timeout": "Timeout *",
"timeout-hint": "Maximum time to wait for a response \nfrom the AI model before the request is terminated.",
"timeout-required": "Timeout is required",
"timeout-validation": "Must be from 1 second to 10 minutes.",