From 1eb339623ce894f8cda9dfa62063c6b295617c2d Mon Sep 17 00:00:00 2001 From: rathi Date: Thu, 5 Dec 2024 17:05:20 -0500 Subject: [PATCH] Working frontend interface with supabse storage --- package-lock.json | 148 +++++++++ package.json | 3 +- src/App.tsx | 13 +- src/components/layout/AuthLayout/index.tsx | 8 +- .../layout/DashboardLayout/index.tsx | 10 +- src/components/layout/Header/index.tsx | 41 ++- src/components/layout/Sidebar/index.tsx | 35 +- src/contexts/AuthContext.tsx | 72 ++++ src/lib/chat-service.ts | 143 ++++++++ src/lib/supabase.ts | 44 +++ src/pages/Home.tsx | 98 +++--- src/pages/auth/Login.tsx | 168 ++++++---- src/pages/auth/Signup.tsx | 193 ++++++----- src/pages/dashboard/ask.tsx | 314 +++++++++++++++--- src/pages/dashboard/history.tsx | 149 +++++---- src/pages/dashboard/index.tsx | 73 ---- src/routes/PrivateRoute.tsx | 20 +- src/routes/index.tsx | 4 +- src/types/supabase.ts | 36 ++ 19 files changed, 1150 insertions(+), 422 deletions(-) create mode 100644 src/contexts/AuthContext.tsx create mode 100644 src/lib/chat-service.ts create mode 100644 src/lib/supabase.ts delete mode 100644 src/pages/dashboard/index.tsx create mode 100644 src/types/supabase.ts diff --git a/package-lock.json b/package-lock.json index aa5bbb6..66c9345 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "@radix-ui/react-separator": "^1.0.3", "@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-tabs": "^1.0.4", + "@supabase/supabase-js": "^2.47.0", "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", "lucide-react": "^0.344.0", @@ -2028,6 +2029,80 @@ "win32" ] }, + "node_modules/@supabase/auth-js": { + "version": "2.65.1", + "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.65.1.tgz", + "integrity": "sha512-IA7i2Xq2SWNCNMKxwmPlHafBQda0qtnFr8QnyyBr+KaSxoXXqEzFCnQ1dGTy6bsZjVBgXu++o3qrDypTspaAPw==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/functions-js": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.4.3.tgz", + "integrity": "sha512-sOLXy+mWRyu4LLv1onYydq+10mNRQ4rzqQxNhbrKLTLTcdcmS9hbWif0bGz/NavmiQfPs4ZcmQJp4WqOXlR4AQ==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/node-fetch": { + "version": "2.6.15", + "resolved": "https://registry.npmjs.org/@supabase/node-fetch/-/node-fetch-2.6.15.tgz", + "integrity": "sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + } + }, + "node_modules/@supabase/postgrest-js": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-1.16.3.tgz", + "integrity": "sha512-HI6dsbW68AKlOPofUjDTaosiDBCtW4XAm0D18pPwxoW3zKOE2Ru13Z69Wuys9fd6iTpfDViNco5sgrtnP0666A==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/realtime-js": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.11.2.tgz", + "integrity": "sha512-u/XeuL2Y0QEhXSoIPZZwR6wMXgB+RQbJzG9VErA3VghVt7uRfSVsjeqd7m5GhX3JR6dM/WRmLbVR8URpDWG4+w==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "^2.6.14", + "@types/phoenix": "^1.5.4", + "@types/ws": "^8.5.10", + "ws": "^8.18.0" + } + }, + "node_modules/@supabase/storage-js": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.7.1.tgz", + "integrity": "sha512-asYHcyDR1fKqrMpytAS1zjyEfvxuOIp1CIXX7ji4lHHcJKqyk+sLl/Vxgm4sN6u8zvuUtae9e4kDxQP2qrwWBA==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/supabase-js": { + "version": "2.47.0", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.47.0.tgz", + "integrity": "sha512-SvLKFI21m3b1NAbc8nvZbOhHihUtujhUaR5TqYxdt52ag6moUJnQNVt0l0Upw3z0BMuG/YOIuI3hMzuo1EqUEw==", + "license": "MIT", + "dependencies": { + "@supabase/auth-js": "2.65.1", + "@supabase/functions-js": "2.4.3", + "@supabase/node-fetch": "2.6.15", + "@supabase/postgrest-js": "1.16.3", + "@supabase/realtime-js": "2.11.2", + "@supabase/storage-js": "2.7.1" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -2081,6 +2156,21 @@ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, + "node_modules/@types/node": { + "version": "22.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.1.tgz", + "integrity": "sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "node_modules/@types/phoenix": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.6.tgz", + "integrity": "sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==", + "license": "MIT" + }, "node_modules/@types/prop-types": { "version": "15.7.13", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz", @@ -2106,6 +2196,15 @@ "@types/react": "*" } }, + "node_modules/@types/ws": { + "version": "8.5.13", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.13.tgz", + "integrity": "sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.8.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.8.1.tgz", @@ -4684,6 +4783,12 @@ "node": ">=8.0" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, "node_modules/ts-api-utils": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", @@ -4755,6 +4860,12 @@ } } }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "license": "MIT" + }, "node_modules/update-browserslist-db": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", @@ -4900,6 +5011,22 @@ } } }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -5045,6 +5172,27 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/package.json b/package.json index 72d66f3..b72e14d 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "@radix-ui/react-separator": "^1.0.3", "@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-tabs": "^1.0.4", + "@supabase/supabase-js": "^2.47.0", "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", "lucide-react": "^0.344.0", @@ -43,4 +44,4 @@ "typescript-eslint": "^8.3.0", "vite": "^5.4.2" } -} \ No newline at end of file +} diff --git a/src/App.tsx b/src/App.tsx index 68f8713..9058e19 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,15 +1,18 @@ import React from 'react'; import { BrowserRouter } from 'react-router-dom'; import { AppRouter } from './routes'; +import { AuthProvider } from './contexts/AuthContext'; import './index.css'; const App: React.FC = () => { return ( - -
- -
-
+ + +
+ +
+
+
); }; diff --git a/src/components/layout/AuthLayout/index.tsx b/src/components/layout/AuthLayout/index.tsx index 9cc0e2f..0fefe7c 100644 --- a/src/components/layout/AuthLayout/index.tsx +++ b/src/components/layout/AuthLayout/index.tsx @@ -4,10 +4,12 @@ import { Header } from '../Header'; const AuthLayout: React.FC = () => { return ( -
+
-
- +
+
+ +
); diff --git a/src/components/layout/DashboardLayout/index.tsx b/src/components/layout/DashboardLayout/index.tsx index 57939b8..28dd180 100644 --- a/src/components/layout/DashboardLayout/index.tsx +++ b/src/components/layout/DashboardLayout/index.tsx @@ -1,13 +1,13 @@ import React from 'react'; import { Outlet } from 'react-router-dom'; -import { Header } from '../Header'; -import { Sidebar } from '../Sidebar'; +import { Header } from '../Header/index'; +import { Sidebar } from '../Sidebar/index'; -const DashboardLayout: React.FC = () => { +const DashboardLayout = () => { return ( -
+
-
+
diff --git a/src/components/layout/Header/index.tsx b/src/components/layout/Header/index.tsx index ccd279e..c7a75ce 100644 --- a/src/components/layout/Header/index.tsx +++ b/src/components/layout/Header/index.tsx @@ -2,8 +2,15 @@ import React from 'react'; import { BookOpen } from 'lucide-react'; import { Link } from 'react-router-dom'; import { Button } from '../../ui/Button'; +import { useAuth } from '../../../contexts/AuthContext'; + +const Header = () => { + const { session, signOut } = useAuth(); + + const handleSignOut = async () => { + await signOut(); + }; -export function Header() { return (
@@ -12,17 +19,29 @@ export function Header() { StudyAI
); -} +}; + +export { Header }; diff --git a/src/components/layout/Sidebar/index.tsx b/src/components/layout/Sidebar/index.tsx index e3dc041..80c8524 100644 --- a/src/components/layout/Sidebar/index.tsx +++ b/src/components/layout/Sidebar/index.tsx @@ -5,7 +5,6 @@ import { MessageSquarePlus, Upload, BookOpen, - GraduationCap, History } from 'lucide-react'; import { cn } from '../../../lib/utils'; @@ -15,6 +14,11 @@ const sidebarItems = [ icon: MessageSquarePlus, label: 'Ask a Question', path: '/dashboard/ask' + }, + { + icon: History, + label: 'Study History', + path: '/dashboard/history' }, { icon: Upload, @@ -26,11 +30,7 @@ const sidebarItems = [ label: 'Study Resources', path: '/dashboard/resources' }, - { - icon: History, - label: 'Study History', - path: '/dashboard/history' - }, + { icon: Settings, label: 'Settings', @@ -38,19 +38,22 @@ const sidebarItems = [ }, ]; -export function Sidebar() { +const Sidebar = () => { const location = useLocation(); + const isActive = (path: string) => { + if (path === '/dashboard/ask') { + return location.pathname === '/dashboard' || location.pathname === path; + } + return location.pathname === path; + }; + return ( -
-
- - Study Dashboard -
+
); -} +}; + +export { Sidebar }; diff --git a/src/contexts/AuthContext.tsx b/src/contexts/AuthContext.tsx new file mode 100644 index 0000000..edc1736 --- /dev/null +++ b/src/contexts/AuthContext.tsx @@ -0,0 +1,72 @@ +import React, { createContext, useContext, useEffect, useState } from 'react'; +import { Session, User, AuthError } from '@supabase/supabase-js'; +import { auth } from '../lib/supabase'; + +interface AuthContextType { + session: Session | null; + user: User | null; + loading: boolean; + signIn: (email: string, password: string) => Promise<{ error: AuthError | null }>; + signUp: (email: string, password: string) => Promise<{ error: AuthError | null }>; + signOut: () => Promise<{ error: AuthError | null }>; +} + +const AuthContext = createContext(undefined); + +export function AuthProvider({ children }: { children: React.ReactNode }) { + const [session, setSession] = useState(null); + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + // Get initial session + auth.getSession().then(({ session: initialSession }) => { + setSession(initialSession); + setUser(initialSession?.user ?? null); + setLoading(false); + }); + + // Listen for auth changes + const { data: { subscription } } = auth.onAuthStateChange((_event, session) => { + setSession(session); + setUser(session?.user ?? null); + setLoading(false); + }); + + return () => { + subscription.unsubscribe(); + }; + }, []); + + const value = { + session, + user, + loading, + signIn: async (email: string, password: string) => { + const { error } = await auth.signIn(email, password); + return { error }; + }, + signUp: async (email: string, password: string) => { + const { error } = await auth.signUp(email, password); + return { error }; + }, + signOut: async () => { + const { error } = await auth.signOut(); + return { error }; + }, + }; + + return ( + + {!loading && children} + + ); +} + +export function useAuth() { + const context = useContext(AuthContext); + if (context === undefined) { + throw new Error('useAuth must be used within an AuthProvider'); + } + return context; +} diff --git a/src/lib/chat-service.ts b/src/lib/chat-service.ts new file mode 100644 index 0000000..c018361 --- /dev/null +++ b/src/lib/chat-service.ts @@ -0,0 +1,143 @@ +import { supabase } from './supabase'; +import type { ChatInstance, ChatMessage } from '../types/supabase'; + +export const chatService = { + async createChatInstance(userId: string, title: string): Promise { + const { data, error } = await supabase + .from('chat_instances') + .insert({ + user_id: userId, + title, + last_message_at: new Date().toISOString(), + }) + .select() + .single(); + + if (error) { + console.error('Error creating chat instance:', error); + return null; + } + + return data; + }, + + async getChatInstance(chatId: string): Promise { + const { data, error } = await supabase + .from('chat_instances') + .select() + .eq('id', chatId) + .single(); + + if (error) { + console.error('Error fetching chat instance:', error); + return null; + } + + return data; + }, + + async updateChatTitle(chatId: string, title: string): Promise { + const { error } = await supabase + .from('chat_instances') + .update({ title }) + .eq('id', chatId); + + if (error) { + console.error('Error updating chat title:', error); + return false; + } + + return true; + }, + + async getChatInstances(userId: string): Promise { + const { data, error } = await supabase + .from('chat_instances') + .select() + .eq('user_id', userId) + .order('last_message_at', { ascending: false }); + + if (error) { + console.error('Error fetching chat instances:', error); + return []; + } + + return data; + }, + + async getChatMessages(chatId: string): Promise { + const { data, error } = await supabase + .from('chat_messages') + .select() + .eq('chat_id', chatId) + .order('created_at', { ascending: true }); + + if (error) { + console.error('Error fetching chat messages:', error); + return []; + } + + return data; + }, + + async addMessage( + chatId: string, + content: string, + role: 'user' | 'assistant', + metadata?: ChatMessage['metadata'] + ): Promise { + const { data: message, error: messageError } = await supabase + .from('chat_messages') + .insert({ + chat_id: chatId, + content, + role, + metadata, + }) + .select() + .single(); + + if (messageError) { + console.error('Error adding message:', messageError); + return null; + } + + // Update last_message_at in chat instance + const { error: updateError } = await supabase + .from('chat_instances') + .update({ last_message_at: new Date().toISOString() }) + .eq('id', chatId); + + if (updateError) { + console.error('Error updating chat instance:', updateError); + } + + return message; + }, + + async deleteChatInstance(chatId: string): Promise { + // Delete all messages first + const { error: messagesError } = await supabase + .from('chat_messages') + .delete() + .eq('chat_id', chatId); + + if (messagesError) { + console.error('Error deleting chat messages:', messagesError); + return false; + } + + // Then delete the chat instance + const { error: instanceError } = await supabase + .from('chat_instances') + .delete() + .eq('id', chatId); + + if (instanceError) { + console.error('Error deleting chat instance:', instanceError); + return false; + } + + return true; + }, +}; diff --git a/src/lib/supabase.ts b/src/lib/supabase.ts new file mode 100644 index 0000000..d62c64a --- /dev/null +++ b/src/lib/supabase.ts @@ -0,0 +1,44 @@ +import { createClient, AuthError, Session, AuthResponse } from '@supabase/supabase-js'; +import type { Database } from '../types/supabase'; + +const supabaseUrl = import.meta.env.VITE_SUPABASE_URL; +const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY; + +if (!supabaseUrl || !supabaseAnonKey) { + throw new Error('Missing Supabase environment variables'); +} + +export const supabase = createClient(supabaseUrl, supabaseAnonKey); + +// Auth helper functions +export const auth = { + signUp: async (email: string, password: string): Promise<{ data: AuthResponse | null; error: AuthError | null }> => { + const { data, error } = await supabase.auth.signUp({ + email, + password, + }); + return { data: data as AuthResponse | null, error }; + }, + + signIn: async (email: string, password: string): Promise<{ data: AuthResponse | null; error: AuthError | null }> => { + const { data, error } = await supabase.auth.signInWithPassword({ + email, + password, + }); + return { data: data as AuthResponse | null, error }; + }, + + signOut: async (): Promise<{ error: AuthError | null }> => { + const { error } = await supabase.auth.signOut(); + return { error }; + }, + + getSession: async (): Promise<{ session: Session | null; error: AuthError | null }> => { + const { data: { session }, error } = await supabase.auth.getSession(); + return { session, error }; + }, + + onAuthStateChange: (callback: (event: string, session: Session | null) => void) => { + return supabase.auth.onAuthStateChange(callback); + }, +}; diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index 8d3f599..cfdd69b 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -1,69 +1,53 @@ import React from 'react'; -import { ArrowRight, Brain, Sparkles, Users } from 'lucide-react'; +import { ArrowRight, Brain, Sparkles, Users, BookOpen } from 'lucide-react'; import { Link } from 'react-router-dom'; import { Button } from '../components/ui/Button'; import { Header } from '../components/layout/Header'; function Home() { return ( -
-
-
-
-
-

- Your AI Study Buddy -

-

- Get instant help with your coursework using advanced AI. Upload your materials, - ask questions, and receive detailed explanations. -

-
- - - -
-
-
+
+
+
+ + + StudyAI + + +
+
-
-
-

Why Choose StudyAI?

-
- {[ - { - icon: Brain, - title: 'Smart Learning', - description: - 'Our AI understands your questions and provides detailed, accurate answers.', - }, - { - icon: Sparkles, - title: 'Instant Help', - description: - 'Get immediate assistance with your coursework, available 24/7.', - }, - { - icon: Users, - title: 'Personalized Experience', - description: - 'The more you use StudyAI, the better it understands your learning style.', - }, - ].map((feature) => ( -
- -

{feature.title}

-

{feature.description}

-
- ))} +
+
+
+
+

+ Learn Smarter with AI +

+

+ Enhance your learning experience with personalized AI assistance. + Ask questions, get instant feedback, and track your progress. +

+
+ + + + + + +
-
-
+ +
); diff --git a/src/pages/auth/Login.tsx b/src/pages/auth/Login.tsx index ca118f3..e745e80 100644 --- a/src/pages/auth/Login.tsx +++ b/src/pages/auth/Login.tsx @@ -1,76 +1,116 @@ import React, { useState } from 'react'; -import { Link } from 'react-router-dom'; +import { Link, useNavigate, useLocation } from 'react-router-dom'; import { Button } from '../../components/ui/Button'; +import { useAuth } from '../../contexts/AuthContext'; -function Login() { - const [email, setEmail] = useState(''); - const [password, setPassword] = useState(''); +interface LocationState { + from?: string; + message?: string; +} - const handleSubmit = (e: React.FormEvent) => { +export default function Login() { + const navigate = useNavigate(); + const location = useLocation(); + const { signIn } = useAuth(); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [message, setMessage] = useState( + (location.state as LocationState)?.message || null + ); + const [formData, setFormData] = useState({ + email: '', + password: '', + }); + + const from = (location.state as LocationState)?.from || '/dashboard'; + + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - // Handle login + setError(null); + setMessage(null); + setLoading(true); + + try { + const { error } = await signIn(formData.email, formData.password); + if (error) throw error; + navigate(from, { replace: true }); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to sign in'); + } finally { + setLoading(false); + } + }; + + const handleChange = (e: React.ChangeEvent) => { + setFormData(prev => ({ + ...prev, + [e.target.name]: e.target.value + })); }; return ( -
-
-
-

- Sign in to your account -

-
-
-
-
- - setEmail(e.target.value)} - className="relative block w-full rounded-md border-0 py-1.5 px-3 bg-background text-foreground shadow-sm ring-1 ring-inset ring-input placeholder:text-muted-foreground focus:ring-2 focus:ring-inset focus:ring-primary sm:text-sm sm:leading-6" - placeholder="Email address" - /> -
-
- - setPassword(e.target.value)} - className="relative block w-full rounded-md border-0 py-1.5 px-3 bg-background text-foreground shadow-sm ring-1 ring-inset ring-input placeholder:text-muted-foreground focus:ring-2 focus:ring-inset focus:ring-primary sm:text-sm sm:leading-6" - placeholder="Password" - /> -
-
- -
- -
-
- -

- Don't have an account?{' '} - - Sign up - -

+
+
+

Welcome Back

+

Enter your credentials to access your account

+ + {message && ( +
+ {message} +
+ )} + + {error && ( +
+ {error} +
+ )} + +
+
+ + +
+ +
+ + +
+ + +
+ +

+ Don't have an account?{' '} + + Sign up + +

); } - -export default Login; diff --git a/src/pages/auth/Signup.tsx b/src/pages/auth/Signup.tsx index 5d1c0f2..1a57eff 100644 --- a/src/pages/auth/Signup.tsx +++ b/src/pages/auth/Signup.tsx @@ -1,92 +1,125 @@ import React, { useState } from 'react'; -import { Link } from 'react-router-dom'; +import { Link, useNavigate } from 'react-router-dom'; import { Button } from '../../components/ui/Button'; +import { useAuth } from '../../contexts/AuthContext'; -function Signup() { - const [email, setEmail] = useState(''); - const [password, setPassword] = useState(''); - const [name, setName] = useState(''); +export default function Signup() { + const navigate = useNavigate(); + const { signUp } = useAuth(); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [formData, setFormData] = useState({ + email: '', + password: '', + confirmPassword: '', + }); - const handleSubmit = (e: React.FormEvent) => { + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - // Handle signup + setError(null); + + if (formData.password !== formData.confirmPassword) { + setError("Passwords don't match"); + return; + } + + setLoading(true); + + try { + const { error } = await signUp(formData.email, formData.password); + if (error) throw error; + + // Redirect to login page with a success message + navigate('/auth/login', { + state: { message: 'Please check your email to confirm your account' } + }); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to create account'); + } finally { + setLoading(false); + } + }; + + const handleChange = (e: React.ChangeEvent) => { + setFormData(prev => ({ + ...prev, + [e.target.name]: e.target.value + })); }; return ( -
-
-
-

- Create your account -

-
-
-
-
- - setName(e.target.value)} - className="relative block w-full rounded-md border-0 py-1.5 px-3 bg-background text-foreground shadow-sm ring-1 ring-inset ring-input placeholder:text-muted-foreground focus:ring-2 focus:ring-inset focus:ring-primary sm:text-sm sm:leading-6" - placeholder="Full name" - /> -
-
- - setEmail(e.target.value)} - className="relative block w-full rounded-md border-0 py-1.5 px-3 bg-background text-foreground shadow-sm ring-1 ring-inset ring-input placeholder:text-muted-foreground focus:ring-2 focus:ring-inset focus:ring-primary sm:text-sm sm:leading-6" - placeholder="Email address" - /> -
-
- - setPassword(e.target.value)} - className="relative block w-full rounded-md border-0 py-1.5 px-3 bg-background text-foreground shadow-sm ring-1 ring-inset ring-input placeholder:text-muted-foreground focus:ring-2 focus:ring-inset focus:ring-primary sm:text-sm sm:leading-6" - placeholder="Password" - /> -
-
- -
- -
-
- -

- Already have an account?{' '} - - Sign in - -

+
+
+

Create an Account

+

Enter your details to create your account

+ + {error && ( +
+ {error} +
+ )} + +
+
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+ +

+ Already have an account?{' '} + + Sign in + +

); } - -export default Signup; diff --git a/src/pages/dashboard/ask.tsx b/src/pages/dashboard/ask.tsx index 60f801d..8d0aee8 100644 --- a/src/pages/dashboard/ask.tsx +++ b/src/pages/dashboard/ask.tsx @@ -1,53 +1,291 @@ -import React, { useState } from 'react'; -import { Send, Upload } from 'lucide-react'; +import React, { useState, useEffect } from 'react'; +import { Send, Loader2, X, History, MessageSquarePlus, AlertCircle } from 'lucide-react'; +import { Link, useParams, useNavigate } from 'react-router-dom'; import { Button } from '../../components/ui/Button'; +import { cn } from '../../lib/utils'; +import { useAuth } from '../../contexts/AuthContext'; +import { chatService } from '../../lib/chat-service'; +import type { ChatMessage, ChatInstance } from '../../types/supabase'; -function AskQuestion() { +export default function AskQuestion() { + const { chatId } = useParams(); + const navigate = useNavigate(); + const { user } = useAuth(); const [question, setQuestion] = useState(''); + const [loading, setLoading] = useState(false); + const [messages, setMessages] = useState([]); + const [error, setError] = useState(null); + const [chat, setChat] = useState(null); + const [isNewChat, setIsNewChat] = useState(false); + const [hasExistingChats, setHasExistingChats] = useState(null); - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault(); - // Handle question submission + // Check for existing chats and redirect to most recent if on base path + useEffect(() => { + if (!user) return; + + const checkExistingChats = async () => { + try { + const chats = await chatService.getChatInstances(user.id); + const hasChats = chats.length > 0; + setHasExistingChats(hasChats); + + // If we're on the base ask page and there are existing chats, + // redirect to the most recent one + if (hasChats && !chatId && !isNewChat) { + const mostRecentChat = chats[0]; // Chats are already sorted by last_message_at desc + navigate(`/dashboard/ask/${mostRecentChat.id}`); + } + } catch (err) { + console.error('Error checking existing chats:', err); + setHasExistingChats(false); + } + }; + + checkExistingChats(); + }, [user, chatId, navigate, isNewChat]); + + useEffect(() => { + if (!user || !chatId) return; + + const loadChat = async () => { + try { + // Don't try to load chat if we're using a temporary ID + if (chatId.startsWith('temp-')) { + return; + } + + // Load chat instance + const chatInstance = await chatService.getChatInstance(chatId); + if (!chatInstance) { + setError('Chat not found'); + return; + } + setChat(chatInstance); + + // Load messages + const messages = await chatService.getChatMessages(chatId); + setMessages(messages); + } catch (err) { + setError('Failed to load chat'); + console.error('Error loading chat:', err); + } + }; + + loadChat(); + }, [chatId, user]); + + const handleNewChat = () => { + setIsNewChat(true); + setChat(null); + setMessages([]); + setError(null); + // Generate a temporary ID for the new chat + const tempId = 'temp-' + Date.now(); + navigate(`/dashboard/ask/${tempId}`); }; + const generateChatTitle = (message: string) => { + // Split into words and take first 5 + const words = message.split(/\s+/).slice(0, 5); + + // Join words and add ellipsis if we truncated + const title = words.join(' '); + return words.length < message.split(/\s+/).length ? `${title}...` : title; + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!question.trim() || loading || !user) return; + + setLoading(true); + setError(null); + + try { + let currentChatId: string; + + // If this is a new chat, create it first with the title from the first message + if (isNewChat) { + const title = generateChatTitle(question.trim()); + const newChat = await chatService.createChatInstance(user.id, title); + if (!newChat) { + throw new Error('Failed to create chat instance'); + } + currentChatId = newChat.id; + setChat(newChat); + setIsNewChat(false); + // Update URL with the real chat ID + navigate(`/dashboard/ask/${newChat.id}`, { replace: true }); + } else if (!chatId) { + throw new Error('No chat ID available'); + } else { + currentChatId = chatId; + } + + // Add user message + const userMessage = await chatService.addMessage(currentChatId, question.trim(), 'user'); + if (userMessage) { + setMessages(prev => [...prev, userMessage]); + } + + setQuestion(''); + + // TODO: Integrate with Make.com for AI response + // For now, using a placeholder response + const aiMessage = await chatService.addMessage( + currentChatId, + 'This is a placeholder response. AI integration coming soon!', + 'assistant', + { make_response_id: 'placeholder' } + ); + if (aiMessage) { + setMessages(prev => [...prev, aiMessage]); + } + } catch (err) { + setError('Failed to send message'); + console.error('Failed to process message:', err); + } finally { + setLoading(false); + } + }; + + const clearChat = async () => { + if (!chatId || isNewChat) return; + + try { + setError(null); + const success = await chatService.deleteChatInstance(chatId); + if (success) { + // After deleting, check if there are other chats to navigate to + const remainingChats = await chatService.getChatInstances(user!.id); + if (remainingChats.length > 0) { + navigate(`/dashboard/ask/${remainingChats[0].id}`); + } else { + navigate('/dashboard/ask'); + } + } + } catch (err) { + setError('Failed to clear chat'); + console.error('Error clearing chat:', err); + } + }; + + // Show loading state while checking for existing chats + if (hasExistingChats === null) { + return ( +
+ +
+ ); + } + return ( -
-

Ask a Question

-
-
-