ui refinements

This commit is contained in:
Saxon Fletcher committed 2025-07-01 22:02:07 +10:00
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}