Files
group_xinghuo_jinrong/docs/course/jinrong-module-shared/main.js
T
zhanghongyu_0626 4b8e11c9bd feat(threshold): Implement customer loss threshold configuration and notification system
- Added `ThresholdRepository` for managing customer loss threshold configurations and notifications.
- Introduced `threshold_service` to handle loss threshold alerts based on customer portfolio performance.
- Enhanced `customer_prompts` to include new intent for querying product net values.
- Updated `customer_service` to integrate new threshold alert functionality into existing workflows.
- Implemented `sanitize_postprocess` for improved compliance handling in customer interactions.
- Enhanced course documentation to reflect updates in advisor training modules and interactive elements.

This update significantly improves the customer experience by providing proactive loss threshold notifications and enhancing the overall service framework.
2026-09-10 10:57:40 +08:00

503 lines
19 KiB
JavaScript

/**
* CODEBASE-TO-COURSE — COMPLETE JS ENGINE
* Copy this file verbatim into the course output directory.
* Never regenerate it. It handles all interactivity generically.
*
* Engines included:
* - Navigation & progress bar
* - Scroll-triggered reveal animations
* - Keyboard navigation
* - Glossary tooltips
* - Quiz (multiple-choice & scenario)
* - Drag-and-drop matching
* - Group chat animation
* - Data flow / message flow animation
* - Architecture diagram
* - "Spot the bug" challenge
* - Layer toggle
*/
(function () {
'use strict';
/* ── HELPERS ──────────────────────────────────────────────── */
function $(sel, ctx) { return (ctx || document).querySelector(sel); }
function $$(sel, ctx) { return Array.from((ctx || document).querySelectorAll(sel)); }
/* ── NAVIGATION & PROGRESS BAR ────────────────────────────── */
const progressBar = $('#progress-bar');
const navDots = $$('.nav-dot');
const modules = $$('.module');
function updateProgress() {
if (!progressBar) return;
const scrollTop = window.scrollY;
const scrollHeight = document.documentElement.scrollHeight - window.innerHeight;
const pct = scrollHeight > 0 ? (scrollTop / scrollHeight) * 100 : 0;
progressBar.style.width = pct + '%';
progressBar.setAttribute('aria-valuenow', Math.round(pct));
updateNavDots();
}
function updateNavDots() {
const scrollMid = window.scrollY + window.innerHeight / 2;
modules.forEach((mod, i) => {
const dot = navDots[i];
if (!dot) return;
const top = mod.offsetTop;
const bottom = top + mod.offsetHeight;
if (scrollMid >= top && scrollMid < bottom) {
dot.classList.add('active');
dot.classList.remove('visited');
} else if (window.scrollY + window.innerHeight > top) {
dot.classList.remove('active');
dot.classList.add('visited');
} else {
dot.classList.remove('active', 'visited');
}
});
}
window.addEventListener('scroll', () => requestAnimationFrame(updateProgress), { passive: true });
updateProgress();
// Nav dot click → scroll to module
navDots.forEach(dot => {
dot.addEventListener('click', () => {
const target = $('#' + dot.dataset.target);
if (target) target.scrollIntoView({ behavior: 'smooth' });
});
});
/* ── KEYBOARD NAVIGATION ───────────────────────────────────── */
function currentModuleIndex() {
const scrollMid = window.scrollY + window.innerHeight / 2;
for (let i = 0; i < modules.length; i++) {
const top = modules[i].offsetTop;
const bottom = top + modules[i].offsetHeight;
if (scrollMid >= top && scrollMid < bottom) return i;
}
return 0;
}
document.addEventListener('keydown', e => {
if (['INPUT', 'TEXTAREA', 'SELECT'].includes(e.target.tagName)) return;
if (e.key === 'ArrowDown' || e.key === 'ArrowRight') {
const next = modules[currentModuleIndex() + 1];
if (next) { next.scrollIntoView({ behavior: 'smooth' }); e.preventDefault(); }
}
if (e.key === 'ArrowUp' || e.key === 'ArrowLeft') {
const prev = modules[currentModuleIndex() - 1];
if (prev) { prev.scrollIntoView({ behavior: 'smooth' }); e.preventDefault(); }
}
});
/* ── SCROLL-TRIGGERED REVEAL ───────────────────────────────── */
const revealObserver = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('visible');
revealObserver.unobserve(entry.target);
}
});
}, { rootMargin: '0px 0px -8% 0px', threshold: 0.08 });
$$('.animate-in').forEach(el => revealObserver.observe(el));
// Stagger children
$$('.stagger-children').forEach(parent => {
Array.from(parent.children).forEach((child, i) => {
child.style.setProperty('--stagger-index', i);
});
});
/* ── GLOSSARY TOOLTIPS ─────────────────────────────────────── */
let activeTooltip = null;
function positionTooltip(term, tip) {
const rect = term.getBoundingClientRect();
const tipWidth = Math.min(320, Math.max(200, window.innerWidth * 0.8));
let left = rect.left + rect.width / 2 - tipWidth / 2;
left = Math.max(8, Math.min(left, window.innerWidth - tipWidth - 8));
tip.style.left = left + 'px';
tip.style.width = tipWidth + 'px';
document.body.appendChild(tip);
const tipHeight = tip.offsetHeight;
if (rect.top - tipHeight - 12 < 0) {
tip.style.top = (rect.bottom + 8) + 'px';
tip.classList.add('flip');
} else {
tip.style.top = (rect.top - tipHeight - 8) + 'px';
tip.classList.remove('flip');
}
}
function showTooltip(term, tip) {
if (activeTooltip && activeTooltip !== tip) {
activeTooltip.classList.remove('visible');
activeTooltip.remove();
}
positionTooltip(term, tip);
requestAnimationFrame(() => tip.classList.add('visible'));
activeTooltip = tip;
}
function hideTooltip(tip) {
tip.classList.remove('visible');
setTimeout(() => { if (!tip.classList.contains('visible')) tip.remove(); }, 150);
if (activeTooltip === tip) activeTooltip = null;
}
$$('.term').forEach(term => {
const tip = document.createElement('span');
tip.className = 'term-tooltip';
tip.textContent = term.dataset.definition;
term.addEventListener('mouseenter', () => showTooltip(term, tip));
term.addEventListener('mouseleave', () => hideTooltip(tip));
term.addEventListener('click', e => {
e.stopPropagation();
tip.classList.contains('visible') ? hideTooltip(tip) : showTooltip(term, tip);
});
});
document.addEventListener('click', () => {
if (activeTooltip) { activeTooltip.classList.remove('visible'); activeTooltip.remove(); activeTooltip = null; }
});
/* ── QUIZ ENGINE ───────────────────────────────────────────── */
window.selectOption = function (btn) {
const block = btn.closest('.quiz-question-block');
$$('.quiz-option', block).forEach(o => o.classList.remove('selected'));
btn.classList.add('selected');
};
window.checkQuiz = function (containerId) {
const container = $('#' + containerId);
if (!container) return;
$$('.quiz-question-block', container).forEach(q => {
const selected = $('.quiz-option.selected', q);
const feedback = $('.quiz-feedback', q);
const correct = q.dataset.correct;
const rightExp = q.dataset.explanationRight || '';
const wrongExp = q.dataset.explanationWrong || '';
if (!selected) {
feedback.textContent = 'Pick an answer first!';
feedback.className = 'quiz-feedback show warning';
return;
}
$$('.quiz-option', q).forEach(o => o.disabled = true);
if (selected.dataset.value === correct) {
selected.classList.add('correct');
feedback.innerHTML = '<strong>Exactly!</strong> ' + rightExp;
feedback.className = 'quiz-feedback show success';
} else {
selected.classList.add('incorrect');
const correctBtn = $(`.quiz-option[data-value="${correct}"]`, q);
if (correctBtn) correctBtn.classList.add('correct');
feedback.innerHTML = '<strong>Not quite.</strong> ' + wrongExp;
feedback.className = 'quiz-feedback show error';
}
});
};
window.resetQuiz = function (containerId) {
const container = $('#' + containerId);
if (!container) return;
$$('.quiz-option', container).forEach(o => {
o.classList.remove('selected', 'correct', 'incorrect');
o.disabled = false;
});
$$('.quiz-feedback', container).forEach(f => { f.className = 'quiz-feedback'; f.textContent = ''; });
};
/* ── DRAG-AND-DROP ENGINE ──────────────────────────────────── */
function initDnD(containerEl) {
if (!containerEl) return;
const chips = $$('.dnd-chip', containerEl);
const zones = $$('.dnd-zone', containerEl);
// Mouse (HTML5 Drag API)
chips.forEach(chip => {
chip.addEventListener('dragstart', e => {
e.dataTransfer.setData('text/plain', chip.dataset.answer);
chip.classList.add('dragging');
});
chip.addEventListener('dragend', () => chip.classList.remove('dragging'));
});
zones.forEach(zone => {
const target = $('.dnd-zone-target', zone);
if (!target) return;
target.addEventListener('dragover', e => { e.preventDefault(); target.classList.add('drag-over'); });
target.addEventListener('dragleave', () => target.classList.remove('drag-over'));
target.addEventListener('drop', e => {
e.preventDefault();
target.classList.remove('drag-over');
const answer = e.dataTransfer.getData('text/plain');
const chip = $(`.dnd-chip[data-answer="${answer}"]`, containerEl);
if (!chip) return;
target.textContent = chip.textContent;
target.dataset.placed = answer;
chip.classList.add('placed');
});
});
// Touch
chips.forEach(chip => {
chip.addEventListener('touchstart', e => {
e.preventDefault();
const touch = e.touches[0];
const ghost = chip.cloneNode(true);
ghost.classList.add('touch-ghost');
ghost.style.cssText = `position:fixed;z-index:9999;pointer-events:none;left:${touch.clientX - 40}px;top:${touch.clientY - 20}px;`;
document.body.appendChild(ghost);
chip._ghost = ghost;
chip._answer = chip.dataset.answer;
}, { passive: false });
chip.addEventListener('touchmove', e => {
e.preventDefault();
const touch = e.touches[0];
if (chip._ghost) {
chip._ghost.style.left = (touch.clientX - 40) + 'px';
chip._ghost.style.top = (touch.clientY - 20) + 'px';
}
zones.forEach(z => { const t = $('.dnd-zone-target', z); if (t) t.classList.remove('drag-over'); });
const el = document.elementFromPoint(touch.clientX, touch.clientY);
const zt = el && el.closest('.dnd-zone-target');
if (zt) zt.classList.add('drag-over');
}, { passive: false });
chip.addEventListener('touchend', e => {
if (chip._ghost) { chip._ghost.remove(); chip._ghost = null; }
const touch = e.changedTouches[0];
const el = document.elementFromPoint(touch.clientX, touch.clientY);
const zt = el && el.closest('.dnd-zone-target');
if (zt) {
zt.textContent = chip.textContent;
zt.dataset.placed = chip._answer;
chip.classList.add('placed');
}
zones.forEach(z => { const t = $('.dnd-zone-target', z); if (t) t.classList.remove('drag-over'); });
});
});
}
window.checkDnD = function (containerId) {
const container = $('#' + containerId);
if (!container) return;
$$('.dnd-zone', container).forEach(zone => {
const target = $('.dnd-zone-target', zone);
if (!target || !target.dataset.placed) return;
if (target.dataset.placed === zone.dataset.correct) {
target.classList.add('correct-placed');
} else {
target.classList.add('incorrect-placed');
}
});
};
window.resetDnD = function (containerId) {
const container = $('#' + containerId);
if (!container) return;
$$('.dnd-zone-target', container).forEach(t => {
t.textContent = 'Drop here';
delete t.dataset.placed;
t.classList.remove('correct-placed', 'incorrect-placed');
});
$$('.dnd-chip', container).forEach(c => c.classList.remove('placed', 'dragging'));
};
// Auto-init all dnd containers
$$('.dnd-container').forEach(el => initDnD(el));
/* ── GROUP CHAT ENGINE ─────────────────────────────────────── */
function initChat(containerEl) {
if (!containerEl) return;
const messages = $$('.chat-message', containerEl);
const typingEl = $('.chat-typing', containerEl);
const typingAvEl = $('#' + containerEl.id + '-typing-avatar') || $('.chat-avatar', typingEl);
const progressEl = $('.chat-progress', containerEl);
let index = 0;
// Build actor map from messages
const actors = {};
messages.forEach(msg => {
const sender = msg.dataset.sender;
const avatar = $('.chat-avatar', msg);
if (avatar && !actors[sender]) {
actors[sender] = { initial: avatar.textContent.trim(), style: avatar.style.background };
}
});
function updateProgress() {
if (progressEl) progressEl.textContent = index + ' / ' + messages.length + ' messages';
}
function showNext() {
if (index >= messages.length) return;
const msg = messages[index];
const sender = msg.dataset.sender;
if (typingEl && actors[sender]) {
if (typingAvEl) {
typingAvEl.textContent = actors[sender].initial;
typingAvEl.style.background = actors[sender].style;
}
typingEl.style.display = 'flex';
}
setTimeout(() => {
if (typingEl) typingEl.style.display = 'none';
msg.style.display = 'flex';
msg.style.animation = 'fadeSlideUp 0.3s var(--ease-out)';
index++;
updateProgress();
}, 800);
}
function showAll() {
const iv = setInterval(() => {
if (index >= messages.length) { clearInterval(iv); return; }
showNext();
}, 1200);
}
function reset() {
index = 0;
messages.forEach(m => { m.style.display = 'none'; m.style.animation = ''; });
if (typingEl) typingEl.style.display = 'none';
updateProgress();
}
// Bind controls
const nextBtn = $('.chat-next-btn', containerEl);
const allBtn = $('.chat-all-btn', containerEl);
const resetBtn = $('.chat-reset-btn', containerEl);
if (nextBtn) nextBtn.addEventListener('click', showNext);
if (allBtn) allBtn.addEventListener('click', showAll);
if (resetBtn) resetBtn.addEventListener('click', reset);
updateProgress();
}
$$('.chat-window').forEach(el => initChat(el));
/* ── FLOW ANIMATION ENGINE ─────────────────────────────────── */
function initFlow(containerEl) {
if (!containerEl) return;
const stepsData = JSON.parse(containerEl.dataset.steps || '[]');
const labelEl = $('.flow-step-label', containerEl);
const progressEl = $('.flow-progress', containerEl);
const packet = $('.flow-packet', containerEl);
let step = 0;
function updateProgress() {
if (progressEl) progressEl.textContent = 'Step ' + step + ' / ' + stepsData.length;
}
function animatePacket(fromId, toId) {
if (!packet) return;
const fromEl = $('#' + fromId);
const toEl = $('#' + toId);
if (!fromEl || !toEl) return;
const fromR = fromEl.getBoundingClientRect();
const toR = toEl.getBoundingClientRect();
const contR = containerEl.getBoundingClientRect();
const fx = fromR.left + fromR.width / 2 - contR.left;
const fy = fromR.top + fromR.height / 2 - contR.top;
const tx = toR.left + toR.width / 2 - contR.left;
const ty = toR.top + toR.height / 2 - contR.top;
packet.style.setProperty('--packet-from-x', fx + 'px');
packet.style.setProperty('--packet-from-y', fy + 'px');
packet.style.setProperty('--packet-to-x', tx + 'px');
packet.style.setProperty('--packet-to-y', ty + 'px');
packet.style.display = 'block';
packet.style.animation = 'none';
packet.offsetHeight; // reflow
packet.style.animation = 'packetMove 0.8s var(--ease-in-out) forwards';
setTimeout(() => { packet.style.display = 'none'; }, 850);
}
function next() {
if (step >= stepsData.length) return;
const s = stepsData[step];
$$('.flow-actor', containerEl).forEach(a => a.classList.remove('active'));
if (s.highlight) {
const hEl = $('#' + s.highlight, containerEl) || $('#flow-' + s.highlight);
if (hEl) hEl.classList.add('active');
}
if (s.packet && s.from && s.to) animatePacket('flow-' + s.from, 'flow-' + s.to);
if (labelEl) labelEl.textContent = s.label || '';
step++;
updateProgress();
}
function reset() {
step = 0;
$$('.flow-actor', containerEl).forEach(a => a.classList.remove('active'));
if (labelEl) labelEl.textContent = 'Click "Next Step" to begin';
if (packet) packet.style.display = 'none';
updateProgress();
}
const nextBtn = $('.flow-next-btn', containerEl);
const resetBtn = $('.flow-reset-btn', containerEl);
if (nextBtn) nextBtn.addEventListener('click', next);
if (resetBtn) resetBtn.addEventListener('click', reset);
updateProgress();
}
$$('.flow-animation').forEach(el => initFlow(el));
/* ── ARCHITECTURE DIAGRAM ──────────────────────────────────── */
$$('.arch-component').forEach(comp => {
comp.addEventListener('click', function () {
const diagram = this.closest('.arch-diagram');
$$('.arch-component', diagram).forEach(c => c.classList.remove('active'));
this.classList.add('active');
const descEl = $('.arch-description', diagram);
if (descEl) descEl.textContent = this.dataset.desc || '';
});
});
/* ── BUG CHALLENGE ─────────────────────────────────────────── */
window.checkBugLine = function (el, isCorrect) {
const challenge = el.closest('.bug-challenge');
const feedback = $('.bug-feedback', challenge);
if (isCorrect) {
el.classList.add('correct');
feedback.innerHTML = '<strong>Found it!</strong> ' + (el.dataset.explanation || '');
feedback.className = 'bug-feedback show success';
$$('.bug-line', challenge).forEach(l => l.style.pointerEvents = 'none');
} else {
el.classList.add('incorrect');
feedback.innerHTML = (el.dataset.hint || 'Not this line — keep looking...');
feedback.className = 'bug-feedback show error';
setTimeout(() => {
el.classList.remove('incorrect');
feedback.className = 'bug-feedback';
}, 1800);
}
};
/* ── LAYER TOGGLE ──────────────────────────────────────────── */
window.showLayer = function (layerId, btn) {
const tab = btn || (window.event && window.event.currentTarget);
if (!tab || !tab.closest) return;
const demo = tab.closest('.layer-demo');
if (!demo) return;
$$('.layer', demo).forEach(l => { l.style.display = 'none'; });
$$('.layer-tab', demo).forEach(t => t.classList.remove('active'));
const esc = typeof CSS !== 'undefined' && CSS.escape ? CSS.escape(layerId) : layerId.replace(/[^\w-]/g, '');
let layer = demo.querySelector('#' + esc);
if (!layer) layer = demo.querySelector('#layer-' + esc);
if (layer) layer.style.display = 'block';
tab.classList.add('active');
};
})();