Files
deerflow-code/web-code/components/angentchat/SixStepCards.tsx
T
2026-05-27 19:43:15 +08:00

361 lines
14 KiB
TypeScript

import React from 'react';
import {
ChevronDown,
Compass,
FileSearch,
Megaphone,
ScrollText,
Sparkles,
Target,
TrendingUp,
UserCheck,
Users,
Zap,
} from 'lucide-react';
import { useTheme } from '../../contexts/ThemeContext';
import type {
ActorCard,
BehaviorDriverCard,
NarrativeCard,
PurposeCard,
} from './types';
interface CollapsibleProps {
title: React.ReactNode;
children: React.ReactNode;
defaultOpen?: boolean;
variant?: 'default' | 'subtle';
}
export const InlineCollapsible: React.FC<CollapsibleProps> = ({ title, children, defaultOpen = true, variant = 'default' }) => {
const { theme } = useTheme();
const isDark = theme === 'dark';
const [open, setOpen] = React.useState(defaultOpen);
const wrapperClass = variant === 'subtle'
? `${isDark ? 'border-slate-700/50 bg-slate-900/30' : 'border-gray-100 bg-white/80'}`
: `${isDark ? 'border-slate-700/60 bg-slate-800/40' : 'border-red-100 bg-white'}`;
return (
<div className={`rounded-2xl border overflow-hidden ${wrapperClass}`}>
<button
type="button"
onClick={() => setOpen((v) => !v)}
className={`w-full flex items-center justify-between gap-3 px-4 py-3 text-left transition-colors ${
isDark ? 'hover:bg-slate-800/70' : 'hover:bg-red-50/60'
}`}
>
<div className="flex-1 min-w-0">{title}</div>
<ChevronDown
className={`w-4 h-4 shrink-0 transition-transform duration-300 ${
open ? 'rotate-0' : '-rotate-90'
} ${isDark ? 'text-slate-400' : 'text-gray-500'}`}
/>
</button>
<div className={`grid transition-[grid-template-rows,opacity] duration-300 ease-out ${
open ? 'grid-rows-[1fr] opacity-100' : 'grid-rows-[0fr] opacity-0'
}`}>
<div className="overflow-hidden">
<div className="px-4 pb-4 pt-1">{children}</div>
</div>
</div>
</div>
);
};
interface CardIndexBadgeProps {
index: number;
variant: 'purpose' | 'actor' | 'behavior' | 'narrative';
}
const VARIANT_COLOR: Record<CardIndexBadgeProps['variant'], { dark: string; light: string }> = {
purpose: { dark: 'bg-violet-500/20 text-violet-200 border-violet-400/30', light: 'bg-orange-50 text-orange-600 border-orange-200' },
actor: { dark: 'bg-cyan-500/20 text-cyan-200 border-cyan-400/30', light: 'bg-red-50 text-red-600 border-red-200' },
behavior: { dark: 'bg-emerald-500/20 text-emerald-200 border-emerald-400/30', light: 'bg-emerald-50 text-emerald-600 border-emerald-200' },
narrative: { dark: 'bg-amber-500/20 text-amber-200 border-amber-400/30', light: 'bg-amber-50 text-amber-600 border-amber-200' },
};
const CardIndexBadge: React.FC<CardIndexBadgeProps> = ({ index, variant }) => {
const { theme } = useTheme();
const isDark = theme === 'dark';
const cls = VARIANT_COLOR[variant][isDark ? 'dark' : 'light'];
return (
<span className={`w-7 h-7 rounded-xl border flex items-center justify-center text-xs font-semibold shrink-0 ${cls}`}>
{index.toString().padStart(2, '0')}
</span>
);
};
export const PurposeCardItem: React.FC<{ card: PurposeCard }> = ({ card }) => {
const { theme } = useTheme();
const isDark = theme === 'dark';
return (
<div className={`group rounded-2xl border p-3 transition-all sixstep-fade-up ${
isDark
? 'border-violet-400/20 bg-gradient-to-br from-slate-800/60 to-violet-900/20 hover:border-violet-300/40'
: 'border-orange-100 bg-gradient-to-br from-white to-orange-50/40 hover:border-orange-200'
}`}>
<div className="flex items-start gap-3">
<CardIndexBadge index={card.index} variant="purpose" />
<div className={`w-7 h-7 rounded-xl flex items-center justify-center shrink-0 ${
isDark ? 'bg-violet-500/20 text-violet-200' : 'bg-orange-50 text-orange-500'
}`}>
<Target className="w-4 h-4" />
</div>
<div className="flex-1 min-w-0">
<div className={`text-sm font-semibold leading-snug ${isDark ? 'text-white' : 'text-gray-900'}`}>
{card.title}
</div>
<div className={`text-xs mt-1 leading-relaxed ${isDark ? 'text-slate-300' : 'text-gray-600'}`}>
<span className={`inline-flex items-center gap-1 mr-1 font-medium ${isDark ? 'text-violet-200' : 'text-orange-600'}`}>
<Compass className="w-3 h-3" />理由
</span>
{card.reason}
</div>
</div>
</div>
</div>
);
};
export const ActorCardItem: React.FC<{ card: ActorCard }> = ({ card }) => {
const { theme } = useTheme();
const isDark = theme === 'dark';
return (
<div className={`rounded-2xl border p-3 transition-all sixstep-fade-up ${
isDark
? 'border-cyan-400/20 bg-gradient-to-br from-slate-800/60 to-cyan-900/20 hover:border-cyan-300/40'
: 'border-red-100 bg-gradient-to-br from-white to-red-50/40 hover:border-red-200'
}`}>
<div className="flex items-start gap-3">
<CardIndexBadge index={card.index} variant="actor" />
<div className={`w-7 h-7 rounded-xl flex items-center justify-center shrink-0 ${
isDark ? 'bg-cyan-500/20 text-cyan-200' : 'bg-red-50 text-red-500'
}`}>
<UserCheck className="w-4 h-4" />
</div>
<div className="flex-1 min-w-0">
<div className={`text-sm font-semibold leading-snug ${isDark ? 'text-white' : 'text-gray-900'}`}>
{card.title}
</div>
<div className={`text-xs mt-1 leading-relaxed ${isDark ? 'text-slate-300' : 'text-gray-600'}`}>
{card.content}
</div>
</div>
</div>
</div>
);
};
export const BehaviorCardItem: React.FC<{ card: BehaviorDriverCard }> = ({ card }) => {
const { theme } = useTheme();
const isDark = theme === 'dark';
return (
<div className={`rounded-2xl border overflow-hidden transition-all sixstep-fade-up ${
isDark
? 'border-emerald-400/20 bg-gradient-to-br from-slate-800/60 to-emerald-900/20 hover:border-emerald-300/40'
: 'border-emerald-100 bg-gradient-to-br from-white to-emerald-50/40 hover:border-emerald-200'
}`}>
<div className="p-3">
<div className="flex items-start gap-3">
<CardIndexBadge index={card.index} variant="behavior" />
<div className={`w-7 h-7 rounded-xl flex items-center justify-center shrink-0 ${
isDark ? 'bg-emerald-500/20 text-emerald-200' : 'bg-emerald-50 text-emerald-600'
}`}>
<TrendingUp className="w-4 h-4" />
</div>
<div className="flex-1 min-w-0">
<div className={`text-[11px] uppercase tracking-wider font-medium mb-1 ${isDark ? 'text-emerald-300/80' : 'text-emerald-600'}`}>
预期行为
</div>
<div className={`text-sm font-semibold leading-snug ${isDark ? 'text-white' : 'text-gray-900'}`}>
{card.expectedBehavior}
</div>
</div>
</div>
</div>
<div className={`border-t px-3 py-3 ${
isDark ? 'border-emerald-400/15 bg-emerald-500/5' : 'border-emerald-100 bg-emerald-50/40'
}`}>
<div className="flex items-start gap-2">
<Zap className={`w-3.5 h-3.5 mt-0.5 shrink-0 ${isDark ? 'text-emerald-300' : 'text-emerald-600'}`} />
<div className="flex-1 min-w-0">
<div className={`text-[11px] uppercase tracking-wider font-medium mb-1 ${isDark ? 'text-emerald-300/80' : 'text-emerald-600'}`}>
驱动因素
</div>
<div className={`text-xs leading-relaxed ${isDark ? 'text-slate-200' : 'text-gray-700'}`}>
{card.driver}
</div>
</div>
</div>
</div>
</div>
);
};
export const NarrativeCardItem: React.FC<{ card: NarrativeCard }> = ({ card }) => {
const { theme } = useTheme();
const isDark = theme === 'dark';
return (
<div className={`rounded-2xl border p-3 transition-all sixstep-fade-up ${
isDark
? 'border-amber-400/20 bg-gradient-to-br from-slate-800/60 to-amber-900/20 hover:border-amber-300/40'
: 'border-amber-100 bg-gradient-to-br from-white to-amber-50/40 hover:border-amber-200'
}`}>
<div className="flex items-start gap-3">
<CardIndexBadge index={card.index} variant="narrative" />
<div className={`w-7 h-7 rounded-xl flex items-center justify-center shrink-0 ${
isDark ? 'bg-amber-500/20 text-amber-200' : 'bg-amber-50 text-amber-600'
}`}>
<ScrollText className="w-4 h-4" />
</div>
<div className="flex-1 min-w-0">
<div className={`text-sm font-semibold leading-snug ${isDark ? 'text-white' : 'text-gray-900'}`}>
{card.title}
</div>
<div className={`text-xs mt-1 leading-relaxed ${isDark ? 'text-slate-300' : 'text-gray-600'}`}>
<span className={`inline-flex items-center gap-1 mr-1 font-medium ${isDark ? 'text-amber-200' : 'text-amber-600'}`}>
<Sparkles className="w-3 h-3" />理由
</span>
{card.reason}
</div>
</div>
</div>
</div>
);
};
interface GroupHeaderProps {
label: string;
parentTitle: string;
count: number;
variant: 'actor' | 'behavior' | 'narrative';
}
export const ParentGroupHeader: React.FC<GroupHeaderProps> = ({ label, parentTitle, count, variant }) => {
const { theme } = useTheme();
const isDark = theme === 'dark';
const Icon = variant === 'actor' ? Target : variant === 'behavior' ? Users : Zap;
const accent = variant === 'actor'
? (isDark ? 'text-violet-200 bg-violet-500/15 border-violet-400/30' : 'text-orange-600 bg-orange-50 border-orange-200')
: variant === 'behavior'
? (isDark ? 'text-cyan-200 bg-cyan-500/15 border-cyan-400/30' : 'text-red-600 bg-red-50 border-red-200')
: (isDark ? 'text-emerald-200 bg-emerald-500/15 border-emerald-400/30' : 'text-emerald-600 bg-emerald-50 border-emerald-200');
return (
<div className={`flex items-center gap-2 px-3 py-1.5 rounded-full border w-fit ${accent}`}>
<Icon className="w-3.5 h-3.5" />
<span className="text-[11px] font-medium">{label}</span>
<span className={`text-xs font-semibold ${isDark ? 'text-white' : 'text-gray-900'} truncate max-w-[260px]`} title={parentTitle}>
{parentTitle}
</span>
<span className={`text-[10px] ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>· {count} 张</span>
</div>
);
};
interface ThinkingBlockProps {
text: string;
loading?: boolean;
empty?: string;
}
export const ThinkingBlock: React.FC<ThinkingBlockProps> = ({ text, loading, empty = '正在生成思考内容...' }) => {
const { theme } = useTheme();
const isDark = theme === 'dark';
return (
<div className={`rounded-2xl border px-4 py-3 ${
isDark ? 'bg-slate-900/30 border-slate-700/60' : 'bg-white border-gray-100'
}`}>
<div className="flex items-center gap-2 mb-2">
<FileSearch className={`w-3.5 h-3.5 ${isDark ? 'text-cyan-300' : 'text-red-500'}`} />
<span className={`text-[11px] font-medium uppercase tracking-wider ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
思考过程
</span>
{loading && (
<span className={`text-[11px] ${isDark ? 'sixstep-shimmer-text' : 'sixstep-shimmer-text-light'}`}>
生成中
</span>
)}
</div>
{text ? (
<pre className={`whitespace-pre-wrap text-xs leading-relaxed font-sans ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
{text}
</pre>
) : (
<div className={`text-xs ${isDark ? 'text-slate-500' : 'text-gray-400'}`}>{empty}</div>
)}
</div>
);
};
interface StageHeaderProps {
title: string;
desc?: string;
count?: number;
loading?: boolean;
done?: boolean;
icon: React.ComponentType<{ className?: string }>;
accent: 'purpose' | 'actor' | 'behavior' | 'narrative';
}
const ACCENT_BG: Record<StageHeaderProps['accent'], { dark: string; light: string }> = {
purpose: { dark: 'bg-violet-500/15 text-violet-200', light: 'bg-orange-50 text-orange-500' },
actor: { dark: 'bg-cyan-500/15 text-cyan-200', light: 'bg-red-50 text-red-500' },
behavior: { dark: 'bg-emerald-500/15 text-emerald-200', light: 'bg-emerald-50 text-emerald-600' },
narrative: { dark: 'bg-amber-500/15 text-amber-200', light: 'bg-amber-50 text-amber-600' },
};
export const StageHeader: React.FC<StageHeaderProps> = ({ title, desc, count, loading, done, icon: Icon, accent }) => {
const { theme } = useTheme();
const isDark = theme === 'dark';
const cls = ACCENT_BG[accent][isDark ? 'dark' : 'light'];
return (
<div className="flex items-center gap-3">
<div className={`w-9 h-9 rounded-2xl flex items-center justify-center ${cls} ${loading ? 'sixstep-pulse' : ''}`}>
<Icon 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'} flex items-center gap-2`}>
<span className="truncate">{title}</span>
{typeof count === 'number' && (
<span className={`text-[10px] font-medium px-1.5 py-0.5 rounded-full ${
isDark ? 'bg-slate-700 text-slate-300' : 'bg-gray-100 text-gray-600'
}`}>
{count} 张
</span>
)}
{done && (
<span className={`text-[10px] font-medium px-1.5 py-0.5 rounded-full ${
isDark ? 'bg-emerald-500/20 text-emerald-300' : 'bg-emerald-50 text-emerald-600'
}`}>
已完成
</span>
)}
{loading && (
<span className={`text-[10px] font-medium px-1.5 py-0.5 rounded-full ${
isDark ? 'bg-cyan-500/20 text-cyan-200' : 'bg-red-50 text-red-600'
}`}>
生成中
</span>
)}
</div>
{desc && (
<div className={`text-xs mt-0.5 ${
loading ? (isDark ? 'sixstep-shimmer-text' : 'sixstep-shimmer-text-light') : (isDark ? 'text-slate-400' : 'text-gray-500')
}`}>
{desc}
</div>
)}
</div>
</div>
);
};
export const STAGE_ICONS = {
purpose: Target,
actor: Users,
behavior: Zap,
narrative: Megaphone,
};