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:
张鹏
2026-04-29 01:20:12 +08:00
commit f9f4560459
808 changed files with 151724 additions and 0 deletions

View 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>
);
}