qwfjgp2 commited on
Commit
a8ea7fe
·
verified ·
1 Parent(s): 9ff82de

Upload 46 files

Browse files
.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: Agrovision
3
- emoji: 😻
4
- colorFrom: indigo
5
- colorTo: indigo
6
- sdk: docker
7
- pinned: false
8
- ---
9
-
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
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

  • SHA256: 1d4a3f9589e3f140e3b8e94a9a312c631227b3fbe545744ce9d83ecd733a4d23
  • Pointer size: 131 Bytes
  • Size of remote file: 136 kB
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()