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:
Saxon FletcherandFrancesco Sansalvadore authored and GitHub committed 2024-12-02 16:59:48 +10:00
1 parent 16e002d8fd
commit 1b2d50cd51
6 files changed
+1458

No files matched your search

+70
View File
@@ -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
+227
View File
@@ -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>
)
}
+240
View File
@@ -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;
}
}
+808
View File
@@ -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