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 += `${header} `;
- });
- 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 += `${cell} `;
- });
- tableHtml += ' ';
- }
- tableHtml += ' ';
-
- tableHtml += '
';
- 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);
- }}>
-
-
- {copied === msg.id ? "Copied!" : "Copy"}
-
-
- sendPositiveFeedback(msg)}
- >
- {
- msg.feedback === 1 ?
-
- :
- }
-
- sendNegativeFeedback(msg)}
- >
- {
- msg.feedback === 2 ?
-
- :
- }
-
-
-
-
- :
-
-
-
-
-
-
-
- }
-
- >
- )
- })
- }
-
-
-
-
-
-
-
-
-
- ) : 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',
- },
- }}
- />
-
-
-
- {isLoading ? (
-
- ) : (
-
- )}
-
-
-
-
-
-
-
-
- >
- )
-}
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 && (
-
- setShowCreateNew(true)}
- className="text-pink-500 hover:text-pink-400 text-xs underline"
- >
- + Create New Project
-
-
- )}
-
- ) : (
-
-
- 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,
- },
- }}
- />
-
- Create
-
-
- {
- setShowCreateNew(false);
- setNewProjectName('');
- }}
- className="text-muted hover:text-content text-xs underline"
- >
- ← Back to project list
-
-
- )}
-
- {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 */}
-
-
- Cancel
-
-
- {isUploading ? (
-
- ) : (
- <>
-
- Upload Document
- >
- )}
-
-
-
-
-
- );
-}
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
-};