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
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ import {
} from '@/app/workspace/[workspaceId]/home/components/message-content'
import { PendingTagIndicator } from '@/app/workspace/[workspaceId]/home/components/message-content/components/special-tags'
import { QueuedMessages } from '@/app/workspace/[workspaceId]/home/components/queued-messages'
import { UserInput } from '@/app/workspace/[workspaceId]/home/components/user-input'
import {
UserInput,
type UserInputHandle,
} from '@/app/workspace/[workspaceId]/home/components/user-input'
import { UserMessageContent } from '@/app/workspace/[workspaceId]/home/components/user-message-content'
import type {
ChatMessage,
Expand All @@ -36,14 +39,12 @@ interface MothershipChatProps {
messageQueue: QueuedMessage[]
onRemoveQueuedMessage: (id: string) => void
onSendQueuedMessage: (id: string) => Promise<void>
onEditQueuedMessage: (id: string) => void
onEditQueuedMessage: (id: string) => QueuedMessage | undefined
userId?: string
chatId?: string
onContextAdd?: (context: ChatContext) => void
onContextRemove?: (context: ChatContext) => void
onWorkspaceResourceSelect?: (resource: MothershipResource) => void
editValue?: string
onEditValueConsumed?: () => void
layout?: 'mothership-view' | 'copilot-view'
initialScrollBlocked?: boolean
animateInput?: boolean
Expand Down Expand Up @@ -91,8 +92,6 @@ export function MothershipChat({
onContextAdd,
onContextRemove,
onWorkspaceResourceSelect,
editValue,
onEditValueConsumed,
layout = 'mothership-view',
initialScrollBlocked = false,
animateInput = false,
Expand All @@ -106,11 +105,24 @@ export function MothershipChat({
})
const hasMessages = messages.length > 0
const initialScrollDoneRef = useRef(false)
const userInputRef = useRef<UserInputHandle>(null)
const handleSendQueuedHead = useCallback(() => {
const topMessage = messageQueue[0]
if (!topMessage) return
void onSendQueuedMessage(topMessage.id)
}, [messageQueue, onSendQueuedMessage])
const handleEditQueued = useCallback(
(id: string) => {
const msg = onEditQueuedMessage(id)
if (msg) userInputRef.current?.loadQueuedMessage(msg)
},
[onEditQueuedMessage]
)
const handleEditQueuedTail = useCallback(() => {
const tail = messageQueue[messageQueue.length - 1]
if (!tail) return
handleEditQueued(tail.id)
}, [messageQueue, handleEditQueued])

useLayoutEffect(() => {
if (!hasMessages) {
Expand Down Expand Up @@ -205,19 +217,19 @@ export function MothershipChat({
messageQueue={messageQueue}
onRemove={onRemoveQueuedMessage}
onSendNow={onSendQueuedMessage}
onEdit={onEditQueuedMessage}
onEdit={handleEditQueued}
/>
<UserInput
ref={userInputRef}
onSubmit={onSubmit}
isSending={isStreamActive}
onStopGeneration={onStopGeneration}
isInitialView={false}
userId={userId}
onContextAdd={onContextAdd}
onContextRemove={onContextRemove}
editValue={editValue}
onEditValueConsumed={onEditValueConsumed}
onSendQueuedHead={handleSendQueuedHead}
onEditQueuedTail={handleEditQueuedTail}
/>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
'use client'

import { useState } from 'react'
import { ArrowUp, ChevronDown, ChevronRight, Pencil, Trash2 } from 'lucide-react'
import { useEffect, useRef, useState } from 'react'
import { ArrowUp, ChevronDown, ChevronRight, Paperclip, Pencil, Trash2 } from 'lucide-react'
import { Tooltip } from '@/components/emcn'
import { UserMessageContent } from '@/app/workspace/[workspaceId]/home/components/user-message-content'
import type { QueuedMessage } from '@/app/workspace/[workspaceId]/home/types'

const NARROW_WIDTH_PX = 320

interface QueuedMessagesProps {
messageQueue: QueuedMessage[]
onRemove: (id: string) => void
Expand All @@ -14,11 +17,28 @@ interface QueuedMessagesProps {

export function QueuedMessages({ messageQueue, onRemove, onSendNow, onEdit }: QueuedMessagesProps) {
const [isExpanded, setIsExpanded] = useState(true)
const containerRef = useRef<HTMLDivElement>(null)
const [isNarrow, setIsNarrow] = useState(false)

const hasMessages = messageQueue.length > 0

if (messageQueue.length === 0) return null
useEffect(() => {
const el = containerRef.current
if (!el) return
const ro = new ResizeObserver((entries) => {
setIsNarrow(entries[0].contentRect.width < NARROW_WIDTH_PX)
})
ro.observe(el)
return () => ro.disconnect()
}, [hasMessages])

if (!hasMessages) return null

return (
<div className='-mb-3 mx-3.5 overflow-hidden rounded-t-[16px] border border-[var(--border-1)] border-b-0 bg-[var(--surface-3)] pb-3'>
<div
ref={containerRef}
className='-mb-3 mx-3.5 overflow-hidden rounded-t-[16px] border border-[var(--border-1)] border-b-0 bg-[var(--surface-3)] pb-3'
>
<button
type='button'
onClick={() => setIsExpanded(!isExpanded)}
Expand All @@ -39,16 +59,41 @@ export function QueuedMessages({ messageQueue, onRemove, onSendNow, onEdit }: Qu
{messageQueue.map((msg) => (
<div
key={msg.id}
className='flex items-center gap-2 px-3.5 py-1.5 transition-colors hover-hover:bg-[var(--surface-active)]'
className='flex items-center gap-2 py-1.5 pr-2 pl-3.5 transition-colors hover-hover:bg-[var(--surface-active)]'
>
<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-[color-mix(in_srgb,var(--text-tertiary)_40%,transparent)]' />
</div>

<div className='min-w-0 flex-1'>
<p className='truncate text-[var(--text-primary)] text-small'>{msg.content}</p>
<div className='min-w-0 flex-1 overflow-hidden'>
<UserMessageContent
content={msg.content}
contexts={msg.contexts}
plainMentions
className='!truncate !whitespace-nowrap !text-small !leading-[20px]'
/>
</div>

{msg.fileAttachments && msg.fileAttachments.length > 0 && (
<span className='inline-flex min-w-0 max-w-[40%] shrink items-center gap-1 rounded-[5px] bg-[var(--surface-5)] px-[5px] py-0.5 text-[var(--text-primary)] text-small'>
<Paperclip className='h-[12px] w-[12px] shrink-0 text-[var(--text-icon)]' />
{isNarrow ? (
<span className='shrink-0 text-[var(--text-secondary)]'>
{msg.fileAttachments.length}
</span>
) : (
<>
<span className='truncate'>{msg.fileAttachments[0].filename}</span>
{msg.fileAttachments.length > 1 && (
<span className='shrink-0 text-[var(--text-secondary)]'>
+{msg.fileAttachments.length - 1}
</span>
)}
</>
)}
</span>
)}

<div className='flex shrink-0 items-center gap-0.5'>
<Tooltip.Root>
<Tooltip.Trigger asChild>
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export { UserInput } from './user-input'
export { UserInput, type UserInputHandle } from './user-input'
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
'use client'

import type React from 'react'
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
import {
forwardRef,
useCallback,
useEffect,
useImperativeHandle,
useLayoutEffect,
useMemo,
useRef,
useState,
} from 'react'
import { useParams } from 'next/navigation'
import { useSession } from '@/lib/auth/auth-client'
import { SIM_RESOURCE_DRAG_TYPE, SIM_RESOURCES_DRAG_TYPE } from '@/lib/copilot/resource-types'
Expand All @@ -26,6 +35,7 @@ import {
import type {
FileAttachmentForApi,
MothershipResource,
QueuedMessage,
} from '@/app/workspace/[workspaceId]/home/types'
import {
useContextManagement,
Expand Down Expand Up @@ -91,8 +101,6 @@ function getCaretAnchor(

interface UserInputProps {
defaultValue?: string
editValue?: string
onEditValueConsumed?: () => void
onSubmit: (
text: string,
fileAttachments?: FileAttachmentForApi[],
Expand All @@ -105,21 +113,28 @@ interface UserInputProps {
onContextAdd?: (context: ChatContext) => void
onContextRemove?: (context: ChatContext) => void
onSendQueuedHead?: () => void
onEditQueuedTail?: () => void
}

export interface UserInputHandle {
loadQueuedMessage: (msg: QueuedMessage) => void
}

export function UserInput({
defaultValue = '',
editValue,
onEditValueConsumed,
onSubmit,
isSending,
onStopGeneration,
isInitialView = true,
userId,
onContextAdd,
onContextRemove,
onSendQueuedHead,
}: UserInputProps) {
export const UserInput = forwardRef<UserInputHandle, UserInputProps>(function UserInput(
{
defaultValue = '',
onSubmit,
isSending,
onStopGeneration,
isInitialView = true,
userId,
onContextAdd,
onContextRemove,
onSendQueuedHead,
onEditQueuedTail,
},
ref
) {
const { workspaceId } = useParams<{ workspaceId: string }>()
const { navigateToSettings } = useSettingsNavigation()
const { data: workflowsById = {} } = useWorkflowMap(workspaceId)
Expand All @@ -136,18 +151,6 @@ export function UserInput({
setPrevDefaultValue(defaultValue)
}

const [prevEditValue, setPrevEditValue] = useState(editValue)
if (editValue && editValue !== prevEditValue) {
setPrevEditValue(editValue)
setValue(editValue)
} else if (!editValue && prevEditValue) {
setPrevEditValue(editValue)
}

useEffect(() => {
if (editValue) onEditValueConsumed?.()
}, [editValue, onEditValueConsumed])

const files = useFileAttachments({
userId: userId || session?.user?.id,
workspaceId,
Expand Down Expand Up @@ -269,6 +272,8 @@ export function UserInput({
contextRef.current = contextManagement
const onSendQueuedHeadRef = useRef(onSendQueuedHead)
onSendQueuedHeadRef.current = onSendQueuedHead
const onEditQueuedTailRef = useRef(onEditQueuedTail)
onEditQueuedTailRef.current = onEditQueuedTail
const isSendingRef = useRef(isSending)
isSendingRef.current = isSending

Expand All @@ -277,6 +282,34 @@ export function UserInput({
const atInsertPosRef = useRef<number | null>(null)
const pendingCursorRef = useRef<number | null>(null)

useImperativeHandle(
ref,
() => ({
loadQueuedMessage: (msg: QueuedMessage) => {
setValue(msg.content)
const restored: AttachedFile[] = (msg.fileAttachments ?? []).map((a) => ({
id: a.id,
name: a.filename,
size: a.size,
type: a.media_type,
path: a.path ?? '',
key: a.key,
uploading: false,
}))
files.restoreAttachedFiles(restored)
contextManagement.setSelectedContexts(msg.contexts ?? [])
requestAnimationFrame(() => {
const textarea = textareaRef.current
if (!textarea) return
textarea.focus()
const end = textarea.value.length
textarea.setSelectionRange(end, end)
})
},
}),
[files.restoreAttachedFiles, contextManagement.setSelectedContexts, textareaRef]
)

useLayoutEffect(() => {
const textarea = textareaRef.current
if (!textarea) return
Expand Down Expand Up @@ -430,6 +463,7 @@ export function UserInput({
filename: f.name,
media_type: f.type,
size: f.size,
...(f.path ? { path: f.path } : {}),
}))

onSubmit(
Expand All @@ -452,6 +486,15 @@ export function UserInput({

const handleKeyDown = useCallback(
(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === 'ArrowUp' && !e.shiftKey && !e.metaKey && !e.ctrlKey && !e.altKey) {
const isEmpty = valueRef.current.length === 0 && filesRef.current.attachedFiles.length === 0
if (isEmpty && onEditQueuedTailRef.current) {
e.preventDefault()
onEditQueuedTailRef.current()
return
}
}

if (e.key === 'Enter' && !e.shiftKey && !e.nativeEvent.isComposing) {
e.preventDefault()
const hasSubmitPayload =
Expand Down Expand Up @@ -763,4 +806,4 @@ export function UserInput({
{files.isDragging && <DropOverlay />}
</div>
)
}
})
Loading
Loading