From 2b07484e96eeda2303e3cab486d23aef450f073c Mon Sep 17 00:00:00 2001 From: Abdullah-dev0 Date: Sun, 10 Aug 2025 23:39:09 +0500 Subject: [PATCH 1/9] feat: enhance chat functionality with message handling and UI improvements Enhances chat functionality and improves UI Adds support for chat history with pagination, real-time message updates, and improved UI components. Introduces message delay handling in the database for accurate timestamps. Updates configuration to support additional image domains and integrates a new dependency for intersection observation. Adjusts stale time for query results to optimize performance. --- next.config.js | 27 ++- package-lock.json | 16 ++ package.json | 3 +- src/actions/messages.action.ts | 120 ++++++++++ src/app/api/chat/route.ts | 40 ++-- src/components/Providers.tsx | 3 +- src/components/chat/ChatComponent.tsx | 226 +++++++++++++------ src/components/chat/MessageItem.tsx | 105 +++++++++ src/components/chat/messages/MessageItem.tsx | 114 ++++++++++ src/components/chat/messages/index.ts | 1 + src/lib/time-utils.ts | 6 + 11 files changed, 563 insertions(+), 98 deletions(-) create mode 100644 src/actions/messages.action.ts create mode 100644 src/components/chat/MessageItem.tsx create mode 100644 src/components/chat/messages/MessageItem.tsx create mode 100644 src/components/chat/messages/index.ts create mode 100644 src/lib/time-utils.ts diff --git a/next.config.js b/next.config.js index 8803f0d..c5d4a08 100644 --- a/next.config.js +++ b/next.config.js @@ -1,17 +1,20 @@ /** @type {import('next').NextConfig} */ const nextConfig = { - images: { - remotePatterns: [ - { - hostname: "gravatar.com", - }, - ], - }, - webpack: (config, { buildId, dev, isServer, defaultLoaders, webpack }) => { - config.resolve.alias.canvas = false; - config.resolve.alias.encoding = false; - return config; - }, + images: { + remotePatterns: [ + { + hostname: 'gravatar.com', + }, + { + hostname: 'img.clerk.com', + }, + ], + }, + webpack: (config, { buildId, dev, isServer, defaultLoaders, webpack }) => { + config.resolve.alias.canvas = false; + config.resolve.alias.encoding = false; + return config; + }, }; module.exports = nextConfig; diff --git a/package-lock.json b/package-lock.json index 3d300ca..57b622e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -51,6 +51,7 @@ "react": "18.2.0", "react-dom": "18.2.0", "react-hook-form": "^7.46.2", + "react-intersection-observer": "^9.16.0", "react-markdown": "^8.0.7", "react-pdf": "^9.1.1", "react-resize-detector": "^9.1.0", @@ -9753,6 +9754,21 @@ "react": "^16.8.0 || ^17 || ^18 || ^19" } }, + "node_modules/react-intersection-observer": { + "version": "9.16.0", + "resolved": "https://registry.npmjs.org/react-intersection-observer/-/react-intersection-observer-9.16.0.tgz", + "integrity": "sha512-w9nJSEp+DrW9KmQmeWHQyfaP6b03v+TdXynaoA964Wxt7mdR3An11z4NNCQgL4gKSK7y1ver2Fq+JKH6CWEzUA==", + "license": "MIT", + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", diff --git a/package.json b/package.json index 4d005a8..bbfa4ef 100644 --- a/package.json +++ b/package.json @@ -52,13 +52,14 @@ "lucide-react": "^0.279.0", "next": "^14.2.14", "next-themes": "^0.4.3", + "pdf-parse": "^1.1.1", "postcss": "^8.4.47", "react": "18.2.0", "react-dom": "18.2.0", "react-hook-form": "^7.46.2", + "react-intersection-observer": "^9.16.0", "react-markdown": "^8.0.7", "react-pdf": "^9.1.1", - "pdf-parse": "^1.1.1", "react-resize-detector": "^9.1.0", "simplebar-react": "^3.2.4", "sonner": "^1.7.0", diff --git a/src/actions/messages.action.ts b/src/actions/messages.action.ts new file mode 100644 index 0000000..986dd48 --- /dev/null +++ b/src/actions/messages.action.ts @@ -0,0 +1,120 @@ +'use server'; + +import { auth } from '@clerk/nextjs/server'; +import type { Prisma } from '@prisma/client'; + +import { db } from '@/db'; + +export type MessageDTO = { + id: string; + text: string; + isUserMessage: boolean; + createdAt: string; // ISO string for client serialization + updatedAt: string; // ISO string for client serialization + userId: string | null; + fileId: string; +}; + +export type MessagesPage = { + items: MessageDTO[]; + nextCursor?: string; // pass as `cursor` to load the next (older) page +}; +export type MessagesChunk = { + items: MessageDTO[]; + hasMore: boolean; + nextCursor?: string; // createdAt ISO of the oldest item returned + total?: number; // total count for simple pagination +}; + +export async function getMessagesChunk({ + fileId, + before, // ISO string; fetch messages with createdAt < before + take = 4, +}: { + fileId: string; + before?: string; + take?: number; +}): Promise { + const { userId } = await auth(); + if (!userId) throw new Error('Unauthorized'); + if (!fileId) throw new Error('File ID is required'); + if (!take || take < 1) throw new Error('take must be positive'); + + const file = await db.file.findFirst({ where: { id: fileId, userId } }); + if (!file) throw new Error('File not found or unauthorized'); + + const where: Prisma.MessageWhereInput = { fileId }; + if (before) { + const d = new Date(before); + if (!isNaN(d.getTime())) where.createdAt = { lt: d }; + } + + const results = await db.message.findMany({ + where, + orderBy: { createdAt: 'desc' }, + take: take + 1, // fetch one extra to determine hasMore + }); + + const hasMore = results.length > take; + const slice = hasMore ? results.slice(0, take) : results; + + const itemsDesc = slice; + const itemsAsc = itemsDesc + .slice() + .reverse() + .map((m) => ({ + id: m.id, + text: m.text, + isUserMessage: m.isUserMessage, + createdAt: m.createdAt.toISOString(), + updatedAt: m.updatedAt.toISOString(), + userId: m.userId ?? null, + fileId: m.fileId!, + })); + + const nextCursor = itemsAsc[0]?.createdAt; // oldest in this batch + + return { items: itemsAsc, hasMore, nextCursor }; +} + +export async function getMessagesSimple({ + fileId, + skip = 0, + take = 10, +}: { + fileId: string; + skip?: number; + take?: number; +}): Promise { + const { userId } = await auth(); + if (!userId) throw new Error('Unauthorized'); + if (!fileId) throw new Error('File ID is required'); + if (!take || take < 1) throw new Error('take must be positive'); + + const file = await db.file.findFirst({ where: { id: fileId, userId } }); + if (!file) throw new Error('File not found or unauthorized'); + + // Get total count for pagination info + const total = await db.message.count({ where: { fileId } }); + + const messages = await db.message.findMany({ + where: { fileId }, + orderBy: { createdAt: 'desc' }, + skip, + take, + }); + + const hasMore = skip + take < total; + + const items = messages.map((m) => ({ + id: m.id, + text: m.text, + isUserMessage: m.isUserMessage, + createdAt: m.createdAt.toISOString(), + updatedAt: m.updatedAt.toISOString(), + userId: m.userId ?? null, + fileId: m.fileId!, + })); + + return { items, hasMore, total }; +} diff --git a/src/app/api/chat/route.ts b/src/app/api/chat/route.ts index f8471fd..7f9bca9 100644 --- a/src/app/api/chat/route.ts +++ b/src/app/api/chat/route.ts @@ -109,24 +109,28 @@ export const POST = async (req: NextRequest) => { abortSignal: req.signal, onFinish: async ({ text: generatedText }) => { try { - await db.$transaction([ - db.message.create({ - data: { - text: messageText, - isUserMessage: true, - userId: user.id, - fileId, - }, - }), - db.message.create({ - data: { - text: generatedText, - isUserMessage: false, - fileId, - userId: user.id, - }, - }), - ]); + // Create user message first + await db.message.create({ + data: { + text: messageText, + isUserMessage: true, + userId: user.id, + fileId, + }, + }); + + // Small delay to ensure different timestamps + await new Promise((resolve) => setTimeout(resolve, 1)); + + // Then create AI response + await db.message.create({ + data: { + text: generatedText, + isUserMessage: false, + fileId, + userId: user.id, + }, + }); } catch (error) { console.error('Error in onFinish callback:', error); } diff --git a/src/components/Providers.tsx b/src/components/Providers.tsx index 2df42ce..771c103 100644 --- a/src/components/Providers.tsx +++ b/src/components/Providers.tsx @@ -13,9 +13,8 @@ const Providers = ({ children }: ProvidersProps) => { new QueryClient({ defaultOptions: { queries: { - staleTime: 60 * 1000, // 1 minute + staleTime: 60 * 5000, // 5 minutes retry: (failureCount, error) => { - // Don't retry on 401/403 errors if ( error instanceof Error && error.message.includes('unauthorized') diff --git a/src/components/chat/ChatComponent.tsx b/src/components/chat/ChatComponent.tsx index 63e5b00..cd2afdb 100644 --- a/src/components/chat/ChatComponent.tsx +++ b/src/components/chat/ChatComponent.tsx @@ -1,63 +1,133 @@ 'use client'; import { useChat } from '@ai-sdk/react'; +import { useUser } from '@clerk/nextjs'; import { File } from '@prisma/client'; -import { DefaultChatTransport } from 'ai'; +import { useInfiniteQuery } from '@tanstack/react-query'; +import { DefaultChatTransport, type UIMessage } from 'ai'; import { ChevronLeft, Loader2, Send } from 'lucide-react'; import Link from 'next/link'; -import { useEffect, useRef, useState } from 'react'; -import ReactMarkdown from 'react-markdown'; +import { useEffect, useMemo, useRef, useState } from 'react'; import { toast } from 'sonner'; +import { + getMessagesChunk, + type MessageDTO, + type MessagesChunk, +} from '@/actions/messages.action'; + import { Button, buttonVariants } from '../ui/button'; import { Textarea } from '../ui/textarea'; import SelectLanguage from './SelectLanguage'; +import MessageItem from './messages/MessageItem'; -interface SimpleChatProps { - file: File; -} - -const ChatComponent = ({ file }: SimpleChatProps) => { +const ChatComponent = ({ file }: { file: File }) => { const [input, setInput] = useState(''); const [selectedLanguage, setSelectedLanguage] = useState('english'); const messagesEndRef = useRef(null); - const { messages, sendMessage, status } = useChat({ + const { user } = useUser(); + + // Live chat with Vercel AI SDK + const { + messages: liveMessages, + sendMessage, + status, + setMessages, + } = useChat({ transport: new DefaultChatTransport({ api: '/api/chat', }), onError: (error) => { toast.error(error.message || 'An error occurred'); + setMessages([]); }, }); + const { + data: historyData, + status: historyStatus, + fetchNextPage, + isLoading, + hasNextPage, + isFetchingNextPage, + refetch: fetchOlder, + } = useInfiniteQuery( + ['messages', file.id], + async ({ pageParam = undefined }) => + await getMessagesChunk({ + fileId: file.id, + before: pageParam as string | undefined, + take: 10, + }), + { + getNextPageParam: (lastPage) => + lastPage.hasMore ? lastPage.nextCursor : undefined, + } + ); + + // Auto scroll to bottom when new live message arrives useEffect(() => { - messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); - }, [messages, status]); + const timer = setTimeout(() => { + messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + }, 100); // slight delay to ensure DOM is painted + + return () => clearTimeout(timer); + }, [liveMessages, status]); + + // Derived lists for simple rendering + const historyFlat: MessageDTO[] = useMemo(() => { + const pages = (historyData?.pages ?? []) as MessagesChunk[]; + if (!pages.length) return []; + // Oldest first across pages + return [...pages].reverse().flatMap((p) => p.items); + }, [historyData]); + + const combinedMessages: Array = useMemo( + () => [...historyFlat, ...liveMessages] as Array, + [historyFlat, liveMessages] + ); + + const historyIdSet = useMemo( + () => new Set(historyFlat.map((m) => m.id)), + [historyFlat] + ); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if (input.trim()) { sendMessage( { text: input }, - { - body: { fileId: file.id, language: selectedLanguage }, - } + { body: { fileId: file.id, language: selectedLanguage } } ); setInput(''); } }; + { + if (isLoading) { + return ( +
+
+ + Loading chat history... +
+
+ ); + } + } + return (
+ {/* Top Bar */}
- Back to Dashboard + Back to Dashboard {
{/* Messages */} -
- {messages.length === 0 ? ( -
-

Ready to chat!

-

- Ask a question about your PDF in {selectedLanguage}. -

-
- ) : ( -
- {messages.map((message) => ( -
+
+ {/* Load older messages */} + {hasNextPage && ( +
+ +
+ )} -
- {message.parts.map((part, index) => - part.type === 'text' ? ( -
- - {part.text} - -
- ) : null - )} -
+ {/* Error */} + {historyStatus === 'error' && ( +
+ Failed to load history + +
+ )} - {/* Avatar/Emoji for User (right side) */} - {message.role === 'user' && ( -
- 👤 -
- )} + {/* Combined messages */} + {combinedMessages.length > 0 ? ( + combinedMessages.map((message) => ( + + )) + ) : ( +
+
+ 💬
- ))} -
- )} - {status === 'submitted' && ( -
-
-
- 🤖 +

+ Ready to chat! +

+

+ Ask questions about your PDF in {selectedLanguage}. +

+
+ )} + + {/* Typing indicator */} + {status === 'submitted' && ( +
+
+ AI
-
- +
+
+
+
+
+
+
+ + AI is thinking... + +
-
- )} - {/* Auto-scroll target */} -
+ )} + +
+
{/* Input */} diff --git a/src/components/chat/MessageItem.tsx b/src/components/chat/MessageItem.tsx new file mode 100644 index 0000000..a92ca87 --- /dev/null +++ b/src/components/chat/MessageItem.tsx @@ -0,0 +1,105 @@ +'use client'; + +import Image from 'next/image'; +import ReactMarkdown from 'react-markdown'; + +import { formatTime } from '@/lib/time-utils'; + +interface HistoryMessage { + id: string; + text: string; + isUserMessage: boolean; + createdAt: string; +} + +interface LiveMessage { + id: string; + role: 'user' | 'assistant' | 'system'; + parts: Array<{ type: string; text?: string }>; +} + +interface MessageItemProps { + message: HistoryMessage | LiveMessage; + user: { + imageUrl?: string | null; + firstName?: string | null; + emailAddresses?: Array<{ emailAddress: string }>; + } | null; + isHistory?: boolean; +} + +const MessageItem = ({ + message, + user, + isHistory = false, +}: MessageItemProps) => { + const isHistoryMessage = ( + msg: HistoryMessage | LiveMessage + ): msg is HistoryMessage => { + return 'isUserMessage' in msg; + }; + + const isUser = isHistoryMessage(message) + ? message.isUserMessage + : message.role === 'user'; + + // Skip system messages + if (!isHistoryMessage(message) && message.role === 'system') { + return null; + } + + const messageText = isHistoryMessage(message) + ? message.text + : message.parts?.find((p) => p.type === 'text')?.text || ''; + + return ( +
+ {/* AI Avatar */} + {!isUser && ( +
+ AI +
+ )} + +
+
+ + {messageText} + +
+
+ {isHistory && isHistoryMessage(message) + ? formatTime(message.createdAt) + : 'just now'} +
+
+ + {/* User Avatar */} + {isUser && ( +
+ {user?.imageUrl ? ( + {user.firstName + ) : ( +
+ {user?.firstName?.[0] || + user?.emailAddresses?.[0]?.emailAddress[0]?.toUpperCase() || + 'U'} +
+ )} +
+ )} +
+ ); +}; + +export default MessageItem; diff --git a/src/components/chat/messages/MessageItem.tsx b/src/components/chat/messages/MessageItem.tsx new file mode 100644 index 0000000..b216a03 --- /dev/null +++ b/src/components/chat/messages/MessageItem.tsx @@ -0,0 +1,114 @@ +'use client'; + +import type { UIMessage } from 'ai'; +import Image from 'next/image'; +import ReactMarkdown from 'react-markdown'; + +import { formatTime } from '@/lib/time-utils'; + +interface HistoryMessage { + id: string; + text: string; + isUserMessage: boolean; + createdAt: string; +} + +// Type for text parts from UIMessage +interface TextMessagePart { + type: 'text'; + text: string; +} + +interface MessageItemProps { + message: HistoryMessage | UIMessage; + user: { + imageUrl?: string | null; + firstName?: string | null; + emailAddresses?: Array<{ emailAddress: string }>; + } | null; + isHistory?: boolean; +} + +const MessageItem = ({ + message, + user, + isHistory = false, +}: MessageItemProps) => { + // Type guards to determine message type + const isHistoryMessage = ( + msg: HistoryMessage | UIMessage + ): msg is HistoryMessage => { + return 'isUserMessage' in msg; + }; + + const isUser = isHistoryMessage(message) + ? message.isUserMessage + : message.role === 'user'; + + // Skip system messages + if (!isHistoryMessage(message) && message.role === 'system') { + return null; + } + + const messageText = isHistoryMessage(message) + ? message.text + : message.parts + ?.filter((part): part is TextMessagePart => part.type === 'text') + ?.map((part) => part.text) + ?.join('') || ''; + + return ( +
+ {/* AI Avatar */} + {!isUser && ( +
+ AI +
+ )} + +
+
+ + {messageText} + +
+
+ {isHistory && isHistoryMessage(message) + ? formatTime(message.createdAt) + : 'just now'} +
+
+ + {/* User Avatar */} + {isUser && ( +
+ {user?.imageUrl ? ( + {user.firstName + ) : ( +
+ {user?.firstName?.[0] || + user?.emailAddresses?.[0]?.emailAddress[0]?.toUpperCase() || + 'U'} +
+ )} +
+ )} +
+ ); +}; + +export default MessageItem; diff --git a/src/components/chat/messages/index.ts b/src/components/chat/messages/index.ts new file mode 100644 index 0000000..71cbc46 --- /dev/null +++ b/src/components/chat/messages/index.ts @@ -0,0 +1 @@ +export { default as MessageItem } from './MessageItem'; diff --git a/src/lib/time-utils.ts b/src/lib/time-utils.ts new file mode 100644 index 0000000..e9c0777 --- /dev/null +++ b/src/lib/time-utils.ts @@ -0,0 +1,6 @@ +export function formatTime(date: string | Date): string { + return new Date(date).toLocaleTimeString([], { + hour: '2-digit', + minute: '2-digit', + }); +} From a4821e0831c5d59f7d6829b58438df2956342a40 Mon Sep 17 00:00:00 2001 From: Abdullah-dev0 Date: Sun, 10 Aug 2025 23:53:10 +0500 Subject: [PATCH 2/9] refactor: update Prisma schema for User and Message models, improve message handling in MessageItem component --- prisma/schema.prisma | 13 ++++++------- src/actions/messages.action.ts | 6 +++--- src/components/Providers.tsx | 2 +- src/components/chat/ChatComponent.tsx | 19 +++++++++---------- src/components/chat/MessageItem.tsx | 20 ++++++++++++++++---- 5 files changed, 35 insertions(+), 25 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 8f9b68c..c05d8c6 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -19,7 +19,6 @@ model User { File File[] Message Message[] - } enum UploadStatus { @@ -41,8 +40,8 @@ model File { createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - User User? @relation(fields: [userId], references: [id]) - userId String? + User User @relation(fields: [userId], references: [id]) + userId String } model Message { @@ -53,8 +52,8 @@ model Message { createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - User User? @relation(fields: [userId], references: [id]) - userId String? - File File? @relation(fields: [fileId], references: [id], onDelete: Cascade) - fileId String? + User User @relation(fields: [userId], references: [id]) + userId String + File File @relation(fields: [fileId], references: [id], onDelete: Cascade) + fileId String } diff --git a/src/actions/messages.action.ts b/src/actions/messages.action.ts index 986dd48..e8a4857 100644 --- a/src/actions/messages.action.ts +++ b/src/actions/messages.action.ts @@ -69,7 +69,7 @@ export async function getMessagesChunk({ createdAt: m.createdAt.toISOString(), updatedAt: m.updatedAt.toISOString(), userId: m.userId ?? null, - fileId: m.fileId!, + fileId: m.fileId, })); const nextCursor = itemsAsc[0]?.createdAt; // oldest in this batch @@ -112,8 +112,8 @@ export async function getMessagesSimple({ isUserMessage: m.isUserMessage, createdAt: m.createdAt.toISOString(), updatedAt: m.updatedAt.toISOString(), - userId: m.userId ?? null, - fileId: m.fileId!, + userId: m.userId, + fileId: m.fileId, })); return { items, hasMore, total }; diff --git a/src/components/Providers.tsx b/src/components/Providers.tsx index 771c103..d7f89a3 100644 --- a/src/components/Providers.tsx +++ b/src/components/Providers.tsx @@ -13,7 +13,7 @@ const Providers = ({ children }: ProvidersProps) => { new QueryClient({ defaultOptions: { queries: { - staleTime: 60 * 5000, // 5 minutes + staleTime: 5 * 60 * 1000, // 5 minutes retry: (failureCount, error) => { if ( error instanceof Error && diff --git a/src/components/chat/ChatComponent.tsx b/src/components/chat/ChatComponent.tsx index cd2afdb..72cb7cb 100644 --- a/src/components/chat/ChatComponent.tsx +++ b/src/components/chat/ChatComponent.tsx @@ -105,17 +105,16 @@ const ChatComponent = ({ file }: { file: File }) => { } }; - { - if (isLoading) { - return ( -
-
- - Loading chat history... -
+ // Early return for loading history + if (isLoading) { + return ( +
+
+ + Loading chat history...
- ); - } +
+ ); } return ( diff --git a/src/components/chat/MessageItem.tsx b/src/components/chat/MessageItem.tsx index a92ca87..757b804 100644 --- a/src/components/chat/MessageItem.tsx +++ b/src/components/chat/MessageItem.tsx @@ -50,7 +50,15 @@ const MessageItem = ({ const messageText = isHistoryMessage(message) ? message.text - : message.parts?.find((p) => p.type === 'text')?.text || ''; + : Array.isArray(message.parts) + ? message.parts + .filter( + (p): p is { type: 'text'; text: string } => + p.type === 'text' && typeof p.text === 'string' + ) + .map((p) => p.text) + .join('') + : ''; return (
@@ -62,12 +70,16 @@ const MessageItem = ({ )}
- + {messageText}
From 68c2057986837a92d0aa032cd00a449efca25a7b Mon Sep 17 00:00:00 2001 From: Abdullah-dev0 Date: Mon, 11 Aug 2025 00:10:58 +0500 Subject: [PATCH 3/9] fix: add user authentication check and redirect to sign-in if not authenticated --- src/app/(root)/dashboard/[fileid]/page.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/app/(root)/dashboard/[fileid]/page.tsx b/src/app/(root)/dashboard/[fileid]/page.tsx index 7e16edb..cf2c399 100644 --- a/src/app/(root)/dashboard/[fileid]/page.tsx +++ b/src/app/(root)/dashboard/[fileid]/page.tsx @@ -1,5 +1,6 @@ import { auth } from '@clerk/nextjs/server'; import Link from 'next/link'; +import { redirect } from 'next/navigation'; import PdfRenderer from '@/components/PdfRenderer'; import ChatComponent from '@/components/chat/ChatComponent'; @@ -16,6 +17,10 @@ const Page = async ({ params }: PageProps) => { const user = await auth(); + if (!user.userId) { + redirect('/sign-in'); + } + const file = await db.file.findFirst({ where: { id: fileid, From 90eea5b556b856cf0cc100d1ce83bb232fea5e05 Mon Sep 17 00:00:00 2001 From: Abdullah-dev0 Date: Mon, 11 Aug 2025 00:14:59 +0500 Subject: [PATCH 4/9] refactor: update linting and formatting scripts for improved consistency --- package.json | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index bbfa4ef..3fe7c65 100644 --- a/package.json +++ b/package.json @@ -7,12 +7,13 @@ "dev": "next dev", "build": " npx prisma generate && next build", "start": "next start", - "lint": "npx eslint \"src/**/*.{ts,tsx,js,jsx}\" --max-warnings=0", - "lint:fix": "npx eslint \"src/**/*.{ts,tsx,js,jsx}\" --fix", - "format": "npx prettier --check \"src/**/*.{ts,tsx,js,jsx,json,md}\"", - "check": "npx eslint \"src/**/*.{ts,tsx,js,jsx}\" --max-warnings=0 && npx prettier --check \"src/**/*.{ts,tsx,js,jsx,json,md}\"", + "lint": "npx eslint \"src/**/*.{ts,tsx}\" --max-warnings=0", + "lint:fix": "npx eslint \"src/**/*.{ts,tsx}\" --fix", + "format": "npx prettier --check \"src/**/*.{ts,tsx,json,md}\"", + "check": "npx eslint \"src/**/*.{ts,tsx}\" --max-warnings=0 && npx prettier --check \"src/**/*.{ts,tsx,json,md}\"", "format:fix": "npx prettier --write \"src/**/*.{ts,tsx,js,jsx,json,md}\"", - "postinstall": "prisma generate" + "postinstall": "prisma generate", + "typecheck": "tsc --noEmit" }, "dependencies": { "@ai-sdk/mistral": "^2.0.1", From aadb6f12a96202e4632208d92f7039c3ca3dc0bf Mon Sep 17 00:00:00 2001 From: Abdullah-dev0 Date: Mon, 11 Aug 2025 00:20:22 +0500 Subject: [PATCH 5/9] refactor: update linting workflow and improve TypeScript type checking command --- .github/workflows/lint-format.yml | 6 ++++++ package.json | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/lint-format.yml b/.github/workflows/lint-format.yml index f2316b7..11b45f6 100644 --- a/.github/workflows/lint-format.yml +++ b/.github/workflows/lint-format.yml @@ -28,8 +28,14 @@ jobs: - name: Install dependencies run: npm ci + - name: Generate Prisma Client + run: npx prisma generate + - name: Lint (src only) run: npx eslint "src/**/*.{ts,tsx,js,jsx}" --max-warnings=0 - name: Prettier Check (src only) run: npm run format + + - name: TypeScript Type Check + run: npm run typecheck diff --git a/package.json b/package.json index 3fe7c65..b2a1a98 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "check": "npx eslint \"src/**/*.{ts,tsx}\" --max-warnings=0 && npx prettier --check \"src/**/*.{ts,tsx,json,md}\"", "format:fix": "npx prettier --write \"src/**/*.{ts,tsx,js,jsx,json,md}\"", "postinstall": "prisma generate", - "typecheck": "tsc --noEmit" + "typecheck": "tsc -p tsconfig.json --noEmit" }, "dependencies": { "@ai-sdk/mistral": "^2.0.1", From fc1fb1728efcd3d3733c99056da0ceadeb838a52 Mon Sep 17 00:00:00 2001 From: Abdullah-dev0 Date: Mon, 11 Aug 2025 04:27:01 +0500 Subject: [PATCH 6/9] feat: integrate PostHog for analytics tracking and add PostHogProvider component --- next.config.js | 21 +++++++-- package-lock.json | 68 ++++++++++++++++++++++++++++++ package.json | 2 + src/app/layout.tsx | 27 ++++++------ src/components/PostHogProvider.tsx | 29 +++++++++++++ src/lib/posthog.ts | 11 +++++ 6 files changed, 142 insertions(+), 16 deletions(-) create mode 100644 src/components/PostHogProvider.tsx create mode 100644 src/lib/posthog.ts diff --git a/next.config.js b/next.config.js index c5d4a08..47eab53 100644 --- a/next.config.js +++ b/next.config.js @@ -10,11 +10,24 @@ const nextConfig = { }, ], }, - webpack: (config, { buildId, dev, isServer, defaultLoaders, webpack }) => { - config.resolve.alias.canvas = false; - config.resolve.alias.encoding = false; - return config; + async rewrites() { + return [ + { + source: '/ingest/static/:path*', + destination: 'https://us-assets.i.posthog.com/static/:path*', + }, + { + source: '/ingest/:path*', + destination: 'https://us.i.posthog.com/:path*', + }, + { + source: '/ingest/flags', + destination: 'https://us.i.posthog.com/flags', + }, + ]; }, + // This is required to support PostHog trailing slash API requests + skipTrailingSlashRedirect: true, }; module.exports = nextConfig; diff --git a/package-lock.json b/package-lock.json index 57b622e..367a08c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -48,6 +48,8 @@ "next-themes": "^0.4.3", "pdf-parse": "^1.1.1", "postcss": "^8.4.47", + "posthog-js": "^1.259.0", + "posthog-node": "^5.6.0", "react": "18.2.0", "react-dom": "18.2.0", "react-hook-form": "^7.46.2", @@ -4887,6 +4889,17 @@ "node": ">=18" } }, + "node_modules/core-js": { + "version": "3.45.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.45.0.tgz", + "integrity": "sha512-c2KZL9lP4DjkN3hk/an4pWn5b5ZefhRJnAc42n6LJ19kSnbeRbdQZE5dSeE2LBol1OwJD3X1BQvFTAsa8ReeDA==", + "hasInstallScript": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, "node_modules/cross-fetch": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.2.0.tgz", @@ -6096,6 +6109,12 @@ "reusify": "^1.0.4" } }, + "node_modules/fflate": { + "version": "0.4.8", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.4.8.tgz", + "integrity": "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==", + "license": "MIT" + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -9452,6 +9471,49 @@ "node": ">=4" } }, + "node_modules/posthog-js": { + "version": "1.259.0", + "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.259.0.tgz", + "integrity": "sha512-6usLnJshky8fQ82ask7PIJh4BSFOU0VkRbFg8Zanm/HIlYMG1VOdRWlToA63JXeO7Bzm9TuREq1wFm5U2VEVCg==", + "license": "SEE LICENSE IN LICENSE", + "dependencies": { + "core-js": "^3.38.1", + "fflate": "^0.4.8", + "preact": "^10.19.3", + "web-vitals": "^4.2.4" + }, + "peerDependencies": { + "@rrweb/types": "2.0.0-alpha.17", + "rrweb-snapshot": "2.0.0-alpha.17" + }, + "peerDependenciesMeta": { + "@rrweb/types": { + "optional": true + }, + "rrweb-snapshot": { + "optional": true + } + } + }, + "node_modules/posthog-node": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-5.6.0.tgz", + "integrity": "sha512-MVXxKmqAYp2cPBrN1YMhnhYsJYIu6yc6wumbHz1dbo67wZBf2WtMm67Uh+4VCrp07049qierWlxQqz1W5zGDeg==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/preact": { + "version": "10.27.0", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.27.0.tgz", + "integrity": "sha512-/DTYoB6mwwgPytiqQTh/7SFRL98ZdiD8Sk8zIUVOxtwq4oWcwrcd1uno9fE/zZmUaUrFNYzbH14CPebOz9tZQw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, "node_modules/prebuild-install": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", @@ -11630,6 +11692,12 @@ "node": ">= 14" } }, + "node_modules/web-vitals": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-4.2.4.tgz", + "integrity": "sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==", + "license": "Apache-2.0" + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", diff --git a/package.json b/package.json index b2a1a98..4e06b9d 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,8 @@ "next-themes": "^0.4.3", "pdf-parse": "^1.1.1", "postcss": "^8.4.47", + "posthog-js": "^1.259.0", + "posthog-node": "^5.6.0", "react": "18.2.0", "react-dom": "18.2.0", "react-hook-form": "^7.46.2", diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 52c457d..3afd038 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -5,6 +5,7 @@ import { Inter } from 'next/font/google'; import { extractRouterConfig } from 'uploadthing/server'; import { ourFileRouter } from '@/app/api/uploadthing/core'; +import { PostHogProvider } from '@/components/PostHogProvider'; import Providers from '@/components/Providers'; import 'simplebar-react/dist/simplebar.min.css'; import './globals.css'; @@ -29,18 +30,20 @@ export default function RootLayout({ inter.className )} > - - - {children} - + + + + {children} + + diff --git a/src/components/PostHogProvider.tsx b/src/components/PostHogProvider.tsx new file mode 100644 index 0000000..47728ce --- /dev/null +++ b/src/components/PostHogProvider.tsx @@ -0,0 +1,29 @@ +'use client'; + +import posthogClient from 'posthog-js'; +import { PostHogProvider as PHProvider } from 'posthog-js/react'; +import { useEffect } from 'react'; + +export function PostHogProvider({ children }: { children: React.ReactNode }) { + const isProd = process.env.NODE_ENV === 'production'; + useEffect(() => { + const key = process.env.NEXT_PUBLIC_POSTHOG_KEY; + + if (!isProd) { + posthogClient.opt_out_capturing?.(); + return; + } + + if (!key) return; + + posthogClient.init(key, { + api_host: '/ingest', + ui_host: 'https://us.posthog.com', + capture_exceptions: true, // Enable PostHog error tracking + debug: false, + }); + }, [isProd]); + + if (!isProd) return <>{children}; + return {children}; +} diff --git a/src/lib/posthog.ts b/src/lib/posthog.ts new file mode 100644 index 0000000..afb8657 --- /dev/null +++ b/src/lib/posthog.ts @@ -0,0 +1,11 @@ +import { PostHog } from "posthog-node" + +// NOTE: This is a Node.js client, so you can use it for sending events from the server side to PostHog. +export default function PostHogClient() { + const posthogClient = new PostHog(process.env.NEXT_PUBLIC_POSTHOG_KEY!, { + host: process.env.NEXT_PUBLIC_POSTHOG_HOST, + flushAt: 1, + flushInterval: 0, + }) + return posthogClient +} \ No newline at end of file From 439eaf088073829c8f23c74bc06e383803fc71af Mon Sep 17 00:00:00 2001 From: Abdullah-dev0 Date: Mon, 11 Aug 2025 04:30:46 +0500 Subject: [PATCH 7/9] refactor: simplify MessageItem component styling and improve message rendering --- src/components/chat/messages/MessageItem.tsx | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/components/chat/messages/MessageItem.tsx b/src/components/chat/messages/MessageItem.tsx index b216a03..5936479 100644 --- a/src/components/chat/messages/MessageItem.tsx +++ b/src/components/chat/messages/MessageItem.tsx @@ -66,21 +66,13 @@ const MessageItem = ({
)} -
+
{messageText}
-
+
{isHistory && isHistoryMessage(message) ? formatTime(message.createdAt) : 'just now'} From c0adb7e77153b99e34ecb806fe2717275ae98336 Mon Sep 17 00:00:00 2001 From: Abdullah-dev0 Date: Mon, 11 Aug 2025 04:32:43 +0500 Subject: [PATCH 8/9] fix: correct import statement formatting and add missing newline at end of file --- src/lib/posthog.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/lib/posthog.ts b/src/lib/posthog.ts index afb8657..8908c6d 100644 --- a/src/lib/posthog.ts +++ b/src/lib/posthog.ts @@ -1,4 +1,4 @@ -import { PostHog } from "posthog-node" +import { PostHog } from 'posthog-node'; // NOTE: This is a Node.js client, so you can use it for sending events from the server side to PostHog. export default function PostHogClient() { @@ -6,6 +6,6 @@ export default function PostHogClient() { host: process.env.NEXT_PUBLIC_POSTHOG_HOST, flushAt: 1, flushInterval: 0, - }) - return posthogClient -} \ No newline at end of file + }); + return posthogClient; +} From 0d421f9adf52b581e73161a03ec3cffeb4c83601 Mon Sep 17 00:00:00 2001 From: Abdullah-dev0 Date: Mon, 11 Aug 2025 04:35:02 +0500 Subject: [PATCH 9/9] refactor: rename getMessagesChunk to getMessages for consistency and clarity --- src/actions/messages.action.ts | 44 +-------------------------- src/components/chat/ChatComponent.tsx | 4 +-- 2 files changed, 3 insertions(+), 45 deletions(-) diff --git a/src/actions/messages.action.ts b/src/actions/messages.action.ts index e8a4857..3b91421 100644 --- a/src/actions/messages.action.ts +++ b/src/actions/messages.action.ts @@ -26,7 +26,7 @@ export type MessagesChunk = { total?: number; // total count for simple pagination }; -export async function getMessagesChunk({ +export async function getMessages({ fileId, before, // ISO string; fetch messages with createdAt < before take = 4, @@ -76,45 +76,3 @@ export async function getMessagesChunk({ return { items: itemsAsc, hasMore, nextCursor }; } - -export async function getMessagesSimple({ - fileId, - skip = 0, - take = 10, -}: { - fileId: string; - skip?: number; - take?: number; -}): Promise { - const { userId } = await auth(); - if (!userId) throw new Error('Unauthorized'); - if (!fileId) throw new Error('File ID is required'); - if (!take || take < 1) throw new Error('take must be positive'); - - const file = await db.file.findFirst({ where: { id: fileId, userId } }); - if (!file) throw new Error('File not found or unauthorized'); - - // Get total count for pagination info - const total = await db.message.count({ where: { fileId } }); - - const messages = await db.message.findMany({ - where: { fileId }, - orderBy: { createdAt: 'desc' }, - skip, - take, - }); - - const hasMore = skip + take < total; - - const items = messages.map((m) => ({ - id: m.id, - text: m.text, - isUserMessage: m.isUserMessage, - createdAt: m.createdAt.toISOString(), - updatedAt: m.updatedAt.toISOString(), - userId: m.userId, - fileId: m.fileId, - })); - - return { items, hasMore, total }; -} diff --git a/src/components/chat/ChatComponent.tsx b/src/components/chat/ChatComponent.tsx index 72cb7cb..17c811a 100644 --- a/src/components/chat/ChatComponent.tsx +++ b/src/components/chat/ChatComponent.tsx @@ -11,7 +11,7 @@ import { useEffect, useMemo, useRef, useState } from 'react'; import { toast } from 'sonner'; import { - getMessagesChunk, + getMessages, type MessageDTO, type MessagesChunk, } from '@/actions/messages.action'; @@ -56,7 +56,7 @@ const ChatComponent = ({ file }: { file: File }) => { } = useInfiniteQuery( ['messages', file.id], async ({ pageParam = undefined }) => - await getMessagesChunk({ + await getMessages({ fileId: file.id, before: pageParam as string | undefined, take: 10,