Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
8800f03
improvement(billing): treat past_due state correctly (#3750)
icecrasher321 Mar 25, 2026
668b948
feat(agents): generalize repository guidance for coding agents (#3760)
Danigm-dev Mar 25, 2026
8caaf01
fix(ui): fix kb id extraction logic for resource, sync tags (#3763)
TheodoreSpeaks Mar 25, 2026
2691c12
feat(rippling): add Rippling HR integration with 19 tools (#3764)
waleedlatif1 Mar 25, 2026
e0f2b8f
feat(hubspot): add 27 CRM tools and fix OAuth scope mismatch (#3765)
waleedlatif1 Mar 25, 2026
54a862d
fix(user-input): fix multiple re-renders on user-input and split the …
adithyaakrishna Mar 25, 2026
f94be08
fix(billing): atomize usage_log and userStats writes via central reco…
waleedlatif1 Mar 25, 2026
87e8d3c
feat(logs) Add messageId and requestId context to all mothership log …
TheodoreSpeaks Mar 25, 2026
438defc
fix(mothership): key resumes by orchestration id (#3771)
icecrasher321 Mar 25, 2026
be6b00d
feat(ui): add request a demo modal (#3766)
TheodoreSpeaks Mar 25, 2026
9d1b976
Feat(logs) upgrade mothership chat messages to error (#3772)
TheodoreSpeaks Mar 25, 2026
104ad03
fix(notifications): auto-dismiss info-level workflow notifications (#…
waleedlatif1 Mar 25, 2026
5a5c33d
fix(client): network drops reconnecting behaviour (#3775)
icecrasher321 Mar 26, 2026
794d5ea
fix(explicit-user-abort): separate explicit user abort semantics (#3776)
icecrasher321 Mar 26, 2026
7b96b0e
add logs
icecrasher321 Mar 26, 2026
7583c8f
feat(misc): skills import, MCP modal, workmark, dispatch modals, coll…
waleedlatif1 Mar 26, 2026
1a14f4c
fix chatHistory reconnect effect
icecrasher321 Mar 26, 2026
9e4fc50
fix(retry): extract code into callback
icecrasher321 Mar 26, 2026
9603fd0
Merge branch 'staging' of github.com:simstudioai/sim into staging
icecrasher321 Mar 26, 2026
d97e22e
chore(docs): update readme (#3778)
waleedlatif1 Mar 26, 2026
a9fc1a2
fix(guard-change): run finalize at right time
icecrasher321 Mar 26, 2026
6610c37
Merge branch 'staging' of github.com:simstudioai/sim into staging
icecrasher321 Mar 26, 2026
2771b67
fix(copilot): expand tool metadata, fix thinking text rendering, clea…
waleedlatif1 Mar 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
fix(user-input): fix multiple re-renders on user-input and split the …
…file (#3768)

* feat: fix rerenders

* chore: split user-input
  • Loading branch information
adithyaakrishna authored Mar 25, 2026
commit 54a862d5b084980a1adcded3f3275ee2c15981f2
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
'use client'

import { useEffect } from 'react'
import { useAnimatedPlaceholder } from '@/app/workspace/[workspaceId]/home/hooks'

interface AnimatedPlaceholderEffectProps {
textareaRef: React.RefObject<HTMLTextAreaElement | null>
isInitialView: boolean
}

export function AnimatedPlaceholderEffect({
textareaRef,
isInitialView,
}: AnimatedPlaceholderEffectProps) {
const animatedPlaceholder = useAnimatedPlaceholder(isInitialView)
const placeholder = isInitialView ? animatedPlaceholder : 'Send message to Sim'

useEffect(() => {
if (textareaRef.current) {
textareaRef.current.placeholder = placeholder
}
}, [placeholder, textareaRef])

return null
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
'use client'

import React from 'react'
import { Loader2, X } from 'lucide-react'
import { Tooltip } from '@/components/emcn'
import { getDocumentIcon } from '@/components/icons/document-icons'
import type { AttachedFile } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-file-attachments'

interface AttachedFilesListProps {
attachedFiles: AttachedFile[]
onFileClick: (file: AttachedFile) => void
onRemoveFile: (id: string) => void
}

export const AttachedFilesList = React.memo(function AttachedFilesList({
attachedFiles,
onFileClick,
onRemoveFile,
}: AttachedFilesListProps) {
if (attachedFiles.length === 0) return null

return (
<div className='mb-[6px] flex flex-wrap gap-[6px]'>
{attachedFiles.map((file) => {
const isImage = file.type.startsWith('image/')
return (
<Tooltip.Root key={file.id}>
<Tooltip.Trigger asChild>
<div
className='group relative h-[56px] w-[56px] flex-shrink-0 cursor-pointer overflow-hidden rounded-[8px] border border-[var(--border-1)] bg-[var(--surface-5)] hover:bg-[var(--surface-4)]'
onClick={() => onFileClick(file)}
>
{isImage && file.previewUrl ? (
<img
src={file.previewUrl}
alt={file.name}
className='h-full w-full object-cover'
/>
) : (
<div className='flex h-full w-full flex-col items-center justify-center gap-[2px] text-[var(--text-icon)]'>
{(() => {
const Icon = getDocumentIcon(file.type, file.name)
return <Icon className='h-[18px] w-[18px]' />
})()}
<span className='max-w-[48px] truncate px-[2px] text-[9px] text-[var(--text-muted)]'>
{file.name.split('.').pop()}
</span>
</div>
)}
{file.uploading && (
<div className='absolute inset-0 flex items-center justify-center bg-black/50'>
<Loader2 className='h-[14px] w-[14px] animate-spin text-white' />
</div>
)}
{!file.uploading && (
<button
type='button'
onClick={(e) => {
e.stopPropagation()
onRemoveFile(file.id)
}}
className='absolute top-[2px] right-[2px] flex h-[16px] w-[16px] items-center justify-center rounded-full bg-black/60 opacity-0 group-hover:opacity-100'
>
<X className='h-[10px] w-[10px] text-white' />
</button>
)}
</div>
</Tooltip.Trigger>
<Tooltip.Content side='top'>
<p className='max-w-[200px] truncate'>{file.name}</p>
</Tooltip.Content>
</Tooltip.Root>
)
})}
</div>
)
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { cn } from '@/lib/core/utils/cn'
import type { MothershipResource } from '@/app/workspace/[workspaceId]/home/types'
import type { ChatContext } from '@/stores/panel'

export interface SpeechRecognitionEvent extends Event {
resultIndex: number
results: SpeechRecognitionResultList
}

export interface SpeechRecognitionErrorEvent extends Event {
error: string
}

export interface SpeechRecognitionInstance extends EventTarget {
continuous: boolean
interimResults: boolean
lang: string
start(): void
stop(): void
abort(): void
onstart: ((ev: Event) => void) | null
onend: ((ev: Event) => void) | null
onresult: ((ev: SpeechRecognitionEvent) => void) | null
onerror: ((ev: SpeechRecognitionErrorEvent) => void) | null
}

export interface SpeechRecognitionStatic {
new (): SpeechRecognitionInstance
}

export type WindowWithSpeech = Window & {
SpeechRecognition?: SpeechRecognitionStatic
webkitSpeechRecognition?: SpeechRecognitionStatic
}

export interface PlusMenuHandle {
open: () => void
}

export 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-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)]',
'focus-visible:ring-0 focus-visible:ring-offset-0',
'[-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden'
)

export 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-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'
)

export const SEND_BUTTON_BASE = 'h-[28px] w-[28px] rounded-full border-0 p-0 transition-colors'
export const SEND_BUTTON_ACTIVE =
'bg-[var(--c-383838)] hover:bg-[var(--c-575757)] dark:bg-[var(--c-E0E0E0)] dark:hover:bg-[var(--c-CFCFCF)]'
export const SEND_BUTTON_DISABLED = 'bg-[var(--c-808080)] dark:bg-[var(--c-808080)]'

export const MAX_CHAT_TEXTAREA_HEIGHT = 200
export const SPEECH_RECOGNITION_LANG = 'en-US'

export function autoResizeTextarea(e: React.FormEvent<HTMLTextAreaElement>, maxHeight: number) {
const target = e.target as HTMLTextAreaElement
target.style.height = 'auto'
target.style.height = `${Math.min(target.scrollHeight, maxHeight)}px`
}

export function mapResourceToContext(resource: MothershipResource): ChatContext {
switch (resource.type) {
case 'workflow':
return {
kind: 'workflow',
workflowId: resource.id,
label: resource.title,
}
case 'knowledgebase':
return {
kind: 'knowledge',
knowledgeId: resource.id,
label: resource.title,
}
case 'table':
return { kind: 'table', tableId: resource.id, label: resource.title }
case 'file':
return { kind: 'file', fileId: resource.id, label: resource.title }
default:
return { kind: 'docs', label: resource.title }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
'use client'

import React from 'react'
import {
AudioIcon,
CsvIcon,
DocxIcon,
JsonIcon,
MarkdownIcon,
PdfIcon,
TxtIcon,
VideoIcon,
XlsxIcon,
} from '@/components/icons/document-icons'

const DROP_OVERLAY_ICONS = [
PdfIcon,
DocxIcon,
XlsxIcon,
CsvIcon,
TxtIcon,
MarkdownIcon,
JsonIcon,
AudioIcon,
VideoIcon,
] as const

export const DropOverlay = React.memo(function DropOverlay() {
return (
<div className='pointer-events-none absolute inset-[6px] z-10 flex items-center justify-center rounded-[14px] border-[1.5px] border-[var(--border-1)] border-dashed bg-[var(--white)] dark:bg-[var(--surface-4)]'>
<div className='flex flex-col items-center gap-[8px]'>
<span className='font-medium text-[13px] text-[var(--text-secondary)]'>Drop files</span>
<div className='flex items-center gap-[8px] text-[var(--text-icon)]'>
{DROP_OVERLAY_ICONS.map((Icon, i) => (
<Icon key={i} className='h-[14px] w-[14px]' />
))}
</div>
</div>
</div>
)
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export { AnimatedPlaceholderEffect } from './animated-placeholder-effect'
export { AttachedFilesList } from './attached-files-list'
export type {
PlusMenuHandle,
SpeechRecognitionErrorEvent,
SpeechRecognitionEvent,
SpeechRecognitionInstance,
WindowWithSpeech,
} from './constants'
export {
autoResizeTextarea,
MAX_CHAT_TEXTAREA_HEIGHT,
mapResourceToContext,
OVERLAY_CLASSES,
SPEECH_RECOGNITION_LANG,
TEXTAREA_BASE_CLASSES,
} from './constants'
export { DropOverlay } from './drop-overlay'
export { MicButton } from './mic-button'
export type { AvailableResourceGroup } from './plus-menu-dropdown'
export { PlusMenuDropdown } from './plus-menu-dropdown'
export { SendButton } from './send-button'
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
'use client'

import React from 'react'
import { Mic } from 'lucide-react'
import { cn } from '@/lib/core/utils/cn'

interface MicButtonProps {
isListening: boolean
onToggle: () => void
}

export const MicButton = React.memo(function MicButton({ isListening, onToggle }: MicButtonProps) {
return (
<button
type='button'
onClick={onToggle}
className={cn(
'flex h-[28px] w-[28px] items-center justify-center rounded-full transition-colors',
isListening
? 'bg-red-500 text-white hover:bg-red-600'
: 'text-[var(--text-icon)] hover:bg-[#F7F7F7] dark:hover:bg-[#303030]'
)}
title={isListening ? 'Stop listening' : 'Voice input'}
>
<Mic className='h-[16px] w-[16px]' strokeWidth={2} />
</button>
)
})
Loading
Loading