@mordn/chat-widget 0.1.3 → 0.1.4
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 +285 -245
- package/dist/cli/init.js +120 -93
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -2,66 +2,210 @@
|
|
|
2
2
|
|
|
3
3
|
A customizable AI chat widget for React/Next.js applications with built-in conversation persistence.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Quick Start
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
|
|
8
|
+
# 1. Install the package
|
|
9
|
+
npm install @mordn/chat-widget drizzle-kit
|
|
10
|
+
|
|
11
|
+
# 2. Run the setup wizard
|
|
12
|
+
npx @mordn/chat-widget
|
|
9
13
|
```
|
|
10
14
|
|
|
15
|
+
The setup wizard creates all required files:
|
|
16
|
+
- API routes (`/api/chat/...`)
|
|
17
|
+
- `drizzle.config.ts`
|
|
18
|
+
- `.env.example`
|
|
19
|
+
|
|
11
20
|
## Requirements
|
|
12
21
|
|
|
13
22
|
- Next.js 14+ (App Router)
|
|
14
23
|
- React 18+
|
|
15
24
|
- PostgreSQL database (Supabase recommended)
|
|
16
25
|
- Tailwind CSS v4
|
|
17
|
-
- Vercel AI SDK
|
|
18
26
|
|
|
19
27
|
## Setup
|
|
20
28
|
|
|
21
29
|
### 1. Environment Variables
|
|
22
30
|
|
|
23
|
-
|
|
31
|
+
Copy `.env.example` to `.env.local` and fill in your credentials:
|
|
24
32
|
|
|
25
33
|
```env
|
|
34
|
+
# Database (Required)
|
|
26
35
|
DATABASE_URL="postgresql://postgres.xxx:[PASSWORD]@aws-0-region.pooler.supabase.com:6543/postgres"
|
|
36
|
+
|
|
37
|
+
# AI Provider (Required)
|
|
27
38
|
AI_GATEWAY_API_KEY="your-ai-gateway-key"
|
|
28
39
|
```
|
|
29
40
|
|
|
30
41
|
### 2. Database Setup
|
|
31
42
|
|
|
32
|
-
|
|
43
|
+
Push the schema to your database:
|
|
33
44
|
|
|
34
45
|
```bash
|
|
35
|
-
|
|
46
|
+
npx drizzle-kit push
|
|
36
47
|
```
|
|
37
48
|
|
|
38
|
-
|
|
49
|
+
### 3. Configure Your AI Model
|
|
50
|
+
|
|
51
|
+
Open `app/api/chat/route.ts` and update the config:
|
|
39
52
|
|
|
40
53
|
```typescript
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
out: './drizzle',
|
|
47
|
-
dialect: 'postgresql',
|
|
48
|
-
dbCredentials: {
|
|
49
|
-
url: process.env.DATABASE_URL!,
|
|
50
|
-
},
|
|
51
|
-
});
|
|
54
|
+
const DEVELOPER_CONFIG = {
|
|
55
|
+
model: 'openai/gpt-4o', // Your AI model
|
|
56
|
+
systemPrompt: 'You are a helpful assistant',
|
|
57
|
+
temperature: 0.7,
|
|
58
|
+
};
|
|
52
59
|
```
|
|
53
60
|
|
|
54
|
-
|
|
61
|
+
### 4. Add the Widget
|
|
55
62
|
|
|
56
|
-
```
|
|
57
|
-
|
|
63
|
+
```tsx
|
|
64
|
+
'use client';
|
|
65
|
+
|
|
66
|
+
import { ChatWidget } from '@mordn/chat-widget';
|
|
67
|
+
import '@mordn/chat-widget/styles.css';
|
|
68
|
+
|
|
69
|
+
export default function Page() {
|
|
70
|
+
return (
|
|
71
|
+
<ChatWidget
|
|
72
|
+
// Required
|
|
73
|
+
userId="user-123"
|
|
74
|
+
|
|
75
|
+
// Theme: 'light' | 'dark'
|
|
76
|
+
theme={{ mode: 'light' }}
|
|
77
|
+
|
|
78
|
+
// Feature toggles
|
|
79
|
+
features={{
|
|
80
|
+
fileUpload: false, // Requires Supabase Storage setup
|
|
81
|
+
}}
|
|
82
|
+
|
|
83
|
+
// Display options
|
|
84
|
+
display={{
|
|
85
|
+
defaultOpen: false, // Start with chat open
|
|
86
|
+
size: 'default', // 'compact' | 'default' | 'large' | 'full'
|
|
87
|
+
resizable: true, // Allow resizing
|
|
88
|
+
showToggleButton: true, // Show FAB toggle button
|
|
89
|
+
}}
|
|
90
|
+
|
|
91
|
+
// Starter prompts shown on empty chat
|
|
92
|
+
starterPrompts={[
|
|
93
|
+
{ title: "What can you help me with?" },
|
|
94
|
+
{ title: "How do I get started?" },
|
|
95
|
+
]}
|
|
96
|
+
/>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## File Uploads (Optional)
|
|
104
|
+
|
|
105
|
+
To enable image attachments, you need Supabase Storage and to enable the feature:
|
|
106
|
+
|
|
107
|
+
```tsx
|
|
108
|
+
<ChatWidget
|
|
109
|
+
userId="user-123"
|
|
110
|
+
features={{ fileUpload: true }}
|
|
111
|
+
/>
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### 1. Create Storage Bucket
|
|
115
|
+
|
|
116
|
+
1. Go to your [Supabase Dashboard](https://supabase.com/dashboard)
|
|
117
|
+
2. Navigate to **Storage** → **New Bucket**
|
|
118
|
+
3. Create a bucket named `chat-attachments`
|
|
119
|
+
4. Set it to **Public** (or configure RLS policies for private access)
|
|
120
|
+
|
|
121
|
+
### 2. Add Environment Variables
|
|
122
|
+
|
|
123
|
+
```env
|
|
124
|
+
NEXT_PUBLIC_SUPABASE_URL="https://your-project.supabase.co"
|
|
125
|
+
SUPABASE_SERVICE_ROLE_KEY="your-service-role-key"
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
You can find these in Supabase Dashboard → **Settings** → **API**
|
|
129
|
+
|
|
130
|
+
### 3. Create Upload Route
|
|
131
|
+
|
|
132
|
+
When running `npx @mordn/chat-widget`, select **Yes** when asked about the upload route.
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## Props
|
|
137
|
+
|
|
138
|
+
| Prop | Type | Default | Description |
|
|
139
|
+
|------|------|---------|-------------|
|
|
140
|
+
| `userId` | `string` | **required** | User identifier for storing conversations |
|
|
141
|
+
| `conversationId` | `string` | - | Load a specific conversation |
|
|
142
|
+
| `initialMessages` | `array` | - | Pre-fill the chat with messages |
|
|
143
|
+
| `className` | `string` | - | Additional CSS classes |
|
|
144
|
+
| `theme` | `ThemeConfig` | - | Theme configuration |
|
|
145
|
+
| `features` | `FeatureConfig` | - | Feature toggles |
|
|
146
|
+
| `display` | `DisplayConfig` | - | Display options |
|
|
147
|
+
|
|
148
|
+
### ThemeConfig
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
{
|
|
152
|
+
mode?: 'light' | 'dark';
|
|
153
|
+
}
|
|
58
154
|
```
|
|
59
155
|
|
|
60
|
-
###
|
|
156
|
+
### FeatureConfig
|
|
61
157
|
|
|
62
|
-
|
|
158
|
+
```typescript
|
|
159
|
+
{
|
|
160
|
+
fileUpload?: boolean; // Enable file attachments (default: false)
|
|
161
|
+
webSearch?: boolean; // Enable web search toggle
|
|
162
|
+
}
|
|
163
|
+
```
|
|
63
164
|
|
|
64
|
-
|
|
165
|
+
### DisplayConfig
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
{
|
|
169
|
+
width?: string; // e.g., '400px' or '30vw'
|
|
170
|
+
defaultOpen?: boolean; // Start with chat open (default: false)
|
|
171
|
+
showToggleButton?: boolean; // Show FAB toggle button (default: true)
|
|
172
|
+
toggleButtonPosition?: {
|
|
173
|
+
bottom?: string;
|
|
174
|
+
right?: string;
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## Exports
|
|
182
|
+
|
|
183
|
+
```typescript
|
|
184
|
+
// Main component
|
|
185
|
+
import { ChatWidget } from '@mordn/chat-widget';
|
|
186
|
+
import '@mordn/chat-widget/styles.css';
|
|
187
|
+
|
|
188
|
+
// Database utilities (server-side only)
|
|
189
|
+
import {
|
|
190
|
+
db,
|
|
191
|
+
conversations,
|
|
192
|
+
messages,
|
|
193
|
+
createChat,
|
|
194
|
+
loadChat,
|
|
195
|
+
saveChat,
|
|
196
|
+
getConversations,
|
|
197
|
+
deleteConversation,
|
|
198
|
+
updateConversationTitle,
|
|
199
|
+
eq, and, or, desc, asc, sql
|
|
200
|
+
} from '@mordn/chat-widget/api';
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
## Generated Files Reference
|
|
206
|
+
|
|
207
|
+
<details>
|
|
208
|
+
<summary><strong>app/api/chat/route.ts</strong> - Main Chat Endpoint</summary>
|
|
65
209
|
|
|
66
210
|
```typescript
|
|
67
211
|
import { saveChat, updateConversationTitle, db, conversations, messages, eq } from '@mordn/chat-widget/api';
|
|
@@ -69,123 +213,128 @@ import { convertToModelMessages, streamText, UIMessage } from 'ai';
|
|
|
69
213
|
|
|
70
214
|
export const maxDuration = 30;
|
|
71
215
|
|
|
72
|
-
// DEVELOPER CONFIG - Set these for your app
|
|
73
216
|
const DEVELOPER_CONFIG = {
|
|
74
|
-
model: 'openai/gpt-4o',
|
|
217
|
+
model: 'openai/gpt-4o',
|
|
75
218
|
systemPrompt: 'You are a helpful assistant',
|
|
76
219
|
temperature: 0.7,
|
|
77
220
|
};
|
|
78
221
|
|
|
79
222
|
export async function POST(req: Request) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
if (!userId) {
|
|
84
|
-
return new Response('userId is required in X-User-Id header', { status: 400 });
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const chatMessages: UIMessage[] = body.messages || [];
|
|
88
|
-
const id: string = body.id || 'temp-id';
|
|
89
|
-
|
|
90
|
-
const { model, systemPrompt, temperature } = DEVELOPER_CONFIG;
|
|
91
|
-
|
|
92
|
-
// Check if conversation exists, create if not
|
|
93
|
-
const existingConv = await db
|
|
94
|
-
.select({ id: conversations.id })
|
|
95
|
-
.from(conversations)
|
|
96
|
-
.where(eq(conversations.id, id))
|
|
97
|
-
.limit(1);
|
|
223
|
+
try {
|
|
224
|
+
const body = await req.json();
|
|
225
|
+
const userId = req.headers.get('X-User-Id');
|
|
98
226
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
userId,
|
|
103
|
-
title: 'New Chat',
|
|
104
|
-
metadata: {},
|
|
105
|
-
});
|
|
106
|
-
}
|
|
227
|
+
if (!userId) {
|
|
228
|
+
return new Response('userId is required in X-User-Id header', { status: 400 });
|
|
229
|
+
}
|
|
107
230
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
const newUserMessage = userMessages[userMessages.length - 1];
|
|
112
|
-
const textPart = newUserMessage.parts?.find(p => p.type === 'text') as { text: string } | undefined;
|
|
113
|
-
const fileParts = newUserMessage.parts?.filter(p => p.type === 'file') || [];
|
|
231
|
+
const chatMessages: UIMessage[] = body.messages || [];
|
|
232
|
+
const id: string = body.id || 'temp-id';
|
|
233
|
+
const { model, systemPrompt, temperature } = DEVELOPER_CONFIG;
|
|
114
234
|
|
|
115
|
-
const
|
|
116
|
-
.select({ id:
|
|
117
|
-
.from(
|
|
118
|
-
.where(eq(
|
|
235
|
+
const existingConv = await db
|
|
236
|
+
.select({ id: conversations.id })
|
|
237
|
+
.from(conversations)
|
|
238
|
+
.where(eq(conversations.id, id))
|
|
119
239
|
.limit(1);
|
|
120
240
|
|
|
121
|
-
if (!
|
|
122
|
-
await db.insert(
|
|
123
|
-
id
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
files: fileParts,
|
|
128
|
-
model: model,
|
|
129
|
-
metadata: { parts: newUserMessage.parts || [] },
|
|
241
|
+
if (!existingConv.length) {
|
|
242
|
+
await db.insert(conversations).values({
|
|
243
|
+
id,
|
|
244
|
+
userId,
|
|
245
|
+
title: 'New Chat',
|
|
246
|
+
metadata: {},
|
|
130
247
|
});
|
|
131
248
|
}
|
|
132
249
|
|
|
133
|
-
|
|
134
|
-
if (
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
250
|
+
const userMessages = chatMessages.filter(msg => msg.role === 'user');
|
|
251
|
+
if (userMessages.length > 0) {
|
|
252
|
+
const newUserMessage = userMessages[userMessages.length - 1];
|
|
253
|
+
const textPart = newUserMessage.parts?.find(p => p.type === 'text') as { text: string } | undefined;
|
|
254
|
+
const fileParts = newUserMessage.parts?.filter(p => p.type === 'file') || [];
|
|
255
|
+
|
|
256
|
+
const existingMsg = await db
|
|
257
|
+
.select({ id: messages.id })
|
|
258
|
+
.from(messages)
|
|
259
|
+
.where(eq(messages.id, newUserMessage.id))
|
|
139
260
|
.limit(1);
|
|
140
261
|
|
|
141
|
-
if (
|
|
142
|
-
await
|
|
262
|
+
if (!existingMsg.length) {
|
|
263
|
+
await db.insert(messages).values({
|
|
264
|
+
id: newUserMessage.id,
|
|
265
|
+
conversationId: id,
|
|
266
|
+
role: newUserMessage.role,
|
|
267
|
+
content: textPart?.text || '',
|
|
268
|
+
files: fileParts,
|
|
269
|
+
model: model,
|
|
270
|
+
metadata: { parts: newUserMessage.parts || [] },
|
|
271
|
+
});
|
|
143
272
|
}
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
273
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
274
|
+
if (textPart?.text) {
|
|
275
|
+
const conv = await db
|
|
276
|
+
.select({ title: conversations.title })
|
|
277
|
+
.from(conversations)
|
|
278
|
+
.where(eq(conversations.id, id))
|
|
279
|
+
.limit(1);
|
|
152
280
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
if (textPart && 'text' in textPart) {
|
|
156
|
-
content.push({ type: 'text', text: textPart.text });
|
|
157
|
-
}
|
|
158
|
-
for (const file of fileParts) {
|
|
159
|
-
if ('mediaType' in file && file.mediaType?.startsWith('image/')) {
|
|
160
|
-
content.push({ type: 'image', image: (file as any).url });
|
|
161
|
-
}
|
|
281
|
+
if (conv[0]?.title === 'New Chat') {
|
|
282
|
+
await updateConversationTitle(id, textPart.text.slice(0, 100));
|
|
162
283
|
}
|
|
163
|
-
return { ...msg, content };
|
|
164
284
|
}
|
|
165
285
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
286
|
+
|
|
287
|
+
const transformedMessages = chatMessages.map(msg => {
|
|
288
|
+
if (msg.role === 'user' && msg.parts) {
|
|
289
|
+
const textPart = msg.parts.find(p => p.type === 'text');
|
|
290
|
+
const fileParts = msg.parts.filter(p => p.type === 'file');
|
|
291
|
+
|
|
292
|
+
if (fileParts.length > 0) {
|
|
293
|
+
const content: any[] = [];
|
|
294
|
+
if (textPart && 'text' in textPart) {
|
|
295
|
+
content.push({ type: 'text', text: textPart.text });
|
|
296
|
+
}
|
|
297
|
+
for (const file of fileParts) {
|
|
298
|
+
if ('mediaType' in file && (file as any).mediaType?.startsWith('image/')) {
|
|
299
|
+
content.push({ type: 'image', image: (file as any).url });
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return { ...msg, content };
|
|
303
|
+
}
|
|
182
304
|
}
|
|
183
|
-
|
|
184
|
-
|
|
305
|
+
return msg;
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
const result = streamText({
|
|
309
|
+
model: model,
|
|
310
|
+
messages: convertToModelMessages(transformedMessages),
|
|
311
|
+
system: systemPrompt,
|
|
312
|
+
temperature: temperature,
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
return result.toUIMessageStreamResponse({
|
|
316
|
+
sendSources: true,
|
|
317
|
+
sendReasoning: true,
|
|
318
|
+
onFinish: ({ messages: finalMessages }) => {
|
|
319
|
+
if (finalMessages.length > 0) {
|
|
320
|
+
saveChat({ chatId: id, messages: finalMessages, model, userId });
|
|
321
|
+
}
|
|
322
|
+
},
|
|
323
|
+
});
|
|
324
|
+
} catch (error) {
|
|
325
|
+
console.error('Chat API error:', error);
|
|
326
|
+
return new Response(JSON.stringify({ error: 'Internal server error' }), {
|
|
327
|
+
status: 500,
|
|
328
|
+
headers: { 'Content-Type': 'application/json' },
|
|
329
|
+
});
|
|
330
|
+
}
|
|
185
331
|
}
|
|
186
332
|
```
|
|
187
333
|
|
|
188
|
-
|
|
334
|
+
</details>
|
|
335
|
+
|
|
336
|
+
<details>
|
|
337
|
+
<summary><strong>app/api/chat/history/route.ts</strong> - List Conversations</summary>
|
|
189
338
|
|
|
190
339
|
```typescript
|
|
191
340
|
import { NextResponse } from 'next/server';
|
|
@@ -219,7 +368,10 @@ export async function GET(request: Request) {
|
|
|
219
368
|
}
|
|
220
369
|
```
|
|
221
370
|
|
|
222
|
-
|
|
371
|
+
</details>
|
|
372
|
+
|
|
373
|
+
<details>
|
|
374
|
+
<summary><strong>app/api/chat/history/[conversationId]/route.ts</strong> - Get Conversation</summary>
|
|
223
375
|
|
|
224
376
|
```typescript
|
|
225
377
|
import { NextResponse } from 'next/server';
|
|
@@ -238,7 +390,6 @@ export async function GET(
|
|
|
238
390
|
return NextResponse.json({ error: 'userId is required' }, { status: 400 });
|
|
239
391
|
}
|
|
240
392
|
|
|
241
|
-
// Verify the conversation belongs to the user
|
|
242
393
|
const conv = await db
|
|
243
394
|
.select({
|
|
244
395
|
id: conversations.id,
|
|
@@ -295,17 +446,28 @@ export async function GET(
|
|
|
295
446
|
}
|
|
296
447
|
```
|
|
297
448
|
|
|
298
|
-
|
|
449
|
+
</details>
|
|
450
|
+
|
|
451
|
+
<details>
|
|
452
|
+
<summary><strong>app/api/chat/upload/route.ts</strong> - File Upload (Optional)</summary>
|
|
299
453
|
|
|
300
454
|
```typescript
|
|
301
455
|
import { createClient } from '@supabase/supabase-js';
|
|
302
456
|
import { nanoid } from 'nanoid';
|
|
303
457
|
|
|
304
|
-
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
|
305
|
-
const supabaseServiceKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
|
306
|
-
|
|
307
458
|
export async function POST(req: Request) {
|
|
308
459
|
try {
|
|
460
|
+
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
|
461
|
+
const supabaseServiceKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
|
|
462
|
+
|
|
463
|
+
// Check for required environment variables
|
|
464
|
+
if (!supabaseUrl || !supabaseServiceKey) {
|
|
465
|
+
console.error('Missing Supabase environment variables');
|
|
466
|
+
return Response.json({
|
|
467
|
+
error: 'File upload is not configured. Please set up Supabase Storage environment variables.'
|
|
468
|
+
}, { status: 503 });
|
|
469
|
+
}
|
|
470
|
+
|
|
309
471
|
const formData = await req.formData();
|
|
310
472
|
const file = formData.get('file') as File;
|
|
311
473
|
const conversationId = formData.get('conversationId') as string;
|
|
@@ -319,12 +481,10 @@ export async function POST(req: Request) {
|
|
|
319
481
|
return Response.json({ error: 'userId is required' }, { status: 400 });
|
|
320
482
|
}
|
|
321
483
|
|
|
322
|
-
// Only images supported
|
|
323
484
|
if (!file.type.startsWith('image/')) {
|
|
324
485
|
return Response.json({ error: 'Only image files are supported' }, { status: 400 });
|
|
325
486
|
}
|
|
326
487
|
|
|
327
|
-
// 5MB limit
|
|
328
488
|
if (file.size > 5 * 1024 * 1024) {
|
|
329
489
|
return Response.json({ error: 'File size exceeds 5MB limit' }, { status: 400 });
|
|
330
490
|
}
|
|
@@ -366,129 +526,9 @@ export async function POST(req: Request) {
|
|
|
366
526
|
}
|
|
367
527
|
```
|
|
368
528
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
**For sitewide usage**, create a provider component:
|
|
372
|
-
|
|
373
|
-
```tsx
|
|
374
|
-
// components/chat-provider.tsx
|
|
375
|
-
'use client';
|
|
529
|
+
</details>
|
|
376
530
|
|
|
377
|
-
|
|
378
|
-
import '@mordn/chat-widget/styles.css';
|
|
379
|
-
import { useAuth } from '@/contexts/auth-context'; // Your auth hook
|
|
380
|
-
|
|
381
|
-
export function ChatProvider() {
|
|
382
|
-
const { user, loading } = useAuth();
|
|
383
|
-
|
|
384
|
-
if (loading || !user) {
|
|
385
|
-
return null;
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
return (
|
|
389
|
-
<ChatWidget
|
|
390
|
-
userId={user.id}
|
|
391
|
-
theme={{ mode: 'dark' }}
|
|
392
|
-
/>
|
|
393
|
-
);
|
|
394
|
-
}
|
|
395
|
-
```
|
|
396
|
-
|
|
397
|
-
Add to your root layout:
|
|
398
|
-
|
|
399
|
-
```tsx
|
|
400
|
-
// app/layout.tsx
|
|
401
|
-
import { ChatProvider } from '@/components/chat-provider';
|
|
402
|
-
|
|
403
|
-
export default function RootLayout({ children }) {
|
|
404
|
-
return (
|
|
405
|
-
<html>
|
|
406
|
-
<body>
|
|
407
|
-
{children}
|
|
408
|
-
<ChatProvider />
|
|
409
|
-
</body>
|
|
410
|
-
</html>
|
|
411
|
-
);
|
|
412
|
-
}
|
|
413
|
-
```
|
|
414
|
-
|
|
415
|
-
## Props
|
|
416
|
-
|
|
417
|
-
| Prop | Type | Default | Description |
|
|
418
|
-
|------|------|---------|-------------|
|
|
419
|
-
| `userId` | `string` | **required** | User identifier for storing conversations |
|
|
420
|
-
| `conversationId` | `string` | - | Load a specific conversation |
|
|
421
|
-
| `initialMessages` | `array` | - | Pre-fill the chat with messages |
|
|
422
|
-
| `className` | `string` | - | Additional CSS classes |
|
|
423
|
-
| `theme` | `ThemeConfig` | - | Theme configuration |
|
|
424
|
-
| `features` | `FeatureConfig` | - | Feature toggles |
|
|
425
|
-
| `display` | `DisplayConfig` | - | Display options |
|
|
426
|
-
|
|
427
|
-
### ThemeConfig
|
|
428
|
-
|
|
429
|
-
```typescript
|
|
430
|
-
{
|
|
431
|
-
mode?: 'light' | 'dark';
|
|
432
|
-
primaryColor?: string;
|
|
433
|
-
backgroundColor?: string;
|
|
434
|
-
textColor?: string;
|
|
435
|
-
}
|
|
436
|
-
```
|
|
437
|
-
|
|
438
|
-
### FeatureConfig
|
|
439
|
-
|
|
440
|
-
```typescript
|
|
441
|
-
{
|
|
442
|
-
fileUpload?: boolean; // Enable file attachments (default: true)
|
|
443
|
-
webSearch?: boolean; // Enable web search toggle
|
|
444
|
-
}
|
|
445
|
-
```
|
|
446
|
-
|
|
447
|
-
### DisplayConfig
|
|
448
|
-
|
|
449
|
-
```typescript
|
|
450
|
-
{
|
|
451
|
-
width?: string; // e.g., '400px' or '30vw' (default: '30vw')
|
|
452
|
-
defaultOpen?: boolean; // Start with chat open (default: false)
|
|
453
|
-
showToggleButton?: boolean; // Show FAB toggle button (default: true)
|
|
454
|
-
toggleButtonPosition?: {
|
|
455
|
-
bottom?: string;
|
|
456
|
-
right?: string;
|
|
457
|
-
};
|
|
458
|
-
}
|
|
459
|
-
```
|
|
460
|
-
|
|
461
|
-
## Exports
|
|
462
|
-
|
|
463
|
-
```typescript
|
|
464
|
-
// Main component
|
|
465
|
-
import { ChatWidget } from '@mordn/chat-widget';
|
|
466
|
-
import '@mordn/chat-widget/styles.css';
|
|
467
|
-
|
|
468
|
-
// Database utilities (server-side only)
|
|
469
|
-
import {
|
|
470
|
-
db,
|
|
471
|
-
conversations,
|
|
472
|
-
messages,
|
|
473
|
-
createChat,
|
|
474
|
-
loadChat,
|
|
475
|
-
saveChat,
|
|
476
|
-
getConversations,
|
|
477
|
-
deleteConversation,
|
|
478
|
-
updateConversationTitle,
|
|
479
|
-
eq, and, or, desc, asc, sql
|
|
480
|
-
} from '@mordn/chat-widget/api';
|
|
481
|
-
|
|
482
|
-
// Types
|
|
483
|
-
import type {
|
|
484
|
-
ChatWidgetConfig,
|
|
485
|
-
ThemeConfig,
|
|
486
|
-
FeatureConfig,
|
|
487
|
-
DisplayConfig,
|
|
488
|
-
Conversation,
|
|
489
|
-
Message
|
|
490
|
-
} from '@mordn/chat-widget';
|
|
491
|
-
```
|
|
531
|
+
---
|
|
492
532
|
|
|
493
533
|
## License
|
|
494
534
|
|