Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 3.8k
improvement(mothership): message queueing for home chat#3576
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
8610d677456c3c84ad16567d328d3f17446d8ad8a4af7a2a871a83af8c0fe13ad0d418File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,6 @@ | ||
| export { MessageContent } from './message-content' | ||
| export { MothershipView } from './mothership-view' | ||
| export { QueuedMessages } from './queued-messages' | ||
| export { TemplatePrompts } from './template-prompts' | ||
| export { UserInput } from './user-input' | ||
| export { UserMessageContent } from './user-message-content' |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export { QueuedMessages } from './queued-messages' |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,113 @@ | ||
| 'use client' | ||
| import { useState } from 'react' | ||
| import { ArrowUp, ChevronDown, ChevronRight, Pencil, Trash2 } from 'lucide-react' | ||
| import { Tooltip } from '@/components/emcn' | ||
| import type { QueuedMessage } from '@/app/workspace/[workspaceId]/home/types' | ||
| interface QueuedMessagesProps { | ||
| messageQueue: QueuedMessage[] | ||
| onRemove: (id: string) => void | ||
| onSendNow: (id: string) => Promise<void> | ||
| onEdit: (id: string) => void | ||
| } | ||
| export function QueuedMessages({ messageQueue, onRemove, onSendNow, onEdit }: QueuedMessagesProps) { | ||
| const [isExpanded, setIsExpanded] = useState(true) | ||
| if (messageQueue.length === 0) return null | ||
| return ( | ||
| <div className='-mb-[12px] mx-[14px] overflow-hidden rounded-t-[16px] border border-[var(--border-1)] border-b-0 bg-[var(--surface-2)] pb-[12px] dark:bg-[var(--surface-3)]'> | ||
| <button | ||
| type='button' | ||
| onClick={() => setIsExpanded(!isExpanded)} | ||
| className='flex w-full items-center gap-[6px] px-[14px] py-[8px] transition-colors hover:bg-black/[0.03] dark:hover:bg-white/[0.03]' | ||
| > | ||
| {isExpanded ? ( | ||
| <ChevronDown className='h-[14px] w-[14px] text-[var(--text-tertiary)]' /> | ||
| ) : ( | ||
| <ChevronRight className='h-[14px] w-[14px] text-[var(--text-tertiary)]' /> | ||
| )} | ||
| <span className='font-medium text-[13px] text-[var(--text-secondary)]'> | ||
| {messageQueue.length} Queued | ||
| </span> | ||
| </button> | ||
| {isExpanded && ( | ||
| <div> | ||
| {messageQueue.map((msg) => ( | ||
| <div | ||
| key={msg.id} | ||
| className='flex items-center gap-[8px] px-[14px] py-[6px] transition-colors hover:bg-black/[0.03] dark:hover:bg-white/[0.03]' | ||
| > | ||
| <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-[var(--text-tertiary)]/40' /> | ||
| </div> | ||
| <div className='min-w-0 flex-1'> | ||
| <p className='truncate text-[13px] text-[var(--text-primary)]'>{msg.content}</p> | ||
| </div> | ||
| <div className='flex shrink-0 items-center gap-[2px]'> | ||
| <Tooltip.Root> | ||
| <Tooltip.Trigger asChild> | ||
| <button | ||
| type='button' | ||
| onClick={(e) => { | ||
| e.stopPropagation() | ||
| onEdit(msg.id) | ||
| }} | ||
| className='rounded-[6px] p-[5px] text-[var(--text-tertiary)] transition-colors hover:bg-black/[0.06] hover:text-[var(--text-primary)] dark:hover:bg-white/[0.06]' | ||
| > | ||
| <Pencil className='h-[13px] w-[13px]' /> | ||
| </button> | ||
| </Tooltip.Trigger> | ||
| <Tooltip.Content side='top' sideOffset={4}> | ||
| Edit queued message | ||
| </Tooltip.Content> | ||
| </Tooltip.Root> | ||
| <Tooltip.Root> | ||
| <Tooltip.Trigger asChild> | ||
| <button | ||
| type='button' | ||
| onClick={(e) => { | ||
| e.stopPropagation() | ||
| void onSendNow(msg.id) | ||
| }} | ||
| className='rounded-[6px] p-[5px] text-[var(--text-tertiary)] transition-colors hover:bg-black/[0.06] hover:text-[var(--text-primary)] dark:hover:bg-white/[0.06]' | ||
| > | ||
| <ArrowUp className='h-[13px] w-[13px]' /> | ||
| </button> | ||
| </Tooltip.Trigger> | ||
| <Tooltip.Content side='top' sideOffset={4}> | ||
| Send now | ||
| </Tooltip.Content> | ||
| </Tooltip.Root> | ||
| <Tooltip.Root> | ||
| <Tooltip.Trigger asChild> | ||
| <button | ||
| type='button' | ||
| onClick={(e) => { | ||
| e.stopPropagation() | ||
| onRemove(msg.id) | ||
| }} | ||
| className='rounded-[6px] p-[5px] text-[var(--text-tertiary)] transition-colors hover:bg-black/[0.06] hover:text-[var(--text-primary)] dark:hover:bg-white/[0.06]' | ||
| > | ||
| <Trash2 className='h-[13px] w-[13px]' /> | ||
| </button> | ||
| </Tooltip.Trigger> | ||
| <Tooltip.Content side='top' sideOffset={4}> | ||
| Remove from queue | ||
| </Tooltip.Content> | ||
| </Tooltip.Root> | ||
| </div> | ||
| </div> | ||
| ))} | ||
| </div> | ||
| )} | ||
| </div> | ||
| ) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -64,7 +64,10 @@ import { cn } from '@/lib/core/utils/cn' | ||
| import { CHAT_ACCEPT_ATTRIBUTE } from '@/lib/uploads/utils/validation' | ||
| import { useAvailableResources } from '@/app/workspace/[workspaceId]/home/components/mothership-view/components/add-resource-dropdown' | ||
| import { getResourceConfig } from '@/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-registry' | ||
| import type { MothershipResource } from '@/app/workspace/[workspaceId]/home/types' | ||
| import type { | ||
| FileAttachmentForApi, | ||
| MothershipResource, | ||
| } from '@/app/workspace/[workspaceId]/home/types' | ||
| import { | ||
| useContextManagement, | ||
| useFileAttachments, | ||
| @@ -125,9 +128,17 @@ function autoResizeTextarea(e: React.FormEvent<HTMLTextAreaElement>, maxHeight: | ||
| function mapResourceToContext(resource: MothershipResource): ChatContext { | ||
| switch (resource.type) { | ||
| case 'workflow': | ||
| return { kind: 'workflow', workflowId: resource.id, label: resource.title } | ||
| return { | ||
| kind: 'workflow', | ||
| workflowId: resource.id, | ||
| label: resource.title, | ||
| } | ||
| case 'knowledgebase': | ||
| return { kind: 'knowledge', knowledgeId: resource.id, label: resource.title } | ||
| return { | ||
| kind: 'knowledge', | ||
| knowledgeId: resource.id, | ||
| label: resource.title, | ||
| } | ||
| case 'table': | ||
| return { kind: 'table', tableId: resource.id, label: resource.title } | ||
| case 'file': | ||
| @@ -137,16 +148,12 @@ function mapResourceToContext(resource: MothershipResource): ChatContext { | ||
| } | ||
| } | ||
| export interface FileAttachmentForApi { | ||
| id: string | ||
| key: string | ||
| filename: string | ||
| media_type: string | ||
| size: number | ||
| } | ||
| export type { FileAttachmentForApi } from '@/app/workspace/[workspaceId]/home/types' | ||
| interface UserInputProps { | ||
| defaultValue?: string | ||
| editValue?: string | ||
| onEditValueConsumed?: () => void | ||
| onSubmit: ( | ||
| text: string, | ||
| fileAttachments?: FileAttachmentForApi[], | ||
| @@ -161,6 +168,8 @@ interface UserInputProps { | ||
| export function UserInput({ | ||
| defaultValue = '', | ||
| editValue, | ||
| onEditValueConsumed, | ||
| onSubmit, | ||
| isSending, | ||
| onStopGeneration, | ||
| @@ -176,9 +185,27 @@ export function UserInput({ | ||
| const [plusMenuActiveIndex, setPlusMenuActiveIndex] = useState(0) | ||
| const overlayRef = useRef<HTMLDivElement>(null) | ||
| const [prevDefaultValue, setPrevDefaultValue] = useState(defaultValue) | ||
| if (defaultValue && defaultValue !== prevDefaultValue) { | ||
| setPrevDefaultValue(defaultValue) | ||
| setValue(defaultValue) | ||
| } else if (!defaultValue && prevDefaultValue) { | ||
| setPrevDefaultValue(defaultValue) | ||
| } | ||
| const [prevEditValue, setPrevEditValue] = useState(editValue) | ||
| if (editValue && editValue !== prevEditValue) { | ||
| setPrevEditValue(editValue) | ||
| setValue(editValue) | ||
| } else if (!editValue && prevEditValue) { | ||
| setPrevEditValue(editValue) | ||
| } | ||
waleedlatif1 marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| useEffect(() => { | ||
| if (defaultValue) setValue(defaultValue) | ||
| }, [defaultValue]) | ||
| if (editValue) { | ||
| onEditValueConsumed?.() | ||
| } | ||
| }, [editValue, onEditValueConsumed]) | ||
| const animatedPlaceholder = useAnimatedPlaceholder(isInitialView) | ||
| const placeholder = isInitialView ? animatedPlaceholder : 'Send message to Sim' | ||
| @@ -393,9 +420,7 @@ export function UserInput({ | ||
| (e: React.KeyboardEvent<HTMLTextAreaElement>) => { | ||
| if (e.key === 'Enter' && !e.shiftKey && !e.nativeEvent.isComposing) { | ||
| e.preventDefault() | ||
| if (!isSending) { | ||
| handleSubmit() | ||
| } | ||
| handleSubmit() | ||
waleedlatif1 marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| return | ||
| } | ||
| @@ -461,7 +486,7 @@ export function UserInput({ | ||
| } | ||
| } | ||
| }, | ||
| [handleSubmit, isSending, mentionTokensWithContext, value, textareaRef] | ||
| [handleSubmit, mentionTokensWithContext, value, textareaRef] | ||
| ) | ||
| const handleInputChange = useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => { | ||
| @@ -637,7 +662,9 @@ export function UserInput({ | ||
| <span | ||
| key={`mention-${i}-${range.start}-${range.end}`} | ||
| className='rounded-[5px] bg-[var(--surface-5)] py-[2px]' | ||
| style={{ boxShadow: '-2px 0 0 var(--surface-5), 2px 0 0 var(--surface-5)' }} | ||
| style={{ | ||
| boxShadow: '-2px 0 0 var(--surface-5), 2px 0 0 var(--surface-5)', | ||
| }} | ||
| > | ||
| <span className='relative'> | ||
| <span className='invisible'>{range.token.charAt(0)}</span> | ||
| @@ -662,7 +689,7 @@ export function UserInput({ | ||
| <div | ||
| onClick={handleContainerClick} | ||
| className={cn( | ||
| 'relative mx-auto w-full max-w-[42rem] cursor-text rounded-[20px] border border-[var(--border-1)] bg-[var(--white)] px-[10px] py-[8px] dark:bg-[var(--surface-4)]', | ||
| 'relative z-10 mx-auto w-full max-w-[42rem] cursor-text rounded-[20px] border border-[var(--border-1)] bg-[var(--white)] px-[10px] py-[8px] dark:bg-[var(--surface-4)]', | ||
| isInitialView && 'shadow-sm' | ||
| )} | ||
| onDragEnter={files.handleDragEnter} | ||
| @@ -818,7 +845,11 @@ export function UserInput({ | ||
| )} | ||
| onMouseEnter={() => setPlusMenuActiveIndex(index)} | ||
| onClick={() => { | ||
| handleResourceSelect({ type, id: item.id, title: item.name }) | ||
| handleResourceSelect({ | ||
| type, | ||
| id: item.id, | ||
| title: item.name, | ||
| }) | ||
| setPlusMenuOpen(false) | ||
| setPlusMenuSearch('') | ||
| setPlusMenuActiveIndex(0) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -19,14 +19,14 @@ import type { ChatContext } from '@/stores/panel' | ||
| import { | ||
| MessageContent, | ||
| MothershipView, | ||
| QueuedMessages, | ||
| TemplatePrompts, | ||
| UserInput, | ||
| UserMessageContent, | ||
| } from './components' | ||
| import { PendingTagIndicator } from './components/message-content/components/special-tags' | ||
| import type { FileAttachmentForApi } from './components/user-input/user-input' | ||
| import { useAutoScroll, useChat } from './hooks' | ||
| import type { MothershipResource, MothershipResourceType } from './types' | ||
| import type { FileAttachmentForApi, MothershipResource, MothershipResourceType } from './types' | ||
| const logger = createLogger('Home') | ||
| @@ -183,8 +183,29 @@ export function Home({ chatId }: HomeProps = {}) { | ||
| addResource, | ||
| removeResource, | ||
| reorderResources, | ||
| messageQueue, | ||
| removeFromQueue, | ||
| sendNow, | ||
| editQueuedMessage, | ||
| } = useChat(workspaceId, chatId, { onResourceEvent: handleResourceEvent }) | ||
| const [editingInputValue, setEditingInputValue] = useState('') | ||
| const clearEditingValue = useCallback(() => setEditingInputValue(''), []) | ||
| const handleEditQueuedMessage = useCallback( | ||
| (id: string) => { | ||
| const msg = editQueuedMessage(id) | ||
| if (msg) { | ||
| setEditingInputValue(msg.content) | ||
| } | ||
| }, | ||
| [editQueuedMessage] | ||
| ) | ||
waleedlatif1 marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. waleedlatif1 marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| useEffect(() => { | ||
| setEditingInputValue('') | ||
| }, [chatId]) | ||
| useEffect(() => { | ||
| wasSendingRef.current = false | ||
| if (resolvedChatId) markRead(resolvedChatId) | ||
| @@ -419,13 +440,21 @@ export function Home({ chatId }: HomeProps = {}) { | ||
| <div className='flex-shrink-0 px-[24px] pb-[16px]'> | ||
| <div className='mx-auto max-w-[42rem]'> | ||
| <QueuedMessages | ||
| messageQueue={messageQueue} | ||
| onRemove={removeFromQueue} | ||
| onSendNow={sendNow} | ||
| onEdit={handleEditQueuedMessage} | ||
| /> | ||
| <UserInput | ||
| onSubmit={handleSubmit} | ||
| isSending={isSending} | ||
| onStopGeneration={stopGeneration} | ||
| isInitialView={false} | ||
| userId={session?.user?.id} | ||
| onContextAdd={handleContextAdd} | ||
| editValue={editingInputValue} | ||
| onEditValueConsumed={clearEditingValue} | ||
| /> | ||
| </div> | ||
| </div> | ||
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.