🐛 fix(playground): improve multimodal content handling and error resilience

Fix TypeError when processing multimodal messages containing both text and images.
The error "textContent.text.trim is not a function" occurred when textContent
was null or textContent.text was not a string type.

Changes:
- Add comprehensive type checking for textContent.text access
- Implement getTextContent() utility function for unified content extraction
- Enhance error handling to support multimodal content display
- Fix message copy functionality to handle array-format content
- Improve message reset functionality to extract text content for retry
- Add user-friendly warnings when copying messages without text content

Technical improvements:
- Validate textContent existence and text property type before calling trim()
- Extract text content from multimodal messages for operations like copy/retry
- Maintain backward compatibility with string-format content
- Preserve all existing functionality while adding robust error handling

Fixes issues with:
- Image + text message processing
- Message copying from multimodal content
- Message retry with image attachments
- Error display for complex message formats

This ensures the playground component handles multimodal content gracefully
without breaking existing text-only message functionality.
This commit is contained in:
Apple\Apple
2025-05-30 19:32:49 +08:00
parent c5ed0753a6
commit 9c5ab755c1
2 changed files with 71 additions and 13 deletions

View File

@@ -15,10 +15,23 @@ const MessageContent = ({ message, className, styleState, onToggleReasoningExpan
const { t } = useTranslation();
if (message.status === 'error') {
let errorText;
if (Array.isArray(message.content)) {
const textContent = message.content.find(item => item.type === 'text');
errorText = textContent && textContent.text && typeof textContent.text === 'string'
? textContent.text
: t('请求发生错误');
} else if (typeof message.content === 'string') {
errorText = message.content;
} else {
errorText = t('请求发生错误');
}
return (
<div className={`${className} flex items-center p-4 bg-red-50 rounded-xl`}>
<Typography.Text type="danger" className="text-sm">
{message.content || t('请求发生错误')}
{errorText}
</Typography.Text>
</div>
);
@@ -26,11 +39,23 @@ const MessageContent = ({ message, className, styleState, onToggleReasoningExpan
const isThinkingStatus = message.status === 'loading' || message.status === 'incomplete';
let currentExtractedThinkingContent = null;
let currentDisplayableFinalContent = message.content || "";
let currentDisplayableFinalContent = "";
let thinkingSource = null;
const getTextContent = (content) => {
if (Array.isArray(content)) {
const textItem = content.find(item => item.type === 'text');
return textItem && textItem.text && typeof textItem.text === 'string' ? textItem.text : '';
} else if (typeof content === 'string') {
return content;
}
return '';
};
currentDisplayableFinalContent = getTextContent(message.content);
if (message.role === 'assistant') {
let baseContentForDisplay = message.content || "";
let baseContentForDisplay = getTextContent(message.content);
let combinedThinkingContent = "";
if (message.reasoningContent) {
@@ -175,7 +200,6 @@ const MessageContent = ({ message, className, styleState, onToggleReasoningExpan
{/* 渲染消息内容 */}
{(() => {
// 处理多模态内容(文本+图片)
if (Array.isArray(message.content)) {
const textContent = message.content.find(item => item.type === 'text');
const imageContents = message.content.filter(item => item.type === 'image_url');
@@ -209,7 +233,7 @@ const MessageContent = ({ message, className, styleState, onToggleReasoningExpan
)}
{/* 显示文本内容 */}
{textContent && textContent.text && textContent.text.trim() !== '' && (
{textContent && textContent.text && typeof textContent.text === 'string' && textContent.text.trim() !== '' && (
<div className="prose prose-xs sm:prose-sm prose-gray max-w-none overflow-x-auto text-xs sm:text-sm">
<MarkdownRender raw={textContent.text} />
</div>
@@ -218,10 +242,8 @@ const MessageContent = ({ message, className, styleState, onToggleReasoningExpan
);
}
// 处理纯文本内容或助手回复
if (typeof message.content === 'string') {
if (message.role === 'assistant') {
// 助手回复使用处理后的内容
if (finalDisplayableFinalContent && finalDisplayableFinalContent.trim() !== '') {
return (
<div className="prose prose-xs sm:prose-sm prose-gray max-w-none overflow-x-auto text-xs sm:text-sm">
@@ -230,7 +252,6 @@ const MessageContent = ({ message, className, styleState, onToggleReasoningExpan
);
}
} else {
// 用户文本消息
return (
<div className="prose prose-xs sm:prose-sm prose-gray max-w-none overflow-x-auto text-xs sm:text-sm">
<MarkdownRender raw={message.content} />

View File

@@ -712,18 +712,41 @@ const Playground = () => {
const handleMessageCopy = useCallback((message) => {
if (!message.content) return;
let textToCopy;
if (Array.isArray(message.content)) {
const textContent = message.content.find(item => item.type === 'text');
if (textContent && textContent.text && typeof textContent.text === 'string') {
textToCopy = textContent.text;
} else {
Toast.warning({
content: t('此消息没有可复制的文本内容'),
duration: 2,
});
return;
}
} else if (typeof message.content === 'string') {
textToCopy = message.content;
} else {
Toast.warning({
content: t('无法复制此类型的消息内容'),
duration: 2,
});
return;
}
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(message.content).then(() => {
navigator.clipboard.writeText(textToCopy).then(() => {
Toast.success({
content: t('消息已复制到剪贴板'),
duration: 2,
});
}).catch(err => {
console.error('Clipboard API 复制失败:', err);
fallbackCopyToClipboard(message.content);
fallbackCopyToClipboard(textToCopy);
});
} else {
fallbackCopyToClipboard(message.content);
fallbackCopyToClipboard(textToCopy);
}
}, [t]);
@@ -790,7 +813,14 @@ const Playground = () => {
if (targetMessage.role === 'user') {
const newMessages = prevMessages.slice(0, messageIndex);
setTimeout(() => {
onMessageSend(targetMessage.content);
let contentToSend;
if (Array.isArray(targetMessage.content)) {
const textContent = targetMessage.content.find(item => item.type === 'text');
contentToSend = textContent && textContent.text ? textContent.text : '';
} else {
contentToSend = targetMessage.content;
}
onMessageSend(contentToSend);
}, 100);
return newMessages;
} else if (targetMessage.role === 'assistant') {
@@ -802,7 +832,14 @@ const Playground = () => {
const userMessage = prevMessages[userMessageIndex];
const newMessages = prevMessages.slice(0, userMessageIndex);
setTimeout(() => {
onMessageSend(userMessage.content);
let contentToSend;
if (Array.isArray(userMessage.content)) {
const textContent = userMessage.content.find(item => item.type === 'text');
contentToSend = textContent && textContent.text ? textContent.text : '';
} else {
contentToSend = userMessage.content;
}
onMessageSend(contentToSend);
}, 100);
return newMessages;
}