mirror of
https://github.com/supabase/supabase.git
synced 2026-10-17 07:09:07 +03:00
preview tab works now
This commit is contained in:
1 parent
7ecd0ff90d
commit
6aafe41eee
5 files changed
+529
-91
No files matched your search
@@ -22,10 +22,10 @@ export const generateToolRoutes = (ref?: string, project?: Project): Route[] =>
|
||||
|
||||
return [
|
||||
{
|
||||
key: 'editor',
|
||||
key: 'explorer',
|
||||
label: 'Explorer',
|
||||
icon: <PackageSearch size={ICON_SIZE} strokeWidth={ICON_STROKE_WIDTH} />,
|
||||
link: ref && (isProjectBuilding ? buildingUrl : `/project/${ref}/editor`),
|
||||
link: ref && (isProjectBuilding ? buildingUrl : `/project/${ref}/explorer`),
|
||||
linkElement: <EditorIndexPageLink projectRef={ref} />,
|
||||
},
|
||||
// {
|
||||
|
||||
@@ -68,9 +68,8 @@ export const EmptyStatePanel = ({
|
||||
)}
|
||||
{aiButtonText && (
|
||||
<Button
|
||||
type="default"
|
||||
icon={<Sparkles className="w-4 h-4 text-white" />}
|
||||
className="!bg-[#6366f1] hover:!bg-[#6366f1]/90 text-white border border-[#6366f1]"
|
||||
type="primary"
|
||||
icon={<Sparkles className="w-4 h-4" />}
|
||||
onClick={onAiButtonClick}
|
||||
>
|
||||
{aiButtonText}
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
|
||||
|
||||
import { useTablesQuery } from 'data/tables/tables-query'
|
||||
import { useSchemasQuery } from './schemas-query'
|
||||
|
||||
/**
|
||||
* Hook to determine if Postgres is being used in a project.
|
||||
* Postgres is considered "in use" if either:
|
||||
* 1. Custom schemas exist (schemas other than 'public')
|
||||
* 2. The public schema contains tables or views
|
||||
*/
|
||||
const DEFAULT_SCHEMAS = [
|
||||
'public',
|
||||
'graphql',
|
||||
'graphql_public',
|
||||
'vault',
|
||||
'pgsodium_masks',
|
||||
'pgsodium',
|
||||
'auth',
|
||||
'storage',
|
||||
'realtime',
|
||||
'extensions',
|
||||
'pgbouncer',
|
||||
]
|
||||
|
||||
export const usePostgresInUse = () => {
|
||||
const { project } = useProjectContext()
|
||||
|
||||
// Query for all schemas in the project
|
||||
const {
|
||||
data: schemas,
|
||||
isLoading: isLoadingSchemas,
|
||||
isError: isSchemasError,
|
||||
error: schemasError,
|
||||
isSuccess: isSchemasSuccess,
|
||||
refetch: refetchSchemas,
|
||||
} = useSchemasQuery({
|
||||
projectRef: project?.ref,
|
||||
connectionString: project?.connectionString,
|
||||
})
|
||||
|
||||
// Query for tables specifically in the public schema
|
||||
const {
|
||||
data: tables,
|
||||
isLoading: isLoadingTables,
|
||||
isError: isTablesError,
|
||||
error: tablesError,
|
||||
isSuccess: isTablesSuccess,
|
||||
refetch: refetchTables,
|
||||
} = useTablesQuery({
|
||||
projectRef: project?.ref,
|
||||
connectionString: project?.connectionString,
|
||||
schema: 'public',
|
||||
})
|
||||
|
||||
console.log('Schemas:', schemas)
|
||||
console.log('Tables:', tables)
|
||||
|
||||
// Debug what we're getting
|
||||
console.log('Tables query result:', {
|
||||
isLoadingTables,
|
||||
tableCount: tables?.length,
|
||||
firstFewTables: tables?.slice(0, 3),
|
||||
})
|
||||
|
||||
// Check if any schemas exist other than 'public'
|
||||
const hasCustomSchemas = (schemas ?? []).some((schema) => !DEFAULT_SCHEMAS.includes(schema.name))
|
||||
|
||||
// Check if the public schema has any tables or views
|
||||
const hasPublicSchemaObjects = (tables ?? []).length > 0
|
||||
|
||||
return {
|
||||
// Computed values indicating Postgres usage
|
||||
isInUse: hasCustomSchemas || hasPublicSchemaObjects,
|
||||
hasCustomSchemas,
|
||||
hasPublicSchemaObjects,
|
||||
|
||||
// Combined states from both queries
|
||||
isLoading: isLoadingSchemas || isLoadingTables,
|
||||
isError: isSchemasError || isTablesError,
|
||||
error: schemasError || tablesError,
|
||||
isSuccess: isSchemasSuccess && isTablesSuccess,
|
||||
|
||||
// Individual query states
|
||||
schemas: {
|
||||
data: schemas,
|
||||
isLoading: isLoadingSchemas,
|
||||
isError: isSchemasError,
|
||||
error: schemasError,
|
||||
isSuccess: isSchemasSuccess,
|
||||
refetch: refetchSchemas,
|
||||
},
|
||||
tables: {
|
||||
data: tables,
|
||||
isLoading: isLoadingTables,
|
||||
isError: isTablesError,
|
||||
error: tablesError,
|
||||
isSuccess: isTablesSuccess,
|
||||
refetch: refetchTables,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
import { EmptyStatePanel } from 'components/layouts/explorer/empty-state-panel'
|
||||
import { ExplorerLayout } from 'components/layouts/explorer/layout'
|
||||
import { ProjectContextFromParamsProvider } from 'components/layouts/ProjectLayout/ProjectContext'
|
||||
import { Sparkles } from 'lucide-react'
|
||||
import type { NextPageWithLayout } from 'types'
|
||||
import { Separator } from 'ui'
|
||||
import { motion, Variants } from 'framer-motion'
|
||||
|
||||
const ExplorerPage: NextPageWithLayout = () => {
|
||||
const EXPLORER_PANELS = [
|
||||
{
|
||||
title: `Create a table`,
|
||||
description: `Write and execute SQL queries in an interactive editor with syntax highlighting and autocompletion.`,
|
||||
buttonText: 'Open SQL Editor',
|
||||
aiButtonText: 'Generate SQL',
|
||||
},
|
||||
{
|
||||
title: `Write SQL`,
|
||||
description: `Browse and manage your database with an intuitive interface. Create and modify tables, manage relationships, and edit data directly.`,
|
||||
buttonText: 'Open Table Editor',
|
||||
aiButtonText: 'Generate Schema',
|
||||
},
|
||||
]
|
||||
|
||||
const containerVariants: Variants = {
|
||||
hidden: { opacity: 0, y: 20 },
|
||||
show: {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
transition: {
|
||||
type: 'spring',
|
||||
stiffness: 600,
|
||||
damping: 50,
|
||||
staggerChildren: 0.08,
|
||||
delayChildren: 0.03,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const itemVariants: Variants = {
|
||||
hidden: { opacity: 0, scale: 0.96, y: 15 },
|
||||
show: {
|
||||
opacity: 1,
|
||||
scale: 1,
|
||||
y: 0,
|
||||
transition: {
|
||||
type: 'spring',
|
||||
stiffness: 600,
|
||||
damping: 50,
|
||||
staggerChildren: 0.08,
|
||||
delayChildren: 0.03,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<motion.div
|
||||
className="flex flex-col items-center justify-center h-full w-full max-w-3xl mx-auto px-5 gap-12"
|
||||
initial="hidden"
|
||||
animate="show"
|
||||
variants={containerVariants}
|
||||
>
|
||||
<motion.div
|
||||
className="w-full bg-surface-200 dark:bg-surface-100 rounded-lg overflow-hidden relative border border-muted"
|
||||
variants={itemVariants}
|
||||
>
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-background-surface-200 via-background-surface-200 to-transparent"></div>
|
||||
{[...Array(5)].map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="absolute inset-y-0 right-0 max-h-[300px]"
|
||||
style={{
|
||||
width: '70%',
|
||||
background: `linear-gradient(135deg,
|
||||
rgba(229, 231, 235, ${0.3 - index * 0.05}) 0%,
|
||||
rgba(209, 213, 219, ${0.3 - index * 0.05}) 50%,
|
||||
transparent 100%)`,
|
||||
transform: `skew(-12deg) translateX(${index * 15}%)`,
|
||||
maskImage: 'linear-gradient(to bottom, black 0%, black 50%, transparent 100%)',
|
||||
WebkitMaskImage: 'linear-gradient(to bottom, black 0%, black 40%, transparent 85%)',
|
||||
boxShadow: `-1px 0 2px rgba(0, 0, 0, 0.03)`,
|
||||
}}
|
||||
></div>
|
||||
))}
|
||||
<div className="relative z-10 p-8 text-gray-700 h-full flex flex-col gap-5">
|
||||
<div className="flex flex-col gap-5">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="inline-flex items-center justify-center w-8 h-8 rounded-lg bg-[#6366f1]/10">
|
||||
<Sparkles size={14} className="text-[#6366f1]" />
|
||||
</div>
|
||||
<h2 className="text-base text-foreground">AI Assistant</h2>
|
||||
</div>
|
||||
<p className="text-sm text-foreground-light max-w-md">
|
||||
This panel features geometric shapes on the right side, each with a distinct shade
|
||||
and reduced opacity, creating a subtle fade-out effect.
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-xs text-foreground-lighter max-w-md">
|
||||
The shapes now have a more delicate gradation, transitioning smoothly from slightly
|
||||
darker to lighter and more transparent.
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
<Separator />
|
||||
|
||||
<motion.div className="flex gap-16" variants={itemVariants}>
|
||||
{EXPLORER_PANELS.map((data, index) => (
|
||||
<motion.div
|
||||
key={data.title}
|
||||
variants={itemVariants}
|
||||
transition={{ delay: 0.2 + index * 0.1 }}
|
||||
>
|
||||
<EmptyStatePanel {...data} />
|
||||
</motion.div>
|
||||
))}
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
className="w-full rounded-lg overflow-hidden relative border border-muted"
|
||||
variants={itemVariants}
|
||||
>
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-background-surface-200 via-background-surface-200 to-transparent"></div>
|
||||
<div className="relative z-10 p-8 text-gray-700 h-full flex flex-col gap-5">
|
||||
<div className="flex flex-col gap-5">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="inline-flex items-center justify-center w-8 h-8 rounded-lg bg-[#6366f1]/10">
|
||||
<Sparkles size={14} className="text-[#6366f1]" />
|
||||
</div>
|
||||
<h2 className="text-base text-foreground">Migrate to Supabase</h2>
|
||||
</div>
|
||||
<p className="text-sm text-foreground-light max-w-md">
|
||||
This panel features geometric shapes on the right side, each with a distinct shade
|
||||
and reduced opacity, creating a subtle fade-out effect.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
ExplorerPage.getLayout = (page) => (
|
||||
<ProjectContextFromParamsProvider>
|
||||
<ExplorerLayout hideTabs>{page}</ExplorerLayout>
|
||||
</ProjectContextFromParamsProvider>
|
||||
)
|
||||
|
||||
export default ExplorerPage
|
||||
@@ -1,27 +1,93 @@
|
||||
import { EmptyStatePanel } from 'components/layouts/explorer/empty-state-panel'
|
||||
import { ExplorerLayout } from 'components/layouts/explorer/layout'
|
||||
import { ProjectContextFromParamsProvider } from 'components/layouts/ProjectLayout/ProjectContext'
|
||||
import { Sparkles } from 'lucide-react'
|
||||
import { Sparkles, Box, X, Move, BaggageClaim, BookOpen } from 'lucide-react'
|
||||
import type { NextPageWithLayout } from 'types'
|
||||
import { Separator } from 'ui'
|
||||
import { motion, Variants } from 'framer-motion'
|
||||
import { Button, Separator, Skeleton } from 'ui'
|
||||
import { motion, Variants, LayoutGroup, AnimatePresence } from 'framer-motion'
|
||||
import { useEffect, useState, useRef } from 'react'
|
||||
import { AssistantChatForm } from 'ui-patterns/AssistantChat'
|
||||
import { usePostgresInUse } from 'data/database/postgres-in-use-query'
|
||||
|
||||
interface AssistantChatDemoProps {
|
||||
value: string
|
||||
setValueState: (value: string) => void
|
||||
onSubmit: () => void
|
||||
}
|
||||
|
||||
function AssistantChatDemo({ value, setValueState, onSubmit }: AssistantChatDemoProps) {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const inputRef = useRef<HTMLTextAreaElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.focus()
|
||||
}
|
||||
}, [value])
|
||||
|
||||
return (
|
||||
<AssistantChatForm
|
||||
textAreaRef={inputRef}
|
||||
placeholder="Ask me anything..."
|
||||
icon={<Box strokeWidth={1.5} size={24} className="text-foreground-muted" />}
|
||||
value={value}
|
||||
loading={loading}
|
||||
disabled={loading}
|
||||
onValueChange={(e) => setValueState(e.target.value)}
|
||||
onSubmit={async (event) => {
|
||||
event.preventDefault()
|
||||
setLoading(true)
|
||||
setTimeout(() => {
|
||||
setLoading(false)
|
||||
onSubmit()
|
||||
}, 1500)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const ExplorerPage: NextPageWithLayout = () => {
|
||||
const EXPLORER_PANELS = [
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [input, setInput] = useState('')
|
||||
const [showRightPanel, setShowRightPanel] = useState(false)
|
||||
const inputRef = useRef<HTMLTextAreaElement>(null)
|
||||
|
||||
const { isInUse: isPostgresInUse, isLoading: isLoadingCheckingPostgres } = usePostgresInUse()
|
||||
|
||||
// Panels to show when Postgres is not in use yet
|
||||
const EMPTY_POSTGRES_PANELS = [
|
||||
{
|
||||
title: `Create a table`,
|
||||
description: `Write and execute SQL queries in an interactive editor with syntax highlighting and autocompletion.`,
|
||||
buttonText: 'Open SQL Editor',
|
||||
aiButtonText: 'Generate SQL',
|
||||
title: `Create your first table`,
|
||||
description: `Create tables in your Postgres database to store and organize your data.`,
|
||||
buttonText: 'Create Table',
|
||||
aiButtonText: 'Ask AI',
|
||||
},
|
||||
{
|
||||
title: `Write SQL`,
|
||||
description: `Browse and manage your database with an intuitive interface. Create and modify tables, manage relationships, and edit data directly.`,
|
||||
buttonText: 'Open Table Editor',
|
||||
aiButtonText: 'Generate Schema',
|
||||
title: `Write your first SQL query`,
|
||||
description: `Write SQL to execute on your Postgres database.`,
|
||||
buttonText: 'Create your first Query',
|
||||
aiButtonText: 'Ask AI',
|
||||
},
|
||||
]
|
||||
|
||||
// Panels to show when Postgres is already in use
|
||||
const POSTGRES_IN_USE_PANELS = [
|
||||
{
|
||||
title: 'Query your data',
|
||||
description: 'Write and execute SQL queries to explore and analyze your Postgres database.',
|
||||
buttonText: 'Create query',
|
||||
aiButtonText: 'Ask AI',
|
||||
},
|
||||
{
|
||||
title: 'Create table',
|
||||
description: 'Add new tables to your database schema.',
|
||||
buttonText: 'Open Table Editor',
|
||||
aiButtonText: 'Ask AI',
|
||||
},
|
||||
]
|
||||
|
||||
const panels = isPostgresInUse ? POSTGRES_IN_USE_PANELS : EMPTY_POSTGRES_PANELS
|
||||
|
||||
const containerVariants: Variants = {
|
||||
hidden: { opacity: 0, y: 20 },
|
||||
show: {
|
||||
@@ -53,91 +119,212 @@ const ExplorerPage: NextPageWithLayout = () => {
|
||||
},
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => setIsLoading(false), 800)
|
||||
return () => clearTimeout(timer)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<LayoutGroup>
|
||||
<motion.div
|
||||
className="flex flex-col items-center justify-center h-full w-full max-w-3xl mx-auto px-5 gap-12"
|
||||
className="flex h-full w-full"
|
||||
initial="hidden"
|
||||
animate="show"
|
||||
variants={containerVariants}
|
||||
>
|
||||
<motion.div
|
||||
className="w-full bg-surface-200 dark:bg-surface-100 rounded-lg overflow-hidden relative border border-muted"
|
||||
variants={itemVariants}
|
||||
>
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-background-surface-200 via-background-surface-200 to-transparent"></div>
|
||||
{[...Array(5)].map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="absolute inset-y-0 right-0 max-h-[300px]"
|
||||
style={{
|
||||
width: '70%',
|
||||
background: `linear-gradient(135deg,
|
||||
rgba(229, 231, 235, ${0.3 - index * 0.05}) 0%,
|
||||
rgba(209, 213, 219, ${0.3 - index * 0.05}) 50%,
|
||||
transparent 100%)`,
|
||||
transform: `skew(-12deg) translateX(${index * 15}%)`,
|
||||
maskImage: 'linear-gradient(to bottom, black 0%, black 50%, transparent 100%)',
|
||||
WebkitMaskImage: 'linear-gradient(to bottom, black 0%, black 40%, transparent 85%)',
|
||||
boxShadow: `-1px 0 2px rgba(0, 0, 0, 0.03)`,
|
||||
}}
|
||||
></div>
|
||||
))}
|
||||
<div className="relative z-10 p-8 text-gray-700 h-full flex flex-col gap-5">
|
||||
<div className="flex flex-col gap-5">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="inline-flex items-center justify-center w-8 h-8 rounded-lg bg-[#6366f1]/10">
|
||||
<Sparkles size={14} className="text-[#6366f1]" />
|
||||
</div>
|
||||
<h2 className="text-base text-foreground">AI Assistant</h2>
|
||||
</div>
|
||||
<p className="text-sm text-foreground-light max-w-md">
|
||||
This panel features geometric shapes on the right side, each with a distinct shade
|
||||
and reduced opacity, creating a subtle fade-out effect.
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-xs text-foreground-lighter max-w-md">
|
||||
The shapes now have a more delicate gradation, transitioning smoothly from slightly
|
||||
darker to lighter and more transparent.
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
<Separator />
|
||||
|
||||
<motion.div className="flex gap-16" variants={itemVariants}>
|
||||
{EXPLORER_PANELS.map((data, index) => (
|
||||
<motion.div
|
||||
key={data.title}
|
||||
variants={itemVariants}
|
||||
transition={{ delay: 0.2 + index * 0.1 }}
|
||||
>
|
||||
<EmptyStatePanel {...data} />
|
||||
<motion.div className="flex-1 flex flex-col items-center justify-center max-w-3xl mx-auto px-5 gap-12">
|
||||
<motion.div className="grid grid-cols-2 gap-16 w-full" variants={itemVariants}>
|
||||
{isLoadingCheckingPostgres ? (
|
||||
<>
|
||||
{[...Array(2)].map((_, i) => (
|
||||
<motion.div
|
||||
key={i}
|
||||
variants={itemVariants}
|
||||
transition={{ delay: 0.2 + i * 0.1 }}
|
||||
className="w-full flex flex-col gap-4"
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
<Skeleton className="w-8 h-8 rounded-lg" />
|
||||
<Skeleton className="h-4 w-24 rounded" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-3 w-full rounded" />
|
||||
<Skeleton className="h-3 w-3/4 rounded" />
|
||||
</div>
|
||||
<div className="flex gap-2 mt-auto">
|
||||
<Skeleton className="h-8 w-24 rounded" />
|
||||
<Skeleton className="h-8 w-32 rounded" />
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
panels.map((data, index) => (
|
||||
<motion.div
|
||||
key={data.title}
|
||||
variants={itemVariants}
|
||||
transition={{ delay: 0.2 + index * 0.1 }}
|
||||
>
|
||||
<EmptyStatePanel {...data} />
|
||||
</motion.div>
|
||||
))
|
||||
)}
|
||||
</motion.div>
|
||||
{/* Migration section - only show if Postgres is not in use */}
|
||||
{isLoadingCheckingPostgres ? (
|
||||
<motion.div className="w-full" variants={itemVariants}>
|
||||
<Skeleton className="w-full h-[160px] rounded-lg" />
|
||||
</motion.div>
|
||||
))}
|
||||
) : (
|
||||
!isPostgresInUse && (
|
||||
<motion.div
|
||||
className="w-full rounded-lg overflow-hidden relative border border-muted"
|
||||
variants={itemVariants}
|
||||
>
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-background-surface-200 via-background-surface-200 to-transparent"></div>
|
||||
<div className="relative z-10 px-5 py-4 h-full flex gap-5 items-center justify-between">
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex gap-4">
|
||||
<div className="inline-flex items-center justify-center w-7 h-7 rounded-lg bg-surface-300 border">
|
||||
<BaggageClaim
|
||||
size={14}
|
||||
className="text-foreground-light"
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<h2 className="text-base text-foreground">
|
||||
Migrate your Database to Postgres
|
||||
</h2>
|
||||
<p className="text-xs text-foreground-light max-w-md">
|
||||
Already have a database? Migrate any database to Supabase to access
|
||||
backups, RESTful auto APIs, Authentication and more.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button type="default" className="rounded-full" iconRight={<BookOpen />}>
|
||||
Migrate to Postgres
|
||||
</Button>
|
||||
</div>
|
||||
</motion.div>
|
||||
)
|
||||
)}
|
||||
{!showRightPanel && <Separator />}
|
||||
<AnimatePresence>
|
||||
{!showRightPanel && (
|
||||
<motion.div
|
||||
variants={itemVariants}
|
||||
className="w-full"
|
||||
initial={{ height: 0, opacity: 0 }}
|
||||
animate={{ height: 'auto', opacity: 1 }}
|
||||
exit={{ height: 0, opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<motion.div className="flex flex-col gap-3">
|
||||
<motion.p
|
||||
className="text-sm text-foreground-light"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: showRightPanel ? 0 : 1 }}
|
||||
transition={{ delay: 0.3 }}
|
||||
>
|
||||
Try these example prompts:
|
||||
</motion.p>
|
||||
{isLoading ? (
|
||||
<>
|
||||
{[...Array(4)].map((_, i) => (
|
||||
<motion.div
|
||||
key={i}
|
||||
className="h-6 bg-foreground/10 rounded animate-pulse"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 0.2 * i }}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
[
|
||||
'Create a table to store user profiles with basic information',
|
||||
'Help me write a query to find the most active users in the last 30 days',
|
||||
'Show me how to implement row level security for a multi-tenant application',
|
||||
'Generate a function to calculate the distance between two geographic points',
|
||||
].map((prompt, i) => (
|
||||
<motion.div
|
||||
key={prompt}
|
||||
className="text-sm text-foreground-light hover:text-foreground cursor-pointer"
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{
|
||||
opacity: showRightPanel ? 0 : 1,
|
||||
x: showRightPanel ? -20 : 0,
|
||||
}}
|
||||
transition={{
|
||||
delay: showRightPanel ? 0 : 0.4 + i * 0.1,
|
||||
duration: 0.2,
|
||||
}}
|
||||
onClick={() => {
|
||||
setInput(prompt)
|
||||
inputRef.current?.focus()
|
||||
}}
|
||||
>
|
||||
"{prompt}"
|
||||
</motion.div>
|
||||
))
|
||||
)}
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
<motion.div
|
||||
variants={itemVariants}
|
||||
className="w-full"
|
||||
layout
|
||||
layoutId="chat-input"
|
||||
key="chat-input"
|
||||
>
|
||||
<AssistantChatDemo
|
||||
value={input}
|
||||
setValueState={setInput}
|
||||
onSubmit={() => setShowRightPanel(true)}
|
||||
/>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
className="w-full rounded-lg overflow-hidden relative border border-muted"
|
||||
variants={itemVariants}
|
||||
>
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-background-surface-200 via-background-surface-200 to-transparent"></div>
|
||||
<div className="relative z-10 p-8 text-gray-700 h-full flex flex-col gap-5">
|
||||
<div className="flex flex-col gap-5">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="inline-flex items-center justify-center w-8 h-8 rounded-lg bg-[#6366f1]/10">
|
||||
<Sparkles size={14} className="text-[#6366f1]" />
|
||||
</div>
|
||||
<h2 className="text-base text-foreground">Migrate to Supabase</h2>
|
||||
</div>
|
||||
<p className="text-sm text-foreground-light max-w-md">
|
||||
This panel features geometric shapes on the right side, each with a distinct shade
|
||||
and reduced opacity, creating a subtle fade-out effect.
|
||||
</p>
|
||||
{showRightPanel && (
|
||||
<motion.div
|
||||
className="w-[400px] border-l border-muted h-full bg-surface-100 flex flex-col"
|
||||
initial={{ x: 400, opacity: 0 }}
|
||||
animate={{ x: 0, opacity: 1 }}
|
||||
exit={{ x: 400, opacity: 0 }}
|
||||
>
|
||||
<div className="flex justify-between items-center p-4 border-b border-muted">
|
||||
<h3 className="text-sm font-medium">AI Assistant</h3>
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowRightPanel(false)
|
||||
setInput('')
|
||||
}}
|
||||
className="p-1 hover:bg-surface-200 rounded-md"
|
||||
>
|
||||
<X size={16} className="text-foreground-light" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<div className="flex-1">{/* Chat content will go here */}</div>
|
||||
|
||||
<motion.div
|
||||
layout
|
||||
layoutId="chat-input"
|
||||
key="chat-input"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
className="p-4 border-t border-muted"
|
||||
>
|
||||
<AssistantChatDemo value={input} setValueState={setInput} onSubmit={() => {}} />
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
)}
|
||||
</motion.div>
|
||||
</>
|
||||
</LayoutGroup>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user