Skip to content

Commit 33f5b7c

Browse files
committed
Working on the error
1 parent 9f8ce6c commit 33f5b7c

File tree

1 file changed

+104
-66
lines changed

1 file changed

+104
-66
lines changed

apps/webapp/app/components/runs/v3/AIFilterInput.tsx

Lines changed: 104 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { objectToSearchParams } from "~/utils/searchParams";
1313
import { type TaskRunListSearchFilters } from "./RunFilters";
1414
import { cn } from "~/utils/cn";
1515
import { motion } from "framer-motion";
16+
import { Popover, PopoverContent, PopoverTrigger } from "~/components/primitives/Popover";
1617

1718
type AIFilterResult =
1819
| {
@@ -90,74 +91,111 @@ export function AIFilterInput() {
9091
action={`/resources/orgs/${organization.slug}/projects/${project.slug}/env/${environment.slug}/runs/ai-filter`}
9192
method="post"
9293
>
93-
<motion.div
94-
initial={{ width: "auto" }}
95-
animate={{ width: isFocused && text.length > 0 ? "24rem" : "auto" }}
96-
transition={{
97-
type: "spring",
98-
stiffness: 300,
99-
damping: 30,
100-
}}
101-
className="animated-gradient-glow relative"
102-
>
103-
<Input
104-
type="text"
105-
name="text"
106-
variant="secondary-small"
107-
placeholder="Describe your filters…"
108-
value={text}
109-
onChange={(e) => setText(e.target.value)}
110-
disabled={isLoading}
111-
fullWidth
112-
ref={inputRef}
113-
className={cn(
114-
"placeholder:text-text-bright",
115-
isFocused && "placeholder:text-text-dimmed"
116-
)}
117-
onKeyDown={(e) => {
118-
if (e.key === "Enter" && text.trim() && !isLoading) {
119-
e.preventDefault();
120-
const form = e.currentTarget.closest("form");
121-
if (form) {
122-
form.requestSubmit();
123-
}
124-
}
94+
<ErrorPopover error={fetcher.data?.success === false ? fetcher.data.error : undefined}>
95+
<motion.div
96+
initial={{ width: "auto" }}
97+
animate={{ width: isFocused && text.length > 0 ? "24rem" : "auto" }}
98+
transition={{
99+
type: "spring",
100+
stiffness: 300,
101+
damping: 30,
125102
}}
126-
onFocus={() => setIsFocused(true)}
127-
onBlur={() => {
128-
// Only blur if the text is empty or we're not loading
129-
if (text.length === 0 || !isLoading) {
130-
setIsFocused(false);
103+
className="animated-gradient-glow relative"
104+
>
105+
<Input
106+
type="text"
107+
name="text"
108+
variant="secondary-small"
109+
placeholder="Describe your filters…"
110+
value={text}
111+
onChange={(e) => setText(e.target.value)}
112+
disabled={isLoading}
113+
fullWidth
114+
ref={inputRef}
115+
className={cn(
116+
"placeholder:text-text-bright",
117+
isFocused && "placeholder:text-text-dimmed"
118+
)}
119+
onKeyDown={(e) => {
120+
if (e.key === "Enter" && text.trim() && !isLoading) {
121+
e.preventDefault();
122+
const form = e.currentTarget.closest("form");
123+
if (form) {
124+
form.requestSubmit();
125+
}
126+
}
127+
}}
128+
onFocus={() => setIsFocused(true)}
129+
onBlur={() => {
130+
// Only blur if the text is empty or we're not loading
131+
if (text.length === 0 || !isLoading) {
132+
setIsFocused(false);
133+
}
134+
}}
135+
icon={<AISparkleIcon className="size-4" />}
136+
accessory={
137+
isLoading ? (
138+
<Spinner color="muted" className="size-4" />
139+
) : text.length > 0 ? (
140+
<ShortcutKey
141+
shortcut={{ key: "enter" }}
142+
variant="small"
143+
className={cn("transition-opacity", text.length === 0 && "opacity-0")}
144+
/>
145+
) : undefined
131146
}
132-
}}
133-
icon={<AISparkleIcon className="size-4" />}
134-
accessory={
135-
isLoading ? (
136-
<Spinner color="muted" className="size-4" />
137-
) : text.length > 0 ? (
138-
<ShortcutKey
139-
shortcut={{ key: "enter" }}
140-
variant="small"
141-
className={cn("transition-opacity", text.length === 0 && "opacity-0")}
142-
/>
143-
) : undefined
144-
}
145-
/>
146-
{fetcher.data?.success === false && (
147-
<Portal>
148-
<div
149-
className="fixed z-[9999] rounded-md bg-rose-500 px-3 py-2 text-sm text-white shadow-lg"
150-
style={{
151-
top: `${errorPosition.top + 8}px`,
152-
left: `${errorPosition.left}px`,
153-
width: `${errorPosition.width}px`,
154-
}}
155-
>
156-
{fetcher.data.error}
157-
</div>
158-
</Portal>
159-
)}
160-
</motion.div>
147+
/>
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+
</motion.div>
163+
</ErrorPopover>
161164
</fetcher.Form>
162165
);
163166
}
167+
168+
function ErrorPopover({
169+
children,
170+
error,
171+
durationMs = 2_000,
172+
}: {
173+
children: React.ReactNode;
174+
error?: string;
175+
durationMs?: number;
176+
}) {
177+
const [isOpen, setIsOpen] = useState(false);
178+
const timeout = useRef<NodeJS.Timeout | undefined>();
179+
180+
useEffect(() => {
181+
if (timeout.current) {
182+
clearTimeout(timeout.current);
183+
}
184+
timeout.current = setTimeout(() => {
185+
setIsOpen((s) => true);
186+
}, durationMs);
187+
188+
return () => {
189+
if (timeout.current) {
190+
clearTimeout(timeout.current);
191+
}
192+
};
193+
}, [error, durationMs]);
194+
195+
return (
196+
<Popover open={isOpen} onOpenChange={setIsOpen}>
197+
<PopoverTrigger asChild>{children}</PopoverTrigger>
198+
<PopoverContent>{error}</PopoverContent>
199+
</Popover>
200+
);
201+
}

0 commit comments

Comments
 (0)