From 770657838cf9390eccd21b2598bab2ae03fe75b0 Mon Sep 17 00:00:00 2001 From: sion123 <450702724@qq.com> Date: Sat, 16 May 2026 19:38:42 +0800 Subject: [PATCH] =?UTF-8?q?feat(chat):=20=E4=B8=BA=E5=AA=92=E4=BD=93?= =?UTF-8?q?=E5=B7=A5=E5=85=B7=E6=B7=BB=E5=8A=A0=E9=AA=A8=E6=9E=B6=E5=B1=8F?= =?UTF-8?q?=E5=8D=A0=E4=BD=8D=E5=B9=B6=E4=BC=98=E5=8C=96=E7=AE=A1=E7=BA=BF?= =?UTF-8?q?=E8=BF=9B=E5=BA=A6=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 ChatMessage 组件中为图片/视频生成工具添加骨架屏占位卡片 - 优化 PipelineProgress 组件,使用阶段标签和状态指示器替代进度条 - 添加骨架屏 shimmer 动画样式 --- .../src/components/chat/ChatMessage.tsx | 40 +++++++++--- .../src/components/chat/PipelineProgress.tsx | 62 ++++++++++++------- web/client/src/index.css | 10 +++ 3 files changed, 80 insertions(+), 32 deletions(-) diff --git a/web/client/src/components/chat/ChatMessage.tsx b/web/client/src/components/chat/ChatMessage.tsx index afeb82d..ee61835 100644 --- a/web/client/src/components/chat/ChatMessage.tsx +++ b/web/client/src/components/chat/ChatMessage.tsx @@ -1,7 +1,7 @@ import { useState, useCallback } from 'react'; import { cn } from '@/lib/utils'; import { renderMarkdown } from '@/lib/markdown'; -import { Bot, User, Copy, Trash2, ChevronDown, ChevronRight, RefreshCw, ArrowRight, Quote, Brain, X, ChevronLeft, ChevronRight as ChevronRightIcon, Play, Loader2, CheckCircle2, XCircle } from 'lucide-react'; +import { Bot, User, Copy, Trash2, ChevronDown, ChevronRight, RefreshCw, ArrowRight, Quote, Brain, X, ChevronLeft, ChevronRight as ChevronRightIcon, Play, Loader2, CheckCircle2, XCircle, Image as ImageIcon, Film } from 'lucide-react'; import { ConfirmDialog } from '@/components/ui/confirm'; import { useConfirm } from '@/hooks/useConfirm'; import { useToast } from '@/hooks/useToast'; @@ -59,6 +59,21 @@ function AssetLightbox({ assets, currentIndex, onClose, onNavigate }: { ); } +function SkeletonCard({ type }: { type: 'image' | 'video' }) { + return ( +
+
+ {type === 'image' ? ( + + ) : ( + + )} +
+
+
+ ); +} + type MessageState = 'streaming' | 'thinking' | 'done'; interface Props { @@ -124,6 +139,13 @@ export function ChatMessage({ message, isLast, isThinking, onRegenerate, onConti } } + // Media tool skeleton logic + const isMediaTool = toolName === 'generate_images' || toolName === 'generate_videos'; + const mediaType: 'image' | 'video' = toolName === 'generate_images' ? 'image' : 'video'; + const expectedCount = isMediaTool && message.toolInput?.count ? (message.toolInput.count as number) : 0; + const assetCount = message.assets?.length || 0; + const remainingCount = isRunning ? Math.max(0, expectedCount - assetCount) : 0; + return (
- {/* Assets grid - images and video thumbnails */} - {message.assets && message.assets.length > 0 && ( + {/* Assets + Skeleton cards grid */} + {(message.assets && message.assets.length > 0) || (isRunning && isMediaTool && remainingCount > 0) ? (
- {message.assets.map((asset, i) => ( + {message.assets?.map((asset, i) => ( ))} + {isRunning && isMediaTool && remainingCount > 0 && Array.from({ length: remainingCount }, (_, i) => ( + + ))}
- )} - - {/* Running animation - liquid fill wave */} - {isRunning && ( + ) : isRunning ? (
- )} + ) : null}
{/* Lightbox */} diff --git a/web/client/src/components/chat/PipelineProgress.tsx b/web/client/src/components/chat/PipelineProgress.tsx index f44487c..33a2caa 100644 --- a/web/client/src/components/chat/PipelineProgress.tsx +++ b/web/client/src/components/chat/PipelineProgress.tsx @@ -1,4 +1,5 @@ import { cn } from '@/lib/utils'; +import { CheckCircle2, Loader2 } from 'lucide-react'; interface Props { phase: string; @@ -8,34 +9,49 @@ interface Props { status?: string; } +const PHASES = [ + { key: 'images', label: '图片' }, + { key: 'upload', label: '上传' }, + { key: 'videos', label: '视频' }, + { key: 'tts', label: '配音' }, + { key: 'assemble', label: '成片' }, +]; + export function PipelineProgress({ phase, progress, currentItem, totalItems, status }: Props) { - const phaseLabel: Record = { - images: '生成图片', - upload: '上传素材', - videos: '生成视频', - tts: '配音', - assemble: '成片组装', - }; + const currentIdx = PHASES.findIndex(p => p.key === phase); return (
-
- - {phaseLabel[phase] || phase} - {currentItem && totalItems ? ` (${currentItem}/${totalItems})` : ''} - - {progress}% +
+ {PHASES.map((p, i) => { + const isDone = i < currentIdx || (i === currentIdx && progress >= 100); + const isActive = i === currentIdx && progress < 100; + + return ( +
+ {isDone ? ( + + ) : isActive ? ( + + ) : ( +
+ )} + {p.label} + {isActive && currentItem != null && totalItems != null && ( + + {currentItem}/{totalItems} + + )} +
+ ); + })}
-
-
-
- {status &&

{status}

} + {status &&

{status}

}
); } diff --git a/web/client/src/index.css b/web/client/src/index.css index c57817a..ed27c51 100644 --- a/web/client/src/index.css +++ b/web/client/src/index.css @@ -65,6 +65,16 @@ animation: liquid-glow 2.4s ease-in-out infinite; } +/* Skeleton card shimmer */ +@keyframes shimmer-slide { + 0% { transform: translateX(-100%); } + 100% { transform: translateX(100%); } +} +.skeleton-shimmer { + background: linear-gradient(90deg, transparent 0%, rgba(255, 255, 255, 0.5) 50%, transparent 100%); + animation: shimmer-slide 1.5s ease-in-out infinite; +} + /* GitHub-style Markdown - light theme */ @layer components { .markdown-body {