@acorex-platform/module-ai-management-client 0.0.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/fesm2022/acorex-platform-module-ai-management-client-agent-registry-tool-configurator-widget-column.component-BHdDG8z0.mjs +44 -0
- package/fesm2022/acorex-platform-module-ai-management-client-agent-registry-tool-configurator-widget-column.component-BHdDG8z0.mjs.map +1 -0
- package/fesm2022/acorex-platform-module-ai-management-client-agent-registry-tool-configurator-widget-edit.component-CMsc3P9O.mjs +259 -0
- package/fesm2022/acorex-platform-module-ai-management-client-agent-registry-tool-configurator-widget-edit.component-CMsc3P9O.mjs.map +1 -0
- package/fesm2022/acorex-platform-module-ai-management-client-assist-delegated-agent-configurator-widget-column.component-CWUhBvpf.mjs +62 -0
- package/fesm2022/acorex-platform-module-ai-management-client-assist-delegated-agent-configurator-widget-column.component-CWUhBvpf.mjs.map +1 -0
- package/fesm2022/acorex-platform-module-ai-management-client-assist-delegated-agent-configurator-widget-edit.component-Dmi8vzfr.mjs +293 -0
- package/fesm2022/acorex-platform-module-ai-management-client-assist-delegated-agent-configurator-widget-edit.component-Dmi8vzfr.mjs.map +1 -0
- package/fesm2022/acorex-platform-module-ai-management-client-contracts.mjs +1038 -0
- package/fesm2022/acorex-platform-module-ai-management-client-contracts.mjs.map +1 -0
- package/fesm2022/acorex-platform-module-ai-management-client-list-ai-command-registry-catalog.query-DHfiKVa2.mjs +106 -0
- package/fesm2022/acorex-platform-module-ai-management-client-list-ai-command-registry-catalog.query-DHfiKVa2.mjs.map +1 -0
- package/fesm2022/acorex-platform-module-ai-management-client-list-ai-query-registry-catalog.query-BX0uU1YX.mjs +107 -0
- package/fesm2022/acorex-platform-module-ai-management-client-list-ai-query-registry-catalog.query-BX0uU1YX.mjs.map +1 -0
- package/fesm2022/acorex-platform-module-ai-management-client-list-output-contract-transcript-segments.query-BQXySOH_.mjs +54 -0
- package/fesm2022/acorex-platform-module-ai-management-client-list-output-contract-transcript-segments.query-BQXySOH_.mjs.map +1 -0
- package/fesm2022/acorex-platform-module-ai-management-client.mjs +3642 -0
- package/fesm2022/acorex-platform-module-ai-management-client.mjs.map +1 -0
- package/package.json +28 -0
- package/types/acorex-platform-module-ai-management-client-contracts.d.ts +896 -0
- package/types/acorex-platform-module-ai-management-client.d.ts +797 -0
|
@@ -0,0 +1,3642 @@
|
|
|
1
|
+
import { AXPSessionService, AXP_PERMISSION_DEFINITION_PROVIDER } from '@acorex-platform/framework-client/auth';
|
|
2
|
+
import { AXPSettingsService, AXPFileStorageService, AXPOutputContractTranscriptSegmentService, AXP_MENU_PROVIDER, AXP_SETTING_DEFINITION_PROVIDER, AXP_OUTPUT_CONTRACT_TRANSCRIPT_SEGMENT_PROVIDER } from '@acorex-platform/framework-client/common';
|
|
3
|
+
import * as i1$1 from '@acorex-platform/framework-client/core';
|
|
4
|
+
import { objectKeyValueTransforms, AXPTagProvider, AXPComponentSlotModule, AXP_MODULE_MANIFEST_PROVIDER, AXP_FEATURE_DEFINITION_PROVIDER, AXP_TAG_PROVIDER, AXP_CONTEXT_DEFINITION_PROVIDER } from '@acorex-platform/framework-client/core';
|
|
5
|
+
import { AXP_WIDGETS_EDITOR_CATEGORY, AXP_WIDGET_DEFINITION_PROVIDER } from '@acorex-platform/framework-client/layout/widget-core';
|
|
6
|
+
import { AXP_NAME_PROPERTY, AXP_DATA_PATH_PROPERTY, AXPWidgetsModule } from '@acorex-platform/framework-client/layout/widgets';
|
|
7
|
+
import { AXPRuntimeModule, provideQuerySetups } from '@acorex-platform/framework-client/runtime';
|
|
8
|
+
import * as i0 from '@angular/core';
|
|
9
|
+
import { inject, Injectable, LOCALE_ID, PLATFORM_ID, input, signal, computed, ViewEncapsulation, ChangeDetectionStrategy, Component, NgModule } from '@angular/core';
|
|
10
|
+
import { ROUTES } from '@angular/router';
|
|
11
|
+
import { RootConfig, AXPAiManagementFeatureKeys, AXPAdministrationPermissionKeys, AXPAiManagementMenuKeys, AXMAiAssistPermissionKeys, AXPAiManagementSettings, isChatPurposeModelRow, readModelPurposes } from '@acorex-platform/module-ai-management-client/contracts';
|
|
12
|
+
import { AXPEntityService } from '@acorex-platform/framework-client/layout/entity';
|
|
13
|
+
import { AXPCommonPermissionGroupKeys } from '@acorex-platform/module-common-client/contracts';
|
|
14
|
+
import * as i7 from '@acorex/core/translation';
|
|
15
|
+
import { resolveMultiLanguageString, AXTranslationService, AXTranslationModule } from '@acorex/core/translation';
|
|
16
|
+
import { AXPWidgetsList, AXPPlatformScope, AXPRegionalSetting, AXPFileStorageStatus } from '@acorex-platform/framework-shared/core';
|
|
17
|
+
import { AXLoadingDialogService } from '@acorex/components/loading-dialog';
|
|
18
|
+
import { assembleAxpAiPlatformRuntimeContext, parseAgentOutputContract, axpAiChatMessageGetText, axpAiAgentOutputContractUsesStructuredFinalize, axpAiAgentOutputContractStructuredRetryCap, axpAiChatTextMessage, resolveAgentOutputContract, AXM_AI_PLATFORM_RUNTIME_CONTEXT_DEFINITIONS, AIMANAGEMENT_CHAT_SYNTHESIZE_SPEECH_COMMAND_KEY, AIMANAGEMENT_CHAT_TRANSCRIBE_SPEECH_COMMAND_KEY, AIMANAGEMENT_CHAT_GENERATE_IMAGE_COMMAND_KEY } from '@acorex-platform/module-ai-management-contracts';
|
|
19
|
+
export * from '@acorex-platform/module-ai-management-contracts';
|
|
20
|
+
import * as i2 from '@acorex/components/button';
|
|
21
|
+
import { AXButtonModule } from '@acorex/components/button';
|
|
22
|
+
import * as i3 from '@acorex/components/decorators';
|
|
23
|
+
import { AXDecoratorModule } from '@acorex/components/decorators';
|
|
24
|
+
import * as i4 from '@acorex/components/dropdown';
|
|
25
|
+
import { AXDropdownModule } from '@acorex/components/dropdown';
|
|
26
|
+
import { AXDropdownButtonModule } from '@acorex/components/dropdown-button';
|
|
27
|
+
import * as i5 from '@acorex/components/text-area';
|
|
28
|
+
import { AXTextAreaModule } from '@acorex/components/text-area';
|
|
29
|
+
import { AXToastService } from '@acorex/components/toast';
|
|
30
|
+
import { AXFileService } from '@acorex/core/file';
|
|
31
|
+
import * as i6 from '@angular/common';
|
|
32
|
+
import { isPlatformBrowser, CommonModule } from '@angular/common';
|
|
33
|
+
import * as i1 from '@angular/forms';
|
|
34
|
+
import { FormsModule } from '@angular/forms';
|
|
35
|
+
import { AXOverlayService } from '@acorex/cdk/overlay';
|
|
36
|
+
import { AXPWidgetGroupEnum } from '@acorex-platform/framework-shared/widget-core';
|
|
37
|
+
|
|
38
|
+
//#region ---- Imports ----
|
|
39
|
+
//#endregion
|
|
40
|
+
//#region ---- Manifest Definition ----
|
|
41
|
+
/**
|
|
42
|
+
* AI Management Module Manifest.
|
|
43
|
+
* Defines module metadata for AI provider and API key management.
|
|
44
|
+
*/
|
|
45
|
+
const AiManagementManifest = {
|
|
46
|
+
name: RootConfig.module.name,
|
|
47
|
+
version: '1.0.0',
|
|
48
|
+
title: RootConfig.module.title,
|
|
49
|
+
icon: RootConfig.module.icon,
|
|
50
|
+
i18n: RootConfig.config.i18n,
|
|
51
|
+
type: 'domain',
|
|
52
|
+
domain: 'ai',
|
|
53
|
+
dependencies: [],
|
|
54
|
+
};
|
|
55
|
+
//#endregion
|
|
56
|
+
|
|
57
|
+
//#region ---- Imports ----
|
|
58
|
+
//#endregion
|
|
59
|
+
//#region ---- Feature Definition Provider ----
|
|
60
|
+
/**
|
|
61
|
+
* AI Management feature definitions.
|
|
62
|
+
* ManageCatalog gates the Intelligence configuration menu (Platform Console edition).
|
|
63
|
+
*/
|
|
64
|
+
class AXMAiManagementFeatureDefinitionProvider {
|
|
65
|
+
async provide(context) {
|
|
66
|
+
const i18n = 'ai-management';
|
|
67
|
+
context.addFeature(AXPAiManagementFeatureKeys.ManageCatalog, {
|
|
68
|
+
title: `@${i18n}:features.manage-catalog.title`,
|
|
69
|
+
description: `@${i18n}:features.manage-catalog.description`,
|
|
70
|
+
defaultValue: false,
|
|
71
|
+
type: 'boolean',
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
//#endregion
|
|
76
|
+
|
|
77
|
+
//#endregion
|
|
78
|
+
//#region ---- Menu Provider ----
|
|
79
|
+
class AXMAiManagementMenuProvider {
|
|
80
|
+
constructor() {
|
|
81
|
+
this.entityService = inject(AXPEntityService);
|
|
82
|
+
}
|
|
83
|
+
async provide(context) {
|
|
84
|
+
const module = RootConfig.module;
|
|
85
|
+
//#region ---- Configuration > AI Management (assists, models) ----
|
|
86
|
+
// Platform Console edition only (same pattern as Identity Providers).
|
|
87
|
+
const aiMenuPolicy = {
|
|
88
|
+
features: [AXPAiManagementFeatureKeys.ManageCatalog],
|
|
89
|
+
permissions: [AXPAdministrationPermissionKeys.MaintainAi],
|
|
90
|
+
};
|
|
91
|
+
const aiManagementConfigMenu = context.find(AXPAiManagementMenuKeys.AiManagementConfig);
|
|
92
|
+
if (!aiManagementConfigMenu.exists) {
|
|
93
|
+
context.find('Common:Menu:Configuration').addItems([
|
|
94
|
+
{
|
|
95
|
+
name: AXPAiManagementMenuKeys.AiManagementConfig,
|
|
96
|
+
text: '@general:menus.ai-management.title',
|
|
97
|
+
icon: module.icon,
|
|
98
|
+
type: 'menu',
|
|
99
|
+
priority: 8941,
|
|
100
|
+
policy: aiMenuPolicy,
|
|
101
|
+
children: [],
|
|
102
|
+
},
|
|
103
|
+
]);
|
|
104
|
+
}
|
|
105
|
+
context.find(AXPAiManagementMenuKeys.AiManagementConfig).addItems([
|
|
106
|
+
{
|
|
107
|
+
name: AXPAiManagementMenuKeys.Agents,
|
|
108
|
+
text: '@ai-management:module.menus.agents.title',
|
|
109
|
+
path: this.entityService.createPath(module.name, RootConfig.entities.agent.name),
|
|
110
|
+
icon: RootConfig.entities.agent.icon,
|
|
111
|
+
priority: -1,
|
|
112
|
+
policy: aiMenuPolicy,
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
name: AXPAiManagementMenuKeys.Assists,
|
|
116
|
+
text: '@ai-management:module.menus.assists.title',
|
|
117
|
+
path: this.entityService.createPath(module.name, RootConfig.entities.assist.name),
|
|
118
|
+
icon: RootConfig.entities.assist.icon,
|
|
119
|
+
priority: 0,
|
|
120
|
+
policy: aiMenuPolicy,
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
name: AXPAiManagementMenuKeys.OpenAiEndpoints,
|
|
124
|
+
text: '@ai-management:module.menus.open-ai-endpoints.title',
|
|
125
|
+
path: this.entityService.createPath(module.name, RootConfig.entities.openAiEndpoint.name),
|
|
126
|
+
icon: RootConfig.entities.openAiEndpoint.icon,
|
|
127
|
+
priority: 2,
|
|
128
|
+
policy: aiMenuPolicy,
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
name: AXPAiManagementMenuKeys.Models,
|
|
132
|
+
text: '@ai-management:module.menus.models.title',
|
|
133
|
+
path: this.entityService.createPath(module.name, RootConfig.entities.model.name),
|
|
134
|
+
icon: RootConfig.entities.model.icon,
|
|
135
|
+
priority: 1,
|
|
136
|
+
policy: aiMenuPolicy,
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
name: AXPAiManagementMenuKeys.Rules,
|
|
140
|
+
text: '@ai-management:module.menus.rules.title',
|
|
141
|
+
path: this.entityService.createPath(module.name, RootConfig.entities.rule.name),
|
|
142
|
+
icon: RootConfig.entities.rule.icon,
|
|
143
|
+
priority: 3,
|
|
144
|
+
policy: aiMenuPolicy,
|
|
145
|
+
},
|
|
146
|
+
]);
|
|
147
|
+
//#endregion
|
|
148
|
+
}
|
|
149
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXMAiManagementMenuProvider, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
150
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXMAiManagementMenuProvider }); }
|
|
151
|
+
}
|
|
152
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXMAiManagementMenuProvider, decorators: [{
|
|
153
|
+
type: Injectable
|
|
154
|
+
}] });
|
|
155
|
+
|
|
156
|
+
//#region ---- Imports ----
|
|
157
|
+
//#endregion
|
|
158
|
+
//#region ---- Catalog load ----
|
|
159
|
+
/**
|
|
160
|
+
* Loads assist catalog rows for dynamic permission definitions.
|
|
161
|
+
* Skips rows without a stable programmatic `name`.
|
|
162
|
+
*/
|
|
163
|
+
async function loadAssistPermissionRows(entityService, localeId) {
|
|
164
|
+
const assistData = entityService
|
|
165
|
+
.withEntity(RootConfig.module.name, RootConfig.entities.assist.name)
|
|
166
|
+
.data();
|
|
167
|
+
const queryResult = await assistData.query({
|
|
168
|
+
skip: 0,
|
|
169
|
+
take: 500,
|
|
170
|
+
sort: [{ field: 'name', dir: 'asc' }],
|
|
171
|
+
});
|
|
172
|
+
const items = (queryResult?.items ?? []);
|
|
173
|
+
const rows = [];
|
|
174
|
+
for (const item of items) {
|
|
175
|
+
const name = item.name?.trim();
|
|
176
|
+
if (!name) {
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
const title = resolveMultiLanguageString(item.title, localeId).trim() || name;
|
|
180
|
+
const descriptionRaw = item.description
|
|
181
|
+
? resolveMultiLanguageString(item.description, localeId).trim()
|
|
182
|
+
: '';
|
|
183
|
+
const description = descriptionRaw || undefined;
|
|
184
|
+
rows.push({ name, title, description });
|
|
185
|
+
}
|
|
186
|
+
return rows.sort((a, b) => a.title.localeCompare(b.title, localeId, { sensitivity: 'base' }));
|
|
187
|
+
}
|
|
188
|
+
//#endregion
|
|
189
|
+
|
|
190
|
+
//#endregion
|
|
191
|
+
//#region ---- Permission Definition Provider ----
|
|
192
|
+
/**
|
|
193
|
+
* Registers AI section capabilities under Administration (including assist children).
|
|
194
|
+
*/
|
|
195
|
+
class AXMPermissionDefinitionProvider {
|
|
196
|
+
constructor() {
|
|
197
|
+
this.entityService = inject(AXPEntityService);
|
|
198
|
+
this.localeId = inject(LOCALE_ID);
|
|
199
|
+
}
|
|
200
|
+
async define(context) {
|
|
201
|
+
const keys = AXPAdministrationPermissionKeys;
|
|
202
|
+
const groups = AXPCommonPermissionGroupKeys;
|
|
203
|
+
const P = '@common:administration.permissions';
|
|
204
|
+
const G = '@common:permission-groups';
|
|
205
|
+
const assistRows = await loadAssistPermissionRows(this.entityService, this.localeId).catch(() => []);
|
|
206
|
+
context.addGroup(groups.Administration.Root, `${G}.administration.title`, `${G}.administration.description`, { order: 120 });
|
|
207
|
+
const groupBuilder = context
|
|
208
|
+
.addGroup(groups.Administration.Ai, `${G}.administration-ai.title`, `${G}.administration-ai.description`, { parent: groups.Administration.Root, order: 50 })
|
|
209
|
+
.addPermission(keys.MaintainAi, `${P}.maintain-ai.title`, `${P}.maintain-ai.description`)
|
|
210
|
+
.endPermission();
|
|
211
|
+
let assistPermissionBuilder = groupBuilder.addPermission(keys.UseAiAssist, `${P}.use-ai-assist.title`, `${P}.use-ai-assist.description`);
|
|
212
|
+
for (const row of assistRows) {
|
|
213
|
+
assistPermissionBuilder = assistPermissionBuilder.addChild(AXMAiAssistPermissionKeys.useForAssist(row.name), row.title, row.description);
|
|
214
|
+
}
|
|
215
|
+
assistPermissionBuilder.endPermission().endGroup();
|
|
216
|
+
}
|
|
217
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXMPermissionDefinitionProvider, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
218
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXMPermissionDefinitionProvider }); }
|
|
219
|
+
}
|
|
220
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXMPermissionDefinitionProvider, decorators: [{
|
|
221
|
+
type: Injectable
|
|
222
|
+
}] });
|
|
223
|
+
|
|
224
|
+
//#region ---- Imports ----
|
|
225
|
+
//#endregion
|
|
226
|
+
//#region ---- Settings Provider ----
|
|
227
|
+
const I18N = '@ai-management:settings';
|
|
228
|
+
class AXMAiManagementSettingProvider {
|
|
229
|
+
async provide(context) {
|
|
230
|
+
context
|
|
231
|
+
.addGroup('ai-settings', `${I18N}.group.nav-title`, `${I18N}.group.description`, 'fa-light fa-robot', `${I18N}.group.title`, 60)
|
|
232
|
+
.addSection('global-ai', `${I18N}.section.global-ai.title`, `${I18N}.section.global-ai.description`)
|
|
233
|
+
.addSetting({
|
|
234
|
+
key: AXPAiManagementSettings.DefaultAssistId,
|
|
235
|
+
title: `${I18N}.default-assist.name.title`,
|
|
236
|
+
scope: 'T',
|
|
237
|
+
overridableAt: [],
|
|
238
|
+
isInherited: true,
|
|
239
|
+
// Persist assist primary key (`id`); demo defaults come from settings seed/overrides.
|
|
240
|
+
valueTransforms: objectKeyValueTransforms('id'),
|
|
241
|
+
widget: {
|
|
242
|
+
type: AXPWidgetsList.Editors.LookupBox,
|
|
243
|
+
options: {
|
|
244
|
+
entity: `${RootConfig.module.name}.${RootConfig.entities.assist.name}`,
|
|
245
|
+
look: 'select',
|
|
246
|
+
multiple: false,
|
|
247
|
+
textField: 'title',
|
|
248
|
+
valueField: 'id',
|
|
249
|
+
hasClearButton: true,
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
description: `${I18N}.default-assist.name.description`,
|
|
253
|
+
})
|
|
254
|
+
.addSetting({
|
|
255
|
+
key: AXPAiManagementSettings.GlobalSystemPrompt,
|
|
256
|
+
title: `${I18N}.global-system-prompt.prefix.title`,
|
|
257
|
+
scope: 'T',
|
|
258
|
+
overridableAt: [],
|
|
259
|
+
isInherited: true,
|
|
260
|
+
defaultValue: undefined,
|
|
261
|
+
widget: {
|
|
262
|
+
type: AXPWidgetsList.Editors.LargeTextBox,
|
|
263
|
+
layout: {
|
|
264
|
+
stacked: true,
|
|
265
|
+
},
|
|
266
|
+
options: {
|
|
267
|
+
rows: 8,
|
|
268
|
+
placeholder: `${I18N}.global-system-prompt.prefix.placeholder`,
|
|
269
|
+
},
|
|
270
|
+
},
|
|
271
|
+
description: `${I18N}.global-system-prompt.prefix.description`,
|
|
272
|
+
})
|
|
273
|
+
.endSection()
|
|
274
|
+
.addSection('gemini', `${I18N}.section.gemini.title`, `${I18N}.section.gemini.description`)
|
|
275
|
+
.addSetting({
|
|
276
|
+
key: AXPAiManagementSettings.Providers.Gemini.ApiKey,
|
|
277
|
+
title: `${I18N}.gemini.api-key.title`,
|
|
278
|
+
scope: 'T',
|
|
279
|
+
overridableAt: [],
|
|
280
|
+
isInherited: true,
|
|
281
|
+
// Server DB at-rest encryption; implies cacheable: false (no client IndexedDB).
|
|
282
|
+
isEncrypted: true,
|
|
283
|
+
defaultValue: undefined,
|
|
284
|
+
widget: {
|
|
285
|
+
type: AXPWidgetsList.Editors.TextBox,
|
|
286
|
+
options: {
|
|
287
|
+
placeholder: `${I18N}.gemini.api-key.placeholder`,
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
description: `${I18N}.gemini.api-key.description`,
|
|
291
|
+
})
|
|
292
|
+
.endSection()
|
|
293
|
+
.endGroup();
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
//#endregion
|
|
297
|
+
|
|
298
|
+
//#region ---- Imports ----
|
|
299
|
+
//#endregion
|
|
300
|
+
/**
|
|
301
|
+
* System tags for the AiModel entity `modelPurposes` field (tag-editor).
|
|
302
|
+
* `title` values are stored as-is on the entity; keep them stable slugs.
|
|
303
|
+
*/
|
|
304
|
+
const AI_MODEL_PURPOSE_TAGS = [
|
|
305
|
+
{
|
|
306
|
+
id: 'ai-model-purpose-chat',
|
|
307
|
+
title: 'chat',
|
|
308
|
+
color: '#0284c7',
|
|
309
|
+
description: 'Chat / text completion',
|
|
310
|
+
scope: 'system',
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
id: 'ai-model-purpose-vision',
|
|
314
|
+
title: 'vision',
|
|
315
|
+
color: '#7c3aed',
|
|
316
|
+
description: 'Multimodal (image input in chat)',
|
|
317
|
+
scope: 'system',
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
id: 'ai-model-purpose-image',
|
|
321
|
+
title: 'image',
|
|
322
|
+
color: '#db2777',
|
|
323
|
+
description: 'Image generation',
|
|
324
|
+
scope: 'system',
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
id: 'ai-model-purpose-speech',
|
|
328
|
+
title: 'speech',
|
|
329
|
+
color: '#16a34a',
|
|
330
|
+
description: 'Speech to text',
|
|
331
|
+
scope: 'system',
|
|
332
|
+
},
|
|
333
|
+
{
|
|
334
|
+
id: 'ai-model-purpose-tts',
|
|
335
|
+
title: 'tts',
|
|
336
|
+
color: '#ea580c',
|
|
337
|
+
description: 'Text to speech',
|
|
338
|
+
scope: 'system',
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
id: 'ai-model-purpose-video',
|
|
342
|
+
title: 'video',
|
|
343
|
+
color: '#0f766e',
|
|
344
|
+
description: 'Video generation',
|
|
345
|
+
scope: 'system',
|
|
346
|
+
},
|
|
347
|
+
];
|
|
348
|
+
//#region ---- Provider ----
|
|
349
|
+
class AXMAiModelPurposeTagProvider extends AXPTagProvider {
|
|
350
|
+
constructor() {
|
|
351
|
+
super(...arguments);
|
|
352
|
+
this.name = 'ai-model-purpose';
|
|
353
|
+
}
|
|
354
|
+
provide() {
|
|
355
|
+
return Promise.resolve(AI_MODEL_PURPOSE_TAGS);
|
|
356
|
+
}
|
|
357
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXMAiModelPurposeTagProvider, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
358
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXMAiModelPurposeTagProvider }); }
|
|
359
|
+
}
|
|
360
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXMAiModelPurposeTagProvider, decorators: [{
|
|
361
|
+
type: Injectable
|
|
362
|
+
}] });
|
|
363
|
+
|
|
364
|
+
//#region ---- Imports ----
|
|
365
|
+
//#endregion
|
|
366
|
+
//#region ---- Abstract token ----
|
|
367
|
+
/**
|
|
368
|
+
* Optional cross-cutting AI assist: programmatic {@link run} and dialog {@link show}.
|
|
369
|
+
* Implemented by {@link AXPAiManagerService}. Consumers outside ai-management should prefer hooks or optional injection.
|
|
370
|
+
*/
|
|
371
|
+
class AXPAiAssistService {
|
|
372
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXPAiAssistService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
373
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXPAiAssistService }); }
|
|
374
|
+
}
|
|
375
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXPAiAssistService, decorators: [{
|
|
376
|
+
type: Injectable
|
|
377
|
+
}] });
|
|
378
|
+
|
|
379
|
+
//#region ---- Imports ----
|
|
380
|
+
//#endregion
|
|
381
|
+
//#region ---- Structured output ----
|
|
382
|
+
/** Default max completion tokens when the AiModel row has no `maxOutputTokens` and the caller omits `maxTokens`. */
|
|
383
|
+
const AXPAiAssistDefaultStructuredMaxTokens = 128_000;
|
|
384
|
+
/** Default maximum follow-up turns after the first model response when validating structured JSON (implementation may use this). */
|
|
385
|
+
const AXPAiAssistDefaultMaxStructuredRetries = 3;
|
|
386
|
+
/**
|
|
387
|
+
* Thrown when {@link AXPAiAssistRunInput.expectStructuredOutput} is true but the assistant output could not be
|
|
388
|
+
* parsed and optionally validated after all attempts.
|
|
389
|
+
*/
|
|
390
|
+
class AXPAiStructuredOutputError extends Error {
|
|
391
|
+
constructor(options) {
|
|
392
|
+
super(options.message, options.cause !== undefined ? { cause: options.cause } : undefined);
|
|
393
|
+
this.name = 'AXPAiStructuredOutputError';
|
|
394
|
+
this.attempts = options.attempts;
|
|
395
|
+
this.lastAssistantText = options.lastAssistantText;
|
|
396
|
+
this.validationMessage = options.validationMessage;
|
|
397
|
+
this.code = options.code;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
//#endregion
|
|
401
|
+
|
|
402
|
+
//#region ---- Imports ----
|
|
403
|
+
//#endregion
|
|
404
|
+
//#region ---- Engine (abstract service) ----
|
|
405
|
+
/**
|
|
406
|
+
* Backend boundary for AI assist runs (chat + tool rounds). Connectivity implementations execute registry
|
|
407
|
+
* commands/queries during the run loop.
|
|
408
|
+
* Wire a concrete implementation via Angular providers (for example `provide: AXPAiEngine` with `useClass`).
|
|
409
|
+
*/
|
|
410
|
+
class AXPAiEngine {
|
|
411
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXPAiEngine, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
412
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXPAiEngine }); }
|
|
413
|
+
}
|
|
414
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXPAiEngine, decorators: [{
|
|
415
|
+
type: Injectable
|
|
416
|
+
}] });
|
|
417
|
+
|
|
418
|
+
//#region ---- Imports ----
|
|
419
|
+
//#endregion
|
|
420
|
+
//#region ---- Message builders (client-only) ----
|
|
421
|
+
function axpAiChatUserMessage(responses) {
|
|
422
|
+
return {
|
|
423
|
+
role: 'user',
|
|
424
|
+
responses,
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
//#endregion
|
|
428
|
+
//#region ---- Transcript read helpers (display only) ----
|
|
429
|
+
function axpAiChatMessageIsDelegatedReflectionExcluded(message) {
|
|
430
|
+
return message.delegatedReflection === true;
|
|
431
|
+
}
|
|
432
|
+
/** Reads typed specialist segments from a normalized {@code agent_result} payload. */
|
|
433
|
+
function axpAiDelegatedAgentResultSegments(payload) {
|
|
434
|
+
if (payload.success !== true || !Array.isArray(payload.data?.responses)) {
|
|
435
|
+
return [];
|
|
436
|
+
}
|
|
437
|
+
return payload.data.responses;
|
|
438
|
+
}
|
|
439
|
+
/** Plain text from specialist segments (text + think) for debug / preview UI. */
|
|
440
|
+
function axpAiDelegatedAgentOutcomeResponsesPlainText(segments) {
|
|
441
|
+
const parts = [];
|
|
442
|
+
for (const s of segments) {
|
|
443
|
+
if (s.type === 'text' || s.type === 'think') {
|
|
444
|
+
const t = s.content.trim();
|
|
445
|
+
if (t.length > 0) {
|
|
446
|
+
parts.push(t);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
return parts.join('\n\n').trim();
|
|
451
|
+
}
|
|
452
|
+
function axpAiChatToolResultBodyJson(message) {
|
|
453
|
+
return axpAiChatToolOrAgentResultBodyJson(message);
|
|
454
|
+
}
|
|
455
|
+
function axpAiChatToolOrAgentResultBodyJson(message) {
|
|
456
|
+
const tr = message.responses.find((r) => r.type === 'tool_result' || r.type === 'agent_result');
|
|
457
|
+
if (!tr || (tr.type !== 'tool_result' && tr.type !== 'agent_result')) {
|
|
458
|
+
return '';
|
|
459
|
+
}
|
|
460
|
+
try {
|
|
461
|
+
return JSON.stringify(tr.content);
|
|
462
|
+
}
|
|
463
|
+
catch {
|
|
464
|
+
return String(tr.content);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
//#endregion
|
|
468
|
+
|
|
469
|
+
//#region ---- Imports ----
|
|
470
|
+
//#endregion
|
|
471
|
+
//#region ---- Builder ----
|
|
472
|
+
/**
|
|
473
|
+
* Builds {@link AXPAiPlatformRuntimeContext} from session and user-scoped regional settings.
|
|
474
|
+
*/
|
|
475
|
+
class AXPAiPlatformRuntimeContextBuilder {
|
|
476
|
+
constructor() {
|
|
477
|
+
//#region ---- Services & Dependencies ----
|
|
478
|
+
this.session = inject(AXPSessionService);
|
|
479
|
+
this.settings = inject(AXPSettingsService);
|
|
480
|
+
this.translation = inject(AXTranslationService);
|
|
481
|
+
}
|
|
482
|
+
//#endregion
|
|
483
|
+
//#region ---- Public API ----
|
|
484
|
+
/**
|
|
485
|
+
* Resolves current user, tenant, application, UI language, and regional preferences from storage.
|
|
486
|
+
*/
|
|
487
|
+
async build() {
|
|
488
|
+
const user = this.session.user;
|
|
489
|
+
const tenant = this.session.tenant;
|
|
490
|
+
const application = this.session.application;
|
|
491
|
+
const userScope = this.settings.scope(AXPPlatformScope.User);
|
|
492
|
+
const [calendarRaw, tzRaw, countryRaw] = await Promise.all([
|
|
493
|
+
userScope.get(AXPRegionalSetting.Calendar),
|
|
494
|
+
userScope.get(AXPRegionalSetting.TimeZone),
|
|
495
|
+
userScope.get(AXPRegionalSetting.Country),
|
|
496
|
+
]);
|
|
497
|
+
const calendarSystem = typeof calendarRaw === 'string' && calendarRaw.trim() ? calendarRaw.trim() : undefined;
|
|
498
|
+
const timeZone = typeof tzRaw === 'string' && tzRaw.trim() ? tzRaw.trim() : undefined;
|
|
499
|
+
const country = typeof countryRaw === 'string' && countryRaw.trim() ? countryRaw.trim() : undefined;
|
|
500
|
+
const locale = this.translation.getActiveLang();
|
|
501
|
+
const now = new Date();
|
|
502
|
+
return assembleAxpAiPlatformRuntimeContext({
|
|
503
|
+
userDisplayName: [user?.title?.trim(), user?.name?.trim()].find((s) => s && s.length > 0),
|
|
504
|
+
tenantTitle: resolveMultiLanguageString(tenant?.title, locale).trim() || tenant?.name?.trim(),
|
|
505
|
+
applicationTitle: resolveMultiLanguageString(application?.title, locale).trim() || application?.name?.trim(),
|
|
506
|
+
locale,
|
|
507
|
+
calendarSystem,
|
|
508
|
+
timeZone,
|
|
509
|
+
country,
|
|
510
|
+
now,
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXPAiPlatformRuntimeContextBuilder, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
514
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXPAiPlatformRuntimeContextBuilder, providedIn: 'root' }); }
|
|
515
|
+
}
|
|
516
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXPAiPlatformRuntimeContextBuilder, decorators: [{
|
|
517
|
+
type: Injectable,
|
|
518
|
+
args: [{ providedIn: 'root' }]
|
|
519
|
+
}] });
|
|
520
|
+
|
|
521
|
+
//#region ---- Imports ----
|
|
522
|
+
//#endregion
|
|
523
|
+
//#region ---- Constants ----
|
|
524
|
+
/** Max length for relaying an https image URL in tool_result when persistence fails (CORS, etc.). */
|
|
525
|
+
const AI_GENERATED_IMAGE_MAX_REMOTE_URL_IN_TOOL = 2048;
|
|
526
|
+
/** refType for files created from in-chat image tool output. */
|
|
527
|
+
const AI_CHAT_GENERATED_IMAGE_REF_TYPE = `${RootConfig.module.name}.AiChatGeneratedImage`;
|
|
528
|
+
/** refType for files created from in-chat text-to-speech tool output. */
|
|
529
|
+
const AI_CHAT_GENERATED_SPEECH_REF_TYPE = `${RootConfig.module.name}.AiChatGeneratedSpeech`;
|
|
530
|
+
/** refType for user-attached images in AI chat; {@link persistAiChatAttachmentImage} uses a batch ref id. */
|
|
531
|
+
const AI_CHAT_ATTACHMENT_REF_TYPE = `${RootConfig.module.name}.AiChatAttachment`;
|
|
532
|
+
/** File storage category for user chat image attachments. */
|
|
533
|
+
const AI_CHAT_ATTACHMENT_CATEGORY = 'ai-chat-attachment';
|
|
534
|
+
//#endregion
|
|
535
|
+
//#region ---- URL / File conversion ----
|
|
536
|
+
/**
|
|
537
|
+
* Builds a File from a vendor image URL (https) or data URL (base64 image).
|
|
538
|
+
*/
|
|
539
|
+
async function blobOrFileFromImageResult(imageUrl, defaultName = 'ai-generated.png') {
|
|
540
|
+
const trimmed = imageUrl.trim();
|
|
541
|
+
if (!trimmed) {
|
|
542
|
+
throw new Error('Image URL is empty.');
|
|
543
|
+
}
|
|
544
|
+
if (trimmed.startsWith('data:')) {
|
|
545
|
+
const res = await fetch(trimmed);
|
|
546
|
+
if (!res.ok) {
|
|
547
|
+
throw new Error(`Failed to read data URL (${res.status}).`);
|
|
548
|
+
}
|
|
549
|
+
const blob = await res.blob();
|
|
550
|
+
const mime = blob.type || parseDataUrlMime(trimmed) || 'image/png';
|
|
551
|
+
return new File([blob], defaultName, { type: mime });
|
|
552
|
+
}
|
|
553
|
+
if (trimmed.startsWith('https://') || trimmed.startsWith('http://')) {
|
|
554
|
+
const res = await fetch(trimmed);
|
|
555
|
+
if (!res.ok) {
|
|
556
|
+
throw new Error(`Failed to fetch image (${res.status}).`);
|
|
557
|
+
}
|
|
558
|
+
const blob = await res.blob();
|
|
559
|
+
const mime = blob.type ||
|
|
560
|
+
res.headers.get('Content-Type')?.split(';')[0]?.trim() ||
|
|
561
|
+
'image/png';
|
|
562
|
+
const ext = mime.includes('jpeg') || mime.includes('jpg') ? 'jpg' : mime.includes('webp') ? 'webp' : 'png';
|
|
563
|
+
return new File([blob], defaultName.replace(/\.[^.]+$/, '') + '.' + ext, { type: mime });
|
|
564
|
+
}
|
|
565
|
+
throw new Error('Unsupported image URL scheme (expected https: or data:).');
|
|
566
|
+
}
|
|
567
|
+
function parseDataUrlMime(dataUrl) {
|
|
568
|
+
const m = /^data:([^;,]+)[;,]/.exec(dataUrl);
|
|
569
|
+
return m?.[1]?.trim();
|
|
570
|
+
}
|
|
571
|
+
//#endregion
|
|
572
|
+
//#region ---- Persistence ----
|
|
573
|
+
/**
|
|
574
|
+
* Saves the file and commits it so it appears in storage queries as committed.
|
|
575
|
+
*/
|
|
576
|
+
async function persistAiGeneratedImage(fileStorage, file, ref) {
|
|
577
|
+
const info = await fileStorage.save({
|
|
578
|
+
file,
|
|
579
|
+
name: file.name,
|
|
580
|
+
refId: ref.refId,
|
|
581
|
+
refType: ref.refType,
|
|
582
|
+
category: 'ai-generated-image',
|
|
583
|
+
status: AXPFileStorageStatus.Temporary,
|
|
584
|
+
});
|
|
585
|
+
const fileId = info.fileId?.trim();
|
|
586
|
+
if (!fileId) {
|
|
587
|
+
throw new Error('File storage did not return a fileId.');
|
|
588
|
+
}
|
|
589
|
+
await fileStorage.commit(fileId);
|
|
590
|
+
return { fileId };
|
|
591
|
+
}
|
|
592
|
+
/**
|
|
593
|
+
* Persists a user-selected chat image; refId groups files uploaded in the same composer batch.
|
|
594
|
+
*/
|
|
595
|
+
async function persistAiChatAttachmentImage(fileStorage, file, refId) {
|
|
596
|
+
const trimmed = refId.trim();
|
|
597
|
+
if (!trimmed) {
|
|
598
|
+
throw new Error('refId is required for chat attachment persistence.');
|
|
599
|
+
}
|
|
600
|
+
const info = await fileStorage.save({
|
|
601
|
+
file,
|
|
602
|
+
name: file.name,
|
|
603
|
+
refId: trimmed,
|
|
604
|
+
refType: AI_CHAT_ATTACHMENT_REF_TYPE,
|
|
605
|
+
category: AI_CHAT_ATTACHMENT_CATEGORY,
|
|
606
|
+
status: AXPFileStorageStatus.Temporary,
|
|
607
|
+
});
|
|
608
|
+
const fileId = info.fileId?.trim();
|
|
609
|
+
if (!fileId) {
|
|
610
|
+
throw new Error('File storage did not return a fileId.');
|
|
611
|
+
}
|
|
612
|
+
await fileStorage.commit(fileId);
|
|
613
|
+
return { fileId };
|
|
614
|
+
}
|
|
615
|
+
/**
|
|
616
|
+
* Saves synthesized speech bytes and commits so the file id can be replayed in chat.
|
|
617
|
+
*/
|
|
618
|
+
async function persistAiGeneratedSpeech(fileStorage, file, ref) {
|
|
619
|
+
const info = await fileStorage.save({
|
|
620
|
+
file,
|
|
621
|
+
name: file.name,
|
|
622
|
+
refId: ref.refId,
|
|
623
|
+
refType: ref.refType,
|
|
624
|
+
category: 'ai-generated-speech',
|
|
625
|
+
status: AXPFileStorageStatus.Temporary,
|
|
626
|
+
});
|
|
627
|
+
const fileId = info.fileId?.trim();
|
|
628
|
+
if (!fileId) {
|
|
629
|
+
throw new Error('File storage did not return a fileId.');
|
|
630
|
+
}
|
|
631
|
+
await fileStorage.commit(fileId);
|
|
632
|
+
return { fileId };
|
|
633
|
+
}
|
|
634
|
+
//#endregion
|
|
635
|
+
|
|
636
|
+
//#region ---- Limits ----
|
|
637
|
+
/** Max image file size for AI chat / assist composer attachments (bytes). */
|
|
638
|
+
const AXM_AI_CHAT_COMPOSER_MAX_IMAGE_BYTES = 20 * 1024 * 1024;
|
|
639
|
+
/** Max images per composer batch (same cap as chat page). */
|
|
640
|
+
const AXM_AI_CHAT_COMPOSER_MAX_IMAGES_PER_TURN = 20;
|
|
641
|
+
/** Max audio file size (bytes). */
|
|
642
|
+
const AXM_AI_CHAT_COMPOSER_MAX_AUDIO_BYTES = 25 * 1024 * 1024;
|
|
643
|
+
/** Max audio files per composer batch. */
|
|
644
|
+
const AXM_AI_CHAT_COMPOSER_MAX_AUDIO_PER_TURN = 5;
|
|
645
|
+
/** Max document file size for PDF / text / office-style attachments (bytes). */
|
|
646
|
+
const AXM_AI_CHAT_COMPOSER_MAX_DOCUMENT_BYTES = 35 * 1024 * 1024;
|
|
647
|
+
/** Max document files per composer batch. */
|
|
648
|
+
const AXM_AI_CHAT_COMPOSER_MAX_DOCUMENTS_PER_TURN = 10;
|
|
649
|
+
//#endregion
|
|
650
|
+
//#region ---- Patterns ----
|
|
651
|
+
const CHAT_IMAGE_NAME_EXT_PATTERN = /\.(png|jpe?g|gif|webp|bmp|svg|ico|heic|heif)$/i;
|
|
652
|
+
const CHAT_AUDIO_NAME_EXT_PATTERN = /\.(mp3|m4a|aac|wav|webm|ogg|opus|flac)$/i;
|
|
653
|
+
const CHAT_DOCUMENT_NAME_EXT_PATTERN = /\.(pdf|txt|md|markdown|csv|tsv|json|jsonl|xml|html?|yaml|yml|log|rtf|docx?|xlsx?|pptx?|odt|ods|odp)$/i;
|
|
654
|
+
//#endregion
|
|
655
|
+
//#region ---- Public API ----
|
|
656
|
+
/**
|
|
657
|
+
* Returns true for normal `image/*` types and for empty/octet-stream when the file name looks like an image.
|
|
658
|
+
*/
|
|
659
|
+
function axmIsLikelyChatImageFile(file) {
|
|
660
|
+
const t = file.type?.trim() ?? '';
|
|
661
|
+
if (t.startsWith('image/')) {
|
|
662
|
+
return true;
|
|
663
|
+
}
|
|
664
|
+
if (t === 'application/octet-stream' || t === '') {
|
|
665
|
+
return CHAT_IMAGE_NAME_EXT_PATTERN.test(file.name);
|
|
666
|
+
}
|
|
667
|
+
return false;
|
|
668
|
+
}
|
|
669
|
+
/**
|
|
670
|
+
* Returns true for normal `audio/*` types and for empty/octet-stream when the file name looks like audio.
|
|
671
|
+
*/
|
|
672
|
+
function axmIsLikelyChatAudioFile(file) {
|
|
673
|
+
const t = file.type?.trim() ?? '';
|
|
674
|
+
if (t.startsWith('audio/')) {
|
|
675
|
+
return true;
|
|
676
|
+
}
|
|
677
|
+
if (t === 'application/octet-stream' || t === '') {
|
|
678
|
+
return CHAT_AUDIO_NAME_EXT_PATTERN.test(file.name);
|
|
679
|
+
}
|
|
680
|
+
return false;
|
|
681
|
+
}
|
|
682
|
+
/**
|
|
683
|
+
* Returns true for PDF, plain text, common code/markup, CSV, and typical Office/OpenDocument uploads.
|
|
684
|
+
*/
|
|
685
|
+
function axmIsLikelyChatDocumentFile(file) {
|
|
686
|
+
const t = file.type?.trim().toLowerCase() ?? '';
|
|
687
|
+
if (t === 'application/pdf' || t.startsWith('text/')) {
|
|
688
|
+
return true;
|
|
689
|
+
}
|
|
690
|
+
if (t === 'application/json' ||
|
|
691
|
+
t === 'application/xml' ||
|
|
692
|
+
t === 'application/javascript' ||
|
|
693
|
+
t === 'application/typescript' ||
|
|
694
|
+
t === 'text/csv' ||
|
|
695
|
+
t === 'application/csv' ||
|
|
696
|
+
t === 'text/tsv' ||
|
|
697
|
+
t === 'application/rtf' ||
|
|
698
|
+
t === 'text/rtf' ||
|
|
699
|
+
t.includes('wordprocessingml') ||
|
|
700
|
+
t.includes('spreadsheetml') ||
|
|
701
|
+
t.includes('presentationml') ||
|
|
702
|
+
t.includes('msword') ||
|
|
703
|
+
t.includes('ms-excel') ||
|
|
704
|
+
t.includes('ms-powerpoint') ||
|
|
705
|
+
t.includes('opendocument')) {
|
|
706
|
+
return true;
|
|
707
|
+
}
|
|
708
|
+
if (t === 'application/octet-stream' || t === '') {
|
|
709
|
+
return CHAT_DOCUMENT_NAME_EXT_PATTERN.test(file.name);
|
|
710
|
+
}
|
|
711
|
+
return false;
|
|
712
|
+
}
|
|
713
|
+
/**
|
|
714
|
+
* MIME for storage/API when the browser omits or misreports `file.type` (images and common audio extensions).
|
|
715
|
+
*/
|
|
716
|
+
function axmMimeTypeForChatAttachment(file) {
|
|
717
|
+
const t = file.type?.trim() ?? '';
|
|
718
|
+
if (t.startsWith('image/')) {
|
|
719
|
+
return t;
|
|
720
|
+
}
|
|
721
|
+
if (t.startsWith('audio/')) {
|
|
722
|
+
return t;
|
|
723
|
+
}
|
|
724
|
+
if (t.startsWith('text/') || t === 'application/pdf' || t === 'application/json' || t === 'application/xml') {
|
|
725
|
+
return t;
|
|
726
|
+
}
|
|
727
|
+
const m = /\.([^.]+)$/i.exec(file.name);
|
|
728
|
+
const ext = (m?.[1] ?? '').toLowerCase();
|
|
729
|
+
const imageByExt = {
|
|
730
|
+
png: 'image/png',
|
|
731
|
+
jpg: 'image/jpeg',
|
|
732
|
+
jpeg: 'image/jpeg',
|
|
733
|
+
gif: 'image/gif',
|
|
734
|
+
webp: 'image/webp',
|
|
735
|
+
bmp: 'image/bmp',
|
|
736
|
+
svg: 'image/svg+xml',
|
|
737
|
+
ico: 'image/x-icon',
|
|
738
|
+
heic: 'image/heic',
|
|
739
|
+
heif: 'image/heif',
|
|
740
|
+
};
|
|
741
|
+
const audioByExt = {
|
|
742
|
+
mp3: 'audio/mpeg',
|
|
743
|
+
m4a: 'audio/mp4',
|
|
744
|
+
aac: 'audio/aac',
|
|
745
|
+
wav: 'audio/wav',
|
|
746
|
+
webm: 'audio/webm',
|
|
747
|
+
ogg: 'audio/ogg',
|
|
748
|
+
opus: 'audio/opus',
|
|
749
|
+
flac: 'audio/flac',
|
|
750
|
+
};
|
|
751
|
+
const docByExt = {
|
|
752
|
+
pdf: 'application/pdf',
|
|
753
|
+
txt: 'text/plain',
|
|
754
|
+
md: 'text/markdown',
|
|
755
|
+
markdown: 'text/markdown',
|
|
756
|
+
csv: 'text/csv',
|
|
757
|
+
tsv: 'text/tab-separated-values',
|
|
758
|
+
json: 'application/json',
|
|
759
|
+
jsonl: 'application/jsonl',
|
|
760
|
+
xml: 'application/xml',
|
|
761
|
+
html: 'text/html',
|
|
762
|
+
htm: 'text/html',
|
|
763
|
+
yaml: 'text/yaml',
|
|
764
|
+
yml: 'text/yaml',
|
|
765
|
+
log: 'text/plain',
|
|
766
|
+
rtf: 'application/rtf',
|
|
767
|
+
doc: 'application/msword',
|
|
768
|
+
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
769
|
+
xls: 'application/vnd.ms-excel',
|
|
770
|
+
xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
771
|
+
ppt: 'application/vnd.ms-powerpoint',
|
|
772
|
+
pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
773
|
+
odt: 'application/vnd.oasis.opendocument.text',
|
|
774
|
+
ods: 'application/vnd.oasis.opendocument.spreadsheet',
|
|
775
|
+
odp: 'application/vnd.oasis.opendocument.presentation',
|
|
776
|
+
};
|
|
777
|
+
return imageByExt[ext] ?? audioByExt[ext] ?? docByExt[ext] ?? 'application/octet-stream';
|
|
778
|
+
}
|
|
779
|
+
//#endregion
|
|
780
|
+
|
|
781
|
+
//#region ---- Imports ----
|
|
782
|
+
//#endregion
|
|
783
|
+
//#region ---- Component ----
|
|
784
|
+
/**
|
|
785
|
+
* Floating prompt UI for AI assist: gradient chrome, inner composer, and image/audio/document attachments. Target assist/agent comes from optional initial ids on open.
|
|
786
|
+
*/
|
|
787
|
+
class AXMAiAssistPromptBoxComponent {
|
|
788
|
+
constructor() {
|
|
789
|
+
//#region ---- Services & Dependencies ----
|
|
790
|
+
this.toastService = inject(AXToastService);
|
|
791
|
+
this.translation = inject(AXTranslationService);
|
|
792
|
+
this.fileStorage = inject(AXPFileStorageService);
|
|
793
|
+
this.fileService = inject(AXFileService);
|
|
794
|
+
this.platformId = inject(PLATFORM_ID);
|
|
795
|
+
//#endregion
|
|
796
|
+
//#region ---- Inputs ----
|
|
797
|
+
this.title = input.required(/* @ts-ignore */
|
|
798
|
+
...(ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []));
|
|
799
|
+
this.placeholder = input('', /* @ts-ignore */
|
|
800
|
+
...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
|
|
801
|
+
this.initialPrompt = input('', /* @ts-ignore */
|
|
802
|
+
...(ngDevMode ? [{ debugName: "initialPrompt" }] : /* istanbul ignore next */ []));
|
|
803
|
+
this.contentMaxWidthPx = input(680, /* @ts-ignore */
|
|
804
|
+
...(ngDevMode ? [{ debugName: "contentMaxWidthPx" }] : /* istanbul ignore next */ []));
|
|
805
|
+
this.initialAssistId = input(undefined, /* @ts-ignore */
|
|
806
|
+
...(ngDevMode ? [{ debugName: "initialAssistId" }] : /* istanbul ignore next */ []));
|
|
807
|
+
this.initialDirectAgentId = input(undefined, /* @ts-ignore */
|
|
808
|
+
...(ngDevMode ? [{ debugName: "initialDirectAgentId" }] : /* istanbul ignore next */ []));
|
|
809
|
+
this.runPrompt = input.required(/* @ts-ignore */
|
|
810
|
+
...(ngDevMode ? [{ debugName: "runPrompt" }] : /* istanbul ignore next */ []));
|
|
811
|
+
this.requestClose = input.required(/* @ts-ignore */
|
|
812
|
+
...(ngDevMode ? [{ debugName: "requestClose" }] : /* istanbul ignore next */ []));
|
|
813
|
+
//#endregion
|
|
814
|
+
//#region ---- State ----
|
|
815
|
+
this.promptDraft = signal('', /* @ts-ignore */
|
|
816
|
+
...(ngDevMode ? [{ debugName: "promptDraft" }] : /* istanbul ignore next */ []));
|
|
817
|
+
this.busy = signal(false, /* @ts-ignore */
|
|
818
|
+
...(ngDevMode ? [{ debugName: "busy" }] : /* istanbul ignore next */ []));
|
|
819
|
+
this.isComposerSaving = signal(false, /* @ts-ignore */
|
|
820
|
+
...(ngDevMode ? [{ debugName: "isComposerSaving" }] : /* istanbul ignore next */ []));
|
|
821
|
+
this.draftAttachmentGroupId = signal(null, /* @ts-ignore */
|
|
822
|
+
...(ngDevMode ? [{ debugName: "draftAttachmentGroupId" }] : /* istanbul ignore next */ []));
|
|
823
|
+
this.draftComposerImages = signal([], /* @ts-ignore */
|
|
824
|
+
...(ngDevMode ? [{ debugName: "draftComposerImages" }] : /* istanbul ignore next */ []));
|
|
825
|
+
this.draftComposerAudios = signal([], /* @ts-ignore */
|
|
826
|
+
...(ngDevMode ? [{ debugName: "draftComposerAudios" }] : /* istanbul ignore next */ []));
|
|
827
|
+
this.draftComposerFiles = signal([], /* @ts-ignore */
|
|
828
|
+
...(ngDevMode ? [{ debugName: "draftComposerFiles" }] : /* istanbul ignore next */ []));
|
|
829
|
+
this.assistTargetKey = signal('', /* @ts-ignore */
|
|
830
|
+
...(ngDevMode ? [{ debugName: "assistTargetKey" }] : /* istanbul ignore next */ []));
|
|
831
|
+
//#endregion
|
|
832
|
+
//#region ---- Computed ----
|
|
833
|
+
this.canSend = computed(() => {
|
|
834
|
+
if (this.busy() || this.isComposerSaving()) {
|
|
835
|
+
return false;
|
|
836
|
+
}
|
|
837
|
+
const textOk = this.promptDraft().trim().length > 0;
|
|
838
|
+
const draftImg = this.draftComposerImages();
|
|
839
|
+
const draftAud = this.draftComposerAudios();
|
|
840
|
+
const draftFiles = this.draftComposerFiles();
|
|
841
|
+
const allImagesSaved = draftImg.length === 0 || draftImg.every((s) => Boolean(s.fileId?.trim()));
|
|
842
|
+
const allAudiosSaved = draftAud.length === 0 || draftAud.every((s) => Boolean(s.fileId?.trim()));
|
|
843
|
+
const allFilesSaved = draftFiles.length === 0 || draftFiles.every((s) => Boolean(s.fileId?.trim()));
|
|
844
|
+
if (!allImagesSaved || !allAudiosSaved || !allFilesSaved) {
|
|
845
|
+
return false;
|
|
846
|
+
}
|
|
847
|
+
if (draftImg.length === 0 && draftAud.length === 0 && draftFiles.length === 0) {
|
|
848
|
+
return textOk;
|
|
849
|
+
}
|
|
850
|
+
return textOk || draftImg.length > 0 || draftAud.length > 0 || draftFiles.length > 0;
|
|
851
|
+
}, /* @ts-ignore */
|
|
852
|
+
...(ngDevMode ? [{ debugName: "canSend" }] : /* istanbul ignore next */ []));
|
|
853
|
+
}
|
|
854
|
+
//#endregion
|
|
855
|
+
//#region ---- Lifecycle ----
|
|
856
|
+
async ngOnInit() {
|
|
857
|
+
this.promptDraft.set(this.initialPrompt().trim());
|
|
858
|
+
const d = this.initialDirectAgentId()?.trim();
|
|
859
|
+
const a = this.initialAssistId()?.trim();
|
|
860
|
+
if (d) {
|
|
861
|
+
this.assistTargetKey.set(`agent:${d}`);
|
|
862
|
+
}
|
|
863
|
+
else if (a) {
|
|
864
|
+
this.assistTargetKey.set(`assist:${a}`);
|
|
865
|
+
}
|
|
866
|
+
else {
|
|
867
|
+
this.assistTargetKey.set('');
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
ngOnDestroy() {
|
|
871
|
+
for (const s of this.draftComposerImages()) {
|
|
872
|
+
URL.revokeObjectURL(s.previewUrl);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
//#endregion
|
|
876
|
+
//#region ---- UI Handlers ----
|
|
877
|
+
onPromptDraftChange(value) {
|
|
878
|
+
this.promptDraft.set((value ?? '').toString());
|
|
879
|
+
}
|
|
880
|
+
/**
|
|
881
|
+
* Enter sends; Shift+Enter inserts a newline. IME composition Enter is left unchanged.
|
|
882
|
+
*/
|
|
883
|
+
onPromptKeydown(event) {
|
|
884
|
+
if (event.key !== 'Enter' || event.shiftKey || event.isComposing) {
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
887
|
+
event.preventDefault();
|
|
888
|
+
void this.onSend();
|
|
889
|
+
}
|
|
890
|
+
onCancel() {
|
|
891
|
+
this.requestClose()(null);
|
|
892
|
+
}
|
|
893
|
+
onMenuCancel(panel) {
|
|
894
|
+
panel.close();
|
|
895
|
+
if (this.busy()) {
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
this.onCancel();
|
|
899
|
+
}
|
|
900
|
+
/**
|
|
901
|
+
* Dismisses the prompt box like Cancel (disabled while a run is in flight, same as footer Cancel).
|
|
902
|
+
*/
|
|
903
|
+
onDocumentEscape() {
|
|
904
|
+
if (!isPlatformBrowser(this.platformId) || this.busy()) {
|
|
905
|
+
return;
|
|
906
|
+
}
|
|
907
|
+
this.onCancel();
|
|
908
|
+
}
|
|
909
|
+
openImagePicker() {
|
|
910
|
+
void this.pickComposerImages();
|
|
911
|
+
}
|
|
912
|
+
openAudioPicker() {
|
|
913
|
+
void this.pickComposerAudios();
|
|
914
|
+
}
|
|
915
|
+
openDocumentPicker() {
|
|
916
|
+
void this.pickComposerDocuments();
|
|
917
|
+
}
|
|
918
|
+
async onSend() {
|
|
919
|
+
if (!this.canSend()) {
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
922
|
+
const text = this.promptDraft().trim();
|
|
923
|
+
const draftImg = this.draftComposerImages();
|
|
924
|
+
const draftAud = this.draftComposerAudios();
|
|
925
|
+
const draftFiles = this.draftComposerFiles();
|
|
926
|
+
for (const s of draftImg) {
|
|
927
|
+
if (!s.fileId?.trim()) {
|
|
928
|
+
const errTitle = await this.translation.translateAsync('@general:messages.error.title');
|
|
929
|
+
const msg = await this.translation.translateAsync('@ai-management:chat.images-still-saving');
|
|
930
|
+
await this.toastService.show({ color: 'warning', title: errTitle, content: msg });
|
|
931
|
+
return;
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
for (const s of draftAud) {
|
|
935
|
+
if (!s.fileId?.trim()) {
|
|
936
|
+
const errTitle = await this.translation.translateAsync('@general:messages.error.title');
|
|
937
|
+
const msg = await this.translation.translateAsync('@ai-management:chat.audio-still-saving');
|
|
938
|
+
await this.toastService.show({ color: 'warning', title: errTitle, content: msg });
|
|
939
|
+
return;
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
for (const s of draftFiles) {
|
|
943
|
+
if (!s.fileId?.trim()) {
|
|
944
|
+
const errTitle = await this.translation.translateAsync('@general:messages.error.title');
|
|
945
|
+
const msg = await this.translation.translateAsync('@ai-management:chat.documents-still-saving');
|
|
946
|
+
await this.toastService.show({ color: 'warning', title: errTitle, content: msg });
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
const key = this.assistTargetKey().trim();
|
|
951
|
+
let assistId;
|
|
952
|
+
let directAgentId;
|
|
953
|
+
if (key.startsWith('assist:')) {
|
|
954
|
+
assistId = key.slice('assist:'.length).trim() || undefined;
|
|
955
|
+
}
|
|
956
|
+
else if (key.startsWith('agent:')) {
|
|
957
|
+
directAgentId = key.slice('agent:'.length).trim() || undefined;
|
|
958
|
+
}
|
|
959
|
+
const userMediaAttachments = [
|
|
960
|
+
...draftImg.map((s) => ({
|
|
961
|
+
kind: 'image',
|
|
962
|
+
fileId: s.fileId.trim(),
|
|
963
|
+
mimeType: s.mimeType.trim(),
|
|
964
|
+
...(s.name?.trim() ? { name: s.name.trim() } : {}),
|
|
965
|
+
})),
|
|
966
|
+
...draftAud.map((s) => ({
|
|
967
|
+
kind: 'audio',
|
|
968
|
+
fileId: s.fileId.trim(),
|
|
969
|
+
mimeType: s.mimeType.trim(),
|
|
970
|
+
...(s.name?.trim() ? { name: s.name.trim() } : {}),
|
|
971
|
+
})),
|
|
972
|
+
...draftFiles.map((s) => ({
|
|
973
|
+
kind: 'file',
|
|
974
|
+
fileId: s.fileId.trim(),
|
|
975
|
+
mimeType: s.mimeType.trim(),
|
|
976
|
+
...(s.name?.trim() ? { name: s.name.trim() } : {}),
|
|
977
|
+
})),
|
|
978
|
+
];
|
|
979
|
+
const payload = {
|
|
980
|
+
prompt: text,
|
|
981
|
+
assistId,
|
|
982
|
+
directAgentId,
|
|
983
|
+
userMediaAttachments,
|
|
984
|
+
};
|
|
985
|
+
this.busy.set(true);
|
|
986
|
+
try {
|
|
987
|
+
const result = await this.runPrompt()(payload);
|
|
988
|
+
this.requestClose()(result);
|
|
989
|
+
}
|
|
990
|
+
catch (err) {
|
|
991
|
+
const errTitle = await this.translation.translateAsync('@general:messages.error.title');
|
|
992
|
+
const fallback = await this.translation.translateAsync('@ai-management:assist-popup.run-error');
|
|
993
|
+
const content = err instanceof Error ? err.message : fallback;
|
|
994
|
+
await this.toastService.show({ color: 'danger', title: errTitle, content });
|
|
995
|
+
}
|
|
996
|
+
finally {
|
|
997
|
+
this.busy.set(false);
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
removeDraftImage(clientId) {
|
|
1001
|
+
const id = clientId.trim();
|
|
1002
|
+
if (!id) {
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
this.draftComposerImages.update((prev) => {
|
|
1006
|
+
const slot = prev.find((s) => s.clientId === id);
|
|
1007
|
+
if (slot) {
|
|
1008
|
+
URL.revokeObjectURL(slot.previewUrl);
|
|
1009
|
+
}
|
|
1010
|
+
const next = prev.filter((s) => s.clientId !== id);
|
|
1011
|
+
if (next.length === 0 &&
|
|
1012
|
+
this.draftComposerAudios().length === 0 &&
|
|
1013
|
+
this.draftComposerFiles().length === 0) {
|
|
1014
|
+
this.draftAttachmentGroupId.set(null);
|
|
1015
|
+
}
|
|
1016
|
+
return next;
|
|
1017
|
+
});
|
|
1018
|
+
}
|
|
1019
|
+
removeDraftAudio(clientId) {
|
|
1020
|
+
const id = clientId.trim();
|
|
1021
|
+
if (!id) {
|
|
1022
|
+
return;
|
|
1023
|
+
}
|
|
1024
|
+
this.draftComposerAudios.update((prev) => {
|
|
1025
|
+
const next = prev.filter((s) => s.clientId !== id);
|
|
1026
|
+
if (next.length === 0 &&
|
|
1027
|
+
this.draftComposerImages().length === 0 &&
|
|
1028
|
+
this.draftComposerFiles().length === 0) {
|
|
1029
|
+
this.draftAttachmentGroupId.set(null);
|
|
1030
|
+
}
|
|
1031
|
+
return next;
|
|
1032
|
+
});
|
|
1033
|
+
}
|
|
1034
|
+
removeDraftFile(clientId) {
|
|
1035
|
+
const id = clientId.trim();
|
|
1036
|
+
if (!id) {
|
|
1037
|
+
return;
|
|
1038
|
+
}
|
|
1039
|
+
this.draftComposerFiles.update((prev) => {
|
|
1040
|
+
const next = prev.filter((s) => s.clientId !== id);
|
|
1041
|
+
if (next.length === 0 &&
|
|
1042
|
+
this.draftComposerImages().length === 0 &&
|
|
1043
|
+
this.draftComposerAudios().length === 0) {
|
|
1044
|
+
this.draftAttachmentGroupId.set(null);
|
|
1045
|
+
}
|
|
1046
|
+
return next;
|
|
1047
|
+
});
|
|
1048
|
+
}
|
|
1049
|
+
//#endregion
|
|
1050
|
+
//#region ---- Composer media (mirrors chat page) ----
|
|
1051
|
+
async pickComposerImages() {
|
|
1052
|
+
if (!isPlatformBrowser(this.platformId)) {
|
|
1053
|
+
return;
|
|
1054
|
+
}
|
|
1055
|
+
if (this.busy() || this.isComposerSaving()) {
|
|
1056
|
+
return;
|
|
1057
|
+
}
|
|
1058
|
+
try {
|
|
1059
|
+
const files = await this.fileService.choose({
|
|
1060
|
+
multiple: true,
|
|
1061
|
+
accept: 'image/*',
|
|
1062
|
+
});
|
|
1063
|
+
if (files.length === 0) {
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
1066
|
+
await this.processComposerImageFiles(files);
|
|
1067
|
+
}
|
|
1068
|
+
catch {
|
|
1069
|
+
/* user cancelled */
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
async pickComposerAudios() {
|
|
1073
|
+
if (!isPlatformBrowser(this.platformId)) {
|
|
1074
|
+
return;
|
|
1075
|
+
}
|
|
1076
|
+
if (this.busy() || this.isComposerSaving()) {
|
|
1077
|
+
return;
|
|
1078
|
+
}
|
|
1079
|
+
try {
|
|
1080
|
+
const files = await this.fileService.choose({
|
|
1081
|
+
multiple: true,
|
|
1082
|
+
accept: 'audio/*',
|
|
1083
|
+
});
|
|
1084
|
+
if (files.length === 0) {
|
|
1085
|
+
return;
|
|
1086
|
+
}
|
|
1087
|
+
await this.processComposerAudioFiles(files);
|
|
1088
|
+
}
|
|
1089
|
+
catch {
|
|
1090
|
+
/* user cancelled */
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
async pickComposerDocuments() {
|
|
1094
|
+
if (!isPlatformBrowser(this.platformId)) {
|
|
1095
|
+
return;
|
|
1096
|
+
}
|
|
1097
|
+
if (this.busy() || this.isComposerSaving()) {
|
|
1098
|
+
return;
|
|
1099
|
+
}
|
|
1100
|
+
try {
|
|
1101
|
+
const files = await this.fileService.choose({
|
|
1102
|
+
multiple: true,
|
|
1103
|
+
accept: '.pdf,.txt,.md,.csv,.tsv,.json,.jsonl,.xml,.html,.htm,.yaml,.yml,.log,.rtf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.odt,.ods,.odp,application/pdf,text/*,application/json',
|
|
1104
|
+
});
|
|
1105
|
+
if (files.length === 0) {
|
|
1106
|
+
return;
|
|
1107
|
+
}
|
|
1108
|
+
await this.processComposerDocumentFiles(files);
|
|
1109
|
+
}
|
|
1110
|
+
catch {
|
|
1111
|
+
/* user cancelled */
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
async processComposerImageFiles(files) {
|
|
1115
|
+
if (!isPlatformBrowser(this.platformId)) {
|
|
1116
|
+
return;
|
|
1117
|
+
}
|
|
1118
|
+
let groupId = this.draftAttachmentGroupId();
|
|
1119
|
+
if (!groupId?.trim()) {
|
|
1120
|
+
groupId = globalThis.crypto.randomUUID();
|
|
1121
|
+
this.draftAttachmentGroupId.set(groupId);
|
|
1122
|
+
}
|
|
1123
|
+
const gid = groupId.trim();
|
|
1124
|
+
const existing = this.draftComposerImages().length;
|
|
1125
|
+
if (existing + files.length > AXM_AI_CHAT_COMPOSER_MAX_IMAGES_PER_TURN) {
|
|
1126
|
+
await this.showUploadWarning('@ai-management:chat.upload-too-many');
|
|
1127
|
+
return;
|
|
1128
|
+
}
|
|
1129
|
+
const newSlots = [];
|
|
1130
|
+
const filesForPersist = [];
|
|
1131
|
+
for (const file of files) {
|
|
1132
|
+
if (!axmIsLikelyChatImageFile(file)) {
|
|
1133
|
+
await this.showUploadWarning('@ai-management:chat.upload-invalid-type');
|
|
1134
|
+
return;
|
|
1135
|
+
}
|
|
1136
|
+
if (file.size > AXM_AI_CHAT_COMPOSER_MAX_IMAGE_BYTES) {
|
|
1137
|
+
await this.showUploadWarning('@ai-management:chat.upload-too-large');
|
|
1138
|
+
return;
|
|
1139
|
+
}
|
|
1140
|
+
const mimeType = axmMimeTypeForChatAttachment(file);
|
|
1141
|
+
const fileForStorage = file.type.trim().startsWith('image/') && file.type.trim().length > 0
|
|
1142
|
+
? file
|
|
1143
|
+
: new File([file], file.name, { type: mimeType });
|
|
1144
|
+
newSlots.push({
|
|
1145
|
+
clientId: globalThis.crypto.randomUUID(),
|
|
1146
|
+
previewUrl: URL.createObjectURL(file),
|
|
1147
|
+
mimeType,
|
|
1148
|
+
name: file.name,
|
|
1149
|
+
});
|
|
1150
|
+
filesForPersist.push(fileForStorage);
|
|
1151
|
+
}
|
|
1152
|
+
this.draftComposerImages.update((prev) => [...prev, ...newSlots]);
|
|
1153
|
+
this.isComposerSaving.set(true);
|
|
1154
|
+
try {
|
|
1155
|
+
for (let i = 0; i < filesForPersist.length; i++) {
|
|
1156
|
+
const file = filesForPersist[i];
|
|
1157
|
+
const clientId = newSlots[i].clientId;
|
|
1158
|
+
try {
|
|
1159
|
+
const { fileId } = await persistAiChatAttachmentImage(this.fileStorage, file, gid);
|
|
1160
|
+
this.draftComposerImages.update((prev) => prev.map((s) => (s.clientId === clientId ? { ...s, fileId } : s)));
|
|
1161
|
+
}
|
|
1162
|
+
catch (e) {
|
|
1163
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1164
|
+
this.draftComposerImages.update((prev) => {
|
|
1165
|
+
const slot = prev.find((s) => s.clientId === clientId);
|
|
1166
|
+
if (slot) {
|
|
1167
|
+
URL.revokeObjectURL(slot.previewUrl);
|
|
1168
|
+
}
|
|
1169
|
+
return prev.filter((s) => s.clientId !== clientId);
|
|
1170
|
+
});
|
|
1171
|
+
if (this.draftComposerImages().length === 0 &&
|
|
1172
|
+
this.draftComposerAudios().length === 0 &&
|
|
1173
|
+
this.draftComposerFiles().length === 0) {
|
|
1174
|
+
this.draftAttachmentGroupId.set(null);
|
|
1175
|
+
}
|
|
1176
|
+
const errTitle = await this.translation.translateAsync('@general:messages.error.title');
|
|
1177
|
+
const uploadFailed = await this.translation.translateAsync('@ai-management:chat.upload-error');
|
|
1178
|
+
await this.toastService.show({
|
|
1179
|
+
color: 'danger',
|
|
1180
|
+
title: errTitle,
|
|
1181
|
+
content: `${uploadFailed} ${msg}`,
|
|
1182
|
+
});
|
|
1183
|
+
return;
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
finally {
|
|
1188
|
+
this.isComposerSaving.set(false);
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
async processComposerAudioFiles(files) {
|
|
1192
|
+
if (!isPlatformBrowser(this.platformId)) {
|
|
1193
|
+
return;
|
|
1194
|
+
}
|
|
1195
|
+
let groupId = this.draftAttachmentGroupId();
|
|
1196
|
+
if (!groupId?.trim()) {
|
|
1197
|
+
groupId = globalThis.crypto.randomUUID();
|
|
1198
|
+
this.draftAttachmentGroupId.set(groupId);
|
|
1199
|
+
}
|
|
1200
|
+
const gid = groupId.trim();
|
|
1201
|
+
const existing = this.draftComposerAudios().length;
|
|
1202
|
+
if (existing + files.length > AXM_AI_CHAT_COMPOSER_MAX_AUDIO_PER_TURN) {
|
|
1203
|
+
await this.showUploadWarning('@ai-management:chat.upload-audio-too-many');
|
|
1204
|
+
return;
|
|
1205
|
+
}
|
|
1206
|
+
const newSlots = [];
|
|
1207
|
+
const filesForPersist = [];
|
|
1208
|
+
for (const file of files) {
|
|
1209
|
+
if (!axmIsLikelyChatAudioFile(file)) {
|
|
1210
|
+
await this.showUploadWarning('@ai-management:chat.upload-audio-invalid-type');
|
|
1211
|
+
return;
|
|
1212
|
+
}
|
|
1213
|
+
if (file.size > AXM_AI_CHAT_COMPOSER_MAX_AUDIO_BYTES) {
|
|
1214
|
+
await this.showUploadWarning('@ai-management:chat.upload-audio-too-large');
|
|
1215
|
+
return;
|
|
1216
|
+
}
|
|
1217
|
+
const mimeType = axmMimeTypeForChatAttachment(file);
|
|
1218
|
+
const fileForStorage = file.type.trim().startsWith('audio/') && file.type.trim().length > 0
|
|
1219
|
+
? file
|
|
1220
|
+
: new File([file], file.name, { type: mimeType });
|
|
1221
|
+
newSlots.push({
|
|
1222
|
+
clientId: globalThis.crypto.randomUUID(),
|
|
1223
|
+
mimeType,
|
|
1224
|
+
name: file.name,
|
|
1225
|
+
});
|
|
1226
|
+
filesForPersist.push(fileForStorage);
|
|
1227
|
+
}
|
|
1228
|
+
this.draftComposerAudios.update((prev) => [...prev, ...newSlots]);
|
|
1229
|
+
this.isComposerSaving.set(true);
|
|
1230
|
+
try {
|
|
1231
|
+
for (let i = 0; i < filesForPersist.length; i++) {
|
|
1232
|
+
const file = filesForPersist[i];
|
|
1233
|
+
const clientId = newSlots[i].clientId;
|
|
1234
|
+
try {
|
|
1235
|
+
const { fileId } = await persistAiChatAttachmentImage(this.fileStorage, file, gid);
|
|
1236
|
+
this.draftComposerAudios.update((prev) => prev.map((s) => (s.clientId === clientId ? { ...s, fileId } : s)));
|
|
1237
|
+
}
|
|
1238
|
+
catch (e) {
|
|
1239
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1240
|
+
this.draftComposerAudios.update((prev) => prev.filter((s) => s.clientId !== clientId));
|
|
1241
|
+
if (this.draftComposerAudios().length === 0 &&
|
|
1242
|
+
this.draftComposerImages().length === 0 &&
|
|
1243
|
+
this.draftComposerFiles().length === 0) {
|
|
1244
|
+
this.draftAttachmentGroupId.set(null);
|
|
1245
|
+
}
|
|
1246
|
+
const errTitle = await this.translation.translateAsync('@general:messages.error.title');
|
|
1247
|
+
const uploadFailed = await this.translation.translateAsync('@ai-management:chat.upload-audio-error');
|
|
1248
|
+
await this.toastService.show({
|
|
1249
|
+
color: 'danger',
|
|
1250
|
+
title: errTitle,
|
|
1251
|
+
content: `${uploadFailed} ${msg}`,
|
|
1252
|
+
});
|
|
1253
|
+
return;
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
finally {
|
|
1258
|
+
this.isComposerSaving.set(false);
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
async processComposerDocumentFiles(files) {
|
|
1262
|
+
if (!isPlatformBrowser(this.platformId)) {
|
|
1263
|
+
return;
|
|
1264
|
+
}
|
|
1265
|
+
let groupId = this.draftAttachmentGroupId();
|
|
1266
|
+
if (!groupId?.trim()) {
|
|
1267
|
+
groupId = globalThis.crypto.randomUUID();
|
|
1268
|
+
this.draftAttachmentGroupId.set(groupId);
|
|
1269
|
+
}
|
|
1270
|
+
const gid = groupId.trim();
|
|
1271
|
+
const existing = this.draftComposerFiles().length;
|
|
1272
|
+
if (existing + files.length > AXM_AI_CHAT_COMPOSER_MAX_DOCUMENTS_PER_TURN) {
|
|
1273
|
+
await this.showUploadWarning('@ai-management:chat.upload-documents-too-many');
|
|
1274
|
+
return;
|
|
1275
|
+
}
|
|
1276
|
+
const newSlots = [];
|
|
1277
|
+
const filesForPersist = [];
|
|
1278
|
+
for (const file of files) {
|
|
1279
|
+
if (!axmIsLikelyChatDocumentFile(file)) {
|
|
1280
|
+
await this.showUploadWarning('@ai-management:chat.upload-documents-invalid-type');
|
|
1281
|
+
return;
|
|
1282
|
+
}
|
|
1283
|
+
if (file.size > AXM_AI_CHAT_COMPOSER_MAX_DOCUMENT_BYTES) {
|
|
1284
|
+
await this.showUploadWarning('@ai-management:chat.upload-documents-too-large');
|
|
1285
|
+
return;
|
|
1286
|
+
}
|
|
1287
|
+
const mimeType = axmMimeTypeForChatAttachment(file);
|
|
1288
|
+
const fileForStorage = file.type.trim().length > 0 && file.type.trim() !== 'application/octet-stream'
|
|
1289
|
+
? file
|
|
1290
|
+
: new File([file], file.name, { type: mimeType });
|
|
1291
|
+
newSlots.push({
|
|
1292
|
+
clientId: globalThis.crypto.randomUUID(),
|
|
1293
|
+
mimeType,
|
|
1294
|
+
name: file.name,
|
|
1295
|
+
});
|
|
1296
|
+
filesForPersist.push(fileForStorage);
|
|
1297
|
+
}
|
|
1298
|
+
this.draftComposerFiles.update((prev) => [...prev, ...newSlots]);
|
|
1299
|
+
this.isComposerSaving.set(true);
|
|
1300
|
+
try {
|
|
1301
|
+
for (let i = 0; i < filesForPersist.length; i++) {
|
|
1302
|
+
const file = filesForPersist[i];
|
|
1303
|
+
const clientId = newSlots[i].clientId;
|
|
1304
|
+
try {
|
|
1305
|
+
const { fileId } = await persistAiChatAttachmentImage(this.fileStorage, file, gid);
|
|
1306
|
+
this.draftComposerFiles.update((prev) => prev.map((s) => (s.clientId === clientId ? { ...s, fileId } : s)));
|
|
1307
|
+
}
|
|
1308
|
+
catch (e) {
|
|
1309
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1310
|
+
this.draftComposerFiles.update((prev) => prev.filter((s) => s.clientId !== clientId));
|
|
1311
|
+
if (this.draftComposerFiles().length === 0 &&
|
|
1312
|
+
this.draftComposerImages().length === 0 &&
|
|
1313
|
+
this.draftComposerAudios().length === 0) {
|
|
1314
|
+
this.draftAttachmentGroupId.set(null);
|
|
1315
|
+
}
|
|
1316
|
+
const errTitle = await this.translation.translateAsync('@general:messages.error.title');
|
|
1317
|
+
const uploadFailed = await this.translation.translateAsync('@ai-management:chat.upload-documents-error');
|
|
1318
|
+
await this.toastService.show({
|
|
1319
|
+
color: 'danger',
|
|
1320
|
+
title: errTitle,
|
|
1321
|
+
content: `${uploadFailed} ${msg}`,
|
|
1322
|
+
});
|
|
1323
|
+
return;
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
finally {
|
|
1328
|
+
this.isComposerSaving.set(false);
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
async showUploadWarning(translationKey) {
|
|
1332
|
+
const errTitle = await this.translation.translateAsync('@general:messages.error.title');
|
|
1333
|
+
const content = await this.translation.translateAsync(translationKey);
|
|
1334
|
+
await this.toastService.show({ color: 'warning', title: errTitle, content });
|
|
1335
|
+
}
|
|
1336
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXMAiAssistPromptBoxComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
1337
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.8", type: AXMAiAssistPromptBoxComponent, isStandalone: true, selector: "axm-ai-assist-prompt-box", inputs: { title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: true, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, initialPrompt: { classPropertyName: "initialPrompt", publicName: "initialPrompt", isSignal: true, isRequired: false, transformFunction: null }, contentMaxWidthPx: { classPropertyName: "contentMaxWidthPx", publicName: "contentMaxWidthPx", isSignal: true, isRequired: false, transformFunction: null }, initialAssistId: { classPropertyName: "initialAssistId", publicName: "initialAssistId", isSignal: true, isRequired: false, transformFunction: null }, initialDirectAgentId: { classPropertyName: "initialDirectAgentId", publicName: "initialDirectAgentId", isSignal: true, isRequired: false, transformFunction: null }, runPrompt: { classPropertyName: "runPrompt", publicName: "runPrompt", isSignal: true, isRequired: true, transformFunction: null }, requestClose: { classPropertyName: "requestClose", publicName: "requestClose", isSignal: true, isRequired: true, transformFunction: null } }, host: { listeners: { "document:keydown.escape": "onDocumentEscape()" } }, ngImport: i0, template: "<div\n class=\"axm-ai-assist-prompt-box flex flex-col gap-2.5 min-h-0 w-full\"\n [style.max-width.px]=\"contentMaxWidthPx()\"\n>\n <div\n class=\"axm-ai-assist-prompt-box__eyebrow flex items-center justify-between gap-3 shrink-0 px-0.5\"\n >\n <div\n class=\"axm-ai-assist-prompt-box__brand inline-flex min-w-0 items-center gap-2\"\n >\n <span class=\"axm-ai-assist-prompt-box__brand-mark\" aria-hidden=\"true\">\n <i class=\"fa-light fa-sparkles\" aria-hidden=\"true\"></i>\n </span>\n <span class=\"axm-ai-assist-prompt-box__brand-title truncate\">{{\n title()\n }}</span>\n </div>\n <ax-button\n class=\"shrink-0 ax-sm\"\n look=\"blank\"\n color=\"default\"\n type=\"button\"\n [iconOnly]=\"true\"\n [disabled]=\"busy()\"\n [attr.aria-label]=\"\n ('@general:actions.close.title' | translate | async) ?? ''\n \"\n (onClick)=\"onCancel()\"\n >\n <ax-prefix>\n <i class=\"fa-light fa-xmark\" aria-hidden=\"true\"></i>\n </ax-prefix>\n </ax-button>\n </div>\n\n <div class=\"axm-ai-assist-prompt-box__sheet flex min-h-0 flex-col gap-2\">\n @if (\n draftComposerImages().length > 0 ||\n draftComposerAudios().length > 0 ||\n draftComposerFiles().length > 0 ||\n isComposerSaving()\n ) {\n <div\n class=\"shrink-0 rounded-xl border border-default/45 dark:border-darker bg-lightest/90 p-1.5 flex flex-col gap-1.5\"\n [attr.aria-label]=\"\n ('@ai-management:chat.composer-attachments-region'\n | translate\n | async) ?? ''\n \"\n >\n <div\n class=\"flex flex-wrap items-center justify-between gap-1.5 text-[0.65rem] text-muted\"\n >\n <span>\n @if (isComposerSaving()) {\n {{ '@ai-management:chat.composer-uploading' | translate | async }}\n } @else {\n @if (draftComposerImages().length > 0) {\n <span\n >{{ draftComposerImages().length }}\n {{\n '@ai-management:chat.composer-images-count'\n | translate\n | async\n }}</span\n >\n }\n @if (\n draftComposerImages().length > 0 &&\n draftComposerAudios().length > 0\n ) {\n <span class=\"mx-1\">\u00B7</span>\n }\n @if (draftComposerAudios().length > 0) {\n <span\n >{{ draftComposerAudios().length }}\n {{\n '@ai-management:chat.composer-audio-count'\n | translate\n | async\n }}</span\n >\n }\n @if (draftComposerFiles().length > 0) {\n @if (\n draftComposerImages().length > 0 ||\n draftComposerAudios().length > 0\n ) {\n <span class=\"mx-1\">\u00B7</span>\n }\n <span\n >{{ draftComposerFiles().length }}\n {{\n '@ai-management:chat.composer-documents-count'\n | translate\n | async\n }}</span\n >\n }\n }\n </span>\n </div>\n @if (draftComposerImages().length > 0) {\n <div class=\"flex flex-wrap gap-1.5\">\n @for (slot of draftComposerImages(); track slot.clientId) {\n <div\n class=\"relative h-12 w-12 shrink-0 overflow-hidden rounded-md border border-default/60 dark:border-darker bg-lightest\"\n >\n <img\n [src]=\"slot.previewUrl\"\n [alt]=\"slot.name ?? ''\"\n class=\"h-full w-full object-cover\"\n />\n @if (!slot.fileId) {\n <span\n class=\"absolute inset-0 flex items-center justify-center bg-default/40 text-[0.6rem] font-medium text-on-lightest\"\n >\u2026</span\n >\n }\n <button\n type=\"button\"\n class=\"absolute end-0.5 top-0.5 flex h-4 w-4 items-center justify-center rounded bg-default/80 text-[0.65rem] leading-none text-on-lightest hover:bg-default focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-400\"\n [disabled]=\"busy() || isComposerSaving()\"\n [attr.aria-label]=\"\n ('@ai-management:chat.remove-draft-image'\n | translate\n | async) ?? ''\n \"\n (click)=\"removeDraftImage(slot.clientId)\"\n >\n \u00D7\n </button>\n </div>\n }\n </div>\n }\n @if (draftComposerAudios().length > 0) {\n <div class=\"flex flex-col gap-1\">\n @for (slot of draftComposerAudios(); track slot.clientId) {\n <div\n class=\"flex items-center justify-between gap-1.5 rounded border border-default/60 dark:border-darker bg-lightest px-1.5 py-1 text-[0.65rem] min-w-0\"\n >\n <span\n class=\"truncate text-on-lightest\"\n [attr.title]=\"slot.name ?? ''\"\n >\n {{\n '@ai-management:chat.attached-audio-label'\n | translate\n | async\n }}:\n {{\n slot.name?.trim() ||\n ('@ai-management:chat.audio-untitled' | translate | async)\n }}\n </span>\n @if (!slot.fileId) {\n <span class=\"text-muted shrink-0\">\u2026</span>\n }\n <button\n type=\"button\"\n class=\"shrink-0 flex h-5 w-5 items-center justify-center rounded bg-default/80 text-[0.75rem] leading-none text-on-lightest hover:bg-default focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-400\"\n [disabled]=\"busy() || isComposerSaving()\"\n [attr.aria-label]=\"\n ('@ai-management:chat.remove-draft-audio'\n | translate\n | async) ?? ''\n \"\n (click)=\"removeDraftAudio(slot.clientId)\"\n >\n \u00D7\n </button>\n </div>\n }\n </div>\n }\n @if (draftComposerFiles().length > 0) {\n <div class=\"flex flex-col gap-1\">\n @for (slot of draftComposerFiles(); track slot.clientId) {\n <div\n class=\"flex items-center justify-between gap-1.5 rounded border border-default/60 dark:border-darker bg-lightest px-1.5 py-1 text-[0.65rem] min-w-0\"\n >\n <span\n class=\"truncate text-on-lightest\"\n [attr.title]=\"slot.name ?? ''\"\n >\n {{\n '@ai-management:chat.attached-document-label'\n | translate\n | async\n }}:\n {{\n slot.name?.trim() ||\n ('@ai-management:chat.document-untitled'\n | translate\n | async)\n }}\n </span>\n @if (!slot.fileId) {\n <span class=\"text-muted shrink-0\">\u2026</span>\n }\n <button\n type=\"button\"\n class=\"shrink-0 flex h-5 w-5 items-center justify-center rounded bg-default/80 text-[0.75rem] leading-none text-on-lightest hover:bg-default focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-400\"\n [disabled]=\"busy() || isComposerSaving()\"\n [attr.aria-label]=\"\n ('@ai-management:chat.remove-draft-document'\n | translate\n | async) ?? ''\n \"\n (click)=\"removeDraftFile(slot.clientId)\"\n >\n \u00D7\n </button>\n </div>\n }\n </div>\n }\n </div>\n }\n\n <div class=\"axm-ai-assist-prompt-box__body flex min-h-0 flex-col\">\n <ax-text-area\n class=\"axm-ai-assist-prompt-box__prompt-input w-full min-h-0\"\n [rows]=\"3\"\n [disabled]=\"busy()\"\n [ngModel]=\"promptDraft()\"\n (ngModelChange)=\"onPromptDraftChange($event)\"\n (keydown)=\"onPromptKeydown($event)\"\n [placeholder]=\"\n placeholder().trim().length > 0\n ? placeholder()\n : (('@ai-management:assist-popup.placeholder'\n | translate\n | async) ?? '')\n \"\n ></ax-text-area>\n </div>\n\n <div\n class=\"axm-ai-assist-prompt-box__footer flex shrink-0 items-center justify-between gap-2\"\n >\n <ax-button\n class=\"axm-ai-assist-prompt-box__menu-trigger\"\n look=\"solid\"\n color=\"primary\"\n type=\"button\"\n [iconOnly]=\"true\"\n [disabled]=\"busy()\"\n [attr.aria-label]=\"\n ('@ai-management:assist-popup.composer-menu-aria'\n | translate\n | async) ?? ''\n \"\n >\n <ax-prefix>\n <i class=\"fa-light fa-plus text-sm\" aria-hidden=\"true\"></i>\n </ax-prefix>\n <ax-dropdown-panel #composerMenu>\n <ax-button-item-list class=\"min-w-56\">\n <ax-button-item\n [text]=\"\n ('@ai-management:chat.upload-images' | translate | async) ?? ''\n \"\n [attr.aria-label]=\"\n ('@ai-management:chat.upload-images-aria'\n | translate\n | async) ?? ''\n \"\n (onClick)=\"openImagePicker(); composerMenu.close()\"\n >\n <ax-prefix>\n <i\n class=\"fa-light fa-image size-4 text-center\"\n aria-hidden=\"true\"\n ></i>\n </ax-prefix>\n </ax-button-item>\n <ax-button-item\n [text]=\"\n ('@ai-management:chat.upload-audio' | translate | async) ?? ''\n \"\n [attr.aria-label]=\"\n ('@ai-management:chat.upload-audio-aria' | translate | async) ??\n ''\n \"\n (onClick)=\"openAudioPicker(); composerMenu.close()\"\n >\n <ax-prefix>\n <i\n class=\"fa-light fa-microphone size-4 text-center\"\n aria-hidden=\"true\"\n ></i>\n </ax-prefix>\n </ax-button-item>\n <ax-button-item\n [text]=\"\n ('@ai-management:chat.upload-documents' | translate | async) ??\n ''\n \"\n [attr.aria-label]=\"\n ('@ai-management:chat.upload-documents-aria'\n | translate\n | async) ?? ''\n \"\n (onClick)=\"openDocumentPicker(); composerMenu.close()\"\n >\n <ax-prefix>\n <i\n class=\"fa-light fa-paperclip size-4 text-center\"\n aria-hidden=\"true\"\n ></i>\n </ax-prefix>\n </ax-button-item>\n </ax-button-item-list>\n </ax-dropdown-panel>\n </ax-button>\n\n <ax-button\n class=\"axm-ai-assist-prompt-box__send axm-ai-assist-prompt-box__send-gradient\"\n color=\"primary\"\n type=\"button\"\n [disabled]=\"busy() || !canSend()\"\n [text]=\"\n busy()\n ? (('@ai-management:assist-popup.generating' | translate | async) ??\n '')\n : (('@ai-management:assist-popup.generate' | translate | async) ??\n '')\n \"\n (onClick)=\"onSend()\"\n ></ax-button>\n </div>\n </div>\n</div>\n", styles: ["@layer properties;.axm-ai-assist-prompt-box-shell{pointer-events:none;box-sizing:border-box;inset:0;display:flex;max-height:100%;width:100%;flex-direction:column;align-items:center;justify-content:flex-end;padding-left:max(.75rem,env(safe-area-inset-left,0px));padding-right:max(.75rem,env(safe-area-inset-right,0px));padding-bottom:max(.75rem,env(safe-area-inset-bottom,0px));padding-top:max(.5rem,env(safe-area-inset-top,0px))}.axm-ai-assist-prompt-box-shell .overlay-pane{pointer-events:auto;--tw-border-style: none;border-style:none;background-color:transparent;flex:0 0 auto;width:auto!important;max-width:calc(100vw - 1.5rem);min-width:min(480px,100vw - 1.5rem)}.axm-ai-assist-prompt-box{--axm-axm-ai-assist-prompt-chrome-gradient: linear-gradient( 125deg, rgb(var(--ax-sys-color-primary-200)) 0%, rgb(var(--ax-sys-color-primary-400)) 48%, rgb(var(--ax-sys-color-primary-300)) 100% );--axm-axm-ai-assist-prompt-send-gradient: linear-gradient( 90deg, rgb(var(--ax-sys-color-primary-500)), rgb(var(--ax-sys-color-primary-700)) );margin:0;box-sizing:border-box;overflow:hidden;padding:0;max-height:min(78vh,620px);padding:.65rem .7rem .7rem;border-radius:1.35rem;background:var(--axm-axm-ai-assist-prompt-chrome-gradient);box-shadow:0 18px 50px -12px rgba(var(--ax-sys-color-on-surface),.12),0 8px 20px -8px rgba(var(--ax-sys-color-primary-400),.2)}.dark .axm-ai-assist-prompt-box{--axm-axm-ai-assist-prompt-chrome-gradient: linear-gradient( 125deg, rgb(var(--ax-sys-color-primary-600)) 0%, rgb(var(--ax-sys-color-primary-500)) 48%, rgb(var(--ax-sys-color-primary-400)) 100% );box-shadow:0 20px 55px -14px rgba(var(--ax-sys-color-dark),.48),0 8px 24px -8px rgba(var(--ax-sys-color-primary-400),.26)}.axm-ai-assist-prompt-box .axm-ai-assist-prompt-box__brand-title{--tw-font-weight: var(--font-weight-semibold, 600);font-weight:var(--font-weight-semibold, 600);font-size:.72rem;line-height:1.25;letter-spacing:.01em;color:rgb(var(--ax-sys-color-on-surface))}.dark :is(.axm-ai-assist-prompt-box .axm-ai-assist-prompt-box__brand-title){color:rgb(var(--ax-sys-color-on-lightest-surface))}.axm-ai-assist-prompt-box .axm-ai-assist-prompt-box__brand-mark{display:inline-flex;flex-shrink:0;align-items:center;justify-content:center;border-radius:calc(infinity * 1px);width:1.65rem;height:1.65rem;font-size:.75rem;line-height:1;color:rgb(var(--ax-sys-color-on-primary-surface));background:linear-gradient(145deg,rgb(var(--ax-sys-color-primary-500)),rgb(var(--ax-sys-color-primary-700)));box-shadow:0 2px 8px rgba(var(--ax-sys-color-primary-500),.35)}.axm-ai-assist-prompt-box .axm-ai-assist-prompt-box__sheet{min-height:0px;flex:auto;padding:.65rem .65rem .55rem;border-radius:1rem;background:rgb(var(--ax-sys-color-lightest-surface));border:1px solid rgba(var(--ax-sys-color-neutral-300),.35);box-shadow:inset 0 1px rgba(var(--ax-sys-color-lightest-surface),.95)}.dark :is(.axm-ai-assist-prompt-box .axm-ai-assist-prompt-box__sheet){background:rgb(var(--ax-sys-color-neutral-800));border-color:rgba(var(--ax-sys-color-neutral-500),.4);box-shadow:inset 0 1px rgba(var(--ax-sys-color-lightest-surface),.06)}.axm-ai-assist-prompt-box .axm-ai-assist-prompt-box__body{min-height:0px;flex:auto;overflow-y:auto}.axm-ai-assist-prompt-box .axm-ai-assist-prompt-box__prompt-input{display:block;min-height:0px;width:100%;--tw-border-style: none !important;border-style:none!important;background-color:transparent!important;--tw-shadow: 0 0 #0000 !important;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)!important}.axm-ai-assist-prompt-box .axm-ai-assist-prompt-box__prompt-input:focus-within{--tw-border-style: none !important;border-style:none!important;--tw-shadow: 0 0 #0000 !important;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)!important;--tw-outline-style: none !important;outline-style:none!important}.axm-ai-assist-prompt-box .axm-ai-assist-prompt-box__prompt-input:focus-within>*{--tw-border-style: none !important;border-style:none!important;--tw-shadow: 0 0 #0000 !important;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)!important;--tw-outline-style: none !important;outline-style:none!important}.axm-ai-assist-prompt-box .axm-ai-assist-prompt-box__prompt-input textarea{min-height:4.5rem;max-height:min(30vh,240px);font-size:.8125rem;line-height:1.45;padding-block:.55rem;padding-inline:.65rem;border-radius:.65rem;--tw-border-style: none !important;border-style:none!important;background-color:transparent!important;--tw-shadow: 0 0 #0000 !important;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)!important}.axm-ai-assist-prompt-box .axm-ai-assist-prompt-box__prompt-input textarea:focus,.axm-ai-assist-prompt-box .axm-ai-assist-prompt-box__prompt-input textarea:focus-visible,.axm-ai-assist-prompt-box .axm-ai-assist-prompt-box__prompt-input textarea:active{--tw-border-style: none !important;border-style:none!important;--tw-shadow: 0 0 #0000 !important;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)!important;--tw-outline-style: none !important;outline-style:none!important;outline-offset:0!important}.dark :is(.axm-ai-assist-prompt-box .axm-ai-assist-prompt-box__prompt-input textarea){color:rgb(var(--ax-sys-color-on-lightest-surface));background:rgb(var(--ax-sys-color-neutral-900))!important}.axm-ai-assist-prompt-box .axm-ai-assist-prompt-box__footer{min-height:0}.axm-ai-assist-prompt-box ax-button.axm-ai-assist-prompt-box__menu-trigger{--ax-comp-button-normal-bg-color: 0, 0, 0, 0 !important;--ax-comp-button-hover-bg-color: 0, 0, 0, 0 !important;--ax-comp-button-pressed-bg-color: 0, 0, 0, 0 !important;--ax-comp-button-border-width: 0 !important;overflow:hidden;border-radius:calc(infinity * 1px)!important;border-color:transparent!important;background-color:transparent!important;background:transparent!important}.axm-ai-assist-prompt-box ax-button.axm-ai-assist-prompt-box__menu-trigger:hover:not(.state-disabled,.state-loading,.state-selected),.axm-ai-assist-prompt-box ax-button.axm-ai-assist-prompt-box__menu-trigger:active:not(.state-disabled,.state-loading,.state-selected){border-color:transparent!important;background-color:transparent!important}.axm-ai-assist-prompt-box ax-button.axm-ai-assist-prompt-box__menu-trigger>button{border-radius:calc(infinity * 1px)!important;--tw-border-style: none !important;border-style:none!important;padding:0!important;width:2.25rem;height:2.25rem;min-width:2.25rem;min-height:2.25rem;color:rgb(var(--ax-sys-color-on-primary-surface))!important;background:linear-gradient(145deg,rgb(var(--ax-sys-color-primary-500)),rgb(var(--ax-sys-color-primary-700)))!important;box-shadow:0 2px 8px rgba(var(--ax-sys-color-primary-500),.35)}.axm-ai-assist-prompt-box ax-button.axm-ai-assist-prompt-box__menu-trigger.ax-state-disabled>button,.axm-ai-assist-prompt-box ax-button.axm-ai-assist-prompt-box__menu-trigger.ax-state-loading>button,.axm-ai-assist-prompt-box ax-button.axm-ai-assist-prompt-box__menu-trigger>button:disabled{opacity:.55;box-shadow:none}.axm-ai-assist-prompt-box ax-button.axm-ai-assist-prompt-box__menu-trigger:hover:not(.state-disabled,.state-loading,.state-selected)>button{filter:brightness(.94)}.axm-ai-assist-prompt-box ax-button.axm-ai-assist-prompt-box__menu-trigger:active:not(.state-disabled,.state-loading,.state-selected)>button{filter:brightness(.88)}.axm-ai-assist-prompt-box ax-button.axm-ai-assist-prompt-box__send-gradient{--ax-comp-button-normal-bg-color: 0, 0, 0, 0 !important;--ax-comp-button-hover-bg-color: 0, 0, 0, 0 !important;--ax-comp-button-pressed-bg-color: 0, 0, 0, 0 !important;--ax-comp-button-border-width: 0 !important;overflow:hidden;border-radius:calc(infinity * 1px)!important;border-color:transparent!important;background-color:transparent!important;background:transparent!important}.axm-ai-assist-prompt-box ax-button.axm-ai-assist-prompt-box__send-gradient:hover:not(.state-disabled,.state-loading,.state-selected),.axm-ai-assist-prompt-box ax-button.axm-ai-assist-prompt-box__send-gradient:active:not(.state-disabled,.state-loading,.state-selected){border-color:transparent!important;background-color:transparent!important}.axm-ai-assist-prompt-box ax-button.axm-ai-assist-prompt-box__send-gradient>button{border-radius:calc(infinity * 1px)!important;--tw-border-style: none !important;border-style:none!important;--tw-font-weight: var(--font-weight-semibold, 600);font-weight:var(--font-weight-semibold, 600);min-height:2.25rem;padding-inline:1rem!important;font-size:.8rem;letter-spacing:.01em;color:rgb(var(--ax-sys-color-on-primary-surface))!important;background:var(--axm-axm-ai-assist-prompt-send-gradient)!important;box-shadow:0 3px 12px rgba(var(--ax-sys-color-secondary-400),.35)}.axm-ai-assist-prompt-box ax-button.axm-ai-assist-prompt-box__send-gradient.ax-state-disabled>button,.axm-ai-assist-prompt-box ax-button.axm-ai-assist-prompt-box__send-gradient.ax-state-loading>button,.axm-ai-assist-prompt-box ax-button.axm-ai-assist-prompt-box__send-gradient>button:disabled{opacity:.55;box-shadow:none}.axm-ai-assist-prompt-box ax-button.axm-ai-assist-prompt-box__send-gradient:hover:not(.state-disabled,.state-loading,.state-selected)>button{filter:brightness(.96)}.axm-ai-assist-prompt-box ax-button.axm-ai-assist-prompt-box__send-gradient:active:not(.state-disabled,.state-loading,.state-selected)>button{filter:brightness(.9)}@property --tw-font-weight{syntax: \"*\"; inherits: false;}@property --tw-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-shadow-color{syntax: \"*\"; inherits: false;}@property --tw-shadow-alpha{syntax: \"<percentage>\"; inherits: false; initial-value: 100%;}@property --tw-inset-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-inset-shadow-color{syntax: \"*\"; inherits: false;}@property --tw-inset-shadow-alpha{syntax: \"<percentage>\"; inherits: false; initial-value: 100%;}@property --tw-ring-color{syntax: \"*\"; inherits: false;}@property --tw-ring-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-inset-ring-color{syntax: \"*\"; inherits: false;}@property --tw-inset-ring-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-ring-inset{syntax: \"*\"; inherits: false;}@property --tw-ring-offset-width{syntax: \"<length>\"; inherits: false; initial-value: 0px;}@property --tw-ring-offset-color{syntax: \"*\"; inherits: false; initial-value: #fff;}@property --tw-ring-offset-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@layer properties{@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-font-weight: initial;--tw-shadow: 0 0 #0000;--tw-shadow-color: initial;--tw-shadow-alpha: 100%;--tw-inset-shadow: 0 0 #0000;--tw-inset-shadow-color: initial;--tw-inset-shadow-alpha: 100%;--tw-ring-color: initial;--tw-ring-shadow: 0 0 #0000;--tw-inset-ring-color: initial;--tw-inset-ring-shadow: 0 0 #0000;--tw-ring-inset: initial;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-offset-shadow: 0 0 #0000}}}\n/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: AXButtonModule }, { kind: "component", type: i2.AXButtonComponent, selector: "ax-button", inputs: ["disabled", "size", "tabIndex", "color", "look", "text", "toggleable", "selected", "iconOnly", "type", "loadingText"], outputs: ["onBlur", "onFocus", "onClick", "selectedChange", "toggleableChange", "lookChange", "colorChange", "disabledChange", "loadingTextChange"] }, { kind: "component", type: i2.AXButtonItemComponent, selector: "ax-button-item", inputs: ["color", "disabled", "text", "selected", "divided", "data", "name"], outputs: ["onClick", "onFocus", "onBlur", "disabledChange"] }, { kind: "component", type: i2.AXButtonItemListComponent, selector: "ax-button-item-list", inputs: ["items", "closeParentOnClick", "lockOnLoading"], outputs: ["onItemClick"] }, { kind: "ngmodule", type: AXDecoratorModule }, { kind: "component", type: i3.AXDecoratorGenericComponent, selector: "ax-footer, ax-header, ax-content, ax-divider, ax-form-hint, ax-prefix, ax-suffix, ax-text, ax-title, ax-subtitle, ax-placeholder, ax-overlay" }, { kind: "ngmodule", type: AXDropdownModule }, { kind: "component", type: i4.AXDropdownPanelComponent, selector: "ax-dropdown-panel", inputs: ["isOpen", "fitParent", "dropdownWidth", "position", "placement", "_target", "adaptivityEnabled"], outputs: ["onOpened", "onClosed"] }, { kind: "ngmodule", type: AXDropdownButtonModule }, { kind: "ngmodule", type: AXTextAreaModule }, { kind: "component", type: i5.AXTextAreaComponent, selector: "ax-text-area", inputs: ["disabled", "tabIndex", "readonly", "value", "state", "name", "placeholder", "maxLength", "look", "rows", "allowResize", "showCounter", "class"], outputs: ["onBlur", "onFocus", "valueChange", "stateChange", "onValueChanged", "readonlyChange", "disabledChange", "onKeyDown", "onKeyUp", "onKeyPress"] }, { kind: "ngmodule", type: AXTranslationModule }, { kind: "pipe", type: i6.AsyncPipe, name: "async" }, { kind: "pipe", type: i7.AXTranslatorPipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); }
|
|
1338
|
+
}
|
|
1339
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXMAiAssistPromptBoxComponent, decorators: [{
|
|
1340
|
+
type: Component,
|
|
1341
|
+
args: [{ selector: 'axm-ai-assist-prompt-box', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, host: {
|
|
1342
|
+
'(document:keydown.escape)': 'onDocumentEscape()',
|
|
1343
|
+
}, imports: [
|
|
1344
|
+
CommonModule,
|
|
1345
|
+
FormsModule,
|
|
1346
|
+
AXButtonModule,
|
|
1347
|
+
AXDecoratorModule,
|
|
1348
|
+
AXDropdownModule,
|
|
1349
|
+
AXDropdownButtonModule,
|
|
1350
|
+
AXTextAreaModule,
|
|
1351
|
+
AXTranslationModule,
|
|
1352
|
+
], template: "<div\n class=\"axm-ai-assist-prompt-box flex flex-col gap-2.5 min-h-0 w-full\"\n [style.max-width.px]=\"contentMaxWidthPx()\"\n>\n <div\n class=\"axm-ai-assist-prompt-box__eyebrow flex items-center justify-between gap-3 shrink-0 px-0.5\"\n >\n <div\n class=\"axm-ai-assist-prompt-box__brand inline-flex min-w-0 items-center gap-2\"\n >\n <span class=\"axm-ai-assist-prompt-box__brand-mark\" aria-hidden=\"true\">\n <i class=\"fa-light fa-sparkles\" aria-hidden=\"true\"></i>\n </span>\n <span class=\"axm-ai-assist-prompt-box__brand-title truncate\">{{\n title()\n }}</span>\n </div>\n <ax-button\n class=\"shrink-0 ax-sm\"\n look=\"blank\"\n color=\"default\"\n type=\"button\"\n [iconOnly]=\"true\"\n [disabled]=\"busy()\"\n [attr.aria-label]=\"\n ('@general:actions.close.title' | translate | async) ?? ''\n \"\n (onClick)=\"onCancel()\"\n >\n <ax-prefix>\n <i class=\"fa-light fa-xmark\" aria-hidden=\"true\"></i>\n </ax-prefix>\n </ax-button>\n </div>\n\n <div class=\"axm-ai-assist-prompt-box__sheet flex min-h-0 flex-col gap-2\">\n @if (\n draftComposerImages().length > 0 ||\n draftComposerAudios().length > 0 ||\n draftComposerFiles().length > 0 ||\n isComposerSaving()\n ) {\n <div\n class=\"shrink-0 rounded-xl border border-default/45 dark:border-darker bg-lightest/90 p-1.5 flex flex-col gap-1.5\"\n [attr.aria-label]=\"\n ('@ai-management:chat.composer-attachments-region'\n | translate\n | async) ?? ''\n \"\n >\n <div\n class=\"flex flex-wrap items-center justify-between gap-1.5 text-[0.65rem] text-muted\"\n >\n <span>\n @if (isComposerSaving()) {\n {{ '@ai-management:chat.composer-uploading' | translate | async }}\n } @else {\n @if (draftComposerImages().length > 0) {\n <span\n >{{ draftComposerImages().length }}\n {{\n '@ai-management:chat.composer-images-count'\n | translate\n | async\n }}</span\n >\n }\n @if (\n draftComposerImages().length > 0 &&\n draftComposerAudios().length > 0\n ) {\n <span class=\"mx-1\">\u00B7</span>\n }\n @if (draftComposerAudios().length > 0) {\n <span\n >{{ draftComposerAudios().length }}\n {{\n '@ai-management:chat.composer-audio-count'\n | translate\n | async\n }}</span\n >\n }\n @if (draftComposerFiles().length > 0) {\n @if (\n draftComposerImages().length > 0 ||\n draftComposerAudios().length > 0\n ) {\n <span class=\"mx-1\">\u00B7</span>\n }\n <span\n >{{ draftComposerFiles().length }}\n {{\n '@ai-management:chat.composer-documents-count'\n | translate\n | async\n }}</span\n >\n }\n }\n </span>\n </div>\n @if (draftComposerImages().length > 0) {\n <div class=\"flex flex-wrap gap-1.5\">\n @for (slot of draftComposerImages(); track slot.clientId) {\n <div\n class=\"relative h-12 w-12 shrink-0 overflow-hidden rounded-md border border-default/60 dark:border-darker bg-lightest\"\n >\n <img\n [src]=\"slot.previewUrl\"\n [alt]=\"slot.name ?? ''\"\n class=\"h-full w-full object-cover\"\n />\n @if (!slot.fileId) {\n <span\n class=\"absolute inset-0 flex items-center justify-center bg-default/40 text-[0.6rem] font-medium text-on-lightest\"\n >\u2026</span\n >\n }\n <button\n type=\"button\"\n class=\"absolute end-0.5 top-0.5 flex h-4 w-4 items-center justify-center rounded bg-default/80 text-[0.65rem] leading-none text-on-lightest hover:bg-default focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-400\"\n [disabled]=\"busy() || isComposerSaving()\"\n [attr.aria-label]=\"\n ('@ai-management:chat.remove-draft-image'\n | translate\n | async) ?? ''\n \"\n (click)=\"removeDraftImage(slot.clientId)\"\n >\n \u00D7\n </button>\n </div>\n }\n </div>\n }\n @if (draftComposerAudios().length > 0) {\n <div class=\"flex flex-col gap-1\">\n @for (slot of draftComposerAudios(); track slot.clientId) {\n <div\n class=\"flex items-center justify-between gap-1.5 rounded border border-default/60 dark:border-darker bg-lightest px-1.5 py-1 text-[0.65rem] min-w-0\"\n >\n <span\n class=\"truncate text-on-lightest\"\n [attr.title]=\"slot.name ?? ''\"\n >\n {{\n '@ai-management:chat.attached-audio-label'\n | translate\n | async\n }}:\n {{\n slot.name?.trim() ||\n ('@ai-management:chat.audio-untitled' | translate | async)\n }}\n </span>\n @if (!slot.fileId) {\n <span class=\"text-muted shrink-0\">\u2026</span>\n }\n <button\n type=\"button\"\n class=\"shrink-0 flex h-5 w-5 items-center justify-center rounded bg-default/80 text-[0.75rem] leading-none text-on-lightest hover:bg-default focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-400\"\n [disabled]=\"busy() || isComposerSaving()\"\n [attr.aria-label]=\"\n ('@ai-management:chat.remove-draft-audio'\n | translate\n | async) ?? ''\n \"\n (click)=\"removeDraftAudio(slot.clientId)\"\n >\n \u00D7\n </button>\n </div>\n }\n </div>\n }\n @if (draftComposerFiles().length > 0) {\n <div class=\"flex flex-col gap-1\">\n @for (slot of draftComposerFiles(); track slot.clientId) {\n <div\n class=\"flex items-center justify-between gap-1.5 rounded border border-default/60 dark:border-darker bg-lightest px-1.5 py-1 text-[0.65rem] min-w-0\"\n >\n <span\n class=\"truncate text-on-lightest\"\n [attr.title]=\"slot.name ?? ''\"\n >\n {{\n '@ai-management:chat.attached-document-label'\n | translate\n | async\n }}:\n {{\n slot.name?.trim() ||\n ('@ai-management:chat.document-untitled'\n | translate\n | async)\n }}\n </span>\n @if (!slot.fileId) {\n <span class=\"text-muted shrink-0\">\u2026</span>\n }\n <button\n type=\"button\"\n class=\"shrink-0 flex h-5 w-5 items-center justify-center rounded bg-default/80 text-[0.75rem] leading-none text-on-lightest hover:bg-default focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-400\"\n [disabled]=\"busy() || isComposerSaving()\"\n [attr.aria-label]=\"\n ('@ai-management:chat.remove-draft-document'\n | translate\n | async) ?? ''\n \"\n (click)=\"removeDraftFile(slot.clientId)\"\n >\n \u00D7\n </button>\n </div>\n }\n </div>\n }\n </div>\n }\n\n <div class=\"axm-ai-assist-prompt-box__body flex min-h-0 flex-col\">\n <ax-text-area\n class=\"axm-ai-assist-prompt-box__prompt-input w-full min-h-0\"\n [rows]=\"3\"\n [disabled]=\"busy()\"\n [ngModel]=\"promptDraft()\"\n (ngModelChange)=\"onPromptDraftChange($event)\"\n (keydown)=\"onPromptKeydown($event)\"\n [placeholder]=\"\n placeholder().trim().length > 0\n ? placeholder()\n : (('@ai-management:assist-popup.placeholder'\n | translate\n | async) ?? '')\n \"\n ></ax-text-area>\n </div>\n\n <div\n class=\"axm-ai-assist-prompt-box__footer flex shrink-0 items-center justify-between gap-2\"\n >\n <ax-button\n class=\"axm-ai-assist-prompt-box__menu-trigger\"\n look=\"solid\"\n color=\"primary\"\n type=\"button\"\n [iconOnly]=\"true\"\n [disabled]=\"busy()\"\n [attr.aria-label]=\"\n ('@ai-management:assist-popup.composer-menu-aria'\n | translate\n | async) ?? ''\n \"\n >\n <ax-prefix>\n <i class=\"fa-light fa-plus text-sm\" aria-hidden=\"true\"></i>\n </ax-prefix>\n <ax-dropdown-panel #composerMenu>\n <ax-button-item-list class=\"min-w-56\">\n <ax-button-item\n [text]=\"\n ('@ai-management:chat.upload-images' | translate | async) ?? ''\n \"\n [attr.aria-label]=\"\n ('@ai-management:chat.upload-images-aria'\n | translate\n | async) ?? ''\n \"\n (onClick)=\"openImagePicker(); composerMenu.close()\"\n >\n <ax-prefix>\n <i\n class=\"fa-light fa-image size-4 text-center\"\n aria-hidden=\"true\"\n ></i>\n </ax-prefix>\n </ax-button-item>\n <ax-button-item\n [text]=\"\n ('@ai-management:chat.upload-audio' | translate | async) ?? ''\n \"\n [attr.aria-label]=\"\n ('@ai-management:chat.upload-audio-aria' | translate | async) ??\n ''\n \"\n (onClick)=\"openAudioPicker(); composerMenu.close()\"\n >\n <ax-prefix>\n <i\n class=\"fa-light fa-microphone size-4 text-center\"\n aria-hidden=\"true\"\n ></i>\n </ax-prefix>\n </ax-button-item>\n <ax-button-item\n [text]=\"\n ('@ai-management:chat.upload-documents' | translate | async) ??\n ''\n \"\n [attr.aria-label]=\"\n ('@ai-management:chat.upload-documents-aria'\n | translate\n | async) ?? ''\n \"\n (onClick)=\"openDocumentPicker(); composerMenu.close()\"\n >\n <ax-prefix>\n <i\n class=\"fa-light fa-paperclip size-4 text-center\"\n aria-hidden=\"true\"\n ></i>\n </ax-prefix>\n </ax-button-item>\n </ax-button-item-list>\n </ax-dropdown-panel>\n </ax-button>\n\n <ax-button\n class=\"axm-ai-assist-prompt-box__send axm-ai-assist-prompt-box__send-gradient\"\n color=\"primary\"\n type=\"button\"\n [disabled]=\"busy() || !canSend()\"\n [text]=\"\n busy()\n ? (('@ai-management:assist-popup.generating' | translate | async) ??\n '')\n : (('@ai-management:assist-popup.generate' | translate | async) ??\n '')\n \"\n (onClick)=\"onSend()\"\n ></ax-button>\n </div>\n </div>\n</div>\n", styles: ["@layer properties;.axm-ai-assist-prompt-box-shell{pointer-events:none;box-sizing:border-box;inset:0;display:flex;max-height:100%;width:100%;flex-direction:column;align-items:center;justify-content:flex-end;padding-left:max(.75rem,env(safe-area-inset-left,0px));padding-right:max(.75rem,env(safe-area-inset-right,0px));padding-bottom:max(.75rem,env(safe-area-inset-bottom,0px));padding-top:max(.5rem,env(safe-area-inset-top,0px))}.axm-ai-assist-prompt-box-shell .overlay-pane{pointer-events:auto;--tw-border-style: none;border-style:none;background-color:transparent;flex:0 0 auto;width:auto!important;max-width:calc(100vw - 1.5rem);min-width:min(480px,100vw - 1.5rem)}.axm-ai-assist-prompt-box{--axm-axm-ai-assist-prompt-chrome-gradient: linear-gradient( 125deg, rgb(var(--ax-sys-color-primary-200)) 0%, rgb(var(--ax-sys-color-primary-400)) 48%, rgb(var(--ax-sys-color-primary-300)) 100% );--axm-axm-ai-assist-prompt-send-gradient: linear-gradient( 90deg, rgb(var(--ax-sys-color-primary-500)), rgb(var(--ax-sys-color-primary-700)) );margin:0;box-sizing:border-box;overflow:hidden;padding:0;max-height:min(78vh,620px);padding:.65rem .7rem .7rem;border-radius:1.35rem;background:var(--axm-axm-ai-assist-prompt-chrome-gradient);box-shadow:0 18px 50px -12px rgba(var(--ax-sys-color-on-surface),.12),0 8px 20px -8px rgba(var(--ax-sys-color-primary-400),.2)}.dark .axm-ai-assist-prompt-box{--axm-axm-ai-assist-prompt-chrome-gradient: linear-gradient( 125deg, rgb(var(--ax-sys-color-primary-600)) 0%, rgb(var(--ax-sys-color-primary-500)) 48%, rgb(var(--ax-sys-color-primary-400)) 100% );box-shadow:0 20px 55px -14px rgba(var(--ax-sys-color-dark),.48),0 8px 24px -8px rgba(var(--ax-sys-color-primary-400),.26)}.axm-ai-assist-prompt-box .axm-ai-assist-prompt-box__brand-title{--tw-font-weight: var(--font-weight-semibold, 600);font-weight:var(--font-weight-semibold, 600);font-size:.72rem;line-height:1.25;letter-spacing:.01em;color:rgb(var(--ax-sys-color-on-surface))}.dark :is(.axm-ai-assist-prompt-box .axm-ai-assist-prompt-box__brand-title){color:rgb(var(--ax-sys-color-on-lightest-surface))}.axm-ai-assist-prompt-box .axm-ai-assist-prompt-box__brand-mark{display:inline-flex;flex-shrink:0;align-items:center;justify-content:center;border-radius:calc(infinity * 1px);width:1.65rem;height:1.65rem;font-size:.75rem;line-height:1;color:rgb(var(--ax-sys-color-on-primary-surface));background:linear-gradient(145deg,rgb(var(--ax-sys-color-primary-500)),rgb(var(--ax-sys-color-primary-700)));box-shadow:0 2px 8px rgba(var(--ax-sys-color-primary-500),.35)}.axm-ai-assist-prompt-box .axm-ai-assist-prompt-box__sheet{min-height:0px;flex:auto;padding:.65rem .65rem .55rem;border-radius:1rem;background:rgb(var(--ax-sys-color-lightest-surface));border:1px solid rgba(var(--ax-sys-color-neutral-300),.35);box-shadow:inset 0 1px rgba(var(--ax-sys-color-lightest-surface),.95)}.dark :is(.axm-ai-assist-prompt-box .axm-ai-assist-prompt-box__sheet){background:rgb(var(--ax-sys-color-neutral-800));border-color:rgba(var(--ax-sys-color-neutral-500),.4);box-shadow:inset 0 1px rgba(var(--ax-sys-color-lightest-surface),.06)}.axm-ai-assist-prompt-box .axm-ai-assist-prompt-box__body{min-height:0px;flex:auto;overflow-y:auto}.axm-ai-assist-prompt-box .axm-ai-assist-prompt-box__prompt-input{display:block;min-height:0px;width:100%;--tw-border-style: none !important;border-style:none!important;background-color:transparent!important;--tw-shadow: 0 0 #0000 !important;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)!important}.axm-ai-assist-prompt-box .axm-ai-assist-prompt-box__prompt-input:focus-within{--tw-border-style: none !important;border-style:none!important;--tw-shadow: 0 0 #0000 !important;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)!important;--tw-outline-style: none !important;outline-style:none!important}.axm-ai-assist-prompt-box .axm-ai-assist-prompt-box__prompt-input:focus-within>*{--tw-border-style: none !important;border-style:none!important;--tw-shadow: 0 0 #0000 !important;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)!important;--tw-outline-style: none !important;outline-style:none!important}.axm-ai-assist-prompt-box .axm-ai-assist-prompt-box__prompt-input textarea{min-height:4.5rem;max-height:min(30vh,240px);font-size:.8125rem;line-height:1.45;padding-block:.55rem;padding-inline:.65rem;border-radius:.65rem;--tw-border-style: none !important;border-style:none!important;background-color:transparent!important;--tw-shadow: 0 0 #0000 !important;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)!important}.axm-ai-assist-prompt-box .axm-ai-assist-prompt-box__prompt-input textarea:focus,.axm-ai-assist-prompt-box .axm-ai-assist-prompt-box__prompt-input textarea:focus-visible,.axm-ai-assist-prompt-box .axm-ai-assist-prompt-box__prompt-input textarea:active{--tw-border-style: none !important;border-style:none!important;--tw-shadow: 0 0 #0000 !important;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)!important;--tw-outline-style: none !important;outline-style:none!important;outline-offset:0!important}.dark :is(.axm-ai-assist-prompt-box .axm-ai-assist-prompt-box__prompt-input textarea){color:rgb(var(--ax-sys-color-on-lightest-surface));background:rgb(var(--ax-sys-color-neutral-900))!important}.axm-ai-assist-prompt-box .axm-ai-assist-prompt-box__footer{min-height:0}.axm-ai-assist-prompt-box ax-button.axm-ai-assist-prompt-box__menu-trigger{--ax-comp-button-normal-bg-color: 0, 0, 0, 0 !important;--ax-comp-button-hover-bg-color: 0, 0, 0, 0 !important;--ax-comp-button-pressed-bg-color: 0, 0, 0, 0 !important;--ax-comp-button-border-width: 0 !important;overflow:hidden;border-radius:calc(infinity * 1px)!important;border-color:transparent!important;background-color:transparent!important;background:transparent!important}.axm-ai-assist-prompt-box ax-button.axm-ai-assist-prompt-box__menu-trigger:hover:not(.state-disabled,.state-loading,.state-selected),.axm-ai-assist-prompt-box ax-button.axm-ai-assist-prompt-box__menu-trigger:active:not(.state-disabled,.state-loading,.state-selected){border-color:transparent!important;background-color:transparent!important}.axm-ai-assist-prompt-box ax-button.axm-ai-assist-prompt-box__menu-trigger>button{border-radius:calc(infinity * 1px)!important;--tw-border-style: none !important;border-style:none!important;padding:0!important;width:2.25rem;height:2.25rem;min-width:2.25rem;min-height:2.25rem;color:rgb(var(--ax-sys-color-on-primary-surface))!important;background:linear-gradient(145deg,rgb(var(--ax-sys-color-primary-500)),rgb(var(--ax-sys-color-primary-700)))!important;box-shadow:0 2px 8px rgba(var(--ax-sys-color-primary-500),.35)}.axm-ai-assist-prompt-box ax-button.axm-ai-assist-prompt-box__menu-trigger.ax-state-disabled>button,.axm-ai-assist-prompt-box ax-button.axm-ai-assist-prompt-box__menu-trigger.ax-state-loading>button,.axm-ai-assist-prompt-box ax-button.axm-ai-assist-prompt-box__menu-trigger>button:disabled{opacity:.55;box-shadow:none}.axm-ai-assist-prompt-box ax-button.axm-ai-assist-prompt-box__menu-trigger:hover:not(.state-disabled,.state-loading,.state-selected)>button{filter:brightness(.94)}.axm-ai-assist-prompt-box ax-button.axm-ai-assist-prompt-box__menu-trigger:active:not(.state-disabled,.state-loading,.state-selected)>button{filter:brightness(.88)}.axm-ai-assist-prompt-box ax-button.axm-ai-assist-prompt-box__send-gradient{--ax-comp-button-normal-bg-color: 0, 0, 0, 0 !important;--ax-comp-button-hover-bg-color: 0, 0, 0, 0 !important;--ax-comp-button-pressed-bg-color: 0, 0, 0, 0 !important;--ax-comp-button-border-width: 0 !important;overflow:hidden;border-radius:calc(infinity * 1px)!important;border-color:transparent!important;background-color:transparent!important;background:transparent!important}.axm-ai-assist-prompt-box ax-button.axm-ai-assist-prompt-box__send-gradient:hover:not(.state-disabled,.state-loading,.state-selected),.axm-ai-assist-prompt-box ax-button.axm-ai-assist-prompt-box__send-gradient:active:not(.state-disabled,.state-loading,.state-selected){border-color:transparent!important;background-color:transparent!important}.axm-ai-assist-prompt-box ax-button.axm-ai-assist-prompt-box__send-gradient>button{border-radius:calc(infinity * 1px)!important;--tw-border-style: none !important;border-style:none!important;--tw-font-weight: var(--font-weight-semibold, 600);font-weight:var(--font-weight-semibold, 600);min-height:2.25rem;padding-inline:1rem!important;font-size:.8rem;letter-spacing:.01em;color:rgb(var(--ax-sys-color-on-primary-surface))!important;background:var(--axm-axm-ai-assist-prompt-send-gradient)!important;box-shadow:0 3px 12px rgba(var(--ax-sys-color-secondary-400),.35)}.axm-ai-assist-prompt-box ax-button.axm-ai-assist-prompt-box__send-gradient.ax-state-disabled>button,.axm-ai-assist-prompt-box ax-button.axm-ai-assist-prompt-box__send-gradient.ax-state-loading>button,.axm-ai-assist-prompt-box ax-button.axm-ai-assist-prompt-box__send-gradient>button:disabled{opacity:.55;box-shadow:none}.axm-ai-assist-prompt-box ax-button.axm-ai-assist-prompt-box__send-gradient:hover:not(.state-disabled,.state-loading,.state-selected)>button{filter:brightness(.96)}.axm-ai-assist-prompt-box ax-button.axm-ai-assist-prompt-box__send-gradient:active:not(.state-disabled,.state-loading,.state-selected)>button{filter:brightness(.9)}@property --tw-font-weight{syntax: \"*\"; inherits: false;}@property --tw-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-shadow-color{syntax: \"*\"; inherits: false;}@property --tw-shadow-alpha{syntax: \"<percentage>\"; inherits: false; initial-value: 100%;}@property --tw-inset-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-inset-shadow-color{syntax: \"*\"; inherits: false;}@property --tw-inset-shadow-alpha{syntax: \"<percentage>\"; inherits: false; initial-value: 100%;}@property --tw-ring-color{syntax: \"*\"; inherits: false;}@property --tw-ring-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-inset-ring-color{syntax: \"*\"; inherits: false;}@property --tw-inset-ring-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-ring-inset{syntax: \"*\"; inherits: false;}@property --tw-ring-offset-width{syntax: \"<length>\"; inherits: false; initial-value: 0px;}@property --tw-ring-offset-color{syntax: \"*\"; inherits: false; initial-value: #fff;}@property --tw-ring-offset-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@layer properties{@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-font-weight: initial;--tw-shadow: 0 0 #0000;--tw-shadow-color: initial;--tw-shadow-alpha: 100%;--tw-inset-shadow: 0 0 #0000;--tw-inset-shadow-color: initial;--tw-inset-shadow-alpha: 100%;--tw-ring-color: initial;--tw-ring-shadow: 0 0 #0000;--tw-inset-ring-color: initial;--tw-inset-ring-shadow: 0 0 #0000;--tw-ring-inset: initial;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-offset-shadow: 0 0 #0000}}}\n/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */\n"] }]
|
|
1353
|
+
}], propDecorators: { title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: true }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], initialPrompt: [{ type: i0.Input, args: [{ isSignal: true, alias: "initialPrompt", required: false }] }], contentMaxWidthPx: [{ type: i0.Input, args: [{ isSignal: true, alias: "contentMaxWidthPx", required: false }] }], initialAssistId: [{ type: i0.Input, args: [{ isSignal: true, alias: "initialAssistId", required: false }] }], initialDirectAgentId: [{ type: i0.Input, args: [{ isSignal: true, alias: "initialDirectAgentId", required: false }] }], runPrompt: [{ type: i0.Input, args: [{ isSignal: true, alias: "runPrompt", required: true }] }], requestClose: [{ type: i0.Input, args: [{ isSignal: true, alias: "requestClose", required: true }] }] } });
|
|
1354
|
+
|
|
1355
|
+
//#endregion
|
|
1356
|
+
//#region ---- Service ----
|
|
1357
|
+
/**
|
|
1358
|
+
* Opens the AI assist prompt box in a bottom overlay using {@link AXOverlayService} (`centered: false`, panel CSS).
|
|
1359
|
+
*/
|
|
1360
|
+
class AXMAiAssistPromptBoxService {
|
|
1361
|
+
constructor() {
|
|
1362
|
+
//#region ---- Services & Dependencies ----
|
|
1363
|
+
this.overlayService = inject(AXOverlayService);
|
|
1364
|
+
//#endregion
|
|
1365
|
+
//#region ---- Private Properties ----
|
|
1366
|
+
this.overlayRef = null;
|
|
1367
|
+
this.pendingResolve = null;
|
|
1368
|
+
this.settled = false;
|
|
1369
|
+
}
|
|
1370
|
+
//#endregion
|
|
1371
|
+
//#region ---- Public Methods ----
|
|
1372
|
+
/**
|
|
1373
|
+
* Presents the assist prompt box and resolves when the user submits, cancels, or dismisses the overlay.
|
|
1374
|
+
*/
|
|
1375
|
+
open(options, executeRun) {
|
|
1376
|
+
if (this.pendingResolve) {
|
|
1377
|
+
const superseded = this.pendingResolve;
|
|
1378
|
+
this.pendingResolve = null;
|
|
1379
|
+
superseded(null);
|
|
1380
|
+
}
|
|
1381
|
+
this.disposeOverlayOnly();
|
|
1382
|
+
return new Promise((resolve) => {
|
|
1383
|
+
this.pendingResolve = resolve;
|
|
1384
|
+
this.settled = false;
|
|
1385
|
+
const requestClose = (result) => {
|
|
1386
|
+
this.settle(result);
|
|
1387
|
+
this.overlayRef?.dispose();
|
|
1388
|
+
};
|
|
1389
|
+
void this.overlayService
|
|
1390
|
+
.create(AXMAiAssistPromptBoxComponent, {
|
|
1391
|
+
centered: false,
|
|
1392
|
+
width: 'auto',
|
|
1393
|
+
actionSheetStyle: false,
|
|
1394
|
+
panelClass: ['axm-ai-assist-prompt-box-shell'],
|
|
1395
|
+
backdrop: {
|
|
1396
|
+
enabled: false,
|
|
1397
|
+
background: false,
|
|
1398
|
+
closeOnClick: false,
|
|
1399
|
+
},
|
|
1400
|
+
closeOnEscape: true,
|
|
1401
|
+
closeOnOutsideClick: true,
|
|
1402
|
+
inputs: {
|
|
1403
|
+
title: options.title,
|
|
1404
|
+
placeholder: options.placeholder ?? '',
|
|
1405
|
+
initialPrompt: options.initialPrompt ?? '',
|
|
1406
|
+
initialAssistId: options.assistId,
|
|
1407
|
+
initialDirectAgentId: options.directAgentId,
|
|
1408
|
+
runPrompt: executeRun,
|
|
1409
|
+
requestClose,
|
|
1410
|
+
},
|
|
1411
|
+
onDispose: () => {
|
|
1412
|
+
this.overlayRef = null;
|
|
1413
|
+
if (!this.settled && this.pendingResolve) {
|
|
1414
|
+
this.settle(null);
|
|
1415
|
+
}
|
|
1416
|
+
},
|
|
1417
|
+
})
|
|
1418
|
+
.then((ref) => {
|
|
1419
|
+
this.overlayRef = ref;
|
|
1420
|
+
})
|
|
1421
|
+
.catch(() => {
|
|
1422
|
+
this.settle(null);
|
|
1423
|
+
});
|
|
1424
|
+
});
|
|
1425
|
+
}
|
|
1426
|
+
//#endregion
|
|
1427
|
+
//#region ---- Utility Methods ----
|
|
1428
|
+
settle(result) {
|
|
1429
|
+
if (this.settled) {
|
|
1430
|
+
return;
|
|
1431
|
+
}
|
|
1432
|
+
const done = this.pendingResolve;
|
|
1433
|
+
if (!done) {
|
|
1434
|
+
return;
|
|
1435
|
+
}
|
|
1436
|
+
this.settled = true;
|
|
1437
|
+
this.pendingResolve = null;
|
|
1438
|
+
done(result);
|
|
1439
|
+
}
|
|
1440
|
+
disposeOverlayOnly() {
|
|
1441
|
+
if (this.overlayRef) {
|
|
1442
|
+
this.overlayRef.dispose();
|
|
1443
|
+
this.overlayRef = null;
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXMAiAssistPromptBoxService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
1447
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXMAiAssistPromptBoxService, providedIn: 'root' }); }
|
|
1448
|
+
}
|
|
1449
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXMAiAssistPromptBoxService, decorators: [{
|
|
1450
|
+
type: Injectable,
|
|
1451
|
+
args: [{ providedIn: 'root' }]
|
|
1452
|
+
}] });
|
|
1453
|
+
|
|
1454
|
+
//#region ---- Imports ----
|
|
1455
|
+
//#endregion
|
|
1456
|
+
//#region ---- Legacy → contract ----
|
|
1457
|
+
/**
|
|
1458
|
+
* Maps legacy `expectStructuredOutput` flags to an {@link AXPAiAgentOutputContract}, or returns explicit `outputContract`.
|
|
1459
|
+
*/
|
|
1460
|
+
function outputContractFromAssistRunInput(input) {
|
|
1461
|
+
if (input.outputContract) {
|
|
1462
|
+
const parsed = parseAgentOutputContract(input.outputContract);
|
|
1463
|
+
if (parsed) {
|
|
1464
|
+
return parsed;
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
if (!input.expectStructuredOutput) {
|
|
1468
|
+
return null;
|
|
1469
|
+
}
|
|
1470
|
+
const instruction = input.structuredOutputInstruction?.trim();
|
|
1471
|
+
const schemaSummary = input.structuredOutputSchemaSummary?.trim();
|
|
1472
|
+
let structuredRetries;
|
|
1473
|
+
if (typeof input.maxStructuredRetries === 'number' && !Number.isNaN(input.maxStructuredRetries)) {
|
|
1474
|
+
structuredRetries = Math.max(0, Math.min(10, Math.floor(input.maxStructuredRetries)));
|
|
1475
|
+
}
|
|
1476
|
+
return {
|
|
1477
|
+
kind: 'object',
|
|
1478
|
+
...(instruction && instruction.length > 0 ? { instruction } : {}),
|
|
1479
|
+
...(schemaSummary && schemaSummary.length > 0 ? { schemaSummary } : {}),
|
|
1480
|
+
...(structuredRetries !== undefined ? { structuredRetries } : {}),
|
|
1481
|
+
};
|
|
1482
|
+
}
|
|
1483
|
+
//#endregion
|
|
1484
|
+
|
|
1485
|
+
//#region ---- Transcript ----
|
|
1486
|
+
/**
|
|
1487
|
+
* Text segments from the last assistant message (`type: 'text'` only).
|
|
1488
|
+
* Omits `think` and `node` segments so JSON parsing is not polluted by widget envelopes or reasoning blocks stored separately.
|
|
1489
|
+
*/
|
|
1490
|
+
function extractLastAssistantTextSegments(messages) {
|
|
1491
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
1492
|
+
const m = messages[i];
|
|
1493
|
+
if (m.role !== 'assistant') {
|
|
1494
|
+
continue;
|
|
1495
|
+
}
|
|
1496
|
+
const parts = [];
|
|
1497
|
+
for (const r of m.responses) {
|
|
1498
|
+
if (r.type === 'text') {
|
|
1499
|
+
parts.push(r.content);
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
return parts;
|
|
1503
|
+
}
|
|
1504
|
+
return [];
|
|
1505
|
+
}
|
|
1506
|
+
/**
|
|
1507
|
+
* Combined text for structured JSON extraction (text segments only).
|
|
1508
|
+
*/
|
|
1509
|
+
function extractLastAssistantStructuredParts(messages) {
|
|
1510
|
+
const segments = extractLastAssistantTextSegments(messages);
|
|
1511
|
+
const combined = segments.join('\n\n').trim();
|
|
1512
|
+
return { combined, segments };
|
|
1513
|
+
}
|
|
1514
|
+
/**
|
|
1515
|
+
* Concatenated plain text from the last assistant message in the transcript (includes `node` payloads like {@link axpAiChatMessageGetText}).
|
|
1516
|
+
*/
|
|
1517
|
+
function extractLastAssistantPlainText(messages) {
|
|
1518
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
1519
|
+
const m = messages[i];
|
|
1520
|
+
if (m.role === 'assistant') {
|
|
1521
|
+
return axpAiChatMessageGetText(m).trim();
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
return '';
|
|
1525
|
+
}
|
|
1526
|
+
//#endregion
|
|
1527
|
+
|
|
1528
|
+
//#region ---- Retry prompt ----
|
|
1529
|
+
/**
|
|
1530
|
+
* User follow-up appended to the transcript when structured JSON parse/validation fails.
|
|
1531
|
+
*
|
|
1532
|
+
* @param validationDetail - Parse or schema validation error text.
|
|
1533
|
+
* @param schemaSummary - Optional expected shape (e.g. TypeScript-style) to improve retry quality.
|
|
1534
|
+
* @param failurePhase - Parse failures get stronger anti-truncation wording; validator failures focus on schema/rules.
|
|
1535
|
+
*/
|
|
1536
|
+
function buildStructuredOutputRetryUserText(validationDetail, schemaSummary, failurePhase) {
|
|
1537
|
+
const detail = validationDetail.trim() || 'Unknown validation error.';
|
|
1538
|
+
const lines = [
|
|
1539
|
+
'The previous response could not be parsed or validated:',
|
|
1540
|
+
detail,
|
|
1541
|
+
'',
|
|
1542
|
+
];
|
|
1543
|
+
if (failurePhase === 'parse') {
|
|
1544
|
+
lines.push('Likely causes: incomplete/truncated JSON, extra prose around the value, or unclosed strings/brackets.', 'Reply with ONE complete JSON value only — fully closed strings, braces, and brackets. Do not stop until the JSON is valid.', '');
|
|
1545
|
+
}
|
|
1546
|
+
else if (failurePhase === 'validator') {
|
|
1547
|
+
lines.push('The JSON was parsed but did not match the required rules or shape. Fix the structure and values; keep the reply as a single JSON value only.', '');
|
|
1548
|
+
}
|
|
1549
|
+
const shape = schemaSummary?.trim();
|
|
1550
|
+
if (shape && shape.length > 0) {
|
|
1551
|
+
lines.push('Expected JSON shape (summary):', shape, '');
|
|
1552
|
+
}
|
|
1553
|
+
lines.push('Return ONLY valid JSON matching the requested structure.', 'Do not include markdown fences or explanations.');
|
|
1554
|
+
return lines.join('\n');
|
|
1555
|
+
}
|
|
1556
|
+
//#endregion
|
|
1557
|
+
|
|
1558
|
+
//#region ---- Parse structured assistant text ----
|
|
1559
|
+
/**
|
|
1560
|
+
* Parses assistant plain text as JSON, supporting optional ```json fences.
|
|
1561
|
+
*/
|
|
1562
|
+
function tryParseStructuredAssistantText(text) {
|
|
1563
|
+
const t = text.trim();
|
|
1564
|
+
if (!t) {
|
|
1565
|
+
return undefined;
|
|
1566
|
+
}
|
|
1567
|
+
try {
|
|
1568
|
+
return JSON.parse(t);
|
|
1569
|
+
}
|
|
1570
|
+
catch {
|
|
1571
|
+
const fence = /^```(?:json)?\s*([\s\S]*?)```$/im.exec(t);
|
|
1572
|
+
if (fence?.[1]) {
|
|
1573
|
+
try {
|
|
1574
|
+
return JSON.parse(fence[1].trim());
|
|
1575
|
+
}
|
|
1576
|
+
catch {
|
|
1577
|
+
return undefined;
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
return undefined;
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
//#endregion
|
|
1584
|
+
|
|
1585
|
+
//#region ---- Imports ----
|
|
1586
|
+
//#endregion
|
|
1587
|
+
//#region ---- Balanced JSON extraction ----
|
|
1588
|
+
function tryParseBalancedJsonObjectAt(s, start) {
|
|
1589
|
+
if (s[start] !== '{') {
|
|
1590
|
+
return null;
|
|
1591
|
+
}
|
|
1592
|
+
let depth = 0;
|
|
1593
|
+
let inString = false;
|
|
1594
|
+
let escape = false;
|
|
1595
|
+
for (let i = start; i < s.length; i++) {
|
|
1596
|
+
const c = s[i];
|
|
1597
|
+
if (inString) {
|
|
1598
|
+
if (escape) {
|
|
1599
|
+
escape = false;
|
|
1600
|
+
continue;
|
|
1601
|
+
}
|
|
1602
|
+
if (c === '\\') {
|
|
1603
|
+
escape = true;
|
|
1604
|
+
continue;
|
|
1605
|
+
}
|
|
1606
|
+
if (c === '"') {
|
|
1607
|
+
inString = false;
|
|
1608
|
+
}
|
|
1609
|
+
continue;
|
|
1610
|
+
}
|
|
1611
|
+
if (c === '"') {
|
|
1612
|
+
inString = true;
|
|
1613
|
+
continue;
|
|
1614
|
+
}
|
|
1615
|
+
if (c === '{') {
|
|
1616
|
+
depth++;
|
|
1617
|
+
}
|
|
1618
|
+
else if (c === '}') {
|
|
1619
|
+
depth--;
|
|
1620
|
+
if (depth === 0) {
|
|
1621
|
+
const slice = s.slice(start, i + 1);
|
|
1622
|
+
try {
|
|
1623
|
+
return { value: JSON.parse(slice), end: i + 1 };
|
|
1624
|
+
}
|
|
1625
|
+
catch {
|
|
1626
|
+
return null;
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
return null;
|
|
1632
|
+
}
|
|
1633
|
+
function tryParseBalancedJsonArrayAt(s, start) {
|
|
1634
|
+
if (s[start] !== '[') {
|
|
1635
|
+
return null;
|
|
1636
|
+
}
|
|
1637
|
+
let depth = 0;
|
|
1638
|
+
let inString = false;
|
|
1639
|
+
let escape = false;
|
|
1640
|
+
for (let i = start; i < s.length; i++) {
|
|
1641
|
+
const c = s[i];
|
|
1642
|
+
if (inString) {
|
|
1643
|
+
if (escape) {
|
|
1644
|
+
escape = false;
|
|
1645
|
+
continue;
|
|
1646
|
+
}
|
|
1647
|
+
if (c === '\\') {
|
|
1648
|
+
escape = true;
|
|
1649
|
+
continue;
|
|
1650
|
+
}
|
|
1651
|
+
if (c === '"') {
|
|
1652
|
+
inString = false;
|
|
1653
|
+
}
|
|
1654
|
+
continue;
|
|
1655
|
+
}
|
|
1656
|
+
if (c === '"') {
|
|
1657
|
+
inString = true;
|
|
1658
|
+
continue;
|
|
1659
|
+
}
|
|
1660
|
+
if (c === '[') {
|
|
1661
|
+
depth++;
|
|
1662
|
+
}
|
|
1663
|
+
else if (c === ']') {
|
|
1664
|
+
depth--;
|
|
1665
|
+
if (depth === 0) {
|
|
1666
|
+
const slice = s.slice(start, i + 1);
|
|
1667
|
+
try {
|
|
1668
|
+
return { value: JSON.parse(slice), end: i + 1 };
|
|
1669
|
+
}
|
|
1670
|
+
catch {
|
|
1671
|
+
return null;
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
return null;
|
|
1677
|
+
}
|
|
1678
|
+
/**
|
|
1679
|
+
* Finds first top-level JSON object or array and returns balanced slice text.
|
|
1680
|
+
*/
|
|
1681
|
+
function extractFirstBalancedJsonSlice(s) {
|
|
1682
|
+
const iObj = s.indexOf('{');
|
|
1683
|
+
const iArr = s.indexOf('[');
|
|
1684
|
+
let start = -1;
|
|
1685
|
+
let kind = null;
|
|
1686
|
+
if (iObj >= 0 && iArr >= 0) {
|
|
1687
|
+
if (iObj <= iArr) {
|
|
1688
|
+
start = iObj;
|
|
1689
|
+
kind = 'obj';
|
|
1690
|
+
}
|
|
1691
|
+
else {
|
|
1692
|
+
start = iArr;
|
|
1693
|
+
kind = 'arr';
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
else if (iObj >= 0) {
|
|
1697
|
+
start = iObj;
|
|
1698
|
+
kind = 'obj';
|
|
1699
|
+
}
|
|
1700
|
+
else if (iArr >= 0) {
|
|
1701
|
+
start = iArr;
|
|
1702
|
+
kind = 'arr';
|
|
1703
|
+
}
|
|
1704
|
+
if (start < 0 || !kind) {
|
|
1705
|
+
return undefined;
|
|
1706
|
+
}
|
|
1707
|
+
const parsed = kind === 'obj' ? tryParseBalancedJsonObjectAt(s, start) : tryParseBalancedJsonArrayAt(s, start);
|
|
1708
|
+
if (!parsed) {
|
|
1709
|
+
return undefined;
|
|
1710
|
+
}
|
|
1711
|
+
return s.slice(start, parsed.end);
|
|
1712
|
+
}
|
|
1713
|
+
//#endregion
|
|
1714
|
+
//#region ---- Lightweight repair ----
|
|
1715
|
+
/**
|
|
1716
|
+
* Removes assistant reasoning tags when they appear inside the text channel (they break JSON extraction).
|
|
1717
|
+
*/
|
|
1718
|
+
function stripAssistantThinkingXml(raw) {
|
|
1719
|
+
return raw
|
|
1720
|
+
.replace(/<(?:redacted_thinking|think)\b[^>]*>[\s\S]*?<\/(?:redacted_thinking|think)>/gi, '')
|
|
1721
|
+
.replace(/<\/?(?:redacted_thinking|think)>/gi, '')
|
|
1722
|
+
.trim();
|
|
1723
|
+
}
|
|
1724
|
+
function stripOuterMarkdownFence(s) {
|
|
1725
|
+
const t = s.trim();
|
|
1726
|
+
const fence = /^```(?:json)?\s*([\s\S]*?)```$/im.exec(t);
|
|
1727
|
+
if (fence?.[1]) {
|
|
1728
|
+
return fence[1].trim();
|
|
1729
|
+
}
|
|
1730
|
+
return t;
|
|
1731
|
+
}
|
|
1732
|
+
/** Removes trailing commas before `}` or `]` outside of strings (best-effort for LLM JSON). */
|
|
1733
|
+
function removeTrailingCommasBeforeBrackets(jsonLike) {
|
|
1734
|
+
return jsonLike.replace(/,(\s*[}\]])/g, '$1');
|
|
1735
|
+
}
|
|
1736
|
+
function tryParseJsonLenient(slice) {
|
|
1737
|
+
const t = slice.trim();
|
|
1738
|
+
if (!t) {
|
|
1739
|
+
return undefined;
|
|
1740
|
+
}
|
|
1741
|
+
try {
|
|
1742
|
+
return JSON.parse(t);
|
|
1743
|
+
}
|
|
1744
|
+
catch {
|
|
1745
|
+
try {
|
|
1746
|
+
return JSON.parse(removeTrailingCommasBeforeBrackets(t));
|
|
1747
|
+
}
|
|
1748
|
+
catch {
|
|
1749
|
+
return undefined;
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
/**
|
|
1754
|
+
* Best-effort parse when the model stopped mid-JSON (truncated completion): closes `{` `[` using a delimiter stack
|
|
1755
|
+
* with string awareness. Unsafe inside unclosed strings; last resort after normal repair.
|
|
1756
|
+
*/
|
|
1757
|
+
function tryRepairTruncatedJsonWithDelimiterStack(raw) {
|
|
1758
|
+
const s = raw.trim();
|
|
1759
|
+
if (!s.length) {
|
|
1760
|
+
return undefined;
|
|
1761
|
+
}
|
|
1762
|
+
const stack = [];
|
|
1763
|
+
let inStr = false;
|
|
1764
|
+
let esc = false;
|
|
1765
|
+
for (let i = 0; i < s.length; i++) {
|
|
1766
|
+
const c = s[i];
|
|
1767
|
+
if (inStr) {
|
|
1768
|
+
if (esc) {
|
|
1769
|
+
esc = false;
|
|
1770
|
+
continue;
|
|
1771
|
+
}
|
|
1772
|
+
if (c === '\\') {
|
|
1773
|
+
esc = true;
|
|
1774
|
+
continue;
|
|
1775
|
+
}
|
|
1776
|
+
if (c === '"') {
|
|
1777
|
+
inStr = false;
|
|
1778
|
+
}
|
|
1779
|
+
continue;
|
|
1780
|
+
}
|
|
1781
|
+
if (c === '"') {
|
|
1782
|
+
inStr = true;
|
|
1783
|
+
continue;
|
|
1784
|
+
}
|
|
1785
|
+
if (c === '{') {
|
|
1786
|
+
stack.push('{');
|
|
1787
|
+
}
|
|
1788
|
+
else if (c === '[') {
|
|
1789
|
+
stack.push('[');
|
|
1790
|
+
}
|
|
1791
|
+
else if (c === '}') {
|
|
1792
|
+
if (stack.length > 0 && stack[stack.length - 1] === '{') {
|
|
1793
|
+
stack.pop();
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
else if (c === ']') {
|
|
1797
|
+
if (stack.length > 0 && stack[stack.length - 1] === '[') {
|
|
1798
|
+
stack.pop();
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
let suffix = '';
|
|
1803
|
+
if (inStr) {
|
|
1804
|
+
suffix = '"';
|
|
1805
|
+
}
|
|
1806
|
+
for (let k = stack.length - 1; k >= 0; k--) {
|
|
1807
|
+
suffix += stack[k] === '{' ? '}' : ']';
|
|
1808
|
+
}
|
|
1809
|
+
if (!suffix.length) {
|
|
1810
|
+
return undefined;
|
|
1811
|
+
}
|
|
1812
|
+
try {
|
|
1813
|
+
return JSON.parse(s + suffix);
|
|
1814
|
+
}
|
|
1815
|
+
catch {
|
|
1816
|
+
return undefined;
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
/**
|
|
1820
|
+
* Parses assistant text after cheap local repairs (fences, trailing commas, first JSON value slice).
|
|
1821
|
+
* Does not call the model.
|
|
1822
|
+
*/
|
|
1823
|
+
function parseStructuredAssistantWithLocalRepair(raw) {
|
|
1824
|
+
const cleaned = stripAssistantThinkingXml(raw);
|
|
1825
|
+
const direct = tryParseStructuredAssistantText(cleaned);
|
|
1826
|
+
if (direct !== undefined) {
|
|
1827
|
+
return direct;
|
|
1828
|
+
}
|
|
1829
|
+
const stripped = stripOuterMarkdownFence(cleaned);
|
|
1830
|
+
let v = tryParseJsonLenient(stripped);
|
|
1831
|
+
if (v !== undefined) {
|
|
1832
|
+
return v;
|
|
1833
|
+
}
|
|
1834
|
+
const slice = extractFirstBalancedJsonSlice(cleaned) ?? extractFirstBalancedJsonSlice(stripped);
|
|
1835
|
+
if (slice !== undefined) {
|
|
1836
|
+
v = tryParseJsonLenient(slice);
|
|
1837
|
+
if (v !== undefined) {
|
|
1838
|
+
return v;
|
|
1839
|
+
}
|
|
1840
|
+
v = tryRepairTruncatedJsonWithDelimiterStack(slice);
|
|
1841
|
+
if (v !== undefined) {
|
|
1842
|
+
return v;
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
v = tryRepairTruncatedJsonWithDelimiterStack(cleaned) ?? tryRepairTruncatedJsonWithDelimiterStack(stripped);
|
|
1846
|
+
if (v !== undefined) {
|
|
1847
|
+
return v;
|
|
1848
|
+
}
|
|
1849
|
+
return undefined;
|
|
1850
|
+
}
|
|
1851
|
+
/**
|
|
1852
|
+
* Tries combined assistant text, then each text segment from last to first (models often put JSON only in the last paragraph).
|
|
1853
|
+
*/
|
|
1854
|
+
function parseStructuredAssistantWithSegmentFallback(combined, segments) {
|
|
1855
|
+
let v = parseStructuredAssistantWithLocalRepair(combined);
|
|
1856
|
+
if (v !== undefined) {
|
|
1857
|
+
return v;
|
|
1858
|
+
}
|
|
1859
|
+
const seen = new Set();
|
|
1860
|
+
const mark = stripAssistantThinkingXml(combined).trim();
|
|
1861
|
+
if (mark.length > 0) {
|
|
1862
|
+
seen.add(mark);
|
|
1863
|
+
}
|
|
1864
|
+
for (let i = segments.length - 1; i >= 0; i--) {
|
|
1865
|
+
const seg = stripAssistantThinkingXml(segments[i] ?? '').trim();
|
|
1866
|
+
if (!seg.length || seen.has(seg)) {
|
|
1867
|
+
continue;
|
|
1868
|
+
}
|
|
1869
|
+
seen.add(seg);
|
|
1870
|
+
v = parseStructuredAssistantWithLocalRepair(seg);
|
|
1871
|
+
if (v !== undefined) {
|
|
1872
|
+
return v;
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
return undefined;
|
|
1876
|
+
}
|
|
1877
|
+
//#endregion
|
|
1878
|
+
|
|
1879
|
+
//#region ---- Imports ----
|
|
1880
|
+
//#endregion
|
|
1881
|
+
//#region ---- Validator ----
|
|
1882
|
+
function typeMatches(schemaType, value) {
|
|
1883
|
+
if (schemaType === undefined) {
|
|
1884
|
+
return true;
|
|
1885
|
+
}
|
|
1886
|
+
switch (schemaType) {
|
|
1887
|
+
case 'object':
|
|
1888
|
+
return value != null && typeof value === 'object' && !Array.isArray(value);
|
|
1889
|
+
case 'array':
|
|
1890
|
+
return Array.isArray(value);
|
|
1891
|
+
case 'string':
|
|
1892
|
+
return typeof value === 'string';
|
|
1893
|
+
case 'number':
|
|
1894
|
+
return typeof value === 'number' && !Number.isNaN(value);
|
|
1895
|
+
case 'integer':
|
|
1896
|
+
return typeof value === 'number' && Number.isInteger(value);
|
|
1897
|
+
case 'boolean':
|
|
1898
|
+
return typeof value === 'boolean';
|
|
1899
|
+
case 'null':
|
|
1900
|
+
return value === null;
|
|
1901
|
+
default:
|
|
1902
|
+
return true;
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
function validateNode(schema, value, path, errors) {
|
|
1906
|
+
if (schema.enum && schema.enum.length > 0) {
|
|
1907
|
+
if (!schema.enum.some((e) => Object.is(e, value))) {
|
|
1908
|
+
errors.push({ path, message: `Value is not one of the allowed enum values.` });
|
|
1909
|
+
return;
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
if (!typeMatches(schema.type, value)) {
|
|
1913
|
+
errors.push({
|
|
1914
|
+
path,
|
|
1915
|
+
message: `Expected type "${schema.type ?? 'any'}", got ${value === null ? 'null' : typeof value}.`,
|
|
1916
|
+
});
|
|
1917
|
+
return;
|
|
1918
|
+
}
|
|
1919
|
+
if (schema.type === 'object' && value != null && typeof value === 'object' && !Array.isArray(value)) {
|
|
1920
|
+
const o = value;
|
|
1921
|
+
const props = schema.properties ?? {};
|
|
1922
|
+
const required = schema.required ?? [];
|
|
1923
|
+
for (const key of required) {
|
|
1924
|
+
if (!(key in o) || o[key] === undefined) {
|
|
1925
|
+
errors.push({ path: path ? `${path}.${key}` : key, message: 'Required property is missing.' });
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
for (const [key, propSchema] of Object.entries(props)) {
|
|
1929
|
+
if (key in o && o[key] !== undefined) {
|
|
1930
|
+
validateNode(propSchema, o[key], path ? `${path}.${key}` : key, errors);
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
if (schema.additionalProperties === false) {
|
|
1934
|
+
for (const key of Object.keys(o)) {
|
|
1935
|
+
if (!(key in props)) {
|
|
1936
|
+
errors.push({
|
|
1937
|
+
path: path ? `${path}.${key}` : key,
|
|
1938
|
+
message: 'Additional property is not allowed.',
|
|
1939
|
+
});
|
|
1940
|
+
}
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
else if (schema.additionalProperties && typeof schema.additionalProperties === 'object') {
|
|
1944
|
+
for (const [key, val] of Object.entries(o)) {
|
|
1945
|
+
if (!(key in props)) {
|
|
1946
|
+
validateNode(schema.additionalProperties, val, path ? `${path}.${key}` : key, errors);
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
if (schema.type === 'array' && Array.isArray(value) && schema.items) {
|
|
1952
|
+
const itemSchema = Array.isArray(schema.items) ? schema.items[0] : schema.items;
|
|
1953
|
+
if (itemSchema) {
|
|
1954
|
+
value.forEach((item, index) => {
|
|
1955
|
+
validateNode(itemSchema, item, `${path}[${index}]`, errors);
|
|
1956
|
+
});
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
/**
|
|
1961
|
+
* Validates a value against a minimal JSON Schema subset (types, required, enum, properties, items).
|
|
1962
|
+
*/
|
|
1963
|
+
function validateJsonSchemaMini(schema, value) {
|
|
1964
|
+
const errors = [];
|
|
1965
|
+
validateNode(schema, value, '', errors);
|
|
1966
|
+
if (errors.length > 0) {
|
|
1967
|
+
return { ok: false, errors };
|
|
1968
|
+
}
|
|
1969
|
+
return { ok: true };
|
|
1970
|
+
}
|
|
1971
|
+
//#endregion
|
|
1972
|
+
|
|
1973
|
+
//#region ---- Imports ----
|
|
1974
|
+
//#endregion
|
|
1975
|
+
//#region ---- Parse + validate ----
|
|
1976
|
+
function defaultParseCommandEnvelope(raw) {
|
|
1977
|
+
if (raw == null || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
1978
|
+
return { valid: false, errorMessage: 'Validation command returned an invalid envelope.' };
|
|
1979
|
+
}
|
|
1980
|
+
const o = raw;
|
|
1981
|
+
if (o['success'] !== true) {
|
|
1982
|
+
const err = typeof o['error'] === 'string' ? o['error'] : 'Validation command failed.';
|
|
1983
|
+
return { valid: false, errorMessage: err };
|
|
1984
|
+
}
|
|
1985
|
+
const data = o['data'];
|
|
1986
|
+
if (!data?.valid) {
|
|
1987
|
+
const errs = Array.isArray(data?.errors) ? data.errors.join('; ') : 'Layout validation failed.';
|
|
1988
|
+
return { valid: false, errorMessage: errs };
|
|
1989
|
+
}
|
|
1990
|
+
const normalized = typeof data.normalizedJson === 'string' ? data.normalizedJson.trim() : '';
|
|
1991
|
+
return { valid: true, ...(normalized.length > 0 ? { normalizedJson: normalized } : {}) };
|
|
1992
|
+
}
|
|
1993
|
+
/**
|
|
1994
|
+
* Parses assistant text and validates against contract schema, registry command, and optional custom validator.
|
|
1995
|
+
*/
|
|
1996
|
+
async function validateAgainstContract(params) {
|
|
1997
|
+
const { contract, assistantPlainText, textSegmentsFallback } = params;
|
|
1998
|
+
if (!axpAiAgentOutputContractUsesStructuredFinalize(contract)) {
|
|
1999
|
+
const trimmed = stripAssistantThinkingXml(assistantPlainText).trim();
|
|
2000
|
+
if (!trimmed.length && !(textSegmentsFallback?.some((s) => (s ?? '').trim().length > 0))) {
|
|
2001
|
+
return {
|
|
2002
|
+
ok: false,
|
|
2003
|
+
errors: [{ phase: 'parse', message: 'Assistant returned empty output.' }],
|
|
2004
|
+
};
|
|
2005
|
+
}
|
|
2006
|
+
return { ok: true, value: trimmed };
|
|
2007
|
+
}
|
|
2008
|
+
const combined = stripAssistantThinkingXml(assistantPlainText);
|
|
2009
|
+
const parsed = parseStructuredAssistantWithSegmentFallback(combined, textSegmentsFallback ?? []);
|
|
2010
|
+
if (parsed === undefined) {
|
|
2011
|
+
return {
|
|
2012
|
+
ok: false,
|
|
2013
|
+
errors: [
|
|
2014
|
+
{
|
|
2015
|
+
phase: 'parse',
|
|
2016
|
+
message: 'Could not parse assistant output as JSON (expected a JSON object or array; optional markdown fence; segment fallback and local repair also failed).',
|
|
2017
|
+
},
|
|
2018
|
+
],
|
|
2019
|
+
};
|
|
2020
|
+
}
|
|
2021
|
+
let value = parsed;
|
|
2022
|
+
if (contract.kind === 'array' && !Array.isArray(value)) {
|
|
2023
|
+
return {
|
|
2024
|
+
ok: false,
|
|
2025
|
+
errors: [{ phase: 'validator', message: 'Expected a JSON array as the root value.' }],
|
|
2026
|
+
};
|
|
2027
|
+
}
|
|
2028
|
+
if (contract.kind === 'object' && (value == null || typeof value !== 'object' || Array.isArray(value))) {
|
|
2029
|
+
return {
|
|
2030
|
+
ok: false,
|
|
2031
|
+
errors: [{ phase: 'validator', message: 'Expected a JSON object as the root value.' }],
|
|
2032
|
+
};
|
|
2033
|
+
}
|
|
2034
|
+
if (contract.schema) {
|
|
2035
|
+
const schemaResult = validateJsonSchemaMini(contract.schema, value);
|
|
2036
|
+
if (!schemaResult.ok) {
|
|
2037
|
+
const message = schemaResult.errors.map((e) => `${e.path || '(root)'}: ${e.message}`).join('; ');
|
|
2038
|
+
return {
|
|
2039
|
+
ok: false,
|
|
2040
|
+
errors: [{ phase: 'validator', message, details: schemaResult.errors }],
|
|
2041
|
+
};
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
const cmd = contract.validationCommandName?.trim();
|
|
2045
|
+
if (cmd && params.commandExecutor) {
|
|
2046
|
+
try {
|
|
2047
|
+
const raw = await params.commandExecutor.execute(cmd, { json: value });
|
|
2048
|
+
const parseEnvelope = params.parseCommandEnvelope ?? defaultParseCommandEnvelope;
|
|
2049
|
+
const envelope = parseEnvelope(raw);
|
|
2050
|
+
if (!envelope.valid) {
|
|
2051
|
+
return {
|
|
2052
|
+
ok: false,
|
|
2053
|
+
errors: [
|
|
2054
|
+
{
|
|
2055
|
+
phase: 'validator',
|
|
2056
|
+
message: envelope.errorMessage ?? 'Validation command failed.',
|
|
2057
|
+
},
|
|
2058
|
+
],
|
|
2059
|
+
};
|
|
2060
|
+
}
|
|
2061
|
+
if (envelope.normalizedJson) {
|
|
2062
|
+
try {
|
|
2063
|
+
value = JSON.parse(envelope.normalizedJson);
|
|
2064
|
+
}
|
|
2065
|
+
catch {
|
|
2066
|
+
value = envelope.normalizedJson;
|
|
2067
|
+
}
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
catch (err) {
|
|
2071
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2072
|
+
return {
|
|
2073
|
+
ok: false,
|
|
2074
|
+
errors: [{ phase: 'validator', message: msg.trim().length > 0 ? msg : 'Validation command failed.' }],
|
|
2075
|
+
};
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
if (params.structuredValidator) {
|
|
2079
|
+
try {
|
|
2080
|
+
value = await params.structuredValidator(value);
|
|
2081
|
+
}
|
|
2082
|
+
catch (err) {
|
|
2083
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2084
|
+
return {
|
|
2085
|
+
ok: false,
|
|
2086
|
+
errors: [{ phase: 'validator', message: msg.trim().length > 0 ? msg : 'Structured validation failed.' }],
|
|
2087
|
+
};
|
|
2088
|
+
}
|
|
2089
|
+
}
|
|
2090
|
+
return { ok: true, value };
|
|
2091
|
+
}
|
|
2092
|
+
//#endregion
|
|
2093
|
+
|
|
2094
|
+
//#region ---- Imports ----
|
|
2095
|
+
//#endregion
|
|
2096
|
+
//#region ---- Usage aggregation ----
|
|
2097
|
+
function mergeEngineUsageTotals(acc, next) {
|
|
2098
|
+
if (!acc && !next) {
|
|
2099
|
+
return undefined;
|
|
2100
|
+
}
|
|
2101
|
+
const promptTokens = (acc?.promptTokens ?? 0) + (next?.promptTokens ?? 0);
|
|
2102
|
+
const completionTokens = (acc?.completionTokens ?? 0) + (next?.completionTokens ?? 0);
|
|
2103
|
+
const totalTokens = (acc?.totalTokens ?? 0) + (next?.totalTokens ?? 0);
|
|
2104
|
+
const cachedPromptTokens = (acc?.cachedPromptTokens ?? 0) + (next?.cachedPromptTokens ?? 0);
|
|
2105
|
+
return {
|
|
2106
|
+
promptTokens,
|
|
2107
|
+
completionTokens,
|
|
2108
|
+
totalTokens,
|
|
2109
|
+
...(cachedPromptTokens > 0 ? { cachedPromptTokens } : {}),
|
|
2110
|
+
};
|
|
2111
|
+
}
|
|
2112
|
+
function engineTotalsToSnapshot(totals) {
|
|
2113
|
+
if (!totals) {
|
|
2114
|
+
return undefined;
|
|
2115
|
+
}
|
|
2116
|
+
return {
|
|
2117
|
+
promptTokens: totals.promptTokens,
|
|
2118
|
+
completionTokens: totals.completionTokens,
|
|
2119
|
+
totalTokens: totals.totalTokens,
|
|
2120
|
+
...(totals.cachedPromptTokens != null && totals.cachedPromptTokens > 0
|
|
2121
|
+
? { cachedPromptTokens: totals.cachedPromptTokens }
|
|
2122
|
+
: {}),
|
|
2123
|
+
};
|
|
2124
|
+
}
|
|
2125
|
+
//#endregion
|
|
2126
|
+
//#region ---- Error snapshot ----
|
|
2127
|
+
const STRUCTURED_OUTPUT_ERROR_TEXT_MAX = 8000;
|
|
2128
|
+
function truncateForStructuredOutputError(text) {
|
|
2129
|
+
if (text.length <= STRUCTURED_OUTPUT_ERROR_TEXT_MAX) {
|
|
2130
|
+
return text;
|
|
2131
|
+
}
|
|
2132
|
+
return `${text.slice(0, STRUCTURED_OUTPUT_ERROR_TEXT_MAX)}…`;
|
|
2133
|
+
}
|
|
2134
|
+
/**
|
|
2135
|
+
* Runs the engine, validates JSON (+ optional custom validator), and appends user retry messages on failure.
|
|
2136
|
+
*/
|
|
2137
|
+
async function runStructuredOutputPipeline(params) {
|
|
2138
|
+
const retryCap = params.maxStructuredRetries ?? axpAiAgentOutputContractStructuredRetryCap(params.contract);
|
|
2139
|
+
const maxAttempts = params.retryOnInvalidStructure ? 1 + Math.max(0, retryCap) : 1;
|
|
2140
|
+
const schemaSummary = params.structuredOutputSchemaSummary?.trim() || params.contract.schemaSummary?.trim();
|
|
2141
|
+
let messages = params.initialMessages;
|
|
2142
|
+
let mergedUsage;
|
|
2143
|
+
let lastProvider;
|
|
2144
|
+
let prevFailureStructuredFingerprint;
|
|
2145
|
+
let prevFailurePhase;
|
|
2146
|
+
const perAttemptUsage = [];
|
|
2147
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
2148
|
+
const result = await params.executeRun(messages);
|
|
2149
|
+
mergedUsage = mergeEngineUsageTotals(mergedUsage, result.usageTotals);
|
|
2150
|
+
if (result.lastProviderResult !== undefined) {
|
|
2151
|
+
lastProvider = result.lastProviderResult;
|
|
2152
|
+
}
|
|
2153
|
+
messages = result.messages;
|
|
2154
|
+
const displayText = extractLastAssistantPlainText(messages);
|
|
2155
|
+
const structuredParts = extractLastAssistantStructuredParts(messages);
|
|
2156
|
+
const structuredFingerprint = stripAssistantThinkingXml(structuredParts.combined).trim();
|
|
2157
|
+
const attemptUsageSnapshot = engineTotalsToSnapshot(result.usageTotals);
|
|
2158
|
+
perAttemptUsage.push({
|
|
2159
|
+
attemptIndex: attempt,
|
|
2160
|
+
usage: attemptUsageSnapshot,
|
|
2161
|
+
hasLastProviderResult: result.lastProviderResult !== undefined,
|
|
2162
|
+
});
|
|
2163
|
+
if (attempt > 1 &&
|
|
2164
|
+
prevFailureStructuredFingerprint !== undefined &&
|
|
2165
|
+
structuredFingerprint === prevFailureStructuredFingerprint &&
|
|
2166
|
+
prevFailurePhase === 'validator') {
|
|
2167
|
+
throw new AXPAiStructuredOutputError({
|
|
2168
|
+
message: 'Assistant returned the same output as the previous attempt while schema validation still failed; stopped to avoid excess token usage.',
|
|
2169
|
+
attempts: attempt,
|
|
2170
|
+
lastAssistantText: truncateForStructuredOutputError(displayText),
|
|
2171
|
+
validationMessage: 'The model repeated identical assistant text after a successful JSON parse; no further retries will be requested for this run.',
|
|
2172
|
+
code: 'duplicate_assistant_output',
|
|
2173
|
+
});
|
|
2174
|
+
}
|
|
2175
|
+
const validation = await validateAgainstContract({
|
|
2176
|
+
contract: params.contract,
|
|
2177
|
+
assistantPlainText: structuredParts.combined,
|
|
2178
|
+
textSegmentsFallback: structuredParts.segments,
|
|
2179
|
+
structuredValidator: params.structuredValidator,
|
|
2180
|
+
commandExecutor: params.commandExecutor,
|
|
2181
|
+
});
|
|
2182
|
+
if (validation.ok) {
|
|
2183
|
+
const structuredRunRaw = {
|
|
2184
|
+
kind: 'structured-output',
|
|
2185
|
+
attempts: perAttemptUsage,
|
|
2186
|
+
totalUsage: engineTotalsToSnapshot(mergedUsage),
|
|
2187
|
+
finalAttempt: attempt,
|
|
2188
|
+
lastProviderResult: lastProvider,
|
|
2189
|
+
};
|
|
2190
|
+
return { success: true,
|
|
2191
|
+
outputKind: params.contract.kind,
|
|
2192
|
+
text: displayText,
|
|
2193
|
+
data: validation.value,
|
|
2194
|
+
structured: validation.value,
|
|
2195
|
+
messages: result.messages,
|
|
2196
|
+
structuredRunRaw,
|
|
2197
|
+
};
|
|
2198
|
+
}
|
|
2199
|
+
const firstError = validation.errors[0];
|
|
2200
|
+
prevFailureStructuredFingerprint = structuredFingerprint;
|
|
2201
|
+
prevFailurePhase = firstError?.phase;
|
|
2202
|
+
const canRetry = params.retryOnInvalidStructure && attempt < maxAttempts;
|
|
2203
|
+
if (!canRetry) {
|
|
2204
|
+
throw new AXPAiStructuredOutputError({
|
|
2205
|
+
message: `Structured output validation failed after ${attempt} attempt(s).`,
|
|
2206
|
+
attempts: attempt,
|
|
2207
|
+
lastAssistantText: truncateForStructuredOutputError(displayText),
|
|
2208
|
+
validationMessage: firstError?.message ?? 'Validation failed.',
|
|
2209
|
+
code: 'validation_exhausted',
|
|
2210
|
+
});
|
|
2211
|
+
}
|
|
2212
|
+
messages = [
|
|
2213
|
+
...result.messages,
|
|
2214
|
+
axpAiChatTextMessage('user', buildStructuredOutputRetryUserText(firstError?.message ?? 'Validation failed.', schemaSummary, firstError?.phase ?? 'parse')),
|
|
2215
|
+
];
|
|
2216
|
+
}
|
|
2217
|
+
throw new AXPAiStructuredOutputError({
|
|
2218
|
+
message: 'Structured output validation failed (unexpected end of retry loop).',
|
|
2219
|
+
attempts: maxAttempts,
|
|
2220
|
+
lastAssistantText: '',
|
|
2221
|
+
validationMessage: 'Unexpected pipeline state.',
|
|
2222
|
+
code: 'validation_exhausted',
|
|
2223
|
+
});
|
|
2224
|
+
}
|
|
2225
|
+
//#endregion
|
|
2226
|
+
|
|
2227
|
+
//#region ---- Imports ----
|
|
2228
|
+
//#endregion
|
|
2229
|
+
//#region ---- Segment id ----
|
|
2230
|
+
function isOutputContractSegmentId(value) {
|
|
2231
|
+
return value === 'text' || value === 'node' || value === 'followUp' || value === 'file';
|
|
2232
|
+
}
|
|
2233
|
+
//#endregion
|
|
2234
|
+
class AXPAiOutputContractResolveService {
|
|
2235
|
+
constructor() {
|
|
2236
|
+
//#region ---- Dependencies ----
|
|
2237
|
+
this.segmentService = inject(AXPOutputContractTranscriptSegmentService);
|
|
2238
|
+
}
|
|
2239
|
+
//#endregion
|
|
2240
|
+
//#region ---- Public API ----
|
|
2241
|
+
/**
|
|
2242
|
+
* Resolves a persisted agent/assist `outputContract` value (segment id string)
|
|
2243
|
+
* or a full contract object (runtime / programmatic).
|
|
2244
|
+
*/
|
|
2245
|
+
async resolveFromPersisted(raw) {
|
|
2246
|
+
if (raw == null) {
|
|
2247
|
+
return null;
|
|
2248
|
+
}
|
|
2249
|
+
if (typeof raw === 'string') {
|
|
2250
|
+
const id = raw.trim();
|
|
2251
|
+
if (!id.length) {
|
|
2252
|
+
return null;
|
|
2253
|
+
}
|
|
2254
|
+
if (isOutputContractSegmentId(id)) {
|
|
2255
|
+
return (await this.segmentService.getContractPreset(id)) ?? null;
|
|
2256
|
+
}
|
|
2257
|
+
return parseAgentOutputContract(raw);
|
|
2258
|
+
}
|
|
2259
|
+
if (typeof raw === 'object' && !Array.isArray(raw)) {
|
|
2260
|
+
return parseAgentOutputContract(raw);
|
|
2261
|
+
}
|
|
2262
|
+
return null;
|
|
2263
|
+
}
|
|
2264
|
+
/**
|
|
2265
|
+
* Effective contract for one engine turn.
|
|
2266
|
+
* Priority: runtime > input > agent > assist > `{ kind: 'string' }`.
|
|
2267
|
+
*/
|
|
2268
|
+
async resolveEffective(params) {
|
|
2269
|
+
const runtime = await this.resolveFromPersisted(params.runtime);
|
|
2270
|
+
if (runtime) {
|
|
2271
|
+
return runtime;
|
|
2272
|
+
}
|
|
2273
|
+
const input = await this.resolveFromPersisted(params.input);
|
|
2274
|
+
if (input) {
|
|
2275
|
+
return input;
|
|
2276
|
+
}
|
|
2277
|
+
const agent = await this.resolveFromPersisted(params.agent);
|
|
2278
|
+
if (agent) {
|
|
2279
|
+
return agent;
|
|
2280
|
+
}
|
|
2281
|
+
const assist = await this.resolveFromPersisted(params.assist);
|
|
2282
|
+
if (assist) {
|
|
2283
|
+
return assist;
|
|
2284
|
+
}
|
|
2285
|
+
return resolveAgentOutputContract(null);
|
|
2286
|
+
}
|
|
2287
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXPAiOutputContractResolveService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
2288
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXPAiOutputContractResolveService, providedIn: 'root' }); }
|
|
2289
|
+
}
|
|
2290
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXPAiOutputContractResolveService, decorators: [{
|
|
2291
|
+
type: Injectable,
|
|
2292
|
+
args: [{
|
|
2293
|
+
providedIn: 'root',
|
|
2294
|
+
}]
|
|
2295
|
+
}] });
|
|
2296
|
+
|
|
2297
|
+
//#region ---- Imports ----
|
|
2298
|
+
//#endregion
|
|
2299
|
+
//#region ---- Assist prompt run loading dialog ----
|
|
2300
|
+
const AI_ASSIST_RUN_LOADING_DIALOG_CLOSE_DELAY_MS = 200;
|
|
2301
|
+
//#endregion
|
|
2302
|
+
//#region ---- Assist run helpers ----
|
|
2303
|
+
function mergePlatformRuntimeContext(base, notes) {
|
|
2304
|
+
if (!notes || Object.keys(notes).length === 0) {
|
|
2305
|
+
return base;
|
|
2306
|
+
}
|
|
2307
|
+
return { ...base, ...notes };
|
|
2308
|
+
}
|
|
2309
|
+
function buildUserPromptBody(input) {
|
|
2310
|
+
const parts = [];
|
|
2311
|
+
const prompt = input.prompt?.trim() ?? '';
|
|
2312
|
+
if (prompt.length > 0) {
|
|
2313
|
+
parts.push(prompt);
|
|
2314
|
+
}
|
|
2315
|
+
const hints = input.context?.hints?.filter((h) => (h ?? '').toString().trim().length > 0) ?? [];
|
|
2316
|
+
if (hints.length > 0) {
|
|
2317
|
+
parts.push('', 'Hints:', ...hints.map((h) => `- ${String(h).trim()}`));
|
|
2318
|
+
}
|
|
2319
|
+
const structured = input.context?.structuredContext;
|
|
2320
|
+
if (structured !== undefined && structured !== null) {
|
|
2321
|
+
let serialized;
|
|
2322
|
+
try {
|
|
2323
|
+
serialized = typeof structured === 'string' ? structured : JSON.stringify(structured, null, 2);
|
|
2324
|
+
}
|
|
2325
|
+
catch {
|
|
2326
|
+
serialized = String(structured);
|
|
2327
|
+
}
|
|
2328
|
+
parts.push('', 'Context (structured):', serialized);
|
|
2329
|
+
}
|
|
2330
|
+
if (input.expectStructuredOutput) {
|
|
2331
|
+
const extra = input.structuredOutputInstruction?.trim();
|
|
2332
|
+
parts.push('', 'Structured output requirements:', '- Reply with exactly one JSON value (a single object or array). It must be complete and valid: close every string, bracket, and brace.', '- Do not truncate — finish the entire structure in one message.', '- No markdown code fences, no text or commentary before or after the JSON.', ...(extra && extra.length > 0 ? ['', 'Additional output rules from the task:', extra] : []));
|
|
2333
|
+
const schemaSummary = input.structuredOutputSchemaSummary?.trim();
|
|
2334
|
+
if (schemaSummary && schemaSummary.length > 0) {
|
|
2335
|
+
parts.push('', 'Expected JSON shape (summary):', schemaSummary);
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
2338
|
+
return parts.join('\n').trim();
|
|
2339
|
+
}
|
|
2340
|
+
function buildChatMessages(input) {
|
|
2341
|
+
const messages = [];
|
|
2342
|
+
const systemInstruction = input.context?.systemInstruction?.trim();
|
|
2343
|
+
if (systemInstruction) {
|
|
2344
|
+
messages.push(axpAiChatTextMessage('system', systemInstruction));
|
|
2345
|
+
}
|
|
2346
|
+
const body = buildUserPromptBody(input);
|
|
2347
|
+
const media = (input.userMediaAttachments ?? []).filter((a) => a.fileId?.trim());
|
|
2348
|
+
if (media.length > 0) {
|
|
2349
|
+
const responses = [];
|
|
2350
|
+
for (const a of media) {
|
|
2351
|
+
const fileId = a.fileId.trim();
|
|
2352
|
+
responses.push({
|
|
2353
|
+
type: 'file',
|
|
2354
|
+
content: { fileId },
|
|
2355
|
+
});
|
|
2356
|
+
}
|
|
2357
|
+
const textBody = body.trim().length > 0 ? body : '(empty prompt)';
|
|
2358
|
+
responses.push({ type: 'text', content: textBody });
|
|
2359
|
+
messages.push(axpAiChatUserMessage(responses));
|
|
2360
|
+
}
|
|
2361
|
+
else {
|
|
2362
|
+
messages.push(axpAiChatTextMessage('user', body.trim().length > 0 ? body : '(empty prompt)'));
|
|
2363
|
+
}
|
|
2364
|
+
return messages;
|
|
2365
|
+
}
|
|
2366
|
+
//#endregion
|
|
2367
|
+
/**
|
|
2368
|
+
* Frontend facade for AI: assist catalog, tenant defaults, engine runs, structured assist runs, and prompt UI.
|
|
2369
|
+
*/
|
|
2370
|
+
class AXPAiManagerService extends AXPAiAssistService {
|
|
2371
|
+
constructor() {
|
|
2372
|
+
super(...arguments);
|
|
2373
|
+
//#region ---- Services & Dependencies ----
|
|
2374
|
+
this.aiEngine = inject(AXPAiEngine);
|
|
2375
|
+
this.outputContractResolve = inject(AXPAiOutputContractResolveService);
|
|
2376
|
+
this.entityService = inject(AXPEntityService);
|
|
2377
|
+
this.settings = inject(AXPSettingsService);
|
|
2378
|
+
this.runtimeContextBuilder = inject(AXPAiPlatformRuntimeContextBuilder);
|
|
2379
|
+
this.assistPromptBox = inject(AXMAiAssistPromptBoxService);
|
|
2380
|
+
this.localeId = inject(LOCALE_ID);
|
|
2381
|
+
this.loadingDialog = inject(AXLoadingDialogService);
|
|
2382
|
+
this.assistData = this.entityService
|
|
2383
|
+
.withEntity(RootConfig.module.name, RootConfig.entities.assist.name)
|
|
2384
|
+
.data();
|
|
2385
|
+
this.modelEntityData = this.entityService
|
|
2386
|
+
.withEntity(RootConfig.module.name, RootConfig.entities.model.name)
|
|
2387
|
+
.data();
|
|
2388
|
+
this.agentEntityData = this.entityService
|
|
2389
|
+
.withEntity(RootConfig.module.name, RootConfig.entities.agent.name)
|
|
2390
|
+
.data();
|
|
2391
|
+
}
|
|
2392
|
+
//#endregion
|
|
2393
|
+
//#region ---- Assist catalog (frontend) ----
|
|
2394
|
+
/**
|
|
2395
|
+
* Loads the effective assist from `DefaultAssistId` (tenant setting), or the assist named `platform-assistant`.
|
|
2396
|
+
*/
|
|
2397
|
+
async getEffectiveAssist() {
|
|
2398
|
+
const raw = await this.settings.get(AXPAiManagementSettings.DefaultAssistId);
|
|
2399
|
+
const assistId = typeof raw === 'string'
|
|
2400
|
+
? raw.trim()
|
|
2401
|
+
: raw?.value != null
|
|
2402
|
+
? String(raw.value).trim()
|
|
2403
|
+
: '';
|
|
2404
|
+
if (assistId) {
|
|
2405
|
+
const byId = await this.assistData.byKey(assistId);
|
|
2406
|
+
if (byId) {
|
|
2407
|
+
return byId;
|
|
2408
|
+
}
|
|
2409
|
+
const byName = await this.findAssistByName(assistId);
|
|
2410
|
+
if (byName) {
|
|
2411
|
+
return byName;
|
|
2412
|
+
}
|
|
2413
|
+
throw new Error(`AI assist with id "${assistId}" was not found. Open AI Management → AI Settings and pick a valid default assist.`);
|
|
2414
|
+
}
|
|
2415
|
+
const record = await this.findAssistByName('platform-assistant');
|
|
2416
|
+
if (!record) {
|
|
2417
|
+
throw new Error('No default assist is configured. Create an assist named "platform-assistant" or select one under AI Settings.');
|
|
2418
|
+
}
|
|
2419
|
+
return record;
|
|
2420
|
+
}
|
|
2421
|
+
async findAssistByName(name) {
|
|
2422
|
+
const queryResult = await this.assistData.query({
|
|
2423
|
+
skip: 0,
|
|
2424
|
+
take: 1,
|
|
2425
|
+
filter: {
|
|
2426
|
+
field: 'name',
|
|
2427
|
+
operator: { type: 'equal' },
|
|
2428
|
+
value: name,
|
|
2429
|
+
},
|
|
2430
|
+
});
|
|
2431
|
+
return (queryResult?.items ?? [])[0] ?? null;
|
|
2432
|
+
}
|
|
2433
|
+
/**
|
|
2434
|
+
* Resolves the assist row for chat: explicit primary key, otherwise the effective default from settings.
|
|
2435
|
+
*/
|
|
2436
|
+
async getAssistForChat(assistId) {
|
|
2437
|
+
const id = assistId?.trim();
|
|
2438
|
+
if (id) {
|
|
2439
|
+
const row = await this.assistData.byKey(id);
|
|
2440
|
+
if (row) {
|
|
2441
|
+
return row;
|
|
2442
|
+
}
|
|
2443
|
+
const byName = await this.findAssistByName(id);
|
|
2444
|
+
if (byName) {
|
|
2445
|
+
return byName;
|
|
2446
|
+
}
|
|
2447
|
+
throw new Error(`Assist "${id}" was not found. Open AI Management → Assists or clear the assist id in the chat URL.`);
|
|
2448
|
+
}
|
|
2449
|
+
return this.getEffectiveAssist();
|
|
2450
|
+
}
|
|
2451
|
+
/**
|
|
2452
|
+
* Primary catalog model transport for an assist (`modelId` row).
|
|
2453
|
+
*/
|
|
2454
|
+
async getAssistPrimaryChatTransport(assistId) {
|
|
2455
|
+
const assist = await this.getAssistForChat(assistId);
|
|
2456
|
+
const modelEntityId = assist.modelId?.trim();
|
|
2457
|
+
if (!modelEntityId) {
|
|
2458
|
+
return undefined;
|
|
2459
|
+
}
|
|
2460
|
+
const model = await this.modelEntityData.byKey(modelEntityId);
|
|
2461
|
+
const raw = String(model?.chatTransport ?? '').toLowerCase();
|
|
2462
|
+
if (raw === 'openai' || raw === 'gemini' || raw === 'demis') {
|
|
2463
|
+
return raw;
|
|
2464
|
+
}
|
|
2465
|
+
return undefined;
|
|
2466
|
+
}
|
|
2467
|
+
//#endregion
|
|
2468
|
+
//#region ---- Engine delegation ----
|
|
2469
|
+
/**
|
|
2470
|
+
* Low-level engine run (chat streaming and tool orchestration).
|
|
2471
|
+
*/
|
|
2472
|
+
runEngine(options) {
|
|
2473
|
+
return this.aiEngine.run(options);
|
|
2474
|
+
}
|
|
2475
|
+
//#endregion
|
|
2476
|
+
//#region ---- Assist API (run / show) ----
|
|
2477
|
+
/**
|
|
2478
|
+
* Normalizes AiModel.maxOutputTokens from the catalog.
|
|
2479
|
+
*/
|
|
2480
|
+
normalizeMaxOutputTokens(value) {
|
|
2481
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) {
|
|
2482
|
+
return undefined;
|
|
2483
|
+
}
|
|
2484
|
+
return Math.floor(value);
|
|
2485
|
+
}
|
|
2486
|
+
/**
|
|
2487
|
+
* Reads {@link AXMAiModelMaxTokensRow.maxOutputTokens} for the model bound to the assist or direct agent.
|
|
2488
|
+
*/
|
|
2489
|
+
async resolveCatalogMaxOutputTokens(assistId, directAgentId) {
|
|
2490
|
+
try {
|
|
2491
|
+
if (directAgentId) {
|
|
2492
|
+
const agent = await this.agentEntityData.byKey(directAgentId);
|
|
2493
|
+
const modelEntityId = agent?.modelEntityId?.trim();
|
|
2494
|
+
if (!modelEntityId) {
|
|
2495
|
+
return undefined;
|
|
2496
|
+
}
|
|
2497
|
+
const model = await this.modelEntityData.byKey(modelEntityId);
|
|
2498
|
+
return this.normalizeMaxOutputTokens(model?.maxOutputTokens);
|
|
2499
|
+
}
|
|
2500
|
+
const assist = await this.getAssistForChat(assistId);
|
|
2501
|
+
const modelEntityId = assist.modelId?.trim();
|
|
2502
|
+
if (!modelEntityId) {
|
|
2503
|
+
return undefined;
|
|
2504
|
+
}
|
|
2505
|
+
const model = await this.modelEntityData.byKey(modelEntityId);
|
|
2506
|
+
return this.normalizeMaxOutputTokens(model?.maxOutputTokens);
|
|
2507
|
+
}
|
|
2508
|
+
catch {
|
|
2509
|
+
return undefined;
|
|
2510
|
+
}
|
|
2511
|
+
}
|
|
2512
|
+
/** @inheritdoc */
|
|
2513
|
+
async run(input) {
|
|
2514
|
+
const baseCtx = await this.runtimeContextBuilder.build();
|
|
2515
|
+
const platformRuntimeContext = mergePlatformRuntimeContext(baseCtx, input.platformRuntimeNotes ?? null);
|
|
2516
|
+
const directAgentId = input.directAgentId?.trim();
|
|
2517
|
+
let assistId = input.assistId?.trim();
|
|
2518
|
+
const assistRow = !directAgentId && !assistId
|
|
2519
|
+
? await this.getEffectiveAssist()
|
|
2520
|
+
: await this.getAssistForChat(assistId);
|
|
2521
|
+
if (!directAgentId && !assistId) {
|
|
2522
|
+
assistId = assistRow.id;
|
|
2523
|
+
}
|
|
2524
|
+
const agentRow = directAgentId ? await this.agentEntityData.byKey(directAgentId) : null;
|
|
2525
|
+
const inputContract = outputContractFromAssistRunInput(input);
|
|
2526
|
+
const effectiveOutputContract = await this.outputContractResolve.resolveEffective({
|
|
2527
|
+
input: inputContract,
|
|
2528
|
+
assist: assistRow.outputContract,
|
|
2529
|
+
agent: agentRow?.outputContract,
|
|
2530
|
+
});
|
|
2531
|
+
const usesStructuredPipeline = input.expectStructuredOutput === true ||
|
|
2532
|
+
axpAiAgentOutputContractUsesStructuredFinalize(effectiveOutputContract);
|
|
2533
|
+
const messages = buildChatMessages(input);
|
|
2534
|
+
const catalogMax = await this.resolveCatalogMaxOutputTokens(assistId, directAgentId);
|
|
2535
|
+
const effectiveMaxTokens = input.maxTokens ?? catalogMax ?? AXPAiAssistDefaultStructuredMaxTokens;
|
|
2536
|
+
const runOptions = {
|
|
2537
|
+
platformRuntimeContext,
|
|
2538
|
+
outputContract: effectiveOutputContract,
|
|
2539
|
+
...(directAgentId ? { directAgentId } : { assistId: assistId }),
|
|
2540
|
+
...(input.maxSteps !== undefined ? { maxSteps: input.maxSteps } : {}),
|
|
2541
|
+
...(input.temperature !== undefined ? { temperature: input.temperature } : {}),
|
|
2542
|
+
maxTokens: effectiveMaxTokens,
|
|
2543
|
+
};
|
|
2544
|
+
if (!usesStructuredPipeline) {
|
|
2545
|
+
const result = await this.runEngine({ ...runOptions, messages });
|
|
2546
|
+
const text = extractLastAssistantPlainText(result.messages);
|
|
2547
|
+
return { success: true, outputKind: effectiveOutputContract.kind,
|
|
2548
|
+
text,
|
|
2549
|
+
raw: { usageTotals: result.usageTotals, lastProviderResult: result.lastProviderResult },
|
|
2550
|
+
messages: result.messages,
|
|
2551
|
+
};
|
|
2552
|
+
}
|
|
2553
|
+
const retryOnInvalidStructure = input.retryOnInvalidStructure !== false;
|
|
2554
|
+
const maxStructuredRetries = input.maxStructuredRetries ?? AXPAiAssistDefaultMaxStructuredRetries;
|
|
2555
|
+
const structuredMaxTokens = input.structuredOutputMaxTokens ?? effectiveMaxTokens;
|
|
2556
|
+
const pipelineResult = await runStructuredOutputPipeline({
|
|
2557
|
+
contract: effectiveOutputContract,
|
|
2558
|
+
initialMessages: messages,
|
|
2559
|
+
executeRun: (nextMessages) => this.runEngine({ ...runOptions, maxTokens: structuredMaxTokens, messages: nextMessages }),
|
|
2560
|
+
maxStructuredRetries,
|
|
2561
|
+
retryOnInvalidStructure,
|
|
2562
|
+
structuredValidator: input.structuredValidator,
|
|
2563
|
+
structuredOutputSchemaSummary: input.structuredOutputSchemaSummary,
|
|
2564
|
+
});
|
|
2565
|
+
return {
|
|
2566
|
+
success: pipelineResult.success,
|
|
2567
|
+
outputKind: pipelineResult.outputKind,
|
|
2568
|
+
text: pipelineResult.text,
|
|
2569
|
+
structured: pipelineResult.structured,
|
|
2570
|
+
data: pipelineResult.data,
|
|
2571
|
+
validationErrors: pipelineResult.validationErrors,
|
|
2572
|
+
messages: pipelineResult.messages,
|
|
2573
|
+
raw: pipelineResult.structuredRunRaw,
|
|
2574
|
+
};
|
|
2575
|
+
}
|
|
2576
|
+
/** @inheritdoc */
|
|
2577
|
+
async show(options) {
|
|
2578
|
+
const runLoadingSpec = options.runLoadingDialog;
|
|
2579
|
+
const data = await this.assistPromptBox.open(options, async (payload) => {
|
|
2580
|
+
let progressDialog;
|
|
2581
|
+
if (runLoadingSpec) {
|
|
2582
|
+
progressDialog = this.loadingDialog.show({
|
|
2583
|
+
title: runLoadingSpec.title,
|
|
2584
|
+
mode: 'indeterminate',
|
|
2585
|
+
progressColor: 'primary',
|
|
2586
|
+
text: runLoadingSpec.text,
|
|
2587
|
+
});
|
|
2588
|
+
}
|
|
2589
|
+
try {
|
|
2590
|
+
return await this.run({
|
|
2591
|
+
prompt: payload.prompt,
|
|
2592
|
+
userMediaAttachments: payload.userMediaAttachments.length > 0 ? payload.userMediaAttachments : undefined,
|
|
2593
|
+
context: options.context,
|
|
2594
|
+
assistId: payload.assistId,
|
|
2595
|
+
directAgentId: payload.directAgentId,
|
|
2596
|
+
maxSteps: options.maxSteps,
|
|
2597
|
+
temperature: options.temperature,
|
|
2598
|
+
maxTokens: options.maxTokens,
|
|
2599
|
+
outputContract: options.outputContract,
|
|
2600
|
+
expectStructuredOutput: options.expectStructuredOutput,
|
|
2601
|
+
structuredOutputInstruction: options.structuredOutputInstruction,
|
|
2602
|
+
platformRuntimeNotes: options.platformRuntimeNotes,
|
|
2603
|
+
maxStructuredRetries: options.maxStructuredRetries,
|
|
2604
|
+
structuredValidator: options.structuredValidator,
|
|
2605
|
+
retryOnInvalidStructure: options.retryOnInvalidStructure,
|
|
2606
|
+
structuredOutputSchemaSummary: options.structuredOutputSchemaSummary,
|
|
2607
|
+
structuredOutputMaxTokens: options.structuredOutputMaxTokens,
|
|
2608
|
+
});
|
|
2609
|
+
}
|
|
2610
|
+
catch (err) {
|
|
2611
|
+
if (progressDialog !== undefined) {
|
|
2612
|
+
try {
|
|
2613
|
+
progressDialog.setProgressColor('danger');
|
|
2614
|
+
progressDialog.setProgressText(err instanceof Error ? err.message : String(err));
|
|
2615
|
+
}
|
|
2616
|
+
catch {
|
|
2617
|
+
// Ignore dialog update failures.
|
|
2618
|
+
}
|
|
2619
|
+
}
|
|
2620
|
+
throw err;
|
|
2621
|
+
}
|
|
2622
|
+
finally {
|
|
2623
|
+
if (progressDialog !== undefined) {
|
|
2624
|
+
const dlg = progressDialog;
|
|
2625
|
+
setTimeout(() => dlg.close(), AI_ASSIST_RUN_LOADING_DIALOG_CLOSE_DELAY_MS);
|
|
2626
|
+
}
|
|
2627
|
+
}
|
|
2628
|
+
});
|
|
2629
|
+
return data ?? null;
|
|
2630
|
+
}
|
|
2631
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXPAiManagerService, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
2632
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXPAiManagerService, providedIn: 'root' }); }
|
|
2633
|
+
}
|
|
2634
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXPAiManagerService, decorators: [{
|
|
2635
|
+
type: Injectable,
|
|
2636
|
+
args: [{ providedIn: 'root' }]
|
|
2637
|
+
}] });
|
|
2638
|
+
|
|
2639
|
+
//#region ---- Imports ----
|
|
2640
|
+
//#endregion
|
|
2641
|
+
//#region ---- Config ----
|
|
2642
|
+
const AXMAgentRegistryToolConfiguratorWidget = {
|
|
2643
|
+
name: 'agent-registry-tool-configurator',
|
|
2644
|
+
title: 'Agent registry tool (command / query)',
|
|
2645
|
+
icon: 'fa-light fa-screwdriver-wrench',
|
|
2646
|
+
description: 'Select a command or query definition from the platform registry and optionally set default argument values (persisted with the agent).',
|
|
2647
|
+
categories: AXP_WIDGETS_EDITOR_CATEGORY,
|
|
2648
|
+
groups: [AXPWidgetGroupEnum.UtilityWidget],
|
|
2649
|
+
type: 'editor',
|
|
2650
|
+
defaultFilterWidgetName: 'string-filter',
|
|
2651
|
+
properties: [
|
|
2652
|
+
AXP_NAME_PROPERTY,
|
|
2653
|
+
AXP_DATA_PATH_PROPERTY,
|
|
2654
|
+
{
|
|
2655
|
+
name: 'label',
|
|
2656
|
+
title: 'Label',
|
|
2657
|
+
visible: true,
|
|
2658
|
+
group: { name: 'general', title: 'General', order: 1 },
|
|
2659
|
+
schema: {
|
|
2660
|
+
dataType: 'string',
|
|
2661
|
+
defaultValue: '',
|
|
2662
|
+
interface: {
|
|
2663
|
+
path: 'label',
|
|
2664
|
+
type: 'text-box',
|
|
2665
|
+
options: {
|
|
2666
|
+
placeholder: 'Field label',
|
|
2667
|
+
},
|
|
2668
|
+
},
|
|
2669
|
+
},
|
|
2670
|
+
},
|
|
2671
|
+
{
|
|
2672
|
+
name: 'registryKind',
|
|
2673
|
+
title: 'Registry kind',
|
|
2674
|
+
visible: true,
|
|
2675
|
+
group: { name: 'behavior', title: 'Behavior', order: 2 },
|
|
2676
|
+
schema: {
|
|
2677
|
+
dataType: 'string',
|
|
2678
|
+
defaultValue: 'command',
|
|
2679
|
+
interface: {
|
|
2680
|
+
path: 'options.registryKind',
|
|
2681
|
+
type: 'select-editor',
|
|
2682
|
+
options: {
|
|
2683
|
+
items: [
|
|
2684
|
+
{ id: 'command', title: 'Command' },
|
|
2685
|
+
{ id: 'query', title: 'Query' },
|
|
2686
|
+
],
|
|
2687
|
+
valueField: 'id',
|
|
2688
|
+
textField: 'title',
|
|
2689
|
+
},
|
|
2690
|
+
},
|
|
2691
|
+
},
|
|
2692
|
+
},
|
|
2693
|
+
{
|
|
2694
|
+
name: 'readonly',
|
|
2695
|
+
title: 'Readonly',
|
|
2696
|
+
visible: true,
|
|
2697
|
+
group: { name: 'behavior', title: 'Behavior', order: 2 },
|
|
2698
|
+
schema: {
|
|
2699
|
+
dataType: 'string',
|
|
2700
|
+
interface: {
|
|
2701
|
+
path: 'options.readonly',
|
|
2702
|
+
type: 'text-box',
|
|
2703
|
+
options: {
|
|
2704
|
+
placeholder: 'true / false / expression',
|
|
2705
|
+
},
|
|
2706
|
+
},
|
|
2707
|
+
},
|
|
2708
|
+
},
|
|
2709
|
+
{
|
|
2710
|
+
name: 'dataSource',
|
|
2711
|
+
title: 'Data source override',
|
|
2712
|
+
visible: true,
|
|
2713
|
+
group: { name: 'data', title: 'Data', order: 3 },
|
|
2714
|
+
schema: {
|
|
2715
|
+
dataType: 'string',
|
|
2716
|
+
interface: {
|
|
2717
|
+
path: 'options.dataSource',
|
|
2718
|
+
type: 'text-box',
|
|
2719
|
+
options: {
|
|
2720
|
+
placeholder: 'Optional; defaults by registry kind',
|
|
2721
|
+
},
|
|
2722
|
+
},
|
|
2723
|
+
},
|
|
2724
|
+
},
|
|
2725
|
+
],
|
|
2726
|
+
components: {
|
|
2727
|
+
edit: {
|
|
2728
|
+
component: () => import('./acorex-platform-module-ai-management-client-agent-registry-tool-configurator-widget-edit.component-CMsc3P9O.mjs').then((c) => c.AXMAgentRegistryToolConfiguratorWidgetEditComponent),
|
|
2729
|
+
},
|
|
2730
|
+
designer: {
|
|
2731
|
+
component: () => import('./acorex-platform-module-ai-management-client-agent-registry-tool-configurator-widget-edit.component-CMsc3P9O.mjs').then((c) => c.AXMAgentRegistryToolConfiguratorWidgetEditComponent),
|
|
2732
|
+
},
|
|
2733
|
+
column: {
|
|
2734
|
+
component: () => import('./acorex-platform-module-ai-management-client-agent-registry-tool-configurator-widget-column.component-BHdDG8z0.mjs').then((c) => c.AXMAgentRegistryToolConfiguratorWidgetColumnComponent),
|
|
2735
|
+
},
|
|
2736
|
+
},
|
|
2737
|
+
};
|
|
2738
|
+
//#endregion
|
|
2739
|
+
|
|
2740
|
+
//#region ---- Imports ----
|
|
2741
|
+
//#endregion
|
|
2742
|
+
//#region ---- Config ----
|
|
2743
|
+
const AXMAssistDelegatedAgentConfiguratorWidget = {
|
|
2744
|
+
name: 'assist-delegated-agent-configurator',
|
|
2745
|
+
title: 'Assist Delegated Agent Configurator',
|
|
2746
|
+
icon: 'fa-light fa-user-robot',
|
|
2747
|
+
description: 'Select a delegated agent and configure injected delegated parameters.',
|
|
2748
|
+
categories: AXP_WIDGETS_EDITOR_CATEGORY,
|
|
2749
|
+
groups: [AXPWidgetGroupEnum.UtilityWidget],
|
|
2750
|
+
type: 'editor',
|
|
2751
|
+
defaultFilterWidgetName: 'string-filter',
|
|
2752
|
+
properties: [
|
|
2753
|
+
AXP_NAME_PROPERTY,
|
|
2754
|
+
AXP_DATA_PATH_PROPERTY,
|
|
2755
|
+
{
|
|
2756
|
+
name: 'label',
|
|
2757
|
+
title: 'Label',
|
|
2758
|
+
visible: true,
|
|
2759
|
+
group: { name: 'general', title: 'General', order: 1 },
|
|
2760
|
+
schema: {
|
|
2761
|
+
dataType: 'string',
|
|
2762
|
+
defaultValue: '@ai-management:assists.entities.assist.fields.delegated-agent.title',
|
|
2763
|
+
interface: {
|
|
2764
|
+
path: 'label',
|
|
2765
|
+
type: 'text-box',
|
|
2766
|
+
options: {
|
|
2767
|
+
placeholder: 'Field label',
|
|
2768
|
+
},
|
|
2769
|
+
},
|
|
2770
|
+
},
|
|
2771
|
+
},
|
|
2772
|
+
{
|
|
2773
|
+
name: 'readonly',
|
|
2774
|
+
title: 'Readonly',
|
|
2775
|
+
visible: true,
|
|
2776
|
+
group: { name: 'behavior', title: 'Behavior', order: 2 },
|
|
2777
|
+
schema: {
|
|
2778
|
+
dataType: 'string',
|
|
2779
|
+
interface: {
|
|
2780
|
+
path: 'options.readonly',
|
|
2781
|
+
type: 'text-box',
|
|
2782
|
+
options: {
|
|
2783
|
+
placeholder: 'true / false / expression',
|
|
2784
|
+
},
|
|
2785
|
+
},
|
|
2786
|
+
},
|
|
2787
|
+
},
|
|
2788
|
+
],
|
|
2789
|
+
components: {
|
|
2790
|
+
edit: {
|
|
2791
|
+
component: () => import('./acorex-platform-module-ai-management-client-assist-delegated-agent-configurator-widget-edit.component-Dmi8vzfr.mjs').then((c) => c.AXMAssistDelegatedAgentConfiguratorWidgetEditComponent),
|
|
2792
|
+
},
|
|
2793
|
+
designer: {
|
|
2794
|
+
component: () => import('./acorex-platform-module-ai-management-client-assist-delegated-agent-configurator-widget-edit.component-Dmi8vzfr.mjs').then((c) => c.AXMAssistDelegatedAgentConfiguratorWidgetEditComponent),
|
|
2795
|
+
},
|
|
2796
|
+
column: {
|
|
2797
|
+
component: () => import('./acorex-platform-module-ai-management-client-assist-delegated-agent-configurator-widget-column.component-CWUhBvpf.mjs').then((c) => c.AXMAssistDelegatedAgentConfiguratorWidgetColumnComponent),
|
|
2798
|
+
},
|
|
2799
|
+
},
|
|
2800
|
+
};
|
|
2801
|
+
//#endregion
|
|
2802
|
+
|
|
2803
|
+
//#endregion
|
|
2804
|
+
//#region ---- AI Management Widgets Provider ----
|
|
2805
|
+
const AI_MANAGEMENT_WIDGETS = [
|
|
2806
|
+
AXMAssistDelegatedAgentConfiguratorWidget,
|
|
2807
|
+
AXMAgentRegistryToolConfiguratorWidget,
|
|
2808
|
+
];
|
|
2809
|
+
class AXMAiManagementWidgetsProvider {
|
|
2810
|
+
getWidgets() {
|
|
2811
|
+
return AI_MANAGEMENT_WIDGETS;
|
|
2812
|
+
}
|
|
2813
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXMAiManagementWidgetsProvider, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
2814
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXMAiManagementWidgetsProvider }); }
|
|
2815
|
+
}
|
|
2816
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXMAiManagementWidgetsProvider, decorators: [{
|
|
2817
|
+
type: Injectable
|
|
2818
|
+
}] });
|
|
2819
|
+
|
|
2820
|
+
//#region ---- Imports ----
|
|
2821
|
+
//#endregion
|
|
2822
|
+
//#region ---- Built-in segment definitions ----
|
|
2823
|
+
/** Single source for entity select options and DI segment presets. */
|
|
2824
|
+
const AXM_BUILTIN_OUTPUT_CONTRACT_TRANSCRIPT_SEGMENT_DEFINITIONS = [
|
|
2825
|
+
{
|
|
2826
|
+
name: 'text',
|
|
2827
|
+
title: '@ai-management:agents.entities.agent.fields.output-contract.transcript-segment.options.text',
|
|
2828
|
+
applicableKinds: ['object'],
|
|
2829
|
+
contract: {
|
|
2830
|
+
kind: 'object',
|
|
2831
|
+
transcriptSegment: 'text',
|
|
2832
|
+
structuredRetries: 2,
|
|
2833
|
+
},
|
|
2834
|
+
},
|
|
2835
|
+
{
|
|
2836
|
+
name: 'node',
|
|
2837
|
+
title: '@ai-management:agents.entities.agent.fields.output-contract.transcript-segment.options.node',
|
|
2838
|
+
applicableKinds: ['object'],
|
|
2839
|
+
contract: {
|
|
2840
|
+
kind: 'object',
|
|
2841
|
+
transcriptSegment: 'node',
|
|
2842
|
+
structuredRetries: 3,
|
|
2843
|
+
},
|
|
2844
|
+
},
|
|
2845
|
+
{
|
|
2846
|
+
name: 'followUp',
|
|
2847
|
+
title: '@ai-management:agents.entities.agent.fields.output-contract.transcript-segment.options.follow-up',
|
|
2848
|
+
applicableKinds: ['object'],
|
|
2849
|
+
contract: {
|
|
2850
|
+
kind: 'object',
|
|
2851
|
+
transcriptSegment: 'followUp',
|
|
2852
|
+
structuredRetries: 2,
|
|
2853
|
+
},
|
|
2854
|
+
},
|
|
2855
|
+
{
|
|
2856
|
+
name: 'file',
|
|
2857
|
+
title: '@ai-management:agents.entities.agent.fields.output-contract.transcript-segment.options.file',
|
|
2858
|
+
applicableKinds: ['string'],
|
|
2859
|
+
contract: {
|
|
2860
|
+
kind: 'string',
|
|
2861
|
+
instruction: 'When a tool persists media (image, audio, etc.), the platform attaches a chat file segment automatically (`{"type":"file","content":{"fileId":"<id>"}}`; optional `"isDownloadable":true` when the user wants a downloadable file — default false for inline preview/playback). User-visible text: at most one friendly sentence. Never paste tool JSON, fileId, or file metadata in text.',
|
|
2862
|
+
},
|
|
2863
|
+
},
|
|
2864
|
+
];
|
|
2865
|
+
//#endregion
|
|
2866
|
+
//#region ---- Provider ----
|
|
2867
|
+
class AXMBuiltinOutputContractTranscriptSegmentProvider {
|
|
2868
|
+
provide() {
|
|
2869
|
+
return AXM_BUILTIN_OUTPUT_CONTRACT_TRANSCRIPT_SEGMENT_DEFINITIONS;
|
|
2870
|
+
}
|
|
2871
|
+
}
|
|
2872
|
+
//#endregion
|
|
2873
|
+
|
|
2874
|
+
//#region ---- Platform runtime time & notes ----
|
|
2875
|
+
class AXMAiPlatformRuntimeContextDefinitionProvider {
|
|
2876
|
+
async items() {
|
|
2877
|
+
return AXM_AI_PLATFORM_RUNTIME_CONTEXT_DEFINITIONS;
|
|
2878
|
+
}
|
|
2879
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXMAiPlatformRuntimeContextDefinitionProvider, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
2880
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXMAiPlatformRuntimeContextDefinitionProvider }); }
|
|
2881
|
+
}
|
|
2882
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXMAiPlatformRuntimeContextDefinitionProvider, decorators: [{
|
|
2883
|
+
type: Injectable
|
|
2884
|
+
}] });
|
|
2885
|
+
|
|
2886
|
+
//#region ---- Imports ----
|
|
2887
|
+
//#endregion
|
|
2888
|
+
//#region ---- Routes ----
|
|
2889
|
+
function routesFactory() {
|
|
2890
|
+
return [];
|
|
2891
|
+
}
|
|
2892
|
+
//#endregion
|
|
2893
|
+
//#region ---- Module ----
|
|
2894
|
+
class AXMAiManagementModule {
|
|
2895
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXMAiManagementModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
|
|
2896
|
+
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "22.0.8", ngImport: i0, type: AXMAiManagementModule, imports: [i1$1.AXPComponentSlotModule, AXPWidgetsModule,
|
|
2897
|
+
AXPRuntimeModule] }); }
|
|
2898
|
+
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXMAiManagementModule, providers: [
|
|
2899
|
+
{
|
|
2900
|
+
provide: ROUTES,
|
|
2901
|
+
multi: true,
|
|
2902
|
+
useFactory: routesFactory,
|
|
2903
|
+
},
|
|
2904
|
+
{
|
|
2905
|
+
provide: AXP_MODULE_MANIFEST_PROVIDER,
|
|
2906
|
+
useValue: AiManagementManifest,
|
|
2907
|
+
multi: true,
|
|
2908
|
+
},
|
|
2909
|
+
{
|
|
2910
|
+
provide: AXP_PERMISSION_DEFINITION_PROVIDER,
|
|
2911
|
+
useClass: AXMPermissionDefinitionProvider,
|
|
2912
|
+
multi: true,
|
|
2913
|
+
},
|
|
2914
|
+
{
|
|
2915
|
+
provide: AXP_FEATURE_DEFINITION_PROVIDER,
|
|
2916
|
+
useClass: AXMAiManagementFeatureDefinitionProvider,
|
|
2917
|
+
multi: true,
|
|
2918
|
+
},
|
|
2919
|
+
{
|
|
2920
|
+
provide: AXP_MENU_PROVIDER,
|
|
2921
|
+
useClass: AXMAiManagementMenuProvider,
|
|
2922
|
+
multi: true,
|
|
2923
|
+
},
|
|
2924
|
+
{
|
|
2925
|
+
provide: AXP_SETTING_DEFINITION_PROVIDER,
|
|
2926
|
+
useClass: AXMAiManagementSettingProvider,
|
|
2927
|
+
multi: true,
|
|
2928
|
+
},
|
|
2929
|
+
{
|
|
2930
|
+
provide: AXP_OUTPUT_CONTRACT_TRANSCRIPT_SEGMENT_PROVIDER,
|
|
2931
|
+
useClass: AXMBuiltinOutputContractTranscriptSegmentProvider,
|
|
2932
|
+
multi: true,
|
|
2933
|
+
},
|
|
2934
|
+
provideQuerySetups([
|
|
2935
|
+
{
|
|
2936
|
+
key: 'AiManagement:ListOutputContractTranscriptSegments',
|
|
2937
|
+
loader: () => import('./acorex-platform-module-ai-management-client-list-output-contract-transcript-segments.query-BQXySOH_.mjs').then((c) => c.AXMListOutputContractTranscriptSegmentsQuery),
|
|
2938
|
+
},
|
|
2939
|
+
{
|
|
2940
|
+
key: 'AiManagement:ListAiCommandRegistryCatalog',
|
|
2941
|
+
loader: () => import('./acorex-platform-module-ai-management-client-list-ai-command-registry-catalog.query-DHfiKVa2.mjs').then((c) => c.AXMListAiCommandRegistryCatalogQuery),
|
|
2942
|
+
},
|
|
2943
|
+
{
|
|
2944
|
+
key: 'AiManagement:ListAiQueryRegistryCatalog',
|
|
2945
|
+
loader: () => import('./acorex-platform-module-ai-management-client-list-ai-query-registry-catalog.query-BX0uU1YX.mjs').then((c) => c.AXMListAiQueryRegistryCatalogQuery),
|
|
2946
|
+
},
|
|
2947
|
+
]),
|
|
2948
|
+
{
|
|
2949
|
+
provide: AXP_WIDGET_DEFINITION_PROVIDER,
|
|
2950
|
+
useClass: AXMAiManagementWidgetsProvider,
|
|
2951
|
+
multi: true,
|
|
2952
|
+
},
|
|
2953
|
+
{
|
|
2954
|
+
provide: AXP_TAG_PROVIDER,
|
|
2955
|
+
useClass: AXMAiModelPurposeTagProvider,
|
|
2956
|
+
multi: true,
|
|
2957
|
+
},
|
|
2958
|
+
{
|
|
2959
|
+
provide: AXPAiAssistService,
|
|
2960
|
+
useExisting: AXPAiManagerService,
|
|
2961
|
+
},
|
|
2962
|
+
{
|
|
2963
|
+
provide: AXP_CONTEXT_DEFINITION_PROVIDER,
|
|
2964
|
+
useClass: AXMAiPlatformRuntimeContextDefinitionProvider,
|
|
2965
|
+
multi: true,
|
|
2966
|
+
},
|
|
2967
|
+
], imports: [AXPComponentSlotModule.forChild({}),
|
|
2968
|
+
AXPWidgetsModule,
|
|
2969
|
+
AXPRuntimeModule] }); }
|
|
2970
|
+
}
|
|
2971
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXMAiManagementModule, decorators: [{
|
|
2972
|
+
type: NgModule,
|
|
2973
|
+
args: [{
|
|
2974
|
+
imports: [
|
|
2975
|
+
AXPComponentSlotModule.forChild({}),
|
|
2976
|
+
AXPWidgetsModule,
|
|
2977
|
+
AXPRuntimeModule,
|
|
2978
|
+
],
|
|
2979
|
+
providers: [
|
|
2980
|
+
{
|
|
2981
|
+
provide: ROUTES,
|
|
2982
|
+
multi: true,
|
|
2983
|
+
useFactory: routesFactory,
|
|
2984
|
+
},
|
|
2985
|
+
{
|
|
2986
|
+
provide: AXP_MODULE_MANIFEST_PROVIDER,
|
|
2987
|
+
useValue: AiManagementManifest,
|
|
2988
|
+
multi: true,
|
|
2989
|
+
},
|
|
2990
|
+
{
|
|
2991
|
+
provide: AXP_PERMISSION_DEFINITION_PROVIDER,
|
|
2992
|
+
useClass: AXMPermissionDefinitionProvider,
|
|
2993
|
+
multi: true,
|
|
2994
|
+
},
|
|
2995
|
+
{
|
|
2996
|
+
provide: AXP_FEATURE_DEFINITION_PROVIDER,
|
|
2997
|
+
useClass: AXMAiManagementFeatureDefinitionProvider,
|
|
2998
|
+
multi: true,
|
|
2999
|
+
},
|
|
3000
|
+
{
|
|
3001
|
+
provide: AXP_MENU_PROVIDER,
|
|
3002
|
+
useClass: AXMAiManagementMenuProvider,
|
|
3003
|
+
multi: true,
|
|
3004
|
+
},
|
|
3005
|
+
{
|
|
3006
|
+
provide: AXP_SETTING_DEFINITION_PROVIDER,
|
|
3007
|
+
useClass: AXMAiManagementSettingProvider,
|
|
3008
|
+
multi: true,
|
|
3009
|
+
},
|
|
3010
|
+
{
|
|
3011
|
+
provide: AXP_OUTPUT_CONTRACT_TRANSCRIPT_SEGMENT_PROVIDER,
|
|
3012
|
+
useClass: AXMBuiltinOutputContractTranscriptSegmentProvider,
|
|
3013
|
+
multi: true,
|
|
3014
|
+
},
|
|
3015
|
+
provideQuerySetups([
|
|
3016
|
+
{
|
|
3017
|
+
key: 'AiManagement:ListOutputContractTranscriptSegments',
|
|
3018
|
+
loader: () => import('./acorex-platform-module-ai-management-client-list-output-contract-transcript-segments.query-BQXySOH_.mjs').then((c) => c.AXMListOutputContractTranscriptSegmentsQuery),
|
|
3019
|
+
},
|
|
3020
|
+
{
|
|
3021
|
+
key: 'AiManagement:ListAiCommandRegistryCatalog',
|
|
3022
|
+
loader: () => import('./acorex-platform-module-ai-management-client-list-ai-command-registry-catalog.query-DHfiKVa2.mjs').then((c) => c.AXMListAiCommandRegistryCatalogQuery),
|
|
3023
|
+
},
|
|
3024
|
+
{
|
|
3025
|
+
key: 'AiManagement:ListAiQueryRegistryCatalog',
|
|
3026
|
+
loader: () => import('./acorex-platform-module-ai-management-client-list-ai-query-registry-catalog.query-BX0uU1YX.mjs').then((c) => c.AXMListAiQueryRegistryCatalogQuery),
|
|
3027
|
+
},
|
|
3028
|
+
]),
|
|
3029
|
+
{
|
|
3030
|
+
provide: AXP_WIDGET_DEFINITION_PROVIDER,
|
|
3031
|
+
useClass: AXMAiManagementWidgetsProvider,
|
|
3032
|
+
multi: true,
|
|
3033
|
+
},
|
|
3034
|
+
{
|
|
3035
|
+
provide: AXP_TAG_PROVIDER,
|
|
3036
|
+
useClass: AXMAiModelPurposeTagProvider,
|
|
3037
|
+
multi: true,
|
|
3038
|
+
},
|
|
3039
|
+
{
|
|
3040
|
+
provide: AXPAiAssistService,
|
|
3041
|
+
useExisting: AXPAiManagerService,
|
|
3042
|
+
},
|
|
3043
|
+
{
|
|
3044
|
+
provide: AXP_CONTEXT_DEFINITION_PROVIDER,
|
|
3045
|
+
useClass: AXMAiPlatformRuntimeContextDefinitionProvider,
|
|
3046
|
+
multi: true,
|
|
3047
|
+
},
|
|
3048
|
+
],
|
|
3049
|
+
}]
|
|
3050
|
+
}] });
|
|
3051
|
+
|
|
3052
|
+
//#region ---- Delegated agent prompt preview ----
|
|
3053
|
+
function normalizeDelegatedAgentToolArgs(argumentsValue) {
|
|
3054
|
+
if (argumentsValue == null) {
|
|
3055
|
+
return null;
|
|
3056
|
+
}
|
|
3057
|
+
if (typeof argumentsValue === 'object' && !Array.isArray(argumentsValue)) {
|
|
3058
|
+
return { ...argumentsValue };
|
|
3059
|
+
}
|
|
3060
|
+
if (typeof argumentsValue === 'string') {
|
|
3061
|
+
const t = argumentsValue.trim();
|
|
3062
|
+
if (!t) {
|
|
3063
|
+
return null;
|
|
3064
|
+
}
|
|
3065
|
+
try {
|
|
3066
|
+
const p = JSON.parse(t);
|
|
3067
|
+
if (p != null && typeof p === 'object' && !Array.isArray(p)) {
|
|
3068
|
+
return p;
|
|
3069
|
+
}
|
|
3070
|
+
}
|
|
3071
|
+
catch {
|
|
3072
|
+
return null;
|
|
3073
|
+
}
|
|
3074
|
+
}
|
|
3075
|
+
return null;
|
|
3076
|
+
}
|
|
3077
|
+
function formatArgsFallback(argumentsValue) {
|
|
3078
|
+
if (argumentsValue == null) {
|
|
3079
|
+
return '';
|
|
3080
|
+
}
|
|
3081
|
+
if (typeof argumentsValue === 'string') {
|
|
3082
|
+
return argumentsValue;
|
|
3083
|
+
}
|
|
3084
|
+
try {
|
|
3085
|
+
return JSON.stringify(argumentsValue, null, 2);
|
|
3086
|
+
}
|
|
3087
|
+
catch {
|
|
3088
|
+
return String(argumentsValue);
|
|
3089
|
+
}
|
|
3090
|
+
}
|
|
3091
|
+
/**
|
|
3092
|
+
* Human-readable specialist prompt from delegated-agent tool arguments (conversation / mini-chat UI).
|
|
3093
|
+
*/
|
|
3094
|
+
function axpAiDelegatedAgentPromptPreview(argumentsValue) {
|
|
3095
|
+
const obj = normalizeDelegatedAgentToolArgs(argumentsValue);
|
|
3096
|
+
if (!obj) {
|
|
3097
|
+
return formatArgsFallback(argumentsValue);
|
|
3098
|
+
}
|
|
3099
|
+
const preferKeys = ['query', 'task', 'prompt', 'message', 'userMessage', 'instruction', 'goal'];
|
|
3100
|
+
for (const k of preferKeys) {
|
|
3101
|
+
const v = obj[k];
|
|
3102
|
+
if (typeof v === 'string' && v.trim()) {
|
|
3103
|
+
return v.trim();
|
|
3104
|
+
}
|
|
3105
|
+
}
|
|
3106
|
+
const keys = Object.keys(obj);
|
|
3107
|
+
if (keys.length === 1) {
|
|
3108
|
+
const only = obj[keys[0]];
|
|
3109
|
+
if (typeof only === 'string' && only.trim()) {
|
|
3110
|
+
return only.trim();
|
|
3111
|
+
}
|
|
3112
|
+
}
|
|
3113
|
+
try {
|
|
3114
|
+
return JSON.stringify(obj, null, 2);
|
|
3115
|
+
}
|
|
3116
|
+
catch {
|
|
3117
|
+
return formatArgsFallback(argumentsValue);
|
|
3118
|
+
}
|
|
3119
|
+
}
|
|
3120
|
+
//#endregion
|
|
3121
|
+
|
|
3122
|
+
//#region ---- Imports ----
|
|
3123
|
+
//#endregion
|
|
3124
|
+
/** Maximum length for assist initial transcript text after HTML strip (characters). */
|
|
3125
|
+
const ASSIST_INITIAL_MESSAGES_MAX_LENGTH = 16_000;
|
|
3126
|
+
//#region ---- HTML / rich text to plain ----
|
|
3127
|
+
/**
|
|
3128
|
+
* Strips HTML tags and decodes a few common entities for safe plain-text transcript lines.
|
|
3129
|
+
* Used for Assist `initialMessages` (may be stored as rich text HTML).
|
|
3130
|
+
*/
|
|
3131
|
+
function axpAiStripAssistInitialHtmlToPlain(raw) {
|
|
3132
|
+
let s = raw
|
|
3133
|
+
.replace(/<\/p>\s*(?:<br\s*\/?>\s*)*<p[^>]*>/gi, '\n\n')
|
|
3134
|
+
.replace(/<\/p>\s*<p[^>]*>/gi, '\n\n')
|
|
3135
|
+
.replace(/<br\s*\/?>/gi, '\n');
|
|
3136
|
+
s = s.replace(/<\/(div|h[1-6]|li|tr)>/gi, '\n');
|
|
3137
|
+
s = s.replace(/<[^>]+>/g, '');
|
|
3138
|
+
s = s
|
|
3139
|
+
.replace(/ /gi, ' ')
|
|
3140
|
+
.replace(/&/g, '&')
|
|
3141
|
+
.replace(/</g, '<')
|
|
3142
|
+
.replace(/>/g, '>')
|
|
3143
|
+
.replace(/"/g, '"')
|
|
3144
|
+
.replace(/'/g, "'");
|
|
3145
|
+
s = s.replace(/\r\n/g, '\n').trim();
|
|
3146
|
+
if (s.length > ASSIST_INITIAL_MESSAGES_MAX_LENGTH) {
|
|
3147
|
+
s = s.slice(0, ASSIST_INITIAL_MESSAGES_MAX_LENGTH).trimEnd() + '…';
|
|
3148
|
+
}
|
|
3149
|
+
return s;
|
|
3150
|
+
}
|
|
3151
|
+
//#endregion
|
|
3152
|
+
//#region ---- Transcript messages ----
|
|
3153
|
+
/**
|
|
3154
|
+
* Converts assist `initialMessages` (HTML or plain) into one or more `assistant` transcript lines.
|
|
3155
|
+
* Non-empty paragraphs (split by blank lines) become separate messages; single block stays one message.
|
|
3156
|
+
*/
|
|
3157
|
+
function axpAiAssistInitialMessagesToTranscript(raw) {
|
|
3158
|
+
const trimmed = raw?.trim() ?? '';
|
|
3159
|
+
if (!trimmed) {
|
|
3160
|
+
return [];
|
|
3161
|
+
}
|
|
3162
|
+
const plain = axpAiStripAssistInitialHtmlToPlain(trimmed);
|
|
3163
|
+
if (!plain) {
|
|
3164
|
+
return [];
|
|
3165
|
+
}
|
|
3166
|
+
const blocks = plain
|
|
3167
|
+
.split(/\n{2,}/)
|
|
3168
|
+
.map((b) => b.replace(/\n+/g, ' ').trim())
|
|
3169
|
+
.filter((b) => b.length > 0);
|
|
3170
|
+
if (blocks.length === 0) {
|
|
3171
|
+
return [];
|
|
3172
|
+
}
|
|
3173
|
+
return blocks.map((text) => axpAiChatTextMessage('assistant', text));
|
|
3174
|
+
}
|
|
3175
|
+
//#endregion
|
|
3176
|
+
|
|
3177
|
+
//#region ---- Imports ----
|
|
3178
|
+
//#endregion
|
|
3179
|
+
//#region ---- Extraction ----
|
|
3180
|
+
/**
|
|
3181
|
+
* Returns non-empty starter prompt labels, preserving repeater order.
|
|
3182
|
+
* Resolve locale in the template via the translate pipe.
|
|
3183
|
+
*/
|
|
3184
|
+
function axpAiAssistStarterPromptLabels(source) {
|
|
3185
|
+
const items = source?.starterPrompts;
|
|
3186
|
+
if (!Array.isArray(items) || items.length === 0) {
|
|
3187
|
+
return [];
|
|
3188
|
+
}
|
|
3189
|
+
const labels = [];
|
|
3190
|
+
for (const item of items) {
|
|
3191
|
+
const text = item?.text;
|
|
3192
|
+
if (!axpAiAssistStarterPromptLabelIsNonEmpty(text)) {
|
|
3193
|
+
continue;
|
|
3194
|
+
}
|
|
3195
|
+
labels.push(text);
|
|
3196
|
+
}
|
|
3197
|
+
return labels;
|
|
3198
|
+
}
|
|
3199
|
+
/**
|
|
3200
|
+
* Returns non-empty starter prompt strings for the active locale, preserving repeater order.
|
|
3201
|
+
*/
|
|
3202
|
+
function axpAiAssistStarterPromptTexts(source, locale) {
|
|
3203
|
+
return axpAiAssistStarterPromptLabels(source)
|
|
3204
|
+
.map((text) => (typeof text === 'string' ? text : resolveMultiLanguageString(text, locale)).trim())
|
|
3205
|
+
.filter((text) => text.length > 0);
|
|
3206
|
+
}
|
|
3207
|
+
function axpAiAssistStarterPromptLabelIsNonEmpty(text) {
|
|
3208
|
+
if (text == null) {
|
|
3209
|
+
return false;
|
|
3210
|
+
}
|
|
3211
|
+
if (typeof text === 'string') {
|
|
3212
|
+
return text.trim().length > 0;
|
|
3213
|
+
}
|
|
3214
|
+
return Object.values(text).some((value) => typeof value === 'string' && value.trim().length > 0);
|
|
3215
|
+
}
|
|
3216
|
+
//#endregion
|
|
3217
|
+
|
|
3218
|
+
//#region ---- Imports ----
|
|
3219
|
+
/**
|
|
3220
|
+
* Per chat run: stores which assist and chat transport are active.
|
|
3221
|
+
* Injected commands (e.g. image generation) read this instead of receiving assist/transport inside tool args.
|
|
3222
|
+
*/
|
|
3223
|
+
class AXPAiChatToolRunContextService {
|
|
3224
|
+
constructor() {
|
|
3225
|
+
this.runContext = signal(null, /* @ts-ignore */
|
|
3226
|
+
...(ngDevMode ? [{ debugName: "runContext" }] : /* istanbul ignore next */ []));
|
|
3227
|
+
}
|
|
3228
|
+
setContext(ctx) {
|
|
3229
|
+
this.runContext.set(ctx);
|
|
3230
|
+
}
|
|
3231
|
+
getContext() {
|
|
3232
|
+
return this.runContext();
|
|
3233
|
+
}
|
|
3234
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXPAiChatToolRunContextService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
3235
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXPAiChatToolRunContextService, providedIn: 'root' }); }
|
|
3236
|
+
}
|
|
3237
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXPAiChatToolRunContextService, decorators: [{
|
|
3238
|
+
type: Injectable,
|
|
3239
|
+
args: [{ providedIn: 'root' }]
|
|
3240
|
+
}] });
|
|
3241
|
+
|
|
3242
|
+
//#region ---- Imports ----
|
|
3243
|
+
//#endregion
|
|
3244
|
+
//#region ---- LLM JSON Schema overrides ----
|
|
3245
|
+
/**
|
|
3246
|
+
* Registry rows map inputs to string-typed JSON schemas. Override parameters for AI chat tools
|
|
3247
|
+
* so models receive accurate types (arrays, booleans, nested objects).
|
|
3248
|
+
*/
|
|
3249
|
+
function getAiChatCommandLlmParametersOverride(commandName) {
|
|
3250
|
+
switch (commandName) {
|
|
3251
|
+
case AIMANAGEMENT_CHAT_GENERATE_IMAGE_COMMAND_KEY:
|
|
3252
|
+
return {
|
|
3253
|
+
type: 'object',
|
|
3254
|
+
properties: {
|
|
3255
|
+
prompt: {
|
|
3256
|
+
type: 'string',
|
|
3257
|
+
description: 'Detailed text description of the image to generate.',
|
|
3258
|
+
},
|
|
3259
|
+
size: {
|
|
3260
|
+
type: 'string',
|
|
3261
|
+
description: 'Optional size hint for backends that support it (e.g. 1024x1024). May be ignored.',
|
|
3262
|
+
},
|
|
3263
|
+
style: {
|
|
3264
|
+
type: 'string',
|
|
3265
|
+
description: 'Optional style hint (e.g. photorealistic, flat vector). May be merged into the prompt by the backend.',
|
|
3266
|
+
},
|
|
3267
|
+
},
|
|
3268
|
+
required: ['prompt'],
|
|
3269
|
+
};
|
|
3270
|
+
case AIMANAGEMENT_CHAT_TRANSCRIBE_SPEECH_COMMAND_KEY:
|
|
3271
|
+
return {
|
|
3272
|
+
type: 'object',
|
|
3273
|
+
properties: {
|
|
3274
|
+
model: {
|
|
3275
|
+
type: 'string',
|
|
3276
|
+
description: 'Optional catalog AiModel row id for speech-to-text. Omit to use options.model on the speech delegated specialist row.',
|
|
3277
|
+
},
|
|
3278
|
+
fileId: {
|
|
3279
|
+
type: 'string',
|
|
3280
|
+
description: 'Platform file storage id for the audio file to transcribe.',
|
|
3281
|
+
},
|
|
3282
|
+
language: {
|
|
3283
|
+
type: 'string',
|
|
3284
|
+
description: 'Optional language hint (e.g. en) when supported by the provider.',
|
|
3285
|
+
},
|
|
3286
|
+
},
|
|
3287
|
+
required: ['fileId'],
|
|
3288
|
+
};
|
|
3289
|
+
case AIMANAGEMENT_CHAT_SYNTHESIZE_SPEECH_COMMAND_KEY:
|
|
3290
|
+
return {
|
|
3291
|
+
type: 'object',
|
|
3292
|
+
properties: {
|
|
3293
|
+
model: {
|
|
3294
|
+
type: 'string',
|
|
3295
|
+
description: 'Catalog AiModel row id for text-to-speech (purpose tts). Pass on the command or rely on agent commandIds options.model; mock default is OpenRouter google/gemini-3.1-flash-tts-preview.',
|
|
3296
|
+
},
|
|
3297
|
+
text: {
|
|
3298
|
+
type: 'string',
|
|
3299
|
+
description: 'Plain text to convert to speech.',
|
|
3300
|
+
},
|
|
3301
|
+
voice: {
|
|
3302
|
+
type: 'string',
|
|
3303
|
+
description: 'Optional voice. Gemini default row: Zephyr, Puck, Charon, Kore, Fenrir, Leda, Orus, Aoede. Omit to use options.voice or Zephyr.',
|
|
3304
|
+
},
|
|
3305
|
+
responseFormat: {
|
|
3306
|
+
type: 'string',
|
|
3307
|
+
description: 'Optional audio format. Gemini default: pcm. OpenRouter also supports mp3 for other TTS models.',
|
|
3308
|
+
},
|
|
3309
|
+
},
|
|
3310
|
+
required: ['text'],
|
|
3311
|
+
};
|
|
3312
|
+
default:
|
|
3313
|
+
return null;
|
|
3314
|
+
}
|
|
3315
|
+
}
|
|
3316
|
+
//#endregion
|
|
3317
|
+
|
|
3318
|
+
//#region ---- Constants ----
|
|
3319
|
+
/** OpenAI / OpenRouter `pcm` TTS output: 16-bit signed LE, mono, 24 kHz. */
|
|
3320
|
+
const AI_CHAT_TTS_PCM_SAMPLE_RATE = 24000;
|
|
3321
|
+
//#endregion
|
|
3322
|
+
//#region ---- MIME helpers ----
|
|
3323
|
+
function isRawPcmMimeType(mimeType) {
|
|
3324
|
+
const mime = mimeType.trim().toLowerCase();
|
|
3325
|
+
return mime === 'audio/pcm' || mime === 'audio/l16' || mime === 'audio/x-pcm' || mime === 'audio/raw';
|
|
3326
|
+
}
|
|
3327
|
+
//#endregion
|
|
3328
|
+
//#region ---- PCM → WAV ----
|
|
3329
|
+
/**
|
|
3330
|
+
* Wraps raw 16-bit signed little-endian PCM in a RIFF WAV container for HTML5 audio playback.
|
|
3331
|
+
*/
|
|
3332
|
+
function wrapPcm16LeInWav(pcmBytes, sampleRate = AI_CHAT_TTS_PCM_SAMPLE_RATE, numChannels = 1) {
|
|
3333
|
+
const bitsPerSample = 16;
|
|
3334
|
+
const blockAlign = (numChannels * bitsPerSample) / 8;
|
|
3335
|
+
const byteRate = sampleRate * blockAlign;
|
|
3336
|
+
const dataSize = pcmBytes.byteLength;
|
|
3337
|
+
const headerSize = 44;
|
|
3338
|
+
const out = new ArrayBuffer(headerSize + dataSize);
|
|
3339
|
+
const view = new DataView(out);
|
|
3340
|
+
const writeStr = (offset, str) => {
|
|
3341
|
+
for (let i = 0; i < str.length; i++) {
|
|
3342
|
+
view.setUint8(offset + i, str.charCodeAt(i));
|
|
3343
|
+
}
|
|
3344
|
+
};
|
|
3345
|
+
writeStr(0, 'RIFF');
|
|
3346
|
+
view.setUint32(4, 36 + dataSize, true);
|
|
3347
|
+
writeStr(8, 'WAVE');
|
|
3348
|
+
writeStr(12, 'fmt ');
|
|
3349
|
+
view.setUint32(16, 16, true);
|
|
3350
|
+
view.setUint16(20, 1, true);
|
|
3351
|
+
view.setUint16(22, numChannels, true);
|
|
3352
|
+
view.setUint32(24, sampleRate, true);
|
|
3353
|
+
view.setUint32(28, byteRate, true);
|
|
3354
|
+
view.setUint16(32, blockAlign, true);
|
|
3355
|
+
view.setUint16(34, bitsPerSample, true);
|
|
3356
|
+
writeStr(36, 'data');
|
|
3357
|
+
view.setUint32(40, dataSize, true);
|
|
3358
|
+
new Uint8Array(out, headerSize).set(new Uint8Array(pcmBytes));
|
|
3359
|
+
return out;
|
|
3360
|
+
}
|
|
3361
|
+
/** Normalizes synthesized speech bytes to a browser-playable container (PCM → WAV). */
|
|
3362
|
+
function prepareTtsBytesForBrowserPlayback(audioBytes, mimeType) {
|
|
3363
|
+
const mt = mimeType.trim() || 'application/octet-stream';
|
|
3364
|
+
if (isRawPcmMimeType(mt)) {
|
|
3365
|
+
return {
|
|
3366
|
+
audioBytes: wrapPcm16LeInWav(audioBytes),
|
|
3367
|
+
mimeType: 'audio/wav',
|
|
3368
|
+
extension: 'wav',
|
|
3369
|
+
};
|
|
3370
|
+
}
|
|
3371
|
+
const lower = mt.toLowerCase();
|
|
3372
|
+
if (lower.includes('mpeg') || lower.includes('mp3')) {
|
|
3373
|
+
return { audioBytes, mimeType: mt, extension: 'mp3' };
|
|
3374
|
+
}
|
|
3375
|
+
if (lower.includes('wav')) {
|
|
3376
|
+
return { audioBytes, mimeType: mt, extension: 'wav' };
|
|
3377
|
+
}
|
|
3378
|
+
if (lower.includes('opus')) {
|
|
3379
|
+
return { audioBytes, mimeType: mt, extension: 'opus' };
|
|
3380
|
+
}
|
|
3381
|
+
if (lower.includes('ogg')) {
|
|
3382
|
+
return { audioBytes, mimeType: mt, extension: 'ogg' };
|
|
3383
|
+
}
|
|
3384
|
+
if (lower.includes('aac')) {
|
|
3385
|
+
return { audioBytes, mimeType: mt, extension: 'aac' };
|
|
3386
|
+
}
|
|
3387
|
+
if (lower.includes('flac')) {
|
|
3388
|
+
return { audioBytes, mimeType: mt, extension: 'flac' };
|
|
3389
|
+
}
|
|
3390
|
+
if (lower.includes('pcm')) {
|
|
3391
|
+
return {
|
|
3392
|
+
audioBytes: wrapPcm16LeInWav(audioBytes),
|
|
3393
|
+
mimeType: 'audio/wav',
|
|
3394
|
+
extension: 'wav',
|
|
3395
|
+
};
|
|
3396
|
+
}
|
|
3397
|
+
return { audioBytes, mimeType: mt, extension: 'bin' };
|
|
3398
|
+
}
|
|
3399
|
+
/**
|
|
3400
|
+
* Builds a blob URL for chat voice playback, wrapping legacy PCM TTS files when needed.
|
|
3401
|
+
*/
|
|
3402
|
+
async function createBrowserPlayableTtsBlobUrl(binary, mimeType, fileName) {
|
|
3403
|
+
const mime = mimeType.trim().toLowerCase();
|
|
3404
|
+
const name = fileName?.trim().toLowerCase() ?? '';
|
|
3405
|
+
const looksLikePcm = isRawPcmMimeType(mime) || (mime === 'application/octet-stream' && name.endsWith('.pcm'));
|
|
3406
|
+
if (!looksLikePcm) {
|
|
3407
|
+
return URL.createObjectURL(binary);
|
|
3408
|
+
}
|
|
3409
|
+
const pcmBytes = await binary.arrayBuffer();
|
|
3410
|
+
return URL.createObjectURL(new Blob([wrapPcm16LeInWav(pcmBytes)], { type: 'audio/wav' }));
|
|
3411
|
+
}
|
|
3412
|
+
//#endregion
|
|
3413
|
+
|
|
3414
|
+
//#region ---- Imports ----
|
|
3415
|
+
//#endregion
|
|
3416
|
+
//#region ---- Stream chunk stripping ----
|
|
3417
|
+
/**
|
|
3418
|
+
* Removes ephemeral SSE chunk arrays from provider `rawResponse` objects.
|
|
3419
|
+
* Assistant text is accumulated during streaming; chunk blobs must not be persisted or exported.
|
|
3420
|
+
*/
|
|
3421
|
+
function axpStripEphemeralStreamChunksFromProviderRaw(raw) {
|
|
3422
|
+
if (raw == null || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
3423
|
+
return raw;
|
|
3424
|
+
}
|
|
3425
|
+
const record = raw;
|
|
3426
|
+
if (record['stream'] !== true || !Array.isArray(record['chunks'])) {
|
|
3427
|
+
return raw;
|
|
3428
|
+
}
|
|
3429
|
+
const rest = { ...record };
|
|
3430
|
+
delete rest['chunks'];
|
|
3431
|
+
delete rest['stream'];
|
|
3432
|
+
return Object.keys(rest).length > 0 ? rest : undefined;
|
|
3433
|
+
}
|
|
3434
|
+
/** Strips stream chunks from {@link AXPAiEngineLastProviderPayload} before message metadata / debug export. */
|
|
3435
|
+
function axpSanitizeLastProviderPayloadForPersistence(payload) {
|
|
3436
|
+
if (!payload) {
|
|
3437
|
+
return undefined;
|
|
3438
|
+
}
|
|
3439
|
+
if (payload.rawResponse === undefined) {
|
|
3440
|
+
return payload;
|
|
3441
|
+
}
|
|
3442
|
+
const rawResponse = axpStripEphemeralStreamChunksFromProviderRaw(payload.rawResponse);
|
|
3443
|
+
if (rawResponse === payload.rawResponse) {
|
|
3444
|
+
return payload;
|
|
3445
|
+
}
|
|
3446
|
+
if (rawResponse === undefined) {
|
|
3447
|
+
const { rawResponse: _removed, ...rest } = payload;
|
|
3448
|
+
return rest;
|
|
3449
|
+
}
|
|
3450
|
+
return { ...payload, rawResponse };
|
|
3451
|
+
}
|
|
3452
|
+
//#endregion
|
|
3453
|
+
|
|
3454
|
+
//#region ---- Imports ----
|
|
3455
|
+
//#endregion
|
|
3456
|
+
//#region ---- Run input snapshot ----
|
|
3457
|
+
/** Builds a JSON-serializable delegated specialist {@link AXPAiEngineRunOptions} snapshot (no callbacks). */
|
|
3458
|
+
function axpBuildDelegatedAgentRunInputSnapshot(delegationCallId, agentId, agentName, specialistArguments, runOptions) {
|
|
3459
|
+
const { onRunEvent: _omit, ...run } = runOptions;
|
|
3460
|
+
const callId = delegationCallId.trim();
|
|
3461
|
+
const catalogId = agentId.trim();
|
|
3462
|
+
return {
|
|
3463
|
+
delegationCallId: callId,
|
|
3464
|
+
agentId: catalogId,
|
|
3465
|
+
...(agentName?.trim() ? { agentName: agentName.trim() } : {}),
|
|
3466
|
+
specialistArguments,
|
|
3467
|
+
run,
|
|
3468
|
+
};
|
|
3469
|
+
}
|
|
3470
|
+
//#endregion
|
|
3471
|
+
|
|
3472
|
+
//#region ---- Imports ----
|
|
3473
|
+
function hasAnyStructuredContent(combined, segments) {
|
|
3474
|
+
if (combined.trim().length > 0) {
|
|
3475
|
+
return true;
|
|
3476
|
+
}
|
|
3477
|
+
if (!segments?.length) {
|
|
3478
|
+
return false;
|
|
3479
|
+
}
|
|
3480
|
+
return segments.some((s) => (s ?? '').trim().length > 0);
|
|
3481
|
+
}
|
|
3482
|
+
/**
|
|
3483
|
+
* Parses JSON from assistant text (including lightweight local repair), then optionally runs a custom validator.
|
|
3484
|
+
*
|
|
3485
|
+
* @param textSegmentsFallback - When provided, each segment is tried after combined text (last segment first); helps when prose and JSON sit in separate assistant text blocks.
|
|
3486
|
+
*/
|
|
3487
|
+
async function validateStructuredAssistantResponse(assistantPlainText, structuredValidator, textSegmentsFallback) {
|
|
3488
|
+
if (!hasAnyStructuredContent(assistantPlainText, textSegmentsFallback)) {
|
|
3489
|
+
return {
|
|
3490
|
+
ok: false,
|
|
3491
|
+
errorMessage: 'Assistant returned empty output.',
|
|
3492
|
+
failurePhase: 'parse',
|
|
3493
|
+
};
|
|
3494
|
+
}
|
|
3495
|
+
const parsed = parseStructuredAssistantWithSegmentFallback(assistantPlainText, textSegmentsFallback ?? []);
|
|
3496
|
+
if (parsed === undefined) {
|
|
3497
|
+
return {
|
|
3498
|
+
ok: false,
|
|
3499
|
+
errorMessage: 'Could not parse assistant output as JSON (expected a JSON object or array; optional markdown fence; segment fallback, local repair, and truncation repair also failed).',
|
|
3500
|
+
failurePhase: 'parse',
|
|
3501
|
+
};
|
|
3502
|
+
}
|
|
3503
|
+
if (!structuredValidator) {
|
|
3504
|
+
return { ok: true, value: parsed };
|
|
3505
|
+
}
|
|
3506
|
+
try {
|
|
3507
|
+
const value = await structuredValidator(parsed);
|
|
3508
|
+
return { ok: true, value };
|
|
3509
|
+
}
|
|
3510
|
+
catch (err) {
|
|
3511
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3512
|
+
return {
|
|
3513
|
+
ok: false,
|
|
3514
|
+
errorMessage: msg.trim().length > 0 ? msg : 'Structured validation failed.',
|
|
3515
|
+
failurePhase: 'validator',
|
|
3516
|
+
};
|
|
3517
|
+
}
|
|
3518
|
+
}
|
|
3519
|
+
//#endregion
|
|
3520
|
+
|
|
3521
|
+
//#region ---- Imports ----
|
|
3522
|
+
//#endregion
|
|
3523
|
+
/**
|
|
3524
|
+
* Lists chat-purpose AI models (catalog), without using {@link AXPAiEngine}.
|
|
3525
|
+
*/
|
|
3526
|
+
class AXPAiAssistChatModelCatalogService {
|
|
3527
|
+
constructor() {
|
|
3528
|
+
//#region ---- Services & Dependencies ----
|
|
3529
|
+
this.entityService = inject(AXPEntityService);
|
|
3530
|
+
this.localeId = inject(LOCALE_ID);
|
|
3531
|
+
this.modelData = this.entityService
|
|
3532
|
+
.withEntity(RootConfig.module.name, RootConfig.entities.model.name)
|
|
3533
|
+
.data();
|
|
3534
|
+
}
|
|
3535
|
+
//#endregion
|
|
3536
|
+
//#region ---- Public API ----
|
|
3537
|
+
/**
|
|
3538
|
+
* All active chat-purpose models in the catalog (assist / agent policy is applied at engine runtime).
|
|
3539
|
+
*/
|
|
3540
|
+
async listModels(_request) {
|
|
3541
|
+
const queryResult = await this.modelData.query({
|
|
3542
|
+
skip: 0,
|
|
3543
|
+
take: 500,
|
|
3544
|
+
});
|
|
3545
|
+
const items = (queryResult?.items ?? []);
|
|
3546
|
+
let rows = items.filter((x) => isChatPurposeModelRow(x));
|
|
3547
|
+
rows = await this.filterModelsActiveForChat(rows);
|
|
3548
|
+
return rows.map((x) => ({
|
|
3549
|
+
id: x.id,
|
|
3550
|
+
name: x.name?.trim() ?? '',
|
|
3551
|
+
title: resolveMultiLanguageString((x.title ?? x.name ?? x.id), this.localeId).trim() || x.id,
|
|
3552
|
+
modelPurposes: readModelPurposes(x),
|
|
3553
|
+
chatTransport: x.chatTransport,
|
|
3554
|
+
maxContextTokens: typeof x.maxContextTokens === 'number' && x.maxContextTokens > 0
|
|
3555
|
+
? x.maxContextTokens
|
|
3556
|
+
: undefined,
|
|
3557
|
+
inputPricePerMillion: typeof x.inputPricePerMillion === 'number' && x.inputPricePerMillion > 0
|
|
3558
|
+
? x.inputPricePerMillion
|
|
3559
|
+
: undefined,
|
|
3560
|
+
outputPricePerMillion: typeof x.outputPricePerMillion === 'number' && x.outputPricePerMillion > 0
|
|
3561
|
+
? x.outputPricePerMillion
|
|
3562
|
+
: undefined,
|
|
3563
|
+
cachedInputPricePerMillion: typeof x.cachedInputPricePerMillion === 'number' && x.cachedInputPricePerMillion > 0
|
|
3564
|
+
? x.cachedInputPricePerMillion
|
|
3565
|
+
: undefined,
|
|
3566
|
+
}));
|
|
3567
|
+
}
|
|
3568
|
+
//#endregion
|
|
3569
|
+
//#region ---- Model filtering ----
|
|
3570
|
+
/**
|
|
3571
|
+
* Chat pickers only use enabled models. Endpoint disabled state is denormalized on the
|
|
3572
|
+
* model (`openAiEndpointIsDisabled`) so tenants never need OpenAI endpoint entity reads.
|
|
3573
|
+
*/
|
|
3574
|
+
async filterModelsActiveForChat(rows) {
|
|
3575
|
+
return rows.filter((x) => {
|
|
3576
|
+
if (x.isDisabled === true) {
|
|
3577
|
+
return false;
|
|
3578
|
+
}
|
|
3579
|
+
if (x.openAiEndpointIsDisabled === true) {
|
|
3580
|
+
return false;
|
|
3581
|
+
}
|
|
3582
|
+
return true;
|
|
3583
|
+
});
|
|
3584
|
+
}
|
|
3585
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXPAiAssistChatModelCatalogService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
3586
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXPAiAssistChatModelCatalogService, providedIn: 'root' }); }
|
|
3587
|
+
}
|
|
3588
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXPAiAssistChatModelCatalogService, decorators: [{
|
|
3589
|
+
type: Injectable,
|
|
3590
|
+
args: [{ providedIn: 'root' }]
|
|
3591
|
+
}] });
|
|
3592
|
+
|
|
3593
|
+
//#region ---- Imports ----
|
|
3594
|
+
//#endregion
|
|
3595
|
+
//#region ---- Service ----
|
|
3596
|
+
class AXPAiDefaultModelPickerService {
|
|
3597
|
+
//#region ---- Public API ----
|
|
3598
|
+
/**
|
|
3599
|
+
* Picks a model from chat/vision catalog candidates.
|
|
3600
|
+
* Prefers {@link preferredModelId} when it supports the purpose; otherwise the first
|
|
3601
|
+
* purpose-capable entry; otherwise the first candidate.
|
|
3602
|
+
*/
|
|
3603
|
+
pickFromChatCatalogForPurpose(models, purpose, preferredModelId) {
|
|
3604
|
+
if (!models.length) {
|
|
3605
|
+
return undefined;
|
|
3606
|
+
}
|
|
3607
|
+
const preferredId = preferredModelId?.trim();
|
|
3608
|
+
if (preferredId) {
|
|
3609
|
+
const preferred = models.find((m) => m.id === preferredId);
|
|
3610
|
+
if (preferred && modelItemSupportsChatCatalogPurpose(preferred, purpose)) {
|
|
3611
|
+
return preferred;
|
|
3612
|
+
}
|
|
3613
|
+
}
|
|
3614
|
+
const byPurpose = models.find((m) => modelItemSupportsChatCatalogPurpose(m, purpose));
|
|
3615
|
+
if (byPurpose) {
|
|
3616
|
+
return byPurpose;
|
|
3617
|
+
}
|
|
3618
|
+
return models[0];
|
|
3619
|
+
}
|
|
3620
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXPAiDefaultModelPickerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
3621
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXPAiDefaultModelPickerService, providedIn: 'root' }); }
|
|
3622
|
+
}
|
|
3623
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXPAiDefaultModelPickerService, decorators: [{
|
|
3624
|
+
type: Injectable,
|
|
3625
|
+
args: [{ providedIn: 'root' }]
|
|
3626
|
+
}] });
|
|
3627
|
+
//#endregion
|
|
3628
|
+
//#region ---- Helpers ----
|
|
3629
|
+
function modelItemSupportsChatCatalogPurpose(m, purpose) {
|
|
3630
|
+
const p = m.modelPurposes?.length ? m.modelPurposes : [];
|
|
3631
|
+
if (purpose === 'vision') {
|
|
3632
|
+
return p.includes('vision');
|
|
3633
|
+
}
|
|
3634
|
+
return p.includes('chat');
|
|
3635
|
+
}
|
|
3636
|
+
|
|
3637
|
+
/**
|
|
3638
|
+
* Generated bundle index. Do not edit.
|
|
3639
|
+
*/
|
|
3640
|
+
|
|
3641
|
+
export { AI_CHAT_ATTACHMENT_CATEGORY, AI_CHAT_ATTACHMENT_REF_TYPE, AI_CHAT_GENERATED_IMAGE_REF_TYPE, AI_CHAT_GENERATED_SPEECH_REF_TYPE, AI_CHAT_TTS_PCM_SAMPLE_RATE, AI_GENERATED_IMAGE_MAX_REMOTE_URL_IN_TOOL, AXMAiAssistPromptBoxComponent, AXMAiAssistPromptBoxService, AXMAiManagementMenuProvider, AXMAiManagementModule, AXMPermissionDefinitionProvider, AXPAiAssistChatModelCatalogService, AXPAiAssistDefaultMaxStructuredRetries, AXPAiAssistDefaultStructuredMaxTokens, AXPAiAssistService, AXPAiChatToolRunContextService, AXPAiDefaultModelPickerService, AXPAiEngine, AXPAiManagerService, AXPAiOutputContractResolveService, AXPAiPlatformRuntimeContextBuilder, AXPAiStructuredOutputError, AiManagementManifest, axpAiAssistInitialMessagesToTranscript, axpAiAssistStarterPromptLabels, axpAiAssistStarterPromptTexts, axpAiChatMessageIsDelegatedReflectionExcluded, axpAiChatToolOrAgentResultBodyJson, axpAiChatToolResultBodyJson, axpAiChatUserMessage, axpAiDelegatedAgentOutcomeResponsesPlainText, axpAiDelegatedAgentPromptPreview, axpAiDelegatedAgentResultSegments, axpAiStripAssistInitialHtmlToPlain, axpBuildDelegatedAgentRunInputSnapshot, axpSanitizeLastProviderPayloadForPersistence, axpStripEphemeralStreamChunksFromProviderRaw, blobOrFileFromImageResult, buildStructuredOutputRetryUserText, createBrowserPlayableTtsBlobUrl, extractLastAssistantStructuredParts, getAiChatCommandLlmParametersOverride, isRawPcmMimeType, loadAssistPermissionRows, persistAiChatAttachmentImage, persistAiGeneratedImage, persistAiGeneratedSpeech, prepareTtsBytesForBrowserPlayback, validateAgainstContract, validateJsonSchemaMini, validateStructuredAssistantResponse, wrapPcm16LeInWav };
|
|
3642
|
+
//# sourceMappingURL=acorex-platform-module-ai-management-client.mjs.map
|