Spaces:
Sleeping
Sleeping
Upload 46 files
Browse files- .gitattributes +38 -35
- .python-version +1 -0
- Dockerfile +20 -0
- README.md +10 -10
- __pycache__/image_routes.cpython-312.pyc +0 -0
- __pycache__/models.cpython-312.pyc +0 -0
- app.py +212 -0
- image_routes.py +335 -0
- instance/users.db +0 -0
- model/agrovision_best.keras +3 -0
- model/agrovision_final.keras +3 -0
- model/class_names.json +24 -0
- model/confusion_matrix.png +3 -0
- model/training_curves.png +0 -0
- models.py +24 -0
- package-lock.json +42 -0
- package.json +5 -0
- prepare_data.py +108 -0
- requirements.txt +16 -0
- static/images/logo.png +0 -0
- static/js/analysis.js +84 -0
- static/js/auth.js +147 -0
- static/js/configSdk.js +90 -0
- static/js/contact.js +12 -0
- static/js/darkmode.js +49 -0
- static/js/dashboard.js +46 -0
- static/js/i18n.js +138 -0
- static/js/main.js +47 -0
- static/js/menu.js +65 -0
- static/js/navigation.js +48 -0
- static/js/upload.js +64 -0
- static/styles.css +493 -0
- static/translations.js +331 -0
- templates/base.html +20 -0
- templates/index.html +10 -0
- templates/partials/dark_toggle.html +1 -0
- templates/partials/header.html +51 -0
- templates/partials/sidebar.html +10 -0
- templates/sections/analyze.html +33 -0
- templates/sections/contact.html +42 -0
- templates/sections/dashboard.html +17 -0
- templates/sections/features.html +15 -0
- templates/sections/gallery.html +15 -0
- templates/sections/home.html +32 -0
- templates/sections/login.html +13 -0
- train_model.py +253 -0
.gitattributes
CHANGED
|
@@ -1,35 +1,38 @@
|
|
| 1 |
-
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
-
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
-
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
-
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
-
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
-
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
-
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
-
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
-
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
-
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
-
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
-
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
-
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
-
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
-
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
-
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
-
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
-
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
-
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
-
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
-
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
-
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
-
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
-
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
-
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
-
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
-
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
-
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
-
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
-
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
-
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
-
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
-
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
-
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
-
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
+
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
+
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
+
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
+
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
+
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
+
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
+
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
+
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
+
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
+
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
+
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
+
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
+
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
+
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
+
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
+
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
+
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
+
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
+
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
+
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
+
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
+
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
+
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
+
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
+
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
+
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
+
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
+
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
+
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
+
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
+
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
+
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
+
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
model/agrovision_best.keras filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
model/agrovision_final.keras filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
model/confusion_matrix.png filter=lfs diff=lfs merge=lfs -text
|
.python-version
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
3.12.10
|
Dockerfile
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Use official Python image
|
| 2 |
+
FROM python:3.10-slim
|
| 3 |
+
|
| 4 |
+
# Set working directory
|
| 5 |
+
WORKDIR /code
|
| 6 |
+
|
| 7 |
+
# Copy files
|
| 8 |
+
COPY . /code
|
| 9 |
+
|
| 10 |
+
# Install requirements
|
| 11 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 12 |
+
|
| 13 |
+
# Expose port for HF
|
| 14 |
+
EXPOSE 7860
|
| 15 |
+
|
| 16 |
+
# Run Flask
|
| 17 |
+
ENV PORT=7860
|
| 18 |
+
ENV FLASK_APP=app.py
|
| 19 |
+
|
| 20 |
+
CMD ["python", "app.py"]
|
README.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
| 1 |
-
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
-
sdk: docker
|
| 7 |
-
pinned: false
|
| 8 |
-
---
|
| 9 |
-
|
| 10 |
-
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: My Flask App
|
| 3 |
+
emoji: 🚀
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: pink
|
| 6 |
+
sdk: docker
|
| 7 |
+
pinned: false
|
| 8 |
+
---
|
| 9 |
+
|
| 10 |
+
# Flask app running with Docker on Hugging Face Spaces
|
__pycache__/image_routes.cpython-312.pyc
ADDED
|
Binary file (16.6 kB). View file
|
|
|
__pycache__/models.cpython-312.pyc
ADDED
|
Binary file (2.23 kB). View file
|
|
|
app.py
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from flask import Flask, render_template, request, redirect, url_for, flash, abort, session, jsonify
|
| 2 |
+
from flask_cors import CORS
|
| 3 |
+
import os
|
| 4 |
+
from models import db, User
|
| 5 |
+
from image_routes import image_bp
|
| 6 |
+
|
| 7 |
+
app = Flask(__name__)
|
| 8 |
+
app.secret_key = os.environ.get("SECRET_KEY", "fallback-secret")
|
| 9 |
+
|
| 10 |
+
app.config['SESSION_COOKIE_SAMESITE'] = 'None'
|
| 11 |
+
app.config['SESSION_COOKIE_SECURE'] = True
|
| 12 |
+
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///users.db'
|
| 13 |
+
app.config['UPLOAD_FOLDER'] = os.path.join('static', 'uploads')
|
| 14 |
+
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024
|
| 15 |
+
|
| 16 |
+
# Enable CORS for API endpoints
|
| 17 |
+
CORS(app, supports_credentials=True)
|
| 18 |
+
|
| 19 |
+
# Initialize DB and register blueprints
|
| 20 |
+
db.init_app(app)
|
| 21 |
+
app.register_blueprint(image_bp)
|
| 22 |
+
|
| 23 |
+
# Expose request and session in all templates
|
| 24 |
+
@app.context_processor
|
| 25 |
+
def inject_request():
|
| 26 |
+
return dict(request=request, session=session)
|
| 27 |
+
|
| 28 |
+
# Serve the SPA frontend
|
| 29 |
+
@app.route('/')
|
| 30 |
+
def home_redirect():
|
| 31 |
+
return render_template('index.html')
|
| 32 |
+
|
| 33 |
+
# Dynamic page renderer
|
| 34 |
+
@app.route('/<lang>/<page>')
|
| 35 |
+
@app.route('/<lang>/', defaults={'page': 'index'})
|
| 36 |
+
def render_page(lang, page):
|
| 37 |
+
if lang not in ['az', 'en']:
|
| 38 |
+
abort(404)
|
| 39 |
+
|
| 40 |
+
template_path = f"{lang}/{page}.html"
|
| 41 |
+
if not os.path.exists(os.path.join('templates', template_path)):
|
| 42 |
+
abort(404)
|
| 43 |
+
|
| 44 |
+
return render_template(template_path, lang=lang, page=page)
|
| 45 |
+
|
| 46 |
+
MESSAGES = {
|
| 47 |
+
'missing_fields': {
|
| 48 |
+
'en': "Email and password are required.",
|
| 49 |
+
'az': "E-poçt və şifrə tələb olunur."
|
| 50 |
+
},
|
| 51 |
+
'user_exists': {
|
| 52 |
+
'en': "User already exists.",
|
| 53 |
+
'az': "İstifadəçi artıq mövcuddur."
|
| 54 |
+
},
|
| 55 |
+
'signup_success': {
|
| 56 |
+
'en': "Account created successfully! Please log in.",
|
| 57 |
+
'az': "Hesab uğurla yaradıldı! Zəhmət olmasa daxil olun."
|
| 58 |
+
},
|
| 59 |
+
'email_not_found': {
|
| 60 |
+
'en': "No account with this email exists. Create a new one.",
|
| 61 |
+
'az': "Bu e-poçt ünvanı ilə hesab tapılmadı. Yeni bir hesab yaradın."
|
| 62 |
+
},
|
| 63 |
+
'wrong_password': {
|
| 64 |
+
'en': "Incorrect password. Please try again.",
|
| 65 |
+
'az': "Şifrə yanlışdır. Zəhmət olmasa yenidən cəhd edin."
|
| 66 |
+
},
|
| 67 |
+
'login_success': {
|
| 68 |
+
'en': "Logged in successfully!",
|
| 69 |
+
'az': "Uğurla daxil oldunuz!"
|
| 70 |
+
},
|
| 71 |
+
'logged_out': {
|
| 72 |
+
'en': "Logged out.",
|
| 73 |
+
'az': "Sistemdən çıxıldı."
|
| 74 |
+
}
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
# Flash message helper
|
| 78 |
+
def flash_message(key, lang):
|
| 79 |
+
category = 'success' if key in ['signup_success', 'login_success', 'logged_out'] else 'danger'
|
| 80 |
+
flash(MESSAGES[key].get(lang, MESSAGES[key]['en']), category)
|
| 81 |
+
|
| 82 |
+
# Signup
|
| 83 |
+
@app.route('/<lang>/signup', methods=['GET', 'POST'])
|
| 84 |
+
def signup(lang):
|
| 85 |
+
if lang not in ['az', 'en']:
|
| 86 |
+
abort(404)
|
| 87 |
+
|
| 88 |
+
if request.method == 'POST':
|
| 89 |
+
email = request.form.get('email')
|
| 90 |
+
password = request.form.get('password')
|
| 91 |
+
|
| 92 |
+
if not email or not password:
|
| 93 |
+
flash_message('missing_fields', lang)
|
| 94 |
+
return redirect(request.url)
|
| 95 |
+
|
| 96 |
+
if User.query.filter_by(email=email).first():
|
| 97 |
+
flash_message('user_exists', lang)
|
| 98 |
+
return redirect(request.url)
|
| 99 |
+
|
| 100 |
+
user = User(email=email)
|
| 101 |
+
user.set_password(password)
|
| 102 |
+
db.session.add(user)
|
| 103 |
+
db.session.commit()
|
| 104 |
+
|
| 105 |
+
flash_message('signup_success', lang)
|
| 106 |
+
return redirect(url_for('login', lang=lang))
|
| 107 |
+
|
| 108 |
+
return render_template(f"{lang}/signup.html", lang=lang, page='signup')
|
| 109 |
+
|
| 110 |
+
# Login
|
| 111 |
+
@app.route('/<lang>/login', methods=['GET', 'POST'])
|
| 112 |
+
def login(lang):
|
| 113 |
+
if lang not in ['az', 'en']:
|
| 114 |
+
abort(404)
|
| 115 |
+
|
| 116 |
+
if request.method == 'POST':
|
| 117 |
+
email = request.form.get('email')
|
| 118 |
+
password = request.form.get('password')
|
| 119 |
+
|
| 120 |
+
if not email or not password:
|
| 121 |
+
flash_message('missing_fields', lang)
|
| 122 |
+
return redirect(request.url)
|
| 123 |
+
|
| 124 |
+
user = User.query.filter_by(email=email).first()
|
| 125 |
+
|
| 126 |
+
if not user:
|
| 127 |
+
flash_message('email_not_found', lang)
|
| 128 |
+
return redirect(request.url)
|
| 129 |
+
|
| 130 |
+
if not user.check_password(password):
|
| 131 |
+
flash_message('wrong_password', lang)
|
| 132 |
+
return redirect(request.url)
|
| 133 |
+
|
| 134 |
+
session['user_id'] = user.id
|
| 135 |
+
flash_message('login_success', lang)
|
| 136 |
+
return redirect(url_for('image.upload_image', lang=lang))
|
| 137 |
+
|
| 138 |
+
return render_template(f"{lang}/login.html", lang=lang, page='login')
|
| 139 |
+
|
| 140 |
+
# Logout
|
| 141 |
+
@app.route('/logout')
|
| 142 |
+
def logout():
|
| 143 |
+
session.pop('user_id', None)
|
| 144 |
+
flash_message('logged_out', session.get('lang', 'en'))
|
| 145 |
+
return redirect('/')
|
| 146 |
+
|
| 147 |
+
# API Routes for Frontend SPA
|
| 148 |
+
@app.route('/api/signup', methods=['POST'])
|
| 149 |
+
def api_signup():
|
| 150 |
+
data = request.get_json()
|
| 151 |
+
email = data.get('email')
|
| 152 |
+
password = data.get('password')
|
| 153 |
+
lang = data.get('lang') or 'en'
|
| 154 |
+
|
| 155 |
+
if not email or not password:
|
| 156 |
+
return jsonify({'success': False, 'message': MESSAGES['missing_fields'].get(lang, MESSAGES['missing_fields']['en'])}), 400
|
| 157 |
+
|
| 158 |
+
if User.query.filter_by(email=email).first():
|
| 159 |
+
return jsonify({'success': False, 'message': MESSAGES['user_exists'].get(lang, MESSAGES['user_exists']['en'])}), 400
|
| 160 |
+
|
| 161 |
+
user = User(email=email)
|
| 162 |
+
user.set_password(password)
|
| 163 |
+
db.session.add(user)
|
| 164 |
+
db.session.commit()
|
| 165 |
+
|
| 166 |
+
return jsonify({'success': True, 'message': MESSAGES['signup_success'].get(lang, MESSAGES['signup_success']['en'])}), 201
|
| 167 |
+
|
| 168 |
+
@app.route('/api/login', methods=['POST'])
|
| 169 |
+
def api_login():
|
| 170 |
+
data = request.get_json()
|
| 171 |
+
email = data.get('email')
|
| 172 |
+
password = data.get('password')
|
| 173 |
+
lang = data.get('lang') or 'en'
|
| 174 |
+
|
| 175 |
+
if not email or not password:
|
| 176 |
+
return jsonify({'success': False, 'message': MESSAGES['missing_fields'].get(lang, MESSAGES['missing_fields']['en'])}), 400
|
| 177 |
+
|
| 178 |
+
user = User.query.filter_by(email=email).first()
|
| 179 |
+
|
| 180 |
+
if not user:
|
| 181 |
+
return jsonify({'success': False, 'message': MESSAGES['email_not_found'].get(lang, MESSAGES['email_not_found']['en'])}), 401
|
| 182 |
+
|
| 183 |
+
if not user.check_password(password):
|
| 184 |
+
return jsonify({'success': False, 'message': MESSAGES['wrong_password'].get(lang, MESSAGES['wrong_password']['en'])}), 401
|
| 185 |
+
|
| 186 |
+
session['user_id'] = user.id
|
| 187 |
+
return jsonify({'success': True, 'message': MESSAGES['login_success'].get(lang, MESSAGES['login_success']['en']), 'user_id': user.id}), 200
|
| 188 |
+
|
| 189 |
+
@app.route('/api/logout', methods=['POST'])
|
| 190 |
+
def api_logout():
|
| 191 |
+
session.pop('user_id', None)
|
| 192 |
+
return jsonify({'success': True, 'message': 'Logged out successfully.'}), 200
|
| 193 |
+
|
| 194 |
+
@app.route('/api/check-auth', methods=['GET'])
|
| 195 |
+
def api_check_auth():
|
| 196 |
+
user_id = session.get('user_id')
|
| 197 |
+
if user_id:
|
| 198 |
+
user = User.query.get(user_id)
|
| 199 |
+
if user:
|
| 200 |
+
return jsonify({'authenticated': True, 'email': user.email}), 200
|
| 201 |
+
return jsonify({'authenticated': False}), 200
|
| 202 |
+
|
| 203 |
+
# Run the app
|
| 204 |
+
if __name__ == '__main__':
|
| 205 |
+
os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
|
| 206 |
+
with app.app_context():
|
| 207 |
+
db.create_all()
|
| 208 |
+
|
| 209 |
+
port = int(os.environ.get("PORT", 7860))
|
| 210 |
+
app.run(host="0.0.0.0", port=port, debug=False)
|
| 211 |
+
|
| 212 |
+
|
image_routes.py
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import os
|
| 3 |
+
import uuid
|
| 4 |
+
from PIL import Image as PILImage
|
| 5 |
+
from flask import Blueprint, request, render_template, redirect, url_for, session, abort, flash, jsonify
|
| 6 |
+
from werkzeug.utils import secure_filename
|
| 7 |
+
from models import db, Image, User
|
| 8 |
+
|
| 9 |
+
# Optional ML dependencies
|
| 10 |
+
try:
|
| 11 |
+
import numpy as np
|
| 12 |
+
except Exception:
|
| 13 |
+
np = None
|
| 14 |
+
try:
|
| 15 |
+
import tensorflow as tf
|
| 16 |
+
except Exception:
|
| 17 |
+
tf = None
|
| 18 |
+
|
| 19 |
+
image_bp = Blueprint('image', __name__)
|
| 20 |
+
UPLOAD_FOLDER = os.path.join('static', 'uploads')
|
| 21 |
+
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'}
|
| 22 |
+
MODEL_LOCATIONS = ['model', 'models']
|
| 23 |
+
MODEL_CANDIDATES = ['agrovision_final.keras', 'agrovision_best.keras', 'agrovision.h5']
|
| 24 |
+
UNKNOWN_LABEL = "Unknown"
|
| 25 |
+
UNKNOWN_CONFIDENCE_THRESHOLD = 0.5
|
| 26 |
+
CLASS_NAMES_FALLBACK = [
|
| 27 |
+
"Corn_Blight",
|
| 28 |
+
"Corn_Common_Rust",
|
| 29 |
+
"Corn_Gray_Leaf_Spot",
|
| 30 |
+
"Corn_Healthy",
|
| 31 |
+
"Cucumber_Downy_mildew",
|
| 32 |
+
"Cucumber_Healthy_leaves",
|
| 33 |
+
"Cucumber_Powdery_mildew",
|
| 34 |
+
"Potato___Early_blight",
|
| 35 |
+
"Potato___Late_blight",
|
| 36 |
+
"Potato___healthy",
|
| 37 |
+
"Tomato_Bacterial_spot",
|
| 38 |
+
"Tomato_Early_blight",
|
| 39 |
+
"Tomato_Late_blight",
|
| 40 |
+
"Tomato_Leaf_Mold",
|
| 41 |
+
"Tomato_Septoria_leaf_spot",
|
| 42 |
+
"Tomato_Spider_mites_Two_spotted_spider_mite",
|
| 43 |
+
"Tomato__Target_Spot",
|
| 44 |
+
"Tomato__Tomato_YellowLeaf__Curl_Virus",
|
| 45 |
+
"Tomato__Tomato_mosaic_virus",
|
| 46 |
+
"Tomato_healthy",
|
| 47 |
+
"Wheat_Healthy",
|
| 48 |
+
"Wheat_diseased",
|
| 49 |
+
]
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def _load_model():
|
| 53 |
+
if tf is None:
|
| 54 |
+
print("TensorFlow not available. Image prediction will be disabled.")
|
| 55 |
+
return None, None
|
| 56 |
+
|
| 57 |
+
for directory in MODEL_LOCATIONS:
|
| 58 |
+
for candidate in MODEL_CANDIDATES:
|
| 59 |
+
candidate_path = os.path.join(directory, candidate)
|
| 60 |
+
if os.path.exists(candidate_path):
|
| 61 |
+
try:
|
| 62 |
+
loaded_model = tf.keras.models.load_model(candidate_path)
|
| 63 |
+
print(f"Model loaded successfully from {candidate_path}")
|
| 64 |
+
return loaded_model, candidate_path
|
| 65 |
+
except Exception as exc:
|
| 66 |
+
print(f"Error loading model at {candidate_path}: {exc}. Trying next candidate.")
|
| 67 |
+
print("Warning: No trained model file found. Image prediction will be disabled.")
|
| 68 |
+
return None, None
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def _load_class_names(model_path):
|
| 72 |
+
candidate_paths = []
|
| 73 |
+
if model_path:
|
| 74 |
+
candidate_paths.append(os.path.join(os.path.dirname(model_path), 'class_names.json'))
|
| 75 |
+
for directory in MODEL_LOCATIONS:
|
| 76 |
+
candidate_paths.append(os.path.join(directory, 'class_names.json'))
|
| 77 |
+
|
| 78 |
+
for path in candidate_paths:
|
| 79 |
+
if os.path.exists(path):
|
| 80 |
+
try:
|
| 81 |
+
with open(path, 'r', encoding='utf-8') as fh:
|
| 82 |
+
names = json.load(fh)
|
| 83 |
+
if isinstance(names, list) and names:
|
| 84 |
+
print(f"Loaded {len(names)} class names from {path}")
|
| 85 |
+
return names
|
| 86 |
+
print(f"{path} is empty or malformed. Trying next candidate.")
|
| 87 |
+
except Exception as exc:
|
| 88 |
+
print(f"Failed to load class names from {path}: {exc}. Trying next candidate.")
|
| 89 |
+
return CLASS_NAMES_FALLBACK
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
model, MODEL_PATH = _load_model()
|
| 93 |
+
CLASS_NAMES = _load_class_names(MODEL_PATH)
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def run_prediction(image_path):
|
| 97 |
+
if model is None:
|
| 98 |
+
raise RuntimeError("Model not loaded.")
|
| 99 |
+
if np is None:
|
| 100 |
+
raise RuntimeError("NumPy not available.")
|
| 101 |
+
|
| 102 |
+
img = PILImage.open(image_path).convert('RGB')
|
| 103 |
+
img = img.resize((224, 224))
|
| 104 |
+
img_array = np.array(img, dtype=np.float32)
|
| 105 |
+
img_array = np.expand_dims(img_array, axis=0)
|
| 106 |
+
|
| 107 |
+
prediction = model.predict(img_array, verbose=0)
|
| 108 |
+
if prediction.ndim == 1:
|
| 109 |
+
prediction = np.expand_dims(prediction, axis=0)
|
| 110 |
+
|
| 111 |
+
num_outputs = prediction.shape[1] if prediction.ndim > 1 else 1
|
| 112 |
+
|
| 113 |
+
if num_outputs == 1:
|
| 114 |
+
confidence = float(prediction[0][0])
|
| 115 |
+
predicted_idx = int(confidence > 0.5)
|
| 116 |
+
label_options = CLASS_NAMES if len(CLASS_NAMES) >= 2 else ["Class_0", "Class_1"]
|
| 117 |
+
predicted_label = label_options[predicted_idx]
|
| 118 |
+
confidence = confidence if predicted_idx == 1 else 1.0 - confidence
|
| 119 |
+
else:
|
| 120 |
+
predicted_idx = int(np.argmax(prediction[0]))
|
| 121 |
+
confidence = float(prediction[0][predicted_idx])
|
| 122 |
+
if len(CLASS_NAMES) != num_outputs:
|
| 123 |
+
print(
|
| 124 |
+
f"Warning: CLASS_NAMES length ({len(CLASS_NAMES)}) does not match model outputs ({num_outputs})."
|
| 125 |
+
)
|
| 126 |
+
label_options = CLASS_NAMES if len(CLASS_NAMES) == num_outputs else [f"Class_{i}" for i in range(num_outputs)]
|
| 127 |
+
predicted_label = label_options[predicted_idx]
|
| 128 |
+
|
| 129 |
+
final_label = predicted_label if confidence >= UNKNOWN_CONFIDENCE_THRESHOLD else UNKNOWN_LABEL
|
| 130 |
+
return final_label, confidence
|
| 131 |
+
|
| 132 |
+
def allowed_file(filename):
|
| 133 |
+
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
|
| 134 |
+
|
| 135 |
+
@image_bp.route('/<lang>/upload', methods=['GET', 'POST'])
|
| 136 |
+
def upload_image(lang):
|
| 137 |
+
if lang not in ['az', 'en']:
|
| 138 |
+
abort(404)
|
| 139 |
+
|
| 140 |
+
user_id = session.get('user_id')
|
| 141 |
+
if not user_id:
|
| 142 |
+
flash("You must be logged in to upload.")
|
| 143 |
+
return redirect(url_for('login', lang=lang))
|
| 144 |
+
|
| 145 |
+
messages = {
|
| 146 |
+
'az': {
|
| 147 |
+
'no_image': 'Şəkil seçilməyib.',
|
| 148 |
+
'no_file': 'Fayl seçilməyib.',
|
| 149 |
+
'wrong_format': 'Yalnız png, jpg, jpeg və gif formatları qəbul olunur.',
|
| 150 |
+
'success': 'Şəkil uğurla yükləndi.'
|
| 151 |
+
},
|
| 152 |
+
'en': {
|
| 153 |
+
'no_image': 'No image selected.',
|
| 154 |
+
'no_file': 'No file selected.',
|
| 155 |
+
'wrong_format': 'Only png, jpg, jpeg and gif files are allowed.',
|
| 156 |
+
'success': 'Image successfully uploaded.'
|
| 157 |
+
}
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
msg = messages[lang]
|
| 161 |
+
|
| 162 |
+
if request.method == 'POST':
|
| 163 |
+
if 'image' not in request.files:
|
| 164 |
+
flash(msg['no_image'])
|
| 165 |
+
return redirect(request.url)
|
| 166 |
+
|
| 167 |
+
file = request.files['image']
|
| 168 |
+
if file.filename == '':
|
| 169 |
+
flash(msg['no_file'])
|
| 170 |
+
return redirect(request.url)
|
| 171 |
+
|
| 172 |
+
if file and allowed_file(file.filename):
|
| 173 |
+
original_filename = secure_filename(file.filename)
|
| 174 |
+
unique_name = f"{uuid.uuid4().hex}_{original_filename}"
|
| 175 |
+
upload_path = os.path.join(UPLOAD_FOLDER, unique_name)
|
| 176 |
+
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
|
| 177 |
+
file.save(upload_path)
|
| 178 |
+
|
| 179 |
+
# Save record in DB
|
| 180 |
+
new_image = Image(filename=unique_name, user_id=user_id)
|
| 181 |
+
db.session.add(new_image)
|
| 182 |
+
db.session.commit()
|
| 183 |
+
|
| 184 |
+
try:
|
| 185 |
+
predicted_label, confidence = run_prediction(upload_path)
|
| 186 |
+
except Exception as e:
|
| 187 |
+
flash(f"Error during prediction: {str(e)}", "danger")
|
| 188 |
+
return redirect(request.url)
|
| 189 |
+
|
| 190 |
+
flash(msg['success'])
|
| 191 |
+
return render_template(
|
| 192 |
+
f"{lang}/upload.html",
|
| 193 |
+
filename=unique_name,
|
| 194 |
+
lang=lang,
|
| 195 |
+
prediction=predicted_label,
|
| 196 |
+
confidence=round(confidence * 100, 2),
|
| 197 |
+
)
|
| 198 |
+
|
| 199 |
+
flash(msg['wrong_format'])
|
| 200 |
+
return redirect(request.url)
|
| 201 |
+
|
| 202 |
+
return render_template(f"{lang}/upload.html", filename=None, lang=lang)
|
| 203 |
+
|
| 204 |
+
@image_bp.route('/<lang>/my_images')
|
| 205 |
+
def my_images(lang):
|
| 206 |
+
if lang not in ['az', 'en']:
|
| 207 |
+
abort(404)
|
| 208 |
+
|
| 209 |
+
user_id = session.get('user_id')
|
| 210 |
+
if not user_id:
|
| 211 |
+
flash("Please log in to see your images.")
|
| 212 |
+
return redirect(url_for('login', lang=lang))
|
| 213 |
+
|
| 214 |
+
user = User.query.get(user_id)
|
| 215 |
+
return render_template(f"{lang}/my_images.html", images=user.images, lang=lang)
|
| 216 |
+
|
| 217 |
+
@image_bp.route('/<lang>/delete/<filename>', methods=['POST'])
|
| 218 |
+
def delete_image(lang, filename):
|
| 219 |
+
if lang not in ['az', 'en']:
|
| 220 |
+
abort(404)
|
| 221 |
+
|
| 222 |
+
user_id = session.get('user_id')
|
| 223 |
+
if not user_id:
|
| 224 |
+
flash("Please log in.")
|
| 225 |
+
return redirect(url_for('login', lang=lang))
|
| 226 |
+
|
| 227 |
+
image = Image.query.filter_by(filename=filename, user_id=user_id).first()
|
| 228 |
+
if not image:
|
| 229 |
+
abort(403)
|
| 230 |
+
|
| 231 |
+
file_path = os.path.join(UPLOAD_FOLDER, filename)
|
| 232 |
+
if os.path.exists(file_path):
|
| 233 |
+
os.remove(file_path)
|
| 234 |
+
|
| 235 |
+
db.session.delete(image)
|
| 236 |
+
db.session.commit()
|
| 237 |
+
|
| 238 |
+
flash("Image deleted successfully.")
|
| 239 |
+
return redirect(url_for('image.my_images', lang=lang))
|
| 240 |
+
|
| 241 |
+
@image_bp.route('/display/<filename>')
|
| 242 |
+
def display_image(filename):
|
| 243 |
+
user_id = session.get('user_id')
|
| 244 |
+
if not user_id:
|
| 245 |
+
flash("Please log in.")
|
| 246 |
+
return redirect(url_for('login', lang='en'))
|
| 247 |
+
|
| 248 |
+
image = Image.query.filter_by(filename=filename, user_id=user_id).first()
|
| 249 |
+
if not image:
|
| 250 |
+
abort(403)
|
| 251 |
+
|
| 252 |
+
return redirect(url_for('static', filename='uploads/' + filename), code=301)
|
| 253 |
+
|
| 254 |
+
# API Routes for Frontend SPA
|
| 255 |
+
@image_bp.route('/api/upload', methods=['POST'])
|
| 256 |
+
def api_upload_image():
|
| 257 |
+
user_id = session.get('user_id')
|
| 258 |
+
if not user_id:
|
| 259 |
+
return jsonify({'success': False, 'message': 'You must be logged in to upload.'}), 401
|
| 260 |
+
|
| 261 |
+
if 'image' not in request.files:
|
| 262 |
+
return jsonify({'success': False, 'message': 'No image selected.'}), 400
|
| 263 |
+
|
| 264 |
+
file = request.files['image']
|
| 265 |
+
if file.filename == '':
|
| 266 |
+
return jsonify({'success': False, 'message': 'No file selected.'}), 400
|
| 267 |
+
|
| 268 |
+
if file and allowed_file(file.filename):
|
| 269 |
+
original_filename = secure_filename(file.filename)
|
| 270 |
+
unique_name = f"{uuid.uuid4().hex}_{original_filename}"
|
| 271 |
+
upload_path = os.path.join(UPLOAD_FOLDER, unique_name)
|
| 272 |
+
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
|
| 273 |
+
file.save(upload_path)
|
| 274 |
+
|
| 275 |
+
# Predict using the model
|
| 276 |
+
try:
|
| 277 |
+
predicted_label, confidence = run_prediction(upload_path)
|
| 278 |
+
except Exception as e:
|
| 279 |
+
print(f"Error during prediction: {str(e)}")
|
| 280 |
+
predicted_label = f"Error: {str(e)}"
|
| 281 |
+
confidence = 0.0
|
| 282 |
+
|
| 283 |
+
# Save record in DB
|
| 284 |
+
new_image = Image(filename=unique_name, user_id=user_id, prediction=predicted_label)
|
| 285 |
+
db.session.add(new_image)
|
| 286 |
+
db.session.commit()
|
| 287 |
+
|
| 288 |
+
return jsonify({
|
| 289 |
+
'success': True,
|
| 290 |
+
'message': 'Image successfully uploaded.',
|
| 291 |
+
'filename': unique_name,
|
| 292 |
+
'prediction': predicted_label,
|
| 293 |
+
'confidence': round(confidence * 100, 2),
|
| 294 |
+
'image_url': f'/static/uploads/{unique_name}'
|
| 295 |
+
}), 200
|
| 296 |
+
|
| 297 |
+
return jsonify({'success': False, 'message': 'Only png, jpg, jpeg and gif files are allowed.'}), 400
|
| 298 |
+
|
| 299 |
+
@image_bp.route('/api/my-images', methods=['GET'])
|
| 300 |
+
def api_my_images():
|
| 301 |
+
user_id = session.get('user_id')
|
| 302 |
+
if not user_id:
|
| 303 |
+
return jsonify({'success': False, 'message': 'Please log in to see your images.'}), 401
|
| 304 |
+
|
| 305 |
+
user = User.query.get(user_id)
|
| 306 |
+
images = []
|
| 307 |
+
for img in user.images:
|
| 308 |
+
images.append({
|
| 309 |
+
'id': img.id,
|
| 310 |
+
'filename': img.filename,
|
| 311 |
+
'prediction': img.prediction if hasattr(img, 'prediction') else 'Unknown',
|
| 312 |
+
'image_url': f'/static/uploads/{img.filename}',
|
| 313 |
+
'created_at': img.created_at.isoformat() if hasattr(img, 'created_at') else None
|
| 314 |
+
})
|
| 315 |
+
|
| 316 |
+
return jsonify({'success': True, 'images': images}), 200
|
| 317 |
+
|
| 318 |
+
@image_bp.route('/api/delete/<filename>', methods=['DELETE'])
|
| 319 |
+
def api_delete_image(filename):
|
| 320 |
+
user_id = session.get('user_id')
|
| 321 |
+
if not user_id:
|
| 322 |
+
return jsonify({'success': False, 'message': 'Please log in.'}), 401
|
| 323 |
+
|
| 324 |
+
image = Image.query.filter_by(filename=filename, user_id=user_id).first()
|
| 325 |
+
if not image:
|
| 326 |
+
return jsonify({'success': False, 'message': 'Image not found.'}), 404
|
| 327 |
+
|
| 328 |
+
file_path = os.path.join(UPLOAD_FOLDER, filename)
|
| 329 |
+
if os.path.exists(file_path):
|
| 330 |
+
os.remove(file_path)
|
| 331 |
+
|
| 332 |
+
db.session.delete(image)
|
| 333 |
+
db.session.commit()
|
| 334 |
+
|
| 335 |
+
return jsonify({'success': True, 'message': 'Image deleted successfully.'}), 200
|
instance/users.db
ADDED
|
Binary file (16.4 kB). View file
|
|
|
model/agrovision_best.keras
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:f0d8023fed6aa250f15b304c9338e487691d84769dc569a25cf5b9143aaa86f2
|
| 3 |
+
size 9974231
|
model/agrovision_final.keras
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:fd315812d9010505dbdcb955eea89e52735f5c0dde2f338b72bdcff6e9a9fe99
|
| 3 |
+
size 9974231
|
model/class_names.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
"Corn_Blight",
|
| 3 |
+
"Corn_Common_Rust",
|
| 4 |
+
"Corn_Gray_Leaf_Spot",
|
| 5 |
+
"Corn_Healthy",
|
| 6 |
+
"Cucumber_Downy_mildew",
|
| 7 |
+
"Cucumber_Healthy_leaves",
|
| 8 |
+
"Cucumber_Powdery_mildew",
|
| 9 |
+
"Potato___Early_blight",
|
| 10 |
+
"Potato___Late_blight",
|
| 11 |
+
"Potato___healthy",
|
| 12 |
+
"Tomato_Bacterial_spot",
|
| 13 |
+
"Tomato_Early_blight",
|
| 14 |
+
"Tomato_Late_blight",
|
| 15 |
+
"Tomato_Leaf_Mold",
|
| 16 |
+
"Tomato_Septoria_leaf_spot",
|
| 17 |
+
"Tomato_Spider_mites_Two_spotted_spider_mite",
|
| 18 |
+
"Tomato__Target_Spot",
|
| 19 |
+
"Tomato__Tomato_YellowLeaf__Curl_Virus",
|
| 20 |
+
"Tomato__Tomato_mosaic_virus",
|
| 21 |
+
"Tomato_healthy",
|
| 22 |
+
"Wheat_Healthy",
|
| 23 |
+
"Wheat_diseased"
|
| 24 |
+
]
|
model/confusion_matrix.png
ADDED
|
Git LFS Details
|
model/training_curves.png
ADDED
|
models.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from flask_sqlalchemy import SQLAlchemy
|
| 2 |
+
from werkzeug.security import generate_password_hash, check_password_hash
|
| 3 |
+
from datetime import datetime
|
| 4 |
+
|
| 5 |
+
db = SQLAlchemy()
|
| 6 |
+
|
| 7 |
+
class User(db.Model):
|
| 8 |
+
id = db.Column(db.Integer, primary_key=True)
|
| 9 |
+
email = db.Column(db.String(150), unique=True, nullable=False)
|
| 10 |
+
password_hash = db.Column(db.String(200), nullable=False)
|
| 11 |
+
images = db.relationship('Image', backref='user', lazy=True)
|
| 12 |
+
|
| 13 |
+
def set_password(self, password):
|
| 14 |
+
self.password_hash = generate_password_hash(password)
|
| 15 |
+
|
| 16 |
+
def check_password(self, password):
|
| 17 |
+
return check_password_hash(self.password_hash, password)
|
| 18 |
+
|
| 19 |
+
class Image(db.Model):
|
| 20 |
+
id = db.Column(db.Integer, primary_key=True)
|
| 21 |
+
filename = db.Column(db.String(255), nullable=False)
|
| 22 |
+
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
| 23 |
+
prediction = db.Column(db.String(255), nullable=True)
|
| 24 |
+
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
package-lock.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "agrovision",
|
| 3 |
+
"lockfileVersion": 3,
|
| 4 |
+
"requires": true,
|
| 5 |
+
"packages": {
|
| 6 |
+
"": {
|
| 7 |
+
"dependencies": {
|
| 8 |
+
"bootstrap": "^5.3.7"
|
| 9 |
+
}
|
| 10 |
+
},
|
| 11 |
+
"node_modules/@popperjs/core": {
|
| 12 |
+
"version": "2.11.8",
|
| 13 |
+
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
|
| 14 |
+
"integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
|
| 15 |
+
"license": "MIT",
|
| 16 |
+
"peer": true,
|
| 17 |
+
"funding": {
|
| 18 |
+
"type": "opencollective",
|
| 19 |
+
"url": "https://opencollective.com/popperjs"
|
| 20 |
+
}
|
| 21 |
+
},
|
| 22 |
+
"node_modules/bootstrap": {
|
| 23 |
+
"version": "5.3.7",
|
| 24 |
+
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.7.tgz",
|
| 25 |
+
"integrity": "sha512-7KgiD8UHjfcPBHEpDNg+zGz8L3LqR3GVwqZiBRFX04a1BCArZOz1r2kjly2HQ0WokqTO0v1nF+QAt8dsW4lKlw==",
|
| 26 |
+
"funding": [
|
| 27 |
+
{
|
| 28 |
+
"type": "github",
|
| 29 |
+
"url": "https://github.com/sponsors/twbs"
|
| 30 |
+
},
|
| 31 |
+
{
|
| 32 |
+
"type": "opencollective",
|
| 33 |
+
"url": "https://opencollective.com/bootstrap"
|
| 34 |
+
}
|
| 35 |
+
],
|
| 36 |
+
"license": "MIT",
|
| 37 |
+
"peerDependencies": {
|
| 38 |
+
"@popperjs/core": "^2.11.8"
|
| 39 |
+
}
|
| 40 |
+
}
|
| 41 |
+
}
|
| 42 |
+
}
|
package.json
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"dependencies": {
|
| 3 |
+
"bootstrap": "^5.3.7"
|
| 4 |
+
}
|
| 5 |
+
}
|
prepare_data.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import random
|
| 3 |
+
import shutil
|
| 4 |
+
|
| 5 |
+
# Base paths
|
| 6 |
+
BASE_DIR = "data"
|
| 7 |
+
SOURCE_DIR = os.path.join(BASE_DIR, "images")
|
| 8 |
+
TRAIN_DIR = os.path.join(BASE_DIR, "train")
|
| 9 |
+
VAL_DIR = os.path.join(BASE_DIR, "val")
|
| 10 |
+
TEST_DIR = os.path.join(BASE_DIR, "test")
|
| 11 |
+
|
| 12 |
+
# Supported image extensions
|
| 13 |
+
IMAGE_EXTS = (".jpg", ".jpeg", ".png", ".bmp", ".webp", ".tif", ".tiff")
|
| 14 |
+
|
| 15 |
+
# Train/Val/Test split ratios
|
| 16 |
+
SPLIT_RATIOS = {
|
| 17 |
+
"train": 0.7,
|
| 18 |
+
"val": 0.15,
|
| 19 |
+
"test": 0.15,
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
random.seed(42) # For reproducibility
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def ensure_dir(path: str):
|
| 26 |
+
os.makedirs(path, exist_ok=True)
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def is_image(fname: str) -> bool:
|
| 30 |
+
return fname.lower().endswith(IMAGE_EXTS)
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def check_empty(path: str):
|
| 34 |
+
"""Ensure the directory is empty before writing."""
|
| 35 |
+
if os.path.exists(path) and os.listdir(path):
|
| 36 |
+
raise RuntimeError(f"{path} is not empty. Clear it before running.")
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def main():
|
| 40 |
+
print(f"Source directory: {os.path.abspath(SOURCE_DIR)}")
|
| 41 |
+
|
| 42 |
+
if not os.path.isdir(SOURCE_DIR):
|
| 43 |
+
raise RuntimeError(f"Source directory not found: {SOURCE_DIR}")
|
| 44 |
+
|
| 45 |
+
# Prepare train/val/test dirs
|
| 46 |
+
for split_dir in [TRAIN_DIR, VAL_DIR, TEST_DIR]:
|
| 47 |
+
ensure_dir(split_dir)
|
| 48 |
+
check_empty(split_dir)
|
| 49 |
+
|
| 50 |
+
# Get all class folders
|
| 51 |
+
class_names = sorted(
|
| 52 |
+
d for d in os.listdir(SOURCE_DIR)
|
| 53 |
+
if os.path.isdir(os.path.join(SOURCE_DIR, d))
|
| 54 |
+
)
|
| 55 |
+
|
| 56 |
+
print("\nClasses detected:")
|
| 57 |
+
for cname in class_names:
|
| 58 |
+
print(" -", cname)
|
| 59 |
+
|
| 60 |
+
total_src = 0
|
| 61 |
+
total_split = {"train": 0, "val": 0, "test": 0}
|
| 62 |
+
|
| 63 |
+
# Process every class folder
|
| 64 |
+
for class_name in class_names:
|
| 65 |
+
src_dir = os.path.join(SOURCE_DIR, class_name)
|
| 66 |
+
print(f"\nProcessing: {class_name}")
|
| 67 |
+
|
| 68 |
+
files = [f for f in os.listdir(src_dir) if is_image(f)]
|
| 69 |
+
files.sort()
|
| 70 |
+
|
| 71 |
+
if not files:
|
| 72 |
+
print(" -> No images found, skipping.")
|
| 73 |
+
continue
|
| 74 |
+
|
| 75 |
+
random.shuffle(files)
|
| 76 |
+
n = len(files)
|
| 77 |
+
total_src += n
|
| 78 |
+
|
| 79 |
+
n_train = int(n * SPLIT_RATIOS["train"])
|
| 80 |
+
n_val = int(n * SPLIT_RATIOS["val"])
|
| 81 |
+
n_test = n - n_train - n_val # Remaining goes to test
|
| 82 |
+
|
| 83 |
+
splits = {
|
| 84 |
+
"train": files[:n_train],
|
| 85 |
+
"val": files[n_train:n_train + n_val],
|
| 86 |
+
"test": files[n_train + n_val:],
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
for split_name, split_files in splits.items():
|
| 90 |
+
dst_dir = os.path.join(BASE_DIR, split_name, class_name)
|
| 91 |
+
ensure_dir(dst_dir)
|
| 92 |
+
|
| 93 |
+
print(f" {split_name}: {len(split_files)} images")
|
| 94 |
+
total_split[split_name] += len(split_files)
|
| 95 |
+
|
| 96 |
+
for fname in split_files:
|
| 97 |
+
shutil.copy2(os.path.join(src_dir, fname),
|
| 98 |
+
os.path.join(dst_dir, fname))
|
| 99 |
+
|
| 100 |
+
print(f"Total images: {total_src}")
|
| 101 |
+
print(f"Train: {total_split['train']}")
|
| 102 |
+
print(f"Val: {total_split['val']}")
|
| 103 |
+
print(f"Test: {total_split['test']}")
|
| 104 |
+
print("Dataset split completed.")
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
if __name__ == "__main__":
|
| 108 |
+
main()
|
requirements.txt
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
blinker==1.9.0
|
| 2 |
+
click==8.2.1
|
| 3 |
+
colorama==0.4.6
|
| 4 |
+
Flask==3.1.1
|
| 5 |
+
Flask-CORS==4.0.0
|
| 6 |
+
Flask-SQLAlchemy==3.1.1
|
| 7 |
+
greenlet==3.2.3
|
| 8 |
+
itsdangerous==2.2.0
|
| 9 |
+
Jinja2==3.1.6
|
| 10 |
+
MarkupSafe==3.0.2
|
| 11 |
+
numpy==1.26.4
|
| 12 |
+
Pillow==10.0.0
|
| 13 |
+
SQLAlchemy==2.0.41
|
| 14 |
+
tensorflow==2.17.0
|
| 15 |
+
typing_extensions==4.14.0
|
| 16 |
+
Werkzeug==3.1.3
|
static/images/logo.png
ADDED
|
static/js/analysis.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { getCurrentLanguage, translate } from './i18n.js';
|
| 2 |
+
import { getCurrentImageFile } from './upload.js';
|
| 3 |
+
|
| 4 |
+
async function analyzeImage() {
|
| 5 |
+
const currentImageFile = getCurrentImageFile();
|
| 6 |
+
if (!currentImageFile) {
|
| 7 |
+
alert(getCurrentLanguage() === 'az' ? 'Zəhmət olmasa əvvəlcə şəkil seçin' : 'Please select an image first');
|
| 8 |
+
return;
|
| 9 |
+
}
|
| 10 |
+
const analyzeBtn = document.querySelector('button[onclick="analyzeImage()"]');
|
| 11 |
+
const originalText = analyzeBtn.textContent;
|
| 12 |
+
analyzeBtn.disabled = true;
|
| 13 |
+
analyzeBtn.textContent = getCurrentLanguage() === 'az' ? 'Təhlil edilir...' : 'Analyzing...';
|
| 14 |
+
const formData = new FormData();
|
| 15 |
+
formData.append('image', currentImageFile);
|
| 16 |
+
try {
|
| 17 |
+
const response = await fetch('/api/upload', { method: 'POST', credentials: 'include', body: formData });
|
| 18 |
+
const result = await response.json();
|
| 19 |
+
analyzeBtn.disabled = false;
|
| 20 |
+
analyzeBtn.textContent = originalText;
|
| 21 |
+
if (result.success) {
|
| 22 |
+
displayResults(result);
|
| 23 |
+
} else {
|
| 24 |
+
alert('Error: ' + result.message);
|
| 25 |
+
}
|
| 26 |
+
} catch (error) {
|
| 27 |
+
analyzeBtn.disabled = false;
|
| 28 |
+
analyzeBtn.textContent = originalText;
|
| 29 |
+
alert('Error uploading image: ' + error.message);
|
| 30 |
+
}
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
function generateQRCode(data) {
|
| 34 |
+
const qrContainer = document.getElementById('qrcode');
|
| 35 |
+
if (!qrContainer) return;
|
| 36 |
+
qrContainer.innerHTML = '';
|
| 37 |
+
const qrData = JSON.stringify({ prediction: data.prediction || 'Unknown', confidence: data.confidence || 0, timestamp: new Date().toISOString(), url: window.location.href });
|
| 38 |
+
if (typeof QRCode === 'undefined') {
|
| 39 |
+
qrContainer.innerHTML = '<p class="text-red-500 text-sm">QR Code library not loaded</p>';
|
| 40 |
+
return;
|
| 41 |
+
}
|
| 42 |
+
QRCode.toCanvas(qrContainer, qrData, { width: 200, margin: 2, color: { dark: '#000000', light: '#FFFFFF' } }, function (error) {
|
| 43 |
+
if (error) {
|
| 44 |
+
qrContainer.innerHTML = '<p class="text-red-500 text-sm">QR Code generation failed</p>';
|
| 45 |
+
}
|
| 46 |
+
});
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
function displayResults(result) {
|
| 50 |
+
const resultsSection = document.getElementById('results-section');
|
| 51 |
+
if (!resultsSection) return;
|
| 52 |
+
const prediction = result.prediction || 'Unknown';
|
| 53 |
+
const confidence = result.confidence || 0;
|
| 54 |
+
generateQRCode(result);
|
| 55 |
+
const titleElement = resultsSection.querySelector('h3');
|
| 56 |
+
if (titleElement) {
|
| 57 |
+
titleElement.textContent = translate('analyze.analysisComplete');
|
| 58 |
+
}
|
| 59 |
+
const confidenceBadge = document.getElementById('confidence-badge');
|
| 60 |
+
if (confidenceBadge) {
|
| 61 |
+
confidenceBadge.textContent = `${confidence}% ${translate('common.confidence')}`;
|
| 62 |
+
}
|
| 63 |
+
const isHealthy = prediction.toLowerCase().includes('healthy');
|
| 64 |
+
const healthStatus = isHealthy ? 'Excellent' : 'Needs Attention';
|
| 65 |
+
const healthColor = isHealthy ? 'text-green-600' : 'text-orange-600';
|
| 66 |
+
const healthStatusElements = resultsSection.querySelectorAll('.text-3xl.font-bold');
|
| 67 |
+
if (healthStatusElements.length > 0) {
|
| 68 |
+
healthStatusElements[0].textContent = healthStatus;
|
| 69 |
+
healthStatusElements[0].className = `text-3xl font-bold ${healthColor}`;
|
| 70 |
+
}
|
| 71 |
+
const diseaseRiskElements = resultsSection.querySelectorAll('.text-3xl.font-bold');
|
| 72 |
+
if (diseaseRiskElements.length > 1) {
|
| 73 |
+
diseaseRiskElements[1].textContent = isHealthy ? 'Low' : 'High';
|
| 74 |
+
diseaseRiskElements[1].className = `text-3xl font-bold ${isHealthy ? 'text-green-600' : 'text-red-600'}`;
|
| 75 |
+
}
|
| 76 |
+
const recommendations = resultsSection.querySelector('ul');
|
| 77 |
+
if (recommendations) {
|
| 78 |
+
recommendations.innerHTML = `<li class="flex items-start"><span class="text-green-500 mr-2">✓</span> <span>Prediction: ${prediction}</span></li><li class="flex items-start"><span class="text-green-500 mr-2">✓</span> <span>Confidence: ${confidence}%</span></li><li class="flex items-start"><span class="text-green-500 mr-2">✓</span> <span>${isHealthy ? 'Plant appears healthy. Continue monitoring.' : 'Disease detected. Please consult with agricultural expert for treatment recommendations.'}</span></li>`;
|
| 79 |
+
}
|
| 80 |
+
resultsSection.classList.remove('hidden');
|
| 81 |
+
resultsSection.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
export { analyzeImage, displayResults, generateQRCode };
|
static/js/auth.js
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { updateTranslations, getCurrentLanguage } from './i18n.js';
|
| 2 |
+
|
| 3 |
+
async function apiCall(url, options = {}) {
|
| 4 |
+
try {
|
| 5 |
+
const response = await fetch(url, {
|
| 6 |
+
...options,
|
| 7 |
+
credentials: 'include',
|
| 8 |
+
headers: {
|
| 9 |
+
'Content-Type': 'application/json',
|
| 10 |
+
...options.headers
|
| 11 |
+
}
|
| 12 |
+
});
|
| 13 |
+
const data = await response.json();
|
| 14 |
+
return { success: response.ok, data, status: response.status };
|
| 15 |
+
} catch (error) {
|
| 16 |
+
return { success: false, data: { message: error.message }, status: 500 };
|
| 17 |
+
}
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
async function checkAuth() {
|
| 21 |
+
const result = await apiCall('/api/check-auth');
|
| 22 |
+
if (result.success && result.data.authenticated) {
|
| 23 |
+
updateUIForLoggedIn(result.data.email);
|
| 24 |
+
return true;
|
| 25 |
+
}
|
| 26 |
+
updateUIForLoggedOut();
|
| 27 |
+
return false;
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
function toggleAccountMenu() {
|
| 31 |
+
const menu = document.getElementById('account-menu');
|
| 32 |
+
if (menu) {
|
| 33 |
+
menu.classList.toggle('hidden');
|
| 34 |
+
}
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
function updateUIForLoggedIn(email) {
|
| 38 |
+
const signInBtn = document.getElementById('sign-in-btn');
|
| 39 |
+
const accountMenu = document.getElementById('account-menu');
|
| 40 |
+
if (signInBtn) {
|
| 41 |
+
signInBtn.textContent = email || 'Dashboard';
|
| 42 |
+
signInBtn.onclick = (event) => {
|
| 43 |
+
event.preventDefault && event.preventDefault();
|
| 44 |
+
toggleAccountMenu();
|
| 45 |
+
};
|
| 46 |
+
}
|
| 47 |
+
if (accountMenu) {
|
| 48 |
+
accountMenu.classList.remove('hidden');
|
| 49 |
+
accountMenu.classList.add('hidden');
|
| 50 |
+
}
|
| 51 |
+
updateTranslations();
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
function updateUIForLoggedOut() {
|
| 55 |
+
const signInBtn = document.getElementById('sign-in-btn');
|
| 56 |
+
const accountMenu = document.getElementById('account-menu');
|
| 57 |
+
if (signInBtn) {
|
| 58 |
+
signInBtn.textContent = 'Sign In';
|
| 59 |
+
signInBtn.onclick = (event) => navigateTo('login', event);
|
| 60 |
+
}
|
| 61 |
+
if (accountMenu) {
|
| 62 |
+
accountMenu.classList.add('hidden');
|
| 63 |
+
}
|
| 64 |
+
updateTranslations();
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
async function handleLogout() {
|
| 68 |
+
const result = await apiCall('/api/logout', { method: 'POST' });
|
| 69 |
+
if (result.success) {
|
| 70 |
+
updateUIForLoggedOut();
|
| 71 |
+
navigateTo('home', { preventDefault: () => {} });
|
| 72 |
+
} else {
|
| 73 |
+
alert(result.data?.message || (getCurrentLanguage() === 'az' ? 'Sistemdən çıxışda xəta.' : 'Error logging out.'));
|
| 74 |
+
}
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
async function handleLogin(event) {
|
| 78 |
+
event.preventDefault();
|
| 79 |
+
const email = document.getElementById('login-email').value;
|
| 80 |
+
const password = document.getElementById('login-password').value;
|
| 81 |
+
const successDiv = document.getElementById('auth-success');
|
| 82 |
+
successDiv.classList.remove('hidden');
|
| 83 |
+
successDiv.textContent = getCurrentLanguage() === 'az' ? 'Yüklənir...' : 'Loading...';
|
| 84 |
+
successDiv.classList.remove('bg-red-100', 'text-red-700');
|
| 85 |
+
successDiv.classList.add('bg-blue-100', 'text-blue-700');
|
| 86 |
+
const result = await apiCall('/api/login', { method: 'POST', body: JSON.stringify({ email, password, lang: getCurrentLanguage() }) });
|
| 87 |
+
if (result.success) {
|
| 88 |
+
successDiv.textContent = '✓ ' + result.data.message;
|
| 89 |
+
successDiv.classList.remove('bg-blue-100', 'text-blue-700', 'bg-red-100', 'text-red-700');
|
| 90 |
+
successDiv.classList.add('bg-green-100', 'text-green-700');
|
| 91 |
+
updateUIForLoggedIn(email);
|
| 92 |
+
setTimeout(() => {
|
| 93 |
+
navigateTo('dashboard', event);
|
| 94 |
+
successDiv.classList.add('hidden');
|
| 95 |
+
document.getElementById('login-form').reset();
|
| 96 |
+
}, 1500);
|
| 97 |
+
} else {
|
| 98 |
+
successDiv.textContent = '✗ ' + result.data.message;
|
| 99 |
+
successDiv.classList.remove('bg-blue-100', 'text-blue-700', 'bg-green-100', 'text-green-700');
|
| 100 |
+
successDiv.classList.add('bg-red-100', 'text-red-700');
|
| 101 |
+
setTimeout(() => {
|
| 102 |
+
successDiv.classList.add('hidden');
|
| 103 |
+
}, 3000);
|
| 104 |
+
}
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
async function handleSignup(event) {
|
| 108 |
+
event.preventDefault();
|
| 109 |
+
const email = document.getElementById('signup-email').value;
|
| 110 |
+
const password = document.getElementById('signup-password').value;
|
| 111 |
+
const confirm = document.getElementById('signup-confirm').value;
|
| 112 |
+
const successDiv = document.getElementById('auth-success');
|
| 113 |
+
if (password !== confirm) {
|
| 114 |
+
successDiv.textContent = getCurrentLanguage() === 'az' ? '✗ Şifrələr uyğun deyil!' : '✗ Passwords do not match!';
|
| 115 |
+
successDiv.classList.remove('bg-green-100', 'text-green-700');
|
| 116 |
+
successDiv.classList.add('bg-red-100', 'text-red-700');
|
| 117 |
+
successDiv.classList.remove('hidden');
|
| 118 |
+
setTimeout(() => {
|
| 119 |
+
successDiv.classList.add('hidden');
|
| 120 |
+
}, 2500);
|
| 121 |
+
return;
|
| 122 |
+
}
|
| 123 |
+
successDiv.classList.remove('hidden');
|
| 124 |
+
successDiv.textContent = getCurrentLanguage() === 'az' ? 'Yüklənir...' : 'Loading...';
|
| 125 |
+
successDiv.classList.remove('bg-red-100', 'text-red-700');
|
| 126 |
+
successDiv.classList.add('bg-blue-100', 'text-blue-700');
|
| 127 |
+
const result = await apiCall('/api/signup', { method: 'POST', body: JSON.stringify({ email, password, lang: getCurrentLanguage() }) });
|
| 128 |
+
if (result.success) {
|
| 129 |
+
successDiv.textContent = '✓ ' + result.data.message;
|
| 130 |
+
successDiv.classList.remove('bg-blue-100', 'text-blue-700', 'bg-red-100', 'text-red-700');
|
| 131 |
+
successDiv.classList.add('bg-green-100', 'text-green-700');
|
| 132 |
+
setTimeout(() => {
|
| 133 |
+
switchAuthTab('login');
|
| 134 |
+
successDiv.classList.add('hidden');
|
| 135 |
+
document.getElementById('signup-form').reset();
|
| 136 |
+
}, 2000);
|
| 137 |
+
} else {
|
| 138 |
+
successDiv.textContent = '✗ ' + result.data.message;
|
| 139 |
+
successDiv.classList.remove('bg-blue-100', 'text-blue-700', 'bg-green-100', 'text-green-700');
|
| 140 |
+
successDiv.classList.add('bg-red-100', 'text-red-700');
|
| 141 |
+
setTimeout(() => {
|
| 142 |
+
successDiv.classList.add('hidden');
|
| 143 |
+
}, 3000);
|
| 144 |
+
}
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
export { apiCall, checkAuth, updateUIForLoggedIn, updateUIForLoggedOut, handleLogout, handleLogin, handleSignup, toggleAccountMenu };
|
static/js/configSdk.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const defaultConfig = {
|
| 2 |
+
site_title: "Agrovision",
|
| 3 |
+
tagline: "Future of Farming",
|
| 4 |
+
hero_description: "Revolutionize your agricultural practices with cutting-edge AI technology. Monitor, analyze, and optimize your crops like never before.",
|
| 5 |
+
cta_button: "Start Free Trial",
|
| 6 |
+
contact_email: "hello@agrovision.io",
|
| 7 |
+
contact_phone: "+1 (888) 555-FARM",
|
| 8 |
+
background_color: "#f9fafb",
|
| 9 |
+
surface_color: "#ffffff",
|
| 10 |
+
text_color: "#111827",
|
| 11 |
+
primary_action_color: "#22c55e",
|
| 12 |
+
secondary_action_color: "#f97316",
|
| 13 |
+
font_family: "Poppins",
|
| 14 |
+
font_size: 16
|
| 15 |
+
};
|
| 16 |
+
|
| 17 |
+
async function onConfigChange(config) {
|
| 18 |
+
const customFont = config.font_family || defaultConfig.font_family;
|
| 19 |
+
const baseSize = config.font_size || defaultConfig.font_size;
|
| 20 |
+
const baseFontStack = '-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif';
|
| 21 |
+
document.body.style.fontFamily = `${customFont}, ${baseFontStack}`;
|
| 22 |
+
document.body.style.backgroundColor = config.background_color || defaultConfig.background_color;
|
| 23 |
+
const navTitle = document.getElementById('nav-title');
|
| 24 |
+
if (navTitle) navTitle.textContent = config.site_title || defaultConfig.site_title;
|
| 25 |
+
const heroTagline = document.getElementById('hero-tagline');
|
| 26 |
+
if (heroTagline) {
|
| 27 |
+
const taglineText = config.tagline || defaultConfig.tagline;
|
| 28 |
+
heroTagline.innerHTML = `${taglineText.split(' ')[0]} <span class="gradient-text">${taglineText.split(' ').slice(1).join(' ')}</span>`;
|
| 29 |
+
heroTagline.style.fontSize = `${baseSize * 3}px`;
|
| 30 |
+
heroTagline.style.fontFamily = `${customFont}, ${baseFontStack}`;
|
| 31 |
+
}
|
| 32 |
+
const heroDescription = document.getElementById('hero-description');
|
| 33 |
+
if (heroDescription) {
|
| 34 |
+
heroDescription.textContent = config.hero_description || defaultConfig.hero_description;
|
| 35 |
+
heroDescription.style.fontSize = `${baseSize * 1.25}px`;
|
| 36 |
+
}
|
| 37 |
+
const ctaButton = document.getElementById('cta-button');
|
| 38 |
+
if (ctaButton) {
|
| 39 |
+
ctaButton.textContent = config.cta_button || defaultConfig.cta_button;
|
| 40 |
+
ctaButton.style.fontSize = `${baseSize * 1.125}px`;
|
| 41 |
+
}
|
| 42 |
+
const contactEmailDisplay = document.getElementById('contact-email-display');
|
| 43 |
+
if (contactEmailDisplay) contactEmailDisplay.textContent = config.contact_email || defaultConfig.contact_email;
|
| 44 |
+
const contactPhoneDisplay = document.getElementById('contact-phone-display');
|
| 45 |
+
if (contactPhoneDisplay) contactPhoneDisplay.textContent = config.contact_phone || defaultConfig.contact_phone;
|
| 46 |
+
const textColor = config.text_color || defaultConfig.text_color;
|
| 47 |
+
const surfaceColor = config.surface_color || defaultConfig.surface_color;
|
| 48 |
+
const bgColor = config.background_color || defaultConfig.background_color;
|
| 49 |
+
document.querySelectorAll('.bg-gray-50').forEach(el => { el.style.backgroundColor = bgColor; });
|
| 50 |
+
document.querySelectorAll('.bg-white').forEach(el => { el.style.backgroundColor = surfaceColor; });
|
| 51 |
+
document.querySelectorAll('.text-gray-900, .text-gray-800, .text-gray-700').forEach(el => { el.style.color = textColor; });
|
| 52 |
+
document.querySelectorAll('h1, h2, h3').forEach(el => {
|
| 53 |
+
el.style.fontFamily = `${customFont}, ${baseFontStack}`;
|
| 54 |
+
if (el.tagName === 'H1') el.style.fontSize = `${baseSize * 2.5}px`;
|
| 55 |
+
if (el.tagName === 'H2') el.style.fontSize = `${baseSize * 2}px`;
|
| 56 |
+
if (el.tagName === 'H3') el.style.fontSize = `${baseSize * 1.5}px`;
|
| 57 |
+
});
|
| 58 |
+
document.querySelectorAll('p, label, button, a, input, textarea').forEach(el => { el.style.fontSize = `${baseSize}px`; });
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
function initElementSdk() {
|
| 62 |
+
if (window.elementSdk) {
|
| 63 |
+
window.elementSdk.init({
|
| 64 |
+
defaultConfig,
|
| 65 |
+
onConfigChange,
|
| 66 |
+
mapToCapabilities: (config) => ({
|
| 67 |
+
recolorables: [
|
| 68 |
+
{ get: () => config.background_color || defaultConfig.background_color, set: (value) => { config.background_color = value; window.elementSdk.setConfig({ background_color: value }); } },
|
| 69 |
+
{ get: () => config.surface_color || defaultConfig.surface_color, set: (value) => { config.surface_color = value; window.elementSdk.setConfig({ surface_color: value }); } },
|
| 70 |
+
{ get: () => config.text_color || defaultConfig.text_color, set: (value) => { config.text_color = value; window.elementSdk.setConfig({ text_color: value }); } },
|
| 71 |
+
{ get: () => config.primary_action_color || defaultConfig.primary_action_color, set: (value) => { config.primary_action_color = value; window.elementSdk.setConfig({ primary_action_color: value }); } },
|
| 72 |
+
{ get: () => config.secondary_action_color || defaultConfig.secondary_action_color, set: (value) => { config.secondary_action_color = value; window.elementSdk.setConfig({ secondary_action_color: value }); } }
|
| 73 |
+
],
|
| 74 |
+
borderables: [],
|
| 75 |
+
fontEditable: { get: () => config.font_family || defaultConfig.font_family, set: (value) => { config.font_family = value; window.elementSdk.setConfig({ font_family: value }); } },
|
| 76 |
+
fontSizeable: { get: () => config.font_size || defaultConfig.font_size, set: (value) => { config.font_size = value; window.elementSdk.setConfig({ font_size: value }); } }
|
| 77 |
+
}),
|
| 78 |
+
mapToEditPanelValues: (config) => new Map([
|
| 79 |
+
["site_title", config.site_title || defaultConfig.site_title],
|
| 80 |
+
["tagline", config.tagline || defaultConfig.tagline],
|
| 81 |
+
["hero_description", config.hero_description || defaultConfig.hero_description],
|
| 82 |
+
["cta_button", config.cta_button || defaultConfig.cta_button],
|
| 83 |
+
["contact_email", config.contact_email || defaultConfig.contact_email],
|
| 84 |
+
["contact_phone", config.contact_phone || defaultConfig.contact_phone]
|
| 85 |
+
])
|
| 86 |
+
});
|
| 87 |
+
}
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
export { defaultConfig, onConfigChange, initElementSdk };
|
static/js/contact.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
function handleContactSubmit(event) {
|
| 2 |
+
event.preventDefault();
|
| 3 |
+
const successDiv = document.getElementById('contact-success');
|
| 4 |
+
successDiv.textContent = document.querySelector('[data-i18n="contact.messageSent"]').textContent || '✓ Message sent successfully! We\'ll respond within 24 hours.';
|
| 5 |
+
successDiv.classList.remove('hidden');
|
| 6 |
+
setTimeout(() => {
|
| 7 |
+
successDiv.classList.add('hidden');
|
| 8 |
+
document.getElementById('contact-form').reset();
|
| 9 |
+
}, 4000);
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
export { handleContactSubmit };
|
static/js/darkmode.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
function toggleDarkMode() {
|
| 2 |
+
document.body.classList.toggle('dark-mode');
|
| 3 |
+
const icon = document.getElementById('dark-mode-icon');
|
| 4 |
+
const isDark = document.body.classList.contains('dark-mode');
|
| 5 |
+
if (icon) {
|
| 6 |
+
icon.textContent = isDark ? '☀️' : '🌙';
|
| 7 |
+
}
|
| 8 |
+
updateDarkModeGradients(isDark);
|
| 9 |
+
localStorage.setItem('darkMode', isDark ? 'enabled' : 'disabled');
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
function updateDarkModeGradients(isDark) {
|
| 13 |
+
const homeCards = document.querySelectorAll('#home-page .card-hover.bg-gradient-to-br');
|
| 14 |
+
homeCards.forEach(card => {
|
| 15 |
+
if (isDark) {
|
| 16 |
+
if (card.classList.contains('from-green-50') && card.classList.contains('to-green-100')) {
|
| 17 |
+
card.style.background = 'linear-gradient(to bottom right, rgba(6, 78, 59, 0.4), rgba(5, 150, 105, 0.3))';
|
| 18 |
+
} else if (card.classList.contains('from-orange-50') && card.classList.contains('to-orange-100')) {
|
| 19 |
+
card.style.background = 'linear-gradient(to bottom right, rgba(124, 45, 18, 0.4), rgba(154, 52, 18, 0.3))';
|
| 20 |
+
} else if (card.classList.contains('from-green-50') && card.classList.contains('to-orange-100')) {
|
| 21 |
+
card.style.background = 'linear-gradient(to bottom right, rgba(6, 78, 59, 0.4), rgba(154, 52, 18, 0.3))';
|
| 22 |
+
}
|
| 23 |
+
} else {
|
| 24 |
+
card.style.background = '';
|
| 25 |
+
}
|
| 26 |
+
});
|
| 27 |
+
const dashboardCards = document.querySelectorAll('#dashboard-page .card-hover.bg-gradient-to-br');
|
| 28 |
+
dashboardCards.forEach(card => {
|
| 29 |
+
if (isDark) {
|
| 30 |
+
if (card.classList.contains('from-red-50') && card.classList.contains('to-orange-50')) {
|
| 31 |
+
card.style.background = 'linear-gradient(to bottom right, rgba(127, 29, 29, 0.4), rgba(124, 45, 18, 0.3))';
|
| 32 |
+
}
|
| 33 |
+
} else {
|
| 34 |
+
card.style.background = '';
|
| 35 |
+
}
|
| 36 |
+
});
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
function loadDarkModePreference() {
|
| 40 |
+
const darkMode = localStorage.getItem('darkMode');
|
| 41 |
+
if (darkMode === 'enabled') {
|
| 42 |
+
document.body.classList.add('dark-mode');
|
| 43 |
+
const icon = document.getElementById('dark-mode-icon');
|
| 44 |
+
if (icon) icon.textContent = '☀️';
|
| 45 |
+
setTimeout(() => updateDarkModeGradients(true), 100);
|
| 46 |
+
}
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
export { toggleDarkMode, updateDarkModeGradients, loadDarkModePreference };
|
static/js/dashboard.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { apiCall } from './auth.js';
|
| 2 |
+
import { updateDarkModeGradients } from './darkmode.js';
|
| 3 |
+
|
| 4 |
+
async function loadDashboard() {
|
| 5 |
+
const authResult = await apiCall('/api/check-auth');
|
| 6 |
+
if (!authResult.success || !authResult.data.authenticated) {
|
| 7 |
+
return;
|
| 8 |
+
}
|
| 9 |
+
const imagesResult = await apiCall('/api/my-images');
|
| 10 |
+
if (imagesResult.success && imagesResult.data.images) {
|
| 11 |
+
updateDashboardWithImages(imagesResult.data.images);
|
| 12 |
+
}
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
function updateDashboardWithImages(images) {
|
| 16 |
+
const dashboardContainer = document.querySelector('#dashboard-page .space-y-6');
|
| 17 |
+
if (!dashboardContainer) return;
|
| 18 |
+
const existingItems = dashboardContainer.querySelectorAll('.card-hover');
|
| 19 |
+
existingItems.forEach(item => item.remove());
|
| 20 |
+
images.forEach((img, index) => {
|
| 21 |
+
if (index >= 3) return;
|
| 22 |
+
const isHealthy = img.prediction && img.prediction.toLowerCase().includes('healthy');
|
| 23 |
+
const bgColor = isHealthy ? 'from-green-50 to-green-100' : 'from-red-50 to-orange-50';
|
| 24 |
+
const borderColor = isHealthy ? 'border-green-200' : 'border-red-200';
|
| 25 |
+
const statusBadge = isHealthy ? '<span class="bg-green-500 text-white px-4 py-2 rounded-full text-sm font-bold">✅ Healthy</span>' : '<span class="bg-red-500 text-white px-4 py-2 rounded-full text-sm font-bold">⚠️ Disease Detected</span>';
|
| 26 |
+
const itemHTML = `<div class="card-hover bg-gradient-to-br ${bgColor} rounded-2xl p-6 border-2 ${borderColor}"><div class="grid md:grid-cols-12 gap-6"><div class="md:col-span-3"><img src="${img.image_url}" alt="Uploaded image" class="w-full h-48 object-cover rounded-xl"></div><div class="md:col-span-9"><div class="flex items-start justify-between mb-4"><div><h3 class="text-2xl font-bold text-gray-900 mb-2">Image Analysis</h3><p class="text-gray-600">Scanned on ${new Date(img.created_at || Date.now()).toLocaleString()}</p></div>${statusBadge}</div><div class="bg-white rounded-xl p-4 shadow"><div class="text-sm text-gray-600 mb-1">Prediction</div><div class="text-xl font-bold text-gray-900">${img.prediction || 'Unknown'}</div></div></div></div></div>`;
|
| 27 |
+
dashboardContainer.insertAdjacentHTML('beforeend', itemHTML);
|
| 28 |
+
});
|
| 29 |
+
const totalScans = images.length;
|
| 30 |
+
const healthyCount = images.filter(img => img.prediction && img.prediction.toLowerCase().includes('healthy')).length;
|
| 31 |
+
const issuesCount = totalScans - healthyCount;
|
| 32 |
+
const avgHealth = totalScans > 0 ? Math.round((healthyCount / totalScans) * 100) : 0;
|
| 33 |
+
const statsElements = document.querySelectorAll('#dashboard-page .text-4xl.font-bold.gradient-text, #dashboard-page .text-4xl.font-bold.text-green-600, #dashboard-page .text-4xl.font-bold.text-orange-600');
|
| 34 |
+
if (statsElements.length >= 4) {
|
| 35 |
+
statsElements[0].textContent = totalScans;
|
| 36 |
+
statsElements[1].textContent = healthyCount;
|
| 37 |
+
statsElements[2].textContent = issuesCount;
|
| 38 |
+
statsElements[3].textContent = avgHealth + '%';
|
| 39 |
+
}
|
| 40 |
+
const isDark = document.body.classList.contains('dark-mode');
|
| 41 |
+
if (isDark) {
|
| 42 |
+
updateDarkModeGradients(true);
|
| 43 |
+
}
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
export { loadDashboard, updateDashboardWithImages };
|
static/js/i18n.js
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
let currentLanguage = localStorage.getItem('language') || 'en';
|
| 2 |
+
|
| 3 |
+
function translate(key) {
|
| 4 |
+
const keys = key.split('.');
|
| 5 |
+
let value = translations[currentLanguage];
|
| 6 |
+
for (const k of keys) {
|
| 7 |
+
value = value?.[k];
|
| 8 |
+
}
|
| 9 |
+
return value || key;
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
function updateTranslations() {
|
| 13 |
+
document.querySelectorAll('[data-i18n]').forEach(el => {
|
| 14 |
+
const key = el.getAttribute('data-i18n');
|
| 15 |
+
const text = translate(key);
|
| 16 |
+
if (text) {
|
| 17 |
+
if (el.tagName === 'INPUT' && el.type === 'submit') {
|
| 18 |
+
el.value = text;
|
| 19 |
+
} else if (el.innerHTML && el.innerHTML.includes('<span')) {
|
| 20 |
+
const parts = text.split(' ');
|
| 21 |
+
if (parts.length > 1) {
|
| 22 |
+
el.innerHTML = parts[0] + ' <span class="gradient-text">' + parts.slice(1).join(' ') + '</span>';
|
| 23 |
+
} else {
|
| 24 |
+
el.textContent = text;
|
| 25 |
+
}
|
| 26 |
+
} else {
|
| 27 |
+
el.textContent = text;
|
| 28 |
+
}
|
| 29 |
+
}
|
| 30 |
+
});
|
| 31 |
+
// Handle placeholder translations
|
| 32 |
+
document.querySelectorAll('[data-i18n-placeholder]').forEach(el => {
|
| 33 |
+
const key = el.getAttribute('data-i18n-placeholder');
|
| 34 |
+
const text = translate(key);
|
| 35 |
+
if (text) {
|
| 36 |
+
el.placeholder = text;
|
| 37 |
+
}
|
| 38 |
+
});
|
| 39 |
+
const heroTagline = document.getElementById('hero-tagline');
|
| 40 |
+
if (heroTagline) {
|
| 41 |
+
const heroText = translate('home.heroTitle');
|
| 42 |
+
const parts = heroText.split(' ');
|
| 43 |
+
if (parts.length > 1) {
|
| 44 |
+
heroTagline.innerHTML = parts[0] + ' <span class="gradient-text">' + parts.slice(1).join(' ') + '</span>';
|
| 45 |
+
} else {
|
| 46 |
+
heroTagline.textContent = heroText;
|
| 47 |
+
}
|
| 48 |
+
}
|
| 49 |
+
const featureTitle = document.querySelector('#features-page h1');
|
| 50 |
+
if (featureTitle) {
|
| 51 |
+
const text = translate('features.title');
|
| 52 |
+
const words = text.split(' ');
|
| 53 |
+
if (words.length > 1) {
|
| 54 |
+
featureTitle.innerHTML = words[0] + ' <span class="gradient-text">' + words.slice(1).join(' ') + '</span>';
|
| 55 |
+
} else {
|
| 56 |
+
featureTitle.innerHTML = '<span class="gradient-text">' + text + '</span>';
|
| 57 |
+
}
|
| 58 |
+
}
|
| 59 |
+
const dashboardTitle = document.querySelector('#dashboard-page h1');
|
| 60 |
+
if (dashboardTitle) {
|
| 61 |
+
const text = translate('dashboard.title');
|
| 62 |
+
const words = text.split(' ');
|
| 63 |
+
if (words.length > 1) {
|
| 64 |
+
dashboardTitle.innerHTML = words[0] + ' <span class="gradient-text">' + words.slice(1).join(' ') + '</span>';
|
| 65 |
+
} else {
|
| 66 |
+
dashboardTitle.innerHTML = '<span class="gradient-text">' + text + '</span>';
|
| 67 |
+
}
|
| 68 |
+
}
|
| 69 |
+
const galleryTitle = document.querySelector('#gallery-page h1');
|
| 70 |
+
if (galleryTitle) {
|
| 71 |
+
const text = translate('gallery.title');
|
| 72 |
+
const words = text.split(' ');
|
| 73 |
+
if (words.length > 1) {
|
| 74 |
+
galleryTitle.innerHTML = words[0] + ' <span class="gradient-text">' + words.slice(1).join(' ') + '</span>';
|
| 75 |
+
} else {
|
| 76 |
+
galleryTitle.innerHTML = '<span class="gradient-text">' + text + '</span>';
|
| 77 |
+
}
|
| 78 |
+
}
|
| 79 |
+
const contactTitle = document.querySelector('#contact-page h1');
|
| 80 |
+
if (contactTitle) {
|
| 81 |
+
const text = translate('contact.title');
|
| 82 |
+
const words = text.split(' ');
|
| 83 |
+
if (words.length >= 3) {
|
| 84 |
+
contactTitle.innerHTML = words.slice(0, 2).join(' ') + ' <span class="gradient-text">' + words.slice(2).join(' ') + '</span>';
|
| 85 |
+
} else if (words.length === 2) {
|
| 86 |
+
contactTitle.innerHTML = words[0] + ' <span class="gradient-text">' + words[1] + '</span>';
|
| 87 |
+
} else {
|
| 88 |
+
contactTitle.innerHTML = '<span class="gradient-text">' + text + '</span>';
|
| 89 |
+
}
|
| 90 |
+
}
|
| 91 |
+
const homeFeaturesTitle = document.querySelector('#home-page .text-4xl.font-bold');
|
| 92 |
+
if (homeFeaturesTitle) {
|
| 93 |
+
const text = translate('homeFeatures.title');
|
| 94 |
+
homeFeaturesTitle.textContent = text;
|
| 95 |
+
}
|
| 96 |
+
const langSwitcher = document.getElementById('current-lang');
|
| 97 |
+
if (langSwitcher) {
|
| 98 |
+
langSwitcher.textContent = currentLanguage.toUpperCase();
|
| 99 |
+
}
|
| 100 |
+
const mobileLangSwitcher = document.getElementById('mobile-current-lang');
|
| 101 |
+
if (mobileLangSwitcher) {
|
| 102 |
+
mobileLangSwitcher.textContent = currentLanguage.toUpperCase();
|
| 103 |
+
}
|
| 104 |
+
const signInBtn = document.getElementById('sign-in-btn');
|
| 105 |
+
if (signInBtn && !signInBtn.textContent.includes('@')) {
|
| 106 |
+
signInBtn.textContent = translate('common.signIn');
|
| 107 |
+
}
|
| 108 |
+
document.querySelectorAll('.sidebar-dot').forEach(dot => {
|
| 109 |
+
const attr = currentLanguage === 'az' ? 'data-title-az' : 'data-title-en';
|
| 110 |
+
const title = dot.getAttribute(attr);
|
| 111 |
+
if (title) {
|
| 112 |
+
dot.setAttribute('title', title);
|
| 113 |
+
}
|
| 114 |
+
});
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
function switchLanguage(lang) {
|
| 118 |
+
currentLanguage = lang;
|
| 119 |
+
localStorage.setItem('language', lang);
|
| 120 |
+
updateTranslations();
|
| 121 |
+
const mobileLangMenu = document.getElementById('mobile-lang-menu');
|
| 122 |
+
if (mobileLangMenu) {
|
| 123 |
+
mobileLangMenu.classList.add('hidden');
|
| 124 |
+
}
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
function toggleLanguageMenu() {
|
| 128 |
+
const menu = document.getElementById('lang-menu');
|
| 129 |
+
if (menu) {
|
| 130 |
+
menu.classList.toggle('hidden');
|
| 131 |
+
}
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
function getCurrentLanguage() {
|
| 135 |
+
return currentLanguage;
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
export { translate, updateTranslations, switchLanguage, toggleLanguageMenu, getCurrentLanguage };
|
static/js/main.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { translate, updateTranslations, switchLanguage, toggleLanguageMenu, getCurrentLanguage } from './i18n.js';
|
| 2 |
+
import { toggleMobileMenu, closeMobileMenu, toggleMobileLanguageMenu, initMenuEvents } from './menu.js';
|
| 3 |
+
import { navigateTo, switchAuthTab } from './navigation.js';
|
| 4 |
+
import { apiCall, checkAuth, updateUIForLoggedIn, updateUIForLoggedOut, handleLogout, handleLogin, handleSignup, toggleAccountMenu } from './auth.js';
|
| 5 |
+
import { initUpload, displayImage, resetUpload, getCurrentImageFile } from './upload.js';
|
| 6 |
+
import { analyzeImage, displayResults, generateQRCode } from './analysis.js';
|
| 7 |
+
import { toggleDarkMode, updateDarkModeGradients, loadDarkModePreference } from './darkmode.js';
|
| 8 |
+
import { loadDashboard, updateDashboardWithImages } from './dashboard.js';
|
| 9 |
+
import { handleContactSubmit } from './contact.js';
|
| 10 |
+
import { initElementSdk } from './configSdk.js';
|
| 11 |
+
|
| 12 |
+
window.toggleDarkMode = toggleDarkMode;
|
| 13 |
+
window.navigateTo = navigateTo;
|
| 14 |
+
window.switchLanguage = switchLanguage;
|
| 15 |
+
window.toggleLanguageMenu = toggleLanguageMenu;
|
| 16 |
+
window.toggleMobileLanguageMenu = toggleMobileLanguageMenu;
|
| 17 |
+
window.toggleMobileMenu = toggleMobileMenu;
|
| 18 |
+
window.closeMobileMenu = closeMobileMenu;
|
| 19 |
+
window.handleLogout = handleLogout;
|
| 20 |
+
window.handleLogin = handleLogin;
|
| 21 |
+
window.handleSignup = handleSignup;
|
| 22 |
+
window.analyzeImage = analyzeImage;
|
| 23 |
+
window.resetUpload = resetUpload;
|
| 24 |
+
window.handleContactSubmit = handleContactSubmit;
|
| 25 |
+
window.switchAuthTab = switchAuthTab;
|
| 26 |
+
|
| 27 |
+
updateTranslations();
|
| 28 |
+
checkAuth();
|
| 29 |
+
loadDarkModePreference();
|
| 30 |
+
initMenuEvents();
|
| 31 |
+
initUpload();
|
| 32 |
+
if (window.location.hash) {
|
| 33 |
+
const page = window.location.hash.substring(1);
|
| 34 |
+
const pageElement = document.getElementById(`${page}-page`);
|
| 35 |
+
if (pageElement) {
|
| 36 |
+
document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
|
| 37 |
+
pageElement.classList.add('active');
|
| 38 |
+
const pages = ['home', 'features', 'analyze', 'dashboard', 'gallery', 'contact', 'login'];
|
| 39 |
+
const index = pages.indexOf(page);
|
| 40 |
+
if (index >= 0) {
|
| 41 |
+
document.querySelectorAll('.sidebar-dot').forEach(dot => dot.classList.remove('active'));
|
| 42 |
+
const dots = document.querySelectorAll('.sidebar-dot');
|
| 43 |
+
if (dots[index]) dots[index].classList.add('active');
|
| 44 |
+
}
|
| 45 |
+
}
|
| 46 |
+
}
|
| 47 |
+
initElementSdk();
|
static/js/menu.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
function toggleMobileMenu() {
|
| 2 |
+
const mobileMenu = document.getElementById('mobile-menu');
|
| 3 |
+
const menuIcon = document.getElementById('menu-icon');
|
| 4 |
+
const closeIcon = document.getElementById('close-icon');
|
| 5 |
+
if (mobileMenu) {
|
| 6 |
+
mobileMenu.classList.toggle('hidden');
|
| 7 |
+
if (menuIcon && closeIcon) {
|
| 8 |
+
menuIcon.classList.toggle('hidden');
|
| 9 |
+
closeIcon.classList.toggle('hidden');
|
| 10 |
+
}
|
| 11 |
+
}
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
function closeMobileMenu() {
|
| 15 |
+
const mobileMenu = document.getElementById('mobile-menu');
|
| 16 |
+
const menuIcon = document.getElementById('menu-icon');
|
| 17 |
+
const closeIcon = document.getElementById('close-icon');
|
| 18 |
+
if (mobileMenu) {
|
| 19 |
+
mobileMenu.classList.add('hidden');
|
| 20 |
+
if (menuIcon && closeIcon) {
|
| 21 |
+
menuIcon.classList.remove('hidden');
|
| 22 |
+
closeIcon.classList.add('hidden');
|
| 23 |
+
}
|
| 24 |
+
}
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
function toggleMobileLanguageMenu() {
|
| 28 |
+
const menu = document.getElementById('mobile-lang-menu');
|
| 29 |
+
if (menu) {
|
| 30 |
+
menu.classList.toggle('hidden');
|
| 31 |
+
}
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
function initMenuEvents() {
|
| 35 |
+
document.addEventListener('click', (event) => {
|
| 36 |
+
const mobileMenu = document.getElementById('mobile-menu');
|
| 37 |
+
const mobileMenuBtn = document.getElementById('mobile-menu-btn');
|
| 38 |
+
const mobileLangMenu = document.getElementById('mobile-lang-menu');
|
| 39 |
+
const mobileLangBtn = document.getElementById('mobile-lang-switcher');
|
| 40 |
+
if (mobileMenu && !mobileMenu.contains(event.target) && !mobileMenuBtn?.contains(event.target)) {
|
| 41 |
+
if (!mobileMenu.classList.contains('hidden')) {
|
| 42 |
+
closeMobileMenu();
|
| 43 |
+
}
|
| 44 |
+
}
|
| 45 |
+
if (mobileLangMenu && !mobileLangMenu.contains(event.target) && !mobileLangBtn?.contains(event.target)) {
|
| 46 |
+
if (!mobileLangMenu.classList.contains('hidden')) {
|
| 47 |
+
mobileLangMenu.classList.add('hidden');
|
| 48 |
+
}
|
| 49 |
+
}
|
| 50 |
+
});
|
| 51 |
+
document.addEventListener('click', (e) => {
|
| 52 |
+
const langSwitcher = document.getElementById('lang-switcher');
|
| 53 |
+
const langMenu = document.getElementById('lang-menu');
|
| 54 |
+
if (langSwitcher && langMenu && !langSwitcher.contains(e.target) && !langMenu.contains(e.target)) {
|
| 55 |
+
langMenu.classList.add('hidden');
|
| 56 |
+
}
|
| 57 |
+
const accountBtn = document.getElementById('sign-in-btn');
|
| 58 |
+
const accountMenu = document.getElementById('account-menu');
|
| 59 |
+
if (accountBtn && accountMenu && !accountBtn.contains(e.target) && !accountMenu.contains(e.target)) {
|
| 60 |
+
accountMenu.classList.add('hidden');
|
| 61 |
+
}
|
| 62 |
+
});
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
export { toggleMobileMenu, closeMobileMenu, toggleMobileLanguageMenu, initMenuEvents };
|
static/js/navigation.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { loadDashboard } from './dashboard.js';
|
| 2 |
+
|
| 3 |
+
function navigateTo(page, event) {
|
| 4 |
+
if (event && event.preventDefault) {
|
| 5 |
+
event.preventDefault();
|
| 6 |
+
}
|
| 7 |
+
document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
|
| 8 |
+
const pageElement = document.getElementById(`${page}-page`);
|
| 9 |
+
if (pageElement) {
|
| 10 |
+
pageElement.classList.add('active');
|
| 11 |
+
}
|
| 12 |
+
document.querySelectorAll('.sidebar-dot').forEach(dot => dot.classList.remove('active'));
|
| 13 |
+
const dots = document.querySelectorAll('.sidebar-dot');
|
| 14 |
+
const pages = ['home', 'features', 'analyze', 'dashboard', 'gallery', 'contact', 'login'];
|
| 15 |
+
const index = pages.indexOf(page);
|
| 16 |
+
if (index >= 0 && dots[index]) {
|
| 17 |
+
dots[index].classList.add('active');
|
| 18 |
+
}
|
| 19 |
+
window.location.hash = page;
|
| 20 |
+
window.scrollTo({ top: 0, behavior: 'smooth' });
|
| 21 |
+
if (page === 'dashboard') {
|
| 22 |
+
loadDashboard();
|
| 23 |
+
}
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
function switchAuthTab(tab) {
|
| 27 |
+
const loginTab = document.getElementById('login-tab');
|
| 28 |
+
const signupTab = document.getElementById('signup-tab');
|
| 29 |
+
const loginForm = document.getElementById('login-form');
|
| 30 |
+
const signupForm = document.getElementById('signup-form');
|
| 31 |
+
if (tab === 'login') {
|
| 32 |
+
loginTab.classList.add('bg-white', 'shadow-sm', 'text-green-600');
|
| 33 |
+
loginTab.classList.remove('text-gray-600');
|
| 34 |
+
signupTab.classList.remove('bg-white', 'shadow-sm', 'text-green-600');
|
| 35 |
+
signupTab.classList.add('text-gray-600');
|
| 36 |
+
loginForm.classList.remove('hidden');
|
| 37 |
+
signupForm.classList.add('hidden');
|
| 38 |
+
} else {
|
| 39 |
+
signupTab.classList.add('bg-white', 'shadow-sm', 'text-green-600');
|
| 40 |
+
signupTab.classList.remove('text-gray-600');
|
| 41 |
+
loginTab.classList.remove('bg-white', 'shadow-sm', 'text-green-600');
|
| 42 |
+
loginTab.classList.add('text-gray-600');
|
| 43 |
+
signupForm.classList.remove('hidden');
|
| 44 |
+
loginForm.classList.add('hidden');
|
| 45 |
+
}
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
export { navigateTo, switchAuthTab };
|
static/js/upload.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
let uploadArea;
|
| 2 |
+
let fileInput;
|
| 3 |
+
let uploadPrompt;
|
| 4 |
+
let previewSection;
|
| 5 |
+
let imagePreview;
|
| 6 |
+
let resultsSection;
|
| 7 |
+
let currentImageFile = null;
|
| 8 |
+
|
| 9 |
+
function displayImage(file) {
|
| 10 |
+
currentImageFile = file;
|
| 11 |
+
const reader = new FileReader();
|
| 12 |
+
reader.onload = (e) => {
|
| 13 |
+
imagePreview.src = e.target.result;
|
| 14 |
+
uploadPrompt.classList.add('hidden');
|
| 15 |
+
previewSection.classList.remove('hidden');
|
| 16 |
+
resultsSection.classList.add('hidden');
|
| 17 |
+
};
|
| 18 |
+
reader.readAsDataURL(file);
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
function resetUpload() {
|
| 22 |
+
fileInput.value = '';
|
| 23 |
+
currentImageFile = null;
|
| 24 |
+
uploadPrompt.classList.remove('hidden');
|
| 25 |
+
previewSection.classList.add('hidden');
|
| 26 |
+
resultsSection.classList.add('hidden');
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
function initUpload() {
|
| 30 |
+
uploadArea = document.getElementById('upload-area');
|
| 31 |
+
fileInput = document.getElementById('file-input');
|
| 32 |
+
uploadPrompt = document.getElementById('upload-prompt');
|
| 33 |
+
previewSection = document.getElementById('preview-section');
|
| 34 |
+
imagePreview = document.getElementById('image-preview');
|
| 35 |
+
resultsSection = document.getElementById('results-section');
|
| 36 |
+
if (!uploadArea || !fileInput) return;
|
| 37 |
+
uploadArea.addEventListener('dragover', (e) => {
|
| 38 |
+
e.preventDefault();
|
| 39 |
+
uploadArea.classList.add('drag-active');
|
| 40 |
+
});
|
| 41 |
+
uploadArea.addEventListener('dragleave', () => {
|
| 42 |
+
uploadArea.classList.remove('drag-active');
|
| 43 |
+
});
|
| 44 |
+
uploadArea.addEventListener('drop', (e) => {
|
| 45 |
+
e.preventDefault();
|
| 46 |
+
uploadArea.classList.remove('drag-active');
|
| 47 |
+
const file = e.dataTransfer.files[0];
|
| 48 |
+
if (file && file.type.startsWith('image/')) {
|
| 49 |
+
displayImage(file);
|
| 50 |
+
}
|
| 51 |
+
});
|
| 52 |
+
fileInput.addEventListener('change', (e) => {
|
| 53 |
+
const file = e.target.files[0];
|
| 54 |
+
if (file) {
|
| 55 |
+
displayImage(file);
|
| 56 |
+
}
|
| 57 |
+
});
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
function getCurrentImageFile() {
|
| 61 |
+
return currentImageFile;
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
export { initUpload, displayImage, resetUpload, getCurrentImageFile };
|
static/styles.css
ADDED
|
@@ -0,0 +1,493 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
body {
|
| 2 |
+
box-sizing: border-box;
|
| 3 |
+
margin: 0;
|
| 4 |
+
padding: 0;
|
| 5 |
+
font-family: 'Poppins', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
| 6 |
+
}
|
| 7 |
+
|
| 8 |
+
* {
|
| 9 |
+
box-sizing: border-box;
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
.page {
|
| 13 |
+
display: none;
|
| 14 |
+
min-height: 100%;
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
.page.active {
|
| 18 |
+
display: block;
|
| 19 |
+
animation: slideIn 0.4s ease-out;
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
@keyframes slideIn {
|
| 23 |
+
from {
|
| 24 |
+
opacity: 0;
|
| 25 |
+
transform: translateY(20px);
|
| 26 |
+
}
|
| 27 |
+
to {
|
| 28 |
+
opacity: 1;
|
| 29 |
+
transform: translateY(0);
|
| 30 |
+
}
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
.hero-pattern {
|
| 34 |
+
background-image:
|
| 35 |
+
radial-gradient(circle at 20% 50%, rgba(34, 197, 94, 0.15) 0%, transparent 50%),
|
| 36 |
+
radial-gradient(circle at 80% 80%, rgba(249, 115, 22, 0.15) 0%, transparent 50%);
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
.card-hover {
|
| 40 |
+
position: relative;
|
| 41 |
+
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
| 42 |
+
will-change: transform, box-shadow;
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
.card-hover:hover {
|
| 46 |
+
transform: translateY(-12px) scale(1.02);
|
| 47 |
+
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
| 48 |
+
z-index: 10;
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
/* Specific fix for dashboard analysis cards to prevent flickering */
|
| 52 |
+
#dashboard-page .space-y-6 {
|
| 53 |
+
position: relative;
|
| 54 |
+
/* Add extra spacing to account for hover transform */
|
| 55 |
+
padding-top: 0.5rem;
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
#dashboard-page .space-y-6 > .card-hover {
|
| 59 |
+
position: relative;
|
| 60 |
+
isolation: isolate;
|
| 61 |
+
transform-origin: center center;
|
| 62 |
+
/* Ensure each card maintains its space */
|
| 63 |
+
display: block;
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
#dashboard-page .space-y-6 > .card-hover:hover {
|
| 67 |
+
z-index: 20;
|
| 68 |
+
/* Reduce transform slightly for dashboard cards to prevent overlap */
|
| 69 |
+
transform: translateY(-8px) scale(1.01);
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
.nav-item {
|
| 73 |
+
position: relative;
|
| 74 |
+
transition: color 0.3s ease;
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
.nav-item::after {
|
| 78 |
+
content: '';
|
| 79 |
+
position: absolute;
|
| 80 |
+
bottom: -4px;
|
| 81 |
+
left: 0;
|
| 82 |
+
width: 0;
|
| 83 |
+
height: 3px;
|
| 84 |
+
background: linear-gradient(90deg, #22c55e, #f97316);
|
| 85 |
+
transition: width 0.3s ease;
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
.nav-item:hover::after {
|
| 89 |
+
width: 100%;
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
.upload-area {
|
| 93 |
+
border: 3px dashed #d1d5db;
|
| 94 |
+
transition: all 0.3s ease;
|
| 95 |
+
background: linear-gradient(135deg, rgba(34, 197, 94, 0.03) 0%, rgba(249, 115, 22, 0.03) 100%);
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
.upload-area:hover {
|
| 99 |
+
border-color: #22c55e;
|
| 100 |
+
background: linear-gradient(135deg, rgba(34, 197, 94, 0.08) 0%, rgba(249, 115, 22, 0.08) 100%);
|
| 101 |
+
transform: scale(1.01);
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
.upload-area.drag-active {
|
| 105 |
+
border-color: #f97316;
|
| 106 |
+
background: linear-gradient(135deg, rgba(34, 197, 94, 0.12) 0%, rgba(249, 115, 22, 0.12) 100%);
|
| 107 |
+
transform: scale(1.02);
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
.gradient-text {
|
| 111 |
+
background: linear-gradient(135deg, #22c55e 0%, #f97316 100%);
|
| 112 |
+
-webkit-background-clip: text;
|
| 113 |
+
-webkit-text-fill-color: transparent;
|
| 114 |
+
background-clip: text;
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
.btn-gradient {
|
| 118 |
+
background: linear-gradient(135deg, #22c55e 0%, #16a34a 100%);
|
| 119 |
+
transition: all 0.3s ease;
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
.btn-gradient:hover {
|
| 123 |
+
background: linear-gradient(135deg, #16a34a 0%, #15803d 100%);
|
| 124 |
+
transform: translateY(-2px);
|
| 125 |
+
box-shadow: 0 10px 25px -5px rgba(34, 197, 94, 0.4);
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
.btn-gradient-orange {
|
| 129 |
+
background: linear-gradient(135deg, #f97316 0%, #ea580c 100%);
|
| 130 |
+
transition: all 0.3s ease;
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
.btn-gradient-orange:hover {
|
| 134 |
+
background: linear-gradient(135deg, #ea580c 0%, #c2410c 100%);
|
| 135 |
+
transform: translateY(-2px);
|
| 136 |
+
box-shadow: 0 10px 25px -5px rgba(249, 115, 22, 0.4);
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
.feature-icon {
|
| 140 |
+
transition: transform 0.3s ease;
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
.card-hover:hover .feature-icon {
|
| 144 |
+
transform: scale(1.2) rotate(5deg);
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
.gallery-item {
|
| 148 |
+
position: relative;
|
| 149 |
+
overflow: hidden;
|
| 150 |
+
transition: all 0.3s ease;
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
.gallery-item::before {
|
| 154 |
+
content: '';
|
| 155 |
+
position: absolute;
|
| 156 |
+
top: 0;
|
| 157 |
+
left: 0;
|
| 158 |
+
right: 0;
|
| 159 |
+
bottom: 0;
|
| 160 |
+
background: linear-gradient(135deg, rgba(34, 197, 94, 0.8) 0%, rgba(249, 115, 22, 0.8) 100%);
|
| 161 |
+
opacity: 0;
|
| 162 |
+
transition: opacity 0.3s ease;
|
| 163 |
+
z-index: 1;
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
.gallery-item:hover::before {
|
| 167 |
+
opacity: 1;
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
.gallery-item:hover {
|
| 171 |
+
transform: scale(1.05);
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
.stat-number {
|
| 175 |
+
font-size: 3rem;
|
| 176 |
+
font-weight: 800;
|
| 177 |
+
line-height: 1;
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
.sidebar-nav {
|
| 181 |
+
position: fixed;
|
| 182 |
+
left: 0;
|
| 183 |
+
top: 50%;
|
| 184 |
+
transform: translateY(-50%);
|
| 185 |
+
z-index: 40;
|
| 186 |
+
padding: 1rem;
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
.sidebar-dot {
|
| 190 |
+
width: 12px;
|
| 191 |
+
height: 12px;
|
| 192 |
+
border-radius: 50%;
|
| 193 |
+
background: #d1d5db;
|
| 194 |
+
transition: all 0.3s ease;
|
| 195 |
+
cursor: pointer;
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
.sidebar-dot:hover,
|
| 199 |
+
.sidebar-dot.active {
|
| 200 |
+
background: linear-gradient(135deg, #22c55e 0%, #f97316 100%);
|
| 201 |
+
transform: scale(1.5);
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
+
/* Dark Mode Styles */
|
| 205 |
+
body.dark-mode {
|
| 206 |
+
background-color: #0f172a;
|
| 207 |
+
color: #e2e8f0;
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
body.dark-mode .bg-white {
|
| 211 |
+
background-color: #1e293b !important;
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
body.dark-mode .bg-gray-50 {
|
| 215 |
+
background-color: #0f172a !important;
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
+
body.dark-mode .text-gray-900,
|
| 219 |
+
body.dark-mode .text-gray-800,
|
| 220 |
+
body.dark-mode .text-gray-700 {
|
| 221 |
+
color: #e2e8f0 !important;
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
body.dark-mode .text-gray-600 {
|
| 225 |
+
color: #94a3b8 !important;
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
body.dark-mode .border-gray-100 {
|
| 229 |
+
border-color: #334155 !important;
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
body.dark-mode .border-gray-200 {
|
| 233 |
+
border-color: #334155 !important;
|
| 234 |
+
}
|
| 235 |
+
|
| 236 |
+
body.dark-mode .bg-gray-100 {
|
| 237 |
+
background-color: #1e293b !important;
|
| 238 |
+
}
|
| 239 |
+
|
| 240 |
+
body.dark-mode .bg-gray-200 {
|
| 241 |
+
background-color: #334155 !important;
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
body.dark-mode header {
|
| 245 |
+
background-color: rgba(30, 41, 59, 0.95) !important;
|
| 246 |
+
}
|
| 247 |
+
|
| 248 |
+
body.dark-mode .hero-pattern {
|
| 249 |
+
background-image:
|
| 250 |
+
radial-gradient(circle at 20% 50%, rgba(34, 197, 94, 0.1) 0%, transparent 50%),
|
| 251 |
+
radial-gradient(circle at 80% 80%, rgba(249, 115, 22, 0.1) 0%, transparent 50%);
|
| 252 |
+
}
|
| 253 |
+
|
| 254 |
+
/* Dark mode for gradient backgrounds - Home page "Why Farmers Choose Us" tiles */
|
| 255 |
+
/* Target cards with gradient backgrounds and override with dark mode colors */
|
| 256 |
+
body.dark-mode .card-hover.bg-gradient-to-br {
|
| 257 |
+
background-image: none !important;
|
| 258 |
+
}
|
| 259 |
+
|
| 260 |
+
body.dark-mode .card-hover[class*="from-green-50"][class*="to-green-100"] {
|
| 261 |
+
background: linear-gradient(to bottom right, rgba(6, 78, 59, 0.4), rgba(5, 150, 105, 0.3)) !important;
|
| 262 |
+
background-image: linear-gradient(to bottom right, rgba(6, 78, 59, 0.4), rgba(5, 150, 105, 0.3)) !important;
|
| 263 |
+
}
|
| 264 |
+
|
| 265 |
+
body.dark-mode .card-hover[class*="from-orange-50"][class*="to-orange-100"] {
|
| 266 |
+
background: linear-gradient(to bottom right, rgba(124, 45, 18, 0.4), rgba(154, 52, 18, 0.3)) !important;
|
| 267 |
+
background-image: linear-gradient(to bottom right, rgba(124, 45, 18, 0.4), rgba(154, 52, 18, 0.3)) !important;
|
| 268 |
+
}
|
| 269 |
+
|
| 270 |
+
body.dark-mode .card-hover[class*="from-green-50"][class*="to-orange-100"] {
|
| 271 |
+
background: linear-gradient(to bottom right, rgba(6, 78, 59, 0.4), rgba(154, 52, 18, 0.3)) !important;
|
| 272 |
+
background-image: linear-gradient(to bottom right, rgba(6, 78, 59, 0.4), rgba(154, 52, 18, 0.3)) !important;
|
| 273 |
+
}
|
| 274 |
+
|
| 275 |
+
/* Dark mode for dashboard recent analysis cards */
|
| 276 |
+
body.dark-mode .card-hover[class*="from-red-50"][class*="to-orange-50"] {
|
| 277 |
+
background: linear-gradient(to bottom right, rgba(127, 29, 29, 0.4), rgba(124, 45, 18, 0.3)) !important;
|
| 278 |
+
background-image: linear-gradient(to bottom right, rgba(127, 29, 29, 0.4), rgba(124, 45, 18, 0.3)) !important;
|
| 279 |
+
}
|
| 280 |
+
|
| 281 |
+
/* Dark mode for Analyze page "Analysis Complete" container */
|
| 282 |
+
body.dark-mode #results-section > div[class*="bg-gradient-to-br"][class*="from-green-50"][class*="to-orange-50"] {
|
| 283 |
+
background: linear-gradient(to bottom right, rgba(6, 78, 59, 0.6), rgba(154, 52, 18, 0.5)) !important;
|
| 284 |
+
background-image: linear-gradient(to bottom right, rgba(6, 78, 59, 0.6), rgba(154, 52, 18, 0.5)) !important;
|
| 285 |
+
border-color: rgba(34, 197, 94, 0.35) !important;
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
+
/* Dark mode for card borders */
|
| 289 |
+
body.dark-mode .border-green-200 {
|
| 290 |
+
border-color: rgba(34, 197, 94, 0.3) !important;
|
| 291 |
+
}
|
| 292 |
+
|
| 293 |
+
body.dark-mode .border-orange-200 {
|
| 294 |
+
border-color: rgba(249, 115, 22, 0.3) !important;
|
| 295 |
+
}
|
| 296 |
+
|
| 297 |
+
body.dark-mode .border-red-200 {
|
| 298 |
+
border-color: rgba(239, 68, 68, 0.3) !important;
|
| 299 |
+
}
|
| 300 |
+
|
| 301 |
+
/* Dark mode for input fields and textareas */
|
| 302 |
+
body.dark-mode input[type="text"],
|
| 303 |
+
body.dark-mode input[type="email"],
|
| 304 |
+
body.dark-mode input[type="password"],
|
| 305 |
+
body.dark-mode textarea {
|
| 306 |
+
background-color: #1e293b !important;
|
| 307 |
+
color: #e2e8f0 !important;
|
| 308 |
+
border-color: #334155 !important;
|
| 309 |
+
}
|
| 310 |
+
|
| 311 |
+
body.dark-mode input[type="text"]:focus,
|
| 312 |
+
body.dark-mode input[type="email"]:focus,
|
| 313 |
+
body.dark-mode input[type="password"]:focus,
|
| 314 |
+
body.dark-mode textarea:focus {
|
| 315 |
+
border-color: #22c55e !important;
|
| 316 |
+
background-color: #1e293b !important;
|
| 317 |
+
color: #e2e8f0 !important;
|
| 318 |
+
}
|
| 319 |
+
|
| 320 |
+
body.dark-mode input[type="text"]::placeholder,
|
| 321 |
+
body.dark-mode input[type="email"]::placeholder,
|
| 322 |
+
body.dark-mode input[type="password"]::placeholder,
|
| 323 |
+
body.dark-mode textarea::placeholder {
|
| 324 |
+
color: #64748b !important;
|
| 325 |
+
}
|
| 326 |
+
|
| 327 |
+
/* Dark mode for text colors in cards */
|
| 328 |
+
body.dark-mode .text-gray-500 {
|
| 329 |
+
color: #94a3b8 !important;
|
| 330 |
+
}
|
| 331 |
+
|
| 332 |
+
/* Dark mode for language switcher */
|
| 333 |
+
body.dark-mode #lang-menu {
|
| 334 |
+
background-color: #1e293b !important;
|
| 335 |
+
border-color: #334155 !important;
|
| 336 |
+
}
|
| 337 |
+
|
| 338 |
+
body.dark-mode #lang-menu button {
|
| 339 |
+
color: #e2e8f0 !important;
|
| 340 |
+
}
|
| 341 |
+
|
| 342 |
+
body.dark-mode #lang-menu button:hover {
|
| 343 |
+
background-color: #334155 !important;
|
| 344 |
+
color: #e2e8f0 !important;
|
| 345 |
+
}
|
| 346 |
+
|
| 347 |
+
/* Dark mode for account dropdown */
|
| 348 |
+
body.dark-mode #account-menu {
|
| 349 |
+
background-color: #1e293b !important;
|
| 350 |
+
border-color: #334155 !important;
|
| 351 |
+
}
|
| 352 |
+
|
| 353 |
+
body.dark-mode #account-menu button {
|
| 354 |
+
color: #e2e8f0 !important;
|
| 355 |
+
}
|
| 356 |
+
|
| 357 |
+
body.dark-mode #account-menu button:hover {
|
| 358 |
+
background-color: #334155 !important;
|
| 359 |
+
}
|
| 360 |
+
|
| 361 |
+
body.dark-mode #lang-switcher {
|
| 362 |
+
background-color: #1e293b !important;
|
| 363 |
+
color: #e2e8f0 !important;
|
| 364 |
+
}
|
| 365 |
+
|
| 366 |
+
body.dark-mode #lang-switcher:hover {
|
| 367 |
+
background-color: #334155 !important;
|
| 368 |
+
}
|
| 369 |
+
|
| 370 |
+
.dark-mode-toggle {
|
| 371 |
+
position: fixed;
|
| 372 |
+
top: 50%;
|
| 373 |
+
right: 2rem;
|
| 374 |
+
transform: translateY(-50%);
|
| 375 |
+
z-index: 40;
|
| 376 |
+
width: 60px;
|
| 377 |
+
height: 60px;
|
| 378 |
+
background: linear-gradient(135deg, #22c55e 0%, #f97316 100%);
|
| 379 |
+
border-radius: 50%;
|
| 380 |
+
display: flex;
|
| 381 |
+
align-items: center;
|
| 382 |
+
justify-content: center;
|
| 383 |
+
cursor: pointer;
|
| 384 |
+
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
|
| 385 |
+
transition: all 0.3s ease;
|
| 386 |
+
}
|
| 387 |
+
|
| 388 |
+
.dark-mode-toggle:hover {
|
| 389 |
+
transform: translateY(-50%) scale(1.1);
|
| 390 |
+
box-shadow: 0 15px 40px rgba(0, 0, 0, 0.4);
|
| 391 |
+
}
|
| 392 |
+
|
| 393 |
+
.dark-mode-toggle span {
|
| 394 |
+
font-size: 28px;
|
| 395 |
+
transition: transform 0.3s ease;
|
| 396 |
+
}
|
| 397 |
+
|
| 398 |
+
.dark-mode-toggle:hover span {
|
| 399 |
+
transform: rotate(20deg);
|
| 400 |
+
}
|
| 401 |
+
|
| 402 |
+
/* Mobile menu styles */
|
| 403 |
+
#mobile-menu-btn {
|
| 404 |
+
position: relative;
|
| 405 |
+
}
|
| 406 |
+
|
| 407 |
+
#mobile-menu {
|
| 408 |
+
max-height: calc(100vh - 80px);
|
| 409 |
+
overflow-y: auto;
|
| 410 |
+
}
|
| 411 |
+
|
| 412 |
+
#mobile-menu a {
|
| 413 |
+
transition: all 0.2s ease;
|
| 414 |
+
}
|
| 415 |
+
|
| 416 |
+
#mobile-menu a:active {
|
| 417 |
+
background-color: #f3f4f6;
|
| 418 |
+
}
|
| 419 |
+
|
| 420 |
+
/* Dark mode for mobile menu */
|
| 421 |
+
body.dark-mode #mobile-menu {
|
| 422 |
+
background-color: #1e293b !important;
|
| 423 |
+
border-color: #334155 !important;
|
| 424 |
+
}
|
| 425 |
+
|
| 426 |
+
body.dark-mode #mobile-menu a {
|
| 427 |
+
color: #e2e8f0 !important;
|
| 428 |
+
}
|
| 429 |
+
|
| 430 |
+
body.dark-mode #mobile-menu a:hover {
|
| 431 |
+
background-color: #334155 !important;
|
| 432 |
+
}
|
| 433 |
+
|
| 434 |
+
body.dark-mode #mobile-menu a:active {
|
| 435 |
+
background-color: #475569 !important;
|
| 436 |
+
}
|
| 437 |
+
|
| 438 |
+
body.dark-mode #mobile-menu .border-gray-200 {
|
| 439 |
+
border-color: #334155 !important;
|
| 440 |
+
}
|
| 441 |
+
|
| 442 |
+
body.dark-mode #mobile-lang-switcher {
|
| 443 |
+
background-color: #334155 !important;
|
| 444 |
+
color: #e2e8f0 !important;
|
| 445 |
+
}
|
| 446 |
+
|
| 447 |
+
body.dark-mode #mobile-lang-switcher:hover {
|
| 448 |
+
background-color: #475569 !important;
|
| 449 |
+
}
|
| 450 |
+
|
| 451 |
+
body.dark-mode #mobile-lang-menu {
|
| 452 |
+
background-color: #1e293b !important;
|
| 453 |
+
border-color: #334155 !important;
|
| 454 |
+
}
|
| 455 |
+
|
| 456 |
+
body.dark-mode #mobile-lang-menu button {
|
| 457 |
+
color: #e2e8f0 !important;
|
| 458 |
+
}
|
| 459 |
+
|
| 460 |
+
body.dark-mode #mobile-lang-menu button:hover {
|
| 461 |
+
background-color: #334155 !important;
|
| 462 |
+
}
|
| 463 |
+
|
| 464 |
+
body.dark-mode #mobile-menu-btn {
|
| 465 |
+
color: #e2e8f0 !important;
|
| 466 |
+
}
|
| 467 |
+
|
| 468 |
+
body.dark-mode #mobile-menu-btn:hover {
|
| 469 |
+
background-color: #334155 !important;
|
| 470 |
+
}
|
| 471 |
+
|
| 472 |
+
/* Mobile responsive adjustments */
|
| 473 |
+
@media (max-width: 768px) {
|
| 474 |
+
header nav {
|
| 475 |
+
position: relative;
|
| 476 |
+
}
|
| 477 |
+
|
| 478 |
+
#mobile-menu {
|
| 479 |
+
position: fixed;
|
| 480 |
+
top: 73px;
|
| 481 |
+
left: 0;
|
| 482 |
+
right: 0;
|
| 483 |
+
max-height: calc(100vh - 73px);
|
| 484 |
+
z-index: 45;
|
| 485 |
+
}
|
| 486 |
+
|
| 487 |
+
/* Ensure mobile menu button is visible */
|
| 488 |
+
#mobile-menu-btn {
|
| 489 |
+
z-index: 50;
|
| 490 |
+
}
|
| 491 |
+
}
|
| 492 |
+
|
| 493 |
+
@view-transition { navigation: auto; }
|
static/translations.js
ADDED
|
@@ -0,0 +1,331 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Translations for Agrovision
|
| 2 |
+
const translations = {
|
| 3 |
+
en: {
|
| 4 |
+
nav: {
|
| 5 |
+
home: 'Home',
|
| 6 |
+
features: 'Features',
|
| 7 |
+
analyze: 'Analyze',
|
| 8 |
+
dashboard: 'Dashboard',
|
| 9 |
+
gallery: 'Gallery',
|
| 10 |
+
contact: 'Contact',
|
| 11 |
+
signout: 'Sign out'
|
| 12 |
+
},
|
| 13 |
+
home: {
|
| 14 |
+
heroTitle: 'Future of Farming',
|
| 15 |
+
heroDescription: 'Revolutionize your agricultural practices with cutting-edge AI technology. Monitor, analyze, and optimize your crops like never before.',
|
| 16 |
+
startTrial: 'Start Now',
|
| 17 |
+
learnMore: 'Learn More',
|
| 18 |
+
activeFarms: 'Active Farms',
|
| 19 |
+
accuracy: 'Accuracy',
|
| 20 |
+
monitoring: 'Monitoring'
|
| 21 |
+
},
|
| 22 |
+
analyze: {
|
| 23 |
+
title: 'AI Analysis',
|
| 24 |
+
studio: 'Studio',
|
| 25 |
+
subtitle: 'Upload your crop images for instant professional analysis',
|
| 26 |
+
dropImage: 'Drop Your Image Here',
|
| 27 |
+
clickBrowse: 'or click to browse from your device',
|
| 28 |
+
chooseFile: 'Choose File',
|
| 29 |
+
supports: 'Supports: JPG, PNG, WebP (Max 10MB)',
|
| 30 |
+
removeImage: 'Remove Image',
|
| 31 |
+
analyzeNow: '🔍 Analyze Now',
|
| 32 |
+
analysisComplete: 'Analysis Complete ✓',
|
| 33 |
+
shareQR: 'Share Analysis',
|
| 34 |
+
scanQR: 'Scan to view analysis',
|
| 35 |
+
cropHealth: 'Crop Health',
|
| 36 |
+
diseaseRisk: 'Disease Risk',
|
| 37 |
+
growthStage: 'Growth Stage',
|
| 38 |
+
estimatedYield: 'Estimated Yield',
|
| 39 |
+
aiRecommendations: 'AI Recommendations'
|
| 40 |
+
},
|
| 41 |
+
features: {
|
| 42 |
+
title: 'Powerful Features',
|
| 43 |
+
subtitle: 'Everything you need to transform your farm',
|
| 44 |
+
diseaseDetection: 'Disease Detection',
|
| 45 |
+
diseaseDetectionDesc: 'Identify plant diseases instantly with 98% accuracy using advanced computer vision',
|
| 46 |
+
smartIrrigation: 'Smart Irrigation',
|
| 47 |
+
smartIrrigationDesc: 'Optimize water usage with AI-driven irrigation scheduling and soil moisture analysis',
|
| 48 |
+
weatherForecasting: 'Weather Forecasting',
|
| 49 |
+
weatherForecastingDesc: 'Hyperlocal weather predictions to plan your farming activities perfectly',
|
| 50 |
+
yieldAnalytics: 'Yield Analytics',
|
| 51 |
+
yieldAnalyticsDesc: 'Predict harvest outcomes and optimize crop rotation strategies',
|
| 52 |
+
satelliteMapping: 'Satellite Mapping',
|
| 53 |
+
satelliteMappingDesc: 'Monitor large-scale farms with satellite imagery and drone integration',
|
| 54 |
+
expertSupport: 'Expert Support',
|
| 55 |
+
expertSupportDesc: '24/7 access to agricultural experts and AI-powered recommendations'
|
| 56 |
+
},
|
| 57 |
+
homeFeatures: {
|
| 58 |
+
title: 'Why Farmers Choose Us',
|
| 59 |
+
subtitle: 'Powerful tools designed for modern agriculture',
|
| 60 |
+
precisionAnalysis: 'Precision Analysis',
|
| 61 |
+
precisionAnalysisDesc: 'Advanced AI algorithms detect issues before they become problems',
|
| 62 |
+
realTimeInsights: 'Real-Time Insights',
|
| 63 |
+
realTimeInsightsDesc: 'Get instant feedback and actionable recommendations',
|
| 64 |
+
maximizeProfits: 'Maximize Profits',
|
| 65 |
+
maximizeProfitsDesc: 'Increase yields by up to 40% with smart farming'
|
| 66 |
+
},
|
| 67 |
+
dashboard: {
|
| 68 |
+
title: 'Your Dashboard',
|
| 69 |
+
subtitle: 'Track your crop analysis history and insights',
|
| 70 |
+
totalScans: 'Total Scans',
|
| 71 |
+
healthyCrops: 'Healthy Crops',
|
| 72 |
+
issuesFound: 'Issues Found',
|
| 73 |
+
avgHealth: 'Avg Health',
|
| 74 |
+
thisWeek: 'this week',
|
| 75 |
+
successRate: 'success rate',
|
| 76 |
+
needsAttention: 'Needs attention',
|
| 77 |
+
excellentOverall: 'Excellent overall',
|
| 78 |
+
recentAnalysis: 'Recent Analysis',
|
| 79 |
+
newScan: '+ New Scan',
|
| 80 |
+
scannedOn: 'Scanned on',
|
| 81 |
+
diseaseDetected: '⚠️ Disease Detected',
|
| 82 |
+
minorIssue: '⚠️ Minor Issue',
|
| 83 |
+
healthy: '✅ Healthy',
|
| 84 |
+
diseaseIdentified: 'Disease Identified',
|
| 85 |
+
confidenceLevel: 'Confidence Level',
|
| 86 |
+
severity: 'Severity',
|
| 87 |
+
affectedArea: 'Affected Area',
|
| 88 |
+
actionRequired: 'Action Required',
|
| 89 |
+
treatmentRecommendations: 'Treatment Recommendations',
|
| 90 |
+
managementRecommendations: 'Management Recommendations',
|
| 91 |
+
optimizationTips: 'Optimization Tips',
|
| 92 |
+
healthStatus: 'Health Status',
|
| 93 |
+
excellentCondition: 'Excellent Condition',
|
| 94 |
+
growthStage: 'Growth Stage',
|
| 95 |
+
vigorScore: 'Vigor Score',
|
| 96 |
+
yieldForecast: 'Yield Forecast',
|
| 97 |
+
aboveAvg: 'Above Avg',
|
| 98 |
+
moderate: 'Moderate',
|
| 99 |
+
low: 'Low',
|
| 100 |
+
urgent: '🚨 Urgent',
|
| 101 |
+
monitor: '📋 Monitor',
|
| 102 |
+
ofLeaves: 'of leaves',
|
| 103 |
+
percent: '%'
|
| 104 |
+
},
|
| 105 |
+
gallery: {
|
| 106 |
+
title: 'Success Stories',
|
| 107 |
+
subtitle: 'Real results from farms around the world',
|
| 108 |
+
cornExcellence: 'Corn Excellence',
|
| 109 |
+
yieldIncrease: 'yield increase',
|
| 110 |
+
tomatoPerfection: 'Tomato Perfection',
|
| 111 |
+
zeroDisease: 'Zero disease detected',
|
| 112 |
+
lettuceSuccess: 'Lettuce Success',
|
| 113 |
+
premiumQuality: 'Premium quality grade',
|
| 114 |
+
wheatTriumph: 'Wheat Triumph',
|
| 115 |
+
recordHarvest: 'Record harvest 2024',
|
| 116 |
+
berryBrilliance: 'Berry Brilliance',
|
| 117 |
+
organicCertified: 'Organic certified',
|
| 118 |
+
cucumberChampion: 'Cucumber Champion',
|
| 119 |
+
yearRoundProduction: 'Year-round production'
|
| 120 |
+
},
|
| 121 |
+
contact: {
|
| 122 |
+
title: 'Get In Touch',
|
| 123 |
+
subtitle: 'We\'re here to help you grow',
|
| 124 |
+
sendMessage: 'Send us a message',
|
| 125 |
+
fullName: 'Full Name',
|
| 126 |
+
emailAddress: 'Email Address',
|
| 127 |
+
locationAddress: 'Baku, Azerbaijan',
|
| 128 |
+
subject: 'Subject',
|
| 129 |
+
message: 'Message',
|
| 130 |
+
sendButton: 'Send Message 🚀',
|
| 131 |
+
messageSent: '✓ Message sent successfully! We\'ll respond within 24 hours.',
|
| 132 |
+
contactInfo: 'Contact Info',
|
| 133 |
+
email: 'Email',
|
| 134 |
+
phone: 'Phone',
|
| 135 |
+
location: 'Location',
|
| 136 |
+
officeHours: 'Office Hours',
|
| 137 |
+
mondayFriday: 'Monday - Friday',
|
| 138 |
+
saturday: 'Saturday',
|
| 139 |
+
sunday: 'Sunday',
|
| 140 |
+
closed: 'Closed',
|
| 141 |
+
followUs: 'Follow Us',
|
| 142 |
+
placeholderName: 'John Doe',
|
| 143 |
+
placeholderEmail: 'your.email@example.com',
|
| 144 |
+
placeholderSubject: 'How can we help?',
|
| 145 |
+
placeholderMessage: 'Tell us more about your inquiry...'
|
| 146 |
+
},
|
| 147 |
+
login: {
|
| 148 |
+
welcomeBack: 'Welcome Back!',
|
| 149 |
+
signInContinue: 'Sign in to continue to Agrovision',
|
| 150 |
+
login: 'Login',
|
| 151 |
+
signUp: 'Sign Up',
|
| 152 |
+
email: 'Email',
|
| 153 |
+
password: 'Password',
|
| 154 |
+
fullName: 'Full Name',
|
| 155 |
+
confirmPassword: 'Confirm Password',
|
| 156 |
+
signIn: 'Sign In',
|
| 157 |
+
forgotPassword: 'Forgot password?',
|
| 158 |
+
createAccount: 'Create Account',
|
| 159 |
+
successRedirect: '✓ Success! Redirecting to dashboard...'
|
| 160 |
+
},
|
| 161 |
+
common: {
|
| 162 |
+
signIn: 'Sign In',
|
| 163 |
+
confidence: 'Confidence'
|
| 164 |
+
}
|
| 165 |
+
},
|
| 166 |
+
az: {
|
| 167 |
+
nav: {
|
| 168 |
+
home: 'Ana səhifə',
|
| 169 |
+
features: 'Xüsusiyyətlər',
|
| 170 |
+
analyze: 'Təhlil',
|
| 171 |
+
dashboard: 'İdarə paneli',
|
| 172 |
+
gallery: 'Qalereya',
|
| 173 |
+
contact: 'Əlaqə',
|
| 174 |
+
signout: 'Çıxış'
|
| 175 |
+
},
|
| 176 |
+
home: {
|
| 177 |
+
heroTitle: 'Kənd təsərrüfatının gələcəyi',
|
| 178 |
+
heroDescription: 'Ən son AI texnologiyası ilə kənd təsərrüfatı praktikalarınızı inqilab edin. Məhsullarınızı əvvəl heç vaxt olmadığı kimi izləyin, təhlil edin və optimallaşdırın.',
|
| 179 |
+
startTrial: 'Şəkili yüklə!',
|
| 180 |
+
learnMore: 'Ətraflı öyrən',
|
| 181 |
+
activeFarms: 'Aktiv fermalar',
|
| 182 |
+
accuracy: 'Dəqiqlik',
|
| 183 |
+
monitoring: 'Monitorinq'
|
| 184 |
+
},
|
| 185 |
+
analyze: {
|
| 186 |
+
title: 'AI Təhlil',
|
| 187 |
+
studio: 'Studiyası',
|
| 188 |
+
subtitle: 'Peşəkar təhlil üçün məhsul şəkillərinizi yükləyin',
|
| 189 |
+
dropImage: 'Şəklinizi buraya atın',
|
| 190 |
+
clickBrowse: 'və ya cihazınızdan seçmək üçün klikləyin',
|
| 191 |
+
chooseFile: 'Fayl seç',
|
| 192 |
+
supports: 'Dəstəklənir: JPG, PNG, WebP (Maks 10MB)',
|
| 193 |
+
removeImage: 'Şəkli sil',
|
| 194 |
+
analyzeNow: '🔍 İndi təhlil et',
|
| 195 |
+
analysisComplete: 'Təhlil tamamlandı ✓',
|
| 196 |
+
shareQR: 'Təhlili paylaş',
|
| 197 |
+
scanQR: 'Təhlili görmək üçün skan edin',
|
| 198 |
+
cropHealth: 'Məhsul sağlamlığı',
|
| 199 |
+
diseaseRisk: 'Xəstəlik riski',
|
| 200 |
+
growthStage: 'Böyümə mərhələsi',
|
| 201 |
+
estimatedYield: 'Təxmini məhsul',
|
| 202 |
+
aiRecommendations: 'AI tövsiyələri'
|
| 203 |
+
},
|
| 204 |
+
features: {
|
| 205 |
+
title: 'Güclü xüsusiyyətlər',
|
| 206 |
+
subtitle: 'Fermanızı dəyişdirmək üçün lazım olan hər şey',
|
| 207 |
+
diseaseDetection: 'Xəstəlik aşkarlama',
|
| 208 |
+
diseaseDetectionDesc: 'Qabaqcıl kompüter görmə texnologiyası ilə bitki xəstəliklərini 98% dəqiqliklə ani aşkarlayın',
|
| 209 |
+
smartIrrigation: 'Ağıllı suvarma',
|
| 210 |
+
smartIrrigationDesc: 'AI ilə idarə olunan suvarma cədvəli və torpaq rütubəti təhlili ilə su istifadəsini optimallaşdırın',
|
| 211 |
+
weatherForecasting: 'Hava proqnozu',
|
| 212 |
+
weatherForecastingDesc: 'Kənd təsərrüfatı fəaliyyətlərinizi mükəmməl planlaşdırmaq üçün hiperlokal hava proqnozları',
|
| 213 |
+
yieldAnalytics: 'Məhsul analitikası',
|
| 214 |
+
yieldAnalyticsDesc: 'Məhsul nəticələrini proqnozlaşdırın və məhsul rotasiyası strategiyalarını optimallaşdırın',
|
| 215 |
+
satelliteMapping: 'Peyk xəritələşdirməsi',
|
| 216 |
+
satelliteMappingDesc: 'Peyk görüntüləri və dron inteqrasiyası ilə böyük miqyaslı fermaları izləyin',
|
| 217 |
+
expertSupport: 'Mütəxəssis dəstəyi',
|
| 218 |
+
expertSupportDesc: 'Kənd təsərrüfatı mütəxəssislərinə və AI ilə idarə olunan tövsiyələrə 24/7 çıxış'
|
| 219 |
+
},
|
| 220 |
+
homeFeatures: {
|
| 221 |
+
title: 'Fermerlər niyə bizi seçir',
|
| 222 |
+
subtitle: 'Müasir kənd təsərrüfatı üçün hazırlanmış güclü alətlər',
|
| 223 |
+
precisionAnalysis: 'Dəqiq təhlil',
|
| 224 |
+
precisionAnalysisDesc: 'Qabaqcıl AI alqoritmləri problemlərə çevrilməzdən əvvəl problemləri aşkarlayır',
|
| 225 |
+
realTimeInsights: 'Real xaxt içgörüləri',
|
| 226 |
+
realTimeInsightsDesc: 'Ani geri bildirim və hərəkətə keçirilə bilən tövsiyələr əldə edin',
|
| 227 |
+
maximizeProfits: 'Mənfəəti maksimallaşdırın',
|
| 228 |
+
maximizeProfitsDesc: 'Ağıllı fermerçilik ilə məhsuldarlığı 40%-ə qədər artırın'
|
| 229 |
+
},
|
| 230 |
+
dashboard: {
|
| 231 |
+
title: 'İdarə paneliniz',
|
| 232 |
+
subtitle: 'Məhsul təhlil tarixçənizi və içgörülərinizi izləyin',
|
| 233 |
+
totalScans: 'Ümumi skanlar',
|
| 234 |
+
healthyCrops: 'Sağlam məhsullar',
|
| 235 |
+
issuesFound: 'Tapılan problemlər',
|
| 236 |
+
avgHealth: 'Orta sağlamlıq',
|
| 237 |
+
thisWeek: 'bu həftə',
|
| 238 |
+
successRate: 'uğur dərəcəsi',
|
| 239 |
+
needsAttention: 'Diqqət tələb edir',
|
| 240 |
+
excellentOverall: 'Ümumilikdə əla',
|
| 241 |
+
recentAnalysis: 'Son təhlillər',
|
| 242 |
+
newScan: '+ Yeni skan',
|
| 243 |
+
scannedOn: 'Skan edildi',
|
| 244 |
+
diseaseDetected: '⚠️ Xəstəlik aşkarlandı',
|
| 245 |
+
minorIssue: '⚠️ Kiçik problem',
|
| 246 |
+
healthy: '✅ Sağlam',
|
| 247 |
+
diseaseIdentified: 'Aşkarlanan xəstəlik',
|
| 248 |
+
confidenceLevel: 'Etimad səviyyəsi',
|
| 249 |
+
severity: 'Ağırlıq',
|
| 250 |
+
affectedArea: 'Təsirlənmiş sahə',
|
| 251 |
+
actionRequired: 'Tələb olunan hərəkət',
|
| 252 |
+
treatmentRecommendations: 'Müalicə tövsiyələri',
|
| 253 |
+
managementRecommendations: 'İdarəetmə tövsiyələri',
|
| 254 |
+
optimizationTips: 'Optimallaşdırma məsləhətləri',
|
| 255 |
+
healthStatus: 'Sağlamlıq statusu',
|
| 256 |
+
excellentCondition: 'Əla vəziyyət',
|
| 257 |
+
growthStage: 'Böyümə mərhələsi',
|
| 258 |
+
vigorScore: 'Güc skoru',
|
| 259 |
+
yieldForecast: 'Məhsul proqnozu',
|
| 260 |
+
aboveAvg: 'Orta üzərində',
|
| 261 |
+
moderate: 'Orta',
|
| 262 |
+
low: 'Aşağı',
|
| 263 |
+
urgent: '🚨 Təcili',
|
| 264 |
+
monitor: '📋 İzlə',
|
| 265 |
+
ofLeaves: 'yarpaqların',
|
| 266 |
+
percent: '%'
|
| 267 |
+
},
|
| 268 |
+
gallery: {
|
| 269 |
+
title: 'Uğur hekayələri',
|
| 270 |
+
subtitle: 'Dünyanın hər yerindən fermalardan real nəticələr',
|
| 271 |
+
cornExcellence: 'Qarğıdalı',
|
| 272 |
+
yieldIncrease: 'məhsul artımı',
|
| 273 |
+
tomatoPerfection: 'Pomidor',
|
| 274 |
+
zeroDisease: 'Sıfır xəstəlik aşkarlandı',
|
| 275 |
+
lettuceSuccess: 'Kahı',
|
| 276 |
+
premiumQuality: 'Premium keyfiyyət dərəcəsi',
|
| 277 |
+
wheatTriumph: 'Buğda',
|
| 278 |
+
recordHarvest: 'Rekord məhsul 2024',
|
| 279 |
+
berryBrilliance: 'Giləmeyvə',
|
| 280 |
+
organicCertified: 'Orqanik sertifikatlı',
|
| 281 |
+
cucumberChampion: 'Xiyar',
|
| 282 |
+
yearRoundProduction: 'İlboyu istehsal'
|
| 283 |
+
},
|
| 284 |
+
contact: {
|
| 285 |
+
title: 'Əlaqə saxlayın',
|
| 286 |
+
subtitle: 'Böyüməyinizə kömək etmək üçün buradayıq',
|
| 287 |
+
sendMessage: 'Bizə mesaj göndərin',
|
| 288 |
+
fullName: 'Tam ad',
|
| 289 |
+
emailAddress: 'E-poçt ünvanı',
|
| 290 |
+
subject: 'Mövzu',
|
| 291 |
+
message: 'Mesaj',
|
| 292 |
+
sendButton: 'Mesaj göndər 🚀',
|
| 293 |
+
messageSent: '✓ Mesaj uğurla göndərildi! 24 saat ərzində cavab verəcəyik.',
|
| 294 |
+
contactInfo: 'Əlaqə məlumatı',
|
| 295 |
+
email: 'E-poçt',
|
| 296 |
+
phone: 'Telefon',
|
| 297 |
+
location: 'Ünvan',
|
| 298 |
+
locationAddress: 'Bakı, Azərbaycan',
|
| 299 |
+
officeHours: 'Ofis saatları',
|
| 300 |
+
mondayFriday: 'Bazar ertəsi - Cümə',
|
| 301 |
+
saturday: 'Şənbə',
|
| 302 |
+
sunday: 'Bazar',
|
| 303 |
+
closed: 'Bağlıdır',
|
| 304 |
+
followUs: 'Bizi izləyin',
|
| 305 |
+
placeholderName: 'Adınız Soyadınız',
|
| 306 |
+
placeholderEmail: 'sizin.email@misal.com',
|
| 307 |
+
placeholderSubject: 'Necə kömək edə bilərik?',
|
| 308 |
+
placeholderMessage: 'Sorğunuz haqqında daha çox məlumat verin...'
|
| 309 |
+
},
|
| 310 |
+
login: {
|
| 311 |
+
welcomeBack: 'Xoş gəlmisiniz!',
|
| 312 |
+
signInContinue: 'Agrovision-a davam etmək üçün daxil olun',
|
| 313 |
+
login: 'Daxil ol',
|
| 314 |
+
signUp: 'Qeydiyyat',
|
| 315 |
+
email: 'E-poçt',
|
| 316 |
+
password: 'Şifrə',
|
| 317 |
+
fullName: 'Tam ad',
|
| 318 |
+
confirmPassword: 'Şifrəni təsdiqlə',
|
| 319 |
+
signIn: 'Daxil ol',
|
| 320 |
+
forgotPassword: 'Şifrəni unutmusunuz?',
|
| 321 |
+
createAccount: 'Hesab yarat',
|
| 322 |
+
successRedirect: '✓ Uğur! İdarə panelinə yönləndirilir...'
|
| 323 |
+
},
|
| 324 |
+
common: {
|
| 325 |
+
signIn: 'Daxil ol',
|
| 326 |
+
confidence: 'Etimad'
|
| 327 |
+
}
|
| 328 |
+
}
|
| 329 |
+
};
|
| 330 |
+
|
| 331 |
+
|
templates/base.html
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Agrovision - Future of Farming</title>
|
| 7 |
+
<link rel="stylesheet" href="/static/styles.css">
|
| 8 |
+
<script src="/_sdk/element_sdk.js"></script>
|
| 9 |
+
<script src="https://cdn.tailwindcss.com"></script>
|
| 10 |
+
<script src="https://cdn.jsdelivr.net/npm/qrcode@1.5.3/build/qrcode.min.js"></script>
|
| 11 |
+
</head>
|
| 12 |
+
<body class="bg-gray-50">
|
| 13 |
+
{% include 'partials/dark_toggle.html' %}
|
| 14 |
+
{% include 'partials/sidebar.html' %}
|
| 15 |
+
{% include 'partials/header.html' %}
|
| 16 |
+
{% block content %}{% endblock %}
|
| 17 |
+
<script src="/static/translations.js"></script>
|
| 18 |
+
<script type="module" src="/static/js/main.js"></script>
|
| 19 |
+
</body>
|
| 20 |
+
</html>
|
templates/index.html
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends 'base.html' %}
|
| 2 |
+
{% block content %}
|
| 3 |
+
{% include 'sections/home.html' %}
|
| 4 |
+
{% include 'sections/features.html' %}
|
| 5 |
+
{% include 'sections/analyze.html' %}
|
| 6 |
+
{% include 'sections/dashboard.html' %}
|
| 7 |
+
{% include 'sections/gallery.html' %}
|
| 8 |
+
{% include 'sections/contact.html' %}
|
| 9 |
+
{% include 'sections/login.html' %}
|
| 10 |
+
{% endblock %}
|
templates/partials/dark_toggle.html
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
<div class="dark-mode-toggle" onclick="toggleDarkMode()" title="Toggle Dark Mode"><span id="dark-mode-icon">🌙</span></div>
|
templates/partials/header.html
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<header class="fixed top-0 left-0 right-0 bg-white/95 backdrop-blur-sm shadow-sm z-50">
|
| 2 |
+
<nav class="max-w-7xl mx-auto px-4 md:px-6 py-4">
|
| 3 |
+
<div class="flex justify-between items-center">
|
| 4 |
+
<div class="flex items-center space-x-2">
|
| 5 |
+
<div class="w-10 h-10 bg-gradient-to-br from-green-500 to-orange-500 rounded-lg flex items-center justify-center text-white text-xl font-bold">A</div><span id="nav-title" class="text-xl md:text-2xl font-bold gradient-text">Agrovision</span>
|
| 6 |
+
</div>
|
| 7 |
+
<div class="hidden md:flex items-center space-x-8"><a href="#home" class="nav-item text-gray-700 font-medium" data-i18n="nav.home" onclick="navigateTo('home', event)">Home</a> <a href="#features" class="nav-item text-gray-700 font-medium" data-i18n="nav.features" onclick="navigateTo('features', event)">Features</a> <a href="#analyze" class="nav-item text-gray-700 font-medium" data-i18n="nav.analyze" onclick="navigateTo('analyze', event)">Analyze</a> <a href="#dashboard" class="nav-item text-gray-700 font-medium" data-i18n="nav.dashboard" onclick="navigateTo('dashboard', event)">Dashboard</a> <a href="#gallery" class="nav-item text-gray-700 font-medium" data-i18n="nav.gallery" onclick="navigateTo('gallery', event)">Gallery</a> <a href="#contact" class="nav-item text-gray-700 font-medium" data-i18n="nav.contact" onclick="navigateTo('contact', event)">Contact</a></div>
|
| 8 |
+
<div class="hidden md:flex items-center space-x-4">
|
| 9 |
+
<div class="relative">
|
| 10 |
+
<button id="lang-switcher" class="bg-gray-100 hover:bg-gray-200 text-gray-700 px-4 py-2 rounded-full font-semibold transition flex items-center space-x-2" onclick="toggleLanguageMenu()"><span id="current-lang">EN</span><span>▼</span></button>
|
| 11 |
+
<div id="lang-menu" class="hidden absolute right-0 mt-2 bg-white rounded-xl shadow-lg border border-gray-200 py-2 z-50 min-w-[120px]">
|
| 12 |
+
<button class="w-full text-left px-4 py-2 hover:bg-gray-100 text-gray-700 font-medium" onclick="switchLanguage('en')">🇬🇧 English</button>
|
| 13 |
+
<button class="w-full text-left px-4 py-2 hover:bg-gray-100 text-gray-700 font-medium" onclick="switchLanguage('az')">🇦🇿 Azərbaycanca</button>
|
| 14 |
+
</div>
|
| 15 |
+
</div>
|
| 16 |
+
<div class="relative">
|
| 17 |
+
<button id="sign-in-btn" class="btn-gradient-orange text-white px-6 py-2.5 rounded-full font-semibold" onclick="navigateTo('login', event)"> Sign In </button>
|
| 18 |
+
<div id="account-menu" class="hidden absolute right-0 mt-2 bg-white rounded-xl shadow-lg border border-gray-200 py-2 z-50 min-w-[160px]">
|
| 19 |
+
<button class="w-full text-left px-4 py-2 hover:bg-gray-100 text-gray-700 font-medium" data-i18n="nav.dashboard" onclick="navigateTo('dashboard', event)">Dashboard</button>
|
| 20 |
+
<button class="w-full text-left px-4 py-2 hover:bg-gray-100 text-red-600 font-medium" data-i18n="nav.signout" onclick="handleLogout()">Sign out</button>
|
| 21 |
+
</div>
|
| 22 |
+
</div>
|
| 23 |
+
</div>
|
| 24 |
+
<button id="mobile-menu-btn" class="md:hidden p-2 rounded-lg text-gray-700 hover:bg-gray-100 transition" onclick="toggleMobileMenu()" aria-label="Toggle menu">
|
| 25 |
+
<svg id="menu-icon" class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path></svg>
|
| 26 |
+
<svg id="close-icon" class="w-6 h-6 hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path></svg>
|
| 27 |
+
</button>
|
| 28 |
+
</div>
|
| 29 |
+
<div id="mobile-menu" class="hidden md:hidden absolute top-full left-0 right-0 bg-white shadow-lg border-t border-gray-200 py-4 z-40">
|
| 30 |
+
<div class="px-4 space-y-2">
|
| 31 |
+
<a href="#home" class="block px-4 py-3 rounded-lg text-gray-700 font-medium hover:bg-gray-100 transition" data-i18n="nav.home" onclick="navigateTo('home', event); closeMobileMenu();">Home</a>
|
| 32 |
+
<a href="#features" class="block px-4 py-3 rounded-lg text-gray-700 font-medium hover:bg-gray-100 transition" data-i18n="nav.features" onclick="navigateTo('features', event); closeMobileMenu();">Features</a>
|
| 33 |
+
<a href="#analyze" class="block px-4 py-3 rounded-lg text-gray-700 font-medium hover:bg-gray-100 transition" data-i18n="nav.analyze" onclick="navigateTo('analyze', event); closeMobileMenu();">Analyze</a>
|
| 34 |
+
<a href="#dashboard" class="block px-4 py-3 rounded-lg text-gray-700 font-medium hover:bg-gray-100 transition" data-i18n="nav.dashboard" onclick="navigateTo('dashboard', event); closeMobileMenu();">Dashboard</a>
|
| 35 |
+
<a href="#gallery" class="block px-4 py-3 rounded-lg text-gray-700 font-medium hover:bg-gray-100 transition" data-i18n="nav.gallery" onclick="navigateTo('gallery', event); closeMobileMenu();">Gallery</a>
|
| 36 |
+
<a href="#contact" class="block px-4 py-3 rounded-lg text-gray-700 font-medium hover:bg-gray-100 transition" data-i18n="nav.contact" onclick="navigateTo('contact', event); closeMobileMenu();">Contact</a>
|
| 37 |
+
<div class="border-t border-gray-200 my-2"></div>
|
| 38 |
+
<div class="px-4 py-2">
|
| 39 |
+
<div class="relative mb-3">
|
| 40 |
+
<button id="mobile-lang-switcher" class="w-full bg-gray-100 hover:bg-gray-200 text-gray-700 px-4 py-2 rounded-lg font-semibold transition flex items-center justify-between" onclick="toggleMobileLanguageMenu()"><span>Language</span><span id="mobile-current-lang">EN</span></button>
|
| 41 |
+
<div id="mobile-lang-menu" class="hidden absolute top-full left-0 right-0 mt-2 bg-white rounded-lg shadow-lg border border-gray-200 py-2 z-50">
|
| 42 |
+
<button class="w-full text-left px-4 py-2 hover:bg-gray-100 text-gray-700 font-medium" onclick="switchLanguage('en'); closeMobileMenu();">🇬🇧 English</button>
|
| 43 |
+
<button class="w-full text-left px-4 py-2 hover:bg-gray-100 text-gray-700 font-medium" onclick="switchLanguage('az'); closeMobileMenu();">🇦🇿 Azərbaycanca</button>
|
| 44 |
+
</div>
|
| 45 |
+
</div>
|
| 46 |
+
<button class="w-full btn-gradient-orange text-white px-6 py-3 rounded-lg font-semibold" onclick="navigateTo('login', event); closeMobileMenu();"> Sign In </button>
|
| 47 |
+
</div>
|
| 48 |
+
</div>
|
| 49 |
+
</div>
|
| 50 |
+
</nav>
|
| 51 |
+
</header>
|
templates/partials/sidebar.html
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<div class="sidebar-nav hidden md:block">
|
| 2 |
+
<div class="flex flex-col space-y-4">
|
| 3 |
+
<div class="sidebar-dot active" onclick="navigateTo('home', event)" data-title-en="Home" data-title-az="Ana Səhifə" title="Home"></div>
|
| 4 |
+
<div class="sidebar-dot" onclick="navigateTo('features', event)" data-title-en="Features" data-title-az="Xüsusiyyətlər" title="Features"></div>
|
| 5 |
+
<div class="sidebar-dot" onclick="navigateTo('analyze', event)" data-title-en="Analyze" data-title-az="Təhlil" title="Analyze"></div>
|
| 6 |
+
<div class="sidebar-dot" onclick="navigateTo('gallery', event)" data-title-en="Gallery" data-title-az="Qalereya" title="Gallery"></div>
|
| 7 |
+
<div class="sidebar-dot" onclick="navigateTo('contact', event)" data-title-en="Contact" data-title-az="Əlaqə" title="Contact"></div>
|
| 8 |
+
<div class="sidebar-dot" onclick="navigateTo('login', event)" data-title-en="Login" data-title-az="Daxil Ol" title="Login"></div>
|
| 9 |
+
</div>
|
| 10 |
+
</div>
|
templates/sections/analyze.html
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<div id="analyze-page" class="page pt-20">
|
| 2 |
+
<section class="py-20 min-h-screen">
|
| 3 |
+
<div class="max-w-5xl mx-auto px-6">
|
| 4 |
+
<div class="text-center mb-12"><h1 class="text-5xl font-extrabold text-gray-900 mb-4"><span class="gradient-text" data-i18n="analyze.title">AI Analysis</span> <span data-i18n="analyze.studio">Studio</span></h1><p class="text-xl text-gray-600" data-i18n="analyze.subtitle">Upload your crop images for instant professional analysis</p></div>
|
| 5 |
+
<div class="bg-white rounded-3xl shadow-2xl p-10 border-2 border-gray-100">
|
| 6 |
+
<div id="upload-area" class="upload-area rounded-2xl p-16 text-center cursor-pointer">
|
| 7 |
+
<div id="upload-prompt">
|
| 8 |
+
<div class="w-24 h-24 bg-gradient-to-br from-green-400 to-orange-400 rounded-full flex items-center justify-center text-5xl mx-auto mb-6">📷</div>
|
| 9 |
+
<h3 class="text-2xl font-bold text-gray-900 mb-3" data-i18n="analyze.dropImage">Drop Your Image Here</h3>
|
| 10 |
+
<p class="text-gray-600 mb-6 text-lg" data-i18n="analyze.clickBrowse">or click to browse from your device</p><input type="file" id="file-input" accept="image/*" class="hidden"> <button onclick="document.getElementById('file-input').click()" class="btn-gradient text-white px-8 py-4 rounded-full font-bold text-lg" data-i18n="analyze.chooseFile"> Choose File </button>
|
| 11 |
+
<p class="text-sm text-gray-500 mt-4" data-i18n="analyze.supports">Supports: JPG, PNG, WebP (Max 10MB)</p>
|
| 12 |
+
</div>
|
| 13 |
+
<div id="preview-section" class="hidden"><img id="image-preview" class="max-w-full max-h-96 mx-auto rounded-xl shadow-lg mb-6"><div class="flex justify-center gap-4"><button onclick="resetUpload()" class="bg-gray-200 text-gray-700 px-6 py-3 rounded-full font-semibold hover:bg-gray-300 transition" data-i18n="analyze.removeImage"> Remove Image </button> <button onclick="analyzeImage()" class="btn-gradient-orange text-white px-8 py-3 rounded-full font-bold" data-i18n="analyze.analyzeNow"> 🔍 Analyze Now </button></div></div>
|
| 14 |
+
</div>
|
| 15 |
+
<div id="results-section" class="hidden mt-10">
|
| 16 |
+
<div class="bg-gradient-to-br from-green-50 to-orange-50 rounded-2xl p-8 border-2 border-green-200">
|
| 17 |
+
<div class="flex items-center justify-between mb-6"><h3 class="text-2xl font-bold text-gray-900" data-i18n="analyze.analysisComplete">Analysis Complete ✓</h3><span id="confidence-badge" class="bg-green-500 text-white px-4 py-2 rounded-full text-sm font-bold">98% Confidence</span></div>
|
| 18 |
+
<div class="grid md:grid-cols-3 gap-6 mb-6">
|
| 19 |
+
<div class="bg-white rounded-xl p-6 shadow"><div class="flex items-center justify-between mb-2"><span class="text-gray-700 font-semibold" data-i18n="analyze.cropHealth">Crop Health</span> <span class="text-2xl">💚</span></div><div class="text-3xl font-bold text-green-600" id="health-status">Excellent</div><div class="w-full bg-gray-200 rounded-full h-3 mt-3"><div class="bg-gradient-to-r from-green-400 to-green-600 h-3 rounded-full" style="width: 92%"></div></div></div>
|
| 20 |
+
<div class="bg-white rounded-xl p-6 shadow"><div class="flex items-center justify-between mb-2"><span class="text-gray-700 font-semibold" data-i18n="analyze.diseaseRisk">Disease Risk</span> <span class="text-2xl">🛡️</span></div><div class="text-3xl font-bold text-green-600" id="disease-risk">Low</div><div class="w-full bg-gray-200 rounded-full h-3 mt-3"><div class="bg-gradient-to-r from-green-400 to-green-600 h-3 rounded-full" style="width: 15%"></div></div></div>
|
| 21 |
+
<div class="bg-white rounded-xl p-6 shadow text-center"><h4 class="font-bold text-gray-900 mb-3" data-i18n="analyze.shareQR">Share Analysis</h4><div id="qrcode" class="flex justify-center mb-3" style="min-height: 200px;"></div><p class="text-sm text-gray-600" data-i18n="analyze.scanQR">Scan to view analysis</p></div>
|
| 22 |
+
</div>
|
| 23 |
+
<div class="grid md:grid-cols-2 gap-6 mb-6">
|
| 24 |
+
<div class="bg-white rounded-xl p-6 shadow"><div class="text-gray-700 font-semibold mb-2" data-i18n="analyze.growthStage">Growth Stage</div><div class="text-xl font-bold text-gray-900">🌱 Vegetative Phase</div></div>
|
| 25 |
+
<div class="bg-white rounded-xl p-6 shadow"><div class="text-gray-700 font-semibold mb-2" data-i18n="analyze.estimatedYield">Estimated Yield</div><div class="text-xl font-bold text-gray-900">📈 Above Average</div></div>
|
| 26 |
+
</div>
|
| 27 |
+
<div class="bg-white rounded-xl p-6 shadow"><h4 class="font-bold text-gray-900 mb-3 flex items-center"><span class="text-2xl mr-2">💡</span> <span data-i18n="analyze.aiRecommendations">AI Recommendations</span></h4><ul class="space-y-2 text-gray-700"><li class="flex items-start"><span class="text-green-500 mr-2">✓</span> <span>Continue current irrigation schedule - soil moisture levels are optimal</span></li><li class="flex items-start"><span class="text-green-500 mr-2">✓</span> <span>Consider nitrogen-rich fertilizer application in 7-10 days</span></li><li class="flex items-start"><span class="text-green-500 mr-2">✓</span> <span>Monitor for pest activity - conditions are favorable for aphids</span></li></ul></div>
|
| 28 |
+
</div>
|
| 29 |
+
</div>
|
| 30 |
+
</div>
|
| 31 |
+
</div>
|
| 32 |
+
</section>
|
| 33 |
+
</div>
|
templates/sections/contact.html
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<div id="contact-page" class="page pt-20">
|
| 2 |
+
<section class="py-20 min-h-screen">
|
| 3 |
+
<div class="max-w-6xl mx-auto px-6">
|
| 4 |
+
<div class="text-center mb-16"><h1 class="text-5xl font-extrabold text-gray-900 mb-4"><span data-i18n="contact.title">Get In</span> <span class="gradient-text">Touch</span></h1><p class="text-xl text-gray-600" data-i18n="contact.subtitle">We're here to help you grow</p></div>
|
| 5 |
+
<div class="grid md:grid-cols-5 gap-8">
|
| 6 |
+
<div class="md:col-span-3">
|
| 7 |
+
<div class="bg-white rounded-3xl shadow-2xl p-10 border-2 border-gray-100">
|
| 8 |
+
<h2 class="text-3xl font-bold text-gray-900 mb-8" data-i18n="contact.sendMessage">Send us a message</h2>
|
| 9 |
+
<form id="contact-form" onsubmit="handleContactSubmit(event)">
|
| 10 |
+
<div class="grid md:grid-cols-2 gap-6 mb-6">
|
| 11 |
+
<div><label for="name" class="block text-gray-700 font-semibold mb-3" data-i18n="contact.fullName">Full Name</label> <input type="text" id="name" required data-i18n-placeholder="contact.placeholderName" placeholder="John Doe" class="w-full px-5 py-4 border-2 border-gray-200 rounded-xl focus:outline-none focus:border-green-500 transition"></div>
|
| 12 |
+
<div><label for="email" class="block text-gray-700 font-semibold mb-3" data-i18n="contact.emailAddress">Email Address</label> <input type="email" id="email" required data-i18n-placeholder="contact.placeholderEmail" placeholder="your.email@example.com" class="w-full px-5 py-4 border-2 border-gray-200 rounded-xl focus:outline-none focus:border-green-500 transition"></div>
|
| 13 |
+
</div>
|
| 14 |
+
<div class="mb-6"><label for="subject" class="block text-gray-700 font-semibold mb-3" data-i18n="contact.subject">Subject</label> <input type="text" id="subject" required data-i18n-placeholder="contact.placeholderSubject" placeholder="How can we help?" class="w-full px-5 py-4 border-2 border-gray-200 rounded-xl focus:outline-none focus:border-green-500 transition"></div>
|
| 15 |
+
<div class="mb-6"><label for="message" class="block text-gray-700 font-semibold mb-3" data-i18n="contact.message">Message</label> <textarea id="message" required rows="5" data-i18n-placeholder="contact.placeholderMessage" placeholder="Tell us more about your inquiry..." class="w-full px-5 py-4 border-2 border-gray-200 rounded-xl focus:outline-none focus:border-green-500 transition"></textarea></div>
|
| 16 |
+
<button type="submit" class="btn-gradient text-white w-full py-4 rounded-xl font-bold text-lg" data-i18n="contact.sendButton"> Send Message 🚀 </button>
|
| 17 |
+
</form>
|
| 18 |
+
<div id="contact-success" class="hidden mt-6 p-5 bg-green-100 text-green-700 rounded-xl text-center font-semibold" data-i18n="contact.messageSent">✓ Message sent successfully! We'll respond within 24 hours.</div>
|
| 19 |
+
</div>
|
| 20 |
+
</div>
|
| 21 |
+
<div class="md:col-span-2 space-y-6">
|
| 22 |
+
<div class="bg-gradient-to-br from-green-500 to-green-600 rounded-3xl shadow-2xl p-8 text-white">
|
| 23 |
+
<h3 class="text-2xl font-bold mb-6" data-i18n="contact.contactInfo">Contact Info</h3>
|
| 24 |
+
<div class="space-y-6">
|
| 25 |
+
<div class="flex items-start"><div class="w-12 h-12 bg-white/20 rounded-xl flex items-center justify-center text-2xl mr-4">📧</div><div><div class="font-semibold mb-1" data-i18n="contact.email">Email</div><div id="contact-email-display" class="text-white/90">hello@agrovision.io</div></div></div>
|
| 26 |
+
<div class="flex items-start"><div class="w-12 h-12 bg-white/20 rounded-xl flex items-center justify-center text-2xl mr-4">📞</div><div><div class="font-semibold mb-1" data-i18n="contact.phone">Phone</div><div id="contact-phone-display" class="text-white/90">+994 50 705 26 16</div></div></div>
|
| 27 |
+
<div class="flex items-start"><div class="w-12 h-12 bg-white/20 rounded-xl flex items-center justify-center text-2xl mr-4">📍</div><div><div class="font-semibold mb-1" data-i18n="contact.location">Location</div><div class="text-white/90" data-i18n="contact.locationAddress">Baku, Azerbaijan</div></div></div>
|
| 28 |
+
</div>
|
| 29 |
+
</div>
|
| 30 |
+
<div class="bg-gradient-to-br from-orange-500 to-orange-600 rounded-3xl shadow-2xl p-8 text-white">
|
| 31 |
+
<h3 class="text-2xl font-bold mb-4" data-i18n="contact.officeHours">Office Hours</h3>
|
| 32 |
+
<div class="space-y-3"><div class="flex justify-between"><span data-i18n="contact.mondayFriday">Monday - Friday</span> <span class="font-bold">8AM - 8PM</span></div><div class="flex justify-between"><span data-i18n="contact.saturday">Saturday</span> <span class="font-bold">9AM - 5PM</span></div><div class="flex justify-between"><span data-i18n="contact.sunday">Sunday</span> <span class="font-bold" data-i18n="contact.closed">Closed</span></div></div>
|
| 33 |
+
</div>
|
| 34 |
+
<div class="bg-white rounded-3xl shadow-2xl p-8 border-2 border-gray-100">
|
| 35 |
+
<h3 class="text-xl font-bold text-gray-900 mb-4" data-i18n="contact.followUs">Follow Us</h3>
|
| 36 |
+
<div class="flex gap-4"><div class="w-12 h-12 bg-gradient-to-br from-green-400 to-green-600 rounded-xl flex items-center justify-center text-white text-xl cursor-pointer hover:scale-110 transition">f</div><div class="w-12 h-12 bg-gradient-to-br from-blue-400 to-blue-600 rounded-xl flex items-center justify-center text-white text-xl cursor-pointer hover:scale-110 transition">t</div><div class="w-12 h-12 bg-gradient-to-br from-pink-400 to-pink-600 rounded-xl flex items-center justify-center text-white text-xl cursor-pointer hover:scale-110 transition">in</div></div>
|
| 37 |
+
</div>
|
| 38 |
+
</div>
|
| 39 |
+
</div>
|
| 40 |
+
</div>
|
| 41 |
+
</section>
|
| 42 |
+
</div>
|
templates/sections/dashboard.html
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<div id="dashboard-page" class="page pt-20">
|
| 2 |
+
<section class="py-20 min-h-screen">
|
| 3 |
+
<div class="max-w-7xl mx-auto px-6">
|
| 4 |
+
<div class="mb-12"><h1 class="text-5xl font-extrabold text-gray-900 mb-4"><span data-i18n="dashboard.title">Your</span> <span class="gradient-text">Dashboard</span></h1><p class="text-xl text-gray-600" data-i18n="dashboard.subtitle">Track your crop analysis history and insights</p></div>
|
| 5 |
+
<div class="grid md:grid-cols-4 gap-6 mb-12">
|
| 6 |
+
<div class="bg-white rounded-2xl shadow-lg p-6 border border-gray-100"><div class="flex items-center justify-between mb-3"><span class="text-gray-600 font-semibold" data-i18n="dashboard.totalScans">Total Scans</span> <span class="text-3xl">📊</span></div><div class="text-4xl font-bold gradient-text">24</div><div class="text-sm text-gray-500 mt-2" data-i18n="dashboard.thisWeek">+3 this week</div></div>
|
| 7 |
+
<div class="bg-white rounded-2xl shadow-lg p-6 border border-gray-100"><div class="flex items-center justify-between mb-3"><span class="text-gray-600 font-semibold" data-i18n="dashboard.healthyCrops">Healthy Crops</span> <span class="text-3xl">✅</span></div><div class="text-4xl font-bold text-green-600">18</div><div class="text-sm text-gray-500 mt-2" data-i18n="dashboard.successRate">75% success rate</div></div>
|
| 8 |
+
<div class="bg-white rounded-2xl shadow-lg p-6 border border-gray-100"><div class="flex items-center justify-between mb-3"><span class="text-gray-600 font-semibold" data-i18n="dashboard.issuesFound">Issues Found</span> <span class="text-3xl">⚠️</span></div><div class="text-4xl font-bold text-orange-600">6</div><div class="text-sm text-gray-500 mt-2" data-i18n="dashboard.needsAttention">Needs attention</div></div>
|
| 9 |
+
<div class="bg-white rounded-2xl shadow-lg p-6 border border-gray-100"><div class="flex items-center justify-between mb-3"><span class="text-gray-600 font-semibold" data-i18n="dashboard.avgHealth">Avg Health</span> <span class="text-3xl">💚</span></div><div class="text-4xl font-bold text-green-600">87%</div><div class="text-sm text-gray-500 mt-2" data-i18n="dashboard.excellentOverall">Excellent overall</div></div>
|
| 10 |
+
</div>
|
| 11 |
+
<div class="bg-white rounded-3xl shadow-2xl p-8 border-2 border-gray-100">
|
| 12 |
+
<div class="flex items-center justify-between mb-8"><h2 class="text-3xl font-bold text-gray-900" data-i18n="dashboard.recentAnalysis">Recent Analysis</h2><button class="btn-gradient text-white px-6 py-3 rounded-full font-semibold" onclick="navigateTo('analyze', event)" data-i18n="dashboard.newScan"> + New Scan </button></div>
|
| 13 |
+
<div class="space-y-6"></div>
|
| 14 |
+
</div>
|
| 15 |
+
</div>
|
| 16 |
+
</section>
|
| 17 |
+
</div>
|
templates/sections/features.html
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<div id="features-page" class="page pt-20">
|
| 2 |
+
<section class="py-20 min-h-screen">
|
| 3 |
+
<div class="max-w-7xl mx-auto px-6">
|
| 4 |
+
<div class="text-center mb-16"><h1 class="text-5xl font-extrabold text-gray-900 mb-4"><span data-i18n="features.title">Powerful</span> <span class="gradient-text">Features</span></h1><p class="text-xl text-gray-600" data-i18n="features.subtitle">Everything you need to transform your farm</p></div>
|
| 5 |
+
<div class="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
|
| 6 |
+
<div class="card-hover bg-white p-8 rounded-2xl shadow-lg border border-gray-100"><div class="w-16 h-16 bg-gradient-to-br from-green-400 to-green-600 rounded-xl flex items-center justify-center text-3xl mb-6">🔬</div><h3 class="text-2xl font-bold text-gray-900 mb-4" data-i18n="features.diseaseDetection">Disease Detection</h3><p class="text-gray-600 leading-relaxed" data-i18n="features.diseaseDetectionDesc">Identify plant diseases instantly with 98% accuracy using advanced computer vision</p></div>
|
| 7 |
+
<div class="card-hover bg-white p-8 rounded-2xl shadow-lg border border-gray-100"><div class="w-16 h-16 bg-gradient-to-br from-blue-400 to-blue-600 rounded-xl flex items-center justify-center text-3xl mb-6">💧</div><h3 class="text-2xl font-bold text-gray-900 mb-4" data-i18n="features.smartIrrigation">Smart Irrigation</h3><p class="text-gray-600 leading-relaxed" data-i18n="features.smartIrrigationDesc">Optimize water usage with AI-driven irrigation scheduling and soil moisture analysis</p></div>
|
| 8 |
+
<div class="card-hover bg-white p-8 rounded-2xl shadow-lg border border-gray-100"><div class="w-16 h-16 bg-gradient-to-br from-orange-400 to-orange-600 rounded-xl flex items-center justify-center text-3xl mb-6">🌡️</div><h3 class="text-2xl font-bold text-gray-900 mb-4" data-i18n="features.weatherForecasting">Weather Forecasting</h3><p class="text-gray-600 leading-relaxed" data-i18n="features.weatherForecastingDesc">Hyperlocal weather predictions to plan your farming activities perfectly</p></div>
|
| 9 |
+
<div class="card-hover bg-white p-8 rounded-2xl shadow-lg border border-gray-100"><div class="w-16 h-16 bg-gradient-to-br from-purple-400 to-purple-600 rounded-xl flex items-center justify-center text-3xl mb-6">📊</div><h3 class="text-2xl font-bold text-gray-900 mb-4" data-i18n="features.yieldAnalytics">Yield Analytics</h3><p class="text-gray-600 leading-relaxed" data-i18n="features.yieldAnalyticsDesc">Predict harvest outcomes and optimize crop rotation strategies</p></div>
|
| 10 |
+
<div class="card-hover bg-white p-8 rounded-2xl shadow-lg border border-gray-100"><div class="w-16 h-16 bg-gradient-to-br from-pink-400 to-pink-600 rounded-xl flex items-center justify-center text-3xl mb-6">🛰️</div><h3 class="text-2xl font-bold text-gray-900 mb-4" data-i18n="features.satelliteMapping">Satellite Mapping</h3><p class="text-gray-600 leading-relaxed" data-i18n="features.satelliteMappingDesc">Monitor large-scale farms with satellite imagery and drone integration</p></div>
|
| 11 |
+
<div class="card-hover bg-white p-8 rounded-2xl shadow-lg border border-gray-100"><div class="w-16 h-16 bg-gradient-to-br from-yellow-400 to-yellow-600 rounded-xl flex items-center justify-center text-3xl mb-6">🤝</div><h3 class="text-2xl font-bold text-gray-900 mb-4" data-i18n="features.expertSupport">Expert Support</h3><p class="text-gray-600 leading-relaxed" data-i18n="features.expertSupportDesc">24/7 access to agricultural experts and AI-powered recommendations</p></div>
|
| 12 |
+
</div>
|
| 13 |
+
</div>
|
| 14 |
+
</section>
|
| 15 |
+
</div>
|
templates/sections/gallery.html
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<div id="gallery-page" class="page pt-20">
|
| 2 |
+
<section class="py-20 min-h-screen">
|
| 3 |
+
<div class="max-w-7xl mx-auto px-6">
|
| 4 |
+
<div class="text-center mb-16"><h1 class="text-5xl font-extrabold text-gray-900 mb-4"><span data-i18n="gallery.title">Success</span> <span class="gradient-text">Stories</span></h1><p class="text-xl text-gray-600" data-i18n="gallery.subtitle">Real results from farms around the world</p></div>
|
| 5 |
+
<div class="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
|
| 6 |
+
<div class="gallery-item rounded-2xl overflow-hidden shadow-xl cursor-pointer"><div class="h-80 bg-gradient-to-br from-green-300 via-green-400 to-green-600 flex flex-col items-center justify-center p-8 text-white"><span class="text-8xl mb-4">🌽</span><h3 class="text-2xl font-bold mb-2" data-i18n="gallery.cornExcellence">Corn Excellence</h3><p class="text-center" data-i18n="gallery.yieldIncrease">+45% yield increase</p></div></div>
|
| 7 |
+
<div class="gallery-item rounded-2xl overflow-hidden shadow-xl cursor-pointer"><div class="h-80 bg-gradient-to-br from-red-300 via-red-400 to-red-600 flex flex-col items-center justify-center p-8 text-white"><span class="text-8xl mb-4">🍅</span><h3 class="text-2xl font-bold mb-2" data-i18n="gallery.tomatoPerfection">Tomato Perfection</h3><p class="text-center" data-i18n="gallery.zeroDisease">Zero disease detected</p></div></div>
|
| 8 |
+
<div class="gallery-item rounded-2xl overflow-hidden shadow-xl cursor-pointer"><div class="h-80 bg-gradient-to-br from-green-400 via-green-500 to-green-700 flex flex-col items-center justify-center p-8 text-white"><span class="text-8xl mb-4">🥬</span><h3 class="text-2xl font-bold mb-2" data-i18n="gallery.lettuceSuccess">Lettuce Success</h3><p class="text-center" data-i18n="gallery.premiumQuality">Premium quality grade</p></div></div>
|
| 9 |
+
<div class="gallery-item rounded-2xl overflow-hidden shadow-xl cursor-pointer"><div class="h-80 bg-gradient-to-br from-yellow-300 via-yellow-400 to-orange-500 flex flex-col items-center justify-center p-8 text-white"><span class="text-8xl mb-4">🌾</span><h3 class="text-2xl font-bold mb-2" data-i18n="gallery.wheatTriumph">Wheat Triumph</h3><p class="text-center" data-i18n="gallery.recordHarvest">Record harvest 2024</p></div></div>
|
| 10 |
+
<div class="gallery-item rounded-2xl overflow-hidden shadow-xl cursor-pointer"><div class="h-80 bg-gradient-to-br from-pink-300 via-red-400 to-red-600 flex flex-col items-center justify-center p-8 text-white"><span class="text-8xl mb-4">🍓</span><h3 class="text-2xl font-bold mb-2" data-i18n="gallery.berryBrilliance">Berry Brilliance</h3><p class="text-center" data-i18n="gallery.organicCertified">Organic certified</p></div></div>
|
| 11 |
+
<div class="gallery-item rounded-2xl overflow-hidden shadow-xl cursor-pointer"><div class="h-80 bg-gradient-to-br from-green-500 via-green-600 to-green-800 flex flex-col items-center justify-center p-8 text-white"><span class="text-8xl mb-4">🥒</span><h3 class="text-2xl font-bold mb-2" data-i18n="gallery.cucumberChampion">Cucumber Champion</h3><p class="text-center" data-i18n="gallery.yearRoundProduction">Year-round production</p></div></div>
|
| 12 |
+
</div>
|
| 13 |
+
</div>
|
| 14 |
+
</section>
|
| 15 |
+
</div>
|
templates/sections/home.html
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<main id="home-page" class="page active pt-20">
|
| 2 |
+
<section class="hero-pattern min-h-screen flex items-center py-20">
|
| 3 |
+
<div class="max-w-7xl mx-auto px-6 grid md:grid-cols-2 gap-12 items-center">
|
| 4 |
+
<div>
|
| 5 |
+
<div class="inline-block bg-green-100 text-green-700 px-4 py-2 rounded-full text-sm font-semibold mb-6">🚀 AI-Powered Agriculture</div>
|
| 6 |
+
<h1 id="hero-tagline" class="text-6xl font-extrabold text-gray-900 mb-6 leading-tight">Future of <span class="gradient-text">Farming</span></h1>
|
| 7 |
+
<p id="hero-description" class="text-xl text-gray-600 mb-8 leading-relaxed" data-i18n="home.heroDescription">Revolutionize your agricultural practices with cutting-edge AI technology. Monitor, analyze, and optimize your crops like never before.</p>
|
| 8 |
+
<div class="flex flex-wrap gap-4"><button id="cta-button" class="btn-gradient text-white px-8 py-4 rounded-full font-bold text-lg" onclick="navigateTo('analyze', event)" data-i18n="home.startTrial"> Start Free Trial </button> <button class="bg-white text-gray-700 px-8 py-4 rounded-full font-bold text-lg border-2 border-gray-200 hover:border-green-500 transition" onclick="navigateTo('features', event)" data-i18n="home.learnMore"> Learn More </button></div>
|
| 9 |
+
<div class="grid grid-cols-3 gap-6 mt-12">
|
| 10 |
+
<div><div class="stat-number gradient-text">50K+</div><div class="text-gray-600 font-medium" data-i18n="home.activeFarms">Active Farms</div></div>
|
| 11 |
+
<div><div class="stat-number gradient-text">98%</div><div class="text-gray-600 font-medium" data-i18n="home.accuracy">Accuracy</div></div>
|
| 12 |
+
<div><div class="stat-number gradient-text">24/7</div><div class="text-gray-600 font-medium" data-i18n="home.monitoring">Monitoring</div></div>
|
| 13 |
+
</div>
|
| 14 |
+
</div>
|
| 15 |
+
<div class="relative">
|
| 16 |
+
<div class="w-full h-96 bg-gradient-to-br from-green-400 via-green-500 to-orange-500 rounded-3xl shadow-2xl flex items-center justify-center transform rotate-3 hover:rotate-0 transition duration-500"><span class="text-9xl">🌾</span></div>
|
| 17 |
+
<div class="absolute -bottom-6 -left-6 w-32 h-32 bg-orange-400 rounded-2xl shadow-xl flex items-center justify-center text-5xl transform -rotate-12">📊</div>
|
| 18 |
+
<div class="absolute -top-6 -right-6 w-32 h-32 bg-green-400 rounded-2xl shadow-xl flex items-center justify-center text-5xl transform rotate-12">🤖</div>
|
| 19 |
+
</div>
|
| 20 |
+
</div>
|
| 21 |
+
</section>
|
| 22 |
+
<section class="py-20 bg-white">
|
| 23 |
+
<div class="max-w-7xl mx-auto px-6">
|
| 24 |
+
<div class="text-center mb-16"><h2 class="text-4xl font-bold text-gray-900 mb-4" data-i18n="homeFeatures.title">Why Farmers Choose Us</h2><p class="text-xl text-gray-600" data-i18n="homeFeatures.subtitle">Powerful tools designed for modern agriculture</p></div>
|
| 25 |
+
<div class="grid md:grid-cols-3 gap-8">
|
| 26 |
+
<div class="card-hover bg-gradient-to-br from-green-50 to-green-100 p-8 rounded-2xl border-2 border-green-200"><div class="feature-icon text-6xl mb-4">🎯</div><h3 class="text-2xl font-bold text-gray-900 mb-3" data-i18n="homeFeatures.precisionAnalysis">Precision Analysis</h3><p class="text-gray-700" data-i18n="homeFeatures.precisionAnalysisDesc">Advanced AI algorithms detect issues before they become problems</p></div>
|
| 27 |
+
<div class="card-hover bg-gradient-to-br from-orange-50 to-orange-100 p-8 rounded-2xl border-2 border-orange-200"><div class="feature-icon text-6xl mb-4">⚡</div><h3 class="text-2xl font-bold text-gray-900 mb-3" data-i18n="homeFeatures.realTimeInsights">Real-Time Insights</h3><p class="text-gray-700" data-i18n="homeFeatures.realTimeInsightsDesc">Get instant feedback and actionable recommendations</p></div>
|
| 28 |
+
<div class="card-hover bg-gradient-to-br from-green-50 to-orange-100 p-8 rounded-2xl border-2 border-green-200"><div class="feature-icon text-6xl mb-4">💰</div><h3 class="text-2xl font-bold text-gray-900 mb-3" data-i18n="homeFeatures.maximizeProfits">Maximize Profits</h3><p class="text-gray-700" data-i18n="homeFeatures.maximizeProfitsDesc">Increase yields by up to 40% with smart farming</p></div>
|
| 29 |
+
</div>
|
| 30 |
+
</div>
|
| 31 |
+
</section>
|
| 32 |
+
</main>
|
templates/sections/login.html
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<div id="login-page" class="page pt-20">
|
| 2 |
+
<section class="py-20 min-h-screen flex items-center">
|
| 3 |
+
<div class="max-w-md mx-auto px-6 w-full">
|
| 4 |
+
<div class="bg-white rounded-3xl shadow-2xl p-10 border-2 border-gray-100">
|
| 5 |
+
<div class="text-center mb-8"><div class="w-20 h-20 bg-gradient-to-br from-green-500 to-orange-500 rounded-2xl flex items-center justify-center text-white text-3xl font-bold mx-auto mb-4">A</div><h1 class="text-3xl font-extrabold text-gray-900 mb-2" data-i18n="login.welcomeBack">Welcome Back!</h1><p class="text-gray-600" data-i18n="login.signInContinue">Sign in to continue to Agrovision</p></div>
|
| 6 |
+
<div class="flex mb-8 bg-gray-100 rounded-xl p-1.5"><button id="login-tab" class="flex-1 py-3 rounded-lg font-bold transition bg-white shadow-sm text-green-600" onclick="switchAuthTab('login')" data-i18n="login.login"> Login </button> <button id="signup-tab" class="flex-1 py-3 rounded-lg font-bold transition text-gray-600" onclick="switchAuthTab('signup')" data-i18n="login.signUp"> Sign Up </button></div>
|
| 7 |
+
<form id="login-form" onsubmit="handleLogin(event)"><div class="mb-5"><label for="login-email" class="block text-gray-700 font-semibold mb-3" data-i18n="login.email">Email</label> <input type="email" id="login-email" required class="w-full px-5 py-4 border-2 border-gray-200 rounded-xl focus:outline-none focus:border-green-500 transition"></div><div class="mb-6"><label for="login-password" class="block text-gray-700 font-semibold mb-3" data-i18n="login.password">Password</label> <input type="password" id="login-password" required class="w-full px-5 py-4 border-2 border-gray-200 rounded-xl focus:outline-none focus:border-green-500 transition"></div><button type="submit" class="btn-gradient text-white w-full py-4 rounded-xl font-bold text-lg mb-4" data-i18n="login.signIn"> Sign In </button><p class="text-center text-gray-600"><a href="#" class="text-green-600 font-semibold hover:underline" data-i18n="login.forgotPassword">Forgot password?</a></p></form>
|
| 8 |
+
<form id="signup-form" class="hidden" onsubmit="handleSignup(event)"><div class="mb-5"><label for="signup-name" class="block text-gray-700 font-semibold mb-3" data-i18n="login.fullName">Full Name</label> <input type="text" id="signup-name" required class="w-full px-5 py-4 border-2 border-gray-200 rounded-xl focus:outline-none focus:border-green-500 transition"></div><div class="mb-5"><label for="signup-email" class="block text-gray-700 font-semibold mb-3" data-i18n="login.email">Email</label> <input type="email" id="signup-email" required class="w-full px-5 py-4 border-2 border-gray-200 rounded-xl focus:outline-none focus:border-green-500 transition"></div><div class="mb-5"><label for="signup-password" class="block text-gray-700 font-semibold mb-3" data-i18n="login.password">Password</label> <input type="password" id="signup-password" required class="w-full px-5 py-4 border-2 border-gray-200 rounded-xl focus:outline-none focus:border-green-500 transition"></div><div class="mb-6"><label for="signup-confirm" class="block text-gray-700 font-semibold mb-3" data-i18n="login.confirmPassword">Confirm Password</label> <input type="password" id="signup-confirm" required class="w-full px-5 py-4 border-2 border-gray-200 rounded-xl focus:outline-none focus:border-green-500 transition"></div><button type="submit" class="btn-gradient-orange text-white w-full py-4 rounded-xl font-bold text-lg" data-i18n="login.createAccount"> Create Account </button></form>
|
| 9 |
+
<div id="auth-success" class="hidden mt-6 p-5 bg-green-100 text-green-700 rounded-xl text-center font-semibold" data-i18n="login.successRedirect">✓ Success! Redirecting to dashboard...</div>
|
| 10 |
+
</div>
|
| 11 |
+
</div>
|
| 12 |
+
</section>
|
| 13 |
+
</div>
|
train_model.py
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
import numpy as np
|
| 4 |
+
import tensorflow as tf
|
| 5 |
+
from sklearn.utils.class_weight import compute_class_weight
|
| 6 |
+
from sklearn.metrics import classification_report, confusion_matrix
|
| 7 |
+
import matplotlib.pyplot as plt
|
| 8 |
+
import seaborn as sns
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
BASE_DIR = Path("data")
|
| 12 |
+
TRAIN_DIR = BASE_DIR / "train"
|
| 13 |
+
VAL_DIR = BASE_DIR / "val"
|
| 14 |
+
TEST_DIR = BASE_DIR / "test"
|
| 15 |
+
|
| 16 |
+
IMAGE_SIZE = (224, 224)
|
| 17 |
+
BATCH_SIZE = 32
|
| 18 |
+
SEED = 42
|
| 19 |
+
EPOCHS = 25
|
| 20 |
+
|
| 21 |
+
OUTPUT_DIR = Path("models")
|
| 22 |
+
OUTPUT_DIR.mkdir(exist_ok=True)
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def load_datasets():
|
| 26 |
+
train_ds = tf.keras.utils.image_dataset_from_directory(
|
| 27 |
+
TRAIN_DIR,
|
| 28 |
+
labels="inferred",
|
| 29 |
+
label_mode="int",
|
| 30 |
+
image_size=IMAGE_SIZE,
|
| 31 |
+
batch_size=BATCH_SIZE,
|
| 32 |
+
shuffle=True,
|
| 33 |
+
seed=SEED,
|
| 34 |
+
)
|
| 35 |
+
|
| 36 |
+
val_ds = tf.keras.utils.image_dataset_from_directory(
|
| 37 |
+
VAL_DIR,
|
| 38 |
+
labels="inferred",
|
| 39 |
+
label_mode="int",
|
| 40 |
+
image_size=IMAGE_SIZE,
|
| 41 |
+
batch_size=BATCH_SIZE,
|
| 42 |
+
shuffle=False,
|
| 43 |
+
)
|
| 44 |
+
|
| 45 |
+
test_ds = tf.keras.utils.image_dataset_from_directory(
|
| 46 |
+
TEST_DIR,
|
| 47 |
+
labels="inferred",
|
| 48 |
+
label_mode="int",
|
| 49 |
+
image_size=IMAGE_SIZE,
|
| 50 |
+
batch_size=BATCH_SIZE,
|
| 51 |
+
shuffle=False,
|
| 52 |
+
)
|
| 53 |
+
|
| 54 |
+
class_names = train_ds.class_names
|
| 55 |
+
|
| 56 |
+
autotune = tf.data.AUTOTUNE
|
| 57 |
+
train_ds = train_ds.prefetch(autotune)
|
| 58 |
+
val_ds = val_ds.prefetch(autotune)
|
| 59 |
+
test_ds = test_ds.prefetch(autotune)
|
| 60 |
+
|
| 61 |
+
return train_ds, val_ds, test_ds, class_names
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def compute_weights():
|
| 65 |
+
raw_train = tf.keras.utils.image_dataset_from_directory(
|
| 66 |
+
TRAIN_DIR,
|
| 67 |
+
labels="inferred",
|
| 68 |
+
label_mode="int",
|
| 69 |
+
image_size=IMAGE_SIZE,
|
| 70 |
+
batch_size=BATCH_SIZE,
|
| 71 |
+
shuffle=True,
|
| 72 |
+
seed=SEED,
|
| 73 |
+
)
|
| 74 |
+
|
| 75 |
+
labels = []
|
| 76 |
+
for _, y in raw_train.unbatch():
|
| 77 |
+
labels.append(int(y.numpy()))
|
| 78 |
+
labels = np.array(labels)
|
| 79 |
+
|
| 80 |
+
classes = np.arange(labels.max() + 1)
|
| 81 |
+
weights = compute_class_weight(
|
| 82 |
+
class_weight="balanced",
|
| 83 |
+
classes=classes,
|
| 84 |
+
y=labels,
|
| 85 |
+
)
|
| 86 |
+
|
| 87 |
+
return {int(i): float(w) for i, w in enumerate(weights)}
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def build_model(num_classes):
|
| 91 |
+
data_augmentation = tf.keras.Sequential(
|
| 92 |
+
[
|
| 93 |
+
tf.keras.layers.RandomFlip("horizontal"),
|
| 94 |
+
tf.keras.layers.RandomRotation(0.1),
|
| 95 |
+
tf.keras.layers.RandomZoom(0.1),
|
| 96 |
+
tf.keras.layers.RandomTranslation(0.05, 0.05),
|
| 97 |
+
],
|
| 98 |
+
name="data_augmentation",
|
| 99 |
+
)
|
| 100 |
+
|
| 101 |
+
rescale = tf.keras.layers.Rescaling(1.0 / 255.0, name="rescale")
|
| 102 |
+
|
| 103 |
+
base_model = tf.keras.applications.MobileNetV2(
|
| 104 |
+
input_shape=IMAGE_SIZE + (3,),
|
| 105 |
+
include_top=False,
|
| 106 |
+
weights="imagenet",
|
| 107 |
+
)
|
| 108 |
+
base_model.trainable = False
|
| 109 |
+
|
| 110 |
+
inputs = tf.keras.Input(shape=IMAGE_SIZE + (3,), name="image")
|
| 111 |
+
x = data_augmentation(inputs)
|
| 112 |
+
x = rescale(x)
|
| 113 |
+
x = base_model(x, training=False)
|
| 114 |
+
x = tf.keras.layers.GlobalAveragePooling2D()(x)
|
| 115 |
+
x = tf.keras.layers.Dropout(0.3)(x)
|
| 116 |
+
outputs = tf.keras.layers.Dense(num_classes, activation="softmax")(x)
|
| 117 |
+
|
| 118 |
+
model = tf.keras.Model(inputs, outputs, name="agrovision_mobilenetv2")
|
| 119 |
+
|
| 120 |
+
model.compile(
|
| 121 |
+
optimizer=tf.keras.optimizers.Adam(learning_rate=1e-4),
|
| 122 |
+
loss=tf.keras.losses.SparseCategoricalCrossentropy(),
|
| 123 |
+
metrics=[
|
| 124 |
+
"accuracy",
|
| 125 |
+
tf.keras.metrics.SparseTopKCategoricalAccuracy(
|
| 126 |
+
k=3, name="top3_accuracy"
|
| 127 |
+
),
|
| 128 |
+
],
|
| 129 |
+
)
|
| 130 |
+
|
| 131 |
+
return model
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
def plot_history(history, out_path):
|
| 135 |
+
acc = history.history.get("accuracy", [])
|
| 136 |
+
val_acc = history.history.get("val_accuracy", [])
|
| 137 |
+
loss = history.history.get("loss", [])
|
| 138 |
+
val_loss = history.history.get("val_loss", [])
|
| 139 |
+
|
| 140 |
+
epochs_range = range(1, len(acc) + 1)
|
| 141 |
+
|
| 142 |
+
plt.figure(figsize=(10, 4))
|
| 143 |
+
plt.subplot(1, 2, 1)
|
| 144 |
+
plt.plot(epochs_range, acc, label="train_acc")
|
| 145 |
+
plt.plot(epochs_range, val_acc, label="val_acc")
|
| 146 |
+
plt.legend()
|
| 147 |
+
plt.title("Accuracy")
|
| 148 |
+
|
| 149 |
+
plt.subplot(1, 2, 2)
|
| 150 |
+
plt.plot(epochs_range, loss, label="train_loss")
|
| 151 |
+
plt.plot(epochs_range, val_loss, label="val_loss")
|
| 152 |
+
plt.legend()
|
| 153 |
+
plt.title("Loss")
|
| 154 |
+
|
| 155 |
+
plt.tight_layout()
|
| 156 |
+
plt.savefig(out_path)
|
| 157 |
+
plt.close()
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
def evaluate_on_test(model, test_ds, class_names, out_cm_path):
|
| 161 |
+
y_true = []
|
| 162 |
+
y_pred = []
|
| 163 |
+
|
| 164 |
+
for x_batch, y_batch in test_ds:
|
| 165 |
+
preds = model.predict(x_batch, verbose=0)
|
| 166 |
+
y_true.extend(y_batch.numpy().tolist())
|
| 167 |
+
y_pred.extend(np.argmax(preds, axis=1).tolist())
|
| 168 |
+
|
| 169 |
+
y_true = np.array(y_true)
|
| 170 |
+
y_pred = np.array(y_pred)
|
| 171 |
+
|
| 172 |
+
print("\nClassification report:")
|
| 173 |
+
print(classification_report(y_true, y_pred, target_names=class_names))
|
| 174 |
+
|
| 175 |
+
cm = confusion_matrix(y_true, y_pred)
|
| 176 |
+
plt.figure(figsize=(14, 12))
|
| 177 |
+
sns.heatmap(
|
| 178 |
+
cm,
|
| 179 |
+
annot=False,
|
| 180 |
+
cmap="Blues",
|
| 181 |
+
xticklabels=class_names,
|
| 182 |
+
yticklabels=class_names,
|
| 183 |
+
)
|
| 184 |
+
plt.xlabel("Predicted")
|
| 185 |
+
plt.ylabel("True")
|
| 186 |
+
plt.xticks(rotation=90)
|
| 187 |
+
plt.yticks(rotation=0)
|
| 188 |
+
plt.tight_layout()
|
| 189 |
+
plt.savefig(out_cm_path)
|
| 190 |
+
plt.close()
|
| 191 |
+
|
| 192 |
+
|
| 193 |
+
def main():
|
| 194 |
+
train_ds, val_ds, test_ds, class_names = load_datasets()
|
| 195 |
+
num_classes = len(class_names)
|
| 196 |
+
|
| 197 |
+
(OUTPUT_DIR / "class_names.json").write_text(
|
| 198 |
+
json.dumps(class_names, indent=2),
|
| 199 |
+
encoding="utf-8",
|
| 200 |
+
)
|
| 201 |
+
|
| 202 |
+
class_weight = compute_weights()
|
| 203 |
+
model = build_model(num_classes)
|
| 204 |
+
|
| 205 |
+
checkpoint_path = OUTPUT_DIR / "agrovision_best.keras"
|
| 206 |
+
|
| 207 |
+
callbacks = [
|
| 208 |
+
tf.keras.callbacks.ModelCheckpoint(
|
| 209 |
+
filepath=str(checkpoint_path),
|
| 210 |
+
monitor="val_accuracy",
|
| 211 |
+
save_best_only=True,
|
| 212 |
+
verbose=1,
|
| 213 |
+
),
|
| 214 |
+
tf.keras.callbacks.EarlyStopping(
|
| 215 |
+
monitor="val_accuracy",
|
| 216 |
+
patience=5,
|
| 217 |
+
restore_best_weights=True,
|
| 218 |
+
verbose=1,
|
| 219 |
+
),
|
| 220 |
+
tf.keras.callbacks.ReduceLROnPlateau(
|
| 221 |
+
monitor="val_loss",
|
| 222 |
+
factor=0.5,
|
| 223 |
+
patience=3,
|
| 224 |
+
verbose=1,
|
| 225 |
+
),
|
| 226 |
+
]
|
| 227 |
+
|
| 228 |
+
history = model.fit(
|
| 229 |
+
train_ds,
|
| 230 |
+
validation_data=val_ds,
|
| 231 |
+
epochs=EPOCHS,
|
| 232 |
+
class_weight=class_weight,
|
| 233 |
+
callbacks=callbacks,
|
| 234 |
+
)
|
| 235 |
+
|
| 236 |
+
hist_path = OUTPUT_DIR / "training_curves.png"
|
| 237 |
+
plot_history(history, hist_path)
|
| 238 |
+
|
| 239 |
+
print("\nTest evaluation:")
|
| 240 |
+
test_metrics = model.evaluate(test_ds, verbose=1)
|
| 241 |
+
for name, value in zip(model.metrics_names, test_metrics):
|
| 242 |
+
print(f"{name}: {value:.4f}")
|
| 243 |
+
|
| 244 |
+
cm_path = OUTPUT_DIR / "confusion_matrix.png"
|
| 245 |
+
evaluate_on_test(model, test_ds, class_names, cm_path)
|
| 246 |
+
|
| 247 |
+
final_path = OUTPUT_DIR / "agrovision_final.keras"
|
| 248 |
+
model.save(final_path)
|
| 249 |
+
print(f"\nSaved final model to: {final_path}")
|
| 250 |
+
|
| 251 |
+
|
| 252 |
+
if __name__ == "__main__":
|
| 253 |
+
main()
|