Skip to content

Commit

Permalink
hmmmmmm
Browse files Browse the repository at this point in the history
  • Loading branch information
Amitminer committed Nov 17, 2024
1 parent 3ef5b1d commit cdbcb5f
Show file tree
Hide file tree
Showing 17 changed files with 684 additions and 1 deletion.
24 changes: 24 additions & 0 deletions .gitignore
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?
102 changes: 101 additions & 1 deletion README.md
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
13 changes: 13 additions & 0 deletions index.html
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>
35 changes: 35 additions & 0 deletions package.json
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"
}
}
6 changes: 6 additions & 0 deletions postcss.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
3 changes: 3 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Flask
Flask-Cors
requests
151 changes: 151 additions & 0 deletions src/App.jsx
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);
47 changes: 47 additions & 0 deletions src/app.py
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)
Loading

0 comments on commit cdbcb5f

Please sign in to comment.