diff --git a/backend/src/main/java/com/dbaagent/controller/DashboardGenerationController.java b/backend/src/main/java/com/dbaagent/controller/DashboardGenerationController.java
index 5824c57..0ce3faf 100644
--- a/backend/src/main/java/com/dbaagent/controller/DashboardGenerationController.java
+++ b/backend/src/main/java/com/dbaagent/controller/DashboardGenerationController.java
@@ -28,8 +28,9 @@
*
* - {@code POST /api/dashboards/generate} — blocking; returns the validated config.
* - {@code POST /api/dashboards/generate/stream} — SSE; streams the agent's live
- * steps ({@code step} events: grounding → planning → validating) then a {@code done}
- * event with the final config (or an {@code error} event).
+ * steps ({@code step} events: grounding → planning → validating) then either a
+ * {@code chat} event (out-of-context reply) or a {@code done} event with the
+ * artifact config (or an {@code error} event).
*
*
* Read-only: generates a config and validates queries by running them read-only; it never
@@ -101,8 +102,20 @@ public SseEmitter generateStream(@RequestBody GenerateRequest request) {
throw new ClientGoneException(io);
}
});
- emitter.send(SseEmitter.event().name("done")
- .data(Map.of("success", true, "dashboardConfig", config)));
+ // Chat-only replies (greetings / tool questions) must not share the
+ // `done` event with a real artifact — the FE's done handler always
+ // appends "Done — built…" and auto-saves. A dedicated `chat` event
+ // keeps that path from swallowing out-of-context messages.
+ if (Boolean.TRUE.equals(config.get("chat"))) {
+ emitter.send(SseEmitter.event().name("chat")
+ .data(Map.of(
+ "success", true,
+ "reply", String.valueOf(config.getOrDefault("reply", "")),
+ "dashboardConfig", config)));
+ } else {
+ emitter.send(SseEmitter.event().name("done")
+ .data(Map.of("success", true, "dashboardConfig", config)));
+ }
emitter.complete();
} catch (ClientGoneException gone) {
emitter.complete();
diff --git a/backend/src/main/java/com/dbaagent/service/DashboardAgentService.java b/backend/src/main/java/com/dbaagent/service/DashboardAgentService.java
index 47ac0e9..15e0c05 100644
--- a/backend/src/main/java/com/dbaagent/service/DashboardAgentService.java
+++ b/backend/src/main/java/com/dbaagent/service/DashboardAgentService.java
@@ -97,6 +97,9 @@ public Map generate(String connectionId, String prompt, Object c
chat.put("chat", true);
chat.put("reply", chatReply.text().trim());
chat.put("trace", trace);
+ log.info("Dashboard chat-only reply ({} chars) for prompt: {}",
+ chat.get("reply").toString().length(),
+ prompt == null ? "" : prompt.trim());
return chat;
}
// Ambiguous or the agent didn't just answer — fall through to a real build
diff --git a/src/components/sections/DashboardWorkspace.jsx b/src/components/sections/DashboardWorkspace.jsx
index 40964de..e1ac508 100644
--- a/src/components/sections/DashboardWorkspace.jsx
+++ b/src/components/sections/DashboardWorkspace.jsx
@@ -135,9 +135,15 @@ export default function DashboardWorkspace({ connectionId, dashboard, onClose })
},
onDone: (next) => {
abortRef.current = null
- setConfig(next)
setThinking(false)
setSteps([])
+ // Belt-and-braces: a chat-shaped payload must never hit the "built" path
+ // (that appends the canned save line and would clobber a real artifact).
+ if (!next?.html || next?.chat) {
+ setMessages((m) => [...m, { role: 'agent', text: next?.reply || '…' }])
+ return
+ }
+ setConfig(next)
// Auto-save as a draft so a refresh never loses it (create first time, update after).
const updated = [...messagesRef.current, { role: 'agent', text: 'Done — built and verified against your data. Saved as a draft — tell me what to change.' }]
setMessages(updated)
diff --git a/src/lib/api/client.js b/src/lib/api/client.js
index d70424f..c1e0663 100644
--- a/src/lib/api/client.js
+++ b/src/lib/api/client.js
@@ -2724,6 +2724,9 @@ export const dashboardGenAPI = {
let data = {};
try { data = JSON.parse(dataLines.join("\n")); } catch { return; }
if (event === "step") onStep && onStep(data);
+ // `chat` = out-of-context reply (hi/thanks); must not fall through to `done`
+ // or the workspace appends the canned "Done — built…" save message.
+ else if (event === "chat") { finished = true; onDone && onDone(data); }
else if (event === "done") { finished = true; onDone && onDone(data); }
else if (event === "error") { finished = true; onError && onError(new Error(data?.error || "Generation failed")); }
};
diff --git a/src/lib/dashboardGenerator.js b/src/lib/dashboardGenerator.js
index b4166d9..53bbaa5 100644
--- a/src/lib/dashboardGenerator.js
+++ b/src/lib/dashboardGenerator.js
@@ -15,15 +15,20 @@ export function generateDashboardStream(connectionId, prompt, currentConfig, { o
return dashboardGenAPI.generateStream(connectionId, prompt, currentConfig, {
onStep,
onDone: (data) => {
- if (data?.dashboardConfig?.chat) {
- onChat && onChat(data.dashboardConfig.reply || '')
+ // Chat-only can arrive as event:chat (reply at top level) or legacy done
+ // with dashboardConfig.chat=true. Treat either as a plain reply — never as
+ // a successful build (that path hardcodes "Done — built…" in the UI).
+ const cfg = data?.dashboardConfig
+ const chatReply = (typeof data?.reply === 'string' && data.reply) || cfg?.reply || ''
+ if (data?.chat === true || cfg?.chat === true || (chatReply && !cfg?.html && !cfg?.renderMode)) {
+ onChat && onChat(chatReply)
return
}
- if (!data?.dashboardConfig) {
+ if (!cfg) {
onError && onError(new Error(data?.error || 'Generation returned no dashboard.'))
return
}
- onDone && onDone({ ...data.dashboardConfig, updatedAt: new Date().toISOString() })
+ onDone && onDone({ ...cfg, updatedAt: new Date().toISOString() })
},
onError,
})