diff --git a/apps/studymesh/src/components/Dasboard/Dashboard.tsx b/apps/studymesh/src/components/Dasboard/Dashboard.tsx index 37faad33..4ab2bee6 100644 --- a/apps/studymesh/src/components/Dasboard/Dashboard.tsx +++ b/apps/studymesh/src/components/Dasboard/Dashboard.tsx @@ -92,6 +92,52 @@ const DEFAULT_STUDY_PATH_OPENED_KEY = 'studymesh-default-study-path-opened-v1' const USER_ROLE_CHANGED_EVENT = 'studymesh-user-role-changed' const OPEN_SAVED_DASHBOARDS_EVENT = 'studymesh-open-saved-dashboards' +const createNotesPageDashboard = (markdown: string): DefaultDashboard => { + const createdAt = new Date().toISOString() + const titleMatch = markdown.match(/^#\s+(.+)$/m) + const title = titleMatch?.[1]?.trim() || 'Untitled notes' + const widgetId = `notes-page-${Date.now()}` + + return { + name: title, + layout: { + type: 'row', + weight: 100, + children: [ + { + type: 'tabset', + weight: 100, + active: true, + selected: 0, + children: [ + { + type: 'tab', + name: 'Notes', + component: 'CustomWidget', + config: { + customProps: { + widgetId, + components: [ + { + id: `${widgetId}-markdown`, + type: 'MarkdownBlock', + props: { + __blockType: 'MarkdownBlock', + title: 'Scratch notes', + markdown, + }, + }, + ], + }, + }, + }, + ], + }, + ], + }, + } +} + const Dashboards = () => { const { theme, @@ -448,6 +494,25 @@ const Dashboards = () => { addDashboard() } + const startNotesPageDashboard = (markdown: string) => { + if (!isAdmin || !markdown.trim()) { + return + } + + const notesDashboard = createNotesPageDashboard(markdown.trim()) + + if (openDashboards[selectedDashboard] && selectedDashboardIsEmpty) { + replaceDashboard(selectedDashboard, notesDashboard) + } else { + addDashboard(notesDashboard) + } + + dispatchWorkspaceOnboardingEvent({ + type: 'saved-dashboard-opened', + dashboardName: notesDashboard.name, + }) + } + const openSavedDashboardInWorkspace = (dashboard: SavedDashboard) => { if (openDashboards[selectedDashboard] && selectedDashboardIsEmpty) { replaceDashboard(selectedDashboard, { @@ -1459,6 +1524,7 @@ const Dashboards = () => { onMessagesChange={(messages) => updateDashboardChatMessages(currentDashboard, messages) } + onCreateNotesPage={startNotesPageDashboard} onClose={closeDashboardChatPanel} /> ) @@ -1604,6 +1670,7 @@ const Dashboards = () => { onUploadMaterial={() => openCreationSources('upload')} onPasteNotes={() => openCreationSources('paste')} onQuickCreate={openQuickCreateFromEmptyDashboard} + onStartNotesPage={startNotesPageDashboard} onOpenSavedLibrary={openSavedLibraryFromEmptyState} dashboardOptions={visibleDashboardOptions} onOpenDashboard={openSavedDashboardInWorkspace} @@ -1813,6 +1880,7 @@ const Dashboards = () => { onUploadMaterial={() => openCreationSources('upload')} onPasteNotes={() => openCreationSources('paste')} onQuickCreate={openQuickCreateFromEmptyDashboard} + onStartNotesPage={startNotesPageDashboard} onOpenSavedLibrary={openSavedLibraryFromEmptyState} dashboardOptions={visibleDashboardOptions} onOpenDashboard={openSavedDashboardFromEmptyState} diff --git a/apps/studymesh/src/components/Dasboard/DashboardEmptyState.tsx b/apps/studymesh/src/components/Dasboard/DashboardEmptyState.tsx index dbbd3360..45b8ad8b 100644 --- a/apps/studymesh/src/components/Dasboard/DashboardEmptyState.tsx +++ b/apps/studymesh/src/components/Dasboard/DashboardEmptyState.tsx @@ -1,5 +1,13 @@ -import React from 'react' -import { Box, Button, Chip, Paper, Stack, Typography } from '@mui/material' +import React, { useState } from 'react' +import { + Box, + Button, + Chip, + Paper, + Stack, + TextField, + Typography, +} from '@mui/material' import { alpha } from '@mui/material/styles' import AutoStoriesIcon from '@mui/icons-material/AutoStories' import CloudUploadIcon from '@mui/icons-material/CloudUpload' @@ -23,6 +31,7 @@ interface DashboardEmptyStateProps { onUploadMaterial: () => void onPasteNotes: () => void onQuickCreate: (intent: EmptyDashboardQuickCreate) => void + onStartNotesPage: (markdown: string) => void onOpenSavedLibrary: () => void dashboardOptions: SavedDashboard[] onOpenDashboard: (dashboard: SavedDashboard) => void @@ -61,6 +70,7 @@ const DashboardEmptyState = ({ onUploadMaterial, onPasteNotes, onQuickCreate, + onStartNotesPage, onOpenSavedLibrary, dashboardOptions, onOpenDashboard, @@ -87,6 +97,8 @@ const DashboardEmptyState = ({ : 'Open existing study guide' : 'Open existing dashboard' const featuredFolders = folderEntries.slice(0, 4) + const [notesDraft, setNotesDraft] = useState('') + const normalizedNotesDraft = notesDraft.trim() return ( + alpha(theme.palette.primary.main, 0.24), + bgcolor: (theme) => alpha(theme.palette.primary.main, 0.055), + }} + > + + + + Start with notes, not AI + + + Write freely in Markdown like a blank Notion page. It opens as + a temporary dashboard with one notes widget, and it only + becomes a saved dashboard when you save it. + + + setNotesDraft(event.target.value)} + placeholder={ + '# My notes\n\nDump your thoughts, links, questions, or source material here...' + } + multiline + minRows={5} + fullWidth + inputProps={{ 'aria-label': 'Markdown notes draft' }} + sx={{ + '& .MuiOutlinedInput-root': { + borderRadius: 2, + bgcolor: 'background.paper', + alignItems: 'flex-start', + fontFamily: 'JetBrains Mono, monospace', + fontSize: '0.92rem', + }, + }} + /> + + + + + + + void + onCreateNotesPage?: (markdown: string) => void onClose: () => void } @@ -56,11 +57,13 @@ const DashboardChatPanel = ({ dashboard, messages, onMessagesChange, + onCreateNotesPage, onClose, }: DashboardChatPanelProps) => { const theme = useTheme() const isMobile = useMediaQuery(theme.breakpoints.down('md')) const [draft, setDraft] = useState('') + const [notesDraft, setNotesDraft] = useState('') const [error, setError] = useState('') const [activeStartedAt, setActiveStartedAt] = useState(null) const [elapsedSeconds, setElapsedSeconds] = useState(0) @@ -289,6 +292,57 @@ const DashboardChatPanel = ({ + {onCreateNotesPage && ( + + + + + Private notes lane + + + Think out loud without asking AI. Turn it into a temporary + Markdown dashboard when it becomes useful. + + + setNotesDraft(event.target.value)} + placeholder="Write your own notes here..." + multiline + minRows={3} + fullWidth + size="small" + inputProps={{ 'aria-label': 'Private notes draft' }} + /> + + + + )} { expect( screen.getByRole('button', { name: /paste notes/i }), ).toBeInTheDocument() + expect(screen.getByText(/start with notes, not ai/i)).toBeInTheDocument() + expect( + screen.getByRole('textbox', { name: /markdown notes draft/i }), + ).toBeInTheDocument() expect( screen.getByRole('button', { name: /create quiz/i }), ).toBeInTheDocument() @@ -246,6 +250,30 @@ describe('Dashboards', () => { ).not.toBeInTheDocument() }) + it('turns an empty dashboard notes draft into a temporary Markdown dashboard', () => { + const replaceDashboard = vi.fn() + mockDashboardProvider({ replaceDashboard }) + + render() + + fireEvent.change( + screen.getByRole('textbox', { name: /markdown notes draft/i }), + { target: { value: '# Biology dump\n\n- cells need energy' } }, + ) + fireEvent.click(screen.getByRole('button', { name: /open notes page/i })) + + expect(replaceDashboard).toHaveBeenCalledWith( + 0, + expect.objectContaining({ + name: 'Biology dump', + layout: expect.objectContaining({ type: 'row' }), + }), + ) + expect(JSON.stringify(replaceDashboard.mock.calls[0][1].layout)).toContain( + 'cells need energy', + ) + }) + it('opens the Study Path modal when the primary empty workspace action is used', () => { const createHubListener = vi.fn() window.addEventListener('studymesh-open-create-hub', createHubListener)