simple-support-chat 0.1.0 → 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 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 { SupportHandlerOptions, IncomingMessage, HandlerResponse } from "simple-support-chat/server";
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
@@ -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,8 +120,89 @@ 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
  }
163
+ function useColorScheme() {
164
+ const [scheme, setScheme] = react.useState(() => {
165
+ if (typeof window === "undefined") return "light";
166
+ return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
167
+ });
168
+ react.useEffect(() => {
169
+ if (typeof window === "undefined") return;
170
+ const mq = window.matchMedia("(prefers-color-scheme: dark)");
171
+ const handler = (e) => {
172
+ setScheme(e.matches ? "dark" : "light");
173
+ };
174
+ mq.addEventListener("change", handler);
175
+ return () => mq.removeEventListener("change", handler);
176
+ }, []);
177
+ return scheme;
178
+ }
179
+ var lightTokens = {
180
+ panelBg: "#ffffff",
181
+ panelBorder: "#e5e7eb",
182
+ inputBg: "#ffffff",
183
+ inputBorder: "#d1d5db",
184
+ inputText: "#1f2937",
185
+ inputPlaceholder: "#9ca3af",
186
+ receivedBg: "#f3f4f6",
187
+ receivedText: "#1f2937",
188
+ emptyText: "#9ca3af",
189
+ inputAreaBorder: "#e5e7eb"
190
+ };
191
+ var darkTokens = {
192
+ panelBg: "#1f2937",
193
+ panelBorder: "#374151",
194
+ inputBg: "#111827",
195
+ inputBorder: "#4b5563",
196
+ inputText: "#f9fafb",
197
+ inputPlaceholder: "#9ca3af",
198
+ receivedBg: "#374151",
199
+ receivedText: "#f3f4f6",
200
+ emptyText: "#6b7280",
201
+ inputAreaBorder: "#374151"
202
+ };
203
+ function getThemeTokens(scheme) {
204
+ return scheme === "dark" ? darkTokens : lightTokens;
205
+ }
120
206
  function injectKeyframes() {
121
207
  if (typeof document === "undefined") return;
122
208
  if (document.querySelector("[data-support-chat-keyframes]")) return;
@@ -141,10 +227,13 @@ function ChatBubble({
141
227
  title = "Support",
142
228
  placeholder = "Type a message...",
143
229
  show = true,
144
- user
230
+ user,
231
+ repliesUrl
145
232
  }) {
146
233
  const [isOpen, setIsOpen] = react.useState(false);
147
- const { messages, input, setInput, sending, sendMessage, handleKeyDown } = useChatEngine({ apiUrl, user });
234
+ const colorScheme = useColorScheme();
235
+ const theme = react.useMemo(() => getThemeTokens(colorScheme), [colorScheme]);
236
+ const { messages, input, setInput, sending, sendMessage, handleKeyDown } = useChatEngine({ apiUrl, user, repliesUrl, isOpen });
148
237
  const [panelState, setPanelState] = react.useState(
149
238
  "closed"
150
239
  );
@@ -301,8 +390,8 @@ function ChatBubble({
301
390
  boxShadow: "0 8px 30px rgba(0,0,0,0.12)",
302
391
  fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
303
392
  fontSize: "14px",
304
- backgroundColor: "#fff",
305
- border: "1px solid #e5e7eb",
393
+ backgroundColor: theme.panelBg,
394
+ border: `1px solid ${theme.panelBorder}`,
306
395
  animation: panelState === "open" ? "sc-slide-in 0.25s ease-out forwards" : "sc-slide-out 0.2s ease-in forwards"
307
396
  },
308
397
  children: [
@@ -359,7 +448,7 @@ function ChatBubble({
359
448
  "div",
360
449
  {
361
450
  style: {
362
- color: "#9ca3af",
451
+ color: theme.emptyText,
363
452
  textAlign: "center",
364
453
  marginTop: "40px"
365
454
  },
@@ -369,13 +458,14 @@ function ChatBubble({
369
458
  messages.map((msg) => /* @__PURE__ */ jsxRuntime.jsx(
370
459
  "div",
371
460
  {
461
+ "data-sender": msg.sender,
372
462
  style: {
373
463
  alignSelf: msg.sender === "user" ? "flex-end" : "flex-start",
374
464
  maxWidth: "80%",
375
465
  padding: "10px 14px",
376
466
  borderRadius: msg.sender === "user" ? "16px 16px 4px 16px" : "16px 16px 16px 4px",
377
- backgroundColor: msg.sender === "user" ? color : "#f3f4f6",
378
- color: msg.sender === "user" ? "#fff" : "#1f2937",
467
+ backgroundColor: msg.sender === "user" ? color : theme.receivedBg,
468
+ color: msg.sender === "user" ? "#fff" : theme.receivedText,
379
469
  wordBreak: "break-word"
380
470
  },
381
471
  children: msg.text
@@ -390,7 +480,7 @@ function ChatBubble({
390
480
  "div",
391
481
  {
392
482
  style: {
393
- borderTop: "1px solid #e5e7eb",
483
+ borderTop: `1px solid ${theme.inputAreaBorder}`,
394
484
  padding: "12px",
395
485
  display: "flex",
396
486
  gap: "8px",
@@ -409,12 +499,14 @@ function ChatBubble({
409
499
  "aria-label": "Type your message",
410
500
  style: {
411
501
  flex: 1,
412
- border: "1px solid #d1d5db",
502
+ border: `1px solid ${theme.inputBorder}`,
413
503
  borderRadius: "8px",
414
504
  padding: "10px 12px",
415
505
  fontSize: "14px",
416
506
  outline: "none",
417
- fontFamily: "inherit"
507
+ fontFamily: "inherit",
508
+ backgroundColor: theme.inputBg,
509
+ color: theme.inputText
418
510
  }
419
511
  }
420
512
  ),
@@ -480,9 +572,12 @@ function SupportChatModal({
480
572
  color = "#2563eb",
481
573
  title = "Contact Us",
482
574
  placeholder = "Type a message...",
483
- user
575
+ user,
576
+ repliesUrl
484
577
  }) {
485
- const { messages, input, setInput, sending, sendMessage, handleKeyDown } = useChatEngine({ apiUrl, user });
578
+ const { messages, input, setInput, sending, sendMessage, handleKeyDown } = useChatEngine({ apiUrl, user, repliesUrl, isOpen });
579
+ const colorScheme = useColorScheme();
580
+ const theme = react.useMemo(() => getThemeTokens(colorScheme), [colorScheme]);
486
581
  const modalRef = react.useRef(null);
487
582
  const inputRef = react.useRef(null);
488
583
  const messagesEndRef = react.useRef(null);
@@ -600,7 +695,7 @@ function SupportChatModal({
600
695
  maxWidth: "calc(100vw - 32px)",
601
696
  height: "600px",
602
697
  maxHeight: "calc(100vh - 64px)",
603
- backgroundColor: "#fff",
698
+ backgroundColor: theme.panelBg,
604
699
  borderRadius: "16px",
605
700
  overflow: "hidden",
606
701
  display: "flex",
@@ -662,7 +757,7 @@ function SupportChatModal({
662
757
  "div",
663
758
  {
664
759
  style: {
665
- color: "#9ca3af",
760
+ color: theme.emptyText,
666
761
  textAlign: "center",
667
762
  marginTop: "60px"
668
763
  },
@@ -672,13 +767,14 @@ function SupportChatModal({
672
767
  messages.map((msg) => /* @__PURE__ */ jsxRuntime.jsx(
673
768
  "div",
674
769
  {
770
+ "data-sender": msg.sender,
675
771
  style: {
676
772
  alignSelf: msg.sender === "user" ? "flex-end" : "flex-start",
677
773
  maxWidth: "80%",
678
774
  padding: "10px 14px",
679
775
  borderRadius: msg.sender === "user" ? "16px 16px 4px 16px" : "16px 16px 16px 4px",
680
- backgroundColor: msg.sender === "user" ? color : "#f3f4f6",
681
- color: msg.sender === "user" ? "#fff" : "#1f2937",
776
+ backgroundColor: msg.sender === "user" ? color : theme.receivedBg,
777
+ color: msg.sender === "user" ? "#fff" : theme.receivedText,
682
778
  wordBreak: "break-word"
683
779
  },
684
780
  children: msg.text
@@ -693,7 +789,7 @@ function SupportChatModal({
693
789
  "div",
694
790
  {
695
791
  style: {
696
- borderTop: "1px solid #e5e7eb",
792
+ borderTop: `1px solid ${theme.inputAreaBorder}`,
697
793
  padding: "16px 20px",
698
794
  display: "flex",
699
795
  gap: "8px",
@@ -712,12 +808,14 @@ function SupportChatModal({
712
808
  "aria-label": "Type your message",
713
809
  style: {
714
810
  flex: 1,
715
- border: "1px solid #d1d5db",
811
+ border: `1px solid ${theme.inputBorder}`,
716
812
  borderRadius: "8px",
717
813
  padding: "12px 14px",
718
814
  fontSize: "14px",
719
815
  outline: "none",
720
- fontFamily: "inherit"
816
+ fontFamily: "inherit",
817
+ backgroundColor: theme.inputBg,
818
+ color: theme.inputText
721
819
  }
722
820
  }
723
821
  ),