🐛 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:
@@ -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} />
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user