@consilioweb/payload-support 0.6.5 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/components/TicketConversation/hooks/useAI.cjs +2 -2
- package/dist/components/TicketConversation/hooks/useAI.js +2 -2
- package/dist/components/TicketConversation/index.cjs +97 -4
- package/dist/components/TicketConversation/index.js +97 -4
- package/dist/index.cjs +387 -11
- package/dist/index.js +387 -11
- package/package.json +1 -1
- package/src/collections/ClientSummaries.ts +118 -0
- package/src/collections/Tickets.ts +54 -8
- package/src/collections/index.ts +1 -0
- package/src/components/TicketConversation/hooks/useAI.ts +2 -2
- package/src/components/TicketConversation/index.tsx +96 -6
- package/src/endpoints/ai.ts +12 -2
- package/src/endpoints/client-intelligence.ts +257 -0
- package/src/endpoints/index.ts +5 -1
- package/src/plugin.ts +2 -0
|
@@ -87,7 +87,7 @@ export function useAI(
|
|
|
87
87
|
setAiReplying(false)
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
const handleAiRewrite = async () => {
|
|
90
|
+
const handleAiRewrite = async (style: string = 'auto') => {
|
|
91
91
|
if (!replyBody.trim()) return
|
|
92
92
|
setAiRewriting(true)
|
|
93
93
|
try {
|
|
@@ -95,7 +95,7 @@ export function useAI(
|
|
|
95
95
|
method: 'POST',
|
|
96
96
|
headers: { 'Content-Type': 'application/json' },
|
|
97
97
|
credentials: 'include',
|
|
98
|
-
body: JSON.stringify({ action: 'rewrite', text: replyBody }),
|
|
98
|
+
body: JSON.stringify({ action: 'rewrite', text: replyBody, style }),
|
|
99
99
|
})
|
|
100
100
|
if (res.ok) {
|
|
101
101
|
const data = await res.json()
|
|
@@ -82,6 +82,98 @@ const layoutStyles = {
|
|
|
82
82
|
} as React.CSSProperties,
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
// ── Rewrite style dropdown ──────────────────────────────────────────────
|
|
86
|
+
const REWRITE_STYLES = [
|
|
87
|
+
{ id: 'auto', label: '✏️ Auto', desc: 'Garde le ton actuel' },
|
|
88
|
+
{ id: 'tutoyer', label: '👋 Tutoyer', desc: 'Passe en tu' },
|
|
89
|
+
{ id: 'vouvoyer', label: '🎩 Vouvoyer', desc: 'Passe en vous' },
|
|
90
|
+
{ id: 'formel', label: '💼 Formel', desc: 'Ton professionnel' },
|
|
91
|
+
{ id: 'amical', label: '😊 Amical', desc: 'Ton chaleureux' },
|
|
92
|
+
{ id: 'court', label: '⚡ Court', desc: 'Version concise' },
|
|
93
|
+
]
|
|
94
|
+
|
|
95
|
+
const RewriteDropdown: React.FC<{
|
|
96
|
+
disabled: boolean
|
|
97
|
+
loading: boolean
|
|
98
|
+
onSelect: (style: string) => void
|
|
99
|
+
}> = ({ disabled, loading, onSelect }) => {
|
|
100
|
+
const [open, setOpen] = useState(false)
|
|
101
|
+
const ref = React.useRef<HTMLDivElement>(null)
|
|
102
|
+
|
|
103
|
+
React.useEffect(() => {
|
|
104
|
+
if (!open) return
|
|
105
|
+
const close = (e: MouseEvent) => {
|
|
106
|
+
if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false)
|
|
107
|
+
}
|
|
108
|
+
document.addEventListener('mousedown', close)
|
|
109
|
+
return () => document.removeEventListener('mousedown', close)
|
|
110
|
+
}, [open])
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
<div ref={ref} style={{ position: 'relative', display: 'inline-block' }}>
|
|
114
|
+
<button
|
|
115
|
+
type="button"
|
|
116
|
+
onClick={() => setOpen(!open)}
|
|
117
|
+
disabled={disabled}
|
|
118
|
+
style={{
|
|
119
|
+
...s.outlineBtn('#0891b2', disabled),
|
|
120
|
+
fontSize: '11px',
|
|
121
|
+
padding: '3px 10px',
|
|
122
|
+
borderRadius: '14px',
|
|
123
|
+
display: 'flex',
|
|
124
|
+
alignItems: 'center',
|
|
125
|
+
gap: '4px',
|
|
126
|
+
}}
|
|
127
|
+
>
|
|
128
|
+
{loading ? 'Reformulation...' : '✏️ Reformuler'}
|
|
129
|
+
{!loading && <span style={{ fontSize: 9, opacity: 0.7 }}>▼</span>}
|
|
130
|
+
</button>
|
|
131
|
+
{open && !disabled && !loading && (
|
|
132
|
+
<div
|
|
133
|
+
style={{
|
|
134
|
+
position: 'absolute',
|
|
135
|
+
bottom: '100%',
|
|
136
|
+
left: 0,
|
|
137
|
+
marginBottom: 4,
|
|
138
|
+
background: 'var(--theme-elevation-0, #fff)',
|
|
139
|
+
border: '1px solid var(--theme-elevation-200, #e5e7eb)',
|
|
140
|
+
borderRadius: 8,
|
|
141
|
+
boxShadow: '0 4px 16px rgba(0,0,0,0.12)',
|
|
142
|
+
zIndex: 100,
|
|
143
|
+
minWidth: 180,
|
|
144
|
+
overflow: 'hidden',
|
|
145
|
+
}}
|
|
146
|
+
>
|
|
147
|
+
{REWRITE_STYLES.map((style) => (
|
|
148
|
+
<button
|
|
149
|
+
key={style.id}
|
|
150
|
+
type="button"
|
|
151
|
+
onClick={() => { setOpen(false); onSelect(style.id) }}
|
|
152
|
+
style={{
|
|
153
|
+
display: 'flex',
|
|
154
|
+
flexDirection: 'column',
|
|
155
|
+
width: '100%',
|
|
156
|
+
padding: '8px 12px',
|
|
157
|
+
border: 'none',
|
|
158
|
+
background: 'transparent',
|
|
159
|
+
cursor: 'pointer',
|
|
160
|
+
textAlign: 'left',
|
|
161
|
+
borderBottom: '1px solid var(--theme-elevation-100, #f3f4f6)',
|
|
162
|
+
transition: 'background 120ms',
|
|
163
|
+
}}
|
|
164
|
+
onMouseEnter={(e) => { (e.target as HTMLElement).style.background = 'var(--theme-elevation-50, #f9fafb)' }}
|
|
165
|
+
onMouseLeave={(e) => { (e.target as HTMLElement).style.background = 'transparent' }}
|
|
166
|
+
>
|
|
167
|
+
<span style={{ fontSize: 12, fontWeight: 600, color: 'var(--theme-text, #111)' }}>{style.label}</span>
|
|
168
|
+
<span style={{ fontSize: 10, color: 'var(--theme-elevation-500, #6b7280)' }}>{style.desc}</span>
|
|
169
|
+
</button>
|
|
170
|
+
))}
|
|
171
|
+
</div>
|
|
172
|
+
)}
|
|
173
|
+
</div>
|
|
174
|
+
)
|
|
175
|
+
}
|
|
176
|
+
|
|
85
177
|
const TicketConversation: React.FC = () => {
|
|
86
178
|
const { id } = useDocumentIdFromUrl()
|
|
87
179
|
const [features] = useState<TicketingFeatures>(() => getFeatures())
|
|
@@ -836,13 +928,11 @@ const TicketConversation: React.FC = () => {
|
|
|
836
928
|
</button>
|
|
837
929
|
)}
|
|
838
930
|
{features.ai && (
|
|
839
|
-
<
|
|
840
|
-
onClick={handleAiRewrite}
|
|
931
|
+
<RewriteDropdown
|
|
841
932
|
disabled={aiRewriting || !replyBody.trim()}
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
</button>
|
|
933
|
+
loading={aiRewriting}
|
|
934
|
+
onSelect={(style) => handleAiRewrite(style)}
|
|
935
|
+
/>
|
|
846
936
|
)}
|
|
847
937
|
<CodeBlockInserter
|
|
848
938
|
style={{ ...s.outlineBtn('#059669', false), fontSize: '11px', padding: '3px 10px', borderRadius: '14px' }}
|
package/src/endpoints/ai.ts
CHANGED
|
@@ -169,10 +169,20 @@ Rédige une réponse appropriée au dernier message du client. Sois concis (3-5
|
|
|
169
169
|
if (!aiSettings.enableRewrite) {
|
|
170
170
|
return Response.json({ rewritten: '', disabled: true })
|
|
171
171
|
}
|
|
172
|
-
const { text } = body as { text: string }
|
|
172
|
+
const { text, style } = body as { text: string; style?: string }
|
|
173
173
|
if (!text?.trim()) return Response.json({ error: 'text required' }, { status: 400 })
|
|
174
174
|
|
|
175
|
-
const
|
|
175
|
+
const styleInstructions: Record<string, string> = {
|
|
176
|
+
auto: 'Garde le même ton (tutoiement/vouvoiement).',
|
|
177
|
+
tutoyer: 'Utilise le tutoiement. Si le texte vouvoie, convertis en tutoiement.',
|
|
178
|
+
vouvoyer: 'Utilise le vouvoiement. Si le texte tutoie, convertis en vouvoiement.',
|
|
179
|
+
formel: 'Adopte un ton formel et professionnel avec vouvoiement.',
|
|
180
|
+
court: 'Raccourcis le texte au maximum tout en gardant le sens. Sois concis et direct.',
|
|
181
|
+
amical: 'Adopte un ton chaleureux et amical avec tutoiement.',
|
|
182
|
+
}
|
|
183
|
+
const styleGuide = styleInstructions[style || 'auto'] || styleInstructions.auto
|
|
184
|
+
|
|
185
|
+
const prompt = `Tu es un agent de support technique professionnel. Reformule le texte ci-dessous de manière plus professionnelle et corrige les fautes d'orthographe/grammaire. ${styleGuide} Ne change pas le fond du message, améliore uniquement la forme. Réponds UNIQUEMENT avec le texte reformulé, sans commentaire ni explication.
|
|
176
186
|
|
|
177
187
|
Texte original :
|
|
178
188
|
${text}`
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import type { Endpoint } from 'payload'
|
|
2
|
+
import type { CollectionSlugs } from '../utils/slugs'
|
|
3
|
+
import { requireAdmin, handleAuthError } from '../utils/auth'
|
|
4
|
+
import { readSupportSettings, type SupportSettings } from '../utils/readSettings'
|
|
5
|
+
|
|
6
|
+
function getClient(aiSettings: SupportSettings['ai']) {
|
|
7
|
+
const Anthropic = require('@anthropic-ai/sdk').default
|
|
8
|
+
if (aiSettings.provider === 'ollama') {
|
|
9
|
+
const baseURL = process.env.OLLAMA_API_URL || 'https://ollama.orkelis.app/v1'
|
|
10
|
+
return new Anthropic({ apiKey: 'ollama', baseURL })
|
|
11
|
+
}
|
|
12
|
+
return new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY })
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function getModel(aiSettings: SupportSettings['ai']): string {
|
|
16
|
+
return aiSettings.model || 'claude-haiku-4-5-20251001'
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const CACHE_TTL_MS = 24 * 60 * 60 * 1000 // 24 hours
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* GET /api/support/client-intelligence?clientId=X
|
|
23
|
+
* Returns cached summary or generates a new one.
|
|
24
|
+
*
|
|
25
|
+
* POST /api/support/client-intelligence?clientId=X
|
|
26
|
+
* Force-refreshes the summary.
|
|
27
|
+
*/
|
|
28
|
+
export function createClientIntelligenceEndpoint(slugs: CollectionSlugs): Endpoint[] {
|
|
29
|
+
const getHandler = async (req: any) => {
|
|
30
|
+
try {
|
|
31
|
+
requireAdmin(req, slugs)
|
|
32
|
+
const payload = req.payload
|
|
33
|
+
const url = new URL(req.url || '', 'http://localhost')
|
|
34
|
+
const clientId = url.searchParams.get('clientId')
|
|
35
|
+
if (!clientId) return Response.json({ error: 'clientId required' }, { status: 400 })
|
|
36
|
+
|
|
37
|
+
// Check cache
|
|
38
|
+
const existing = await payload.find({
|
|
39
|
+
collection: 'client-summaries',
|
|
40
|
+
where: { client: { equals: Number(clientId) } },
|
|
41
|
+
limit: 1,
|
|
42
|
+
depth: 0,
|
|
43
|
+
overrideAccess: true,
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
if (existing.docs.length > 0) {
|
|
47
|
+
const cached = existing.docs[0]
|
|
48
|
+
const age = Date.now() - new Date(cached.generatedAt || 0).getTime()
|
|
49
|
+
if (age < CACHE_TTL_MS) {
|
|
50
|
+
return Response.json({ ...cached, fromCache: true })
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Generate new summary
|
|
55
|
+
return await generateSummary(payload, clientId, slugs, existing.docs[0]?.id)
|
|
56
|
+
} catch (error) {
|
|
57
|
+
const authResponse = handleAuthError(error)
|
|
58
|
+
if (authResponse) return authResponse
|
|
59
|
+
console.error('[client-intelligence] Error:', error)
|
|
60
|
+
return Response.json({ error: 'Internal server error' }, { status: 500 })
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const postHandler = async (req: any) => {
|
|
65
|
+
try {
|
|
66
|
+
requireAdmin(req, slugs)
|
|
67
|
+
const payload = req.payload
|
|
68
|
+
const body = await req.json?.() || {}
|
|
69
|
+
const clientId = body.clientId
|
|
70
|
+
if (!clientId) return Response.json({ error: 'clientId required' }, { status: 400 })
|
|
71
|
+
|
|
72
|
+
// Find existing to update
|
|
73
|
+
const existing = await payload.find({
|
|
74
|
+
collection: 'client-summaries',
|
|
75
|
+
where: { client: { equals: Number(clientId) } },
|
|
76
|
+
limit: 1,
|
|
77
|
+
depth: 0,
|
|
78
|
+
overrideAccess: true,
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
return await generateSummary(payload, clientId, slugs, existing.docs[0]?.id)
|
|
82
|
+
} catch (error) {
|
|
83
|
+
const authResponse = handleAuthError(error)
|
|
84
|
+
if (authResponse) return authResponse
|
|
85
|
+
console.error('[client-intelligence] Refresh error:', error)
|
|
86
|
+
return Response.json({ error: 'Internal server error' }, { status: 500 })
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return [
|
|
91
|
+
{ path: '/support/client-intelligence', method: 'get', handler: getHandler },
|
|
92
|
+
{ path: '/support/client-intelligence', method: 'post', handler: postHandler },
|
|
93
|
+
]
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function generateSummary(
|
|
97
|
+
payload: any,
|
|
98
|
+
clientId: string,
|
|
99
|
+
slugs: CollectionSlugs,
|
|
100
|
+
existingId?: number,
|
|
101
|
+
) {
|
|
102
|
+
const aiSettings = (await readSupportSettings(payload)).ai
|
|
103
|
+
if (!aiSettings.enableSynthesis) {
|
|
104
|
+
return Response.json({ error: 'AI synthesis disabled in settings' }, { status: 400 })
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// 1. Fetch client info
|
|
108
|
+
const client = await payload.findByID({
|
|
109
|
+
collection: slugs.supportClients,
|
|
110
|
+
id: Number(clientId),
|
|
111
|
+
depth: 0,
|
|
112
|
+
overrideAccess: true,
|
|
113
|
+
})
|
|
114
|
+
if (!client) return Response.json({ error: 'Client not found' }, { status: 404 })
|
|
115
|
+
|
|
116
|
+
const clientName = [client.firstName, client.lastName].filter(Boolean).join(' ') || client.company || client.email
|
|
117
|
+
|
|
118
|
+
// 2. Fetch all tickets for this client
|
|
119
|
+
const tickets = await payload.find({
|
|
120
|
+
collection: slugs.tickets,
|
|
121
|
+
where: { client: { equals: Number(clientId) } },
|
|
122
|
+
sort: '-createdAt',
|
|
123
|
+
limit: 50,
|
|
124
|
+
depth: 0,
|
|
125
|
+
overrideAccess: true,
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
if (tickets.totalDocs === 0) {
|
|
129
|
+
return Response.json({
|
|
130
|
+
summary: 'Aucun ticket pour ce client.',
|
|
131
|
+
recurringTopics: [],
|
|
132
|
+
patterns: [],
|
|
133
|
+
keyFacts: [],
|
|
134
|
+
ticketCount: 0,
|
|
135
|
+
messageCount: 0,
|
|
136
|
+
})
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// 3. Fetch messages for recent tickets (last 20)
|
|
140
|
+
const ticketIds = tickets.docs.slice(0, 20).map((t: any) => t.id)
|
|
141
|
+
const messages = await payload.find({
|
|
142
|
+
collection: slugs.ticketMessages,
|
|
143
|
+
where: { ticket: { in: ticketIds.join(',') } },
|
|
144
|
+
sort: 'createdAt',
|
|
145
|
+
limit: 200,
|
|
146
|
+
depth: 0,
|
|
147
|
+
overrideAccess: true,
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
// 4. Fetch satisfaction surveys
|
|
151
|
+
let avgSatisfaction: number | null = null
|
|
152
|
+
try {
|
|
153
|
+
const surveys = await payload.find({
|
|
154
|
+
collection: slugs.satisfactionSurveys || 'satisfaction-surveys',
|
|
155
|
+
where: { client: { equals: Number(clientId) } },
|
|
156
|
+
limit: 50,
|
|
157
|
+
depth: 0,
|
|
158
|
+
overrideAccess: true,
|
|
159
|
+
})
|
|
160
|
+
if (surveys.totalDocs > 0) {
|
|
161
|
+
const ratings = surveys.docs.filter((s: any) => s.rating).map((s: any) => s.rating)
|
|
162
|
+
if (ratings.length > 0) avgSatisfaction = Math.round((ratings.reduce((a: number, b: number) => a + b, 0) / ratings.length) * 10) / 10
|
|
163
|
+
}
|
|
164
|
+
} catch { /* satisfaction collection might not exist */ }
|
|
165
|
+
|
|
166
|
+
// 5. Build context for AI
|
|
167
|
+
const ticketSummaries = tickets.docs.map((t: any) => {
|
|
168
|
+
const msgs = messages.docs.filter((m: any) => {
|
|
169
|
+
const mTicket = typeof m.ticket === 'object' ? m.ticket.id : m.ticket
|
|
170
|
+
return mTicket === t.id
|
|
171
|
+
})
|
|
172
|
+
const clientMsgs = msgs.filter((m: any) => m.authorType === 'client' || m.authorType === 'email')
|
|
173
|
+
const adminMsgs = msgs.filter((m: any) => m.authorType === 'admin')
|
|
174
|
+
return `Ticket ${t.ticketNumber} (${t.status}) — "${t.subject}"
|
|
175
|
+
Client: ${clientMsgs.map((m: any) => m.body?.slice(0, 200)).join(' | ')}
|
|
176
|
+
Admin: ${adminMsgs.map((m: any) => m.body?.slice(0, 200)).join(' | ')}`
|
|
177
|
+
}).join('\n\n')
|
|
178
|
+
|
|
179
|
+
const prompt = `Tu es un assistant d'analyse CRM pour un support technique. Analyse l'historique complet de ce client et génère un rapport structuré.
|
|
180
|
+
|
|
181
|
+
CLIENT : ${clientName} (${client.company || 'pas de société'})
|
|
182
|
+
Email : ${client.email}
|
|
183
|
+
Nombre de tickets : ${tickets.totalDocs}
|
|
184
|
+
Satisfaction moyenne : ${avgSatisfaction ?? 'non évaluée'}
|
|
185
|
+
|
|
186
|
+
HISTORIQUE DES TICKETS :
|
|
187
|
+
${ticketSummaries.slice(0, 4000)}
|
|
188
|
+
|
|
189
|
+
Réponds en JSON strict (pas de markdown, pas de commentaires) avec cette structure :
|
|
190
|
+
{
|
|
191
|
+
"summary": "Résumé global du client en 2-3 phrases (qui il est, ce qu'il demande habituellement, son niveau de satisfaction)",
|
|
192
|
+
"recurringTopics": [{"topic": "nom du sujet", "count": N, "lastSeen": "YYYY-MM-DD"}],
|
|
193
|
+
"patterns": ["pattern 1 observé", "pattern 2 observé"],
|
|
194
|
+
"keyFacts": ["fait clé 1 sur le client", "fait clé 2"]
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
Sois factuel. Ne dépasse pas 5 items par tableau. Réponds UNIQUEMENT avec le JSON.`
|
|
198
|
+
|
|
199
|
+
// 6. Call AI
|
|
200
|
+
const anthropic = getClient(aiSettings)
|
|
201
|
+
const model = getModel(aiSettings)
|
|
202
|
+
|
|
203
|
+
const res = await anthropic.messages.create({
|
|
204
|
+
model,
|
|
205
|
+
max_tokens: 1000,
|
|
206
|
+
messages: [{ role: 'user', content: prompt }],
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
const rawText = res.content[0].type === 'text' ? res.content[0].text : '{}'
|
|
210
|
+
|
|
211
|
+
// 7. Parse AI response
|
|
212
|
+
let parsed: any = {}
|
|
213
|
+
try {
|
|
214
|
+
// Extract JSON from potential markdown fences
|
|
215
|
+
const jsonMatch = rawText.match(/\{[\s\S]*\}/)
|
|
216
|
+
if (jsonMatch) parsed = JSON.parse(jsonMatch[0])
|
|
217
|
+
} catch {
|
|
218
|
+
parsed = { summary: rawText, recurringTopics: [], patterns: [], keyFacts: [] }
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// 8. Save to DB
|
|
222
|
+
const data = {
|
|
223
|
+
client: Number(clientId),
|
|
224
|
+
clientName,
|
|
225
|
+
summary: parsed.summary || 'Résumé non disponible',
|
|
226
|
+
recurringTopics: parsed.recurringTopics || [],
|
|
227
|
+
patterns: parsed.patterns || [],
|
|
228
|
+
keyFacts: parsed.keyFacts || [],
|
|
229
|
+
ticketCount: tickets.totalDocs,
|
|
230
|
+
messageCount: messages.totalDocs,
|
|
231
|
+
averageSatisfaction: avgSatisfaction,
|
|
232
|
+
firstTicketAt: tickets.docs[tickets.docs.length - 1]?.createdAt || null,
|
|
233
|
+
lastTicketAt: tickets.docs[0]?.createdAt || null,
|
|
234
|
+
generatedAt: new Date().toISOString(),
|
|
235
|
+
aiModel: model,
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
let saved: any
|
|
239
|
+
if (existingId) {
|
|
240
|
+
saved = await payload.update({
|
|
241
|
+
collection: 'client-summaries',
|
|
242
|
+
id: existingId,
|
|
243
|
+
data,
|
|
244
|
+
overrideAccess: true,
|
|
245
|
+
})
|
|
246
|
+
} else {
|
|
247
|
+
saved = await payload.create({
|
|
248
|
+
collection: 'client-summaries',
|
|
249
|
+
data,
|
|
250
|
+
overrideAccess: true,
|
|
251
|
+
})
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
console.log(`[client-intelligence] Generated summary for ${clientName} (${tickets.totalDocs} tickets, ${messages.totalDocs} messages)`)
|
|
255
|
+
|
|
256
|
+
return Response.json({ ...saved, fromCache: false })
|
|
257
|
+
}
|
package/src/endpoints/index.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { CollectionSlugs } from '../utils/slugs'
|
|
|
3
3
|
import type { SupportFeatures } from '../types'
|
|
4
4
|
|
|
5
5
|
import { createAiEndpoint } from './ai'
|
|
6
|
+
import { createClientIntelligenceEndpoint } from './client-intelligence'
|
|
6
7
|
import { createSearchEndpoint } from './search'
|
|
7
8
|
import { createBulkActionEndpoint } from './bulk-action'
|
|
8
9
|
import { createMergeTicketsEndpoint } from './merge-tickets'
|
|
@@ -117,7 +118,10 @@ export function createSupportEndpoints(slugs: CollectionSlugs, options?: Support
|
|
|
117
118
|
]
|
|
118
119
|
|
|
119
120
|
// Conditional endpoints based on feature flags
|
|
120
|
-
if (!f || f.ai !== false)
|
|
121
|
+
if (!f || f.ai !== false) {
|
|
122
|
+
endpoints.push(createAiEndpoint(slugs))
|
|
123
|
+
endpoints.push(...createClientIntelligenceEndpoint(slugs))
|
|
124
|
+
}
|
|
121
125
|
if (!f || f.bulkActions !== false) endpoints.push(createBulkActionEndpoint(slugs))
|
|
122
126
|
if (!f || f.merge !== false) endpoints.push(createMergeTicketsEndpoint(slugs))
|
|
123
127
|
if (!f || f.splitTicket !== false) endpoints.push(createSplitTicketEndpoint(slugs))
|
package/src/plugin.ts
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
createSlaPoliciesCollection,
|
|
21
21
|
createMacrosCollection,
|
|
22
22
|
createTicketStatusesCollection,
|
|
23
|
+
createClientSummariesCollection,
|
|
23
24
|
} from './collections'
|
|
24
25
|
|
|
25
26
|
function viewConfig(component: string, path: string): AdminViewConfig {
|
|
@@ -99,6 +100,7 @@ export function supportPlugin(config?: SupportPluginConfig): Plugin {
|
|
|
99
100
|
if (features.customStatuses !== false) supportCollections.push(createTicketStatusesCollection(slugs))
|
|
100
101
|
if (features.chat) supportCollections.push(createChatMessagesCollection(slugs))
|
|
101
102
|
if (features.pendingEmails) supportCollections.push(createPendingEmailsCollection(slugs))
|
|
103
|
+
if (features.ai !== false) supportCollections.push(createClientSummariesCollection(slugs))
|
|
102
104
|
|
|
103
105
|
// ─── Admin Views ─────────────────────────────────────
|
|
104
106
|
|