🐛 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();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
if (message.status === 'error') {
|
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 (
|
return (
|
||||||
<div className={`${className} flex items-center p-4 bg-red-50 rounded-xl`}>
|
<div className={`${className} flex items-center p-4 bg-red-50 rounded-xl`}>
|
||||||
<Typography.Text type="danger" className="text-sm">
|
<Typography.Text type="danger" className="text-sm">
|
||||||
{message.content || t('请求发生错误')}
|
{errorText}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -26,11 +39,23 @@ const MessageContent = ({ message, className, styleState, onToggleReasoningExpan
|
|||||||
|
|
||||||
const isThinkingStatus = message.status === 'loading' || message.status === 'incomplete';
|
const isThinkingStatus = message.status === 'loading' || message.status === 'incomplete';
|
||||||
let currentExtractedThinkingContent = null;
|
let currentExtractedThinkingContent = null;
|
||||||
let currentDisplayableFinalContent = message.content || "";
|
let currentDisplayableFinalContent = "";
|
||||||
let thinkingSource = null;
|
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') {
|
if (message.role === 'assistant') {
|
||||||
let baseContentForDisplay = message.content || "";
|
let baseContentForDisplay = getTextContent(message.content);
|
||||||
let combinedThinkingContent = "";
|
let combinedThinkingContent = "";
|
||||||
|
|
||||||
if (message.reasoningContent) {
|
if (message.reasoningContent) {
|
||||||
@@ -175,7 +200,6 @@ const MessageContent = ({ message, className, styleState, onToggleReasoningExpan
|
|||||||
|
|
||||||
{/* 渲染消息内容 */}
|
{/* 渲染消息内容 */}
|
||||||
{(() => {
|
{(() => {
|
||||||
// 处理多模态内容(文本+图片)
|
|
||||||
if (Array.isArray(message.content)) {
|
if (Array.isArray(message.content)) {
|
||||||
const textContent = message.content.find(item => item.type === 'text');
|
const textContent = message.content.find(item => item.type === 'text');
|
||||||
const imageContents = message.content.filter(item => item.type === 'image_url');
|
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">
|
<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} />
|
<MarkdownRender raw={textContent.text} />
|
||||||
</div>
|
</div>
|
||||||
@@ -218,10 +242,8 @@ const MessageContent = ({ message, className, styleState, onToggleReasoningExpan
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理纯文本内容或助手回复
|
|
||||||
if (typeof message.content === 'string') {
|
if (typeof message.content === 'string') {
|
||||||
if (message.role === 'assistant') {
|
if (message.role === 'assistant') {
|
||||||
// 助手回复使用处理后的内容
|
|
||||||
if (finalDisplayableFinalContent && finalDisplayableFinalContent.trim() !== '') {
|
if (finalDisplayableFinalContent && finalDisplayableFinalContent.trim() !== '') {
|
||||||
return (
|
return (
|
||||||
<div className="prose prose-xs sm:prose-sm prose-gray max-w-none overflow-x-auto text-xs sm:text-sm">
|
<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 {
|
} else {
|
||||||
// 用户文本消息
|
|
||||||
return (
|
return (
|
||||||
<div className="prose prose-xs sm:prose-sm prose-gray max-w-none overflow-x-auto text-xs sm:text-sm">
|
<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} />
|
<MarkdownRender raw={message.content} />
|
||||||
|
|||||||
@@ -712,18 +712,41 @@ const Playground = () => {
|
|||||||
const handleMessageCopy = useCallback((message) => {
|
const handleMessageCopy = useCallback((message) => {
|
||||||
if (!message.content) return;
|
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) {
|
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||||
navigator.clipboard.writeText(message.content).then(() => {
|
navigator.clipboard.writeText(textToCopy).then(() => {
|
||||||
Toast.success({
|
Toast.success({
|
||||||
content: t('消息已复制到剪贴板'),
|
content: t('消息已复制到剪贴板'),
|
||||||
duration: 2,
|
duration: 2,
|
||||||
});
|
});
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
console.error('Clipboard API 复制失败:', err);
|
console.error('Clipboard API 复制失败:', err);
|
||||||
fallbackCopyToClipboard(message.content);
|
fallbackCopyToClipboard(textToCopy);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
fallbackCopyToClipboard(message.content);
|
fallbackCopyToClipboard(textToCopy);
|
||||||
}
|
}
|
||||||
}, [t]);
|
}, [t]);
|
||||||
|
|
||||||
@@ -790,7 +813,14 @@ const Playground = () => {
|
|||||||
if (targetMessage.role === 'user') {
|
if (targetMessage.role === 'user') {
|
||||||
const newMessages = prevMessages.slice(0, messageIndex);
|
const newMessages = prevMessages.slice(0, messageIndex);
|
||||||
setTimeout(() => {
|
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);
|
}, 100);
|
||||||
return newMessages;
|
return newMessages;
|
||||||
} else if (targetMessage.role === 'assistant') {
|
} else if (targetMessage.role === 'assistant') {
|
||||||
@@ -802,7 +832,14 @@ const Playground = () => {
|
|||||||
const userMessage = prevMessages[userMessageIndex];
|
const userMessage = prevMessages[userMessageIndex];
|
||||||
const newMessages = prevMessages.slice(0, userMessageIndex);
|
const newMessages = prevMessages.slice(0, userMessageIndex);
|
||||||
setTimeout(() => {
|
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);
|
}, 100);
|
||||||
return newMessages;
|
return newMessages;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user