simple-support-chat 0.1.1 → 0.2.0
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/README.md +302 -3
- package/dist/client/index.cjs +52 -5
- package/dist/client/index.cjs.map +1 -1
- package/dist/client/index.d.cts +13 -5
- package/dist/client/index.d.ts +13 -5
- package/dist/client/index.js +53 -6
- package/dist/client/index.js.map +1 -1
- package/dist/server/index.cjs +2176 -9
- package/dist/server/index.cjs.map +1 -1
- package/dist/server/index.d.cts +189 -1
- package/dist/server/index.d.ts +189 -1
- package/dist/server/index.js +2166 -10
- package/dist/server/index.js.map +1 -1
- package/package.json +87 -85
package/README.md
CHANGED
|
@@ -4,6 +4,8 @@ Embeddable chat widget SDK that routes customer messages to Slack threads. Open-
|
|
|
4
4
|
|
|
5
5
|
- Zero cost, self-hosted
|
|
6
6
|
- Slack-native: messages appear as threads in your workspace
|
|
7
|
+
- Bidirectional: team replies in Slack threads show in the chat widget
|
|
8
|
+
- Slack emoji shortcodes (`:heart:`) auto-converted to Unicode (❤️)
|
|
7
9
|
- Works with any React app (Next.js, Remix, Vite, etc.)
|
|
8
10
|
- Anonymous and authenticated user support
|
|
9
11
|
- No external CSS or Tailwind dependency
|
|
@@ -70,8 +72,11 @@ Add these to your `.env` file:
|
|
|
70
72
|
```bash
|
|
71
73
|
SLACK_BOT_TOKEN=xoxb-your-token-here
|
|
72
74
|
SLACK_CHANNEL_ID=C0123ABCDEF
|
|
75
|
+
SLACK_SIGNING_SECRET=your-signing-secret-here # Required for receiving replies (see "Receiving Replies" section)
|
|
73
76
|
```
|
|
74
77
|
|
|
78
|
+
The signing secret is found under **Basic Information** > **App Credentials** in your Slack app settings. It is only needed if you want bidirectional messaging (team replies appearing in the widget).
|
|
79
|
+
|
|
75
80
|
### Validate Your Token
|
|
76
81
|
|
|
77
82
|
Use the built-in `validateSlackToken` utility to verify your token works before going live:
|
|
@@ -128,6 +133,280 @@ export default function App() {
|
|
|
128
133
|
|
|
129
134
|
That's it! Messages from your users will appear as threaded conversations in your Slack channel.
|
|
130
135
|
|
|
136
|
+
## Receiving Replies
|
|
137
|
+
|
|
138
|
+
By default, simple-support-chat is one-way: users send messages that appear in Slack. To make it bidirectional -- so team replies in Slack appear in the user's chat widget -- you need three additional pieces:
|
|
139
|
+
|
|
140
|
+
1. A **webhook route** that receives events from Slack when someone replies in a thread
|
|
141
|
+
2. A **replies endpoint** that the client polls for new replies
|
|
142
|
+
3. The `repliesUrl` prop on the client widget
|
|
143
|
+
|
|
144
|
+
### Prerequisites
|
|
145
|
+
|
|
146
|
+
- Completed the [Slack App Setup](#slack-app-setup) above
|
|
147
|
+
- Your Slack app's **Signing Secret** (found under **Basic Information** > **App Credentials** in [api.slack.com/apps](https://api.slack.com/apps))
|
|
148
|
+
|
|
149
|
+
Add the signing secret to your environment variables:
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
SLACK_SIGNING_SECRET=your-signing-secret-here
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Step 1: Enable Event Subscriptions
|
|
156
|
+
|
|
157
|
+
1. Go to your app at [api.slack.com/apps](https://api.slack.com/apps)
|
|
158
|
+
2. Navigate to **Event Subscriptions** in the sidebar
|
|
159
|
+
3. Toggle **Enable Events** to On
|
|
160
|
+
4. Set the **Request URL** to `https://your-domain.com/api/support/webhook`
|
|
161
|
+
- Slack will send a verification challenge -- your webhook handler responds automatically
|
|
162
|
+
5. Under **Subscribe to bot events**, add `message.channels`
|
|
163
|
+
6. Click **Save Changes**
|
|
164
|
+
|
|
165
|
+
A pre-built manifest is included at [`slack-app-manifest.json`](./slack-app-manifest.json). Replace `YOUR_DOMAIN` with your actual domain and use it when creating or reconfiguring your Slack app.
|
|
166
|
+
|
|
167
|
+
> **Local development:** Slack cannot reach `localhost`. Use a tunnel like [ngrok](https://ngrok.com/) (`ngrok http 3000`) and set the tunnel URL as the Request URL. You can skip webhook setup entirely for local dev -- one-way messaging works without it.
|
|
168
|
+
|
|
169
|
+
### Step 2: Create a Shared Store
|
|
170
|
+
|
|
171
|
+
The webhook handler, replies handler, and message handler must share the same store instance so that thread mappings and replies are consistent.
|
|
172
|
+
|
|
173
|
+
```ts
|
|
174
|
+
// lib/support-store.ts (or wherever you keep shared singletons)
|
|
175
|
+
import { InMemoryStore } from "simple-support-chat/server";
|
|
176
|
+
|
|
177
|
+
export const store = new InMemoryStore();
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
`InMemoryStore` works for local development. For production, implement the `SupportChatStore` interface with your database (see [Custom Storage](#custom-storage-supportchatstore) below).
|
|
181
|
+
|
|
182
|
+
### Step 3: Set Up Routes
|
|
183
|
+
|
|
184
|
+
#### Next.js App Router
|
|
185
|
+
|
|
186
|
+
```ts
|
|
187
|
+
// app/api/support/route.ts
|
|
188
|
+
import { createSupportHandler } from "simple-support-chat/server";
|
|
189
|
+
import { store } from "@/lib/support-store";
|
|
190
|
+
|
|
191
|
+
export const POST = createSupportHandler({
|
|
192
|
+
slackBotToken: process.env.SLACK_BOT_TOKEN!,
|
|
193
|
+
slackChannel: process.env.SLACK_CHANNEL_ID!,
|
|
194
|
+
store,
|
|
195
|
+
});
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
```ts
|
|
199
|
+
// app/api/support/webhook/route.ts
|
|
200
|
+
import { createWebhookHandler } from "simple-support-chat/server";
|
|
201
|
+
import { store } from "@/lib/support-store";
|
|
202
|
+
|
|
203
|
+
export const POST = createWebhookHandler({
|
|
204
|
+
store,
|
|
205
|
+
signingSecret: process.env.SLACK_SIGNING_SECRET!,
|
|
206
|
+
});
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
```ts
|
|
210
|
+
// app/api/support/replies/route.ts
|
|
211
|
+
import { createRepliesHandler } from "simple-support-chat/server";
|
|
212
|
+
import { store } from "@/lib/support-store";
|
|
213
|
+
|
|
214
|
+
export const GET = createRepliesHandler({ store });
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
#### Express
|
|
218
|
+
|
|
219
|
+
```ts
|
|
220
|
+
import express from "express";
|
|
221
|
+
import {
|
|
222
|
+
createExpressHandler,
|
|
223
|
+
createExpressWebhookHandler,
|
|
224
|
+
createExpressRepliesHandler,
|
|
225
|
+
InMemoryStore,
|
|
226
|
+
} from "simple-support-chat/server";
|
|
227
|
+
|
|
228
|
+
const app = express();
|
|
229
|
+
const store = new InMemoryStore();
|
|
230
|
+
|
|
231
|
+
// Message handler (uses JSON body parser)
|
|
232
|
+
app.post(
|
|
233
|
+
"/api/support",
|
|
234
|
+
express.json(),
|
|
235
|
+
createExpressHandler({
|
|
236
|
+
slackBotToken: process.env.SLACK_BOT_TOKEN!,
|
|
237
|
+
slackChannel: process.env.SLACK_CHANNEL_ID!,
|
|
238
|
+
store,
|
|
239
|
+
}),
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
// Webhook handler (uses raw body parser for signature verification)
|
|
243
|
+
app.post(
|
|
244
|
+
"/api/support/webhook",
|
|
245
|
+
express.raw({ type: "application/json" }),
|
|
246
|
+
createExpressWebhookHandler({
|
|
247
|
+
store,
|
|
248
|
+
signingSecret: process.env.SLACK_SIGNING_SECRET!,
|
|
249
|
+
}),
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
// Replies handler (GET endpoint for client polling)
|
|
253
|
+
app.get(
|
|
254
|
+
"/api/support/replies",
|
|
255
|
+
createExpressRepliesHandler({ store }),
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
app.listen(3000);
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
> **Important:** The webhook route must use `express.raw()` (not `express.json()`) so that the raw request body is available for Slack signature verification.
|
|
262
|
+
|
|
263
|
+
### Step 4: Add repliesUrl to the Client
|
|
264
|
+
|
|
265
|
+
Pass the `repliesUrl` prop to enable reply polling:
|
|
266
|
+
|
|
267
|
+
```tsx
|
|
268
|
+
<ChatBubble
|
|
269
|
+
apiUrl="/api/support"
|
|
270
|
+
repliesUrl="/api/support/replies"
|
|
271
|
+
/>
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
Or with the modal:
|
|
275
|
+
|
|
276
|
+
```tsx
|
|
277
|
+
<SupportChatModal
|
|
278
|
+
apiUrl="/api/support"
|
|
279
|
+
repliesUrl="/api/support/replies"
|
|
280
|
+
isOpen={isOpen}
|
|
281
|
+
onClose={close}
|
|
282
|
+
/>
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
When `repliesUrl` is set, the widget polls every 4 seconds while the chat panel is open. Replies appear as left-aligned gray bubbles. Polling stops when the chat is closed.
|
|
286
|
+
|
|
287
|
+
### Custom Storage (SupportChatStore)
|
|
288
|
+
|
|
289
|
+
For production, implement the `SupportChatStore` interface to persist thread mappings and replies to your database. Here is the interface:
|
|
290
|
+
|
|
291
|
+
```ts
|
|
292
|
+
interface SupportChatStore {
|
|
293
|
+
saveThread(sessionId: string, threadTs: string, metadata?: Record<string, unknown>): Promise<void>;
|
|
294
|
+
getThreadBySession(sessionId: string): Promise<ThreadRecord | null>;
|
|
295
|
+
getThreadByTs(threadTs: string): Promise<ThreadRecord | null>;
|
|
296
|
+
saveReply(sessionId: string, reply: Reply): Promise<void>;
|
|
297
|
+
getReplies(sessionId: string, since?: string): Promise<Reply[]>;
|
|
298
|
+
}
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
#### Example: Supabase Implementation
|
|
302
|
+
|
|
303
|
+
First, create the tables:
|
|
304
|
+
|
|
305
|
+
```sql
|
|
306
|
+
-- Supabase migration
|
|
307
|
+
create table support_chat_threads (
|
|
308
|
+
session_id text primary key,
|
|
309
|
+
thread_ts text not null unique,
|
|
310
|
+
metadata jsonb default '{}',
|
|
311
|
+
created_at timestamptz default now()
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
create table support_chat_replies (
|
|
315
|
+
id text primary key,
|
|
316
|
+
session_id text not null references support_chat_threads(session_id),
|
|
317
|
+
text text not null,
|
|
318
|
+
sender text not null,
|
|
319
|
+
timestamp timestamptz not null,
|
|
320
|
+
thread_ts text not null
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
create index idx_replies_session_timestamp
|
|
324
|
+
on support_chat_replies(session_id, timestamp);
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
Then implement the store:
|
|
328
|
+
|
|
329
|
+
```ts
|
|
330
|
+
import type { SupportChatStore, ThreadRecord, Reply } from "simple-support-chat/server";
|
|
331
|
+
import { createClient } from "@supabase/supabase-js";
|
|
332
|
+
|
|
333
|
+
const supabase = createClient(
|
|
334
|
+
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
|
335
|
+
process.env.SUPABASE_SERVICE_ROLE_KEY!,
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
export class SupabaseChatStore implements SupportChatStore {
|
|
339
|
+
async saveThread(sessionId: string, threadTs: string, metadata?: Record<string, unknown>) {
|
|
340
|
+
await supabase.from("support_chat_threads").upsert({
|
|
341
|
+
session_id: sessionId,
|
|
342
|
+
thread_ts: threadTs,
|
|
343
|
+
metadata: metadata ?? {},
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
async getThreadBySession(sessionId: string): Promise<ThreadRecord | null> {
|
|
348
|
+
const { data } = await supabase
|
|
349
|
+
.from("support_chat_threads")
|
|
350
|
+
.select("*")
|
|
351
|
+
.eq("session_id", sessionId)
|
|
352
|
+
.single();
|
|
353
|
+
if (!data) return null;
|
|
354
|
+
return { sessionId: data.session_id, threadTs: data.thread_ts, metadata: data.metadata };
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
async getThreadByTs(threadTs: string): Promise<ThreadRecord | null> {
|
|
358
|
+
const { data } = await supabase
|
|
359
|
+
.from("support_chat_threads")
|
|
360
|
+
.select("*")
|
|
361
|
+
.eq("thread_ts", threadTs)
|
|
362
|
+
.single();
|
|
363
|
+
if (!data) return null;
|
|
364
|
+
return { sessionId: data.session_id, threadTs: data.thread_ts, metadata: data.metadata };
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
async saveReply(sessionId: string, reply: Reply) {
|
|
368
|
+
await supabase.from("support_chat_replies").insert({
|
|
369
|
+
id: reply.id,
|
|
370
|
+
session_id: sessionId,
|
|
371
|
+
text: reply.text,
|
|
372
|
+
sender: reply.sender,
|
|
373
|
+
timestamp: reply.timestamp,
|
|
374
|
+
thread_ts: reply.threadTs,
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
async getReplies(sessionId: string, since?: string): Promise<Reply[]> {
|
|
379
|
+
let query = supabase
|
|
380
|
+
.from("support_chat_replies")
|
|
381
|
+
.select("*")
|
|
382
|
+
.eq("session_id", sessionId)
|
|
383
|
+
.order("timestamp", { ascending: true });
|
|
384
|
+
|
|
385
|
+
if (since) {
|
|
386
|
+
query = query.gt("timestamp", since);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const { data } = await query;
|
|
390
|
+
return (data ?? []).map((r) => ({
|
|
391
|
+
id: r.id,
|
|
392
|
+
text: r.text,
|
|
393
|
+
sender: r.sender,
|
|
394
|
+
timestamp: r.timestamp,
|
|
395
|
+
threadTs: r.thread_ts,
|
|
396
|
+
}));
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
Then pass the store to all handlers:
|
|
402
|
+
|
|
403
|
+
```ts
|
|
404
|
+
import { SupabaseChatStore } from "@/lib/supabase-chat-store";
|
|
405
|
+
|
|
406
|
+
const store = new SupabaseChatStore();
|
|
407
|
+
// Use this store instance in createSupportHandler, createWebhookHandler, and createRepliesHandler
|
|
408
|
+
```
|
|
409
|
+
|
|
131
410
|
## Configuration
|
|
132
411
|
|
|
133
412
|
### ChatBubble Props
|
|
@@ -135,6 +414,7 @@ That's it! Messages from your users will appear as threaded conversations in you
|
|
|
135
414
|
| Prop | Type | Default | Description |
|
|
136
415
|
|------|------|---------|-------------|
|
|
137
416
|
| `apiUrl` | `string` | (required) | URL of your support API endpoint |
|
|
417
|
+
| `repliesUrl` | `string` | `undefined` | URL of the replies endpoint. Enables bidirectional chat. |
|
|
138
418
|
| `position` | `'bottom-right' \| 'bottom-left' \| 'top-right' \| 'top-left'` | `'bottom-right'` | Bubble position |
|
|
139
419
|
| `color` | `string` | `'#2563eb'` | Primary color (bubble, header, sent messages) |
|
|
140
420
|
| `title` | `string` | `'Support'` | Chat panel header title |
|
|
@@ -151,6 +431,7 @@ That's it! Messages from your users will appear as threaded conversations in you
|
|
|
151
431
|
| `botName` | `string` | Custom bot display name |
|
|
152
432
|
| `botIcon` | `string` | Bot icon emoji (e.g., `:speech_balloon:`) |
|
|
153
433
|
| `onMessage` | `(data) => void` | Callback on each message |
|
|
434
|
+
| `store` | `SupportChatStore` | Pluggable storage backend (defaults to `InMemoryStore`) |
|
|
154
435
|
|
|
155
436
|
## Identity Integration
|
|
156
437
|
|
|
@@ -211,6 +492,7 @@ The modal renders centered on desktop (max-width 500px) with a backdrop overlay,
|
|
|
211
492
|
| Prop | Type | Default | Description |
|
|
212
493
|
|------|------|---------|-------------|
|
|
213
494
|
| `apiUrl` | `string` | (required) | URL of your support API endpoint |
|
|
495
|
+
| `repliesUrl` | `string` | `undefined` | URL of the replies endpoint. Enables bidirectional chat. |
|
|
214
496
|
| `isOpen` | `boolean` | (required) | Whether the modal is open |
|
|
215
497
|
| `onClose` | `() => void` | (required) | Callback to close the modal |
|
|
216
498
|
| `color` | `string` | `'#2563eb'` | Primary color |
|
|
@@ -264,9 +546,16 @@ The Express handler uses the same threading logic as the Web API handler. The re
|
|
|
264
546
|
|
|
265
547
|
| Export | Description |
|
|
266
548
|
|--------|-------------|
|
|
267
|
-
| `createSupportHandler(options)` | Returns a Web API `(Request) => Promise<Response>` handler |
|
|
268
|
-
| `createExpressHandler(options)` | Returns an Express `(req, res) => Promise<void>` handler |
|
|
549
|
+
| `createSupportHandler(options)` | Returns a Web API `(Request) => Promise<Response>` handler for messages |
|
|
550
|
+
| `createExpressHandler(options)` | Returns an Express `(req, res) => Promise<void>` handler for messages |
|
|
551
|
+
| `createWebhookHandler(options)` | Returns a Web API handler for Slack event webhooks |
|
|
552
|
+
| `createExpressWebhookHandler(options)` | Returns an Express handler for Slack event webhooks |
|
|
553
|
+
| `createRepliesHandler(options)` | Returns a Web API handler for the replies polling endpoint |
|
|
554
|
+
| `createExpressRepliesHandler(options)` | Returns an Express handler for the replies polling endpoint |
|
|
555
|
+
| `verifySlackSignature(secret, sig, ts, body)` | Verifies a Slack request signature |
|
|
556
|
+
| `InMemoryStore` | Default in-memory implementation of `SupportChatStore` |
|
|
269
557
|
| `validateSlackToken(token)` | Validates a Slack token via `auth.test` |
|
|
558
|
+
| `emojify(text)` | Converts Slack emoji shortcodes (`:heart:`) to Unicode (❤️) |
|
|
270
559
|
|
|
271
560
|
### Client Exports (`simple-support-chat`)
|
|
272
561
|
|
|
@@ -288,7 +577,17 @@ All TypeScript types are exported from both entry points:
|
|
|
288
577
|
import type { ChatBubbleProps, ChatUser, SupportChatModalProps, ChatMessage } from "simple-support-chat";
|
|
289
578
|
|
|
290
579
|
// Server types
|
|
291
|
-
import type {
|
|
580
|
+
import type {
|
|
581
|
+
SupportHandlerOptions,
|
|
582
|
+
IncomingMessage,
|
|
583
|
+
HandlerResponse,
|
|
584
|
+
SupportChatStore,
|
|
585
|
+
Reply,
|
|
586
|
+
ThreadRecord,
|
|
587
|
+
WebhookHandlerOptions,
|
|
588
|
+
RepliesHandlerOptions,
|
|
589
|
+
RepliesResponse,
|
|
590
|
+
} from "simple-support-chat/server";
|
|
292
591
|
```
|
|
293
592
|
|
|
294
593
|
## Development
|
package/dist/client/index.cjs
CHANGED
|
@@ -50,13 +50,18 @@ function collectAnonymousContext() {
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
// src/client/useChatEngine.ts
|
|
53
|
+
var POLL_INTERVAL = 4e3;
|
|
53
54
|
function useChatEngine({
|
|
54
55
|
apiUrl,
|
|
55
|
-
user
|
|
56
|
+
user,
|
|
57
|
+
repliesUrl,
|
|
58
|
+
isOpen = true
|
|
56
59
|
}) {
|
|
57
60
|
const [messages, setMessages] = react.useState([]);
|
|
58
61
|
const [input, setInput] = react.useState("");
|
|
59
62
|
const [sending, setSending] = react.useState(false);
|
|
63
|
+
const lastReplyTimestampRef = react.useRef(null);
|
|
64
|
+
const knownReplyIdsRef = react.useRef(/* @__PURE__ */ new Set());
|
|
60
65
|
const sendMessage = react.useCallback(async () => {
|
|
61
66
|
const text = input.trim();
|
|
62
67
|
if (!text || sending) return;
|
|
@@ -115,6 +120,44 @@ function useChatEngine({
|
|
|
115
120
|
},
|
|
116
121
|
[sendMessage]
|
|
117
122
|
);
|
|
123
|
+
react.useEffect(() => {
|
|
124
|
+
if (!repliesUrl || !isOpen) return;
|
|
125
|
+
const sessionId = user?.id ?? getSessionId();
|
|
126
|
+
const fetchReplies = async () => {
|
|
127
|
+
try {
|
|
128
|
+
const params = new URLSearchParams({ sessionId });
|
|
129
|
+
if (lastReplyTimestampRef.current) {
|
|
130
|
+
params.set("since", lastReplyTimestampRef.current);
|
|
131
|
+
}
|
|
132
|
+
const response = await fetch(`${repliesUrl}?${params.toString()}`);
|
|
133
|
+
if (!response.ok) return;
|
|
134
|
+
const data = await response.json();
|
|
135
|
+
if (data.replies.length === 0) return;
|
|
136
|
+
const newReplies = data.replies.filter(
|
|
137
|
+
(r) => !knownReplyIdsRef.current.has(r.id)
|
|
138
|
+
);
|
|
139
|
+
if (newReplies.length === 0) return;
|
|
140
|
+
for (const r of newReplies) {
|
|
141
|
+
knownReplyIdsRef.current.add(r.id);
|
|
142
|
+
}
|
|
143
|
+
const latestTimestamp = newReplies.reduce((latest, r) => {
|
|
144
|
+
return r.timestamp > latest ? r.timestamp : latest;
|
|
145
|
+
}, lastReplyTimestampRef.current ?? "");
|
|
146
|
+
lastReplyTimestampRef.current = latestTimestamp;
|
|
147
|
+
const replyMessages = newReplies.map((r) => ({
|
|
148
|
+
id: r.id,
|
|
149
|
+
text: r.text,
|
|
150
|
+
sender: "received",
|
|
151
|
+
timestamp: new Date(r.timestamp).getTime()
|
|
152
|
+
}));
|
|
153
|
+
setMessages((prev) => [...prev, ...replyMessages]);
|
|
154
|
+
} catch {
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
void fetchReplies();
|
|
158
|
+
const intervalId = setInterval(() => void fetchReplies(), POLL_INTERVAL);
|
|
159
|
+
return () => clearInterval(intervalId);
|
|
160
|
+
}, [repliesUrl, isOpen, user]);
|
|
118
161
|
return { messages, input, setInput, sending, sendMessage, handleKeyDown };
|
|
119
162
|
}
|
|
120
163
|
function useColorScheme() {
|
|
@@ -184,12 +227,13 @@ function ChatBubble({
|
|
|
184
227
|
title = "Support",
|
|
185
228
|
placeholder = "Type a message...",
|
|
186
229
|
show = true,
|
|
187
|
-
user
|
|
230
|
+
user,
|
|
231
|
+
repliesUrl
|
|
188
232
|
}) {
|
|
189
233
|
const [isOpen, setIsOpen] = react.useState(false);
|
|
190
234
|
const colorScheme = useColorScheme();
|
|
191
235
|
const theme = react.useMemo(() => getThemeTokens(colorScheme), [colorScheme]);
|
|
192
|
-
const { messages, input, setInput, sending, sendMessage, handleKeyDown } = useChatEngine({ apiUrl, user });
|
|
236
|
+
const { messages, input, setInput, sending, sendMessage, handleKeyDown } = useChatEngine({ apiUrl, user, repliesUrl, isOpen });
|
|
193
237
|
const [panelState, setPanelState] = react.useState(
|
|
194
238
|
"closed"
|
|
195
239
|
);
|
|
@@ -414,6 +458,7 @@ function ChatBubble({
|
|
|
414
458
|
messages.map((msg) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
415
459
|
"div",
|
|
416
460
|
{
|
|
461
|
+
"data-sender": msg.sender,
|
|
417
462
|
style: {
|
|
418
463
|
alignSelf: msg.sender === "user" ? "flex-end" : "flex-start",
|
|
419
464
|
maxWidth: "80%",
|
|
@@ -527,9 +572,10 @@ function SupportChatModal({
|
|
|
527
572
|
color = "#2563eb",
|
|
528
573
|
title = "Contact Us",
|
|
529
574
|
placeholder = "Type a message...",
|
|
530
|
-
user
|
|
575
|
+
user,
|
|
576
|
+
repliesUrl
|
|
531
577
|
}) {
|
|
532
|
-
const { messages, input, setInput, sending, sendMessage, handleKeyDown } = useChatEngine({ apiUrl, user });
|
|
578
|
+
const { messages, input, setInput, sending, sendMessage, handleKeyDown } = useChatEngine({ apiUrl, user, repliesUrl, isOpen });
|
|
533
579
|
const colorScheme = useColorScheme();
|
|
534
580
|
const theme = react.useMemo(() => getThemeTokens(colorScheme), [colorScheme]);
|
|
535
581
|
const modalRef = react.useRef(null);
|
|
@@ -721,6 +767,7 @@ function SupportChatModal({
|
|
|
721
767
|
messages.map((msg) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
722
768
|
"div",
|
|
723
769
|
{
|
|
770
|
+
"data-sender": msg.sender,
|
|
724
771
|
style: {
|
|
725
772
|
alignSelf: msg.sender === "user" ? "flex-end" : "flex-start",
|
|
726
773
|
maxWidth: "80%",
|