Skip to content

Commit c5db856

Browse files
committed
feat: enhance chat UI with new animations and utility classes
- Introduced a wave typing indicator to replace the previous bouncing dots, improving user feedback during chat interactions. - Added animated border gradients for input focus, enhancing visual cues for user engagement. - Implemented a streaming progress bar for indeterminate states, providing better context during message processing. - Created utility classes for message avatar rings, code block copy buttons, and collapsible message wrappers, improving overall chat functionality and user experience. - Updated the ChatHeader component to display streaming status and model information, enriching the chat context for users.
1 parent 3546bac commit c5db856

11 files changed

Lines changed: 1872 additions & 551 deletions

app/globals.css

Lines changed: 363 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3989,3 +3989,366 @@ p {
39893989
background: rgba(245, 169, 184, 0.08);
39903990
color: #a8566a;
39913991
}
3992+
3993+
/* ══════════════════════════════════════════════════════════════════
3994+
Chat UI Overhaul — Animations & Utility Classes
3995+
══════════════════════════════════════════════════════════════════ */
3996+
3997+
/* ─── Wave typing indicator (replaces bouncing dots) ───────────── */
3998+
@keyframes wave-typing {
3999+
0%,
4000+
100% {
4001+
transform: translateY(0);
4002+
opacity: 0.4;
4003+
}
4004+
20% {
4005+
transform: translateY(-3px);
4006+
opacity: 1;
4007+
}
4008+
40% {
4009+
transform: translateY(0);
4010+
opacity: 0.6;
4011+
}
4012+
}
4013+
4014+
.typing-wave {
4015+
display: inline-flex;
4016+
gap: 2px;
4017+
align-items: flex-end;
4018+
}
4019+
4020+
.typing-wave span {
4021+
width: 3px;
4022+
height: 3px;
4023+
border-radius: 50%;
4024+
background: var(--brand);
4025+
animation: wave-typing 1.2s ease-in-out infinite;
4026+
}
4027+
4028+
.typing-wave span:nth-child(2) {
4029+
animation-delay: 0.1s;
4030+
}
4031+
.typing-wave span:nth-child(3) {
4032+
animation-delay: 0.2s;
4033+
}
4034+
.typing-wave span:nth-child(4) {
4035+
animation-delay: 0.3s;
4036+
}
4037+
4038+
/* ─── Animated border gradient for input focus ─────────────────── */
4039+
@keyframes border-rotate {
4040+
from {
4041+
--border-angle: 0deg;
4042+
}
4043+
to {
4044+
--border-angle: 360deg;
4045+
}
4046+
}
4047+
4048+
.input-focus-glow {
4049+
position: relative;
4050+
}
4051+
4052+
.input-focus-glow::before {
4053+
content: '';
4054+
position: absolute;
4055+
inset: -1px;
4056+
border-radius: inherit;
4057+
padding: 1px;
4058+
background: conic-gradient(
4059+
from var(--border-angle, 0deg),
4060+
transparent 40%,
4061+
color-mix(in srgb, var(--brand) 40%, transparent) 50%,
4062+
transparent 60%
4063+
);
4064+
mask:
4065+
linear-gradient(#fff 0 0) content-box,
4066+
linear-gradient(#fff 0 0);
4067+
mask-composite: exclude;
4068+
-webkit-mask-composite: xor;
4069+
animation: border-rotate 4s linear infinite;
4070+
pointer-events: none;
4071+
opacity: 0;
4072+
transition: opacity 0.3s ease;
4073+
}
4074+
4075+
.input-focus-glow:focus-within::before {
4076+
opacity: 1;
4077+
}
4078+
4079+
/* ─── Progress bar (indeterminate streaming) ───────────────────── */
4080+
@keyframes progress-indeterminate {
4081+
0% {
4082+
transform: translateX(-100%);
4083+
}
4084+
100% {
4085+
transform: translateX(400%);
4086+
}
4087+
}
4088+
4089+
.streaming-progress-bar {
4090+
position: relative;
4091+
height: 2px;
4092+
width: 100%;
4093+
overflow: hidden;
4094+
background: color-mix(in srgb, var(--brand) 10%, transparent);
4095+
}
4096+
4097+
.streaming-progress-bar::after {
4098+
content: '';
4099+
position: absolute;
4100+
top: 0;
4101+
left: 0;
4102+
width: 25%;
4103+
height: 100%;
4104+
background: var(--brand);
4105+
border-radius: 2px;
4106+
animation: progress-indeterminate 1.5s ease-in-out infinite;
4107+
}
4108+
4109+
/* ─── Message avatar ring (brand glow around KnotLogo) ─────────── */
4110+
.message-avatar-ring {
4111+
display: flex;
4112+
align-items: center;
4113+
justify-content: center;
4114+
width: 22px;
4115+
height: 22px;
4116+
border-radius: 6px;
4117+
background: color-mix(in srgb, var(--brand) 10%, var(--bg-elevated));
4118+
box-shadow:
4119+
0 0 0 1px color-mix(in srgb, var(--brand) 20%, transparent),
4120+
0 0 6px 0 var(--brand-glow);
4121+
flex-shrink: 0;
4122+
}
4123+
4124+
/* ─── Code block copy button (hover-to-show) ───────────────────── */
4125+
.code-block-wrapper {
4126+
position: relative;
4127+
}
4128+
4129+
.code-block-copy {
4130+
position: absolute;
4131+
top: 4px;
4132+
right: 4px;
4133+
display: flex;
4134+
align-items: center;
4135+
justify-content: center;
4136+
width: 24px;
4137+
height: 24px;
4138+
border-radius: 6px;
4139+
background: color-mix(in srgb, var(--bg-elevated) 90%, transparent);
4140+
border: 1px solid var(--border);
4141+
color: var(--text-tertiary);
4142+
cursor: pointer;
4143+
opacity: 0;
4144+
transition:
4145+
opacity 0.15s ease,
4146+
color 0.15s ease,
4147+
background 0.15s ease;
4148+
z-index: 2;
4149+
backdrop-filter: blur(4px);
4150+
}
4151+
4152+
.code-block-wrapper:hover .code-block-copy {
4153+
opacity: 1;
4154+
}
4155+
4156+
.code-block-copy:hover {
4157+
color: var(--text-primary);
4158+
background: var(--bg-elevated);
4159+
}
4160+
4161+
.code-block-copy.copied {
4162+
color: var(--color-additions, #22c55e);
4163+
}
4164+
4165+
/* ─── Message collapse fade (gradient overlay on long messages) ── */
4166+
.message-collapse-fade {
4167+
position: relative;
4168+
max-height: 480px;
4169+
overflow: hidden;
4170+
}
4171+
4172+
.message-collapse-fade::after {
4173+
content: '';
4174+
position: absolute;
4175+
bottom: 0;
4176+
left: 0;
4177+
right: 0;
4178+
height: 80px;
4179+
background: linear-gradient(to bottom, transparent, var(--bg-subtle));
4180+
pointer-events: none;
4181+
}
4182+
4183+
.message-collapse-fade.collapsed::after {
4184+
display: block;
4185+
}
4186+
4187+
.message-collapse-fade:not(.collapsed)::after {
4188+
display: none;
4189+
}
4190+
4191+
.message-collapse-fade:not(.collapsed) {
4192+
max-height: none;
4193+
}
4194+
4195+
/* ─── Logo breathing glow (connection-aware) ───────────────────── */
4196+
@keyframes logo-breathe-connected {
4197+
0%,
4198+
100% {
4199+
filter: drop-shadow(0 0 4px color-mix(in srgb, var(--success) 30%, transparent));
4200+
opacity: 0.85;
4201+
}
4202+
50% {
4203+
filter: drop-shadow(0 0 10px color-mix(in srgb, var(--success) 50%, transparent));
4204+
opacity: 1;
4205+
}
4206+
}
4207+
4208+
@keyframes logo-breathe-idle {
4209+
0%,
4210+
100% {
4211+
opacity: 0.5;
4212+
}
4213+
50% {
4214+
opacity: 0.7;
4215+
}
4216+
}
4217+
4218+
.logo-breathe-connected {
4219+
animation: logo-breathe-connected 3s ease-in-out infinite;
4220+
}
4221+
4222+
.logo-breathe-idle {
4223+
animation: logo-breathe-idle 4s ease-in-out infinite;
4224+
}
4225+
4226+
/* ─── Quick action cards (upgraded chips) ──────────────────────── */
4227+
.quick-action-card {
4228+
transition:
4229+
transform 0.2s ease,
4230+
box-shadow 0.2s ease,
4231+
border-color 0.2s ease;
4232+
}
4233+
4234+
.quick-action-card:hover {
4235+
transform: translateY(-2px);
4236+
box-shadow: 0 4px 12px color-mix(in srgb, var(--brand) 10%, rgba(0, 0, 0, 0.12));
4237+
border-color: color-mix(in srgb, var(--brand) 30%, var(--border));
4238+
}
4239+
4240+
.quick-action-card:active {
4241+
transform: translateY(0);
4242+
}
4243+
4244+
/* ─── Context usage bar ────────────────────────────────────────── */
4245+
.context-usage-bar {
4246+
height: 2px;
4247+
background: color-mix(in srgb, var(--text-disabled) 15%, transparent);
4248+
overflow: hidden;
4249+
}
4250+
4251+
.context-usage-bar-fill {
4252+
height: 100%;
4253+
background: var(--brand);
4254+
transition: width 0.5s ease-out;
4255+
border-radius: 1px;
4256+
}
4257+
4258+
/* ─── Stagger animation for paragraphs ─────────────────────────── */
4259+
.stagger-paragraph > p,
4260+
.stagger-paragraph > ul,
4261+
.stagger-paragraph > ol,
4262+
.stagger-paragraph > pre,
4263+
.stagger-paragraph > blockquote,
4264+
.stagger-paragraph > h1,
4265+
.stagger-paragraph > h2,
4266+
.stagger-paragraph > h3,
4267+
.stagger-paragraph > h4 {
4268+
animation: fade-in-up 0.3s ease both;
4269+
}
4270+
4271+
.stagger-paragraph > *:nth-child(1) {
4272+
animation-delay: 0ms;
4273+
}
4274+
.stagger-paragraph > *:nth-child(2) {
4275+
animation-delay: 50ms;
4276+
}
4277+
.stagger-paragraph > *:nth-child(3) {
4278+
animation-delay: 100ms;
4279+
}
4280+
.stagger-paragraph > *:nth-child(4) {
4281+
animation-delay: 150ms;
4282+
}
4283+
.stagger-paragraph > *:nth-child(5) {
4284+
animation-delay: 200ms;
4285+
}
4286+
.stagger-paragraph > *:nth-child(6) {
4287+
animation-delay: 250ms;
4288+
}
4289+
.stagger-paragraph > *:nth-child(7) {
4290+
animation-delay: 300ms;
4291+
}
4292+
.stagger-paragraph > *:nth-child(8) {
4293+
animation-delay: 350ms;
4294+
}
4295+
4296+
/* ─── Thinking disclosure triangle ─────────────────────────────── */
4297+
.thinking-disclosure summary {
4298+
cursor: pointer;
4299+
list-style: none;
4300+
display: flex;
4301+
align-items: center;
4302+
gap: 6px;
4303+
}
4304+
4305+
.thinking-disclosure summary::-webkit-details-marker {
4306+
display: none;
4307+
}
4308+
4309+
.thinking-disclosure[open] summary .thinking-chevron {
4310+
transform: rotate(90deg);
4311+
}
4312+
4313+
.thinking-chevron {
4314+
transition: transform 0.2s ease;
4315+
}
4316+
4317+
/* ─── Diff apply flash ─────────────────────────────────────────── */
4318+
@keyframes diff-apply-flash {
4319+
0% {
4320+
background: color-mix(in srgb, var(--color-additions) 25%, transparent);
4321+
}
4322+
100% {
4323+
background: transparent;
4324+
}
4325+
}
4326+
4327+
.diff-apply-flash {
4328+
animation: diff-apply-flash 0.8s ease-out;
4329+
}
4330+
4331+
/* ─── Hunk action buttons ──────────────────────────────────────── */
4332+
.hunk-separator {
4333+
background: color-mix(in srgb, var(--brand) 6%, transparent);
4334+
border-top: 1px solid color-mix(in srgb, var(--brand) 15%, var(--border));
4335+
border-bottom: 1px solid color-mix(in srgb, var(--brand) 15%, var(--border));
4336+
}
4337+
4338+
/* ─── Reduced motion: disable all new animations ───────────────── */
4339+
@media (prefers-reduced-motion: reduce) {
4340+
.typing-wave span,
4341+
.input-focus-glow::before,
4342+
.streaming-progress-bar::after,
4343+
.logo-breathe-connected,
4344+
.logo-breathe-idle,
4345+
.stagger-paragraph > * {
4346+
animation: none !important;
4347+
}
4348+
.quick-action-card:hover {
4349+
transform: none;
4350+
}
4351+
.message-avatar-ring {
4352+
box-shadow: 0 0 0 1px color-mix(in srgb, var(--brand) 20%, transparent);
4353+
}
4354+
}

components/agent-panel.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1490,7 +1490,13 @@ export function AgentPanel() {
14901490
return (
14911491
<div className="flex flex-col h-full w-full overflow-hidden bg-[var(--sidebar-bg)]">
14921492
{/* Header — children are no-drag so they stay clickable above drag region */}
1493-
<ChatHeader title={chatTitle ?? undefined} messageCount={messages.length} />
1493+
<ChatHeader
1494+
title={chatTitle ?? undefined}
1495+
messageCount={messages.length}
1496+
isStreaming={isStreaming}
1497+
modelName={modelInfo.current || undefined}
1498+
contextTokens={contextTokens}
1499+
/>
14941500
{messages.length > 0 && (
14951501
<div className="flex items-center justify-end px-3 py-1 border-b border-[var(--border)] bg-[var(--bg-elevated)] shrink-0">
14961502
<button

0 commit comments

Comments
 (0)