@consilioweb/payload-support 0.9.10 → 0.9.12

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.
@@ -77,22 +77,41 @@ function useAI(messages, client, ticketSubject, replyBody, setReplyBody, setRepl
77
77
  };
78
78
  const handleAiRewrite = async (style = "auto") => {
79
79
  if (!replyBody.trim()) return;
80
+ const sel = typeof window !== "undefined" ? window.getSelection() : null;
81
+ const selectedText = sel?.toString().trim() || "";
82
+ const hasSelection = selectedText.length > 3;
83
+ const textToRewrite = hasSelection ? selectedText : replyBody;
80
84
  setAiRewriting(true);
81
85
  try {
82
86
  const res = await fetch("/api/support/ai", {
83
87
  method: "POST",
84
88
  headers: { "Content-Type": "application/json" },
85
89
  credentials: "include",
86
- body: JSON.stringify({ action: "rewrite", text: replyBody, style })
90
+ body: JSON.stringify({ action: "rewrite", text: textToRewrite, style })
87
91
  });
88
92
  if (res.ok) {
89
93
  const data = await res.json();
90
94
  const rewritten = data.rewritten || "";
91
95
  if (rewritten) {
92
- setReplyBody(rewritten);
93
- setReplyHtml(rewritten.replace(/\n/g, "<br/>"));
94
- if (replyEditorRef.current?.setContent) {
95
- replyEditorRef.current.setContent(rewritten.replace(/\n/g, "<br/>"));
96
+ if (hasSelection && sel && sel.rangeCount > 0) {
97
+ const range = sel.getRangeAt(0);
98
+ range.deleteContents();
99
+ range.insertNode(document.createTextNode(rewritten));
100
+ const editorEl = replyEditorRef.current;
101
+ editorEl?.focus?.();
102
+ const rootEl = range.commonAncestorContainer.closest?.("[contenteditable]");
103
+ if (rootEl) {
104
+ const newHtml = rootEl.innerHTML;
105
+ const newText = rootEl.innerText?.trim() || "";
106
+ setReplyBody(newText);
107
+ setReplyHtml(newHtml);
108
+ }
109
+ } else {
110
+ setReplyBody(rewritten);
111
+ setReplyHtml(rewritten.replace(/\n/g, "<br/>"));
112
+ if (replyEditorRef.current?.setContent) {
113
+ replyEditorRef.current.setContent(rewritten.replace(/\n/g, "<br/>"));
114
+ }
96
115
  }
97
116
  }
98
117
  }
@@ -76,22 +76,41 @@ function useAI(messages, client, ticketSubject, replyBody, setReplyBody, setRepl
76
76
  };
77
77
  const handleAiRewrite = async (style = "auto") => {
78
78
  if (!replyBody.trim()) return;
79
+ const sel = typeof window !== "undefined" ? window.getSelection() : null;
80
+ const selectedText = sel?.toString().trim() || "";
81
+ const hasSelection = selectedText.length > 3;
82
+ const textToRewrite = hasSelection ? selectedText : replyBody;
79
83
  setAiRewriting(true);
80
84
  try {
81
85
  const res = await fetch("/api/support/ai", {
82
86
  method: "POST",
83
87
  headers: { "Content-Type": "application/json" },
84
88
  credentials: "include",
85
- body: JSON.stringify({ action: "rewrite", text: replyBody, style })
89
+ body: JSON.stringify({ action: "rewrite", text: textToRewrite, style })
86
90
  });
87
91
  if (res.ok) {
88
92
  const data = await res.json();
89
93
  const rewritten = data.rewritten || "";
90
94
  if (rewritten) {
91
- setReplyBody(rewritten);
92
- setReplyHtml(rewritten.replace(/\n/g, "<br/>"));
93
- if (replyEditorRef.current?.setContent) {
94
- replyEditorRef.current.setContent(rewritten.replace(/\n/g, "<br/>"));
95
+ if (hasSelection && sel && sel.rangeCount > 0) {
96
+ const range = sel.getRangeAt(0);
97
+ range.deleteContents();
98
+ range.insertNode(document.createTextNode(rewritten));
99
+ const editorEl = replyEditorRef.current;
100
+ editorEl?.focus?.();
101
+ const rootEl = range.commonAncestorContainer.closest?.("[contenteditable]");
102
+ if (rootEl) {
103
+ const newHtml = rootEl.innerHTML;
104
+ const newText = rootEl.innerText?.trim() || "";
105
+ setReplyBody(newText);
106
+ setReplyHtml(newHtml);
107
+ }
108
+ } else {
109
+ setReplyBody(rewritten);
110
+ setReplyHtml(rewritten.replace(/\n/g, "<br/>"));
111
+ if (replyEditorRef.current?.setContent) {
112
+ replyEditorRef.current.setContent(rewritten.replace(/\n/g, "<br/>"));
113
+ }
95
114
  }
96
115
  }
97
116
  }
package/dist/index.cjs CHANGED
@@ -283,15 +283,20 @@ R\xE9dige une r\xE9ponse appropri\xE9e au dernier message du client. Sois concis
283
283
  const { text, style } = body;
284
284
  if (!text?.trim()) return Response.json({ error: "text required" }, { status: 400 });
285
285
  const styleInstructions = {
286
- auto: "Garde le m\xEAme ton (tutoiement/vouvoiement).",
287
- tutoyer: "Utilise le tutoiement. Si le texte vouvoie, convertis en tutoiement.",
288
- vouvoyer: "Utilise le vouvoiement. Si le texte tutoie, convertis en vouvoiement.",
289
- formel: "Adopte un ton formel et professionnel avec vouvoiement.",
290
- court: "Raccourcis le texte au maximum tout en gardant le sens. Sois concis et direct.",
291
- amical: "Adopte un ton chaleureux et amical avec tutoiement."
286
+ auto: { tone: "Garde le ton actuel (neutre professionnel).", person: "IMPORTANT: Pr\xE9serve EXACTEMENT le tutoiement ou vouvoiement du texte original." },
287
+ tutoyer: { tone: "Ton d\xE9contract\xE9 et direct, mais correct.", person: 'CRITIQUE: Utilise IMP\xC9RATIVEMENT le tutoiement partout (tu, ton, te, toi). Si le texte vouvoie, convertis TOUT en tutoiement. Exemple: "vous pouvez" \u2192 "tu peux", "votre" \u2192 "ton".' },
288
+ vouvoyer: { tone: "Ton neutre et poli.", person: "CRITIQUE: Utilise IMP\xC9RATIVEMENT le vouvoiement partout (vous, votre, etc). Si le texte tutoie, convertis TOUT en vouvoiement." },
289
+ formel: { tone: "Ton formel et institutionnel.", person: "Utilise le vouvoiement partout." },
290
+ court: { tone: "Style concis et direct, phrases courtes, aller \xE0 l'essentiel.", person: "Pr\xE9serve le tutoiement/vouvoiement du texte original." },
291
+ amical: { tone: "Ton chaleureux, amical, sympathique, avec un peu de cordialit\xE9.", person: "CRITIQUE: Utilise IMP\xC9RATIVEMENT le tutoiement partout. Convertis le vouvoiement en tutoiement." }
292
292
  };
293
293
  const styleGuide = styleInstructions[style || "auto"] || styleInstructions.auto;
294
- const prompt = `Tu es un agent de support technique professionnel. Reformule le texte ci-dessous de mani\xE8re plus professionnelle et corrige les fautes d'orthographe/grammaire. ${styleGuide} Ne change pas le fond du message, am\xE9liore uniquement la forme. R\xE9ponds UNIQUEMENT avec le texte reformul\xE9, sans commentaire ni explication.
294
+ const prompt = `Reformule le texte ci-dessous en corrigeant les fautes d'orthographe/grammaire. Ne change pas le fond du message, am\xE9liore uniquement la forme.
295
+
296
+ TON REQUIS: ${styleGuide.tone}
297
+ FORME REQUISE: ${styleGuide.person}
298
+
299
+ R\xE9ponds UNIQUEMENT avec le texte reformul\xE9, sans commentaire ni explication.
295
300
 
296
301
  Texte original :
297
302
  ${text}`;
@@ -2970,9 +2975,36 @@ function createBillingEndpoint(slugs) {
2970
2975
  return Response.json({ error: "Missing from/to params" }, { status: 400 });
2971
2976
  }
2972
2977
  const projectId = url.searchParams.get("projectId");
2978
+ const toExclusive = new Date(to);
2979
+ toExclusive.setDate(toExclusive.getDate() + 1);
2980
+ const toExclusiveIso = toExclusive.toISOString();
2973
2981
  const ticketWhere = {
2974
- billable: { equals: true },
2975
- ...projectId ? { project: { equals: Number(projectId) } } : {}
2982
+ and: [
2983
+ { billable: { equals: true } },
2984
+ ...projectId ? [{ project: { equals: Number(projectId) } }] : [],
2985
+ {
2986
+ or: [
2987
+ {
2988
+ and: [
2989
+ { updatedAt: { greater_than_equal: from } },
2990
+ { updatedAt: { less_than: toExclusiveIso } }
2991
+ ]
2992
+ },
2993
+ {
2994
+ and: [
2995
+ { createdAt: { greater_than_equal: from } },
2996
+ { createdAt: { less_than: toExclusiveIso } }
2997
+ ]
2998
+ },
2999
+ {
3000
+ and: [
3001
+ { resolvedAt: { greater_than_equal: from } },
3002
+ { resolvedAt: { less_than: toExclusiveIso } }
3003
+ ]
3004
+ }
3005
+ ]
3006
+ }
3007
+ ]
2976
3008
  };
2977
3009
  const allTickets = [];
2978
3010
  let ticketPage = 1;
@@ -3025,8 +3057,8 @@ function createBillingEndpoint(slugs) {
3025
3057
  const projectGroups = /* @__PURE__ */ new Map();
3026
3058
  for (const ticket of allTickets) {
3027
3059
  const t = ticket;
3028
- const ticketEntries = entriesByTicket.get(t.id);
3029
- if (!ticketEntries || ticketEntries.length === 0) continue;
3060
+ const ticketEntries = entriesByTicket.get(t.id) || [];
3061
+ const hasNoTimeEntries = ticketEntries.length === 0;
3030
3062
  const project = typeof t.project === "object" && t.project ? { id: t.project.id, name: t.project.name || "Sans nom" } : null;
3031
3063
  const projectKey = project ? String(project.id) : "no-project";
3032
3064
  if (!projectGroups.has(projectKey)) {
@@ -3054,9 +3086,14 @@ function createBillingEndpoint(slugs) {
3054
3086
  id: t.id,
3055
3087
  ticketNumber: t.ticketNumber || "",
3056
3088
  subject: t.subject || "",
3089
+ status: t.status || "",
3057
3090
  entries: ticketEntries,
3058
3091
  totalMinutes: ticketTotalMinutes,
3059
- billedAmount
3092
+ billedAmount,
3093
+ hasNoTimeEntries,
3094
+ aiSummary: t.aiSummary || null,
3095
+ aiSummaryGeneratedAt: t.aiSummaryGeneratedAt || null,
3096
+ aiSummaryStatus: t.aiSummaryStatus || null
3060
3097
  });
3061
3098
  projectGroups.get(projectKey).totalMinutes += ticketTotalMinutes;
3062
3099
  if (billedAmount) projectGroups.get(projectKey).totalBilledAmount += billedAmount;
@@ -3064,7 +3101,16 @@ function createBillingEndpoint(slugs) {
3064
3101
  const groups = Array.from(projectGroups.values());
3065
3102
  const grandTotalMinutes = groups.reduce((sum, g) => sum + g.totalMinutes, 0);
3066
3103
  const grandTotalBilledAmount = groups.reduce((sum, g) => sum + g.totalBilledAmount, 0);
3067
- return new Response(JSON.stringify({ groups, grandTotalMinutes, grandTotalBilledAmount }), {
3104
+ const ticketsWithoutTime = groups.reduce(
3105
+ (sum, g) => sum + g.tickets.filter((t) => t.hasNoTimeEntries).length,
3106
+ 0
3107
+ );
3108
+ return new Response(JSON.stringify({
3109
+ groups,
3110
+ grandTotalMinutes,
3111
+ grandTotalBilledAmount,
3112
+ ticketsWithoutTime
3113
+ }), {
3068
3114
  headers: {
3069
3115
  "Content-Type": "application/json",
3070
3116
  "Cache-Control": "private, max-age=300, stale-while-revalidate=600"
@@ -3080,6 +3126,177 @@ function createBillingEndpoint(slugs) {
3080
3126
  };
3081
3127
  }
3082
3128
 
3129
+ // src/utils/generateTicketSynthesis.ts
3130
+ function getClient3(aiSettings) {
3131
+ const Anthropic = __require("@anthropic-ai/sdk").default;
3132
+ if (aiSettings.provider === "ollama") {
3133
+ const baseURL = process.env.OLLAMA_API_URL || "https://ollama.orkelis.app/v1";
3134
+ return new Anthropic({ apiKey: "ollama", baseURL });
3135
+ }
3136
+ return new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
3137
+ }
3138
+ function getModel3(aiSettings) {
3139
+ return aiSettings.model || "claude-haiku-4-5-20251001";
3140
+ }
3141
+ async function generateTicketSynthesis(args) {
3142
+ const { payload, slugs, ticketId } = args;
3143
+ const settings = await readSupportSettings(payload);
3144
+ if (settings.ai.enableSynthesis === false) {
3145
+ return { summary: "", generatedAt: (/* @__PURE__ */ new Date()).toISOString(), status: "skipped", reason: "synthesis disabled" };
3146
+ }
3147
+ const ticket = await payload.findByID({
3148
+ collection: slugs.tickets,
3149
+ id: ticketId,
3150
+ depth: 1,
3151
+ overrideAccess: true
3152
+ });
3153
+ if (!ticket) {
3154
+ return { summary: "", generatedAt: (/* @__PURE__ */ new Date()).toISOString(), status: "error", reason: "ticket not found" };
3155
+ }
3156
+ await payload.update({
3157
+ collection: slugs.tickets,
3158
+ id: ticketId,
3159
+ data: { aiSummaryStatus: "pending" },
3160
+ overrideAccess: true
3161
+ }).catch(() => {
3162
+ });
3163
+ const messagesResult = await payload.find({
3164
+ collection: slugs.ticketMessages,
3165
+ where: { ticket: { equals: ticketId } },
3166
+ sort: "createdAt",
3167
+ limit: 500,
3168
+ depth: 0,
3169
+ overrideAccess: true
3170
+ });
3171
+ const messages = messagesResult.docs;
3172
+ if (messages.length === 0) {
3173
+ const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
3174
+ await payload.update({
3175
+ collection: slugs.tickets,
3176
+ id: ticketId,
3177
+ data: {
3178
+ aiSummary: "(Aucun message dans ce ticket)",
3179
+ aiSummaryGeneratedAt: generatedAt,
3180
+ aiSummaryStatus: "done"
3181
+ },
3182
+ overrideAccess: true
3183
+ });
3184
+ return { summary: "(Aucun message dans ce ticket)", generatedAt, status: "done" };
3185
+ }
3186
+ const conversation = messages.map((m) => {
3187
+ const author = m.authorType === "admin" ? "Support" : "Client";
3188
+ const date = m.createdAt ? new Date(m.createdAt).toLocaleDateString("fr-FR", {
3189
+ day: "numeric",
3190
+ month: "short",
3191
+ hour: "2-digit",
3192
+ minute: "2-digit",
3193
+ timeZone: "Europe/Paris"
3194
+ }) : "";
3195
+ return `[${date}] ${author}: ${m.body || ""}`;
3196
+ }).join("\n\n");
3197
+ const clientObj = typeof ticket.client === "object" && ticket.client ? ticket.client : null;
3198
+ const clientCompany = clientObj?.company || "";
3199
+ const clientName = clientObj ? [clientObj.firstName, clientObj.lastName].filter(Boolean).join(" ") : "";
3200
+ const prompt = `Tu es un consultant technique qui prepare un recap factuel pour une facturation client.
3201
+
3202
+ Sujet du ticket : ${ticket.subject || "(sans sujet)"}
3203
+ Client : ${clientName || "Inconnu"}${clientCompany ? ` \u2014 ${clientCompany}` : ""}
3204
+
3205
+ Conversation complete du ticket :
3206
+ ${conversation}
3207
+
3208
+ Genere un recap factuel sous forme d'une liste a puces courtes et actionnables, decrivant CE QUI A ETE FAIT cote support pendant ce ticket. C'est destine a etre colle dans un devis ou une facture.
3209
+
3210
+ Regles strictes :
3211
+ - Une puce = une action realisee, formulee en groupe nominal court (ex : "Diagnostic configuration DNS et authentification Mailchimp")
3212
+ - Pas de phrases completes, pas de "j'ai fait", pas de pronoms
3213
+ - Pas de salutations, pas d'introduction, pas de conclusion
3214
+ - Pas de markdown autre que les puces "- "
3215
+ - 5 a 10 puces maximum, ordonnees chronologiquement
3216
+ - Ne mentionne PAS le client par son nom dans les puces
3217
+ - Si le ticket n'a pas abouti, decris quand meme le travail d'analyse realise
3218
+
3219
+ Reponds UNIQUEMENT avec la liste de puces, rien d'autre.`;
3220
+ const anthropic = getClient3(settings.ai);
3221
+ const model = getModel3(settings.ai);
3222
+ try {
3223
+ const res = await anthropic.messages.create({
3224
+ model,
3225
+ max_tokens: 600,
3226
+ messages: [{ role: "user", content: prompt }]
3227
+ });
3228
+ const summary = res.content[0]?.type === "text" ? res.content[0].text.trim() : "";
3229
+ const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
3230
+ await payload.update({
3231
+ collection: slugs.tickets,
3232
+ id: ticketId,
3233
+ data: {
3234
+ aiSummary: summary,
3235
+ aiSummaryGeneratedAt: generatedAt,
3236
+ aiSummaryStatus: "done"
3237
+ },
3238
+ overrideAccess: true
3239
+ });
3240
+ return { summary, generatedAt, status: "done" };
3241
+ } catch (err) {
3242
+ const message = err instanceof Error ? err.message : String(err);
3243
+ await payload.update({
3244
+ collection: slugs.tickets,
3245
+ id: ticketId,
3246
+ data: { aiSummaryStatus: "error" },
3247
+ overrideAccess: true
3248
+ }).catch(() => {
3249
+ });
3250
+ return { summary: "", generatedAt: (/* @__PURE__ */ new Date()).toISOString(), status: "error", reason: message };
3251
+ }
3252
+ }
3253
+
3254
+ // src/endpoints/ticket-synthesis.ts
3255
+ function createTicketSynthesisEndpoint(slugs) {
3256
+ return {
3257
+ path: "/support/ticket-synthesis",
3258
+ method: "post",
3259
+ handler: async (req) => {
3260
+ try {
3261
+ requireAdmin(req, slugs);
3262
+ const payload = req.payload;
3263
+ const url = new URL(req.url || "", "http://localhost");
3264
+ const ticketIdRaw = url.searchParams.get("ticketId");
3265
+ const force = url.searchParams.get("force") === "true";
3266
+ if (!ticketIdRaw) {
3267
+ return Response.json({ error: "ticketId required" }, { status: 400 });
3268
+ }
3269
+ const ticketId = Number(ticketIdRaw);
3270
+ if (Number.isNaN(ticketId)) {
3271
+ return Response.json({ error: "ticketId must be a number" }, { status: 400 });
3272
+ }
3273
+ if (!force) {
3274
+ const existing = await payload.findByID({
3275
+ collection: slugs.tickets,
3276
+ id: ticketId,
3277
+ depth: 0,
3278
+ overrideAccess: true
3279
+ });
3280
+ if (existing?.aiSummary && existing.aiSummaryStatus === "done") {
3281
+ return Response.json({
3282
+ summary: existing.aiSummary,
3283
+ generatedAt: existing.aiSummaryGeneratedAt,
3284
+ status: "cached"
3285
+ });
3286
+ }
3287
+ }
3288
+ const result = await generateTicketSynthesis({ payload, slugs, ticketId });
3289
+ return Response.json(result);
3290
+ } catch (err) {
3291
+ const authResponse = handleAuthError(err);
3292
+ if (authResponse) return authResponse;
3293
+ console.error("[support/ticket-synthesis] Error:", err);
3294
+ return Response.json({ error: "Internal server error" }, { status: 500 });
3295
+ }
3296
+ }
3297
+ };
3298
+ }
3299
+
3083
3300
  // src/endpoints/email-stats.ts
3084
3301
  function createEmailStatsEndpoint(slugs) {
3085
3302
  return {
@@ -5018,6 +5235,7 @@ function createSupportEndpoints(slugs, options) {
5018
5235
  if (!f || f.ai !== false) {
5019
5236
  endpoints.push(createAiEndpoint(slugs));
5020
5237
  endpoints.push(...createClientIntelligenceEndpoint(slugs));
5238
+ endpoints.push(createTicketSynthesisEndpoint(slugs));
5021
5239
  }
5022
5240
  if (!f || f.bulkActions !== false) endpoints.push(createBulkActionEndpoint(slugs));
5023
5241
  if (!f || f.merge !== false) endpoints.push(createMergeTicketsEndpoint(slugs));
@@ -5674,6 +5892,32 @@ function createTrackSLA(slugs) {
5674
5892
  return doc;
5675
5893
  };
5676
5894
  }
5895
+ function createTrackAiSummaryOnResolve(slugs) {
5896
+ return async ({ doc, previousDoc, operation, req }) => {
5897
+ if (operation !== "update" || !previousDoc) return doc;
5898
+ const wasResolved = previousDoc.status === "resolved";
5899
+ const isResolved = doc.status === "resolved";
5900
+ if (wasResolved && !isResolved && doc.aiSummary) {
5901
+ try {
5902
+ await req.payload.update({
5903
+ collection: slugs.tickets,
5904
+ id: doc.id,
5905
+ data: { aiSummary: null, aiSummaryGeneratedAt: null, aiSummaryStatus: null },
5906
+ overrideAccess: true
5907
+ });
5908
+ } catch (err) {
5909
+ console.error("[support] Failed to clear ai summary on reopen:", err);
5910
+ }
5911
+ return doc;
5912
+ }
5913
+ if (!wasResolved && isResolved && !doc.aiSummary) {
5914
+ setImmediate(() => {
5915
+ generateTicketSynthesis({ payload: req.payload, slugs, ticketId: doc.id }).catch((err) => console.error("[support] Background ai synthesis failed:", err));
5916
+ });
5917
+ }
5918
+ return doc;
5919
+ };
5920
+ }
5677
5921
  function createLogTicketActivity(slugs) {
5678
5922
  return async ({ doc, previousDoc, operation, req }) => {
5679
5923
  if (operation !== "update" || !previousDoc) return doc;
@@ -6078,6 +6322,49 @@ function createTicketsCollection(slugs, options) {
6078
6322
  admin: { initCollapsed: true },
6079
6323
  fields: billingFields
6080
6324
  },
6325
+ // AI Synthesis collapsible — auto-filled when ticket is resolved
6326
+ {
6327
+ type: "collapsible",
6328
+ label: "Synthese IA",
6329
+ admin: {
6330
+ initCollapsed: true,
6331
+ description: "Recap factuel genere automatiquement au passage en resolu. Sert au copier-coller dans devis/factures."
6332
+ },
6333
+ fields: [
6334
+ {
6335
+ name: "aiSummary",
6336
+ type: "textarea",
6337
+ label: "Synthese",
6338
+ admin: {
6339
+ readOnly: true,
6340
+ rows: 8,
6341
+ description: "Vide tant que le ticket n'est pas resolu. Effacee si le ticket est reouvert."
6342
+ }
6343
+ },
6344
+ {
6345
+ type: "row",
6346
+ fields: [
6347
+ {
6348
+ name: "aiSummaryGeneratedAt",
6349
+ type: "date",
6350
+ label: "Genere le",
6351
+ admin: { readOnly: true, width: "50%", date: { displayFormat: "dd/MM/yyyy HH:mm" } }
6352
+ },
6353
+ {
6354
+ name: "aiSummaryStatus",
6355
+ type: "select",
6356
+ label: "Statut",
6357
+ options: [
6358
+ { label: "En cours", value: "pending" },
6359
+ { label: "Genere", value: "done" },
6360
+ { label: "Erreur", value: "error" }
6361
+ ],
6362
+ admin: { readOnly: true, width: "50%" }
6363
+ }
6364
+ ]
6365
+ }
6366
+ ]
6367
+ },
6081
6368
  // SLA & Delais
6082
6369
  {
6083
6370
  type: "collapsible",
@@ -6202,6 +6489,7 @@ function createTicketsCollection(slugs, options) {
6202
6489
  ],
6203
6490
  afterChange: [
6204
6491
  createTrackSLA(slugs),
6492
+ createTrackAiSummaryOnResolve(slugs),
6205
6493
  createAutoCalculateSLA(slugs),
6206
6494
  createAssignSlaDeadlines(slugs, notificationSlug),
6207
6495
  createCheckSlaOnResolve(slugs, notificationSlug),