import { ArrowLeft, Send, Sparkles } from 'lucide-react'; import { useEffect, useRef, useState } from 'react'; import { type CreationAgentProgressCopy, normalizeCreationAgentProgress, resolveCreationAgentProgressHint, } from '../../services/creation-agent'; export type CreationAgentAnchorView = { key: string; label: string; value: string; status: string; }; export type CreationAgentMessageView = { id: string; role: string; kind?: string; text: string; createdAt?: string; }; export type CreationAgentOperationView = { operationId?: string; type?: string; status: string; phaseLabel: string; phaseDetail?: string; progress: number; error?: string | null; }; export type CreationAgentSessionView = { sessionId: string; title?: string | null; assistantSummary?: string | null; currentTurn: number; progressPercent: number; anchors: CreationAgentAnchorView[]; messages: CreationAgentMessageView[]; recommendedReplies?: string[]; }; export type CreationAgentTheme = { accentTextClass: string; accentBgClass: string; accentButtonClass: string; userBubbleClass: string; heroClass: string; anchorGridClass?: string; }; export type CreationAgentQuickAction = { key: string; label: string; minTurn?: number; minProgress?: number; showWhenComplete?: boolean; }; type CreationAgentWorkspaceProps = { session: CreationAgentSessionView | null; theme: CreationAgentTheme; loadingText: string; composerPlaceholder: string; primaryActionLabel: string; progressCopy?: CreationAgentProgressCopy; activeOperation?: CreationAgentOperationView | null; streamingReplyText?: string; isStreamingReply?: boolean; isBusy?: boolean; error?: string | null; quickActions?: CreationAgentQuickAction[]; onBack: () => void; onSubmitText: (text: string, quickActionKey?: string) => void; onPrimaryAction: () => void; onQuickAction?: (action: CreationAgentQuickAction) => void; }; const AUTO_SCROLL_FOLLOW_THRESHOLD_PX = 96; function uniqueRecommendedReplies(recommendedReplies: string[] = []) { return [...new Set(recommendedReplies.map((reply) => reply.trim()).filter(Boolean))].slice( 0, 3, ); } function CreationAgentOperationBanner({ operation, }: { operation: CreationAgentOperationView | null | undefined; }) { const [visibleOperation, setVisibleOperation] = useState(operation ?? null); useEffect(() => { setVisibleOperation(operation ?? null); if (operation?.status !== 'completed') { return; } const timeoutId = window.setTimeout(() => { setVisibleOperation((current) => current?.operationId === operation.operationId ? null : current, ); }, 1200); return () => window.clearTimeout(timeoutId); }, [operation]); if (!visibleOperation) { return null; } const isFailed = visibleOperation.status === 'failed'; const isRunning = visibleOperation.status === 'running' || visibleOperation.status === 'queued'; const bannerToneClass = isFailed ? 'platform-banner--danger' : isRunning ? 'platform-banner--info' : 'platform-banner--success'; const progress = normalizeCreationAgentProgress(visibleOperation.progress); const progressFillStyle = isFailed ? { background: 'linear-gradient(90deg, #fb7185 0%, #f43f5e 100%)' } : isRunning ? { background: 'var(--platform-button-primary-fill)' } : { background: 'linear-gradient(90deg, #86efac 0%, #34d399 100%)' }; return (
{visibleOperation.phaseLabel}
{progress}%
{visibleOperation.phaseDetail ? (
{visibleOperation.phaseDetail}
) : null} {visibleOperation.error ? (
{visibleOperation.error}
) : null}
); } function CreationAgentMessageBubble({ message, theme, recommendedReplies, isStreaming = false, onRecommendedReply, }: { message: CreationAgentMessageView; theme: CreationAgentTheme; recommendedReplies?: string[]; isStreaming?: boolean; onRecommendedReply: (text: string) => void; }) { const isUser = message.role === 'user'; const isSystem = message.role === 'system'; const visibleRecommendedReplies = isUser || isStreaming ? [] : uniqueRecommendedReplies(recommendedReplies); const bubbleToneClass = isUser ? theme.userBubbleClass : isSystem ? 'border border-[var(--platform-warm-border)] bg-[var(--platform-warm-bg)] text-[var(--platform-warm-text)]' : 'platform-subpanel text-[var(--platform-text-strong)]'; return (
{isStreaming ? ( message.text ? (
{message.text}
) : (
) ) : (
{message.text}
)} {visibleRecommendedReplies.length > 0 ? (
{visibleRecommendedReplies.map((reply, replyIndex) => ( ))}
) : null}
); } function shouldShowQuickAction( action: CreationAgentQuickAction, session: CreationAgentSessionView, progress: number, ) { if (action.showWhenComplete && progress < 100) { return false; } if (typeof action.minTurn === 'number' && session.currentTurn < action.minTurn) { return false; } if (typeof action.minProgress === 'number' && progress < action.minProgress) { return false; } return true; } function isMessageListNearBottom(container: HTMLDivElement) { return ( container.scrollHeight - container.scrollTop - container.clientHeight <= AUTO_SCROLL_FOLLOW_THRESHOLD_PX ); } function scrollMessageListToBottom(container: HTMLDivElement) { if (typeof container.scrollTo === 'function') { container.scrollTo({ top: container.scrollHeight, behavior: 'auto', }); return; } container.scrollTop = container.scrollHeight; } export function CreationAgentWorkspace({ session, theme, loadingText, composerPlaceholder, primaryActionLabel, progressCopy, activeOperation = null, streamingReplyText = '', isStreamingReply = false, isBusy = false, error = null, quickActions = [], onBack, onSubmitText, onPrimaryAction, onQuickAction, }: CreationAgentWorkspaceProps) { const [draftText, setDraftText] = useState(''); // 统一聊天区只在用户仍停留在底部附近时跟随新内容,避免流式回复持续抢走阅读位置。 const messageListRef = useRef(null); const shouldAutoScrollRef = useRef(true); useEffect(() => { const container = messageListRef.current; if (!container || !shouldAutoScrollRef.current) { return; } scrollMessageListToBottom(container); }, [session?.sessionId, session?.messages, streamingReplyText, isStreamingReply]); if (!session) { return (
{error || loadingText}
); } const progress = normalizeCreationAgentProgress(session.progressPercent); const hasHeroCopy = Boolean(session.title || session.assistantSummary); const canShowPrimaryAction = progress >= 100; const visibleQuickActions = quickActions.filter((action) => shouldShowQuickAction(action, session, progress), ); const streamingMessageId = `streaming-assistant-${session.sessionId}`; const shouldShowStreamingReply = isStreamingReply && streamingReplyText.trim(); const displayedMessages = shouldShowStreamingReply ? [ ...session.messages, { id: streamingMessageId, role: 'assistant', kind: 'chat', text: streamingReplyText, } satisfies CreationAgentMessageView, ] : session.messages; const lastAssistantMessageIndex = session.messages.reduce( (lastIndex, message, index) => message.role === 'assistant' ? index : lastIndex, -1, ); const armAutoScrollToBottom = () => { shouldAutoScrollRef.current = true; }; const handleMessageListScroll = () => { const container = messageListRef.current; if (!container) { return; } shouldAutoScrollRef.current = isMessageListNearBottom(container); }; const submitRecommendedReply = (text: string) => { armAutoScrollToBottom(); onSubmitText(text); }; const submit = () => { const text = draftText.trim(); if (!text || isBusy) { return; } armAutoScrollToBottom(); onSubmitText(text); setDraftText(''); }; return (
{canShowPrimaryAction ? ( ) : null}
{hasHeroCopy ? (
{session.title ? (
{session.title}
) : null} {session.assistantSummary ? (
{session.assistantSummary}
) : null}
) : null}
创作进度 {progress}%
{resolveCreationAgentProgressHint(progress, progressCopy)}
{visibleQuickActions.length > 0 ? (
{visibleQuickActions.map((action) => ( ))}
) : null}
{displayedMessages.length === 0 ? (
暂无消息
) : ( displayedMessages.map((message, index) => ( )) )}
{error ? (
{error}
) : null}