mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 18:05:11 +03:00
<img width="2252" height="1228" alt="image" src="https://github.com/user-attachments/assets/5c1165ae-cb65-4495-97dd-427b30ceaefc" /> ## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? Studio UI improvement. ## Stack context Builds on #49350. ## What is the current behavior? The Assistant conversation uses one outer width constraint. This leaves query and notebook previews too narrow, separates consecutive generic tool rows, and leaves message actions aligned to the far left. ## What is the new behavior? - Gives Assistant query cells and notebook previews a `max-w-6xl` container. - Keeps text and other regular message parts at their existing `max-w-3xl` width. - Keeps consecutive generic tool rows such as Reasoned and Ran load_knowledge compact. - Aligns message action rows with regular message content. ## To test 1. In the Assistant, produce a response containing text plus a SQL query or notebook preview. Confirm the preview is wide while regular text remains at the normal width. 2. Produce a response that reasons and runs consecutive non-preview tools. Confirm those rows remain close together with their separators. 3. Hover an Assistant response and confirm copy, rating, and branch actions align with the regular message content. 4. Hover a user message and confirm edit and delete actions use the same alignment. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **Style** - Improved AI Assistant message layout with centered, consistent content widths. - Expanded notebooks, SQL results, and query-related content where additional space is helpful. - Improved alignment and spacing for actions, tool outputs, loading states, errors, and disclaimers. - Improved query editor visibility when switching between cells. - Loading indicators now respect reduced-motion preferences. - **Tests** - Added coverage for message layouts, tool grouping, and notebook preview sizing. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Saxon Fletcher <SaxonF@users.noreply.github.com>
152 lines
5.1 KiB
TypeScript
152 lines
5.1 KiB
TypeScript
import { UIMessage as VercelMessage } from '@ai-sdk/react'
|
|
import { useState } from 'react'
|
|
import { toast } from 'sonner'
|
|
import { cn, copyToClipboard } from 'ui'
|
|
|
|
import { DeleteMessageConfirmModal } from './DeleteMessageConfirmModal'
|
|
import { MessageActions } from './Message.Actions'
|
|
import type { AddToolApprovalResponse, MessageInfo } from './Message.Context'
|
|
import { MessageProvider, useMessageActionsContext, useMessageInfoContext } from './Message.Context'
|
|
import { MessageDisplay } from './Message.Display'
|
|
|
|
function AssistantMessage({ message }: { message: VercelMessage }) {
|
|
const { onBranch, onCancelEdit, onRate } = useMessageActionsContext()
|
|
const { id, variant, state, isLastMessage, readOnly, rating, isLoading } = useMessageInfoContext()
|
|
|
|
const handleRate = (newRating: 'positive' | 'negative', reason?: string) => {
|
|
onRate?.(id, newRating, reason)
|
|
}
|
|
|
|
const handleCopy = (onSuccess: () => void) => {
|
|
const response = message.parts
|
|
.filter((x) => x.type === 'text')
|
|
.map((x) => x.text)
|
|
.join('\n')
|
|
copyToClipboard(response, onSuccess)
|
|
}
|
|
|
|
return (
|
|
<MessageDisplay.Container
|
|
className={cn(
|
|
variant === 'warning' && 'bg-warning-200',
|
|
state === 'predecessor-editing' && 'opacity-50 transition-opacity cursor-pointer'
|
|
)}
|
|
onClick={state === 'predecessor-editing' ? onCancelEdit : undefined}
|
|
>
|
|
<MessageDisplay.MainArea>
|
|
<MessageDisplay.Content message={message} />
|
|
</MessageDisplay.MainArea>
|
|
{!readOnly && onRate && !isLoading && (
|
|
<MessageActions alwaysShow={isLastMessage}>
|
|
<MessageActions.Copy onClick={handleCopy} />
|
|
<MessageActions.ThumbsUp
|
|
onClick={() => handleRate('positive')}
|
|
isActive={rating === 'positive'}
|
|
disabled={!!rating}
|
|
/>
|
|
<MessageActions.ThumbsDown
|
|
onClick={(reason) => handleRate('negative', reason)}
|
|
isActive={rating === 'negative'}
|
|
disabled={!!rating}
|
|
/>
|
|
<MessageActions.Branch onClick={() => onBranch(id)} />
|
|
</MessageActions>
|
|
)}
|
|
</MessageDisplay.Container>
|
|
)
|
|
}
|
|
|
|
function UserMessage({ message }: { message: VercelMessage }) {
|
|
const { id, variant, state } = useMessageInfoContext()
|
|
const { onCancelEdit, onEdit, onDelete } = useMessageActionsContext()
|
|
const [showDeleteConfirmModal, setShowDeleteConfirmModal] = useState(false)
|
|
|
|
return (
|
|
<>
|
|
<MessageDisplay.Container
|
|
className={cn(
|
|
'mt-6 text-foreground',
|
|
variant === 'warning' && 'bg-warning-200',
|
|
state === 'predecessor-editing' && 'opacity-50 transition-opacity cursor-pointer'
|
|
)}
|
|
onClick={state === 'predecessor-editing' ? onCancelEdit : undefined}
|
|
>
|
|
<MessageDisplay.MainArea className="w-full max-w-3xl mx-auto">
|
|
<MessageDisplay.ProfileImage />
|
|
<MessageDisplay.Content message={message} />
|
|
</MessageDisplay.MainArea>
|
|
<MessageActions>
|
|
<MessageActions.Edit
|
|
onClick={state === 'idle' ? () => onEdit(id) : onCancelEdit}
|
|
tooltip={state === 'idle' ? 'Edit message' : 'Cancel editing'}
|
|
/>
|
|
<MessageActions.Delete onClick={() => setShowDeleteConfirmModal(true)} />
|
|
</MessageActions>
|
|
</MessageDisplay.Container>
|
|
<DeleteMessageConfirmModal
|
|
visible={showDeleteConfirmModal}
|
|
onConfirm={() => {
|
|
onDelete(id)
|
|
setShowDeleteConfirmModal(false)
|
|
toast.success('Message deleted successfully')
|
|
}}
|
|
onCancel={() => setShowDeleteConfirmModal(false)}
|
|
/>
|
|
</>
|
|
)
|
|
}
|
|
|
|
interface MessageProps {
|
|
id: string
|
|
message: VercelMessage
|
|
isLoading: boolean
|
|
readOnly?: boolean
|
|
variant?: 'default' | 'warning'
|
|
addToolApprovalResponse?: AddToolApprovalResponse
|
|
onDelete: (id: string) => void
|
|
onEdit: (id: string) => void
|
|
onBranch: (id: string) => void
|
|
isAfterEditedMessage: boolean
|
|
isBeingEdited: boolean
|
|
onCancelEdit: () => void
|
|
isLastMessage?: boolean
|
|
onRate?: (id: string, rating: 'positive' | 'negative', reason?: string) => void
|
|
rating?: 'positive' | 'negative' | null
|
|
}
|
|
|
|
export function Message(props: MessageProps) {
|
|
const message = props.message
|
|
const { role } = message
|
|
const isUserMessage = role === 'user'
|
|
|
|
const messageInfo = {
|
|
id: props.id,
|
|
isLoading: props.isLoading,
|
|
readOnly: props.readOnly,
|
|
variant: props.variant,
|
|
isUserMessage,
|
|
state: props.isBeingEdited
|
|
? 'editing'
|
|
: props.isAfterEditedMessage
|
|
? 'predecessor-editing'
|
|
: 'idle',
|
|
isLastMessage: props.isLastMessage,
|
|
rating: props.rating,
|
|
} satisfies MessageInfo
|
|
|
|
const messageActions = {
|
|
addToolApprovalResponse: props.addToolApprovalResponse,
|
|
onDelete: props.onDelete,
|
|
onEdit: props.onEdit,
|
|
onBranch: props.onBranch,
|
|
onCancelEdit: props.onCancelEdit,
|
|
onRate: props.onRate,
|
|
}
|
|
|
|
return (
|
|
<MessageProvider messageInfo={messageInfo} messageActions={messageActions}>
|
|
{isUserMessage ? <UserMessage message={message} /> : <AssistantMessage message={message} />}
|
|
</MessageProvider>
|
|
)
|
|
}
|