510 lines
14 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GooseFactory — Report Card</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #111;
--paper: #FFF8E7;
--paper-dark: #F5EDDA;
--ink: #2a2a2a;
--ink-muted: #888;
--grade-a: #2255CC;
--grade-b: #22AA55;
--grade-c: #CCAA00;
--grade-d: #DD7700;
--grade-f: #CC2222;
--radius: 6px;
}
body {
background: var(--bg);
font-family: 'Georgia', 'Times New Roman', serif;
display: flex;
justify-content: center;
padding: 20px 12px;
min-height: 100vh;
}
/* ── Report Card Container ── */
.report-card {
background: var(--paper);
border-radius: 4px;
width: 100%;
max-width: 500px;
padding: 0;
box-shadow:
3px 5px 16px rgba(0,0,0,0.45),
0 0 0 1px rgba(0,0,0,0.08);
position: relative;
overflow: hidden;
color: var(--ink);
}
/* ── Header ── */
.rc-header {
background: linear-gradient(135deg, #1a2a4a, #2a3a5a);
color: #fff;
padding: 20px 24px;
display: flex;
align-items: center;
justify-content: space-between;
position: relative;
}
.rc-header::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 3px;
background: linear-gradient(90deg, var(--grade-a), var(--grade-b), var(--grade-c), var(--grade-d), var(--grade-f));
}
.rc-header .school {
font-size: 10px;
text-transform: uppercase;
letter-spacing: 2.5px;
opacity: 0.7;
margin-bottom: 4px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}
.rc-header .student-name {
font-size: 20px;
font-weight: 700;
}
/* ── GPA Circle ── */
.gpa-circle {
width: 64px;
height: 64px;
border-radius: 50%;
border: 3px solid rgba(255,255,255,0.3);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
flex-shrink: 0;
transition: all 0.5s;
}
.gpa-circle .gpa-num {
font-size: 22px;
font-weight: 700;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
line-height: 1;
}
.gpa-circle .gpa-label {
font-size: 8px;
text-transform: uppercase;
letter-spacing: 1px;
opacity: 0.6;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}
.gpa-circle.excellent { border-color: var(--grade-a); }
.gpa-circle.good { border-color: var(--grade-b); }
.gpa-circle.ok { border-color: var(--grade-c); }
.gpa-circle.poor { border-color: var(--grade-d); }
.gpa-circle.failing { border-color: var(--grade-f); }
/* ── Subjects Section ── */
.subjects {
padding: 20px 24px 16px;
}
.subjects-label {
font-size: 10px;
text-transform: uppercase;
letter-spacing: 2px;
color: var(--ink-muted);
margin-bottom: 14px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}
.subject-row {
display: flex;
align-items: center;
padding: 10px 0;
border-bottom: 1px solid #e8dcc8;
gap: 12px;
}
.subject-row:last-child { border-bottom: none; }
.subject-name {
flex: 1;
font-size: 14px;
font-weight: 600;
min-width: 0;
}
.grade-bubbles {
display: flex;
gap: 6px;
flex-shrink: 0;
}
/* ── Grade Bubbles ── */
.bubble {
width: 38px;
height: 38px;
border-radius: 50%;
border: 2.5px solid #bbb;
background: transparent;
font-size: 15px;
font-weight: 700;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
color: #999;
cursor: pointer;
position: relative;
overflow: hidden;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
-webkit-tap-highlight-color: transparent;
}
.bubble::before {
content: '';
position: absolute;
inset: 0;
border-radius: 50%;
background: var(--fill-color);
transform: scale(0);
transition: transform 0.35s cubic-bezier(0.175, 0.885, 0.32, 1.275);
z-index: 0;
}
.bubble span { position: relative; z-index: 1; }
.bubble:hover {
border-color: #888;
transform: scale(1.08);
}
.bubble.filled::before {
transform: scale(1);
}
.bubble.filled {
border-color: var(--fill-color);
color: #fff;
}
.bubble[data-grade="A"] { --fill-color: var(--grade-a); }
.bubble[data-grade="B"] { --fill-color: var(--grade-b); }
.bubble[data-grade="C"] { --fill-color: var(--grade-c); }
.bubble[data-grade="D"] { --fill-color: var(--grade-d); }
.bubble[data-grade="F"] { --fill-color: var(--grade-f); }
/* ── Teacher's Comments ── */
.comments-section {
padding: 0 24px 20px;
}
.comments-label {
font-size: 10px;
text-transform: uppercase;
letter-spacing: 2px;
color: var(--ink-muted);
margin-bottom: 10px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}
.teachers-comments {
width: 100%;
min-height: 80px;
background: transparent;
border: none;
border-bottom: 2px solid #e8dcc8;
font-family: 'Georgia', serif;
font-size: 14px;
color: var(--ink);
line-height: 28px;
padding: 4px 0;
resize: vertical;
outline: none;
background-image: repeating-linear-gradient(
transparent,
transparent 27px,
#d4c8b4 27px,
#d4c8b4 28px
);
}
.teachers-comments::placeholder { color: #bbb; font-style: italic; }
/* ── Submit ── */
.submit-section {
padding: 16px 24px 24px;
background: var(--paper-dark);
display: flex;
align-items: center;
justify-content: space-between;
border-top: 1px solid #e0d5c0;
}
.grade-progress {
font-size: 12px;
color: var(--ink-muted);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}
.submit-btn {
padding: 12px 28px;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 700;
cursor: pointer;
background: linear-gradient(135deg, #2a3a5a, #1a2a4a);
color: #fff;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
transition: all 0.25s;
letter-spacing: 0.3px;
}
.submit-btn:hover { transform: translateY(-2px); box-shadow: 0 4px 16px rgba(0,0,0,0.3); }
.submit-btn:active { transform: scale(0.97); }
.submit-btn:disabled { opacity: 0.35; pointer-events: none; }
/* ── Done ── */
.done-overlay {
position: fixed; inset: 0; background: var(--bg);
display: flex; flex-direction: column; align-items: center; justify-content: center;
opacity: 0; pointer-events: none; transition: opacity 0.5s; z-index: 100;
}
.done-overlay.visible { opacity: 1; pointer-events: auto; }
.done-overlay .done-emoji { font-size: 56px; margin-bottom: 8px; }
.done-overlay .done-text { font-size: 18px; color: #aaa; font-family: -apple-system, sans-serif; }
.done-overlay .done-gpa { font-size: 40px; font-weight: 800; color: #fff; margin-top: 4px; font-family: -apple-system, sans-serif; }
/* ── Grade tooltip ── */
.grade-hint {
position: fixed;
background: #333;
color: #fff;
padding: 4px 10px;
border-radius: 6px;
font-size: 11px;
font-family: -apple-system, sans-serif;
pointer-events: none;
opacity: 0;
transition: opacity 0.15s;
z-index: 50;
white-space: nowrap;
}
.grade-hint.show { opacity: 1; }
</style>
</head>
<body>
<div class="report-card" id="card">
<div class="rc-header">
<div>
<div class="school">GooseFactory Academy</div>
<div class="student-name" id="studentName">MCP Server</div>
</div>
<div class="gpa-circle" id="gpaCircle">
<div class="gpa-num" id="gpaNum"></div>
<div class="gpa-label">GPA</div>
</div>
</div>
<div class="subjects" id="subjects">
<div class="subjects-label">Subject Grades</div>
<!-- rows injected by JS -->
</div>
<div class="comments-section">
<div class="comments-label">Teacher's Comments</div>
<textarea class="teachers-comments" id="comments" placeholder="Add notes about this review…" rows="3"></textarea>
</div>
<div class="submit-section">
<div class="grade-progress" id="gradeProgress">0 / 6 graded</div>
<button class="submit-btn" id="submitBtn" disabled>Sign & Submit ✍️</button>
</div>
</div>
<div class="done-overlay" id="doneOverlay">
<div class="done-emoji">📋</div>
<div class="done-text">Report Card Filed</div>
<div class="done-gpa" id="doneGpa">4.0</div>
</div>
<div class="grade-hint" id="gradeHint"></div>
<script>
(function() {
'use strict';
const ctx = window.__FACTORY_CONTEXT__ || {};
const modalType = 'report-card';
const modalVersion = '1.0.0';
const startTime = Date.now();
function post(msg) { try { window.parent.postMessage(msg, '*'); } catch(e) {} }
post({ type: 'factory_modal_ready', modalType, version: modalVersion });
// ── Config ──
const DEFAULT_DIMENSIONS = [
{ id: 'code_quality', name: 'Code Quality' },
{ id: 'documentation', name: 'Documentation' },
{ id: 'error_handling', name: 'Error Handling' },
{ id: 'performance', name: 'Performance' },
{ id: 'creativity', name: 'Creativity' },
{ id: 'completeness', name: 'Completeness' },
];
const dimensions = (ctx.dimensions && ctx.dimensions.length > 0)
? ctx.dimensions
: DEFAULT_DIMENSIONS;
const GRADES = ['A', 'B', 'C', 'D', 'F'];
const GPA_MAP = { A: 4.0, B: 3.0, C: 2.0, D: 1.0, F: 0.0 };
const GRADE_HINTS = {
A: 'Exceptional',
B: 'Good',
C: 'Acceptable',
D: 'Below Standard',
F: 'Failing',
};
document.getElementById('studentName').textContent = ctx.itemName || ctx.pipelineName || 'MCP Server';
// ── State ──
const grades = {}; // dimId → grade letter
const gradeChanges = []; // track every change
let interactionCount = 0;
let firstInteraction = null;
function track() { interactionCount++; if (!firstInteraction) firstInteraction = Date.now(); }
// ── Build Subject Rows ──
const subjectsEl = document.getElementById('subjects');
dimensions.forEach(dim => {
const row = document.createElement('div');
row.className = 'subject-row';
row.innerHTML = `
<div class="subject-name">${dim.name}</div>
<div class="grade-bubbles" data-dim="${dim.id}">
${GRADES.map(g => `<button class="bubble" data-grade="${g}" data-dim="${dim.id}" aria-label="${g} grade"><span>${g}</span></button>`).join('')}
</div>
`;
subjectsEl.appendChild(row);
});
// ── Grade Selection ──
const hint = document.getElementById('gradeHint');
document.querySelectorAll('.bubble').forEach(bubble => {
bubble.addEventListener('click', () => {
track();
const dimId = bubble.dataset.dim;
const grade = bubble.dataset.grade;
const prev = grades[dimId];
if (prev && prev !== grade) {
gradeChanges.push({ dim: dimId, from: prev, to: grade, at: Date.now() - startTime });
}
grades[dimId] = grade;
// Update visuals
document.querySelectorAll(`.bubble[data-dim="${dimId}"]`).forEach(b => {
b.classList.remove('filled');
if (b.dataset.grade === grade) b.classList.add('filled');
});
updateGPA();
updateProgress();
});
// Tooltip
bubble.addEventListener('mouseenter', (e) => {
hint.textContent = GRADE_HINTS[bubble.dataset.grade];
hint.style.left = e.clientX + 8 + 'px';
hint.style.top = e.clientY - 30 + 'px';
hint.classList.add('show');
});
bubble.addEventListener('mouseleave', () => hint.classList.remove('show'));
});
// ── GPA ──
const gpaNum = document.getElementById('gpaNum');
const gpaCircle = document.getElementById('gpaCircle');
function updateGPA() {
const graded = Object.values(grades);
if (graded.length === 0) { gpaNum.textContent = '—'; return; }
const sum = graded.reduce((s, g) => s + GPA_MAP[g], 0);
const gpa = sum / graded.length;
gpaNum.textContent = gpa.toFixed(1);
gpaCircle.className = 'gpa-circle ' + (
gpa >= 3.7 ? 'excellent' :
gpa >= 2.7 ? 'good' :
gpa >= 1.7 ? 'ok' :
gpa >= 0.7 ? 'poor' : 'failing'
);
}
function updateProgress() {
const filled = Object.keys(grades).length;
const total = dimensions.length;
document.getElementById('gradeProgress').textContent = `${filled} / ${total} graded`;
document.getElementById('submitBtn').disabled = (filled < total);
}
// ── Submit ──
document.getElementById('submitBtn').addEventListener('click', () => {
if (document.getElementById('submitBtn').disabled) return;
track();
const graded = Object.values(grades);
const gpa = graded.reduce((s, g) => s + GPA_MAP[g], 0) / graded.length;
const comments = document.getElementById('comments').value.trim();
const scores = dimensions.map(dim => ({
dimension: dim.id,
score: GPA_MAP[grades[dim.id]] / 4 * 10, // Convert to 1-10 scale
grade: grades[dim.id],
weight: dim.weight || 1,
}));
const payload = {
type: 'factory_modal_response',
modalType,
modalVersion,
pipelineId: ctx.pipelineId || 'unknown',
itemId: ctx.itemId || 'unknown',
sessionId: ctx.sessionId || 'sess_' + Date.now(),
timestamp: new Date().toISOString(),
responseTimeMs: Date.now() - startTime,
feedback: {
scores,
freeText: comments ? { generalNotes: comments } : undefined,
custom: {
grades: { ...grades },
gpa: parseFloat(gpa.toFixed(2)),
gradeChanges,
},
},
meta: {
timeToFirstInteractionMs: firstInteraction ? firstInteraction - startTime : null,
timeToDecisionMs: Date.now() - startTime,
totalInteractions: interactionCount,
fieldsModified: [...Object.keys(grades).map(d => 'grade_' + d), ...(comments ? ['comments'] : [])],
deviceType: window.innerWidth < 768 ? 'mobile' : 'desktop',
viewportSize: { width: window.innerWidth, height: window.innerHeight },
},
};
post(payload);
// Done animation
document.getElementById('doneGpa').textContent = gpa.toFixed(1);
document.getElementById('doneOverlay').classList.add('visible');
setTimeout(() => {
post({ type: 'factory_modal_close', reason: 'completed' });
}, 1400);
});
})();
</script>
</body>
</html>