@consilioweb/payload-support 0.5.1

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 (189) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +525 -0
  3. package/dist/client.cjs +7 -0
  4. package/dist/client.d.cts +3 -0
  5. package/dist/client.d.ts +3 -0
  6. package/dist/client.js +5 -0
  7. package/dist/index.cjs +7766 -0
  8. package/dist/index.d.cts +384 -0
  9. package/dist/index.d.ts +384 -0
  10. package/dist/index.js +7730 -0
  11. package/dist/views.d.cts +30 -0
  12. package/dist/views.d.ts +30 -0
  13. package/package.json +131 -0
  14. package/src/client.ts +1 -0
  15. package/src/collections/AuthLogs.ts +65 -0
  16. package/src/collections/CannedResponses.ts +69 -0
  17. package/src/collections/ChatMessages.ts +98 -0
  18. package/src/collections/EmailLogs.ts +94 -0
  19. package/src/collections/KnowledgeBase.ts +99 -0
  20. package/src/collections/Macros.ts +98 -0
  21. package/src/collections/PendingEmails.ts +122 -0
  22. package/src/collections/SatisfactionSurveys.ts +98 -0
  23. package/src/collections/SlaPolicies.ts +123 -0
  24. package/src/collections/SupportClients.ts +210 -0
  25. package/src/collections/TicketActivityLog.ts +81 -0
  26. package/src/collections/TicketMessages.ts +364 -0
  27. package/src/collections/TicketStatuses.ts +108 -0
  28. package/src/collections/Tickets.ts +704 -0
  29. package/src/collections/TimeEntries.ts +105 -0
  30. package/src/collections/WebhookEndpoints.ts +96 -0
  31. package/src/collections/index.ts +16 -0
  32. package/src/components/TicketConversation/components/AISummaryPanel.tsx +85 -0
  33. package/src/components/TicketConversation/components/ActionPanels.tsx +140 -0
  34. package/src/components/TicketConversation/components/ActivityLog.tsx +39 -0
  35. package/src/components/TicketConversation/components/ClientBar.tsx +37 -0
  36. package/src/components/TicketConversation/components/ClientHistory.tsx +117 -0
  37. package/src/components/TicketConversation/components/CodeBlock.tsx +186 -0
  38. package/src/components/TicketConversation/components/CodeBlockInserter.tsx +166 -0
  39. package/src/components/TicketConversation/components/QuickActions.tsx +82 -0
  40. package/src/components/TicketConversation/components/TicketHeader.tsx +91 -0
  41. package/src/components/TicketConversation/components/TimeTrackingPanel.tsx +161 -0
  42. package/src/components/TicketConversation/config.ts +82 -0
  43. package/src/components/TicketConversation/constants.ts +74 -0
  44. package/src/components/TicketConversation/context.ts +63 -0
  45. package/src/components/TicketConversation/hooks/useAI.ts +180 -0
  46. package/src/components/TicketConversation/hooks/useMessageActions.ts +131 -0
  47. package/src/components/TicketConversation/hooks/useReply.ts +190 -0
  48. package/src/components/TicketConversation/hooks/useTicketActions.ts +205 -0
  49. package/src/components/TicketConversation/hooks/useTimeTracking.ts +107 -0
  50. package/src/components/TicketConversation/hooks/useTranslation.ts +116 -0
  51. package/src/components/TicketConversation/index.tsx +1110 -0
  52. package/src/components/TicketConversation/locales/en.json +878 -0
  53. package/src/components/TicketConversation/locales/fr.json +878 -0
  54. package/src/components/TicketConversation/types.ts +54 -0
  55. package/src/components/TicketConversation/utils.ts +25 -0
  56. package/src/endpoints/admin-chat-stream.ts +238 -0
  57. package/src/endpoints/admin-chat.ts +263 -0
  58. package/src/endpoints/admin-stats.ts +200 -0
  59. package/src/endpoints/ai.ts +199 -0
  60. package/src/endpoints/apply-macro.ts +144 -0
  61. package/src/endpoints/auth-2fa.ts +163 -0
  62. package/src/endpoints/auto-close.ts +175 -0
  63. package/src/endpoints/billing.ts +167 -0
  64. package/src/endpoints/bulk-action.ts +103 -0
  65. package/src/endpoints/chat-stream.ts +127 -0
  66. package/src/endpoints/chat.ts +188 -0
  67. package/src/endpoints/chatbot.ts +113 -0
  68. package/src/endpoints/delete-account.ts +129 -0
  69. package/src/endpoints/email-stats.ts +109 -0
  70. package/src/endpoints/export-csv.ts +84 -0
  71. package/src/endpoints/export-data.ts +104 -0
  72. package/src/endpoints/import-conversation.ts +307 -0
  73. package/src/endpoints/index.ts +154 -0
  74. package/src/endpoints/login.ts +92 -0
  75. package/src/endpoints/merge-clients.ts +132 -0
  76. package/src/endpoints/merge-tickets.ts +137 -0
  77. package/src/endpoints/oauth-google.ts +179 -0
  78. package/src/endpoints/pending-emails-process.ts +224 -0
  79. package/src/endpoints/presence.ts +104 -0
  80. package/src/endpoints/process-scheduled.ts +144 -0
  81. package/src/endpoints/purge-logs.ts +58 -0
  82. package/src/endpoints/resend-notification.ts +99 -0
  83. package/src/endpoints/round-robin-config.ts +92 -0
  84. package/src/endpoints/satisfaction.ts +93 -0
  85. package/src/endpoints/search.ts +106 -0
  86. package/src/endpoints/seed-kb.ts +153 -0
  87. package/src/endpoints/settings.ts +144 -0
  88. package/src/endpoints/signature.ts +93 -0
  89. package/src/endpoints/sla-check.ts +124 -0
  90. package/src/endpoints/split-ticket.ts +131 -0
  91. package/src/endpoints/statuses.ts +45 -0
  92. package/src/endpoints/track-open.ts +154 -0
  93. package/src/endpoints/typing.ts +101 -0
  94. package/src/endpoints/user-prefs.ts +125 -0
  95. package/src/hooks/checkSLA.ts +414 -0
  96. package/src/hooks/ticketStatusEmail.ts +182 -0
  97. package/src/index.ts +51 -0
  98. package/src/plugin.ts +157 -0
  99. package/src/portal/LiveChat.tsx +1353 -0
  100. package/src/portal/auth/ChatWidget.tsx +350 -0
  101. package/src/portal/auth/ChatbotWidget.tsx +285 -0
  102. package/src/portal/auth/SupportHeader.tsx +409 -0
  103. package/src/portal/auth/dashboard/DashboardClient.tsx +650 -0
  104. package/src/portal/auth/dashboard/page.tsx +84 -0
  105. package/src/portal/auth/faq/FAQSearch.tsx +117 -0
  106. package/src/portal/auth/faq/page.tsx +199 -0
  107. package/src/portal/auth/layout.tsx +61 -0
  108. package/src/portal/auth/profile/page.tsx +705 -0
  109. package/src/portal/auth/tickets/detail/CloseTicketButton.tsx +74 -0
  110. package/src/portal/auth/tickets/detail/CollapsibleMessages.tsx +46 -0
  111. package/src/portal/auth/tickets/detail/MarkSolutionButton.tsx +50 -0
  112. package/src/portal/auth/tickets/detail/MessageActions.tsx +158 -0
  113. package/src/portal/auth/tickets/detail/PrintButton.tsx +16 -0
  114. package/src/portal/auth/tickets/detail/ReadReceipt.tsx +34 -0
  115. package/src/portal/auth/tickets/detail/ReopenTicketButton.tsx +74 -0
  116. package/src/portal/auth/tickets/detail/SatisfactionForm.tsx +156 -0
  117. package/src/portal/auth/tickets/detail/TicketPolling.tsx +57 -0
  118. package/src/portal/auth/tickets/detail/TicketReplyForm.tsx +294 -0
  119. package/src/portal/auth/tickets/detail/TypingIndicator.tsx +58 -0
  120. package/src/portal/auth/tickets/detail/page.tsx +738 -0
  121. package/src/portal/auth/tickets/new/page.tsx +515 -0
  122. package/src/portal/forgot-password/page.tsx +114 -0
  123. package/src/portal/layout.tsx +26 -0
  124. package/src/portal/locales/en.json +374 -0
  125. package/src/portal/locales/fr.json +374 -0
  126. package/src/portal/login/page.tsx +351 -0
  127. package/src/portal/page.tsx +162 -0
  128. package/src/portal/register/page.tsx +281 -0
  129. package/src/portal/reset-password/page.tsx +152 -0
  130. package/src/styles/BillingView.module.scss +311 -0
  131. package/src/styles/ChatView.module.scss +438 -0
  132. package/src/styles/CommandPalette.module.scss +160 -0
  133. package/src/styles/CrmView.module.scss +554 -0
  134. package/src/styles/EmailTracking.module.scss +238 -0
  135. package/src/styles/ImportConversation.module.scss +267 -0
  136. package/src/styles/Layout.module.scss +55 -0
  137. package/src/styles/Logs.module.scss +164 -0
  138. package/src/styles/NewTicket.module.scss +143 -0
  139. package/src/styles/PendingEmails.module.scss +629 -0
  140. package/src/styles/SupportDashboard.module.scss +649 -0
  141. package/src/styles/TicketDetail.module.scss +1043 -0
  142. package/src/styles/TicketInbox.module.scss +296 -0
  143. package/src/styles/TicketingSettings.module.scss +358 -0
  144. package/src/styles/TimeDashboard.module.scss +287 -0
  145. package/src/styles/_tokens.scss +78 -0
  146. package/src/styles/theme.css +633 -0
  147. package/src/types.ts +255 -0
  148. package/src/utils/adminNotification.ts +38 -0
  149. package/src/utils/auth.ts +46 -0
  150. package/src/utils/emailTemplate.ts +343 -0
  151. package/src/utils/fireWebhooks.ts +84 -0
  152. package/src/utils/index.ts +22 -0
  153. package/src/utils/rateLimiter.ts +52 -0
  154. package/src/utils/readSettings.ts +67 -0
  155. package/src/utils/slugs.ts +54 -0
  156. package/src/utils/webhookDispatcher.ts +120 -0
  157. package/src/views/BillingView/client.tsx +137 -0
  158. package/src/views/BillingView/index.tsx +33 -0
  159. package/src/views/ChatView/client.tsx +294 -0
  160. package/src/views/ChatView/index.tsx +33 -0
  161. package/src/views/CrmView/client.tsx +206 -0
  162. package/src/views/CrmView/index.tsx +33 -0
  163. package/src/views/EmailTrackingView/client.tsx +124 -0
  164. package/src/views/EmailTrackingView/index.tsx +33 -0
  165. package/src/views/ImportConversationView/client.tsx +133 -0
  166. package/src/views/ImportConversationView/index.tsx +33 -0
  167. package/src/views/LogsView/client.tsx +151 -0
  168. package/src/views/LogsView/index.tsx +30 -0
  169. package/src/views/NewTicketView/client.tsx +227 -0
  170. package/src/views/NewTicketView/index.tsx +30 -0
  171. package/src/views/PendingEmailsView/client.tsx +177 -0
  172. package/src/views/PendingEmailsView/index.tsx +33 -0
  173. package/src/views/SupportDashboardView/client.tsx +424 -0
  174. package/src/views/SupportDashboardView/index.tsx +33 -0
  175. package/src/views/TicketDetailView/client.tsx +775 -0
  176. package/src/views/TicketDetailView/index.tsx +33 -0
  177. package/src/views/TicketInboxView/client.tsx +313 -0
  178. package/src/views/TicketInboxView/index.tsx +30 -0
  179. package/src/views/TicketingSettingsView/client.tsx +866 -0
  180. package/src/views/TicketingSettingsView/index.tsx +33 -0
  181. package/src/views/TimeDashboardView/client.tsx +144 -0
  182. package/src/views/TimeDashboardView/index.tsx +33 -0
  183. package/src/views/shared/AdminViewHeader.tsx +69 -0
  184. package/src/views/shared/ErrorBoundary.tsx +68 -0
  185. package/src/views/shared/Skeleton.tsx +125 -0
  186. package/src/views/shared/adminTokens.ts +37 -0
  187. package/src/views/shared/config.ts +82 -0
  188. package/src/views/shared/index.ts +6 -0
  189. package/src/views.ts +16 -0
@@ -0,0 +1,350 @@
1
+ 'use client'
2
+
3
+ import React, { useState, useEffect, useRef, useCallback } from 'react'
4
+
5
+ interface ChatMessage {
6
+ id: string
7
+ senderType: 'client' | 'agent' | 'system'
8
+ message: string
9
+ createdAt: string
10
+ agent?: { firstName?: string; lastName?: string } | null
11
+ }
12
+
13
+ export function ChatWidget() {
14
+ const [isOpen, setIsOpen] = useState(false)
15
+ const [session, setSession] = useState<string | null>(null)
16
+ const [messages, setMessages] = useState<ChatMessage[]>([])
17
+ const [input, setInput] = useState('')
18
+ const [sending, setSending] = useState(false)
19
+ const [closed, setClosed] = useState(false)
20
+ const messagesEndRef = useRef<HTMLDivElement>(null)
21
+ const lastFetchRef = useRef<string | null>(null)
22
+ const pollInterval = useRef(3000)
23
+ const pollTimeout = useRef<NodeJS.Timeout>(undefined)
24
+
25
+ const scrollToBottom = useCallback(() => {
26
+ messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
27
+ }, [])
28
+
29
+ // Receive messages via SSE (with polling fallback)
30
+ const [pollExpired, setPollExpired] = useState(false)
31
+ const eventSourceRef = useRef<EventSource | null>(null)
32
+ const usingSSE = useRef(false)
33
+
34
+ useEffect(() => {
35
+ if (!session || closed || pollExpired) return
36
+
37
+ // Helper to merge new messages into state
38
+ const mergeMessages = (newMsgs: ChatMessage[]) => {
39
+ setMessages((prev) => {
40
+ const existingIds = new Set(prev.map((m) => m.id))
41
+ const filtered = newMsgs.filter((m) => !existingIds.has(m.id))
42
+ if (filtered.length === 0) return prev
43
+ return [...prev, ...filtered]
44
+ })
45
+ if (newMsgs.length > 0) {
46
+ lastFetchRef.current = newMsgs[newMsgs.length - 1].createdAt
47
+ // Check if session was closed by agent
48
+ const lastMsg = newMsgs[newMsgs.length - 1]
49
+ if (lastMsg.senderType === 'system' && lastMsg.message.includes('terminé')) {
50
+ setClosed(true)
51
+ }
52
+ }
53
+ }
54
+
55
+ // Try SSE first
56
+ if (typeof EventSource !== 'undefined') {
57
+ const es = new EventSource(`/api/support/chat-stream?session=${session}`)
58
+ eventSourceRef.current = es
59
+ usingSSE.current = true
60
+
61
+ es.onmessage = (event) => {
62
+ try {
63
+ const parsed = JSON.parse(event.data)
64
+ if (parsed.type === 'messages' && parsed.data?.length > 0) {
65
+ mergeMessages(parsed.data)
66
+ } else if (parsed.type === 'closed') {
67
+ setClosed(true)
68
+ }
69
+ } catch {
70
+ // Ignore parse errors
71
+ }
72
+ }
73
+
74
+ es.onerror = () => {
75
+ // SSE connection lost — close and fall back to polling
76
+ es.close()
77
+ eventSourceRef.current = null
78
+ usingSSE.current = false
79
+ }
80
+
81
+ return () => {
82
+ es.close()
83
+ eventSourceRef.current = null
84
+ usingSSE.current = false
85
+ }
86
+ }
87
+
88
+ // Fallback: adaptive polling (same as before)
89
+ usingSSE.current = false
90
+
91
+ const poll = async () => {
92
+ let hadNewMessages = false
93
+ try {
94
+ const after = lastFetchRef.current || ''
95
+ const url = `/api/support/chat?session=${session}${after ? `&after=${after}` : ''}`
96
+ const res = await fetch(url, { credentials: 'include' })
97
+ if (res.status === 401 || res.status === 403) { setPollExpired(true); return }
98
+ if (res.ok) {
99
+ const data = await res.json()
100
+ if (data.messages?.length > 0) {
101
+ mergeMessages(data.messages)
102
+ hadNewMessages = true
103
+ }
104
+ }
105
+ } catch (err) {
106
+ console.warn('[ChatWidget] Polling error:', err)
107
+ }
108
+ if (hadNewMessages) {
109
+ pollInterval.current = 3000
110
+ } else {
111
+ pollInterval.current = Math.min(pollInterval.current + 2000, 15000)
112
+ }
113
+ }
114
+
115
+ pollInterval.current = 3000
116
+ poll()
117
+
118
+ const schedulePoll = () => {
119
+ pollTimeout.current = setTimeout(async () => {
120
+ if (usingSSE.current) return // SSE reconnected, stop polling
121
+ await poll()
122
+ schedulePoll()
123
+ }, pollInterval.current)
124
+ }
125
+ schedulePoll()
126
+
127
+ return () => clearTimeout(pollTimeout.current)
128
+ }, [session, closed, pollExpired])
129
+
130
+ useEffect(() => {
131
+ scrollToBottom()
132
+ }, [messages, scrollToBottom])
133
+
134
+ const startChat = async () => {
135
+ try {
136
+ const res = await fetch('/api/support/chat', {
137
+ method: 'POST',
138
+ headers: { 'Content-Type': 'application/json' },
139
+ credentials: 'include',
140
+ body: JSON.stringify({ action: 'start' }),
141
+ })
142
+ if (res.ok) {
143
+ const data = await res.json()
144
+ setSession(data.session)
145
+ setMessages(data.messages || [])
146
+ lastFetchRef.current = data.messages?.[data.messages.length - 1]?.createdAt || null
147
+ setClosed(false)
148
+ }
149
+ } catch (err) {
150
+ console.warn('[ChatWidget] Error starting chat:', err)
151
+ }
152
+ }
153
+
154
+ const sendMessage = async (e: React.FormEvent) => {
155
+ e.preventDefault()
156
+ if (!input.trim() || !session || sending) return
157
+
158
+ setSending(true)
159
+ try {
160
+ const res = await fetch('/api/support/chat', {
161
+ method: 'POST',
162
+ headers: { 'Content-Type': 'application/json' },
163
+ credentials: 'include',
164
+ body: JSON.stringify({ action: 'send', session, message: input.trim() }),
165
+ })
166
+ if (res.ok) {
167
+ const data = await res.json()
168
+ setMessages((prev) => [...prev, data.message])
169
+ lastFetchRef.current = data.message.createdAt
170
+ setInput('')
171
+ }
172
+ } catch (err) {
173
+ console.warn('[ChatWidget] Error sending message:', err)
174
+ } finally {
175
+ setSending(false)
176
+ }
177
+ }
178
+
179
+ const closeChat = async () => {
180
+ if (!session) return
181
+ try {
182
+ await fetch('/api/support/chat', {
183
+ method: 'POST',
184
+ headers: { 'Content-Type': 'application/json' },
185
+ credentials: 'include',
186
+ body: JSON.stringify({ action: 'close', session }),
187
+ })
188
+ } catch (err) {
189
+ console.warn('[ChatWidget] Error closing chat:', err)
190
+ }
191
+ setClosed(true)
192
+ }
193
+
194
+ const resetChat = () => {
195
+ setSession(null)
196
+ setMessages([])
197
+ setClosed(false)
198
+ lastFetchRef.current = null
199
+ }
200
+
201
+ return (
202
+ <>
203
+ {/* Chat toggle button */}
204
+ {!isOpen && (
205
+ <button
206
+ onClick={() => setIsOpen(true)}
207
+ className="fixed bottom-6 right-6 z-50 flex h-14 w-14 items-center justify-center rounded-full border-3 border-black bg-[#00E5FF] shadow-[4px_4px_0px_#000] transition-all hover:translate-x-[2px] hover:translate-y-[2px] hover:shadow-[2px_2px_0px_#000] dark:border-gray-600 dark:shadow-[4px_4px_0px_#333]"
208
+ title="Chat en direct"
209
+ >
210
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" className="h-6 w-6">
211
+ <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
212
+ </svg>
213
+ </button>
214
+ )}
215
+
216
+ {/* Chat window */}
217
+ {isOpen && (
218
+ <div className="fixed bottom-6 right-6 z-50 flex h-[500px] w-[380px] max-w-[calc(100vw-2rem)] flex-col rounded-2xl border-4 border-black bg-white shadow-[6px_6px_0px_#000] dark:border-gray-600 dark:bg-gray-900 dark:shadow-[6px_6px_0px_#333]">
219
+ {/* Header */}
220
+ <div className="flex items-center justify-between rounded-t-xl border-b-3 border-black bg-[#00E5FF] px-4 py-3 dark:border-gray-600">
221
+ <div className="flex items-center gap-2">
222
+ <div className="h-3 w-3 rounded-full border-2 border-black bg-green-400" />
223
+ <span className="text-sm font-black text-black">Chat en direct</span>
224
+ </div>
225
+ <div className="flex items-center gap-1">
226
+ {session && !closed && (
227
+ <button
228
+ onClick={closeChat}
229
+ className="rounded-lg p-1 text-black/60 transition-colors hover:bg-black/10 hover:text-black"
230
+ title="Terminer le chat"
231
+ >
232
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="h-4 w-4">
233
+ <rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
234
+ <line x1="9" y1="9" x2="15" y2="15" />
235
+ <line x1="15" y1="9" x2="9" y2="15" />
236
+ </svg>
237
+ </button>
238
+ )}
239
+ <button
240
+ onClick={() => setIsOpen(false)}
241
+ className="rounded-lg p-1 text-black/60 transition-colors hover:bg-black/10 hover:text-black"
242
+ title="Réduire"
243
+ >
244
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" className="h-4 w-4">
245
+ <line x1="5" y1="12" x2="19" y2="12" />
246
+ </svg>
247
+ </button>
248
+ </div>
249
+ </div>
250
+
251
+ {/* Messages area */}
252
+ <div className="flex-1 overflow-y-auto p-4">
253
+ {!session ? (
254
+ <div className="flex h-full flex-col items-center justify-center text-center">
255
+ <div className="mb-4 flex h-16 w-16 items-center justify-center rounded-2xl border-3 border-black bg-[#FFD600] shadow-[3px_3px_0px_#000] dark:border-gray-600">
256
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="h-8 w-8">
257
+ <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
258
+ </svg>
259
+ </div>
260
+ <h3 className="mb-2 text-lg font-black text-black dark:text-white">Besoin d&apos;aide ?</h3>
261
+ <p className="mb-6 text-sm text-gray-500 dark:text-gray-400">
262
+ Démarrez une conversation avec notre équipe support.
263
+ </p>
264
+ <button
265
+ onClick={startChat}
266
+ className="rounded-xl border-3 border-black bg-[#00E5FF] px-6 py-3 text-sm font-black text-black shadow-[4px_4px_0px_#000] transition-all hover:translate-x-[2px] hover:translate-y-[2px] hover:shadow-[2px_2px_0px_#000]"
267
+ >
268
+ Démarrer le chat
269
+ </button>
270
+ </div>
271
+ ) : (
272
+ <div className="space-y-3">
273
+ {messages.map((msg) => (
274
+ <div
275
+ key={msg.id}
276
+ className={`flex ${msg.senderType === 'client' ? 'justify-end' : 'justify-start'}`}
277
+ >
278
+ {msg.senderType === 'system' ? (
279
+ <div className="w-full rounded-lg bg-gray-100 px-3 py-2 text-center text-xs text-gray-500 dark:bg-gray-800 dark:text-gray-400">
280
+ {msg.message}
281
+ </div>
282
+ ) : (
283
+ <div
284
+ className={`max-w-[80%] rounded-2xl px-4 py-2.5 ${
285
+ msg.senderType === 'client'
286
+ ? 'border-2 border-black bg-[#00E5FF] text-black'
287
+ : 'border-2 border-gray-200 bg-gray-100 text-black dark:border-gray-700 dark:bg-gray-800 dark:text-white'
288
+ }`}
289
+ >
290
+ {msg.senderType === 'agent' && msg.agent && (
291
+ <p className="mb-1 text-xs font-bold text-gray-500 dark:text-gray-400">
292
+ {(msg.agent as { firstName?: string })?.firstName || 'Agent'}
293
+ </p>
294
+ )}
295
+ <p className="text-sm whitespace-pre-wrap">{msg.message}</p>
296
+ <p className={`mt-1 text-xs ${msg.senderType === 'client' ? 'text-black/50' : 'text-gray-400'}`}>
297
+ {new Date(msg.createdAt).toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' })}
298
+ </p>
299
+ </div>
300
+ )}
301
+ </div>
302
+ ))}
303
+ <div ref={messagesEndRef} />
304
+ </div>
305
+ )}
306
+
307
+ {closed && (
308
+ <div className="mt-4 text-center">
309
+ <p className="mb-3 text-sm text-gray-500 dark:text-gray-400">Le chat est terminé.</p>
310
+ <button
311
+ onClick={resetChat}
312
+ className="rounded-xl border-2 border-black bg-[#FFD600] px-4 py-2 text-xs font-bold text-black shadow-[2px_2px_0px_#000] transition-all hover:translate-x-[1px] hover:translate-y-[1px] hover:shadow-[1px_1px_0px_#000]"
313
+ >
314
+ Nouveau chat
315
+ </button>
316
+ </div>
317
+ )}
318
+ </div>
319
+
320
+ {/* Input area */}
321
+ {session && !closed && (
322
+ <form onSubmit={sendMessage} className="border-t-3 border-black p-3 dark:border-gray-600">
323
+ <div className="flex gap-2">
324
+ <input
325
+ type="text"
326
+ value={input}
327
+ onChange={(e) => setInput(e.target.value)}
328
+ placeholder="Tapez votre message..."
329
+ maxLength={2000}
330
+ className="flex-1 rounded-xl border-2 border-black px-3 py-2 text-sm outline-none transition-shadow focus:shadow-[2px_2px_0px_#00E5FF] dark:border-gray-600 dark:bg-gray-800 dark:text-white"
331
+ autoFocus
332
+ />
333
+ <button
334
+ type="submit"
335
+ disabled={!input.trim() || sending}
336
+ className="rounded-xl border-2 border-black bg-[#00E5FF] px-3 py-2 font-bold text-black transition-all hover:shadow-[2px_2px_0px_#000] disabled:opacity-50 dark:border-gray-600"
337
+ >
338
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" className="h-4 w-4">
339
+ <line x1="22" y1="2" x2="11" y2="13" />
340
+ <polygon points="22 2 15 22 11 13 2 9 22 2" />
341
+ </svg>
342
+ </button>
343
+ </div>
344
+ </form>
345
+ )}
346
+ </div>
347
+ )}
348
+ </>
349
+ )
350
+ }
@@ -0,0 +1,285 @@
1
+ 'use client'
2
+
3
+ import React, { useState, useRef, useEffect, useCallback } from 'react'
4
+ import Link from 'next/link'
5
+
6
+ interface ChatbotMessage {
7
+ id: string
8
+ role: 'user' | 'assistant' | 'system'
9
+ content: string
10
+ suggestion?: 'create_ticket' | 'resolved' | null
11
+ }
12
+
13
+ let messageIdCounter = 0
14
+ function generateId(): string {
15
+ return `msg-${Date.now()}-${++messageIdCounter}`
16
+ }
17
+
18
+ export function ChatbotWidget() {
19
+ const [isOpen, setIsOpen] = useState(false)
20
+ const [messages, setMessages] = useState<ChatbotMessage[]>([])
21
+ const [input, setInput] = useState('')
22
+ const [loading, setLoading] = useState(false)
23
+ const messagesEndRef = useRef<HTMLDivElement>(null)
24
+ const inputRef = useRef<HTMLInputElement>(null)
25
+
26
+ const scrollToBottom = useCallback(() => {
27
+ messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
28
+ }, [])
29
+
30
+ useEffect(() => {
31
+ scrollToBottom()
32
+ }, [messages, scrollToBottom])
33
+
34
+ // Focus input when panel opens
35
+ useEffect(() => {
36
+ if (isOpen) {
37
+ setTimeout(() => inputRef.current?.focus(), 100)
38
+ }
39
+ }, [isOpen])
40
+
41
+ // Show welcome message on first open
42
+ useEffect(() => {
43
+ if (isOpen && messages.length === 0) {
44
+ setMessages([
45
+ {
46
+ id: generateId(),
47
+ role: 'assistant',
48
+ content:
49
+ 'Bonjour ! Je suis l\'assistant Support. Posez-moi une question et je chercherai dans notre base de connaissances.',
50
+ },
51
+ ])
52
+ }
53
+ }, [isOpen, messages.length])
54
+
55
+ const handleSubmit = async (e: React.FormEvent) => {
56
+ e.preventDefault()
57
+ const question = input.trim()
58
+ if (!question || loading) return
59
+
60
+ const userMessage: ChatbotMessage = {
61
+ id: generateId(),
62
+ role: 'user',
63
+ content: question,
64
+ }
65
+ setMessages((prev) => [...prev, userMessage])
66
+ setInput('')
67
+ setLoading(true)
68
+
69
+ try {
70
+ const res = await fetch('/api/support/chatbot', {
71
+ method: 'POST',
72
+ headers: { 'Content-Type': 'application/json' },
73
+ body: JSON.stringify({ question }),
74
+ })
75
+
76
+ if (!res.ok) {
77
+ throw new Error('Erreur serveur')
78
+ }
79
+
80
+ const data = await res.json()
81
+
82
+ const botMessage: ChatbotMessage = {
83
+ id: generateId(),
84
+ role: 'assistant',
85
+ content: data.answer || data.message || 'Désolé, je n\'ai pas pu traiter votre demande.',
86
+ suggestion: data.suggestion === 'create_ticket' ? 'create_ticket' : null,
87
+ }
88
+ setMessages((prev) => [...prev, botMessage])
89
+ } catch {
90
+ setMessages((prev) => [
91
+ ...prev,
92
+ {
93
+ id: generateId(),
94
+ role: 'system',
95
+ content: 'Une erreur est survenue. Veuillez réessayer.',
96
+ },
97
+ ])
98
+ } finally {
99
+ setLoading(false)
100
+ }
101
+ }
102
+
103
+ return (
104
+ <>
105
+ {/* Floating trigger button — only show when chatbot panel is closed */}
106
+ {!isOpen && (
107
+ <button
108
+ onClick={() => setIsOpen(true)}
109
+ className="fixed bottom-24 right-6 z-40 flex h-14 w-14 items-center justify-center rounded-full bg-blue-600 text-white shadow-lg transition-all hover:bg-blue-700 hover:shadow-xl focus:outline-none focus:ring-2 focus:ring-blue-400 focus:ring-offset-2 dark:bg-blue-500 dark:hover:bg-blue-600"
110
+ aria-label="Ouvrir l'assistant IA"
111
+ title="Assistant IA"
112
+ >
113
+ {/* Sparkle / AI icon */}
114
+ <svg
115
+ xmlns="http://www.w3.org/2000/svg"
116
+ viewBox="0 0 24 24"
117
+ fill="none"
118
+ stroke="currentColor"
119
+ strokeWidth="2"
120
+ strokeLinecap="round"
121
+ strokeLinejoin="round"
122
+ className="h-6 w-6"
123
+ >
124
+ <path d="M12 2L15.09 8.26L22 9.27L17 14.14L18.18 21.02L12 17.77L5.82 21.02L7 14.14L2 9.27L8.91 8.26L12 2Z" />
125
+ </svg>
126
+ </button>
127
+ )}
128
+
129
+ {/* Chat panel */}
130
+ {isOpen && (
131
+ <div
132
+ className="fixed bottom-24 right-6 z-40 flex w-[360px] max-w-[calc(100vw-2rem)] flex-col overflow-hidden rounded-2xl border border-gray-200 bg-white shadow-2xl transition-all dark:border-gray-700 dark:bg-gray-900"
133
+ style={{ height: '500px' }}
134
+ role="dialog"
135
+ aria-label="Assistant Support"
136
+ >
137
+ {/* Header */}
138
+ <div className="flex items-center justify-between border-b border-gray-200 bg-blue-600 px-4 py-3 dark:border-gray-700">
139
+ <div className="flex items-center gap-2.5">
140
+ <div className="flex h-8 w-8 items-center justify-center rounded-full bg-white/20">
141
+ <svg
142
+ xmlns="http://www.w3.org/2000/svg"
143
+ viewBox="0 0 24 24"
144
+ fill="none"
145
+ stroke="currentColor"
146
+ strokeWidth="2"
147
+ strokeLinecap="round"
148
+ strokeLinejoin="round"
149
+ className="h-4 w-4 text-white"
150
+ >
151
+ <path d="M12 2L15.09 8.26L22 9.27L17 14.14L18.18 21.02L12 17.77L5.82 21.02L7 14.14L2 9.27L8.91 8.26L12 2Z" />
152
+ </svg>
153
+ </div>
154
+ <div>
155
+ <h2 className="text-sm font-semibold text-white">Assistant Support</h2>
156
+ <p className="text-xs text-white/70">Base de connaissances</p>
157
+ </div>
158
+ </div>
159
+ <button
160
+ onClick={() => setIsOpen(false)}
161
+ className="rounded-lg p-1.5 text-white/70 transition-colors hover:bg-white/10 hover:text-white"
162
+ aria-label="Fermer l'assistant"
163
+ >
164
+ <svg
165
+ xmlns="http://www.w3.org/2000/svg"
166
+ viewBox="0 0 24 24"
167
+ fill="none"
168
+ stroke="currentColor"
169
+ strokeWidth="2"
170
+ strokeLinecap="round"
171
+ strokeLinejoin="round"
172
+ className="h-5 w-5"
173
+ >
174
+ <line x1="18" y1="6" x2="6" y2="18" />
175
+ <line x1="6" y1="6" x2="18" y2="18" />
176
+ </svg>
177
+ </button>
178
+ </div>
179
+
180
+ {/* Messages area */}
181
+ <div className="flex-1 overflow-y-auto px-4 py-4">
182
+ <div className="space-y-4">
183
+ {messages.map((msg) => (
184
+ <div
185
+ key={msg.id}
186
+ className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}
187
+ >
188
+ {msg.role === 'system' ? (
189
+ <div className="w-full rounded-lg bg-red-50 px-3 py-2 text-center text-xs text-red-600 dark:bg-red-900/20 dark:text-red-400">
190
+ {msg.content}
191
+ </div>
192
+ ) : (
193
+ <div
194
+ className={`max-w-[85%] rounded-2xl px-4 py-2.5 ${
195
+ msg.role === 'user'
196
+ ? 'bg-blue-600 text-white'
197
+ : 'bg-gray-100 text-gray-900 dark:bg-gray-800 dark:text-gray-100'
198
+ }`}
199
+ >
200
+ <p className="whitespace-pre-wrap text-sm leading-relaxed">{msg.content}</p>
201
+
202
+ {/* Create ticket CTA */}
203
+ {msg.suggestion === 'create_ticket' && (
204
+ <Link
205
+ href="/support/tickets/new"
206
+ className="mt-3 inline-flex items-center gap-2 rounded-lg bg-blue-600 px-4 py-2 text-xs font-semibold text-white transition-colors hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600"
207
+ >
208
+ <svg
209
+ xmlns="http://www.w3.org/2000/svg"
210
+ viewBox="0 0 24 24"
211
+ fill="none"
212
+ stroke="currentColor"
213
+ strokeWidth="2"
214
+ strokeLinecap="round"
215
+ strokeLinejoin="round"
216
+ className="h-3.5 w-3.5"
217
+ >
218
+ <line x1="12" y1="5" x2="12" y2="19" />
219
+ <line x1="5" y1="12" x2="19" y2="12" />
220
+ </svg>
221
+ Créer un ticket
222
+ </Link>
223
+ )}
224
+ </div>
225
+ )}
226
+ </div>
227
+ ))}
228
+
229
+ {/* Typing indicator */}
230
+ {loading && (
231
+ <div className="flex justify-start">
232
+ <div className="rounded-2xl bg-gray-100 px-4 py-3 dark:bg-gray-800">
233
+ <div className="flex items-center gap-1.5">
234
+ <span className="h-2 w-2 animate-bounce rounded-full bg-gray-400 [animation-delay:0ms]" />
235
+ <span className="h-2 w-2 animate-bounce rounded-full bg-gray-400 [animation-delay:150ms]" />
236
+ <span className="h-2 w-2 animate-bounce rounded-full bg-gray-400 [animation-delay:300ms]" />
237
+ </div>
238
+ </div>
239
+ </div>
240
+ )}
241
+
242
+ <div ref={messagesEndRef} />
243
+ </div>
244
+ </div>
245
+
246
+ {/* Input area */}
247
+ <form onSubmit={handleSubmit} className="border-t border-gray-200 p-3 dark:border-gray-700">
248
+ <div className="flex gap-2">
249
+ <input
250
+ ref={inputRef}
251
+ type="text"
252
+ value={input}
253
+ onChange={(e) => setInput(e.target.value)}
254
+ placeholder="Posez votre question..."
255
+ maxLength={500}
256
+ disabled={loading}
257
+ className="flex-1 rounded-xl border border-gray-300 bg-white px-3.5 py-2.5 text-sm text-gray-900 outline-none transition-all placeholder:text-gray-400 focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20 disabled:opacity-50 dark:border-gray-600 dark:bg-gray-800 dark:text-white dark:placeholder:text-gray-500 dark:focus:border-blue-400"
258
+ />
259
+ <button
260
+ type="submit"
261
+ disabled={!input.trim() || loading}
262
+ className="flex h-[42px] w-[42px] flex-shrink-0 items-center justify-center rounded-xl bg-blue-600 text-white transition-all hover:bg-blue-700 disabled:opacity-40 disabled:hover:bg-blue-600 dark:bg-blue-500 dark:hover:bg-blue-600"
263
+ aria-label="Envoyer"
264
+ >
265
+ <svg
266
+ xmlns="http://www.w3.org/2000/svg"
267
+ viewBox="0 0 24 24"
268
+ fill="none"
269
+ stroke="currentColor"
270
+ strokeWidth="2.5"
271
+ strokeLinecap="round"
272
+ strokeLinejoin="round"
273
+ className="h-4 w-4"
274
+ >
275
+ <line x1="22" y1="2" x2="11" y2="13" />
276
+ <polygon points="22 2 15 22 11 13 2 9 22 2" />
277
+ </svg>
278
+ </button>
279
+ </div>
280
+ </form>
281
+ </div>
282
+ )}
283
+ </>
284
+ )
285
+ }