feat: aggregated ai chain of thought
This commit is contained in:
parent
4d4dce682c
commit
48e84ca209
|
|
@ -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<AIChainOfThoughtProps> = 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 (
|
||||
<div className={cn("border-border min-w-0 max-w-full text-left", className)}>
|
||||
<div className="w-[calc(var(--ai-chat-layout-width,65ch))] max-w-full" />
|
||||
|
||||
<CollapseCssGroup key={groupKey}>
|
||||
<div>
|
||||
<CollapseCss
|
||||
hideArrow
|
||||
collapseId={collapseId}
|
||||
defaultOpen={isStreaming}
|
||||
onOpenChange={(opened) => {
|
||||
// While streaming, keep it open and block manual collapse
|
||||
if (isStreaming && !opened) {
|
||||
setRemountTick((x) => x + 1)
|
||||
}
|
||||
}}
|
||||
title={
|
||||
<div className="group flex h-6 min-w-0 flex-1 items-center py-0">
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
{/* <i className="i-mgc-brain-cute-re text-purple" /> */}
|
||||
<span className="text-text-secondary">
|
||||
{isStreaming ? "Thinking..." : "Finished Thinking"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="ml-2 flex items-center justify-center opacity-0 transition-opacity duration-200 group-hover:opacity-100">
|
||||
<i className="i-mgc-right-cute-re size-3 shrink-0 transition-transform duration-200 group-data-[state=open]:rotate-90" />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
className="group w-full border-none"
|
||||
contentClassName="pb-2 pt-1"
|
||||
>
|
||||
{children}
|
||||
</CollapseCss>
|
||||
</div>
|
||||
</CollapseCssGroup>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
AIChainOfThought.displayName = "AIChainOfThought"
|
||||
|
|
@ -33,19 +33,17 @@ export const AIReasoningPart: React.FC<AIReasoningPartProps> = React.memo(
|
|||
}
|
||||
}}
|
||||
title={
|
||||
<div className="group flex h-6 min-w-0 flex-1 items-center justify-between py-0">
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<i className="i-mingcute-bulb-2-line text-purple" />
|
||||
<span className="text-text-secondary">
|
||||
{isStreaming ? "Reasoning..." : "Reasoning"}
|
||||
</span>
|
||||
<div className="group/inner flex h-6 min-w-0 flex-1 items-center py-0">
|
||||
<div className="text-text-secondary flex items-center gap-2 text-xs">
|
||||
<i className="i-mgc-brain-cute-re" />
|
||||
<span>{isStreaming ? "Reasoning..." : "Reasoning"}</span>
|
||||
</div>
|
||||
<div className="ml-auto flex items-center justify-center opacity-0 transition-opacity duration-200 group-hover:opacity-100">
|
||||
<i className="i-mgc-right-cute-re size-3 shrink-0 transition-transform duration-200 group-data-[state=open]:rotate-90" />
|
||||
<div className="ml-2 flex items-center justify-center opacity-0 transition-opacity duration-200 group-hover/inner:opacity-100">
|
||||
<i className="i-mgc-right-cute-re size-3 shrink-0 transition-transform duration-200 group-data-[state=open]/inner:rotate-90" />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
className="group w-full border-none"
|
||||
className="group/inner w-full border-none"
|
||||
contentClassName="pb-0 pt-2"
|
||||
>
|
||||
<div className="text-xs">
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
export { AIChainOfThought } from "./AIChainOfThought"
|
||||
export { AIDisplayAnalyticsPart } from "./AIDisplayAnalyticsPart"
|
||||
export { AIDisplayEntriesPart } from "./AIDisplayEntriesPart"
|
||||
export { AIDisplayFeedPart } from "./AIDisplayFeedsPart"
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
|
||||
|
|
|
|||
|
|
@ -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<AIMessagePartsProps> = 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 (
|
||||
<AIMarkdownStreamingMessage
|
||||
key={partKey}
|
||||
text={part.text}
|
||||
className={"text-text"}
|
||||
isStreaming={shouldMessageAnimation}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<AIChainOfThought isStreaming={displayParts.length === 0}>
|
||||
{thoughtParts.map((part, index) => {
|
||||
const partKey = `${message.id}-${index}`
|
||||
|
||||
case "reasoning": {
|
||||
return (
|
||||
<AIReasoningPart
|
||||
key={partKey}
|
||||
text={part.text}
|
||||
isStreaming={part.state === "streaming"}
|
||||
/>
|
||||
)
|
||||
}
|
||||
switch (part.type) {
|
||||
case "reasoning": {
|
||||
return (
|
||||
<AIReasoningPart
|
||||
key={partKey}
|
||||
text={part.text}
|
||||
isStreaming={part.state === "streaming"}
|
||||
/>
|
||||
)
|
||||
}
|
||||
default: {
|
||||
if (part.type.startsWith("tool-")) {
|
||||
if (part.type.startsWith("tool-chunkBreak")) {
|
||||
return null
|
||||
}
|
||||
return <ToolInvocationComponent key={partKey} part={part as ToolUIPart} />
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
})}
|
||||
</AIChainOfThought>
|
||||
{displayParts.map((part, index) => {
|
||||
const partKey = `${message.id}-${index}`
|
||||
|
||||
case "tool-displayAnalytics": {
|
||||
return <AIDisplayAnalyticsPart key={partKey} part={part as AIDisplayAnalyticsTool} />
|
||||
}
|
||||
case "tool-displayEntries": {
|
||||
return <AIDisplayEntriesPart key={partKey} part={part as AIDisplayEntriesTool} />
|
||||
}
|
||||
case "tool-displaySubscriptions": {
|
||||
return (
|
||||
<AIDisplaySubscriptionsPart key={partKey} part={part as AIDisplaySubscriptionsTool} />
|
||||
)
|
||||
}
|
||||
case "tool-displayFeed": {
|
||||
return <AIDisplayFeedPart key={partKey} part={part as AIDisplayFeedTool} />
|
||||
}
|
||||
switch (part.type) {
|
||||
case "text": {
|
||||
return (
|
||||
<AIMarkdownStreamingMessage
|
||||
key={partKey}
|
||||
text={part.text}
|
||||
className={"text-text"}
|
||||
isStreaming={shouldMessageAnimation}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
case "tool-displayFlowChart": {
|
||||
const loadingElement = (
|
||||
<div className="bg-material-medium my-2 flex aspect-[4/3] w-[calc(var(--ai-chat-layout-width,65ch))] max-w-full items-center justify-center rounded">
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<i className="i-mgc-loading-3-cute-re text-text-secondary size-4 animate-spin" />
|
||||
<span className="text-text-secondary text-sm font-medium">
|
||||
Generating Flow Chart...
|
||||
</span>
|
||||
case "tool-displayAnalytics": {
|
||||
return <AIDisplayAnalyticsPart key={partKey} part={part as AIDisplayAnalyticsTool} />
|
||||
}
|
||||
case "tool-displayEntries": {
|
||||
return <AIDisplayEntriesPart key={partKey} part={part as AIDisplayEntriesTool} />
|
||||
}
|
||||
case "tool-displaySubscriptions": {
|
||||
return (
|
||||
<AIDisplaySubscriptionsPart
|
||||
key={partKey}
|
||||
part={part as AIDisplaySubscriptionsTool}
|
||||
/>
|
||||
)
|
||||
}
|
||||
case "tool-displayFeed": {
|
||||
return <AIDisplayFeedPart key={partKey} part={part as AIDisplayFeedTool} />
|
||||
}
|
||||
|
||||
case "tool-displayFlowChart": {
|
||||
const loadingElement = (
|
||||
<div className="bg-material-medium my-2 flex aspect-[4/3] w-[calc(var(--ai-chat-layout-width,65ch))] max-w-full items-center justify-center rounded">
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<i className="i-mgc-loading-3-cute-re text-text-secondary size-4 animate-spin" />
|
||||
<span className="text-text-secondary text-sm font-medium">
|
||||
Generating Flow Chart...
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
return (
|
||||
<ErrorBoundary key={partKey} beforeCapture={alwaysFalse}>
|
||||
<React.Suspense fallback={loadingElement}>
|
||||
<LazyAIDisplayFlowPart part={part as AIDisplayFlowTool} />
|
||||
</React.Suspense>
|
||||
</ErrorBoundary>
|
||||
)
|
||||
}
|
||||
)
|
||||
return (
|
||||
<ErrorBoundary key={partKey} beforeCapture={alwaysFalse}>
|
||||
<React.Suspense fallback={loadingElement}>
|
||||
<LazyAIDisplayFlowPart part={part as AIDisplayFlowTool} />
|
||||
</React.Suspense>
|
||||
</ErrorBoundary>
|
||||
)
|
||||
}
|
||||
|
||||
default: {
|
||||
if (part.type.startsWith("tool-")) {
|
||||
if (part.type.startsWith("tool-chunkBreak")) {
|
||||
default: {
|
||||
return null
|
||||
}
|
||||
return <ToolInvocationComponent key={partKey} part={part as ToolUIPart} />
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
})
|
||||
})}
|
||||
</>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -32,13 +32,11 @@ export const ToolInvocationComponent: React.FC<ToolInvocationComponentProps> = R
|
|||
className="group border-none"
|
||||
title={
|
||||
<div className="group flex h-6 min-w-0 flex-1 items-center justify-between py-0">
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<div className="text-text-secondary flex items-center gap-2 text-xs">
|
||||
<i
|
||||
className={hasError ? "i-mgc-close-cute-re text-red" : "i-mgc-tool-cute-re"}
|
||||
/>
|
||||
<span className="text-text-secondary">
|
||||
{hasError ? "Tool Failed:" : "Tool Called:"}
|
||||
</span>
|
||||
<span>{hasError ? "Tool Failed:" : "Tool Called:"}</span>
|
||||
<h4 className={`truncate font-medium ${hasError ? "text-red" : "text-text"}`}>
|
||||
{toolName}
|
||||
</h4>
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none"><path fill="#fff" fill-opacity=".01" d="M24 0v24H0V0z"/><path fill="#10161F" d="M6 9v1a1 1 0 0 0 1-1zm3 2a1 1 0 1 0 0 2zm2 4a1 1 0 1 0 2 0zm-3.667.772a1 1 0 1 0-.666-1.886zM18 9h-1a1 1 0 0 0 1 1zm-3 4a1 1 0 1 0 0-2zm2.333.886a1 1 0 1 0-.666 1.886zM11 7v10h2V7zM7 9V7H5v2zm0 8v-2H5v2zm2 2a2 2 0 0 1-2-2H5a4 4 0 0 0 4 4zm2-2a2 2 0 0 1-2 2v2a4 4 0 0 0 4-4zM9 5a2 2 0 0 1 2 2h2a4 4 0 0 0-4-4zm0-2a4 4 0 0 0-4 4h2a2 2 0 0 1 2-2zm0 10a2 2 0 0 1 2 2h2a4 4 0 0 0-4-4zm-3 1a2 2 0 0 1-2-2H2a4 4 0 0 0 4 4zm-2-2a2 2 0 0 1 2-2V8a4 4 0 0 0-4 4zm2.667 1.886A1.996 1.996 0 0 1 6 14v2c.466 0 .915-.08 1.333-.228zM19 9V7h-2v2zm0 8v-2h-2v2zm-4 4a4 4 0 0 0 4-4h-2a2 2 0 0 1-2 2zm-4-4a4 4 0 0 0 4 4v-2a2 2 0 0 1-2-2zm4-14a4 4 0 0 0-4 4h2a2 2 0 0 1 2-2zm0 2a2 2 0 0 1 2 2h2a4 4 0 0 0-4-4zm0 6a4 4 0 0 0-4 4h2a2 2 0 0 1 2-2zm3 5a4 4 0 0 0 4-4h-2a2 2 0 0 1-2 2zm4-4a4 4 0 0 0-4-4v2a2 2 0 0 1 2 2zm-5.333 3.772c.418.148.867.228 1.333.228v-2c-.235 0-.46-.04-.667-.114z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
Loading…
Reference in New Issue