mirror of
https://github.com/supabase/supabase.git
synced 2026-10-08 19:05:06 +03:00
## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? UI refactor of the notebook create/update preview in the AI Assistant panel, plus a small additive prop on the shared `CodeBlock`. ## What is the current behavior? The assistant's notebook diff renders each cell as its own bordered box with a gap between them, under a `6 cells` line that is easy to miss. Cells can't be collapsed, each one carries a repeated `ADDED` badge and a nested "Show more" toggle, and long markdown scrolls sideways instead of wrapping. ## What is the new behavior? <img width="796" height="1076" alt="CleanShot 2026-08-18 at 14 41 30@2x" src="https://github.com/user-attachments/assets/45e58c6c-48f2-404b-8699-757ee96a4a8d" /> - The whole diff is one card: a distinct header row (notebook name, summary, expand/collapse all) over cells glued together by dividers. - Every cell is a `Collapsible`. Added and replaced cells open by default; unchanged, moved, and removed cells stay as single rows but are now inspectable instead of being content-free. - The per-row badge is replaced by a colored gutter glyph (`+` `−` `~` `↕`) with a tooltip naming the change type. The change type reaches the accessible name via `aria-label` on the row. - The nested "Show more" toggle inside each cell is gone — the row itself is the only control. - `CodeBlock` gains a `wrapLongLines` prop (default `false`, no change for existing callers), used here so markdown and SQL soft-wrap. The highlighter sets `white-space` inline on the `<code>` element, so a class on the `<pre>` can't do this. ## Additional context Towards FE-4143 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Notebook previews now display titles, notebook icons, and clearer bordered layouts. * Added per-cell expand/collapse controls, including “Expand all” and “Collapse all.” * Long code lines can now wrap for improved readability. * **Improvements** * Added mode-based fallback labels when notebook titles are unavailable. * Newly added and replaced cells expand by default, while unchanged cells remain collapsed. * Improved change markers, tooltips, removed-cell styling, and notebook proposal preview spacing. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
151 lines
4.7 KiB
TypeScript
151 lines
4.7 KiB
TypeScript
import dayjs from 'dayjs'
|
||
import type { CodeBlockLang } from 'ui-patterns/CodeBlock'
|
||
|
||
import type { NotebookCellDiffEntry } from '@/data/content/notebooks/notebook-operations'
|
||
import type { AgentCell, CellWire, TimeRange } from '@/data/content/notebooks/notebook-schema'
|
||
|
||
/** React key for a diff entry. Added/replaced cells have no `id`, so they key off the operation. */
|
||
export function getEntryKey(entry: NotebookCellDiffEntry): string {
|
||
switch (entry._tag) {
|
||
case 'unchanged':
|
||
case 'removed':
|
||
case 'moved':
|
||
return entry.cell.id
|
||
case 'added':
|
||
case 'replaced':
|
||
return `op-${entry.operationIndex}`
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Whether a diff entry starts expanded. Only the entries the user has to actually read to
|
||
* decide — the ones whose content the assistant is proposing — open on their own; unchanged,
|
||
* moved, and removed cells stay as single rows until asked for.
|
||
*/
|
||
export function isEntryExpandedByDefault(entry: NotebookCellDiffEntry): boolean {
|
||
return entry._tag === 'added' || entry._tag === 'replaced'
|
||
}
|
||
|
||
/** Human label for a collapsed/badge row. */
|
||
export function getCellLabel(cell: CellWire | AgentCell): string {
|
||
switch (cell._tag) {
|
||
case 'markdown_cell':
|
||
return 'Markdown cell'
|
||
case 'database_cell':
|
||
case 'log_cell':
|
||
return `Query: ${cell.title ?? 'Untitled query'}`
|
||
}
|
||
}
|
||
|
||
/** The cell's underlying source text, regardless of backend. */
|
||
export function getCellSourceText(cell: CellWire | AgentCell): string {
|
||
switch (cell._tag) {
|
||
case 'markdown_cell':
|
||
return cell.text
|
||
case 'database_cell':
|
||
case 'log_cell':
|
||
return cell.sql
|
||
}
|
||
}
|
||
|
||
/** Language for rendering the cell's source via `CodeBlock`. */
|
||
export function getCellCodeBlockLanguage(cell: CellWire | AgentCell): CodeBlockLang {
|
||
switch (cell._tag) {
|
||
case 'markdown_cell':
|
||
return 'markdown'
|
||
case 'database_cell':
|
||
case 'log_cell':
|
||
return 'sql'
|
||
}
|
||
}
|
||
|
||
/** Monaco language id for rendering the cell's source via `DiffEditor`. */
|
||
export function getCellMonacoLanguage(cell: CellWire | AgentCell): string {
|
||
switch (cell._tag) {
|
||
case 'markdown_cell':
|
||
return 'markdown'
|
||
case 'database_cell':
|
||
case 'log_cell':
|
||
return 'pgsql'
|
||
}
|
||
}
|
||
|
||
/** Formats a `TimeRange` as plain text, e.g. "Last 7 days" or an absolute bound pair. */
|
||
export function formatTimeRange(range: TimeRange): string {
|
||
if (range._tag === 'relative_time_range') {
|
||
return `Last ${range.amount} ${range.unit}${range.amount === 1 ? '' : 's'}`
|
||
}
|
||
|
||
const format = (value: string) => dayjs(value).format('MMM D, YYYY h:mm A')
|
||
return `${format(range.start)} → ${format(range.end)}`
|
||
}
|
||
|
||
/**
|
||
* Plain-text metadata line for a query cell (its source parameters, not its SQL) — `null`
|
||
* when the cell has none. A `replace_cell` can change only this and leave `sql` identical,
|
||
* so it's compared independently of the source text rather than folded into it.
|
||
*/
|
||
export function getCellMetadataLine(cell: CellWire | AgentCell): string | null {
|
||
switch (cell._tag) {
|
||
case 'markdown_cell':
|
||
return null
|
||
case 'database_cell':
|
||
return cell.database_identifier ? `Database: ${cell.database_identifier}` : null
|
||
case 'log_cell':
|
||
return `Time range: ${formatTimeRange(cell.time_range)}`
|
||
}
|
||
}
|
||
|
||
export type NotebookDiffSummary =
|
||
| { mode: 'create'; cellCount: number }
|
||
| { mode: 'update'; counts: { added: number; removed: number; replaced: number; moved: number } }
|
||
|
||
/** Summarizes a set of diff entries into counts suitable for a header line. */
|
||
export function summarizeNotebookDiff(
|
||
entries: NotebookCellDiffEntry[],
|
||
mode: 'create' | 'update'
|
||
): NotebookDiffSummary {
|
||
if (mode === 'create') {
|
||
return { mode: 'create', cellCount: entries.length }
|
||
}
|
||
|
||
const counts = { added: 0, removed: 0, replaced: 0, moved: 0 }
|
||
for (const entry of entries) {
|
||
switch (entry._tag) {
|
||
case 'added':
|
||
counts.added++
|
||
break
|
||
case 'removed':
|
||
counts.removed++
|
||
break
|
||
case 'replaced':
|
||
counts.replaced++
|
||
break
|
||
case 'moved':
|
||
counts.moved++
|
||
break
|
||
case 'unchanged':
|
||
break
|
||
}
|
||
}
|
||
|
||
return { mode: 'update', counts }
|
||
}
|
||
|
||
/** Formats a `NotebookDiffSummary` into the header string. */
|
||
export function formatNotebookDiffSummary(summary: NotebookDiffSummary): string {
|
||
if (summary.mode === 'create') {
|
||
const { cellCount } = summary
|
||
return `${cellCount} cell${cellCount === 1 ? '' : 's'}`
|
||
}
|
||
|
||
const { added, removed, replaced, moved } = summary.counts
|
||
const parts: string[] = []
|
||
if (added > 0) parts.push(`+${added}`)
|
||
if (removed > 0) parts.push(`−${removed}`)
|
||
if (replaced > 0) parts.push(`~${replaced}`)
|
||
if (moved > 0) parts.push(`↕${moved}`)
|
||
|
||
return parts.length > 0 ? parts.join(' ') : 'No changes'
|
||
}
|