mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
Feat/ai landing page (#30559)
* ai landing page * compress image * use jpg for ai image * support light mode * rename to assistant and style fixes * update copy and id issue * assistant cta * assistant copy * shine refine * add more assistant examples * update assistant examples * copy * use dots instead of image * refine design * gap * refinements to landing * remove image --------- Co-authored-by: Francesco Sansalvadore <f.sansalvadore@gmail.com>
This commit is contained in:
1 parent
16e002d8fd
commit
1b2d50cd51
6 files changed
+1458
No files matched your search
@@ -0,0 +1,70 @@
|
||||
import React from 'react'
|
||||
import { motion } from 'framer-motion'
|
||||
|
||||
interface DotGridProps {
|
||||
rows: number
|
||||
columns: number
|
||||
count: number
|
||||
}
|
||||
|
||||
const DotGrid: React.FC<DotGridProps> = ({ rows, columns, count }) => {
|
||||
const container = {
|
||||
hidden: { opacity: 1 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
transition: {
|
||||
staggerChildren: 0.01,
|
||||
delayChildren: 0.01,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const item = {
|
||||
hidden: { opacity: 0 },
|
||||
visible: { opacity: 0.3 },
|
||||
}
|
||||
|
||||
const highlightedVariants = {
|
||||
visible: {
|
||||
opacity: [1, 0.5, 1],
|
||||
transition: {
|
||||
repeat: Infinity,
|
||||
duration: 0.5,
|
||||
repeatDelay: 1.5,
|
||||
ease: 'easeInOut',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative w-full h-full min-h-[600px]">
|
||||
<motion.div
|
||||
className="grid h-full md:w-full aspect-square justify-between items-space-between items-start"
|
||||
style={{
|
||||
gridTemplateColumns: `repeat(${columns}, 1px)`,
|
||||
gridTemplateRows: `repeat(${rows}, 1fr)`,
|
||||
rowGap: 'auto',
|
||||
}}
|
||||
variants={container}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
aria-label={`Grid of ${rows * columns} dots, ${count} highlighted`}
|
||||
>
|
||||
{Array.from({ length: rows * columns }).map((_, index) => {
|
||||
const isHighlighted = index < count
|
||||
return (
|
||||
<motion.div
|
||||
key={index}
|
||||
variants={isHighlighted ? highlightedVariants : item}
|
||||
className="w-[2px] h-[2px] dark:w-[1px] dark:h-[1px] rounded-full bg-foreground-lighter"
|
||||
/>
|
||||
)
|
||||
})}
|
||||
<div className="absolute bg-gradient-to-b lg:bg-gradient-to-r inset-0 w-full h-full from-background to-transparent to-40%" />
|
||||
<div className="absolute bg-gradient-to-t lg:bg-gradient-to-l inset-0 w-full h-full from-background to-transparent to-20%" />
|
||||
</motion.div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default DotGrid
|
||||
@@ -0,0 +1,227 @@
|
||||
// components/AIDemoPanel.tsx
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import { motion, AnimatePresence } from 'framer-motion'
|
||||
import { Button, Input } from 'ui'
|
||||
import { AiIconAnimation, cn } from 'ui'
|
||||
import errorGif from 'public/images/ai/error.gif'
|
||||
import Image from 'next/image'
|
||||
import styles from './assistant.module.css'
|
||||
|
||||
interface DemoMessage {
|
||||
id: string
|
||||
role: 'user' | 'assistant'
|
||||
content: string | React.ReactNode
|
||||
render?: React.ReactNode
|
||||
createdAt: Date
|
||||
}
|
||||
|
||||
export const AIDemoPanel = ({ incomingMessages = [] }: { incomingMessages?: DemoMessage[] }) => {
|
||||
const [messages, setMessages] = useState<DemoMessage[]>([])
|
||||
const [isComplete, setIsComplete] = useState(false)
|
||||
const [pendingMessages, setPendingMessages] = useState<DemoMessage[]>([])
|
||||
const [input, setInput] = useState('')
|
||||
const [showFade, setShowFade] = useState(false)
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null)
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const scrollToBottom = () => {
|
||||
const container = messagesEndRef.current?.closest('.overflow-y-auto')
|
||||
if (container) {
|
||||
container.scrollTo({
|
||||
top: container.scrollHeight,
|
||||
behavior: 'smooth',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleScroll = () => {
|
||||
const container = scrollContainerRef.current
|
||||
if (container) {
|
||||
const scrollPercentage =
|
||||
(container.scrollTop / (container.scrollHeight - container.clientHeight)) * 100
|
||||
const isScrollable = container.scrollHeight > container.clientHeight
|
||||
const isAtBottom = scrollPercentage >= 100
|
||||
|
||||
setShowFade(isScrollable && !isAtBottom)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
scrollToBottom()
|
||||
handleScroll()
|
||||
}, [messages])
|
||||
|
||||
useEffect(() => {
|
||||
const container = scrollContainerRef.current
|
||||
if (container) {
|
||||
container.addEventListener('scroll', handleScroll)
|
||||
handleScroll()
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (container) {
|
||||
container.removeEventListener('scroll', handleScroll)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (pendingMessages.length > 0) {
|
||||
const timer = setTimeout(() => {
|
||||
setMessages((prev) => [...prev, pendingMessages[0]])
|
||||
setPendingMessages((prev) => prev.slice(1))
|
||||
}, 3000)
|
||||
return () => clearTimeout(timer)
|
||||
} else if (!isComplete) {
|
||||
setIsComplete(true)
|
||||
}
|
||||
}, [isComplete, pendingMessages])
|
||||
|
||||
useEffect(() => {
|
||||
if (incomingMessages.length > 0) {
|
||||
setMessages((prev) => [...prev, ...pendingMessages, incomingMessages[0]])
|
||||
setPendingMessages(incomingMessages.slice(1))
|
||||
}
|
||||
}, [incomingMessages])
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
if (!input.trim()) return
|
||||
|
||||
const userMessage: DemoMessage = {
|
||||
id: Date.now().toString(),
|
||||
role: 'user',
|
||||
content: input,
|
||||
createdAt: new Date(),
|
||||
}
|
||||
|
||||
const assistantMessage: DemoMessage = {
|
||||
id: (Date.now() + 1).toString(),
|
||||
role: 'assistant',
|
||||
content: (
|
||||
<div className="rounded-md overflow-hidden relative">
|
||||
<Image
|
||||
src={errorGif}
|
||||
alt="Try the assistant in your dashboard for real results"
|
||||
className="w-full grayscale"
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
createdAt: new Date(),
|
||||
}
|
||||
|
||||
const followUp: DemoMessage = {
|
||||
id: (Date.now() + 1).toString(),
|
||||
role: 'assistant',
|
||||
content: "Sorry, custom queries aren't supported in this demo.",
|
||||
createdAt: new Date(),
|
||||
}
|
||||
|
||||
setMessages((prev) => [...prev, userMessage])
|
||||
setPendingMessages((prev) => [...prev, assistantMessage, followUp])
|
||||
setInput('')
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full shadow-2xl relative h-full bg-surface-75 border border-text-light/25 rounded-md overflow-hidden">
|
||||
<div className="absolute inset-0 flex flex-col">
|
||||
{/* Header */}
|
||||
<div className="z-30 border-b border-b-muted flex items-center gap-x-3 px-5 h-[46px]">
|
||||
<AiIconAnimation loading={false} allowHoverEffect />
|
||||
<div className="text-sm flex-1">Assistant</div>
|
||||
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }}>
|
||||
<Button
|
||||
type="default"
|
||||
onClick={() => {
|
||||
setMessages([])
|
||||
setIsComplete(true)
|
||||
setPendingMessages([])
|
||||
}}
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* Messages */}
|
||||
<div
|
||||
ref={scrollContainerRef}
|
||||
className="flex-1 overflow-y-auto p-5 flex flex-col [&::-webkit-scrollbar]:hidden [-ms-overflow-style:none] [scrollbar-width:none]"
|
||||
>
|
||||
{messages.map((message, index) => (
|
||||
<>
|
||||
<motion.div
|
||||
key={index}
|
||||
layout="position"
|
||||
initial={{ y: 5, opacity: 0, scale: 0.99 }}
|
||||
animate={{ y: 0, opacity: 1, scale: 1 }}
|
||||
className={cn(
|
||||
'text-foreground-light text-sm mb-6',
|
||||
message.role === 'user'
|
||||
? 'px-5 py-3 rounded-lg bg-background-muted/80 w-fit self-end max-w-64'
|
||||
: ''
|
||||
)}
|
||||
>
|
||||
{message.content}
|
||||
{message.render && <div className="mt-4">{message.render}</div>}
|
||||
</motion.div>
|
||||
</>
|
||||
))}
|
||||
{messages.length > 0 && messages[messages.length - 1].role === 'user' && (
|
||||
<motion.div className="text-foreground-lighter text-sm flex gap-1.5 items-center">
|
||||
<span>Thinking</span>
|
||||
<div className="flex gap-1">
|
||||
<motion.span
|
||||
animate={{ opacity: [0, 1, 0] }}
|
||||
transition={{ duration: 1.5, repeat: Infinity, delay: 0 }}
|
||||
>
|
||||
.
|
||||
</motion.span>
|
||||
<motion.span
|
||||
animate={{ opacity: [0, 1, 0] }}
|
||||
transition={{ duration: 1.5, repeat: Infinity, delay: 0.3 }}
|
||||
>
|
||||
.
|
||||
</motion.span>
|
||||
<motion.span
|
||||
animate={{ opacity: [0, 1, 0] }}
|
||||
transition={{ duration: 1.5, repeat: Infinity, delay: 0.6 }}
|
||||
>
|
||||
.
|
||||
</motion.span>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
|
||||
{/* Input */}
|
||||
<AnimatePresence>
|
||||
{showFade && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
exit={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
className="pointer-events-none z-10 -mt-24"
|
||||
>
|
||||
<div className="h-24 w-full bg-gradient-to-t from-background muted to-transparent" />
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
<div className="p-5 pt-0 relative z-10">
|
||||
<form onSubmit={handleSubmit} className={cn('rounded-md', styles['border-gradient'])}>
|
||||
<Input
|
||||
type="text"
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
placeholder="Ask your data anything..."
|
||||
className={cn(
|
||||
'w-full bg-background-muted rounded-md [&>textarea]:border-1 [&>textarea]:rounded-md [&>textarea]:!outline-none [&>textarea]:!ring-offset-0 [&>textarea]:!ring-0 focus:outline-none'
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { motion } from 'framer-motion'
|
||||
import { Code, Play, DatabaseIcon } from 'lucide-react'
|
||||
import { Button, cn, ChartContainer, ChartTooltip, ChartTooltipContent, SQL_ICON } from 'ui'
|
||||
import { BarChart, Bar, XAxis, CartesianGrid } from 'recharts'
|
||||
import CodeBlock from '../CodeBlock/CodeBlock'
|
||||
|
||||
interface SqlSnippetProps {
|
||||
id: string
|
||||
sql: string
|
||||
title?: string
|
||||
resultType?: 'chart' | 'table'
|
||||
mockData?: any
|
||||
}
|
||||
|
||||
// Example mock data matching the Results component format
|
||||
const MOCK_TABLE_DATA = {
|
||||
rows: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'John Doe',
|
||||
email: 'john@example.com',
|
||||
created_at: '2024-01-15T10:00:00Z',
|
||||
status: 'active',
|
||||
metadata: { role: 'admin', last_login: '2024-01-14' },
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Jane Smith',
|
||||
email: 'jane@example.com',
|
||||
created_at: '2024-01-14T15:30:00Z',
|
||||
status: 'pending',
|
||||
metadata: null,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Bob Wilson',
|
||||
email: 'bob@example.com',
|
||||
created_at: '2024-01-13T09:15:00Z',
|
||||
status: 'active',
|
||||
metadata: { role: 'user', last_login: '2024-01-12' },
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const MOCK_CHART_DATA = [
|
||||
{ name: 'Jan', value: 400 },
|
||||
{ name: 'Feb', value: 300 },
|
||||
{ name: 'Mar', value: 600 },
|
||||
{ name: 'Apr', value: 800 },
|
||||
{ name: 'May', value: 700 },
|
||||
]
|
||||
|
||||
export const SqlSnippet = ({
|
||||
id,
|
||||
sql,
|
||||
title = 'SQL Query',
|
||||
resultType = 'table',
|
||||
mockData,
|
||||
}: SqlSnippetProps) => {
|
||||
const [showCode, setShowCode] = useState(!mockData)
|
||||
const [isExecuting, setIsExecuting] = useState(false)
|
||||
const [showResults, setShowResults] = useState(false)
|
||||
const [results, setResults] = useState<any>(null)
|
||||
|
||||
const handleExecute = () => {
|
||||
setIsExecuting(true)
|
||||
setShowResults(false)
|
||||
|
||||
// Simulate SQL execution delay
|
||||
setTimeout(() => {
|
||||
setIsExecuting(false)
|
||||
if (mockData) {
|
||||
setShowResults(true)
|
||||
// Use provided mock data or default mock data
|
||||
setResults(
|
||||
resultType === 'chart'
|
||||
? mockData || MOCK_CHART_DATA
|
||||
: mockData?.rows || MOCK_TABLE_DATA.rows
|
||||
)
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
// Auto-execute on mount
|
||||
useEffect(() => {
|
||||
handleExecute()
|
||||
}, [])
|
||||
|
||||
const ResultsDisplay = () => {
|
||||
if (resultType === 'chart') {
|
||||
return (
|
||||
<div className="p-5 border-t border-t-muted">
|
||||
<ChartContainer config={{}} className="aspect-auto h-[250px] w-full">
|
||||
<BarChart
|
||||
data={results}
|
||||
margin={{
|
||||
left: 12,
|
||||
right: 12,
|
||||
}}
|
||||
>
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis
|
||||
dataKey="name"
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={8}
|
||||
minTickGap={16}
|
||||
padding={{ left: 24, right: 24 }}
|
||||
/>
|
||||
<ChartTooltip
|
||||
cursor={false}
|
||||
content={<ChartTooltipContent className="w-[150px]" />}
|
||||
/>
|
||||
<Bar dataKey="value" fill="hsl(var(--brand-default))" />
|
||||
</BarChart>
|
||||
</ChartContainer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={cn(
|
||||
showResults ? 'h-auto max-h-64 overflow-auto border-t border-t-muted' : 'h-0'
|
||||
)}
|
||||
>
|
||||
<table className="w-full m-0 p-0">
|
||||
<thead>
|
||||
<tr className="bg-surface-100">
|
||||
{results?.length > 0 &&
|
||||
Object.keys(results[0]).map((key) => (
|
||||
<th key={key} className="p-2 px-5 text-left text-xs font-mono">
|
||||
{key}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{results?.map((row: any, i: number) => (
|
||||
<tr key={i} className="border-b">
|
||||
{Object.values(row).map((value: any, j: number) => (
|
||||
<td key={j} className="p-2 px-5 text-left text-xs font-mono">
|
||||
{value === null
|
||||
? 'NULL'
|
||||
: typeof value === 'object'
|
||||
? JSON.stringify(value)
|
||||
: String(value)}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className="flex items-center justify-between bg-surface-100 py-2 px-4">
|
||||
<p className="text-xs text-foreground-light">
|
||||
{results.length} rows
|
||||
{results.length >= 100 && ' (Limited to only 100 rows)'}
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="my-3 mt-2 border rounded bg-surface-100 overflow-hidden">
|
||||
<div className="">
|
||||
<div className="flex items-center gap-2 px-4 py-1">
|
||||
<SQL_ICON
|
||||
className={cn(
|
||||
'transition-colors',
|
||||
'fill-foreground-muted',
|
||||
'group-aria-selected:fill-foreground',
|
||||
'w-5 h-5 shrink-0',
|
||||
'-ml-0.5'
|
||||
)}
|
||||
size={16}
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
<h3 className="text-xs font-medium flex-1">{title}</h3>
|
||||
<div className="flex">
|
||||
<Button
|
||||
type="text"
|
||||
size="tiny"
|
||||
className="w-7 h-7"
|
||||
icon={<Code size={14} />}
|
||||
onClick={() => setShowCode(!showCode)}
|
||||
/>
|
||||
<Button
|
||||
type="text"
|
||||
size="tiny"
|
||||
className="w-7 h-7"
|
||||
icon={<Play size={14} />}
|
||||
loading={isExecuting}
|
||||
onClick={handleExecute}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showCode && (
|
||||
<div className="border-t">
|
||||
<CodeBlock className="border-0 rounded-none" showLineNumbers lang="sql">
|
||||
{sql}
|
||||
</CodeBlock>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isExecuting ? (
|
||||
<motion.div className="text-foreground-lighter text-sm flex gap-1.5 items-center p-5">
|
||||
<span>Executing query</span>
|
||||
<div className="flex gap-1">
|
||||
<motion.span
|
||||
animate={{ opacity: [0, 1, 0] }}
|
||||
transition={{ duration: 1.5, repeat: Infinity, delay: 0 }}
|
||||
>
|
||||
.
|
||||
</motion.span>
|
||||
<motion.span
|
||||
animate={{ opacity: [0, 1, 0] }}
|
||||
transition={{ duration: 1.5, repeat: Infinity, delay: 0.3 }}
|
||||
>
|
||||
.
|
||||
</motion.span>
|
||||
<motion.span
|
||||
animate={{ opacity: [0, 1, 0] }}
|
||||
transition={{ duration: 1.5, repeat: Infinity, delay: 0.6 }}
|
||||
>
|
||||
.
|
||||
</motion.span>
|
||||
</div>
|
||||
</motion.div>
|
||||
) : showResults && results ? (
|
||||
<ResultsDisplay />
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
.border-gradient {
|
||||
--color1: #0a5c36;
|
||||
--color2: #2ecc71;
|
||||
--color3: #b44bff;
|
||||
--angle: 0deg;
|
||||
|
||||
border: none;
|
||||
position: relative;
|
||||
transform-style: preserve-3d;
|
||||
}
|
||||
|
||||
.border-gradient::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin-inline: auto;
|
||||
width: calc(100% + 2px);
|
||||
height: calc(100% + 2px);
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
border-radius: 6px;
|
||||
transform: translate3d(-50%, -50%, -2px);
|
||||
background: linear-gradient(var(--angle), var(--color1), var(--color2), var(--color3));
|
||||
filter: blur(0);
|
||||
animation: animate-bg 10s linear infinite alternate;
|
||||
}
|
||||
|
||||
.border-gradient::before {
|
||||
transform: translate3d(-50%, -50%, -1px);
|
||||
backdrop-filter: blur(0);
|
||||
}
|
||||
|
||||
@property --angle {
|
||||
syntax: '<angle>';
|
||||
initial-value: 0deg;
|
||||
inherits: false;
|
||||
}
|
||||
|
||||
@property --color1 {
|
||||
syntax: '<color>';
|
||||
initial-value: #0a5c36;
|
||||
inherits: false;
|
||||
}
|
||||
|
||||
@property --color2 {
|
||||
syntax: '<color>';
|
||||
initial-value: #2ecc71;
|
||||
inherits: false;
|
||||
}
|
||||
|
||||
@property --color3 {
|
||||
syntax: '<color>';
|
||||
initial-value: #b44bff;
|
||||
inherits: false;
|
||||
}
|
||||
|
||||
@keyframes animate-bg {
|
||||
0% {
|
||||
--angle: 0deg;
|
||||
--color1: #0a5c36;
|
||||
--color2: #2ecc71;
|
||||
--color3: #b44bff;
|
||||
}
|
||||
12.5% {
|
||||
--angle: 45deg;
|
||||
--color1: #2ecc71;
|
||||
--color2: #b44bff;
|
||||
--color3: #0a5c36;
|
||||
}
|
||||
25% {
|
||||
--angle: 90deg;
|
||||
--color1: #b44bff;
|
||||
--color2: #0a5c36;
|
||||
--color3: #2ecc71;
|
||||
}
|
||||
37.5% {
|
||||
--angle: 135deg;
|
||||
--color1: #0a5c36;
|
||||
--color2: #2ecc71;
|
||||
--color3: #b44bff;
|
||||
}
|
||||
50% {
|
||||
--angle: 180deg;
|
||||
--color1: #2ecc71;
|
||||
--color2: #b44bff;
|
||||
--color3: #0a5c36;
|
||||
}
|
||||
62.5% {
|
||||
--angle: 225deg;
|
||||
--color1: #b44bff;
|
||||
--color2: #0a5c36;
|
||||
--color3: #2ecc71;
|
||||
}
|
||||
75% {
|
||||
--angle: 270deg;
|
||||
--color1: #0a5c36;
|
||||
--color2: #2ecc71;
|
||||
--color3: #b44bff;
|
||||
}
|
||||
87.5% {
|
||||
--angle: 315deg;
|
||||
--color1: #2ecc71;
|
||||
--color2: #b44bff;
|
||||
--color3: #0a5c36;
|
||||
}
|
||||
100% {
|
||||
--angle: 360deg;
|
||||
--color1: #0a5c36;
|
||||
--color2: #2ecc71;
|
||||
--color3: #b44bff;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,808 @@
|
||||
import { Button } from 'ui'
|
||||
import dynamic from 'next/dynamic'
|
||||
import { AIDemoPanel } from '~/components/AIDemo/Panel'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { SqlSnippet } from '~/components/AIDemo/SqlSnippet'
|
||||
import { NextSeo } from 'next-seo'
|
||||
import { motion } from 'framer-motion'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useIsLoggedIn, useIsUserLoading } from 'common'
|
||||
import Link from 'next/link'
|
||||
import DotGrid from '~/components/AIDemo/DotGrid'
|
||||
import { EASE_OUT } from '../lib/animations'
|
||||
|
||||
const DefaultLayout = dynamic(() => import('~/components/Layouts/Default'))
|
||||
const SectionContainer = dynamic(() => import('~/components/Layouts/SectionContainer'))
|
||||
|
||||
interface Message {
|
||||
id: string
|
||||
role: 'user' | 'assistant'
|
||||
content: string | JSX.Element
|
||||
createdAt: Date
|
||||
render?: JSX.Element
|
||||
}
|
||||
|
||||
const welcomeMessages = [
|
||||
{
|
||||
id: 'ph-1',
|
||||
role: 'user' as const,
|
||||
content: 'I have come from ProductHunt. Show me the goods!',
|
||||
createdAt: new Date(),
|
||||
},
|
||||
{
|
||||
id: 'ph-2',
|
||||
role: 'assistant' as const,
|
||||
content: (
|
||||
<div className="flex gap-4">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="40"
|
||||
height="40"
|
||||
viewBox="0 0 40 40"
|
||||
className="w-6 h-6 shrink-0"
|
||||
>
|
||||
<g fill="none" fill-rule="evenodd">
|
||||
<path
|
||||
fill="#FF6154"
|
||||
d="M40 20c0 11.046-8.954 20-20 20S0 31.046 0 20 8.954 0 20 0s20 8.954 20 20"
|
||||
></path>
|
||||
<path
|
||||
fill="#FFF"
|
||||
d="M22.667 20H17v-6h5.667a3 3 0 0 1 0 6m0-10H13v20h4v-6h5.667a7 7 0 1 0 0-14"
|
||||
></path>
|
||||
</g>
|
||||
</svg>
|
||||
<p>
|
||||
Welcome Product Hunter! 👋 Thanks for checking out the Supabase assistant. Let me show you
|
||||
what I can do!
|
||||
</p>
|
||||
</div>
|
||||
),
|
||||
createdAt: new Date(),
|
||||
},
|
||||
]
|
||||
|
||||
const demoQueries = [
|
||||
{
|
||||
label: 'User Growth Over Time',
|
||||
messages: [
|
||||
{
|
||||
id: '1',
|
||||
role: 'user' as const,
|
||||
content: 'Show me user signups over the past 12 months',
|
||||
createdAt: new Date(),
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
role: 'assistant' as const,
|
||||
content: 'Here is a chart showing user growth by month:',
|
||||
createdAt: new Date(),
|
||||
render: (
|
||||
<SqlSnippet
|
||||
id="user-growth"
|
||||
title="Monthly User Signups"
|
||||
resultType="chart"
|
||||
sql={`SELECT
|
||||
date_trunc('month', created_at) as month,
|
||||
count(*) as new_users
|
||||
FROM users
|
||||
GROUP BY month
|
||||
ORDER BY month
|
||||
LIMIT 12;`}
|
||||
mockData={[
|
||||
{ name: 'Jan 2023', value: 245 },
|
||||
{ name: 'Feb 2023', value: 312 },
|
||||
{ name: 'Mar 2023', value: 287 },
|
||||
{ name: 'Apr 2023', value: 401 },
|
||||
{ name: 'May 2023', value: 352 },
|
||||
{ name: 'Jun 2023', value: 489 },
|
||||
{ name: 'Jul 2023', value: 567 },
|
||||
{ name: 'Aug 2023', value: 623 },
|
||||
{ name: 'Sep 2023', value: 712 },
|
||||
{ name: 'Oct 2023', value: 812 },
|
||||
{ name: 'Nov 2023', value: 934 },
|
||||
{ name: 'Dec 2023', value: 1023 },
|
||||
]}
|
||||
/>
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Create Function',
|
||||
messages: [
|
||||
{
|
||||
id: '19',
|
||||
role: 'user' as const,
|
||||
content: 'Help me create a function to calculate user points',
|
||||
createdAt: new Date(),
|
||||
},
|
||||
{
|
||||
id: '20',
|
||||
role: 'assistant' as const,
|
||||
content: "Here's a Postgres function that calculates user points based on their activity:",
|
||||
createdAt: new Date(),
|
||||
render: (
|
||||
<SqlSnippet
|
||||
id="create-function"
|
||||
title="Create User Points Function"
|
||||
sql={`CREATE OR REPLACE FUNCTION calculate_user_points(user_id UUID)
|
||||
RETURNS INTEGER AS $$
|
||||
DECLARE
|
||||
total_points INTEGER;
|
||||
BEGIN
|
||||
SELECT
|
||||
COALESCE(
|
||||
(SELECT COUNT(*) * 10 FROM posts WHERE author_id = user_id) + -- 10 points per post
|
||||
(SELECT COUNT(*) * 5 FROM comments WHERE user_id = user_id) + -- 5 points per comment
|
||||
(SELECT COUNT(*) * 2 FROM post_likes WHERE user_id = user_id), -- 2 points per like
|
||||
0
|
||||
) INTO total_points;
|
||||
|
||||
RETURN total_points;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;`}
|
||||
/>
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'RLS Policy',
|
||||
messages: [
|
||||
{
|
||||
id: '21',
|
||||
role: 'user' as const,
|
||||
content: 'Show me how to create an RLS policy for a team members table',
|
||||
createdAt: new Date(),
|
||||
},
|
||||
{
|
||||
id: '22',
|
||||
role: 'assistant' as const,
|
||||
content:
|
||||
"Here's an RLS policy that allows team members to only see other members in their team:",
|
||||
createdAt: new Date(),
|
||||
render: (
|
||||
<SqlSnippet
|
||||
id="rls-policy"
|
||||
title="Team Members RLS Policy"
|
||||
sql={`-- Enable RLS on the table
|
||||
ALTER TABLE team_members ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- Create policy for viewing team members
|
||||
CREATE POLICY view_team_members ON team_members
|
||||
FOR SELECT
|
||||
USING (
|
||||
team_id IN (
|
||||
-- User can see members of teams they belong to
|
||||
SELECT team_id
|
||||
FROM team_members
|
||||
WHERE user_id = auth.uid()
|
||||
)
|
||||
OR
|
||||
-- Team admins can see all members
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM team_admins
|
||||
WHERE user_id = auth.uid()
|
||||
AND team_id = team_members.team_id
|
||||
)
|
||||
);`}
|
||||
/>
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'SQL to Supabase-js',
|
||||
messages: [
|
||||
{
|
||||
id: '23',
|
||||
role: 'user' as const,
|
||||
content: 'How do I query all projects?',
|
||||
createdAt: new Date(),
|
||||
},
|
||||
{
|
||||
id: '24',
|
||||
role: 'assistant' as const,
|
||||
content: "Here's the SQL query to get all projects:",
|
||||
createdAt: new Date(),
|
||||
render: (
|
||||
<SqlSnippet
|
||||
id="projects-sql"
|
||||
title="Get All Projects SQL"
|
||||
sql={`SELECT
|
||||
p.id,
|
||||
p.name,
|
||||
p.description,
|
||||
p.created_at,
|
||||
u.email as owner_email
|
||||
FROM projects p
|
||||
JOIN users u ON p.owner_id = u.id
|
||||
WHERE p.is_active = true
|
||||
ORDER BY p.created_at DESC;`}
|
||||
mockData={{
|
||||
rows: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Project A',
|
||||
description: 'First project',
|
||||
created_at: '2024-01-15T10:00:00Z',
|
||||
owner_email: 'user@example.com',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Project B',
|
||||
description: 'Second project',
|
||||
created_at: '2024-01-14T10:00:00Z',
|
||||
owner_email: 'user2@example.com',
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: '25',
|
||||
role: 'user' as const,
|
||||
content: 'Can you show me how to do this with the Supabase client?',
|
||||
createdAt: new Date(),
|
||||
},
|
||||
{
|
||||
id: '26',
|
||||
role: 'assistant' as const,
|
||||
content: "Here's how to perform the same query using the Supabase JavaScript client:",
|
||||
createdAt: new Date(),
|
||||
render: (
|
||||
<SqlSnippet
|
||||
id="projects-js"
|
||||
title="Get All Projects with Supabase Client"
|
||||
sql={`const { data: projects, error } = await supabase
|
||||
.from('projects')
|
||||
.select(\`
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
created_at,
|
||||
owner_id (
|
||||
email
|
||||
)
|
||||
\`)
|
||||
.eq('is_active', true)
|
||||
.order('created_at', { ascending: false })`}
|
||||
/>
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Recent Orders',
|
||||
messages: [
|
||||
{
|
||||
id: '3',
|
||||
role: 'user' as const,
|
||||
content: 'Show me the most recent orders',
|
||||
createdAt: new Date(),
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
role: 'assistant' as const,
|
||||
content: 'Here are the 5 most recent orders:',
|
||||
createdAt: new Date(),
|
||||
render: (
|
||||
<SqlSnippet
|
||||
id="recent-orders"
|
||||
title="Recent Orders"
|
||||
sql={`SELECT
|
||||
order_id,
|
||||
customer_name,
|
||||
amount,
|
||||
status,
|
||||
created_at
|
||||
FROM orders
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 5;`}
|
||||
mockData={{
|
||||
rows: [
|
||||
{
|
||||
order_id: 'ORD-001',
|
||||
customer_name: 'John Smith',
|
||||
amount: 299.99,
|
||||
status: 'processing',
|
||||
created_at: '2024-01-15T14:22:31Z',
|
||||
},
|
||||
{
|
||||
order_id: 'ORD-002',
|
||||
customer_name: 'Sarah Johnson',
|
||||
amount: 149.5,
|
||||
status: 'shipped',
|
||||
created_at: '2024-01-15T13:45:12Z',
|
||||
},
|
||||
{
|
||||
order_id: 'ORD-003',
|
||||
customer_name: 'Mike Wilson',
|
||||
amount: 499.99,
|
||||
status: 'completed',
|
||||
created_at: '2024-01-15T12:30:45Z',
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Revenue by Category',
|
||||
messages: [
|
||||
{
|
||||
id: '5',
|
||||
role: 'user' as const,
|
||||
content: 'What are our top performing product categories?',
|
||||
createdAt: new Date(),
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
role: 'assistant' as const,
|
||||
content: 'Here is the revenue breakdown by product category:',
|
||||
createdAt: new Date(),
|
||||
render: (
|
||||
<SqlSnippet
|
||||
id="category-revenue"
|
||||
title="Revenue by Category"
|
||||
resultType="chart"
|
||||
sql={`SELECT
|
||||
category_name,
|
||||
sum(order_amount) as revenue
|
||||
FROM orders o
|
||||
JOIN products p ON o.product_id = p.id
|
||||
GROUP BY category_name
|
||||
ORDER BY revenue DESC;`}
|
||||
mockData={[
|
||||
{ name: 'Electronics', value: 125000 },
|
||||
{ name: 'Clothing', value: 98000 },
|
||||
{ name: 'Home & Garden', value: 87000 },
|
||||
{ name: 'Sports', value: 76000 },
|
||||
{ name: 'Books', value: 65000 },
|
||||
{ name: 'Toys', value: 54000 },
|
||||
{ name: 'Beauty', value: 43000 },
|
||||
{ name: 'Automotive', value: 32000 },
|
||||
{ name: 'Food', value: 21000 },
|
||||
{ name: 'Art', value: 15000 },
|
||||
]}
|
||||
/>
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Active Users',
|
||||
messages: [
|
||||
{
|
||||
id: '7',
|
||||
role: 'user' as const,
|
||||
content: 'Show me our currently active users',
|
||||
createdAt: new Date(),
|
||||
},
|
||||
{
|
||||
id: '8',
|
||||
role: 'assistant' as const,
|
||||
content: 'Here are the currently active users:',
|
||||
createdAt: new Date(),
|
||||
render: (
|
||||
<SqlSnippet
|
||||
id="active-users"
|
||||
title="Active Users"
|
||||
sql={`SELECT
|
||||
username,
|
||||
last_login,
|
||||
session_count
|
||||
FROM users
|
||||
WHERE last_active > now() - interval '24 hours'
|
||||
ORDER BY last_login DESC;`}
|
||||
mockData={{
|
||||
rows: [
|
||||
{
|
||||
username: 'alice_smith',
|
||||
last_login: '2024-01-15T15:30:00Z',
|
||||
session_count: 45,
|
||||
},
|
||||
{
|
||||
username: 'bob_jones',
|
||||
last_login: '2024-01-15T15:15:00Z',
|
||||
session_count: 32,
|
||||
},
|
||||
{
|
||||
username: 'carol_white',
|
||||
last_login: '2024-01-15T14:45:00Z',
|
||||
session_count: 28,
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Storage Usage',
|
||||
messages: [
|
||||
{
|
||||
id: '9',
|
||||
role: 'user' as const,
|
||||
content: 'What is our current storage usage?',
|
||||
createdAt: new Date(),
|
||||
},
|
||||
{
|
||||
id: '10',
|
||||
role: 'assistant' as const,
|
||||
content: 'Here is the storage usage by table:',
|
||||
createdAt: new Date(),
|
||||
render: (
|
||||
<SqlSnippet
|
||||
id="storage-usage"
|
||||
title="Database Storage Usage"
|
||||
sql={`SELECT
|
||||
schemaname,
|
||||
tablename,
|
||||
pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) as total_size
|
||||
FROM pg_tables
|
||||
WHERE schemaname = 'public'
|
||||
ORDER BY pg_total_relation_size(schemaname||'.'||tablename) DESC;`}
|
||||
mockData={{
|
||||
rows: [
|
||||
{
|
||||
schemaname: 'public',
|
||||
tablename: 'users',
|
||||
total_size: '1.2 GB',
|
||||
},
|
||||
{
|
||||
schemaname: 'public',
|
||||
tablename: 'orders',
|
||||
total_size: '856 MB',
|
||||
},
|
||||
{
|
||||
schemaname: 'public',
|
||||
tablename: 'products',
|
||||
total_size: '234 MB',
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Daily Page Views',
|
||||
messages: [
|
||||
{
|
||||
id: '11',
|
||||
role: 'user' as const,
|
||||
content: 'Show me daily page views for the last 2 weeks',
|
||||
createdAt: new Date(),
|
||||
},
|
||||
{
|
||||
id: '12',
|
||||
role: 'assistant' as const,
|
||||
content: 'Here are the daily page views:',
|
||||
createdAt: new Date(),
|
||||
render: (
|
||||
<SqlSnippet
|
||||
id="page-views"
|
||||
title="Daily Page Views"
|
||||
resultType="chart"
|
||||
sql={`SELECT
|
||||
date_trunc('day', timestamp) as day,
|
||||
count(*) as views
|
||||
FROM page_views
|
||||
WHERE timestamp > now() - interval '14 days'
|
||||
GROUP BY day
|
||||
ORDER BY day;`}
|
||||
mockData={[
|
||||
{ name: '2024-01-01', value: 15234 },
|
||||
{ name: '2024-01-02', value: 14567 },
|
||||
{ name: '2024-01-03', value: 16789 },
|
||||
{ name: '2024-01-04', value: 15678 },
|
||||
{ name: '2024-01-05', value: 17890 },
|
||||
{ name: '2024-01-06', value: 13456 },
|
||||
{ name: '2024-01-07', value: 12345 },
|
||||
{ name: '2024-01-08', value: 16543 },
|
||||
{ name: '2024-01-09', value: 18765 },
|
||||
{ name: '2024-01-10', value: 19876 },
|
||||
{ name: '2024-01-11', value: 20123 },
|
||||
{ name: '2024-01-12', value: 21234 },
|
||||
{ name: '2024-01-13', value: 19876 },
|
||||
{ name: '2024-01-14', value: 18765 },
|
||||
]}
|
||||
/>
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Error Distribution',
|
||||
messages: [
|
||||
{
|
||||
id: '1',
|
||||
role: 'user' as const,
|
||||
content: 'Show me the distribution of error types',
|
||||
createdAt: new Date(),
|
||||
},
|
||||
{
|
||||
id: '15',
|
||||
role: 'assistant' as const,
|
||||
content: 'Here is the distribution of error types:',
|
||||
createdAt: new Date(),
|
||||
render: (
|
||||
<SqlSnippet
|
||||
id="error-distribution"
|
||||
title="Error Distribution"
|
||||
resultType="chart"
|
||||
sql={`SELECT
|
||||
error_type,
|
||||
count(*) as error_count
|
||||
FROM error_logs
|
||||
GROUP BY error_type
|
||||
ORDER BY error_count DESC;`}
|
||||
mockData={[
|
||||
{ name: '404 Not Found', value: 1234 },
|
||||
{ name: '500 Server Error', value: 567 },
|
||||
{ name: '403 Forbidden', value: 456 },
|
||||
{ name: 'Rate Limit Exceeded', value: 345 },
|
||||
{ name: 'Database Timeout', value: 234 },
|
||||
{ name: 'Authentication Failed', value: 123 },
|
||||
{ name: 'Invalid Input', value: 98 },
|
||||
{ name: 'Network Error', value: 87 },
|
||||
{ name: 'API Error', value: 76 },
|
||||
{ name: 'Other', value: 45 },
|
||||
]}
|
||||
/>
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Database Performance',
|
||||
messages: [
|
||||
{
|
||||
id: '1',
|
||||
role: 'user' as const,
|
||||
content: 'Show me database performance metrics',
|
||||
createdAt: new Date(),
|
||||
},
|
||||
{
|
||||
id: '16',
|
||||
role: 'assistant' as const,
|
||||
content: 'Here are the current database performance metrics:',
|
||||
createdAt: new Date(),
|
||||
render: (
|
||||
<SqlSnippet
|
||||
id="db-performance"
|
||||
title="Database Performance"
|
||||
sql={`SELECT
|
||||
datname,
|
||||
numbackends,
|
||||
xact_commit,
|
||||
xact_rollback,
|
||||
blks_read,
|
||||
blks_hit,
|
||||
tup_returned,
|
||||
tup_fetched
|
||||
FROM pg_stat_database
|
||||
WHERE datname = current_database();`}
|
||||
mockData={{
|
||||
rows: [
|
||||
{
|
||||
datname: 'production_db',
|
||||
numbackends: 50,
|
||||
xact_commit: 1500000,
|
||||
xact_rollback: 1200,
|
||||
blks_read: 25000,
|
||||
blks_hit: 980000,
|
||||
tup_returned: 15000000,
|
||||
tup_fetched: 2500000,
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'API Usage Trends',
|
||||
messages: [
|
||||
{
|
||||
id: '17',
|
||||
role: 'user' as const,
|
||||
content: 'Show me API usage trends over time',
|
||||
createdAt: new Date(),
|
||||
},
|
||||
{
|
||||
id: '18',
|
||||
role: 'assistant' as const,
|
||||
content: 'Here are the API usage trends:',
|
||||
createdAt: new Date(),
|
||||
render: (
|
||||
<SqlSnippet
|
||||
id="api-usage"
|
||||
title="API Usage Trends"
|
||||
resultType="chart"
|
||||
sql={`SELECT
|
||||
date_trunc('hour', timestamp) as hour,
|
||||
count(*) as request_count
|
||||
FROM api_logs
|
||||
WHERE timestamp > now() - interval '24 hours'
|
||||
GROUP BY hour
|
||||
ORDER BY hour;`}
|
||||
mockData={[
|
||||
{ name: '00:00', value: 1200 },
|
||||
{ name: '02:00', value: 800 },
|
||||
{ name: '04:00', value: 600 },
|
||||
{ name: '06:00', value: 900 },
|
||||
{ name: '08:00', value: 2500 },
|
||||
{ name: '10:00', value: 3500 },
|
||||
{ name: '12:00', value: 4000 },
|
||||
{ name: '14:00', value: 3800 },
|
||||
{ name: '16:00', value: 3200 },
|
||||
{ name: '18:00', value: 2800 },
|
||||
{ name: '20:00', value: 2000 },
|
||||
{ name: '22:00', value: 1500 },
|
||||
]}
|
||||
/>
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
function Assistant() {
|
||||
const [incomingMessages, setIncomingMessages] = useState<Message[]>([])
|
||||
const isLoggedIn = useIsLoggedIn()
|
||||
const isUserLoading = useIsUserLoading()
|
||||
const meta_title = 'AI | Supabase'
|
||||
const meta_description = 'Build AI-powered applications with Supabase'
|
||||
const router = useRouter()
|
||||
const { query } = router
|
||||
|
||||
useEffect(() => {
|
||||
if (incomingMessages.length === 0) {
|
||||
const timeoutId = setTimeout(() => {
|
||||
const isFromProductHunt = query.ref === 'producthunt'
|
||||
const initialMessages = isFromProductHunt
|
||||
? [...welcomeMessages, ...demoQueries[0].messages]
|
||||
: demoQueries[Math.floor(Math.random() * demoQueries.length)].messages
|
||||
|
||||
setIncomingMessages(initialMessages)
|
||||
}, 3000)
|
||||
return () => clearTimeout(timeoutId)
|
||||
}
|
||||
}, [query.ref])
|
||||
|
||||
const handleNewMessage = (messages: Message[]) => {
|
||||
setIncomingMessages(messages.map((message) => ({ ...message, id: Date.now().toString() })))
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<NextSeo
|
||||
title={meta_title}
|
||||
description={meta_description}
|
||||
openGraph={{
|
||||
title: meta_title,
|
||||
description: meta_description,
|
||||
url: `https://supabase.com/ai`,
|
||||
}}
|
||||
/>
|
||||
<DefaultLayout className="lg:h-screen min-h-screen overflow-hidden">
|
||||
<SectionContainer className="h-full lg:pt-6 relative">
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1, transition: { delay: 1, duration: 5 } }}
|
||||
className="absolute inset-0 hidden lg:block left-1/3"
|
||||
>
|
||||
<DotGrid rows={30} columns={30} count={900} />
|
||||
</motion.div>
|
||||
<div className="h-full grid grid-cols-12 gap-8 items-center">
|
||||
{/* Left Column */}
|
||||
<div className="col-span-12 lg:col-span-6 relative z-10">
|
||||
{/* Main content */}
|
||||
<div className="mb-6 flex flex-col gap-4 max-w-lg">
|
||||
<h1 className="text-4xl sm:text-5xl sm:leading-none">Chat with Postgres</h1>
|
||||
<p className="p text-lg !m-0">
|
||||
Generate, run and debug queries, chart your data, create functions, policies and
|
||||
more. The assistant is here to help.
|
||||
</p>
|
||||
<div className="min-h-12">
|
||||
{!isUserLoading &&
|
||||
(isLoggedIn ? (
|
||||
<Button type="primary" size="medium" asChild>
|
||||
<Link href="/dashboard/projects">Dashboard</Link>
|
||||
</Button>
|
||||
) : (
|
||||
<Button type="primary" size="medium" asChild>
|
||||
<Link href="https://supabase.com/dashboard">Start your project</Link>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Grid of secondary buttons */}
|
||||
<div>
|
||||
<h2 className="font-mono text-foreground-lighter text-sm mb-4">Try me</h2>
|
||||
<div className="flex flex-wrap items-start gap-2 min-h-24">
|
||||
<motion.div
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
variants={{
|
||||
visible: {
|
||||
transition: {
|
||||
staggerChildren: 0.05,
|
||||
},
|
||||
},
|
||||
}}
|
||||
className="flex flex-wrap gap-2"
|
||||
>
|
||||
{demoQueries.map((query, i) => (
|
||||
<motion.div
|
||||
key={i}
|
||||
variants={{
|
||||
hidden: { opacity: 0, y: 10 },
|
||||
visible: { opacity: 1, y: 0 },
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
className="rounded-full"
|
||||
type="default"
|
||||
onClick={() => handleNewMessage(query.messages)}
|
||||
>
|
||||
{query.label}
|
||||
</Button>
|
||||
</motion.div>
|
||||
))}
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Column */}
|
||||
<motion.div className="col-span-12 relative lg:col-span-6 h-[500px] lg:h-full justify-center flex items-center">
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1, transition: { delay: 1, duration: 5 } }}
|
||||
className="absolute w-screen bottom-0 -top-28 -inset-x-[calc(((100vw-480px)/2)+24px)] sm:-inset-x-[calc(((100vw-640px)/2)+24px)] md:-inset-x-[calc(((100vw-768px)/2)+24px)] lg:hidden"
|
||||
>
|
||||
<DotGrid rows={30} columns={30} count={900} />
|
||||
</motion.div>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 1.04, filter: 'blur(5px)' }}
|
||||
animate={{
|
||||
opacity: 1,
|
||||
scale: 1,
|
||||
filter: 'blur(0px)',
|
||||
transition: { duration: 1.5, ease: EASE_OUT, delay: 0.5 },
|
||||
}}
|
||||
className="w-full lg:max-w-[400px] h-full max-h-[700px] relative overflow-hidden"
|
||||
>
|
||||
<motion.div
|
||||
initial={{ x: '-100%' }}
|
||||
animate={{ x: '200%' }}
|
||||
transition={{
|
||||
duration: 0.5,
|
||||
ease: 'easeInOut',
|
||||
delay: 0.5,
|
||||
}}
|
||||
className="absolute inset-0 bg-gradient-to-r from-transparent via-background-muted/40 to-transparent transform -skew-x-12 z-10"
|
||||
/>
|
||||
<AIDemoPanel incomingMessages={incomingMessages} />
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</SectionContainer>
|
||||
</DefaultLayout>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Assistant
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 567 KiB |
Reference in new issue
Block a user