Skip to content

Commit 8b18a53

Browse files
authored
improvement(ui/ux): chat deploy (#496)
* improvement(ui/ux): chat deploy experience * improvement(ui/ux): chat fontweight
1 parent 1c7aa5c commit 8b18a53

File tree

5 files changed

+154
-158
lines changed

5 files changed

+154
-158
lines changed

apps/sim/app/chat/[subdomain]/chat-client.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -570,8 +570,8 @@ export default function ChatClient({ subdomain }: { subdomain: string }) {
570570
/>
571571

572572
{/* Input area (free-standing at the bottom) */}
573-
<div className='relative p-4 pb-6'>
574-
<div className='relative mx-auto max-w-3xl'>
573+
<div className='relative p-3 pb-4 md:p-4 md:pb-6'>
574+
<div className='relative mx-auto max-w-3xl md:max-w-[748px]'>
575575
<ChatInput
576576
onSubmit={(value, isVoiceInput) => {
577577
void handleSendMessage(value, isVoiceInput)

apps/sim/app/chat/[subdomain]/components/header/header.tsx

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
'use client'
22

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

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

2120
return (
22-
<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'>
21+
<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'>
2322
<div className='flex items-center gap-3'>
2423
{chatConfig?.customizations?.logoUrl && (
2524
<img
@@ -32,21 +31,17 @@ export function ChatHeader({ chatConfig, starCount }: ChatHeaderProps) {
3231
{chatConfig?.customizations?.headerText || chatConfig?.title || 'Chat'}
3332
</h2>
3433
</div>
35-
<div className='flex items-center gap-1'>
36-
<motion.a
34+
<div className='flex items-center gap-2'>
35+
<a
3736
href='https://github.com/simstudioai/sim'
38-
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'
37+
className='flex items-center gap-1 text-foreground'
3938
aria-label='GitHub'
4039
target='_blank'
4140
rel='noopener noreferrer'
42-
initial={{ opacity: 0, y: -5 }}
43-
animate={{ opacity: 1, y: 0 }}
44-
transition={{ duration: 0.3, ease: 'easeOut' }}
45-
whileHover={{ scale: 1.02 }}
4641
>
4742
<GithubIcon className='h-[18px] w-[18px]' />
4843
<span className='hidden font-medium text-xs sm:inline-block'>{starCount}</span>
49-
</motion.a>
44+
</a>
5045
<a
5146
href='https://simstudio.ai'
5247
target='_blank'

apps/sim/app/chat/[subdomain]/components/input/input.tsx

Lines changed: 98 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,8 @@ import { VoiceInput } from './voice-input'
99

1010
const PLACEHOLDER_MOBILE = 'Enter a message'
1111
const PLACEHOLDER_DESKTOP = 'Enter a message or click the mic to speak'
12-
const MAX_TEXTAREA_HEIGHT = 160 // Max height in pixels (e.g., for about 4-5 lines)
13-
14-
const containerVariants = {
15-
collapsed: {
16-
height: '56px', // Fixed height when collapsed
17-
boxShadow: '0 1px 6px 0 rgba(0,0,0,0.05)',
18-
},
19-
expanded: {
20-
height: 'auto',
21-
boxShadow: '0 2px 10px 0 rgba(0,0,0,0.1)',
22-
},
23-
} as const
12+
const MAX_TEXTAREA_HEIGHT = 120 // Max height in pixels (e.g., for about 3-4 lines)
13+
const MAX_TEXTAREA_HEIGHT_MOBILE = 100 // Smaller for mobile
2414

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

48-
if (scrollHeight > MAX_TEXTAREA_HEIGHT) {
49-
el.style.height = `${MAX_TEXTAREA_HEIGHT}px`
38+
// Use mobile height on mobile devices, desktop height on desktop
39+
const isMobile = window.innerWidth < 768
40+
const maxHeight = isMobile ? MAX_TEXTAREA_HEIGHT_MOBILE : MAX_TEXTAREA_HEIGHT
41+
42+
if (scrollHeight > maxHeight) {
43+
el.style.height = `${maxHeight}px`
5044
el.style.overflowY = 'auto'
5145
} else {
5246
el.style.height = `${scrollHeight}px`
@@ -136,32 +130,28 @@ export const ChatInput: React.FC<{
136130

137131
return (
138132
<>
139-
<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'>
140-
<motion.div
141-
ref={wrapperRef}
142-
className='w-full max-w-3xl px-4'
143-
variants={containerVariants}
144-
animate={'expanded'}
145-
initial='collapsed'
146-
style={{
147-
overflow: 'hidden',
148-
borderRadius: 32,
149-
background: '#fff',
150-
border: '1px solid rgba(0,0,0,0.1)',
151-
marginLeft: 'auto',
152-
marginRight: 'auto',
153-
}}
154-
onClick={handleActivate}
155-
>
156-
<div className='flex h-full w-full items-center rounded-full p-2'>
157-
{/* Voice Input with Tooltip */}
158-
{isSttAvailable && (
159-
<div className='mr-2'>
133+
<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'>
134+
<div ref={wrapperRef} className='w-full max-w-3xl md:max-w-[748px]'>
135+
{/* Text Input Area with Controls */}
136+
<motion.div
137+
className='rounded-2xl border border-gray-200 bg-white shadow-sm md:rounded-3xl'
138+
onClick={handleActivate}
139+
initial={{ opacity: 0, y: 10 }}
140+
animate={{ opacity: 1, y: 0 }}
141+
transition={{ duration: 0.2 }}
142+
>
143+
<div className='flex items-center gap-2 p-3 md:p-4'>
144+
{/* Voice Input */}
145+
{isSttAvailable && (
160146
<TooltipProvider>
161147
<Tooltip>
162148
<TooltipTrigger asChild>
163149
<div>
164-
<VoiceInput onVoiceStart={handleVoiceStart} disabled={isStreaming} />
150+
<VoiceInput
151+
onVoiceStart={handleVoiceStart}
152+
disabled={isStreaming}
153+
minimal
154+
/>
165155
</div>
166156
</TooltipTrigger>
167157
<TooltipContent side='top'>
@@ -170,97 +160,87 @@ export const ChatInput: React.FC<{
170160
</TooltipContent>
171161
</Tooltip>
172162
</TooltipProvider>
163+
)}
164+
165+
{/* Text Input Container */}
166+
<div className='relative flex-1'>
167+
<textarea
168+
ref={textareaRef}
169+
value={inputValue}
170+
onChange={handleInputChange}
171+
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'
172+
placeholder={isActive ? '' : ''}
173+
rows={1}
174+
style={{
175+
minHeight: window.innerWidth >= 768 ? '24px' : '28px',
176+
lineHeight: '1.4',
177+
paddingTop: window.innerWidth >= 768 ? '4px' : '3px',
178+
paddingBottom: window.innerWidth >= 768 ? '4px' : '3px',
179+
}}
180+
onKeyDown={(e) => {
181+
if (e.key === 'Enter' && !e.shiftKey) {
182+
e.preventDefault()
183+
handleSubmit()
184+
}
185+
}}
186+
/>
187+
188+
{/* Placeholder */}
189+
<div className='pointer-events-none absolute top-0 left-0 flex h-full w-full items-center'>
190+
{!isActive && !inputValue && (
191+
<>
192+
{/* Mobile placeholder */}
193+
<div
194+
className='-translate-y-1/2 absolute top-1/2 left-0 transform select-none text-gray-400 text-sm md:hidden md:text-base'
195+
style={{ paddingTop: '3px', paddingBottom: '3px' }}
196+
>
197+
{PLACEHOLDER_MOBILE}
198+
</div>
199+
{/* Desktop placeholder */}
200+
<div
201+
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'
202+
style={{ paddingTop: '4px', paddingBottom: '4px' }}
203+
>
204+
{PLACEHOLDER_DESKTOP}
205+
</div>
206+
</>
207+
)}
208+
</div>
173209
</div>
174-
)}
175210

176-
{/* Text Input & Placeholder */}
177-
<div className='relative min-h-[40px] flex-1'>
178-
<textarea
179-
ref={textareaRef}
180-
value={inputValue}
181-
onChange={handleInputChange}
182-
className='w-full resize-none overflow-hidden bg-transparent px-3 py-3 text-base outline-none placeholder:text-gray-400'
183-
placeholder={isActive ? '' : ''}
184-
rows={1}
185-
style={{
186-
minHeight: '40px',
187-
lineHeight: '1.4',
188-
}}
189-
onKeyDown={(e) => {
190-
if (e.key === 'Enter' && !e.shiftKey) {
191-
e.preventDefault()
211+
{/* Send Button */}
212+
<button
213+
className={`flex items-center justify-center rounded-full p-1.5 text-white transition-colors md:p-2 ${
214+
inputValue.trim()
215+
? 'bg-black hover:bg-zinc-700'
216+
: 'cursor-default bg-gray-300 hover:bg-gray-400'
217+
}`}
218+
title={isStreaming ? 'Stop' : 'Send'}
219+
type='button'
220+
onClick={(e) => {
221+
e.stopPropagation()
222+
if (isStreaming) {
223+
onStopStreaming?.()
224+
} else {
192225
handleSubmit()
193226
}
194227
}}
195-
/>
196-
197-
<div className='pointer-events-none absolute top-0 left-0 flex h-full w-full items-center'>
198-
{!isActive && !inputValue && (
228+
>
229+
{isStreaming ? (
230+
<>
231+
<Square size={16} className='md:hidden' />
232+
<Square size={18} className='hidden md:block' />
233+
</>
234+
) : (
199235
<>
200-
{/* Mobile placeholder */}
201-
<div
202-
className='-translate-y-1/2 absolute top-1/2 left-3 select-none text-gray-400 md:hidden'
203-
style={{
204-
whiteSpace: 'nowrap',
205-
zIndex: 0,
206-
background:
207-
'linear-gradient(90deg, rgba(150,150,150,0.2) 0%, rgba(150,150,150,0.8) 50%, rgba(150,150,150,0.2) 100%)',
208-
backgroundSize: '200% 100%',
209-
WebkitBackgroundClip: 'text',
210-
WebkitTextFillColor: 'transparent',
211-
animation: 'shimmer 10s infinite linear',
212-
}}
213-
>
214-
{PLACEHOLDER_MOBILE}
215-
</div>
216-
{/* Desktop placeholder */}
217-
<div
218-
className='-translate-y-1/2 absolute top-1/2 left-3 hidden select-none text-gray-400 md:block'
219-
style={{
220-
whiteSpace: 'nowrap',
221-
zIndex: 0,
222-
background:
223-
'linear-gradient(90deg, rgba(150,150,150,0.2) 0%, rgba(150,150,150,0.8) 50%, rgba(150,150,150,0.2) 100%)',
224-
backgroundSize: '200% 100%',
225-
WebkitBackgroundClip: 'text',
226-
WebkitTextFillColor: 'transparent',
227-
animation: 'shimmer 10s infinite linear',
228-
}}
229-
>
230-
{PLACEHOLDER_DESKTOP}
231-
<style jsx global>{`
232-
@keyframes shimmer {
233-
0% {
234-
background-position: 200% 0;
235-
}
236-
100% {
237-
background-position: -200% 0;
238-
}
239-
}
240-
`}</style>
241-
</div>
236+
<Send size={16} className='md:hidden' />
237+
<Send size={18} className='hidden md:block' />
242238
</>
243239
)}
244-
</div>
240+
</button>
245241
</div>
246-
247-
<button
248-
className='flex items-center justify-center rounded-full bg-black p-3 text-white hover:bg-zinc-700'
249-
title={isStreaming ? 'Stop' : 'Send'}
250-
type='button'
251-
onClick={(e) => {
252-
e.stopPropagation()
253-
if (isStreaming) {
254-
onStopStreaming?.()
255-
} else {
256-
handleSubmit()
257-
}
258-
}}
259-
>
260-
{isStreaming ? <Square size={18} /> : <Send size={18} />}
261-
</button>
262-
</div>
263-
</motion.div>
242+
</motion.div>
243+
</div>
264244
</div>
265245
</>
266246
)

apps/sim/app/chat/[subdomain]/components/input/voice-input.tsx

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,15 @@ interface VoiceInputProps {
4343
isListening?: boolean
4444
disabled?: boolean
4545
large?: boolean
46+
minimal?: boolean
4647
}
4748

4849
export function VoiceInput({
4950
onVoiceStart,
5051
isListening = false,
5152
disabled = false,
5253
large = false,
54+
minimal = false,
5355
}: VoiceInputProps) {
5456
const [isSupported, setIsSupported] = useState(false)
5557

@@ -68,6 +70,24 @@ export function VoiceInput({
6870
return null
6971
}
7072

73+
if (minimal) {
74+
return (
75+
<motion.button
76+
type='button'
77+
onClick={handleVoiceClick}
78+
disabled={disabled}
79+
className={`flex items-center justify-center p-1 transition-colors duration-200 ${
80+
disabled ? 'cursor-not-allowed opacity-50' : 'cursor-pointer hover:text-gray-600'
81+
}`}
82+
whileHover={{ scale: 1.05 }}
83+
whileTap={{ scale: 0.95 }}
84+
title='Start voice conversation'
85+
>
86+
<Mic size={18} className='text-gray-500' />
87+
</motion.button>
88+
)
89+
}
90+
7191
if (large) {
7292
return (
7393
<div className='flex flex-col items-center'>
@@ -93,21 +113,22 @@ export function VoiceInput({
93113

94114
return (
95115
<div className='flex items-center'>
96-
{/* Voice Button */}
116+
{/* Voice Button - Now matches send button styling */}
97117
<motion.button
98118
type='button'
99119
onClick={handleVoiceClick}
100120
disabled={disabled}
101-
className={`flex items-center justify-center rounded-full p-2 transition-all duration-200 ${
121+
className={`flex items-center justify-center rounded-full p-2.5 transition-all duration-200 md:p-3 ${
102122
isListening
103123
? 'bg-red-500 text-white hover:bg-red-600'
104-
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
124+
: 'bg-black text-white hover:bg-zinc-700'
105125
} ${disabled ? 'cursor-not-allowed opacity-50' : 'cursor-pointer'}`}
106126
whileHover={{ scale: 1.05 }}
107127
whileTap={{ scale: 0.95 }}
108128
title='Start voice conversation'
109129
>
110-
<Mic size={16} />
130+
<Mic size={16} className='md:hidden' />
131+
<Mic size={18} className='hidden md:block' />
111132
</motion.button>
112133
</div>
113134
)

0 commit comments

Comments
 (0)