mirror of
https://github.com/supabase/supabase.git
synced 2026-10-19 08:09:11 +03:00
ui refinements
This commit is contained in:
1 parent
9765a70a73
commit
c8c20f195b
9 files changed
+83
-104
No files matched your search
@@ -1,7 +1,7 @@
|
||||
import type { Message as MessageType } from 'ai/react'
|
||||
import { useChat } from 'ai/react'
|
||||
import { AnimatePresence, motion } from 'framer-motion'
|
||||
import { ArrowDown, FileText, Info, RefreshCw, X } from 'lucide-react'
|
||||
import { ArrowDown, FileText, Info, RefreshCw, Settings, X } from 'lucide-react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
|
||||
@@ -312,56 +312,40 @@ export const AIAssistant = ({ className }: AIAssistantProps) => {
|
||||
<div className={cn('flex flex-col h-full', className)}>
|
||||
<div ref={scrollContainerRef} className={cn('flex-grow overflow-auto flex flex-col')}>
|
||||
<div className="z-30 sticky top-0">
|
||||
<div className="border-b flex items-center bg gap-x-3 pl-5 pr-4 h-[46px]">
|
||||
<AiIconAnimation allowHoverEffect />
|
||||
|
||||
<div className="border-b border-b-muted flex items-center bg gap-x-4 pl-3 pr-5 h-[46px]">
|
||||
<div className="text-sm flex-1 flex items-center gap-x-2">
|
||||
Assistant
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Info size={14} className="text-foreground-light" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="w-80">
|
||||
The Assistant is in Alpha and your prompts might be rate limited.{' '}
|
||||
{aiOptInLevel === 'schema_and_log_and_data' &&
|
||||
'Schema, logs, and query data are being shared to improve Assistant responses.'}
|
||||
{aiOptInLevel === 'schema_and_log' &&
|
||||
'Schema and logs are being shared to improve Assistant responses.'}
|
||||
{aiOptInLevel === 'schema' &&
|
||||
'Only schema metadata is being shared to improve Assistant responses.'}
|
||||
{aiOptInLevel === 'disabled' &&
|
||||
'Project metadata is not being shared. Opt in to improve Assistant responses.'}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<AIAssistantChatSelector disabled={isChatLoading} />
|
||||
</div>
|
||||
<div className="flex items-center gap-x-4">
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<p
|
||||
title={currentChat}
|
||||
className="text-xs text-foreground-light truncate max-w-[145px] 2xl:max-w-full"
|
||||
>
|
||||
{currentChat}
|
||||
</p>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">Current chat: {currentChat}</TooltipContent>
|
||||
</Tooltip>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<AIAssistantChatSelector disabled={isChatLoading} />
|
||||
<div className="flex items-center">
|
||||
<ButtonTooltip
|
||||
type="default"
|
||||
type="text"
|
||||
size="tiny"
|
||||
icon={<RefreshCw size={14} />}
|
||||
icon={<Settings strokeWidth={1.5} size={14} />}
|
||||
onClick={() => setIsConfirmOptInModalOpen(true)}
|
||||
className="h-7 w-7 p-0"
|
||||
disabled={isChatLoading}
|
||||
tooltip={{
|
||||
content: {
|
||||
side: 'bottom',
|
||||
text: 'Permission settings',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<ButtonTooltip
|
||||
type="text"
|
||||
size="tiny"
|
||||
icon={<RefreshCw strokeWidth={1.5} size={14} />}
|
||||
onClick={handleClearMessages}
|
||||
className="h-7 w-7 p-0"
|
||||
disabled={isChatLoading}
|
||||
tooltip={{ content: { side: 'bottom', text: 'Clear messages' } }}
|
||||
/>
|
||||
<ButtonTooltip
|
||||
type="default"
|
||||
type="text"
|
||||
className="w-7 h-7"
|
||||
onClick={snap.closeAssistant}
|
||||
icon={<X />}
|
||||
icon={<X strokeWidth={1.5} size={14} />}
|
||||
tooltip={{ content: { side: 'bottom', text: 'Close assistant' } }}
|
||||
/>
|
||||
</div>
|
||||
@@ -410,7 +394,7 @@ export const AIAssistant = ({ className }: AIAssistantProps) => {
|
||||
</div>
|
||||
)}
|
||||
{hasMessages ? (
|
||||
<div className="w-full p-5">
|
||||
<div className="w-full px-7 py-8 space-y-8">
|
||||
{renderedMessages}
|
||||
<AnimatePresence>
|
||||
{isChatLoading && (
|
||||
@@ -576,9 +560,9 @@ export const AIAssistant = ({ className }: AIAssistantProps) => {
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
<div className="p-5 pt-0 z-20 relative">
|
||||
<div className="px-3 pb-3 z-20 relative">
|
||||
{snap.sqlSnippets && snap.sqlSnippets.length > 0 && (
|
||||
<div className="mb-2">
|
||||
<div className="mb-0 mx-4">
|
||||
{snap.sqlSnippets.map((snippet: string, index: number) => (
|
||||
<CollapsibleCodeBlock
|
||||
key={index}
|
||||
@@ -589,7 +573,7 @@ export const AIAssistant = ({ className }: AIAssistantProps) => {
|
||||
newSnippets.splice(index, 1)
|
||||
snap.setSqlSnippets(newSnippets)
|
||||
}}
|
||||
className="text-xs"
|
||||
className="text-xs rounded-b-none"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Check, Edit, MessageSquare, Plus, Trash, X } from 'lucide-react'
|
||||
import { Check, ChevronDown, Edit, MessageSquare, Plus, Trash, X } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
|
||||
import { useAiAssistantStateSnapshot } from 'state/ai-assistant-state'
|
||||
@@ -30,6 +30,7 @@ export const AIAssistantChatSelector = ({
|
||||
disabled = false,
|
||||
}: AIAssistantChatSelectorProps) => {
|
||||
const snap = useAiAssistantStateSnapshot()
|
||||
const currentChat = snap.activeChat?.name
|
||||
|
||||
const [chatSelectorOpen, setChatSelectorOpen] = useState(false)
|
||||
const [editingChatId, setEditingChatId] = useState<string | null>(null)
|
||||
@@ -91,15 +92,11 @@ export const AIAssistantChatSelector = ({
|
||||
return (
|
||||
<Popover_Shadcn_ open={chatSelectorOpen} onOpenChange={setChatSelectorOpen}>
|
||||
<PopoverTrigger_Shadcn_ asChild>
|
||||
<ButtonTooltip
|
||||
type="default"
|
||||
size="tiny"
|
||||
icon={<MessageSquare size={14} />}
|
||||
className={cn('h-7 w-7', className)}
|
||||
tooltip={{ content: { side: 'bottom', text: 'View chats' } }}
|
||||
/>
|
||||
<Button type="text" size="tiny" iconRight={<ChevronDown size={14} />}>
|
||||
{currentChat}
|
||||
</Button>
|
||||
</PopoverTrigger_Shadcn_>
|
||||
<PopoverContent_Shadcn_ className="w-[250px] p-0" align="end">
|
||||
<PopoverContent_Shadcn_ className="w-[250px] p-0" align="start">
|
||||
<Command_Shadcn_>
|
||||
<CommandInput_Shadcn_ placeholder="Search chats..." />
|
||||
<CommandList_Shadcn_>
|
||||
|
||||
@@ -1,49 +1,47 @@
|
||||
import { ChevronDown, ChevronUp, X } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
import { Code, X } from 'lucide-react'
|
||||
|
||||
import { Button, CodeBlock, CodeBlockProps, cn } from 'ui'
|
||||
import {
|
||||
Button,
|
||||
CodeBlock,
|
||||
CodeBlockProps,
|
||||
cn,
|
||||
HoverCard_Shadcn_,
|
||||
HoverCardContent_Shadcn_,
|
||||
HoverCardTrigger_Shadcn_,
|
||||
} from 'ui'
|
||||
|
||||
interface CollapsibleCodeBlockProps extends CodeBlockProps {
|
||||
onRemove?: () => void
|
||||
}
|
||||
|
||||
export const CollapsibleCodeBlock = ({ onRemove, ...props }: CollapsibleCodeBlockProps) => {
|
||||
const [isExpanded, setIsExpanded] = useState(false)
|
||||
|
||||
const codeString = (props.value || props.children) as string
|
||||
const firstLine = isExpanded
|
||||
? codeString
|
||||
: codeString?.substring(0, codeString.indexOf('\n')) || codeString
|
||||
const firstLine = codeString?.substring(0, codeString.indexOf('\n')) || codeString
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center gap-1 p-1 bg-surface-100 border border-default w-full overflow-hidden',
|
||||
'rounded-md'
|
||||
'flex items-center gap-1 p-2 bg-surface-100 border border-default w-full overflow-hidden',
|
||||
'rounded-md',
|
||||
props.className
|
||||
)}
|
||||
>
|
||||
<Button
|
||||
type="text"
|
||||
size="tiny"
|
||||
className="w-6 h-6"
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
icon={isExpanded ? <ChevronUp size={14} /> : <ChevronDown size={14} />}
|
||||
/>
|
||||
<div className="flex-1 shrink-1 overflow-auto [&::-webkit-scrollbar]:hidden [-ms-overflow-style:none] [scrollbar-width:none]">
|
||||
<CodeBlock
|
||||
{...props}
|
||||
value={firstLine}
|
||||
hideCopy
|
||||
className={cn(
|
||||
'block !bg-transparent max-h-32 max-w-full !py-0 !px-0 !border-t-0 prose dark:prose-dark border-0 text-foreground !rounded-none w-full text-wrap whitespace-pre-wrap',
|
||||
// change the look of the code block. The flex hack is so that the code is wrapping since
|
||||
// every word is a separate span
|
||||
'[&>code]:m-0 [&>code>span]:flex border-t-0 [&>code>span]:flex-wrap [&>code]:block [&>code>span]:text-foreground text-wrap whitespace-pre-wrap',
|
||||
props.className
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<HoverCard_Shadcn_>
|
||||
<HoverCardTrigger_Shadcn_ asChild>
|
||||
<div className="flex flex-1 items-center gap-2 text-foreground-light px-2 hover:text-foreground cursor-pointer">
|
||||
<Code size={14} strokeWidth={1.5} />
|
||||
<span className="text-xs font-mono flex-1 truncate pointer">{firstLine}...</span>
|
||||
</div>
|
||||
</HoverCardTrigger_Shadcn_>
|
||||
<HoverCardContent_Shadcn_ className="w-96 max-h-96 overflow-auto p-0">
|
||||
<CodeBlock
|
||||
{...props}
|
||||
value={codeString}
|
||||
className={cn('text-xs font-mono border-none p-3')}
|
||||
/>
|
||||
</HoverCardContent_Shadcn_>
|
||||
</HoverCard_Shadcn_>
|
||||
|
||||
{onRemove && (
|
||||
<Button
|
||||
|
||||
@@ -105,7 +105,7 @@ export const DisplayBlockRenderer = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-auto overflow-x-hidden">
|
||||
<div className="display-block w-auto overflow-x-hidden !my-6">
|
||||
<QueryBlock
|
||||
label={label}
|
||||
sql={sqlQuery}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { Message as VercelMessage } from 'ai/react'
|
||||
import { User } from 'lucide-react'
|
||||
import { Loader2, User } from 'lucide-react'
|
||||
import { createContext, PropsWithChildren, ReactNode, useMemo } from 'react'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import { Components } from 'react-markdown/lib/ast-to-react'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
|
||||
import { ProfileImage } from 'components/ui/ProfileImage'
|
||||
import { useProfile } from 'lib/profile'
|
||||
import { cn, markdownComponents, WarningIcon } from 'ui'
|
||||
import { EdgeFunctionBlock } from '../EdgeFunctionBlock/EdgeFunctionBlock'
|
||||
import { DisplayBlockRenderer } from './DisplayBlockRenderer'
|
||||
@@ -59,6 +61,7 @@ export const Message = function Message({
|
||||
variant = 'default',
|
||||
onResults,
|
||||
}: PropsWithChildren<MessageProps>) {
|
||||
const { profile } = useProfile()
|
||||
const allMarkdownComponents: Partial<Components> = useMemo(
|
||||
() => ({
|
||||
...markdownComponents,
|
||||
@@ -88,7 +91,7 @@ export const Message = function Message({
|
||||
<MessageContext.Provider value={{ isLoading, readOnly }}>
|
||||
<div
|
||||
className={cn(
|
||||
'mb-4 text-foreground-light text-sm',
|
||||
'text-foreground-light text-sm',
|
||||
isUser && 'text-foreground',
|
||||
variant === 'warning' && 'bg-warning-200'
|
||||
)}
|
||||
@@ -99,9 +102,11 @@ export const Message = function Message({
|
||||
|
||||
<div className="flex gap-4 w-auto overflow-hidden">
|
||||
{isUser && (
|
||||
<figure className="w-5 h-5 shrink-0 bg-foreground rounded-full flex items-center justify-center">
|
||||
<User size={16} strokeWidth={1.5} className="text-background" />
|
||||
</figure>
|
||||
<ProfileImage
|
||||
alt={profile?.username}
|
||||
src={profile?.profileImageUrl}
|
||||
className="w-5 h-5 shrink-0 rounded-full"
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="flex-1 min-w-0 space-y-2">
|
||||
@@ -116,7 +121,7 @@ export const Message = function Message({
|
||||
<ReactMarkdown
|
||||
key={`${id}-part-${index}`}
|
||||
className={cn(
|
||||
'prose prose-sm max-w-full [&_h3]:text-base [&_ol>li]:pl-4 [&_ol>li]:my-0 [&_li>p]:mt-0 space-y-5 [&>*>code]:text-xs [&>*>*>code]:text-xs [&_li]:space-y-4',
|
||||
'prose prose-sm break-words [&>p:not(:last-child)]:!mb-2',
|
||||
isUser && 'text-foreground font-semibold'
|
||||
)}
|
||||
remarkPlugins={[remarkGfm]}
|
||||
@@ -130,14 +135,13 @@ export const Message = function Message({
|
||||
const { toolCallId, toolName, args, state } = part.toolInvocation
|
||||
if (state === 'call' || state === 'partial-call') {
|
||||
if (shownLoadingTools.has(toolName)) {
|
||||
// Already shown loading for this toolName in this step
|
||||
return null
|
||||
}
|
||||
shownLoadingTools.add(toolName)
|
||||
return (
|
||||
<div
|
||||
key={`${id}-tool-loading-${toolName}`}
|
||||
className="rounded border text-xs font-mono text-xs text-foreground-lighter py-2 px-3"
|
||||
className="rounded-lg border bg-surface-75 text-xs font-mono text-xs text-foreground-lighter py-2 px-3 flex items-center gap-2"
|
||||
>
|
||||
{`Calling ${toolName}...`}
|
||||
</div>
|
||||
@@ -191,7 +195,7 @@ export const Message = function Message({
|
||||
})()
|
||||
) : hasTextContent ? (
|
||||
<ReactMarkdown
|
||||
className="prose prose-sm [&_>h3]:text-base [&_ol>li]:pl-4 [&_ol>li]:my-0 space-y-5 flex-1 [&>*>code]:text-xs [&>*>*>code]:text-xs min-w-0 [&_li]:space-y-4"
|
||||
className="prose prose-sm max-w-none break-words"
|
||||
remarkPlugins={[remarkGfm]}
|
||||
components={allMarkdownComponents}
|
||||
>
|
||||
|
||||
@@ -43,7 +43,7 @@ export const BlockViewConfiguration = ({
|
||||
id="help-popover-button"
|
||||
type="text"
|
||||
className="px-1"
|
||||
icon={<Settings2 size={14} />}
|
||||
icon={<Settings2 size={14} strokeWidth={1.5} />}
|
||||
tooltip={{ content: { side: 'bottom', text: 'View data' } }}
|
||||
/>
|
||||
</PopoverTrigger_Shadcn_>
|
||||
|
||||
@@ -62,7 +62,7 @@ export const EditQueryButton = ({
|
||||
type={type}
|
||||
size="tiny"
|
||||
className={cn('w-7 h-7', className)}
|
||||
icon={<Edit size={14} />}
|
||||
icon={<Edit size={14} strokeWidth={1.5} />}
|
||||
tooltip={tooltip}
|
||||
>
|
||||
<Link href={`/project/${ref}/sql/${id}`} />
|
||||
@@ -75,7 +75,7 @@ export const EditQueryButton = ({
|
||||
type={type}
|
||||
size="tiny"
|
||||
className={cn('w-7 h-7', className)}
|
||||
icon={<Edit size={14} />}
|
||||
icon={<Edit size={14} strokeWidth={1.5} />}
|
||||
onClick={() => {
|
||||
if (isInlineEditorEnabled) {
|
||||
setEditorPanel({
|
||||
@@ -105,7 +105,7 @@ export const EditQueryButton = ({
|
||||
size="tiny"
|
||||
disabled={!sql}
|
||||
className={cn('w-7 h-7', className)}
|
||||
icon={<Edit size={14} />}
|
||||
icon={<Edit size={14} strokeWidth={1.5} />}
|
||||
tooltip={!!sql ? tooltip : { content: { side: 'bottom', text: undefined } }}
|
||||
/>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
@@ -258,7 +258,7 @@ export const QueryBlock = ({
|
||||
type="text"
|
||||
size="tiny"
|
||||
className="w-7 h-7"
|
||||
icon={<Code size={14} />}
|
||||
icon={<Code size={14} strokeWidth={1.5} />}
|
||||
onClick={() => setShowSql(!showSql)}
|
||||
tooltip={{
|
||||
content: { side: 'bottom', text: showSql ? 'Hide query' : 'Show query' },
|
||||
@@ -302,7 +302,7 @@ export const QueryBlock = ({
|
||||
type="text"
|
||||
size="tiny"
|
||||
className="w-7 h-7"
|
||||
icon={<Play size={14} />}
|
||||
icon={<Play size={14} strokeWidth={1.5} />}
|
||||
loading={isExecuting || isLoading}
|
||||
disabled={isLoading}
|
||||
onClick={() => {
|
||||
|
||||
@@ -75,18 +75,14 @@ const AssistantChatFormComponent = React.forwardRef<HTMLFormElement, FormProps>(
|
||||
onSubmit={onSubmit}
|
||||
className={cn('relative', props.className)}
|
||||
>
|
||||
{icon && (
|
||||
<div className={cn('absolute', 'top-2 left-2', 'ml-1 w-6 h-6 rounded-full bg-dbnew')}>
|
||||
{icon}
|
||||
</div>
|
||||
)}
|
||||
<ExpandingTextArea
|
||||
ref={textAreaRef}
|
||||
autoFocus={isMobile}
|
||||
disabled={disabled}
|
||||
className={cn(icon ? 'pl-12' : '', 'text-sm pr-10 rounded-[18px]')}
|
||||
className={cn(icon ? 'pl-12' : '', 'text-sm pr-10 rounded')}
|
||||
placeholder={placeholder}
|
||||
spellCheck={false}
|
||||
rows={3}
|
||||
value={value}
|
||||
onChange={(event) => onValueChange(event)}
|
||||
onKeyDown={handleKeyDown}
|
||||
|
||||
Reference in new issue
Block a user