Pacing (WPM)
${sessionData.wpm} ${pacingFeedback}
Filler Words Detected:
${Object.entries(sessionData.fillerCounts).map(([word, count]) => `
${word}: ${count}
`).join('') || '
None detected. Great job!
'}
`;
document.getElementById('pdfButtonContainer').classList.remove('hidden');
}
function addLiveFeedback(message, type = 'info') {
const container = document.getElementById('liveFeedbackContainer');
document.getElementById('liveFeedbackPlaceholder').classList.add('hidden');
const colors = { info: 'text-cyan-400', warning: 'text-yellow-400', alert: 'text-red-400' };
const timestamp = new Date(liveTimer.seconds * 1000).toISOString().substr(14, 5);
const p = document.createElement('p');
p.className = `feedback-item ${colors[type]}`;
p.innerHTML = `[${timestamp}] ${message}`;
container.appendChild(p);
container.scrollTop = container.scrollHeight;
}
// --- TIMER & CONTROLS ---
function manageTimer(timerObj, action) {
switch(action) {
case 'start':
timerObj.isPaused = false;
timerObj.startBtn.disabled = true;
timerObj.pauseBtn.disabled = false;
timerObj.stopBtn.disabled = false;
timerObj.interval = setInterval(() => {
if (!timerObj.isPaused) {
timerObj.seconds++;
timerObj.display.textContent = new Date(timerObj.seconds * 1000).toISOString().substr(11, 8);
if (timerObj === liveTimer) runLiveAssistantLogic();
}
}, 1000);
break;
case 'pause':
timerObj.isPaused = !timerObj.isPaused;
timerObj.pauseBtn.textContent = timerObj.isPaused ? 'Resume' : 'Pause';
break;
case 'stop':
clearInterval(timerObj.interval);
timerObj.isPaused = true;
timerObj.startBtn.disabled = false;
timerObj.pauseBtn.disabled = true;
timerObj.stopBtn.disabled = true;
timerObj.pauseBtn.textContent = 'Pause';
if (timerObj === timer) analyzeSession();
timerObj.seconds = 0; // Reset for next time
break;
}
}
function runLiveAssistantLogic() {
// Simulate filler word detection
if (Math.random() < 0.2 && fillerWords.length > 0) { // 20% chance each second
const word = fillerWords[Math.floor(Math.random() * fillerWords.length)];
addLiveFeedback(`Filler word detected: "${word}"`, 'alert');
}
// Simulate pacing feedback every 10 seconds
if (liveTimer.seconds > 0 && liveTimer.seconds % 10 === 0) {
const pacingRoll = Math.random();
if (pacingRoll < 0.33) addLiveFeedback('Pacing is a bit fast.', 'warning');
else if (pacingRoll < 0.66) addLiveFeedback('Pacing is a bit slow.', 'warning');
else addLiveFeedback('Pacing is good.', 'info');
}
}
function analyzeSession() {
const speechText = document.getElementById('speechText').value;
const wordCount = speechText.trim().split(/\s+/).filter(Boolean).length;
const minutes = timer.seconds / 60;
const wpm = minutes > 0 ? Math.round(wordCount / minutes) : 0;
// Simulate finding filler words based on duration
const fillerCounts = {};
if(fillerWords.length > 0) {
const totalFillers = Math.floor(timer.seconds / 10); // 1 filler every 10 seconds on average
for (let i = 0; i < totalFillers; i++) {
const word = fillerWords[Math.floor(Math.random() * fillerWords.length)];
fillerCounts[word] = (fillerCounts[word] || 0) + 1;
}
}
sessionData = {
title: document.getElementById('speechTitle').value || 'Untitled Speech',
duration: timer.display.textContent,
wpm: wpm,
fillerCounts: fillerCounts
};
displayAnalysis();
}
// --- EVENT HANDLERS & LOGIC ---
window.changeTab = (tabName) => {
currentTab = tabName;
Object.values(tabElements).forEach(el => el.classList.add('hidden'));
Object.values(tabButtons).forEach(btn => btn.classList.remove('active'));
tabElements[tabName].classList.remove('hidden');
tabButtons[tabName].classList.add('active');
updateNavButtons();
};
window.navigateTabs = (direction) => {
const currentIndex = tabs.indexOf(currentTab);
const nextIndex = direction === 'next' ? (currentIndex + 1) : (currentIndex - 1);
if (nextIndex >= 0 && nextIndex < tabs.length) changeTab(tabs[nextIndex]);
};
const updateNavButtons = () => {
const currentIndex = tabs.indexOf(currentTab);
prevButton.classList.toggle('invisible', currentIndex === 0);
nextButton.classList.toggle('invisible', currentIndex === tabs.length - 1);
};
function setupEventListeners() {
addWordForm.addEventListener('submit', (e) => {
e.preventDefault();
const input = document.getElementById('newWord');
const newWord = input.value.trim().toLowerCase();
if (newWord && !fillerWords.includes(newWord)) {
fillerWords.push(newWord);
renderWordList();
input.value = '';
}
});
timer.startBtn.addEventListener('click', () => manageTimer(timer, 'start'));
timer.pauseBtn.addEventListener('click', () => manageTimer(timer, 'pause'));
timer.stopBtn.addEventListener('click', () => manageTimer(timer, 'stop'));
liveTimer.startBtn.addEventListener('click', () => manageTimer(liveTimer, 'start'));
liveTimer.pauseBtn.addEventListener('click', () => manageTimer(liveTimer, 'pause'));
liveTimer.stopBtn.addEventListener('click', () => manageTimer(liveTimer, 'stop'));
}
window.deleteWord = (wordToDelete) => {
fillerWords = fillerWords.filter(w => w !== wordToDelete);
renderWordList();
};
// --- PDF DOWNLOAD ---
window.downloadPDF = async function() {
if (!sessionData) return;
const { jsPDF } = window.jspdf;
const doc = new jsPDF('p', 'pt', 'a4');
const canvas = document.createElement('canvas');
canvas.width = 400; canvas.height = 200;
if (Object.keys(sessionData.fillerCounts).length > 0) {
new Chart(canvas.getContext('2d'), {
type: 'bar',
data: {
labels: Object.keys(sessionData.fillerCounts),
datasets: [{ label: 'Count', data: Object.values(sessionData.fillerCounts), backgroundColor: '#EF4444' }]
},
options: { responsive: false, animation: false, plugins: { legend: { display: false } } }
});
}
await new Promise(resolve => setTimeout(resolve, 500));
const chartImage = canvas.toDataURL('image/png');
const pageWidth = doc.internal.pageSize.getWidth();
const pageHeight = doc.internal.pageSize.getHeight();
doc.setFillColor(41, 128, 185);
doc.rect(0, 0, pageWidth, 60, 'F');
doc.setFontSize(20); doc.setTextColor(255); doc.setFont('helvetica', 'bold');
doc.text('Public Speaking Performance Report', 30, 38);
doc.setFontSize(12); doc.setTextColor(50);
doc.text(`Speech Title: ${sessionData.title}`, 30, 90);
doc.text(`Practice Duration: ${sessionData.duration}`, 30, 105);
doc.autoTable({
startY: 125,
head: [['Metric', 'Result', 'Feedback']],
body: [
['Pacing', `${sessionData.wpm} WPM`, sessionData.wpm > 160 ? 'Too Fast' : (sessionData.wpm < 120 ? 'Too Slow' : 'Good Pace')],
['Filler Words', `${Object.values(sessionData.fillerCounts).reduce((a, b) => a + b, 0)} total`, 'Aim to reduce for clarity']
],
theme: 'grid',
headStyles: { fillColor: [41, 128, 185] },
});
let finalY = doc.lastAutoTable.finalY;
if (Object.keys(sessionData.fillerCounts).length > 0) {
doc.setFontSize(14); doc.text('Filler Word Distribution', 30, finalY + 30);
doc.addImage(chartImage, 'PNG', 30, finalY + 40, 400, 200);
}
const pageCount = doc.internal.getNumberOfPages();
for(let i = 1; i <= pageCount; i++) {
doc.setPage(i);
doc.setFontSize(8); doc.setTextColor(150);
doc.text(`Page ${i} of ${pageCount}`, pageWidth / 2, pageHeight - 20, { align: 'center' });
}
doc.save('Speaking_Performance_Report.pdf');
};
// --- INITIALIZE ---
initializeApp();
});