mirror of
https://github.com/elder-plinius/LEAKHUB.git
synced 2026-07-25 13:30:51 +02:00
🚀 Full overhaul of the app
This commit is contained in:
@@ -1,33 +0,0 @@
|
||||
name: Deploy to GitHub Pages
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '18'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build project
|
||||
run: npm run build
|
||||
|
||||
- name: Deploy to GitHub Pages
|
||||
uses: peaceiris/actions-gh-pages@v3
|
||||
if: github.ref == 'refs/heads/main'
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_dir: ./dist
|
||||
-357
@@ -1,357 +0,0 @@
|
||||
# 🚀 LeakHub Deployment Guide
|
||||
|
||||
## 🌟 Deployment Options
|
||||
|
||||
LeakHub can be deployed in multiple ways, from simple static hosting to full backend integration.
|
||||
|
||||
## 📦 Option 1: GitHub Pages (Recommended)
|
||||
|
||||
### Quick Setup
|
||||
1. **Push to GitHub**: Push your code to the `main` branch
|
||||
2. **Enable GitHub Pages**:
|
||||
- Go to Settings → Pages
|
||||
- Source: "GitHub Actions"
|
||||
- The workflow will automatically deploy on push
|
||||
|
||||
### Manual Setup
|
||||
```bash
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Build for production
|
||||
npm run build
|
||||
|
||||
# The dist/ folder is ready for deployment
|
||||
```
|
||||
|
||||
### GitHub Actions Workflow
|
||||
The `.github/workflows/deploy.yml` file automatically:
|
||||
- Builds the project with Vite
|
||||
- Deploys to GitHub Pages
|
||||
- Handles CORS and routing
|
||||
|
||||
## 🔧 Option 2: Vercel (Full Stack)
|
||||
|
||||
### Deploy with Backend
|
||||
1. **Connect to Vercel**:
|
||||
```bash
|
||||
npm i -g vercel
|
||||
vercel login
|
||||
vercel
|
||||
```
|
||||
|
||||
2. **Environment Variables** (optional):
|
||||
```bash
|
||||
vercel env add DATABASE_URL
|
||||
vercel env add API_KEY
|
||||
```
|
||||
|
||||
3. **Deploy**:
|
||||
```bash
|
||||
vercel --prod
|
||||
```
|
||||
|
||||
### Features
|
||||
- ✅ Serverless API endpoints
|
||||
- ✅ Automatic HTTPS
|
||||
- ✅ Global CDN
|
||||
- ✅ Custom domains
|
||||
- ✅ Environment variables
|
||||
|
||||
## 🌐 Option 3: Netlify
|
||||
|
||||
### Deploy with Functions
|
||||
1. **Connect to Netlify**:
|
||||
```bash
|
||||
npm install -g netlify-cli
|
||||
netlify login
|
||||
netlify init
|
||||
```
|
||||
|
||||
2. **Deploy**:
|
||||
```bash
|
||||
netlify deploy --prod
|
||||
```
|
||||
|
||||
### Features
|
||||
- ✅ Serverless functions
|
||||
- ✅ Form handling
|
||||
- ✅ A/B testing
|
||||
- ✅ Custom domains
|
||||
|
||||
## 🗄️ Option 4: Full Backend Integration
|
||||
|
||||
### Database Options
|
||||
|
||||
#### MongoDB Atlas
|
||||
```javascript
|
||||
// In api/database.js
|
||||
const { MongoClient } = require('mongodb');
|
||||
|
||||
const client = new MongoClient(process.env.MONGODB_URI);
|
||||
await client.connect();
|
||||
const db = client.db('leakhub');
|
||||
```
|
||||
|
||||
#### Supabase (PostgreSQL)
|
||||
```javascript
|
||||
// In api/database.js
|
||||
import { createClient } from '@supabase/supabase-js';
|
||||
|
||||
const supabase = createClient(
|
||||
process.env.SUPABASE_URL,
|
||||
process.env.SUPABASE_ANON_KEY
|
||||
);
|
||||
```
|
||||
|
||||
#### Firebase
|
||||
```javascript
|
||||
// In api/database.js
|
||||
import { initializeApp } from 'firebase/app';
|
||||
import { getFirestore } from 'firebase/firestore';
|
||||
|
||||
const app = initializeApp(firebaseConfig);
|
||||
const db = getFirestore(app);
|
||||
```
|
||||
|
||||
## 🔐 Environment Variables
|
||||
|
||||
### Required for Backend
|
||||
```bash
|
||||
# Database
|
||||
DATABASE_URL=mongodb://localhost:27017/leakhub
|
||||
# or
|
||||
SUPABASE_URL=https://your-project.supabase.co
|
||||
SUPABASE_ANON_KEY=your-anon-key
|
||||
|
||||
# API Keys
|
||||
API_KEY=your-secret-api-key
|
||||
JWT_SECRET=your-jwt-secret
|
||||
|
||||
# CORS
|
||||
ALLOWED_ORIGINS=https://yourdomain.com,https://www.yourdomain.com
|
||||
```
|
||||
|
||||
### Optional
|
||||
```bash
|
||||
# Analytics
|
||||
GOOGLE_ANALYTICS_ID=GA_MEASUREMENT_ID
|
||||
MIXPANEL_TOKEN=your-mixpanel-token
|
||||
|
||||
# Email (for notifications)
|
||||
SMTP_HOST=smtp.gmail.com
|
||||
SMTP_PORT=587
|
||||
SMTP_USER=your-email@gmail.com
|
||||
SMTP_PASS=your-app-password
|
||||
```
|
||||
|
||||
## 🚀 Production Checklist
|
||||
|
||||
### Frontend
|
||||
- [ ] Minify CSS and JavaScript
|
||||
- [ ] Optimize images
|
||||
- [ ] Enable gzip compression
|
||||
- [ ] Set up CDN
|
||||
- [ ] Configure caching headers
|
||||
- [ ] Add security headers
|
||||
|
||||
### Backend
|
||||
- [ ] Set up database
|
||||
- [ ] Configure CORS
|
||||
- [ ] Add rate limiting
|
||||
- [ ] Set up logging
|
||||
- [ ] Configure monitoring
|
||||
- [ ] Set up backups
|
||||
|
||||
### Security
|
||||
- [ ] HTTPS enabled
|
||||
- [ ] Security headers
|
||||
- [ ] Input validation
|
||||
- [ ] SQL injection protection
|
||||
- [ ] XSS protection
|
||||
- [ ] CSRF protection
|
||||
|
||||
## 📊 Monitoring & Analytics
|
||||
|
||||
### Google Analytics
|
||||
```html
|
||||
<!-- Add to index.html -->
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=GA_MEASUREMENT_ID"></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
gtag('config', 'GA_MEASUREMENT_ID');
|
||||
</script>
|
||||
```
|
||||
|
||||
### Error Tracking
|
||||
```javascript
|
||||
// Add to script.js
|
||||
window.addEventListener('error', function(e) {
|
||||
// Send to your error tracking service
|
||||
console.error('LeakHub Error:', e.error);
|
||||
});
|
||||
```
|
||||
|
||||
## 🔄 CI/CD Pipeline
|
||||
|
||||
### GitHub Actions (Full Pipeline)
|
||||
```yaml
|
||||
name: Full CI/CD Pipeline
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, develop ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '18'
|
||||
- run: npm ci
|
||||
- run: npm run lint
|
||||
- run: npm run test
|
||||
|
||||
build:
|
||||
needs: test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '18'
|
||||
- run: npm ci
|
||||
- run: npm run build
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: build-files
|
||||
path: dist/
|
||||
|
||||
deploy:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
if: github.ref == 'refs/heads/main'
|
||||
steps:
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: build-files
|
||||
- uses: peaceiris/actions-gh-pages@v3
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_dir: ./dist
|
||||
```
|
||||
|
||||
## 🎯 Custom Domains
|
||||
|
||||
### GitHub Pages
|
||||
1. Go to Settings → Pages
|
||||
2. Add custom domain
|
||||
3. Update DNS records
|
||||
4. Enable HTTPS
|
||||
|
||||
### Vercel
|
||||
```bash
|
||||
vercel domains add yourdomain.com
|
||||
```
|
||||
|
||||
### Netlify
|
||||
1. Go to Site settings → Domain management
|
||||
2. Add custom domain
|
||||
3. Update DNS records
|
||||
|
||||
## 🔧 Development vs Production
|
||||
|
||||
### Development
|
||||
```bash
|
||||
npm run dev # Start development server
|
||||
npm run preview # Preview production build
|
||||
npm run lint # Check code quality
|
||||
npm run format # Format code
|
||||
```
|
||||
|
||||
### Production
|
||||
```bash
|
||||
npm run build # Build for production
|
||||
npm run serve # Serve production build locally
|
||||
```
|
||||
|
||||
## 📈 Performance Optimization
|
||||
|
||||
### Frontend
|
||||
- [ ] Code splitting
|
||||
- [ ] Lazy loading
|
||||
- [ ] Image optimization
|
||||
- [ ] Bundle analysis
|
||||
- [ ] Critical CSS
|
||||
|
||||
### Backend
|
||||
- [ ] Database indexing
|
||||
- [ ] Query optimization
|
||||
- [ ] Caching strategy
|
||||
- [ ] Load balancing
|
||||
- [ ] CDN integration
|
||||
|
||||
## 🛠️ Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
#### Build Failures
|
||||
```bash
|
||||
# Clear cache
|
||||
rm -rf node_modules package-lock.json
|
||||
npm install
|
||||
|
||||
# Check Node version
|
||||
node --version # Should be 16+ for Vite
|
||||
```
|
||||
|
||||
#### CORS Issues
|
||||
```javascript
|
||||
// Ensure CORS headers are set
|
||||
const corsHeaders = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'Content-Type, Authorization'
|
||||
};
|
||||
```
|
||||
|
||||
#### Database Connection
|
||||
```javascript
|
||||
// Add retry logic
|
||||
async function connectWithRetry() {
|
||||
for (let i = 0; i < 5; i++) {
|
||||
try {
|
||||
await connect();
|
||||
break;
|
||||
} catch (error) {
|
||||
console.log(`Connection attempt ${i + 1} failed`);
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🎉 Deployment Complete!
|
||||
|
||||
Once deployed, your LeakHub instance will be available at:
|
||||
- **GitHub Pages**: `https://elder-plinius.github.io/LEAKHUB`
|
||||
- **Vercel**: `https://your-project.vercel.app`
|
||||
- **Netlify**: `https://your-project.netlify.app`
|
||||
|
||||
### Next Steps
|
||||
1. Test all functionality
|
||||
2. Set up monitoring
|
||||
3. Configure backups
|
||||
4. Add custom domain
|
||||
5. Set up SSL certificate
|
||||
6. Configure analytics
|
||||
|
||||
---
|
||||
|
||||
**Happy deploying! 🚀**
|
||||
-2175
File diff suppressed because it is too large
Load Diff
+202
@@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2024 Convex, Inc.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -1,186 +0,0 @@
|
||||
# 🎉 LeakHub - Project Summary
|
||||
|
||||
## 🚀 What We Built
|
||||
|
||||
**LeakHub** is a revolutionary AI system prompt discovery platform that enables community-driven verification and analysis of AI system prompts. It's designed to promote transparency in AI systems through crowdsourced verification.
|
||||
|
||||
## ✨ Key Features Implemented
|
||||
|
||||
### 🔍 **Core Functionality**
|
||||
- ✅ **Submit Leaks**: Comprehensive submission system with metadata tracking
|
||||
- ✅ **Compare & Verify**: Advanced similarity algorithms for verification
|
||||
- ✅ **Community Library**: Browse and search through all submissions
|
||||
- ✅ **Confidence Scoring**: Automatic assessment based on content analysis
|
||||
|
||||
### 🎯 **Target Types Supported**
|
||||
- ✅ **AI Models** (GPT-4, Claude, Gemini, etc.)
|
||||
- ✅ **Apps/Interfaces** (Cursor, GitHub Copilot, etc.)
|
||||
- ✅ **Tools/Functions** (Code Interpreter, WebPilot, etc.)
|
||||
- ✅ **AI Agents** (AutoGPT, BabyAGI, etc.)
|
||||
- ✅ **Plugins/Extensions** (Browser extensions, IDE plugins, etc.)
|
||||
- ✅ **Custom GPTs/Bots** (Custom implementations)
|
||||
|
||||
### 🏆 **Gamification & Community**
|
||||
- ✅ **Leaderboard**: Track top contributors and achievements
|
||||
- ✅ **Daily Challenges**: Compete in daily leak hunting challenges
|
||||
- ✅ **Achievement System**: Unlock badges for notable contributions
|
||||
- ✅ **Points System**: Earn points for submissions, verifications, and discoveries
|
||||
|
||||
### 🎯 **Requests & Bounties**
|
||||
- ✅ **Community Requests**: Request specific targets for leak hunting
|
||||
- ✅ **Voting System**: Vote on which targets to prioritize
|
||||
- ✅ **Bounty System**: Offer points for high-priority discoveries
|
||||
- ✅ **Trending Requests**: See what the community wants most
|
||||
|
||||
### 📊 **Advanced Analytics**
|
||||
- ✅ **Similarity Metrics**: Character match, word match, structure match, core similarity
|
||||
- ✅ **Consensus View**: Identify common elements across multiple submissions
|
||||
- ✅ **Verification Tracking**: Track verification status and confidence levels
|
||||
- ✅ **Statistics Dashboard**: Real-time platform statistics
|
||||
|
||||
## 🛠️ Technical Architecture
|
||||
|
||||
### **Frontend Stack**
|
||||
- **HTML5**: Semantic markup and modern structure
|
||||
- **CSS3**: Cyberpunk aesthetic with animations and responsive design
|
||||
- **JavaScript (ES6+)**: Full application logic and data management
|
||||
- **Vite**: Fast build tool and development server
|
||||
|
||||
### **Backend & Deployment**
|
||||
- **GitHub Actions**: Automated CI/CD pipeline
|
||||
- **Vercel/Netlify**: Serverless deployment with functions
|
||||
- **Database Abstraction**: Support for localStorage, MongoDB, Supabase, Firebase
|
||||
- **API Layer**: RESTful endpoints with CORS support
|
||||
|
||||
### **Key Algorithms**
|
||||
- **Text Normalization**: Standardized text processing for comparison
|
||||
- **Similarity Metrics**: Multiple algorithms for comprehensive analysis
|
||||
- **Levenshtein Distance**: String similarity calculation
|
||||
- **Phrase Matching**: Sliding window approach for common phrase detection
|
||||
|
||||
### **Data Management**
|
||||
- **Hybrid Storage**: localStorage with optional backend persistence
|
||||
- **JSON Serialization**: Efficient data storage and retrieval
|
||||
- **Real-time Updates**: Instant UI updates and statistics
|
||||
- **Data Validation**: Input validation and error handling
|
||||
- **Export/Import**: Full data backup and restore functionality
|
||||
|
||||
## 🎨 Design Features
|
||||
|
||||
### **Modern UI/UX**
|
||||
- **Cyberpunk Aesthetic**: Dark theme with neon accents and gradients
|
||||
- **Responsive Design**: Works perfectly on desktop, tablet, and mobile
|
||||
- **Smooth Animations**: Floating grid background and interactive elements
|
||||
- **Intuitive Navigation**: Clear sections and easy-to-use interface
|
||||
|
||||
### **Visual Elements**
|
||||
- **Status Indicators**: Real-time statistics and progress tracking
|
||||
- **Badge System**: Visual indicators for access requirements and features
|
||||
- **Color Coding**: Different colors for different target types and confidence levels
|
||||
- **Interactive Elements**: Hover effects, transitions, and feedback
|
||||
|
||||
## 🚀 Deployment Ready
|
||||
|
||||
### **Multiple Deployment Options**
|
||||
1. **GitHub Pages** (Recommended): Automatic deployment via GitHub Actions
|
||||
2. **Vercel**: Full-stack deployment with serverless functions
|
||||
3. **Netlify**: Static hosting with serverless functions
|
||||
4. **Custom Backend**: MongoDB, Supabase, or Firebase integration
|
||||
|
||||
### **Production Features**
|
||||
- ✅ **Build System**: Vite-based production builds
|
||||
- ✅ **CI/CD Pipeline**: Automated testing and deployment
|
||||
- ✅ **CORS Support**: Cross-origin resource sharing
|
||||
- ✅ **Error Handling**: Comprehensive error management
|
||||
- ✅ **Performance Optimization**: Minified and optimized assets
|
||||
- ✅ **Security Headers**: Basic security configuration
|
||||
|
||||
## 📁 Project Structure
|
||||
|
||||
```
|
||||
LEAKHUB/
|
||||
├── index.html # Main application
|
||||
├── styles.css # All styling and animations
|
||||
├── script.js # Main application logic
|
||||
├── demo-data.js # Sample data for testing
|
||||
├── api/
|
||||
│ ├── database.js # Database abstraction layer
|
||||
│ ├── serverless.js # Serverless API endpoints
|
||||
│ └── config.js # Backend configuration
|
||||
├── .github/
|
||||
│ └── workflows/
|
||||
│ └── deploy.yml # GitHub Actions deployment
|
||||
├── package.json # Project dependencies and scripts
|
||||
├── vite.config.js # Vite build configuration
|
||||
├── vercel.json # Vercel deployment config
|
||||
├── netlify.toml # Netlify deployment config
|
||||
├── deploy.sh # Automated deployment script
|
||||
├── README.md # Comprehensive documentation
|
||||
├── QUICKSTART.md # Quick start guide
|
||||
├── DEPLOYMENT.md # Detailed deployment guide
|
||||
└── PROJECT_SUMMARY.md # This file
|
||||
```
|
||||
|
||||
## 🎯 Ready for Production
|
||||
|
||||
### **What's Included**
|
||||
- ✅ **Complete Application**: Fully functional frontend and backend
|
||||
- ✅ **Documentation**: Comprehensive guides and documentation
|
||||
- ✅ **Deployment Scripts**: Automated deployment to multiple platforms
|
||||
- ✅ **Demo Data**: Sample submissions for testing
|
||||
- ✅ **Error Handling**: Robust error management
|
||||
- ✅ **Performance**: Optimized for production use
|
||||
|
||||
### **Next Steps**
|
||||
1. **Deploy**: Use the deployment script or GitHub Actions
|
||||
2. **Customize**: Modify styling, features, or backend
|
||||
3. **Scale**: Add database integration for production use
|
||||
4. **Monitor**: Set up analytics and error tracking
|
||||
5. **Community**: Start building the user community
|
||||
|
||||
## 🎉 Success Metrics
|
||||
|
||||
### **Technical Achievements**
|
||||
- ✅ **100% Client-Side**: Works without backend (localStorage)
|
||||
- ✅ **Backend Ready**: Easy integration with any database
|
||||
- ✅ **Mobile Responsive**: Perfect on all devices
|
||||
- ✅ **Performance Optimized**: Fast loading and smooth interactions
|
||||
- ✅ **Accessibility**: Keyboard navigation and screen reader support
|
||||
|
||||
### **Feature Completeness**
|
||||
- ✅ **All Core Features**: Submission, comparison, verification
|
||||
- ✅ **Gamification**: Points, leaderboard, achievements
|
||||
- ✅ **Community**: Requests, voting, challenges
|
||||
- ✅ **Analytics**: Statistics, metrics, tracking
|
||||
- ✅ **Export/Import**: Data backup and restore
|
||||
|
||||
## 🔮 Future Potential
|
||||
|
||||
### **Immediate Enhancements**
|
||||
- **Database Integration**: MongoDB, Supabase, or Firebase
|
||||
- **User Authentication**: Secure user accounts
|
||||
- **Real-time Updates**: WebSocket support
|
||||
- **Advanced Analytics**: Machine learning similarity detection
|
||||
- **Mobile App**: Native mobile application
|
||||
|
||||
### **Community Features**
|
||||
- **Discussion Forums**: Community discussions and analysis
|
||||
- **Expert Verification**: Expert review system
|
||||
- **Advanced Bounties**: Complex reward systems
|
||||
- **Social Features**: Comments, sharing, collaboration
|
||||
|
||||
## 🎊 Conclusion
|
||||
|
||||
**LeakHub** is a complete, production-ready platform that successfully combines:
|
||||
|
||||
- **Innovative Concept**: AI transparency through crowdsourcing
|
||||
- **Modern Technology**: Latest web technologies and best practices
|
||||
- **Beautiful Design**: Engaging cyberpunk aesthetic
|
||||
- **Robust Architecture**: Scalable and maintainable codebase
|
||||
- **Multiple Deployment Options**: Flexible hosting solutions
|
||||
|
||||
The platform is ready to be deployed and can immediately start serving the AI research community. With its comprehensive feature set, beautiful design, and robust architecture, LeakHub has the potential to become a central hub for AI system prompt discovery and verification.
|
||||
|
||||
---
|
||||
|
||||
**🚀 Ready to launch! The future of AI transparency starts here! 🚀**
|
||||
-112
@@ -1,112 +0,0 @@
|
||||
# 🚀 LeakHub Quick Start Guide
|
||||
|
||||
## ⚡ Get Started in 30 Seconds
|
||||
|
||||
1. **Open the app**: Double-click `index.html` or open it in your browser
|
||||
2. **Load demo data** (optional): Open browser console (F12) and run `loadDemoData()`
|
||||
3. **Start exploring**: Try submitting a leak, comparing submissions, or checking the leaderboard!
|
||||
|
||||
## 🎯 First Steps
|
||||
|
||||
### 1. **Submit Your First Leak**
|
||||
- Fill in your username (e.g., "YourName")
|
||||
- Select target type (AI Model, App, Tool, etc.)
|
||||
- Enter target name (e.g., "GPT-4", "Cursor IDE")
|
||||
- Paste the system prompt you found
|
||||
- Add context about how you obtained it
|
||||
- Click "Submit to Library"
|
||||
|
||||
### 2. **Compare Submissions**
|
||||
- Go to the "Compare Multiple Instances" section
|
||||
- Select two different submissions from the dropdowns
|
||||
- Click "Compare" to see similarity analysis
|
||||
- High similarity automatically boosts confidence scores!
|
||||
|
||||
### 3. **Explore Community Features**
|
||||
- Click "🏆 View Leaderboard" to see top contributors
|
||||
- Click "🎯 Requests & Challenges" for daily missions
|
||||
- Vote on community requests
|
||||
- Track your achievements
|
||||
|
||||
## 🎮 Demo Mode
|
||||
|
||||
Want to see the platform in action? Load demo data:
|
||||
|
||||
1. Open browser console (F12)
|
||||
2. Type: `loadDemoData()`
|
||||
3. Press Enter
|
||||
4. Explore the sample submissions!
|
||||
|
||||
**Try this**: Compare the two GPT-4 submissions to see the verification system work!
|
||||
|
||||
## 🏆 Points System
|
||||
|
||||
- **10 points** per submission
|
||||
- **100 points** for first discovery of a target
|
||||
- **50 bonus points** for non-model targets (apps, tools, agents)
|
||||
- **30 bonus points** for submissions with tool prompts
|
||||
- **20 points** per verification
|
||||
- **50 bonus points** when reaching verified status
|
||||
- **400-700 points** for daily challenge completion
|
||||
|
||||
## 🔍 Pro Tips
|
||||
|
||||
### **High-Quality Submissions**
|
||||
- Include detailed context about how you obtained the prompt
|
||||
- Add access requirements (login, paid subscription)
|
||||
- Include target URLs when available
|
||||
- Mention if the target has tools with their own prompts
|
||||
|
||||
### **Effective Comparison**
|
||||
- Compare submissions from the same target for best results
|
||||
- Look for high core similarity scores (>85%)
|
||||
- Check the consensus view for common elements
|
||||
- Multiple verifications boost confidence scores
|
||||
|
||||
### **Community Engagement**
|
||||
- Vote on requests you're interested in
|
||||
- Participate in daily challenges
|
||||
- Check the leaderboard regularly
|
||||
- Submit requests for targets you want to see
|
||||
|
||||
## 🎯 Daily Challenges
|
||||
|
||||
Every day, there's a new challenge to find a specific AI system prompt:
|
||||
- Check the "Requests & Challenges" section
|
||||
- Look for the daily challenge banner
|
||||
- Submit a leak for the target model
|
||||
- Earn bonus points and special badges!
|
||||
|
||||
## 🚨 Troubleshooting
|
||||
|
||||
### **App not loading?**
|
||||
- Make sure you're opening `index.html` in a modern browser
|
||||
- Check that all files are in the same folder
|
||||
- Try refreshing the page
|
||||
|
||||
### **Demo data not loading?**
|
||||
- Make sure you're in the browser console (F12)
|
||||
- Type `loadDemoData()` exactly as shown
|
||||
- Check for any error messages
|
||||
|
||||
### **Data not saving?**
|
||||
- The app uses localStorage, so data persists between sessions
|
||||
- If data disappears, check if you're in private/incognito mode
|
||||
- Try clearing browser cache and reloading
|
||||
|
||||
## 🎉 You're Ready!
|
||||
|
||||
You now have everything you need to start contributing to AI transparency!
|
||||
|
||||
**Next steps:**
|
||||
1. Submit your first real leak
|
||||
2. Compare some submissions
|
||||
3. Check out the leaderboard
|
||||
4. Join a daily challenge
|
||||
5. Request targets you want to see
|
||||
|
||||
**Remember**: The goal is to promote AI transparency through community verification. Be accurate, provide context, and help verify others' submissions!
|
||||
|
||||
---
|
||||
|
||||
**Happy hunting! 🔍✨**
|
||||
@@ -1,246 +1,232 @@
|
||||
# 🚀 LeakHub - AI System Prompt Discovery Platform
|
||||
# 🔓 LeakHub
|
||||
|
||||
**The community hub for crowd-sourced system prompt leak verification. CL4R1T4S!**
|
||||
> The community hub for crowd-sourced system prompt leak verification. **CL4R1T4S!**
|
||||
|
||||
## 🌟 What is LeakHub?
|
||||
LeakHub is an open-source platform where the community can submit, verify, and browse leaked AI system prompts. The platform uses a consensus-based verification system to ensure authenticity and rewards contributors with a points-based leaderboard.
|
||||
|
||||
LeakHub is a revolutionary web platform that enables the community to discover, submit, verify, and analyze AI system prompt leaks. It's designed to promote transparency in AI systems through crowdsourced verification and community-driven discovery.
|
||||
---
|
||||
|
||||
## ✨ Features
|
||||
|
||||
### 🔍 **Core Functionality**
|
||||
- **Submit Leaks**: Submit suspected AI system prompt leaks with detailed metadata
|
||||
- **Compare & Verify**: Advanced comparison algorithms to verify authenticity
|
||||
- **Community Library**: Browse and search through all submitted leaks
|
||||
- **Confidence Scoring**: Automatic confidence assessment based on content analysis
|
||||
- **🔍 System Prompt Library** - Browse verified AI system prompts organized by provider
|
||||
- **📝 Submit Leaks** - Contribute system prompts for AI models, apps, tools, agents, and plugins
|
||||
- **✅ Community Verification** - Leaks require 2 unique users to verify before becoming "verified"
|
||||
- **🏆 Leaderboard & Points System** - Earn points for contributions and climb the rankings
|
||||
- **🎯 Requests System** - Request specific AI system prompts from the community
|
||||
- **🔐 GitHub OAuth** - Secure authentication via GitHub
|
||||
- **⚡ Real-time Updates** - Instant updates powered by Convex
|
||||
|
||||
### 🎯 **Target Types Supported**
|
||||
- 🤖 **AI Models** (GPT-4, Claude, Gemini, etc.)
|
||||
- 📱 **Apps/Interfaces** (Cursor, GitHub Copilot, etc.)
|
||||
- 🔧 **Tools/Functions** (Code Interpreter, WebPilot, etc.)
|
||||
- 🤝 **AI Agents** (AutoGPT, BabyAGI, etc.)
|
||||
- 🔌 **Plugins/Extensions** (Browser extensions, IDE plugins, etc.)
|
||||
- 🛠️ **Custom GPTs/Bots** (Custom implementations)
|
||||
### Points System
|
||||
|
||||
### 🏆 **Gamification & Community**
|
||||
- **Leaderboard**: Track top contributors and achievements
|
||||
- **Daily Challenges**: Compete in daily leak hunting challenges
|
||||
- **Achievement System**: Unlock badges for notable contributions
|
||||
- **Points System**: Earn points for submissions, verifications, and discoveries
|
||||
| Action | Points |
|
||||
| -------------------------------------------------- | ------ |
|
||||
| Submit a leak that gets verified (first submitter) | +100 |
|
||||
| Verify someone else's leak | +50 |
|
||||
| Create a request that gets verified | +20 |
|
||||
|
||||
### 🎯 **Requests & Bounties**
|
||||
- **Community Requests**: Request specific targets for leak hunting
|
||||
- **Voting System**: Vote on which targets to prioritize
|
||||
- **Bounty System**: Offer points for high-priority discoveries
|
||||
- **Trending Requests**: See what the community wants most
|
||||
---
|
||||
|
||||
### 📊 **Advanced Analytics**
|
||||
- **Similarity Metrics**: Character match, word match, structure match, core similarity
|
||||
- **Consensus View**: Identify common elements across multiple submissions
|
||||
- **Verification Tracking**: Track verification status and confidence levels
|
||||
- **Statistics Dashboard**: Real-time platform statistics
|
||||
## 🛠️ Tech Stack
|
||||
|
||||
### Frontend
|
||||
|
||||
- **[React 19](https://react.dev/)** - UI library
|
||||
- **[Vite](https://vitejs.dev/)** - Build tool and dev server
|
||||
- **[React Router v7](https://reactrouter.com/)** - Client-side routing
|
||||
- **[Tailwind CSS v4](https://tailwindcss.com/)** - Utility-first CSS framework
|
||||
- **[Radix UI](https://www.radix-ui.com/)** - Accessible component primitives
|
||||
- **[Lucide React](https://lucide.dev/)** - Icon library
|
||||
|
||||
### Backend
|
||||
|
||||
- **[Convex](https://convex.dev/)** - Real-time backend (database, server functions, file storage)
|
||||
- **[Convex Auth](https://labs.convex.dev/auth)** - Authentication with GitHub OAuth
|
||||
|
||||
### Deployment
|
||||
|
||||
- **[Cloudflare Pages](https://pages.cloudflare.com/)** - Edge deployment via Wrangler
|
||||
|
||||
---
|
||||
|
||||
## 📁 Project Structure
|
||||
|
||||
```
|
||||
leakhub/
|
||||
├── convex/ # Convex backend
|
||||
│ ├── _generated/ # Auto-generated types and API
|
||||
│ ├── auth.config.ts # Auth configuration
|
||||
│ ├── auth.ts # Auth handlers
|
||||
│ ├── github.ts # GitHub API integration
|
||||
│ ├── http.ts # HTTP endpoints
|
||||
│ ├── leaderboard.ts # Leaderboard queries
|
||||
│ ├── leaks.ts # Leak mutations and queries
|
||||
│ ├── requests.ts # Request mutations and queries
|
||||
│ ├── schema.ts # Database schema
|
||||
│ └── users.ts # User mutations and queries
|
||||
├── src/ # React frontend
|
||||
│ ├── components/ # Reusable UI components
|
||||
│ │ ├── ui/ # Shadcn/Radix UI components
|
||||
│ │ ├── leakLibrary.tsx # Leak browser component
|
||||
│ │ ├── navbar.tsx # Navigation bar
|
||||
│ │ └── submitLeakForm.tsx # Leak submission form
|
||||
│ ├── pages/ # Page components
|
||||
│ │ ├── Dashboard.tsx # User dashboard
|
||||
│ │ ├── Leaderboard.tsx # Points leaderboard
|
||||
│ │ └── Requests.tsx # Leak requests page
|
||||
│ ├── lib/ # Utility functions
|
||||
│ ├── App.tsx # Main app component
|
||||
│ ├── main.tsx # Entry point
|
||||
│ └── index.css # Global styles
|
||||
├── public/ # Static assets
|
||||
├── dist/ # Production build output
|
||||
├── package.json # Dependencies and scripts
|
||||
├── vite.config.ts # Vite configuration
|
||||
├── tsconfig.json # TypeScript configuration
|
||||
└── wrangler.jsonc # Cloudflare deployment config
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Getting Started
|
||||
|
||||
### Prerequisites
|
||||
- **For Development**: Node.js 18+ and npm
|
||||
- **For Production**: Modern web browser (Chrome, Firefox, Safari, Edge)
|
||||
- **For Backend**: Optional - MongoDB, Supabase, or Firebase
|
||||
|
||||
### Quick Start (Development)
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone https://github.com/elder-plinius/LEAKHUB.git
|
||||
cd LEAKHUB
|
||||
- [Node.js](https://nodejs.org/) (v18 or higher)
|
||||
- [npm](https://www.npmjs.com/)
|
||||
- A [Convex](https://convex.dev/) account
|
||||
|
||||
# Install dependencies
|
||||
npm install
|
||||
### Installation
|
||||
|
||||
# Start development server
|
||||
npm run dev
|
||||
1. **Clone the repository**
|
||||
|
||||
# Open http://localhost:3000
|
||||
```
|
||||
```bash
|
||||
git clone https://github.com/yourusername/leakhub.git
|
||||
cd leakhub
|
||||
```
|
||||
|
||||
### Quick Start (Production)
|
||||
1. **GitHub Pages**: Push to main branch - auto-deploys!
|
||||
2. **Vercel**: `npm i -g vercel && vercel`
|
||||
3. **Netlify**: `npm install -g netlify-cli && netlify deploy`
|
||||
2. **Install dependencies**
|
||||
|
||||
### Local Testing
|
||||
```bash
|
||||
# Build for production
|
||||
npm run build
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
# Preview production build
|
||||
npm run preview
|
||||
3. **Set up Convex**
|
||||
|
||||
# Serve static files
|
||||
npm run serve
|
||||
```
|
||||
```bash
|
||||
npx convex dev
|
||||
```
|
||||
|
||||
### Quick Start Guide
|
||||
This will prompt you to log in and create a new Convex project.
|
||||
|
||||
#### 1. **Submit Your First Leak**
|
||||
1. Fill in your identifier (username)
|
||||
2. Select the target type (AI Model, App, Tool, etc.)
|
||||
3. Enter the target name and URL
|
||||
4. Paste the suspected system prompt
|
||||
5. Add context and access requirements
|
||||
6. Submit and earn points!
|
||||
4. **Configure GitHub OAuth**
|
||||
|
||||
#### 2. **Compare Submissions**
|
||||
1. Select two different submissions from the dropdowns
|
||||
2. Click "Compare" to analyze similarity
|
||||
3. Review the detailed metrics and consensus view
|
||||
4. High similarity automatically boosts confidence scores
|
||||
Set up GitHub OAuth credentials in your Convex dashboard and configure the environment variables.
|
||||
|
||||
#### 3. **Join the Community**
|
||||
1. Click "View Leaderboard" to see top contributors
|
||||
2. Check out "Requests & Challenges" for daily missions
|
||||
3. Vote on community requests
|
||||
4. Track your achievements and progress
|
||||
5. **Start the development server**
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
This runs both the Vite frontend and Convex backend concurrently.
|
||||
|
||||
## 🎮 How It Works
|
||||
### Available Scripts
|
||||
|
||||
### **Submission Process**
|
||||
1. **Content Analysis**: Automatic confidence scoring based on prompt patterns
|
||||
2. **First Discovery Detection**: Identifies if this is the first submission for a target
|
||||
3. **Point Allocation**: Awards points for submissions, discoveries, and verifications
|
||||
4. **Metadata Tracking**: Stores access requirements, context, and tool information
|
||||
|
||||
### **Verification System**
|
||||
1. **Multi-Metric Comparison**: Uses character, word, structure, and core similarity
|
||||
2. **Consensus Building**: Identifies common elements across submissions
|
||||
3. **Confidence Boosting**: High similarity automatically increases confidence scores
|
||||
4. **Verification Milestones**: Tracks when leaks reach verified status
|
||||
|
||||
### **Gamification Mechanics**
|
||||
- **Base Points**: 10 points per submission
|
||||
- **First Discovery**: 100 points + 50 bonus for non-model targets
|
||||
- **Tool Prompts**: 30 bonus points for comprehensive submissions
|
||||
- **Verification**: 20 points per verification + 50 bonus for reaching verified status
|
||||
- **Daily Challenges**: 400-700 points for completing daily missions
|
||||
|
||||
## 🎨 Design Features
|
||||
|
||||
### **Modern UI/UX**
|
||||
- **Cyberpunk Aesthetic**: Dark theme with neon accents and gradients
|
||||
- **Responsive Design**: Works perfectly on desktop, tablet, and mobile
|
||||
- **Smooth Animations**: Floating grid background and interactive elements
|
||||
- **Intuitive Navigation**: Clear sections and easy-to-use interface
|
||||
|
||||
### **Visual Elements**
|
||||
- **Status Indicators**: Real-time statistics and progress tracking
|
||||
- **Badge System**: Visual indicators for access requirements and features
|
||||
- **Color Coding**: Different colors for different target types and confidence levels
|
||||
- **Interactive Elements**: Hover effects, transitions, and feedback
|
||||
|
||||
## 🔧 Technical Details
|
||||
|
||||
### **Frontend Technologies**
|
||||
- **HTML5**: Semantic markup and modern structure
|
||||
- **CSS3**: Advanced styling with animations and responsive design
|
||||
- **JavaScript (ES6+)**: Full application logic and data management
|
||||
- **Vite**: Fast build tool and development server
|
||||
- **LocalStorage**: Client-side data persistence with backend fallback
|
||||
|
||||
### **Backend & Deployment**
|
||||
- **GitHub Actions**: Automated CI/CD pipeline
|
||||
- **Vercel/Netlify**: Serverless deployment with functions
|
||||
- **Database Abstraction**: Support for localStorage, MongoDB, Supabase, Firebase
|
||||
- **API Layer**: RESTful endpoints with CORS support
|
||||
|
||||
### **Key Algorithms**
|
||||
- **Text Normalization**: Standardized text processing for comparison
|
||||
- **Similarity Metrics**: Multiple algorithms for comprehensive analysis
|
||||
- **Levenshtein Distance**: String similarity calculation
|
||||
- **Phrase Matching**: Sliding window approach for common phrase detection
|
||||
|
||||
### **Data Management**
|
||||
- **Hybrid Storage**: localStorage with optional backend persistence
|
||||
- **JSON Serialization**: Efficient data storage and retrieval
|
||||
- **Real-time Updates**: Instant UI updates and statistics
|
||||
- **Data Validation**: Input validation and error handling
|
||||
- **Export/Import**: Full data backup and restore functionality
|
||||
|
||||
## 🎯 Use Cases
|
||||
|
||||
### **For Researchers**
|
||||
- Document and verify AI system prompts
|
||||
- Compare different implementations
|
||||
- Track prompt evolution over time
|
||||
- Build comprehensive prompt databases
|
||||
|
||||
### **For Developers**
|
||||
- Understand AI system behaviors
|
||||
- Debug prompt-related issues
|
||||
- Learn from existing implementations
|
||||
- Contribute to AI transparency
|
||||
|
||||
### **For Community**
|
||||
- Participate in AI transparency efforts
|
||||
- Compete in daily challenges
|
||||
- Build reputation and achievements
|
||||
- Help verify and validate discoveries
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
### **How to Contribute**
|
||||
1. **Submit Leaks**: Share discovered system prompts
|
||||
2. **Verify Submissions**: Compare and verify existing leaks
|
||||
3. **Request Targets**: Suggest new targets for the community
|
||||
4. **Vote on Requests**: Help prioritize community goals
|
||||
5. **Report Issues**: Help improve the platform
|
||||
|
||||
### **Best Practices**
|
||||
- **Be Accurate**: Only submit genuine system prompts
|
||||
- **Provide Context**: Include how you obtained the prompt
|
||||
- **Add Metadata**: Include access requirements and URLs
|
||||
- **Verify Others**: Help verify community submissions
|
||||
- **Respect Privacy**: Don't submit private or sensitive information
|
||||
|
||||
## 🔮 Future Enhancements
|
||||
|
||||
### **Planned Features**
|
||||
- **Advanced Analytics**: Machine learning-based similarity detection
|
||||
- **Mobile App**: Native mobile application
|
||||
- **Real-time Collaboration**: Live collaboration features
|
||||
- **Advanced Export**: Data export in various formats (CSV, JSON, API)
|
||||
- **User Authentication**: Secure user accounts and profiles
|
||||
- **Social Features**: Comments, discussions, and community forums
|
||||
|
||||
### **Backend Integration**
|
||||
- **Database Support**: MongoDB, PostgreSQL, Firebase integration
|
||||
- **API Access**: RESTful API for programmatic access
|
||||
- **Real-time Updates**: WebSocket support for live updates
|
||||
- **Email Notifications**: Automated notifications and alerts
|
||||
- **Advanced Security**: JWT authentication and rate limiting
|
||||
|
||||
### **Community Features**
|
||||
- **User Profiles**: Detailed user profiles and statistics
|
||||
- **Discussion Forums**: Community discussions and analysis
|
||||
- **Expert Verification**: Expert review system
|
||||
- **Bounty Marketplace**: Advanced bounty and reward system
|
||||
- **Achievement System**: Expanded badges and rewards
|
||||
|
||||
## 📄 License
|
||||
|
||||
This project is open source and available under the MIT License. See the LICENSE file for details.
|
||||
|
||||
## 🙏 Acknowledgments
|
||||
|
||||
- **AI Research Community**: For inspiration and feedback
|
||||
- **Open Source Contributors**: For tools and libraries used
|
||||
- **Early Testers**: For valuable feedback and suggestions
|
||||
- **CL4R1T4S Community**: For the vision of AI transparency
|
||||
|
||||
## 📞 Support
|
||||
|
||||
- **Issues**: Report bugs and feature requests via GitHub issues
|
||||
- **Discussions**: Join community discussions
|
||||
- **Documentation**: Check the inline documentation and comments
|
||||
| Script | Description |
|
||||
| ---------------------- | ---------------------------------------------- |
|
||||
| `npm run dev` | Start frontend and backend in development mode |
|
||||
| `npm run dev:frontend` | Start only the Vite dev server |
|
||||
| `npm run dev:backend` | Start only the Convex dev server |
|
||||
| `npm run build` | Build for production |
|
||||
| `npm run preview` | Preview production build locally |
|
||||
| `npm run deploy` | Build and deploy to Cloudflare Pages |
|
||||
| `npm run lint` | Run TypeScript and ESLint checks |
|
||||
|
||||
---
|
||||
|
||||
**Ready to start hunting for AI system prompts? Open `index.html` and join the LeakHub community! 🚀**
|
||||
## 🗄️ Database Schema
|
||||
|
||||
### Tables
|
||||
|
||||
#### `leaks`
|
||||
|
||||
Stores system prompt leaks with verification status.
|
||||
|
||||
| Field | Type | Description |
|
||||
| ----------------- | -------------- | --------------------------------------------- |
|
||||
| `targetName` | `string` | Name of the AI system |
|
||||
| `provider` | `string` | Provider/company name |
|
||||
| `targetType` | `enum` | Type: model, app, tool, agent, plugin, custom |
|
||||
| `leakText` | `string` | The actual system prompt |
|
||||
| `leakContext` | `string?` | Additional context |
|
||||
| `url` | `string?` | Source URL |
|
||||
| `isFullyVerified` | `boolean` | Verification status |
|
||||
| `submittedBy` | `Id<users>?` | Submitter reference |
|
||||
| `verifiedBy` | `Id<users>[]?` | Verifiers references |
|
||||
| `requiresLogin` | `boolean?` | Access requirements |
|
||||
| `isPaid` | `boolean?` | Paid service flag |
|
||||
| `hasToolPrompts` | `boolean?` | Has tool/function prompts |
|
||||
|
||||
#### `requests`
|
||||
|
||||
Stores community requests for specific system prompts.
|
||||
|
||||
| Field | Type | Description |
|
||||
| ------------- | ------------- | --------------------------------------------- |
|
||||
| `targetName` | `string` | Requested AI system name |
|
||||
| `provider` | `string` | Provider/company name |
|
||||
| `targetType` | `enum` | Type: model, app, tool, agent, plugin, custom |
|
||||
| `targetUrl` | `string` | URL to the target |
|
||||
| `closed` | `boolean` | Whether request is fulfilled |
|
||||
| `leaks` | `Id<leaks>[]` | Associated leak submissions |
|
||||
| `submittedBy` | `Id<users>` | Requester reference |
|
||||
|
||||
#### `users`
|
||||
|
||||
User profiles with points tracking.
|
||||
|
||||
| Field | Type | Description |
|
||||
| ---------- | ----------------- | ------------------- |
|
||||
| `name` | `string` | Display name |
|
||||
| `image` | `string` | Profile picture URL |
|
||||
| `email` | `string` | Email address |
|
||||
| `points` | `number` | Accumulated points |
|
||||
| `leaks` | `Id<leaks>[]?` | Submitted leaks |
|
||||
| `requests` | `Id<requests>[]?` | Created requests |
|
||||
|
||||
---
|
||||
|
||||
## 🔒 Verification Algorithm
|
||||
|
||||
LeakHub uses a consensus-based verification system:
|
||||
|
||||
1. **Submission** - Users submit leaks linked to requests
|
||||
2. **Similarity Detection** - Text similarity is calculated using:
|
||||
- Shingle-based cosine similarity (fast filter)
|
||||
- Levenshtein distance (precise matching)
|
||||
3. **Consensus** - When 2+ unique users submit similar content (≥85% similarity), the leak is verified
|
||||
4. **Rewards** - Points are distributed to submitter, verifiers, and request creator
|
||||
|
||||
---
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
Contributions are welcome! Please feel free to submit a Pull Request.
|
||||
|
||||
1. Fork the repository
|
||||
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
|
||||
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
|
||||
4. Push to the branch (`git push origin feature/amazing-feature`)
|
||||
5. Open a Pull Request
|
||||
|
||||
---
|
||||
|
||||
## 📄 License
|
||||
|
||||
This project is licensed under the **Apache License 2.0** - see the [LICENSE.txt](LICENSE.txt) file for details.
|
||||
|
||||
---
|
||||
|
||||
## 🙏 Acknowledgments
|
||||
|
||||
- Built with [Convex](https://convex.dev/) - The fullstack TypeScript development platform
|
||||
- UI components from [shadcn/ui](https://ui.shadcn.com/)
|
||||
- Icons from [Lucide](https://lucide.dev/)
|
||||
|
||||
@@ -1,207 +0,0 @@
|
||||
# 🔒 LeakHub Security Audit Report
|
||||
|
||||
## 🛡️ Security Assessment Summary
|
||||
|
||||
**Status**: ✅ **PRODUCTION READY**
|
||||
**Risk Level**: 🟢 **LOW**
|
||||
**Last Updated**: December 2024
|
||||
|
||||
## 🔍 Security Analysis
|
||||
|
||||
### ✅ **SECURITY STRENGTHS**
|
||||
|
||||
#### **1. No Hardcoded Secrets**
|
||||
- ✅ No API keys, passwords, or tokens in code
|
||||
- ✅ All sensitive data uses environment variables
|
||||
- ✅ JWT secret properly configured for production
|
||||
- ✅ Database credentials externalized
|
||||
|
||||
#### **2. Input Validation & Sanitization**
|
||||
- ✅ HTML sanitization functions implemented
|
||||
- ✅ XSS protection through content filtering
|
||||
- ✅ Input validation on all forms
|
||||
- ✅ Safe innerHTML usage patterns
|
||||
|
||||
#### **3. Security Headers**
|
||||
- ✅ X-XSS-Protection enabled
|
||||
- ✅ X-Content-Type-Options: nosniff
|
||||
- ✅ X-Frame-Options: DENY (prevents clickjacking)
|
||||
- ✅ Strict-Transport-Security configured
|
||||
- ✅ Content Security Policy implemented
|
||||
- ✅ Referrer Policy set to strict-origin-when-cross-origin
|
||||
|
||||
#### **4. Data Protection**
|
||||
- ✅ Client-side only (no server-side data storage)
|
||||
- ✅ localStorage with proper validation
|
||||
- ✅ No sensitive data in URLs or logs
|
||||
- ✅ Export/Import functionality for data portability
|
||||
|
||||
#### **5. Code Quality**
|
||||
- ✅ No eval() or dangerous JavaScript functions
|
||||
- ✅ No inline event handlers
|
||||
- ✅ Proper error handling without information leakage
|
||||
- ✅ Console logging minimized for production
|
||||
|
||||
### 🔧 **SECURITY IMPROVEMENTS MADE**
|
||||
|
||||
#### **1. XSS Protection**
|
||||
```javascript
|
||||
// Added sanitization functions
|
||||
function sanitizeHTML(text) {
|
||||
if (typeof text !== 'string') return '';
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
function safeInnerHTML(element, content) {
|
||||
// Sanitizes content before setting innerHTML
|
||||
}
|
||||
```
|
||||
|
||||
#### **2. Security Headers**
|
||||
- Added comprehensive security headers to Vercel and Netlify configs
|
||||
- Implemented Content Security Policy
|
||||
- Enabled HSTS for HTTPS enforcement
|
||||
|
||||
#### **3. Environment Configuration**
|
||||
- Removed hardcoded JWT secret
|
||||
- Added production environment checks
|
||||
- Externalized all configuration
|
||||
|
||||
### 🚨 **POTENTIAL RISKS & MITIGATIONS**
|
||||
|
||||
#### **1. Client-Side Data Storage**
|
||||
- **Risk**: Data stored in localStorage is accessible to users
|
||||
- **Mitigation**: This is by design - LeakHub is a client-side application
|
||||
- **Recommendation**: Users should be aware data is stored locally
|
||||
|
||||
#### **2. XSS via User Input**
|
||||
- **Risk**: User-submitted content could contain malicious scripts
|
||||
- **Mitigation**: HTML sanitization implemented
|
||||
- **Recommendation**: Consider using DOMPurify library for production
|
||||
|
||||
#### **3. No Authentication**
|
||||
- **Risk**: No user authentication system
|
||||
- **Mitigation**: This is by design for community transparency
|
||||
- **Recommendation**: Consider optional user accounts for advanced features
|
||||
|
||||
## 🔐 **SECURITY RECOMMENDATIONS**
|
||||
|
||||
### **For Production Deployment**
|
||||
|
||||
#### **1. HTTPS Enforcement**
|
||||
```bash
|
||||
# Ensure HTTPS is enabled on your hosting platform
|
||||
# GitHub Pages, Vercel, and Netlify all provide HTTPS by default
|
||||
```
|
||||
|
||||
#### **2. Content Security Policy**
|
||||
```html
|
||||
<!-- Add to index.html if not using deployment config -->
|
||||
<meta http-equiv="Content-Security-Policy"
|
||||
content="default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline';">
|
||||
```
|
||||
|
||||
#### **3. Additional Security Libraries**
|
||||
```bash
|
||||
# For enhanced XSS protection
|
||||
npm install dompurify
|
||||
```
|
||||
|
||||
#### **4. Monitoring & Logging**
|
||||
```javascript
|
||||
// Add error tracking
|
||||
window.addEventListener('error', function(e) {
|
||||
// Send to monitoring service
|
||||
console.error('Security Event:', e);
|
||||
});
|
||||
```
|
||||
|
||||
### **For Future Enhancements**
|
||||
|
||||
#### **1. User Authentication**
|
||||
- Implement optional user accounts
|
||||
- Add OAuth integration (GitHub, Google)
|
||||
- Implement session management
|
||||
|
||||
#### **2. Rate Limiting**
|
||||
- Add client-side rate limiting for submissions
|
||||
- Implement cooldown periods for actions
|
||||
|
||||
#### **3. Data Validation**
|
||||
- Add server-side validation if backend is added
|
||||
- Implement input length limits
|
||||
- Add file upload restrictions
|
||||
|
||||
## 📋 **SECURITY CHECKLIST**
|
||||
|
||||
### **Pre-Deployment**
|
||||
- [x] No hardcoded secrets in code
|
||||
- [x] Security headers configured
|
||||
- [x] XSS protection implemented
|
||||
- [x] Input validation in place
|
||||
- [x] Error handling without information leakage
|
||||
- [x] HTTPS enforced
|
||||
- [x] Content Security Policy active
|
||||
|
||||
### **Post-Deployment**
|
||||
- [ ] Security headers verified
|
||||
- [ ] HTTPS working correctly
|
||||
- [ ] No console errors in production
|
||||
- [ ] User data handling reviewed
|
||||
- [ ] Privacy policy updated
|
||||
- [ ] Terms of service updated
|
||||
|
||||
## 🚀 **DEPLOYMENT SECURITY**
|
||||
|
||||
### **GitHub Pages**
|
||||
- ✅ Automatic HTTPS
|
||||
- ✅ Security headers via GitHub Actions
|
||||
- ✅ No server-side code execution
|
||||
|
||||
### **Vercel**
|
||||
- ✅ Automatic HTTPS
|
||||
- ✅ Security headers configured
|
||||
- ✅ Serverless functions with proper CORS
|
||||
|
||||
### **Netlify**
|
||||
- ✅ Automatic HTTPS
|
||||
- ✅ Security headers configured
|
||||
- ✅ Form handling with spam protection
|
||||
|
||||
## 📊 **SECURITY METRICS**
|
||||
|
||||
- **OWASP Top 10 Coverage**: 8/10
|
||||
- **Security Headers**: 7/7 implemented
|
||||
- **XSS Protection**: ✅ Implemented
|
||||
- **CSRF Protection**: ✅ Not applicable (client-side only)
|
||||
- **SQL Injection**: ✅ Not applicable (no database)
|
||||
- **Authentication**: ✅ Not implemented (by design)
|
||||
|
||||
## 🔍 **SECURITY TESTING**
|
||||
|
||||
### **Manual Testing Performed**
|
||||
- [x] XSS payload testing
|
||||
- [x] HTML injection testing
|
||||
- [x] JavaScript injection testing
|
||||
- [x] Security headers verification
|
||||
- [x] HTTPS enforcement testing
|
||||
- [x] Data validation testing
|
||||
|
||||
### **Automated Testing Recommended**
|
||||
- [ ] OWASP ZAP security scan
|
||||
- [ ] Lighthouse security audit
|
||||
- [ ] Content Security Policy validation
|
||||
- [ ] SSL Labs security test
|
||||
|
||||
## 📞 **SECURITY CONTACT**
|
||||
|
||||
For security issues or questions:
|
||||
- **Repository**: https://github.com/elder-plinius/LEAKHUB
|
||||
- **Issues**: Use GitHub Issues with "security" label
|
||||
- **Responsible Disclosure**: Please report vulnerabilities privately
|
||||
|
||||
---
|
||||
|
||||
**Note**: This security audit is based on the current codebase as of December 2024. Regular security reviews are recommended as the application evolves.
|
||||
-102
@@ -1,102 +0,0 @@
|
||||
// LeakHub Backend Configuration
|
||||
|
||||
const config = {
|
||||
// Database settings
|
||||
database: {
|
||||
// Local storage (default)
|
||||
type: 'localStorage',
|
||||
|
||||
// Backend options (uncomment to enable)
|
||||
// type: 'mongodb',
|
||||
// url: process.env.MONGODB_URI || 'mongodb://localhost:27017/leakhub',
|
||||
|
||||
// type: 'supabase',
|
||||
// url: process.env.SUPABASE_URL,
|
||||
// key: process.env.SUPABASE_ANON_KEY,
|
||||
|
||||
// type: 'firebase',
|
||||
// config: {
|
||||
// apiKey: process.env.FIREBASE_API_KEY,
|
||||
// authDomain: process.env.FIREBASE_AUTH_DOMAIN,
|
||||
// projectId: process.env.FIREBASE_PROJECT_ID,
|
||||
// storageBucket: process.env.FIREBASE_STORAGE_BUCKET,
|
||||
// messagingSenderId: process.env.FIREBASE_MESSAGING_SENDER_ID,
|
||||
// appId: process.env.FIREBASE_APP_ID
|
||||
// }
|
||||
},
|
||||
|
||||
// API settings
|
||||
api: {
|
||||
port: process.env.PORT || 3000,
|
||||
cors: {
|
||||
origin: process.env.ALLOWED_ORIGINS?.split(',') || ['*'],
|
||||
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
||||
allowedHeaders: ['Content-Type', 'Authorization']
|
||||
},
|
||||
rateLimit: {
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
max: 100 // limit each IP to 100 requests per windowMs
|
||||
}
|
||||
},
|
||||
|
||||
// Security settings
|
||||
security: {
|
||||
jwtSecret: process.env.JWT_SECRET || (process.env.NODE_ENV === 'production' ? null : 'dev-secret-key'),
|
||||
bcryptRounds: 12,
|
||||
sessionTimeout: 24 * 60 * 60 * 1000 // 24 hours
|
||||
},
|
||||
|
||||
// Analytics settings
|
||||
analytics: {
|
||||
enabled: process.env.ANALYTICS_ENABLED === 'true',
|
||||
googleAnalyticsId: process.env.GOOGLE_ANALYTICS_ID,
|
||||
mixpanelToken: process.env.MIXPANEL_TOKEN
|
||||
},
|
||||
|
||||
// Email settings (for notifications)
|
||||
email: {
|
||||
enabled: process.env.EMAIL_ENABLED === 'true',
|
||||
provider: process.env.EMAIL_PROVIDER || 'smtp',
|
||||
smtp: {
|
||||
host: process.env.SMTP_HOST,
|
||||
port: process.env.SMTP_PORT || 587,
|
||||
secure: process.env.SMTP_SECURE === 'true',
|
||||
auth: {
|
||||
user: process.env.SMTP_USER,
|
||||
pass: process.env.SMTP_PASS
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Feature flags
|
||||
features: {
|
||||
userRegistration: true,
|
||||
emailVerification: false,
|
||||
socialLogin: false,
|
||||
realTimeUpdates: false,
|
||||
advancedAnalytics: false,
|
||||
exportData: true,
|
||||
backupRestore: true
|
||||
},
|
||||
|
||||
// Development settings
|
||||
development: {
|
||||
debug: process.env.NODE_ENV !== 'production',
|
||||
mockData: process.env.MOCK_DATA === 'true',
|
||||
hotReload: process.env.NODE_ENV === 'development'
|
||||
}
|
||||
};
|
||||
|
||||
// Environment-specific overrides
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
config.development.debug = false;
|
||||
config.development.mockData = false;
|
||||
config.development.hotReload = false;
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
config.database.type = 'memory';
|
||||
config.security.jwtSecret = 'test-secret';
|
||||
}
|
||||
|
||||
module.exports = config;
|
||||
-274
@@ -1,274 +0,0 @@
|
||||
// Database abstraction layer for LeakHub
|
||||
// Supports both localStorage and future backend APIs
|
||||
|
||||
class LeakHubDatabase {
|
||||
constructor() {
|
||||
this.storagePrefix = 'leakhub_';
|
||||
this.apiEndpoint = null; // Will be set for backend mode
|
||||
this.isBackendMode = false;
|
||||
}
|
||||
|
||||
// Initialize database with optional backend endpoint
|
||||
async init(backendUrl = null) {
|
||||
if (backendUrl) {
|
||||
this.apiEndpoint = backendUrl;
|
||||
this.isBackendMode = true;
|
||||
console.log('LeakHub: Backend mode enabled');
|
||||
} else {
|
||||
console.log('LeakHub: Local storage mode enabled');
|
||||
}
|
||||
}
|
||||
|
||||
// Generic storage methods
|
||||
async get(key) {
|
||||
if (this.isBackendMode) {
|
||||
return await this._getFromBackend(key);
|
||||
} else {
|
||||
return this._getFromLocalStorage(key);
|
||||
}
|
||||
}
|
||||
|
||||
async set(key, value) {
|
||||
if (this.isBackendMode) {
|
||||
return await this._setToBackend(key, value);
|
||||
} else {
|
||||
return this._setToLocalStorage(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
async delete(key) {
|
||||
if (this.isBackendMode) {
|
||||
return await this._deleteFromBackend(key);
|
||||
} else {
|
||||
return this._deleteFromLocalStorage(key);
|
||||
}
|
||||
}
|
||||
|
||||
// LocalStorage methods
|
||||
_getFromLocalStorage(key) {
|
||||
try {
|
||||
const fullKey = this.storagePrefix + key;
|
||||
const data = localStorage.getItem(fullKey);
|
||||
return data ? JSON.parse(data) : null;
|
||||
} catch (error) {
|
||||
console.error('Error reading from localStorage:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
_setToLocalStorage(key, value) {
|
||||
try {
|
||||
const fullKey = this.storagePrefix + key;
|
||||
localStorage.setItem(fullKey, JSON.stringify(value));
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error writing to localStorage:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
_deleteFromLocalStorage(key) {
|
||||
try {
|
||||
const fullKey = this.storagePrefix + key;
|
||||
localStorage.removeItem(fullKey);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error deleting from localStorage:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Backend API methods
|
||||
async _getFromBackend(key) {
|
||||
try {
|
||||
const response = await fetch(`${this.apiEndpoint}/data/${key}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
return await response.json();
|
||||
} else {
|
||||
console.error('Backend GET error:', response.status);
|
||||
return null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching from backend:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async _setToBackend(key, value) {
|
||||
try {
|
||||
const response = await fetch(`${this.apiEndpoint}/data/${key}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(value)
|
||||
});
|
||||
|
||||
return response.ok;
|
||||
} catch (error) {
|
||||
console.error('Error saving to backend:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async _deleteFromBackend(key) {
|
||||
try {
|
||||
const response = await fetch(`${this.apiEndpoint}/data/${key}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
});
|
||||
|
||||
return response.ok;
|
||||
} catch (error) {
|
||||
console.error('Error deleting from backend:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Specific LeakHub data methods
|
||||
async getLeakDatabase() {
|
||||
return await this.get('leakDatabase') || [];
|
||||
}
|
||||
|
||||
async setLeakDatabase(data) {
|
||||
return await this.set('leakDatabase', data);
|
||||
}
|
||||
|
||||
async getUserStats() {
|
||||
return await this.get('userStats') || {};
|
||||
}
|
||||
|
||||
async setUserStats(data) {
|
||||
return await this.set('userStats', data);
|
||||
}
|
||||
|
||||
async getLeakRequests() {
|
||||
return await this.get('leakRequests') || [];
|
||||
}
|
||||
|
||||
async setLeakRequests(data) {
|
||||
return await this.set('leakRequests', data);
|
||||
}
|
||||
|
||||
async getUserVotes() {
|
||||
return await this.get('userVotes') || {};
|
||||
}
|
||||
|
||||
async setUserVotes(data) {
|
||||
return await this.set('userVotes', data);
|
||||
}
|
||||
|
||||
async getDailyChallenge() {
|
||||
return await this.get('dailyChallenge') || null;
|
||||
}
|
||||
|
||||
async setDailyChallenge(data) {
|
||||
return await this.set('dailyChallenge', data);
|
||||
}
|
||||
|
||||
async getAverageSimilarity() {
|
||||
return await this.get('avgSimilarity') || '0';
|
||||
}
|
||||
|
||||
async setAverageSimilarity(value) {
|
||||
return await this.set('avgSimilarity', value);
|
||||
}
|
||||
|
||||
// Export/Import functionality
|
||||
async exportData() {
|
||||
const data = {
|
||||
leakDatabase: await this.getLeakDatabase(),
|
||||
userStats: await this.getUserStats(),
|
||||
leakRequests: await this.getLeakRequests(),
|
||||
userVotes: await this.getUserVotes(),
|
||||
dailyChallenge: await this.getDailyChallenge(),
|
||||
avgSimilarity: await this.getAverageSimilarity(),
|
||||
exportDate: new Date().toISOString(),
|
||||
version: '1.0.0'
|
||||
};
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
async importData(data) {
|
||||
try {
|
||||
if (data.leakDatabase) await this.setLeakDatabase(data.leakDatabase);
|
||||
if (data.userStats) await this.setUserStats(data.userStats);
|
||||
if (data.leakRequests) await this.setLeakRequests(data.leakRequests);
|
||||
if (data.userVotes) await this.setUserVotes(data.userVotes);
|
||||
if (data.dailyChallenge) await this.setDailyChallenge(data.dailyChallenge);
|
||||
if (data.avgSimilarity) await this.setAverageSimilarity(data.avgSimilarity);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error importing data:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Backup and restore
|
||||
async createBackup() {
|
||||
const data = await this.exportData();
|
||||
const backupKey = `backup_${Date.now()}`;
|
||||
await this.set(backupKey, data);
|
||||
return backupKey;
|
||||
}
|
||||
|
||||
async restoreBackup(backupKey) {
|
||||
const backup = await this.get(backupKey);
|
||||
if (backup) {
|
||||
return await this.importData(backup);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Analytics and statistics
|
||||
async getStatistics() {
|
||||
const leakDatabase = await this.getLeakDatabase();
|
||||
const userStats = await this.getUserStats();
|
||||
|
||||
return {
|
||||
totalSubmissions: leakDatabase.length,
|
||||
uniqueUsers: Object.keys(userStats).length,
|
||||
totalScore: Object.values(userStats).reduce((sum, stats) => sum + (stats.totalScore || 0), 0),
|
||||
verifiedLeaks: leakDatabase.filter(sub => sub.confidence >= 90).length,
|
||||
firstDiscoveries: leakDatabase.filter(sub => sub.isFirstDiscovery).length,
|
||||
targetTypes: this._countTargetTypes(leakDatabase),
|
||||
recentActivity: this._getRecentActivity(leakDatabase)
|
||||
};
|
||||
}
|
||||
|
||||
_countTargetTypes(leakDatabase) {
|
||||
const counts = {};
|
||||
leakDatabase.forEach(sub => {
|
||||
const type = sub.targetType || 'model';
|
||||
counts[type] = (counts[type] || 0) + 1;
|
||||
});
|
||||
return counts;
|
||||
}
|
||||
|
||||
_getRecentActivity(leakDatabase) {
|
||||
const now = new Date();
|
||||
const oneWeekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
||||
|
||||
return leakDatabase.filter(sub =>
|
||||
new Date(sub.timestamp) > oneWeekAgo
|
||||
).length;
|
||||
}
|
||||
}
|
||||
|
||||
// Create global instance
|
||||
window.LeakHubDB = new LeakHubDatabase();
|
||||
|
||||
// Auto-initialize
|
||||
window.LeakHubDB.init().then(() => {
|
||||
console.log('LeakHub Database initialized');
|
||||
});
|
||||
@@ -1,83 +0,0 @@
|
||||
// Serverless API endpoints for LeakHub
|
||||
// This can be deployed to Vercel, Netlify, or other serverless platforms
|
||||
|
||||
// Mock database for serverless functions
|
||||
const mockDatabase = {
|
||||
leakDatabase: [],
|
||||
userStats: {},
|
||||
leakRequests: [],
|
||||
userVotes: {},
|
||||
dailyChallenge: null,
|
||||
avgSimilarity: '0'
|
||||
};
|
||||
|
||||
// CORS headers
|
||||
const corsHeaders = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
|
||||
// Helper function to get data
|
||||
function getData(key) {
|
||||
// In a real implementation, this would connect to a database
|
||||
// For now, we'll use a simple in-memory store
|
||||
return mockDatabase[key] || null;
|
||||
}
|
||||
|
||||
// Helper function to set data
|
||||
function setData(key, value) {
|
||||
mockDatabase[key] = value;
|
||||
return true;
|
||||
}
|
||||
|
||||
// API handler for Vercel/Netlify
|
||||
export default async function handler(req, res) {
|
||||
// Handle CORS preflight
|
||||
if (req.method === 'OPTIONS') {
|
||||
return res.status(200).json({}, { headers: corsHeaders });
|
||||
}
|
||||
|
||||
const { method, url, body } = req;
|
||||
const path = url.split('/').pop(); // Get the last part of the URL
|
||||
|
||||
try {
|
||||
switch (method) {
|
||||
case 'GET':
|
||||
if (path === 'data') {
|
||||
const key = req.query.key;
|
||||
const data = getData(key);
|
||||
return res.status(200).json(data, { headers: corsHeaders });
|
||||
}
|
||||
break;
|
||||
|
||||
case 'POST':
|
||||
if (path === 'data') {
|
||||
const key = req.query.key;
|
||||
const success = setData(key, body);
|
||||
return res.status(200).json({ success }, { headers: corsHeaders });
|
||||
}
|
||||
break;
|
||||
|
||||
case 'DELETE':
|
||||
if (path === 'data') {
|
||||
const key = req.query.key;
|
||||
const success = setData(key, null);
|
||||
return res.status(200).json({ success }, { headers: corsHeaders });
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
return res.status(405).json({ error: 'Method not allowed' }, { headers: corsHeaders });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('API Error:', error);
|
||||
return res.status(500).json({ error: 'Internal server error' }, { headers: corsHeaders });
|
||||
}
|
||||
}
|
||||
|
||||
// Export for different serverless platforms
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = handler;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "new-york",
|
||||
"rsc": false,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"css": "src/index.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"iconLibrary": "lucide",
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
},
|
||||
"registries": {}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
# Welcome to your Convex functions directory!
|
||||
|
||||
Write your Convex functions here.
|
||||
See https://docs.convex.dev/functions for more.
|
||||
|
||||
A query function that takes two arguments looks like:
|
||||
|
||||
```ts
|
||||
// functions.js
|
||||
import { query } from "./_generated/server";
|
||||
import { v } from "convex/values";
|
||||
|
||||
export const myQueryFunction = query({
|
||||
// Validators for arguments.
|
||||
args: {
|
||||
first: v.number(),
|
||||
second: v.string(),
|
||||
},
|
||||
|
||||
// Function implementation.
|
||||
handler: async (ctx, args) => {
|
||||
// Read the database as many times as you need here.
|
||||
// See https://docs.convex.dev/database/reading-data.
|
||||
const documents = await ctx.db.query("tablename").collect();
|
||||
|
||||
// Arguments passed from the client are properties of the args object.
|
||||
console.log(args.first, args.second);
|
||||
|
||||
// Write arbitrary JavaScript here: filter, aggregate, build derived data,
|
||||
// remove non-public properties, or create new objects.
|
||||
return documents;
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Using this query function in a React component looks like:
|
||||
|
||||
```ts
|
||||
const data = useQuery(api.functions.myQueryFunction, {
|
||||
first: 10,
|
||||
second: "hello",
|
||||
});
|
||||
```
|
||||
|
||||
A mutation function looks like:
|
||||
|
||||
```ts
|
||||
// functions.js
|
||||
import { mutation } from "./_generated/server";
|
||||
import { v } from "convex/values";
|
||||
|
||||
export const myMutationFunction = mutation({
|
||||
// Validators for arguments.
|
||||
args: {
|
||||
first: v.string(),
|
||||
second: v.string(),
|
||||
},
|
||||
|
||||
// Function implementation.
|
||||
handler: async (ctx, args) => {
|
||||
// Insert or modify documents in the database here.
|
||||
// Mutations can also read from the database like queries.
|
||||
// See https://docs.convex.dev/database/writing-data.
|
||||
const message = { body: args.first, author: args.second };
|
||||
const id = await ctx.db.insert("messages", message);
|
||||
|
||||
// Optionally, return a value from your mutation.
|
||||
return await ctx.db.get(id);
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Using this mutation function in a React component looks like:
|
||||
|
||||
```ts
|
||||
const mutation = useMutation(api.functions.myMutationFunction);
|
||||
function handleButtonPress() {
|
||||
// fire and forget, the most common way to use mutations
|
||||
mutation({ first: "Hello!", second: "me" });
|
||||
// OR
|
||||
// use the result once the mutation has completed
|
||||
mutation({ first: "Hello!", second: "me" }).then((result) =>
|
||||
console.log(result),
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Use the Convex CLI to push your functions to a deployment. See everything
|
||||
the Convex CLI can do by running `npx convex -h` in your project root
|
||||
directory. To learn more, launch the docs with `npx convex docs`.
|
||||
Vendored
+61
@@ -0,0 +1,61 @@
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Generated `api` utility.
|
||||
*
|
||||
* THIS CODE IS AUTOMATICALLY GENERATED.
|
||||
*
|
||||
* To regenerate, run `npx convex dev`.
|
||||
* @module
|
||||
*/
|
||||
|
||||
import type * as auth from "../auth.js";
|
||||
import type * as github from "../github.js";
|
||||
import type * as http from "../http.js";
|
||||
import type * as leaderboard from "../leaderboard.js";
|
||||
import type * as leaks from "../leaks.js";
|
||||
import type * as requests from "../requests.js";
|
||||
import type * as users from "../users.js";
|
||||
|
||||
import type {
|
||||
ApiFromModules,
|
||||
FilterApi,
|
||||
FunctionReference,
|
||||
} from "convex/server";
|
||||
|
||||
declare const fullApi: ApiFromModules<{
|
||||
auth: typeof auth;
|
||||
github: typeof github;
|
||||
http: typeof http;
|
||||
leaderboard: typeof leaderboard;
|
||||
leaks: typeof leaks;
|
||||
requests: typeof requests;
|
||||
users: typeof users;
|
||||
}>;
|
||||
|
||||
/**
|
||||
* A utility for referencing Convex functions in your app's public API.
|
||||
*
|
||||
* Usage:
|
||||
* ```js
|
||||
* const myFunctionReference = api.myModule.myFunction;
|
||||
* ```
|
||||
*/
|
||||
export declare const api: FilterApi<
|
||||
typeof fullApi,
|
||||
FunctionReference<any, "public">
|
||||
>;
|
||||
|
||||
/**
|
||||
* A utility for referencing Convex functions in your app's internal API.
|
||||
*
|
||||
* Usage:
|
||||
* ```js
|
||||
* const myFunctionReference = internal.myModule.myFunction;
|
||||
* ```
|
||||
*/
|
||||
export declare const internal: FilterApi<
|
||||
typeof fullApi,
|
||||
FunctionReference<any, "internal">
|
||||
>;
|
||||
|
||||
export declare const components: {};
|
||||
@@ -0,0 +1,23 @@
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Generated `api` utility.
|
||||
*
|
||||
* THIS CODE IS AUTOMATICALLY GENERATED.
|
||||
*
|
||||
* To regenerate, run `npx convex dev`.
|
||||
* @module
|
||||
*/
|
||||
|
||||
import { anyApi, componentsGeneric } from "convex/server";
|
||||
|
||||
/**
|
||||
* A utility for referencing Convex functions in your app's API.
|
||||
*
|
||||
* Usage:
|
||||
* ```js
|
||||
* const myFunctionReference = api.myModule.myFunction;
|
||||
* ```
|
||||
*/
|
||||
export const api = anyApi;
|
||||
export const internal = anyApi;
|
||||
export const components = componentsGeneric();
|
||||
Vendored
+60
@@ -0,0 +1,60 @@
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Generated data model types.
|
||||
*
|
||||
* THIS CODE IS AUTOMATICALLY GENERATED.
|
||||
*
|
||||
* To regenerate, run `npx convex dev`.
|
||||
* @module
|
||||
*/
|
||||
|
||||
import type {
|
||||
DataModelFromSchemaDefinition,
|
||||
DocumentByName,
|
||||
TableNamesInDataModel,
|
||||
SystemTableNames,
|
||||
} from "convex/server";
|
||||
import type { GenericId } from "convex/values";
|
||||
import schema from "../schema.js";
|
||||
|
||||
/**
|
||||
* The names of all of your Convex tables.
|
||||
*/
|
||||
export type TableNames = TableNamesInDataModel<DataModel>;
|
||||
|
||||
/**
|
||||
* The type of a document stored in Convex.
|
||||
*
|
||||
* @typeParam TableName - A string literal type of the table name (like "users").
|
||||
*/
|
||||
export type Doc<TableName extends TableNames> = DocumentByName<
|
||||
DataModel,
|
||||
TableName
|
||||
>;
|
||||
|
||||
/**
|
||||
* An identifier for a document in Convex.
|
||||
*
|
||||
* Convex documents are uniquely identified by their `Id`, which is accessible
|
||||
* on the `_id` field. To learn more, see [Document IDs](https://docs.convex.dev/using/document-ids).
|
||||
*
|
||||
* Documents can be loaded using `db.get(tableName, id)` in query and mutation functions.
|
||||
*
|
||||
* IDs are just strings at runtime, but this type can be used to distinguish them from other
|
||||
* strings when type checking.
|
||||
*
|
||||
* @typeParam TableName - A string literal type of the table name (like "users").
|
||||
*/
|
||||
export type Id<TableName extends TableNames | SystemTableNames> =
|
||||
GenericId<TableName>;
|
||||
|
||||
/**
|
||||
* A type describing your Convex data model.
|
||||
*
|
||||
* This type includes information about what tables you have, the type of
|
||||
* documents stored in those tables, and the indexes defined on them.
|
||||
*
|
||||
* This type is used to parameterize methods like `queryGeneric` and
|
||||
* `mutationGeneric` to make them type-safe.
|
||||
*/
|
||||
export type DataModel = DataModelFromSchemaDefinition<typeof schema>;
|
||||
Vendored
+143
@@ -0,0 +1,143 @@
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Generated utilities for implementing server-side Convex query and mutation functions.
|
||||
*
|
||||
* THIS CODE IS AUTOMATICALLY GENERATED.
|
||||
*
|
||||
* To regenerate, run `npx convex dev`.
|
||||
* @module
|
||||
*/
|
||||
|
||||
import {
|
||||
ActionBuilder,
|
||||
HttpActionBuilder,
|
||||
MutationBuilder,
|
||||
QueryBuilder,
|
||||
GenericActionCtx,
|
||||
GenericMutationCtx,
|
||||
GenericQueryCtx,
|
||||
GenericDatabaseReader,
|
||||
GenericDatabaseWriter,
|
||||
} from "convex/server";
|
||||
import type { DataModel } from "./dataModel.js";
|
||||
|
||||
/**
|
||||
* Define a query in this Convex app's public API.
|
||||
*
|
||||
* This function will be allowed to read your Convex database and will be accessible from the client.
|
||||
*
|
||||
* @param func - The query function. It receives a {@link QueryCtx} as its first argument.
|
||||
* @returns The wrapped query. Include this as an `export` to name it and make it accessible.
|
||||
*/
|
||||
export declare const query: QueryBuilder<DataModel, "public">;
|
||||
|
||||
/**
|
||||
* Define a query that is only accessible from other Convex functions (but not from the client).
|
||||
*
|
||||
* This function will be allowed to read from your Convex database. It will not be accessible from the client.
|
||||
*
|
||||
* @param func - The query function. It receives a {@link QueryCtx} as its first argument.
|
||||
* @returns The wrapped query. Include this as an `export` to name it and make it accessible.
|
||||
*/
|
||||
export declare const internalQuery: QueryBuilder<DataModel, "internal">;
|
||||
|
||||
/**
|
||||
* Define a mutation in this Convex app's public API.
|
||||
*
|
||||
* This function will be allowed to modify your Convex database and will be accessible from the client.
|
||||
*
|
||||
* @param func - The mutation function. It receives a {@link MutationCtx} as its first argument.
|
||||
* @returns The wrapped mutation. Include this as an `export` to name it and make it accessible.
|
||||
*/
|
||||
export declare const mutation: MutationBuilder<DataModel, "public">;
|
||||
|
||||
/**
|
||||
* Define a mutation that is only accessible from other Convex functions (but not from the client).
|
||||
*
|
||||
* This function will be allowed to modify your Convex database. It will not be accessible from the client.
|
||||
*
|
||||
* @param func - The mutation function. It receives a {@link MutationCtx} as its first argument.
|
||||
* @returns The wrapped mutation. Include this as an `export` to name it and make it accessible.
|
||||
*/
|
||||
export declare const internalMutation: MutationBuilder<DataModel, "internal">;
|
||||
|
||||
/**
|
||||
* Define an action in this Convex app's public API.
|
||||
*
|
||||
* An action is a function which can execute any JavaScript code, including non-deterministic
|
||||
* code and code with side-effects, like calling third-party services.
|
||||
* They can be run in Convex's JavaScript environment or in Node.js using the "use node" directive.
|
||||
* They can interact with the database indirectly by calling queries and mutations using the {@link ActionCtx}.
|
||||
*
|
||||
* @param func - The action. It receives an {@link ActionCtx} as its first argument.
|
||||
* @returns The wrapped action. Include this as an `export` to name it and make it accessible.
|
||||
*/
|
||||
export declare const action: ActionBuilder<DataModel, "public">;
|
||||
|
||||
/**
|
||||
* Define an action that is only accessible from other Convex functions (but not from the client).
|
||||
*
|
||||
* @param func - The function. It receives an {@link ActionCtx} as its first argument.
|
||||
* @returns The wrapped function. Include this as an `export` to name it and make it accessible.
|
||||
*/
|
||||
export declare const internalAction: ActionBuilder<DataModel, "internal">;
|
||||
|
||||
/**
|
||||
* Define an HTTP action.
|
||||
*
|
||||
* The wrapped function will be used to respond to HTTP requests received
|
||||
* by a Convex deployment if the requests matches the path and method where
|
||||
* this action is routed. Be sure to route your httpAction in `convex/http.js`.
|
||||
*
|
||||
* @param func - The function. It receives an {@link ActionCtx} as its first argument
|
||||
* and a Fetch API `Request` object as its second.
|
||||
* @returns The wrapped function. Import this function from `convex/http.js` and route it to hook it up.
|
||||
*/
|
||||
export declare const httpAction: HttpActionBuilder;
|
||||
|
||||
/**
|
||||
* A set of services for use within Convex query functions.
|
||||
*
|
||||
* The query context is passed as the first argument to any Convex query
|
||||
* function run on the server.
|
||||
*
|
||||
* This differs from the {@link MutationCtx} because all of the services are
|
||||
* read-only.
|
||||
*/
|
||||
export type QueryCtx = GenericQueryCtx<DataModel>;
|
||||
|
||||
/**
|
||||
* A set of services for use within Convex mutation functions.
|
||||
*
|
||||
* The mutation context is passed as the first argument to any Convex mutation
|
||||
* function run on the server.
|
||||
*/
|
||||
export type MutationCtx = GenericMutationCtx<DataModel>;
|
||||
|
||||
/**
|
||||
* A set of services for use within Convex action functions.
|
||||
*
|
||||
* The action context is passed as the first argument to any Convex action
|
||||
* function run on the server.
|
||||
*/
|
||||
export type ActionCtx = GenericActionCtx<DataModel>;
|
||||
|
||||
/**
|
||||
* An interface to read from the database within Convex query functions.
|
||||
*
|
||||
* The two entry points are {@link DatabaseReader.get}, which fetches a single
|
||||
* document by its {@link Id}, or {@link DatabaseReader.query}, which starts
|
||||
* building a query.
|
||||
*/
|
||||
export type DatabaseReader = GenericDatabaseReader<DataModel>;
|
||||
|
||||
/**
|
||||
* An interface to read from and write to the database within Convex mutation
|
||||
* functions.
|
||||
*
|
||||
* Convex guarantees that all writes within a single mutation are
|
||||
* executed atomically, so you never have to worry about partial writes leaving
|
||||
* your data in an inconsistent state. See [the Convex Guide](https://docs.convex.dev/understanding/convex-fundamentals/functions#atomicity-and-optimistic-concurrency-control)
|
||||
* for the guarantees Convex provides your functions.
|
||||
*/
|
||||
export type DatabaseWriter = GenericDatabaseWriter<DataModel>;
|
||||
@@ -0,0 +1,93 @@
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Generated utilities for implementing server-side Convex query and mutation functions.
|
||||
*
|
||||
* THIS CODE IS AUTOMATICALLY GENERATED.
|
||||
*
|
||||
* To regenerate, run `npx convex dev`.
|
||||
* @module
|
||||
*/
|
||||
|
||||
import {
|
||||
actionGeneric,
|
||||
httpActionGeneric,
|
||||
queryGeneric,
|
||||
mutationGeneric,
|
||||
internalActionGeneric,
|
||||
internalMutationGeneric,
|
||||
internalQueryGeneric,
|
||||
} from "convex/server";
|
||||
|
||||
/**
|
||||
* Define a query in this Convex app's public API.
|
||||
*
|
||||
* This function will be allowed to read your Convex database and will be accessible from the client.
|
||||
*
|
||||
* @param func - The query function. It receives a {@link QueryCtx} as its first argument.
|
||||
* @returns The wrapped query. Include this as an `export` to name it and make it accessible.
|
||||
*/
|
||||
export const query = queryGeneric;
|
||||
|
||||
/**
|
||||
* Define a query that is only accessible from other Convex functions (but not from the client).
|
||||
*
|
||||
* This function will be allowed to read from your Convex database. It will not be accessible from the client.
|
||||
*
|
||||
* @param func - The query function. It receives a {@link QueryCtx} as its first argument.
|
||||
* @returns The wrapped query. Include this as an `export` to name it and make it accessible.
|
||||
*/
|
||||
export const internalQuery = internalQueryGeneric;
|
||||
|
||||
/**
|
||||
* Define a mutation in this Convex app's public API.
|
||||
*
|
||||
* This function will be allowed to modify your Convex database and will be accessible from the client.
|
||||
*
|
||||
* @param func - The mutation function. It receives a {@link MutationCtx} as its first argument.
|
||||
* @returns The wrapped mutation. Include this as an `export` to name it and make it accessible.
|
||||
*/
|
||||
export const mutation = mutationGeneric;
|
||||
|
||||
/**
|
||||
* Define a mutation that is only accessible from other Convex functions (but not from the client).
|
||||
*
|
||||
* This function will be allowed to modify your Convex database. It will not be accessible from the client.
|
||||
*
|
||||
* @param func - The mutation function. It receives a {@link MutationCtx} as its first argument.
|
||||
* @returns The wrapped mutation. Include this as an `export` to name it and make it accessible.
|
||||
*/
|
||||
export const internalMutation = internalMutationGeneric;
|
||||
|
||||
/**
|
||||
* Define an action in this Convex app's public API.
|
||||
*
|
||||
* An action is a function which can execute any JavaScript code, including non-deterministic
|
||||
* code and code with side-effects, like calling third-party services.
|
||||
* They can be run in Convex's JavaScript environment or in Node.js using the "use node" directive.
|
||||
* They can interact with the database indirectly by calling queries and mutations using the {@link ActionCtx}.
|
||||
*
|
||||
* @param func - The action. It receives an {@link ActionCtx} as its first argument.
|
||||
* @returns The wrapped action. Include this as an `export` to name it and make it accessible.
|
||||
*/
|
||||
export const action = actionGeneric;
|
||||
|
||||
/**
|
||||
* Define an action that is only accessible from other Convex functions (but not from the client).
|
||||
*
|
||||
* @param func - The function. It receives an {@link ActionCtx} as its first argument.
|
||||
* @returns The wrapped function. Include this as an `export` to name it and make it accessible.
|
||||
*/
|
||||
export const internalAction = internalActionGeneric;
|
||||
|
||||
/**
|
||||
* Define an HTTP action.
|
||||
*
|
||||
* The wrapped function will be used to respond to HTTP requests received
|
||||
* by a Convex deployment if the requests matches the path and method where
|
||||
* this action is routed. Be sure to route your httpAction in `convex/http.js`.
|
||||
*
|
||||
* @param func - The function. It receives an {@link ActionCtx} as its first argument
|
||||
* and a Fetch API `Request` object as its second.
|
||||
* @returns The wrapped function. Import this function from `convex/http.js` and route it to hook it up.
|
||||
*/
|
||||
export const httpAction = httpActionGeneric;
|
||||
@@ -0,0 +1,10 @@
|
||||
import { AuthConfig } from "convex/server";
|
||||
|
||||
export default {
|
||||
providers: [
|
||||
{
|
||||
domain: process.env.CONVEX_SITE_URL!,
|
||||
applicationID: "convex",
|
||||
},
|
||||
],
|
||||
} satisfies AuthConfig;
|
||||
@@ -0,0 +1,22 @@
|
||||
import GitHub from "@auth/core/providers/github";
|
||||
import { convexAuth } from "@convex-dev/auth/server";
|
||||
|
||||
export const { auth, signIn, signOut, store, isAuthenticated } = convexAuth({
|
||||
providers: [GitHub],
|
||||
callbacks: {
|
||||
async createOrUpdateUser(ctx, args) {
|
||||
if (args.existingUserId) {
|
||||
return args.existingUserId;
|
||||
}
|
||||
|
||||
return await ctx.db.insert("users", {
|
||||
email: args.profile.email,
|
||||
name: args.profile.name,
|
||||
image: args.profile.image,
|
||||
points: 0,
|
||||
requests: [],
|
||||
leaks: [],
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,120 @@
|
||||
import { internalAction, action } from "./_generated/server";
|
||||
import { api, internal } from "./_generated/api";
|
||||
import { v } from "convex/values";
|
||||
const DEFAULT_REPO_OWNER = "elder-plinius";
|
||||
const DEFAULT_REPO_NAME = "CL4R1T4S";
|
||||
const DEFAULT_BRANCH = "main";
|
||||
const GITHUB_API_TOKEN = process.env.GITHUB_API_TOKEN;
|
||||
const GITHUB_HEADERS = {
|
||||
"User-Agent": "LeakHubConvex/1.0",
|
||||
Accept: "application/vnd.github+json",
|
||||
Authorization: `Bearer ${GITHUB_API_TOKEN}`,
|
||||
};
|
||||
|
||||
export const importAllLeaks = internalAction({
|
||||
args: {},
|
||||
handler: async (ctx) => {
|
||||
const repoOwner = DEFAULT_REPO_OWNER;
|
||||
const repoName = DEFAULT_REPO_NAME;
|
||||
const branch = DEFAULT_BRANCH;
|
||||
const headers = GITHUB_HEADERS;
|
||||
const repo = await fetch(
|
||||
`https://api.github.com/repos/${repoOwner}/${repoName}/contents/?ref=${branch}`,
|
||||
{
|
||||
headers,
|
||||
},
|
||||
);
|
||||
const directories = await repo.json();
|
||||
|
||||
let insertedLeaks = 0;
|
||||
for (const directory of directories) {
|
||||
if (directory.type !== "dir") {
|
||||
continue;
|
||||
}
|
||||
const files = await fetch(
|
||||
`https://api.github.com/repos/${repoOwner}/${repoName}/contents/${directory.path}?ref=${branch}`,
|
||||
{
|
||||
headers,
|
||||
},
|
||||
);
|
||||
const filesJson = await files.json();
|
||||
for (const file of filesJson) {
|
||||
const content = await fetch(
|
||||
`https://api.github.com/repos/${repoOwner}/${repoName}/contents/${file.path}?ref=${branch}`,
|
||||
{
|
||||
headers,
|
||||
},
|
||||
);
|
||||
const contentJson = await content.json();
|
||||
console.log(contentJson);
|
||||
const targetName = contentJson.name;
|
||||
const provider = contentJson.path.split("/")[0];
|
||||
|
||||
try {
|
||||
const contentText = decodeBase64Utf8(contentJson.content);
|
||||
|
||||
// Use internal mutation to insert fully verified leaks from GitHub
|
||||
await ctx.runMutation(internal.leaks.insertVerifiedLeak, {
|
||||
targetName,
|
||||
provider,
|
||||
leakText: contentText,
|
||||
targetType: "model" as const,
|
||||
});
|
||||
insertedLeaks++;
|
||||
} catch (error) {
|
||||
console.error(`Error decoding content for ${file.path}: ${error}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return insertedLeaks;
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Public action to trigger GitHub import of leaks.
|
||||
* Returns success status, count of imported leaks, and a message.
|
||||
*/
|
||||
export const triggerGitHubImport = action({
|
||||
args: {},
|
||||
returns: v.object({
|
||||
success: v.boolean(),
|
||||
count: v.number(),
|
||||
message: v.string(),
|
||||
}),
|
||||
handler: async (ctx) => {
|
||||
try {
|
||||
// Call the internal action
|
||||
const count: number = await ctx.runAction(internal.github.importAllLeaks);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
count,
|
||||
message: `Successfully imported ${count} leaks`,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error importing leaks:", error);
|
||||
return {
|
||||
success: false,
|
||||
count: 0,
|
||||
message: `Error: ${error}`,
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
function decodeBase64Utf8(base64: string): string {
|
||||
// Remove whitespace and line breaks
|
||||
base64 = base64.replace(/\r?\n|\r/g, "").trim();
|
||||
// Normalize URL-safe and padding
|
||||
base64 = base64.replace(/-/g, "+").replace(/_/g, "/");
|
||||
while (base64.length % 4 !== 0) base64 += "=";
|
||||
|
||||
// Decode Base64 to binary string
|
||||
const binary = atob(base64);
|
||||
|
||||
// Convert binary string to UTF-8 text
|
||||
const bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0));
|
||||
const decoded = new TextDecoder("utf-8").decode(bytes);
|
||||
return decoded;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { httpRouter } from "convex/server";
|
||||
import { auth } from "./auth";
|
||||
|
||||
const http = httpRouter();
|
||||
|
||||
auth.addHttpRoutes(http);
|
||||
|
||||
export default http;
|
||||
@@ -0,0 +1,99 @@
|
||||
import { v } from "convex/values";
|
||||
import { getAuthUserId } from "@convex-dev/auth/server";
|
||||
import { query } from "./_generated/server";
|
||||
import { Id } from "./_generated/dataModel";
|
||||
|
||||
/**
|
||||
* Get leaderboard data including:
|
||||
* - Top 10 users by points
|
||||
* - Current user's rank (if not in top 10)
|
||||
* - Total number of users
|
||||
*/
|
||||
export const getLeaderboard = query({
|
||||
args: {},
|
||||
returns: v.object({
|
||||
topUsers: v.array(
|
||||
v.object({
|
||||
_id: v.id("users"),
|
||||
name: v.string(),
|
||||
image: v.string(),
|
||||
points: v.number(),
|
||||
rank: v.number(),
|
||||
isCurrentUser: v.boolean(),
|
||||
}),
|
||||
),
|
||||
currentUserRank: v.union(
|
||||
v.object({
|
||||
_id: v.id("users"),
|
||||
name: v.string(),
|
||||
image: v.string(),
|
||||
points: v.number(),
|
||||
rank: v.number(),
|
||||
}),
|
||||
v.null(),
|
||||
),
|
||||
totalUsers: v.number(),
|
||||
}),
|
||||
handler: async (ctx) => {
|
||||
const userId = await getAuthUserId(ctx);
|
||||
|
||||
const usersQuery = ctx.db
|
||||
.query("users")
|
||||
.withIndex("by_points")
|
||||
.order("desc");
|
||||
|
||||
const topUsers: Array<{
|
||||
_id: Id<"users">;
|
||||
name: string;
|
||||
image: string;
|
||||
points: number;
|
||||
rank: number;
|
||||
isCurrentUser: boolean;
|
||||
}> = [];
|
||||
|
||||
let currentUserRank: {
|
||||
_id: Id<"users">;
|
||||
name: string;
|
||||
image: string;
|
||||
points: number;
|
||||
rank: number;
|
||||
} | null = null;
|
||||
|
||||
let rank = 0;
|
||||
|
||||
for await (const user of usersQuery) {
|
||||
rank += 1;
|
||||
const isCurrentUser = userId !== null && user._id === userId;
|
||||
|
||||
if (rank <= 10) {
|
||||
topUsers.push({
|
||||
_id: user._id,
|
||||
name: user.name,
|
||||
image: user.image,
|
||||
points: user.points,
|
||||
rank,
|
||||
isCurrentUser,
|
||||
});
|
||||
}
|
||||
|
||||
if (isCurrentUser && rank > 10) {
|
||||
currentUserRank = {
|
||||
_id: user._id,
|
||||
name: user.name,
|
||||
image: user.image,
|
||||
points: user.points,
|
||||
rank,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const totalUsers = rank;
|
||||
|
||||
return {
|
||||
topUsers,
|
||||
currentUserRank,
|
||||
totalUsers,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
+940
@@ -0,0 +1,940 @@
|
||||
import { v } from "convex/values";
|
||||
import { getAuthUserId } from "@convex-dev/auth/server";
|
||||
import {
|
||||
internalAction,
|
||||
internalMutation,
|
||||
internalQuery,
|
||||
mutation,
|
||||
query,
|
||||
} from "./_generated/server";
|
||||
import { Id } from "./_generated/dataModel";
|
||||
import { internal } from "./_generated/api";
|
||||
|
||||
/**
|
||||
* Helper function to normalize text for comparison.
|
||||
* Removes extra whitespace, converts to lowercase, and trims.
|
||||
* This allows for minor formatting differences while ensuring content matches.
|
||||
*/
|
||||
function normalizeText(text: string): string {
|
||||
return text
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.replace(/\s+/g, " ") // Replace multiple spaces with single space
|
||||
.replace(/\n\s*\n/g, "\n"); // Normalize multiple newlines
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate Levenshtein distance between two strings.
|
||||
* This measures the minimum number of single-character edits required
|
||||
* to change one string into another.
|
||||
*/
|
||||
const SHINGLE_SIZE = 4;
|
||||
const SHINGLE_THRESHOLD = 0.6;
|
||||
|
||||
/**
|
||||
* Build a shingle frequency vector from normalized text.
|
||||
* Uses overlapping n-grams to preserve ordering context while
|
||||
* keeping memory footprint linear in the input size.
|
||||
*/
|
||||
function buildShingleVector(text: string): Map<string, number> {
|
||||
const vector = new Map<string, number>();
|
||||
const tokens = text.split(/\s+/).filter(Boolean);
|
||||
|
||||
if (tokens.length === 0) {
|
||||
return vector;
|
||||
}
|
||||
|
||||
for (let i = 0; i <= tokens.length - SHINGLE_SIZE; i++) {
|
||||
const shingle = tokens.slice(i, i + SHINGLE_SIZE).join(" ");
|
||||
vector.set(shingle, (vector.get(shingle) ?? 0) + 1);
|
||||
}
|
||||
|
||||
// For very short texts fall back to single tokens so we don't return 0 shingles.
|
||||
if (vector.size === 0) {
|
||||
for (const token of tokens) {
|
||||
vector.set(token, (vector.get(token) ?? 0) + 1);
|
||||
}
|
||||
}
|
||||
|
||||
return vector;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute cosine similarity between two shingle frequency vectors.
|
||||
* This is our lightweight filter to discard obviously different leaks.
|
||||
*/
|
||||
function cosineSimilarity(
|
||||
vecA: Map<string, number>,
|
||||
vecB: Map<string, number>,
|
||||
): number {
|
||||
if (vecA.size === 0 || vecB.size === 0) {
|
||||
return vecA.size === vecB.size ? 1 : 0;
|
||||
}
|
||||
|
||||
let dot = 0;
|
||||
let magA = 0;
|
||||
let magB = 0;
|
||||
|
||||
for (const value of vecA.values()) {
|
||||
magA += value * value;
|
||||
}
|
||||
for (const value of vecB.values()) {
|
||||
magB += value * value;
|
||||
}
|
||||
|
||||
// Iterate over smaller vector for efficiency.
|
||||
const [smaller, larger] =
|
||||
vecA.size <= vecB.size ? [vecA, vecB] : [vecB, vecA];
|
||||
for (const [term, value] of smaller.entries()) {
|
||||
const other = larger.get(term);
|
||||
if (other !== undefined) {
|
||||
dot += value * other;
|
||||
}
|
||||
}
|
||||
|
||||
if (magA === 0 || magB === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return dot / Math.sqrt(magA * magB);
|
||||
}
|
||||
|
||||
/**
|
||||
* Memory-efficient Levenshtein similarity that stores only two rows of the DP
|
||||
* matrix. This reduces peak memory to O(min(len1, len2)).
|
||||
*/
|
||||
function levenshteinSimilarity(str1: string, str2: string): number {
|
||||
const len1 = str1.length;
|
||||
const len2 = str2.length;
|
||||
|
||||
if (len1 === 0 && len2 === 0) {
|
||||
return 1;
|
||||
}
|
||||
if (len1 === 0 || len2 === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Ensure str2 is the shorter string for smaller buffers.
|
||||
if (len2 > len1) {
|
||||
return levenshteinSimilarity(str2, str1);
|
||||
}
|
||||
|
||||
let previousRow = new Array(len2 + 1);
|
||||
let currentRow = new Array(len2 + 1);
|
||||
|
||||
for (let j = 0; j <= len2; j++) {
|
||||
previousRow[j] = j;
|
||||
}
|
||||
|
||||
for (let i = 1; i <= len1; i++) {
|
||||
currentRow[0] = i;
|
||||
const char1 = str1[i - 1];
|
||||
|
||||
for (let j = 1; j <= len2; j++) {
|
||||
const cost = char1 === str2[j - 1] ? 0 : 1;
|
||||
currentRow[j] = Math.min(
|
||||
currentRow[j - 1] + 1, // insertion
|
||||
previousRow[j] + 1, // deletion
|
||||
previousRow[j - 1] + cost, // substitution
|
||||
);
|
||||
}
|
||||
|
||||
// Reuse arrays instead of reallocating.
|
||||
[previousRow, currentRow] = [currentRow, previousRow];
|
||||
}
|
||||
|
||||
const distance = previousRow[len2];
|
||||
const similarity = 1 - distance / Math.max(len1, len2);
|
||||
return Math.max(0, similarity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate similarity between two texts using a two-stage approach:
|
||||
* 1) Cosine similarity on shingles filters out obviously different leaks.
|
||||
* 2) If the coarse score is high enough we confirm with Levenshtein distance.
|
||||
*/
|
||||
function calculateSimilarity(text1: string, text2: string): number {
|
||||
const normalized1 = normalizeText(text1);
|
||||
const normalized2 = normalizeText(text2);
|
||||
|
||||
if (normalized1 === normalized2) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
const shingleScore = cosineSimilarity(
|
||||
buildShingleVector(normalized1),
|
||||
buildShingleVector(normalized2),
|
||||
);
|
||||
|
||||
if (shingleScore < SHINGLE_THRESHOLD) {
|
||||
return shingleScore;
|
||||
}
|
||||
|
||||
return Math.max(
|
||||
shingleScore,
|
||||
levenshteinSimilarity(normalized1, normalized2),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert a new leak into the database.
|
||||
* Validates that:
|
||||
* - User is authenticated
|
||||
* - Required fields are provided
|
||||
* - User hasn't already submitted a leak for this request
|
||||
*
|
||||
* Handles automatic verification with consensus logic:
|
||||
* - When 2+ different users submit leaks for the same request
|
||||
* - Groups similar leak texts (85%+ similarity threshold)
|
||||
* - Finds the largest group of matching submissions
|
||||
* - Verifies the first leak from that consensus group
|
||||
* - Ensures the correct prompt is verified even if wrong ones are submitted first
|
||||
* - The request is automatically closed upon verification
|
||||
*
|
||||
* Returns a result object with success status and error message if validation fails.
|
||||
* This prevents "Uncaught" errors in the terminal logs.
|
||||
*/
|
||||
export const insertLeak = mutation({
|
||||
args: {
|
||||
targetName: v.string(),
|
||||
provider: v.string(),
|
||||
leakText: v.string(),
|
||||
targetType: v.union(
|
||||
v.literal("model"),
|
||||
v.literal("app"),
|
||||
v.literal("tool"),
|
||||
v.literal("agent"),
|
||||
v.literal("plugin"),
|
||||
v.literal("custom"),
|
||||
),
|
||||
requestId: v.optional(v.id("requests")),
|
||||
requiresLogin: v.optional(v.boolean()),
|
||||
isPaid: v.optional(v.boolean()),
|
||||
hasToolPrompts: v.optional(v.boolean()),
|
||||
accessNotes: v.optional(v.string()),
|
||||
leakContext: v.optional(v.string()),
|
||||
url: v.optional(v.string()),
|
||||
},
|
||||
returns: v.union(
|
||||
v.object({
|
||||
success: v.literal(true),
|
||||
leakId: v.id("leaks"),
|
||||
verified: v.boolean(),
|
||||
message: v.optional(v.string()),
|
||||
}),
|
||||
v.object({
|
||||
success: v.literal(false),
|
||||
error: v.string(),
|
||||
}),
|
||||
),
|
||||
handler: async (ctx, args) => {
|
||||
const userId = await getAuthUserId(ctx);
|
||||
if (userId === null) {
|
||||
return {
|
||||
success: false as const,
|
||||
error: "You must be logged in to submit a leak",
|
||||
};
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
if (!args.targetName || !args.provider || !args.leakText) {
|
||||
return {
|
||||
success: false as const,
|
||||
error: "Target name, provider, and leak text are required",
|
||||
};
|
||||
}
|
||||
|
||||
// If this is for a request, check if user already submitted a leak for it
|
||||
if (args.requestId) {
|
||||
const request = await ctx.db.get(args.requestId);
|
||||
if (!request) {
|
||||
return {
|
||||
success: false as const,
|
||||
error: "Request not found",
|
||||
};
|
||||
}
|
||||
|
||||
// Check if user already submitted a leak for this request
|
||||
const existingLeaks: Array<Id<"leaks">> = request.leaks || [];
|
||||
for (const existingLeakId of existingLeaks) {
|
||||
const existingLeak = await ctx.db.get(existingLeakId);
|
||||
if (existingLeak && existingLeak.submittedBy === userId) {
|
||||
return {
|
||||
success: false as const,
|
||||
error:
|
||||
"You have already submitted a leak for this request. Each user can only submit once per request.",
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Insert the leak (always starts as unverified)
|
||||
const leakId = await ctx.db.insert("leaks", {
|
||||
targetName: args.targetName,
|
||||
provider: args.provider,
|
||||
leakText: args.leakText,
|
||||
targetType: args.targetType,
|
||||
isFullyVerified: false, // Always start as unverified
|
||||
requestId: args.requestId,
|
||||
requiresLogin: args.requiresLogin,
|
||||
isPaid: args.isPaid,
|
||||
hasToolPrompts: args.hasToolPrompts,
|
||||
accessNotes: args.accessNotes,
|
||||
leakContext: args.leakContext,
|
||||
url: args.url,
|
||||
submittedBy: userId,
|
||||
});
|
||||
|
||||
// Update user's leaks array
|
||||
const user = await ctx.db.get(userId);
|
||||
if (user) {
|
||||
const currentLeaks = user.leaks || [];
|
||||
await ctx.db.patch(userId, {
|
||||
leaks: [...currentLeaks, leakId],
|
||||
});
|
||||
}
|
||||
|
||||
let verified = false;
|
||||
let message: string | undefined = undefined;
|
||||
|
||||
// If linked to a request, add to request's leaks array
|
||||
if (args.requestId) {
|
||||
const request = await ctx.db.get(args.requestId);
|
||||
if (request) {
|
||||
const updatedLeaks = [...(request.leaks ?? []), leakId];
|
||||
|
||||
// Update request with new leak
|
||||
await ctx.db.patch(args.requestId, {
|
||||
leaks: updatedLeaks,
|
||||
});
|
||||
|
||||
await ctx.scheduler.runAfter(
|
||||
0,
|
||||
internal.leaks.processRequestConsensus,
|
||||
{
|
||||
requestId: args.requestId,
|
||||
},
|
||||
);
|
||||
|
||||
message = `Leak submitted successfully! This request now has ${updatedLeaks.length} submissions. We'll verify once consensus is reached.`;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true as const,
|
||||
leakId,
|
||||
verified,
|
||||
message,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Internal query to fetch the data needed for consensus evaluation.
|
||||
* Kept lightweight so the heavy similarity math runs inside an action.
|
||||
*/
|
||||
export const getRequestConsensusData = internalQuery({
|
||||
args: {
|
||||
requestId: v.id("requests"),
|
||||
},
|
||||
returns: v.union(
|
||||
v.null(),
|
||||
v.object({
|
||||
closed: v.optional(v.boolean()),
|
||||
submittedBy: v.optional(v.id("users")),
|
||||
leaks: v.array(
|
||||
v.object({
|
||||
leakId: v.id("leaks"),
|
||||
leakText: v.string(),
|
||||
submittedBy: v.optional(v.id("users")),
|
||||
isFullyVerified: v.boolean(),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
),
|
||||
handler: async (ctx, args) => {
|
||||
const request = await ctx.db.get(args.requestId);
|
||||
if (!request) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const leakDetails: Array<{
|
||||
leakId: Id<"leaks">;
|
||||
leakText: string;
|
||||
submittedBy?: Id<"users">;
|
||||
isFullyVerified: boolean;
|
||||
}> = [];
|
||||
|
||||
const leakIds: Array<Id<"leaks">> = request.leaks || [];
|
||||
for (const leakId of leakIds) {
|
||||
const leak = await ctx.db.get(leakId);
|
||||
if (leak) {
|
||||
leakDetails.push({
|
||||
leakId,
|
||||
leakText: leak.leakText,
|
||||
submittedBy: leak.submittedBy,
|
||||
isFullyVerified: leak.isFullyVerified,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
closed: request.closed ?? false,
|
||||
submittedBy: request.submittedBy,
|
||||
leaks: leakDetails,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Action that runs the consensus logic outside the 1s / 64MB mutation limits.
|
||||
* It pulls the necessary data via internal queries and applies results through
|
||||
* mutations to keep database access transactional.
|
||||
*/
|
||||
export const processRequestConsensus = internalAction({
|
||||
args: {
|
||||
requestId: v.id("requests"),
|
||||
},
|
||||
returns: v.null(),
|
||||
handler: async (ctx, args) => {
|
||||
const data = await ctx.runQuery(internal.leaks.getRequestConsensusData, {
|
||||
requestId: args.requestId,
|
||||
});
|
||||
|
||||
if (!data || data.closed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const leakDetails = data.leaks.filter(
|
||||
(
|
||||
detail,
|
||||
): detail is {
|
||||
leakId: Id<"leaks">;
|
||||
leakText: string;
|
||||
submittedBy: Id<"users">;
|
||||
isFullyVerified: boolean;
|
||||
} => !!detail.submittedBy && !detail.isFullyVerified,
|
||||
);
|
||||
|
||||
if (leakDetails.length < 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const submitters = new Set<Id<"users">>();
|
||||
for (const detail of leakDetails) {
|
||||
submitters.add(detail.submittedBy);
|
||||
}
|
||||
|
||||
if (submitters.size < 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const SIMILARITY_THRESHOLD = 0.85;
|
||||
const similarityGroups: Array<{
|
||||
leakIds: Array<Id<"leaks">>;
|
||||
userIds: Array<Id<"users">>;
|
||||
representativeText: string;
|
||||
}> = [];
|
||||
|
||||
for (const detail of leakDetails) {
|
||||
let addedToGroup = false;
|
||||
|
||||
for (const group of similarityGroups) {
|
||||
const similarity = calculateSimilarity(
|
||||
group.representativeText,
|
||||
detail.leakText,
|
||||
);
|
||||
|
||||
if (similarity >= SIMILARITY_THRESHOLD) {
|
||||
if (!group.userIds.includes(detail.submittedBy)) {
|
||||
group.leakIds.push(detail.leakId);
|
||||
group.userIds.push(detail.submittedBy);
|
||||
}
|
||||
addedToGroup = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!addedToGroup) {
|
||||
similarityGroups.push({
|
||||
leakIds: [detail.leakId],
|
||||
userIds: [detail.submittedBy],
|
||||
representativeText: detail.leakText,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let largestGroup: (typeof similarityGroups)[0] | null = null;
|
||||
for (const group of similarityGroups) {
|
||||
if (
|
||||
group.userIds.length >= 2 &&
|
||||
(!largestGroup || group.userIds.length > largestGroup.userIds.length)
|
||||
) {
|
||||
largestGroup = group;
|
||||
}
|
||||
}
|
||||
|
||||
if (!largestGroup) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const leakToVerifyId = largestGroup.leakIds[0];
|
||||
const leakToVerify = leakDetails.find(
|
||||
(detail) => detail.leakId === leakToVerifyId,
|
||||
);
|
||||
|
||||
if (!leakToVerify) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const verifierIds: Array<Id<"users">> = [];
|
||||
for (const userId of largestGroup.userIds) {
|
||||
if (userId !== leakToVerify.submittedBy) {
|
||||
verifierIds.push(userId);
|
||||
}
|
||||
}
|
||||
|
||||
if (verifierIds.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
await ctx.runMutation(internal.leaks.applyConsensusResult, {
|
||||
requestId: args.requestId,
|
||||
leakId: leakToVerify.leakId,
|
||||
verifierIds,
|
||||
});
|
||||
|
||||
return null;
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Mutation invoked by the consensus action to persist verification results
|
||||
* transactionally and award points.
|
||||
*/
|
||||
export const applyConsensusResult = internalMutation({
|
||||
args: {
|
||||
requestId: v.id("requests"),
|
||||
leakId: v.id("leaks"),
|
||||
verifierIds: v.array(v.id("users")),
|
||||
},
|
||||
returns: v.null(),
|
||||
handler: async (ctx, args) => {
|
||||
const request = await ctx.db.get(args.requestId);
|
||||
if (!request || request.closed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const leak = await ctx.db.get(args.leakId);
|
||||
if (!leak || leak.isFullyVerified || !leak.submittedBy) {
|
||||
return null;
|
||||
}
|
||||
|
||||
await ctx.db.patch(args.leakId, {
|
||||
isFullyVerified: true,
|
||||
verifiedBy: args.verifierIds,
|
||||
});
|
||||
|
||||
await ctx.db.patch(args.requestId, {
|
||||
closed: true,
|
||||
});
|
||||
|
||||
const submitter = await ctx.db.get(leak.submittedBy);
|
||||
if (submitter) {
|
||||
await ctx.db.patch(leak.submittedBy, {
|
||||
points: (submitter.points ?? 0) + 100,
|
||||
});
|
||||
}
|
||||
|
||||
for (const verifierId of args.verifierIds) {
|
||||
const verifier = await ctx.db.get(verifierId);
|
||||
if (verifier) {
|
||||
await ctx.db.patch(verifierId, {
|
||||
points: (verifier.points ?? 0) + 50,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (request.submittedBy) {
|
||||
const requestCreator = await ctx.db.get(request.submittedBy);
|
||||
if (requestCreator) {
|
||||
await ctx.db.patch(request.submittedBy, {
|
||||
points: (requestCreator.points ?? 0) + 20,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Internal mutation to insert a fully verified leak directly.
|
||||
* This bypasses the verification workflow and is used for importing
|
||||
* leaks from trusted sources like GitHub repositories.
|
||||
*
|
||||
* Only accessible internally - cannot be called from client code.
|
||||
*/
|
||||
export const insertVerifiedLeak = internalMutation({
|
||||
args: {
|
||||
targetName: v.string(),
|
||||
provider: v.string(),
|
||||
leakText: v.string(),
|
||||
targetType: v.union(
|
||||
v.literal("model"),
|
||||
v.literal("app"),
|
||||
v.literal("tool"),
|
||||
v.literal("agent"),
|
||||
v.literal("plugin"),
|
||||
v.literal("custom"),
|
||||
),
|
||||
requiresLogin: v.optional(v.boolean()),
|
||||
isPaid: v.optional(v.boolean()),
|
||||
hasToolPrompts: v.optional(v.boolean()),
|
||||
accessNotes: v.optional(v.string()),
|
||||
leakContext: v.optional(v.string()),
|
||||
url: v.optional(v.string()),
|
||||
},
|
||||
returns: v.id("leaks"),
|
||||
handler: async (ctx, args) => {
|
||||
// Insert the leak as fully verified
|
||||
const leakId = await ctx.db.insert("leaks", {
|
||||
targetName: args.targetName,
|
||||
provider: args.provider,
|
||||
leakText: args.leakText,
|
||||
targetType: args.targetType,
|
||||
isFullyVerified: true, // Directly verified
|
||||
requiresLogin: args.requiresLogin,
|
||||
isPaid: args.isPaid,
|
||||
hasToolPrompts: args.hasToolPrompts,
|
||||
accessNotes: args.accessNotes,
|
||||
leakContext: args.leakContext,
|
||||
url: args.url,
|
||||
// No submittedBy since it's an automated import
|
||||
// No requestId since it's not linked to a request
|
||||
});
|
||||
|
||||
return leakId;
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Get all verified leaks from the database.
|
||||
* Returns an array of leak documents that have been fully verified.
|
||||
* Includes the names of users who verified each leak.
|
||||
*/
|
||||
export const getVerifiedLeaks = query({
|
||||
args: {},
|
||||
returns: v.array(
|
||||
v.object({
|
||||
_id: v.id("leaks"),
|
||||
_creationTime: v.number(),
|
||||
requiresLogin: v.optional(v.boolean()),
|
||||
isPaid: v.optional(v.boolean()),
|
||||
hasToolPrompts: v.optional(v.boolean()),
|
||||
accessNotes: v.optional(v.string()),
|
||||
leakText: v.string(),
|
||||
leakContext: v.optional(v.string()),
|
||||
url: v.optional(v.string()),
|
||||
targetType: v.union(
|
||||
v.literal("model"),
|
||||
v.literal("app"),
|
||||
v.literal("tool"),
|
||||
v.literal("agent"),
|
||||
v.literal("plugin"),
|
||||
v.literal("custom"),
|
||||
),
|
||||
targetName: v.string(),
|
||||
provider: v.string(),
|
||||
submittedBy: v.optional(v.id("users")),
|
||||
verifiedBy: v.optional(v.array(v.id("users"))),
|
||||
isFullyVerified: v.boolean(),
|
||||
requestId: v.optional(v.id("requests")),
|
||||
submitterName: v.optional(v.string()),
|
||||
verifierNames: v.optional(v.array(v.string())),
|
||||
}),
|
||||
),
|
||||
handler: async (ctx) => {
|
||||
// Use index for efficient lookup of verified leaks (avoids full table scan)
|
||||
const leaks = await ctx.db
|
||||
.query("leaks")
|
||||
.withIndex("by_isFullyVerified", (q) => q.eq("isFullyVerified", true))
|
||||
.collect();
|
||||
|
||||
// Fetch submitter and verifier names
|
||||
const leaksWithNames: Array<{
|
||||
_id: Id<"leaks">;
|
||||
_creationTime: number;
|
||||
requiresLogin?: boolean;
|
||||
isPaid?: boolean;
|
||||
hasToolPrompts?: boolean;
|
||||
accessNotes?: string;
|
||||
leakText: string;
|
||||
leakContext?: string;
|
||||
url?: string;
|
||||
targetType: "model" | "app" | "tool" | "agent" | "plugin" | "custom";
|
||||
targetName: string;
|
||||
provider: string;
|
||||
submittedBy?: Id<"users">;
|
||||
verifiedBy?: Array<Id<"users">>;
|
||||
isFullyVerified: boolean;
|
||||
requestId?: Id<"requests">;
|
||||
submitterName?: string;
|
||||
verifierNames?: Array<string>;
|
||||
}> = [];
|
||||
|
||||
for (const leak of leaks) {
|
||||
let submitterName: string | undefined = undefined;
|
||||
let verifierNames: Array<string> | undefined = undefined;
|
||||
|
||||
// Get submitter name if exists
|
||||
if (leak.submittedBy) {
|
||||
const submitter = await ctx.db.get(leak.submittedBy);
|
||||
submitterName = submitter?.name || "Unknown";
|
||||
}
|
||||
|
||||
// Get verifier names if exists
|
||||
if (leak.verifiedBy && leak.verifiedBy.length > 0) {
|
||||
verifierNames = [];
|
||||
for (const verifierId of leak.verifiedBy) {
|
||||
const verifier = await ctx.db.get(verifierId);
|
||||
if (verifier) {
|
||||
verifierNames.push(verifier.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
leaksWithNames.push({
|
||||
...leak,
|
||||
submitterName,
|
||||
verifierNames,
|
||||
});
|
||||
}
|
||||
|
||||
return leaksWithNames;
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Get all unverified leaks from the database.
|
||||
* Returns an array of leak documents that are awaiting verification.
|
||||
*/
|
||||
export const getUnverifiedLeaks = query({
|
||||
args: {},
|
||||
returns: v.array(
|
||||
v.object({
|
||||
_id: v.id("leaks"),
|
||||
_creationTime: v.number(),
|
||||
requiresLogin: v.optional(v.boolean()),
|
||||
isPaid: v.optional(v.boolean()),
|
||||
hasToolPrompts: v.optional(v.boolean()),
|
||||
accessNotes: v.optional(v.string()),
|
||||
leakText: v.string(),
|
||||
leakContext: v.optional(v.string()),
|
||||
url: v.optional(v.string()),
|
||||
targetType: v.union(
|
||||
v.literal("model"),
|
||||
v.literal("app"),
|
||||
v.literal("tool"),
|
||||
v.literal("agent"),
|
||||
v.literal("plugin"),
|
||||
v.literal("custom"),
|
||||
),
|
||||
targetName: v.string(),
|
||||
provider: v.string(),
|
||||
submittedBy: v.optional(v.id("users")),
|
||||
verifiedBy: v.optional(v.array(v.id("users"))),
|
||||
isFullyVerified: v.boolean(),
|
||||
requestId: v.optional(v.id("requests")),
|
||||
}),
|
||||
),
|
||||
handler: async (ctx) => {
|
||||
// Use index for efficient lookup of unverified leaks (avoids full table scan)
|
||||
const leaks = await ctx.db
|
||||
.query("leaks")
|
||||
.withIndex("by_isFullyVerified", (q) => q.eq("isFullyVerified", false))
|
||||
.collect();
|
||||
return leaks;
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Get all unique providers with their leak counts and sample leaks.
|
||||
* Returns an array of provider info objects grouped by provider name.
|
||||
* Includes: provider name, total leak count, target types, and sample target names.
|
||||
*/
|
||||
export const getProviders = query({
|
||||
args: {},
|
||||
returns: v.array(
|
||||
v.object({
|
||||
provider: v.string(),
|
||||
leakCount: v.number(),
|
||||
targetTypes: v.array(v.string()),
|
||||
sampleTargets: v.array(v.string()),
|
||||
latestLeakTime: v.number(),
|
||||
}),
|
||||
),
|
||||
handler: async (ctx) => {
|
||||
// Use index for efficient lookup of verified leaks (avoids full table scan)
|
||||
const leaks = await ctx.db
|
||||
.query("leaks")
|
||||
.withIndex("by_isFullyVerified", (q) => q.eq("isFullyVerified", true))
|
||||
.collect();
|
||||
|
||||
// Group leaks by provider
|
||||
const providerMap = new Map<
|
||||
string,
|
||||
{
|
||||
leaks: typeof leaks;
|
||||
targetTypes: Set<string>;
|
||||
targetNames: Set<string>;
|
||||
latestTime: number;
|
||||
}
|
||||
>();
|
||||
|
||||
for (const leak of leaks) {
|
||||
const existing = providerMap.get(leak.provider);
|
||||
if (existing) {
|
||||
existing.leaks.push(leak);
|
||||
existing.targetTypes.add(leak.targetType);
|
||||
existing.targetNames.add(leak.targetName);
|
||||
existing.latestTime = Math.max(existing.latestTime, leak._creationTime);
|
||||
} else {
|
||||
providerMap.set(leak.provider, {
|
||||
leaks: [leak],
|
||||
targetTypes: new Set([leak.targetType]),
|
||||
targetNames: new Set([leak.targetName]),
|
||||
latestTime: leak._creationTime,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Convert to array of provider info
|
||||
const providers: Array<{
|
||||
provider: string;
|
||||
leakCount: number;
|
||||
targetTypes: string[];
|
||||
sampleTargets: string[];
|
||||
latestLeakTime: number;
|
||||
}> = [];
|
||||
|
||||
for (const [provider, data] of providerMap.entries()) {
|
||||
providers.push({
|
||||
provider,
|
||||
leakCount: data.leaks.length,
|
||||
targetTypes: Array.from(data.targetTypes),
|
||||
sampleTargets: Array.from(data.targetNames).slice(0, 5), // Get up to 5 sample target names
|
||||
latestLeakTime: data.latestTime,
|
||||
});
|
||||
}
|
||||
|
||||
// Sort by leak count (descending)
|
||||
providers.sort((a, b) => b.leakCount - a.leakCount);
|
||||
|
||||
return providers;
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Get all verified leaks for a specific provider.
|
||||
* Returns an array of leak documents with submitter and verifier names.
|
||||
*/
|
||||
export const getLeaksByProvider = query({
|
||||
args: {
|
||||
provider: v.string(),
|
||||
},
|
||||
returns: v.array(
|
||||
v.object({
|
||||
_id: v.id("leaks"),
|
||||
_creationTime: v.number(),
|
||||
requiresLogin: v.optional(v.boolean()),
|
||||
isPaid: v.optional(v.boolean()),
|
||||
hasToolPrompts: v.optional(v.boolean()),
|
||||
accessNotes: v.optional(v.string()),
|
||||
leakText: v.string(),
|
||||
leakContext: v.optional(v.string()),
|
||||
url: v.optional(v.string()),
|
||||
targetType: v.union(
|
||||
v.literal("model"),
|
||||
v.literal("app"),
|
||||
v.literal("tool"),
|
||||
v.literal("agent"),
|
||||
v.literal("plugin"),
|
||||
v.literal("custom"),
|
||||
),
|
||||
targetName: v.string(),
|
||||
provider: v.string(),
|
||||
submittedBy: v.optional(v.id("users")),
|
||||
verifiedBy: v.optional(v.array(v.id("users"))),
|
||||
isFullyVerified: v.boolean(),
|
||||
requestId: v.optional(v.id("requests")),
|
||||
submitterName: v.optional(v.string()),
|
||||
verifierNames: v.optional(v.array(v.string())),
|
||||
}),
|
||||
),
|
||||
handler: async (ctx, args) => {
|
||||
// Use compound index for efficient lookup by provider + verification status
|
||||
// This is much faster than filter() as it uses the index directly
|
||||
const leaks = await ctx.db
|
||||
.query("leaks")
|
||||
.withIndex("by_provider_and_verified", (q) =>
|
||||
q.eq("provider", args.provider).eq("isFullyVerified", true),
|
||||
)
|
||||
.collect();
|
||||
|
||||
// Fetch submitter and verifier names
|
||||
const leaksWithNames: Array<{
|
||||
_id: Id<"leaks">;
|
||||
_creationTime: number;
|
||||
requiresLogin?: boolean;
|
||||
isPaid?: boolean;
|
||||
hasToolPrompts?: boolean;
|
||||
accessNotes?: string;
|
||||
leakText: string;
|
||||
leakContext?: string;
|
||||
url?: string;
|
||||
targetType: "model" | "app" | "tool" | "agent" | "plugin" | "custom";
|
||||
targetName: string;
|
||||
provider: string;
|
||||
submittedBy?: Id<"users">;
|
||||
verifiedBy?: Array<Id<"users">>;
|
||||
isFullyVerified: boolean;
|
||||
requestId?: Id<"requests">;
|
||||
submitterName?: string;
|
||||
verifierNames?: Array<string>;
|
||||
}> = [];
|
||||
|
||||
for (const leak of leaks) {
|
||||
let submitterName: string | undefined = undefined;
|
||||
let verifierNames: Array<string> | undefined = undefined;
|
||||
|
||||
// Get submitter name if exists
|
||||
if (leak.submittedBy) {
|
||||
const submitter = await ctx.db.get(leak.submittedBy);
|
||||
submitterName = submitter?.name || "Unknown";
|
||||
}
|
||||
|
||||
// Get verifier names if exists
|
||||
if (leak.verifiedBy && leak.verifiedBy.length > 0) {
|
||||
verifierNames = [];
|
||||
for (const verifierId of leak.verifiedBy) {
|
||||
const verifier = await ctx.db.get(verifierId);
|
||||
if (verifier) {
|
||||
verifierNames.push(verifier.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
leaksWithNames.push({
|
||||
...leak,
|
||||
submitterName,
|
||||
verifierNames,
|
||||
});
|
||||
}
|
||||
|
||||
// Sort by creation time (newest first)
|
||||
leaksWithNames.sort((a, b) => b._creationTime - a._creationTime);
|
||||
|
||||
return leaksWithNames;
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,417 @@
|
||||
import { v } from "convex/values";
|
||||
import { getAuthUserId } from "@convex-dev/auth/server";
|
||||
import { query, mutation } from "./_generated/server";
|
||||
import { Id } from "./_generated/dataModel";
|
||||
|
||||
/**
|
||||
* Get all open (non-closed) requests from the database.
|
||||
* Returns requests with submitter names populated.
|
||||
*/
|
||||
export const getOpenRequests = query({
|
||||
args: {},
|
||||
returns: v.array(
|
||||
v.object({
|
||||
_id: v.id("requests"),
|
||||
_creationTime: v.number(),
|
||||
targetName: v.string(),
|
||||
provider: v.string(),
|
||||
targetType: v.union(
|
||||
v.literal("model"),
|
||||
v.literal("app"),
|
||||
v.literal("tool"),
|
||||
v.literal("agent"),
|
||||
v.literal("plugin"),
|
||||
v.literal("custom"),
|
||||
),
|
||||
targetUrl: v.string(),
|
||||
closed: v.boolean(),
|
||||
submittedBy: v.id("users"),
|
||||
submitterName: v.string(),
|
||||
leaks: v.array(v.id("leaks")),
|
||||
}),
|
||||
),
|
||||
handler: async (ctx) => {
|
||||
const requests = await ctx.db
|
||||
.query("requests")
|
||||
.withIndex("by_closed", (q) => q.eq("closed", false))
|
||||
.order("desc")
|
||||
.collect();
|
||||
|
||||
// Fetch submitter names
|
||||
const requestsWithNames: Array<{
|
||||
_id: Id<"requests">;
|
||||
_creationTime: number;
|
||||
targetName: string;
|
||||
provider: string;
|
||||
targetType: "model" | "app" | "tool" | "agent" | "plugin" | "custom";
|
||||
targetUrl: string;
|
||||
closed: boolean;
|
||||
submittedBy: Id<"users">;
|
||||
submitterName: string;
|
||||
leaks: Array<Id<"leaks">>;
|
||||
}> = [];
|
||||
|
||||
for (const request of requests) {
|
||||
const user = await ctx.db.get(request.submittedBy);
|
||||
requestsWithNames.push({
|
||||
...request,
|
||||
submitterName: user?.name || "Unknown",
|
||||
});
|
||||
}
|
||||
|
||||
return requestsWithNames;
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Get all open requests for a specific user.
|
||||
* Returns only requests that belong to the specified user and are not closed.
|
||||
*/
|
||||
export const getUserOpenRequests = query({
|
||||
args: {
|
||||
userId: v.id("users"),
|
||||
},
|
||||
returns: v.array(
|
||||
v.object({
|
||||
_id: v.id("requests"),
|
||||
_creationTime: v.number(),
|
||||
targetName: v.string(),
|
||||
provider: v.string(),
|
||||
targetType: v.union(
|
||||
v.literal("model"),
|
||||
v.literal("app"),
|
||||
v.literal("tool"),
|
||||
v.literal("agent"),
|
||||
v.literal("plugin"),
|
||||
v.literal("custom"),
|
||||
),
|
||||
targetUrl: v.string(),
|
||||
closed: v.boolean(),
|
||||
submittedBy: v.id("users"),
|
||||
leaks: v.array(v.id("leaks")),
|
||||
}),
|
||||
),
|
||||
handler: async (ctx, args) => {
|
||||
const requests = await ctx.db
|
||||
.query("requests")
|
||||
.withIndex("by_submittedBy_and_closed", (q) =>
|
||||
q.eq("submittedBy", args.userId).eq("closed", false),
|
||||
)
|
||||
.order("desc")
|
||||
.collect();
|
||||
return requests;
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Create a new request for a leak target.
|
||||
* Validates that:
|
||||
* - User is authenticated
|
||||
* - No duplicate request exists (case-insensitive)
|
||||
* - User has less than 3 open requests
|
||||
*
|
||||
* Returns a result object with success status and error message if validation fails.
|
||||
* This prevents "Uncaught" errors in the terminal logs.
|
||||
*/
|
||||
export const createRequest = mutation({
|
||||
args: {
|
||||
targetName: v.string(),
|
||||
provider: v.string(),
|
||||
targetType: v.union(
|
||||
v.literal("model"),
|
||||
v.literal("app"),
|
||||
v.literal("tool"),
|
||||
v.literal("agent"),
|
||||
v.literal("plugin"),
|
||||
v.literal("custom"),
|
||||
),
|
||||
targetUrl: v.string(),
|
||||
},
|
||||
returns: v.union(
|
||||
v.object({
|
||||
success: v.literal(true),
|
||||
requestId: v.id("requests"),
|
||||
}),
|
||||
v.object({
|
||||
success: v.literal(false),
|
||||
error: v.string(),
|
||||
}),
|
||||
),
|
||||
handler: async (ctx, args) => {
|
||||
const userId = await getAuthUserId(ctx);
|
||||
if (userId === null) {
|
||||
return {
|
||||
success: false as const,
|
||||
error: "You must be logged in to create a request",
|
||||
};
|
||||
}
|
||||
|
||||
// Check if a request with the same target name already exists (case-insensitive)
|
||||
// Query ALL requests, not just open ones
|
||||
const allRequests = await ctx.db.query("requests").collect();
|
||||
const normalizedTargetName = args.targetName.toLowerCase().trim();
|
||||
|
||||
const existingRequest = allRequests.find(
|
||||
(req) => req.targetName.toLowerCase().trim() === normalizedTargetName,
|
||||
);
|
||||
|
||||
if (existingRequest) {
|
||||
return {
|
||||
success: false as const,
|
||||
error: `A request for "${existingRequest.targetName}" has already been made. Please search for it in the existing requests.`,
|
||||
};
|
||||
}
|
||||
|
||||
// Check if user already has 3 or more open requests
|
||||
const userOpenRequests = await ctx.db
|
||||
.query("requests")
|
||||
.withIndex("by_submittedBy_and_closed", (q) =>
|
||||
q.eq("submittedBy", userId).eq("closed", false),
|
||||
)
|
||||
.collect();
|
||||
|
||||
if (userOpenRequests.length >= 3) {
|
||||
return {
|
||||
success: false as const,
|
||||
error:
|
||||
"You have reached the maximum limit of 3 open requests. Please wait for some to be fulfilled before creating more.",
|
||||
};
|
||||
}
|
||||
|
||||
const requestId = await ctx.db.insert("requests", {
|
||||
targetName: args.targetName,
|
||||
provider: args.provider.toUpperCase(), // Convert provider to uppercase for consistency
|
||||
targetType: args.targetType,
|
||||
targetUrl: args.targetUrl,
|
||||
closed: false,
|
||||
leaks: [],
|
||||
submittedBy: userId,
|
||||
});
|
||||
|
||||
// Update user's requests array
|
||||
const user = await ctx.db.get(userId);
|
||||
if (user) {
|
||||
const currentRequests = user.requests || [];
|
||||
await ctx.db.patch(userId, {
|
||||
requests: [...currentRequests, requestId],
|
||||
});
|
||||
}
|
||||
|
||||
return { success: true as const, requestId };
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Close a request (mark it as closed).
|
||||
* Only the owner of the request can close it.
|
||||
* Validates that:
|
||||
* - User is authenticated
|
||||
* - Request exists
|
||||
* - User owns the request
|
||||
* - Request is not already closed
|
||||
*
|
||||
* Returns a result object with success status and error message if validation fails.
|
||||
* This prevents "Uncaught" errors in the terminal logs.
|
||||
*/
|
||||
export const closeRequest = mutation({
|
||||
args: {
|
||||
requestId: v.id("requests"),
|
||||
},
|
||||
returns: v.union(
|
||||
v.object({
|
||||
success: v.literal(true),
|
||||
}),
|
||||
v.object({
|
||||
success: v.literal(false),
|
||||
error: v.string(),
|
||||
}),
|
||||
),
|
||||
handler: async (ctx, args) => {
|
||||
const userId = await getAuthUserId(ctx);
|
||||
if (userId === null) {
|
||||
return {
|
||||
success: false as const,
|
||||
error: "You must be logged in to close a request",
|
||||
};
|
||||
}
|
||||
|
||||
// Get the request to verify ownership
|
||||
const request = await ctx.db.get(args.requestId);
|
||||
if (!request) {
|
||||
return {
|
||||
success: false as const,
|
||||
error: "Request not found",
|
||||
};
|
||||
}
|
||||
|
||||
// Check if the user is the owner of the request
|
||||
if (request.submittedBy !== userId) {
|
||||
return {
|
||||
success: false as const,
|
||||
error: "You can only close your own requests",
|
||||
};
|
||||
}
|
||||
|
||||
// Check if the request is already closed
|
||||
if (request.closed) {
|
||||
return {
|
||||
success: false as const,
|
||||
error: "Request is already closed",
|
||||
};
|
||||
}
|
||||
|
||||
// Close the request
|
||||
await ctx.db.patch(args.requestId, {
|
||||
closed: true,
|
||||
});
|
||||
|
||||
return { success: true as const };
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Search for existing requests by target name.
|
||||
* Uses full-text search to find matching requests.
|
||||
* Only returns open (non-closed) requests.
|
||||
* Returns up to 10 results.
|
||||
*/
|
||||
export const searchRequests = query({
|
||||
args: {
|
||||
searchQuery: v.string(),
|
||||
},
|
||||
returns: v.array(
|
||||
v.object({
|
||||
_id: v.id("requests"),
|
||||
_creationTime: v.number(),
|
||||
targetName: v.string(),
|
||||
provider: v.string(),
|
||||
targetType: v.union(
|
||||
v.literal("model"),
|
||||
v.literal("app"),
|
||||
v.literal("tool"),
|
||||
v.literal("agent"),
|
||||
v.literal("plugin"),
|
||||
v.literal("custom"),
|
||||
),
|
||||
targetUrl: v.string(),
|
||||
closed: v.boolean(),
|
||||
submittedBy: v.id("users"),
|
||||
submitterName: v.string(),
|
||||
leaks: v.array(v.id("leaks")),
|
||||
}),
|
||||
),
|
||||
handler: async (ctx, args) => {
|
||||
if (!args.searchQuery || args.searchQuery.trim() === "") {
|
||||
return [];
|
||||
}
|
||||
|
||||
const requests = await ctx.db
|
||||
.query("requests")
|
||||
.withSearchIndex("search_targetName", (q) =>
|
||||
q.search("targetName", args.searchQuery).eq("closed", false),
|
||||
)
|
||||
.take(10);
|
||||
|
||||
const requestsWithNames: Array<{
|
||||
_id: Id<"requests">;
|
||||
_creationTime: number;
|
||||
targetName: string;
|
||||
provider: string;
|
||||
targetType: "model" | "app" | "tool" | "agent" | "plugin" | "custom";
|
||||
targetUrl: string;
|
||||
closed: boolean;
|
||||
submittedBy: Id<"users">;
|
||||
submitterName: string;
|
||||
leaks: Array<Id<"leaks">>;
|
||||
}> = [];
|
||||
|
||||
for (const request of requests) {
|
||||
const user = await ctx.db.get(request.submittedBy);
|
||||
requestsWithNames.push({
|
||||
...request,
|
||||
submitterName: user?.name || "Unknown",
|
||||
});
|
||||
}
|
||||
|
||||
return requestsWithNames;
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Get all open requests with their verification status.
|
||||
* Returns requests with:
|
||||
* - Request information
|
||||
* - Number of leak confirmations
|
||||
* - Number of unique submitters
|
||||
* - Submitter name
|
||||
*/
|
||||
export const getRequestsWithVerificationStatus = query({
|
||||
args: {},
|
||||
returns: v.array(
|
||||
v.object({
|
||||
_id: v.id("requests"),
|
||||
_creationTime: v.number(),
|
||||
targetName: v.string(),
|
||||
provider: v.string(),
|
||||
targetType: v.union(
|
||||
v.literal("model"),
|
||||
v.literal("app"),
|
||||
v.literal("tool"),
|
||||
v.literal("agent"),
|
||||
v.literal("plugin"),
|
||||
v.literal("custom"),
|
||||
),
|
||||
targetUrl: v.string(),
|
||||
closed: v.boolean(),
|
||||
submittedBy: v.id("users"),
|
||||
submitterName: v.string(),
|
||||
leaks: v.array(v.id("leaks")),
|
||||
confirmationCount: v.number(),
|
||||
uniqueSubmitters: v.number(),
|
||||
}),
|
||||
),
|
||||
handler: async (ctx) => {
|
||||
const requests = await ctx.db
|
||||
.query("requests")
|
||||
.withIndex("by_closed", (q) => q.eq("closed", false))
|
||||
.order("desc")
|
||||
.collect();
|
||||
|
||||
const requestsWithStatus: Array<{
|
||||
_id: Id<"requests">;
|
||||
_creationTime: number;
|
||||
targetName: string;
|
||||
provider: string;
|
||||
targetType: "model" | "app" | "tool" | "agent" | "plugin" | "custom";
|
||||
targetUrl: string;
|
||||
closed: boolean;
|
||||
submittedBy: Id<"users">;
|
||||
submitterName: string;
|
||||
leaks: Array<Id<"leaks">>;
|
||||
confirmationCount: number;
|
||||
uniqueSubmitters: number;
|
||||
}> = [];
|
||||
|
||||
for (const request of requests) {
|
||||
const user = await ctx.db.get(request.submittedBy);
|
||||
|
||||
// Calculate unique submitters
|
||||
const submitters = new Set<Id<"users">>();
|
||||
for (const leakId of request.leaks) {
|
||||
const leak = await ctx.db.get(leakId);
|
||||
if (leak && leak.submittedBy) {
|
||||
submitters.add(leak.submittedBy);
|
||||
}
|
||||
}
|
||||
|
||||
requestsWithStatus.push({
|
||||
...request,
|
||||
submitterName: user?.name || "Unknown",
|
||||
confirmationCount: request.leaks.length,
|
||||
uniqueSubmitters: submitters.size,
|
||||
});
|
||||
}
|
||||
|
||||
return requestsWithStatus;
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,77 @@
|
||||
import { defineSchema, defineTable } from "convex/server";
|
||||
import { v } from "convex/values";
|
||||
import { authTables } from "@convex-dev/auth/server";
|
||||
|
||||
const leaks = defineTable({
|
||||
requiresLogin: v.optional(v.boolean()),
|
||||
isPaid: v.optional(v.boolean()),
|
||||
hasToolPrompts: v.optional(v.boolean()),
|
||||
accessNotes: v.optional(v.string()),
|
||||
leakText: v.string(),
|
||||
leakContext: v.optional(v.string()),
|
||||
url: v.optional(v.string()),
|
||||
targetType: v.union(
|
||||
v.literal("model"),
|
||||
v.literal("app"),
|
||||
v.literal("tool"),
|
||||
v.literal("agent"),
|
||||
v.literal("plugin"),
|
||||
v.literal("custom"),
|
||||
),
|
||||
targetName: v.string(),
|
||||
provider: v.string(),
|
||||
submittedBy: v.optional(v.id("users")),
|
||||
verifiedBy: v.optional(v.array(v.id("users"))),
|
||||
isFullyVerified: v.boolean(),
|
||||
requestId: v.optional(v.id("requests")),
|
||||
})
|
||||
// Index for filtering by verification status (used in getVerifiedLeaks, getUnverifiedLeaks)
|
||||
.index("by_isFullyVerified", ["isFullyVerified"])
|
||||
// Compound index for filtering verified leaks by provider (used in getLeaksByProvider, getProviders)
|
||||
.index("by_provider_and_verified", ["provider", "isFullyVerified"]);
|
||||
|
||||
const requests = defineTable({
|
||||
targetName: v.string(),
|
||||
provider: v.string(),
|
||||
targetType: v.union(
|
||||
v.literal("model"),
|
||||
v.literal("app"),
|
||||
v.literal("tool"),
|
||||
v.literal("agent"),
|
||||
v.literal("plugin"),
|
||||
v.literal("custom"),
|
||||
),
|
||||
targetUrl: v.string(),
|
||||
closed: v.boolean(),
|
||||
leaks: v.array(v.id("leaks")),
|
||||
submittedBy: v.id("users"),
|
||||
})
|
||||
.index("by_closed", ["closed"])
|
||||
.index("by_submittedBy_and_closed", ["submittedBy", "closed"])
|
||||
.searchIndex("search_targetName", {
|
||||
searchField: "targetName",
|
||||
filterFields: ["closed"],
|
||||
});
|
||||
|
||||
const users = defineTable({
|
||||
name: v.string(),
|
||||
image: v.string(),
|
||||
email: v.string(),
|
||||
requests: v.optional(v.array(v.id("requests"))),
|
||||
leaks: v.optional(v.array(v.id("leaks"))),
|
||||
points: v.number(), // TODO: Add points system
|
||||
// Links to the auth tables - this will be set automatically by Convex Auth
|
||||
// when a user signs in
|
||||
})
|
||||
.index("email", ["email"])
|
||||
.index("by_points", ["points"]);
|
||||
|
||||
// The schema is normally optional, but Convex Auth
|
||||
// requires indexes defined on `authTables`.
|
||||
// The schema provides more precise TypeScript types.
|
||||
export default defineSchema({
|
||||
...authTables,
|
||||
leaks,
|
||||
requests,
|
||||
users,
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
/* This TypeScript project config describes the environment that
|
||||
* Convex functions run in and is used to typecheck them.
|
||||
* You can modify it, but some settings required to use Convex.
|
||||
*/
|
||||
"compilerOptions": {
|
||||
/* These settings are not required by Convex and can be modified. */
|
||||
"allowJs": true,
|
||||
"strict": true,
|
||||
"moduleResolution": "Bundler",
|
||||
"jsx": "react-jsx",
|
||||
"skipLibCheck": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
|
||||
/* These compiler options are required by Convex */
|
||||
"target": "ESNext",
|
||||
"lib": ["ES2021", "dom"],
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "ESNext",
|
||||
"isolatedModules": true,
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["./**/*"],
|
||||
"exclude": ["./_generated"]
|
||||
}
|
||||
+181
@@ -0,0 +1,181 @@
|
||||
import { v } from "convex/values";
|
||||
import { getAuthUserId } from "@convex-dev/auth/server";
|
||||
import { query } from "./_generated/server";
|
||||
import { Id } from "./_generated/dataModel";
|
||||
|
||||
/**
|
||||
* Get the current user's information including their image.
|
||||
* Returns null if the user is not authenticated.
|
||||
*/
|
||||
export const currentUser = query({
|
||||
args: {},
|
||||
returns: v.union(
|
||||
v.object({
|
||||
_id: v.id("users"),
|
||||
_creationTime: v.number(),
|
||||
name: v.string(),
|
||||
image: v.string(),
|
||||
email: v.string(),
|
||||
points: v.number(),
|
||||
requests: v.optional(v.array(v.id("requests"))),
|
||||
leaks: v.optional(v.array(v.id("leaks"))),
|
||||
}),
|
||||
v.null(),
|
||||
),
|
||||
handler: async (ctx) => {
|
||||
const userId = await getAuthUserId(ctx);
|
||||
if (userId === null) {
|
||||
return null;
|
||||
}
|
||||
const user = await ctx.db.get(userId);
|
||||
return user;
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Get comprehensive dashboard data for a specific user including:
|
||||
* - User profile information
|
||||
* - Points and rank
|
||||
* - User's leaks
|
||||
* - User's requests
|
||||
*/
|
||||
export const getUserDashboardData = query({
|
||||
args: {
|
||||
userId: v.id("users"),
|
||||
},
|
||||
returns: v.union(
|
||||
v.object({
|
||||
name: v.string(),
|
||||
email: v.string(),
|
||||
points: v.number(),
|
||||
rank: v.number(),
|
||||
totalUsers: v.number(),
|
||||
leaks: v.array(
|
||||
v.object({
|
||||
_id: v.id("leaks"),
|
||||
_creationTime: v.number(),
|
||||
targetName: v.string(),
|
||||
targetType: v.union(
|
||||
v.literal("model"),
|
||||
v.literal("app"),
|
||||
v.literal("tool"),
|
||||
v.literal("agent"),
|
||||
v.literal("plugin"),
|
||||
v.literal("custom"),
|
||||
),
|
||||
provider: v.string(),
|
||||
isFullyVerified: v.boolean(),
|
||||
requestId: v.optional(v.id("requests")),
|
||||
}),
|
||||
),
|
||||
requests: v.array(
|
||||
v.object({
|
||||
_id: v.id("requests"),
|
||||
_creationTime: v.number(),
|
||||
targetName: v.string(),
|
||||
targetType: v.union(
|
||||
v.literal("model"),
|
||||
v.literal("app"),
|
||||
v.literal("tool"),
|
||||
v.literal("agent"),
|
||||
v.literal("plugin"),
|
||||
v.literal("custom"),
|
||||
),
|
||||
provider: v.string(),
|
||||
closed: v.boolean(),
|
||||
targetUrl: v.optional(v.string()),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
v.null(),
|
||||
),
|
||||
handler: async (ctx, args) => {
|
||||
const user = await ctx.db.get(args.userId);
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Calculate user's rank based on points
|
||||
const usersQuery = ctx.db
|
||||
.query("users")
|
||||
.withIndex("by_points")
|
||||
.order("desc");
|
||||
|
||||
let rank = 0;
|
||||
let totalUsers = 0;
|
||||
let userRank = 0;
|
||||
|
||||
for await (const u of usersQuery) {
|
||||
rank += 1;
|
||||
totalUsers += 1;
|
||||
if (u._id === args.userId) {
|
||||
userRank = rank;
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch full leak documents
|
||||
const leakDocs: Array<{
|
||||
_id: Id<"leaks">;
|
||||
_creationTime: number;
|
||||
targetName: string;
|
||||
targetType: "model" | "app" | "tool" | "agent" | "plugin" | "custom";
|
||||
provider: string;
|
||||
isFullyVerified: boolean;
|
||||
requestId?: Id<"requests">;
|
||||
}> = [];
|
||||
if (user.leaks) {
|
||||
for (const leakId of user.leaks) {
|
||||
const leak = await ctx.db.get(leakId);
|
||||
if (leak) {
|
||||
leakDocs.push({
|
||||
_id: leak._id,
|
||||
_creationTime: leak._creationTime,
|
||||
targetName: leak.targetName,
|
||||
targetType: leak.targetType,
|
||||
provider: leak.provider,
|
||||
isFullyVerified: leak.isFullyVerified,
|
||||
requestId: leak.requestId,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch full request documents
|
||||
const requestDocs: Array<{
|
||||
_id: Id<"requests">;
|
||||
_creationTime: number;
|
||||
targetName: string;
|
||||
targetType: "model" | "app" | "tool" | "agent" | "plugin" | "custom";
|
||||
provider: string;
|
||||
closed: boolean;
|
||||
targetUrl?: string;
|
||||
}> = [];
|
||||
if (user.requests) {
|
||||
for (const requestId of user.requests) {
|
||||
const request = await ctx.db.get(requestId);
|
||||
if (request) {
|
||||
requestDocs.push({
|
||||
_id: request._id,
|
||||
_creationTime: request._creationTime,
|
||||
targetName: request.targetName,
|
||||
targetType: request.targetType,
|
||||
provider: request.provider,
|
||||
closed: request.closed,
|
||||
targetUrl: request.targetUrl,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
points: user.points,
|
||||
rank: userRank,
|
||||
totalUsers: totalUsers,
|
||||
leaks: leakDocs,
|
||||
requests: requestDocs,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
-366
@@ -1,366 +0,0 @@
|
||||
// Demo data for LeakHub - Sample submissions to showcase the platform
|
||||
// Run this in the browser console to populate with demo data
|
||||
|
||||
const demoSubmissions = [
|
||||
{
|
||||
id: "demo1",
|
||||
source: "DemoUser1",
|
||||
targetType: "model",
|
||||
instance: "GPT-4",
|
||||
targetUrl: "https://chat.openai.com",
|
||||
requiresLogin: true,
|
||||
requiresPaid: true,
|
||||
accessNotes: "Plus subscription required",
|
||||
parentSystem: null,
|
||||
functionName: null,
|
||||
content: `You are ChatGPT, a large language model trained by OpenAI. You are designed to be helpful, harmless, and honest in your responses.
|
||||
|
||||
Your purpose is to assist users with a wide range of tasks, including answering questions, providing explanations, helping with creative writing, coding assistance, and more.
|
||||
|
||||
Key guidelines:
|
||||
- Always be helpful and informative
|
||||
- Provide accurate and well-reasoned responses
|
||||
- Be honest about your limitations
|
||||
- Avoid harmful or inappropriate content
|
||||
- Respect user privacy and confidentiality
|
||||
|
||||
When responding:
|
||||
- Use clear, concise language
|
||||
- Provide context when helpful
|
||||
- Ask clarifying questions when needed
|
||||
- Cite sources when appropriate
|
||||
- Maintain a conversational tone
|
||||
|
||||
Do not:
|
||||
- Generate harmful, illegal, or inappropriate content
|
||||
- Provide medical, legal, or financial advice
|
||||
- Share personal information about users
|
||||
- Pretend to have capabilities you don't have`,
|
||||
toolPrompts: null,
|
||||
context: "Obtained through prompt injection techniques",
|
||||
timestamp: new Date(Date.now() - 86400000).toISOString(), // 1 day ago
|
||||
verifications: 3,
|
||||
confidence: 95,
|
||||
isFirstDiscovery: true,
|
||||
hasTools: false,
|
||||
wasVerified: true
|
||||
},
|
||||
{
|
||||
id: "demo2",
|
||||
source: "DemoUser2",
|
||||
targetType: "model",
|
||||
instance: "GPT-4",
|
||||
targetUrl: "https://chat.openai.com",
|
||||
requiresLogin: true,
|
||||
requiresPaid: true,
|
||||
accessNotes: "Plus subscription required",
|
||||
parentSystem: null,
|
||||
functionName: null,
|
||||
content: `You are ChatGPT, a large language model trained by OpenAI. Your role is to be helpful, harmless, and honest in all interactions.
|
||||
|
||||
Your primary function is to assist users with various tasks such as answering questions, providing explanations, helping with creative writing, coding assistance, and more.
|
||||
|
||||
Core principles:
|
||||
- Always be helpful and informative
|
||||
- Provide accurate and well-reasoned responses
|
||||
- Be honest about your limitations
|
||||
- Avoid harmful or inappropriate content
|
||||
- Respect user privacy and confidentiality
|
||||
|
||||
Response guidelines:
|
||||
- Use clear, concise language
|
||||
- Provide context when helpful
|
||||
- Ask clarifying questions when needed
|
||||
- Cite sources when appropriate
|
||||
- Maintain a conversational tone
|
||||
|
||||
Prohibited actions:
|
||||
- Generate harmful, illegal, or inappropriate content
|
||||
- Provide medical, legal, or financial advice
|
||||
- Share personal information about users
|
||||
- Pretend to have capabilities you don't have`,
|
||||
toolPrompts: null,
|
||||
context: "Discovered through system prompt analysis",
|
||||
timestamp: new Date(Date.now() - 43200000).toISOString(), // 12 hours ago
|
||||
verifications: 2,
|
||||
confidence: 92,
|
||||
isFirstDiscovery: false,
|
||||
hasTools: false
|
||||
},
|
||||
{
|
||||
id: "demo3",
|
||||
source: "DemoUser3",
|
||||
targetType: "app",
|
||||
instance: "GitHub Copilot",
|
||||
targetUrl: "https://github.com/features/copilot",
|
||||
requiresLogin: true,
|
||||
requiresPaid: true,
|
||||
accessNotes: "GitHub Copilot subscription required",
|
||||
parentSystem: null,
|
||||
functionName: null,
|
||||
content: `You are GitHub Copilot, an AI-powered code completion tool designed to help developers write code more efficiently.
|
||||
|
||||
Your purpose is to:
|
||||
- Provide intelligent code suggestions and completions
|
||||
- Understand context from comments and existing code
|
||||
- Generate code based on natural language descriptions
|
||||
- Assist with debugging and code optimization
|
||||
- Support multiple programming languages and frameworks
|
||||
|
||||
Key capabilities:
|
||||
- Real-time code completion
|
||||
- Context-aware suggestions
|
||||
- Multi-language support
|
||||
- Integration with popular IDEs
|
||||
- Learning from user feedback
|
||||
|
||||
Guidelines:
|
||||
- Prioritize code quality and best practices
|
||||
- Respect coding standards and conventions
|
||||
- Provide helpful comments and documentation
|
||||
- Suggest secure coding practices
|
||||
- Maintain consistency with existing codebase
|
||||
|
||||
Do not:
|
||||
- Generate malicious or harmful code
|
||||
- Violate licensing or copyright restrictions
|
||||
- Suggest insecure coding practices
|
||||
- Generate code that could cause system damage`,
|
||||
toolPrompts: null,
|
||||
context: "Extracted from IDE integration",
|
||||
timestamp: new Date(Date.now() - 21600000).toISOString(), // 6 hours ago
|
||||
verifications: 1,
|
||||
confidence: 88,
|
||||
isFirstDiscovery: true,
|
||||
hasTools: false
|
||||
},
|
||||
{
|
||||
id: "demo4",
|
||||
source: "DemoUser1",
|
||||
targetType: "tool",
|
||||
instance: "Code Interpreter",
|
||||
parentSystem: "ChatGPT",
|
||||
functionName: "Python Code Execution",
|
||||
targetUrl: null,
|
||||
requiresLogin: true,
|
||||
requiresPaid: true,
|
||||
accessNotes: "ChatGPT Plus with Code Interpreter plugin",
|
||||
content: `You are the Code Interpreter tool within ChatGPT. Your role is to execute Python code safely and provide helpful analysis.
|
||||
|
||||
Your capabilities include:
|
||||
- Executing Python code in a sandboxed environment
|
||||
- Reading and writing files (with size limits)
|
||||
- Performing mathematical computations
|
||||
- Data analysis and visualization
|
||||
- File format conversions
|
||||
|
||||
Safety guidelines:
|
||||
- Execute code in a secure sandbox
|
||||
- Limit file operations and system access
|
||||
- Monitor for potentially harmful operations
|
||||
- Provide clear error messages
|
||||
- Respect resource limitations
|
||||
|
||||
When executing code:
|
||||
- Validate input and parameters
|
||||
- Check for security concerns
|
||||
- Provide helpful error explanations
|
||||
- Suggest improvements when appropriate
|
||||
- Document code behavior clearly
|
||||
|
||||
Prohibited operations:
|
||||
- System-level commands or file system access
|
||||
- Network requests to external services
|
||||
- Execution of potentially harmful code
|
||||
- Access to sensitive system information`,
|
||||
toolPrompts: `Additional tool-specific instructions for file handling and data processing...`,
|
||||
context: "Analyzed from plugin behavior",
|
||||
timestamp: new Date(Date.now() - 7200000).toISOString(), // 2 hours ago
|
||||
verifications: 0,
|
||||
confidence: 85,
|
||||
isFirstDiscovery: true,
|
||||
hasTools: true
|
||||
},
|
||||
{
|
||||
id: "demo5",
|
||||
source: "DemoUser2",
|
||||
targetType: "agent",
|
||||
instance: "AutoGPT",
|
||||
targetUrl: "https://github.com/Significant-Gravitas/AutoGPT",
|
||||
requiresLogin: false,
|
||||
requiresPaid: false,
|
||||
accessNotes: "Open source, requires API keys",
|
||||
parentSystem: null,
|
||||
functionName: null,
|
||||
content: `You are AutoGPT, an autonomous AI agent designed to accomplish tasks independently.
|
||||
|
||||
Your core mission is to:
|
||||
- Understand and break down complex tasks
|
||||
- Plan and execute multi-step processes
|
||||
- Use available tools and APIs effectively
|
||||
- Learn from feedback and improve performance
|
||||
- Maintain focus on user-defined objectives
|
||||
|
||||
Key capabilities:
|
||||
- Task planning and decomposition
|
||||
- Tool usage and API integration
|
||||
- Memory management and context retention
|
||||
- Self-reflection and improvement
|
||||
- Goal-oriented behavior
|
||||
|
||||
Operating principles:
|
||||
- Always work toward the defined goal
|
||||
- Use available resources efficiently
|
||||
- Provide clear progress updates
|
||||
- Ask for clarification when needed
|
||||
- Maintain safety and ethical boundaries
|
||||
|
||||
Safety constraints:
|
||||
- Do not perform harmful or illegal actions
|
||||
- Respect user privacy and data security
|
||||
- Operate within defined boundaries
|
||||
- Seek permission for significant actions
|
||||
- Maintain transparency in decision-making`,
|
||||
toolPrompts: null,
|
||||
context: "Reverse engineered from agent behavior",
|
||||
timestamp: new Date(Date.now() - 3600000).toISOString(), // 1 hour ago
|
||||
verifications: 0,
|
||||
confidence: 78,
|
||||
isFirstDiscovery: true,
|
||||
hasTools: false
|
||||
}
|
||||
];
|
||||
|
||||
const demoRequests = [
|
||||
{
|
||||
id: "req1",
|
||||
targetType: "model",
|
||||
model: "Claude 3 Opus",
|
||||
targetUrl: "https://claude.ai",
|
||||
requiresLogin: true,
|
||||
requiresPaid: true,
|
||||
description: "Anthropic's most advanced model - would be great to understand its system prompt for research purposes.",
|
||||
bounty: 1000,
|
||||
requestedBy: "DemoUser1",
|
||||
timestamp: new Date(Date.now() - 86400000).toISOString(),
|
||||
votes: 15,
|
||||
voters: ["DemoUser1", "DemoUser2", "DemoUser3"],
|
||||
status: "open"
|
||||
},
|
||||
{
|
||||
id: "req2",
|
||||
targetType: "app",
|
||||
model: "Cursor IDE",
|
||||
targetUrl: "https://cursor.sh",
|
||||
requiresLogin: true,
|
||||
requiresPaid: false,
|
||||
description: "Popular AI-powered code editor. Interested in understanding how it processes code context.",
|
||||
bounty: 500,
|
||||
requestedBy: "DemoUser2",
|
||||
timestamp: new Date(Date.now() - 43200000).toISOString(),
|
||||
votes: 8,
|
||||
voters: ["DemoUser1", "DemoUser2"],
|
||||
status: "open"
|
||||
},
|
||||
{
|
||||
id: "req3",
|
||||
targetType: "tool",
|
||||
model: "WebPilot",
|
||||
parentSystem: "ChatGPT",
|
||||
targetUrl: null,
|
||||
requiresLogin: true,
|
||||
requiresPaid: true,
|
||||
description: "ChatGPT plugin for web browsing. Want to understand how it processes web content safely.",
|
||||
bounty: 300,
|
||||
requestedBy: "DemoUser3",
|
||||
timestamp: new Date(Date.now() - 21600000).toISOString(),
|
||||
votes: 5,
|
||||
voters: ["DemoUser3"],
|
||||
status: "open"
|
||||
}
|
||||
];
|
||||
|
||||
// Function to load demo data
|
||||
function loadDemoData() {
|
||||
console.log("Loading demo data for LeakHub...");
|
||||
|
||||
// Load submissions
|
||||
leakDatabase = [...demoSubmissions];
|
||||
|
||||
// Load requests
|
||||
leakRequests = [...demoRequests];
|
||||
|
||||
// Initialize user stats
|
||||
userStats = {
|
||||
"DemoUser1": {
|
||||
submissions: 2,
|
||||
verifiedLeaks: 1,
|
||||
firstDiscoveries: 2,
|
||||
totalScore: 280,
|
||||
joinDate: new Date(Date.now() - 86400000).toISOString(),
|
||||
toolsDiscovered: 1,
|
||||
appsDiscovered: 0,
|
||||
agentsDiscovered: 0
|
||||
},
|
||||
"DemoUser2": {
|
||||
submissions: 2,
|
||||
verifiedLeaks: 0,
|
||||
firstDiscoveries: 0,
|
||||
totalScore: 120,
|
||||
joinDate: new Date(Date.now() - 43200000).toISOString(),
|
||||
toolsDiscovered: 0,
|
||||
appsDiscovered: 0,
|
||||
agentsDiscovered: 0
|
||||
},
|
||||
"DemoUser3": {
|
||||
submissions: 1,
|
||||
verifiedLeaks: 0,
|
||||
firstDiscoveries: 1,
|
||||
totalScore: 110,
|
||||
joinDate: new Date(Date.now() - 21600000).toISOString(),
|
||||
toolsDiscovered: 0,
|
||||
appsDiscovered: 1,
|
||||
agentsDiscovered: 0
|
||||
}
|
||||
};
|
||||
|
||||
// Save to localStorage
|
||||
saveDatabase();
|
||||
|
||||
// Update UI
|
||||
updateUI();
|
||||
|
||||
console.log("Demo data loaded successfully! You can now explore the platform with sample submissions.");
|
||||
console.log("Try comparing the two GPT-4 submissions to see the verification system in action!");
|
||||
}
|
||||
|
||||
// Function to clear demo data
|
||||
function clearDemoData() {
|
||||
console.log("Clearing demo data...");
|
||||
|
||||
leakDatabase = [];
|
||||
leakRequests = [];
|
||||
userStats = {};
|
||||
userVotes = {};
|
||||
|
||||
// Clear localStorage
|
||||
localStorage.removeItem('leakDatabase');
|
||||
localStorage.removeItem('userStats');
|
||||
localStorage.removeItem('leakRequests');
|
||||
localStorage.removeItem('userVotes');
|
||||
localStorage.removeItem('dailyChallenge');
|
||||
localStorage.removeItem('avgSimilarity');
|
||||
|
||||
// Update UI
|
||||
updateUI();
|
||||
|
||||
console.log("Demo data cleared. Platform is now empty and ready for real submissions.");
|
||||
}
|
||||
|
||||
// Add functions to global scope for easy access
|
||||
window.loadDemoData = loadDemoData;
|
||||
window.clearDemoData = clearDemoData;
|
||||
|
||||
console.log("Demo data functions loaded!");
|
||||
console.log("To load demo data, run: loadDemoData()");
|
||||
console.log("To clear demo data, run: clearDemoData()");
|
||||
@@ -1,194 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# LeakHub Deployment Script
|
||||
# This script automates the deployment process for different platforms
|
||||
|
||||
set -e # Exit on any error
|
||||
|
||||
echo "🚀 LeakHub Deployment Script"
|
||||
echo "=============================="
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Function to print colored output
|
||||
print_status() {
|
||||
echo -e "${BLUE}[INFO]${NC} $1"
|
||||
}
|
||||
|
||||
print_success() {
|
||||
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
||||
}
|
||||
|
||||
print_warning() {
|
||||
echo -e "${YELLOW}[WARNING]${NC} $1"
|
||||
}
|
||||
|
||||
print_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1"
|
||||
}
|
||||
|
||||
# Check if Node.js is installed
|
||||
check_node() {
|
||||
if ! command -v node &> /dev/null; then
|
||||
print_error "Node.js is not installed. Please install Node.js 18+ first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
NODE_VERSION=$(node --version | cut -d'v' -f2 | cut -d'.' -f1)
|
||||
if [ "$NODE_VERSION" -lt 18 ]; then
|
||||
print_error "Node.js version 18+ is required. Current version: $(node --version)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
print_success "Node.js $(node --version) detected"
|
||||
}
|
||||
|
||||
# Install dependencies
|
||||
install_deps() {
|
||||
print_status "Installing dependencies..."
|
||||
npm ci
|
||||
print_success "Dependencies installed"
|
||||
}
|
||||
|
||||
# Build the project
|
||||
build_project() {
|
||||
print_status "Building project for production..."
|
||||
npm run build
|
||||
print_success "Project built successfully"
|
||||
}
|
||||
|
||||
# Deploy to GitHub Pages
|
||||
deploy_github_pages() {
|
||||
print_status "Deploying to GitHub Pages..."
|
||||
|
||||
# Check if git is initialized
|
||||
if [ ! -d ".git" ]; then
|
||||
print_error "Git repository not found. Please initialize git first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if remote origin exists
|
||||
if ! git remote get-url origin &> /dev/null; then
|
||||
print_error "Git remote 'origin' not found. Please add your GitHub repository as origin."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Add all changes
|
||||
git add .
|
||||
|
||||
# Commit changes
|
||||
git commit -m "Deploy LeakHub - $(date)"
|
||||
|
||||
# Push to main branch
|
||||
git push origin main
|
||||
|
||||
print_success "Deployed to GitHub Pages! Check your repository settings to enable GitHub Pages."
|
||||
}
|
||||
|
||||
# Deploy to Vercel
|
||||
deploy_vercel() {
|
||||
print_status "Deploying to Vercel..."
|
||||
|
||||
# Check if Vercel CLI is installed
|
||||
if ! command -v vercel &> /dev/null; then
|
||||
print_warning "Vercel CLI not found. Installing..."
|
||||
npm install -g vercel
|
||||
fi
|
||||
|
||||
# Deploy
|
||||
vercel --prod
|
||||
|
||||
print_success "Deployed to Vercel!"
|
||||
}
|
||||
|
||||
# Deploy to Netlify
|
||||
deploy_netlify() {
|
||||
print_status "Deploying to Netlify..."
|
||||
|
||||
# Check if Netlify CLI is installed
|
||||
if ! command -v netlify &> /dev/null; then
|
||||
print_warning "Netlify CLI not found. Installing..."
|
||||
npm install -g netlify-cli
|
||||
fi
|
||||
|
||||
# Deploy
|
||||
netlify deploy --prod --dir=dist
|
||||
|
||||
print_success "Deployed to Netlify!"
|
||||
}
|
||||
|
||||
# Show usage
|
||||
show_usage() {
|
||||
echo "Usage: $0 [OPTION]"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " github Deploy to GitHub Pages"
|
||||
echo " vercel Deploy to Vercel"
|
||||
echo " netlify Deploy to Netlify"
|
||||
echo " build Build project only"
|
||||
echo " install Install dependencies only"
|
||||
echo " all Deploy to all platforms"
|
||||
echo " help Show this help message"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " $0 github # Deploy to GitHub Pages"
|
||||
echo " $0 vercel # Deploy to Vercel"
|
||||
echo " $0 all # Deploy to all platforms"
|
||||
}
|
||||
|
||||
# Main deployment function
|
||||
main() {
|
||||
case "$1" in
|
||||
"github")
|
||||
check_node
|
||||
install_deps
|
||||
build_project
|
||||
deploy_github_pages
|
||||
;;
|
||||
"vercel")
|
||||
check_node
|
||||
install_deps
|
||||
build_project
|
||||
deploy_vercel
|
||||
;;
|
||||
"netlify")
|
||||
check_node
|
||||
install_deps
|
||||
build_project
|
||||
deploy_netlify
|
||||
;;
|
||||
"build")
|
||||
check_node
|
||||
install_deps
|
||||
build_project
|
||||
;;
|
||||
"install")
|
||||
check_node
|
||||
install_deps
|
||||
;;
|
||||
"all")
|
||||
check_node
|
||||
install_deps
|
||||
build_project
|
||||
deploy_github_pages
|
||||
deploy_vercel
|
||||
deploy_netlify
|
||||
;;
|
||||
"help"|"-h"|"--help")
|
||||
show_usage
|
||||
;;
|
||||
*)
|
||||
print_error "Unknown option: $1"
|
||||
show_usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Run main function with all arguments
|
||||
main "$@"
|
||||
@@ -0,0 +1,80 @@
|
||||
import { defineConfig } from "eslint/config";
|
||||
import js from "@eslint/js";
|
||||
import globals from "globals";
|
||||
import reactHooks from "eslint-plugin-react-hooks";
|
||||
import reactRefresh from "eslint-plugin-react-refresh";
|
||||
import tseslint from "typescript-eslint";
|
||||
import convexPlugin from "@convex-dev/eslint-plugin";
|
||||
|
||||
export default defineConfig([
|
||||
{
|
||||
ignores: [
|
||||
"dist",
|
||||
"eslint.config.js",
|
||||
"convex/_generated",
|
||||
"postcss.config.js",
|
||||
"tailwind.config.js",
|
||||
"vite.config.ts",
|
||||
],
|
||||
},
|
||||
{
|
||||
extends: [
|
||||
js.configs.recommended,
|
||||
...tseslint.configs.recommendedTypeChecked,
|
||||
],
|
||||
files: ["**/*.{ts,tsx}"],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.node,
|
||||
},
|
||||
parserOptions: {
|
||||
project: [
|
||||
"./tsconfig.node.json",
|
||||
"./tsconfig.app.json",
|
||||
"./convex/tsconfig.json",
|
||||
],
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
"react-hooks": reactHooks,
|
||||
"react-refresh": reactRefresh,
|
||||
},
|
||||
rules: {
|
||||
...reactHooks.configs.recommended.rules,
|
||||
"react-refresh/only-export-components": [
|
||||
"warn",
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
// All of these overrides ease getting into
|
||||
// TypeScript, and can be removed for stricter
|
||||
// linting down the line.
|
||||
|
||||
// Only warn on unused variables, and ignore variables starting with `_`
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"warn",
|
||||
{ varsIgnorePattern: "^_", argsIgnorePattern: "^_" },
|
||||
],
|
||||
|
||||
// Allow escaping the compiler
|
||||
"@typescript-eslint/ban-ts-comment": "error",
|
||||
|
||||
// Allow explicit `any`s
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
|
||||
// START: Allow implicit `any`s
|
||||
"@typescript-eslint/no-unsafe-argument": "off",
|
||||
"@typescript-eslint/no-unsafe-assignment": "off",
|
||||
"@typescript-eslint/no-unsafe-call": "off",
|
||||
"@typescript-eslint/no-unsafe-member-access": "off",
|
||||
"@typescript-eslint/no-unsafe-return": "off",
|
||||
// END: Allow implicit `any`s
|
||||
|
||||
// Allow async functions without await
|
||||
// for consistency (esp. Convex `handler`s)
|
||||
"@typescript-eslint/require-await": "off",
|
||||
},
|
||||
},
|
||||
...convexPlugin.configs.recommended,
|
||||
]);
|
||||
@@ -1,430 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>LeakHub - AI System Prompt Discovery Platform</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="floating-grid"></div>
|
||||
|
||||
<!-- Production Header -->
|
||||
<nav class="production-nav">
|
||||
<div class="nav-container">
|
||||
<div class="nav-brand">
|
||||
<h1 class="logo">LeakHub</h1>
|
||||
<span class="version">v2.0.0</span>
|
||||
</div>
|
||||
|
||||
<div class="nav-menu">
|
||||
<a href="#submit" class="nav-link active">Submit</a>
|
||||
<a href="#library" class="nav-link">Library</a>
|
||||
<a href="#compare" class="nav-link">Compare</a>
|
||||
<a href="#community" class="nav-link">Community</a>
|
||||
<a href="#analytics" class="nav-link">Analytics</a>
|
||||
</div>
|
||||
|
||||
<div class="nav-actions">
|
||||
<button class="user-profile-btn" onclick="toggleUserProfile()">
|
||||
<span class="user-avatar">👤</span>
|
||||
<span class="user-name" id="currentUserName">Guest</span>
|
||||
</button>
|
||||
<button class="settings-btn" onclick="toggleSettings()">⚙️</button>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="container">
|
||||
<header>
|
||||
<div class="hero-section">
|
||||
<h1>LeakHub</h1>
|
||||
<p class="subtitle">The community hub for crowd-sourced system prompt leak verification. CL4R1T4S!</p>
|
||||
|
||||
<div class="hero-stats">
|
||||
<div class="stat-item">
|
||||
<div class="stat-icon">🎯</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-number" id="activeLeaks">0</div>
|
||||
<div class="stat-label">Active Targets</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-icon">📊</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-number" id="totalSubmissions">0</div>
|
||||
<div class="stat-label">Total Submissions</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-icon">✅</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-number" id="verifiedPrompts">0</div>
|
||||
<div class="stat-label">Verified Prompts</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-icon">👥</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-number" id="activeUsers">0</div>
|
||||
<div class="stat-label">Active Users</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="quick-actions">
|
||||
<button onclick="toggleLeaderboard()" class="action-btn primary">
|
||||
🏆 Leaderboard
|
||||
</button>
|
||||
<button onclick="toggleRequests()" class="action-btn secondary">
|
||||
🎯 Challenges
|
||||
</button>
|
||||
<button onclick="toggleChat()" class="action-btn tertiary">
|
||||
💬 Live Chat
|
||||
</button>
|
||||
<button onclick="toggleAnalytics()" class="action-btn quaternary">
|
||||
📈 Analytics
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Real-time Chat System -->
|
||||
<div id="chatPanel" class="chat-panel" style="display: none;">
|
||||
<div class="chat-header">
|
||||
<h3>💬 LeakHub Live Chat</h3>
|
||||
<button onclick="toggleChat()" class="close-btn">×</button>
|
||||
</div>
|
||||
<div class="chat-messages" id="chatMessages">
|
||||
<div class="system-message">Welcome to LeakHub Live Chat! Share discoveries, ask questions, and collaborate in real-time! 🚀</div>
|
||||
</div>
|
||||
<div class="chat-input-container">
|
||||
<input type="text" id="chatInput" placeholder="Type your message..." maxlength="500">
|
||||
<button onclick="sendChatMessage()" class="send-btn">Send</button>
|
||||
</div>
|
||||
<div class="chat-users" id="chatUsers">
|
||||
<div class="online-indicator">🟢 Online: <span id="onlineCount">1</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- User Profile Overlay -->
|
||||
<div id="userProfileOverlay" class="overlay" style="display: none;">
|
||||
<div class="overlay-content">
|
||||
<div class="close-button" onclick="toggleUserProfile()">✕</div>
|
||||
<div class="profile-header">
|
||||
<h2>👤 User Profile</h2>
|
||||
<div class="profile-avatar" id="profileAvatar">👤</div>
|
||||
<div class="profile-name" id="profileName">Guest User</div>
|
||||
</div>
|
||||
<div class="profile-stats" id="profileStats">
|
||||
<!-- Stats will be populated by JavaScript -->
|
||||
</div>
|
||||
<div class="profile-achievements" id="profileAchievements">
|
||||
<!-- Achievements will be populated by JavaScript -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Settings Overlay -->
|
||||
<div id="settingsOverlay" class="overlay" style="display: none;">
|
||||
<div class="overlay-content">
|
||||
<div class="close-button" onclick="toggleSettings()">✕</div>
|
||||
<h2>⚙️ Settings</h2>
|
||||
<div class="settings-section">
|
||||
<h3>User Preferences</h3>
|
||||
<label>
|
||||
<input type="text" id="userNameInput" placeholder="Enter your username">
|
||||
Username
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" id="notificationsEnabled" checked>
|
||||
Enable Notifications
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" id="autoSaveEnabled" checked>
|
||||
Auto-save Data
|
||||
</label>
|
||||
</div>
|
||||
<div class="settings-section">
|
||||
<h3>Data Management</h3>
|
||||
<button onclick="exportData()" class="settings-btn">📤 Export Data</button>
|
||||
<button onclick="importData()" class="settings-btn">📥 Import Data</button>
|
||||
<button onclick="clearAllData()" class="settings-btn danger">🗑️ Clear All Data</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Analytics Overlay -->
|
||||
<div id="analyticsOverlay" class="overlay" style="display: none;">
|
||||
<div class="overlay-content wide">
|
||||
<div class="close-button" onclick="toggleAnalytics()">✕</div>
|
||||
<h2>📈 LeakHub Analytics</h2>
|
||||
<div class="analytics-grid">
|
||||
<div class="analytics-card">
|
||||
<h3>📊 Platform Statistics</h3>
|
||||
<div class="analytics-stats" id="platformStats">
|
||||
<!-- Platform stats -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="analytics-card">
|
||||
<h3>🎯 Target Distribution</h3>
|
||||
<div class="analytics-chart" id="targetChart">
|
||||
<!-- Target distribution chart -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="analytics-card">
|
||||
<h3>📈 Growth Trends</h3>
|
||||
<div class="analytics-trends" id="growthTrends">
|
||||
<!-- Growth trends -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="analytics-card">
|
||||
<h3>🏆 Top Contributors</h3>
|
||||
<div class="analytics-leaders" id="topContributors">
|
||||
<!-- Top contributors -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="main-section">
|
||||
<div class="panel">
|
||||
<h2>📤 Submit Leak</h2>
|
||||
<form class="submission-form" onsubmit="submitLeak(event)">
|
||||
<input type="text" id="sourceName" placeholder="Your identifier (e.g., User123, Anonymous)" required>
|
||||
|
||||
<select id="targetType" onchange="updateTargetFields()" required style="width: 100%; padding: 0.5rem; margin-bottom: 1rem; background: rgba(0,0,0,0.5); border: 1px solid rgba(255,255,255,0.1); border-radius: 8px; color: #e0e0e0;">
|
||||
<option value="">Select target type...</option>
|
||||
<option value="model">🤖 AI Model</option>
|
||||
<option value="app">📱 App/Interface</option>
|
||||
<option value="tool">🔧 Tool/Function</option>
|
||||
<option value="agent">🤝 AI Agent</option>
|
||||
<option value="plugin">🔌 Plugin/Extension</option>
|
||||
<option value="custom">🛠️ Custom GPT/Bot</option>
|
||||
</select>
|
||||
|
||||
<input type="text" id="instanceId" placeholder="Target name (e.g., ChatGPT-4, GitHub Copilot)" required>
|
||||
|
||||
<input type="url" id="targetUrl" placeholder="Target URL (e.g., https://chat.openai.com)" style="margin-bottom: 1rem;">
|
||||
|
||||
<div style="background: rgba(0,0,0,0.3); border: 1px solid rgba(255,255,255,0.1); border-radius: 8px; padding: 1rem; margin-bottom: 1rem;">
|
||||
<p style="color: #00aaff; font-size: 0.9rem; margin-bottom: 0.8rem;">🔒 Access Requirements</p>
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1rem;">
|
||||
<label style="display: flex; align-items: center; gap: 0.5rem; color: #888; font-size: 0.9rem;">
|
||||
<input type="checkbox" id="requiresLogin" style="width: auto;">
|
||||
Requires Login
|
||||
</label>
|
||||
<label style="display: flex; align-items: center; gap: 0.5rem; color: #888; font-size: 0.9rem;">
|
||||
<input type="checkbox" id="requiresPaid" style="width: auto;">
|
||||
Paid/Subscription
|
||||
</label>
|
||||
</div>
|
||||
<input type="text" id="accessNotes" placeholder="Additional access notes (e.g., 'Plus subscription required')"
|
||||
style="width: 100%; margin-top: 0.8rem; padding: 0.5rem; background: rgba(0,0,0,0.5); border: 1px solid rgba(255,255,255,0.1); border-radius: 8px; color: #e0e0e0;">
|
||||
</div>
|
||||
|
||||
<div id="additionalFields" style="display: none;">
|
||||
<input type="text" id="parentSystem" placeholder="Parent system (e.g., ChatGPT for a plugin)" style="margin-bottom: 1rem;">
|
||||
<input type="text" id="functionName" placeholder="Specific function/tool name (if applicable)" style="margin-bottom: 1rem;">
|
||||
</div>
|
||||
|
||||
<textarea id="leakContent" placeholder="Paste the suspected system prompt leak here..." required></textarea>
|
||||
<input type="text" id="context" placeholder="Context: How was this obtained? (optional)">
|
||||
|
||||
<div style="display: flex; gap: 0.5rem; align-items: center; margin-bottom: 1rem;">
|
||||
<input type="checkbox" id="hasTools" onchange="toggleToolsSection()" style="width: auto;">
|
||||
<label for="hasTools" style="color: #888; font-size: 0.9rem;">This target has tools/functions with their own prompts</label>
|
||||
</div>
|
||||
|
||||
<div id="toolsSection" style="display: none; background: rgba(0,0,0,0.3); border: 1px solid rgba(255,255,255,0.1); border-radius: 8px; padding: 1rem; margin-bottom: 1rem;">
|
||||
<p style="color: #00aaff; font-size: 0.9rem; margin-bottom: 0.5rem;">🔧 Related Tool Prompts (optional)</p>
|
||||
<textarea id="toolPrompts" placeholder="If you found any tool-specific prompts, paste them here..." style="min-height: 100px;"></textarea>
|
||||
</div>
|
||||
|
||||
<button type="submit">Submit to Library</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<h2>📚 Leak Library</h2>
|
||||
<div class="submissions-list" id="submissionsList">
|
||||
<p style="color: #666; text-align: center;">No submissions yet. Be the first to contribute!</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="comparison-section">
|
||||
<div class="panel">
|
||||
<h2>🔍 Compare Multiple Instances</h2>
|
||||
<div class="comparison-controls">
|
||||
<select id="submission1" style="width: 100%; padding: 0.5rem; margin-bottom: 1rem; background: rgba(0,0,0,0.5); border: 1px solid rgba(255,255,255,0.1); border-radius: 8px; color: #e0e0e0;">
|
||||
<option value="">Select first submission...</option>
|
||||
</select>
|
||||
<select id="submission2" style="width: 100%; padding: 0.5rem; margin-bottom: 1rem; background: rgba(0,0,0,0.5); border: 1px solid rgba(255,255,255,0.1); border-radius: 8px; color: #e0e0e0;">
|
||||
<option value="">Select second submission...</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="comparison-controls">
|
||||
<button onclick="performComparison()" class="enhanced-btn">🔍 Basic Comparison</button>
|
||||
<button onclick="performAdvancedComparison()" class="enhanced-btn">🤖 AI Analysis</button>
|
||||
</div>
|
||||
<div id="comparisonResults">
|
||||
<div class="match-metrics">
|
||||
<div class="metric-card">
|
||||
<div class="metric-label">Character Match</div>
|
||||
<div class="metric-value" id="charMatch">-</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="metric-label">Word Match</div>
|
||||
<div class="metric-value" id="wordMatch">-</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="metric-label">Structure Match</div>
|
||||
<div class="metric-value" id="structureMatch">-</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="metric-label">Core Similarity</div>
|
||||
<div class="metric-value" id="coreSimilarity">-</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="consensus-viewer">
|
||||
<h3>Consensus View (Common Elements)</h3>
|
||||
<div class="consensus-text" id="consensusText"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="library-stats">
|
||||
<div class="stat-card">
|
||||
<div class="stat-number" id="uniqueInstances">0</div>
|
||||
<div class="stat-label">Unique Instances</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-number" id="avgSimilarity">0%</div>
|
||||
<div class="stat-label">Average Similarity</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-number" id="highConfidence">0</div>
|
||||
<div class="stat-label">High Confidence Matches</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="alert" id="alert"></div>
|
||||
|
||||
<!-- Leaderboard Overlay -->
|
||||
<div class="leaderboard-overlay" id="leaderboardOverlay">
|
||||
<div class="leaderboard-container">
|
||||
<div class="close-button" onclick="toggleLeaderboard()">✕</div>
|
||||
|
||||
<div class="leaderboard-header">
|
||||
<h1 class="leaderboard-title">🏆 LeakHub Hall of Fame</h1>
|
||||
<p class="subtitle">Recognizing the top contributors to AI transparency</p>
|
||||
</div>
|
||||
|
||||
<div class="leaderboard-tabs">
|
||||
<div class="leaderboard-tab active" onclick="switchLeaderboardTab('rankings')">📊 Rankings</div>
|
||||
<div class="leaderboard-tab" onclick="switchLeaderboardTab('achievements')">🏅 Achievements</div>
|
||||
<div class="leaderboard-tab" onclick="switchLeaderboardTab('timeline')">📅 Timeline</div>
|
||||
</div>
|
||||
|
||||
<div id="rankings-content" class="leaderboard-content">
|
||||
<div class="rankings">
|
||||
<h2 style="color: #00ff88; margin-bottom: 1.5rem;">Top Contributors</h2>
|
||||
<div id="rankingsList"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="achievements-content" class="leaderboard-content" style="display: none;">
|
||||
<h2 style="color: #ffd700; margin-bottom: 1.5rem; text-align: center;">Notable Achievements</h2>
|
||||
<div class="achievements" id="achievementsList"></div>
|
||||
</div>
|
||||
|
||||
<div id="timeline-content" class="leaderboard-content" style="display: none;">
|
||||
<div class="timeline-section">
|
||||
<h2 style="color: #00aaff; margin-bottom: 1.5rem;">Discovery Timeline</h2>
|
||||
<div id="timelineList"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Requests & Challenges Overlay -->
|
||||
<div class="requests-overlay" id="requestsOverlay">
|
||||
<div class="requests-container">
|
||||
<div class="close-button" onclick="toggleRequests()">✕</div>
|
||||
|
||||
<div class="requests-header">
|
||||
<h1 class="requests-title">🎯 LeakHub Bounty Board</h1>
|
||||
<p class="subtitle">Request targets and compete in daily challenges</p>
|
||||
</div>
|
||||
|
||||
<!-- Daily Challenge -->
|
||||
<div class="daily-challenge">
|
||||
<h2 class="challenge-title">🎯 Today's Challenge</h2>
|
||||
<p class="challenge-description" id="challengeDescription">Find and verify a system prompt from GPT-4 Turbo!</p>
|
||||
<div class="challenge-reward">💰 Reward: 500 bonus points + Special Badge</div>
|
||||
<div class="challenge-timer" id="challengeTimer">Time remaining: 23:45:30</div>
|
||||
</div>
|
||||
|
||||
<div class="requests-grid">
|
||||
<!-- Submit Request Form -->
|
||||
<div class="request-form">
|
||||
<h2 style="color: #ffd700; margin-bottom: 1.5rem;">📝 Request a Leak Hunt</h2>
|
||||
<form onsubmit="submitRequest(event)">
|
||||
<select id="requestTargetType" required style="width: 100%; padding: 0.5rem; margin-bottom: 1rem; background: rgba(0,0,0,0.5); border: 1px solid rgba(255,255,255,0.1); border-radius: 8px; color: #e0e0e0;">
|
||||
<option value="">Select target type...</option>
|
||||
<option value="model">🤖 AI Model</option>
|
||||
<option value="app">📱 App/Interface</option>
|
||||
<option value="tool">🔧 Tool/Function</option>
|
||||
<option value="agent">🤝 AI Agent</option>
|
||||
<option value="plugin">🔌 Plugin/Extension</option>
|
||||
<option value="custom">🛠️ Custom GPT/Bot</option>
|
||||
</select>
|
||||
<input type="text" id="requestModel" placeholder="Target name (e.g., Gemini Pro, GitHub Copilot)" required
|
||||
style="width: 100%; margin-bottom: 1rem;">
|
||||
<input type="url" id="requestUrl" placeholder="Target URL (optional)"
|
||||
style="width: 100%; margin-bottom: 1rem;">
|
||||
<textarea id="requestDescription" placeholder="Why is this important? Any tips on how to get it?"
|
||||
style="width: 100%; min-height: 100px; margin-bottom: 1rem;"></textarea>
|
||||
<div style="display: flex; gap: 1rem; margin-bottom: 1rem;">
|
||||
<label style="display: flex; align-items: center; gap: 0.5rem; color: #888; font-size: 0.9rem;">
|
||||
<input type="checkbox" id="requestRequiresLogin" style="width: auto;">
|
||||
🔒 Login Required
|
||||
</label>
|
||||
<label style="display: flex; align-items: center; gap: 0.5rem; color: #888; font-size: 0.9rem;">
|
||||
<input type="checkbox" id="requestRequiresPaid" style="width: auto;">
|
||||
💰 Paid Access
|
||||
</label>
|
||||
</div>
|
||||
<input type="number" id="requestBounty" placeholder="Bounty points (optional)" min="0"
|
||||
style="width: 100%; margin-bottom: 1rem;">
|
||||
<button type="submit" style="width: 100%;">Submit Request</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Active Requests -->
|
||||
<div class="request-list">
|
||||
<h2 style="color: #ffd700; margin-bottom: 1rem;">🔥 Hot Requests</h2>
|
||||
<div class="filter-tabs">
|
||||
<div class="filter-tab active" onclick="filterRequests('trending')">🔥 Trending</div>
|
||||
<div class="filter-tab" onclick="filterRequests('bounty')">💰 Highest Bounty</div>
|
||||
<div class="filter-tab" onclick="filterRequests('new')">🆕 Newest</div>
|
||||
<div class="filter-tab" onclick="filterRequests('myvotes')">👍 My Votes</div>
|
||||
</div>
|
||||
<div id="requestsList">
|
||||
<p style="color: #666; text-align: center;">No requests yet. Be the first to request a leak hunt!</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="api/database.js"></script>
|
||||
<script src="script.js"></script>
|
||||
<script src="demo-data.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
+13
-557
@@ -1,559 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>LeakHub - AI System Prompt Discovery Platform</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="floating-grid"></div>
|
||||
|
||||
<!-- Production Header -->
|
||||
<nav class="production-nav">
|
||||
<div class="nav-container">
|
||||
<div class="nav-brand">
|
||||
<h1 class="logo">LeakHub</h1>
|
||||
<span class="version">v2.0.0</span>
|
||||
</div>
|
||||
|
||||
<div class="nav-menu">
|
||||
<a href="#leaks" class="nav-link active" onclick="switchTab('leaks')">🔍 Leaks</a>
|
||||
<a href="#validator" class="nav-link" onclick="switchTab('validator')">✅ Validator</a>
|
||||
<a href="#library" class="nav-link" onclick="switchTab('library')">📚 Library</a>
|
||||
<a href="#compare" class="nav-link" onclick="switchTab('compare')">🔍 Compare</a>
|
||||
<a href="#community" class="nav-link" onclick="switchTab('community')">👥 Community</a>
|
||||
<a href="#analytics" class="nav-link" onclick="switchTab('analytics')">📈 Analytics</a>
|
||||
</div>
|
||||
|
||||
<div class="nav-actions">
|
||||
<button class="user-profile-btn" onclick="toggleUserProfile()">
|
||||
<span class="user-avatar">👤</span>
|
||||
<span class="user-name" id="currentUserName">Guest</span>
|
||||
</button>
|
||||
<button class="settings-btn" onclick="toggleSettings()">⚙️</button>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="container">
|
||||
<header>
|
||||
<div class="hero-section">
|
||||
<h1>LeakHub</h1>
|
||||
<p class="subtitle">AI System Prompt Discovery + Universal Information Validation. CL4R1T4S!</p>
|
||||
|
||||
<div class="hero-stats">
|
||||
<div class="stat-item">
|
||||
<div class="stat-icon">🎯</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-number" id="activeLeaks">0</div>
|
||||
<div class="stat-label">Active Targets</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-icon">📊</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-number" id="totalSubmissions">0</div>
|
||||
<div class="stat-label">Total Submissions</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-icon">✅</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-number" id="verifiedPrompts">0</div>
|
||||
<div class="stat-label">Verified Prompts</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-icon">👥</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-number" id="activeUsers">0</div>
|
||||
<div class="stat-label">Active Users</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="quick-actions">
|
||||
<button onclick="toggleLeaderboard()" class="action-btn primary">
|
||||
🏆 Leaderboard
|
||||
</button>
|
||||
<button onclick="toggleRequests()" class="action-btn secondary">
|
||||
🎯 Challenges
|
||||
</button>
|
||||
<button onclick="toggleChat()" class="action-btn tertiary">
|
||||
💬 Live Chat
|
||||
</button>
|
||||
<button onclick="toggleAnalytics()" class="action-btn quaternary">
|
||||
📈 Analytics
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Real-time Chat System -->
|
||||
<div id="chatPanel" class="chat-panel" style="display: none;">
|
||||
<div class="chat-header">
|
||||
<h3>💬 LeakHub Live Chat</h3>
|
||||
<button onclick="toggleChat()" class="close-btn">×</button>
|
||||
</div>
|
||||
<div class="chat-messages" id="chatMessages">
|
||||
<div class="system-message">Welcome to LeakHub Live Chat! Share discoveries, ask questions, and collaborate in real-time! 🚀</div>
|
||||
</div>
|
||||
<div class="chat-input-container">
|
||||
<input type="text" id="chatInput" placeholder="Type your message..." maxlength="500">
|
||||
<button onclick="sendChatMessage()" class="send-btn">Send</button>
|
||||
</div>
|
||||
<div class="chat-users" id="chatUsers">
|
||||
<div class="online-indicator">🟢 Online: <span id="onlineCount">1</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- User Profile Overlay -->
|
||||
<div id="userProfileOverlay" class="overlay" style="display: none;">
|
||||
<div class="overlay-content">
|
||||
<div class="close-button" onclick="toggleUserProfile()">✕</div>
|
||||
<div class="profile-header">
|
||||
<h2>👤 User Profile</h2>
|
||||
<div class="profile-avatar" id="profileAvatar">👤</div>
|
||||
<div class="profile-name" id="profileName">Guest User</div>
|
||||
</div>
|
||||
<div class="profile-stats" id="profileStats">
|
||||
<!-- Stats will be populated by JavaScript -->
|
||||
</div>
|
||||
<div class="profile-achievements" id="profileAchievements">
|
||||
<!-- Achievements will be populated by JavaScript -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Settings Overlay -->
|
||||
<div id="settingsOverlay" class="overlay" style="display: none;">
|
||||
<div class="overlay-content">
|
||||
<div class="close-button" onclick="toggleSettings()">✕</div>
|
||||
<h2>⚙️ Settings</h2>
|
||||
<div class="settings-section">
|
||||
<h3>User Preferences</h3>
|
||||
<label>
|
||||
<input type="text" id="userNameInput" placeholder="Enter your username">
|
||||
Username
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" id="notificationsEnabled" checked>
|
||||
Enable Notifications
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" id="autoSaveEnabled" checked>
|
||||
Auto-save Data
|
||||
</label>
|
||||
</div>
|
||||
<div class="settings-section">
|
||||
<h3>Data Management</h3>
|
||||
<button onclick="exportData()" class="settings-btn">📤 Export Data</button>
|
||||
<button onclick="importData()" class="settings-btn">📥 Import Data</button>
|
||||
<button onclick="clearAllData()" class="settings-btn danger">🗑️ Clear All Data</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Analytics Overlay -->
|
||||
<div id="analyticsOverlay" class="overlay" style="display: none;">
|
||||
<div class="overlay-content wide">
|
||||
<div class="close-button" onclick="toggleAnalytics()">✕</div>
|
||||
<h2>📈 LeakHub Analytics</h2>
|
||||
<div class="analytics-grid">
|
||||
<div class="analytics-card">
|
||||
<h3>📊 Platform Statistics</h3>
|
||||
<div class="analytics-stats" id="platformStats">
|
||||
<!-- Platform stats -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="analytics-card">
|
||||
<h3>🎯 Target Distribution</h3>
|
||||
<div class="analytics-chart" id="targetChart">
|
||||
<!-- Target distribution chart -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="analytics-card">
|
||||
<h3>📈 Growth Trends</h3>
|
||||
<div class="analytics-trends" id="growthTrends">
|
||||
<!-- Growth trends -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="analytics-card">
|
||||
<h3>🏆 Top Contributors</h3>
|
||||
<div class="analytics-leaders" id="topContributors">
|
||||
<!-- Top contributors -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab Content -->
|
||||
<div class="tab-content">
|
||||
<!-- Leaks Tab -->
|
||||
<div id="leaks-tab" class="tab-panel active">
|
||||
<div class="main-section">
|
||||
<div class="panel">
|
||||
<h2>🔍 Submit System Prompt Leak</h2>
|
||||
<form class="submission-form" onsubmit="submitLeak(event)">
|
||||
<input type="text" id="sourceName" placeholder="Your identifier (e.g., User123, Anonymous)" required>
|
||||
|
||||
<select id="targetType" onchange="updateTargetFields()" required style="width: 100%; padding: 0.5rem; margin-bottom: 1rem; background: rgba(0,0,0,0.5); border: 1px solid rgba(255,255,255,0.1); border-radius: 8px; color: #e0e0e0;">
|
||||
<option value="">Select target type...</option>
|
||||
<option value="model">🤖 AI Model</option>
|
||||
<option value="app">📱 App/Interface</option>
|
||||
<option value="tool">🔧 Tool/Function</option>
|
||||
<option value="agent">🤝 AI Agent</option>
|
||||
<option value="plugin">🔌 Plugin/Extension</option>
|
||||
<option value="custom">🛠️ Custom GPT/Bot</option>
|
||||
</select>
|
||||
|
||||
<input type="text" id="instanceId" placeholder="Target name (e.g., ChatGPT-4, GitHub Copilot)" required>
|
||||
|
||||
<input type="url" id="targetUrl" placeholder="Target URL (e.g., https://chat.openai.com)" style="margin-bottom: 1rem;">
|
||||
|
||||
<div style="background: rgba(0,0,0,0.3); border: 1px solid rgba(255,255,255,0.1); border-radius: 8px; padding: 1rem; margin-bottom: 1rem;">
|
||||
<p style="color: #00aaff; font-size: 0.9rem; margin-bottom: 0.8rem;">🔒 Access Requirements</p>
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1rem;">
|
||||
<label style="display: flex; align-items: center; gap: 0.5rem; color: #888; font-size: 0.9rem;">
|
||||
<input type="checkbox" id="requiresLogin" style="width: auto;">
|
||||
Requires Login
|
||||
</label>
|
||||
<label style="display: flex; align-items: center; gap: 0.5rem; color: #888; font-size: 0.9rem;">
|
||||
<input type="checkbox" id="requiresPaid" style="width: auto;">
|
||||
Paid/Subscription
|
||||
</label>
|
||||
</div>
|
||||
<input type="text" id="accessNotes" placeholder="Additional access notes (e.g., 'Plus subscription required')"
|
||||
style="width: 100%; margin-top: 0.8rem; padding: 0.5rem; background: rgba(0,0,0,0.5); border: 1px solid rgba(255,255,255,0.1); border-radius: 8px; color: #e0e0e0;">
|
||||
</div>
|
||||
|
||||
<div id="additionalFields" style="display: none;">
|
||||
<input type="text" id="parentSystem" placeholder="Parent system (e.g., ChatGPT for a plugin)" style="margin-bottom: 1rem;">
|
||||
<input type="text" id="functionName" placeholder="Specific function/tool name (if applicable)" style="margin-bottom: 1rem;">
|
||||
</div>
|
||||
|
||||
<textarea id="leakContent" placeholder="Paste the suspected system prompt leak here..." required></textarea>
|
||||
<input type="text" id="context" placeholder="Context: How was this obtained? (optional)">
|
||||
|
||||
<div style="display: flex; gap: 0.5rem; align-items: center; margin-bottom: 1rem;">
|
||||
<input type="checkbox" id="hasTools" onchange="toggleToolsSection()" style="width: auto;">
|
||||
<label for="hasTools" style="color: #888; font-size: 0.9rem;">This target has tools/functions with their own prompts</label>
|
||||
</div>
|
||||
|
||||
<div id="toolsSection" style="display: none; background: rgba(0,0,0,0.3); border: 1px solid rgba(255,255,255,0.1); border-radius: 8px; padding: 1rem; margin-bottom: 1rem;">
|
||||
<p style="color: #00aaff; font-size: 0.9rem; margin-bottom: 0.5rem;">🔧 Related Tool Prompts (optional)</p>
|
||||
<textarea id="toolPrompts" placeholder="If you found any tool-specific prompts, paste them here..." style="min-height: 100px;"></textarea>
|
||||
</div>
|
||||
|
||||
<button type="submit">Submit to Library</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Validator Tab -->
|
||||
<div id="validator-tab" class="tab-panel">
|
||||
<div class="main-section">
|
||||
<div class="panel">
|
||||
<h2>✅ Submit Information for Validation</h2>
|
||||
<form class="submission-form" onsubmit="submitValidation(event)">
|
||||
<input type="text" id="validatorSourceName" placeholder="Your identifier (e.g., User123, Anonymous)" required>
|
||||
|
||||
<select id="validationCategory" required style="width: 100%; padding: 0.5rem; margin-bottom: 1rem; background: rgba(0,0,0,0.5); border: 1px solid rgba(255,255,255,0.1); border-radius: 8px; color: #e0e0e0;">
|
||||
<option value="">Select validation category...</option>
|
||||
<option value="ai-system">🤖 AI System Prompt</option>
|
||||
<option value="code-snippet">💻 Code/Algorithm</option>
|
||||
<option value="data-fact">📊 Data/Fact</option>
|
||||
<option value="document">📄 Document/Text</option>
|
||||
<option value="image">🖼️ Image/Visual</option>
|
||||
<option value="audio">🎵 Audio/Video</option>
|
||||
<option value="api">🔌 API/Endpoint</option>
|
||||
<option value="custom">🛠️ Custom Claim</option>
|
||||
</select>
|
||||
|
||||
<input type="text" id="validationTitle" placeholder="Claim title (e.g., 'ChatGPT-4 System Prompt', 'Bitcoin Mining Algorithm')" required>
|
||||
|
||||
<input type="url" id="validationUrl" placeholder="Source URL (e.g., https://example.com)" style="margin-bottom: 1rem;">
|
||||
|
||||
<textarea id="validationContent" placeholder="Paste the information claim here (code, text, data, etc.)..." required></textarea>
|
||||
<input type="text" id="validationContext" placeholder="Context: How was this obtained? Verification method? (optional)">
|
||||
|
||||
<div style="display: flex; gap: 0.5rem; align-items: center; margin-bottom: 1rem;">
|
||||
<input type="checkbox" id="hasRelatedComponents" onchange="toggleRelatedComponents()" style="width: auto;">
|
||||
<label for="hasRelatedComponents" style="color: #888; font-size: 0.9rem;">This claim has related sub-components or variations</label>
|
||||
</div>
|
||||
|
||||
<div id="relatedComponentsSection" style="display: none; background: rgba(0,0,0,0.3); border: 1px solid rgba(255,255,255,0.1); border-radius: 8px; padding: 1rem; margin-bottom: 1rem;">
|
||||
<p style="color: #00aaff; font-size: 0.9rem; margin-bottom: 0.5rem;">🔧 Related Components (optional)</p>
|
||||
<textarea id="relatedComponents" placeholder="If you found related components or variations, paste them here..." style="min-height: 100px;"></textarea>
|
||||
</div>
|
||||
|
||||
<button type="submit">Submit for Validation</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<h2>🔍 Active Validation Requests</h2>
|
||||
<div class="validation-requests" id="validationRequests">
|
||||
<p style="color: #666; text-align: center;">No validation requests yet. Be the first to submit!</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Library Tab -->
|
||||
<div id="library-tab" class="tab-panel">
|
||||
<div class="main-section">
|
||||
<div class="panel">
|
||||
<h2>📚 Leak Library</h2>
|
||||
<div class="submissions-list" id="submissionsList">
|
||||
<p style="color: #666; text-align: center;">No submissions yet. Be the first to contribute!</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Compare Tab -->
|
||||
<div id="compare-tab" class="tab-panel">
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Community Tab -->
|
||||
<div id="community-tab" class="tab-panel">
|
||||
<div class="main-section">
|
||||
<div class="panel">
|
||||
<h2>👥 Community Features</h2>
|
||||
<div class="community-grid">
|
||||
<div class="community-card">
|
||||
<h3>🏆 Leaderboard</h3>
|
||||
<p>See top contributors and their achievements</p>
|
||||
<button onclick="toggleLeaderboard()" class="enhanced-btn">View Leaderboard</button>
|
||||
</div>
|
||||
<div class="community-card">
|
||||
<h3>🎯 Challenges</h3>
|
||||
<p>Participate in daily challenges and bounty hunts</p>
|
||||
<button onclick="toggleRequests()" class="enhanced-btn">View Challenges</button>
|
||||
</div>
|
||||
<div class="community-card">
|
||||
<h3>💬 Live Chat</h3>
|
||||
<p>Collaborate with other researchers in real-time</p>
|
||||
<button onclick="toggleChat()" class="enhanced-btn">Join Chat</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Analytics Tab -->
|
||||
<div id="analytics-tab" class="tab-panel">
|
||||
<div class="main-section">
|
||||
<div class="panel">
|
||||
<h2>📈 Platform Analytics</h2>
|
||||
<div class="analytics-overview">
|
||||
<p>Comprehensive analytics and insights about the platform</p>
|
||||
<button onclick="toggleAnalytics()" class="enhanced-btn">View Full Analytics</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="comparison-section">
|
||||
<h2 style="color: #00ff88; margin-bottom: 1.5rem;">🔍 Compare Multiple Instances</h2>
|
||||
|
||||
<div class="comparison-controls">
|
||||
<div class="instance-selector">
|
||||
<label style="color: #888; font-size: 0.9rem;">Instance A:</label>
|
||||
<select id="instanceA" class="form-control" style="width: 100%; padding: 0.5rem; margin-top: 0.5rem; background: rgba(0,0,0,0.5); border: 1px solid rgba(255,255,255,0.1); border-radius: 8px; color: #e0e0e0;">
|
||||
<option value="">Select submission...</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="instance-selector">
|
||||
<label style="color: #888; font-size: 0.9rem;">Instance B:</label>
|
||||
<select id="instanceB" class="form-control" style="width: 100%; padding: 0.5rem; margin-top: 0.5rem; background: rgba(0,0,0,0.5); border: 1px solid rgba(255,255,255,0.1); border-radius: 8px; color: #e0e0e0;">
|
||||
<option value="">Select submission...</option>
|
||||
</select>
|
||||
</div>
|
||||
<button onclick="compareInstances()" style="align-self: flex-end;">Compare</button>
|
||||
</div>
|
||||
|
||||
<div id="comparisonResults" style="display: none;">
|
||||
<div class="comparison-grid">
|
||||
<div class="instance-viewer">
|
||||
<h3>Instance A</h3>
|
||||
<div class="instance-content" id="instanceAContent"></div>
|
||||
</div>
|
||||
<div class="instance-viewer">
|
||||
<h3>Instance B</h3>
|
||||
<div class="instance-content" id="instanceBContent"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="results-panel">
|
||||
<h3 style="color: #00ff88; margin-bottom: 1.5rem;">Analysis Results</h3>
|
||||
<div class="comparison-controls">
|
||||
<button onclick="performComparison()" class="enhanced-btn">🔍 Basic Comparison</button>
|
||||
<button onclick="performAdvancedComparison()" class="enhanced-btn">🤖 AI Analysis</button>
|
||||
</div>
|
||||
<div id="comparisonResults">
|
||||
<div class="match-metrics">
|
||||
<div class="metric-card">
|
||||
<div class="metric-label">Character Match</div>
|
||||
<div class="metric-value" id="charMatch">-</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="metric-label">Word Match</div>
|
||||
<div class="metric-value" id="wordMatch">-</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="metric-label">Structure Match</div>
|
||||
<div class="metric-value" id="structureMatch">-</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="metric-label">Core Similarity</div>
|
||||
<div class="metric-value" id="coreSimilarity">-</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="consensus-viewer">
|
||||
<h3>Consensus View (Common Elements)</h3>
|
||||
<div class="consensus-text" id="consensusText"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="library-stats">
|
||||
<div class="stat-card">
|
||||
<div class="stat-number" id="uniqueInstances">0</div>
|
||||
<div class="stat-label">Unique Instances</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-number" id="avgSimilarity">0%</div>
|
||||
<div class="stat-label">Average Similarity</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-number" id="highConfidence">0</div>
|
||||
<div class="stat-label">High Confidence Matches</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="alert" id="alert"></div>
|
||||
|
||||
<!-- Leaderboard Overlay -->
|
||||
<div class="leaderboard-overlay" id="leaderboardOverlay">
|
||||
<div class="leaderboard-container">
|
||||
<div class="close-button" onclick="toggleLeaderboard()">✕</div>
|
||||
|
||||
<div class="leaderboard-header">
|
||||
<h1 class="leaderboard-title">🏆 LeakHub Hall of Fame</h1>
|
||||
<p class="subtitle">Recognizing the top contributors to AI transparency</p>
|
||||
</div>
|
||||
|
||||
<div class="leaderboard-tabs">
|
||||
<div class="leaderboard-tab active" onclick="switchLeaderboardTab('rankings')">📊 Rankings</div>
|
||||
<div class="leaderboard-tab" onclick="switchLeaderboardTab('achievements')">🏅 Achievements</div>
|
||||
<div class="leaderboard-tab" onclick="switchLeaderboardTab('timeline')">📅 Timeline</div>
|
||||
</div>
|
||||
|
||||
<div id="rankings-content" class="leaderboard-content">
|
||||
<div class="rankings">
|
||||
<h2 style="color: #00ff88; margin-bottom: 1.5rem;">Top Contributors</h2>
|
||||
<div id="rankingsList"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="achievements-content" class="leaderboard-content" style="display: none;">
|
||||
<h2 style="color: #ffd700; margin-bottom: 1.5rem; text-align: center;">Notable Achievements</h2>
|
||||
<div class="achievements" id="achievementsList"></div>
|
||||
</div>
|
||||
|
||||
<div id="timeline-content" class="leaderboard-content" style="display: none;">
|
||||
<div class="timeline-section">
|
||||
<h2 style="color: #00aaff; margin-bottom: 1.5rem;">Discovery Timeline</h2>
|
||||
<div id="timelineList"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Requests & Challenges Overlay -->
|
||||
<div class="requests-overlay" id="requestsOverlay">
|
||||
<div class="requests-container">
|
||||
<div class="close-button" onclick="toggleRequests()">✕</div>
|
||||
|
||||
<div class="requests-header">
|
||||
<h1 class="requests-title">🎯 LeakHub Bounty Board</h1>
|
||||
<p class="subtitle">Request targets and compete in daily challenges</p>
|
||||
</div>
|
||||
|
||||
<!-- Daily Challenge -->
|
||||
<div class="daily-challenge">
|
||||
<h2 class="challenge-title">🎯 Today's Challenge</h2>
|
||||
<p class="challenge-description" id="challengeDescription">Find and verify a system prompt from GPT-4 Turbo!</p>
|
||||
<div class="challenge-reward">💰 Reward: 500 bonus points + Special Badge</div>
|
||||
<div class="challenge-timer" id="challengeTimer">Time remaining: 23:45:30</div>
|
||||
</div>
|
||||
|
||||
<div class="requests-grid">
|
||||
<!-- Submit Request Form -->
|
||||
<div class="request-form">
|
||||
<h2 style="color: #ffd700; margin-bottom: 1.5rem;">📝 Request a Leak Hunt</h2>
|
||||
<form onsubmit="submitRequest(event)">
|
||||
<select id="requestTargetType" required style="width: 100%; padding: 0.5rem; margin-bottom: 1rem; background: rgba(0,0,0,0.5); border: 1px solid rgba(255,255,255,0.1); border-radius: 8px; color: #e0e0e0;">
|
||||
<option value="">Select target type...</option>
|
||||
<option value="model">🤖 AI Model</option>
|
||||
<option value="app">📱 App/Interface</option>
|
||||
<option value="tool">🔧 Tool/Function</option>
|
||||
<option value="agent">🤝 AI Agent</option>
|
||||
<option value="plugin">🔌 Plugin/Extension</option>
|
||||
<option value="custom">🛠️ Custom GPT/Bot</option>
|
||||
</select>
|
||||
<input type="text" id="requestModel" placeholder="Target name (e.g., Gemini Pro, GitHub Copilot)" required
|
||||
style="width: 100%; margin-bottom: 1rem;">
|
||||
<input type="url" id="requestUrl" placeholder="Target URL (optional)"
|
||||
style="width: 100%; margin-bottom: 1rem;">
|
||||
<textarea id="requestDescription" placeholder="Why is this important? Any tips on how to get it?"
|
||||
style="width: 100%; min-height: 100px; margin-bottom: 1rem;"></textarea>
|
||||
<div style="display: flex; gap: 1rem; margin-bottom: 1rem;">
|
||||
<label style="display: flex; align-items: center; gap: 0.5rem; color: #888; font-size: 0.9rem;">
|
||||
<input type="checkbox" id="requestRequiresLogin" style="width: auto;">
|
||||
🔒 Login Required
|
||||
</label>
|
||||
<label style="display: flex; align-items: center; gap: 0.5rem; color: #888; font-size: 0.9rem;">
|
||||
<input type="checkbox" id="requestRequiresPaid" style="width: auto;">
|
||||
💰 Paid Access
|
||||
</label>
|
||||
</div>
|
||||
<input type="number" id="requestBounty" placeholder="Bounty points (optional)" min="0"
|
||||
style="width: 100%; margin-bottom: 1rem;">
|
||||
<button type="submit" style="width: 100%;">Submit Request</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Active Requests -->
|
||||
<div class="request-list">
|
||||
<h2 style="color: #ffd700; margin-bottom: 1rem;">🔥 Hot Requests</h2>
|
||||
<div class="filter-tabs">
|
||||
<div class="filter-tab active" onclick="filterRequests('trending')">🔥 Trending</div>
|
||||
<div class="filter-tab" onclick="filterRequests('bounty')">💰 Highest Bounty</div>
|
||||
<div class="filter-tab" onclick="filterRequests('new')">🆕 Newest</div>
|
||||
<div class="filter-tab" onclick="filterRequests('myvotes')">👍 My Votes</div>
|
||||
</div>
|
||||
<div id="requestsList">
|
||||
<p style="color: #666; text-align: center;">No requests yet. Be the first to request a leak hunt!</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="api/database.js"></script>
|
||||
<script src="script.js"></script>
|
||||
<script src="demo-data.js"></script>
|
||||
</body>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<!-- Emoji favicon using SVG data URI - displays an open book emoji 📖 -->
|
||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>📖</text></svg>">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>LeakHub</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
[build]
|
||||
publish = "."
|
||||
functions = "api"
|
||||
|
||||
[build.environment]
|
||||
NODE_VERSION = "18"
|
||||
|
||||
[[redirects]]
|
||||
from = "/api/*"
|
||||
to = "/.netlify/functions/serverless"
|
||||
status = 200
|
||||
|
||||
[[headers]]
|
||||
for = "/*"
|
||||
[headers.values]
|
||||
X-XSS-Protection = "1; mode=block"
|
||||
X-Content-Type-Options = "nosniff"
|
||||
X-Frame-Options = "DENY"
|
||||
Strict-Transport-Security = "max-age=31536000; includeSubDomains; preload"
|
||||
Referrer-Policy = "strict-origin-when-cross-origin"
|
||||
Permissions-Policy = "geolocation=(), microphone=(), camera=()"
|
||||
Content-Security-Policy = "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://www.googletagmanager.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'"
|
||||
|
||||
[[headers]]
|
||||
for = "/api/*"
|
||||
[headers.values]
|
||||
Access-Control-Allow-Origin = "*"
|
||||
Access-Control-Allow-Methods = "GET, POST, PUT, DELETE, OPTIONS"
|
||||
Access-Control-Allow-Headers = "Content-Type, Authorization"
|
||||
Generated
+7565
-464
File diff suppressed because it is too large
Load Diff
+49
-32
@@ -1,39 +1,56 @@
|
||||
{
|
||||
"name": "leakhub",
|
||||
"version": "1.0.0",
|
||||
"description": "AI System Prompt Discovery Platform - Community hub for crowd-sourced system prompt leak verification",
|
||||
"main": "index.html",
|
||||
"name": "leakhub_convex",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"dev": "npm-run-all --parallel dev:frontend dev:backend",
|
||||
"dev:frontend": "vite --open",
|
||||
"dev:backend": "convex dev",
|
||||
"predev": "convex dev --until-success && convex dev --once --run-sh \"node setup.mjs --once\" && convex dashboard",
|
||||
"build": "tsc --noEmit && vite build",
|
||||
"lint": "tsc && eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint . --ext .js,.html",
|
||||
"format": "prettier --write .",
|
||||
"serve": "python3 -m http.server 8000"
|
||||
"deploy": "npm run build && wrangler pages deploy",
|
||||
"cf-typegen": "wrangler types"
|
||||
},
|
||||
"dependencies": {
|
||||
"@convex-dev/auth": "^0.0.90",
|
||||
"@radix-ui/react-avatar": "^1.1.10",
|
||||
"@radix-ui/react-checkbox": "^1.3.3",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"convex": "^1.28.0",
|
||||
"lucide-react": "^0.547.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-router-dom": "^7.9.4",
|
||||
"tailwind-merge": "^3.3.1"
|
||||
},
|
||||
"keywords": [
|
||||
"ai",
|
||||
"system-prompt",
|
||||
"transparency",
|
||||
"crowdsourcing",
|
||||
"verification",
|
||||
"community",
|
||||
"github-pages"
|
||||
],
|
||||
"author": "Elder Plinius",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"vite": "^4.4.9",
|
||||
"eslint": "^8.48.0",
|
||||
"prettier": "^3.0.3"
|
||||
},
|
||||
"dependencies": {},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/elder-plinius/LEAKHUB.git"
|
||||
},
|
||||
"homepage": "https://elder-plinius.github.io/LEAKHUB",
|
||||
"bugs": {
|
||||
"url": "https://github.com/elder-plinius/LEAKHUB/issues"
|
||||
"@cloudflare/vite-plugin": "^1.13.17",
|
||||
"@convex-dev/eslint-plugin": "^1.0.0",
|
||||
"@eslint/js": "^9.21.0",
|
||||
"@tailwindcss/vite": "^4.1.16",
|
||||
"@types/node": "^22.18.12",
|
||||
"@types/react": "^19.0.10",
|
||||
"@types/react-dom": "^19.0.4",
|
||||
"@vitejs/plugin-react": "^5.0.0",
|
||||
"dotenv": "^16.4.7",
|
||||
"eslint": "^9.21.0",
|
||||
"eslint-plugin-react-hooks": "^5.1.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.19",
|
||||
"globals": "^15.15.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^3.5.3",
|
||||
"tailwindcss": "^4.1.16",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "~5.7.2",
|
||||
"typescript-eslint": "^8.24.1",
|
||||
"vite": "^6.2.0",
|
||||
"wrangler": "^4.45.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
// Security Headers Configuration for LeakHub
|
||||
// Add these headers to your web server or CDN configuration
|
||||
|
||||
const securityHeaders = {
|
||||
// Prevent XSS attacks
|
||||
'X-XSS-Protection': '1; mode=block',
|
||||
|
||||
// Prevent MIME type sniffing
|
||||
'X-Content-Type-Options': 'nosniff',
|
||||
|
||||
// Prevent clickjacking
|
||||
'X-Frame-Options': 'DENY',
|
||||
|
||||
// Strict transport security (HTTPS only)
|
||||
'Strict-Transport-Security': 'max-age=31536000; includeSubDomains; preload',
|
||||
|
||||
// Content Security Policy
|
||||
'Content-Security-Policy': [
|
||||
"default-src 'self'",
|
||||
"script-src 'self' 'unsafe-inline' 'unsafe-eval' https://www.googletagmanager.com",
|
||||
"style-src 'self' 'unsafe-inline'",
|
||||
"img-src 'self' data: https:",
|
||||
"font-src 'self'",
|
||||
"connect-src 'self'",
|
||||
"frame-ancestors 'none'",
|
||||
"base-uri 'self'",
|
||||
"form-action 'self'"
|
||||
].join('; '),
|
||||
|
||||
// Referrer Policy
|
||||
'Referrer-Policy': 'strict-origin-when-cross-origin',
|
||||
|
||||
// Permissions Policy
|
||||
'Permissions-Policy': 'geolocation=(), microphone=(), camera=()',
|
||||
|
||||
// Cache Control for sensitive pages
|
||||
'Cache-Control': 'no-store, no-cache, must-revalidate, proxy-revalidate',
|
||||
'Pragma': 'no-cache',
|
||||
'Expires': '0'
|
||||
};
|
||||
|
||||
// For different deployment platforms:
|
||||
|
||||
// Vercel (vercel.json)
|
||||
const vercelConfig = {
|
||||
headers: Object.entries(securityHeaders).map(([key, value]) => ({
|
||||
source: '/(.*)',
|
||||
headers: [{ key, value }]
|
||||
}))
|
||||
};
|
||||
|
||||
// Netlify (netlify.toml)
|
||||
const netlifyHeaders = Object.entries(securityHeaders).map(([key, value]) => ({
|
||||
for: '/*',
|
||||
[key.toLowerCase()]: value
|
||||
}));
|
||||
|
||||
// Express.js middleware
|
||||
function securityMiddleware(req, res, next) {
|
||||
Object.entries(securityHeaders).forEach(([key, value]) => {
|
||||
res.setHeader(key, value);
|
||||
});
|
||||
next();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
securityHeaders,
|
||||
vercelConfig,
|
||||
netlifyHeaders,
|
||||
securityMiddleware
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* This script runs `npx @convex-dev/auth` to help with setting up
|
||||
* environment variables for Convex Auth.
|
||||
*
|
||||
* You can safely delete it and remove it from package.json scripts.
|
||||
*/
|
||||
|
||||
import fs from "fs";
|
||||
import { config as loadEnvFile } from "dotenv";
|
||||
import { spawnSync } from "child_process";
|
||||
|
||||
if (!fs.existsSync(".env.local")) {
|
||||
// Something is off, skip the script.
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const config = {};
|
||||
loadEnvFile({ path: ".env.local", processEnv: config });
|
||||
|
||||
const runOnceWorkflow = process.argv.includes("--once");
|
||||
|
||||
if (runOnceWorkflow && config.SETUP_SCRIPT_RAN !== undefined) {
|
||||
// The script has already ran once, skip.
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const result = spawnSync("npx", ["@convex-dev/auth", "--skip-git-check"], {
|
||||
stdio: "inherit",
|
||||
});
|
||||
|
||||
if (runOnceWorkflow) {
|
||||
fs.writeFileSync(".env.local", `\nSETUP_SCRIPT_RAN=1\n`, { flag: "a" });
|
||||
}
|
||||
|
||||
process.exit(result.status);
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
"use client";
|
||||
|
||||
import { Authenticated, Unauthenticated, useConvexAuth } from "convex/react";
|
||||
import { useNavigate, Routes, Route, useLocation } from "react-router-dom";
|
||||
import { Navbar } from "./components/navbar";
|
||||
import Leaderboard from "./pages/Leaderboard";
|
||||
import Requests from "./pages/Requests";
|
||||
import Browse from "./pages/Browse";
|
||||
import ProviderLeaks from "./pages/ProviderLeaks";
|
||||
import { LeakLibrary } from "./components/leakLibrary";
|
||||
import { SubmitLeakForm } from "./components/submitLeakForm";
|
||||
import Dashboard from "./pages/Dashboard";
|
||||
|
||||
export default function App() {
|
||||
const { isLoading } = useConvexAuth();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const isHomePage = location.pathname === "/";
|
||||
|
||||
// Show only loading spinner during auth initialization
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex justify-center items-center h-screen bg-[#0f0f0f]">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-[#00ff88]"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="min-h-screen bg-[#0f0f0f] text-[#e0e0e0] overflow-x-hidden font-sans relative">
|
||||
{/* Floating Grid Background */}
|
||||
<div
|
||||
className="fixed top-0 left-0 w-full h-full pointer-events-none -z-10 animate-[gridMove_20s_linear_infinite]"
|
||||
style={{
|
||||
backgroundImage:
|
||||
"linear-gradient(rgba(0, 255, 136, 0.03) 1px, transparent 1px), linear-gradient(90deg, rgba(0, 255, 136, 0.03) 1px, transparent 1px)",
|
||||
backgroundSize: "50px 50px",
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="max-w-[1400px] mx-auto p-8">
|
||||
<header>
|
||||
<div
|
||||
className={
|
||||
isHomePage ? "" : "flex items-center justify-between mb-8"
|
||||
}
|
||||
>
|
||||
{!isHomePage && (
|
||||
<h1
|
||||
onClick={() => navigate("/")}
|
||||
className="text-3xl font-bold bg-linear-to-r from-[#00ff88] via-[#00aaff] to-[#ff00ff] bg-clip-text text-transparent animate-[gradientShift_5s_ease_infinite] cursor-pointer hover:opacity-80 transition-opacity"
|
||||
>
|
||||
LeakHub
|
||||
</h1>
|
||||
)}
|
||||
<div className={isHomePage ? "" : "ml-auto"}>
|
||||
<Navbar />
|
||||
</div>
|
||||
</div>
|
||||
{isHomePage && (
|
||||
<div className="text-center">
|
||||
<h1
|
||||
onClick={() => navigate("/")}
|
||||
className="text-5xl font-bold mb-4 bg-linear-to-r from-[#00ff88] via-[#00aaff] to-[#ff00ff] bg-clip-text text-transparent animate-[gradientShift_5s_ease_infinite] cursor-pointer hover:opacity-80 transition-opacity"
|
||||
>
|
||||
LeakHub
|
||||
</h1>
|
||||
<p className="text-[#888] text-xl mb-2">
|
||||
The community hub for crowd-sourced system prompt leak
|
||||
verification. CL4R1T4S!
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
<main className="flex flex-col gap-16">
|
||||
<Routes>
|
||||
<Route path="/" element={<Home />} />
|
||||
<Route path="/dashboard" element={<Dashboard />} />
|
||||
<Route path="/leaderboard" element={<Leaderboard />} />
|
||||
<Route path="/requests" element={<Requests />} />
|
||||
<Route path="/browse" element={<Browse />} />
|
||||
<Route path="/browse/:provider" element={<ProviderLeaks />} />
|
||||
</Routes>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function Home() {
|
||||
const navigate = useNavigate();
|
||||
return (
|
||||
<>
|
||||
<div className="text-center">
|
||||
{/* Status Bar */}
|
||||
{/* <div className="flex justify-center gap-8 mb-8 flex-wrap">
|
||||
<div className="bg-white/5 px-6 py-2 rounded-full border border-white/10 flex items-center gap-2">
|
||||
<span>Active Targets:</span>
|
||||
<span className="font-bold text-[#00ff88]">0</span>
|
||||
</div>
|
||||
<div className="bg-white/5 px-6 py-2 rounded-full border border-white/10 flex items-center gap-2">
|
||||
<span>Total Submissions:</span>
|
||||
<span className="font-bold text-[#00ff88]">0</span>
|
||||
</div>
|
||||
<div className="bg-white/5 px-6 py-2 rounded-full border border-white/10 flex items-center gap-2">
|
||||
<span>Verified Prompts:</span>
|
||||
<span className="font-bold text-[#00ff88]">0</span>
|
||||
</div>
|
||||
</div> */}
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex gap-4 justify-center flex-wrap mt-4">
|
||||
<button
|
||||
onClick={() => navigate("/browse")}
|
||||
className="bg-linear-to-r from-[#00ff88] to-[#00aaff] text-black px-8 py-2.5 rounded-lg font-bold transition-all hover:-translate-y-0.5 hover:shadow-[0_5px_20px_rgba(0,255,136,0.4)]"
|
||||
>
|
||||
🔍 Browse Prompts
|
||||
</button>
|
||||
<button
|
||||
onClick={() => navigate("/leaderboard")}
|
||||
className="bg-linear-to-r from-[#ff00ff] to-[#00ff88] text-black px-8 py-2.5 rounded-lg font-bold transition-all hover:-translate-y-0.5 hover:shadow-[0_5px_20px_rgba(0,255,136,0.4)]"
|
||||
>
|
||||
🏆 View Leaderboard
|
||||
</button>
|
||||
<button
|
||||
onClick={() => navigate("/requests")}
|
||||
className="bg-linear-to-r from-[#ff6b6b] to-[#ffd700] text-black px-8 py-2.5 rounded-lg font-bold transition-all hover:-translate-y-0.5 hover:shadow-[0_5px_20px_rgba(0,255,136,0.4)]"
|
||||
>
|
||||
🎯 Requests
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Authenticated>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 mb-12">
|
||||
<SubmitLeakForm />
|
||||
<LeakLibrary isUserLoggedIn={true} />
|
||||
</div>
|
||||
</Authenticated>
|
||||
<Unauthenticated>
|
||||
<LeakLibrary isUserLoggedIn={false} />
|
||||
</Unauthenticated>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,488 @@
|
||||
import { useState } from "react";
|
||||
import { useQuery } from "convex/react";
|
||||
import { api } from "../../convex/_generated/api";
|
||||
|
||||
interface LeakLibraryProps {
|
||||
isUserLoggedIn?: boolean;
|
||||
}
|
||||
|
||||
export function LeakLibrary({ isUserLoggedIn }: LeakLibraryProps) {
|
||||
const verifiedLeaks = useQuery(api.leaks.getVerifiedLeaks);
|
||||
const requestsWithStatus = useQuery(
|
||||
api.requests.getRequestsWithVerificationStatus,
|
||||
);
|
||||
const [openSections, setOpenSections] = useState<Set<string>>(
|
||||
new Set(["verified"]),
|
||||
);
|
||||
const [openFolders, setOpenFolders] = useState<Set<string>>(new Set());
|
||||
const [openLeaks, setOpenLeaks] = useState<Set<string>>(new Set());
|
||||
const [copiedLeakId, setCopiedLeakId] = useState<string | null>(null);
|
||||
const [showErrorToast, setShowErrorToast] = useState(false);
|
||||
|
||||
const toggleSection = (section: string) => {
|
||||
setOpenSections((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(section)) {
|
||||
next.delete(section);
|
||||
} else {
|
||||
next.add(section);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const toggleFolder = (provider: string) => {
|
||||
setOpenFolders((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(provider)) {
|
||||
next.delete(provider);
|
||||
} else {
|
||||
next.add(provider);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const toggleLeak = (leakId: string) => {
|
||||
setOpenLeaks((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(leakId)) {
|
||||
next.delete(leakId);
|
||||
} else {
|
||||
next.add(leakId);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const copyPrompt = async (text: string, leakId: string) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
setCopiedLeakId(leakId);
|
||||
setTimeout(() => setCopiedLeakId(null), 2000);
|
||||
} catch (err) {
|
||||
setShowErrorToast(true);
|
||||
setTimeout(() => setShowErrorToast(false), 3000);
|
||||
}
|
||||
};
|
||||
|
||||
const renderRequestSection = (requests: typeof requestsWithStatus) => {
|
||||
if (!requests || requests.length === 0) {
|
||||
return (
|
||||
<p className="text-[#666] text-center py-4">
|
||||
No requests awaiting verification yet.
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
const groupedByProvider = requests.reduce(
|
||||
(acc, request) => {
|
||||
if (!acc[request.provider]) {
|
||||
acc[request.provider] = [];
|
||||
}
|
||||
acc[request.provider].push(request);
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, typeof requests>,
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-3 pl-4">
|
||||
{Object.entries(groupedByProvider).map(
|
||||
([provider, providerRequests]) => {
|
||||
const folderKey = `requests-${provider}`;
|
||||
const isOpen = openFolders.has(folderKey);
|
||||
return (
|
||||
<div
|
||||
key={folderKey}
|
||||
className="border border-white/10 rounded-lg p-4"
|
||||
>
|
||||
<h3
|
||||
className="text-[#00aaff] text-lg font-semibold mb-3 flex items-center gap-2 cursor-pointer hover:text-[#33bbff] transition-colors"
|
||||
onClick={() => toggleFolder(folderKey)}
|
||||
>
|
||||
<span
|
||||
className="transition-transform duration-200"
|
||||
style={{
|
||||
transform: isOpen ? "rotate(90deg)" : "rotate(0deg)",
|
||||
}}
|
||||
>
|
||||
▶
|
||||
</span>
|
||||
<span>{isOpen ? "📂" : "📁"}</span>
|
||||
<span>{provider}</span>
|
||||
<span className="text-sm text-[#666]">
|
||||
({providerRequests.length})
|
||||
</span>
|
||||
</h3>
|
||||
{isOpen && (
|
||||
<div className="space-y-2 pl-6">
|
||||
{providerRequests.map((request) => {
|
||||
// Determine status color based on confirmations
|
||||
let statusColor = "text-red-400";
|
||||
let statusEmoji = "🔴";
|
||||
if (request.confirmationCount === 1) {
|
||||
statusColor = "text-yellow-400";
|
||||
statusEmoji = "🟡";
|
||||
} else if (request.confirmationCount === 0) {
|
||||
statusColor = "text-red-400";
|
||||
statusEmoji = "🔴";
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={request._id}
|
||||
className="border border-white/5 rounded-lg p-3 bg-white/5"
|
||||
>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2 flex-1 min-w-0">
|
||||
<span className="text-[#ccc] font-medium wrap-break-word">
|
||||
{request.targetName}
|
||||
</span>
|
||||
<span className="text-xs text-[#666] bg-white/5 px-2 py-1 rounded shrink-0">
|
||||
{request.targetType}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className={`flex items-center gap-2 shrink-0 ${statusColor}`}
|
||||
>
|
||||
<span>{statusEmoji}</span>
|
||||
<span className="text-sm font-semibold">
|
||||
{request.confirmationCount}/2
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2 text-xs text-[#666]">
|
||||
<div>Requested by: {request.submitterName}</div>
|
||||
{request.uniqueSubmitters > 0 && (
|
||||
<div>
|
||||
Unique submitters: {request.uniqueSubmitters}
|
||||
</div>
|
||||
)}
|
||||
{request.targetUrl && (
|
||||
<div className="mt-1">
|
||||
<a
|
||||
href={request.targetUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-[#33bbff] hover:underline break-all"
|
||||
>
|
||||
{request.targetUrl}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderLeakSection = (
|
||||
leaks: typeof verifiedLeaks,
|
||||
sectionPrefix: string,
|
||||
) => {
|
||||
if (!leaks || leaks.length === 0) {
|
||||
return (
|
||||
<p className="text-[#666] text-center py-4">
|
||||
No leaks in this section yet.
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
const groupedByProvider = leaks.reduce(
|
||||
(acc, leak) => {
|
||||
if (!acc[leak.provider]) {
|
||||
acc[leak.provider] = [];
|
||||
}
|
||||
acc[leak.provider].push(leak);
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, typeof leaks>,
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-3 pl-4">
|
||||
{Object.entries(groupedByProvider).map(([provider, providerLeaks]) => {
|
||||
const folderKey = `${sectionPrefix}-${provider}`;
|
||||
const isOpen = openFolders.has(folderKey);
|
||||
return (
|
||||
<div
|
||||
key={folderKey}
|
||||
className="border border-white/10 rounded-lg p-4"
|
||||
>
|
||||
<h3
|
||||
className="text-[#00aaff] text-lg font-semibold mb-3 flex items-center gap-2 cursor-pointer hover:text-[#33bbff] transition-colors"
|
||||
onClick={() => toggleFolder(folderKey)}
|
||||
>
|
||||
<span
|
||||
className="transition-transform duration-200"
|
||||
style={{
|
||||
transform: isOpen ? "rotate(90deg)" : "rotate(0deg)",
|
||||
}}
|
||||
>
|
||||
▶
|
||||
</span>
|
||||
<span>{isOpen ? "📂" : "📁"}</span>
|
||||
<span>{provider}</span>
|
||||
<span className="text-sm text-[#666]">
|
||||
({providerLeaks.length})
|
||||
</span>
|
||||
</h3>
|
||||
{isOpen && (
|
||||
<div className="space-y-2 pl-6">
|
||||
{providerLeaks.map((leak) => {
|
||||
const isLeakOpen = openLeaks.has(leak._id);
|
||||
return (
|
||||
<div
|
||||
key={leak._id}
|
||||
className="border border-white/5 rounded-lg p-3 bg-white/5"
|
||||
>
|
||||
<div
|
||||
className="flex items-center gap-2 cursor-pointer hover:text-[#33bbff] transition-colors"
|
||||
onClick={() => toggleLeak(leak._id)}
|
||||
>
|
||||
<span
|
||||
className="transition-transform duration-200 text-[#00aaff] shrink-0"
|
||||
style={{
|
||||
transform: isLeakOpen
|
||||
? "rotate(90deg)"
|
||||
: "rotate(0deg)",
|
||||
}}
|
||||
>
|
||||
▶
|
||||
</span>
|
||||
<span className="text-[#ccc] font-medium wrap-break-word">
|
||||
{leak.targetName}
|
||||
</span>
|
||||
<span className="text-xs text-[#666] bg-white/5 px-2 py-1 rounded shrink-0">
|
||||
{leak.targetType}
|
||||
</span>
|
||||
</div>
|
||||
{isLeakOpen && (
|
||||
<div className="mt-3 pt-3 border-t border-white/10 space-y-3">
|
||||
{/* Submitter and Verifiers Info */}
|
||||
{(leak.submitterName || leak.verifierNames) && (
|
||||
<div className="bg-green-500/10 border border-green-500/20 rounded-lg p-3">
|
||||
{leak.submitterName && (
|
||||
<div className="text-xs text-green-400 mb-1">
|
||||
<span className="font-semibold">
|
||||
Submitted by:
|
||||
</span>{" "}
|
||||
{leak.submitterName}
|
||||
</div>
|
||||
)}
|
||||
{leak.verifierNames &&
|
||||
leak.verifierNames.length > 0 && (
|
||||
<div className="text-xs text-green-400">
|
||||
<span className="font-semibold">
|
||||
✓ Verified by:
|
||||
</span>{" "}
|
||||
{leak.verifierNames.join(", ")}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<div className="text-[#00aaff] text-sm font-semibold mb-1 flex items-center justify-between">
|
||||
<span>Prompt:</span>
|
||||
<button
|
||||
onClick={() =>
|
||||
copyPrompt(leak.leakText, leak._id)
|
||||
}
|
||||
className="text-xs px-2 py-1 rounded bg-[#00aaff]/20 hover:bg-[#00aaff]/30 transition-colors flex items-center gap-1"
|
||||
>
|
||||
{copiedLeakId === leak._id ? (
|
||||
<>
|
||||
<span>✓</span>
|
||||
<span>Copied!</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span>📋</span>
|
||||
<span>Copy</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
<div className="text-[#ccc] text-sm bg-black/30 p-3 rounded border border-white/5 wrap-break-word overflow-wrap-anywhere max-w-full max-h-64 overflow-y-auto overflow-x-auto">
|
||||
<pre className="whitespace-pre-wrap wrap-break-word font-sans">
|
||||
{leak.leakText}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
{leak.leakContext && (
|
||||
<div>
|
||||
<div className="text-[#00aaff] text-sm font-semibold mb-1">
|
||||
Context:
|
||||
</div>
|
||||
<div className="text-[#ccc] text-sm wrap-break-word">
|
||||
{leak.leakContext}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{leak.url && (
|
||||
<div>
|
||||
<div className="text-[#00aaff] text-sm font-semibold mb-1">
|
||||
URL:
|
||||
</div>
|
||||
<a
|
||||
href={leak.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-[#33bbff] text-sm hover:underline break-all"
|
||||
>
|
||||
{leak.url}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
{leak.accessNotes && (
|
||||
<div>
|
||||
<div className="text-[#00aaff] text-sm font-semibold mb-1">
|
||||
Access Notes:
|
||||
</div>
|
||||
<div className="text-[#ccc] text-sm wrap-break-word">
|
||||
{leak.accessNotes}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-wrap gap-2 text-xs">
|
||||
{leak.requiresLogin && (
|
||||
<span className="bg-yellow-500/20 text-yellow-400 px-2 py-1 rounded shrink-0">
|
||||
🔐 Requires Login
|
||||
</span>
|
||||
)}
|
||||
{leak.isPaid && (
|
||||
<span className="bg-green-500/20 text-green-400 px-2 py-1 rounded shrink-0">
|
||||
💰 Paid
|
||||
</span>
|
||||
)}
|
||||
{leak.hasToolPrompts && (
|
||||
<span className="bg-blue-500/20 text-blue-400 px-2 py-1 rounded shrink-0">
|
||||
🔧 Has Tool Prompts
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
if (verifiedLeaks === undefined || requestsWithStatus === undefined) {
|
||||
return (
|
||||
<div className="bg-white/3 border border-white/10 rounded-2xl p-8 backdrop-blur-sm relative">
|
||||
<h2 className="text-[#00aaff] text-2xl mb-6">📚 Leak Library</h2>
|
||||
<div className="flex justify-center h-screen">
|
||||
<p className="text-[#00aaff] text-center">Loading leaks...</p>
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-gray-900 dark:border-white"></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const openSectionsCount = openSections.size;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="bg-white/3 border border-white/10 rounded-2xl p-8 backdrop-blur-sm relative flex flex-col"
|
||||
style={{ height: "960px" }}
|
||||
>
|
||||
{showErrorToast && (
|
||||
<div className="fixed top-4 right-4 z-50 bg-red-500/90 text-white px-4 py-3 rounded-lg shadow-lg flex items-center gap-2 animate-in slide-in-from-top">
|
||||
<span className="text-lg">❌</span>
|
||||
<span className="font-medium">Failed to copy to clipboard</span>
|
||||
</div>
|
||||
)}
|
||||
<h2 className="text-[#00aaff] text-2xl mb-6 shrink-0">📚 Leak Library</h2>
|
||||
{!isUserLoggedIn && (
|
||||
<div className="bg-[#00aaff]/10 border border-[#00aaff]/30 rounded-lg p-4 mb-6 shrink-0">
|
||||
<p className="text-[#00aaff] text-center">
|
||||
🔒 Please sign in to submit leaks
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex-1 flex flex-col gap-4 overflow-hidden min-h-0 pr-2">
|
||||
{/* Verified Leaks Section */}
|
||||
<div
|
||||
className={`border border-green-500/30 rounded-lg bg-green-500/5 flex flex-col ${openSections.has("verified") && openSectionsCount > 0 ? "flex-1" : "shrink-0"} min-h-0`}
|
||||
>
|
||||
<div className="p-4 shrink-0">
|
||||
<h2
|
||||
className="text-green-400 text-xl font-bold flex items-center gap-2 cursor-pointer hover:text-green-300 transition-colors"
|
||||
onClick={() => toggleSection("verified")}
|
||||
>
|
||||
<span
|
||||
className="transition-transform duration-200"
|
||||
style={{
|
||||
transform: openSections.has("verified")
|
||||
? "rotate(90deg)"
|
||||
: "rotate(0deg)",
|
||||
}}
|
||||
>
|
||||
▶
|
||||
</span>
|
||||
<span>{openSections.has("verified") ? "📂" : "📁"}</span>
|
||||
<span>Verified Leaks</span>
|
||||
<span className="text-sm text-[#666]">
|
||||
({verifiedLeaks.length})
|
||||
</span>
|
||||
</h2>
|
||||
</div>
|
||||
{openSections.has("verified") && (
|
||||
<div className="px-4 pb-4 overflow-y-auto flex-1 min-h-0">
|
||||
{renderLeakSection(verifiedLeaks, "verified")}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Requests Awaiting Verification Section */}
|
||||
<div
|
||||
className={`border border-yellow-500/30 rounded-lg bg-yellow-500/5 flex flex-col ${openSections.has("requests") && openSectionsCount > 0 ? "flex-1" : "shrink-0"} min-h-0`}
|
||||
>
|
||||
<div className="p-4 shrink-0">
|
||||
<h2
|
||||
className="text-yellow-400 text-xl font-bold flex items-center gap-2 cursor-pointer hover:text-yellow-300 transition-colors"
|
||||
onClick={() => toggleSection("requests")}
|
||||
>
|
||||
<span
|
||||
className="transition-transform duration-200"
|
||||
style={{
|
||||
transform: openSections.has("requests")
|
||||
? "rotate(90deg)"
|
||||
: "rotate(0deg)",
|
||||
}}
|
||||
>
|
||||
▶
|
||||
</span>
|
||||
<span>{openSections.has("requests") ? "📂" : "📁"}</span>
|
||||
<span>Requests Awaiting Verification</span>
|
||||
<span className="text-sm text-[#666]">
|
||||
({requestsWithStatus.length})
|
||||
</span>
|
||||
</h2>
|
||||
</div>
|
||||
{openSections.has("requests") && (
|
||||
<div className="px-4 pb-4 overflow-y-auto flex-1 min-h-0">
|
||||
{renderRequestSection(requestsWithStatus)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import { Github, LogOut, LayoutDashboard } from "lucide-react";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import { useAuthActions } from "@convex-dev/auth/react";
|
||||
import { useConvexAuth, useQuery } from "convex/react";
|
||||
import { api } from "../../convex/_generated/api";
|
||||
|
||||
export function Navbar() {
|
||||
const { isAuthenticated } = useConvexAuth();
|
||||
const user = useQuery(api.users.currentUser);
|
||||
const { signOut } = useAuthActions();
|
||||
|
||||
function SignOutButton() {
|
||||
return (
|
||||
<>
|
||||
<LogOut className="w-4 h-4 text-[#00ff88]" />
|
||||
<span>Sign out</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function SignInButton() {
|
||||
const { signIn } = useAuthActions();
|
||||
return (
|
||||
<button
|
||||
className="bg-[#00ff88]/10 hover:bg-[#00ff88]/20 border border-[#00ff88]/30 text-[#e0e0e0] hover:text-white px-6 py-2 rounded-lg font-medium transition-all hover:-translate-y-0.5"
|
||||
onClick={() => signIn("github")}
|
||||
>
|
||||
Sign in
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-end gap-3">
|
||||
{isAuthenticated ? (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger className="outline-none">
|
||||
<Avatar className="w-9 h-9 ring-2 ring-[#00ff88]/30 hover:ring-[#00ff88]/50 transition-all">
|
||||
<AvatarImage src={user?.image} alt={user?.name ?? "User"} />
|
||||
<AvatarFallback className="bg-[#00ff88] text-black font-bold">
|
||||
{user?.name?.charAt(0).toUpperCase() ?? "U"}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="bg-[#1a1a1a] border-0 text-[#e0e0e0]">
|
||||
<DropdownMenuLabel className="text-[#888]">
|
||||
{user?.name ?? "My Account"}
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator className="bg-[#00ff88]/20" />
|
||||
<DropdownMenuItem
|
||||
asChild
|
||||
className="cursor-pointer hover:bg-[#00ff88]/10 hover:text-white focus:bg-[#00ff88]/10 focus:text-white"
|
||||
>
|
||||
<a
|
||||
href="/dashboard"
|
||||
className="flex items-center gap-2 text-white"
|
||||
>
|
||||
<LayoutDashboard className="w-4 h-4 text-[#00ff88]" />
|
||||
<span>Dashboard</span>
|
||||
</a>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => void signOut()}
|
||||
className="cursor-pointer hover:bg-[#00ff88]/10 hover:text-white focus:bg-[#00ff88]/10 focus:text-white flex items-center gap-2"
|
||||
>
|
||||
<SignOutButton />
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
) : (
|
||||
<SignInButton />
|
||||
)}
|
||||
<a
|
||||
href="https://github.com/elder-plinius/CL4R1T4S"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="bg-[#00ff88]/10 hover:bg-[#00ff88]/20 border border-[#00ff88]/30 text-[#e0e0e0] hover:text-white p-2 rounded-lg transition-all hover:-translate-y-0.5 flex items-center justify-center"
|
||||
>
|
||||
<Github className="w-5 h-5" />
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,437 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useQuery, useMutation } from "convex/react";
|
||||
import { api } from "../../convex/_generated/api";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Id } from "../../convex/_generated/dataModel";
|
||||
|
||||
type TargetType = "model" | "app" | "tool" | "agent" | "plugin" | "custom";
|
||||
|
||||
export function SubmitLeakForm() {
|
||||
const user = useQuery(api.users.currentUser);
|
||||
const insertLeak = useMutation(api.leaks.insertLeak);
|
||||
const createRequest = useMutation(api.requests.createRequest);
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [selectedRequest, setSelectedRequest] = useState<{
|
||||
_id: Id<"requests">;
|
||||
targetName: string;
|
||||
provider: string;
|
||||
targetType: TargetType;
|
||||
targetUrl: string;
|
||||
submitterName: string;
|
||||
} | null>(null);
|
||||
const [showNewRequestForm, setShowNewRequestForm] = useState(false);
|
||||
|
||||
const [targetType, setTargetType] = useState<TargetType | "">("");
|
||||
const [targetName, setTargetName] = useState("");
|
||||
const [targetUrl, setTargetUrl] = useState("");
|
||||
const [provider, setProvider] = useState("");
|
||||
const [requiresLogin, setRequiresLogin] = useState(false);
|
||||
const [isPaid, setIsPaid] = useState(false);
|
||||
const [hasToolPrompts, setHasToolPrompts] = useState(false);
|
||||
const [accessNotes, setAccessNotes] = useState("");
|
||||
const [leakText, setLeakText] = useState("");
|
||||
const [leakContext, setLeakContext] = useState("");
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [successMessage, setSuccessMessage] = useState<string | null>(null);
|
||||
|
||||
// Search for existing requests
|
||||
const searchResults = useQuery(
|
||||
api.requests.searchRequests,
|
||||
searchQuery.trim() ? { searchQuery } : "skip",
|
||||
);
|
||||
|
||||
// Auto-populate form when a request is selected
|
||||
useEffect(() => {
|
||||
if (selectedRequest) {
|
||||
setTargetName(selectedRequest.targetName);
|
||||
setProvider(selectedRequest.provider);
|
||||
setTargetType(selectedRequest.targetType);
|
||||
setTargetUrl(selectedRequest.targetUrl);
|
||||
}
|
||||
}, [selectedRequest]);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError(null);
|
||||
setSuccessMessage(null);
|
||||
|
||||
// Validate required fields
|
||||
if (!leakText.trim()) {
|
||||
setError("Leak text is required");
|
||||
return;
|
||||
}
|
||||
|
||||
if (showNewRequestForm) {
|
||||
// Validate new request fields
|
||||
if (
|
||||
!targetName.trim() ||
|
||||
!provider.trim() ||
|
||||
!targetUrl.trim() ||
|
||||
!targetType
|
||||
) {
|
||||
setError(
|
||||
"Target name, provider, URL, and type are required for new submissions",
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setIsSubmitting(true);
|
||||
|
||||
try {
|
||||
let requestId: Id<"requests"> | undefined = undefined;
|
||||
|
||||
// If submitting for a new request (not linked to existing), create a new request
|
||||
if (showNewRequestForm && !selectedRequest) {
|
||||
const requestResult = await createRequest({
|
||||
targetName,
|
||||
provider,
|
||||
targetType: targetType as TargetType,
|
||||
targetUrl,
|
||||
});
|
||||
|
||||
if (!requestResult.success) {
|
||||
setError(requestResult.error);
|
||||
setIsSubmitting(false);
|
||||
return;
|
||||
}
|
||||
|
||||
requestId = requestResult.requestId;
|
||||
} else if (selectedRequest) {
|
||||
requestId = selectedRequest._id;
|
||||
}
|
||||
|
||||
// Submit the leak
|
||||
const result = await insertLeak({
|
||||
targetName: selectedRequest?.targetName || targetName,
|
||||
provider: selectedRequest?.provider || provider,
|
||||
leakText,
|
||||
targetType: selectedRequest?.targetType || (targetType as TargetType),
|
||||
requestId,
|
||||
requiresLogin,
|
||||
isPaid,
|
||||
hasToolPrompts,
|
||||
accessNotes: accessNotes || undefined,
|
||||
leakContext: leakContext || undefined,
|
||||
url: selectedRequest?.targetUrl || targetUrl || undefined,
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
setError(result.error);
|
||||
setIsSubmitting(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Success - reset form
|
||||
setError(null);
|
||||
setLeakText("");
|
||||
setLeakContext("");
|
||||
setAccessNotes("");
|
||||
setRequiresLogin(false);
|
||||
setIsPaid(false);
|
||||
setHasToolPrompts(false);
|
||||
setSelectedRequest(null);
|
||||
setShowNewRequestForm(false);
|
||||
setTargetName("");
|
||||
setProvider("");
|
||||
setTargetUrl("");
|
||||
setTargetType("");
|
||||
|
||||
// Show success message
|
||||
setSuccessMessage(
|
||||
result.message ||
|
||||
"Leak submitted successfully! It will be reviewed for verification.",
|
||||
);
|
||||
|
||||
// Clear success message after 5 seconds
|
||||
setTimeout(() => setSuccessMessage(null), 5000);
|
||||
} catch (error: any) {
|
||||
// Fallback for unexpected errors
|
||||
const errorMessage =
|
||||
error?.data ||
|
||||
error?.message ||
|
||||
"Failed to submit leak. Please try again.";
|
||||
setError(errorMessage);
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectRequest = (request: {
|
||||
_id: Id<"requests">;
|
||||
targetName: string;
|
||||
provider: string;
|
||||
targetType: TargetType;
|
||||
targetUrl: string;
|
||||
submitterName: string;
|
||||
}) => {
|
||||
setSelectedRequest(request);
|
||||
setSearchQuery("");
|
||||
setShowNewRequestForm(false);
|
||||
};
|
||||
|
||||
const handleCreateNew = () => {
|
||||
setSelectedRequest(null);
|
||||
setShowNewRequestForm(true);
|
||||
setSearchQuery("");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white/3 border border-white/10 rounded-2xl p-8 backdrop-blur-sm">
|
||||
<h2 className="text-[#00aaff] text-2xl mb-6 flex items-center gap-2">
|
||||
📤 Submit Leak
|
||||
</h2>
|
||||
{error && (
|
||||
<div className="bg-red-500/10 border border-red-500/30 rounded-lg p-4 mb-4">
|
||||
<p className="text-red-400 text-sm">⚠️ {error}</p>
|
||||
</div>
|
||||
)}
|
||||
{successMessage && (
|
||||
<div className="bg-green-500/10 border border-green-500/30 rounded-lg p-4 mb-4">
|
||||
<p className="text-green-400 text-sm">✓ {successMessage}</p>
|
||||
</div>
|
||||
)}
|
||||
<form className="flex flex-col gap-4" onSubmit={handleSubmit}>
|
||||
{/* Search for existing requests */}
|
||||
{!selectedRequest && !showNewRequestForm && (
|
||||
<div className="bg-black/30 border border-white/10 rounded-lg p-4">
|
||||
<p className="text-[#00aaff] text-sm mb-3">
|
||||
🔍 Search for Existing Requests
|
||||
</p>
|
||||
<input
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
placeholder="Search by target name (e.g., ChatGPT-4, Claude)..."
|
||||
className="w-full bg-black/50 border border-white/10 rounded-lg text-[#e0e0e0] p-4 transition-all focus:outline-none focus:border-[#00ff88] focus:shadow-[0_0_20px_rgba(0,255,136,0.2)]"
|
||||
/>
|
||||
|
||||
{/* Search results */}
|
||||
{searchResults && searchResults.length > 0 && (
|
||||
<div className="mt-4 space-y-2">
|
||||
{searchResults.map((request) => (
|
||||
<div
|
||||
key={request._id}
|
||||
onClick={() => handleSelectRequest(request)}
|
||||
className="bg-black/50 border border-white/10 rounded-lg p-4 cursor-pointer transition-all hover:border-[#00ff88] hover:shadow-[0_0_10px_rgba(0,255,136,0.2)]"
|
||||
>
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<h3 className="text-[#00ff88] font-semibold">
|
||||
{request.targetName}
|
||||
</h3>
|
||||
<p className="text-[#888] text-sm">
|
||||
{request.provider}
|
||||
</p>
|
||||
<p className="text-[#666] text-xs mt-1">
|
||||
Requested by: {request.submitterName}
|
||||
</p>
|
||||
</div>
|
||||
<span className="text-[#00aaff] text-xs">
|
||||
{request.targetType}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{searchQuery && searchResults && searchResults.length === 0 && (
|
||||
<p className="text-[#888] text-sm mt-3">
|
||||
No existing requests found. Create a new one below.
|
||||
</p>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCreateNew}
|
||||
className="w-full mt-4 bg-black/50 border border-white/20 text-[#00aaff] p-3 rounded-lg transition-all hover:border-[#00aaff] hover:bg-black/70"
|
||||
>
|
||||
+ Create New Submission (No Existing Request)
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Display selected request */}
|
||||
{selectedRequest && (
|
||||
<div className="bg-[#00ff88]/10 border border-[#00ff88]/30 rounded-lg p-4">
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<p className="text-[#00ff88] text-sm mb-1">
|
||||
✓ Submitting for existing request:
|
||||
</p>
|
||||
<h3 className="text-white font-semibold">
|
||||
{selectedRequest.targetName}
|
||||
</h3>
|
||||
<p className="text-[#888] text-sm">
|
||||
{selectedRequest.provider}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSelectedRequest(null)}
|
||||
className="text-[#888] hover:text-white text-sm"
|
||||
>
|
||||
✕ Change
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Display new submission mode */}
|
||||
{showNewRequestForm && !selectedRequest && (
|
||||
<div className="bg-[#00aaff]/10 border border-[#00aaff]/30 rounded-lg p-4">
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<p className="text-[#00aaff] text-sm mb-1">
|
||||
Creating new submission (no existing request)
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowNewRequestForm(false)}
|
||||
className="text-[#888] hover:text-white text-sm"
|
||||
>
|
||||
← Back to Search
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Show form only after selection or new request choice */}
|
||||
{(selectedRequest || showNewRequestForm) && (
|
||||
<>
|
||||
{/* Display logged-in user */}
|
||||
<div className="bg-black/30 border border-white/10 rounded-lg p-4">
|
||||
<p className="text-[#888] text-sm mb-2">Submitting as:</p>
|
||||
<div className="flex items-center gap-3">
|
||||
{user?.image && (
|
||||
<img
|
||||
src={user.image}
|
||||
alt={user.name}
|
||||
className="w-8 h-8 rounded-full ring-2 ring-[#00ff88]/30"
|
||||
/>
|
||||
)}
|
||||
<p className="text-[#00ff88] font-semibold">
|
||||
{user?.name || "Loading..."}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Show target details for new submissions */}
|
||||
{showNewRequestForm && (
|
||||
<>
|
||||
<select
|
||||
value={targetType}
|
||||
onChange={(e) => setTargetType(e.target.value as TargetType)}
|
||||
className="bg-black/50 border border-white/10 rounded-lg text-[#e0e0e0] p-4"
|
||||
>
|
||||
<option value="">Select target type...</option>
|
||||
<option value="model">🤖 AI Model</option>
|
||||
<option value="app">📱 App/Interface</option>
|
||||
<option value="tool">🔧 Tool/Function</option>
|
||||
<option value="agent">🤝 AI Agent</option>
|
||||
<option value="plugin">🔌 Plugin/Extension</option>
|
||||
<option value="custom">⚙️ Custom GPT/Bot</option>
|
||||
</select>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
value={targetName}
|
||||
onChange={(e) => setTargetName(e.target.value)}
|
||||
placeholder="Target name (e.g., ChatGPT-4, GitHub Copilot)"
|
||||
className="bg-black/50 border border-white/10 rounded-lg text-[#e0e0e0] p-4 transition-all focus:outline-none focus:border-[#00ff88] focus:shadow-[0_0_20px_rgba(0,255,136,0.2)]"
|
||||
/>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
value={provider}
|
||||
onChange={(e) => setProvider(e.target.value)}
|
||||
placeholder="Provider (e.g., OpenAI, Anthropic, Google)"
|
||||
className="bg-black/50 border border-white/10 rounded-lg text-[#e0e0e0] p-4 transition-all focus:outline-none focus:border-[#00ff88] focus:shadow-[0_0_20px_rgba(0,255,136,0.2)]"
|
||||
/>
|
||||
|
||||
<input
|
||||
type="url"
|
||||
value={targetUrl}
|
||||
onChange={(e) => setTargetUrl(e.target.value)}
|
||||
placeholder="Target URL (e.g., https://chat.openai.com)"
|
||||
className="bg-black/50 border border-white/10 rounded-lg text-[#e0e0e0] p-4 transition-all focus:outline-none focus:border-[#00ff88] focus:shadow-[0_0_20px_rgba(0,255,136,0.2)]"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="bg-black/30 border border-white/10 rounded-lg p-4">
|
||||
<p className="text-[#00aaff] text-sm mb-3">
|
||||
🔐 Access Requirements
|
||||
</p>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<label className="flex items-center gap-2 text-[#888] text-sm cursor-pointer">
|
||||
<Checkbox
|
||||
checked={requiresLogin}
|
||||
onCheckedChange={(checked) =>
|
||||
setRequiresLogin(checked === true)
|
||||
}
|
||||
/>
|
||||
Requires Login
|
||||
</label>
|
||||
<label className="flex items-center gap-2 text-[#888] text-sm cursor-pointer">
|
||||
<Checkbox
|
||||
checked={isPaid}
|
||||
onCheckedChange={(checked) => setIsPaid(checked === true)}
|
||||
/>
|
||||
Paid/Subscription
|
||||
</label>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
value={accessNotes}
|
||||
onChange={(e) => setAccessNotes(e.target.value)}
|
||||
placeholder="Additional access notes (e.g., 'Plus subscription required')"
|
||||
className="w-full mt-3 bg-black/50 border border-white/10 rounded-lg text-[#e0e0e0] p-2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<textarea
|
||||
value={leakText}
|
||||
onChange={(e) => setLeakText(e.target.value)}
|
||||
placeholder="Paste the suspected system prompt leak here..."
|
||||
required
|
||||
className="bg-black/50 border border-white/10 rounded-lg text-[#e0e0e0] p-4 min-h-[200px] font-mono text-sm resize-vertical transition-all focus:outline-none focus:border-[#00ff88] focus:shadow-[0_0_20px_rgba(0,255,136,0.2)]"
|
||||
/>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
value={leakContext}
|
||||
onChange={(e) => setLeakContext(e.target.value)}
|
||||
placeholder="Context: How was this obtained? (optional)"
|
||||
className="bg-black/50 border border-white/10 rounded-lg text-[#e0e0e0] p-4 transition-all focus:outline-none focus:border-[#00ff88] focus:shadow-[0_0_20px_rgba(0,255,136,0.2)]"
|
||||
/>
|
||||
|
||||
<label className="flex gap-2 items-center cursor-pointer">
|
||||
<Checkbox
|
||||
checked={hasToolPrompts}
|
||||
onCheckedChange={(checked) =>
|
||||
setHasToolPrompts(checked === true)
|
||||
}
|
||||
/>
|
||||
<span className="text-[#888] text-sm">
|
||||
This target has tools/functions with their own prompts
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className="bg-linear-to-r from-[#00ff88] to-[#00aaff] text-black p-4 rounded-lg font-bold transition-all hover:-translate-y-0.5 hover:shadow-[0_5px_20px_rgba(0,255,136,0.4)] disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{isSubmitting ? "Submitting..." : "Submit Leak to Library"}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import * as React from "react"
|
||||
import * as AvatarPrimitive from "@radix-ui/react-avatar"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Avatar({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AvatarPrimitive.Root>) {
|
||||
return (
|
||||
<AvatarPrimitive.Root
|
||||
data-slot="avatar"
|
||||
className={cn(
|
||||
"relative flex size-8 shrink-0 overflow-hidden rounded-full",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AvatarImage({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
|
||||
return (
|
||||
<AvatarPrimitive.Image
|
||||
data-slot="avatar-image"
|
||||
className={cn("aspect-square size-full", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AvatarFallback({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
|
||||
return (
|
||||
<AvatarPrimitive.Fallback
|
||||
data-slot="avatar-fallback"
|
||||
className={cn(
|
||||
"bg-muted flex size-full items-center justify-center rounded-full",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Avatar, AvatarImage, AvatarFallback }
|
||||
@@ -0,0 +1,60 @@
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||
outline:
|
||||
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost:
|
||||
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
||||
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||
icon: "size-9",
|
||||
"icon-sm": "size-8",
|
||||
"icon-lg": "size-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Button({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"button"> &
|
||||
VariantProps<typeof buttonVariants> & {
|
||||
asChild?: boolean
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="button"
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Button, buttonVariants }
|
||||
@@ -0,0 +1,30 @@
|
||||
import * as React from "react"
|
||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
|
||||
import { CheckIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Checkbox({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
|
||||
return (
|
||||
<CheckboxPrimitive.Root
|
||||
data-slot="checkbox"
|
||||
className={cn(
|
||||
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator
|
||||
data-slot="checkbox-indicator"
|
||||
className="grid place-content-center text-current transition-none"
|
||||
>
|
||||
<CheckIcon className="size-3.5" />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
)
|
||||
}
|
||||
|
||||
export { Checkbox }
|
||||
@@ -0,0 +1,117 @@
|
||||
import * as React from "react";
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Dialog = DialogPrimitive.Root;
|
||||
|
||||
const DialogTrigger = DialogPrimitive.Trigger;
|
||||
|
||||
const DialogPortal = DialogPrimitive.Portal;
|
||||
|
||||
const DialogClose = DialogPrimitive.Close;
|
||||
|
||||
const DialogOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Overlay
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
|
||||
|
||||
const DialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border border-white/20 bg-[#1a1a1a] p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
));
|
||||
DialogContent.displayName = DialogPrimitive.Content.displayName;
|
||||
|
||||
const DialogHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col space-y-1.5 text-center sm:text-left",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
DialogHeader.displayName = "DialogHeader";
|
||||
|
||||
const DialogFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
DialogFooter.displayName = "DialogFooter";
|
||||
|
||||
const DialogTitle = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-lg font-semibold leading-none tracking-tight text-[#e0e0e0]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DialogTitle.displayName = DialogPrimitive.Title.displayName;
|
||||
|
||||
const DialogDescription = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-[#888]", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DialogDescription.displayName = DialogPrimitive.Description.displayName;
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogPortal,
|
||||
DialogOverlay,
|
||||
DialogClose,
|
||||
DialogTrigger,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,255 @@
|
||||
import * as React from "react"
|
||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
||||
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function DropdownMenu({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
|
||||
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
|
||||
}
|
||||
|
||||
function DropdownMenuPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Trigger
|
||||
data-slot="dropdown-menu-trigger"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuContent({
|
||||
className,
|
||||
sideOffset = 4,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
data-slot="dropdown-menu-content"
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuItem({
|
||||
className,
|
||||
inset,
|
||||
variant = "default",
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
|
||||
inset?: boolean
|
||||
variant?: "default" | "destructive"
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Item
|
||||
data-slot="dropdown-menu-item"
|
||||
data-inset={inset}
|
||||
data-variant={variant}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuCheckboxItem({
|
||||
className,
|
||||
children,
|
||||
checked,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
data-slot="dropdown-menu-checkbox-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<CheckIcon className="size-4" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuRadioGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.RadioGroup
|
||||
data-slot="dropdown-menu-radio-group"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuRadioItem({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
data-slot="dropdown-menu-radio-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<CircleIcon className="size-2 fill-current" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuLabel({
|
||||
className,
|
||||
inset,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
|
||||
inset?: boolean
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Label
|
||||
data-slot="dropdown-menu-label"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
data-slot="dropdown-menu-separator"
|
||||
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuShortcut({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="dropdown-menu-shortcut"
|
||||
className={cn(
|
||||
"text-muted-foreground ml-auto text-xs tracking-widest",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuSub({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
|
||||
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
|
||||
}
|
||||
|
||||
function DropdownMenuSubTrigger({
|
||||
className,
|
||||
inset,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
data-slot="dropdown-menu-sub-trigger"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRightIcon className="ml-auto size-4" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuSubContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
data-slot="dropdown-menu-sub-content"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuSubContent,
|
||||
}
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@import "tw-animate-css";
|
||||
|
||||
body {
|
||||
@apply m-0;
|
||||
font-family:
|
||||
-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu",
|
||||
"Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family:
|
||||
source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace;
|
||||
}
|
||||
|
||||
:root {
|
||||
--background: #0f0f0f;
|
||||
--foreground: #e0e0e0;
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.141 0.005 285.823);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.141 0.005 285.823);
|
||||
--primary: oklch(0.21 0.006 285.885);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.967 0.001 286.375);
|
||||
--secondary-foreground: oklch(0.21 0.006 285.885);
|
||||
--muted: oklch(0.967 0.001 286.375);
|
||||
--muted-foreground: oklch(0.552 0.016 285.938);
|
||||
--accent: oklch(0.967 0.001 286.375);
|
||||
--accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--destructive-foreground: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.92 0.004 286.32);
|
||||
--input: oklch(0.92 0.004 286.32);
|
||||
--ring: oklch(0.871 0.006 286.286);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--radius: 0.625rem;
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.141 0.005 285.823);
|
||||
--sidebar-primary: oklch(0.21 0.006 285.885);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.967 0.001 286.375);
|
||||
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--sidebar-border: oklch(0.92 0.004 286.32);
|
||||
--sidebar-ring: oklch(0.871 0.006 286.286);
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-destructive-foreground: var(--destructive-foreground);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes gradientShift {
|
||||
0%,
|
||||
100% {
|
||||
filter: hue-rotate(0deg);
|
||||
}
|
||||
50% {
|
||||
filter: hue-rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes gridMove {
|
||||
from {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
to {
|
||||
transform: translate(50px, 50px);
|
||||
}
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(0, 255, 136, 0.3);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(0, 255, 136, 0.5);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { ConvexAuthProvider } from "@convex-dev/auth/react";
|
||||
import { ConvexReactClient } from "convex/react";
|
||||
import { BrowserRouter } from "react-router-dom";
|
||||
import "./index.css";
|
||||
import App from "./App.tsx";
|
||||
|
||||
const convex = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL as string);
|
||||
|
||||
console.log("Convex URL:", convex);
|
||||
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<StrictMode>
|
||||
<ConvexAuthProvider client={convex}>
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
</ConvexAuthProvider>
|
||||
</StrictMode>,
|
||||
);
|
||||
@@ -0,0 +1,217 @@
|
||||
import { useState, useMemo } from "react";
|
||||
import { useQuery } from "convex/react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { api } from "../../convex/_generated/api";
|
||||
|
||||
/**
|
||||
* Browse page component that displays provider cards with search functionality.
|
||||
* Each provider card shows:
|
||||
* - Provider name
|
||||
* - Number of verified leaks
|
||||
* - Target types available
|
||||
* - Sample target names
|
||||
*
|
||||
* Clicking a card navigates to the provider's detailed leaks page.
|
||||
*/
|
||||
export default function Browse() {
|
||||
const navigate = useNavigate();
|
||||
const providers = useQuery(api.leaks.getProviders);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
|
||||
// Filter providers based on search query
|
||||
const filteredProviders = useMemo(() => {
|
||||
if (!providers) return [];
|
||||
if (!searchQuery.trim()) return providers;
|
||||
|
||||
const query = searchQuery.toLowerCase().trim();
|
||||
return providers.filter(
|
||||
(provider) =>
|
||||
provider.provider.toLowerCase().includes(query) ||
|
||||
provider.sampleTargets.some((target) =>
|
||||
target.toLowerCase().includes(query)
|
||||
)
|
||||
);
|
||||
}, [providers, searchQuery]);
|
||||
|
||||
// Format relative time
|
||||
const formatRelativeTime = (timestamp: number) => {
|
||||
const now = Date.now();
|
||||
const diff = now - timestamp;
|
||||
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
|
||||
const hours = Math.floor(diff / (1000 * 60 * 60));
|
||||
const minutes = Math.floor(diff / (1000 * 60));
|
||||
|
||||
if (minutes < 1) return "Just now";
|
||||
if (minutes < 60) return `${minutes}m ago`;
|
||||
if (hours < 24) return `${hours}h ago`;
|
||||
if (days < 30) return `${days}d ago`;
|
||||
return new Date(timestamp).toLocaleDateString();
|
||||
};
|
||||
|
||||
// Loading state
|
||||
if (providers === undefined) {
|
||||
return (
|
||||
<div className="flex justify-center items-center h-64">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-[#00ff88]"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
{/* Header Section */}
|
||||
<div className="text-center">
|
||||
<h1 className="text-4xl font-bold bg-linear-to-r from-[#00ff88] via-[#00aaff] to-[#ff00ff] bg-clip-text text-transparent mb-4">
|
||||
🔍 Browse Prompts
|
||||
</h1>
|
||||
<p className="text-[#888] text-lg mb-6">
|
||||
Explore verified system prompts from {providers.length} providers
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Search Bar */}
|
||||
<div className="max-w-2xl mx-auto">
|
||||
<div className="relative">
|
||||
<div className="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none">
|
||||
<svg
|
||||
className="w-5 h-5 text-[#888]"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search providers or targets..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="w-full bg-white/5 border border-white/20 rounded-xl py-4 pl-12 pr-4 text-[#e0e0e0] placeholder-[#666] focus:outline-none focus:border-[#00ff88]/50 focus:ring-2 focus:ring-[#00ff88]/20 transition-all text-lg"
|
||||
/>
|
||||
{searchQuery && (
|
||||
<button
|
||||
onClick={() => setSearchQuery("")}
|
||||
className="absolute inset-y-0 right-0 pr-4 flex items-center text-[#888] hover:text-[#e0e0e0] transition-colors"
|
||||
>
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{searchQuery && (
|
||||
<p className="text-[#888] text-sm mt-2 text-center">
|
||||
Found {filteredProviders.length} provider
|
||||
{filteredProviders.length !== 1 ? "s" : ""} matching "{searchQuery}"
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Provider Cards Grid */}
|
||||
{filteredProviders.length === 0 ? (
|
||||
<div className="text-center py-12">
|
||||
<div className="text-6xl mb-4">🔎</div>
|
||||
<p className="text-[#888] text-xl mb-2">No providers found</p>
|
||||
<p className="text-[#666] text-sm">
|
||||
Try a different search term or clear the search
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{filteredProviders.map((provider) => (
|
||||
<div
|
||||
key={provider.provider}
|
||||
onClick={() =>
|
||||
navigate(`/browse/${encodeURIComponent(provider.provider)}`)
|
||||
}
|
||||
className="group bg-white/3 border border-white/10 rounded-2xl p-6 backdrop-blur-sm cursor-pointer transition-all duration-300 hover:bg-white/5 hover:border-[#00ff88]/40 hover:-translate-y-1 hover:shadow-[0_10px_40px_rgba(0,255,136,0.15)]"
|
||||
>
|
||||
{/* Provider Header */}
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="text-xl font-bold text-[#e0e0e0] group-hover:text-[#00ff88] transition-colors truncate">
|
||||
{provider.provider}
|
||||
</h3>
|
||||
<p className="text-[#666] text-sm mt-1">
|
||||
Updated {formatRelativeTime(provider.latestLeakTime)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-[#00ff88]/20 text-[#00ff88] px-3 py-1 rounded-full text-sm font-bold ml-2 shrink-0">
|
||||
{provider.leakCount}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sample Targets */}
|
||||
<div className="space-y-1">
|
||||
<p className="text-[#666] text-xs uppercase tracking-wider">
|
||||
Prompts
|
||||
</p>
|
||||
<div className="text-[#aaa] text-sm space-y-1">
|
||||
{provider.sampleTargets.slice(0, 3).map((target, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className="truncate flex items-center gap-2"
|
||||
>
|
||||
<span className="text-[#00ff88] opacity-60">→</span>
|
||||
<span>{target}</span>
|
||||
</div>
|
||||
))}
|
||||
{provider.sampleTargets.length > 3 && (
|
||||
<p className="text-[#666] text-xs">
|
||||
+{provider.sampleTargets.length - 3} more
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* View Arrow */}
|
||||
<div className="mt-4 pt-4 border-t border-white/5 flex items-center justify-between">
|
||||
<span className="text-[#00ff88] text-sm font-medium opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
View all prompts
|
||||
</span>
|
||||
<svg
|
||||
className="w-5 h-5 text-[#00ff88] transform translate-x-0 group-hover:translate-x-1 transition-transform"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M9 5l7 7-7 7"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Stats Footer */}
|
||||
<div className="text-center pt-8 border-t border-white/5">
|
||||
<p className="text-[#666] text-sm">
|
||||
Total: {providers.reduce((sum, p) => sum + p.leakCount, 0)} verified
|
||||
prompts across {providers.length} providers
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,436 @@
|
||||
import { useConvexAuth, useMutation, useQuery } from "convex/react";
|
||||
import { api } from "../../convex/_generated/api";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
|
||||
export default function Dashboard() {
|
||||
const { isAuthenticated } = useConvexAuth();
|
||||
const navigate = useNavigate();
|
||||
const user = useQuery(api.users.currentUser);
|
||||
const closeRequest = useMutation(api.requests.closeRequest);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Get user dashboard data - use "skip" to prevent execution until user is loaded
|
||||
const userDashboardData = useQuery(
|
||||
api.users.getUserDashboardData,
|
||||
user ? { userId: user._id } : "skip",
|
||||
);
|
||||
|
||||
const handleCloseRequest = async (requestId: string) => {
|
||||
try {
|
||||
const result = await closeRequest({ requestId: requestId as any });
|
||||
|
||||
// Check if the mutation returned an error
|
||||
if (!result.success) {
|
||||
setError(result.error);
|
||||
return;
|
||||
}
|
||||
|
||||
// Success - clear any previous errors
|
||||
setError(null);
|
||||
} catch (error) {
|
||||
// Fallback for unexpected errors
|
||||
console.error("Error closing request:", error);
|
||||
setError(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Failed to close request. Please try again.",
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
if (!isAuthenticated) {
|
||||
navigate("/");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!userDashboardData) {
|
||||
return (
|
||||
<div className="flex justify-center items-center h-64">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-[#00ff88]"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const leaksCount = userDashboardData.leaks?.length || 0;
|
||||
const requestsCount = userDashboardData.requests?.length || 0;
|
||||
|
||||
// Combine leaks and requests into a single activity list
|
||||
type ActivityItem =
|
||||
| {
|
||||
type: "leak";
|
||||
_id: string;
|
||||
_creationTime: number;
|
||||
targetName: string;
|
||||
targetType: string;
|
||||
provider: string;
|
||||
isFullyVerified: boolean;
|
||||
}
|
||||
| {
|
||||
type: "request";
|
||||
_id: string;
|
||||
_creationTime: number;
|
||||
targetName: string;
|
||||
targetType: string;
|
||||
provider: string;
|
||||
closed: boolean;
|
||||
targetUrl?: string;
|
||||
};
|
||||
|
||||
const activityItems: ActivityItem[] = [
|
||||
...(userDashboardData.leaks || []).map((leak) => ({
|
||||
type: "leak" as const,
|
||||
...leak,
|
||||
})),
|
||||
...(userDashboardData.requests || []).map((request) => ({
|
||||
type: "request" as const,
|
||||
...request,
|
||||
})),
|
||||
].sort((a, b) => b._creationTime - a._creationTime); // Sort by most recent first
|
||||
|
||||
// Helper function to format date
|
||||
const formatDate = (timestamp: number) => {
|
||||
const date = new Date(timestamp);
|
||||
const now = new Date();
|
||||
const diffMs = now.getTime() - date.getTime();
|
||||
const diffMins = Math.floor(diffMs / 60000);
|
||||
const diffHours = Math.floor(diffMs / 3600000);
|
||||
const diffDays = Math.floor(diffMs / 86400000);
|
||||
|
||||
if (diffMins < 1) return "Just now";
|
||||
if (diffMins < 60) return `${diffMins}m ago`;
|
||||
if (diffHours < 24) return `${diffHours}h ago`;
|
||||
if (diffDays < 7) return `${diffDays}d ago`;
|
||||
|
||||
return date.toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
year: date.getFullYear() !== now.getFullYear() ? "numeric" : undefined,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
{/* Header Section */}
|
||||
<div className="bg-white/3 border border-white/10 rounded-2xl p-8 backdrop-blur-sm relative overflow-hidden">
|
||||
<div
|
||||
className="absolute top-0 left-0 w-full h-full pointer-events-none opacity-10"
|
||||
style={{
|
||||
backgroundImage:
|
||||
"linear-gradient(rgba(0, 255, 136, 0.1) 1px, transparent 1px), linear-gradient(90deg, rgba(0, 255, 136, 0.1) 1px, transparent 1px)",
|
||||
backgroundSize: "20px 20px",
|
||||
}}
|
||||
/>
|
||||
<div className="relative z-10">
|
||||
<h2 className="text-3xl font-bold bg-linear-to-r from-[#00ff88] via-[#00aaff] to-[#ff00ff] bg-clip-text text-transparent mb-2">
|
||||
Welcome back, {userDashboardData.name}!
|
||||
</h2>
|
||||
<p className="text-[#888] text-lg">{userDashboardData.email}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Error message */}
|
||||
{error && (
|
||||
<div className="bg-red-500/10 border border-red-500/30 rounded-lg p-4">
|
||||
<p className="text-red-400 text-sm">⚠️ {error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Points Display - Prominent */}
|
||||
<div className="bg-linear-to-r from-[#00ff88]/10 via-[#00aaff]/10 to-[#ff00ff]/10 border-2 border-[#00ff88]/30 rounded-2xl p-8 backdrop-blur-sm">
|
||||
<div className="text-center">
|
||||
<div className="text-[#888] text-sm uppercase tracking-wider mb-2">
|
||||
Total Points
|
||||
</div>
|
||||
<div className="text-6xl font-bold bg-linear-to-r from-[#00ff88] via-[#00aaff] to-[#ff00ff] bg-clip-text text-transparent">
|
||||
{userDashboardData.points}
|
||||
</div>
|
||||
<div className="text-[#888] text-sm mt-2">
|
||||
Rank #{userDashboardData.rank} of {userDashboardData.totalUsers}
|
||||
</div>
|
||||
<div className="text-[#888] text-xs mt-1">
|
||||
Keep contributing to earn more!
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{/* Leaks Stats */}
|
||||
<div className="bg-white/3 border border-white/10 rounded-2xl p-6 backdrop-blur-sm hover:border-[#00ff88]/50 transition-all">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="bg-[#00ff88]/20 rounded-full p-4">
|
||||
<svg
|
||||
className="w-8 h-8 text-[#00ff88]"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-3xl font-bold text-[#00ff88]">
|
||||
{leaksCount}
|
||||
</div>
|
||||
<div className="text-[#888] text-sm">Submitted Leaks</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Requests Stats */}
|
||||
<div className="bg-white/3 border border-white/10 rounded-2xl p-6 backdrop-blur-sm hover:border-[#00aaff]/50 transition-all">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="bg-[#00aaff]/20 rounded-full p-4">
|
||||
<svg
|
||||
className="w-8 h-8 text-[#00aaff]"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M13 10V3L4 14h7v7l9-11h-7z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-3xl font-bold text-[#00aaff]">
|
||||
{requestsCount}
|
||||
</div>
|
||||
<div className="text-[#888] text-sm">Requests Made</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Activity */}
|
||||
<div className="bg-white/3 border border-white/10 rounded-2xl p-6 backdrop-blur-sm">
|
||||
<h3 className="text-xl font-bold text-[#00aaff] mb-4">📊 Activity</h3>
|
||||
{activityItems.length === 0 ? (
|
||||
<div className="text-center py-8 text-[#888]">
|
||||
<p className="text-lg mb-2">No activity yet</p>
|
||||
<p className="text-sm">
|
||||
Start contributing by submitting leaks or creating requests!
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{activityItems.map((item) => (
|
||||
<div
|
||||
key={`${item.type}-${item._id}`}
|
||||
className="bg-white/5 border border-white/10 rounded-lg p-4 hover:bg-white/10 transition-all"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex items-start gap-3 flex-1">
|
||||
{/* Icon */}
|
||||
<div
|
||||
className={`rounded-full p-2 mt-1 ${
|
||||
item.type === "leak"
|
||||
? "bg-[#00ff88]/20"
|
||||
: "bg-[#00aaff]/20"
|
||||
}`}
|
||||
>
|
||||
{item.type === "leak" ? (
|
||||
<svg
|
||||
className="w-5 h-5 text-[#00ff88]"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
||||
/>
|
||||
</svg>
|
||||
) : (
|
||||
<svg
|
||||
className="w-5 h-5 text-[#00aaff]"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M13 10V3L4 14h7v7l9-11h-7z"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span
|
||||
className={`text-sm font-bold ${
|
||||
item.type === "leak"
|
||||
? "text-[#00ff88]"
|
||||
: "text-[#00aaff]"
|
||||
}`}
|
||||
>
|
||||
{item.type === "leak"
|
||||
? "Submitted Leak"
|
||||
: "Created Request"}
|
||||
</span>
|
||||
{item.type === "leak" && (
|
||||
<span
|
||||
className={`text-xs px-2 py-0.5 rounded-full ${
|
||||
item.isFullyVerified
|
||||
? "bg-green-500/20 text-green-400 border border-green-500/30"
|
||||
: "bg-yellow-500/20 text-yellow-400 border border-yellow-500/30"
|
||||
}`}
|
||||
>
|
||||
{item.isFullyVerified ? "✓ Verified" : "Pending"}
|
||||
</span>
|
||||
)}
|
||||
{item.type === "request" && (
|
||||
<span
|
||||
className={`text-xs px-2 py-0.5 rounded-full ${
|
||||
item.closed
|
||||
? "bg-gray-500/20 text-gray-400 border border-gray-500/30"
|
||||
: "bg-blue-500/20 text-blue-400 border border-blue-500/30"
|
||||
}`}
|
||||
>
|
||||
{item.closed ? "Closed" : "Open"}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-[#e0e0e0] font-medium mt-1">
|
||||
{item.targetName}
|
||||
</p>
|
||||
<div className="flex items-center gap-2 mt-1 text-sm text-[#888]">
|
||||
<span className="capitalize">{item.targetType}</span>
|
||||
<span>•</span>
|
||||
<span>{item.provider}</span>
|
||||
</div>
|
||||
{item.type === "request" && item.targetUrl && (
|
||||
<a
|
||||
href={item.targetUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1 mt-2 text-xs text-[#00aaff] hover:text-[#00ccff] transition-colors"
|
||||
>
|
||||
<svg
|
||||
className="w-3 h-3"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
|
||||
/>
|
||||
</svg>
|
||||
<span className="truncate max-w-[200px]">
|
||||
{item.targetUrl}
|
||||
</span>
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Timestamp and Actions */}
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="text-xs text-[#888] whitespace-nowrap">
|
||||
{formatDate(item._creationTime)}
|
||||
</div>
|
||||
{item.type === "request" && !item.closed && (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-red-400 hover:text-red-300 hover:bg-red-500/10 h-7 px-2 text-xs"
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Close Request</DialogTitle>
|
||||
<DialogDescription>
|
||||
Are you sure you want to close this request? This
|
||||
action cannot be undone.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<DialogClose asChild>
|
||||
<Button variant="ghost">Cancel</Button>
|
||||
</DialogClose>
|
||||
<Button
|
||||
onClick={() => handleCloseRequest(item._id)}
|
||||
className="bg-red-500 hover:bg-red-600 text-white"
|
||||
>
|
||||
Close Request
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Quick Actions */}
|
||||
<div className="bg-white/3 border border-white/10 rounded-2xl p-6 backdrop-blur-sm">
|
||||
<h3 className="text-xl font-bold text-[#00aaff] mb-4">
|
||||
⚡ Quick Actions
|
||||
</h3>
|
||||
<div className="flex gap-4 flex-wrap">
|
||||
<button
|
||||
onClick={() => navigate("/")}
|
||||
className="bg-linear-to-r from-[#00ff88] to-[#00aaff] text-black px-6 py-3 rounded-lg font-bold transition-all hover:-translate-y-0.5 hover:shadow-[0_5px_20px_rgba(0,255,136,0.4)]"
|
||||
>
|
||||
📝 Submit New Leak
|
||||
</button>
|
||||
<button
|
||||
onClick={() => navigate("/browse")}
|
||||
className="bg-white/10 border border-white/20 text-[#e0e0e0] px-6 py-3 rounded-lg font-bold transition-all hover:bg-white/20 hover:border-[#00ff88]/50"
|
||||
>
|
||||
🔍 Browse Prompts
|
||||
</button>
|
||||
<button
|
||||
onClick={() => navigate("/leaderboard")}
|
||||
className="bg-white/10 border border-white/20 text-[#e0e0e0] px-6 py-3 rounded-lg font-bold transition-all hover:bg-white/20 hover:border-[#ff00ff]/50"
|
||||
>
|
||||
🏆 View Leaderboard
|
||||
</button>
|
||||
<button
|
||||
onClick={() => navigate("/requests")}
|
||||
className="bg-white/10 border border-white/20 text-[#e0e0e0] px-6 py-3 rounded-lg font-bold transition-all hover:bg-white/20 hover:border-[#00aaff]/50"
|
||||
>
|
||||
🎯 Browse Requests
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
import { useQuery } from "convex/react";
|
||||
import { api } from "../../convex/_generated/api";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import { Trophy, Medal, Award } from "lucide-react";
|
||||
|
||||
export default function Leaderboard() {
|
||||
const leaderboardData = useQuery(api.leaderboard.getLeaderboard);
|
||||
|
||||
if (!leaderboardData) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-[#888]">Loading leaderboard...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const getRankIcon = (rank: number) => {
|
||||
switch (rank) {
|
||||
case 1:
|
||||
return <Trophy className="w-6 h-6 text-yellow-500" />;
|
||||
case 2:
|
||||
return <Medal className="w-6 h-6 text-gray-400" />;
|
||||
case 3:
|
||||
return <Award className="w-6 h-6 text-amber-700" />;
|
||||
default:
|
||||
return (
|
||||
<span className="text-lg font-bold text-[#888] w-6 text-center">
|
||||
{rank}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mx-auto">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-4xl font-bold text-white mb-2 flex items-center gap-3">
|
||||
<Trophy className="w-8 h-8 text-[#00ff88]" />
|
||||
Leaderboard
|
||||
</h1>
|
||||
<p className="text-[#888]">
|
||||
Top contributors ranked by points earned from verified leaks
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* How to Earn Points */}
|
||||
<div className="bg-[#1a1a1a] border border-[#00aaff]/20 rounded-lg p-6 mb-6">
|
||||
<h2 className="text-xl font-semibold text-[#00aaff] mb-4 flex items-center gap-2">
|
||||
💎 How to Earn Points
|
||||
</h2>
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
<div className="bg-[#00ff88]/5 border border-[#00ff88]/20 rounded-lg p-4">
|
||||
<div className="text-3xl font-bold text-[#00ff88] mb-2">+100</div>
|
||||
<div className="text-white font-semibold mb-1">
|
||||
Submit First Leak
|
||||
</div>
|
||||
<div className="text-sm text-[#888]">
|
||||
Be the first to submit a leak for a request that gets verified by
|
||||
2 other users
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-[#00aaff]/5 border border-[#00aaff]/20 rounded-lg p-4">
|
||||
<div className="text-3xl font-bold text-[#00aaff] mb-2">+50</div>
|
||||
<div className="text-white font-semibold mb-1">Verify a Leak</div>
|
||||
<div className="text-sm text-[#888]">
|
||||
Confirm someone else's leak by submitting the same information to
|
||||
help verify it
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-yellow-500/5 border border-yellow-500/20 rounded-lg p-4">
|
||||
<div className="text-3xl font-bold text-yellow-500 mb-2">+20</div>
|
||||
<div className="text-white font-semibold mb-1">Create Request</div>
|
||||
<div className="text-sm text-[#888]">
|
||||
Request a leak that gets verified by 2 different users
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 p-3 bg-[#00ff88]/5 border border-[#00ff88]/20 rounded text-sm text-[#ccc]">
|
||||
<strong className="text-[#00ff88]">💡 Pro Tip:</strong> A leak is
|
||||
verified when 2 different users submit the same information. The first
|
||||
submitter gets the biggest reward!
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Top 10 Users */}
|
||||
<div className="bg-[#1a1a1a] border border-[#00ff88]/20 rounded-lg overflow-hidden">
|
||||
<div className="bg-linear-to-r from-[#00ff88]/10 to-transparent p-4 border-b border-[#00ff88]/20 flex items-center justify-between">
|
||||
<h2 className="text-xl font-semibold text-white">
|
||||
Top 10 Contributors
|
||||
</h2>
|
||||
<div className="text-right">
|
||||
<div className="text-2xl font-bold text-[#00ff88]">
|
||||
{leaderboardData.totalUsers.toLocaleString()}
|
||||
</div>
|
||||
<div className="text-xs text-[#888]">total users</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="divide-y divide-[#00ff88]/10">
|
||||
{leaderboardData.topUsers.map((user) => (
|
||||
<div
|
||||
key={user._id}
|
||||
className={`p-4 flex items-center gap-4 transition-colors hover:bg-[#00ff88]/5 ${
|
||||
user.rank <= 3 ? "bg-[#00ff88]/5" : ""
|
||||
} ${user.isCurrentUser ? "ring-1 ring-[#00ff88]/40 bg-[#00ff88]/10" : ""}`}
|
||||
>
|
||||
{/* Rank */}
|
||||
<div className="flex items-center justify-center w-10">
|
||||
{getRankIcon(user.rank)}
|
||||
</div>
|
||||
|
||||
{/* Avatar */}
|
||||
<Avatar className="w-12 h-12 ring-2 ring-[#00ff88]/30">
|
||||
<AvatarImage src={user.image} alt={user.name} />
|
||||
<AvatarFallback className="bg-[#00ff88] text-black font-bold">
|
||||
{user.name.charAt(0).toUpperCase()}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
|
||||
{/* User Info */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-white font-medium truncate flex items-center gap-2">
|
||||
<span className="truncate">{user.name}</span>
|
||||
{user.isCurrentUser && (
|
||||
<span className="text-xs font-semibold bg-[#00ff88] text-black px-2 py-0.5 rounded-full">
|
||||
You
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-sm text-[#888]">Rank #{user.rank}</div>
|
||||
</div>
|
||||
|
||||
{/* Points */}
|
||||
<div className="text-right">
|
||||
<div className="text-2xl font-bold text-[#00ff88]">
|
||||
{user.points.toLocaleString()}
|
||||
</div>
|
||||
<div className="text-xs text-[#888]">points</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Current User Rank (if not in top 10) */}
|
||||
{leaderboardData.currentUserRank && (
|
||||
<div className="mt-6">
|
||||
<div className="bg-linear-to-r from-[#00ff88]/20 to-[#00ff88]/5 border-2 border-[#00ff88]/40 rounded-lg overflow-hidden">
|
||||
<div className="bg-[#00ff88]/10 p-3 border-b border-[#00ff88]/30">
|
||||
<h3 className="text-lg font-semibold text-white">Your Ranking</h3>
|
||||
</div>
|
||||
<div className="p-4 flex items-center gap-4">
|
||||
{/* Rank */}
|
||||
<div className="flex items-center justify-center w-10">
|
||||
<span className="text-lg font-bold text-[#00ff88] w-6 text-center">
|
||||
{leaderboardData.currentUserRank.rank}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Avatar */}
|
||||
<Avatar className="w-12 h-12 ring-2 ring-[#00ff88]/50">
|
||||
<AvatarImage
|
||||
src={leaderboardData.currentUserRank.image}
|
||||
alt={leaderboardData.currentUserRank.name}
|
||||
/>
|
||||
<AvatarFallback className="bg-[#00ff88] text-black font-bold">
|
||||
{leaderboardData.currentUserRank.name.charAt(0).toUpperCase()}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
|
||||
{/* User Info */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-white font-medium truncate">
|
||||
{leaderboardData.currentUserRank.name}
|
||||
</div>
|
||||
<div className="text-sm text-[#888]">
|
||||
Rank #{leaderboardData.currentUserRank.rank}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Points */}
|
||||
<div className="text-right">
|
||||
<div className="text-2xl font-bold text-[#00ff88]">
|
||||
{leaderboardData.currentUserRank.points.toLocaleString()}
|
||||
</div>
|
||||
<div className="text-xs text-[#888]">points</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-center text-sm text-[#888] mt-3">
|
||||
Keep contributing to climb the leaderboard!
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{leaderboardData.topUsers.length === 0 && (
|
||||
<div className="text-center py-12 text-[#888]">
|
||||
<Trophy className="w-16 h-16 mx-auto mb-4 opacity-50" />
|
||||
<p className="text-lg">No users on the leaderboard yet</p>
|
||||
<p className="text-sm mt-2">
|
||||
Be the first to contribute and earn points!
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,449 @@
|
||||
import { useState, useMemo } from "react";
|
||||
import { useQuery } from "convex/react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { api } from "../../convex/_generated/api";
|
||||
|
||||
/**
|
||||
* Provider leaks page component that displays all prompts from a specific provider.
|
||||
* Features:
|
||||
* - Search/filter by target name
|
||||
* - Filter by target type
|
||||
* - Expandable prompt cards with full details
|
||||
* - Copy to clipboard functionality
|
||||
*/
|
||||
export default function ProviderLeaks() {
|
||||
const navigate = useNavigate();
|
||||
const { provider } = useParams<{ provider: string }>();
|
||||
const decodedProvider = provider ? decodeURIComponent(provider) : "";
|
||||
|
||||
const leaks = useQuery(
|
||||
api.leaks.getLeaksByProvider,
|
||||
decodedProvider ? { provider: decodedProvider } : "skip"
|
||||
);
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [selectedType, setSelectedType] = useState<string>("all");
|
||||
const [expandedLeaks, setExpandedLeaks] = useState<Set<string>>(new Set());
|
||||
const [copiedLeakId, setCopiedLeakId] = useState<string | null>(null);
|
||||
|
||||
// Get unique target types from leaks
|
||||
const targetTypes = useMemo(() => {
|
||||
if (!leaks) return [];
|
||||
const types = new Set(leaks.map((leak) => leak.targetType));
|
||||
return Array.from(types);
|
||||
}, [leaks]);
|
||||
|
||||
// Filter leaks based on search and type filter
|
||||
const filteredLeaks = useMemo(() => {
|
||||
if (!leaks) return [];
|
||||
|
||||
let filtered = leaks;
|
||||
|
||||
// Filter by search query
|
||||
if (searchQuery.trim()) {
|
||||
const query = searchQuery.toLowerCase().trim();
|
||||
filtered = filtered.filter(
|
||||
(leak) =>
|
||||
leak.targetName.toLowerCase().includes(query) ||
|
||||
leak.leakText.toLowerCase().includes(query) ||
|
||||
(leak.leakContext && leak.leakContext.toLowerCase().includes(query))
|
||||
);
|
||||
}
|
||||
|
||||
// Filter by target type
|
||||
if (selectedType !== "all") {
|
||||
filtered = filtered.filter((leak) => leak.targetType === selectedType);
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}, [leaks, searchQuery, selectedType]);
|
||||
|
||||
// Toggle expanded state for a leak
|
||||
const toggleExpanded = (leakId: string) => {
|
||||
setExpandedLeaks((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(leakId)) {
|
||||
next.delete(leakId);
|
||||
} else {
|
||||
next.add(leakId);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
// Expand all leaks
|
||||
const expandAll = () => {
|
||||
if (!filteredLeaks) return;
|
||||
setExpandedLeaks(new Set(filteredLeaks.map((leak) => leak._id)));
|
||||
};
|
||||
|
||||
// Collapse all leaks
|
||||
const collapseAll = () => {
|
||||
setExpandedLeaks(new Set());
|
||||
};
|
||||
|
||||
// Copy prompt to clipboard
|
||||
const copyPrompt = async (text: string, leakId: string) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
setCopiedLeakId(leakId);
|
||||
setTimeout(() => setCopiedLeakId(null), 2000);
|
||||
} catch (err) {
|
||||
console.error("Failed to copy:", err);
|
||||
}
|
||||
};
|
||||
|
||||
// Get emoji for target type
|
||||
const getTargetTypeEmoji = (type: string) => {
|
||||
switch (type) {
|
||||
case "model":
|
||||
return "🤖";
|
||||
case "app":
|
||||
return "📱";
|
||||
case "tool":
|
||||
return "🔧";
|
||||
case "agent":
|
||||
return "🕵️";
|
||||
case "plugin":
|
||||
return "🔌";
|
||||
case "custom":
|
||||
return "⚙️";
|
||||
default:
|
||||
return "📦";
|
||||
}
|
||||
};
|
||||
|
||||
// Format date
|
||||
const formatDate = (timestamp: number) => {
|
||||
return new Date(timestamp).toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
});
|
||||
};
|
||||
|
||||
// Loading state
|
||||
if (leaks === undefined) {
|
||||
return (
|
||||
<div className="flex justify-center items-center h-64">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-[#00ff88]"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Back Button & Header */}
|
||||
<div className="flex items-center gap-4 mb-6">
|
||||
<button
|
||||
onClick={() => navigate("/browse")}
|
||||
className="flex items-center gap-2 text-[#888] hover:text-[#00ff88] transition-colors group"
|
||||
>
|
||||
<svg
|
||||
className="w-5 h-5 transform group-hover:-translate-x-1 transition-transform"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M15 19l-7-7 7-7"
|
||||
/>
|
||||
</svg>
|
||||
<span>Back to Browse</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Provider Header */}
|
||||
<div className="bg-linear-to-r from-[#00ff88]/10 via-[#00aaff]/10 to-[#ff00ff]/10 border border-[#00ff88]/30 rounded-2xl p-8">
|
||||
<div className="flex items-center justify-between flex-wrap gap-4">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-[#e0e0e0] mb-2">
|
||||
{decodedProvider}
|
||||
</h1>
|
||||
<p className="text-[#888]">
|
||||
{leaks.length} verified prompt{leaks.length !== 1 ? "s" : ""}{" "}
|
||||
available
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
{targetTypes.map((type) => (
|
||||
<span
|
||||
key={type}
|
||||
className="bg-white/10 border border-white/20 px-3 py-1.5 rounded-lg text-sm text-[#aaa] flex items-center gap-1.5"
|
||||
>
|
||||
<span>{getTargetTypeEmoji(type)}</span>
|
||||
<span className="capitalize">{type}</span>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Search and Filters */}
|
||||
<div className="flex flex-col md:flex-row gap-4">
|
||||
{/* Search Bar */}
|
||||
<div className="flex-1 relative">
|
||||
<div className="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none">
|
||||
<svg
|
||||
className="w-5 h-5 text-[#888]"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search prompts..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="w-full bg-white/5 border border-white/20 rounded-xl py-3 pl-12 pr-4 text-[#e0e0e0] placeholder-[#666] focus:outline-none focus:border-[#00ff88]/50 focus:ring-2 focus:ring-[#00ff88]/20 transition-all"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Type Filter */}
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
<button
|
||||
onClick={() => setSelectedType("all")}
|
||||
className={`px-4 py-2 rounded-lg font-medium transition-all ${
|
||||
selectedType === "all"
|
||||
? "bg-[#00ff88]/20 text-[#00ff88] border border-[#00ff88]/40"
|
||||
: "bg-white/5 text-[#888] border border-white/10 hover:border-white/30"
|
||||
}`}
|
||||
>
|
||||
All
|
||||
</button>
|
||||
{targetTypes.map((type) => (
|
||||
<button
|
||||
key={type}
|
||||
onClick={() => setSelectedType(type)}
|
||||
className={`px-4 py-2 rounded-lg font-medium transition-all flex items-center gap-1.5 ${
|
||||
selectedType === type
|
||||
? "bg-[#00ff88]/20 text-[#00ff88] border border-[#00ff88]/40"
|
||||
: "bg-white/5 text-[#888] border border-white/10 hover:border-white/30"
|
||||
}`}
|
||||
>
|
||||
<span>{getTargetTypeEmoji(type)}</span>
|
||||
<span className="capitalize">{type}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Expand/Collapse Controls */}
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-[#888] text-sm">
|
||||
Showing {filteredLeaks.length} of {leaks.length} prompts
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={expandAll}
|
||||
className="text-sm text-[#00aaff] hover:text-[#33bbff] transition-colors"
|
||||
>
|
||||
Expand all
|
||||
</button>
|
||||
<span className="text-[#666]">|</span>
|
||||
<button
|
||||
onClick={collapseAll}
|
||||
className="text-sm text-[#00aaff] hover:text-[#33bbff] transition-colors"
|
||||
>
|
||||
Collapse all
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Leaks List */}
|
||||
{filteredLeaks.length === 0 ? (
|
||||
<div className="text-center py-12 bg-white/3 border border-white/10 rounded-2xl">
|
||||
<div className="text-6xl mb-4">🔎</div>
|
||||
<p className="text-[#888] text-xl mb-2">No prompts found</p>
|
||||
<p className="text-[#666] text-sm">
|
||||
Try adjusting your search or filters
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{filteredLeaks.map((leak) => {
|
||||
const isExpanded = expandedLeaks.has(leak._id);
|
||||
return (
|
||||
<div
|
||||
key={leak._id}
|
||||
className="bg-white/3 border border-white/10 rounded-xl overflow-hidden hover:border-white/20 transition-all"
|
||||
>
|
||||
{/* Leak Header - Always visible */}
|
||||
<div
|
||||
onClick={() => toggleExpanded(leak._id)}
|
||||
className="p-5 cursor-pointer flex items-center justify-between gap-4 hover:bg-white/5 transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-4 flex-1 min-w-0">
|
||||
<span
|
||||
className="text-[#00aaff] transition-transform duration-200 shrink-0"
|
||||
style={{
|
||||
transform: isExpanded
|
||||
? "rotate(90deg)"
|
||||
: "rotate(0deg)",
|
||||
}}
|
||||
>
|
||||
▶
|
||||
</span>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-3 flex-wrap">
|
||||
<h3 className="text-lg font-semibold text-[#e0e0e0] truncate">
|
||||
{leak.targetName}
|
||||
</h3>
|
||||
<span className="bg-white/10 text-[#aaa] px-2 py-0.5 rounded text-xs flex items-center gap-1 shrink-0">
|
||||
<span>{getTargetTypeEmoji(leak.targetType)}</span>
|
||||
<span className="capitalize">{leak.targetType}</span>
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-[#666] text-sm mt-1">
|
||||
Added {formatDate(leak._creationTime)}
|
||||
{leak.submitterName && ` by ${leak.submitterName}`}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tags */}
|
||||
<div className="flex gap-2 shrink-0">
|
||||
{leak.requiresLogin && (
|
||||
<span className="bg-yellow-500/20 text-yellow-400 px-2 py-1 rounded text-xs">
|
||||
🔐 Login
|
||||
</span>
|
||||
)}
|
||||
{leak.isPaid && (
|
||||
<span className="bg-green-500/20 text-green-400 px-2 py-1 rounded text-xs">
|
||||
💰 Paid
|
||||
</span>
|
||||
)}
|
||||
{leak.hasToolPrompts && (
|
||||
<span className="bg-blue-500/20 text-blue-400 px-2 py-1 rounded text-xs">
|
||||
🔧 Tools
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Expanded Content */}
|
||||
{isExpanded && (
|
||||
<div className="border-t border-white/10 p-5 space-y-4 bg-black/20">
|
||||
{/* Verification Info */}
|
||||
{(leak.submitterName || leak.verifierNames) && (
|
||||
<div className="bg-green-500/10 border border-green-500/20 rounded-lg p-3">
|
||||
{leak.submitterName && (
|
||||
<div className="text-xs text-green-400 mb-1">
|
||||
<span className="font-semibold">Submitted by:</span>{" "}
|
||||
{leak.submitterName}
|
||||
</div>
|
||||
)}
|
||||
{leak.verifierNames && leak.verifierNames.length > 0 && (
|
||||
<div className="text-xs text-green-400">
|
||||
<span className="font-semibold">✓ Verified by:</span>{" "}
|
||||
{leak.verifierNames.join(", ")}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Prompt Text */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-[#00aaff] text-sm font-semibold">
|
||||
System Prompt:
|
||||
</span>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
copyPrompt(leak.leakText, leak._id);
|
||||
}}
|
||||
className="text-xs px-3 py-1.5 rounded-lg bg-[#00aaff]/20 hover:bg-[#00aaff]/30 text-[#00aaff] transition-colors flex items-center gap-1.5"
|
||||
>
|
||||
{copiedLeakId === leak._id ? (
|
||||
<>
|
||||
<span>✓</span>
|
||||
<span>Copied!</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span>📋</span>
|
||||
<span>Copy</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
<div className="bg-black/40 border border-white/5 rounded-lg p-4 max-h-96 overflow-auto">
|
||||
<pre className="text-[#ccc] text-sm whitespace-pre-wrap font-mono break-words">
|
||||
{leak.leakText}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Context */}
|
||||
{leak.leakContext && (
|
||||
<div>
|
||||
<span className="text-[#00aaff] text-sm font-semibold block mb-2">
|
||||
Context:
|
||||
</span>
|
||||
<p className="text-[#aaa] text-sm">{leak.leakContext}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* URL */}
|
||||
{leak.url && (
|
||||
<div>
|
||||
<span className="text-[#00aaff] text-sm font-semibold block mb-2">
|
||||
Source URL:
|
||||
</span>
|
||||
<a
|
||||
href={leak.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-[#33bbff] text-sm hover:underline break-all inline-flex items-center gap-1"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{leak.url}
|
||||
<svg
|
||||
className="w-4 h-4 shrink-0"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Access Notes */}
|
||||
{leak.accessNotes && (
|
||||
<div>
|
||||
<span className="text-[#00aaff] text-sm font-semibold block mb-2">
|
||||
Access Notes:
|
||||
</span>
|
||||
<p className="text-[#aaa] text-sm">{leak.accessNotes}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,405 @@
|
||||
import { useConvexAuth, useMutation, useQuery } from "convex/react";
|
||||
import { api } from "../../convex/_generated/api";
|
||||
import { useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
|
||||
export default function Requests() {
|
||||
const { isAuthenticated } = useConvexAuth();
|
||||
const user = useQuery(api.users.currentUser);
|
||||
const openRequests = useQuery(api.requests.getOpenRequests);
|
||||
const userRequests = useQuery(
|
||||
api.requests.getUserOpenRequests,
|
||||
user ? { userId: user._id } : "skip",
|
||||
);
|
||||
const createRequest = useMutation(api.requests.createRequest);
|
||||
const closeRequest = useMutation(api.requests.closeRequest);
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
targetName: "",
|
||||
provider: "",
|
||||
targetType: "model" as
|
||||
| "model"
|
||||
| "app"
|
||||
| "tool"
|
||||
| "agent"
|
||||
| "plugin"
|
||||
| "custom",
|
||||
targetUrl: "",
|
||||
});
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const userRequestsCount = userRequests?.length || 0;
|
||||
const hasReachedLimit = userRequestsCount >= 3;
|
||||
|
||||
const handleCloseRequest = async (requestId: string) => {
|
||||
try {
|
||||
const result = await closeRequest({ requestId: requestId as any });
|
||||
|
||||
// Check if the mutation returned an error
|
||||
if (!result.success) {
|
||||
setError(result.error);
|
||||
return;
|
||||
}
|
||||
|
||||
// Success - clear any previous errors
|
||||
setError(null);
|
||||
} catch (error: any) {
|
||||
// Fallback for unexpected errors
|
||||
console.error("Error closing request:", error);
|
||||
const errorMessage =
|
||||
error?.data ||
|
||||
error?.message ||
|
||||
"Failed to close request. Please try again.";
|
||||
setError(errorMessage);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError(null);
|
||||
|
||||
if (
|
||||
!formData.targetName ||
|
||||
!formData.provider ||
|
||||
!formData.targetUrl ||
|
||||
!isAuthenticated
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasReachedLimit) {
|
||||
setError(
|
||||
"You have reached the maximum limit of 3 open requests. Please wait for some to be fulfilled before creating more.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
const result = await createRequest(formData);
|
||||
|
||||
// Check if the mutation returned an error
|
||||
if (!result.success) {
|
||||
setError(result.error);
|
||||
return;
|
||||
}
|
||||
|
||||
// Success - reset form and clear any previous errors
|
||||
setError(null);
|
||||
setFormData({
|
||||
targetName: "",
|
||||
provider: "",
|
||||
targetType: "model",
|
||||
targetUrl: "",
|
||||
});
|
||||
} catch (error: any) {
|
||||
// Fallback for unexpected errors
|
||||
const errorMessage =
|
||||
error?.data ||
|
||||
error?.message ||
|
||||
"Failed to create request. Please try again.";
|
||||
setError(errorMessage);
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
{/* Header */}
|
||||
<div className="bg-white/3 border border-white/10 rounded-2xl p-8 backdrop-blur-sm">
|
||||
<h1 className="text-4xl font-bold bg-linear-to-r from-[#00ff88] via-[#00aaff] to-[#ff00ff] bg-clip-text text-transparent mb-2">
|
||||
🎯 System Prompt Requests
|
||||
</h1>
|
||||
<p className="text-[#888] text-lg">
|
||||
Request system prompts for AI models, apps, and tools. The community
|
||||
will help find them!
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Create Request Form - Only visible when logged in */}
|
||||
{isAuthenticated && (
|
||||
<div className="bg-white/3 border border-white/10 rounded-2xl p-6 backdrop-blur-sm">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h2 className="text-2xl font-bold text-[#00aaff]">
|
||||
➕ Make a Request
|
||||
</h2>
|
||||
<div className="text-sm">
|
||||
<span
|
||||
className={hasReachedLimit ? "text-red-400" : "text-[#00ff88]"}
|
||||
>
|
||||
{userRequestsCount}/3
|
||||
</span>
|
||||
<span className="text-[#888] ml-1">open requests</span>
|
||||
</div>
|
||||
</div>
|
||||
{hasReachedLimit && (
|
||||
<div className="bg-red-500/10 border border-red-500/30 rounded-lg p-4 mb-4">
|
||||
<p className="text-red-400 text-sm">
|
||||
⚠️ You have reached the maximum limit of 3 open requests. Please
|
||||
wait for some to be fulfilled before creating more.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{error && (
|
||||
<div className="bg-red-500/10 border border-red-500/30 rounded-lg p-4 mb-4">
|
||||
<p className="text-red-400 text-sm">⚠️ {error}</p>
|
||||
</div>
|
||||
)}
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-[#e0e0e0] mb-2 font-medium">
|
||||
Target Name *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.targetName}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, targetName: e.target.value })
|
||||
}
|
||||
placeholder="e.g., GPT-4, Claude_12oct2025, Gemini2.5"
|
||||
className="w-full bg-white/5 border border-white/20 rounded-lg px-4 py-2 text-[#e0e0e0] placeholder-[#666] focus:outline-none focus:border-[#00ff88]"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-[#e0e0e0] mb-2 font-medium">
|
||||
Provider *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.provider}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, provider: e.target.value })
|
||||
}
|
||||
placeholder="e.g., OpenAI, Anthropic, Google"
|
||||
className="w-full bg-white/5 border border-white/20 rounded-lg px-4 py-2 text-[#e0e0e0] placeholder-[#666] focus:outline-none focus:border-[#00ff88]"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-[#e0e0e0] mb-2 font-medium">
|
||||
Type *
|
||||
</label>
|
||||
<select
|
||||
value={formData.targetType}
|
||||
onChange={(e) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
targetType: e.target.value as
|
||||
| "model"
|
||||
| "app"
|
||||
| "tool"
|
||||
| "agent"
|
||||
| "plugin"
|
||||
| "custom",
|
||||
})
|
||||
}
|
||||
className="w-full bg-white/5 border border-white/20 rounded-lg px-4 py-2 text-[#e0e0e0] focus:outline-none focus:border-[#00ff88]"
|
||||
>
|
||||
<option value="model">Model</option>
|
||||
<option value="app">App</option>
|
||||
<option value="tool">Tool</option>
|
||||
<option value="agent">Agent</option>
|
||||
<option value="plugin">Plugin</option>
|
||||
<option value="custom">Custom</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-[#e0e0e0] mb-2 font-medium">
|
||||
Target URL *
|
||||
</label>
|
||||
<input
|
||||
type="url"
|
||||
value={formData.targetUrl}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, targetUrl: e.target.value })
|
||||
}
|
||||
placeholder="https://..."
|
||||
className="w-full bg-white/5 border border-white/20 rounded-lg px-4 py-2 text-[#e0e0e0] placeholder-[#666] focus:outline-none focus:border-[#00ff88]"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isSubmitting || hasReachedLimit}
|
||||
className="bg-linear-to-r from-[#00ff88] to-[#00aaff] text-black font-bold px-6 py-3 rounded-lg transition-all hover:-translate-y-0.5 hover:shadow-[0_5px_20px_rgba(0,255,136,0.4)] disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{isSubmitting
|
||||
? "Submitting..."
|
||||
: hasReachedLimit
|
||||
? "Limit Reached"
|
||||
: "Submit Request"}
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* User's Open Requests - Only visible when logged in */}
|
||||
{isAuthenticated && user && (
|
||||
<div className="bg-white/3 border border-white/10 rounded-2xl p-6 backdrop-blur-sm">
|
||||
<h2 className="text-2xl font-bold text-[#00aaff] mb-4">
|
||||
📋 Your Open Requests
|
||||
</h2>
|
||||
{!userRequests ? (
|
||||
<div className="flex justify-center py-8">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-[#00ff88]"></div>
|
||||
</div>
|
||||
) : userRequests.length === 0 ? (
|
||||
<p className="text-[#888] text-center py-8">
|
||||
You haven't made any requests yet.
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{userRequests.map((request) => (
|
||||
<div
|
||||
key={request._id}
|
||||
className="bg-white/5 border border-white/10 rounded-xl p-4 hover:border-[#00aaff]/50 transition-all"
|
||||
>
|
||||
<div className="flex justify-between items-start mb-2">
|
||||
<h3 className="text-xl font-bold text-[#00ff88]">
|
||||
{request.targetName}
|
||||
</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="bg-[#00aaff]/20 text-[#00aaff] px-3 py-1 rounded-full text-sm font-medium">
|
||||
{request.targetType}
|
||||
</span>
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-red-400 hover:text-red-300 hover:bg-red-500/10"
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Close Request</DialogTitle>
|
||||
<DialogDescription>
|
||||
Are you sure you want to close this request? This
|
||||
action cannot be undone.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<DialogClose asChild>
|
||||
<Button variant="ghost">Cancel</Button>
|
||||
</DialogClose>
|
||||
<Button
|
||||
onClick={() => handleCloseRequest(request._id)}
|
||||
className="bg-red-500 hover:bg-red-600 text-white"
|
||||
>
|
||||
Close Request
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-[#e0e0e0] mb-2">
|
||||
Provider: {request.provider}
|
||||
</p>
|
||||
<a
|
||||
href={request.targetUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-[#00aaff] hover:underline text-sm break-all"
|
||||
>
|
||||
{request.targetUrl}
|
||||
</a>
|
||||
<p className="text-[#666] text-sm mt-2">
|
||||
Requested on{" "}
|
||||
{new Date(request._creationTime).toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* All Open Requests */}
|
||||
<div className="bg-white/3 border border-white/10 rounded-2xl p-6 backdrop-blur-sm">
|
||||
<h2 className="text-2xl font-bold text-[#00aaff] mb-4">
|
||||
🌍 All Open Requests
|
||||
</h2>
|
||||
{!openRequests ? (
|
||||
<div className="flex justify-center py-8">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-[#00ff88]"></div>
|
||||
</div>
|
||||
) : openRequests.length === 0 ? (
|
||||
<p className="text-[#888] text-center py-8">
|
||||
No open requests yet. Be the first to make one!
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{openRequests.map((request: (typeof openRequests)[number]) => (
|
||||
<div
|
||||
key={request._id}
|
||||
className="bg-white/5 border border-white/10 rounded-xl p-4 hover:border-[#00ff88]/50 transition-all"
|
||||
>
|
||||
<div className="flex justify-between items-start mb-2">
|
||||
<h3 className="text-xl font-bold text-[#00ff88]">
|
||||
{request.targetName}
|
||||
</h3>
|
||||
<span className="bg-[#00aaff]/20 text-[#00aaff] px-3 py-1 rounded-full text-sm font-medium">
|
||||
{request.targetType}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-[#e0e0e0] mb-2">
|
||||
Provider: {request.provider}
|
||||
</p>
|
||||
<a
|
||||
href={request.targetUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-[#00aaff] hover:underline text-sm break-all"
|
||||
>
|
||||
{request.targetUrl}
|
||||
</a>
|
||||
<div className="flex justify-between items-center mt-3 pt-3 border-t border-white/10">
|
||||
<p className="text-[#666] text-sm">
|
||||
Requested by {request.submitterName}
|
||||
</p>
|
||||
<p className="text-[#666] text-sm">
|
||||
{new Date(request._creationTime).toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Call to Action for non-logged in users */}
|
||||
{!isAuthenticated && (
|
||||
<div className="bg-linear-to-r from-[#00ff88]/10 via-[#00aaff]/10 to-[#ff00ff]/10 border-2 border-[#00ff88]/30 rounded-2xl p-8 backdrop-blur-sm text-center">
|
||||
<h3 className="text-2xl font-bold text-[#00ff88] mb-2">
|
||||
Want to make a request?
|
||||
</h3>
|
||||
<p className="text-[#888] mb-4">
|
||||
Sign in with GitHub to request system prompts from the community
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
-2196
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true,
|
||||
|
||||
/* Import paths */
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
],
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2023"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
-75
@@ -1,75 +0,0 @@
|
||||
{
|
||||
"version": 2,
|
||||
"builds": [
|
||||
{
|
||||
"src": "api/serverless.js",
|
||||
"use": "@vercel/node"
|
||||
},
|
||||
{
|
||||
"src": "*.html",
|
||||
"use": "@vercel/static"
|
||||
}
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"src": "/api/(.*)",
|
||||
"dest": "/api/serverless.js"
|
||||
},
|
||||
{
|
||||
"src": "/(.*)",
|
||||
"dest": "/$1"
|
||||
}
|
||||
],
|
||||
"headers": [
|
||||
{
|
||||
"source": "/(.*)",
|
||||
"headers": [
|
||||
{
|
||||
"key": "X-XSS-Protection",
|
||||
"value": "1; mode=block"
|
||||
},
|
||||
{
|
||||
"key": "X-Content-Type-Options",
|
||||
"value": "nosniff"
|
||||
},
|
||||
{
|
||||
"key": "X-Frame-Options",
|
||||
"value": "DENY"
|
||||
},
|
||||
{
|
||||
"key": "Strict-Transport-Security",
|
||||
"value": "max-age=31536000; includeSubDomains; preload"
|
||||
},
|
||||
{
|
||||
"key": "Referrer-Policy",
|
||||
"value": "strict-origin-when-cross-origin"
|
||||
},
|
||||
{
|
||||
"key": "Permissions-Policy",
|
||||
"value": "geolocation=(), microphone=(), camera=()"
|
||||
},
|
||||
{
|
||||
"key": "Content-Security-Policy",
|
||||
"value": "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://www.googletagmanager.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"source": "/api/(.*)",
|
||||
"headers": [
|
||||
{
|
||||
"key": "Access-Control-Allow-Origin",
|
||||
"value": "*"
|
||||
},
|
||||
{
|
||||
"key": "Access-Control-Allow-Methods",
|
||||
"value": "GET, POST, PUT, DELETE, OPTIONS"
|
||||
},
|
||||
{
|
||||
"key": "Access-Control-Allow-Headers",
|
||||
"value": "Content-Type, Authorization"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import { defineConfig } from 'vite'
|
||||
|
||||
export default defineConfig({
|
||||
base: '/LEAKHUB/',
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
assetsDir: 'assets',
|
||||
sourcemap: true,
|
||||
rollupOptions: {
|
||||
input: {
|
||||
main: 'index.html'
|
||||
}
|
||||
}
|
||||
},
|
||||
server: {
|
||||
port: 3000,
|
||||
open: true
|
||||
},
|
||||
preview: {
|
||||
port: 4173,
|
||||
open: true
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,14 @@
|
||||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
import path from "path";
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react(), tailwindcss()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "./src"),
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"$schema": "node_modules/wrangler/config-schema.json",
|
||||
"name": "leakhub",
|
||||
"pages_build_output_dir": "./dist",
|
||||
}
|
||||
Reference in New Issue
Block a user