fix(platform): restore M1/M2/M3 regressions and LazyMotion on landing page

M1 — Invitation guard: re-introduce usePermissionConfig().isInvitationsDisabled
alongside the workspace inviteDisabledReason check. The flag now also
respects NEXT_PUBLIC_DISABLE_INVITATIONS and EE permission-group
disableInvitations, not just billing policy.

M2 — Settings redirects: /settings/integrations and /settings/skills
now server-redirect to /integrations and /skills respectively so old
bookmarks and emails don't silently land on General.

M3 — Starter block search exclusion: restore block.type !== 'starter'
guard in the search store so users cannot add a duplicate Starter block
via the command palette.

LazyMotion: restore LazyMotion + domMax/domAnimation wrappers and m.*
components in landing-preview-panel and landing-preview-home. The
removal was accidental (the full motion bundle was left after an
import cleanup), which caused the entire framer-motion feature set to
load eagerly on the landing page.
This commit is contained in:
waleed
2026-06-01 15:18:24 -07:00
parent 0447e4a348
commit cca5054cf6
5 changed files with 329 additions and 311 deletions
@@ -1,7 +1,7 @@
'use client'
import { memo, useCallback, useEffect, useRef, useState } from 'react'
import { AnimatePresence, motion } from 'framer-motion'
import { AnimatePresence, domAnimation, LazyMotion, m } from 'framer-motion'
import { ArrowUp, Table } from 'lucide-react'
import { Blimp, Checkbox, ChevronDown } from '@/components/emcn'
import { TypeBoolean, TypeNumber, TypeText } from '@/components/emcn/icons'
@@ -177,176 +177,180 @@ export const LandingPreviewHome = memo(function LandingPreviewHome({
const showToolCall = chatPhase === 'tool-call' || isResponding
return (
<div className='flex min-w-0 flex-1 overflow-hidden'>
{/* Chat area — matches mothership-view layout */}
<div className='min-h-0 flex-1 overflow-y-auto overflow-x-hidden px-6 pt-4 pb-8'>
<div className='mx-auto max-w-[42rem] space-y-6'>
{/* User message — rounded bubble, right-aligned */}
<motion.div
className='flex flex-col items-end gap-[6px] pt-3'
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, ease: EASE_OUT }}
>
<div className='max-w-[70%] overflow-hidden rounded-[16px] bg-[#363636] px-3.5 py-2'>
<p
className='font-body text-[14px] leading-[1.5]'
style={{ color: C.TEXT_PRIMARY }}
>
{AUTO_PROMPT}
</p>
</div>
</motion.div>
{/* Assistant — no bubble, full-width prose */}
<AnimatePresence>
{showToolCall && (
<motion.div
className='space-y-2.5'
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, ease: EASE_OUT }}
>
{/* Agent group header — icon + label + chevron */}
<button
type='button'
onClick={() => setToolsExpanded((p) => !p)}
className='flex cursor-pointer items-center gap-2'
<LazyMotion features={domAnimation}>
<div className='flex min-w-0 flex-1 overflow-hidden'>
{/* Chat area — matches mothership-view layout */}
<div className='min-h-0 flex-1 overflow-y-auto overflow-x-hidden px-6 pt-4 pb-8'>
<div className='mx-auto max-w-[42rem] space-y-6'>
{/* User message — rounded bubble, right-aligned */}
<m.div
className='flex flex-col items-end gap-[6px] pt-3'
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, ease: EASE_OUT }}
>
<div className='max-w-[70%] overflow-hidden rounded-[16px] bg-[#363636] px-3.5 py-2'>
<p
className='font-body text-[14px] leading-[1.5]'
style={{ color: C.TEXT_PRIMARY }}
>
<div className='flex h-[16px] w-[16px] flex-shrink-0 items-center justify-center'>
<Blimp className='h-[16px] w-[16px]' style={{ color: C.TEXT_ICON }} />
</div>
<span className='text-sm' style={{ color: C.TEXT_BODY }}>
Mothership
</span>
<ChevronDown
className='h-[7px] w-[9px] transition-transform duration-150'
{AUTO_PROMPT}
</p>
</div>
</m.div>
{/* Assistant — no bubble, full-width prose */}
<AnimatePresence>
{showToolCall && (
<m.div
className='space-y-2.5'
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, ease: EASE_OUT }}
>
{/* Agent group header — icon + label + chevron */}
<button
type='button'
onClick={() => setToolsExpanded((p) => !p)}
className='flex cursor-pointer items-center gap-2'
>
<div className='flex h-[16px] w-[16px] flex-shrink-0 items-center justify-center'>
<Blimp className='h-[16px] w-[16px]' style={{ color: C.TEXT_ICON }} />
</div>
<span className='text-sm' style={{ color: C.TEXT_BODY }}>
Mothership
</span>
<ChevronDown
className='h-[7px] w-[9px] transition-transform duration-150'
style={{
color: C.TEXT_ICON,
transform: toolsExpanded ? 'rotate(0deg)' : 'rotate(-90deg)',
}}
/>
</button>
{/* Tool call items — collapsible */}
<div
className='grid transition-[grid-template-rows] duration-200 ease-out'
style={{
color: C.TEXT_ICON,
transform: toolsExpanded ? 'rotate(0deg)' : 'rotate(-90deg)',
gridTemplateRows: toolsExpanded ? '1fr' : '0fr',
}}
/>
</button>
{/* Tool call items — collapsible */}
<div
className='grid transition-[grid-template-rows] duration-200 ease-out'
style={{
gridTemplateRows: toolsExpanded ? '1fr' : '0fr',
}}
>
<div className='overflow-hidden'>
<div className='flex flex-col gap-1.5 pt-0.5'>
<ToolCallRow
icon={
<Table
className='h-[15px] w-[15px]'
style={{ color: C.TEXT_TERTIARY }}
/>
}
title='Read Customer Leads'
/>
>
<div className='overflow-hidden'>
<div className='flex flex-col gap-1.5 pt-0.5'>
<ToolCallRow
icon={
<Table
className='h-[15px] w-[15px]'
style={{ color: C.TEXT_TERTIARY }}
/>
}
title='Read Customer Leads'
/>
</div>
</div>
</div>
</div>
{/* Response prose — full width, no card */}
{isResponding && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.2, ease: EASE_OUT }}
>
<ChatMarkdown
content={MOCK_RESPONSE}
visibleLength={responseTypedLength}
isTyping={chatPhase === 'responding'}
/>
</motion.div>
)}
</motion.div>
)}
</AnimatePresence>
{/* Response prose — full width, no card */}
{isResponding && (
<m.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.2, ease: EASE_OUT }}
>
<ChatMarkdown
content={MOCK_RESPONSE}
visibleLength={responseTypedLength}
isTyping={chatPhase === 'responding'}
/>
</m.div>
)}
</m.div>
)}
</AnimatePresence>
</div>
</div>
</div>
{/* Resource panel — slides in from right */}
<AnimatePresence>
{showResourcePanel && (
<motion.div
className='hidden h-full flex-shrink-0 overflow-hidden border-[#2c2c2c] border-l lg:flex'
initial={{ width: 0, opacity: 0 }}
animate={{ width: '55%', opacity: 1 }}
transition={{ duration: 0.35, ease: EASE_OUT }}
>
<MiniTablePanel />
</motion.div>
)}
</AnimatePresence>
</div>
{/* Resource panel — slides in from right */}
<AnimatePresence>
{showResourcePanel && (
<m.div
className='hidden h-full flex-shrink-0 overflow-hidden border-[#2c2c2c] border-l lg:flex'
initial={{ width: 0, opacity: 0 }}
animate={{ width: '55%', opacity: 1 }}
transition={{ duration: 0.35, ease: EASE_OUT }}
>
<MiniTablePanel />
</m.div>
)}
</AnimatePresence>
</div>
</LazyMotion>
)
}
return (
<div className='flex min-w-0 flex-1 flex-col items-center justify-center px-6 pb-[2vh]'>
<motion.p
role='presentation'
className='mb-6 max-w-[42rem] font-[430] font-season text-[32px] tracking-[-0.02em]'
style={{ color: C.TEXT_PRIMARY }}
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4, ease: EASE_OUT }}
>
What should we get done?
</motion.p>
<motion.div
className='w-full max-w-[32rem]'
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4, delay: 0.1, ease: EASE_OUT }}
>
<div
className='cursor-text rounded-[20px] border px-2.5 py-2'
style={{ borderColor: C.BORDER, backgroundColor: C.SURFACE }}
onClick={() => textareaRef.current?.focus()}
<LazyMotion features={domAnimation}>
<div className='flex min-w-0 flex-1 flex-col items-center justify-center px-6 pb-[2vh]'>
<m.p
role='presentation'
className='mb-6 max-w-[42rem] font-[430] font-season text-[32px] tracking-[-0.02em]'
style={{ color: C.TEXT_PRIMARY }}
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4, ease: EASE_OUT }}
>
<textarea
ref={textareaRef}
value={inputValue}
onChange={(e) => {
if (!autoType) setInputValue(e.target.value)
}}
onKeyDown={handleKeyDown}
onInput={handleInput}
placeholder={animatedPlaceholder}
rows={1}
readOnly={autoType}
className='m-0 box-border min-h-[24px] w-full resize-none overflow-y-auto border-0 bg-transparent px-1 py-1 font-body text-[15px] leading-[24px] tracking-[-0.015em] outline-none placeholder:font-[380] placeholder:text-[#787878] focus-visible:ring-0'
style={{
color: C.TEXT_PRIMARY,
caretColor: autoType ? 'transparent' : C.TEXT_PRIMARY,
maxHeight: '200px',
}}
/>
<div className='flex items-center justify-end'>
<button
type='button'
onClick={handleSubmit}
disabled={isEmpty}
aria-label='Submit message'
className='flex h-[28px] w-[28px] items-center justify-center rounded-full border-0 p-0 transition-colors'
style={{
background: isEmpty ? '#808080' : '#e0e0e0',
cursor: isEmpty ? 'not-allowed' : 'pointer',
What should we get done?
</m.p>
<m.div
className='w-full max-w-[32rem]'
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4, delay: 0.1, ease: EASE_OUT }}
>
<div
className='cursor-text rounded-[20px] border px-2.5 py-2'
style={{ borderColor: C.BORDER, backgroundColor: C.SURFACE }}
onClick={() => textareaRef.current?.focus()}
>
<textarea
ref={textareaRef}
value={inputValue}
onChange={(e) => {
if (!autoType) setInputValue(e.target.value)
}}
>
<ArrowUp size={16} strokeWidth={2.25} color='#1b1b1b' />
</button>
onKeyDown={handleKeyDown}
onInput={handleInput}
placeholder={animatedPlaceholder}
rows={1}
readOnly={autoType}
className='m-0 box-border min-h-[24px] w-full resize-none overflow-y-auto border-0 bg-transparent px-1 py-1 font-body text-[15px] leading-[24px] tracking-[-0.015em] outline-none placeholder:font-[380] placeholder:text-[#787878] focus-visible:ring-0'
style={{
color: C.TEXT_PRIMARY,
caretColor: autoType ? 'transparent' : C.TEXT_PRIMARY,
maxHeight: '200px',
}}
/>
<div className='flex items-center justify-end'>
<button
type='button'
onClick={handleSubmit}
disabled={isEmpty}
aria-label='Submit message'
className='flex h-[28px] w-[28px] items-center justify-center rounded-full border-0 p-0 transition-colors'
style={{
background: isEmpty ? '#808080' : '#e0e0e0',
cursor: isEmpty ? 'not-allowed' : 'pointer',
}}
>
<ArrowUp size={16} strokeWidth={2.25} color='#1b1b1b' />
</button>
</div>
</div>
</div>
</motion.div>
</div>
</m.div>
</div>
</LazyMotion>
)
})
@@ -385,7 +389,7 @@ function ChatMarkdown({
<div className='font-body text-[14px] leading-[1.6]' style={{ color: C.TEXT_PRIMARY }}>
<span dangerouslySetInnerHTML={{ __html: rendered }} />
{isTyping && (
<motion.span
<m.span
className='inline-block h-[14px] w-[1.5px] translate-y-[2px] bg-[#e6e6e6]'
animate={{ opacity: [1, 0] }}
transition={{
@@ -444,7 +448,7 @@ function MiniTablePanel() {
</thead>
<tbody>
{MINI_TABLE_ROWS.map((row, i) => (
<motion.tr
<m.tr
key={i}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
@@ -472,7 +476,7 @@ function MiniTablePanel() {
</td>
)
})}
</motion.tr>
</m.tr>
))}
</tbody>
</table>
@@ -1,7 +1,7 @@
'use client'
import { memo, useCallback, useEffect, useRef, useState } from 'react'
import { AnimatePresence, motion } from 'framer-motion'
import { AnimatePresence, domMax, LazyMotion, m } from 'framer-motion'
import { ArrowUp } from 'lucide-react'
import dynamic from 'next/dynamic'
import { useRouter } from 'next/navigation'
@@ -174,166 +174,168 @@ export const LandingPreviewPanel = memo(function LandingPreviewPanel({
)
return (
<div className='flex h-full w-[280px] flex-shrink-0 flex-col bg-[#1e1e1e]'>
<div className='flex h-full flex-col border-[#2c2c2c] border-l pt-3.5'>
{/* Header */}
<div className='flex flex-shrink-0 items-center justify-between px-2'>
<div className='pointer-events-none flex gap-1.5'>
<div className='flex h-[30px] w-[30px] items-center justify-center rounded-[5px] border border-[#3d3d3d] bg-[#363636]'>
<MoreHorizontal className='h-[14px] w-[14px] text-[#e6e6e6]' />
</div>
<div className='flex h-[30px] w-[30px] items-center justify-center rounded-[5px] border border-[#3d3d3d] bg-[#363636]'>
<BubbleChatPreview className='h-[14px] w-[14px] text-[#e6e6e6]' />
</div>
</div>
<AuthModal defaultView='signup' source='landing_preview'>
<button
type='button'
className='flex gap-1.5'
onMouseMove={(e) => setCursorPos({ x: e.clientX, y: e.clientY })}
onMouseLeave={() => setCursorPos(null)}
onClick={() =>
trackLandingCta({
label: 'Deploy',
section: 'landing_preview',
destination: 'auth_modal',
})
}
>
<div className='flex h-[30px] items-center rounded-[5px] bg-[#33C482] px-2.5 transition-colors hover:bg-[#2DAC72]'>
<span className='font-medium text-[#1b1b1b] text-[12px]'>Deploy</span>
<LazyMotion features={domMax}>
<div className='flex h-full w-[280px] flex-shrink-0 flex-col bg-[#1e1e1e]'>
<div className='flex h-full flex-col border-[#2c2c2c] border-l pt-3.5'>
{/* Header */}
<div className='flex flex-shrink-0 items-center justify-between px-2'>
<div className='pointer-events-none flex gap-1.5'>
<div className='flex h-[30px] w-[30px] items-center justify-center rounded-[5px] border border-[#3d3d3d] bg-[#363636]'>
<MoreHorizontal className='h-[14px] w-[14px] text-[#e6e6e6]' />
</div>
<div className='flex h-[30px] items-center gap-2 rounded-[5px] bg-[#33C482] px-2.5 transition-colors hover:bg-[#2DAC72]'>
<Play className='h-[11.5px] w-[11.5px] text-[#1b1b1b]' />
<span className='font-medium text-[#1b1b1b] text-[12px]'>Run</span>
<div className='flex h-[30px] w-[30px] items-center justify-center rounded-[5px] border border-[#3d3d3d] bg-[#363636]'>
<BubbleChatPreview className='h-[14px] w-[14px] text-[#e6e6e6]' />
</div>
</button>
</AuthModal>
{cursorPos &&
createPortal(
<div
className='pointer-events-none fixed z-[9999]'
style={{ left: cursorPos.x + 14, top: cursorPos.y + 14 }}
</div>
<AuthModal defaultView='signup' source='landing_preview'>
<button
type='button'
className='flex gap-1.5'
onMouseMove={(e) => setCursorPos({ x: e.clientX, y: e.clientY })}
onMouseLeave={() => setCursorPos(null)}
onClick={() =>
trackLandingCta({
label: 'Deploy',
section: 'landing_preview',
destination: 'auth_modal',
})
}
>
<div className='flex h-[4px]'>
<div className='h-full w-[8px] bg-[#2ABBF8]' />
<div className='h-full w-[14px] bg-[#2ABBF8] opacity-60' />
<div className='h-full w-[8px] bg-[#00F701]' />
<div className='h-full w-[16px] bg-[#00F701] opacity-60' />
<div className='h-full w-[8px] bg-[#FFCC02]' />
<div className='h-full w-[10px] bg-[#FFCC02] opacity-60' />
<div className='h-full w-[8px] bg-[#FA4EDF]' />
<div className='h-full w-[14px] bg-[#FA4EDF] opacity-60' />
<div className='flex h-[30px] items-center rounded-[5px] bg-[#33C482] px-2.5 transition-colors hover:bg-[#2DAC72]'>
<span className='font-medium text-[#1b1b1b] text-[12px]'>Deploy</span>
</div>
<div className='flex items-center gap-[5px] bg-white px-1.5 py-1 font-medium text-[#1C1C1C] text-[11px]'>
Get started
<ChevronDown className='-rotate-90 h-[7px] w-[7px] text-[#1C1C1C]' />
<div className='flex h-[30px] items-center gap-2 rounded-[5px] bg-[#33C482] px-2.5 transition-colors hover:bg-[#2DAC72]'>
<Play className='h-[11.5px] w-[11.5px] text-[#1b1b1b]' />
<span className='font-medium text-[#1b1b1b] text-[12px]'>Run</span>
</div>
</div>,
document.body
)}
</div>
{/* Tabs with sliding active indicator */}
<div className='flex flex-shrink-0 items-center px-2 pt-3.5'>
<div className='flex gap-1'>
{TABS_WITH_TOOLBAR.map((tab) => {
if (tab.disabled) {
return (
<div
key={tab.id}
className='pointer-events-none flex h-[28px] items-center rounded-md border border-transparent px-2 py-[5px]'
>
<span className='font-medium text-[#787878] text-[12.5px]'>{tab.label}</span>
</div>
)
}
const isActive = activeTab === tab.id
return (
<button
key={tab.id}
type='button'
onClick={() => handleTabSwitch(tab.id as PanelTab)}
className='relative flex h-[28px] items-center rounded-md border border-transparent px-2 py-[5px] font-medium text-[12.5px] transition-colors hover:border-[#3d3d3d] hover:bg-[#363636] hover:text-[#e6e6e6]'
style={{ color: isActive ? '#e6e6e6' : '#787878' }}
</button>
</AuthModal>
{cursorPos &&
createPortal(
<div
className='pointer-events-none fixed z-[9999]'
style={{ left: cursorPos.x + 14, top: cursorPos.y + 14 }}
>
{isActive && (
<motion.div
layoutId='panel-tab-indicator'
className='absolute inset-0 rounded-md border border-[#3d3d3d] bg-[#363636]'
transition={{ type: 'spring', stiffness: 400, damping: 30 }}
/>
)}
<span className='relative z-10'>{tab.label}</span>
</button>
)
})}
<div className='flex h-[4px]'>
<div className='h-full w-[8px] bg-[#2ABBF8]' />
<div className='h-full w-[14px] bg-[#2ABBF8] opacity-60' />
<div className='h-full w-[8px] bg-[#00F701]' />
<div className='h-full w-[16px] bg-[#00F701] opacity-60' />
<div className='h-full w-[8px] bg-[#FFCC02]' />
<div className='h-full w-[10px] bg-[#FFCC02] opacity-60' />
<div className='h-full w-[8px] bg-[#FA4EDF]' />
<div className='h-full w-[14px] bg-[#FA4EDF] opacity-60' />
</div>
<div className='flex items-center gap-[5px] bg-white px-1.5 py-1 font-medium text-[#1C1C1C] text-[11px]'>
Get started
<ChevronDown className='-rotate-90 h-[7px] w-[7px] text-[#1C1C1C]' />
</div>
</div>,
document.body
)}
</div>
</div>
{/* Tab content with cross-fade */}
<div className='flex flex-1 flex-col overflow-hidden pt-3'>
<AnimatePresence mode='wait'>
{activeTab === 'copilot' && (
<motion.div
key='copilot'
className='flex h-full flex-col'
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.15, ease: EASE_OUT }}
>
<div className='pointer-events-none mx-[-1px] flex flex-shrink-0 items-center justify-between gap-2 border border-[#2c2c2c] bg-[#292929] px-3 py-1.5'>
<span className='min-w-0 flex-1 truncate font-medium text-[#e6e6e6] text-[14px]'>
New Chat
</span>
</div>
<div className='px-2 pt-3 pb-2'>
<div className='rounded-[4px] border border-[#3d3d3d] bg-[#292929] px-1.5 py-1.5'>
<textarea
ref={textareaRef}
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyDown={handleKeyDown}
placeholder='Build an AI agent...'
rows={2}
className='mb-1.5 min-h-[48px] w-full cursor-text resize-none border-0 bg-transparent px-0.5 py-1 text-[#e6e6e6] text-sm leading-[1.25rem] placeholder-[#787878] caret-[#e6e6e6] outline-none'
/>
<div className='flex items-center justify-end'>
<button
type='button'
onClick={handleSubmit}
disabled={isEmpty}
className='flex h-[22px] w-[22px] items-center justify-center rounded-full border-0 p-0 transition-colors'
style={{
background: isEmpty ? '#808080' : '#e0e0e0',
cursor: isEmpty ? 'not-allowed' : 'pointer',
}}
>
<ArrowUp size={14} strokeWidth={2.25} color='#1b1b1b' />
</button>
{/* Tabs with sliding active indicator */}
<div className='flex flex-shrink-0 items-center px-2 pt-3.5'>
<div className='flex gap-1'>
{TABS_WITH_TOOLBAR.map((tab) => {
if (tab.disabled) {
return (
<div
key={tab.id}
className='pointer-events-none flex h-[28px] items-center rounded-md border border-transparent px-2 py-[5px]'
>
<span className='font-medium text-[#787878] text-[12.5px]'>{tab.label}</span>
</div>
)
}
const isActive = activeTab === tab.id
return (
<button
key={tab.id}
type='button'
onClick={() => handleTabSwitch(tab.id as PanelTab)}
className='relative flex h-[28px] items-center rounded-md border border-transparent px-2 py-[5px] font-medium text-[12.5px] transition-colors hover:border-[#3d3d3d] hover:bg-[#363636] hover:text-[#e6e6e6]'
style={{ color: isActive ? '#e6e6e6' : '#787878' }}
>
{isActive && (
<m.div
layoutId='panel-tab-indicator'
className='absolute inset-0 rounded-md border border-[#3d3d3d] bg-[#363636]'
transition={{ type: 'spring', stiffness: 400, damping: 30 }}
/>
)}
<span className='relative z-10'>{tab.label}</span>
</button>
)
})}
</div>
</div>
{/* Tab content with cross-fade */}
<div className='flex flex-1 flex-col overflow-hidden pt-3'>
<AnimatePresence mode='wait'>
{activeTab === 'copilot' && (
<m.div
key='copilot'
className='flex h-full flex-col'
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.15, ease: EASE_OUT }}
>
<div className='pointer-events-none mx-[-1px] flex flex-shrink-0 items-center justify-between gap-2 border border-[#2c2c2c] bg-[#292929] px-3 py-1.5'>
<span className='min-w-0 flex-1 truncate font-medium text-[#e6e6e6] text-[14px]'>
New Chat
</span>
</div>
<div className='px-2 pt-3 pb-2'>
<div className='rounded-[4px] border border-[#3d3d3d] bg-[#292929] px-1.5 py-1.5'>
<textarea
ref={textareaRef}
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyDown={handleKeyDown}
placeholder='Build an AI agent...'
rows={2}
className='mb-1.5 min-h-[48px] w-full cursor-text resize-none border-0 bg-transparent px-0.5 py-1 text-[#e6e6e6] text-sm leading-[1.25rem] placeholder-[#787878] caret-[#e6e6e6] outline-none'
/>
<div className='flex items-center justify-end'>
<button
type='button'
onClick={handleSubmit}
disabled={isEmpty}
className='flex h-[22px] w-[22px] items-center justify-center rounded-full border-0 p-0 transition-colors'
style={{
background: isEmpty ? '#808080' : '#e0e0e0',
cursor: isEmpty ? 'not-allowed' : 'pointer',
}}
>
<ArrowUp size={14} strokeWidth={2.25} color='#1b1b1b' />
</button>
</div>
</div>
</div>
</div>
</motion.div>
)}
</m.div>
)}
{activeTab === 'editor' && (
<motion.div
key='editor'
className='flex h-full flex-col'
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.15, ease: EASE_OUT }}
>
<EditorTabContent editorPrompt={editorPrompt} typedLength={typedLength} />
</motion.div>
)}
</AnimatePresence>
{activeTab === 'editor' && (
<m.div
key='editor'
className='flex h-full flex-col'
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.15, ease: EASE_OUT }}
>
<EditorTabContent editorPrompt={editorPrompt} typedLength={typedLength} />
</m.div>
)}
</AnimatePresence>
</div>
</div>
</div>
</div>
</LazyMotion>
)
})
@@ -407,7 +409,7 @@ function EditorTabContent({ editorPrompt, typedLength }: EditorTabContentProps)
<p className='min-h-[48px] whitespace-pre-wrap break-words font-medium font-sans text-[#e6e6e6] text-sm leading-[1.5]'>
{visibleText}
{isTyping && (
<motion.span
<m.span
className='inline-block h-[14px] w-[1.5px] translate-y-[2px] bg-[#e6e6e6]'
animate={{ opacity: [1, 0] }}
transition={{
@@ -1,12 +1,18 @@
import { Suspense } from 'react'
import { dehydrate, HydrationBoundary } from '@tanstack/react-query'
import type { Metadata } from 'next'
import { redirect } from 'next/navigation'
import { isBillingEnabled } from '@/lib/core/config/feature-flags'
import { getQueryClient } from '@/app/_shell/providers/get-query-client'
import type { SettingsSection } from '@/app/workspace/[workspaceId]/settings/navigation'
import { prefetchGeneralSettings, prefetchSubscriptionData, prefetchUserProfile } from './prefetch'
import { SettingsPage } from './settings'
const SETTINGS_REDIRECTS: Record<string, (workspaceId: string) => string> = {
integrations: (id) => `/workspace/${id}/integrations`,
skills: (id) => `/workspace/${id}/skills`,
}
const SECTION_TITLES: Record<string, string> = {
general: 'General',
secrets: 'Secrets',
@@ -43,7 +49,11 @@ export default async function SettingsSectionPage({
}: {
params: Promise<{ workspaceId: string; section: string }>
}) {
const { section } = await params
const { workspaceId, section } = await params
const redirectTo = SETTINGS_REDIRECTS[section]
if (redirectTo) redirect(redirectTo(workspaceId))
const queryClient = getQueryClient()
void prefetchGeneralSettings(queryClient)
@@ -29,6 +29,7 @@ import { DeleteModal } from '@/app/workspace/[workspaceId]/w/components/sidebar/
import { CreateWorkspaceModal } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/create-workspace-modal/create-workspace-modal'
import { InviteModal } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/invite-modal'
import type { Workspace, WorkspaceCreationPolicy } from '@/hooks/queries/workspace'
import { usePermissionConfig } from '@/hooks/use-permission-config'
import { useSettingsNavigation } from '@/hooks/use-settings-navigation'
const logger = createLogger('WorkspaceHeader')
@@ -130,8 +131,9 @@ function WorkspaceHeaderImpl({
const canCreateWorkspace = workspaceCreationPolicy?.canCreate ?? true
const createWorkspaceDisabledReason =
workspaceCreationPolicy?.canCreate === false ? workspaceCreationPolicy.reason : null
const { isInvitationsDisabled: isInvitationsDisabledByConfig } = usePermissionConfig()
const inviteDisabledReason = activeWorkspaceFull?.inviteDisabledReason ?? null
const isInvitationsDisabled = inviteDisabledReason !== null
const isInvitationsDisabled = isInvitationsDisabledByConfig || inviteDisabledReason !== null
useEffect(() => {
const handleOpenInvite = () => {
+1 -1
View File
@@ -101,7 +101,7 @@ export const useSearchModalStore = create<SearchModalState>()(
searchValue: `${block.name} ${block.type} block-${block.type} ${buildCommandSearchableOptionSearchValue(block)}`,
}
if (block.category === 'blocks') {
if (block.category === 'blocks' && block.type !== 'starter') {
regularBlocks.push(searchItem)
} else if (block.category === 'tools') {
tools.push(searchItem)