v0.6.46: mothership queueing, web vitals

This commit is contained in:
Waleed
2026-04-16 00:12:50 -07:00
committed by GitHub
21 changed files with 275 additions and 107 deletions

View File

@@ -1,12 +1,17 @@
'use client'
import { useCallback, useEffect, useRef, useState } from 'react'
import dynamic from 'next/dynamic'
import Image from 'next/image'
import Link from 'next/link'
import { Badge } from '@/components/emcn'
import { AuthModal } from '@/app/(landing)/components/auth-modal/auth-modal'
import { trackLandingCta } from '@/app/(landing)/landing-analytics'
const AuthModal = dynamic(
() => import('@/app/(landing)/components/auth-modal/auth-modal').then((m) => m.AuthModal),
{ loading: () => null }
)
interface DotGridProps {
className?: string
cols: number

View File

@@ -2,12 +2,17 @@
import { useRef, useState } from 'react'
import { type MotionValue, motion, useScroll, useTransform } from 'framer-motion'
import dynamic from 'next/dynamic'
import Image from 'next/image'
import { Badge } from '@/components/emcn'
import { AuthModal } from '@/app/(landing)/components/auth-modal/auth-modal'
import { FeaturesPreview } from '@/app/(landing)/components/features/components/features-preview'
import { trackLandingCta } from '@/app/(landing)/landing-analytics'
const AuthModal = dynamic(
() => import('@/app/(landing)/components/auth-modal/auth-modal').then((m) => m.AuthModal),
{ loading: () => null }
)
function hexToRgba(hex: string, alpha: number): string {
const r = Number.parseInt(hex.slice(1, 3), 16)
const g = Number.parseInt(hex.slice(3, 5), 16)

View File

@@ -2,13 +2,18 @@
import { useCallback, useRef, useState } from 'react'
import { ArrowUp } from 'lucide-react'
import dynamic from 'next/dynamic'
import { cn } from '@/lib/core/utils/cn'
import { captureClientEvent } from '@/lib/posthog/client'
import { AuthModal } from '@/app/(landing)/components/auth-modal/auth-modal'
import { useLandingSubmit } from '@/app/(landing)/components/landing-preview/components/landing-preview-panel/landing-preview-panel'
import { trackLandingCta } from '@/app/(landing)/landing-analytics'
import { useAnimatedPlaceholder } from '@/hooks/use-animated-placeholder'
const AuthModal = dynamic(
() => import('@/app/(landing)/components/auth-modal/auth-modal').then((m) => m.AuthModal),
{ loading: () => null }
)
const MAX_HEIGHT = 120
const CTA_BUTTON =

View File

@@ -38,7 +38,7 @@ const BLOCK_LINKS: FooterItem[] = [
{ label: 'Router', href: 'https://docs.sim.ai/blocks/router', external: true },
{ label: 'Function', href: 'https://docs.sim.ai/blocks/function', external: true },
{ label: 'Condition', href: 'https://docs.sim.ai/blocks/condition', external: true },
{ label: 'API', href: 'https://docs.sim.ai/blocks/api', external: true },
{ label: 'API Block', href: 'https://docs.sim.ai/blocks/api', external: true },
{ label: 'Workflow', href: 'https://docs.sim.ai/blocks/workflow', external: true },
{ label: 'Parallel', href: 'https://docs.sim.ai/blocks/parallel', external: true },
{ label: 'Guardrails', href: 'https://docs.sim.ai/blocks/guardrails', external: true },
@@ -194,7 +194,7 @@ export default function Footer({ hideCTA }: FooterProps) {
<Link href='/' aria-label='Sim home'>
<Image
src='/logo/sim-landing.svg'
alt='Sim'
alt=''
width={85}
height={26}
className='h-[26.4px] w-auto'

View File

@@ -2,10 +2,21 @@
import dynamic from 'next/dynamic'
import { cn } from '@/lib/core/utils/cn'
import { AuthModal } from '@/app/(landing)/components/auth-modal/auth-modal'
import { DemoRequestModal } from '@/app/(landing)/components/demo-request/demo-request-modal'
import { trackLandingCta } from '@/app/(landing)/landing-analytics'
const AuthModal = dynamic(
() => import('@/app/(landing)/components/auth-modal/auth-modal').then((m) => m.AuthModal),
{ loading: () => null }
)
const DemoRequestModal = dynamic(
() =>
import('@/app/(landing)/components/demo-request/demo-request-modal').then(
(m) => m.DemoRequestModal
),
{ loading: () => null }
)
const LandingPreview = dynamic(
() =>
import('@/app/(landing)/components/landing-preview/landing-preview').then(

View File

@@ -334,6 +334,7 @@ export const LandingPreviewHome = memo(function LandingPreviewHome({
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',

View File

@@ -3,13 +3,13 @@
import { memo, useCallback, useEffect, useRef, useState } from 'react'
import { AnimatePresence, motion } from 'framer-motion'
import { ArrowUp } from 'lucide-react'
import dynamic from 'next/dynamic'
import { useRouter } from 'next/navigation'
import { createPortal } from 'react-dom'
import { Blimp, BubbleChatPreview, ChevronDown, MoreHorizontal, Play } from '@/components/emcn'
import { AgentIcon, HubspotIcon, OpenAIIcon, SalesforceIcon } from '@/components/icons'
import { LandingPromptStorage } from '@/lib/core/utils/browser-storage'
import { captureClientEvent } from '@/lib/posthog/client'
import { AuthModal } from '@/app/(landing)/components/auth-modal/auth-modal'
import {
EASE_OUT,
type EditorPromptData,
@@ -21,6 +21,11 @@ import {
} from '@/app/(landing)/components/landing-preview/components/landing-preview-workflow/workflow-data'
import { trackLandingCta } from '@/app/(landing)/landing-analytics'
const AuthModal = dynamic(
() => import('@/app/(landing)/components/auth-modal/auth-modal').then((m) => m.AuthModal),
{ loading: () => null }
)
type PanelTab = 'copilot' | 'editor'
const EDITOR_BLOCK_ICONS: Record<string, React.ComponentType<{ className?: string }>> = {

View File

@@ -1,13 +1,13 @@
'use client'
import { useCallback, useEffect, useRef, useState, useSyncExternalStore } from 'react'
import { useCallback, useContext, useEffect, useRef, useState, useSyncExternalStore } from 'react'
import dynamic from 'next/dynamic'
import Image from 'next/image'
import Link from 'next/link'
import { useSearchParams } from 'next/navigation'
import { GithubOutlineIcon } from '@/components/icons'
import { useSession } from '@/lib/auth/auth-client'
import { cn } from '@/lib/core/utils/cn'
import { AuthModal } from '@/app/(landing)/components/auth-modal/auth-modal'
import { SessionContext } from '@/app/_shell/providers/session-provider'
import {
BlogDropdown,
type NavBlogPost,
@@ -17,6 +17,11 @@ import { GitHubStars } from '@/app/(landing)/components/navbar/components/github
import { trackLandingCta } from '@/app/(landing)/landing-analytics'
import { getBrandConfig } from '@/ee/whitelabeling'
const AuthModal = dynamic(
() => import('@/app/(landing)/components/auth-modal/auth-modal').then((m) => m.AuthModal),
{ loading: () => null }
)
type DropdownId = 'docs' | 'blog' | null
interface NavLink {
@@ -48,7 +53,9 @@ interface NavbarProps {
export default function Navbar({ logoOnly = false, blogPosts = [] }: NavbarProps) {
const brand = getBrandConfig()
const searchParams = useSearchParams()
const { data: session, isPending: isSessionPending } = useSession()
const sessionCtx = useContext(SessionContext)
const session = sessionCtx?.data ?? null
const isSessionPending = sessionCtx?.isPending ?? true
const isAuthenticated = Boolean(session?.user?.id)
const isBrowsingHome = searchParams.has('home')
const useHomeLinks = isAuthenticated || isBrowsingHome
@@ -125,7 +132,7 @@ export default function Navbar({ logoOnly = false, blogPosts = [] }: NavbarProps
) : (
<Image
src='/logo/sim-landing.svg'
alt='Sim'
alt=''
width={71}
height={22}
className='h-[22px] w-auto'

View File

@@ -1,10 +1,22 @@
'use client'
import dynamic from 'next/dynamic'
import { Badge } from '@/components/emcn'
import { AuthModal } from '@/app/(landing)/components/auth-modal/auth-modal'
import { DemoRequestModal } from '@/app/(landing)/components/demo-request/demo-request-modal'
import { trackLandingCta } from '@/app/(landing)/landing-analytics'
const AuthModal = dynamic(
() => import('@/app/(landing)/components/auth-modal/auth-modal').then((m) => m.AuthModal),
{ loading: () => null }
)
const DemoRequestModal = dynamic(
() =>
import('@/app/(landing)/components/demo-request/demo-request-modal').then(
(m) => m.DemoRequestModal
),
{ loading: () => null }
)
interface PricingTier {
id: string
name: string

View File

@@ -470,7 +470,7 @@ export default function Templates() {
aria-labelledby={`template-tab-${activeIndex}`}
className='relative hidden flex-1 lg:block'
>
<div aria-hidden='true' className='h-full'>
<div aria-hidden='true' inert className='h-full'>
<LandingPreviewWorkflow
key={activeIndex}
workflow={activeWorkflow}

View File

@@ -35,6 +35,7 @@ export function PostHogProvider({ children }: { children: React.ReactNode }) {
capture_performance: false,
capture_dead_clicks: false,
enable_heatmaps: false,
disable_session_recording: true,
session_recording: {
maskAllInputs: false,
maskInputOptions: {

View File

@@ -92,6 +92,13 @@ export function SessionProvider({ children }: { children: React.ReactNode }) {
email_verified: data.user.emailVerified,
created_at: data.user.createdAt,
})
if (
typeof posthog.startSessionRecording === 'function' &&
typeof posthog.sessionRecordingStarted === 'function' &&
!posthog.sessionRecordingStarted()
) {
posthog.startSessionRecording()
}
} else {
posthog.reset()
}

View File

@@ -10,7 +10,10 @@ import {
} from '@/app/workspace/[workspaceId]/home/components/message-content'
import { PendingTagIndicator } from '@/app/workspace/[workspaceId]/home/components/message-content/components/special-tags'
import { QueuedMessages } from '@/app/workspace/[workspaceId]/home/components/queued-messages'
import { UserInput } from '@/app/workspace/[workspaceId]/home/components/user-input'
import {
UserInput,
type UserInputHandle,
} from '@/app/workspace/[workspaceId]/home/components/user-input'
import { UserMessageContent } from '@/app/workspace/[workspaceId]/home/components/user-message-content'
import type {
ChatMessage,
@@ -36,14 +39,12 @@ interface MothershipChatProps {
messageQueue: QueuedMessage[]
onRemoveQueuedMessage: (id: string) => void
onSendQueuedMessage: (id: string) => Promise<void>
onEditQueuedMessage: (id: string) => void
onEditQueuedMessage: (id: string) => QueuedMessage | undefined
userId?: string
chatId?: string
onContextAdd?: (context: ChatContext) => void
onContextRemove?: (context: ChatContext) => void
onWorkspaceResourceSelect?: (resource: MothershipResource) => void
editValue?: string
onEditValueConsumed?: () => void
layout?: 'mothership-view' | 'copilot-view'
initialScrollBlocked?: boolean
animateInput?: boolean
@@ -91,8 +92,6 @@ export function MothershipChat({
onContextAdd,
onContextRemove,
onWorkspaceResourceSelect,
editValue,
onEditValueConsumed,
layout = 'mothership-view',
initialScrollBlocked = false,
animateInput = false,
@@ -106,11 +105,24 @@ export function MothershipChat({
})
const hasMessages = messages.length > 0
const initialScrollDoneRef = useRef(false)
const userInputRef = useRef<UserInputHandle>(null)
const handleSendQueuedHead = useCallback(() => {
const topMessage = messageQueue[0]
if (!topMessage) return
void onSendQueuedMessage(topMessage.id)
}, [messageQueue, onSendQueuedMessage])
const handleEditQueued = useCallback(
(id: string) => {
const msg = onEditQueuedMessage(id)
if (msg) userInputRef.current?.loadQueuedMessage(msg)
},
[onEditQueuedMessage]
)
const handleEditQueuedTail = useCallback(() => {
const tail = messageQueue[messageQueue.length - 1]
if (!tail) return
handleEditQueued(tail.id)
}, [messageQueue, handleEditQueued])
useLayoutEffect(() => {
if (!hasMessages) {
@@ -205,9 +217,10 @@ export function MothershipChat({
messageQueue={messageQueue}
onRemove={onRemoveQueuedMessage}
onSendNow={onSendQueuedMessage}
onEdit={onEditQueuedMessage}
onEdit={handleEditQueued}
/>
<UserInput
ref={userInputRef}
onSubmit={onSubmit}
isSending={isStreamActive}
onStopGeneration={onStopGeneration}
@@ -215,9 +228,8 @@ export function MothershipChat({
userId={userId}
onContextAdd={onContextAdd}
onContextRemove={onContextRemove}
editValue={editValue}
onEditValueConsumed={onEditValueConsumed}
onSendQueuedHead={handleSendQueuedHead}
onEditQueuedTail={handleEditQueuedTail}
/>
</div>
</div>

View File

@@ -1,10 +1,13 @@
'use client'
import { useState } from 'react'
import { ArrowUp, ChevronDown, ChevronRight, Pencil, Trash2 } from 'lucide-react'
import { useCallback, useRef, useState } from 'react'
import { ArrowUp, ChevronDown, ChevronRight, Paperclip, Pencil, Trash2 } from 'lucide-react'
import { Tooltip } from '@/components/emcn'
import { UserMessageContent } from '@/app/workspace/[workspaceId]/home/components/user-message-content'
import type { QueuedMessage } from '@/app/workspace/[workspaceId]/home/types'
const NARROW_WIDTH_PX = 320
interface QueuedMessagesProps {
messageQueue: QueuedMessage[]
onRemove: (id: string) => void
@@ -14,11 +17,29 @@ interface QueuedMessagesProps {
export function QueuedMessages({ messageQueue, onRemove, onSendNow, onEdit }: QueuedMessagesProps) {
const [isExpanded, setIsExpanded] = useState(true)
const [isNarrow, setIsNarrow] = useState(false)
const roRef = useRef<ResizeObserver | null>(null)
const containerRef = useCallback((el: HTMLDivElement | null) => {
if (roRef.current) {
roRef.current.disconnect()
roRef.current = null
}
if (!el) return
const ro = new ResizeObserver((entries) => {
setIsNarrow(entries[0].contentRect.width < NARROW_WIDTH_PX)
})
ro.observe(el)
roRef.current = ro
}, [])
if (messageQueue.length === 0) return null
return (
<div className='-mb-3 mx-3.5 overflow-hidden rounded-t-[16px] border border-[var(--border-1)] border-b-0 bg-[var(--surface-3)] pb-3'>
<div
ref={containerRef}
className='-mb-3 mx-3.5 overflow-hidden rounded-t-[16px] border border-[var(--border-1)] border-b-0 bg-[var(--surface-3)] pb-3'
>
<button
type='button'
onClick={() => setIsExpanded(!isExpanded)}
@@ -39,16 +60,41 @@ export function QueuedMessages({ messageQueue, onRemove, onSendNow, onEdit }: Qu
{messageQueue.map((msg) => (
<div
key={msg.id}
className='flex items-center gap-2 px-3.5 py-1.5 transition-colors hover-hover:bg-[var(--surface-active)]'
className='flex items-center gap-2 py-1.5 pr-2 pl-3.5 transition-colors hover-hover:bg-[var(--surface-active)]'
>
<div className='flex h-[16px] w-[16px] shrink-0 items-center justify-center'>
<div className='h-[10px] w-[10px] rounded-full border-[1.5px] border-[color-mix(in_srgb,var(--text-tertiary)_40%,transparent)]' />
</div>
<div className='min-w-0 flex-1'>
<p className='truncate text-[var(--text-primary)] text-small'>{msg.content}</p>
<div className='min-w-0 flex-1 overflow-hidden'>
<UserMessageContent
content={msg.content}
contexts={msg.contexts}
plainMentions
compact
/>
</div>
{msg.fileAttachments && msg.fileAttachments.length > 0 && (
<span className='inline-flex min-w-0 max-w-[40%] shrink items-center gap-1 rounded-[5px] bg-[var(--surface-5)] px-[5px] py-0.5 text-[var(--text-primary)] text-small'>
<Paperclip className='h-[12px] w-[12px] shrink-0 text-[var(--text-icon)]' />
{isNarrow ? (
<span className='shrink-0 text-[var(--text-secondary)]'>
{msg.fileAttachments.length}
</span>
) : (
<>
<span className='truncate'>{msg.fileAttachments[0].filename}</span>
{msg.fileAttachments.length > 1 && (
<span className='shrink-0 text-[var(--text-secondary)]'>
+{msg.fileAttachments.length - 1}
</span>
)}
</>
)}
</span>
)}
<div className='flex shrink-0 items-center gap-0.5'>
<Tooltip.Root>
<Tooltip.Trigger asChild>

View File

@@ -1 +1 @@
export { UserInput } from './user-input'
export { UserInput, type UserInputHandle } from './user-input'

View File

@@ -1,7 +1,16 @@
'use client'
import type React from 'react'
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
import {
forwardRef,
useCallback,
useEffect,
useImperativeHandle,
useLayoutEffect,
useMemo,
useRef,
useState,
} from 'react'
import { useParams } from 'next/navigation'
import { useSession } from '@/lib/auth/auth-client'
import { SIM_RESOURCE_DRAG_TYPE, SIM_RESOURCES_DRAG_TYPE } from '@/lib/copilot/resource-types'
@@ -26,6 +35,7 @@ import {
import type {
FileAttachmentForApi,
MothershipResource,
QueuedMessage,
} from '@/app/workspace/[workspaceId]/home/types'
import {
useContextManagement,
@@ -91,8 +101,6 @@ function getCaretAnchor(
interface UserInputProps {
defaultValue?: string
editValue?: string
onEditValueConsumed?: () => void
onSubmit: (
text: string,
fileAttachments?: FileAttachmentForApi[],
@@ -105,21 +113,28 @@ interface UserInputProps {
onContextAdd?: (context: ChatContext) => void
onContextRemove?: (context: ChatContext) => void
onSendQueuedHead?: () => void
onEditQueuedTail?: () => void
}
export function UserInput({
defaultValue = '',
editValue,
onEditValueConsumed,
onSubmit,
isSending,
onStopGeneration,
isInitialView = true,
userId,
onContextAdd,
onContextRemove,
onSendQueuedHead,
}: UserInputProps) {
export interface UserInputHandle {
loadQueuedMessage: (msg: QueuedMessage) => void
}
export const UserInput = forwardRef<UserInputHandle, UserInputProps>(function UserInput(
{
defaultValue = '',
onSubmit,
isSending,
onStopGeneration,
isInitialView = true,
userId,
onContextAdd,
onContextRemove,
onSendQueuedHead,
onEditQueuedTail,
},
ref
) {
const { workspaceId } = useParams<{ workspaceId: string }>()
const { navigateToSettings } = useSettingsNavigation()
const { data: workflowsById = {} } = useWorkflowMap(workspaceId)
@@ -136,18 +151,6 @@ export function UserInput({
setPrevDefaultValue(defaultValue)
}
const [prevEditValue, setPrevEditValue] = useState(editValue)
if (editValue && editValue !== prevEditValue) {
setPrevEditValue(editValue)
setValue(editValue)
} else if (!editValue && prevEditValue) {
setPrevEditValue(editValue)
}
useEffect(() => {
if (editValue) onEditValueConsumed?.()
}, [editValue, onEditValueConsumed])
const files = useFileAttachments({
userId: userId || session?.user?.id,
workspaceId,
@@ -269,6 +272,8 @@ export function UserInput({
contextRef.current = contextManagement
const onSendQueuedHeadRef = useRef(onSendQueuedHead)
onSendQueuedHeadRef.current = onSendQueuedHead
const onEditQueuedTailRef = useRef(onEditQueuedTail)
onEditQueuedTailRef.current = onEditQueuedTail
const isSendingRef = useRef(isSending)
isSendingRef.current = isSending
@@ -277,6 +282,34 @@ export function UserInput({
const atInsertPosRef = useRef<number | null>(null)
const pendingCursorRef = useRef<number | null>(null)
useImperativeHandle(
ref,
() => ({
loadQueuedMessage: (msg: QueuedMessage) => {
setValue(msg.content)
const restored: AttachedFile[] = (msg.fileAttachments ?? []).map((a) => ({
id: a.id,
name: a.filename,
size: a.size,
type: a.media_type,
path: a.path ?? '',
key: a.key,
uploading: false,
}))
files.restoreAttachedFiles(restored)
contextManagement.setSelectedContexts(msg.contexts ?? [])
requestAnimationFrame(() => {
const textarea = textareaRef.current
if (!textarea) return
textarea.focus()
const end = textarea.value.length
textarea.setSelectionRange(end, end)
})
},
}),
[files.restoreAttachedFiles, contextManagement.setSelectedContexts, textareaRef]
)
useLayoutEffect(() => {
const textarea = textareaRef.current
if (!textarea) return
@@ -430,6 +463,7 @@ export function UserInput({
filename: f.name,
media_type: f.type,
size: f.size,
...(f.path ? { path: f.path } : {}),
}))
onSubmit(
@@ -452,6 +486,15 @@ export function UserInput({
const handleKeyDown = useCallback(
(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === 'ArrowUp' && !e.shiftKey && !e.metaKey && !e.ctrlKey && !e.altKey) {
const isEmpty = valueRef.current.length === 0 && filesRef.current.attachedFiles.length === 0
if (isEmpty && onEditQueuedTailRef.current) {
e.preventDefault()
onEditQueuedTailRef.current()
return
}
}
if (e.key === 'Enter' && !e.shiftKey && !e.nativeEvent.isComposing) {
e.preventDefault()
const hasSubmitPayload =
@@ -763,4 +806,4 @@ export function UserInput({
{files.isDragging && <DropOverlay />}
</div>
)
}
})

View File

@@ -2,6 +2,7 @@
import { useMemo } from 'react'
import { useParams } from 'next/navigation'
import { cn } from '@/lib/core/utils/cn'
import { ContextMentionIcon } from '@/app/workspace/[workspaceId]/home/components/context-mention-icon'
import type { ChatMessageContext } from '@/app/workspace/[workspaceId]/home/types'
import { useWorkflows } from '@/hooks/queries/workflows'
@@ -9,9 +10,17 @@ import { useWorkflows } from '@/hooks/queries/workflows'
const USER_MESSAGE_CLASSES =
'whitespace-pre-wrap break-words [overflow-wrap:anywhere] font-[430] font-[family-name:var(--font-inter)] text-base text-[var(--text-primary)] leading-[23px] tracking-[0] antialiased'
const COMPACT_CLASSES =
'truncate text-small leading-[20px] font-[430] font-[family-name:var(--font-inter)] text-[var(--text-primary)] tracking-[0] antialiased'
interface UserMessageContentProps {
content: string
contexts?: ChatMessageContext[]
className?: string
/** When true, render mentions as plain inline text (no icon/pill) so truncation flows naturally. */
plainMentions?: boolean
/** Use compact single-line layout with truncation. */
compact?: boolean
}
function escapeRegex(str: string): string {
@@ -64,17 +73,23 @@ function MentionHighlight({ context }: { context: ChatMessageContext }) {
)
}
export function UserMessageContent({ content, contexts }: UserMessageContentProps) {
export function UserMessageContent({
content,
contexts,
className,
plainMentions = false,
compact = false,
}: UserMessageContentProps) {
const trimmed = content.trim()
const classes = cn(compact ? COMPACT_CLASSES : USER_MESSAGE_CLASSES, className)
if (!contexts || contexts.length === 0) {
return <p className={USER_MESSAGE_CLASSES}>{trimmed}</p>
}
const ranges = computeMentionRanges(content, contexts)
const ranges = useMemo(
() => (contexts && contexts.length > 0 ? computeMentionRanges(content, contexts) : []),
[content, contexts]
)
if (ranges.length === 0) {
return <p className={USER_MESSAGE_CLASSES}>{trimmed}</p>
return <p className={classes}>{trimmed}</p>
}
const elements: React.ReactNode[] = []
@@ -88,7 +103,20 @@ export function UserMessageContent({ content, contexts }: UserMessageContentProp
elements.push(<span key={`text-${i}-${lastIndex}`}>{before}</span>)
}
elements.push(<MentionHighlight key={`mention-${i}-${range.start}`} context={range.context} />)
if (plainMentions) {
elements.push(
<span
key={`mention-${i}-${range.start}`}
className='font-medium text-[var(--text-primary)]'
>
{content.slice(range.start, range.end)}
</span>
)
} else {
elements.push(
<MentionHighlight key={`mention-${i}-${range.start}`} context={range.context} />
)
}
lastIndex = range.end
}
@@ -97,5 +125,5 @@ export function UserMessageContent({ content, contexts }: UserMessageContentProp
elements.push(<span key={`tail-${lastIndex}`}>{tail}</span>)
}
return <p className={USER_MESSAGE_CLASSES}>{elements}</p>
return <p className={classes}>{elements}</p>
}

View File

@@ -159,26 +159,6 @@ export function Home({ chatId }: HomeProps = {}) {
})
)
const [editingInputValue, setEditingInputValue] = useState('')
const [prevChatId, setPrevChatId] = useState(chatId)
const clearEditingValue = useCallback(() => setEditingInputValue(''), [])
// Clear editing value when navigating to a different chat (guarded render-phase update)
if (chatId !== prevChatId) {
setPrevChatId(chatId)
setEditingInputValue('')
}
const handleEditQueuedMessage = useCallback(
(id: string) => {
const msg = editQueuedMessage(id)
if (msg) {
setEditingInputValue(msg.content)
}
},
[editQueuedMessage]
)
useEffect(() => {
const url = new URL(window.location.href)
if (activeResourceId) {
@@ -375,13 +355,11 @@ export function Home({ chatId }: HomeProps = {}) {
messageQueue={messageQueue}
onRemoveQueuedMessage={removeFromQueue}
onSendQueuedMessage={sendNow}
onEditQueuedMessage={handleEditQueuedMessage}
onEditQueuedMessage={editQueuedMessage}
userId={session?.user?.id}
chatId={resolvedChatId}
onContextAdd={handleContextAdd}
onWorkspaceResourceSelect={handleWorkspaceResourceSelect}
editValue={editingInputValue}
onEditValueConsumed={clearEditingValue}
animateInput={isInputEntering}
onInputAnimationEnd={isInputEntering ? () => setIsInputEntering(false) : undefined}
initialScrollBlocked={resources.length > 0 && isResourceCollapsed}

View File

@@ -46,6 +46,7 @@ export interface FileAttachmentForApi {
filename: string
media_type: string
size: number
path?: string
}
export interface QueuedMessage {

View File

@@ -301,6 +301,19 @@ export function useFileAttachments(props: UseFileAttachmentsProps) {
setAttachedFiles([])
}, [attachedFiles])
/**
* Replaces the current attached files with a given set.
* Cleans up preview URLs from the prior set before replacing.
*/
const restoreAttachedFiles = useCallback((files: AttachedFile[]) => {
setAttachedFiles((prev) => {
prev.forEach((f) => {
if (f.previewUrl) URL.revokeObjectURL(f.previewUrl)
})
return files
})
}, [])
return {
// State
attachedFiles,
@@ -321,6 +334,7 @@ export function useFileAttachments(props: UseFileAttachmentsProps) {
handleDragOver,
handleDrop,
clearAttachedFiles,
restoreAttachedFiles,
processFiles,
}
}

View File

@@ -392,17 +392,6 @@ export const Panel = memo(function Panel({ workspaceId: propWorkspaceId }: Panel
wasCopilotSendingRef.current = copilotIsSending
}, [copilotIsSending, loadCopilotChats])
const [copilotEditingInputValue, setCopilotEditingInputValue] = useState('')
const clearCopilotEditingValue = useCallback(() => setCopilotEditingInputValue(''), [])
const handleCopilotEditQueuedMessage = useCallback(
(id: string) => {
const msg = copilotEditQueuedMessage(id)
if (msg) setCopilotEditingInputValue(msg.content)
},
[copilotEditQueuedMessage]
)
const handleCopilotStopGeneration = useCallback(() => {
captureEvent(posthogRef.current, 'task_generation_aborted', {
workspace_id: workspaceId,
@@ -865,11 +854,9 @@ export const Panel = memo(function Panel({ workspaceId: propWorkspaceId }: Panel
messageQueue={copilotMessageQueue}
onRemoveQueuedMessage={copilotRemoveFromQueue}
onSendQueuedMessage={copilotSendNow}
onEditQueuedMessage={handleCopilotEditQueuedMessage}
onEditQueuedMessage={copilotEditQueuedMessage}
userId={session?.user?.id}
chatId={copilotResolvedChatId}
editValue={copilotEditingInputValue}
onEditValueConsumed={clearCopilotEditingValue}
layout='copilot-view'
/>
</div>