Spaces:
Sleeping
Sleeping
| // 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; | |