SaveSnippetDialog.tsx 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124
  1. import { useParams } from 'common'
  2. import { useEffect, useState } from 'react'
  3. import { toast } from 'sonner'
  4. import {
  5. AiIconAnimation,
  6. Button,
  7. Dialog,
  8. DialogContent,
  9. DialogFooter,
  10. DialogHeader,
  11. DialogSection,
  12. DialogSectionSeparator,
  13. DialogTitle,
  14. Input,
  15. Label,
  16. } from 'ui'
  17. import { subscriptionHasHipaaAddon } from '@/components/interfaces/Billing/Subscription/Subscription.utils'
  18. import { generateSnippetTitle } from '@/components/interfaces/SQLEditor/SQLEditor.constants'
  19. import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
  20. import { useCheckOpenAIKeyQuery } from '@/data/ai/check-api-key-query'
  21. import { useSqlTitleGenerateMutation } from '@/data/ai/sql-title-mutation'
  22. import { useProjectSettingsV2Query } from '@/data/config/project-settings-v2-query'
  23. import { useOrgSubscriptionQuery } from '@/data/subscriptions/org-subscription-query'
  24. import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
  25. interface SaveSnippetDialogProps {
  26. open: boolean
  27. sql: string
  28. onOpenChange: (open: boolean) => void
  29. onSave: (name: string) => void
  30. }
  31. export const SaveSnippetDialog = ({ open, sql, onOpenChange, onSave }: SaveSnippetDialogProps) => {
  32. const { ref } = useParams()
  33. const { data: organization } = useSelectedOrganizationQuery()
  34. const { data: subscription } = useOrgSubscriptionQuery(
  35. { orgSlug: organization?.slug },
  36. { enabled: open }
  37. )
  38. const { data: projectSettings } = useProjectSettingsV2Query({ projectRef: ref })
  39. const { data: check } = useCheckOpenAIKeyQuery()
  40. const [name, setName] = useState(generateSnippetTitle())
  41. const isApiKeySet = !!check?.hasKey
  42. const hasHipaaAddon = subscriptionHasHipaaAddon(subscription) && projectSettings?.is_sensitive
  43. const { mutate: generateTitle, isPending: isGenerating } = useSqlTitleGenerateMutation({
  44. onSuccess: ({ title }) => setName(title),
  45. onError: (error) => toast.error(`Failed to generate title: ${error.message}`),
  46. })
  47. // Reset the name each time the dialog opens
  48. useEffect(() => {
  49. if (open) setName(generateSnippetTitle())
  50. }, [open])
  51. const handleSave = () => {
  52. const trimmed = name.trim()
  53. if (!trimmed) return
  54. onSave(trimmed)
  55. onOpenChange(false)
  56. }
  57. return (
  58. <Dialog open={open} onOpenChange={onOpenChange}>
  59. <DialogContent size="small">
  60. <DialogHeader>
  61. <DialogTitle>Save snippet</DialogTitle>
  62. </DialogHeader>
  63. <DialogSectionSeparator />
  64. <DialogSection className="flex flex-col gap-y-4 py-5">
  65. <div className="flex flex-col gap-y-2">
  66. <Label htmlFor="snippet-name">Name</Label>
  67. <Input
  68. id="snippet-name"
  69. autoFocus
  70. value={name}
  71. onChange={(e) => setName(e.target.value)}
  72. onKeyDown={(e) => {
  73. if (e.key === 'Enter') handleSave()
  74. }}
  75. />
  76. </div>
  77. {!hasHipaaAddon && (
  78. <div className="flex justify-end">
  79. <ButtonTooltip
  80. type="default"
  81. size="tiny"
  82. disabled={isGenerating || !isApiKeySet}
  83. onClick={() => generateTitle({ sql })}
  84. tooltip={{
  85. content: {
  86. side: 'bottom',
  87. text: isApiKeySet
  88. ? undefined
  89. : 'Add your "OPENAI_API_KEY" to your environment variables to use this feature.',
  90. },
  91. }}
  92. >
  93. <div className="flex items-center gap-1">
  94. <div className="scale-75">
  95. <AiIconAnimation loading={isGenerating} />
  96. </div>
  97. <span>Generate with AI</span>
  98. </div>
  99. </ButtonTooltip>
  100. </div>
  101. )}
  102. </DialogSection>
  103. <DialogSectionSeparator />
  104. <DialogFooter className="px-5 py-4">
  105. <Button type="default" onClick={() => onOpenChange(false)}>
  106. Cancel
  107. </Button>
  108. <Button disabled={!name.trim()} onClick={handleSave}>
  109. Save snippet
  110. </Button>
  111. </DialogFooter>
  112. </DialogContent>
  113. </Dialog>
  114. )
  115. }