From 53f6ad41830450c72673935e63619095ca81eec3 Mon Sep 17 00:00:00 2001 From: PoshanP Date: Thu, 8 Jan 2026 10:42:17 -0800 Subject: [PATCH] refactor: Code cleanup and deduplication - Extract magic values to constants (SWR intervals, panel dimensions, URL expiry) - Create shared ChatMessage/ChatSession types in lib/types/chat.ts - Create reusable BaseModal component for collection modals - Consolidate useUserId hook pattern in useApi.ts - Remove unused code: cache.ts, QueryBuilder, validation helpers, context exports - Fix duplicate CSS rules in globals.css - Update RAG config to use centralized constants --- app/api/rag/summary/route.ts | 11 +- app/chat-new/page.tsx | 47 ++-- app/globals.css | 9 - app/papers/page.tsx | 3 +- frontend/components/ExportChatButton.tsx | 14 +- frontend/components/NextReadList.tsx | 4 +- frontend/components/RecentPapers.tsx | 4 +- frontend/components/chat/MobileChatLayout.tsx | 27 +- frontend/components/chat/MobileChatView.tsx | 15 +- .../collections/AddToCollectionModal.tsx | 193 ++++++-------- frontend/components/collections/BaseModal.tsx | 133 ++++++++++ .../collections/CreateCollectionModal.tsx | 202 +++++++-------- .../collections/EditCollectionModal.tsx | 235 ++++++++---------- frontend/components/collections/index.ts | 1 + lib/constants/index.ts | 22 ++ lib/contexts/AuthContext.tsx | 5 +- lib/contexts/DataContext.tsx | 2 - lib/contexts/StatsContext.tsx | 13 - lib/db/index.ts | 122 +-------- lib/hooks/useApi.ts | 61 ++--- lib/rag/config.ts | 24 +- lib/types/chat.ts | 27 ++ lib/types/index.ts | 65 +---- lib/utils/cache.ts | 51 ---- lib/validation/index.ts | 76 +----- 25 files changed, 532 insertions(+), 834 deletions(-) create mode 100644 frontend/components/collections/BaseModal.tsx create mode 100644 lib/types/chat.ts delete mode 100644 lib/utils/cache.ts diff --git a/app/api/rag/summary/route.ts b/app/api/rag/summary/route.ts index f2c61e9..e362257 100644 --- a/app/api/rag/summary/route.ts +++ b/app/api/rag/summary/route.ts @@ -1,6 +1,7 @@ import { NextRequest, NextResponse } from 'next/server'; import { createClient } from '@supabase/supabase-js'; import OpenAI from 'openai'; +import { SUMMARY_MODEL, SUMMARY_MAX_TOKENS, MAX_SUMMARY_CONTEXT_LENGTH, TOP_K_CHUNKS } from '@/lib/constants'; const supabase = createClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, @@ -72,18 +73,18 @@ export async function POST(request: NextRequest) { .select('content') .eq('paper_id', paper_id) .order('page_no', { ascending: true }) - .limit(5); + .limit(TOP_K_CHUNKS); // If we have chunks, use them. Otherwise, try to use raw PDF content let contextForSummary = ''; if (chunks && chunks.length > 0) { - contextForSummary = chunks.map(c => c.content).join('\n\n').slice(0, 4000); + contextForSummary = chunks.map(c => c.content).join('\n\n').slice(0, MAX_SUMMARY_CONTEXT_LENGTH); } else if (pdfContent) { // Extract readable text from the PDF content (it might be garbled but often contains some text) const textMatch = pdfContent.match(/[\x20-\x7E\n\r]+/g); if (textMatch) { - contextForSummary = textMatch.join(' ').slice(0, 4000); + contextForSummary = textMatch.join(' ').slice(0, MAX_SUMMARY_CONTEXT_LENGTH); } } @@ -104,7 +105,7 @@ You can inquire about complex concepts, request explanations of technical detail // Generate summary using OpenAI with whatever content we have const completion = await openai.chat.completions.create({ - model: 'gpt-3.5-turbo', + model: SUMMARY_MODEL, messages: [ { role: 'system', @@ -115,7 +116,7 @@ You can inquire about complex concepts, request explanations of technical detail content: `Extract the title and summarize this research paper content:\n\n${contextForSummary}` } ], - max_tokens: 400, + max_tokens: SUMMARY_MAX_TOKENS, temperature: 0.3, }); diff --git a/app/chat-new/page.tsx b/app/chat-new/page.tsx index b1924ee..2f957d3 100644 --- a/app/chat-new/page.tsx +++ b/app/chat-new/page.tsx @@ -22,27 +22,18 @@ import { useConfirm } from "@/lib/contexts/ConfirmContext"; import { useBreakpoint } from "@/lib/hooks/useMediaQuery"; import { MobileChatLayout } from "@/frontend/components/chat/MobileChatLayout"; import { PdfViewer } from "@/frontend/components/PdfViewer"; +import { ChatMessage, ChatSession } from "@/lib/types/chat"; +import { + CHAT_PANEL_DEFAULT_WIDTH, + CHAT_PANEL_MIN_WIDTH, + CHAT_PANEL_MAX_WIDTH, + SIDEBAR_EXPANDED_WIDTH, + SIDEBAR_COLLAPSED_WIDTH, + MESSAGE_TITLE_TRUNCATE_LENGTH, + SIGNED_URL_EXPIRY_SECONDS, +} from "@/lib/constants"; -interface ChatSession { - id: string; - paper_id: string | null; - title: string; - created_at: string; - updated_at: string; - user_id: string; - paper?: { - title: string; - }; -} - -interface Message { - id: string; - content: string; - role: 'user' | 'assistant'; - created_at: string; - session_id: string; - metadata?: any; -} +type Message = ChatMessage; function ChatNewPageContent() { const supabase = useSupabase(); @@ -64,7 +55,7 @@ function ChatNewPageContent() { const [previewLoading, setPreviewLoading] = useState(false); const [chatOpen, setChatOpen] = useState(false); const [chatEverOpened, setChatEverOpened] = useState(false); - const [chatWidth, setChatWidth] = useState(550); + const [chatWidth, setChatWidth] = useState(CHAT_PANEL_DEFAULT_WIDTH); const [isResizing, setIsResizing] = useState(false); const [pdfBaseUrl, setPdfBaseUrl] = useState(null); const [processingStatus, setProcessingStatus] = useState<'pending' | 'processing' | 'completed' | 'failed' | null>(null); @@ -258,13 +249,13 @@ function ChatNewPageContent() { if (messagesError) throw messagesError; - const transformedMessages = (messagesData || []).map((msg, index) => ({ + const transformedMessages: ChatMessage[] = (messagesData || []).map((msg, index) => ({ id: `${msg.id}-${index}`, content: msg.content, role: msg.role as 'user' | 'assistant', created_at: msg.created_at, session_id: msg.session_id, - metadata: msg.metadata + metadata: msg.metadata as ChatMessage['metadata'] })); // Cache the messages @@ -318,7 +309,7 @@ function ChatNewPageContent() { try { if (messages.length === 0) { - const newTitle = messageContent.slice(0, 50) + (messageContent.length > 50 ? '...' : ''); + const newTitle = messageContent.slice(0, MESSAGE_TITLE_TRUNCATE_LENGTH) + (messageContent.length > MESSAGE_TITLE_TRUNCATE_LENGTH ? '...' : ''); await supabase .from('chat_sessions') .update({ @@ -516,7 +507,7 @@ function ChatNewPageContent() { if (updatedPaper.storage_path) { const { data: signed } = await supabase.storage .from('papers') - .createSignedUrl(updatedPaper.storage_path, 60 * 60); + .createSignedUrl(updatedPaper.storage_path, SIGNED_URL_EXPIRY_SECONDS); if (signed?.signedUrl) { setPdfBaseUrl(signed.signedUrl); } @@ -556,7 +547,7 @@ function ChatNewPageContent() { const { data: signed, error: signedError } = await supabase.storage .from('papers') - .createSignedUrl(paper.storage_path, 60 * 60); + .createSignedUrl(paper.storage_path, SIGNED_URL_EXPIRY_SECONDS); if (signedError || !signed?.signedUrl) { console.error('Signed URL error:', signedError); @@ -661,10 +652,10 @@ function ChatNewPageContent() { const handleMouseMove = (e: MouseEvent) => { if (!isResizing) return; e.preventDefault(); - const sidebarWidth = sidebarOpen ? 256 : 56; + const sidebarWidth = sidebarOpen ? SIDEBAR_EXPANDED_WIDTH : SIDEBAR_COLLAPSED_WIDTH; const newWidth = e.clientX - sidebarWidth; requestAnimationFrame(() => { - setChatWidth(Math.min(Math.max(280, newWidth), 800)); + setChatWidth(Math.min(Math.max(CHAT_PANEL_MIN_WIDTH, newWidth), CHAT_PANEL_MAX_WIDTH)); }); }; diff --git a/app/globals.css b/app/globals.css index 6580779..0e3cabd 100644 --- a/app/globals.css +++ b/app/globals.css @@ -46,15 +46,6 @@ body { -webkit-overflow-scrolling: touch; } -/* Hide scrollbar on mobile for cleaner UI */ -.scrollbar-hide::-webkit-scrollbar { - display: none; -} -.scrollbar-hide { - -ms-overflow-style: none; - scrollbar-width: none; -} - /* Hide scrollbar utility */ .scrollbar-hide { -ms-overflow-style: none; diff --git a/app/papers/page.tsx b/app/papers/page.tsx index f102af9..36e7ea6 100644 --- a/app/papers/page.tsx +++ b/app/papers/page.tsx @@ -18,6 +18,7 @@ import { import { CollectionsSidebar, AddToCollectionModal, CollectionBadges } from "@/frontend/components/collections"; import { useCollections, useCollectionPapers, invalidateCollectionCaches } from "@/lib/hooks/useCollections"; import { CollectionWithCount } from "@/lib/types/database"; +import { SIGNED_URL_EXPIRY_SECONDS } from "@/lib/constants"; import { formatDate } from "@/lib/utils/dateFormat"; import { renderPdfFirstPage } from "@/lib/utils/pdfPreview"; @@ -155,7 +156,7 @@ function PapersPageContent() { if (paper.storage_path) { const { data, error } = await supabase.storage .from('papers') - .createSignedUrl(paper.storage_path, 60 * 60); + .createSignedUrl(paper.storage_path, SIGNED_URL_EXPIRY_SECONDS); if (!error && data?.signedUrl) { const img = await renderPdfFirstPage(data.signedUrl, 560); if (img) { diff --git a/frontend/components/ExportChatButton.tsx b/frontend/components/ExportChatButton.tsx index 48c08e7..e89d3d1 100644 --- a/frontend/components/ExportChatButton.tsx +++ b/frontend/components/ExportChatButton.tsx @@ -4,24 +4,14 @@ import { useState } from "react"; import { Download, Loader2, FileText, File } from "lucide-react"; import { exportChatAsPDF, exportChatAsTXT } from "@/lib/utils/export-chat"; import { useAlert } from "@/lib/contexts/AlertContext"; +import { ChatMessage } from "@/lib/types/chat"; interface ExportChatButtonProps { sessionId: string; sessionTitle: string; paperTitle?: string; sessionDate: string; - messages: Array<{ - id: string; - content: string; - role: 'user' | 'assistant'; - created_at: string; - metadata?: { - citations?: Array<{ - page_no: number; - score: number; - }>; - }; - }>; + messages: ChatMessage[]; } export function ExportChatButton({ diff --git a/frontend/components/NextReadList.tsx b/frontend/components/NextReadList.tsx index a2585f9..4b6a7e7 100644 --- a/frontend/components/NextReadList.tsx +++ b/frontend/components/NextReadList.tsx @@ -14,7 +14,7 @@ import { hasPreview } from "@/lib/utils/previewCache"; import { useCollections, useCollectionPapers, invalidateCollectionCaches } from "@/lib/hooks/useCollections"; -import { NEXT_READ_COLLECTION_NAME } from "@/lib/constants"; +import { NEXT_READ_COLLECTION_NAME, SIGNED_URL_EXPIRY_SECONDS } from "@/lib/constants"; export function NextReadList() { const supabase = useSupabase(); @@ -91,7 +91,7 @@ export function NextReadList() { if (paper.storage_path) { const signed = await supabase.storage .from('papers') - .createSignedUrl(paper.storage_path, 60 * 60); + .createSignedUrl(paper.storage_path, SIGNED_URL_EXPIRY_SECONDS); const pdfUrl = signed.data?.signedUrl; if (pdfUrl) { diff --git a/frontend/components/RecentPapers.tsx b/frontend/components/RecentPapers.tsx index d05dfd9..b2d8424 100644 --- a/frontend/components/RecentPapers.tsx +++ b/frontend/components/RecentPapers.tsx @@ -15,7 +15,7 @@ import { clearPreview } from "@/lib/utils/previewCache"; import { useCollections, useCollectionPapers } from "@/lib/hooks/useCollections"; -import { SYSTEM_COLLECTION_NAME } from "@/lib/constants"; +import { SYSTEM_COLLECTION_NAME, SIGNED_URL_EXPIRY_SECONDS } from "@/lib/constants"; import { formatDate } from "@/lib/utils/dateFormat"; import { renderPdfFirstPage } from "@/lib/utils/pdfPreview"; @@ -110,7 +110,7 @@ export function RecentPapers() { if (paper.storage_path) { const signed = await supabase.storage .from('papers') - .createSignedUrl(paper.storage_path, 60 * 60); + .createSignedUrl(paper.storage_path, SIGNED_URL_EXPIRY_SECONDS); const pdfUrl = signed.data?.signedUrl; diff --git a/frontend/components/chat/MobileChatLayout.tsx b/frontend/components/chat/MobileChatLayout.tsx index ef74a5e..2626b57 100644 --- a/frontend/components/chat/MobileChatLayout.tsx +++ b/frontend/components/chat/MobileChatLayout.tsx @@ -4,36 +4,13 @@ import { useState, useCallback, useRef, useEffect } from "react"; import { MobileChatView } from "./MobileChatView"; import { MobilePdfView } from "./MobilePdfView"; import { MessageSquare, X, ChevronDown, Trash2, Plus, Search } from "lucide-react"; - -interface Message { - id: string; - content: string; - role: "user" | "assistant"; - created_at: string; - session_id: string; - metadata?: { - is_loading?: boolean; - is_system_summary?: boolean; - }; -} - -interface ChatSession { - id: string; - paper_id: string | null; - title: string; - created_at: string; - updated_at: string; - user_id: string; - paper?: { - title: string; - }; -} +import { ChatMessage, ChatSession } from "@/lib/types/chat"; interface MobileChatLayoutProps { sessions: ChatSession[]; currentSession: ChatSession | null; filterPaperId: string | null; - messages: Message[]; + messages: ChatMessage[]; input: string; onInputChange: (value: string) => void; onSendMessage: () => void; diff --git a/frontend/components/chat/MobileChatView.tsx b/frontend/components/chat/MobileChatView.tsx index c9516b9..1b53d60 100644 --- a/frontend/components/chat/MobileChatView.tsx +++ b/frontend/components/chat/MobileChatView.tsx @@ -2,21 +2,10 @@ import { useRef, useEffect } from "react"; import { Send, Loader2 } from "lucide-react"; - -interface Message { - id: string; - content: string; - role: "user" | "assistant"; - created_at: string; - session_id: string; - metadata?: { - is_loading?: boolean; - is_system_summary?: boolean; - }; -} +import { ChatMessage } from "@/lib/types/chat"; interface MobileChatViewProps { - messages: Message[]; + messages: ChatMessage[]; input: string; onInputChange: (value: string) => void; onSendMessage: () => void; diff --git a/frontend/components/collections/AddToCollectionModal.tsx b/frontend/components/collections/AddToCollectionModal.tsx index 53f84ef..bfc918f 100644 --- a/frontend/components/collections/AddToCollectionModal.tsx +++ b/frontend/components/collections/AddToCollectionModal.tsx @@ -1,7 +1,8 @@ "use client"; import { useState, useEffect } from "react"; -import { X, Loader2, Check, FolderPlus } from "lucide-react"; +import { Loader2, Check, FolderPlus } from "lucide-react"; +import { BaseModal, ModalButton } from "./BaseModal"; import { CollectionIcon } from "./CollectionIcon"; import { useCollections, usePaperCollections, invalidateCollectionCaches } from "@/lib/hooks/useCollections"; import { updatePaperCollections } from "@/lib/api/collections"; @@ -65,126 +66,96 @@ export function AddToCollectionModal({ } }; - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === "Escape") { - onClose(); - } - }; - const isLoading = collectionsLoading || paperCollectionsLoading; - if (!isOpen || !paperId) return null; + if (!paperId) return null; return ( -
-