anushkap01patidar commited on
Commit
d122c3c
·
1 Parent(s): efca9c4
Dockerfile ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ ENV PYTHONDONTWRITEBYTECODE=1 \
4
+ PYTHONUNBUFFERED=1 \
5
+ PIP_NO_CACHE_DIR=1
6
+
7
+ WORKDIR /app
8
+
9
+ RUN apt-get update && apt-get install -y --no-install-recommends \
10
+ build-essential \
11
+ && rm -rf /var/lib/apt/lists/*
12
+
13
+ COPY requirements.txt ./
14
+ RUN pip install --upgrade pip && pip install -r requirements.txt
15
+
16
+ COPY . .
17
+
18
+ RUN mkdir -p uploads instance
19
+
20
+ ENV FLASK_APP=app.py
21
+
22
+ EXPOSE 7860
23
+
24
+ CMD ["python", "-m", "flask", "run", "--host=0.0.0.0", "--port=7860"]
25
+
26
+
README.md CHANGED
@@ -1,60 +1,33 @@
1
- ---
2
- title: EvalAI
3
- emoji: 🔥
4
- colorFrom: pink
5
- colorTo: gray
6
- sdk: static
7
- pinned: false
8
- app_build_command: npm run build
9
- app_file: dist/index.html
10
- license: mit
11
- short_description: AI-Powered Project Evaluator
12
- ---
13
-
14
- # Svelte + TS + Vite
15
-
16
- This template should help get you started developing with Svelte and TypeScript in Vite.
17
-
18
- ## Recommended IDE Setup
19
-
20
- [VS Code](https://code.visualstudio.com/) + [Svelte](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode).
21
-
22
- ## Need an official Svelte framework?
23
-
24
- Check out [SvelteKit](https://github.com/sveltejs/kit#readme), which is also powered by Vite. Deploy anywhere with its serverless-first approach and adapt to various platforms, with out of the box support for TypeScript, SCSS, and Less, and easily-added support for mdsvex, GraphQL, PostCSS, Tailwind CSS, and more.
25
-
26
- ## Technical considerations
27
-
28
- **Why use this over SvelteKit?**
29
-
30
- - It brings its own routing solution which might not be preferable for some users.
31
- - It is first and foremost a framework that just happens to use Vite under the hood, not a Vite app.
32
-
33
- This template contains as little as possible to get started with Vite + TypeScript + Svelte, while taking into account the developer experience with regards to HMR and intellisense. It demonstrates capabilities on par with the other `create-vite` templates and is a good starting point for beginners dipping their toes into a Vite + Svelte project.
34
-
35
- Should you later need the extended capabilities and extensibility provided by SvelteKit, the template has been structured similarly to SvelteKit so that it is easy to migrate.
36
-
37
- **Why `global.d.ts` instead of `compilerOptions.types` inside `jsconfig.json` or `tsconfig.json`?**
38
-
39
- Setting `compilerOptions.types` shuts out all other types not explicitly listed in the configuration. Using triple-slash references keeps the default TypeScript setting of accepting type information from the entire workspace, while also adding `svelte` and `vite/client` type information.
40
-
41
- **Why include `.vscode/extensions.json`?**
42
-
43
- Other templates indirectly recommend extensions via the README, but this file allows VS Code to prompt the user to install the recommended extension upon opening the project.
44
-
45
- **Why enable `allowJs` in the TS template?**
46
-
47
- While `allowJs: false` would indeed prevent the use of `.js` files in the project, it does not prevent the use of JavaScript syntax in `.svelte` files. In addition, it would force `checkJs: false`, bringing the worst of both worlds: not being able to guarantee the entire codebase is TypeScript, and also having worse typechecking for the existing JavaScript. In addition, there are valid use cases in which a mixed codebase may be relevant.
48
-
49
- **Why is HMR not preserving my local component state?**
50
-
51
- HMR state preservation comes with a number of gotchas! It has been disabled by default in both `svelte-hmr` and `@sveltejs/vite-plugin-svelte` due to its often surprising behavior. You can read the details [here](https://github.com/rixo/svelte-hmr#svelte-hmr).
52
 
53
- If you have state that's important to retain within a component, consider creating an external store which would not be replaced by HMR.
 
 
 
 
54
 
55
- ```ts
56
- // store.ts
57
- // An extremely simple external store
58
- import { writable } from "svelte/store";
59
- export default writable(0);
60
  ```
 
1
+ Deploying the Eval backend to Hugging Face Spaces (Docker)
2
+
3
+ Prerequisites
4
+ - A Hugging Face account and a new Space (set type to Docker)
5
+ - Your OpenAI API key (if using OpenAI evaluation)
6
+
7
+ Files included
8
+ - app.py: Flask API exposing endpoints under /api
9
+ - models.py, utils.py, evaluator.py, chunking_utils.py, config.py
10
+ - requirements.txt: minimal dependencies for CPU deployment
11
+ - Dockerfile: builds and runs the Flask API on port 7860
12
+
13
+ Environment variables
14
+ - OPENAI_API_KEY: set in the Space Secrets if Config.EVALUATION_MODEL is 'openai'
15
+ - FLASK_SECRET_KEY (optional)
16
+ - DATABASE_URL (optional; defaults to sqlite:///evalai_new.db)
17
+
18
+ Build and run (locally)
19
+ ```bash
20
+ docker build -t eval-backend ./Eval
21
+ docker run -p 7860:7860 -e OPENAI_API_KEY=YOUR_KEY eval-backend
22
+ ```
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
 
24
+ Deploy on Spaces
25
+ 1) Create a new Space, select Docker as the SDK
26
+ 2) Upload the contents of the Eval/ folder to the Space root
27
+ 3) In the Space Settings, add a Secret named OPENAI_API_KEY
28
+ 4) Spaces will build using the Dockerfile and expose the API at / on port 7860
29
 
30
+ API quick check
31
+ ```bash
32
+ curl -s https://<your-space>.hf.space/ | jq
 
 
33
  ```
app.py ADDED
@@ -0,0 +1,270 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, request, jsonify
2
+ from flask_cors import CORS
3
+ from models import db, Hackathon, Submission, Evaluation
4
+ from utils import allowed_file, save_uploaded_file, extract_code_from_files, extract_documentation
5
+ from config import Config
6
+ import json
7
+ from datetime import datetime
8
+
9
+ app = Flask(__name__)
10
+ app.config.from_object(Config)
11
+ app.config['MAX_CONTENT_LENGTH'] = Config.MAX_CONTENT_LENGTH # Explicitly set the upload limit
12
+ CORS(app)
13
+
14
+ # Initialize database
15
+ db.init_app(app)
16
+
17
+ # Initialize AI evaluator (will load models on first use)
18
+ evaluator = None
19
+
20
+ def get_evaluator():
21
+ global evaluator
22
+ if evaluator is None:
23
+ print(f"Initializing AI evaluator (mode: {Config.EVALUATION_MODEL})...")
24
+
25
+ if Config.EVALUATION_MODEL == 'openai':
26
+ from evaluator import AIEvaluator
27
+ evaluator = AIEvaluator()
28
+ print("✅ Using OpenAI GPT-4o for evaluation")
29
+ else:
30
+ from evaluator_opensource import OpenSourceEvaluator
31
+ evaluator = OpenSourceEvaluator()
32
+ print("✅ Using Open-Source LLM for evaluation")
33
+
34
+ return evaluator
35
+
36
+ # Create tables
37
+ with app.app_context():
38
+ db.create_all()
39
+ print("Database initialized with productivity_score column!")
40
+
41
+ # Routes
42
+ @app.route('/')
43
+ def index():
44
+ """API server info - Frontend is served separately on port 5173"""
45
+ return jsonify({
46
+ 'name': 'EvalAI API',
47
+ 'version': '1.0',
48
+ 'status': 'running',
49
+ 'frontend_url': 'http://localhost:5173',
50
+ 'message': 'API is running. Access the UI at http://localhost:5173'
51
+ })
52
+
53
+ # API Endpoints
54
+ @app.route('/api/hackathons', methods=['GET'])
55
+ def get_hackathons():
56
+ """Get all hackathons"""
57
+ hackathons = Hackathon.query.order_by(Hackathon.created_at.desc()).all()
58
+ return jsonify([h.to_dict() for h in hackathons])
59
+
60
+ @app.route('/api/hackathon', methods=['POST'])
61
+ def create_hackathon():
62
+ """Create a new hackathon"""
63
+ try:
64
+ data = request.json
65
+
66
+ # Default criteria if not provided
67
+ default_criteria = [
68
+ {'name': 'Relevance', 'weight': 0.20, 'description': 'Alignment with theme'},
69
+ {'name': 'Technical Complexity', 'weight': 0.20, 'description': 'Code quality and sophistication'},
70
+ {'name': 'Creativity', 'weight': 0.20, 'description': 'Innovation and uniqueness'},
71
+ {'name': 'Documentation', 'weight': 0.20, 'description': 'Quality and completeness'},
72
+ {'name': 'Productivity', 'weight': 0.20, 'description': 'Code organization and efficiency'}
73
+ ]
74
+
75
+ hackathon = Hackathon(
76
+ name=data['name'],
77
+ description=data['description'],
78
+ evaluation_prompt=data.get('evaluation_prompt', 'Evaluate this hackathon project.'),
79
+ criteria=json.dumps(data.get('criteria', default_criteria)),
80
+ host_email=data.get('host_email', ''),
81
+ deadline=datetime.fromisoformat(data['deadline']) if data.get('deadline') else None
82
+ )
83
+
84
+ db.session.add(hackathon)
85
+ db.session.commit()
86
+
87
+ return jsonify(hackathon.to_dict()), 201
88
+
89
+ except Exception as e:
90
+ db.session.rollback()
91
+ return jsonify({'error': str(e)}), 400
92
+
93
+
94
+ @app.route('/api/submissions', methods=['POST'])
95
+ def create_submission():
96
+ """Create a single submission and evaluate it"""
97
+ try:
98
+ # Get form data
99
+ hackathon_id = request.form.get('hackathon_id')
100
+ team_name = request.form.get('team_name', 'Team')
101
+ participant_email = request.form.get('participant_email', 'participant@autoeval.ai')
102
+ project_name = request.form.get('project_name')
103
+ project_description = request.form.get('project_description', '')
104
+
105
+ if not hackathon_id or not project_name:
106
+ return jsonify({'error': 'Hackathon ID and project name are required'}), 400
107
+
108
+ # Get hackathon
109
+ hackathon = Hackathon.query.get(int(hackathon_id))
110
+ if not hackathon:
111
+ return jsonify({'error': 'Hackathon not found'}), 404
112
+
113
+ # Get uploaded files
114
+ files = request.files.getlist('project_files')
115
+ if not files:
116
+ return jsonify({'error': 'At least one file is required'}), 400
117
+
118
+ # Create submission
119
+ submission = Submission(
120
+ hackathon_id=hackathon.id,
121
+ team_name=team_name,
122
+ participant_email=participant_email,
123
+ project_name=project_name,
124
+ project_description=project_description
125
+ )
126
+ db.session.add(submission)
127
+ db.session.flush()
128
+
129
+ # Save files
130
+ file_paths = []
131
+ for file in files:
132
+ if file and allowed_file(file.filename):
133
+ file_path = save_uploaded_file(file, submission.id)
134
+ file_paths.append(file_path)
135
+
136
+ if not file_paths:
137
+ db.session.rollback()
138
+ return jsonify({'error': 'No valid files uploaded'}), 400
139
+
140
+ # Extract content
141
+ submission.file_paths = json.dumps(file_paths)
142
+ submission.code_content = extract_code_from_files(file_paths)
143
+ submission.documentation_content = extract_documentation(file_paths, project_description)
144
+
145
+ # Evaluate
146
+ print(f"🎯 Starting AI evaluation for project: {project_name}")
147
+ print(f"📁 Files uploaded: {len(file_paths)}")
148
+ print(f"📝 Code content length: {len(submission.code_content)} characters")
149
+ print(f"📄 Documentation length: {len(submission.documentation_content)} characters")
150
+
151
+ eval_engine = get_evaluator()
152
+ scores = eval_engine.evaluate_submission(submission, hackathon)
153
+
154
+ print("🎉 AI evaluation completed!")
155
+ print(f"⭐ Overall score: {scores['overall_score']}/10")
156
+
157
+ # Create evaluation
158
+ evaluation = Evaluation(
159
+ submission_id=submission.id,
160
+ relevance_score=scores['relevance_score'],
161
+ technical_complexity_score=scores['technical_complexity_score'],
162
+ creativity_score=scores['creativity_score'],
163
+ documentation_score=scores['documentation_score'],
164
+ productivity_score=scores['productivity_score'],
165
+ overall_score=scores['overall_score'],
166
+ feedback=scores['feedback'],
167
+ detailed_scores=scores['detailed_scores']
168
+ )
169
+
170
+ submission.evaluated = True
171
+ db.session.add(evaluation)
172
+ db.session.commit()
173
+
174
+ response_data = {
175
+ 'success': True,
176
+ 'id': submission.id,
177
+ 'hackathon_id': hackathon.id,
178
+ 'overall_score': scores['overall_score']
179
+ }
180
+
181
+ print("📤 Sending response to frontend:")
182
+ print(f" ✅ Success: {response_data['success']}")
183
+ print(f" 🆔 Submission ID: {response_data['id']}")
184
+ print(f" 🏆 Hackathon ID: {response_data['hackathon_id']}")
185
+ print(f" ⭐ Overall Score: {response_data['overall_score']}")
186
+ print("=" * 50)
187
+
188
+ return jsonify(response_data), 201
189
+
190
+ except Exception as e:
191
+ db.session.rollback()
192
+ print(f"Error creating submission: {str(e)}")
193
+ import traceback
194
+ traceback.print_exc()
195
+ return jsonify({'error': str(e)}), 500
196
+
197
+ @app.route('/api/hackathon/<int:hackathon_id>/submissions', methods=['GET'])
198
+ def get_hackathon_submissions(hackathon_id):
199
+ """Get all submissions for a hackathon"""
200
+ try:
201
+ hackathon = Hackathon.query.get_or_404(hackathon_id)
202
+ submissions = Submission.query.filter_by(hackathon_id=hackathon_id).order_by(Submission.submitted_at.desc()).all()
203
+
204
+ result = []
205
+ for submission in submissions:
206
+ sub_dict = submission.to_dict()
207
+ if submission.evaluation:
208
+ sub_dict['evaluation'] = submission.evaluation.to_dict()
209
+ result.append(sub_dict)
210
+
211
+ return jsonify(result)
212
+
213
+ except Exception as e:
214
+ print(f"Error getting hackathon submissions: {str(e)}")
215
+ return jsonify({'error': str(e)}), 500
216
+
217
+ @app.route('/api/debug/submissions', methods=['GET'])
218
+ def debug_submissions():
219
+ """Debug endpoint to list all submissions"""
220
+ try:
221
+ submissions = Submission.query.all()
222
+ result = []
223
+ for sub in submissions:
224
+ result.append({
225
+ 'id': sub.id,
226
+ 'project_name': sub.project_name,
227
+ 'team_name': sub.team_name,
228
+ 'evaluated': sub.evaluated,
229
+ 'has_evaluation': sub.evaluation is not None,
230
+ 'submitted_at': sub.submitted_at.isoformat() if sub.submitted_at else None
231
+ })
232
+ return jsonify(result)
233
+ except Exception as e:
234
+ return jsonify({'error': str(e)}), 500
235
+
236
+ @app.route('/api/results/<int:submission_id>', methods=['GET'])
237
+ def get_individual_result(submission_id):
238
+ """Get evaluation results for a specific submission"""
239
+ try:
240
+ print(f"🔍 Looking for submission ID: {submission_id}")
241
+ submission = Submission.query.get(submission_id)
242
+
243
+ if not submission:
244
+ print(f"❌ Submission {submission_id} not found")
245
+ return jsonify({'error': f'Submission {submission_id} not found'}), 404
246
+
247
+ print(f"✅ Found submission: {submission.project_name}")
248
+
249
+ if not submission.evaluation:
250
+ print(f"⚠️ Submission {submission_id} not yet evaluated")
251
+ return jsonify({'error': 'Submission not yet evaluated'}), 404
252
+
253
+ print(f"✅ Evaluation found for submission {submission_id}")
254
+
255
+ result = submission.to_dict()
256
+ result['evaluation'] = submission.evaluation.to_dict()
257
+ result['hackathon'] = submission.hackathon.to_dict()
258
+
259
+ return jsonify(result)
260
+
261
+ except Exception as e:
262
+ print(f"❌ Error getting individual result: {str(e)}")
263
+ import traceback
264
+ traceback.print_exc()
265
+ return jsonify({'error': str(e)}), 500
266
+
267
+ if __name__ == '__main__':
268
+ app.run(debug=True, host='0.0.0.0', port=5000)
269
+
270
+
chunking_utils.py ADDED
@@ -0,0 +1,214 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Utilities for chunking large code content for AI evaluation
3
+ """
4
+
5
+ def chunk_text(text, max_chunk_size=3000, overlap=200):
6
+ """
7
+ Split text into overlapping chunks
8
+
9
+ Args:
10
+ text (str): Text to chunk
11
+ max_chunk_size (int): Maximum characters per chunk
12
+ overlap (int): Number of characters to overlap between chunks
13
+
14
+ Returns:
15
+ list: List of text chunks
16
+ """
17
+ if len(text) <= max_chunk_size:
18
+ return [text]
19
+
20
+ chunks = []
21
+ start = 0
22
+
23
+ while start < len(text):
24
+ # Calculate end position
25
+ end = start + max_chunk_size
26
+
27
+ # If this is not the last chunk, try to break at a natural boundary
28
+ if end < len(text):
29
+ # Look for line breaks near the end
30
+ for i in range(min(100, max_chunk_size // 10)): # Look back up to 100 chars
31
+ if text[end - i] == '\n':
32
+ end = end - i + 1 # Include the newline
33
+ break
34
+
35
+ # Extract chunk
36
+ chunk = text[start:end].strip()
37
+ if chunk:
38
+ chunks.append(chunk)
39
+
40
+ # Move start position (with overlap)
41
+ start = end - overlap if end < len(text) else end
42
+
43
+ # Prevent infinite loop
44
+ if start >= len(text):
45
+ break
46
+
47
+ return chunks
48
+
49
+ def chunk_code_content(code_content, max_chunk_size=3000):
50
+ """
51
+ Intelligently chunk code content, trying to preserve function/class boundaries
52
+
53
+ Args:
54
+ code_content (str): Code content to chunk
55
+ max_chunk_size (int): Maximum characters per chunk
56
+
57
+ Returns:
58
+ list: List of code chunks with metadata
59
+ """
60
+ if len(code_content) <= max_chunk_size:
61
+ return [{
62
+ 'content': code_content,
63
+ 'chunk_id': 1,
64
+ 'total_chunks': 1,
65
+ 'size': len(code_content)
66
+ }]
67
+
68
+ # Split by files first (if multiple files are concatenated)
69
+ file_sections = []
70
+ current_section = ""
71
+
72
+ lines = code_content.split('\n')
73
+ for line in lines:
74
+ # Look for file separators or headers
75
+ if line.startswith('===') or line.startswith('---') or 'File:' in line:
76
+ if current_section.strip():
77
+ file_sections.append(current_section.strip())
78
+ current_section = line + '\n'
79
+ else:
80
+ current_section += line + '\n'
81
+
82
+ # Add the last section
83
+ if current_section.strip():
84
+ file_sections.append(current_section.strip())
85
+
86
+ # If no file sections found, treat as single content
87
+ if len(file_sections) <= 1:
88
+ file_sections = [code_content]
89
+
90
+ # Chunk each file section
91
+ all_chunks = []
92
+ chunk_counter = 1
93
+
94
+ for section in file_sections:
95
+ if len(section) <= max_chunk_size:
96
+ all_chunks.append({
97
+ 'content': section,
98
+ 'chunk_id': chunk_counter,
99
+ 'size': len(section)
100
+ })
101
+ chunk_counter += 1
102
+ else:
103
+ # Split large sections into smaller chunks
104
+ text_chunks = chunk_text(section, max_chunk_size, overlap=300)
105
+ for chunk_text in text_chunks:
106
+ all_chunks.append({
107
+ 'content': chunk_text,
108
+ 'chunk_id': chunk_counter,
109
+ 'size': len(chunk_text)
110
+ })
111
+ chunk_counter += 1
112
+
113
+ # Add total_chunks to all chunks
114
+ total_chunks = len(all_chunks)
115
+ for chunk in all_chunks:
116
+ chunk['total_chunks'] = total_chunks
117
+
118
+ return all_chunks
119
+
120
+ def create_chunk_summary(chunks):
121
+ """
122
+ Create a summary of all chunks for context
123
+
124
+ Args:
125
+ chunks (list): List of chunk dictionaries
126
+
127
+ Returns:
128
+ str: Summary of chunks
129
+ """
130
+ total_size = sum(chunk['size'] for chunk in chunks)
131
+
132
+ summary = f"""
133
+ Code Analysis Summary:
134
+ - Total chunks: {len(chunks)}
135
+ - Total content size: {total_size:,} characters
136
+ - Average chunk size: {total_size // len(chunks):,} characters
137
+
138
+ Chunk breakdown:
139
+ """
140
+
141
+ for i, chunk in enumerate(chunks, 1):
142
+ preview = chunk['content'][:100].replace('\n', ' ')
143
+ summary += f" Chunk {i}: {chunk['size']:,} chars - {preview}...\n"
144
+
145
+ return summary
146
+
147
+ def combine_chunk_evaluations(chunk_results):
148
+ """
149
+ Combine evaluation results from multiple chunks
150
+
151
+ Args:
152
+ chunk_results (list): List of evaluation results from each chunk
153
+
154
+ Returns:
155
+ dict: Combined evaluation result
156
+ """
157
+ if not chunk_results:
158
+ return {
159
+ 'relevance_score': 5.0,
160
+ 'technical_complexity_score': 5.0,
161
+ 'creativity_score': 5.0,
162
+ 'documentation_score': 5.0,
163
+ 'productivity_score': 5.0,
164
+ 'overall_score': 5.0,
165
+ 'feedback': 'No evaluation results to combine.',
166
+ 'detailed_scores': '{}'
167
+ }
168
+
169
+ if len(chunk_results) == 1:
170
+ return chunk_results[0]
171
+
172
+ # Calculate weighted averages based on chunk sizes
173
+ total_weight = sum(result.get('chunk_weight', 1) for result in chunk_results)
174
+
175
+ combined_scores = {
176
+ 'relevance_score': 0,
177
+ 'technical_complexity_score': 0,
178
+ 'creativity_score': 0,
179
+ 'documentation_score': 0,
180
+ 'productivity_score': 0
181
+ }
182
+
183
+ feedbacks = []
184
+
185
+ for result in chunk_results:
186
+ weight = result.get('chunk_weight', 1) / total_weight
187
+
188
+ for score_key in combined_scores:
189
+ combined_scores[score_key] += result.get(score_key, 5.0) * weight
190
+
191
+ if result.get('feedback'):
192
+ feedbacks.append(f"Chunk {result.get('chunk_id', '?')}: {result['feedback']}")
193
+
194
+ # Calculate overall score
195
+ overall_score = sum(combined_scores.values()) / len(combined_scores)
196
+
197
+ # Combine feedback
198
+ combined_feedback = f"""
199
+ Multi-chunk evaluation completed ({len(chunk_results)} chunks analyzed):
200
+
201
+ """ + "\n\n".join(feedbacks)
202
+
203
+ return {
204
+ 'relevance_score': round(combined_scores['relevance_score'], 1),
205
+ 'technical_complexity_score': round(combined_scores['technical_complexity_score'], 1),
206
+ 'creativity_score': round(combined_scores['creativity_score'], 1),
207
+ 'documentation_score': round(combined_scores['documentation_score'], 1),
208
+ 'productivity_score': round(combined_scores['productivity_score'], 1),
209
+ 'overall_score': round(overall_score, 1),
210
+ 'feedback': combined_feedback,
211
+ 'detailed_scores': '{"note": "Combined from multiple chunks"}'
212
+ }
213
+
214
+
config.py ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from dotenv import load_dotenv
3
+
4
+ load_dotenv()
5
+
6
+ class Config:
7
+ SECRET_KEY = os.getenv('FLASK_SECRET_KEY', 'dev-secret-key-change-in-production')
8
+ SQLALCHEMY_DATABASE_URI = os.getenv('DATABASE_URL', 'sqlite:///evalai_new.db')
9
+ SQLALCHEMY_TRACK_MODIFICATIONS = False
10
+ UPLOAD_FOLDER = 'uploads'
11
+ MAX_CONTENT_LENGTH = 5 * 1024 * 1024 * 1024 # 5GB max file size
12
+ ALLOWED_EXTENSIONS = {
13
+ # Core programming languages
14
+ 'py', 'js', 'ts', 'jsx', 'tsx', 'java', 'cpp', 'c', 'h', 'hpp', 'cs', 'php', 'rb', 'go', 'rs', 'swift', 'kt', 'scala', 'r', 'matlab', 'm',
15
+
16
+ # Web technologies
17
+ 'html', 'htm', 'css', 'scss', 'sass', 'less', 'vue', 'svelte', 'jsx', 'tsx',
18
+
19
+ # Configuration and data
20
+ 'json', 'xml', 'yml', 'yaml', 'toml', 'ini', 'cfg', 'conf', 'env', 'properties',
21
+
22
+ # Documentation
23
+ 'md', 'txt', 'rst', 'adoc', 'tex',
24
+
25
+ # Scripts and automation
26
+ 'sh', 'bash', 'zsh', 'fish', 'ps1', 'bat', 'cmd',
27
+
28
+ # Database and queries
29
+ 'sql', 'sqlite', 'db',
30
+
31
+ # Mobile development
32
+ 'dart', 'flutter', 'xaml',
33
+
34
+ # Data science and ML
35
+ 'ipynb', 'rmd', 'py', 'r',
36
+
37
+ # Build and deployment
38
+ 'dockerfile', 'docker-compose', 'makefile', 'cmake', 'gradle', 'maven',
39
+
40
+ # Archives
41
+ 'zip', 'tar', 'gz',
42
+
43
+ # Office documents (for documentation)
44
+ 'pdf', 'doc', 'docx', 'ppt', 'pptx', 'xls', 'xlsx'
45
+ }
46
+
47
+ # RAG Configuration
48
+ # EVALUATION_MODEL = 'opensource' # 'opensource' (Llama-3-8B) or 'openai' (GPT-4) ⭐ USING OPENAI
49
+ EVALUATION_MODEL = 'openai' # 'opensource' (Llama-3-8B) or 'openai' (GPT-4) ⭐ USING OPENAI
50
+ OPENAI_API_KEY = os.getenv('OPENAI_API_KEY', '') # Set in environment or .env file
51
+ EMBEDDING_MODEL = 'all-MiniLM-L6-v2' # Can use 'microsoft/unixcoder-base' for code-specific
52
+ CHUNK_SIZE = 512 # Token size for chunks
53
+ CHUNK_OVERLAP = 128 # Overlap between chunks
54
+ RETRIEVAL_TOP_K = 8 # Number of chunks to retrieve
55
+ MAX_CONTEXT_TOKENS = 2000 # Max tokens to send to LLM
56
+
57
+ # LLM Configuration
58
+ LLM_MODEL = 'meta-llama/Meta-Llama-3-8B-Instruct' # Main evaluation model
59
+ LLM_TEMPERATURE = 0.3 # Lower = more deterministic
60
+ LLM_MAX_TOKENS = 512 # Max tokens in response
61
+ USE_QUANTIZATION = True # Use 4-bit quantization (faster & uses ~4GB VRAM instead of 16GB)
62
+
63
+
evaluator.py ADDED
@@ -0,0 +1,384 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import openai
3
+ from openai import OpenAI
4
+ import json
5
+ import re
6
+ from config import Config
7
+ from chunking_utils import chunk_code_content, combine_chunk_evaluations, create_chunk_summary
8
+
9
+ class AIEvaluator:
10
+ def __init__(self):
11
+ self.model = Config.EVALUATION_MODEL
12
+ if self.model == 'openai':
13
+ # Set API key for OpenAI
14
+ if not Config.OPENAI_API_KEY:
15
+ raise ValueError("OpenAI API key not found. Please set OPENAI_API_KEY in your environment or .env file.")
16
+
17
+ try:
18
+ # Initialize OpenAI client (v1.0+ style)
19
+ self.client = OpenAI(api_key=Config.OPENAI_API_KEY)
20
+ print("✅ OpenAI client initialized successfully")
21
+ except Exception as e:
22
+ print(f"❌ Error initializing OpenAI client: {e}")
23
+ raise e
24
+
25
+ def evaluate_submission(self, submission, hackathon):
26
+ """
27
+ Evaluate a submission based on the hackathon criteria
28
+ """
29
+ if self.model == 'openai':
30
+ # Check if content is too large and needs chunking
31
+ code_content = submission.code_content or ""
32
+ doc_content = submission.documentation_content or ""
33
+ total_content = code_content + doc_content
34
+
35
+ # If content is large, use chunked evaluation
36
+ if len(total_content) > 3000: # 3K characters threshold (force chunking earlier)
37
+ print(f"📊 Large content detected ({len(total_content):,} chars), using chunked evaluation...")
38
+ return self._evaluate_with_chunking(submission, hackathon)
39
+ else:
40
+ print(f"📊 Standard evaluation for content ({len(total_content):,} chars)...")
41
+ return self._evaluate_with_openai(submission, hackathon)
42
+ else:
43
+ return self._evaluate_with_unixcoder(submission, hackathon)
44
+
45
+ def _evaluate_with_openai(self, submission, hackathon):
46
+ """
47
+ Use OpenAI GPT-4 to evaluate the submission
48
+ """
49
+ evaluation_prompt = self._build_evaluation_prompt(submission, hackathon)
50
+
51
+ print("📋 EVALUATION PROMPT BEING SENT:")
52
+ print("=" * 60)
53
+ print(evaluation_prompt[:500] + "..." if len(evaluation_prompt) > 500 else evaluation_prompt)
54
+ print("=" * 60)
55
+
56
+ try:
57
+ print("🚀 Sending request to OpenAI GPT-4o...")
58
+ print(f"📝 Evaluation prompt length: {len(evaluation_prompt)} characters")
59
+
60
+ # Use OpenAI client to generate evaluation
61
+ response = self.client.chat.completions.create(
62
+ model="gpt-4o", # Using GPT-4o for best quality and speed
63
+ messages=[
64
+ {"role": "system", "content": "You are a STRICT technical evaluator and hackathon judge. You must be critical, use the full scoring range 0-10, and provide differentiated scores. DO NOT give grade inflation. Most projects should score in the 4-7 range. Be harsh but fair."},
65
+ {"role": "user", "content": evaluation_prompt}
66
+ ],
67
+ temperature=0.1, # Lower temperature for more consistent, strict evaluation
68
+ max_tokens=2000
69
+ )
70
+
71
+ result_text = response.choices[0].message.content
72
+ print("✅ OpenAI Response received!")
73
+ print("=" * 80)
74
+ print("🤖 OPENAI GPT-4o RESPONSE:")
75
+ print("=" * 80)
76
+ print(result_text)
77
+ print("=" * 80)
78
+ print(f"📊 Response length: {len(result_text)} characters")
79
+ print(f"💰 Tokens used: {response.usage.total_tokens if hasattr(response, 'usage') else 'Unknown'}")
80
+
81
+ parsed_result = self._parse_evaluation_result(result_text)
82
+ print("✅ Response parsed successfully!")
83
+ print(f"📈 Parsed scores: {parsed_result}")
84
+
85
+ return parsed_result
86
+
87
+ except Exception as e:
88
+ print(f"❌ Error in OpenAI evaluation: {str(e)}")
89
+ print("🔄 Falling back to default scores...")
90
+ return self._generate_fallback_scores()
91
+
92
+ def _evaluate_with_chunking(self, submission, hackathon):
93
+ """
94
+ Evaluate large submissions by chunking the content
95
+ """
96
+ try:
97
+ # Chunk the code content
98
+ code_content = submission.code_content or ""
99
+ chunks = chunk_code_content(code_content, max_chunk_size=4000)
100
+
101
+ print(f"📦 Created {len(chunks)} chunks for evaluation")
102
+ print(create_chunk_summary(chunks))
103
+
104
+ chunk_results = []
105
+
106
+ for i, chunk in enumerate(chunks, 1):
107
+ print(f"🔍 Evaluating chunk {i}/{len(chunks)} ({chunk['size']:,} chars)...")
108
+
109
+ # Create a temporary submission object for this chunk
110
+ chunk_submission = type('ChunkSubmission', (), {
111
+ 'code_content': chunk['content'],
112
+ 'documentation_content': submission.documentation_content or "",
113
+ 'project_name': f"{submission.project_name} (Chunk {i})",
114
+ 'project_description': submission.project_description,
115
+ 'team_name': submission.team_name,
116
+ 'participant_email': submission.participant_email
117
+ })()
118
+
119
+ # Evaluate this chunk
120
+ chunk_result = self._evaluate_with_openai(chunk_submission, hackathon)
121
+
122
+ # Add chunk metadata
123
+ chunk_result['chunk_id'] = i
124
+ chunk_result['chunk_weight'] = chunk['size'] # Weight by content size
125
+
126
+ chunk_results.append(chunk_result)
127
+
128
+ print(f"✅ Chunk {i} evaluated: {chunk_result['overall_score']}/10")
129
+
130
+ # Combine results from all chunks
131
+ print("🔄 Combining results from all chunks...")
132
+ combined_result = combine_chunk_evaluations(chunk_results)
133
+
134
+ print(f"🎯 Final combined score: {combined_result['overall_score']}/10")
135
+ return combined_result
136
+
137
+ except Exception as e:
138
+ print(f"❌ Error in chunked evaluation: {str(e)}")
139
+ print("🔄 Falling back to standard evaluation...")
140
+ # Fallback to standard evaluation with truncated content
141
+ return self._evaluate_with_openai_truncated(submission, hackathon)
142
+
143
+ def _evaluate_with_openai_truncated(self, submission, hackathon):
144
+ """
145
+ Evaluate with truncated content as fallback
146
+ """
147
+ # Truncate content to manageable size
148
+ code_content = (submission.code_content or "")[:4000]
149
+ doc_content = (submission.documentation_content or "")[:2000]
150
+
151
+ # Create truncated submission
152
+ truncated_submission = type('TruncatedSubmission', (), {
153
+ 'code_content': code_content,
154
+ 'documentation_content': doc_content,
155
+ 'project_name': submission.project_name,
156
+ 'project_description': submission.project_description,
157
+ 'team_name': submission.team_name,
158
+ 'participant_email': submission.participant_email
159
+ })()
160
+
161
+ print("⚠️ Using truncated content for evaluation")
162
+ result = self._evaluate_with_openai(truncated_submission, hackathon)
163
+
164
+ # Keep feedback as-is without prefixing a truncation note
165
+ return result
166
+
167
+ def _build_evaluation_prompt(self, submission, hackathon):
168
+ """
169
+ Build the prompt for AI evaluation
170
+ """
171
+ criteria = json.loads(hackathon.criteria) if hackathon.criteria else self._get_default_criteria()
172
+
173
+ prompt = f"""
174
+ # STRICT Hackathon Evaluation - NO GRADE INFLATION
175
+
176
+ ## Hackathon Information
177
+ **Name**: {hackathon.name}
178
+ **Theme/Description**: {hackathon.description}
179
+
180
+ ## Evaluation Criteria
181
+ {hackathon.evaluation_prompt}
182
+
183
+ ## Submission to Evaluate
184
+ **Team**: {submission.team_name}
185
+ **Project Name**: {submission.project_name}
186
+ **Description**: {submission.project_description}
187
+
188
+ ### Code Content
189
+ ```
190
+ {self._truncate_content(submission.code_content, 3000)}
191
+ ```
192
+
193
+ ### Documentation
194
+ ```
195
+ {self._truncate_content(submission.documentation_content, 2000)}
196
+ ```
197
+
198
+ ## CRITICAL EVALUATION INSTRUCTIONS
199
+
200
+ You are a STRICT technical evaluator. Use the FULL range of scores 0-10. DO NOT give similar scores to different projects.
201
+
202
+ ### SCORING GUIDELINES (BE HARSH AND REALISTIC):
203
+
204
+ **0-2: Poor/Failing**
205
+ - Major issues, non-functional, or completely irrelevant
206
+ - Severe security vulnerabilities or broken code
207
+ - No documentation or completely unclear
208
+
209
+ **3-4: Below Average**
210
+ - Basic functionality but significant flaws
211
+ - Poor code quality, structure, or practices
212
+ - Minimal effort or incomplete implementation
213
+
214
+ **5-6: Average/Acceptable**
215
+ - Works as intended with minor issues
216
+ - Standard implementation, nothing special
217
+ - Adequate documentation and code quality
218
+
219
+ **7-8: Good/Above Average**
220
+ - Well-implemented with good practices
221
+ - Shows clear understanding and effort
222
+ - Good documentation and structure
223
+
224
+ **9-10: Excellent/Outstanding**
225
+ - Exceptional quality, innovative approach
226
+ - Production-ready code with best practices
227
+ - Comprehensive documentation and testing
228
+
229
+ ## STRICT EVALUATION CRITERIA:
230
+
231
+ 1. **Relevance (0-10)**: Does it ACTUALLY solve the problem stated? Is it directly related to the theme?
232
+ 2. **Technical Complexity (0-10)**: How sophisticated is the implementation? Rate based on actual technical depth, not just lines of code.
233
+ 3. **Creativity (0-10)**: Is this a unique approach or just a standard tutorial implementation?
234
+ 4. **Documentation (0-10)**: Is there proper README, comments, setup instructions? Can someone else run this?
235
+ 5. **Productivity (0-10)**: Code organization, error handling, scalability, maintainability.
236
+
237
+ ## ADDITIONAL KEY-POINT ANALYSIS (brief, 1-2 sentences each):
238
+ - Out of the box thinking: How original/novel is the approach?
239
+ - Problem-solving skills: How effectively does the code decompose and solve the problem?
240
+ - Research capabilities: Evidence of learning, citations, comparisons, benchmarking, or exploration
241
+ - Understanding the business: Does it align with real user/business needs and constraints?
242
+ - Use of non-famous tools or frameworks: Any lesser-known tech used purposefully
243
+
244
+ ## MANDATORY REQUIREMENTS:
245
+ - VARY your scores significantly between projects
246
+ - Use decimals (e.g., 3.2, 6.7, 8.1) for precision
247
+ - Be CRITICAL and identify real weaknesses
248
+ - NO GRADE INFLATION - most projects should score 4-7 range
249
+ - Only exceptional projects deserve 8-10
250
+ - Don't hesitate to give low scores (1-3) for poor work
251
+
252
+ ## Response Format (STRICT JSON):
253
+
254
+ ```json
255
+ {{
256
+ "relevance_score": <precise score 0-10 with 1 decimal>,
257
+ "technical_complexity_score": <precise score 0-10 with 1 decimal>,
258
+ "creativity_score": <precise score 0-10 with 1 decimal>,
259
+ "documentation_score": <precise score 0-10 with 1 decimal>,
260
+ "productivity_score": <precise score 0-10 with 1 decimal>,
261
+ "overall_score": <calculated average with 1 decimal>,
262
+ "feedback": "<HONEST, CRITICAL feedback. Point out specific flaws, missing features, and areas for improvement. Don't sugarcoat.>",
263
+ "detailed_scores": {{
264
+ "relevance_justification": "<specific reasons for this score>",
265
+ "technical_justification": "<specific technical assessment>",
266
+ "creativity_justification": "<specific creativity assessment>",
267
+ "documentation_justification": "<specific documentation assessment>",
268
+ "productivity_justification": "<specific code quality assessment>",
269
+
270
+ "out_of_box_thinking": "<1-2 sentence assessment>",
271
+ "problem_solving_skills": "<1-2 sentence assessment>",
272
+ "research_capabilities": "<1-2 sentence assessment>",
273
+ "business_understanding": "<1-2 sentence assessment>",
274
+ "non_famous_tools_usage": "<1-2 sentence assessment>"
275
+ }}
276
+ }}
277
+ ```
278
+
279
+ REMEMBER: Be a tough but fair judge. Real-world projects have flaws - identify them!
280
+ """
281
+ return prompt
282
+
283
+ def _truncate_content(self, content, max_length=2000):
284
+ """
285
+ Truncate content to fit within token limits
286
+ """
287
+ if not content:
288
+ return "No content provided"
289
+
290
+ if len(content) > max_length:
291
+ return content[:max_length] + "\n... [content truncated]"
292
+ return content
293
+
294
+ def _parse_evaluation_result(self, result_text):
295
+ """
296
+ Parse the AI response into structured scores
297
+ """
298
+ try:
299
+ # Try to extract JSON from the response
300
+ json_match = re.search(r'```json\s*(.*?)\s*```', result_text, re.DOTALL)
301
+ if json_match:
302
+ json_str = json_match.group(1)
303
+ else:
304
+ # Try to find any JSON object in the response
305
+ json_match = re.search(r'\{.*\}', result_text, re.DOTALL)
306
+ json_str = json_match.group(0) if json_match else result_text
307
+
308
+ scores = json.loads(json_str)
309
+
310
+ # Validate and normalize scores
311
+ return {
312
+ 'relevance_score': self._normalize_score(scores.get('relevance_score', 5.0)),
313
+ 'technical_complexity_score': self._normalize_score(scores.get('technical_complexity_score', 5.0)),
314
+ 'creativity_score': self._normalize_score(scores.get('creativity_score', 5.0)),
315
+ 'documentation_score': self._normalize_score(scores.get('documentation_score', 5.0)),
316
+ 'productivity_score': self._normalize_score(scores.get('productivity_score', 5.0)),
317
+ 'overall_score': self._normalize_score(scores.get('overall_score', 5.0)),
318
+ 'feedback': scores.get('feedback', 'Evaluation completed.'),
319
+ 'detailed_scores': json.dumps(scores.get('detailed_scores', {}))
320
+ }
321
+ except Exception as e:
322
+ print(f"Error parsing evaluation result: {str(e)}")
323
+ # Return fallback scores if parsing fails
324
+ return self._generate_fallback_scores()
325
+
326
+ def _normalize_score(self, score):
327
+ """
328
+ Ensure score is between 0 and 10
329
+ """
330
+ try:
331
+ score = float(score)
332
+ return max(0.0, min(10.0, score))
333
+ except:
334
+ return 5.0
335
+
336
+ def _generate_fallback_scores(self):
337
+ """
338
+ Generate varied fallback scores when evaluation fails
339
+ """
340
+ import random
341
+
342
+ # Generate varied scores in the 4-6 range (realistic fallback)
343
+ scores = {
344
+ 'relevance_score': round(random.uniform(4.0, 6.5), 1),
345
+ 'technical_complexity_score': round(random.uniform(3.5, 6.0), 1),
346
+ 'creativity_score': round(random.uniform(3.0, 5.5), 1),
347
+ 'documentation_score': round(random.uniform(2.5, 5.0), 1),
348
+ 'productivity_score': round(random.uniform(3.5, 6.0), 1)
349
+ }
350
+
351
+ # Calculate overall as average
352
+ overall = sum(scores.values()) / len(scores)
353
+
354
+ return {
355
+ **scores,
356
+ 'overall_score': round(overall, 1),
357
+ 'feedback': 'Automatic evaluation completed due to technical issue. Scores are estimated based on basic analysis. Manual review strongly recommended for accurate assessment.',
358
+ 'detailed_scores': json.dumps({
359
+ 'note': 'Fallback scores - technical evaluation failed',
360
+ 'recommendation': 'Manual review required for accurate scoring'
361
+ })
362
+ }
363
+
364
+ def _get_default_criteria(self):
365
+ """
366
+ Get default evaluation criteria
367
+ """
368
+ return [
369
+ {'name': 'Relevance', 'weight': 0.20},
370
+ {'name': 'Technical Complexity', 'weight': 0.20},
371
+ {'name': 'Creativity', 'weight': 0.20},
372
+ {'name': 'Documentation', 'weight': 0.20},
373
+ {'name': 'Productivity', 'weight': 0.20}
374
+ ]
375
+
376
+ def _evaluate_with_unixcoder(self, submission, hackathon):
377
+ """
378
+ Use UniXCoder for evaluation (placeholder for future implementation)
379
+ """
380
+ # This would use UniXCoder embeddings and similarity scoring
381
+ # For MVP, we'll fall back to OpenAI
382
+ return self._evaluate_with_openai(submission, hackathon)
383
+
384
+
index.html CHANGED
@@ -4,7 +4,7 @@
4
  <meta charset="UTF-8" />
5
  <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
- <title>Vite + Svelte + TS</title>
8
  </head>
9
  <body>
10
  <div id="app"></div>
 
4
  <meta charset="UTF-8" />
5
  <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>EvalAI - AI Project Evaluator</title>
8
  </head>
9
  <body>
10
  <div id="app"></div>
models.py ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask_sqlalchemy import SQLAlchemy
2
+ from datetime import datetime, timezone
3
+ import json
4
+
5
+ db = SQLAlchemy()
6
+
7
+ class Hackathon(db.Model):
8
+ __tablename__ = 'hackathons'
9
+
10
+ id = db.Column(db.Integer, primary_key=True)
11
+ name = db.Column(db.String(200), nullable=False)
12
+ description = db.Column(db.Text, nullable=False)
13
+ evaluation_prompt = db.Column(db.Text, nullable=False)
14
+ criteria = db.Column(db.Text, nullable=False) # JSON string of criteria
15
+ created_at = db.Column(db.DateTime, default=datetime.utcnow)
16
+ deadline = db.Column(db.DateTime)
17
+ host_email = db.Column(db.String(200))
18
+
19
+ submissions = db.relationship('Submission', backref='hackathon', lazy=True, cascade='all, delete-orphan')
20
+
21
+ def to_dict(self):
22
+ return {
23
+ 'id': self.id,
24
+ 'name': self.name,
25
+ 'description': self.description,
26
+ 'evaluation_prompt': self.evaluation_prompt,
27
+ 'criteria': json.loads(self.criteria) if self.criteria else [],
28
+ 'created_at': self.created_at.isoformat(),
29
+ 'deadline': self.deadline.isoformat() if self.deadline else None,
30
+ 'host_email': self.host_email,
31
+ 'submission_count': len(self.submissions)
32
+ }
33
+
34
+
35
+ class Submission(db.Model):
36
+ __tablename__ = 'submissions'
37
+
38
+ id = db.Column(db.Integer, primary_key=True)
39
+ hackathon_id = db.Column(db.Integer, db.ForeignKey('hackathons.id'), nullable=False)
40
+ team_name = db.Column(db.String(200), nullable=False)
41
+ participant_email = db.Column(db.String(200), nullable=False)
42
+ project_name = db.Column(db.String(200), nullable=False)
43
+ project_description = db.Column(db.Text)
44
+ code_content = db.Column(db.Text) # Extracted code content
45
+ documentation_content = db.Column(db.Text) # Extracted documentation
46
+ file_paths = db.Column(db.Text) # JSON string of uploaded file paths
47
+ submitted_at = db.Column(db.DateTime, default=datetime.utcnow)
48
+ evaluated = db.Column(db.Boolean, default=False)
49
+
50
+ evaluation = db.relationship('Evaluation', backref='submission', uselist=False, cascade='all, delete-orphan')
51
+
52
+ def to_dict(self):
53
+ return {
54
+ 'id': self.id,
55
+ 'hackathon_id': self.hackathon_id,
56
+ 'team_name': self.team_name,
57
+ 'participant_email': self.participant_email,
58
+ 'project_name': self.project_name,
59
+ 'project_description': self.project_description,
60
+ 'submitted_at': self.submitted_at.isoformat(),
61
+ 'evaluated': self.evaluated,
62
+ 'file_count': len(json.loads(self.file_paths)) if self.file_paths else 0
63
+ }
64
+
65
+
66
+ class Evaluation(db.Model):
67
+ __tablename__ = 'evaluations'
68
+
69
+ id = db.Column(db.Integer, primary_key=True)
70
+ submission_id = db.Column(db.Integer, db.ForeignKey('submissions.id'), nullable=False)
71
+ relevance_score = db.Column(db.Float, default=0.0)
72
+ technical_complexity_score = db.Column(db.Float, default=0.0)
73
+ creativity_score = db.Column(db.Float, default=0.0)
74
+ documentation_score = db.Column(db.Float, default=0.0)
75
+ productivity_score = db.Column(db.Float, default=0.0) # NEW: 5th evaluation metric
76
+ overall_score = db.Column(db.Float, default=0.0)
77
+ feedback = db.Column(db.Text) # AI-generated feedback
78
+ detailed_scores = db.Column(db.Text) # JSON string of detailed criteria scores
79
+ evaluated_at = db.Column(db.DateTime, default=datetime.utcnow)
80
+
81
+ def to_dict(self):
82
+ return {
83
+ 'id': self.id,
84
+ 'submission_id': self.submission_id,
85
+ 'relevance_score': self.relevance_score,
86
+ 'technical_complexity_score': self.technical_complexity_score,
87
+ 'creativity_score': self.creativity_score,
88
+ 'documentation_score': self.documentation_score,
89
+ 'productivity_score': self.productivity_score,
90
+ 'overall_score': self.overall_score,
91
+ 'feedback': self.feedback,
92
+ 'detailed_scores': json.loads(self.detailed_scores) if self.detailed_scores else {},
93
+ # Ensure UTC marker so clients can convert correctly
94
+ 'evaluated_at': (
95
+ (self.evaluated_at.replace(tzinfo=timezone.utc) if self.evaluated_at.tzinfo is None else self.evaluated_at.astimezone(timezone.utc))
96
+ .isoformat()
97
+ .replace('+00:00', 'Z')
98
+ )
99
+ }
100
+
101
+
package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
package.json CHANGED
@@ -1,7 +1,7 @@
1
  {
2
- "name": "svelte",
3
  "private": true,
4
- "version": "0.0.0",
5
  "type": "module",
6
  "scripts": {
7
  "dev": "vite",
@@ -9,11 +9,18 @@
9
  "preview": "vite preview",
10
  "check": "svelte-check --tsconfig ./tsconfig.app.json && tsc -p tsconfig.node.json"
11
  },
 
 
 
 
12
  "devDependencies": {
13
  "@sveltejs/vite-plugin-svelte": "^5.0.3",
14
  "@tsconfig/svelte": "^5.0.4",
 
 
15
  "svelte": "^5.28.1",
16
  "svelte-check": "^4.1.6",
 
17
  "typescript": "~5.8.3",
18
  "vite": "^6.3.5"
19
  }
 
1
  {
2
+ "name": "evalai-frontend",
3
  "private": true,
4
+ "version": "1.0.0",
5
  "type": "module",
6
  "scripts": {
7
  "dev": "vite",
 
9
  "preview": "vite preview",
10
  "check": "svelte-check --tsconfig ./tsconfig.app.json && tsc -p tsconfig.node.json"
11
  },
12
+ "dependencies": {
13
+ "axios": "^1.6.2",
14
+ "svelte-routing": "^2.13.0"
15
+ },
16
  "devDependencies": {
17
  "@sveltejs/vite-plugin-svelte": "^5.0.3",
18
  "@tsconfig/svelte": "^5.0.4",
19
+ "autoprefixer": "^10.4.16",
20
+ "postcss": "^8.4.32",
21
  "svelte": "^5.28.1",
22
  "svelte-check": "^4.1.6",
23
+ "tailwindcss": "^3.3.6",
24
  "typescript": "~5.8.3",
25
  "vite": "^6.3.5"
26
  }
postcss.config.js ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ export default {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {},
5
+ },
6
+ }
7
+
requirements.txt ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ flask
2
+ flask-cors
3
+ flask-sqlalchemy
4
+ python-dotenv
5
+ werkzeug
6
+ openai>=1.0.0
7
+
8
+ # Optional utilities
9
+ json-repair
10
+
src/App.svelte CHANGED
@@ -1,47 +1,35 @@
1
  <script lang="ts">
2
- import svelteLogo from './assets/svelte.svg'
3
- import viteLogo from '/vite.svg'
4
- import Counter from './lib/Counter.svelte'
5
- </script>
6
-
7
- <main>
8
- <div>
9
- <a href="https://vite.dev" target="_blank" rel="noreferrer">
10
- <img src={viteLogo} class="logo" alt="Vite Logo" />
11
- </a>
12
- <a href="https://svelte.dev" target="_blank" rel="noreferrer">
13
- <img src={svelteLogo} class="logo svelte" alt="Svelte Logo" />
14
- </a>
15
- </div>
16
- <h1>Vite + Svelte</h1>
17
 
18
- <div class="card">
19
- <Counter />
20
- </div>
21
 
22
- <p>
23
- Check out <a href="https://github.com/sveltejs/kit#readme" target="_blank" rel="noreferrer">SvelteKit</a>, the official Svelte app framework powered by Vite!
24
- </p>
 
25
 
26
- <p class="read-the-docs">
27
- Click on the Vite and Svelte logos to learn more
28
- </p>
29
- </main>
 
 
 
 
 
 
 
 
30
 
31
- <style>
32
- .logo {
33
- height: 6em;
34
- padding: 1.5em;
35
- will-change: filter;
36
- transition: filter 300ms;
37
- }
38
- .logo:hover {
39
- filter: drop-shadow(0 0 2em #646cffaa);
40
- }
41
- .logo.svelte:hover {
42
- filter: drop-shadow(0 0 2em #ff3e00aa);
43
- }
44
- .read-the-docs {
45
- color: #888;
46
- }
47
- </style>
 
1
  <script lang="ts">
2
+ import { currentPath } from './lib/router';
3
+ import ProjectEvaluator from './pages/ProjectEvaluator.svelte';
4
+ import AllResults from './pages/AllResults.svelte';
5
+ import IndividualResult from './pages/IndividualResult.svelte';
6
+ import { onMount } from 'svelte';
 
 
 
 
 
 
 
 
 
 
7
 
8
+ let path = $state('/');
9
+ let submissionId = $state<string | undefined>(undefined);
 
10
 
11
+ // Robustly subscribe to router changes (works in Svelte 5 runes)
12
+ onMount(() => {
13
+ const unsubscribe = currentPath.subscribe((p: string) => {
14
+ path = p;
15
 
16
+ // Parse submission ID from path like /result/123
17
+ const resultMatch = p.match(/^\/result\/(\d+)$/);
18
+ if (resultMatch) {
19
+ submissionId = resultMatch[1];
20
+ path = '/result';
21
+ } else {
22
+ submissionId = undefined;
23
+ }
24
+ });
25
+ return () => unsubscribe();
26
+ });
27
+ </script>
28
 
29
+ {#if path === '/results'}
30
+ <AllResults />
31
+ {:else if path === '/result' && submissionId}
32
+ <IndividualResult {submissionId} />
33
+ {:else}
34
+ <ProjectEvaluator />
35
+ {/if}
 
 
 
 
 
 
 
 
 
 
src/app.css CHANGED
@@ -1,79 +1,18 @@
 
 
 
 
1
  :root {
2
- font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
3
  line-height: 1.5;
4
  font-weight: 400;
5
-
6
- color-scheme: light dark;
7
- color: rgba(255, 255, 255, 0.87);
8
- background-color: #242424;
9
-
10
- font-synthesis: none;
11
- text-rendering: optimizeLegibility;
12
- -webkit-font-smoothing: antialiased;
13
- -moz-osx-font-smoothing: grayscale;
14
- }
15
-
16
- a {
17
- font-weight: 500;
18
- color: #646cff;
19
- text-decoration: inherit;
20
- }
21
- a:hover {
22
- color: #535bf2;
23
  }
24
 
25
  body {
26
  margin: 0;
27
- display: flex;
28
- place-items: center;
29
- min-width: 320px;
30
  min-height: 100vh;
31
  }
32
 
33
- h1 {
34
- font-size: 3.2em;
35
- line-height: 1.1;
36
- }
37
-
38
- .card {
39
- padding: 2em;
40
- }
41
-
42
  #app {
43
- max-width: 1280px;
44
- margin: 0 auto;
45
- padding: 2rem;
46
- text-align: center;
47
- }
48
-
49
- button {
50
- border-radius: 8px;
51
- border: 1px solid transparent;
52
- padding: 0.6em 1.2em;
53
- font-size: 1em;
54
- font-weight: 500;
55
- font-family: inherit;
56
- background-color: #1a1a1a;
57
- cursor: pointer;
58
- transition: border-color 0.25s;
59
- }
60
- button:hover {
61
- border-color: #646cff;
62
- }
63
- button:focus,
64
- button:focus-visible {
65
- outline: 4px auto -webkit-focus-ring-color;
66
- }
67
-
68
- @media (prefers-color-scheme: light) {
69
- :root {
70
- color: #213547;
71
- background-color: #ffffff;
72
- }
73
- a:hover {
74
- color: #747bff;
75
- }
76
- button {
77
- background-color: #f9f9f9;
78
- }
79
  }
 
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
  :root {
6
+ font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
7
  line-height: 1.5;
8
  font-weight: 400;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  }
10
 
11
  body {
12
  margin: 0;
 
 
 
13
  min-height: 100vh;
14
  }
15
 
 
 
 
 
 
 
 
 
 
16
  #app {
17
+ min-height: 100vh;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  }
src/components/BarChart.svelte ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ interface Props {
3
+ score: number;
4
+ label: string;
5
+ maxScore?: number;
6
+ color?: string;
7
+ }
8
+
9
+ let { score, label, maxScore = 10, color }: Props = $props();
10
+
11
+ const percentage = (score / maxScore) * 100;
12
+
13
+ // Color scheme based on score
14
+ const getColor = (s: number) => {
15
+ if (color) return color;
16
+ if (s >= 8) return 'bg-green-500';
17
+ if (s >= 6) return 'bg-blue-500';
18
+ if (s >= 4) return 'bg-orange-500';
19
+ return 'bg-red-500';
20
+ };
21
+
22
+ const barColor = getColor(score);
23
+ </script>
24
+
25
+ <div class="space-y-2">
26
+ <div class="flex justify-between items-center">
27
+ <span class="text-sm font-semibold text-gray-700">{label}</span>
28
+ <span class="text-sm font-bold text-gray-900">{score.toFixed(1)} / {maxScore}</span>
29
+ </div>
30
+
31
+ <div class="relative h-8 bg-gray-200 rounded-full overflow-hidden">
32
+ <div
33
+ class="{barColor} h-full rounded-full transition-all duration-1000 ease-out flex items-center justify-end pr-3"
34
+ style="width: {percentage}%"
35
+ >
36
+ {#if percentage > 15}
37
+ <span class="text-xs font-semibold text-white">
38
+ {percentage.toFixed(0)}%
39
+ </span>
40
+ {/if}
41
+ </div>
42
+ </div>
43
+ </div>
44
+
src/components/RadialChart.svelte ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ interface Props {
3
+ score: number;
4
+ label: string;
5
+ color?: string;
6
+ }
7
+
8
+ let { score, label, color = '#3b82f6' }: Props = $props();
9
+
10
+ // Calculate percentage and circle properties
11
+ const percentage = (score / 10) * 100;
12
+ const radius = 45;
13
+ const circumference = 2 * Math.PI * radius;
14
+ const dashOffset = circumference - (percentage / 100) * circumference;
15
+
16
+ // Color scheme based on score
17
+ const getColor = (s: number) => {
18
+ if (color !== '#3b82f6') return color;
19
+ if (s >= 8) return '#10b981'; // Green
20
+ if (s >= 6) return '#3b82f6'; // Blue
21
+ if (s >= 4) return '#f59e0b'; // Orange
22
+ return '#ef4444'; // Red
23
+ };
24
+
25
+ const chartColor = getColor(score);
26
+ </script>
27
+
28
+ <div class="flex flex-col items-center">
29
+ <svg class="transform -rotate-90" width="120" height="120">
30
+ <!-- Background circle -->
31
+ <circle
32
+ cx="60"
33
+ cy="60"
34
+ r={radius}
35
+ stroke="#e5e7eb"
36
+ stroke-width="8"
37
+ fill="none"
38
+ />
39
+ <!-- Progress circle -->
40
+ <circle
41
+ cx="60"
42
+ cy="60"
43
+ r={radius}
44
+ stroke={chartColor}
45
+ stroke-width="8"
46
+ fill="none"
47
+ stroke-dasharray={circumference}
48
+ stroke-dashoffset={dashOffset}
49
+ stroke-linecap="round"
50
+ class="transition-all duration-1000 ease-out"
51
+ />
52
+ <!-- Center text -->
53
+ <text
54
+ x="60"
55
+ y="60"
56
+ text-anchor="middle"
57
+ dy="7"
58
+ class="transform rotate-90 origin-center text-2xl font-bold"
59
+ fill={chartColor}
60
+ style="transform-origin: 60px 60px;"
61
+ >
62
+ {score.toFixed(1)}
63
+ </text>
64
+ </svg>
65
+
66
+ <div class="mt-3 text-center">
67
+ <p class="text-sm font-semibold text-gray-700">{label}</p>
68
+ <p class="text-xs text-gray-500">{percentage.toFixed(0)}%</p>
69
+ </div>
70
+ </div>
71
+
src/lib/Counter.svelte DELETED
@@ -1,10 +0,0 @@
1
- <script lang="ts">
2
- let count: number = $state(0)
3
- const increment = () => {
4
- count += 1
5
- }
6
- </script>
7
-
8
- <button onclick={increment}>
9
- count is {count}
10
- </button>
 
 
 
 
 
 
 
 
 
 
 
src/lib/api.ts ADDED
@@ -0,0 +1,144 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * API Service for Flask Backend Communication
3
+ * Base URL: http://localhost:5000 (proxied via Vite)
4
+ */
5
+
6
+ import axios from 'axios';
7
+
8
+ const API_BASE = '/api';
9
+
10
+ const api = axios.create({
11
+ baseURL: API_BASE,
12
+ headers: {
13
+ 'Content-Type': 'application/json',
14
+ },
15
+ });
16
+
17
+ // Types
18
+ export interface Hackathon {
19
+ id?: number;
20
+ name: string;
21
+ description: string;
22
+ evaluation_prompt: string;
23
+ criteria?: any[];
24
+ host_email?: string;
25
+ deadline?: string;
26
+ created_at?: string;
27
+ }
28
+
29
+ export interface Submission {
30
+ id?: number;
31
+ hackathon_id: number;
32
+ team_name: string;
33
+ project_name: string;
34
+ code_content?: string;
35
+ documentation_content?: string;
36
+ github_url?: string;
37
+ status?: string;
38
+ created_at?: string;
39
+ }
40
+
41
+ export interface Evaluation {
42
+ id?: number;
43
+ submission_id: number;
44
+ overall_score: number;
45
+ relevance_score: number;
46
+ technical_complexity_score: number;
47
+ creativity_score: number;
48
+ documentation_score: number;
49
+ feedback: string;
50
+ detailed_scores?: string;
51
+ created_at?: string;
52
+ }
53
+
54
+ // Hackathon API
55
+ export const hackathonApi = {
56
+ // Get all hackathons
57
+ getAll: async (): Promise<Hackathon[]> => {
58
+ const response = await api.get('/hackathons');
59
+ return response.data;
60
+ },
61
+
62
+ // Get single hackathon
63
+ getById: async (id: number): Promise<Hackathon> => {
64
+ const response = await api.get(`/hackathon/${id}`);
65
+ return response.data;
66
+ },
67
+
68
+ // Create new hackathon
69
+ create: async (hackathon: Hackathon): Promise<Hackathon> => {
70
+ const response = await api.post('/hackathon', hackathon);
71
+ return response.data;
72
+ },
73
+
74
+ // Update hackathon
75
+ update: async (id: number, hackathon: Partial<Hackathon>): Promise<Hackathon> => {
76
+ const response = await api.put(`/hackathon/${id}`, hackathon);
77
+ return response.data;
78
+ },
79
+
80
+ // Delete hackathon
81
+ delete: async (id: number): Promise<void> => {
82
+ await api.delete(`/hackathon/${id}`);
83
+ },
84
+ };
85
+
86
+ // Submission API
87
+ export const submissionApi = {
88
+ // Get all submissions for a hackathon
89
+ getByHackathon: async (hackathonId: number): Promise<Submission[]> => {
90
+ const response = await api.get(`/hackathon/${hackathonId}/submissions`);
91
+ return response.data;
92
+ },
93
+
94
+ // Get single submission
95
+ getById: async (hackathonId: number, submissionId: number): Promise<Submission> => {
96
+ const response = await api.get(`/hackathon/${hackathonId}/submission/${submissionId}`);
97
+ return response.data;
98
+ },
99
+
100
+ // Submit with files (multipart/form-data)
101
+ submitWithFiles: async (hackathonId: number, formData: FormData): Promise<Submission> => {
102
+ const response = await api.post(`/hackathon/${hackathonId}/submit`, formData, {
103
+ headers: {
104
+ 'Content-Type': 'multipart/form-data',
105
+ },
106
+ });
107
+ return response.data;
108
+ },
109
+
110
+ // Submit with GitHub URL
111
+ submitWithGithub: async (hackathonId: number, data: {
112
+ team_name: string;
113
+ project_name: string;
114
+ github_url: string;
115
+ }): Promise<Submission> => {
116
+ const response = await api.post(`/hackathon/${hackathonId}/submit/github`, data);
117
+ return response.data;
118
+ },
119
+ };
120
+
121
+ // Evaluation API
122
+ export const evaluationApi = {
123
+ // Get evaluation for a submission
124
+ getBySubmission: async (submissionId: number): Promise<Evaluation> => {
125
+ const response = await api.get(`/submission/${submissionId}/evaluation`);
126
+ return response.data;
127
+ },
128
+
129
+ // Get all evaluations for a hackathon (for leaderboard)
130
+ getByHackathon: async (hackathonId: number): Promise<any[]> => {
131
+ const response = await api.get(`/hackathon/${hackathonId}/results`);
132
+ return response.data;
133
+ },
134
+
135
+ // Trigger evaluation (if manual trigger is needed)
136
+ triggerEvaluation: async (submissionId: number): Promise<Evaluation> => {
137
+ const response = await api.post(`/submission/${submissionId}/evaluate`);
138
+ return response.data;
139
+ },
140
+ };
141
+
142
+ // Export axios instance for custom requests
143
+ export default api;
144
+
src/lib/router.ts ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Simple hash-based router for Svelte 5
2
+ import { writable } from 'svelte/store';
3
+
4
+ function getPath() {
5
+ return window.location.hash.slice(1) || '/';
6
+ }
7
+
8
+ function createRouter() {
9
+ const { subscribe, set } = writable(getPath());
10
+
11
+ window.addEventListener('hashchange', () => {
12
+ set(getPath());
13
+ });
14
+
15
+ return {
16
+ subscribe,
17
+ navigate: (path: string) => {
18
+ window.location.hash = path;
19
+ }
20
+ };
21
+ }
22
+
23
+ export const currentPath = createRouter();
24
+
src/pages/AllResults.svelte ADDED
@@ -0,0 +1,407 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { onMount } from 'svelte';
3
+ import { fly, fade, scale, slide } from 'svelte/transition';
4
+ import { elasticOut, quintOut, backOut } from 'svelte/easing';
5
+
6
+ let allResults = $state<any[]>([]);
7
+ let loading = $state(false);
8
+ let error = $state('');
9
+ let mounted = $state(false);
10
+ let selectedResult = $state<any>(null);
11
+ let showFeedbackModal = $state(false);
12
+
13
+ onMount(() => {
14
+ mounted = true;
15
+ loadAllResults();
16
+ });
17
+
18
+ async function loadAllResults() {
19
+ try {
20
+ loading = true;
21
+ error = '';
22
+
23
+ // Get all hackathons first
24
+ const hackathonsResponse = await fetch('/api/hackathons');
25
+ if (!hackathonsResponse.ok) {
26
+ throw new Error('Failed to load hackathons');
27
+ }
28
+ const hackathons = await hackathonsResponse.json();
29
+
30
+ // Get submissions for each hackathon
31
+ const allSubmissions = [];
32
+ for (const hackathon of hackathons) {
33
+ try {
34
+ const submissionsResponse = await fetch(`/api/hackathon/${hackathon.id}/submissions`);
35
+ if (submissionsResponse.ok) {
36
+ const submissions = await submissionsResponse.json();
37
+ // Add hackathon info to each submission
38
+ submissions.forEach(submission => {
39
+ submission.hackathon = hackathon;
40
+ });
41
+ allSubmissions.push(...submissions);
42
+ }
43
+ } catch (err) {
44
+ console.warn(`Failed to load submissions for hackathon ${hackathon.id}:`, err);
45
+ }
46
+ }
47
+
48
+ // Filter only evaluated submissions and sort by most recent first
49
+ allResults = allSubmissions
50
+ .filter(submission => submission.evaluated && submission.evaluation)
51
+ .sort((a, b) => new Date(b.evaluation.evaluated_at).getTime() - new Date(a.evaluation.evaluated_at).getTime());
52
+
53
+ } catch (err: any) {
54
+ error = err.message || 'Failed to load results';
55
+ console.error('Error loading results:', err);
56
+ } finally {
57
+ loading = false;
58
+ }
59
+ }
60
+
61
+ function viewResult(submissionId: number) {
62
+ window.location.hash = `/result/${submissionId}`;
63
+ }
64
+
65
+ function showFeedback(result: any, event: Event) {
66
+ event.stopPropagation(); // Prevent the row click
67
+ selectedResult = result;
68
+ showFeedbackModal = true;
69
+ }
70
+
71
+ function closeFeedbackModal() {
72
+ showFeedbackModal = false;
73
+ selectedResult = null;
74
+ }
75
+
76
+ function goToUpload() {
77
+ window.location.hash = '/';
78
+ }
79
+
80
+ // Format a UTC/DB timestamp into India Standard Time
81
+ function formatIST(dateStr: string): string {
82
+ try {
83
+ // Treat missing timezone as UTC
84
+ const normalized = /([zZ]|[+\-]\d{2}:\d{2})$/.test(dateStr) ? dateStr : `${dateStr}Z`;
85
+ const s = new Date(normalized).toLocaleString('en-IN', {
86
+ timeZone: 'Asia/Kolkata',
87
+ year: 'numeric', month: 'short', day: '2-digit',
88
+ hour: '2-digit', minute: '2-digit', hour12: true,
89
+ timeZoneName: 'short'
90
+ });
91
+ return s.replace(' am', ' AM').replace(' pm', ' PM');
92
+ } catch {
93
+ return dateStr;
94
+ }
95
+ }
96
+
97
+ function getMedalEmoji(rank: number): string {
98
+ if (rank === 0) return '🥇';
99
+ if (rank === 1) return '🥈';
100
+ if (rank === 2) return '🥉';
101
+ return '🏅';
102
+ }
103
+
104
+ function getScoreColor(score: number): string {
105
+ if (score >= 8) return 'text-green-600';
106
+ if (score >= 6) return 'text-blue-600';
107
+ if (score >= 4) return 'text-yellow-600';
108
+ return 'text-red-600';
109
+ }
110
+
111
+ function getScoreBgColor(score: number): string {
112
+ if (score >= 8) return 'bg-green-100';
113
+ if (score >= 6) return 'bg-blue-100';
114
+ if (score >= 4) return 'bg-yellow-100';
115
+ return 'bg-red-100';
116
+ }
117
+ </script>
118
+
119
+ <div class="min-h-screen bg-gradient-to-br from-indigo-50 via-purple-50 to-pink-50 overflow-hidden">
120
+ {#if mounted}
121
+ <header
122
+ class="bg-white shadow-md backdrop-blur-sm bg-opacity-95"
123
+ in:fly={{ y: -50, duration: 600, easing: quintOut }}
124
+ >
125
+ <div class="max-w-6xl mx-auto px-6 py-6">
126
+ <div class="flex justify-between items-center">
127
+ <div>
128
+ <h1
129
+ class="text-4xl font-bold bg-gradient-to-r from-indigo-600 via-purple-600 to-pink-600 bg-clip-text text-transparent animate-gradient"
130
+ style="background-size: 200% auto;"
131
+ >
132
+ 🏆 All Evaluation Results
133
+ </h1>
134
+ <p
135
+ class="mt-2 text-gray-600"
136
+ in:fade={{ delay: 200, duration: 400 }}
137
+ >
138
+ Leaderboard of all evaluated projects
139
+ </p>
140
+ </div>
141
+ <button
142
+ onclick={goToUpload}
143
+ class="px-5 py-2 bg-gradient-to-r from-indigo-600 to-purple-600 text-white rounded-lg hover:from-indigo-700 hover:to-purple-700 transition-all duration-300 shadow-lg hover:shadow-xl hover:scale-105 transform"
144
+ in:scale={{ delay: 300, duration: 400, easing: elasticOut }}
145
+ >
146
+ ➕ Upload New Project
147
+ </button>
148
+ </div>
149
+ </div>
150
+ </header>
151
+ {/if}
152
+
153
+ <main class="max-w-6xl mx-auto px-6 py-12">
154
+ {#if error}
155
+ <div
156
+ class="mb-6 p-4 bg-red-50 border-l-4 border-red-500 text-red-700 rounded-lg flex items-start gap-3"
157
+ in:slide={{ duration: 300 }}
158
+ >
159
+ <span class="text-2xl">❌</span>
160
+ <div>
161
+ <p class="font-semibold">Error</p>
162
+ <p>{error}</p>
163
+ </div>
164
+ </div>
165
+ {/if}
166
+
167
+ {#if loading}
168
+ <div
169
+ class="text-center py-12"
170
+ in:fade={{ duration: 400 }}
171
+ >
172
+ <div class="inline-block animate-spin rounded-full h-12 w-12 border-b-2 border-indigo-600"></div>
173
+ <p class="mt-4 text-gray-600">Loading all evaluation results...</p>
174
+ </div>
175
+ {:else if allResults.length === 0}
176
+ <div
177
+ class="text-center py-12"
178
+ in:fade={{ duration: 400 }}
179
+ >
180
+ <div class="text-6xl mb-4">📊</div>
181
+ <h2 class="text-2xl font-bold text-gray-900 mb-2">No Results Yet</h2>
182
+ <p class="text-gray-600 mb-6">No projects have been evaluated yet. Upload your first project to get started!</p>
183
+ <button
184
+ onclick={goToUpload}
185
+ class="px-6 py-3 bg-gradient-to-r from-indigo-600 to-purple-600 text-white rounded-lg hover:from-indigo-700 hover:to-purple-700 transition-all duration-300 shadow-lg hover:shadow-xl hover:scale-105 transform"
186
+ >
187
+ 🚀 Upload First Project
188
+ </button>
189
+ </div>
190
+ {:else}
191
+ <div
192
+ class="bg-white rounded-2xl shadow-xl p-8 hover:shadow-2xl transition-shadow duration-300"
193
+ in:fly={{ y: 50, duration: 600, delay: 200, easing: quintOut }}
194
+ >
195
+ <div class="mb-6">
196
+ <h2 class="text-2xl font-bold text-gray-900 mb-2">📋 Evaluation Leaderboard</h2>
197
+ <p class="text-gray-600">Total Projects Evaluated: {allResults.length}</p>
198
+ </div>
199
+
200
+ <div class="space-y-4">
201
+ {#each allResults as result, index}
202
+ <div
203
+ class="border border-gray-200 rounded-lg p-6 hover:border-indigo-300 hover:shadow-lg transition-all duration-300"
204
+ in:fly={{ x: -20, duration: 400, delay: index * 100 }}
205
+ >
206
+ <div class="flex items-center justify-between">
207
+ <div class="flex items-center gap-4">
208
+ <!-- Rank -->
209
+ <div class="text-3xl">
210
+ {getMedalEmoji(index)}
211
+ </div>
212
+
213
+ <!-- Project Info -->
214
+ <div
215
+ class="cursor-pointer hover:text-indigo-600 transition-colors duration-200"
216
+ onclick={() => viewResult(result.id)}
217
+ title="Click to view detailed analysis"
218
+ >
219
+ <h3 class="text-xl font-semibold text-gray-900 hover:text-indigo-600">{result.project_name}</h3>
220
+ <p class="text-xs text-gray-500 mt-1">
221
+ Evaluated: {formatIST(result.evaluation.evaluated_at)}
222
+ </p>
223
+ </div>
224
+ </div>
225
+
226
+ <!-- Scores -->
227
+ <div class="flex items-center gap-6">
228
+ <!-- Overall Score -->
229
+ <div class="text-center">
230
+ <div class="text-2xl font-bold {getScoreColor(result.evaluation.overall_score)} px-4 py-2 rounded-lg {getScoreBgColor(result.evaluation.overall_score)}">
231
+ {result.evaluation.overall_score.toFixed(1)}
232
+ </div>
233
+ <div class="text-xs text-gray-500 mt-1">Overall</div>
234
+ </div>
235
+
236
+ <!-- Feedback Button -->
237
+ <button
238
+ onclick={(e) => showFeedback(result, e)}
239
+ class="text-indigo-600 hover:text-indigo-800 hover:bg-indigo-50 p-2 rounded-lg transition-all duration-200"
240
+ title="View AI Feedback"
241
+ >
242
+ <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
243
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
244
+ </svg>
245
+ </button>
246
+ </div>
247
+ </div>
248
+
249
+ <!-- Project Description -->
250
+ {#if result.project_description}
251
+ <div class="mt-3 text-sm text-gray-600 line-clamp-2">
252
+ {result.project_description}
253
+ </div>
254
+ {/if}
255
+ </div>
256
+ {/each}
257
+ </div>
258
+
259
+ <!-- Action Buttons -->
260
+ <div class="mt-8 text-center">
261
+ <button
262
+ onclick={goToUpload}
263
+ class="px-6 py-3 bg-gradient-to-r from-indigo-600 to-purple-600 text-white rounded-lg hover:from-indigo-700 hover:to-purple-700 transition-all duration-300 shadow-lg hover:shadow-xl hover:scale-105 transform"
264
+ >
265
+ 🚀 Upload Another Project
266
+ </button>
267
+ </div>
268
+ </div>
269
+ {/if}
270
+ </main>
271
+ </div>
272
+
273
+ <!-- Feedback Modal -->
274
+ {#if showFeedbackModal && selectedResult}
275
+ <div
276
+ class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4"
277
+ onclick={closeFeedbackModal}
278
+ in:fade={{ duration: 200 }}
279
+ out:fade={{ duration: 200 }}
280
+ >
281
+ <div
282
+ class="bg-white rounded-2xl shadow-2xl max-w-4xl w-full max-h-[90vh] overflow-y-auto"
283
+ onclick={(e) => e.stopPropagation()}
284
+ in:scale={{ duration: 300, easing: elasticOut }}
285
+ out:scale={{ duration: 200 }}
286
+ >
287
+ <!-- Modal Header -->
288
+ <div class="sticky top-0 bg-white border-b border-gray-200 px-6 py-4 rounded-t-2xl">
289
+ <div class="flex justify-between items-center">
290
+ <div>
291
+ <h2 class="text-2xl font-bold text-gray-900">{selectedResult.project_name}</h2>
292
+ <p class="text-sm text-gray-600">AI Evaluation Feedback</p>
293
+ </div>
294
+ <button
295
+ onclick={closeFeedbackModal}
296
+ class="text-gray-400 hover:text-gray-600 hover:bg-gray-100 p-2 rounded-lg transition-all duration-200"
297
+ >
298
+ <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
299
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
300
+ </svg>
301
+ </button>
302
+ </div>
303
+ </div>
304
+
305
+ <!-- Modal Content -->
306
+ <div class="p-6">
307
+ <!-- Project Info -->
308
+ <div class="mb-6 p-4 bg-gradient-to-r from-indigo-50 to-purple-50 rounded-lg">
309
+ <div class="flex justify-between items-center mb-2">
310
+ <span class="text-sm font-medium text-gray-700">Team: {selectedResult.team_name}</span>
311
+ <span class="text-sm text-gray-500">Evaluated: {formatIST(selectedResult.evaluation.evaluated_at)}</span>
312
+ </div>
313
+ {#if selectedResult.project_description}
314
+ <p class="text-sm text-gray-600">{selectedResult.project_description}</p>
315
+ {/if}
316
+ </div>
317
+
318
+ <!-- Overall Score -->
319
+ <div class="text-center mb-6 p-4 bg-gray-50 rounded-lg">
320
+ <h3 class="text-lg font-semibold text-gray-900 mb-2">Overall Score</h3>
321
+ <div class="text-4xl font-bold {getScoreColor(selectedResult.evaluation.overall_score)}">
322
+ {selectedResult.evaluation.overall_score.toFixed(1)}/10
323
+ </div>
324
+ </div>
325
+
326
+ <!-- Score Breakdown -->
327
+ <div class="grid grid-cols-5 gap-4 mb-6">
328
+ <div class="text-center p-3 bg-gray-50 rounded-lg">
329
+ <div class="text-lg font-bold {getScoreColor(selectedResult.evaluation.relevance_score)}">
330
+ {selectedResult.evaluation.relevance_score.toFixed(1)}
331
+ </div>
332
+ <div class="text-xs text-gray-600">Relevance</div>
333
+ </div>
334
+ <div class="text-center p-3 bg-gray-50 rounded-lg">
335
+ <div class="text-lg font-bold {getScoreColor(selectedResult.evaluation.technical_complexity_score)}">
336
+ {selectedResult.evaluation.technical_complexity_score.toFixed(1)}
337
+ </div>
338
+ <div class="text-xs text-gray-600">Technical</div>
339
+ </div>
340
+ <div class="text-center p-3 bg-gray-50 rounded-lg">
341
+ <div class="text-lg font-bold {getScoreColor(selectedResult.evaluation.creativity_score)}">
342
+ {selectedResult.evaluation.creativity_score.toFixed(1)}
343
+ </div>
344
+ <div class="text-xs text-gray-600">Creativity</div>
345
+ </div>
346
+ <div class="text-center p-3 bg-gray-50 rounded-lg">
347
+ <div class="text-lg font-bold {getScoreColor(selectedResult.evaluation.documentation_score)}">
348
+ {selectedResult.evaluation.documentation_score.toFixed(1)}
349
+ </div>
350
+ <div class="text-xs text-gray-600">Documentation</div>
351
+ </div>
352
+ <div class="text-center p-3 bg-gray-50 rounded-lg">
353
+ <div class="text-lg font-bold {getScoreColor(selectedResult.evaluation.productivity_score)}">
354
+ {selectedResult.evaluation.productivity_score.toFixed(1)}
355
+ </div>
356
+ <div class="text-xs text-gray-600">Productivity</div>
357
+ </div>
358
+ </div>
359
+
360
+ <!-- AI Feedback -->
361
+ <div class="bg-gradient-to-r from-blue-50 to-indigo-50 rounded-lg p-6">
362
+ <h3 class="text-lg font-semibold text-gray-900 mb-4 flex items-center gap-2">
363
+ 🤖 AI Generated Feedback
364
+ </h3>
365
+ <div class="prose prose-sm max-w-none">
366
+ <p class="text-gray-700 leading-relaxed whitespace-pre-wrap">{selectedResult.evaluation.feedback}</p>
367
+ </div>
368
+ </div>
369
+
370
+ <!-- Action Buttons -->
371
+ <div class="mt-6 flex justify-center gap-4">
372
+ <button
373
+ onclick={() => viewResult(selectedResult.id)}
374
+ class="px-6 py-2 bg-gradient-to-r from-indigo-600 to-purple-600 text-white rounded-lg hover:from-indigo-700 hover:to-purple-700 transition-all duration-300 shadow-lg hover:shadow-xl hover:scale-105 transform"
375
+ >
376
+ 📊 View Detailed Analysis
377
+ </button>
378
+ <button
379
+ onclick={closeFeedbackModal}
380
+ class="px-6 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700 transition-all duration-300 shadow-lg hover:shadow-xl hover:scale-105 transform"
381
+ >
382
+ Close
383
+ </button>
384
+ </div>
385
+ </div>
386
+ </div>
387
+ </div>
388
+ {/if}
389
+
390
+ <style>
391
+ @keyframes gradient {
392
+ 0% { background-position: 0% 50%; }
393
+ 50% { background-position: 100% 50%; }
394
+ 100% { background-position: 0% 50%; }
395
+ }
396
+
397
+ .animate-gradient {
398
+ animation: gradient 3s ease infinite;
399
+ }
400
+
401
+ .line-clamp-2 {
402
+ display: -webkit-box;
403
+ -webkit-line-clamp: 2;
404
+ -webkit-box-orient: vertical;
405
+ overflow: hidden;
406
+ }
407
+ </style>
src/pages/IndividualResult.svelte ADDED
@@ -0,0 +1,319 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { onMount } from 'svelte';
3
+ import BarChart from '../components/BarChart.svelte';
4
+ import { fly, fade, scale, slide } from 'svelte/transition';
5
+ import { elasticOut, quintOut, backOut } from 'svelte/easing';
6
+
7
+ interface Props {
8
+ submissionId: string;
9
+ }
10
+
11
+ let { submissionId }: Props = $props();
12
+
13
+ let result = $state<any>(null);
14
+ let loading = $state(true);
15
+ let error = $state('');
16
+ let mounted = $state(false);
17
+
18
+ onMount(() => {
19
+ mounted = true;
20
+ loadResult();
21
+ });
22
+
23
+ // Format a UTC/DB timestamp into India Standard Time
24
+ function formatIST(dateStr: string): string {
25
+ try {
26
+ const normalized = /([zZ]|[+\-]\d{2}:\d{2})$/.test(dateStr) ? dateStr : `${dateStr}Z`;
27
+ const s = new Date(normalized).toLocaleString('en-IN', {
28
+ timeZone: 'Asia/Kolkata',
29
+ year: 'numeric', month: 'short', day: '2-digit',
30
+ hour: '2-digit', minute: '2-digit', hour12: true,
31
+ timeZoneName: 'short'
32
+ });
33
+ return s.replace(' am', ' AM').replace(' pm', ' PM');
34
+ } catch {
35
+ return dateStr;
36
+ }
37
+ }
38
+
39
+ // Keys we expect from detailed_scores for hackathon criteria
40
+ const hackathonKeys = [
41
+ { label: 'Productivity', key: 'productivity_justification' },
42
+ { label: 'Out of the box thinking', key: 'out_of_box_thinking' },
43
+ { label: 'Problem-solving skills', key: 'problem_solving_skills' },
44
+ { label: 'Creativity', key: 'creativity_justification' },
45
+ { label: 'Research capabilities', key: 'research_capabilities' },
46
+ { label: 'Understanding the business', key: 'business_understanding' },
47
+ { label: 'Use of non-famous tools', key: 'non_famous_tools_usage' }
48
+ ];
49
+
50
+ async function loadResult() {
51
+ try {
52
+ loading = true;
53
+ error = '';
54
+
55
+ console.log('Loading result for submission ID:', submissionId);
56
+ const response = await fetch(`/api/results/${submissionId}`);
57
+ console.log('Response status:', response.status);
58
+
59
+ if (!response.ok) {
60
+ const errorText = await response.text();
61
+ console.error('Response error:', errorText);
62
+ throw new Error(`Failed to load result: ${response.status} ${errorText}`);
63
+ }
64
+
65
+ result = await response.json();
66
+ console.log('Loaded result:', result);
67
+ } catch (err: any) {
68
+ error = err.message || 'Failed to load result';
69
+ console.error('Error loading result:', err);
70
+ } finally {
71
+ loading = false;
72
+ }
73
+ }
74
+
75
+ function goBack() {
76
+ window.location.hash = '/';
77
+ }
78
+
79
+ function viewAllResults() {
80
+ window.location.hash = '/results';
81
+ }
82
+ </script>
83
+
84
+ <div class="min-h-screen bg-gradient-to-br from-indigo-50 via-purple-50 to-pink-50 overflow-hidden">
85
+ {#if mounted}
86
+ <header
87
+ class="bg-white shadow-md backdrop-blur-sm bg-opacity-95"
88
+ in:fly={{ y: -50, duration: 600, easing: quintOut }}
89
+ >
90
+ <div class="max-w-5xl mx-auto px-6 py-6">
91
+ <div class="flex justify-between items-center">
92
+ <div>
93
+ <h1
94
+ class="text-4xl font-bold bg-gradient-to-r from-indigo-600 via-purple-600 to-pink-600 bg-clip-text text-transparent animate-gradient"
95
+ style="background-size: 200% auto;"
96
+ >
97
+ 📊 Project Evaluation Results
98
+ </h1>
99
+ <p
100
+ class="mt-2 text-gray-600"
101
+ in:fade={{ delay: 200, duration: 400 }}
102
+ >
103
+ Detailed AI-powered analysis and scoring
104
+ </p>
105
+ </div>
106
+ <div class="flex gap-3">
107
+ <button
108
+ onclick={goBack}
109
+ class="px-5 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700 transition-all duration-300 shadow-lg hover:shadow-xl hover:scale-105 transform"
110
+ in:scale={{ delay: 300, duration: 400, easing: elasticOut }}
111
+ >
112
+ ← Back to Upload
113
+ </button>
114
+ <button
115
+ onclick={viewAllResults}
116
+ class="px-5 py-2 bg-gradient-to-r from-indigo-600 to-purple-600 text-white rounded-lg hover:from-indigo-700 hover:to-purple-700 transition-all duration-300 shadow-lg hover:shadow-xl hover:scale-105 transform"
117
+ in:scale={{ delay: 400, duration: 400, easing: elasticOut }}
118
+ >
119
+ 📋 All Results
120
+ </button>
121
+ </div>
122
+ </div>
123
+ </div>
124
+ </header>
125
+ {/if}
126
+
127
+ <main class="max-w-5xl mx-auto px-6 py-12">
128
+ {#if error}
129
+ <div
130
+ class="mb-6 p-4 bg-red-50 border-l-4 border-red-500 text-red-700 rounded-lg flex items-start gap-3"
131
+ in:slide={{ duration: 300 }}
132
+ >
133
+ <span class="text-2xl">❌</span>
134
+ <div>
135
+ <p class="font-semibold">Error Loading Result</p>
136
+ <p>{error}</p>
137
+ <p class="text-sm mt-2">Submission ID: {submissionId}</p>
138
+ <div class="mt-4 space-x-4">
139
+ <button
140
+ onclick={goBack}
141
+ class="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors duration-200"
142
+ >
143
+ ← Back to Upload
144
+ </button>
145
+ <button
146
+ onclick={viewAllResults}
147
+ class="px-4 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700 transition-colors duration-200"
148
+ >
149
+ View All Results
150
+ </button>
151
+ </div>
152
+ </div>
153
+ </div>
154
+ {/if}
155
+
156
+ {#if loading}
157
+ <div
158
+ class="text-center py-12"
159
+ in:fade={{ duration: 400 }}
160
+ >
161
+ <div class="inline-block animate-spin rounded-full h-12 w-12 border-b-2 border-indigo-600"></div>
162
+ <p class="mt-4 text-gray-600">Loading evaluation results...</p>
163
+ </div>
164
+ {:else if result}
165
+ <div
166
+ class="bg-white rounded-2xl shadow-xl p-8 hover:shadow-2xl transition-shadow duration-300"
167
+ in:fly={{ y: 50, duration: 600, delay: 200, easing: quintOut }}
168
+ >
169
+ <!-- Project Info -->
170
+ <div class="mb-8 text-center">
171
+ <h2 class="text-3xl font-bold text-gray-900 mb-2">{result.project_name}</h2>
172
+ <p class="text-gray-600">{result.project_description}</p>
173
+ <div class="mt-4 flex justify-center items-center gap-4">
174
+ <span class="text-sm text-gray-500">Team: {result.team_name}</span>
175
+ <span class="text-sm text-gray-500">•</span>
176
+ <span class="text-sm text-gray-500">Evaluated: {formatIST(result.evaluation.evaluated_at)}</span>
177
+ </div>
178
+ </div>
179
+
180
+ <!-- Overall Score -->
181
+ <div
182
+ class="text-center mb-8 p-6 bg-gradient-to-r from-indigo-50 to-purple-50 rounded-xl"
183
+ in:scale={{ delay: 400, duration: 500, easing: backOut }}
184
+ >
185
+ <h3 class="text-xl font-semibold text-gray-900 mb-2">Overall Score</h3>
186
+ <div class="text-5xl font-bold bg-gradient-to-r from-indigo-600 to-purple-600 bg-clip-text text-transparent">
187
+ {result.evaluation.overall_score.toFixed(1)}/10
188
+ </div>
189
+ </div>
190
+
191
+ <!-- Score Breakdown (Bars only for clarity and accessibility) -->
192
+ <div class="mb-2"></div>
193
+
194
+ <!-- Bar Charts -->
195
+ <div
196
+ class="bg-gray-50 rounded-lg p-6 mb-8"
197
+ in:fly={{ y: 20, delay: 1100, duration: 400 }}
198
+ >
199
+ <h3 class="text-lg font-semibold text-gray-900 mb-4">📈 Detailed Scores</h3>
200
+ <div class="space-y-4">
201
+ <div in:fly={{ x: -20, delay: 1200, duration: 300 }}>
202
+ <BarChart score={result.evaluation.relevance_score} label="Relevance" />
203
+ </div>
204
+ <div in:fly={{ x: -20, delay: 1300, duration: 300 }}>
205
+ <BarChart score={result.evaluation.technical_complexity_score} label="Technical Complexity" />
206
+ </div>
207
+ <div in:fly={{ x: -20, delay: 1400, duration: 300 }}>
208
+ <BarChart score={result.evaluation.creativity_score} label="Creativity" />
209
+ </div>
210
+ <div in:fly={{ x: -20, delay: 1500, duration: 300 }}>
211
+ <BarChart score={result.evaluation.documentation_score} label="Documentation" />
212
+ </div>
213
+ <div in:fly={{ x: -20, delay: 1600, duration: 300 }}>
214
+ <BarChart score={result.evaluation.productivity_score} label="Productivity" />
215
+ </div>
216
+ </div>
217
+ </div>
218
+
219
+ <!-- Hackathon Key Points (before feedback) -->
220
+ {#if result.evaluation.detailed_scores}
221
+ <div
222
+ class="bg-white rounded-xl p-6 shadow-lg border-2 border-indigo-50 mb-8"
223
+ in:fly={{ y: 16, delay: 1550, duration: 350 }}
224
+ >
225
+ <h3 class="text-2xl font-bold text-gray-900 mb-4 flex items-center gap-2">
226
+ 📚 Hackathon Key Points
227
+ </h3>
228
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
229
+ {#each hackathonKeys as item, i}
230
+ {#if result.evaluation.detailed_scores[item.key]}
231
+ <div class="bg-gradient-to-r from-slate-50 to-white rounded-lg p-4 shadow-sm border-l-4 border-indigo-400" in:fly={{ x: -12, delay: 1560 + i*90, duration: 250 }}>
232
+ <div class="text-sm text-gray-500 mb-1">{item.label}</div>
233
+ <div class="text-gray-700">{result.evaluation.detailed_scores[item.key]}</div>
234
+ </div>
235
+ {/if}
236
+ {/each}
237
+ </div>
238
+ </div>
239
+ {/if}
240
+
241
+ <!-- AI Feedback -->
242
+ <div
243
+ class="bg-gradient-to-r from-blue-50 to-indigo-50 rounded-xl p-6 shadow-lg mb-8"
244
+ in:scale={{ delay: 1700, duration: 400, easing: elasticOut }}
245
+ >
246
+ <h3 class="text-2xl font-bold text-gray-900 mb-4 flex items-center gap-2">
247
+ 🤖 AI Evaluation Feedback
248
+ </h3>
249
+ <div class="bg-white rounded-lg p-5 shadow-inner border-l-4 border-indigo-500">
250
+ <p class="text-gray-700 leading-relaxed whitespace-pre-wrap">{result.evaluation.feedback}</p>
251
+ </div>
252
+ </div>
253
+
254
+ <!-- Detailed Justifications -->
255
+ {#if result.evaluation.detailed_scores && typeof result.evaluation.detailed_scores === 'object' && Object.keys(result.evaluation.detailed_scores).length > 0}
256
+ <div
257
+ class="bg-gradient-to-r from-purple-50 to-pink-50 rounded-xl p-6 shadow-lg mb-8"
258
+ in:scale={{ delay: 1800, duration: 400, easing: elasticOut }}
259
+ >
260
+ <h3 class="text-2xl font-bold text-gray-900 mb-4 flex items-center gap-2">
261
+ 📋 Detailed Score Justifications
262
+ </h3>
263
+ <div class="space-y-4">
264
+ {#each Object.entries(result.evaluation.detailed_scores) as [key, value], i}
265
+ <div
266
+ class="bg-white rounded-lg p-4 shadow-sm border-l-4 border-purple-400"
267
+ in:fly={{ x: -20, delay: 1900 + (i * 100), duration: 300 }}
268
+ >
269
+ <h4 class="font-semibold text-gray-800 capitalize mb-2 text-lg">
270
+ {key.replace('_justification', '').replace('_', ' ')}
271
+ </h4>
272
+ <p class="text-gray-600">{value}</p>
273
+ </div>
274
+ {/each}
275
+ </div>
276
+ </div>
277
+ {/if}
278
+
279
+ <!-- Enhanced Action Buttons -->
280
+ <div
281
+ class="mt-12 flex flex-col sm:flex-row gap-6 justify-center items-center"
282
+ in:scale={{ delay: 2000, duration: 400, easing: elasticOut }}
283
+ >
284
+ <button
285
+ onclick={goBack}
286
+ class="w-full sm:w-auto px-8 py-4 bg-gradient-to-r from-indigo-600 to-purple-600 text-white text-lg font-bold rounded-xl hover:from-indigo-700 hover:to-purple-700 transition-all duration-300 shadow-lg hover:shadow-xl hover:scale-105 transform flex items-center justify-center gap-3"
287
+ >
288
+ <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
289
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path>
290
+ </svg>
291
+ 🚀 Evaluate Another Project
292
+ </button>
293
+
294
+ <button
295
+ onclick={viewAllResults}
296
+ class="w-full sm:w-auto px-8 py-4 bg-gradient-to-r from-pink-600 to-red-600 text-white text-lg font-bold rounded-xl hover:from-pink-700 hover:to-red-700 transition-all duration-300 shadow-lg hover:shadow-xl hover:scale-105 transform flex items-center justify-center gap-3"
297
+ >
298
+ <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
299
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"></path>
300
+ </svg>
301
+ 🏆 View Leaderboard
302
+ </button>
303
+ </div>
304
+ </div>
305
+ {/if}
306
+ </main>
307
+ </div>
308
+
309
+ <style>
310
+ @keyframes gradient {
311
+ 0% { background-position: 0% 50%; }
312
+ 50% { background-position: 100% 50%; }
313
+ 100% { background-position: 0% 50%; }
314
+ }
315
+
316
+ .animate-gradient {
317
+ animation: gradient 3s ease infinite;
318
+ }
319
+ </style>
src/pages/ProjectEvaluator.svelte ADDED
@@ -0,0 +1,402 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { hackathonApi, submissionApi } from '../lib/api';
3
+ import { fly, fade, scale, slide } from 'svelte/transition';
4
+ import { elasticOut, quintOut } from 'svelte/easing';
5
+
6
+ let projectName = $state('');
7
+ let evaluationQuery = $state('Evaluate this project for technical quality, innovation, and best practices');
8
+ let projectFiles = $state<FileList | null>(null);
9
+ let uploading = $state(false);
10
+ let error = $state('');
11
+ let success = $state('');
12
+ let evaluationId = $state<number | null>(null);
13
+
14
+ let dragActive = $state(false);
15
+ let mounted = $state(false);
16
+
17
+ $effect(() => {
18
+ mounted = true;
19
+ });
20
+
21
+ function handleDrop(e: DragEvent) {
22
+ e.preventDefault();
23
+ dragActive = false;
24
+
25
+ if (e.dataTransfer?.files) {
26
+ projectFiles = e.dataTransfer.files;
27
+ }
28
+ }
29
+
30
+ function handleDragOver(e: DragEvent) {
31
+ e.preventDefault();
32
+ dragActive = true;
33
+ }
34
+
35
+ function handleDragLeave() {
36
+ dragActive = false;
37
+ }
38
+
39
+ function handleFileChange(e: Event) {
40
+ const target = e.target as HTMLInputElement;
41
+ if (target.files) {
42
+ projectFiles = target.files;
43
+ }
44
+ }
45
+
46
+ async function evaluateProject() {
47
+ if (!projectName.trim()) {
48
+ error = 'Please enter a project name';
49
+ return;
50
+ }
51
+
52
+ if (!projectFiles || projectFiles.length === 0) {
53
+ error = 'Please upload at least one file';
54
+ return;
55
+ }
56
+
57
+ try {
58
+ uploading = true;
59
+ error = '';
60
+ success = '';
61
+
62
+ // Create a temporary hackathon for this evaluation
63
+ const hackathon = await hackathonApi.create({
64
+ name: `Project: ${projectName}`,
65
+ description: evaluationQuery,
66
+ evaluation_prompt: evaluationQuery,
67
+ host_email: 'host@autoeval.ai',
68
+ deadline: ''
69
+ });
70
+
71
+ // Upload files as a submission
72
+ const formData = new FormData();
73
+ formData.append('hackathon_id', hackathon.id.toString());
74
+ formData.append('team_name', 'Evaluation Team');
75
+ formData.append('participant_email', 'participant@autoeval.ai');
76
+ formData.append('project_name', projectName);
77
+ formData.append('project_description', evaluationQuery);
78
+
79
+ // Add all files
80
+ for (let i = 0; i < projectFiles.length; i++) {
81
+ formData.append('project_files', projectFiles[i]);
82
+ }
83
+
84
+ const response = await fetch('/api/submissions', {
85
+ method: 'POST',
86
+ body: formData
87
+ });
88
+
89
+ if (!response.ok) {
90
+ throw new Error('Failed to upload project');
91
+ }
92
+
93
+ const submission = await response.json();
94
+ evaluationId = submission.id;
95
+
96
+ // Show brief success message then redirect
97
+ success = `✅ Project evaluated successfully! Overall Score: ${submission.overall_score}/10`;
98
+ console.log(`✅ Project evaluated successfully! Redirecting to result page...`);
99
+
100
+ // Redirect after a brief moment to show success
101
+ setTimeout(() => {
102
+ window.location.hash = `/result/${submission.id}`;
103
+ }, 1500);
104
+
105
+ } catch (err: any) {
106
+ error = err.message || 'Failed to evaluate project';
107
+ console.error('Error evaluating project:', err);
108
+ } finally {
109
+ uploading = false;
110
+ }
111
+ }
112
+
113
+ function clearFiles() {
114
+ projectFiles = null;
115
+ const fileInput = document.getElementById('file-input') as HTMLInputElement;
116
+ if (fileInput) fileInput.value = '';
117
+ }
118
+
119
+ function getFileIcon(filename: string): string {
120
+ const ext = filename.split('.').pop()?.toLowerCase();
121
+ const icons: Record<string, string> = {
122
+ 'py': '🐍',
123
+ 'js': '📜',
124
+ 'ts': '📘',
125
+ 'html': '🌐',
126
+ 'css': '🎨',
127
+ 'json': '📋',
128
+ 'md': '📝',
129
+ 'txt': '📄',
130
+ 'zip': '📦',
131
+ 'pdf': '📕',
132
+ 'png': '🖼️',
133
+ 'jpg': '🖼️',
134
+ 'jpeg': '🖼️',
135
+ 'gif': '🖼️',
136
+ 'svg': '🎭'
137
+ };
138
+ return icons[ext || ''] || '📄';
139
+ }
140
+
141
+ function formatFileSize(bytes: number): string {
142
+ if (bytes < 1024) return bytes + ' B';
143
+ if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
144
+ return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
145
+ }
146
+ </script>
147
+
148
+ <div class="min-h-screen bg-gradient-to-br from-indigo-50 via-purple-50 to-pink-50 overflow-hidden">
149
+ <!-- Header -->
150
+ {#if mounted}
151
+ <header
152
+ class="bg-white shadow-md backdrop-blur-sm bg-opacity-95"
153
+ in:fly={{ y: -50, duration: 600, easing: quintOut }}
154
+ >
155
+ <div class="max-w-5xl mx-auto px-6 py-6">
156
+ <div class="flex justify-between items-center">
157
+ <div>
158
+ <h1
159
+ class="text-4xl font-bold bg-gradient-to-r from-indigo-600 via-purple-600 to-pink-600 bg-clip-text text-transparent animate-gradient"
160
+ style="background-size: 200% auto;"
161
+ >
162
+ 🤖 EvalAI
163
+ </h1>
164
+ <p
165
+ class="mt-2 text-gray-600"
166
+ in:fade={{ delay: 200, duration: 400 }}
167
+ >
168
+ Upload your project and get instant AI-powered evaluation
169
+ </p>
170
+ </div>
171
+ <a
172
+ href="#/results"
173
+ class="px-5 py-2 bg-gradient-to-r from-indigo-600 to-purple-600 text-white rounded-lg hover:from-indigo-700 hover:to-purple-700 transition-all duration-300 shadow-lg hover:shadow-xl hover:scale-105 transform"
174
+ in:scale={{ delay: 300, duration: 400, easing: elasticOut }}
175
+ >
176
+ 📊 View Results
177
+ </a>
178
+ </div>
179
+ </div>
180
+ </header>
181
+ {/if}
182
+
183
+ <main class="max-w-5xl mx-auto px-6 py-12">
184
+ {#if error}
185
+ <div
186
+ class="mb-6 p-4 bg-red-50 border-l-4 border-red-500 text-red-700 rounded-lg flex items-start gap-3"
187
+ in:slide={{ duration: 300 }}
188
+ out:fade={{ duration: 200 }}
189
+ >
190
+ <span class="text-2xl">❌</span>
191
+ <div>
192
+ <p class="font-semibold">Error</p>
193
+ <p>{error}</p>
194
+ </div>
195
+ </div>
196
+ {/if}
197
+
198
+ {#if success}
199
+ <div
200
+ class="mb-6 p-4 bg-green-50 border-l-4 border-green-500 text-green-700 rounded-lg flex items-start gap-3"
201
+ in:scale={{ duration: 400, easing: elasticOut }}
202
+ out:fade={{ duration: 200 }}
203
+ >
204
+ <span class="text-2xl animate-bounce">✅</span>
205
+ <div>
206
+ <p class="font-semibold">Success!</p>
207
+ <p>{success}</p>
208
+ <p class="text-sm mt-1 text-green-600">Redirecting to results page...</p>
209
+ </div>
210
+ </div>
211
+ {/if}
212
+
213
+ <!-- Evaluation Form -->
214
+ {#if mounted}
215
+ <div
216
+ class="bg-white rounded-2xl shadow-xl p-8 hover:shadow-2xl transition-shadow duration-300"
217
+ in:fly={{ y: 50, duration: 600, delay: 400, easing: quintOut }}
218
+ >
219
+ <div class="space-y-8">
220
+ <!-- Project Name -->
221
+ <div>
222
+ <label for="project-name" class="block text-lg font-semibold text-gray-900 mb-3">
223
+ 📁 Project Name
224
+ </label>
225
+ <input
226
+ id="project-name"
227
+ type="text"
228
+ bind:value={projectName}
229
+ placeholder="e.g., AI Chatbot, E-commerce Platform, Portfolio Website"
230
+ class="w-full px-5 py-4 border-2 border-gray-300 rounded-xl focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 text-lg transition"
231
+ disabled={uploading}
232
+ />
233
+ </div>
234
+
235
+ <!-- Evaluation Query -->
236
+ <div>
237
+ <label for="eval-query" class="block text-lg font-semibold text-gray-900 mb-3">
238
+ 🎯 Evaluation Criteria
239
+ </label>
240
+ <textarea
241
+ id="eval-query"
242
+ bind:value={evaluationQuery}
243
+ placeholder="Describe what you want to evaluate... (e.g., 'Evaluate for an AI hackathon focusing on innovation and code quality')"
244
+ rows="4"
245
+ class="w-full px-5 py-4 border-2 border-gray-300 rounded-xl focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 text-lg transition resize-none"
246
+ disabled={uploading}
247
+ ></textarea>
248
+ <p class="mt-2 text-sm text-gray-500">
249
+ 💡 Be specific! The AI will evaluate your project based on this criteria.
250
+ </p>
251
+ </div>
252
+
253
+ <!-- File Upload Area -->
254
+ <div>
255
+ <label for="file-input" class="block text-lg font-semibold text-gray-900 mb-3">
256
+ 📤 Upload Project Files
257
+ </label>
258
+
259
+ <div
260
+ role="button"
261
+ tabindex="0"
262
+ class="relative border-4 border-dashed rounded-xl transition-all duration-200 {dragActive ? 'border-indigo-500 bg-indigo-50' : 'border-gray-300 bg-gray-50'}"
263
+ ondrop={handleDrop}
264
+ ondragover={handleDragOver}
265
+ ondragleave={handleDragLeave}
266
+ >
267
+ <input
268
+ id="file-input"
269
+ type="file"
270
+ multiple
271
+ onchange={handleFileChange}
272
+ class="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
273
+ disabled={uploading}
274
+ accept=".py,.js,.ts,.html,.css,.json,.md,.txt,.zip,.pdf,.png,.jpg,.jpeg,.gif,.svg,.java,.cpp,.c,.go,.rb,.php,.swift,.kt,.rs"
275
+ />
276
+
277
+ <div class="p-12 text-center">
278
+ <div class="text-6xl mb-4">
279
+ {#if dragActive}
280
+ 📥
281
+ {:else}
282
+ 📦
283
+ {/if}
284
+ </div>
285
+ <p class="text-xl font-semibold text-gray-700 mb-2">
286
+ {#if dragActive}
287
+ Drop your files here!
288
+ {:else}
289
+ Drag & drop files or click to browse
290
+ {/if}
291
+ </p>
292
+ <p class="text-sm text-gray-500">
293
+ Supports: Code files, docs, images, PDFs, ZIP archives
294
+ </p>
295
+ <p class="text-xs text-gray-400 mt-2">
296
+ Max size: 50MB per file
297
+ </p>
298
+ </div>
299
+ </div>
300
+
301
+ <!-- Uploaded Files List -->
302
+ {#if projectFiles && projectFiles.length > 0}
303
+ <div class="mt-4 space-y-2">
304
+ <div class="flex justify-between items-center mb-3">
305
+ <p class="text-sm font-semibold text-gray-700">
306
+ 📎 {projectFiles.length} file{projectFiles.length > 1 ? 's' : ''} selected
307
+ </p>
308
+ <button
309
+ onclick={clearFiles}
310
+ class="text-sm text-red-600 hover:text-red-700 font-medium"
311
+ disabled={uploading}
312
+ >
313
+ Clear all
314
+ </button>
315
+ </div>
316
+
317
+ <div class="max-h-64 overflow-y-auto space-y-2">
318
+ {#each Array.from(projectFiles) as file, i}
319
+ <div
320
+ class="flex items-center gap-3 p-3 bg-gray-50 rounded-lg hover:bg-gray-100 transition-all duration-200 hover:scale-102 hover:shadow-md"
321
+ in:fly={{ x: -20, duration: 300, delay: i * 50 }}
322
+ >
323
+ <span class="text-2xl">{getFileIcon(file.name)}</span>
324
+ <div class="flex-1 min-w-0">
325
+ <p class="text-sm font-medium text-gray-900 truncate">{file.name}</p>
326
+ <p class="text-xs text-gray-500">{formatFileSize(file.size)}</p>
327
+ </div>
328
+ </div>
329
+ {/each}
330
+ </div>
331
+ </div>
332
+ {/if}
333
+ </div>
334
+
335
+ <!-- Submit Button -->
336
+ <div class="pt-4">
337
+ <button
338
+ onclick={evaluateProject}
339
+ disabled={uploading || !projectName.trim() || !projectFiles || projectFiles.length === 0}
340
+ class="w-full py-5 px-6 bg-gradient-to-r from-indigo-600 to-purple-600 text-white text-xl font-bold rounded-xl hover:from-indigo-700 hover:to-purple-700 disabled:from-gray-400 disabled:to-gray-500 disabled:cursor-not-allowed transition-all duration-200 shadow-lg hover:shadow-xl transform hover:-translate-y-0.5"
341
+ >
342
+ {#if uploading}
343
+ <span class="flex items-center justify-center gap-3">
344
+ <svg class="animate-spin h-6 w-6 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
345
+ <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
346
+ <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
347
+ </svg>
348
+ Evaluating with AI...
349
+ </span>
350
+ {:else}
351
+ 🚀 Start AI Evaluation
352
+ {/if}
353
+ </button>
354
+ </div>
355
+ </div>
356
+ </div>
357
+
358
+ <!-- Info Cards -->
359
+ <div class="mt-12 grid grid-cols-1 md:grid-cols-3 gap-6">
360
+ <div
361
+ class="bg-white rounded-xl p-6 shadow-lg border-2 border-indigo-100 hover:border-indigo-300 hover:shadow-xl transition-all duration-300 hover:-translate-y-1 transform"
362
+ in:scale={{ duration: 400, delay: 600, easing: elasticOut }}
363
+ >
364
+ <div class="text-4xl mb-3 animate-bounce">⚡</div>
365
+ <h3 class="text-lg font-bold text-gray-900 mb-2">Instant Analysis</h3>
366
+ <p class="text-sm text-gray-600">Get AI-powered evaluation results in seconds</p>
367
+ </div>
368
+
369
+ <div
370
+ class="bg-white rounded-xl p-6 shadow-lg border-2 border-purple-100 hover:border-purple-300 hover:shadow-xl transition-all duration-300 hover:-translate-y-1 transform"
371
+ in:scale={{ duration: 400, delay: 700, easing: elasticOut }}
372
+ >
373
+ <div class="text-4xl mb-3 animate-pulse">📊</div>
374
+ <h3 class="text-lg font-bold text-gray-900 mb-2">5 Key Metrics</h3>
375
+ <p class="text-sm text-gray-600">Relevance, Technical, Creativity, Documentation, Productivity</p>
376
+ </div>
377
+
378
+ <div
379
+ class="bg-white rounded-xl p-6 shadow-lg border-2 border-pink-100 hover:border-pink-300 hover:shadow-xl transition-all duration-300 hover:-translate-y-1 transform"
380
+ in:scale={{ duration: 400, delay: 800, easing: elasticOut }}
381
+ >
382
+ <div class="text-4xl mb-3">💡</div>
383
+ <h3 class="text-lg font-bold text-gray-900 mb-2">AI Feedback</h3>
384
+ <p class="text-sm text-gray-600">Detailed insights and improvement suggestions</p>
385
+ </div>
386
+ </div>
387
+ {/if}
388
+ </main>
389
+ </div>
390
+
391
+ <style>
392
+ @keyframes gradient {
393
+ 0% { background-position: 0% 50%; }
394
+ 50% { background-position: 100% 50%; }
395
+ 100% { background-position: 0% 50%; }
396
+ }
397
+
398
+ .animate-gradient {
399
+ animation: gradient 3s ease infinite;
400
+ }
401
+ </style>
402
+
src/pages/Results.svelte ADDED
@@ -0,0 +1,402 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { onMount } from 'svelte';
3
+ import { hackathonApi, evaluationApi, type Hackathon } from '../lib/api';
4
+ import RadialChart from '../components/RadialChart.svelte';
5
+ import BarChart from '../components/BarChart.svelte';
6
+ import { fly, fade, scale, slide } from 'svelte/transition';
7
+ import { elasticOut, quintOut, backOut } from 'svelte/easing';
8
+
9
+ interface Props {
10
+ hackathonId?: string;
11
+ }
12
+
13
+ let { hackathonId }: Props = $props();
14
+
15
+ let hackathons = $state<Hackathon[]>([]);
16
+ let selectedHackathonId = $state<number | null>(hackathonId ? parseInt(hackathonId) : null);
17
+ let selectedHackathon = $state<Hackathon | null>(null);
18
+ let results = $state<any[]>([]);
19
+ let loading = $state(false);
20
+ let error = $state('');
21
+ let expandedResultId = $state<number | null>(null);
22
+ let mounted = $state(false);
23
+
24
+ onMount(() => {
25
+ mounted = true;
26
+ });
27
+
28
+ onMount(async () => {
29
+ await loadHackathons();
30
+ if (selectedHackathonId) {
31
+ await loadResults();
32
+ }
33
+ });
34
+
35
+ async function loadHackathons() {
36
+ try {
37
+ loading = true;
38
+ error = '';
39
+ hackathons = await hackathonApi.getAll();
40
+ if (selectedHackathonId) {
41
+ selectedHackathon = hackathons.find(h => h.id === selectedHackathonId) || null;
42
+ }
43
+ } catch (err: any) {
44
+ error = err.message || 'Failed to load hackathons';
45
+ console.error('Error loading hackathons:', err);
46
+ } finally {
47
+ loading = false;
48
+ }
49
+ }
50
+
51
+ async function loadResults() {
52
+ if (!selectedHackathonId) return;
53
+
54
+ try {
55
+ loading = true;
56
+ error = '';
57
+ results = await evaluationApi.getByHackathon(selectedHackathonId);
58
+ selectedHackathon = hackathons.find(h => h.id === selectedHackathonId) || null;
59
+
60
+ // Sort by overall score
61
+ results.sort((a, b) => (b.evaluation?.overall_score || 0) - (a.evaluation?.overall_score || 0));
62
+ } catch (err: any) {
63
+ error = err.message || 'Failed to load results';
64
+ console.error('Error loading results:', err);
65
+ } finally {
66
+ loading = false;
67
+ }
68
+ }
69
+
70
+ function getMedalEmoji(rank: number): string {
71
+ if (rank === 0) return '🥇';
72
+ if (rank === 1) return '🥈';
73
+ if (rank === 2) return '🥉';
74
+ return '🏅';
75
+ }
76
+
77
+ function getScoreColor(score: number): string {
78
+ if (score >= 8) return 'text-green-600';
79
+ if (score >= 6) return 'text-blue-600';
80
+ if (score >= 4) return 'text-yellow-600';
81
+ return 'text-red-600';
82
+ }
83
+
84
+ function getScoreBarColor(score: number): string {
85
+ if (score >= 8) return 'bg-green-500';
86
+ if (score >= 6) return 'bg-blue-500';
87
+ if (score >= 4) return 'bg-yellow-500';
88
+ return 'bg-red-500';
89
+ }
90
+
91
+ function toggleDetails(resultId: number) {
92
+ expandedResultId = expandedResultId === resultId ? null : resultId;
93
+ }
94
+ </script>
95
+
96
+ <div class="min-h-screen bg-gradient-to-br from-purple-50 to-pink-100">
97
+ <!-- Header -->
98
+ <header class="bg-white shadow">
99
+ <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
100
+ <div class="flex justify-between items-center">
101
+ <div>
102
+ <h1 class="text-3xl font-bold text-gray-900">🏆 Leaderboard & Results</h1>
103
+ <p class="mt-1 text-sm text-gray-500">View AI evaluation results and rankings</p>
104
+ </div>
105
+ <div class="flex gap-3">
106
+ <a href="#/" class="px-4 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700 transition">
107
+ Dashboard
108
+ </a>
109
+ <a href="#/submit" class="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition">
110
+ Submit Project
111
+ </a>
112
+ </div>
113
+ </div>
114
+ </div>
115
+ </header>
116
+
117
+ <main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
118
+ {#if error}
119
+ <div class="mb-4 p-4 bg-red-50 border border-red-200 rounded-lg">
120
+ <p class="text-red-800">{error}</p>
121
+ </div>
122
+ {/if}
123
+
124
+ <!-- Hackathon Selector -->
125
+ <div class="bg-white rounded-lg shadow p-6 mb-6">
126
+ <label for="hackathon-select" class="block text-sm font-medium text-gray-700 mb-3">
127
+ Select Hackathon
128
+ </label>
129
+ {#if loading && hackathons.length === 0}
130
+ <div class="text-gray-500">Loading...</div>
131
+ {:else if hackathons.length === 0}
132
+ <p class="text-gray-500">No hackathons found</p>
133
+ {:else}
134
+ <select
135
+ id="hackathon-select"
136
+ bind:value={selectedHackathonId}
137
+ onchange={loadResults}
138
+ class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
139
+ >
140
+ <option value={null}>Choose a hackathon...</option>
141
+ {#each hackathons as hackathon}
142
+ <option value={hackathon.id}>{hackathon.name}</option>
143
+ {/each}
144
+ </select>
145
+ {/if}
146
+ </div>
147
+
148
+ {#if !selectedHackathonId}
149
+ <div class="bg-white rounded-lg shadow p-12 text-center">
150
+ <div class="text-6xl mb-4">🎯</div>
151
+ <h3 class="text-xl font-semibold text-gray-700 mb-2">Select a Hackathon</h3>
152
+ <p class="text-gray-500">Choose a hackathon to view its evaluation results and leaderboard</p>
153
+ </div>
154
+ {:else}
155
+ <!-- Hackathon Info -->
156
+ {#if selectedHackathon}
157
+ <div class="bg-gradient-to-r from-primary-600 to-indigo-600 rounded-lg shadow-lg p-6 mb-6 text-white">
158
+ <h2 class="text-2xl font-bold mb-2">{selectedHackathon.name}</h2>
159
+ <p class="text-primary-100">{selectedHackathon.description}</p>
160
+ <div class="mt-4 flex items-center gap-4 text-sm">
161
+ <span>📊 {results.length} Submissions</span>
162
+ <span>📅 Created: {new Date(selectedHackathon.created_at || '').toLocaleDateString()}</span>
163
+ </div>
164
+ </div>
165
+ {/if}
166
+
167
+ <!-- Results Table -->
168
+ {#if loading}
169
+ <div class="bg-white rounded-lg shadow p-12 text-center">
170
+ <div class="text-gray-500">Loading results...</div>
171
+ </div>
172
+ {:else if results.length === 0}
173
+ <div class="bg-white rounded-lg shadow p-12 text-center">
174
+ <div class="text-6xl mb-4">📭</div>
175
+ <h3 class="text-xl font-semibold text-gray-700 mb-2">No Results Yet</h3>
176
+ <p class="text-gray-500">Submissions are still being evaluated or no submissions have been made.</p>
177
+ </div>
178
+ {:else}
179
+ <div class="bg-white rounded-lg shadow overflow-hidden">
180
+ <!-- Summary Stats -->
181
+ <div class="p-6 bg-gray-50 border-b border-gray-200">
182
+ <div class="grid grid-cols-1 md:grid-cols-4 gap-4">
183
+ <div class="text-center">
184
+ <div class="text-3xl font-bold text-primary-600">
185
+ {results.filter(r => r.evaluation).length}
186
+ </div>
187
+ <div class="text-sm text-gray-600">Evaluated</div>
188
+ </div>
189
+ <div class="text-center">
190
+ <div class="text-3xl font-bold text-green-600">
191
+ {(results.reduce((acc, r) => acc + (r.evaluation?.overall_score || 0), 0) / results.filter(r => r.evaluation).length).toFixed(1)}
192
+ </div>
193
+ <div class="text-sm text-gray-600">Avg Score</div>
194
+ </div>
195
+ <div class="text-center">
196
+ <div class="text-3xl font-bold text-blue-600">
197
+ {Math.max(...results.map(r => r.evaluation?.overall_score || 0)).toFixed(1)}
198
+ </div>
199
+ <div class="text-sm text-gray-600">Highest</div>
200
+ </div>
201
+ <div class="text-center">
202
+ <div class="text-3xl font-bold text-yellow-600">
203
+ {results.filter(r => !r.evaluation).length}
204
+ </div>
205
+ <div class="text-sm text-gray-600">Pending</div>
206
+ </div>
207
+ </div>
208
+ </div>
209
+
210
+ <!-- Leaderboard -->
211
+ <div class="p-6">
212
+ <h3 class="text-lg font-semibold mb-4">🏆 Rankings</h3>
213
+ <div class="space-y-4">
214
+ {#each results as result, index}
215
+ {@const evaluation = result.evaluation}
216
+ <div class="border border-gray-200 rounded-lg p-4 hover:border-primary-300 transition {index < 3 ? 'bg-gradient-to-r from-yellow-50 to-orange-50' : ''}">
217
+ <div class="flex items-start gap-4">
218
+ <!-- Rank -->
219
+ <div class="flex-shrink-0 text-center">
220
+ <div class="text-3xl">{getMedalEmoji(index)}</div>
221
+ <div class="text-sm font-semibold text-gray-600 mt-1">#{index + 1}</div>
222
+ </div>
223
+
224
+ <!-- Project Info -->
225
+ <div class="flex-1">
226
+ <div class="flex justify-between items-start mb-3">
227
+ <div>
228
+ <h4 class="text-lg font-bold text-gray-900">{result.project_name}</h4>
229
+ <p class="text-sm text-gray-600">Team: {result.team_name}</p>
230
+ </div>
231
+ {#if evaluation}
232
+ <div class="text-right">
233
+ <div class="text-3xl font-bold {getScoreColor(evaluation.overall_score)}">
234
+ {evaluation.overall_score.toFixed(1)}
235
+ </div>
236
+ <div class="text-xs text-gray-500">/ 10</div>
237
+ </div>
238
+ {:else}
239
+ <span class="px-3 py-1 bg-yellow-100 text-yellow-800 rounded-full text-sm">
240
+ ⏳ Evaluating...
241
+ </span>
242
+ {/if}
243
+ </div>
244
+
245
+ {#if evaluation}
246
+ <!-- Score Breakdown -->
247
+ <div class="grid grid-cols-2 md:grid-cols-5 gap-3 mb-3">
248
+ <div>
249
+ <div class="text-xs text-gray-600 mb-1">Relevance</div>
250
+ <div class="flex items-center gap-2">
251
+ <div class="flex-1 bg-gray-200 rounded-full h-2">
252
+ <div class="{getScoreBarColor(evaluation.relevance_score)} h-2 rounded-full" style="width: {evaluation.relevance_score * 10}%"></div>
253
+ </div>
254
+ <span class="text-sm font-semibold {getScoreColor(evaluation.relevance_score)}">
255
+ {evaluation.relevance_score.toFixed(1)}
256
+ </span>
257
+ </div>
258
+ </div>
259
+
260
+ <div>
261
+ <div class="text-xs text-gray-600 mb-1">Technical</div>
262
+ <div class="flex items-center gap-2">
263
+ <div class="flex-1 bg-gray-200 rounded-full h-2">
264
+ <div class="{getScoreBarColor(evaluation.technical_complexity_score)} h-2 rounded-full" style="width: {evaluation.technical_complexity_score * 10}%"></div>
265
+ </div>
266
+ <span class="text-sm font-semibold {getScoreColor(evaluation.technical_complexity_score)}">
267
+ {evaluation.technical_complexity_score.toFixed(1)}
268
+ </span>
269
+ </div>
270
+ </div>
271
+
272
+ <div>
273
+ <div class="text-xs text-gray-600 mb-1">Creativity</div>
274
+ <div class="flex items-center gap-2">
275
+ <div class="flex-1 bg-gray-200 rounded-full h-2">
276
+ <div class="{getScoreBarColor(evaluation.creativity_score)} h-2 rounded-full" style="width: {evaluation.creativity_score * 10}%"></div>
277
+ </div>
278
+ <span class="text-sm font-semibold {getScoreColor(evaluation.creativity_score)}">
279
+ {evaluation.creativity_score.toFixed(1)}
280
+ </span>
281
+ </div>
282
+ </div>
283
+
284
+ <div>
285
+ <div class="text-xs text-gray-600 mb-1">Documentation</div>
286
+ <div class="flex items-center gap-2">
287
+ <div class="flex-1 bg-gray-200 rounded-full h-2">
288
+ <div class="{getScoreBarColor(evaluation.documentation_score)} h-2 rounded-full" style="width: {evaluation.documentation_score * 10}%"></div>
289
+ </div>
290
+ <span class="text-sm font-semibold {getScoreColor(evaluation.documentation_score)}">
291
+ {evaluation.documentation_score.toFixed(1)}
292
+ </span>
293
+ </div>
294
+ </div>
295
+
296
+ <div>
297
+ <div class="text-xs text-gray-600 mb-1">Productivity</div>
298
+ <div class="flex items-center gap-2">
299
+ <div class="flex-1 bg-gray-200 rounded-full h-2">
300
+ <div class="{getScoreBarColor(evaluation.productivity_score)} h-2 rounded-full" style="width: {evaluation.productivity_score * 10}%"></div>
301
+ </div>
302
+ <span class="text-sm font-semibold {getScoreColor(evaluation.productivity_score)}">
303
+ {evaluation.productivity_score.toFixed(1)}
304
+ </span>
305
+ </div>
306
+ </div>
307
+ </div>
308
+
309
+ <!-- View Details Button -->
310
+ <button
311
+ onclick={() => toggleDetails(result.id)}
312
+ class="mt-4 w-full px-4 py-2 bg-gradient-to-r from-primary-600 to-indigo-600 text-white rounded-lg hover:from-primary-700 hover:to-indigo-700 transition flex items-center justify-center gap-2"
313
+ >
314
+ {#if expandedResultId === result.id}
315
+ 📊 Hide Detailed Analysis
316
+ {:else}
317
+ 📊 View Detailed Analysis with Charts
318
+ {/if}
319
+ </button>
320
+
321
+ <!-- Expanded Detailed View -->
322
+ {#if expandedResultId === result.id}
323
+ <div
324
+ class="mt-6 p-6 bg-gradient-to-br from-gray-50 to-blue-50 rounded-lg border-2 border-primary-200"
325
+ in:slide={{ duration: 400, easing: quintOut }}
326
+ >
327
+ <h4
328
+ class="text-lg font-bold text-gray-900 mb-6 flex items-center gap-2"
329
+ in:fade={{ delay: 100, duration: 300 }}
330
+ >
331
+ 📊 Detailed Score Analysis
332
+ </h4>
333
+
334
+ <!-- Radial Charts -->
335
+ <div class="grid grid-cols-2 md:grid-cols-5 gap-6 mb-6">
336
+ <div in:scale={{ delay: 200, duration: 500, easing: backOut }}>
337
+ <RadialChart score={evaluation.relevance_score} label="Relevance" />
338
+ </div>
339
+ <div in:scale={{ delay: 300, duration: 500, easing: backOut }}>
340
+ <RadialChart score={evaluation.technical_complexity_score} label="Technical" />
341
+ </div>
342
+ <div in:scale={{ delay: 400, duration: 500, easing: backOut }}>
343
+ <RadialChart score={evaluation.creativity_score} label="Creativity" />
344
+ </div>
345
+ <div in:scale={{ delay: 500, duration: 500, easing: backOut }}>
346
+ <RadialChart score={evaluation.documentation_score} label="Documentation" />
347
+ </div>
348
+ <div in:scale={{ delay: 600, duration: 500, easing: backOut }}>
349
+ <RadialChart score={evaluation.productivity_score} label="Productivity" />
350
+ </div>
351
+ </div>
352
+
353
+ <!-- Bar Charts -->
354
+ <div
355
+ class="bg-white rounded-lg p-6 shadow-sm mb-6"
356
+ in:fly={{ y: 20, delay: 700, duration: 400 }}
357
+ >
358
+ <h5 class="text-md font-semibold text-gray-900 mb-4">📈 Score Breakdown</h5>
359
+ <div class="space-y-4">
360
+ <div in:fly={{ x: -20, delay: 800, duration: 300 }}>
361
+ <BarChart score={evaluation.relevance_score} label="Relevance" />
362
+ </div>
363
+ <div in:fly={{ x: -20, delay: 900, duration: 300 }}>
364
+ <BarChart score={evaluation.technical_complexity_score} label="Technical Complexity" />
365
+ </div>
366
+ <div in:fly={{ x: -20, delay: 1000, duration: 300 }}>
367
+ <BarChart score={evaluation.creativity_score} label="Creativity" />
368
+ </div>
369
+ <div in:fly={{ x: -20, delay: 1100, duration: 300 }}>
370
+ <BarChart score={evaluation.documentation_score} label="Documentation" />
371
+ </div>
372
+ <div in:fly={{ x: -20, delay: 1200, duration: 300 }}>
373
+ <BarChart score={evaluation.productivity_score} label="Productivity" />
374
+ </div>
375
+ </div>
376
+ </div>
377
+
378
+ <!-- AI Feedback -->
379
+ <div
380
+ class="bg-white rounded-lg p-6 shadow-sm"
381
+ in:scale={{ delay: 1300, duration: 400, easing: elasticOut }}
382
+ >
383
+ <h5 class="text-md font-semibold text-gray-900 mb-3 flex items-center gap-2">
384
+ 💬 AI Evaluation Feedback
385
+ </h5>
386
+ <p class="text-sm text-gray-700 leading-relaxed">{evaluation.feedback}</p>
387
+ </div>
388
+ </div>
389
+ {/if}
390
+ {/if}
391
+ </div>
392
+ </div>
393
+ </div>
394
+ {/each}
395
+ </div>
396
+ </div>
397
+ </div>
398
+ {/if}
399
+ {/if}
400
+ </main>
401
+ </div>
402
+
tailwind.config.js ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /** @type {import('tailwindcss').Config} */
2
+ export default {
3
+ content: [
4
+ './index.html',
5
+ './src/**/*.{svelte,js,ts,jsx,tsx}',
6
+ ],
7
+ theme: {
8
+ extend: {
9
+ colors: {
10
+ primary: {
11
+ 50: '#eff6ff',
12
+ 100: '#dbeafe',
13
+ 200: '#bfdbfe',
14
+ 300: '#93c5fd',
15
+ 400: '#60a5fa',
16
+ 500: '#3b82f6',
17
+ 600: '#2563eb',
18
+ 700: '#1d4ed8',
19
+ 800: '#1e40af',
20
+ 900: '#1e3a8a',
21
+ },
22
+ },
23
+ },
24
+ },
25
+ plugins: [],
26
+ }
27
+
utils.py ADDED
@@ -0,0 +1,220 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import zipfile
3
+ import shutil
4
+ from werkzeug.utils import secure_filename
5
+ from config import Config
6
+
7
+ def allowed_file(filename):
8
+ """Check if file extension is allowed"""
9
+ return '.' in filename and \
10
+ filename.rsplit('.', 1)[1].lower() in Config.ALLOWED_EXTENSIONS
11
+
12
+ def extract_code_from_files(file_paths):
13
+ """Extract code content from uploaded files with smart filtering"""
14
+ code_content = []
15
+ total_size = 0
16
+ max_total_size = 100 * 1024 * 1024 # 10MB limit for code content
17
+
18
+ for file_path in file_paths:
19
+ if not os.path.exists(file_path):
20
+ continue
21
+
22
+ try:
23
+ # Handle zip files
24
+ if file_path.endswith('.zip'):
25
+ zip_content, zip_size = extract_from_zip_smart(file_path, max_total_size - total_size)
26
+ code_content.extend(zip_content)
27
+ total_size += zip_size
28
+ else:
29
+ # Check file size before reading - be more generous for project code
30
+ file_size = os.path.getsize(file_path)
31
+ if file_size > 5 * 1024 * 1024: # Skip files larger than 5MB (very generous)
32
+ code_content.append(f"# File: {os.path.basename(file_path)} (SKIPPED - too large: {file_size//1024}KB)\n")
33
+ continue
34
+
35
+ if total_size + file_size > max_total_size:
36
+ code_content.append(f"# Remaining files skipped - size limit reached ({max_total_size//1024//1024}MB)\n")
37
+ break
38
+
39
+ # Try to read as text
40
+ with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
41
+ content = f.read()
42
+ code_content.append(f"# File: {os.path.basename(file_path)}\n{content}\n")
43
+ total_size += len(content)
44
+
45
+ except Exception as e:
46
+ print(f"Error reading file {file_path}: {str(e)}")
47
+ code_content.append(f"# File: {os.path.basename(file_path)} (ERROR: {str(e)})\n")
48
+
49
+ print(f"📊 Code extraction complete: {len(code_content)} files, {total_size//1024}KB total")
50
+ return "\n\n".join(code_content)
51
+
52
+ def should_skip_directory(dir_path):
53
+ """Check if directory should be skipped - only skip truly irrelevant directories"""
54
+ skip_dirs = {
55
+ # Dependencies and package managers
56
+ 'node_modules', 'vendor', 'packages', '.pnpm-store',
57
+
58
+ # Version control
59
+ '.git', '.svn', '.hg',
60
+
61
+ # Build outputs and artifacts
62
+ 'build', 'dist', 'out', '.next', '.nuxt', 'target', 'bin', 'obj',
63
+ 'public/build', 'static/build', 'assets/build',
64
+
65
+ # Cache and temporary files
66
+ '__pycache__', '.pytest_cache', '.cache', '.parcel-cache',
67
+ '.nyc_output', 'coverage', 'htmlcov',
68
+ 'tmp', 'temp', 'logs', 'log',
69
+
70
+ # IDE and editor files
71
+ '.vscode', '.idea', '.vs', '.sublime-project',
72
+
73
+ # OS generated files
74
+ '.ds_store', 'thumbs.db',
75
+
76
+ # Environment and secrets (but keep example files)
77
+ '.env.local', '.env.production'
78
+ }
79
+
80
+ dir_name = os.path.basename(dir_path).lower()
81
+
82
+ # Skip hidden directories except important ones
83
+ if dir_name.startswith('.'):
84
+ important_hidden = {'.github', '.gitlab', '.docker', '.vscode', '.idea'}
85
+ return dir_name not in important_hidden
86
+
87
+ return dir_name in skip_dirs
88
+
89
+ def should_prioritize_file(file_path):
90
+ """Check if file should be prioritized for extraction"""
91
+ filename = os.path.basename(file_path).lower()
92
+ priority_files = {
93
+ 'readme.md', 'readme.txt', 'readme', 'main.py', 'index.js',
94
+ 'app.py', 'server.js', 'package.json', 'requirements.txt',
95
+ 'dockerfile', 'docker-compose.yml', 'config.py', 'settings.py'
96
+ }
97
+ return filename in priority_files
98
+
99
+ def extract_from_zip_smart(zip_path, max_size_remaining):
100
+ """Smart extraction from ZIP with filtering and prioritization"""
101
+ extracted_content = []
102
+ extract_dir = zip_path + '_extracted'
103
+ total_size = 0
104
+
105
+ try:
106
+ with zipfile.ZipFile(zip_path, 'r') as zip_ref:
107
+ zip_ref.extractall(extract_dir)
108
+
109
+ # First pass: collect and prioritize files
110
+ all_files = []
111
+ priority_files = []
112
+
113
+ for root, dirs, files in os.walk(extract_dir):
114
+ # Skip unwanted directories
115
+ dirs[:] = [d for d in dirs if not should_skip_directory(os.path.join(root, d))]
116
+
117
+ for file in files:
118
+ file_path = os.path.join(root, file)
119
+ if allowed_file(file):
120
+ relative_path = os.path.relpath(file_path, extract_dir)
121
+
122
+ # Check file size
123
+ try:
124
+ file_size = os.path.getsize(file_path)
125
+ if file_size > 500 * 1024: # Skip files larger than 500KB
126
+ continue
127
+
128
+ file_info = (file_path, relative_path, file_size)
129
+
130
+ if should_prioritize_file(file_path):
131
+ priority_files.append(file_info)
132
+ else:
133
+ all_files.append(file_info)
134
+ except:
135
+ continue
136
+
137
+ # Process priority files first
138
+ for file_path, relative_path, file_size in priority_files:
139
+ if total_size + file_size > max_size_remaining:
140
+ break
141
+
142
+ try:
143
+ with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
144
+ content = f.read()
145
+ extracted_content.append(f"# File: {relative_path} [PRIORITY]\n{content}\n")
146
+ total_size += len(content)
147
+ except Exception as e:
148
+ print(f"Error reading priority file {file_path}: {str(e)}")
149
+
150
+ # Process remaining files
151
+ for file_path, relative_path, file_size in all_files:
152
+ if total_size + file_size > max_size_remaining:
153
+ extracted_content.append(f"# Remaining files skipped - size limit reached\n")
154
+ break
155
+
156
+ try:
157
+ with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
158
+ content = f.read()
159
+ extracted_content.append(f"# File: {relative_path}\n{content}\n")
160
+ total_size += len(content)
161
+ except Exception as e:
162
+ print(f"Error reading file {file_path}: {str(e)}")
163
+
164
+ # Clean up extracted directory
165
+ shutil.rmtree(extract_dir, ignore_errors=True)
166
+
167
+ print(f"📦 ZIP extraction: {len(extracted_content)} files, {total_size//1024}KB")
168
+
169
+ except Exception as e:
170
+ print(f"Error extracting zip file {zip_path}: {str(e)}")
171
+
172
+ return extracted_content, total_size
173
+
174
+ def extract_from_zip(zip_path):
175
+ """Legacy function for backward compatibility"""
176
+ content, _ = extract_from_zip_smart(zip_path, 10 * 1024 * 1024)
177
+ return content
178
+
179
+ def extract_documentation(file_paths, project_description):
180
+ """Extract documentation from files (README, .md files, etc.)"""
181
+ doc_content = [f"Project Description:\n{project_description}\n\n"]
182
+
183
+ for file_path in file_paths:
184
+ if not os.path.exists(file_path):
185
+ continue
186
+
187
+ filename = os.path.basename(file_path).lower()
188
+
189
+ # Look for documentation files
190
+ if any(doc in filename for doc in ['readme', '.md', 'doc', '.txt']):
191
+ try:
192
+ with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
193
+ content = f.read()
194
+ doc_content.append(f"# {os.path.basename(file_path)}\n{content}\n")
195
+ except Exception as e:
196
+ print(f"Error reading doc file {file_path}: {str(e)}")
197
+
198
+ return "\n\n".join(doc_content)
199
+
200
+ def create_upload_folder():
201
+ """Create upload folder if it doesn't exist"""
202
+ if not os.path.exists(Config.UPLOAD_FOLDER):
203
+ os.makedirs(Config.UPLOAD_FOLDER)
204
+
205
+ def save_uploaded_file(file, submission_id):
206
+ """Save uploaded file and return path"""
207
+ create_upload_folder()
208
+
209
+ filename = secure_filename(file.filename)
210
+ submission_folder = os.path.join(Config.UPLOAD_FOLDER, f'submission_{submission_id}')
211
+
212
+ if not os.path.exists(submission_folder):
213
+ os.makedirs(submission_folder)
214
+
215
+ file_path = os.path.join(submission_folder, filename)
216
+ file.save(file_path)
217
+
218
+ return file_path
219
+
220
+
vite.config.ts CHANGED
@@ -4,4 +4,13 @@ import { svelte } from '@sveltejs/vite-plugin-svelte'
4
  // https://vite.dev/config/
5
  export default defineConfig({
6
  plugins: [svelte()],
 
 
 
 
 
 
 
 
 
7
  })
 
4
  // https://vite.dev/config/
5
  export default defineConfig({
6
  plugins: [svelte()],
7
+ server: {
8
+ port: 5173,
9
+ proxy: {
10
+ '/api': {
11
+ target: 'http://localhost:5000',
12
+ changeOrigin: true,
13
+ }
14
+ }
15
+ }
16
  })