mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 17:35:10 +03:00
feat(studio): Discord community on support ticket creation (#40972)
* clean up AI assistant aside * add Discord aside * misc tweaks to AI cta * improve Discord CTA * markup * fix test file * Minor clean up and fixes * Small improvement --------- Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
This commit is contained in:
1 parent
52c0de8394
commit
da686b2cd9
6 files changed
+141
-45
No files matched your search
@@ -2,25 +2,19 @@ import { AnimatePresence, motion } from 'framer-motion'
|
||||
import { MessageSquare } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
// End of third-party imports
|
||||
|
||||
import { useSendEventMutation } from 'data/telemetry/send-event-mutation'
|
||||
import { Button } from 'ui'
|
||||
import { AiIconAnimation, Button } from 'ui'
|
||||
import { NO_ORG_MARKER, NO_PROJECT_MARKER } from './SupportForm.utils'
|
||||
|
||||
interface AIAssistantOptionProps {
|
||||
projectRef?: string | null
|
||||
organizationSlug?: string | null
|
||||
isCondensed?: boolean
|
||||
}
|
||||
|
||||
export const AIAssistantOption = ({
|
||||
projectRef,
|
||||
organizationSlug,
|
||||
isCondensed = false,
|
||||
}: AIAssistantOptionProps) => {
|
||||
export const AIAssistantOption = ({ projectRef, organizationSlug }: AIAssistantOptionProps) => {
|
||||
const { mutate: sendEvent } = useSendEventMutation()
|
||||
const [isVisible, setIsVisible] = useState(isCondensed)
|
||||
const [isVisible, setIsVisible] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => setIsVisible(true), 800)
|
||||
@@ -46,57 +40,56 @@ export const AIAssistantOption = ({
|
||||
if (!organizationSlug || organizationSlug === NO_ORG_MARKER) return null
|
||||
|
||||
return (
|
||||
<AnimatePresence initial={false}>
|
||||
<AnimatePresence>
|
||||
{isVisible && (
|
||||
<motion.div
|
||||
<motion.aside
|
||||
initial={{ height: 0, opacity: 0 }}
|
||||
animate={{ height: 'auto', opacity: 1 }}
|
||||
exit={{ height: 0, opacity: 0 }}
|
||||
className="w-full overflow-hidden border rounded-md relative"
|
||||
className="w-full overflow-hidden border rounded-md relative bg-200"
|
||||
>
|
||||
<div className={`flex flex-col xl:flex-row ${isCondensed ? 'py-4 px-5' : 'py-8 px-10'}`}>
|
||||
<div className="flex items-center p-6">
|
||||
<div className="flex flex-col gap-3 z-[2] flex-shrink-0 w-full">
|
||||
<div>
|
||||
<p className="text-sm text-foreground">Try the AI Assistant</p>
|
||||
<h5 className="text-sm font-medium text-foreground">Try Supabase Assistant</h5>
|
||||
<p className="text-sm text-foreground-lighter">
|
||||
Ask the AI assistant to help you with your support issue
|
||||
Ask our AI assistant to help you with your support issue.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Link href={aiLink} onClick={onAiAssistantClicked}>
|
||||
<Button size="tiny" type="default">
|
||||
Ask AI assistant
|
||||
<Button size="tiny" type="default" icon={<AiIconAnimation size={14} />}>
|
||||
Ask the Assistant
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
{!isCondensed && (
|
||||
<div className="absolute z-[1] scale-75 -right-24 top-0">
|
||||
<div className="relative grow flex flex-col gap-3 w-[400px]">
|
||||
<div className="flex items-start gap-3 pl-12">
|
||||
<div className="w-8 h-8 rounded-full bg-background-surface-300 flex items-center justify-center">
|
||||
<MessageSquare size={16} className="text-foreground-lighter" />
|
||||
</div>
|
||||
<div className="flex-1 bg-background-surface-200 rounded-lg p-4 max-w-[280px]">
|
||||
<p className="text-sm text-foreground-lighter">
|
||||
Hi! I'm your AI assistant. How can I help you today?
|
||||
</p>
|
||||
</div>
|
||||
{/* Decorative background */}
|
||||
<div className="absolute z-[1] scale-75 -right-40 md:-right-24 -top-6 md:top-0">
|
||||
<div className="relative grow flex flex-col gap-3 w-[400px]">
|
||||
<div className="flex items-start gap-3 pl-12">
|
||||
<div className="w-8 h-8 rounded-full bg-background-surface-300 flex items-center justify-center">
|
||||
<MessageSquare size={16} className="text-foreground-lighter" />
|
||||
</div>
|
||||
<div className="flex items-start justify-end gap-3 pr-10">
|
||||
<div className="bg-background-surface-200 rounded-lg p-4 max-w-[280px]">
|
||||
<p className="text-sm text-foreground-lighter">
|
||||
I can help you with database queries, API endpoints, or any other technical
|
||||
questions you might have.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex-1 bg-background-surface-200 rounded-lg p-4 max-w-[280px]">
|
||||
<p className="text-sm text-foreground-lighter">
|
||||
Hi! I'm your AI assistant. How can I help you today?
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start justify-end gap-3 pr-10">
|
||||
<div className="bg-background-surface-200 rounded-lg p-4 max-w-[280px]">
|
||||
<p className="text-sm text-foreground-lighter">
|
||||
I can help you with database queries, API endpoints, or any other technical
|
||||
questions you might have.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute -inset-2 bg-gradient-to-l from-transparent via-background-200 via-[95%] to-background-200 to-[100%] z-[1]" />
|
||||
</div>
|
||||
)}
|
||||
<div className="absolute -inset-2 bg-gradient-to-l from-transparent via-background-200 via-[90%] to-background-200 to-[100%] z-[1]" />
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.aside>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import { useOrganizationsQuery } from 'data/organizations/organizations-query'
|
||||
import { AnimatePresence, motion } from 'framer-motion'
|
||||
import Link from 'next/link'
|
||||
import router from 'next/router'
|
||||
import { useEffect, useState } from 'react'
|
||||
import SVG from 'react-inlinesvg'
|
||||
import { Button } from 'ui'
|
||||
import { NO_ORG_MARKER } from './SupportForm.utils'
|
||||
|
||||
interface DiscordCTACardProps {
|
||||
organizationSlug?: string | null
|
||||
}
|
||||
|
||||
export const DiscordCTACard = ({ organizationSlug }: DiscordCTACardProps) => {
|
||||
const { data: organizations } = useOrganizationsQuery()
|
||||
const [isVisible, setIsVisible] = useState(false)
|
||||
|
||||
const selectedOrg = organizations?.find((org) => org.slug === organizationSlug)
|
||||
const isFreePlan = selectedOrg?.plan.id === 'free'
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => setIsVisible(true), 800)
|
||||
return () => clearTimeout(timer)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{isVisible && isFreePlan && organizationSlug !== NO_ORG_MARKER && (
|
||||
<motion.aside
|
||||
initial={{ height: 0, opacity: 0 }}
|
||||
animate={{ height: 'auto', opacity: 1 }}
|
||||
exit={{ height: 0, opacity: 0 }}
|
||||
className="w-full overflow-hidden border border-transparent rounded-md relative"
|
||||
style={{
|
||||
background: '#404EED',
|
||||
borderColor: 'color-mix(in srgb, #404EED 95%, #000000)',
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center p-6">
|
||||
{/* Decorative background */}
|
||||
<div
|
||||
className="absolute inset-0 opacity-20 md:opacity-30"
|
||||
style={{
|
||||
backgroundImage: `url(${router.basePath}/img/support/discord-bg-small.jpg)`,
|
||||
backgroundSize: '75%',
|
||||
backgroundPosition: '112% 50%',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
maskImage: 'linear-gradient(to left, rgba(0,0,0,1) 10%, rgba(0,0,0,0) 100%)',
|
||||
WebkitMaskImage: 'linear-gradient(to left, rgba(0,0,0,1) 10%, rgba(0,0,0,0) 100%)',
|
||||
}}
|
||||
/>
|
||||
{/* Content */}
|
||||
<div className="relative z-10">
|
||||
<div className="flex flex-col gap-3">
|
||||
<div>
|
||||
<h5 className="text-sm font-medium text-white">Ask the Discord community</h5>
|
||||
<p className="text-sm text-white/75">
|
||||
Many code-related questions are answered within minutes.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Link href="https://discord.supabase.com" target="_blank" rel="noreferrer">
|
||||
<Button
|
||||
size="tiny"
|
||||
type="secondary"
|
||||
icon={
|
||||
<SVG src={`${router.basePath}/img/discord-icon.svg`} className="h-4 w-4" />
|
||||
}
|
||||
className="bg-white hover:bg-white/90" // Force white button on all color schemes
|
||||
>
|
||||
<span style={{ color: '#404EED' }}>Ask on Discord</span>
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.aside>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
)
|
||||
}
|
||||
@@ -10,12 +10,14 @@ import { toast } from 'sonner'
|
||||
import CopyButton from 'components/ui/CopyButton'
|
||||
import InformationBox from 'components/ui/InformationBox'
|
||||
import { InlineLink, InlineLinkClassName } from 'components/ui/InlineLink'
|
||||
import { useOrganizationsQuery } from 'data/organizations/organizations-query'
|
||||
import { usePlatformStatusQuery } from 'data/platform/platform-status-query'
|
||||
import { useSendEventMutation } from 'data/telemetry/send-event-mutation'
|
||||
import { useStateTransition } from 'hooks/misc/useStateTransition'
|
||||
import { BASE_PATH, DOCS_URL } from 'lib/constants'
|
||||
import { Button, cn, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
|
||||
import { AIAssistantOption } from './AIAssistantOption'
|
||||
import { DiscordCTACard } from './DiscordCTACard'
|
||||
import { HighlightProjectRefProvider, useHighlightProjectRefContext } from './HighlightContext'
|
||||
import { Success } from './Success'
|
||||
import type { ExtendedSupportCategories } from './Support.constants'
|
||||
@@ -65,9 +67,12 @@ export function SupportFormPage() {
|
||||
}
|
||||
|
||||
function SupportFormPageContent() {
|
||||
const { data: organizations } = useOrganizationsQuery()
|
||||
const [state, dispatch] = useReducer(supportFormReducer, undefined, createInitialSupportFormState)
|
||||
|
||||
const { form, initialError, projectRef, orgSlug } = useSupportForm(dispatch)
|
||||
const selectedOrg = organizations?.find((org) => org.slug === orgSlug)
|
||||
const isFreePlan = selectedOrg?.plan.id === 'free'
|
||||
|
||||
const sendTelemetry = useSupportFormTelemetry()
|
||||
useStateTransition(state, 'submitting', 'success', (_, curr) => {
|
||||
@@ -88,7 +93,12 @@ function SupportFormPageContent() {
|
||||
return (
|
||||
<SupportFormWrapper>
|
||||
<SupportFormHeader />
|
||||
<AIAssistantOption projectRef={projectRef} organizationSlug={orgSlug} />
|
||||
|
||||
<div className="flex flex-col gap-y-4">
|
||||
<AIAssistantOption projectRef={projectRef} organizationSlug={orgSlug} />
|
||||
<DiscordCTACard organizationSlug={orgSlug} />
|
||||
</div>
|
||||
|
||||
<SupportFormBody
|
||||
form={form}
|
||||
state={state}
|
||||
|
||||
@@ -14,7 +14,14 @@ export const SupportLink = ({
|
||||
const href = createSupportFormUrl(queryParams ?? {})
|
||||
|
||||
return (
|
||||
<Link {...props} href={href} onClick={takeBreadcrumbSnapshot}>
|
||||
<Link
|
||||
{...props}
|
||||
href={href}
|
||||
onClick={(event) => {
|
||||
takeBreadcrumbSnapshot()
|
||||
props.onClick?.(event)
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Link>
|
||||
)
|
||||
|
||||
@@ -1036,7 +1036,7 @@ describe('SupportFormPage', () => {
|
||||
renderSupportFormPage()
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Try the AI Assistant')).toBeInTheDocument()
|
||||
expect(screen.getByText('Try Supabase Assistant')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -134,17 +134,22 @@ export const HelpPopover = () => {
|
||||
{IS_PLATFORM && (
|
||||
<>
|
||||
<ButtonGroupItem
|
||||
asChild
|
||||
size="tiny"
|
||||
icon={<Activity strokeWidth={1.5} size={14} />}
|
||||
asChild
|
||||
>
|
||||
<a href="https://status.supabase.com/" target="_blank" rel="noreferrer">
|
||||
Supabase status
|
||||
</a>
|
||||
</ButtonGroupItem>
|
||||
|
||||
<ButtonGroupItem size="tiny" icon={<Mail strokeWidth={1.5} size={14} />}>
|
||||
<SupportLink queryParams={supportLinkQueryParams}>Contact support</SupportLink>
|
||||
<ButtonGroupItem asChild size="tiny" icon={<Mail strokeWidth={1.5} size={14} />}>
|
||||
<SupportLink
|
||||
queryParams={supportLinkQueryParams}
|
||||
onClick={() => setIsOpen(false)}
|
||||
>
|
||||
Contact support
|
||||
</SupportLink>
|
||||
</ButtonGroupItem>
|
||||
</>
|
||||
)}
|
||||
|
||||
Reference in new issue
Block a user