Files
doc_ai_frontend/src/components/UserGuide.tsx
2026-01-24 13:53:50 +08:00

175 lines
5.8 KiB
TypeScript

import React, { useState, useEffect, useCallback } from 'react';
import { X, ChevronRight, ChevronLeft } from 'lucide-react';
import { useLanguage } from '../contexts/LanguageContext';
interface Step {
id: string;
title: string;
content: string;
position: 'top' | 'bottom' | 'left' | 'right';
}
export default function UserGuide({ isOpen, onClose }: { isOpen: boolean; onClose: () => void }) {
const { t } = useLanguage();
const [currentStep, setCurrentStep] = useState(0);
const [highlightStyle, setHighlightStyle] = useState<React.CSSProperties>({});
const steps: Step[] = [
{
id: 'sidebar-upload-area',
title: t.guide.step1Title,
content: t.guide.step1Content,
position: 'right',
},
{
id: 'sidebar-history',
title: t.guide.step2Title,
content: t.guide.step2Content,
position: 'right',
},
{
id: 'file-preview-empty',
title: t.guide.step3Title,
content: t.guide.step3Content,
position: 'right',
},
{
id: 'result-empty-state',
title: t.guide.step4Title,
content: t.guide.step4Content,
position: 'left',
},
{
id: 'export-button',
title: t.guide.stepExportTitle,
content: t.guide.stepExportContent,
position: 'left',
},
];
const updateHighlight = useCallback(() => {
if (!isOpen || steps.length === 0) return;
const element = document.getElementById(steps[currentStep].id);
if (element) {
const rect = element.getBoundingClientRect();
setHighlightStyle({
top: rect.top - 8,
left: rect.left - 8,
width: rect.width + 16,
height: rect.height + 16,
opacity: 1,
});
element.scrollIntoView({ behavior: 'smooth', block: 'center' });
} else {
setHighlightStyle({ opacity: 0 });
}
}, [currentStep, isOpen, steps, t.guide]);
useEffect(() => {
if (isOpen) {
updateHighlight();
window.addEventListener('resize', updateHighlight);
}
return () => window.removeEventListener('resize', updateHighlight);
}, [isOpen, updateHighlight]);
if (!isOpen) return null;
const handleNext = () => {
if (currentStep < steps.length - 1) {
setCurrentStep(currentStep + 1);
} else {
onClose();
setCurrentStep(0);
}
};
const handlePrev = () => {
if (currentStep > 0) {
setCurrentStep(currentStep - 1);
}
};
return (
<div className="fixed inset-0 z-[100] pointer-events-none">
{/* Backdrop with hole */}
<div className="absolute inset-0 bg-black/60 pointer-events-auto" onClick={onClose} style={{
clipPath: highlightStyle.top !== undefined ? `polygon(
0% 0%, 0% 100%,
${highlightStyle.left}px 100%,
${highlightStyle.left}px ${highlightStyle.top}px,
${(highlightStyle.left as number) + (highlightStyle.width as number)}px ${highlightStyle.top}px,
${(highlightStyle.left as number) + (highlightStyle.width as number)}px ${(highlightStyle.top as number) + (highlightStyle.height as number)}px,
${highlightStyle.left}px ${(highlightStyle.top as number) + (highlightStyle.height as number)}px,
${highlightStyle.left}px 100%,
100% 100%, 100% 0%
)` : 'none'
}} />
{/* Highlight border */}
<div
className="absolute border-2 border-blue-500 rounded-xl transition-all duration-300 shadow-[0_0_0_9999px_rgba(0,0,0,0.5)]"
style={highlightStyle}
/>
{/* Tooltip */}
<div
className="absolute pointer-events-auto bg-white rounded-xl shadow-2xl p-6 w-80 transition-all duration-300 animate-in fade-in zoom-in-95"
style={highlightStyle.top !== undefined ? {
top: steps[currentStep].position === 'bottom'
? (highlightStyle.top as number) + (highlightStyle.height as number) + 16
: steps[currentStep].position === 'top'
? (highlightStyle.top as number) - 200 // approximation
: (highlightStyle.top as number),
left: steps[currentStep].position === 'right'
? (highlightStyle.left as number) + (highlightStyle.width as number) + 16
: steps[currentStep].position === 'left'
? (highlightStyle.left as number) - 336
: (highlightStyle.left as number),
} : {
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
}}
>
<button
onClick={onClose}
className="absolute top-4 right-4 text-gray-400 hover:text-gray-600"
>
<X size={18} />
</button>
<div className="mb-4">
<span className="text-xs font-bold text-blue-600 uppercase tracking-wider">
Step {currentStep + 1} of {steps.length}
</span>
<h3 className="text-lg font-bold text-gray-900 mt-1">{steps[currentStep].title}</h3>
<p className="text-sm text-gray-600 mt-2 leading-relaxed">
{steps[currentStep].content}
</p>
</div>
<div className="flex items-center justify-between mt-6">
<button
onClick={handlePrev}
disabled={currentStep === 0}
className={`flex items-center gap-1 text-sm font-medium ${currentStep === 0 ? 'text-gray-300 cursor-not-allowed' : 'text-gray-600 hover:text-gray-900'}`}
>
<ChevronLeft size={16} />
{t.guide.prev}
</button>
<button
onClick={handleNext}
className="px-4 py-2 bg-blue-600 text-white text-sm font-medium rounded-lg hover:bg-blue-700 transition-colors flex items-center gap-1"
>
{currentStep === steps.length - 1 ? t.guide.finish : t.guide.next}
{currentStep < steps.length - 1 && <ChevronRight size={16} />}
</button>
</div>
</div>
</div>
);
}