diff --git a/web/.prettierignore b/web/.prettierignore index eb8ce09a8d..33f39170b3 100644 --- a/web/.prettierignore +++ b/web/.prettierignore @@ -3,3 +3,7 @@ helm/ # Lock file pnpm-lock.yaml + +# Build output +dist/ +packages/*/dist/ diff --git a/web/packages/studio/src/routes/DashboardLandingRoute/index.spec.tsx b/web/packages/studio/src/routes/DashboardLandingRoute/index.spec.tsx new file mode 100644 index 0000000000..068d1ca7b7 --- /dev/null +++ b/web/packages/studio/src/routes/DashboardLandingRoute/index.spec.tsx @@ -0,0 +1,66 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { ROUTES } from '@studio/constants/routes'; +import { DashboardLandingRoute } from '@studio/routes/DashboardLandingRoute'; +import { TestProviders } from '@studio/tests/util/TestProviders'; +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { createMemoryRouter, generatePath, RouterProvider } from 'react-router'; + +const workspace = 'default'; + +const renderRoute = () => { + const route = generatePath(ROUTES.workspace.dashboard, { workspace }); + const router = createMemoryRouter( + [{ path: ROUTES.workspace.dashboard, element: }], + { + initialEntries: [route], + } + ); + + return render( + + + + ); +}; + +describe('DashboardLandingRoute', () => { + it('renders the dashboard landing page', async () => { + renderRoute(); + + expect(await screen.findByText('What would you like to do?')).toBeInTheDocument(); + expect(screen.getByRole('textbox', { name: 'Message Claude' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /Explore repo/ })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /Draft a change/ })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /Review recent work/ })).toBeInTheDocument(); + }); + + it('lets prompt suggestions populate the landing composer', async () => { + const user = userEvent.setup(); + renderRoute(); + + await user.click(await screen.findByRole('button', { name: /Explore repo/ })); + + expect(screen.getByRole('textbox', { name: 'Message Claude' })).toHaveValue( + 'Give me a concise map of this repo and the main places I should know about.' + ); + }); + + it('only enables the send affordance once the composer has text', async () => { + const user = userEvent.setup(); + renderRoute(); + + const composer = await screen.findByRole('textbox', { name: 'Message Claude' }); + const sendButton = screen.getByRole('button', { name: 'Send message' }); + + expect(sendButton).toBeDisabled(); + + await user.type(composer, 'Sketch a dashboard'); + + await waitFor(() => { + expect(screen.getByRole('button', { name: 'Send message' })).toBeEnabled(); + }); + }); +}); diff --git a/web/packages/studio/src/routes/DashboardLandingRoute/index.tsx b/web/packages/studio/src/routes/DashboardLandingRoute/index.tsx new file mode 100644 index 0000000000..eb26f83cc3 --- /dev/null +++ b/web/packages/studio/src/routes/DashboardLandingRoute/index.tsx @@ -0,0 +1,145 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { Button, Card, Flex, Text, TextArea, Tooltip } from '@nvidia/foundations-react-core'; +import { AccessibleTitle } from '@studio/components/AccessibleTitle'; +import { useBreadcrumbs } from '@studio/providers/breadcrumbs/useBreadcrumbs'; +import { GitBranch, Hammer, Search, Send, Terminal } from 'lucide-react'; +import { + type ChangeEvent, + type FC, + type FormEvent, + type ReactNode, + useCallback, + useState, +} from 'react'; + +interface PromptSuggestion { + title: string; + prompt: string; + icon: ReactNode; +} + +const PROMPT_SUGGESTIONS: PromptSuggestion[] = [ + { + title: 'Explore repo', + prompt: 'Give me a concise map of this repo and the main places I should know about.', + icon: , + }, + { + title: 'Draft a change', + prompt: 'Help me plan and implement the next small product improvement in nemo-platform.', + icon: , + }, + { + title: 'Review recent work', + prompt: 'Review the current working tree and call out anything risky or unfinished.', + icon: , + }, +]; + +const PromptCard = ({ + suggestion, + onSelect, +}: { + suggestion: PromptSuggestion; + onSelect: () => void; +}) => ( + + + +); + +const LandingComposer = ({ + input, + onChange, +}: { + input: string; + onChange: (value: string) => void; +}) => { + const handleSubmit = (event: FormEvent) => { + event.preventDefault(); + }; + + return ( +
+