@open-mercato/ai-assistant 0.6.2-develop.3461.1.605f31c2c9 → 0.6.2

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 (63) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/AGENTS.md +2 -2
  3. package/dist/modules/ai_assistant/acl.js +1 -0
  4. package/dist/modules/ai_assistant/acl.js.map +2 -2
  5. package/dist/modules/ai_assistant/api/ai/chat/route.js +197 -2
  6. package/dist/modules/ai_assistant/api/ai/chat/route.js.map +2 -2
  7. package/dist/modules/ai_assistant/api/ai/conversations/[conversationId]/route.js +272 -0
  8. package/dist/modules/ai_assistant/api/ai/conversations/[conversationId]/route.js.map +7 -0
  9. package/dist/modules/ai_assistant/api/ai/conversations/import/route.js +108 -0
  10. package/dist/modules/ai_assistant/api/ai/conversations/import/route.js.map +7 -0
  11. package/dist/modules/ai_assistant/api/ai/conversations/route.js +207 -0
  12. package/dist/modules/ai_assistant/api/ai/conversations/route.js.map +7 -0
  13. package/dist/modules/ai_assistant/data/entities/AiChatConversation.js +5 -0
  14. package/dist/modules/ai_assistant/data/entities/AiChatConversation.js.map +7 -0
  15. package/dist/modules/ai_assistant/data/entities/AiChatConversationParticipant.js +5 -0
  16. package/dist/modules/ai_assistant/data/entities/AiChatConversationParticipant.js.map +7 -0
  17. package/dist/modules/ai_assistant/data/entities/AiChatMessage.js +5 -0
  18. package/dist/modules/ai_assistant/data/entities/AiChatMessage.js.map +7 -0
  19. package/dist/modules/ai_assistant/data/entities.js +200 -0
  20. package/dist/modules/ai_assistant/data/entities.js.map +2 -2
  21. package/dist/modules/ai_assistant/data/repositories/AiChatConversationRepository.js +448 -0
  22. package/dist/modules/ai_assistant/data/repositories/AiChatConversationRepository.js.map +7 -0
  23. package/dist/modules/ai_assistant/data/validators.js +72 -0
  24. package/dist/modules/ai_assistant/data/validators.js.map +7 -0
  25. package/dist/modules/ai_assistant/i18n/de.json +3 -0
  26. package/dist/modules/ai_assistant/i18n/en.json +3 -0
  27. package/dist/modules/ai_assistant/i18n/es.json +3 -0
  28. package/dist/modules/ai_assistant/i18n/pl.json +3 -0
  29. package/dist/modules/ai_assistant/lib/conversation-storage.js +43 -0
  30. package/dist/modules/ai_assistant/lib/conversation-storage.js.map +7 -0
  31. package/dist/modules/ai_assistant/migrations/Migration20260518092853_ai_assistant.js +28 -0
  32. package/dist/modules/ai_assistant/migrations/Migration20260518092853_ai_assistant.js.map +7 -0
  33. package/dist/modules/ai_assistant/setup.js +1 -0
  34. package/dist/modules/ai_assistant/setup.js.map +2 -2
  35. package/generated/entities/ai_chat_conversation/index.ts +15 -0
  36. package/generated/entities/ai_chat_conversation_participant/index.ts +9 -0
  37. package/generated/entities/ai_chat_message/index.ts +16 -0
  38. package/generated/entities.ids.generated.ts +4 -1
  39. package/generated/entity-fields-registry.ts +46 -0
  40. package/jest.config.cjs +3 -1
  41. package/package.json +14 -15
  42. package/src/modules/ai_assistant/acl.ts +1 -0
  43. package/src/modules/ai_assistant/api/ai/chat/__tests__/route.test.ts +107 -0
  44. package/src/modules/ai_assistant/api/ai/chat/route.ts +245 -1
  45. package/src/modules/ai_assistant/api/ai/conversations/[conversationId]/route.ts +320 -0
  46. package/src/modules/ai_assistant/api/ai/conversations/__tests__/route.test.ts +93 -0
  47. package/src/modules/ai_assistant/api/ai/conversations/import/route.ts +122 -0
  48. package/src/modules/ai_assistant/api/ai/conversations/route.ts +241 -0
  49. package/src/modules/ai_assistant/data/entities/AiChatConversation.ts +2 -0
  50. package/src/modules/ai_assistant/data/entities/AiChatConversationParticipant.ts +2 -0
  51. package/src/modules/ai_assistant/data/entities/AiChatMessage.ts +2 -0
  52. package/src/modules/ai_assistant/data/entities.ts +255 -0
  53. package/src/modules/ai_assistant/data/repositories/AiChatConversationRepository.ts +597 -0
  54. package/src/modules/ai_assistant/data/repositories/__tests__/AiChatConversationRepository.test.ts +592 -0
  55. package/src/modules/ai_assistant/data/validators.ts +134 -0
  56. package/src/modules/ai_assistant/i18n/de.json +3 -0
  57. package/src/modules/ai_assistant/i18n/en.json +3 -0
  58. package/src/modules/ai_assistant/i18n/es.json +3 -0
  59. package/src/modules/ai_assistant/i18n/pl.json +3 -0
  60. package/src/modules/ai_assistant/lib/conversation-storage.ts +93 -0
  61. package/src/modules/ai_assistant/migrations/.snapshot-open-mercato.json +822 -0
  62. package/src/modules/ai_assistant/migrations/Migration20260518092853_ai_assistant.ts +39 -0
  63. package/src/modules/ai_assistant/setup.ts +1 -0
@@ -0,0 +1,448 @@
1
+ import {
2
+ findOneWithDecryption,
3
+ findWithDecryption
4
+ } from "@open-mercato/shared/lib/encryption/find";
5
+ import {
6
+ AiChatConversation,
7
+ AiChatConversationParticipant,
8
+ AiChatMessage
9
+ } from "../entities.js";
10
+ const DEFAULT_LIST_LIMIT = 50;
11
+ const MAX_LIST_LIMIT = 100;
12
+ const DEFAULT_TRANSCRIPT_LIMIT = 100;
13
+ const MAX_TRANSCRIPT_LIMIT = 200;
14
+ class AiChatConversationAccessError extends Error {
15
+ constructor(message = "Conversation is not accessible to the caller.") {
16
+ super(message);
17
+ this.name = "AiChatConversationAccessError";
18
+ }
19
+ }
20
+ class AiChatConversationRepository {
21
+ constructor(em) {
22
+ this.em = em;
23
+ }
24
+ /**
25
+ * Idempotent create. If a non-deleted conversation already exists for the
26
+ * caller in this tenant/org with the same `conversationId`, returns the
27
+ * existing row. The owner-participant row is created in the same
28
+ * transaction; a partial failure leaves no orphan conversation.
29
+ */
30
+ async createOrGet(input, ctx) {
31
+ assertContext(ctx, "createOrGet");
32
+ if (!input?.agentId) {
33
+ throw new Error("AiChatConversationRepository.createOrGet requires agentId");
34
+ }
35
+ const now = input.now ?? /* @__PURE__ */ new Date();
36
+ const conversationId = (input.conversationId ?? "").trim() || generateConversationId();
37
+ return this.em.transactional(async (tx) => {
38
+ const existing = await findOneAccessibleConversation(
39
+ tx,
40
+ conversationId,
41
+ ctx
42
+ );
43
+ if (existing) {
44
+ if (existing.ownerUserId !== ctx.userId) {
45
+ throw new AiChatConversationAccessError();
46
+ }
47
+ return existing;
48
+ }
49
+ const conversation = tx.create(AiChatConversation, {
50
+ tenantId: ctx.tenantId,
51
+ organizationId: ctx.organizationId ?? null,
52
+ conversationId,
53
+ agentId: input.agentId,
54
+ ownerUserId: ctx.userId,
55
+ title: normalizeTitle(input.title),
56
+ status: "open",
57
+ visibility: "private",
58
+ pageContext: input.pageContext ?? null,
59
+ lastMessageAt: null,
60
+ importedFromLocalAt: input.importedFromLocal ? now : null,
61
+ createdAt: now,
62
+ updatedAt: now,
63
+ deletedAt: null
64
+ });
65
+ const participant = tx.create(AiChatConversationParticipant, {
66
+ tenantId: ctx.tenantId,
67
+ organizationId: ctx.organizationId ?? null,
68
+ conversationId,
69
+ userId: ctx.userId,
70
+ role: "owner",
71
+ lastReadAt: null,
72
+ createdAt: now,
73
+ updatedAt: now
74
+ });
75
+ await tx.persist(conversation).persist(participant).flush();
76
+ return conversation;
77
+ });
78
+ }
79
+ /** Fetch within tenant/org. View-only callers see only their own conversations. */
80
+ async getById(conversationId, ctx) {
81
+ assertContext(ctx, "getById");
82
+ if (!conversationId) return null;
83
+ const row = await findOneAccessibleConversation(this.em, conversationId, ctx);
84
+ if (!row) return null;
85
+ if (!canAccessConversation(row, ctx)) return null;
86
+ return row;
87
+ }
88
+ /** Owner-scoped list unless the caller has tenant/org manage access. */
89
+ async list(ctx, options = {}) {
90
+ assertContext(ctx, "list");
91
+ const limit = clampLimit(options.limit, DEFAULT_LIST_LIMIT, MAX_LIST_LIMIT);
92
+ const where = {
93
+ tenantId: ctx.tenantId,
94
+ organizationId: ctx.organizationId ?? null,
95
+ deletedAt: null
96
+ };
97
+ if (!canManageConversations(ctx)) where.ownerUserId = ctx.userId;
98
+ if (options.agentId) where.agentId = options.agentId;
99
+ if (options.status) where.status = options.status;
100
+ if (options.cursor) {
101
+ const cursorDate = parseIso(options.cursor);
102
+ if (cursorDate) {
103
+ where.lastMessageAt = { $lt: cursorDate };
104
+ }
105
+ }
106
+ const rows = await findWithDecryption(
107
+ this.em,
108
+ AiChatConversation,
109
+ where,
110
+ {
111
+ orderBy: [{ lastMessageAt: "desc" }, { createdAt: "desc" }],
112
+ limit: limit + 1
113
+ },
114
+ {
115
+ tenantId: ctx.tenantId ?? null,
116
+ organizationId: ctx.organizationId ?? null
117
+ }
118
+ );
119
+ let nextCursor = null;
120
+ if (rows.length > limit) {
121
+ const lastIncluded = rows[limit - 1];
122
+ const cursorValue = lastIncluded.lastMessageAt ?? lastIncluded.createdAt;
123
+ nextCursor = cursorValue ? cursorValue.toISOString() : null;
124
+ }
125
+ return { items: rows.slice(0, limit), nextCursor };
126
+ }
127
+ /** Update within tenant/org. View-only callers can update only their own conversations. */
128
+ async update(conversationId, patch, ctx) {
129
+ assertContext(ctx, "update");
130
+ if (!conversationId) {
131
+ throw new Error("AiChatConversationRepository.update requires conversationId");
132
+ }
133
+ return this.em.transactional(async (tx) => {
134
+ const existing = await findOneAccessibleConversation(
135
+ tx,
136
+ conversationId,
137
+ ctx
138
+ );
139
+ if (!existing) {
140
+ throw new AiChatConversationAccessError(
141
+ `Conversation "${conversationId}" was not found for the caller.`
142
+ );
143
+ }
144
+ if (!canAccessConversation(existing, ctx)) {
145
+ throw new AiChatConversationAccessError();
146
+ }
147
+ const now = patch.now ?? /* @__PURE__ */ new Date();
148
+ if (Object.prototype.hasOwnProperty.call(patch, "title")) {
149
+ existing.title = normalizeTitle(patch.title);
150
+ }
151
+ if (patch.status) existing.status = patch.status;
152
+ if (Object.prototype.hasOwnProperty.call(patch, "pageContext")) {
153
+ existing.pageContext = patch.pageContext ?? null;
154
+ }
155
+ existing.updatedAt = now;
156
+ await tx.persist(existing).flush();
157
+ return existing;
158
+ });
159
+ }
160
+ /** Soft-delete the conversation and all its messages in one transaction. */
161
+ async softDelete(conversationId, ctx, now = /* @__PURE__ */ new Date()) {
162
+ assertContext(ctx, "softDelete");
163
+ if (!conversationId) {
164
+ throw new Error("AiChatConversationRepository.softDelete requires conversationId");
165
+ }
166
+ await this.em.transactional(async (tx) => {
167
+ const existing = await findOneAccessibleConversation(
168
+ tx,
169
+ conversationId,
170
+ ctx
171
+ );
172
+ if (!existing) {
173
+ throw new AiChatConversationAccessError(
174
+ `Conversation "${conversationId}" was not found for the caller.`
175
+ );
176
+ }
177
+ if (!canAccessConversation(existing, ctx)) {
178
+ throw new AiChatConversationAccessError();
179
+ }
180
+ existing.deletedAt = now;
181
+ existing.status = "closed";
182
+ existing.updatedAt = now;
183
+ await tx.persist(existing).flush();
184
+ const messages = await findWithDecryption(
185
+ tx,
186
+ AiChatMessage,
187
+ {
188
+ tenantId: ctx.tenantId,
189
+ organizationId: ctx.organizationId ?? null,
190
+ conversationId,
191
+ deletedAt: null
192
+ },
193
+ {},
194
+ {
195
+ tenantId: ctx.tenantId ?? null,
196
+ organizationId: ctx.organizationId ?? null
197
+ }
198
+ );
199
+ for (const msg of messages) {
200
+ msg.deletedAt = now;
201
+ msg.updatedAt = now;
202
+ tx.persist(msg);
203
+ }
204
+ if (messages.length > 0) await tx.flush();
205
+ });
206
+ }
207
+ /**
208
+ * Owner-only transcript hydration. Internally fetched DESC so the `before`
209
+ * cursor naturally advances toward older messages, then reversed so the
210
+ * response contract (`messages` array ordered ascending by `createdAt`)
211
+ * stays stable for callers. `nextCursor` points to the OLDEST message in
212
+ * the returned page — the next call with `before=<cursor>` fetches the
213
+ * next-older window.
214
+ */
215
+ async getTranscript(conversationId, ctx, options = {}) {
216
+ assertContext(ctx, "getTranscript");
217
+ if (!conversationId) return null;
218
+ const conversation = await this.getById(conversationId, ctx);
219
+ if (!conversation) return null;
220
+ const limit = clampLimit(options.limit, DEFAULT_TRANSCRIPT_LIMIT, MAX_TRANSCRIPT_LIMIT);
221
+ const where = {
222
+ tenantId: ctx.tenantId,
223
+ organizationId: ctx.organizationId ?? null,
224
+ conversationId,
225
+ deletedAt: null
226
+ };
227
+ if (options.before) {
228
+ const beforeDate = parseIso(options.before);
229
+ if (beforeDate) {
230
+ where.createdAt = { $lt: beforeDate };
231
+ }
232
+ }
233
+ const rows = await findWithDecryption(
234
+ this.em,
235
+ AiChatMessage,
236
+ where,
237
+ {
238
+ orderBy: { createdAt: "desc" },
239
+ limit: limit + 1
240
+ },
241
+ {
242
+ tenantId: ctx.tenantId ?? null,
243
+ organizationId: ctx.organizationId ?? null
244
+ }
245
+ );
246
+ let nextCursor = null;
247
+ let pageDesc;
248
+ if (rows.length > limit) {
249
+ pageDesc = rows.slice(0, limit);
250
+ const oldestIncluded = pageDesc[pageDesc.length - 1];
251
+ nextCursor = oldestIncluded?.createdAt ? oldestIncluded.createdAt.toISOString() : null;
252
+ } else {
253
+ pageDesc = rows;
254
+ }
255
+ const messages = [...pageDesc].reverse();
256
+ return { conversation, messages, nextCursor };
257
+ }
258
+ /**
259
+ * Append a single message to an owner-accessible conversation. Honors
260
+ * `clientMessageId` idempotency: if a non-deleted message with the same
261
+ * client id already exists, returns it untouched.
262
+ */
263
+ async appendMessage(conversationId, input, ctx, options = {}) {
264
+ assertContext(ctx, "appendMessage");
265
+ if (!conversationId) {
266
+ throw new Error("AiChatConversationRepository.appendMessage requires conversationId");
267
+ }
268
+ return this.em.transactional(async (tx) => {
269
+ const conversation = await findOneAccessibleConversation(
270
+ tx,
271
+ conversationId,
272
+ ctx
273
+ );
274
+ if (!conversation) {
275
+ throw new AiChatConversationAccessError(
276
+ `Conversation "${conversationId}" was not found for the caller.`
277
+ );
278
+ }
279
+ if (conversation.ownerUserId !== ctx.userId) {
280
+ throw new AiChatConversationAccessError();
281
+ }
282
+ const now = options.createdAt ?? /* @__PURE__ */ new Date();
283
+ if (input.clientMessageId) {
284
+ const existing = await findOneWithDecryption(
285
+ tx,
286
+ AiChatMessage,
287
+ {
288
+ tenantId: ctx.tenantId,
289
+ organizationId: ctx.organizationId ?? null,
290
+ conversationId,
291
+ clientMessageId: input.clientMessageId,
292
+ deletedAt: null
293
+ },
294
+ {},
295
+ {
296
+ tenantId: ctx.tenantId ?? null,
297
+ organizationId: ctx.organizationId ?? null
298
+ }
299
+ );
300
+ if (existing) return existing;
301
+ }
302
+ const message = tx.create(AiChatMessage, {
303
+ tenantId: ctx.tenantId,
304
+ organizationId: ctx.organizationId ?? null,
305
+ conversationId,
306
+ clientMessageId: input.clientMessageId ?? null,
307
+ role: input.role,
308
+ content: input.content,
309
+ uiParts: normalizeArray(input.uiParts),
310
+ attachmentIds: normalizeArray(input.attachmentIds),
311
+ filesMetadata: normalizeArray(input.files),
312
+ model: input.model ?? null,
313
+ metadata: input.metadata ?? null,
314
+ createdByUserId: options.createdByUserId === void 0 ? input.role === "user" ? ctx.userId : null : options.createdByUserId,
315
+ createdAt: now,
316
+ updatedAt: now,
317
+ deletedAt: null
318
+ });
319
+ conversation.lastMessageAt = now;
320
+ conversation.updatedAt = now;
321
+ await tx.persist(message).persist(conversation).flush();
322
+ return message;
323
+ });
324
+ }
325
+ /**
326
+ * Lazy migration entrypoint: create-or-get the conversation and append the
327
+ * provided messages with `clientMessageId` dedupe. Designed to be safe to
328
+ * call repeatedly — repeated imports of the same payload return the same
329
+ * counts of imported/skipped rows.
330
+ */
331
+ async importLocalConversation(input, ctx, now = /* @__PURE__ */ new Date()) {
332
+ assertContext(ctx, "importLocalConversation");
333
+ const conversation = await this.createOrGet(
334
+ { ...input.conversation, importedFromLocal: true, now },
335
+ ctx
336
+ );
337
+ if (input.conversation.status && conversation.status !== input.conversation.status) {
338
+ await this.update(
339
+ conversation.conversationId,
340
+ { status: input.conversation.status, now },
341
+ ctx
342
+ );
343
+ }
344
+ let imported = 0;
345
+ let skipped = 0;
346
+ for (const message of input.messages) {
347
+ if (!message.clientMessageId) {
348
+ skipped += 1;
349
+ continue;
350
+ }
351
+ const before = await findOneWithDecryption(
352
+ this.em,
353
+ AiChatMessage,
354
+ {
355
+ tenantId: ctx.tenantId,
356
+ organizationId: ctx.organizationId ?? null,
357
+ conversationId: conversation.conversationId,
358
+ clientMessageId: message.clientMessageId,
359
+ deletedAt: null
360
+ },
361
+ {},
362
+ {
363
+ tenantId: ctx.tenantId ?? null,
364
+ organizationId: ctx.organizationId ?? null
365
+ }
366
+ );
367
+ if (before) {
368
+ skipped += 1;
369
+ continue;
370
+ }
371
+ await this.appendMessage(
372
+ conversation.conversationId,
373
+ message,
374
+ ctx,
375
+ { createdAt: now }
376
+ );
377
+ imported += 1;
378
+ }
379
+ return {
380
+ conversation,
381
+ importedMessageCount: imported,
382
+ skippedMessageCount: skipped
383
+ };
384
+ }
385
+ }
386
+ function assertContext(ctx, method) {
387
+ if (!ctx?.tenantId) {
388
+ throw new Error(`AiChatConversationRepository.${method} requires tenantId`);
389
+ }
390
+ if (!ctx?.userId) {
391
+ throw new Error(`AiChatConversationRepository.${method} requires userId`);
392
+ }
393
+ }
394
+ function canManageConversations(ctx) {
395
+ return ctx.canManageConversations === true;
396
+ }
397
+ function canAccessConversation(row, ctx) {
398
+ return canManageConversations(ctx) || row.ownerUserId === ctx.userId;
399
+ }
400
+ async function findOneAccessibleConversation(em, conversationId, ctx) {
401
+ const row = await findOneWithDecryption(
402
+ em,
403
+ AiChatConversation,
404
+ {
405
+ tenantId: ctx.tenantId,
406
+ organizationId: ctx.organizationId ?? null,
407
+ conversationId,
408
+ deletedAt: null
409
+ },
410
+ {},
411
+ {
412
+ tenantId: ctx.tenantId ?? null,
413
+ organizationId: ctx.organizationId ?? null
414
+ }
415
+ );
416
+ return row ?? null;
417
+ }
418
+ function normalizeTitle(title) {
419
+ if (title === void 0) return null;
420
+ if (title === null) return null;
421
+ const trimmed = title.trim();
422
+ return trimmed.length > 0 ? trimmed : null;
423
+ }
424
+ function normalizeArray(value) {
425
+ if (!Array.isArray(value) || value.length === 0) return null;
426
+ return value;
427
+ }
428
+ function clampLimit(value, fallback, max) {
429
+ if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
430
+ return Math.max(1, Math.min(Math.floor(value), max));
431
+ }
432
+ function parseIso(value) {
433
+ if (!value) return null;
434
+ const date = new Date(value);
435
+ return Number.isNaN(date.getTime()) ? null : date;
436
+ }
437
+ function generateConversationId() {
438
+ const cryptoMod = typeof globalThis === "object" ? globalThis.crypto : void 0;
439
+ if (cryptoMod?.randomUUID) return cryptoMod.randomUUID();
440
+ return `chat_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 12)}`;
441
+ }
442
+ var AiChatConversationRepository_default = AiChatConversationRepository;
443
+ export {
444
+ AiChatConversationAccessError,
445
+ AiChatConversationRepository,
446
+ AiChatConversationRepository_default as default
447
+ };
448
+ //# sourceMappingURL=AiChatConversationRepository.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../../src/modules/ai_assistant/data/repositories/AiChatConversationRepository.ts"],
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport {\n findOneWithDecryption,\n findWithDecryption,\n} from '@open-mercato/shared/lib/encryption/find'\nimport {\n AiChatConversation,\n AiChatConversationParticipant,\n AiChatMessage,\n} from '../entities'\nimport type {\n AiChatMessageAppendInput,\n AiChatPageContextInput,\n} from '../validators'\n\n/**\n * Persistent store for AI chat conversations, participants, and messages.\n *\n * Owner-first MVP per spec\n * `2026-05-05-ai-chat-server-side-conversation-storage`. Every read/write\n * goes through `findOneWithDecryption` / `findWithDecryption` so the repo\n * stays consistent with the rest of the module and is GDPR-encryption-ready\n * without a second refactor when `content` / `ui_parts` columns are\n * eventually flagged.\n *\n * Tenant + organization scope is required on every method. View-only callers\n * are owner-scoped. Callers with `ai_assistant.conversations.manage` may\n * list/read/update/delete any conversation in the same tenant/org, but never\n * outside that boundary. The participant row is written transactionally\n * alongside conversation create/import.\n *\n * TODO(ai-chat-sharing): widen the non-manage read predicate to include\n * explicit undeleted participants once shared conversations are implemented.\n */\n\nexport interface AiChatConversationContext {\n tenantId: string\n organizationId?: string | null\n userId: string\n canManageConversations?: boolean\n}\n\nexport interface AiChatConversationCreateOrGetInput {\n conversationId?: string | null\n agentId: string\n title?: string | null\n pageContext?: AiChatPageContextInput | null\n /** Marks the conversation as imported from local storage (sets `importedFromLocalAt`). */\n importedFromLocal?: boolean\n /** Optional explicit `now` for deterministic tests. */\n now?: Date\n}\n\nexport interface AiChatConversationListOptions {\n agentId?: string | null\n status?: 'open' | 'closed' | null\n limit?: number\n cursor?: string | null\n}\n\nexport interface AiChatConversationUpdateInput {\n title?: string | null\n status?: 'open' | 'closed'\n pageContext?: AiChatPageContextInput | null\n /** Optional explicit `now` for deterministic tests. */\n now?: Date\n}\n\nexport interface AiChatTranscriptOptions {\n limit?: number\n /** ISO timestamp string; rows strictly older than this are returned. */\n before?: string | null\n}\n\nexport interface AiChatTranscriptResult {\n conversation: AiChatConversation\n messages: AiChatMessage[]\n nextCursor: string | null\n}\n\nexport interface AiChatMessageAppendOptions {\n /** Override the message timestamp (used to thread server-injected stream-completion turns). */\n createdAt?: Date\n /** Override `createdByUserId` (defaults to the calling context user). */\n createdByUserId?: string | null\n}\n\nexport interface AiChatConversationImportResult {\n conversation: AiChatConversation\n importedMessageCount: number\n skippedMessageCount: number\n}\n\nconst DEFAULT_LIST_LIMIT = 50\nconst MAX_LIST_LIMIT = 100\nconst DEFAULT_TRANSCRIPT_LIMIT = 100\nconst MAX_TRANSCRIPT_LIMIT = 200\n\nexport class AiChatConversationAccessError extends Error {\n override readonly name = 'AiChatConversationAccessError'\n constructor(message: string = 'Conversation is not accessible to the caller.') {\n super(message)\n }\n}\n\nexport class AiChatConversationRepository {\n constructor(private readonly em: EntityManager) {}\n\n /**\n * Idempotent create. If a non-deleted conversation already exists for the\n * caller in this tenant/org with the same `conversationId`, returns the\n * existing row. The owner-participant row is created in the same\n * transaction; a partial failure leaves no orphan conversation.\n */\n async createOrGet(\n input: AiChatConversationCreateOrGetInput,\n ctx: AiChatConversationContext,\n ): Promise<AiChatConversation> {\n assertContext(ctx, 'createOrGet')\n if (!input?.agentId) {\n throw new Error('AiChatConversationRepository.createOrGet requires agentId')\n }\n const now = input.now ?? new Date()\n const conversationId = (input.conversationId ?? '').trim() || generateConversationId()\n\n return this.em.transactional(async (tx) => {\n const existing = await findOneAccessibleConversation(\n tx as unknown as EntityManager,\n conversationId,\n ctx,\n )\n if (existing) {\n if (existing.ownerUserId !== ctx.userId) {\n throw new AiChatConversationAccessError()\n }\n return existing\n }\n const conversation = tx.create(AiChatConversation, {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId ?? null,\n conversationId,\n agentId: input.agentId,\n ownerUserId: ctx.userId,\n title: normalizeTitle(input.title),\n status: 'open',\n visibility: 'private',\n pageContext: input.pageContext ?? null,\n lastMessageAt: null,\n importedFromLocalAt: input.importedFromLocal ? now : null,\n createdAt: now,\n updatedAt: now,\n deletedAt: null,\n } as unknown as AiChatConversation)\n const participant = tx.create(AiChatConversationParticipant, {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId ?? null,\n conversationId,\n userId: ctx.userId,\n role: 'owner',\n lastReadAt: null,\n createdAt: now,\n updatedAt: now,\n } as unknown as AiChatConversationParticipant)\n await tx.persist(conversation).persist(participant).flush()\n return conversation\n })\n }\n\n /** Fetch within tenant/org. View-only callers see only their own conversations. */\n async getById(\n conversationId: string,\n ctx: AiChatConversationContext,\n ): Promise<AiChatConversation | null> {\n assertContext(ctx, 'getById')\n if (!conversationId) return null\n const row = await findOneAccessibleConversation(this.em, conversationId, ctx)\n if (!row) return null\n if (!canAccessConversation(row, ctx)) return null\n return row\n }\n\n /** Owner-scoped list unless the caller has tenant/org manage access. */\n async list(\n ctx: AiChatConversationContext,\n options: AiChatConversationListOptions = {},\n ): Promise<{ items: AiChatConversation[]; nextCursor: string | null }> {\n assertContext(ctx, 'list')\n const limit = clampLimit(options.limit, DEFAULT_LIST_LIMIT, MAX_LIST_LIMIT)\n const where: Record<string, unknown> = {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId ?? null,\n deletedAt: null,\n }\n if (!canManageConversations(ctx)) where.ownerUserId = ctx.userId\n if (options.agentId) where.agentId = options.agentId\n if (options.status) where.status = options.status\n if (options.cursor) {\n const cursorDate = parseIso(options.cursor)\n if (cursorDate) {\n where.lastMessageAt = { $lt: cursorDate }\n }\n }\n const rows = await findWithDecryption<AiChatConversation>(\n this.em,\n AiChatConversation,\n where as any,\n {\n orderBy: [{ lastMessageAt: 'desc' }, { createdAt: 'desc' }] as any,\n limit: limit + 1,\n },\n {\n tenantId: ctx.tenantId ?? null,\n organizationId: ctx.organizationId ?? null,\n },\n )\n let nextCursor: string | null = null\n if (rows.length > limit) {\n const lastIncluded = rows[limit - 1]\n const cursorValue = lastIncluded.lastMessageAt ?? lastIncluded.createdAt\n nextCursor = cursorValue ? cursorValue.toISOString() : null\n }\n return { items: rows.slice(0, limit), nextCursor }\n }\n\n /** Update within tenant/org. View-only callers can update only their own conversations. */\n async update(\n conversationId: string,\n patch: AiChatConversationUpdateInput,\n ctx: AiChatConversationContext,\n ): Promise<AiChatConversation> {\n assertContext(ctx, 'update')\n if (!conversationId) {\n throw new Error('AiChatConversationRepository.update requires conversationId')\n }\n return this.em.transactional(async (tx) => {\n const existing = await findOneAccessibleConversation(\n tx as unknown as EntityManager,\n conversationId,\n ctx,\n )\n if (!existing) {\n throw new AiChatConversationAccessError(\n `Conversation \"${conversationId}\" was not found for the caller.`,\n )\n }\n if (!canAccessConversation(existing, ctx)) {\n throw new AiChatConversationAccessError()\n }\n const now = patch.now ?? new Date()\n if (Object.prototype.hasOwnProperty.call(patch, 'title')) {\n existing.title = normalizeTitle(patch.title)\n }\n if (patch.status) existing.status = patch.status\n if (Object.prototype.hasOwnProperty.call(patch, 'pageContext')) {\n existing.pageContext = patch.pageContext ?? null\n }\n existing.updatedAt = now\n await tx.persist(existing).flush()\n return existing\n })\n }\n\n /** Soft-delete the conversation and all its messages in one transaction. */\n async softDelete(\n conversationId: string,\n ctx: AiChatConversationContext,\n now: Date = new Date(),\n ): Promise<void> {\n assertContext(ctx, 'softDelete')\n if (!conversationId) {\n throw new Error('AiChatConversationRepository.softDelete requires conversationId')\n }\n await this.em.transactional(async (tx) => {\n const existing = await findOneAccessibleConversation(\n tx as unknown as EntityManager,\n conversationId,\n ctx,\n )\n if (!existing) {\n throw new AiChatConversationAccessError(\n `Conversation \"${conversationId}\" was not found for the caller.`,\n )\n }\n if (!canAccessConversation(existing, ctx)) {\n throw new AiChatConversationAccessError()\n }\n existing.deletedAt = now\n existing.status = 'closed'\n existing.updatedAt = now\n await tx.persist(existing).flush()\n\n const messages = await findWithDecryption<AiChatMessage>(\n tx as unknown as EntityManager,\n AiChatMessage,\n {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId ?? null,\n conversationId,\n deletedAt: null,\n } as any,\n {},\n {\n tenantId: ctx.tenantId ?? null,\n organizationId: ctx.organizationId ?? null,\n },\n )\n for (const msg of messages) {\n msg.deletedAt = now\n msg.updatedAt = now\n tx.persist(msg)\n }\n if (messages.length > 0) await tx.flush()\n })\n }\n\n /**\n * Owner-only transcript hydration. Internally fetched DESC so the `before`\n * cursor naturally advances toward older messages, then reversed so the\n * response contract (`messages` array ordered ascending by `createdAt`)\n * stays stable for callers. `nextCursor` points to the OLDEST message in\n * the returned page \u2014 the next call with `before=<cursor>` fetches the\n * next-older window.\n */\n async getTranscript(\n conversationId: string,\n ctx: AiChatConversationContext,\n options: AiChatTranscriptOptions = {},\n ): Promise<AiChatTranscriptResult | null> {\n assertContext(ctx, 'getTranscript')\n if (!conversationId) return null\n const conversation = await this.getById(conversationId, ctx)\n if (!conversation) return null\n const limit = clampLimit(options.limit, DEFAULT_TRANSCRIPT_LIMIT, MAX_TRANSCRIPT_LIMIT)\n const where: Record<string, unknown> = {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId ?? null,\n conversationId,\n deletedAt: null,\n }\n if (options.before) {\n const beforeDate = parseIso(options.before)\n if (beforeDate) {\n where.createdAt = { $lt: beforeDate }\n }\n }\n const rows = await findWithDecryption<AiChatMessage>(\n this.em,\n AiChatMessage,\n where as any,\n {\n orderBy: { createdAt: 'desc' } as any,\n limit: limit + 1,\n },\n {\n tenantId: ctx.tenantId ?? null,\n organizationId: ctx.organizationId ?? null,\n },\n )\n let nextCursor: string | null = null\n let pageDesc: AiChatMessage[]\n if (rows.length > limit) {\n pageDesc = rows.slice(0, limit)\n const oldestIncluded = pageDesc[pageDesc.length - 1]\n nextCursor = oldestIncluded?.createdAt ? oldestIncluded.createdAt.toISOString() : null\n } else {\n pageDesc = rows\n }\n const messages = [...pageDesc].reverse()\n return { conversation, messages, nextCursor }\n }\n\n /**\n * Append a single message to an owner-accessible conversation. Honors\n * `clientMessageId` idempotency: if a non-deleted message with the same\n * client id already exists, returns it untouched.\n */\n async appendMessage(\n conversationId: string,\n input: AiChatMessageAppendInput,\n ctx: AiChatConversationContext,\n options: AiChatMessageAppendOptions = {},\n ): Promise<AiChatMessage> {\n assertContext(ctx, 'appendMessage')\n if (!conversationId) {\n throw new Error('AiChatConversationRepository.appendMessage requires conversationId')\n }\n return this.em.transactional(async (tx) => {\n const conversation = await findOneAccessibleConversation(\n tx as unknown as EntityManager,\n conversationId,\n ctx,\n )\n if (!conversation) {\n throw new AiChatConversationAccessError(\n `Conversation \"${conversationId}\" was not found for the caller.`,\n )\n }\n if (conversation.ownerUserId !== ctx.userId) {\n throw new AiChatConversationAccessError()\n }\n const now = options.createdAt ?? new Date()\n if (input.clientMessageId) {\n const existing = await findOneWithDecryption<AiChatMessage>(\n tx as unknown as EntityManager,\n AiChatMessage,\n {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId ?? null,\n conversationId,\n clientMessageId: input.clientMessageId,\n deletedAt: null,\n } as any,\n {},\n {\n tenantId: ctx.tenantId ?? null,\n organizationId: ctx.organizationId ?? null,\n },\n )\n if (existing) return existing\n }\n const message = tx.create(AiChatMessage, {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId ?? null,\n conversationId,\n clientMessageId: input.clientMessageId ?? null,\n role: input.role,\n content: input.content,\n uiParts: normalizeArray(input.uiParts),\n attachmentIds: normalizeArray(input.attachmentIds),\n filesMetadata: normalizeArray(input.files),\n model: input.model ?? null,\n metadata: input.metadata ?? null,\n createdByUserId:\n options.createdByUserId === undefined\n ? input.role === 'user'\n ? ctx.userId\n : null\n : options.createdByUserId,\n createdAt: now,\n updatedAt: now,\n deletedAt: null,\n } as unknown as AiChatMessage)\n conversation.lastMessageAt = now\n conversation.updatedAt = now\n await tx.persist(message).persist(conversation).flush()\n return message\n })\n }\n\n /**\n * Lazy migration entrypoint: create-or-get the conversation and append the\n * provided messages with `clientMessageId` dedupe. Designed to be safe to\n * call repeatedly \u2014 repeated imports of the same payload return the same\n * counts of imported/skipped rows.\n */\n async importLocalConversation(\n input: {\n conversation: AiChatConversationCreateOrGetInput & {\n status?: 'open' | 'closed'\n }\n messages: AiChatMessageAppendInput[]\n },\n ctx: AiChatConversationContext,\n now: Date = new Date(),\n ): Promise<AiChatConversationImportResult> {\n assertContext(ctx, 'importLocalConversation')\n const conversation = await this.createOrGet(\n { ...input.conversation, importedFromLocal: true, now },\n ctx,\n )\n if (input.conversation.status && conversation.status !== input.conversation.status) {\n await this.update(\n conversation.conversationId,\n { status: input.conversation.status, now },\n ctx,\n )\n }\n let imported = 0\n let skipped = 0\n for (const message of input.messages) {\n if (!message.clientMessageId) {\n // Without an idempotency key the import has no safe way to dedupe.\n skipped += 1\n continue\n }\n const before = await findOneWithDecryption<AiChatMessage>(\n this.em,\n AiChatMessage,\n {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId ?? null,\n conversationId: conversation.conversationId,\n clientMessageId: message.clientMessageId,\n deletedAt: null,\n } as any,\n {},\n {\n tenantId: ctx.tenantId ?? null,\n organizationId: ctx.organizationId ?? null,\n },\n )\n if (before) {\n skipped += 1\n continue\n }\n await this.appendMessage(\n conversation.conversationId,\n message,\n ctx,\n { createdAt: now },\n )\n imported += 1\n }\n return {\n conversation,\n importedMessageCount: imported,\n skippedMessageCount: skipped,\n }\n }\n}\n\nfunction assertContext(ctx: AiChatConversationContext | undefined, method: string): void {\n if (!ctx?.tenantId) {\n throw new Error(`AiChatConversationRepository.${method} requires tenantId`)\n }\n if (!ctx?.userId) {\n throw new Error(`AiChatConversationRepository.${method} requires userId`)\n }\n}\n\nfunction canManageConversations(ctx: AiChatConversationContext): boolean {\n return ctx.canManageConversations === true\n}\n\nfunction canAccessConversation(\n row: AiChatConversation,\n ctx: AiChatConversationContext,\n): boolean {\n return canManageConversations(ctx) || row.ownerUserId === ctx.userId\n}\n\nasync function findOneAccessibleConversation(\n em: EntityManager,\n conversationId: string,\n ctx: AiChatConversationContext,\n): Promise<AiChatConversation | null> {\n const row = await findOneWithDecryption<AiChatConversation>(\n em,\n AiChatConversation,\n {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId ?? null,\n conversationId,\n deletedAt: null,\n } as any,\n {},\n {\n tenantId: ctx.tenantId ?? null,\n organizationId: ctx.organizationId ?? null,\n },\n )\n return row ?? null\n}\n\nfunction normalizeTitle(title: string | null | undefined): string | null {\n if (title === undefined) return null\n if (title === null) return null\n const trimmed = title.trim()\n return trimmed.length > 0 ? trimmed : null\n}\n\nfunction normalizeArray<T>(value: T[] | null | undefined): T[] | null {\n if (!Array.isArray(value) || value.length === 0) return null\n return value\n}\n\nfunction clampLimit(value: number | undefined | null, fallback: number, max: number): number {\n if (typeof value !== 'number' || !Number.isFinite(value)) return fallback\n return Math.max(1, Math.min(Math.floor(value), max))\n}\n\nfunction parseIso(value: string): Date | null {\n if (!value) return null\n const date = new Date(value)\n return Number.isNaN(date.getTime()) ? null : date\n}\n\nfunction generateConversationId(): string {\n // Prefer the runtime crypto generator when present; fall back to a non-cryptographic\n // string for environments without `crypto.randomUUID()` (older Node / test mocks).\n const cryptoMod: { randomUUID?: () => string } | undefined =\n typeof globalThis === 'object' ? (globalThis as any).crypto : undefined\n if (cryptoMod?.randomUUID) return cryptoMod.randomUUID()\n return `chat_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 12)}`\n}\n\nexport default AiChatConversationRepository\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,72 @@
1
+ import { z } from "zod";
2
+ const aiChatPageContextSchema = z.object({
3
+ pageId: z.string().min(1).max(256).optional(),
4
+ entityType: z.string().min(1).max(256).optional(),
5
+ recordId: z.string().min(1).max(256).optional()
6
+ }).passthrough();
7
+ const aiAgentIdSchema = z.string().trim().min(1, "agentId must be a non-empty string").max(256, "agentId exceeds the maximum length of 256 characters");
8
+ const conversationIdSchema = z.string().trim().min(1, "conversationId must be a non-empty string").max(128, "conversationId exceeds the maximum length of 128 characters");
9
+ const titleSchema = z.string().trim().min(1).max(200);
10
+ const aiChatConversationCreateSchema = z.object({
11
+ agentId: aiAgentIdSchema,
12
+ conversationId: conversationIdSchema.optional(),
13
+ title: titleSchema.optional(),
14
+ pageContext: aiChatPageContextSchema.nullable().optional()
15
+ });
16
+ const aiChatConversationListQuerySchema = z.object({
17
+ agent: aiAgentIdSchema.optional(),
18
+ status: z.enum(["open", "closed"]).optional(),
19
+ limit: z.coerce.number().int().min(1).max(100).optional(),
20
+ cursor: z.string().trim().min(1).max(200).optional()
21
+ });
22
+ const aiChatConversationUpdateSchema = z.object({
23
+ title: titleSchema.nullable().optional(),
24
+ status: z.enum(["open", "closed"]).optional(),
25
+ pageContext: aiChatPageContextSchema.nullable().optional()
26
+ }).refine(
27
+ (value) => typeof value.title !== "undefined" || typeof value.status !== "undefined" || typeof value.pageContext !== "undefined",
28
+ { message: "At least one of title, status, or pageContext is required." }
29
+ );
30
+ const messageRoleSchema = z.enum(["user", "assistant", "system"]);
31
+ const messageBaseSchema = z.object({
32
+ clientMessageId: z.string().trim().min(1).max(128).optional(),
33
+ role: messageRoleSchema,
34
+ content: z.string().max(64e3),
35
+ uiParts: z.array(z.unknown()).max(64).optional(),
36
+ attachmentIds: z.array(z.string().trim().min(1).max(128)).max(32).optional(),
37
+ files: z.array(
38
+ z.object({
39
+ id: z.string().trim().min(1).max(128).optional(),
40
+ name: z.string().trim().min(1).max(256).optional(),
41
+ mimeType: z.string().trim().min(1).max(128).optional(),
42
+ size: z.number().int().nonnegative().optional()
43
+ }).passthrough()
44
+ ).max(32).optional(),
45
+ model: z.string().trim().min(1).max(128).optional(),
46
+ metadata: z.record(z.string(), z.unknown()).optional()
47
+ });
48
+ const aiChatConversationImportSchema = z.object({
49
+ conversation: z.object({
50
+ conversationId: conversationIdSchema,
51
+ agentId: aiAgentIdSchema,
52
+ title: titleSchema.optional(),
53
+ status: z.enum(["open", "closed"]).optional(),
54
+ pageContext: aiChatPageContextSchema.nullable().optional()
55
+ }),
56
+ messages: z.array(messageBaseSchema).max(100)
57
+ });
58
+ const aiChatConversationTranscriptQuerySchema = z.object({
59
+ limit: z.coerce.number().int().min(1).max(200).optional(),
60
+ before: z.string().trim().min(1).max(200).optional()
61
+ });
62
+ const aiChatMessageAppendSchema = messageBaseSchema;
63
+ export {
64
+ aiChatConversationCreateSchema,
65
+ aiChatConversationImportSchema,
66
+ aiChatConversationListQuerySchema,
67
+ aiChatConversationTranscriptQuerySchema,
68
+ aiChatConversationUpdateSchema,
69
+ aiChatMessageAppendSchema,
70
+ aiChatPageContextSchema
71
+ };
72
+ //# sourceMappingURL=validators.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/ai_assistant/data/validators.ts"],
4
+ "sourcesContent": ["import { z } from 'zod'\n\n/**\n * Zod validators for the server-side AI chat conversation APIs.\n *\n * Spec: `2026-05-05-ai-chat-server-side-conversation-storage`. Used by the\n * REST routes under `api/ai/conversations/...` and by the repository to keep\n * input shapes aligned with the database constraints declared in\n * `data/entities.ts`.\n */\n\n/** Page-context snapshot persisted on a conversation row. */\nexport const aiChatPageContextSchema = z\n .object({\n pageId: z.string().min(1).max(256).optional(),\n entityType: z.string().min(1).max(256).optional(),\n recordId: z.string().min(1).max(256).optional(),\n })\n .passthrough()\n\nexport type AiChatPageContextInput = z.infer<typeof aiChatPageContextSchema>\n\nconst aiAgentIdSchema = z\n .string()\n .trim()\n .min(1, 'agentId must be a non-empty string')\n .max(256, 'agentId exceeds the maximum length of 256 characters')\n\nconst conversationIdSchema = z\n .string()\n .trim()\n .min(1, 'conversationId must be a non-empty string')\n .max(128, 'conversationId exceeds the maximum length of 128 characters')\n\nconst titleSchema = z.string().trim().min(1).max(200)\n\n/** `POST /api/ai_assistant/ai/conversations` */\nexport const aiChatConversationCreateSchema = z.object({\n agentId: aiAgentIdSchema,\n conversationId: conversationIdSchema.optional(),\n title: titleSchema.optional(),\n pageContext: aiChatPageContextSchema.nullable().optional(),\n})\n\nexport type AiChatConversationCreateInput = z.infer<typeof aiChatConversationCreateSchema>\n\n/** `GET /api/ai_assistant/ai/conversations?agent=&status=&limit=&cursor=` */\nexport const aiChatConversationListQuerySchema = z.object({\n agent: aiAgentIdSchema.optional(),\n status: z.enum(['open', 'closed']).optional(),\n limit: z.coerce.number().int().min(1).max(100).optional(),\n cursor: z.string().trim().min(1).max(200).optional(),\n})\n\nexport type AiChatConversationListQuery = z.infer<typeof aiChatConversationListQuerySchema>\n\n/** `PATCH /api/ai_assistant/ai/conversations/:conversationId` */\nexport const aiChatConversationUpdateSchema = z\n .object({\n title: titleSchema.nullable().optional(),\n status: z.enum(['open', 'closed']).optional(),\n pageContext: aiChatPageContextSchema.nullable().optional(),\n })\n .refine(\n (value) =>\n typeof value.title !== 'undefined' ||\n typeof value.status !== 'undefined' ||\n typeof value.pageContext !== 'undefined',\n { message: 'At least one of title, status, or pageContext is required.' },\n )\n\nexport type AiChatConversationUpdateInput = z.infer<typeof aiChatConversationUpdateSchema>\n\nconst messageRoleSchema = z.enum(['user', 'assistant', 'system'])\n\n/**\n * Shared message-body shape. `clientMessageId` is the idempotency key for\n * retries and lazy imports; `content` is capped to keep transcript rows\n * bounded; `uiParts` / `attachmentIds` / `files` accept the serializable\n * subset the chat UI already produces. Attachment previews (`data:` URLs\n * and transient blob URLs) MUST NOT pass through here \u2014 the UI strips them\n * before upload.\n */\nconst messageBaseSchema = z.object({\n clientMessageId: z.string().trim().min(1).max(128).optional(),\n role: messageRoleSchema,\n content: z.string().max(64_000),\n uiParts: z.array(z.unknown()).max(64).optional(),\n attachmentIds: z.array(z.string().trim().min(1).max(128)).max(32).optional(),\n files: z\n .array(\n z\n .object({\n id: z.string().trim().min(1).max(128).optional(),\n name: z.string().trim().min(1).max(256).optional(),\n mimeType: z.string().trim().min(1).max(128).optional(),\n size: z.number().int().nonnegative().optional(),\n })\n .passthrough(),\n )\n .max(32)\n .optional(),\n model: z.string().trim().min(1).max(128).optional(),\n metadata: z.record(z.string(), z.unknown()).optional(),\n})\n\n/** `POST /api/ai_assistant/ai/conversations/import` */\nexport const aiChatConversationImportSchema = z.object({\n conversation: z.object({\n conversationId: conversationIdSchema,\n agentId: aiAgentIdSchema,\n title: titleSchema.optional(),\n status: z.enum(['open', 'closed']).optional(),\n pageContext: aiChatPageContextSchema.nullable().optional(),\n }),\n messages: z.array(messageBaseSchema).max(100),\n})\n\nexport type AiChatConversationImportInput = z.infer<typeof aiChatConversationImportSchema>\n\n/** Transcript GET query: `?limit=&before=` */\nexport const aiChatConversationTranscriptQuerySchema = z.object({\n limit: z.coerce.number().int().min(1).max(200).optional(),\n before: z.string().trim().min(1).max(200).optional(),\n})\n\nexport type AiChatConversationTranscriptQuery = z.infer<\n typeof aiChatConversationTranscriptQuerySchema\n>\n\n/** Internal payload accepted by the repository when appending a single message. */\nexport const aiChatMessageAppendSchema = messageBaseSchema\n\nexport type AiChatMessageAppendInput = z.infer<typeof aiChatMessageAppendSchema>\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -176,6 +176,7 @@
176
176
  "ai_assistant.chat.dock.close": "KI-Dock schließen",
177
177
  "ai_assistant.chat.dock.collapse": "KI-Dock einklappen",
178
178
  "ai_assistant.chat.dock.expand": "KI-Dock erweitern",
179
+ "ai_assistant.chat.downloadFile": "{name} herunterladen",
179
180
  "ai_assistant.chat.emptyTranscript": "Noch keine Nachrichten. Stellen Sie dem Agenten eine Frage, um zu beginnen.",
180
181
  "ai_assistant.chat.errorTitle": "Agenten-Anfrage fehlgeschlagen",
181
182
  "ai_assistant.chat.example.create": "Ein neues Produkt erstellen",
@@ -183,6 +184,7 @@
183
184
  "ai_assistant.chat.example.show": "Aktuelle Bestellungen anzeigen",
184
185
  "ai_assistant.chat.idleExamples": "Beispiele:",
185
186
  "ai_assistant.chat.idleTitle": "Stellen Sie eine Frage oder beschreiben Sie, was Sie tun möchten.",
187
+ "ai_assistant.chat.imagePreviewDialogDescription": "Bildvorschau",
186
188
  "ai_assistant.chat.mutation_cards.confirmation.cancel": "Abbrechen",
187
189
  "ai_assistant.chat.mutation_cards.confirmation.defaultSummary": "Die angeforderten Änderungen werden ausgeführt...",
188
190
  "ai_assistant.chat.mutation_cards.confirmation.errorTitle": "Bestätigung fehlgeschlagen",
@@ -273,6 +275,7 @@
273
275
  "ai_assistant.chat.uiPartPending": "Ausstehendes UI-Element:",
274
276
  "ai_assistant.chat.userRoleLabel": "Sie",
275
277
  "ai_assistant.chat.welcomeTitle": "How can I help?",
278
+ "ai_assistant.chat.zoomImage": "Vorschau von {name} öffnen",
276
279
  "ai_assistant.dock.bottom": "Unten andocken",
277
280
  "ai_assistant.dock.close": "Schließen",
278
281
  "ai_assistant.dock.floating": "Schwebend",
@@ -176,6 +176,7 @@
176
176
  "ai_assistant.chat.dock.close": "Close AI dock",
177
177
  "ai_assistant.chat.dock.collapse": "Collapse AI dock",
178
178
  "ai_assistant.chat.dock.expand": "Expand AI dock",
179
+ "ai_assistant.chat.downloadFile": "Download {name}",
179
180
  "ai_assistant.chat.emptyTranscript": "No messages yet. Ask the agent anything to get started.",
180
181
  "ai_assistant.chat.errorTitle": "Agent dispatch failed",
181
182
  "ai_assistant.chat.example.create": "Create a new product",
@@ -183,6 +184,7 @@
183
184
  "ai_assistant.chat.example.show": "Show me recent orders",
184
185
  "ai_assistant.chat.idleExamples": "Examples:",
185
186
  "ai_assistant.chat.idleTitle": "Ask me anything or describe what you want to do.",
187
+ "ai_assistant.chat.imagePreviewDialogDescription": "Image preview",
186
188
  "ai_assistant.chat.mutation_cards.confirmation.cancel": "Cancel",
187
189
  "ai_assistant.chat.mutation_cards.confirmation.defaultSummary": "Applying the requested changes...",
188
190
  "ai_assistant.chat.mutation_cards.confirmation.errorTitle": "Confirm failed",
@@ -273,6 +275,7 @@
273
275
  "ai_assistant.chat.uiPartPending": "Pending UI part:",
274
276
  "ai_assistant.chat.userRoleLabel": "You",
275
277
  "ai_assistant.chat.welcomeTitle": "How can I help?",
278
+ "ai_assistant.chat.zoomImage": "Open {name} preview",
276
279
  "ai_assistant.dock.bottom": "Dock Bottom",
277
280
  "ai_assistant.dock.close": "Close",
278
281
  "ai_assistant.dock.floating": "Floating",