فهرست منبع

🐛 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.
Apple\Apple 9 ماه پیش
والد
کامیت
9c5ab755c1
2فایلهای تغییر یافته به همراه71 افزوده شده و 13 حذف شده
  1. 29 8
      web/src/components/playground/MessageContent.js
  2. 42 5
      web/src/pages/Playground/Playground.js

+ 29 - 8
web/src/components/playground/MessageContent.js

@@ -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} />

+ 42 - 5
web/src/pages/Playground/Playground.js

@@ -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;
         }