Skip to content

Commit 3393a39

Browse files
authored
Some UI Cleanup Items (#6)
* Fix message scroll to be only on every user message * Remove unused files * Show scroll button above input always * Add our custom toast component * Consistent scrolling when user submits message * Show toast on task send or message send failure * Make entire reasoning / tool-pair dropdown clickable * Update scroll button on all task id changes
1 parent f1ac3a1 commit 3393a39

File tree

11 files changed

+168
-237
lines changed

11 files changed

+168
-237
lines changed

agentex-ui/CLAUDE.md

Lines changed: 0 additions & 95 deletions
This file was deleted.

agentex-ui/app/main-content.tsx

Lines changed: 43 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,6 @@ function ContentArea({
102102
const { agentName, updateParams } = useSafeSearchParams();
103103
const [prompt, setPrompt] = useState<string>('');
104104
const scrollContainerRef = useRef<HTMLDivElement>(null);
105-
const [autoScrollEnabled, setAutoScrollEnabled] = useState(true);
106105
const [showScrollButton, setShowScrollButton] = useState(false);
107106
const [localAgentName, setLocalAgentName] = useLocalStorageState<
108107
string | undefined
@@ -117,9 +116,6 @@ function ContentArea({
117116
// eslint-disable-next-line react-hooks/exhaustive-deps
118117
}, [isLoading]);
119118

120-
// Show thread view when we have a task ID
121-
const inThread = !!taskID;
122-
123119
const handleSelectAgent = useCallback(
124120
(agentName: string | undefined) => {
125121
updateParams({
@@ -134,7 +130,10 @@ function ContentArea({
134130
// Scroll detection - track if user is near bottom
135131
useEffect(() => {
136132
const container = scrollContainerRef.current;
137-
if (!container) return;
133+
if (!container) {
134+
setShowScrollButton(false);
135+
return;
136+
}
138137

139138
const handleScroll = () => {
140139
const { scrollTop, scrollHeight, clientHeight } = container;
@@ -143,38 +142,28 @@ function ContentArea({
143142
const scrollThreshold = 100; // pixels from bottom
144143
const isNearBottom = distanceFromBottom < scrollThreshold;
145144

146-
setAutoScrollEnabled(isNearBottom);
147145
setShowScrollButton(!isNearBottom);
148146
};
149147

150148
container.addEventListener('scroll', handleScroll);
151149
return () => container.removeEventListener('scroll', handleScroll);
152-
}, [inThread]);
150+
}, [taskID]);
151+
152+
const scrollToBottom = useCallback(() => {
153+
if (!scrollContainerRef.current) return;
154+
scrollContainerRef.current.scrollTo({
155+
top: scrollContainerRef.current.scrollHeight,
156+
behavior: 'smooth',
157+
});
158+
}, [scrollContainerRef]);
153159

154-
// Scroll to absolute bottom when task loads or changes
155160
useEffect(() => {
156161
if (scrollContainerRef.current && taskID) {
157-
// Use a small delay to ensure content is rendered and heights are calculated
158162
setTimeout(() => {
159-
if (scrollContainerRef.current) {
160-
// Scroll to the maximum possible scroll position (includes blank space)
161-
scrollContainerRef.current.scrollTo({
162-
top: scrollContainerRef.current.scrollHeight,
163-
behavior: 'smooth',
164-
});
165-
}
163+
scrollToBottom();
166164
}, 150);
167165
}
168-
}, [taskID]);
169-
170-
// Scroll to bottom handler for button
171-
const scrollToBottom = () => {
172-
scrollContainerRef.current?.scrollTo({
173-
top: scrollContainerRef.current.scrollHeight,
174-
behavior: 'smooth',
175-
});
176-
setAutoScrollEnabled(true); // Re-enable auto-scroll when user clicks button
177-
};
166+
}, [scrollToBottom, taskID]);
178167

179168
return (
180169
<AnimatePresence>
@@ -183,7 +172,6 @@ function ContentArea({
183172
className={`relative flex h-full flex-1 flex-col ${!taskID ? 'justify-center' : 'justify-between'}`}
184173
transition={{ duration: 0.25, ease: 'easeInOut' }}
185174
>
186-
{/* Top Bar */}
187175
{taskID && agentName && (
188176
<motion.div
189177
key="topbar"
@@ -201,7 +189,6 @@ function ContentArea({
201189
</motion.div>
202190
)}
203191

204-
{/* Content Area */}
205192
{taskID ? (
206193
<motion.div
207194
key="chat-view"
@@ -215,10 +202,7 @@ function ContentArea({
215202
<div className="flex min-h-full w-full flex-col items-center px-4 sm:px-6 md:px-8">
216203
<div className="w-full max-w-3xl">
217204
<TaskProvider taskId={taskID}>
218-
<MemoizedTaskMessagesComponent
219-
taskId={taskID}
220-
autoScrollEnabled={autoScrollEnabled}
221-
/>
205+
<MemoizedTaskMessagesComponent taskId={taskID} />
222206
</TaskProvider>
223207
</div>
224208
</div>
@@ -249,37 +233,9 @@ function ContentArea({
249233
</motion.div>
250234
)}
251235

252-
{/* Scroll to bottom button */}
253-
{taskID && (
254-
<AnimatePresence>
255-
{showScrollButton && (
256-
<motion.div
257-
className="pointer-events-none absolute bottom-28 left-1/2 -translate-x-1/2"
258-
initial={{ y: 30, opacity: 0 }}
259-
animate={{ y: 0, opacity: 1 }}
260-
exit={{ y: 30, opacity: 0 }}
261-
transition={{
262-
duration: 0.2,
263-
type: 'spring',
264-
stiffness: 300,
265-
damping: 35,
266-
mass: 0.8,
267-
}}
268-
>
269-
<IconButton
270-
className="pointer-events-auto size-10 rounded-full shadow-lg"
271-
onClick={scrollToBottom}
272-
icon={ArrowDown}
273-
/>
274-
</motion.div>
275-
)}
276-
</AnimatePresence>
277-
)}
278-
279-
{/* Prompt Input */}
280236
<motion.div
281237
layout="position"
282-
className="flex w-full justify-center px-4 py-4 sm:px-6 md:px-8"
238+
className="relative flex w-full justify-center px-4 py-4 sm:px-6 md:px-8"
283239
transition={{
284240
layout: {
285241
type: 'spring',
@@ -289,6 +245,32 @@ function ContentArea({
289245
},
290246
}}
291247
>
248+
{taskID && (
249+
<AnimatePresence>
250+
{showScrollButton && (
251+
<motion.div
252+
className="pointer-events-none absolute bottom-full left-1/2 z-10 mb-4 -translate-x-1/2"
253+
initial={{ y: 30, opacity: 0 }}
254+
animate={{ y: 0, opacity: 1 }}
255+
exit={{ y: 30, opacity: 0 }}
256+
transition={{
257+
duration: 0.2,
258+
type: 'spring',
259+
stiffness: 300,
260+
damping: 35,
261+
mass: 0.8,
262+
}}
263+
>
264+
<IconButton
265+
className="pointer-events-auto size-10 rounded-full shadow-lg"
266+
onClick={scrollToBottom}
267+
icon={ArrowDown}
268+
/>
269+
</motion.div>
270+
)}
271+
</AnimatePresence>
272+
)}
273+
292274
<div className="w-full max-w-3xl">
293275
<PromptInput prompt={prompt} setPrompt={setPrompt} />
294276
</div>

agentex-ui/components/agentex/copy-button.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
import { useCallback, useRef, useState } from 'react';
44

55
import { Check, Copy } from 'lucide-react';
6-
import { toast } from 'react-toastify';
76

87
import { IconButton } from '@/components/agentex/icon-button';
8+
import { toast } from '@/components/agentex/toast';
99
import {
1010
Tooltip,
1111
TooltipContent,
@@ -47,7 +47,10 @@ export function CopyButton({
4747
}, timeout);
4848
},
4949
err => {
50-
toast.error('Failed to copy content:', err);
50+
toast.error({
51+
title: 'Failed to copy content',
52+
message: err instanceof Error ? err.message : 'Please try again.',
53+
});
5154
}
5255
);
5356
} else if (onClick) {

agentex-ui/components/agentex/prompt-input.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ import { EditorView, keymap } from '@codemirror/view';
99
import CodeMirror from '@uiw/react-codemirror';
1010
import { DataContent, TextContent } from 'agentex/resources';
1111
import { ArrowUp } from 'lucide-react';
12-
import { toast } from 'react-toastify';
1312

1413
import { IconButton } from '@/components/agentex/icon-button';
14+
import { toast } from '@/components/agentex/toast';
1515
import { useAgentexClient } from '@/components/providers';
1616
import { Switch } from '@/components/ui/switch';
1717
import { useAgents } from '@/hooks/use-agents';

agentex-ui/components/agentex/task-message-reasoning-content.tsx

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,11 @@ export function TaskMessageReasoning({ message }: TaskMessageReasoningProps) {
5151
ease: 'easeInOut',
5252
}}
5353
>
54-
<div className="mb-2 flex items-center gap-2">
54+
<button
55+
className="mb-2 flex items-center gap-2"
56+
type="button"
57+
onClick={() => setIsCollapsed(!isCollapsed)}
58+
>
5559
<BrainIcon className="size-4" />
5660
<ShimmeringText
5761
enabled={!nextMessage}
@@ -61,15 +65,14 @@ export function TaskMessageReasoning({ message }: TaskMessageReasoningProps) {
6165
: `Planning next steps...`
6266
}
6367
/>
64-
<button onClick={() => setIsCollapsed(!isCollapsed)}>
65-
<ChevronDownIcon
66-
className={cn(
67-
'size-4 transition-transform duration-500',
68-
isCollapsed ? '' : 'scale-y-[-1]'
69-
)}
70-
/>
71-
</button>
72-
</div>
68+
69+
<ChevronDownIcon
70+
className={cn(
71+
'size-4 transition-transform duration-500',
72+
isCollapsed ? '' : 'scale-y-[-1]'
73+
)}
74+
/>
75+
</button>
7376
<Collapsible collapsed={isCollapsed}>
7477
<Response className="ml-6 grid border-l-4 border-gray-300 pl-3 text-gray-500">
7578
{reasoningText}

agentex-ui/components/agentex/task-message-tool-pair.tsx

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,11 @@ function TaskMessageToolPairComponent({
7070
ease: 'easeInOut',
7171
}}
7272
>
73-
<div className="mb-2 flex items-center gap-2">
73+
<button
74+
className="mb-2 flex items-center gap-2"
75+
type="button"
76+
onClick={() => setIsCollapsed(!isCollapsed)}
77+
>
7478
<Wrench className="size-4" />
7579
<ShimmeringText
7680
enabled={!toolResponseMessage}
@@ -80,17 +84,13 @@ function TaskMessageToolPairComponent({
8084
: 'Used tool: ' + toolRequestMessage.content.name
8185
}
8286
/>
83-
<button type="button" onClick={() => setIsCollapsed(!isCollapsed)}>
84-
<ChevronDownIcon
85-
className={cn(
86-
'size-4 transition-transform duration-500',
87-
isCollapsed ? '' : 'scale-y-[-1]'
88-
)}
89-
/>
90-
</button>
91-
{/* The taskmessage state doesn't seem to be working properly, so this status badge is inaccurate. */}
92-
{/* {getStatusBadge(state)} */}
93-
</div>
87+
<ChevronDownIcon
88+
className={cn(
89+
'size-4 transition-transform duration-500',
90+
isCollapsed ? '' : 'scale-y-[-1]'
91+
)}
92+
/>
93+
</button>
9494
<Collapsible collapsed={isCollapsed}>
9595
<div className="ml-6 flex flex-col gap-4">
9696
<ToolInput input={toolRequestMessage.content.arguments} />
@@ -103,4 +103,4 @@ function TaskMessageToolPairComponent({
103103

104104
const MemoizedTaskMessageToolPairComponent = memo(TaskMessageToolPairComponent);
105105

106-
export { TaskMessageToolPairComponent, MemoizedTaskMessageToolPairComponent };
106+
export { MemoizedTaskMessageToolPairComponent, TaskMessageToolPairComponent };

0 commit comments

Comments
 (0)