@@ -34,9 +24,7 @@ export const StreamingIndicator = memo(({ className }: StreamingIndicatorProps)
StreamingIndicator.displayName = 'StreamingIndicator'
-/**
- * Props for the SmoothStreamingText component
- */
+/** Props for the SmoothStreamingText component */
interface SmoothStreamingTextProps {
/** Content to display with streaming animation */
content: string
@@ -44,20 +32,12 @@ interface SmoothStreamingTextProps {
isStreaming: boolean
}
-/**
- * SmoothStreamingText component displays text with character-by-character animation
- * Creates a smooth streaming effect for AI responses
- *
- * @param props - Component props
- * @returns Streaming text with smooth animation
- */
+/** Displays text with character-by-character animation for smooth streaming */
export const SmoothStreamingText = memo(
({ content, isStreaming }: SmoothStreamingTextProps) => {
- // Initialize with full content when not streaming to avoid flash on page load
const [displayedContent, setDisplayedContent] = useState(() => (isStreaming ? '' : content))
const contentRef = useRef(content)
const timeoutRef = useRef
(null)
- // Initialize index based on streaming state
const indexRef = useRef(isStreaming ? 0 : content.length)
const isAnimatingRef = useRef(false)
@@ -95,7 +75,6 @@ export const SmoothStreamingText = memo(
}
}
} else {
- // Streaming ended - show full content immediately
if (timeoutRef.current) {
clearTimeout(timeoutRef.current)
}
@@ -119,7 +98,6 @@ export const SmoothStreamingText = memo(
)
},
(prevProps, nextProps) => {
- // Prevent re-renders during streaming unless content actually changed
return (
prevProps.content === nextProps.content && prevProps.isStreaming === nextProps.isStreaming
)
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/thinking-block/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/thinking-block/index.ts
new file mode 100644
index 0000000000..515f72bb04
--- /dev/null
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/thinking-block/index.ts
@@ -0,0 +1 @@
+export * from './thinking-block'
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/thinking-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/thinking-block/thinking-block.tsx
similarity index 77%
rename from apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/thinking-block.tsx
rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/thinking-block/thinking-block.tsx
index 2b5b023362..de632ca5f4 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/thinking-block.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/thinking-block/thinking-block.tsx
@@ -3,66 +3,45 @@
import { memo, useEffect, useMemo, useRef, useState } from 'react'
import clsx from 'clsx'
import { ChevronUp } from 'lucide-react'
-import CopilotMarkdownRenderer from './markdown-renderer'
+import { CopilotMarkdownRenderer } from '../markdown-renderer'
-/**
- * Removes thinking tags (raw or escaped) from streamed content.
- */
+/** Removes thinking tags (raw or escaped) and special tags from streamed content */
function stripThinkingTags(text: string): string {
return text
.replace(/<\/?thinking[^>]*>/gi, '')
.replace(/<\/?thinking[^&]*>/gi, '')
+ .replace(/[\s\S]*?<\/options>/gi, '')
+ .replace(/[\s\S]*$/gi, '')
+ .replace(/[\s\S]*?<\/plan>/gi, '')
+ .replace(/[\s\S]*$/gi, '')
.trim()
}
-/**
- * Max height for thinking content before internal scrolling kicks in
- */
-const THINKING_MAX_HEIGHT = 150
-
-/**
- * Height threshold before gradient fade kicks in
- */
-const GRADIENT_THRESHOLD = 100
-
-/**
- * Interval for auto-scroll during streaming (ms)
- */
+/** Interval for auto-scroll during streaming (ms) */
const SCROLL_INTERVAL = 50
-/**
- * Timer update interval in milliseconds
- */
+/** Timer update interval in milliseconds */
const TIMER_UPDATE_INTERVAL = 100
-/**
- * Thinking text streaming - much faster than main text
- * Essentially instant with minimal delay
- */
+/** Thinking text streaming delay - faster than main text */
const THINKING_DELAY = 0.5
const THINKING_CHARS_PER_FRAME = 3
-/**
- * Props for the SmoothThinkingText component
- */
+/** Props for the SmoothThinkingText component */
interface SmoothThinkingTextProps {
content: string
isStreaming: boolean
}
/**
- * SmoothThinkingText renders thinking content with fast streaming animation
- * Uses gradient fade at top when content is tall enough
+ * Renders thinking content with fast streaming animation.
*/
const SmoothThinkingText = memo(
({ content, isStreaming }: SmoothThinkingTextProps) => {
- // Initialize with full content when not streaming to avoid flash on page load
const [displayedContent, setDisplayedContent] = useState(() => (isStreaming ? '' : content))
- const [showGradient, setShowGradient] = useState(false)
const contentRef = useRef(content)
const textRef = useRef(null)
const rafRef = useRef(null)
- // Initialize index based on streaming state
const indexRef = useRef(isStreaming ? 0 : content.length)
const lastFrameTimeRef = useRef(0)
const isAnimatingRef = useRef(false)
@@ -88,7 +67,6 @@ const SmoothThinkingText = memo(
if (elapsed >= THINKING_DELAY) {
if (currentIndex < currentContent.length) {
- // Reveal multiple characters per frame for faster streaming
const newIndex = Math.min(
currentIndex + THINKING_CHARS_PER_FRAME,
currentContent.length
@@ -110,7 +88,6 @@ const SmoothThinkingText = memo(
rafRef.current = requestAnimationFrame(animateText)
}
} else {
- // Streaming ended - show full content immediately
if (rafRef.current) {
cancelAnimationFrame(rafRef.current)
}
@@ -127,30 +104,10 @@ const SmoothThinkingText = memo(
}
}, [content, isStreaming])
- // Check if content height exceeds threshold for gradient
- useEffect(() => {
- if (textRef.current && isStreaming) {
- const height = textRef.current.scrollHeight
- setShowGradient(height > GRADIENT_THRESHOLD)
- } else {
- setShowGradient(false)
- }
- }, [displayedContent, isStreaming])
-
- // Apply vertical gradient fade at the top only when content is tall enough
- const gradientStyle =
- isStreaming && showGradient
- ? {
- maskImage: 'linear-gradient(to bottom, transparent 0%, black 30%, black 100%)',
- WebkitMaskImage: 'linear-gradient(to bottom, transparent 0%, black 30%, black 100%)',
- }
- : undefined
-
return (
@@ -165,9 +122,7 @@ const SmoothThinkingText = memo(
SmoothThinkingText.displayName = 'SmoothThinkingText'
-/**
- * Props for the ThinkingBlock component
- */
+/** Props for the ThinkingBlock component */
interface ThinkingBlockProps {
/** Content of the thinking block */
content: string
@@ -182,13 +137,8 @@ interface ThinkingBlockProps {
}
/**
- * ThinkingBlock component displays AI reasoning/thinking process
- * Shows collapsible content with duration timer
- * Auto-expands during streaming and collapses when complete
- * Auto-collapses when a tool call or other content comes in after it
- *
- * @param props - Component props
- * @returns Thinking block with expandable content and timer
+ * Displays AI reasoning/thinking process with collapsible content and duration timer.
+ * Auto-expands during streaming and collapses when complete.
*/
export function ThinkingBlock({
content,
@@ -197,7 +147,6 @@ export function ThinkingBlock({
label = 'Thought',
hasSpecialTags = false,
}: ThinkingBlockProps) {
- // Strip thinking tags from content on render to handle persisted messages
const cleanContent = useMemo(() => stripThinkingTags(content || ''), [content])
const [isExpanded, setIsExpanded] = useState(false)
@@ -209,12 +158,8 @@ export function ThinkingBlock({
const lastScrollTopRef = useRef(0)
const programmaticScrollRef = useRef(false)
- /**
- * Auto-expands block when streaming with content
- * Auto-collapses when streaming ends OR when following content arrives
- */
+ /** Auto-expands during streaming, auto-collapses when streaming ends or following content arrives */
useEffect(() => {
- // Collapse if streaming ended, there's following content, or special tags arrived
if (!isStreaming || hasFollowingContent || hasSpecialTags) {
setIsExpanded(false)
userCollapsedRef.current = false
@@ -227,7 +172,6 @@ export function ThinkingBlock({
}
}, [isStreaming, cleanContent, hasFollowingContent, hasSpecialTags])
- // Reset start time when streaming begins
useEffect(() => {
if (isStreaming && !hasFollowingContent) {
startTimeRef.current = Date.now()
@@ -236,9 +180,7 @@ export function ThinkingBlock({
}
}, [isStreaming, hasFollowingContent])
- // Update duration timer during streaming (stop when following content arrives)
useEffect(() => {
- // Stop timer if not streaming or if there's following content (thinking is done)
if (!isStreaming || hasFollowingContent) return
const interval = setInterval(() => {
@@ -248,7 +190,6 @@ export function ThinkingBlock({
return () => clearInterval(interval)
}, [isStreaming, hasFollowingContent])
- // Handle scroll events to detect user scrolling away
useEffect(() => {
const container = scrollContainerRef.current
if (!container || !isExpanded) return
@@ -267,7 +208,6 @@ export function ThinkingBlock({
setUserHasScrolledAway(true)
}
- // Re-stick if user scrolls back to bottom with intent
if (userHasScrolledAway && isNearBottom && delta > 10) {
setUserHasScrolledAway(false)
}
@@ -281,7 +221,6 @@ export function ThinkingBlock({
return () => container.removeEventListener('scroll', handleScroll)
}, [isExpanded, userHasScrolledAway])
- // Smart auto-scroll: always scroll to bottom while streaming unless user scrolled away
useEffect(() => {
if (!isStreaming || !isExpanded || userHasScrolledAway) return
@@ -302,20 +241,16 @@ export function ThinkingBlock({
return () => window.clearInterval(intervalId)
}, [isStreaming, isExpanded, userHasScrolledAway])
- /**
- * Formats duration in milliseconds to seconds
- * Always shows seconds, rounded to nearest whole second, minimum 1s
- */
+ /** Formats duration in milliseconds to seconds (minimum 1s) */
const formatDuration = (ms: number) => {
const seconds = Math.max(1, Math.round(ms / 1000))
return `${seconds}s`
}
const hasContent = cleanContent.length > 0
- // Thinking is "done" when streaming ends OR when there's following content (like a tool call) OR when special tags appear
const isThinkingDone = !isStreaming || hasFollowingContent || hasSpecialTags
const durationText = `${label} for ${formatDuration(duration)}`
- // Convert past tense label to present tense for streaming (e.g., "Thought" → "Thinking")
+
const getStreamingLabel = (lbl: string) => {
if (lbl === 'Thought') return 'Thinking'
if (lbl.endsWith('ed')) return `${lbl.slice(0, -2)}ing`
@@ -323,11 +258,9 @@ export function ThinkingBlock({
}
const streamingLabel = getStreamingLabel(label)
- // During streaming: show header with shimmer effect + expanded content
if (!isThinkingDone) {
return (
- {/* Define shimmer keyframes */}