Skip to content
Merged
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
47 changes: 40 additions & 7 deletions src/components/sections/DashboardWorkspace.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,20 @@ const EXAMPLE_PROMPTS = [
{ icon: Layers, title: 'Business overview', prompt: 'Build a dashboard with the most important KPIs and a couple of charts summarizing overall business health.' },
]

// Rotates under the real live step while a build is in flight — genuine,
// shippped-feature facts (not filler), so the wait teaches something instead
// of just decorating it. Kept short; the real step above stays the primary line.
const BUILD_TIPS = [
'Every query is verified against your real data before it ever reaches the screen.',
'Dashboards run read-only — the agent can look, never write.',
'Ask to add a chart or change a metric any time — it edits in place.',
'Every save keeps a version history, so you can always roll back a change.',
'Publish a dashboard to get a read-only link — no login required to view it.',
'Turn any dashboard into a TV/kiosk view that auto-refreshes on a wall display.',
'Set a plain-English alert and DeepSQL checks it on a schedule for you.',
'Duplicate a dashboard to branch it without touching the original.',
]

// Focused, chrome-less builder. Left = the agent (generate / refine); main = the
// live dashboard canvas. The DeepSQL logo and breadcrumb return to the gallery.
//
Expand Down Expand Up @@ -234,6 +248,17 @@ export default function DashboardWorkspace({ connectionId, dashboard, onClose })

useEffect(() => { if (scrollRef.current) scrollRef.current.scrollTop = scrollRef.current.scrollHeight }, [messages, thinking])

// Rotates BUILD_TIPS while the main canvas shows the "Building…" state, so a
// 15-30s wait has something changing on screen beyond the elapsed-time
// ticker. Starts at a random index (not always the same first tip) and
// resets once the build ends so the next one starts fresh.
const [tipIndex, setTipIndex] = useState(() => Math.floor(Math.random() * BUILD_TIPS.length))
useEffect(() => {
if (!thinking) return undefined
const id = setInterval(() => setTipIndex((i) => (i + 1) % BUILD_TIPS.length), 3500)
return () => clearInterval(id)
}, [thinking])

const refreshNow = useCallback(() => {
artifactRef.current?.reload()
setLastRefreshedAt(Date.now())
Expand Down Expand Up @@ -891,17 +916,25 @@ export default function DashboardWorkspace({ connectionId, dashboard, onClose })
)}
</div>
</>
) : (
) : thinking ? (
<div className={styles.newCanvas}>
<div className={styles.newCanvasInner}>
<span className={styles.newIcon}><Sparkles size={22} color="#534AB7" /></span>
<h2 className={styles.newTitle}>{thinking ? 'Building your dashboard…' : 'Nothing built here yet'}</h2>
<span className={styles.newIconPulse}><Sparkles size={22} color="#534AB7" /></span>
<h2 className={styles.newTitle}>Building your dashboard…</h2>
<p className={styles.newSub}>
{thinking
? 'The agent is grounding on your schema and verifying every query — this usually takes a bit.'
: '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.'}
{steps.length > 0 ? steps[steps.length - 1].message : 'Grounding on your schema and verifying every query…'}
<span className={styles.buildElapsed}> · {elapsed}s</span>
</p>
{!thinking && <button className={styles.backLink} onClick={onClose}><ChevronLeft size={14} /> Back to dashboards</button>}
<p key={tipIndex} className={styles.buildTip}>{BUILD_TIPS[tipIndex]}</p>
</div>
</div>
) : (
<div className={styles.newCanvas}>
<div className={styles.newCanvasInner}>
<span className={styles.newIcon}><Sparkles size={22} color="#534AB7" /></span>
<h2 className={styles.newTitle}>Nothing built here yet</h2>
<p className={styles.newSub}>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.</p>
<button className={styles.backLink} onClick={onClose}><ChevronLeft size={14} /> Back to dashboards</button>
</div>
</div>
)}
Expand Down
49 changes: 49 additions & 0 deletions src/components/sections/DashboardWorkspace.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -1243,9 +1243,58 @@
box-shadow: 0 1px 2px rgba(83, 74, 183, 0.08);
}

/* Same shape as .newIcon, but alive while a build is in flight — a static
sparkle here reads as frozen given the sidebar's own trace is animating. */
.newIconPulse {
width: 52px;
height: 52px;
border-radius: 14px;
background: #EEEDFE;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 4px;
box-shadow: 0 1px 2px rgba(83, 74, 183, 0.08);
animation: dsqlBuildPulse 1.8s ease-in-out infinite;
}

@keyframes dsqlBuildPulse {
0%, 100% { transform: scale(1); box-shadow: 0 1px 2px rgba(83, 74, 183, 0.08), 0 0 0 0 rgba(83, 74, 183, 0.18); }
50% { transform: scale(1.06); box-shadow: 0 1px 2px rgba(83, 74, 183, 0.08), 0 0 0 10px rgba(83, 74, 183, 0); }
}

@media (prefers-reduced-motion: reduce) {
.newIconPulse { animation: none; }
}

.newTitle { font-size: 19px; font-weight: 650; letter-spacing: -0.014em; color: #111; margin: 0; }
.newSub { font-size: 13px; color: #777; max-width: 380px; line-height: 1.6; margin: 0; }

.buildElapsed { color: #a3a3a3; font-variant-numeric: tabular-nums; }

/* Rotates every few seconds (see BUILD_TIPS) — the `key={tipIndex}` on the
element itself retriggers this fade-in each time the tip text changes. */
.buildTip {
margin: 18px 0 0;
padding: 10px 16px;
max-width: 420px;
font-size: 12.5px;
line-height: 1.55;
color: #534AB7;
background: #F5F3FF;
border-radius: 10px;
animation: dsqlTipIn 320ms cubic-bezier(0.2, 0.8, 0.2, 1);
}

@keyframes dsqlTipIn {
from { opacity: 0; transform: translateY(4px); }
to { opacity: 1; transform: none; }
}

@media (prefers-reduced-motion: reduce) {
.buildTip { animation: none; }
}

.exampleGrid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
Expand Down
31 changes: 29 additions & 2 deletions src/components/sections/DashboardsHome.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,19 @@ function Thumb({ id }) {
)
}

// Placeholder count for the initial-load skeleton — enough to fill a typical
// viewport width without over-committing to a specific grid density.
const SKELETON_COUNT = 8

export default function DashboardsHome({ connectionId, onOpen }) {
const [dashboards, setDashboards] = useState([])
const [loading, setLoading] = useState(false)
// Starts true (not false) so the very first render — before the fetch effect
// has even run — shows the loading skeleton, not the "No dashboards yet"
// empty state. Without this, dashboards.length === 0 && !loading was
// momentarily true on every mount, flashing the empty state before the real
// list (or genuine empty state) landed a beat later.
const [loading, setLoading] = useState(true)
const [hasLoadedOnce, setHasLoadedOnce] = useState(false)
const [filter, setFilter] = useState('all')
const [folder, setFolder] = useState(null) // null = all folders
const [search, setSearch] = useState('')
Expand Down Expand Up @@ -161,9 +171,14 @@ export default function DashboardsHome({ connectionId, onOpen }) {
setDashboards([])
} finally {
setLoading(false)
setHasLoadedOnce(true)
}
}, [connectionId])

// Switching connections must re-show the skeleton, not the previous
// connection's cards (or a premature "no dashboards" for the new one) while
// the new fetch is in flight.
useEffect(() => { setHasLoadedOnce(false); setLoading(true) }, [connectionId])
useEffect(() => { load() }, [load])

const folders = useMemo(() => {
Expand Down Expand Up @@ -241,7 +256,19 @@ export default function DashboardsHome({ connectionId, onOpen }) {
))}
</div>

{!loading && dashboards.length === 0 ? (
{!hasLoadedOnce ? (
<div className={styles.grid}>
{Array.from({ length: SKELETON_COUNT }, (_, i) => (
<div key={i} className={styles.skeletonCard}>
<div className={styles.skeletonThumb} />
<div className={styles.skeletonBody}>
<div className={styles.skeletonLine} style={{ width: '60%' }} />
<div className={styles.skeletonLine} style={{ width: '35%' }} />
</div>
</div>
))}
</div>
) : dashboards.length === 0 ? (
<div className={styles.emptyState}>
<span className={styles.emptyIcon}><LayoutDashboard size={22} /></span>
<h2 className={styles.emptyTitle}>No dashboards yet</h2>
Expand Down
41 changes: 41 additions & 0 deletions src/components/sections/DashboardsHome.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,47 @@
color: #999;
}

/* Initial-load placeholder — mirrors .card's shape (thumb + body) so the grid
doesn't jump when real cards replace these. */
.skeletonCard {
display: flex;
flex-direction: column;
border: 1px solid #ececec;
border-radius: 14px;
background: #fff;
overflow: hidden;
}

.skeletonThumb {
height: 108px;
background: linear-gradient(180deg, #fbfaff 0%, #fafafa 100%);
border-bottom: 1px solid #f0f0f0;
}

.skeletonBody {
display: flex;
flex-direction: column;
gap: 8px;
padding: 14px;
}

.skeletonLine {
height: 11px;
border-radius: 5px;
background: linear-gradient(90deg, #eee 25%, #f5f5f5 50%, #eee 75%);
background-size: 200% 100%;
animation: dsqlshimmer 1.4s ease-in-out infinite;
}

@keyframes dsqlshimmer {
from { background-position: 200% 0; }
to { background-position: -200% 0; }
}

@media (prefers-reduced-motion: reduce) {
.skeletonLine { animation: none; background: #eee; }
}

.ctaCard {
display: flex;
flex-direction: column;
Expand Down
Loading