Skip to content

Commit cebb598

Browse files
Qa chatbot styling (#2320)
* style(qa-chatbot): improve UI/UX and component styling - Add welcome screen with example questions when chat is empty - Implement auto-resizing textarea with scroll-to-bottom behavior - Update reasoning component: remove auto-open, add animated dots to 'Thinking...' state - Improve tool component: replace status badges with icons, remove border - Update link colors in response component for better visibility - Move warning message to bottom of chat interface * style: update code block styling in QA chatbot - Remove border from code blocks - Change background from white to light grey These changes only affect the qa chatbot. The CodeBlock component is also used on the home page (feature tabs), but they override the background style, so no visible changes there. * fix(qa-chatbot): fix logic for when to show feedback buttons - Add logic to identify the last text part in a message - Introduce conditions to determine if a message is complete before displaying feedback actions * fix(qa-chatbot): only show feedback buttons once message completed * style(qa-chatbot): align spacing between message parts - Add conditional spacing between different types of message parts (text, reasoning, dynamic-tool) for consistent layout
1 parent 6bc9ce4 commit cebb598

File tree

6 files changed

+172
-61
lines changed

6 files changed

+172
-61
lines changed

components/ai-elements/code-block.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,15 +41,15 @@ export const CodeBlock = ({
4141
margin: 0,
4242
padding: '1rem',
4343
fontSize: '0.875rem',
44-
background: 'hsl(var(--background))',
44+
background: 'hsl(var(--muted) / 0.02)',
4545
color: 'hsl(var(--foreground))',
4646
}
4747

4848
return (
4949
<CodeBlockContext.Provider value={{ code }}>
5050
<div
5151
className={cn(
52-
'relative w-full overflow-hidden rounded-md border bg-background text-foreground',
52+
'relative w-full overflow-hidden rounded-md bg-muted/30 text-foreground',
5353
className,
5454
)}
5555
{...props}

components/ai-elements/prompt-input.tsx

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import type {
66
HTMLAttributes,
77
KeyboardEventHandler,
88
} from "react";
9-
import { Children } from "react";
9+
import { Children, forwardRef } from "react";
1010
import { Button } from "@/components/ui/button";
1111
import {
1212
Select,
@@ -36,14 +36,17 @@ export type PromptInputTextareaProps = ComponentProps<typeof Textarea> & {
3636
maxHeight?: number;
3737
};
3838

39-
export const PromptInputTextarea = ({
39+
export const PromptInputTextarea = forwardRef<
40+
HTMLTextAreaElement,
41+
PromptInputTextareaProps
42+
>(({
4043
onChange,
4144
className,
4245
placeholder = "What would you like to know?",
4346
minHeight = 48,
4447
maxHeight = 164,
4548
...props
46-
}: PromptInputTextareaProps) => {
49+
}, ref) => {
4750
const handleKeyDown: KeyboardEventHandler<HTMLTextAreaElement> = (e) => {
4851
if (e.key === "Enter") {
4952
if (e.shiftKey) {
@@ -62,6 +65,7 @@ export const PromptInputTextarea = ({
6265

6366
return (
6467
<Textarea
68+
ref={ref}
6569
className={cn(
6670
"w-full resize-none rounded-none border-none p-3 shadow-none outline-none ring-0",
6771
"bg-transparent dark:bg-transparent field-sizing-content max-h-[6lh]",
@@ -77,7 +81,8 @@ export const PromptInputTextarea = ({
7781
{...props}
7882
/>
7983
);
80-
};
84+
});
85+
PromptInputTextarea.displayName = "PromptInputTextarea";
8186

8287
export type PromptInputToolbarProps = HTMLAttributes<HTMLDivElement>;
8388

components/ai-elements/reasoning.tsx

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -73,11 +73,10 @@ export const Reasoning = memo(
7373
}
7474
}, [isStreaming, startTime, setDuration]);
7575

76-
// Auto-open when streaming starts, auto-close when streaming ends (once only)
76+
// Keep collapsed by default - don't auto-open when streaming
77+
// Auto-close when streaming ends (once only) if it was opened
7778
useEffect(() => {
78-
if (isStreaming && !isOpen) {
79-
setIsOpen(true);
80-
} else if (!isStreaming && isOpen && !defaultOpen && !hasAutoClosedRef) {
79+
if (!isStreaming && isOpen && !defaultOpen && !hasAutoClosedRef) {
8180
// Add a small delay before closing to allow user to see the content
8281
const timer = setTimeout(() => {
8382
setIsOpen(false);
@@ -122,6 +121,30 @@ export const ReasoningTrigger = memo(
122121
...props
123122
}: ReasoningTriggerProps) => {
124123
const { isStreaming, isOpen, duration } = useReasoning();
124+
const [dots, setDots] = useState("");
125+
126+
useEffect(() => {
127+
if (!isStreaming) {
128+
setDots("");
129+
return;
130+
}
131+
132+
let interval: NodeJS.Timeout;
133+
let dotCount = 0;
134+
135+
const animateDots = () => {
136+
dotCount = (dotCount % 3) + 1;
137+
setDots(".".repeat(dotCount));
138+
};
139+
140+
// Start immediately
141+
animateDots();
142+
interval = setInterval(animateDots, 500);
143+
144+
return () => {
145+
if (interval) clearInterval(interval);
146+
};
147+
}, [isStreaming]);
125148

126149
return (
127150
<CollapsibleTrigger
@@ -135,7 +158,10 @@ export const ReasoningTrigger = memo(
135158
<>
136159
<BrainIcon className="size-4" />
137160
{isStreaming || duration === 0 ? (
138-
<p>Thinking...</p>
161+
<p className="flex items-center">
162+
<span>Thinking</span>
163+
<span className="w-4 inline-block">{dots}</span>
164+
</p>
139165
) : (
140166
<p>Thought for {duration} seconds</p>
141167
)}
@@ -168,7 +194,7 @@ export const ReasoningContent = memo(
168194
)}
169195
{...props}
170196
>
171-
<Response className="grid gap-2">{children}</Response>
197+
<Response className="grid gap-2 text-muted-foreground italic">{children}</Response>
172198
</CollapsibleContent>
173199
)
174200
);

components/ai-elements/response.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ const components: Options["components"] = {
204204
),
205205
a: ({ node, children, className, ...props }) => (
206206
<a
207-
className={cn("font-medium text-primary underline", className)}
207+
className={cn("font-medium text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300 underline", className)}
208208
rel="noreferrer"
209209
target="_blank"
210210
{...props}

components/ai-elements/tool.tsx

Lines changed: 7 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import {
99
XCircleIcon,
1010
} from "lucide-react";
1111
import type { ComponentProps, ReactNode } from "react";
12-
import { Badge } from "@/components/ui/badge";
1312
import {
1413
Collapsible,
1514
CollapsibleContent,
@@ -23,7 +22,7 @@ export type ToolProps = ComponentProps<typeof Collapsible>;
2322

2423
export const Tool = ({ className, ...props }: ToolProps) => (
2524
<Collapsible
26-
className={cn("not-prose mb-4 w-full rounded-md border", className)}
25+
className={cn("not-prose mb-4 w-full rounded-md", className)}
2726
{...props}
2827
/>
2928
);
@@ -34,27 +33,15 @@ export type ToolHeaderProps = {
3433
className?: string;
3534
};
3635

37-
const getStatusBadge = (status: ToolUIPart["state"]) => {
38-
const labels = {
39-
"input-streaming": "Pending",
40-
"input-available": "Running",
41-
"output-available": "Completed",
42-
"output-error": "Error",
43-
} as const;
44-
36+
const getStatusIcon = (status: ToolUIPart["state"]) => {
4537
const icons = {
4638
"input-streaming": <CircleIcon className="size-4" />,
4739
"input-available": <ClockIcon className="size-4 animate-pulse" />,
4840
"output-available": <CheckCircleIcon className="size-4 text-green-600" />,
4941
"output-error": <XCircleIcon className="size-4 text-red-600" />,
5042
} as const;
5143

52-
return (
53-
<Badge className="rounded-full text-xs gap-2" variant="secondary">
54-
{icons[status]}
55-
{labels[status]}
56-
</Badge>
57-
);
44+
return icons[status];
5845
};
5946

6047
export const ToolHeader = ({
@@ -64,15 +51,15 @@ export const ToolHeader = ({
6451
...props
6552
}: ToolHeaderProps) => (
6653
<CollapsibleTrigger
67-
className={cn("flex w-full items-center justify-between gap-4", className)}
54+
className={cn("flex w-full items-center gap-4", className)}
6855
{...props}
6956
>
7057
<div className="flex items-center gap-2">
7158
<WrenchIcon className="size-4 text-muted-foreground" />
72-
<span className="font-medium text-sm">{type}</span>
73-
{getStatusBadge(state)}
59+
<span className="font-medium text-sm text-muted-foreground">{type}</span>
60+
{getStatusIcon(state)}
61+
<ChevronDownIcon className="size-4 text-muted-foreground transition-transform group-data-[state=open]:rotate-180" />
7462
</div>
75-
<ChevronDownIcon className="size-4 text-muted-foreground transition-transform group-data-[state=open]:rotate-180" />
7663
</CollapsibleTrigger>
7764
);
7865

0 commit comments

Comments
 (0)