Skip to content

Commit 192c0a6

Browse files
committed
feat(dashboards): resumable chat across navigation/reload + editable Source view
Generation state (chat, streaming steps, the built config) moves out of DashboardWorkspace's component-local useState into a new useDashboardChatStore, keyed by dashboard id (or new:<connectionId> before the first save, with an alias/rekey scheme for the transition). Navigating away no longer aborts the in-flight SSE stream, and on mount, if the persisted dashboard's generationStatus is RUNNING (a turn was in flight when this tab wasn't around, per the paired backend commit), the store polls until it resolves instead of assuming nothing is happening. Also adds: - A Source/Preview toggle with an uncontrolled Monaco HTML editor (defaultValue + remount key, not a controlled value — a controlled value rewrites Monaco's model on every keystroke, resetting the caret mid-word). Apply re-derives the dashboard's title from the edited <title>/<h1> so a manual rename actually propagates to the breadcrumb/gallery, and is disabled while a generation is in flight to avoid racing the backend's own write of the same config. - A Queries panel: DashboardArtifact now reports every query the artifact runs (SQL, row count or error, timing) via a new onQuery prop, wired to a side panel with copy-to-clipboard. dashboardQueryAPI.run now surfaces the backend's actual error message instead of axios's generic "Request failed with status code 400" (the interceptor only reads response.data.message; this endpoint's payload uses `error`). - generateStream's fetch is aborted proactively on the page's `pagehide` event and the resulting failure is dropped rather than surfaced as a chat-history error: a reload/close tears down the request as a bare TypeError, indistinguishable in shape from a dead backend, which would otherwise get permanently written into the saved chat on every reopen.
1 parent c325b5b commit 192c0a6

6 files changed

Lines changed: 849 additions & 148 deletions

File tree

src/components/DashboardArtifact.jsx

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
9797
const REDUCED_MOTION = typeof window !== 'undefined'
9898
&& window.matchMedia?.('(prefers-reduced-motion: reduce)').matches
9999

100-
export default function DashboardArtifact({ connectionId, html, onError, queryFn }) {
100+
export default function DashboardArtifact({ connectionId, html, onError, queryFn, onQuery }) {
101101
const iframeRef = useRef(null)
102102
const [height, setHeight] = useState(600)
103103
const [loaded, setLoaded] = useState(false)
@@ -111,6 +111,12 @@ export default function DashboardArtifact({ connectionId, html, onError, queryFn
111111
const runQueryRef = useRef(null)
112112
runQueryRef.current = queryFn || ((sql, limit, signal) => dashboardQueryAPI.run(connectionId, sql, limit, signal))
113113

114+
// Log every query the artifact runs (SQL, row count, timing) for the
115+
// Queries panel — same ref-indirection as runQueryRef, since pump/runJob's
116+
// closures are frozen at first render (empty-dep useCallback).
117+
const onQueryRef = useRef(null)
118+
onQueryRef.current = onQuery
119+
114120
function post(msg) {
115121
iframeRef.current?.contentWindow?.postMessage(msg, '*')
116122
}
@@ -124,13 +130,18 @@ export default function DashboardArtifact({ connectionId, html, onError, queryFn
124130
}, [])
125131

126132
async function runJob(job) {
133+
const startedAt = Date.now()
127134
for (let attempt = 0; ; attempt += 1) {
128135
const controller = new AbortController()
129136
const timer = setTimeout(() => controller.abort(), QUERY_TIMEOUT_MS)
130137
try {
131138
const res = await runQueryRef.current(job.sql, job.limit, controller.signal)
132139
clearTimeout(timer)
133140
post({ __deepsql: true, type: 'result', id: job.id, columns: res.columns, rows: res.rows })
141+
onQueryRef.current?.({
142+
id: job.id, sql: job.sql, status: 'success',
143+
rowCount: res.rows?.length || 0, durationMs: Date.now() - startedAt, timestamp: startedAt,
144+
})
134145
return
135146
} catch (err) {
136147
clearTimeout(timer)
@@ -142,9 +153,11 @@ export default function DashboardArtifact({ connectionId, html, onError, queryFn
142153
await sleep(400 * (attempt + 1) + Math.floor(Math.random() * 250))
143154
continue
144155
}
145-
post({
146-
__deepsql: true, type: 'result', id: job.id,
147-
error: timedOut ? 'Timed out' : (err?.message || 'query failed'),
156+
const errorMsg = timedOut ? 'Timed out' : (err?.message || 'query failed')
157+
post({ __deepsql: true, type: 'result', id: job.id, error: errorMsg })
158+
onQueryRef.current?.({
159+
id: job.id, sql: job.sql, status: 'error',
160+
rowCount: 0, durationMs: Date.now() - startedAt, timestamp: startedAt, error: errorMsg,
148161
})
149162
return
150163
}

0 commit comments

Comments
 (0)