document.addEventListener('DOMContentLoaded', () => {
const data = window.algorithmData || [];
const report = window.llmReport || {};
const emojiFreq = window.emojiFreq || {};
const initiatorRatio = window.initiatorRatio || {};
const powerDynamics = window.powerDynamics || {};
const affectionFriction = window.affectionFriction || {};
const supportGap = window.supportGap || {};
const mirroring = window.mirroring || {};
const topicMix = window.topicMix || {};
// Scroll Progress Bar
const progressBarScroll = document.getElementById('scroll-progress');
if (progressBarScroll) {
window.addEventListener('scroll', () => {
const winScroll = document.body.scrollTop || document.documentElement.scrollTop;
const height = document.documentElement.scrollHeight - document.documentElement.clientHeight;
const scrolled = (winScroll / height) * 100;
progressBarScroll.style.width = scrolled + '%';
});
}
// Intersection Observer for Progressive Reveal & Animations
const observerOptions = {
root: null,
rootMargin: '0px',
threshold: 0.15
};
const revealObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('in-view');
// If it's the score card, trigger count up
if (entry.target.id === 'score-card-container') {
const compScore = parseInt(report.compatibility_score) || 85;
animateValue('report-compatibility', 0, compScore, 1500);
}
observer.unobserve(entry.target);
}
});
}, observerOptions);
// Observe elements with .observe-me class
document.querySelectorAll('.observe-me').forEach(el => revealObserver.observe(el));
// 1. V4.0: Narrative & Coaching Elements
document.getElementById('report-headline').textContent = report.dynamic_headline || "Your Relationship Pulse";
document.getElementById('report-pulse').textContent = report.pulse_summary || "Could not generate pulse summary.";
document.getElementById('report-persona').textContent = report.relationship_persona || "The Enigma";
// Compatibility Animation
const compScore = parseInt(report.compatibility_score) || 85;
animateValue('report-compatibility', 0, compScore, 1500);
// Support Score Calculation
const meSupport = supportGap['ME'] || { stress_count: 0, support_received: 0 };
const pSupport = supportGap['PARTNER'] || { stress_count: 0, support_received: 0 };
const totalStress = meSupport.stress_count + pSupport.stress_count;
const totalSupport = meSupport.support_received + pSupport.support_received;
const supportScore = totalStress > 0 ? Math.round((totalSupport / totalStress) * 100) : null;
if (supportScore !== null && document.getElementById('support-score')) {
animateValue('support-score', 0, supportScore, 1500, '%');
} else if (document.getElementById('support-score')) {
document.getElementById('support-score').textContent = '--';
}
// Mirroring Value
const mirroringVal = (mirroring['ME_mirroring'] || 0) + (mirroring['PARTNER_mirroring'] || 0);
const mirEl = document.getElementById('mirroring-value');
if (mirEl) mirEl.textContent = mirroringVal > 0 ? mirroringVal + ' level' : 'Low';
// Core Topic
const sortedTopics = Object.entries(topicMix).sort((a, b) => b[1] - a[1]);
const topicEl = document.getElementById('core-topic');
if (topicEl) topicEl.textContent = sortedTopics.length > 0 ? sortedTopics[0][0] : 'General';
// --- Stat Cards: Populate values + micro-insights ---
// Total messages
const totalMsgs = data.reduce((sum, w) => sum + (w.me_count || 0) + (w.partner_count || 0), 0);
const totalMsgsEl = document.getElementById('stat-total-msgs');
if (totalMsgsEl) {
totalMsgsEl.textContent = totalMsgs.toLocaleString();
}
const insightMsgs = document.getElementById('stat-insight-msgs');
if (insightMsgs) {
if (totalMsgs > 10000) insightMsgs.textContent = 'Deep conversation history';
else if (totalMsgs > 3000) insightMsgs.textContent = 'Solid dataset for analysis';
else if (totalMsgs > 500) insightMsgs.textContent = 'Good sample size';
else insightMsgs.textContent = 'More data = better insights';
}
// Your share %
const meTotal = data.reduce((sum, w) => sum + (w.me_count || 0), 0);
const mePct = totalMsgs > 0 ? Math.round((meTotal / totalMsgs) * 100) : 50;
const mePctEl = document.getElementById('stat-me-pct');
if (mePctEl) mePctEl.textContent = mePct + '%';
const insightShare = document.getElementById('stat-insight-share');
if (insightShare) {
if (mePct > 60) insightShare.textContent = 'You drive the conversation';
else if (mePct > 45) insightShare.textContent = 'Balanced exchange';
else if (mePct > 30) insightShare.textContent = 'Good listener';
else insightShare.textContent = 'Very reserved';
}
// Avg response latency
const latencies = data.map(w => w.avg_latency_seconds).filter(l => l && l > 0);
const avgLatency = latencies.length > 0 ? latencies.reduce((a, b) => a + b, 0) / latencies.length : 0;
const avgLatencyEl = document.getElementById('stat-avg-latency');
if (avgLatencyEl) {
if (avgLatency > 3600) avgLatencyEl.textContent = Math.round(avgLatency / 3600) + 'h';
else if (avgLatency > 60) avgLatencyEl.textContent = Math.round(avgLatency / 60) + 'm';
else if (avgLatency > 0) avgLatencyEl.textContent = Math.round(avgLatency) + 's';
else avgLatencyEl.textContent = '--';
}
const insightLatency = document.getElementById('stat-insight-latency');
if (insightLatency) {
if (avgLatency > 0 && avgLatency < 120) insightLatency.textContent = 'Quick replies, high engagement';
else if (avgLatency < 600) insightLatency.textContent = 'Responsive communication';
else if (avgLatency < 3600) insightLatency.textContent = 'Thoughtful pauses';
else if (avgLatency > 0) insightLatency.textContent = 'Relaxed pace';
else insightLatency.textContent = '';
}
// Mirroring insight
const insightMirror = document.getElementById('stat-insight-mirror');
if (insightMirror) {
if (mirroringVal >= 3) insightMirror.textContent = 'Strong linguistic sync';
else if (mirroringVal >= 1) insightMirror.textContent = 'Moderate alignment';
else insightMirror.textContent = 'Independent styles';
}
// Repair Tips (Nudges)
const nudgeContainer = document.getElementById('report-nudges');
if (nudgeContainer && report.repair_tips && Array.isArray(report.repair_tips)) {
nudgeContainer.innerHTML = report.repair_tips.map(tip => `
●
${escapeHTML(tip)}
`).join('');
}
// Milestones
const milestoneContainer = document.getElementById('report-milestones');
if (milestoneContainer && report.milestones && Array.isArray(report.milestones)) {
milestoneContainer.innerHTML = report.milestones.map(m => `
`).join('');
}
const snippetEl = document.getElementById('report-snippet');
const snippetContainer = document.getElementById('report-snippet-container');
const copyBtn = document.getElementById('copySnippetBtn');
if (snippetEl && report.top_shareable_snippet) {
snippetEl.textContent = `"${report.top_shareable_snippet}"`;
if (snippetContainer) snippetContainer.classList.remove('hidden');
if (copyBtn) {
copyBtn.addEventListener('click', () => {
const originalHTML = copyBtn.innerHTML;
navigator.clipboard.writeText(report.top_shareable_snippet).then(() => {
showToast("Snippet copied to clipboard!", "success");
copyBtn.innerHTML = `
`;
setTimeout(() => {
copyBtn.innerHTML = originalHTML;
}, 2000);
}).catch(err => {
console.error('Failed to copy text: ', err);
});
});
}
}
// Populate Chart Insights
const insights = report.chart_insights || {};
const insightMap = {
'insight-stability': insights.stability,
'insight-volume': insights.volume,
'insight-latency': insights.latency,
'insight-emoji': insights.emoji,
'insight-initiator': insights.initiator,
'insight-power': insights.power,
'insight-affection': insights.affection
};
Object.entries(insightMap).forEach(([id, text]) => {
const el = document.getElementById(id);
if (el && text) el.textContent = text;
});
if (!data.length) return;
// Removed raw chart code (riskChart, volumeChart, latencyChart) since we replaced them with text cards.
// 3. Modals & Flashbacks
async function showFlashback(week) {
const modal = document.getElementById('flashback-modal');
const content = document.getElementById('flashback-content');
const dateEl = document.getElementById('flashback-date');
modal.classList.remove('hidden');
modal.classList.add('active');
dateEl.textContent = `Week of ${week}`;
content.innerHTML = 'Reliving memories...
';
try {
const resp = await fetch(`/flashback?week=${week}`);
const messages = await resp.json();
if (!messages || messages.length === 0) {
content.innerHTML = 'No message data available for this week.
';
return;
}
content.innerHTML = messages.map(m => `
${m.sender === 'ME' ? 'You' : 'Partner'}
${escapeHTML(m.text)}
`).join('');
} catch (e) {
content.innerHTML = 'Error loading flashback.
';
}
}
// --- Existing Stats (Emoji, Initiator, Power) ---
const renderEmojiList = (containerId, items) => {
const el = document.getElementById(containerId);
if (!items || items.length === 0) { el.innerHTML = 'No emojis found
'; return; }
const maxCount = items[0].count;
el.innerHTML = items.map(item => `
${escapeHTML(item.emoji)}
${item.count}
`).join('');
};
renderEmojiList('emojiListMe', emojiFreq['ME'] || []);
renderEmojiList('emojiListPartner', emojiFreq['PARTNER'] || []);
// 4. Trigger Deep Dive Animations on Expansion
const deepDiveDetails = document.getElementById('deep-dive-details');
if (deepDiveDetails) {
deepDiveDetails.addEventListener('toggle', () => {
if (deepDiveDetails.open) {
// Initiator counts
animateValue('meInitCount', 0, initiatorRatio.me_initiations || 0, 1000);
animateValue('partnerInitCount', 0, initiatorRatio.partner_initiations || 0, 1000);
const totalInit = (initiatorRatio.me_initiations || 0) + (initiatorRatio.partner_initiations || 0);
if (totalInit > 0) {
const mePct = ((initiatorRatio.me_initiations || 0) / totalInit) * 100;
const partnerPct = ((initiatorRatio.partner_initiations || 0) / totalInit) * 100;
document.getElementById('meInitiatorBar').style.width = `${mePct}%`;
document.getElementById('partnerInitiatorBar').style.width = `${partnerPct}%`;
}
// Power Dynamics (Chat Balance)
const ratioVal = powerDynamics.power_ratio || 1.0;
animateValue('powerRatioValue', 0, ratioVal, 1000, 'x', 1);
// Affection counts
const affCount = affectionFriction.affirmative_count || 0;
const disCount = affectionFriction.dismissive_count || 0;
animateValue('affCount', 0, affCount, 1000);
animateValue('disCount', 0, disCount, 1000);
const totalAf = affCount + disCount;
if (totalAf > 0) {
document.getElementById('affBar').style.width = `${(affCount / totalAf) * 100}%`;
document.getElementById('disBar').style.width = `${(disCount / totalAf) * 100}%`;
}
}
}, { once: true }); // Only animate once per session
}
// Static description population
const ratioVal = powerDynamics.power_ratio || 1.0;
let ratioDesc = "You both text about the same amount.";
if (ratioVal > 1.2) ratioDesc = "You generally send longer or more frequent messages.";
else if (ratioVal < 0.8) ratioDesc = "Your partner generally sends longer or more frequent messages.";
document.getElementById('powerRatioText').textContent = ratioDesc;
});
/**
* Shows a styled toast notification.
* @param {string} message - The message to display.
* @param {'success' | 'error' | 'info'} type - The type of toast.
*/
function showToast(message, type = 'info') {
const toastContainer = document.getElementById('toast-container');
if (!toastContainer) return;
const toast = document.createElement('div');
let bgColor = 'var(--blue)';
let icon = 'ℹ️';
if (type === 'success') { bgColor = 'var(--green)'; icon = '✅'; }
else if (type === 'error') { bgColor = 'var(--pink)'; icon = '❌'; }
toast.style.cssText = `padding:1rem;border-radius:var(--r-md);border:3px solid var(--black);background:${bgColor};box-shadow:var(--shadow);min-width:280px;max-width:360px;pointer-events:auto;transform:translateY(1rem);opacity:0;transition:all 0.4s ease`;
toast.innerHTML = `
${icon}
${escapeHTML(message)}
`;
toastContainer.appendChild(toast);
requestAnimationFrame(() => { toast.style.transform = 'translateY(0)'; toast.style.opacity = '1'; });
setTimeout(() => {
toast.style.transform = 'translateY(1rem)'; toast.style.opacity = '0';
setTimeout(() => toast.remove(), 500);
}, 5000);
}
// --- Spotify Wrapped Download Logic ---
async function downloadWrappedCard() {
const downloadBtn = document.getElementById('downloadVibeBtn');
const originalContent = downloadBtn ? downloadBtn.innerHTML : '';
try {
if (downloadBtn) {
downloadBtn.disabled = true;
downloadBtn.innerHTML = `
Capturing...
`;
}
const report = window.llmReport || {};
const topicMix = window.topicMix || {};
const supportGap = window.supportGap || {};
const mirroring = window.mirroring || {};
// 1. Calculate values
const sortedTopics = Object.entries(topicMix).sort((a, b) => b[1] - a[1]);
const coreTopic = sortedTopics.length > 0 ? sortedTopics[0][0] : 'General';
const meSupport = supportGap['ME'] || { stress_count: 0, support_received: 0 };
const pSupport = supportGap['PARTNER'] || { stress_count: 0, support_received: 0 };
const totalStress = meSupport.stress_count + pSupport.stress_count;
const totalSupport = meSupport.support_received + pSupport.support_received;
const supportScore = totalStress > 0 ? Math.round((totalSupport / totalStress) * 100) + '%' : '--';
// 2. Populate the hidden card
document.getElementById('share-persona').textContent = report.relationship_persona || "The Mystery";
document.getElementById('share-topic').textContent = coreTopic;
document.getElementById('share-support').textContent = supportScore;
document.getElementById('share-snippet').textContent = report.top_shareable_snippet || "Just vibing.";
document.getElementById('share-predictive').textContent = report.predictive_path || "Walking the path together.";
document.getElementById('share-time-machine').textContent = report.time_machine_insights || "Building history.";
document.getElementById('share-compatibility').textContent = report.compatibility_score || "85";
// 2b. Populate new stat fields
const weeklyData = window.algorithmData || [];
const shareTotalMsgs = weeklyData.reduce((s, w) => s + (w.me_count || 0) + (w.partner_count || 0), 0);
const shareMeTotal = weeklyData.reduce((s, w) => s + (w.me_count || 0), 0);
const shareMePct = shareTotalMsgs > 0 ? Math.round((shareMeTotal / shareTotalMsgs) * 100) : 50;
const shareLatencies = weeklyData.map(w => w.avg_latency_seconds).filter(l => l && l > 0);
const shareAvgLat = shareLatencies.length > 0 ? shareLatencies.reduce((a, b) => a + b, 0) / shareLatencies.length : 0;
const shareMirrorVal = (mirroring['ME_mirroring'] || 0) + (mirroring['PARTNER_mirroring'] || 0);
const stm = document.getElementById('share-total-msgs');
if (stm) stm.textContent = shareTotalMsgs.toLocaleString();
const smp = document.getElementById('share-me-pct');
if (smp) smp.textContent = shareMePct + '%';
const sl = document.getElementById('share-latency');
if (sl) {
if (shareAvgLat > 3600) sl.textContent = Math.round(shareAvgLat / 3600) + 'h';
else if (shareAvgLat > 60) sl.textContent = Math.round(shareAvgLat / 60) + 'm';
else if (shareAvgLat > 0) sl.textContent = Math.round(shareAvgLat) + 's';
else sl.textContent = '--';
}
const smr = document.getElementById('share-mirroring');
if (smr) smr.textContent = shareMirrorVal > 0 ? 'Level ' + shareMirrorVal : 'Low';
// 3. Unhide, Capture, and Re-hide
const container = document.getElementById('shareable-capture-container');
const card = document.getElementById('shareable-card');
// Move on-screen temporarily for exact rendering
container.style.left = '0px';
container.style.top = '0px';
container.style.zIndex = '-999';
const canvas = await html2canvas(card, {
scale: 2, // High-res export
useCORS: true,
backgroundColor: '#111827' // match dark bg
});
// Trigger download
const link = document.createElement('a');
link.download = 'relationship-wrapped.png';
link.href = canvas.toDataURL('image/png');
link.click();
// Show success toast
showToast("Vibe card downloaded successfully!", "success");
} catch (err) {
console.error("Failed to generate wrapped image:", err);
showToast("Couldn't generate your wrapped image. Please try again.", "error");
} finally {
// Hide again
container.style.left = '-9999px';
container.style.top = '-9999px';
container.style.zIndex = 'auto';
if (downloadBtn) {
downloadBtn.disabled = false;
downloadBtn.innerHTML = originalContent;
}
}
}
// --- Contextual Highlights Popup Logic ---
async function initHighlights() {
const toastContainer = document.getElementById('toast-container');
if (!toastContainer) return;
try {
const resp = await fetch('/highlights');
const data = await resp.json();
const highlights = data.highlights;
const connectionType = data.connection_type || 'romantic';
if (!highlights || highlights.length === 0) return;
let iconHtml = '❤️';
if (connectionType === 'friend') iconHtml = '🤝';
else if (connectionType === 'professional') iconHtml = '💼';
else if (connectionType === 'family') iconHtml = '🏠';
let currentIndex = 0;
const showNextHighlight = () => {
if (currentIndex >= highlights.length) currentIndex = 0;
const item = highlights[currentIndex];
currentIndex++;
const toast = document.createElement('div');
toast.style.cssText = 'padding:1rem;display:flex;flex-direction:column;gap:.5rem;border-radius:var(--r-md);border:2px solid var(--black);background:var(--cream);box-shadow:var(--shadow);min-width:280px;max-width:360px;pointer-events:auto;transform:translateY(1rem);opacity:0;transition:all 0.4s ease';
toast.innerHTML = `
${iconHtml} ${escapeHTML(item.title)}
${escapeHTML(item.timestamp.split(' ')[0])}
"${escapeHTML(item.text)}"
— ${escapeHTML(item.sender)}
`;
toastContainer.appendChild(toast);
requestAnimationFrame(() => { toast.style.transform = 'translateY(0)'; toast.style.opacity = '1'; });
setTimeout(() => {
toast.style.transform = 'translateY(1rem)'; toast.style.opacity = '0';
setTimeout(() => toast.remove(), 700);
}, 6000);
};
setTimeout(() => {
showNextHighlight();
setInterval(showNextHighlight, 8000);
}, 3000);
} catch (e) {
console.error('Failed to load highlights:', e);
}
}
// Initialize highlights after everything else
document.addEventListener('DOMContentLoaded', () => {
// Delay initialization slightly to let the heavy dashboard charts render first
setTimeout(initHighlights, 1500);
});