代码修改
This commit is contained in:
1 parent
8f5af46ec2
commit
8f75e12871
168 files changed
+76575
No files matched your search
@@ -0,0 +1,360 @@
|
||||
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,
|
||||
};
|
||||
Reference in new issue
Block a user