diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/components/displays/AIChainOfThought.tsx b/apps/desktop/layer/renderer/src/modules/ai-chat/components/displays/AIChainOfThought.tsx new file mode 100644 index 000000000..ffe8e3e61 --- /dev/null +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/components/displays/AIChainOfThought.tsx @@ -0,0 +1,60 @@ +import { CollapseCss, CollapseCssGroup } from "@follow/components/ui/collapse/CollapseCss.js" +import { cn } from "@follow/utils" +import * as React from "react" + +interface AIChainOfThoughtProps { + children: React.ReactNode + isStreaming?: boolean + className?: string +} + +export const AIChainOfThought: React.FC = React.memo( + ({ children, isStreaming = false, className }) => { + const collapseId = React.useMemo(() => `chain-${Math.random().toString(36).slice(2)}`, []) + // Re-mount CollapseCssGroup when streaming state changes or when we need to force-open while streaming + const [remountTick, setRemountTick] = React.useState(0) + const groupKey = `${isStreaming ? "streaming" : "idle"}:${remountTick}` + if (!children) return null + + return ( +
+
+ + +
+ { + // While streaming, keep it open and block manual collapse + if (isStreaming && !opened) { + setRemountTick((x) => x + 1) + } + }} + title={ +
+
+ {/* */} + + {isStreaming ? "Thinking..." : "Finished Thinking"} + +
+
+ +
+
+ } + className="group w-full border-none" + contentClassName="pb-2 pt-1" + > + {children} +
+
+
+
+ ) + }, +) + +AIChainOfThought.displayName = "AIChainOfThought" diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/components/displays/AIReasoningPart.tsx b/apps/desktop/layer/renderer/src/modules/ai-chat/components/displays/AIReasoningPart.tsx index 91990bbbc..74b360afa 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/components/displays/AIReasoningPart.tsx +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/components/displays/AIReasoningPart.tsx @@ -33,19 +33,17 @@ export const AIReasoningPart: React.FC = React.memo( } }} title={ -
-
- - - {isStreaming ? "Reasoning..." : "Reasoning"} - +
+
+ + {isStreaming ? "Reasoning..." : "Reasoning"}
-
- +
+
} - className="group w-full border-none" + className="group/inner w-full border-none" contentClassName="pb-0 pt-2" >
diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/components/displays/index.ts b/apps/desktop/layer/renderer/src/modules/ai-chat/components/displays/index.ts index 84f96791b..0f5c9ebba 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/components/displays/index.ts +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/components/displays/index.ts @@ -1,3 +1,4 @@ +export { AIChainOfThought } from "./AIChainOfThought" export { AIDisplayAnalyticsPart } from "./AIDisplayAnalyticsPart" export { AIDisplayEntriesPart } from "./AIDisplayEntriesPart" export { AIDisplayFeedPart } from "./AIDisplayFeedsPart" diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/AIMarkdownMessage.old.tsx b/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/AIMarkdownMessage.old.tsx index cc7b9168b..cca345950 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/AIMarkdownMessage.old.tsx +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/AIMarkdownMessage.old.tsx @@ -351,7 +351,7 @@ export const AIMarkdownStreamingMessage = memo( className?: string isProcessing?: boolean }) => { - const className = `prose max-w-full dark:prose-invert text-sm + const className = `prose max-w-full dark:prose-invert prose-sm prose-h1:text-2xl prose-h2:text-xl prose-h3:text-lg prose-h4:text-base prose-h5:text-base prose-h6:text-sm prose-li:list-disc prose-li:marker:text-accent prose-hr:border-border prose-hr:mx-8` diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/AIMarkdownMessage.v2.tsx b/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/AIMarkdownMessage.v2.tsx index d2b98e7a7..3dcab6a44 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/AIMarkdownMessage.v2.tsx +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/AIMarkdownMessage.v2.tsx @@ -14,7 +14,7 @@ export const AIMarkdownStreamingMessage = memo( className?: string isStreaming?: boolean }) => { - const className = `prose max-w-full dark:prose-invert text-sm + const className = `prose max-w-full dark:prose-invert prose-sm prose-h1:text-2xl prose-h2:text-xl prose-h3:text-lg prose-h4:text-base prose-h5:text-base prose-h6:text-sm prose-li:list-disc prose-li:marker:text-accent prose-hr:border-border prose-hr:mx-8` diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/AIMessageParts.tsx b/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/AIMessageParts.tsx index 823b0df56..034058656 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/AIMessageParts.tsx +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/AIMessageParts.tsx @@ -16,6 +16,7 @@ import type { import { useChatStatus } from "../../store/hooks" import { + AIChainOfThought, AIDisplayAnalyticsPart, AIDisplayEntriesPart, AIDisplayFeedPart, @@ -51,79 +52,107 @@ export const AIMessageParts: React.FC = React.memo( return chatStatus === "streaming" && shouldStreamingAnimation && isLastMessage }, [chatStatus, isLastMessage, shouldStreamingAnimation]) - return message.parts.map((part, index) => { - const partKey = `${message.id}-${index}` + const displayParts = React.useMemo(() => { + return message.parts.filter( + (part) => part.type === "text" || part.type.startsWith("tool-display"), + ) + }, [message.parts]) + const thoughtParts = React.useMemo(() => { + return message.parts.filter( + (part) => !(part.type === "text" || part.type.startsWith("tool-display")), + ) + }, [message.parts]) - switch (part.type) { - case "text": { - return ( - - ) - } + return ( + <> + + {thoughtParts.map((part, index) => { + const partKey = `${message.id}-${index}` - case "reasoning": { - return ( - - ) - } + switch (part.type) { + case "reasoning": { + return ( + + ) + } + default: { + if (part.type.startsWith("tool-")) { + if (part.type.startsWith("tool-chunkBreak")) { + return null + } + return + } + return null + } + } + })} + + {displayParts.map((part, index) => { + const partKey = `${message.id}-${index}` - case "tool-displayAnalytics": { - return - } - case "tool-displayEntries": { - return - } - case "tool-displaySubscriptions": { - return ( - - ) - } - case "tool-displayFeed": { - return - } + switch (part.type) { + case "text": { + return ( + + ) + } - case "tool-displayFlowChart": { - const loadingElement = ( -
-
-
- - - Generating Flow Chart... - + case "tool-displayAnalytics": { + return + } + case "tool-displayEntries": { + return + } + case "tool-displaySubscriptions": { + return ( + + ) + } + case "tool-displayFeed": { + return + } + + case "tool-displayFlowChart": { + const loadingElement = ( +
+
+
+ + + Generating Flow Chart... + +
+
-
-
- ) - return ( - - - - - - ) - } + ) + return ( + + + + + + ) + } - default: { - if (part.type.startsWith("tool-")) { - if (part.type.startsWith("tool-chunkBreak")) { + default: { return null } - return } - return null - } - } - }) + })} + + ) }, ) diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/ToolInvocationComponent.tsx b/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/ToolInvocationComponent.tsx index 11337c6b9..95866898d 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/ToolInvocationComponent.tsx +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/ToolInvocationComponent.tsx @@ -32,13 +32,11 @@ export const ToolInvocationComponent: React.FC = R className="group border-none" title={
-
+
- - {hasError ? "Tool Failed:" : "Tool Called:"} - + {hasError ? "Tool Failed:" : "Tool Called:"}

{toolName}

diff --git a/icons/mgc/brain_cute_re.svg b/icons/mgc/brain_cute_re.svg new file mode 100644 index 000000000..add382dc6 --- /dev/null +++ b/icons/mgc/brain_cute_re.svg @@ -0,0 +1 @@ + \ No newline at end of file