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-separator": "^1.0.3",
"@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-tabs": "^1.0.4", "@radix-ui/react-tabs": "^1.0.4",
"@supabase/supabase-js": "^2.47.0",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"clsx": "^2.1.0", "clsx": "^2.1.0",
"lucide-react": "^0.344.0", "lucide-react": "^0.344.0",
@ -2028,6 +2029,80 @@
"win32" "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": { "node_modules/@types/babel__core": {
"version": "7.20.5", "version": "7.20.5",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
@ -2081,6 +2156,21 @@
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
"dev": true "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": { "node_modules/@types/prop-types": {
"version": "15.7.13", "version": "15.7.13",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz",
@ -2106,6 +2196,15 @@
"@types/react": "*" "@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": { "node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.8.1", "version": "8.8.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.8.1.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.8.1.tgz",
@ -4684,6 +4783,12 @@
"node": ">=8.0" "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": { "node_modules/ts-api-utils": {
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", "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": { "node_modules/update-browserslist-db": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", "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": { "node_modules/which": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@ -5045,6 +5172,27 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1" "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": { "node_modules/yallist": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "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-separator": "^1.0.3",
"@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-tabs": "^1.0.4", "@radix-ui/react-tabs": "^1.0.4",
"@supabase/supabase-js": "^2.47.0",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"clsx": "^2.1.0", "clsx": "^2.1.0",
"lucide-react": "^0.344.0", "lucide-react": "^0.344.0",

View file

@ -1,15 +1,18 @@
import React from 'react'; import React from 'react';
import { BrowserRouter } from 'react-router-dom'; import { BrowserRouter } from 'react-router-dom';
import { AppRouter } from './routes'; import { AppRouter } from './routes';
import { AuthProvider } from './contexts/AuthContext';
import './index.css'; import './index.css';
const App: React.FC = () => { const App: React.FC = () => {
return ( return (
<AuthProvider>
<BrowserRouter> <BrowserRouter>
<div className="min-h-screen bg-background text-foreground"> <div className="min-h-screen bg-background text-foreground">
<AppRouter /> <AppRouter />
</div> </div>
</BrowserRouter> </BrowserRouter>
</AuthProvider>
); );
}; };

View file

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

View file

@ -1,13 +1,13 @@
import React from 'react'; import React from 'react';
import { Outlet } from 'react-router-dom'; import { Outlet } from 'react-router-dom';
import { Header } from '../Header'; import { Header } from '../Header/index';
import { Sidebar } from '../Sidebar'; import { Sidebar } from '../Sidebar/index';
const DashboardLayout: React.FC = () => { const DashboardLayout = () => {
return ( return (
<div className="flex min-h-screen flex-col"> <div className="flex h-screen flex-col overflow-hidden">
<Header /> <Header />
<div className="flex flex-1"> <div className="flex flex-1 overflow-hidden">
<Sidebar /> <Sidebar />
<main className="flex-1 overflow-y-auto bg-muted/50 p-6"> <main className="flex-1 overflow-y-auto bg-muted/50 p-6">
<Outlet /> <Outlet />

View file

@ -2,8 +2,15 @@ import React from 'react';
import { BookOpen } from 'lucide-react'; import { BookOpen } from 'lucide-react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { Button } from '../../ui/Button'; 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 ( return (
<header className="border-b bg-background"> <header className="border-b bg-background">
<div className="mx-auto flex max-w-7xl items-center justify-between px-4 py-4"> <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> <span>StudyAI</span>
</Link> </Link>
<nav className="flex items-center gap-4"> <nav className="flex items-center gap-4">
{session ? (
<>
<Link to="/dashboard"> <Link to="/dashboard">
<Button variant="ghost">Dashboard</Button> <Button variant="ghost">Dashboard</Button>
</Link> </Link>
<Button variant="ghost" onClick={handleSignOut}>
Sign out
</Button>
</>
) : (
<>
<Link to="/auth/login"> <Link to="/auth/login">
<Button variant="ghost">Log in</Button> <Button variant="ghost">Log in</Button>
</Link> </Link>
<Link to="/auth/signup"> <Link to="/auth/signup">
<Button>Sign up</Button> <Button>Sign up</Button>
</Link> </Link>
</>
)}
</nav> </nav>
</div> </div>
</header> </header>
); );
} };
export { Header };

View file

@ -5,7 +5,6 @@ import {
MessageSquarePlus, MessageSquarePlus,
Upload, Upload,
BookOpen, BookOpen,
GraduationCap,
History History
} from 'lucide-react'; } from 'lucide-react';
import { cn } from '../../../lib/utils'; import { cn } from '../../../lib/utils';
@ -16,6 +15,11 @@ const sidebarItems = [
label: 'Ask a Question', label: 'Ask a Question',
path: '/dashboard/ask' path: '/dashboard/ask'
}, },
{
icon: History,
label: 'Study History',
path: '/dashboard/history'
},
{ {
icon: Upload, icon: Upload,
label: 'Upload Materials', label: 'Upload Materials',
@ -26,11 +30,7 @@ const sidebarItems = [
label: 'Study Resources', label: 'Study Resources',
path: '/dashboard/resources' path: '/dashboard/resources'
}, },
{
icon: History,
label: 'Study History',
path: '/dashboard/history'
},
{ {
icon: Settings, icon: Settings,
label: 'Settings', label: 'Settings',
@ -38,19 +38,22 @@ const sidebarItems = [
}, },
]; ];
export function Sidebar() { const Sidebar = () => {
const location = useLocation(); const location = useLocation();
const isActive = (path: string) => {
if (path === '/dashboard/ask') {
return location.pathname === '/dashboard' || location.pathname === path;
}
return location.pathname === path;
};
return ( return (
<div className="flex h-full w-64 flex-col border-r bg-background"> <div className="flex h-screen 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>
<nav className="flex-1 space-y-1 px-2 py-4"> <nav className="flex-1 space-y-1 px-2 py-4">
{sidebarItems.map((item) => { {sidebarItems.map((item) => {
const Icon = item.icon; const Icon = item.icon;
const isActive = location.pathname === item.path; const active = isActive(item.path);
return ( return (
<Link <Link
@ -58,7 +61,7 @@ export function Sidebar() {
to={item.path} to={item.path}
className={cn( className={cn(
'flex items-center rounded-lg px-3 py-2 text-sm font-medium transition-colors', 'flex items-center rounded-lg px-3 py-2 text-sm font-medium transition-colors',
isActive active
? 'bg-muted text-foreground' ? 'bg-muted text-foreground'
: 'text-muted-foreground hover:bg-muted hover:text-foreground' : 'text-muted-foreground hover:bg-muted hover:text-foreground'
)} )}
@ -71,4 +74,6 @@ export function Sidebar() {
</nav> </nav>
</div> </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 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 { Link } from 'react-router-dom';
import { Button } from '../components/ui/Button'; import { Button } from '../components/ui/Button';
import { Header } from '../components/layout/Header'; import { Header } from '../components/layout/Header';
function Home() { function Home() {
return ( return (
<div> <div className="flex h-screen flex-col overflow-hidden">
<Header /> <header className="border-b bg-background">
<main> <div className="mx-auto flex max-w-7xl items-center justify-between px-4 py-4">
<section className="bg-muted/50 px-4 py-20"> <Link to="/" className="flex items-center gap-2 text-xl font-bold">
<div className="mx-auto max-w-7xl text-center"> <BookOpen className="h-6 w-6 text-foreground" />
<h1 className="text-5xl font-bold tracking-tight"> <span>StudyAI</span>
Your AI Study Buddy </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> </h1>
<p className="mx-auto mt-6 max-w-2xl text-lg text-muted-foreground"> <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, Enhance your learning experience with personalized AI assistance.
ask questions, and receive detailed explanations. Ask questions, get instant feedback, and track your progress.
</p> </p>
<div className="mt-10"> <div className="mt-10 flex justify-center gap-4">
<Link to="/auth/signup"> <Link to="/auth/signup">
<Button size="lg" className="gap-2"> <Button size="lg">Get Started</Button>
Get Started <ArrowRight className="h-5 w-5" /> </Link>
<Link to="/auth/login">
<Button variant="outline" size="lg">
Learn More
</Button> </Button>
</Link> </Link>
</div> </div>
</div> </div>
</section> </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>
</div>
</section>
</main> </main>
</div> </div>
); );

View file

@ -1,76 +1,116 @@
import React, { useState } from 'react'; 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 { Button } from '../../components/ui/Button';
import { useAuth } from '../../contexts/AuthContext';
function Login() { interface LocationState {
const [email, setEmail] = useState(''); from?: string;
const [password, setPassword] = useState(''); 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(); 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 ( 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="mx-auto max-w-md space-y-6 p-6">
<div className="w-full max-w-md space-y-8"> <div className="space-y-2 text-center">
<div> <h1 className="text-3xl font-bold">Welcome Back</h1>
<h2 className="mt-6 text-center text-3xl font-bold tracking-tight"> <p className="text-muted-foreground">Enter your credentials to access your account</p>
Sign in to your account
</h2>
</div> </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> <div>
<label htmlFor="email" className="sr-only"> <label htmlFor="email" className="block text-sm font-medium text-foreground">
Email address Email
</label> </label>
<input <input
id="email" id="email"
name="email" name="email"
type="email" type="email"
autoComplete="email"
required required
value={email} value={formData.email}
onChange={(e) => setEmail(e.target.value)} onChange={handleChange}
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" className="mt-1 block w-full rounded-md border border-input bg-background px-3 py-2"
placeholder="Email address" placeholder="you@example.com"
/> />
</div> </div>
<div> <div>
<label htmlFor="password" className="sr-only"> <label htmlFor="password" className="block text-sm font-medium text-foreground">
Password Password
</label> </label>
<input <input
id="password" id="password"
name="password" name="password"
type="password" type="password"
autoComplete="current-password"
required required
value={password} value={formData.password}
onChange={(e) => setPassword(e.target.value)} onChange={handleChange}
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" className="mt-1 block w-full rounded-md border border-input bg-background px-3 py-2"
placeholder="Password" placeholder="••••••••"
/> />
</div> </div>
</div>
<div> <Button type="submit" className="w-full" disabled={loading}>
<Button type="submit" className="w-full"> {loading ? 'Signing in...' : 'Sign in'}
Sign in
</Button> </Button>
</div>
</form> </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?{' '} 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 Sign up
</Link> </Link>
</p> </p>
</div> </div>
</div>
); );
} }
export default Login;

View file

@ -1,92 +1,125 @@
import React, { useState } from 'react'; 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 { Button } from '../../components/ui/Button';
import { useAuth } from '../../contexts/AuthContext';
function Signup() { export default function Signup() {
const [email, setEmail] = useState(''); const navigate = useNavigate();
const [password, setPassword] = useState(''); const { signUp } = useAuth();
const [name, setName] = useState(''); 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(); 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 ( 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="mx-auto max-w-md space-y-6 p-6">
<div className="w-full max-w-md space-y-8"> <div className="space-y-2 text-center">
<div> <h1 className="text-3xl font-bold">Create an Account</h1>
<h2 className="mt-6 text-center text-3xl font-bold tracking-tight"> <p className="text-muted-foreground">Enter your details to create your account</p>
Create your account
</h2>
</div> </div>
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
<div className="space-y-4 rounded-md"> {error && (
<div> <div className="rounded-lg bg-destructive/15 p-3 text-sm text-destructive">
<label htmlFor="name" className="sr-only"> {error}
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"
/>
</div> </div>
)}
<form onSubmit={handleSubmit} className="space-y-4">
<div> <div>
<label htmlFor="email" className="sr-only"> <label htmlFor="email" className="block text-sm font-medium text-foreground">
Email address Email
</label> </label>
<input <input
id="email" id="email"
name="email" name="email"
type="email" type="email"
autoComplete="email"
required required
value={email} value={formData.email}
onChange={(e) => setEmail(e.target.value)} onChange={handleChange}
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" className="mt-1 block w-full rounded-md border border-input bg-background px-3 py-2"
placeholder="Email address" placeholder="you@example.com"
/> />
</div> </div>
<div> <div>
<label htmlFor="password" className="sr-only"> <label htmlFor="password" className="block text-sm font-medium text-foreground">
Password Password
</label> </label>
<input <input
id="password" id="password"
name="password" name="password"
type="password" type="password"
autoComplete="new-password"
required required
value={password} value={formData.password}
onChange={(e) => setPassword(e.target.value)} onChange={handleChange}
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" className="mt-1 block w-full rounded-md border border-input bg-background px-3 py-2"
placeholder="Password" placeholder="••••••••"
/> />
</div> </div>
</div>
<div> <div>
<Button type="submit" className="w-full"> <label htmlFor="confirmPassword" className="block text-sm font-medium text-foreground">
Sign up Confirm Password
</Button> </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> </div>
<Button type="submit" className="w-full" disabled={loading}>
{loading ? 'Creating account...' : 'Create account'}
</Button>
</form> </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?{' '} 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 Sign in
</Link> </Link>
</p> </p>
</div> </div>
</div>
); );
} }
export default Signup;

View file

@ -1,53 +1,291 @@
import React, { useState } from 'react'; import React, { useState, useEffect } from 'react';
import { Send, Upload } from 'lucide-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 { 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 [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) => { // Check for existing chats and redirect to most recent if on base path
e.preventDefault(); useEffect(() => {
// Handle question submission 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 (
<div className="space-y-6"> <div className="flex h-full items-center justify-center">
<h1 className="text-3xl font-bold">Ask a Question</h1> <Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
<form onSubmit={handleSubmit} className="space-y-6"> </div>
<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? return (
</span> <div className="flex h-full flex-col">
<textarea <div className="flex items-center justify-between border-b px-4 py-3">
value={question} <div>
onChange={(e) => setQuestion(e.target.value)} <h1 className="text-lg font-semibold">Ask a Question</h1>
className="mt-2 block w-full rounded-lg border border-input px-4 py-2 focus:border-primary focus:ring-primary" {chat && (
rows={4} <p className="text-sm text-muted-foreground">
placeholder="Type your question here..." {chat.title}
/> </p>
</label> )}
</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>
<div className="rounded-lg bg-card p-6 shadow-sm"> {error && (
<div className="flex items-center justify-between"> <div className="m-4 flex items-center gap-2 rounded-lg bg-destructive/15 p-3 text-sm text-destructive">
<span className="text-sm font-medium text-foreground"> <AlertCircle className="h-4 w-4" />
Upload Study Materials (Optional) {error}
</span> </div>
<Button variant="secondary" type="button" className="gap-2"> )}
<Upload className="h-4 w-4" />
Upload Files <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> </Button>
</div> </div>
</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"> {(isNewChat || chatId) && (
<Send className="h-5 w-5" /> <div className="border-t p-4">
Submit Question <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> </Button>
</form> </form>
</div> </div>
)}
</div>
); );
} }
export default AskQuestion;

View file

@ -1,65 +1,96 @@
import React from 'react'; import React, { useEffect, useState } from 'react';
import { Calendar } from 'lucide-react'; import { Link } from 'react-router-dom';
import { Card } from '../../components/ui/card'; 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 = [ export default function StudyHistory() {
{ const { user } = useAuth();
id: '1', const [chats, setChats] = useState<ChatInstance[]>([]);
date: new Date('2024-03-10'), const [loading, setLoading] = useState(true);
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'],
},
];
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 ( return (
<div className="space-y-6"> <div className="flex h-full items-center justify-center">
<h1 className="text-3xl font-bold">Study History</h1> <div className="text-lg">Loading...</div>
<div className="space-y-4"> </div>
{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" /> return (
<div className="flex-1"> <div className="space-y-6 p-6">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h3 className="font-medium">{item.activity}</h3> <div>
<span className="text-sm text-muted-foreground"> <h1 className="text-2xl font-bold">Study History</h1>
{item.date.toLocaleDateString()} <p className="text-muted-foreground">View your past conversations</p>
</span>
</div> </div>
<p className="text-sm text-muted-foreground">Duration: {item.duration}</p> <Link to="/dashboard/ask">
<div className="mt-2 flex flex-wrap gap-2"> <Button>
{item.topics.map((topic) => ( <MessageSquare className="mr-2 h-4 w-4" />
<span New Chat
key={topic} </Button>
className="rounded-full bg-primary/10 px-2 py-1 text-xs text-primary" </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} <Link to={`/dashboard/ask/${chat.id}`} className="block">
</span> <h3 className="font-medium">{chat.title}</h3>
))} <p className="mt-1 text-sm text-muted-foreground">
</div> Last updated: {new Date(chat.last_message_at).toLocaleString()}
</div> </p>
</div> </Link>
</Card> <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>
)}
</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 React from 'react';
import { Navigate, useLocation } from 'react-router-dom'; import { Navigate, useLocation } from 'react-router-dom';
import { routes } from './routes'; import { routes } from './routes';
import { useAuth } from '../contexts/AuthContext';
// TODO: Replace with your actual auth hook
const useAuth = () => {
return {
isAuthenticated: true, // Temporarily set to true for development
};
};
interface PrivateRouteProps { interface PrivateRouteProps {
children: React.ReactNode; children: React.ReactNode;
} }
export function PrivateRoute({ children }: PrivateRouteProps) { export function PrivateRoute({ children }: PrivateRouteProps) {
const { isAuthenticated } = useAuth(); const { session, loading } = useAuth();
const location = useLocation(); 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 />; 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')); const Signup = React.lazy(() => import('../pages/auth/Signup'));
// Dashboard Pages // Dashboard Pages
const Dashboard = React.lazy(() => import('../pages/dashboard'));
const AskQuestion = React.lazy(() => import('../pages/dashboard/ask')); const AskQuestion = React.lazy(() => import('../pages/dashboard/ask'));
const UploadMaterials = React.lazy(() => import('../pages/dashboard/upload')); const UploadMaterials = React.lazy(() => import('../pages/dashboard/upload'));
const StudyResources = React.lazy(() => import('../pages/dashboard/resources')); const StudyResources = React.lazy(() => import('../pages/dashboard/resources'));
@ -57,8 +56,9 @@ export const AppRouter: React.FC = () => {
</PrivateRoute> </PrivateRoute>
} }
> >
<Route index element={withSuspense(Dashboard)} /> <Route index element={<Navigate to="ask" replace />} />
<Route path="ask" element={withSuspense(AskQuestion)} /> <Route path="ask" element={withSuspense(AskQuestion)} />
<Route path="ask/:chatId" element={withSuspense(AskQuestion)} />
<Route path="upload" element={withSuspense(UploadMaterials)} /> <Route path="upload" element={withSuspense(UploadMaterials)} />
<Route path="resources" element={withSuspense(StudyResources)} /> <Route path="resources" element={withSuspense(StudyResources)} />
<Route path="history" element={withSuspense(StudyHistory)} /> <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'>>;
};
};
};
};