feat(docs): ai prompts section (#30342)

Co-authored-by: Francesco Sansalvadore <f.sansalvadore@gmail.com>
This commit is contained in:
CharisandFrancesco Sansalvadore authored and GitHub committed 2024-11-26 13:43:04 -05:00
1 parent 72910fc30f
commit 2fb8b9f74c
18 files changed
+438 -50

No files matched your search

@@ -0,0 +1,96 @@
import type { Code, Heading, Root } from 'mdast'
import { fromMarkdown } from 'mdast-util-from-markdown'
import { toMarkdown } from 'mdast-util-to-markdown'
import { readdir, readFile, stat } from 'node:fs/promises'
import { basename, extname, join } from 'node:path'
import { cache } from 'react'
import { visit, EXIT } from 'unist-util-visit'
import { EXAMPLES_DIRECTORY } from '~/lib/docs'
const PROMPTS_DIRECTORY = join(EXAMPLES_DIRECTORY, 'prompts')
function parseMarkdown(markdown: string) {
const mdast = fromMarkdown(markdown)
let heading = ''
visit(mdast, 'heading', (node: Heading) => {
if (node.depth === 1) {
if ('value' in node.children[0]) {
heading = node.children[0].value
}
return EXIT
}
})
const codeBlock: Code = {
type: 'code',
lang: 'markdown',
value: markdown,
}
const root: Root = {
type: 'root',
children: [codeBlock],
}
const content = toMarkdown(root)
return { heading, content }
}
async function getAiPromptsImpl() {
const directoryContents = await readdir(PROMPTS_DIRECTORY)
const prompts = directoryContents
.filter(async (file) => {
if (extname(file) !== '.md') {
return false
}
const fileStats = await stat(join(PROMPTS_DIRECTORY, file))
const isFile = fileStats.isFile()
return isFile
})
.map(async (filename) => {
const rawContent = await readFile(join(PROMPTS_DIRECTORY, filename), 'utf-8')
const { heading, content } = parseMarkdown(rawContent)
return {
filename: basename(filename, '.md'),
heading,
content,
}
})
return (await Promise.all(prompts)).sort((a, b) => b.filename.localeCompare(a.filename))
}
export const getAiPrompts = cache(getAiPromptsImpl)
async function getAiPromptImpl(prompt: string) {
const filePath = join(PROMPTS_DIRECTORY, `${prompt}.md`)
try {
const rawContent = await readFile(filePath, 'utf-8')
const { heading, content } = parseMarkdown(rawContent)
return { heading, content }
} catch (err) {
console.error('Failed to fetch prompt from repo: %o', err)
}
}
export const getAiPrompt = cache(getAiPromptImpl)
export async function generateAiPromptMetadata({ params: { slug } }: { params: { slug: string } }) {
const prompt = await getAiPrompt(slug)
return {
title: `AI Prompt: ${prompt.heading} | Supabase Docs`,
}
}
export async function generateAiPromptsStaticParams() {
const prompts = await getAiPrompts()
return prompts.map((prompt) => {
return {
slug: prompt.filename,
}
})
}
@@ -0,0 +1,26 @@
import Link from 'next/link'
import { GlassPanel } from 'ui-patterns'
import { getAiPrompts } from './AiPrompts.utils'
export async function AiPromptsIndex() {
const prompts = await getAiPrompts()
return (
<div className="grid grid-cols-[repeat(auto-fit,minmax(200px,1fr))] gap-6 not-prose">
{prompts.map((prompt) => (
<Link
key={prompt.filename}
href={`/guides/getting-started/ai-prompts/${prompt.filename}`}
passHref
>
<GlassPanel
key={prompt.filename}
title={prompt.heading ?? prompt.filename.replaceAll('-', ' ')}
/>
</Link>
))}
</div>
)
}
@@ -0,0 +1,34 @@
import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template'
import {
generateAiPromptMetadata,
generateAiPromptsStaticParams,
getAiPrompt,
} from './AiPrompts.utils'
export const dynamicParams = false
export default async function AiPromptsPage({ params: { slug } }: { params: { slug: string } }) {
let { heading, content } = await getAiPrompt(slug)
content = `
## How to use
Copy the prompt to a file in your repo.
Use the "include file" feature from your AI tool to include the prompt when chatting with your AI assistant. For example, with GitHub Copilot, use \`#<filename>\`, in Cursor, use \`@Files\`, and in Zed, use \`/file\`.
## Prompt
${content}
`.trim()
return (
<GuideTemplate
meta={{ title: `API Prompt: ${heading}` }}
content={content}
editLink={newEditLink(`supabase/supabase/blob/master/examples/prompts/${slug}.md`)}
/>
)
}
export const generateMetadata = generateAiPromptMetadata
export const generateStaticParams = generateAiPromptsStaticParams
+32 -8
View File
@@ -2,25 +2,41 @@ import { cache, type PropsWithChildren } from 'react'
import { IS_PLATFORM } from 'common'
import { supabaseMisc } from '~/lib/supabaseMisc'
import { NavMenuSection } from '~/components/Navigation/Navigation.types'
import Layout from '~/layouts/guides'
import { supabaseMisc } from '~/lib/supabaseMisc'
import { getAiPrompts } from './getting-started/ai-prompts/[slug]/AiPrompts.utils'
// Revalidate occasionally to pick up changes to partners
// 60 seconds/minute * 60 minutes/hour * 24 hours/day
export const revalidate = 86_400
const GuidesLayout = async ({ children }: PropsWithChildren) => {
const partners = IS_PLATFORM ? await getPartners() : []
const partnerNavItems = partners.map((partner) => ({
name: partner.title,
url: `https://supabase.com/partners/integrations/${partner.slug}` as `https://${string}`,
}))
const [partnerNavItems, promptNavItems] = await Promise.all([getPartners(), getPrompts()])
return <Layout additionalNavItems={partnerNavItems}>{children}</Layout>
const additionalNavItems =
partnerNavItems.length > 0 || promptNavItems.length > 0
? { integrations: partnerNavItems, prompts: promptNavItems }
: undefined
return <Layout additionalNavItems={additionalNavItems}>{children}</Layout>
}
async function getPrompts() {
const prompts = await getAiPrompts()
return prompts.map(
(prompt) =>
({
name: prompt.heading,
url: `/guides/getting-started/ai-prompts/${prompt.filename}`,
}) as Partial<NavMenuSection>
)
}
const getPartners = cache(getPartnersImpl)
async function getPartnersImpl() {
if (!IS_PLATFORM) return []
const { data, error } = await supabaseMisc()
.from('partners')
.select('slug, title')
@@ -31,7 +47,15 @@ async function getPartnersImpl() {
console.error(new Error('Error fetching partners', { cause: error }))
}
return data ?? []
const partnerNavItems = (data ?? []).map(
(partner) =>
({
name: partner.title,
url: `https://supabase.com/partners/integrations/${partner.slug}` as `https://${string}`,
}) as Partial<NavMenuSection>
)
return partnerNavItems
}
export default GuidesLayout
+9
View File
@@ -166,6 +166,15 @@ function useBreadcrumbs() {
return breadcrumbs
}
const isAiPromptsPage = pathname.startsWith('/guides/getting-started/ai-prompts')
if (isAiPromptsPage) {
const breadcrumbs = [
{ name: 'Getting started', url: '/guides/getting-started' },
{ name: 'AI Prompts', url: '/guides/getting-started/ai-prompts' },
]
return breadcrumbs
}
const menuId = getMenuId(pathname)
const menu = NavItems[menuId]
return findMenuItemByUrl(menu, pathname, [])
+40 -16
View File
@@ -1,11 +1,32 @@
'use client'
import { useBreakpoint } from 'common'
import { ChevronRight, Play, Sparkles } from 'lucide-react'
import Link from 'next/link'
import { IconBackground } from 'ui'
import { useBreakpoint } from 'common'
import { cn, IconBackground } from 'ui'
import { IconPanel } from 'ui-patterns/IconPanel'
import DocsCoverLogo from './DocsCoverLogo'
import { Play } from 'lucide-react'
function AiPrompt({ className }: { className?: string }) {
return (
<Link
className={cn(
'group',
'w-fit rounded-full border px-3 py-1 flex gap-2 items-center text-foreground-light text-sm',
'hover:border-brand hover:text-brand focus-visible:text-brand',
'transition-colors',
className
)}
href="/guides/getting-started/ai-prompts"
>
<Sparkles size={14} />
Start with Supabase AI prompts
<ChevronRight size={14} className="group-hover:translate-x-1 transition-transform" />
</Link>
)
}
const HomePageCover = (props) => {
const isXs = useBreakpoint(639)
@@ -74,7 +95,7 @@ const HomePageCover = (props) => {
p-5 md:p-8
"
>
<div className="col-span-full flex flex-col md:flex-row xl:flex-col justify-between gap-1 md:gap-3">
<div className="col-span-full flex flex-col md:flex-row xl:flex-col justify-between gap-3">
<div className="md:max-w-xs shrink w-fit xl:max-w-none">
<div className="flex items-center gap-3 mb-3">
<IconBackground>
@@ -83,20 +104,23 @@ const HomePageCover = (props) => {
<h2 className="text-2xl m-0 text-foreground">Getting Started</h2>
</div>
<p className="text-foreground-light text-sm">
Discover how to set up a database to an app making queries in just a few minutes.
Set up and connect a database in just a few minutes.
</p>
</div>
<div className="flex shrink-0 flex-wrap md:grid md:grid-cols-5 gap-2 sm:gap-3">
{frameworks.map((framework, i) => (
<Link key={i} href={framework.href} passHref className="no-underline">
<IconPanel
iconSize={iconSize}
hideArrow
tooltip={framework.tooltip}
icon={framework.icon}
/>
</Link>
))}
<div className="shrink-0">
<div className="flex flex-wrap md:grid md:grid-cols-5 gap-2 sm:gap-3">
{frameworks.map((framework, i) => (
<Link key={i} href={framework.href} passHref className="no-underline">
<IconPanel
iconSize={iconSize}
hideArrow
tooltip={framework.tooltip}
icon={framework.icon}
/>
</Link>
))}
</div>
<AiPrompt className="mt-6" />
</div>
</div>
</div>
@@ -349,6 +349,16 @@ export const gettingstarted: NavMenuConstant = {
},
],
},
{
name: 'AI Prompts',
url: undefined,
items: [
{
name: 'Overview',
url: '/guides/getting-started/ai-prompts',
},
],
},
],
}
@@ -256,7 +256,7 @@ const NavigationMenu = ({
additionalNavItems,
}: {
menuId: MenuId
additionalNavItems?: Partial<NavMenuSection>[]
additionalNavItems?: Record<string, Partial<NavMenuSection>[]>
}) => {
const level = menuId
const menu = getMenuById(level)
@@ -6,20 +6,23 @@ import { type NavMenuSection } from '../Navigation.types'
import * as NavItems from './NavigationMenu.constants'
import NavigationMenuGuideListItems from './NavigationMenuGuideListItems'
import { usePathname } from 'next/navigation'
import { PropsWithChildren } from 'react'
import { MenuId } from './NavigationMenu'
const NavigationMenuGuideList = ({
id,
additionalNavItems,
}: {
id: string
additionalNavItems?: Partial<NavMenuSection>[]
additionalNavItems?: Record<string, Partial<NavMenuSection>[]>
}) => {
const pathname = usePathname()
const firstLevelRoute = pathname?.split('/')?.slice(0, 4)?.join('/')
// eslint-disable-next-line import/namespace -- dynamic access, can't lint properly
let menu = NavItems[id]
if (id === 'integrations' && additionalNavItems) {
if (id === MenuId.Integrations && additionalNavItems?.integrations) {
const integrationsListIndex = menu.items.findIndex((item) => item.name === 'Integrations')
if (integrationsListIndex !== -1) {
menu = {
@@ -28,7 +31,7 @@ const NavigationMenuGuideList = ({
...menu.items.slice(0, integrationsListIndex),
{
...menu.items[integrationsListIndex],
items: [...menu.items[integrationsListIndex].items, ...additionalNavItems],
items: [...menu.items[integrationsListIndex].items, ...additionalNavItems.integrations],
},
...menu.items.slice(integrationsListIndex + 1),
],
@@ -36,6 +39,38 @@ const NavigationMenuGuideList = ({
}
}
if (id === MenuId.GettingStarted && additionalNavItems?.prompts) {
const promptsSectionIndex = menu.items.findIndex((item) => item.name === 'AI Prompts')
if (promptsSectionIndex !== -1) {
menu = {
...menu,
items: [
...menu.items.slice(0, promptsSectionIndex),
{
...menu.items[promptsSectionIndex],
items: [...menu.items[promptsSectionIndex].items, ...additionalNavItems.prompts],
},
...menu.items.slice(promptsSectionIndex + 1),
],
}
}
}
return (
<NavigationMenuGuideListWrapper id={id} firstLevelRoute={firstLevelRoute}>
<NavigationMenuGuideListItems menu={menu} id={id} />
</NavigationMenuGuideListWrapper>
)
}
export function NavigationMenuGuideListWrapper({
id,
firstLevelRoute,
children,
}: PropsWithChildren<{
id: string
firstLevelRoute?: string
}>) {
return (
<Accordion.Root
collapsible={true}
@@ -44,7 +79,7 @@ const NavigationMenuGuideList = ({
value={firstLevelRoute}
className="transition-all duration-150 ease-out opacity-100 ml-0 delay-150"
>
<NavigationMenuGuideListItems menu={menu} id={id} />
{children}
</Accordion.Root>
)
}
@@ -0,0 +1,16 @@
---
title: AI Prompts
subtitle: Prompts for working with Supabase using AI-powered IDE tools
---
We've curated a selection of prompts to help you work with Supabase using your favorite AI-powered IDE tools, such as GitHub Copilot or Cursor.
## How to use
Copy the prompt to a file in your repo.
Use the "include file" feature from your AI tool to include the prompt when chatting with your AI assistant. For example, with GitHub Copilot, use `#<filename>`, in Cursor, use `@Files`, and in Zed, use `/file`.
## Prompts
<AiPromptsIndex />
+6 -1
View File
@@ -7,9 +7,14 @@ import remarkGfm from 'remark-gfm'
import rehypeKatex from 'rehype-katex'
import remarkMath from 'remark-math'
import { AiPromptsIndex } from '~/app/guides/(with-sidebar)/getting-started/ai-prompts/[slug]/AiPromptsIndex'
import { preprocessMdxWithDefaults } from '~/features/directives/utils'
import { components } from '~/features/docs/MdxBase.shared'
const serverOnlyComponents = {
AiPromptsIndex,
}
const codeHikeOptions: CodeHikeConfig = {
theme: codeHikeTheme,
lineNumbers: true,
@@ -66,7 +71,7 @@ const MDXRemoteBase = async ({
return (
<MDXRemote
source={preprocessedSource}
components={components}
components={{ ...components, ...serverOnlyComponents }}
options={finalOptions}
{...props}
/>
+1 -1
View File
@@ -336,7 +336,7 @@ interface SkeletonProps extends PropsWithChildren {
NavigationMenu?: ReactNode
hideFooter?: boolean
className?: string
additionalNavItems?: Partial<NavMenuSection>[]
additionalNavItems?: Record<string, Partial<NavMenuSection>[]>
}
function TopNavSkeleton({ children }) {
+7 -3
View File
@@ -1,5 +1,5 @@
import 'katex/dist/katex.min.css'
import { type PropsWithChildren } from 'react'
import type { ReactNode, PropsWithChildren } from 'react'
import { type NavMenuSection } from '~/components/Navigation/Navigation.types'
import { LayoutMainContent } from '~/layouts/DefaultLayout'
@@ -8,9 +8,13 @@ import { SidebarSkeleton } from '~/layouts/MainSkeleton'
const Layout = ({
children,
additionalNavItems,
}: PropsWithChildren<{ additionalNavItems?: Partial<NavMenuSection>[] }>) => {
NavigationMenu,
}: PropsWithChildren<{
additionalNavItems?: Record<string, Partial<NavMenuSection>[]>
NavigationMenu?: ReactNode
}>) => {
return (
<SidebarSkeleton additionalNavItems={additionalNavItems}>
<SidebarSkeleton NavigationMenu={NavigationMenu} additionalNavItems={additionalNavItems}>
<LayoutMainContent className="pb-0">{children}</LayoutMainContent>
</SidebarSkeleton>
)
-1
View File
@@ -1,6 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
/// <reference types="next/navigation-types/compat/navigation" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
-1
View File
@@ -101,7 +101,6 @@
"unist-builder": "^3.0.1",
"unist-util-filter": "^4.0.1",
"unist-util-visit": "^4.1.2",
"unist-util-visit-parents": "^5.0.0",
"uuid": "^9.0.1",
"valtio": "^1.12.0",
"yaml": "^2.4.5",
+13 -13
View File
@@ -1,4 +1,4 @@
# PostgreSQL SQL Style Guide
# Postgres SQL Style Guide
## General
@@ -12,7 +12,7 @@
- Avoid SQL reserved words and ensure names are unique and under 63 characters.
- Use snake_case for tables and columns.
- Prefer plurals for table names
- Prefer plurals for table names
- Prefer singular names for columns.
## Tables
@@ -62,12 +62,12 @@ where employee_id = 1001;
Larger queries:
```sql
select
first_name,
select
first_name,
last_name
from
from
employees
where
where
start_date between '2021-01-01' and '2021-12-31'
and
status = 'employed';
@@ -80,14 +80,14 @@ and
- Prefer full table names when referencing tables. This helps for readability.
```sql
select
employees.employee_name,
select
employees.employee_name,
departments.department_name
from
from
employees
join
join
departments on employees.department_id = departments.department_id
where
where
employees.start_date > '2022-01-01';
```
@@ -104,7 +104,7 @@ where end_date is null;
## Complex queries and CTEs
- If a query is extremely complex, prefer a CTE.
- If a query is extremely complex, prefer a CTE.
- Make sure the CTE is clear and linear. Prefer readability over performance.
- Add comments to each block.
@@ -138,4 +138,4 @@ from
employee_counts
order by
department_name;
```
```
+108
View File
@@ -0,0 +1,108 @@
# Bootstrap Next.js app with Supabase Auth
Create a Next.js app that uses App Router with Supabase Auth.
Follow Supabase's guidelines for using the `@supabase/ssr` package and Server-Side Auth. Specifically, there should be:
- A utility function to create a client on the client side
- A utility function create a client on the server side, using the Next.js `cookies` API to access the cookies. Use the latest version of the API, where `cookies` must be awaited.
- A utility function to handle refreshing the user session in middleware.
## Working with cookies
Use the latest version of `@supabase/ssr`, where cookie options are defined with the `getAll` and `setAll` functions, like so:
```
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return request.cookies.getAll()
},
setAll(cookiesToSet) {
cookiesToSet.forEach(({ name, value, options }) => request.cookies.set(name, value))
supabaseResponse = NextResponse.next({
request,
})
cookiesToSet.forEach(({ name, value, options }) =>
supabaseResponse.cookies.set(name, value, options)
)
},
},
}
)
```
No other cookie options should be provided.
## Middleware
The middleware should use the following `updateSession` function:
```
import { createServerClient } from '@supabase/ssr'
import { NextResponse, type NextRequest } from 'next/server'
export async function updateSession(request: NextRequest) {
let supabaseResponse = NextResponse.next({
request,
})
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return request.cookies.getAll()
},
setAll(cookiesToSet) {
cookiesToSet.forEach(({ name, value, options }) => request.cookies.set(name, value))
supabaseResponse = NextResponse.next({
request,
})
cookiesToSet.forEach(({ name, value, options }) =>
supabaseResponse.cookies.set(name, value, options)
)
},
},
}
)
// IMPORTANT: Avoid writing any logic between createServerClient and
// supabase.auth.getUser(). A simple mistake could make it very hard to debug
// issues with users being randomly logged out.
const {
data: { user },
} = await supabase.auth.getUser()
if (
!user &&
!request.nextUrl.pathname.startsWith('/login') &&
!request.nextUrl.pathname.startsWith('/auth')
) {
// no user, potentially respond by redirecting the user to the login page
const url = request.nextUrl.clone()
url.pathname = '/login'
return NextResponse.redirect(url)
}
// IMPORTANT: You *must* return the supabaseResponse object as it is. If you're
// creating a new response object with NextResponse.next() make sure to:
// 1. Pass the request in it, like so:
// const myNewResponse = NextResponse.next({ request })
// 2. Copy over the cookies, like so:
// myNewResponse.cookies.setAll(supabaseResponse.cookies.getAll())
// 3. Change the myNewResponse object to fit your needs, but avoid changing
// the cookies!
// 4. Finally:
// return myNewResponse
// If this is not done, you may be causing the browser and server to go out
// of sync and terminate the user's session prematurely!
return supabaseResponse
}
```
-1
View File
@@ -931,7 +931,6 @@
"unist-builder": "^3.0.1",
"unist-util-filter": "^4.0.1",
"unist-util-visit": "^4.1.2",
"unist-util-visit-parents": "^5.0.0",
"uuid": "^9.0.1",
"valtio": "^1.12.0",
"yaml": "^2.4.5",