From fac5cd358fa890c602638f22b75e4f29482882a3 Mon Sep 17 00:00:00 2001 From: Mehfuz Date: Thu, 5 Nov 2026 10:51:53 -0800 Subject: [PATCH] Fix invite request and remove dead code The invite-verify page posted to /v1/invites/request, which the API no longer serves (404), so "Request invite" always failed with "Not Found". It now posts to /v1/invites, the same call the /invite page makes. Removes the chat component, the document upload dialog and the project service. Nothing imported them, and they called API routes that no longer exist (/projects, /openai). Also drops the openai, marked and typewriter-effect packages, which only those files used. --- README.md | 2 +- package-lock.json | 64 - package.json | 3 - src/app/components/chat.tsx | 1028 ----------------- src/app/components/document-upload-dialog.tsx | 526 --------- src/app/invite-verify/page.tsx | 2 +- src/app/services/project.service.ts | 253 ---- 7 files changed, 2 insertions(+), 1876 deletions(-) delete mode 100644 src/app/components/chat.tsx delete mode 100644 src/app/components/document-upload-dialog.tsx delete mode 100644 src/app/services/project.service.ts diff --git a/README.md b/README.md index 20c16a3..0a8a26a 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ API after you sign in. src/app/ One folder per route (Next.js App Router), each with a page.tsx components/ Shared UI: app shell, header, charts, sign-in buttons settings/sections/ The settings panels: tokens, web search, billing, credits - services/ API clients for users and projects + services/ API client for users and sessions hooks/useAuth.ts Session handling constants.js API endpoints and shared copy models/ Model catalogue page and published-scores.json diff --git a/package-lock.json b/package-lock.json index 737f356..1673e46 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,11 +16,9 @@ "crypto-js": "^4.2.0", "easymde": "^2.20.0", "markdown-to-jsx": "^7.5.0", - "marked": "^14.1.3", "moment": "^2.30.1", "mui-one-time-password-input": "^2.0.3", "next": "14.2.7", - "openai": "^6.9.1", "primereact": "^10.8.2", "prismjs": "^1.29.0", "react": "^18", @@ -36,7 +34,6 @@ "rehype-raw": "^7.0.0", "rehype-sanitize": "^6.0.0", "remark-gfm": "^4.0.1", - "typewriter-effect": "^2.21.0", "uuid": "^10.0.0" }, "devDependencies": { @@ -4719,17 +4716,6 @@ "react": ">= 0.14.0" } }, - "node_modules/marked": { - "version": "14.1.3", - "resolved": "https://registry.npmjs.org/marked/-/marked-14.1.3.tgz", - "integrity": "sha512-ZibJqTULGlt9g5k4VMARAktMAjXoVnnr+Y3aCqW1oDftcV4BA3UmrBifzXoZyenHRk75csiPu9iwsTj4VNBT0g==", - "bin": { - "marked": "bin/marked.js" - }, - "engines": { - "node": ">= 18" - } - }, "node_modules/mdast-util-find-and-replace": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", @@ -6256,27 +6242,6 @@ "wrappy": "1" } }, - "node_modules/openai": { - "version": "6.9.1", - "resolved": "https://registry.npmjs.org/openai/-/openai-6.9.1.tgz", - "integrity": "sha512-vQ5Rlt0ZgB3/BNmTa7bIijYFhz3YBceAA3Z4JuoMSBftBF9YqFHIEhZakSs+O/Ad7EaoEimZvHxD5ylRjN11Lg==", - "license": "Apache-2.0", - "bin": { - "openai": "bin/cli" - }, - "peerDependencies": { - "ws": "^8.18.0", - "zod": "^3.25 || ^4.0" - }, - "peerDependenciesMeta": { - "ws": { - "optional": true - }, - "zod": { - "optional": true - } - } - }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -6437,12 +6402,6 @@ "node": ">=8" } }, - "node_modules/performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", - "license": "MIT" - }, "node_modules/picocolors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", @@ -6732,15 +6691,6 @@ } ] }, - "node_modules/raf": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", - "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", - "license": "MIT", - "dependencies": { - "performance-now": "^2.1.0" - } - }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", @@ -8013,20 +7963,6 @@ "node": ">=14.17" } }, - "node_modules/typewriter-effect": { - "version": "2.21.0", - "resolved": "https://registry.npmjs.org/typewriter-effect/-/typewriter-effect-2.21.0.tgz", - "integrity": "sha512-Y3VL1fuJpUBj0gS4OTXBLzy1gnYTYaBuVuuO99tGNyTkkub5CXi+b/hsV7Og9fp6HlhogOwWJwgq7iXI5sQlEg==", - "license": "MIT", - "dependencies": { - "prop-types": "^15.8.1", - "raf": "^3.4.1" - }, - "peerDependencies": { - "react": "^17.x || ^18.x", - "react-dom": "^17.x || ^18.x" - } - }, "node_modules/typo-js": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/typo-js/-/typo-js-1.3.1.tgz", diff --git a/package.json b/package.json index fbec9c6..77e5326 100644 --- a/package.json +++ b/package.json @@ -18,11 +18,9 @@ "crypto-js": "^4.2.0", "easymde": "^2.20.0", "markdown-to-jsx": "^7.5.0", - "marked": "^14.1.3", "moment": "^2.30.1", "mui-one-time-password-input": "^2.0.3", "next": "14.2.7", - "openai": "^6.9.1", "primereact": "^10.8.2", "prismjs": "^1.29.0", "react": "^18", @@ -38,7 +36,6 @@ "rehype-raw": "^7.0.0", "rehype-sanitize": "^6.0.0", "remark-gfm": "^4.0.1", - "typewriter-effect": "^2.21.0", "uuid": "^10.0.0" }, "devDependencies": { diff --git a/src/app/components/chat.tsx b/src/app/components/chat.tsx deleted file mode 100644 index 518182b..0000000 --- a/src/app/components/chat.tsx +++ /dev/null @@ -1,1028 +0,0 @@ -'use client'; - -import Image from "next/image"; -import React from "react"; - -import { useRouter, usePathname } from "next/navigation"; - -import { userService } from '@/app/services/user.service'; -import { createRef, useEffect, useRef, useState } from "react"; -import { Box, Button, CircularProgress, Input, InputAdornment, InputBase, LinearProgress, unstable_useId, withStyles } from "@mui/material"; -import OpenAI from 'openai'; - -import ArrowCircleUpIcon from '@mui/icons-material/ArrowCircleUp'; -import ContentCopyIcon from '@mui/icons-material/ContentCopy'; -import AssignmentIcon from '@mui/icons-material/Assignment'; -import AutoAwesomeIcon from '@mui/icons-material/AutoAwesome'; -import SettingsIcon from '@mui/icons-material/Settings'; -import SearchIcon from '@mui/icons-material/Search'; -import InsertDriveFileIcon from '@mui/icons-material/InsertDriveFile'; -import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward'; - -import KeyboardIcon from '@mui/icons-material/Keyboard'; -import StopCircleIcon from '@mui/icons-material/StopCircle'; - -import { API_ENDPOINT } from '@/app/constants'; - -import { marked, Renderer, Tokens } from 'marked'; - -import LightbulbIcon from '@mui/icons-material/Lightbulb'; -import KeyboardArrowRightIcon from '@mui/icons-material/KeyboardArrowRight'; -import CheckCircleIcon from '@mui/icons-material/CheckCircle'; - -import Typewriter from 'typewriter-effect'; -import { CopyToClipboard } from 'react-copy-to-clipboard'; -import ThumbUpOffAltIcon from '@mui/icons-material/ThumbUpOffAlt'; -import ThumbDownAltIcon from '@mui/icons-material/ThumbDownOffAlt'; -import ThumbUpIcon from '@mui/icons-material/ThumbUp'; -import ThumbDownIcon from '@mui/icons-material/ThumbDown'; -import WarningAmberIcon from '@mui/icons-material/WarningAmber'; -import LockIcon from '@mui/icons-material/Lock'; -import BuildIcon from '@mui/icons-material/Build'; -import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline'; - -// Configure marked options for proper code rendering -marked.setOptions({ - breaks: true, - gfm: true, -}); - -// Custom renderer for code blocks to preserve formatting -const renderer = new Renderer(); -const originalCodeRenderer = renderer.code.bind(renderer); -renderer.code = function(token: Tokens.Code) { - const code = token.text; - const language = token.lang || ''; - - // For CSV and csvtext, render as table (each line as a row) - if (language.toLowerCase() === 'csv' || language.toLowerCase() === 'csvtext' || language.toLowerCase().includes('csv')) { - const lines = code.trim().split('\n').filter(line => line.trim()); - if (lines.length === 0) return ''; - - let tableHtml = '
'; - - // First line as header - const headers = lines[0].split(',').map(h => h.trim()); - tableHtml += ''; - headers.forEach(header => { - tableHtml += ``; - }); - tableHtml += ''; - - // All remaining lines as data rows - tableHtml += ''; - for (let i = 1; i < lines.length; i++) { - const cells = lines[i].split(',').map(cell => cell.trim()); - tableHtml += ''; - cells.forEach(cell => { - tableHtml += ``; - }); - tableHtml += ''; - } - tableHtml += ''; - - tableHtml += '
${header}
${cell}
'; - return tableHtml; - } - return originalCodeRenderer(token); -}; - -marked.use({ renderer }); - -export default function Chat(props: { project?: any, initialMessage?: string | null, onTokenUpdate?: () => void }) { - const messagesEndRef = useRef(null) - - const [formData, setFormData] = useState({ - message: "", - }); - - const [messages, setMessages]: any = useState([]); - const [searchQuery, setSearchQuery] = useState(""); - const [filteredMessages, setFilteredMessages]: any = useState([]); - const [isLoading, setIsLoading] = useState(false); - const [showTips, setShowTips] = useState(null); - const [copied, setCopied]: any = useState(""); - const [seed, setSeed] = useState(1); - const [messageSent, setMessageSent] = useState(false); - const abortControllerRef = useRef(null); - const [userId, setUserId] = useState(null); - const [generalProjectId, setGeneralProjectId] = useState(null); - const [sessionId, setSessionId] = useState(null); - const currentProjectIdRef = useRef(null); - const inputRef = useRef(null); - - const handleInput = (e: any) => { - const fieldName = e.target.name; - const fieldValue = e.target.value; - - setFormData((prevState) => ({ - ...prevState, - [fieldName]: fieldValue - })); - } - - const handleSearchInput = (e: any) => { - setSearchQuery(e.target.value); - } - - const scrollToBottom = () => { - if (messagesEndRef.current) { - messagesEndRef.current.scrollIntoView({ - inline: 'nearest', - behavior: 'smooth', - block: 'end', - }); - } - } - - const sendFeedback = async (msg: any) => { - var url = `${API_ENDPOINT}/projects/${props.project.id}/messages/${msg.id}/feedback`; - - const options = { - method: 'PUT', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${userService.token()}` - }, - body: JSON.stringify({ - feedback: msg.feedback - }) - } - - await fetch(url, options); - } - - const sendPositiveFeedback = (msg: any) => { - setMessages((prevState: any) => ([ - ...prevState.map((_msg: any) => { - if (_msg.id === msg.id) { - _msg.feedback = 1; - } - return _msg; - }) - ])); - - sendFeedback(msg); - } - - const sendNegativeFeedback = (msg: any) => { - setMessages((prevState: any) => ([ - ...prevState.map((_msg: any) => { - if (_msg.id === msg.id) { - _msg.feedback = 2; - } - return _msg; - }) - ])); - sendFeedback(msg); - } - - const handleKeyDown = (e: any) => { - if (e.keyCode === 13 && !e.shiftKey) { - e.preventDefault(); - sendMessage(e); - scrollToBottom(); - } - } - - // Save messages to localStorage - const saveMessagesToStorage = (msgs: any[]) => { - if (sessionId) { - const projectId = props.project?.id || generalProjectId || 'general'; - const key = `chat_${sessionId}_${projectId}`; - localStorage.setItem(key, JSON.stringify(msgs)); - } - }; - - // Load messages from localStorage - const loadMessagesFromStorage = () => { - if (sessionId) { - const projectId = props.project?.id || generalProjectId || 'general'; - const key = `chat_${sessionId}_${projectId}`; - const stored = localStorage.getItem(key); - if (stored) { - try { - return JSON.parse(stored); - } catch (e) { - console.error('Error parsing stored messages:', e); - } - } - } - return []; - }; - - const sendMessage = async (e: any, messageOverride?: string) => { - if (isLoading) { - console.log('Already loading, skipping message send'); - return; - } - - let userMessage = messageOverride || formData.message.trim(); - - if (userMessage.length === 0) { - console.log('Empty message, skipping'); - return; - } - - const messageId = `msg-${Date.now()}`; - const responseId = `response-${Date.now()}`; - - const effectiveProjectId = props.project?.id || generalProjectId || 'general'; - console.log('Sending message:', userMessage, 'Project ID:', effectiveProjectId, 'General ID:', generalProjectId); - - // Add user message to the chat - const userMsg = { - id: messageId, - direction: 'in', - payload: { - type: 'text', - text: userMessage, - html: marked(userMessage), - is_new: false - } - }; - - const updatedMessages = [...messages, userMsg]; - setMessages(updatedMessages); - saveMessagesToStorage(updatedMessages); - setFormData({ message: "" }); - setIsLoading(true); - - // Create abort controller for this request - abortControllerRef.current = new AbortController(); - - // Create initial response message for streaming - let responseMsg = { - id: responseId, - direction: 'out', - is_new: true, - payload: { - type: 'text', - text: '', - html: '' - } - }; - - setMessages((prevState: any) => [...prevState, responseMsg]); - - try { - // Initialize OpenAI client - const openai = new OpenAI({ - baseURL: `${API_ENDPOINT}/openai`, - apiKey: userService.token() || '', - dangerouslyAllowBrowser: true - }); - - let fullText = ''; - - // Determine assistant type and project ID based on whether a project is selected - const assistantType = props.project?.id ? 'project_assistant' : 'general_assistant'; - const projectId = props.project?.id ? String(props.project.id) : (generalProjectId || 'general'); - - // Create streaming chat completion with custom fields - const stream = await openai.chat.completions.create({ - model: 'slp-model-001', - messages: [{ role: 'user', content: userMessage }], - stream: true, - assistant_type: assistantType, - project_id: projectId, - session_id: sessionId || 'default_session' - } as any) as any; - - // Process the stream - for await (const chunk of stream) { - // Check if aborted - if (abortControllerRef.current?.signal.aborted) { - throw new Error('AbortError'); - } - - const content = chunk.choices[0]?.delta?.content; - - if (content) { - fullText += content; - - setMessages((prevMessages: any) => - prevMessages.map((msg: any) => - msg.id === responseId ? { - ...msg, - is_new: true, - payload: { - ...msg.payload, - text: fullText, - html: marked(fullText) as string - } - } : msg - ) - ); - - setTimeout(() => { - scrollToBottom(); - }, 10); - } - - // Check for finish - if (chunk.choices[0]?.finish_reason) { - break; - } - } - - // Mark response as complete and save to storage - setMessages((prevMessages: any) => { - const finalMessages = prevMessages.map((msg: any) => - msg.id === responseId ? { - ...msg, - is_new: false - } : msg - ); - saveMessagesToStorage(finalMessages); - return finalMessages; - }); - - // Notify parent about token update - if (props.onTokenUpdate) { - props.onTokenUpdate(); - } - - // Refocus input after response - setTimeout(() => { - inputRef.current?.focus(); - }, 200); - - } catch (error: any) { - if (error.message === 'AbortError' || error.name === 'AbortError') { - console.log('Request aborted'); - } else { - console.error('Error sending message:', error); - - // Check for 429 rate limit / out of credits error - let errorMessage = 'An error occurred while processing your message. Please try again.'; - let errorIconType = 'error'; - - if (error.status === 429 || error.message?.includes('429') || error.message?.includes('rate limit')) { - errorIconType = 'warning'; - errorMessage = 'You have exceeded your usage limit or are out of credits. Please upgrade your account or reach out to us at support@smartloop.ai'; - } else if (error.status === 401 || error.status === 403) { - errorIconType = 'lock'; - errorMessage = 'Authentication error. Please log in again.'; - } else if (error.status >= 500) { - errorIconType = 'build'; - errorMessage = 'Server error. Our team has been notified. Please try again later.'; - } else if (error.message) { - errorMessage = `Error: ${error.message}`; - } - - // Update the response message with the error instead of removing it - setMessages((prevState: any) => { - const updatedMessages = prevState.map((msg: any) => - msg.id === responseId ? { - ...msg, - is_new: false, - isError: true, - errorIconType: errorIconType, - payload: { - ...msg.payload, - text: errorMessage, - html: marked(errorMessage) as string - } - } : msg - ); - saveMessagesToStorage(updatedMessages); - return updatedMessages; - }); - } - } finally { - setIsLoading(false); - abortControllerRef.current = null; - setTimeout(() => { - scrollToBottom(); - inputRef.current?.focus(); - }, 100); - } - } - - // Load userId on mount and generate general project ID and session ID - useEffect(() => { - userService.get().then(u => { - if (u && u.email) { - setUserId(u.email); - - // Generate or retrieve a consistent project ID for general assistant - const storageKey = `general_assistant_project_${u.email}`; - let storedProjectId = localStorage.getItem(storageKey); - - if (!storedProjectId) { - // Generate a random project ID for this user's general assistant - storedProjectId = `general_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`; - localStorage.setItem(storageKey, storedProjectId); - } - - console.log('General project ID set:', storedProjectId); - setGeneralProjectId(storedProjectId); - - // Generate or retrieve a session ID for this user session - const sessionKey = `chat_session_${u.email}`; - let storedSessionId = sessionStorage.getItem(sessionKey); - - if (!storedSessionId) { - // Generate a unique session ID for this browser session - storedSessionId = `session_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`; - sessionStorage.setItem(sessionKey, storedSessionId); - } - - console.log('Session ID set:', storedSessionId); - setSessionId(storedSessionId); - } - }); - }, []); - - // Load messages from localStorage when sessionId is available or project changes - useEffect(() => { - if (sessionId) { - const currentProjectId = props.project?.id || generalProjectId || 'general'; - - // Only reload if the project ID actually changed - if (currentProjectIdRef.current !== currentProjectId) { - currentProjectIdRef.current = currentProjectId; - - // Small delay to ensure generalProjectId is set - setTimeout(() => { - const storedMessages = loadMessagesFromStorage(); - console.log('Loading messages for project:', currentProjectId, 'Count:', storedMessages.length); - if (storedMessages.length > 0) { - setMessages(storedMessages); - setShowTips(false); - } else { - setMessages([]); - setShowTips(true); - } - }, 100); - } - } - }, [sessionId, generalProjectId, props.project?.id]); - - useEffect(() => { - // Show tips if no messages and no initial message is being sent - if (messages.length === 0 && !props.initialMessage && !messageSent) { - setTimeout(() => { - setShowTips(true); - }, 100); - } else if (messages.length > 0 || props.initialMessage || messageSent) { - setShowTips(false); - } - }, [messages.length, props.initialMessage, messageSent]); - - // Cleanup: abort any ongoing requests when component unmounts - useEffect(() => { - return () => { - if (abortControllerRef.current) { - abortControllerRef.current.abort(); - } - }; - }, []); - - // Auto-send initial message when provided - useEffect(() => { - if (props.initialMessage && !messageSent && !isLoading && userId) { - // Small delay to ensure component is ready (and generalProjectId to be set) - setTimeout(() => { - sendMessage({} as any, props.initialMessage || undefined); - setMessageSent(true); - }, 800); - } - }, [props.initialMessage, messageSent, isLoading, userId]); - - useEffect(() => { - if (searchQuery.trim() === "") { - setFilteredMessages(messages); - } else { - const filtered = messages.filter((msg: any) => { - return msg.payload.text.toLowerCase().includes(searchQuery.toLowerCase()); - }); - setFilteredMessages(filtered); - } - }, [searchQuery, messages]); - - // Auto-scroll to bottom when messages change - useEffect(() => { - scrollToBottom(); - }, [messages]); - - // Save messages to localStorage whenever they change (backup save) - useEffect(() => { - if (messages.length > 0 && sessionId) { - saveMessagesToStorage(messages); - } - }, [messages, sessionId, props.project?.id]); - - const displayMessages = searchQuery.trim() === "" ? messages : filteredMessages; - - return ( - <> - - -
-
-
- - {(displayMessages || []).length === 0 && showTips && searchQuery.trim() === "" && -
-
- -
- - - -

Tips to Get Started

-
- -
-
- -

- Ask specific questions: Be detailed and clear. Request summaries, comparisons, or tables. The AI remembers context, so feel free to ask follow-up questions -

-
- -
- -

- Customize with project rules: Visit the Settings tab to define project rules that refine how the AI responds to your specific needs -

-
- -
- -

- Work across documents: The AI analyzes all uploaded documents together. Ask comprehensive questions to get insights from multiple sources -

-
-
-
-
-
- } - {(displayMessages || []).length === 0 && searchQuery.trim() !== "" && -
-
-
- -

No messages found matching "{searchQuery}"

-
-
-
- } -
- {displayMessages && displayMessages.map((msg: any) => { - return ( - <> - {msg.direction === 'out' ? - - - - - - -
-
- {msg.isError && msg.errorIconType && ( - - {msg.errorIconType === 'warning' && } - {msg.errorIconType === 'lock' && } - {msg.errorIconType === 'build' && } - {msg.errorIconType === 'error' && } - - )} - {(!msg.payload.text || msg.payload.text.trim() === '') && msg.is_new ? ( - - ) : ( - <> -
- {msg.is_new && ( - - )} - - )} -
-
- - { - setCopied(msg.id); - setTimeout(() => setCopied(""), 1000); - }}> - - - - - - - - : - -
-
-
-
- - - -
-
- } - - - ) - }) - } -
-
-
-
- - - - - - ) : null - } - sx={{ - border: 'none', - height: '100%', - fontSize: '15px', - lineHeight: '1.5', - '& .MuiInputBase-input': { - caretColor: '#ec4899', - }, - '& .MuiInputBase-input.Mui-disabled': { - color: 'var(--muted) !important', - WebkitTextFillColor: 'var(--muted) !important', - }, - '& .MuiInputBase-input::placeholder': { - color: 'var(--muted)', - opacity: 1 - }, - '& .MuiInputBase-input.Mui-disabled::placeholder': { - color: 'var(--muted) !important', - opacity: '1 !important', - WebkitTextFillColor: 'var(--muted) !important', - }, - }} - /> - - - - - - -
- -
- - - ) -} diff --git a/src/app/components/document-upload-dialog.tsx b/src/app/components/document-upload-dialog.tsx deleted file mode 100644 index ecd1f5c..0000000 --- a/src/app/components/document-upload-dialog.tsx +++ /dev/null @@ -1,526 +0,0 @@ -'use client'; - -import { useState, useEffect } from 'react'; -import { Box, Dialog, DialogContent, DialogTitle, TextField, Typography, CircularProgress, IconButton, Autocomplete } from '@mui/material'; -import CloseIcon from '@mui/icons-material/Close'; -import CloudUploadIcon from '@mui/icons-material/CloudUpload'; -import InsertDriveFileIcon from '@mui/icons-material/InsertDriveFile'; -import FolderIcon from '@mui/icons-material/Folder'; -import CheckIcon from '@mui/icons-material/Check'; -import { userService } from '@/app/services/user.service'; -import { projectService } from '@/app/services/project.service'; -import { API_ENDPOINT } from '@/app/constants'; - -interface DocumentUploadDialogProps { - open: boolean; - onClose: () => void; - onSuccess: () => void; - user: any; - setUser: (user: any) => void; - preselectedProjectId?: string; - disableProjectSelection?: boolean; -} - -export default function DocumentUploadDialog({ - open, - onClose, - onSuccess, - user, - setUser, - preselectedProjectId, - disableProjectSelection = false -}: DocumentUploadDialogProps) { - const [selectedProject, setSelectedProject] = useState(null); - const [projectInputValue, setProjectInputValue] = useState(''); - const [newProjectName, setNewProjectName] = useState(''); - const [uploadFiles, setUploadFiles] = useState([]); - const [isUploading, setIsUploading] = useState(false); - const [uploadError, setUploadError] = useState(''); - const [isDragging, setIsDragging] = useState(false); - const [showCreateNew, setShowCreateNew] = useState(false); - - // Set preselected project when dialog opens - useEffect(() => { - if (open && preselectedProjectId && user?.projects) { - const project = user.projects.find((p: any) => p.id === preselectedProjectId); - if (project) { - setSelectedProject(project); - setProjectInputValue(project.title || project.name); - } - } - }, [open, preselectedProjectId, user?.projects]); - - const handleClose = () => { - setSelectedProject(null); - setProjectInputValue(''); - setNewProjectName(''); - setUploadFiles([]); - setUploadError(''); - setIsDragging(false); - setShowCreateNew(false); - onClose(); - }; - - const validateAndSetFile = (file: File) => { - const allowedTypes = ['application/pdf', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'text/plain', 'text/csv']; - const maxSize = 5 * 1024 * 1024; // 5MB - - if (!allowedTypes.includes(file.type)) { - setUploadError('Invalid file type. Supported: PDF, DOCX, TXT, CSV'); - return; - } - - if (file.size > maxSize) { - setUploadError('File size exceeds 5MB limit'); - return; - } - - setUploadError(''); - setUploadFiles([file]); - }; - - const handleFileSelect = (e: React.ChangeEvent) => { - if (e.target.files && e.target.files.length > 0) { - const file = e.target.files[0]; - validateAndSetFile(file); - } - }; - - const handleDragOver = (e: React.DragEvent) => { - e.preventDefault(); - setIsDragging(true); - }; - - const handleDragLeave = (e: React.DragEvent) => { - e.preventDefault(); - setIsDragging(false); - }; - - const handleDrop = (e: React.DragEvent) => { - e.preventDefault(); - setIsDragging(false); - - if (e.dataTransfer.files && e.dataTransfer.files.length > 0) { - const file = e.dataTransfer.files[0]; - validateAndSetFile(file); - } - }; - - const handleCreateProject = async () => { - if (!newProjectName.trim()) { - setUploadError('Please enter a project name'); - return; - } - - try { - const result = await projectService.createProject({ title: newProjectName.trim() }); - - if (!result.success) { - throw new Error(result.error || 'Failed to create project'); - } - - const updatedUser = await userService.get(); - if (updatedUser) { - setUser(updatedUser); - - const newProject = updatedUser.projects.find((p: any) => p.title === newProjectName.trim()); - setSelectedProject(newProject); - setNewProjectName(''); - setUploadError(''); - } - } catch (error: any) { - setUploadError(error.message || 'Failed to create project'); - } - }; - - const handleUploadDocument = async () => { - if (uploadFiles.length === 0) { - setUploadError('Please select a file to upload'); - return; - } - - setIsUploading(true); - setUploadError(''); - - try { - let targetProject = selectedProject; - - // Auto-create project if none selected - if (!targetProject) { - const fileName = uploadFiles[0].name; - const projectName = fileName.substring(0, fileName.lastIndexOf('.')) || fileName; - - const result = await projectService.createProject({ title: projectName.trim() }); - - if (!result.success) { - throw new Error(result.error || 'Failed to create project'); - } - - const updatedUser = await userService.get(); - if (updatedUser) { - setUser(updatedUser); - const newProject = updatedUser.projects.find((p: any) => p.title === projectName.trim()); - if (newProject) { - targetProject = newProject; - setSelectedProject(newProject); - } else { - throw new Error('Failed to find created project'); - } - } - } if (!targetProject) { - throw new Error('No project available for upload'); - } - - const formData = new FormData(); - formData.append('file', uploadFiles[0]); - - const url = `${API_ENDPOINT}/projects/${targetProject.id}/documents`; - const response = await fetch(url, { - method: 'PUT', - headers: { - 'Authorization': `Bearer ${userService.token()}` - }, - body: formData - }); - - if (!response.ok) { - const result = await response.json(); - throw new Error(result.detail || result.message || 'Upload failed'); - } - - const updatedUser = await userService.get(); - if (updatedUser) { - setUser(updatedUser); - } - - // Dispatch custom event to notify other components that a document was uploaded - window.dispatchEvent(new CustomEvent('documentUploaded', { - detail: { projectId: targetProject.id } - })); - - handleClose(); - onSuccess(); - } catch (error: any) { - setUploadError(error.message || 'Failed to upload document'); - } finally { - setIsUploading(false); - } - }; - - return ( - - - {/* Close Button */} - - - - - {/* Title */} -

- Add New Document -

- - - {/* Project Selection */} - {!disableProjectSelection && ( - - - Select or Create Project - - - {!showCreateNew ? ( - - { - if (newValue && typeof newValue === 'object') { - setSelectedProject(newValue); - setProjectInputValue(newValue.title || newValue.name); - } else { - setSelectedProject(null); - setProjectInputValue(''); - } - }} - inputValue={projectInputValue} - onInputChange={(event, newInputValue) => { - setProjectInputValue(newInputValue); - }} - options={user?.projects || []} - getOptionLabel={(option) => option.title || option.name} - renderOption={(props, option) => ( - - - {option.title || option.name} - - )} - renderInput={(params) => ( - - )} - sx={{ - '& .MuiAutocomplete-paper': { - bgcolor: 'var(--border)', - color: 'var(--content)', - }, - '& .MuiAutocomplete-listbox': { - bgcolor: 'var(--border)', - '& .MuiAutocomplete-option': { - color: 'var(--content)', - '&:hover': { - bgcolor: 'var(--border-2)', - }, - '&[aria-selected="true"]': { - bgcolor: 'var(--border-2)', - }, - }, - }, - }} - ListboxProps={{ - sx: { - bgcolor: 'var(--border)', - '& .MuiAutocomplete-option': { - color: 'var(--content)', - '&:hover': { - bgcolor: 'var(--border-2)', - }, - '&[aria-selected="true"]': { - bgcolor: 'var(--border-2)', - }, - }, - }, - }} - PaperComponent={({ children }) => ( - - {children} - - )} - /> - - {!disableProjectSelection && ( - - - - )} - - ) : ( - - - setNewProjectName(e.target.value)} - fullWidth - size="small" - autoFocus - sx={{ - '& .MuiOutlinedInput-root': { - color: 'var(--content)', - bgcolor: '#000', - borderRadius: '8px', - '& fieldset': { - borderColor: 'var(--border-2)', - }, - '&:hover fieldset': { - borderColor: 'var(--border-2)', - }, - '&.Mui-focused fieldset': { - borderColor: '#ec4899', - }, - }, - '& .MuiInputBase-input::placeholder': { - color: 'var(--faint)', - opacity: 1, - }, - }} - /> - - - - - )} - - {selectedProject && ( - - - - Selected: {selectedProject.title || selectedProject.name} - - - )} - - )} - - {/* File Upload */} - - - Upload Document - - - {uploadFiles.length === 0 ? ( - document.getElementById('file-upload-input')?.click()} - > - - - Drag and drop your file here - - - or click to browse - - - Supported: PDF, DOCX, TXT, CSV (Max 50MB) - - - - ) : ( - - - - - {uploadFiles[0].name} - - {(uploadFiles[0].size / 1024 / 1024).toFixed(2)} MB - - - - setUploadFiles([])} - size="small" - sx={{ color: '#ef4444' }} - > - - - - )} - - - {/* Error Message */} - {uploadError && ( - - - {uploadError} - - - )} - - {/* Action Buttons */} - - - - - -
-
- ); -} diff --git a/src/app/invite-verify/page.tsx b/src/app/invite-verify/page.tsx index 718fe5b..8b74308 100644 --- a/src/app/invite-verify/page.tsx +++ b/src/app/invite-verify/page.tsx @@ -45,7 +45,7 @@ function InviteVerifyContent() { setSuccessMessage(""); setIsLoading(true); - const url = `${API_ENDPOINT}/invites/request`; + const url = `${API_ENDPOINT}/invites`; try { const response = await userService.makeAuthenticatedRequest(url, { diff --git a/src/app/services/project.service.ts b/src/app/services/project.service.ts deleted file mode 100644 index d376aec..0000000 --- a/src/app/services/project.service.ts +++ /dev/null @@ -1,253 +0,0 @@ -import { API_ENDPOINT } from "@/app/constants"; -import { userService } from "./user.service"; - -/** - * Project Service - * Handles project creation, deletion, and related cleanup operations - */ - -interface CreateProjectParams { - title: string; - temperature?: number; -} - -interface ProjectServiceResponse { - success: boolean; - data?: any; - error?: string; -} - -/** - * Create a new project - * @param params - Project creation parameters (title, temperature) - * @returns Promise with project data or error - */ -const createProject = async (params: CreateProjectParams): Promise => { - try { - const { title, temperature = 0.3 } = params; - - if (!title || !title.trim()) { - return { - success: false, - error: 'Project title is required' - }; - } - - const url = `${API_ENDPOINT}/projects`; - const response = await userService.makeAuthenticatedRequest(url, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - title: title.trim(), - temperature - }) - }); - - if (!response) { - return { - success: false, - error: 'No response from server' - }; - } - - if (response.status === 200 || response.status === 201) { - const project = await response.json(); - return { - success: true, - data: project - }; - } - - const errorData = await response.json().catch(() => ({})); - return { - success: false, - error: errorData.detail || errorData.message || 'Failed to create project' - }; - } catch (error: any) { - console.error('Error creating project:', error); - return { - success: false, - error: error.message || 'An error occurred while creating the project' - }; - } -}; - -/** - * Delete a project and clean up associated data - * @param projectId - ID of the project to delete - * @param userId - User ID (email) for cleaning up chat history - * @returns Promise with success status - */ -const deleteProject = async (projectId: string, userId?: string): Promise => { - try { - if (!projectId) { - return { - success: false, - error: 'Project ID is required' - }; - } - - // Delete the project via API - const url = `${API_ENDPOINT}/projects/${projectId}`; - const response = await userService.makeAuthenticatedRequest(url, { - method: 'DELETE' - }); - - if (!response) { - return { - success: false, - error: 'No response from server' - }; - } - - if (response.status === 200 || response.status === 204) { - // Clean up local storage data associated with this project - cleanupProjectData(projectId, userId); - - return { - success: true, - data: { projectId } - }; - } - - const errorData = await response.json().catch(() => ({})); - return { - success: false, - error: errorData.detail || errorData.message || 'Failed to delete project' - }; - } catch (error: any) { - console.error('Error deleting project:', error); - return { - success: false, - error: error.message || 'An error occurred while deleting the project' - }; - } -}; - -/** - * Clean up localStorage and sessionStorage data for a deleted project - * @param projectId - ID of the deleted project - * @param userId - User ID (email) for identifying chat history keys - */ -const cleanupProjectData = (projectId: string, userId?: string): void => { - if (typeof window === 'undefined') return; - - try { - // Clean up chat history from localStorage - if (userId) { - const chatKey = `chat_${userId}_${projectId}`; - localStorage.removeItem(chatKey); - } - - // Clean up any session storage related to this project - const sessionKey = `project_session_${projectId}`; - sessionStorage.removeItem(sessionKey); - - // If we don't have userId, try to find and remove any chat keys with this projectId - if (!userId) { - const keysToRemove: string[] = []; - for (let i = 0; i < localStorage.length; i++) { - const key = localStorage.key(i); - if (key && key.includes(`_${projectId}`)) { - keysToRemove.push(key); - } - } - keysToRemove.forEach(key => localStorage.removeItem(key)); - } - - console.log(`Cleaned up data for project: ${projectId}`); - } catch (error) { - console.error('Error cleaning up project data:', error); - } -}; - -/** - * Get all projects for the current user - * @returns Promise with user data containing projects - */ -const getProjects = async (): Promise => { - try { - const user = await userService.get(); - - if (!user) { - return { - success: false, - error: 'Failed to fetch user data' - }; - } - - return { - success: true, - data: user.projects || [] - }; - } catch (error: any) { - console.error('Error fetching projects:', error); - return { - success: false, - error: error.message || 'An error occurred while fetching projects' - }; - } -}; - -/** - * Update project details - * @param projectId - ID of the project to update - * @param updates - Fields to update (title, temperature, etc.) - * @returns Promise with updated project data - */ -const updateProject = async (projectId: string, updates: Partial): Promise => { - try { - if (!projectId) { - return { - success: false, - error: 'Project ID is required' - }; - } - - const url = `${API_ENDPOINT}/projects/${projectId}`; - const response = await userService.makeAuthenticatedRequest(url, { - method: 'PATCH', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(updates) - }); - - if (!response) { - return { - success: false, - error: 'No response from server' - }; - } - - if (response.status === 200) { - const project = await response.json(); - return { - success: true, - data: project - }; - } - - const errorData = await response.json().catch(() => ({})); - return { - success: false, - error: errorData.detail || errorData.message || 'Failed to update project' - }; - } catch (error: any) { - console.error('Error updating project:', error); - return { - success: false, - error: error.message || 'An error occurred while updating the project' - }; - } -}; - -export const projectService = { - createProject, - deleteProject, - getProjects, - updateProject, - cleanupProjectData -};