代码修改
This commit is contained in:
1 parent
8f5af46ec2
commit
8f75e12871
168 files changed
+76575
No files matched your search
@@ -0,0 +1,660 @@
|
||||
import React, { useEffect, useMemo, useRef } from 'react';
|
||||
import { Bot, CheckCircle2, Database, FileCheck2, Sparkles, UserRound } from 'lucide-react';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import { useTheme } from '../../contexts/ThemeContext';
|
||||
import ExpertMatchCards from './ExpertMatchCards';
|
||||
import StreamErrorNotice from './StreamErrorNotice';
|
||||
import {
|
||||
ActorCardItem,
|
||||
BehaviorCardItem,
|
||||
InlineCollapsible,
|
||||
NarrativeCardItem,
|
||||
ParentGroupHeader,
|
||||
PurposeCardItem,
|
||||
STAGE_ICONS,
|
||||
StageHeader,
|
||||
ThinkingBlock,
|
||||
} from './SixStepCards';
|
||||
import type {
|
||||
ActorCard,
|
||||
BehaviorDriverCard,
|
||||
InfoStageData,
|
||||
NarrativeCard,
|
||||
PurposeCard,
|
||||
SixStepMessage,
|
||||
} from './types';
|
||||
|
||||
export interface ExpertMatchHandlers {
|
||||
onSelectExpert: (messageId: string, expertId: string) => void;
|
||||
onRefetchExpert: (messageId: string) => void;
|
||||
onCreateExpert: (messageId: string) => void;
|
||||
onConfirmExpert: (messageId: string) => void;
|
||||
}
|
||||
|
||||
interface SixStepConversationProps {
|
||||
messages: SixStepMessage[];
|
||||
isGenerating: boolean;
|
||||
activeMessageId?: string | null;
|
||||
expertHandlers?: ExpertMatchHandlers;
|
||||
}
|
||||
|
||||
interface AvatarProps {
|
||||
role: 'user' | 'assistant';
|
||||
active?: boolean;
|
||||
}
|
||||
|
||||
const Avatar: React.FC<AvatarProps> = ({ role, active }) => {
|
||||
const { theme } = useTheme();
|
||||
const isDark = theme === 'dark';
|
||||
const Icon = role === 'assistant' ? Bot : UserRound;
|
||||
return (
|
||||
<div className={`relative w-9 h-9 rounded-2xl flex items-center justify-center shrink-0 shadow-sm overflow-hidden ${
|
||||
role === 'assistant'
|
||||
? isDark
|
||||
? 'bg-gradient-to-br from-cyan-500 via-blue-600 to-violet-600 text-white'
|
||||
: 'bg-gradient-to-br from-red-400 via-rose-500 to-orange-400 text-white'
|
||||
: isDark
|
||||
? 'bg-gradient-to-br from-slate-500 via-slate-700 to-slate-900 text-white'
|
||||
: 'bg-gradient-to-br from-sky-400 via-indigo-500 to-violet-500 text-white'
|
||||
} ${active ? 'sixstep-pulse' : ''}`}>
|
||||
<Icon className="w-5 h-5" />
|
||||
{role === 'assistant' && active && (
|
||||
<Sparkles className="absolute -right-0.5 -top-0.5 w-3.5 h-3.5 text-yellow-200" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const InfoStage: React.FC<{ data: InfoStageData; running: boolean }> = ({ data, running }) => {
|
||||
const { theme } = useTheme();
|
||||
const isDark = theme === 'dark';
|
||||
return (
|
||||
<InlineCollapsible
|
||||
defaultOpen={false}
|
||||
title={
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`w-9 h-9 rounded-2xl flex items-center justify-center shrink-0 ${
|
||||
isDark
|
||||
? `bg-blue-500/15 text-blue-200 ${running ? 'sixstep-pulse' : ''}`
|
||||
: `bg-blue-50 text-blue-500 ${running ? 'sixstep-pulse' : ''}`
|
||||
}`}>
|
||||
<Database className="w-5 h-5" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className={`text-sm font-semibold flex items-center gap-2 ${isDark ? 'text-white' : 'text-gray-900'}`}>
|
||||
<span>信息准备</span>
|
||||
{data.complete && (
|
||||
<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>
|
||||
)}
|
||||
{running && (
|
||||
<span className={`text-[10px] font-medium px-1.5 py-0.5 rounded-full ${
|
||||
isDark ? 'bg-blue-500/20 text-blue-200' : 'bg-blue-50 text-blue-600'
|
||||
}`}>收集中</span>
|
||||
)}
|
||||
</div>
|
||||
<div className={`text-xs mt-0.5 ${
|
||||
running
|
||||
? isDark ? 'sixstep-shimmer-text' : 'sixstep-shimmer-text-light'
|
||||
: isDark ? 'text-slate-400' : 'text-gray-500'
|
||||
}`}>
|
||||
{running ? '正在收集热搜与上下文信息...' : '已整合信息准备素材'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{data.content ? (
|
||||
<div className={`prose prose-sm max-w-none text-sm leading-relaxed ${
|
||||
isDark ? 'prose-invert text-slate-300' : 'text-gray-700'
|
||||
}`}>
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>{data.content}</ReactMarkdown>
|
||||
</div>
|
||||
) : (
|
||||
<div className={`text-xs ${isDark ? 'text-slate-500' : 'text-gray-400'}`}>
|
||||
{running ? '正在获取信息...' : '无内容'}
|
||||
</div>
|
||||
)}
|
||||
</InlineCollapsible>
|
||||
);
|
||||
};
|
||||
|
||||
const MarkdownMessage: React.FC<{ content: string; isDark: boolean }> = ({ content, isDark }) => (
|
||||
<div className={`rounded-2xl border px-5 py-4 ${
|
||||
isDark
|
||||
? 'border-slate-700/50 bg-slate-800/30'
|
||||
: 'border-gray-100 bg-white'
|
||||
}`}>
|
||||
<div className={`prose prose-sm max-w-none leading-relaxed ${isDark ? 'prose-invert' : ''}`}>
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>{content}</ReactMarkdown>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const PurposeStage: React.FC<{ data: NonNullable<SixStepMessage['stages']>['purpose']; running: boolean }> = ({ data, running }) => {
|
||||
if (!data) return null;
|
||||
const Icon = STAGE_ICONS.purpose;
|
||||
const cards = data.cards ?? [];
|
||||
return (
|
||||
<InlineCollapsible
|
||||
defaultOpen
|
||||
title={
|
||||
<StageHeader
|
||||
title="目的生成"
|
||||
desc={running ? '正在推演核心目的...' : '已识别核心目的方向'}
|
||||
count={cards.length}
|
||||
loading={running}
|
||||
done={data.complete}
|
||||
icon={Icon}
|
||||
accent="purpose"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div className="space-y-3">
|
||||
<ThinkingBlock text={data.thinking} loading={running && !data.thinking} />
|
||||
<InlineCollapsible
|
||||
variant="subtle"
|
||||
defaultOpen
|
||||
title={
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs font-medium">目的卡片列表</span>
|
||||
<span className="text-[10px] opacity-70">{cards.length} 张</span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{cards.length === 0 ? (
|
||||
<div className="text-xs opacity-60">等待生成目的卡片...</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{cards.map((card) => (
|
||||
<PurposeCardItem key={card.id} card={card} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</InlineCollapsible>
|
||||
</div>
|
||||
</InlineCollapsible>
|
||||
);
|
||||
};
|
||||
|
||||
const ActorStage: React.FC<{
|
||||
data: NonNullable<SixStepMessage['stages']>['actor'];
|
||||
purposes: PurposeCard[];
|
||||
running: boolean;
|
||||
}> = ({ data, purposes, running }) => {
|
||||
if (!data) return null;
|
||||
const Icon = STAGE_ICONS.actor;
|
||||
const cards = data.cards ?? [];
|
||||
|
||||
const groups = useMemo(() => {
|
||||
const map = new Map<string, ActorCard[]>();
|
||||
cards.forEach((card) => {
|
||||
const list = map.get(card.purposeId) ?? [];
|
||||
list.push(card);
|
||||
map.set(card.purposeId, list);
|
||||
});
|
||||
return purposes
|
||||
.map((purpose) => ({ purpose, items: map.get(purpose.id) ?? [] }))
|
||||
.filter((g) => g.items.length > 0);
|
||||
}, [cards, purposes]);
|
||||
|
||||
return (
|
||||
<InlineCollapsible
|
||||
defaultOpen
|
||||
title={
|
||||
<StageHeader
|
||||
title="行为体生成"
|
||||
desc={running ? '正在匹配行为体角色...' : '已生成行为体清单'}
|
||||
count={cards.length}
|
||||
loading={running}
|
||||
done={data.complete}
|
||||
icon={Icon}
|
||||
accent="actor"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div className="space-y-3">
|
||||
<ThinkingBlock text={data.thinking} loading={running && !data.thinking} />
|
||||
<InlineCollapsible
|
||||
variant="subtle"
|
||||
defaultOpen
|
||||
title={
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs font-medium">行为体卡片列表</span>
|
||||
<span className="text-[10px] opacity-70">{cards.length} 张</span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{groups.length === 0 ? (
|
||||
<div className="text-xs opacity-60">等待生成行为体卡片...</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{groups.map((group) => (
|
||||
<div key={group.purpose.id} className="space-y-2 sixstep-fade-up">
|
||||
<ParentGroupHeader label="所属目的" parentTitle={group.purpose.title} count={group.items.length} variant="actor" />
|
||||
<div className="space-y-2 pl-2 border-l-2 border-dashed border-slate-400/20">
|
||||
{group.items.map((card) => (
|
||||
<ActorCardItem key={card.id} card={card} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</InlineCollapsible>
|
||||
</div>
|
||||
</InlineCollapsible>
|
||||
);
|
||||
};
|
||||
|
||||
const BehaviorStage: React.FC<{
|
||||
data: NonNullable<SixStepMessage['stages']>['behavior'];
|
||||
actors: ActorCard[];
|
||||
running: boolean;
|
||||
}> = ({ data, actors, running }) => {
|
||||
if (!data) return null;
|
||||
const Icon = STAGE_ICONS.behavior;
|
||||
const cards = data.cards ?? [];
|
||||
|
||||
const groups = useMemo(() => {
|
||||
const map = new Map<string, BehaviorDriverCard[]>();
|
||||
cards.forEach((card) => {
|
||||
const list = map.get(card.actorId) ?? [];
|
||||
list.push(card);
|
||||
map.set(card.actorId, list);
|
||||
});
|
||||
return actors
|
||||
.map((actor) => ({ actor, items: map.get(actor.id) ?? [] }))
|
||||
.filter((g) => g.items.length > 0);
|
||||
}, [cards, actors]);
|
||||
|
||||
return (
|
||||
<InlineCollapsible
|
||||
defaultOpen
|
||||
title={
|
||||
<StageHeader
|
||||
title="预期行为与驱动因素生成"
|
||||
desc={running ? '正在推演行为及驱动因素...' : '已生成预期行为与驱动因素'}
|
||||
count={cards.length}
|
||||
loading={running}
|
||||
done={data.complete}
|
||||
icon={Icon}
|
||||
accent="behavior"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div className="space-y-3">
|
||||
<ThinkingBlock text={data.thinking} loading={running && !data.thinking} />
|
||||
<InlineCollapsible
|
||||
variant="subtle"
|
||||
defaultOpen
|
||||
title={
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs font-medium">预期行为与驱动因素卡片列表</span>
|
||||
<span className="text-[10px] opacity-70">{cards.length} 张</span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{groups.length === 0 ? (
|
||||
<div className="text-xs opacity-60">等待生成预期行为与驱动因素卡片...</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{groups.map((group) => (
|
||||
<div key={group.actor.id} className="space-y-2 sixstep-fade-up">
|
||||
<ParentGroupHeader label="所属行为体" parentTitle={group.actor.title} count={group.items.length} variant="behavior" />
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-2 pl-2 border-l-2 border-dashed border-slate-400/20">
|
||||
{group.items.map((card) => (
|
||||
<BehaviorCardItem key={card.id} card={card} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</InlineCollapsible>
|
||||
</div>
|
||||
</InlineCollapsible>
|
||||
);
|
||||
};
|
||||
|
||||
const NarrativeStage: React.FC<{
|
||||
data: NonNullable<SixStepMessage['stages']>['narrative'];
|
||||
behaviors: BehaviorDriverCard[];
|
||||
running: boolean;
|
||||
}> = ({ data, behaviors, running }) => {
|
||||
if (!data) return null;
|
||||
const Icon = STAGE_ICONS.narrative;
|
||||
const cards = data.cards ?? [];
|
||||
|
||||
const groups = useMemo(() => {
|
||||
const map = new Map<string, NarrativeCard[]>();
|
||||
cards.forEach((card) => {
|
||||
const list = map.get(card.driverId) ?? [];
|
||||
list.push(card);
|
||||
map.set(card.driverId, list);
|
||||
});
|
||||
return behaviors
|
||||
.map((behavior) => ({ behavior, items: map.get(behavior.id) ?? [] }))
|
||||
.filter((g) => g.items.length > 0);
|
||||
}, [cards, behaviors]);
|
||||
|
||||
return (
|
||||
<InlineCollapsible
|
||||
defaultOpen
|
||||
title={
|
||||
<StageHeader
|
||||
title="叙事生成"
|
||||
desc={running ? '正在构建叙事框架...' : '已生成叙事卡片'}
|
||||
count={cards.length}
|
||||
loading={running}
|
||||
done={data.complete}
|
||||
icon={Icon}
|
||||
accent="narrative"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div className="space-y-3">
|
||||
<ThinkingBlock text={data.thinking} loading={running && !data.thinking} />
|
||||
<InlineCollapsible
|
||||
variant="subtle"
|
||||
defaultOpen
|
||||
title={
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs font-medium">叙事卡片列表</span>
|
||||
<span className="text-[10px] opacity-70">{cards.length} 张</span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{groups.length === 0 ? (
|
||||
<div className="text-xs opacity-60">等待生成叙事卡片...</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{groups.map((group) => (
|
||||
<div key={group.behavior.id} className="space-y-2 sixstep-fade-up">
|
||||
<ParentGroupHeader
|
||||
label="所属驱动因素"
|
||||
parentTitle={group.behavior.driver}
|
||||
count={group.items.length}
|
||||
variant="narrative"
|
||||
/>
|
||||
<div className="space-y-2 pl-2 border-l-2 border-dashed border-slate-400/20">
|
||||
{group.items.map((card) => (
|
||||
<NarrativeCardItem key={card.id} card={card} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</InlineCollapsible>
|
||||
</div>
|
||||
</InlineCollapsible>
|
||||
);
|
||||
};
|
||||
|
||||
const FinalAnswer: React.FC<{ text: string; running: boolean }> = ({ text, running }) => {
|
||||
const { theme } = useTheme();
|
||||
const isDark = theme === 'dark';
|
||||
if (!text && !running) return null;
|
||||
return (
|
||||
<div className={`rounded-2xl border p-4 ${
|
||||
isDark
|
||||
? 'border-slate-600/40 bg-slate-700/40 shadow-lg shadow-black/10'
|
||||
: 'border-red-100 bg-white shadow-sm'
|
||||
}`}>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<FileCheck2 className={`w-4 h-4 ${isDark ? 'text-emerald-300' : 'text-emerald-600'}`} />
|
||||
<span className={`text-sm font-semibold ${isDark ? 'text-white' : 'text-gray-900'}`}>最终结果</span>
|
||||
{running && (
|
||||
<span className={`text-[11px] ${isDark ? 'sixstep-shimmer-text' : 'sixstep-shimmer-text-light'}`}>
|
||||
正在汇总输出
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<pre className={`whitespace-pre-wrap text-sm leading-relaxed font-sans ${isDark ? 'text-slate-200' : 'text-gray-700'}`}>
|
||||
{text || '准备汇总输出...'}
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const SelectedSummaryPill: React.FC<{ message: SixStepMessage }> = ({ message }) => {
|
||||
const { theme } = useTheme();
|
||||
const isDark = theme === 'dark';
|
||||
if (!message.selectedExpert) return null;
|
||||
const mcps = message.selectedMcps ?? [];
|
||||
const labelMap: Record<string, string> = {
|
||||
purpose: '目的',
|
||||
actor: '行为体',
|
||||
behavior_factor: '预期行为和驱动因素',
|
||||
narrative: '叙事',
|
||||
};
|
||||
return (
|
||||
<div className={`flex flex-wrap items-center gap-2 rounded-2xl border px-3 py-2 ${
|
||||
isDark ? 'border-cyan-400/30 bg-cyan-500/10 text-cyan-100' : 'border-red-200 bg-red-50/70 text-red-700'
|
||||
}`}>
|
||||
<CheckCircle2 className="w-4 h-4" />
|
||||
<span className="text-xs font-semibold">已选专家</span>
|
||||
<span className={`text-xs font-medium px-2 py-0.5 rounded-full ${
|
||||
isDark ? 'bg-slate-900/40 text-cyan-100' : 'bg-white text-red-600 shadow-sm'
|
||||
}`}>
|
||||
{message.selectedExpert.name}
|
||||
</span>
|
||||
{mcps.length > 0 && (
|
||||
<>
|
||||
<span className={`text-[11px] ${isDark ? 'text-slate-300' : 'text-gray-500'}`}>·</span>
|
||||
<span className={`text-[11px] ${isDark ? 'text-slate-300' : 'text-gray-600'}`}>
|
||||
执行步骤:{mcps.map((k) => labelMap[k] || k).join(' → ')}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface AssistantMessageProps {
|
||||
message: SixStepMessage;
|
||||
active: boolean;
|
||||
expertHandlers?: ExpertMatchHandlers;
|
||||
}
|
||||
|
||||
const AssistantMessage: React.FC<AssistantMessageProps> = ({ message, active, expertHandlers }) => {
|
||||
const { theme } = useTheme();
|
||||
const isDark = theme === 'dark';
|
||||
|
||||
const isMarkdown = message.outputFormat === 'markdown';
|
||||
const isFollowUp = message.phase === 'follow-up';
|
||||
const stages = message.stages ?? {};
|
||||
const purposes = stages.purpose?.cards ?? [];
|
||||
const actors = stages.actor?.cards ?? [];
|
||||
const behaviors = stages.behavior?.cards ?? [];
|
||||
|
||||
const isInfoRunning = message.mainStepStatus?.info === 'running';
|
||||
const isPurposeRunning = message.subStepStatus?.purpose === 'running';
|
||||
const isActorRunning = message.subStepStatus?.actor === 'running';
|
||||
const isBehaviorRunning = message.subStepStatus?.behavior === 'running';
|
||||
const isNarrativeRunning = message.subStepStatus?.narrative === 'running';
|
||||
const isFinalRunning = message.mainStepStatus?.final === 'running';
|
||||
const generating =
|
||||
isInfoRunning ||
|
||||
message.mainStepStatus?.sixstep === 'running' ||
|
||||
isFinalRunning;
|
||||
// follow-up 在等待第一个 delta 时显示加载态
|
||||
const followUpGenerating = isFollowUp && active && !message.markdownContent;
|
||||
|
||||
const noStageYet = !stages.info && !stages.purpose && !stages.actor && !stages.behavior && !stages.narrative;
|
||||
|
||||
const expertMatch = message.expertMatch;
|
||||
const inExpertPhase = message.phase === 'expert-match' && !!expertMatch;
|
||||
const expertConfirmed = expertMatch?.status === 'confirmed' || !!message.selectedExpert;
|
||||
const expertActive = active && inExpertPhase && !expertConfirmed;
|
||||
|
||||
// Only render info/stages/finalAnswer when we're in an analysis phase.
|
||||
// Expert-match, clarifying, and follow-up phases show their own dedicated UI instead.
|
||||
const showAnalysisContent =
|
||||
!inExpertPhase &&
|
||||
message.phase !== 'clarifying' &&
|
||||
message.phase !== 'follow-up';
|
||||
|
||||
return (
|
||||
<div className="flex gap-3">
|
||||
<Avatar role="assistant" active={generating || expertActive || followUpGenerating} />
|
||||
<div className={`flex-1 min-w-0 max-w-full rounded-2xl px-4 py-3 border ${
|
||||
isDark
|
||||
? 'bg-slate-800/40 border-slate-700/50'
|
||||
: 'bg-transparent border-transparent shadow-none px-0 py-0'
|
||||
}`}>
|
||||
<div className="space-y-3">
|
||||
{/* Expert match phase: show expert cards before stages */}
|
||||
{inExpertPhase && expertMatch && (
|
||||
<ExpertMatchCards
|
||||
data={expertMatch}
|
||||
locked={expertConfirmed || !active}
|
||||
onSelectExpert={(id) => expertHandlers?.onSelectExpert(message.id, id)}
|
||||
onRefetch={() => expertHandlers?.onRefetchExpert(message.id)}
|
||||
onCreateExpert={() => expertHandlers?.onCreateExpert(message.id)}
|
||||
onConfirm={() => expertHandlers?.onConfirmExpert(message.id)}
|
||||
/>
|
||||
)}
|
||||
{inExpertPhase && (message.errors?.length ?? 0) > 0 && (
|
||||
<StreamErrorNotice errors={message.errors!} />
|
||||
)}
|
||||
|
||||
{/* Selected expert / MCP summary once moved to generate phase */}
|
||||
{message.selectedExpert && message.phase !== 'expert-match' && (
|
||||
<SelectedSummaryPill message={message} />
|
||||
)}
|
||||
|
||||
{showAnalysisContent && (
|
||||
<>
|
||||
{/* Info preparing placeholder when no stage data yet */}
|
||||
{isInfoRunning && noStageYet && (
|
||||
<div className={`rounded-2xl border px-4 py-3 ${
|
||||
isDark ? 'border-slate-700/60 bg-slate-900/40 text-slate-300' : 'border-blue-100 bg-white text-gray-600'
|
||||
}`}>
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<Database className={`w-4 h-4 ${isDark ? 'text-blue-300' : 'text-blue-500'}`} />
|
||||
<span className={isDark ? 'sixstep-shimmer-text' : 'sixstep-shimmer-text-light'}>
|
||||
正在调用信息准备,收集热搜与上下文...
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Info prepare result (collapsible) */}
|
||||
{stages.info && <InfoStage data={stages.info} running={isInfoRunning} />}
|
||||
|
||||
{(isMarkdown || isFollowUp) ? (
|
||||
/* ---- Markdown / Follow-up mode ---- */
|
||||
<>
|
||||
{(message.markdownContent || generating || followUpGenerating) && (
|
||||
message.markdownContent ? (
|
||||
<MarkdownMessage content={message.markdownContent} isDark={isDark} />
|
||||
) : (
|
||||
<div className={`rounded-2xl border px-4 py-3 ${
|
||||
isDark ? 'border-slate-700/60 bg-slate-900/40' : 'border-gray-100 bg-white'
|
||||
}`}>
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<Sparkles className={`w-4 h-4 ${isDark ? 'text-cyan-300' : 'text-red-500'}`} />
|
||||
<span className={isDark ? 'sixstep-shimmer-text' : 'sixstep-shimmer-text-light'}>
|
||||
{isFollowUp ? '正在回答你的问题...' : '正在生成推演内容...'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
{(message.errors?.length ?? 0) > 0 && (
|
||||
<StreamErrorNotice errors={message.errors!} />
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
/* ---- Structured mode ---- */
|
||||
<>
|
||||
{stages.purpose && <PurposeStage data={stages.purpose} running={isPurposeRunning} />}
|
||||
{stages.actor && <ActorStage data={stages.actor} purposes={purposes} running={isActorRunning} />}
|
||||
{stages.behavior && <BehaviorStage data={stages.behavior} actors={actors} running={isBehaviorRunning} />}
|
||||
{stages.narrative && (
|
||||
<NarrativeStage data={stages.narrative} behaviors={behaviors} running={isNarrativeRunning} />
|
||||
)}
|
||||
{(message.finalAnswer || isFinalRunning) && (
|
||||
<FinalAnswer text={message.finalAnswer ?? ''} running={isFinalRunning} />
|
||||
)}
|
||||
{(message.errors?.length ?? 0) > 0 && (
|
||||
<StreamErrorNotice errors={message.errors!} />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const UserMessage: React.FC<{ message: SixStepMessage }> = ({ message }) => {
|
||||
const { theme } = useTheme();
|
||||
const isDark = theme === 'dark';
|
||||
return (
|
||||
<div className="flex gap-3 justify-end">
|
||||
<div className={`max-w-[72%] rounded-2xl px-4 py-3 shadow-sm ${
|
||||
isDark ? 'bg-blue-600/70 text-white' : 'bg-gradient-to-r from-red-500 to-rose-500 text-white'
|
||||
}`}>
|
||||
<p className="whitespace-pre-wrap leading-relaxed text-sm">{message.content}</p>
|
||||
</div>
|
||||
<Avatar role="user" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const SixStepConversation: React.FC<SixStepConversationProps> = ({ messages, isGenerating, activeMessageId, expertHandlers }) => {
|
||||
const { theme } = useTheme();
|
||||
const isDark = theme === 'dark';
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const el = scrollRef.current;
|
||||
if (!el) return;
|
||||
el.scrollTop = el.scrollHeight;
|
||||
}, [messages, isGenerating]);
|
||||
|
||||
if (messages.length === 0) {
|
||||
return (
|
||||
<div className={`h-full flex flex-col items-center justify-center px-6 text-center ${
|
||||
isDark ? 'text-slate-400' : 'text-gray-500'
|
||||
}`}>
|
||||
<div className={`w-20 h-20 rounded-3xl mb-5 flex items-center justify-center ${
|
||||
isDark ? 'bg-slate-700/40 text-cyan-300' : 'bg-gradient-to-br from-red-50 to-orange-50 text-red-500'
|
||||
}`}>
|
||||
<Sparkles className="w-10 h-10 opacity-80" />
|
||||
</div>
|
||||
<p className={`text-lg font-medium ${isDark ? 'text-white' : 'text-gray-800'}`}>开始 6 步法推演</p>
|
||||
<p className="text-sm mt-2 max-w-md">
|
||||
输入问题或场景描述,模型会按照"信息准备 → 6 步法推演 → 最终结果"的流程逐步生成结构化输出。
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={scrollRef} className="h-full overflow-y-auto px-5 py-5 space-y-5">
|
||||
{messages.map((msg) => (
|
||||
<div key={msg.id}>
|
||||
{msg.role === 'user' ? (
|
||||
<UserMessage message={msg} />
|
||||
) : (
|
||||
<AssistantMessage
|
||||
message={msg}
|
||||
active={msg.id === activeMessageId}
|
||||
expertHandlers={expertHandlers}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SixStepConversation;
|
||||
Reference in new issue
Block a user