315 lines
16 KiB
TypeScript
315 lines
16 KiB
TypeScript
import React from 'react';
|
|
import {
|
|
CheckCircle2,
|
|
ChevronLeft,
|
|
ChevronRight,
|
|
Circle,
|
|
Database,
|
|
FileCheck2,
|
|
Loader2,
|
|
Megaphone,
|
|
Sparkles,
|
|
Target,
|
|
Users,
|
|
Workflow,
|
|
Zap,
|
|
} from 'lucide-react';
|
|
import { useTheme } from '../../contexts/ThemeContext';
|
|
import type { MainStepKey, StepStatus, SubStepKey } from './types';
|
|
|
|
interface SixStepProgressProps {
|
|
mainStatus: Record<MainStepKey, StepStatus>;
|
|
subStatus: Record<SubStepKey, StepStatus>;
|
|
collapsed?: boolean;
|
|
onToggleCollapse?: () => void;
|
|
}
|
|
|
|
interface StepItem<T extends string> {
|
|
key: T;
|
|
title: string;
|
|
desc: string;
|
|
icon: React.ComponentType<{ className?: string }>;
|
|
color: { dark: string; light: string };
|
|
}
|
|
|
|
const MAIN_STEPS: StepItem<MainStepKey>[] = [
|
|
{ key: 'info', title: '信息准备', desc: '梳理输入素材与上下文', icon: Database, color: { dark: 'text-sky-300 bg-sky-500/15 border-sky-400/40', light: 'text-sky-600 bg-sky-50 border-sky-200' } },
|
|
{ key: 'sixstep', title: '6 步法推演', desc: '依次完成四个阶段推演', icon: Workflow, color: { dark: 'text-violet-300 bg-violet-500/15 border-violet-400/40', light: 'text-violet-600 bg-violet-50 border-violet-200' } },
|
|
{ key: 'final', title: '最终结果生成', desc: '汇总各阶段产出,输出结论', icon: FileCheck2, color: { dark: 'text-emerald-300 bg-emerald-500/15 border-emerald-400/40', light: 'text-emerald-600 bg-emerald-50 border-emerald-200' } },
|
|
];
|
|
|
|
const SUB_STEPS: StepItem<SubStepKey>[] = [
|
|
{ key: 'purpose', title: '目的生成', desc: '识别核心目的方向', icon: Target, color: { dark: 'text-violet-300 bg-violet-500/15 border-violet-400/40', light: 'text-violet-600 bg-orange-50 border-orange-200' } },
|
|
{ key: 'actor', title: '行为体生成', desc: '匹配行为体角色', icon: Users, color: { dark: 'text-cyan-300 bg-cyan-500/15 border-cyan-400/40', light: 'text-red-600 bg-red-50 border-red-200' } },
|
|
{ key: 'behavior', title: '预期行为与驱动因素', desc: '推演行为与动因', icon: Zap, color: { dark: 'text-emerald-300 bg-emerald-500/15 border-emerald-400/40', light: 'text-emerald-600 bg-emerald-50 border-emerald-200' } },
|
|
{ key: 'narrative', title: '叙事生成', desc: '构建叙事框架', icon: Megaphone,color: { dark: 'text-amber-300 bg-amber-500/15 border-amber-400/40', light: 'text-amber-600 bg-amber-50 border-amber-200' } },
|
|
];
|
|
|
|
function dotColor(status: StepStatus, isDark: boolean) {
|
|
if (status === 'done') return isDark ? 'bg-emerald-400' : 'bg-emerald-500';
|
|
if (status === 'running') return isDark ? 'bg-cyan-400 sixstep-pulse' : 'bg-red-500 sixstep-pulse';
|
|
return isDark ? 'bg-slate-600' : 'bg-gray-300';
|
|
}
|
|
|
|
function StatusIcon({ status, isDark }: { status: StepStatus; isDark: boolean }) {
|
|
if (status === 'done') return <CheckCircle2 className={`w-4 h-4 ${isDark ? 'text-emerald-300' : 'text-emerald-500'}`} />;
|
|
if (status === 'running') return <Loader2 className={`w-4 h-4 sixstep-spin-soft ${isDark ? 'text-cyan-300' : 'text-red-500'}`} />;
|
|
return <Circle className={`w-3.5 h-3.5 ${isDark ? 'text-slate-500' : 'text-gray-300'}`} />;
|
|
}
|
|
|
|
function cardBorder(status: StepStatus, isDark: boolean) {
|
|
if (status === 'running') return isDark
|
|
? 'border-cyan-400/40 bg-cyan-500/5 shadow-[0_0_0_1px_rgba(34,211,238,0.15)]'
|
|
: 'border-red-300 bg-red-50/60 shadow-sm';
|
|
return isDark ? 'border-slate-700/60 bg-slate-800/40' : 'border-gray-200 bg-white';
|
|
}
|
|
|
|
function connectorColor(status: StepStatus, isDark: boolean) {
|
|
return status === 'done'
|
|
? isDark ? 'bg-emerald-400/40' : 'bg-emerald-300'
|
|
: isDark ? 'bg-slate-700/60' : 'bg-gray-200';
|
|
}
|
|
|
|
// ── Collapsed view ────────────────────────────────────────────────────────────
|
|
const CollapsedView: React.FC<{
|
|
mainStatus: Record<MainStepKey, StepStatus>;
|
|
subStatus: Record<SubStepKey, StepStatus>;
|
|
isDark: boolean;
|
|
onExpand: () => void;
|
|
}> = ({ mainStatus, subStatus, isDark, onExpand }) => {
|
|
const sixstepUnlocked = mainStatus.sixstep !== 'pending';
|
|
return (
|
|
<div className={`h-full flex flex-col items-center py-3 gap-3 border-r ${
|
|
isDark ? 'border-slate-700/50 bg-slate-800/30' : 'border-red-100 bg-gray-50/60'
|
|
}`}>
|
|
<button
|
|
type="button"
|
|
onClick={onExpand}
|
|
title="展开进度面板"
|
|
className={`w-9 h-9 rounded-xl flex items-center justify-center transition-colors ${
|
|
isDark ? 'text-slate-400 hover:text-white hover:bg-slate-700/60' : 'text-gray-500 hover:text-gray-800 hover:bg-gray-100'
|
|
}`}
|
|
>
|
|
<ChevronRight className="w-4 h-4" />
|
|
</button>
|
|
|
|
<div className="flex flex-col items-center gap-2 flex-1">
|
|
{MAIN_STEPS.map((step) => {
|
|
const status = mainStatus[step.key];
|
|
const Icon = step.icon;
|
|
const clr = step.color[isDark ? 'dark' : 'light'];
|
|
return (
|
|
<div key={step.key} className="relative flex flex-col items-center gap-1" title={step.title}>
|
|
<div className={`w-9 h-9 rounded-xl border flex items-center justify-center ${clr} ${
|
|
status === 'running' ? 'sixstep-pulse' : ''
|
|
}`}>
|
|
{status === 'running'
|
|
? <Loader2 className="w-4 h-4 sixstep-spin-soft" />
|
|
: <Icon className="w-4 h-4" />
|
|
}
|
|
</div>
|
|
<span className={`w-2 h-2 rounded-full ${dotColor(status, isDark)}`} />
|
|
|
|
{step.key === 'sixstep' && sixstepUnlocked && (
|
|
<div className="flex flex-col items-center gap-1 mt-1">
|
|
{SUB_STEPS.map((sub) => {
|
|
const ss = subStatus[sub.key];
|
|
const SubIcon = sub.icon;
|
|
const sc = sub.color[isDark ? 'dark' : 'light'];
|
|
return (
|
|
<div key={sub.key} title={sub.title} className={`w-7 h-7 rounded-lg border flex items-center justify-center ${sc} ${
|
|
ss === 'running' ? 'sixstep-pulse' : ''
|
|
}`}>
|
|
{ss === 'running'
|
|
? <Loader2 className="w-3 h-3 sixstep-spin-soft" />
|
|
: <SubIcon className="w-3 h-3" />
|
|
}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
// ── Expanded view ─────────────────────────────────────────────────────────────
|
|
const SixStepProgress: React.FC<SixStepProgressProps> = ({
|
|
mainStatus,
|
|
subStatus,
|
|
collapsed = false,
|
|
onToggleCollapse,
|
|
}) => {
|
|
const { theme } = useTheme();
|
|
const isDark = theme === 'dark';
|
|
const sixstepUnlocked = mainStatus.sixstep !== 'pending';
|
|
const sixstepRunning = mainStatus.sixstep === 'running';
|
|
|
|
if (collapsed) {
|
|
return (
|
|
<CollapsedView
|
|
mainStatus={mainStatus}
|
|
subStatus={subStatus}
|
|
isDark={isDark}
|
|
onExpand={() => onToggleCollapse?.()}
|
|
/>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className={`h-full flex flex-col overflow-hidden border-r ${isDark ? 'border-slate-700/50 bg-slate-800/30' : 'border-red-100 bg-gray-50/60'}`}>
|
|
{/* Header */}
|
|
<div className={`px-4 py-3 border-b flex items-center gap-2 ${isDark ? 'border-slate-700/50' : 'border-red-100'}`}>
|
|
<div className={`w-9 h-9 rounded-2xl flex items-center justify-center shrink-0 ${
|
|
isDark ? 'bg-cyan-500/15 text-cyan-300' : 'bg-red-50 text-red-500'
|
|
}`}>
|
|
<Sparkles className="w-5 h-5" />
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<div className={`text-sm font-semibold ${isDark ? 'text-white' : 'text-gray-900'}`}>生成进度</div>
|
|
<div className={`text-xs ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>实时跟踪推演阶段</div>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
onClick={onToggleCollapse}
|
|
title="收起进度面板"
|
|
className={`w-8 h-8 rounded-xl flex items-center justify-center transition-colors shrink-0 ${
|
|
isDark ? 'text-slate-400 hover:text-white hover:bg-slate-700/60' : 'text-gray-500 hover:text-gray-800 hover:bg-gray-100'
|
|
}`}
|
|
>
|
|
<ChevronLeft className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
|
|
{/* Steps */}
|
|
<div className="flex-1 overflow-y-auto px-4 py-4 space-y-3">
|
|
{MAIN_STEPS.map((step, idx) => {
|
|
const status = mainStatus[step.key];
|
|
const Icon = step.icon;
|
|
const clr = step.color[isDark ? 'dark' : 'light'];
|
|
const showSubBlock = step.key === 'sixstep' && sixstepUnlocked;
|
|
|
|
return (
|
|
<div key={step.key} className="relative">
|
|
{idx < MAIN_STEPS.length - 1 && (
|
|
<div className={`absolute left-[22px] top-12 bottom-[-12px] w-px ${connectorColor(status, isDark)}`} />
|
|
)}
|
|
<div className={`relative rounded-2xl border p-3 transition-all ${cardBorder(status, isDark)}`}>
|
|
<div className="flex items-start gap-3">
|
|
<div className={`w-11 h-11 rounded-2xl border flex items-center justify-center shrink-0 transition-colors ${clr} ${
|
|
status === 'running' ? 'sixstep-pulse' : ''
|
|
}`}>
|
|
{status === 'running'
|
|
? <Loader2 className="w-5 h-5 sixstep-spin-soft" />
|
|
: <Icon className="w-5 h-5" />
|
|
}
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex items-center justify-between gap-2">
|
|
<div className={`text-sm font-semibold truncate ${
|
|
status === 'running'
|
|
? isDark ? 'text-white' : 'text-gray-900'
|
|
: status === 'done'
|
|
? isDark ? 'text-slate-100' : 'text-gray-800'
|
|
: isDark ? 'text-slate-300' : 'text-gray-600'
|
|
}`}>{step.title}</div>
|
|
<span className={`px-2 py-0.5 rounded-full text-[10px] flex items-center gap-1 ${
|
|
status === 'done'
|
|
? isDark ? 'bg-emerald-500/20 text-emerald-300 border border-emerald-400/30' : 'bg-emerald-50 text-emerald-600 border border-emerald-200'
|
|
: status === 'running'
|
|
? isDark ? 'bg-cyan-500/20 text-cyan-200 border border-cyan-400/40' : 'bg-red-50 text-red-600 border border-red-200'
|
|
: isDark ? 'bg-slate-700/40 text-slate-400 border border-slate-600/40' : 'bg-gray-100 text-gray-500 border border-gray-200'
|
|
}`}>
|
|
<StatusIcon status={status} isDark={isDark} />
|
|
<span>{status === 'done' ? '已完成' : status === 'running' ? '生成中' : '待开始'}</span>
|
|
</span>
|
|
</div>
|
|
<div className={`text-xs mt-1 ${
|
|
status === 'running'
|
|
? isDark ? 'sixstep-shimmer-text' : 'sixstep-shimmer-text-light'
|
|
: isDark ? 'text-slate-400' : 'text-gray-500'
|
|
}`}>{step.desc}</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 6步法子阶段 */}
|
|
{showSubBlock && (
|
|
<div className="mt-3 pl-1">
|
|
<div className={`text-[10px] font-medium tracking-wider uppercase mb-2 ${
|
|
isDark ? 'text-slate-500' : 'text-gray-400'
|
|
}`}>
|
|
6 步法 · 子阶段
|
|
</div>
|
|
<div className="space-y-2">
|
|
{SUB_STEPS.map((sub, subIdx) => {
|
|
const ss = subStatus[sub.key];
|
|
const SubIcon = sub.icon;
|
|
const sc = sub.color[isDark ? 'dark' : 'light'];
|
|
return (
|
|
<div key={sub.key} className="relative pl-4 sixstep-fade-up">
|
|
{subIdx < SUB_STEPS.length - 1 && (
|
|
<div className={`absolute left-[7px] top-7 bottom-[-8px] w-px ${connectorColor(ss, isDark)}`} />
|
|
)}
|
|
<div className={`absolute left-0 top-3 w-3.5 h-3.5 rounded-full border-2 flex items-center justify-center ${
|
|
ss === 'running'
|
|
? isDark ? 'bg-cyan-500/30 border-cyan-300' : 'bg-red-100 border-red-400'
|
|
: ss === 'done'
|
|
? isDark ? 'bg-emerald-500/30 border-emerald-300' : 'bg-emerald-100 border-emerald-400'
|
|
: isDark ? 'bg-slate-900 border-slate-600' : 'bg-white border-gray-300'
|
|
}`} />
|
|
<div className={`flex items-start gap-2 rounded-xl border p-2 ${
|
|
ss === 'running'
|
|
? isDark ? 'border-cyan-400/40 bg-cyan-500/5' : 'border-red-300 bg-red-50/70'
|
|
: isDark ? 'border-slate-700/50 bg-slate-800/40' : 'border-gray-200 bg-white'
|
|
}`}>
|
|
<div className={`w-7 h-7 rounded-lg border flex items-center justify-center shrink-0 ${sc} ${
|
|
ss === 'running' ? 'sixstep-pulse' : ''
|
|
}`}>
|
|
{ss === 'running'
|
|
? <Loader2 className="w-3.5 h-3.5 sixstep-spin-soft" />
|
|
: <SubIcon className="w-3.5 h-3.5" />
|
|
}
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex items-center justify-between gap-2">
|
|
<div className={`text-xs font-semibold truncate ${
|
|
ss === 'done' ? isDark ? 'text-slate-100' : 'text-gray-800'
|
|
: ss === 'running' ? isDark ? 'text-white' : 'text-gray-900'
|
|
: isDark ? 'text-slate-300' : 'text-gray-600'
|
|
}`}>{sub.title}</div>
|
|
<StatusIcon status={ss} isDark={isDark} />
|
|
</div>
|
|
<div className={`text-[11px] mt-0.5 ${
|
|
ss === 'running'
|
|
? isDark ? 'sixstep-shimmer-text' : 'sixstep-shimmer-text-light'
|
|
: isDark ? 'text-slate-500' : 'text-gray-400'
|
|
}`}>{sub.desc}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
{sixstepRunning && (
|
|
<div className={`mt-2 text-[11px] flex items-center gap-1.5 ${isDark ? 'text-cyan-300' : 'text-red-500'}`}>
|
|
<Loader2 className="w-3 h-3 sixstep-spin-soft" />
|
|
<span className={isDark ? 'sixstep-shimmer-text' : 'sixstep-shimmer-text-light'}>推演正在进行...</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default SixStepProgress;
|