Files
supabase/apps/studio/components/interfaces/ProjectHome/NotebooksSection.tsx
Saxon FletcherandClaude Opus 5.5 d3f3903b69 feat(studio): show recent notebooks on the project homepage (#50825)
<img width="1758" height="1252" alt="image"
src="https://github.com/user-attachments/assets/6f5ff072-9b47-4265-924d-0ca22368f6cb"
/>


## What

With the Explorer feature preview on, the homepage **Reports** row
becomes a **Notebooks** row showing the four newest notebooks.

- Advisor and notebook cards share a new `HomeCard` layout: same height
and header, content anchored to the bottom, 3-line description clamp.
- Notebook cards show the cell count, the creation date, and an
**Analyze** button that starts the same chat as the Explorer notebook
tab (`useAnalyzeNotebook`, now shared by both).
- Advisor lint cards replace the assistant dropdown with two icon
buttons: **Ask Assistant** and **Copy prompt**. Copy prompt shows a
check after copying.
- The empty state matches the Reports one.

## How to test

1. Turn on the Explorer feature preview and open a project's Home page.
2. The Reports row should be replaced by Notebooks. With no notebooks,
you should see the empty state; **Create your first notebook** opens a
new notebook in Explorer.
3. Click a notebook card to open it. Click **Analyze** to start an
assistant chat. It should be disabled on a notebook with no cells.
4. On an advisor lint card, check **Ask Assistant** opens the assistant
with the lint prompt, and **Copy prompt** copies it and briefly shows a
check.
5. Turn the preview off: the Reports row comes back unchanged.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Project home now displays up to four notebooks, with options to create
a notebook, open it, and see its cell count.
* Start an AI analysis of a notebook directly from its card or from the
notebook explorer. Analysis is unavailable for notebooks without cells.
  * Advisor cards now offer a one-click AI chat and a copyable prompt.
* Project home shows notebooks when the Explorer preview is enabled;
otherwise, it continues to show custom reports.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 16:41:56 +08:00

141 lines
4.3 KiB
TypeScript

import { useParams } from 'common'
import dayjs from 'dayjs'
import { NotebookText, Plus } from 'lucide-react'
import { AiIconAnimation, Badge, Button } from 'ui'
import { Row } from 'ui-patterns/Row'
import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
import { HomeCard } from './HomeCard'
import { useAnalyzeNotebook, useCreateNotebook } from '@/components/interfaces/Explorer/hooks'
import { AlertError } from '@/components/ui/AlertError'
import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
import {
useNotebooksInfiniteQuery,
type NotebookRow,
} from '@/data/content/notebooks/notebooks-infinite-query'
const MAX_HOMEPAGE_NOTEBOOKS = 4
export const NotebooksSection = () => {
const { ref: projectRef } = useParams()
const { createNotebook } = useCreateNotebook()
const { data, error, isPending, isError, isSuccess, refetch } = useNotebooksInfiniteQuery({
projectRef,
limit: MAX_HOMEPAGE_NOTEBOOKS,
sort: 'inserted_at',
})
// The content endpoint can return more rows than `limit`, so cap the row here too
const notebooks = (data?.pages[0]?.content ?? []).slice(0, MAX_HOMEPAGE_NOTEBOOKS)
return (
<div>
{isPending && (
<>
<ShimmeringLoader className="w-96 mb-6" />
<div className="flex flex-col gap-2">
<ShimmeringLoader />
<ShimmeringLoader className="w-3/4" />
<ShimmeringLoader className="w-1/2" />
</div>
</>
)}
{!isPending && (
<div className="flex justify-between items-center mb-6">
<h2>Notebooks</h2>
<Button icon={<Plus />} onClick={() => createNotebook()}>
New notebook
</Button>
</div>
)}
{isError && (
<AlertError
error={error instanceof Error ? error : null}
subject="Failed to retrieve notebooks"
projectRef={projectRef}
additionalActions={
<Button variant="default" onClick={() => refetch()}>
Retry
</Button>
}
/>
)}
{isSuccess && notebooks.length === 0 && (
<EmptyState onCreateNotebook={() => createNotebook()} />
)}
{isSuccess && notebooks.length > 0 && (
<Row maxColumns={4} minWidth={280}>
{notebooks.map((notebook) => (
<NotebookCard key={notebook.id} notebook={notebook} projectRef={projectRef} />
))}
</Row>
)}
</div>
)
}
function NotebookCard({
notebook,
projectRef,
}: {
notebook: NotebookRow
projectRef: string | undefined
}) {
const { analyzeNotebook, isCreating } = useAnalyzeNotebook()
const cellCount = notebook.content?.cells?.length
// Treat an unknown cell count as empty so Analyze is never offered on a notebook with no cells
const isEmpty = !cellCount
return (
<HomeCard
href={`/project/${projectRef}/explorer/notebook/${notebook.id}`}
icon={<NotebookText size={16} strokeWidth={1.5} className="text-foreground-light" />}
label="NOTEBOOK"
title={notebook.name}
description={notebook.description?.trim()}
meta={`Created ${dayjs(notebook.inserted_at).fromNow()}`}
actions={
<>
{cellCount !== undefined && (
<Badge className="w-fit">
{cellCount} {cellCount === 1 ? 'CELL' : 'CELLS'}
</Badge>
)}
<ButtonTooltip
variant="text"
className="w-7 h-7 px-1.5"
icon={<AiIconAnimation size={16} loading={isCreating} />}
disabled={isEmpty}
onClick={() => analyzeNotebook({ id: notebook.id, name: notebook.name })}
aria-label="Analyze"
tooltip={{
content: {
text: isEmpty ? 'Add a cell to the notebook to analyze it' : 'Analyze',
},
}}
/>
</>
}
/>
)
}
function EmptyState({ onCreateNotebook }: { onCreateNotebook: () => void }) {
return (
<div className="h-64 flex flex-col items-center justify-center rounded-sm border-2 border-dashed p-16">
<h4>Create a notebook</h4>
<p className="text-sm text-foreground-light mb-4">
Save queries and notes together to revisit and share
</p>
<Button iconRight={<Plus size={14} />} onClick={onCreateNotebook}>
Create your first notebook
</Button>
</div>
)
}