Working frontend interface with supabse storage

This commit is contained in:
Harivansh Rathi 2024-12-05 17:05:20 -05:00
parent ae239a2849
commit 054abcee98
20 changed files with 1153 additions and 422 deletions

3
.env Normal file
View file

@ -0,0 +1,3 @@
# Get these values from your Supabase project settings -> API
VITE_SUPABASE_URL=https://haeoggmmluxgjlkjlmbv.supabase.co
VITE_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImhhZW9nZ21tbHV4Z2psa2psbWJ2Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3MzM0MzI0NDMsImV4cCI6MjA0OTAwODQ0M30.vQxeGmj9f3cV1RHHNDoyFJ8tNPw29SrNHgepzbGEaSY

148
package-lock.json generated
View file

@ -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",

View file

@ -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",

View file

@ -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 (
<AuthProvider>
<BrowserRouter>
<div className="min-h-screen bg-background text-foreground">
<AppRouter />
</div>
</BrowserRouter>
</AuthProvider>
);
};

View file

@ -4,12 +4,14 @@ import { Header } from '../Header';
const AuthLayout: React.FC = () => {
return (
<div className="min-h-screen bg-background">
<div className="flex h-screen flex-col overflow-hidden bg-background">
<Header />
<div className="flex-1 overflow-y-auto">
<div className="container mx-auto px-4 py-8">
<Outlet />
</div>
</div>
</div>
);
};

View file

@ -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 (
<div className="flex min-h-screen flex-col">
<div className="flex h-screen flex-col overflow-hidden">
<Header />
<div className="flex flex-1">
<div className="flex flex-1 overflow-hidden">
<Sidebar />
<main className="flex-1 overflow-y-auto bg-muted/50 p-6">
<Outlet />

View file

@ -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 (
<header className="border-b bg-background">
<div className="mx-auto flex max-w-7xl items-center justify-between px-4 py-4">
@ -12,17 +19,29 @@ export function Header() {
<span>StudyAI</span>
</Link>
<nav className="flex items-center gap-4">
{session ? (
<>
<Link to="/dashboard">
<Button variant="ghost">Dashboard</Button>
</Link>
<Button variant="ghost" onClick={handleSignOut}>
Sign out
</Button>
</>
) : (
<>
<Link to="/auth/login">
<Button variant="ghost">Log in</Button>
</Link>
<Link to="/auth/signup">
<Button>Sign up</Button>
</Link>
</>
)}
</nav>
</div>
</header>
);
}
};
export { Header };

View file

@ -5,7 +5,6 @@ import {
MessageSquarePlus,
Upload,
BookOpen,
GraduationCap,
History
} from 'lucide-react';
import { cn } from '../../../lib/utils';
@ -16,6 +15,11 @@ const sidebarItems = [
label: 'Ask a Question',
path: '/dashboard/ask'
},
{
icon: History,
label: 'Study History',
path: '/dashboard/history'
},
{
icon: Upload,
label: 'Upload Materials',
@ -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 (
<div className="flex h-full w-64 flex-col border-r bg-background">
<div className="flex h-14 items-center border-b px-4">
<GraduationCap className="h-6 w-6" />
<span className="ml-2 font-semibold">Study Dashboard</span>
</div>
<div className="flex h-screen w-64 flex-col border-r bg-background">
<nav className="flex-1 space-y-1 px-2 py-4">
{sidebarItems.map((item) => {
const Icon = item.icon;
const isActive = location.pathname === item.path;
const active = isActive(item.path);
return (
<Link
@ -58,7 +61,7 @@ export function Sidebar() {
to={item.path}
className={cn(
'flex items-center rounded-lg px-3 py-2 text-sm font-medium transition-colors',
isActive
active
? 'bg-muted text-foreground'
: 'text-muted-foreground hover:bg-muted hover:text-foreground'
)}
@ -71,4 +74,6 @@ export function Sidebar() {
</nav>
</div>
);
}
};
export { Sidebar };

View file

@ -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<AuthContextType | undefined>(undefined);
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [session, setSession] = useState<Session | null>(null);
const [user, setUser] = useState<User | null>(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 (
<AuthContext.Provider value={value}>
{!loading && children}
</AuthContext.Provider>
);
}
export function useAuth() {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
}

143
src/lib/chat-service.ts Normal file
View file

@ -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<ChatInstance | null> {
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<ChatInstance | null> {
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<boolean> {
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<ChatInstance[]> {
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<ChatMessage[]> {
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<ChatMessage | null> {
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<boolean> {
// 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;
},
};

44
src/lib/supabase.ts Normal file
View file

@ -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<Database>(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);
},
};

View file

@ -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 (
<div>
<Header />
<main>
<section className="bg-muted/50 px-4 py-20">
<div className="mx-auto max-w-7xl text-center">
<h1 className="text-5xl font-bold tracking-tight">
Your AI Study Buddy
<div className="flex h-screen flex-col overflow-hidden">
<header className="border-b bg-background">
<div className="mx-auto flex max-w-7xl items-center justify-between px-4 py-4">
<Link to="/" className="flex items-center gap-2 text-xl font-bold">
<BookOpen className="h-6 w-6 text-foreground" />
<span>StudyAI</span>
</Link>
<nav className="flex items-center gap-4">
<Link to="/auth/login">
<Button variant="ghost">Log in</Button>
</Link>
<Link to="/auth/signup">
<Button>Sign up</Button>
</Link>
</nav>
</div>
</header>
<main className="flex-1 overflow-y-auto">
<div className="container mx-auto px-4">
<section className="py-20">
<div className="text-center">
<h1 className="text-4xl font-bold sm:text-6xl">
Learn Smarter with AI
</h1>
<p className="mx-auto mt-6 max-w-2xl text-lg text-muted-foreground">
Get instant help with your coursework using advanced AI. Upload your materials,
ask questions, and receive detailed explanations.
Enhance your learning experience with personalized AI assistance.
Ask questions, get instant feedback, and track your progress.
</p>
<div className="mt-10">
<div className="mt-10 flex justify-center gap-4">
<Link to="/auth/signup">
<Button size="lg" className="gap-2">
Get Started <ArrowRight className="h-5 w-5" />
<Button size="lg">Get Started</Button>
</Link>
<Link to="/auth/login">
<Button variant="outline" size="lg">
Learn More
</Button>
</Link>
</div>
</div>
</section>
<section className="py-20">
<div className="mx-auto max-w-7xl px-4">
<h2 className="text-center text-3xl font-bold">Why Choose StudyAI?</h2>
<div className="mt-12 grid grid-cols-1 gap-8 md:grid-cols-3">
{[
{
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) => (
<div
key={feature.title}
className="rounded-xl border bg-card p-6 text-center shadow-sm"
>
<feature.icon className="mx-auto h-12 w-12 text-primary" />
<h3 className="mt-4 text-xl font-semibold">{feature.title}</h3>
<p className="mt-2 text-muted-foreground">{feature.description}</p>
</div>
))}
</div>
</div>
</section>
</main>
</div>
);

View file

@ -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<string | null>(null);
const [message, setMessage] = useState<string | null>(
(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<HTMLInputElement>) => {
setFormData(prev => ({
...prev,
[e.target.name]: e.target.value
}));
};
return (
<div className="flex min-h-[calc(100vh-4rem)] items-center justify-center bg-muted/50 py-12 px-4 sm:px-6 lg:px-8">
<div className="w-full max-w-md space-y-8">
<div>
<h2 className="mt-6 text-center text-3xl font-bold tracking-tight">
Sign in to your account
</h2>
<div className="mx-auto max-w-md space-y-6 p-6">
<div className="space-y-2 text-center">
<h1 className="text-3xl font-bold">Welcome Back</h1>
<p className="text-muted-foreground">Enter your credentials to access your account</p>
</div>
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
<div className="space-y-4 rounded-md">
{message && (
<div className="rounded-lg bg-primary/15 p-3 text-sm text-primary">
{message}
</div>
)}
{error && (
<div className="rounded-lg bg-destructive/15 p-3 text-sm text-destructive">
{error}
</div>
)}
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label htmlFor="email" className="sr-only">
Email address
<label htmlFor="email" className="block text-sm font-medium text-foreground">
Email
</label>
<input
id="email"
name="email"
type="email"
autoComplete="email"
required
value={email}
onChange={(e) => 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"
value={formData.email}
onChange={handleChange}
className="mt-1 block w-full rounded-md border border-input bg-background px-3 py-2"
placeholder="you@example.com"
/>
</div>
<div>
<label htmlFor="password" className="sr-only">
<label htmlFor="password" className="block text-sm font-medium text-foreground">
Password
</label>
<input
id="password"
name="password"
type="password"
autoComplete="current-password"
required
value={password}
onChange={(e) => 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"
value={formData.password}
onChange={handleChange}
className="mt-1 block w-full rounded-md border border-input bg-background px-3 py-2"
placeholder="••••••••"
/>
</div>
</div>
<div>
<Button type="submit" className="w-full">
Sign in
<Button type="submit" className="w-full" disabled={loading}>
{loading ? 'Signing in...' : 'Sign in'}
</Button>
</div>
</form>
<p className="mt-2 text-center text-sm text-muted-foreground">
<p className="text-center text-sm text-muted-foreground">
Don't have an account?{' '}
<Link to="/signup" className="font-medium text-primary hover:text-primary/90">
<Link to="/auth/signup" className="font-medium text-primary hover:text-primary/90">
Sign up
</Link>
</p>
</div>
</div>
);
}
export default Login;

View file

@ -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<string | null>(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<HTMLInputElement>) => {
setFormData(prev => ({
...prev,
[e.target.name]: e.target.value
}));
};
return (
<div className="flex min-h-[calc(100vh-4rem)] items-center justify-center bg-muted/50 py-12 px-4 sm:px-6 lg:px-8">
<div className="w-full max-w-md space-y-8">
<div>
<h2 className="mt-6 text-center text-3xl font-bold tracking-tight">
Create your account
</h2>
<div className="mx-auto max-w-md space-y-6 p-6">
<div className="space-y-2 text-center">
<h1 className="text-3xl font-bold">Create an Account</h1>
<p className="text-muted-foreground">Enter your details to create your account</p>
</div>
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
<div className="space-y-4 rounded-md">
<div>
<label htmlFor="name" className="sr-only">
Full name
</label>
<input
id="name"
name="name"
type="text"
required
value={name}
onChange={(e) => 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"
/>
{error && (
<div className="rounded-lg bg-destructive/15 p-3 text-sm text-destructive">
{error}
</div>
)}
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label htmlFor="email" className="sr-only">
Email address
<label htmlFor="email" className="block text-sm font-medium text-foreground">
Email
</label>
<input
id="email"
name="email"
type="email"
autoComplete="email"
required
value={email}
onChange={(e) => 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"
value={formData.email}
onChange={handleChange}
className="mt-1 block w-full rounded-md border border-input bg-background px-3 py-2"
placeholder="you@example.com"
/>
</div>
<div>
<label htmlFor="password" className="sr-only">
<label htmlFor="password" className="block text-sm font-medium text-foreground">
Password
</label>
<input
id="password"
name="password"
type="password"
autoComplete="new-password"
required
value={password}
onChange={(e) => 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"
value={formData.password}
onChange={handleChange}
className="mt-1 block w-full rounded-md border border-input bg-background px-3 py-2"
placeholder="••••••••"
/>
</div>
</div>
<div>
<Button type="submit" className="w-full">
Sign up
</Button>
<label htmlFor="confirmPassword" className="block text-sm font-medium text-foreground">
Confirm Password
</label>
<input
id="confirmPassword"
name="confirmPassword"
type="password"
required
value={formData.confirmPassword}
onChange={handleChange}
className="mt-1 block w-full rounded-md border border-input bg-background px-3 py-2"
placeholder="••••••••"
/>
</div>
<Button type="submit" className="w-full" disabled={loading}>
{loading ? 'Creating account...' : 'Create account'}
</Button>
</form>
<p className="mt-2 text-center text-sm text-muted-foreground">
<p className="text-center text-sm text-muted-foreground">
Already have an account?{' '}
<Link to="/login" className="font-medium text-primary hover:text-primary/90">
<Link to="/auth/login" className="font-medium text-primary hover:text-primary/90">
Sign in
</Link>
</p>
</div>
</div>
);
}
export default Signup;

View file

@ -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<ChatMessage[]>([]);
const [error, setError] = useState<string | null>(null);
const [chat, setChat] = useState<ChatInstance | null>(null);
const [isNewChat, setIsNewChat] = useState(false);
const [hasExistingChats, setHasExistingChats] = useState<boolean | null>(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 (
<div className="space-y-6">
<h1 className="text-3xl font-bold">Ask a Question</h1>
<form onSubmit={handleSubmit} className="space-y-6">
<div className="rounded-lg bg-card p-6 shadow-sm">
<label className="block">
<span className="text-sm font-medium text-foreground">
What would you like to know?
</span>
<textarea
value={question}
onChange={(e) => setQuestion(e.target.value)}
className="mt-2 block w-full rounded-lg border border-input px-4 py-2 focus:border-primary focus:ring-primary"
rows={4}
placeholder="Type your question here..."
/>
</label>
<div className="flex h-full items-center justify-center">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
);
}
return (
<div className="flex h-full flex-col">
<div className="flex items-center justify-between border-b px-4 py-3">
<div>
<h1 className="text-lg font-semibold">Ask a Question</h1>
{chat && (
<p className="text-sm text-muted-foreground">
{chat.title}
</p>
)}
</div>
<div className="flex items-center gap-2">
<Button onClick={handleNewChat}>
<MessageSquarePlus className="mr-2 h-4 w-4" />
New Chat
</Button>
{chatId && !isNewChat && messages.length > 0 && (
<Button variant="ghost" size="sm" onClick={clearChat}>
<X className="mr-2 h-4 w-4" />
Clear chat
</Button>
)}
{hasExistingChats && (
<Link to="/dashboard/history">
<Button variant="ghost" size="sm">
<History className="mr-2 h-4 w-4" />
History
</Button>
</Link>
)}
</div>
</div>
<div className="rounded-lg bg-card p-6 shadow-sm">
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-foreground">
Upload Study Materials (Optional)
</span>
<Button variant="secondary" type="button" className="gap-2">
<Upload className="h-4 w-4" />
Upload Files
{error && (
<div className="m-4 flex items-center gap-2 rounded-lg bg-destructive/15 p-3 text-sm text-destructive">
<AlertCircle className="h-4 w-4" />
{error}
</div>
)}
<div className="flex-1 overflow-y-auto p-4 space-y-4">
{!isNewChat && !chatId ? (
<div className="flex h-full items-center justify-center text-center">
<div className="max-w-md space-y-4">
<p className="text-lg font-medium">Welcome to StudyAI Chat</p>
<p className="text-sm text-muted-foreground">
Start a new conversation by clicking the "New Chat" button above
</p>
<Button onClick={handleNewChat}>
<MessageSquarePlus className="mr-2 h-4 w-4" />
Start New Chat
</Button>
</div>
</div>
) : messages.length === 0 ? (
<div className="flex h-full items-center justify-center text-center">
<div className="max-w-md space-y-2">
<p className="text-lg font-medium">No messages yet</p>
<p className="text-sm text-muted-foreground">
Start by asking a question about your studies
</p>
</div>
</div>
) : (
messages.map((message) => (
<div
key={message.id}
className={cn(
'flex w-full',
message.role === 'user' ? 'justify-end' : 'justify-start'
)}
>
<div
className={cn(
'max-w-[80%] rounded-lg px-4 py-2',
message.role === 'user'
? 'bg-primary text-primary-foreground'
: 'bg-muted'
)}
>
{message.content}
</div>
</div>
))
)}
</div>
<Button type="submit" className="w-full gap-2">
<Send className="h-5 w-5" />
Submit Question
{(isNewChat || chatId) && (
<div className="border-t p-4">
<form onSubmit={handleSubmit} className="flex gap-2">
<input
type="text"
value={question}
onChange={(e) => setQuestion(e.target.value)}
placeholder="Type your question..."
className="flex-1 rounded-md border border-input bg-background px-3 py-2"
disabled={loading}
/>
<Button type="submit" disabled={loading || !question.trim()}>
{loading ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Send className="h-4 w-4" />
)}
</Button>
</form>
</div>
)}
</div>
);
}
export default AskQuestion;

View file

@ -1,65 +1,96 @@
import React from 'react';
import { Calendar } from 'lucide-react';
import { Card } from '../../components/ui/card';
import React, { useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { MessageSquare, Trash2 } from 'lucide-react';
import { Button } from '../../components/ui/Button';
import { useAuth } from '../../contexts/AuthContext';
import { chatService } from '../../lib/chat-service';
import type { ChatInstance } from '../../types/supabase';
const mockHistory = [
{
id: '1',
date: new Date('2024-03-10'),
activity: 'Studied Biology',
duration: '2 hours',
topics: ['Photosynthesis', 'Cell Structure'],
},
{
id: '2',
date: new Date('2024-03-09'),
activity: 'Practice Problems',
duration: '1.5 hours',
topics: ['Algebra', 'Calculus'],
},
{
id: '3',
date: new Date('2024-03-08'),
activity: 'Reading Assignment',
duration: '1 hour',
topics: ['Literature', 'Poetry Analysis'],
},
];
export default function StudyHistory() {
const { user } = useAuth();
const [chats, setChats] = useState<ChatInstance[]>([]);
const [loading, setLoading] = useState(true);
function StudyHistory() {
useEffect(() => {
if (!user) return;
const loadChats = async () => {
const chatInstances = await chatService.getChatInstances(user.id);
setChats(chatInstances);
setLoading(false);
};
loadChats();
}, [user]);
const handleDelete = async (chatId: string) => {
const success = await chatService.deleteChatInstance(chatId);
if (success) {
setChats(prev => prev.filter(chat => chat.id !== chatId));
}
};
if (loading) {
return (
<div className="space-y-6">
<h1 className="text-3xl font-bold">Study History</h1>
<div className="space-y-4">
{mockHistory.map((item) => (
<Card key={item.id} className="p-4">
<div className="flex items-start space-x-4">
<Calendar className="h-5 w-5 text-muted-foreground" />
<div className="flex-1">
<div className="flex h-full items-center justify-center">
<div className="text-lg">Loading...</div>
</div>
);
}
return (
<div className="space-y-6 p-6">
<div className="flex items-center justify-between">
<h3 className="font-medium">{item.activity}</h3>
<span className="text-sm text-muted-foreground">
{item.date.toLocaleDateString()}
</span>
<div>
<h1 className="text-2xl font-bold">Study History</h1>
<p className="text-muted-foreground">View your past conversations</p>
</div>
<p className="text-sm text-muted-foreground">Duration: {item.duration}</p>
<div className="mt-2 flex flex-wrap gap-2">
{item.topics.map((topic) => (
<span
key={topic}
className="rounded-full bg-primary/10 px-2 py-1 text-xs text-primary"
<Link to="/dashboard/ask">
<Button>
<MessageSquare className="mr-2 h-4 w-4" />
New Chat
</Button>
</Link>
</div>
{chats.length === 0 ? (
<div className="rounded-lg border border-dashed p-8 text-center">
<h3 className="text-lg font-medium">No chat history</h3>
<p className="mt-1 text-sm text-muted-foreground">
Start a new conversation to see it here
</p>
<Link to="/dashboard/ask" className="mt-4 inline-block">
<Button>Start a new chat</Button>
</Link>
</div>
) : (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{chats.map((chat) => (
<div
key={chat.id}
className="group relative rounded-lg border bg-card p-4 transition-colors hover:bg-muted/50"
>
{topic}
</span>
))}
</div>
</div>
</div>
</Card>
<Link to={`/dashboard/ask/${chat.id}`} className="block">
<h3 className="font-medium">{chat.title}</h3>
<p className="mt-1 text-sm text-muted-foreground">
Last updated: {new Date(chat.last_message_at).toLocaleString()}
</p>
</Link>
<Button
variant="ghost"
size="sm"
className="absolute right-2 top-2 opacity-0 transition-opacity group-hover:opacity-100"
onClick={(e) => {
e.preventDefault();
handleDelete(chat.id);
}}
>
<Trash2 className="h-4 w-4 text-muted-foreground hover:text-destructive" />
</Button>
</div>
))}
</div>
)}
</div>
);
}
export default StudyHistory;

View file

@ -1,73 +0,0 @@
import React from 'react';
import { Users, CreditCard, Activity, PlusCircle } from 'lucide-react';
import { Link } from 'react-router-dom';
import { Button } from '../../components/ui/Button';
import { QuestionList } from '../../components/dashboard/QuestionList';
import { StatsCard } from '../../components/dashboard/StatsCard';
import { Card } from '../../components/ui/card';
import { Separator } from '../../components/ui/separator';
const mockQuestions = [
{
id: '1',
text: 'How does photosynthesis work?',
timestamp: new Date('2024-03-10'),
},
{
id: '2',
text: 'Explain the concept of supply and demand in economics.',
timestamp: new Date('2024-03-09'),
},
];
function Dashboard() {
return (
<div className="space-y-8">
<div className="flex items-center justify-between">
<h1 className="text-3xl font-bold">Dashboard</h1>
<Link to="/dashboard/ask">
<Button className="gap-2">
<PlusCircle className="h-5 w-5" />
Ask a Question
</Button>
</Link>
</div>
<div className="grid gap-4 md:grid-cols-3">
<StatsCard
title="Total Questions"
value="156"
icon={<Users className="h-4 w-4 text-muted-foreground" />}
trend={{ value: "+12%", label: "from last month" }}
/>
<StatsCard
title="Study Sessions"
value="32"
icon={<CreditCard className="h-4 w-4 text-muted-foreground" />}
trend={{ value: "+8", label: "from last week" }}
/>
<StatsCard
title="Active Streak"
value="7 days"
icon={<Activity className="h-4 w-4 text-muted-foreground" />}
trend={{ value: "+2", label: "days" }}
/>
</div>
<Card>
<div className="p-6">
<h2 className="text-lg font-semibold">Recent Questions</h2>
<p className="text-sm text-muted-foreground">
Your most recent study questions and their status.
</p>
</div>
<Separator />
<div className="p-6">
<QuestionList questions={mockQuestions} />
</div>
</Card>
</div>
);
}
export default Dashboard;

View file

@ -1,23 +1,25 @@
import React from 'react';
import { Navigate, useLocation } from 'react-router-dom';
import { routes } from './routes';
// TODO: Replace with your actual auth hook
const useAuth = () => {
return {
isAuthenticated: true, // Temporarily set to true for development
};
};
import { useAuth } from '../contexts/AuthContext';
interface PrivateRouteProps {
children: React.ReactNode;
}
export function PrivateRoute({ children }: PrivateRouteProps) {
const { isAuthenticated } = useAuth();
const { session, loading } = useAuth();
const location = useLocation();
if (!isAuthenticated) {
if (loading) {
return (
<div className="flex h-screen items-center justify-center">
<div className="text-lg">Loading...</div>
</div>
);
}
if (!session) {
return <Navigate to={routes.public.login} state={{ from: location }} replace />;
}

View file

@ -20,7 +20,6 @@ const Login = React.lazy(() => import('../pages/auth/Login'));
const Signup = React.lazy(() => import('../pages/auth/Signup'));
// Dashboard Pages
const Dashboard = React.lazy(() => import('../pages/dashboard'));
const AskQuestion = React.lazy(() => import('../pages/dashboard/ask'));
const UploadMaterials = React.lazy(() => import('../pages/dashboard/upload'));
const StudyResources = React.lazy(() => import('../pages/dashboard/resources'));
@ -57,8 +56,9 @@ export const AppRouter: React.FC = () => {
</PrivateRoute>
}
>
<Route index element={withSuspense(Dashboard)} />
<Route index element={<Navigate to="ask" replace />} />
<Route path="ask" element={withSuspense(AskQuestion)} />
<Route path="ask/:chatId" element={withSuspense(AskQuestion)} />
<Route path="upload" element={withSuspense(UploadMaterials)} />
<Route path="resources" element={withSuspense(StudyResources)} />
<Route path="history" element={withSuspense(StudyHistory)} />

36
src/types/supabase.ts Normal file
View file

@ -0,0 +1,36 @@
export interface ChatInstance {
id: string;
created_at: string;
user_id: string;
title: string;
last_message_at: string;
}
export interface ChatMessage {
id: string;
chat_id: string;
content: string;
role: 'user' | 'assistant';
created_at: string;
metadata?: {
make_response_id?: string;
error?: string;
};
}
export type Database = {
public: {
Tables: {
chat_instances: {
Row: ChatInstance;
Insert: Omit<ChatInstance, 'id' | 'created_at'>;
Update: Partial<Omit<ChatInstance, 'id' | 'created_at' | 'user_id'>>;
};
chat_messages: {
Row: ChatMessage;
Insert: Omit<ChatMessage, 'id' | 'created_at'>;
Update: Partial<Omit<ChatMessage, 'id' | 'created_at' | 'chat_id'>>;
};
};
};
};