CodeEvaluator / index.html
DhyandevP's picture
chore: Apply Ponytail audit fixes and code cleanup
a963267
Raw History Blame Contribute Delete
23.5 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Code Evaluator</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: system-ui, sans-serif;
background: #000;
color: #e5e5e5;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
}
.box {
background: #111;
border: 1px solid #333;
border-radius: 12px;
padding: 36px;
width: 100%;
max-width: 640px;
}
h1 { font-size: 1.4rem; font-weight: 700; margin-bottom: 6px; color: #fff; }
.sub { color: #777; font-size: 0.875rem; margin-bottom: 28px; }
label {
display: block;
font-size: 0.78rem;
font-weight: 600;
color: #999;
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 6px;
}
input[type="text"], input[type="password"], textarea, .file-area {
width: 100%;
background: #000;
border: 1px solid #333;
border-radius: 8px;
color: #e5e5e5;
font-size: 0.875rem;
padding: 10px 14px;
outline: none;
margin-bottom: 16px;
font-family: monospace;
}
textarea { resize: vertical; min-height: 80px; }
input[type="text"]:focus, input[type="password"]:focus, textarea:focus { border-color: #888; }
/* ── Pick links inside drop zone ────────────────────────────── */
.pick-links {
display: flex;
gap: 16px;
justify-content: center;
margin-top: 8px;
}
.pick-link {
color: #888;
font-size: 0.76rem;
cursor: pointer;
text-decoration: underline;
transition: color 0.2s;
}
.pick-link:hover { color: #ddd; }
/* ── Drop zone ──────────────────────────────────────────────── */
.file-area {
cursor: pointer;
text-align: center;
padding: 24px;
border-style: dashed;
color: #777;
transition: border-color 0.2s, color 0.2s;
position: relative;
}
.file-area:hover, .file-area.drag { border-color: #aaa; color: #ccc; }
.drop-icon { font-size: 1.6rem; margin-bottom: 6px; display: block; }
.drop-label { font-size: 0.82rem; }
/* ── File preview ───────────────────────────────────────────── */
#filePreview {
display: none;
max-height: 160px;
overflow-y: auto;
background: #0a0a0a;
border: 1px solid #333;
border-radius: 8px;
padding: 10px 14px;
margin-bottom: 16px;
font-family: monospace;
font-size: 0.78rem;
color: #aaa;
line-height: 1.6;
}
#filePreview .preview-header {
color: #ccc;
font-weight: 700;
margin-bottom: 4px;
}
#filePreview .preview-file { color: #888; }
#filePreview .preview-file .fsize { color: #555; }
#filePreview .preview-total {
border-top: 1px solid #222;
margin-top: 6px;
padding-top: 6px;
color: #999;
font-weight: 600;
}
/* ── Submit button ──────────────────────────────────────────── */
button#submitBtn {
width: 100%;
background: #fff;
color: #000;
border: none;
border-radius: 8px;
padding: 12px;
font-size: 0.95rem;
font-weight: 600;
cursor: pointer;
transition: background 0.2s;
}
button#submitBtn:hover:not(:disabled) { background: #ddd; }
button#submitBtn:disabled { opacity: 0.35; cursor: not-allowed; }
/* ── Status / Report ────────────────────────────────────────── */
#status {
margin-top: 20px;
padding: 14px;
border-radius: 8px;
font-size: 0.875rem;
display: none;
background: #0a0a0a;
border: 1px solid #333;
}
.tag {
display: inline-block;
padding: 2px 10px;
border-radius: 99px;
font-size: 0.75rem;
font-weight: 700;
margin-bottom: 8px;
}
.tag-processing { background: rgba(255,255,255,0.08); color: #aaa; }
.tag-completed { background: rgba(255,255,255,0.1); color: #fff; }
.tag-failed { background: rgba(255,255,255,0.06); color: #888; }
.meta-row { display: flex; gap: 10px; align-items: center; margin: 8px 0; flex-wrap: wrap; }
.lang-badge {
display: inline-block; padding: 3px 12px; border-radius: 6px;
font-size: 0.78rem; font-weight: 700;
background: rgba(255,255,255,0.08); color: #ccc;
}
.score-row { display: flex; gap: 16px; margin: 12px 0; }
.score-block { flex: 1; background: #111; border: 1px solid #333; border-radius: 8px; padding: 12px; text-align: center; }
.score-num { font-size: 1.8rem; font-weight: 700; color: #fff; }
.score-lbl { font-size: 0.7rem; color: #777; text-transform: uppercase; }
.section-title { font-size: 0.78rem; font-weight: 700; color: #999; text-transform: uppercase; letter-spacing: 0.05em; margin: 14px 0 6px; }
.spinner { display: inline-block; width: 12px; height: 12px; border: 2px solid rgba(255,255,255,0.15); border-top-color: #fff; border-radius: 50%; animation: spin 0.7s linear infinite; vertical-align: middle; margin-right: 6px; }
@keyframes spin { to { transform: rotate(360deg); } }
.job-id { font-family: monospace; font-size: 0.72rem; color: #555; margin-top: 8px; word-break: break-all; }
/* ── Question cards ─────────────────────────────────────────── */
.q-card {
background: #111;
border: 1px solid #333;
border-radius: 8px;
padding: 12px 14px;
margin-bottom: 8px;
}
.q-header {
display: flex;
align-items: flex-start;
gap: 8px;
margin-bottom: 6px;
}
.q-badge {
flex-shrink: 0;
display: inline-block;
padding: 2px 8px;
border-radius: 4px;
font-size: 0.72rem;
font-weight: 700;
margin-top: 1px;
}
.q-badge-pass { background: rgba(74,222,128,0.15); color: #4ade80; }
.q-badge-fail { background: rgba(248,113,113,0.15); color: #f87171; }
.q-text { font-size: 0.84rem; color: #e5e5e5; font-weight: 600; line-height: 1.4; }
.q-feedback { font-size: 0.8rem; color: #999; line-height: 1.5; margin-top: 4px; }
/* ── Code quality block ─────────────────────────────────────── */
.quality-block {
padding: 10px 14px;
background: rgba(255,255,255,0.03);
border: 1px solid #333;
border-radius: 6px;
font-size: 0.82rem;
color: #ccc;
line-height: 1.5;
}
</style>
</head>
<body>
<div class="box">
<h1>⚡ Code Evaluator</h1>
<p class="sub">Upload a source code file, project folder, or .zip archive for AI grading.</p>
<label>API Base URL</label>
<input type="text" id="apiUrl" placeholder="https://dhyandevp-codeevaluator.hf.space" />
<label>X-API-Key</label>
<input type="password" id="apiKey" placeholder="ek-..." />
<label>Question / Objective (optional)</label>
<textarea id="questionObjective" placeholder="Describe what the submitted code should accomplish…"></textarea>
<label>Upload</label>
<div class="file-area" id="dropZone">
<input type="file" id="fileInput" multiple accept=".py,.js,.ts,.jsx,.tsx,.c,.cpp,.h,.java,.go,.rs,.rb,.php,.swift,.cs,.html,.css,.scss,.json,.yaml,.yml,.toml,.xml,.md,.txt,.sql,.sh,.vue,.svelte,.zip,.tar.gz,.tgz" style="display:none" />
<input type="file" id="folderInput" webkitdirectory directory multiple style="display:none" />
<span class="drop-icon">📂</span>
<span class="drop-label">Drop files, a folder, or an archive here</span>
<div class="pick-links">
<span class="pick-link" id="pickFiles">Choose file(s)</span>
<span class="pick-link" id="pickFolder">Choose folder</span>
</div>
</div>
<div id="filePreview"></div>
<button id="submitBtn" onclick="submit()" disabled>Submit for Evaluation</button>
<div id="status"></div>
</div>
<script>
/* ──────────────────────────────────────────────────────────────────
* State
* ────────────────────────────────────────────────────────────────── */
let selectedFiles = []; // Array of File objects (with optional ._relativePath)
let pollTimer = null;
/* ──────────────────────────────────────────────────────────────────
* Initialisation
* ────────────────────────────────────────────────────────────────── */
window.onload = () => {
const origin = location.origin;
document.getElementById('apiUrl').value =
(origin.startsWith('http') && !origin.includes('localhost') && !origin.includes('file:'))
? origin
: 'https://dhyandevp-codeevaluator.hf.space';
};
/* ──────────────────────────────────────────────────────────────────
* Pick links — trigger the hidden file inputs
* ────────────────────────────────────────────────────────────────── */
document.getElementById('pickFiles').addEventListener('click', e => {
e.stopPropagation();
document.getElementById('fileInput').click();
});
document.getElementById('pickFolder').addEventListener('click', e => {
e.stopPropagation();
document.getElementById('folderInput').click();
});
document.getElementById('fileInput').addEventListener('change', function () { onFilesSelected(this); });
document.getElementById('folderInput').addEventListener('change', function () { onFilesSelected(this); });
/* ──────────────────────────────────────────────────────────────────
* File selection handler (works for both inputs)
* ────────────────────────────────────────────────────────────────── */
function onFilesSelected(input) {
const files = Array.from(input.files || []);
if (!files.length) return;
selectedFiles = files.map(f => {
f._relativePath = f.webkitRelativePath || f.name;
return f;
});
renderPreview();
document.getElementById('submitBtn').disabled = false;
}
/* ──────────────────────────────────────────────────────────────────
* Auto-detect upload mode from selected files
* ────────────────────────────────────────────────────────────────── */
function _detectMode() {
if (selectedFiles.length === 1) {
const name = (selectedFiles[0].name || '').toLowerCase();
if (name.endsWith('.zip') || name.endsWith('.tar.gz') || name.endsWith('.tgz')) {
return 'archive';
}
return 'single';
}
return 'multi';
}
/* ──────────────────────────────────────────────────────────────────
* Drag & drop — auto-detects folders vs files
* ────────────────────────────────────────────────────────────────── */
const dz = document.getElementById('dropZone');
dz.addEventListener('dragover', e => {
e.preventDefault();
e.stopPropagation();
dz.classList.add('drag');
});
dz.addEventListener('dragleave', () => dz.classList.remove('drag'));
dz.addEventListener('drop', async e => {
e.preventDefault();
e.stopPropagation();
dz.classList.remove('drag');
// Try recursive directory read first (works for dropped folders)
const items = Array.from(e.dataTransfer.items || []);
const allFiles = [];
for (const item of items) {
const entry = item.webkitGetAsEntry ? item.webkitGetAsEntry() : null;
if (entry && entry.isDirectory) {
const files = await readEntryRecursive(entry, '');
allFiles.push(...files);
} else if (entry && entry.isFile) {
const file = await new Promise((resolve, reject) => entry.file(resolve, reject));
file._relativePath = file.name;
allFiles.push(file);
}
}
// Fallback to dataTransfer.files if webkitGetAsEntry didn't work
if (!allFiles.length) {
const dropped = Array.from(e.dataTransfer.files || []);
dropped.forEach(f => { f._relativePath = f.name; });
allFiles.push(...dropped);
}
if (allFiles.length) {
selectedFiles = allFiles;
renderPreview();
document.getElementById('submitBtn').disabled = false;
}
});
/**
* Recursively read a dropped directory entry using the FileSystem API.
*/
async function readEntryRecursive(entry, basePath) {
if (entry.isFile) {
const file = await new Promise((resolve, reject) => entry.file(resolve, reject));
file._relativePath = basePath + file.name;
return [file];
}
if (entry.isDirectory) {
const reader = entry.createReader();
// readEntries can return partial results, so loop until empty
let allEntries = [];
let batch;
do {
batch = await new Promise((resolve, reject) => reader.readEntries(resolve, reject));
allEntries.push(...batch);
} while (batch.length > 0);
const files = [];
for (const child of allEntries) {
const childFiles = await readEntryRecursive(child, basePath + entry.name + '/');
files.push(...childFiles);
}
return files;
}
return [];
}
/* ──────────────────────────────────────────────────────────────────
* File preview panel
* ────────────────────────────────────────────────────────────────── */
function renderPreview() {
const el = document.getElementById('filePreview');
if (!selectedFiles.length) {
el.style.display = 'none';
return;
}
let totalBytes = 0;
const lines = selectedFiles.map(f => {
totalBytes += f.size;
const name = f._relativePath || f.webkitRelativePath || f.name;
return `<div class="preview-file"> ${esc(name)} <span class="fsize">${formatSize(f.size)}</span></div>`;
});
el.innerHTML =
`<div class="preview-header">Selected Files (${selectedFiles.length}):</div>` +
lines.join('') +
`<div class="preview-total">Total: ${formatSize(totalBytes)}</div>`;
el.style.display = 'block';
}
function formatSize(bytes) {
if (bytes < 1024) return bytes + ' B';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
return (bytes / (1024 * 1024)).toFixed(2) + ' MB';
}
/* ──────────────────────────────────────────────────────────────────
* Submission
* ────────────────────────────────────────────────────────────────── */
async function submit() {
const url = document.getElementById('apiUrl').value.trim().replace(/\/$/, '');
const apiKey = document.getElementById('apiKey').value.trim();
const qObj = document.getElementById('questionObjective').value.trim();
if (!url || !apiKey || !selectedFiles.length) return;
if (pollTimer) clearInterval(pollTimer);
document.getElementById('submitBtn').disabled = true;
showStatus('processing', null, null, 'Submitting…');
const form = new FormData();
const mode = _detectMode();
if (mode === 'single') {
// Single source file — use 'file' field for backwards compatibility
form.append('file', selectedFiles[0], selectedFiles[0].name);
} else if (mode === 'archive') {
// Auto-detected .zip / .tar.gz / .tgz
form.append('files', selectedFiles[0], selectedFiles[0].name);
} else {
// Multiple files (folder pick, multi-select, or dropped folder)
for (const f of selectedFiles) {
const relPath = f._relativePath || f.webkitRelativePath || f.name;
form.append('files', f, relPath);
}
}
if (qObj) form.append('question_objective', qObj);
try {
const res = await fetch(`${url}/submit`, { method: 'POST', headers: { 'X-API-Key': apiKey }, body: form });
const data = await res.json();
if (!res.ok) {
if (res.status === 413) {
showError('⚠️ Payload Too Large — The uploaded content exceeds the server\'s size limit. ' +
'For .zip archives, the total uncompressed source code must be under 1 MB. ' +
'Try removing non-essential files (images, binaries, dependencies) and re-upload.');
} else {
showError(`Error ${res.status}: ${data.detail}`);
}
return;
}
const jobId = data.job_id;
showStatus('processing', jobId, null, 'Evaluating your code…');
pollTimer = setInterval(() => poll(url, apiKey, jobId), 3000);
} catch (e) {
showError('Cannot reach the API. Check the URL and your internet connection.');
}
}
/* ──────────────────────────────────────────────────────────────────
* Polling
* ────────────────────────────────────────────────────────────────── */
async function poll(url, apiKey, jobId) {
try {
const res = await fetch(`${url}/report/${jobId}`, { headers: { 'X-API-Key': apiKey } });
const data = await res.json();
if (data.status === 'Processing') return;
clearInterval(pollTimer);
document.getElementById('submitBtn').disabled = false;
if (data.status === 'Completed') {
showStatus('completed', jobId, data.report, null);
} else {
showError(`Failed: ${data.error?.error_message || 'Unknown error'}`);
}
} catch (_) { /* keep polling */ }
}
/* ──────────────────────────────────────────────────────────────────
* Result rendering
* ────────────────────────────────────────────────────────────────── */
function showStatus(state, jobId, report, message) {
const el = document.getElementById('status');
el.style.display = 'block';
const tagClass = { processing: 'tag-processing', completed: 'tag-completed', failed: 'tag-failed' }[state];
const tagLabel = { processing: '⏳ Processing', completed: '✅ Completed', failed: '❌ Failed' }[state];
let html = `<span class="tag ${tagClass}">${tagLabel}</span>`;
if (state === 'processing') {
html += `<br/><span class="spinner"></span>${message || 'Working…'}`;
}
if (state === 'completed' && report) {
const r = report;
/* ── Language badge ────────────────────────────────────────── */
html += `<div class="meta-row">`;
html += `<span class="lang-badge">${esc(r.language || 'Unknown')}</span>`;
html += `</div>`;
/* ── Score ─────────────────────────────────────────────────── */
html += `<div class="score-row">
<div class="score-block">
<div class="score-num">${r.score}</div>
<div class="score-lbl">Score / 100</div>
</div>
</div>`;
/* ── Code Quality ──────────────────────────────────────────── */
if (r.code_quality) {
html += `<div class="section-title">Code Quality</div>`;
html += `<div class="quality-block">${esc(r.code_quality)}</div>`;
}
/* ── Questions ─────────────────────────────────────────────── */
if (r.questions && r.questions.length > 0) {
html += `<div class="section-title">Questions (${r.questions.length})</div>`;
r.questions.forEach(q => {
const badgeClass = q.is_correct ? 'q-badge-pass' : 'q-badge-fail';
const badgeText = q.is_correct ? '✓ PASS' : '✗ FAIL';
html += `<div class="q-card">
<div class="q-header">
<span class="q-badge ${badgeClass}">${badgeText}</span>
<span class="q-text">${esc(q.question)}</span>
</div>
<div class="q-feedback">${esc(q.feedback)}</div>
</div>`;
});
}
}
if (jobId) html += `<div class="job-id">job: ${jobId}</div>`;
el.innerHTML = html;
}
function showError(msg) {
clearInterval(pollTimer);
document.getElementById('submitBtn').disabled = false;
const el = document.getElementById('status');
el.style.display = 'block';
el.innerHTML = `<span class="tag tag-failed">❌ Failed</span><br/>${esc(msg)}`;
}
function esc(s) {
return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}
</script>
</body>
</html>