Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Renders merged explore-only rounds as a collapsible region.
*/

import React, { useRef, useMemo, useCallback, useEffect, useLayoutEffect, useState } from 'react';
import React, { useRef, useMemo, useCallback, useEffect } from 'react';
import { ChevronRight } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import type { FlowItem, FlowToolItem, FlowTextItem, FlowThinkingItem } from '../../types/flow-chat';
Expand Down Expand Up @@ -38,16 +38,11 @@ export const ExploreGroupRenderer: React.FC<ExploreGroupRendererProps> = ({
allItems,
stats,
isGroupStreaming,
isFollowedByCritical,
isLastGroupInTurn
} = data;
const previousGroupIdRef = useRef(groupId);

const [hasAutoCollapsed, setHasAutoCollapsed] = useState(isFollowedByCritical);
const {
cardRootRef,
applyExpandedState,
dispatchToolCardToggle,
} = useToolCardHeightContract({
toolId: groupId,
toolName: 'explore-group',
Expand All @@ -58,41 +53,8 @@ export const ExploreGroupRenderer: React.FC<ExploreGroupRendererProps> = ({
),
});

const userExpanded = exploreGroupStates?.get(groupId) ?? false;
const shouldAutoCollapse = hasAutoCollapsed;

const isCollapsed = shouldAutoCollapse && !userExpanded;

useLayoutEffect(() => {
if (previousGroupIdRef.current !== groupId) {
previousGroupIdRef.current = groupId;
setHasAutoCollapsed(isFollowedByCritical);
return;
}

if (!isFollowedByCritical || hasAutoCollapsed) {
return;
}

if (!userExpanded) {
applyExpandedState(true, false, () => {
setHasAutoCollapsed(true);
}, {
reason: 'auto',
});
return;
}

setHasAutoCollapsed(true);
dispatchToolCardToggle();
}, [
applyExpandedState,
dispatchToolCardToggle,
groupId,
hasAutoCollapsed,
isFollowedByCritical,
userExpanded,
]);
const isExpanded = exploreGroupStates?.get(groupId) ?? false;
const isCollapsed = !isExpanded;

// Auto-scroll to bottom during streaming.
useEffect(() => {
Expand Down Expand Up @@ -143,34 +105,10 @@ export const ExploreGroupRenderer: React.FC<ExploreGroupRendererProps> = ({
// Build class list.
const className = [
'explore-region',
shouldAutoCollapse ? 'explore-region--collapsible' : null,
'explore-region--collapsible',
isCollapsed ? 'explore-region--collapsed' : 'explore-region--expanded',
isGroupStreaming ? 'explore-region--streaming' : null,
].filter(Boolean).join(' ');

// Non-collapsible: just render content without header (streaming, no auto-collapse yet).
if (!shouldAutoCollapse) {
return (
<div
ref={cardRootRef}
data-tool-card-id={groupId}
className={className}
>
<div ref={containerRef} className="explore-region__content">
{allItems.map((item, idx) => (
<ExploreItemRenderer
key={item.id}
item={item}
turnId={turnId}
isLastItem={isLastGroupInTurn && idx === allItems.length - 1}
/>
))}
</div>
</div>
);
}

// Collapsible: unified header + animated content wrapper.
return (
<div
ref={cardRootRef}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ export interface FlowChatContextValue {

// ========== Explore group collapse state ==========
/**
* User expansion state for explore groups.
* key: groupId, value: true means user expanded manually.
* Expanded/collapsed state for explore groups.
* key: groupId, value: true means expanded.
*/
exploreGroupStates?: Map<string, boolean>;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ import type { VirtualItem } from '../../store/modernFlowChatStore';
type ExploreGroupVirtualItem = Extract<VirtualItem, { type: 'explore-group' }>;

interface UseExploreGroupStateResult {
/**
* Expanded/collapsed state for each explore group.
* key: groupId, value: true means expanded.
*/
exploreGroupStates: Map<string, boolean>;
onExploreGroupToggle: (groupId: string) => void;
onExpandAllInTurn: (turnId: string) => void;
Expand All @@ -22,7 +26,8 @@ export function useExploreGroupState(
const onExploreGroupToggle = useCallback((groupId: string) => {
setExploreGroupStates(prev => {
const next = new Map(prev);
next.set(groupId, !prev.get(groupId));
const currentExpanded = prev.get(groupId) ?? false;
next.set(groupId, !currentExpanded);
return next;
});
}, []);
Expand Down
39 changes: 1 addition & 38 deletions src/web-ui/src/flow_chat/store/modernFlowChatStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

import { create } from 'zustand';
import { immer } from 'zustand/middleware/immer';
import type { Session, DialogTurn, ModelRound, FlowItem, FlowToolItem, FlowTextItem } from '../types/flow-chat';
import type { Session, DialogTurn, ModelRound, FlowItem, FlowToolItem } from '../types/flow-chat';
import { isCollapsibleTool, READ_TOOL_NAMES, SEARCH_TOOL_NAMES } from '../tool-cards';

/**
Expand All @@ -29,11 +29,6 @@ export interface ExploreGroupData {
stats: ExploreGroupStats;
isGroupStreaming: boolean;
isLastGroupInTurn: boolean;
/**
* When true, ExploreGroupRenderer auto-collapses after a critical follow-up round (e.g. Mermaid).
* Set false if the group contains assistant `text` items so narrative stays visible.
*/
isFollowedByCritical: boolean;
}

/**
Expand Down Expand Up @@ -135,18 +130,6 @@ function computeRoundStats(round: ModelRound): { readCount: number; searchCount:
return { readCount, searchCount, thinkingCount };
}

/**
* True when the merged explore group includes assistant markdown/text with real content.
* Auto-collapse on "followed by critical tool" must not hide this narrative.
*/
function exploreGroupHasNarrativeText(items: FlowItem[]): boolean {
return items.some(
(item) =>
item.type === 'text' &&
String((item as FlowTextItem).content || '').trim().length > 0
);
}

let cachedSession: Session | null = null;
let cachedDialogTurnsRef: DialogTurn[] | null = null;
let cachedVirtualItems: VirtualItem[] = [];
Expand Down Expand Up @@ -254,25 +237,6 @@ export function sessionToVirtualItems(session: Session | null): VirtualItem[] {

if (group && group.startIndex === roundIndex) {
const isLastGroup = groupIndex === tempGroups.length - 1;

let isFollowedByCritical = false;
const nextRoundIndex = group.endIndex + 1;
if (nextRoundIndex < nonEmptyRounds.length) {
const nextRound = nonEmptyRounds[nextRoundIndex];

const hasAnyTool = nextRound.items.some(item => item.type === 'tool');

if (nextRound.isStreaming && !hasAnyTool) {
isFollowedByCritical = false;
} else {
isFollowedByCritical = !isExploreOnlyRound(nextRound);
}
}

if (exploreGroupHasNarrativeText(group.allItems)) {
isFollowedByCritical = false;
}

const isGroupStreaming = group.rounds.some(r => r.isStreaming);

items.push({
Expand All @@ -285,7 +249,6 @@ export function sessionToVirtualItems(session: Session | null): VirtualItem[] {
stats: { readCount: group.readCount, searchCount: group.searchCount, thinkingCount: group.thinkingCount },
isGroupStreaming,
isLastGroupInTurn: isLastGroup,
isFollowedByCritical,
}
});

Expand Down
Loading