AIOnboarding.tsx 6.8 KB

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