-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
17 changed files
with
684 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
# Logs | ||
logs | ||
*.log | ||
npm-debug.log* | ||
yarn-debug.log* | ||
yarn-error.log* | ||
pnpm-debug.log* | ||
lerna-debug.log* | ||
|
||
node_modules | ||
dist | ||
dist-ssr | ||
*.local | ||
|
||
# Editor directories and files | ||
.vscode/* | ||
!.vscode/extensions.json | ||
.idea | ||
.DS_Store | ||
*.suo | ||
*.ntvs* | ||
*.njsproj | ||
*.sln | ||
*.sw? |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,101 @@ | ||
# ParaSearch | ||
# ParaSearch | ||
|
||
A modern search interface built with React that combines traditional search functionality with AI-powered responses. Features a beautiful dark/light theme, animated transitions, and search history management. | ||
|
||
## Features | ||
|
||
- 🔍 Dual-mode search (traditional and AI-powered) | ||
- 🎨 Dark/Light theme with smooth transitions | ||
- ✨ Animated UI elements using Framer Motion | ||
- 📝 Search history management with localStorage | ||
- 🤖 AI-powered responses using Google's Gemini API | ||
- 📱 Fully responsive design | ||
- 🎯 Performance optimized with React.memo and useCallback | ||
|
||
## Tech Stack | ||
|
||
- React | ||
- Tailwind CSS | ||
- Framer Motion | ||
- Google Generative AI (Gemini) | ||
- Lucide React Icons | ||
|
||
## Getting Started | ||
|
||
1. Clone the repository: | ||
```bash | ||
git clone https://github.com/yourusername/parasearch.git | ||
cd parasearch | ||
``` | ||
|
||
2. Install dependencies: | ||
```bash | ||
npm install | ||
``` | ||
|
||
3. Create a `.env` file in the project root and add your Gemini API key: | ||
```env | ||
VITE_GOOGLE_API_KEY=your_api_key_here | ||
``` | ||
|
||
4. Start the development server: | ||
```bash | ||
npm run dev | ||
``` | ||
|
||
## Project Structure | ||
|
||
``` | ||
src/ | ||
├── components/ | ||
│ ├── SearchBar.jsx # Search input with AI toggle | ||
│ ├── SearchHistory.jsx # Recent searches display | ||
│ ├── SearchResults.jsx # Search results display | ||
│ └── ThemeToggle.jsx # Theme switcher | ||
├── App.jsx # Main application component | ||
└── index.js # Application entry point | ||
``` | ||
|
||
## API Integration | ||
|
||
### Search API | ||
|
||
The application expects a backend API endpoint at `http://127.0.0.1:5000/api/search` that accepts GET requests with a query parameter `q`. The expected response format is: | ||
|
||
```json | ||
{ | ||
"items": [ | ||
{ | ||
"title": "Result Title", | ||
"link": "https://example.com", | ||
"snippet": "Result description..." | ||
} | ||
] | ||
} | ||
``` | ||
|
||
### AI Integration | ||
|
||
The application uses Google's Gemini API for AI-powered responses. Make sure to: | ||
1. Set up a Gemini API key | ||
2. Add the key to your environment variables | ||
3. Handle rate limiting and error cases | ||
|
||
## Styling | ||
|
||
The application uses Tailwind CSS with a custom color scheme: | ||
- Dark theme: Rich purple backgrounds (#1a0033, #2a0052) | ||
- Light theme: Clean grays with purple accents | ||
- Gradient text effects for headings | ||
- Smooth transitions between themes | ||
|
||
## License | ||
|
||
This project is licensed under the MIT License - see the LICENSE file for details | ||
|
||
## env | ||
|
||
// .env.example | ||
API_KEY=your_api_key_here | ||
CSE_ID=your_api_key_here | ||
VITE_GOOGLE_API_KEY=your_key_here |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
<!doctype html> | ||
<html lang="en"> | ||
<head> | ||
<meta charset="UTF-8" /> | ||
<link rel="icon" type="image/svg+xml" href="src/assets/search.svg" /> | ||
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||
<title>ParaSearch</title> | ||
</head> | ||
<body> | ||
<div id="root"></div> | ||
<script type="module" src="/src/main.jsx"></script> | ||
</body> | ||
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
{ | ||
"name": "parasearch", | ||
"private": true, | ||
"version": "0.0.0", | ||
"type": "module", | ||
"scripts": { | ||
"dev": "concurrently \"python ./src/app.py\" \"vite\"", | ||
"build": "vite build", | ||
"lint": "eslint .", | ||
"preview": "vite preview" | ||
}, | ||
"dependencies": { | ||
"@google/generative-ai": "^0.21.0", | ||
"framer-motion": "^11.11.17", | ||
"lucide-react": "^0.460.0", | ||
"react": "^18.3.1", | ||
"react-dom": "^18.3.1" | ||
}, | ||
"devDependencies": { | ||
"@eslint/js": "^9.13.0", | ||
"@types/react": "^18.3.12", | ||
"@types/react-dom": "^18.3.1", | ||
"@vitejs/plugin-react": "^4.3.3", | ||
"autoprefixer": "^10.4.20", | ||
"concurrently": "^9.1.0", | ||
"eslint": "^9.13.0", | ||
"eslint-plugin-react": "^7.37.2", | ||
"eslint-plugin-react-hooks": "^5.0.0", | ||
"eslint-plugin-react-refresh": "^0.4.14", | ||
"globals": "^15.11.0", | ||
"postcss": "^8.4.49", | ||
"tailwindcss": "^3.4.15", | ||
"vite": "^5.4.10" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
export default { | ||
plugins: { | ||
tailwindcss: {}, | ||
autoprefixer: {}, | ||
}, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
Flask | ||
Flask-Cors | ||
requests |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,151 @@ | ||
import React, { useState, useEffect, useCallback, memo } from "react"; | ||
import SearchBar from "./components/SearchBar"; | ||
import SearchResults from "./components/SearchResults"; | ||
import SearchHistory from "./components/SearchHistory"; | ||
import ThemeToggle from "./components/ThemeToggle"; | ||
import { motion, AnimatePresence } from "framer-motion"; | ||
|
||
function App() { | ||
const [results, setResults] = useState([]); | ||
const [loading, setLoading] = useState(false); | ||
const [searchHistory, setSearchHistory] = useState([]); | ||
const [isDarkMode, setIsDarkMode] = useState(true); | ||
const [aiResponse, setAIResponse] = useState(null); | ||
|
||
useEffect(() => { | ||
const savedHistory = localStorage.getItem('searchHistory'); | ||
if (savedHistory) { | ||
try { | ||
setSearchHistory(JSON.parse(savedHistory)); | ||
} catch (error) { | ||
console.error('Error parsing search history:', error); | ||
localStorage.removeItem('searchHistory'); | ||
} | ||
} | ||
}, []); | ||
|
||
// Memoized search handler | ||
const handleSearch = useCallback(async (query) => { | ||
if (!query.trim()) return; | ||
|
||
setLoading(true); | ||
setAIResponse(null); | ||
|
||
try { | ||
const response = await fetch( | ||
`http://127.0.0.1:5000/api/search?q=${encodeURIComponent(query)}` | ||
); | ||
if (!response.ok) throw new Error('Search request failed'); | ||
|
||
const data = await response.json(); | ||
setResults(data.items || []); | ||
|
||
const newHistory = [query, ...searchHistory.filter(q => q !== query)].slice(0, 5); | ||
setSearchHistory(newHistory); | ||
localStorage.setItem('searchHistory', JSON.stringify(newHistory)); | ||
} catch (error) { | ||
console.error("Error fetching results:", error); | ||
setResults([]); | ||
} finally { | ||
setLoading(false); | ||
} | ||
}, [searchHistory]); | ||
|
||
// Memoized AI response handler | ||
const handleAIResponse = useCallback((response) => { | ||
setAIResponse(response); | ||
setResults([]); | ||
}, []); | ||
|
||
const clearHistory = useCallback(() => { | ||
setSearchHistory([]); | ||
localStorage.removeItem('searchHistory'); | ||
}, []); | ||
|
||
return ( | ||
<motion.div | ||
initial={{ opacity: 0 }} | ||
animate={{ opacity: 1 }} | ||
transition={{ duration: 0.5 }} | ||
className={`min-h-screen transition-colors duration-300 ${ | ||
isDarkMode ? 'bg-[#1a0033] text-white' : 'bg-gray-100 text-gray-900' | ||
}`} | ||
> | ||
<div className="container mx-auto px-4 py-16"> | ||
<motion.div | ||
initial={{ scale: 0.9 }} | ||
animate={{ scale: 1 }} | ||
className="absolute top-4 right-4" | ||
> | ||
<ThemeToggle isDarkMode={isDarkMode} setIsDarkMode={setIsDarkMode} /> | ||
</motion.div> | ||
|
||
<div className="flex flex-col items-center justify-center mb-16"> | ||
<motion.h1 | ||
initial={{ y: -50 }} | ||
animate={{ y: 0 }} | ||
transition={{ type: "spring", stiffness: 300 }} | ||
className={`text-6xl font-bold mb-12 ${ | ||
isDarkMode | ||
? 'bg-gradient-to-r from-purple-400 to-pink-300' | ||
: 'bg-gradient-to-r from-purple-600 to-pink-500' | ||
} bg-clip-text text-transparent`} | ||
> | ||
ParaSearch | ||
</motion.h1> | ||
|
||
<SearchBar | ||
onSearch={handleSearch} | ||
onAIResponse={handleAIResponse} | ||
isDarkMode={isDarkMode} | ||
/> | ||
|
||
<AnimatePresence> | ||
{searchHistory.length > 0 && ( | ||
<SearchHistory | ||
history={searchHistory} | ||
onSearchAgain={handleSearch} | ||
onClear={clearHistory} | ||
isDarkMode={isDarkMode} | ||
/> | ||
)} | ||
</AnimatePresence> | ||
</div> | ||
|
||
<AnimatePresence mode="wait"> | ||
{loading ? ( | ||
<motion.div | ||
key="loader" | ||
initial={{ opacity: 0 }} | ||
animate={{ opacity: 1 }} | ||
exit={{ opacity: 0 }} | ||
className="text-center" | ||
> | ||
<div className={`animate-spin rounded-full h-12 w-12 border-b-2 ${ | ||
isDarkMode ? 'border-purple-400' : 'border-purple-600' | ||
} mx-auto`}></div> | ||
</motion.div> | ||
) : ( | ||
<motion.div | ||
key="content" | ||
initial={{ opacity: 0, y: 20 }} | ||
animate={{ opacity: 1, y: 0 }} | ||
exit={{ opacity: 0, y: -20 }} | ||
transition={{ duration: 0.3 }} | ||
> | ||
{aiResponse ? ( | ||
<div className="max-w-4xl mx-auto p-6 bg-[#2a0052] rounded-xl border border-purple-500/30"> | ||
<p className="text-purple-200/80 whitespace-pre-wrap">{aiResponse}</p> | ||
</div> | ||
) : ( | ||
<SearchResults results={results} isDarkMode={isDarkMode} /> | ||
)} | ||
</motion.div> | ||
)} | ||
</AnimatePresence> | ||
</div> | ||
</motion.div> | ||
); | ||
} | ||
|
||
export default memo(App); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
from flask import Flask, request, jsonify | ||
from flask_cors import CORS | ||
import requests | ||
import os | ||
from dotenv import load_dotenv | ||
|
||
load_dotenv() | ||
|
||
app = Flask(__name__) | ||
CORS(app) | ||
|
||
API_KEY = os.getenv("API_KEY") | ||
CSE_ID = os.getenv("CSE_ID") | ||
|
||
def google_search(query, start=1, search_type=None): | ||
"""Fetch search results from Google Custom Search API.""" | ||
url = "https://www.googleapis.com/customsearch/v1" | ||
params = { | ||
"q": query, | ||
"key": API_KEY, | ||
"cx": CSE_ID, | ||
"start": start, | ||
} | ||
|
||
if search_type == 'image': | ||
params["searchType"] = "image" | ||
|
||
response = requests.get(url, params=params) | ||
if response.status_code == 200: | ||
return response.json() | ||
else: | ||
return {"error": response.text} | ||
|
||
@app.route("/api/search", methods=["GET"]) | ||
def search(): | ||
query = request.args.get("q") | ||
start = int(request.args.get("start", 1)) | ||
search_type = request.args.get("type") | ||
|
||
if not query: | ||
return jsonify({"error": "No query provided"}), 400 | ||
|
||
results = google_search(query, start=start, search_type=search_type) | ||
return jsonify(results) | ||
|
||
if __name__ == "__main__": | ||
app.run(debug=True) |
Oops, something went wrong.