Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions apps/studymesh/src/components/Dasboard/Dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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, {
Expand Down Expand Up @@ -1459,6 +1524,7 @@ const Dashboards = () => {
onMessagesChange={(messages) =>
updateDashboardChatMessages(currentDashboard, messages)
}
onCreateNotesPage={startNotesPageDashboard}
onClose={closeDashboardChatPanel}
/>
)
Expand Down Expand Up @@ -1604,6 +1670,7 @@ const Dashboards = () => {
onUploadMaterial={() => openCreationSources('upload')}
onPasteNotes={() => openCreationSources('paste')}
onQuickCreate={openQuickCreateFromEmptyDashboard}
onStartNotesPage={startNotesPageDashboard}
onOpenSavedLibrary={openSavedLibraryFromEmptyState}
dashboardOptions={visibleDashboardOptions}
onOpenDashboard={openSavedDashboardInWorkspace}
Expand Down Expand Up @@ -1813,6 +1880,7 @@ const Dashboards = () => {
onUploadMaterial={() => openCreationSources('upload')}
onPasteNotes={() => openCreationSources('paste')}
onQuickCreate={openQuickCreateFromEmptyDashboard}
onStartNotesPage={startNotesPageDashboard}
onOpenSavedLibrary={openSavedLibraryFromEmptyState}
dashboardOptions={visibleDashboardOptions}
onOpenDashboard={openSavedDashboardFromEmptyState}
Expand Down
91 changes: 89 additions & 2 deletions apps/studymesh/src/components/Dasboard/DashboardEmptyState.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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
Expand Down Expand Up @@ -61,6 +70,7 @@ const DashboardEmptyState = ({
onUploadMaterial,
onPasteNotes,
onQuickCreate,
onStartNotesPage,
onOpenSavedLibrary,
dashboardOptions,
onOpenDashboard,
Expand All @@ -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 (
<Box
Expand Down Expand Up @@ -245,6 +257,81 @@ const DashboardEmptyState = ({
<ChevronRightIcon color="primary" />
</Paper>

<Paper
elevation={0}
sx={{
p: { xs: 1.25, sm: 1.5 },
borderRadius: 2.5,
border: 1,
borderColor: (theme) => alpha(theme.palette.primary.main, 0.24),
bgcolor: (theme) => alpha(theme.palette.primary.main, 0.055),
}}
>
<Stack spacing={1.25}>
<Box>
<Typography variant="subtitle1" fontWeight={900}>
Start with notes, not AI
</Typography>
<Typography
variant="body2"
color="text.secondary"
sx={{ mt: 0.3 }}
>
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.
</Typography>
</Box>
<TextField
value={notesDraft}
onChange={(event) => 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',
},
}}
/>
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={1}>
<Button
variant="contained"
onClick={() => onStartNotesPage(normalizedNotesDraft)}
disabled={!isAdmin || !normalizedNotesDraft}
sx={{
textTransform: 'none',
fontWeight: 900,
borderRadius: 2,
}}
>
Open notes page
</Button>
<Button
variant="outlined"
onClick={onPasteNotes}
disabled={!isAdmin}
sx={{
textTransform: 'none',
fontWeight: 900,
borderRadius: 2,
bgcolor: 'background.paper',
}}
>
Use Creation instead
</Button>
</Stack>
</Stack>
</Paper>

<Box
sx={{
display: 'grid',
Expand Down
54 changes: 54 additions & 0 deletions apps/studymesh/src/components/dashboardChat/DashboardChatPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ interface DashboardChatPanelProps {
dashboard?: StateDashboard
messages: DashboardChatMessage[]
onMessagesChange: (messages: DashboardChatMessage[]) => void
onCreateNotesPage?: (markdown: string) => void
onClose: () => void
}

Expand All @@ -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<number | null>(null)
const [elapsedSeconds, setElapsedSeconds] = useState(0)
Expand Down Expand Up @@ -289,6 +292,57 @@ const DashboardChatPanel = ({
</Stack>
</Box>

{onCreateNotesPage && (
<Box
sx={{
px: 2,
py: 1.5,
borderBottom: 1,
borderColor: 'divider',
bgcolor: 'background.default',
}}
>
<Stack spacing={1}>
<Box>
<Typography variant="subtitle2" fontWeight={900}>
Private notes lane
</Typography>
<Typography variant="caption" color="text.secondary">
Think out loud without asking AI. Turn it into a temporary
Markdown dashboard when it becomes useful.
</Typography>
</Box>
<TextField
value={notesDraft}
onChange={(event) => setNotesDraft(event.target.value)}
placeholder="Write your own notes here..."
multiline
minRows={3}
fullWidth
size="small"
inputProps={{ 'aria-label': 'Private notes draft' }}
/>
<Button
variant="outlined"
size="small"
onClick={() => {
const markdown = notesDraft.trim()
if (!markdown) return
onCreateNotesPage(markdown)
setNotesDraft('')
}}
disabled={!notesDraft.trim()}
sx={{
alignSelf: 'flex-start',
textTransform: 'none',
fontWeight: 800,
}}
>
Open as notes dashboard
</Button>
</Stack>
</Box>
)}
<Box
sx={{
flex: 1,
Expand Down
28 changes: 28 additions & 0 deletions apps/studymesh/tests/unit/components/dashboard/Dashboard.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,10 @@ describe('Dashboards', () => {
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()
Expand All @@ -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(<Dashboards />)

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)
Expand Down