diff --git a/src/components/sections/DashboardWorkspace.jsx b/src/components/sections/DashboardWorkspace.jsx index b49b8e7..404b137 100644 --- a/src/components/sections/DashboardWorkspace.jsx +++ b/src/components/sections/DashboardWorkspace.jsx @@ -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. // @@ -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()) @@ -891,17 +916,25 @@ export default function DashboardWorkspace({ connectionId, dashboard, onClose }) )} - ) : ( + ) : thinking ? (
- -

{thinking ? 'Building your dashboard…' : 'Nothing built here yet'}

+ +

Building your dashboard…

- {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…'} + · {elapsed}s

- {!thinking && } +

{BUILD_TIPS[tipIndex]}

+
+
+ ) : ( +
+
+ +

Nothing built here yet

+

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.

+
)} diff --git a/src/components/sections/DashboardWorkspace.module.css b/src/components/sections/DashboardWorkspace.module.css index b1c0ebf..11eeaa5 100644 --- a/src/components/sections/DashboardWorkspace.module.css +++ b/src/components/sections/DashboardWorkspace.module.css @@ -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)); diff --git a/src/components/sections/DashboardsHome.jsx b/src/components/sections/DashboardsHome.jsx index e5a3c70..6b48bc0 100644 --- a/src/components/sections/DashboardsHome.jsx +++ b/src/components/sections/DashboardsHome.jsx @@ -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('') @@ -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(() => { @@ -241,7 +256,19 @@ export default function DashboardsHome({ connectionId, onOpen }) { ))} - {!loading && dashboards.length === 0 ? ( + {!hasLoadedOnce ? ( +
+ {Array.from({ length: SKELETON_COUNT }, (_, i) => ( +
+
+
+
+
+
+
+ ))} +
+ ) : dashboards.length === 0 ? (

No dashboards yet

diff --git a/src/components/sections/DashboardsHome.module.css b/src/components/sections/DashboardsHome.module.css index 8c1a0e0..7d3225c 100644 --- a/src/components/sections/DashboardsHome.module.css +++ b/src/components/sections/DashboardsHome.module.css @@ -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;