Skip to content

Commit 8c4e6a3

Browse files
author
Theodore Li
committed
improvement(ui): Merge ui definitions for mothership chat
1 parent 77eafab commit 8c4e6a3

File tree

6 files changed

+272
-207
lines changed

6 files changed

+272
-207
lines changed

apps/sim/app/workspace/[workspaceId]/home/components/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ export {
33
assistantMessageHasRenderableContent,
44
MessageContent,
55
} from './message-content'
6+
export { MothershipChat } from './mothership-chat/mothership-chat'
67
export { MothershipView } from './mothership-view'
78
export { QueuedMessages } from './queued-messages'
89
export { TemplatePrompts } from './template-prompts'
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
'use client'
2+
3+
import { useLayoutEffect, useRef } from 'react'
4+
import { cn } from '@/lib/core/utils/cn'
5+
import { MessageActions } from '@/app/workspace/[workspaceId]/components'
6+
import { ChatMessageAttachments } from '@/app/workspace/[workspaceId]/home/components/chat-message-attachments'
7+
import {
8+
assistantMessageHasRenderableContent,
9+
MessageContent,
10+
} from '@/app/workspace/[workspaceId]/home/components/message-content'
11+
import { PendingTagIndicator } from '@/app/workspace/[workspaceId]/home/components/message-content/components/special-tags'
12+
import { QueuedMessages } from '@/app/workspace/[workspaceId]/home/components/queued-messages'
13+
import { UserInput } from '@/app/workspace/[workspaceId]/home/components/user-input'
14+
import { UserMessageContent } from '@/app/workspace/[workspaceId]/home/components/user-message-content'
15+
import { useAutoScroll } from '@/app/workspace/[workspaceId]/home/hooks'
16+
import type {
17+
ChatMessage,
18+
FileAttachmentForApi,
19+
QueuedMessage,
20+
} from '@/app/workspace/[workspaceId]/home/types'
21+
import type { ChatContext } from '@/stores/panel'
22+
23+
interface MothershipChatProps {
24+
messages: ChatMessage[]
25+
isSending: boolean
26+
onSubmit: (
27+
text: string,
28+
fileAttachments?: FileAttachmentForApi[],
29+
contexts?: ChatContext[]
30+
) => void
31+
onStopGeneration: () => void
32+
messageQueue: QueuedMessage[]
33+
onRemoveQueuedMessage: (id: string) => void
34+
onSendQueuedMessage: (id: string) => Promise<void>
35+
onEditQueuedMessage: (id: string) => void
36+
userId?: string
37+
onContextAdd?: (context: ChatContext) => void
38+
editValue?: string
39+
onEditValueConsumed?: () => void
40+
layout?: 'mothership-view' | 'copilot-view'
41+
initialScrollBlocked?: boolean
42+
animateInput?: boolean
43+
onInputAnimationEnd?: () => void
44+
className?: string
45+
}
46+
47+
const LAYOUT_STYLES = {
48+
'mothership-view': {
49+
scrollContainer:
50+
'min-h-0 flex-1 overflow-y-auto overflow-x-hidden px-6 pt-4 pb-8 [scrollbar-gutter:stable]',
51+
content: 'mx-auto max-w-[42rem] space-y-6',
52+
userRow: 'flex flex-col items-end gap-[6px] pt-3',
53+
attachmentWidth: 'max-w-[70%]',
54+
userBubble: 'max-w-[70%] overflow-hidden rounded-[16px] bg-[var(--surface-5)] px-3.5 py-2',
55+
assistantRow: 'group/msg relative pb-5',
56+
footer: 'flex-shrink-0 px-[24px] pb-[16px]',
57+
footerInner: 'mx-auto max-w-[42rem]',
58+
},
59+
'copilot-view': {
60+
scrollContainer: 'min-h-0 flex-1 overflow-y-auto overflow-x-hidden px-3 pt-2 pb-4',
61+
content: 'space-y-4',
62+
userRow: 'flex flex-col items-end gap-[6px] pt-2',
63+
attachmentWidth: 'max-w-[85%]',
64+
userBubble: 'max-w-[85%] overflow-hidden rounded-[16px] bg-[var(--surface-5)] px-3 py-2',
65+
assistantRow: 'group/msg relative pb-3',
66+
footer: 'flex-shrink-0 px-3 pb-3',
67+
footerInner: '',
68+
},
69+
} as const
70+
71+
export function MothershipChat({
72+
messages,
73+
isSending,
74+
onSubmit,
75+
onStopGeneration,
76+
messageQueue,
77+
onRemoveQueuedMessage,
78+
onSendQueuedMessage,
79+
onEditQueuedMessage,
80+
userId,
81+
onContextAdd,
82+
editValue,
83+
onEditValueConsumed,
84+
layout = 'mothership-view',
85+
initialScrollBlocked = false,
86+
animateInput = false,
87+
onInputAnimationEnd,
88+
className,
89+
}: MothershipChatProps) {
90+
const styles = LAYOUT_STYLES[layout]
91+
const { ref: scrollContainerRef, scrollToBottom } = useAutoScroll(isSending)
92+
const hasMessages = messages.length > 0
93+
const initialScrollDoneRef = useRef(false)
94+
95+
useLayoutEffect(() => {
96+
if (!hasMessages) {
97+
initialScrollDoneRef.current = false
98+
return
99+
}
100+
if (initialScrollDoneRef.current || initialScrollBlocked) return
101+
initialScrollDoneRef.current = true
102+
scrollToBottom()
103+
}, [hasMessages, initialScrollBlocked, scrollToBottom])
104+
105+
return (
106+
<div className={cn('flex h-full min-h-0 flex-col', className)}>
107+
<div ref={scrollContainerRef} className={styles.scrollContainer}>
108+
<div className={styles.content}>
109+
{messages.map((msg, index) => {
110+
if (msg.role === 'user') {
111+
const hasAttachments = Boolean(msg.attachments?.length)
112+
return (
113+
<div key={msg.id} className={styles.userRow}>
114+
{hasAttachments && (
115+
<ChatMessageAttachments
116+
attachments={msg.attachments ?? []}
117+
align='end'
118+
className={styles.attachmentWidth}
119+
/>
120+
)}
121+
<div className={styles.userBubble}>
122+
<UserMessageContent content={msg.content} contexts={msg.contexts} />
123+
</div>
124+
</div>
125+
)
126+
}
127+
128+
const hasAnyBlocks = Boolean(msg.contentBlocks?.length)
129+
const hasRenderableAssistant = assistantMessageHasRenderableContent(
130+
msg.contentBlocks ?? [],
131+
msg.content ?? ''
132+
)
133+
const isLastAssistant = index === messages.length - 1
134+
const isThisStreaming = isSending && isLastAssistant
135+
136+
if (!hasAnyBlocks && !msg.content?.trim() && isThisStreaming) {
137+
return <PendingTagIndicator key={msg.id} />
138+
}
139+
140+
if (!hasRenderableAssistant && !msg.content?.trim() && !isThisStreaming) {
141+
return null
142+
}
143+
144+
const isLastMessage = index === messages.length - 1
145+
146+
return (
147+
<div key={msg.id} className={styles.assistantRow}>
148+
{!isThisStreaming && (msg.content || msg.contentBlocks?.length) && (
149+
<div className='absolute right-0 bottom-0 z-10'>
150+
<MessageActions content={msg.content} requestId={msg.requestId} />
151+
</div>
152+
)}
153+
<MessageContent
154+
blocks={msg.contentBlocks || []}
155+
fallbackContent={msg.content}
156+
isStreaming={isThisStreaming}
157+
onOptionSelect={isLastMessage ? onSubmit : undefined}
158+
/>
159+
</div>
160+
)
161+
})}
162+
</div>
163+
</div>
164+
165+
<div
166+
className={cn(styles.footer, animateInput && 'animate-slide-in-bottom')}
167+
onAnimationEnd={animateInput ? onInputAnimationEnd : undefined}
168+
>
169+
<div className={styles.footerInner}>
170+
<QueuedMessages
171+
messageQueue={messageQueue}
172+
onRemove={onRemoveQueuedMessage}
173+
onSendNow={onSendQueuedMessage}
174+
onEdit={onEditQueuedMessage}
175+
/>
176+
<UserInput
177+
onSubmit={onSubmit}
178+
isSending={isSending}
179+
onStopGeneration={onStopGeneration}
180+
isInitialView={false}
181+
userId={userId}
182+
onContextAdd={onContextAdd}
183+
editValue={editValue}
184+
onEditValueConsumed={onEditValueConsumed}
185+
/>
186+
</div>
187+
</div>
188+
</div>
189+
)
190+
}

apps/sim/app/workspace/[workspaceId]/home/home.tsx

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

3-
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'
3+
import { useCallback, useEffect, useRef, useState } from 'react'
44
import { createLogger } from '@sim/logger'
55
import { useParams, useRouter } from 'next/navigation'
66
import { PanelLeft } from '@/components/emcn/icons'
@@ -11,21 +11,10 @@ import {
1111
LandingWorkflowSeedStorage,
1212
} from '@/lib/core/utils/browser-storage'
1313
import { persistImportedWorkflow } from '@/lib/workflows/operations/import-export'
14-
import { MessageActions } from '@/app/workspace/[workspaceId]/components'
1514
import { useChatHistory, useMarkTaskRead } from '@/hooks/queries/tasks'
1615
import type { ChatContext } from '@/stores/panel'
17-
import {
18-
assistantMessageHasRenderableContent,
19-
ChatMessageAttachments,
20-
MessageContent,
21-
MothershipView,
22-
QueuedMessages,
23-
TemplatePrompts,
24-
UserInput,
25-
UserMessageContent,
26-
} from './components'
27-
import { PendingTagIndicator } from './components/message-content/components/special-tags'
28-
import { useAutoScroll, useChat, useMothershipResize } from './hooks'
16+
import { MothershipChat, MothershipView, TemplatePrompts, UserInput } from './components'
17+
import { getMothershipUseChatOptions, useChat, useMothershipResize } from './hooks'
2918
import type { FileAttachmentForApi, MothershipResource, MothershipResourceType } from './types'
3019

3120
const logger = createLogger('Home')
@@ -173,7 +162,11 @@ export function Home({ chatId }: HomeProps = {}) {
173162
sendNow,
174163
editQueuedMessage,
175164
streamingFile,
176-
} = useChat(workspaceId, chatId, { onResourceEvent: handleResourceEvent })
165+
} = useChat(
166+
workspaceId,
167+
chatId,
168+
getMothershipUseChatOptions({ onResourceEvent: handleResourceEvent })
169+
)
177170

178171
const [editingInputValue, setEditingInputValue] = useState('')
179172
const [prevChatId, setPrevChatId] = useState(chatId)
@@ -285,22 +278,7 @@ export function Home({ chatId }: HomeProps = {}) {
285278
[addResource, handleResourceEvent]
286279
)
287280

288-
const { ref: scrollContainerRef, scrollToBottom } = useAutoScroll(isSending)
289-
290281
const hasMessages = messages.length > 0
291-
const initialScrollDoneRef = useRef(false)
292-
293-
useLayoutEffect(() => {
294-
if (!hasMessages) {
295-
initialScrollDoneRef.current = false
296-
return
297-
}
298-
if (initialScrollDoneRef.current) return
299-
if (resources.length > 0 && isResourceCollapsed) return
300-
301-
initialScrollDoneRef.current = true
302-
scrollToBottom()
303-
}, [hasMessages, resources.length, isResourceCollapsed, scrollToBottom])
304282

305283
useEffect(() => {
306284
if (hasMessages) return
@@ -354,90 +332,23 @@ export function Home({ chatId }: HomeProps = {}) {
354332
return (
355333
<div className='relative flex h-full bg-[var(--bg)]'>
356334
<div className='flex h-full min-w-[320px] flex-1 flex-col'>
357-
<div
358-
ref={scrollContainerRef}
359-
className='min-h-0 flex-1 overflow-y-auto overflow-x-hidden px-6 pt-4 pb-8 [scrollbar-gutter:stable]'
360-
>
361-
<div className='mx-auto max-w-[42rem] space-y-6'>
362-
{messages.map((msg, index) => {
363-
if (msg.role === 'user') {
364-
const hasAttachments = msg.attachments && msg.attachments.length > 0
365-
return (
366-
<div key={msg.id} className='flex flex-col items-end gap-[6px] pt-3'>
367-
{hasAttachments && (
368-
<ChatMessageAttachments
369-
attachments={msg.attachments!}
370-
align='end'
371-
className='max-w-[70%]'
372-
/>
373-
)}
374-
<div className='max-w-[70%] overflow-hidden rounded-[16px] bg-[var(--surface-5)] px-3.5 py-2'>
375-
<UserMessageContent content={msg.content} contexts={msg.contexts} />
376-
</div>
377-
</div>
378-
)
379-
}
380-
381-
const hasAnyBlocks = Boolean(msg.contentBlocks?.length)
382-
const hasRenderableAssistant = assistantMessageHasRenderableContent(
383-
msg.contentBlocks ?? [],
384-
msg.content ?? ''
385-
)
386-
const isLastAssistant = msg.role === 'assistant' && index === messages.length - 1
387-
const isThisStreaming = isSending && isLastAssistant
388-
389-
if (!hasAnyBlocks && !msg.content?.trim() && isThisStreaming) {
390-
return <PendingTagIndicator key={msg.id} />
391-
}
392-
393-
if (!hasRenderableAssistant && !msg.content?.trim() && !isThisStreaming) {
394-
return null
395-
}
396-
397-
const isLastMessage = index === messages.length - 1
398-
399-
return (
400-
<div key={msg.id} className='group/msg relative pb-5'>
401-
{!isThisStreaming && (msg.content || msg.contentBlocks?.length) && (
402-
<div className='absolute right-0 bottom-0 z-10'>
403-
<MessageActions content={msg.content} requestId={msg.requestId} />
404-
</div>
405-
)}
406-
<MessageContent
407-
blocks={msg.contentBlocks || []}
408-
fallbackContent={msg.content}
409-
isStreaming={isThisStreaming}
410-
onOptionSelect={isLastMessage ? sendMessage : undefined}
411-
/>
412-
</div>
413-
)
414-
})}
415-
</div>
416-
</div>
417-
418-
<div
419-
className={`flex-shrink-0 px-[24px] pb-[16px]${isInputEntering ? ' animate-slide-in-bottom' : ''}`}
420-
onAnimationEnd={isInputEntering ? () => setIsInputEntering(false) : undefined}
421-
>
422-
<div className='mx-auto max-w-[42rem]'>
423-
<QueuedMessages
424-
messageQueue={messageQueue}
425-
onRemove={removeFromQueue}
426-
onSendNow={sendNow}
427-
onEdit={handleEditQueuedMessage}
428-
/>
429-
<UserInput
430-
onSubmit={handleSubmit}
431-
isSending={isSending}
432-
onStopGeneration={stopGeneration}
433-
isInitialView={false}
434-
userId={session?.user?.id}
435-
onContextAdd={handleContextAdd}
436-
editValue={editingInputValue}
437-
onEditValueConsumed={clearEditingValue}
438-
/>
439-
</div>
440-
</div>
335+
<MothershipChat
336+
messages={messages}
337+
isSending={isSending}
338+
onSubmit={handleSubmit}
339+
onStopGeneration={stopGeneration}
340+
messageQueue={messageQueue}
341+
onRemoveQueuedMessage={removeFromQueue}
342+
onSendQueuedMessage={sendNow}
343+
onEditQueuedMessage={handleEditQueuedMessage}
344+
userId={session?.user?.id}
345+
onContextAdd={handleContextAdd}
346+
editValue={editingInputValue}
347+
onEditValueConsumed={clearEditingValue}
348+
animateInput={isInputEntering}
349+
onInputAnimationEnd={isInputEntering ? () => setIsInputEntering(false) : undefined}
350+
initialScrollBlocked={resources.length > 0 && isResourceCollapsed}
351+
/>
441352
</div>
442353

443354
{/* Resize handle — zero-width flex child whose absolute child straddles the border */}
Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
export { useAnimatedPlaceholder } from './use-animated-placeholder'
22
export { useAutoScroll } from './use-auto-scroll'
33
export type { UseChatReturn } from './use-chat'
4-
export { useChat } from './use-chat'
4+
export {
5+
getMothershipUseChatOptions,
6+
getWorkflowCopilotUseChatOptions,
7+
useChat,
8+
} from './use-chat'
59
export { useMothershipResize } from './use-mothership-resize'
610
export { useStreamingReveal } from './use-streaming-reveal'

0 commit comments

Comments
 (0)