AIOnboarding.tsx177 lines · main
1import { useParams } from 'common'
2import { motion } from 'framer-motion'
3import { BarChart, FileText, Shield } from 'lucide-react'
4import { Button, Skeleton } from 'ui'
5
6import { codeSnippetPrompts, defaultPrompts } from './AIAssistant.prompts'
7import type { SqlSnippet } from './AIAssistant.types'
8import { LINTER_LEVELS } from '@/components/interfaces/Linter/Linter.constants'
9import { createLintSummaryPrompt } from '@/components/interfaces/Linter/Linter.utils'
10import { useProjectLintsQuery, type Lint } from '@/data/lint/lint-query'
11
12interface AIOnboardingProps {
13 sqlSnippets?: SqlSnippet[]
14 suggestions?: {
15 title?: string
16 prompts?: { label: string; description: string }[]
17 }
18 onValueChange: (value: string) => void
19 onFocusInput?: () => void
20}
21
22export const AIOnboarding = ({
23 sqlSnippets,
24 suggestions,
25 onValueChange,
26 onFocusInput,
27}: AIOnboardingProps) => {
28 const prompts = suggestions?.prompts
29 ? suggestions.prompts.map((suggestion) => ({
30 title: suggestion.label,
31 prompt: suggestion.description,
32 icon: <FileText strokeWidth={1.25} size={14} className="w-4! h-4!" />,
33 }))
34 : sqlSnippets && sqlSnippets.length > 0
35 ? codeSnippetPrompts
36 : defaultPrompts
37
38 const { ref: projectRef } = useParams()
39 const {
40 data: lints,
41 isPending: isLoadingLints,
42 isFetching: isFetchingLints,
43 } = useProjectLintsQuery({ projectRef })
44 const isLintsLoading = isLoadingLints || isFetchingLints
45
46 const errorLints: Lint[] = (lints?.filter((lint) => lint.level === LINTER_LEVELS.ERROR) ??
47 []) as Lint[]
48 const securityErrorLints = errorLints.filter((lint) => lint.categories?.[0] === 'SECURITY')
49 const performanceErrorLints = errorLints.filter((lint) => lint.categories?.[0] !== 'SECURITY')
50
51 return (
52 <div className="flex-1 overflow-y-auto">
53 <div className="w-full flex-1 max-h-full min-h-full px-4 flex flex-col gap-0">
54 <div className="mt-auto w-full space-y-6 py-8 ">
55 <h2 className="heading-section text-foreground mx-4">How can I assist you?</h2>
56 {suggestions?.prompts?.length ? (
57 <div>
58 <h3 className="heading-meta text-foreground-light mb-3 mx-4">Suggestions</h3>
59 {prompts.map((item, index) => (
60 <motion.div
61 key={item.title}
62 initial={{ y: 5, opacity: 0 }}
63 animate={{ y: 0, opacity: 1 }}
64 transition={{ duration: 0.3, delay: index * 0.05 }}
65 >
66 <Button
67 size="small"
68 type="text"
69 className="w-full justify-start border-b hover:border-b-0 hover:rounded-md rounded-none"
70 icon={
71 <FileText strokeWidth={1.5} size={14} className="text-foreground-light" />
72 }
73 onClick={() => {
74 onValueChange(item.prompt)
75 onFocusInput?.()
76 }}
77 >
78 {item.title}
79 </Button>
80 </motion.div>
81 ))}
82 </div>
83 ) : (
84 <>
85 {isLintsLoading ? (
86 <div className="px-4 flex flex-col gap-2">
87 {Array.from({ length: 6 }).map((_, index) => (
88 <Skeleton key={`loader-${index}`} className="h-4 w-full" />
89 ))}
90 </div>
91 ) : (
92 <>
93 {performanceErrorLints.length > 0 && (
94 <div className="mb-4">
95 <h3 className="heading-meta text-foreground-light mb-3 mx-4">
96 Improve Performance
97 </h3>
98 {performanceErrorLints.map((lint, index) => {
99 return (
100 <Button
101 key={`${lint.name}-${index}`}
102 size="small"
103 type="text"
104 className="w-full justify-start"
105 icon={
106 <BarChart
107 strokeWidth={1.5}
108 size={14}
109 className="text-foreground-light"
110 />
111 }
112 onClick={() => {
113 onValueChange(createLintSummaryPrompt(lint))
114 onFocusInput?.()
115 }}
116 >
117 {lint.detail ? lint.detail.replace('\\`', '') : lint.title}
118 </Button>
119 )
120 })}
121 </div>
122 )}
123
124 {securityErrorLints.length > 0 && (
125 <div className="mb-4">
126 <h3 className="heading-meta text-foreground-light mb-3 mx-4">
127 Improve Security
128 </h3>
129 {securityErrorLints.map((lint, index) => {
130 return (
131 <Button
132 key={`${lint.name}-${index}`}
133 size="small"
134 type="text"
135 className="w-full justify-start"
136 icon={<Shield strokeWidth={1.5} size={14} className="text-warning" />}
137 onClick={() => {
138 onValueChange(createLintSummaryPrompt(lint))
139 onFocusInput?.()
140 }}
141 >
142 {lint.detail ? lint.detail.replace(/\\`/g, '') : lint.title}
143 </Button>
144 )
145 })}
146 </div>
147 )}
148
149 <div>
150 <h3 className="heading-meta text-foreground-light mb-3 mx-4">Ideas</h3>
151 {prompts.map((item, index) => (
152 <Button
153 key={`${item.title}-${index}`}
154 size="small"
155 type="text"
156 className="w-full justify-start"
157 icon={
158 <FileText strokeWidth={1.5} size={14} className="text-foreground-light" />
159 }
160 onClick={() => {
161 onValueChange(item.prompt)
162 onFocusInput?.()
163 }}
164 >
165 {item.title}
166 </Button>
167 ))}
168 </div>
169 </>
170 )}
171 </>
172 )}
173 </div>
174 </div>
175 </div>
176 )
177}