@mordn/chat-widget 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Arnav Gupta
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,338 @@
1
+ # @mordn/chat-widget
2
+
3
+ A customizable AI chat widget for React/Next.js applications with built-in conversation persistence.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @mordn/chat-widget
9
+ ```
10
+
11
+ ## Requirements
12
+
13
+ - Next.js 14+ (App Router)
14
+ - React 18+
15
+ - PostgreSQL database (Supabase recommended)
16
+ - Tailwind CSS v4
17
+
18
+ ## Setup
19
+
20
+ ### 1. Environment Variable
21
+
22
+ Add your database connection string:
23
+
24
+ ```env
25
+ DATABASE_URL="postgresql://postgres.xxx:[PASSWORD]@aws-0-region.pooler.supabase.com:6543/postgres"
26
+ ```
27
+
28
+ ### 2. Database Setup
29
+
30
+ Install drizzle-kit as a dev dependency:
31
+
32
+ ```bash
33
+ npm install drizzle-kit --save-dev
34
+ ```
35
+
36
+ Create `drizzle.config.ts` in your project root:
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
+ ```
50
+
51
+ Run the migration to create tables:
52
+
53
+ ```bash
54
+ npx drizzle-kit push
55
+ ```
56
+
57
+ ### 3. API Routes
58
+
59
+ Create the following API routes in your Next.js app:
60
+
61
+ #### `app/api/chat/route.ts` - Main Chat Endpoint
62
+
63
+ ```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
+
68
+ export async function POST(req: Request) {
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
+ }
124
+ ```
125
+
126
+ #### `app/api/chat/history/[id]/route.ts` - Get Conversation Messages
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:
193
+
194
+ ```tsx
195
+ // components/chat-provider.tsx
196
+ 'use client';
197
+
198
+ import { ChatWidget } from '@mordn/chat-widget';
199
+ 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
+
209
+ return (
210
+ <ChatWidget
211
+ userId={user.id}
212
+ theme={{ mode: 'dark' }}
213
+ />
214
+ );
215
+ }
216
+ ```
217
+
218
+ Add to your root layout:
219
+
220
+ ```tsx
221
+ // app/layout.tsx
222
+ import { ChatProvider } from '@/components/chat-provider';
223
+
224
+ export default function RootLayout({ children }) {
225
+ return (
226
+ <html>
227
+ <body>
228
+ {children}
229
+ <ChatProvider />
230
+ </body>
231
+ </html>
232
+ );
233
+ }
234
+ ```
235
+
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
+ ## Props
256
+
257
+ | Prop | Type | Default | Description |
258
+ |------|------|---------|-------------|
259
+ | `userId` | `string` | **required** | User identifier for storing conversations |
260
+ | `conversationId` | `string` | - | Load a specific conversation |
261
+ | `initialMessages` | `array` | - | Pre-fill the chat with messages |
262
+ | `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
+ | `theme` | `ThemeConfig` | - | Theme configuration |
267
+ | `features` | `FeatureConfig` | - | Feature toggles |
268
+ | `display` | `DisplayConfig` | - | Display options |
269
+
270
+ ### ThemeConfig
271
+
272
+ ```typescript
273
+ {
274
+ mode?: 'light' | 'dark';
275
+ primaryColor?: string; // Hex color
276
+ backgroundColor?: string; // Hex color
277
+ textColor?: string; // Hex color
278
+ }
279
+ ```
280
+
281
+ ### FeatureConfig
282
+
283
+ ```typescript
284
+ {
285
+ fileUpload?: boolean; // Enable file attachments (default: true)
286
+ webSearch?: boolean; // Enable web search toggle
287
+ }
288
+ ```
289
+
290
+ ### DisplayConfig
291
+
292
+ ```typescript
293
+ {
294
+ width?: string; // e.g., '400px' or '30vw' (default: '30vw')
295
+ defaultOpen?: boolean; // Start with chat open (default: false)
296
+ showToggleButton?: boolean; // Show FAB toggle button (default: true)
297
+ toggleButtonPosition?: {
298
+ bottom?: string; // e.g., '24px'
299
+ right?: string; // e.g., '24px'
300
+ };
301
+ }
302
+ ```
303
+
304
+ ## Exports
305
+
306
+ ```typescript
307
+ // Main component
308
+ import { ChatWidget } from '@mordn/chat-widget';
309
+ import '@mordn/chat-widget/styles.css';
310
+
311
+ // Database utilities (server-side only)
312
+ import {
313
+ db,
314
+ conversations,
315
+ messages,
316
+ createChat,
317
+ loadChat,
318
+ saveChat,
319
+ getConversations,
320
+ deleteConversation,
321
+ updateConversationTitle,
322
+ eq, and, or, desc, asc, sql
323
+ } from '@mordn/chat-widget/api';
324
+
325
+ // Types
326
+ import type {
327
+ ChatWidgetConfig,
328
+ ThemeConfig,
329
+ FeatureConfig,
330
+ DisplayConfig,
331
+ Conversation,
332
+ Message
333
+ } from '@mordn/chat-widget';
334
+ ```
335
+
336
+ ## License
337
+
338
+ MIT
@@ -0,0 +1,6 @@
1
+ export { Conversation, Message, NewConversation, NewMessage, conversations, createChat, db, deleteConversation, getConversations, loadChat, messages, saveChat, updateConversationTitle } from '../db/index.mjs';
2
+ export { and, asc, desc, eq, or, sql } from 'drizzle-orm';
3
+ import 'ai';
4
+ import 'drizzle-orm/postgres-js';
5
+ import 'postgres';
6
+ import 'drizzle-orm/pg-core';
@@ -0,0 +1,6 @@
1
+ export { Conversation, Message, NewConversation, NewMessage, conversations, createChat, db, deleteConversation, getConversations, loadChat, messages, saveChat, updateConversationTitle } from '../db/index.js';
2
+ export { and, asc, desc, eq, or, sql } from 'drizzle-orm';
3
+ import 'ai';
4
+ import 'drizzle-orm/postgres-js';
5
+ import 'postgres';
6
+ import 'drizzle-orm/pg-core';
@@ -0,0 +1,242 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc3) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc3 = __getOwnPropDesc(from, key)) || desc3.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/api/index.ts
31
+ var api_exports = {};
32
+ __export(api_exports, {
33
+ and: () => import_drizzle_orm2.and,
34
+ asc: () => import_drizzle_orm2.asc,
35
+ conversations: () => conversations,
36
+ createChat: () => createChat,
37
+ db: () => db,
38
+ deleteConversation: () => deleteConversation,
39
+ desc: () => import_drizzle_orm2.desc,
40
+ eq: () => import_drizzle_orm2.eq,
41
+ getConversations: () => getConversations,
42
+ loadChat: () => loadChat,
43
+ messages: () => messages,
44
+ or: () => import_drizzle_orm2.or,
45
+ saveChat: () => saveChat,
46
+ sql: () => import_drizzle_orm2.sql,
47
+ updateConversationTitle: () => updateConversationTitle
48
+ });
49
+ module.exports = __toCommonJS(api_exports);
50
+
51
+ // src/db/chat-store.ts
52
+ var import_ai = require("ai");
53
+
54
+ // src/db/index.ts
55
+ var import_server_only = require("server-only");
56
+ var import_postgres_js = require("drizzle-orm/postgres-js");
57
+ var import_postgres = __toESM(require("postgres"));
58
+
59
+ // src/db/schema.ts
60
+ var schema_exports = {};
61
+ __export(schema_exports, {
62
+ conversations: () => conversations,
63
+ messages: () => messages
64
+ });
65
+ var import_pg_core = require("drizzle-orm/pg-core");
66
+ var conversations = (0, import_pg_core.pgTable)("conversations", {
67
+ id: (0, import_pg_core.text)("id").primaryKey(),
68
+ userId: (0, import_pg_core.text)("user_id").notNull(),
69
+ title: (0, import_pg_core.text)("title").notNull().default("New Chat"),
70
+ metadata: (0, import_pg_core.jsonb)("metadata"),
71
+ createdAt: (0, import_pg_core.timestamp)("created_at").defaultNow().notNull(),
72
+ updatedAt: (0, import_pg_core.timestamp)("updated_at").defaultNow().notNull()
73
+ }, (table) => [
74
+ (0, import_pg_core.index)("conversations_user_id_idx").on(table.userId),
75
+ (0, import_pg_core.index)("conversations_updated_at_idx").on(table.updatedAt)
76
+ ]);
77
+ var messages = (0, import_pg_core.pgTable)("messages", {
78
+ id: (0, import_pg_core.text)("id").primaryKey(),
79
+ conversationId: (0, import_pg_core.text)("conversation_id").notNull().references(() => conversations.id, { onDelete: "cascade" }),
80
+ role: (0, import_pg_core.text)("role").notNull(),
81
+ // 'user' | 'assistant' | 'system'
82
+ content: (0, import_pg_core.text)("content").notNull(),
83
+ files: (0, import_pg_core.jsonb)("files"),
84
+ // Array of file attachments
85
+ model: (0, import_pg_core.text)("model"),
86
+ // AI model used
87
+ metadata: (0, import_pg_core.jsonb)("metadata"),
88
+ // Additional data (parts, reasoning, etc.)
89
+ createdAt: (0, import_pg_core.timestamp)("created_at").defaultNow().notNull()
90
+ }, (table) => [
91
+ (0, import_pg_core.index)("messages_conversation_id_idx").on(table.conversationId),
92
+ (0, import_pg_core.index)("messages_created_at_idx").on(table.createdAt)
93
+ ]);
94
+
95
+ // src/db/index.ts
96
+ var connectionString = process.env.DATABASE_URL;
97
+ var client = (0, import_postgres.default)(connectionString, { prepare: false });
98
+ var db = (0, import_postgres_js.drizzle)(client, { schema: schema_exports });
99
+
100
+ // src/db/chat-store.ts
101
+ var import_drizzle_orm = require("drizzle-orm");
102
+ async function createChat(userId) {
103
+ const id = (0, import_ai.generateId)();
104
+ await db.insert(conversations).values({
105
+ id,
106
+ userId,
107
+ title: "New Chat",
108
+ metadata: {}
109
+ });
110
+ return id;
111
+ }
112
+ async function loadChat(conversationId) {
113
+ try {
114
+ const dbMessages = await db.select().from(messages).where((0, import_drizzle_orm.eq)(messages.conversationId, conversationId)).orderBy((0, import_drizzle_orm.asc)(messages.createdAt));
115
+ if (!dbMessages.length) return [];
116
+ return dbMessages.map((msg) => {
117
+ const metadata = msg.metadata;
118
+ if (metadata?.parts && Array.isArray(metadata.parts)) {
119
+ return {
120
+ id: msg.id,
121
+ role: msg.role,
122
+ parts: metadata.parts,
123
+ createdAt: msg.createdAt
124
+ };
125
+ }
126
+ return {
127
+ id: msg.id,
128
+ role: msg.role,
129
+ parts: [{ type: "text", text: msg.content }],
130
+ createdAt: msg.createdAt
131
+ };
132
+ });
133
+ } catch (error) {
134
+ console.error("Error loading chat:", error);
135
+ return [];
136
+ }
137
+ }
138
+ async function updateConversationTitle(chatId, title) {
139
+ try {
140
+ await db.update(conversations).set({ title, updatedAt: /* @__PURE__ */ new Date() }).where((0, import_drizzle_orm.eq)(conversations.id, chatId));
141
+ } catch (error) {
142
+ console.error("Error updating conversation title:", error);
143
+ }
144
+ }
145
+ async function saveChat({
146
+ chatId,
147
+ messages: chatMessages,
148
+ model,
149
+ userId
150
+ }) {
151
+ if (!userId) {
152
+ console.error("userId is required for saveChat");
153
+ return;
154
+ }
155
+ try {
156
+ const existingConv = await db.select({ id: conversations.id, title: conversations.title }).from(conversations).where((0, import_drizzle_orm.eq)(conversations.id, chatId)).limit(1);
157
+ if (!existingConv.length) {
158
+ console.error("Conversation not found:", chatId);
159
+ return;
160
+ }
161
+ const conv = existingConv[0];
162
+ if (conv.title === "New Chat") {
163
+ const firstUserMessage = chatMessages.find((m) => m.role === "user");
164
+ if (firstUserMessage) {
165
+ const textPart = firstUserMessage.parts?.find((p) => p.type === "text");
166
+ if (textPart?.text) {
167
+ const newTitle = textPart.text.slice(0, 100);
168
+ await updateConversationTitle(chatId, newTitle);
169
+ }
170
+ }
171
+ }
172
+ const existingMessages = await db.select({ id: messages.id }).from(messages).where((0, import_drizzle_orm.eq)(messages.conversationId, chatId));
173
+ const existingIds = new Set(existingMessages.map((m) => m.id));
174
+ const newMessages = chatMessages.filter((msg) => !existingIds.has(msg.id));
175
+ if (newMessages.length > 0) {
176
+ for (const msg of newMessages) {
177
+ const textPart = msg.parts?.find((p) => p.type === "text");
178
+ const fileParts = msg.parts?.filter((p) => p.type === "file") || [];
179
+ await db.insert(messages).values({
180
+ id: msg.id,
181
+ conversationId: chatId,
182
+ role: msg.role,
183
+ content: textPart?.text || "",
184
+ files: fileParts,
185
+ model: model || "openai/gpt-4o-mini",
186
+ metadata: { parts: msg.parts || [] }
187
+ });
188
+ }
189
+ await db.update(conversations).set({ updatedAt: /* @__PURE__ */ new Date() }).where((0, import_drizzle_orm.eq)(conversations.id, chatId));
190
+ }
191
+ } catch (error) {
192
+ console.error("Error saving chat:", error);
193
+ }
194
+ }
195
+ async function getConversations(userId) {
196
+ try {
197
+ const result = await db.select({
198
+ id: conversations.id,
199
+ title: conversations.title,
200
+ createdAt: conversations.createdAt,
201
+ updatedAt: conversations.updatedAt,
202
+ metadata: conversations.metadata,
203
+ messageCount: import_drizzle_orm.sql`(
204
+ SELECT COUNT(*) FROM ${messages}
205
+ WHERE ${messages.conversationId} = ${conversations.id}
206
+ )`
207
+ }).from(conversations).where((0, import_drizzle_orm.eq)(conversations.userId, userId)).orderBy((0, import_drizzle_orm.desc)(conversations.updatedAt));
208
+ return result;
209
+ } catch (error) {
210
+ console.error("Error getting conversations:", error);
211
+ return [];
212
+ }
213
+ }
214
+ async function deleteConversation(chatId) {
215
+ try {
216
+ await db.delete(conversations).where((0, import_drizzle_orm.eq)(conversations.id, chatId));
217
+ } catch (error) {
218
+ console.error("Error deleting conversation:", error);
219
+ }
220
+ }
221
+
222
+ // src/api/index.ts
223
+ var import_drizzle_orm2 = require("drizzle-orm");
224
+ // Annotate the CommonJS export names for ESM import in node:
225
+ 0 && (module.exports = {
226
+ and,
227
+ asc,
228
+ conversations,
229
+ createChat,
230
+ db,
231
+ deleteConversation,
232
+ desc,
233
+ eq,
234
+ getConversations,
235
+ loadChat,
236
+ messages,
237
+ or,
238
+ saveChat,
239
+ sql,
240
+ updateConversationTitle
241
+ });
242
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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"]}