-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
AI run filtering #2285
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
AI run filtering #2285
Changes from 26 commits
Commits
Show all changes
34 commits
Select commit
Hold shift + click to select a range
34ea140
Queue in run table and filtering
matt-aitken 471849a
Debounce the filter changes
matt-aitken f84131d
Remove console log
matt-aitken a1aac6b
Added machine filtering
matt-aitken f34f4c9
Added version filtering
matt-aitken 314ecbf
Filter by version in the db
matt-aitken 5c1d696
Removed duplicate classes
matt-aitken 4f601f3
Version filtering hasFilters consistency
matt-aitken 73ba30b
Added queues and machines to the bulk action summary
matt-aitken 0d42fd1
runs.list filtering for queue and machine
matt-aitken 77e9c93
Fix for machine errors
matt-aitken 023d6fb
Input field now has accessory instead of shortcut
matt-aitken dc70047
First experiments with the UI
matt-aitken 8491caa
Got the fake filtering working
matt-aitken c05b38c
AI filtering is working pretty well ✨
matt-aitken d86d165
Started working on tool calling
matt-aitken e19f2ce
Tool calling is working
matt-aitken 8b24416
Styling progress
matt-aitken e7ac8f9
Working on the error
matt-aitken 13735cc
Errors work, improved the styling
matt-aitken 958395d
Nice glow effect
matt-aitken dfb0f86
Tweak the darkness of the text field
matt-aitken d4e332a
Re-ordered the UI, set AI settings to use system prompt and telemetry
matt-aitken f156160
Refactored to make it testable
matt-aitken be9351c
Added basic evals
matt-aitken 80562a9
Better time inputs and evals
matt-aitken de42243
Removed some code comments
matt-aitken 0b57867
Remove unused useSearchParam change
matt-aitken 56b075d
Tidy imports
matt-aitken 6192213
If no OpenAI API key send json back
matt-aitken 0039452
Fix for merge conflict with duplicate query filters
matt-aitken beaadb9
Another conflict resolved
matt-aitken b467eba
Another merge conflict resolved
matt-aitken 178aa90
Pass the model in, allow changing it
matt-aitken File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,202 @@ | ||
| import { Portal } from "@radix-ui/react-portal"; | ||
| import { useFetcher, useNavigate } from "@remix-run/react"; | ||
| import { useEffect, useRef, useState } from "react"; | ||
| import { AISparkleIcon } from "~/assets/icons/AISparkleIcon"; | ||
| import { Input } from "~/components/primitives/Input"; | ||
| import { ShortcutKey } from "~/components/primitives/ShortcutKey"; | ||
| import { Spinner } from "~/components/primitives/Spinner"; | ||
| import { useEnvironment } from "~/hooks/useEnvironment"; | ||
| import { useOrganization } from "~/hooks/useOrganizations"; | ||
| import { useProject } from "~/hooks/useProject"; | ||
| import { useSearchParams } from "~/hooks/useSearchParam"; | ||
| import { objectToSearchParams } from "~/utils/searchParams"; | ||
| import { type TaskRunListSearchFilters } from "./RunFilters"; | ||
| import { cn } from "~/utils/cn"; | ||
| import { motion, AnimatePresence } from "framer-motion"; | ||
| import { Popover, PopoverContent, PopoverTrigger } from "~/components/primitives/Popover"; | ||
|
|
||
| type AIFilterResult = | ||
| | { | ||
| success: true; | ||
| filters: TaskRunListSearchFilters; | ||
| explanation?: string; | ||
| } | ||
| | { | ||
| success: false; | ||
| error: string; | ||
| suggestions?: string[]; | ||
| }; | ||
|
|
||
| export function AIFilterInput() { | ||
| const [text, setText] = useState(""); | ||
| const [isFocused, setIsFocused] = useState(false); | ||
| const navigate = useNavigate(); | ||
| const organization = useOrganization(); | ||
| const project = useProject(); | ||
| const environment = useEnvironment(); | ||
| const inputRef = useRef<HTMLInputElement>(null); | ||
| const fetcher = useFetcher<AIFilterResult>(); | ||
|
|
||
| useEffect(() => { | ||
| if (fetcher.data?.success && fetcher.state === "loading") { | ||
| // Clear the input after successful application | ||
| setText(""); | ||
| // Ensure focus is removed after successful submission | ||
| setIsFocused(false); | ||
|
|
||
| const searchParams = objectToSearchParams(fetcher.data.filters); | ||
| if (!searchParams) { | ||
| return; | ||
| } | ||
|
|
||
| console.log("AI filter success", { | ||
| data: fetcher.data, | ||
| searchParams: searchParams.toString(), | ||
| }); | ||
|
|
||
| navigate(`${location.pathname}?${searchParams.toString()}`, { replace: true }); | ||
|
|
||
| //focus the input again | ||
| if (inputRef.current) { | ||
| inputRef.current.focus(); | ||
| } | ||
|
|
||
| // TODO: Show success message with explanation | ||
| console.log(`AI applied filters: ${fetcher.data.explanation}`); | ||
| } else if (fetcher.data?.success === false) { | ||
| // TODO: Show error with suggestions | ||
| console.error(fetcher.data.error, fetcher.data.suggestions); | ||
| } | ||
| }, [fetcher.data, navigate]); | ||
|
|
||
| const isLoading = fetcher.state === "submitting"; | ||
|
|
||
| return ( | ||
| <fetcher.Form | ||
| className="flex items-center gap-2" | ||
| action={`/resources/orgs/${organization.slug}/projects/${project.slug}/env/${environment.slug}/runs/ai-filter`} | ||
| method="post" | ||
| > | ||
| <ErrorPopover error={fetcher.data?.success === false ? fetcher.data.error : undefined}> | ||
| <motion.div | ||
| initial={{ width: "auto" }} | ||
| animate={{ width: isFocused && text.length > 0 ? "24rem" : "auto" }} | ||
| transition={{ | ||
| type: "spring", | ||
| stiffness: 300, | ||
| damping: 30, | ||
| }} | ||
| className="relative h-6 min-w-44" | ||
| > | ||
| <AnimatePresence> | ||
| {isFocused && ( | ||
| <motion.div | ||
| initial={{ opacity: 0 }} | ||
| animate={{ opacity: 1 }} | ||
| exit={{ opacity: 0 }} | ||
| transition={{ duration: 0.2, ease: "linear" }} | ||
| className="animated-gradient-glow-small pointer-events-none absolute inset-0 h-6" | ||
| /> | ||
| )} | ||
| </AnimatePresence> | ||
| <div className="absolute inset-0 left-0 top-0 h-6"> | ||
| <Input | ||
| type="text" | ||
| name="text" | ||
| variant="secondary-small" | ||
| placeholder="Describe your filters…" | ||
| value={text} | ||
| onChange={(e) => setText(e.target.value)} | ||
| disabled={isLoading} | ||
| fullWidth | ||
| ref={inputRef} | ||
| className={cn( | ||
| "disabled:text-text-dimmed/50", | ||
| isFocused && "placeholder:text-text-dimmed/70" | ||
| )} | ||
| containerClassName="has-[:disabled]:opacity-100" | ||
| onKeyDown={(e) => { | ||
| if (e.key === "Enter" && text.trim() && !isLoading) { | ||
| e.preventDefault(); | ||
| const form = e.currentTarget.closest("form"); | ||
| if (form) { | ||
| form.requestSubmit(); | ||
| } | ||
| } | ||
| }} | ||
| onFocus={() => setIsFocused(true)} | ||
| onBlur={() => { | ||
| // Only blur if the text is empty or we're not loading | ||
| if (text.length === 0 || !isLoading) { | ||
| setIsFocused(false); | ||
| } | ||
| }} | ||
| icon={<AISparkleIcon className="size-4" />} | ||
| accessory={ | ||
| isLoading ? ( | ||
| <Spinner | ||
| color={{ | ||
| background: "rgba(99, 102, 241, 1)", | ||
| foreground: "rgba(217, 70, 239, 1)", | ||
| }} | ||
| className="size-4 opacity-80" | ||
| /> | ||
| ) : text.length > 0 ? ( | ||
| <ShortcutKey | ||
| shortcut={{ key: "enter" }} | ||
| variant="small" | ||
| className={cn("transition-opacity", text.length === 0 && "opacity-0")} | ||
| /> | ||
| ) : undefined | ||
| } | ||
| /> | ||
| </div> | ||
| </motion.div> | ||
| </ErrorPopover> | ||
| </fetcher.Form> | ||
| ); | ||
| } | ||
|
|
||
| function ErrorPopover({ | ||
| children, | ||
| error, | ||
| durationMs = 10_000, | ||
| }: { | ||
| children: React.ReactNode; | ||
| error?: string; | ||
| durationMs?: number; | ||
| }) { | ||
| const [isOpen, setIsOpen] = useState(false); | ||
| const timeout = useRef<NodeJS.Timeout | undefined>(); | ||
|
|
||
| useEffect(() => { | ||
| if (error) { | ||
| setIsOpen(true); | ||
| } | ||
| if (timeout.current) { | ||
| clearTimeout(timeout.current); | ||
| } | ||
| timeout.current = setTimeout(() => { | ||
| setIsOpen(false); | ||
| }, durationMs); | ||
|
|
||
| return () => { | ||
| if (timeout.current) { | ||
| clearTimeout(timeout.current); | ||
| } | ||
| }; | ||
| }, [error, durationMs]); | ||
|
|
||
| return ( | ||
| <Popover open={isOpen}> | ||
| <PopoverTrigger asChild>{children}</PopoverTrigger> | ||
| <PopoverContent | ||
| align="start" | ||
| side="bottom" | ||
| 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" | ||
| > | ||
| {error} | ||
| </PopoverContent> | ||
| </Popover> | ||
| ); | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.