@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,1353 @@
1
+ 'use client'
2
+
3
+ import React, { useState, useEffect, useRef, useCallback } from 'react'
4
+ import { MessageCircle, Send, X, Minimize2, ArrowLeft } from 'lucide-react'
5
+
6
+ // ─── Types ───────────────────────────────────────────────────
7
+
8
+ interface ChatMessage {
9
+ id: number | string
10
+ body: string
11
+ senderType: 'client' | 'agent' | 'system'
12
+ createdAt: string
13
+ }
14
+
15
+ interface LiveChatSession {
16
+ clientToken: string
17
+ email: string
18
+ name: string
19
+ sessionId?: string
20
+ }
21
+
22
+ type Screen = 'closed' | 'identify' | 'chat' | 'rating'
23
+
24
+ // ─── LocalStorage keys ──────────────────────────────────────
25
+
26
+ const LS_KEY = 'support-livechat'
27
+
28
+ function loadSession(): LiveChatSession | null {
29
+ if (typeof window === 'undefined') return null
30
+ try {
31
+ const raw = localStorage.getItem(LS_KEY)
32
+ if (!raw) return null
33
+ const session = JSON.parse(raw) as LiveChatSession
34
+
35
+ // Validate token expiry (24h) — extract timestamp from token
36
+ if (session.clientToken) {
37
+ const parts = session.clientToken.split('_')
38
+ if (parts.length === 4) {
39
+ const timestamp = parseInt(parts[2], 10)
40
+ if (!isNaN(timestamp) && Date.now() - timestamp > 24 * 60 * 60 * 1000) {
41
+ localStorage.removeItem(LS_KEY)
42
+ return null
43
+ }
44
+ }
45
+ }
46
+
47
+ return session
48
+ } catch {
49
+ return null
50
+ }
51
+ }
52
+
53
+ function saveSession(session: LiveChatSession): void {
54
+ try {
55
+ localStorage.setItem(LS_KEY, JSON.stringify(session))
56
+ } catch {
57
+ // localStorage full or disabled
58
+ }
59
+ }
60
+
61
+ function clearSession(): void {
62
+ try {
63
+ localStorage.removeItem(LS_KEY)
64
+ } catch {
65
+ // Ignore
66
+ }
67
+ }
68
+
69
+ // ─── Component ───────────────────────────────────────────────
70
+
71
+ export function LiveChat() {
72
+ const [screen, setScreen] = useState<Screen>('closed')
73
+ const [session, setSession] = useState<LiveChatSession | null>(null)
74
+ const [messages, setMessages] = useState<ChatMessage[]>([])
75
+ const [input, setInput] = useState('')
76
+ const [sending, setSending] = useState(false)
77
+ const [error, setError] = useState<string | null>(null)
78
+ const [unreadCount, setUnreadCount] = useState(0)
79
+ const [identifyLoading, setIdentifyLoading] = useState(false)
80
+ const [rating, setRating] = useState<number>(0)
81
+ const [ratingHover, setRatingHover] = useState<number>(0)
82
+ const [ratingComment, setRatingComment] = useState('')
83
+ const [ratingSubmitted, setRatingSubmitted] = useState(false)
84
+
85
+ // Identify form
86
+ const [formEmail, setFormEmail] = useState('')
87
+ const [formName, setFormName] = useState('')
88
+
89
+ const messagesEndRef = useRef<HTMLDivElement>(null)
90
+ const lastFetchRef = useRef<string | null>(null)
91
+ const inputRef = useRef<HTMLInputElement>(null)
92
+ const pollIntervalRef = useRef<ReturnType<typeof setTimeout> | null>(null)
93
+
94
+ // Load session from localStorage on mount
95
+ useEffect(() => {
96
+ const saved = loadSession()
97
+ if (saved) {
98
+ setSession(saved)
99
+ setFormEmail(saved.email)
100
+ setFormName(saved.name)
101
+ }
102
+ }, [])
103
+
104
+ // Auto-scroll to bottom when messages change
105
+ useEffect(() => {
106
+ messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
107
+ }, [messages])
108
+
109
+ // Poll for new messages when chat is open and we have a session
110
+ const pollMessages = useCallback(async (): Promise<boolean> => {
111
+ if (!session?.sessionId || !session?.clientToken) return false
112
+
113
+ try {
114
+ const after = lastFetchRef.current || ''
115
+ const params = new URLSearchParams({
116
+ session: session.sessionId,
117
+ clientToken: session.clientToken,
118
+ ...(after ? { after } : {}),
119
+ })
120
+ const res = await fetch(`/api/live-chat/messages?${params}`)
121
+ if (!res.ok) return false
122
+
123
+ const data = await res.json()
124
+ if (data.messages?.length > 0) {
125
+ let hadNewMessages = false
126
+ setMessages((prev) => {
127
+ const existingIds = new Set(prev.map((m) => String(m.id)))
128
+ const newMsgs = data.messages.filter(
129
+ (m: ChatMessage) => !existingIds.has(String(m.id)),
130
+ )
131
+ if (newMsgs.length === 0) return prev
132
+
133
+ hadNewMessages = true
134
+
135
+ // Count agent messages as unread if chat panel is closed
136
+ if (screen === 'closed') {
137
+ const agentNewMsgs = newMsgs.filter(
138
+ (m: ChatMessage) => m.senderType === 'agent',
139
+ )
140
+ if (agentNewMsgs.length > 0) {
141
+ setUnreadCount((prev) => prev + agentNewMsgs.length)
142
+ }
143
+ }
144
+
145
+ return [...prev, ...newMsgs]
146
+ })
147
+ lastFetchRef.current =
148
+ data.messages[data.messages.length - 1].createdAt
149
+ return hadNewMessages
150
+ }
151
+ return false
152
+ } catch {
153
+ // Ignore polling errors silently
154
+ return false
155
+ }
156
+ }, [session?.sessionId, session?.clientToken, screen])
157
+
158
+ useEffect(() => {
159
+ // Start adaptive polling if we have a session
160
+ if (session?.sessionId) {
161
+ let currentInterval = 5000
162
+ const MIN_INTERVAL = 5000
163
+ const MAX_INTERVAL = 30000
164
+
165
+ const adaptivePoll = async () => {
166
+ const hasNewMessages = await pollMessages()
167
+ if (hasNewMessages) {
168
+ currentInterval = MIN_INTERVAL
169
+ } else {
170
+ currentInterval = Math.min(currentInterval * 1.5, MAX_INTERVAL)
171
+ }
172
+ pollIntervalRef.current = setTimeout(adaptivePoll, currentInterval)
173
+ }
174
+
175
+ adaptivePoll()
176
+ return () => {
177
+ if (pollIntervalRef.current) clearTimeout(pollIntervalRef.current)
178
+ }
179
+ }
180
+ }, [session?.sessionId, pollMessages])
181
+
182
+ // ─── Handlers ────────────────────────────────────────────
183
+
184
+ const handleOpen = () => {
185
+ setUnreadCount(0)
186
+ if (session?.clientToken) {
187
+ setScreen('chat')
188
+ // If we have a session, fetch messages
189
+ if (session.sessionId) {
190
+ lastFetchRef.current = null
191
+ setMessages([])
192
+ pollMessages()
193
+ }
194
+ } else {
195
+ setScreen('identify')
196
+ }
197
+ setError(null)
198
+ }
199
+
200
+ const handleClose = () => {
201
+ setScreen('closed')
202
+ setError(null)
203
+ }
204
+
205
+ const handleIdentify = async (e: React.FormEvent) => {
206
+ e.preventDefault()
207
+ setError(null)
208
+ setIdentifyLoading(true)
209
+
210
+ try {
211
+ const res = await fetch('/api/live-chat/start', {
212
+ method: 'POST',
213
+ headers: { 'Content-Type': 'application/json' },
214
+ body: JSON.stringify({ email: formEmail.trim(), name: formName.trim() }),
215
+ })
216
+
217
+ const data = await res.json()
218
+
219
+ if (!res.ok) {
220
+ setError(data.error || 'Erreur de connexion.')
221
+ setIdentifyLoading(false)
222
+ return
223
+ }
224
+
225
+ const newSession: LiveChatSession = {
226
+ clientToken: data.token,
227
+ email: formEmail.trim().toLowerCase(),
228
+ name: formName.trim(),
229
+ sessionId: data.session,
230
+ }
231
+
232
+ setSession(newSession)
233
+ saveSession(newSession)
234
+ setScreen('chat')
235
+ } catch {
236
+ setError('Impossible de se connecter. Verifiez votre connexion.')
237
+ } finally {
238
+ setIdentifyLoading(false)
239
+ }
240
+ }
241
+
242
+ const handleSendMessage = async (e: React.FormEvent) => {
243
+ e.preventDefault()
244
+ if (!input.trim() || !session?.clientToken || sending) return
245
+
246
+ const messageText = input.trim()
247
+ setInput('')
248
+ setSending(true)
249
+ setError(null)
250
+
251
+ // Optimistic update
252
+ const tempId = `temp_${Date.now()}`
253
+ const optimisticMsg: ChatMessage = {
254
+ id: tempId,
255
+ body: messageText,
256
+ senderType: 'client',
257
+ createdAt: new Date().toISOString(),
258
+ }
259
+ setMessages((prev) => [...prev, optimisticMsg])
260
+
261
+ try {
262
+ const res = await fetch('/api/live-chat/message', {
263
+ method: 'POST',
264
+ headers: { 'Content-Type': 'application/json' },
265
+ body: JSON.stringify({
266
+ session: session.sessionId || undefined,
267
+ message: messageText,
268
+ clientToken: session.clientToken,
269
+ }),
270
+ })
271
+
272
+ const data = await res.json()
273
+
274
+ if (!res.ok) {
275
+ // Remove optimistic message on error
276
+ setMessages((prev) => prev.filter((m) => m.id !== tempId))
277
+ setError(data.error || 'Impossible d\'envoyer le message.')
278
+ setInput(messageText) // Restore input
279
+ setSending(false)
280
+ return
281
+ }
282
+
283
+ // Replace optimistic message with real one
284
+ setMessages((prev) =>
285
+ prev.map((m) => (m.id === tempId ? data.message : m)),
286
+ )
287
+ lastFetchRef.current = data.message.createdAt
288
+
289
+ // Save sessionId if this was the first message (fallback session creation)
290
+ if (!session.sessionId && data.session) {
291
+ const updatedSession = { ...session, sessionId: data.session }
292
+ setSession(updatedSession)
293
+ saveSession(updatedSession)
294
+ }
295
+ } catch {
296
+ setMessages((prev) => prev.filter((m) => m.id !== tempId))
297
+ setError('Erreur reseau. Reessayez.')
298
+ setInput(messageText)
299
+ } finally {
300
+ setSending(false)
301
+ }
302
+ }
303
+
304
+ const handleNewConversation = async () => {
305
+ // Start a new session (keep the client identity)
306
+ if (!session?.clientToken) return
307
+
308
+ try {
309
+ const res = await fetch('/api/live-chat/start', {
310
+ method: 'POST',
311
+ headers: { 'Content-Type': 'application/json' },
312
+ body: JSON.stringify({ email: session.email, name: session.name }),
313
+ })
314
+
315
+ const data = await res.json()
316
+
317
+ if (res.ok && data.session) {
318
+ const updatedSession = {
319
+ ...session,
320
+ clientToken: data.token,
321
+ sessionId: data.session,
322
+ }
323
+ setSession(updatedSession)
324
+ saveSession(updatedSession)
325
+ setMessages([])
326
+ lastFetchRef.current = null
327
+ }
328
+ } catch (err) {
329
+ console.warn('[LiveChat] Error starting chat:', err)
330
+ }
331
+ }
332
+
333
+ const handleEndChat = () => {
334
+ if (!session?.sessionId) return
335
+ // Show rating screen instead of closing immediately
336
+ setRating(0)
337
+ setRatingHover(0)
338
+ setRatingComment('')
339
+ setRatingSubmitted(false)
340
+ setScreen('rating')
341
+ }
342
+
343
+ const handleSubmitRating = async () => {
344
+ if (!session?.sessionId || !session?.clientToken) return
345
+
346
+ // Send rating as a system message with structured data
347
+ const ratingText = rating > 0
348
+ ? `Note: ${'★'.repeat(rating)}${'☆'.repeat(5 - rating)} (${rating}/5)${ratingComment ? ` — ${ratingComment}` : ''}`
349
+ : ratingComment ? `Commentaire: ${ratingComment}` : null
350
+
351
+ try {
352
+ // Close the session
353
+ await fetch('/api/live-chat/message', {
354
+ method: 'POST',
355
+ headers: { 'Content-Type': 'application/json' },
356
+ body: JSON.stringify({
357
+ session: session.sessionId,
358
+ message: '__close__',
359
+ clientToken: session.clientToken,
360
+ }),
361
+ })
362
+
363
+ // Send rating if provided
364
+ if (ratingText) {
365
+ await fetch('/api/live-chat/message', {
366
+ method: 'POST',
367
+ headers: { 'Content-Type': 'application/json' },
368
+ body: JSON.stringify({
369
+ session: session.sessionId,
370
+ message: `__rating__${ratingText}`,
371
+ clientToken: session.clientToken,
372
+ }),
373
+ })
374
+ }
375
+ } catch (err) {
376
+ console.warn('[LiveChat] Error closing chat:', err)
377
+ }
378
+
379
+ setRatingSubmitted(true)
380
+
381
+ // Clear session but keep identity for next time
382
+ const updatedSession = { ...session, sessionId: undefined }
383
+ setSession(updatedSession)
384
+ saveSession(updatedSession)
385
+ lastFetchRef.current = null
386
+
387
+ // After delay, return to closed state
388
+ setTimeout(() => {
389
+ setMessages([])
390
+ setScreen('closed')
391
+ }, 2500)
392
+ }
393
+
394
+ const handleSkipRating = async () => {
395
+ // Close without rating
396
+ if (session?.sessionId && session?.clientToken) {
397
+ try {
398
+ await fetch('/api/live-chat/message', {
399
+ method: 'POST',
400
+ headers: { 'Content-Type': 'application/json' },
401
+ body: JSON.stringify({
402
+ session: session.sessionId,
403
+ message: '__close__',
404
+ clientToken: session.clientToken,
405
+ }),
406
+ })
407
+ } catch (err) {
408
+ console.warn('[LiveChat] Error closing session:', err)
409
+ }
410
+ }
411
+
412
+ const updatedSession = session ? { ...session, sessionId: undefined } : null
413
+ if (updatedSession) {
414
+ setSession(updatedSession)
415
+ saveSession(updatedSession)
416
+ }
417
+ lastFetchRef.current = null
418
+ setMessages([])
419
+ setScreen('closed')
420
+ }
421
+
422
+ const handleLogout = () => {
423
+ clearSession()
424
+ setSession(null)
425
+ setMessages([])
426
+ setFormEmail('')
427
+ setFormName('')
428
+ lastFetchRef.current = null
429
+ setScreen('identify')
430
+ }
431
+
432
+ // ─── Render ──────────────────────────────────────────────
433
+
434
+ // Floating button
435
+ if (screen === 'closed') {
436
+ return (
437
+ <button
438
+ onClick={handleOpen}
439
+ aria-label="Ouvrir le chat en direct"
440
+ style={{
441
+ position: 'fixed',
442
+ bottom: 134,
443
+ right: 16,
444
+ zIndex: 55,
445
+ display: 'flex',
446
+ alignItems: 'center',
447
+ justifyContent: 'center',
448
+ width: 48,
449
+ height: 48,
450
+ borderRadius: 16,
451
+ border: '3px solid #000',
452
+ backgroundColor: '#00E5FF',
453
+ color: '#000',
454
+ cursor: 'pointer',
455
+ boxShadow: '4px 4px 0px #000',
456
+ transition: 'all 0.2s ease',
457
+ }}
458
+ onMouseEnter={(e) => {
459
+ e.currentTarget.style.transform = 'translate(2px, 2px)'
460
+ e.currentTarget.style.boxShadow = '2px 2px 0px #000'
461
+ }}
462
+ onMouseLeave={(e) => {
463
+ e.currentTarget.style.transform = 'translate(0, 0)'
464
+ e.currentTarget.style.boxShadow = '4px 4px 0px #000'
465
+ }}
466
+ >
467
+ <MessageCircle size={24} strokeWidth={2.5} />
468
+ {unreadCount > 0 && (
469
+ <span
470
+ style={{
471
+ position: 'absolute',
472
+ top: -6,
473
+ right: -6,
474
+ minWidth: 22,
475
+ height: 22,
476
+ borderRadius: 11,
477
+ backgroundColor: '#FF4444',
478
+ color: '#fff',
479
+ fontSize: 11,
480
+ fontWeight: 800,
481
+ display: 'flex',
482
+ alignItems: 'center',
483
+ justifyContent: 'center',
484
+ padding: '0 5px',
485
+ border: '2px solid #000',
486
+ }}
487
+ >
488
+ {unreadCount}
489
+ </span>
490
+ )}
491
+ </button>
492
+ )
493
+ }
494
+
495
+ return (
496
+ <div
497
+ style={{
498
+ position: 'fixed',
499
+ bottom: 16,
500
+ right: 16,
501
+ zIndex: 55,
502
+ width: 380,
503
+ maxWidth: 'calc(100vw - 32px)',
504
+ height: screen === 'identify' || screen === 'rating' ? 'auto' : 520,
505
+ maxHeight: 'calc(100vh - 32px)',
506
+ borderRadius: 16,
507
+ border: '3px solid #000',
508
+ backgroundColor: '#fff',
509
+ boxShadow: '6px 6px 0px #000',
510
+ display: 'flex',
511
+ flexDirection: 'column',
512
+ overflow: 'hidden',
513
+ fontFamily: 'var(--font-geist-sans), system-ui, sans-serif',
514
+ }}
515
+ >
516
+ {/* ── Header ── */}
517
+ <div
518
+ style={{
519
+ display: 'flex',
520
+ alignItems: 'center',
521
+ justifyContent: 'space-between',
522
+ padding: '12px 16px',
523
+ backgroundColor: '#00E5FF',
524
+ borderBottom: '3px solid #000',
525
+ minHeight: 48,
526
+ }}
527
+ >
528
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
529
+ {screen === 'chat' && session && !session.sessionId && (
530
+ <button
531
+ onClick={() => setScreen('identify')}
532
+ aria-label="Retour"
533
+ style={{
534
+ background: 'none',
535
+ border: 'none',
536
+ cursor: 'pointer',
537
+ padding: 2,
538
+ display: 'flex',
539
+ color: '#000',
540
+ }}
541
+ >
542
+ <ArrowLeft size={18} strokeWidth={2.5} />
543
+ </button>
544
+ )}
545
+ <div
546
+ style={{
547
+ width: 10,
548
+ height: 10,
549
+ borderRadius: '50%',
550
+ backgroundColor: '#22c55e',
551
+ border: '2px solid #000',
552
+ flexShrink: 0,
553
+ }}
554
+ />
555
+ <span style={{ fontWeight: 900, fontSize: 14, color: '#000' }}>
556
+ Chat en direct
557
+ </span>
558
+ </div>
559
+ <div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
560
+ {session?.clientToken && (
561
+ <button
562
+ onClick={handleLogout}
563
+ title="Changer d'identite"
564
+ style={{
565
+ background: 'none',
566
+ border: 'none',
567
+ cursor: 'pointer',
568
+ padding: 4,
569
+ borderRadius: 6,
570
+ color: 'rgba(0,0,0,0.5)',
571
+ display: 'flex',
572
+ transition: 'color 0.15s',
573
+ }}
574
+ onMouseEnter={(e) => {
575
+ e.currentTarget.style.color = '#000'
576
+ }}
577
+ onMouseLeave={(e) => {
578
+ e.currentTarget.style.color = 'rgba(0,0,0,0.5)'
579
+ }}
580
+ >
581
+ <ArrowLeft size={16} strokeWidth={2} />
582
+ </button>
583
+ )}
584
+ <button
585
+ onClick={handleClose}
586
+ aria-label="Reduire le chat"
587
+ style={{
588
+ background: 'none',
589
+ border: 'none',
590
+ cursor: 'pointer',
591
+ padding: 4,
592
+ borderRadius: 6,
593
+ color: 'rgba(0,0,0,0.5)',
594
+ display: 'flex',
595
+ transition: 'color 0.15s',
596
+ }}
597
+ onMouseEnter={(e) => {
598
+ e.currentTarget.style.color = '#000'
599
+ }}
600
+ onMouseLeave={(e) => {
601
+ e.currentTarget.style.color = 'rgba(0,0,0,0.5)'
602
+ }}
603
+ >
604
+ <Minimize2 size={16} strokeWidth={2.5} />
605
+ </button>
606
+ <button
607
+ onClick={() => {
608
+ handleClose()
609
+ }}
610
+ aria-label="Fermer le chat"
611
+ style={{
612
+ background: 'none',
613
+ border: 'none',
614
+ cursor: 'pointer',
615
+ padding: 4,
616
+ borderRadius: 6,
617
+ color: 'rgba(0,0,0,0.5)',
618
+ display: 'flex',
619
+ transition: 'color 0.15s',
620
+ }}
621
+ onMouseEnter={(e) => {
622
+ e.currentTarget.style.color = '#000'
623
+ }}
624
+ onMouseLeave={(e) => {
625
+ e.currentTarget.style.color = 'rgba(0,0,0,0.5)'
626
+ }}
627
+ >
628
+ <X size={16} strokeWidth={2.5} />
629
+ </button>
630
+ </div>
631
+ </div>
632
+
633
+ {/* ── Identify Screen ── */}
634
+ {screen === 'identify' && (
635
+ <div style={{ padding: 24, flex: 1 }}>
636
+ <div style={{ textAlign: 'center', marginBottom: 24 }}>
637
+ <div
638
+ style={{
639
+ width: 56,
640
+ height: 56,
641
+ borderRadius: 16,
642
+ border: '3px solid #000',
643
+ backgroundColor: '#FFD600',
644
+ boxShadow: '3px 3px 0px #000',
645
+ display: 'inline-flex',
646
+ alignItems: 'center',
647
+ justifyContent: 'center',
648
+ marginBottom: 12,
649
+ }}
650
+ >
651
+ <MessageCircle size={28} strokeWidth={2} color="#000" />
652
+ </div>
653
+ <h3
654
+ style={{
655
+ fontSize: 18,
656
+ fontWeight: 900,
657
+ color: '#000',
658
+ margin: '0 0 4px',
659
+ }}
660
+ >
661
+ Besoin d&apos;aide ?
662
+ </h3>
663
+ <p
664
+ style={{
665
+ fontSize: 13,
666
+ color: '#666',
667
+ margin: 0,
668
+ lineHeight: 1.5,
669
+ }}
670
+ >
671
+ Envoyez-nous un message, nous vous repondrons rapidement.
672
+ </p>
673
+ </div>
674
+
675
+ <form onSubmit={handleIdentify}>
676
+ <div style={{ marginBottom: 12 }}>
677
+ <label
678
+ htmlFor="livechat-name"
679
+ style={{
680
+ display: 'block',
681
+ fontSize: 12,
682
+ fontWeight: 800,
683
+ color: '#000',
684
+ marginBottom: 4,
685
+ textTransform: 'uppercase',
686
+ letterSpacing: 0.5,
687
+ }}
688
+ >
689
+ Votre nom
690
+ </label>
691
+ <input
692
+ id="livechat-name"
693
+ type="text"
694
+ value={formName}
695
+ onChange={(e) => setFormName(e.target.value)}
696
+ placeholder="Jean Dupont"
697
+ required
698
+ maxLength={100}
699
+ style={{
700
+ width: '100%',
701
+ padding: '10px 12px',
702
+ borderRadius: 10,
703
+ border: '2px solid #000',
704
+ fontSize: 14,
705
+ color: '#000',
706
+ backgroundColor: '#fff',
707
+ outline: 'none',
708
+ boxSizing: 'border-box',
709
+ transition: 'box-shadow 0.15s',
710
+ }}
711
+ onFocus={(e) => {
712
+ e.currentTarget.style.boxShadow = '2px 2px 0px #00E5FF'
713
+ }}
714
+ onBlur={(e) => {
715
+ e.currentTarget.style.boxShadow = 'none'
716
+ }}
717
+ />
718
+ </div>
719
+
720
+ <div style={{ marginBottom: 16 }}>
721
+ <label
722
+ htmlFor="livechat-email"
723
+ style={{
724
+ display: 'block',
725
+ fontSize: 12,
726
+ fontWeight: 800,
727
+ color: '#000',
728
+ marginBottom: 4,
729
+ textTransform: 'uppercase',
730
+ letterSpacing: 0.5,
731
+ }}
732
+ >
733
+ Votre email
734
+ </label>
735
+ <input
736
+ id="livechat-email"
737
+ type="email"
738
+ value={formEmail}
739
+ onChange={(e) => setFormEmail(e.target.value)}
740
+ placeholder="jean@exemple.fr"
741
+ required
742
+ style={{
743
+ width: '100%',
744
+ padding: '10px 12px',
745
+ borderRadius: 10,
746
+ border: '2px solid #000',
747
+ fontSize: 14,
748
+ color: '#000',
749
+ backgroundColor: '#fff',
750
+ outline: 'none',
751
+ boxSizing: 'border-box',
752
+ transition: 'box-shadow 0.15s',
753
+ }}
754
+ onFocus={(e) => {
755
+ e.currentTarget.style.boxShadow = '2px 2px 0px #00E5FF'
756
+ }}
757
+ onBlur={(e) => {
758
+ e.currentTarget.style.boxShadow = 'none'
759
+ }}
760
+ />
761
+ </div>
762
+
763
+ {error && (
764
+ <div
765
+ style={{
766
+ padding: '8px 12px',
767
+ borderRadius: 8,
768
+ border: '2px solid #FF4444',
769
+ backgroundColor: '#FFF0F0',
770
+ color: '#CC0000',
771
+ fontSize: 13,
772
+ marginBottom: 12,
773
+ fontWeight: 600,
774
+ }}
775
+ >
776
+ {error}
777
+ </div>
778
+ )}
779
+
780
+ <button
781
+ type="submit"
782
+ disabled={identifyLoading || !formEmail.trim() || !formName.trim()}
783
+ style={{
784
+ width: '100%',
785
+ padding: '12px 16px',
786
+ borderRadius: 12,
787
+ border: '3px solid #000',
788
+ backgroundColor: '#00E5FF',
789
+ color: '#000',
790
+ fontSize: 14,
791
+ fontWeight: 900,
792
+ cursor:
793
+ identifyLoading || !formEmail.trim() || !formName.trim()
794
+ ? 'not-allowed'
795
+ : 'pointer',
796
+ boxShadow: '4px 4px 0px #000',
797
+ transition: 'all 0.15s',
798
+ opacity:
799
+ identifyLoading || !formEmail.trim() || !formName.trim()
800
+ ? 0.5
801
+ : 1,
802
+ }}
803
+ onMouseEnter={(e) => {
804
+ if (!identifyLoading) {
805
+ e.currentTarget.style.transform = 'translate(2px, 2px)'
806
+ e.currentTarget.style.boxShadow = '2px 2px 0px #000'
807
+ }
808
+ }}
809
+ onMouseLeave={(e) => {
810
+ e.currentTarget.style.transform = 'translate(0, 0)'
811
+ e.currentTarget.style.boxShadow = '4px 4px 0px #000'
812
+ }}
813
+ >
814
+ {identifyLoading ? 'Connexion...' : 'Demarrer le chat'}
815
+ </button>
816
+ </form>
817
+ </div>
818
+ )}
819
+
820
+ {/* ── Rating Screen ── */}
821
+ {screen === 'rating' && (
822
+ <div style={{ padding: 24, flex: 1 }}>
823
+ {ratingSubmitted ? (
824
+ <div
825
+ style={{
826
+ textAlign: 'center',
827
+ padding: '32px 0',
828
+ }}
829
+ >
830
+ <div
831
+ style={{
832
+ width: 56,
833
+ height: 56,
834
+ borderRadius: 16,
835
+ border: '3px solid #000',
836
+ backgroundColor: '#22c55e',
837
+ boxShadow: '3px 3px 0px #000',
838
+ display: 'inline-flex',
839
+ alignItems: 'center',
840
+ justifyContent: 'center',
841
+ marginBottom: 16,
842
+ color: '#fff',
843
+ fontSize: 28,
844
+ }}
845
+ >
846
+
847
+ </div>
848
+ <p
849
+ style={{
850
+ fontSize: 16,
851
+ fontWeight: 900,
852
+ color: '#000',
853
+ margin: '0 0 6px',
854
+ }}
855
+ >
856
+ Merci pour votre avis !
857
+ </p>
858
+ <p style={{ fontSize: 13, color: '#666', margin: 0 }}>
859
+ Votre retour nous aide a nous ameliorer.
860
+ </p>
861
+ </div>
862
+ ) : (
863
+ <>
864
+ <div style={{ textAlign: 'center', marginBottom: 20 }}>
865
+ <p
866
+ style={{
867
+ fontSize: 16,
868
+ fontWeight: 900,
869
+ color: '#000',
870
+ margin: '0 0 6px',
871
+ }}
872
+ >
873
+ Comment etait cet echange ?
874
+ </p>
875
+ <p style={{ fontSize: 13, color: '#666', margin: 0 }}>
876
+ Notez votre experience (optionnel)
877
+ </p>
878
+ </div>
879
+
880
+ {/* Stars */}
881
+ <div
882
+ style={{
883
+ display: 'flex',
884
+ justifyContent: 'center',
885
+ gap: 8,
886
+ marginBottom: 20,
887
+ }}
888
+ >
889
+ {[1, 2, 3, 4, 5].map((star) => (
890
+ <button
891
+ key={star}
892
+ type="button"
893
+ onClick={() => setRating(star)}
894
+ onMouseEnter={() => setRatingHover(star)}
895
+ onMouseLeave={() => setRatingHover(0)}
896
+ aria-label={`${star} etoile${star > 1 ? 's' : ''}`}
897
+ style={{
898
+ background: 'none',
899
+ border: 'none',
900
+ cursor: 'pointer',
901
+ padding: 4,
902
+ fontSize: 32,
903
+ lineHeight: 1,
904
+ color:
905
+ star <= (ratingHover || rating) ? '#FFD600' : '#E5E5E5',
906
+ transition: 'color 0.15s, transform 0.15s',
907
+ transform:
908
+ star <= ratingHover ? 'scale(1.2)' : 'scale(1)',
909
+ filter:
910
+ star <= (ratingHover || rating)
911
+ ? 'drop-shadow(1px 1px 0px #000)'
912
+ : 'none',
913
+ }}
914
+ >
915
+
916
+ </button>
917
+ ))}
918
+ </div>
919
+
920
+ {/* Comment */}
921
+ <textarea
922
+ value={ratingComment}
923
+ onChange={(e) => setRatingComment(e.target.value)}
924
+ placeholder="Un commentaire ? (optionnel)"
925
+ maxLength={500}
926
+ rows={3}
927
+ style={{
928
+ width: '100%',
929
+ padding: '10px 12px',
930
+ borderRadius: 10,
931
+ border: '2px solid #000',
932
+ fontSize: 13,
933
+ color: '#000',
934
+ backgroundColor: '#fff',
935
+ outline: 'none',
936
+ resize: 'vertical',
937
+ boxSizing: 'border-box',
938
+ fontFamily: 'inherit',
939
+ transition: 'box-shadow 0.15s',
940
+ }}
941
+ onFocus={(e) => {
942
+ e.currentTarget.style.boxShadow = '2px 2px 0px #00E5FF'
943
+ }}
944
+ onBlur={(e) => {
945
+ e.currentTarget.style.boxShadow = 'none'
946
+ }}
947
+ />
948
+
949
+ {/* Actions */}
950
+ <div
951
+ style={{
952
+ display: 'flex',
953
+ flexDirection: 'column',
954
+ gap: 8,
955
+ marginTop: 16,
956
+ }}
957
+ >
958
+ <button
959
+ type="button"
960
+ onClick={handleSubmitRating}
961
+ disabled={rating === 0 && !ratingComment.trim()}
962
+ style={{
963
+ width: '100%',
964
+ padding: '12px 16px',
965
+ borderRadius: 12,
966
+ border: '3px solid #000',
967
+ backgroundColor:
968
+ rating === 0 && !ratingComment.trim()
969
+ ? '#E5E5E5'
970
+ : '#00E5FF',
971
+ color: '#000',
972
+ fontSize: 14,
973
+ fontWeight: 900,
974
+ cursor:
975
+ rating === 0 && !ratingComment.trim()
976
+ ? 'not-allowed'
977
+ : 'pointer',
978
+ boxShadow: '4px 4px 0px #000',
979
+ transition: 'all 0.15s',
980
+ opacity:
981
+ rating === 0 && !ratingComment.trim() ? 0.5 : 1,
982
+ }}
983
+ onMouseEnter={(e) => {
984
+ if (rating > 0 || ratingComment.trim()) {
985
+ e.currentTarget.style.transform = 'translate(2px, 2px)'
986
+ e.currentTarget.style.boxShadow = '2px 2px 0px #000'
987
+ }
988
+ }}
989
+ onMouseLeave={(e) => {
990
+ e.currentTarget.style.transform = 'translate(0, 0)'
991
+ e.currentTarget.style.boxShadow = '4px 4px 0px #000'
992
+ }}
993
+ >
994
+ Envoyer mon avis
995
+ </button>
996
+ <button
997
+ type="button"
998
+ onClick={handleSkipRating}
999
+ style={{
1000
+ background: 'none',
1001
+ border: 'none',
1002
+ cursor: 'pointer',
1003
+ fontSize: 13,
1004
+ color: '#999',
1005
+ textDecoration: 'underline',
1006
+ padding: '6px 0',
1007
+ fontWeight: 600,
1008
+ }}
1009
+ >
1010
+ Passer et fermer
1011
+ </button>
1012
+ </div>
1013
+ </>
1014
+ )}
1015
+ </div>
1016
+ )}
1017
+
1018
+ {/* ── Chat Screen ── */}
1019
+ {screen === 'chat' && (
1020
+ <>
1021
+ {/* Messages area */}
1022
+ <div
1023
+ style={{
1024
+ flex: 1,
1025
+ overflowY: 'auto',
1026
+ padding: 16,
1027
+ display: 'flex',
1028
+ flexDirection: 'column',
1029
+ gap: 8,
1030
+ }}
1031
+ >
1032
+ {messages.length === 0 && !session?.sessionId && (
1033
+ <div
1034
+ style={{
1035
+ flex: 1,
1036
+ display: 'flex',
1037
+ flexDirection: 'column',
1038
+ alignItems: 'center',
1039
+ justifyContent: 'center',
1040
+ textAlign: 'center',
1041
+ padding: '20px 0',
1042
+ }}
1043
+ >
1044
+ <div
1045
+ style={{
1046
+ width: 48,
1047
+ height: 48,
1048
+ borderRadius: 14,
1049
+ border: '2px solid #000',
1050
+ backgroundColor: '#FEFCE8',
1051
+ display: 'flex',
1052
+ alignItems: 'center',
1053
+ justifyContent: 'center',
1054
+ marginBottom: 12,
1055
+ boxShadow: '2px 2px 0px #000',
1056
+ }}
1057
+ >
1058
+ <Send size={20} color="#000" />
1059
+ </div>
1060
+ <p
1061
+ style={{
1062
+ fontSize: 14,
1063
+ fontWeight: 700,
1064
+ color: '#000',
1065
+ margin: '0 0 4px',
1066
+ }}
1067
+ >
1068
+ Bonjour {session?.name?.split(' ')[0]} !
1069
+ </p>
1070
+ <p
1071
+ style={{
1072
+ fontSize: 13,
1073
+ color: '#666',
1074
+ margin: 0,
1075
+ lineHeight: 1.5,
1076
+ }}
1077
+ >
1078
+ Ecrivez votre message ci-dessous.
1079
+ </p>
1080
+ </div>
1081
+ )}
1082
+
1083
+ {messages.length === 0 && session?.sessionId && (
1084
+ <div
1085
+ style={{
1086
+ textAlign: 'center',
1087
+ padding: 20,
1088
+ color: '#999',
1089
+ fontSize: 13,
1090
+ }}
1091
+ >
1092
+ Chargement des messages...
1093
+ </div>
1094
+ )}
1095
+
1096
+ {messages.map((msg) => {
1097
+ const isClient = msg.senderType === 'client'
1098
+ const isSystem = msg.senderType === 'system'
1099
+
1100
+ // System messages are centered
1101
+ if (isSystem) {
1102
+ return (
1103
+ <div
1104
+ key={msg.id}
1105
+ style={{
1106
+ display: 'flex',
1107
+ justifyContent: 'center',
1108
+ }}
1109
+ >
1110
+ <div
1111
+ style={{
1112
+ padding: '6px 14px',
1113
+ borderRadius: 8,
1114
+ backgroundColor: '#F5F5F5',
1115
+ border: '1px solid #E5E5E5',
1116
+ color: '#888',
1117
+ fontSize: 12,
1118
+ fontStyle: 'italic',
1119
+ textAlign: 'center',
1120
+ maxWidth: '90%',
1121
+ }}
1122
+ >
1123
+ {msg.body}
1124
+ <div
1125
+ style={{
1126
+ fontSize: 10,
1127
+ color: '#bbb',
1128
+ marginTop: 2,
1129
+ }}
1130
+ >
1131
+ {new Date(msg.createdAt).toLocaleTimeString('fr-FR', {
1132
+ hour: '2-digit',
1133
+ minute: '2-digit',
1134
+ })}
1135
+ </div>
1136
+ </div>
1137
+ </div>
1138
+ )
1139
+ }
1140
+
1141
+ return (
1142
+ <div
1143
+ key={msg.id}
1144
+ style={{
1145
+ display: 'flex',
1146
+ justifyContent: isClient ? 'flex-end' : 'flex-start',
1147
+ }}
1148
+ >
1149
+ <div
1150
+ style={{
1151
+ maxWidth: '80%',
1152
+ padding: '10px 14px',
1153
+ borderRadius: isClient
1154
+ ? '14px 14px 4px 14px'
1155
+ : '14px 14px 14px 4px',
1156
+ backgroundColor: isClient ? '#00E5FF' : '#F5F5F5',
1157
+ border: `2px solid ${isClient ? '#000' : '#E5E5E5'}`,
1158
+ color: '#000',
1159
+ }}
1160
+ >
1161
+ {!isClient && (
1162
+ <div
1163
+ style={{
1164
+ fontSize: 11,
1165
+ fontWeight: 800,
1166
+ color: '#FF8A00',
1167
+ marginBottom: 4,
1168
+ textTransform: 'uppercase',
1169
+ }}
1170
+ >
1171
+ Support
1172
+ </div>
1173
+ )}
1174
+ <div
1175
+ style={{
1176
+ fontSize: 13,
1177
+ lineHeight: 1.5,
1178
+ whiteSpace: 'pre-wrap',
1179
+ wordBreak: 'break-word',
1180
+ }}
1181
+ >
1182
+ {msg.body}
1183
+ </div>
1184
+ <div
1185
+ style={{
1186
+ fontSize: 10,
1187
+ color: isClient ? 'rgba(0,0,0,0.4)' : '#999',
1188
+ marginTop: 4,
1189
+ textAlign: 'right',
1190
+ }}
1191
+ >
1192
+ {new Date(msg.createdAt).toLocaleTimeString('fr-FR', {
1193
+ hour: '2-digit',
1194
+ minute: '2-digit',
1195
+ })}
1196
+ </div>
1197
+ </div>
1198
+ </div>
1199
+ )
1200
+ })}
1201
+ <div ref={messagesEndRef} />
1202
+ </div>
1203
+
1204
+ {/* Error banner */}
1205
+ {error && (
1206
+ <div
1207
+ style={{
1208
+ padding: '8px 16px',
1209
+ backgroundColor: '#FFF0F0',
1210
+ borderTop: '2px solid #FF4444',
1211
+ color: '#CC0000',
1212
+ fontSize: 12,
1213
+ fontWeight: 600,
1214
+ display: 'flex',
1215
+ justifyContent: 'space-between',
1216
+ alignItems: 'center',
1217
+ }}
1218
+ >
1219
+ <span>{error}</span>
1220
+ <button
1221
+ onClick={() => setError(null)}
1222
+ style={{
1223
+ background: 'none',
1224
+ border: 'none',
1225
+ cursor: 'pointer',
1226
+ color: '#CC0000',
1227
+ padding: 2,
1228
+ display: 'flex',
1229
+ }}
1230
+ >
1231
+ <X size={14} />
1232
+ </button>
1233
+ </div>
1234
+ )}
1235
+
1236
+ {/* Input area */}
1237
+ <div
1238
+ style={{
1239
+ borderTop: '3px solid #000',
1240
+ padding: 12,
1241
+ backgroundColor: '#fff',
1242
+ }}
1243
+ >
1244
+ <form
1245
+ onSubmit={handleSendMessage}
1246
+ style={{ display: 'flex', gap: 8 }}
1247
+ >
1248
+ <input
1249
+ ref={inputRef}
1250
+ type="text"
1251
+ value={input}
1252
+ onChange={(e) => setInput(e.target.value)}
1253
+ placeholder="Votre message..."
1254
+ maxLength={2000}
1255
+ style={{
1256
+ flex: 1,
1257
+ padding: '10px 12px',
1258
+ borderRadius: 10,
1259
+ border: '2px solid #000',
1260
+ fontSize: 14,
1261
+ color: '#000',
1262
+ backgroundColor: '#fff',
1263
+ outline: 'none',
1264
+ transition: 'box-shadow 0.15s',
1265
+ }}
1266
+ onFocus={(e) => {
1267
+ e.currentTarget.style.boxShadow = '2px 2px 0px #00E5FF'
1268
+ }}
1269
+ onBlur={(e) => {
1270
+ e.currentTarget.style.boxShadow = 'none'
1271
+ }}
1272
+ autoFocus
1273
+ />
1274
+ <button
1275
+ type="submit"
1276
+ disabled={!input.trim() || sending}
1277
+ aria-label="Envoyer"
1278
+ style={{
1279
+ width: 42,
1280
+ height: 42,
1281
+ borderRadius: 10,
1282
+ border: '2px solid #000',
1283
+ backgroundColor:
1284
+ !input.trim() || sending ? '#E5E5E5' : '#00E5FF',
1285
+ color: '#000',
1286
+ cursor:
1287
+ !input.trim() || sending ? 'not-allowed' : 'pointer',
1288
+ display: 'flex',
1289
+ alignItems: 'center',
1290
+ justifyContent: 'center',
1291
+ flexShrink: 0,
1292
+ transition: 'all 0.15s',
1293
+ opacity: !input.trim() || sending ? 0.5 : 1,
1294
+ }}
1295
+ >
1296
+ <Send size={18} strokeWidth={2.5} />
1297
+ </button>
1298
+ </form>
1299
+
1300
+ {/* Session info + actions */}
1301
+ {session?.sessionId && (
1302
+ <div
1303
+ style={{
1304
+ display: 'flex',
1305
+ justifyContent: 'space-between',
1306
+ alignItems: 'center',
1307
+ marginTop: 8,
1308
+ paddingTop: 8,
1309
+ borderTop: '1px solid #E5E5E5',
1310
+ }}
1311
+ >
1312
+ <span style={{ fontSize: 11, color: '#999' }}>
1313
+ {session.email}
1314
+ </span>
1315
+ <div style={{ display: 'flex', gap: 12 }}>
1316
+ <button
1317
+ onClick={handleEndChat}
1318
+ style={{
1319
+ background: 'none',
1320
+ border: 'none',
1321
+ cursor: 'pointer',
1322
+ fontSize: 11,
1323
+ fontWeight: 700,
1324
+ color: '#FF4444',
1325
+ padding: 0,
1326
+ }}
1327
+ >
1328
+ Terminer
1329
+ </button>
1330
+ <button
1331
+ onClick={handleNewConversation}
1332
+ style={{
1333
+ background: 'none',
1334
+ border: 'none',
1335
+ cursor: 'pointer',
1336
+ fontSize: 11,
1337
+ fontWeight: 700,
1338
+ color: '#00E5FF',
1339
+ textDecoration: 'underline',
1340
+ padding: 0,
1341
+ }}
1342
+ >
1343
+ Nouveau sujet
1344
+ </button>
1345
+ </div>
1346
+ </div>
1347
+ )}
1348
+ </div>
1349
+ </>
1350
+ )}
1351
+ </div>
1352
+ )
1353
+ }