From aa44566fefa5c267cec5c0c00a9daf6915211ee1 Mon Sep 17 00:00:00 2001 From: Henry Li Date: Sun, 18 Jan 2026 19:56:07 +0800 Subject: [PATCH] feat: re-implement message group --- .../ai-elements/chain-of-thought.tsx | 12 +- .../src/components/ai-elements/code-block.tsx | 2 +- .../src/components/workspace/flip-display.tsx | 2 +- .../workspace/messages/message-group.tsx | 222 ++++++++++-------- 4 files changed, 132 insertions(+), 106 deletions(-) diff --git a/frontend/src/components/ai-elements/chain-of-thought.tsx b/frontend/src/components/ai-elements/chain-of-thought.tsx index 1a8550a..b0fde74 100644 --- a/frontend/src/components/ai-elements/chain-of-thought.tsx +++ b/frontend/src/components/ai-elements/chain-of-thought.tsx @@ -15,7 +15,13 @@ import { type LucideIcon, } from "lucide-react"; import type { ComponentProps, ReactNode } from "react"; -import { createContext, memo, useContext, useMemo } from "react"; +import { + createContext, + isValidElement, + memo, + useContext, + useMemo, +} from "react"; type ChainOfThoughtContextValue = { isOpen: boolean; @@ -108,7 +114,7 @@ export const ChainOfThoughtHeader = memo( ); export type ChainOfThoughtStepProps = ComponentProps<"div"> & { - icon?: LucideIcon; + icon?: LucideIcon | React.ReactElement; label: ReactNode; description?: ReactNode; status?: "complete" | "active" | "pending"; @@ -141,7 +147,7 @@ export const ChainOfThoughtStep = memo( {...props} >
- + {isValidElement(Icon) ? Icon : }
diff --git a/frontend/src/components/ai-elements/code-block.tsx b/frontend/src/components/ai-elements/code-block.tsx index a3d0d09..68d562a 100644 --- a/frontend/src/components/ai-elements/code-block.tsx +++ b/frontend/src/components/ai-elements/code-block.tsx @@ -114,7 +114,7 @@ export const CodeBlock = ({ dangerouslySetInnerHTML={{ __html: html }} />
diff --git a/frontend/src/components/workspace/flip-display.tsx b/frontend/src/components/workspace/flip-display.tsx index 6c3a2ca..19a4229 100644 --- a/frontend/src/components/workspace/flip-display.tsx +++ b/frontend/src/components/workspace/flip-display.tsx @@ -12,7 +12,7 @@ export function FlipDisplay({ className?: string; }) { return ( -
+
convertToSteps(messages), [messages]); - const stepCount = useMemo( - () => steps.filter((step) => step.type !== "reasoning").length, - [steps], - ); + const lastToolCallStep = useMemo(() => { + const filteredSteps = steps.filter((step) => step.type === "toolCall"); + return filteredSteps[filteredSteps.length - 1]; + }, [steps]); + const aboveLastToolCallSteps = useMemo(() => { + if (lastToolCallStep) { + const index = steps.indexOf(lastToolCallStep); + return steps.slice(0, index); + } + return []; + }, [lastToolCallStep, steps]); + const lastReasoningStep = useMemo(() => { + if (lastToolCallStep) { + const index = steps.indexOf(lastToolCallStep); + return steps.slice(index + 1).find((step) => step.type === "reasoning"); + } else { + const filteredSteps = steps.filter((step) => step.type === "reasoning"); + return filteredSteps[filteredSteps.length - 1]; + } + }, [lastToolCallStep, steps]); const rehypePlugins = useRehypeSplitWordsIntoSpans(isLoading); - const [open, setOpen] = useState(false); - const lastStep = steps[steps.length - 1]; - const { label, icon } = describeStep(lastStep); return ( - 1 ? : icon - } - > -
-
-
- {open && stepCount > 1 ? ( -
{stepCount} steps
+ {aboveLastToolCallSteps.length > 0 && ( + + )} + {aboveLastToolCallSteps.length > 0 && ( + + {showAbove && + aboveLastToolCallSteps.map((step) => + step.type === "reasoning" ? ( + + {step.reasoning ?? ""} + + } + > ) : ( - - - {label} - - - )} -
-
-
- {!open && stepCount > 1 && ( -
- {stepCount > 1 ? `${stepCount} steps` : `${stepCount} step`} -
+ + ), )} -
-
-
- - {steps.map((step) => - step.type === "reasoning" ? ( - - {step.reasoning ?? ""} - - } - /> - ) : ( - - ), - )} - + {lastToolCallStep && ( + + + + )} + + )} + {lastReasoningStep && ( + <> + + {showLastThinking && ( + + + {lastReasoningStep.reasoning ?? ""} + + } + > + + )} + + )}
); } @@ -156,7 +212,15 @@ function ToolCall({ } } return ( - + { + window.open(url, "_blank"); + }} + > {url && ( @@ -331,47 +395,3 @@ function convertToSteps(messages: Message[]): CoTStep[] { } return steps; } - -function describeStep(step: CoTStep | undefined): { - label: string; - icon: React.ReactElement; -} { - if (!step) { - return { label: "Thinking", icon: }; - } - if (step.type === "reasoning") { - return { label: "Thinking", icon: }; - } else { - let label: string; - let icon: React.ReactElement = ; - if (step.name === "web_search") { - label = `Search "${(step.args as { query: string }).query}" on web`; - icon = ; - } else if (step.name === "web_fetch") { - label = "View web page"; - icon = ; - } else if (step.name === "ls") { - label = "List folder"; - icon = ; - } else if (step.name === "read_file") { - label = "Read file"; - icon = ; - } else if (step.name === "write_file" || step.name === "str_replace") { - label = "Write file"; - icon = ; - } else if (step.name === "bash") { - label = "Execute command"; - icon = ; - } else if (step.name === "present_files") { - label = "Present files"; - icon = ; - } else { - label = `Call tool "${step.name}"`; - icon = ; - } - if (typeof step.args.description === "string") { - label = step.args.description; - } - return { label, icon }; - } -}