Spaces:
Running
Running
Pulastya B commited on
Commit ·
c0e18bf
1
Parent(s): 2f3df85
Trying to add Auth
Browse files- .gitignore +1 -0
- FRRONTEEEND/.env.example +4 -0
- FRRONTEEEND/App.tsx +84 -10
- FRRONTEEEND/SUPABASE_SETUP.md +120 -0
- FRRONTEEEND/components/AuthPage.tsx +262 -0
- FRRONTEEEND/components/ChatInterface.tsx +64 -11
- FRRONTEEEND/lib/AuthContext.tsx +140 -0
- FRRONTEEEND/lib/supabase.ts +170 -0
- FRRONTEEEND/package-lock.json +126 -3
- FRRONTEEEND/package.json +5 -4
- FRRONTEEEND/supabase_schema.sql +124 -0
.gitignore
CHANGED
|
@@ -11,6 +11,7 @@ downloads/
|
|
| 11 |
eggs/
|
| 12 |
.eggs/
|
| 13 |
lib/
|
|
|
|
| 14 |
lib64/
|
| 15 |
parts/
|
| 16 |
sdist/
|
|
|
|
| 11 |
eggs/
|
| 12 |
.eggs/
|
| 13 |
lib/
|
| 14 |
+
!FRRONTEEEND/lib/
|
| 15 |
lib64/
|
| 16 |
parts/
|
| 17 |
sdist/
|
FRRONTEEEND/.env.example
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Supabase Configuration
|
| 2 |
+
# Get these values from your Supabase project settings: https://app.supabase.com/
|
| 3 |
+
VITE_SUPABASE_URL=your_supabase_project_url
|
| 4 |
+
VITE_SUPABASE_ANON_KEY=your_supabase_anon_key
|
FRRONTEEEND/App.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
|
| 2 |
-
import React, { useState } from 'react';
|
| 3 |
import { HeroGeometric } from './components/HeroGeometric';
|
| 4 |
import ProblemSolution from './components/ProblemSolution';
|
| 5 |
import KeyCapabilities from './components/KeyCapabilities';
|
|
@@ -9,9 +9,31 @@ import Footer from './components/Footer';
|
|
| 9 |
import { BackgroundPaths } from './components/BackgroundPaths';
|
| 10 |
import { Logo } from './components/Logo';
|
| 11 |
import { ChatInterface } from './components/ChatInterface';
|
|
|
|
|
|
|
|
|
|
| 12 |
|
| 13 |
-
|
| 14 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
|
| 16 |
if (view === 'chat') {
|
| 17 |
return <ChatInterface onBack={() => setView('landing')} />;
|
|
@@ -28,16 +50,59 @@ const App: React.FC = () => {
|
|
| 28 |
</span>
|
| 29 |
</div>
|
| 30 |
|
| 31 |
-
<
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
</nav>
|
| 38 |
|
| 39 |
<main>
|
| 40 |
-
<HeroGeometric onChatClick={
|
| 41 |
<TechStack />
|
| 42 |
<ProblemSolution />
|
| 43 |
<KeyCapabilities />
|
|
@@ -56,4 +121,13 @@ const App: React.FC = () => {
|
|
| 56 |
);
|
| 57 |
};
|
| 58 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
export default App;
|
|
|
|
| 1 |
|
| 2 |
+
import React, { useState, useEffect } from 'react';
|
| 3 |
import { HeroGeometric } from './components/HeroGeometric';
|
| 4 |
import ProblemSolution from './components/ProblemSolution';
|
| 5 |
import KeyCapabilities from './components/KeyCapabilities';
|
|
|
|
| 9 |
import { BackgroundPaths } from './components/BackgroundPaths';
|
| 10 |
import { Logo } from './components/Logo';
|
| 11 |
import { ChatInterface } from './components/ChatInterface';
|
| 12 |
+
import { AuthPage } from './components/AuthPage';
|
| 13 |
+
import { AuthProvider, useAuth } from './lib/AuthContext';
|
| 14 |
+
import { User, LogOut } from 'lucide-react';
|
| 15 |
|
| 16 |
+
// Inner app component that uses auth context
|
| 17 |
+
const AppContent: React.FC = () => {
|
| 18 |
+
const [view, setView] = useState<'landing' | 'chat' | 'auth'>('landing');
|
| 19 |
+
const { user, isAuthenticated, loading, signOut } = useAuth();
|
| 20 |
+
const [showUserMenu, setShowUserMenu] = useState(false);
|
| 21 |
+
|
| 22 |
+
// Handle launch console - redirect to auth if not logged in
|
| 23 |
+
const handleLaunchConsole = () => {
|
| 24 |
+
// Allow both authenticated and guest users
|
| 25 |
+
setView('chat');
|
| 26 |
+
};
|
| 27 |
+
|
| 28 |
+
// Show auth page
|
| 29 |
+
if (view === 'auth') {
|
| 30 |
+
return (
|
| 31 |
+
<AuthPage
|
| 32 |
+
onSuccess={() => setView('chat')}
|
| 33 |
+
onSkip={() => setView('chat')}
|
| 34 |
+
/>
|
| 35 |
+
);
|
| 36 |
+
}
|
| 37 |
|
| 38 |
if (view === 'chat') {
|
| 39 |
return <ChatInterface onBack={() => setView('landing')} />;
|
|
|
|
| 50 |
</span>
|
| 51 |
</div>
|
| 52 |
|
| 53 |
+
<div className="flex items-center gap-3">
|
| 54 |
+
{/* User menu */}
|
| 55 |
+
{isAuthenticated ? (
|
| 56 |
+
<div className="relative">
|
| 57 |
+
<button
|
| 58 |
+
onClick={() => setShowUserMenu(!showUserMenu)}
|
| 59 |
+
className="flex items-center gap-2 px-3 py-2 bg-white/5 hover:bg-white/10 border border-white/10 rounded-lg text-sm transition-all"
|
| 60 |
+
>
|
| 61 |
+
<User className="w-4 h-4" />
|
| 62 |
+
<span className="hidden sm:block max-w-[120px] truncate">
|
| 63 |
+
{user?.email?.split('@')[0]}
|
| 64 |
+
</span>
|
| 65 |
+
</button>
|
| 66 |
+
|
| 67 |
+
{showUserMenu && (
|
| 68 |
+
<div className="absolute right-0 mt-2 w-48 bg-[#1a1a1a] border border-white/10 rounded-lg shadow-xl py-1">
|
| 69 |
+
<div className="px-4 py-2 border-b border-white/10">
|
| 70 |
+
<p className="text-xs text-white/50">Signed in as</p>
|
| 71 |
+
<p className="text-sm text-white truncate">{user?.email}</p>
|
| 72 |
+
</div>
|
| 73 |
+
<button
|
| 74 |
+
onClick={async () => {
|
| 75 |
+
await signOut();
|
| 76 |
+
setShowUserMenu(false);
|
| 77 |
+
}}
|
| 78 |
+
className="w-full flex items-center gap-2 px-4 py-2 text-sm text-red-400 hover:bg-white/5 transition-colors"
|
| 79 |
+
>
|
| 80 |
+
<LogOut className="w-4 h-4" />
|
| 81 |
+
Sign Out
|
| 82 |
+
</button>
|
| 83 |
+
</div>
|
| 84 |
+
)}
|
| 85 |
+
</div>
|
| 86 |
+
) : (
|
| 87 |
+
<button
|
| 88 |
+
onClick={() => setView('auth')}
|
| 89 |
+
className="px-4 py-2 text-sm text-white/70 hover:text-white transition-colors"
|
| 90 |
+
>
|
| 91 |
+
Sign In
|
| 92 |
+
</button>
|
| 93 |
+
)}
|
| 94 |
+
|
| 95 |
+
<button
|
| 96 |
+
onClick={handleLaunchConsole}
|
| 97 |
+
className="px-5 py-2 bg-white/5 hover:bg-white/10 border border-white/10 rounded-lg text-sm font-medium transition-all"
|
| 98 |
+
>
|
| 99 |
+
Launch Console
|
| 100 |
+
</button>
|
| 101 |
+
</div>
|
| 102 |
</nav>
|
| 103 |
|
| 104 |
<main>
|
| 105 |
+
<HeroGeometric onChatClick={handleLaunchConsole} />
|
| 106 |
<TechStack />
|
| 107 |
<ProblemSolution />
|
| 108 |
<KeyCapabilities />
|
|
|
|
| 121 |
);
|
| 122 |
};
|
| 123 |
|
| 124 |
+
// Wrap with AuthProvider
|
| 125 |
+
const App: React.FC = () => {
|
| 126 |
+
return (
|
| 127 |
+
<AuthProvider>
|
| 128 |
+
<AppContent />
|
| 129 |
+
</AuthProvider>
|
| 130 |
+
);
|
| 131 |
+
};
|
| 132 |
+
|
| 133 |
export default App;
|
FRRONTEEEND/SUPABASE_SETUP.md
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Supabase Authentication & Analytics Setup
|
| 2 |
+
|
| 3 |
+
## Quick Setup (5 minutes)
|
| 4 |
+
|
| 5 |
+
### 1. Create Supabase Project
|
| 6 |
+
1. Go to [https://app.supabase.com/](https://app.supabase.com/)
|
| 7 |
+
2. Click "New Project"
|
| 8 |
+
3. Choose organization, name your project (e.g., "ds-agent-analytics")
|
| 9 |
+
4. Set a strong database password (save it!)
|
| 10 |
+
5. Choose a region close to your users
|
| 11 |
+
6. Click "Create new project" and wait ~2 minutes
|
| 12 |
+
|
| 13 |
+
### 2. Get Your API Keys
|
| 14 |
+
1. Go to **Settings** → **API**
|
| 15 |
+
2. Copy:
|
| 16 |
+
- **Project URL**: `https://xxxxx.supabase.co`
|
| 17 |
+
- **anon/public key**: `eyJhbGciOi...` (long string)
|
| 18 |
+
|
| 19 |
+
### 3. Configure Environment
|
| 20 |
+
Create `.env` file in `FRRONTEEEND/`:
|
| 21 |
+
```bash
|
| 22 |
+
VITE_SUPABASE_URL=https://your-project-id.supabase.co
|
| 23 |
+
VITE_SUPABASE_ANON_KEY=your-anon-key-here
|
| 24 |
+
```
|
| 25 |
+
|
| 26 |
+
For HuggingFace Spaces, add these as **Secrets**:
|
| 27 |
+
1. Go to your Space → Settings → Repository secrets
|
| 28 |
+
2. Add `VITE_SUPABASE_URL` and `VITE_SUPABASE_ANON_KEY`
|
| 29 |
+
|
| 30 |
+
### 4. Set Up Database Tables
|
| 31 |
+
1. Go to **SQL Editor** in Supabase dashboard
|
| 32 |
+
2. Copy contents of `supabase_schema.sql`
|
| 33 |
+
3. Click "Run" to create tables and policies
|
| 34 |
+
|
| 35 |
+
### 5. Enable Authentication Providers (Optional)
|
| 36 |
+
|
| 37 |
+
#### Email (enabled by default)
|
| 38 |
+
- Works out of the box
|
| 39 |
+
|
| 40 |
+
#### Google OAuth
|
| 41 |
+
1. Go to **Authentication** → **Providers** → **Google**
|
| 42 |
+
2. Enable it
|
| 43 |
+
3. Create OAuth credentials at [Google Cloud Console](https://console.cloud.google.com/apis/credentials)
|
| 44 |
+
4. Add your Supabase callback URL: `https://your-project.supabase.co/auth/v1/callback`
|
| 45 |
+
5. Copy Client ID and Secret to Supabase
|
| 46 |
+
|
| 47 |
+
#### GitHub OAuth
|
| 48 |
+
1. Go to **Authentication** → **Providers** → **GitHub**
|
| 49 |
+
2. Enable it
|
| 50 |
+
3. Create OAuth App at [GitHub Developer Settings](https://github.com/settings/developers)
|
| 51 |
+
4. Add callback URL: `https://your-project.supabase.co/auth/v1/callback`
|
| 52 |
+
5. Copy Client ID and Secret to Supabase
|
| 53 |
+
|
| 54 |
+
## Features Included
|
| 55 |
+
|
| 56 |
+
### Authentication
|
| 57 |
+
- ✅ Email/Password sign up & sign in
|
| 58 |
+
- ✅ Google OAuth (optional)
|
| 59 |
+
- ✅ GitHub OAuth (optional)
|
| 60 |
+
- ✅ Persistent sessions
|
| 61 |
+
- ✅ Guest mode (can use without signing in)
|
| 62 |
+
|
| 63 |
+
### Analytics Tracking
|
| 64 |
+
- ✅ Per-query tracking (user, session, query text, success/failure)
|
| 65 |
+
- ✅ Session tracking (start time, end time, query count)
|
| 66 |
+
- ✅ Browser info capture
|
| 67 |
+
- ✅ Anonymous user support
|
| 68 |
+
|
| 69 |
+
### Dashboard Views (in Supabase)
|
| 70 |
+
- `daily_active_users` - DAU metrics
|
| 71 |
+
- `popular_queries` - Most common queries
|
| 72 |
+
- `agent_usage_stats` - Which agents are used most
|
| 73 |
+
|
| 74 |
+
## Viewing Analytics
|
| 75 |
+
|
| 76 |
+
### Quick Stats in Supabase
|
| 77 |
+
1. Go to **Table Editor**
|
| 78 |
+
2. Select `usage_analytics` or `user_sessions`
|
| 79 |
+
3. Use filters and sorting to analyze data
|
| 80 |
+
|
| 81 |
+
### SQL Queries
|
| 82 |
+
```sql
|
| 83 |
+
-- Total users last 7 days
|
| 84 |
+
SELECT COUNT(DISTINCT user_id)
|
| 85 |
+
FROM usage_analytics
|
| 86 |
+
WHERE created_at > NOW() - INTERVAL '7 days';
|
| 87 |
+
|
| 88 |
+
-- Queries per day
|
| 89 |
+
SELECT DATE(created_at), COUNT(*)
|
| 90 |
+
FROM usage_analytics
|
| 91 |
+
GROUP BY DATE(created_at)
|
| 92 |
+
ORDER BY 1 DESC;
|
| 93 |
+
|
| 94 |
+
-- Success rate
|
| 95 |
+
SELECT
|
| 96 |
+
SUM(CASE WHEN success THEN 1 ELSE 0 END)::float / COUNT(*) * 100 as success_rate
|
| 97 |
+
FROM usage_analytics;
|
| 98 |
+
```
|
| 99 |
+
|
| 100 |
+
## Free Tier Limits (Plenty for demos!)
|
| 101 |
+
- 50,000 monthly active users
|
| 102 |
+
- 500 MB database storage
|
| 103 |
+
- 1 GB file storage
|
| 104 |
+
- Unlimited API requests
|
| 105 |
+
- 2 projects
|
| 106 |
+
|
| 107 |
+
## Troubleshooting
|
| 108 |
+
|
| 109 |
+
### "Invalid API key"
|
| 110 |
+
- Check that your `.env` file has the correct values
|
| 111 |
+
- Make sure you're using the `anon` key, not the `service_role` key
|
| 112 |
+
|
| 113 |
+
### OAuth not working
|
| 114 |
+
- Verify callback URL is correct in provider settings
|
| 115 |
+
- Check that the provider is enabled in Supabase
|
| 116 |
+
|
| 117 |
+
### Data not appearing
|
| 118 |
+
- Check browser console for errors
|
| 119 |
+
- Verify RLS policies are created (run the SQL schema)
|
| 120 |
+
- Check Supabase logs for errors
|
FRRONTEEEND/components/AuthPage.tsx
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState } from 'react';
|
| 2 |
+
import { motion, AnimatePresence } from 'framer-motion';
|
| 3 |
+
import { Mail, Lock, User, Github, ArrowLeft, Loader2, AlertCircle, CheckCircle } from 'lucide-react';
|
| 4 |
+
import { useAuth } from '../lib/AuthContext';
|
| 5 |
+
import { Logo } from './Logo';
|
| 6 |
+
|
| 7 |
+
interface AuthPageProps {
|
| 8 |
+
onSuccess?: () => void;
|
| 9 |
+
onSkip?: () => void;
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
export const AuthPage: React.FC<AuthPageProps> = ({ onSuccess, onSkip }) => {
|
| 13 |
+
const { signIn, signUp, signInWithGoogle, signInWithGithub } = useAuth();
|
| 14 |
+
const [mode, setMode] = useState<'signin' | 'signup'>('signin');
|
| 15 |
+
const [email, setEmail] = useState('');
|
| 16 |
+
const [password, setPassword] = useState('');
|
| 17 |
+
const [loading, setLoading] = useState(false);
|
| 18 |
+
const [error, setError] = useState<string | null>(null);
|
| 19 |
+
const [success, setSuccess] = useState<string | null>(null);
|
| 20 |
+
|
| 21 |
+
const handleSubmit = async (e: React.FormEvent) => {
|
| 22 |
+
e.preventDefault();
|
| 23 |
+
setLoading(true);
|
| 24 |
+
setError(null);
|
| 25 |
+
setSuccess(null);
|
| 26 |
+
|
| 27 |
+
try {
|
| 28 |
+
if (mode === 'signup') {
|
| 29 |
+
const { error } = await signUp(email, password);
|
| 30 |
+
if (error) {
|
| 31 |
+
setError(error.message);
|
| 32 |
+
} else {
|
| 33 |
+
setSuccess('Check your email to confirm your account!');
|
| 34 |
+
}
|
| 35 |
+
} else {
|
| 36 |
+
const { error } = await signIn(email, password);
|
| 37 |
+
if (error) {
|
| 38 |
+
setError(error.message);
|
| 39 |
+
} else {
|
| 40 |
+
onSuccess?.();
|
| 41 |
+
}
|
| 42 |
+
}
|
| 43 |
+
} catch (err: any) {
|
| 44 |
+
setError(err.message || 'An error occurred');
|
| 45 |
+
} finally {
|
| 46 |
+
setLoading(false);
|
| 47 |
+
}
|
| 48 |
+
};
|
| 49 |
+
|
| 50 |
+
const handleOAuthSignIn = async (provider: 'google' | 'github') => {
|
| 51 |
+
setLoading(true);
|
| 52 |
+
setError(null);
|
| 53 |
+
|
| 54 |
+
try {
|
| 55 |
+
const { error } = provider === 'google' ? await signInWithGoogle() : await signInWithGithub();
|
| 56 |
+
if (error) {
|
| 57 |
+
setError(error.message);
|
| 58 |
+
}
|
| 59 |
+
} catch (err: any) {
|
| 60 |
+
setError(err.message || 'An error occurred');
|
| 61 |
+
} finally {
|
| 62 |
+
setLoading(false);
|
| 63 |
+
}
|
| 64 |
+
};
|
| 65 |
+
|
| 66 |
+
return (
|
| 67 |
+
<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center p-4">
|
| 68 |
+
{/* Background gradient */}
|
| 69 |
+
<div className="absolute inset-0 overflow-hidden">
|
| 70 |
+
<div className="absolute -top-40 -right-40 w-80 h-80 bg-indigo-500/10 rounded-full blur-3xl" />
|
| 71 |
+
<div className="absolute -bottom-40 -left-40 w-80 h-80 bg-purple-500/10 rounded-full blur-3xl" />
|
| 72 |
+
</div>
|
| 73 |
+
|
| 74 |
+
<motion.div
|
| 75 |
+
initial={{ opacity: 0, y: 20 }}
|
| 76 |
+
animate={{ opacity: 1, y: 0 }}
|
| 77 |
+
className="relative w-full max-w-md"
|
| 78 |
+
>
|
| 79 |
+
{/* Card */}
|
| 80 |
+
<div className="bg-white/[0.03] border border-white/10 rounded-2xl p-8 backdrop-blur-xl shadow-2xl">
|
| 81 |
+
{/* Logo */}
|
| 82 |
+
<div className="flex justify-center mb-6">
|
| 83 |
+
<Logo />
|
| 84 |
+
</div>
|
| 85 |
+
|
| 86 |
+
{/* Title */}
|
| 87 |
+
<div className="text-center mb-8">
|
| 88 |
+
<h1 className="text-2xl font-bold text-white mb-2">
|
| 89 |
+
{mode === 'signin' ? 'Welcome Back' : 'Create Account'}
|
| 90 |
+
</h1>
|
| 91 |
+
<p className="text-white/50 text-sm">
|
| 92 |
+
{mode === 'signin'
|
| 93 |
+
? 'Sign in to access your data science workflows'
|
| 94 |
+
: 'Join us to start analyzing your data'}
|
| 95 |
+
</p>
|
| 96 |
+
</div>
|
| 97 |
+
|
| 98 |
+
{/* OAuth Buttons */}
|
| 99 |
+
<div className="space-y-3 mb-6">
|
| 100 |
+
<button
|
| 101 |
+
onClick={() => handleOAuthSignIn('google')}
|
| 102 |
+
disabled={loading}
|
| 103 |
+
className="w-full flex items-center justify-center gap-3 px-4 py-3 bg-white/5 hover:bg-white/10 border border-white/10 rounded-xl transition-all text-white font-medium disabled:opacity-50"
|
| 104 |
+
>
|
| 105 |
+
<svg className="w-5 h-5" viewBox="0 0 24 24">
|
| 106 |
+
<path
|
| 107 |
+
fill="currentColor"
|
| 108 |
+
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
|
| 109 |
+
/>
|
| 110 |
+
<path
|
| 111 |
+
fill="currentColor"
|
| 112 |
+
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
|
| 113 |
+
/>
|
| 114 |
+
<path
|
| 115 |
+
fill="currentColor"
|
| 116 |
+
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
|
| 117 |
+
/>
|
| 118 |
+
<path
|
| 119 |
+
fill="currentColor"
|
| 120 |
+
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
|
| 121 |
+
/>
|
| 122 |
+
</svg>
|
| 123 |
+
Continue with Google
|
| 124 |
+
</button>
|
| 125 |
+
|
| 126 |
+
<button
|
| 127 |
+
onClick={() => handleOAuthSignIn('github')}
|
| 128 |
+
disabled={loading}
|
| 129 |
+
className="w-full flex items-center justify-center gap-3 px-4 py-3 bg-white/5 hover:bg-white/10 border border-white/10 rounded-xl transition-all text-white font-medium disabled:opacity-50"
|
| 130 |
+
>
|
| 131 |
+
<Github className="w-5 h-5" />
|
| 132 |
+
Continue with GitHub
|
| 133 |
+
</button>
|
| 134 |
+
</div>
|
| 135 |
+
|
| 136 |
+
{/* Divider */}
|
| 137 |
+
<div className="relative mb-6">
|
| 138 |
+
<div className="absolute inset-0 flex items-center">
|
| 139 |
+
<div className="w-full border-t border-white/10"></div>
|
| 140 |
+
</div>
|
| 141 |
+
<div className="relative flex justify-center text-sm">
|
| 142 |
+
<span className="px-4 bg-[#0a0a0a] text-white/40">or continue with email</span>
|
| 143 |
+
</div>
|
| 144 |
+
</div>
|
| 145 |
+
|
| 146 |
+
{/* Form */}
|
| 147 |
+
<form onSubmit={handleSubmit} className="space-y-4">
|
| 148 |
+
<div>
|
| 149 |
+
<label className="block text-sm font-medium text-white/70 mb-2">Email</label>
|
| 150 |
+
<div className="relative">
|
| 151 |
+
<Mail className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-white/30" />
|
| 152 |
+
<input
|
| 153 |
+
type="email"
|
| 154 |
+
value={email}
|
| 155 |
+
onChange={(e) => setEmail(e.target.value)}
|
| 156 |
+
placeholder="you@example.com"
|
| 157 |
+
required
|
| 158 |
+
className="w-full pl-11 pr-4 py-3 bg-white/5 border border-white/10 rounded-xl text-white placeholder-white/30 focus:outline-none focus:border-indigo-500/50 focus:ring-1 focus:ring-indigo-500/50 transition-all"
|
| 159 |
+
/>
|
| 160 |
+
</div>
|
| 161 |
+
</div>
|
| 162 |
+
|
| 163 |
+
<div>
|
| 164 |
+
<label className="block text-sm font-medium text-white/70 mb-2">Password</label>
|
| 165 |
+
<div className="relative">
|
| 166 |
+
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-white/30" />
|
| 167 |
+
<input
|
| 168 |
+
type="password"
|
| 169 |
+
value={password}
|
| 170 |
+
onChange={(e) => setPassword(e.target.value)}
|
| 171 |
+
placeholder="••••••••"
|
| 172 |
+
required
|
| 173 |
+
minLength={6}
|
| 174 |
+
className="w-full pl-11 pr-4 py-3 bg-white/5 border border-white/10 rounded-xl text-white placeholder-white/30 focus:outline-none focus:border-indigo-500/50 focus:ring-1 focus:ring-indigo-500/50 transition-all"
|
| 175 |
+
/>
|
| 176 |
+
</div>
|
| 177 |
+
</div>
|
| 178 |
+
|
| 179 |
+
{/* Error Message */}
|
| 180 |
+
<AnimatePresence>
|
| 181 |
+
{error && (
|
| 182 |
+
<motion.div
|
| 183 |
+
initial={{ opacity: 0, y: -10 }}
|
| 184 |
+
animate={{ opacity: 1, y: 0 }}
|
| 185 |
+
exit={{ opacity: 0 }}
|
| 186 |
+
className="flex items-center gap-2 p-3 bg-red-500/10 border border-red-500/20 rounded-xl text-red-400 text-sm"
|
| 187 |
+
>
|
| 188 |
+
<AlertCircle className="w-4 h-4 flex-shrink-0" />
|
| 189 |
+
{error}
|
| 190 |
+
</motion.div>
|
| 191 |
+
)}
|
| 192 |
+
</AnimatePresence>
|
| 193 |
+
|
| 194 |
+
{/* Success Message */}
|
| 195 |
+
<AnimatePresence>
|
| 196 |
+
{success && (
|
| 197 |
+
<motion.div
|
| 198 |
+
initial={{ opacity: 0, y: -10 }}
|
| 199 |
+
animate={{ opacity: 1, y: 0 }}
|
| 200 |
+
exit={{ opacity: 0 }}
|
| 201 |
+
className="flex items-center gap-2 p-3 bg-green-500/10 border border-green-500/20 rounded-xl text-green-400 text-sm"
|
| 202 |
+
>
|
| 203 |
+
<CheckCircle className="w-4 h-4 flex-shrink-0" />
|
| 204 |
+
{success}
|
| 205 |
+
</motion.div>
|
| 206 |
+
)}
|
| 207 |
+
</AnimatePresence>
|
| 208 |
+
|
| 209 |
+
{/* Submit Button */}
|
| 210 |
+
<button
|
| 211 |
+
type="submit"
|
| 212 |
+
disabled={loading}
|
| 213 |
+
className="w-full py-3 bg-gradient-to-r from-indigo-600 to-purple-600 hover:from-indigo-500 hover:to-purple-500 text-white font-medium rounded-xl transition-all disabled:opacity-50 flex items-center justify-center gap-2"
|
| 214 |
+
>
|
| 215 |
+
{loading ? (
|
| 216 |
+
<Loader2 className="w-5 h-5 animate-spin" />
|
| 217 |
+
) : mode === 'signin' ? (
|
| 218 |
+
'Sign In'
|
| 219 |
+
) : (
|
| 220 |
+
'Create Account'
|
| 221 |
+
)}
|
| 222 |
+
</button>
|
| 223 |
+
</form>
|
| 224 |
+
|
| 225 |
+
{/* Toggle Mode */}
|
| 226 |
+
<div className="mt-6 text-center">
|
| 227 |
+
<p className="text-white/50 text-sm">
|
| 228 |
+
{mode === 'signin' ? "Don't have an account? " : 'Already have an account? '}
|
| 229 |
+
<button
|
| 230 |
+
onClick={() => {
|
| 231 |
+
setMode(mode === 'signin' ? 'signup' : 'signin');
|
| 232 |
+
setError(null);
|
| 233 |
+
setSuccess(null);
|
| 234 |
+
}}
|
| 235 |
+
className="text-indigo-400 hover:text-indigo-300 font-medium"
|
| 236 |
+
>
|
| 237 |
+
{mode === 'signin' ? 'Sign Up' : 'Sign In'}
|
| 238 |
+
</button>
|
| 239 |
+
</p>
|
| 240 |
+
</div>
|
| 241 |
+
|
| 242 |
+
{/* Skip Button */}
|
| 243 |
+
{onSkip && (
|
| 244 |
+
<div className="mt-4 text-center">
|
| 245 |
+
<button
|
| 246 |
+
onClick={onSkip}
|
| 247 |
+
className="text-white/30 hover:text-white/50 text-sm transition-colors"
|
| 248 |
+
>
|
| 249 |
+
Continue without signing in →
|
| 250 |
+
</button>
|
| 251 |
+
</div>
|
| 252 |
+
)}
|
| 253 |
+
</div>
|
| 254 |
+
|
| 255 |
+
{/* Footer */}
|
| 256 |
+
<p className="text-center text-white/30 text-xs mt-6">
|
| 257 |
+
By signing in, you agree to our Terms of Service and Privacy Policy
|
| 258 |
+
</p>
|
| 259 |
+
</motion.div>
|
| 260 |
+
</div>
|
| 261 |
+
);
|
| 262 |
+
};
|
FRRONTEEEND/components/ChatInterface.tsx
CHANGED
|
@@ -1,10 +1,12 @@
|
|
| 1 |
|
| 2 |
import React, { useState, useRef, useEffect } from 'react';
|
| 3 |
import { motion, AnimatePresence } from 'framer-motion';
|
| 4 |
-
import { Send, Plus, Search, Settings, MoreHorizontal, User, Bot, ArrowLeft, Paperclip, Sparkles, Trash2, X, Upload, Package, FileText, BarChart3, ChevronRight } from 'lucide-react';
|
| 5 |
import { cn } from '../lib/utils';
|
| 6 |
import { Logo } from './Logo';
|
| 7 |
import ReactMarkdown from 'react-markdown';
|
|
|
|
|
|
|
| 8 |
|
| 9 |
interface Message {
|
| 10 |
id: string;
|
|
@@ -106,6 +108,9 @@ export const ChatInterface: React.FC<{ onBack: () => void }> = ({ onBack }) => {
|
|
| 106 |
const eventSourceRef = useRef<EventSource | null>(null);
|
| 107 |
const processedAnalysisRef = useRef<Set<string>>(new Set()); // Track processed analysis_complete events
|
| 108 |
|
|
|
|
|
|
|
|
|
|
| 109 |
const activeSession = sessions.find(s => s.id === activeSessionId) || sessions[0];
|
| 110 |
|
| 111 |
// Persist sessions to localStorage whenever they change
|
|
@@ -409,11 +414,30 @@ export const ChatInterface: React.FC<{ onBack: () => void }> = ({ onBack }) => {
|
|
| 409 |
formData.append('use_cache', 'false'); // Disabled to show multi-agent execution
|
| 410 |
formData.append('max_iterations', '20');
|
| 411 |
|
|
|
|
|
|
|
|
|
|
| 412 |
response = await fetch(`${API_URL}/run-async`, {
|
| 413 |
method: 'POST',
|
| 414 |
body: formData
|
| 415 |
});
|
| 416 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 417 |
setUploadedFile(null);
|
| 418 |
} else {
|
| 419 |
// No file and no backend session - use simple chat endpoint
|
|
@@ -709,17 +733,46 @@ export const ChatInterface: React.FC<{ onBack: () => void }> = ({ onBack }) => {
|
|
| 709 |
))}
|
| 710 |
</div>
|
| 711 |
|
| 712 |
-
<div className="mt-auto pt-4 border-t border-white/5
|
| 713 |
-
|
| 714 |
-
|
| 715 |
-
|
| 716 |
-
|
| 717 |
-
|
| 718 |
-
<
|
| 719 |
-
|
| 720 |
-
|
| 721 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 722 |
</button>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 723 |
</div>
|
| 724 |
</div>
|
| 725 |
</div>
|
|
|
|
| 1 |
|
| 2 |
import React, { useState, useRef, useEffect } from 'react';
|
| 3 |
import { motion, AnimatePresence } from 'framer-motion';
|
| 4 |
+
import { Send, Plus, Search, Settings, MoreHorizontal, User, Bot, ArrowLeft, Paperclip, Sparkles, Trash2, X, Upload, Package, FileText, BarChart3, ChevronRight, LogOut } from 'lucide-react';
|
| 5 |
import { cn } from '../lib/utils';
|
| 6 |
import { Logo } from './Logo';
|
| 7 |
import ReactMarkdown from 'react-markdown';
|
| 8 |
+
import { useAuth } from '../lib/AuthContext';
|
| 9 |
+
import { trackQuery, incrementSessionQueries } from '../lib/supabase';
|
| 10 |
|
| 11 |
interface Message {
|
| 12 |
id: string;
|
|
|
|
| 108 |
const eventSourceRef = useRef<EventSource | null>(null);
|
| 109 |
const processedAnalysisRef = useRef<Set<string>>(new Set()); // Track processed analysis_complete events
|
| 110 |
|
| 111 |
+
// Auth context for user tracking
|
| 112 |
+
const { user, isAuthenticated, dbSessionId, signOut } = useAuth();
|
| 113 |
+
|
| 114 |
const activeSession = sessions.find(s => s.id === activeSessionId) || sessions[0];
|
| 115 |
|
| 116 |
// Persist sessions to localStorage whenever they change
|
|
|
|
| 414 |
formData.append('use_cache', 'false'); // Disabled to show multi-agent execution
|
| 415 |
formData.append('max_iterations', '20');
|
| 416 |
|
| 417 |
+
// Track query start time for analytics
|
| 418 |
+
const queryStartTime = Date.now();
|
| 419 |
+
|
| 420 |
response = await fetch(`${API_URL}/run-async`, {
|
| 421 |
method: 'POST',
|
| 422 |
body: formData
|
| 423 |
});
|
| 424 |
|
| 425 |
+
// 📊 Track analytics (non-blocking)
|
| 426 |
+
if (user || dbSessionId) {
|
| 427 |
+
trackQuery({
|
| 428 |
+
user_id: user?.id || 'anonymous',
|
| 429 |
+
user_email: user?.email,
|
| 430 |
+
session_id: sessionKey,
|
| 431 |
+
query: input || 'File analysis',
|
| 432 |
+
success: response.ok,
|
| 433 |
+
error_message: response.ok ? undefined : `HTTP ${response.status}`
|
| 434 |
+
}).catch(console.error);
|
| 435 |
+
|
| 436 |
+
if (dbSessionId) {
|
| 437 |
+
incrementSessionQueries(dbSessionId).catch(console.error);
|
| 438 |
+
}
|
| 439 |
+
}
|
| 440 |
+
|
| 441 |
setUploadedFile(null);
|
| 442 |
} else {
|
| 443 |
// No file and no backend session - use simple chat endpoint
|
|
|
|
| 733 |
))}
|
| 734 |
</div>
|
| 735 |
|
| 736 |
+
<div className="mt-auto pt-4 border-t border-white/5">
|
| 737 |
+
{/* User info section */}
|
| 738 |
+
{isAuthenticated && user ? (
|
| 739 |
+
<div className="flex items-center gap-3 px-2 py-2 mb-2">
|
| 740 |
+
<div className="w-8 h-8 rounded-full bg-gradient-to-br from-indigo-500 to-purple-600 flex items-center justify-center text-xs font-bold">
|
| 741 |
+
{user.email?.[0]?.toUpperCase() || 'U'}
|
| 742 |
+
</div>
|
| 743 |
+
<div className="flex-1 min-w-0">
|
| 744 |
+
<p className="text-sm text-white truncate">{user.email?.split('@')[0]}</p>
|
| 745 |
+
<p className="text-xs text-white/40 truncate">{user.email}</p>
|
| 746 |
+
</div>
|
| 747 |
+
<button
|
| 748 |
+
onClick={() => signOut()}
|
| 749 |
+
className="p-2 hover:bg-white/5 rounded-lg transition-colors text-white/40 hover:text-red-400"
|
| 750 |
+
title="Sign Out"
|
| 751 |
+
>
|
| 752 |
+
<LogOut className="w-4 h-4" />
|
| 753 |
+
</button>
|
| 754 |
+
</div>
|
| 755 |
+
) : (
|
| 756 |
+
<div className="flex items-center gap-3 px-2 py-2 mb-2">
|
| 757 |
+
<div className="w-8 h-8 rounded-full bg-white/10 flex items-center justify-center">
|
| 758 |
+
<User className="w-4 h-4 text-white/50" />
|
| 759 |
+
</div>
|
| 760 |
+
<div className="flex-1">
|
| 761 |
+
<p className="text-sm text-white/50">Guest User</p>
|
| 762 |
+
<p className="text-xs text-white/30">Sign in to save history</p>
|
| 763 |
+
</div>
|
| 764 |
+
</div>
|
| 765 |
+
)}
|
| 766 |
+
|
| 767 |
+
<div className="flex items-center justify-between px-2">
|
| 768 |
+
<button onClick={onBack} className="p-2 hover:bg-white/5 rounded-lg transition-colors text-white/40 hover:text-white">
|
| 769 |
+
<ArrowLeft className="w-5 h-5" />
|
| 770 |
</button>
|
| 771 |
+
<div className="flex gap-2">
|
| 772 |
+
<button className="p-2 hover:bg-white/5 rounded-lg transition-colors text-white/40 hover:text-white">
|
| 773 |
+
<Settings className="w-5 h-5" />
|
| 774 |
+
</button>
|
| 775 |
+
</div>
|
| 776 |
</div>
|
| 777 |
</div>
|
| 778 |
</div>
|
FRRONTEEEND/lib/AuthContext.tsx
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { createContext, useContext, useEffect, useState } from 'react';
|
| 2 |
+
import { User, Session, AuthChangeEvent } from '@supabase/supabase-js';
|
| 3 |
+
import { supabase, startUserSession, endUserSession } from './supabase';
|
| 4 |
+
|
| 5 |
+
interface AuthContextType {
|
| 6 |
+
user: User | null;
|
| 7 |
+
session: Session | null;
|
| 8 |
+
dbSessionId: string | null;
|
| 9 |
+
loading: boolean;
|
| 10 |
+
signIn: (email: string, password: string) => Promise<{ error: any }>;
|
| 11 |
+
signUp: (email: string, password: string) => Promise<{ error: any }>;
|
| 12 |
+
signInWithGoogle: () => Promise<{ error: any }>;
|
| 13 |
+
signInWithGithub: () => Promise<{ error: any }>;
|
| 14 |
+
signOut: () => Promise<void>;
|
| 15 |
+
isAuthenticated: boolean;
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
| 19 |
+
|
| 20 |
+
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
| 21 |
+
const [user, setUser] = useState<User | null>(null);
|
| 22 |
+
const [session, setSession] = useState<Session | null>(null);
|
| 23 |
+
const [dbSessionId, setDbSessionId] = useState<string | null>(null);
|
| 24 |
+
const [loading, setLoading] = useState(true);
|
| 25 |
+
|
| 26 |
+
useEffect(() => {
|
| 27 |
+
// Get initial session
|
| 28 |
+
supabase.auth.getSession().then(({ data: { session } }) => {
|
| 29 |
+
setSession(session);
|
| 30 |
+
setUser(session?.user ?? null);
|
| 31 |
+
setLoading(false);
|
| 32 |
+
|
| 33 |
+
// Start tracking session if user is logged in
|
| 34 |
+
if (session?.user) {
|
| 35 |
+
startUserSession(session.user.id, session.user.email).then((dbSession) => {
|
| 36 |
+
if (dbSession) {
|
| 37 |
+
setDbSessionId(dbSession.id);
|
| 38 |
+
}
|
| 39 |
+
});
|
| 40 |
+
}
|
| 41 |
+
});
|
| 42 |
+
|
| 43 |
+
// Listen for auth changes
|
| 44 |
+
const { data: { subscription } } = supabase.auth.onAuthStateChange(
|
| 45 |
+
async (event: AuthChangeEvent, session) => {
|
| 46 |
+
setSession(session);
|
| 47 |
+
setUser(session?.user ?? null);
|
| 48 |
+
setLoading(false);
|
| 49 |
+
|
| 50 |
+
if (event === 'SIGNED_IN' && session?.user) {
|
| 51 |
+
// Start new tracking session
|
| 52 |
+
const dbSession = await startUserSession(session.user.id, session.user.email);
|
| 53 |
+
if (dbSession) {
|
| 54 |
+
setDbSessionId(dbSession.id);
|
| 55 |
+
}
|
| 56 |
+
} else if (event === 'SIGNED_OUT') {
|
| 57 |
+
// End tracking session
|
| 58 |
+
if (dbSessionId) {
|
| 59 |
+
await endUserSession(dbSessionId);
|
| 60 |
+
setDbSessionId(null);
|
| 61 |
+
}
|
| 62 |
+
}
|
| 63 |
+
}
|
| 64 |
+
);
|
| 65 |
+
|
| 66 |
+
// Cleanup on unmount
|
| 67 |
+
return () => {
|
| 68 |
+
subscription.unsubscribe();
|
| 69 |
+
// End session when component unmounts
|
| 70 |
+
if (dbSessionId) {
|
| 71 |
+
endUserSession(dbSessionId);
|
| 72 |
+
}
|
| 73 |
+
};
|
| 74 |
+
}, []);
|
| 75 |
+
|
| 76 |
+
const signIn = async (email: string, password: string) => {
|
| 77 |
+
const { error } = await supabase.auth.signInWithPassword({ email, password });
|
| 78 |
+
return { error };
|
| 79 |
+
};
|
| 80 |
+
|
| 81 |
+
const signUp = async (email: string, password: string) => {
|
| 82 |
+
const { error } = await supabase.auth.signUp({ email, password });
|
| 83 |
+
return { error };
|
| 84 |
+
};
|
| 85 |
+
|
| 86 |
+
const signInWithGoogle = async () => {
|
| 87 |
+
const { error } = await supabase.auth.signInWithOAuth({
|
| 88 |
+
provider: 'google',
|
| 89 |
+
options: {
|
| 90 |
+
redirectTo: window.location.origin
|
| 91 |
+
}
|
| 92 |
+
});
|
| 93 |
+
return { error };
|
| 94 |
+
};
|
| 95 |
+
|
| 96 |
+
const signInWithGithub = async () => {
|
| 97 |
+
const { error } = await supabase.auth.signInWithOAuth({
|
| 98 |
+
provider: 'github',
|
| 99 |
+
options: {
|
| 100 |
+
redirectTo: window.location.origin
|
| 101 |
+
}
|
| 102 |
+
});
|
| 103 |
+
return { error };
|
| 104 |
+
};
|
| 105 |
+
|
| 106 |
+
const signOut = async () => {
|
| 107 |
+
if (dbSessionId) {
|
| 108 |
+
await endUserSession(dbSessionId);
|
| 109 |
+
setDbSessionId(null);
|
| 110 |
+
}
|
| 111 |
+
await supabase.auth.signOut();
|
| 112 |
+
};
|
| 113 |
+
|
| 114 |
+
return (
|
| 115 |
+
<AuthContext.Provider
|
| 116 |
+
value={{
|
| 117 |
+
user,
|
| 118 |
+
session,
|
| 119 |
+
dbSessionId,
|
| 120 |
+
loading,
|
| 121 |
+
signIn,
|
| 122 |
+
signUp,
|
| 123 |
+
signInWithGoogle,
|
| 124 |
+
signInWithGithub,
|
| 125 |
+
signOut,
|
| 126 |
+
isAuthenticated: !!user
|
| 127 |
+
}}
|
| 128 |
+
>
|
| 129 |
+
{children}
|
| 130 |
+
</AuthContext.Provider>
|
| 131 |
+
);
|
| 132 |
+
};
|
| 133 |
+
|
| 134 |
+
export const useAuth = () => {
|
| 135 |
+
const context = useContext(AuthContext);
|
| 136 |
+
if (context === undefined) {
|
| 137 |
+
throw new Error('useAuth must be used within an AuthProvider');
|
| 138 |
+
}
|
| 139 |
+
return context;
|
| 140 |
+
};
|
FRRONTEEEND/lib/supabase.ts
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { createClient } from '@supabase/supabase-js';
|
| 2 |
+
|
| 3 |
+
// Supabase configuration - these will be loaded from environment variables
|
| 4 |
+
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL || '';
|
| 5 |
+
const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY || '';
|
| 6 |
+
|
| 7 |
+
// Create Supabase client
|
| 8 |
+
export const supabase = createClient(supabaseUrl, supabaseAnonKey);
|
| 9 |
+
|
| 10 |
+
// Types for our analytics
|
| 11 |
+
export interface UsageAnalytics {
|
| 12 |
+
id?: string;
|
| 13 |
+
user_id: string;
|
| 14 |
+
user_email?: string;
|
| 15 |
+
session_id: string;
|
| 16 |
+
query: string;
|
| 17 |
+
agent_used?: string;
|
| 18 |
+
tools_executed?: string[];
|
| 19 |
+
tokens_used?: number;
|
| 20 |
+
duration_ms?: number;
|
| 21 |
+
success: boolean;
|
| 22 |
+
error_message?: string;
|
| 23 |
+
created_at?: string;
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
export interface UserSession {
|
| 27 |
+
id?: string;
|
| 28 |
+
user_id: string;
|
| 29 |
+
user_email?: string;
|
| 30 |
+
started_at: string;
|
| 31 |
+
ended_at?: string;
|
| 32 |
+
queries_count: number;
|
| 33 |
+
browser_info?: string;
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
// Analytics functions
|
| 37 |
+
export const trackQuery = async (analytics: Omit<UsageAnalytics, 'id' | 'created_at'>) => {
|
| 38 |
+
try {
|
| 39 |
+
const { data, error } = await supabase
|
| 40 |
+
.from('usage_analytics')
|
| 41 |
+
.insert([{
|
| 42 |
+
...analytics,
|
| 43 |
+
created_at: new Date().toISOString()
|
| 44 |
+
}]);
|
| 45 |
+
|
| 46 |
+
if (error) {
|
| 47 |
+
console.error('Failed to track query:', error);
|
| 48 |
+
return null;
|
| 49 |
+
}
|
| 50 |
+
return data;
|
| 51 |
+
} catch (err) {
|
| 52 |
+
console.error('Analytics tracking error:', err);
|
| 53 |
+
return null;
|
| 54 |
+
}
|
| 55 |
+
};
|
| 56 |
+
|
| 57 |
+
export const startUserSession = async (userId: string, userEmail?: string) => {
|
| 58 |
+
try {
|
| 59 |
+
const { data, error } = await supabase
|
| 60 |
+
.from('user_sessions')
|
| 61 |
+
.insert([{
|
| 62 |
+
user_id: userId,
|
| 63 |
+
user_email: userEmail,
|
| 64 |
+
started_at: new Date().toISOString(),
|
| 65 |
+
queries_count: 0,
|
| 66 |
+
browser_info: typeof navigator !== 'undefined' ? navigator.userAgent : null
|
| 67 |
+
}])
|
| 68 |
+
.select()
|
| 69 |
+
.single();
|
| 70 |
+
|
| 71 |
+
if (error) {
|
| 72 |
+
console.error('Failed to start session:', error);
|
| 73 |
+
return null;
|
| 74 |
+
}
|
| 75 |
+
return data;
|
| 76 |
+
} catch (err) {
|
| 77 |
+
console.error('Session tracking error:', err);
|
| 78 |
+
return null;
|
| 79 |
+
}
|
| 80 |
+
};
|
| 81 |
+
|
| 82 |
+
export const endUserSession = async (sessionId: string) => {
|
| 83 |
+
try {
|
| 84 |
+
const { error } = await supabase
|
| 85 |
+
.from('user_sessions')
|
| 86 |
+
.update({ ended_at: new Date().toISOString() })
|
| 87 |
+
.eq('id', sessionId);
|
| 88 |
+
|
| 89 |
+
if (error) {
|
| 90 |
+
console.error('Failed to end session:', error);
|
| 91 |
+
}
|
| 92 |
+
} catch (err) {
|
| 93 |
+
console.error('Session end error:', err);
|
| 94 |
+
}
|
| 95 |
+
};
|
| 96 |
+
|
| 97 |
+
export const incrementSessionQueries = async (sessionId: string) => {
|
| 98 |
+
try {
|
| 99 |
+
// Use RPC for atomic increment
|
| 100 |
+
const { error } = await supabase.rpc('increment_session_queries', {
|
| 101 |
+
session_id: sessionId
|
| 102 |
+
});
|
| 103 |
+
|
| 104 |
+
if (error) {
|
| 105 |
+
// Fallback: fetch and update
|
| 106 |
+
const { data } = await supabase
|
| 107 |
+
.from('user_sessions')
|
| 108 |
+
.select('queries_count')
|
| 109 |
+
.eq('id', sessionId)
|
| 110 |
+
.single();
|
| 111 |
+
|
| 112 |
+
if (data) {
|
| 113 |
+
await supabase
|
| 114 |
+
.from('user_sessions')
|
| 115 |
+
.update({ queries_count: (data.queries_count || 0) + 1 })
|
| 116 |
+
.eq('id', sessionId);
|
| 117 |
+
}
|
| 118 |
+
}
|
| 119 |
+
} catch (err) {
|
| 120 |
+
console.error('Failed to increment queries:', err);
|
| 121 |
+
}
|
| 122 |
+
};
|
| 123 |
+
|
| 124 |
+
// Get usage stats (for admin dashboard)
|
| 125 |
+
export const getUsageStats = async (days: number = 7) => {
|
| 126 |
+
try {
|
| 127 |
+
const startDate = new Date();
|
| 128 |
+
startDate.setDate(startDate.getDate() - days);
|
| 129 |
+
|
| 130 |
+
const { data, error } = await supabase
|
| 131 |
+
.from('usage_analytics')
|
| 132 |
+
.select('*')
|
| 133 |
+
.gte('created_at', startDate.toISOString())
|
| 134 |
+
.order('created_at', { ascending: false });
|
| 135 |
+
|
| 136 |
+
if (error) {
|
| 137 |
+
console.error('Failed to get stats:', error);
|
| 138 |
+
return null;
|
| 139 |
+
}
|
| 140 |
+
return data;
|
| 141 |
+
} catch (err) {
|
| 142 |
+
console.error('Stats fetch error:', err);
|
| 143 |
+
return null;
|
| 144 |
+
}
|
| 145 |
+
};
|
| 146 |
+
|
| 147 |
+
// Get unique users count
|
| 148 |
+
export const getUniqueUsersCount = async (days: number = 7) => {
|
| 149 |
+
try {
|
| 150 |
+
const startDate = new Date();
|
| 151 |
+
startDate.setDate(startDate.getDate() - days);
|
| 152 |
+
|
| 153 |
+
const { data, error } = await supabase
|
| 154 |
+
.from('user_sessions')
|
| 155 |
+
.select('user_id')
|
| 156 |
+
.gte('started_at', startDate.toISOString());
|
| 157 |
+
|
| 158 |
+
if (error) {
|
| 159 |
+
console.error('Failed to get unique users:', error);
|
| 160 |
+
return 0;
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
// Count unique user IDs
|
| 164 |
+
const uniqueUsers = new Set(data?.map(d => d.user_id));
|
| 165 |
+
return uniqueUsers.size;
|
| 166 |
+
} catch (err) {
|
| 167 |
+
console.error('Unique users fetch error:', err);
|
| 168 |
+
return 0;
|
| 169 |
+
}
|
| 170 |
+
};
|
FRRONTEEEND/package-lock.json
CHANGED
|
@@ -8,6 +8,7 @@
|
|
| 8 |
"name": "data-science-agent",
|
| 9 |
"version": "0.0.0",
|
| 10 |
"dependencies": {
|
|
|
|
| 11 |
"clsx": "^2.1.1",
|
| 12 |
"framer-motion": "^12.23.26",
|
| 13 |
"lucide-react": "^0.562.0",
|
|
@@ -1113,6 +1114,86 @@
|
|
| 1113 |
"win32"
|
| 1114 |
]
|
| 1115 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1116 |
"node_modules/@types/babel__core": {
|
| 1117 |
"version": "7.20.5",
|
| 1118 |
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
|
@@ -1210,13 +1291,17 @@
|
|
| 1210 |
"version": "22.19.3",
|
| 1211 |
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.3.tgz",
|
| 1212 |
"integrity": "sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA==",
|
| 1213 |
-
"dev": true,
|
| 1214 |
"license": "MIT",
|
| 1215 |
-
"peer": true,
|
| 1216 |
"dependencies": {
|
| 1217 |
"undici-types": "~6.21.0"
|
| 1218 |
}
|
| 1219 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1220 |
"node_modules/@types/react": {
|
| 1221 |
"version": "19.2.7",
|
| 1222 |
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz",
|
|
@@ -1233,6 +1318,15 @@
|
|
| 1233 |
"integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
|
| 1234 |
"license": "MIT"
|
| 1235 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1236 |
"node_modules/@ungap/structured-clone": {
|
| 1237 |
"version": "1.3.0",
|
| 1238 |
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
|
|
@@ -1665,6 +1759,15 @@
|
|
| 1665 |
"url": "https://opencollective.com/unified"
|
| 1666 |
}
|
| 1667 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1668 |
"node_modules/inline-style-parser": {
|
| 1669 |
"version": "0.2.7",
|
| 1670 |
"resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz",
|
|
@@ -2790,7 +2893,6 @@
|
|
| 2790 |
"version": "6.21.0",
|
| 2791 |
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
| 2792 |
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
| 2793 |
-
"dev": true,
|
| 2794 |
"license": "MIT"
|
| 2795 |
},
|
| 2796 |
"node_modules/unified": {
|
|
@@ -3015,6 +3117,27 @@
|
|
| 3015 |
}
|
| 3016 |
}
|
| 3017 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3018 |
"node_modules/yallist": {
|
| 3019 |
"version": "3.1.1",
|
| 3020 |
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
|
|
|
| 8 |
"name": "data-science-agent",
|
| 9 |
"version": "0.0.0",
|
| 10 |
"dependencies": {
|
| 11 |
+
"@supabase/supabase-js": "^2.93.3",
|
| 12 |
"clsx": "^2.1.1",
|
| 13 |
"framer-motion": "^12.23.26",
|
| 14 |
"lucide-react": "^0.562.0",
|
|
|
|
| 1114 |
"win32"
|
| 1115 |
]
|
| 1116 |
},
|
| 1117 |
+
"node_modules/@supabase/auth-js": {
|
| 1118 |
+
"version": "2.93.3",
|
| 1119 |
+
"resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.93.3.tgz",
|
| 1120 |
+
"integrity": "sha512-JdnkHZPKexVGSNONtu89RHU4bxz3X9kxx+f5ZnR5osoCIX+vs/MckwWRPZEybAEvlJXt5xjomDb3IB876QCxWQ==",
|
| 1121 |
+
"license": "MIT",
|
| 1122 |
+
"dependencies": {
|
| 1123 |
+
"tslib": "2.8.1"
|
| 1124 |
+
},
|
| 1125 |
+
"engines": {
|
| 1126 |
+
"node": ">=20.0.0"
|
| 1127 |
+
}
|
| 1128 |
+
},
|
| 1129 |
+
"node_modules/@supabase/functions-js": {
|
| 1130 |
+
"version": "2.93.3",
|
| 1131 |
+
"resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.93.3.tgz",
|
| 1132 |
+
"integrity": "sha512-qWO0gHNDm/5jRjROv/nv9L6sYabCWS1kzorOLUv3kqCwRvEJLYZga93ppJPrZwOgoZfXmJzvpjY8fODA4HQfBw==",
|
| 1133 |
+
"license": "MIT",
|
| 1134 |
+
"dependencies": {
|
| 1135 |
+
"tslib": "2.8.1"
|
| 1136 |
+
},
|
| 1137 |
+
"engines": {
|
| 1138 |
+
"node": ">=20.0.0"
|
| 1139 |
+
}
|
| 1140 |
+
},
|
| 1141 |
+
"node_modules/@supabase/postgrest-js": {
|
| 1142 |
+
"version": "2.93.3",
|
| 1143 |
+
"resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.93.3.tgz",
|
| 1144 |
+
"integrity": "sha512-+iJ96g94skO2e4clsRSmEXg22NUOjh9BziapsJSAvnB1grOBf/BA8vGtCHjNOA+Z6lvKXL1jwBqcL9+fS1W/Lg==",
|
| 1145 |
+
"license": "MIT",
|
| 1146 |
+
"dependencies": {
|
| 1147 |
+
"tslib": "2.8.1"
|
| 1148 |
+
},
|
| 1149 |
+
"engines": {
|
| 1150 |
+
"node": ">=20.0.0"
|
| 1151 |
+
}
|
| 1152 |
+
},
|
| 1153 |
+
"node_modules/@supabase/realtime-js": {
|
| 1154 |
+
"version": "2.93.3",
|
| 1155 |
+
"resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.93.3.tgz",
|
| 1156 |
+
"integrity": "sha512-gnYpcFzwy8IkezRP4CDbT5I8jOsiOjrWrqTY1B+7jIriXsnpifmlM6RRjLBm9oD7OwPG0/WksniGPdKW67sXOA==",
|
| 1157 |
+
"license": "MIT",
|
| 1158 |
+
"dependencies": {
|
| 1159 |
+
"@types/phoenix": "^1.6.6",
|
| 1160 |
+
"@types/ws": "^8.18.1",
|
| 1161 |
+
"tslib": "2.8.1",
|
| 1162 |
+
"ws": "^8.18.2"
|
| 1163 |
+
},
|
| 1164 |
+
"engines": {
|
| 1165 |
+
"node": ">=20.0.0"
|
| 1166 |
+
}
|
| 1167 |
+
},
|
| 1168 |
+
"node_modules/@supabase/storage-js": {
|
| 1169 |
+
"version": "2.93.3",
|
| 1170 |
+
"resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.93.3.tgz",
|
| 1171 |
+
"integrity": "sha512-cw4qXiLrx3apglDM02Tx/w/stvFlrkKocC6vCvuFAz3JtVEl1zH8MUfDQDTH59kJAQVaVdbewrMWSoBob7REnA==",
|
| 1172 |
+
"license": "MIT",
|
| 1173 |
+
"dependencies": {
|
| 1174 |
+
"iceberg-js": "^0.8.1",
|
| 1175 |
+
"tslib": "2.8.1"
|
| 1176 |
+
},
|
| 1177 |
+
"engines": {
|
| 1178 |
+
"node": ">=20.0.0"
|
| 1179 |
+
}
|
| 1180 |
+
},
|
| 1181 |
+
"node_modules/@supabase/supabase-js": {
|
| 1182 |
+
"version": "2.93.3",
|
| 1183 |
+
"resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.93.3.tgz",
|
| 1184 |
+
"integrity": "sha512-paUqEqdBI9ztr/4bbMoCgeJ6M8ZTm2fpfjSOlzarPuzYveKFM20ZfDZqUpi9CFfYagYj5Iv3m3ztUjaI9/tM1w==",
|
| 1185 |
+
"license": "MIT",
|
| 1186 |
+
"dependencies": {
|
| 1187 |
+
"@supabase/auth-js": "2.93.3",
|
| 1188 |
+
"@supabase/functions-js": "2.93.3",
|
| 1189 |
+
"@supabase/postgrest-js": "2.93.3",
|
| 1190 |
+
"@supabase/realtime-js": "2.93.3",
|
| 1191 |
+
"@supabase/storage-js": "2.93.3"
|
| 1192 |
+
},
|
| 1193 |
+
"engines": {
|
| 1194 |
+
"node": ">=20.0.0"
|
| 1195 |
+
}
|
| 1196 |
+
},
|
| 1197 |
"node_modules/@types/babel__core": {
|
| 1198 |
"version": "7.20.5",
|
| 1199 |
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
|
|
|
| 1291 |
"version": "22.19.3",
|
| 1292 |
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.3.tgz",
|
| 1293 |
"integrity": "sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA==",
|
|
|
|
| 1294 |
"license": "MIT",
|
|
|
|
| 1295 |
"dependencies": {
|
| 1296 |
"undici-types": "~6.21.0"
|
| 1297 |
}
|
| 1298 |
},
|
| 1299 |
+
"node_modules/@types/phoenix": {
|
| 1300 |
+
"version": "1.6.7",
|
| 1301 |
+
"resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.7.tgz",
|
| 1302 |
+
"integrity": "sha512-oN9ive//QSBkf19rfDv45M7eZPi0eEXylht2OLEXicu5b4KoQ1OzXIw+xDSGWxSxe1JmepRR/ZH283vsu518/Q==",
|
| 1303 |
+
"license": "MIT"
|
| 1304 |
+
},
|
| 1305 |
"node_modules/@types/react": {
|
| 1306 |
"version": "19.2.7",
|
| 1307 |
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz",
|
|
|
|
| 1318 |
"integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
|
| 1319 |
"license": "MIT"
|
| 1320 |
},
|
| 1321 |
+
"node_modules/@types/ws": {
|
| 1322 |
+
"version": "8.18.1",
|
| 1323 |
+
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
|
| 1324 |
+
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
|
| 1325 |
+
"license": "MIT",
|
| 1326 |
+
"dependencies": {
|
| 1327 |
+
"@types/node": "*"
|
| 1328 |
+
}
|
| 1329 |
+
},
|
| 1330 |
"node_modules/@ungap/structured-clone": {
|
| 1331 |
"version": "1.3.0",
|
| 1332 |
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
|
|
|
|
| 1759 |
"url": "https://opencollective.com/unified"
|
| 1760 |
}
|
| 1761 |
},
|
| 1762 |
+
"node_modules/iceberg-js": {
|
| 1763 |
+
"version": "0.8.1",
|
| 1764 |
+
"resolved": "https://registry.npmjs.org/iceberg-js/-/iceberg-js-0.8.1.tgz",
|
| 1765 |
+
"integrity": "sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA==",
|
| 1766 |
+
"license": "MIT",
|
| 1767 |
+
"engines": {
|
| 1768 |
+
"node": ">=20.0.0"
|
| 1769 |
+
}
|
| 1770 |
+
},
|
| 1771 |
"node_modules/inline-style-parser": {
|
| 1772 |
"version": "0.2.7",
|
| 1773 |
"resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz",
|
|
|
|
| 2893 |
"version": "6.21.0",
|
| 2894 |
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
| 2895 |
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
|
|
|
| 2896 |
"license": "MIT"
|
| 2897 |
},
|
| 2898 |
"node_modules/unified": {
|
|
|
|
| 3117 |
}
|
| 3118 |
}
|
| 3119 |
},
|
| 3120 |
+
"node_modules/ws": {
|
| 3121 |
+
"version": "8.19.0",
|
| 3122 |
+
"resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
|
| 3123 |
+
"integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
|
| 3124 |
+
"license": "MIT",
|
| 3125 |
+
"engines": {
|
| 3126 |
+
"node": ">=10.0.0"
|
| 3127 |
+
},
|
| 3128 |
+
"peerDependencies": {
|
| 3129 |
+
"bufferutil": "^4.0.1",
|
| 3130 |
+
"utf-8-validate": ">=5.0.2"
|
| 3131 |
+
},
|
| 3132 |
+
"peerDependenciesMeta": {
|
| 3133 |
+
"bufferutil": {
|
| 3134 |
+
"optional": true
|
| 3135 |
+
},
|
| 3136 |
+
"utf-8-validate": {
|
| 3137 |
+
"optional": true
|
| 3138 |
+
}
|
| 3139 |
+
}
|
| 3140 |
+
},
|
| 3141 |
"node_modules/yallist": {
|
| 3142 |
"version": "3.1.1",
|
| 3143 |
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
FRRONTEEEND/package.json
CHANGED
|
@@ -9,13 +9,14 @@
|
|
| 9 |
"preview": "vite preview"
|
| 10 |
},
|
| 11 |
"dependencies": {
|
| 12 |
-
"
|
| 13 |
-
"react-dom": "^19.2.3",
|
| 14 |
"clsx": "^2.1.1",
|
| 15 |
-
"tailwind-merge": "^3.4.0",
|
| 16 |
"framer-motion": "^12.23.26",
|
| 17 |
"lucide-react": "^0.562.0",
|
| 18 |
-
"react
|
|
|
|
|
|
|
|
|
|
| 19 |
},
|
| 20 |
"devDependencies": {
|
| 21 |
"@types/node": "^22.14.0",
|
|
|
|
| 9 |
"preview": "vite preview"
|
| 10 |
},
|
| 11 |
"dependencies": {
|
| 12 |
+
"@supabase/supabase-js": "^2.93.3",
|
|
|
|
| 13 |
"clsx": "^2.1.1",
|
|
|
|
| 14 |
"framer-motion": "^12.23.26",
|
| 15 |
"lucide-react": "^0.562.0",
|
| 16 |
+
"react": "^19.2.3",
|
| 17 |
+
"react-dom": "^19.2.3",
|
| 18 |
+
"react-markdown": "^9.0.1",
|
| 19 |
+
"tailwind-merge": "^3.4.0"
|
| 20 |
},
|
| 21 |
"devDependencies": {
|
| 22 |
"@types/node": "^22.14.0",
|
FRRONTEEEND/supabase_schema.sql
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
-- =====================================================
|
| 2 |
+
-- Supabase Database Schema for Data Science Agent Analytics
|
| 3 |
+
-- =====================================================
|
| 4 |
+
-- Run this in your Supabase SQL Editor: https://app.supabase.com/project/_/sql
|
| 5 |
+
|
| 6 |
+
-- 1. Usage Analytics Table
|
| 7 |
+
-- Tracks individual queries/requests made by users
|
| 8 |
+
CREATE TABLE IF NOT EXISTS usage_analytics (
|
| 9 |
+
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
|
| 10 |
+
user_id TEXT NOT NULL,
|
| 11 |
+
user_email TEXT,
|
| 12 |
+
session_id TEXT NOT NULL,
|
| 13 |
+
query TEXT NOT NULL,
|
| 14 |
+
agent_used TEXT,
|
| 15 |
+
tools_executed TEXT[],
|
| 16 |
+
tokens_used INTEGER,
|
| 17 |
+
duration_ms INTEGER,
|
| 18 |
+
success BOOLEAN DEFAULT true,
|
| 19 |
+
error_message TEXT,
|
| 20 |
+
created_at TIMESTAMPTZ DEFAULT NOW()
|
| 21 |
+
);
|
| 22 |
+
|
| 23 |
+
-- 2. User Sessions Table
|
| 24 |
+
-- Tracks user sessions for engagement metrics
|
| 25 |
+
CREATE TABLE IF NOT EXISTS user_sessions (
|
| 26 |
+
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
|
| 27 |
+
user_id TEXT NOT NULL,
|
| 28 |
+
user_email TEXT,
|
| 29 |
+
started_at TIMESTAMPTZ DEFAULT NOW(),
|
| 30 |
+
ended_at TIMESTAMPTZ,
|
| 31 |
+
queries_count INTEGER DEFAULT 0,
|
| 32 |
+
browser_info TEXT
|
| 33 |
+
);
|
| 34 |
+
|
| 35 |
+
-- 3. Indexes for performance
|
| 36 |
+
CREATE INDEX IF NOT EXISTS idx_usage_analytics_user_id ON usage_analytics(user_id);
|
| 37 |
+
CREATE INDEX IF NOT EXISTS idx_usage_analytics_created_at ON usage_analytics(created_at);
|
| 38 |
+
CREATE INDEX IF NOT EXISTS idx_usage_analytics_session_id ON usage_analytics(session_id);
|
| 39 |
+
CREATE INDEX IF NOT EXISTS idx_user_sessions_user_id ON user_sessions(user_id);
|
| 40 |
+
CREATE INDEX IF NOT EXISTS idx_user_sessions_started_at ON user_sessions(started_at);
|
| 41 |
+
|
| 42 |
+
-- 4. Function to increment session query count atomically
|
| 43 |
+
CREATE OR REPLACE FUNCTION increment_session_queries(session_id UUID)
|
| 44 |
+
RETURNS VOID AS $$
|
| 45 |
+
BEGIN
|
| 46 |
+
UPDATE user_sessions
|
| 47 |
+
SET queries_count = queries_count + 1
|
| 48 |
+
WHERE id = session_id;
|
| 49 |
+
END;
|
| 50 |
+
$$ LANGUAGE plpgsql;
|
| 51 |
+
|
| 52 |
+
-- 5. Enable Row Level Security (RLS)
|
| 53 |
+
ALTER TABLE usage_analytics ENABLE ROW LEVEL SECURITY;
|
| 54 |
+
ALTER TABLE user_sessions ENABLE ROW LEVEL SECURITY;
|
| 55 |
+
|
| 56 |
+
-- 6. RLS Policies - Allow authenticated users to insert their own data
|
| 57 |
+
-- Policy for usage_analytics
|
| 58 |
+
CREATE POLICY "Users can insert their own analytics" ON usage_analytics
|
| 59 |
+
FOR INSERT WITH CHECK (true);
|
| 60 |
+
|
| 61 |
+
CREATE POLICY "Users can view their own analytics" ON usage_analytics
|
| 62 |
+
FOR SELECT USING (auth.uid()::text = user_id OR user_id = 'anonymous');
|
| 63 |
+
|
| 64 |
+
-- Policy for user_sessions
|
| 65 |
+
CREATE POLICY "Users can insert their own sessions" ON user_sessions
|
| 66 |
+
FOR INSERT WITH CHECK (true);
|
| 67 |
+
|
| 68 |
+
CREATE POLICY "Users can update their own sessions" ON user_sessions
|
| 69 |
+
FOR UPDATE USING (auth.uid()::text = user_id OR user_id = 'anonymous');
|
| 70 |
+
|
| 71 |
+
CREATE POLICY "Users can view their own sessions" ON user_sessions
|
| 72 |
+
FOR SELECT USING (auth.uid()::text = user_id OR user_id = 'anonymous');
|
| 73 |
+
|
| 74 |
+
-- 7. Helpful Views for Analytics Dashboard
|
| 75 |
+
|
| 76 |
+
-- Daily active users
|
| 77 |
+
CREATE OR REPLACE VIEW daily_active_users AS
|
| 78 |
+
SELECT
|
| 79 |
+
DATE(created_at) as date,
|
| 80 |
+
COUNT(DISTINCT user_id) as unique_users,
|
| 81 |
+
COUNT(*) as total_queries
|
| 82 |
+
FROM usage_analytics
|
| 83 |
+
GROUP BY DATE(created_at)
|
| 84 |
+
ORDER BY date DESC;
|
| 85 |
+
|
| 86 |
+
-- Popular queries
|
| 87 |
+
CREATE OR REPLACE VIEW popular_queries AS
|
| 88 |
+
SELECT
|
| 89 |
+
query,
|
| 90 |
+
COUNT(*) as count,
|
| 91 |
+
COUNT(DISTINCT user_id) as unique_users
|
| 92 |
+
FROM usage_analytics
|
| 93 |
+
WHERE created_at > NOW() - INTERVAL '7 days'
|
| 94 |
+
GROUP BY query
|
| 95 |
+
ORDER BY count DESC
|
| 96 |
+
LIMIT 50;
|
| 97 |
+
|
| 98 |
+
-- Agent usage stats
|
| 99 |
+
CREATE OR REPLACE VIEW agent_usage_stats AS
|
| 100 |
+
SELECT
|
| 101 |
+
agent_used,
|
| 102 |
+
COUNT(*) as total_uses,
|
| 103 |
+
AVG(duration_ms) as avg_duration_ms,
|
| 104 |
+
SUM(CASE WHEN success THEN 1 ELSE 0 END)::float / COUNT(*) * 100 as success_rate
|
| 105 |
+
FROM usage_analytics
|
| 106 |
+
WHERE agent_used IS NOT NULL
|
| 107 |
+
GROUP BY agent_used
|
| 108 |
+
ORDER BY total_uses DESC;
|
| 109 |
+
|
| 110 |
+
-- =====================================================
|
| 111 |
+
-- SETUP INSTRUCTIONS:
|
| 112 |
+
-- =====================================================
|
| 113 |
+
-- 1. Go to https://app.supabase.com/ and create a new project
|
| 114 |
+
-- 2. Go to Settings > API to get your Project URL and anon key
|
| 115 |
+
-- 3. Create a .env file in FRRONTEEEND/ with:
|
| 116 |
+
-- VITE_SUPABASE_URL=your_project_url
|
| 117 |
+
-- VITE_SUPABASE_ANON_KEY=your_anon_key
|
| 118 |
+
-- 4. Go to Authentication > Providers and enable:
|
| 119 |
+
-- - Email (enabled by default)
|
| 120 |
+
-- - Google (optional - need OAuth credentials)
|
| 121 |
+
-- - GitHub (optional - need OAuth app)
|
| 122 |
+
-- 5. Run this SQL in the SQL Editor
|
| 123 |
+
-- 6. Done! Your analytics will start tracking automatically
|
| 124 |
+
-- =====================================================
|