@@ -12,7 +12,7 @@ import { useSearchParams } from "~/hooks/useSearchParam";
12
12
import { objectToSearchParams } from "~/utils/searchParams" ;
13
13
import { type TaskRunListSearchFilters } from "./RunFilters" ;
14
14
import { cn } from "~/utils/cn" ;
15
- import { motion } from "framer-motion" ;
15
+ import { motion , AnimatePresence } from "framer-motion" ;
16
16
import { Popover , PopoverContent , PopoverTrigger } from "~/components/primitives/Popover" ;
17
17
18
18
type AIFilterResult =
@@ -37,20 +37,6 @@ export function AIFilterInput() {
37
37
const inputRef = useRef < HTMLInputElement > ( null ) ;
38
38
const fetcher = useFetcher < AIFilterResult > ( ) ;
39
39
40
- // Calculate position for error message
41
- const [ errorPosition , setErrorPosition ] = useState ( { top : 0 , left : 0 , width : 0 } ) ;
42
-
43
- useEffect ( ( ) => {
44
- if ( fetcher . data ?. success === false && inputRef . current ) {
45
- const rect = inputRef . current . getBoundingClientRect ( ) ;
46
- setErrorPosition ( {
47
- top : rect . bottom + window . scrollY ,
48
- left : rect . left + window . scrollX ,
49
- width : rect . width ,
50
- } ) ;
51
- }
52
- } , [ fetcher . data ?. success ] ) ;
53
-
54
40
useEffect ( ( ) => {
55
41
if ( fetcher . data ?. success && fetcher . state === "loading" ) {
56
42
// Clear the input after successful application
@@ -100,8 +86,19 @@ export function AIFilterInput() {
100
86
stiffness : 300 ,
101
87
damping : 30 ,
102
88
} }
103
- className = "animated-gradient-glow relative"
89
+ className = "relative"
104
90
>
91
+ < AnimatePresence >
92
+ { isFocused && (
93
+ < motion . div
94
+ initial = { { opacity : 0 } }
95
+ animate = { { opacity : 1 } }
96
+ exit = { { opacity : 0 } }
97
+ transition = { { duration : 0.2 , ease : "linear" } }
98
+ className = "animated-gradient-glow pointer-events-none absolute inset-0"
99
+ />
100
+ ) }
101
+ </ AnimatePresence >
105
102
< Input
106
103
type = "text"
107
104
name = "text"
@@ -113,9 +110,10 @@ export function AIFilterInput() {
113
110
fullWidth
114
111
ref = { inputRef }
115
112
className = { cn (
116
- "placeholder :text-text-bright " ,
113
+ "disabled :text-text-dimmed/50 " ,
117
114
isFocused && "placeholder:text-text-dimmed"
118
115
) }
116
+ containerClassName = "has-[:disabled]:opacity-100"
119
117
onKeyDown = { ( e ) => {
120
118
if ( e . key === "Enter" && text . trim ( ) && ! isLoading ) {
121
119
e . preventDefault ( ) ;
@@ -135,7 +133,13 @@ export function AIFilterInput() {
135
133
icon = { < AISparkleIcon className = "size-4" /> }
136
134
accessory = {
137
135
isLoading ? (
138
- < Spinner color = "muted" className = "size-4" />
136
+ < Spinner
137
+ color = { {
138
+ background : "rgba(99, 102, 241, 1)" ,
139
+ foreground : "rgba(217, 70, 239, 1)" ,
140
+ } }
141
+ className = "size-4"
142
+ />
139
143
) : text . length > 0 ? (
140
144
< ShortcutKey
141
145
shortcut = { { key : "enter" } }
@@ -145,20 +149,6 @@ export function AIFilterInput() {
145
149
) : undefined
146
150
}
147
151
/>
148
- { fetcher . data ?. success === false && (
149
- < Portal >
150
- < div
151
- className = "fixed z-[9999] rounded-md bg-rose-500 px-3 py-2 text-sm text-white shadow-lg"
152
- style = { {
153
- top : `${ errorPosition . top + 8 } px` ,
154
- left : `${ errorPosition . left } px` ,
155
- width : `${ errorPosition . width } px` ,
156
- } }
157
- >
158
- { fetcher . data . error }
159
- </ div >
160
- </ Portal >
161
- ) }
162
152
</ motion . div >
163
153
</ ErrorPopover >
164
154
</ fetcher . Form >
@@ -168,7 +158,7 @@ export function AIFilterInput() {
168
158
function ErrorPopover ( {
169
159
children,
170
160
error,
171
- durationMs = 2_000 ,
161
+ durationMs = 10_000 ,
172
162
} : {
173
163
children : React . ReactNode ;
174
164
error ?: string ;
@@ -178,11 +168,14 @@ function ErrorPopover({
178
168
const timeout = useRef < NodeJS . Timeout | undefined > ( ) ;
179
169
180
170
useEffect ( ( ) => {
171
+ if ( error ) {
172
+ setIsOpen ( true ) ;
173
+ }
181
174
if ( timeout . current ) {
182
175
clearTimeout ( timeout . current ) ;
183
176
}
184
177
timeout . current = setTimeout ( ( ) => {
185
- setIsOpen ( ( s ) => true ) ;
178
+ setIsOpen ( false ) ;
186
179
} , durationMs ) ;
187
180
188
181
return ( ) => {
@@ -193,9 +186,15 @@ function ErrorPopover({
193
186
} , [ error , durationMs ] ) ;
194
187
195
188
return (
196
- < Popover open = { isOpen } onOpenChange = { setIsOpen } >
189
+ < Popover open = { isOpen } >
197
190
< PopoverTrigger asChild > { children } </ PopoverTrigger >
198
- < PopoverContent > { error } </ PopoverContent >
191
+ < PopoverContent
192
+ align = "start"
193
+ side = "bottom"
194
+ className = "w-[var(--radix-popover-trigger-width)] min-w-[var(--radix-popover-trigger-width)] max-w-[var(--radix-popover-trigger-width)] border border-error/20 bg-[#2F1D24] px-3 py-2 text-xs text-text-dimmed"
195
+ >
196
+ { error }
197
+ </ PopoverContent >
199
198
</ Popover >
200
199
) ;
201
200
}
0 commit comments