@mordn/chat-widget 0.1.1 → 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 +412 -215
- package/dist/api/index.d.mts +2 -1
- package/dist/api/index.d.ts +2 -1
- package/dist/api/index.js +2 -1
- package/dist/api/index.js.map +1 -1
- package/dist/api/index.mjs +2 -1
- package/dist/api/index.mjs.map +1 -1
- package/dist/cli/init.js +457 -0
- package/dist/db/index.d.mts +4 -277
- package/dist/db/index.d.ts +4 -277
- package/dist/db/index.js +2 -1
- package/dist/db/index.js.map +1 -1
- package/dist/db/index.mjs +2 -1
- package/dist/db/index.mjs.map +1 -1
- package/dist/index-uiN6exzS.d.mts +278 -0
- package/dist/index-uiN6exzS.d.ts +278 -0
- package/dist/index.d.mts +64 -5
- package/dist/index.d.ts +64 -5
- package/dist/index.js +311 -236
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +257 -186
- package/dist/index.mjs.map +1 -1
- package/dist/schema/index.d.mts +2 -0
- package/dist/schema/index.d.ts +2 -0
- package/dist/schema/index.js +63 -0
- package/dist/schema/index.js.map +1 -0
- package/dist/schema/index.mjs +35 -0
- package/dist/schema/index.mjs.map +1 -0
- package/dist/styles.css +1 -1
- package/package.json +14 -4
package/README.md
CHANGED
|
@@ -2,12 +2,21 @@
|
|
|
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)
|
|
@@ -17,241 +26,113 @@ npm install @mordn/chat-widget
|
|
|
17
26
|
|
|
18
27
|
## Setup
|
|
19
28
|
|
|
20
|
-
### 1. Environment
|
|
29
|
+
### 1. Environment Variables
|
|
21
30
|
|
|
22
|
-
|
|
31
|
+
Copy `.env.example` to `.env.local` and fill in your credentials:
|
|
23
32
|
|
|
24
33
|
```env
|
|
34
|
+
# Database (Required)
|
|
25
35
|
DATABASE_URL="postgresql://postgres.xxx:[PASSWORD]@aws-0-region.pooler.supabase.com:6543/postgres"
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
### 2. Database Setup
|
|
29
36
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
```bash
|
|
33
|
-
npm install drizzle-kit --save-dev
|
|
37
|
+
# AI Provider (Required)
|
|
38
|
+
AI_GATEWAY_API_KEY="your-ai-gateway-key"
|
|
34
39
|
```
|
|
35
40
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
```typescript
|
|
39
|
-
import { defineConfig } from 'drizzle-kit';
|
|
40
|
-
|
|
41
|
-
export default defineConfig({
|
|
42
|
-
schema: './node_modules/@mordn/chat-widget/dist/db/index.js',
|
|
43
|
-
out: './drizzle',
|
|
44
|
-
dialect: 'postgresql',
|
|
45
|
-
dbCredentials: {
|
|
46
|
-
url: process.env.DATABASE_URL!,
|
|
47
|
-
},
|
|
48
|
-
});
|
|
49
|
-
```
|
|
41
|
+
### 2. Database Setup
|
|
50
42
|
|
|
51
|
-
|
|
43
|
+
Push the schema to your database:
|
|
52
44
|
|
|
53
45
|
```bash
|
|
54
46
|
npx drizzle-kit push
|
|
55
47
|
```
|
|
56
48
|
|
|
57
|
-
### 3.
|
|
58
|
-
|
|
59
|
-
Create the following API routes in your Next.js app:
|
|
49
|
+
### 3. Configure Your AI Model
|
|
60
50
|
|
|
61
|
-
|
|
51
|
+
Open `app/api/chat/route.ts` and update the config:
|
|
62
52
|
|
|
63
53
|
```typescript
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const { messages, id } = await req.json();
|
|
70
|
-
const userId = req.headers.get('X-User-Id');
|
|
71
|
-
|
|
72
|
-
if (!userId) {
|
|
73
|
-
return new Response('Unauthorized', { status: 401 });
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// Create conversation if it doesn't exist
|
|
77
|
-
const existing = await db
|
|
78
|
-
.select({ id: conversations.id })
|
|
79
|
-
.from(conversations)
|
|
80
|
-
.where(eq(conversations.id, id))
|
|
81
|
-
.limit(1);
|
|
82
|
-
|
|
83
|
-
if (existing.length === 0) {
|
|
84
|
-
await createChat(userId);
|
|
85
|
-
// Update the ID to match the provided one
|
|
86
|
-
await db.update(conversations)
|
|
87
|
-
.set({ id })
|
|
88
|
-
.where(eq(conversations.id, existing[0]?.id));
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const result = streamText({
|
|
92
|
-
model: openai('gpt-4o'),
|
|
93
|
-
system: 'You are a helpful assistant.',
|
|
94
|
-
messages,
|
|
95
|
-
onFinish: async ({ response }) => {
|
|
96
|
-
await saveChat({
|
|
97
|
-
chatId: id,
|
|
98
|
-
messages: [...messages, ...response.messages],
|
|
99
|
-
userId,
|
|
100
|
-
});
|
|
101
|
-
},
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
return result.toDataStreamResponse();
|
|
105
|
-
}
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
#### `app/api/chat/history/route.ts` - List Conversations
|
|
109
|
-
|
|
110
|
-
```typescript
|
|
111
|
-
import { NextResponse } from 'next/server';
|
|
112
|
-
import { getConversations } from '@mordn/chat-widget/api';
|
|
113
|
-
|
|
114
|
-
export async function GET(request: Request) {
|
|
115
|
-
const userId = request.headers.get('X-User-Id');
|
|
116
|
-
|
|
117
|
-
if (!userId) {
|
|
118
|
-
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
const conversations = await getConversations(userId);
|
|
122
|
-
return NextResponse.json({ conversations });
|
|
123
|
-
}
|
|
54
|
+
const DEVELOPER_CONFIG = {
|
|
55
|
+
model: 'openai/gpt-4o', // Your AI model
|
|
56
|
+
systemPrompt: 'You are a helpful assistant',
|
|
57
|
+
temperature: 0.7,
|
|
58
|
+
};
|
|
124
59
|
```
|
|
125
60
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
```typescript
|
|
129
|
-
import { NextResponse } from 'next/server';
|
|
130
|
-
import { loadChat } from '@mordn/chat-widget/api';
|
|
131
|
-
|
|
132
|
-
export async function GET(
|
|
133
|
-
request: Request,
|
|
134
|
-
{ params }: { params: { id: string } }
|
|
135
|
-
) {
|
|
136
|
-
const userId = request.headers.get('X-User-Id');
|
|
137
|
-
|
|
138
|
-
if (!userId) {
|
|
139
|
-
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const messages = await loadChat(params.id);
|
|
143
|
-
return NextResponse.json({ messages });
|
|
144
|
-
}
|
|
145
|
-
```
|
|
146
|
-
|
|
147
|
-
#### `app/api/chat/upload/route.ts` - File Upload (Optional)
|
|
148
|
-
|
|
149
|
-
```typescript
|
|
150
|
-
import { NextResponse } from 'next/server';
|
|
151
|
-
import { createClient } from '@supabase/supabase-js';
|
|
152
|
-
|
|
153
|
-
const supabase = createClient(
|
|
154
|
-
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
|
155
|
-
process.env.SUPABASE_SERVICE_ROLE_KEY!
|
|
156
|
-
);
|
|
157
|
-
|
|
158
|
-
export async function POST(request: Request) {
|
|
159
|
-
const formData = await request.formData();
|
|
160
|
-
const file = formData.get('file') as File;
|
|
161
|
-
const conversationId = formData.get('conversationId') as string;
|
|
162
|
-
const userId = formData.get('userId') as string;
|
|
163
|
-
|
|
164
|
-
if (!file || !userId) {
|
|
165
|
-
return NextResponse.json({ error: 'Missing required fields' }, { status: 400 });
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
const filename = `${userId}/${conversationId}/${Date.now()}-${file.name}`;
|
|
169
|
-
const { data, error } = await supabase.storage
|
|
170
|
-
.from('chat-uploads')
|
|
171
|
-
.upload(filename, file);
|
|
172
|
-
|
|
173
|
-
if (error) {
|
|
174
|
-
return NextResponse.json({ error: error.message }, { status: 500 });
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
const { data: { publicUrl } } = supabase.storage
|
|
178
|
-
.from('chat-uploads')
|
|
179
|
-
.getPublicUrl(filename);
|
|
180
|
-
|
|
181
|
-
return NextResponse.json({
|
|
182
|
-
url: publicUrl,
|
|
183
|
-
filename: file.name,
|
|
184
|
-
mediaType: file.type,
|
|
185
|
-
size: file.size,
|
|
186
|
-
});
|
|
187
|
-
}
|
|
188
|
-
```
|
|
189
|
-
|
|
190
|
-
### 4. Use the Widget
|
|
191
|
-
|
|
192
|
-
**For sitewide usage**, create a provider component:
|
|
61
|
+
### 4. Add the Widget
|
|
193
62
|
|
|
194
63
|
```tsx
|
|
195
|
-
// components/chat-provider.tsx
|
|
196
64
|
'use client';
|
|
197
65
|
|
|
198
66
|
import { ChatWidget } from '@mordn/chat-widget';
|
|
199
67
|
import '@mordn/chat-widget/styles.css';
|
|
200
|
-
import { useAuth } from '@/contexts/auth-context'; // Your auth hook
|
|
201
|
-
|
|
202
|
-
export function ChatProvider() {
|
|
203
|
-
const { user, loading } = useAuth();
|
|
204
|
-
|
|
205
|
-
if (loading || !user) {
|
|
206
|
-
return null;
|
|
207
|
-
}
|
|
208
68
|
|
|
69
|
+
export default function Page() {
|
|
209
70
|
return (
|
|
210
71
|
<ChatWidget
|
|
211
|
-
|
|
212
|
-
|
|
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
|
+
]}
|
|
213
96
|
/>
|
|
214
97
|
);
|
|
215
98
|
}
|
|
216
99
|
```
|
|
217
100
|
|
|
218
|
-
|
|
101
|
+
---
|
|
219
102
|
|
|
220
|
-
|
|
221
|
-
// app/layout.tsx
|
|
222
|
-
import { ChatProvider } from '@/components/chat-provider';
|
|
103
|
+
## File Uploads (Optional)
|
|
223
104
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
</html>
|
|
232
|
-
);
|
|
233
|
-
}
|
|
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
|
+
/>
|
|
234
112
|
```
|
|
235
113
|
|
|
236
|
-
|
|
114
|
+
### 1. Create Storage Bucket
|
|
237
115
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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)
|
|
241
120
|
|
|
242
|
-
|
|
243
|
-
import '@mordn/chat-widget/styles.css';
|
|
121
|
+
### 2. Add Environment Variables
|
|
244
122
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
<h1>Dashboard</h1>
|
|
249
|
-
<ChatWidget userId="user-123" />
|
|
250
|
-
</div>
|
|
251
|
-
);
|
|
252
|
-
}
|
|
123
|
+
```env
|
|
124
|
+
NEXT_PUBLIC_SUPABASE_URL="https://your-project.supabase.co"
|
|
125
|
+
SUPABASE_SERVICE_ROLE_KEY="your-service-role-key"
|
|
253
126
|
```
|
|
254
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
|
+
|
|
255
136
|
## Props
|
|
256
137
|
|
|
257
138
|
| Prop | Type | Default | Description |
|
|
@@ -260,9 +141,6 @@ export default function DashboardPage() {
|
|
|
260
141
|
| `conversationId` | `string` | - | Load a specific conversation |
|
|
261
142
|
| `initialMessages` | `array` | - | Pre-fill the chat with messages |
|
|
262
143
|
| `className` | `string` | - | Additional CSS classes |
|
|
263
|
-
| `model` | `string` | - | AI model identifier |
|
|
264
|
-
| `systemPrompt` | `string` | - | System prompt for the AI |
|
|
265
|
-
| `temperature` | `number` | - | AI temperature (0-1) |
|
|
266
144
|
| `theme` | `ThemeConfig` | - | Theme configuration |
|
|
267
145
|
| `features` | `FeatureConfig` | - | Feature toggles |
|
|
268
146
|
| `display` | `DisplayConfig` | - | Display options |
|
|
@@ -272,9 +150,6 @@ export default function DashboardPage() {
|
|
|
272
150
|
```typescript
|
|
273
151
|
{
|
|
274
152
|
mode?: 'light' | 'dark';
|
|
275
|
-
primaryColor?: string; // Hex color
|
|
276
|
-
backgroundColor?: string; // Hex color
|
|
277
|
-
textColor?: string; // Hex color
|
|
278
153
|
}
|
|
279
154
|
```
|
|
280
155
|
|
|
@@ -282,7 +157,7 @@ export default function DashboardPage() {
|
|
|
282
157
|
|
|
283
158
|
```typescript
|
|
284
159
|
{
|
|
285
|
-
fileUpload?: boolean; // Enable file attachments (default:
|
|
160
|
+
fileUpload?: boolean; // Enable file attachments (default: false)
|
|
286
161
|
webSearch?: boolean; // Enable web search toggle
|
|
287
162
|
}
|
|
288
163
|
```
|
|
@@ -291,16 +166,18 @@ export default function DashboardPage() {
|
|
|
291
166
|
|
|
292
167
|
```typescript
|
|
293
168
|
{
|
|
294
|
-
width?: string; // e.g., '400px' or '30vw'
|
|
169
|
+
width?: string; // e.g., '400px' or '30vw'
|
|
295
170
|
defaultOpen?: boolean; // Start with chat open (default: false)
|
|
296
171
|
showToggleButton?: boolean; // Show FAB toggle button (default: true)
|
|
297
172
|
toggleButtonPosition?: {
|
|
298
|
-
bottom?: string;
|
|
299
|
-
right?: string;
|
|
173
|
+
bottom?: string;
|
|
174
|
+
right?: string;
|
|
300
175
|
};
|
|
301
176
|
}
|
|
302
177
|
```
|
|
303
178
|
|
|
179
|
+
---
|
|
180
|
+
|
|
304
181
|
## Exports
|
|
305
182
|
|
|
306
183
|
```typescript
|
|
@@ -321,18 +198,338 @@ import {
|
|
|
321
198
|
updateConversationTitle,
|
|
322
199
|
eq, and, or, desc, asc, sql
|
|
323
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>
|
|
209
|
+
|
|
210
|
+
```typescript
|
|
211
|
+
import { saveChat, updateConversationTitle, db, conversations, messages, eq } from '@mordn/chat-widget/api';
|
|
212
|
+
import { convertToModelMessages, streamText, UIMessage } from 'ai';
|
|
213
|
+
|
|
214
|
+
export const maxDuration = 30;
|
|
215
|
+
|
|
216
|
+
const DEVELOPER_CONFIG = {
|
|
217
|
+
model: 'openai/gpt-4o',
|
|
218
|
+
systemPrompt: 'You are a helpful assistant',
|
|
219
|
+
temperature: 0.7,
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
export async function POST(req: Request) {
|
|
223
|
+
try {
|
|
224
|
+
const body = await req.json();
|
|
225
|
+
const userId = req.headers.get('X-User-Id');
|
|
226
|
+
|
|
227
|
+
if (!userId) {
|
|
228
|
+
return new Response('userId is required in X-User-Id header', { status: 400 });
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const chatMessages: UIMessage[] = body.messages || [];
|
|
232
|
+
const id: string = body.id || 'temp-id';
|
|
233
|
+
const { model, systemPrompt, temperature } = DEVELOPER_CONFIG;
|
|
234
|
+
|
|
235
|
+
const existingConv = await db
|
|
236
|
+
.select({ id: conversations.id })
|
|
237
|
+
.from(conversations)
|
|
238
|
+
.where(eq(conversations.id, id))
|
|
239
|
+
.limit(1);
|
|
240
|
+
|
|
241
|
+
if (!existingConv.length) {
|
|
242
|
+
await db.insert(conversations).values({
|
|
243
|
+
id,
|
|
244
|
+
userId,
|
|
245
|
+
title: 'New Chat',
|
|
246
|
+
metadata: {},
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
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))
|
|
260
|
+
.limit(1);
|
|
261
|
+
|
|
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
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
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);
|
|
280
|
+
|
|
281
|
+
if (conv[0]?.title === 'New Chat') {
|
|
282
|
+
await updateConversationTitle(id, textPart.text.slice(0, 100));
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
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
|
+
}
|
|
304
|
+
}
|
|
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
|
+
}
|
|
331
|
+
}
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
</details>
|
|
324
335
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
336
|
+
<details>
|
|
337
|
+
<summary><strong>app/api/chat/history/route.ts</strong> - List Conversations</summary>
|
|
338
|
+
|
|
339
|
+
```typescript
|
|
340
|
+
import { NextResponse } from 'next/server';
|
|
341
|
+
import { getConversations } from '@mordn/chat-widget/api';
|
|
342
|
+
|
|
343
|
+
export async function GET(request: Request) {
|
|
344
|
+
try {
|
|
345
|
+
const url = new URL(request.url);
|
|
346
|
+
const userId = url.searchParams.get('userId') || request.headers.get('X-User-Id');
|
|
347
|
+
|
|
348
|
+
if (!userId) {
|
|
349
|
+
return NextResponse.json({ error: 'userId is required' }, { status: 400 });
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const conversationsData = await getConversations(userId);
|
|
353
|
+
|
|
354
|
+
const conversations = conversationsData.map(conv => ({
|
|
355
|
+
id: conv.id,
|
|
356
|
+
title: conv.title,
|
|
357
|
+
created_at: conv.createdAt,
|
|
358
|
+
updated_at: conv.updatedAt,
|
|
359
|
+
metadata: conv.metadata,
|
|
360
|
+
message_count: conv.messageCount,
|
|
361
|
+
}));
|
|
362
|
+
|
|
363
|
+
return NextResponse.json({ conversations });
|
|
364
|
+
} catch (error) {
|
|
365
|
+
console.error('Error in chat history API:', error);
|
|
366
|
+
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
</details>
|
|
372
|
+
|
|
373
|
+
<details>
|
|
374
|
+
<summary><strong>app/api/chat/history/[conversationId]/route.ts</strong> - Get Conversation</summary>
|
|
375
|
+
|
|
376
|
+
```typescript
|
|
377
|
+
import { NextResponse } from 'next/server';
|
|
378
|
+
import { db, conversations, messages, eq, and, asc } from '@mordn/chat-widget/api';
|
|
379
|
+
|
|
380
|
+
export async function GET(
|
|
381
|
+
request: Request,
|
|
382
|
+
{ params }: { params: Promise<{ conversationId: string }> }
|
|
383
|
+
) {
|
|
384
|
+
try {
|
|
385
|
+
const { conversationId } = await params;
|
|
386
|
+
const url = new URL(request.url);
|
|
387
|
+
const userId = url.searchParams.get('userId') || request.headers.get('X-User-Id');
|
|
388
|
+
|
|
389
|
+
if (!userId) {
|
|
390
|
+
return NextResponse.json({ error: 'userId is required' }, { status: 400 });
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const conv = await db
|
|
394
|
+
.select({
|
|
395
|
+
id: conversations.id,
|
|
396
|
+
title: conversations.title,
|
|
397
|
+
metadata: conversations.metadata,
|
|
398
|
+
})
|
|
399
|
+
.from(conversations)
|
|
400
|
+
.where(and(
|
|
401
|
+
eq(conversations.id, conversationId),
|
|
402
|
+
eq(conversations.userId, userId)
|
|
403
|
+
))
|
|
404
|
+
.limit(1);
|
|
405
|
+
|
|
406
|
+
if (!conv.length) {
|
|
407
|
+
return NextResponse.json({ error: 'Conversation not found' }, { status: 404 });
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const conversation = conv[0];
|
|
411
|
+
|
|
412
|
+
const dbMessages = await db
|
|
413
|
+
.select()
|
|
414
|
+
.from(messages)
|
|
415
|
+
.where(eq(messages.conversationId, conversationId))
|
|
416
|
+
.orderBy(asc(messages.createdAt))
|
|
417
|
+
.limit(1000);
|
|
418
|
+
|
|
419
|
+
const transformedMessages = dbMessages.map(msg => {
|
|
420
|
+
const metadata = msg.metadata as { parts?: any[] } | null;
|
|
421
|
+
|
|
422
|
+
if (metadata?.parts && Array.isArray(metadata.parts)) {
|
|
423
|
+
return {
|
|
424
|
+
id: msg.id,
|
|
425
|
+
role: msg.role,
|
|
426
|
+
content: msg.content,
|
|
427
|
+
created_at: msg.createdAt,
|
|
428
|
+
parts: metadata.parts
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
return {
|
|
433
|
+
id: msg.id,
|
|
434
|
+
role: msg.role,
|
|
435
|
+
content: msg.content,
|
|
436
|
+
created_at: msg.createdAt,
|
|
437
|
+
parts: msg.content ? [{ type: 'text', text: msg.content }] : undefined
|
|
438
|
+
};
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
return NextResponse.json({ conversation, messages: transformedMessages });
|
|
442
|
+
} catch (error) {
|
|
443
|
+
console.error('Error loading conversation:', error);
|
|
444
|
+
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
|
445
|
+
}
|
|
446
|
+
}
|
|
334
447
|
```
|
|
335
448
|
|
|
449
|
+
</details>
|
|
450
|
+
|
|
451
|
+
<details>
|
|
452
|
+
<summary><strong>app/api/chat/upload/route.ts</strong> - File Upload (Optional)</summary>
|
|
453
|
+
|
|
454
|
+
```typescript
|
|
455
|
+
import { createClient } from '@supabase/supabase-js';
|
|
456
|
+
import { nanoid } from 'nanoid';
|
|
457
|
+
|
|
458
|
+
export async function POST(req: Request) {
|
|
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
|
+
|
|
471
|
+
const formData = await req.formData();
|
|
472
|
+
const file = formData.get('file') as File;
|
|
473
|
+
const conversationId = formData.get('conversationId') as string;
|
|
474
|
+
const userId = formData.get('userId') as string;
|
|
475
|
+
|
|
476
|
+
if (!file) {
|
|
477
|
+
return Response.json({ error: 'No file provided' }, { status: 400 });
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if (!userId) {
|
|
481
|
+
return Response.json({ error: 'userId is required' }, { status: 400 });
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
if (!file.type.startsWith('image/')) {
|
|
485
|
+
return Response.json({ error: 'Only image files are supported' }, { status: 400 });
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
if (file.size > 5 * 1024 * 1024) {
|
|
489
|
+
return Response.json({ error: 'File size exceeds 5MB limit' }, { status: 400 });
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
const supabase = createClient(supabaseUrl, supabaseServiceKey);
|
|
493
|
+
|
|
494
|
+
const timestamp = Date.now();
|
|
495
|
+
const randomId = nanoid(8);
|
|
496
|
+
const safeFilename = file.name.replace(/[^a-zA-Z0-9.-]/g, '_');
|
|
497
|
+
const filePath = `${userId}/${conversationId || 'default'}/${timestamp}-${randomId}-${safeFilename}`;
|
|
498
|
+
|
|
499
|
+
const fileBuffer = await file.arrayBuffer();
|
|
500
|
+
|
|
501
|
+
const { error: uploadError } = await supabase.storage
|
|
502
|
+
.from('chat-attachments')
|
|
503
|
+
.upload(filePath, fileBuffer, {
|
|
504
|
+
contentType: file.type,
|
|
505
|
+
upsert: false,
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
if (uploadError) {
|
|
509
|
+
return Response.json({ error: 'Failed to upload file' }, { status: 500 });
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const { data: urlData } = supabase.storage
|
|
513
|
+
.from('chat-attachments')
|
|
514
|
+
.getPublicUrl(filePath);
|
|
515
|
+
|
|
516
|
+
return Response.json({
|
|
517
|
+
url: urlData.publicUrl,
|
|
518
|
+
filename: file.name,
|
|
519
|
+
mediaType: file.type,
|
|
520
|
+
size: file.size,
|
|
521
|
+
type: 'file',
|
|
522
|
+
});
|
|
523
|
+
} catch (error) {
|
|
524
|
+
return Response.json({ error: 'Internal server error' }, { status: 500 });
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
</details>
|
|
530
|
+
|
|
531
|
+
---
|
|
532
|
+
|
|
336
533
|
## License
|
|
337
534
|
|
|
338
535
|
MIT
|