@acorex-platform/module-conversation-client 0.0.37 → 0.0.46

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.
Files changed (73) hide show
  1. package/fesm2022/{acorex-platform-module-conversation-client-acorex-platform-module-conversation-client-C8FR7Snp.mjs → acorex-platform-module-conversation-client-acorex-platform-module-conversation-client-DUR2oyfP.mjs} +10935 -9294
  2. package/fesm2022/acorex-platform-module-conversation-client-acorex-platform-module-conversation-client-DUR2oyfP.mjs.map +1 -0
  3. package/fesm2022/{acorex-platform-module-conversation-client-after-message-sent.command-hVFQo07j.mjs → acorex-platform-module-conversation-client-after-message-sent.command-qTWATpgC.mjs} +2 -2
  4. package/fesm2022/{acorex-platform-module-conversation-client-after-message-sent.command-hVFQo07j.mjs.map → acorex-platform-module-conversation-client-after-message-sent.command-qTWATpgC.mjs.map} +1 -1
  5. package/fesm2022/{acorex-platform-module-conversation-client-assist-delegated-agent-detail-popup.component-D6XrMTQm.mjs → acorex-platform-module-conversation-client-assist-delegated-agent-detail-popup.component-DQWhOmBE.mjs} +8 -7
  6. package/fesm2022/{acorex-platform-module-conversation-client-assist-delegated-agent-detail-popup.component-D6XrMTQm.mjs.map → acorex-platform-module-conversation-client-assist-delegated-agent-detail-popup.component-DQWhOmBE.mjs.map} +1 -1
  7. package/fesm2022/acorex-platform-module-conversation-client-assist-message-renderer.component-DyVepuaW.mjs +1756 -0
  8. package/fesm2022/acorex-platform-module-conversation-client-assist-message-renderer.component-DyVepuaW.mjs.map +1 -0
  9. package/fesm2022/acorex-platform-module-conversation-client-assist-tool-detail-popup.component-D8na_1QJ.mjs +120 -0
  10. package/fesm2022/{acorex-platform-module-conversation-client-assist-tool-detail-popup.component-BV1YH96m.mjs.map → acorex-platform-module-conversation-client-assist-tool-detail-popup.component-D8na_1QJ.mjs.map} +1 -1
  11. package/fesm2022/{acorex-platform-module-conversation-client-chat-call-session-page.component-b84Na7R_.mjs → acorex-platform-module-conversation-client-chat-call-session-page.component-BeS4Duip.mjs} +747 -25
  12. package/fesm2022/acorex-platform-module-conversation-client-chat-call-session-page.component-BeS4Duip.mjs.map +1 -0
  13. package/fesm2022/{acorex-platform-module-conversation-client-chat-call-start-popup.component-BTOtRRPY.mjs → acorex-platform-module-conversation-client-chat-call-start-popup.component-BbEhkLLh.mjs} +4 -4
  14. package/fesm2022/{acorex-platform-module-conversation-client-chat-call-start-popup.component-BTOtRRPY.mjs.map → acorex-platform-module-conversation-client-chat-call-start-popup.component-BbEhkLLh.mjs.map} +1 -1
  15. package/fesm2022/{acorex-platform-module-conversation-client-chat-conversation-info-media-view.component-CppNPfqe.mjs → acorex-platform-module-conversation-client-chat-conversation-info-media-view.component-Bnbj0e6M.mjs} +3 -3
  16. package/fesm2022/{acorex-platform-module-conversation-client-chat-conversation-info-media-view.component-CppNPfqe.mjs.map → acorex-platform-module-conversation-client-chat-conversation-info-media-view.component-Bnbj0e6M.mjs.map} +1 -1
  17. package/fesm2022/acorex-platform-module-conversation-client-chat-conversation-info-panel.component-bwT1-lAn.mjs +497 -0
  18. package/fesm2022/acorex-platform-module-conversation-client-chat-conversation-info-panel.component-bwT1-lAn.mjs.map +1 -0
  19. package/fesm2022/{acorex-platform-module-conversation-client-comments-page.component-BhSJiJY5.mjs → acorex-platform-module-conversation-client-comments-page.component-BxUz9jTX.mjs} +2 -2
  20. package/fesm2022/{acorex-platform-module-conversation-client-comments-page.component-BhSJiJY5.mjs.map → acorex-platform-module-conversation-client-comments-page.component-BxUz9jTX.mjs.map} +1 -1
  21. package/fesm2022/acorex-platform-module-conversation-client-contracts-entities-channel-audience.mjs +277 -0
  22. package/fesm2022/acorex-platform-module-conversation-client-contracts-entities-channel-audience.mjs.map +1 -0
  23. package/fesm2022/acorex-platform-module-conversation-client-contracts-entities-channel-post.mjs +530 -0
  24. package/fesm2022/acorex-platform-module-conversation-client-contracts-entities-channel-post.mjs.map +1 -0
  25. package/fesm2022/acorex-platform-module-conversation-client-contracts-entities-channel-receipt.mjs +206 -0
  26. package/fesm2022/acorex-platform-module-conversation-client-contracts-entities-channel-receipt.mjs.map +1 -0
  27. package/fesm2022/acorex-platform-module-conversation-client-contracts-entities-message.mjs +65 -17
  28. package/fesm2022/acorex-platform-module-conversation-client-contracts-entities-message.mjs.map +1 -1
  29. package/fesm2022/acorex-platform-module-conversation-client-contracts-entities-room.mjs +231 -59
  30. package/fesm2022/acorex-platform-module-conversation-client-contracts-entities-room.mjs.map +1 -1
  31. package/fesm2022/acorex-platform-module-conversation-client-contracts-entity-catalog-acorex-platform-module-conversation-client-contracts-entity-catalog-CV_sCYGf.mjs +87 -0
  32. package/fesm2022/acorex-platform-module-conversation-client-contracts-entity-catalog-acorex-platform-module-conversation-client-contracts-entity-catalog-CV_sCYGf.mjs.map +1 -0
  33. package/fesm2022/acorex-platform-module-conversation-client-contracts-entity-catalog-channel-audience.entity-DyaUcESJ.mjs +213 -0
  34. package/fesm2022/acorex-platform-module-conversation-client-contracts-entity-catalog-channel-audience.entity-DyaUcESJ.mjs.map +1 -0
  35. package/fesm2022/acorex-platform-module-conversation-client-contracts-entity-catalog-channel-post.entity-CbBcChEt.mjs +466 -0
  36. package/fesm2022/acorex-platform-module-conversation-client-contracts-entity-catalog-channel-post.entity-CbBcChEt.mjs.map +1 -0
  37. package/fesm2022/acorex-platform-module-conversation-client-contracts-entity-catalog-channel-receipt.entity-DtRFj8zV.mjs +142 -0
  38. package/fesm2022/acorex-platform-module-conversation-client-contracts-entity-catalog-channel-receipt.entity-DtRFj8zV.mjs.map +1 -0
  39. package/fesm2022/{acorex-platform-module-conversation-client-contracts-entity-catalog-message.entity-Dz7cO1P4.mjs → acorex-platform-module-conversation-client-contracts-entity-catalog-message.entity-CJk1wEvA.mjs} +35 -19
  40. package/fesm2022/acorex-platform-module-conversation-client-contracts-entity-catalog-message.entity-CJk1wEvA.mjs.map +1 -0
  41. package/fesm2022/acorex-platform-module-conversation-client-contracts-entity-catalog-room.entity-Ctt7qzfF.mjs +403 -0
  42. package/fesm2022/acorex-platform-module-conversation-client-contracts-entity-catalog-room.entity-Ctt7qzfF.mjs.map +1 -0
  43. package/fesm2022/acorex-platform-module-conversation-client-contracts-entity-catalog.mjs +1 -1
  44. package/fesm2022/acorex-platform-module-conversation-client-contracts.mjs +846 -4
  45. package/fesm2022/acorex-platform-module-conversation-client-contracts.mjs.map +1 -1
  46. package/fesm2022/acorex-platform-module-conversation-client-local-entities.mjs +3 -0
  47. package/fesm2022/acorex-platform-module-conversation-client-local-entities.mjs.map +1 -1
  48. package/fesm2022/{acorex-platform-module-conversation-client-mark-conversation-unread.command-D0rr0npJ.mjs → acorex-platform-module-conversation-client-mark-conversation-unread.command-juoWxu7I.mjs} +2 -2
  49. package/fesm2022/{acorex-platform-module-conversation-client-mark-conversation-unread.command-D0rr0npJ.mjs.map → acorex-platform-module-conversation-client-mark-conversation-unread.command-juoWxu7I.mjs.map} +1 -1
  50. package/fesm2022/{acorex-platform-module-conversation-client-open-chat.command-Bbgp4ZfT.mjs → acorex-platform-module-conversation-client-open-chat.command-ridcdmj7.mjs} +2 -2
  51. package/fesm2022/{acorex-platform-module-conversation-client-open-chat.command-Bbgp4ZfT.mjs.map → acorex-platform-module-conversation-client-open-chat.command-ridcdmj7.mjs.map} +1 -1
  52. package/fesm2022/{acorex-platform-module-conversation-client-retry-assist-response.command-BMZTab8j.mjs → acorex-platform-module-conversation-client-retry-assist-response.command-BsYxaH3g.mjs} +2 -2
  53. package/fesm2022/{acorex-platform-module-conversation-client-retry-assist-response.command-BMZTab8j.mjs.map → acorex-platform-module-conversation-client-retry-assist-response.command-BsYxaH3g.mjs.map} +1 -1
  54. package/fesm2022/{acorex-platform-module-conversation-client-sync-read-state.command-9G3Knrre.mjs → acorex-platform-module-conversation-client-sync-read-state.command-DsCyIdEk.mjs} +7 -2
  55. package/fesm2022/acorex-platform-module-conversation-client-sync-read-state.command-DsCyIdEk.mjs.map +1 -0
  56. package/fesm2022/acorex-platform-module-conversation-client.mjs +1 -1
  57. package/package.json +14 -2
  58. package/types/acorex-platform-module-conversation-client-contracts-entities-channel-audience.d.ts +5 -0
  59. package/types/acorex-platform-module-conversation-client-contracts-entities-channel-post.d.ts +5 -0
  60. package/types/acorex-platform-module-conversation-client-contracts-entities-channel-receipt.d.ts +5 -0
  61. package/types/acorex-platform-module-conversation-client-contracts.d.ts +432 -2
  62. package/types/acorex-platform-module-conversation-client.d.ts +680 -161
  63. package/fesm2022/acorex-platform-module-conversation-client-acorex-platform-module-conversation-client-C8FR7Snp.mjs.map +0 -1
  64. package/fesm2022/acorex-platform-module-conversation-client-assist-tool-detail-popup.component-BV1YH96m.mjs +0 -120
  65. package/fesm2022/acorex-platform-module-conversation-client-chat-call-session-page.component-b84Na7R_.mjs.map +0 -1
  66. package/fesm2022/acorex-platform-module-conversation-client-chat-conversation-info-panel.component-C5xS98oE.mjs +0 -472
  67. package/fesm2022/acorex-platform-module-conversation-client-chat-conversation-info-panel.component-C5xS98oE.mjs.map +0 -1
  68. package/fesm2022/acorex-platform-module-conversation-client-contracts-entity-catalog-acorex-platform-module-conversation-client-contracts-entity-catalog-Cy3LwYMZ.mjs +0 -52
  69. package/fesm2022/acorex-platform-module-conversation-client-contracts-entity-catalog-acorex-platform-module-conversation-client-contracts-entity-catalog-Cy3LwYMZ.mjs.map +0 -1
  70. package/fesm2022/acorex-platform-module-conversation-client-contracts-entity-catalog-message.entity-Dz7cO1P4.mjs.map +0 -1
  71. package/fesm2022/acorex-platform-module-conversation-client-contracts-entity-catalog-room.entity-B9txvc3y.mjs +0 -263
  72. package/fesm2022/acorex-platform-module-conversation-client-contracts-entity-catalog-room.entity-B9txvc3y.mjs.map +0 -1
  73. package/fesm2022/acorex-platform-module-conversation-client-sync-read-state.command-9G3Knrre.mjs.map +0 -1
@@ -1,8 +1,12 @@
1
+ import { isLocaleStringMap, resolveMultiLanguageString } from '@acorex-platform/framework-shared/core';
2
+
1
3
  //#region ---- Comment entity reference ----
2
4
  //#endregion
3
5
 
4
6
  const AXPConversationMenuKeys = {
5
7
  Conversations: 'Conversation:Menu:Conversations',
8
+ Channels: 'Conversation:Menu:Channels',
9
+ Feed: 'Conversation:Menu:ChannelFeed',
6
10
  };
7
11
 
8
12
  /**
@@ -17,8 +21,34 @@ const AXPCollaborationPermissionKeys = {
17
21
  MaintainCommunications: 'Collaboration:Permission:MaintainCommunications',
18
22
  MaintainReservations: 'Collaboration:Permission:MaintainReservations',
19
23
  MaintainHelpDesk: 'Collaboration:Permission:MaintainHelpDesk',
24
+ ViewBulletins: 'Collaboration:Permission:ViewBulletins',
25
+ MaintainBulletins: 'Collaboration:Permission:MaintainBulletins',
26
+ PublishBulletins: 'Collaboration:Permission:PublishBulletins',
27
+ ModerateBulletinDiscussions: 'Collaboration:Permission:ModerateBulletinDiscussions',
20
28
  Setup: 'Collaboration:Permission:Setup',
21
29
  };
30
+ const viewBulletins = AXPCollaborationPermissionKeys.ViewBulletins;
31
+ const maintainBulletins = AXPCollaborationPermissionKeys.MaintainBulletins;
32
+ const publishBulletins = AXPCollaborationPermissionKeys.PublishBulletins;
33
+ function channelPostCrud() {
34
+ return {
35
+ Management: maintainBulletins,
36
+ View: viewBulletins,
37
+ Edit: maintainBulletins,
38
+ Delete: maintainBulletins,
39
+ Create: maintainBulletins,
40
+ Publish: publishBulletins,
41
+ };
42
+ }
43
+ function channelChildCrud() {
44
+ return {
45
+ Management: maintainBulletins,
46
+ View: viewBulletins,
47
+ Edit: maintainBulletins,
48
+ Delete: maintainBulletins,
49
+ Create: maintainBulletins,
50
+ };
51
+ }
22
52
  /**
23
53
  * @deprecated Prefer {@link AXPCollaborationPermissionKeys}.
24
54
  * Legacy nested shape kept so existing imports compile during migration.
@@ -26,6 +56,9 @@ const AXPCollaborationPermissionKeys = {
26
56
  const AXMPermissionsKeys = {
27
57
  Conversation: {
28
58
  Management: AXPCollaborationPermissionKeys.MaintainConversations,
59
+ ChannelPosts: channelPostCrud(),
60
+ ChannelAudiences: channelChildCrud(),
61
+ ChannelReceipts: channelChildCrud(),
29
62
  },
30
63
  };
31
64
 
@@ -55,8 +88,270 @@ const RootConfig = {
55
88
  source: `${config.module}.Message`,
56
89
  icon: 'fa-light fa-message',
57
90
  },
91
+ channelPost: {
92
+ name: 'ChannelPost',
93
+ source: `${config.module}.ChannelPost`,
94
+ title: `@${config.i18n}:entities.channel-post.title`,
95
+ titlePlural: `@${config.i18n}:entities.channel-post.plural`,
96
+ icon: 'fa-light fa-bullhorn',
97
+ },
98
+ channelAudience: {
99
+ name: 'ChannelAudience',
100
+ source: `${config.module}.ChannelAudience`,
101
+ title: `@${config.i18n}:entities.channel-audience.title`,
102
+ titlePlural: `@${config.i18n}:entities.channel-audience.plural`,
103
+ icon: 'fa-light fa-users',
104
+ },
105
+ channelReceipt: {
106
+ name: 'ChannelReceipt',
107
+ source: `${config.module}.ChannelReceipt`,
108
+ title: `@${config.i18n}:entities.channel-receipt.title`,
109
+ titlePlural: `@${config.i18n}:entities.channel-receipt.plural`,
110
+ icon: 'fa-light fa-envelope-open',
111
+ },
112
+ },
113
+ pages: {
114
+ feed: {
115
+ path: 'announcements',
116
+ title: `@${config.i18n}:pages.feed.title`,
117
+ description: `@${config.i18n}:pages.feed.description`,
118
+ icon: 'fa-light fa-bullhorn',
119
+ },
120
+ },
121
+ };
122
+ const CHANNEL_POST_ENTITY_NAME = RootConfig.entities.channelPost.source;
123
+ const CHANNEL_AUDIENCE_ENTITY_NAME = RootConfig.entities.channelAudience.source;
124
+ const CHANNEL_RECEIPT_ENTITY_NAME = RootConfig.entities.channelReceipt.source;
125
+
126
+ //#region ---- Imports ----
127
+ //#endregion
128
+ //#region ---- Manifest Definition ----
129
+ /**
130
+ * Conversation Module Manifest.
131
+ * Defines module metadata, features, and provider references.
132
+ */
133
+ const ConversationManifest = {
134
+ name: RootConfig.module.name,
135
+ version: '1.0.0',
136
+ title: RootConfig.module.title,
137
+ icon: RootConfig.module.icon,
138
+ i18n: RootConfig.config.i18n,
139
+ type: 'domain',
140
+ domain: 'communication',
141
+ // Module dependencies
142
+ dependencies: ['CommunicationManagement'],
143
+ };
144
+ //#endregion
145
+
146
+ const AXMChannelErrorCodes = {
147
+ InvalidTransition: 'CHANNEL_INVALID_TRANSITION',
148
+ ScheduleRequiresFuturePublishAt: 'CHANNEL_SCHEDULE_REQUIRES_FUTURE_PUBLISH_AT',
149
+ NotAddressed: 'CHANNEL_NOT_ADDRESSED',
150
+ };
151
+
152
+ //#region ----- Conversation channel runtime keys -----
153
+ const AXPConversationQueryKeys = {
154
+ ChannelFeed: {
155
+ Get: 'Conversation:ChannelFeed:Get',
156
+ GetUnreadCount: 'Conversation:ChannelFeed:GetUnreadCount',
58
157
  },
59
158
  };
159
+ const AXPConversationCommandKeys = {
160
+ ChannelFeed: {
161
+ MarkRead: 'Conversation:ChannelFeed:MarkRead',
162
+ },
163
+ };
164
+ //#endregion
165
+
166
+ //#region ----- Imports -----
167
+ //#endregion
168
+
169
+ //#region ----- Imports -----
170
+ //#endregion
171
+ //#region ----- Mapping -----
172
+ /** Projects a channel post onto the chat/feed card shape stored on Message metadata. */
173
+ function toChannelFeedItem(post, isRead, isPinned, commentCount = 0) {
174
+ return {
175
+ id: post.id,
176
+ roomId: post.roomId,
177
+ title: post.title,
178
+ body: post.body,
179
+ kind: post.kind,
180
+ authorUserId: post.authorUserId,
181
+ publishedAt: toIso(post.publishedAt),
182
+ eventStartsAt: toIso(post.eventStartsAt),
183
+ eventEndsAt: toIso(post.eventEndsAt),
184
+ eventLocation: post.eventLocation,
185
+ isPinned,
186
+ allowReactions: post.allowReactions,
187
+ allowComments: post.allowComments,
188
+ commentCount,
189
+ isRead,
190
+ gallery: asFileList(post.gallery),
191
+ attachments: asFileList(post.attachments),
192
+ tags: asTags(post.tags),
193
+ };
194
+ }
195
+ /** Plain-text title for chat message payload (MLS or string). */
196
+ function channelPostTitleText(title) {
197
+ if (typeof title === 'string') {
198
+ return title;
199
+ }
200
+ if (title == null) {
201
+ return '';
202
+ }
203
+ try {
204
+ return JSON.stringify(title);
205
+ }
206
+ catch {
207
+ return '';
208
+ }
209
+ }
210
+ function channelPostMessageName(postId) {
211
+ return `channel-post:${postId}`;
212
+ }
213
+ //#endregion
214
+ //#region ----- Helpers -----
215
+ function toIso(value) {
216
+ if (!value) {
217
+ return undefined;
218
+ }
219
+ const date = value instanceof Date ? value : new Date(value);
220
+ return Number.isNaN(date.getTime()) ? undefined : date.toISOString();
221
+ }
222
+ function asTags(value) {
223
+ if (value == null) {
224
+ return [];
225
+ }
226
+ if (typeof value === 'string') {
227
+ const trimmed = value.trim();
228
+ if (!trimmed) {
229
+ return [];
230
+ }
231
+ try {
232
+ return asTags(JSON.parse(trimmed));
233
+ }
234
+ catch {
235
+ return [trimmed];
236
+ }
237
+ }
238
+ if (!Array.isArray(value)) {
239
+ return [];
240
+ }
241
+ return value
242
+ .map((item) => normalizeTag(item))
243
+ .filter((item) => item != null);
244
+ }
245
+ function normalizeTag(value) {
246
+ if (typeof value === 'string') {
247
+ const trimmed = value.trim();
248
+ return trimmed.length > 0 ? trimmed : null;
249
+ }
250
+ if (isLocaleStringMap(value)) {
251
+ const hasContent = Object.values(value).some((entry) => typeof entry === 'string' && entry.trim().length > 0);
252
+ return hasContent ? value : null;
253
+ }
254
+ return null;
255
+ }
256
+ function asFileList(value) {
257
+ if (value == null) {
258
+ return [];
259
+ }
260
+ if (typeof value === 'string') {
261
+ const trimmed = value.trim();
262
+ if (!trimmed) {
263
+ return [];
264
+ }
265
+ try {
266
+ return asFileList(JSON.parse(trimmed));
267
+ }
268
+ catch {
269
+ return [];
270
+ }
271
+ }
272
+ if (!Array.isArray(value)) {
273
+ return [];
274
+ }
275
+ return value.filter((item) => item != null && typeof item === 'object' && typeof item.name === 'string');
276
+ }
277
+ //#endregion
278
+
279
+ //#region ----- Imports -----
280
+ //#endregion
281
+ //#region ----- Distribution & notification contracts -----
282
+ const CHANNEL_POST_PUBLISHED_EVENT_NAME = 'Conversation.ChannelPostPublished';
283
+ const CHANNEL_DISTRIBUTOR = Symbol('CHANNEL_DISTRIBUTOR');
284
+ const CHANNEL_NOTIFICATION_PORT = Symbol('CHANNEL_NOTIFICATION_PORT');
285
+ //#endregion
286
+
287
+ //#region ----- Imports -----
288
+ //#endregion
289
+ //#region ----- Constants -----
290
+ const TARGET_MEMBERSHIP_KEY = {
291
+ user: 'userId',
292
+ role: 'roleIds',
293
+ team: 'teamIds',
294
+ businessUnit: 'businessUnitIds',
295
+ position: 'positionIds',
296
+ jobDefinition: 'jobDefinitionIds',
297
+ };
298
+ //#endregion
299
+ //#region ----- Audience matching -----
300
+ function axmChannelAudienceRuleMatches(rule, member) {
301
+ if (rule.targetType === 'organization') {
302
+ return true;
303
+ }
304
+ const key = TARGET_MEMBERSHIP_KEY[rule.targetType];
305
+ const value = member[key];
306
+ return Array.isArray(value) ? value.includes(rule.targetId ?? '') : value === rule.targetId;
307
+ }
308
+ function axmChannelAudienceIsVisible(rules, member) {
309
+ if (rules.length === 0) {
310
+ return true;
311
+ }
312
+ const included = rules.some((rule) => rule.mode !== 'exclude' && axmChannelAudienceRuleMatches(rule, member));
313
+ const excluded = rules.some((rule) => rule.mode === 'exclude' && axmChannelAudienceRuleMatches(rule, member));
314
+ return included && !excluded;
315
+ }
316
+ function axmResolveChannelAudienceUserIds(rules, members) {
317
+ return members.filter((member) => axmChannelAudienceIsVisible(rules, member)).map((member) => member.userId);
318
+ }
319
+ //#endregion
320
+
321
+ //#region ----- Imports -----
322
+ //#endregion
323
+ //#region ----- Target entity map -----
324
+ /** Lookup entity for each audience target type. `organization` has no row lookup. */
325
+ const AXM_CHANNEL_AUDIENCE_TARGET_ENTITY = {
326
+ organization: null,
327
+ user: 'SecurityManagement.User',
328
+ role: 'SecurityManagement.Role',
329
+ team: 'OrganizationManagement.Team',
330
+ businessUnit: 'OrganizationManagement.BusinessUnit',
331
+ position: 'OrganizationManagement.Position',
332
+ jobDefinition: 'OrganizationManagement.JobDefinition',
333
+ };
334
+ function axmChannelAudienceTargetEntity(targetType) {
335
+ if (!targetType) {
336
+ return null;
337
+ }
338
+ return AXM_CHANNEL_AUDIENCE_TARGET_ENTITY[targetType] ?? null;
339
+ }
340
+ function axmRoomAudienceRule(room) {
341
+ const roomId = room.id?.trim();
342
+ const targetType = room.audienceTargetType?.trim();
343
+ if (!roomId || !targetType) {
344
+ return null;
345
+ }
346
+ return {
347
+ id: `room-audience:${roomId}`,
348
+ roomId,
349
+ targetType,
350
+ targetId: room.audienceTargetId?.trim() || undefined,
351
+ mode: room.audienceMode?.trim() ?? 'include',
352
+ };
353
+ }
354
+ //#endregion
60
355
 
61
356
  //#region ---- Media helpers ----
62
357
  //#endregion
@@ -74,10 +369,136 @@ const AXM_COMMENT_LIKE_EMOJI = '👍';
74
369
  //#region ---- Imports ----
75
370
  //#endregion
76
371
 
372
+ //#region ----- Imports -----
373
+ //#endregion
374
+
375
+ //#region ----- Imports -----
376
+ //#endregion
377
+
378
+ //#region ----- Imports -----
379
+ //#endregion
380
+
381
+ //#region ----- Constants -----
382
+ /** Persisted `Room.name` for the tenant-wide announcements channel (unique per tenant scope). */
383
+ const AXM_CONVERSATION_ANNOUNCEMENT_CHANNEL_NAME = 'system.tenant-announcements';
384
+ /** `metadata.systemChannel` value for the tenant-wide announcements channel. */
385
+ const AXM_CONVERSATION_ANNOUNCEMENT_CHANNEL_KEY = 'announcements';
386
+ /** Custom conversation message type rendered by the channel feed-item plugin. */
387
+ const AXM_CONVERSATION_ANNOUNCEMENT_MESSAGE_TYPE = 'channel-feed-item';
388
+ /** Message metadata key that stores the `AXMChannelFeedItem` projection. */
389
+ const AXM_CONVERSATION_ANNOUNCEMENT_FEED_ITEM_META_KEY = 'channelFeedItem';
390
+ /** Message metadata key that stores the source ChannelPost id. */
391
+ const AXM_CONVERSATION_CHANNEL_POST_ID_META_KEY = 'channelPostId';
392
+ /** True when the room is the tenant announcements channel. */
393
+ function axmIsAnnouncementChannel(value) {
394
+ if (!value) {
395
+ return false;
396
+ }
397
+ if (value.name === AXM_CONVERSATION_ANNOUNCEMENT_CHANNEL_NAME) {
398
+ return true;
399
+ }
400
+ return value.metadata?.['systemChannel'] === AXM_CONVERSATION_ANNOUNCEMENT_CHANNEL_KEY;
401
+ }
402
+ function axmAnnouncementChannelMetadata() {
403
+ return {
404
+ systemChannel: AXM_CONVERSATION_ANNOUNCEMENT_CHANNEL_KEY,
405
+ reactionsEnabled: false,
406
+ };
407
+ }
408
+ /** True when the room is an entity comment thread (not a chat inbox conversation). */
409
+ function axmIsCommentRoom(value) {
410
+ return value?.kind === 'comment';
411
+ }
412
+ /** True when the room is a chat channel (broadcast posts, no composer). */
413
+ function axmIsChannelRoom(value) {
414
+ if (!value) {
415
+ return false;
416
+ }
417
+ if (axmIsAnnouncementChannel(value)) {
418
+ return true;
419
+ }
420
+ return value.type === 'channel';
421
+ }
422
+ function axmChannelPostIdFromMessage(message) {
423
+ if (!message) {
424
+ return null;
425
+ }
426
+ const meta = message.metadata ?? {};
427
+ const fromMeta = meta[AXM_CONVERSATION_CHANNEL_POST_ID_META_KEY];
428
+ if (typeof fromMeta === 'string' && fromMeta.trim()) {
429
+ return fromMeta.trim();
430
+ }
431
+ const prefix = 'channel-post:';
432
+ const name = message.name?.trim() ?? '';
433
+ if (name.startsWith(prefix)) {
434
+ const id = name.slice(prefix.length).trim();
435
+ if (id) {
436
+ return id;
437
+ }
438
+ }
439
+ const feed = meta[AXM_CONVERSATION_ANNOUNCEMENT_FEED_ITEM_META_KEY];
440
+ if (feed && typeof feed === 'object' && typeof feed.id === 'string') {
441
+ const id = feed.id.trim();
442
+ return id || null;
443
+ }
444
+ return null;
445
+ }
446
+ //#endregion
447
+
448
+ //#region ---- Conversation message scope ----
449
+ /**
450
+ * Resolves tenant/application scope for a new {@link Conversation.Message} row.
451
+ * Prefer the parent room's stamped scope so call logs match sibling messages on read.
452
+ */
453
+ function axmResolveConversationMessageScopeInfo(room, session) {
454
+ const fromRoom = readScopeInfo(room['scopeInfo']);
455
+ if (fromRoom?.['tenantId']) {
456
+ return fromRoom;
457
+ }
458
+ const tenantId = session.tenantId?.trim();
459
+ if (!tenantId) {
460
+ return undefined;
461
+ }
462
+ const scope = { tenantId };
463
+ const applicationId = session.applicationId?.trim();
464
+ if (applicationId) {
465
+ scope['applicationId'] = applicationId;
466
+ }
467
+ return scope;
468
+ }
469
+ function axmScopeInfoNeedsTenantStamp(scopeInfo, expectedTenantId) {
470
+ const tenantId = typeof scopeInfo?.['tenantId'] === 'string' ? scopeInfo['tenantId'].trim() : '';
471
+ return tenantId !== expectedTenantId.trim();
472
+ }
473
+ function readScopeInfo(value) {
474
+ if (!value || typeof value !== 'object') {
475
+ return undefined;
476
+ }
477
+ const record = value;
478
+ const tenantId = typeof record['tenantId'] === 'string' ? record['tenantId'].trim() : '';
479
+ if (!tenantId) {
480
+ return undefined;
481
+ }
482
+ const scope = { tenantId };
483
+ const applicationId = typeof record['applicationId'] === 'string' ? record['applicationId'].trim() : '';
484
+ if (applicationId) {
485
+ scope['applicationId'] = applicationId;
486
+ }
487
+ return scope;
488
+ }
489
+ //#endregion
490
+
77
491
  //#region ---- Assist stream wire events (shared with server) ----
78
492
  //#endregion
79
493
 
80
494
  //#region ---- Assist metadata helpers (shared) ----
495
+ /** UI flags persisted on every AI (bot) room. */
496
+ function axmAiChatConversationUiMetadata() {
497
+ return {
498
+ reactionsEnabled: false,
499
+ composerAttachmentPresentation: 'banner',
500
+ };
501
+ }
81
502
  function axmExtractAssistIdFromMetadata(metadata) {
82
503
  if (!metadata) {
83
504
  return undefined;
@@ -155,16 +576,29 @@ function axmReadAssistResponseStatus(metadata) {
155
576
  const status = metadata?.['assistResponseStatus'];
156
577
  return typeof status === 'string' ? status : undefined;
157
578
  }
579
+ function axmReadAssistFollowUpStatus(metadata) {
580
+ const status = metadata?.['followUpStatus'];
581
+ if (status === 'preparing' || status === 'ready' || status === 'skipped') {
582
+ return status;
583
+ }
584
+ return undefined;
585
+ }
158
586
  function axmReadReplyToUserMessageId(metadata) {
159
587
  const replyTo = metadata?.['replyToUserMessageId'];
160
588
  return typeof replyTo === 'string' && replyTo.trim() ? replyTo.trim() : undefined;
161
589
  }
162
590
  function extractMessageTextFromPayload(payload) {
591
+ if (!payload || typeof payload !== 'object') {
592
+ return '';
593
+ }
163
594
  const record = payload;
164
- if (record?.type === 'text' && typeof record.text === 'string') {
165
- return record.text;
595
+ if (typeof record.text !== 'string') {
596
+ return '';
166
597
  }
167
- return '';
598
+ if (record.type && record.type !== 'text') {
599
+ return '';
600
+ }
601
+ return record.text;
168
602
  }
169
603
  function axmNormalizeConversationTitle(raw) {
170
604
  const normalized = raw
@@ -174,6 +608,50 @@ function axmNormalizeConversationTitle(raw) {
174
608
  .trim();
175
609
  return normalized.slice(0, 90).trim();
176
610
  }
611
+ /** System prompt for a lightweight follow-up-chip model call after the Assist answer. */
612
+ const AXM_ASSIST_CONVERSATION_FOLLOW_UP_SYSTEM_PROMPT = `You generate short follow-up suggestion chips for a chat assistant.
613
+
614
+ Given the latest user message, assistant reply, optional this-turn tool evidence, optional earlier turns from **this same conversation**, and optional assist capabilities, reply with ONLY one JSON object:
615
+ {"type":"followUp","content":[{"text":"chip label","command":{"name":"Conversation:SendAssistChatMessage","options":{}}}]}
616
+
617
+ Rules:
618
+ - Always propose 2–5 short, actionable next steps as Conversation:SendAssistChatMessage chips. Never return an empty content array.
619
+ - Navigation chips (Entity:OpenList / Entity:OpenDetails) are extra and never replace send-message chips. Always include at least two send-message chips.
620
+ - Default chips send the label as the next chat message via Conversation:SendAssistChatMessage.
621
+ - **Priority:** ground every chip in the latest user message and the latest assistant reply. Earlier turns in this conversation may only inspire wording. Ignore any other conversation, tenant, or leftover memory.
622
+ - Chip labels must continue **this exchange**. After a list or query (including an empty result), suggest the next obvious question or filter — a different date, range, person, or related view — in the user's language. After a create/update, offer the next step on that record.
623
+ - Empty results still need concrete next steps (for example a different day, this week, or who is away), not generic filler.
624
+ - Example: user asked who is on leave today and the assistant found none → chips such as "leave tomorrow", "this week's leave", "who is away next week" in the user's language.
625
+ - Never emit generic filler such as "Ask another question", "What else can you do?", "Ask me anything", or their translations.
626
+ - Navigation chips only when **this turn** created, updated, queried, opened a specific entity, or answered a how-to using PlatformManagement:ListPlatformNavigationForAi with a single Module.Entity. Never on greeting, thanks, goodbye, or orientation. Chip **text** must be in the same language as the user (never English when the user wrote Persian).
627
+ - List: {"text":"<Open List or List {menu title} in the user's language>","command":{"name":"Entity:OpenList","options":{"entity":"Module.Entity"}}}
628
+ - How-to / menu answers: Open List only (no Details unless a record id exists). Prefer a named chip such as "List Users" when the menu title is known.
629
+ - Details (only when this turn has a record id and the row was not just deleted): {"text":"<Open Details in the user's language>","command":{"name":"Entity:OpenDetails","options":{"entity":"Module.Entity","data":{"id":"<record-id>"}}}}
630
+ - Other chips remain send-message requests in the user's language.
631
+ - Do not invent platform facts, IDs, or screens that were not mentioned and are not listed in assist capabilities.
632
+ - When the exchange is orientation (greeting, thanks, goodbye, or no specific task yet), suggest what this assist can help with. Prefer the provided assist capabilities, rewritten as short chip labels in the user's language.
633
+ - Use the same language as the user.
634
+ - Do not repeat the assistant's full answer. Chip labels only.
635
+ - No markdown fences, commentary, or extra keys.`;
636
+ function axmBuildAssistConversationFollowUpUserPrompt(input) {
637
+ const parts = [
638
+ `Latest user message (authoritative):\n${input.userMessageText.trim()}`,
639
+ `Latest assistant reply (authoritative):\n${input.assistantMessageText.trim()}`,
640
+ ];
641
+ const turn = input.turnGrounding?.trim();
642
+ if (turn) {
643
+ parts.push(turn);
644
+ }
645
+ const thread = input.threadInspiration?.trim();
646
+ if (thread) {
647
+ parts.push(`Earlier turns in this conversation only (optional inspiration; do not override the latest exchange; ignore any other chat):\n${thread}`);
648
+ }
649
+ const grounding = input.assistGrounding?.trim();
650
+ if (grounding) {
651
+ parts.push(grounding);
652
+ }
653
+ return parts.join('\n\n');
654
+ }
177
655
  /** System prompt for a lightweight title-only model call (first user message → sidebar title). */
178
656
  const AXM_ASSIST_CONVERSATION_TITLE_SYSTEM_PROMPT = `You generate short conversation titles for chat sidebars.
179
657
 
@@ -242,17 +720,381 @@ function axmCompareMessagesChronologically(a, b) {
242
720
  const diff = axmCoerceMessageTimestampMs(a.timestamp) - axmCoerceMessageTimestampMs(b.timestamp);
243
721
  return diff !== 0 ? diff : a.id.localeCompare(b.id);
244
722
  }
723
+ /** Messages that sort after {@link messageId} in conversation order (deleted rows excluded). */
724
+ function axmMessagesAfterId(messages, messageId) {
725
+ const scoped = messageId.trim();
726
+ if (!scoped) {
727
+ return [];
728
+ }
729
+ const sorted = [...messages].sort((a, b) => axmCompareMessagesChronologically(a, b));
730
+ const idx = sorted.findIndex((message) => message.id === scoped);
731
+ if (idx < 0) {
732
+ return [];
733
+ }
734
+ return sorted.slice(idx + 1).filter((message) => message.deleted !== true);
735
+ }
736
+ //#endregion
737
+
738
+ //#region ---- Follow-up nav chip locale ----
739
+ /** Must match `conversation.json` → `chat.assist-renderer.follow-up.open-entity-list`. */
740
+ const AXM_FOLLOW_UP_OPEN_LIST_LABELS = {
741
+ 'en-US': 'Open List',
742
+ 'en-AU': 'Open List',
743
+ 'fa-IR': 'باز کردن لیست',
744
+ };
745
+ /** Must match `conversation.json` → `chat.assist-renderer.follow-up.open-entity-list-named`. */
746
+ const AXM_FOLLOW_UP_OPEN_LIST_NAMED_LABELS = {
747
+ 'en-US': 'List {{name}}',
748
+ 'en-AU': 'List {{name}}',
749
+ 'fa-IR': 'لیست {{name}}',
750
+ };
751
+ /** Must match `conversation.json` → `chat.assist-renderer.follow-up.open-entity-details`. */
752
+ const AXM_FOLLOW_UP_OPEN_DETAILS_LABELS = {
753
+ 'en-US': 'Open Details',
754
+ 'en-AU': 'Open Details',
755
+ 'fa-IR': 'باز کردن جزئیات',
756
+ };
757
+ const FOLLOW_UP_CHIP_LOCALES = ['en-US', 'en-AU', 'fa-IR'];
758
+ const PERSIAN_OR_ARABIC_SCRIPT = /[\u0600-\u06FF]/;
759
+ function normalizeChipLocale(raw) {
760
+ const locale = raw?.trim() || 'en-US';
761
+ return locale;
762
+ }
763
+ function isKnownChipLocale(locale) {
764
+ return FOLLOW_UP_CHIP_LOCALES.includes(locale);
765
+ }
766
+ /**
767
+ * Spoken language from the latest user text when that locale exists in the product;
768
+ * otherwise the system UI locale.
769
+ */
770
+ function axmInferFollowUpChipLocale(spokenText, systemLocale) {
771
+ const system = normalizeChipLocale(systemLocale);
772
+ const text = spokenText.trim();
773
+ let spoken;
774
+ if (PERSIAN_OR_ARABIC_SCRIPT.test(text)) {
775
+ spoken = 'fa-IR';
776
+ }
777
+ else if (/[A-Za-z]/.test(text)) {
778
+ spoken = system.toLowerCase().startsWith('en') ? system : 'en-US';
779
+ }
780
+ if (spoken && isKnownChipLocale(spoken)) {
781
+ return spoken;
782
+ }
783
+ if (isKnownChipLocale(system)) {
784
+ return system;
785
+ }
786
+ return 'en-US';
787
+ }
788
+ /** Resolved Open List / Open Details labels — plain text, never i18n keys. */
789
+ function axmResolveFollowUpNavChipLabels(spokenText, systemLocale, options) {
790
+ const locale = axmInferFollowUpChipLocale(spokenText, systemLocale);
791
+ const namedTitle = options?.listEntityTitle?.trim();
792
+ const listTemplate = namedTitle
793
+ ? resolveMultiLanguageString(AXM_FOLLOW_UP_OPEN_LIST_NAMED_LABELS, locale)
794
+ : resolveMultiLanguageString(AXM_FOLLOW_UP_OPEN_LIST_LABELS, locale);
795
+ return {
796
+ list: namedTitle ? listTemplate.replace(/\{\{\s*name\s*\}\}/g, namedTitle) : listTemplate,
797
+ details: resolveMultiLanguageString(AXM_FOLLOW_UP_OPEN_DETAILS_LABELS, locale),
798
+ };
799
+ }
245
800
  //#endregion
246
801
 
247
802
  //#region ---- Assist message handler port (server) ----
248
803
  const AXM_CONVERSATION_ASSIST_MESSAGE_HANDLER = Symbol('AXM_CONVERSATION_ASSIST_MESSAGE_HANDLER');
249
804
  //#endregion
250
805
 
806
+ //#region ----- Types -----
807
+ /** Metadata key on a Conversation room for contextual assist launch payload. */
808
+ const AXM_ASSIST_LAUNCH_CONTEXT_METADATA_KEY = 'assistLaunchContext';
809
+ /** v1 entity-row launch kind. Later callers may pass other kind strings (e.g. document). */
810
+ const AXM_CONTEXTUAL_ASSIST_KIND_ENTITY_RECORD = 'entity-record';
811
+ /** v1 entity-row action name passed into the launcher. */
812
+ const AXM_CONTEXTUAL_ASSIST_ACTION_ENTITY_RECORD = 'entity.record.assist';
813
+ /**
814
+ * Payload key for a live designer/document JSON string.
815
+ * Sanitizer keeps this field much larger than ordinary string values.
816
+ */
817
+ const AXM_CONTEXTUAL_ASSIST_LAYOUT_JSON_KEY = 'layoutJson';
818
+ //#endregion
819
+
820
+ //#region ----- Imports -----
821
+ //#endregion
822
+ //#region ----- Constants -----
823
+ const STARTER_MAX_CHIPS = 3;
824
+ const MAX_STRING_CHARS = 400;
825
+ const MAX_JSON_CHARS = 800;
826
+ /** Live designer JSON must survive sanitize so the specialist can edit the canvas. */
827
+ const AXM_CONTEXTUAL_ASSIST_MAX_LAYOUT_JSON_CHARS = 100_000;
828
+ const LARGE_CONTEXT_STRING_KEYS = new Set([
829
+ AXM_CONTEXTUAL_ASSIST_LAYOUT_JSON_KEY,
830
+ 'documentJson',
831
+ 'formJson',
832
+ ]);
833
+ const SECRET_NAME_PATTERN = /password|secret|token|apikey|api_key|credential|privatekey|private_key/i;
834
+ const AXM_CONTEXTUAL_ASSIST_STARTER_SYSTEM_PROMPT = `You generate short suggested starter questions for a chat assistant.
835
+
836
+ The user has not sent a message yet. They opened assist from a product surface (for example one entity record).
837
+
838
+ Reply with ONLY one JSON object:
839
+ {"items":[{"text":"short question"}]}
840
+
841
+ Rules:
842
+ - Return at most 3 items. Each text is one short suggested question or request the user could send.
843
+ - Ground every chip only in the provided launch context. Do not invent ids, fields, or facts that are not in the context.
844
+ - Use the same language as the locale (and any user-facing titles in the context).
845
+ - Do not claim data was saved or that you already changed the record.
846
+ - No markdown fences, commentary, or extra keys.`;
847
+ //#endregion
848
+ //#region ----- Metadata -----
849
+ /**
850
+ * Reads {@link AXM_ASSIST_LAUNCH_CONTEXT_METADATA_KEY} from conversation metadata
851
+ * (including the nested `metadata.metadata` shape used by the conversation UI).
852
+ */
853
+ function axmIsFormTemplateDesignerAssistLaunch(metadata) {
854
+ const ctx = axmExtractAssistLaunchContextFromMetadata(metadata);
855
+ if (!ctx) {
856
+ return false;
857
+ }
858
+ return ctx.kind === 'form-template-designer' || ctx.payload['scenario'] === 'form-template-designer';
859
+ }
860
+ function axmExtractAssistLaunchContextFromMetadata(metadata) {
861
+ if (!metadata) {
862
+ return undefined;
863
+ }
864
+ const direct = readLaunchContext(metadata[AXM_ASSIST_LAUNCH_CONTEXT_METADATA_KEY]);
865
+ if (direct) {
866
+ return direct;
867
+ }
868
+ const nested = metadata['metadata'];
869
+ if (nested && typeof nested === 'object' && !Array.isArray(nested)) {
870
+ return readLaunchContext(nested[AXM_ASSIST_LAUNCH_CONTEXT_METADATA_KEY]);
871
+ }
872
+ return undefined;
873
+ }
874
+ function readLaunchContext(raw) {
875
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
876
+ return undefined;
877
+ }
878
+ const row = raw;
879
+ const kind = typeof row['kind'] === 'string' ? row['kind'].trim() : '';
880
+ const actionName = typeof row['actionName'] === 'string' ? row['actionName'].trim() : '';
881
+ const payload = row['payload'];
882
+ if (!kind || !actionName || !payload || typeof payload !== 'object' || Array.isArray(payload)) {
883
+ return undefined;
884
+ }
885
+ const title = typeof row['title'] === 'string' ? row['title'].trim() : undefined;
886
+ return {
887
+ kind,
888
+ actionName,
889
+ ...(title ? { title } : {}),
890
+ payload: payload,
891
+ };
892
+ }
893
+ //#endregion
894
+ //#region ----- Sanitize -----
895
+ function axmSanitizeContextualAssistContext(context) {
896
+ const title = context.title?.trim();
897
+ return {
898
+ kind: context.kind.trim(),
899
+ actionName: context.actionName.trim(),
900
+ ...(title ? { title } : {}),
901
+ payload: sanitizeRecord(context.payload, 0),
902
+ };
903
+ }
904
+ function sanitizeRecord(value, depth) {
905
+ const out = {};
906
+ for (const [key, raw] of Object.entries(value)) {
907
+ if (shouldSkipKey(key)) {
908
+ continue;
909
+ }
910
+ const next = sanitizeUnknown(raw, depth + 1, key);
911
+ if (next !== undefined) {
912
+ out[key] = next;
913
+ }
914
+ }
915
+ return out;
916
+ }
917
+ function axmSanitizeContextualAssistValue(value) {
918
+ return sanitizeUnknown(value, 0);
919
+ }
920
+ function sanitizeUnknown(value, depth, key) {
921
+ if (value == null) {
922
+ return value;
923
+ }
924
+ if (typeof value === 'string') {
925
+ const trimmed = value.trim();
926
+ const maxChars = key && LARGE_CONTEXT_STRING_KEYS.has(key) ? AXM_CONTEXTUAL_ASSIST_MAX_LAYOUT_JSON_CHARS : MAX_STRING_CHARS;
927
+ return trimmed.length > maxChars ? `${trimmed.slice(0, maxChars)}…` : trimmed;
928
+ }
929
+ if (typeof value === 'number' || typeof value === 'boolean') {
930
+ return value;
931
+ }
932
+ if (value instanceof Date) {
933
+ return value.toISOString();
934
+ }
935
+ if (typeof value === 'function' || typeof value === 'bigint' || typeof value === 'symbol') {
936
+ return undefined;
937
+ }
938
+ if (Array.isArray(value)) {
939
+ if (depth > 2) {
940
+ return summarizeJson(value);
941
+ }
942
+ return value
943
+ .slice(0, 12)
944
+ .map((item) => sanitizeUnknown(item, depth + 1))
945
+ .filter((item) => item !== undefined);
946
+ }
947
+ if (typeof value === 'object') {
948
+ if (depth > 2) {
949
+ return summarizeJson(value);
950
+ }
951
+ return sanitizeRecord(value, depth);
952
+ }
953
+ return undefined;
954
+ }
955
+ function axmShouldSkipContextualAssistKey(key) {
956
+ return shouldSkipKey(key);
957
+ }
958
+ function shouldSkipKey(key) {
959
+ const trimmed = key.trim();
960
+ if (!trimmed || trimmed.startsWith('_') || trimmed.startsWith('$')) {
961
+ return true;
962
+ }
963
+ return SECRET_NAME_PATTERN.test(trimmed);
964
+ }
965
+ function summarizeJson(value) {
966
+ try {
967
+ const text = JSON.stringify(value);
968
+ if (!text || text === '{}' || text === '[]') {
969
+ return undefined;
970
+ }
971
+ return text.length > MAX_JSON_CHARS ? `${text.slice(0, MAX_JSON_CHARS)}…` : text;
972
+ }
973
+ catch {
974
+ return undefined;
975
+ }
976
+ }
977
+ //#endregion
978
+ //#region ----- Prompts -----
979
+ function axmBuildContextualAssistStarterUserPrompt(context, locale) {
980
+ const sanitized = axmSanitizeContextualAssistContext(context);
981
+ const localeLabel = locale.trim() || 'en-US';
982
+ return [
983
+ `Locale: ${localeLabel}`,
984
+ `Action: ${sanitized.actionName}`,
985
+ `Kind: ${sanitized.kind}`,
986
+ sanitized.title ? `Surface title: ${sanitized.title}` : '',
987
+ 'Launch context (JSON):',
988
+ JSON.stringify(sanitized.payload, null, 2),
989
+ ]
990
+ .filter(Boolean)
991
+ .join('\n');
992
+ }
993
+ /** System/user-visible injection prepended on every assist run for a launched room. */
994
+ function axmFormatContextualAssistLaunchInjection(context) {
995
+ const sanitized = axmSanitizeContextualAssistContext(context);
996
+ return [
997
+ 'The user opened assist from a product surface. Use this launch context for the whole conversation.',
998
+ 'Do not invent persisted ids or claim data was saved.',
999
+ `Action: ${sanitized.actionName}`,
1000
+ `Kind: ${sanitized.kind}`,
1001
+ sanitized.title ? `Surface title: ${sanitized.title}` : '',
1002
+ sanitized.kind === 'form-template-designer' ||
1003
+ sanitized.payload['scenario'] === 'form-template-designer' ||
1004
+ typeof sanitized.payload[AXM_CONTEXTUAL_ASSIST_LAYOUT_JSON_KEY] === 'string'
1005
+ ? [
1006
+ 'The Form Template designer canvas is open.',
1007
+ 'Follow the edit-layout-designer Skill: Page:GetContext then Page:RunAction with add-widget / update-widget / remove-widget / move-widget from snapshot.actions.',
1008
+ 'Calculated fields, show/hide, and cascades use write-platform-expression: patch node.triggers and node.visible (not options.hint).',
1009
+ 'Read snapshot.outline for widget name/type/path. Do not rewrite or apply the full layout JSON for a single field. Do not emit a widget tree in chat.',
1010
+ 'The user-visible chat reply is one or two short sentences only.',
1011
+ ].join(' ')
1012
+ : '',
1013
+ 'Context (JSON):',
1014
+ JSON.stringify(sanitized.payload, null, 2),
1015
+ ]
1016
+ .filter(Boolean)
1017
+ .join('\n');
1018
+ }
1019
+ //#endregion
1020
+ //#region ----- Parse starters -----
1021
+ function axmParseContextualAssistStarterItems(rawText) {
1022
+ const parsed = parseJsonPayload(rawText);
1023
+ const labels = [];
1024
+ for (const candidate of collectStarterCandidates(parsed)) {
1025
+ const text = candidate.trim();
1026
+ if (!text) {
1027
+ continue;
1028
+ }
1029
+ labels.push(text);
1030
+ if (labels.length >= STARTER_MAX_CHIPS) {
1031
+ break;
1032
+ }
1033
+ }
1034
+ return labels;
1035
+ }
1036
+ function collectStarterCandidates(parsed) {
1037
+ if (Array.isArray(parsed)) {
1038
+ return parsed.map(labelFromUnknown).filter(Boolean);
1039
+ }
1040
+ if (!parsed || typeof parsed !== 'object') {
1041
+ return [];
1042
+ }
1043
+ const row = parsed;
1044
+ const items = row['items'] ?? row['content'];
1045
+ if (!Array.isArray(items)) {
1046
+ return [];
1047
+ }
1048
+ return items.map(labelFromUnknown).filter(Boolean);
1049
+ }
1050
+ function labelFromUnknown(value) {
1051
+ if (typeof value === 'string') {
1052
+ return value.trim();
1053
+ }
1054
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
1055
+ const text = value['text'];
1056
+ if (typeof text === 'string') {
1057
+ return text.trim();
1058
+ }
1059
+ }
1060
+ return '';
1061
+ }
1062
+ function parseJsonPayload(rawText) {
1063
+ let text = rawText.trim();
1064
+ if (!text) {
1065
+ return null;
1066
+ }
1067
+ text = text.replace(/^```(?:json)?\s*/i, '').replace(/\s*```$/u, '').trim();
1068
+ try {
1069
+ return JSON.parse(text);
1070
+ }
1071
+ catch {
1072
+ const objectStart = text.indexOf('{');
1073
+ const arrayStart = text.indexOf('[');
1074
+ const startCandidates = [objectStart, arrayStart].filter((index) => index >= 0);
1075
+ if (startCandidates.length === 0) {
1076
+ return null;
1077
+ }
1078
+ const start = Math.min(...startCandidates);
1079
+ const end = text.lastIndexOf(start === objectStart ? '}' : ']');
1080
+ if (end <= start) {
1081
+ return null;
1082
+ }
1083
+ try {
1084
+ return JSON.parse(text.slice(start, end + 1));
1085
+ }
1086
+ catch {
1087
+ return null;
1088
+ }
1089
+ }
1090
+ }
1091
+ //#endregion
1092
+
251
1093
  //#region ---- Contracts barrel ----
252
1094
 
253
1095
  /**
254
1096
  * Generated bundle index. Do not edit.
255
1097
  */
256
1098
 
257
- export { AXMPermissionsKeys, AXM_ASSIST_CONVERSATION_TITLE_SYSTEM_PROMPT, AXM_COMMENT_LIKE_EMOJI, AXM_CONVERSATION_ASSIST_MESSAGE_HANDLER, AXPCollaborationPermissionKeys, AXPConversationMenuKeys, RootConfig, axmBuildHeuristicConversationTitleFallback, axmChatAiPeerParticipantId, axmCoerceMessageTimestampMs, axmCompareMessagesChronologically, axmConversationHasAiChat, axmEnsureAssistMessageSortsAfterReplyTarget, axmExtractAssistIdFromMetadata, axmExtractDirectAgentIdFromMetadata, axmIsUnsupportedLegacyAgentAssistConversation, axmNormalizeConversationTitle, axmReadAssistResponseStatus, axmReadReplyToUserMessageId, axmResolveAssistMessageTimestamp, axmSanitizeAiGeneratedConversationTitle, extractMessageTextFromPayload };
1099
+ export { AXMChannelErrorCodes, AXMPermissionsKeys, AXM_ASSIST_CONVERSATION_FOLLOW_UP_SYSTEM_PROMPT, AXM_ASSIST_CONVERSATION_TITLE_SYSTEM_PROMPT, AXM_ASSIST_LAUNCH_CONTEXT_METADATA_KEY, AXM_CHANNEL_AUDIENCE_TARGET_ENTITY, AXM_COMMENT_LIKE_EMOJI, AXM_CONTEXTUAL_ASSIST_ACTION_ENTITY_RECORD, AXM_CONTEXTUAL_ASSIST_KIND_ENTITY_RECORD, AXM_CONTEXTUAL_ASSIST_LAYOUT_JSON_KEY, AXM_CONTEXTUAL_ASSIST_MAX_LAYOUT_JSON_CHARS, AXM_CONTEXTUAL_ASSIST_STARTER_SYSTEM_PROMPT, AXM_CONVERSATION_ANNOUNCEMENT_CHANNEL_KEY, AXM_CONVERSATION_ANNOUNCEMENT_CHANNEL_NAME, AXM_CONVERSATION_ANNOUNCEMENT_FEED_ITEM_META_KEY, AXM_CONVERSATION_ANNOUNCEMENT_MESSAGE_TYPE, AXM_CONVERSATION_ASSIST_MESSAGE_HANDLER, AXM_CONVERSATION_CHANNEL_POST_ID_META_KEY, AXPCollaborationPermissionKeys, AXPConversationCommandKeys, AXPConversationMenuKeys, AXPConversationQueryKeys, CHANNEL_AUDIENCE_ENTITY_NAME, CHANNEL_DISTRIBUTOR, CHANNEL_NOTIFICATION_PORT, CHANNEL_POST_ENTITY_NAME, CHANNEL_POST_PUBLISHED_EVENT_NAME, CHANNEL_RECEIPT_ENTITY_NAME, ConversationManifest, RootConfig, axmAiChatConversationUiMetadata, axmAnnouncementChannelMetadata, axmBuildAssistConversationFollowUpUserPrompt, axmBuildContextualAssistStarterUserPrompt, axmBuildHeuristicConversationTitleFallback, axmChannelAudienceIsVisible, axmChannelAudienceRuleMatches, axmChannelAudienceTargetEntity, axmChannelPostIdFromMessage, axmChatAiPeerParticipantId, axmCoerceMessageTimestampMs, axmCompareMessagesChronologically, axmConversationHasAiChat, axmEnsureAssistMessageSortsAfterReplyTarget, axmExtractAssistIdFromMetadata, axmExtractAssistLaunchContextFromMetadata, axmExtractDirectAgentIdFromMetadata, axmFormatContextualAssistLaunchInjection, axmInferFollowUpChipLocale, axmIsAnnouncementChannel, axmIsChannelRoom, axmIsCommentRoom, axmIsFormTemplateDesignerAssistLaunch, axmIsUnsupportedLegacyAgentAssistConversation, axmMessagesAfterId, axmNormalizeConversationTitle, axmParseContextualAssistStarterItems, axmReadAssistFollowUpStatus, axmReadAssistResponseStatus, axmReadReplyToUserMessageId, axmResolveAssistMessageTimestamp, axmResolveChannelAudienceUserIds, axmResolveConversationMessageScopeInfo, axmResolveFollowUpNavChipLabels, axmRoomAudienceRule, axmSanitizeAiGeneratedConversationTitle, axmSanitizeContextualAssistContext, axmSanitizeContextualAssistValue, axmScopeInfoNeedsTenantStamp, axmShouldSkipContextualAssistKey, channelPostMessageName, channelPostTitleText, extractMessageTextFromPayload, toChannelFeedItem };
258
1100
  //# sourceMappingURL=acorex-platform-module-conversation-client-contracts.mjs.map