Spaces:
Configuration error
Configuration error
GitHub Action commited on
Commit ·
bcce530
0
Parent(s):
Automated sync to Hugging Face
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitattributes +3 -0
- .github/workflows/sync_to_hf.yml +33 -0
- .gitignore +49 -0
- .nvmrc +1 -0
- DEPLOYMENT.md +414 -0
- README.md +414 -0
- components.json +22 -0
- eslint.config.mjs +18 -0
- next.config.ts +95 -0
- openprompt-extension/README.md +137 -0
- openprompt-extension/package-lock.json +0 -0
- openprompt-extension/package.json +28 -0
- openprompt-extension/popup.html +12 -0
- openprompt-extension/postcss.config.js +6 -0
- openprompt-extension/public/icon.svg +17 -0
- openprompt-extension/public/icons/icon128.png +3 -0
- openprompt-extension/public/icons/icon16.png +3 -0
- openprompt-extension/public/icons/icon48.png +3 -0
- openprompt-extension/public/icons/logo.svg +53 -0
- openprompt-extension/public/manifest.json +150 -0
- openprompt-extension/public/small-promo-tile.png +3 -0
- openprompt-extension/scripts/create-icons.js +27 -0
- openprompt-extension/scripts/generate-icons.js +36 -0
- openprompt-extension/src/background/index.ts +450 -0
- openprompt-extension/src/content-scripts/chatgpt.ts +87 -0
- openprompt-extension/src/content-scripts/claude.ts +83 -0
- openprompt-extension/src/content-scripts/copilot.ts +72 -0
- openprompt-extension/src/content-scripts/gemini.ts +87 -0
- openprompt-extension/src/content-scripts/mistral.ts +83 -0
- openprompt-extension/src/content-scripts/openprompt-site.ts +120 -0
- openprompt-extension/src/content-scripts/perplexity.ts +83 -0
- openprompt-extension/src/content-scripts/utils.ts +258 -0
- openprompt-extension/src/popup/App.tsx +743 -0
- openprompt-extension/src/popup/index.css +34 -0
- openprompt-extension/src/popup/main.tsx +10 -0
- openprompt-extension/tailwind.config.js +26 -0
- openprompt-extension/tsconfig.json +21 -0
- openprompt-extension/vite.config.ts +63 -0
- package-lock.json +0 -0
- package.json +81 -0
- postcss.config.mjs +7 -0
- prisma.config.ts +14 -0
- prisma/schema.prisma +760 -0
- prisma/seed-more.ts +128 -0
- prisma/seed.ts +644 -0
- prisma/tool-models.txt +43 -0
- prisma/update-models.ts +34 -0
- public/favicon.ico +3 -0
- public/file.svg +1 -0
- public/globe.svg +1 -0
.gitattributes
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
*.png filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
*.jpg filter=lfs diff=lfs merge=lfs -text
|
| 3 |
+
*.ico filter=lfs diff=lfs merge=lfs -text
|
.github/workflows/sync_to_hf.yml
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: Sync to Hugging Face
|
| 2 |
+
on:
|
| 3 |
+
push:
|
| 4 |
+
branches: [master]
|
| 5 |
+
|
| 6 |
+
jobs:
|
| 7 |
+
sync-to-hub:
|
| 8 |
+
runs-on: ubuntu-latest
|
| 9 |
+
steps:
|
| 10 |
+
- uses: actions/checkout@v3
|
| 11 |
+
- name: Push to hub
|
| 12 |
+
env:
|
| 13 |
+
HF_TOKEN: ${{ secrets.HF_TOKEN }}
|
| 14 |
+
run: |
|
| 15 |
+
# 1. Turn on Large File Storage (LFS)
|
| 16 |
+
git lfs install
|
| 17 |
+
|
| 18 |
+
# 2. Tell Hugging Face to properly handle images
|
| 19 |
+
echo "*.png filter=lfs diff=lfs merge=lfs -text" >> .gitattributes
|
| 20 |
+
echo "*.jpg filter=lfs diff=lfs merge=lfs -text" >> .gitattributes
|
| 21 |
+
echo "*.ico filter=lfs diff=lfs merge=lfs -text" >> .gitattributes
|
| 22 |
+
|
| 23 |
+
# 3. Wipe the hidden git history in the runner to clear past image errors
|
| 24 |
+
rm -rf .git
|
| 25 |
+
git config --global user.email "action@github.com"
|
| 26 |
+
git config --global user.name "GitHub Action"
|
| 27 |
+
|
| 28 |
+
# 4. Create a fresh, clean package and force push it
|
| 29 |
+
git init
|
| 30 |
+
git checkout -b main
|
| 31 |
+
git add .
|
| 32 |
+
git commit -m "Automated sync to Hugging Face"
|
| 33 |
+
git push --force https://anky2002:$HF_TOKEN@huggingface.co/spaces/anky2002/open-prompt main
|
.gitignore
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
| 2 |
+
|
| 3 |
+
# dependencies
|
| 4 |
+
/node_modules
|
| 5 |
+
/.pnp
|
| 6 |
+
.pnp.*
|
| 7 |
+
.yarn/*
|
| 8 |
+
!.yarn/patches
|
| 9 |
+
!.yarn/plugins
|
| 10 |
+
!.yarn/releases
|
| 11 |
+
!.yarn/versions
|
| 12 |
+
|
| 13 |
+
# testing
|
| 14 |
+
/coverage
|
| 15 |
+
|
| 16 |
+
# next.js
|
| 17 |
+
/.next/
|
| 18 |
+
/out/
|
| 19 |
+
|
| 20 |
+
# production
|
| 21 |
+
/build
|
| 22 |
+
|
| 23 |
+
# misc
|
| 24 |
+
.DS_Store
|
| 25 |
+
*.pem
|
| 26 |
+
|
| 27 |
+
# debug
|
| 28 |
+
npm-debug.log*
|
| 29 |
+
yarn-debug.log*
|
| 30 |
+
yarn-error.log*
|
| 31 |
+
.pnpm-debug.log*
|
| 32 |
+
|
| 33 |
+
# env files (can opt-in for committing if needed)
|
| 34 |
+
.env*
|
| 35 |
+
|
| 36 |
+
# vercel
|
| 37 |
+
.vercel
|
| 38 |
+
|
| 39 |
+
# typescript
|
| 40 |
+
*.tsbuildinfo
|
| 41 |
+
next-env.d.ts
|
| 42 |
+
|
| 43 |
+
/src/generated/prisma
|
| 44 |
+
# openprompt extension build output
|
| 45 |
+
/openprompt-extension/dist
|
| 46 |
+
/openprompt-extension/build
|
| 47 |
+
/openprompt-extension/node_modules
|
| 48 |
+
goal.md
|
| 49 |
+
features.md
|
.nvmrc
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
20
|
DEPLOYMENT.md
ADDED
|
@@ -0,0 +1,414 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🚀 OpenPrompt - Quick Deployment Guide
|
| 2 |
+
|
| 3 |
+
**From Zero to Live in 10 Minutes**
|
| 4 |
+
|
| 5 |
+
---
|
| 6 |
+
|
| 7 |
+
## Prerequisites
|
| 8 |
+
|
| 9 |
+
- ✅ Node.js 18+ installed
|
| 10 |
+
- ✅ Git installed
|
| 11 |
+
- ✅ Vercel account (free)
|
| 12 |
+
- ✅ OpenAI API key
|
| 13 |
+
|
| 14 |
+
---
|
| 15 |
+
|
| 16 |
+
## Step 1: Environment Variables (2 minutes)
|
| 17 |
+
|
| 18 |
+
Create `.env.local` file in the root directory:
|
| 19 |
+
|
| 20 |
+
```env
|
| 21 |
+
# Database (already configured)
|
| 22 |
+
DATABASE_URL="your-neon-postgres-url"
|
| 23 |
+
|
| 24 |
+
# Authentication (Stack Auth - already configured)
|
| 25 |
+
NEXT_PUBLIC_STACK_PROJECT_ID="your-stack-project-id"
|
| 26 |
+
NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY="your-stack-key"
|
| 27 |
+
STACK_SECRET_SERVER_KEY="your-stack-secret"
|
| 28 |
+
|
| 29 |
+
# AI Models (REQUIRED for tools to work)
|
| 30 |
+
OPENAI_API_KEY="sk-proj-..."
|
| 31 |
+
|
| 32 |
+
# Optional but recommended
|
| 33 |
+
ANTHROPIC_API_KEY="sk-ant-..."
|
| 34 |
+
GOOGLE_AI_API_KEY="..."
|
| 35 |
+
|
| 36 |
+
# Redis Cache (Optional - improves performance)
|
| 37 |
+
UPSTASH_REDIS_REST_URL="https://..."
|
| 38 |
+
UPSTASH_REDIS_REST_TOKEN="..."
|
| 39 |
+
|
| 40 |
+
# Bot Protection (Optional)
|
| 41 |
+
NEXT_PUBLIC_TURNSTILE_SITE_KEY="..."
|
| 42 |
+
TURNSTILE_SECRET_KEY="..."
|
| 43 |
+
```
|
| 44 |
+
|
| 45 |
+
### Get API Keys:
|
| 46 |
+
|
| 47 |
+
**OpenAI (Required):**
|
| 48 |
+
1. Go to https://platform.openai.com/api-keys
|
| 49 |
+
2. Create new secret key
|
| 50 |
+
3. Copy to `OPENAI_API_KEY`
|
| 51 |
+
|
| 52 |
+
**Upstash Redis (Recommended):**
|
| 53 |
+
1. Go to https://upstash.com
|
| 54 |
+
2. Create new database
|
| 55 |
+
3. Copy REST URL and Token
|
| 56 |
+
|
| 57 |
+
---
|
| 58 |
+
|
| 59 |
+
## Step 2: Install & Build (3 minutes)
|
| 60 |
+
|
| 61 |
+
```bash
|
| 62 |
+
# Install dependencies
|
| 63 |
+
npm install
|
| 64 |
+
|
| 65 |
+
# Generate Prisma client
|
| 66 |
+
npx prisma generate
|
| 67 |
+
|
| 68 |
+
# Push database schema
|
| 69 |
+
npx prisma db push
|
| 70 |
+
|
| 71 |
+
# Optional: Seed with sample data
|
| 72 |
+
npx prisma db seed
|
| 73 |
+
|
| 74 |
+
# Build for production
|
| 75 |
+
npm run build
|
| 76 |
+
|
| 77 |
+
# Test locally
|
| 78 |
+
npm start
|
| 79 |
+
```
|
| 80 |
+
|
| 81 |
+
Visit http://localhost:3000 to verify everything works.
|
| 82 |
+
|
| 83 |
+
---
|
| 84 |
+
|
| 85 |
+
## Step 3: Deploy to Vercel (5 minutes)
|
| 86 |
+
|
| 87 |
+
### Option A: Vercel CLI (Fastest)
|
| 88 |
+
|
| 89 |
+
```bash
|
| 90 |
+
# Install Vercel CLI
|
| 91 |
+
npm i -g vercel
|
| 92 |
+
|
| 93 |
+
# Login
|
| 94 |
+
vercel login
|
| 95 |
+
|
| 96 |
+
# Deploy
|
| 97 |
+
vercel --prod
|
| 98 |
+
```
|
| 99 |
+
|
| 100 |
+
### Option B: Vercel Dashboard
|
| 101 |
+
|
| 102 |
+
1. Go to https://vercel.com/new
|
| 103 |
+
2. Import your Git repository
|
| 104 |
+
3. Add environment variables in dashboard
|
| 105 |
+
4. Click "Deploy"
|
| 106 |
+
|
| 107 |
+
**Environment Variables to Add in Vercel:**
|
| 108 |
+
- `DATABASE_URL`
|
| 109 |
+
- `NEXT_PUBLIC_STACK_PROJECT_ID`
|
| 110 |
+
- `NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY`
|
| 111 |
+
- `STACK_SECRET_SERVER_KEY`
|
| 112 |
+
- `OPENAI_API_KEY`
|
| 113 |
+
- `UPSTASH_REDIS_REST_URL` (if using)
|
| 114 |
+
- `UPSTASH_REDIS_REST_TOKEN` (if using)
|
| 115 |
+
|
| 116 |
+
---
|
| 117 |
+
|
| 118 |
+
## Step 4: Post-Deployment Checks (2 minutes)
|
| 119 |
+
|
| 120 |
+
Visit your live site and verify:
|
| 121 |
+
|
| 122 |
+
**Basic Functionality:**
|
| 123 |
+
- [ ] Homepage loads
|
| 124 |
+
- [ ] Can browse `/explore`
|
| 125 |
+
- [ ] Can view `/tools`
|
| 126 |
+
- [ ] Can sign in/sign up
|
| 127 |
+
- [ ] Dark mode toggle works
|
| 128 |
+
|
| 129 |
+
**Critical Features:**
|
| 130 |
+
- [ ] Can run a prompt at `/p/[slug]`
|
| 131 |
+
- [ ] AI response streams correctly
|
| 132 |
+
- [ ] Can execute a tool at `/tools/[slug]`
|
| 133 |
+
- [ ] Star/unstar works
|
| 134 |
+
- [ ] Search works
|
| 135 |
+
|
| 136 |
+
**Performance:**
|
| 137 |
+
- [ ] Page load < 3 seconds
|
| 138 |
+
- [ ] No console errors
|
| 139 |
+
- [ ] Images load properly
|
| 140 |
+
- [ ] Mobile responsive
|
| 141 |
+
|
| 142 |
+
---
|
| 143 |
+
|
| 144 |
+
## Step 5: Domain Setup (Optional)
|
| 145 |
+
|
| 146 |
+
**Add Custom Domain in Vercel:**
|
| 147 |
+
|
| 148 |
+
1. Go to your project settings
|
| 149 |
+
2. Click "Domains"
|
| 150 |
+
3. Add your domain (e.g., https://open-prompt.netlify.app)
|
| 151 |
+
4. Update DNS records as instructed
|
| 152 |
+
5. Wait for SSL certificate (automatic)
|
| 153 |
+
|
| 154 |
+
---
|
| 155 |
+
|
| 156 |
+
## Troubleshooting
|
| 157 |
+
|
| 158 |
+
### Build Errors
|
| 159 |
+
|
| 160 |
+
**Error:** "Module not found"
|
| 161 |
+
```bash
|
| 162 |
+
# Clear cache and reinstall
|
| 163 |
+
rm -rf node_modules .next
|
| 164 |
+
npm install
|
| 165 |
+
npm run build
|
| 166 |
+
```
|
| 167 |
+
|
| 168 |
+
**Error:** "Prisma Client not generated"
|
| 169 |
+
```bash
|
| 170 |
+
npx prisma generate
|
| 171 |
+
npm run build
|
| 172 |
+
```
|
| 173 |
+
|
| 174 |
+
### Runtime Errors
|
| 175 |
+
|
| 176 |
+
**Error:** "OpenAI API key not found"
|
| 177 |
+
- Check `OPENAI_API_KEY` is set in Vercel environment variables
|
| 178 |
+
- Redeploy after adding env vars
|
| 179 |
+
|
| 180 |
+
**Error:** "Database connection failed"
|
| 181 |
+
- Verify `DATABASE_URL` is correct
|
| 182 |
+
- Check if Neon database is active
|
| 183 |
+
- Ensure IP allowlist includes Vercel IPs
|
| 184 |
+
|
| 185 |
+
**Error:** "Rate limit exceeded"
|
| 186 |
+
- Redis not configured (optional but recommended)
|
| 187 |
+
- Add Upstash Redis credentials
|
| 188 |
+
- Or increase rate limits in `lib/rate-limit.ts`
|
| 189 |
+
|
| 190 |
+
### Performance Issues
|
| 191 |
+
|
| 192 |
+
**Slow page loads:**
|
| 193 |
+
- Enable Redis caching
|
| 194 |
+
- Optimize images (use Next.js Image component)
|
| 195 |
+
- Check database query performance
|
| 196 |
+
|
| 197 |
+
**AI tools timeout:**
|
| 198 |
+
- Increase timeout in tool execution API
|
| 199 |
+
- Use streaming for long responses
|
| 200 |
+
- Consider using GPT-4o-mini for faster responses
|
| 201 |
+
|
| 202 |
+
---
|
| 203 |
+
|
| 204 |
+
## Monitoring & Analytics
|
| 205 |
+
|
| 206 |
+
### Add Vercel Analytics (Free)
|
| 207 |
+
|
| 208 |
+
1. Go to Vercel project settings
|
| 209 |
+
2. Enable "Analytics" tab
|
| 210 |
+
3. Automatically tracks:
|
| 211 |
+
- Page views
|
| 212 |
+
- Performance metrics
|
| 213 |
+
- User engagement
|
| 214 |
+
|
| 215 |
+
### Add Vercel Speed Insights
|
| 216 |
+
|
| 217 |
+
```bash
|
| 218 |
+
npm install @vercel/speed-insights
|
| 219 |
+
```
|
| 220 |
+
|
| 221 |
+
In `app/layout.tsx`:
|
| 222 |
+
```typescript
|
| 223 |
+
import { SpeedInsights } from '@vercel/speed-insights/next'
|
| 224 |
+
|
| 225 |
+
export default function RootLayout({ children }) {
|
| 226 |
+
return (
|
| 227 |
+
<html>
|
| 228 |
+
<body>
|
| 229 |
+
{children}
|
| 230 |
+
<SpeedInsights />
|
| 231 |
+
</body>
|
| 232 |
+
</html>
|
| 233 |
+
)
|
| 234 |
+
}
|
| 235 |
+
```
|
| 236 |
+
|
| 237 |
+
### Optional: Add PostHog (Product Analytics)
|
| 238 |
+
|
| 239 |
+
1. Sign up at https://posthog.com
|
| 240 |
+
2. Add tracking code to layout
|
| 241 |
+
3. Track:
|
| 242 |
+
- Tool usage
|
| 243 |
+
- Prompt runs
|
| 244 |
+
- User journeys
|
| 245 |
+
- Conversions
|
| 246 |
+
|
| 247 |
+
---
|
| 248 |
+
|
| 249 |
+
## Scaling Considerations
|
| 250 |
+
|
| 251 |
+
### When to Upgrade
|
| 252 |
+
|
| 253 |
+
**Database:**
|
| 254 |
+
- Free tier: ~10GB storage, 1GB bandwidth
|
| 255 |
+
- Upgrade when: >5,000 users or >100,000 prompts
|
| 256 |
+
|
| 257 |
+
**Redis:**
|
| 258 |
+
- Free tier: 10,000 commands/day
|
| 259 |
+
- Upgrade when: >1,000 daily active users
|
| 260 |
+
|
| 261 |
+
**AI APIs:**
|
| 262 |
+
- Monitor usage in OpenAI dashboard
|
| 263 |
+
- Set usage limits to prevent surprise bills
|
| 264 |
+
- Consider caching common responses
|
| 265 |
+
|
| 266 |
+
### Cost Estimates
|
| 267 |
+
|
| 268 |
+
**MVP (0-1,000 users):**
|
| 269 |
+
- Vercel: Free
|
| 270 |
+
- Neon DB: Free
|
| 271 |
+
- Redis: Free
|
| 272 |
+
- OpenAI: ~$50-100/month
|
| 273 |
+
- **Total: ~$50-100/month**
|
| 274 |
+
|
| 275 |
+
**Growth (1,000-10,000 users):**
|
| 276 |
+
- Vercel: $20/month
|
| 277 |
+
- Neon DB: $19/month
|
| 278 |
+
- Redis: $10/month
|
| 279 |
+
- OpenAI: ~$300-500/month
|
| 280 |
+
- **Total: ~$350-550/month**
|
| 281 |
+
|
| 282 |
+
**Scale (10,000+ users):**
|
| 283 |
+
- Vercel: $20-50/month
|
| 284 |
+
- Neon DB: $50+/month
|
| 285 |
+
- Redis: $50+/month
|
| 286 |
+
- OpenAI: ~$1,000+/month
|
| 287 |
+
- **Total: ~$1,120+/month**
|
| 288 |
+
|
| 289 |
+
---
|
| 290 |
+
|
| 291 |
+
## Production Checklist
|
| 292 |
+
|
| 293 |
+
### Security
|
| 294 |
+
- [ ] Environment variables secured
|
| 295 |
+
- [ ] API keys rotated regularly
|
| 296 |
+
- [ ] Rate limiting enabled
|
| 297 |
+
- [ ] CORS configured
|
| 298 |
+
- [ ] Input validation on all forms
|
| 299 |
+
- [ ] XSS protection enabled
|
| 300 |
+
|
| 301 |
+
### Performance
|
| 302 |
+
- [ ] Redis caching enabled
|
| 303 |
+
- [ ] Images optimized
|
| 304 |
+
- [ ] Code split by route
|
| 305 |
+
- [ ] Lazy loading implemented
|
| 306 |
+
- [ ] CDN configured (automatic with Vercel)
|
| 307 |
+
|
| 308 |
+
### Monitoring
|
| 309 |
+
- [ ] Error tracking (Sentry recommended)
|
| 310 |
+
- [ ] Analytics enabled
|
| 311 |
+
- [ ] Uptime monitoring
|
| 312 |
+
- [ ] Database backups configured
|
| 313 |
+
- [ ] Logs accessible
|
| 314 |
+
|
| 315 |
+
### SEO
|
| 316 |
+
- [ ] Meta tags complete
|
| 317 |
+
- [ ] Sitemap generated
|
| 318 |
+
- [ ] robots.txt configured
|
| 319 |
+
- [ ] Open Graph images
|
| 320 |
+
- [ ] Schema markup added
|
| 321 |
+
|
| 322 |
+
### Legal
|
| 323 |
+
- [ ] Privacy policy published
|
| 324 |
+
- [ ] Terms of service published
|
| 325 |
+
- [ ] Cookie consent (if EU users)
|
| 326 |
+
- [ ] GDPR compliance (if EU users)
|
| 327 |
+
- [ ] CCPA compliance (if CA users)
|
| 328 |
+
|
| 329 |
+
---
|
| 330 |
+
|
| 331 |
+
## Launch Day Actions
|
| 332 |
+
|
| 333 |
+
**Hour 1: Deploy**
|
| 334 |
+
- [ ] Final build successful
|
| 335 |
+
- [ ] All environment variables set
|
| 336 |
+
- [ ] Domain connected
|
| 337 |
+
- [ ] SSL active
|
| 338 |
+
|
| 339 |
+
**Hour 2: Verify**
|
| 340 |
+
- [ ] Test all critical paths
|
| 341 |
+
- [ ] Check mobile experience
|
| 342 |
+
- [ ] Verify AI tools work
|
| 343 |
+
- [ ] Test authentication flow
|
| 344 |
+
|
| 345 |
+
**Hour 3: Announce**
|
| 346 |
+
- [ ] Post on social media
|
| 347 |
+
- [ ] Email early users
|
| 348 |
+
- [ ] Submit to Product Hunt
|
| 349 |
+
- [ ] Share in communities
|
| 350 |
+
|
| 351 |
+
---
|
| 352 |
+
|
| 353 |
+
## Support Resources
|
| 354 |
+
|
| 355 |
+
**Documentation:**
|
| 356 |
+
- Next.js: https://nextjs.org/docs
|
| 357 |
+
- Prisma: https://www.prisma.io/docs
|
| 358 |
+
- Vercel: https://vercel.com/docs
|
| 359 |
+
- Stack Auth: https://docs.stack-auth.com
|
| 360 |
+
|
| 361 |
+
**Community:**
|
| 362 |
+
- Discord servers for each stack
|
| 363 |
+
- GitHub discussions
|
| 364 |
+
- Stack Overflow
|
| 365 |
+
|
| 366 |
+
**Paid Support:**
|
| 367 |
+
- Vercel Enterprise support
|
| 368 |
+
- Database managed services
|
| 369 |
+
- Development agencies
|
| 370 |
+
|
| 371 |
+
---
|
| 372 |
+
|
| 373 |
+
## Quick Commands Reference
|
| 374 |
+
|
| 375 |
+
```bash
|
| 376 |
+
# Development
|
| 377 |
+
npm run dev # Start dev server
|
| 378 |
+
npm run build # Build for production
|
| 379 |
+
npm start # Run production build
|
| 380 |
+
npm run lint # Run ESLint
|
| 381 |
+
|
| 382 |
+
# Database
|
| 383 |
+
npx prisma studio # Open database GUI
|
| 384 |
+
npx prisma db push # Push schema to database
|
| 385 |
+
npx prisma db seed # Seed database
|
| 386 |
+
npx prisma generate # Generate Prisma client
|
| 387 |
+
npx prisma migrate dev # Create migration
|
| 388 |
+
|
| 389 |
+
# Deployment
|
| 390 |
+
vercel # Deploy to preview
|
| 391 |
+
vercel --prod # Deploy to production
|
| 392 |
+
vercel env pull # Pull env variables locally
|
| 393 |
+
vercel logs # View deployment logs
|
| 394 |
+
```
|
| 395 |
+
|
| 396 |
+
---
|
| 397 |
+
|
| 398 |
+
## Success! 🎉
|
| 399 |
+
|
| 400 |
+
Your OpenPrompt platform is now LIVE and ready to:
|
| 401 |
+
- Serve users
|
| 402 |
+
- Generate revenue
|
| 403 |
+
- Scale infinitely
|
| 404 |
+
- Change the world
|
| 405 |
+
|
| 406 |
+
**Next:** Monitor usage, gather feedback, iterate!
|
| 407 |
+
|
| 408 |
+
---
|
| 409 |
+
|
| 410 |
+
**Need Help?** Check the documentation or open an issue on GitHub.
|
| 411 |
+
|
| 412 |
+
**Ready to Scale?** Consider the growth roadmap in `final_summary.md`.
|
| 413 |
+
|
| 414 |
+
**Want More Features?** See `implementation_plan.md` for adding more tools.
|
README.md
ADDED
|
@@ -0,0 +1,414 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🚀 OpenPrompt - AI Prompts Marketplace + Tools Suite
|
| 2 |
+
|
| 3 |
+
> **Production-ready platform combining a prompts marketplace with 177+ AI-powered tools, browser extension, and multi-model support**
|
| 4 |
+
|
| 5 |
+
[](https://nextjs.org/)
|
| 6 |
+
[](https://www.typescriptlang.org/)
|
| 7 |
+
[](https://www.prisma.io/)
|
| 8 |
+
[](https://react.dev/)
|
| 9 |
+
|
| 10 |
+
**Live Demo:** [https://open-prompt.netlify.app](https://open-prompt.netlify.app)
|
| 11 |
+
|
| 12 |
+
---
|
| 13 |
+
|
| 14 |
+
## 📖 Table of Contents
|
| 15 |
+
|
| 16 |
+
- [Features](#-features)
|
| 17 |
+
- [Tech Stack](#-tech-stack)
|
| 18 |
+
- [Getting Started](#-getting-started)
|
| 19 |
+
- [Project Structure](#-project-structure)
|
| 20 |
+
- [Browser Extension](#-browser-extension)
|
| 21 |
+
- [Available Tools](#-available-tools)
|
| 22 |
+
- [Deployment](#-deployment)
|
| 23 |
+
- [Documentation](#-documentation)
|
| 24 |
+
- [Support](#-support)
|
| 25 |
+
|
| 26 |
+
---
|
| 27 |
+
|
| 28 |
+
## ✨ Features
|
| 29 |
+
|
| 30 |
+
### Prompts Marketplace
|
| 31 |
+
|
| 32 |
+
- 🔍 **Advanced Search** - Full-text search with real-time filtering
|
| 33 |
+
- 📊 **Trending Algorithm** - Hot score calculation for viral prompts
|
| 34 |
+
- 🏷️ **7 Categories** - Content, Development, Marketing, Business, Education, Creative, Research
|
| 35 |
+
- ⭐ **User Engagement** - Star, share, remix, and collect prompts
|
| 36 |
+
- 👥 **Creator Economy** - Profile pages, rankings (Bronze/Silver/Gold/Verified), stats
|
| 37 |
+
- 🎨 **10 Frameworks** - RACE, CARE, APE, CREATE, RISEN, RTF, TAG, BAB, STAR, PREP
|
| 38 |
+
- 🏅 **Quality Badges** - Auto-calculated (Hot, Viral, Featured, Top Rated, etc.)
|
| 39 |
+
- 📦 **Collections** - Organize and share prompt collections
|
| 40 |
+
- 🔗 **Embeds** - Share prompts with 3 theme options
|
| 41 |
+
- 💬 **Comments** - Threaded discussions with likes and replies
|
| 42 |
+
- 🌓 **Dark Mode** - Beautiful light and dark themes
|
| 43 |
+
|
| 44 |
+
### AI Tools Suite (177 Tools Across 15 Categories)
|
| 45 |
+
|
| 46 |
+
- 🎯 **Prompting** (9) - Optimizer, Checker, Chain-of-Thought, Few-Shot, Meta-Prompt, etc.
|
| 47 |
+
- 📢 **Marketing** (14) - Strategy, Sales Copy, Hooks, Ads, Campaigns
|
| 48 |
+
- 🏢 **Branding** (11) - Business Names, Slogans, USP, Brand Voice
|
| 49 |
+
- ✍️ **Copywriting** (12) - Titles, Headlines, Meta, Landing Pages
|
| 50 |
+
- 💼 **Business** (15) - Plans, Mission, SWOT, Pitches, Financial Models
|
| 51 |
+
- 📧 **Email** (10) - Subject Lines, Cold Email, Sequences, Templates
|
| 52 |
+
- 📦 **Product** (12) - Descriptions, PRDs, User Stories, Roadmaps
|
| 53 |
+
- 💼 **HR** (11) - Job Descriptions, Interviews, Reviews, Onboarding
|
| 54 |
+
- ⭐ **Personal Brand** (8) - LinkedIn, Twitter, Instagram, Bio
|
| 55 |
+
- 📋 **Operations** (9) - SOPs, KPIs, Process Improvement
|
| 56 |
+
- 📱 **Social Media** (12) - Posts, Captions, Hashtags, Calendars
|
| 57 |
+
- 🎓 **Education** (10) - Lesson Plans, Quizzes, Curriculum
|
| 58 |
+
- 💻 **Development** (15) - Code Review, Documentation, APIs
|
| 59 |
+
- 🎨 **Creative** (14) - Stories, Scripts, Poetry, Art Prompts
|
| 60 |
+
- 🔬 **Research** (15) - Analysis, Summaries, Literature Review
|
| 61 |
+
|
| 62 |
+
### Image Prompts Gallery
|
| 63 |
+
- 🎨 **Midjourney** - v6.1, v6, v5.2, niji 6
|
| 64 |
+
- 🖼️ **DALL-E** - DALL-E 3, DALL-E 2
|
| 65 |
+
- 🎯 **Stable Diffusion** - SDXL, SD 3.5, SD 1.5
|
| 66 |
+
- ⚡ **FLUX** - FLUX.1-dev, schnell, pro
|
| 67 |
+
- 🦁 **Leonardo AI** - Phoenix, Kino XL, Vision XL
|
| 68 |
+
- 🔥 **Adobe Firefly** - Firefly 3, Firefly 2
|
| 69 |
+
|
| 70 |
+
### AI Characters
|
| 71 |
+
- 🤖 **Experts** - Coding, Writing, Business mentors
|
| 72 |
+
- 🎭 **Roleplay** - Storytelling, Adventure companions
|
| 73 |
+
- 🎓 **Education** - Tutors, Study buddies
|
| 74 |
+
- 💼 **Productivity** - Task assistants, Planners
|
| 75 |
+
- 🎨 **Creative** - Art directors, Music composers
|
| 76 |
+
|
| 77 |
+
### Thunderdome ⚔️
|
| 78 |
+
- **Model vs Model** - Compare AI responses head-to-head
|
| 79 |
+
- **Community Voting** - Real-time leaderboard
|
| 80 |
+
- **20+ Models** - GPT-4o, Claude 3.5, Gemini 2.5, Ollama models
|
| 81 |
+
|
| 82 |
+
### Workflow Chains 🔗
|
| 83 |
+
- **Multi-step Prompts** - Chain prompts together
|
| 84 |
+
- **Variable Passing** - Output → Input automation
|
| 85 |
+
- **Templates** - Pre-built workflow patterns
|
| 86 |
+
|
| 87 |
+
### Infrastructure
|
| 88 |
+
|
| 89 |
+
- ⚡ **Multi-Model AI** - OpenAI, Anthropic, Google AI, Ollama (20+ local models)
|
| 90 |
+
- 🔄 **Response Caching** - Redis with 7-day TTL
|
| 91 |
+
- 🛡️ **Rate Limiting** - 10/hr guests, 50/hr users
|
| 92 |
+
- 🤖 **Bot Protection** - Cloudflare Turnstile
|
| 93 |
+
- 📊 **Analytics** - Usage tracking, engagement metrics
|
| 94 |
+
- 🔐 **Authentication** - Stack Auth integration
|
| 95 |
+
- 📱 **Mobile Responsive** - Works on all devices
|
| 96 |
+
- 🧩 **Browser Extension** - Open prompts in any AI chat
|
| 97 |
+
|
| 98 |
+
---
|
| 99 |
+
|
| 100 |
+
## 🛠️ Tech Stack
|
| 101 |
+
|
| 102 |
+
**Frontend:**
|
| 103 |
+
- [Next.js 16.0.8](https://nextjs.org/) - React framework with Turbopack
|
| 104 |
+
- [React 19.2.1](https://react.dev/) - UI library
|
| 105 |
+
- [TypeScript 5.8](https://www.typescriptlang.org/) - Type safety
|
| 106 |
+
- [Tailwind CSS 4.x](https://tailwindcss.com/) - Styling
|
| 107 |
+
- [Shadcn/ui](https://ui.shadcn.com/) - UI components
|
| 108 |
+
- [Framer Motion](https://www.framer.com/motion/) - Animations
|
| 109 |
+
|
| 110 |
+
**Backend:**
|
| 111 |
+
- [Next.js API Routes](https://nextjs.org/docs/api-routes/introduction) - Serverless functions
|
| 112 |
+
- [Prisma 7.1](https://www.prisma.io/) - Database ORM
|
| 113 |
+
- [PostgreSQL](https://www.postgresql.org/) - Database (via Neon)
|
| 114 |
+
- [Redis](https://redis.io/) - Caching (via Upstash)
|
| 115 |
+
|
| 116 |
+
**AI & Services:**
|
| 117 |
+
- [OpenAI API](https://platform.openai.com/) - GPT-4o, GPT-4o-mini
|
| 118 |
+
- [Anthropic API](https://www.anthropic.com/) - Claude 3.5 Sonnet, Claude 3 Haiku
|
| 119 |
+
- [Google AI](https://ai.google.dev/) - Gemini 2.5 Flash, Gemini 2.0 Flash
|
| 120 |
+
- [Ollama](https://ollama.ai/) - 20+ local models (Llama, Mistral, Phi, etc.)
|
| 121 |
+
- [Stack Auth](https://stack-auth.com/) - Authentication
|
| 122 |
+
- [Cloudflare Turnstile](https://www.cloudflare.com/products/turnstile/) - Bot protection
|
| 123 |
+
|
| 124 |
+
**Deployment:**
|
| 125 |
+
- [Vercel](https://vercel.com/) - Hosting & deployments
|
| 126 |
+
- [GitHub](https://github.com/) - Version control
|
| 127 |
+
|
| 128 |
+
---
|
| 129 |
+
|
| 130 |
+
## 🚀 Getting Started
|
| 131 |
+
|
| 132 |
+
### Prerequisites
|
| 133 |
+
|
| 134 |
+
- Node.js 18+
|
| 135 |
+
- npm or yarn
|
| 136 |
+
- PostgreSQL database (or Neon account)
|
| 137 |
+
- OpenAI API key
|
| 138 |
+
|
| 139 |
+
### Installation
|
| 140 |
+
|
| 141 |
+
1. **Clone the repository**
|
| 142 |
+
|
| 143 |
+
```bash
|
| 144 |
+
git clone https://github.com/Anky9972/open-prompt.git
|
| 145 |
+
cd open-prompt
|
| 146 |
+
```
|
| 147 |
+
|
| 148 |
+
2. **Install dependencies**
|
| 149 |
+
|
| 150 |
+
```bash
|
| 151 |
+
npm install
|
| 152 |
+
```
|
| 153 |
+
|
| 154 |
+
3. **Set up environment variables**
|
| 155 |
+
|
| 156 |
+
Create `.env.local`:
|
| 157 |
+
|
| 158 |
+
```env
|
| 159 |
+
# Database
|
| 160 |
+
DATABASE_URL="postgresql://..."
|
| 161 |
+
|
| 162 |
+
# Authentication (Stack Auth)
|
| 163 |
+
NEXT_PUBLIC_STACK_PROJECT_ID="..."
|
| 164 |
+
NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY="..."
|
| 165 |
+
STACK_SECRET_SERVER_KEY="..."
|
| 166 |
+
|
| 167 |
+
# AI Models (Required)
|
| 168 |
+
OPENAI_API_KEY="sk-..."
|
| 169 |
+
|
| 170 |
+
# Optional
|
| 171 |
+
ANTHROPIC_API_KEY="sk-ant-..."
|
| 172 |
+
GOOGLE_AI_API_KEY="..."
|
| 173 |
+
UPSTASH_REDIS_REST_URL="..."
|
| 174 |
+
UPSTASH_REDIS_REST_TOKEN="..."
|
| 175 |
+
NEXT_PUBLIC_TURNSTILE_SITE_KEY="..."
|
| 176 |
+
TURNSTILE_SECRET_KEY="..."
|
| 177 |
+
```
|
| 178 |
+
|
| 179 |
+
4. **Set up database**
|
| 180 |
+
|
| 181 |
+
```bash
|
| 182 |
+
npx prisma generate
|
| 183 |
+
npx prisma db push
|
| 184 |
+
npx prisma db seed # Optional: adds sample data
|
| 185 |
+
```
|
| 186 |
+
|
| 187 |
+
5. **Run development server**
|
| 188 |
+
|
| 189 |
+
```bash
|
| 190 |
+
npm run dev
|
| 191 |
+
```
|
| 192 |
+
|
| 193 |
+
Visit [http://localhost:3000](http://localhost:3000)
|
| 194 |
+
|
| 195 |
+
---
|
| 196 |
+
|
| 197 |
+
## 📁 Project Structure
|
| 198 |
+
|
| 199 |
+
```
|
| 200 |
+
openprompt/
|
| 201 |
+
├── src/
|
| 202 |
+
│ ├── app/ # Next.js app directory
|
| 203 |
+
│ │ ├── api/ # API routes
|
| 204 |
+
│ │ ├── categories/ # Category pages
|
| 205 |
+
│ │ ├── category/ # Dynamic category
|
| 206 |
+
│ │ ├── collections/ # Collections
|
| 207 |
+
│ │ ├── create/ # Create prompt
|
| 208 |
+
│ │ ├── creators/ # Creators index
|
| 209 |
+
│ │ ├── creator/ # Creator profile
|
| 210 |
+
│ │ ├── embed/ # Embed pages
|
| 211 |
+
│ │ ├── explore/ # Explore page
|
| 212 |
+
│ │ ├── image-prompts/ # Image generation prompts
|
| 213 |
+
│ │ ├── characters/ # AI characters
|
| 214 |
+
│ │ ├── leaderboard/ # Creator rankings
|
| 215 |
+
│ │ ├── p/ # Prompt runner
|
| 216 |
+
│ │ ├── thunderdome/ # Model comparison
|
| 217 |
+
│ │ ├── workflows/ # Workflow chains
|
| 218 |
+
│ │ ├── tools/ # AI tools suite
|
| 219 |
+
│ │ ├── about/ # About page
|
| 220 |
+
│ │ ├── docs/ # Documentation
|
| 221 |
+
│ │ ├── guides/ # User guides
|
| 222 |
+
│ │ ├── blog/ # Blog
|
| 223 |
+
│ │ ├── layout.tsx # Root layout
|
| 224 |
+
│ │ └── page.tsx # Landing page
|
| 225 |
+
│ ├── components/ # React components
|
| 226 |
+
│ │ ├── auth/ # Auth components
|
| 227 |
+
│ │ ├── comments/ # Comments system
|
| 228 |
+
│ │ ├── create/ # Creation components
|
| 229 |
+
│ │ ├── engagement/ # Engagement metrics
|
| 230 |
+
│ │ ├── explore/ # Discovery components
|
| 231 |
+
│ │ ├── layout/ # Layout (header, footer)
|
| 232 |
+
│ │ ├── prompt-runner/ # Prompt execution
|
| 233 |
+
│ │ ├── prompts/ # Prompt components
|
| 234 |
+
│ │ ├── thunderdome/ # Model comparison UI
|
| 235 |
+
│ │ ├── tools/ # Tool components
|
| 236 |
+
│ │ ├── workflow/ # Workflow builder
|
| 237 |
+
│ │ └── ui/ # Shadcn UI components
|
| 238 |
+
│ ├── lib/ # Utilities
|
| 239 |
+
│ │ ├── prisma.ts # Prisma client
|
| 240 |
+
│ │ ├── tools.ts # 177 tool definitions
|
| 241 |
+
│ │ ├── frameworks.ts # Prompt frameworks
|
| 242 |
+
│ │ └── utils.ts # Helper functions
|
| 243 |
+
│ └── types/ # TypeScript types
|
| 244 |
+
├── prisma/
|
| 245 |
+
│ ├── schema.prisma # Database schema
|
| 246 |
+
│ └── seed.ts # Seed data
|
| 247 |
+
├── openprompt-extension/ # Browser extension
|
| 248 |
+
│ ├── src/ # Extension source
|
| 249 |
+
│ ├── dist/ # Built extension
|
| 250 |
+
│ └── README.md # Extension docs
|
| 251 |
+
├── public/ # Static assets
|
| 252 |
+
├── DEPLOYMENT.md # Deployment guide
|
| 253 |
+
├── FEATURE-GAP-ANALYSIS.md # Feature roadmap
|
| 254 |
+
└── README.md # This file
|
| 255 |
+
```
|
| 256 |
+
|
| 257 |
+
---
|
| 258 |
+
|
| 259 |
+
## 🧩 Browser Extension
|
| 260 |
+
|
| 261 |
+
Open prompts directly in ChatGPT, Claude, Gemini, and other AI interfaces with one click!
|
| 262 |
+
|
| 263 |
+
### Supported Platforms
|
| 264 |
+
- ✅ ChatGPT (chatgpt.com)
|
| 265 |
+
- ✅ Claude (claude.ai)
|
| 266 |
+
- ✅ Gemini (gemini.google.com)
|
| 267 |
+
- ✅ Perplexity (perplexity.ai)
|
| 268 |
+
- ✅ Mistral (chat.mistral.ai)
|
| 269 |
+
- ✅ Microsoft Copilot (copilot.microsoft.com)
|
| 270 |
+
|
| 271 |
+
### Installation
|
| 272 |
+
|
| 273 |
+
```bash
|
| 274 |
+
cd openprompt-extension
|
| 275 |
+
npm install
|
| 276 |
+
npm run build
|
| 277 |
+
```
|
| 278 |
+
|
| 279 |
+
Then load the `dist` folder as an unpacked extension in Chrome.
|
| 280 |
+
|
| 281 |
+
See [openprompt-extension/README.md](openprompt-extension/README.md) for detailed instructions.
|
| 282 |
+
|
| 283 |
+
---
|
| 284 |
+
|
| 285 |
+
## 🔧 Available Tools
|
| 286 |
+
|
| 287 |
+
### 177 Tools Across 15 Categories
|
| 288 |
+
|
| 289 |
+
| Category | Count | Examples |
|
| 290 |
+
|----------|-------|----------|
|
| 291 |
+
| **Prompting** | 9 | Optimizer, Chain-of-Thought, Meta-Prompt |
|
| 292 |
+
| **Marketing** | 14 | Strategy, Ads, Campaigns, Funnels |
|
| 293 |
+
| **Branding** | 11 | Names, Slogans, Voice, Guidelines |
|
| 294 |
+
| **Copywriting** | 12 | Headlines, Landing Pages, CTAs |
|
| 295 |
+
| **Business** | 15 | Plans, SWOT, Pitches, Financials |
|
| 296 |
+
| **Email** | 10 | Sequences, Templates, Subject Lines |
|
| 297 |
+
| **Product** | 12 | PRDs, Roadmaps, User Stories |
|
| 298 |
+
| **HR** | 11 | Job Posts, Interviews, Onboarding |
|
| 299 |
+
| **Personal Brand** | 8 | LinkedIn, Twitter, Bio Generators |
|
| 300 |
+
| **Operations** | 9 | SOPs, KPIs, Process Improvement |
|
| 301 |
+
| **Social Media** | 12 | Posts, Reels, Calendars, Hashtags |
|
| 302 |
+
| **Education** | 10 | Lesson Plans, Quizzes, Curriculum |
|
| 303 |
+
| **Development** | 15 | Code Review, Docs, API Design |
|
| 304 |
+
| **Creative** | 14 | Stories, Scripts, Poetry, Art |
|
| 305 |
+
| **Research** | 15 | Analysis, Summaries, Literature |
|
| 306 |
+
|
| 307 |
+
See [src/lib/tools.ts](src/lib/tools.ts) for complete definitions.
|
| 308 |
+
|
| 309 |
+
---
|
| 310 |
+
|
| 311 |
+
## 🌐 Deployment
|
| 312 |
+
|
| 313 |
+
### Deploy to Vercel (Recommended)
|
| 314 |
+
|
| 315 |
+
1. **Push to GitHub**
|
| 316 |
+
|
| 317 |
+
```bash
|
| 318 |
+
git add .
|
| 319 |
+
git commit -m "Initial commit"
|
| 320 |
+
git push origin main
|
| 321 |
+
```
|
| 322 |
+
|
| 323 |
+
2. **Import to Vercel**
|
| 324 |
+
|
| 325 |
+
- Go to [vercel.com/new](https://vercel.com/new)
|
| 326 |
+
- Import your repository
|
| 327 |
+
- Add environment variables
|
| 328 |
+
- Deploy!
|
| 329 |
+
|
| 330 |
+
3. **Set up domain** (optional)
|
| 331 |
+
|
| 332 |
+
- Add custom domain in Vercel dashboard
|
| 333 |
+
- Configure DNS settings
|
| 334 |
+
- SSL automatically provisioned
|
| 335 |
+
|
| 336 |
+
For detailed instructions, see [DEPLOYMENT.md](DEPLOYMENT.md)
|
| 337 |
+
|
| 338 |
+
---
|
| 339 |
+
|
| 340 |
+
## 📚 Documentation
|
| 341 |
+
|
| 342 |
+
- **[DEPLOYMENT.md](DEPLOYMENT.md)** - Complete deployment guide
|
| 343 |
+
- **[FEATURE-GAP-ANALYSIS.md](FEATURE-GAP-ANALYSIS.md)** - Competitor analysis & roadmap
|
| 344 |
+
- **[openprompt-extension/README.md](openprompt-extension/README.md)** - Browser extension docs
|
| 345 |
+
|
| 346 |
+
---
|
| 347 |
+
|
| 348 |
+
## 🎯 Roadmap
|
| 349 |
+
|
| 350 |
+
### ✅ Current: v1.0 (Complete)
|
| 351 |
+
- ✅ Full prompts marketplace with 7 categories
|
| 352 |
+
- ✅ 177 AI tools across 15 categories
|
| 353 |
+
- ✅ Multi-model support (OpenAI, Anthropic, Google, Ollama)
|
| 354 |
+
- ✅ Thunderdome model comparison
|
| 355 |
+
- ✅ Workflow chains
|
| 356 |
+
- ✅ Image prompts gallery
|
| 357 |
+
- ✅ AI characters
|
| 358 |
+
- ✅ Comments & engagement
|
| 359 |
+
- ✅ Creator leaderboard
|
| 360 |
+
- ✅ Browser extension
|
| 361 |
+
|
| 362 |
+
### 🚧 Next: v1.5
|
| 363 |
+
- [ ] Prompt marketplace (buy/sell)
|
| 364 |
+
- [ ] Team workspaces
|
| 365 |
+
- [ ] API access
|
| 366 |
+
- [ ] Custom fine-tuning
|
| 367 |
+
|
| 368 |
+
### 🔮 Future: v2.0
|
| 369 |
+
- [ ] Mobile app
|
| 370 |
+
- [ ] Enterprise features
|
| 371 |
+
- [ ] White-label option
|
| 372 |
+
- [ ] Plugin ecosystem
|
| 373 |
+
|
| 374 |
+
---
|
| 375 |
+
|
| 376 |
+
## 💰 Monetization
|
| 377 |
+
|
| 378 |
+
### Free Tier
|
| 379 |
+
- All basic features
|
| 380 |
+
- 20 tool executions/day
|
| 381 |
+
- Community prompts
|
| 382 |
+
|
| 383 |
+
### Pro ($9/month)
|
| 384 |
+
- Unlimited tool executions
|
| 385 |
+
- Access to PRO tools
|
| 386 |
+
- Execution history
|
| 387 |
+
- Priority support
|
| 388 |
+
|
| 389 |
+
### Enterprise (Custom)
|
| 390 |
+
- API access
|
| 391 |
+
- Custom tools
|
| 392 |
+
- White-label
|
| 393 |
+
- SLA guarantee
|
| 394 |
+
|
| 395 |
+
---
|
| 396 |
+
|
| 397 |
+
## 📞 Support
|
| 398 |
+
|
| 399 |
+
- **Issues:** [GitHub Issues](https://github.com/Anky9972/open-prompt/issues)
|
| 400 |
+
- **Discussions:** [GitHub Discussions](https://github.com/Anky9972/open-prompt/discussions)
|
| 401 |
+
- **Email:** ankygaur9972@gmail.com
|
| 402 |
+
- **Twitter:** [@anky_vivek](https://x.com/anky_vivek)
|
| 403 |
+
|
| 404 |
+
---
|
| 405 |
+
|
| 406 |
+
## 🌟 Star History
|
| 407 |
+
|
| 408 |
+
If you find this project useful, please consider giving it a star ⭐
|
| 409 |
+
|
| 410 |
+
---
|
| 411 |
+
|
| 412 |
+
**Built with ❤️ by [Anky9972](https://github.com/Anky9972)**
|
| 413 |
+
|
| 414 |
+
[Website](https://open-prompt.netlify.app) · [Twitter](https://x.com/anky_vivek) · [GitHub](https://github.com/Anky9972/open-prompt)
|
components.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"$schema": "https://ui.shadcn.com/schema.json",
|
| 3 |
+
"style": "new-york",
|
| 4 |
+
"rsc": true,
|
| 5 |
+
"tsx": true,
|
| 6 |
+
"tailwind": {
|
| 7 |
+
"config": "",
|
| 8 |
+
"css": "src/app/globals.css",
|
| 9 |
+
"baseColor": "neutral",
|
| 10 |
+
"cssVariables": true,
|
| 11 |
+
"prefix": ""
|
| 12 |
+
},
|
| 13 |
+
"iconLibrary": "lucide",
|
| 14 |
+
"aliases": {
|
| 15 |
+
"components": "@/components",
|
| 16 |
+
"utils": "@/lib/utils",
|
| 17 |
+
"ui": "@/components/ui",
|
| 18 |
+
"lib": "@/lib",
|
| 19 |
+
"hooks": "@/hooks"
|
| 20 |
+
},
|
| 21 |
+
"registries": {}
|
| 22 |
+
}
|
eslint.config.mjs
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { defineConfig, globalIgnores } from "eslint/config";
|
| 2 |
+
import nextVitals from "eslint-config-next/core-web-vitals";
|
| 3 |
+
import nextTs from "eslint-config-next/typescript";
|
| 4 |
+
|
| 5 |
+
const eslintConfig = defineConfig([
|
| 6 |
+
...nextVitals,
|
| 7 |
+
...nextTs,
|
| 8 |
+
// Override default ignores of eslint-config-next.
|
| 9 |
+
globalIgnores([
|
| 10 |
+
// Default ignores of eslint-config-next:
|
| 11 |
+
".next/**",
|
| 12 |
+
"out/**",
|
| 13 |
+
"build/**",
|
| 14 |
+
"next-env.d.ts",
|
| 15 |
+
]),
|
| 16 |
+
]);
|
| 17 |
+
|
| 18 |
+
export default eslintConfig;
|
next.config.ts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { NextConfig } from "next";
|
| 2 |
+
|
| 3 |
+
const nextConfig: NextConfig = {
|
| 4 |
+
// Image optimization – whitelist only known-safe domains (no wildcard **)
|
| 5 |
+
images: {
|
| 6 |
+
remotePatterns: [
|
| 7 |
+
// GitHub avatars (OAuth)
|
| 8 |
+
{ protocol: "https", hostname: "avatars.githubusercontent.com" },
|
| 9 |
+
// Google avatars (OAuth)
|
| 10 |
+
{ protocol: "https", hostname: "lh3.googleusercontent.com" },
|
| 11 |
+
{ protocol: "https", hostname: "lh4.googleusercontent.com" },
|
| 12 |
+
{ protocol: "https", hostname: "lh5.googleusercontent.com" },
|
| 13 |
+
// Stack Auth CDN
|
| 14 |
+
{ protocol: "https", hostname: "*.stackauth.com" },
|
| 15 |
+
// Prompt preview / gallery images (Cloudflare R2 / storage)
|
| 16 |
+
{ protocol: "https", hostname: "imagedelivery.net" },
|
| 17 |
+
{ protocol: "https", hostname: "images.unsplash.com" },
|
| 18 |
+
// AI image CDNs
|
| 19 |
+
{ protocol: "https", hostname: "oaidalleapiprodscus.blob.core.windows.net" },
|
| 20 |
+
{ protocol: "https", hostname: "cdn.midjourney.com" },
|
| 21 |
+
{ protocol: "https", hostname: "image.civitai.com" },
|
| 22 |
+
],
|
| 23 |
+
formats: ["image/avif", "image/webp"],
|
| 24 |
+
},
|
| 25 |
+
|
| 26 |
+
// Security headers
|
| 27 |
+
async headers() {
|
| 28 |
+
return [
|
| 29 |
+
{
|
| 30 |
+
source: "/(.*)",
|
| 31 |
+
headers: [
|
| 32 |
+
// ✅ SAMEORIGIN (not DENY) so /embed/ iframes work
|
| 33 |
+
{ key: "X-Frame-Options", value: "SAMEORIGIN" },
|
| 34 |
+
{ key: "X-Content-Type-Options", value: "nosniff" },
|
| 35 |
+
{ key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
|
| 36 |
+
{ key: "X-DNS-Prefetch-Control", value: "on" },
|
| 37 |
+
{ key: "X-Permitted-Cross-Domain-Policies", value: "none" },
|
| 38 |
+
{ key: "Strict-Transport-Security", value: "max-age=31536000; includeSubDomains; preload" },
|
| 39 |
+
{ key: "Permissions-Policy", value: "camera=(), microphone=(), geolocation=()" },
|
| 40 |
+
{
|
| 41 |
+
key: "Content-Security-Policy",
|
| 42 |
+
value: [
|
| 43 |
+
"default-src 'self'",
|
| 44 |
+
// Scripts: Next.js needs unsafe-inline for inline event handlers; Turnstile needs its domain
|
| 45 |
+
"script-src 'self' 'unsafe-inline' 'unsafe-eval' https://challenges.cloudflare.com https://www.googletagmanager.com",
|
| 46 |
+
// Styles: Google Fonts + Tailwind inline
|
| 47 |
+
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
|
| 48 |
+
// Fonts: Google Fonts CDN
|
| 49 |
+
"font-src 'self' https://fonts.gstatic.com",
|
| 50 |
+
// Images: broad HTTPS allowed (user avatar, AI-generated images)
|
| 51 |
+
"img-src 'self' data: blob: https:",
|
| 52 |
+
// Frames: Cloudflare Turnstile widget + same-origin embeds
|
| 53 |
+
"frame-src 'self' https://challenges.cloudflare.com",
|
| 54 |
+
// Connect: AI APIs + Cloudflare + analytics
|
| 55 |
+
"connect-src 'self' https://api.openai.com https://api.anthropic.com https://generativelanguage.googleapis.com https://challenges.cloudflare.com wss: ws:",
|
| 56 |
+
// Media / workers (Next.js internals)
|
| 57 |
+
"media-src 'self' blob:",
|
| 58 |
+
"worker-src 'self' blob:",
|
| 59 |
+
].join("; "),
|
| 60 |
+
},
|
| 61 |
+
],
|
| 62 |
+
},
|
| 63 |
+
{
|
| 64 |
+
// Cache logos & brand assets for 1 year
|
| 65 |
+
source: "/logos/(.*)",
|
| 66 |
+
headers: [
|
| 67 |
+
{ key: "Cache-Control", value: "public, max-age=31536000, immutable" },
|
| 68 |
+
],
|
| 69 |
+
},
|
| 70 |
+
{
|
| 71 |
+
// Cache other static public assets for 1 day
|
| 72 |
+
source: "/(.*\\.(?:svg|png|jpg|jpeg|gif|ico|woff2|woff|ttf))",
|
| 73 |
+
headers: [
|
| 74 |
+
{ key: "Cache-Control", value: "public, max-age=86400, stale-while-revalidate=3600" },
|
| 75 |
+
],
|
| 76 |
+
},
|
| 77 |
+
];
|
| 78 |
+
},
|
| 79 |
+
|
| 80 |
+
// Performance optimizations – tree-shake large packages
|
| 81 |
+
experimental: {
|
| 82 |
+
optimizePackageImports: [
|
| 83 |
+
"lucide-react",
|
| 84 |
+
"react-icons",
|
| 85 |
+
"framer-motion",
|
| 86 |
+
"recharts",
|
| 87 |
+
"@radix-ui/react-dropdown-menu",
|
| 88 |
+
"@radix-ui/react-select",
|
| 89 |
+
"@radix-ui/react-tabs",
|
| 90 |
+
"@radix-ui/react-tooltip",
|
| 91 |
+
],
|
| 92 |
+
},
|
| 93 |
+
};
|
| 94 |
+
|
| 95 |
+
export default nextConfig;
|
openprompt-extension/README.md
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# OpenPrompt Browser Extension
|
| 2 |
+
|
| 3 |
+
A browser extension that allows you to open prompts directly in ChatGPT, Claude, Gemini, Perplexity, Mistral, and Microsoft Copilot with one click.
|
| 4 |
+
|
| 5 |
+
## Features
|
| 6 |
+
|
| 7 |
+
- 🚀 **One-Click Injection** - Send prompts directly to your favorite AI chat interface
|
| 8 |
+
- 🤖 **Multiple Platforms** - Supports ChatGPT, Claude, Gemini, Perplexity, Mistral, Copilot
|
| 9 |
+
- 📝 **Recent History** - Access your recently used prompts
|
| 10 |
+
- ⚡ **Default Platform** - Set your preferred AI platform for quick access
|
| 11 |
+
- 🔒 **Privacy First** - All data stays in your browser
|
| 12 |
+
|
| 13 |
+
## Supported Platforms
|
| 14 |
+
|
| 15 |
+
| Platform | URL | Status |
|
| 16 |
+
|----------|-----|--------|
|
| 17 |
+
| ChatGPT | chatgpt.com | ✅ Supported |
|
| 18 |
+
| Claude | claude.ai | ✅ Supported |
|
| 19 |
+
| Gemini | gemini.google.com | ✅ Supported |
|
| 20 |
+
| Perplexity | perplexity.ai | ✅ Supported |
|
| 21 |
+
| Mistral | chat.mistral.ai | ✅ Supported |
|
| 22 |
+
| Copilot | copilot.microsoft.com | ✅ Supported |
|
| 23 |
+
|
| 24 |
+
## Installation
|
| 25 |
+
|
| 26 |
+
### Development Build
|
| 27 |
+
|
| 28 |
+
1. Install dependencies:
|
| 29 |
+
```bash
|
| 30 |
+
cd openprompt-extension
|
| 31 |
+
npm install
|
| 32 |
+
```
|
| 33 |
+
|
| 34 |
+
2. Build the extension:
|
| 35 |
+
```bash
|
| 36 |
+
npm run build
|
| 37 |
+
```
|
| 38 |
+
|
| 39 |
+
3. Load in Chrome:
|
| 40 |
+
- Open `chrome://extensions/`
|
| 41 |
+
- Enable "Developer mode"
|
| 42 |
+
- Click "Load unpacked"
|
| 43 |
+
- Select the `dist` folder
|
| 44 |
+
|
| 45 |
+
### Development Mode
|
| 46 |
+
|
| 47 |
+
For live reloading during development:
|
| 48 |
+
|
| 49 |
+
```bash
|
| 50 |
+
npm run dev
|
| 51 |
+
```
|
| 52 |
+
|
| 53 |
+
This will watch for changes and rebuild automatically.
|
| 54 |
+
|
| 55 |
+
## Project Structure
|
| 56 |
+
|
| 57 |
+
```
|
| 58 |
+
openprompt-extension/
|
| 59 |
+
├── public/
|
| 60 |
+
│ ├── manifest.json # Chrome extension manifest
|
| 61 |
+
│ └── icons/ # Extension icons
|
| 62 |
+
├── src/
|
| 63 |
+
│ ├── popup/ # Extension popup UI (React)
|
| 64 |
+
│ │ ├── App.tsx
|
| 65 |
+
│ │ ├── main.tsx
|
| 66 |
+
│ │ └── index.css
|
| 67 |
+
│ ├── background/ # Service worker
|
| 68 |
+
│ │ └── index.ts
|
| 69 |
+
│ └── content-scripts/ # Platform-specific injection scripts
|
| 70 |
+
│ ├── chatgpt.ts
|
| 71 |
+
│ ├── claude.ts
|
| 72 |
+
│ ├── gemini.ts
|
| 73 |
+
│ ├── perplexity.ts
|
| 74 |
+
│ ├── mistral.ts
|
| 75 |
+
│ ├── copilot.ts
|
| 76 |
+
│ ├── openprompt-site.ts
|
| 77 |
+
│ └── utils.ts
|
| 78 |
+
├── popup.html
|
| 79 |
+
├── package.json
|
| 80 |
+
├── vite.config.ts
|
| 81 |
+
├── tailwind.config.js
|
| 82 |
+
└── tsconfig.json
|
| 83 |
+
```
|
| 84 |
+
|
| 85 |
+
## How It Works
|
| 86 |
+
|
| 87 |
+
1. **From OpenPrompt Website:**
|
| 88 |
+
- Click "Open In..." on any prompt
|
| 89 |
+
- Select your preferred AI platform
|
| 90 |
+
- The prompt is automatically filled in the chat input
|
| 91 |
+
|
| 92 |
+
2. **From Extension Popup:**
|
| 93 |
+
- Click the extension icon
|
| 94 |
+
- View pending prompts
|
| 95 |
+
- Choose where to open them
|
| 96 |
+
- Access recent prompt history
|
| 97 |
+
|
| 98 |
+
## API Communication
|
| 99 |
+
|
| 100 |
+
The extension communicates with the OpenPrompt website via:
|
| 101 |
+
- `window.postMessage` for content script communication
|
| 102 |
+
- `chrome.runtime.sendMessage` for external messaging
|
| 103 |
+
- Chrome storage API for state persistence
|
| 104 |
+
|
| 105 |
+
## Building for Production
|
| 106 |
+
|
| 107 |
+
```bash
|
| 108 |
+
npm run build
|
| 109 |
+
```
|
| 110 |
+
|
| 111 |
+
The built extension will be in the `dist` folder, ready for Chrome Web Store submission.
|
| 112 |
+
|
| 113 |
+
## Publishing
|
| 114 |
+
|
| 115 |
+
1. Create a developer account on Chrome Web Store
|
| 116 |
+
2. Package the `dist` folder as a ZIP
|
| 117 |
+
3. Upload to Chrome Web Store Developer Dashboard
|
| 118 |
+
4. Submit for review
|
| 119 |
+
|
| 120 |
+
## Icons
|
| 121 |
+
|
| 122 |
+
For production, you'll need to convert the SVG icon to PNG at these sizes:
|
| 123 |
+
- icon16.png (16x16)
|
| 124 |
+
- icon48.png (48x48)
|
| 125 |
+
- icon128.png (128x128)
|
| 126 |
+
|
| 127 |
+
Use a tool like [CloudConvert](https://cloudconvert.com/svg-to-png) or run:
|
| 128 |
+
```bash
|
| 129 |
+
# With ImageMagick
|
| 130 |
+
convert -background none -resize 16x16 public/icons/icon.svg public/icons/icon16.png
|
| 131 |
+
convert -background none -resize 48x48 public/icons/icon.svg public/icons/icon48.png
|
| 132 |
+
convert -background none -resize 128x128 public/icons/icon.svg public/icons/icon128.png
|
| 133 |
+
```
|
| 134 |
+
|
| 135 |
+
## License
|
| 136 |
+
|
| 137 |
+
MIT - Part of the OpenPrompt project
|
openprompt-extension/package-lock.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
openprompt-extension/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "openprompt-extension",
|
| 3 |
+
"version": "1.0.0",
|
| 4 |
+
"description": "Open prompts directly in ChatGPT, Claude, Gemini, and other AI chat interfaces",
|
| 5 |
+
"type": "module",
|
| 6 |
+
"scripts": {
|
| 7 |
+
"dev": "vite build --watch",
|
| 8 |
+
"build": "tsc && vite build",
|
| 9 |
+
"preview": "vite preview"
|
| 10 |
+
},
|
| 11 |
+
"dependencies": {
|
| 12 |
+
"lucide-react": "^0.469.0",
|
| 13 |
+
"react": "^18.3.1",
|
| 14 |
+
"react-dom": "^18.3.1"
|
| 15 |
+
},
|
| 16 |
+
"devDependencies": {
|
| 17 |
+
"@types/chrome": "^0.0.283",
|
| 18 |
+
"@types/react": "^18.3.17",
|
| 19 |
+
"@types/react-dom": "^18.3.5",
|
| 20 |
+
"@vitejs/plugin-react": "^4.3.4",
|
| 21 |
+
"autoprefixer": "^10.4.20",
|
| 22 |
+
"postcss": "^8.4.49",
|
| 23 |
+
"sharp": "^0.34.5",
|
| 24 |
+
"tailwindcss": "^3.4.17",
|
| 25 |
+
"typescript": "^5.7.2",
|
| 26 |
+
"vite": "^6.0.5"
|
| 27 |
+
}
|
| 28 |
+
}
|
openprompt-extension/popup.html
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 6 |
+
<title>OpenPrompt</title>
|
| 7 |
+
</head>
|
| 8 |
+
<body>
|
| 9 |
+
<div id="root"></div>
|
| 10 |
+
<script type="module" src="/src/popup/main.tsx"></script>
|
| 11 |
+
</body>
|
| 12 |
+
</html>
|
openprompt-extension/postcss.config.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export default {
|
| 2 |
+
plugins: {
|
| 3 |
+
tailwindcss: {},
|
| 4 |
+
autoprefixer: {},
|
| 5 |
+
},
|
| 6 |
+
}
|
openprompt-extension/public/icon.svg
ADDED
|
|
openprompt-extension/public/icons/icon128.png
ADDED
|
|
Git LFS Details
|
openprompt-extension/public/icons/icon16.png
ADDED
|
|
Git LFS Details
|
openprompt-extension/public/icons/icon48.png
ADDED
|
|
Git LFS Details
|
openprompt-extension/public/icons/logo.svg
ADDED
|
|
openprompt-extension/public/manifest.json
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"manifest_version": 3,
|
| 3 |
+
"name": "OpenPrompt - Open in AI",
|
| 4 |
+
"version": "1.1.0",
|
| 5 |
+
"description": "Open prompts directly in ChatGPT, Claude, Gemini, Perplexity, and other AI chat interfaces with one click.",
|
| 6 |
+
"permissions": [
|
| 7 |
+
"activeTab",
|
| 8 |
+
"storage",
|
| 9 |
+
"tabs",
|
| 10 |
+
"contextMenus",
|
| 11 |
+
"clipboardWrite",
|
| 12 |
+
"clipboardRead"
|
| 13 |
+
],
|
| 14 |
+
"commands": {
|
| 15 |
+
"_execute_action": {
|
| 16 |
+
"suggested_key": {
|
| 17 |
+
"default": "Ctrl+Shift+O",
|
| 18 |
+
"mac": "Command+Shift+O"
|
| 19 |
+
},
|
| 20 |
+
"description": "Open OpenPrompt popup"
|
| 21 |
+
},
|
| 22 |
+
"quick-search": {
|
| 23 |
+
"suggested_key": {
|
| 24 |
+
"default": "Ctrl+Shift+P",
|
| 25 |
+
"mac": "Command+Shift+P"
|
| 26 |
+
},
|
| 27 |
+
"description": "Quick search prompts"
|
| 28 |
+
},
|
| 29 |
+
"capture-selection": {
|
| 30 |
+
"suggested_key": {
|
| 31 |
+
"default": "Ctrl+Shift+S",
|
| 32 |
+
"mac": "Command+Shift+S"
|
| 33 |
+
},
|
| 34 |
+
"description": "Capture selected text as prompt"
|
| 35 |
+
}
|
| 36 |
+
},
|
| 37 |
+
"host_permissions": [
|
| 38 |
+
"https://chat.openai.com/*",
|
| 39 |
+
"https://chatgpt.com/*",
|
| 40 |
+
"https://claude.ai/*",
|
| 41 |
+
"https://gemini.google.com/*",
|
| 42 |
+
"https://www.perplexity.ai/*",
|
| 43 |
+
"https://chat.mistral.ai/*",
|
| 44 |
+
"https://poe.com/*",
|
| 45 |
+
"https://you.com/*",
|
| 46 |
+
"https://copilot.microsoft.com/*",
|
| 47 |
+
"https://open-prompt.netlify.app/*",
|
| 48 |
+
"http://localhost:3000/*"
|
| 49 |
+
],
|
| 50 |
+
"action": {
|
| 51 |
+
"default_popup": "popup.html",
|
| 52 |
+
"default_icon": {
|
| 53 |
+
"16": "icons/icon16.png",
|
| 54 |
+
"48": "icons/icon48.png",
|
| 55 |
+
"128": "icons/icon128.png"
|
| 56 |
+
}
|
| 57 |
+
},
|
| 58 |
+
"icons": {
|
| 59 |
+
"16": "icons/icon16.png",
|
| 60 |
+
"48": "icons/icon48.png",
|
| 61 |
+
"128": "icons/icon128.png"
|
| 62 |
+
},
|
| 63 |
+
"background": {
|
| 64 |
+
"service_worker": "background.js",
|
| 65 |
+
"type": "module"
|
| 66 |
+
},
|
| 67 |
+
"content_scripts": [
|
| 68 |
+
{
|
| 69 |
+
"matches": [
|
| 70 |
+
"https://chat.openai.com/*",
|
| 71 |
+
"https://chatgpt.com/*"
|
| 72 |
+
],
|
| 73 |
+
"js": [
|
| 74 |
+
"content-scripts/chatgpt.js"
|
| 75 |
+
],
|
| 76 |
+
"run_at": "document_idle"
|
| 77 |
+
},
|
| 78 |
+
{
|
| 79 |
+
"matches": [
|
| 80 |
+
"https://claude.ai/*"
|
| 81 |
+
],
|
| 82 |
+
"js": [
|
| 83 |
+
"content-scripts/claude.js"
|
| 84 |
+
],
|
| 85 |
+
"run_at": "document_idle"
|
| 86 |
+
},
|
| 87 |
+
{
|
| 88 |
+
"matches": [
|
| 89 |
+
"https://gemini.google.com/*"
|
| 90 |
+
],
|
| 91 |
+
"js": [
|
| 92 |
+
"content-scripts/gemini.js"
|
| 93 |
+
],
|
| 94 |
+
"run_at": "document_idle"
|
| 95 |
+
},
|
| 96 |
+
{
|
| 97 |
+
"matches": [
|
| 98 |
+
"https://www.perplexity.ai/*"
|
| 99 |
+
],
|
| 100 |
+
"js": [
|
| 101 |
+
"content-scripts/perplexity.js"
|
| 102 |
+
],
|
| 103 |
+
"run_at": "document_idle"
|
| 104 |
+
},
|
| 105 |
+
{
|
| 106 |
+
"matches": [
|
| 107 |
+
"https://chat.mistral.ai/*"
|
| 108 |
+
],
|
| 109 |
+
"js": [
|
| 110 |
+
"content-scripts/mistral.js"
|
| 111 |
+
],
|
| 112 |
+
"run_at": "document_idle"
|
| 113 |
+
},
|
| 114 |
+
{
|
| 115 |
+
"matches": [
|
| 116 |
+
"https://copilot.microsoft.com/*"
|
| 117 |
+
],
|
| 118 |
+
"js": [
|
| 119 |
+
"content-scripts/copilot.js"
|
| 120 |
+
],
|
| 121 |
+
"run_at": "document_idle"
|
| 122 |
+
},
|
| 123 |
+
{
|
| 124 |
+
"matches": [
|
| 125 |
+
"https://open-prompt.netlify.app/*",
|
| 126 |
+
"http://localhost:3000/*"
|
| 127 |
+
],
|
| 128 |
+
"js": [
|
| 129 |
+
"content-scripts/openprompt-site.js"
|
| 130 |
+
],
|
| 131 |
+
"run_at": "document_idle"
|
| 132 |
+
}
|
| 133 |
+
],
|
| 134 |
+
"externally_connectable": {
|
| 135 |
+
"matches": [
|
| 136 |
+
"https://open-prompt.netlify.app/*",
|
| 137 |
+
"http://localhost:3000/*"
|
| 138 |
+
]
|
| 139 |
+
},
|
| 140 |
+
"web_accessible_resources": [
|
| 141 |
+
{
|
| 142 |
+
"resources": [
|
| 143 |
+
"icons/*"
|
| 144 |
+
],
|
| 145 |
+
"matches": [
|
| 146 |
+
"<all_urls>"
|
| 147 |
+
]
|
| 148 |
+
}
|
| 149 |
+
]
|
| 150 |
+
}
|
openprompt-extension/public/small-promo-tile.png
ADDED
|
Git LFS Details
|
openprompt-extension/scripts/create-icons.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Simple script to create placeholder icon PNGs
|
| 2 |
+
// Run with: node scripts/create-icons.js
|
| 3 |
+
|
| 4 |
+
const fs = require('fs');
|
| 5 |
+
const path = require('path');
|
| 6 |
+
|
| 7 |
+
// Simple 1x1 purple pixel PNG base64 - we'll use SVG icons for now
|
| 8 |
+
// For production, use a proper image conversion tool
|
| 9 |
+
|
| 10 |
+
const iconSizes = [16, 48, 128];
|
| 11 |
+
const iconsDir = path.join(__dirname, '../public/icons');
|
| 12 |
+
|
| 13 |
+
// Create a simple colored square PNG (this is a minimal valid PNG)
|
| 14 |
+
function createMinimalPng(size) {
|
| 15 |
+
// For now, just create a note that real icons are needed
|
| 16 |
+
console.log(`Note: Create ${size}x${size} PNG icon at public/icons/icon${size}.png`);
|
| 17 |
+
console.log(`You can convert the SVG using: https://cloudconvert.com/svg-to-png`);
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
iconSizes.forEach(size => {
|
| 21 |
+
createMinimalPng(size);
|
| 22 |
+
});
|
| 23 |
+
|
| 24 |
+
console.log('\nTo create proper icons, run:');
|
| 25 |
+
console.log('npx sharp-cli --input public/icons/icon.svg --output public/icons/icon16.png --resize 16');
|
| 26 |
+
console.log('npx sharp-cli --input public/icons/icon.svg --output public/icons/icon48.png --resize 48');
|
| 27 |
+
console.log('npx sharp-cli --input public/icons/icon.svg --output public/icons/icon128.png --resize 128');
|
openprompt-extension/scripts/generate-icons.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Create extension icons from SVG
|
| 2 |
+
import sharp from 'sharp';
|
| 3 |
+
import path from 'path';
|
| 4 |
+
import fs from 'fs';
|
| 5 |
+
import { fileURLToPath } from 'url';
|
| 6 |
+
|
| 7 |
+
const __filename = fileURLToPath(import.meta.url);
|
| 8 |
+
const __dirname = path.dirname(__filename);
|
| 9 |
+
|
| 10 |
+
const sizes = [16, 48, 128];
|
| 11 |
+
const svgPath = path.join(__dirname, '../public/icons/icon.svg');
|
| 12 |
+
const outputDir = path.join(__dirname, '../public/icons');
|
| 13 |
+
|
| 14 |
+
// Ensure output directory exists
|
| 15 |
+
if (!fs.existsSync(outputDir)) {
|
| 16 |
+
fs.mkdirSync(outputDir, { recursive: true });
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
async function createIcons() {
|
| 20 |
+
const svgBuffer = fs.readFileSync(svgPath);
|
| 21 |
+
|
| 22 |
+
for (const size of sizes) {
|
| 23 |
+
const outputPath = path.join(outputDir, `icon${size}.png`);
|
| 24 |
+
|
| 25 |
+
await sharp(svgBuffer)
|
| 26 |
+
.resize(size, size)
|
| 27 |
+
.png()
|
| 28 |
+
.toFile(outputPath);
|
| 29 |
+
|
| 30 |
+
console.log(`Created: icon${size}.png`);
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
console.log('\nAll icons created successfully!');
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
createIcons().catch(console.error);
|
openprompt-extension/src/background/index.ts
ADDED
|
@@ -0,0 +1,450 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Background Service Worker for OpenPrompt Extension
|
| 2 |
+
|
| 3 |
+
// Configurable API base URL — defaults to production, overridable via extension storage
|
| 4 |
+
let API_BASE = 'https://open-prompt.netlify.app';
|
| 5 |
+
|
| 6 |
+
// Load custom API URL from extension settings (set in options page)
|
| 7 |
+
(async () => {
|
| 8 |
+
try {
|
| 9 |
+
const result = await chrome.storage.sync.get(['apiBaseUrl']);
|
| 10 |
+
if (result.apiBaseUrl) API_BASE = result.apiBaseUrl;
|
| 11 |
+
} catch { /* use default */ }
|
| 12 |
+
})();
|
| 13 |
+
|
| 14 |
+
const PLATFORMS: Record<string, string> = {
|
| 15 |
+
chatgpt: 'https://chatgpt.com',
|
| 16 |
+
claude: 'https://claude.ai',
|
| 17 |
+
gemini: 'https://gemini.google.com',
|
| 18 |
+
perplexity: 'https://www.perplexity.ai',
|
| 19 |
+
mistral: 'https://chat.mistral.ai',
|
| 20 |
+
copilot: 'https://copilot.microsoft.com',
|
| 21 |
+
};
|
| 22 |
+
|
| 23 |
+
const PLATFORM_URLS: Record<string, string[]> = {
|
| 24 |
+
chatgpt: ['chat.openai.com', 'chatgpt.com'],
|
| 25 |
+
claude: ['claude.ai'],
|
| 26 |
+
gemini: ['gemini.google.com'],
|
| 27 |
+
perplexity: ['perplexity.ai'],
|
| 28 |
+
mistral: ['chat.mistral.ai'],
|
| 29 |
+
copilot: ['copilot.microsoft.com'],
|
| 30 |
+
};
|
| 31 |
+
|
| 32 |
+
// Get or create device ID for analytics
|
| 33 |
+
async function getDeviceId(): Promise<string> {
|
| 34 |
+
const result = await chrome.storage.local.get(['deviceId']);
|
| 35 |
+
if (result.deviceId) return result.deviceId;
|
| 36 |
+
|
| 37 |
+
const deviceId = 'ext_' + crypto.randomUUID();
|
| 38 |
+
await chrome.storage.local.set({ deviceId });
|
| 39 |
+
return deviceId;
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
// Get user ID if logged in
|
| 43 |
+
async function getUserId(): Promise<string | null> {
|
| 44 |
+
const result = await chrome.storage.local.get(['userId']);
|
| 45 |
+
return result.userId || null;
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
// Track analytics
|
| 49 |
+
async function trackAnalytics(data: {
|
| 50 |
+
action: string;
|
| 51 |
+
platform?: string;
|
| 52 |
+
platforms?: string[];
|
| 53 |
+
promptId?: string;
|
| 54 |
+
promptTitle?: string;
|
| 55 |
+
promptText?: string;
|
| 56 |
+
metadata?: Record<string, unknown>;
|
| 57 |
+
}): Promise<void> {
|
| 58 |
+
try {
|
| 59 |
+
const deviceId = await getDeviceId();
|
| 60 |
+
const userId = await getUserId();
|
| 61 |
+
|
| 62 |
+
await fetch(`${API_BASE}/api/extension/analytics`, {
|
| 63 |
+
method: 'POST',
|
| 64 |
+
headers: { 'Content-Type': 'application/json' },
|
| 65 |
+
body: JSON.stringify({
|
| 66 |
+
deviceId,
|
| 67 |
+
userId,
|
| 68 |
+
...data,
|
| 69 |
+
}),
|
| 70 |
+
});
|
| 71 |
+
} catch (error) {
|
| 72 |
+
console.error('[OpenPrompt] Analytics error:', error);
|
| 73 |
+
}
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
// Create context menus on install
|
| 77 |
+
function createContextMenus(): void {
|
| 78 |
+
chrome.contextMenus.removeAll(() => {
|
| 79 |
+
// Send selected text to AI
|
| 80 |
+
chrome.contextMenus.create({
|
| 81 |
+
id: 'send-to-ai',
|
| 82 |
+
title: 'Send to AI',
|
| 83 |
+
contexts: ['selection'],
|
| 84 |
+
});
|
| 85 |
+
|
| 86 |
+
// Sub-menus for each platform
|
| 87 |
+
Object.entries(PLATFORMS).forEach(([id, _url]) => {
|
| 88 |
+
chrome.contextMenus.create({
|
| 89 |
+
id: `send-to-${id}`,
|
| 90 |
+
parentId: 'send-to-ai',
|
| 91 |
+
title: id.charAt(0).toUpperCase() + id.slice(1),
|
| 92 |
+
contexts: ['selection'],
|
| 93 |
+
});
|
| 94 |
+
});
|
| 95 |
+
|
| 96 |
+
// Capture prompt
|
| 97 |
+
chrome.contextMenus.create({
|
| 98 |
+
id: 'capture-prompt',
|
| 99 |
+
title: 'Save to OpenPrompt',
|
| 100 |
+
contexts: ['selection'],
|
| 101 |
+
});
|
| 102 |
+
|
| 103 |
+
// Open OpenPrompt
|
| 104 |
+
chrome.contextMenus.create({
|
| 105 |
+
id: 'open-openprompt',
|
| 106 |
+
title: 'Browse OpenPrompt',
|
| 107 |
+
contexts: ['page'],
|
| 108 |
+
});
|
| 109 |
+
});
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
// Handle context menu clicks
|
| 113 |
+
chrome.contextMenus.onClicked.addListener(async (info, tab) => {
|
| 114 |
+
const selectedText = info.selectionText;
|
| 115 |
+
|
| 116 |
+
if (info.menuItemId === 'open-openprompt') {
|
| 117 |
+
chrome.tabs.create({ url: API_BASE });
|
| 118 |
+
return;
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
if (info.menuItemId === 'capture-prompt' && selectedText) {
|
| 122 |
+
// Save to captured prompts
|
| 123 |
+
const deviceId = await getDeviceId();
|
| 124 |
+
const userId = await getUserId();
|
| 125 |
+
const platform = detectPlatformFromUrl(tab?.url || '');
|
| 126 |
+
|
| 127 |
+
try {
|
| 128 |
+
await fetch(`${API_BASE}/api/extension/capture`, {
|
| 129 |
+
method: 'POST',
|
| 130 |
+
headers: { 'Content-Type': 'application/json' },
|
| 131 |
+
body: JSON.stringify({
|
| 132 |
+
deviceId,
|
| 133 |
+
userId,
|
| 134 |
+
text: selectedText,
|
| 135 |
+
platform: platform || 'unknown',
|
| 136 |
+
url: tab?.url,
|
| 137 |
+
}),
|
| 138 |
+
});
|
| 139 |
+
|
| 140 |
+
// Show notification
|
| 141 |
+
if (tab?.id) {
|
| 142 |
+
chrome.tabs.sendMessage(tab.id, {
|
| 143 |
+
type: 'SHOW_NOTIFICATION',
|
| 144 |
+
message: '✓ Prompt saved to OpenPrompt!',
|
| 145 |
+
}).catch(() => {});
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
trackAnalytics({
|
| 149 |
+
action: 'capture',
|
| 150 |
+
platform: platform || 'unknown',
|
| 151 |
+
promptText: selectedText.slice(0, 500),
|
| 152 |
+
metadata: { source: 'context_menu' },
|
| 153 |
+
});
|
| 154 |
+
} catch (error) {
|
| 155 |
+
console.error('[OpenPrompt] Capture error:', error);
|
| 156 |
+
}
|
| 157 |
+
return;
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
// Handle send-to-* menus
|
| 161 |
+
if (info.menuItemId?.toString().startsWith('send-to-') && selectedText) {
|
| 162 |
+
const platformId = info.menuItemId.toString().replace('send-to-', '');
|
| 163 |
+
const url = PLATFORMS[platformId];
|
| 164 |
+
|
| 165 |
+
if (url) {
|
| 166 |
+
await chrome.storage.local.set({
|
| 167 |
+
promptToInject: {
|
| 168 |
+
prompt: selectedText,
|
| 169 |
+
platform: platformId,
|
| 170 |
+
timestamp: Date.now(),
|
| 171 |
+
},
|
| 172 |
+
});
|
| 173 |
+
|
| 174 |
+
chrome.tabs.create({ url });
|
| 175 |
+
|
| 176 |
+
trackAnalytics({
|
| 177 |
+
action: 'context_menu',
|
| 178 |
+
platform: platformId,
|
| 179 |
+
promptText: selectedText.slice(0, 500),
|
| 180 |
+
});
|
| 181 |
+
}
|
| 182 |
+
}
|
| 183 |
+
});
|
| 184 |
+
|
| 185 |
+
// Detect which AI platform from URL
|
| 186 |
+
function detectPlatformFromUrl(url: string): string | null {
|
| 187 |
+
for (const [platform, urls] of Object.entries(PLATFORM_URLS)) {
|
| 188 |
+
if (urls.some(u => url.includes(u))) {
|
| 189 |
+
return platform;
|
| 190 |
+
}
|
| 191 |
+
}
|
| 192 |
+
return null;
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
+
// Handle keyboard shortcuts
|
| 196 |
+
chrome.commands.onCommand.addListener(async (command) => {
|
| 197 |
+
console.log('[OpenPrompt] Command:', command);
|
| 198 |
+
|
| 199 |
+
if (command === 'quick-search') {
|
| 200 |
+
// Open popup with search focused
|
| 201 |
+
chrome.action.openPopup().catch(() => {
|
| 202 |
+
// Fallback: set badge to indicate action
|
| 203 |
+
chrome.action.setBadgeText({ text: 'S' });
|
| 204 |
+
chrome.action.setBadgeBackgroundColor({ color: '#9333ea' });
|
| 205 |
+
setTimeout(() => chrome.action.setBadgeText({ text: '' }), 3000);
|
| 206 |
+
});
|
| 207 |
+
|
| 208 |
+
trackAnalytics({
|
| 209 |
+
action: 'shortcut_use',
|
| 210 |
+
metadata: { shortcut: 'quick-search' },
|
| 211 |
+
});
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
if (command === 'capture-selection') {
|
| 215 |
+
// Get current tab and capture selected text
|
| 216 |
+
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
| 217 |
+
if (tab?.id) {
|
| 218 |
+
chrome.tabs.sendMessage(tab.id, { type: 'CAPTURE_SELECTION' }).catch(() => {});
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
trackAnalytics({
|
| 222 |
+
action: 'shortcut_use',
|
| 223 |
+
metadata: { shortcut: 'capture-selection' },
|
| 224 |
+
});
|
| 225 |
+
}
|
| 226 |
+
});
|
| 227 |
+
|
| 228 |
+
// Handle messages from content scripts
|
| 229 |
+
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
| 230 |
+
if (message.type === 'OPEN_TAB') {
|
| 231 |
+
chrome.tabs.create({ url: message.url }, (tab) => {
|
| 232 |
+
sendResponse({ success: true, tabId: tab?.id });
|
| 233 |
+
});
|
| 234 |
+
return true;
|
| 235 |
+
}
|
| 236 |
+
|
| 237 |
+
if (message.type === 'OPEN_POPUP') {
|
| 238 |
+
chrome.action.setBadgeText({ text: '1' });
|
| 239 |
+
chrome.action.setBadgeBackgroundColor({ color: '#9333ea' });
|
| 240 |
+
setTimeout(() => chrome.action.setBadgeText({ text: '' }), 5000);
|
| 241 |
+
sendResponse({ success: true });
|
| 242 |
+
return true;
|
| 243 |
+
}
|
| 244 |
+
|
| 245 |
+
if (message.type === 'CAPTURE_PROMPT') {
|
| 246 |
+
// Capture from content script
|
| 247 |
+
(async () => {
|
| 248 |
+
const deviceId = await getDeviceId();
|
| 249 |
+
const userId = await getUserId();
|
| 250 |
+
const platform = detectPlatformFromUrl(sender.tab?.url || '');
|
| 251 |
+
|
| 252 |
+
try {
|
| 253 |
+
const response = await fetch(`${API_BASE}/api/extension/capture`, {
|
| 254 |
+
method: 'POST',
|
| 255 |
+
headers: { 'Content-Type': 'application/json' },
|
| 256 |
+
body: JSON.stringify({
|
| 257 |
+
deviceId,
|
| 258 |
+
userId,
|
| 259 |
+
text: message.text,
|
| 260 |
+
title: message.title,
|
| 261 |
+
platform: platform || 'unknown',
|
| 262 |
+
url: sender.tab?.url,
|
| 263 |
+
}),
|
| 264 |
+
});
|
| 265 |
+
|
| 266 |
+
const data = await response.json();
|
| 267 |
+
sendResponse({ success: true, id: data.id });
|
| 268 |
+
|
| 269 |
+
trackAnalytics({
|
| 270 |
+
action: 'capture',
|
| 271 |
+
platform: platform || 'unknown',
|
| 272 |
+
promptText: message.text?.slice(0, 500),
|
| 273 |
+
});
|
| 274 |
+
} catch (error) {
|
| 275 |
+
sendResponse({ success: false, error: String(error) });
|
| 276 |
+
}
|
| 277 |
+
})();
|
| 278 |
+
return true;
|
| 279 |
+
}
|
| 280 |
+
|
| 281 |
+
if (message.type === 'BROADCAST_PROMPT') {
|
| 282 |
+
// Send to multiple platforms
|
| 283 |
+
const { prompt, platforms: targetPlatforms } = message;
|
| 284 |
+
|
| 285 |
+
(async () => {
|
| 286 |
+
for (const platformId of targetPlatforms) {
|
| 287 |
+
const url = PLATFORMS[platformId];
|
| 288 |
+
if (url) {
|
| 289 |
+
await chrome.storage.local.set({
|
| 290 |
+
[`promptToInject_${platformId}`]: {
|
| 291 |
+
prompt,
|
| 292 |
+
platform: platformId,
|
| 293 |
+
timestamp: Date.now(),
|
| 294 |
+
},
|
| 295 |
+
});
|
| 296 |
+
chrome.tabs.create({ url });
|
| 297 |
+
}
|
| 298 |
+
}
|
| 299 |
+
|
| 300 |
+
trackAnalytics({
|
| 301 |
+
action: 'broadcast',
|
| 302 |
+
platforms: targetPlatforms,
|
| 303 |
+
promptText: prompt?.slice(0, 500),
|
| 304 |
+
});
|
| 305 |
+
|
| 306 |
+
sendResponse({ success: true });
|
| 307 |
+
})();
|
| 308 |
+
return true;
|
| 309 |
+
}
|
| 310 |
+
|
| 311 |
+
if (message.type === 'TRACK_ANALYTICS') {
|
| 312 |
+
trackAnalytics(message.data).then(() => sendResponse({ success: true }));
|
| 313 |
+
return true;
|
| 314 |
+
}
|
| 315 |
+
|
| 316 |
+
if (message.type === 'SET_USER_ID') {
|
| 317 |
+
chrome.storage.local.set({ userId: message.userId }).then(() => {
|
| 318 |
+
sendResponse({ success: true });
|
| 319 |
+
});
|
| 320 |
+
return true;
|
| 321 |
+
}
|
| 322 |
+
|
| 323 |
+
if (message.type === 'GET_DEVICE_ID') {
|
| 324 |
+
getDeviceId().then(deviceId => sendResponse({ deviceId }));
|
| 325 |
+
return true;
|
| 326 |
+
}
|
| 327 |
+
});
|
| 328 |
+
|
| 329 |
+
// Handle external messages from the website
|
| 330 |
+
chrome.runtime.onMessageExternal.addListener(
|
| 331 |
+
(message, sender, sendResponse) => {
|
| 332 |
+
console.log('[OpenPrompt] External message received:', message, 'from:', sender);
|
| 333 |
+
|
| 334 |
+
if (message.type === 'CHECK_EXTENSION') {
|
| 335 |
+
sendResponse({ installed: true, version: chrome.runtime.getManifest().version });
|
| 336 |
+
return true;
|
| 337 |
+
}
|
| 338 |
+
|
| 339 |
+
if (message.type === 'SYNC_USER') {
|
| 340 |
+
chrome.storage.local.set({ userId: message.userId }).then(() => {
|
| 341 |
+
sendResponse({ success: true });
|
| 342 |
+
});
|
| 343 |
+
return true;
|
| 344 |
+
}
|
| 345 |
+
|
| 346 |
+
if (message.type === 'OPEN_IN_AI') {
|
| 347 |
+
chrome.storage.local.set({
|
| 348 |
+
pendingPrompt: {
|
| 349 |
+
prompt: message.prompt,
|
| 350 |
+
title: message.title,
|
| 351 |
+
promptId: message.promptId,
|
| 352 |
+
timestamp: Date.now(),
|
| 353 |
+
}
|
| 354 |
+
}).then(async () => {
|
| 355 |
+
if (message.platform) {
|
| 356 |
+
const url = PLATFORMS[message.platform];
|
| 357 |
+
if (url) {
|
| 358 |
+
await chrome.storage.local.set({
|
| 359 |
+
promptToInject: {
|
| 360 |
+
prompt: message.prompt,
|
| 361 |
+
platform: message.platform,
|
| 362 |
+
timestamp: Date.now(),
|
| 363 |
+
}
|
| 364 |
+
});
|
| 365 |
+
chrome.tabs.create({ url });
|
| 366 |
+
|
| 367 |
+
trackAnalytics({
|
| 368 |
+
action: 'inject',
|
| 369 |
+
platform: message.platform,
|
| 370 |
+
promptId: message.promptId,
|
| 371 |
+
promptTitle: message.title,
|
| 372 |
+
});
|
| 373 |
+
}
|
| 374 |
+
}
|
| 375 |
+
sendResponse({ success: true });
|
| 376 |
+
});
|
| 377 |
+
return true;
|
| 378 |
+
}
|
| 379 |
+
}
|
| 380 |
+
);
|
| 381 |
+
|
| 382 |
+
// Handle installation
|
| 383 |
+
chrome.runtime.onInstalled.addListener((details) => {
|
| 384 |
+
if (details.reason === 'install') {
|
| 385 |
+
console.log('[OpenPrompt] Extension installed');
|
| 386 |
+
createContextMenus();
|
| 387 |
+
chrome.tabs.create({
|
| 388 |
+
url: `${API_BASE}/extension?installed=true`
|
| 389 |
+
});
|
| 390 |
+
|
| 391 |
+
trackAnalytics({
|
| 392 |
+
action: 'install',
|
| 393 |
+
metadata: { version: chrome.runtime.getManifest().version },
|
| 394 |
+
});
|
| 395 |
+
} else if (details.reason === 'update') {
|
| 396 |
+
console.log('[OpenPrompt] Extension updated to version', chrome.runtime.getManifest().version);
|
| 397 |
+
createContextMenus();
|
| 398 |
+
|
| 399 |
+
trackAnalytics({
|
| 400 |
+
action: 'update',
|
| 401 |
+
metadata: { version: chrome.runtime.getManifest().version },
|
| 402 |
+
});
|
| 403 |
+
}
|
| 404 |
+
});
|
| 405 |
+
|
| 406 |
+
// Handle tab updates to inject prompts
|
| 407 |
+
chrome.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {
|
| 408 |
+
if (changeInfo.status === 'complete' && tab.url) {
|
| 409 |
+
// Check for regular prompt injection
|
| 410 |
+
const result = await chrome.storage.local.get(['promptToInject']);
|
| 411 |
+
|
| 412 |
+
if (result.promptToInject) {
|
| 413 |
+
const { platform } = result.promptToInject;
|
| 414 |
+
const urls = PLATFORM_URLS[platform] || [];
|
| 415 |
+
const isMatchingUrl = urls.some(url => tab.url?.includes(url));
|
| 416 |
+
|
| 417 |
+
if (isMatchingUrl) {
|
| 418 |
+
setTimeout(() => {
|
| 419 |
+
chrome.tabs.sendMessage(tabId, { type: 'INJECT_PROMPT' }).catch(() => {});
|
| 420 |
+
}, 1000);
|
| 421 |
+
}
|
| 422 |
+
}
|
| 423 |
+
|
| 424 |
+
// Check for broadcast prompts
|
| 425 |
+
for (const platformId of Object.keys(PLATFORMS)) {
|
| 426 |
+
const broadcastResult = await chrome.storage.local.get([`promptToInject_${platformId}`]);
|
| 427 |
+
const promptData = broadcastResult[`promptToInject_${platformId}`];
|
| 428 |
+
|
| 429 |
+
if (promptData) {
|
| 430 |
+
const urls = PLATFORM_URLS[platformId] || [];
|
| 431 |
+
const isMatchingUrl = urls.some(url => tab.url?.includes(url));
|
| 432 |
+
|
| 433 |
+
if (isMatchingUrl) {
|
| 434 |
+
// Move broadcast prompt to regular prompt slot for injection
|
| 435 |
+
await chrome.storage.local.set({ promptToInject: promptData });
|
| 436 |
+
await chrome.storage.local.remove(`promptToInject_${platformId}`);
|
| 437 |
+
|
| 438 |
+
setTimeout(() => {
|
| 439 |
+
chrome.tabs.sendMessage(tabId, { type: 'INJECT_PROMPT' }).catch(() => {});
|
| 440 |
+
}, 1000);
|
| 441 |
+
}
|
| 442 |
+
}
|
| 443 |
+
}
|
| 444 |
+
}
|
| 445 |
+
});
|
| 446 |
+
|
| 447 |
+
// Initialize context menus on startup
|
| 448 |
+
createContextMenus();
|
| 449 |
+
|
| 450 |
+
console.log('[OpenPrompt] Background service worker started');
|
openprompt-extension/src/content-scripts/chatgpt.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// ChatGPT Content Script
|
| 2 |
+
import {
|
| 3 |
+
getPromptToInject,
|
| 4 |
+
clearPromptToInject,
|
| 5 |
+
waitForElement,
|
| 6 |
+
showNotification,
|
| 7 |
+
setupCaptureListener,
|
| 8 |
+
createCaptureButton
|
| 9 |
+
} from './utils';
|
| 10 |
+
|
| 11 |
+
async function injectPrompt(): Promise<void> {
|
| 12 |
+
const promptData = await getPromptToInject();
|
| 13 |
+
|
| 14 |
+
if (!promptData || promptData.platform !== 'chatgpt') {
|
| 15 |
+
return;
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
console.log('[OpenPrompt] Injecting prompt into ChatGPT...');
|
| 19 |
+
|
| 20 |
+
// Wait for the textarea to be available
|
| 21 |
+
// ChatGPT uses a contenteditable div with id="prompt-textarea"
|
| 22 |
+
const textareaSelector = '#prompt-textarea';
|
| 23 |
+
const textarea = await waitForElement(textareaSelector, 15000);
|
| 24 |
+
|
| 25 |
+
if (!textarea) {
|
| 26 |
+
console.error('[OpenPrompt] Could not find ChatGPT textarea');
|
| 27 |
+
showNotification('Could not find input field. Please try again.', 'error');
|
| 28 |
+
return;
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
try {
|
| 32 |
+
// Focus the textarea
|
| 33 |
+
(textarea as HTMLElement).focus();
|
| 34 |
+
|
| 35 |
+
// Clear existing content
|
| 36 |
+
textarea.innerHTML = '';
|
| 37 |
+
|
| 38 |
+
// Create a paragraph element with the prompt text
|
| 39 |
+
const p = document.createElement('p');
|
| 40 |
+
p.textContent = promptData.prompt;
|
| 41 |
+
textarea.appendChild(p);
|
| 42 |
+
|
| 43 |
+
// Dispatch input event to trigger React state update
|
| 44 |
+
textarea.dispatchEvent(new InputEvent('input', {
|
| 45 |
+
bubbles: true,
|
| 46 |
+
cancelable: true,
|
| 47 |
+
inputType: 'insertText',
|
| 48 |
+
data: promptData.prompt,
|
| 49 |
+
}));
|
| 50 |
+
|
| 51 |
+
// Clear the pending prompt
|
| 52 |
+
await clearPromptToInject();
|
| 53 |
+
|
| 54 |
+
showNotification('✓ Prompt loaded! Press Enter to send.');
|
| 55 |
+
console.log('[OpenPrompt] Prompt injected successfully');
|
| 56 |
+
} catch (error) {
|
| 57 |
+
console.error('[OpenPrompt] Error injecting prompt:', error);
|
| 58 |
+
showNotification('Failed to inject prompt. Please try again.', 'error');
|
| 59 |
+
}
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
// Initialize
|
| 63 |
+
function init(): void {
|
| 64 |
+
// Setup capture listener for keyboard shortcut
|
| 65 |
+
setupCaptureListener();
|
| 66 |
+
|
| 67 |
+
// Add floating capture button
|
| 68 |
+
createCaptureButton('ChatGPT');
|
| 69 |
+
|
| 70 |
+
// Try to inject prompt
|
| 71 |
+
setTimeout(injectPrompt, 1500);
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
// Run on page load
|
| 75 |
+
if (document.readyState === 'loading') {
|
| 76 |
+
document.addEventListener('DOMContentLoaded', init);
|
| 77 |
+
} else {
|
| 78 |
+
init();
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
// Listen for messages from the extension
|
| 82 |
+
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
| 83 |
+
if (message.type === 'INJECT_PROMPT') {
|
| 84 |
+
injectPrompt().then(() => sendResponse({ success: true }));
|
| 85 |
+
return true;
|
| 86 |
+
}
|
| 87 |
+
});
|
openprompt-extension/src/content-scripts/claude.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Claude Content Script
|
| 2 |
+
import {
|
| 3 |
+
getPromptToInject,
|
| 4 |
+
clearPromptToInject,
|
| 5 |
+
waitForElement,
|
| 6 |
+
showNotification,
|
| 7 |
+
setupCaptureListener,
|
| 8 |
+
createCaptureButton
|
| 9 |
+
} from './utils';
|
| 10 |
+
|
| 11 |
+
async function injectPrompt(): Promise<void> {
|
| 12 |
+
const promptData = await getPromptToInject();
|
| 13 |
+
|
| 14 |
+
if (!promptData || promptData.platform !== 'claude') {
|
| 15 |
+
return;
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
console.log('[OpenPrompt] Injecting prompt into Claude...');
|
| 19 |
+
|
| 20 |
+
// Claude uses a contenteditable div with class containing "ProseMirror"
|
| 21 |
+
const textareaSelector = '[contenteditable="true"].ProseMirror, div[contenteditable="true"]';
|
| 22 |
+
const textarea = await waitForElement(textareaSelector, 15000);
|
| 23 |
+
|
| 24 |
+
if (!textarea) {
|
| 25 |
+
console.error('[OpenPrompt] Could not find Claude textarea');
|
| 26 |
+
showNotification('Could not find input field. Please try again.', 'error');
|
| 27 |
+
return;
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
try {
|
| 31 |
+
const element = textarea as HTMLElement;
|
| 32 |
+
|
| 33 |
+
// Focus the element
|
| 34 |
+
element.focus();
|
| 35 |
+
|
| 36 |
+
// Clear existing content
|
| 37 |
+
element.innerHTML = '';
|
| 38 |
+
|
| 39 |
+
// For ProseMirror editors, we need to create a paragraph
|
| 40 |
+
const p = document.createElement('p');
|
| 41 |
+
p.textContent = promptData.prompt;
|
| 42 |
+
element.appendChild(p);
|
| 43 |
+
|
| 44 |
+
// Dispatch input event
|
| 45 |
+
element.dispatchEvent(new InputEvent('input', {
|
| 46 |
+
bubbles: true,
|
| 47 |
+
cancelable: true,
|
| 48 |
+
inputType: 'insertText',
|
| 49 |
+
data: promptData.prompt,
|
| 50 |
+
}));
|
| 51 |
+
|
| 52 |
+
// Clear the pending prompt
|
| 53 |
+
await clearPromptToInject();
|
| 54 |
+
|
| 55 |
+
showNotification('✓ Prompt loaded! Press Enter to send.');
|
| 56 |
+
console.log('[OpenPrompt] Prompt injected successfully');
|
| 57 |
+
} catch (error) {
|
| 58 |
+
console.error('[OpenPrompt] Error injecting prompt:', error);
|
| 59 |
+
showNotification('Failed to inject prompt. Please try again.', 'error');
|
| 60 |
+
}
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
// Initialize
|
| 64 |
+
function init(): void {
|
| 65 |
+
setupCaptureListener();
|
| 66 |
+
createCaptureButton('Claude');
|
| 67 |
+
setTimeout(injectPrompt, 2000);
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
// Run on page load
|
| 71 |
+
if (document.readyState === 'loading') {
|
| 72 |
+
document.addEventListener('DOMContentLoaded', init);
|
| 73 |
+
} else {
|
| 74 |
+
init();
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
// Listen for messages from the extension
|
| 78 |
+
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
| 79 |
+
if (message.type === 'INJECT_PROMPT') {
|
| 80 |
+
injectPrompt().then(() => sendResponse({ success: true }));
|
| 81 |
+
return true;
|
| 82 |
+
}
|
| 83 |
+
});
|
openprompt-extension/src/content-scripts/copilot.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Microsoft Copilot Content Script
|
| 2 |
+
import {
|
| 3 |
+
getPromptToInject,
|
| 4 |
+
clearPromptToInject,
|
| 5 |
+
waitForElement,
|
| 6 |
+
showNotification,
|
| 7 |
+
setupCaptureListener,
|
| 8 |
+
createCaptureButton
|
| 9 |
+
} from './utils';
|
| 10 |
+
|
| 11 |
+
async function injectPrompt(): Promise<void> {
|
| 12 |
+
const promptData = await getPromptToInject();
|
| 13 |
+
|
| 14 |
+
if (!promptData || promptData.platform !== 'copilot') {
|
| 15 |
+
return;
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
console.log('[OpenPrompt] Injecting prompt into Copilot...');
|
| 19 |
+
|
| 20 |
+
// Copilot uses a textarea
|
| 21 |
+
const textareaSelector = 'textarea#userInput, textarea[name="searchbox"], textarea';
|
| 22 |
+
const textarea = await waitForElement(textareaSelector, 15000);
|
| 23 |
+
|
| 24 |
+
if (!textarea) {
|
| 25 |
+
console.error('[OpenPrompt] Could not find Copilot textarea');
|
| 26 |
+
showNotification('Could not find input field. Please try again.', 'error');
|
| 27 |
+
return;
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
try {
|
| 31 |
+
const element = textarea as HTMLTextAreaElement;
|
| 32 |
+
|
| 33 |
+
// Focus and set value
|
| 34 |
+
element.focus();
|
| 35 |
+
element.value = promptData.prompt;
|
| 36 |
+
|
| 37 |
+
// Dispatch events
|
| 38 |
+
element.dispatchEvent(new Event('input', { bubbles: true }));
|
| 39 |
+
element.dispatchEvent(new Event('change', { bubbles: true }));
|
| 40 |
+
|
| 41 |
+
// Clear the pending prompt
|
| 42 |
+
await clearPromptToInject();
|
| 43 |
+
|
| 44 |
+
showNotification('✓ Prompt loaded! Press Enter to send.');
|
| 45 |
+
console.log('[OpenPrompt] Prompt injected successfully');
|
| 46 |
+
} catch (error) {
|
| 47 |
+
console.error('[OpenPrompt] Error injecting prompt:', error);
|
| 48 |
+
showNotification('Failed to inject prompt. Please try again.', 'error');
|
| 49 |
+
}
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
// Initialize
|
| 53 |
+
function init(): void {
|
| 54 |
+
setupCaptureListener();
|
| 55 |
+
createCaptureButton('Copilot');
|
| 56 |
+
setTimeout(injectPrompt, 2000);
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
// Run on page load
|
| 60 |
+
if (document.readyState === 'loading') {
|
| 61 |
+
document.addEventListener('DOMContentLoaded', init);
|
| 62 |
+
} else {
|
| 63 |
+
init();
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
// Listen for messages from the extension
|
| 67 |
+
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
| 68 |
+
if (message.type === 'INJECT_PROMPT') {
|
| 69 |
+
injectPrompt().then(() => sendResponse({ success: true }));
|
| 70 |
+
return true;
|
| 71 |
+
}
|
| 72 |
+
});
|
openprompt-extension/src/content-scripts/gemini.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Gemini Content Script
|
| 2 |
+
import {
|
| 3 |
+
getPromptToInject,
|
| 4 |
+
clearPromptToInject,
|
| 5 |
+
waitForElement,
|
| 6 |
+
showNotification,
|
| 7 |
+
setupCaptureListener,
|
| 8 |
+
createCaptureButton
|
| 9 |
+
} from './utils';
|
| 10 |
+
|
| 11 |
+
async function injectPrompt(): Promise<void> {
|
| 12 |
+
const promptData = await getPromptToInject();
|
| 13 |
+
|
| 14 |
+
if (!promptData || promptData.platform !== 'gemini') {
|
| 15 |
+
return;
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
console.log('[OpenPrompt] Injecting prompt into Gemini...');
|
| 19 |
+
|
| 20 |
+
// Gemini uses a rich text input with contenteditable
|
| 21 |
+
const textareaSelector = '.ql-editor, [contenteditable="true"], textarea';
|
| 22 |
+
const textarea = await waitForElement(textareaSelector, 15000);
|
| 23 |
+
|
| 24 |
+
if (!textarea) {
|
| 25 |
+
console.error('[OpenPrompt] Could not find Gemini textarea');
|
| 26 |
+
showNotification('Could not find input field. Please try again.', 'error');
|
| 27 |
+
return;
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
try {
|
| 31 |
+
const element = textarea as HTMLElement;
|
| 32 |
+
|
| 33 |
+
// Focus the element
|
| 34 |
+
element.focus();
|
| 35 |
+
|
| 36 |
+
if (element.getAttribute('contenteditable') === 'true') {
|
| 37 |
+
// For contenteditable elements
|
| 38 |
+
element.innerHTML = '';
|
| 39 |
+
|
| 40 |
+
const p = document.createElement('p');
|
| 41 |
+
p.textContent = promptData.prompt;
|
| 42 |
+
element.appendChild(p);
|
| 43 |
+
|
| 44 |
+
element.dispatchEvent(new InputEvent('input', {
|
| 45 |
+
bubbles: true,
|
| 46 |
+
cancelable: true,
|
| 47 |
+
inputType: 'insertText',
|
| 48 |
+
data: promptData.prompt,
|
| 49 |
+
}));
|
| 50 |
+
} else if (element instanceof HTMLTextAreaElement) {
|
| 51 |
+
// For textarea elements
|
| 52 |
+
element.value = promptData.prompt;
|
| 53 |
+
element.dispatchEvent(new Event('input', { bubbles: true }));
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
// Clear the pending prompt
|
| 57 |
+
await clearPromptToInject();
|
| 58 |
+
|
| 59 |
+
showNotification('✓ Prompt loaded! Press Enter to send.');
|
| 60 |
+
console.log('[OpenPrompt] Prompt injected successfully');
|
| 61 |
+
} catch (error) {
|
| 62 |
+
console.error('[OpenPrompt] Error injecting prompt:', error);
|
| 63 |
+
showNotification('Failed to inject prompt. Please try again.', 'error');
|
| 64 |
+
}
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
// Initialize
|
| 68 |
+
function init(): void {
|
| 69 |
+
setupCaptureListener();
|
| 70 |
+
createCaptureButton('Gemini');
|
| 71 |
+
setTimeout(injectPrompt, 2000);
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
// Run on page load
|
| 75 |
+
if (document.readyState === 'loading') {
|
| 76 |
+
document.addEventListener('DOMContentLoaded', init);
|
| 77 |
+
} else {
|
| 78 |
+
init();
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
// Listen for messages from the extension
|
| 82 |
+
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
| 83 |
+
if (message.type === 'INJECT_PROMPT') {
|
| 84 |
+
injectPrompt().then(() => sendResponse({ success: true }));
|
| 85 |
+
return true;
|
| 86 |
+
}
|
| 87 |
+
});
|
openprompt-extension/src/content-scripts/mistral.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Mistral Content Script
|
| 2 |
+
import {
|
| 3 |
+
getPromptToInject,
|
| 4 |
+
clearPromptToInject,
|
| 5 |
+
waitForElement,
|
| 6 |
+
showNotification,
|
| 7 |
+
setupCaptureListener,
|
| 8 |
+
createCaptureButton
|
| 9 |
+
} from './utils';
|
| 10 |
+
|
| 11 |
+
async function injectPrompt(): Promise<void> {
|
| 12 |
+
const promptData = await getPromptToInject();
|
| 13 |
+
|
| 14 |
+
if (!promptData || promptData.platform !== 'mistral') {
|
| 15 |
+
return;
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
console.log('[OpenPrompt] Injecting prompt into Mistral...');
|
| 19 |
+
|
| 20 |
+
// Mistral uses a textarea or contenteditable
|
| 21 |
+
const textareaSelector = 'textarea, [contenteditable="true"]';
|
| 22 |
+
const textarea = await waitForElement(textareaSelector, 15000);
|
| 23 |
+
|
| 24 |
+
if (!textarea) {
|
| 25 |
+
console.error('[OpenPrompt] Could not find Mistral textarea');
|
| 26 |
+
showNotification('Could not find input field. Please try again.', 'error');
|
| 27 |
+
return;
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
try {
|
| 31 |
+
const element = textarea as HTMLElement;
|
| 32 |
+
element.focus();
|
| 33 |
+
|
| 34 |
+
if (element instanceof HTMLTextAreaElement) {
|
| 35 |
+
element.value = promptData.prompt;
|
| 36 |
+
element.dispatchEvent(new Event('input', { bubbles: true }));
|
| 37 |
+
element.dispatchEvent(new Event('change', { bubbles: true }));
|
| 38 |
+
} else if (element.getAttribute('contenteditable') === 'true') {
|
| 39 |
+
element.innerHTML = '';
|
| 40 |
+
const p = document.createElement('p');
|
| 41 |
+
p.textContent = promptData.prompt;
|
| 42 |
+
element.appendChild(p);
|
| 43 |
+
|
| 44 |
+
element.dispatchEvent(new InputEvent('input', {
|
| 45 |
+
bubbles: true,
|
| 46 |
+
cancelable: true,
|
| 47 |
+
inputType: 'insertText',
|
| 48 |
+
data: promptData.prompt,
|
| 49 |
+
}));
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
// Clear the pending prompt
|
| 53 |
+
await clearPromptToInject();
|
| 54 |
+
|
| 55 |
+
showNotification('✓ Prompt loaded! Press Enter to send.');
|
| 56 |
+
console.log('[OpenPrompt] Prompt injected successfully');
|
| 57 |
+
} catch (error) {
|
| 58 |
+
console.error('[OpenPrompt] Error injecting prompt:', error);
|
| 59 |
+
showNotification('Failed to inject prompt. Please try again.', 'error');
|
| 60 |
+
}
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
// Initialize
|
| 64 |
+
function init(): void {
|
| 65 |
+
setupCaptureListener();
|
| 66 |
+
createCaptureButton('Mistral');
|
| 67 |
+
setTimeout(injectPrompt, 1500);
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
// Run on page load
|
| 71 |
+
if (document.readyState === 'loading') {
|
| 72 |
+
document.addEventListener('DOMContentLoaded', init);
|
| 73 |
+
} else {
|
| 74 |
+
init();
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
// Listen for messages from the extension
|
| 78 |
+
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
| 79 |
+
if (message.type === 'INJECT_PROMPT') {
|
| 80 |
+
injectPrompt().then(() => sendResponse({ success: true }));
|
| 81 |
+
return true;
|
| 82 |
+
}
|
| 83 |
+
});
|
openprompt-extension/src/content-scripts/openprompt-site.ts
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// OpenPrompt Website Content Script
|
| 2 |
+
// This script runs on the OpenPrompt website to enable communication with the extension
|
| 3 |
+
|
| 4 |
+
interface OpenInMessage {
|
| 5 |
+
type: 'OPEN_IN_AI';
|
| 6 |
+
prompt: string;
|
| 7 |
+
title?: string;
|
| 8 |
+
platform?: string;
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
// Helper to check if extension context is still valid
|
| 12 |
+
function isExtensionContextValid(): boolean {
|
| 13 |
+
try {
|
| 14 |
+
// This will throw if context is invalidated
|
| 15 |
+
return !!chrome.runtime?.id;
|
| 16 |
+
} catch {
|
| 17 |
+
return false;
|
| 18 |
+
}
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
// Safely call chrome APIs with error handling
|
| 22 |
+
async function safeStorageSet(data: Record<string, unknown>): Promise<boolean> {
|
| 23 |
+
try {
|
| 24 |
+
if (!isExtensionContextValid()) return false;
|
| 25 |
+
await chrome.storage.local.set(data);
|
| 26 |
+
return true;
|
| 27 |
+
} catch (error) {
|
| 28 |
+
console.warn('[OpenPrompt Extension] Storage error (extension may need reload):', error);
|
| 29 |
+
return false;
|
| 30 |
+
}
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
function safeSendMessage(message: unknown): void {
|
| 34 |
+
try {
|
| 35 |
+
if (!isExtensionContextValid()) return;
|
| 36 |
+
chrome.runtime.sendMessage(message);
|
| 37 |
+
} catch (error) {
|
| 38 |
+
console.warn('[OpenPrompt Extension] Message error (extension may need reload):', error);
|
| 39 |
+
}
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
// Listen for messages from the website
|
| 43 |
+
window.addEventListener('message', async (event) => {
|
| 44 |
+
// Only accept messages from the same origin
|
| 45 |
+
if (event.origin !== window.location.origin) {
|
| 46 |
+
return;
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
const message = event.data as OpenInMessage;
|
| 50 |
+
|
| 51 |
+
if (message.type === 'OPEN_IN_AI') {
|
| 52 |
+
console.log('[OpenPrompt Extension] Received prompt from website:', message);
|
| 53 |
+
|
| 54 |
+
// Check if extension context is still valid
|
| 55 |
+
if (!isExtensionContextValid()) {
|
| 56 |
+
console.warn('[OpenPrompt Extension] Extension context invalidated. Please refresh the page.');
|
| 57 |
+
window.postMessage({
|
| 58 |
+
type: 'OPEN_IN_AI_RECEIVED',
|
| 59 |
+
success: false,
|
| 60 |
+
error: 'Extension needs to be reloaded. Please refresh the page.',
|
| 61 |
+
}, '*');
|
| 62 |
+
return;
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
// Store the prompt for the popup or direct injection
|
| 66 |
+
await safeStorageSet({
|
| 67 |
+
pendingPrompt: {
|
| 68 |
+
prompt: message.prompt,
|
| 69 |
+
title: message.title,
|
| 70 |
+
timestamp: Date.now(),
|
| 71 |
+
}
|
| 72 |
+
});
|
| 73 |
+
|
| 74 |
+
// If a specific platform is requested, open it directly
|
| 75 |
+
if (message.platform) {
|
| 76 |
+
const platforms: Record<string, string> = {
|
| 77 |
+
chatgpt: 'https://chatgpt.com',
|
| 78 |
+
claude: 'https://claude.ai',
|
| 79 |
+
gemini: 'https://gemini.google.com',
|
| 80 |
+
perplexity: 'https://www.perplexity.ai',
|
| 81 |
+
mistral: 'https://chat.mistral.ai',
|
| 82 |
+
copilot: 'https://copilot.microsoft.com',
|
| 83 |
+
};
|
| 84 |
+
|
| 85 |
+
const url = platforms[message.platform];
|
| 86 |
+
if (url) {
|
| 87 |
+
await safeStorageSet({
|
| 88 |
+
promptToInject: {
|
| 89 |
+
prompt: message.prompt,
|
| 90 |
+
platform: message.platform,
|
| 91 |
+
timestamp: Date.now(),
|
| 92 |
+
}
|
| 93 |
+
});
|
| 94 |
+
|
| 95 |
+
safeSendMessage({
|
| 96 |
+
type: 'OPEN_TAB',
|
| 97 |
+
url: url,
|
| 98 |
+
});
|
| 99 |
+
}
|
| 100 |
+
} else {
|
| 101 |
+
// Open the extension popup to let user choose
|
| 102 |
+
safeSendMessage({ type: 'OPEN_POPUP' });
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
// Send confirmation back to the website
|
| 106 |
+
window.postMessage({
|
| 107 |
+
type: 'OPEN_IN_AI_RECEIVED',
|
| 108 |
+
success: true,
|
| 109 |
+
}, '*');
|
| 110 |
+
}
|
| 111 |
+
});
|
| 112 |
+
|
| 113 |
+
// Inject a script to add the extension detection
|
| 114 |
+
// We use postMessage instead of inline script to avoid CSP issues
|
| 115 |
+
window.postMessage({ type: 'OPENPROMPT_EXTENSION_READY' }, '*');
|
| 116 |
+
|
| 117 |
+
// Also set a data attribute on the body
|
| 118 |
+
document.body.setAttribute('data-openprompt-extension', 'true');
|
| 119 |
+
|
| 120 |
+
console.log('[OpenPrompt Extension] Content script loaded on OpenPrompt website');
|
openprompt-extension/src/content-scripts/perplexity.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Perplexity Content Script
|
| 2 |
+
import {
|
| 3 |
+
getPromptToInject,
|
| 4 |
+
clearPromptToInject,
|
| 5 |
+
waitForElement,
|
| 6 |
+
showNotification,
|
| 7 |
+
setupCaptureListener,
|
| 8 |
+
createCaptureButton
|
| 9 |
+
} from './utils';
|
| 10 |
+
|
| 11 |
+
async function injectPrompt(): Promise<void> {
|
| 12 |
+
const promptData = await getPromptToInject();
|
| 13 |
+
|
| 14 |
+
if (!promptData || promptData.platform !== 'perplexity') {
|
| 15 |
+
return;
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
console.log('[OpenPrompt] Injecting prompt into Perplexity...');
|
| 19 |
+
|
| 20 |
+
// Perplexity uses a textarea
|
| 21 |
+
const textareaSelector = 'textarea[placeholder*="Ask"], textarea';
|
| 22 |
+
const textarea = await waitForElement(textareaSelector, 15000);
|
| 23 |
+
|
| 24 |
+
if (!textarea) {
|
| 25 |
+
console.error('[OpenPrompt] Could not find Perplexity textarea');
|
| 26 |
+
showNotification('Could not find input field. Please try again.', 'error');
|
| 27 |
+
return;
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
try {
|
| 31 |
+
const element = textarea as HTMLTextAreaElement;
|
| 32 |
+
|
| 33 |
+
// Focus and set value
|
| 34 |
+
element.focus();
|
| 35 |
+
element.value = promptData.prompt;
|
| 36 |
+
|
| 37 |
+
// Dispatch events for React
|
| 38 |
+
element.dispatchEvent(new Event('input', { bubbles: true }));
|
| 39 |
+
element.dispatchEvent(new Event('change', { bubbles: true }));
|
| 40 |
+
|
| 41 |
+
// For React synthetic events
|
| 42 |
+
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
|
| 43 |
+
HTMLTextAreaElement.prototype,
|
| 44 |
+
'value'
|
| 45 |
+
)?.set;
|
| 46 |
+
|
| 47 |
+
if (nativeInputValueSetter) {
|
| 48 |
+
nativeInputValueSetter.call(element, promptData.prompt);
|
| 49 |
+
element.dispatchEvent(new Event('input', { bubbles: true }));
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
// Clear the pending prompt
|
| 53 |
+
await clearPromptToInject();
|
| 54 |
+
|
| 55 |
+
showNotification('✓ Prompt loaded! Press Enter to send.');
|
| 56 |
+
console.log('[OpenPrompt] Prompt injected successfully');
|
| 57 |
+
} catch (error) {
|
| 58 |
+
console.error('[OpenPrompt] Error injecting prompt:', error);
|
| 59 |
+
showNotification('Failed to inject prompt. Please try again.', 'error');
|
| 60 |
+
}
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
// Initialize
|
| 64 |
+
function init(): void {
|
| 65 |
+
setupCaptureListener();
|
| 66 |
+
createCaptureButton('Perplexity');
|
| 67 |
+
setTimeout(injectPrompt, 1500);
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
// Run on page load
|
| 71 |
+
if (document.readyState === 'loading') {
|
| 72 |
+
document.addEventListener('DOMContentLoaded', init);
|
| 73 |
+
} else {
|
| 74 |
+
init();
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
// Listen for messages from the extension
|
| 78 |
+
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
| 79 |
+
if (message.type === 'INJECT_PROMPT') {
|
| 80 |
+
injectPrompt().then(() => sendResponse({ success: true }));
|
| 81 |
+
return true;
|
| 82 |
+
}
|
| 83 |
+
});
|
openprompt-extension/src/content-scripts/utils.ts
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Shared utilities for content scripts
|
| 2 |
+
|
| 3 |
+
export interface PromptToInject {
|
| 4 |
+
prompt: string;
|
| 5 |
+
platform: string;
|
| 6 |
+
timestamp: number;
|
| 7 |
+
}
|
| 8 |
+
|
| 9 |
+
export async function getPromptToInject(): Promise<PromptToInject | null> {
|
| 10 |
+
return new Promise((resolve) => {
|
| 11 |
+
chrome.storage.local.get(['promptToInject'], (result) => {
|
| 12 |
+
if (result.promptToInject) {
|
| 13 |
+
// Check if prompt is recent (within last 30 seconds)
|
| 14 |
+
const isRecent = Date.now() - result.promptToInject.timestamp < 30000;
|
| 15 |
+
if (isRecent) {
|
| 16 |
+
resolve(result.promptToInject);
|
| 17 |
+
} else {
|
| 18 |
+
// Clear stale prompt
|
| 19 |
+
chrome.storage.local.remove('promptToInject');
|
| 20 |
+
resolve(null);
|
| 21 |
+
}
|
| 22 |
+
} else {
|
| 23 |
+
resolve(null);
|
| 24 |
+
}
|
| 25 |
+
});
|
| 26 |
+
});
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
export async function clearPromptToInject(): Promise<void> {
|
| 30 |
+
return new Promise((resolve) => {
|
| 31 |
+
chrome.storage.local.remove('promptToInject', () => resolve());
|
| 32 |
+
});
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
export function waitForElement(
|
| 36 |
+
selector: string,
|
| 37 |
+
timeout = 10000
|
| 38 |
+
): Promise<Element | null> {
|
| 39 |
+
return new Promise((resolve) => {
|
| 40 |
+
const element = document.querySelector(selector);
|
| 41 |
+
if (element) {
|
| 42 |
+
resolve(element);
|
| 43 |
+
return;
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
const observer = new MutationObserver(() => {
|
| 47 |
+
const element = document.querySelector(selector);
|
| 48 |
+
if (element) {
|
| 49 |
+
observer.disconnect();
|
| 50 |
+
resolve(element);
|
| 51 |
+
}
|
| 52 |
+
});
|
| 53 |
+
|
| 54 |
+
observer.observe(document.body, {
|
| 55 |
+
childList: true,
|
| 56 |
+
subtree: true,
|
| 57 |
+
});
|
| 58 |
+
|
| 59 |
+
setTimeout(() => {
|
| 60 |
+
observer.disconnect();
|
| 61 |
+
resolve(null);
|
| 62 |
+
}, timeout);
|
| 63 |
+
});
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
export function simulateInput(element: HTMLElement, text: string): void {
|
| 67 |
+
// For contenteditable elements
|
| 68 |
+
if (element.getAttribute('contenteditable') === 'true') {
|
| 69 |
+
element.focus();
|
| 70 |
+
element.innerHTML = '';
|
| 71 |
+
|
| 72 |
+
// Use execCommand for better compatibility
|
| 73 |
+
document.execCommand('insertText', false, text);
|
| 74 |
+
|
| 75 |
+
// Dispatch input event
|
| 76 |
+
element.dispatchEvent(new InputEvent('input', {
|
| 77 |
+
bubbles: true,
|
| 78 |
+
cancelable: true,
|
| 79 |
+
inputType: 'insertText',
|
| 80 |
+
data: text,
|
| 81 |
+
}));
|
| 82 |
+
}
|
| 83 |
+
// For textarea/input elements
|
| 84 |
+
else if (element instanceof HTMLTextAreaElement || element instanceof HTMLInputElement) {
|
| 85 |
+
element.focus();
|
| 86 |
+
element.value = text;
|
| 87 |
+
|
| 88 |
+
// Dispatch events to trigger React state updates
|
| 89 |
+
element.dispatchEvent(new Event('input', { bubbles: true }));
|
| 90 |
+
element.dispatchEvent(new Event('change', { bubbles: true }));
|
| 91 |
+
|
| 92 |
+
// For React 17+ synthetic events
|
| 93 |
+
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
|
| 94 |
+
element instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype,
|
| 95 |
+
'value'
|
| 96 |
+
)?.set;
|
| 97 |
+
|
| 98 |
+
if (nativeInputValueSetter) {
|
| 99 |
+
nativeInputValueSetter.call(element, text);
|
| 100 |
+
element.dispatchEvent(new Event('input', { bubbles: true }));
|
| 101 |
+
}
|
| 102 |
+
}
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
export function showNotification(message: string, type: 'success' | 'error' = 'success'): void {
|
| 106 |
+
const notification = document.createElement('div');
|
| 107 |
+
notification.style.cssText = `
|
| 108 |
+
position: fixed;
|
| 109 |
+
top: 20px;
|
| 110 |
+
right: 20px;
|
| 111 |
+
padding: 12px 20px;
|
| 112 |
+
border-radius: 8px;
|
| 113 |
+
background: ${type === 'success' ? 'linear-gradient(135deg, #10b981, #059669)' : 'linear-gradient(135deg, #ef4444, #dc2626)'};
|
| 114 |
+
color: white;
|
| 115 |
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
| 116 |
+
font-size: 14px;
|
| 117 |
+
font-weight: 500;
|
| 118 |
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
| 119 |
+
z-index: 999999;
|
| 120 |
+
animation: slideIn 0.3s ease;
|
| 121 |
+
`;
|
| 122 |
+
|
| 123 |
+
notification.textContent = message;
|
| 124 |
+
|
| 125 |
+
// Add animation keyframes
|
| 126 |
+
const style = document.createElement('style');
|
| 127 |
+
style.textContent = `
|
| 128 |
+
@keyframes slideIn {
|
| 129 |
+
from {
|
| 130 |
+
transform: translateX(100%);
|
| 131 |
+
opacity: 0;
|
| 132 |
+
}
|
| 133 |
+
to {
|
| 134 |
+
transform: translateX(0);
|
| 135 |
+
opacity: 1;
|
| 136 |
+
}
|
| 137 |
+
}
|
| 138 |
+
`;
|
| 139 |
+
document.head.appendChild(style);
|
| 140 |
+
document.body.appendChild(notification);
|
| 141 |
+
|
| 142 |
+
setTimeout(() => {
|
| 143 |
+
notification.style.animation = 'slideIn 0.3s ease reverse';
|
| 144 |
+
setTimeout(() => {
|
| 145 |
+
notification.remove();
|
| 146 |
+
style.remove();
|
| 147 |
+
}, 300);
|
| 148 |
+
}, 3000);
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
// Capture selected text
|
| 152 |
+
export function getSelectedText(): string {
|
| 153 |
+
const selection = window.getSelection();
|
| 154 |
+
return selection ? selection.toString().trim() : '';
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
// Handle capture selection command from background
|
| 158 |
+
export function setupCaptureListener(): void {
|
| 159 |
+
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
| 160 |
+
if (message.type === 'CAPTURE_SELECTION') {
|
| 161 |
+
const text = getSelectedText();
|
| 162 |
+
if (text) {
|
| 163 |
+
chrome.runtime.sendMessage({
|
| 164 |
+
type: 'CAPTURE_PROMPT',
|
| 165 |
+
text,
|
| 166 |
+
title: document.title,
|
| 167 |
+
}).then(() => {
|
| 168 |
+
showNotification('✓ Prompt captured!');
|
| 169 |
+
}).catch(() => {
|
| 170 |
+
showNotification('Failed to capture prompt', 'error');
|
| 171 |
+
});
|
| 172 |
+
} else {
|
| 173 |
+
showNotification('No text selected', 'error');
|
| 174 |
+
}
|
| 175 |
+
sendResponse({ success: true });
|
| 176 |
+
return true;
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
if (message.type === 'SHOW_NOTIFICATION') {
|
| 180 |
+
showNotification(message.message, message.type || 'success');
|
| 181 |
+
sendResponse({ success: true });
|
| 182 |
+
return true;
|
| 183 |
+
}
|
| 184 |
+
});
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
// Create floating capture button for AI chat pages
|
| 188 |
+
export function createCaptureButton(platform: string): void {
|
| 189 |
+
// Remove existing button
|
| 190 |
+
const existing = document.getElementById('openprompt-capture-btn');
|
| 191 |
+
if (existing) existing.remove();
|
| 192 |
+
|
| 193 |
+
const button = document.createElement('button');
|
| 194 |
+
button.id = 'openprompt-capture-btn';
|
| 195 |
+
button.innerHTML = `
|
| 196 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
| 197 |
+
<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/>
|
| 198 |
+
<polyline points="17 21 17 13 7 13 7 21"/>
|
| 199 |
+
<polyline points="7 3 7 8 15 8"/>
|
| 200 |
+
</svg>
|
| 201 |
+
<span>Save to OpenPrompt</span>
|
| 202 |
+
`;
|
| 203 |
+
button.style.cssText = `
|
| 204 |
+
position: fixed;
|
| 205 |
+
bottom: 20px;
|
| 206 |
+
right: 20px;
|
| 207 |
+
display: none;
|
| 208 |
+
align-items: center;
|
| 209 |
+
gap: 8px;
|
| 210 |
+
padding: 10px 16px;
|
| 211 |
+
border: none;
|
| 212 |
+
border-radius: 8px;
|
| 213 |
+
background: linear-gradient(135deg, #9333ea, #db2777);
|
| 214 |
+
color: white;
|
| 215 |
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
| 216 |
+
font-size: 14px;
|
| 217 |
+
font-weight: 500;
|
| 218 |
+
cursor: pointer;
|
| 219 |
+
box-shadow: 0 4px 12px rgba(147, 51, 234, 0.4);
|
| 220 |
+
z-index: 999998;
|
| 221 |
+
transition: transform 0.2s, box-shadow 0.2s;
|
| 222 |
+
`;
|
| 223 |
+
|
| 224 |
+
button.addEventListener('mouseenter', () => {
|
| 225 |
+
button.style.transform = 'scale(1.05)';
|
| 226 |
+
button.style.boxShadow = '0 6px 16px rgba(147, 51, 234, 0.5)';
|
| 227 |
+
});
|
| 228 |
+
|
| 229 |
+
button.addEventListener('mouseleave', () => {
|
| 230 |
+
button.style.transform = 'scale(1)';
|
| 231 |
+
button.style.boxShadow = '0 4px 12px rgba(147, 51, 234, 0.4)';
|
| 232 |
+
});
|
| 233 |
+
|
| 234 |
+
button.addEventListener('click', () => {
|
| 235 |
+
const text = getSelectedText();
|
| 236 |
+
if (text) {
|
| 237 |
+
chrome.runtime.sendMessage({
|
| 238 |
+
type: 'CAPTURE_PROMPT',
|
| 239 |
+
text,
|
| 240 |
+
title: `Captured from ${platform}`,
|
| 241 |
+
}).then(() => {
|
| 242 |
+
showNotification('✓ Prompt saved to OpenPrompt!');
|
| 243 |
+
}).catch(() => {
|
| 244 |
+
showNotification('Failed to save prompt', 'error');
|
| 245 |
+
});
|
| 246 |
+
} else {
|
| 247 |
+
showNotification('Select text to capture', 'error');
|
| 248 |
+
}
|
| 249 |
+
});
|
| 250 |
+
|
| 251 |
+
document.body.appendChild(button);
|
| 252 |
+
|
| 253 |
+
// Show/hide based on selection
|
| 254 |
+
document.addEventListener('selectionchange', () => {
|
| 255 |
+
const text = getSelectedText();
|
| 256 |
+
button.style.display = text.length > 10 ? 'flex' : 'none';
|
| 257 |
+
});
|
| 258 |
+
}
|
openprompt-extension/src/popup/App.tsx
ADDED
|
@@ -0,0 +1,743 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useState, useEffect, useMemo } from 'react';
|
| 2 |
+
import {
|
| 3 |
+
Sparkles,
|
| 4 |
+
ExternalLink,
|
| 5 |
+
Settings,
|
| 6 |
+
History,
|
| 7 |
+
Copy,
|
| 8 |
+
Check,
|
| 9 |
+
ChevronRight,
|
| 10 |
+
Zap,
|
| 11 |
+
Search,
|
| 12 |
+
BarChart3,
|
| 13 |
+
Send,
|
| 14 |
+
Layers,
|
| 15 |
+
X,
|
| 16 |
+
Keyboard,
|
| 17 |
+
Save,
|
| 18 |
+
Bot,
|
| 19 |
+
Brain,
|
| 20 |
+
Wind,
|
| 21 |
+
Rocket,
|
| 22 |
+
FileText
|
| 23 |
+
} from 'lucide-react';
|
| 24 |
+
|
| 25 |
+
const API_BASE = 'https://open-prompt.netlify.app';
|
| 26 |
+
// const API_BASE = 'http://localhost:3000'; // For development
|
| 27 |
+
|
| 28 |
+
// Icon components for platforms
|
| 29 |
+
const PlatformIcons: Record<string, typeof Bot> = {
|
| 30 |
+
chatgpt: Bot,
|
| 31 |
+
claude: Brain,
|
| 32 |
+
gemini: Sparkles,
|
| 33 |
+
perplexity: Search,
|
| 34 |
+
mistral: Wind,
|
| 35 |
+
copilot: Rocket,
|
| 36 |
+
};
|
| 37 |
+
|
| 38 |
+
// Supported AI platforms
|
| 39 |
+
const AI_PLATFORMS = [
|
| 40 |
+
{
|
| 41 |
+
id: 'chatgpt',
|
| 42 |
+
name: 'ChatGPT',
|
| 43 |
+
url: 'https://chatgpt.com',
|
| 44 |
+
color: 'from-green-500 to-emerald-600',
|
| 45 |
+
bgColor: 'bg-green-500/10',
|
| 46 |
+
borderColor: 'border-green-500/30',
|
| 47 |
+
},
|
| 48 |
+
{
|
| 49 |
+
id: 'claude',
|
| 50 |
+
name: 'Claude',
|
| 51 |
+
url: 'https://claude.ai',
|
| 52 |
+
color: 'from-orange-500 to-amber-600',
|
| 53 |
+
bgColor: 'bg-orange-500/10',
|
| 54 |
+
borderColor: 'border-orange-500/30',
|
| 55 |
+
},
|
| 56 |
+
{
|
| 57 |
+
id: 'gemini',
|
| 58 |
+
name: 'Gemini',
|
| 59 |
+
url: 'https://gemini.google.com',
|
| 60 |
+
color: 'from-blue-500 to-indigo-600',
|
| 61 |
+
bgColor: 'bg-blue-500/10',
|
| 62 |
+
borderColor: 'border-blue-500/30',
|
| 63 |
+
},
|
| 64 |
+
{
|
| 65 |
+
id: 'perplexity',
|
| 66 |
+
name: 'Perplexity',
|
| 67 |
+
url: 'https://www.perplexity.ai',
|
| 68 |
+
color: 'from-cyan-500 to-teal-600',
|
| 69 |
+
bgColor: 'bg-cyan-500/10',
|
| 70 |
+
borderColor: 'border-cyan-500/30',
|
| 71 |
+
},
|
| 72 |
+
{
|
| 73 |
+
id: 'mistral',
|
| 74 |
+
name: 'Mistral',
|
| 75 |
+
url: 'https://chat.mistral.ai',
|
| 76 |
+
color: 'from-purple-500 to-violet-600',
|
| 77 |
+
bgColor: 'bg-purple-500/10',
|
| 78 |
+
borderColor: 'border-purple-500/30',
|
| 79 |
+
},
|
| 80 |
+
{
|
| 81 |
+
id: 'copilot',
|
| 82 |
+
name: 'Copilot',
|
| 83 |
+
url: 'https://copilot.microsoft.com',
|
| 84 |
+
color: 'from-sky-500 to-blue-600',
|
| 85 |
+
bgColor: 'bg-sky-500/10',
|
| 86 |
+
borderColor: 'border-sky-500/30',
|
| 87 |
+
},
|
| 88 |
+
];
|
| 89 |
+
|
| 90 |
+
interface PendingPrompt {
|
| 91 |
+
prompt: string;
|
| 92 |
+
title?: string;
|
| 93 |
+
promptId?: string;
|
| 94 |
+
timestamp: number;
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
interface RecentPrompt {
|
| 98 |
+
prompt: string;
|
| 99 |
+
title?: string;
|
| 100 |
+
platform: string;
|
| 101 |
+
timestamp: number;
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
interface CapturedPrompt {
|
| 105 |
+
id: string;
|
| 106 |
+
text: string;
|
| 107 |
+
title?: string;
|
| 108 |
+
platform: string;
|
| 109 |
+
createdAt: string;
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
interface Analytics {
|
| 113 |
+
stats: { action: string; count: number }[];
|
| 114 |
+
platformStats: { platform: string; count: number }[];
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
type TabType = 'main' | 'settings' | 'analytics' | 'search' | 'captured';
|
| 118 |
+
|
| 119 |
+
// Helper to get platform icon component
|
| 120 |
+
const getPlatformIcon = (platformId: string, className: string = "h-5 w-5") => {
|
| 121 |
+
const IconComponent = PlatformIcons[platformId] || FileText;
|
| 122 |
+
return <IconComponent className={className} />;
|
| 123 |
+
};
|
| 124 |
+
|
| 125 |
+
function App() {
|
| 126 |
+
const [pendingPrompt, setPendingPrompt] = useState<PendingPrompt | null>(null);
|
| 127 |
+
const [recentPrompts, setRecentPrompts] = useState<RecentPrompt[]>([]);
|
| 128 |
+
const [capturedPrompts, setCapturedPrompts] = useState<CapturedPrompt[]>([]);
|
| 129 |
+
const [defaultPlatform, setDefaultPlatform] = useState('chatgpt');
|
| 130 |
+
const [activeTab, setActiveTab] = useState<TabType>('main');
|
| 131 |
+
const [copied, setCopied] = useState(false);
|
| 132 |
+
const [sending, setSending] = useState<string | null>(null);
|
| 133 |
+
const [searchQuery, setSearchQuery] = useState('');
|
| 134 |
+
const [analytics, setAnalytics] = useState<Analytics | null>(null);
|
| 135 |
+
const [broadcastMode, setBroadcastMode] = useState(false);
|
| 136 |
+
const [selectedPlatforms, setSelectedPlatforms] = useState<string[]>([]);
|
| 137 |
+
|
| 138 |
+
useEffect(() => {
|
| 139 |
+
// Load data from storage
|
| 140 |
+
chrome.storage.local.get(['pendingPrompt', 'recentPrompts', 'defaultPlatform', 'deviceId'], (result) => {
|
| 141 |
+
if (result.pendingPrompt) {
|
| 142 |
+
setPendingPrompt(result.pendingPrompt);
|
| 143 |
+
}
|
| 144 |
+
if (result.recentPrompts) {
|
| 145 |
+
setRecentPrompts(result.recentPrompts.slice(0, 10));
|
| 146 |
+
}
|
| 147 |
+
if (result.defaultPlatform) {
|
| 148 |
+
setDefaultPlatform(result.defaultPlatform);
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
// Fetch analytics and captured prompts
|
| 152 |
+
if (result.deviceId) {
|
| 153 |
+
fetchAnalytics(result.deviceId);
|
| 154 |
+
fetchCapturedPrompts(result.deviceId);
|
| 155 |
+
}
|
| 156 |
+
});
|
| 157 |
+
}, []);
|
| 158 |
+
|
| 159 |
+
const fetchAnalytics = async (deviceId: string) => {
|
| 160 |
+
try {
|
| 161 |
+
const response = await fetch(`${API_BASE}/api/extension/analytics?deviceId=${deviceId}`);
|
| 162 |
+
if (response.ok) {
|
| 163 |
+
const data = await response.json();
|
| 164 |
+
setAnalytics(data);
|
| 165 |
+
}
|
| 166 |
+
} catch (error) {
|
| 167 |
+
console.error('Failed to fetch analytics:', error);
|
| 168 |
+
}
|
| 169 |
+
};
|
| 170 |
+
|
| 171 |
+
const fetchCapturedPrompts = async (deviceId: string) => {
|
| 172 |
+
try {
|
| 173 |
+
const response = await fetch(`${API_BASE}/api/extension/capture?deviceId=${deviceId}&limit=10`);
|
| 174 |
+
if (response.ok) {
|
| 175 |
+
const data = await response.json();
|
| 176 |
+
setCapturedPrompts(data.captured || []);
|
| 177 |
+
}
|
| 178 |
+
} catch (error) {
|
| 179 |
+
console.error('Failed to fetch captured prompts:', error);
|
| 180 |
+
}
|
| 181 |
+
};
|
| 182 |
+
|
| 183 |
+
// Fuzzy search through recent prompts
|
| 184 |
+
const filteredPrompts = useMemo(() => {
|
| 185 |
+
if (!searchQuery.trim()) return recentPrompts;
|
| 186 |
+
|
| 187 |
+
const query = searchQuery.toLowerCase();
|
| 188 |
+
return recentPrompts.filter(p =>
|
| 189 |
+
p.title?.toLowerCase().includes(query) ||
|
| 190 |
+
p.prompt.toLowerCase().includes(query)
|
| 191 |
+
);
|
| 192 |
+
}, [searchQuery, recentPrompts]);
|
| 193 |
+
|
| 194 |
+
const handleOpenIn = async (platformId: string) => {
|
| 195 |
+
if (!pendingPrompt) return;
|
| 196 |
+
|
| 197 |
+
setSending(platformId);
|
| 198 |
+
const platform = AI_PLATFORMS.find(p => p.id === platformId);
|
| 199 |
+
if (!platform) return;
|
| 200 |
+
|
| 201 |
+
// Save to recent prompts
|
| 202 |
+
const newRecent: RecentPrompt = {
|
| 203 |
+
prompt: pendingPrompt.prompt,
|
| 204 |
+
title: pendingPrompt.title,
|
| 205 |
+
platform: platformId,
|
| 206 |
+
timestamp: Date.now(),
|
| 207 |
+
};
|
| 208 |
+
|
| 209 |
+
const updatedRecents = [newRecent, ...recentPrompts.filter(r =>
|
| 210 |
+
r.prompt !== pendingPrompt.prompt || r.platform !== platformId
|
| 211 |
+
)].slice(0, 10);
|
| 212 |
+
|
| 213 |
+
await chrome.storage.local.set({ recentPrompts: updatedRecents });
|
| 214 |
+
setRecentPrompts(updatedRecents);
|
| 215 |
+
|
| 216 |
+
// Store prompt for the content script to pick up
|
| 217 |
+
await chrome.storage.local.set({
|
| 218 |
+
promptToInject: {
|
| 219 |
+
prompt: pendingPrompt.prompt,
|
| 220 |
+
platform: platformId,
|
| 221 |
+
timestamp: Date.now(),
|
| 222 |
+
}
|
| 223 |
+
});
|
| 224 |
+
|
| 225 |
+
// Track analytics
|
| 226 |
+
chrome.runtime.sendMessage({
|
| 227 |
+
type: 'TRACK_ANALYTICS',
|
| 228 |
+
data: {
|
| 229 |
+
action: 'inject',
|
| 230 |
+
platform: platformId,
|
| 231 |
+
promptId: pendingPrompt.promptId,
|
| 232 |
+
promptTitle: pendingPrompt.title,
|
| 233 |
+
}
|
| 234 |
+
});
|
| 235 |
+
|
| 236 |
+
// Open the platform in a new tab
|
| 237 |
+
chrome.tabs.create({ url: platform.url }, () => {
|
| 238 |
+
setSending(null);
|
| 239 |
+
chrome.storage.local.remove('pendingPrompt');
|
| 240 |
+
setPendingPrompt(null);
|
| 241 |
+
});
|
| 242 |
+
};
|
| 243 |
+
|
| 244 |
+
const handleBroadcast = async () => {
|
| 245 |
+
if (!pendingPrompt || selectedPlatforms.length === 0) return;
|
| 246 |
+
|
| 247 |
+
setSending('broadcast');
|
| 248 |
+
|
| 249 |
+
// Track analytics
|
| 250 |
+
chrome.runtime.sendMessage({
|
| 251 |
+
type: 'TRACK_ANALYTICS',
|
| 252 |
+
data: {
|
| 253 |
+
action: 'broadcast',
|
| 254 |
+
platforms: selectedPlatforms,
|
| 255 |
+
promptTitle: pendingPrompt.title,
|
| 256 |
+
}
|
| 257 |
+
});
|
| 258 |
+
|
| 259 |
+
// Send broadcast message
|
| 260 |
+
chrome.runtime.sendMessage({
|
| 261 |
+
type: 'BROADCAST_PROMPT',
|
| 262 |
+
prompt: pendingPrompt.prompt,
|
| 263 |
+
platforms: selectedPlatforms,
|
| 264 |
+
}, () => {
|
| 265 |
+
setSending(null);
|
| 266 |
+
setBroadcastMode(false);
|
| 267 |
+
setSelectedPlatforms([]);
|
| 268 |
+
chrome.storage.local.remove('pendingPrompt');
|
| 269 |
+
setPendingPrompt(null);
|
| 270 |
+
});
|
| 271 |
+
};
|
| 272 |
+
|
| 273 |
+
const togglePlatformSelection = (platformId: string) => {
|
| 274 |
+
setSelectedPlatforms(prev =>
|
| 275 |
+
prev.includes(platformId)
|
| 276 |
+
? prev.filter(p => p !== platformId)
|
| 277 |
+
: [...prev, platformId]
|
| 278 |
+
);
|
| 279 |
+
};
|
| 280 |
+
|
| 281 |
+
const handleCopyPrompt = () => {
|
| 282 |
+
if (!pendingPrompt) return;
|
| 283 |
+
navigator.clipboard.writeText(pendingPrompt.prompt);
|
| 284 |
+
setCopied(true);
|
| 285 |
+
setTimeout(() => setCopied(false), 2000);
|
| 286 |
+
};
|
| 287 |
+
|
| 288 |
+
const handleSetDefault = (platformId: string) => {
|
| 289 |
+
setDefaultPlatform(platformId);
|
| 290 |
+
chrome.storage.local.set({ defaultPlatform: platformId });
|
| 291 |
+
};
|
| 292 |
+
|
| 293 |
+
const handleReopenPrompt = async (recent: RecentPrompt) => {
|
| 294 |
+
const platform = AI_PLATFORMS.find(p => p.id === recent.platform);
|
| 295 |
+
if (!platform) return;
|
| 296 |
+
|
| 297 |
+
await chrome.storage.local.set({
|
| 298 |
+
promptToInject: {
|
| 299 |
+
prompt: recent.prompt,
|
| 300 |
+
platform: recent.platform,
|
| 301 |
+
timestamp: Date.now(),
|
| 302 |
+
}
|
| 303 |
+
});
|
| 304 |
+
|
| 305 |
+
chrome.tabs.create({ url: platform.url });
|
| 306 |
+
};
|
| 307 |
+
|
| 308 |
+
const handleUseCaptured = async (captured: CapturedPrompt) => {
|
| 309 |
+
setPendingPrompt({
|
| 310 |
+
prompt: captured.text,
|
| 311 |
+
title: captured.title || `From ${captured.platform}`,
|
| 312 |
+
timestamp: Date.now(),
|
| 313 |
+
});
|
| 314 |
+
setActiveTab('main');
|
| 315 |
+
};
|
| 316 |
+
|
| 317 |
+
const renderTabContent = () => {
|
| 318 |
+
switch (activeTab) {
|
| 319 |
+
case 'settings':
|
| 320 |
+
return (
|
| 321 |
+
<div className="space-y-4">
|
| 322 |
+
<h2 className="text-sm font-medium text-gray-300">Default Platform</h2>
|
| 323 |
+
<div className="space-y-2">
|
| 324 |
+
{AI_PLATFORMS.map((platform) => (
|
| 325 |
+
<button
|
| 326 |
+
key={platform.id}
|
| 327 |
+
onClick={() => handleSetDefault(platform.id)}
|
| 328 |
+
className={`w-full flex items-center gap-3 p-3 rounded-lg border transition-all ${
|
| 329 |
+
defaultPlatform === platform.id
|
| 330 |
+
? `${platform.bgColor} ${platform.borderColor}`
|
| 331 |
+
: 'border-white/10 hover:border-white/20 hover:bg-white/5'
|
| 332 |
+
}`}
|
| 333 |
+
>
|
| 334 |
+
{getPlatformIcon(platform.id, "h-5 w-5")}
|
| 335 |
+
<span className="flex-1 text-left text-sm font-medium">{platform.name}</span>
|
| 336 |
+
{defaultPlatform === platform.id && (
|
| 337 |
+
<Check className="h-4 w-4 text-green-400" />
|
| 338 |
+
)}
|
| 339 |
+
</button>
|
| 340 |
+
))}
|
| 341 |
+
</div>
|
| 342 |
+
|
| 343 |
+
<div className="pt-4 border-t border-white/10">
|
| 344 |
+
<h3 className="text-sm font-medium text-gray-300 mb-3 flex items-center gap-2">
|
| 345 |
+
<Keyboard className="h-4 w-4" />
|
| 346 |
+
Keyboard Shortcuts
|
| 347 |
+
</h3>
|
| 348 |
+
<div className="space-y-2 text-xs text-gray-400">
|
| 349 |
+
<div className="flex justify-between">
|
| 350 |
+
<span>Open Popup</span>
|
| 351 |
+
<kbd className="px-2 py-1 bg-white/10 rounded">Ctrl+Shift+O</kbd>
|
| 352 |
+
</div>
|
| 353 |
+
<div className="flex justify-between">
|
| 354 |
+
<span>Quick Search</span>
|
| 355 |
+
<kbd className="px-2 py-1 bg-white/10 rounded">Ctrl+Shift+P</kbd>
|
| 356 |
+
</div>
|
| 357 |
+
<div className="flex justify-between">
|
| 358 |
+
<span>Capture Selection</span>
|
| 359 |
+
<kbd className="px-2 py-1 bg-white/10 rounded">Ctrl+Shift+S</kbd>
|
| 360 |
+
</div>
|
| 361 |
+
</div>
|
| 362 |
+
</div>
|
| 363 |
+
</div>
|
| 364 |
+
);
|
| 365 |
+
|
| 366 |
+
case 'analytics':
|
| 367 |
+
return (
|
| 368 |
+
<div className="space-y-4">
|
| 369 |
+
<h2 className="text-sm font-medium text-gray-300 flex items-center gap-2">
|
| 370 |
+
<BarChart3 className="h-4 w-4" />
|
| 371 |
+
Your Usage Stats
|
| 372 |
+
</h2>
|
| 373 |
+
|
| 374 |
+
{analytics ? (
|
| 375 |
+
<>
|
| 376 |
+
<div className="grid grid-cols-2 gap-3">
|
| 377 |
+
{analytics.stats.map(stat => (
|
| 378 |
+
<div key={stat.action} className="p-3 rounded-lg bg-white/5 border border-white/10">
|
| 379 |
+
<div className="text-2xl font-bold text-purple-400">{stat.count}</div>
|
| 380 |
+
<div className="text-xs text-gray-400 capitalize">{stat.action}s</div>
|
| 381 |
+
</div>
|
| 382 |
+
))}
|
| 383 |
+
</div>
|
| 384 |
+
|
| 385 |
+
<h3 className="text-sm font-medium text-gray-300 mt-4">By Platform</h3>
|
| 386 |
+
<div className="space-y-2">
|
| 387 |
+
{analytics.platformStats.map(stat => {
|
| 388 |
+
const platform = AI_PLATFORMS.find(p => p.id === stat.platform);
|
| 389 |
+
return (
|
| 390 |
+
<div key={stat.platform} className="flex items-center gap-3 p-2 rounded-lg bg-white/5">
|
| 391 |
+
{getPlatformIcon(stat.platform)}
|
| 392 |
+
<span className="flex-1 text-sm">{platform?.name || stat.platform}</span>
|
| 393 |
+
<span className="text-sm font-semibold text-purple-400">{stat.count}</span>
|
| 394 |
+
</div>
|
| 395 |
+
);
|
| 396 |
+
})}
|
| 397 |
+
</div>
|
| 398 |
+
</>
|
| 399 |
+
) : (
|
| 400 |
+
<div className="text-center py-8 text-gray-500">
|
| 401 |
+
<BarChart3 className="h-12 w-12 mx-auto mb-3 opacity-50" />
|
| 402 |
+
<p>No usage data yet</p>
|
| 403 |
+
<p className="text-xs mt-1">Start using prompts to see stats</p>
|
| 404 |
+
</div>
|
| 405 |
+
)}
|
| 406 |
+
</div>
|
| 407 |
+
);
|
| 408 |
+
|
| 409 |
+
case 'search':
|
| 410 |
+
return (
|
| 411 |
+
<div className="space-y-4">
|
| 412 |
+
<div className="relative">
|
| 413 |
+
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-500" />
|
| 414 |
+
<input
|
| 415 |
+
type="text"
|
| 416 |
+
value={searchQuery}
|
| 417 |
+
onChange={(e) => setSearchQuery(e.target.value)}
|
| 418 |
+
placeholder="Search your prompts..."
|
| 419 |
+
className="w-full pl-10 pr-4 py-2 rounded-lg bg-white/10 border border-white/20 text-sm focus:outline-none focus:ring-2 focus:ring-purple-500"
|
| 420 |
+
autoFocus
|
| 421 |
+
/>
|
| 422 |
+
</div>
|
| 423 |
+
|
| 424 |
+
<div className="space-y-2">
|
| 425 |
+
{filteredPrompts.length > 0 ? (
|
| 426 |
+
filteredPrompts.map((prompt, index) => (
|
| 427 |
+
<button
|
| 428 |
+
key={index}
|
| 429 |
+
onClick={() => handleReopenPrompt(prompt)}
|
| 430 |
+
className="w-full flex items-center gap-3 p-3 rounded-lg border border-white/10 hover:border-white/20 hover:bg-white/5 transition-all text-left"
|
| 431 |
+
>
|
| 432 |
+
{getPlatformIcon(prompt.platform)}
|
| 433 |
+
<div className="flex-1 min-w-0">
|
| 434 |
+
<p className="text-sm font-medium truncate">
|
| 435 |
+
{prompt.title || 'Untitled Prompt'}
|
| 436 |
+
</p>
|
| 437 |
+
<p className="text-xs text-gray-500 truncate">
|
| 438 |
+
{prompt.prompt.slice(0, 50)}...
|
| 439 |
+
</p>
|
| 440 |
+
</div>
|
| 441 |
+
<ChevronRight className="h-4 w-4 text-gray-500" />
|
| 442 |
+
</button>
|
| 443 |
+
))
|
| 444 |
+
) : (
|
| 445 |
+
<div className="text-center py-8 text-gray-500">
|
| 446 |
+
<Search className="h-12 w-12 mx-auto mb-3 opacity-50" />
|
| 447 |
+
<p>No matching prompts</p>
|
| 448 |
+
</div>
|
| 449 |
+
)}
|
| 450 |
+
</div>
|
| 451 |
+
</div>
|
| 452 |
+
);
|
| 453 |
+
|
| 454 |
+
case 'captured':
|
| 455 |
+
return (
|
| 456 |
+
<div className="space-y-4">
|
| 457 |
+
<h2 className="text-sm font-medium text-gray-300 flex items-center gap-2">
|
| 458 |
+
<Save className="h-4 w-4" />
|
| 459 |
+
Captured Prompts
|
| 460 |
+
</h2>
|
| 461 |
+
|
| 462 |
+
{capturedPrompts.length > 0 ? (
|
| 463 |
+
<div className="space-y-2">
|
| 464 |
+
{capturedPrompts.map((captured) => (
|
| 465 |
+
<button
|
| 466 |
+
key={captured.id}
|
| 467 |
+
onClick={() => handleUseCaptured(captured)}
|
| 468 |
+
className="w-full flex items-center gap-3 p-3 rounded-lg border border-white/10 hover:border-white/20 hover:bg-white/5 transition-all text-left"
|
| 469 |
+
>
|
| 470 |
+
{getPlatformIcon(captured.platform)}
|
| 471 |
+
<div className="flex-1 min-w-0">
|
| 472 |
+
<p className="text-sm font-medium truncate">
|
| 473 |
+
{captured.title || `From ${captured.platform}`}
|
| 474 |
+
</p>
|
| 475 |
+
<p className="text-xs text-gray-500 truncate">
|
| 476 |
+
{captured.text.slice(0, 50)}...
|
| 477 |
+
</p>
|
| 478 |
+
</div>
|
| 479 |
+
<ChevronRight className="h-4 w-4 text-gray-500" />
|
| 480 |
+
</button>
|
| 481 |
+
))}
|
| 482 |
+
</div>
|
| 483 |
+
) : (
|
| 484 |
+
<div className="text-center py-8 text-gray-500">
|
| 485 |
+
<Save className="h-12 w-12 mx-auto mb-3 opacity-50" />
|
| 486 |
+
<p>No captured prompts yet</p>
|
| 487 |
+
<p className="text-xs mt-1">Select text in any AI chat and use Ctrl+Shift+S</p>
|
| 488 |
+
</div>
|
| 489 |
+
)}
|
| 490 |
+
</div>
|
| 491 |
+
);
|
| 492 |
+
|
| 493 |
+
default:
|
| 494 |
+
// Main view
|
| 495 |
+
if (pendingPrompt) {
|
| 496 |
+
return (
|
| 497 |
+
<div className="space-y-4">
|
| 498 |
+
<div className="p-3 rounded-lg bg-white/5 border border-white/10">
|
| 499 |
+
<div className="flex items-start justify-between gap-2 mb-2">
|
| 500 |
+
<h3 className="text-sm font-medium text-purple-300">
|
| 501 |
+
{pendingPrompt.title || 'Prompt Ready'}
|
| 502 |
+
</h3>
|
| 503 |
+
<button
|
| 504 |
+
onClick={handleCopyPrompt}
|
| 505 |
+
className="p-1.5 rounded hover:bg-white/10 transition-colors"
|
| 506 |
+
title="Copy prompt"
|
| 507 |
+
>
|
| 508 |
+
{copied ? (
|
| 509 |
+
<Check className="h-3.5 w-3.5 text-green-400" />
|
| 510 |
+
) : (
|
| 511 |
+
<Copy className="h-3.5 w-3.5 text-gray-400" />
|
| 512 |
+
)}
|
| 513 |
+
</button>
|
| 514 |
+
</div>
|
| 515 |
+
<p className="text-xs text-gray-400 line-clamp-3">
|
| 516 |
+
{pendingPrompt.prompt}
|
| 517 |
+
</p>
|
| 518 |
+
</div>
|
| 519 |
+
|
| 520 |
+
{broadcastMode ? (
|
| 521 |
+
<>
|
| 522 |
+
<div className="flex items-center justify-between">
|
| 523 |
+
<h2 className="text-sm font-medium text-gray-300 flex items-center gap-2">
|
| 524 |
+
<Layers className="h-4 w-4 text-yellow-400" />
|
| 525 |
+
Broadcast to Multiple
|
| 526 |
+
</h2>
|
| 527 |
+
<button
|
| 528 |
+
onClick={() => {
|
| 529 |
+
setBroadcastMode(false);
|
| 530 |
+
setSelectedPlatforms([]);
|
| 531 |
+
}}
|
| 532 |
+
className="p-1 rounded hover:bg-white/10"
|
| 533 |
+
>
|
| 534 |
+
<X className="h-4 w-4 text-gray-400" />
|
| 535 |
+
</button>
|
| 536 |
+
</div>
|
| 537 |
+
|
| 538 |
+
<div className="grid grid-cols-2 gap-2">
|
| 539 |
+
{AI_PLATFORMS.map((platform) => (
|
| 540 |
+
<button
|
| 541 |
+
key={platform.id}
|
| 542 |
+
onClick={() => togglePlatformSelection(platform.id)}
|
| 543 |
+
className={`flex items-center gap-2 p-3 rounded-lg border transition-all ${
|
| 544 |
+
selectedPlatforms.includes(platform.id)
|
| 545 |
+
? `${platform.bgColor} ${platform.borderColor}`
|
| 546 |
+
: 'border-white/10 hover:border-white/20'
|
| 547 |
+
}`}
|
| 548 |
+
>
|
| 549 |
+
{getPlatformIcon(platform.id)}
|
| 550 |
+
<span className="text-sm font-medium">{platform.name}</span>
|
| 551 |
+
{selectedPlatforms.includes(platform.id) && (
|
| 552 |
+
<Check className="ml-auto h-4 w-4 text-green-400" />
|
| 553 |
+
)}
|
| 554 |
+
</button>
|
| 555 |
+
))}
|
| 556 |
+
</div>
|
| 557 |
+
|
| 558 |
+
<button
|
| 559 |
+
onClick={handleBroadcast}
|
| 560 |
+
disabled={selectedPlatforms.length === 0 || sending === 'broadcast'}
|
| 561 |
+
className="w-full py-3 px-4 rounded-lg bg-gradient-to-r from-yellow-600 to-orange-600 hover:from-yellow-700 hover:to-orange-700 disabled:opacity-50 transition-colors text-sm font-medium flex items-center justify-center gap-2"
|
| 562 |
+
>
|
| 563 |
+
{sending === 'broadcast' ? (
|
| 564 |
+
<div className="h-4 w-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />
|
| 565 |
+
) : (
|
| 566 |
+
<>
|
| 567 |
+
<Send className="h-4 w-4" />
|
| 568 |
+
Send to {selectedPlatforms.length} Platform{selectedPlatforms.length !== 1 ? 's' : ''}
|
| 569 |
+
</>
|
| 570 |
+
)}
|
| 571 |
+
</button>
|
| 572 |
+
</>
|
| 573 |
+
) : (
|
| 574 |
+
<>
|
| 575 |
+
<div className="flex items-center justify-between">
|
| 576 |
+
<h2 className="text-sm font-medium text-gray-300 flex items-center gap-2">
|
| 577 |
+
<Zap className="h-4 w-4 text-yellow-400" />
|
| 578 |
+
Open in...
|
| 579 |
+
</h2>
|
| 580 |
+
<button
|
| 581 |
+
onClick={() => setBroadcastMode(true)}
|
| 582 |
+
className="text-xs text-purple-400 hover:text-purple-300 flex items-center gap-1"
|
| 583 |
+
>
|
| 584 |
+
<Layers className="h-3 w-3" />
|
| 585 |
+
Broadcast
|
| 586 |
+
</button>
|
| 587 |
+
</div>
|
| 588 |
+
|
| 589 |
+
<div className="grid grid-cols-2 gap-2">
|
| 590 |
+
{AI_PLATFORMS.map((platform) => (
|
| 591 |
+
<button
|
| 592 |
+
key={platform.id}
|
| 593 |
+
onClick={() => handleOpenIn(platform.id)}
|
| 594 |
+
disabled={sending !== null}
|
| 595 |
+
className={`flex items-center gap-2 p-3 rounded-lg border transition-all ${
|
| 596 |
+
platform.bgColor
|
| 597 |
+
} ${platform.borderColor} hover:scale-[1.02] active:scale-[0.98] disabled:opacity-50`}
|
| 598 |
+
>
|
| 599 |
+
{getPlatformIcon(platform.id)}
|
| 600 |
+
<span className="text-sm font-medium">{platform.name}</span>
|
| 601 |
+
{sending === platform.id ? (
|
| 602 |
+
<div className="ml-auto h-4 w-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />
|
| 603 |
+
) : (
|
| 604 |
+
<ExternalLink className="ml-auto h-3.5 w-3.5 opacity-50" />
|
| 605 |
+
)}
|
| 606 |
+
</button>
|
| 607 |
+
))}
|
| 608 |
+
</div>
|
| 609 |
+
</>
|
| 610 |
+
)}
|
| 611 |
+
</div>
|
| 612 |
+
);
|
| 613 |
+
}
|
| 614 |
+
|
| 615 |
+
// No pending prompt
|
| 616 |
+
return (
|
| 617 |
+
<div className="space-y-4">
|
| 618 |
+
<div className="text-center py-6">
|
| 619 |
+
<div className="inline-flex items-center justify-center h-14 w-14 rounded-2xl bg-gradient-to-br from-purple-500/20 to-pink-500/20 border border-purple-500/30 mb-3">
|
| 620 |
+
<Sparkles className="h-7 w-7 text-purple-400" />
|
| 621 |
+
</div>
|
| 622 |
+
<h2 className="text-base font-medium mb-1">No Prompt Selected</h2>
|
| 623 |
+
<p className="text-xs text-gray-400 max-w-[280px] mx-auto">
|
| 624 |
+
Visit OpenPrompt and click "Open In..." on any prompt to send it to your favorite AI chat.
|
| 625 |
+
</p>
|
| 626 |
+
</div>
|
| 627 |
+
|
| 628 |
+
{recentPrompts.length > 0 && (
|
| 629 |
+
<div>
|
| 630 |
+
<h3 className="text-sm font-medium text-gray-300 flex items-center gap-2 mb-3">
|
| 631 |
+
<History className="h-4 w-4" />
|
| 632 |
+
Recent Prompts
|
| 633 |
+
</h3>
|
| 634 |
+
<div className="space-y-2">
|
| 635 |
+
{recentPrompts.slice(0, 3).map((recent, index) => (
|
| 636 |
+
<button
|
| 637 |
+
key={index}
|
| 638 |
+
onClick={() => handleReopenPrompt(recent)}
|
| 639 |
+
className="w-full flex items-center gap-3 p-3 rounded-lg border border-white/10 hover:border-white/20 hover:bg-white/5 transition-all text-left"
|
| 640 |
+
>
|
| 641 |
+
{getPlatformIcon(recent.platform)}
|
| 642 |
+
<div className="flex-1 min-w-0">
|
| 643 |
+
<p className="text-sm font-medium truncate">
|
| 644 |
+
{recent.title || 'Untitled Prompt'}
|
| 645 |
+
</p>
|
| 646 |
+
<p className="text-xs text-gray-500 truncate">
|
| 647 |
+
{recent.prompt.slice(0, 50)}...
|
| 648 |
+
</p>
|
| 649 |
+
</div>
|
| 650 |
+
<ChevronRight className="h-4 w-4 text-gray-500" />
|
| 651 |
+
</button>
|
| 652 |
+
))}
|
| 653 |
+
</div>
|
| 654 |
+
</div>
|
| 655 |
+
)}
|
| 656 |
+
|
| 657 |
+
<a
|
| 658 |
+
href={API_BASE}
|
| 659 |
+
target="_blank"
|
| 660 |
+
rel="noopener noreferrer"
|
| 661 |
+
className="block w-full py-3 px-4 rounded-lg bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-700 hover:to-pink-700 transition-colors text-center text-sm font-medium"
|
| 662 |
+
>
|
| 663 |
+
Browse Prompts on OpenPrompt
|
| 664 |
+
<ExternalLink className="inline-block ml-2 h-3.5 w-3.5" />
|
| 665 |
+
</a>
|
| 666 |
+
</div>
|
| 667 |
+
);
|
| 668 |
+
}
|
| 669 |
+
};
|
| 670 |
+
|
| 671 |
+
return (
|
| 672 |
+
<div className="w-[360px] min-h-[480px] flex flex-col bg-[#0f0f14]">
|
| 673 |
+
{/* Header */}
|
| 674 |
+
<header className="p-4 border-b border-white/10">
|
| 675 |
+
<div className="flex items-center justify-between">
|
| 676 |
+
<div className="flex items-center gap-2">
|
| 677 |
+
<div className="h-8 w-8 rounded-lg bg-gradient-to-br from-purple-500 to-pink-500 flex items-center justify-center">
|
| 678 |
+
<Sparkles className="h-4 w-4 text-white" />
|
| 679 |
+
</div>
|
| 680 |
+
<div>
|
| 681 |
+
<h1 className="font-semibold text-white">OpenPrompt</h1>
|
| 682 |
+
<p className="text-xs text-gray-400">Open in AI</p>
|
| 683 |
+
</div>
|
| 684 |
+
</div>
|
| 685 |
+
|
| 686 |
+
{/* Tab buttons */}
|
| 687 |
+
<div className="flex items-center gap-1">
|
| 688 |
+
<button
|
| 689 |
+
onClick={() => setActiveTab('search')}
|
| 690 |
+
className={`p-2 rounded-lg transition-colors ${activeTab === 'search' ? 'bg-white/20' : 'hover:bg-white/10'}`}
|
| 691 |
+
title="Search"
|
| 692 |
+
>
|
| 693 |
+
<Search className="h-4 w-4 text-gray-400" />
|
| 694 |
+
</button>
|
| 695 |
+
<button
|
| 696 |
+
onClick={() => setActiveTab('captured')}
|
| 697 |
+
className={`p-2 rounded-lg transition-colors ${activeTab === 'captured' ? 'bg-white/20' : 'hover:bg-white/10'}`}
|
| 698 |
+
title="Captured Prompts"
|
| 699 |
+
>
|
| 700 |
+
<Save className="h-4 w-4 text-gray-400" />
|
| 701 |
+
</button>
|
| 702 |
+
<button
|
| 703 |
+
onClick={() => setActiveTab('analytics')}
|
| 704 |
+
className={`p-2 rounded-lg transition-colors ${activeTab === 'analytics' ? 'bg-white/20' : 'hover:bg-white/10'}`}
|
| 705 |
+
title="Analytics"
|
| 706 |
+
>
|
| 707 |
+
<BarChart3 className="h-4 w-4 text-gray-400" />
|
| 708 |
+
</button>
|
| 709 |
+
<button
|
| 710 |
+
onClick={() => setActiveTab('settings')}
|
| 711 |
+
className={`p-2 rounded-lg transition-colors ${activeTab === 'settings' ? 'bg-white/20' : 'hover:bg-white/10'}`}
|
| 712 |
+
title="Settings"
|
| 713 |
+
>
|
| 714 |
+
<Settings className="h-4 w-4 text-gray-400" />
|
| 715 |
+
</button>
|
| 716 |
+
</div>
|
| 717 |
+
</div>
|
| 718 |
+
</header>
|
| 719 |
+
|
| 720 |
+
{/* Main Content */}
|
| 721 |
+
<main className="flex-1 p-4 overflow-y-auto">
|
| 722 |
+
{activeTab !== 'main' && (
|
| 723 |
+
<button
|
| 724 |
+
onClick={() => setActiveTab('main')}
|
| 725 |
+
className="mb-4 text-sm text-purple-400 hover:text-purple-300 flex items-center gap-1"
|
| 726 |
+
>
|
| 727 |
+
← Back
|
| 728 |
+
</button>
|
| 729 |
+
)}
|
| 730 |
+
{renderTabContent()}
|
| 731 |
+
</main>
|
| 732 |
+
|
| 733 |
+
{/* Footer */}
|
| 734 |
+
<footer className="p-3 border-t border-white/10 text-center">
|
| 735 |
+
<p className="text-xs text-gray-500">
|
| 736 |
+
OpenPrompt Extension v1.1.0
|
| 737 |
+
</p>
|
| 738 |
+
</footer>
|
| 739 |
+
</div>
|
| 740 |
+
);
|
| 741 |
+
}
|
| 742 |
+
|
| 743 |
+
export default App;
|
openprompt-extension/src/popup/index.css
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@tailwind base;
|
| 2 |
+
@tailwind components;
|
| 3 |
+
@tailwind utilities;
|
| 4 |
+
|
| 5 |
+
* {
|
| 6 |
+
margin: 0;
|
| 7 |
+
padding: 0;
|
| 8 |
+
box-sizing: border-box;
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
body {
|
| 12 |
+
width: 360px;
|
| 13 |
+
min-height: 480px;
|
| 14 |
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
| 15 |
+
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f0f23 100%);
|
| 16 |
+
color: #fff;
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
::-webkit-scrollbar {
|
| 20 |
+
width: 6px;
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
::-webkit-scrollbar-track {
|
| 24 |
+
background: rgba(255, 255, 255, 0.05);
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
::-webkit-scrollbar-thumb {
|
| 28 |
+
background: rgba(168, 85, 247, 0.4);
|
| 29 |
+
border-radius: 3px;
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
::-webkit-scrollbar-thumb:hover {
|
| 33 |
+
background: rgba(168, 85, 247, 0.6);
|
| 34 |
+
}
|
openprompt-extension/src/popup/main.tsx
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
import ReactDOM from 'react-dom/client';
|
| 3 |
+
import App from './App';
|
| 4 |
+
import './index.css';
|
| 5 |
+
|
| 6 |
+
ReactDOM.createRoot(document.getElementById('root')!).render(
|
| 7 |
+
<React.StrictMode>
|
| 8 |
+
<App />
|
| 9 |
+
</React.StrictMode>
|
| 10 |
+
);
|
openprompt-extension/tailwind.config.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/** @type {import('tailwindcss').Config} */
|
| 2 |
+
export default {
|
| 3 |
+
content: [
|
| 4 |
+
"./popup.html",
|
| 5 |
+
"./src/**/*.{js,ts,jsx,tsx}",
|
| 6 |
+
],
|
| 7 |
+
theme: {
|
| 8 |
+
extend: {
|
| 9 |
+
colors: {
|
| 10 |
+
primary: {
|
| 11 |
+
50: '#faf5ff',
|
| 12 |
+
100: '#f3e8ff',
|
| 13 |
+
200: '#e9d5ff',
|
| 14 |
+
300: '#d8b4fe',
|
| 15 |
+
400: '#c084fc',
|
| 16 |
+
500: '#a855f7',
|
| 17 |
+
600: '#9333ea',
|
| 18 |
+
700: '#7c3aed',
|
| 19 |
+
800: '#6b21a8',
|
| 20 |
+
900: '#581c87',
|
| 21 |
+
},
|
| 22 |
+
},
|
| 23 |
+
},
|
| 24 |
+
},
|
| 25 |
+
plugins: [],
|
| 26 |
+
}
|
openprompt-extension/tsconfig.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"compilerOptions": {
|
| 3 |
+
"target": "ES2020",
|
| 4 |
+
"useDefineForClassFields": true,
|
| 5 |
+
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
| 6 |
+
"module": "ESNext",
|
| 7 |
+
"skipLibCheck": true,
|
| 8 |
+
"types": ["chrome"],
|
| 9 |
+
"moduleResolution": "bundler",
|
| 10 |
+
"allowImportingTsExtensions": true,
|
| 11 |
+
"resolveJsonModule": true,
|
| 12 |
+
"isolatedModules": true,
|
| 13 |
+
"noEmit": true,
|
| 14 |
+
"jsx": "react-jsx",
|
| 15 |
+
"strict": true,
|
| 16 |
+
"noUnusedLocals": true,
|
| 17 |
+
"noUnusedParameters": true,
|
| 18 |
+
"noFallthroughCasesInSwitch": true
|
| 19 |
+
},
|
| 20 |
+
"include": ["src"]
|
| 21 |
+
}
|
openprompt-extension/vite.config.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { defineConfig, build } from 'vite';
|
| 2 |
+
import react from '@vitejs/plugin-react';
|
| 3 |
+
import { resolve } from 'path';
|
| 4 |
+
|
| 5 |
+
// Custom plugin to build content scripts as IIFE after main build
|
| 6 |
+
function buildContentScriptsPlugin() {
|
| 7 |
+
return {
|
| 8 |
+
name: 'build-content-scripts',
|
| 9 |
+
async closeBundle() {
|
| 10 |
+
const contentScripts = [
|
| 11 |
+
'chatgpt',
|
| 12 |
+
'claude',
|
| 13 |
+
'gemini',
|
| 14 |
+
'perplexity',
|
| 15 |
+
'mistral',
|
| 16 |
+
'copilot',
|
| 17 |
+
'openprompt-site',
|
| 18 |
+
];
|
| 19 |
+
|
| 20 |
+
for (const script of contentScripts) {
|
| 21 |
+
await build({
|
| 22 |
+
configFile: false,
|
| 23 |
+
build: {
|
| 24 |
+
outDir: 'dist/content-scripts',
|
| 25 |
+
emptyOutDir: false,
|
| 26 |
+
lib: {
|
| 27 |
+
entry: resolve(__dirname, `src/content-scripts/${script}.ts`),
|
| 28 |
+
name: script,
|
| 29 |
+
fileName: () => `${script}.js`,
|
| 30 |
+
formats: ['iife'],
|
| 31 |
+
},
|
| 32 |
+
rollupOptions: {
|
| 33 |
+
output: {
|
| 34 |
+
extend: true,
|
| 35 |
+
},
|
| 36 |
+
},
|
| 37 |
+
},
|
| 38 |
+
});
|
| 39 |
+
}
|
| 40 |
+
},
|
| 41 |
+
};
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
export default defineConfig({
|
| 45 |
+
plugins: [react(), buildContentScriptsPlugin()],
|
| 46 |
+
build: {
|
| 47 |
+
outDir: 'dist',
|
| 48 |
+
rollupOptions: {
|
| 49 |
+
input: {
|
| 50 |
+
popup: resolve(__dirname, 'popup.html'),
|
| 51 |
+
background: resolve(__dirname, 'src/background/index.ts'),
|
| 52 |
+
},
|
| 53 |
+
output: {
|
| 54 |
+
entryFileNames: '[name].js',
|
| 55 |
+
assetFileNames: 'assets/[name].[ext]',
|
| 56 |
+
format: 'es',
|
| 57 |
+
},
|
| 58 |
+
},
|
| 59 |
+
emptyOutDir: true,
|
| 60 |
+
sourcemap: process.env.NODE_ENV === 'development',
|
| 61 |
+
},
|
| 62 |
+
publicDir: 'public',
|
| 63 |
+
});
|
package-lock.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
package.json
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "openprompt",
|
| 3 |
+
"version": "0.1.0",
|
| 4 |
+
"private": true,
|
| 5 |
+
"author": {
|
| 6 |
+
"name": "Anky9972",
|
| 7 |
+
"url": "https://github.com/Anky9972"
|
| 8 |
+
},
|
| 9 |
+
"repository": {
|
| 10 |
+
"type": "git",
|
| 11 |
+
"url": "https://github.com/Anky9972/open-prompt.git"
|
| 12 |
+
},
|
| 13 |
+
"scripts": {
|
| 14 |
+
"dev": "next dev",
|
| 15 |
+
"build": "prisma generate && next build",
|
| 16 |
+
"postinstall": "prisma generate",
|
| 17 |
+
"start": "next start",
|
| 18 |
+
"lint": "eslint"
|
| 19 |
+
},
|
| 20 |
+
"dependencies": {
|
| 21 |
+
"@ai-sdk/anthropic": "^2.0.54",
|
| 22 |
+
"@ai-sdk/google": "^2.0.45",
|
| 23 |
+
"@ai-sdk/openai": "^2.0.80",
|
| 24 |
+
"@ai-sdk/react": "^2.0.109",
|
| 25 |
+
"@hookform/resolvers": "^5.2.2",
|
| 26 |
+
"@neondatabase/serverless": "^1.0.2",
|
| 27 |
+
"@prisma/adapter-neon": "^7.1.0",
|
| 28 |
+
"@prisma/adapter-pg": "^7.1.0",
|
| 29 |
+
"@prisma/client": "^7.1.0",
|
| 30 |
+
"@radix-ui/react-checkbox": "^1.3.3",
|
| 31 |
+
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
| 32 |
+
"@radix-ui/react-label": "^2.1.8",
|
| 33 |
+
"@radix-ui/react-select": "^2.2.6",
|
| 34 |
+
"@radix-ui/react-slot": "^1.2.4",
|
| 35 |
+
"@radix-ui/react-tabs": "^1.1.13",
|
| 36 |
+
"@radix-ui/react-tooltip": "^1.2.8",
|
| 37 |
+
"@react-three/drei": "^10.7.7",
|
| 38 |
+
"@react-three/fiber": "^9.4.2",
|
| 39 |
+
"@stackframe/stack": "^2.8.56",
|
| 40 |
+
"@stackframe/stack-shared": "^2.8.56",
|
| 41 |
+
"@stripe/stripe-js": "^9.4.0",
|
| 42 |
+
"@studio-freight/lenis": "^1.0.42",
|
| 43 |
+
"@types/three": "^0.182.0",
|
| 44 |
+
"@upstash/redis": "^1.35.8",
|
| 45 |
+
"ai": "^5.0.108",
|
| 46 |
+
"class-variance-authority": "^0.7.1",
|
| 47 |
+
"clsx": "^2.1.1",
|
| 48 |
+
"framer-motion": "^12.23.26",
|
| 49 |
+
"lucide-react": "^0.559.0",
|
| 50 |
+
"nanoid": "^5.1.6",
|
| 51 |
+
"next": "16.0.8",
|
| 52 |
+
"next-themes": "^0.4.6",
|
| 53 |
+
"pg": "^8.16.3",
|
| 54 |
+
"prisma": "^7.1.0",
|
| 55 |
+
"qrcode.react": "^4.2.0",
|
| 56 |
+
"radix-ui": "^1.4.3",
|
| 57 |
+
"react": "19.2.1",
|
| 58 |
+
"react-dom": "19.2.1",
|
| 59 |
+
"react-hook-form": "^7.68.0",
|
| 60 |
+
"react-icons": "^5.5.0",
|
| 61 |
+
"react-markdown": "^10.1.0",
|
| 62 |
+
"recharts": "^3.5.1",
|
| 63 |
+
"stripe": "^22.1.0",
|
| 64 |
+
"tailwind-merge": "^3.4.0",
|
| 65 |
+
"three": "^0.182.0",
|
| 66 |
+
"zod": "^4.1.13",
|
| 67 |
+
"zustand": "^5.0.9"
|
| 68 |
+
},
|
| 69 |
+
"devDependencies": {
|
| 70 |
+
"@tailwindcss/postcss": "^4",
|
| 71 |
+
"@types/node": "^20",
|
| 72 |
+
"@types/react": "^19",
|
| 73 |
+
"@types/react-dom": "^19",
|
| 74 |
+
"eslint": "^9",
|
| 75 |
+
"eslint-config-next": "16.0.8",
|
| 76 |
+
"sharp": "^0.34.5",
|
| 77 |
+
"tailwindcss": "^4",
|
| 78 |
+
"tw-animate-css": "^1.4.0",
|
| 79 |
+
"typescript": "^5"
|
| 80 |
+
}
|
| 81 |
+
}
|
postcss.config.mjs
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const config = {
|
| 2 |
+
plugins: {
|
| 3 |
+
"@tailwindcss/postcss": {},
|
| 4 |
+
},
|
| 5 |
+
};
|
| 6 |
+
|
| 7 |
+
export default config;
|
prisma.config.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// This file was generated by Prisma and assumes you have installed the following:
|
| 2 |
+
// npm install --save-dev prisma dotenv
|
| 3 |
+
import "dotenv/config";
|
| 4 |
+
import { defineConfig, env } from "prisma/config";
|
| 5 |
+
|
| 6 |
+
export default defineConfig({
|
| 7 |
+
schema: "prisma/schema.prisma",
|
| 8 |
+
migrations: {
|
| 9 |
+
path: "prisma/migrations",
|
| 10 |
+
},
|
| 11 |
+
datasource: {
|
| 12 |
+
url: env("DATABASE_URL"),
|
| 13 |
+
},
|
| 14 |
+
});
|
prisma/schema.prisma
ADDED
|
@@ -0,0 +1,760 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
generator client {
|
| 2 |
+
provider = "prisma-client-js"
|
| 3 |
+
}
|
| 4 |
+
|
| 5 |
+
datasource db {
|
| 6 |
+
provider = "postgresql"
|
| 7 |
+
}
|
| 8 |
+
|
| 9 |
+
model User {
|
| 10 |
+
id String @id @default(cuid())
|
| 11 |
+
email String @unique
|
| 12 |
+
name String?
|
| 13 |
+
username String? @unique
|
| 14 |
+
image String?
|
| 15 |
+
bio String?
|
| 16 |
+
rank String? @default("bronze") // bronze, silver, gold, verified
|
| 17 |
+
totalRuns Int @default(0)
|
| 18 |
+
totalStars Int @default(0)
|
| 19 |
+
totalRemixes Int @default(0)
|
| 20 |
+
socialLinks Json?
|
| 21 |
+
|
| 22 |
+
// Notification preferences
|
| 23 |
+
notifyEmail Boolean @default(true)
|
| 24 |
+
notifyStars Boolean @default(true)
|
| 25 |
+
notifyRemixes Boolean @default(true)
|
| 26 |
+
|
| 27 |
+
// Stripe / Billing
|
| 28 |
+
stripeCustomerId String? @unique
|
| 29 |
+
stripePlan String @default("free") // free, pro
|
| 30 |
+
stripeSubscriptionId String? @unique
|
| 31 |
+
stripeSubscriptionStatus String? // active, canceled, past_due, etc.
|
| 32 |
+
|
| 33 |
+
createdAt DateTime @default(now())
|
| 34 |
+
updatedAt DateTime @updatedAt
|
| 35 |
+
|
| 36 |
+
prompts Prompt[]
|
| 37 |
+
stars Star[]
|
| 38 |
+
runs Run[]
|
| 39 |
+
collections Collection[]
|
| 40 |
+
comments Comment[]
|
| 41 |
+
commentLikes CommentLike[]
|
| 42 |
+
imagePrompts ImagePrompt[]
|
| 43 |
+
imageVotes ImagePromptVote[]
|
| 44 |
+
characters Character[]
|
| 45 |
+
characterVotes CharacterVote[]
|
| 46 |
+
workflows Workflow[] @relation("WorkflowCreator")
|
| 47 |
+
forumPosts ForumPost[]
|
| 48 |
+
forumReplies ForumReply[]
|
| 49 |
+
notifications Notification[] @relation("NotificationReceiver")
|
| 50 |
+
followers Follow[] @relation("Following")
|
| 51 |
+
following Follow[] @relation("Follower")
|
| 52 |
+
subscription Subscription?
|
| 53 |
+
purchases PromptPurchase[]
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
model Prompt {
|
| 57 |
+
id String @id @default(cuid())
|
| 58 |
+
slug String @unique
|
| 59 |
+
title String
|
| 60 |
+
description String?
|
| 61 |
+
template String @db.Text
|
| 62 |
+
schema Json @default("{\"variables\":[]}")
|
| 63 |
+
|
| 64 |
+
category String?
|
| 65 |
+
tags String[]
|
| 66 |
+
visibility String @default("public") // public, unlisted, private
|
| 67 |
+
|
| 68 |
+
modelDefault String @default("gpt-4o-mini")
|
| 69 |
+
modelAllowed String[] @default(["gpt-4o-mini", "gpt-4o", "claude-3-5-sonnet"])
|
| 70 |
+
maxTokens Int @default(2048)
|
| 71 |
+
|
| 72 |
+
totalRuns Int @default(0)
|
| 73 |
+
starsCount Int @default(0)
|
| 74 |
+
remixesCount Int @default(0)
|
| 75 |
+
|
| 76 |
+
// Tier 3 fields
|
| 77 |
+
framework String? // RACE, CARE, APE, etc.
|
| 78 |
+
badges String[] @default([]) // Auto-calculated quality badges
|
| 79 |
+
|
| 80 |
+
// Version tracking
|
| 81 |
+
currentVersion Int @default(1)
|
| 82 |
+
|
| 83 |
+
// Premium marketplace
|
| 84 |
+
isPremium Boolean @default(false) // Is this a paid prompt?
|
| 85 |
+
price Int? // Price in cents (e.g., 299 = $2.99)
|
| 86 |
+
|
| 87 |
+
creatorId String?
|
| 88 |
+
creator User? @relation(fields: [creatorId], references: [id])
|
| 89 |
+
|
| 90 |
+
parentId String?
|
| 91 |
+
parent Prompt? @relation("Remixes", fields: [parentId], references: [id])
|
| 92 |
+
remixes Prompt[] @relation("Remixes")
|
| 93 |
+
|
| 94 |
+
stars Star[]
|
| 95 |
+
runs Run[]
|
| 96 |
+
savedRuns SavedPromptRun[]
|
| 97 |
+
collections CollectionPrompt[]
|
| 98 |
+
comments Comment[]
|
| 99 |
+
versions PromptVersion[]
|
| 100 |
+
purchases PromptPurchase[] @relation("PremiumPurchases")
|
| 101 |
+
|
| 102 |
+
createdAt DateTime @default(now())
|
| 103 |
+
updatedAt DateTime @updatedAt
|
| 104 |
+
|
| 105 |
+
@@index([slug])
|
| 106 |
+
@@index([creatorId])
|
| 107 |
+
@@index([category])
|
| 108 |
+
@@index([visibility])
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
model Star {
|
| 112 |
+
userId String
|
| 113 |
+
promptId String
|
| 114 |
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
| 115 |
+
prompt Prompt @relation(fields: [promptId], references: [id], onDelete: Cascade)
|
| 116 |
+
createdAt DateTime @default(now())
|
| 117 |
+
|
| 118 |
+
@@id([userId, promptId])
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
model Run {
|
| 122 |
+
id String @id @default(cuid())
|
| 123 |
+
promptId String
|
| 124 |
+
userId String?
|
| 125 |
+
model String
|
| 126 |
+
tokens Int?
|
| 127 |
+
cached Boolean @default(false)
|
| 128 |
+
ipHash String?
|
| 129 |
+
prompt Prompt @relation(fields: [promptId], references: [id], onDelete: Cascade)
|
| 130 |
+
user User? @relation(fields: [userId], references: [id])
|
| 131 |
+
createdAt DateTime @default(now())
|
| 132 |
+
|
| 133 |
+
@@index([promptId])
|
| 134 |
+
@@index([userId])
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
model Collection {
|
| 138 |
+
id String @id @default(cuid())
|
| 139 |
+
name String
|
| 140 |
+
description String?
|
| 141 |
+
userId String
|
| 142 |
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
| 143 |
+
prompts CollectionPrompt[]
|
| 144 |
+
isPublic Boolean @default(false)
|
| 145 |
+
viewCount Int @default(0)
|
| 146 |
+
createdAt DateTime @default(now())
|
| 147 |
+
updatedAt DateTime @updatedAt
|
| 148 |
+
|
| 149 |
+
@@index([userId])
|
| 150 |
+
@@index([isPublic])
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
model CollectionPrompt {
|
| 154 |
+
collectionId String
|
| 155 |
+
promptId String
|
| 156 |
+
collection Collection @relation(fields: [collectionId], references: [id], onDelete: Cascade)
|
| 157 |
+
prompt Prompt @relation(fields: [promptId], references: [id], onDelete: Cascade)
|
| 158 |
+
addedAt DateTime @default(now())
|
| 159 |
+
|
| 160 |
+
@@id([collectionId, promptId])
|
| 161 |
+
@@index([collectionId])
|
| 162 |
+
@@index([promptId])
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
model Workflow {
|
| 166 |
+
id String @id @default(cuid())
|
| 167 |
+
slug String @unique
|
| 168 |
+
name String
|
| 169 |
+
description String?
|
| 170 |
+
steps Json // Array of workflow steps
|
| 171 |
+
variables Json? // Input variables for the workflow
|
| 172 |
+
|
| 173 |
+
visibility String @default("private") // public, unlisted, private
|
| 174 |
+
|
| 175 |
+
totalRuns Int @default(0)
|
| 176 |
+
starsCount Int @default(0)
|
| 177 |
+
|
| 178 |
+
creatorId String?
|
| 179 |
+
creator User? @relation("WorkflowCreator", fields: [creatorId], references: [id])
|
| 180 |
+
|
| 181 |
+
runs WorkflowRun[]
|
| 182 |
+
|
| 183 |
+
createdAt DateTime @default(now())
|
| 184 |
+
updatedAt DateTime @updatedAt
|
| 185 |
+
|
| 186 |
+
@@index([slug])
|
| 187 |
+
@@index([creatorId])
|
| 188 |
+
@@index([visibility])
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
model WorkflowRun {
|
| 192 |
+
id String @id @default(cuid())
|
| 193 |
+
|
| 194 |
+
workflowId String
|
| 195 |
+
workflow Workflow @relation(fields: [workflowId], references: [id], onDelete: Cascade)
|
| 196 |
+
|
| 197 |
+
userId String? // Who ran it
|
| 198 |
+
|
| 199 |
+
name String? // Optional name for the run (user can name it)
|
| 200 |
+
|
| 201 |
+
variables Json? // The variable values used for this run
|
| 202 |
+
outputs Json // Array of step outputs
|
| 203 |
+
model String // Model used for the run
|
| 204 |
+
|
| 205 |
+
createdAt DateTime @default(now())
|
| 206 |
+
|
| 207 |
+
@@index([workflowId])
|
| 208 |
+
@@index([userId])
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
// Saved prompt execution results
|
| 212 |
+
model SavedPromptRun {
|
| 213 |
+
id String @id @default(cuid())
|
| 214 |
+
|
| 215 |
+
promptId String
|
| 216 |
+
prompt Prompt @relation(fields: [promptId], references: [id], onDelete: Cascade)
|
| 217 |
+
|
| 218 |
+
userId String?
|
| 219 |
+
|
| 220 |
+
name String?
|
| 221 |
+
variables Json? // Variable values used
|
| 222 |
+
output String @db.Text // The AI output
|
| 223 |
+
model String
|
| 224 |
+
|
| 225 |
+
createdAt DateTime @default(now())
|
| 226 |
+
|
| 227 |
+
@@index([promptId])
|
| 228 |
+
@@index([userId])
|
| 229 |
+
}
|
| 230 |
+
|
| 231 |
+
// Saved AI tool execution results
|
| 232 |
+
model SavedToolRun {
|
| 233 |
+
id String @id @default(cuid())
|
| 234 |
+
|
| 235 |
+
toolSlug String // Tool identifier (no relation, tools are static)
|
| 236 |
+
|
| 237 |
+
userId String?
|
| 238 |
+
|
| 239 |
+
name String?
|
| 240 |
+
inputs Json? // Input values used
|
| 241 |
+
output String @db.Text // The AI output
|
| 242 |
+
model String
|
| 243 |
+
|
| 244 |
+
createdAt DateTime @default(now())
|
| 245 |
+
|
| 246 |
+
@@index([toolSlug])
|
| 247 |
+
@@index([userId])
|
| 248 |
+
}
|
| 249 |
+
|
| 250 |
+
// Saved model comparison results (Thunderdome / Performance)
|
| 251 |
+
model SavedComparison {
|
| 252 |
+
id String @id @default(cuid())
|
| 253 |
+
|
| 254 |
+
type String // "thunderdome" or "performance"
|
| 255 |
+
|
| 256 |
+
userId String?
|
| 257 |
+
|
| 258 |
+
name String?
|
| 259 |
+
prompt String @db.Text // The prompt used
|
| 260 |
+
results Json // Array of model results with outputs
|
| 261 |
+
winner String? // Winning model (for thunderdome)
|
| 262 |
+
|
| 263 |
+
createdAt DateTime @default(now())
|
| 264 |
+
|
| 265 |
+
@@index([userId])
|
| 266 |
+
@@index([type])
|
| 267 |
+
}
|
| 268 |
+
|
| 269 |
+
// ============================================
|
| 270 |
+
// NEW FEATURES - December 2025
|
| 271 |
+
// ============================================
|
| 272 |
+
|
| 273 |
+
// Comments on prompts (threaded discussions)
|
| 274 |
+
model Comment {
|
| 275 |
+
id String @id @default(cuid())
|
| 276 |
+
content String @db.Text
|
| 277 |
+
|
| 278 |
+
promptId String
|
| 279 |
+
prompt Prompt @relation(fields: [promptId], references: [id], onDelete: Cascade)
|
| 280 |
+
|
| 281 |
+
userId String
|
| 282 |
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
| 283 |
+
|
| 284 |
+
parentId String? // For threaded replies
|
| 285 |
+
parent Comment? @relation("CommentReplies", fields: [parentId], references: [id], onDelete: Cascade)
|
| 286 |
+
replies Comment[] @relation("CommentReplies")
|
| 287 |
+
|
| 288 |
+
// Per-user like tracking (replaces plain Int to prevent spam)
|
| 289 |
+
commentLikes CommentLike[]
|
| 290 |
+
likesCount Int @default(0) // Denormalized count for quick display
|
| 291 |
+
|
| 292 |
+
createdAt DateTime @default(now())
|
| 293 |
+
updatedAt DateTime @updatedAt
|
| 294 |
+
|
| 295 |
+
@@index([promptId])
|
| 296 |
+
@@index([userId])
|
| 297 |
+
@@index([parentId])
|
| 298 |
+
}
|
| 299 |
+
|
| 300 |
+
// Per-user comment likes — prevents duplicate/spam likes
|
| 301 |
+
model CommentLike {
|
| 302 |
+
commentId String
|
| 303 |
+
comment Comment @relation(fields: [commentId], references: [id], onDelete: Cascade)
|
| 304 |
+
|
| 305 |
+
userId String
|
| 306 |
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
| 307 |
+
|
| 308 |
+
createdAt DateTime @default(now())
|
| 309 |
+
|
| 310 |
+
@@id([commentId, userId])
|
| 311 |
+
@@index([commentId])
|
| 312 |
+
@@index([userId])
|
| 313 |
+
}
|
| 314 |
+
|
| 315 |
+
// Image Generation Prompts (Midjourney, DALL-E, Stable Diffusion, FLUX)
|
| 316 |
+
model ImagePrompt {
|
| 317 |
+
id String @id @default(cuid())
|
| 318 |
+
slug String @unique
|
| 319 |
+
title String
|
| 320 |
+
description String?
|
| 321 |
+
|
| 322 |
+
// The actual prompt text
|
| 323 |
+
prompt String @db.Text
|
| 324 |
+
negativePrompt String? @db.Text // For SD/FLUX
|
| 325 |
+
|
| 326 |
+
// Target model
|
| 327 |
+
model String // midjourney, dalle, stable-diffusion, flux, leonardo
|
| 328 |
+
modelVersion String? // v6.1, SDXL, FLUX.1-dev, etc.
|
| 329 |
+
|
| 330 |
+
// Image settings
|
| 331 |
+
aspectRatio String? // 16:9, 1:1, 9:16, etc.
|
| 332 |
+
style String? // photorealistic, anime, cartoon, etc.
|
| 333 |
+
|
| 334 |
+
// Generated image preview (URL)
|
| 335 |
+
previewImage String?
|
| 336 |
+
gallery String[] @default([]) // Multiple generated images
|
| 337 |
+
|
| 338 |
+
// Metadata
|
| 339 |
+
category String? // portrait, landscape, abstract, etc.
|
| 340 |
+
tags String[]
|
| 341 |
+
visibility String @default("public")
|
| 342 |
+
|
| 343 |
+
// Stats
|
| 344 |
+
totalUses Int @default(0)
|
| 345 |
+
votesCount Int @default(0)
|
| 346 |
+
savesCount Int @default(0)
|
| 347 |
+
|
| 348 |
+
creatorId String?
|
| 349 |
+
creator User? @relation(fields: [creatorId], references: [id])
|
| 350 |
+
|
| 351 |
+
votes ImagePromptVote[]
|
| 352 |
+
|
| 353 |
+
createdAt DateTime @default(now())
|
| 354 |
+
updatedAt DateTime @updatedAt
|
| 355 |
+
|
| 356 |
+
@@index([slug])
|
| 357 |
+
@@index([model])
|
| 358 |
+
@@index([creatorId])
|
| 359 |
+
@@index([category])
|
| 360 |
+
}
|
| 361 |
+
|
| 362 |
+
// Votes for image prompts
|
| 363 |
+
model ImagePromptVote {
|
| 364 |
+
id String @id @default(cuid())
|
| 365 |
+
|
| 366 |
+
imagePromptId String
|
| 367 |
+
imagePrompt ImagePrompt @relation(fields: [imagePromptId], references: [id], onDelete: Cascade)
|
| 368 |
+
|
| 369 |
+
userId String
|
| 370 |
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
| 371 |
+
|
| 372 |
+
value Int @default(1) // 1 for upvote, -1 for downvote
|
| 373 |
+
|
| 374 |
+
createdAt DateTime @default(now())
|
| 375 |
+
|
| 376 |
+
@@unique([imagePromptId, userId])
|
| 377 |
+
@@index([imagePromptId])
|
| 378 |
+
@@index([userId])
|
| 379 |
+
}
|
| 380 |
+
|
| 381 |
+
// Character/Persona System
|
| 382 |
+
model Character {
|
| 383 |
+
id String @id @default(cuid())
|
| 384 |
+
slug String @unique
|
| 385 |
+
name String
|
| 386 |
+
description String? @db.Text
|
| 387 |
+
|
| 388 |
+
// Persona definition
|
| 389 |
+
personality String @db.Text // Character's personality traits
|
| 390 |
+
background String? @db.Text // Character's backstory
|
| 391 |
+
systemPrompt String @db.Text // The actual system prompt
|
| 392 |
+
|
| 393 |
+
// Visual representation
|
| 394 |
+
avatar String? // URL to avatar image
|
| 395 |
+
style String? // anime, realistic, cartoon, etc.
|
| 396 |
+
|
| 397 |
+
// Metadata
|
| 398 |
+
category String? // assistant, roleplay, educational, etc.
|
| 399 |
+
tags String[]
|
| 400 |
+
visibility String @default("public")
|
| 401 |
+
|
| 402 |
+
// Conversation settings
|
| 403 |
+
responseStyle String? // formal, casual, playful, etc.
|
| 404 |
+
temperature Float @default(0.7)
|
| 405 |
+
|
| 406 |
+
// Stats
|
| 407 |
+
totalChats Int @default(0)
|
| 408 |
+
votesCount Int @default(0)
|
| 409 |
+
savesCount Int @default(0)
|
| 410 |
+
|
| 411 |
+
creatorId String?
|
| 412 |
+
creator User? @relation(fields: [creatorId], references: [id])
|
| 413 |
+
|
| 414 |
+
votes CharacterVote[]
|
| 415 |
+
|
| 416 |
+
createdAt DateTime @default(now())
|
| 417 |
+
updatedAt DateTime @updatedAt
|
| 418 |
+
|
| 419 |
+
@@index([slug])
|
| 420 |
+
@@index([creatorId])
|
| 421 |
+
@@index([category])
|
| 422 |
+
}
|
| 423 |
+
|
| 424 |
+
// Votes for characters
|
| 425 |
+
model CharacterVote {
|
| 426 |
+
id String @id @default(cuid())
|
| 427 |
+
|
| 428 |
+
characterId String
|
| 429 |
+
character Character @relation(fields: [characterId], references: [id], onDelete: Cascade)
|
| 430 |
+
|
| 431 |
+
userId String
|
| 432 |
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
| 433 |
+
|
| 434 |
+
value Int @default(1)
|
| 435 |
+
|
| 436 |
+
createdAt DateTime @default(now())
|
| 437 |
+
|
| 438 |
+
@@unique([characterId, userId])
|
| 439 |
+
@@index([characterId])
|
| 440 |
+
@@index([userId])
|
| 441 |
+
}
|
| 442 |
+
|
| 443 |
+
// Engagement metrics tracking
|
| 444 |
+
model EngagementMetric {
|
| 445 |
+
id String @id @default(cuid())
|
| 446 |
+
|
| 447 |
+
// What was engaged with
|
| 448 |
+
targetType String // prompt, imagePrompt, character, workflow
|
| 449 |
+
targetId String
|
| 450 |
+
|
| 451 |
+
// Type of engagement
|
| 452 |
+
action String // view, use, save, share, copy
|
| 453 |
+
|
| 454 |
+
userId String? // Null for anonymous
|
| 455 |
+
ipHash String? // For anonymous tracking
|
| 456 |
+
|
| 457 |
+
createdAt DateTime @default(now())
|
| 458 |
+
|
| 459 |
+
@@index([targetType, targetId])
|
| 460 |
+
@@index([action])
|
| 461 |
+
@@index([createdAt])
|
| 462 |
+
}
|
| 463 |
+
|
| 464 |
+
// Extension Analytics - tracks prompt usage from extension
|
| 465 |
+
model ExtensionAnalytics {
|
| 466 |
+
id String @id @default(cuid())
|
| 467 |
+
|
| 468 |
+
// User tracking
|
| 469 |
+
userId String? // Null for anonymous users
|
| 470 |
+
deviceId String // Unique device identifier from extension
|
| 471 |
+
|
| 472 |
+
// What action was taken
|
| 473 |
+
action String // inject, capture, broadcast, shortcut_use, context_menu
|
| 474 |
+
|
| 475 |
+
// Platform info
|
| 476 |
+
platform String // chatgpt, claude, gemini, etc.
|
| 477 |
+
platforms String[] @default([]) // For broadcast (multiple platforms)
|
| 478 |
+
|
| 479 |
+
// Prompt info (optional - for inject/capture)
|
| 480 |
+
promptId String? // If from OpenPrompt library
|
| 481 |
+
promptTitle String?
|
| 482 |
+
promptText String? @db.Text // For captured prompts
|
| 483 |
+
|
| 484 |
+
// Source info
|
| 485 |
+
source String @default("extension") // extension, website
|
| 486 |
+
|
| 487 |
+
// Metadata
|
| 488 |
+
metadata Json? // Additional data like shortcut used, etc.
|
| 489 |
+
|
| 490 |
+
createdAt DateTime @default(now())
|
| 491 |
+
|
| 492 |
+
@@index([userId])
|
| 493 |
+
@@index([deviceId])
|
| 494 |
+
@@index([action])
|
| 495 |
+
@@index([platform])
|
| 496 |
+
@@index([createdAt])
|
| 497 |
+
}
|
| 498 |
+
|
| 499 |
+
// Captured prompts from AI chats (saved by users via extension)
|
| 500 |
+
model CapturedPrompt {
|
| 501 |
+
id String @id @default(cuid())
|
| 502 |
+
|
| 503 |
+
userId String?
|
| 504 |
+
deviceId String
|
| 505 |
+
|
| 506 |
+
title String?
|
| 507 |
+
text String @db.Text
|
| 508 |
+
|
| 509 |
+
platform String // Where it was captured from
|
| 510 |
+
url String? // Original URL
|
| 511 |
+
|
| 512 |
+
// If saved to library
|
| 513 |
+
savedToLibrary Boolean @default(false)
|
| 514 |
+
promptId String? // If converted to a Prompt
|
| 515 |
+
|
| 516 |
+
tags String[] @default([])
|
| 517 |
+
|
| 518 |
+
createdAt DateTime @default(now())
|
| 519 |
+
|
| 520 |
+
@@index([userId])
|
| 521 |
+
@@index([deviceId])
|
| 522 |
+
@@index([platform])
|
| 523 |
+
@@index([createdAt])
|
| 524 |
+
}
|
| 525 |
+
|
| 526 |
+
// ============================================
|
| 527 |
+
// COMMUNITY FEATURES - December 2025
|
| 528 |
+
// ============================================
|
| 529 |
+
|
| 530 |
+
// Forum Posts (Community Q&A)
|
| 531 |
+
model ForumPost {
|
| 532 |
+
id String @id @default(cuid())
|
| 533 |
+
title String
|
| 534 |
+
content String @db.Text
|
| 535 |
+
category String // general, help, showcase, feature-request, tips
|
| 536 |
+
|
| 537 |
+
userId String
|
| 538 |
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
| 539 |
+
|
| 540 |
+
replies ForumReply[]
|
| 541 |
+
|
| 542 |
+
views Int @default(0)
|
| 543 |
+
likes Int @default(0)
|
| 544 |
+
isPinned Boolean @default(false)
|
| 545 |
+
isClosed Boolean @default(false)
|
| 546 |
+
|
| 547 |
+
createdAt DateTime @default(now())
|
| 548 |
+
updatedAt DateTime @updatedAt
|
| 549 |
+
|
| 550 |
+
@@index([userId])
|
| 551 |
+
@@index([category])
|
| 552 |
+
@@index([createdAt])
|
| 553 |
+
}
|
| 554 |
+
|
| 555 |
+
// Forum Replies
|
| 556 |
+
model ForumReply {
|
| 557 |
+
id String @id @default(cuid())
|
| 558 |
+
content String @db.Text
|
| 559 |
+
|
| 560 |
+
postId String
|
| 561 |
+
post ForumPost @relation(fields: [postId], references: [id], onDelete: Cascade)
|
| 562 |
+
|
| 563 |
+
userId String
|
| 564 |
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
| 565 |
+
|
| 566 |
+
parentId String? // For nested replies
|
| 567 |
+
|
| 568 |
+
likes Int @default(0)
|
| 569 |
+
isAccepted Boolean @default(false)
|
| 570 |
+
|
| 571 |
+
createdAt DateTime @default(now())
|
| 572 |
+
updatedAt DateTime @updatedAt
|
| 573 |
+
|
| 574 |
+
@@index([postId])
|
| 575 |
+
@@index([userId])
|
| 576 |
+
@@index([parentId])
|
| 577 |
+
}
|
| 578 |
+
|
| 579 |
+
// User Conversations (Messaging)
|
| 580 |
+
model Conversation {
|
| 581 |
+
id String @id @default(cuid())
|
| 582 |
+
|
| 583 |
+
participant1 String
|
| 584 |
+
participant2 String
|
| 585 |
+
|
| 586 |
+
messages Message[]
|
| 587 |
+
|
| 588 |
+
lastMessage String? @db.Text
|
| 589 |
+
lastActivity DateTime @default(now())
|
| 590 |
+
|
| 591 |
+
createdAt DateTime @default(now())
|
| 592 |
+
|
| 593 |
+
@@unique([participant1, participant2])
|
| 594 |
+
@@index([participant1])
|
| 595 |
+
@@index([participant2])
|
| 596 |
+
@@index([lastActivity])
|
| 597 |
+
}
|
| 598 |
+
|
| 599 |
+
// Direct Messages
|
| 600 |
+
model Message {
|
| 601 |
+
id String @id @default(cuid())
|
| 602 |
+
content String @db.Text
|
| 603 |
+
|
| 604 |
+
senderId String
|
| 605 |
+
|
| 606 |
+
conversationId String
|
| 607 |
+
conversation Conversation @relation(fields: [conversationId], references: [id], onDelete: Cascade)
|
| 608 |
+
|
| 609 |
+
isRead Boolean @default(false)
|
| 610 |
+
|
| 611 |
+
createdAt DateTime @default(now())
|
| 612 |
+
|
| 613 |
+
@@index([conversationId])
|
| 614 |
+
@@index([senderId])
|
| 615 |
+
@@index([createdAt])
|
| 616 |
+
}
|
| 617 |
+
|
| 618 |
+
// Character Memory (Persistent context)
|
| 619 |
+
model CharacterMemory {
|
| 620 |
+
id String @id @default(cuid())
|
| 621 |
+
|
| 622 |
+
characterId String
|
| 623 |
+
userId String
|
| 624 |
+
|
| 625 |
+
contextSummary String? @db.Text
|
| 626 |
+
recentMessages Json @default("[]")
|
| 627 |
+
userFacts Json @default("[]")
|
| 628 |
+
messageCount Int @default(0)
|
| 629 |
+
|
| 630 |
+
createdAt DateTime @default(now())
|
| 631 |
+
updatedAt DateTime @updatedAt
|
| 632 |
+
|
| 633 |
+
@@unique([characterId, userId])
|
| 634 |
+
@@index([characterId])
|
| 635 |
+
@@index([userId])
|
| 636 |
+
}
|
| 637 |
+
|
| 638 |
+
// ============================================
|
| 639 |
+
// NEW FEATURES - Follow & Notifications
|
| 640 |
+
// ============================================
|
| 641 |
+
|
| 642 |
+
// Follow system for creators
|
| 643 |
+
model Follow {
|
| 644 |
+
id String @id @default(cuid())
|
| 645 |
+
|
| 646 |
+
followerId String
|
| 647 |
+
follower User @relation("Follower", fields: [followerId], references: [id], onDelete: Cascade)
|
| 648 |
+
|
| 649 |
+
followingId String
|
| 650 |
+
following User @relation("Following", fields: [followingId], references: [id], onDelete: Cascade)
|
| 651 |
+
|
| 652 |
+
createdAt DateTime @default(now())
|
| 653 |
+
|
| 654 |
+
@@unique([followerId, followingId])
|
| 655 |
+
@@index([followerId])
|
| 656 |
+
@@index([followingId])
|
| 657 |
+
}
|
| 658 |
+
|
| 659 |
+
// Notification system
|
| 660 |
+
model Notification {
|
| 661 |
+
id String @id @default(cuid())
|
| 662 |
+
|
| 663 |
+
userId String // Who receives the notification
|
| 664 |
+
user User @relation("NotificationReceiver", fields: [userId], references: [id], onDelete: Cascade)
|
| 665 |
+
|
| 666 |
+
type String // star, remix, comment, follow, mention
|
| 667 |
+
title String
|
| 668 |
+
message String?
|
| 669 |
+
|
| 670 |
+
// What triggered the notification
|
| 671 |
+
actorId String? // Who performed the action
|
| 672 |
+
targetType String? // prompt, workflow, comment, etc.
|
| 673 |
+
targetId String?
|
| 674 |
+
targetSlug String? // For linking
|
| 675 |
+
|
| 676 |
+
isRead Boolean @default(false)
|
| 677 |
+
|
| 678 |
+
createdAt DateTime @default(now())
|
| 679 |
+
|
| 680 |
+
@@index([userId, isRead])
|
| 681 |
+
@@index([userId, createdAt])
|
| 682 |
+
}
|
| 683 |
+
|
| 684 |
+
// Prompt version history
|
| 685 |
+
model PromptVersion {
|
| 686 |
+
id String @id @default(cuid())
|
| 687 |
+
|
| 688 |
+
promptId String
|
| 689 |
+
prompt Prompt @relation(fields: [promptId], references: [id], onDelete: Cascade)
|
| 690 |
+
|
| 691 |
+
version Int @default(1)
|
| 692 |
+
title String
|
| 693 |
+
template String @db.Text
|
| 694 |
+
schema Json @default("{\"variables\":[]}")
|
| 695 |
+
changeNote String? // What changed
|
| 696 |
+
|
| 697 |
+
createdAt DateTime @default(now())
|
| 698 |
+
|
| 699 |
+
@@index([promptId])
|
| 700 |
+
@@index([promptId, version])
|
| 701 |
+
}
|
| 702 |
+
|
| 703 |
+
// ============================================
|
| 704 |
+
// BILLING — Stripe Integration
|
| 705 |
+
// ============================================
|
| 706 |
+
|
| 707 |
+
// Active subscription record (one per user)
|
| 708 |
+
model Subscription {
|
| 709 |
+
id String @id @default(cuid())
|
| 710 |
+
|
| 711 |
+
userId String @unique
|
| 712 |
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
| 713 |
+
|
| 714 |
+
// Stripe identifiers
|
| 715 |
+
stripeCustomerId String @unique
|
| 716 |
+
stripeSubscriptionId String @unique
|
| 717 |
+
stripePriceId String
|
| 718 |
+
|
| 719 |
+
// Plan info
|
| 720 |
+
plan String @default("pro") // pro, enterprise
|
| 721 |
+
status String // active, canceled, past_due, trialing, unpaid
|
| 722 |
+
|
| 723 |
+
// Billing cycle
|
| 724 |
+
currentPeriodStart DateTime
|
| 725 |
+
currentPeriodEnd DateTime
|
| 726 |
+
cancelAtPeriodEnd Boolean @default(false)
|
| 727 |
+
canceledAt DateTime?
|
| 728 |
+
|
| 729 |
+
createdAt DateTime @default(now())
|
| 730 |
+
updatedAt DateTime @updatedAt
|
| 731 |
+
|
| 732 |
+
@@index([userId])
|
| 733 |
+
@@index([stripeSubscriptionId])
|
| 734 |
+
}
|
| 735 |
+
|
| 736 |
+
// Records when a user purchases a premium prompt
|
| 737 |
+
model PromptPurchase {
|
| 738 |
+
id String @id @default(cuid())
|
| 739 |
+
|
| 740 |
+
userId String
|
| 741 |
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
| 742 |
+
|
| 743 |
+
promptId String
|
| 744 |
+
prompt Prompt @relation("PremiumPurchases", fields: [promptId], references: [id], onDelete: Cascade)
|
| 745 |
+
|
| 746 |
+
// Payment info
|
| 747 |
+
amount Int // in cents
|
| 748 |
+
currency String @default("usd")
|
| 749 |
+
stripePaymentIntentId String @unique
|
| 750 |
+
|
| 751 |
+
// Creator payout (80% to creator)
|
| 752 |
+
creatorPayout Int // 80% of amount in cents
|
| 753 |
+
creatorPaid Boolean @default(false)
|
| 754 |
+
|
| 755 |
+
createdAt DateTime @default(now())
|
| 756 |
+
|
| 757 |
+
@@unique([userId, promptId]) // One purchase per user per prompt
|
| 758 |
+
@@index([userId])
|
| 759 |
+
@@index([promptId])
|
| 760 |
+
}
|
prisma/seed-more.ts
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Pool } from 'pg'
|
| 2 |
+
import { PrismaPg } from '@prisma/adapter-pg'
|
| 3 |
+
import { PrismaClient } from '@prisma/client'
|
| 4 |
+
import 'dotenv/config'
|
| 5 |
+
|
| 6 |
+
const connectionString = process.env.DATABASE_URL
|
| 7 |
+
if (!connectionString) throw new Error('DATABASE_URL not set')
|
| 8 |
+
|
| 9 |
+
const pool = new Pool({ connectionString })
|
| 10 |
+
const adapter = new PrismaPg(pool)
|
| 11 |
+
const prisma = new PrismaClient({ adapter })
|
| 12 |
+
|
| 13 |
+
const morePrompts = [
|
| 14 |
+
// ============ CONTENT (10 more) ============
|
| 15 |
+
{ slug: 'newsletter-writer', title: 'Newsletter Writer Pro', description: 'Create engaging email newsletters that get opened and read.', category: 'Content', template: 'Write a {{frequency}} newsletter about {{topic}} for {{audience}}.\n\nInclude:\n- Catchy subject line\n- Personal intro\n- 3 main sections\n- CTA\n- P.S. line', schema: { variables: [{ name: 'topic', type: 'text', label: 'Newsletter Topic', required: true }, { name: 'frequency', type: 'dropdown', label: 'Frequency', options: ['Weekly', 'Monthly', 'Daily'] }, { name: 'audience', type: 'text', label: 'Target Audience' }] }, tags: ['newsletter', 'email'], totalRuns: 4532, starsCount: 312 },
|
| 16 |
+
{ slug: 'linkedin-post', title: 'LinkedIn Post Generator', description: 'Create viral LinkedIn posts that build your personal brand.', category: 'Content', template: 'Write a LinkedIn post about {{topic}}.\n\nStyle: {{style}}\nInclude hook, story, lesson, CTA.\nUse line breaks for readability.\nAdd relevant emojis.', schema: { variables: [{ name: 'topic', type: 'text', label: 'Post Topic', required: true }, { name: 'style', type: 'dropdown', label: 'Style', options: ['Story', 'Tips', 'Hot Take', 'Celebration', 'Question'] }] }, tags: ['linkedin', 'social-media'], totalRuns: 7654, starsCount: 543 },
|
| 17 |
+
{ slug: 'product-description', title: 'E-Commerce Product Description', description: 'Write product descriptions that convert browsers to buyers.', category: 'Content', template: 'Write a product description for:\n\nProduct: {{product}}\nPrice: {{price}}\nTarget Customer: {{customer}}\n\nInclude features, benefits, social proof elements, and urgency.', schema: { variables: [{ name: 'product', type: 'text', label: 'Product Name', required: true }, { name: 'price', type: 'text', label: 'Price' }, { name: 'customer', type: 'text', label: 'Target Customer' }] }, tags: ['ecommerce', 'product'], totalRuns: 8765, starsCount: 654 },
|
| 18 |
+
{ slug: 'podcast-outline', title: 'Podcast Episode Outline', description: 'Plan engaging podcast episodes with structured outlines.', category: 'Content', template: 'Create a podcast outline for:\n\nTopic: {{topic}}\nDuration: {{duration}} minutes\nFormat: {{format}}\n\nInclude intro hook, main segments, key talking points, and outro.', schema: { variables: [{ name: 'topic', type: 'text', label: 'Episode Topic', required: true }, { name: 'duration', type: 'dropdown', label: 'Duration', options: ['15', '30', '45', '60'] }, { name: 'format', type: 'dropdown', label: 'Format', options: ['Solo', 'Interview', 'Co-hosted', 'Panel'] }] }, tags: ['podcast', 'audio'], totalRuns: 3456, starsCount: 234 },
|
| 19 |
+
{ slug: 'press-release', title: 'Press Release Generator', description: 'Write professional press releases for announcements.', category: 'Content', template: 'Write a press release for:\n\nCompany: {{company}}\nAnnouncement: {{announcement}}\nQuote from: {{spokesperson}}\n\nFollow AP style, include headline, dateline, boilerplate.', schema: { variables: [{ name: 'company', type: 'text', label: 'Company Name', required: true }, { name: 'announcement', type: 'textarea', label: 'Announcement', required: true }, { name: 'spokesperson', type: 'text', label: 'Spokesperson Name' }] }, tags: ['pr', 'press'], totalRuns: 2345, starsCount: 187 },
|
| 20 |
+
{ slug: 'case-study', title: 'Case Study Writer', description: 'Create compelling case studies that showcase results.', category: 'Content', template: 'Write a case study about:\n\nClient: {{client}}\nChallenge: {{challenge}}\nSolution: {{solution}}\nResults: {{results}}\n\nFormat with Problem, Solution, Results sections.', schema: { variables: [{ name: 'client', type: 'text', label: 'Client/Company', required: true }, { name: 'challenge', type: 'textarea', label: 'Challenge', required: true }, { name: 'solution', type: 'textarea', label: 'Solution' }, { name: 'results', type: 'textarea', label: 'Results' }] }, tags: ['case-study', 'marketing'], totalRuns: 4567, starsCount: 345 },
|
| 21 |
+
{ slug: 'faq-generator', title: 'FAQ Generator', description: 'Generate comprehensive FAQs for products or services.', category: 'Content', template: 'Generate {{count}} FAQs for:\n\nProduct/Service: {{product}}\nTarget Audience: {{audience}}\n\nInclude common questions and detailed, helpful answers.', schema: { variables: [{ name: 'product', type: 'text', label: 'Product/Service', required: true }, { name: 'count', type: 'dropdown', label: 'Number of FAQs', options: ['5', '10', '15', '20'] }, { name: 'audience', type: 'text', label: 'Target Audience' }] }, tags: ['faq', 'support'], totalRuns: 5678, starsCount: 432 },
|
| 22 |
+
{ slug: 'meta-description', title: 'SEO Meta Description Writer', description: 'Write click-worthy meta descriptions for better CTR.', category: 'Content', template: 'Write 3 meta description variations for:\n\nPage: {{page}}\nKeyword: {{keyword}}\nTarget Length: 155 characters\n\nMake them compelling with CTAs.', schema: { variables: [{ name: 'page', type: 'text', label: 'Page Description', required: true }, { name: 'keyword', type: 'text', label: 'Target Keyword', required: true }] }, tags: ['seo', 'meta'], totalRuns: 8765, starsCount: 567 },
|
| 23 |
+
{ slug: 'book-summary', title: 'Book Summary Generator', description: 'Create comprehensive book summaries with key takeaways.', category: 'Content', template: 'Summarize the book "{{book}}" by {{author}}.\n\nInclude:\n- Overview\n- Key concepts\n- Main takeaways\n- Who should read it\n- Best quotes', schema: { variables: [{ name: 'book', type: 'text', label: 'Book Title', required: true }, { name: 'author', type: 'text', label: 'Author' }] }, tags: ['books', 'summary'], totalRuns: 6543, starsCount: 456 },
|
| 24 |
+
{ slug: 'caption-writer', title: 'Instagram Caption Writer', description: 'Write engaging Instagram captions with hashtags.', category: 'Content', template: 'Write an Instagram caption for:\n\nPhoto/Video: {{content}}\nBrand Voice: {{voice}}\nGoal: {{goal}}\n\nInclude emojis, call-to-action, and 10-15 relevant hashtags.', schema: { variables: [{ name: 'content', type: 'text', label: 'Content Description', required: true }, { name: 'voice', type: 'dropdown', label: 'Brand Voice', options: ['Professional', 'Fun', 'Inspirational', 'Educational'] }, { name: 'goal', type: 'dropdown', label: 'Goal', options: ['Engagement', 'Sales', 'Brand Awareness', 'Traffic'] }] }, tags: ['instagram', 'social-media', 'captions'], totalRuns: 9876, starsCount: 765 },
|
| 25 |
+
|
| 26 |
+
// ============ DEVELOPMENT (10 more) ============
|
| 27 |
+
{ slug: 'unit-test-writer', title: 'Unit Test Generator', description: 'Generate comprehensive unit tests for your code.', category: 'Development', template: 'Write unit tests for this {{language}} code:\n\n```{{language}}\n{{code}}\n```\n\nFramework: {{framework}}\n\nInclude edge cases, error handling, and mocks.', schema: { variables: [{ name: 'language', type: 'dropdown', label: 'Language', options: ['JavaScript', 'TypeScript', 'Python', 'Java', 'Go'], required: true }, { name: 'code', type: 'textarea', label: 'Code to Test', required: true }, { name: 'framework', type: 'text', label: 'Test Framework', placeholder: 'e.g., Jest, pytest' }] }, tags: ['testing', 'unit-tests'], totalRuns: 7654, starsCount: 543 },
|
| 28 |
+
{ slug: 'sql-query-builder', title: 'SQL Query Builder', description: 'Generate optimized SQL queries from natural language.', category: 'Development', template: 'Generate a SQL query for:\n\n{{request}}\n\nDatabase: {{database}}\nTables: {{tables}}\n\nOptimize for performance and include comments.', schema: { variables: [{ name: 'request', type: 'textarea', label: 'What do you need?', required: true }, { name: 'database', type: 'dropdown', label: 'Database', options: ['PostgreSQL', 'MySQL', 'SQLite', 'SQL Server'] }, { name: 'tables', type: 'textarea', label: 'Table Schemas' }] }, tags: ['sql', 'database'], totalRuns: 8765, starsCount: 654 },
|
| 29 |
+
{ slug: 'git-commit-message', title: 'Git Commit Message Generator', description: 'Write conventional, descriptive commit messages.', category: 'Development', template: 'Write a git commit message for these changes:\n\n{{changes}}\n\nStyle: {{style}}\n\nFollow conventional commits format.', schema: { variables: [{ name: 'changes', type: 'textarea', label: 'Describe your changes', required: true }, { name: 'style', type: 'dropdown', label: 'Style', options: ['Conventional Commits', 'Detailed', 'Brief'] }] }, tags: ['git', 'commits'], totalRuns: 12345, starsCount: 876 },
|
| 30 |
+
{ slug: 'dockerfile-generator', title: 'Dockerfile Generator', description: 'Create optimized Dockerfiles for your applications.', category: 'Development', template: 'Create a Dockerfile for:\n\nApp Type: {{appType}}\nBase Image: {{base}}\nRequirements: {{requirements}}\n\nOptimize for size and security.', schema: { variables: [{ name: 'appType', type: 'dropdown', label: 'Application Type', options: ['Node.js', 'Python', 'Go', 'Java', 'Rust', 'Multi-stage'], required: true }, { name: 'base', type: 'text', label: 'Base Image', placeholder: 'e.g., node:20-alpine' }, { name: 'requirements', type: 'textarea', label: 'Additional Requirements' }] }, tags: ['docker', 'devops'], totalRuns: 5678, starsCount: 432 },
|
| 31 |
+
{ slug: 'error-debugger', title: 'Error Message Debugger', description: 'Debug error messages and get solutions.', category: 'Development', template: 'Help me debug this error:\n\n```\n{{error}}\n```\n\nContext: {{context}}\nLanguage/Framework: {{language}}\n\nExplain the cause and provide solutions.', schema: { variables: [{ name: 'error', type: 'textarea', label: 'Error Message', required: true }, { name: 'context', type: 'textarea', label: 'What were you doing?' }, { name: 'language', type: 'text', label: 'Language/Framework' }] }, tags: ['debugging', 'errors'], totalRuns: 15678, starsCount: 1234 },
|
| 32 |
+
{ slug: 'api-request-builder', title: 'API Request Builder', description: 'Generate API requests in multiple languages.', category: 'Development', template: 'Generate API request code for:\n\nEndpoint: {{endpoint}}\nMethod: {{method}}\nHeaders: {{headers}}\nBody: {{body}}\n\nLanguages: {{languages}}', schema: { variables: [{ name: 'endpoint', type: 'text', label: 'API Endpoint', required: true }, { name: 'method', type: 'dropdown', label: 'Method', options: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'] }, { name: 'headers', type: 'textarea', label: 'Headers (JSON)' }, { name: 'body', type: 'textarea', label: 'Request Body' }, { name: 'languages', type: 'text', label: 'Languages', default: 'JavaScript, Python, cURL' }] }, tags: ['api', 'http'], totalRuns: 6789, starsCount: 543 },
|
| 33 |
+
{ slug: 'readme-generator', title: 'README.md Generator', description: 'Create professional README files for your projects.', category: 'Development', template: 'Generate a README.md for:\n\nProject: {{project}}\nDescription: {{description}}\nTech Stack: {{tech}}\nFeatures: {{features}}\n\nInclude badges, installation, usage, contributing.', schema: { variables: [{ name: 'project', type: 'text', label: 'Project Name', required: true }, { name: 'description', type: 'textarea', label: 'Description', required: true }, { name: 'tech', type: 'text', label: 'Tech Stack' }, { name: 'features', type: 'textarea', label: 'Key Features' }] }, tags: ['readme', 'documentation'], totalRuns: 9876, starsCount: 765 },
|
| 34 |
+
{ slug: 'cron-expression', title: 'Cron Expression Generator', description: 'Generate cron expressions from natural language.', category: 'Development', template: 'Generate a cron expression for:\n\n{{schedule}}\n\nProvide:\n- Cron expression\n- Explanation of each field\n- Example run times', schema: { variables: [{ name: 'schedule', type: 'text', label: 'Schedule Description', placeholder: 'e.g., Every Monday at 9 AM', required: true }] }, tags: ['cron', 'scheduling'], totalRuns: 4567, starsCount: 345 },
|
| 35 |
+
{ slug: 'shell-script', title: 'Shell Script Generator', description: 'Generate bash/shell scripts for automation.', category: 'Development', template: 'Write a shell script to:\n\n{{task}}\n\nOS: {{os}}\nShell: {{shell}}\n\nInclude error handling and comments.', schema: { variables: [{ name: 'task', type: 'textarea', label: 'What should the script do?', required: true }, { name: 'os', type: 'dropdown', label: 'Operating System', options: ['Linux', 'macOS', 'Windows (PowerShell)'] }, { name: 'shell', type: 'dropdown', label: 'Shell', options: ['bash', 'zsh', 'sh', 'PowerShell'] }] }, tags: ['bash', 'automation', 'scripts'], totalRuns: 7890, starsCount: 567 },
|
| 36 |
+
{ slug: 'prisma-schema', title: 'Prisma Schema Generator', description: 'Generate Prisma schemas from requirements.', category: 'Development', template: 'Generate a Prisma schema for:\n\n{{requirements}}\n\nDatabase: {{database}}\n\nInclude relations, indexes, and enums as needed.', schema: { variables: [{ name: 'requirements', type: 'textarea', label: 'Describe your data model', required: true }, { name: 'database', type: 'dropdown', label: 'Database', options: ['PostgreSQL', 'MySQL', 'SQLite', 'MongoDB'] }] }, tags: ['prisma', 'database', 'orm'], totalRuns: 3456, starsCount: 234 },
|
| 37 |
+
|
| 38 |
+
// ============ MARKETING (8 more) ============
|
| 39 |
+
{ slug: 'email-sequence', title: 'Email Sequence Creator', description: 'Create automated email sequences that convert.', category: 'Marketing', template: 'Create a {{emails}}-email sequence for:\n\nGoal: {{goal}}\nProduct: {{product}}\nAudience: {{audience}}\n\nInclude subject lines, preview text, and body for each.', schema: { variables: [{ name: 'emails', type: 'dropdown', label: 'Number of Emails', options: ['3', '5', '7', '10'], required: true }, { name: 'goal', type: 'dropdown', label: 'Sequence Goal', options: ['Welcome', 'Sales', 'Onboarding', 'Re-engagement'] }, { name: 'product', type: 'text', label: 'Product/Service' }, { name: 'audience', type: 'text', label: 'Target Audience' }] }, tags: ['email-marketing', 'automation'], totalRuns: 5678, starsCount: 432 },
|
| 40 |
+
{ slug: 'seo-keywords', title: 'SEO Keyword Research', description: 'Generate keyword ideas with search intent analysis.', category: 'Marketing', template: 'Generate keyword ideas for:\n\nTopic: {{topic}}\nNiche: {{niche}}\n\nFor each keyword, include:\n- Search intent\n- Difficulty estimate\n- Content type recommendation', schema: { variables: [{ name: 'topic', type: 'text', label: 'Main Topic', required: true }, { name: 'niche', type: 'text', label: 'Industry/Niche' }] }, tags: ['seo', 'keywords'], totalRuns: 7654, starsCount: 543 },
|
| 41 |
+
{ slug: 'ab-test-ideas', title: 'A/B Test Ideas Generator', description: 'Generate A/B test hypotheses to improve conversion.', category: 'Marketing', template: 'Generate A/B test ideas for:\n\nPage Type: {{page}}\nCurrent Conversion Rate: {{rate}}\nGoal: {{goal}}\n\nFor each test, include hypothesis, expected impact, and how to measure.', schema: { variables: [{ name: 'page', type: 'dropdown', label: 'Page Type', options: ['Landing Page', 'Checkout', 'Pricing', 'Homepage', 'Product Page'], required: true }, { name: 'rate', type: 'text', label: 'Current Conversion Rate' }, { name: 'goal', type: 'text', label: 'Improvement Goal' }] }, tags: ['ab-testing', 'cro'], totalRuns: 3456, starsCount: 234 },
|
| 42 |
+
{ slug: 'brand-voice', title: 'Brand Voice Guidelines', description: 'Define your brand voice and communication style.', category: 'Marketing', template: 'Create brand voice guidelines for:\n\nBrand: {{brand}}\nIndustry: {{industry}}\nTarget Audience: {{audience}}\nPersonality: {{personality}}\n\nInclude dos, don\'ts, and examples.', schema: { variables: [{ name: 'brand', type: 'text', label: 'Brand Name', required: true }, { name: 'industry', type: 'text', label: 'Industry' }, { name: 'audience', type: 'text', label: 'Target Audience' }, { name: 'personality', type: 'text', label: 'Brand Personality Traits' }] }, tags: ['branding', 'voice'], totalRuns: 4567, starsCount: 345 },
|
| 43 |
+
{ slug: 'competitor-analysis', title: 'Competitor Analysis', description: 'Analyze competitors and find opportunities.', category: 'Marketing', template: 'Analyze these competitors for {{company}}:\n\nCompetitors: {{competitors}}\n\nAnalyze:\n- Strengths & weaknesses\n- Pricing strategies\n- Marketing channels\n- Opportunity gaps', schema: { variables: [{ name: 'company', type: 'text', label: 'Your Company', required: true }, { name: 'competitors', type: 'textarea', label: 'Competitor Names', required: true }] }, tags: ['competitive-analysis', 'strategy'], totalRuns: 2345, starsCount: 187 },
|
| 44 |
+
{ slug: 'utm-generator', title: 'UTM Parameter Generator', description: 'Generate UTM parameters for campaign tracking.', category: 'Marketing', template: 'Generate UTM parameters for:\n\nCampaign: {{campaign}}\nChannels: {{channels}}\nBase URL: {{url}}\n\nProvide full URLs with proper naming conventions.', schema: { variables: [{ name: 'campaign', type: 'text', label: 'Campaign Name', required: true }, { name: 'channels', type: 'text', label: 'Marketing Channels', placeholder: 'e.g., email, twitter, facebook' }, { name: 'url', type: 'text', label: 'Base URL', required: true }] }, tags: ['utm', 'analytics'], totalRuns: 5678, starsCount: 432 },
|
| 45 |
+
{ slug: 'value-proposition', title: 'Value Proposition Canvas', description: 'Create a compelling value proposition.', category: 'Marketing', template: 'Create a value proposition for:\n\nProduct: {{product}}\nTarget Customer: {{customer}}\nMain Problem: {{problem}}\n\nUse the Value Proposition Canvas framework.', schema: { variables: [{ name: 'product', type: 'text', label: 'Product/Service', required: true }, { name: 'customer', type: 'text', label: 'Target Customer', required: true }, { name: 'problem', type: 'textarea', label: 'Main Problem You Solve' }] }, tags: ['value-proposition', 'positioning'], totalRuns: 4567, starsCount: 345 },
|
| 46 |
+
{ slug: 'influencer-brief', title: 'Influencer Brief Creator', description: 'Create briefs for influencer collaborations.', category: 'Marketing', template: 'Create an influencer brief for:\n\nBrand: {{brand}}\nCampaign: {{campaign}}\nPlatform: {{platform}}\nDeliverables: {{deliverables}}\n\nInclude brand guidelines, key messages, dos and don\'ts.', schema: { variables: [{ name: 'brand', type: 'text', label: 'Brand Name', required: true }, { name: 'campaign', type: 'text', label: 'Campaign Name' }, { name: 'platform', type: 'dropdown', label: 'Platform', options: ['Instagram', 'TikTok', 'YouTube', 'Twitter'] }, { name: 'deliverables', type: 'textarea', label: 'Deliverables' }] }, tags: ['influencer', 'social-media'], totalRuns: 3456, starsCount: 234 },
|
| 47 |
+
|
| 48 |
+
// ============ BUSINESS (8 more) ============
|
| 49 |
+
{ slug: 'meeting-agenda', title: 'Meeting Agenda Generator', description: 'Create structured meeting agendas.', category: 'Business', template: 'Create a meeting agenda for:\n\nMeeting Type: {{type}}\nDuration: {{duration}} minutes\nTopics: {{topics}}\nAttendees: {{attendees}}\n\nInclude time allocations and action items section.', schema: { variables: [{ name: 'type', type: 'dropdown', label: 'Meeting Type', options: ['Team Standup', 'Project Review', 'Brainstorm', 'Client Call', 'All Hands'], required: true }, { name: 'duration', type: 'dropdown', label: 'Duration (min)', options: ['15', '30', '45', '60'] }, { name: 'topics', type: 'textarea', label: 'Topics to Discuss' }, { name: 'attendees', type: 'text', label: 'Key Attendees' }] }, tags: ['meetings', 'productivity'], totalRuns: 8765, starsCount: 654 },
|
| 50 |
+
{ slug: 'okr-generator', title: 'OKR Generator', description: 'Create objectives and key results for teams.', category: 'Business', template: 'Generate OKRs for:\n\nTeam: {{team}}\nQuarter: {{quarter}}\nFocus Area: {{focus}}\nCurrent Challenges: {{challenges}}\n\nCreate 3-5 objectives with 3-4 key results each.', schema: { variables: [{ name: 'team', type: 'text', label: 'Team/Department', required: true }, { name: 'quarter', type: 'dropdown', label: 'Quarter', options: ['Q1', 'Q2', 'Q3', 'Q4'] }, { name: 'focus', type: 'text', label: 'Focus Area' }, { name: 'challenges', type: 'textarea', label: 'Current Challenges' }] }, tags: ['okrs', 'goals', 'planning'], totalRuns: 4567, starsCount: 345 },
|
| 51 |
+
{ slug: 'job-description', title: 'Job Description Writer', description: 'Write compelling job descriptions that attract talent.', category: 'Business', template: 'Write a job description for:\n\nPosition: {{position}}\nCompany: {{company}}\nLevel: {{level}}\nLocation: {{location}}\nKey Skills: {{skills}}\n\nMake it engaging and inclusive.', schema: { variables: [{ name: 'position', type: 'text', label: 'Job Title', required: true }, { name: 'company', type: 'text', label: 'Company Name' }, { name: 'level', type: 'dropdown', label: 'Level', options: ['Entry', 'Mid', 'Senior', 'Lead', 'Manager', 'Director'] }, { name: 'location', type: 'text', label: 'Location' }, { name: 'skills', type: 'textarea', label: 'Required Skills' }] }, tags: ['hiring', 'hr', 'jobs'], totalRuns: 6543, starsCount: 456 },
|
| 52 |
+
{ slug: 'swot-analysis', title: 'SWOT Analysis Generator', description: 'Create comprehensive SWOT analyses.', category: 'Business', template: 'Create a SWOT analysis for:\n\nCompany/Project: {{subject}}\nIndustry: {{industry}}\nContext: {{context}}\n\nProvide detailed Strengths, Weaknesses, Opportunities, and Threats.', schema: { variables: [{ name: 'subject', type: 'text', label: 'Company/Project', required: true }, { name: 'industry', type: 'text', label: 'Industry' }, { name: 'context', type: 'textarea', label: 'Context/Background' }] }, tags: ['swot', 'strategy', 'analysis'], totalRuns: 5678, starsCount: 432 },
|
| 53 |
+
{ slug: 'performance-review', title: 'Performance Review Helper', description: 'Write constructive performance reviews.', category: 'Business', template: 'Help write a performance review for:\n\nRole: {{role}}\nAchievements: {{achievements}}\nAreas to Improve: {{improvements}}\nTone: {{tone}}\n\nBe constructive and specific.', schema: { variables: [{ name: 'role', type: 'text', label: 'Employee Role', required: true }, { name: 'achievements', type: 'textarea', label: 'Key Achievements' }, { name: 'improvements', type: 'textarea', label: 'Areas for Improvement' }, { name: 'tone', type: 'dropdown', label: 'Tone', options: ['Highly Positive', 'Positive', 'Constructive', 'Needs Improvement'] }] }, tags: ['hr', 'reviews', 'management'], totalRuns: 7654, starsCount: 543 },
|
| 54 |
+
{ slug: 'invoice-email', title: 'Invoice & Payment Email', description: 'Write professional invoice and payment emails.', category: 'Business', template: 'Write {{type}} email for:\n\nClient: {{client}}\nAmount: {{amount}}\nProject: {{project}}\nDue Date: {{due}}\n\nBe professional but friendly.', schema: { variables: [{ name: 'type', type: 'dropdown', label: 'Email Type', options: ['Invoice', 'Payment Reminder', 'Overdue Notice', 'Payment Received'], required: true }, { name: 'client', type: 'text', label: 'Client Name' }, { name: 'amount', type: 'text', label: 'Amount' }, { name: 'project', type: 'text', label: 'Project Name' }, { name: 'due', type: 'text', label: 'Due Date' }] }, tags: ['invoice', 'payment', 'finance'], totalRuns: 4567, starsCount: 345 },
|
| 55 |
+
{ slug: 'sop-writer', title: 'SOP Document Writer', description: 'Create Standard Operating Procedures.', category: 'Business', template: 'Create an SOP for:\n\nProcess: {{process}}\nDepartment: {{department}}\nComplexity: {{complexity}}\n\nInclude purpose, scope, procedure steps, and exceptions.', schema: { variables: [{ name: 'process', type: 'text', label: 'Process Name', required: true }, { name: 'department', type: 'text', label: 'Department' }, { name: 'complexity', type: 'dropdown', label: 'Complexity', options: ['Simple', 'Moderate', 'Complex'] }] }, tags: ['sop', 'documentation', 'processes'], totalRuns: 3456, starsCount: 234 },
|
| 56 |
+
{ slug: 'pitch-deck', title: 'Pitch Deck Outline', description: 'Create investor pitch deck structure.', category: 'Business', template: 'Create a pitch deck outline for:\n\nStartup: {{startup}}\nStage: {{stage}}\nAsking: {{ask}}\nProblem: {{problem}}\n\nInclude all essential slides with talking points.', schema: { variables: [{ name: 'startup', type: 'text', label: 'Startup Name', required: true }, { name: 'stage', type: 'dropdown', label: 'Stage', options: ['Pre-seed', 'Seed', 'Series A', 'Series B+'] }, { name: 'ask', type: 'text', label: 'Funding Ask' }, { name: 'problem', type: 'textarea', label: 'Problem You Solve' }] }, tags: ['pitch', 'investors', 'startup'], totalRuns: 5678, starsCount: 432 },
|
| 57 |
+
|
| 58 |
+
// ============ EDUCATION (6 more) ============
|
| 59 |
+
{ slug: 'quiz-generator', title: 'Quiz Generator', description: 'Create quizzes and tests on any topic.', category: 'Education', template: 'Create a quiz on:\n\nTopic: {{topic}}\nDifficulty: {{difficulty}}\nQuestions: {{count}}\nType: {{type}}\n\nInclude answers and explanations.', schema: { variables: [{ name: 'topic', type: 'text', label: 'Topic', required: true }, { name: 'difficulty', type: 'dropdown', label: 'Difficulty', options: ['Easy', 'Medium', 'Hard'] }, { name: 'count', type: 'dropdown', label: 'Questions', options: ['5', '10', '15', '20'] }, { name: 'type', type: 'dropdown', label: 'Question Type', options: ['Multiple Choice', 'True/False', 'Short Answer', 'Mixed'] }] }, tags: ['quiz', 'testing', 'education'], totalRuns: 8765, starsCount: 654 },
|
| 60 |
+
{ slug: 'study-guide', title: 'Study Guide Creator', description: 'Create comprehensive study guides.', category: 'Education', template: 'Create a study guide for:\n\nSubject: {{subject}}\nTopics: {{topics}}\nExam Type: {{exam}}\n\nInclude key concepts, practice questions, and memory tips.', schema: { variables: [{ name: 'subject', type: 'text', label: 'Subject', required: true }, { name: 'topics', type: 'textarea', label: 'Topics to Cover' }, { name: 'exam', type: 'dropdown', label: 'Exam Type', options: ['Multiple Choice', 'Essay', 'Practical', 'Mixed'] }] }, tags: ['study', 'exam', 'learning'], totalRuns: 6543, starsCount: 456 },
|
| 61 |
+
{ slug: 'flashcard-maker', title: 'Flashcard Generator', description: 'Create flashcards for spaced repetition learning.', category: 'Education', template: 'Create {{count}} flashcards for:\n\nTopic: {{topic}}\nLevel: {{level}}\n\nFormat: Front | Back\nInclude hints where helpful.', schema: { variables: [{ name: 'topic', type: 'text', label: 'Topic', required: true }, { name: 'count', type: 'dropdown', label: 'Number', options: ['10', '20', '30', '50'] }, { name: 'level', type: 'dropdown', label: 'Level', options: ['Beginner', 'Intermediate', 'Advanced'] }] }, tags: ['flashcards', 'memorization'], totalRuns: 7654, starsCount: 543 },
|
| 62 |
+
{ slug: 'essay-outline', title: 'Essay Outline Generator', description: 'Structure essays with compelling arguments.', category: 'Education', template: 'Create an essay outline for:\n\nTopic: {{topic}}\nThesis: {{thesis}}\nLength: {{length}} words\nStyle: {{style}}\n\nInclude intro, body paragraphs, and conclusion structure.', schema: { variables: [{ name: 'topic', type: 'text', label: 'Essay Topic', required: true }, { name: 'thesis', type: 'text', label: 'Thesis Statement' }, { name: 'length', type: 'dropdown', label: 'Length (words)', options: ['500', '1000', '1500', '2000', '3000'] }, { name: 'style', type: 'dropdown', label: 'Style', options: ['Argumentative', 'Expository', 'Narrative', 'Persuasive'] }] }, tags: ['essay', 'writing', 'academic'], totalRuns: 9876, starsCount: 765 },
|
| 63 |
+
{ slug: 'presentation-outline', title: 'Presentation Outline', description: 'Create structured presentation outlines.', category: 'Education', template: 'Create a presentation outline for:\n\nTopic: {{topic}}\nSlides: {{slides}}\nAudience: {{audience}}\nDuration: {{duration}} minutes\n\nInclude slide titles and key points.', schema: { variables: [{ name: 'topic', type: 'text', label: 'Presentation Topic', required: true }, { name: 'slides', type: 'dropdown', label: 'Number of Slides', options: ['5', '10', '15', '20'] }, { name: 'audience', type: 'text', label: 'Audience' }, { name: 'duration', type: 'dropdown', label: 'Duration (min)', options: ['5', '10', '15', '30'] }] }, tags: ['presentation', 'slides', 'speaking'], totalRuns: 5678, starsCount: 432 },
|
| 64 |
+
{ slug: 'language-tutor', title: 'Language Learning Tutor', description: 'Practice conversations in any language.', category: 'Education', template: 'Help me practice {{language}}.\n\nLevel: {{level}}\nScenario: {{scenario}}\nFocus: {{focus}}\n\nCorrect my mistakes and explain grammar.', schema: { variables: [{ name: 'language', type: 'dropdown', label: 'Language', options: ['Spanish', 'French', 'German', 'Japanese', 'Chinese', 'Italian', 'Portuguese', 'Korean'], required: true }, { name: 'level', type: 'dropdown', label: 'Level', options: ['Beginner', 'Intermediate', 'Advanced'] }, { name: 'scenario', type: 'text', label: 'Scenario', placeholder: 'e.g., ordering at a restaurant' }, { name: 'focus', type: 'dropdown', label: 'Focus Area', options: ['Conversation', 'Grammar', 'Vocabulary', 'Pronunciation'] }] }, tags: ['language', 'learning', 'tutor'], totalRuns: 12345, starsCount: 876 },
|
| 65 |
+
|
| 66 |
+
// ============ CREATIVE (6 more) ============
|
| 67 |
+
{ slug: 'character-creator', title: 'Character Creator', description: 'Create detailed fictional characters.', category: 'Creative', template: 'Create a character for {{genre}} fiction:\n\nRole: {{role}}\nPersonality: {{personality}}\nBackground: {{background}}\n\nInclude appearance, motivation, flaws, and quirks.', schema: { variables: [{ name: 'genre', type: 'dropdown', label: 'Genre', options: ['Fantasy', 'Sci-Fi', 'Mystery', 'Romance', 'Horror', 'Literary'], required: true }, { name: 'role', type: 'text', label: 'Character Role', placeholder: 'e.g., protagonist, villain' }, { name: 'personality', type: 'text', label: 'Personality Traits' }, { name: 'background', type: 'text', label: 'Background' }] }, tags: ['character', 'fiction', 'writing'], totalRuns: 6543, starsCount: 456 },
|
| 68 |
+
{ slug: 'world-builder', title: 'World Builder', description: 'Create detailed fictional worlds.', category: 'Creative', template: 'Build a world for {{genre}} fiction:\n\nSetting: {{setting}}\nMagic/Tech Level: {{tech}}\nKey Features: {{features}}\n\nInclude geography, culture, history, and unique elements.', schema: { variables: [{ name: 'genre', type: 'dropdown', label: 'Genre', options: ['Fantasy', 'Sci-Fi', 'Post-Apocalyptic', 'Steampunk', 'Urban Fantasy'], required: true }, { name: 'setting', type: 'text', label: 'Basic Setting', placeholder: 'e.g., floating cities' }, { name: 'tech', type: 'dropdown', label: 'Tech/Magic Level', options: ['Low', 'Medium', 'High', 'Mixed'] }, { name: 'features', type: 'textarea', label: 'Key Features' }] }, tags: ['worldbuilding', 'fiction'], totalRuns: 4567, starsCount: 345 },
|
| 69 |
+
{ slug: 'poetry-generator', title: 'Poetry Generator', description: 'Write poems in various styles.', category: 'Creative', template: 'Write a {{style}} poem about:\n\n{{topic}}\n\nMood: {{mood}}\nLength: {{length}}\n\nInclude vivid imagery and emotion.', schema: { variables: [{ name: 'style', type: 'dropdown', label: 'Style', options: ['Free Verse', 'Sonnet', 'Haiku', 'Limerick', 'Ballad', 'Ode'], required: true }, { name: 'topic', type: 'text', label: 'Topic/Theme', required: true }, { name: 'mood', type: 'dropdown', label: 'Mood', options: ['Happy', 'Sad', 'Romantic', 'Angry', 'Peaceful', 'Dark'] }, { name: 'length', type: 'dropdown', label: 'Length', options: ['Short', 'Medium', 'Long'] }] }, tags: ['poetry', 'creative-writing'], totalRuns: 5678, starsCount: 432 },
|
| 70 |
+
{ slug: 'dialogue-writer', title: 'Dialogue Writer', description: 'Write realistic dialogue between characters.', category: 'Creative', template: 'Write dialogue between:\n\nCharacter 1: {{char1}}\nCharacter 2: {{char2}}\nSituation: {{situation}}\nTone: {{tone}}\n\nMake it natural and reveal character.', schema: { variables: [{ name: 'char1', type: 'text', label: 'Character 1', required: true }, { name: 'char2', type: 'text', label: 'Character 2', required: true }, { name: 'situation', type: 'textarea', label: 'Situation/Context' }, { name: 'tone', type: 'dropdown', label: 'Tone', options: ['Serious', 'Humorous', 'Tense', 'Romantic', 'Casual'] }] }, tags: ['dialogue', 'screenplay'], totalRuns: 4567, starsCount: 345 },
|
| 71 |
+
{ slug: 'plot-twist', title: 'Plot Twist Generator', description: 'Generate surprising plot twists.', category: 'Creative', template: 'Generate plot twists for:\n\nGenre: {{genre}}\nCurrent Setup: {{setup}}\nCharacters: {{characters}}\n\nProvide 5 plot twist ideas from subtle to shocking.', schema: { variables: [{ name: 'genre', type: 'dropdown', label: 'Genre', options: ['Mystery', 'Thriller', 'Fantasy', 'Romance', 'Horror', 'Drama'], required: true }, { name: 'setup', type: 'textarea', label: 'Current Plot Setup', required: true }, { name: 'characters', type: 'textarea', label: 'Main Characters' }] }, tags: ['plot', 'storytelling'], totalRuns: 3456, starsCount: 234 },
|
| 72 |
+
{ slug: 'name-generator', title: 'Character Name Generator', description: 'Generate character names for any setting.', category: 'Creative', template: 'Generate {{count}} names for:\n\nSetting: {{setting}}\nGender: {{gender}}\nVibe: {{vibe}}\n\nInclude meaning or origin if relevant.', schema: { variables: [{ name: 'count', type: 'dropdown', label: 'Number of Names', options: ['5', '10', '20'], required: true }, { name: 'setting', type: 'dropdown', label: 'Setting', options: ['Fantasy', 'Sci-Fi', 'Historical', 'Modern', 'Mythological'] }, { name: 'gender', type: 'dropdown', label: 'Gender', options: ['Male', 'Female', 'Neutral', 'Mixed'] }, { name: 'vibe', type: 'text', label: 'Vibe/Feeling', placeholder: 'e.g., elegant, fierce, mysterious' }] }, tags: ['names', 'fantasy', 'characters'], totalRuns: 8765, starsCount: 654 },
|
| 73 |
+
|
| 74 |
+
// ============ RESEARCH (2 more) ============
|
| 75 |
+
{ slug: 'literature-review', title: 'Literature Review Helper', description: 'Structure and write literature reviews.', category: 'Research', template: 'Help write a literature review on:\n\nTopic: {{topic}}\nField: {{field}}\nSources Summary: {{sources}}\n\nOrganize thematically with synthesis and gaps.', schema: { variables: [{ name: 'topic', type: 'text', label: 'Research Topic', required: true }, { name: 'field', type: 'text', label: 'Academic Field' }, { name: 'sources', type: 'textarea', label: 'Brief Summary of Sources' }] }, tags: ['literature-review', 'academic'], totalRuns: 3456, starsCount: 234 },
|
| 76 |
+
{ slug: 'hypothesis-generator', title: 'Research Hypothesis Generator', description: 'Generate testable research hypotheses.', category: 'Research', template: 'Generate research hypotheses for:\n\nResearch Question: {{question}}\nField: {{field}}\nVariables: {{variables}}\n\nInclude null and alternative hypotheses with rationale.', schema: { variables: [{ name: 'question', type: 'textarea', label: 'Research Question', required: true }, { name: 'field', type: 'text', label: 'Research Field' }, { name: 'variables', type: 'textarea', label: 'Key Variables' }] }, tags: ['hypothesis', 'research-methods'], totalRuns: 2345, starsCount: 187 },
|
| 77 |
+
]
|
| 78 |
+
|
| 79 |
+
async function seedMore() {
|
| 80 |
+
console.log('🌱 Adding 50 more prompts...\n')
|
| 81 |
+
|
| 82 |
+
let created = 0
|
| 83 |
+
let skipped = 0
|
| 84 |
+
|
| 85 |
+
for (const prompt of morePrompts) {
|
| 86 |
+
const existing = await prisma.prompt.findUnique({
|
| 87 |
+
where: { slug: prompt.slug },
|
| 88 |
+
})
|
| 89 |
+
|
| 90 |
+
if (existing) {
|
| 91 |
+
console.log(` ⏭️ Skipping "${prompt.title}"`)
|
| 92 |
+
skipped++
|
| 93 |
+
continue
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
await prisma.prompt.create({
|
| 97 |
+
data: {
|
| 98 |
+
slug: prompt.slug,
|
| 99 |
+
title: prompt.title,
|
| 100 |
+
description: prompt.description,
|
| 101 |
+
template: prompt.template,
|
| 102 |
+
schema: prompt.schema,
|
| 103 |
+
category: prompt.category,
|
| 104 |
+
tags: prompt.tags,
|
| 105 |
+
totalRuns: prompt.totalRuns,
|
| 106 |
+
starsCount: prompt.starsCount,
|
| 107 |
+
remixesCount: 0,
|
| 108 |
+
modelDefault: 'gpt-4o-mini',
|
| 109 |
+
modelAllowed: ['gpt-4o-mini', 'gpt-4o', 'claude-3-5-sonnet'],
|
| 110 |
+
visibility: 'public',
|
| 111 |
+
},
|
| 112 |
+
})
|
| 113 |
+
console.log(` ✅ Created "${prompt.title}" [${prompt.category}]`)
|
| 114 |
+
created++
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
console.log(`\n✨ Done! Created ${created} prompts, skipped ${skipped}`)
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
seedMore()
|
| 121 |
+
.catch((e) => {
|
| 122 |
+
console.error('❌ Seeding failed:', e)
|
| 123 |
+
process.exit(1)
|
| 124 |
+
})
|
| 125 |
+
.finally(async () => {
|
| 126 |
+
await prisma.$disconnect()
|
| 127 |
+
await pool.end()
|
| 128 |
+
})
|
prisma/seed.ts
ADDED
|
@@ -0,0 +1,644 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Pool } from 'pg'
|
| 2 |
+
import { PrismaPg } from '@prisma/adapter-pg'
|
| 3 |
+
import { PrismaClient } from '@prisma/client'
|
| 4 |
+
import 'dotenv/config'
|
| 5 |
+
|
| 6 |
+
const connectionString = process.env.DATABASE_URL
|
| 7 |
+
if (!connectionString) {
|
| 8 |
+
throw new Error('DATABASE_URL not set')
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
const pool = new Pool({ connectionString })
|
| 12 |
+
const adapter = new PrismaPg(pool)
|
| 13 |
+
const prisma = new PrismaClient({ adapter })
|
| 14 |
+
|
| 15 |
+
const seedPrompts = [
|
| 16 |
+
// ============ CONTENT ============
|
| 17 |
+
{
|
| 18 |
+
slug: 'blog-writer-pro',
|
| 19 |
+
title: 'AI Blog Writer Pro',
|
| 20 |
+
description: 'Generate SEO-optimized, engaging blog posts on any topic with customizable tone and length.',
|
| 21 |
+
template: `Write a comprehensive blog post about {{topic}}.
|
| 22 |
+
|
| 23 |
+
Tone: {{tone}}
|
| 24 |
+
Target Length: {{length}} words
|
| 25 |
+
Target Audience: {{audience}}
|
| 26 |
+
|
| 27 |
+
Requirements:
|
| 28 |
+
- Write an attention-grabbing headline
|
| 29 |
+
- Include an engaging introduction that hooks the reader
|
| 30 |
+
- Cover 3-5 main points with detailed explanations
|
| 31 |
+
- Use subheadings for better readability
|
| 32 |
+
- Include relevant examples or case studies
|
| 33 |
+
- End with a strong conclusion and call-to-action
|
| 34 |
+
- Optimize for SEO with natural keyword usage`,
|
| 35 |
+
schema: {
|
| 36 |
+
variables: [
|
| 37 |
+
{ name: 'topic', type: 'text', label: 'Blog Topic', placeholder: 'e.g., The Future of AI in Healthcare', required: true },
|
| 38 |
+
{ name: 'tone', type: 'dropdown', label: 'Writing Tone', options: ['Professional', 'Casual', 'Humorous', 'Academic', 'Inspirational'], default: 'Professional' },
|
| 39 |
+
{ name: 'length', type: 'dropdown', label: 'Target Length (words)', options: ['500', '1000', '1500', '2000'], default: '1000' },
|
| 40 |
+
{ name: 'audience', type: 'text', label: 'Target Audience', placeholder: 'e.g., Tech professionals', default: 'General audience' },
|
| 41 |
+
],
|
| 42 |
+
},
|
| 43 |
+
category: 'Content',
|
| 44 |
+
tags: ['blog', 'writing', 'seo', 'content-marketing'],
|
| 45 |
+
totalRuns: 12453,
|
| 46 |
+
starsCount: 892,
|
| 47 |
+
},
|
| 48 |
+
{
|
| 49 |
+
slug: 'twitter-thread-generator',
|
| 50 |
+
title: 'Viral Twitter Thread Generator',
|
| 51 |
+
description: 'Create engaging Twitter/X threads that get likes and retweets.',
|
| 52 |
+
template: `Create a viral Twitter thread about {{topic}}.
|
| 53 |
+
|
| 54 |
+
Style: {{style}}
|
| 55 |
+
Number of tweets: {{count}}
|
| 56 |
+
Call-to-action: {{cta}}
|
| 57 |
+
|
| 58 |
+
Each tweet should:
|
| 59 |
+
- Start with a hook
|
| 60 |
+
- Be under 280 characters
|
| 61 |
+
- Use emojis strategically
|
| 62 |
+
- End with engagement prompts
|
| 63 |
+
|
| 64 |
+
Format as:
|
| 65 |
+
🧵 1/{{count}}
|
| 66 |
+
[Tweet content]
|
| 67 |
+
|
| 68 |
+
Continue for all tweets.`,
|
| 69 |
+
schema: {
|
| 70 |
+
variables: [
|
| 71 |
+
{ name: 'topic', type: 'text', label: 'Thread Topic', placeholder: 'e.g., 10 AI tools that will 10x your productivity', required: true },
|
| 72 |
+
{ name: 'style', type: 'dropdown', label: 'Style', options: ['Educational', 'Storytelling', 'Tips & Tricks', 'Hot Take', 'Thread of Resources'] },
|
| 73 |
+
{ name: 'count', type: 'dropdown', label: 'Number of Tweets', options: ['5', '7', '10', '15'], default: '10' },
|
| 74 |
+
{ name: 'cta', type: 'text', label: 'Call-to-Action', placeholder: 'e.g., Follow for more AI tips', default: 'Like & Retweet if helpful!' },
|
| 75 |
+
],
|
| 76 |
+
},
|
| 77 |
+
category: 'Content',
|
| 78 |
+
tags: ['twitter', 'social-media', 'viral', 'threads'],
|
| 79 |
+
totalRuns: 8234,
|
| 80 |
+
starsCount: 567,
|
| 81 |
+
},
|
| 82 |
+
{
|
| 83 |
+
slug: 'youtube-script-writer',
|
| 84 |
+
title: 'YouTube Video Script Writer',
|
| 85 |
+
description: 'Create engaging YouTube scripts with hooks, chapters, and CTAs.',
|
| 86 |
+
template: `Write a YouTube video script about {{topic}}.
|
| 87 |
+
|
| 88 |
+
Video Length: {{length}} minutes
|
| 89 |
+
Style: {{style}}
|
| 90 |
+
Target Audience: {{audience}}
|
| 91 |
+
|
| 92 |
+
Include:
|
| 93 |
+
1. **HOOK** (First 30 seconds) - Grab attention immediately
|
| 94 |
+
2. **INTRO** - Brief channel intro and video overview
|
| 95 |
+
3. **MAIN CONTENT** - Divided into timestamped chapters
|
| 96 |
+
4. **EXAMPLES** - Real-world examples or demonstrations
|
| 97 |
+
5. **OUTRO** - Summary + CTA (like, subscribe, comment)
|
| 98 |
+
|
| 99 |
+
Format with timestamps like:
|
| 100 |
+
[0:00] Hook
|
| 101 |
+
[0:30] Intro
|
| 102 |
+
...`,
|
| 103 |
+
schema: {
|
| 104 |
+
variables: [
|
| 105 |
+
{ name: 'topic', type: 'text', label: 'Video Topic', placeholder: 'e.g., How to build a SaaS in 30 days', required: true },
|
| 106 |
+
{ name: 'length', type: 'dropdown', label: 'Video Length', options: ['5', '10', '15', '20'], default: '10' },
|
| 107 |
+
{ name: 'style', type: 'dropdown', label: 'Content Style', options: ['Tutorial', 'Review', 'Commentary', 'Story', 'Listicle'] },
|
| 108 |
+
{ name: 'audience', type: 'text', label: 'Target Audience', placeholder: 'e.g., Beginner developers' },
|
| 109 |
+
],
|
| 110 |
+
},
|
| 111 |
+
category: 'Content',
|
| 112 |
+
tags: ['youtube', 'video', 'script', 'content-creation'],
|
| 113 |
+
totalRuns: 6789,
|
| 114 |
+
starsCount: 445,
|
| 115 |
+
},
|
| 116 |
+
|
| 117 |
+
// ============ DEVELOPMENT ============
|
| 118 |
+
{
|
| 119 |
+
slug: 'code-review-assistant',
|
| 120 |
+
title: 'Senior Dev Code Review',
|
| 121 |
+
description: 'Get detailed, constructive code reviews like from a senior developer.',
|
| 122 |
+
template: `Act as a senior software engineer doing a thorough code review.
|
| 123 |
+
|
| 124 |
+
Language: {{language}}
|
| 125 |
+
|
| 126 |
+
\`\`\`{{language}}
|
| 127 |
+
{{code}}
|
| 128 |
+
\`\`\`
|
| 129 |
+
|
| 130 |
+
Provide your review with:
|
| 131 |
+
|
| 132 |
+
## 📊 Overall Assessment
|
| 133 |
+
Brief quality rating and summary
|
| 134 |
+
|
| 135 |
+
## ✅ What's Good
|
| 136 |
+
Highlight good practices
|
| 137 |
+
|
| 138 |
+
## 🚨 Critical Issues
|
| 139 |
+
Security vulnerabilities, bugs, performance issues
|
| 140 |
+
|
| 141 |
+
## ⚠️ Suggestions
|
| 142 |
+
Code improvements and best practices
|
| 143 |
+
|
| 144 |
+
## 🔧 Refactored Code
|
| 145 |
+
Show the improved version with comments`,
|
| 146 |
+
schema: {
|
| 147 |
+
variables: [
|
| 148 |
+
{ name: 'language', type: 'dropdown', label: 'Language', options: ['JavaScript', 'TypeScript', 'Python', 'Java', 'Go', 'Rust', 'C++', 'C#'], default: 'TypeScript', required: true },
|
| 149 |
+
{ name: 'code', type: 'textarea', label: 'Code to Review', placeholder: 'Paste your code here...', required: true },
|
| 150 |
+
],
|
| 151 |
+
},
|
| 152 |
+
category: 'Development',
|
| 153 |
+
tags: ['code-review', 'programming', 'best-practices'],
|
| 154 |
+
totalRuns: 15672,
|
| 155 |
+
starsCount: 1024,
|
| 156 |
+
},
|
| 157 |
+
{
|
| 158 |
+
slug: 'regex-generator',
|
| 159 |
+
title: 'Regex Pattern Generator',
|
| 160 |
+
description: 'Generate and explain regex patterns for any use case.',
|
| 161 |
+
template: `Create a regex pattern for: {{description}}
|
| 162 |
+
|
| 163 |
+
Language/Tool: {{language}}
|
| 164 |
+
|
| 165 |
+
Please provide:
|
| 166 |
+
|
| 167 |
+
1. **The Regex Pattern**
|
| 168 |
+
\`\`\`
|
| 169 |
+
[pattern here]
|
| 170 |
+
\`\`\`
|
| 171 |
+
|
| 172 |
+
2. **Explanation**
|
| 173 |
+
Break down each part of the regex
|
| 174 |
+
|
| 175 |
+
3. **Test Cases**
|
| 176 |
+
- ✅ Should match: [examples]
|
| 177 |
+
- ❌ Should NOT match: [examples]
|
| 178 |
+
|
| 179 |
+
4. **Code Example**
|
| 180 |
+
Show how to use it in {{language}}
|
| 181 |
+
|
| 182 |
+
5. **Common Variations**
|
| 183 |
+
Any alternative patterns for edge cases`,
|
| 184 |
+
schema: {
|
| 185 |
+
variables: [
|
| 186 |
+
{ name: 'description', type: 'textarea', label: 'What should the regex match?', placeholder: 'e.g., Email addresses with common domains only', required: true },
|
| 187 |
+
{ name: 'language', type: 'dropdown', label: 'Language/Tool', options: ['JavaScript', 'Python', 'Java', 'PHP', 'Go', 'grep', 'sed'], default: 'JavaScript' },
|
| 188 |
+
],
|
| 189 |
+
},
|
| 190 |
+
category: 'Development',
|
| 191 |
+
tags: ['regex', 'programming', 'patterns'],
|
| 192 |
+
totalRuns: 9876,
|
| 193 |
+
starsCount: 678,
|
| 194 |
+
},
|
| 195 |
+
{
|
| 196 |
+
slug: 'api-documentation',
|
| 197 |
+
title: 'API Documentation Generator',
|
| 198 |
+
description: 'Generate comprehensive API documentation from code or descriptions.',
|
| 199 |
+
template: `Generate professional API documentation for:
|
| 200 |
+
|
| 201 |
+
{{api_description}}
|
| 202 |
+
|
| 203 |
+
Format: {{format}}
|
| 204 |
+
|
| 205 |
+
Include:
|
| 206 |
+
- **Endpoint Overview**
|
| 207 |
+
- **Authentication** requirements
|
| 208 |
+
- **Request** (method, URL, headers, body with types)
|
| 209 |
+
- **Response** (success and error responses with examples)
|
| 210 |
+
- **Code Examples** in {{languages}}
|
| 211 |
+
- **Rate Limits** and error handling
|
| 212 |
+
- **Changelog** section`,
|
| 213 |
+
schema: {
|
| 214 |
+
variables: [
|
| 215 |
+
{ name: 'api_description', type: 'textarea', label: 'API Description or Code', placeholder: 'Describe your API endpoints or paste code...', required: true },
|
| 216 |
+
{ name: 'format', type: 'dropdown', label: 'Documentation Format', options: ['OpenAPI/Swagger', 'Markdown', 'REST API Blueprint', 'Postman Collection'], default: 'Markdown' },
|
| 217 |
+
{ name: 'languages', type: 'text', label: 'Code Example Languages', placeholder: 'e.g., JavaScript, Python, cURL', default: 'JavaScript, Python, cURL' },
|
| 218 |
+
],
|
| 219 |
+
},
|
| 220 |
+
category: 'Development',
|
| 221 |
+
tags: ['api', 'documentation', 'swagger', 'developer-tools'],
|
| 222 |
+
totalRuns: 5432,
|
| 223 |
+
starsCount: 389,
|
| 224 |
+
},
|
| 225 |
+
|
| 226 |
+
// ============ MARKETING ============
|
| 227 |
+
{
|
| 228 |
+
slug: 'landing-page-copy',
|
| 229 |
+
title: 'High-Converting Landing Page Copy',
|
| 230 |
+
description: 'Generate landing page copy that converts visitors into customers.',
|
| 231 |
+
template: `Create high-converting landing page copy for:
|
| 232 |
+
|
| 233 |
+
Product/Service: {{product}}
|
| 234 |
+
Target Audience: {{audience}}
|
| 235 |
+
Unique Value: {{uvp}}
|
| 236 |
+
Key Benefits: {{benefits}}
|
| 237 |
+
|
| 238 |
+
Generate:
|
| 239 |
+
|
| 240 |
+
# 🎯 HERO SECTION
|
| 241 |
+
- Headline (max 10 words, powerful)
|
| 242 |
+
- Subheadline (expand the promise)
|
| 243 |
+
- CTA button text
|
| 244 |
+
|
| 245 |
+
# 😰 PROBLEM SECTION
|
| 246 |
+
Pain points your audience faces
|
| 247 |
+
|
| 248 |
+
# ✨ SOLUTION SECTION
|
| 249 |
+
How you solve it
|
| 250 |
+
|
| 251 |
+
# 💎 BENEFITS (not features)
|
| 252 |
+
3-4 benefit blocks with icons
|
| 253 |
+
|
| 254 |
+
# 🏆 SOCIAL PROOF
|
| 255 |
+
Testimonial templates and stats placement
|
| 256 |
+
|
| 257 |
+
# 🚀 FINAL CTA
|
| 258 |
+
Urgency-driven closing`,
|
| 259 |
+
schema: {
|
| 260 |
+
variables: [
|
| 261 |
+
{ name: 'product', type: 'text', label: 'Product/Service', placeholder: 'e.g., CloudSync - AI-Powered Note Taking', required: true },
|
| 262 |
+
{ name: 'audience', type: 'text', label: 'Target Audience', placeholder: 'e.g., Busy professionals who take lots of notes', required: true },
|
| 263 |
+
{ name: 'uvp', type: 'textarea', label: 'Unique Value Proposition', placeholder: 'What makes you different?', required: true },
|
| 264 |
+
{ name: 'benefits', type: 'textarea', label: 'Key Benefits', placeholder: 'List 3-5 main benefits' },
|
| 265 |
+
],
|
| 266 |
+
},
|
| 267 |
+
category: 'Marketing',
|
| 268 |
+
tags: ['copywriting', 'landing-page', 'conversion'],
|
| 269 |
+
totalRuns: 11234,
|
| 270 |
+
starsCount: 876,
|
| 271 |
+
},
|
| 272 |
+
{
|
| 273 |
+
slug: 'ad-copy-generator',
|
| 274 |
+
title: 'Facebook/Google Ad Copy',
|
| 275 |
+
description: 'Create high-CTR ad copy for paid campaigns.',
|
| 276 |
+
template: `Create {{platform}} ad copy for:
|
| 277 |
+
|
| 278 |
+
Product: {{product}}
|
| 279 |
+
Target Audience: {{audience}}
|
| 280 |
+
Offer: {{offer}}
|
| 281 |
+
Tone: {{tone}}
|
| 282 |
+
|
| 283 |
+
Generate 5 variations of:
|
| 284 |
+
- **Headline** ({{platform}} character limit)
|
| 285 |
+
- **Description**
|
| 286 |
+
- **Call-to-Action**
|
| 287 |
+
|
| 288 |
+
For each variation, include:
|
| 289 |
+
- 📊 Estimated CTR potential
|
| 290 |
+
- 🎯 Best audience type
|
| 291 |
+
- 💡 A/B testing suggestion`,
|
| 292 |
+
schema: {
|
| 293 |
+
variables: [
|
| 294 |
+
{ name: 'platform', type: 'dropdown', label: 'Platform', options: ['Facebook/Instagram', 'Google Ads', 'LinkedIn', 'TikTok'], default: 'Facebook/Instagram', required: true },
|
| 295 |
+
{ name: 'product', type: 'text', label: 'Product/Service', placeholder: 'e.g., Online Fitness Coaching', required: true },
|
| 296 |
+
{ name: 'audience', type: 'text', label: 'Target Audience', placeholder: 'e.g., Women 25-45 interested in weight loss' },
|
| 297 |
+
{ name: 'offer', type: 'text', label: 'Offer/Discount', placeholder: 'e.g., 50% off first month' },
|
| 298 |
+
{ name: 'tone', type: 'dropdown', label: 'Tone', options: ['Professional', 'Friendly', 'Urgent', 'Playful', 'Luxury'], default: 'Friendly' },
|
| 299 |
+
],
|
| 300 |
+
},
|
| 301 |
+
category: 'Marketing',
|
| 302 |
+
tags: ['ads', 'facebook', 'google-ads', 'copywriting'],
|
| 303 |
+
totalRuns: 8765,
|
| 304 |
+
starsCount: 654,
|
| 305 |
+
},
|
| 306 |
+
|
| 307 |
+
// ============ BUSINESS ============
|
| 308 |
+
{
|
| 309 |
+
slug: 'professional-email',
|
| 310 |
+
title: 'Professional Email Composer',
|
| 311 |
+
description: 'Write perfect professional emails for any situation.',
|
| 312 |
+
template: `Write a professional email for:
|
| 313 |
+
|
| 314 |
+
Purpose: {{purpose}}
|
| 315 |
+
Recipient: {{recipient}}
|
| 316 |
+
Key Points: {{key_points}}
|
| 317 |
+
Tone: {{tone}}
|
| 318 |
+
|
| 319 |
+
Include:
|
| 320 |
+
- Appropriate subject line
|
| 321 |
+
- Professional greeting
|
| 322 |
+
- Clear, concise body
|
| 323 |
+
- Specific call-to-action
|
| 324 |
+
- Professional sign-off
|
| 325 |
+
|
| 326 |
+
Context: {{context}}`,
|
| 327 |
+
schema: {
|
| 328 |
+
variables: [
|
| 329 |
+
{ name: 'purpose', type: 'dropdown', label: 'Email Purpose', options: ['Follow-up', 'Cold Outreach', 'Thank You', 'Request', 'Apology', 'Announcement', 'Negotiation', 'Introduction'], required: true },
|
| 330 |
+
{ name: 'recipient', type: 'text', label: 'Recipient', placeholder: 'e.g., Hiring Manager at Google', required: true },
|
| 331 |
+
{ name: 'key_points', type: 'textarea', label: 'Key Points', placeholder: 'What do you need to communicate?', required: true },
|
| 332 |
+
{ name: 'tone', type: 'dropdown', label: 'Tone', options: ['Formal', 'Friendly Professional', 'Casual', 'Urgent'], default: 'Friendly Professional' },
|
| 333 |
+
{ name: 'context', type: 'textarea', label: 'Additional Context', placeholder: 'Any relevant background...' },
|
| 334 |
+
],
|
| 335 |
+
},
|
| 336 |
+
category: 'Business',
|
| 337 |
+
tags: ['email', 'professional', 'communication'],
|
| 338 |
+
totalRuns: 18234,
|
| 339 |
+
starsCount: 1234,
|
| 340 |
+
},
|
| 341 |
+
{
|
| 342 |
+
slug: 'business-proposal',
|
| 343 |
+
title: 'Business Proposal Generator',
|
| 344 |
+
description: 'Create compelling business proposals that win clients.',
|
| 345 |
+
template: `Create a professional business proposal for:
|
| 346 |
+
|
| 347 |
+
Client: {{client}}
|
| 348 |
+
Project: {{project}}
|
| 349 |
+
Budget Range: {{budget}}
|
| 350 |
+
Timeline: {{timeline}}
|
| 351 |
+
|
| 352 |
+
Include these sections:
|
| 353 |
+
|
| 354 |
+
1. **Executive Summary**
|
| 355 |
+
2. **Understanding of Needs**
|
| 356 |
+
3. **Proposed Solution**
|
| 357 |
+
4. **Scope of Work** (detailed deliverables)
|
| 358 |
+
5. **Timeline & Milestones**
|
| 359 |
+
6. **Investment** (pricing breakdown)
|
| 360 |
+
7. **About Us**
|
| 361 |
+
8. **Next Steps**`,
|
| 362 |
+
schema: {
|
| 363 |
+
variables: [
|
| 364 |
+
{ name: 'client', type: 'text', label: 'Client Name/Company', placeholder: 'e.g., Acme Corporation', required: true },
|
| 365 |
+
{ name: 'project', type: 'textarea', label: 'Project Description', placeholder: 'Describe the project...', required: true },
|
| 366 |
+
{ name: 'budget', type: 'text', label: 'Budget Range', placeholder: 'e.g., $5,000 - $10,000' },
|
| 367 |
+
{ name: 'timeline', type: 'text', label: 'Timeline', placeholder: 'e.g., 4-6 weeks' },
|
| 368 |
+
],
|
| 369 |
+
},
|
| 370 |
+
category: 'Business',
|
| 371 |
+
tags: ['proposal', 'business', 'sales', 'client'],
|
| 372 |
+
totalRuns: 6543,
|
| 373 |
+
starsCount: 456,
|
| 374 |
+
},
|
| 375 |
+
|
| 376 |
+
// ============ EDUCATION ============
|
| 377 |
+
{
|
| 378 |
+
slug: 'lesson-plan-creator',
|
| 379 |
+
title: 'AI Lesson Plan Creator',
|
| 380 |
+
description: 'Create comprehensive lesson plans for any subject and grade level.',
|
| 381 |
+
template: `Create a detailed lesson plan for:
|
| 382 |
+
|
| 383 |
+
Subject: {{subject}}
|
| 384 |
+
Topic: {{topic}}
|
| 385 |
+
Grade Level: {{grade}}
|
| 386 |
+
Duration: {{duration}} minutes
|
| 387 |
+
|
| 388 |
+
Include:
|
| 389 |
+
1. **Learning Objectives** (3-5 measurable goals)
|
| 390 |
+
2. **Materials Needed**
|
| 391 |
+
3. **Warm-Up Activity** (5-10 min)
|
| 392 |
+
4. **Main Instruction** (step-by-step)
|
| 393 |
+
5. **Guided Practice**
|
| 394 |
+
6. **Independent Practice**
|
| 395 |
+
7. **Assessment** (formative/summative)
|
| 396 |
+
8. **Differentiation** (for different learners)
|
| 397 |
+
9. **Homework/Extension**`,
|
| 398 |
+
schema: {
|
| 399 |
+
variables: [
|
| 400 |
+
{ name: 'subject', type: 'dropdown', label: 'Subject', options: ['Math', 'Science', 'English', 'History', 'Art', 'Music', 'Computer Science', 'Physical Education'], required: true },
|
| 401 |
+
{ name: 'topic', type: 'text', label: 'Topic', placeholder: 'e.g., Introduction to Fractions', required: true },
|
| 402 |
+
{ name: 'grade', type: 'dropdown', label: 'Grade Level', options: ['K-2', '3-5', '6-8', '9-12', 'College', 'Adult'], default: '6-8' },
|
| 403 |
+
{ name: 'duration', type: 'dropdown', label: 'Duration (minutes)', options: ['30', '45', '60', '90'], default: '45' },
|
| 404 |
+
],
|
| 405 |
+
},
|
| 406 |
+
category: 'Education',
|
| 407 |
+
tags: ['lesson-plan', 'teaching', 'education'],
|
| 408 |
+
totalRuns: 7654,
|
| 409 |
+
starsCount: 543,
|
| 410 |
+
},
|
| 411 |
+
{
|
| 412 |
+
slug: 'explain-like-im-5',
|
| 413 |
+
title: 'Explain Like I\'m 5',
|
| 414 |
+
description: 'Break down complex topics into simple, fun explanations.',
|
| 415 |
+
template: `Explain {{topic}} like I'm {{age}} years old.
|
| 416 |
+
|
| 417 |
+
Use:
|
| 418 |
+
- Simple words
|
| 419 |
+
- Fun analogies (like games, toys, food)
|
| 420 |
+
- Short sentences
|
| 421 |
+
- Emojis where appropriate
|
| 422 |
+
- A fun example at the end
|
| 423 |
+
|
| 424 |
+
Topic complexity: {{complexity}}
|
| 425 |
+
|
| 426 |
+
Make it engaging and memorable!`,
|
| 427 |
+
schema: {
|
| 428 |
+
variables: [
|
| 429 |
+
{ name: 'topic', type: 'text', label: 'Topic to Explain', placeholder: 'e.g., How does WiFi work?', required: true },
|
| 430 |
+
{ name: 'age', type: 'dropdown', label: 'Target Age', options: ['5', '8', '10', '12', '15'], default: '5' },
|
| 431 |
+
{ name: 'complexity', type: 'dropdown', label: 'Original Complexity', options: ['Simple', 'Moderate', 'Complex', 'Expert-level'], default: 'Complex' },
|
| 432 |
+
],
|
| 433 |
+
},
|
| 434 |
+
category: 'Education',
|
| 435 |
+
tags: ['learning', 'simple', 'explanation', 'eli5'],
|
| 436 |
+
totalRuns: 12345,
|
| 437 |
+
starsCount: 876,
|
| 438 |
+
},
|
| 439 |
+
|
| 440 |
+
// ============ CREATIVE ============
|
| 441 |
+
{
|
| 442 |
+
slug: 'story-generator',
|
| 443 |
+
title: 'Creative Story Generator',
|
| 444 |
+
description: 'Generate creative stories in any genre with compelling characters.',
|
| 445 |
+
template: `Write a {{genre}} story with:
|
| 446 |
+
|
| 447 |
+
Main Character: {{character}}
|
| 448 |
+
Setting: {{setting}}
|
| 449 |
+
Conflict: {{conflict}}
|
| 450 |
+
Length: {{length}}
|
| 451 |
+
Tone: {{tone}}
|
| 452 |
+
|
| 453 |
+
Include:
|
| 454 |
+
- Vivid descriptions
|
| 455 |
+
- Dialogue
|
| 456 |
+
- Character development
|
| 457 |
+
- Satisfying resolution`,
|
| 458 |
+
schema: {
|
| 459 |
+
variables: [
|
| 460 |
+
{ name: 'genre', type: 'dropdown', label: 'Genre', options: ['Fantasy', 'Sci-Fi', 'Romance', 'Mystery', 'Horror', 'Adventure', 'Comedy', 'Drama'], required: true },
|
| 461 |
+
{ name: 'character', type: 'text', label: 'Main Character', placeholder: 'e.g., A time-traveling librarian', required: true },
|
| 462 |
+
{ name: 'setting', type: 'text', label: 'Setting', placeholder: 'e.g., Victorian London' },
|
| 463 |
+
{ name: 'conflict', type: 'text', label: 'Central Conflict', placeholder: 'e.g., Must prevent a paradox' },
|
| 464 |
+
{ name: 'length', type: 'dropdown', label: 'Length', options: ['Flash Fiction (500 words)', 'Short Story (1500 words)', 'Long Story (3000 words)'], default: 'Short Story (1500 words)' },
|
| 465 |
+
{ name: 'tone', type: 'dropdown', label: 'Tone', options: ['Light & Fun', 'Dark & Serious', 'Suspenseful', 'Heartwarming'], default: 'Light & Fun' },
|
| 466 |
+
],
|
| 467 |
+
},
|
| 468 |
+
category: 'Creative',
|
| 469 |
+
tags: ['story', 'fiction', 'creative-writing'],
|
| 470 |
+
totalRuns: 9876,
|
| 471 |
+
starsCount: 765,
|
| 472 |
+
},
|
| 473 |
+
{
|
| 474 |
+
slug: 'song-lyrics-writer',
|
| 475 |
+
title: 'Song Lyrics Writer',
|
| 476 |
+
description: 'Create original song lyrics in any genre with custom themes.',
|
| 477 |
+
template: `Write song lyrics about {{theme}}.
|
| 478 |
+
|
| 479 |
+
Genre: {{genre}}
|
| 480 |
+
Mood: {{mood}}
|
| 481 |
+
Structure: {{structure}}
|
| 482 |
+
|
| 483 |
+
Include:
|
| 484 |
+
- Verse 1
|
| 485 |
+
- Chorus
|
| 486 |
+
- Verse 2
|
| 487 |
+
- Chorus
|
| 488 |
+
- Bridge
|
| 489 |
+
- Final Chorus
|
| 490 |
+
|
| 491 |
+
Make it catchy, emotional, and singable!`,
|
| 492 |
+
schema: {
|
| 493 |
+
variables: [
|
| 494 |
+
{ name: 'theme', type: 'text', label: 'Song Theme', placeholder: 'e.g., Overcoming heartbreak', required: true },
|
| 495 |
+
{ name: 'genre', type: 'dropdown', label: 'Genre', options: ['Pop', 'Rock', 'Hip-Hop', 'Country', 'R&B', 'Indie', 'EDM'], default: 'Pop' },
|
| 496 |
+
{ name: 'mood', type: 'dropdown', label: 'Mood', options: ['Happy', 'Sad', 'Empowering', 'Romantic', 'Angry', 'Nostalgic'], default: 'Empowering' },
|
| 497 |
+
{ name: 'structure', type: 'dropdown', label: 'Structure', options: ['Standard', 'Extended', 'Minimal'], default: 'Standard' },
|
| 498 |
+
],
|
| 499 |
+
},
|
| 500 |
+
category: 'Creative',
|
| 501 |
+
tags: ['music', 'lyrics', 'songwriting'],
|
| 502 |
+
totalRuns: 5678,
|
| 503 |
+
starsCount: 432,
|
| 504 |
+
},
|
| 505 |
+
|
| 506 |
+
// ============ RESEARCH ============
|
| 507 |
+
{
|
| 508 |
+
slug: 'research-summarizer',
|
| 509 |
+
title: 'Research Paper Summarizer',
|
| 510 |
+
description: 'Summarize academic papers and research into digestible insights.',
|
| 511 |
+
template: `Summarize this research:
|
| 512 |
+
|
| 513 |
+
{{content}}
|
| 514 |
+
|
| 515 |
+
Provide:
|
| 516 |
+
|
| 517 |
+
## 📋 TL;DR
|
| 518 |
+
One paragraph summary
|
| 519 |
+
|
| 520 |
+
## 🎯 Key Findings
|
| 521 |
+
Bullet points of main discoveries
|
| 522 |
+
|
| 523 |
+
## 🔬 Methodology
|
| 524 |
+
How the research was conducted
|
| 525 |
+
|
| 526 |
+
## 📊 Data & Results
|
| 527 |
+
Key statistics and outcomes
|
| 528 |
+
|
| 529 |
+
## 💡 Implications
|
| 530 |
+
What this means for {{field}}
|
| 531 |
+
|
| 532 |
+
## ❓ Limitations
|
| 533 |
+
Study limitations and gaps
|
| 534 |
+
|
| 535 |
+
## 📚 Citation
|
| 536 |
+
Proper academic citation format`,
|
| 537 |
+
schema: {
|
| 538 |
+
variables: [
|
| 539 |
+
{ name: 'content', type: 'textarea', label: 'Paste Research Content', placeholder: 'Paste the abstract, paper, or research text...', required: true },
|
| 540 |
+
{ name: 'field', type: 'text', label: 'Research Field', placeholder: 'e.g., Machine Learning, Medicine', default: 'the field' },
|
| 541 |
+
],
|
| 542 |
+
},
|
| 543 |
+
category: 'Research',
|
| 544 |
+
tags: ['research', 'academic', 'summary', 'papers'],
|
| 545 |
+
totalRuns: 4567,
|
| 546 |
+
starsCount: 345,
|
| 547 |
+
},
|
| 548 |
+
{
|
| 549 |
+
slug: 'fact-checker',
|
| 550 |
+
title: 'AI Fact Checker',
|
| 551 |
+
description: 'Verify claims and get evidence-based analysis.',
|
| 552 |
+
template: `Analyze this claim for accuracy:
|
| 553 |
+
|
| 554 |
+
Claim: "{{claim}}"
|
| 555 |
+
|
| 556 |
+
Provide:
|
| 557 |
+
|
| 558 |
+
## 📊 Verdict
|
| 559 |
+
[TRUE / MOSTLY TRUE / MIXED / MOSTLY FALSE / FALSE]
|
| 560 |
+
|
| 561 |
+
## 🔍 Analysis
|
| 562 |
+
Detailed breakdown of the claim
|
| 563 |
+
|
| 564 |
+
## 📚 Evidence
|
| 565 |
+
Supporting or refuting evidence
|
| 566 |
+
|
| 567 |
+
## 🌐 Sources
|
| 568 |
+
Where to verify (types of sources to check)
|
| 569 |
+
|
| 570 |
+
## ⚠️ Context
|
| 571 |
+
Important context that affects accuracy
|
| 572 |
+
|
| 573 |
+
## 🎯 Bottom Line
|
| 574 |
+
One sentence summary`,
|
| 575 |
+
schema: {
|
| 576 |
+
variables: [
|
| 577 |
+
{ name: 'claim', type: 'textarea', label: 'Claim to Check', placeholder: 'e.g., Drinking 8 glasses of water a day is necessary for health', required: true },
|
| 578 |
+
],
|
| 579 |
+
},
|
| 580 |
+
category: 'Research',
|
| 581 |
+
tags: ['fact-check', 'verification', 'research'],
|
| 582 |
+
totalRuns: 3456,
|
| 583 |
+
starsCount: 234,
|
| 584 |
+
},
|
| 585 |
+
]
|
| 586 |
+
|
| 587 |
+
async function seed() {
|
| 588 |
+
console.log('🌱 Seeding database with awesome prompts...\n')
|
| 589 |
+
|
| 590 |
+
let created = 0
|
| 591 |
+
let skipped = 0
|
| 592 |
+
|
| 593 |
+
for (const prompt of seedPrompts) {
|
| 594 |
+
const existing = await prisma.prompt.findUnique({
|
| 595 |
+
where: { slug: prompt.slug },
|
| 596 |
+
})
|
| 597 |
+
|
| 598 |
+
if (existing) {
|
| 599 |
+
console.log(` ⏭️ Skipping "${prompt.title}" (already exists)`)
|
| 600 |
+
skipped++
|
| 601 |
+
continue
|
| 602 |
+
}
|
| 603 |
+
|
| 604 |
+
await prisma.prompt.create({
|
| 605 |
+
data: {
|
| 606 |
+
slug: prompt.slug,
|
| 607 |
+
title: prompt.title,
|
| 608 |
+
description: prompt.description,
|
| 609 |
+
template: prompt.template,
|
| 610 |
+
schema: prompt.schema,
|
| 611 |
+
category: prompt.category,
|
| 612 |
+
tags: prompt.tags,
|
| 613 |
+
totalRuns: prompt.totalRuns,
|
| 614 |
+
starsCount: prompt.starsCount,
|
| 615 |
+
remixesCount: 0,
|
| 616 |
+
modelDefault: 'gpt-4o-mini',
|
| 617 |
+
modelAllowed: ['gpt-4o-mini', 'gpt-4o', 'claude-3-5-sonnet'],
|
| 618 |
+
visibility: 'public',
|
| 619 |
+
},
|
| 620 |
+
})
|
| 621 |
+
console.log(` ✅ Created "${prompt.title}" [${prompt.category}]`)
|
| 622 |
+
created++
|
| 623 |
+
}
|
| 624 |
+
|
| 625 |
+
console.log(`\n✨ Done! Created ${created} prompts, skipped ${skipped}`)
|
| 626 |
+
console.log('\n📊 Prompts by category:')
|
| 627 |
+
console.log(' • Content: 3')
|
| 628 |
+
console.log(' • Development: 3')
|
| 629 |
+
console.log(' • Marketing: 2')
|
| 630 |
+
console.log(' • Business: 2')
|
| 631 |
+
console.log(' • Education: 2')
|
| 632 |
+
console.log(' • Creative: 2')
|
| 633 |
+
console.log(' • Research: 2')
|
| 634 |
+
}
|
| 635 |
+
|
| 636 |
+
seed()
|
| 637 |
+
.catch((e) => {
|
| 638 |
+
console.error('❌ Seeding failed:', e)
|
| 639 |
+
process.exit(1)
|
| 640 |
+
})
|
| 641 |
+
.finally(async () => {
|
| 642 |
+
await prisma.$disconnect()
|
| 643 |
+
await pool.end()
|
| 644 |
+
})
|
prisma/tool-models.txt
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
// AI Tools System
|
| 3 |
+
model Tool {
|
| 4 |
+
id String @id @default(cuid())
|
| 5 |
+
slug String @unique
|
| 6 |
+
name String
|
| 7 |
+
description String
|
| 8 |
+
category String // "prompting", "marketing", "branding", etc.
|
| 9 |
+
icon String? // Emoji or icon name
|
| 10 |
+
isActive Boolean @default(true)
|
| 11 |
+
isPremium Boolean @default(false)
|
| 12 |
+
|
| 13 |
+
// Tool configuration
|
| 14 |
+
inputSchema Json // { fields: [{name, type, label, placeholder}] }
|
| 15 |
+
systemPrompt String @db.Text // The AI prompt template
|
| 16 |
+
model String @default("gpt-4o-mini") // AI model to use
|
| 17 |
+
|
| 18 |
+
// Stats
|
| 19 |
+
usageCount Int @default(0)
|
| 20 |
+
|
| 21 |
+
executions ToolExecution[]
|
| 22 |
+
|
| 23 |
+
createdAt DateTime @default(now())
|
| 24 |
+
updatedAt DateTime @updatedAt
|
| 25 |
+
|
| 26 |
+
@@index([category])
|
| 27 |
+
@@index([slug])
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
model ToolExecution {
|
| 31 |
+
id String @id @default(cuid())
|
| 32 |
+
toolId String
|
| 33 |
+
tool Tool @relation(fields: [toolId], references: [id], onDelete: Cascade)
|
| 34 |
+
userId String?
|
| 35 |
+
|
| 36 |
+
inputs Json // User's inputs
|
| 37 |
+
output String @db.Text // AI-generated output
|
| 38 |
+
|
| 39 |
+
createdAt DateTime @default(now())
|
| 40 |
+
|
| 41 |
+
@@index([toolId])
|
| 42 |
+
@@index([userId])
|
| 43 |
+
}
|
prisma/update-models.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Pool } from 'pg'
|
| 2 |
+
import { PrismaPg } from '@prisma/adapter-pg'
|
| 3 |
+
import { PrismaClient } from '@prisma/client'
|
| 4 |
+
import 'dotenv/config'
|
| 5 |
+
|
| 6 |
+
const connectionString = process.env.DATABASE_URL
|
| 7 |
+
if (!connectionString) throw new Error('DATABASE_URL not set')
|
| 8 |
+
|
| 9 |
+
const pool = new Pool({ connectionString })
|
| 10 |
+
const adapter = new PrismaPg(pool)
|
| 11 |
+
const prisma = new PrismaClient({ adapter })
|
| 12 |
+
|
| 13 |
+
async function updateToGemini() {
|
| 14 |
+
console.log('🔄 Updating all prompts to use Gemini 2.5 Flash as default...\n')
|
| 15 |
+
|
| 16 |
+
const result = await prisma.prompt.updateMany({
|
| 17 |
+
data: {
|
| 18 |
+
modelDefault: 'gemini-2.5-flash',
|
| 19 |
+
modelAllowed: ['gemini-2.5-flash', 'gemini-2.0-flash', 'gpt-4o-mini', 'gpt-4o', 'claude-3-5-sonnet'],
|
| 20 |
+
},
|
| 21 |
+
})
|
| 22 |
+
|
| 23 |
+
console.log(`✅ Updated ${result.count} prompts to use Gemini 2.5 Flash!`)
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
updateToGemini()
|
| 27 |
+
.catch((e) => {
|
| 28 |
+
console.error('❌ Update failed:', e)
|
| 29 |
+
process.exit(1)
|
| 30 |
+
})
|
| 31 |
+
.finally(async () => {
|
| 32 |
+
await prisma.$disconnect()
|
| 33 |
+
await pool.end()
|
| 34 |
+
})
|
public/favicon.ico
ADDED
|
|
Git LFS Details
|
public/file.svg
ADDED
|
|
public/globe.svg
ADDED
|
|