Skip to content

Commit fdddd70

Browse files
committed
First experiments with the UI
1 parent 544c899 commit fdddd70

File tree

4 files changed

+148
-6
lines changed

4 files changed

+148
-6
lines changed
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { useFetcher } from "@remix-run/react";
2+
import { useState, useEffect } from "react";
3+
import { AISparkleIcon } from "~/assets/icons/AISparkleIcon";
4+
import { Button } from "~/components/primitives/Buttons";
5+
import { Input } from "~/components/primitives/Input";
6+
import { Spinner } from "~/components/primitives/Spinner";
7+
import { useSearchParams } from "~/hooks/useSearchParam";
8+
import { useOrganization } from "~/hooks/useOrganizations";
9+
import { useProject } from "~/hooks/useProject";
10+
import { useEnvironment } from "~/hooks/useEnvironment";
11+
import { type TaskRunListSearchFilters } from "./RunFilters";
12+
import { objectToSearchParams } from "~/utils/searchParams";
13+
import { ShortcutKey } from "~/components/primitives/ShortcutKey";
14+
15+
type AIFilterResult =
16+
| {
17+
success: true;
18+
filters: TaskRunListSearchFilters;
19+
explanation?: string;
20+
}
21+
| {
22+
success: false;
23+
error: string;
24+
suggestions?: string[];
25+
};
26+
27+
export function AIFilterInput() {
28+
const [text, setText] = useState("");
29+
const { replace } = useSearchParams();
30+
const organization = useOrganization();
31+
const project = useProject();
32+
const environment = useEnvironment();
33+
34+
const fetcher = useFetcher<AIFilterResult>();
35+
36+
useEffect(() => {
37+
if (fetcher.data?.success) {
38+
const searchParams = objectToSearchParams(fetcher.data.filters);
39+
if (!searchParams) {
40+
return;
41+
}
42+
43+
replace(searchParams);
44+
45+
// Clear the input after successful application
46+
setText("");
47+
48+
// TODO: Show success message with explanation
49+
console.log(`AI applied filters: ${fetcher.data.explanation}`);
50+
} else if (fetcher.data?.success === false) {
51+
// TODO: Show error with suggestions
52+
console.error(fetcher.data.error, fetcher.data.suggestions);
53+
}
54+
}, [fetcher.data, replace]);
55+
56+
const isLoading = fetcher.state === "submitting";
57+
58+
return (
59+
<fetcher.Form
60+
className="flex items-center gap-2"
61+
action={`/resources/orgs/${organization.slug}/projects/${project.slug}/env/${environment.slug}/runs/ai-filter`}
62+
method="post"
63+
>
64+
<div className="relative flex-1">
65+
<Input
66+
type="text"
67+
name="text"
68+
variant="small"
69+
placeholder="Describe your filters…"
70+
value={text}
71+
onChange={(e) => setText(e.target.value)}
72+
disabled={isLoading}
73+
className="pr-10"
74+
onKeyDown={(e) => {
75+
if (e.key === "Enter" && text.trim() && !isLoading) {
76+
e.preventDefault();
77+
const form = e.currentTarget.closest("form");
78+
if (form) {
79+
form.requestSubmit();
80+
}
81+
}
82+
}}
83+
icon={<AISparkleIcon className="size-4" />}
84+
accessory={
85+
text.length > 0 ? (
86+
<ShortcutKey shortcut={{ key: "enter" }} variant="small" />
87+
) : undefined
88+
}
89+
/>
90+
{isLoading && (
91+
<div className="absolute right-3 top-1/2 -translate-y-1/2">
92+
<Spinner color="muted" />
93+
</div>
94+
)}
95+
</div>
96+
</fetcher.Form>
97+
);
98+
}

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ import { type loader as versionsLoader } from "~/routes/resources.orgs.$organiza
6161
import { Button } from "../../primitives/Buttons";
6262
import { BulkActionTypeCombo } from "./BulkAction";
6363
import { appliedSummary, FilterMenuProvider, TimeFilter } from "./SharedFilters";
64+
import { AIFilterInput } from "./AIFilterInput";
6465
import {
6566
allTaskRunStatuses,
6667
descriptionForTaskRunStatus,
@@ -305,6 +306,7 @@ export function RunsFilters(props: RunFiltersProps) {
305306

306307
return (
307308
<div className="flex flex-row flex-wrap items-center gap-1">
309+
<AIFilterInput />
308310
<FilterMenu {...props} />
309311
<RootOnlyToggle defaultValue={props.rootOnlyDefault} />
310312
<TimeFilter />
@@ -355,9 +357,8 @@ function FilterMenu(props: RunFiltersProps) {
355357
variant={"secondary/small"}
356358
shortcut={shortcut}
357359
tooltipTitle={"Filter runs"}
358-
>
359-
Filter
360-
</SelectTrigger>
360+
className="pr-0.5"
361+
/>
361362
);
362363

363364
return (

apps/webapp/app/hooks/useSearchParam.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,8 @@ export function useSearchParams() {
99
const location = useOptimisticLocation();
1010

1111
const replace = useCallback(
12-
(values: Values) => {
12+
(values: Values | URLSearchParams) => {
1313
const s = set(new URLSearchParams(location.search), values);
14-
1514
navigate(`${location.pathname}?${s.toString()}`, { replace: true });
1615
},
1716
[location, navigate]
@@ -70,7 +69,7 @@ export function useSearchParams() {
7069
};
7170
}
7271

73-
function set(searchParams: URLSearchParams, values: Values) {
72+
function set(searchParams: URLSearchParams, values: Values | URLSearchParams) {
7473
const search = new URLSearchParams(searchParams);
7574
for (const [param, value] of Object.entries(values)) {
7675
if (value === undefined) {
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { type ActionFunctionArgs, json } from "@remix-run/server-runtime";
2+
import { z } from "zod";
3+
import { requireUserId } from "~/services/session.server";
4+
import { EnvironmentParamSchema } from "~/utils/pathBuilder";
5+
import { type TaskRunListSearchFilters } from "~/components/runs/v3/RunFilters";
6+
7+
const RequestSchema = z.object({
8+
text: z.string().min(1),
9+
});
10+
11+
export async function action({ request, params }: ActionFunctionArgs) {
12+
const userId = await requireUserId(request);
13+
const { organizationSlug, projectParam, envParam } = EnvironmentParamSchema.parse(params);
14+
15+
// Parse the request body
16+
const formData = await request.formData();
17+
const submission = RequestSchema.safeParse(Object.fromEntries(formData));
18+
19+
if (!submission.success) {
20+
return json<{ success: false; error: string }>(
21+
{
22+
success: false,
23+
error: "Invalid request data",
24+
},
25+
{ status: 400 }
26+
);
27+
}
28+
29+
const { text } = submission.data;
30+
31+
// TODO: Replace this with actual AI processing
32+
// For now, return fake successful data
33+
const fakeFilters: TaskRunListSearchFilters = {
34+
statuses: ["COMPLETED_WITH_ERRORS"],
35+
period: "7d",
36+
tags: ["test-tag"],
37+
};
38+
39+
return json<{ success: true; filters: TaskRunListSearchFilters; explanation: string }>({
40+
success: true,
41+
filters: fakeFilters,
42+
explanation: `Applied filters: failed status, last 7 days, with tag "test-tag"`,
43+
});
44+
}

0 commit comments

Comments
 (0)