-
Notifications
You must be signed in to change notification settings - Fork 8
feat(studio): Prompts FF and list UI #450
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| // SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| import { buildQueries, Matcher } from '@testing-library/react'; | ||
|
|
||
| /** Lucide icons render as `<svg class="lucide lucide-{name}">`. Use the kebab-case suffix (e.g. `cog`, `refresh-cw`). */ | ||
| const lucideIconSelector = (iconName: string) => `svg.lucide-${iconName}`; | ||
|
|
||
| /** Returns all matching Lucide SVGs under `container` (defaults to `document.body`). */ | ||
| export const queryAllLucideIcons = ( | ||
| iconName: string, | ||
| container: ParentNode = document.body | ||
| ): SVGElement[] => { | ||
| return [...container.querySelectorAll(lucideIconSelector(iconName))] as SVGElement[]; | ||
| }; | ||
|
|
||
| const getMultipleError = (_container: Element | null, iconName: string) => | ||
| `Found multiple lucide icons matching: ${iconName}`; | ||
| const getMissingError = (_container: Element | null, iconName: string) => | ||
| `Unable to find lucide icon: ${iconName}`; | ||
|
|
||
| /** Finds a single Lucide icon by kebab-case name, optionally scoped to a container. */ | ||
| export const getLucideIcon = ( | ||
| iconName: string, | ||
| container: ParentNode = document.body | ||
| ): SVGElement => { | ||
| const icons = queryAllLucideIcons(iconName, container); | ||
| if (icons.length === 0) { | ||
| throw new Error(getMissingError(null, iconName)); | ||
| } | ||
| if (icons.length > 1) { | ||
| throw new Error(getMultipleError(null, iconName)); | ||
| } | ||
| return icons[0]; | ||
| }; | ||
|
|
||
| /** Returns the first matching Lucide icon, or null if none. */ | ||
| export const queryLucideIcon = ( | ||
| iconName: string, | ||
| container: ParentNode = document.body | ||
| ): SVGElement | null => queryAllLucideIcons(iconName, container)[0] ?? null; | ||
|
|
||
| const queryAllByLucideIcon = (container: HTMLElement, iconName: Matcher) => { | ||
| if (typeof iconName !== 'string') { | ||
| throw new TypeError(`Lucide icon name must be a string, received ${typeof iconName}`); | ||
| } | ||
| return queryAllLucideIcons(iconName, container) as unknown as HTMLElement[]; | ||
| }; | ||
|
|
||
| export const [ | ||
| queryByLucideIcon, | ||
| getAllByLucideIcon, | ||
| getByLucideIcon, | ||
| findAllByLucideIcon, | ||
| findByLucideIcon, | ||
| ] = buildQueries(queryAllByLucideIcon, getMultipleError, getMissingError); |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,227 @@ | ||||||||||||||||||||||||||||||||||||||||
| // SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||||||||||||||||||||||||||||||||||||||||
| // SPDX-License-Identifier: Apache-2.0 | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| import { | ||||||||||||||||||||||||||||||||||||||||
| ROW_ACTIONS_COLUMN_SIZE, | ||||||||||||||||||||||||||||||||||||||||
| StudioDataView, | ||||||||||||||||||||||||||||||||||||||||
| } from '@nemo/common/src/components/DataView/StudioDataView'; | ||||||||||||||||||||||||||||||||||||||||
| import { RelativeTime } from '@nemo/common/src/components/RelativeTime'; | ||||||||||||||||||||||||||||||||||||||||
| import { TableEmptyState } from '@nemo/common/src/components/TableEmptyState'; | ||||||||||||||||||||||||||||||||||||||||
| import { useStudioDataViewState } from '@nemo/common/src/hooks/useStudioDataViewState'; | ||||||||||||||||||||||||||||||||||||||||
| import { useToast } from '@nemo/common/src/providers/toast/useToast'; | ||||||||||||||||||||||||||||||||||||||||
| import { useModelsDeletePrompt, useModelsListPrompts } from '@nemo/sdk/generated/platform/api'; | ||||||||||||||||||||||||||||||||||||||||
| import type { Prompt } from '@nemo/sdk/generated/platform/schema'; | ||||||||||||||||||||||||||||||||||||||||
| import { Button, Stack, Text } from '@nvidia/foundations-react-core'; | ||||||||||||||||||||||||||||||||||||||||
| import { getErrorMessage } from '@studio/api/common/utils'; | ||||||||||||||||||||||||||||||||||||||||
| import { DeleteConfirmationModal } from '@studio/components/DeleteConfirmationModal'; | ||||||||||||||||||||||||||||||||||||||||
| import { ErrorPanel } from '@studio/components/ErrorPanel'; | ||||||||||||||||||||||||||||||||||||||||
| import { PromptIconFc } from '@studio/constants/constants'; | ||||||||||||||||||||||||||||||||||||||||
| import { keepPreviousData } from '@tanstack/react-query'; | ||||||||||||||||||||||||||||||||||||||||
| import { Trash } from 'lucide-react'; | ||||||||||||||||||||||||||||||||||||||||
| import { ComponentProps, FC, useCallback, useMemo, useState } from 'react'; | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| export interface PromptsDataViewProps { | ||||||||||||||||||||||||||||||||||||||||
| workspace: string; | ||||||||||||||||||||||||||||||||||||||||
| emptyStateActions?: React.ReactNode; | ||||||||||||||||||||||||||||||||||||||||
| attributes?: { | ||||||||||||||||||||||||||||||||||||||||
| Stack?: React.ComponentProps<typeof Stack>; | ||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| type PromptWithId = Prompt & { id: string }; | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| export const PromptsDataView: FC<PromptsDataViewProps> = ({ | ||||||||||||||||||||||||||||||||||||||||
| workspace, | ||||||||||||||||||||||||||||||||||||||||
| emptyStateActions, | ||||||||||||||||||||||||||||||||||||||||
| attributes, | ||||||||||||||||||||||||||||||||||||||||
| }) => { | ||||||||||||||||||||||||||||||||||||||||
| const toast = useToast(); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| const dataViewState = useStudioDataViewState({ | ||||||||||||||||||||||||||||||||||||||||
| defaultSort: { id: 'created_at', desc: true }, | ||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| const [promptToDelete, setPromptToDelete] = useState<Prompt>(); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| const { data, refetch, isFetching, error } = useModelsListPrompts( | ||||||||||||||||||||||||||||||||||||||||
| workspace, | ||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||
| page: dataViewState.pagination.state.pageIndex + 1, | ||||||||||||||||||||||||||||||||||||||||
| page_size: dataViewState.pagination.state.pageSize, | ||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||
| query: { | ||||||||||||||||||||||||||||||||||||||||
| placeholderData: keepPreviousData, | ||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| const deletePromptMutation = useModelsDeletePrompt(); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| const prompts = useMemo(() => data?.data ?? [], [data?.data]); | ||||||||||||||||||||||||||||||||||||||||
| const pagination = data?.pagination; | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| const searchBar = dataViewState.searchBar.state; | ||||||||||||||||||||||||||||||||||||||||
| const filteredPrompts = useMemo(() => { | ||||||||||||||||||||||||||||||||||||||||
| if (!searchBar) return prompts; | ||||||||||||||||||||||||||||||||||||||||
| return prompts.filter((prompt: Prompt) => | ||||||||||||||||||||||||||||||||||||||||
| prompt.name?.toLowerCase().includes(searchBar.toLowerCase()) | ||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||
| }, [prompts, searchBar]); | ||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+65
to
+70
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major 🧩 Analysis chain🏁 Script executed: # Inspect the component structure and the pagination/search flow around the flagged lines.
git ls-files 'web/packages/studio/src/components/dataViews/PromptsDataView/*' && \
wc -l web/packages/studio/src/components/dataViews/PromptsDataView/index.tsx && \
ast-grep outline web/packages/studio/src/components/dataViews/PromptsDataView/index.tsx --view expandedRepository: NVIDIA-NeMo/nemo-platform Length of output: 624 🏁 Script executed: sed -n '1,260p' web/packages/studio/src/components/dataViews/PromptsDataView/index.tsxRepository: NVIDIA-NeMo/nemo-platform Length of output: 7190 🏁 Script executed: # Find where prompts are fetched and whether search is server-side or client-side.
rg -n "searchBar|page|pagination|useQuery|useInfiniteQuery|offset|limit|cursor|total" web/packages/studio/src/components/dataViews/PromptsDataView/index.tsxRepository: NVIDIA-NeMo/nemo-platform Length of output: 620 🏁 Script executed: # Inspect nearby code if the previous output is large.
sed -n '1,220p' web/packages/studio/src/components/dataViews/PromptsDataView/index.tsxRepository: NVIDIA-NeMo/nemo-platform Length of output: 7025 🏁 Script executed: sed -n '1,140p' web/packages/studio/src/components/dataViews/PromptsDataView/index.tsxRepository: NVIDIA-NeMo/nemo-platform Length of output: 4557 🏁 Script executed: sed -n '140,240p' web/packages/studio/src/components/dataViews/PromptsDataView/index.tsxRepository: NVIDIA-NeMo/nemo-platform Length of output: 2808 🏁 Script executed: sed -n '1,140p' web/packages/studio/src/components/dataViews/PromptsDataView/index.tsxRepository: NVIDIA-NeMo/nemo-platform Length of output: 4557 🏁 Script executed: sed -n '140,240p' web/packages/studio/src/components/dataViews/PromptsDataView/index.tsxRepository: NVIDIA-NeMo/nemo-platform Length of output: 2808 🏁 Script executed: # Inspect how StudioDataView uses data, searchField, and totalCount.
ast-grep outline web/packages/common/src/components/DataView/StudioDataView.tsx --view expandedRepository: NVIDIA-NeMo/nemo-platform Length of output: 1120 🏁 Script executed: fd 'StudioDataView.tsx' web packages . -aRepository: NVIDIA-NeMo/nemo-platform Length of output: 481 🏁 Script executed: fd -a 'StudioDataView.tsx' web . | headRepository: NVIDIA-NeMo/nemo-platform Length of output: 481 🏁 Script executed: fd -a 'StudioDataView.tsx' . | head -20Repository: NVIDIA-NeMo/nemo-platform Length of output: 322 🏁 Script executed: fd -a 'useStudioDataViewState.ts' . | head -20Repository: NVIDIA-NeMo/nemo-platform Length of output: 163 🏁 Script executed: fd -a 'StudioDataView.tsx' . | head -20Repository: NVIDIA-NeMo/nemo-platform Length of output: 322 🏁 Script executed: fd -a 'useStudioDataViewState.ts' . | head -20Repository: NVIDIA-NeMo/nemo-platform Length of output: 163 🏁 Script executed: fd -a 'StudioDataView.tsx' . | head -20Repository: NVIDIA-NeMo/nemo-platform Length of output: 322 🏁 Script executed: fd -a 'useStudioDataViewState.ts' . | head -20Repository: NVIDIA-NeMo/nemo-platform Length of output: 163 Move search before pagination Search only filters the fetched page, while 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| const promptsWithId = useMemo<PromptWithId[]>( | ||||||||||||||||||||||||||||||||||||||||
| () => | ||||||||||||||||||||||||||||||||||||||||
| filteredPrompts.map((prompt: Prompt) => ({ | ||||||||||||||||||||||||||||||||||||||||
| ...prompt, | ||||||||||||||||||||||||||||||||||||||||
| id: `${prompt.workspace}/${prompt.name}`, | ||||||||||||||||||||||||||||||||||||||||
| })), | ||||||||||||||||||||||||||||||||||||||||
| [filteredPrompts] | ||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| const handleDeletePrompt = async () => { | ||||||||||||||||||||||||||||||||||||||||
| if (!promptToDelete) return false; | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||
| await deletePromptMutation.mutateAsync({ | ||||||||||||||||||||||||||||||||||||||||
| workspace, | ||||||||||||||||||||||||||||||||||||||||
| name: promptToDelete.name, | ||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+81
to
+88
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Guard delete payload when
Proposed patch const handleDeletePrompt = async () => {
- if (!promptToDelete) return false;
+ if (!promptToDelete?.name) {
+ toast.error('Prompt name is missing');
+ return false;
+ }
try {
await deletePromptMutation.mutateAsync({
workspace,
name: promptToDelete.name,
});📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||
| refetch(); | ||||||||||||||||||||||||||||||||||||||||
| return true; | ||||||||||||||||||||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||||||||||||||||||||
| toast.error('Failed to delete prompt'); | ||||||||||||||||||||||||||||||||||||||||
| return false; | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| const makeColumns: ComponentProps<typeof StudioDataView<PromptWithId>>['makeColumns'] = | ||||||||||||||||||||||||||||||||||||||||
| useCallback( | ||||||||||||||||||||||||||||||||||||||||
| ({ accessor }, { rowActionsColumn }) => [ | ||||||||||||||||||||||||||||||||||||||||
| accessor('name', { | ||||||||||||||||||||||||||||||||||||||||
| header: 'Name', | ||||||||||||||||||||||||||||||||||||||||
| enableSorting: false, | ||||||||||||||||||||||||||||||||||||||||
| size: 200, | ||||||||||||||||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||||||||||||||||
| accessor('description', { | ||||||||||||||||||||||||||||||||||||||||
| header: 'Description', | ||||||||||||||||||||||||||||||||||||||||
| cell({ row }) { | ||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||
| <Text className="truncate" title={row.original.description}> | ||||||||||||||||||||||||||||||||||||||||
| {row.original.description || '-'} | ||||||||||||||||||||||||||||||||||||||||
| </Text> | ||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||||||||||||||||
| accessor('tags', { | ||||||||||||||||||||||||||||||||||||||||
| header: 'Tags', | ||||||||||||||||||||||||||||||||||||||||
| enableSorting: false, | ||||||||||||||||||||||||||||||||||||||||
| size: 180, | ||||||||||||||||||||||||||||||||||||||||
| cell({ row }) { | ||||||||||||||||||||||||||||||||||||||||
| const tags = row.original.tags; | ||||||||||||||||||||||||||||||||||||||||
| if (!tags?.length) return <Text>-</Text>; | ||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||
| <Text className="truncate" title={tags.join(', ')}> | ||||||||||||||||||||||||||||||||||||||||
| {tags.join(', ')} | ||||||||||||||||||||||||||||||||||||||||
| </Text> | ||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||||||||||||||||
| accessor('created_at', { | ||||||||||||||||||||||||||||||||||||||||
| header: 'Created', | ||||||||||||||||||||||||||||||||||||||||
| enableSorting: true, | ||||||||||||||||||||||||||||||||||||||||
| size: 150, | ||||||||||||||||||||||||||||||||||||||||
| cell({ row }) { | ||||||||||||||||||||||||||||||||||||||||
| return row.original.created_at ? ( | ||||||||||||||||||||||||||||||||||||||||
| <RelativeTime datetime={row.original.created_at} /> | ||||||||||||||||||||||||||||||||||||||||
| ) : ( | ||||||||||||||||||||||||||||||||||||||||
| <Text>-</Text> | ||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||||||||||||||||
| accessor('updated_at', { | ||||||||||||||||||||||||||||||||||||||||
| header: 'Updated', | ||||||||||||||||||||||||||||||||||||||||
| enableSorting: true, | ||||||||||||||||||||||||||||||||||||||||
| size: 150, | ||||||||||||||||||||||||||||||||||||||||
| cell({ row }) { | ||||||||||||||||||||||||||||||||||||||||
| return row.original.updated_at ? ( | ||||||||||||||||||||||||||||||||||||||||
| <RelativeTime datetime={row.original.updated_at} /> | ||||||||||||||||||||||||||||||||||||||||
| ) : ( | ||||||||||||||||||||||||||||||||||||||||
| <Text>-</Text> | ||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||||||||||||||||
| rowActionsColumn({ | ||||||||||||||||||||||||||||||||||||||||
| size: ROW_ACTIONS_COLUMN_SIZE, | ||||||||||||||||||||||||||||||||||||||||
| enableResizing: false, | ||||||||||||||||||||||||||||||||||||||||
| rowActions: (prompt: PromptWithId) => [ | ||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||
| slotLeft: <Trash />, | ||||||||||||||||||||||||||||||||||||||||
| children: 'Delete', | ||||||||||||||||||||||||||||||||||||||||
| danger: true, | ||||||||||||||||||||||||||||||||||||||||
| onSelect: () => setPromptToDelete(prompt), | ||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||
| ], | ||||||||||||||||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||||||||||||||||
| ], | ||||||||||||||||||||||||||||||||||||||||
| [] | ||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| const hasActiveFilters = !!searchBar; | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||
| <Stack gap="density-2xl" {...attributes?.Stack}> | ||||||||||||||||||||||||||||||||||||||||
| <StudioDataView | ||||||||||||||||||||||||||||||||||||||||
| dataViewState={dataViewState} | ||||||||||||||||||||||||||||||||||||||||
| searchField="name" | ||||||||||||||||||||||||||||||||||||||||
| makeColumns={makeColumns} | ||||||||||||||||||||||||||||||||||||||||
| attributes={{ | ||||||||||||||||||||||||||||||||||||||||
| DataViewSearchBar: { | ||||||||||||||||||||||||||||||||||||||||
| placeholder: 'Search Prompts...', | ||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||
| DataViewRoot: { | ||||||||||||||||||||||||||||||||||||||||
| data: promptsWithId, | ||||||||||||||||||||||||||||||||||||||||
| totalCount: pagination?.total_results, | ||||||||||||||||||||||||||||||||||||||||
| requestStatus: error ? 'error' : isFetching ? 'loading' : undefined, | ||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||
| DataViewTableContent: { | ||||||||||||||||||||||||||||||||||||||||
| renderEmptyState: () => | ||||||||||||||||||||||||||||||||||||||||
| hasActiveFilters ? ( | ||||||||||||||||||||||||||||||||||||||||
| <TableEmptyState | ||||||||||||||||||||||||||||||||||||||||
| header="No Results Found" | ||||||||||||||||||||||||||||||||||||||||
| emptyMessage="No prompts match your search" | ||||||||||||||||||||||||||||||||||||||||
| actions={ | ||||||||||||||||||||||||||||||||||||||||
| <Button kind="tertiary" onClick={dataViewState.resetFilters}> | ||||||||||||||||||||||||||||||||||||||||
| Clear Search | ||||||||||||||||||||||||||||||||||||||||
| </Button> | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||
| ) : ( | ||||||||||||||||||||||||||||||||||||||||
| <TableEmptyState | ||||||||||||||||||||||||||||||||||||||||
| icon={<PromptIconFc className="size-16" />} | ||||||||||||||||||||||||||||||||||||||||
| header="No Prompts Yet" | ||||||||||||||||||||||||||||||||||||||||
| emptyMessage="Reusable prompt templates will appear here once created." | ||||||||||||||||||||||||||||||||||||||||
| actions={emptyStateActions} | ||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||||||||
| renderErrorState: () => ( | ||||||||||||||||||||||||||||||||||||||||
| <ErrorPanel | ||||||||||||||||||||||||||||||||||||||||
| errorMessage={getErrorMessage(error ?? new Error('Failed to fetch prompts'))} | ||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||
| }} | ||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| {promptToDelete && ( | ||||||||||||||||||||||||||||||||||||||||
| <DeleteConfirmationModal | ||||||||||||||||||||||||||||||||||||||||
| open | ||||||||||||||||||||||||||||||||||||||||
| simpleConfirm | ||||||||||||||||||||||||||||||||||||||||
| onDelete={handleDeletePrompt} | ||||||||||||||||||||||||||||||||||||||||
| title={`Delete: ${promptToDelete.name}`} | ||||||||||||||||||||||||||||||||||||||||
| description="Are you sure you want to delete this prompt?" | ||||||||||||||||||||||||||||||||||||||||
| onClose={() => setPromptToDelete(undefined)} | ||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||
| </Stack> | ||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.