feat(chat): replace thinking indicator with gooey loader (#5525)

* feat(chat): replace thinking indicator with gooey loader

Promotes ThinkingLoader from the landing route group to the shared
components/ui barrel and swaps it in for the 4-square color loader
in the chat's PendingTagIndicator. Removes the now-dead
animate-thinking-block keyframe/animation.

* fix(chat): keep a stable Thinking… label instead of cycling phase text

Greptile flagged that phase text repeatedly re-announces to assistive tech
during a single pending stream; a static label avoids the repeated
live-region updates while keeping the gooey shape morph.
This commit is contained in:
Waleed
2026-07-08 17:57:47 -07:00
committed by GitHub
parent 7bd734ceef
commit 8bbe6f55ca
12 changed files with 11 additions and 31 deletions
@@ -1,5 +1,5 @@
import { ThinkingLoader } from '@/components/ui'
import { WorkflowShowcase } from '@/app/(landing)/components/features/components/build-callout/components/workflow-showcase'
import { ThinkingLoader } from '@/app/(landing)/components/thinking-loader'
/**
* The Build beat's callout - the finished workflow canvas centered on the
@@ -4,7 +4,7 @@ import { useEffect, useRef, useState } from 'react'
import { cn } from '@sim/emcn'
import { Blimp } from '@sim/emcn/icons'
import { ArrowUp, Mic, Paperclip, Plus, Slash } from 'lucide-react'
import { ThinkingLoader } from '@/app/(landing)/components/thinking-loader'
import { ThinkingLoader } from '@/components/ui'
const PROMPT = 'Build a workflow to schedule and publish posts to my X account.'
const REPLY =
@@ -13,8 +13,8 @@ import {
ThumbsDown,
ThumbsUp,
} from 'lucide-react'
import { ThinkingLoader } from '@/components/ui'
import { HERO_TOOLTIP_OFFSET } from '@/app/(landing)/components/hero/components/hero-platform-loop/sidebar-hotspots'
import { ThinkingLoader } from '@/app/(landing)/components/thinking-loader'
/** The conversation the loop plays - mirrors the seeded capture chat. */
const USER_MESSAGE = 'When a new lead signs up, enrich it with company data and post it to #sales.'
@@ -9,6 +9,7 @@ import {
useState,
} from 'react'
import { cn } from '@sim/emcn'
import { ThinkingLoader } from '@/components/ui'
import { BlockHandles } from '@/app/(landing)/components/hero/components/hero-visual/block-handles'
import {
type HomeMode,
@@ -37,7 +38,6 @@ import {
TYPE_MS_PER_ATOM,
WORKFLOW_FOCUS_SCALE,
} from '@/app/(landing)/components/hero/components/hero-visual/workflow-data'
import { ThinkingLoader } from '@/app/(landing)/components/thinking-loader'
/**
* Animated hero visual - the only client island in the hero, decorative and
@@ -2,11 +2,11 @@
import { useEffect, useRef, useState } from 'react'
import { cn } from '@sim/emcn'
import { ThinkingLoader } from '@/components/ui'
import { LandingPreviewChatInput } from '@/app/(landing)/components/landing-preview/components/landing-preview-chat/chat-input'
import { LandingPreviewChatTitleBar } from '@/app/(landing)/components/landing-preview/components/landing-preview-chat/chat-title-bar'
import type { PreviewChat } from '@/app/(landing)/components/landing-preview/components/landing-preview-workflow/workflow-data'
import { useLandingSubmit } from '@/app/(landing)/components/landing-preview/hooks/use-landing-submit'
import { ThinkingLoader } from '@/app/(landing)/components/thinking-loader'
interface LandingPreviewChatProps {
/** The scripted exchange to play, or `null` to show only the input. */
@@ -2,7 +2,7 @@
import { type CSSProperties, type ReactNode, useState } from 'react'
import { cn } from '@sim/emcn'
import { ThinkingLoader } from '@/app/(landing)/components/thinking-loader'
import { ThinkingLoader } from '@/components/ui'
interface LogoMarkProps {
/** Server-rendered Sim wordmark, shown by default. */
@@ -1 +0,0 @@
export { ThinkingLoader, type ThinkingLoaderVariant } from './thinking-loader'
@@ -14,6 +14,7 @@ import {
toast,
} from '@sim/emcn'
import { useParams } from 'next/navigation'
import { ThinkingLoader } from '@/components/ui'
import { canonicalWorkspaceFilePath } from '@/lib/copilot/vfs/path-utils'
import { isSafeHttpUrl } from '@/lib/core/utils/urls'
import { OAUTH_PROVIDERS } from '@/lib/oauth/oauth'
@@ -390,13 +391,6 @@ export function parseSpecialTags(content: string, isStreaming: boolean): ParsedS
return { segments, hasPendingTag }
}
const THINKING_BLOCKS = [
{ color: '#2ABBF8', delay: '0s' },
{ color: '#00F701', delay: '0.2s' },
{ color: '#FA4EDF', delay: '0.6s' },
{ color: '#FFCC02', delay: '0.4s' },
] as const
interface SpecialTagsProps {
segment: Exclude<ContentSegment, { type: 'text' }>
onOptionSelect?: (id: string) => void
@@ -435,17 +429,8 @@ export function SpecialTags({
*/
export function PendingTagIndicator() {
return (
<div className='flex animate-stream-fade-in items-center gap-2 py-2'>
<div className='grid size-[16px] grid-cols-2 gap-[1.5px]'>
{THINKING_BLOCKS.map((block, i) => (
<div
key={i}
className='animate-thinking-block rounded-xs'
style={{ backgroundColor: block.color, animationDelay: block.delay }}
/>
))}
</div>
<span className='text-[var(--text-body)] text-sm'>Thinking…</span>
<div className='animate-stream-fade-in py-2'>
<ThinkingLoader size={20} startVariant='corners' label='Thinking…' labelRatio={0.7} />
</div>
)
}
+1
View File
@@ -14,3 +14,4 @@ export {
SelectTrigger,
SelectValue,
} from './select'
export { ThinkingLoader, type ThinkingLoaderVariant } from './thinking-loader'
@@ -2,7 +2,7 @@
import { type CSSProperties, type ReactNode, useEffect, useId, useState } from 'react'
import { cn } from '@sim/emcn'
import styles from '@/app/(landing)/components/thinking-loader/thinking-loader.module.css'
import styles from '@/components/ui/thinking-loader.module.css'
const VARIANTS = [
'metaballs',
-5
View File
@@ -189,10 +189,6 @@ export default {
from: { opacity: '0' },
to: { opacity: '1' },
},
'thinking-block': {
'0%, 100%': { opacity: '0.15' },
'30%, 55%': { opacity: '1' },
},
'slide-in-right': {
from: { transform: 'translateX(40px)' },
to: { transform: 'translateX(0)' },
@@ -251,7 +247,6 @@ export default {
'ring-pulse': 'ring-pulse 1.5s ease-in-out infinite',
'stream-fade-in': 'stream-fade-in 300ms ease-out forwards',
'stream-fade-in-delayed': 'stream-fade-in 300ms ease-out 1.5s forwards',
'thinking-block': 'thinking-block 1.6s ease-in-out infinite',
'slide-in-right': 'slide-in-right 350ms ease-out forwards',
'slide-in-bottom': 'slide-in-bottom 400ms cubic-bezier(0.16, 1, 0.3, 1)',
'collapsible-down': 'collapsible-down 300ms cubic-bezier(0.4, 0, 0.2, 1)',