Notes: ${escapeHTML(data.setup.notes)}
2. Summary Statistics
${stats ? `
${stats.count}
Total Trials
${stats.mean}
Mean RT (ms)
${stats.median}
Median RT (ms)
${stats.stdDev}
Std. Dev. (ms)
` : `
No valid trial data logged for statistics.
`}
3. Raw Data Log
| Trial # |
Stimulus Type |
RT (ms) |
Notes |
${data.log.length > 0 ? data.log.map((t, index) => `
| ${index + 1} |
${escapeHTML(t.stimulus)} |
${t.rt} |
${escapeHTML(t.notes)} |
`).join('') : '| No trial data logged. |
'}
`;
};
const downloadTxt = () => {
const data = getReportData();
const stats = data.stats;
let content = `REACTION TIME TEST LOG\n`;
content += `========================================\n\n`;
content += "1. EXPERIMENT SETUP\n";
content += "--------------------\n";
content += `Title: ${data.setup.title}\n`;
content += `Subject ID: ${data.setup.subjectId}\n`;
content += `Group: ${data.setup.group}\n`;
content += `Date: ${data.setup.date}\n`;
content += `Notes: ${data.setup.notes}\n\n`;
if (stats) {
content += "2. SUMMARY STATISTICS\n";
content += "--------------------\n";
content += `Total Trials: ${stats.count}\n`;
content += `Mean RT (ms): ${stats.mean}\n`;
content += `Median RT (ms): ${stats.median}\n`;
content += `Std. Dev. (ms): ${stats.stdDev}\n`;
content += `Min RT (ms): ${stats.min}\n`;
content += `Max RT (ms): ${stats.max}\n\n`;
} else {
content += "2. SUMMARY STATISTICS\n--------------------\nNo valid trial data logged.\n\n";
}
content += "3. RAW DATA LOG\n";
content += "--------------------\n";
content += "Trial # | Stimulus Type | RT (ms) | Notes\n";
content += "--------------------------------------------------------\n";
data.log.forEach((t, index) => {
content += `${(index + 1).toString().padEnd(7)} | ${t.stimulus.padEnd(13)} | ${t.rt.toString().padEnd(7)} | ${t.notes}\n`;
});
const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = `RT_Log_${data.setup.subjectId}.txt`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(a.href);
};
const downloadPDF = () => {
if (typeof window.jspdf === 'undefined' || typeof window.jspdf.jsPDF === 'undefined') {
alert('Error: jsPDF library not loaded.');
return;
}
const { jsPDF } = window.jspdf;
const doc = new jsPDF('p', 'mm', 'a4');
const data = getReportData();
const stats = data.stats;
const margin = 15;
let yPos = 20;
const pageWidth = doc.internal.pageSize.getWidth();
// 1. Title
doc.setFontSize(18);
doc.setFont(undefined, 'bold');
doc.setTextColor(44, 62, 80);
doc.text(`Reaction Time Test Report`, pageWidth / 2, yPos, { align: 'center' });
yPos += 10;
// 2. Setup Section
doc.setFontSize(12);
doc.setTextColor(230, 126, 34); // Orange
doc.text("1. Experiment Setup", margin, yPos);
yPos += 8;
doc.setFontSize(10);
doc.setFont(undefined, 'normal');
doc.setTextColor(52, 73, 94);
const setupText = `Title: ${data.setup.title}\nSubject ID: ${data.setup.subjectId}\nGroup: ${data.setup.group}\nDate: ${data.setup.date}\nNotes: ${data.setup.notes}`;
const splitSetup = doc.splitTextToSize(setupText, pageWidth - margin * 2);
doc.text(splitSetup, margin, yPos);
yPos += (splitSetup.length * 5) + 5;
// 3. Stats Section
if (stats) {
doc.setFontSize(12);
doc.setTextColor(230, 126, 34);
doc.text("2. Summary Statistics", margin, yPos);
yPos += 8;
const statsArray = [
['Total Trials', stats.count],
['Mean RT (ms)', stats.mean],
['Median RT (ms)', stats.median],
['Std. Dev. (ms)', stats.stdDev],
['Min RT (ms)', stats.min],
['Max RT (ms)', stats.max]
];
doc.autoTable({
startY: yPos,
head: [['Statistic', 'Value (ms)']],
body: statsArray,
theme: 'grid',
styles: { fontSize: 10, cellPadding: 3, textColor: [52, 73, 94] },
headStyles: { fillColor: [243, 156, 18], textColor: [255, 255, 255] },
margin: { left: margin, right: margin }
});
yPos = doc.autoTable.previous.finalY + 10;
}
// 4. Raw Data Log
doc.setFontSize(12);
doc.setTextColor(230, 126, 34);
doc.text("3. Raw Data Log", margin, yPos);
yPos += 8;
const logHead = [['Trial #', 'Stimulus Type', 'RT (ms)', 'Notes']];
const logBody = data.log.map((t, index) => [
index + 1,
t.stimulus,
t.rt,
t.notes
]);
doc.autoTable({
startY: yPos,
head: logHead,
body: logBody,
theme: 'grid',
styles: { fontSize: 10, cellPadding: 3, textColor: [52, 73, 94] },
headStyles: { fillColor: [200, 200, 200], textColor: [52, 73, 94] },
margin: { left: margin, right: margin }
});
doc.save(`RT_Report_${data.setup.subjectId}.pdf`);
};
// --- Event Listeners ---
// Tab Buttons
tabButtons.forEach((btn, index) => {
btn.addEventListener('click', () => showTab(index + 1));
});
// Next/Prev Navigation
nextBtn.addEventListener('click', () => showTab(currentTab + 1));
prevBtn.addEventListener('click', () => showTab(currentTab - 1));
// Tab 2 Actions (Add, Remove, Edit)
addTrialBtn.addEventListener('click', () => addTrial());
logTbody.addEventListener('click', (e) => {
if (e.target.dataset.removeId) {
removeTrial(parseInt(e.target.dataset.removeId));
}
});
logTbody.addEventListener('blur', (e) => {
if (e.target.tagName === 'TD' && e.target.isContentEditable) {
const id = parseInt(e.target.dataset.id);
const field = e.target.dataset.field;
updateTrial(id, field, e.target.textContent);
}
}, true);
// Tab 4 Actions
downloadPdfBtn.addEventListener('click', downloadPDF);
downloadTxtBtn.addEventListener('click', downloadTxt);
// --- Initialization ---
inputs.date.value = new Date().toISOString().substring(0, 10);
inputs.title.value = "Auditory vs. Visual Reaction Time Study";
inputs.subjectId.value = "S-US-01";
inputs.group.value = "Control Group";
renderLogTable();
showTab(1); // Set initial state
});