11import { useState , useEffect , useRef , useCallback , lazy , Suspense } from 'react'
2- import { LineChart , ArrowUp , ChevronLeft , Sparkles , Check , Loader2 , Brain , PencilRuler , ClipboardCheck , TrendingUp , Users , PieChart , Layers , Code2 , X , Copy , Undo2 , Play , Database , History , RotateCcw , Eye , RefreshCw , ChevronDown , BellRing , Trash2 } from 'lucide-react'
2+ import { LineChart , ArrowUp , ChevronLeft , Sparkles , Check , Loader2 , Brain , PencilRuler , Pencil , ClipboardCheck , TrendingUp , Users , PieChart , Layers , Code2 , X , Copy , Undo2 , Play , Database , History , RotateCcw , Eye , RefreshCw , ChevronDown , BellRing , Trash2 } from 'lucide-react'
33import DashboardArtifact from '@/components/DashboardArtifact'
44import ShareMenu from './ShareMenu'
55import { savedDashboardsAPI } from '@/lib/api/client'
@@ -117,8 +117,8 @@ export default function DashboardWorkspace({ connectionId, dashboard, onClose })
117117 const key = keyRef . current
118118
119119 const session = useDashboardSession ( key )
120- const { ensureSession, patchSession, releaseAlias, submitPrompt, resumeIfRunning, persistDraft } = useDashboardChatActions ( )
121- const { messages, thinking, steps, startedAt, config, savedId, dirty, liveShell, liveWidget, renderHtml } = session
120+ const { ensureSession, patchSession, releaseAlias, submitPrompt, resumeIfRunning, persistDraft, renameDashboard } = useDashboardChatActions ( )
121+ const { messages, thinking, steps, startedAt, config, savedId, dirty, liveShell, liveWidget, renderHtml, titleOverride } = session
122122
123123 const [ input , setInput ] = useState ( '' )
124124 const [ elapsed , setElapsed ] = useState ( 0 )
@@ -281,14 +281,54 @@ export default function DashboardWorkspace({ connectionId, dashboard, onClose })
281281 // to show, and resetting again here would wrongly wipe the ones just logged.
282282 useEffect ( ( ) => { if ( liveShell ) setQueries ( [ ] ) } , [ liveShell ] )
283283
284- const name = config ?. title || ( isNew ? 'New dashboard' : dashboard ?. name ) || 'Dashboard'
284+ // A user's explicit rename always wins — over both the HTML-derived title
285+ // and the saved row's name — until they rename again (see renameDashboard).
286+ const name = titleOverride || config ?. title || ( isNew ? 'Untitled dashboard' : dashboard ?. name ) || 'Untitled dashboard'
285287 // "Live" once published to the web; the ShareMenu keeps this in sync.
286288 const status = isPublic ? 'live' : 'draft'
287289 const sourceDirty = sourceDraft !== ( config ?. html || '' )
288290
291+ const [ editingTitle , setEditingTitle ] = useState ( false )
292+ const [ titleDraft , setTitleDraft ] = useState ( name )
293+ const titleInputRef = useRef ( null )
294+
295+ function startEditingTitle ( ) {
296+ setTitleDraft ( name )
297+ setEditingTitle ( true )
298+ }
299+ function commitTitle ( ) {
300+ setEditingTitle ( false )
301+ const trimmed = titleDraft . trim ( )
302+ if ( ! trimmed || trimmed === name ) return
303+ renameDashboard ( key , trimmed )
304+ }
305+
306+ useEffect ( ( ) => {
307+ if ( editingTitle ) { titleInputRef . current ?. focus ( ) ; titleInputRef . current ?. select ( ) }
308+ } , [ editingTitle ] )
309+
310+ // v0.dev-style intro: a brand-new dashboard starts with the composer centered
311+ // and no side panel/canvas chrome at all. The instant the first user message
312+ // is sent, it docks into the left panel and the canvas takes over — driven by
313+ // whether a user message exists yet (not local state), so it resolves
314+ // correctly even if this component remounts mid-turn (e.g. reopening a
315+ // dashboard whose first turn already completed skips the intro entirely).
316+ const hasUserMessage = messages . some ( ( m ) => m . role === 'user' )
317+ const [ docking , setDocking ] = useState ( false )
318+ // Keep rendering the intro layout for one animation cycle after the message
319+ // that ends it — flipping the layout in the same commit as the message would
320+ // cut straight to the docked panel instead of gliding into it.
321+ const intro = isNew && ( ! hasUserMessage || docking )
322+ useEffect ( ( ) => {
323+ if ( ! docking ) return undefined
324+ const id = setTimeout ( ( ) => setDocking ( false ) , 420 )
325+ return ( ) => clearTimeout ( id )
326+ } , [ docking ] )
327+
289328 function submit ( directPrompt ) {
290329 const prompt = ( directPrompt ?? input ) . trim ( )
291330 if ( ! prompt || thinking ) return
331+ if ( isNew && ! hasUserMessage ) setDocking ( true )
292332 setInput ( '' )
293333 submitPrompt ( key , connectionId , prompt )
294334 }
@@ -445,7 +485,24 @@ export default function DashboardWorkspace({ connectionId, dashboard, onClose })
445485 </ button >
446486 < button className = { styles . crumbLink } onClick = { onClose } > Dashboards</ button >
447487 < span className = { styles . sep } > /</ span >
448- < span className = { styles . crumbCur } > { name } </ span >
488+ { editingTitle ? (
489+ < input
490+ ref = { titleInputRef }
491+ className = { styles . crumbInput }
492+ value = { titleDraft }
493+ onChange = { ( e ) => setTitleDraft ( e . target . value ) }
494+ onBlur = { commitTitle }
495+ onKeyDown = { ( e ) => {
496+ if ( e . key === 'Enter' ) { e . preventDefault ( ) ; commitTitle ( ) }
497+ if ( e . key === 'Escape' ) { e . preventDefault ( ) ; setEditingTitle ( false ) }
498+ } }
499+ />
500+ ) : (
501+ < button className = { styles . crumbEditable } onClick = { startEditingTitle } title = "Rename dashboard" >
502+ < span className = { styles . crumbCur } > { name } </ span >
503+ < Pencil size = { 12 } className = { styles . crumbEditIcon } />
504+ </ button >
505+ ) }
449506 < span className = { styles . spacer } />
450507 < span className = { status === 'live' ? styles . pillLive : styles . pillDraft } > { status === 'live' ? 'Live' : 'Draft' } </ span >
451508 { config && ( dirty || ! savedId ) && (
@@ -462,15 +519,22 @@ export default function DashboardWorkspace({ connectionId, dashboard, onClose })
462519 />
463520 </ header >
464521
465- < div className = { styles . body } >
466- < aside className = { styles . agent } >
467- < div className = { styles . agentScroll } ref = { scrollRef } >
468- { messages . map ( ( msg , i ) => (
522+ < div className = { intro ? styles . bodyIntro : styles . body } >
523+ < aside className = { intro ? `${ styles . agentIntro } ${ docking ? styles . agentDocking : '' } ` : styles . agent } >
524+ { intro && ! docking && (
525+ < div className = { styles . introHead } >
526+ < span className = { styles . newIcon } > < Sparkles size = { 22 } color = "#534AB7" /> </ span >
527+ < h2 className = { styles . newTitle } > Build a dashboard</ h2 >
528+ < p className = { styles . newSub } > Describe what you want and the DeepSQL agent builds it — read-only, grounded on your data.</ p >
529+ </ div >
530+ ) }
531+ < div className = { intro ? styles . agentScrollIntro : styles . agentScroll } ref = { scrollRef } >
532+ { ! intro && messages . map ( ( msg , i ) => (
469533 < div key = { i } className = { msg . role === 'user' ? styles . bubbleUser : ( msg . error ? styles . bubbleErr : styles . bubbleAgent ) } >
470534 { msg . text }
471535 </ div >
472536 ) ) }
473- { thinking && (
537+ { ! intro && thinking && (
474538 < div className = { styles . trace } >
475539 < div className = { styles . traceHead } >
476540 < span className = { styles . traceHeadLabel } >
@@ -501,7 +565,18 @@ export default function DashboardWorkspace({ connectionId, dashboard, onClose })
501565 </ div >
502566 ) }
503567 </ div >
504- < div className = { styles . composer } >
568+ { intro && ! docking && (
569+ < div className = { styles . exampleGridIntro } >
570+ { EXAMPLE_PROMPTS . map ( ( { icon : Icon , title, prompt } ) => (
571+ < button key = { title } className = { styles . exampleCard } onClick = { ( ) => submit ( prompt ) } disabled = { thinking } >
572+ < span className = { styles . exampleIcon } > < Icon size = { 16 } /> </ span >
573+ < span className = { styles . exampleTitle } > { title } </ span >
574+ < span className = { styles . examplePrompt } > { prompt } </ span >
575+ </ button >
576+ ) ) }
577+ </ div >
578+ ) }
579+ < div className = { intro ? `${ styles . composerIntro } ${ docking ? styles . composerDocking : '' } ` : styles . composer } >
505580 < textarea
506581 ref = { inputRef }
507582 rows = { 1 }
@@ -514,8 +589,10 @@ export default function DashboardWorkspace({ connectionId, dashboard, onClose })
514589 />
515590 < button className = { styles . sendBtn } onClick = { ( ) => submit ( ) } disabled = { thinking || ! input . trim ( ) } aria-label = "Send" > < ArrowUp size = { 16 } /> </ button >
516591 </ div >
592+ { intro && ! docking && < button className = { styles . backLinkIntro } onClick = { onClose } > < ChevronLeft size = { 14 } /> Back to dashboards</ button > }
517593 </ aside >
518594
595+ { ! intro && (
519596 < main className = { styles . canvas } >
520597 { config ?. html || liveConfig ? (
521598 < >
@@ -818,33 +895,18 @@ export default function DashboardWorkspace({ connectionId, dashboard, onClose })
818895 < div className = { styles . newCanvas } >
819896 < div className = { styles . newCanvasInner } >
820897 < span className = { styles . newIcon } > < Sparkles size = { 22 } color = "#534AB7" /> </ span >
821- < h2 className = { styles . newTitle } > { isNew ? 'Build a dashboard' : 'Nothing built here yet' } </ h2 >
898+ < h2 className = { styles . newTitle } > { thinking ? 'Building your dashboard… ' : 'Nothing built here yet' } </ h2 >
822899 < p className = { styles . newSub } >
823- { isNew
824- ? 'Describe what you want on the left and the DeepSQL agent builds it here — read-only, grounded on your data .'
900+ { thinking
901+ ? 'The agent is grounding on your schema and verifying every query — this usually takes a bit .'
825902 : 'This dashboard doesn’t have a build yet — the chat on the left may just be planning so far. Ask for a chart to get started.' }
826903 </ p >
827-
828- < div className = { styles . exampleGrid } >
829- { EXAMPLE_PROMPTS . map ( ( { icon : Icon , title, prompt } ) => (
830- < button
831- key = { title }
832- className = { styles . exampleCard }
833- onClick = { ( ) => submit ( prompt ) }
834- disabled = { thinking }
835- >
836- < span className = { styles . exampleIcon } > < Icon size = { 16 } /> </ span >
837- < span className = { styles . exampleTitle } > { title } </ span >
838- < span className = { styles . examplePrompt } > { prompt } </ span >
839- </ button >
840- ) ) }
841- </ div >
842-
843- < button className = { styles . backLink } onClick = { onClose } > < ChevronLeft size = { 14 } /> Back to dashboards</ button >
904+ { ! thinking && < button className = { styles . backLink } onClick = { onClose } > < ChevronLeft size = { 14 } /> Back to dashboards</ button > }
844905 </ div >
845906 </ div >
846907 ) }
847908 </ main >
909+ ) }
848910 </ div >
849911 </ div >
850912 )
0 commit comments