Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions apps/sim/app/chat/[subdomain]/chat-client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -570,8 +570,8 @@ export default function ChatClient({ subdomain }: { subdomain: string }) {
/>

{/* Input area (free-standing at the bottom) */}
<div className='relative p-4 pb-6'>
<div className='relative mx-auto max-w-3xl'>
<div className='relative p-3 pb-4 md:p-4 md:pb-6'>
<div className='relative mx-auto max-w-3xl md:max-w-[748px]'>
<ChatInput
onSubmit={(value, isVoiceInput) => {
void handleSendMessage(value, isVoiceInput)
Expand Down
15 changes: 5 additions & 10 deletions apps/sim/app/chat/[subdomain]/components/header/header.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
'use client'

import { motion } from 'framer-motion'
import { GithubIcon } from '@/components/icons'

interface ChatHeaderProps {
Expand All @@ -19,7 +18,7 @@ export function ChatHeader({ chatConfig, starCount }: ChatHeaderProps) {
const primaryColor = chatConfig?.customizations?.primaryColor || '#701FFC'

return (
<div className='flex items-center justify-between border-border border-b bg-background/95 px-6 py-3 backdrop-blur supports-[backdrop-filter]:bg-background/60'>
<div className='flex items-center justify-between bg-background/95 px-5 py-3 pt-4 backdrop-blur supports-[backdrop-filter]:bg-background/60 md:px-6 md:pt-3'>
<div className='flex items-center gap-3'>
{chatConfig?.customizations?.logoUrl && (
<img
Expand All @@ -32,21 +31,17 @@ export function ChatHeader({ chatConfig, starCount }: ChatHeaderProps) {
{chatConfig?.customizations?.headerText || chatConfig?.title || 'Chat'}
</h2>
</div>
<div className='flex items-center gap-1'>
<motion.a
<div className='flex items-center gap-2'>
<a
href='https://github.com/simstudioai/sim'
className='flex items-center gap-1 rounded-md px-1.5 py-1 text-foreground/70 transition-colors duration-200 hover:bg-foreground/5 hover:text-foreground'
className='flex items-center gap-1 text-foreground'
aria-label='GitHub'
target='_blank'
rel='noopener noreferrer'
initial={{ opacity: 0, y: -5 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, ease: 'easeOut' }}
whileHover={{ scale: 1.02 }}
>
<GithubIcon className='h-[18px] w-[18px]' />
<span className='hidden font-medium text-xs sm:inline-block'>{starCount}</span>
</motion.a>
</a>
<a
href='https://simstudio.ai'
target='_blank'
Expand Down
216 changes: 98 additions & 118 deletions apps/sim/app/chat/[subdomain]/components/input/input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,8 @@ import { VoiceInput } from './voice-input'

const PLACEHOLDER_MOBILE = 'Enter a message'
const PLACEHOLDER_DESKTOP = 'Enter a message or click the mic to speak'
const MAX_TEXTAREA_HEIGHT = 160 // Max height in pixels (e.g., for about 4-5 lines)

const containerVariants = {
collapsed: {
height: '56px', // Fixed height when collapsed
boxShadow: '0 1px 6px 0 rgba(0,0,0,0.05)',
},
expanded: {
height: 'auto',
boxShadow: '0 2px 10px 0 rgba(0,0,0,0.1)',
},
} as const
const MAX_TEXTAREA_HEIGHT = 120 // Max height in pixels (e.g., for about 3-4 lines)
const MAX_TEXTAREA_HEIGHT_MOBILE = 100 // Smaller for mobile

export const ChatInput: React.FC<{
onSubmit?: (value: string, isVoiceInput?: boolean) => void
Expand All @@ -45,8 +35,12 @@ export const ChatInput: React.FC<{
el.style.height = 'auto' // Reset height to correctly calculate scrollHeight
const scrollHeight = el.scrollHeight

if (scrollHeight > MAX_TEXTAREA_HEIGHT) {
el.style.height = `${MAX_TEXTAREA_HEIGHT}px`
// Use mobile height on mobile devices, desktop height on desktop
const isMobile = window.innerWidth < 768
const maxHeight = isMobile ? MAX_TEXTAREA_HEIGHT_MOBILE : MAX_TEXTAREA_HEIGHT
Comment thread
emir-karabeg marked this conversation as resolved.

if (scrollHeight > maxHeight) {
el.style.height = `${maxHeight}px`
el.style.overflowY = 'auto'
} else {
el.style.height = `${scrollHeight}px`
Expand Down Expand Up @@ -136,32 +130,28 @@ export const ChatInput: React.FC<{

return (
<>
<div className='fixed right-0 bottom-0 left-0 flex w-full items-center justify-center bg-gradient-to-t from-white to-transparent pb-4 text-black'>
<motion.div
ref={wrapperRef}
className='w-full max-w-3xl px-4'
variants={containerVariants}
animate={'expanded'}
initial='collapsed'
style={{
overflow: 'hidden',
borderRadius: 32,
background: '#fff',
border: '1px solid rgba(0,0,0,0.1)',
marginLeft: 'auto',
marginRight: 'auto',
}}
onClick={handleActivate}
>
<div className='flex h-full w-full items-center rounded-full p-2'>
{/* Voice Input with Tooltip */}
{isSttAvailable && (
<div className='mr-2'>
<div className='fixed right-0 bottom-0 left-0 flex w-full items-center justify-center bg-gradient-to-t from-white to-transparent px-4 pb-4 text-black md:px-0 md:pb-4'>
<div ref={wrapperRef} className='w-full max-w-3xl md:max-w-[748px]'>
{/* Text Input Area with Controls */}
<motion.div
className='rounded-2xl border border-gray-200 bg-white shadow-sm md:rounded-3xl'
onClick={handleActivate}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.2 }}
>
<div className='flex items-center gap-2 p-3 md:p-4'>
{/* Voice Input */}
{isSttAvailable && (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div>
<VoiceInput onVoiceStart={handleVoiceStart} disabled={isStreaming} />
<VoiceInput
onVoiceStart={handleVoiceStart}
disabled={isStreaming}
minimal
/>
</div>
</TooltipTrigger>
<TooltipContent side='top'>
Expand All @@ -170,97 +160,87 @@ export const ChatInput: React.FC<{
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}

{/* Text Input Container */}
<div className='relative flex-1'>
<textarea
ref={textareaRef}
value={inputValue}
onChange={handleInputChange}
className='flex w-full resize-none items-center overflow-hidden bg-transparent text-sm outline-none placeholder:text-gray-400 md:font-[330] md:text-base'
placeholder={isActive ? '' : ''}
rows={1}
style={{
minHeight: window.innerWidth >= 768 ? '24px' : '28px',
lineHeight: '1.4',
paddingTop: window.innerWidth >= 768 ? '4px' : '3px',
paddingBottom: window.innerWidth >= 768 ? '4px' : '3px',
}}
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault()
handleSubmit()
}
}}
/>

{/* Placeholder */}
<div className='pointer-events-none absolute top-0 left-0 flex h-full w-full items-center'>
{!isActive && !inputValue && (
<>
{/* Mobile placeholder */}
<div
className='-translate-y-1/2 absolute top-1/2 left-0 transform select-none text-gray-400 text-sm md:hidden md:text-base'
style={{ paddingTop: '3px', paddingBottom: '3px' }}
>
{PLACEHOLDER_MOBILE}
</div>
{/* Desktop placeholder */}
<div
className='-translate-y-1/2 absolute top-1/2 left-0 hidden transform select-none font-[330] text-gray-400 text-sm md:block md:text-base'
style={{ paddingTop: '4px', paddingBottom: '4px' }}
>
{PLACEHOLDER_DESKTOP}
</div>
</>
)}
</div>
</div>
)}

{/* Text Input & Placeholder */}
<div className='relative min-h-[40px] flex-1'>
<textarea
ref={textareaRef}
value={inputValue}
onChange={handleInputChange}
className='w-full resize-none overflow-hidden bg-transparent px-3 py-3 text-base outline-none placeholder:text-gray-400'
placeholder={isActive ? '' : ''}
rows={1}
style={{
minHeight: '40px',
lineHeight: '1.4',
}}
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault()
{/* Send Button */}
<button
className={`flex items-center justify-center rounded-full p-1.5 text-white transition-colors md:p-2 ${
inputValue.trim()
? 'bg-black hover:bg-zinc-700'
: 'cursor-default bg-gray-300 hover:bg-gray-400'
}`}
title={isStreaming ? 'Stop' : 'Send'}
type='button'
onClick={(e) => {
e.stopPropagation()
if (isStreaming) {
onStopStreaming?.()
} else {
handleSubmit()
}
}}
/>

<div className='pointer-events-none absolute top-0 left-0 flex h-full w-full items-center'>
{!isActive && !inputValue && (
>
{isStreaming ? (
<>
<Square size={16} className='md:hidden' />
<Square size={18} className='hidden md:block' />
</>
) : (
<>
{/* Mobile placeholder */}
<div
className='-translate-y-1/2 absolute top-1/2 left-3 select-none text-gray-400 md:hidden'
style={{
whiteSpace: 'nowrap',
zIndex: 0,
background:
'linear-gradient(90deg, rgba(150,150,150,0.2) 0%, rgba(150,150,150,0.8) 50%, rgba(150,150,150,0.2) 100%)',
backgroundSize: '200% 100%',
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
animation: 'shimmer 10s infinite linear',
}}
>
{PLACEHOLDER_MOBILE}
</div>
{/* Desktop placeholder */}
<div
className='-translate-y-1/2 absolute top-1/2 left-3 hidden select-none text-gray-400 md:block'
style={{
whiteSpace: 'nowrap',
zIndex: 0,
background:
'linear-gradient(90deg, rgba(150,150,150,0.2) 0%, rgba(150,150,150,0.8) 50%, rgba(150,150,150,0.2) 100%)',
backgroundSize: '200% 100%',
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
animation: 'shimmer 10s infinite linear',
}}
>
{PLACEHOLDER_DESKTOP}
<style jsx global>{`
@keyframes shimmer {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
`}</style>
</div>
<Send size={16} className='md:hidden' />
<Send size={18} className='hidden md:block' />
</>
)}
</div>
</button>
</div>

<button
className='flex items-center justify-center rounded-full bg-black p-3 text-white hover:bg-zinc-700'
title={isStreaming ? 'Stop' : 'Send'}
type='button'
onClick={(e) => {
e.stopPropagation()
if (isStreaming) {
onStopStreaming?.()
} else {
handleSubmit()
}
}}
>
{isStreaming ? <Square size={18} /> : <Send size={18} />}
</button>
</div>
</motion.div>
</motion.div>
</div>
</div>
</>
)
Expand Down
29 changes: 25 additions & 4 deletions apps/sim/app/chat/[subdomain]/components/input/voice-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,15 @@ interface VoiceInputProps {
isListening?: boolean
disabled?: boolean
large?: boolean
minimal?: boolean
}

export function VoiceInput({
onVoiceStart,
isListening = false,
disabled = false,
large = false,
minimal = false,
}: VoiceInputProps) {
const [isSupported, setIsSupported] = useState(false)

Expand All @@ -68,6 +70,24 @@ export function VoiceInput({
return null
}

if (minimal) {
return (
<motion.button
type='button'
onClick={handleVoiceClick}
disabled={disabled}
className={`flex items-center justify-center p-1 transition-colors duration-200 ${
disabled ? 'cursor-not-allowed opacity-50' : 'cursor-pointer hover:text-gray-600'
}`}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
title='Start voice conversation'
>
<Mic size={18} className='text-gray-500' />
</motion.button>
)
}

if (large) {
return (
<div className='flex flex-col items-center'>
Expand All @@ -93,21 +113,22 @@ export function VoiceInput({

return (
<div className='flex items-center'>
{/* Voice Button */}
{/* Voice Button - Now matches send button styling */}
<motion.button
type='button'
onClick={handleVoiceClick}
disabled={disabled}
className={`flex items-center justify-center rounded-full p-2 transition-all duration-200 ${
className={`flex items-center justify-center rounded-full p-2.5 transition-all duration-200 md:p-3 ${
isListening
? 'bg-red-500 text-white hover:bg-red-600'
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
: 'bg-black text-white hover:bg-zinc-700'
} ${disabled ? 'cursor-not-allowed opacity-50' : 'cursor-pointer'}`}
Comment on lines +121 to 125
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Consider extracting button color classes into a config file for consistency across components

whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
title='Start voice conversation'
>
<Mic size={16} />
<Mic size={16} className='md:hidden' />
<Mic size={18} className='hidden md:block' />
</motion.button>
</div>
)
Expand Down
Loading