mirror of
https://github.com/supabase/supabase.git
synced 2026-10-07 02:15:05 +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>
250 lines
6.9 KiB
TypeScript
250 lines
6.9 KiB
TypeScript
import { zodResolver } from '@hookform/resolvers/zod'
|
|
import { Check, Copy, Pencil, Split, ThumbsDown, ThumbsUp, Trash2 } from 'lucide-react'
|
|
import { useEffect, useState, type PropsWithChildren } from 'react'
|
|
import { useForm } from 'react-hook-form'
|
|
import {
|
|
Button,
|
|
cn,
|
|
Form,
|
|
FormControl,
|
|
FormField,
|
|
Popover,
|
|
PopoverContent,
|
|
PopoverTrigger,
|
|
TextArea,
|
|
} from 'ui'
|
|
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
|
|
import * as z from 'zod'
|
|
|
|
import { ButtonTooltip } from '../ButtonTooltip'
|
|
|
|
export function MessageActions({
|
|
children,
|
|
alwaysShow = false,
|
|
}: PropsWithChildren<{ alwaysShow?: boolean }>) {
|
|
return (
|
|
<div className="w-full max-w-3xl mx-auto flex items-center gap-4 mt-2 mb-1">
|
|
<span className="h-0.5 w-5 bg-muted" />
|
|
<div className={cn('group-hover:opacity-100 transition-opacity', !alwaysShow && 'opacity-0')}>
|
|
{children}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
function MessageActionsEdit({ onClick, tooltip }: { onClick: () => void; tooltip: string }) {
|
|
return (
|
|
<ButtonTooltip
|
|
variant="text"
|
|
icon={<Pencil size={14} strokeWidth={1.5} />}
|
|
onClick={onClick}
|
|
className="text-foreground-light hover:text-foreground p-1 rounded-sm"
|
|
aria-label={tooltip}
|
|
tooltip={{
|
|
content: {
|
|
side: 'bottom',
|
|
text: tooltip,
|
|
},
|
|
}}
|
|
/>
|
|
)
|
|
}
|
|
MessageActions.Edit = MessageActionsEdit
|
|
|
|
function MessageActionsDelete({ onClick }: { onClick: () => void }) {
|
|
return (
|
|
<ButtonTooltip
|
|
variant="text"
|
|
icon={<Trash2 size={14} strokeWidth={1.5} />}
|
|
tooltip={{ content: { side: 'bottom', text: 'Delete message' } }}
|
|
onClick={onClick}
|
|
className="text-foreground-light hover:text-foreground p-1 rounded-sm"
|
|
title="Delete message"
|
|
aria-label="Delete message"
|
|
/>
|
|
)
|
|
}
|
|
MessageActions.Delete = MessageActionsDelete
|
|
|
|
function MessageActionsBranch({ onClick }: { onClick: () => void }) {
|
|
return (
|
|
<ButtonTooltip
|
|
variant="text"
|
|
icon={<Split size={14} strokeWidth={1.5} />}
|
|
onClick={onClick}
|
|
className="text-foreground-light hover:text-foreground p-1 rounded-sm"
|
|
title="Branch in new chat"
|
|
aria-label="Branch in new chat"
|
|
tooltip={{ content: { side: 'bottom', text: 'Branch in new chat' } }}
|
|
/>
|
|
)
|
|
}
|
|
MessageActions.Branch = MessageActionsBranch
|
|
|
|
function MessageActionsCopy({ onClick }: { onClick: (onSuccess: () => void) => void }) {
|
|
const [copied, setCopied] = useState(false)
|
|
|
|
useEffect(() => {
|
|
if (copied) setTimeout(() => setCopied(false), 1000)
|
|
}, [copied])
|
|
|
|
return (
|
|
<ButtonTooltip
|
|
variant="text"
|
|
icon={copied ? <Check size={14} strokeWidth={2} /> : <Copy size={14} strokeWidth={1.5} />}
|
|
onClick={() => onClick(() => setCopied(true))}
|
|
className="text-foreground-light hover:text-foreground p-1 rounded-sm"
|
|
title="Copy response"
|
|
aria-label="Copy response"
|
|
tooltip={{ content: { side: 'bottom', text: 'Copy response' } }}
|
|
/>
|
|
)
|
|
}
|
|
MessageActions.Copy = MessageActionsCopy
|
|
|
|
function MessageActionsThumbsUp({
|
|
onClick,
|
|
isActive,
|
|
disabled,
|
|
}: {
|
|
onClick: () => void
|
|
isActive?: boolean
|
|
disabled?: boolean
|
|
}) {
|
|
return (
|
|
<Button
|
|
variant="text"
|
|
disabled={disabled}
|
|
icon={
|
|
<ThumbsUp
|
|
size={14}
|
|
strokeWidth={1.5}
|
|
className={cn(
|
|
isActive
|
|
? 'text-brand hover:text-brand-700'
|
|
: 'text-foreground-light hover:text-foreground'
|
|
)}
|
|
/>
|
|
}
|
|
onClick={onClick}
|
|
className={cn(
|
|
'p-1 rounded-sm transition-colors',
|
|
disabled && 'opacity-50 pointer-events-none'
|
|
)}
|
|
title="Good response"
|
|
aria-label="Good response"
|
|
/>
|
|
)
|
|
}
|
|
MessageActions.ThumbsUp = MessageActionsThumbsUp
|
|
|
|
const feedbackSchema = z.object({
|
|
reason: z.string().optional(),
|
|
})
|
|
|
|
type FeedbackFormValues = z.infer<typeof feedbackSchema>
|
|
|
|
function MessageActionsThumbsDown({
|
|
onClick,
|
|
isActive,
|
|
disabled,
|
|
}: {
|
|
onClick: (reason?: string) => void
|
|
isActive?: boolean
|
|
disabled?: boolean
|
|
}) {
|
|
const [open, setOpen] = useState(false)
|
|
|
|
const form = useForm<FeedbackFormValues>({
|
|
resolver: zodResolver(feedbackSchema),
|
|
defaultValues: { reason: '' },
|
|
mode: 'onSubmit',
|
|
})
|
|
|
|
const handleOpenChange = (newOpen: boolean) => {
|
|
if (disabled) return
|
|
// When popover closes, submit the rating if not already submitted
|
|
if (!newOpen && open && !form.formState.isSubmitSuccessful) {
|
|
onClick()
|
|
}
|
|
setOpen(newOpen)
|
|
if (!newOpen) {
|
|
form.reset()
|
|
}
|
|
}
|
|
|
|
const onSubmit = (values: FeedbackFormValues) => {
|
|
onClick(values.reason || undefined)
|
|
}
|
|
|
|
// Auto-close popover after showing thank you message
|
|
useEffect(() => {
|
|
if (form.formState.isSubmitSuccessful) {
|
|
const timer = setTimeout(() => {
|
|
setOpen(false)
|
|
}, 2000)
|
|
return () => clearTimeout(timer)
|
|
}
|
|
}, [form.formState.isSubmitSuccessful])
|
|
|
|
return (
|
|
<Popover open={open} onOpenChange={handleOpenChange}>
|
|
<PopoverTrigger asChild>
|
|
<Button
|
|
variant="text"
|
|
disabled={disabled}
|
|
onClick={() => !disabled && setOpen(true)}
|
|
className={cn(
|
|
'p-1 rounded-sm transition-colors',
|
|
disabled && 'opacity-50 pointer-events-none'
|
|
)}
|
|
title="Bad response"
|
|
aria-label="Bad response"
|
|
>
|
|
<ThumbsDown
|
|
size={14}
|
|
strokeWidth={1.5}
|
|
className={cn(
|
|
isActive
|
|
? 'text-warning hover:text-warning-700'
|
|
: 'text-foreground-light hover:text-foreground'
|
|
)}
|
|
/>
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent className="w-80" align="start">
|
|
{form.formState.isSubmitSuccessful ? (
|
|
<p className="text-sm">We appreciate your feedback!</p>
|
|
) : (
|
|
<Form {...form}>
|
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-3">
|
|
<FormField
|
|
control={form.control}
|
|
name="reason"
|
|
render={({ field }) => (
|
|
<FormItemLayout label="What went wrong?" labelOptional="optional">
|
|
<FormControl>
|
|
<TextArea
|
|
placeholder="Describe why the response was not helpful..."
|
|
autoComplete="off"
|
|
rows={4}
|
|
autoFocus
|
|
{...field}
|
|
/>
|
|
</FormControl>
|
|
</FormItemLayout>
|
|
)}
|
|
/>
|
|
<div className="flex justify-end">
|
|
<Button variant="primary" type="submit" size="tiny">
|
|
Submit feedback
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
</Form>
|
|
)}
|
|
</PopoverContent>
|
|
</Popover>
|
|
)
|
|
}
|
|
MessageActions.ThumbsDown = MessageActionsThumbsDown
|