Spaces:
Configuration error
Configuration error
anushkap01patidar
commited on
Commit
·
d122c3c
1
Parent(s):
efca9c4
full code
Browse files- Dockerfile +26 -0
- README.md +30 -57
- app.py +270 -0
- chunking_utils.py +214 -0
- config.py +63 -0
- evaluator.py +384 -0
- index.html +1 -1
- models.py +101 -0
- package-lock.json +0 -0
- package.json +9 -2
- postcss.config.js +7 -0
- requirements.txt +10 -0
- src/App.svelte +30 -42
- src/app.css +6 -67
- src/components/BarChart.svelte +44 -0
- src/components/RadialChart.svelte +71 -0
- src/lib/Counter.svelte +0 -10
- src/lib/api.ts +144 -0
- src/lib/router.ts +24 -0
- src/pages/AllResults.svelte +407 -0
- src/pages/IndividualResult.svelte +319 -0
- src/pages/ProjectEvaluator.svelte +402 -0
- src/pages/Results.svelte +402 -0
- tailwind.config.js +27 -0
- utils.py +220 -0
- vite.config.ts +9 -0
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 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 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>
|
| 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": "
|
| 3 |
"private": true,
|
| 4 |
-
"version": "
|
| 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
|
| 3 |
-
import
|
| 4 |
-
import
|
| 5 |
-
|
| 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 |
-
|
| 19 |
-
|
| 20 |
-
</div>
|
| 21 |
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
|
|
|
| 25 |
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 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 |
-
|
| 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 |
})
|