@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.
- package/LICENSE +21 -0
- package/README.md +525 -0
- package/dist/client.cjs +7 -0
- package/dist/client.d.cts +3 -0
- package/dist/client.d.ts +3 -0
- package/dist/client.js +5 -0
- package/dist/index.cjs +7766 -0
- package/dist/index.d.cts +384 -0
- package/dist/index.d.ts +384 -0
- package/dist/index.js +7730 -0
- package/dist/views.d.cts +30 -0
- package/dist/views.d.ts +30 -0
- package/package.json +131 -0
- package/src/client.ts +1 -0
- package/src/collections/AuthLogs.ts +65 -0
- package/src/collections/CannedResponses.ts +69 -0
- package/src/collections/ChatMessages.ts +98 -0
- package/src/collections/EmailLogs.ts +94 -0
- package/src/collections/KnowledgeBase.ts +99 -0
- package/src/collections/Macros.ts +98 -0
- package/src/collections/PendingEmails.ts +122 -0
- package/src/collections/SatisfactionSurveys.ts +98 -0
- package/src/collections/SlaPolicies.ts +123 -0
- package/src/collections/SupportClients.ts +210 -0
- package/src/collections/TicketActivityLog.ts +81 -0
- package/src/collections/TicketMessages.ts +364 -0
- package/src/collections/TicketStatuses.ts +108 -0
- package/src/collections/Tickets.ts +704 -0
- package/src/collections/TimeEntries.ts +105 -0
- package/src/collections/WebhookEndpoints.ts +96 -0
- package/src/collections/index.ts +16 -0
- package/src/components/TicketConversation/components/AISummaryPanel.tsx +85 -0
- package/src/components/TicketConversation/components/ActionPanels.tsx +140 -0
- package/src/components/TicketConversation/components/ActivityLog.tsx +39 -0
- package/src/components/TicketConversation/components/ClientBar.tsx +37 -0
- package/src/components/TicketConversation/components/ClientHistory.tsx +117 -0
- package/src/components/TicketConversation/components/CodeBlock.tsx +186 -0
- package/src/components/TicketConversation/components/CodeBlockInserter.tsx +166 -0
- package/src/components/TicketConversation/components/QuickActions.tsx +82 -0
- package/src/components/TicketConversation/components/TicketHeader.tsx +91 -0
- package/src/components/TicketConversation/components/TimeTrackingPanel.tsx +161 -0
- package/src/components/TicketConversation/config.ts +82 -0
- package/src/components/TicketConversation/constants.ts +74 -0
- package/src/components/TicketConversation/context.ts +63 -0
- package/src/components/TicketConversation/hooks/useAI.ts +180 -0
- package/src/components/TicketConversation/hooks/useMessageActions.ts +131 -0
- package/src/components/TicketConversation/hooks/useReply.ts +190 -0
- package/src/components/TicketConversation/hooks/useTicketActions.ts +205 -0
- package/src/components/TicketConversation/hooks/useTimeTracking.ts +107 -0
- package/src/components/TicketConversation/hooks/useTranslation.ts +116 -0
- package/src/components/TicketConversation/index.tsx +1110 -0
- package/src/components/TicketConversation/locales/en.json +878 -0
- package/src/components/TicketConversation/locales/fr.json +878 -0
- package/src/components/TicketConversation/types.ts +54 -0
- package/src/components/TicketConversation/utils.ts +25 -0
- package/src/endpoints/admin-chat-stream.ts +238 -0
- package/src/endpoints/admin-chat.ts +263 -0
- package/src/endpoints/admin-stats.ts +200 -0
- package/src/endpoints/ai.ts +199 -0
- package/src/endpoints/apply-macro.ts +144 -0
- package/src/endpoints/auth-2fa.ts +163 -0
- package/src/endpoints/auto-close.ts +175 -0
- package/src/endpoints/billing.ts +167 -0
- package/src/endpoints/bulk-action.ts +103 -0
- package/src/endpoints/chat-stream.ts +127 -0
- package/src/endpoints/chat.ts +188 -0
- package/src/endpoints/chatbot.ts +113 -0
- package/src/endpoints/delete-account.ts +129 -0
- package/src/endpoints/email-stats.ts +109 -0
- package/src/endpoints/export-csv.ts +84 -0
- package/src/endpoints/export-data.ts +104 -0
- package/src/endpoints/import-conversation.ts +307 -0
- package/src/endpoints/index.ts +154 -0
- package/src/endpoints/login.ts +92 -0
- package/src/endpoints/merge-clients.ts +132 -0
- package/src/endpoints/merge-tickets.ts +137 -0
- package/src/endpoints/oauth-google.ts +179 -0
- package/src/endpoints/pending-emails-process.ts +224 -0
- package/src/endpoints/presence.ts +104 -0
- package/src/endpoints/process-scheduled.ts +144 -0
- package/src/endpoints/purge-logs.ts +58 -0
- package/src/endpoints/resend-notification.ts +99 -0
- package/src/endpoints/round-robin-config.ts +92 -0
- package/src/endpoints/satisfaction.ts +93 -0
- package/src/endpoints/search.ts +106 -0
- package/src/endpoints/seed-kb.ts +153 -0
- package/src/endpoints/settings.ts +144 -0
- package/src/endpoints/signature.ts +93 -0
- package/src/endpoints/sla-check.ts +124 -0
- package/src/endpoints/split-ticket.ts +131 -0
- package/src/endpoints/statuses.ts +45 -0
- package/src/endpoints/track-open.ts +154 -0
- package/src/endpoints/typing.ts +101 -0
- package/src/endpoints/user-prefs.ts +125 -0
- package/src/hooks/checkSLA.ts +414 -0
- package/src/hooks/ticketStatusEmail.ts +182 -0
- package/src/index.ts +51 -0
- package/src/plugin.ts +157 -0
- package/src/portal/LiveChat.tsx +1353 -0
- package/src/portal/auth/ChatWidget.tsx +350 -0
- package/src/portal/auth/ChatbotWidget.tsx +285 -0
- package/src/portal/auth/SupportHeader.tsx +409 -0
- package/src/portal/auth/dashboard/DashboardClient.tsx +650 -0
- package/src/portal/auth/dashboard/page.tsx +84 -0
- package/src/portal/auth/faq/FAQSearch.tsx +117 -0
- package/src/portal/auth/faq/page.tsx +199 -0
- package/src/portal/auth/layout.tsx +61 -0
- package/src/portal/auth/profile/page.tsx +705 -0
- package/src/portal/auth/tickets/detail/CloseTicketButton.tsx +74 -0
- package/src/portal/auth/tickets/detail/CollapsibleMessages.tsx +46 -0
- package/src/portal/auth/tickets/detail/MarkSolutionButton.tsx +50 -0
- package/src/portal/auth/tickets/detail/MessageActions.tsx +158 -0
- package/src/portal/auth/tickets/detail/PrintButton.tsx +16 -0
- package/src/portal/auth/tickets/detail/ReadReceipt.tsx +34 -0
- package/src/portal/auth/tickets/detail/ReopenTicketButton.tsx +74 -0
- package/src/portal/auth/tickets/detail/SatisfactionForm.tsx +156 -0
- package/src/portal/auth/tickets/detail/TicketPolling.tsx +57 -0
- package/src/portal/auth/tickets/detail/TicketReplyForm.tsx +294 -0
- package/src/portal/auth/tickets/detail/TypingIndicator.tsx +58 -0
- package/src/portal/auth/tickets/detail/page.tsx +738 -0
- package/src/portal/auth/tickets/new/page.tsx +515 -0
- package/src/portal/forgot-password/page.tsx +114 -0
- package/src/portal/layout.tsx +26 -0
- package/src/portal/locales/en.json +374 -0
- package/src/portal/locales/fr.json +374 -0
- package/src/portal/login/page.tsx +351 -0
- package/src/portal/page.tsx +162 -0
- package/src/portal/register/page.tsx +281 -0
- package/src/portal/reset-password/page.tsx +152 -0
- package/src/styles/BillingView.module.scss +311 -0
- package/src/styles/ChatView.module.scss +438 -0
- package/src/styles/CommandPalette.module.scss +160 -0
- package/src/styles/CrmView.module.scss +554 -0
- package/src/styles/EmailTracking.module.scss +238 -0
- package/src/styles/ImportConversation.module.scss +267 -0
- package/src/styles/Layout.module.scss +55 -0
- package/src/styles/Logs.module.scss +164 -0
- package/src/styles/NewTicket.module.scss +143 -0
- package/src/styles/PendingEmails.module.scss +629 -0
- package/src/styles/SupportDashboard.module.scss +649 -0
- package/src/styles/TicketDetail.module.scss +1043 -0
- package/src/styles/TicketInbox.module.scss +296 -0
- package/src/styles/TicketingSettings.module.scss +358 -0
- package/src/styles/TimeDashboard.module.scss +287 -0
- package/src/styles/_tokens.scss +78 -0
- package/src/styles/theme.css +633 -0
- package/src/types.ts +255 -0
- package/src/utils/adminNotification.ts +38 -0
- package/src/utils/auth.ts +46 -0
- package/src/utils/emailTemplate.ts +343 -0
- package/src/utils/fireWebhooks.ts +84 -0
- package/src/utils/index.ts +22 -0
- package/src/utils/rateLimiter.ts +52 -0
- package/src/utils/readSettings.ts +67 -0
- package/src/utils/slugs.ts +54 -0
- package/src/utils/webhookDispatcher.ts +120 -0
- package/src/views/BillingView/client.tsx +137 -0
- package/src/views/BillingView/index.tsx +33 -0
- package/src/views/ChatView/client.tsx +294 -0
- package/src/views/ChatView/index.tsx +33 -0
- package/src/views/CrmView/client.tsx +206 -0
- package/src/views/CrmView/index.tsx +33 -0
- package/src/views/EmailTrackingView/client.tsx +124 -0
- package/src/views/EmailTrackingView/index.tsx +33 -0
- package/src/views/ImportConversationView/client.tsx +133 -0
- package/src/views/ImportConversationView/index.tsx +33 -0
- package/src/views/LogsView/client.tsx +151 -0
- package/src/views/LogsView/index.tsx +30 -0
- package/src/views/NewTicketView/client.tsx +227 -0
- package/src/views/NewTicketView/index.tsx +30 -0
- package/src/views/PendingEmailsView/client.tsx +177 -0
- package/src/views/PendingEmailsView/index.tsx +33 -0
- package/src/views/SupportDashboardView/client.tsx +424 -0
- package/src/views/SupportDashboardView/index.tsx +33 -0
- package/src/views/TicketDetailView/client.tsx +775 -0
- package/src/views/TicketDetailView/index.tsx +33 -0
- package/src/views/TicketInboxView/client.tsx +313 -0
- package/src/views/TicketInboxView/index.tsx +30 -0
- package/src/views/TicketingSettingsView/client.tsx +866 -0
- package/src/views/TicketingSettingsView/index.tsx +33 -0
- package/src/views/TimeDashboardView/client.tsx +144 -0
- package/src/views/TimeDashboardView/index.tsx +33 -0
- package/src/views/shared/AdminViewHeader.tsx +69 -0
- package/src/views/shared/ErrorBoundary.tsx +68 -0
- package/src/views/shared/Skeleton.tsx +125 -0
- package/src/views/shared/adminTokens.ts +37 -0
- package/src/views/shared/config.ts +82 -0
- package/src/views/shared/index.ts +6 -0
- 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'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
|
+
}
|