feat(ui): improve streaming reasoning block lifecycle control
- Add autoLifecycle prop to Reasoning component for fine-grained control - Extract StreamingReasoningBlock with manual/auto collapse state management - Fix active thinking index logic for streaming messages
This commit is contained in:
parent
b35d2d4fe7
commit
f597da6e33
@ -342,7 +342,10 @@ function StreamingBubble({ parts, isDone }: { parts: StreamPart[]; isDone: boole
|
|||||||
}
|
}
|
||||||
}, [displayParts.length]);
|
}, [displayParts.length]);
|
||||||
|
|
||||||
const firstThinkingIdx = displayParts.findIndex((p) => p.type === "thinking");
|
const activeThinkingIdx =
|
||||||
|
!displayDone && displayParts[displayParts.length - 1]?.type === "thinking"
|
||||||
|
? displayParts.length - 1
|
||||||
|
: -1;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={contentRef} className="flex gap-4 px-4 py-3 max-w-3xl mx-auto w-full">
|
<div ref={contentRef} className="flex gap-4 px-4 py-3 max-w-3xl mx-auto w-full">
|
||||||
@ -364,12 +367,12 @@ function StreamingBubble({ parts, isDone }: { parts: StreamPart[]; isDone: boole
|
|||||||
<div className="text-sm" style={{ color: "var(--text-primary)" }}>
|
<div className="text-sm" style={{ color: "var(--text-primary)" }}>
|
||||||
{displayParts.map((part, i) => {
|
{displayParts.map((part, i) => {
|
||||||
if (part.type === "thinking") {
|
if (part.type === "thinking") {
|
||||||
const isActivelyThinking = !displayDone && i === firstThinkingIdx;
|
|
||||||
return (
|
return (
|
||||||
<Reasoning key={i} isStreaming={isActivelyThinking} defaultOpen={isActivelyThinking}>
|
<StreamingReasoningBlock
|
||||||
<ReasoningTrigger />
|
key={i}
|
||||||
<ReasoningContent>{part.content}</ReasoningContent>
|
content={part.content}
|
||||||
</Reasoning>
|
isActivelyThinking={i === activeThinkingIdx}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (part.type === "tool_call") {
|
if (part.type === "tool_call") {
|
||||||
@ -407,6 +410,59 @@ function StreamingBubble({ parts, isDone }: { parts: StreamPart[]; isDone: boole
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function StreamingReasoningBlock({
|
||||||
|
content,
|
||||||
|
isActivelyThinking,
|
||||||
|
}: {
|
||||||
|
content: string;
|
||||||
|
isActivelyThinking: boolean;
|
||||||
|
}) {
|
||||||
|
const [manualOpen, setManualOpen] = useState(false);
|
||||||
|
const [autoCollapsed, setAutoCollapsed] = useState(false);
|
||||||
|
const wasActivelyThinkingRef = useRef(isActivelyThinking);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const wasActivelyThinking = wasActivelyThinkingRef.current;
|
||||||
|
|
||||||
|
if (isActivelyThinking && !wasActivelyThinking) {
|
||||||
|
setAutoCollapsed(false);
|
||||||
|
setManualOpen(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isActivelyThinking && wasActivelyThinking) {
|
||||||
|
setAutoCollapsed(false);
|
||||||
|
setManualOpen(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
wasActivelyThinkingRef.current = isActivelyThinking;
|
||||||
|
}, [isActivelyThinking]);
|
||||||
|
|
||||||
|
const isOpen = isActivelyThinking ? !autoCollapsed : manualOpen;
|
||||||
|
|
||||||
|
const handleOpenChange = useCallback(
|
||||||
|
(nextOpen: boolean) => {
|
||||||
|
if (isActivelyThinking) {
|
||||||
|
setAutoCollapsed(!nextOpen);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setManualOpen(nextOpen);
|
||||||
|
},
|
||||||
|
[isActivelyThinking]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Reasoning
|
||||||
|
autoLifecycle={false}
|
||||||
|
isStreaming={isActivelyThinking}
|
||||||
|
onOpenChange={handleOpenChange}
|
||||||
|
open={isOpen}
|
||||||
|
>
|
||||||
|
<ReasoningTrigger />
|
||||||
|
<ReasoningContent>{content}</ReasoningContent>
|
||||||
|
</Reasoning>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function StreamingCursor() {
|
function StreamingCursor() {
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
|
|||||||
@ -51,6 +51,7 @@ export type ReasoningProps = ComponentProps<typeof Collapsible> & {
|
|||||||
defaultOpen?: boolean;
|
defaultOpen?: boolean;
|
||||||
onOpenChange?: (open: boolean) => void;
|
onOpenChange?: (open: boolean) => void;
|
||||||
duration?: number;
|
duration?: number;
|
||||||
|
autoLifecycle?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const AUTO_CLOSE_DELAY = 1000;
|
const AUTO_CLOSE_DELAY = 1000;
|
||||||
@ -64,6 +65,7 @@ export const Reasoning = memo(
|
|||||||
defaultOpen,
|
defaultOpen,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
duration: durationProp,
|
duration: durationProp,
|
||||||
|
autoLifecycle = true,
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: ReasoningProps) => {
|
}: ReasoningProps) => {
|
||||||
@ -100,15 +102,16 @@ export const Reasoning = memo(
|
|||||||
|
|
||||||
// Auto-open when streaming starts (unless explicitly closed)
|
// Auto-open when streaming starts (unless explicitly closed)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isStreaming && !isOpen && !isExplicitlyClosed) {
|
if (autoLifecycle && isStreaming && !isOpen && !isExplicitlyClosed) {
|
||||||
setIsOpen(true);
|
setIsOpen(true);
|
||||||
}
|
}
|
||||||
}, [isStreaming, isOpen, setIsOpen, isExplicitlyClosed]);
|
}, [autoLifecycle, isStreaming, isOpen, setIsOpen, isExplicitlyClosed]);
|
||||||
|
|
||||||
// Auto-close when streaming ends (once only, and only if it ever streamed)
|
// Auto-close when streaming ends (once only, and only if it ever streamed)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
hasEverStreamedRef.current &&
|
hasEverStreamedRef.current &&
|
||||||
|
autoLifecycle &&
|
||||||
!isStreaming &&
|
!isStreaming &&
|
||||||
isOpen &&
|
isOpen &&
|
||||||
!hasAutoClosed
|
!hasAutoClosed
|
||||||
@ -120,7 +123,7 @@ export const Reasoning = memo(
|
|||||||
|
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}
|
}
|
||||||
}, [isStreaming, isOpen, setIsOpen, hasAutoClosed]);
|
}, [autoLifecycle, isStreaming, isOpen, setIsOpen, hasAutoClosed]);
|
||||||
|
|
||||||
const handleOpenChange = useCallback(
|
const handleOpenChange = useCallback(
|
||||||
(newOpen: boolean) => {
|
(newOpen: boolean) => {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user