Initial commit: Pixel AI comic/video creation platform
- FastAPI backend with SQLModel, Alembic migrations, AgentScope agents - Next.js 15 frontend with React 19, Tailwind, Zustand, React Flow - Multi-provider AI system (DashScope, Kling, MiniMax, Volcengine, OpenAI, etc.) - All HTTP clients migrated from sync requests to async httpx - Admin-managed API keys via environment variables - SSRF vulnerability fixed in ensure_url()
This commit is contained in:
270
frontend/src/components/canvas/controls/OnboardingTour.tsx
Normal file
270
frontend/src/components/canvas/controls/OnboardingTour.tsx
Normal file
@@ -0,0 +1,270 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { X, ChevronRight, ChevronLeft, Sparkles, MousePointer, Keyboard, Zap, Check } from 'lucide-react';
|
||||
import { cn } from '@/utils';
|
||||
|
||||
interface OnboardingStep {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
icon: React.ReactNode;
|
||||
target?: string;
|
||||
position?: 'top' | 'bottom' | 'left' | 'right' | 'center';
|
||||
}
|
||||
|
||||
const onboardingSteps: OnboardingStep[] = [
|
||||
{
|
||||
id: 'welcome',
|
||||
title: '欢迎使用 Pixel',
|
||||
description: 'Pixel 是一个 AI 驱动的漫画和视频创作平台。让我们花几分钟了解一下核心功能。',
|
||||
icon: <Sparkles className="w-6 h-6 text-cyan-400" />,
|
||||
position: 'center'
|
||||
},
|
||||
{
|
||||
id: 'canvas',
|
||||
title: '画布工作区',
|
||||
description: '这是你的创作画布。你可以在这里添加节点、连接它们来构建创作流程。拖拽左侧的节点到画布上开始创作。',
|
||||
icon: <MousePointer className="w-6 h-6 text-purple-400" />,
|
||||
target: '.react-flow__pane',
|
||||
position: 'center'
|
||||
},
|
||||
{
|
||||
id: 'sidebar',
|
||||
title: '左侧工具栏',
|
||||
description: '这里提供了各种 AI 生成节点:图像生成、视频生成、剧本处理等。点击或拖拽到画布上使用。',
|
||||
icon: <Zap className="w-6 h-6 text-yellow-400" />,
|
||||
target: '[data-panel="left-dock"]',
|
||||
position: 'right'
|
||||
},
|
||||
{
|
||||
id: 'shortcuts',
|
||||
title: '快捷键',
|
||||
description: '使用快捷键可以提高创作效率。Ctrl+S 保存,Ctrl+Z 撤销,Ctrl+A 全选。点击右下角的键盘图标查看全部快捷键。',
|
||||
icon: <Keyboard className="w-6 h-6 text-green-400" />,
|
||||
target: '[data-shortcut-button]',
|
||||
position: 'top'
|
||||
},
|
||||
{
|
||||
id: 'complete',
|
||||
title: '准备开始创作',
|
||||
description: '你已经了解了基本操作。开始你的第一个 AI 创作项目吧!如有问题,随时查看帮助文档。',
|
||||
icon: <Check className="w-6 h-6 text-emerald-400" />,
|
||||
position: 'center'
|
||||
}
|
||||
];
|
||||
|
||||
const STORAGE_KEY = 'pixel-onboarding-completed';
|
||||
|
||||
export function OnboardingTour() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [currentStep, setCurrentStep] = useState(0);
|
||||
const [hasCompleted, setHasCompleted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Check if user has completed onboarding
|
||||
const completed = localStorage.getItem(STORAGE_KEY);
|
||||
if (!completed) {
|
||||
// Show onboarding after a short delay
|
||||
const timer = setTimeout(() => {
|
||||
setIsOpen(true);
|
||||
}, 1000);
|
||||
return () => clearTimeout(timer);
|
||||
} else {
|
||||
setHasCompleted(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleComplete = useCallback(() => {
|
||||
localStorage.setItem(STORAGE_KEY, 'true');
|
||||
setHasCompleted(true);
|
||||
setIsOpen(false);
|
||||
}, []);
|
||||
|
||||
const handleNext = useCallback(() => {
|
||||
if (currentStep < onboardingSteps.length - 1) {
|
||||
setCurrentStep(prev => prev + 1);
|
||||
} else {
|
||||
handleComplete();
|
||||
}
|
||||
}, [currentStep, handleComplete]);
|
||||
|
||||
const handlePrev = useCallback(() => {
|
||||
if (currentStep > 0) {
|
||||
setCurrentStep(prev => prev - 1);
|
||||
}
|
||||
}, [currentStep]);
|
||||
|
||||
const handleSkip = useCallback(() => {
|
||||
localStorage.setItem(STORAGE_KEY, 'true');
|
||||
setHasCompleted(true);
|
||||
setIsOpen(false);
|
||||
}, []);
|
||||
|
||||
const handleRestart = useCallback(() => {
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
setHasCompleted(false);
|
||||
setCurrentStep(0);
|
||||
setIsOpen(true);
|
||||
}, []);
|
||||
|
||||
const step = onboardingSteps[currentStep];
|
||||
const isFirstStep = currentStep === 0;
|
||||
const isLastStep = currentStep === onboardingSteps.length - 1;
|
||||
const progress = ((currentStep + 1) / onboardingSteps.length) * 100;
|
||||
|
||||
if (!isOpen) {
|
||||
// Show restart button if user has completed but wants to see it again
|
||||
if (hasCompleted) {
|
||||
return (
|
||||
<button
|
||||
onClick={handleRestart}
|
||||
className="fixed bottom-6 left-6 z-50 p-2 bg-black/40 hover:bg-black/60 backdrop-blur-md border border-white/10 rounded-full text-muted-foreground hover:text-white transition-all"
|
||||
title="重新查看新手引导"
|
||||
>
|
||||
<Sparkles size={18} />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Backdrop */}
|
||||
<AnimatePresence>
|
||||
{isOpen && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="fixed inset-0 z-[9998] bg-black/60 backdrop-blur-sm"
|
||||
onClick={handleSkip}
|
||||
/>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Onboarding Card */}
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key={step.id}
|
||||
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||
exit={{ opacity: 0, scale: 0.95, y: -20 }}
|
||||
transition={{ duration: 0.3, ease: [0.23, 1, 0.32, 1] }}
|
||||
className={cn(
|
||||
"fixed z-[9999] w-[400px] bg-black/80 backdrop-blur-xl border border-white/10 rounded-2xl shadow-2xl overflow-hidden",
|
||||
step.position === 'center' && "top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2",
|
||||
step.position === 'top' && "bottom-24 right-6",
|
||||
step.position === 'bottom' && "top-24 left-1/2 -translate-x-1/2",
|
||||
step.position === 'left' && "top-1/2 right-6 -translate-y-1/2",
|
||||
step.position === 'right' && "top-1/2 left-6 -translate-y-1/2"
|
||||
)}
|
||||
>
|
||||
{/* Progress bar */}
|
||||
<div className="absolute top-0 left-0 right-0 h-1 bg-white/5">
|
||||
<motion.div
|
||||
className="h-full bg-gradient-to-r from-cyan-500 to-purple-500"
|
||||
initial={{ width: 0 }}
|
||||
animate={{ width: `${progress}%` }}
|
||||
transition={{ duration: 0.3 }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Close button */}
|
||||
<button
|
||||
onClick={handleSkip}
|
||||
className="absolute top-3 right-3 p-1.5 text-muted-foreground hover:text-white hover:bg-white/10 rounded-lg transition-colors"
|
||||
>
|
||||
<X size={16} />
|
||||
</button>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-6">
|
||||
{/* Icon */}
|
||||
<div className="w-14 h-14 rounded-xl bg-white/5 border border-white/10 flex items-center justify-center mb-4">
|
||||
{step.icon}
|
||||
</div>
|
||||
|
||||
{/* Title */}
|
||||
<h3 className="text-lg font-semibold text-white mb-2">
|
||||
{step.title}
|
||||
</h3>
|
||||
|
||||
{/* Description */}
|
||||
<p className="text-sm text-muted-foreground leading-relaxed mb-6">
|
||||
{step.description}
|
||||
</p>
|
||||
|
||||
{/* Step indicator */}
|
||||
<div className="flex items-center justify-center gap-1.5 mb-6">
|
||||
{onboardingSteps.map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={cn(
|
||||
"w-2 h-2 rounded-full transition-colors",
|
||||
index === currentStep
|
||||
? "bg-cyan-400"
|
||||
: index < currentStep
|
||||
? "bg-white/40"
|
||||
: "bg-white/10"
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Navigation */}
|
||||
<div className="flex items-center justify-between">
|
||||
<button
|
||||
onClick={handlePrev}
|
||||
disabled={isFirstStep}
|
||||
className={cn(
|
||||
"flex items-center gap-1 px-3 py-2 text-sm rounded-lg transition-colors",
|
||||
isFirstStep
|
||||
? "text-muted-foreground/50 cursor-not-allowed"
|
||||
: "text-muted-foreground hover:text-white hover:bg-white/10"
|
||||
)}
|
||||
>
|
||||
<ChevronLeft size={16} />
|
||||
上一步
|
||||
</button>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{!isLastStep && (
|
||||
<button
|
||||
onClick={handleSkip}
|
||||
className="px-3 py-2 text-sm text-muted-foreground hover:text-white transition-colors"
|
||||
>
|
||||
跳过
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={handleNext}
|
||||
className="flex items-center gap-1 px-4 py-2 bg-white text-black text-sm font-medium rounded-lg hover:bg-white/90 transition-colors"
|
||||
>
|
||||
{isLastStep ? '完成' : '下一步'}
|
||||
{!isLastStep && <ChevronRight size={16} />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function ResetOnboardingButton() {
|
||||
const handleReset = () => {
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={handleReset}
|
||||
className="text-xs text-muted-foreground hover:text-white underline"
|
||||
>
|
||||
重新显示新手引导
|
||||
</button>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user