@mordn/chat-widget 0.1.1 → 0.1.3

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
@@ -14,15 +14,17 @@ npm install @mordn/chat-widget
14
14
  - React 18+
15
15
  - PostgreSQL database (Supabase recommended)
16
16
  - Tailwind CSS v4
17
+ - Vercel AI SDK
17
18
 
18
19
  ## Setup
19
20
 
20
- ### 1. Environment Variable
21
+ ### 1. Environment Variables
21
22
 
22
- Add your database connection string:
23
+ Add your database connection string and AI Gateway API key:
23
24
 
24
25
  ```env
25
26
  DATABASE_URL="postgresql://postgres.xxx:[PASSWORD]@aws-0-region.pooler.supabase.com:6543/postgres"
27
+ AI_GATEWAY_API_KEY="your-ai-gateway-key"
26
28
  ```
27
29
 
28
30
  ### 2. Database Setup
@@ -36,10 +38,11 @@ npm install drizzle-kit --save-dev
36
38
  Create `drizzle.config.ts` in your project root:
37
39
 
38
40
  ```typescript
41
+ import 'dotenv/config';
39
42
  import { defineConfig } from 'drizzle-kit';
40
43
 
41
44
  export default defineConfig({
42
- schema: './node_modules/@mordn/chat-widget/dist/db/index.js',
45
+ schema: './node_modules/@mordn/chat-widget/dist/schema/index.js',
43
46
  out: './drizzle',
44
47
  dialect: 'postgresql',
45
48
  dbCredentials: {
@@ -61,47 +64,124 @@ Create the following API routes in your Next.js app:
61
64
  #### `app/api/chat/route.ts` - Main Chat Endpoint
62
65
 
63
66
  ```typescript
64
- import { saveChat, createChat, db, conversations, eq } from '@mordn/chat-widget/api';
65
- import { streamText } from 'ai';
66
- import { openai } from '@ai-sdk/openai';
67
+ import { saveChat, updateConversationTitle, db, conversations, messages, eq } from '@mordn/chat-widget/api';
68
+ import { convertToModelMessages, streamText, UIMessage } from 'ai';
69
+
70
+ export const maxDuration = 30;
71
+
72
+ // DEVELOPER CONFIG - Set these for your app
73
+ const DEVELOPER_CONFIG = {
74
+ model: 'openai/gpt-4o', // Your AI model (provider/model format)
75
+ systemPrompt: 'You are a helpful assistant',
76
+ temperature: 0.7,
77
+ };
67
78
 
68
79
  export async function POST(req: Request) {
69
- const { messages, id } = await req.json();
80
+ const body = await req.json();
70
81
  const userId = req.headers.get('X-User-Id');
71
82
 
72
83
  if (!userId) {
73
- return new Response('Unauthorized', { status: 401 });
84
+ return new Response('userId is required in X-User-Id header', { status: 400 });
74
85
  }
75
86
 
76
- // Create conversation if it doesn't exist
77
- const existing = await db
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
78
94
  .select({ id: conversations.id })
79
95
  .from(conversations)
80
96
  .where(eq(conversations.id, id))
81
97
  .limit(1);
82
98
 
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));
99
+ if (!existingConv.length) {
100
+ await db.insert(conversations).values({
101
+ id,
102
+ userId,
103
+ title: 'New Chat',
104
+ metadata: {},
105
+ });
89
106
  }
90
107
 
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,
108
+ // Save the new user message
109
+ const userMessages = chatMessages.filter(msg => msg.role === 'user');
110
+ if (userMessages.length > 0) {
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') || [];
114
+
115
+ const existingMsg = await db
116
+ .select({ id: messages.id })
117
+ .from(messages)
118
+ .where(eq(messages.id, newUserMessage.id))
119
+ .limit(1);
120
+
121
+ if (!existingMsg.length) {
122
+ await db.insert(messages).values({
123
+ id: newUserMessage.id,
124
+ conversationId: id,
125
+ role: newUserMessage.role,
126
+ content: textPart?.text || '',
127
+ files: fileParts,
128
+ model: model,
129
+ metadata: { parts: newUserMessage.parts || [] },
100
130
  });
101
- },
131
+ }
132
+
133
+ // Update conversation title if needed
134
+ if (textPart?.text) {
135
+ const conv = await db
136
+ .select({ title: conversations.title })
137
+ .from(conversations)
138
+ .where(eq(conversations.id, id))
139
+ .limit(1);
140
+
141
+ if (conv[0]?.title === 'New Chat') {
142
+ await updateConversationTitle(id, textPart.text.slice(0, 100));
143
+ }
144
+ }
145
+ }
146
+
147
+ // Transform messages for AI (handle images)
148
+ const transformedMessages = chatMessages.map(msg => {
149
+ if (msg.role === 'user' && msg.parts) {
150
+ const textPart = msg.parts.find(p => p.type === 'text');
151
+ const fileParts = msg.parts.filter(p => p.type === 'file');
152
+
153
+ if (fileParts.length > 0) {
154
+ const content = [];
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
+ }
162
+ }
163
+ return { ...msg, content };
164
+ }
165
+ }
166
+ return msg;
167
+ });
168
+
169
+ const result = streamText({
170
+ model: model,
171
+ messages: convertToModelMessages(transformedMessages),
172
+ system: systemPrompt,
173
+ temperature: temperature,
102
174
  });
103
175
 
104
- return result.toDataStreamResponse();
176
+ return result.toUIMessageStreamResponse({
177
+ sendSources: true,
178
+ sendReasoning: true,
179
+ onFinish: ({ messages: finalMessages }) => {
180
+ if (finalMessages.length > 0) {
181
+ saveChat({ chatId: id, messages: finalMessages, model, userId });
182
+ }
183
+ },
184
+ });
105
185
  }
106
186
  ```
107
187
 
@@ -112,78 +192,177 @@ import { NextResponse } from 'next/server';
112
192
  import { getConversations } from '@mordn/chat-widget/api';
113
193
 
114
194
  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 });
195
+ try {
196
+ const url = new URL(request.url);
197
+ const userId = url.searchParams.get('userId') || request.headers.get('X-User-Id');
198
+
199
+ if (!userId) {
200
+ return NextResponse.json({ error: 'userId is required' }, { status: 400 });
201
+ }
202
+
203
+ const conversationsData = await getConversations(userId);
204
+
205
+ const conversations = conversationsData.map(conv => ({
206
+ id: conv.id,
207
+ title: conv.title,
208
+ created_at: conv.createdAt,
209
+ updated_at: conv.updatedAt,
210
+ metadata: conv.metadata,
211
+ message_count: conv.messageCount,
212
+ }));
213
+
214
+ return NextResponse.json({ conversations });
215
+ } catch (error) {
216
+ console.error('Error in chat history API:', error);
217
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
119
218
  }
120
-
121
- const conversations = await getConversations(userId);
122
- return NextResponse.json({ conversations });
123
219
  }
124
220
  ```
125
221
 
126
- #### `app/api/chat/history/[id]/route.ts` - Get Conversation Messages
222
+ #### `app/api/chat/history/[conversationId]/route.ts` - Get Conversation Messages
127
223
 
128
224
  ```typescript
129
225
  import { NextResponse } from 'next/server';
130
- import { loadChat } from '@mordn/chat-widget/api';
226
+ import { db, conversations, messages, eq, and, asc } from '@mordn/chat-widget/api';
131
227
 
132
228
  export async function GET(
133
229
  request: Request,
134
- { params }: { params: { id: string } }
230
+ { params }: { params: Promise<{ conversationId: string }> }
135
231
  ) {
136
- const userId = request.headers.get('X-User-Id');
137
-
138
- if (!userId) {
139
- return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
232
+ try {
233
+ const { conversationId } = await params;
234
+ const url = new URL(request.url);
235
+ const userId = url.searchParams.get('userId') || request.headers.get('X-User-Id');
236
+
237
+ if (!userId) {
238
+ return NextResponse.json({ error: 'userId is required' }, { status: 400 });
239
+ }
240
+
241
+ // Verify the conversation belongs to the user
242
+ const conv = await db
243
+ .select({
244
+ id: conversations.id,
245
+ title: conversations.title,
246
+ metadata: conversations.metadata,
247
+ })
248
+ .from(conversations)
249
+ .where(and(
250
+ eq(conversations.id, conversationId),
251
+ eq(conversations.userId, userId)
252
+ ))
253
+ .limit(1);
254
+
255
+ if (!conv.length) {
256
+ return NextResponse.json({ error: 'Conversation not found' }, { status: 404 });
257
+ }
258
+
259
+ const conversation = conv[0];
260
+
261
+ const dbMessages = await db
262
+ .select()
263
+ .from(messages)
264
+ .where(eq(messages.conversationId, conversationId))
265
+ .orderBy(asc(messages.createdAt))
266
+ .limit(1000);
267
+
268
+ const transformedMessages = dbMessages.map(msg => {
269
+ const metadata = msg.metadata as { parts?: any[] } | null;
270
+
271
+ if (metadata?.parts && Array.isArray(metadata.parts)) {
272
+ return {
273
+ id: msg.id,
274
+ role: msg.role,
275
+ content: msg.content,
276
+ created_at: msg.createdAt,
277
+ parts: metadata.parts
278
+ };
279
+ }
280
+
281
+ return {
282
+ id: msg.id,
283
+ role: msg.role,
284
+ content: msg.content,
285
+ created_at: msg.createdAt,
286
+ parts: msg.content ? [{ type: 'text', text: msg.content }] : undefined
287
+ };
288
+ });
289
+
290
+ return NextResponse.json({ conversation, messages: transformedMessages });
291
+ } catch (error) {
292
+ console.error('Error loading conversation:', error);
293
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
140
294
  }
141
-
142
- const messages = await loadChat(params.id);
143
- return NextResponse.json({ messages });
144
295
  }
145
296
  ```
146
297
 
147
298
  #### `app/api/chat/upload/route.ts` - File Upload (Optional)
148
299
 
149
300
  ```typescript
150
- import { NextResponse } from 'next/server';
151
301
  import { createClient } from '@supabase/supabase-js';
302
+ import { nanoid } from 'nanoid';
152
303
 
153
- const supabase = createClient(
154
- process.env.NEXT_PUBLIC_SUPABASE_URL!,
155
- process.env.SUPABASE_SERVICE_ROLE_KEY!
156
- );
304
+ const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
305
+ const supabaseServiceKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
157
306
 
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);
307
+ export async function POST(req: Request) {
308
+ try {
309
+ const formData = await req.formData();
310
+ const file = formData.get('file') as File;
311
+ const conversationId = formData.get('conversationId') as string;
312
+ const userId = formData.get('userId') as string;
313
+
314
+ if (!file) {
315
+ return Response.json({ error: 'No file provided' }, { status: 400 });
316
+ }
317
+
318
+ if (!userId) {
319
+ return Response.json({ error: 'userId is required' }, { status: 400 });
320
+ }
321
+
322
+ // Only images supported
323
+ if (!file.type.startsWith('image/')) {
324
+ return Response.json({ error: 'Only image files are supported' }, { status: 400 });
325
+ }
326
+
327
+ // 5MB limit
328
+ if (file.size > 5 * 1024 * 1024) {
329
+ return Response.json({ error: 'File size exceeds 5MB limit' }, { status: 400 });
330
+ }
331
+
332
+ const supabase = createClient(supabaseUrl, supabaseServiceKey);
333
+
334
+ const timestamp = Date.now();
335
+ const randomId = nanoid(8);
336
+ const safeFilename = file.name.replace(/[^a-zA-Z0-9.-]/g, '_');
337
+ const filePath = `${userId}/${conversationId || 'default'}/${timestamp}-${randomId}-${safeFilename}`;
338
+
339
+ const fileBuffer = await file.arrayBuffer();
340
+
341
+ const { error: uploadError } = await supabase.storage
342
+ .from('chat-attachments')
343
+ .upload(filePath, fileBuffer, {
344
+ contentType: file.type,
345
+ upsert: false,
346
+ });
172
347
 
173
- if (error) {
174
- return NextResponse.json({ error: error.message }, { status: 500 });
348
+ if (uploadError) {
349
+ return Response.json({ error: 'Failed to upload file' }, { status: 500 });
350
+ }
351
+
352
+ const { data: urlData } = supabase.storage
353
+ .from('chat-attachments')
354
+ .getPublicUrl(filePath);
355
+
356
+ return Response.json({
357
+ url: urlData.publicUrl,
358
+ filename: file.name,
359
+ mediaType: file.type,
360
+ size: file.size,
361
+ type: 'file',
362
+ });
363
+ } catch (error) {
364
+ return Response.json({ error: 'Internal server error' }, { status: 500 });
175
365
  }
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
366
  }
188
367
  ```
189
368
 
@@ -233,25 +412,6 @@ export default function RootLayout({ children }) {
233
412
  }
234
413
  ```
235
414
 
236
- **For a specific page only:**
237
-
238
- ```tsx
239
- // app/dashboard/page.tsx
240
- 'use client';
241
-
242
- import { ChatWidget } from '@mordn/chat-widget';
243
- import '@mordn/chat-widget/styles.css';
244
-
245
- export default function DashboardPage() {
246
- return (
247
- <div>
248
- <h1>Dashboard</h1>
249
- <ChatWidget userId="user-123" />
250
- </div>
251
- );
252
- }
253
- ```
254
-
255
415
  ## Props
256
416
 
257
417
  | Prop | Type | Default | Description |
@@ -260,9 +420,6 @@ export default function DashboardPage() {
260
420
  | `conversationId` | `string` | - | Load a specific conversation |
261
421
  | `initialMessages` | `array` | - | Pre-fill the chat with messages |
262
422
  | `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
423
  | `theme` | `ThemeConfig` | - | Theme configuration |
267
424
  | `features` | `FeatureConfig` | - | Feature toggles |
268
425
  | `display` | `DisplayConfig` | - | Display options |
@@ -272,9 +429,9 @@ export default function DashboardPage() {
272
429
  ```typescript
273
430
  {
274
431
  mode?: 'light' | 'dark';
275
- primaryColor?: string; // Hex color
276
- backgroundColor?: string; // Hex color
277
- textColor?: string; // Hex color
432
+ primaryColor?: string;
433
+ backgroundColor?: string;
434
+ textColor?: string;
278
435
  }
279
436
  ```
280
437
 
@@ -295,8 +452,8 @@ export default function DashboardPage() {
295
452
  defaultOpen?: boolean; // Start with chat open (default: false)
296
453
  showToggleButton?: boolean; // Show FAB toggle button (default: true)
297
454
  toggleButtonPosition?: {
298
- bottom?: string; // e.g., '24px'
299
- right?: string; // e.g., '24px'
455
+ bottom?: string;
456
+ right?: string;
300
457
  };
301
458
  }
302
459
  ```
@@ -1,4 +1,5 @@
1
- export { Conversation, Message, NewConversation, NewMessage, conversations, createChat, db, deleteConversation, getConversations, loadChat, messages, saveChat, updateConversationTitle } from '../db/index.mjs';
1
+ export { createChat, db, deleteConversation, getConversations, loadChat, saveChat, updateConversationTitle } from '../db/index.mjs';
2
+ export { C as Conversation, M as Message, N as NewConversation, a as NewMessage, c as conversations, m as messages } from '../index-uiN6exzS.mjs';
2
3
  export { and, asc, desc, eq, or, sql } from 'drizzle-orm';
3
4
  import 'ai';
4
5
  import 'drizzle-orm/postgres-js';
@@ -1,4 +1,5 @@
1
- export { Conversation, Message, NewConversation, NewMessage, conversations, createChat, db, deleteConversation, getConversations, loadChat, messages, saveChat, updateConversationTitle } from '../db/index.js';
1
+ export { createChat, db, deleteConversation, getConversations, loadChat, saveChat, updateConversationTitle } from '../db/index.js';
2
+ export { C as Conversation, M as Message, N as NewConversation, a as NewMessage, c as conversations, m as messages } from '../index-uiN6exzS.js';
2
3
  export { and, asc, desc, eq, or, sql } from 'drizzle-orm';
3
4
  import 'ai';
4
5
  import 'drizzle-orm/postgres-js';
package/dist/api/index.js CHANGED
@@ -176,8 +176,9 @@ async function saveChat({
176
176
  for (const msg of newMessages) {
177
177
  const textPart = msg.parts?.find((p) => p.type === "text");
178
178
  const fileParts = msg.parts?.filter((p) => p.type === "file") || [];
179
+ const messageId = msg.role === "assistant" ? (0, import_ai.generateId)() : msg.id;
179
180
  await db.insert(messages).values({
180
- id: msg.id,
181
+ id: messageId,
181
182
  conversationId: chatId,
182
183
  role: msg.role,
183
184
  content: textPart?.text || "",
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/api/index.ts","../../src/db/chat-store.ts","../../src/db/index.ts","../../src/db/schema.ts"],"sourcesContent":["/**\n * API Helpers for Chat Widget\n *\n * These utilities help you create the required API routes for the chat widget.\n * Copy the route examples to your Next.js app/api folder.\n */\n\nexport {\n createChat,\n loadChat,\n saveChat,\n getConversations,\n updateConversationTitle,\n deleteConversation\n} from '../db/chat-store';\n\nexport { db, conversations, messages } from '../db';\nexport type { Conversation, Message, NewConversation, NewMessage } from '../db/schema';\n\n// Re-export drizzle utilities for convenience\nexport { eq, and, or, desc, asc, sql } from 'drizzle-orm';\n","import { generateId, UIMessage } from 'ai';\nimport { db, conversations, messages } from './index';\nimport { eq, desc, asc, sql } from 'drizzle-orm';\n\n/**\n * Create a new conversation\n */\nexport async function createChat(userId: string): Promise<string> {\n const id = generateId();\n\n await db.insert(conversations).values({\n id,\n userId,\n title: 'New Chat',\n metadata: {},\n });\n\n return id;\n}\n\n/**\n * Load messages for a conversation\n */\nexport async function loadChat(conversationId: string): Promise<UIMessage[]> {\n try {\n const dbMessages = await db\n .select()\n .from(messages)\n .where(eq(messages.conversationId, conversationId))\n .orderBy(asc(messages.createdAt));\n\n if (!dbMessages.length) return [];\n\n // Convert database messages to UIMessage format\n return dbMessages.map((msg) => {\n // If we have metadata with parts, use those (includes reasoning)\n const metadata = msg.metadata as { parts?: any[] } | null;\n if (metadata?.parts && Array.isArray(metadata.parts)) {\n return {\n id: msg.id,\n role: msg.role as 'user' | 'assistant' | 'system',\n parts: metadata.parts,\n createdAt: msg.createdAt,\n };\n }\n\n // Fallback to simple text message\n return {\n id: msg.id,\n role: msg.role as 'user' | 'assistant' | 'system',\n parts: [{ type: 'text', text: msg.content }],\n createdAt: msg.createdAt,\n };\n });\n } catch (error) {\n console.error('Error loading chat:', error);\n return [];\n }\n}\n\n/**\n * Update conversation title\n */\nexport async function updateConversationTitle(\n chatId: string,\n title: string\n): Promise<void> {\n try {\n await db\n .update(conversations)\n .set({ title, updatedAt: new Date() })\n .where(eq(conversations.id, chatId));\n } catch (error) {\n console.error('Error updating conversation title:', error);\n }\n}\n\n/**\n * Save messages to a conversation\n */\nexport async function saveChat({\n chatId,\n messages: chatMessages,\n model,\n userId,\n}: {\n chatId: string;\n messages: UIMessage[];\n model?: string;\n userId: string;\n}): Promise<void> {\n if (!userId) {\n console.error('userId is required for saveChat');\n return;\n }\n\n try {\n // Verify conversation exists\n const existingConv = await db\n .select({ id: conversations.id, title: conversations.title })\n .from(conversations)\n .where(eq(conversations.id, chatId))\n .limit(1);\n\n if (!existingConv.length) {\n console.error('Conversation not found:', chatId);\n return;\n }\n\n const conv = existingConv[0];\n\n // Update title if this is the first user message and title is still \"New Chat\"\n if (conv.title === 'New Chat') {\n const firstUserMessage = chatMessages.find((m) => m.role === 'user');\n if (firstUserMessage) {\n const textPart = firstUserMessage.parts?.find((p) => p.type === 'text') as { text: string } | undefined;\n if (textPart?.text) {\n const newTitle = textPart.text.slice(0, 100);\n await updateConversationTitle(chatId, newTitle);\n }\n }\n }\n\n // Get existing message IDs from database\n const existingMessages = await db\n .select({ id: messages.id })\n .from(messages)\n .where(eq(messages.conversationId, chatId));\n\n const existingIds = new Set(existingMessages.map((m) => m.id));\n\n // Insert only new messages\n const newMessages = chatMessages.filter((msg) => !existingIds.has(msg.id));\n\n if (newMessages.length > 0) {\n for (const msg of newMessages) {\n const textPart = msg.parts?.find((p) => p.type === 'text') as { text: string } | undefined;\n const fileParts = msg.parts?.filter((p) => p.type === 'file') || [];\n\n await db.insert(messages).values({\n id: msg.id,\n conversationId: chatId,\n role: msg.role,\n content: textPart?.text || '',\n files: fileParts,\n model: model || 'openai/gpt-4o-mini',\n metadata: { parts: msg.parts || [] },\n });\n }\n\n // Update conversation's updatedAt\n await db\n .update(conversations)\n .set({ updatedAt: new Date() })\n .where(eq(conversations.id, chatId));\n }\n } catch (error) {\n console.error('Error saving chat:', error);\n }\n}\n\n/**\n * Get all conversations for a user\n */\nexport async function getConversations(userId: string) {\n try {\n const result = await db\n .select({\n id: conversations.id,\n title: conversations.title,\n createdAt: conversations.createdAt,\n updatedAt: conversations.updatedAt,\n metadata: conversations.metadata,\n messageCount: sql<number>`(\n SELECT COUNT(*) FROM ${messages}\n WHERE ${messages.conversationId} = ${conversations.id}\n )`,\n })\n .from(conversations)\n .where(eq(conversations.userId, userId))\n .orderBy(desc(conversations.updatedAt));\n\n return result;\n } catch (error) {\n console.error('Error getting conversations:', error);\n return [];\n }\n}\n\n/**\n * Delete a conversation and all its messages\n */\nexport async function deleteConversation(chatId: string): Promise<void> {\n try {\n // Messages are deleted automatically due to cascade\n await db.delete(conversations).where(eq(conversations.id, chatId));\n } catch (error) {\n console.error('Error deleting conversation:', error);\n }\n}\n","import 'server-only';\nimport { drizzle } from 'drizzle-orm/postgres-js';\nimport postgres from 'postgres';\nimport * as schema from './schema';\n\n// Create postgres connection\nconst connectionString = process.env.DATABASE_URL!;\n\n// Disable prefetch as it is not supported for \"Transaction\" pool mode\nconst client = postgres(connectionString, { prepare: false });\n\n// Create drizzle database instance\nexport const db = drizzle(client, { schema });\n\n// Export schema for convenience\nexport * from './schema';\n\n// Export chat store functions\nexport * from './chat-store';\n","import { pgTable, text, timestamp, jsonb, index } from 'drizzle-orm/pg-core';\n\n/**\n * Conversations table\n * Stores chat conversation metadata\n */\nexport const conversations = pgTable('conversations', {\n id: text('id').primaryKey(),\n userId: text('user_id').notNull(),\n title: text('title').notNull().default('New Chat'),\n metadata: jsonb('metadata'),\n createdAt: timestamp('created_at').defaultNow().notNull(),\n updatedAt: timestamp('updated_at').defaultNow().notNull(),\n}, (table) => [\n index('conversations_user_id_idx').on(table.userId),\n index('conversations_updated_at_idx').on(table.updatedAt),\n]);\n\n/**\n * Messages table\n * Stores individual chat messages\n */\nexport const messages = pgTable('messages', {\n id: text('id').primaryKey(),\n conversationId: text('conversation_id').notNull().references(() => conversations.id, { onDelete: 'cascade' }),\n role: text('role').notNull(), // 'user' | 'assistant' | 'system'\n content: text('content').notNull(),\n files: jsonb('files'), // Array of file attachments\n model: text('model'), // AI model used\n metadata: jsonb('metadata'), // Additional data (parts, reasoning, etc.)\n createdAt: timestamp('created_at').defaultNow().notNull(),\n}, (table) => [\n index('messages_conversation_id_idx').on(table.conversationId),\n index('messages_created_at_idx').on(table.createdAt),\n]);\n\n// Type exports for use in application code\nexport type Conversation = typeof conversations.$inferSelect;\nexport type NewConversation = typeof conversations.$inferInsert;\nexport type Message = typeof messages.$inferSelect;\nexport type NewMessage = typeof messages.$inferInsert;\n"],"mappings":"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","names":["postgres","import_drizzle_orm"]}
1
+ {"version":3,"sources":["../../src/api/index.ts","../../src/db/chat-store.ts","../../src/db/index.ts","../../src/db/schema.ts"],"sourcesContent":["/**\n * API Helpers for Chat Widget\n *\n * These utilities help you create the required API routes for the chat widget.\n * Copy the route examples to your Next.js app/api folder.\n */\n\nexport {\n createChat,\n loadChat,\n saveChat,\n getConversations,\n updateConversationTitle,\n deleteConversation\n} from '../db/chat-store';\n\nexport { db, conversations, messages } from '../db';\nexport type { Conversation, Message, NewConversation, NewMessage } from '../db/schema';\n\n// Re-export drizzle utilities for convenience\nexport { eq, and, or, desc, asc, sql } from 'drizzle-orm';\n","import { generateId, UIMessage } from 'ai';\nimport { db, conversations, messages } from './index';\nimport { eq, desc, asc, sql } from 'drizzle-orm';\n\n/**\n * Create a new conversation\n */\nexport async function createChat(userId: string): Promise<string> {\n const id = generateId();\n\n await db.insert(conversations).values({\n id,\n userId,\n title: 'New Chat',\n metadata: {},\n });\n\n return id;\n}\n\n/**\n * Load messages for a conversation\n */\nexport async function loadChat(conversationId: string): Promise<UIMessage[]> {\n try {\n const dbMessages = await db\n .select()\n .from(messages)\n .where(eq(messages.conversationId, conversationId))\n .orderBy(asc(messages.createdAt));\n\n if (!dbMessages.length) return [];\n\n // Convert database messages to UIMessage format\n return dbMessages.map((msg) => {\n // If we have metadata with parts, use those (includes reasoning)\n const metadata = msg.metadata as { parts?: any[] } | null;\n if (metadata?.parts && Array.isArray(metadata.parts)) {\n return {\n id: msg.id,\n role: msg.role as 'user' | 'assistant' | 'system',\n parts: metadata.parts,\n createdAt: msg.createdAt,\n };\n }\n\n // Fallback to simple text message\n return {\n id: msg.id,\n role: msg.role as 'user' | 'assistant' | 'system',\n parts: [{ type: 'text', text: msg.content }],\n createdAt: msg.createdAt,\n };\n });\n } catch (error) {\n console.error('Error loading chat:', error);\n return [];\n }\n}\n\n/**\n * Update conversation title\n */\nexport async function updateConversationTitle(\n chatId: string,\n title: string\n): Promise<void> {\n try {\n await db\n .update(conversations)\n .set({ title, updatedAt: new Date() })\n .where(eq(conversations.id, chatId));\n } catch (error) {\n console.error('Error updating conversation title:', error);\n }\n}\n\n/**\n * Save messages to a conversation\n */\nexport async function saveChat({\n chatId,\n messages: chatMessages,\n model,\n userId,\n}: {\n chatId: string;\n messages: UIMessage[];\n model?: string;\n userId: string;\n}): Promise<void> {\n if (!userId) {\n console.error('userId is required for saveChat');\n return;\n }\n\n try {\n // Verify conversation exists\n const existingConv = await db\n .select({ id: conversations.id, title: conversations.title })\n .from(conversations)\n .where(eq(conversations.id, chatId))\n .limit(1);\n\n if (!existingConv.length) {\n console.error('Conversation not found:', chatId);\n return;\n }\n\n const conv = existingConv[0];\n\n // Update title if this is the first user message and title is still \"New Chat\"\n if (conv.title === 'New Chat') {\n const firstUserMessage = chatMessages.find((m) => m.role === 'user');\n if (firstUserMessage) {\n const textPart = firstUserMessage.parts?.find((p) => p.type === 'text') as { text: string } | undefined;\n if (textPart?.text) {\n const newTitle = textPart.text.slice(0, 100);\n await updateConversationTitle(chatId, newTitle);\n }\n }\n }\n\n // Get existing message IDs from database\n const existingMessages = await db\n .select({ id: messages.id })\n .from(messages)\n .where(eq(messages.conversationId, chatId));\n\n const existingIds = new Set(existingMessages.map((m) => m.id));\n\n // Insert only new messages\n const newMessages = chatMessages.filter((msg) => !existingIds.has(msg.id));\n\n if (newMessages.length > 0) {\n for (const msg of newMessages) {\n const textPart = msg.parts?.find((p) => p.type === 'text') as { text: string } | undefined;\n const fileParts = msg.parts?.filter((p) => p.type === 'file') || [];\n\n // Generate ID for assistant messages, use existing ID for user messages\n const messageId = msg.role === 'assistant' ? generateId() : msg.id;\n\n await db.insert(messages).values({\n id: messageId,\n conversationId: chatId,\n role: msg.role,\n content: textPart?.text || '',\n files: fileParts,\n model: model || 'openai/gpt-4o-mini',\n metadata: { parts: msg.parts || [] },\n });\n }\n\n // Update conversation's updatedAt\n await db\n .update(conversations)\n .set({ updatedAt: new Date() })\n .where(eq(conversations.id, chatId));\n }\n } catch (error) {\n console.error('Error saving chat:', error);\n }\n}\n\n/**\n * Get all conversations for a user\n */\nexport async function getConversations(userId: string) {\n try {\n const result = await db\n .select({\n id: conversations.id,\n title: conversations.title,\n createdAt: conversations.createdAt,\n updatedAt: conversations.updatedAt,\n metadata: conversations.metadata,\n messageCount: sql<number>`(\n SELECT COUNT(*) FROM ${messages}\n WHERE ${messages.conversationId} = ${conversations.id}\n )`,\n })\n .from(conversations)\n .where(eq(conversations.userId, userId))\n .orderBy(desc(conversations.updatedAt));\n\n return result;\n } catch (error) {\n console.error('Error getting conversations:', error);\n return [];\n }\n}\n\n/**\n * Delete a conversation and all its messages\n */\nexport async function deleteConversation(chatId: string): Promise<void> {\n try {\n // Messages are deleted automatically due to cascade\n await db.delete(conversations).where(eq(conversations.id, chatId));\n } catch (error) {\n console.error('Error deleting conversation:', error);\n }\n}\n","import 'server-only';\nimport { drizzle } from 'drizzle-orm/postgres-js';\nimport postgres from 'postgres';\nimport * as schema from './schema';\n\n// Create postgres connection\nconst connectionString = process.env.DATABASE_URL!;\n\n// Disable prefetch as it is not supported for \"Transaction\" pool mode\nconst client = postgres(connectionString, { prepare: false });\n\n// Create drizzle database instance\nexport const db = drizzle(client, { schema });\n\n// Export schema for convenience\nexport * from './schema';\n\n// Export chat store functions\nexport * from './chat-store';\n","import { pgTable, text, timestamp, jsonb, index } from 'drizzle-orm/pg-core';\n\n/**\n * Conversations table\n * Stores chat conversation metadata\n */\nexport const conversations = pgTable('conversations', {\n id: text('id').primaryKey(),\n userId: text('user_id').notNull(),\n title: text('title').notNull().default('New Chat'),\n metadata: jsonb('metadata'),\n createdAt: timestamp('created_at').defaultNow().notNull(),\n updatedAt: timestamp('updated_at').defaultNow().notNull(),\n}, (table) => [\n index('conversations_user_id_idx').on(table.userId),\n index('conversations_updated_at_idx').on(table.updatedAt),\n]);\n\n/**\n * Messages table\n * Stores individual chat messages\n */\nexport const messages = pgTable('messages', {\n id: text('id').primaryKey(),\n conversationId: text('conversation_id').notNull().references(() => conversations.id, { onDelete: 'cascade' }),\n role: text('role').notNull(), // 'user' | 'assistant' | 'system'\n content: text('content').notNull(),\n files: jsonb('files'), // Array of file attachments\n model: text('model'), // AI model used\n metadata: jsonb('metadata'), // Additional data (parts, reasoning, etc.)\n createdAt: timestamp('created_at').defaultNow().notNull(),\n}, (table) => [\n index('messages_conversation_id_idx').on(table.conversationId),\n index('messages_created_at_idx').on(table.createdAt),\n]);\n\n// Type exports for use in application code\nexport type Conversation = typeof conversations.$inferSelect;\nexport type NewConversation = typeof conversations.$inferInsert;\nexport type Message = typeof messages.$inferSelect;\nexport type NewMessage = typeof messages.$inferInsert;\n"],"mappings":"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","names":["postgres","import_drizzle_orm"]}
@@ -132,8 +132,9 @@ async function saveChat({
132
132
  for (const msg of newMessages) {
133
133
  const textPart = msg.parts?.find((p) => p.type === "text");
134
134
  const fileParts = msg.parts?.filter((p) => p.type === "file") || [];
135
+ const messageId = msg.role === "assistant" ? generateId() : msg.id;
135
136
  await db.insert(messages).values({
136
- id: msg.id,
137
+ id: messageId,
137
138
  conversationId: chatId,
138
139
  role: msg.role,
139
140
  content: textPart?.text || "",
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/db/chat-store.ts","../../src/db/index.ts","../../src/db/schema.ts","../../src/api/index.ts"],"sourcesContent":["import { generateId, UIMessage } from 'ai';\nimport { db, conversations, messages } from './index';\nimport { eq, desc, asc, sql } from 'drizzle-orm';\n\n/**\n * Create a new conversation\n */\nexport async function createChat(userId: string): Promise<string> {\n const id = generateId();\n\n await db.insert(conversations).values({\n id,\n userId,\n title: 'New Chat',\n metadata: {},\n });\n\n return id;\n}\n\n/**\n * Load messages for a conversation\n */\nexport async function loadChat(conversationId: string): Promise<UIMessage[]> {\n try {\n const dbMessages = await db\n .select()\n .from(messages)\n .where(eq(messages.conversationId, conversationId))\n .orderBy(asc(messages.createdAt));\n\n if (!dbMessages.length) return [];\n\n // Convert database messages to UIMessage format\n return dbMessages.map((msg) => {\n // If we have metadata with parts, use those (includes reasoning)\n const metadata = msg.metadata as { parts?: any[] } | null;\n if (metadata?.parts && Array.isArray(metadata.parts)) {\n return {\n id: msg.id,\n role: msg.role as 'user' | 'assistant' | 'system',\n parts: metadata.parts,\n createdAt: msg.createdAt,\n };\n }\n\n // Fallback to simple text message\n return {\n id: msg.id,\n role: msg.role as 'user' | 'assistant' | 'system',\n parts: [{ type: 'text', text: msg.content }],\n createdAt: msg.createdAt,\n };\n });\n } catch (error) {\n console.error('Error loading chat:', error);\n return [];\n }\n}\n\n/**\n * Update conversation title\n */\nexport async function updateConversationTitle(\n chatId: string,\n title: string\n): Promise<void> {\n try {\n await db\n .update(conversations)\n .set({ title, updatedAt: new Date() })\n .where(eq(conversations.id, chatId));\n } catch (error) {\n console.error('Error updating conversation title:', error);\n }\n}\n\n/**\n * Save messages to a conversation\n */\nexport async function saveChat({\n chatId,\n messages: chatMessages,\n model,\n userId,\n}: {\n chatId: string;\n messages: UIMessage[];\n model?: string;\n userId: string;\n}): Promise<void> {\n if (!userId) {\n console.error('userId is required for saveChat');\n return;\n }\n\n try {\n // Verify conversation exists\n const existingConv = await db\n .select({ id: conversations.id, title: conversations.title })\n .from(conversations)\n .where(eq(conversations.id, chatId))\n .limit(1);\n\n if (!existingConv.length) {\n console.error('Conversation not found:', chatId);\n return;\n }\n\n const conv = existingConv[0];\n\n // Update title if this is the first user message and title is still \"New Chat\"\n if (conv.title === 'New Chat') {\n const firstUserMessage = chatMessages.find((m) => m.role === 'user');\n if (firstUserMessage) {\n const textPart = firstUserMessage.parts?.find((p) => p.type === 'text') as { text: string } | undefined;\n if (textPart?.text) {\n const newTitle = textPart.text.slice(0, 100);\n await updateConversationTitle(chatId, newTitle);\n }\n }\n }\n\n // Get existing message IDs from database\n const existingMessages = await db\n .select({ id: messages.id })\n .from(messages)\n .where(eq(messages.conversationId, chatId));\n\n const existingIds = new Set(existingMessages.map((m) => m.id));\n\n // Insert only new messages\n const newMessages = chatMessages.filter((msg) => !existingIds.has(msg.id));\n\n if (newMessages.length > 0) {\n for (const msg of newMessages) {\n const textPart = msg.parts?.find((p) => p.type === 'text') as { text: string } | undefined;\n const fileParts = msg.parts?.filter((p) => p.type === 'file') || [];\n\n await db.insert(messages).values({\n id: msg.id,\n conversationId: chatId,\n role: msg.role,\n content: textPart?.text || '',\n files: fileParts,\n model: model || 'openai/gpt-4o-mini',\n metadata: { parts: msg.parts || [] },\n });\n }\n\n // Update conversation's updatedAt\n await db\n .update(conversations)\n .set({ updatedAt: new Date() })\n .where(eq(conversations.id, chatId));\n }\n } catch (error) {\n console.error('Error saving chat:', error);\n }\n}\n\n/**\n * Get all conversations for a user\n */\nexport async function getConversations(userId: string) {\n try {\n const result = await db\n .select({\n id: conversations.id,\n title: conversations.title,\n createdAt: conversations.createdAt,\n updatedAt: conversations.updatedAt,\n metadata: conversations.metadata,\n messageCount: sql<number>`(\n SELECT COUNT(*) FROM ${messages}\n WHERE ${messages.conversationId} = ${conversations.id}\n )`,\n })\n .from(conversations)\n .where(eq(conversations.userId, userId))\n .orderBy(desc(conversations.updatedAt));\n\n return result;\n } catch (error) {\n console.error('Error getting conversations:', error);\n return [];\n }\n}\n\n/**\n * Delete a conversation and all its messages\n */\nexport async function deleteConversation(chatId: string): Promise<void> {\n try {\n // Messages are deleted automatically due to cascade\n await db.delete(conversations).where(eq(conversations.id, chatId));\n } catch (error) {\n console.error('Error deleting conversation:', error);\n }\n}\n","import 'server-only';\nimport { drizzle } from 'drizzle-orm/postgres-js';\nimport postgres from 'postgres';\nimport * as schema from './schema';\n\n// Create postgres connection\nconst connectionString = process.env.DATABASE_URL!;\n\n// Disable prefetch as it is not supported for \"Transaction\" pool mode\nconst client = postgres(connectionString, { prepare: false });\n\n// Create drizzle database instance\nexport const db = drizzle(client, { schema });\n\n// Export schema for convenience\nexport * from './schema';\n\n// Export chat store functions\nexport * from './chat-store';\n","import { pgTable, text, timestamp, jsonb, index } from 'drizzle-orm/pg-core';\n\n/**\n * Conversations table\n * Stores chat conversation metadata\n */\nexport const conversations = pgTable('conversations', {\n id: text('id').primaryKey(),\n userId: text('user_id').notNull(),\n title: text('title').notNull().default('New Chat'),\n metadata: jsonb('metadata'),\n createdAt: timestamp('created_at').defaultNow().notNull(),\n updatedAt: timestamp('updated_at').defaultNow().notNull(),\n}, (table) => [\n index('conversations_user_id_idx').on(table.userId),\n index('conversations_updated_at_idx').on(table.updatedAt),\n]);\n\n/**\n * Messages table\n * Stores individual chat messages\n */\nexport const messages = pgTable('messages', {\n id: text('id').primaryKey(),\n conversationId: text('conversation_id').notNull().references(() => conversations.id, { onDelete: 'cascade' }),\n role: text('role').notNull(), // 'user' | 'assistant' | 'system'\n content: text('content').notNull(),\n files: jsonb('files'), // Array of file attachments\n model: text('model'), // AI model used\n metadata: jsonb('metadata'), // Additional data (parts, reasoning, etc.)\n createdAt: timestamp('created_at').defaultNow().notNull(),\n}, (table) => [\n index('messages_conversation_id_idx').on(table.conversationId),\n index('messages_created_at_idx').on(table.createdAt),\n]);\n\n// Type exports for use in application code\nexport type Conversation = typeof conversations.$inferSelect;\nexport type NewConversation = typeof conversations.$inferInsert;\nexport type Message = typeof messages.$inferSelect;\nexport type NewMessage = typeof messages.$inferInsert;\n","/**\n * API Helpers for Chat Widget\n *\n * These utilities help you create the required API routes for the chat widget.\n * Copy the route examples to your Next.js app/api folder.\n */\n\nexport {\n createChat,\n loadChat,\n saveChat,\n getConversations,\n updateConversationTitle,\n deleteConversation\n} from '../db/chat-store';\n\nexport { db, conversations, messages } from '../db';\nexport type { Conversation, Message, NewConversation, NewMessage } from '../db/schema';\n\n// Re-export drizzle utilities for convenience\nexport { eq, and, or, desc, asc, sql } from 'drizzle-orm';\n"],"mappings":";;;;;;;AAAA,SAAS,kBAA6B;;;ACAtC,OAAO;AACP,SAAS,eAAe;AACxB,OAAO,cAAc;;;ACFrB;AAAA;AAAA;AAAA;AAAA;AAAA,SAAS,SAAS,MAAM,WAAW,OAAO,aAAa;AAMhD,IAAM,gBAAgB,QAAQ,iBAAiB;AAAA,EACpD,IAAI,KAAK,IAAI,EAAE,WAAW;AAAA,EAC1B,QAAQ,KAAK,SAAS,EAAE,QAAQ;AAAA,EAChC,OAAO,KAAK,OAAO,EAAE,QAAQ,EAAE,QAAQ,UAAU;AAAA,EACjD,UAAU,MAAM,UAAU;AAAA,EAC1B,WAAW,UAAU,YAAY,EAAE,WAAW,EAAE,QAAQ;AAAA,EACxD,WAAW,UAAU,YAAY,EAAE,WAAW,EAAE,QAAQ;AAC1D,GAAG,CAAC,UAAU;AAAA,EACZ,MAAM,2BAA2B,EAAE,GAAG,MAAM,MAAM;AAAA,EAClD,MAAM,8BAA8B,EAAE,GAAG,MAAM,SAAS;AAC1D,CAAC;AAMM,IAAM,WAAW,QAAQ,YAAY;AAAA,EAC1C,IAAI,KAAK,IAAI,EAAE,WAAW;AAAA,EAC1B,gBAAgB,KAAK,iBAAiB,EAAE,QAAQ,EAAE,WAAW,MAAM,cAAc,IAAI,EAAE,UAAU,UAAU,CAAC;AAAA,EAC5G,MAAM,KAAK,MAAM,EAAE,QAAQ;AAAA;AAAA,EAC3B,SAAS,KAAK,SAAS,EAAE,QAAQ;AAAA,EACjC,OAAO,MAAM,OAAO;AAAA;AAAA,EACpB,OAAO,KAAK,OAAO;AAAA;AAAA,EACnB,UAAU,MAAM,UAAU;AAAA;AAAA,EAC1B,WAAW,UAAU,YAAY,EAAE,WAAW,EAAE,QAAQ;AAC1D,GAAG,CAAC,UAAU;AAAA,EACZ,MAAM,8BAA8B,EAAE,GAAG,MAAM,cAAc;AAAA,EAC7D,MAAM,yBAAyB,EAAE,GAAG,MAAM,SAAS;AACrD,CAAC;;;AD5BD,IAAM,mBAAmB,QAAQ,IAAI;AAGrC,IAAM,SAAS,SAAS,kBAAkB,EAAE,SAAS,MAAM,CAAC;AAGrD,IAAM,KAAK,QAAQ,QAAQ,EAAE,uBAAO,CAAC;;;ADV5C,SAAS,IAAI,MAAM,KAAK,WAAW;AAKnC,eAAsB,WAAW,QAAiC;AAChE,QAAM,KAAK,WAAW;AAEtB,QAAM,GAAG,OAAO,aAAa,EAAE,OAAO;AAAA,IACpC;AAAA,IACA;AAAA,IACA,OAAO;AAAA,IACP,UAAU,CAAC;AAAA,EACb,CAAC;AAED,SAAO;AACT;AAKA,eAAsB,SAAS,gBAA8C;AAC3E,MAAI;AACF,UAAM,aAAa,MAAM,GACtB,OAAO,EACP,KAAK,QAAQ,EACb,MAAM,GAAG,SAAS,gBAAgB,cAAc,CAAC,EACjD,QAAQ,IAAI,SAAS,SAAS,CAAC;AAElC,QAAI,CAAC,WAAW,OAAQ,QAAO,CAAC;AAGhC,WAAO,WAAW,IAAI,CAAC,QAAQ;AAE7B,YAAM,WAAW,IAAI;AACrB,UAAI,UAAU,SAAS,MAAM,QAAQ,SAAS,KAAK,GAAG;AACpD,eAAO;AAAA,UACL,IAAI,IAAI;AAAA,UACR,MAAM,IAAI;AAAA,UACV,OAAO,SAAS;AAAA,UAChB,WAAW,IAAI;AAAA,QACjB;AAAA,MACF;AAGA,aAAO;AAAA,QACL,IAAI,IAAI;AAAA,QACR,MAAM,IAAI;AAAA,QACV,OAAO,CAAC,EAAE,MAAM,QAAQ,MAAM,IAAI,QAAQ,CAAC;AAAA,QAC3C,WAAW,IAAI;AAAA,MACjB;AAAA,IACF,CAAC;AAAA,EACH,SAAS,OAAO;AACd,YAAQ,MAAM,uBAAuB,KAAK;AAC1C,WAAO,CAAC;AAAA,EACV;AACF;AAKA,eAAsB,wBACpB,QACA,OACe;AACf,MAAI;AACF,UAAM,GACH,OAAO,aAAa,EACpB,IAAI,EAAE,OAAO,WAAW,oBAAI,KAAK,EAAE,CAAC,EACpC,MAAM,GAAG,cAAc,IAAI,MAAM,CAAC;AAAA,EACvC,SAAS,OAAO;AACd,YAAQ,MAAM,sCAAsC,KAAK;AAAA,EAC3D;AACF;AAKA,eAAsB,SAAS;AAAA,EAC7B;AAAA,EACA,UAAU;AAAA,EACV;AAAA,EACA;AACF,GAKkB;AAChB,MAAI,CAAC,QAAQ;AACX,YAAQ,MAAM,iCAAiC;AAC/C;AAAA,EACF;AAEA,MAAI;AAEF,UAAM,eAAe,MAAM,GACxB,OAAO,EAAE,IAAI,cAAc,IAAI,OAAO,cAAc,MAAM,CAAC,EAC3D,KAAK,aAAa,EAClB,MAAM,GAAG,cAAc,IAAI,MAAM,CAAC,EAClC,MAAM,CAAC;AAEV,QAAI,CAAC,aAAa,QAAQ;AACxB,cAAQ,MAAM,2BAA2B,MAAM;AAC/C;AAAA,IACF;AAEA,UAAM,OAAO,aAAa,CAAC;AAG3B,QAAI,KAAK,UAAU,YAAY;AAC7B,YAAM,mBAAmB,aAAa,KAAK,CAAC,MAAM,EAAE,SAAS,MAAM;AACnE,UAAI,kBAAkB;AACpB,cAAM,WAAW,iBAAiB,OAAO,KAAK,CAAC,MAAM,EAAE,SAAS,MAAM;AACtE,YAAI,UAAU,MAAM;AAClB,gBAAM,WAAW,SAAS,KAAK,MAAM,GAAG,GAAG;AAC3C,gBAAM,wBAAwB,QAAQ,QAAQ;AAAA,QAChD;AAAA,MACF;AAAA,IACF;AAGA,UAAM,mBAAmB,MAAM,GAC5B,OAAO,EAAE,IAAI,SAAS,GAAG,CAAC,EAC1B,KAAK,QAAQ,EACb,MAAM,GAAG,SAAS,gBAAgB,MAAM,CAAC;AAE5C,UAAM,cAAc,IAAI,IAAI,iBAAiB,IAAI,CAAC,MAAM,EAAE,EAAE,CAAC;AAG7D,UAAM,cAAc,aAAa,OAAO,CAAC,QAAQ,CAAC,YAAY,IAAI,IAAI,EAAE,CAAC;AAEzE,QAAI,YAAY,SAAS,GAAG;AAC1B,iBAAW,OAAO,aAAa;AAC7B,cAAM,WAAW,IAAI,OAAO,KAAK,CAAC,MAAM,EAAE,SAAS,MAAM;AACzD,cAAM,YAAY,IAAI,OAAO,OAAO,CAAC,MAAM,EAAE,SAAS,MAAM,KAAK,CAAC;AAElE,cAAM,GAAG,OAAO,QAAQ,EAAE,OAAO;AAAA,UAC/B,IAAI,IAAI;AAAA,UACR,gBAAgB;AAAA,UAChB,MAAM,IAAI;AAAA,UACV,SAAS,UAAU,QAAQ;AAAA,UAC3B,OAAO;AAAA,UACP,OAAO,SAAS;AAAA,UAChB,UAAU,EAAE,OAAO,IAAI,SAAS,CAAC,EAAE;AAAA,QACrC,CAAC;AAAA,MACH;AAGA,YAAM,GACH,OAAO,aAAa,EACpB,IAAI,EAAE,WAAW,oBAAI,KAAK,EAAE,CAAC,EAC7B,MAAM,GAAG,cAAc,IAAI,MAAM,CAAC;AAAA,IACvC;AAAA,EACF,SAAS,OAAO;AACd,YAAQ,MAAM,sBAAsB,KAAK;AAAA,EAC3C;AACF;AAKA,eAAsB,iBAAiB,QAAgB;AACrD,MAAI;AACF,UAAM,SAAS,MAAM,GAClB,OAAO;AAAA,MACN,IAAI,cAAc;AAAA,MAClB,OAAO,cAAc;AAAA,MACrB,WAAW,cAAc;AAAA,MACzB,WAAW,cAAc;AAAA,MACzB,UAAU,cAAc;AAAA,MACxB,cAAc;AAAA,iCACW,QAAQ;AAAA,kBACvB,SAAS,cAAc,MAAM,cAAc,EAAE;AAAA;AAAA,IAEzD,CAAC,EACA,KAAK,aAAa,EAClB,MAAM,GAAG,cAAc,QAAQ,MAAM,CAAC,EACtC,QAAQ,KAAK,cAAc,SAAS,CAAC;AAExC,WAAO;AAAA,EACT,SAAS,OAAO;AACd,YAAQ,MAAM,gCAAgC,KAAK;AACnD,WAAO,CAAC;AAAA,EACV;AACF;AAKA,eAAsB,mBAAmB,QAA+B;AACtE,MAAI;AAEF,UAAM,GAAG,OAAO,aAAa,EAAE,MAAM,GAAG,cAAc,IAAI,MAAM,CAAC;AAAA,EACnE,SAAS,OAAO;AACd,YAAQ,MAAM,gCAAgC,KAAK;AAAA,EACrD;AACF;;;AGnLA,SAAS,MAAAA,KAAI,KAAK,IAAI,QAAAC,OAAM,OAAAC,MAAK,OAAAC,YAAW;","names":["eq","desc","asc","sql"]}
1
+ {"version":3,"sources":["../../src/db/chat-store.ts","../../src/db/index.ts","../../src/db/schema.ts","../../src/api/index.ts"],"sourcesContent":["import { generateId, UIMessage } from 'ai';\nimport { db, conversations, messages } from './index';\nimport { eq, desc, asc, sql } from 'drizzle-orm';\n\n/**\n * Create a new conversation\n */\nexport async function createChat(userId: string): Promise<string> {\n const id = generateId();\n\n await db.insert(conversations).values({\n id,\n userId,\n title: 'New Chat',\n metadata: {},\n });\n\n return id;\n}\n\n/**\n * Load messages for a conversation\n */\nexport async function loadChat(conversationId: string): Promise<UIMessage[]> {\n try {\n const dbMessages = await db\n .select()\n .from(messages)\n .where(eq(messages.conversationId, conversationId))\n .orderBy(asc(messages.createdAt));\n\n if (!dbMessages.length) return [];\n\n // Convert database messages to UIMessage format\n return dbMessages.map((msg) => {\n // If we have metadata with parts, use those (includes reasoning)\n const metadata = msg.metadata as { parts?: any[] } | null;\n if (metadata?.parts && Array.isArray(metadata.parts)) {\n return {\n id: msg.id,\n role: msg.role as 'user' | 'assistant' | 'system',\n parts: metadata.parts,\n createdAt: msg.createdAt,\n };\n }\n\n // Fallback to simple text message\n return {\n id: msg.id,\n role: msg.role as 'user' | 'assistant' | 'system',\n parts: [{ type: 'text', text: msg.content }],\n createdAt: msg.createdAt,\n };\n });\n } catch (error) {\n console.error('Error loading chat:', error);\n return [];\n }\n}\n\n/**\n * Update conversation title\n */\nexport async function updateConversationTitle(\n chatId: string,\n title: string\n): Promise<void> {\n try {\n await db\n .update(conversations)\n .set({ title, updatedAt: new Date() })\n .where(eq(conversations.id, chatId));\n } catch (error) {\n console.error('Error updating conversation title:', error);\n }\n}\n\n/**\n * Save messages to a conversation\n */\nexport async function saveChat({\n chatId,\n messages: chatMessages,\n model,\n userId,\n}: {\n chatId: string;\n messages: UIMessage[];\n model?: string;\n userId: string;\n}): Promise<void> {\n if (!userId) {\n console.error('userId is required for saveChat');\n return;\n }\n\n try {\n // Verify conversation exists\n const existingConv = await db\n .select({ id: conversations.id, title: conversations.title })\n .from(conversations)\n .where(eq(conversations.id, chatId))\n .limit(1);\n\n if (!existingConv.length) {\n console.error('Conversation not found:', chatId);\n return;\n }\n\n const conv = existingConv[0];\n\n // Update title if this is the first user message and title is still \"New Chat\"\n if (conv.title === 'New Chat') {\n const firstUserMessage = chatMessages.find((m) => m.role === 'user');\n if (firstUserMessage) {\n const textPart = firstUserMessage.parts?.find((p) => p.type === 'text') as { text: string } | undefined;\n if (textPart?.text) {\n const newTitle = textPart.text.slice(0, 100);\n await updateConversationTitle(chatId, newTitle);\n }\n }\n }\n\n // Get existing message IDs from database\n const existingMessages = await db\n .select({ id: messages.id })\n .from(messages)\n .where(eq(messages.conversationId, chatId));\n\n const existingIds = new Set(existingMessages.map((m) => m.id));\n\n // Insert only new messages\n const newMessages = chatMessages.filter((msg) => !existingIds.has(msg.id));\n\n if (newMessages.length > 0) {\n for (const msg of newMessages) {\n const textPart = msg.parts?.find((p) => p.type === 'text') as { text: string } | undefined;\n const fileParts = msg.parts?.filter((p) => p.type === 'file') || [];\n\n // Generate ID for assistant messages, use existing ID for user messages\n const messageId = msg.role === 'assistant' ? generateId() : msg.id;\n\n await db.insert(messages).values({\n id: messageId,\n conversationId: chatId,\n role: msg.role,\n content: textPart?.text || '',\n files: fileParts,\n model: model || 'openai/gpt-4o-mini',\n metadata: { parts: msg.parts || [] },\n });\n }\n\n // Update conversation's updatedAt\n await db\n .update(conversations)\n .set({ updatedAt: new Date() })\n .where(eq(conversations.id, chatId));\n }\n } catch (error) {\n console.error('Error saving chat:', error);\n }\n}\n\n/**\n * Get all conversations for a user\n */\nexport async function getConversations(userId: string) {\n try {\n const result = await db\n .select({\n id: conversations.id,\n title: conversations.title,\n createdAt: conversations.createdAt,\n updatedAt: conversations.updatedAt,\n metadata: conversations.metadata,\n messageCount: sql<number>`(\n SELECT COUNT(*) FROM ${messages}\n WHERE ${messages.conversationId} = ${conversations.id}\n )`,\n })\n .from(conversations)\n .where(eq(conversations.userId, userId))\n .orderBy(desc(conversations.updatedAt));\n\n return result;\n } catch (error) {\n console.error('Error getting conversations:', error);\n return [];\n }\n}\n\n/**\n * Delete a conversation and all its messages\n */\nexport async function deleteConversation(chatId: string): Promise<void> {\n try {\n // Messages are deleted automatically due to cascade\n await db.delete(conversations).where(eq(conversations.id, chatId));\n } catch (error) {\n console.error('Error deleting conversation:', error);\n }\n}\n","import 'server-only';\nimport { drizzle } from 'drizzle-orm/postgres-js';\nimport postgres from 'postgres';\nimport * as schema from './schema';\n\n// Create postgres connection\nconst connectionString = process.env.DATABASE_URL!;\n\n// Disable prefetch as it is not supported for \"Transaction\" pool mode\nconst client = postgres(connectionString, { prepare: false });\n\n// Create drizzle database instance\nexport const db = drizzle(client, { schema });\n\n// Export schema for convenience\nexport * from './schema';\n\n// Export chat store functions\nexport * from './chat-store';\n","import { pgTable, text, timestamp, jsonb, index } from 'drizzle-orm/pg-core';\n\n/**\n * Conversations table\n * Stores chat conversation metadata\n */\nexport const conversations = pgTable('conversations', {\n id: text('id').primaryKey(),\n userId: text('user_id').notNull(),\n title: text('title').notNull().default('New Chat'),\n metadata: jsonb('metadata'),\n createdAt: timestamp('created_at').defaultNow().notNull(),\n updatedAt: timestamp('updated_at').defaultNow().notNull(),\n}, (table) => [\n index('conversations_user_id_idx').on(table.userId),\n index('conversations_updated_at_idx').on(table.updatedAt),\n]);\n\n/**\n * Messages table\n * Stores individual chat messages\n */\nexport const messages = pgTable('messages', {\n id: text('id').primaryKey(),\n conversationId: text('conversation_id').notNull().references(() => conversations.id, { onDelete: 'cascade' }),\n role: text('role').notNull(), // 'user' | 'assistant' | 'system'\n content: text('content').notNull(),\n files: jsonb('files'), // Array of file attachments\n model: text('model'), // AI model used\n metadata: jsonb('metadata'), // Additional data (parts, reasoning, etc.)\n createdAt: timestamp('created_at').defaultNow().notNull(),\n}, (table) => [\n index('messages_conversation_id_idx').on(table.conversationId),\n index('messages_created_at_idx').on(table.createdAt),\n]);\n\n// Type exports for use in application code\nexport type Conversation = typeof conversations.$inferSelect;\nexport type NewConversation = typeof conversations.$inferInsert;\nexport type Message = typeof messages.$inferSelect;\nexport type NewMessage = typeof messages.$inferInsert;\n","/**\n * API Helpers for Chat Widget\n *\n * These utilities help you create the required API routes for the chat widget.\n * Copy the route examples to your Next.js app/api folder.\n */\n\nexport {\n createChat,\n loadChat,\n saveChat,\n getConversations,\n updateConversationTitle,\n deleteConversation\n} from '../db/chat-store';\n\nexport { db, conversations, messages } from '../db';\nexport type { Conversation, Message, NewConversation, NewMessage } from '../db/schema';\n\n// Re-export drizzle utilities for convenience\nexport { eq, and, or, desc, asc, sql } from 'drizzle-orm';\n"],"mappings":"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","names":["eq","desc","asc","sql"]}