Dify-Learning / static /js /tutorial.js
AtZa
Upload 16 files
7cb4836 verified
// Tutorial-specific JavaScript functionality
let currentTutorial = null;
let currentStep = 0;
let totalSteps = 0;
/**
* Initialize tutorial functionality
*/
function initializeTutorial(tutorialData) {
currentTutorial = tutorialData;
currentStep = tutorialData.currentStep || 0;
totalSteps = tutorialData.totalSteps;
setupTutorialNavigation();
setupStepTracking();
setupKeyboardNavigation();
updateProgress();
// Show the current step
showStep(currentStep);
}
/**
* Setup tutorial navigation
*/
function setupTutorialNavigation() {
// Step navigation links
document.querySelectorAll('.step-nav-link').forEach(link => {
link.addEventListener('click', function(e) {
e.preventDefault();
const stepIndex = parseInt(this.dataset.step);
navigateToStep(stepIndex);
});
});
// Next/Previous buttons
document.querySelectorAll('[onclick*="navigateToStep"]').forEach(btn => {
btn.addEventListener('click', function(e) {
e.preventDefault();
const match = this.getAttribute('onclick').match(/navigateToStep\((\d+)\)/);
if (match) {
navigateToStep(parseInt(match[1]));
}
});
});
}
/**
* Setup step tracking
*/
function setupStepTracking() {
// Track time spent on each step
let stepStartTime = Date.now();
window.addEventListener('beforeunload', () => {
const timeSpent = Math.floor((Date.now() - stepStartTime) / 1000);
trackStepTime(currentStep, timeSpent);
});
// Track step completion
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
const timeSpent = Math.floor((Date.now() - stepStartTime) / 1000);
trackStepTime(currentStep, timeSpent);
} else {
stepStartTime = Date.now();
}
});
}
/**
* Setup keyboard navigation
*/
function setupKeyboardNavigation() {
document.addEventListener('keydown', function(e) {
// Only handle keyboard navigation when tutorial content is focused
if (!document.querySelector('.tutorial-content').contains(document.activeElement)) {
return;
}
switch(e.key) {
case 'ArrowLeft':
if (currentStep > 0) {
e.preventDefault();
navigateToStep(currentStep - 1);
}
break;
case 'ArrowRight':
if (currentStep < totalSteps - 1) {
e.preventDefault();
navigateToStep(currentStep + 1);
}
break;
case 'Home':
e.preventDefault();
navigateToStep(0);
break;
case 'End':
e.preventDefault();
navigateToStep(totalSteps - 1);
break;
}
});
}
/**
* Navigate to a specific step
*/
function navigateToStep(stepIndex) {
if (stepIndex < 0 || stepIndex >= totalSteps) {
return;
}
// Hide current step
const currentStepElement = document.querySelector(`#step-${currentStep}`);
if (currentStepElement) {
currentStepElement.classList.remove('active');
}
// Update step navigation
document.querySelectorAll('.step-nav-link').forEach(link => {
link.classList.remove('active');
});
// Show new step
currentStep = stepIndex;
showStep(currentStep);
// Update navigation
const newNavLink = document.querySelector(`[data-step="${currentStep}"]`);
if (newNavLink) {
newNavLink.classList.add('active');
}
// Update progress
updateProgress();
// Save progress
saveProgress();
// Track step view
window.DifyLearning?.trackEvent('tutorial_step_viewed', {
tutorial_id: currentTutorial.id,
step: currentStep + 1,
step_title: document.querySelector(`#step-${currentStep} .step-title`)?.textContent
});
// Scroll to top of content
const tutorialContent = document.querySelector('.tutorial-content');
if (tutorialContent) {
tutorialContent.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
}
/**
* Show a specific step
*/
function showStep(stepIndex) {
// Hide all steps
document.querySelectorAll('.tutorial-step').forEach(step => {
step.classList.remove('active');
});
// Show target step
const targetStep = document.querySelector(`#step-${stepIndex}`);
if (targetStep) {
targetStep.classList.add('active');
// Initialize any interactive elements in this step
initializeStepElements(targetStep);
// Add fade-in animation
targetStep.classList.add('fade-in');
// Update step counter
updateStepCounter(stepIndex + 1);
}
}
/**
* Initialize interactive elements in a step
*/
function initializeStepElements(stepElement) {
// Initialize code examples with syntax highlighting
const codeBlocks = stepElement.querySelectorAll('pre code');
codeBlocks.forEach(block => {
// Add copy button
addCopyButton(block);
// Add line numbers if needed
if (block.textContent.split('\n').length > 5) {
addLineNumbers(block);
}
});
// Initialize interactive demos
const demos = stepElement.querySelectorAll('.interactive-demo');
demos.forEach(demo => {
initializeDemo(demo);
});
// Initialize tooltips for this step
const tooltips = stepElement.querySelectorAll('[data-bs-toggle="tooltip"]');
tooltips.forEach(tooltip => {
new bootstrap.Tooltip(tooltip);
});
}
/**
* Add copy button to code blocks
*/
function addCopyButton(codeBlock) {
if (codeBlock.querySelector('.copy-button')) {
return; // Already has copy button
}
const copyButton = document.createElement('button');
copyButton.className = 'btn btn-sm btn-outline-secondary copy-button position-absolute top-0 end-0 m-2';
copyButton.innerHTML = '<i data-feather="copy"></i>';
copyButton.title = 'Copy code';
copyButton.addEventListener('click', async () => {
try {
await navigator.clipboard.writeText(codeBlock.textContent);
copyButton.innerHTML = '<i data-feather="check"></i>';
copyButton.classList.add('btn-success');
copyButton.classList.remove('btn-outline-secondary');
setTimeout(() => {
copyButton.innerHTML = '<i data-feather="copy"></i>';
copyButton.classList.remove('btn-success');
copyButton.classList.add('btn-outline-secondary');
feather.replace();
}, 2000);
window.DifyLearning?.showToast('Code copied to clipboard!', 'success');
} catch (err) {
window.DifyLearning?.showToast('Failed to copy code', 'danger');
}
});
// Make parent relative positioned
const pre = codeBlock.closest('pre');
if (pre) {
pre.style.position = 'relative';
pre.appendChild(copyButton);
feather.replace();
}
}
/**
* Add line numbers to code blocks
*/
function addLineNumbers(codeBlock) {
const lines = codeBlock.textContent.split('\n');
const lineNumbers = lines.map((_, index) => index + 1).join('\n');
const lineNumbersElement = document.createElement('pre');
lineNumbersElement.className = 'line-numbers';
lineNumbersElement.textContent = lineNumbers;
lineNumbersElement.style.cssText = `
position: absolute;
left: 0;
top: 0;
padding: 1rem 0.5rem;
background: #f1f3f4;
color: #666;
font-size: 0.8rem;
border-right: 1px solid #ddd;
user-select: none;
width: 3rem;
text-align: right;
`;
const pre = codeBlock.closest('pre');
if (pre) {
pre.style.position = 'relative';
pre.style.paddingLeft = '4rem';
pre.appendChild(lineNumbersElement);
}
}
/**
* Initialize interactive demos
*/
function initializeDemo(demoElement) {
const demoType = demoElement.dataset.demoType;
switch(demoType) {
case 'workflow-builder':
initializeWorkflowDemo(demoElement);
break;
case 'chatbot-preview':
initializeChatbotDemo(demoElement);
break;
case 'agent-configuration':
initializeAgentDemo(demoElement);
break;
default:
// Generic demo initialization
demoElement.innerHTML = `
<div class="demo-placeholder">
<i data-feather="play-circle" class="large-icon text-primary mb-2"></i>
<p>Interactive Demo</p>
<button class="btn btn-primary btn-sm">Try It</button>
</div>
`;
feather.replace();
}
}
/**
* Initialize workflow builder demo
*/
function initializeWorkflowDemo(element) {
element.innerHTML = `
<div class="workflow-demo">
<div class="workflow-canvas">
<div class="workflow-node start-node">
<i data-feather="play"></i>
<span>Start</span>
</div>
<div class="workflow-arrow">→</div>
<div class="workflow-node llm-node">
<i data-feather="cpu"></i>
<span>LLM</span>
</div>
<div class="workflow-arrow">→</div>
<div class="workflow-node end-node">
<i data-feather="message-circle"></i>
<span>Response</span>
</div>
</div>
<div class="workflow-controls mt-3">
<button class="btn btn-sm btn-primary" onclick="addWorkflowNode(this)">
<i data-feather="plus"></i> Add Node
</button>
</div>
</div>
`;
feather.replace();
}
/**
* Initialize chatbot demo
*/
function initializeChatbotDemo(element) {
element.innerHTML = `
<div class="chatbot-demo">
<div class="chat-window">
<div class="chat-messages" id="demo-chat-messages">
<div class="message bot-message">
<div class="message-content">
Hello! I'm your Dify AI assistant. How can I help you today?
</div>
</div>
</div>
<div class="chat-input">
<input type="text" class="form-control" placeholder="Type your message..."
onkeypress="handleDemoChatInput(event)">
<button class="btn btn-primary" onclick="sendDemoMessage()">
<i data-feather="send"></i>
</button>
</div>
</div>
</div>
`;
feather.replace();
}
/**
* Handle demo chat input
*/
function handleDemoChatInput(event) {
if (event.key === 'Enter') {
sendDemoMessage();
}
}
/**
* Send demo message
*/
function sendDemoMessage() {
const input = event.target.closest('.chat-input').querySelector('input');
const message = input.value.trim();
if (!message) return;
const messagesContainer = document.getElementById('demo-chat-messages');
// Add user message
const userMessage = document.createElement('div');
userMessage.className = 'message user-message';
userMessage.innerHTML = `<div class="message-content">${message}</div>`;
messagesContainer.appendChild(userMessage);
// Clear input
input.value = '';
// Simulate bot response
setTimeout(() => {
const botMessage = document.createElement('div');
botMessage.className = 'message bot-message';
botMessage.innerHTML = `
<div class="message-content">
That's a great question about Dify! This is a demo response showing how your chatbot would interact with users.
</div>
`;
messagesContainer.appendChild(botMessage);
messagesContainer.scrollTop = messagesContainer.scrollHeight;
}, 1000);
messagesContainer.scrollTop = messagesContainer.scrollHeight;
}
/**
* Update progress display
*/
function updateProgress() {
// Update progress bar
const progressBar = document.getElementById('tutorial-progress-bar');
if (progressBar) {
const percentage = ((currentStep + 1) / totalSteps) * 100;
window.DifyLearning?.animateProgressBar(progressBar, percentage);
}
// Update step counter
updateStepCounter(currentStep + 1);
// Update sidebar navigation
updateSidebarNavigation();
}
/**
* Update step counter
*/
function updateStepCounter(stepNumber) {
const counter = document.getElementById('current-step');
if (counter) {
counter.textContent = stepNumber;
}
}
/**
* Update sidebar navigation
*/
function updateSidebarNavigation() {
document.querySelectorAll('.step-nav-link').forEach((link, index) => {
link.classList.remove('active');
if (index === currentStep) {
link.classList.add('active');
}
// Remove completed indicators (checkmarks disabled)
const existingCheck = link.querySelector('i[data-feather="check"]');
if (existingCheck) {
existingCheck.remove();
}
});
}
/**
* Save progress to server
*/
async function saveProgress() {
if (!currentTutorial) return;
try {
const response = await fetch('/api/progress/update', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
tutorial_id: currentTutorial.id,
current_step: currentStep,
completed: false
})
});
if (!response.ok) {
throw new Error('Failed to save progress');
}
} catch (error) {
console.error('Error saving progress:', error);
// Show error message to user
window.DifyLearning?.showToast('Failed to save progress', 'warning');
}
}
/**
* Complete tutorial
*/
async function completeTutorial() {
if (!currentTutorial) return;
try {
const response = await fetch('/api/progress/update', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
tutorial_id: currentTutorial.id,
current_step: totalSteps - 1,
completed: true
})
});
if (response.ok) {
// Track completion
window.DifyLearning?.trackEvent('tutorial_completed', {
tutorial_id: currentTutorial.id,
total_steps: totalSteps,
completion_time: new Date().toISOString()
});
// Show completion modal
const modal = new bootstrap.Modal(document.getElementById('completionModal'));
modal.show();
// Update UI to show completion
document.querySelectorAll('.progress-circle').forEach(circle => {
circle.className = 'progress-circle completed';
circle.innerHTML = '<i data-feather="check"></i>';
});
feather.replace();
} else {
throw new Error('Failed to complete tutorial');
}
} catch (error) {
console.error('Error completing tutorial:', error);
window.DifyLearning?.showToast('Failed to save completion status', 'warning');
}
}
/**
* Track time spent on step
*/
function trackStepTime(stepIndex, seconds) {
if (seconds < 5) return; // Ignore very short durations
const stepData = {
tutorial_id: currentTutorial?.id,
step: stepIndex + 1,
time_spent: seconds
};
// Store locally for analytics
const stepTimes = JSON.parse(localStorage.getItem('dify_step_times') || '[]');
stepTimes.push({
...stepData,
timestamp: new Date().toISOString()
});
localStorage.setItem('dify_step_times', JSON.stringify(stepTimes.slice(-1000)));
}
/**
* Add workflow node (demo function)
*/
function addWorkflowNode(button) {
const canvas = button.closest('.workflow-demo').querySelector('.workflow-canvas');
const newNode = document.createElement('div');
newNode.className = 'workflow-node';
newNode.innerHTML = `
<i data-feather="settings"></i>
<span>New Node</span>
`;
// Add arrow before new node
const arrow = document.createElement('div');
arrow.className = 'workflow-arrow';
arrow.textContent = '→';
// Insert before end node
const endNode = canvas.querySelector('.end-node');
canvas.insertBefore(arrow, endNode);
canvas.insertBefore(newNode, endNode);
feather.replace();
// Show success message
window.DifyLearning?.showToast('Node added to workflow!', 'success');
}
// Make functions available globally
window.navigateToStep = navigateToStep;
window.completeTutorial = completeTutorial;
window.handleDemoChatInput = handleDemoChatInput;
window.sendDemoMessage = sendDemoMessage;
window.addWorkflowNode = addWorkflowNode;