@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dilipod/ui",
3
- "version": "0.4.24",
3
+ "version": "0.4.25",
4
4
  "description": "Dilipod Design System - Shared UI components and styles",
5
5
  "author": "Dilipod <hello@dilipod.com>",
6
6
  "license": "MIT",
@@ -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'