Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
fix ui
  • Loading branch information
icecrasher321 committed Mar 18, 2026
commit c0ff3cae2a707406596cb439737280c73ce31a31
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
'use client'

import { useCallback, useState } from 'react'
import { useCallback, useEffect, useRef, useState } from 'react'
import { Check, Copy, Ellipsis, Hash } from 'lucide-react'
import {
Popover,
PopoverContent,
PopoverItem,
PopoverScrollArea,
PopoverTrigger,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
Tooltip,
} from '@/components/emcn'

interface MessageActionsProps {
Expand All @@ -16,60 +16,75 @@ interface MessageActionsProps {
}

export function MessageActions({ content, requestId }: MessageActionsProps) {
const [open, setOpen] = useState(false)
const [copied, setCopied] = useState<'message' | 'request' | null>(null)
const resetTimeoutRef = useRef<number | null>(null)

useEffect(() => {
return () => {
if (resetTimeoutRef.current !== null) {
window.clearTimeout(resetTimeoutRef.current)
}
}
}, [])

const copyToClipboard = useCallback(async (text: string, type: 'message' | 'request') => {
try {
await navigator.clipboard.writeText(text)
setCopied(type)
setTimeout(() => setCopied(null), 1500)
if (resetTimeoutRef.current !== null) {
window.clearTimeout(resetTimeoutRef.current)
}
resetTimeoutRef.current = window.setTimeout(() => setCopied(null), 1500)
} catch {
// Silently fail
return
}
setOpen(false)
}, [])

if (!content && !requestId) {
return null
}

return (
<Popover variant='default' size='sm' open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<button
type='button'
className='rounded-md p-1 text-[var(--text-icon)] opacity-0 transition-opacity hover:bg-[var(--surface-3)] group-hover/msg:opacity-100 data-[state=open]:opacity-100'
onClick={(e) => e.stopPropagation()}
<DropdownMenu modal={false}>
<Tooltip.Root>
<Tooltip.Trigger asChild>
<DropdownMenuTrigger asChild>
<button
type='button'
aria-label='More options'
className='flex h-5 w-5 items-center justify-center rounded-sm text-[var(--text-icon)] opacity-0 transition-colors transition-opacity hover:bg-[var(--surface-3)] hover:text-[var(--text-primary)] focus-visible:opacity-100 focus-visible:outline-none group-hover/msg:opacity-100 data-[state=open]:opacity-100'
onClick={(event) => event.stopPropagation()}
>
<Ellipsis className='h-3 w-3' strokeWidth={2} />
</button>
</DropdownMenuTrigger>
</Tooltip.Trigger>
<Tooltip.Content side='top'>More options</Tooltip.Content>
</Tooltip.Root>
<DropdownMenuContent align='end' side='top' sideOffset={4}>
<DropdownMenuItem
disabled={!content}
onSelect={(event) => {
event.stopPropagation()
void copyToClipboard(content, 'message')
}}
>
{copied === 'message' ? <Check /> : <Copy />}
<span>Copy Message</span>
</DropdownMenuItem>
<DropdownMenuItem
disabled={!requestId}
onSelect={(event) => {
event.stopPropagation()
if (requestId) {
void copyToClipboard(requestId, 'request')
}
}}
>
<Ellipsis className='h-[14px] w-[14px]' strokeWidth={2} />
</button>
</PopoverTrigger>
<PopoverContent
side='bottom'
align='end'
sideOffset={4}
maxHeight={120}
style={{ width: '160px', minWidth: '160px' }}
>
<PopoverScrollArea>
<PopoverItem onClick={() => copyToClipboard(content, 'message')} disabled={!content}>
{copied === 'message' ? (
<Check className='h-[13px] w-[13px]' />
) : (
<Copy className='h-[13px] w-[13px]' />
)}
<span>Copy Message</span>
</PopoverItem>
<PopoverItem
onClick={() => requestId && copyToClipboard(requestId, 'request')}
disabled={!requestId}
>
{copied === 'request' ? (
<Check className='h-[13px] w-[13px]' />
) : (
<Hash className='h-[13px] w-[13px]' />
)}
<span>Copy Request ID</span>
</PopoverItem>
</PopoverScrollArea>
</PopoverContent>
</Popover>
{copied === 'request' ? <Check /> : <Hash />}
<span>Copy Request ID</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
}
4 changes: 2 additions & 2 deletions apps/sim/app/workspace/[workspaceId]/home/home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -415,9 +415,9 @@ export function Home({ chatId }: HomeProps = {}) {
const isLastMessage = index === messages.length - 1

return (
<div key={msg.id} className='group/msg relative pb-4'>
<div key={msg.id} className='group/msg relative pb-5'>
{!isThisStreaming && (msg.content || msg.contentBlocks?.length) && (
<div className='-top-1 absolute right-0 z-10'>
<div className='absolute right-0 bottom-0 z-10'>
<MessageActions content={msg.content} requestId={msg.requestId} />
</div>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -414,11 +414,11 @@ const CopilotMessage: FC<CopilotMessageProps> = memo(
style={{ '--panel-max-width': `${panelWidth - 16}px` } as React.CSSProperties}
>
{!isStreaming && message.content && (
Comment thread
icecrasher321 marked this conversation as resolved.
Outdated
<div className='-top-1 absolute right-0 z-10'>
<div className='absolute right-0 bottom-0 z-10'>
<MessageActions content={message.content} requestId={message.requestId} />
</div>
Comment thread
icecrasher321 marked this conversation as resolved.
)}
<div className='max-w-full space-y-[4px] px-[2px] pb-[4px]'>
<div className='max-w-full space-y-[4px] px-[2px] pb-5'>
{/* Content blocks in chronological order */}
{memoizedContentBlocks || (isStreaming && <div className='min-h-0' />)}

Expand Down
9 changes: 9 additions & 0 deletions apps/sim/lib/copilot/client-sse/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ export function flushStreamingUpdates(set: StoreSet) {
if (update) {
return {
...msg,
requestId: update.requestId ?? msg.requestId,
content: '',
contentBlocks:
update.contentBlocks.length > 0
Expand Down Expand Up @@ -129,6 +130,7 @@ export function updateStreamingMessage(set: StoreSet, context: ClientStreamingCo
const newMessages = [...messages]
newMessages[messages.length - 1] = {
...lastMessage,
requestId: lastMessageUpdate.requestId ?? lastMessage.requestId,
content: '',
contentBlocks:
lastMessageUpdate.contentBlocks.length > 0
Expand All @@ -143,6 +145,7 @@ export function updateStreamingMessage(set: StoreSet, context: ClientStreamingCo
if (update) {
return {
...msg,
requestId: update.requestId ?? msg.requestId,
content: '',
contentBlocks:
update.contentBlocks.length > 0
Expand Down Expand Up @@ -429,6 +432,12 @@ export const sseHandlers: Record<string, SSEHandler> = {
writeActiveStreamToStorage(updatedStream)
}
},
request_id: (data, context) => {
const requestId = typeof data.data === 'string' ? data.data : undefined
if (requestId) {
context.requestId = requestId
}
},
title_updated: (_data, _context, get, set) => {
const title = _data.title
if (!title) return
Expand Down
1 change: 1 addition & 0 deletions apps/sim/lib/copilot/client-sse/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export interface ClientContentBlock {

export interface StreamingContext {
messageId: string
requestId?: string
accumulatedContent: string
contentBlocks: ClientContentBlock[]
currentTextBlock: ClientContentBlock | null
Expand Down
2 changes: 2 additions & 0 deletions apps/sim/stores/panel/copilot/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,7 @@ function replaceTextBlocks(blocks: ClientContentBlock[], text: string): ClientCo
function createClientStreamingContext(messageId: string): ClientStreamingContext {
return {
messageId,
requestId: undefined,
accumulatedContent: '',
contentBlocks: [],
currentTextBlock: null,
Expand Down Expand Up @@ -2043,6 +2044,7 @@ export const useCopilotStore = create<CopilotStore>()(
msg.id === assistantMessageId
? {
...msg,
requestId: context.requestId ?? msg.requestId,
content: finalContentWithOptions,
contentBlocks: sanitizedContentBlocks,
}
Expand Down
Loading