Jelajahi Sumber

🐛 fix(playground): ensure proper streaming updates & safeguard message handling

Summary
This commit addresses two critical issues affecting the real-time chat experience in the Playground:

1. Optimized re-rendering of reasoning content
   • Added `reasoningContent` to the comparison function of `OptimizedMessageContent` (`web/src/components/playground/OptimizedComponents.js`).
   • Ensures the component re-renders while reasoning text streams, resolving the bug where only the first characters (“好,”) were shown until the stream finished.

2. Defensive checks for SSE message updates
   • Added early-return guards in `streamMessageUpdate` (`web/src/hooks/useApiRequest.js`).
   • Skips updates when `lastMessage` is undefined or the last message isn’t from the assistant, preventing `TypeError: Cannot read properties of undefined (reading 'status')` during rapid SSE responses.

Impact
• Real-time reasoning content now appears progressively, enhancing user feedback.
• Eliminates runtime crashes caused by undefined message references, improving overall stability.
Apple\Apple 9 bulan lalu
induk
melakukan
1d37867f39

+ 1 - 0
web/src/components/playground/OptimizedComponents.js

@@ -12,6 +12,7 @@ export const OptimizedMessageContent = React.memo(MessageContent, (prevProps, ne
     prevProps.message.content === nextProps.message.content &&
     prevProps.message.content === nextProps.message.content &&
     prevProps.message.status === nextProps.message.status &&
     prevProps.message.status === nextProps.message.status &&
     prevProps.message.role === nextProps.message.role &&
     prevProps.message.role === nextProps.message.role &&
+    prevProps.message.reasoningContent === nextProps.message.reasoningContent &&
     prevProps.message.isReasoningExpanded === nextProps.message.isReasoningExpanded &&
     prevProps.message.isReasoningExpanded === nextProps.message.isReasoningExpanded &&
     prevProps.isEditing === nextProps.isEditing &&
     prevProps.isEditing === nextProps.isEditing &&
     prevProps.editValue === nextProps.editValue &&
     prevProps.editValue === nextProps.editValue &&

+ 9 - 1
web/src/components/playground/ThinkingContent.js

@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { useEffect, useRef } from 'react';
 import { Typography } from '@douyinfe/semi-ui';
 import { Typography } from '@douyinfe/semi-ui';
 import MarkdownRenderer from '../common/markdown/MarkdownRenderer';
 import MarkdownRenderer from '../common/markdown/MarkdownRenderer';
 import { ChevronRight, ChevronUp, Brain, Loader2 } from 'lucide-react';
 import { ChevronRight, ChevronUp, Brain, Loader2 } from 'lucide-react';
@@ -12,12 +12,19 @@ const ThinkingContent = ({
   onToggleReasoningExpansion
   onToggleReasoningExpansion
 }) => {
 }) => {
   const { t } = useTranslation();
   const { t } = useTranslation();
+  const scrollRef = useRef(null);
 
 
   if (!finalExtractedThinkingContent) return null;
   if (!finalExtractedThinkingContent) return null;
 
 
   const isThinkingStatus = message.status === 'loading' || message.status === 'incomplete';
   const isThinkingStatus = message.status === 'loading' || message.status === 'incomplete';
   const headerText = (isThinkingStatus && !message.isThinkingComplete) ? t('思考中...') : t('思考过程');
   const headerText = (isThinkingStatus && !message.isThinkingComplete) ? t('思考中...') : t('思考过程');
 
 
+  useEffect(() => {
+    if (scrollRef.current) {
+      scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
+    }
+  }, [finalExtractedThinkingContent, message.isReasoningExpanded]);
+
   return (
   return (
     <div className="rounded-xl sm:rounded-2xl mb-2 sm:mb-4 overflow-hidden shadow-sm backdrop-blur-sm">
     <div className="rounded-xl sm:rounded-2xl mb-2 sm:mb-4 overflow-hidden shadow-sm backdrop-blur-sm">
       <div
       <div
@@ -73,6 +80,7 @@ const ThinkingContent = ({
         {message.isReasoningExpanded && (
         {message.isReasoningExpanded && (
           <div className="p-3 sm:p-5 pt-2 sm:pt-4">
           <div className="p-3 sm:p-5 pt-2 sm:pt-4">
             <div
             <div
+              ref={scrollRef}
               className="bg-white/70 backdrop-blur-sm rounded-lg sm:rounded-xl p-2 shadow-inner overflow-x-auto overflow-y-auto thinking-content-scroll"
               className="bg-white/70 backdrop-blur-sm rounded-lg sm:rounded-xl p-2 shadow-inner overflow-x-auto overflow-y-auto thinking-content-scroll"
               style={{
               style={{
                 maxHeight: '200px',
                 maxHeight: '200px',

+ 2 - 0
web/src/hooks/useApiRequest.js

@@ -38,6 +38,8 @@ export const useApiRequest = (
   const streamMessageUpdate = useCallback((textChunk, type) => {
   const streamMessageUpdate = useCallback((textChunk, type) => {
     setMessage(prevMessage => {
     setMessage(prevMessage => {
       const lastMessage = prevMessage[prevMessage.length - 1];
       const lastMessage = prevMessage[prevMessage.length - 1];
+      if (!lastMessage) return prevMessage;
+      if (lastMessage.role !== 'assistant') return prevMessage;
       if (lastMessage.status === MESSAGE_STATUS.ERROR) {
       if (lastMessage.status === MESSAGE_STATUS.ERROR) {
         return prevMessage;
         return prevMessage;
       }
       }