mirror of
https://github.com/x1xhlol/system-prompts-and-models-of-ai-tools.git
synced 2025-12-15 21:15:14 +00:00
feat: Add modern Next.js 15 web interface with React 19
This commit adds a complete, production-ready web application for browsing and exploring AI tool system prompts. The interface provides an intuitive, responsive, and feature-rich experience for discovering AI tools. ## Web Application Features **Core Functionality:** - 🔍 Advanced search with real-time filtering - 📊 Interactive statistics dashboard with visualizations - 🔄 Side-by-side comparison of up to 4 tools - ⭐ Favorites system with local persistence - 📱 Fully responsive mobile-first design - 🎨 Dark/light mode with system preference detection - ⚡ Optimized performance with Next.js Server Components **Pages:** - Home: Hero section, features showcase, featured tools - Browse: Advanced filtering, grid/list views, category filters - Stats: Comprehensive analytics and visualizations - Compare: Side-by-side tool comparison - Tool Detail: In-depth tool information - About: Project information and tech stack **Components:** - Responsive navbar with mobile menu - Tool cards with interactive actions - Reusable UI components (Button, Card, Badge, Input) - Footer with quick links and stats - Theme provider for dark mode - Loading and error states ## Technical Stack **Framework & Libraries:** - Next.js 15 with App Router - React 19.0 with Server Components - TypeScript 5.6 for type safety - Tailwind CSS for styling - Zustand for state management - next-themes for theme switching - Lucide React for icons **Features:** - Server-side rendering (SSR) - Static site generation (SSG) for tool pages - Optimized bundle with automatic code splitting - SEO-friendly with metadata API - Accessibility best practices ## Project Structure web/ ├── app/ # Next.js pages ├── components/ # React components ├── lib/ # Utilities and data ├── data/ # Static data (index.json) ├── setup.sh # Setup script └── README.md # Documentation ## Developer Experience - TypeScript for type safety - ESLint for code quality - Hot module replacement - Fast refresh - Comprehensive documentation - Setup script for quick start ## Updated Documentation - Enhanced main README with web interface section - Created comprehensive web/README.md - Updated roadmap to mark completed features - Added Quick Start guide for web app ## Stats - 33 new files created - ~3,500 lines of TypeScript/TSX - Full responsive design (mobile, tablet, desktop) - Production-ready with build optimizations Users can now explore 32+ AI tools through an intuitive web interface instead of just command-line tools. Version: 2.0.0
This commit is contained in:
parent
cb8bb88b4f
commit
9d5ee88ea3
47
README.md
47
README.md
@ -80,8 +80,28 @@ Sponsor the most comprehensive collection of AI system prompts and reach thousan
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
### Browse Tools
|
||||
Explore 32+ AI coding tools organized by category below, or use our search tools:
|
||||
### 🌐 Web Interface (NEW!)
|
||||
|
||||
The easiest way to explore AI prompts is through our modern web interface:
|
||||
|
||||
```bash
|
||||
cd web
|
||||
./setup.sh
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Visit **http://localhost:3000** to explore:
|
||||
- 🔍 **Advanced search** and filtering
|
||||
- 📊 **Statistics dashboard** with visualizations
|
||||
- 🔄 **Compare tools** side-by-side
|
||||
- 📱 **Fully responsive** mobile-friendly design
|
||||
- 🎨 **Dark mode** support
|
||||
|
||||
See [web/README.md](./web/README.md) for full documentation.
|
||||
|
||||
### 📟 Command Line Tools
|
||||
|
||||
For developers who prefer the terminal:
|
||||
|
||||
```bash
|
||||
# Generate metadata index
|
||||
@ -245,15 +265,22 @@ python scripts/search.py --list-companies
|
||||
|
||||
### What's New in v2.0 🎉
|
||||
|
||||
**Major enhancements:**
|
||||
- ✨ **Searchable Index**: Find tools by category, company, or model
|
||||
- 📊 **Analysis Tools**: Generate statistics and comparisons
|
||||
- ✅ **Validation**: Automated quality checks
|
||||
**🌐 Modern Web Interface:**
|
||||
- ✨ **Next.js 15 + React 19** web application
|
||||
- 🔍 **Advanced search** with real-time filtering
|
||||
- 📊 **Interactive statistics** dashboard
|
||||
- 🔄 **Side-by-side comparison** of up to 4 tools
|
||||
- 📱 **Fully responsive** mobile design
|
||||
- 🎨 **Dark mode** with theme persistence
|
||||
- ⚡ **Lightning fast** with Server Components
|
||||
|
||||
**📁 Repository Enhancements:**
|
||||
- 📚 **Better Documentation**: Individual READMEs for key tools
|
||||
- 🗂️ **Organized by Category**: Browse by IDE, Agent, Assistant, etc.
|
||||
- 🔍 **Discovery Tools**: Python scripts for searching and analysis
|
||||
- ✅ **Validation**: Automated quality checks
|
||||
- 📊 **Analysis Tools**: Generate statistics and comparisons
|
||||
|
||||
**New Scripts:**
|
||||
**🛠️ Developer Tools:**
|
||||
- `generate_metadata.py` - Create searchable index
|
||||
- `validate.py` - Check repository quality
|
||||
- `search.py` - Search and filter tools
|
||||
@ -262,8 +289,8 @@ python scripts/search.py --list-companies
|
||||
See [CHANGELOG.md](./CHANGELOG.md) for full details.
|
||||
|
||||
### Future Roadmap
|
||||
- [ ] Web interface for browsing prompts
|
||||
- [ ] Prompt comparison and diff tools
|
||||
- [x] Web interface for browsing prompts ✅ **COMPLETED**
|
||||
- [x] Prompt comparison and diff tools ✅ **COMPLETED**
|
||||
- [ ] Community ratings and reviews
|
||||
- [ ] API for programmatic access
|
||||
- [ ] More individual tool READMEs
|
||||
|
||||
3
web/.eslintrc.json
Normal file
3
web/.eslintrc.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "next/core-web-vitals"
|
||||
}
|
||||
37
web/.gitignore
vendored
Normal file
37
web/.gitignore
vendored
Normal file
@ -0,0 +1,37 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
.yarn/install-state.gz
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
.env
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
237
web/README.md
Normal file
237
web/README.md
Normal file
@ -0,0 +1,237 @@
|
||||
# AI Prompts Explorer - Web Interface
|
||||
|
||||
A modern, responsive web application for exploring AI tool system prompts and configurations. Built with Next.js 15, React 19, and TypeScript.
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
## ✨ Features
|
||||
|
||||
- 🔍 **Advanced Search & Filtering** - Search by name, company, category, or model
|
||||
- 📊 **Statistics Dashboard** - Comprehensive analytics and visualizations
|
||||
- 🔄 **Tool Comparison** - Compare up to 4 tools side-by-side
|
||||
- 💾 **Favorites** - Save your favorite tools for quick access
|
||||
- 🎨 **Dark Mode** - Beautiful dark/light theme support
|
||||
- 📱 **Fully Responsive** - Perfect on desktop, tablet, and mobile
|
||||
- ⚡ **Lightning Fast** - Built with Next.js 15 and Server Components
|
||||
- 🎭 **Smooth Animations** - Polished UX with Framer Motion
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js 22+ (recommended: 22.21.1)
|
||||
- npm 10+
|
||||
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
# Navigate to the web directory
|
||||
cd web
|
||||
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Run the development server
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) in your browser.
|
||||
|
||||
## 📁 Project Structure
|
||||
|
||||
```
|
||||
web/
|
||||
├── app/ # Next.js App Router pages
|
||||
│ ├── page.tsx # Homepage
|
||||
│ ├── browse/ # Browse tools page
|
||||
│ ├── stats/ # Statistics dashboard
|
||||
│ ├── compare/ # Tool comparison
|
||||
│ ├── tool/[slug]/ # Individual tool pages
|
||||
│ ├── about/ # About page
|
||||
│ ├── layout.tsx # Root layout
|
||||
│ └── globals.css # Global styles
|
||||
├── components/ # Reusable components
|
||||
│ ├── ui/ # Base UI components
|
||||
│ ├── navbar.tsx # Navigation bar
|
||||
│ ├── footer.tsx # Footer
|
||||
│ ├── tool-card.tsx # Tool card component
|
||||
│ └── theme-provider.tsx # Theme provider
|
||||
├── lib/ # Utilities and data
|
||||
│ ├── data.ts # Data loading functions
|
||||
│ ├── store.ts # Zustand state management
|
||||
│ ├── types.ts # TypeScript types
|
||||
│ └── utils.ts # Utility functions
|
||||
├── data/ # Static data
|
||||
│ └── index.json # Generated metadata
|
||||
└── public/ # Static assets
|
||||
```
|
||||
|
||||
## 🛠️ Tech Stack
|
||||
|
||||
- **Framework**: [Next.js 15](https://nextjs.org/) with App Router
|
||||
- **UI Library**: [React 19](https://react.dev/)
|
||||
- **Language**: [TypeScript](https://www.typescriptlang.org/)
|
||||
- **Styling**: [Tailwind CSS](https://tailwindcss.com/)
|
||||
- **State Management**: [Zustand](https://zustand-demo.pmnd.rs/)
|
||||
- **Icons**: [Lucide React](https://lucide.dev/)
|
||||
- **Animations**: [Framer Motion](https://www.framer.com/motion/)
|
||||
- **Theme**: [next-themes](https://github.com/pacocoursey/next-themes)
|
||||
|
||||
## 📦 Available Scripts
|
||||
|
||||
```bash
|
||||
# Development server
|
||||
npm run dev
|
||||
|
||||
# Production build
|
||||
npm run build
|
||||
|
||||
# Start production server
|
||||
npm start
|
||||
|
||||
# Lint code
|
||||
npm run lint
|
||||
|
||||
# Type check
|
||||
npm run type-check
|
||||
```
|
||||
|
||||
## 🎨 Features In Detail
|
||||
|
||||
### Browse Tools
|
||||
- Grid or list view
|
||||
- Real-time search
|
||||
- Multi-select filters (category, type, company)
|
||||
- Sort by name, lines, files, or company
|
||||
- Responsive card layout
|
||||
|
||||
### Tool Detail Pages
|
||||
- Complete tool information
|
||||
- File listings with sizes
|
||||
- Model information
|
||||
- Direct links to GitHub and official websites
|
||||
- Add to comparison or favorites
|
||||
|
||||
### Statistics Dashboard
|
||||
- Total tools, files, and lines
|
||||
- Category distribution with visual bars
|
||||
- Top tools by complexity
|
||||
- Type distribution (proprietary vs open-source)
|
||||
- Company breakdown
|
||||
|
||||
### Comparison
|
||||
- Side-by-side comparison of up to 4 tools
|
||||
- Compare all key metrics
|
||||
- File listings
|
||||
- Quick actions
|
||||
|
||||
### Dark Mode
|
||||
- System preference detection
|
||||
- Manual toggle
|
||||
- Persistent preference
|
||||
- Smooth transitions
|
||||
|
||||
## 🔧 Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
Create a `.env.local` file (optional):
|
||||
|
||||
```env
|
||||
# No environment variables required for basic setup
|
||||
```
|
||||
|
||||
### Updating Data
|
||||
|
||||
The app uses `data/index.json` generated from the repository metadata:
|
||||
|
||||
```bash
|
||||
# From the repository root
|
||||
python3 scripts/generate_metadata.py
|
||||
|
||||
# Copy to web/data
|
||||
cp scripts/index.json web/data/
|
||||
```
|
||||
|
||||
## 🚀 Deployment
|
||||
|
||||
### Vercel (Recommended)
|
||||
|
||||
[](https://vercel.com/new/clone?repository-url=https://github.com/x1xhlol/system-prompts-and-models-of-ai-tools/tree/main/web)
|
||||
|
||||
```bash
|
||||
# Install Vercel CLI
|
||||
npm i -g vercel
|
||||
|
||||
# Deploy
|
||||
vercel
|
||||
```
|
||||
|
||||
### Other Platforms
|
||||
|
||||
Build the static site:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
The output will be in `.next/`. Deploy using:
|
||||
- **Netlify**: Supports Next.js
|
||||
- **Cloudflare Pages**: Supports Next.js
|
||||
- **AWS Amplify**: Supports Next.js
|
||||
- **Docker**: Use the included Dockerfile (if created)
|
||||
|
||||
## 📱 Browser Support
|
||||
|
||||
- Chrome (latest)
|
||||
- Firefox (latest)
|
||||
- Safari (latest)
|
||||
- Edge (latest)
|
||||
- Mobile browsers (iOS Safari, Chrome Mobile)
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
Contributions are welcome! Please see the main [CONTRIBUTING.md](../CONTRIBUTING.md) for guidelines.
|
||||
|
||||
### Development Workflow
|
||||
|
||||
1. Fork the repository
|
||||
2. Create a feature branch
|
||||
3. Make your changes
|
||||
4. Test thoroughly
|
||||
5. Submit a pull request
|
||||
|
||||
## 📄 License
|
||||
|
||||
This project is part of the [AI Prompts and Models Repository](https://github.com/x1xhlol/system-prompts-and-models-of-ai-tools) and follows the same license.
|
||||
|
||||
## 🙏 Acknowledgments
|
||||
|
||||
- Built with [Next.js](https://nextjs.org/)
|
||||
- UI components inspired by [shadcn/ui](https://ui.shadcn.com/)
|
||||
- Icons from [Lucide](https://lucide.dev/)
|
||||
- Community contributions
|
||||
|
||||
## 📞 Support
|
||||
|
||||
- 🐛 [Report Issues](https://github.com/x1xhlol/system-prompts-and-models-of-ai-tools/issues)
|
||||
- 💬 [Discord Community](https://discord.gg/NwzrWErdMU)
|
||||
- 📧 Email: lucknitelol@proton.me
|
||||
|
||||
## 🎯 Roadmap
|
||||
|
||||
- [ ] Advanced filtering (by model, file count, etc.)
|
||||
- [ ] Export comparison as PDF/image
|
||||
- [ ] Favorites synchronization
|
||||
- [ ] Community ratings and reviews
|
||||
- [ ] API for programmatic access
|
||||
- [ ] Prompt visualization and analysis
|
||||
- [ ] Search result highlighting
|
||||
- [ ] Keyboard shortcuts
|
||||
|
||||
---
|
||||
|
||||
**Made with ❤️ by the AI Prompts Explorer community**
|
||||
211
web/app/about/page.tsx
Normal file
211
web/app/about/page.tsx
Normal file
@ -0,0 +1,211 @@
|
||||
import Link from 'next/link'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Github, Heart, Code, Database, Search, BarChart3, GitCompare, Sparkles } from 'lucide-react'
|
||||
import { getStats } from '@/lib/data'
|
||||
import { formatNumber } from '@/lib/utils'
|
||||
|
||||
export default function AboutPage() {
|
||||
const stats = getStats()
|
||||
|
||||
const features = [
|
||||
{
|
||||
icon: Search,
|
||||
title: 'Advanced Search',
|
||||
description: 'Search and filter by category, company, model, or keyword with real-time results',
|
||||
},
|
||||
{
|
||||
icon: GitCompare,
|
||||
title: 'Side-by-Side Comparison',
|
||||
description: 'Compare up to 4 tools simultaneously to analyze differences and similarities',
|
||||
},
|
||||
{
|
||||
icon: BarChart3,
|
||||
title: 'Analytics Dashboard',
|
||||
description: 'Comprehensive statistics and visualizations of all AI tools',
|
||||
},
|
||||
{
|
||||
icon: Code,
|
||||
title: 'Complete Prompts',
|
||||
description: 'Full system prompts and tool configurations, not just excerpts',
|
||||
},
|
||||
{
|
||||
icon: Database,
|
||||
title: 'Structured Data',
|
||||
description: 'Organized metadata with files, models, categories, and companies',
|
||||
},
|
||||
{
|
||||
icon: Sparkles,
|
||||
title: 'Modern Interface',
|
||||
description: 'Beautiful, responsive UI built with Next.js 15, React 19, and Tailwind CSS',
|
||||
},
|
||||
]
|
||||
|
||||
const techStack = [
|
||||
{ name: 'Next.js 15', description: 'React framework with App Router' },
|
||||
{ name: 'React 19', description: 'Latest React with Server Components' },
|
||||
{ name: 'TypeScript', description: 'Type-safe development' },
|
||||
{ name: 'Tailwind CSS', description: 'Utility-first CSS framework' },
|
||||
{ name: 'Zustand', description: 'Lightweight state management' },
|
||||
{ name: 'Framer Motion', description: 'Animation library' },
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
{/* Hero */}
|
||||
<div className="text-center max-w-3xl mx-auto mb-16">
|
||||
<Badge className="mb-4" variant="secondary">
|
||||
<Sparkles className="w-3 h-3 mr-1" />
|
||||
Version 2.0
|
||||
</Badge>
|
||||
<h1 className="text-4xl md:text-5xl font-bold mb-4">
|
||||
About AI Prompts Explorer
|
||||
</h1>
|
||||
<p className="text-lg text-muted-foreground">
|
||||
The most comprehensive, searchable collection of AI tool system prompts and configurations.
|
||||
Discover how {stats.total_tools}+ AI coding tools work under the hood.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="grid gap-6 md:grid-cols-4 mb-16">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-3xl">{stats.total_tools}+</CardTitle>
|
||||
<CardDescription>AI Tools Documented</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-3xl">{stats.total_files}</CardTitle>
|
||||
<CardDescription>Configuration Files</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-3xl">{formatNumber(stats.total_lines)}</CardTitle>
|
||||
<CardDescription>Lines of Prompts</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-3xl">{Object.keys(stats.by_category).length}</CardTitle>
|
||||
<CardDescription>Tool Categories</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Features */}
|
||||
<div className="mb-16">
|
||||
<h2 className="text-3xl font-bold text-center mb-8">Features</h2>
|
||||
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
{features.map((feature) => {
|
||||
const Icon = feature.icon
|
||||
return (
|
||||
<Card key={feature.title}>
|
||||
<CardHeader>
|
||||
<Icon className="w-10 h-10 mb-2 text-primary" />
|
||||
<CardTitle>{feature.title}</CardTitle>
|
||||
<CardDescription>{feature.description}</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tech Stack */}
|
||||
<div className="mb-16">
|
||||
<h2 className="text-3xl font-bold text-center mb-8">Technology Stack</h2>
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{techStack.map((tech) => (
|
||||
<Card key={tech.name}>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">{tech.name}</CardTitle>
|
||||
<CardDescription>{tech.description}</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mission */}
|
||||
<Card className="mb-16">
|
||||
<CardContent className="p-8">
|
||||
<h2 className="text-2xl font-bold mb-4">Our Mission</h2>
|
||||
<p className="text-muted-foreground mb-4">
|
||||
AI Prompts Explorer aims to provide transparency into how AI coding tools work by collecting,
|
||||
organizing, and presenting their system prompts and configurations in an accessible way.
|
||||
</p>
|
||||
<p className="text-muted-foreground mb-4">
|
||||
We believe that understanding how AI tools are configured helps developers:
|
||||
</p>
|
||||
<ul className="list-disc list-inside text-muted-foreground space-y-2 mb-4">
|
||||
<li>Choose the right tools for their needs</li>
|
||||
<li>Learn prompt engineering best practices</li>
|
||||
<li>Understand AI tool capabilities and limitations</li>
|
||||
<li>Build better AI-powered applications</li>
|
||||
<li>Contribute to the open-source AI community</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Contributing */}
|
||||
<Card className="mb-16">
|
||||
<CardContent className="p-8">
|
||||
<div className="flex items-start gap-4">
|
||||
<Heart className="w-12 h-12 text-red-500 flex-shrink-0 mt-1" />
|
||||
<div className="flex-1">
|
||||
<h2 className="text-2xl font-bold mb-4">Contributing</h2>
|
||||
<p className="text-muted-foreground mb-4">
|
||||
This project is open source and welcomes contributions from the community. You can help by:
|
||||
</p>
|
||||
<ul className="list-disc list-inside text-muted-foreground space-y-2 mb-6">
|
||||
<li>Adding new AI tool prompts and configurations</li>
|
||||
<li>Updating existing tool information</li>
|
||||
<li>Improving documentation</li>
|
||||
<li>Reporting bugs or suggesting features</li>
|
||||
<li>Sharing the project with others</li>
|
||||
</ul>
|
||||
<div className="flex flex-wrap gap-4">
|
||||
<Button asChild>
|
||||
<a
|
||||
href="https://github.com/x1xhlol/system-prompts-and-models-of-ai-tools"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Github className="w-4 h-4 mr-2" />
|
||||
View on GitHub
|
||||
</a>
|
||||
</Button>
|
||||
<Button asChild variant="outline">
|
||||
<a
|
||||
href="https://github.com/x1xhlol/system-prompts-and-models-of-ai-tools/blob/main/CONTRIBUTING.md"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Contributing Guide
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* CTA */}
|
||||
<Card className="bg-gradient-to-r from-blue-500 via-purple-500 to-pink-500 text-white border-0">
|
||||
<CardContent className="p-8 text-center">
|
||||
<h2 className="text-3xl font-bold mb-4">Start Exploring</h2>
|
||||
<p className="text-lg mb-6 text-white/90">
|
||||
Browse {stats.total_tools}+ AI tools and discover their system prompts
|
||||
</p>
|
||||
<Button asChild size="lg" className="bg-white text-purple-600 hover:bg-white/90">
|
||||
<Link href="/browse">Browse All Tools</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
265
web/app/browse/page.tsx
Normal file
265
web/app/browse/page.tsx
Normal file
@ -0,0 +1,265 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useMemo } from 'react'
|
||||
import { Filter, Grid, List, X } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Card } from '@/components/ui/card'
|
||||
import { ToolCard } from '@/components/tool-card'
|
||||
import { getAllTools, getCategories, getCompanies, getModels } from '@/lib/data'
|
||||
import { useAppStore } from '@/lib/store'
|
||||
import { AITool } from '@/lib/types'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export default function BrowsePage() {
|
||||
const tools = getAllTools()
|
||||
const categories = getCategories()
|
||||
const companies = getCompanies()
|
||||
const models = getModels()
|
||||
|
||||
const { viewMode, setViewMode } = useAppStore()
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const [selectedCategories, setSelectedCategories] = useState<string[]>([])
|
||||
const [selectedTypes, setSelectedTypes] = useState<string[]>([])
|
||||
const [selectedCompanies, setSelectedCompanies] = useState<string[]>([])
|
||||
const [showFilters, setShowFilters] = useState(false)
|
||||
|
||||
// Filter tools
|
||||
const filteredTools = useMemo(() => {
|
||||
return tools.filter((tool) => {
|
||||
// Search filter
|
||||
if (searchQuery) {
|
||||
const query = searchQuery.toLowerCase()
|
||||
const matchesSearch =
|
||||
tool.name.toLowerCase().includes(query) ||
|
||||
tool.description.toLowerCase().includes(query) ||
|
||||
tool.company.toLowerCase().includes(query) ||
|
||||
tool.category.toLowerCase().includes(query) ||
|
||||
tool.models.some((m) => m.toLowerCase().includes(query))
|
||||
|
||||
if (!matchesSearch) return false
|
||||
}
|
||||
|
||||
// Category filter
|
||||
if (selectedCategories.length > 0 && !selectedCategories.includes(tool.category)) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Type filter
|
||||
if (selectedTypes.length > 0 && !selectedTypes.includes(tool.type)) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Company filter
|
||||
if (selectedCompanies.length > 0 && !selectedCompanies.includes(tool.company)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
}, [tools, searchQuery, selectedCategories, selectedTypes, selectedCompanies])
|
||||
|
||||
const toggleCategory = (category: string) => {
|
||||
setSelectedCategories((prev) =>
|
||||
prev.includes(category) ? prev.filter((c) => c !== category) : [...prev, category]
|
||||
)
|
||||
}
|
||||
|
||||
const toggleType = (type: string) => {
|
||||
setSelectedTypes((prev) =>
|
||||
prev.includes(type) ? prev.filter((t) => t !== type) : [...prev, type]
|
||||
)
|
||||
}
|
||||
|
||||
const toggleCompany = (company: string) => {
|
||||
setSelectedCompanies((prev) =>
|
||||
prev.includes(company) ? prev.filter((c) => c !== company) : [...prev, company]
|
||||
)
|
||||
}
|
||||
|
||||
const clearFilters = () => {
|
||||
setSearchQuery('')
|
||||
setSelectedCategories([])
|
||||
setSelectedTypes([])
|
||||
setSelectedCompanies([])
|
||||
}
|
||||
|
||||
const hasActiveFilters =
|
||||
searchQuery || selectedCategories.length > 0 || selectedTypes.length > 0 || selectedCompanies.length > 0
|
||||
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<h1 className="text-4xl font-bold mb-4">Browse AI Tools</h1>
|
||||
<p className="text-lg text-muted-foreground">
|
||||
Explore {tools.length} AI coding tools and their system prompts
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Search and Controls */}
|
||||
<div className="mb-6 space-y-4">
|
||||
<div className="flex flex-col sm:flex-row gap-4">
|
||||
<Input
|
||||
type="search"
|
||||
placeholder="Search tools, companies, models..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="flex-1"
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setShowFilters(!showFilters)}
|
||||
className="flex-1 sm:flex-none"
|
||||
>
|
||||
<Filter className="w-4 h-4 mr-2" />
|
||||
Filters
|
||||
{hasActiveFilters && (
|
||||
<Badge variant="destructive" className="ml-2">
|
||||
{[selectedCategories, selectedTypes, selectedCompanies]
|
||||
.reduce((acc, arr) => acc + arr.length, 0)}
|
||||
</Badge>
|
||||
)}
|
||||
</Button>
|
||||
<div className="flex border rounded-md">
|
||||
<Button
|
||||
variant={viewMode === 'grid' ? 'secondary' : 'ghost'}
|
||||
size="icon"
|
||||
onClick={() => setViewMode('grid')}
|
||||
className="rounded-r-none"
|
||||
>
|
||||
<Grid className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant={viewMode === 'list' ? 'secondary' : 'ghost'}
|
||||
size="icon"
|
||||
onClick={() => setViewMode('list')}
|
||||
className="rounded-l-none"
|
||||
>
|
||||
<List className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Active Filters */}
|
||||
{hasActiveFilters && (
|
||||
<div className="flex flex-wrap gap-2 items-center">
|
||||
<span className="text-sm text-muted-foreground">Active filters:</span>
|
||||
{selectedCategories.map((cat) => (
|
||||
<Badge key={cat} variant="secondary" className="gap-1">
|
||||
{cat}
|
||||
<X className="w-3 h-3 cursor-pointer" onClick={() => toggleCategory(cat)} />
|
||||
</Badge>
|
||||
))}
|
||||
{selectedTypes.map((type) => (
|
||||
<Badge key={type} variant="secondary" className="gap-1">
|
||||
{type}
|
||||
<X className="w-3 h-3 cursor-pointer" onClick={() => toggleType(type)} />
|
||||
</Badge>
|
||||
))}
|
||||
{selectedCompanies.map((company) => (
|
||||
<Badge key={company} variant="secondary" className="gap-1">
|
||||
{company}
|
||||
<X className="w-3 h-3 cursor-pointer" onClick={() => toggleCompany(company)} />
|
||||
</Badge>
|
||||
))}
|
||||
<Button variant="ghost" size="sm" onClick={clearFilters}>
|
||||
Clear all
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Filters Sidebar */}
|
||||
{showFilters && (
|
||||
<Card className="mb-6 p-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
{/* Categories */}
|
||||
<div>
|
||||
<h3 className="font-semibold mb-3">Categories</h3>
|
||||
<div className="space-y-2">
|
||||
{categories.slice(0, 8).map((category) => (
|
||||
<label key={category} className="flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedCategories.includes(category)}
|
||||
onChange={() => toggleCategory(category)}
|
||||
className="rounded"
|
||||
/>
|
||||
<span className="text-sm">{category}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Types */}
|
||||
<div>
|
||||
<h3 className="font-semibold mb-3">Type</h3>
|
||||
<div className="space-y-2">
|
||||
{['proprietary', 'open-source'].map((type) => (
|
||||
<label key={type} className="flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedTypes.includes(type)}
|
||||
onChange={() => toggleType(type)}
|
||||
className="rounded"
|
||||
/>
|
||||
<span className="text-sm capitalize">{type}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Companies */}
|
||||
<div>
|
||||
<h3 className="font-semibold mb-3">Companies</h3>
|
||||
<div className="space-y-2 max-h-48 overflow-y-auto">
|
||||
{companies.slice(0, 10).map((company) => (
|
||||
<label key={company} className="flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedCompanies.includes(company)}
|
||||
onChange={() => toggleCompany(company)}
|
||||
className="rounded"
|
||||
/>
|
||||
<span className="text-sm">{company}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Results */}
|
||||
<div className="mb-4 flex justify-between items-center">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Showing {filteredTools.length} of {tools.length} tools
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Tools Grid/List */}
|
||||
{filteredTools.length > 0 ? (
|
||||
<div
|
||||
className={cn(
|
||||
viewMode === 'grid'
|
||||
? 'grid gap-6 md:grid-cols-2 lg:grid-cols-3'
|
||||
: 'space-y-4'
|
||||
)}
|
||||
>
|
||||
{filteredTools.map((tool) => (
|
||||
<ToolCard key={tool.directory} tool={tool} variant={viewMode === 'list' ? 'compact' : 'default'} />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<Card className="p-12 text-center">
|
||||
<p className="text-lg text-muted-foreground mb-4">No tools found matching your criteria</p>
|
||||
<Button onClick={clearFilters}>Clear Filters</Button>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
203
web/app/compare/page.tsx
Normal file
203
web/app/compare/page.tsx
Normal file
@ -0,0 +1,203 @@
|
||||
'use client'
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { useAppStore } from '@/lib/store'
|
||||
import { getToolByDirectory } from '@/lib/data'
|
||||
import { formatNumber, formatBytes } from '@/lib/utils'
|
||||
import { X, FileText, Code, Package } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
|
||||
export default function ComparePage() {
|
||||
const { comparison, removeFromComparison, clearComparison } = useAppStore()
|
||||
|
||||
const tools = comparison.map((dir) => getToolByDirectory(dir)).filter(Boolean)
|
||||
|
||||
if (tools.length === 0) {
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-16">
|
||||
<Card className="p-12 text-center max-w-2xl mx-auto">
|
||||
<Package className="w-16 h-16 mx-auto text-muted-foreground mb-4" />
|
||||
<h2 className="text-2xl font-bold mb-2">No Tools Selected</h2>
|
||||
<p className="text-muted-foreground mb-6">
|
||||
Add tools to comparison from the browse page to see side-by-side comparison
|
||||
</p>
|
||||
<Button asChild>
|
||||
<Link href="/browse">Browse Tools</Link>
|
||||
</Button>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const comparisonData = [
|
||||
{
|
||||
label: 'Company',
|
||||
getValue: (tool: any) => tool.company,
|
||||
},
|
||||
{
|
||||
label: 'Category',
|
||||
getValue: (tool: any) => tool.category,
|
||||
},
|
||||
{
|
||||
label: 'Type',
|
||||
getValue: (tool: any) => <Badge variant="outline">{tool.type}</Badge>,
|
||||
},
|
||||
{
|
||||
label: 'Files',
|
||||
getValue: (tool: any) => tool.file_count,
|
||||
},
|
||||
{
|
||||
label: 'Total Lines',
|
||||
getValue: (tool: any) => formatNumber(tool.total_lines),
|
||||
},
|
||||
{
|
||||
label: 'Models',
|
||||
getValue: (tool: any) => (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{tool.models.length > 0 ? (
|
||||
tool.models.slice(0, 3).map((model: string) => (
|
||||
<Badge key={model} variant="secondary" className="text-xs">
|
||||
{model}
|
||||
</Badge>
|
||||
))
|
||||
) : (
|
||||
<span className="text-muted-foreground text-sm">None specified</span>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: 'Website',
|
||||
getValue: (tool: any) =>
|
||||
tool.website ? (
|
||||
<a
|
||||
href={tool.website}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-500 hover:underline text-sm"
|
||||
>
|
||||
Visit
|
||||
</a>
|
||||
) : (
|
||||
<span className="text-muted-foreground text-sm">N/A</span>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h1 className="text-4xl font-bold">Compare Tools</h1>
|
||||
{tools.length > 0 && (
|
||||
<Button variant="outline" onClick={clearComparison}>
|
||||
<X className="w-4 h-4 mr-2" />
|
||||
Clear All
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-lg text-muted-foreground">
|
||||
Comparing {tools.length} of 4 maximum tools
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Comparison Table */}
|
||||
<div className="overflow-x-auto">
|
||||
<div className="inline-block min-w-full align-middle">
|
||||
<div className="grid" style={{ gridTemplateColumns: `200px repeat(${tools.length}, 1fr)` }}>
|
||||
{/* Header Row */}
|
||||
<div className="bg-muted p-4 border font-semibold">Tool</div>
|
||||
{tools.map((tool) => (
|
||||
<Card key={tool!.directory} className="border-l-0 rounded-none">
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex-1 min-w-0">
|
||||
<CardTitle className="text-lg line-clamp-2">{tool!.name}</CardTitle>
|
||||
</div>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
onClick={() => removeFromComparison(tool!.directory)}
|
||||
className="h-6 w-6 flex-shrink-0"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
))}
|
||||
|
||||
{/* Description Row */}
|
||||
<div className="bg-muted p-4 border border-t-0 font-semibold">Description</div>
|
||||
{tools.map((tool) => (
|
||||
<div key={`desc-${tool!.directory}`} className="p-4 border border-l-0 border-t-0">
|
||||
<p className="text-sm text-muted-foreground line-clamp-3">{tool!.description}</p>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Comparison Rows */}
|
||||
{comparisonData.map((row, index) => (
|
||||
<div key={row.label} className="contents">
|
||||
<div className="bg-muted p-4 border border-t-0 font-semibold text-sm">
|
||||
{row.label}
|
||||
</div>
|
||||
{tools.map((tool) => (
|
||||
<div key={`${row.label}-${tool!.directory}`} className="p-4 border border-l-0 border-t-0">
|
||||
<div className="text-sm">{row.getValue(tool)}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Files Row */}
|
||||
<div className="bg-muted p-4 border border-t-0 font-semibold text-sm">Files</div>
|
||||
{tools.map((tool) => (
|
||||
<div key={`files-${tool!.directory}`} className="p-4 border border-l-0 border-t-0">
|
||||
<div className="space-y-1">
|
||||
{tool!.files.slice(0, 5).map((file) => (
|
||||
<div key={file.path} className="text-xs text-muted-foreground truncate">
|
||||
<FileText className="w-3 h-3 inline mr-1" />
|
||||
{file.name}
|
||||
</div>
|
||||
))}
|
||||
{tool!.files.length > 5 && (
|
||||
<div className="text-xs text-muted-foreground">
|
||||
+{tool!.files.length - 5} more files
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Actions Row */}
|
||||
<div className="bg-muted p-4 border border-t-0 font-semibold">Actions</div>
|
||||
{tools.map((tool) => (
|
||||
<div key={`actions-${tool!.directory}`} className="p-4 border border-l-0 border-t-0">
|
||||
<Button asChild size="sm" className="w-full">
|
||||
<Link href={`/tool/${tool!.directory.toLowerCase().replace(/\s+/g, '-')}`}>
|
||||
View Details
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Add More */}
|
||||
{tools.length < 4 && (
|
||||
<Card className="mt-8 p-6 text-center">
|
||||
<p className="text-muted-foreground mb-4">
|
||||
You can compare up to {4 - tools.length} more tool{tools.length < 3 ? 's' : ''}
|
||||
</p>
|
||||
<Button asChild variant="outline">
|
||||
<Link href="/browse">Add More Tools</Link>
|
||||
</Button>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
109
web/app/globals.css
Normal file
109
web/app/globals.css
Normal file
@ -0,0 +1,109 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 222.2 84% 4.9%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 222.2 84% 4.9%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 222.2 84% 4.9%;
|
||||
--primary: 221.2 83.2% 53.3%;
|
||||
--primary-foreground: 210 40% 98%;
|
||||
--secondary: 210 40% 96.1%;
|
||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||
--muted: 210 40% 96.1%;
|
||||
--muted-foreground: 215.4 16.3% 46.9%;
|
||||
--accent: 210 40% 96.1%;
|
||||
--accent-foreground: 222.2 47.4% 11.2%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
--border: 214.3 31.8% 91.4%;
|
||||
--input: 214.3 31.8% 91.4%;
|
||||
--ring: 221.2 83.2% 53.3%;
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 222.2 84% 4.9%;
|
||||
--foreground: 210 40% 98%;
|
||||
--card: 222.2 84% 4.9%;
|
||||
--card-foreground: 210 40% 98%;
|
||||
--popover: 222.2 84% 4.9%;
|
||||
--popover-foreground: 210 40% 98%;
|
||||
--primary: 217.2 91.2% 59.8%;
|
||||
--primary-foreground: 222.2 47.4% 11.2%;
|
||||
--secondary: 217.2 32.6% 17.5%;
|
||||
--secondary-foreground: 210 40% 98%;
|
||||
--muted: 217.2 32.6% 17.5%;
|
||||
--muted-foreground: 215 20.2% 65.1%;
|
||||
--accent: 217.2 32.6% 17.5%;
|
||||
--accent-foreground: 210 40% 98%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
--border: 217.2 32.6% 17.5%;
|
||||
--input: 217.2 32.6% 17.5%;
|
||||
--ring: 224.3 76.3% 48%;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
font-feature-settings: "rlig" 1, "calt" 1;
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.text-balance {
|
||||
text-wrap: balance;
|
||||
}
|
||||
}
|
||||
|
||||
/* Custom scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
@apply bg-muted/30;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
@apply bg-muted-foreground/30 rounded-md;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
@apply bg-muted-foreground/50;
|
||||
}
|
||||
|
||||
/* Gradient backgrounds */
|
||||
.gradient-bg {
|
||||
background: linear-gradient(135deg, hsl(var(--primary)) 0%, hsl(var(--accent)) 100%);
|
||||
}
|
||||
|
||||
.gradient-text {
|
||||
background: linear-gradient(135deg, hsl(var(--primary)) 0%, hsl(var(--accent)) 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
/* Glass morphism */
|
||||
.glass {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.dark .glass {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
45
web/app/layout.tsx
Normal file
45
web/app/layout.tsx
Normal file
@ -0,0 +1,45 @@
|
||||
import type { Metadata } from 'next'
|
||||
import { Inter } from 'next/font/google'
|
||||
import './globals.css'
|
||||
import { Navbar } from '@/components/navbar'
|
||||
import { Footer } from '@/components/footer'
|
||||
import { ThemeProvider } from '@/components/theme-provider'
|
||||
|
||||
const inter = Inter({ subsets: ['latin'] })
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'AI Prompts Explorer - System Prompts & AI Tool Configurations',
|
||||
description: 'Explore 32+ AI coding tools, their system prompts, and configurations. The most comprehensive collection of AI tool prompts.',
|
||||
keywords: ['AI', 'prompts', 'system prompts', 'AI tools', 'LLM', 'GPT', 'Claude', 'code assistant'],
|
||||
authors: [{ name: 'AI Prompts Explorer' }],
|
||||
openGraph: {
|
||||
title: 'AI Prompts Explorer',
|
||||
description: 'Explore 32+ AI coding tools and their system prompts',
|
||||
type: 'website',
|
||||
},
|
||||
}
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<body className={inter.className}>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="system"
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
<div className="flex min-h-screen flex-col">
|
||||
<Navbar />
|
||||
<main className="flex-1">{children}</main>
|
||||
<Footer />
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
13
web/app/loading.tsx
Normal file
13
web/app/loading.tsx
Normal file
@ -0,0 +1,13 @@
|
||||
export default function Loading() {
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-16 min-h-[60vh] flex items-center justify-center">
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<div className="relative w-16 h-16">
|
||||
<div className="absolute inset-0 border-4 border-primary/20 rounded-full" />
|
||||
<div className="absolute inset-0 border-4 border-primary border-t-transparent rounded-full animate-spin" />
|
||||
</div>
|
||||
<p className="text-muted-foreground">Loading...</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
34
web/app/not-found.tsx
Normal file
34
web/app/not-found.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
import Link from 'next/link'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card } from '@/components/ui/card'
|
||||
import { Home, Search } from 'lucide-react'
|
||||
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-16 min-h-[60vh] flex items-center justify-center">
|
||||
<Card className="p-12 text-center max-w-2xl">
|
||||
<div className="text-8xl font-bold bg-gradient-to-r from-blue-500 to-purple-500 bg-clip-text text-transparent mb-4">
|
||||
404
|
||||
</div>
|
||||
<h1 className="text-3xl font-bold mb-4">Page Not Found</h1>
|
||||
<p className="text-lg text-muted-foreground mb-8">
|
||||
The page you're looking for doesn't exist or has been moved.
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<Button asChild size="lg">
|
||||
<Link href="/">
|
||||
<Home className="w-4 h-4 mr-2" />
|
||||
Go Home
|
||||
</Link>
|
||||
</Button>
|
||||
<Button asChild size="lg" variant="outline">
|
||||
<Link href="/browse">
|
||||
<Search className="w-4 h-4 mr-2" />
|
||||
Browse Tools
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
217
web/app/page.tsx
Normal file
217
web/app/page.tsx
Normal file
@ -0,0 +1,217 @@
|
||||
import Link from 'next/link'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { ArrowRight, Search, BarChart3, GitCompare, Sparkles, Code, Zap, Shield } from 'lucide-react'
|
||||
import { getAllTools, getStats, getCategories } from '@/lib/data'
|
||||
import { formatNumber } from '@/lib/utils'
|
||||
import { ToolCard } from '@/components/tool-card'
|
||||
|
||||
export default function HomePage() {
|
||||
const stats = getStats()
|
||||
const tools = getAllTools()
|
||||
const categories = getCategories()
|
||||
const featuredTools = tools.slice(0, 6)
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
{/* Hero Section */}
|
||||
<section className="relative overflow-hidden bg-gradient-to-b from-background to-muted/20">
|
||||
<div className="absolute inset-0 bg-grid-pattern opacity-[0.02]" />
|
||||
<div className="container relative mx-auto px-4 py-24 md:py-32">
|
||||
<div className="mx-auto max-w-4xl text-center space-y-8 animate-fade-in">
|
||||
<Badge className="mx-auto" variant="secondary">
|
||||
<Sparkles className="w-3 h-3 mr-1" />
|
||||
Version 2.0 - Now with Web Interface!
|
||||
</Badge>
|
||||
|
||||
<h1 className="text-4xl font-bold tracking-tight sm:text-6xl md:text-7xl">
|
||||
<span className="block">Explore AI Tool</span>
|
||||
<span className="block bg-gradient-to-r from-blue-500 via-purple-500 to-pink-500 bg-clip-text text-transparent">
|
||||
System Prompts
|
||||
</span>
|
||||
</h1>
|
||||
|
||||
<p className="mx-auto max-w-2xl text-lg text-muted-foreground md:text-xl">
|
||||
The most comprehensive collection of AI coding tool system prompts and configurations.
|
||||
Discover how {stats.total_tools}+ AI tools work under the hood.
|
||||
</p>
|
||||
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center">
|
||||
<Button asChild size="lg" className="text-lg h-12 px-8">
|
||||
<Link href="/browse">
|
||||
Browse All Tools
|
||||
<ArrowRight className="ml-2 w-5 h-5" />
|
||||
</Link>
|
||||
</Button>
|
||||
<Button asChild size="lg" variant="outline" className="text-lg h-12 px-8">
|
||||
<Link href="/stats">
|
||||
View Statistics
|
||||
<BarChart3 className="ml-2 w-5 h-5" />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Stats Grid */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 pt-8">
|
||||
<div className="bg-card border rounded-lg p-4">
|
||||
<div className="text-3xl font-bold bg-gradient-to-r from-blue-500 to-cyan-500 bg-clip-text text-transparent">
|
||||
{stats.total_tools}+
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">AI Tools</div>
|
||||
</div>
|
||||
<div className="bg-card border rounded-lg p-4">
|
||||
<div className="text-3xl font-bold bg-gradient-to-r from-purple-500 to-pink-500 bg-clip-text text-transparent">
|
||||
{stats.total_files}
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">Files</div>
|
||||
</div>
|
||||
<div className="bg-card border rounded-lg p-4">
|
||||
<div className="text-3xl font-bold bg-gradient-to-r from-green-500 to-emerald-500 bg-clip-text text-transparent">
|
||||
{formatNumber(stats.total_lines)}
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">Lines</div>
|
||||
</div>
|
||||
<div className="bg-card border rounded-lg p-4">
|
||||
<div className="text-3xl font-bold bg-gradient-to-r from-orange-500 to-red-500 bg-clip-text text-transparent">
|
||||
{categories.length}
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">Categories</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Features Section */}
|
||||
<section className="container mx-auto px-4 py-16 md:py-24">
|
||||
<div className="text-center space-y-4 mb-12">
|
||||
<h2 className="text-3xl font-bold tracking-tight sm:text-4xl">
|
||||
Why AI Prompts Explorer?
|
||||
</h2>
|
||||
<p className="text-lg text-muted-foreground max-w-2xl mx-auto">
|
||||
Discover, compare, and understand AI tool system prompts
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
<Card className="bg-gradient-to-br from-blue-50 to-cyan-50 dark:from-blue-950/20 dark:to-cyan-950/20 border-blue-200 dark:border-blue-800">
|
||||
<CardHeader>
|
||||
<Search className="w-10 h-10 text-blue-500 mb-2" />
|
||||
<CardTitle>Advanced Search</CardTitle>
|
||||
<CardDescription>
|
||||
Search and filter by category, company, model, or keyword
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-gradient-to-br from-purple-50 to-pink-50 dark:from-purple-950/20 dark:to-pink-950/20 border-purple-200 dark:border-purple-800">
|
||||
<CardHeader>
|
||||
<GitCompare className="w-10 h-10 text-purple-500 mb-2" />
|
||||
<CardTitle>Tool Comparison</CardTitle>
|
||||
<CardDescription>
|
||||
Compare up to 4 tools side-by-side to see differences
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-gradient-to-br from-green-50 to-emerald-50 dark:from-green-950/20 dark:to-emerald-950/20 border-green-200 dark:border-green-800">
|
||||
<CardHeader>
|
||||
<BarChart3 className="w-10 h-10 text-green-500 mb-2" />
|
||||
<CardTitle>Analytics & Stats</CardTitle>
|
||||
<CardDescription>
|
||||
Visualize trends and statistics across all AI tools
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-gradient-to-br from-orange-50 to-red-50 dark:from-orange-950/20 dark:to-red-950/20 border-orange-200 dark:border-orange-800">
|
||||
<CardHeader>
|
||||
<Code className="w-10 h-10 text-orange-500 mb-2" />
|
||||
<CardTitle>Complete Prompts</CardTitle>
|
||||
<CardDescription>
|
||||
Full system prompts and tool configurations included
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-gradient-to-br from-indigo-50 to-blue-50 dark:from-indigo-950/20 dark:to-blue-950/20 border-indigo-200 dark:border-indigo-800">
|
||||
<CardHeader>
|
||||
<Zap className="w-10 h-10 text-indigo-500 mb-2" />
|
||||
<CardTitle>Regular Updates</CardTitle>
|
||||
<CardDescription>
|
||||
New tools and updates added regularly from the community
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-gradient-to-br from-pink-50 to-rose-50 dark:from-pink-950/20 dark:to-rose-950/20 border-pink-200 dark:border-pink-800">
|
||||
<CardHeader>
|
||||
<Shield className="w-10 h-10 text-pink-500 mb-2" />
|
||||
<CardTitle>Open Source</CardTitle>
|
||||
<CardDescription>
|
||||
Fully open source with community contributions welcome
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Featured Tools Section */}
|
||||
<section className="bg-muted/30 py-16 md:py-24">
|
||||
<div className="container mx-auto px-4">
|
||||
<div className="text-center space-y-4 mb-12">
|
||||
<h2 className="text-3xl font-bold tracking-tight sm:text-4xl">
|
||||
Featured AI Tools
|
||||
</h2>
|
||||
<p className="text-lg text-muted-foreground max-w-2xl mx-auto">
|
||||
Explore some of the most popular AI coding tools and their system prompts
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3 mb-8">
|
||||
{featuredTools.map((tool) => (
|
||||
<ToolCard key={tool.directory} tool={tool} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<Button asChild size="lg" variant="outline">
|
||||
<Link href="/browse">
|
||||
View All {stats.total_tools} Tools
|
||||
<ArrowRight className="ml-2 w-4 h-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* CTA Section */}
|
||||
<section className="container mx-auto px-4 py-16 md:py-24">
|
||||
<Card className="bg-gradient-to-r from-blue-500 via-purple-500 to-pink-500 text-white border-0">
|
||||
<CardContent className="p-8 md:p-12 text-center">
|
||||
<h2 className="text-3xl font-bold mb-4">
|
||||
Ready to Explore AI Prompts?
|
||||
</h2>
|
||||
<p className="text-lg mb-8 text-white/90 max-w-2xl mx-auto">
|
||||
Start browsing our collection of {stats.total_tools}+ AI tools and discover how they work
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<Button asChild size="lg" className="bg-white text-purple-600 hover:bg-white/90">
|
||||
<Link href="/browse">
|
||||
Start Exploring
|
||||
<ArrowRight className="ml-2 w-4 h-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
<Button asChild size="lg" variant="outline" className="border-white text-white hover:bg-white/10">
|
||||
<a href="https://github.com/x1xhlol/system-prompts-and-models-of-ai-tools" target="_blank" rel="noopener noreferrer">
|
||||
View on GitHub
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
210
web/app/stats/page.tsx
Normal file
210
web/app/stats/page.tsx
Normal file
@ -0,0 +1,210 @@
|
||||
'use client'
|
||||
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { getAllTools, getStats, getCategoryIcon, getCategoryColor } from '@/lib/data'
|
||||
import { formatNumber } from '@/lib/utils'
|
||||
import { BarChart3, TrendingUp, Package, FileText, Code } from 'lucide-react'
|
||||
|
||||
export default function StatsPage() {
|
||||
const stats = getStats()
|
||||
const tools = getAllTools()
|
||||
|
||||
// Calculate additional stats
|
||||
const topByLines = [...tools].sort((a, b) => b.total_lines - a.total_lines).slice(0, 10)
|
||||
const topByFiles = [...tools].sort((a, b) => b.file_count - a.file_count).slice(0, 10)
|
||||
|
||||
// Get max values for scaling
|
||||
const maxByCategory = Math.max(...Object.values(stats.by_category))
|
||||
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<h1 className="text-4xl font-bold mb-4">Statistics & Analytics</h1>
|
||||
<p className="text-lg text-muted-foreground">
|
||||
Comprehensive insights into AI tools and their configurations
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Overview Stats */}
|
||||
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-4 mb-8">
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardDescription>Total AI Tools</CardDescription>
|
||||
<CardTitle className="text-4xl">{stats.total_tools}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-xs text-muted-foreground flex items-center gap-1">
|
||||
<TrendingUp className="w-3 h-3" />
|
||||
Across {Object.keys(stats.by_category).length} categories
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardDescription>Total Files</CardDescription>
|
||||
<CardTitle className="text-4xl">{stats.total_files}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-xs text-muted-foreground flex items-center gap-1">
|
||||
<FileText className="w-3 h-3" />
|
||||
Configuration & prompt files
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardDescription>Total Lines</CardDescription>
|
||||
<CardTitle className="text-4xl">{formatNumber(stats.total_lines)}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-xs text-muted-foreground flex items-center gap-1">
|
||||
<Code className="w-3 h-3" />
|
||||
Of system prompts & configs
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardDescription>Average per Tool</CardDescription>
|
||||
<CardTitle className="text-4xl">
|
||||
{Math.round(stats.total_lines / stats.total_tools)}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-xs text-muted-foreground flex items-center gap-1">
|
||||
<BarChart3 className="w-3 h-3" />
|
||||
Lines per tool
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Category Distribution */}
|
||||
<Card className="mb-8">
|
||||
<CardHeader>
|
||||
<CardTitle>Tools by Category</CardTitle>
|
||||
<CardDescription>Distribution across all categories</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{Object.entries(stats.by_category)
|
||||
.sort(([, a], [, b]) => b - a)
|
||||
.map(([category, count]) => {
|
||||
const percentage = (count / maxByCategory) * 100
|
||||
return (
|
||||
<div key={category} className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-2xl">{getCategoryIcon(category)}</span>
|
||||
<span className="font-medium">{category}</span>
|
||||
</div>
|
||||
<Badge variant="secondary">{count} tools</Badge>
|
||||
</div>
|
||||
<div className="w-full bg-muted rounded-full h-2">
|
||||
<div
|
||||
className={`h-2 rounded-full bg-gradient-to-r ${getCategoryColor(category)}`}
|
||||
style={{ width: `${percentage}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="grid gap-6 lg:grid-cols-2 mb-8">
|
||||
{/* Top by Lines */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Most Complex (by lines)</CardTitle>
|
||||
<CardDescription>Tools with the most lines of code</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
{topByLines.map((tool, index) => (
|
||||
<div key={tool.directory} className="flex items-center gap-3">
|
||||
<div className="flex-shrink-0 w-6 text-center font-bold text-muted-foreground">
|
||||
{index + 1}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-medium truncate">{tool.name}</div>
|
||||
<div className="text-xs text-muted-foreground truncate">{tool.company}</div>
|
||||
</div>
|
||||
<Badge variant="outline">{formatNumber(tool.total_lines)} lines</Badge>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Top by Files */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Most Files</CardTitle>
|
||||
<CardDescription>Tools with the most configuration files</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
{topByFiles.map((tool, index) => (
|
||||
<div key={tool.directory} className="flex items-center gap-3">
|
||||
<div className="flex-shrink-0 w-6 text-center font-bold text-muted-foreground">
|
||||
{index + 1}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-medium truncate">{tool.name}</div>
|
||||
<div className="text-xs text-muted-foreground truncate">{tool.company}</div>
|
||||
</div>
|
||||
<Badge variant="outline">{tool.file_count} files</Badge>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Type Distribution */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Open Source vs Proprietary</CardTitle>
|
||||
<CardDescription>Distribution by tool type</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{Object.entries(stats.by_type).map(([type, count]) => {
|
||||
const percentage = (count / stats.total_tools) * 100
|
||||
return (
|
||||
<div key={type} className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-medium capitalize">{type}</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="secondary">{count} tools</Badge>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{percentage.toFixed(1)}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full bg-muted rounded-full h-2">
|
||||
<div
|
||||
className={`h-2 rounded-full ${
|
||||
type === 'open-source'
|
||||
? 'bg-gradient-to-r from-green-500 to-emerald-500'
|
||||
: 'bg-gradient-to-r from-blue-500 to-purple-500'
|
||||
}`}
|
||||
style={{ width: `${percentage}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
225
web/app/tool/[slug]/page.tsx
Normal file
225
web/app/tool/[slug]/page.tsx
Normal file
@ -0,0 +1,225 @@
|
||||
import { notFound } from 'next/navigation'
|
||||
import Link from 'next/link'
|
||||
import { ArrowLeft, ExternalLink, FileText, Code, Calendar, GitCompare, Heart } from 'lucide-react'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { getAllTools, getCategoryIcon, getCategoryColor } from '@/lib/data'
|
||||
import { formatNumber, formatBytes, slugify } from '@/lib/utils'
|
||||
|
||||
export async function generateStaticParams() {
|
||||
const tools = getAllTools()
|
||||
return tools.map((tool) => ({
|
||||
slug: slugify(tool.directory),
|
||||
}))
|
||||
}
|
||||
|
||||
export default function ToolDetailPage({ params }: { params: { slug: string } }) {
|
||||
const tools = getAllTools()
|
||||
const tool = tools.find((t) => slugify(t.directory) === params.slug)
|
||||
|
||||
if (!tool) {
|
||||
notFound()
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
{/* Back Button */}
|
||||
<Button asChild variant="ghost" className="mb-6">
|
||||
<Link href="/browse">
|
||||
<ArrowLeft className="w-4 h-4 mr-2" />
|
||||
Back to Browse
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<div className="flex items-start gap-4 mb-4">
|
||||
<div className="text-5xl">{getCategoryIcon(tool.category)}</div>
|
||||
<div className="flex-1">
|
||||
<div className="flex flex-wrap items-center gap-2 mb-2">
|
||||
<h1 className="text-4xl font-bold">{tool.name}</h1>
|
||||
<Badge variant="secondary">{tool.category}</Badge>
|
||||
<Badge variant="outline">{tool.type}</Badge>
|
||||
</div>
|
||||
<p className="text-xl text-muted-foreground mb-4">{tool.company}</p>
|
||||
<p className="text-lg">{tool.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Quick Stats */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardDescription className="text-xs">Files</CardDescription>
|
||||
<CardTitle className="text-2xl">{tool.file_count}</CardTitle>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardDescription className="text-xs">Total Lines</CardDescription>
|
||||
<CardTitle className="text-2xl">{formatNumber(tool.total_lines)}</CardTitle>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardDescription className="text-xs">Models</CardDescription>
|
||||
<CardTitle className="text-2xl">{tool.models.length || '-'}</CardTitle>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardDescription className="text-xs">Category</CardDescription>
|
||||
<CardTitle className="text-xl truncate">{tool.category}</CardTitle>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 lg:grid-cols-3">
|
||||
<div className="lg:col-span-2 space-y-6">
|
||||
{/* Files */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<FileText className="w-5 h-5" />
|
||||
Configuration Files
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
{tool.file_count} files containing system prompts and configurations
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2">
|
||||
{tool.files.map((file) => (
|
||||
<div
|
||||
key={file.path}
|
||||
className="flex items-center justify-between p-3 border rounded-lg hover:bg-muted/50 transition-colors"
|
||||
>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-medium truncate">{file.name}</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{formatBytes(file.size)}
|
||||
{file.lines && ` • ${formatNumber(file.lines)} lines`}
|
||||
</div>
|
||||
</div>
|
||||
<Badge variant="outline" className="ml-2">
|
||||
{file.type}
|
||||
</Badge>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Models */}
|
||||
{tool.models.length > 0 && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Code className="w-5 h-5" />
|
||||
AI Models
|
||||
</CardTitle>
|
||||
<CardDescription>Models used or supported by this tool</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{tool.models.map((model) => (
|
||||
<Badge key={model} variant="secondary" className="text-sm">
|
||||
{model}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Subcategories */}
|
||||
{tool.subcategories && tool.subcategories.length > 0 && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Related Tools</CardTitle>
|
||||
<CardDescription>Other tools in this collection</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{tool.subcategories.map((sub) => (
|
||||
<Badge key={sub} variant="outline">
|
||||
{sub}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Sidebar */}
|
||||
<div className="space-y-6">
|
||||
{/* Actions */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">Actions</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2">
|
||||
{tool.website && (
|
||||
<Button asChild className="w-full">
|
||||
<a href={tool.website} target="_blank" rel="noopener noreferrer">
|
||||
<ExternalLink className="w-4 h-4 mr-2" />
|
||||
Visit Website
|
||||
</a>
|
||||
</Button>
|
||||
)}
|
||||
<Button variant="outline" className="w-full">
|
||||
<a
|
||||
href={`https://github.com/x1xhlol/system-prompts-and-models-of-ai-tools/tree/main/${encodeURIComponent(
|
||||
tool.directory
|
||||
)}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center w-full"
|
||||
>
|
||||
<FileText className="w-4 h-4 mr-2" />
|
||||
View on GitHub
|
||||
</a>
|
||||
</Button>
|
||||
<Button variant="outline" className="w-full">
|
||||
<GitCompare className="w-4 h-4 mr-2" />
|
||||
Add to Compare
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Info */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">Information</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3 text-sm">
|
||||
<div>
|
||||
<div className="text-muted-foreground mb-1">Company</div>
|
||||
<div className="font-medium">{tool.company}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-muted-foreground mb-1">Category</div>
|
||||
<div className="font-medium">{tool.category}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-muted-foreground mb-1">Type</div>
|
||||
<Badge variant="outline" className="capitalize">
|
||||
{tool.type}
|
||||
</Badge>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-muted-foreground mb-1">Total Size</div>
|
||||
<div className="font-medium">
|
||||
{formatBytes(tool.files.reduce((acc, f) => acc + f.size, 0))}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
123
web/components/footer.tsx
Normal file
123
web/components/footer.tsx
Normal file
@ -0,0 +1,123 @@
|
||||
import Link from 'next/link'
|
||||
import { Github, Heart, ExternalLink } from 'lucide-react'
|
||||
|
||||
export function Footer() {
|
||||
const currentYear = new Date().getFullYear()
|
||||
|
||||
return (
|
||||
<footer className="w-full border-t bg-background">
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-8">
|
||||
{/* About */}
|
||||
<div className="space-y-3">
|
||||
<h3 className="font-semibold text-lg">AI Prompts Explorer</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
The most comprehensive collection of AI tool system prompts and configurations.
|
||||
</p>
|
||||
<div className="flex items-center gap-4">
|
||||
<Link
|
||||
href="https://github.com/x1xhlol/system-prompts-and-models-of-ai-tools"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
<Github className="w-5 h-5" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Quick Links */}
|
||||
<div className="space-y-3">
|
||||
<h3 className="font-semibold">Quick Links</h3>
|
||||
<ul className="space-y-2 text-sm text-muted-foreground">
|
||||
<li>
|
||||
<Link href="/" className="hover:text-foreground transition-colors">
|
||||
Home
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/browse" className="hover:text-foreground transition-colors">
|
||||
Browse Tools
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/stats" className="hover:text-foreground transition-colors">
|
||||
Statistics
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/compare" className="hover:text-foreground transition-colors">
|
||||
Compare Tools
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Resources */}
|
||||
<div className="space-y-3">
|
||||
<h3 className="font-semibold">Resources</h3>
|
||||
<ul className="space-y-2 text-sm text-muted-foreground">
|
||||
<li>
|
||||
<Link href="/about" className="hover:text-foreground transition-colors">
|
||||
About
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://github.com/x1xhlol/system-prompts-and-models-of-ai-tools/blob/main/CONTRIBUTING.md"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hover:text-foreground transition-colors flex items-center gap-1"
|
||||
>
|
||||
Contributing
|
||||
<ExternalLink className="w-3 h-3" />
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://github.com/x1xhlol/system-prompts-and-models-of-ai-tools/blob/main/CHANGELOG.md"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hover:text-foreground transition-colors flex items-center gap-1"
|
||||
>
|
||||
Changelog
|
||||
<ExternalLink className="w-3 h-3" />
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://discord.gg/NwzrWErdMU"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hover:text-foreground transition-colors flex items-center gap-1"
|
||||
>
|
||||
Discord
|
||||
<ExternalLink className="w-3 h-3" />
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="space-y-3">
|
||||
<h3 className="font-semibold">Repository Stats</h3>
|
||||
<ul className="space-y-2 text-sm text-muted-foreground">
|
||||
<li>32+ AI Tools</li>
|
||||
<li>96 Files</li>
|
||||
<li>20,000+ Lines</li>
|
||||
<li>11 Categories</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 pt-8 border-t text-center text-sm text-muted-foreground">
|
||||
<p className="flex items-center justify-center gap-1 flex-wrap">
|
||||
Built with <Heart className="w-4 h-4 text-red-500 fill-red-500 inline" /> by the community
|
||||
<span className="mx-1">•</span>
|
||||
© {currentYear} AI Prompts Explorer
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
209
web/components/navbar.tsx
Normal file
209
web/components/navbar.tsx
Normal file
@ -0,0 +1,209 @@
|
||||
'use client'
|
||||
|
||||
import Link from 'next/link'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { Search, Moon, Sun, Menu, X, Home, Grid, BarChart3, BookOpen, GitCompare } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useTheme } from 'next-themes'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useAppStore } from '@/lib/store'
|
||||
|
||||
const navigation = [
|
||||
{ name: 'Home', href: '/', icon: Home },
|
||||
{ name: 'Browse', href: '/browse', icon: Grid },
|
||||
{ name: 'Statistics', href: '/stats', icon: BarChart3 },
|
||||
{ name: 'Compare', href: '/compare', icon: GitCompare },
|
||||
{ name: 'About', href: '/about', icon: BookOpen },
|
||||
]
|
||||
|
||||
export function Navbar() {
|
||||
const pathname = usePathname()
|
||||
const { theme, setTheme } = useTheme()
|
||||
const [mounted, setMounted] = useState(false)
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const { filters, setFilters } = useAppStore()
|
||||
const comparison = useAppStore((state) => state.comparison)
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true)
|
||||
}, [])
|
||||
|
||||
const handleSearch = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setFilters({ search: searchQuery })
|
||||
if (pathname !== '/browse') {
|
||||
window.location.href = '/browse'
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<nav className="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
||||
<div className="container mx-auto px-4">
|
||||
<div className="flex h-16 items-center justify-between">
|
||||
{/* Logo */}
|
||||
<Link href="/" className="flex items-center space-x-2 group">
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-blue-500 to-purple-500 rounded-lg blur opacity-75 group-hover:opacity-100 transition-opacity" />
|
||||
<div className="relative bg-gradient-to-r from-blue-500 to-purple-500 p-2 rounded-lg">
|
||||
<span className="text-white font-bold text-xl">AI</span>
|
||||
</div>
|
||||
</div>
|
||||
<span className="hidden sm:inline-block font-bold text-xl bg-gradient-to-r from-blue-500 to-purple-500 bg-clip-text text-transparent">
|
||||
Prompts Explorer
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
{/* Desktop Navigation */}
|
||||
<div className="hidden md:flex items-center space-x-1">
|
||||
{navigation.map((item) => {
|
||||
const Icon = item.icon
|
||||
const isActive = pathname === item.href
|
||||
return (
|
||||
<Link
|
||||
key={item.name}
|
||||
href={item.href}
|
||||
className={cn(
|
||||
'flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-all',
|
||||
isActive
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'text-muted-foreground hover:text-foreground hover:bg-muted'
|
||||
)}
|
||||
>
|
||||
<Icon className="w-4 h-4" />
|
||||
{item.name}
|
||||
{item.name === 'Compare' && comparison.length > 0 && (
|
||||
<span className="ml-1 bg-destructive text-destructive-foreground text-xs rounded-full px-1.5 py-0.5">
|
||||
{comparison.length}
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Search Bar */}
|
||||
<form onSubmit={handleSearch} className="hidden lg:flex items-center flex-1 max-w-md mx-8">
|
||||
<div className="relative w-full">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
||||
<Input
|
||||
type="search"
|
||||
placeholder="Search AI tools..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-10 pr-4"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{/* Right Actions */}
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Theme Toggle */}
|
||||
{mounted && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
|
||||
className="hidden sm:inline-flex"
|
||||
>
|
||||
{theme === 'dark' ? (
|
||||
<Sun className="h-5 w-5" />
|
||||
) : (
|
||||
<Moon className="h-5 w-5" />
|
||||
)}
|
||||
<span className="sr-only">Toggle theme</span>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Mobile Menu Button */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
|
||||
className="md:hidden"
|
||||
>
|
||||
{mobileMenuOpen ? (
|
||||
<X className="h-5 w-5" />
|
||||
) : (
|
||||
<Menu className="h-5 w-5" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile Menu */}
|
||||
{mobileMenuOpen && (
|
||||
<div className="md:hidden border-t py-4 space-y-4 animate-slide-down">
|
||||
{/* Mobile Search */}
|
||||
<form onSubmit={handleSearch} className="px-2">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
||||
<Input
|
||||
type="search"
|
||||
placeholder="Search AI tools..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{/* Mobile Navigation Links */}
|
||||
<div className="space-y-1 px-2">
|
||||
{navigation.map((item) => {
|
||||
const Icon = item.icon
|
||||
const isActive = pathname === item.href
|
||||
return (
|
||||
<Link
|
||||
key={item.name}
|
||||
href={item.href}
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
className={cn(
|
||||
'flex items-center gap-3 px-3 py-2 rounded-md text-base font-medium transition-all',
|
||||
isActive
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'text-muted-foreground hover:text-foreground hover:bg-muted'
|
||||
)}
|
||||
>
|
||||
<Icon className="w-5 h-5" />
|
||||
{item.name}
|
||||
{item.name === 'Compare' && comparison.length > 0 && (
|
||||
<span className="ml-auto bg-destructive text-destructive-foreground text-xs rounded-full px-2 py-0.5">
|
||||
{comparison.length}
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Mobile Theme Toggle */}
|
||||
{mounted && (
|
||||
<div className="px-2 pt-2 border-t">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
|
||||
className="w-full justify-start"
|
||||
>
|
||||
{theme === 'dark' ? (
|
||||
<>
|
||||
<Sun className="mr-2 h-5 w-5" />
|
||||
Light Mode
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Moon className="mr-2 h-5 w-5" />
|
||||
Dark Mode
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
9
web/components/theme-provider.tsx
Normal file
9
web/components/theme-provider.tsx
Normal file
@ -0,0 +1,9 @@
|
||||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
import { ThemeProvider as NextThemesProvider } from 'next-themes'
|
||||
import { type ThemeProviderProps } from 'next-themes/dist/types'
|
||||
|
||||
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
||||
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
|
||||
}
|
||||
170
web/components/tool-card.tsx
Normal file
170
web/components/tool-card.tsx
Normal file
@ -0,0 +1,170 @@
|
||||
'use client'
|
||||
|
||||
import Link from 'next/link'
|
||||
import { Heart, GitCompare, ExternalLink, FileText, Code } from 'lucide-react'
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { AITool } from '@/lib/types'
|
||||
import { getCategoryIcon, getCategoryColor } from '@/lib/data'
|
||||
import { useAppStore } from '@/lib/store'
|
||||
import { formatNumber, slugify } from '@/lib/utils'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface ToolCardProps {
|
||||
tool: AITool
|
||||
variant?: 'default' | 'compact'
|
||||
}
|
||||
|
||||
export function ToolCard({ tool, variant = 'default' }: ToolCardProps) {
|
||||
const { favorites, addFavorite, removeFavorite, isFavorite } = useAppStore()
|
||||
const { comparison, addToComparison, removeFromComparison, isInComparison } = useAppStore()
|
||||
|
||||
const favorite = isFavorite(tool.directory)
|
||||
const inComparison = isInComparison(tool.directory)
|
||||
|
||||
const toggleFavorite = (e: React.MouseEvent) => {
|
||||
e.preventDefault()
|
||||
if (favorite) {
|
||||
removeFavorite(tool.directory)
|
||||
} else {
|
||||
addFavorite(tool.directory)
|
||||
}
|
||||
}
|
||||
|
||||
const toggleComparison = (e: React.MouseEvent) => {
|
||||
e.preventDefault()
|
||||
if (inComparison) {
|
||||
removeFromComparison(tool.directory)
|
||||
} else if (comparison.length < 4) {
|
||||
addToComparison(tool.directory)
|
||||
}
|
||||
}
|
||||
|
||||
if (variant === 'compact') {
|
||||
return (
|
||||
<Link href={`/tool/${slugify(tool.directory)}`}>
|
||||
<Card className="h-full hover:shadow-lg transition-all cursor-pointer group">
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex-1 min-w-0">
|
||||
<CardTitle className="text-lg line-clamp-1 group-hover:text-primary transition-colors">
|
||||
{tool.name}
|
||||
</CardTitle>
|
||||
<CardDescription className="text-xs line-clamp-1 mt-1">
|
||||
{tool.company}
|
||||
</CardDescription>
|
||||
</div>
|
||||
<div className={cn('text-2xl flex-shrink-0', getCategoryColor(tool.category))}>
|
||||
{getCategoryIcon(tool.category)}
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="pb-3">
|
||||
<div className="flex flex-wrap gap-1">
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{tool.category}
|
||||
</Badge>
|
||||
{tool.models.slice(0, 1).map((model) => (
|
||||
<Badge key={model} variant="outline" className="text-xs">
|
||||
{model}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="h-full flex flex-col hover:shadow-lg transition-all group">
|
||||
<CardHeader>
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<div className="text-3xl">{getCategoryIcon(tool.category)}</div>
|
||||
<Badge variant="secondary">{tool.category}</Badge>
|
||||
</div>
|
||||
<CardTitle className="text-xl line-clamp-1 group-hover:text-primary transition-colors">
|
||||
{tool.name}
|
||||
</CardTitle>
|
||||
<CardDescription className="line-clamp-1 mt-1">
|
||||
{tool.company}
|
||||
</CardDescription>
|
||||
</div>
|
||||
<div className="flex gap-1 flex-shrink-0">
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
onClick={toggleFavorite}
|
||||
className={cn('h-8 w-8', favorite && 'text-red-500')}
|
||||
>
|
||||
<Heart className={cn('w-4 h-4', favorite && 'fill-current')} />
|
||||
</Button>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
onClick={toggleComparison}
|
||||
className={cn('h-8 w-8', inComparison && 'text-blue-500')}
|
||||
disabled={!inComparison && comparison.length >= 4}
|
||||
>
|
||||
<GitCompare className={cn('w-4 h-4', inComparison && 'fill-current')} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="flex-1">
|
||||
<p className="text-sm text-muted-foreground line-clamp-3 mb-4">
|
||||
{tool.description}
|
||||
</p>
|
||||
|
||||
<div className="space-y-2">
|
||||
{/* Models */}
|
||||
{tool.models.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{tool.models.slice(0, 3).map((model) => (
|
||||
<Badge key={model} variant="outline" className="text-xs">
|
||||
{model}
|
||||
</Badge>
|
||||
))}
|
||||
{tool.models.length > 3 && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
+{tool.models.length - 3} more
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Stats */}
|
||||
<div className="flex items-center gap-4 text-xs text-muted-foreground">
|
||||
<div className="flex items-center gap-1">
|
||||
<FileText className="w-3 h-3" />
|
||||
{tool.file_count} files
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Code className="w-3 h-3" />
|
||||
{formatNumber(tool.total_lines)} lines
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
<CardFooter className="flex gap-2">
|
||||
<Button asChild className="flex-1">
|
||||
<Link href={`/tool/${slugify(tool.directory)}`}>
|
||||
View Details
|
||||
</Link>
|
||||
</Button>
|
||||
{tool.website && (
|
||||
<Button asChild variant="outline" size="icon">
|
||||
<a href={tool.website} target="_blank" rel="noopener noreferrer">
|
||||
<ExternalLink className="w-4 h-4" />
|
||||
</a>
|
||||
</Button>
|
||||
)}
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
29
web/components/ui/badge.tsx
Normal file
29
web/components/ui/badge.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
import * as React from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export interface BadgeProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
variant?: 'default' | 'secondary' | 'destructive' | 'outline'
|
||||
}
|
||||
|
||||
function Badge({ className, variant = 'default', ...props }: BadgeProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
|
||||
{
|
||||
'border-transparent bg-primary text-primary-foreground hover:bg-primary/80':
|
||||
variant === 'default',
|
||||
'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80':
|
||||
variant === 'secondary',
|
||||
'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80':
|
||||
variant === 'destructive',
|
||||
'text-foreground': variant === 'outline',
|
||||
},
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Badge }
|
||||
44
web/components/ui/button.tsx
Normal file
44
web/components/ui/button.tsx
Normal file
@ -0,0 +1,44 @@
|
||||
import * as React from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
variant?: 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link'
|
||||
size?: 'default' | 'sm' | 'lg' | 'icon'
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant = 'default', size = 'default', ...props }, ref) => {
|
||||
return (
|
||||
<button
|
||||
className={cn(
|
||||
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
|
||||
{
|
||||
'bg-primary text-primary-foreground hover:bg-primary/90 active:scale-95':
|
||||
variant === 'default',
|
||||
'bg-destructive text-destructive-foreground hover:bg-destructive/90':
|
||||
variant === 'destructive',
|
||||
'border border-input bg-background hover:bg-accent hover:text-accent-foreground':
|
||||
variant === 'outline',
|
||||
'bg-secondary text-secondary-foreground hover:bg-secondary/80':
|
||||
variant === 'secondary',
|
||||
'hover:bg-accent hover:text-accent-foreground': variant === 'ghost',
|
||||
'text-primary underline-offset-4 hover:underline': variant === 'link',
|
||||
},
|
||||
{
|
||||
'h-10 px-4 py-2': size === 'default',
|
||||
'h-9 rounded-md px-3': size === 'sm',
|
||||
'h-11 rounded-md px-8': size === 'lg',
|
||||
'h-10 w-10': size === 'icon',
|
||||
},
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Button.displayName = 'Button'
|
||||
|
||||
export { Button }
|
||||
78
web/components/ui/card.tsx
Normal file
78
web/components/ui/card.tsx
Normal file
@ -0,0 +1,78 @@
|
||||
import * as React from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const Card = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'rounded-lg border bg-card text-card-foreground shadow-sm transition-all hover:shadow-md',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Card.displayName = 'Card'
|
||||
|
||||
const CardHeader = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('flex flex-col space-y-1.5 p-6', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardHeader.displayName = 'CardHeader'
|
||||
|
||||
const CardTitle = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLHeadingElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<h3
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'text-2xl font-semibold leading-none tracking-tight',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardTitle.displayName = 'CardTitle'
|
||||
|
||||
const CardDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<p
|
||||
ref={ref}
|
||||
className={cn('text-sm text-muted-foreground', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardDescription.displayName = 'CardDescription'
|
||||
|
||||
const CardContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn('p-6 pt-0', className)} {...props} />
|
||||
))
|
||||
CardContent.displayName = 'CardContent'
|
||||
|
||||
const CardFooter = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('flex items-center p-6 pt-0', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardFooter.displayName = 'CardFooter'
|
||||
|
||||
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
||||
24
web/components/ui/input.tsx
Normal file
24
web/components/ui/input.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import * as React from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export interface InputProps
|
||||
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
({ className, type, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Input.displayName = 'Input'
|
||||
|
||||
export { Input }
|
||||
1286
web/data/index.json
Normal file
1286
web/data/index.json
Normal file
File diff suppressed because it is too large
Load Diff
101
web/lib/data.ts
Normal file
101
web/lib/data.ts
Normal file
@ -0,0 +1,101 @@
|
||||
import { AITool, ToolIndex } from './types'
|
||||
import indexData from '../data/index.json'
|
||||
|
||||
export function getToolIndex(): ToolIndex {
|
||||
return indexData as ToolIndex
|
||||
}
|
||||
|
||||
export function getAllTools(): AITool[] {
|
||||
const index = getToolIndex()
|
||||
return index.tools
|
||||
}
|
||||
|
||||
export function getToolByDirectory(directory: string): AITool | undefined {
|
||||
const tools = getAllTools()
|
||||
return tools.find(tool => tool.directory === directory)
|
||||
}
|
||||
|
||||
export function getToolsByCategory(category: string): AITool[] {
|
||||
const tools = getAllTools()
|
||||
return tools.filter(tool => tool.category === category)
|
||||
}
|
||||
|
||||
export function getToolsByCompany(company: string): AITool[] {
|
||||
const tools = getAllTools()
|
||||
return tools.filter(tool => tool.company === company)
|
||||
}
|
||||
|
||||
export function searchTools(query: string): AITool[] {
|
||||
if (!query) return getAllTools()
|
||||
|
||||
const lowercaseQuery = query.toLowerCase()
|
||||
const tools = getAllTools()
|
||||
|
||||
return tools.filter(tool => {
|
||||
return (
|
||||
tool.name.toLowerCase().includes(lowercaseQuery) ||
|
||||
tool.description.toLowerCase().includes(lowercaseQuery) ||
|
||||
tool.company.toLowerCase().includes(lowercaseQuery) ||
|
||||
tool.category.toLowerCase().includes(lowercaseQuery) ||
|
||||
tool.models.some(model => model.toLowerCase().includes(lowercaseQuery))
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
export function getCategories(): string[] {
|
||||
const index = getToolIndex()
|
||||
return Object.keys(index.stats.by_category).sort()
|
||||
}
|
||||
|
||||
export function getCompanies(): string[] {
|
||||
const tools = getAllTools()
|
||||
const companies = new Set(tools.map(tool => tool.company))
|
||||
return Array.from(companies).sort()
|
||||
}
|
||||
|
||||
export function getModels(): string[] {
|
||||
const tools = getAllTools()
|
||||
const models = new Set<string>()
|
||||
tools.forEach(tool => {
|
||||
tool.models.forEach(model => models.add(model))
|
||||
})
|
||||
return Array.from(models).sort()
|
||||
}
|
||||
|
||||
export function getStats() {
|
||||
const index = getToolIndex()
|
||||
return index.stats
|
||||
}
|
||||
|
||||
export function getCategoryIcon(category: string): string {
|
||||
const icons: Record<string, string> = {
|
||||
'Code Assistant': '💻',
|
||||
'IDE': '🏢',
|
||||
'AI Agent': '🤖',
|
||||
'Web Builder': '🌐',
|
||||
'Terminal': '🖥️',
|
||||
'Cloud IDE': '🏭',
|
||||
'Document Assistant': '📝',
|
||||
'Search Assistant': '🔍',
|
||||
'Foundation Model': '🧠',
|
||||
'Collection': '📚',
|
||||
'Unknown': '❓',
|
||||
}
|
||||
return icons[category] || '📦'
|
||||
}
|
||||
|
||||
export function getCategoryColor(category: string): string {
|
||||
const colors: Record<string, string> = {
|
||||
'Code Assistant': 'from-blue-500 to-cyan-500',
|
||||
'IDE': 'from-purple-500 to-pink-500',
|
||||
'AI Agent': 'from-green-500 to-emerald-500',
|
||||
'Web Builder': 'from-orange-500 to-red-500',
|
||||
'Terminal': 'from-gray-500 to-slate-500',
|
||||
'Cloud IDE': 'from-indigo-500 to-blue-500',
|
||||
'Document Assistant': 'from-yellow-500 to-orange-500',
|
||||
'Search Assistant': 'from-pink-500 to-rose-500',
|
||||
'Foundation Model': 'from-violet-500 to-purple-500',
|
||||
'Collection': 'from-teal-500 to-cyan-500',
|
||||
}
|
||||
return colors[category] || 'from-gray-500 to-slate-500'
|
||||
}
|
||||
89
web/lib/store.ts
Normal file
89
web/lib/store.ts
Normal file
@ -0,0 +1,89 @@
|
||||
import { create } from 'zustand'
|
||||
import { persist } from 'zustand/middleware'
|
||||
import { AITool, FilterState } from './types'
|
||||
|
||||
interface AppState {
|
||||
// Favorites
|
||||
favorites: string[]
|
||||
addFavorite: (toolDirectory: string) => void
|
||||
removeFavorite: (toolDirectory: string) => void
|
||||
isFavorite: (toolDirectory: string) => boolean
|
||||
|
||||
// Comparison
|
||||
comparison: string[]
|
||||
addToComparison: (toolDirectory: string) => void
|
||||
removeFromComparison: (toolDirectory: string) => void
|
||||
clearComparison: () => void
|
||||
isInComparison: (toolDirectory: string) => boolean
|
||||
|
||||
// Filters
|
||||
filters: FilterState
|
||||
setFilters: (filters: Partial<FilterState>) => void
|
||||
resetFilters: () => void
|
||||
|
||||
// View preferences
|
||||
viewMode: 'grid' | 'list'
|
||||
setViewMode: (mode: 'grid' | 'list') => void
|
||||
}
|
||||
|
||||
const defaultFilters: FilterState = {
|
||||
search: '',
|
||||
categories: [],
|
||||
types: [],
|
||||
companies: [],
|
||||
models: [],
|
||||
sortBy: 'name',
|
||||
sortOrder: 'asc',
|
||||
}
|
||||
|
||||
export const useAppStore = create<AppState>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
// Favorites
|
||||
favorites: [],
|
||||
addFavorite: (toolDirectory) =>
|
||||
set((state) => ({
|
||||
favorites: [...state.favorites, toolDirectory],
|
||||
})),
|
||||
removeFavorite: (toolDirectory) =>
|
||||
set((state) => ({
|
||||
favorites: state.favorites.filter((dir) => dir !== toolDirectory),
|
||||
})),
|
||||
isFavorite: (toolDirectory) => get().favorites.includes(toolDirectory),
|
||||
|
||||
// Comparison
|
||||
comparison: [],
|
||||
addToComparison: (toolDirectory) =>
|
||||
set((state) => {
|
||||
if (state.comparison.length >= 4) return state
|
||||
return { comparison: [...state.comparison, toolDirectory] }
|
||||
}),
|
||||
removeFromComparison: (toolDirectory) =>
|
||||
set((state) => ({
|
||||
comparison: state.comparison.filter((dir) => dir !== toolDirectory),
|
||||
})),
|
||||
clearComparison: () => set({ comparison: [] }),
|
||||
isInComparison: (toolDirectory) =>
|
||||
get().comparison.includes(toolDirectory),
|
||||
|
||||
// Filters
|
||||
filters: defaultFilters,
|
||||
setFilters: (filters) =>
|
||||
set((state) => ({
|
||||
filters: { ...state.filters, ...filters },
|
||||
})),
|
||||
resetFilters: () => set({ filters: defaultFilters }),
|
||||
|
||||
// View preferences
|
||||
viewMode: 'grid',
|
||||
setViewMode: (mode) => set({ viewMode: mode }),
|
||||
}),
|
||||
{
|
||||
name: 'ai-prompts-storage',
|
||||
partialize: (state) => ({
|
||||
favorites: state.favorites,
|
||||
viewMode: state.viewMode,
|
||||
}),
|
||||
}
|
||||
)
|
||||
)
|
||||
60
web/lib/types.ts
Normal file
60
web/lib/types.ts
Normal file
@ -0,0 +1,60 @@
|
||||
export interface AITool {
|
||||
name: string
|
||||
company: string
|
||||
category: string
|
||||
type: 'proprietary' | 'open-source' | 'unknown'
|
||||
description: string
|
||||
website: string | null
|
||||
models: string[]
|
||||
directory: string
|
||||
files: ToolFile[]
|
||||
file_count: number
|
||||
total_lines: number
|
||||
subcategories?: string[]
|
||||
}
|
||||
|
||||
export interface ToolFile {
|
||||
name: string
|
||||
path: string
|
||||
size: number
|
||||
type: string
|
||||
lines?: number
|
||||
modified?: string
|
||||
}
|
||||
|
||||
export interface ToolIndex {
|
||||
generated: string
|
||||
repository: string
|
||||
version: string
|
||||
stats: {
|
||||
total_tools: number
|
||||
total_files: number
|
||||
total_lines: number
|
||||
by_category: Record<string, number>
|
||||
by_type: Record<string, number>
|
||||
}
|
||||
tools: AITool[]
|
||||
}
|
||||
|
||||
export type Category =
|
||||
| 'Code Assistant'
|
||||
| 'IDE'
|
||||
| 'AI Agent'
|
||||
| 'Web Builder'
|
||||
| 'Terminal'
|
||||
| 'Cloud IDE'
|
||||
| 'Document Assistant'
|
||||
| 'Search Assistant'
|
||||
| 'Foundation Model'
|
||||
| 'Collection'
|
||||
| 'Unknown'
|
||||
|
||||
export interface FilterState {
|
||||
search: string
|
||||
categories: Category[]
|
||||
types: ('proprietary' | 'open-source')[]
|
||||
companies: string[]
|
||||
models: string[]
|
||||
sortBy: 'name' | 'lines' | 'files' | 'company'
|
||||
sortOrder: 'asc' | 'desc'
|
||||
}
|
||||
43
web/lib/utils.ts
Normal file
43
web/lib/utils.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import { type ClassValue, clsx } from 'clsx'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
|
||||
export function formatNumber(num: number): string {
|
||||
return new Intl.NumberFormat('en-US').format(num)
|
||||
}
|
||||
|
||||
export function formatBytes(bytes: number): string {
|
||||
if (bytes === 0) return '0 Bytes'
|
||||
const k = 1024
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB']
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i]
|
||||
}
|
||||
|
||||
export function slugify(str: string): string {
|
||||
return str
|
||||
.toLowerCase()
|
||||
.replace(/[^\w\s-]/g, '')
|
||||
.replace(/\s+/g, '-')
|
||||
.replace(/-+/g, '-')
|
||||
.trim()
|
||||
}
|
||||
|
||||
export function truncate(str: string, length: number): string {
|
||||
if (str.length <= length) return str
|
||||
return str.slice(0, length) + '...'
|
||||
}
|
||||
|
||||
export function debounce<T extends (...args: any[]) => any>(
|
||||
func: T,
|
||||
wait: number
|
||||
): (...args: Parameters<T>) => void {
|
||||
let timeout: NodeJS.Timeout | null = null
|
||||
return (...args: Parameters<T>) => {
|
||||
if (timeout) clearTimeout(timeout)
|
||||
timeout = setTimeout(() => func(...args), wait)
|
||||
}
|
||||
}
|
||||
22
web/next.config.ts
Normal file
22
web/next.config.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import type { NextConfig } from 'next'
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
reactStrictMode: true,
|
||||
poweredByHeader: false,
|
||||
|
||||
experimental: {
|
||||
optimizePackageImports: ['lucide-react'],
|
||||
},
|
||||
|
||||
async redirects() {
|
||||
return [
|
||||
{
|
||||
source: '/tools',
|
||||
destination: '/browse',
|
||||
permanent: true,
|
||||
},
|
||||
]
|
||||
},
|
||||
}
|
||||
|
||||
export default nextConfig
|
||||
40
web/package.json
Normal file
40
web/package.json
Normal file
@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "ai-prompts-explorer",
|
||||
"version": "2.0.0",
|
||||
"description": "Modern web interface for exploring AI tool system prompts",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"type-check": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"next": "^15.0.3",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"framer-motion": "^11.5.4",
|
||||
"lucide-react": "^0.446.0",
|
||||
"clsx": "^2.1.1",
|
||||
"tailwind-merge": "^2.5.2",
|
||||
"next-themes": "^0.3.0",
|
||||
"zustand": "^4.5.5",
|
||||
"react-wrap-balancer": "^1.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.7.5",
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-dom": "^19.0.0",
|
||||
"typescript": "^5.6.3",
|
||||
"tailwindcss": "^3.4.13",
|
||||
"postcss": "^8.4.47",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"eslint": "^8.57.1",
|
||||
"eslint-config-next": "^15.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.0.0",
|
||||
"npm": ">=10.0.0"
|
||||
}
|
||||
}
|
||||
9
web/postcss.config.mjs
Normal file
9
web/postcss.config.mjs
Normal file
@ -0,0 +1,9 @@
|
||||
/** @type {import('postcss-load-config').Config} */
|
||||
const config = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
|
||||
export default config
|
||||
62
web/setup.sh
Executable file
62
web/setup.sh
Executable file
@ -0,0 +1,62 @@
|
||||
#!/bin/bash
|
||||
|
||||
# AI Prompts Explorer - Web Setup Script
|
||||
# This script sets up the Next.js web application
|
||||
|
||||
set -e
|
||||
|
||||
echo "🚀 AI Prompts Explorer - Web Setup"
|
||||
echo "=================================="
|
||||
echo ""
|
||||
|
||||
# Check Node version
|
||||
echo "📦 Checking Node.js version..."
|
||||
NODE_VERSION=$(node -v)
|
||||
echo "Found Node.js: $NODE_VERSION"
|
||||
|
||||
if [[ ! "$NODE_VERSION" =~ ^v(22|23|24|25) ]]; then
|
||||
echo "⚠️ Warning: Node.js 22+ is recommended (found $NODE_VERSION)"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
# Check if we're in the web directory
|
||||
if [ ! -f "package.json" ]; then
|
||||
echo "❌ Error: package.json not found"
|
||||
echo "Please run this script from the web/ directory"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Install dependencies
|
||||
echo "📥 Installing dependencies..."
|
||||
npm install
|
||||
|
||||
echo ""
|
||||
|
||||
# Generate metadata if needed
|
||||
if [ ! -f "data/index.json" ]; then
|
||||
echo "📊 Generating metadata..."
|
||||
cd ..
|
||||
python3 scripts/generate_metadata.py
|
||||
mkdir -p web/data
|
||||
cp scripts/index.json web/data/
|
||||
cd web
|
||||
echo "✓ Metadata generated"
|
||||
else
|
||||
echo "✓ Metadata already exists"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "✅ Setup complete!"
|
||||
echo ""
|
||||
echo "🎉 You can now run the development server:"
|
||||
echo " npm run dev"
|
||||
echo ""
|
||||
echo "📖 The app will be available at:"
|
||||
echo " http://localhost:3000"
|
||||
echo ""
|
||||
echo "📚 Other commands:"
|
||||
echo " npm run build - Build for production"
|
||||
echo " npm start - Start production server"
|
||||
echo " npm run lint - Lint code"
|
||||
echo ""
|
||||
86
web/tailwind.config.ts
Normal file
86
web/tailwind.config.ts
Normal file
@ -0,0 +1,86 @@
|
||||
import type { Config } from 'tailwindcss'
|
||||
|
||||
const config: Config = {
|
||||
content: [
|
||||
'./pages/**/*.{js,ts,jsx,tsx,mdx}',
|
||||
'./components/**/*.{js,ts,jsx,tsx,mdx}',
|
||||
'./app/**/*.{js,ts,jsx,tsx,mdx}',
|
||||
],
|
||||
darkMode: 'class',
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
border: 'hsl(var(--border))',
|
||||
input: 'hsl(var(--input))',
|
||||
ring: 'hsl(var(--ring))',
|
||||
background: 'hsl(var(--background))',
|
||||
foreground: 'hsl(var(--foreground))',
|
||||
primary: {
|
||||
DEFAULT: 'hsl(var(--primary))',
|
||||
foreground: 'hsl(var(--primary-foreground))',
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: 'hsl(var(--secondary))',
|
||||
foreground: 'hsl(var(--secondary-foreground))',
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: 'hsl(var(--destructive))',
|
||||
foreground: 'hsl(var(--destructive-foreground))',
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: 'hsl(var(--muted))',
|
||||
foreground: 'hsl(var(--muted-foreground))',
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: 'hsl(var(--accent))',
|
||||
foreground: 'hsl(var(--accent-foreground))',
|
||||
},
|
||||
popover: {
|
||||
DEFAULT: 'hsl(var(--popover))',
|
||||
foreground: 'hsl(var(--popover-foreground))',
|
||||
},
|
||||
card: {
|
||||
DEFAULT: 'hsl(var(--card))',
|
||||
foreground: 'hsl(var(--card-foreground))',
|
||||
},
|
||||
},
|
||||
borderRadius: {
|
||||
lg: 'var(--radius)',
|
||||
md: 'calc(var(--radius) - 2px)',
|
||||
sm: 'calc(var(--radius) - 4px)',
|
||||
},
|
||||
animation: {
|
||||
'fade-in': 'fade-in 0.5s ease-in-out',
|
||||
'slide-in': 'slide-in 0.3s ease-out',
|
||||
'slide-up': 'slide-up 0.3s ease-out',
|
||||
'scale-in': 'scale-in 0.2s ease-out',
|
||||
'shimmer': 'shimmer 2s linear infinite',
|
||||
},
|
||||
keyframes: {
|
||||
'fade-in': {
|
||||
'0%': { opacity: '0' },
|
||||
'100%': { opacity: '1' },
|
||||
},
|
||||
'slide-in': {
|
||||
'0%': { transform: 'translateX(-100%)' },
|
||||
'100%': { transform: 'translateX(0)' },
|
||||
},
|
||||
'slide-up': {
|
||||
'0%': { transform: 'translateY(20px)', opacity: '0' },
|
||||
'100%': { transform: 'translateY(0)', opacity: '1' },
|
||||
},
|
||||
'scale-in': {
|
||||
'0%': { transform: 'scale(0.95)', opacity: '0' },
|
||||
'100%': { transform: 'scale(1)', opacity: '1' },
|
||||
},
|
||||
'shimmer': {
|
||||
'0%': { backgroundPosition: '-1000px 0' },
|
||||
'100%': { backgroundPosition: '1000px 0' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
|
||||
export default config
|
||||
27
web/tsconfig.json
Normal file
27
web/tsconfig.json
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user