@dilipod/ui 0.4.24 → 0.4.25
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/dist/components/support-chat.d.ts +28 -0
- package/dist/components/support-chat.d.ts.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +131 -0
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +132 -2
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/components/support-chat.tsx +236 -0
- package/src/index.ts +4 -0
package/package.json
CHANGED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useRef, useCallback, ReactNode } from 'react'
|
|
4
|
+
import { Button } from './button'
|
|
5
|
+
import { Textarea } from './textarea'
|
|
6
|
+
import {
|
|
7
|
+
CircleNotch,
|
|
8
|
+
PaperPlaneTilt,
|
|
9
|
+
} from '@phosphor-icons/react'
|
|
10
|
+
|
|
11
|
+
// ============================================
|
|
12
|
+
// Types
|
|
13
|
+
// ============================================
|
|
14
|
+
|
|
15
|
+
export interface SupportChatMessage {
|
|
16
|
+
id: string
|
|
17
|
+
role: 'customer' | 'admin' | 'system'
|
|
18
|
+
content: string
|
|
19
|
+
type: 'message' | 'event' | 'suggestion'
|
|
20
|
+
created_at: string
|
|
21
|
+
metadata?: Record<string, unknown>
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface SupportChatProps {
|
|
25
|
+
/** Fetch messages from this endpoint */
|
|
26
|
+
messagesEndpoint: string
|
|
27
|
+
/** POST new messages to this endpoint */
|
|
28
|
+
sendEndpoint: string
|
|
29
|
+
/** Role of the current user */
|
|
30
|
+
currentRole: 'customer' | 'admin'
|
|
31
|
+
/** Poll interval in ms (0 to disable). Default: 5000 */
|
|
32
|
+
pollInterval?: number
|
|
33
|
+
/** Placeholder for the input */
|
|
34
|
+
placeholder?: string
|
|
35
|
+
/** Custom empty state */
|
|
36
|
+
emptyMessage?: string | ReactNode
|
|
37
|
+
/** Additional CSS class */
|
|
38
|
+
className?: string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ============================================
|
|
42
|
+
// Helpers
|
|
43
|
+
// ============================================
|
|
44
|
+
|
|
45
|
+
function formatMessageTime(dateStr: string): string {
|
|
46
|
+
const d = new Date(dateStr)
|
|
47
|
+
const now = new Date()
|
|
48
|
+
const diffMs = now.getTime() - d.getTime()
|
|
49
|
+
const diffMins = Math.floor(diffMs / 60000)
|
|
50
|
+
const diffHours = Math.floor(diffMs / 3600000)
|
|
51
|
+
|
|
52
|
+
if (diffMins < 1) return 'just now'
|
|
53
|
+
if (diffMins < 60) return `${diffMins}m ago`
|
|
54
|
+
if (diffHours < 24) return `${diffHours}h ago`
|
|
55
|
+
|
|
56
|
+
return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' })
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ============================================
|
|
60
|
+
// Component
|
|
61
|
+
// ============================================
|
|
62
|
+
|
|
63
|
+
export function SupportChat({
|
|
64
|
+
messagesEndpoint,
|
|
65
|
+
sendEndpoint,
|
|
66
|
+
currentRole,
|
|
67
|
+
pollInterval = 5000,
|
|
68
|
+
placeholder = 'Type a message...',
|
|
69
|
+
emptyMessage = 'No messages yet. Start the conversation!',
|
|
70
|
+
className = '',
|
|
71
|
+
}: SupportChatProps) {
|
|
72
|
+
const [messages, setMessages] = useState<SupportChatMessage[]>([])
|
|
73
|
+
const [loading, setLoading] = useState(true)
|
|
74
|
+
const [sending, setSending] = useState(false)
|
|
75
|
+
const [inputValue, setInputValue] = useState('')
|
|
76
|
+
const scrollRef = useRef<HTMLDivElement>(null)
|
|
77
|
+
const prevMessageCountRef = useRef(0)
|
|
78
|
+
|
|
79
|
+
const fetchMessages = useCallback(async () => {
|
|
80
|
+
try {
|
|
81
|
+
const res = await fetch(messagesEndpoint)
|
|
82
|
+
if (!res.ok) return
|
|
83
|
+
const data = await res.json()
|
|
84
|
+
setMessages(data.messages || [])
|
|
85
|
+
} catch {
|
|
86
|
+
// Silently fail
|
|
87
|
+
} finally {
|
|
88
|
+
setLoading(false)
|
|
89
|
+
}
|
|
90
|
+
}, [messagesEndpoint])
|
|
91
|
+
|
|
92
|
+
// Initial fetch + polling
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
fetchMessages()
|
|
95
|
+
if (pollInterval > 0) {
|
|
96
|
+
const interval = setInterval(fetchMessages, pollInterval)
|
|
97
|
+
return () => clearInterval(interval)
|
|
98
|
+
}
|
|
99
|
+
}, [fetchMessages, pollInterval])
|
|
100
|
+
|
|
101
|
+
// Auto-scroll when new messages arrive
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
if (messages.length > prevMessageCountRef.current) {
|
|
104
|
+
scrollRef.current?.scrollTo({ top: scrollRef.current.scrollHeight, behavior: 'smooth' })
|
|
105
|
+
}
|
|
106
|
+
prevMessageCountRef.current = messages.length
|
|
107
|
+
}, [messages.length])
|
|
108
|
+
|
|
109
|
+
const handleSend = async () => {
|
|
110
|
+
const content = inputValue.trim()
|
|
111
|
+
if (!content || sending) return
|
|
112
|
+
|
|
113
|
+
setSending(true)
|
|
114
|
+
setInputValue('')
|
|
115
|
+
|
|
116
|
+
// Optimistic update
|
|
117
|
+
const optimisticMsg: SupportChatMessage = {
|
|
118
|
+
id: `temp-${Date.now()}`,
|
|
119
|
+
role: currentRole,
|
|
120
|
+
content,
|
|
121
|
+
type: 'message',
|
|
122
|
+
created_at: new Date().toISOString(),
|
|
123
|
+
}
|
|
124
|
+
setMessages((prev) => [...prev, optimisticMsg])
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
const res = await fetch(sendEndpoint, {
|
|
128
|
+
method: 'POST',
|
|
129
|
+
headers: { 'Content-Type': 'application/json' },
|
|
130
|
+
body: JSON.stringify({ content }),
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
if (res.ok) {
|
|
134
|
+
// Replace optimistic message with real one
|
|
135
|
+
await fetchMessages()
|
|
136
|
+
} else {
|
|
137
|
+
// Remove optimistic message on failure
|
|
138
|
+
setMessages((prev) => prev.filter((m) => m.id !== optimisticMsg.id))
|
|
139
|
+
}
|
|
140
|
+
} catch {
|
|
141
|
+
setMessages((prev) => prev.filter((m) => m.id !== optimisticMsg.id))
|
|
142
|
+
} finally {
|
|
143
|
+
setSending(false)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
148
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
149
|
+
e.preventDefault()
|
|
150
|
+
handleSend()
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (loading) {
|
|
155
|
+
return (
|
|
156
|
+
<div className={`flex items-center justify-center py-8 ${className}`}>
|
|
157
|
+
<CircleNotch className="w-5 h-5 animate-spin text-muted-foreground" />
|
|
158
|
+
</div>
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return (
|
|
163
|
+
<div className={`flex flex-col ${className}`}>
|
|
164
|
+
{/* Messages area */}
|
|
165
|
+
<div ref={scrollRef} className="flex-1 overflow-y-auto space-y-3 mb-4 max-h-[400px] min-h-[200px] px-1">
|
|
166
|
+
{messages.length === 0 ? (
|
|
167
|
+
<div className="flex items-center justify-center h-full text-sm text-muted-foreground py-8">
|
|
168
|
+
{typeof emptyMessage === 'string' ? <p>{emptyMessage}</p> : emptyMessage}
|
|
169
|
+
</div>
|
|
170
|
+
) : (
|
|
171
|
+
messages.map((msg) => {
|
|
172
|
+
// System events — centered, subtle
|
|
173
|
+
if (msg.type === 'event' || msg.role === 'system') {
|
|
174
|
+
return (
|
|
175
|
+
<div key={msg.id} className="flex justify-center">
|
|
176
|
+
<div className="bg-gray-50 border border-gray-100 rounded-full px-3 py-1 text-xs text-muted-foreground max-w-[90%] text-center">
|
|
177
|
+
{msg.content}
|
|
178
|
+
<span className="ml-2 opacity-60">{formatMessageTime(msg.created_at)}</span>
|
|
179
|
+
</div>
|
|
180
|
+
</div>
|
|
181
|
+
)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Regular messages
|
|
185
|
+
const isCurrentUser = msg.role === currentRole
|
|
186
|
+
return (
|
|
187
|
+
<div key={msg.id} className={`flex ${isCurrentUser ? 'justify-end' : 'justify-start'}`}>
|
|
188
|
+
<div className={`max-w-[80%] rounded-lg px-3 py-2 ${
|
|
189
|
+
isCurrentUser
|
|
190
|
+
? 'bg-[var(--cyan)] text-white'
|
|
191
|
+
: 'bg-gray-100 text-[var(--black)]'
|
|
192
|
+
}`}>
|
|
193
|
+
{!isCurrentUser && (
|
|
194
|
+
<p className="text-xs font-medium mb-0.5 opacity-70">
|
|
195
|
+
{msg.role === 'admin' ? 'Dilipod Team' : 'You'}
|
|
196
|
+
</p>
|
|
197
|
+
)}
|
|
198
|
+
<p className="text-sm whitespace-pre-wrap">{msg.content}</p>
|
|
199
|
+
<p className={`text-[10px] mt-1 ${isCurrentUser ? 'text-white/60' : 'text-muted-foreground'}`}>
|
|
200
|
+
{formatMessageTime(msg.created_at)}
|
|
201
|
+
</p>
|
|
202
|
+
</div>
|
|
203
|
+
</div>
|
|
204
|
+
)
|
|
205
|
+
})
|
|
206
|
+
)}
|
|
207
|
+
</div>
|
|
208
|
+
|
|
209
|
+
{/* Input area */}
|
|
210
|
+
<div className="flex gap-2 border-t border-gray-100 pt-3">
|
|
211
|
+
<Textarea
|
|
212
|
+
value={inputValue}
|
|
213
|
+
onChange={(e) => setInputValue(e.target.value)}
|
|
214
|
+
onKeyDown={handleKeyDown}
|
|
215
|
+
placeholder={placeholder}
|
|
216
|
+
rows={1}
|
|
217
|
+
className="resize-none min-h-[36px] py-2 flex-1"
|
|
218
|
+
/>
|
|
219
|
+
<Button
|
|
220
|
+
onClick={handleSend}
|
|
221
|
+
disabled={sending || !inputValue.trim()}
|
|
222
|
+
size="sm"
|
|
223
|
+
className="flex-shrink-0 h-9"
|
|
224
|
+
>
|
|
225
|
+
{sending ? (
|
|
226
|
+
<CircleNotch className="w-4 h-4 animate-spin" />
|
|
227
|
+
) : (
|
|
228
|
+
<PaperPlaneTilt className="w-4 h-4" weight="bold" />
|
|
229
|
+
)}
|
|
230
|
+
</Button>
|
|
231
|
+
</div>
|
|
232
|
+
</div>
|
|
233
|
+
)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export default SupportChat
|
package/src/index.ts
CHANGED
|
@@ -281,6 +281,10 @@ export type { ImpactMetrics, ImpactMetricsFormProps } from './components/impact-
|
|
|
281
281
|
export { ActivityTimeline } from './components/activity-timeline'
|
|
282
282
|
export type { Activity, ActivityTimelineProps } from './components/activity-timeline'
|
|
283
283
|
|
|
284
|
+
// Support Chat Components
|
|
285
|
+
export { SupportChat } from './components/support-chat'
|
|
286
|
+
export type { SupportChatMessage, SupportChatProps } from './components/support-chat'
|
|
287
|
+
|
|
284
288
|
// Workflow Components
|
|
285
289
|
export { WorkflowFlow } from './components/workflow-flow'
|
|
286
290
|
export type { WorkflowFlowProps, N8nWorkflow as WorkflowFlowN8nWorkflow, N8nNode } from './components/workflow-flow'
|