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.
-
-
-
-
+
+
-
-
-
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
-
-
-
-
-
- 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
-
-
-
-
-
- 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 (
-