Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
The diff you're trying to view is too large. We only load the first 3000 changed files.
439 changes: 110 additions & 329 deletions apps/studio/components/ui/AIAssistantPanel/AIAssistant.tsx

Large diffs are not rendered by default.

119 changes: 119 additions & 0 deletions apps/studio/components/ui/AIAssistantPanel/AIAssistantHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { Eraser, Settings, X } from 'lucide-react'
import { useState } from 'react'

import { AiIconAnimation, Button } from 'ui'
import { Admonition } from 'ui-patterns'
import { ButtonTooltip } from '../ButtonTooltip'
import { AIAssistantChatSelector } from './AIAssistantChatSelector'
import { AIOptInModal } from './AIOptInModal'

interface AIAssistantHeaderProps {
isChatLoading: boolean
onClearMessages: () => void
onCloseAssistant: () => void
showMetadataWarning: boolean
updatedOptInSinceMCP: boolean
isHipaaProjectDisallowed: boolean
aiOptInLevel: 'disabled' | 'schema' | 'full' | string | undefined
}

export const AIAssistantHeader = ({
isChatLoading,
onClearMessages,
onCloseAssistant,
showMetadataWarning,
updatedOptInSinceMCP,
isHipaaProjectDisallowed,
aiOptInLevel,
}: AIAssistantHeaderProps) => {
const [isOptInModalOpen, setIsOptInModalOpen] = useState(false)
return (
<div className="z-30 sticky top-0">
<div className="border-b border-b-muted flex items-center bg gap-x-4 px-3 h-[46px]">
<div className="text-sm flex-1 flex items-center">
<AiIconAnimation size={20} allowHoverEffect={false} />
<span className="text-border-stronger dark:text-border-strong ml-3">
<svg
viewBox="0 0 24 24"
width="16"
height="16"
stroke="currentColor"
strokeWidth="1"
strokeLinecap="round"
strokeLinejoin="round"
fill="none"
shapeRendering="geometricPrecision"
>
<path d="M16 3.549L7.12 20.600" />
</svg>
</span>
<AIAssistantChatSelector disabled={isChatLoading} />
</div>
<div className="flex items-center gap-x-4">
<div className="flex items-center">
<ButtonTooltip
type="text"
size="tiny"
icon={<Settings strokeWidth={1.5} />}
onClick={() => setIsOptInModalOpen(true)}
className="h-7 w-7 p-0"
disabled={isChatLoading}
tooltip={{
content: { side: 'bottom', text: 'Permission settings' },
}}
/>
<ButtonTooltip
type="text"
size="tiny"
icon={<Eraser strokeWidth={1.5} />}
onClick={onClearMessages}
className="h-7 w-7 p-0"
disabled={isChatLoading}
tooltip={{ content: { side: 'bottom', text: 'Clear messages' } }}
/>
<ButtonTooltip
type="text"
className="w-7 h-7"
onClick={onCloseAssistant}
icon={<X strokeWidth={1.5} />}
tooltip={{ content: { side: 'bottom', text: 'Close assistant' } }}
/>
</div>
</div>
</div>
{showMetadataWarning && (
<Admonition
type="default"
title={
!updatedOptInSinceMCP
? 'The Assistant has just been updated to help you better!'
: isHipaaProjectDisallowed
? 'Project metadata is not shared due to HIPAA'
: aiOptInLevel === 'disabled'
? 'Project metadata is currently not shared'
: 'Limited metadata is shared to the Assistant'
}
description={
!updatedOptInSinceMCP
? 'You may now opt-in to share schema metadata and even logs for better results'
: isHipaaProjectDisallowed
? 'Your organization has the HIPAA addon and will not send project metadata with your prompts for projects marked as HIPAA.'
: aiOptInLevel === 'disabled'
? 'The Assistant can provide better answers if you opt-in to share schema metadata.'
: aiOptInLevel === 'schema'
? 'Sharing query data in addition to schema can further improve responses. Update AI settings to enable this.'
: ''
}
className="border-0 border-b rounded-none bg-background mb-0"
>
{!isHipaaProjectDisallowed && (
<Button type="default" className="w-fit mt-4" onClick={() => setIsOptInModalOpen(true)}>
Permission settings
</Button>
)}
</Admonition>
)}
<AIOptInModal visible={isOptInModalOpen} onCancel={() => setIsOptInModalOpen(false)} />
</div>
)
}
100 changes: 32 additions & 68 deletions apps/studio/components/ui/AIAssistantPanel/AIOnboarding.tsx
Original file line number Diff line number Diff line change
@@ -1,34 +1,26 @@
import { motion } from 'framer-motion'
import { FileText } from 'lucide-react'
import { useRef } from 'react'

import { Button, cn } from 'ui'
import { AssistantChatForm } from './AssistantChatForm'
import { Button } from 'ui'
import { type SqlSnippet } from './AIAssistant.types'
import { codeSnippetPrompts, defaultPrompts } from './AIAssistant.prompts'

interface AIOnboardingProps {
onMessageSend: (message: string) => void
sqlSnippets?: SqlSnippet[]
onRemoveSnippet?: (index: number) => void
suggestions?: {
title?: string
prompts?: { label: string; description: string }[]
}
value: string
onValueChange: (value: string) => void
onFocusInput?: () => void
}

export const AIOnboarding = ({
onMessageSend,
sqlSnippets,
onRemoveSnippet,
suggestions,
value,
onValueChange,
onFocusInput,
}: AIOnboardingProps) => {
const inputRef = useRef<HTMLTextAreaElement>(null)

const prompts = suggestions?.prompts
? suggestions.prompts.map((suggestion) => ({
title: suggestion.label,
Expand All @@ -40,64 +32,36 @@ export const AIOnboarding = ({
: defaultPrompts

return (
<div className="w-full h-full flex flex-col">
{/* Header Section */}
<div className="flex-1 flex flex-col items-center justify-center p-6">
<motion.div
initial={{ y: 10, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ duration: 0.5 }}
className="text-center max-w-xl w-full overflow-hiddenflex flex-col items-center"
>
<h2 className="text-2xl mb-6">How can I assist you?</h2>

<div className="w-full mb-6">
<AssistantChatForm
textAreaRef={inputRef}
className={cn(
'z-20 [&>form>textarea]:text-base [&>form>textarea]:md:text-sm [&>form>textarea]:border-1 [&>form>textarea]:rounded-md [&>form>textarea]:!outline-none [&>form>textarea]:!ring-offset-0 [&>form>textarea]:!ring-0'
)}
loading={false}
disabled={false}
placeholder="Ask me anything..."
value={value}
onValueChange={(e) => onValueChange(e.target.value)}
onSubmit={(finalMessage) => {
if (finalMessage.trim()) {
onMessageSend(finalMessage)
onValueChange('')
}
<div className="w-full mb-6">
<div className="px-4 mb-4">
<h2 className="heading-section text-foreground mb-1">How can I assist you?</h2>
<p className="text-foreground-light text-sm">
Generate SQL, RLS policies and edge functions, debug issues or check on your project
health.
</p>
</div>
<div>
{prompts.map((item, index) => (
<motion.div
key={index}
initial={{ y: 5, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ duration: 0.3, delay: index * 0.05 }}
>
<Button
size="small"
type="text"
className="w-full justify-start"
icon={<FileText strokeWidth={1.5} size={14} className="text-foreground-light" />}
onClick={() => {
onValueChange(item.prompt)
onFocusInput?.()
}}
sqlSnippets={sqlSnippets}
onRemoveSnippet={onRemoveSnippet}
snippetsClassName="text-left"
includeSnippetsInMessage={true}
/>
</div>

<div className="flex flex-wrap items-center justify-center gap-2">
{prompts.map((item, index) => (
<motion.div
key={index}
initial={{ y: 5, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ duration: 0.3, delay: index * 0.1 }}
>
<Button
size="small"
type="outline"
className="text-xs rounded-full !h-auto py-1 px-2 text-foreground-light"
onClick={() => {
onValueChange(item.prompt)
inputRef.current?.focus()
}}
>
{item.title}
</Button>
</motion.div>
))}
</div>
</motion.div>
>
{item.title}
</Button>
</motion.div>
))}
</div>
</div>
)
Expand Down
Loading
Loading