Spaces:
Sleeping
Sleeping
| import React, { useState, useEffect, useMemo } from 'react'; | |
| import Joyride, { STATUS } from 'react-joyride'; | |
| const GuideTour = ({ darkMode, tourVersion, onTourEnd }) => { | |
| const steps = useMemo(() => [ | |
| { | |
| target: 'body', | |
| title: 'Xin chào, tôi là Pochi', | |
| content: 'Cùng bắt đầu khám phá không gian làm việc của bạn nhé!', | |
| placement: 'center', | |
| disableBeacon: true, | |
| }, | |
| { | |
| target: 'body', | |
| title: 'Thao tác nhanh', | |
| content: ( | |
| <div className="tour-shortcuts-container"> | |
| <div className="shortcuts-intro">Sử dụng bàn phím để điều khiển linh hoạt:</div> | |
| <div className="shortcuts-grid"> | |
| <span className="kbd-bullet">•</span> | |
| <div className="kbd-center"> | |
| <span className="kbd-key">←</span> | |
| <span className="kbd-key">→</span> | |
| </div> | |
| <span className="kbd-colon">:</span> | |
| <span className="kbd-text">Điều hướng các bước.</span> | |
| <span className="kbd-bullet">•</span> | |
| <div className="kbd-center"> | |
| <span className="kbd-key">Esc</span> | |
| </div> | |
| <span className="kbd-colon">:</span> | |
| <span className="kbd-text">Bỏ qua hướng dẫn nhanh.</span> | |
| <span className="kbd-bullet">•</span> | |
| <div className="kbd-center"> | |
| <span className="kbd-key">Enter</span> | |
| </div> | |
| <span className="kbd-colon">:</span> | |
| <span className="kbd-text">Hoàn tất hướng dẫn.</span> | |
| </div> | |
| </div> | |
| ), | |
| placement: 'center', | |
| disableBeacon: true, | |
| }, | |
| { | |
| target: '#tour-new-chat', | |
| title: 'Đoạn chat mới', | |
| content: 'Nơi bạn bắt đầu những cuộc trò chuyện mới với Pochi linh hoạt và nhanh chóng.', | |
| disableBeacon: true, | |
| }, | |
| { | |
| target: '#tour-chat-input-area', | |
| title: 'Khung gửi tin nhắn', | |
| content: 'Nhập nội dung câu hỏi, đính kèm hình ảnh để trò chuyện cùng Pochi.', | |
| disableBeacon: true, | |
| }, | |
| { | |
| target: '#tour-chat-interface', | |
| title: 'Giao diện trò chuyện', | |
| content: 'Khu vực hiển thị nội dung trao đổi giữa bạn và Pochi một cách trực quan.', | |
| disableBeacon: true, | |
| }, | |
| { | |
| target: '#tour-history-section', | |
| title: 'Lịch sử trò chuyện', | |
| content: 'Toàn bộ các cuộc trò chuyện của bạn đều được lưu trữ an toàn tại đây.', | |
| spotlightPadding: 5, | |
| disableBeacon: true, | |
| }, | |
| { | |
| target: '#tour-chat-features', | |
| title: 'Tính năng nâng cao', | |
| content: 'Bạn có thể ghim, đổi tên, lưu trữ hoặc xóa các cuộc trò chuyện thông qua menu này.', | |
| spotlightPadding: 5, | |
| disableBeacon: true, | |
| }, | |
| { | |
| target: '#tour-profile-header-avatar', | |
| title: 'Cài đặt cá nhân (Header)', | |
| content: 'Bạn có thể truy cập cài đặt, quản lý tài khoản hoặc đăng xuất nhanh tại logo góc trên này.', | |
| spotlightPadding: 5, | |
| disableBeacon: true, | |
| }, | |
| { | |
| target: '#tour-profile-sidebar-avatar', | |
| title: 'Cài đặt cá nhân (Sidebar)', | |
| content: 'Hoặc bạn cũng có thể thay đổi chế độ tối / sáng và xem tin nhắn lưu trữ tại logo phía dưới này.', | |
| spotlightPadding: 5, | |
| disableBeacon: true, | |
| }, | |
| { | |
| target: '#tour-search', | |
| title: 'Tìm kiếm đoạn chat', | |
| content: 'Giúp bạn tìm lại những thông tin cũ trong lịch sử trò chuyện chỉ với vài phím gõ.', | |
| spotlightPadding: 5, | |
| disableBeacon: true, | |
| }, | |
| { | |
| target: '#tour-toggle-sidebar', | |
| title: 'Thu gọn thanh bên', | |
| content: 'Tối ưu không gian làm việc bằng cách thu gọn thanh bên khi cần thiết.', | |
| spotlightPadding: 5, | |
| disableBeacon: true, | |
| }, | |
| { | |
| target: '#tour-help-btn', | |
| title: 'Trợ giúp & Hướng dẫn', | |
| content: 'Bất cứ lúc nào bạn cần, hãy nhấn vào đây để xem lại hướng dẫn sử dụng nhé!', | |
| disableBeacon: true, | |
| }, | |
| { | |
| target: 'body', | |
| title: 'Tổng hợp phím tắt', | |
| content: ( | |
| <div className="tour-shortcuts-container"> | |
| <div className="shortcuts-intro">Làm chủ ứng dụng qua các phím tắt nhanh:</div> | |
| <div className="shortcuts-grid"> | |
| <span className="kbd-bullet">•</span> | |
| <div className="kbd-center"> | |
| <span className="kbd-key">{navigator.platform.toUpperCase().indexOf('MAC') >= 0 ? '⌘' : 'Ctrl'}</span> | |
| <span className="kbd-key">F</span> | |
| </div> | |
| <span className="kbd-colon">:</span> | |
| <span className="kbd-text">Tìm kiếm trò chuyện.</span> | |
| <span className="kbd-bullet">•</span> | |
| <div className="kbd-center"> | |
| <span className="kbd-key">{navigator.platform.toUpperCase().indexOf('MAC') >= 0 ? '⌘' : 'Ctrl'}</span> | |
| <span className="kbd-key">E</span> | |
| </div> | |
| <span className="kbd-colon">:</span> | |
| <span className="kbd-text">Tạo đoạn chat mới.</span> | |
| <span className="kbd-bullet">•</span> | |
| <div className="kbd-center"> | |
| <span className="kbd-key">{navigator.platform.toUpperCase().indexOf('MAC') >= 0 ? '⌘' : 'Ctrl'}</span> | |
| <span className="kbd-key">K</span> | |
| </div> | |
| <span className="kbd-colon">:</span> | |
| <span className="kbd-text">Bật/tắt chế độ tối.</span> | |
| <span className="kbd-bullet">•</span> | |
| <div className="kbd-center"> | |
| <span className="kbd-key">{navigator.platform.toUpperCase().indexOf('MAC') >= 0 ? '⌘' : 'Ctrl'}</span> | |
| <span className="kbd-key">X</span> | |
| </div> | |
| <span className="kbd-colon">:</span> | |
| <span className="kbd-text">Mở cài đặt chung.</span> | |
| <span className="kbd-bullet">•</span> | |
| <div className="kbd-center"> | |
| <span className="kbd-key">{navigator.platform.toUpperCase().indexOf('MAC') >= 0 ? '⌘' : 'Ctrl'}</span> | |
| <span className="kbd-key">I</span> | |
| </div> | |
| <span className="kbd-colon">:</span> | |
| <span className="kbd-text">Thông tin tài khoản.</span> | |
| </div> | |
| <div style={{ | |
| marginTop: '20px', | |
| fontSize: '0.95rem', | |
| fontWeight: 600, | |
| color: darkMode ? 'var(--text-secondary)' : '#4b5563', | |
| textAlign: 'center', | |
| paddingTop: '12px', | |
| borderTop: `1px solid ${darkMode ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.05)'}` | |
| }}> | |
| Ấn phím tắt <span className="kbd-key">Enter</span> để hoàn tất hướng dẫn. | |
| </div> | |
| </div> | |
| ), | |
| placement: 'center', | |
| disableBeacon: true, | |
| } | |
| ], [darkMode]); | |
| const [run, setRun] = useState(false); | |
| const [stepIndex, setStepIndex] = useState(() => { | |
| const savedIndex = localStorage.getItem('tourStepIndex'); | |
| return (savedIndex && savedIndex !== '-1') ? parseInt(savedIndex, 10) : 0; | |
| }); | |
| const finalizeTour = () => { | |
| setRun(false); | |
| setStepIndex(0); | |
| localStorage.setItem('hasSeenTour', 'true'); | |
| localStorage.setItem('tourStepIndex', '-1'); | |
| if (onTourEnd) onTourEnd(); | |
| // Remove focus from whatever triggered the end/skip to prevent persistent focus outlines | |
| if (document.activeElement && typeof document.activeElement.blur === 'function') { | |
| document.activeElement.blur(); | |
| } | |
| }; | |
| const handleJoyrideCallback = (data) => { | |
| const { action, index, status, type, size } = data; | |
| if (type === 'step:after' || type === 'target:not_found') { | |
| const isLastStep = index === size - 1; | |
| if (isLastStep && action === 'next') { | |
| finalizeTour(); | |
| return; | |
| } | |
| const nextIndex = index + (action === 'next' ? 1 : -1); | |
| if (nextIndex >= 0 && nextIndex < size) { | |
| setStepIndex(nextIndex); | |
| localStorage.setItem('tourStepIndex', nextIndex.toString()); | |
| } | |
| } | |
| const finishedStatuses = [STATUS.FINISHED, STATUS.SKIPPED]; | |
| if (finishedStatuses.includes(status)) { | |
| finalizeTour(); | |
| } | |
| }; | |
| useEffect(() => { | |
| const hasSeenTour = localStorage.getItem('hasSeenTour'); | |
| const savedIndex = localStorage.getItem('tourStepIndex'); | |
| if (tourVersion > 0) { | |
| setStepIndex(0); | |
| localStorage.setItem('tourStepIndex', '0'); | |
| setRun(true); | |
| } else if (tourVersion === -1) { | |
| setRun(false); | |
| } else if (!hasSeenTour || (savedIndex !== null && savedIndex !== '-1')) { | |
| setRun(true); | |
| } else { | |
| setRun(false); | |
| } | |
| }, [tourVersion]); | |
| useEffect(() => { | |
| if (!run) return; | |
| const handleKeyDown = (e) => { | |
| const isLastStep = stepIndex === steps.length - 1; | |
| const isFirstStep = stepIndex === 0; | |
| switch (e.key) { | |
| case 'ArrowRight': | |
| if (!isLastStep) { | |
| e.preventDefault(); | |
| const nextIndex = stepIndex + 1; | |
| setStepIndex(nextIndex); | |
| localStorage.setItem('tourStepIndex', nextIndex.toString()); | |
| } | |
| break; | |
| case 'ArrowLeft': | |
| if (!isFirstStep) { | |
| e.preventDefault(); | |
| const prevIndex = stepIndex - 1; | |
| setStepIndex(prevIndex); | |
| localStorage.setItem('tourStepIndex', prevIndex.toString()); | |
| } | |
| break; | |
| case 'Enter': | |
| e.preventDefault(); | |
| if (isLastStep) { | |
| finalizeTour(); | |
| } | |
| break; | |
| case 'Escape': | |
| e.preventDefault(); | |
| finalizeTour(); | |
| break; | |
| default: | |
| break; | |
| } | |
| }; | |
| window.addEventListener('keydown', handleKeyDown); | |
| return () => window.removeEventListener('keydown', handleKeyDown); | |
| }, [run, stepIndex, steps]); | |
| const CustomTooltip = ({ | |
| index, | |
| step, | |
| size, | |
| backProps, | |
| primaryProps, | |
| skipProps, | |
| tooltipProps, | |
| arrowProps, | |
| isLastStep | |
| }) => { | |
| const safeTooltipProps = tooltipProps || {}; | |
| const placement = safeTooltipProps['data-placement'] || step?.placement || 'bottom'; | |
| return ( | |
| <div {...safeTooltipProps} className="tour-tooltip glass" data-placement={placement}> | |
| <div className="tour-header"> | |
| <div className="tour-header-left"> | |
| <span className="tour-progress">{index + 1} / {size}</span> | |
| </div> | |
| <div className="tour-header-right"> | |
| {index > 0 && ( | |
| <button {...backProps} className="tour-btn-nav tour-btn-back-header"> | |
| Quay lại | |
| </button> | |
| )} | |
| <button {...primaryProps} className="tour-btn-nav tour-btn-next-header"> | |
| {isLastStep ? 'Hoàn tất' : 'Tiếp theo'} | |
| </button> | |
| </div> | |
| </div> | |
| <div className="tour-content"> | |
| {step?.title && <h4 className="tour-title">{step.title}</h4>} | |
| <div className="tour-body">{step?.content}</div> | |
| </div> | |
| <div className="tour-footer"> | |
| {!isLastStep && ( | |
| <button {...skipProps} className="tour-btn-skip"> | |
| Bỏ qua | |
| </button> | |
| )} | |
| </div> | |
| <style dangerouslySetInnerHTML={{ | |
| __html: ` | |
| .tour-tooltip { | |
| max-width: 360px; | |
| padding: 20px; | |
| border-radius: 20px; | |
| box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5); | |
| font-family: 'Inter', sans-serif; | |
| color: var(--text-primary); | |
| border: 1px solid var(--border-light); | |
| position: relative; | |
| background: ${darkMode ? 'var(--bg-surface)' : '#f9fafb'} !important; | |
| } | |
| .tour-header { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| margin-bottom: 20px; | |
| padding-bottom: 12px; | |
| border-bottom: 1px solid var(--border-light); | |
| } | |
| .tour-header-right { | |
| display: flex; | |
| gap: 8px; | |
| } | |
| .tour-progress { | |
| font-size: 0.75rem; | |
| font-weight: 700; | |
| color: var(--brand-primary); | |
| background: var(--bg-surface-hover); | |
| padding: 4px 10px; | |
| border-radius: 12px; | |
| font-family: 'Outfit', sans-serif; | |
| } | |
| .tour-btn-nav { | |
| border: none; | |
| padding: 6px 14px; | |
| border-radius: 8px; | |
| font-weight: 700; | |
| font-size: 0.85rem; | |
| cursor: pointer; | |
| transition: background 0.2s, color 0.2s; | |
| font-family: 'Outfit', sans-serif; | |
| outline: none !important; | |
| } | |
| .tour-btn-next-header { | |
| background: var(--brand-primary); | |
| color: white; | |
| } | |
| .tour-btn-next-header:hover { | |
| background: var(--brand-hover); | |
| } | |
| .tour-btn-back-header { | |
| background: var(--bg-surface-hover); | |
| color: var(--text-secondary); | |
| } | |
| .tour-btn-back-header:hover { | |
| color: var(--text-primary); | |
| background: var(--border-light); | |
| } | |
| .tour-title { | |
| font-family: 'Outfit', sans-serif; | |
| font-size: 1.2rem; | |
| font-weight: 800; | |
| margin: 0 0 10px 0; | |
| color: var(--text-primary); | |
| letter-spacing: -0.01em; | |
| } | |
| .tour-body { | |
| font-size: 0.95rem; | |
| line-height: 1.6; | |
| color: ${darkMode ? 'var(--text-secondary)' : '#4b5563'}; | |
| white-space: pre-wrap; | |
| font-weight: 600; | |
| } | |
| .tour-footer { | |
| margin-top: 16px; | |
| display: flex; | |
| justify-content: flex-start; | |
| } | |
| .tour-btn-skip { | |
| background: transparent; | |
| color: var(--text-tertiary); | |
| border: none; | |
| padding: 4px 0; | |
| font-weight: 600; | |
| font-size: 0.85rem; | |
| cursor: pointer; | |
| transition: color 0.2s; | |
| text-decoration: underline; | |
| text-underline-offset: 4px; | |
| font-family: 'Outfit', sans-serif; | |
| outline: none !important; | |
| } | |
| .tour-btn-skip:hover { | |
| color: var(--text-primary); | |
| } | |
| .kbd-key { | |
| background: ${darkMode ? '#2d2d2d' : '#ffffff'}; | |
| border: 1px solid ${darkMode ? '#444' : '#94a3b8'}; | |
| border-bottom-width: 3px; | |
| border-radius: 6px; | |
| padding: 1px 8px; | |
| font-size: 0.8rem; | |
| font-weight: 800; | |
| color: ${darkMode ? 'var(--text-primary)' : '#1e293b'}; | |
| font-family: 'Inter', system-ui, sans-serif; | |
| display: inline-flex; | |
| align-items: center; | |
| justify-content: center; | |
| min-width: 24px; | |
| height: 24px; | |
| margin: 0 2px; | |
| box-shadow: 0 2px 0 ${darkMode ? 'rgba(0,0,0,0.5)' : 'rgba(0,0,0,0.15)'}; | |
| } | |
| .tour-shortcuts-container { | |
| text-align: left; | |
| font-weight: 600; | |
| } | |
| .shortcuts-intro { | |
| margin-bottom: 12px; | |
| color: var(--text-primary); | |
| } | |
| .shortcuts-grid { | |
| display: grid; | |
| grid-template-columns: 20px 80px 10px 1fr; | |
| align-items: center; | |
| gap: 6px 0; | |
| } | |
| .kbd-bullet { | |
| color: var(--text-tertiary); | |
| font-size: 1.2rem; | |
| } | |
| .kbd-center { | |
| display: flex; | |
| justify-content: center; | |
| gap: 6px; | |
| } | |
| .kbd-colon { | |
| text-align: center; | |
| font-weight: 800; | |
| color: var(--text-secondary); | |
| } | |
| .kbd-text { | |
| padding-left: 8px; | |
| color: ${darkMode ? 'var(--text-secondary)' : '#4b5563'}; | |
| } | |
| `}} /> | |
| </div> | |
| ); | |
| }; | |
| return ( | |
| <Joyride | |
| key={tourVersion} | |
| steps={steps} | |
| run={run} | |
| stepIndex={stepIndex} | |
| continuous | |
| showSkipButton | |
| showProgress | |
| disableOverlayClose={true} | |
| disableBeacon={true} | |
| disableScrolling={false} | |
| tooltipComponent={CustomTooltip} | |
| callback={handleJoyrideCallback} | |
| styles={{ | |
| options: { | |
| arrowColor: 'transparent', | |
| overlayColor: 'rgba(0, 0, 0, 0.7)', | |
| zIndex: 10000, | |
| }, | |
| spotlight: { | |
| borderRadius: 8, | |
| border: `4px dashed ${darkMode ? 'rgba(255, 255, 255, 0.7)' : '#334155'}`, | |
| }, | |
| beacon: { | |
| display: 'none', | |
| } | |
| }} | |
| /> | |
| ); | |
| }; | |
| export default GuideTour; | |