feat(组件): 消息气泡支持图形展示

MessageBubble.tsx:
- 集成 CodeExecutionResult 组件显示代码执行图片
- 添加 Pyodide 加载状态显示
- 支持 images 和 pyodideStatus 属性
- 新增 ToolResultDisplay 子组件处理工具结果

MarkdownRenderer.tsx:
- 修复图片组件属性传递问题
- 改用 spread 操作符传递所有 img 属性
This commit is contained in:
gaoziman 2025-12-19 20:20:33 +08:00
parent 58d288637a
commit e5c5593686
2 changed files with 71 additions and 5 deletions

View File

@ -5,8 +5,9 @@ import { Copy, ThumbsUp, ThumbsDown, RefreshCw, ChevronDown, ChevronUp, Brain, L
import { Avatar } from '@/components/ui/Avatar'; import { Avatar } from '@/components/ui/Avatar';
import { AILogo } from '@/components/ui/AILogo'; import { AILogo } from '@/components/ui/AILogo';
import { MarkdownRenderer } from '@/components/markdown/MarkdownRenderer'; import { MarkdownRenderer } from '@/components/markdown/MarkdownRenderer';
import { CodeExecutionResult, PyodideLoading } from '@/components/features/CodeExecutionResult';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import type { Message, User } from '@/types'; import type { Message, User, ToolResult } from '@/types';
interface MessageBubbleProps { interface MessageBubbleProps {
message: Message; message: Message;
@ -14,9 +15,17 @@ interface MessageBubbleProps {
thinkingContent?: string; thinkingContent?: string;
isStreaming?: boolean; isStreaming?: boolean;
error?: string; error?: string;
/** 代码执行产生的图片Base64 */
images?: string[];
/** Pyodide 加载状态 */
pyodideStatus?: {
stage: 'loading' | 'ready' | 'error';
message: string;
progress?: number;
};
} }
export function MessageBubble({ message, user, thinkingContent, isStreaming, error }: MessageBubbleProps) { export function MessageBubble({ message, user, thinkingContent, isStreaming, error, images, pyodideStatus }: MessageBubbleProps) {
const isUser = message.role === 'user'; const isUser = message.role === 'user';
const [thinkingExpanded, setThinkingExpanded] = useState(false); const [thinkingExpanded, setThinkingExpanded] = useState(false);
const [copied, setCopied] = useState(false); const [copied, setCopied] = useState(false);
@ -96,6 +105,34 @@ export function MessageBubble({ message, user, thinkingContent, isStreaming, err
) : null} ) : null}
</div> </div>
{/* 工具调用结果 - 代码执行图片展示 */}
{message.toolResults && message.toolResults.length > 0 && (
<div className="mt-4">
{message.toolResults.map((result, index) => (
<ToolResultDisplay key={index} result={result} />
))}
</div>
)}
{/* Pyodide 加载状态 */}
{pyodideStatus && (
<div className="mt-4">
<PyodideLoading
stage={pyodideStatus.stage}
message={pyodideStatus.message}
progress={pyodideStatus.progress}
/>
</div>
)}
{/* 代码执行图片(从 props 传入) */}
{images && images.length > 0 && (
<CodeExecutionResult
images={images}
success={true}
/>
)}
{/* 流式状态指示器 */} {/* 流式状态指示器 */}
{isStreaming && message.content && ( {isStreaming && message.content && (
<div className="flex items-center gap-2 mt-3 text-sm text-[var(--color-text-tertiary)]"> <div className="flex items-center gap-2 mt-3 text-sm text-[var(--color-text-tertiary)]">
@ -149,3 +186,32 @@ function ActionButton({ icon: Icon, title, onClick }: ActionButtonProps) {
</button> </button>
); );
} }
/**
*
*
*/
interface ToolResultDisplayProps {
result: ToolResult;
}
function ToolResultDisplay({ result }: ToolResultDisplayProps) {
// 只有代码执行工具才显示图片
if (result.toolName !== 'code_execution') {
return null;
}
// 如果没有图片,不显示
if (!result.images || result.images.length === 0) {
return null;
}
return (
<CodeExecutionResult
images={result.images}
engine={result.engine}
executionTime={result.executionTime}
success={!result.isError}
/>
);
}

View File

@ -196,11 +196,11 @@ const markdownComponents = {
}, },
// 图片 // 图片
img({ src, alt }: { src?: string; alt?: string }) { img(props: React.ImgHTMLAttributes<HTMLImageElement>) {
return ( return (
<img <img
src={src} {...props}
alt={alt || ''} alt={props.alt || ''}
className="max-w-full h-auto rounded-lg my-3" className="max-w-full h-auto rounded-lg my-3"
/> />
); );