Pranit commited on
Commit
2c2d1c2
·
1 Parent(s): c6dd202

Initial commit

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitignore +6 -0
  2. Dockerfile +26 -0
  3. README.md +36 -5
  4. app.py +433 -0
  5. requirements.txt +5 -0
  6. templates/index.html +1584 -0
  7. test/Canteen/Screenshot 2024-12-05 195018.png +0 -0
  8. test/Canteen/Screenshot 2024-12-05 195051.png +0 -0
  9. test/Ground/IMG-20241205-WA0014.jpg +0 -0
  10. test/Ground/Screenshot 2024-12-05 195126.png +0 -0
  11. test/Ground/Screenshot 2024-12-05 195143.png +0 -0
  12. test/IMG-20241205-WA0006.jpg +0 -0
  13. test/IMG-20241205-WA0007.jpg +0 -0
  14. test/IMG-20241205-WA0008.jpg +0 -0
  15. test/IMG-20241205-WA0009.jpg +0 -0
  16. test/IMG-20241205-WA0010.jpg +0 -0
  17. test/IMG-20241205-WA0011.jpg +0 -0
  18. test/IMG-20241205-WA0012.jpg +0 -0
  19. test/IMG-20241205-WA0013.jpg +0 -0
  20. test/IMG-20241205-WA0014.jpg +0 -0
  21. test/IMG-20241205-WA0015.jpg +0 -0
  22. test/IMG-20241205-WA0016.jpg +0 -0
  23. test/IMG-20241205-WA0017.jpg +0 -0
  24. test/IMG-20241205-WA0018.jpg +0 -0
  25. test/IMG-20241205-WA0019.jpg +0 -0
  26. test/IMG-20241205-WA0020.jpg +0 -0
  27. test/IMG-20241205-WA0021.jpg +0 -0
  28. test/IMG-20241205-WA0023.jpg +0 -0
  29. test/IMG-20241205-WA0024.jpg +0 -0
  30. test/IMG-20241205-WA0025.jpg +0 -0
  31. test/IMG-20241205-WA0027.jpg +0 -0
  32. test/IMG-20241205-WA0029.jpg +0 -0
  33. test/IMG-20241205-WA0030.jpg +0 -0
  34. test/IMG-20241205-WA0031.jpg +0 -0
  35. test/IMG-20241205-WA0032.jpg +0 -0
  36. test/IMG-20241205-WA0034.jpg +0 -0
  37. test/IMG-20241205-WA0035.jpg +0 -0
  38. test/IMG-20241205-WA0036.jpg +0 -0
  39. test/IMG-20241205-WA0037.jpg +0 -0
  40. test/IMG-20241205-WA0038.jpg +0 -0
  41. test/Parking/IMG-20241205-WA0031.jpg +0 -0
  42. test/Parking/IMG-20241205-WA0032.jpg +0 -0
  43. test/Parking/IMG-20241205-WA0037.jpg +0 -0
  44. test/Road/IMG-20241205-WA0032.jpg +0 -0
  45. test/Road/Screenshot 2024-12-05 194245 - Copy.png +0 -0
  46. test/Road/Screenshot 2024-12-05 194245.png +0 -0
  47. test/Road/Screenshot 2024-12-05 194600 - Copy.png +0 -0
  48. test/Road/Screenshot 2024-12-05 194600.png +0 -0
  49. test/Road/Screenshot 2024-12-05 194820 - Copy.png +0 -0
  50. test/Road/Screenshot 2024-12-05 194820.png +0 -0
.gitignore ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ __pycache__/
2
+ *.pyc
3
+ .env
4
+ .venv
5
+ venv/
6
+ ENV/
Dockerfile ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.9-slim
2
+
3
+ WORKDIR /code
4
+
5
+ # Install system dependencies for WeasyPrint
6
+ RUN apt-get update && apt-get install -y \
7
+ build-essential \
8
+ python3-dev \
9
+ python3-pip \
10
+ python3-setuptools \
11
+ python3-wheel \
12
+ python3-cffi \
13
+ libcairo2 \
14
+ libpango-1.0-0 \
15
+ libpangocairo-1.0-0 \
16
+ libgdk-pixbuf2.0-0 \
17
+ libffi-dev \
18
+ shared-mime-info \
19
+ && rm -rf /var/lib/apt/lists/*
20
+
21
+ COPY requirements.txt .
22
+ RUN pip install --no-cache-dir -r requirements.txt
23
+
24
+ COPY . .
25
+
26
+ CMD ["gunicorn", "--bind", "0.0.0.0:7860", "app:app"]
README.md CHANGED
@@ -1,10 +1,41 @@
1
  ---
2
- title: Campus Inspection
3
- emoji: 🐨
4
- colorFrom: green
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: Campus Inspection Report Generator
3
+ emoji: 🏫
4
+ colorFrom: blue
5
+ colorTo: green
6
  sdk: docker
7
  pinned: false
8
  ---
9
 
10
+ # Campus Inspection Report Generator
11
+
12
+ This application generates detailed campus inspection reports using Google's Gemini AI model. Upload campus images and get comprehensive analysis including:
13
+
14
+ - Infrastructure assessment
15
+ - Safety evaluation
16
+ - Environmental analysis
17
+ - Detailed recommendations
18
+ - PDF report generation
19
+
20
+ ## Setup
21
+
22
+ 1. Clone the repository
23
+ 2. Set up environment variables:
24
+ ```bash
25
+ GOOGLE_API_KEY=your_gemini_api_key
26
+ ```
27
+ 3. Install dependencies:
28
+ ```bash
29
+ pip install -r requirements.txt
30
+ ```
31
+ 4. Run the application:
32
+ ```bash
33
+ python app.py
34
+ ```
35
+
36
+ ## Features
37
+
38
+ - Multi-image analysis
39
+ - Detailed report generation
40
+ - PDF export functionality
41
+ - Interactive web interface
app.py ADDED
@@ -0,0 +1,433 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, render_template, request, jsonify, send_file
2
+ import google.generativeai as genai
3
+ import base64
4
+ import logging
5
+ from weasyprint import HTML
6
+ import os
7
+ from datetime import datetime
8
+ import tempfile
9
+ from io import BytesIO
10
+ import jinja2
11
+ from dotenv import load_dotenv
12
+
13
+ app = Flask(__name__)
14
+
15
+ # Configure logging
16
+ logging.basicConfig(level=logging.DEBUG)
17
+ logger = logging.getLogger(__name__)
18
+
19
+ # Configure Gemini API
20
+ load_dotenv()
21
+ genai.configure(api_key=os.getenv('GOOGLE_API_KEY'))
22
+ # Use gemini-pro-vision model for image processing
23
+ model = genai.GenerativeModel("gemini-1.5-flash")
24
+
25
+ # Updated prompt for dual-format report
26
+ prompt = """You are a professional campus facility inspector with over 15 years of experience in infrastructure assessment in India. Analyze the campus with total area of ${college_area} acres. Generate two reports based on the provided campus images:
27
+ + The campus includes ${ground_count} grounds with a total area of ${ground_area} acres.
28
+
29
+ REPORT 1: EXECUTIVE SUMMARY TABLES
30
+
31
+ Table 1: Campus Overview
32
+ | Aspect | Grade | Key Observations | Priority Level |
33
+ |--------|-------|-----------------|----------------|
34
+ | Overall Infrastructure | (A+/A/B+/B/C) | • Key points | High/Medium/Low |
35
+ | Buildings | (A+/A/B+/B/C) | • Key points | High/Medium/Low |
36
+ | Roads & Parking | (A+/A/B+/B/C) | • Key points | High/Medium/Low |
37
+ | Sports Grounds (${ground_area} acres) | (A+/A/B+/B/C) | • Key points | High/Medium/Low |
38
+ | Canteens | (A+/A/B+/B/C) | • Key points | High/Medium/Low |
39
+ | Entry/Exit & Security | (A+/A/B+/B/C) | • Key points | High/Medium/Low |
40
+ | Environmental | (A+/A/B+/B/C) | • Key points | High/Medium/Low |
41
+
42
+ Table 2: Critical Issues and Solutions
43
+ | Area | Issue | Proposed Solution/Measures |
44
+ |------|-------|---------------------------|
45
+ | Area 1 | Description of issue | • Detailed solution steps |
46
+ | Area 2 | Description of issue | • Detailed solution steps |
47
+
48
+ REPORT 2: DETAILED ASSESSMENT
49
+
50
+ 1. Overall Campus Analysis
51
+ A. Infrastructure Overview
52
+ - General campus layout and planning
53
+ - Common areas and circulation
54
+ - Campus-wide systems (drainage, lighting)
55
+ - Shared facilities condition
56
+
57
+ B. Safety & Security Assessment
58
+ - Boundary security
59
+ - Emergency systems
60
+ - Lighting and surveillance
61
+ - Fire safety measures
62
+
63
+ C. Environmental Analysis
64
+ - Green spaces and landscaping
65
+ - Water management
66
+ - Waste management
67
+ - Natural lighting and ventilation
68
+
69
+ D. Accessibility & Connectivity
70
+ - Internal roads and pathways
71
+ - Parking facilities
72
+ - Emergency access
73
+ - Campus connectivity
74
+
75
+ 2. Area-wise Assessment
76
+ A. Buildings
77
+ - Overall condition
78
+ - Key features
79
+ - Notable issues
80
+ - Maintenance status
81
+
82
+ B. Roads & Parking
83
+ - Surface condition
84
+ - Traffic flow
85
+ - Parking adequacy
86
+ - Safety features
87
+
88
+ C. Sports Facilities
89
+ - Ground conditions
90
+ - Equipment status
91
+ - Safety measures
92
+ - Maintenance level
93
+
94
+ D. Canteens
95
+ - Structure condition
96
+ - Hygiene standards
97
+ - Ventilation
98
+ - Seating capacity
99
+
100
+ E. Entry/Exit Points
101
+ - Security measures
102
+ - Traffic management
103
+ - Access control
104
+ - Emergency preparedness
105
+
106
+ 3. Campus Strengths
107
+ A. Infrastructure Strengths
108
+ - Notable features
109
+ - Well-maintained areas
110
+ - Effective systems
111
+ - Best practices observed
112
+
113
+ B. Enhancement Potential
114
+ - Areas showing excellence
115
+ - Opportunities for showcase
116
+ - Positive aspects to build upon
117
+ - Innovative features
118
+
119
+ 4. Areas of Concern & Recommendations
120
+ A. Critical Issues
121
+ - Infrastructure gaps
122
+ - Safety concerns
123
+ - Maintenance needs
124
+ - Operational challenges
125
+
126
+ B. Improvement Measures
127
+ - Specific solutions
128
+ - Practical steps
129
+ - Enhancement strategies
130
+ - Preventive measures
131
+
132
+ 5. Final Assessment
133
+ A. Overall Grade: [A+/A/B+/B/C]
134
+ Brief justification of the grade based on:
135
+ - Infrastructure quality
136
+ - Maintenance standards
137
+ - Safety measures
138
+ - Environmental aspects
139
+
140
+ B. Concluding Remarks
141
+ - Key takeaways
142
+ - Critical focus areas
143
+ - Positive highlights
144
+ - Path forward
145
+
146
+ Please focus on significant issues only and ignore minor cosmetic concerns. Consider local weather patterns (monsoon, summer) and regional building practices. Present information in clear, concise formats."""
147
+
148
+ @app.route('/')
149
+ def index():
150
+ return render_template('index.html')
151
+
152
+ def calculate_grade(grades, weights):
153
+ total_weight = sum(weights.values())
154
+ weighted_score = sum(grades[aspect] * weights[aspect] for aspect in grades)
155
+ average_score = weighted_score / total_weight
156
+ # Convert average score to a grade
157
+ if average_score >= 90:
158
+ return 'A+'
159
+ elif average_score >= 80:
160
+ return 'A'
161
+ elif average_score >= 70:
162
+ return 'B+'
163
+ elif average_score >= 60:
164
+ return 'B'
165
+ else:
166
+ return 'C'
167
+
168
+ def extract_grades(report_text):
169
+ # Dummy implementation for extracting grades from report text
170
+ # This should be replaced with actual logic to parse the report
171
+ return {
172
+ 'Overall Infrastructure': 85,
173
+ 'Buildings': 80,
174
+ 'Roads & Parking': 75,
175
+ 'Sports Facilities': 70,
176
+ 'Canteens': 65,
177
+ 'Entry/Exit & Security': 80,
178
+ 'Environmental': 90
179
+ }
180
+
181
+ def extract_priority_distribution(report_text):
182
+ """Extract priority distribution from the report text"""
183
+ try:
184
+ # Count priority levels from the Overview table
185
+ priorities = {
186
+ 'High': 0,
187
+ 'Medium': 0,
188
+ 'Low': 0
189
+ }
190
+
191
+ # Look for priority levels in the Overview table
192
+ lines = report_text.split('\n')
193
+ for line in lines:
194
+ if '|' in line: # Table row
195
+ if 'High' in line:
196
+ priorities['High'] += 1
197
+ elif 'Medium' in line:
198
+ priorities['Medium'] += 1
199
+ elif 'Low' in line:
200
+ priorities['Low'] += 1
201
+
202
+ return [priorities['High'], priorities['Medium'], priorities['Low']]
203
+ except Exception as e:
204
+ logger.error(f"Error extracting priority distribution: {str(e)}")
205
+ return [30, 45, 25] # Default values if extraction fails
206
+
207
+ def extract_area_performance(report_text):
208
+ """Extract performance scores for different areas"""
209
+ try:
210
+ # Extract scores from the Overview table
211
+ areas = {
212
+ 'Buildings': 0,
213
+ 'Roads & Parking': 0,
214
+ 'Sports Facilities': 0,
215
+ 'Canteens': 0,
216
+ 'Security': 0,
217
+ 'Environmental': 0
218
+ }
219
+
220
+ lines = report_text.split('\n')
221
+ for line in lines:
222
+ if '|' in line: # Table row
223
+ parts = line.split('|')
224
+ if len(parts) >= 2:
225
+ area = parts[1].strip()
226
+ if area in areas:
227
+ # Convert grade to numeric score
228
+ grade = parts[2].strip() if len(parts) > 2 else ''
229
+ score = {
230
+ 'A+': 95,
231
+ 'A': 85,
232
+ 'B+': 75,
233
+ 'B': 65,
234
+ 'C': 55
235
+ }.get(grade, 70)
236
+ areas[area] = score
237
+
238
+ return list(areas.values())
239
+ except Exception as e:
240
+ logger.error(f"Error extracting area performance: {str(e)}")
241
+ return [85, 75, 70, 80, 90, 85] # Default values if extraction fails
242
+
243
+ def extract_maintenance_status(report_text):
244
+ """Extract maintenance status distribution"""
245
+ try:
246
+ status = {
247
+ 'Well Maintained': 0,
248
+ 'Needs Attention': 0,
249
+ 'Critical': 0
250
+ }
251
+
252
+ # Count maintenance status mentions in the report
253
+ well_maintained = report_text.lower().count('well maintained')
254
+ needs_attention = report_text.lower().count('needs attention') + report_text.lower().count('requires attention')
255
+ critical = report_text.lower().count('critical') + report_text.lower().count('urgent')
256
+
257
+ total = well_maintained + needs_attention + critical or 1 # Avoid division by zero
258
+
259
+ return [
260
+ int(well_maintained * 100 / total),
261
+ int(needs_attention * 100 / total),
262
+ int(critical * 100 / total)
263
+ ]
264
+ except Exception as e:
265
+ logger.error(f"Error extracting maintenance status: {str(e)}")
266
+ return [60, 30, 10] # Default values if extraction fails
267
+
268
+ @app.route('/generate_report', methods=['POST'])
269
+ def generate_report():
270
+ try:
271
+ data = request.json
272
+ images = data.get('images', [])
273
+ basic_info = data.get('basicInfo', {})
274
+
275
+ # Add file size validation
276
+ MAX_FILE_SIZE = 4 * 1024 * 1024 # 4MB
277
+ for image_data in images:
278
+ if len(base64.b64decode(image_data['data'].split(',')[1])) > MAX_FILE_SIZE:
279
+ return jsonify({'error': 'Image file size exceeds 4MB limit'}), 400
280
+
281
+ # Create image context with numbering
282
+ image_contexts = []
283
+ all_images = []
284
+ image_count = 1
285
+
286
+ for image_data in images:
287
+ if image_data['data'].startswith('data:image'):
288
+ image_data['data'] = image_data['data'].split(',')[1]
289
+ image_bytes = base64.b64decode(image_data['data'])
290
+
291
+ # Store image data for the report
292
+ image_context = {
293
+ 'number': image_count,
294
+ 'category': image_data['category'],
295
+ 'data': image_bytes,
296
+ 'mime_type': 'image/jpeg'
297
+ }
298
+ image_contexts.append(image_context)
299
+
300
+ # Add to list for API
301
+ all_images.append({
302
+ "mime_type": "image/jpeg",
303
+ "data": image_bytes
304
+ })
305
+
306
+ image_count += 1
307
+
308
+ # Update prompt with actual values
309
+ contextualized_prompt = prompt.replace('${college_area}', str(basic_info.get('collegeArea', '')))
310
+ contextualized_prompt = contextualized_prompt.replace('${building_count}', str(basic_info.get('buildingCount', '')))
311
+ contextualized_prompt = contextualized_prompt.replace('${parking_area}', str(basic_info.get('parkingCount', '')))
312
+ contextualized_prompt = contextualized_prompt.replace('${canteen_count}', str(basic_info.get('canteenCount', '')))
313
+ contextualized_prompt = contextualized_prompt.replace('${ground_count}', str(basic_info.get('groundCount', '')))
314
+ contextualized_prompt = contextualized_prompt.replace('${ground_area}', str(basic_info.get('groundArea', '')))
315
+ contextualized_prompt = contextualized_prompt.replace('${gate_count}', str(basic_info.get('gateCount', '')))
316
+
317
+ # Generate report with image references
318
+ response = model.generate_content([contextualized_prompt] + all_images)
319
+ report_text = response.text
320
+
321
+ # Extract all metrics
322
+ grades = extract_grades(report_text)
323
+ priority_distribution = extract_priority_distribution(report_text)
324
+ area_performance = extract_area_performance(report_text)
325
+ maintenance_status = extract_maintenance_status(report_text)
326
+
327
+ # Calculate overall grade
328
+ weights = {
329
+ 'Overall Infrastructure': 0.2,
330
+ 'Buildings': 0.2,
331
+ 'Roads & Parking': 0.15,
332
+ 'Sports Facilities': 0.1,
333
+ 'Canteens': 0.1,
334
+ 'Entry/Exit & Security': 0.15,
335
+ 'Environmental': 0.1
336
+ }
337
+ overall_grade = calculate_grade(grades, weights)
338
+
339
+ # Add metrics to the response
340
+ return jsonify({
341
+ 'report': report_text,
342
+ 'overallGrade': overall_grade,
343
+ 'metrics': {
344
+ 'priorityDistribution': priority_distribution,
345
+ 'areaPerformance': area_performance,
346
+ 'maintenanceStatus': maintenance_status
347
+ },
348
+ 'images': [{
349
+ 'number': img['number'],
350
+ 'category': img['category'],
351
+ 'data': base64.b64encode(img['data']).decode('utf-8')
352
+ } for img in image_contexts]
353
+ })
354
+
355
+ except Exception as e:
356
+ logger.error(f"Error generating report: {str(e)}")
357
+ return jsonify({'error': str(e)}), 500
358
+
359
+ @app.route('/download_pdf', methods=['POST'])
360
+ def download_pdf():
361
+ pdf_buffer = None
362
+ try:
363
+ logger.debug("Received PDF download request")
364
+ html_content = request.json.get('html')
365
+
366
+ if not html_content:
367
+ raise ValueError("No HTML content provided")
368
+
369
+ logger.debug("Converting HTML to PDF")
370
+ # Create PDF in memory
371
+ pdf_buffer = BytesIO()
372
+ HTML(string=html_content).write_pdf(pdf_buffer)
373
+ pdf_buffer.seek(0)
374
+
375
+ logger.debug("Sending PDF file")
376
+ # Create a copy of the buffer contents
377
+ pdf_data = pdf_buffer.getvalue()
378
+
379
+ # Close the original buffer
380
+ if pdf_buffer:
381
+ pdf_buffer.close()
382
+
383
+ # Create a new buffer with the copied data
384
+ return_buffer = BytesIO(pdf_data)
385
+
386
+ return send_file(
387
+ return_buffer,
388
+ mimetype='application/pdf',
389
+ as_attachment=True,
390
+ download_name=f'campus-inspection-report-{datetime.now().strftime("%Y%m%d")}.pdf'
391
+ )
392
+
393
+ except Exception as e:
394
+ logger.error(f"Error generating PDF: {str(e)}")
395
+ return jsonify({'error': str(e)}), 500
396
+ finally:
397
+ # Clean up the original buffer if it exists
398
+ if pdf_buffer:
399
+ pdf_buffer.close()
400
+
401
+ def generate_report(findings, inspector_name, location, weather):
402
+ # Format timestamp
403
+ timestamp = datetime.now().strftime("%B %d, %Y at %I:%M %p")
404
+
405
+ # Ensure findings have all required fields and proper formatting
406
+ formatted_findings = []
407
+ for finding in findings:
408
+ formatted_finding = {
409
+ 'title': finding.get('title', 'Untitled Finding'),
410
+ 'description': finding.get('description', 'No description provided'),
411
+ 'severity': finding.get('severity', 'Medium').capitalize(),
412
+ 'recommendation': finding.get('recommendation', 'No recommendation provided'),
413
+ 'image_path': finding.get('image_path', None)
414
+ }
415
+ formatted_findings.append(formatted_finding)
416
+
417
+ # Load and render template
418
+ template_loader = jinja2.FileSystemLoader('.')
419
+ template_env = jinja2.Environment(loader=template_loader)
420
+ template = template_env.get_template('campus-inspection-report.html')
421
+
422
+ html_content = template.render(
423
+ timestamp=timestamp,
424
+ inspector_name=inspector_name,
425
+ location=location,
426
+ weather=weather,
427
+ findings=formatted_findings
428
+ )
429
+
430
+ return html_content
431
+
432
+ if __name__ == '__main__':
433
+ app.run(host='0.0.0.0', port=7860)
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ flask==2.0.1
2
+ google-generativeai==0.3.2
3
+ weasyprint==60.1
4
+ Jinja2==3.0.1
5
+ gunicorn==21.2.0
templates/index.html ADDED
@@ -0,0 +1,1584 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>Campus Inspection Report Generator</title>
7
+ <!-- Bootstrap 5 CSS -->
8
+ <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
9
+ <!-- Font Awesome -->
10
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
11
+ <style>
12
+ :root {
13
+ --bs-primary: #2563eb;
14
+ --bs-primary-rgb: 37, 99, 235;
15
+ --transition: all 0.3s ease;
16
+ }
17
+
18
+ body {
19
+ background-color: #f8fafc;
20
+ color: #1e293b;
21
+ }
22
+
23
+ .navbar {
24
+ background-color: white;
25
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1);
26
+ padding: 1rem 0;
27
+ }
28
+
29
+ .navbar-brand {
30
+ color: var(--bs-primary);
31
+ font-weight: 600;
32
+ }
33
+
34
+ .card {
35
+ background: white;
36
+ border-radius: 1rem;
37
+ transition: var(--transition);
38
+ }
39
+
40
+ .card:hover {
41
+ transform: translateY(-2px);
42
+ }
43
+
44
+ .form-floating > .form-control {
45
+ border-radius: 0.75rem;
46
+ border: 1px solid #e2e8f0;
47
+ padding: 1rem;
48
+ }
49
+
50
+ .form-floating > .form-control:focus {
51
+ border-color: var(--bs-primary);
52
+ box-shadow: 0 0 0 4px rgba(var(--bs-primary-rgb), 0.1);
53
+ }
54
+
55
+ .upload-area {
56
+ padding: 3rem;
57
+ border: 2px dashed #e2e8f0;
58
+ background-color: #f8fafc;
59
+ transition: var(--transition);
60
+ }
61
+
62
+ .upload-area.dragover {
63
+ border-color: var(--bs-primary);
64
+ background-color: rgba(var(--bs-primary-rgb), 0.05);
65
+ }
66
+
67
+ .btn-primary {
68
+ background-color: var(--bs-primary);
69
+ border: none;
70
+ font-weight: 500;
71
+ transition: var(--transition);
72
+ }
73
+
74
+ .btn-primary:hover {
75
+ background-color: #1d4ed8;
76
+ transform: translateY(-1px);
77
+ }
78
+
79
+ .image-preview {
80
+ display: grid;
81
+ grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
82
+ gap: 1rem;
83
+ }
84
+
85
+ .image-container {
86
+ position: relative;
87
+ border-radius: 0.75rem;
88
+ overflow: hidden;
89
+ aspect-ratio: 1;
90
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
91
+ transition: var(--transition);
92
+ }
93
+
94
+ .image-container img {
95
+ width: 100%;
96
+ height: 100%;
97
+ object-fit: cover;
98
+ }
99
+
100
+ .remove-image {
101
+ position: absolute;
102
+ top: 0.5rem;
103
+ right: 0.5rem;
104
+ background: rgba(255, 255, 255, 0.9);
105
+ border: none;
106
+ border-radius: 50%;
107
+ width: 28px;
108
+ height: 28px;
109
+ cursor: pointer;
110
+ display: flex;
111
+ align-items: center;
112
+ justify-content: center;
113
+ color: #ef4444;
114
+ transition: var(--transition);
115
+ }
116
+
117
+ .remove-image:hover {
118
+ background: #ef4444;
119
+ color: white;
120
+ }
121
+
122
+ .alert-container {
123
+ position: fixed;
124
+ top: 20px;
125
+ left: 50%;
126
+ transform: translateX(-50%);
127
+ z-index: 1050;
128
+ min-width: 300px;
129
+ max-width: 600px;
130
+ }
131
+
132
+ .alert {
133
+ border-radius: 0.75rem;
134
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
135
+ }
136
+
137
+ .loading-animation {
138
+ display: flex;
139
+ justify-content: center;
140
+ gap: 0.5rem;
141
+ }
142
+
143
+ .hidden {
144
+ display: none !important;
145
+ }
146
+
147
+ .report-content {
148
+ animation: fadeIn 0.5s ease-out;
149
+ }
150
+
151
+ @keyframes fadeIn {
152
+ from { opacity: 0; transform: translateY(20px); }
153
+ to { opacity: 1; transform: translateY(0); }
154
+ }
155
+
156
+ /* Responsive adjustments */
157
+ @media (max-width: 768px) {
158
+ .container {
159
+ padding-left: 1rem;
160
+ padding-right: 1rem;
161
+ }
162
+
163
+ .upload-area {
164
+ padding: 2rem 1rem;
165
+ }
166
+ }
167
+
168
+ /* Add styles for image labels */
169
+ .image-container {
170
+ position: relative;
171
+ border-radius: 0.75rem;
172
+ overflow: hidden;
173
+ aspect-ratio: 1;
174
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
175
+ transition: var(--transition);
176
+ }
177
+
178
+ .image-label {
179
+ position: absolute;
180
+ bottom: 0.5rem;
181
+ left: 0.5rem;
182
+ background: rgba(255, 255, 255, 0.9);
183
+ padding: 0.25rem 0.5rem;
184
+ border-radius: 0.25rem;
185
+ font-size: 0.875rem;
186
+ font-weight: 500;
187
+ color: var(--bs-primary);
188
+ }
189
+
190
+ .image-container:hover .image-label {
191
+ background: var(--bs-primary);
192
+ color: white;
193
+ }
194
+
195
+ .chart-container {
196
+ background: white;
197
+ border-radius: 8px;
198
+ padding: 20px;
199
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
200
+ margin-bottom: 20px;
201
+ }
202
+
203
+ .charts-section {
204
+ margin-bottom: 2rem;
205
+ }
206
+
207
+ .row {
208
+ display: flex;
209
+ flex-wrap: wrap;
210
+ margin: -15px;
211
+ }
212
+
213
+ .col-md-6 {
214
+ width: 50%;
215
+ padding: 15px;
216
+ }
217
+
218
+ .col-md-12 {
219
+ width: 100%;
220
+ padding: 15px;
221
+ }
222
+
223
+ @media (max-width: 768px) {
224
+ .col-md-6 {
225
+ width: 100%;
226
+ }
227
+ }
228
+ </style>
229
+ <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
230
+ <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
231
+ </head>
232
+ <body class="bg-light">
233
+ <!-- Navbar -->
234
+ <nav class="navbar navbar-expand-lg">
235
+ <div class="container">
236
+ <a class="navbar-brand d-flex align-items-center" href="#">
237
+ <i class="fas fa-building-columns me-2"></i>
238
+ Campus Inspector AI
239
+ </a>
240
+ </div>
241
+ </nav>
242
+
243
+ <div class="alert-container" id="alertContainer"></div>
244
+
245
+ <div class="container py-4">
246
+ <!-- Header Section -->
247
+ <div class="text-center mb-5">
248
+ <h1 class="display-5 fw-bold text-primary mb-3">Campus Inspection Report Generator</h1>
249
+ <p class="lead text-muted">Generate comprehensive inspection reports using AI analysis of campus images</p>
250
+ </div>
251
+
252
+ <div class="row g-4">
253
+ <!-- Upload Section -->
254
+ <div class="col-lg-6">
255
+ <div class="card shadow-sm border-0 h-100">
256
+ <div class="card-body p-4">
257
+ <!-- Basic Information -->
258
+ <div class="basic-info mb-4">
259
+ <h5 class="card-title mb-4">
260
+ <i class="fas fa-info-circle text-primary me-2"></i>
261
+ Basic Information
262
+ </h5>
263
+ <div class="row g-3">
264
+ <div class="col-md-12">
265
+ <div class="form-floating">
266
+ <input type="text" class="form-control" id="collegeName" placeholder="College Name">
267
+ <label for="collegeName">College Name</label>
268
+ </div>
269
+ </div>
270
+ <div class="col-md-6">
271
+ <div class="form-floating">
272
+ <input type="text" class="form-control" id="location" placeholder="Location">
273
+ <label for="location">Location</label>
274
+ </div>
275
+ </div>
276
+ <div class="col-md-6">
277
+ <div class="form-floating">
278
+ <input type="date" class="form-control" id="inspectionDate">
279
+ <label for="inspectionDate">Inspection Date</label>
280
+ </div>
281
+ </div>
282
+ </div>
283
+ </div>
284
+
285
+ <!-- Campus Metrics -->
286
+ <div class="campus-metrics mb-4">
287
+ <h5 class="card-title mb-4">
288
+ <i class="fas fa-ruler-combined text-primary me-2"></i>
289
+ Campus Metrics
290
+ </h5>
291
+ <div class="row g-3">
292
+ <div class="col-md-6">
293
+ <div class="form-floating">
294
+ <input type="number" class="form-control" id="collegeArea" min="1" step="0.1" required>
295
+ <label for="collegeArea">Total Campus Area (acres)</label>
296
+ </div>
297
+ </div>
298
+ <div class="col-md-6">
299
+ <div class="form-floating">
300
+ <input type="number" class="form-control" id="buildingCount" min="1" required>
301
+ <label for="buildingCount">Number of Buildings</label>
302
+ <small class="text-muted">Minimum 5 external images per building required</small>
303
+ </div>
304
+ </div>
305
+ <div class="col-md-6">
306
+ <div class="form-floating">
307
+ <input type="number" class="form-control" id="parkingCount" min="1" required>
308
+ <label for="parkingCount">Number of Parking Areas</label>
309
+ <small class="text-muted">Minimum 3 images per parking area required</small>
310
+ </div>
311
+ </div>
312
+ <div class="col-md-6">
313
+ <div class="form-floating">
314
+ <input type="number" class="form-control" id="canteenCount" min="0" required>
315
+ <label for="canteenCount">Number of Canteens</label>
316
+ <small class="text-muted">Minimum 2 images per canteen required</small>
317
+ </div>
318
+ </div>
319
+ <div class="col-md-6">
320
+ <div class="form-floating">
321
+ <input type="number" class="form-control" id="groundCount" min="0" required>
322
+ <label for="groundCount">Number of Grounds</label>
323
+ <small class="text-muted">Minimum 3 images per ground required</small>
324
+ </div>
325
+ </div>
326
+ <div class="col-md-6">
327
+ <div class="form-floating">
328
+ <input type="number" class="form-control" id="groundArea" min="0" step="0.1" required>
329
+ <label for="groundArea">Total Ground Area (acres)</label>
330
+ <small class="text-muted">Combined area of all grounds</small>
331
+ </div>
332
+ </div>
333
+ <div class="col-md-6">
334
+ <div class="form-floating">
335
+ <input type="number" class="form-control" id="gateCount" min="1" required>
336
+ <label for="gateCount">Number of Entry/Exit Gates</label>
337
+ <small class="text-muted">Minimum 2 images per gate required</small>
338
+ </div>
339
+ </div>
340
+ </div>
341
+ </div>
342
+
343
+ <!-- Buildings -->
344
+ <div class="upload-sections mb-4">
345
+ <h5 class="card-title mb-4">
346
+ <i class="fas fa-images text-primary me-2"></i>
347
+ Upload Images
348
+ </h5>
349
+
350
+ <!-- Buildings Upload -->
351
+ <div class="upload-category mb-4">
352
+ <h6 class="mb-3">Building Images</h6>
353
+ <small class="text-muted d-block mb-2">Minimum 5 external images per building required</small>
354
+ <div class="upload-area rounded-4" id="building-drop-zone">
355
+ <input type="file" id="building-images" class="building-images" accept="image/*" multiple hidden>
356
+ <div class="upload-content">
357
+ <i class="fas fa-building display-4 text-primary mb-3"></i>
358
+ <button class="btn btn-primary px-4 rounded-pill" onclick="document.getElementById('building-images').click()">
359
+ Upload Building Images
360
+ </button>
361
+ </div>
362
+ </div>
363
+ <div id="building-preview" class="image-preview mt-3"></div>
364
+ </div>
365
+
366
+ <!-- Parking Upload -->
367
+ <div class="upload-category mb-4">
368
+ <h6 class="mb-3">Parking Area Images</h6>
369
+ <small class="text-muted d-block mb-2">Minimum 3 images per parking area required</small>
370
+ <div class="upload-area rounded-4" id="parking-drop-zone">
371
+ <input type="file" id="parking-images" accept="image/*" multiple hidden>
372
+ <div class="upload-content">
373
+ <i class="fas fa-parking display-4 text-primary mb-3"></i>
374
+ <button class="btn btn-primary px-4 rounded-pill" onclick="document.getElementById('parking-images').click()">
375
+ Upload Parking Images
376
+ </button>
377
+ </div>
378
+ </div>
379
+ <div id="parking-preview" class="image-preview mt-3"></div>
380
+ </div>
381
+
382
+ <!-- Roads Upload -->
383
+ <div class="upload-category mb-4">
384
+ <h6 class="mb-3">Road Images</h6>
385
+ <small class="text-muted d-block mb-2">Minimum 7 road images required</small>
386
+ <div class="upload-area rounded-4" id="road-drop-zone">
387
+ <input type="file" id="road-images" accept="image/*" multiple hidden>
388
+ <div class="upload-content">
389
+ <i class="fas fa-road display-4 text-primary mb-3"></i>
390
+ <button class="btn btn-primary px-4 rounded-pill" onclick="document.getElementById('road-images').click()">
391
+ Upload Road Images
392
+ </button>
393
+ </div>
394
+ </div>
395
+ <div id="road-preview" class="image-preview mt-3"></div>
396
+ </div>
397
+
398
+ <!-- Canteen Upload -->
399
+ <div class="upload-category mb-4">
400
+ <h6 class="mb-3">Canteen Images</h6>
401
+ <small class="text-muted d-block mb-2">Minimum 2 images per canteen required</small>
402
+ <div class="upload-area rounded-4" id="canteen-drop-zone">
403
+ <input type="file" id="canteen-images" accept="image/*" multiple hidden>
404
+ <div class="upload-content">
405
+ <i class="fas fa-utensils display-4 text-primary mb-3"></i>
406
+ <button class="btn btn-primary px-4 rounded-pill" onclick="document.getElementById('canteen-images').click()">
407
+ Upload Canteen Images
408
+ </button>
409
+ </div>
410
+ </div>
411
+ <div id="canteen-preview" class="image-preview mt-3"></div>
412
+ </div>
413
+
414
+ <!-- Ground Upload -->
415
+ <div class="upload-category mb-4">
416
+ <h6 class="mb-3">Ground Images</h6>
417
+ <small class="text-muted d-block mb-2">Minimum 3 images per ground required</small>
418
+ <div class="upload-area rounded-4" id="ground-drop-zone">
419
+ <input type="file" id="ground-images" accept="image/*" multiple hidden>
420
+ <div class="upload-content">
421
+ <i class="fas fa-futbol display-4 text-primary mb-3"></i>
422
+ <button class="btn btn-primary px-4 rounded-pill" onclick="document.getElementById('ground-images').click()">
423
+ Upload Ground Images
424
+ </button>
425
+ </div>
426
+ </div>
427
+ <div id="ground-preview" class="image-preview mt-3"></div>
428
+ </div>
429
+
430
+ <!-- Gates Upload -->
431
+ <div class="upload-category mb-4">
432
+ <h6 class="mb-3">Entry/Exit Gate Images</h6>
433
+ <small class="text-muted d-block mb-2">Minimum 2 images per gate required</small>
434
+ <div class="upload-area rounded-4" id="gate-drop-zone">
435
+ <input type="file" id="gate-images" accept="image/*" multiple hidden>
436
+ <div class="upload-content">
437
+ <i class="fas fa-door-open display-4 text-primary mb-3"></i>
438
+ <button class="btn btn-primary px-4 rounded-pill" onclick="document.getElementById('gate-images').click()">
439
+ Upload Gate Images
440
+ </button>
441
+ </div>
442
+ </div>
443
+ <div id="gate-preview" class="image-preview mt-3"></div>
444
+ </div>
445
+ </div>
446
+
447
+ <div class="mt-4">
448
+ <button class="btn btn-primary w-100 py-3 rounded-pill" onclick="generateReport()">
449
+ <i class="fas fa-wand-magic-sparkles me-2"></i>
450
+ Generate Comprehensive Report
451
+ </button>
452
+ </div>
453
+ </div>
454
+ </div>
455
+ </div>
456
+
457
+ <!-- Report Section -->
458
+ <div class="col-lg-6">
459
+ <div class="card shadow-sm border-0 h-100">
460
+ <div class="card-body p-4">
461
+ <div class="d-flex justify-content-between align-items-center mb-4">
462
+ <h5 class="card-title mb-0">
463
+ <i class="fas fa-file-lines text-primary me-2"></i>
464
+ Generated Report
465
+ </h5>
466
+ <div id="downloadBtn" class="btn-group">
467
+ <!-- Will be populated by JavaScript -->
468
+ </div>
469
+ </div>
470
+
471
+ <div id="loading-indicator" class="text-center py-5 hidden">
472
+ <div class="loading-animation mb-4">
473
+ <div class="spinner-grow text-primary" role="status">
474
+ <span class="visually-hidden">Loading...</span>
475
+ </div>
476
+ </div>
477
+ <h5 class="text-primary mb-3">Analyzing Campus Images</h5>
478
+ <p class="text-muted">Our AI is examining your campus images and generating a detailed report...</p>
479
+ <div class="progress" style="height: 4px;">
480
+ <div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" style="width: 100%"></div>
481
+ </div>
482
+ </div>
483
+
484
+ <div id="report-output" class="report-content"></div>
485
+ </div>
486
+ </div>
487
+ </div>
488
+ </div>
489
+ </div>
490
+
491
+ <!-- Bootstrap JS Bundle -->
492
+ <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
493
+ <script>
494
+ let buildingCounter = 1;
495
+ let reportData = null;
496
+
497
+ function addNewBuilding() {
498
+ buildingCounter++;
499
+ const buildingsContainer = document.getElementById('buildings-list');
500
+
501
+ const buildingSection = document.createElement('div');
502
+ buildingSection.className = 'building-section mb-4';
503
+ buildingSection.dataset.buildingId = buildingCounter;
504
+
505
+ buildingSection.innerHTML = `
506
+ <div class="building-header d-flex justify-content-between align-items-center bg-light p-3 rounded-3 mb-3">
507
+ <h6 class="mb-0">Building #${buildingCounter}</h6>
508
+ <div class="building-actions d-flex gap-2">
509
+ <input type="text" class="form-control form-control-sm building-name"
510
+ placeholder="Building Name" style="width: 200px;">
511
+ <button class="btn btn-outline-danger btn-sm" onclick="removeBuilding(${buildingCounter})">
512
+ <i class="fas fa-trash"></i>
513
+ </button>
514
+ </div>
515
+ </div>
516
+
517
+ <div class="upload-section">
518
+ <div class="upload-area rounded-4" id="drop-zone-${buildingCounter}">
519
+ <input type="file" id="building-images-${buildingCounter}" class="building-images" accept="image/*" multiple hidden>
520
+ <div class="upload-content">
521
+ <i class="fas fa-cloud-upload-alt display-4 text-primary mb-3"></i>
522
+ <p class="mb-3">Drag and drop images here or</p>
523
+ <button class="btn btn-primary px-4 rounded-pill" onclick="document.getElementById('building-images-${buildingCounter}').click()">
524
+ Choose Files
525
+ </button>
526
+ <p class="text-muted small mt-3">Upload up to 5 images (max 4MB each)</p>
527
+ </div>
528
+ </div>
529
+ <div id="image-preview-${buildingCounter}" class="image-preview mt-4"></div>
530
+ </div>
531
+ `;
532
+
533
+ buildingsContainer.appendChild(buildingSection);
534
+
535
+ // Initialize drag and drop for new building
536
+ initializeDragAndDrop(buildingCounter);
537
+ // Initialize file input handler for new building
538
+ initializeFileInput(buildingCounter);
539
+ }
540
+
541
+ function removeBuilding(buildingId) {
542
+ const buildingSection = document.querySelector(`[data-building-id="${buildingId}"]`);
543
+ buildingSection.remove();
544
+ updateGenerateButton();
545
+ }
546
+
547
+ function showAlert(message, type = 'info', duration = 5000) {
548
+ const alertContainer = document.getElementById('alertContainer');
549
+ const alert = document.createElement('div');
550
+ alert.className = `alert alert-${type} alert-dismissible fade show`;
551
+ alert.innerHTML = `
552
+ ${message}
553
+ <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
554
+ `;
555
+
556
+ alertContainer.appendChild(alert);
557
+
558
+ if (duration) {
559
+ setTimeout(() => {
560
+ alert.classList.remove('show');
561
+ setTimeout(() => alert.remove(), 150);
562
+ }, duration);
563
+ }
564
+ }
565
+
566
+ function formatReportForDownload(reportHtml, basicInfo, images, chartImages) {
567
+ const [summaryTables, detailedReport] = reportHtml.split('REPORT 2:');
568
+
569
+ return `<!DOCTYPE html>
570
+ <html lang="en">
571
+ <head>
572
+ <meta charset="UTF-8">
573
+ <title>Campus Inspection Report - ${basicInfo.collegeName}</title>
574
+ <style>
575
+ @page {
576
+ margin: 2.5cm;
577
+ @top-center {
578
+ content: "Campus Inspection Report - ${basicInfo.collegeName}";
579
+ font-family: Arial, sans-serif;
580
+ font-size: 10pt;
581
+ }
582
+ @bottom-right {
583
+ content: counter(page) " of " counter(pages);
584
+ font-size: 10pt;
585
+ }
586
+ }
587
+
588
+ body {
589
+ font-family: Arial, sans-serif;
590
+ line-height: 1.6;
591
+ color: #333;
592
+ font-size: 11pt;
593
+ }
594
+
595
+ .cover-page {
596
+ text-align: center;
597
+ height: 100vh;
598
+ display: flex;
599
+ flex-direction: column;
600
+ justify-content: center;
601
+ align-items: center;
602
+ page-break-after: always;
603
+ }
604
+
605
+ .cover-page h1 {
606
+ font-size: 32pt;
607
+ color: #0d6efd;
608
+ margin-bottom: 2em;
609
+ }
610
+
611
+ table {
612
+ width: 100%;
613
+ border-collapse: collapse;
614
+ margin: 1.5em 0;
615
+ background: white;
616
+ box-shadow: 0 1px 3px rgba(0,0,0,0.2);
617
+ }
618
+
619
+ th {
620
+ background: #0d6efd;
621
+ color: white;
622
+ font-weight: 600;
623
+ padding: 12px;
624
+ text-align: left;
625
+ font-size: 11pt;
626
+ border: 1px solid #0d6efd;
627
+ }
628
+
629
+ td {
630
+ padding: 12px;
631
+ border: 1px solid #dee2e6;
632
+ font-size: 10pt;
633
+ vertical-align: top;
634
+ }
635
+
636
+ tr:nth-child(even) {
637
+ background-color: #f8f9fa;
638
+ }
639
+
640
+ tr:hover {
641
+ background-color: #f1f3f5;
642
+ }
643
+
644
+ .grade {
645
+ display: inline-block;
646
+ padding: 4px 8px;
647
+ border-radius: 4px;
648
+ font-weight: bold;
649
+ text-align: center;
650
+ min-width: 40px;
651
+ }
652
+
653
+ .grade-A-plus { background-color: #198754; color: white; }
654
+ .grade-A { background-color: #28a745; color: white; }
655
+ .grade-B-plus { background-color: #ffc107; }
656
+ .grade-B { background-color: #fd7e14; color: white; }
657
+ .grade-C { background-color: #dc3545; color: white; }
658
+
659
+ .priority {
660
+ display: inline-block;
661
+ padding: 4px 8px;
662
+ border-radius: 4px;
663
+ font-weight: bold;
664
+ text-align: center;
665
+ min-width: 60px;
666
+ }
667
+
668
+ .priority-high { background-color: #dc3545; color: white; }
669
+ .priority-medium { background-color: #fd7e14; color: white; }
670
+ .priority-low { background-color: #198754; color: white; }
671
+
672
+ ul {
673
+ margin: 0;
674
+ padding-left: 20px;
675
+ }
676
+
677
+ li {
678
+ margin-bottom: 4px;
679
+ }
680
+
681
+ .chart-image {
682
+ width: 100%;
683
+ max-width: 600px;
684
+ margin: 20px auto;
685
+ display: block;
686
+ }
687
+
688
+ .page-break {
689
+ page-break-before: always;
690
+ }
691
+
692
+ h2 {
693
+ color: #0d6efd;
694
+ font-size: 18pt;
695
+ margin-bottom: 1em;
696
+ border-bottom: 2px solid #0d6efd;
697
+ padding-bottom: 0.5em;
698
+ }
699
+
700
+ h3 {
701
+ color: #495057;
702
+ font-size: 14pt;
703
+ margin: 1.5em 0 1em;
704
+ }
705
+
706
+ .key-points {
707
+ margin: 0;
708
+ padding-left: 0;
709
+ list-style: none;
710
+ }
711
+
712
+ .key-points li {
713
+ position: relative;
714
+ padding-left: 20px;
715
+ margin-bottom: 6px;
716
+ }
717
+
718
+ .key-points li:before {
719
+ content: "•";
720
+ color: #0d6efd;
721
+ font-weight: bold;
722
+ position: absolute;
723
+ left: 0;
724
+ }
725
+
726
+ .solutions {
727
+ margin: 0;
728
+ padding-left: 0;
729
+ list-style: none;
730
+ }
731
+
732
+ .solutions li {
733
+ position: relative;
734
+ padding-left: 25px;
735
+ margin-bottom: 6px;
736
+ }
737
+
738
+ .solutions li:before {
739
+ content: "→";
740
+ color: #28a745;
741
+ font-weight: bold;
742
+ position: absolute;
743
+ left: 0;
744
+ }
745
+ </style>
746
+ </head>
747
+ <body>
748
+ <!-- Cover Page -->
749
+ <div class="cover-page">
750
+ <h1>Campus Inspection Report</h1>
751
+ <div class="info">
752
+ <p><strong>${basicInfo.collegeName}</strong></p>
753
+ <p>Location: ${basicInfo.location}</p>
754
+ <p>Inspection Date: ${basicInfo.inspectionDate}</p>
755
+ <p>Total Area: ${basicInfo.collegeArea} acres</p>
756
+ </div>
757
+ <div class="report-meta">
758
+ <p>Generated on: ${new Date().toLocaleDateString()}</p>
759
+ </div>
760
+ </div>
761
+
762
+ <!-- Executive Summary -->
763
+ <div class="page-break executive-summary">
764
+ <h2>Executive Summary</h2>
765
+ ${summaryTables}
766
+ </div>
767
+
768
+ <!-- Detailed Assessment -->
769
+ <div class="page-break detailed-assessment">
770
+ <h2>Detailed Assessment</h2>
771
+ REPORT 2:${detailedReport}
772
+ </div>
773
+
774
+ <!-- Visual Analysis Section -->
775
+ <div class="page-break charts-section">
776
+ <h2>Visual Analysis</h2>
777
+
778
+ <div class="row">
779
+ <div class="col-md-6">
780
+ <div class="chart-container">
781
+ <h3>Grade Distribution</h3>
782
+ <img src="${chartImages.gradeChart}" class="chart-image" alt="Grade Distribution">
783
+ </div>
784
+ </div>
785
+
786
+ <div class="col-md-6">
787
+ <div class="chart-container">
788
+ <h3>Priority Distribution</h3>
789
+ <img src="${chartImages.priorityChart}" class="chart-image" alt="Priority Distribution">
790
+ </div>
791
+ </div>
792
+ </div>
793
+
794
+ <div class="row">
795
+ <div class="col-md-12">
796
+ <div class="chart-container">
797
+ <h3>Area Performance Analysis</h3>
798
+ <img src="${chartImages.areaPerformanceChart}" class="chart-image" alt="Area Performance">
799
+ </div>
800
+ </div>
801
+ </div>
802
+
803
+ <div class="row">
804
+ <div class="col-md-6">
805
+ <div class="chart-container">
806
+ <h3>Maintenance Status</h3>
807
+ <img src="${chartImages.maintenanceChart}" class="chart-image" alt="Maintenance Status">
808
+ </div>
809
+ </div>
810
+ </div>
811
+ </div>
812
+ </body>
813
+ </html>`;
814
+ }
815
+
816
+ async function downloadReportAsPDF(reportHtml, basicInfo, images) {
817
+ console.debug('Preparing PDF report for download');
818
+
819
+ // Get base64 images of all charts
820
+ const chartImages = {
821
+ gradeChart: getChartImage('gradeChart'),
822
+ priorityChart: getChartImage('priorityChart'),
823
+ areaPerformanceChart: getChartImage('areaPerformanceChart'),
824
+ maintenanceChart: getChartImage('maintenanceChart')
825
+ };
826
+
827
+ // Create HTML with embedded chart images
828
+ const pdfHtml = formatReportForDownload(reportHtml, basicInfo, images, chartImages);
829
+
830
+ try {
831
+ const response = await fetch('/download_pdf', {
832
+ method: 'POST',
833
+ headers: {
834
+ 'Content-Type': 'application/json'
835
+ },
836
+ body: JSON.stringify({
837
+ html: pdfHtml
838
+ })
839
+ });
840
+
841
+ if (!response.ok) {
842
+ const error = await response.json();
843
+ throw new Error(error.error || 'Failed to generate PDF');
844
+ }
845
+
846
+ const blob = await response.blob();
847
+ const url = window.URL.createObjectURL(blob);
848
+ const a = document.createElement('a');
849
+ a.href = url;
850
+ a.download = `campus-inspection-report-${new Date().toISOString().split('T')[0]}.pdf`;
851
+ document.body.appendChild(a);
852
+ a.click();
853
+ window.URL.revokeObjectURL(url);
854
+ document.body.removeChild(a);
855
+
856
+ showAlert('PDF downloaded successfully!', 'success');
857
+ console.debug('PDF download completed');
858
+ } catch (error) {
859
+ console.error('Error downloading PDF:', error);
860
+ showAlert('Failed to generate PDF: ' + error.message, 'danger');
861
+ }
862
+ }
863
+
864
+ // Wait for DOM to be fully loaded
865
+ document.addEventListener('DOMContentLoaded', function() {
866
+ initializeDragAndDrop(1);
867
+ initializeFileInput(1);
868
+ });
869
+
870
+ function initializeDragAndDrop(buildingId) {
871
+ const dropZone = document.getElementById(`drop-zone-${buildingId}`);
872
+
873
+ ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
874
+ dropZone.addEventListener(eventName, preventDefaults, false);
875
+ });
876
+
877
+ ['dragenter', 'dragover'].forEach(eventName => {
878
+ dropZone.addEventListener(eventName, () => highlight(buildingId), false);
879
+ });
880
+
881
+ ['dragleave', 'drop'].forEach(eventName => {
882
+ dropZone.addEventListener(eventName, () => unhighlight(buildingId), false);
883
+ });
884
+
885
+ dropZone.addEventListener('drop', (e) => handleDrop(e, buildingId), false);
886
+ }
887
+
888
+ function initializeFileInput(buildingId) {
889
+ document.getElementById(`building-images-${buildingId}`).addEventListener('change', function(event) {
890
+ handleFiles(event.target.files, buildingId);
891
+ });
892
+ }
893
+
894
+ function preventDefaults(e) {
895
+ e.preventDefault();
896
+ e.stopPropagation();
897
+ }
898
+
899
+ function highlight(buildingId) {
900
+ document.getElementById(`drop-zone-${buildingId}`).classList.add('dragover');
901
+ }
902
+
903
+ function unhighlight(buildingId) {
904
+ document.getElementById(`drop-zone-${buildingId}`).classList.remove('dragover');
905
+ }
906
+
907
+ function handleDrop(e, buildingId) {
908
+ const dt = e.dataTransfer;
909
+ const files = dt.files;
910
+ handleFiles(files, buildingId);
911
+ }
912
+
913
+ function handleFiles(files, category) {
914
+ const preview = document.getElementById(`${category}-preview`);
915
+
916
+ Array.from(files).forEach(file => {
917
+ const reader = new FileReader();
918
+
919
+ reader.onerror = function() {
920
+ showAlert(`Error loading image: ${file.name}`, 'danger');
921
+ };
922
+
923
+ reader.onload = function(e) {
924
+ try {
925
+ // Create image element to verify it loads properly
926
+ const img = new Image();
927
+ img.onerror = function() {
928
+ showAlert(`Invalid image file: ${file.name}`, 'danger');
929
+ };
930
+ img.onload = function() {
931
+ // Only add to preview if image loads successfully
932
+ addImageToPreview(e.target.result, category, file.name);
933
+ };
934
+ img.src = e.target.result;
935
+ } catch (error) {
936
+ showAlert(`Error processing image: ${file.name}`, 'danger');
937
+ console.error(error);
938
+ }
939
+ };
940
+
941
+ reader.readAsDataURL(file);
942
+ });
943
+ }
944
+
945
+ function addImageToPreview(imageData, category, filename) {
946
+ const preview = document.getElementById(`${category}-preview`);
947
+ const container = document.createElement('div');
948
+ container.className = 'image-container';
949
+ container.dataset.category = category;
950
+
951
+ const img = document.createElement('img');
952
+ img.src = imageData;
953
+ img.alt = filename;
954
+
955
+ const removeBtn = document.createElement('button');
956
+ removeBtn.className = 'remove-image';
957
+ removeBtn.innerHTML = '<i class="fas fa-times"></i>';
958
+ removeBtn.onclick = function() {
959
+ container.remove();
960
+ updateImageCount(category);
961
+ };
962
+
963
+ container.appendChild(img);
964
+ container.appendChild(removeBtn);
965
+ preview.appendChild(container);
966
+ updateImageCount(category);
967
+ }
968
+
969
+ function updateImageLabels(buildingId) {
970
+ const preview = document.getElementById(`image-preview-${buildingId}`);
971
+ const labels = preview.querySelectorAll('.image-label');
972
+ labels.forEach((label, index) => {
973
+ label.textContent = `View ${index + 1}`;
974
+ });
975
+ }
976
+
977
+ function showError(message) {
978
+ const preview = document.getElementById('image-preview');
979
+ const errorDiv = document.createElement('div');
980
+ errorDiv.className = 'error-message';
981
+ errorDiv.innerHTML = `<i class="fas fa-exclamation-circle"></i> ${message}`;
982
+ preview.appendChild(errorDiv);
983
+ setTimeout(() => errorDiv.remove(), 3000);
984
+ }
985
+
986
+ function updateGenerateButton() {
987
+ const imageCount = document.querySelectorAll('.image-container').length;
988
+ generateBtn.disabled = imageCount === 0;
989
+ }
990
+
991
+ function validateImageRequirements() {
992
+ const requirements = {
993
+ building: { count: document.getElementById('buildingCount').value, minImages: 5 },
994
+ parking: { count: document.getElementById('parkingCount').value, minImages: 3 },
995
+ canteen: { count: document.getElementById('canteenCount').value, minImages: 2 },
996
+ ground: { count: document.getElementById('groundCount').value, minImages: 3 },
997
+ gate: { count: document.getElementById('gateCount').value, minImages: 2 },
998
+ road: { minImages: 7 }
999
+ };
1000
+
1001
+ // Calculate total required images
1002
+ const totalRequired =
1003
+ (requirements.building.count * requirements.building.minImages) +
1004
+ (requirements.parking.count * requirements.parking.minImages) +
1005
+ (requirements.canteen.count * requirements.canteen.minImages) +
1006
+ (requirements.ground.count * requirements.ground.minImages) +
1007
+ (requirements.gate.count * requirements.gate.minImages) +
1008
+ requirements.road.minImages;
1009
+
1010
+ // Get actual image count
1011
+ const actualImages = document.querySelectorAll('.image-container').length;
1012
+
1013
+ if (actualImages < totalRequired) {
1014
+ showAlert(`Please upload at least ${totalRequired} images to meet minimum requirements:
1015
+ - ${requirements.building.count} buildings × 5 images each
1016
+ - ${requirements.parking.count} parking areas × 3 images each
1017
+ - ${requirements.canteen.count} canteens × 2 images each
1018
+ - ${requirements.ground.count} grounds × 3 images each
1019
+ - ${requirements.gate.count} gates × 2 images each
1020
+ - At least 7 road images`, 'danger');
1021
+ return false;
1022
+ }
1023
+ return true;
1024
+ }
1025
+
1026
+ function validateForm() {
1027
+ const requiredFields = {
1028
+ 'collegeName': 'College Name',
1029
+ 'location': 'Location',
1030
+ 'inspectionDate': 'Inspection Date',
1031
+ 'collegeArea': 'College Area',
1032
+ 'buildingCount': 'Building Count',
1033
+ 'parkingCount': 'Parking Count',
1034
+ 'groundArea': 'Ground Area',
1035
+ 'gateCount': 'Gate Count'
1036
+ };
1037
+
1038
+ for (const [fieldId, fieldName] of Object.entries(requiredFields)) {
1039
+ const field = document.getElementById(fieldId);
1040
+ if (!field.value.trim()) {
1041
+ showAlert(`Please enter ${fieldName}`, 'danger');
1042
+ field.focus();
1043
+ return false;
1044
+ }
1045
+ }
1046
+ return true;
1047
+ }
1048
+
1049
+ // Keep generateReport outside DOMContentLoaded since it's called from HTML
1050
+ async function generateReport() {
1051
+ if (!validateForm()) {
1052
+ return;
1053
+ }
1054
+
1055
+ const button = document.querySelector('.btn-primary[onclick="generateReport()"]');
1056
+ const loadingIndicator = document.getElementById('loading-indicator');
1057
+ const reportOutput = document.getElementById('report-output');
1058
+ const downloadBtn = document.getElementById('downloadBtn');
1059
+
1060
+ // Get basic info
1061
+ const collegeName = document.getElementById('collegeName').value.trim();
1062
+ const location = document.getElementById('location').value.trim();
1063
+ const inspectionDate = document.getElementById('inspectionDate').value;
1064
+
1065
+ // Create basicInfo object
1066
+ const basicInfo = {
1067
+ collegeName,
1068
+ location,
1069
+ inspectionDate: new Date(inspectionDate).toLocaleDateString(),
1070
+ collegeArea: document.getElementById('collegeArea').value,
1071
+ buildingCount: document.getElementById('buildingCount').value,
1072
+ parkingCount: document.getElementById('parkingCount').value,
1073
+ canteenCount: document.getElementById('canteenCount').value,
1074
+ groundCount: document.getElementById('groundCount').value,
1075
+ groundArea: document.getElementById('groundArea').value,
1076
+ gateCount: document.getElementById('gateCount').value
1077
+ };
1078
+
1079
+ try {
1080
+ if (!collegeName || !location || !inspectionDate) {
1081
+ showAlert('Please fill in all the basic information', 'danger');
1082
+ return;
1083
+ }
1084
+
1085
+ // Collect images from all categories
1086
+ const allImages = [];
1087
+ const categories = ['building', 'parking', 'road', 'canteen', 'ground', 'gate'];
1088
+
1089
+ categories.forEach(category => {
1090
+ const imageContainers = document.querySelectorAll(`#${category}-preview .image-container img`);
1091
+ imageContainers.forEach(img => {
1092
+ allImages.push({
1093
+ category: category,
1094
+ data: img.src
1095
+ });
1096
+ });
1097
+ });
1098
+
1099
+ if (allImages.length === 0) {
1100
+ showAlert('Please upload at least one image', 'danger');
1101
+ return;
1102
+ }
1103
+
1104
+ showAlert('Generating report, please wait...', 'info');
1105
+ console.debug('Starting report generation');
1106
+ button.disabled = true;
1107
+ loadingIndicator.classList.remove('hidden');
1108
+ reportOutput.innerHTML = '';
1109
+
1110
+ console.debug('Sending request to server');
1111
+ const response = await fetch('/generate_report', {
1112
+ method: 'POST',
1113
+ headers: {
1114
+ 'Content-Type': 'application/json'
1115
+ },
1116
+ body: JSON.stringify({
1117
+ images: allImages,
1118
+ basicInfo
1119
+ })
1120
+ });
1121
+
1122
+ const data = await response.json();
1123
+ if (data.error) {
1124
+ throw new Error(data.error);
1125
+ }
1126
+
1127
+ console.debug('Received report from server');
1128
+ const reportHtml = marked.parse(data.report);
1129
+
1130
+ // Create report container with charts section
1131
+ const reportContainer = `
1132
+ <div class="report-header">
1133
+ <h2>${basicInfo.collegeName}</h2>
1134
+ <p>
1135
+ <strong>Location:</strong> ${basicInfo.location}<br>
1136
+ <strong>Inspection Date:</strong> ${basicInfo.inspectionDate}
1137
+ </p>
1138
+ </div>
1139
+ <hr>
1140
+ <div class="charts-section mb-4">
1141
+ <h3>Visual Analysis</h3>
1142
+ <div class="row">
1143
+ <div class="col-md-6">
1144
+ <div class="chart-container">
1145
+ <canvas id="gradeChart"></canvas>
1146
+ </div>
1147
+ </div>
1148
+ <div class="col-md-6">
1149
+ <div class="chart-container">
1150
+ <canvas id="priorityChart"></canvas>
1151
+ </div>
1152
+ </div>
1153
+ </div>
1154
+ <div class="row mt-4">
1155
+ <div class="col-md-12">
1156
+ <div class="chart-container">
1157
+ <canvas id="areaPerformanceChart"></canvas>
1158
+ </div>
1159
+ </div>
1160
+ </div>
1161
+ <div class="row mt-4">
1162
+ <div class="col-md-6">
1163
+ <div class="chart-container">
1164
+ <canvas id="maintenanceChart"></canvas>
1165
+ </div>
1166
+ </div>
1167
+ </div>
1168
+ </div>
1169
+ ${reportHtml}
1170
+ `;
1171
+
1172
+ reportOutput.innerHTML = reportContainer;
1173
+
1174
+ // Initialize charts with actual data
1175
+ new Chart(document.getElementById('gradeChart'), {
1176
+ type: 'bar',
1177
+ data: {
1178
+ labels: ['A+', 'A', 'B+', 'B', 'C'],
1179
+ datasets: [{
1180
+ label: 'Grade Distribution',
1181
+ data: data.metrics.gradeDistribution || [2, 3, 1, 1, 0],
1182
+ backgroundColor: ['#198754', '#28a745', '#ffc107', '#fd7e14', '#dc3545']
1183
+ }]
1184
+ },
1185
+ options: {
1186
+ responsive: true,
1187
+ plugins: {
1188
+ title: {
1189
+ display: true,
1190
+ text: 'Grade Distribution Across Areas'
1191
+ }
1192
+ }
1193
+ }
1194
+ });
1195
+
1196
+ new Chart(document.getElementById('priorityChart'), {
1197
+ type: 'pie',
1198
+ data: {
1199
+ labels: ['High Priority', 'Medium Priority', 'Low Priority'],
1200
+ datasets: [{
1201
+ data: data.metrics.priorityDistribution || [30, 45, 25],
1202
+ backgroundColor: ['#dc3545', '#ffc107', '#198754']
1203
+ }]
1204
+ },
1205
+ options: {
1206
+ responsive: true,
1207
+ plugins: {
1208
+ title: {
1209
+ display: true,
1210
+ text: 'Issue Priority Distribution'
1211
+ },
1212
+ legend: {
1213
+ position: 'bottom'
1214
+ }
1215
+ }
1216
+ }
1217
+ });
1218
+
1219
+ new Chart(document.getElementById('areaPerformanceChart'), {
1220
+ type: 'radar',
1221
+ data: {
1222
+ labels: ['Buildings', 'Roads & Parking', 'Sports Facilities', 'Canteens', 'Security', 'Environmental'],
1223
+ datasets: [{
1224
+ label: 'Performance Score',
1225
+ data: data.metrics.areaPerformance || [85, 75, 70, 80, 90, 85],
1226
+ fill: true,
1227
+ backgroundColor: 'rgba(13, 110, 253, 0.2)',
1228
+ borderColor: 'rgb(13, 110, 253)',
1229
+ pointBackgroundColor: 'rgb(13, 110, 253)',
1230
+ pointBorderColor: '#fff',
1231
+ pointHoverBackgroundColor: '#fff',
1232
+ pointHoverBorderColor: 'rgb(13, 110, 253)'
1233
+ }]
1234
+ },
1235
+ options: {
1236
+ responsive: true,
1237
+ plugins: {
1238
+ title: {
1239
+ display: true,
1240
+ text: 'Area-wise Performance Analysis'
1241
+ }
1242
+ },
1243
+ scales: {
1244
+ r: {
1245
+ angleLines: {
1246
+ display: true
1247
+ },
1248
+ suggestedMin: 0,
1249
+ suggestedMax: 100
1250
+ }
1251
+ }
1252
+ }
1253
+ });
1254
+
1255
+ new Chart(document.getElementById('maintenanceChart'), {
1256
+ type: 'doughnut',
1257
+ data: {
1258
+ labels: ['Well Maintained', 'Needs Attention', 'Critical'],
1259
+ datasets: [{
1260
+ data: data.metrics.maintenanceStatus || [60, 30, 10],
1261
+ backgroundColor: ['#198754', '#ffc107', '#dc3545']
1262
+ }]
1263
+ },
1264
+ options: {
1265
+ responsive: true,
1266
+ plugins: {
1267
+ title: {
1268
+ display: true,
1269
+ text: 'Infrastructure Maintenance Status'
1270
+ },
1271
+ legend: {
1272
+ position: 'bottom'
1273
+ }
1274
+ }
1275
+ }
1276
+ });
1277
+
1278
+ // Store the report data in a global variable or data attribute
1279
+ reportData = {
1280
+ reportHtml: reportContainer,
1281
+ basicInfo: basicInfo,
1282
+ images: data.images
1283
+ };
1284
+
1285
+ // Update the download buttons to use this data
1286
+ downloadBtn.innerHTML = `
1287
+ <div class="dropdown">
1288
+ <button class="btn btn-outline-primary btn-sm dropdown-toggle" type="button" data-bs-toggle="dropdown">
1289
+ <i class="fas fa-download me-2"></i>Download Report
1290
+ </button>
1291
+ <ul class="dropdown-menu">
1292
+ <li>
1293
+ <button class="dropdown-item" type="button" onclick="downloadReport(reportData.reportHtml, reportData.basicInfo, reportData.images)">
1294
+ <i class="fas fa-file-code me-2"></i>Download as HTML
1295
+ </button>
1296
+ </li>
1297
+ <li>
1298
+ <button class="dropdown-item" type="button" onclick="downloadReportAsPDF(reportData.reportHtml, reportData.basicInfo, reportData.images)">
1299
+ <i class="fas fa-file-pdf me-2"></i>Download as PDF
1300
+ </button>
1301
+ </li>
1302
+ </ul>
1303
+ </div>
1304
+ `;
1305
+
1306
+ showAlert('Report generated successfully!', 'success');
1307
+
1308
+ } catch (error) {
1309
+ showAlert(`Error: ${error.message}`, 'danger');
1310
+ console.error('Error generating report:', error);
1311
+ reportOutput.innerHTML = `
1312
+ <div class="error-message">
1313
+ <i class="fas fa-exclamation-circle"></i>
1314
+ ${error.message}
1315
+ </div>
1316
+ `;
1317
+ downloadBtn.innerHTML = '';
1318
+ } finally {
1319
+ button.disabled = false;
1320
+ loadingIndicator.classList.add('hidden');
1321
+ }
1322
+ }
1323
+
1324
+ function downloadReport(reportHtml, basicInfo, images) {
1325
+ // Create a formatted report
1326
+ const formattedReport = formatReportForDownload(reportHtml, basicInfo, images);
1327
+
1328
+ // Create a blob and download it
1329
+ const blob = new Blob([formattedReport], { type: 'text/html' });
1330
+ const url = window.URL.createObjectURL(blob);
1331
+ const a = document.createElement('a');
1332
+ a.href = url;
1333
+ a.download = `campus-inspection-report-${new Date().toISOString().split('T')[0]}.html`;
1334
+ document.body.appendChild(a);
1335
+ a.click();
1336
+ window.URL.revokeObjectURL(url);
1337
+ document.body.removeChild(a);
1338
+ }
1339
+
1340
+ // Initialize all upload zones
1341
+ document.addEventListener('DOMContentLoaded', function() {
1342
+ const categories = ['building', 'parking', 'road', 'canteen', 'ground', 'gate'];
1343
+
1344
+ categories.forEach(category => {
1345
+ initializeUploadZone(category);
1346
+ });
1347
+ });
1348
+
1349
+ function initializeUploadZone(category) {
1350
+ const dropZone = document.getElementById(`${category}-drop-zone`);
1351
+ const input = document.getElementById(`${category}-images`);
1352
+
1353
+ // Handle drag and drop
1354
+ ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
1355
+ dropZone.addEventListener(eventName, preventDefaults, false);
1356
+ });
1357
+
1358
+ ['dragenter', 'dragover'].forEach(eventName => {
1359
+ dropZone.addEventListener(eventName, () => highlight(category), false);
1360
+ });
1361
+
1362
+ ['dragleave', 'drop'].forEach(eventName => {
1363
+ dropZone.addEventListener(eventName, () => unhighlight(category), false);
1364
+ });
1365
+
1366
+ dropZone.addEventListener('drop', (e) => {
1367
+ const dt = e.dataTransfer;
1368
+ handleFiles(dt.files, category);
1369
+ });
1370
+
1371
+ // Handle file input
1372
+ input.addEventListener('change', (e) => {
1373
+ handleFiles(e.target.files, category);
1374
+ });
1375
+ }
1376
+
1377
+ function handleFiles(files, category) {
1378
+ const preview = document.getElementById(`${category}-preview`);
1379
+ const requirements = {
1380
+ building: 5,
1381
+ parking: 3,
1382
+ road: 7,
1383
+ canteen: 2,
1384
+ ground: 3,
1385
+ gate: 2
1386
+ };
1387
+
1388
+ // Validate file count based on category requirements
1389
+ const requiredCount = requirements[category] *
1390
+ (category === 'road' ? 1 : document.getElementById(`${category}Count`).value);
1391
+
1392
+ Array.from(files).forEach(file => {
1393
+ if (!file.type.startsWith('image/')) {
1394
+ showAlert(`"${file.name}" is not an image file`, 'danger');
1395
+ return;
1396
+ }
1397
+
1398
+ const reader = new FileReader();
1399
+ reader.onload = function(e) {
1400
+ const container = document.createElement('div');
1401
+ container.className = 'image-container';
1402
+ container.dataset.category = category;
1403
+
1404
+ const img = document.createElement('img');
1405
+ img.src = e.target.result;
1406
+
1407
+ const removeBtn = document.createElement('button');
1408
+ removeBtn.className = 'remove-image';
1409
+ removeBtn.innerHTML = '<i class="fas fa-times"></i>';
1410
+ removeBtn.onclick = function() {
1411
+ container.remove();
1412
+ updateImageCount(category);
1413
+ };
1414
+
1415
+ container.appendChild(img);
1416
+ container.appendChild(removeBtn);
1417
+ preview.appendChild(container);
1418
+ updateImageCount(category);
1419
+ };
1420
+ reader.readAsDataURL(file);
1421
+ });
1422
+ }
1423
+
1424
+ function updateImageCount(category) {
1425
+ const count = document.querySelectorAll(`#${category}-preview .image-container`).length;
1426
+ const required = getRequiredImageCount(category);
1427
+
1428
+ const uploadBtn = document.querySelector(`#${category}-drop-zone button`);
1429
+ if (count >= required) {
1430
+ uploadBtn.classList.remove('btn-primary');
1431
+ uploadBtn.classList.add('btn-success');
1432
+ } else {
1433
+ uploadBtn.classList.remove('btn-success');
1434
+ uploadBtn.classList.add('btn-primary');
1435
+ }
1436
+ }
1437
+
1438
+ function getRequiredImageCount(category) {
1439
+ const requirements = {
1440
+ building: 5,
1441
+ parking: 3,
1442
+ road: 7,
1443
+ canteen: 2,
1444
+ ground: 3,
1445
+ gate: 2
1446
+ };
1447
+
1448
+ const count = category === 'road' ? 1 :
1449
+ parseInt(document.getElementById(`${category}Count`).value) || 0;
1450
+ return requirements[category] * count;
1451
+ }
1452
+
1453
+ // Helper functions to generate charts
1454
+ function generateGradeChart() {
1455
+ return `
1456
+ const gradeCtx = document.getElementById('gradeChart');
1457
+ new Chart(gradeCtx, {
1458
+ type: 'bar',
1459
+ data: {
1460
+ labels: ['A+', 'A', 'B+', 'B', 'C'],
1461
+ datasets: [{
1462
+ label: 'Grade Distribution',
1463
+ data: [2, 3, 1, 1, 0], // Example data, replace with actual data
1464
+ backgroundColor: ['#198754', '#28a745', '#ffc107', '#fd7e14', '#dc3545']
1465
+ }]
1466
+ },
1467
+ options: {
1468
+ responsive: true,
1469
+ plugins: {
1470
+ title: {
1471
+ display: true,
1472
+ text: 'Grade Distribution Across Areas'
1473
+ }
1474
+ }
1475
+ }
1476
+ });
1477
+ `;
1478
+ }
1479
+
1480
+ function generatePriorityChart(data) {
1481
+ return `
1482
+ const priorityCtx = document.getElementById('priorityChart');
1483
+ new Chart(priorityCtx, {
1484
+ type: 'pie',
1485
+ data: {
1486
+ labels: ['High Priority', 'Medium Priority', 'Low Priority'],
1487
+ datasets: [{
1488
+ data: ${JSON.stringify(data)},
1489
+ backgroundColor: ['#dc3545', '#ffc107', '#198754']
1490
+ }]
1491
+ },
1492
+ options: {
1493
+ responsive: true,
1494
+ plugins: {
1495
+ title: {
1496
+ display: true,
1497
+ text: 'Issue Priority Distribution'
1498
+ },
1499
+ legend: {
1500
+ position: 'bottom'
1501
+ }
1502
+ }
1503
+ }
1504
+ });
1505
+ `;
1506
+ }
1507
+
1508
+ function generateAreaPerformanceChart() {
1509
+ return `
1510
+ const performanceCtx = document.getElementById('areaPerformanceChart');
1511
+ new Chart(performanceCtx, {
1512
+ type: 'radar',
1513
+ data: {
1514
+ labels: ['Buildings', 'Roads & Parking', 'Sports Facilities', 'Canteens', 'Security', 'Environmental'],
1515
+ datasets: [{
1516
+ label: 'Performance Score',
1517
+ data: [85, 75, 70, 80, 90, 85], // Will be replaced with actual data
1518
+ fill: true,
1519
+ backgroundColor: 'rgba(13, 110, 253, 0.2)',
1520
+ borderColor: 'rgb(13, 110, 253)',
1521
+ pointBackgroundColor: 'rgb(13, 110, 253)',
1522
+ pointBorderColor: '#fff',
1523
+ pointHoverBackgroundColor: '#fff',
1524
+ pointHoverBorderColor: 'rgb(13, 110, 253)'
1525
+ }]
1526
+ },
1527
+ options: {
1528
+ responsive: true,
1529
+ plugins: {
1530
+ title: {
1531
+ display: true,
1532
+ text: 'Area-wise Performance Analysis'
1533
+ }
1534
+ },
1535
+ scales: {
1536
+ r: {
1537
+ angleLines: {
1538
+ display: true
1539
+ },
1540
+ suggestedMin: 0,
1541
+ suggestedMax: 100
1542
+ }
1543
+ }
1544
+ }
1545
+ });
1546
+ `;
1547
+ }
1548
+
1549
+ function generateMaintenanceStatusChart() {
1550
+ return `
1551
+ const maintenanceCtx = document.getElementById('maintenanceStatusChart');
1552
+ new Chart(maintenanceCtx, {
1553
+ type: 'doughnut',
1554
+ data: {
1555
+ labels: ['Well Maintained', 'Needs Attention', 'Critical'],
1556
+ datasets: [{
1557
+ data: [60, 30, 10], // Will be replaced with actual data
1558
+ backgroundColor: ['#198754', '#ffc107', '#dc3545']
1559
+ }]
1560
+ },
1561
+ options: {
1562
+ responsive: true,
1563
+ plugins: {
1564
+ title: {
1565
+ display: true,
1566
+ text: 'Infrastructure Maintenance Status'
1567
+ },
1568
+ legend: {
1569
+ position: 'bottom'
1570
+ }
1571
+ }
1572
+ }
1573
+ });
1574
+ `;
1575
+ }
1576
+
1577
+ // Add this function to convert charts to base64 images
1578
+ function getChartImage(chartId) {
1579
+ const canvas = document.getElementById(chartId);
1580
+ return canvas.toDataURL('image/png');
1581
+ }
1582
+ </script>
1583
+ </body>
1584
+ </html>
test/Canteen/Screenshot 2024-12-05 195018.png ADDED
test/Canteen/Screenshot 2024-12-05 195051.png ADDED
test/Ground/IMG-20241205-WA0014.jpg ADDED
test/Ground/Screenshot 2024-12-05 195126.png ADDED
test/Ground/Screenshot 2024-12-05 195143.png ADDED
test/IMG-20241205-WA0006.jpg ADDED
test/IMG-20241205-WA0007.jpg ADDED
test/IMG-20241205-WA0008.jpg ADDED
test/IMG-20241205-WA0009.jpg ADDED
test/IMG-20241205-WA0010.jpg ADDED
test/IMG-20241205-WA0011.jpg ADDED
test/IMG-20241205-WA0012.jpg ADDED
test/IMG-20241205-WA0013.jpg ADDED
test/IMG-20241205-WA0014.jpg ADDED
test/IMG-20241205-WA0015.jpg ADDED
test/IMG-20241205-WA0016.jpg ADDED
test/IMG-20241205-WA0017.jpg ADDED
test/IMG-20241205-WA0018.jpg ADDED
test/IMG-20241205-WA0019.jpg ADDED
test/IMG-20241205-WA0020.jpg ADDED
test/IMG-20241205-WA0021.jpg ADDED
test/IMG-20241205-WA0023.jpg ADDED
test/IMG-20241205-WA0024.jpg ADDED
test/IMG-20241205-WA0025.jpg ADDED
test/IMG-20241205-WA0027.jpg ADDED
test/IMG-20241205-WA0029.jpg ADDED
test/IMG-20241205-WA0030.jpg ADDED
test/IMG-20241205-WA0031.jpg ADDED
test/IMG-20241205-WA0032.jpg ADDED
test/IMG-20241205-WA0034.jpg ADDED
test/IMG-20241205-WA0035.jpg ADDED
test/IMG-20241205-WA0036.jpg ADDED
test/IMG-20241205-WA0037.jpg ADDED
test/IMG-20241205-WA0038.jpg ADDED
test/Parking/IMG-20241205-WA0031.jpg ADDED
test/Parking/IMG-20241205-WA0032.jpg ADDED
test/Parking/IMG-20241205-WA0037.jpg ADDED
test/Road/IMG-20241205-WA0032.jpg ADDED
test/Road/Screenshot 2024-12-05 194245 - Copy.png ADDED
test/Road/Screenshot 2024-12-05 194245.png ADDED
test/Road/Screenshot 2024-12-05 194600 - Copy.png ADDED
test/Road/Screenshot 2024-12-05 194600.png ADDED
test/Road/Screenshot 2024-12-05 194820 - Copy.png ADDED
test/Road/Screenshot 2024-12-05 194820.png ADDED