@mordn/chat-widget 0.1.0 → 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.
@@ -0,0 +1,430 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
18
+ // If the importer is in node compatibility mode or this is not an ESM
19
+ // file that has been converted to a CommonJS file using a Babel-
20
+ // compatible transform (i.e. "__esModule" has not been set), then set
21
+ // "default" to the CommonJS "module.exports" for node compatibility.
22
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
23
+ mod
24
+ ));
25
+
26
+ // src/cli/init.ts
27
+ var fs = __toESM(require("fs"));
28
+ var path = __toESM(require("path"));
29
+ var readline = __toESM(require("readline"));
30
+ var rl = readline.createInterface({
31
+ input: process.stdin,
32
+ output: process.stdout
33
+ });
34
+ function ask(question) {
35
+ return new Promise((resolve) => {
36
+ rl.question(question, (answer) => {
37
+ resolve(answer.toLowerCase());
38
+ });
39
+ });
40
+ }
41
+ async function confirm(message) {
42
+ const answer = await ask(`${message} (y/n): `);
43
+ return answer === "y" || answer === "yes";
44
+ }
45
+ function detectAppDir() {
46
+ if (fs.existsSync(path.join(process.cwd(), "src", "app"))) {
47
+ return path.join(process.cwd(), "src", "app");
48
+ }
49
+ if (fs.existsSync(path.join(process.cwd(), "app"))) {
50
+ return path.join(process.cwd(), "app");
51
+ }
52
+ return path.join(process.cwd(), "src", "app");
53
+ }
54
+ async function writeFileWithConfirm(filePath, content) {
55
+ if (fs.existsSync(filePath)) {
56
+ const overwrite = await confirm(`File ${path.relative(process.cwd(), filePath)} already exists. Overwrite?`);
57
+ if (!overwrite) {
58
+ console.log(` Skipped: ${path.relative(process.cwd(), filePath)}`);
59
+ return false;
60
+ }
61
+ }
62
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
63
+ fs.writeFileSync(filePath, content);
64
+ console.log(` Created: ${path.relative(process.cwd(), filePath)}`);
65
+ return true;
66
+ }
67
+ var MAIN_ROUTE = `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
+ // ============================================
73
+ // DEVELOPER CONFIG - Set these for your app
74
+ // ============================================
75
+ const DEVELOPER_CONFIG = {
76
+ model: 'openai/gpt-4o', // Your AI model (provider/model format)
77
+ systemPrompt: 'You are a helpful assistant',
78
+ temperature: 0.7,
79
+ };
80
+
81
+ export async function POST(req: Request) {
82
+ const body = await req.json();
83
+ const userId = req.headers.get('X-User-Id');
84
+
85
+ if (!userId) {
86
+ return new Response('userId is required in X-User-Id header', { status: 400 });
87
+ }
88
+
89
+ const chatMessages: UIMessage[] = body.messages || [];
90
+ const id: string = body.id || 'temp-id';
91
+
92
+ const { model, systemPrompt, temperature } = DEVELOPER_CONFIG;
93
+
94
+ // Check if conversation exists, create if not
95
+ const existingConv = await db
96
+ .select({ id: conversations.id })
97
+ .from(conversations)
98
+ .where(eq(conversations.id, id))
99
+ .limit(1);
100
+
101
+ if (!existingConv.length) {
102
+ await db.insert(conversations).values({
103
+ id,
104
+ userId,
105
+ title: 'New Chat',
106
+ metadata: {},
107
+ });
108
+ }
109
+
110
+ // Save the new user message
111
+ const userMessages = chatMessages.filter(msg => msg.role === 'user');
112
+ if (userMessages.length > 0) {
113
+ const newUserMessage = userMessages[userMessages.length - 1];
114
+ const textPart = newUserMessage.parts?.find(p => p.type === 'text') as { text: string } | undefined;
115
+ const fileParts = newUserMessage.parts?.filter(p => p.type === 'file') || [];
116
+
117
+ const existingMsg = await db
118
+ .select({ id: messages.id })
119
+ .from(messages)
120
+ .where(eq(messages.id, newUserMessage.id))
121
+ .limit(1);
122
+
123
+ if (!existingMsg.length) {
124
+ await db.insert(messages).values({
125
+ id: newUserMessage.id,
126
+ conversationId: id,
127
+ role: newUserMessage.role,
128
+ content: textPart?.text || '',
129
+ files: fileParts,
130
+ model: model,
131
+ metadata: { parts: newUserMessage.parts || [] },
132
+ });
133
+ }
134
+
135
+ // Update conversation title if needed
136
+ if (textPart?.text) {
137
+ const conv = await db
138
+ .select({ title: conversations.title })
139
+ .from(conversations)
140
+ .where(eq(conversations.id, id))
141
+ .limit(1);
142
+
143
+ if (conv[0]?.title === 'New Chat') {
144
+ await updateConversationTitle(id, textPart.text.slice(0, 100));
145
+ }
146
+ }
147
+ }
148
+
149
+ // Transform messages for AI (handle images)
150
+ const transformedMessages = chatMessages.map(msg => {
151
+ if (msg.role === 'user' && msg.parts) {
152
+ const textPart = msg.parts.find(p => p.type === 'text');
153
+ const fileParts = msg.parts.filter(p => p.type === 'file');
154
+
155
+ if (fileParts.length > 0) {
156
+ const content: any[] = [];
157
+ if (textPart && 'text' in textPart) {
158
+ content.push({ type: 'text', text: textPart.text });
159
+ }
160
+ for (const file of fileParts) {
161
+ if ('mediaType' in file && (file as any).mediaType?.startsWith('image/')) {
162
+ content.push({ type: 'image', image: (file as any).url });
163
+ }
164
+ }
165
+ return { ...msg, content };
166
+ }
167
+ }
168
+ return msg;
169
+ });
170
+
171
+ const result = streamText({
172
+ model: model,
173
+ messages: convertToModelMessages(transformedMessages),
174
+ system: systemPrompt,
175
+ temperature: temperature,
176
+ });
177
+
178
+ return result.toUIMessageStreamResponse({
179
+ sendSources: true,
180
+ sendReasoning: true,
181
+ onFinish: ({ messages: finalMessages }) => {
182
+ if (finalMessages.length > 0) {
183
+ saveChat({ chatId: id, messages: finalMessages, model, userId });
184
+ }
185
+ },
186
+ });
187
+ }
188
+ `;
189
+ var HISTORY_ROUTE = `import { NextResponse } from 'next/server';
190
+ import { getConversations } from '@mordn/chat-widget/api';
191
+
192
+ export async function GET(request: Request) {
193
+ try {
194
+ const url = new URL(request.url);
195
+ const userId = url.searchParams.get('userId') || request.headers.get('X-User-Id');
196
+
197
+ if (!userId) {
198
+ return NextResponse.json({ error: 'userId is required' }, { status: 400 });
199
+ }
200
+
201
+ const conversationsData = await getConversations(userId);
202
+
203
+ const conversations = conversationsData.map(conv => ({
204
+ id: conv.id,
205
+ title: conv.title,
206
+ created_at: conv.createdAt,
207
+ updated_at: conv.updatedAt,
208
+ metadata: conv.metadata,
209
+ message_count: conv.messageCount,
210
+ }));
211
+
212
+ return NextResponse.json({ conversations });
213
+ } catch (error) {
214
+ console.error('Error in chat history API:', error);
215
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
216
+ }
217
+ }
218
+ `;
219
+ var CONVERSATION_ROUTE = `import { NextResponse } from 'next/server';
220
+ import { db, conversations, messages, eq, and, asc } from '@mordn/chat-widget/api';
221
+
222
+ export async function GET(
223
+ request: Request,
224
+ { params }: { params: Promise<{ conversationId: string }> }
225
+ ) {
226
+ try {
227
+ const { conversationId } = await params;
228
+ const url = new URL(request.url);
229
+ const userId = url.searchParams.get('userId') || request.headers.get('X-User-Id');
230
+
231
+ if (!userId) {
232
+ return NextResponse.json({ error: 'userId is required' }, { status: 400 });
233
+ }
234
+
235
+ // Verify the conversation belongs to the user
236
+ const conv = await db
237
+ .select({
238
+ id: conversations.id,
239
+ title: conversations.title,
240
+ metadata: conversations.metadata,
241
+ })
242
+ .from(conversations)
243
+ .where(and(
244
+ eq(conversations.id, conversationId),
245
+ eq(conversations.userId, userId)
246
+ ))
247
+ .limit(1);
248
+
249
+ if (!conv.length) {
250
+ return NextResponse.json({ error: 'Conversation not found' }, { status: 404 });
251
+ }
252
+
253
+ const conversation = conv[0];
254
+
255
+ const dbMessages = await db
256
+ .select()
257
+ .from(messages)
258
+ .where(eq(messages.conversationId, conversationId))
259
+ .orderBy(asc(messages.createdAt))
260
+ .limit(1000);
261
+
262
+ const transformedMessages = dbMessages.map(msg => {
263
+ const metadata = msg.metadata as { parts?: any[] } | null;
264
+
265
+ if (metadata?.parts && Array.isArray(metadata.parts)) {
266
+ return {
267
+ id: msg.id,
268
+ role: msg.role,
269
+ content: msg.content,
270
+ created_at: msg.createdAt,
271
+ parts: metadata.parts
272
+ };
273
+ }
274
+
275
+ return {
276
+ id: msg.id,
277
+ role: msg.role,
278
+ content: msg.content,
279
+ created_at: msg.createdAt,
280
+ parts: msg.content ? [{ type: 'text', text: msg.content }] : undefined
281
+ };
282
+ });
283
+
284
+ return NextResponse.json({ conversation, messages: transformedMessages });
285
+ } catch (error) {
286
+ console.error('Error loading conversation:', error);
287
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
288
+ }
289
+ }
290
+ `;
291
+ var UPLOAD_ROUTE = `import { createClient } from '@supabase/supabase-js';
292
+ import { nanoid } from 'nanoid';
293
+
294
+ const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
295
+ const supabaseServiceKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
296
+
297
+ export async function POST(req: Request) {
298
+ try {
299
+ const formData = await req.formData();
300
+ const file = formData.get('file') as File;
301
+ const conversationId = formData.get('conversationId') as string;
302
+ const userId = formData.get('userId') as string;
303
+
304
+ if (!file) {
305
+ return Response.json({ error: 'No file provided' }, { status: 400 });
306
+ }
307
+
308
+ if (!userId) {
309
+ return Response.json({ error: 'userId is required' }, { status: 400 });
310
+ }
311
+
312
+ // Only images supported
313
+ if (!file.type.startsWith('image/')) {
314
+ return Response.json({ error: 'Only image files are supported' }, { status: 400 });
315
+ }
316
+
317
+ // 5MB limit
318
+ if (file.size > 5 * 1024 * 1024) {
319
+ return Response.json({ error: 'File size exceeds 5MB limit' }, { status: 400 });
320
+ }
321
+
322
+ const supabase = createClient(supabaseUrl, supabaseServiceKey);
323
+
324
+ const timestamp = Date.now();
325
+ const randomId = nanoid(8);
326
+ const safeFilename = file.name.replace(/[^a-zA-Z0-9.-]/g, '_');
327
+ const filePath = \`\${userId}/\${conversationId || 'default'}/\${timestamp}-\${randomId}-\${safeFilename}\`;
328
+
329
+ const fileBuffer = await file.arrayBuffer();
330
+
331
+ const { error: uploadError } = await supabase.storage
332
+ .from('chat-attachments')
333
+ .upload(filePath, fileBuffer, {
334
+ contentType: file.type,
335
+ upsert: false,
336
+ });
337
+
338
+ if (uploadError) {
339
+ console.error('Upload error:', uploadError);
340
+ return Response.json({ error: 'Failed to upload file' }, { status: 500 });
341
+ }
342
+
343
+ const { data: urlData } = supabase.storage
344
+ .from('chat-attachments')
345
+ .getPublicUrl(filePath);
346
+
347
+ return Response.json({
348
+ url: urlData.publicUrl,
349
+ filename: file.name,
350
+ mediaType: file.type,
351
+ size: file.size,
352
+ type: 'file',
353
+ });
354
+ } catch (error) {
355
+ console.error('Upload API error:', error);
356
+ return Response.json({ error: 'Internal server error' }, { status: 500 });
357
+ }
358
+ }
359
+ `;
360
+ var DRIZZLE_CONFIG = `import 'dotenv/config';
361
+ import { defineConfig } from 'drizzle-kit';
362
+
363
+ export default defineConfig({
364
+ schema: './node_modules/@mordn/chat-widget/dist/schema/index.js',
365
+ out: './drizzle',
366
+ dialect: 'postgresql',
367
+ dbCredentials: {
368
+ url: process.env.DATABASE_URL!,
369
+ },
370
+ });
371
+ `;
372
+ var ENV_EXAMPLE = `# Database (Required)
373
+ DATABASE_URL="postgresql://postgres.xxx:[PASSWORD]@aws-0-region.pooler.supabase.com:6543/postgres"
374
+
375
+ # AI Gateway (Required)
376
+ AI_GATEWAY_API_KEY="your-ai-gateway-key"
377
+
378
+ # Supabase Storage (Optional - for file uploads)
379
+ NEXT_PUBLIC_SUPABASE_URL="https://xxx.supabase.co"
380
+ SUPABASE_SERVICE_ROLE_KEY="your-service-role-key"
381
+ `;
382
+ async function init() {
383
+ console.log("\n@mordn/chat-widget init\n");
384
+ console.log("This will create the required API routes and configuration files.\n");
385
+ const appDir = detectAppDir();
386
+ const apiChatDir = path.join(appDir, "api", "chat");
387
+ console.log(`Detected app directory: ${path.relative(process.cwd(), appDir)}
388
+ `);
389
+ let filesCreated = 0;
390
+ console.log("Creating API routes...");
391
+ if (await writeFileWithConfirm(path.join(apiChatDir, "route.ts"), MAIN_ROUTE)) {
392
+ filesCreated++;
393
+ }
394
+ if (await writeFileWithConfirm(path.join(apiChatDir, "history", "route.ts"), HISTORY_ROUTE)) {
395
+ filesCreated++;
396
+ }
397
+ if (await writeFileWithConfirm(path.join(apiChatDir, "history", "[conversationId]", "route.ts"), CONVERSATION_ROUTE)) {
398
+ filesCreated++;
399
+ }
400
+ const createUpload = await confirm("\nCreate file upload route? (requires Supabase Storage)");
401
+ if (createUpload) {
402
+ if (await writeFileWithConfirm(path.join(apiChatDir, "upload", "route.ts"), UPLOAD_ROUTE)) {
403
+ filesCreated++;
404
+ }
405
+ }
406
+ console.log("\nCreating configuration files...");
407
+ if (await writeFileWithConfirm(path.join(process.cwd(), "drizzle.config.ts"), DRIZZLE_CONFIG)) {
408
+ filesCreated++;
409
+ }
410
+ if (await writeFileWithConfirm(path.join(process.cwd(), ".env.example"), ENV_EXAMPLE)) {
411
+ filesCreated++;
412
+ }
413
+ console.log(`
414
+ \u2713 Created ${filesCreated} files
415
+ `);
416
+ console.log("Next steps:");
417
+ console.log(" 1. Copy .env.example to .env.local and fill in your credentials");
418
+ console.log(" 2. Run: npx drizzle-kit push");
419
+ console.log(" 3. Add the ChatWidget to your app:\n");
420
+ console.log(" import { ChatWidget } from '@mordn/chat-widget';");
421
+ console.log(" import '@mordn/chat-widget/styles.css';");
422
+ console.log("");
423
+ console.log(' <ChatWidget userId="user-123" />\n');
424
+ rl.close();
425
+ }
426
+ init().catch((error) => {
427
+ console.error("Error:", error);
428
+ rl.close();
429
+ process.exit(1);
430
+ });