Skip to content

Commit 6278047

Browse files
authored
Merge pull request #1200 from yeyan1996/feature/playback
fix: streaming in playback mode
2 parents 5bb7925 + 53775fc commit 6278047

File tree

4 files changed

+93
-46
lines changed

4 files changed

+93
-46
lines changed

frontend/src/app/share/[threadId]/page.tsx

Lines changed: 28 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ import { useAgentStream } from '@/hooks/useAgentStream';
3131
import { ThreadSkeleton } from '@/components/thread/content/ThreadSkeleton';
3232
import { extractToolName } from '@/components/thread/tool-views/xml-parser';
3333

34+
// Memoized components
35+
const MemoizedToolCallSidePanel = React.memo(ToolCallSidePanel);
36+
const MemoizedFileViewerModal = React.memo(FileViewerModal);
37+
3438
const threadErrorCodeMessages: Record<string, string> = {
3539
PGRST116: 'The requested chat does not exist, has been deleted, or you do not have access to it.',
3640
};
@@ -177,7 +181,7 @@ export default function ThreadPage({
177181
break;
178182
}
179183
},
180-
[threadId],
184+
[],
181185
);
182186

183187
const handleStreamError = useCallback((errorMessage: string) => {
@@ -284,32 +288,32 @@ export default function ThreadPage({
284288
[],
285289
);
286290

287-
useEffect(() => {
288-
if (!isPlaying || messages.length === 0) return;
291+
// useEffect(() => {
292+
// if (!isPlaying || messages.length === 0) return;
289293

290-
let playbackTimeout: NodeJS.Timeout;
294+
// let playbackTimeout: NodeJS.Timeout;
291295

292-
const playbackNextMessage = async () => {
293-
if (currentMessageIndex >= messages.length) {
294-
setIsPlaying(false);
295-
return;
296-
}
296+
// const playbackNextMessage = async () => {
297+
// if (currentMessageIndex >= messages.length) {
298+
// setIsPlaying(false);
299+
// return;
300+
// }
297301

298-
const currentMessage = messages[currentMessageIndex];
299-
console.log(
300-
`Playing message ${currentMessageIndex}:`,
301-
currentMessage.type,
302-
currentMessage.message_id,
303-
);
302+
// const currentMessage = messages[currentMessageIndex];
303+
// console.log(
304+
// `Playing message ${currentMessageIndex}:`,
305+
// currentMessage.type,
306+
// currentMessage.message_id,
307+
// );
304308

305-
setCurrentMessageIndex((prevIndex) => prevIndex + 1);
306-
};
307-
playbackTimeout = setTimeout(playbackNextMessage, 500);
309+
// setCurrentMessageIndex((prevIndex) => prevIndex + 1);
310+
// };
311+
// playbackTimeout = setTimeout(playbackNextMessage, 500);
308312

309-
return () => {
310-
clearTimeout(playbackTimeout);
311-
};
312-
}, [isPlaying, currentMessageIndex, messages]);
313+
// return () => {
314+
// clearTimeout(playbackTimeout);
315+
// };
316+
// }, [isPlaying, currentMessageIndex, messages]);
313317

314318
const {
315319
status: streamHookStatus,
@@ -804,7 +808,7 @@ export default function ThreadPage({
804808
{renderFloatingControls()}
805809
</div>
806810

807-
<ToolCallSidePanel
811+
<MemoizedToolCallSidePanel
808812
isOpen={isSidePanelOpen}
809813
onClose={() => {
810814
setIsSidePanelOpen(false);
@@ -820,7 +824,7 @@ export default function ThreadPage({
820824
onFileClick={handleOpenFileViewer}
821825
/>
822826

823-
<FileViewerModal
827+
<MemoizedFileViewerModal
824828
open={fileViewerOpen}
825829
onOpenChange={setFileViewerOpen}
826830
sandboxId={sandboxId || ''}

frontend/src/components/thread/content/PlaybackControls.tsx

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
import React, { useCallback, useEffect, useState, useRef } from 'react';
22
import { Button } from '@/components/ui/button';
3-
import { Play, Pause, ArrowDown, FileText, Info, ArrowUp } from 'lucide-react';
3+
import {
4+
Play,
5+
Pause,
6+
ArrowDown,
7+
FileText,
8+
PanelRightOpen,
9+
ArrowUp,
10+
} from 'lucide-react';
411
import { UnifiedMessage } from '@/components/thread/types';
512
import { safeJsonParse } from '@/components/thread/utils';
613
import Link from 'next/link';
@@ -95,6 +102,8 @@ export const PlaybackControls = ({
95102
toolPlaybackIndex,
96103
} = playbackState;
97104

105+
const playbackTimeout = useRef<NodeJS.Timeout | null>(null);
106+
98107
// Helper function to update playback state
99108
const updatePlaybackState = useCallback((updates: Partial<PlaybackState>) => {
100109
setPlaybackState((prev) => ({ ...prev, ...updates }));
@@ -123,6 +132,12 @@ export const PlaybackControls = ({
123132
toolPlaybackIndex: -1,
124133
});
125134
setCurrentToolIndex(-1);
135+
136+
if (playbackTimeout.current) {
137+
clearTimeout(playbackTimeout.current);
138+
}
139+
140+
// If the side panel is open, close it
126141
if (isSidePanelOpen) {
127142
onToggleSidePanel();
128143
}
@@ -140,6 +155,10 @@ export const PlaybackControls = ({
140155
messages.length,
141156
);
142157

158+
if (!isSidePanelOpen) {
159+
onToggleSidePanel();
160+
}
161+
143162
// If we're moving to a new message, update the visible messages
144163
if (newMessageIndex > currentMessageIndex) {
145164
const newVisibleMessages = messages.slice(0, newMessageIndex);
@@ -156,7 +175,13 @@ export const PlaybackControls = ({
156175
updatePlaybackState({ isPlaying: false });
157176
}
158177
},
159-
[currentMessageIndex, messages, updatePlaybackState],
178+
[
179+
currentMessageIndex,
180+
messages,
181+
isSidePanelOpen,
182+
onToggleSidePanel,
183+
updatePlaybackState,
184+
],
160185
);
161186

162187
const skipToEnd = useCallback(() => {
@@ -375,7 +400,6 @@ export const PlaybackControls = ({
375400
useEffect(() => {
376401
if (!isPlaying || messages.length === 0) return;
377402

378-
let playbackTimeout: NodeJS.Timeout;
379403
let cleanupStreaming: (() => void) | undefined;
380404

381405
const playbackNextMessage = async () => {
@@ -430,10 +454,10 @@ export const PlaybackControls = ({
430454
};
431455

432456
// Start playback with a small delay
433-
playbackTimeout = setTimeout(playbackNextMessage, 500);
457+
playbackTimeout.current = setTimeout(playbackNextMessage, 500);
434458

435459
return () => {
436-
clearTimeout(playbackTimeout);
460+
clearTimeout(playbackTimeout.current);
437461
if (cleanupStreaming) cleanupStreaming();
438462
};
439463
}, [
@@ -537,7 +561,7 @@ export const PlaybackControls = ({
537561
className={`h-8 w-8 ${isSidePanelOpen ? 'text-primary' : ''}`}
538562
aria-label="Toggle Tool Panel"
539563
>
540-
<Info className="h-4 w-4" />
564+
<PanelRightOpen className="h-4 w-4" />
541565
</Button>
542566
</div>
543567
</div>
@@ -588,8 +612,7 @@ export const PlaybackControls = ({
588612
currentMessageIndex,
589613
messages.length,
590614
skipToEnd,
591-
togglePlayback,
592-
isPlaying,
615+
PlayButton,
593616
ResetButton,
594617
ForwardButton,
595618
],

frontend/src/components/thread/content/ThreadContent.tsx

Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -608,44 +608,59 @@ export const ThreadContent: React.FC<ThreadContentProps> = ({
608608
// Use merged groups instead of original grouped messages
609609
const finalGroupedMessages = mergedGroups;
610610

611-
// Handle streaming content - only add to existing group or create new one if needed
612-
if (streamingTextContent) {
611+
612+
// Helper function to add streaming content to groups
613+
const appendStreamingContent = (content: string, isPlayback: boolean = false) => {
614+
const messageId = isPlayback ? 'playbackStreamingText' : 'streamingTextContent';
615+
const metadata = isPlayback ? 'playbackStreamingText' : 'streamingTextContent';
616+
const keySuffix = isPlayback ? 'playback-streaming' : 'streaming';
617+
613618
const lastGroup = finalGroupedMessages.at(-1);
614619
if (!lastGroup || lastGroup.type === 'user') {
615620
// Create new assistant group for streaming content
616621
assistantGroupCounter++;
617622
finalGroupedMessages.push({
618623
type: 'assistant_group',
619624
messages: [{
620-
content: streamingTextContent,
625+
content,
621626
type: 'assistant',
622-
message_id: 'streamingTextContent',
623-
metadata: 'streamingTextContent',
627+
message_id: messageId,
628+
metadata,
624629
created_at: new Date().toISOString(),
625630
updated_at: new Date().toISOString(),
626631
is_llm_message: true,
627-
thread_id: 'streamingTextContent',
632+
thread_id: messageId,
628633
sequence: Infinity,
629634
}],
630-
key: `assistant-group-${assistantGroupCounter}-streaming`
635+
key: `assistant-group-${assistantGroupCounter}-${keySuffix}`
631636
});
632637
} else if (lastGroup.type === 'assistant_group') {
633638
// Only add streaming content if it's not already represented in the last message
634639
const lastMessage = lastGroup.messages[lastGroup.messages.length - 1];
635-
if (lastMessage.message_id !== 'streamingTextContent') {
640+
if (lastMessage.message_id !== messageId) {
636641
lastGroup.messages.push({
637-
content: streamingTextContent,
642+
content,
638643
type: 'assistant',
639-
message_id: 'streamingTextContent',
640-
metadata: 'streamingTextContent',
644+
message_id: messageId,
645+
metadata,
641646
created_at: new Date().toISOString(),
642647
updated_at: new Date().toISOString(),
643648
is_llm_message: true,
644-
thread_id: 'streamingTextContent',
649+
thread_id: messageId,
645650
sequence: Infinity,
646651
});
647652
}
648653
}
654+
};
655+
656+
// Handle streaming content - only add to existing group or create new one if needed
657+
if (streamingTextContent) {
658+
appendStreamingContent(streamingTextContent, false);
659+
}
660+
661+
// Handle playback mode streaming text
662+
if (readOnly && streamingText && isStreamingText) {
663+
appendStreamingContent(streamingText, true);
649664
}
650665

651666
return finalGroupedMessages.map((group, groupIndex) => {
@@ -827,7 +842,12 @@ export const ThreadContent: React.FC<ThreadContentProps> = ({
827842

828843
const textToRender = streamingTextContent || '';
829844
const textBeforeTag = detectedTag ? textToRender.substring(0, tagStartIndex) : textToRender;
830-
const showCursor = (streamHookStatus === 'streaming' || streamHookStatus === 'connecting') && !detectedTag;
845+
const showCursor =
846+
(streamHookStatus ===
847+
'streaming' ||
848+
streamHookStatus ===
849+
'connecting') &&
850+
!detectedTag;
831851

832852
return (
833853
<>

frontend/src/components/ui/tabs.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ function TabsTrigger({
4242
<TabsPrimitive.Trigger
4343
data-slot="tabs-trigger"
4444
className={cn(
45-
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
45+
"cursor-pointer data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
4646
className,
4747
)}
4848
{...props}

0 commit comments

Comments
 (0)