diff --git a/.gitignore b/.gitignore
index f7d3395..07fa505 100644
--- a/.gitignore
+++ b/.gitignore
@@ -164,3 +164,4 @@ dataset/train/images
dataset/train/labels
app_logs.txt
.aider*
+*.tex
\ No newline at end of file
diff --git a/src/search_images.py b/src/search_images.py
index a9476b6..40c6f4c 100644
--- a/src/search_images.py
+++ b/src/search_images.py
@@ -1,4 +1,5 @@
import requests
+import json
def search_images(query, api_key, search_engine_id, num_results=10):
@@ -16,8 +17,8 @@ def search_images(query, api_key, search_engine_id, num_results=10):
response = requests.get(search_url)
if response.status_code != 200:
- raise Exception(
- f"Failed to fetch images: Status code {response.status_code}, Response: {response.text}")
+ error_message = _parse_api_error(response)
+ raise Exception(error_message)
data = response.json()
if 'items' not in data:
@@ -33,3 +34,36 @@ def search_images(query, api_key, search_engine_id, num_results=10):
break # No more results available
return images
+
+
+def _parse_api_error(response):
+ """Parse Google API error response and return a user-friendly message"""
+ try:
+ data = response.json()
+ if 'error' in data:
+ error_obj = data['error']
+
+ # Handle different error formats
+ if isinstance(error_obj, dict):
+ message = error_obj.get('message', 'Unknown error')
+ code = error_obj.get('code', response.status_code)
+ status = error_obj.get('status', 'UNKNOWN')
+
+ # Map common errors to user-friendly messages
+ if status == 'PERMISSION_DENIED' or code == 403:
+ return f"Access Denied: {message} - Please check your Google API credentials and ensure the Custom Search JSON API is enabled in your Google Cloud project."
+ elif status == 'INVALID_ARGUMENT' or code == 400:
+ return f"Invalid Request: {message} - Please verify your search query and API configuration."
+ elif status == 'UNAUTHENTICATED' or code == 401:
+ return f"Authentication Failed: {message} - Your API key may be invalid or expired."
+ elif status == 'RESOURCE_EXHAUSTED' or code == 429:
+ return f"Rate Limited: {message} - You've exceeded your daily search quota. Please try again later."
+ else:
+ return f"API Error ({code}): {message}"
+ else:
+ return f"API Error: {str(error_obj)}"
+ except:
+ pass
+
+ # Fallback error message
+ return f"Failed to fetch images: Status code {response.status_code}. The image search service returned an error. Please verify your API keys and search query."
diff --git a/src/templates/base.html b/src/templates/base.html
index 8f05a50..0c19813 100644
--- a/src/templates/base.html
+++ b/src/templates/base.html
@@ -4,6 +4,9 @@
{% block title %}SearchVision{% endblock %}
+
+
+
diff --git a/src/templates/error.html b/src/templates/error.html
index 980b0ec..3fe1451 100644
--- a/src/templates/error.html
+++ b/src/templates/error.html
@@ -3,47 +3,168 @@
{% block title %}Error - SearchVision{% endblock %}
{% block styles %}
+@keyframes fadeInDown {
+ from {
+ opacity: 0;
+ transform: translateY(-20px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
.error-container {
max-width: 600px;
margin: 0 auto;
text-align: center;
+ animation: fadeInDown 0.6s ease-out;
+ padding: 0 20px;
}
.error-icon {
- font-size: 4rem;
- margin-bottom: 20px;
+ width: 64px;
+ height: 64px;
+ margin: 0 auto 24px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 8px;
+ background: #fee2e2;
+}
+
+.error-icon svg {
+ width: 36px;
+ height: 36px;
+ stroke: #991b1b;
+ stroke-width: 2;
+ fill: none;
+}
+
+.error-container h2 {
+ font-size: 2rem;
+ font-weight: 700;
+ color: #020817;
+ margin-bottom: 12px;
+ letter-spacing: -0.5px;
}
.error-message {
- background: #fee;
- border: 1px solid #fcc;
- border-radius: 12px;
- padding: 24px;
+ background: #fee2e2;
+ border: 1px solid #fecaca;
+ border-radius: 8px;
+ padding: 20px;
margin: 24px 0;
text-align: left;
}
.error-message h3 {
- color: #c33;
- margin-bottom: 10px;
+ color: #991b1b;
+ margin-bottom: 12px;
+ font-size: 1rem;
+ font-weight: 600;
}
.error-message p {
- color: #666;
+ color: #7f1d1d;
word-break: break-word;
+ line-height: 1.6;
+ font-size: 0.95rem;
+}
+
+.error-details {
+ background: #f8fafc;
+ border: 1px solid #e2e8f0;
+ border-radius: 8px;
+ padding: 16px;
+ margin: 24px 0;
+ text-align: left;
+}
+
+.error-details-title {
+ color: #020817;
+ font-weight: 600;
+ font-size: 0.9rem;
+ margin-bottom: 8px;
+}
+
+.error-details-content {
+ color: #64748b;
+ font-size: 0.85rem;
+ font-family: 'Monaco', 'Courier New', monospace;
+ line-height: 1.4;
+ overflow-x: auto;
+}
+
+.error-actions {
+ display: flex;
+ gap: 12px;
+ margin-top: 24px;
+ justify-content: center;
+}
+
+.btn-link {
+ display: inline-block;
+ padding: 10px 24px;
+ background: #e2e8f0;
+ color: #020817;
+ border: 1px solid #cbd5e1;
+ border-radius: 6px;
+ font-size: 0.95rem;
+ font-weight: 500;
+ cursor: pointer;
+ text-decoration: none;
+ transition: all 0.2s ease;
+}
+
+.btn-link:hover {
+ background: #cbd5e1;
+ border-color: #94a3b8;
+}
+
+@media (max-width: 768px) {
+ .error-container h2 {
+ font-size: 1.5rem;
+ }
+
+ .error-actions {
+ flex-direction: column;
+ }
+
+ .btn-link {
+ width: 100%;
+ }
}
{% endblock %}
{% block content %}
-
⚠️
+
Something went wrong
-
Error Details:
+
Error Details
{{ error }}
-
Try Again
+
+
What you can do:
+
+ • Check your API keys and configuration
+ • Verify your internet connection
+ • Try a different search term
+ • Contact support if the issue persists
+
+
+
+
{% endblock %}
diff --git a/src/templates/search.html b/src/templates/search.html
index 6673877..f986913 100644
--- a/src/templates/search.html
+++ b/src/templates/search.html
@@ -3,98 +3,382 @@
{% block title %}Search - SearchVision{% endblock %}
{% block styles %}
-.search-box {
- max-width: 600px;
- margin: 0 auto;
- text-align: center;
+@keyframes fadeInDown {
+ from {
+ opacity: 0;
+ transform: translateY(-20px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
}
-.search-form {
+@keyframes fadeInUp {
+ from {
+ opacity: 0;
+ transform: translateY(20px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+@keyframes slideDown {
+ from {
+ opacity: 0;
+ transform: translateY(-10px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+.error-alert {
+ animation: slideDown 0.3s ease-out;
+ background: #fee2e2;
+ border: 1px solid #fecaca;
+ border-radius: 8px;
+ padding: 16px;
+ margin-bottom: 24px;
display: flex;
gap: 12px;
- margin-top: 30px;
+ align-items: flex-start;
}
-.search-input {
+.error-alert-icon {
+ flex-shrink: 0;
+ width: 24px;
+ height: 24px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 4px;
+ background: #fca5a5;
+}
+
+.error-alert-icon svg {
+ width: 16px;
+ height: 16px;
+ stroke: #991b1b;
+ stroke-width: 2;
+ fill: none;
+}
+
+.error-alert-content {
flex: 1;
- padding: 16px 20px;
+}
+
+.error-alert-title {
+ color: #991b1b;
+ font-weight: 600;
+ font-size: 0.95rem;
+ margin-bottom: 4px;
+}
+
+.error-alert-message {
+ color: #7f1d1d;
+ font-size: 0.9rem;
+ line-height: 1.4;
+}
+
+.error-alert-close {
+ flex-shrink: 0;
+ background: none;
+ border: none;
+ color: #991b1b;
+ cursor: pointer;
+ padding: 4px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: opacity 0.2s;
+}
+
+.error-alert-close:hover {
+ opacity: 0.7;
+}
+
+.error-alert-close svg {
+ width: 16px;
+ height: 16px;
+ stroke: #991b1b;
+ stroke-width: 2;
+}
+
+.hero-section {
+ width: 100%;
+ max-width: 900px;
+ text-align: center;
+ animation: fadeInDown 0.6s ease-out;
+ padding: 0 20px;
+}
+
+.hero-section h2 {
+ font-size: 3.5rem;
+ font-weight: 700;
+ margin-bottom: 12px;
+ color: #020817;
+ letter-spacing: -1px;
+ line-height: 1.1;
+}
+
+.hero-section > p {
font-size: 1.1rem;
- border: 2px solid #e0e0e0;
+ color: #64748b;
+ margin-bottom: 48px;
+ font-weight: 400;
+ opacity: 0.9;
+ animation: fadeInDown 0.6s ease-out 0.1s backwards;
+ line-height: 1.5;
+}
+
+.search-box {
+ max-width: 700px;
+ margin: 0 auto 64px;
+ animation: fadeInUp 0.6s ease-out 0.2s backwards;
+ padding: 0 20px;
+}
+
+.search-form {
+ display: flex;
+ gap: 8px;
+ margin-top: 0;
+ background: #ffffff;
+ padding: 4px;
border-radius: 8px;
+ border: 1px solid #e2e8f0;
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+ transition: all 0.2s ease;
+}
+
+.search-form:hover {
+ border-color: #cbd5e1;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
+}
+
+.search-form:focus-within {
+ border-color: #020817;
+ box-shadow: 0 0 0 3px rgba(2, 8, 23, 0.1), 0 1px 3px rgba(0, 0, 0, 0.1);
+}
+
+.search-input {
+ flex: 1;
+ padding: 12px 16px;
+ font-size: 1rem;
+ border: none;
+ border-radius: 6px;
outline: none;
- transition: border-color 0.3s;
+ background: transparent;
+ color: #020817;
+ transition: all 0.2s ease;
+ font-weight: 500;
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
+}
+
+.search-input::placeholder {
+ color: #94a3b8;
+ font-weight: 400;
}
.search-input:focus {
- border-color: #667eea;
+ outline: none;
}
.search-btn {
- padding: 16px 32px;
- font-size: 1.1rem;
+ padding: 12px 28px;
+ font-size: 0.95rem;
+ background: #020817;
+ color: #ffffff;
+ border: 1px solid #020817;
+ border-radius: 6px;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+ white-space: nowrap;
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
+}
+
+.search-btn:hover {
+ background: #1e293b;
+ border-color: #1e293b;
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
+}
+
+.search-btn:active {
+ transform: scale(0.98);
}
.features {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
- gap: 24px;
- margin-top: 50px;
+ gap: 20px;
+ margin-top: 0;
+ animation: fadeInUp 0.6s ease-out 0.3s backwards;
+ padding: 0 20px;
+ max-width: 900px;
+ width: 100%;
}
.feature-card {
padding: 24px;
- background: #f8f9fa;
- border-radius: 12px;
+ background: #ffffff;
+ border-radius: 8px;
text-align: center;
- transition: transform 0.2s;
+ transition: all 0.2s ease;
+ border: 1px solid #e2e8f0;
+ cursor: pointer;
}
.feature-card:hover {
- transform: translateY(-4px);
+ border-color: #cbd5e1;
+ background: #f8fafc;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
+ transform: translateY(-2px);
}
.feature-icon {
- font-size: 3rem;
- margin-bottom: 16px;
+ width: 48px;
+ height: 48px;
+ margin: 0 auto 16px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 6px;
+ background: #f1f5f9;
+ transition: all 0.2s ease;
+}
+
+.feature-icon svg {
+ width: 24px;
+ height: 24px;
+ stroke: #020817;
+ stroke-width: 2;
+ fill: none;
+}
+
+.feature-card:hover .feature-icon {
+ background: #e2e8f0;
}
.feature-card h3 {
- color: #333;
+ color: #020817;
margin-bottom: 8px;
+ font-size: 1.05rem;
+ font-weight: 600;
+ letter-spacing: -0.3px;
}
.feature-card p {
- color: #666;
- font-size: 0.95rem;
+ color: #64748b;
+ font-size: 0.9rem;
+ line-height: 1.5;
+ font-weight: 400;
+}
+
+@media (max-width: 768px) {
+ .hero-section h2 {
+ font-size: 2.2rem;
+ }
+
+ .hero-section > p {
+ font-size: 1rem;
+ }
+
+ .search-box {
+ margin-bottom: 48px;
+ }
+
+ .search-form {
+ flex-direction: column;
+ }
+
+ .search-input,
+ .search-btn {
+ width: 100%;
+ }
+
+ .search-btn {
+ padding: 12px 20px;
+ }
+
+ .features {
+ gap: 16px;
+ }
+
+ .feature-card {
+ padding: 20px;
+ }
+
+ .error-alert {
+ margin-bottom: 20px;
+ }
}
{% endblock %}
{% block content %}
-
+
+ {% if error %}
+
+
+
+
Search Failed
+
{{ error }}
+
+
+
+
+
+
+
+ {% endif %}
+
What would you like to detect?
-
Enter any object to train a custom detection model
+
Enter any object to train a custom detection model in minutes
-
-
-
-
-
-
🔍
-
Search Images
-
Find relevant images using Google Custom Search
+
+
-
-
✏️
-
Annotate
-
Draw bounding boxes on selected images
-
-
-
🤖
-
Auto-Train
-
Automatically scrape more data and train YOLOv8
+
+
+
+
+
Search Images
+
Find relevant images using Google Custom Search
+
+
+
+
Annotate
+
Draw bounding boxes on selected images
+
+
+
+
Auto-Train
+
Automatically scrape more data and train YOLOv8
+
{% endblock %}