698 lines
20 KiB
HTML
698 lines
20 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 — Spotlight Review</title>
|
|
<style>
|
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
|
|
:root {
|
|
--green: #00FF66;
|
|
--red: #FF3333;
|
|
--yellow: #FFD700;
|
|
--bg: #111;
|
|
--card: #1a1a2e;
|
|
--text: #e0e0e0;
|
|
--muted: #888;
|
|
--radius: 14px;
|
|
}
|
|
|
|
body {
|
|
background: var(--bg);
|
|
color: var(--text);
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
display: flex;
|
|
min-height: 100vh;
|
|
overflow: hidden;
|
|
}
|
|
|
|
/* ── Main Panel ── */
|
|
.main-panel {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
padding: 16px;
|
|
min-width: 0;
|
|
}
|
|
|
|
/* ── Mode Bar ── */
|
|
.mode-bar {
|
|
display: flex;
|
|
gap: 8px;
|
|
margin-bottom: 12px;
|
|
flex-shrink: 0;
|
|
}
|
|
.mode-btn {
|
|
padding: 8px 18px;
|
|
border-radius: 10px;
|
|
font-size: 13px;
|
|
font-weight: 700;
|
|
cursor: pointer;
|
|
border: 2px solid #333;
|
|
background: var(--card);
|
|
color: var(--text);
|
|
transition: all 0.25s;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
}
|
|
.mode-btn:hover { border-color: #555; }
|
|
.mode-btn.active.good-mode {
|
|
border-color: var(--green);
|
|
background: rgba(0, 255, 102, 0.1);
|
|
color: var(--green);
|
|
box-shadow: 0 0 16px rgba(0, 255, 102, 0.15);
|
|
}
|
|
.mode-btn.active.bad-mode {
|
|
border-color: var(--red);
|
|
background: rgba(255, 51, 51, 0.1);
|
|
color: var(--red);
|
|
box-shadow: 0 0 16px rgba(255, 51, 51, 0.15);
|
|
}
|
|
.mode-shortcut {
|
|
font-size: 10px;
|
|
padding: 2px 6px;
|
|
border-radius: 4px;
|
|
background: rgba(255,255,255,0.08);
|
|
color: var(--muted);
|
|
font-weight: 600;
|
|
}
|
|
|
|
/* ── Content Area ── */
|
|
.content-area {
|
|
position: relative;
|
|
flex: 1;
|
|
background: #0d0d1a;
|
|
border-radius: var(--radius);
|
|
border: 1px solid #2a2a3e;
|
|
overflow: hidden;
|
|
cursor: crosshair;
|
|
}
|
|
|
|
.code-content {
|
|
padding: 20px;
|
|
font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace;
|
|
font-size: 13px;
|
|
line-height: 1.7;
|
|
color: #d4d4d4;
|
|
white-space: pre-wrap;
|
|
word-break: break-word;
|
|
min-height: 100%;
|
|
position: relative;
|
|
z-index: 1;
|
|
transition: mask-image 0.05s;
|
|
-webkit-mask-image: radial-gradient(circle 120px at var(--mx, 50%) var(--my, 50%), rgba(0,0,0,1) 30%, rgba(0,0,0,0.12) 100%);
|
|
mask-image: radial-gradient(circle 120px at var(--mx, 50%) var(--my, 50%), rgba(0,0,0,1) 30%, rgba(0,0,0,0.12) 100%);
|
|
}
|
|
.code-content.no-spotlight {
|
|
-webkit-mask-image: none;
|
|
mask-image: none;
|
|
}
|
|
|
|
.line-numbers {
|
|
position: absolute;
|
|
top: 20px;
|
|
left: 0;
|
|
width: 40px;
|
|
text-align: right;
|
|
padding-right: 8px;
|
|
font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace;
|
|
font-size: 13px;
|
|
line-height: 1.7;
|
|
color: #444;
|
|
z-index: 0;
|
|
pointer-events: none;
|
|
user-select: none;
|
|
}
|
|
|
|
.code-content { padding-left: 52px; }
|
|
|
|
/* ── Annotation Markers ── */
|
|
.annotation-markers {
|
|
position: absolute;
|
|
inset: 0;
|
|
pointer-events: none;
|
|
z-index: 5;
|
|
}
|
|
.annotation-marker {
|
|
position: absolute;
|
|
width: 26px;
|
|
height: 26px;
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: white;
|
|
font-size: 12px;
|
|
font-weight: 800;
|
|
cursor: pointer;
|
|
pointer-events: auto;
|
|
z-index: 10;
|
|
transform: translate(-50%, -50%) scale(0);
|
|
animation: marker-pop 0.3s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.4);
|
|
}
|
|
.annotation-marker.good { background: var(--green); }
|
|
.annotation-marker.bad { background: var(--red); }
|
|
|
|
@keyframes marker-pop {
|
|
to { transform: translate(-50%, -50%) scale(1); }
|
|
}
|
|
|
|
/* ── Comment Popup ── */
|
|
.comment-popup {
|
|
position: absolute;
|
|
z-index: 20;
|
|
background: var(--card);
|
|
border: 1px solid #333;
|
|
border-radius: 10px;
|
|
padding: 10px;
|
|
display: none;
|
|
box-shadow: 0 8px 24px rgba(0,0,0,0.5);
|
|
width: 240px;
|
|
}
|
|
.comment-popup.visible { display: block; }
|
|
.comment-popup input {
|
|
width: 100%;
|
|
background: #0d0d1a;
|
|
border: 1px solid #333;
|
|
border-radius: 6px;
|
|
color: var(--text);
|
|
font-family: inherit;
|
|
font-size: 13px;
|
|
padding: 8px 10px;
|
|
outline: none;
|
|
}
|
|
.comment-popup input:focus { border-color: #555; }
|
|
.comment-popup .popup-hint {
|
|
font-size: 10px;
|
|
color: #555;
|
|
margin-top: 6px;
|
|
text-align: center;
|
|
}
|
|
|
|
/* ── Sidebar ── */
|
|
.sidebar {
|
|
width: 260px;
|
|
background: var(--card);
|
|
border-left: 1px solid #2a2a3e;
|
|
display: flex;
|
|
flex-direction: column;
|
|
flex-shrink: 0;
|
|
}
|
|
.sidebar-header {
|
|
padding: 16px;
|
|
border-bottom: 1px solid #2a2a3e;
|
|
}
|
|
.sidebar-header h4 {
|
|
font-size: 14px;
|
|
font-weight: 700;
|
|
margin-bottom: 4px;
|
|
}
|
|
.sidebar-header .annotation-count {
|
|
font-size: 12px;
|
|
color: var(--muted);
|
|
}
|
|
|
|
.annotation-list {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
padding: 8px;
|
|
list-style: none;
|
|
}
|
|
.annotation-item {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: 10px;
|
|
padding: 10px;
|
|
border-radius: 8px;
|
|
margin-bottom: 4px;
|
|
transition: background 0.2s;
|
|
cursor: pointer;
|
|
animation: slide-in 0.3s ease-out;
|
|
}
|
|
.annotation-item:hover { background: rgba(255,255,255,0.04); }
|
|
@keyframes slide-in {
|
|
from { opacity: 0; transform: translateX(20px); }
|
|
to { opacity: 1; transform: translateX(0); }
|
|
}
|
|
.annotation-num {
|
|
width: 22px;
|
|
height: 22px;
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: white;
|
|
font-size: 11px;
|
|
font-weight: 800;
|
|
flex-shrink: 0;
|
|
}
|
|
.annotation-num.good { background: var(--green); }
|
|
.annotation-num.bad { background: var(--red); }
|
|
.annotation-detail {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
.annotation-detail .line-ref {
|
|
font-size: 11px;
|
|
color: var(--muted);
|
|
margin-bottom: 2px;
|
|
}
|
|
.annotation-detail .comment-text {
|
|
font-size: 12px;
|
|
color: var(--text);
|
|
word-break: break-word;
|
|
}
|
|
.annotation-item .delete-btn {
|
|
background: none;
|
|
border: none;
|
|
color: #555;
|
|
cursor: pointer;
|
|
font-size: 14px;
|
|
padding: 2px;
|
|
opacity: 0;
|
|
transition: opacity 0.2s;
|
|
}
|
|
.annotation-item:hover .delete-btn { opacity: 1; }
|
|
.annotation-item .delete-btn:hover { color: var(--red); }
|
|
|
|
/* ── Bottom Bar ── */
|
|
.bottom-bar {
|
|
padding: 12px 16px;
|
|
border-top: 1px solid #2a2a3e;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
}
|
|
.verdict-row {
|
|
display: flex;
|
|
gap: 6px;
|
|
}
|
|
.verdict-btn {
|
|
flex: 1;
|
|
padding: 8px;
|
|
border-radius: 8px;
|
|
font-size: 11px;
|
|
font-weight: 700;
|
|
cursor: pointer;
|
|
border: 2px solid #333;
|
|
background: var(--card);
|
|
color: var(--text);
|
|
transition: all 0.2s;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
.verdict-btn:hover { border-color: #555; }
|
|
.verdict-btn.selected.v-pass {
|
|
border-color: var(--green);
|
|
background: rgba(0,255,102,0.1);
|
|
color: var(--green);
|
|
}
|
|
.verdict-btn.selected.v-marginal {
|
|
border-color: var(--yellow);
|
|
background: rgba(255,215,0,0.1);
|
|
color: var(--yellow);
|
|
}
|
|
.verdict-btn.selected.v-fail {
|
|
border-color: var(--red);
|
|
background: rgba(255,51,51,0.1);
|
|
color: var(--red);
|
|
}
|
|
|
|
.done-btn {
|
|
padding: 12px;
|
|
border: none;
|
|
border-radius: 10px;
|
|
font-size: 14px;
|
|
font-weight: 700;
|
|
cursor: pointer;
|
|
background: linear-gradient(135deg, #00cc55, #00aa44);
|
|
color: #fff;
|
|
transition: all 0.25s;
|
|
}
|
|
.done-btn:hover { transform: translateY(-1px); box-shadow: 0 4px 16px rgba(0,204,85,0.3); }
|
|
.done-btn:active { transform: scale(0.98); }
|
|
.done-btn:disabled { opacity: 0.4; pointer-events: none; }
|
|
|
|
/* ── Done Overlay ── */
|
|
.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.4s;
|
|
z-index: 100;
|
|
}
|
|
.done-overlay.visible { opacity: 1; pointer-events: auto; }
|
|
.done-overlay .done-emoji { font-size: 64px; margin-bottom: 12px; }
|
|
.done-overlay .done-text { font-size: 18px; color: var(--muted); }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<div class="main-panel">
|
|
<div class="mode-bar">
|
|
<button class="mode-btn good-mode active" id="modeGood" data-mode="good">
|
|
🟢 Highlight Good <span class="mode-shortcut">G</span>
|
|
</button>
|
|
<button class="mode-btn bad-mode" id="modeBad" data-mode="bad">
|
|
🔴 Highlight Bad <span class="mode-shortcut">B</span>
|
|
</button>
|
|
</div>
|
|
|
|
<div class="content-area" id="contentArea">
|
|
<div class="line-numbers" id="lineNumbers"></div>
|
|
<div class="code-content" id="codeContent"></div>
|
|
<div class="annotation-markers" id="annotationMarkers"></div>
|
|
<div class="comment-popup" id="commentPopup">
|
|
<input type="text" id="commentInput" placeholder="Why? (Enter to save, Esc to skip)" maxlength="120">
|
|
<div class="popup-hint">Enter to save · Esc to skip</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="sidebar">
|
|
<div class="sidebar-header">
|
|
<h4>Annotations</h4>
|
|
<div class="annotation-count" id="annotationCount">0 annotations</div>
|
|
</div>
|
|
<ol class="annotation-list" id="annotationList"></ol>
|
|
<div class="bottom-bar">
|
|
<div class="verdict-row">
|
|
<button class="verdict-btn v-pass" data-verdict="pass">✅ Pass</button>
|
|
<button class="verdict-btn v-marginal" data-verdict="marginal">🟡 Mixed</button>
|
|
<button class="verdict-btn v-fail" data-verdict="fail">🔴 Fail</button>
|
|
</div>
|
|
<button class="done-btn" id="doneBtn">Done Reviewing ✓</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="done-overlay" id="doneOverlay">
|
|
<div class="done-emoji">🔍</div>
|
|
<div class="done-text">Review submitted</div>
|
|
</div>
|
|
|
|
<script>
|
|
(function() {
|
|
'use strict';
|
|
|
|
const ctx = window.__FACTORY_CONTEXT__ || {};
|
|
const modalType = 'spotlight';
|
|
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 });
|
|
|
|
// ── State ──
|
|
let currentMode = 'good';
|
|
let annotations = [];
|
|
let annotationIdCounter = 0;
|
|
let selectedVerdict = null;
|
|
let interactionCount = 0;
|
|
let firstInteractionTime = null;
|
|
let isPlacingAnnotation = false;
|
|
let scanHeatmap = {};
|
|
let lastHeatmapUpdate = 0;
|
|
|
|
function trackInteraction() {
|
|
interactionCount++;
|
|
if (!firstInteractionTime) firstInteractionTime = Date.now();
|
|
}
|
|
|
|
// ── Load content ──
|
|
const sampleContent = ctx.content || ctx.deliverableContent || [
|
|
'import express from "express";',
|
|
'import { validateToken } from "./auth";',
|
|
'import { logger } from "./utils/logger";',
|
|
'',
|
|
'const router = express.Router();',
|
|
'',
|
|
'// GET /users - Returns all users',
|
|
'router.get("/users", async (req, res) => {',
|
|
' try {',
|
|
' const users = await db.query("SELECT * FROM users");',
|
|
' res.json({ data: users, count: users.length });',
|
|
' } catch (err) {',
|
|
' console.log(err); // TODO: proper logging',
|
|
' res.status(500).json({ error: "Something went wrong" });',
|
|
' }',
|
|
'});',
|
|
'',
|
|
'// POST /users - Create a new user',
|
|
'router.post("/users", validateToken, async (req, res) => {',
|
|
' const { name, email, role } = req.body;',
|
|
' if (!name || !email) {',
|
|
' return res.status(400).json({ error: "Missing fields" });',
|
|
' }',
|
|
' const user = await db.query(',
|
|
' "INSERT INTO users (name, email, role) VALUES ($1, $2, $3) RETURNING *",',
|
|
' [name, email, role || "viewer"]',
|
|
' );',
|
|
' res.status(201).json({ data: user.rows[0] });',
|
|
'});',
|
|
'',
|
|
'// DELETE /users/:id',
|
|
'router.delete("/users/:id", validateToken, async (req, res) => {',
|
|
' await db.query("DELETE FROM users WHERE id = $1", [req.params.id]);',
|
|
' res.status(204).send();',
|
|
'});',
|
|
'',
|
|
'export default router;',
|
|
].join('\n');
|
|
|
|
const lines = typeof sampleContent === 'string' ? sampleContent.split('\n') : sampleContent;
|
|
const codeContent = document.getElementById('codeContent');
|
|
const lineNumbers = document.getElementById('lineNumbers');
|
|
|
|
codeContent.textContent = Array.isArray(lines) ? lines.join('\n') : lines;
|
|
|
|
// Generate line numbers
|
|
const lineCount = (Array.isArray(lines) ? lines : lines.split('\n')).length;
|
|
lineNumbers.innerHTML = Array.from({ length: lineCount }, (_, i) => `<div>${i + 1}</div>`).join('');
|
|
|
|
// ── Spotlight ──
|
|
const contentArea = document.getElementById('contentArea');
|
|
|
|
contentArea.addEventListener('mousemove', (e) => {
|
|
if (isPlacingAnnotation) return;
|
|
const rect = contentArea.getBoundingClientRect();
|
|
const x = e.clientX - rect.left;
|
|
const y = e.clientY - rect.top;
|
|
codeContent.style.setProperty('--mx', x + 'px');
|
|
codeContent.style.setProperty('--my', y + 'px');
|
|
|
|
// Track heatmap (sample every 200ms)
|
|
const now = Date.now();
|
|
if (now - lastHeatmapUpdate > 200) {
|
|
const lineEst = Math.max(1, Math.floor(y / (13 * 1.7)) + 1);
|
|
const key = `line_${lineEst}`;
|
|
scanHeatmap[key] = (scanHeatmap[key] || 0) + 1;
|
|
lastHeatmapUpdate = now;
|
|
}
|
|
});
|
|
|
|
// ── Mode switching ──
|
|
const modeGood = document.getElementById('modeGood');
|
|
const modeBad = document.getElementById('modeBad');
|
|
|
|
function setMode(mode) {
|
|
currentMode = mode;
|
|
modeGood.classList.toggle('active', mode === 'good');
|
|
modeBad.classList.toggle('active', mode === 'bad');
|
|
trackInteraction();
|
|
}
|
|
|
|
modeGood.addEventListener('click', () => setMode('good'));
|
|
modeBad.addEventListener('click', () => setMode('bad'));
|
|
|
|
document.addEventListener('keydown', (e) => {
|
|
if (isPlacingAnnotation) {
|
|
if (e.key === 'Escape') finishAnnotation(null);
|
|
return;
|
|
}
|
|
if (e.key === 'g' || e.key === 'G') setMode('good');
|
|
if (e.key === 'b' || e.key === 'B') setMode('bad');
|
|
});
|
|
|
|
// ── Click to annotate ──
|
|
const annotationMarkers = document.getElementById('annotationMarkers');
|
|
const commentPopup = document.getElementById('commentPopup');
|
|
const commentInput = document.getElementById('commentInput');
|
|
let pendingAnnotation = null;
|
|
|
|
contentArea.addEventListener('click', (e) => {
|
|
if (isPlacingAnnotation) return;
|
|
if (e.target.classList.contains('annotation-marker')) return;
|
|
|
|
trackInteraction();
|
|
|
|
const rect = contentArea.getBoundingClientRect();
|
|
const x = e.clientX - rect.left;
|
|
const y = e.clientY - rect.top;
|
|
const lineEst = Math.max(1, Math.floor((y - 20) / (13 * 1.7)) + 1);
|
|
|
|
annotationIdCounter++;
|
|
const id = annotationIdCounter;
|
|
|
|
// Place marker
|
|
const marker = document.createElement('div');
|
|
marker.className = `annotation-marker ${currentMode}`;
|
|
marker.textContent = id;
|
|
marker.style.left = x + 'px';
|
|
marker.style.top = y + 'px';
|
|
marker.dataset.id = id;
|
|
annotationMarkers.appendChild(marker);
|
|
|
|
pendingAnnotation = {
|
|
id,
|
|
type: currentMode,
|
|
line: lineEst,
|
|
x, y,
|
|
marker,
|
|
comment: '',
|
|
};
|
|
|
|
// Show comment popup
|
|
isPlacingAnnotation = true;
|
|
commentPopup.style.left = Math.min(x + 20, contentArea.offsetWidth - 260) + 'px';
|
|
commentPopup.style.top = Math.min(y, contentArea.offsetHeight - 80) + 'px';
|
|
commentPopup.classList.add('visible');
|
|
commentInput.value = '';
|
|
setTimeout(() => commentInput.focus(), 50);
|
|
});
|
|
|
|
commentInput.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Enter') {
|
|
e.preventDefault();
|
|
finishAnnotation(commentInput.value.trim());
|
|
} else if (e.key === 'Escape') {
|
|
finishAnnotation(null);
|
|
}
|
|
});
|
|
|
|
function finishAnnotation(comment) {
|
|
commentPopup.classList.remove('visible');
|
|
isPlacingAnnotation = false;
|
|
|
|
if (pendingAnnotation) {
|
|
pendingAnnotation.comment = comment || '';
|
|
annotations.push(pendingAnnotation);
|
|
renderAnnotationList();
|
|
pendingAnnotation = null;
|
|
}
|
|
}
|
|
|
|
// ── Render annotation list ──
|
|
const annotationList = document.getElementById('annotationList');
|
|
const annotationCountEl = document.getElementById('annotationCount');
|
|
|
|
function renderAnnotationList() {
|
|
const goodCount = annotations.filter(a => a.type === 'good').length;
|
|
const badCount = annotations.filter(a => a.type === 'bad').length;
|
|
annotationCountEl.textContent = `${annotations.length} annotations · ${goodCount} good · ${badCount} bad`;
|
|
|
|
annotationList.innerHTML = '';
|
|
annotations.forEach((a) => {
|
|
const li = document.createElement('li');
|
|
li.className = 'annotation-item';
|
|
li.innerHTML = `
|
|
<div class="annotation-num ${a.type}">${a.id}</div>
|
|
<div class="annotation-detail">
|
|
<div class="line-ref">Line ~${a.line} · ${a.type === 'good' ? '✅ Good' : '❌ Bad'}</div>
|
|
<div class="comment-text">${a.comment || '(no comment)'}</div>
|
|
</div>
|
|
<button class="delete-btn" data-id="${a.id}">✕</button>
|
|
`;
|
|
li.querySelector('.delete-btn').addEventListener('click', (e) => {
|
|
e.stopPropagation();
|
|
removeAnnotation(a.id);
|
|
});
|
|
annotationList.appendChild(li);
|
|
});
|
|
}
|
|
|
|
function removeAnnotation(id) {
|
|
annotations = annotations.filter(a => a.id !== id);
|
|
const marker = annotationMarkers.querySelector(`[data-id="${id}"]`);
|
|
if (marker) marker.remove();
|
|
renderAnnotationList();
|
|
trackInteraction();
|
|
}
|
|
|
|
// ── Verdict buttons ──
|
|
const verdictBtns = document.querySelectorAll('.verdict-btn');
|
|
verdictBtns.forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
trackInteraction();
|
|
selectedVerdict = btn.dataset.verdict;
|
|
verdictBtns.forEach(b => b.classList.remove('selected'));
|
|
btn.classList.add('selected');
|
|
});
|
|
});
|
|
|
|
// ── Done button ──
|
|
document.getElementById('doneBtn').addEventListener('click', () => {
|
|
trackInteraction();
|
|
const responseTimeMs = Date.now() - startTime;
|
|
|
|
const goodAnnotations = annotations.filter(a => a.type === 'good');
|
|
const badAnnotations = annotations.filter(a => a.type === 'bad');
|
|
|
|
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,
|
|
feedback: {
|
|
decision: selectedVerdict || (badAnnotations.length > goodAnnotations.length ? 'fail' : 'pass'),
|
|
annotations: annotations.map(a => ({
|
|
id: a.id,
|
|
type: a.type,
|
|
line: a.line,
|
|
comment: a.comment,
|
|
})),
|
|
summary: {
|
|
total_annotations: annotations.length,
|
|
good_highlights: goodAnnotations.length,
|
|
bad_highlights: badAnnotations.length,
|
|
annotation_density: annotations.length > 0 ?
|
|
(goodAnnotations.length / annotations.length).toFixed(2) : null,
|
|
},
|
|
overall_verdict: selectedVerdict || null,
|
|
},
|
|
meta: {
|
|
timeToFirstInteractionMs: firstInteractionTime ? firstInteractionTime - startTime : null,
|
|
timeToDecisionMs: responseTimeMs,
|
|
totalInteractions: interactionCount,
|
|
scanHeatmap,
|
|
deviceType: window.innerWidth < 768 ? 'mobile' : 'desktop',
|
|
viewportSize: { width: window.innerWidth, height: window.innerHeight },
|
|
responseTimeMs,
|
|
},
|
|
};
|
|
|
|
post(payload);
|
|
|
|
document.getElementById('doneOverlay').classList.add('visible');
|
|
setTimeout(() => {
|
|
post({ type: 'factory_modal_close', reason: 'completed' });
|
|
}, 1200);
|
|
});
|
|
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|