Skip to content

Commit 9e98f9c

Browse files
authored
Merge pull request #3147 from Dokploy/Add-search-functionality-to-AI-model-selection-dropdown
feat: enhance AI model selection with popover and search functionality
2 parents 75a4979 + c8e7aae commit 9e98f9c

File tree

1 file changed

+137
-48
lines changed

1 file changed

+137
-48
lines changed

apps/dokploy/components/dashboard/settings/handle-ai.tsx

Lines changed: 137 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,19 @@
11
"use client";
22
import { zodResolver } from "@hookform/resolvers/zod";
3-
import { PenBoxIcon, PlusIcon } from "lucide-react";
3+
import { Check, ChevronDown, PenBoxIcon, PlusIcon } from "lucide-react";
44
import { useEffect, useState } from "react";
55
import { useForm } from "react-hook-form";
66
import { toast } from "sonner";
77
import { z } from "zod";
88
import { AlertBlock } from "@/components/shared/alert-block";
99
import { Button } from "@/components/ui/button";
10+
import {
11+
Command,
12+
CommandEmpty,
13+
CommandInput,
14+
CommandItem,
15+
CommandList,
16+
} from "@/components/ui/command";
1017
import {
1118
Dialog,
1219
DialogContent,
@@ -26,13 +33,12 @@ import {
2633
} from "@/components/ui/form";
2734
import { Input } from "@/components/ui/input";
2835
import {
29-
Select,
30-
SelectContent,
31-
SelectItem,
32-
SelectTrigger,
33-
SelectValue,
34-
} from "@/components/ui/select";
36+
Popover,
37+
PopoverContent,
38+
PopoverTrigger,
39+
} from "@/components/ui/popover";
3540
import { Switch } from "@/components/ui/switch";
41+
import { cn } from "@/lib/utils";
3642
import { api } from "@/utils/api";
3743

3844
const Schema = z.object({
@@ -53,6 +59,8 @@ export const HandleAi = ({ aiId }: Props) => {
5359
const utils = api.useUtils();
5460
const [error, setError] = useState<string | null>(null);
5561
const [open, setOpen] = useState(false);
62+
const [modelPopoverOpen, setModelPopoverOpen] = useState(false);
63+
const [modelSearch, setModelSearch] = useState("");
5664
const { data, refetch } = api.ai.one.useQuery(
5765
{
5866
aiId: aiId || "",
@@ -77,13 +85,17 @@ export const HandleAi = ({ aiId }: Props) => {
7785
});
7886

7987
useEffect(() => {
80-
form.reset({
81-
name: data?.name ?? "",
82-
apiUrl: data?.apiUrl ?? "https://api.openai.com/v1",
83-
apiKey: data?.apiKey ?? "",
84-
model: data?.model ?? "",
85-
isEnabled: data?.isEnabled ?? true,
86-
});
88+
if (data) {
89+
form.reset({
90+
name: data?.name ?? "",
91+
apiUrl: data?.apiUrl ?? "https://api.openai.com/v1",
92+
apiKey: data?.apiKey ?? "",
93+
model: data?.model ?? "",
94+
isEnabled: data?.isEnabled ?? true,
95+
});
96+
}
97+
setModelSearch("");
98+
setModelPopoverOpen(false);
8799
}, [aiId, form, data]);
88100

89101
const apiUrl = form.watch("apiUrl");
@@ -104,14 +116,6 @@ export const HandleAi = ({ aiId }: Props) => {
104116
},
105117
);
106118

107-
useEffect(() => {
108-
const apiUrl = form.watch("apiUrl");
109-
const apiKey = form.watch("apiKey");
110-
if (apiUrl && apiKey) {
111-
form.setValue("model", "");
112-
}
113-
}, [form.watch("apiUrl"), form.watch("apiKey")]);
114-
115119
const onSubmit = async (data: Schema) => {
116120
try {
117121
await mutateAsync({
@@ -131,7 +135,16 @@ export const HandleAi = ({ aiId }: Props) => {
131135
};
132136

133137
return (
134-
<Dialog open={open} onOpenChange={setOpen}>
138+
<Dialog
139+
open={open}
140+
onOpenChange={(isOpen) => {
141+
setOpen(isOpen);
142+
if (!isOpen) {
143+
setModelSearch("");
144+
setModelPopoverOpen(false);
145+
}
146+
}}
147+
>
135148
<DialogTrigger className="" asChild>
136149
{aiId ? (
137150
<Button
@@ -182,7 +195,17 @@ export const HandleAi = ({ aiId }: Props) => {
182195
<FormItem>
183196
<FormLabel>API URL</FormLabel>
184197
<FormControl>
185-
<Input placeholder="https://api.openai.com/v1" {...field} />
198+
<Input
199+
placeholder="https://api.openai.com/v1"
200+
{...field}
201+
onChange={(e) => {
202+
field.onChange(e);
203+
// Reset model when user changes API URL
204+
if (form.getValues("model")) {
205+
form.setValue("model", "");
206+
}
207+
}}
208+
/>
186209
</FormControl>
187210
<FormDescription>
188211
The base URL for your AI provider's API
@@ -205,6 +228,13 @@ export const HandleAi = ({ aiId }: Props) => {
205228
placeholder="sk-..."
206229
autoComplete="one-time-code"
207230
{...field}
231+
onChange={(e) => {
232+
field.onChange(e);
233+
// Reset model when user changes API Key
234+
if (form.getValues("model")) {
235+
form.setValue("model", "");
236+
}
237+
}}
208238
/>
209239
</FormControl>
210240
<FormDescription>
@@ -232,30 +262,89 @@ export const HandleAi = ({ aiId }: Props) => {
232262
<FormField
233263
control={form.control}
234264
name="model"
235-
render={({ field }) => (
236-
<FormItem>
237-
<FormLabel>Model</FormLabel>
238-
<Select
239-
onValueChange={field.onChange}
240-
value={field.value || ""}
241-
>
242-
<FormControl>
243-
<SelectTrigger>
244-
<SelectValue placeholder="Select a model" />
245-
</SelectTrigger>
246-
</FormControl>
247-
<SelectContent>
248-
{models.map((model) => (
249-
<SelectItem key={model.id} value={model.id}>
250-
{model.id}
251-
</SelectItem>
252-
))}
253-
</SelectContent>
254-
</Select>
255-
<FormDescription>Select an AI model to use</FormDescription>
256-
<FormMessage />
257-
</FormItem>
258-
)}
265+
render={({ field }) => {
266+
const selectedModel = models.find(
267+
(m) => m.id === field.value,
268+
);
269+
const filteredModels = models.filter((model) =>
270+
model.id.toLowerCase().includes(modelSearch.toLowerCase()),
271+
);
272+
273+
// Ensure selected model is always in the filtered list
274+
const displayModels =
275+
field.value &&
276+
!filteredModels.find((m) => m.id === field.value) &&
277+
selectedModel
278+
? [selectedModel, ...filteredModels]
279+
: filteredModels;
280+
281+
return (
282+
<FormItem>
283+
<FormLabel>Model</FormLabel>
284+
<Popover
285+
open={modelPopoverOpen}
286+
onOpenChange={setModelPopoverOpen}
287+
>
288+
<PopoverTrigger asChild>
289+
<FormControl>
290+
<Button
291+
variant="outline"
292+
className={cn(
293+
"w-full justify-between",
294+
!field.value && "text-muted-foreground",
295+
)}
296+
>
297+
{field.value
298+
? (selectedModel?.id ?? field.value)
299+
: "Select a model"}
300+
<ChevronDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
301+
</Button>
302+
</FormControl>
303+
</PopoverTrigger>
304+
<PopoverContent className="w-[400px] p-0" align="start">
305+
<Command>
306+
<CommandInput
307+
placeholder="Search models..."
308+
value={modelSearch}
309+
onValueChange={setModelSearch}
310+
/>
311+
<CommandList>
312+
<CommandEmpty>No models found.</CommandEmpty>
313+
{displayModels.map((model) => {
314+
const isSelected = field.value === model.id;
315+
return (
316+
<CommandItem
317+
key={model.id}
318+
value={model.id}
319+
onSelect={() => {
320+
field.onChange(model.id);
321+
setModelPopoverOpen(false);
322+
setModelSearch("");
323+
}}
324+
>
325+
<Check
326+
className={cn(
327+
"mr-2 h-4 w-4",
328+
isSelected
329+
? "opacity-100"
330+
: "opacity-0",
331+
)}
332+
/>
333+
{model.id}
334+
</CommandItem>
335+
);
336+
})}
337+
</CommandList>
338+
</Command>
339+
</PopoverContent>
340+
</Popover>
341+
<FormDescription>
342+
Select an AI model to use
343+
</FormDescription>
344+
<FormMessage />
345+
</FormItem>
346+
);
347+
}}
259348
/>
260349
)}
261350

0 commit comments

Comments
 (0)