Skip to content

Commit 7192906

Browse files
notSumit25claude
andcommitted
feat(agent): redesign Agent tab to match dashboard visual language
Rebuilds the empty state, header, composer, message bubbles, and activity trace with the same purple-accent/material treatment already used across Dashboards, plus real error/boot states with icons and Apple-style press feedback and entrance animations throughout. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 parent 2635a54 commit 7192906

2 files changed

Lines changed: 216 additions & 62 deletions

File tree

src/components/AgentChat/AgentChatPanel.jsx

Lines changed: 46 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useState, useRef, useEffect, useCallback } from 'react'
2-
import { ArrowUp, Plus, Square, Loader2, Database } from 'lucide-react'
2+
import { ArrowUp, Plus, Square, Loader2, Database, Sparkles, Hash, Table2, Clock, AlertCircle } from 'lucide-react'
33
import { agentChatAPI, withConnectionContext } from '@/lib/api/agentClient'
44
import { agentConversationAPI } from '@/lib/api/client'
55
import AgentMarkdown from './AgentMarkdown'
@@ -35,9 +35,9 @@ function deriveTitle(messages) {
3535
// a domain-specific table/column name here (see the chat guardrail in
3636
// AGENTS.md); AgentChatPanel has no idea what tables the active connection has.
3737
const SUGGESTIONS = [
38-
'How many tables are there?',
39-
'Show the largest tables',
40-
'What are the top slow queries?',
38+
{ icon: Hash, text: 'How many tables are there?' },
39+
{ icon: Table2, text: 'Show the largest tables' },
40+
{ icon: Clock, text: 'What are the top slow queries?' },
4141
]
4242

4343
export default function AgentChatPanel({ connectionId, connectionName }) {
@@ -197,15 +197,36 @@ export default function AgentChatPanel({ connectionId, connectionName }) {
197197
</header>
198198

199199
<div className={styles.messages} ref={listRef}>
200-
{booting && <div className={styles.notice}><Loader2 size={14} className={styles.spin} /> Starting your agent…</div>}
201-
{bootError && <div className={styles.error}>{bootError} <button onClick={() => boot()} className={styles.retry}>Retry</button></div>}
200+
{booting && (
201+
<div className={styles.bootState}>
202+
<span className={styles.bootIcon}><Loader2 size={18} className={styles.spin} /></span>
203+
<span className={styles.bootText}>Starting your agent…</span>
204+
</div>
205+
)}
206+
{bootError && (
207+
<div className={styles.errorState}>
208+
<span className={styles.errorIcon}><AlertCircle size={18} /></span>
209+
<span className={styles.errorText}>{bootError}</span>
210+
<button onClick={() => boot()} className={styles.retry}>Retry</button>
211+
</div>
212+
)}
202213

203214
{!booting && !bootError && messages.length === 0 && (
204215
<div className={styles.empty}>
205-
<div className={styles.emptyTitle}>Ask about your database</div>
216+
<span className={styles.emptyIcon}><Sparkles size={22} color="#534AB7" /></span>
217+
<h2 className={styles.emptyTitle}>Ask about your database</h2>
218+
<p className={styles.emptySub}>Get instant answers grounded on your schema, live data, and query history.</p>
206219
<div className={styles.suggestions}>
207-
{SUGGESTIONS.map((s) => (
208-
<button key={s} className={styles.suggestion} onClick={() => send(s)}>{s}</button>
220+
{SUGGESTIONS.map(({ icon: Icon, text }, i) => (
221+
<button
222+
key={text}
223+
className={styles.suggestion}
224+
style={{ '--stagger': i }}
225+
onClick={() => send(text)}
226+
>
227+
<span className={styles.suggestionIcon}><Icon size={14} /></span>
228+
{text}
229+
</button>
209230
))}
210231
</div>
211232
</div>
@@ -234,20 +255,22 @@ export default function AgentChatPanel({ connectionId, connectionName }) {
234255
</div>
235256

236257
<div className={styles.composer}>
237-
<textarea
238-
className={styles.textarea}
239-
placeholder={authBlocked ? 'Agent unavailable — reconnect above' : sessionId ? 'Message the DeepSQL Agent…' : 'Starting…'}
240-
value={input}
241-
onChange={(e) => setInput(e.target.value)}
242-
onKeyDown={onKeyDown}
243-
rows={1}
244-
disabled={!sessionId || booting || authBlocked}
245-
/>
246-
{sending ? (
247-
<button className={styles.stopBtn} onClick={stop} title="Stop"><Square size={15} /></button>
248-
) : (
249-
<button className={styles.sendBtn} onClick={() => send()} disabled={!input.trim() || !sessionId || authBlocked} title="Send"><ArrowUp size={16} /></button>
250-
)}
258+
<div className={styles.composerInner}>
259+
<textarea
260+
className={styles.textarea}
261+
placeholder={authBlocked ? 'Agent unavailable — reconnect above' : sessionId ? 'Message the DeepSQL Agent…' : 'Starting…'}
262+
value={input}
263+
onChange={(e) => setInput(e.target.value)}
264+
onKeyDown={onKeyDown}
265+
rows={1}
266+
disabled={!sessionId || booting || authBlocked}
267+
/>
268+
{sending ? (
269+
<button className={styles.stopBtn} onClick={stop} title="Stop"><Square size={15} /></button>
270+
) : (
271+
<button className={styles.sendBtn} onClick={() => send()} disabled={!input.trim() || !sessionId || authBlocked} title="Send"><ArrowUp size={16} /></button>
272+
)}
273+
</div>
251274
</div>
252275
</div>
253276
)

src/components/AgentChat/AgentChatPanel.module.css

Lines changed: 170 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -10,59 +10,158 @@
1010
display: flex;
1111
align-items: center;
1212
justify-content: space-between;
13-
padding: 14px 20px;
14-
border-bottom: 1px solid #eceff3;
13+
height: 52px;
1514
flex-shrink: 0;
15+
padding: 0 20px;
16+
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
17+
background: rgba(255, 255, 255, 0.72);
18+
backdrop-filter: blur(20px) saturate(180%);
19+
-webkit-backdrop-filter: blur(20px) saturate(180%);
20+
position: relative;
21+
z-index: 5;
22+
}
23+
@media (prefers-reduced-transparency: reduce) {
24+
.header { background: #fff; backdrop-filter: none; -webkit-backdrop-filter: none; }
1625
}
17-
.title { font-size: 14px; font-weight: 600; color: #111827; letter-spacing: -0.01em; }
18-
.headerRight { display: flex; align-items: center; gap: 12px; }
26+
27+
.title { font-size: 14px; font-weight: 590; color: #111; letter-spacing: -0.006em; }
28+
.headerRight { display: flex; align-items: center; gap: 10px; }
1929
.connBadge {
2030
display: inline-flex; align-items: center; gap: 5px;
21-
font-size: 11px; color: #6b7280; background: #f7f7f7;
22-
border: 1px solid #eceff3; border-radius: 6px; padding: 3px 8px;
31+
font-size: 11.5px; font-weight: 500; color: #555; background: #f7f7f7;
32+
border: 1px solid #ececec; border-radius: 999px; padding: 4px 10px;
2333
}
34+
.connBadge svg { flex-shrink: 0; color: #534AB7; }
35+
2436
.newChat {
2537
display: inline-flex; align-items: center; gap: 5px;
26-
font-size: 12px; color: #374151; background: #fff;
27-
border: 1px solid #e5e7eb; border-radius: 6px; padding: 5px 10px; cursor: pointer;
38+
height: 30px;
39+
font-size: 12.5px; font-weight: 500; color: #333; background: #fff;
40+
border: 1px solid #e2e2e2; border-radius: 7px; padding: 0 11px; cursor: pointer;
41+
transition: background 120ms ease-out, transform 120ms cubic-bezier(0.34, 1.4, 0.64, 1);
42+
}
43+
.newChat:hover:not(:disabled) { background: #f6f6f6; }
44+
.newChat:active:not(:disabled) { transform: scale(0.96); }
45+
.newChat:disabled { opacity: 0.45; cursor: default; }
46+
@media (prefers-reduced-motion: reduce) {
47+
.newChat { transition: background 120ms ease-out; }
48+
.newChat:active { transform: none; }
2849
}
29-
.newChat:hover { background: #f7f7f7; }
30-
.newChat:disabled { opacity: 0.4; cursor: default; }
3150

3251
.messages { flex: 1; overflow-y: auto; padding: 24px 0; scroll-behavior: smooth; }
3352

34-
.notice, .error {
35-
max-width: 720px; margin: 0 auto; padding: 10px 20px;
36-
display: flex; align-items: center; gap: 8px; font-size: 13px; color: #6b7280;
53+
.bootState {
54+
max-width: 720px; margin: 96px auto 0; padding: 0 20px;
55+
display: flex; flex-direction: column; align-items: center; gap: 12px;
56+
animation: fadeIn 280ms cubic-bezier(0.2, 0.8, 0.2, 1);
57+
}
58+
.bootIcon { color: #534AB7; }
59+
.bootText { font-size: 13.5px; color: #777; }
60+
61+
.errorState {
62+
max-width: 560px; margin: 96px auto 0; padding: 18px 20px;
63+
display: flex; flex-direction: column; align-items: center; gap: 10px;
64+
text-align: center;
65+
background: #fdf2f2; border: 1px solid #f6d5d5; border-radius: 14px;
66+
animation: fadeIn 280ms cubic-bezier(0.2, 0.8, 0.2, 1);
67+
}
68+
.errorIcon { color: #b42318; }
69+
.errorText { font-size: 13.5px; line-height: 1.5; color: #9c2b2b; }
70+
.retry {
71+
margin-top: 2px; border: 1px solid #f0c2c2; background: #fff; color: #9c2b2b;
72+
border-radius: 8px; padding: 6px 14px; cursor: pointer; font-size: 12.5px; font-weight: 500;
73+
transition: background 120ms ease-out, transform 120ms cubic-bezier(0.34, 1.4, 0.64, 1);
3774
}
38-
.error { color: #b42318; }
39-
.retry { margin-left: 8px; border: 1px solid #e5e7eb; background: #fff; border-radius: 6px; padding: 2px 8px; cursor: pointer; font-size: 12px; }
75+
.retry:hover { background: #fdf2f2; }
76+
.retry:active { transform: scale(0.96); }
77+
@media (prefers-reduced-motion: reduce) { .retry { transition: background 120ms ease-out; } .retry:active { transform: none; } }
78+
4079
.spin { animation: spin 1s linear infinite; }
4180
@keyframes spin { to { transform: rotate(360deg); } }
81+
@keyframes fadeIn { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }
82+
@media (prefers-reduced-motion: reduce) {
83+
.bootState, .errorState { animation: none; }
84+
}
4285

43-
.empty { max-width: 720px; margin: 48px auto; text-align: center; padding: 0 20px; }
44-
.emptyTitle { font-size: 18px; font-weight: 600; color: #111827; margin-bottom: 20px; }
45-
.suggestions { display: flex; flex-direction: column; gap: 8px; align-items: stretch; }
86+
.empty {
87+
max-width: 640px; margin: 0 auto; padding: 64px 20px 0;
88+
display: flex; flex-direction: column; align-items: center; text-align: center;
89+
animation: fadeIn 320ms cubic-bezier(0.2, 0.8, 0.2, 1);
90+
}
91+
.emptyIcon {
92+
width: 48px; height: 48px; border-radius: 13px;
93+
background: #EEEDFE; display: flex; align-items: center; justify-content: center;
94+
margin-bottom: 16px; box-shadow: 0 1px 2px rgba(83, 74, 183, 0.08);
95+
}
96+
.emptyTitle { font-size: 19px; font-weight: 650; letter-spacing: -0.014em; color: #111; margin: 0 0 8px; }
97+
.emptySub { font-size: 13.5px; color: #777; line-height: 1.55; max-width: 380px; margin: 0 0 24px; }
98+
99+
.suggestions { display: flex; flex-direction: column; gap: 8px; align-items: stretch; width: 100%; max-width: 440px; }
46100
.suggestion {
47-
text-align: left; background: #fff; border: 1px solid #e5e7eb; border-radius: 10px;
48-
padding: 12px 14px; font-size: 13px; color: #374151; cursor: pointer; transition: background .12s;
101+
display: flex; align-items: center; gap: 10px;
102+
text-align: left; background: #fff; border: 1px solid #ececec; border-radius: 12px;
103+
padding: 12px 14px; font-size: 13.5px; color: #333; cursor: pointer;
104+
transition: border-color 180ms ease-out, transform 180ms cubic-bezier(0.2, 0.8, 0.2, 1), box-shadow 180ms ease-out;
105+
animation: suggestionIn 320ms cubic-bezier(0.2, 0.8, 0.2, 1) backwards;
106+
animation-delay: calc(var(--stagger, 0) * 60ms);
107+
}
108+
.suggestion:hover {
109+
border-color: #cfcaf0;
110+
transform: translateY(-1px);
111+
box-shadow: 0 6px 14px -8px rgba(83, 74, 183, 0.25), 0 1px 3px rgba(0, 0, 0, 0.04);
112+
}
113+
.suggestion:active { transform: translateY(0) scale(0.99); }
114+
@keyframes suggestionIn { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
115+
@media (prefers-reduced-motion: reduce) {
116+
.suggestion { transition: border-color 180ms ease-out; animation: none; }
117+
.suggestion:hover, .suggestion:active { transform: none; }
49118
}
50-
.suggestion:hover { background: #f7f7f7; border-color: #d1d5db; }
51119

52-
.msgUser, .msgAssistant { max-width: 720px; margin: 0 auto 18px; padding: 0 20px; }
120+
.suggestionIcon {
121+
flex-shrink: 0; width: 26px; height: 26px; border-radius: 8px;
122+
background: #EEEDFE; color: #534AB7;
123+
display: flex; align-items: center; justify-content: center;
124+
}
125+
126+
.msgUser, .msgAssistant { max-width: 720px; margin: 0 auto 18px; padding: 0 20px; animation: msgIn 260ms cubic-bezier(0.2, 0.8, 0.2, 1); }
53127
.msgUser { display: flex; justify-content: flex-end; }
128+
@keyframes msgIn { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }
129+
@media (prefers-reduced-motion: reduce) {
130+
.msgUser, .msgAssistant { animation: none; }
131+
}
132+
54133
.userBubble {
55-
background: #111827; color: #fff; border-radius: 14px 14px 4px 14px;
134+
background: #534AB7; color: #fff; border-radius: 14px 14px 4px 14px;
56135
padding: 10px 14px; font-size: 13.5px; line-height: 1.5; max-width: 80%; white-space: pre-wrap;
136+
box-shadow: 0 1px 2px rgba(83, 74, 183, 0.18);
57137
}
58138
.assistantBody { font-size: 13.5px; line-height: 1.6; color: #111827; }
59139

60-
.activity { margin-bottom: 8px; font-size: 12px; color: #6b7280; }
61-
.activity summary { cursor: pointer; color: #6b7280; user-select: none; }
62-
.activity ul { margin: 6px 0 4px; padding-left: 16px; list-style: none; }
140+
.activity {
141+
margin-bottom: 10px; font-size: 12px; color: #777;
142+
border: 1px solid #ececec; border-radius: 10px; background: #fafafa;
143+
padding: 9px 12px;
144+
}
145+
.activity summary {
146+
cursor: pointer; color: #555; user-select: none; font-weight: 500;
147+
display: flex; align-items: center; gap: 7px;
148+
list-style: none;
149+
}
150+
.activity summary::-webkit-details-marker { display: none; }
151+
.activity summary::before {
152+
content: '';
153+
width: 6px; height: 6px; border-radius: 50%;
154+
background: #534AB7; flex-shrink: 0;
155+
}
156+
.activity[open] summary::before { animation: activityPulse 1.4s ease-in-out infinite; }
157+
@keyframes activityPulse { 0%, 100% { opacity: 0.4; transform: scale(0.85); } 50% { opacity: 1; transform: scale(1); } }
158+
.activity ul { margin: 8px 0 2px; padding-left: 13px; list-style: none; }
63159
.activity li {
64160
font-family: var(--font-family-mono, ui-monospace, Menlo, monospace);
65-
font-size: 11.5px; color: #6b7280; padding: 1px 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
161+
font-size: 11.5px; color: #888; padding: 2px 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
162+
}
163+
@media (prefers-reduced-motion: reduce) {
164+
.activity[open] summary::before { animation: none; opacity: 0.9; }
66165
}
67166

68167
.markdown :global(p) { margin: 0 0 10px; }
@@ -168,27 +267,59 @@
168267
.msgError { font-size: 12px; color: #b42318; margin-top: 6px; }
169268

170269
.typing { display: inline-flex; gap: 4px; padding: 4px 0; }
171-
.typing span { width: 6px; height: 6px; border-radius: 50%; background: #d1d5db; animation: blink 1.2s infinite both; }
270+
.typing span { width: 6px; height: 6px; border-radius: 50%; background: #cfcaf0; animation: blink 1.2s infinite both; }
172271
.typing span:nth-child(2) { animation-delay: .2s; }
173272
.typing span:nth-child(3) { animation-delay: .4s; }
174-
@keyframes blink { 0%, 80%, 100% { opacity: .3; } 40% { opacity: 1; } }
273+
@keyframes blink { 0%, 80%, 100% { opacity: .35; } 40% { opacity: 1; } }
274+
@media (prefers-reduced-motion: reduce) {
275+
.typing span { animation: none; opacity: 0.7; }
276+
}
175277

176278
.composer {
177-
flex-shrink: 0; border-top: 1px solid #eceff3; padding: 14px 20px;
178-
display: flex; align-items: flex-end; gap: 10px;
179-
max-width: 760px; margin: 0 auto; width: 100%; box-sizing: border-box;
279+
flex-shrink: 0; padding: 14px 20px;
280+
display: flex; justify-content: center;
281+
border-top: 1px solid rgba(0, 0, 0, 0.06);
282+
background: rgba(250, 250, 250, 0.75);
283+
backdrop-filter: blur(20px) saturate(180%);
284+
-webkit-backdrop-filter: blur(20px) saturate(180%);
285+
}
286+
@media (prefers-reduced-transparency: reduce) {
287+
.composer { background: #fafafa; backdrop-filter: none; -webkit-backdrop-filter: none; }
288+
}
289+
290+
.composerInner {
291+
display: flex; align-items: flex-end; gap: 8px;
292+
max-width: 760px; width: 100%; box-sizing: border-box;
293+
padding: 8px;
294+
border: 1px solid #e2e2e2; border-radius: 16px; background: #fff;
295+
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
296+
transition: border-color 150ms ease-out, box-shadow 150ms ease-out;
180297
}
298+
.composerInner:focus-within {
299+
border-color: #534AB7;
300+
box-shadow: 0 0 0 3px rgba(83, 74, 183, 0.12);
301+
}
302+
181303
.textarea {
182-
flex: 1; resize: none; border: 1px solid #e5e7eb; border-radius: 12px;
183-
padding: 11px 14px; font-size: 13.5px; line-height: 1.5; font-family: inherit;
184-
max-height: 160px; outline: none; color: #111827;
304+
flex: 1; resize: none; border: none; background: transparent;
305+
padding: 8px 8px; font-size: 13.5px; line-height: 1.5; font-family: inherit;
306+
max-height: 160px; outline: none; color: #111;
185307
}
186-
.textarea:focus { border-color: #111827; }
187-
.textarea:disabled { background: #fafafa; }
308+
.textarea::placeholder { color: #a3a3a3; }
309+
.textarea:disabled { color: #999; }
188310
.sendBtn, .stopBtn {
189311
width: 36px; height: 36px; border-radius: 10px; border: none; cursor: pointer;
190312
display: flex; align-items: center; justify-content: center; flex-shrink: 0; color: #fff;
313+
transition: background 120ms ease-out, transform 120ms cubic-bezier(0.34, 1.4, 0.64, 1);
191314
}
192-
.sendBtn { background: #111827; }
193-
.sendBtn:disabled { background: #d1d5db; cursor: default; }
315+
.sendBtn { background: #534AB7; }
316+
.sendBtn:hover:not(:disabled) { background: #463c9f; }
317+
.sendBtn:active:not(:disabled) { transform: scale(0.9); }
318+
.sendBtn:disabled { background: #d8d5f0; cursor: default; }
194319
.stopBtn { background: #b42318; }
320+
.stopBtn:hover { background: #9c1e14; }
321+
.stopBtn:active { transform: scale(0.9); }
322+
@media (prefers-reduced-motion: reduce) {
323+
.sendBtn, .stopBtn { transition: background 120ms ease-out; }
324+
.sendBtn:active, .stopBtn:active { transform: none; }
325+
}

0 commit comments

Comments
 (0)