diff --git a/backend/src/main/java/com/dbaagent/service/DashboardAgentService.java b/backend/src/main/java/com/dbaagent/service/DashboardAgentService.java index 027431b..8f2439c 100644 --- a/backend/src/main/java/com/dbaagent/service/DashboardAgentService.java +++ b/backend/src/main/java/com/dbaagent/service/DashboardAgentService.java @@ -220,7 +220,9 @@ private String buildChatTask(String prompt) { + "It does not read as a request to build or change a chart/dashboard — it looks like a " + "greeting, small talk, or a question about what you can do. Reply briefly and naturally " + "in plain text (no HTML, no code block, no tool calls, no grounding, no SQL). If it's a " - + "greeting, greet back and invite them to describe a dashboard. Keep it to 1-2 sentences."; + + "greeting, greet back and invite them to describe a dashboard.\n\n" + + "STYLE: Keep it to 1-2 short sentences. Lead with the answer. No filler phrases like " + + "\"Great question!\" or restating what they asked. Just answer directly."; } // ── the task the agent runs ──────────────────────────────────────────── @@ -335,17 +337,25 @@ Output your FINAL message as ONLY fenced blocks (one ```dashboard-shell``` first outside a fence, no tool calls after the last one. Do NOT wrap the whole thing in a single ```html block — the shell and each widget are SEPARATE fences. - 7. END with a ```dashboard-note``` fence: 1-3 sentences to the person who asked, in the same plain + 7. END with a ```dashboard-note``` fence: a SHORT reply to the person who asked, in the same plain business language as the two hard rules above (a note naming a table, a column, SQL, or the connection id breaks the same security requirement the dashboard itself is bound by). - Say what THIS turn actually changed — not that a dashboard exists. Then, in the same note: - - State anything you could NOT do, could not verify, or chose to skip, and why. A build that - partly worked must say so. Never claim a number is correct because a query returned it. + + REPLY STYLE — brevity is mandatory: + - Lead with what changed or what you built. No filler, no restating their request. + - Keep it to 1-3 short lines. Use markdown bullets when listing multiple items (requirements + met, status updates, or next steps). Never write long paragraphs. + - Skip phrases like "I've created...", "Here's what I did...", "As requested...". Just state + the facts: "Added revenue chart. Date range defaults to last 30 days." + - Only write more than 3 lines when explaining an error, a limitation you hit, or a breaking + change — and even then stay concise. + + Content requirements (still apply): + - State anything you could NOT do, could not verify, or chose to skip, and why. - If the user was correcting or disputing something (a wrong figure, a chart that didn't load), - say plainly whether it is now fixed, and what the value/behaviour is now versus what they - reported. If you could not reproduce or resolve their complaint, say THAT — do not answer a - correction with a description of what you built. - Write it as you would to a colleague: specific and short. Never open with "Done"."""); + say plainly whether it is now fixed. If you could not resolve it, say THAT. + - Never claim a number is correct just because a query returned it. + - Never open with "Done" or "Great"."""); return sb.toString(); } diff --git a/src/components/layout/AppSidebar.jsx b/src/components/layout/AppSidebar.jsx index 809b935..c94c0c6 100644 --- a/src/components/layout/AppSidebar.jsx +++ b/src/components/layout/AppSidebar.jsx @@ -7,6 +7,7 @@ import { AGENTS_ENABLED, canAccessHomeSection, getConnectionAccessBadge, getConn import { PERMISSIONS } from '@/lib/permissions' import ManageConnectionsModal from '@/components/ManageConnectionsModal' import SettingsModal from '@/components/SettingsModal' +import ProfileSwitch from './ProfileSwitch' import { useAuth } from '@/hooks/useAuth' import styles from './AppSidebar.module.css' @@ -30,7 +31,7 @@ export default function AppSidebar() { const [showConnectionDropdown, setShowConnectionDropdown] = useState(false) const userMenuRef = useRef(null) const connectionDropdownRef = useRef(null) - const { logout, role, username, impersonating, permissions, hasPermission } = useAuth() + const { logout, role, username, impersonating, permissions, hasPermission, canSwitchProfile } = useAuth() // Connections (add/edit/delete a database) is administrative: the backend already // refuses it to Developer and Data Engineer with 403, so hiding the button stops the // UI offering a door that only leads to an error. Settings is likewise administrative @@ -137,12 +138,13 @@ export default function AppSidebar() { {/* Bottom: connections + profile */}
+ {canSwitchProfile && }
- {showConnectionDropdown && !collapsed && connections.length > 0 && ( + {showConnectionDropdown && !collapsed && (
-
Connections
- {connections.map((conn) => ( -
+ {connections.length > 0 && ( + <> +
Connections
+ {connections.map((conn) => ( +
+ + +
+ ))} + + )} + {canManageConnections && ( + <> + {connections.length > 0 &&
} - -
- ))} + + )}
)}
- {canManageConnections && ( - - )}
- -
- {open && ( - - )} -
- ) - } - - return ( -
+
- {open && ( + {open && !collapsed && ( +
+
+ Viewing as + {username} + {role} +
+ {impersonatorUsername && ( +
Signed in as {impersonatorUsername}
+ )} +
+ {error &&
{error}
} + + + {open && ( + + )} +
+ )} +
+ ) + } + + // Normal state: show "View as" trigger + return ( +
+ + {open && !collapsed && ( )}
) } -function CandidateMenu({ candidates, loading, switching, error, activeUsername, onSelect }) { +function CandidateMenu({ candidates, loading, switching, error, activeUsername, onSelect, isSidebar }) { return ( -
+
Switch into a user
{loading &&
Loading users…
} {!loading && error &&
{error}
} diff --git a/src/components/layout/ProfileSwitch.module.css b/src/components/layout/ProfileSwitch.module.css index e03cc49..ffee625 100644 --- a/src/components/layout/ProfileSwitch.module.css +++ b/src/components/layout/ProfileSwitch.module.css @@ -227,3 +227,197 @@ background: #374151; color: #e5e7eb; } + +/* ── Sidebar variant ── */ +.sidebarWrap { + position: relative; + width: 100%; +} + +.sidebarTrigger, +.sidebarTriggerActive { + display: flex; + align-items: center; + justify-content: flex-start; + gap: 10px; + padding: 8px 10px; + border-radius: 8px; + border: none; + background: transparent; + color: #6b7280; + font-size: 14px; + font-weight: 400; + font-family: var(--font-family-sans); + cursor: pointer; + width: 100%; + text-align: left; + transition: background 0.1s, color 0.1s; + white-space: nowrap; + overflow: hidden; +} + +.sidebarTrigger:hover:not(:disabled) { + background: #f3f4f6; + color: #111827; +} + +.sidebarTrigger:disabled { + opacity: 0.6; + cursor: default; +} + +.sidebarTriggerActive { + background: #fef3c7; + color: #92400e; +} + +.sidebarTriggerActive:hover:not(:disabled) { + background: #fde68a; +} + +.sidebarIcon { + flex-shrink: 0; + opacity: 0.6; +} + +.sidebarTriggerActive .sidebarIcon { + opacity: 0.9; +} + +.sidebarLabel { + overflow: hidden; + opacity: 1; + transition: opacity 0.12s ease; + flex: 1; + min-width: 0; + text-overflow: ellipsis; +} + +.sidebarLabelHidden { + opacity: 0; + width: 0; + pointer-events: none; +} + +.chevronIcon { + opacity: 0.6; + flex-shrink: 0; +} + +.sidebarDropdown { + position: absolute; + bottom: calc(100% + 6px); + left: 0; + right: 0; + background: #fff; + border: 1px solid #e5e7eb; + border-radius: 10px; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12); + z-index: 220; + overflow: hidden; +} + +.sidebarDropdownInfo { + display: flex; + align-items: center; + gap: 8px; + padding: 10px 12px; + flex-wrap: wrap; +} + +.sidebarDropdownLabel { + font-size: 11px; + font-weight: 500; + letter-spacing: 0.04em; + text-transform: uppercase; + color: #9ca3af; +} + +.sidebarDropdownName { + font-size: 13px; + font-weight: 600; + color: #111827; +} + +.sidebarDropdownMeta { + padding: 0 12px 8px; + font-size: 11px; + color: #6b7280; +} + +.sidebarDropdownDivider { + height: 1px; + background: #f3f4f6; +} + +.sidebarDropdownError { + padding: 8px 12px; + font-size: 12px; + color: #b91c1c; +} + +.sidebarDropdownAction { + display: flex; + align-items: center; + gap: 8px; + width: 100%; + padding: 9px 12px; + background: transparent; + border: none; + font-size: 13px; + color: #374151; + cursor: pointer; + text-align: left; + transition: background 0.1s; +} + +.sidebarDropdownAction:hover:not(:disabled) { + background: #f9fafb; + color: #111827; +} + +.sidebarDropdownAction:disabled { + opacity: 0.6; + cursor: default; +} + +.sidebarDropdownActionPrimary { + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + width: calc(100% - 16px); + margin: 8px; + padding: 8px 12px; + background: #111827; + border: none; + border-radius: 6px; + font-size: 13px; + font-weight: 500; + color: #fff; + cursor: pointer; + transition: background 0.1s; +} + +.sidebarDropdownActionPrimary:hover:not(:disabled) { + background: #1f2937; +} + +.sidebarDropdownActionPrimary:disabled { + opacity: 0.6; + cursor: default; +} + +.sidebarMenu { + position: absolute; + bottom: calc(100% + 6px); + left: 0; + right: 0; + max-height: 320px; + overflow: auto; + background: #fff; + border: 1px solid #e5e7eb; + border-radius: 10px; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12); + z-index: 220; +} diff --git a/src/components/sections/DashboardWorkspace.jsx b/src/components/sections/DashboardWorkspace.jsx index 404b137..4409890 100644 --- a/src/components/sections/DashboardWorkspace.jsx +++ b/src/components/sections/DashboardWorkspace.jsx @@ -1,5 +1,7 @@ import { useState, useEffect, useRef, useCallback, lazy, Suspense } from 'react' 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' +import ReactMarkdown from 'react-markdown' +import remarkGfm from 'remark-gfm' import DashboardArtifact from '@/components/DashboardArtifact' import ShareMenu from './ShareMenu' import { savedDashboardsAPI } from '@/lib/api/client' @@ -506,7 +508,7 @@ export default function DashboardWorkspace({ connectionId, dashboard, onClose })
/ @@ -556,7 +558,11 @@ export default function DashboardWorkspace({ connectionId, dashboard, onClose })
{!intro && messages.map((msg, i) => (
- {msg.text} + {msg.role === 'agent' && !msg.error ? ( +
+ {msg.text} +
+ ) : msg.text}
))} {!intro && thinking && ( diff --git a/src/components/sections/DashboardWorkspace.module.css b/src/components/sections/DashboardWorkspace.module.css index 11eeaa5..3fba05c 100644 --- a/src/components/sections/DashboardWorkspace.module.css +++ b/src/components/sections/DashboardWorkspace.module.css @@ -38,15 +38,12 @@ } .logoBtn:active { transform: scale(0.92); } -.logoMark { - width: 26px; - height: 26px; - border-radius: 7px; - background: #534AB7; - display: flex; - align-items: center; - justify-content: center; +.logoIcon { + color: #555; + flex-shrink: 0; + transition: color 120ms ease-out; } +.logoBtn:hover .logoIcon { color: #111; } .crumbLink { border: none; @@ -306,6 +303,60 @@ animation: bubbleIn 260ms cubic-bezier(0.2, 0.8, 0.2, 1); } +.agentMarkdown { + font-size: 13px; + line-height: 1.6; + color: #26262a; +} +.agentMarkdown p { margin: 0 0 8px; } +.agentMarkdown p:last-child { margin-bottom: 0; } +.agentMarkdown ul, .agentMarkdown ol { + margin: 6px 0; + padding-left: 18px; +} +.agentMarkdown li { + margin: 3px 0; +} +.agentMarkdown li::marker { + color: #9a97ad; +} +.agentMarkdown strong { + font-weight: 600; + color: #111; +} +.agentMarkdown code { + font-family: ui-monospace, "SF Mono", Menlo, monospace; + font-size: 12px; + background: #f5f5f7; + padding: 1px 5px; + border-radius: 4px; +} +.agentMarkdown pre { + background: #f5f5f7; + border-radius: 6px; + padding: 10px 12px; + overflow-x: auto; + margin: 8px 0; +} +.agentMarkdown pre code { + background: none; + padding: 0; +} +.agentMarkdown h1, .agentMarkdown h2, .agentMarkdown h3 { + font-size: 13px; + font-weight: 600; + margin: 10px 0 6px; + color: #111; +} +.agentMarkdown h1:first-child, .agentMarkdown h2:first-child, .agentMarkdown h3:first-child { + margin-top: 0; +} +.agentMarkdown a { + color: #534AB7; + text-decoration: none; +} +.agentMarkdown a:hover { text-decoration: underline; } + .bubbleUser { align-self: flex-end; max-width: 92%; diff --git a/src/pages/Home.jsx b/src/pages/Home.jsx index a091279..f9e692c 100644 --- a/src/pages/Home.jsx +++ b/src/pages/Home.jsx @@ -1,6 +1,5 @@ import { useState, useEffect, useMemo, useRef } from 'react' import AppSidebar from '@/components/layout/AppSidebar' -import ProfileSwitch from '@/components/layout/ProfileSwitch' import AgentView from '@/components/Agent/AgentView' import AgentChatSection from '@/components/sections/AgentChatSection' import DigestFeedSection from '@/components/sections/DigestSection' @@ -27,7 +26,7 @@ const SECTION_MAP = { } export default function Home() { - const { role, canSwitchProfile, permissions } = useAuth() + const { role, permissions } = useAuth() const { selectedConnection } = useConnectionManager() const activeSection = useActiveSection() const setActiveSection = useSetActiveSection() @@ -78,9 +77,7 @@ export default function Home() { {!immersive && } {/* Main content — lazy-mount sections on first visit, then keep alive */} -
- {canSwitchProfile && } -
+
{visibleSections.map(([key, Section]) => { if (!mounted.has(key)) return null return ( @@ -98,7 +95,6 @@ export default function Home() { ) })}
-
) }