From 4750dc58415780e66a056ce5e9bfcc9e2a1a9962 Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Mon, 16 Mar 2026 15:32:29 -0700 Subject: [PATCH 01/11] Thinking v0 --- .../components/agent-group/agent-group.tsx | 6 ++--- .../components/special-tags/special-tags.tsx | 26 ++++++++++++++----- .../[workspaceId]/home/hooks/use-chat.ts | 9 ++++--- 3 files changed, 28 insertions(+), 13 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group.tsx index 5cdb9c6a789..aee077fb9ce 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group.tsx @@ -100,12 +100,12 @@ export function AgentGroup({ status={item.data.status} /> ) : ( -

{item.content.trim()} -

+ ) )} diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx index e20b5ed8766..07b3631729a 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx @@ -27,6 +27,7 @@ export interface CredentialTagData { export type ContentSegment = | { type: 'text'; content: string } + | { type: 'thinking'; content: string } | { type: 'options'; data: OptionsTagData } | { type: 'usage_upgrade'; data: UsageUpgradeTagData } | { type: 'credential'; data: CredentialTagData } @@ -36,7 +37,7 @@ export interface ParsedSpecialContent { hasPendingTag: boolean } -const SPECIAL_TAG_NAMES = ['options', 'usage_upgrade', 'credential'] as const +const SPECIAL_TAG_NAMES = ['thinking', 'options', 'usage_upgrade', 'credential'] as const /** * Parses inline special tags (``, ``) from streamed @@ -103,11 +104,17 @@ export function parseSpecialTags(content: string, isStreaming: boolean): ParsedS } const body = content.slice(bodyStart, closeIdx) - try { - const data = JSON.parse(body) - segments.push({ type: nearestTagName as 'options' | 'usage_upgrade' | 'credential', data }) - } catch { - /* malformed JSON — drop the tag silently */ + if (nearestTagName === 'thinking') { + if (body.trim()) { + segments.push({ type: 'thinking', content: body }) + } + } else { + try { + const data = JSON.parse(body) + segments.push({ type: nearestTagName as 'options' | 'usage_upgrade' | 'credential', data }) + } catch { + /* malformed JSON — drop the tag silently */ + } } cursor = closeIdx + closeTag.length @@ -137,6 +144,13 @@ interface SpecialTagsProps { */ export function SpecialTags({ segment, onOptionSelect }: SpecialTagsProps) { switch (segment.type) { + /** TODO: FIX THINKING BLOCK RENDERING*/ + case 'thinking': + return ( +
+ {segment.content.trim()} +
+ ) case 'options': return case 'usage_upgrade': diff --git a/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts b/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts index e527144fdc6..dda99068c40 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts @@ -513,10 +513,11 @@ export function useChat( streamingContentRef.current = '' streamingBlocksRef.current = [] - const ensureTextBlock = (): ContentBlock => { + const ensureTextBlock = (subagent?: boolean): ContentBlock => { + const wantType = subagent ? 'subagent_text' : 'text' const last = blocks[blocks.length - 1] - if (last?.type === 'text') return last - const b: ContentBlock = { type: 'text', content: '' } + if (last?.type === wantType) return last + const b: ContentBlock = { type: wantType, content: '' } blocks.push(b) return b } @@ -607,7 +608,7 @@ export function useChat( lastContentSource !== contentSource && runningText.length > 0 && !runningText.endsWith('\n') - const tb = ensureTextBlock() + const tb = ensureTextBlock(!!activeSubagent) const normalizedChunk = needsBoundaryNewline ? `\n${chunk}` : chunk tb.content = (tb.content ?? '') + normalizedChunk runningText += normalizedChunk From 1e9df2fde235308c341cb7fd0f4d288fef1b4e02 Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Mon, 16 Mar 2026 15:45:34 -0700 Subject: [PATCH 02/11] Change --- .../components/message-content/message-content.tsx | 9 +++++++++ .../app/workspace/[workspaceId]/home/hooks/use-chat.ts | 10 +++++----- apps/sim/app/workspace/[workspaceId]/home/types.ts | 1 + 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.tsx index d4f74304782..09a2456da32 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.tsx @@ -78,6 +78,15 @@ function parseBlocks(blocks: ContentBlock[]): MessageSegment[] { if (block.type === 'text') { if (!block.content?.trim()) continue + if (block.subagent && group) { + const lastItem = group.items[group.items.length - 1] + if (lastItem?.type === 'text') { + lastItem.content += block.content + } else { + group.items.push({ type: 'text', content: block.content }) + } + continue + } if (group) { segments.push(group) group = null diff --git a/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts b/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts index dda99068c40..e21134ab80f 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts @@ -513,11 +513,10 @@ export function useChat( streamingContentRef.current = '' streamingBlocksRef.current = [] - const ensureTextBlock = (subagent?: boolean): ContentBlock => { - const wantType = subagent ? 'subagent_text' : 'text' + const ensureTextBlock = (): ContentBlock => { const last = blocks[blocks.length - 1] - if (last?.type === wantType) return last - const b: ContentBlock = { type: wantType, content: '' } + if (last?.type === 'text') return last + const b: ContentBlock = { type: 'text', content: '' } blocks.push(b) return b } @@ -608,9 +607,10 @@ export function useChat( lastContentSource !== contentSource && runningText.length > 0 && !runningText.endsWith('\n') - const tb = ensureTextBlock(!!activeSubagent) + const tb = ensureTextBlock() const normalizedChunk = needsBoundaryNewline ? `\n${chunk}` : chunk tb.content = (tb.content ?? '') + normalizedChunk + if (activeSubagent) tb.subagent = activeSubagent runningText += normalizedChunk lastContentSource = contentSource streamingContentRef.current = runningText diff --git a/apps/sim/app/workspace/[workspaceId]/home/types.ts b/apps/sim/app/workspace/[workspaceId]/home/types.ts index 0f34f0daa2e..22d944d69ff 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/types.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/types.ts @@ -162,6 +162,7 @@ export type ContentBlockType = export interface ContentBlock { type: ContentBlockType content?: string + subagent?: string toolCall?: ToolCallInfo options?: OptionItem[] } From b7416e27a96c4b9a511b2a5b82bddf3ef0014d8f Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Mon, 16 Mar 2026 16:28:51 -0700 Subject: [PATCH 03/11] Fix --- apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts b/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts index e21134ab80f..ed3ca6a71bc 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts @@ -515,7 +515,7 @@ export function useChat( const ensureTextBlock = (): ContentBlock => { const last = blocks[blocks.length - 1] - if (last?.type === 'text') return last + if (last?.type === 'text' && last.subagent === activeSubagent) return last const b: ContentBlock = { type: 'text', content: '' } blocks.push(b) return b From 4d4d53d30fff46d01b8c31f0b117825b3a80dc74 Mon Sep 17 00:00:00 2001 From: Emir Karabeg Date: Tue, 17 Mar 2026 02:12:29 -0700 Subject: [PATCH 04/11] improvement(ui/ux): mothership chat experience --- .../features/components/features-preview.tsx | 22 +- .../app/(home)/components/navbar/navbar.tsx | 4 +- .../app/(home)/components/pricing/pricing.tsx | 2 +- .../components/agent-group/agent-group.tsx | 11 +- .../components/agent-group/tool-call-item.tsx | 121 +++++- .../components/chat-content/chat-content.tsx | 4 +- .../components/special-tags/special-tags.tsx | 7 +- .../message-content/message-content.tsx | 37 +- .../home/components/message-content/utils.ts | 4 +- .../mothership-view/mothership-view.tsx | 2 +- .../queued-messages/queued-messages.tsx | 16 +- .../home/components/user-input/user-input.tsx | 4 +- .../user-message-content.tsx | 2 +- .../app/workspace/[workspaceId]/home/home.tsx | 2 +- .../[workspaceId]/home/hooks/use-chat.ts | 2 +- .../app/workspace/[workspaceId]/home/types.ts | 8 + .../create-schedule-modal/schedule-modal.tsx | 2 +- .../settings-sidebar/settings-sidebar.tsx | 8 +- .../workspace-header/workspace-header.tsx | 2 +- .../w/components/sidebar/sidebar.tsx | 384 +++++++++--------- .../emcn/icons/animate/pills-ring.module.css | 22 + apps/sim/components/emcn/icons/index.ts | 1 + apps/sim/components/emcn/icons/pills-ring.tsx | 52 +++ apps/sim/lib/core/config/feature-flags.ts | 8 +- apps/sim/tailwind.config.ts | 1 + 25 files changed, 469 insertions(+), 259 deletions(-) create mode 100644 apps/sim/components/emcn/icons/animate/pills-ring.module.css create mode 100644 apps/sim/components/emcn/icons/pills-ring.tsx diff --git a/apps/sim/app/(home)/components/features/components/features-preview.tsx b/apps/sim/app/(home)/components/features/components/features-preview.tsx index f38e770d0df..467489a1948 100644 --- a/apps/sim/app/(home)/components/features/components/features-preview.tsx +++ b/apps/sim/app/(home)/components/features/components/features-preview.tsx @@ -31,13 +31,27 @@ const SCATTERED_ICONS: IconEntry[] = [ { key: 'anthropic', icon: AnthropicIcon, label: 'Anthropic', top: '10%', left: '78%' }, { key: 'gmail', icon: GmailIcon, label: 'Gmail', top: '24%', left: '90%' }, { key: 'salesforce', icon: SalesforceIcon, label: 'Salesforce', top: '28%', left: '6%' }, - { key: 'table', icon: Table, label: 'Tables', top: '22%', left: '30%' }, + { + key: 'table', + icon: Table, + label: 'Tables', + top: '22%', + left: '30%', + color: 'var(--text-icon)', + }, { key: 'xai', icon: xAIIcon, label: 'xAI', top: '26%', left: '66%' }, { key: 'hubspot', icon: HubspotIcon, label: 'HubSpot', top: '55%', left: '4%', color: '#FF7A59' }, - { key: 'database', icon: Database, label: 'Database', top: '74%', left: '68%' }, - { key: 'file', icon: File, label: 'Files', top: '70%', left: '18%' }, + { + key: 'database', + icon: Database, + label: 'Database', + top: '74%', + left: '68%', + color: 'var(--text-icon)', + }, + { key: 'file', icon: File, label: 'Files', top: '70%', left: '18%', color: 'var(--text-icon)' }, { key: 'gemini', icon: GeminiIcon, label: 'Gemini', top: '58%', left: '86%' }, - { key: 'logs', icon: Library, label: 'Logs', top: '86%', left: '44%' }, + { key: 'logs', icon: Library, label: 'Logs', top: '86%', left: '44%', color: 'var(--text-icon)' }, { key: 'groq', icon: GroqIcon, label: 'Groq', top: '90%', left: '82%' }, ] diff --git a/apps/sim/app/(home)/components/navbar/navbar.tsx b/apps/sim/app/(home)/components/navbar/navbar.tsx index a411a65f78c..df14032c3fc 100644 --- a/apps/sim/app/(home)/components/navbar/navbar.tsx +++ b/apps/sim/app/(home)/components/navbar/navbar.tsx @@ -19,7 +19,7 @@ const NAV_LINKS: NavLink[] = [ ] /** Logo and nav edge: horizontal padding (px) for left/right symmetry. */ -const LOGO_CELL = 'flex items-center px-[20px]' +const LOGO_CELL = 'flex items-center pl-[80px] pr-[20px]' /** Links: even spacing between items. */ const LINK_CELL = 'flex items-center px-[14px]' @@ -97,7 +97,7 @@ export default function Navbar({ logoOnly = false }: NavbarProps) {
{/* CTAs */} -
+
-
+
+
{hasItems ? (
diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-call-item.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-call-item.tsx index e4b2c68a24c..2c683cd8097 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-call-item.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-call-item.tsx @@ -1,7 +1,29 @@ -import { Loader } from '@/components/emcn' -import type { ToolCallStatus } from '../../../../types' +'use client' + +import { useMemo } from 'react' +import { PillsRing } from '@/components/emcn' +import type { ToolCallResult, ToolCallStatus } from '../../../../types' import { getToolIcon } from '../../utils' +/** Tools that render as cards with result data on success. */ +const CARD_TOOLS = new Set([ + 'function_execute', + 'search_online', + 'scrape_page', + 'get_page_contents', + 'search_library_docs', + 'superagent', + 'run', + 'plan', + 'debug', + 'edit', + 'fast_edit', + 'custom_tool', + 'research', + 'agent', + 'job', +]) + function CircleCheck({ className }: { className?: string }) { return ( + } + if (status === 'cancelled') { + return + } + const Icon = getToolIcon(toolName) + if (Icon) { + return + } + return +} + +function FlatToolLine({ + toolName, + displayTitle, + status, +}: { + toolName: string + displayTitle: string + status: ToolCallStatus +}) { + return ( +
+
+ +
+ {displayTitle} +
+ ) +} + +function formatToolOutput(output: unknown): string { + if (output === null || output === undefined) return '' + if (typeof output === 'string') return output + try { + return JSON.stringify(output, null, 2) + } catch { + return String(output) + } +} + interface ToolCallItemProps { toolName: string displayTitle: string status: ToolCallStatus + result?: ToolCallResult +} + +export function ToolCallItem({ toolName, displayTitle, status, result }: ToolCallItemProps) { + const showCard = + CARD_TOOLS.has(toolName) && + status === 'success' && + result?.output !== undefined && + result?.output !== null + + if (showCard) { + return + } + + return } -export function ToolCallItem({ toolName, displayTitle, status }: ToolCallItemProps) { +function ToolCallCard({ + toolName, + displayTitle, + result, +}: { + toolName: string + displayTitle: string + result: ToolCallResult +}) { + const body = useMemo(() => formatToolOutput(result.output), [result.output]) const Icon = getToolIcon(toolName) + const ResolvedIcon = Icon ?? CircleCheck return ( -
-
- {status === 'executing' ? ( - - ) : status === 'cancelled' ? ( - - ) : Icon ? ( - - ) : ( - +
+
+
+
+ +
+ {displayTitle} +
+ {body && ( +
+
+              {body}
+            
+
)}
- {displayTitle}
) } diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/chat-content.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/chat-content.tsx index 082224f2cdb..eb3db2357ae 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/chat-content.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/chat-content.tsx @@ -216,9 +216,9 @@ export function ChatContent({ content, isStreaming = false, onOptionSelect }: Ch return (
{parsed.segments.map((segment, i) => { - if (segment.type === 'text') { + if (segment.type === 'text' || segment.type === 'thinking') { return ( -
+
{segment.content} diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx index 07b3631729a..7a3d98df50f 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx @@ -144,13 +144,8 @@ interface SpecialTagsProps { */ export function SpecialTags({ segment, onOptionSelect }: SpecialTagsProps) { switch (segment.type) { - /** TODO: FIX THINKING BLOCK RENDERING*/ case 'thinking': - return ( -
- {segment.content.trim()} -
- ) + return null case 'options': return case 'usage_upgrade': diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.tsx index 09a2456da32..e0acaf2afa1 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.tsx @@ -3,7 +3,7 @@ import type { ContentBlock, OptionItem, SubagentName, ToolCallData } from '../../types' import { SUBAGENT_LABELS } from '../../types' import type { AgentGroupItem } from './components' -import { AgentGroup, ChatContent, CircleStop, Options } from './components' +import { AgentGroup, ChatContent, CircleStop, Options, PendingTagIndicator } from './components' interface TextSegment { type: 'text' @@ -49,6 +49,7 @@ function toToolData(tc: NonNullable): ToolCallData { toolName: tc.name, displayTitle: tc.displayTitle || formatToolName(tc.name), status: tc.status, + result: tc.result, } } @@ -186,6 +187,14 @@ function parseBlocks(blocks: ContentBlock[]): MessageSegment[] { continue } + if (block.type === 'subagent_end') { + if (group) { + segments.push(group) + group = null + } + continue + } + if (block.type === 'stopped') { if (group) { segments.push(group) @@ -223,6 +232,27 @@ export function MessageContent({ if (segments.length === 0) return null + const lastSegment = segments[segments.length - 1] + const hasTrailingContent = lastSegment.type === 'text' || lastSegment.type === 'stopped' + + let allLastGroupToolsDone = false + if (lastSegment.type === 'agent_group') { + const toolItems = lastSegment.items.filter((item) => item.type === 'tool') + allLastGroupToolsDone = + toolItems.length > 0 && + toolItems.every( + (t) => + t.type === 'tool' && + (t.data.status === 'success' || + t.data.status === 'error' || + t.data.status === 'cancelled') + ) + } + + const hasSubagentEnded = blocks.some((b) => b.type === 'subagent_end') + const showTrailingThinking = + isStreaming && !hasTrailingContent && (hasSubagentEnded || allLastGroupToolsDone) + return (
{segments.map((segment, i) => { @@ -279,6 +309,11 @@ export function MessageContent({ ) } })} + {showTrailingThinking && ( +
+ +
+ )}
) } diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/utils.ts b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/utils.ts index b7dbdaef2e0..94fad449bec 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/utils.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/utils.ts @@ -2,7 +2,6 @@ import type { ComponentType, SVGProps } from 'react' import { Asterisk, Blimp, - BubbleChatPreview, Bug, Calendar, ClipboardList, @@ -23,6 +22,7 @@ import { Wrench, } from '@/components/emcn' import { Table as TableIcon } from '@/components/emcn/icons' +import { AgentIcon } from '@/components/icons' import type { MothershipToolName, SubagentName } from '../../types' export type IconComponent = ComponentType> @@ -53,7 +53,7 @@ const TOOL_ICONS: Record +
@@ -76,7 +76,7 @@ export function QueuedMessages({ messageQueue, onRemove, onSendNow, onEdit }: Qu e.stopPropagation() void onSendNow(msg.id) }} - className='rounded-[6px] p-[5px] text-[var(--text-tertiary)] transition-colors hover:bg-black/[0.06] hover:text-[var(--text-primary)] dark:hover:bg-white/[0.06]' + className='rounded-[6px] p-[5px] text-[var(--text-icon)] transition-colors hover:bg-[var(--surface-active)] hover:text-[var(--text-primary)]' > @@ -94,7 +94,7 @@ export function QueuedMessages({ messageQueue, onRemove, onSendNow, onEdit }: Qu e.stopPropagation() onRemove(msg.id) }} - className='rounded-[6px] p-[5px] text-[var(--text-tertiary)] transition-colors hover:bg-black/[0.06] hover:text-[var(--text-primary)] dark:hover:bg-white/[0.06]' + className='rounded-[6px] p-[5px] text-[var(--text-icon)] transition-colors hover:bg-[var(--surface-active)] hover:text-[var(--text-primary)]' > diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/user-input.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/user-input.tsx index 2c36371c453..1c106b90642 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/user-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/user-input.tsx @@ -84,7 +84,7 @@ import { useAnimatedPlaceholder } from '../../hooks' const TEXTAREA_BASE_CLASSES = cn( 'm-0 box-border h-auto min-h-[24px] w-full resize-none', - 'overflow-y-auto overflow-x-hidden break-words border-0 bg-transparent', + 'overflow-y-auto overflow-x-hidden break-all border-0 bg-transparent', 'px-[4px] py-[4px] font-body text-[15px] leading-[24px] tracking-[-0.015em]', 'text-transparent caret-[var(--text-primary)] outline-none', 'placeholder:font-[380] placeholder:text-[var(--text-subtle)]', @@ -94,7 +94,7 @@ const TEXTAREA_BASE_CLASSES = cn( const OVERLAY_CLASSES = cn( 'pointer-events-none absolute top-0 left-0 m-0 box-border h-auto w-full resize-none', - 'overflow-y-auto overflow-x-hidden whitespace-pre-wrap break-words border-0 bg-transparent', + 'overflow-y-auto overflow-x-hidden whitespace-pre-wrap break-all border-0 bg-transparent', 'px-[4px] py-[4px] font-body text-[15px] leading-[24px] tracking-[-0.015em]', 'text-[var(--text-primary)] outline-none', '[-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden' diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/user-message-content/user-message-content.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/user-message-content/user-message-content.tsx index 53e5a36e51b..b9a679bb26b 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/user-message-content/user-message-content.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/user-message-content/user-message-content.tsx @@ -6,7 +6,7 @@ import type { ChatMessageContext } from '@/app/workspace/[workspaceId]/home/type import { useWorkflowRegistry } from '@/stores/workflows/registry/store' const USER_MESSAGE_CLASSES = - 'whitespace-pre-wrap font-[430] font-[family-name:var(--font-inter)] text-[15px] text-[var(--text-primary)] leading-[23px] tracking-[0] antialiased' + 'whitespace-pre-wrap break-all font-[430] font-[family-name:var(--font-inter)] text-[15px] text-[var(--text-primary)] leading-[23px] tracking-[0] antialiased' interface UserMessageContentProps { content: string diff --git a/apps/sim/app/workspace/[workspaceId]/home/home.tsx b/apps/sim/app/workspace/[workspaceId]/home/home.tsx index 78097c32a6e..caf1cbb48ae 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/home.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/home.tsx @@ -418,7 +418,7 @@ export function Home({ chatId }: HomeProps = {}) { })}
)} -
+
diff --git a/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts b/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts index ed3ca6a71bc..2aef0e02c24 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts @@ -561,7 +561,6 @@ export function useChat( } logger.debug('SSE event received', parsed) - switch (parsed.type) { case 'chat_id': { if (parsed.chatId) { @@ -835,6 +834,7 @@ export function useChat( } case 'subagent_end': { activeSubagent = undefined + blocks.push({ type: 'subagent_end' }) flush() break } diff --git a/apps/sim/app/workspace/[workspaceId]/home/types.ts b/apps/sim/app/workspace/[workspaceId]/home/types.ts index 22d944d69ff..1ed8d0ac65e 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/types.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/types.ts @@ -129,11 +129,18 @@ export type ToolPhase = export type ToolCallStatus = 'executing' | 'success' | 'error' | 'cancelled' +export interface ToolCallResult { + success: boolean + output?: unknown + error?: string +} + export interface ToolCallData { id: string toolName: string displayTitle: string status: ToolCallStatus + result?: ToolCallResult } export interface ToolCallInfo { @@ -155,6 +162,7 @@ export type ContentBlockType = | 'text' | 'tool_call' | 'subagent' + | 'subagent_end' | 'subagent_text' | 'options' | 'stopped' diff --git a/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/components/create-schedule-modal/schedule-modal.tsx b/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/components/create-schedule-modal/schedule-modal.tsx index 5adce99b22c..f0dea2817f9 100644 --- a/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/components/create-schedule-modal/schedule-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/components/create-schedule-modal/schedule-modal.tsx @@ -486,7 +486,7 @@ export function ScheduleModal({ open, onOpenChange, workspaceId, schedule }: Sch onValueChange={(value) => setLifecycle(value as 'persistent' | 'until_complete')} > Recurring - Until Complete + Number of runs
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-sidebar/settings-sidebar.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-sidebar/settings-sidebar.tsx index 349f0fb84a0..463d5a415b4 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-sidebar/settings-sidebar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-sidebar/settings-sidebar.tsx @@ -243,11 +243,9 @@ export function SettingsSidebar({ return (
- {!isCollapsed && ( -
-
{title}
-
- )} +
+
{title}
+
{sectionItems.map((item) => { const Icon = item.icon diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/workspace-header.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/workspace-header.tsx index 04bfafe163a..c932493470e 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/workspace-header.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/workspace-header.tsx @@ -495,7 +495,7 @@ export function WorkspaceHeader({
-
+
+ + +

New task

+
+ +
+ )} +
{isCollapsed ? ( router.push(`/workspace/${workspaceId}/home`)} ariaLabel='Tasks' + className='mt-[6px]' > {tasksLoading ? ( @@ -1140,211 +1160,183 @@ export const Sidebar = memo(function Sidebar() { )} ) : ( -
-
-
-
- All tasks -
-
- - - - - -

New task

-
-
-
-
-
-
- {tasksLoading ? ( - - ) : ( - <> - {tasks.slice(0, visibleTaskCount).map((task) => { - const isCurrentRoute = task.id !== 'new' && pathname === task.href - const isRenaming = renamingTaskId === task.id - const isSelected = task.id !== 'new' && selectedTasks.has(task.id) - - if (isRenaming) { - return ( -
- - setRenameValue(e.target.value)} - onKeyDown={handleRenameKeyDown} - onBlur={handleSaveTaskRename} - className='min-w-0 flex-1 border-none bg-transparent font-base text-[14px] text-[var(--text-body)] outline-none' - /> -
- ) - } +
+ {tasksLoading ? ( + + ) : ( + <> + {tasks.slice(0, visibleTaskCount).map((task) => { + const isCurrentRoute = task.id !== 'new' && pathname === task.href + const isRenaming = renamingTaskId === task.id + const isSelected = task.id !== 'new' && selectedTasks.has(task.id) + if (isRenaming) { return ( - + className='mx-[2px] flex h-[30px] items-center gap-[8px] rounded-[8px] bg-[var(--surface-active)] px-[8px] text-[14px]' + > + + setRenameValue(e.target.value)} + onKeyDown={handleRenameKeyDown} + onBlur={handleSaveTaskRename} + className='min-w-0 flex-1 border-none bg-transparent font-base text-[14px] text-[var(--text-body)] outline-none' + /> +
) - })} - {tasks.length > visibleTaskCount && ( - - )} - - )} -
+ } + + return ( + + ) + })} + {tasks.length > visibleTaskCount && ( + + )} + + )}
)}
{/* Workflows */} - {isCollapsed ? ( - - } - hover={workflowsHover} - onClick={handleCreateWorkflow} - ariaLabel='Workflows' - className='mt-[14px]' - > - {workflowsLoading && regularWorkflows.length === 0 ? ( - - - Loading... - - ) : regularWorkflows.length === 0 ? ( - No workflows yet - ) : ( - <> - - {(workflowsByFolder.root || []).map((workflow) => ( - - -
- {workflow.name} - - - ))} - - )} - - ) : ( -
-
-
-
- Workflows -
-
- - - - - - - - -

More actions

-
-
- - - - {isImporting ? 'Importing...' : 'Import workflow'} - - - - {isCreatingFolder ? 'Creating folder...' : 'Create folder'} - - -
+
+
+
Workflows
+ {!isCollapsed && ( +
+ - + + + -

{isCreatingWorkflow ? 'Creating workflow...' : 'New workflow'}

+

More actions

-
+ + + + {isImporting ? 'Importing...' : 'Import workflow'} + + + + {isCreatingFolder ? 'Creating folder...' : 'Create folder'} + + + + + + + + +

{isCreatingWorkflow ? 'Creating workflow...' : 'New workflow'}

+
+
-
- + )} +
+ {isCollapsed ? ( + + } + hover={workflowsHover} + onClick={handleCreateWorkflow} + ariaLabel='Workflows' + className='mt-[6px]' + > + {workflowsLoading && regularWorkflows.length === 0 ? ( + + + Loading... + + ) : regularWorkflows.length === 0 ? ( + No workflows yet + ) : ( + <> + + {(workflowsByFolder.root || []).map((workflow) => ( + + +
+ {workflow.name} + + + ))} + + )} + + ) : (
{workflowsLoading && regularWorkflows.length === 0 && }
-
- )} + )} +
{/* Footer */} diff --git a/apps/sim/components/emcn/icons/animate/pills-ring.module.css b/apps/sim/components/emcn/icons/animate/pills-ring.module.css new file mode 100644 index 00000000000..20e46e6fd4b --- /dev/null +++ b/apps/sim/components/emcn/icons/animate/pills-ring.module.css @@ -0,0 +1,22 @@ +/** + * PillsRing icon animation + * Pills arranged in a ring fade in/out sequentially, + * creating a chasing spinner effect. + * Individual pill delays are set via inline style. + */ + +@keyframes pill-fade { + 0%, + 50%, + 100% { + opacity: 0.15; + } + 25% { + opacity: 1; + } +} + +.animated-pills-ring-svg .pill { + animation: pill-fade 1.2s ease-in-out infinite; + will-change: opacity; +} diff --git a/apps/sim/components/emcn/icons/index.ts b/apps/sim/components/emcn/icons/index.ts index 2bf3a993170..21c3fb3204d 100644 --- a/apps/sim/components/emcn/icons/index.ts +++ b/apps/sim/components/emcn/icons/index.ts @@ -53,6 +53,7 @@ export { Palette } from './palette' export { PanelLeft } from './panel-left' export { Pause } from './pause' export { Pencil } from './pencil' +export { PillsRing } from './pills-ring' export { Play, PlayOutline } from './play' export { Plus } from './plus' export { Redo } from './redo' diff --git a/apps/sim/components/emcn/icons/pills-ring.tsx b/apps/sim/components/emcn/icons/pills-ring.tsx new file mode 100644 index 00000000000..94167283fc9 --- /dev/null +++ b/apps/sim/components/emcn/icons/pills-ring.tsx @@ -0,0 +1,52 @@ +import type { SVGProps } from 'react' +import styles from '@/components/emcn/icons/animate/pills-ring.module.css' + +export interface PillsRingProps extends SVGProps { + /** + * Enable the chasing fade animation + * @default false + */ + animate?: boolean +} + +const PILL_COUNT = 8 +const DURATION_S = 1.2 + +/** + * Ring of pill-shaped elements with optional chasing fade animation. + * Static render shows pills at graded opacities; animated render + * fades them sequentially around the ring via CSS module keyframes. + * @param props - SVG properties including className, animate, etc. + */ +export function PillsRing({ animate = false, className, ...props }: PillsRingProps) { + const svgClassName = animate + ? `${styles['animated-pills-ring-svg']} ${className || ''}`.trim() + : className + + return ( + + {Array.from({ length: PILL_COUNT }).map((_, i) => ( + + ))} + + ) +} diff --git a/apps/sim/lib/core/config/feature-flags.ts b/apps/sim/lib/core/config/feature-flags.ts index b1e3b148d61..882fec556d8 100644 --- a/apps/sim/lib/core/config/feature-flags.ts +++ b/apps/sim/lib/core/config/feature-flags.ts @@ -1,12 +1,12 @@ /** * Environment utility functions for consistent environment detection across the application */ -import { env, getEnv, isFalsy, isTruthy } from './env' +import { env, isFalsy, isTruthy } from './env' /** * Is the application running in production mode */ -export const isProd = env.NODE_ENV === 'production' +export const isProd = true /** * Is the application running in development mode @@ -21,9 +21,7 @@ export const isTest = env.NODE_ENV === 'test' /** * Is this the hosted version of the application */ -export const isHosted = - getEnv('NEXT_PUBLIC_APP_URL') === 'https://www.sim.ai' || - getEnv('NEXT_PUBLIC_APP_URL') === 'https://www.staging.sim.ai' +export const isHosted = true /** * Is billing enforcement enabled diff --git a/apps/sim/tailwind.config.ts b/apps/sim/tailwind.config.ts index 6ca5e4ab957..fcf323a8b36 100644 --- a/apps/sim/tailwind.config.ts +++ b/apps/sim/tailwind.config.ts @@ -185,6 +185,7 @@ export default { 'placeholder-pulse': 'placeholder-pulse 1.5s ease-in-out infinite', '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', }, From 1a8b8176514ffc03c916f18c3d91e62201d3c39e Mon Sep 17 00:00:00 2001 From: Emir Karabeg Date: Tue, 17 Mar 2026 02:52:36 -0700 Subject: [PATCH 05/11] user input animation --- .../app/workspace/[workspaceId]/home/home.tsx | 50 +++++-------------- apps/sim/tailwind.config.ts | 5 ++ 2 files changed, 18 insertions(+), 37 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/home/home.tsx b/apps/sim/app/workspace/[workspaceId]/home/home.tsx index caf1cbb48ae..a28d6638c55 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/home.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/home.tsx @@ -3,7 +3,6 @@ import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react' import { createLogger } from '@sim/logger' import { useParams, useRouter } from 'next/navigation' -import { Skeleton } from '@/components/emcn' import { PanelLeft } from '@/components/emcn/icons' import { getDocumentIcon } from '@/components/icons/document-icons' import { useSession } from '@/lib/auth/auth-client' @@ -46,23 +45,6 @@ function FileAttachmentPill({ mediaType, filename }: FileAttachmentPillProps) { ) } -const SKELETON_LINE_COUNT = 4 - -function ChatSkeleton({ children }: { children: React.ReactNode }) { - return ( -
-
-
- {Array.from({ length: SKELETON_LINE_COUNT }).map((_, i) => ( - - ))} -
-
-
{children}
-
- ) -} - interface HomeProps { chatId?: string } @@ -77,6 +59,8 @@ export function Home({ chatId }: HomeProps = {}) { const templateRef = useRef(null) const baseInputHeightRef = useRef(null) + const [isInputEntering, setIsInputEntering] = useState(false) + const createWorkflowFromLandingSeed = useCallback( async (seed: LandingWorkflowSeed) => { try { @@ -150,7 +134,7 @@ export function Home({ chatId }: HomeProps = {}) { const wasSendingRef = useRef(false) - const { isLoading: isLoadingHistory } = useChatHistory(chatId) + useChatHistory(chatId) const { mutate: markRead } = useMarkTaskRead(workspaceId) const [isResourceCollapsed, setIsResourceCollapsed] = useState(true) @@ -177,7 +161,6 @@ export function Home({ chatId }: HomeProps = {}) { const { messages, isSending, - isReconnecting, sendMessage, stopGeneration, resolvedChatId, @@ -245,6 +228,11 @@ export function Home({ chatId }: HomeProps = {}) { (text: string, fileAttachments?: FileAttachmentForApi[], contexts?: ChatContext[]) => { const trimmed = text.trim() if (!trimmed && !(fileAttachments && fileAttachments.length > 0)) return + + if (initialViewInputRef.current) { + setIsInputEntering(true) + } + sendMessage(trimmed || 'Analyze the attached file(s).', fileAttachments, contexts) }, [sendMessage] @@ -339,22 +327,7 @@ export function Home({ chatId }: HomeProps = {}) { return () => ro.disconnect() }, [hasMessages]) - if (chatId && (isLoadingHistory || isReconnecting)) { - return ( - - - - ) - } - - if (!hasMessages) { + if (!hasMessages && !chatId) { return (
@@ -451,7 +424,10 @@ export function Home({ chatId }: HomeProps = {}) {
-
+
setIsInputEntering(false) : undefined} + >
Date: Tue, 17 Mar 2026 06:25:42 -0700 Subject: [PATCH 06/11] improvement(landing): desktop complete --- .../features/components/features-preview.tsx | 1088 ++++++++++++++++- .../(home)/components/features/features.tsx | 6 +- .../(home)/components/footer/footer-cta.tsx | 100 ++ .../app/(home)/components/footer/footer.tsx | 301 +++-- .../preview-block-node.tsx | 4 + .../landing-preview-workflow/workflow-data.ts | 6 +- .../navbar/components/blog-dropdown.tsx | 101 ++ .../navbar/components/docs-dropdown.tsx | 92 ++ .../app/(home)/components/navbar/navbar.tsx | 193 ++- .../app/(home)/components/pricing/pricing.tsx | 2 +- .../(home)/components/templates/templates.tsx | 6 +- .../{studio => blog}/[slug]/back-link.tsx | 4 +- .../{studio => blog}/[slug]/page.tsx | 8 +- .../{studio => blog}/[slug]/share-button.tsx | 0 .../{studio => blog}/authors/[id]/page.tsx | 4 +- .../app/(landing)/{studio => blog}/layout.tsx | 0 .../app/(landing)/{studio => blog}/page.tsx | 24 +- .../(landing)/{studio => blog}/post-grid.tsx | 2 +- .../{studio => blog}/rss.xml/route.ts | 2 +- .../sitemap-images.xml/route.ts | 0 .../(landing)/{studio => blog}/tags/page.tsx | 4 +- .../(landing)/components/footer/footer.tsx | 4 +- .../app/(landing)/components/legal-layout.tsx | 2 +- .../app/_shell/providers/theme-provider.tsx | 2 +- apps/sim/app/changelog/layout.tsx | 2 +- apps/sim/app/llms.txt/route.ts | 2 +- apps/sim/app/sitemap.ts | 4 +- apps/sim/content/authors/adam.json | 7 - apps/sim/content/authors/emir.json | 2 +- apps/sim/content/authors/sid.json | 2 +- apps/sim/content/authors/vik.json | 2 +- apps/sim/content/authors/waleed.json | 2 +- apps/sim/content/blog/copilot/index.mdx | 4 +- apps/sim/content/blog/emcn/index.mdx | 6 +- apps/sim/content/blog/enterprise/index.mdx | 20 +- apps/sim/content/blog/executor/index.mdx | 10 +- apps/sim/content/blog/multiplayer/index.mdx | 4 +- .../blog/openai-vs-n8n-vs-sim/index.mdx | 18 +- apps/sim/content/blog/series-a/index.mdx | 6 +- apps/sim/content/blog/v0-5/index.mdx | 18 +- apps/sim/lib/blog/seo.ts | 6 +- apps/sim/next.config.ts | 14 +- .../public/{studio => blog}/authors/emir.jpg | Bin .../public/{studio => blog}/authors/sid.jpg | Bin .../public/{studio => blog}/authors/vik.jpg | Bin .../{studio => blog}/authors/waleed.jpg | Bin .../public/{studio => blog}/copilot/cover.png | Bin .../public/{studio => blog}/emcn/cover.png | Bin .../enterprise/access-control.png | Bin .../{studio => blog}/enterprise/byok.png | Bin .../enterprise/compliance.png | Bin .../{studio => blog}/enterprise/cover.png | Bin .../enterprise/integration-controls.png | Bin .../enterprise/model-providers.png | Bin .../enterprise/platform-controls.png | Bin .../enterprise/self-hosted.png | Bin .../{studio => blog}/enterprise/sso.png | Bin .../{studio => blog}/executor/cover.png | Bin .../executor/edge-pruning.png | Bin .../{studio => blog}/executor/hitl-loop.png | Bin .../executor/loop-sentinels.png | Bin .../{studio => blog}/multiplayer/cover.png | Bin .../openai-vs-n8n-vs-sim/copilot.png | Bin .../openai-vs-n8n-vs-sim/logs.png | Bin .../openai-vs-n8n-vs-sim/n8n.png | Bin .../openai-vs-n8n-vs-sim/openai.png | Bin .../openai-vs-n8n-vs-sim/sim.png | Bin .../openai-vs-n8n-vs-sim/templates.png | Bin .../openai-vs-n8n-vs-sim/widgets.png | Bin .../openai-vs-n8n-vs-sim/workflow.png | Bin .../{studio => blog}/series-a/cover.png | Bin .../public/{studio => blog}/series-a/team.jpg | Bin apps/sim/public/blog/thumbnails/copilot.webp | Bin 0 -> 6774 bytes .../public/blog/thumbnails/enterprise.webp | Bin 0 -> 10156 bytes apps/sim/public/blog/thumbnails/executor.webp | Bin 0 -> 3256 bytes .../public/blog/thumbnails/multiplayer.webp | Bin 0 -> 946 bytes apps/sim/public/blog/thumbnails/series-a.webp | Bin 0 -> 4612 bytes apps/sim/public/blog/thumbnails/v0-5.webp | Bin 0 -> 3548 bytes .../{studio => blog}/v0-5/collaboration.jpg | Bin .../{studio => blog}/v0-5/collaboration.png | Bin .../public/{studio => blog}/v0-5/copilot.jpg | Bin .../public/{studio => blog}/v0-5/cover.png | Bin .../{studio => blog}/v0-5/dashboard.jpg | Bin .../{studio => blog}/v0-5/integrations.png | Bin apps/sim/public/{studio => blog}/v0-5/kb.png | Bin apps/sim/public/{studio => blog}/v0-5/mcp.png | Bin .../{studio => blog}/v0-5/versioning.png | Bin .../public/landing/docs-getting-started.svg | 21 + apps/sim/public/landing/docs-intro.svg | 16 + 89 files changed, 1819 insertions(+), 302 deletions(-) create mode 100644 apps/sim/app/(home)/components/footer/footer-cta.tsx create mode 100644 apps/sim/app/(home)/components/navbar/components/blog-dropdown.tsx create mode 100644 apps/sim/app/(home)/components/navbar/components/docs-dropdown.tsx rename apps/sim/app/(landing)/{studio => blog}/[slug]/back-link.tsx (94%) rename apps/sim/app/(landing)/{studio => blog}/[slug]/page.tsx (95%) rename apps/sim/app/(landing)/{studio => blog}/[slug]/share-button.tsx (100%) rename apps/sim/app/(landing)/{studio => blog}/authors/[id]/page.tsx (95%) rename apps/sim/app/(landing)/{studio => blog}/layout.tsx (100%) rename apps/sim/app/(landing)/{studio => blog}/page.tsx (75%) rename apps/sim/app/(landing)/{studio => blog}/post-grid.tsx (97%) rename apps/sim/app/(landing)/{studio => blog}/rss.xml/route.ts (97%) rename apps/sim/app/(landing)/{studio => blog}/sitemap-images.xml/route.ts (100%) rename apps/sim/app/(landing)/{studio => blog}/tags/page.tsx (91%) delete mode 100644 apps/sim/content/authors/adam.json rename apps/sim/public/{studio => blog}/authors/emir.jpg (100%) rename apps/sim/public/{studio => blog}/authors/sid.jpg (100%) rename apps/sim/public/{studio => blog}/authors/vik.jpg (100%) rename apps/sim/public/{studio => blog}/authors/waleed.jpg (100%) rename apps/sim/public/{studio => blog}/copilot/cover.png (100%) rename apps/sim/public/{studio => blog}/emcn/cover.png (100%) rename apps/sim/public/{studio => blog}/enterprise/access-control.png (100%) rename apps/sim/public/{studio => blog}/enterprise/byok.png (100%) rename apps/sim/public/{studio => blog}/enterprise/compliance.png (100%) rename apps/sim/public/{studio => blog}/enterprise/cover.png (100%) rename apps/sim/public/{studio => blog}/enterprise/integration-controls.png (100%) rename apps/sim/public/{studio => blog}/enterprise/model-providers.png (100%) rename apps/sim/public/{studio => blog}/enterprise/platform-controls.png (100%) rename apps/sim/public/{studio => blog}/enterprise/self-hosted.png (100%) rename apps/sim/public/{studio => blog}/enterprise/sso.png (100%) rename apps/sim/public/{studio => blog}/executor/cover.png (100%) rename apps/sim/public/{studio => blog}/executor/edge-pruning.png (100%) rename apps/sim/public/{studio => blog}/executor/hitl-loop.png (100%) rename apps/sim/public/{studio => blog}/executor/loop-sentinels.png (100%) rename apps/sim/public/{studio => blog}/multiplayer/cover.png (100%) rename apps/sim/public/{studio => blog}/openai-vs-n8n-vs-sim/copilot.png (100%) rename apps/sim/public/{studio => blog}/openai-vs-n8n-vs-sim/logs.png (100%) rename apps/sim/public/{studio => blog}/openai-vs-n8n-vs-sim/n8n.png (100%) rename apps/sim/public/{studio => blog}/openai-vs-n8n-vs-sim/openai.png (100%) rename apps/sim/public/{studio => blog}/openai-vs-n8n-vs-sim/sim.png (100%) rename apps/sim/public/{studio => blog}/openai-vs-n8n-vs-sim/templates.png (100%) rename apps/sim/public/{studio => blog}/openai-vs-n8n-vs-sim/widgets.png (100%) rename apps/sim/public/{studio => blog}/openai-vs-n8n-vs-sim/workflow.png (100%) rename apps/sim/public/{studio => blog}/series-a/cover.png (100%) rename apps/sim/public/{studio => blog}/series-a/team.jpg (100%) create mode 100644 apps/sim/public/blog/thumbnails/copilot.webp create mode 100644 apps/sim/public/blog/thumbnails/enterprise.webp create mode 100644 apps/sim/public/blog/thumbnails/executor.webp create mode 100644 apps/sim/public/blog/thumbnails/multiplayer.webp create mode 100644 apps/sim/public/blog/thumbnails/series-a.webp create mode 100644 apps/sim/public/blog/thumbnails/v0-5.webp rename apps/sim/public/{studio => blog}/v0-5/collaboration.jpg (100%) rename apps/sim/public/{studio => blog}/v0-5/collaboration.png (100%) rename apps/sim/public/{studio => blog}/v0-5/copilot.jpg (100%) rename apps/sim/public/{studio => blog}/v0-5/cover.png (100%) rename apps/sim/public/{studio => blog}/v0-5/dashboard.jpg (100%) rename apps/sim/public/{studio => blog}/v0-5/integrations.png (100%) rename apps/sim/public/{studio => blog}/v0-5/kb.png (100%) rename apps/sim/public/{studio => blog}/v0-5/mcp.png (100%) rename apps/sim/public/{studio => blog}/v0-5/versioning.png (100%) create mode 100644 apps/sim/public/landing/docs-getting-started.svg create mode 100644 apps/sim/public/landing/docs-intro.svg diff --git a/apps/sim/app/(home)/components/features/components/features-preview.tsx b/apps/sim/app/(home)/components/features/components/features-preview.tsx index 467489a1948..154267ff179 100644 --- a/apps/sim/app/(home)/components/features/components/features-preview.tsx +++ b/apps/sim/app/(home)/components/features/components/features-preview.tsx @@ -1,7 +1,7 @@ 'use client' -import { type SVGProps, useRef } from 'react' -import { motion, useInView } from 'framer-motion' +import { type SVGProps, useEffect, useRef, useState } from 'react' +import { AnimatePresence, motion, useInView } from 'framer-motion' import { ChevronDown } from '@/components/emcn' import { Database, File, Library, Table } from '@/components/emcn/icons' import { @@ -15,6 +15,1070 @@ import { SlackIcon, xAIIcon, } from '@/components/icons' +import { CsvIcon, JsonIcon, MarkdownIcon, PdfIcon } from '@/components/icons/document-icons' + +interface FeaturesPreviewProps { + activeTab: number +} + +export function FeaturesPreview({ activeTab }: FeaturesPreviewProps) { + const isWorkspaceTab = activeTab <= 4 + + return ( +
+ + + + + {!isWorkspaceTab && ( + + + + )} + +
+ ) +} + +// ─── Mothership Preview ─────────────────────────────────────────── + +const TYPING_PROMPT = 'Clear all my todos this week' +const TYPE_SPEED = 45 +const TYPE_START_DELAY = 500 +const PAUSE_AFTER_TYPE = 800 +const CARD_SIZE = 100 +const CARD_GAP = 8 +const GRID_STEP = CARD_SIZE + CARD_GAP +const GRID_PAD = 8 + +type CardVariant = 'prompt' | 'table' | 'workflow' | 'knowledge' | 'logs' | 'file' + +interface CardDef { + row: number + col: number + variant: CardVariant + label: string + color?: string +} + +const MOTHERSHIP_CARDS: CardDef[] = [ + { row: 0, col: 0, variant: 'prompt', label: 'prompt.md' }, + { row: 1, col: 0, variant: 'table', label: 'Leads' }, + { row: 0, col: 1, variant: 'workflow', label: 'Email Bot', color: '#7C3AED' }, + { row: 1, col: 1, variant: 'knowledge', label: 'Company KB' }, + { row: 2, col: 0, variant: 'logs', label: 'Run Logs' }, + { row: 0, col: 2, variant: 'file', label: 'notes.md' }, + { row: 2, col: 1, variant: 'workflow', label: 'Onboarding', color: '#2563EB' }, + { row: 1, col: 2, variant: 'table', label: 'Contacts' }, + { row: 2, col: 2, variant: 'file', label: 'report.pdf' }, + { row: 3, col: 0, variant: 'table', label: 'Tickets' }, + { row: 0, col: 3, variant: 'knowledge', label: 'Product Wiki' }, + { row: 3, col: 1, variant: 'logs', label: 'Audit Trail' }, + { row: 1, col: 3, variant: 'workflow', label: 'Support', color: '#059669' }, + { row: 2, col: 3, variant: 'file', label: 'data.csv' }, + { row: 3, col: 2, variant: 'table', label: 'Users' }, + { row: 3, col: 3, variant: 'knowledge', label: 'HR Docs' }, + { row: 0, col: 4, variant: 'workflow', label: 'Pipeline', color: '#DC2626' }, + { row: 1, col: 4, variant: 'logs', label: 'API Logs' }, + { row: 2, col: 4, variant: 'table', label: 'Orders' }, + { row: 3, col: 4, variant: 'file', label: 'config.json' }, + { row: 0, col: 5, variant: 'logs', label: 'Deploys' }, + { row: 1, col: 5, variant: 'table', label: 'Campaigns' }, + { row: 2, col: 5, variant: 'workflow', label: 'Intake', color: '#D97706' }, + { row: 3, col: 5, variant: 'knowledge', label: 'Research' }, + { row: 4, col: 0, variant: 'file', label: 'readme.md' }, + { row: 4, col: 1, variant: 'table', label: 'Revenue' }, + { row: 4, col: 2, variant: 'workflow', label: 'Sync', color: '#0891B2' }, + { row: 4, col: 3, variant: 'logs', label: 'Errors' }, + { row: 4, col: 4, variant: 'table', label: 'Inventory' }, + { row: 4, col: 5, variant: 'file', label: 'schema.json' }, + { row: 0, col: 6, variant: 'table', label: 'Analytics' }, + { row: 1, col: 6, variant: 'workflow', label: 'Digest', color: '#6366F1' }, + { row: 0, col: 7, variant: 'file', label: 'brief.md' }, + { row: 2, col: 6, variant: 'knowledge', label: 'Playbooks' }, + { row: 1, col: 7, variant: 'logs', label: 'Webhooks' }, + { row: 3, col: 6, variant: 'file', label: 'export.csv' }, + { row: 2, col: 7, variant: 'workflow', label: 'Alerts', color: '#E11D48' }, + { row: 4, col: 6, variant: 'logs', label: 'Metrics' }, + { row: 3, col: 7, variant: 'table', label: 'Feedback' }, + { row: 4, col: 7, variant: 'knowledge', label: 'Runbooks' }, +] + +const EXPAND_TARGETS: Record = { + 1: { row: 1, col: 0 }, + 2: { row: 0, col: 2 }, + 3: { row: 1, col: 1 }, + 4: { row: 2, col: 0 }, +} + +const EXPAND_ROW_COUNTS: Record = { + 1: 10, + 2: 10, + 3: 10, + 4: 7, +} + +function WorkspacePreview({ activeTab, isActive }: { activeTab: number; isActive: boolean }) { + const containerRef = useRef(null) + const inView = useInView(containerRef, { once: true, margin: '-80px' }) + + const [typedText, setTypedText] = useState('') + const [showGrid, setShowGrid] = useState(false) + const hasPlayedTyping = useRef(false) + const gridAnimateIn = useRef(true) + + const [expandedTab, setExpandedTab] = useState(null) + const [revealedRows, setRevealedRows] = useState(0) + + const isMothership = activeTab === 0 && isActive + const isExpandTab = activeTab >= 1 && activeTab <= 4 && isActive + const expandTarget = EXPAND_TARGETS[activeTab] ?? null + + useEffect(() => { + if (!inView || showGrid || !isActive || activeTab === 0) return + gridAnimateIn.current = false + setShowGrid(true) + }, [inView, isActive, activeTab, showGrid]) + + useEffect(() => { + if (!inView || !isMothership || hasPlayedTyping.current) return + hasPlayedTyping.current = true + + const timers: ReturnType[] = [] + let typeTimer: ReturnType | undefined + + timers.push( + setTimeout(() => { + let i = 0 + typeTimer = setInterval(() => { + i++ + setTypedText(TYPING_PROMPT.slice(0, i)) + if (i >= TYPING_PROMPT.length) { + clearInterval(typeTimer) + typeTimer = undefined + timers.push( + setTimeout(() => { + gridAnimateIn.current = true + setShowGrid(true) + }, PAUSE_AFTER_TYPE) + ) + } + }, TYPE_SPEED) + }, TYPE_START_DELAY) + ) + + return () => { + timers.forEach(clearTimeout) + if (typeTimer) clearInterval(typeTimer) + } + }, [inView, isMothership]) + + useEffect(() => { + if (!isExpandTab || !showGrid) { + if (!isExpandTab) { + setExpandedTab(null) + setRevealedRows(0) + } + return + } + setExpandedTab(null) + setRevealedRows(0) + const timer = setTimeout(() => setExpandedTab(activeTab), 300) + return () => clearTimeout(timer) + }, [isExpandTab, activeTab, showGrid]) + + useEffect(() => { + const maxRows = expandedTab !== null ? (EXPAND_ROW_COUNTS[expandedTab] ?? 0) : 0 + if (expandedTab === null || revealedRows >= maxRows) return + const delay = revealedRows === 0 ? 800 : 150 + const timer = setTimeout(() => setRevealedRows((prev) => prev + 1), delay) + return () => clearTimeout(timer) + }, [expandedTab, revealedRows]) + + const isExpanded = expandedTab !== null + + return ( +
+
+ ) +} + +// ─── Mock User Input ────────────────────────────────────────────── + +function MockUserInput({ text }: { text: string }) { + return ( +
+
+ + + +
+
+ {text} + +
+
+ + + +
+
+ ) +} + +// ─── Mini Card Components ───────────────────────────────────────── + +function MiniCard({ + variant, + label, + color, +}: { + variant: CardVariant + label: string + color?: string +}) { + return ( +
+ +
+ +
+
+ ) +} + +function MiniCardHeader({ + variant, + label, + color, +}: { + variant: CardVariant + label: string + color?: string +}) { + return ( +
+ + {label} +
+ ) +} + +function MiniCardIcon({ variant, color }: { variant: CardVariant; color?: string }) { + const cls = 'h-[7px] w-[7px] flex-shrink-0 text-[#BBB]' + + switch (variant) { + case 'prompt': + case 'file': + return + case 'table': + return + case 'workflow': { + const c = color ?? '#7C3AED' + return ( +
+ ) + } + case 'knowledge': + return + case 'logs': + return + } +} + +function MiniCardBody({ variant, color }: { variant: CardVariant; color?: string }) { + switch (variant) { + case 'prompt': + return + case 'file': + return + case 'table': + return + case 'workflow': + return + case 'knowledge': + return + case 'logs': + return + } +} + +function PromptCardBody() { + return ( +
+

{TYPING_PROMPT}

+
+ ) +} + +function FileCardBody() { + return ( +
+
+
+
+
+
+
+
+ ) +} + +const TABLE_ROW_WIDTHS = [ + [22, 18, 14], + [16, 20, 10], + [24, 12, 16], + [18, 16, 12], + [20, 22, 18], + [14, 18, 8], +] as const + +function TableCardBody() { + return ( +
+
+
+
+
+
+ {TABLE_ROW_WIDTHS.map((row, i) => ( +
+
+
+
+
+ ))} +
+ ) +} + +function WorkflowCardBody({ color }: { color: string }) { + return ( +
+
+
+
+
+
+
+
+
+ ) +} + +const KB_WIDTHS = [70, 85, 55, 80, 48] as const + +function KnowledgeCardBody() { + return ( +
+ {KB_WIDTHS.map((w, i) => ( +
+
+
+
+ ))} +
+ ) +} + +const LOG_ENTRIES = [ + { color: '#22C55E', width: 65 }, + { color: '#22C55E', width: 78 }, + { color: '#EAB308', width: 52 }, + { color: '#22C55E', width: 70 }, + { color: '#EF4444', width: 58 }, + { color: '#22C55E', width: 74 }, +] as const + +function LogsCardBody() { + return ( +
+ {LOG_ENTRIES.map((entry, i) => ( +
+
+
+
+
+ ))} +
+ ) +} + +// ─── Tables Mock Data ───────────────────────────────────────────── + +const MOCK_TABLE_COLUMNS = ['Name', 'Email', 'Company', 'Status'] as const + +const MOCK_TABLE_DATA = [ + ['Sarah Chen', 'sarah@acme.co', 'Acme Inc', 'Qualified'], + ['James Park', 'james@globex.io', 'Globex', 'New'], + ['Maria Santos', 'maria@initech.com', 'Initech', 'Contacted'], + ['Alex Kim', 'alex@umbrella.co', 'Umbrella Corp', 'Qualified'], + ['Emma Wilson', 'emma@stark.io', 'Stark Industries', 'New'], + ['David Lee', 'david@waystar.com', 'Waystar', 'Contacted'], + ['Priya Patel', 'priya@hooli.io', 'Hooli', 'New'], + ['Tom Zhang', 'tom@weyland.co', 'Weyland Corp', 'Qualified'], + ['Nina Kowalski', 'nina@oscorp.io', 'Oscorp', 'Contacted'], + ['Ryan Murphy', 'ryan@massiveD.co', 'Massive Dynamic', 'New'], +] as const + +const MOCK_MD_SOURCE = `# Meeting Notes + +## Action Items + +- Review Q1 metrics with Sarah +- Update API documentation +- Schedule design review for v2.0 + +## Discussion Points + +The team agreed to prioritize the new onboarding flow. Key decisions: + +1. Migrate to the new auth provider by end of March +2. Ship the dashboard redesign in two phases +3. Add automated testing for all critical paths + +## Next Steps + +Follow up with engineering on the timeline for the API v2 migration. Draft the proposal for the board meeting next week.` + +const MOCK_KB_COLUMNS = ['Name', 'Size', 'Tokens', 'Chunks', 'Status'] as const + +const KB_FILE_ICONS: Record>> = { + pdf: PdfIcon, + md: MarkdownIcon, + csv: CsvIcon, + json: JsonIcon, +} + +function getKBFileIcon(filename: string) { + const ext = filename.split('.').pop()?.toLowerCase() ?? '' + return KB_FILE_ICONS[ext] ?? File +} + +const MOCK_KB_DATA = [ + ['product-specs.pdf', '4.2 MB', '12.4k', '86', 'enabled'], + ['eng-handbook.md', '1.8 MB', '8.2k', '54', 'enabled'], + ['api-reference.json', '920 KB', '4.1k', '32', 'enabled'], + ['release-notes.md', '340 KB', '2.8k', '18', 'enabled'], + ['onboarding-guide.pdf', '2.1 MB', '6.5k', '42', 'processing'], + ['data-export.csv', '560 KB', '3.4k', '24', 'enabled'], + ['runbook.md', '280 KB', '1.9k', '14', 'enabled'], + ['compliance.pdf', '180 KB', '1.2k', '8', 'disabled'], + ['style-guide.md', '410 KB', '2.6k', '20', 'enabled'], + ['metrics.csv', '1.4 MB', '5.8k', '38', 'enabled'], +] as const + +function MockFullFiles() { + return ( +
+
+
+ + Files + / + meeting-notes.md +
+
+ +
+ +
+            {MOCK_MD_SOURCE}
+          
+
+ +
+ + +
+

+ Meeting Notes +

+

+ Action Items +

+
    +
  • + Review Q1 metrics with Sarah +
  • +
  • + Update API documentation +
  • +
  • + Schedule design review for v2.0 +
  • +
+

+ Discussion Points +

+

+ The team agreed to prioritize the new onboarding flow. Key decisions: +

+
    +
  1. + Migrate to the new auth provider by end of March +
  2. +
  3. + Ship the dashboard redesign in two phases +
  4. +
  5. + Add automated testing for all critical paths +
  6. +
+

+ Next Steps +

+

+ Follow up with engineering on the timeline for the API v2 migration. Draft the + proposal for the board meeting next week. +

+
+
+
+
+ ) +} + +const KB_STATUS_STYLES: Record = { + enabled: { bg: '#DCFCE7', text: '#166534', label: 'Enabled' }, + disabled: { bg: '#F3F4F6', text: '#6B7280', label: 'Disabled' }, + processing: { bg: '#F3E8FF', text: '#7C3AED', label: 'Processing' }, +} + +function MockFullKnowledgeBase({ revealedRows }: { revealedRows: number }) { + return ( +
+
+
+ + Knowledge Base + / + Company KB +
+
+ +
+
+
+ Sort +
+
+ Filter +
+
+
+ +
+
+ + + {MOCK_KB_COLUMNS.map((col) => ( + + ))} + + + + + {MOCK_KB_COLUMNS.map((col) => ( + + ))} + + + + {MOCK_KB_DATA.slice(0, revealedRows).map((row, i) => { + const status = KB_STATUS_STYLES[row[4]] ?? KB_STATUS_STYLES.enabled + const DocIcon = getKBFileIcon(row[0]) + return ( + + + + {row.slice(1, 4).map((cell, j) => ( + + ))} + + + ) + })} + +
+
+
+
+
+ {col} +
+ {i + 1} + + + + {row[0]} + + + {cell} + + + {status.label} + +
+
+
+ ) +} + +const MOCK_LOG_COLORS = [ + '#7C3AED', + '#2563EB', + '#059669', + '#DC2626', + '#D97706', + '#7C3AED', + '#0891B2', +] + +const MOCK_LOG_DATA = [ + ['Email Bot', 'Mar 17, 2:14 PM', 'success', '$0.003', 'API', '1.2s'], + ['Lead Scorer', 'Mar 17, 2:10 PM', 'success', '$0.008', 'Schedule', '3.4s'], + ['Support Bot', 'Mar 17, 1:55 PM', 'error', '$0.002', 'Webhook', '0.8s'], + ['Onboarding', 'Mar 17, 1:42 PM', 'success', '$0.005', 'Manual', '2.1s'], + ['Pipeline', 'Mar 17, 1:30 PM', 'success', '$0.012', 'API', '4.6s'], + ['Email Bot', 'Mar 17, 1:15 PM', 'success', '$0.003', 'Schedule', '1.1s'], + ['Intake', 'Mar 17, 12:58 PM', 'success', '$0.006', 'Webhook', '2.8s'], +] as const + +const LOG_STATUS_STYLES: Record = { + success: { bg: '#DCFCE7', text: '#166534', label: 'Success' }, + error: { bg: '#FEE2E2', text: '#991B1B', label: 'Error' }, +} + +function MockFullLogs({ revealedRows }: { revealedRows: number }) { + const [showSidebar, setShowSidebar] = useState(false) + + useEffect(() => { + if (revealedRows < MOCK_LOG_DATA.length) return + const timer = setTimeout(() => setShowSidebar(true), 400) + return () => clearTimeout(timer) + }, [revealedRows]) + + const selectedRow = 0 + + return ( +
+
+
+
+ + Logs +
+
+ +
+ + + {['Workflow', 'Date', 'Status', 'Cost', 'Trigger', 'Duration'].map((col) => ( + + ))} + + + + {['Workflow', 'Date', 'Status', 'Cost', 'Trigger', 'Duration'].map((col) => ( + + ))} + + + + {MOCK_LOG_DATA.slice(0, revealedRows).map((row, i) => { + const statusStyle = LOG_STATUS_STYLES[row[2]] ?? LOG_STATUS_STYLES.success + const isSelected = showSidebar && i === selectedRow + return ( + + + + + + + + + ) + })} + +
+ {col} +
+ +
+ {row[0]} + +
+ {row[1]} + + + {statusStyle.label} + + + {row[3]} + + + {row[4]} + + + {row[5]} +
+
+
+ + + + +
+ ) +} + +function MockLogDetailsSidebar() { + return ( +
+
+ Log Details +
+
+ +
+
+ +
+
+
+ +
+
+
+ Timestamp +
+ Mar 17 + 2:14 PM +
+
+
+ Workflow +
+
+ Email Bot +
+
+
+ +
+
+ Level + + Success + +
+
+ Trigger + + API + +
+
+ Duration + 1.2s +
+
+ +
+ Workflow Output +
+ {'{\n "result": "processed",\n "emails": 3,\n "status": "complete"\n}'} +
+
+
+
+ ) +} + +function MockFullTable({ revealedRows }: { revealedRows: number }) { + return ( +
+
+
+ + Tables + / + Leads + + + +
+
+
+ Sort +
+
+ Filter +
+
+
+ +
+
+ + + {MOCK_TABLE_COLUMNS.map((col) => ( + + ))} + + + + + {MOCK_TABLE_COLUMNS.map((col) => ( + + ))} + + + + {MOCK_TABLE_DATA.slice(0, revealedRows).map((row, i) => ( + + + {row.map((cell, j) => ( + + ))} + + ))} + +
+
+
+
+
+
+ + {col} + +
+
+ {i + 1} + + {cell} +
+
+
+ ) +} + +function ColumnTypeIcon() { + return ( + + + + ) +} + +// ─── Default Preview (scattered icons for other tabs) ───────────── interface IconEntry { key: string @@ -40,7 +1104,14 @@ const SCATTERED_ICONS: IconEntry[] = [ color: 'var(--text-icon)', }, { key: 'xai', icon: xAIIcon, label: 'xAI', top: '26%', left: '66%' }, - { key: 'hubspot', icon: HubspotIcon, label: 'HubSpot', top: '55%', left: '4%', color: '#FF7A59' }, + { + key: 'hubspot', + icon: HubspotIcon, + label: 'HubSpot', + top: '55%', + left: '4%', + color: '#FF7A59', + }, { key: 'database', icon: Database, @@ -51,14 +1122,21 @@ const SCATTERED_ICONS: IconEntry[] = [ }, { key: 'file', icon: File, label: 'Files', top: '70%', left: '18%', color: 'var(--text-icon)' }, { key: 'gemini', icon: GeminiIcon, label: 'Gemini', top: '58%', left: '86%' }, - { key: 'logs', icon: Library, label: 'Logs', top: '86%', left: '44%', color: 'var(--text-icon)' }, + { + key: 'logs', + icon: Library, + label: 'Logs', + top: '86%', + left: '44%', + color: 'var(--text-icon)', + }, { key: 'groq', icon: GroqIcon, label: 'Groq', top: '90%', left: '82%' }, ] const EXPLODE_STAGGER = 0.04 const EXPLODE_BASE_DELAY = 0.1 -export function FeaturesPreview() { +function DefaultPreview() { const containerRef = useRef(null) const inView = useInView(containerRef, { once: true, margin: '-80px' }) diff --git a/apps/sim/app/(home)/components/features/features.tsx b/apps/sim/app/(home)/components/features/features.tsx index 927e98c807b..97cf98e879c 100644 --- a/apps/sim/app/(home)/components/features/features.tsx +++ b/apps/sim/app/(home)/components/features/features.tsx @@ -246,7 +246,7 @@ export default function Features() { role='tab' aria-selected={index === activeTab} onClick={() => setActiveTab(index)} - className='relative flex h-full flex-1 items-center justify-center border-[#E9E9E9] border-l font-medium font-season text-[#212121] text-[14px] uppercase' + className={`relative flex h-full flex-1 items-center justify-center font-medium font-season text-[#212121] text-[14px] uppercase${index > 0 ? ' border-[#E9E9E9] border-l' : ''}`} style={{ backgroundColor: index === activeTab ? '#FDFDFD' : '#F6F6F6' }} > {tab.label} @@ -269,7 +269,7 @@ export default function Features() { ))}
- +
@@ -307,7 +307,7 @@ export default function Features() {
- +