forked from hoarder-app/hoarder
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feature(web): Add the ability to customize the inference prompts. Fixes
- Loading branch information
1 parent
57f5faa
commit 36fb5a4
Showing
13 changed files
with
1,764 additions
and
39 deletions.
There are no files selected for viewing
This file contains 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 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,325 @@ | ||
"use client"; | ||
|
||
import { ActionButton } from "@/components/ui/action-button"; | ||
import { | ||
Form, | ||
FormControl, | ||
FormField, | ||
FormItem, | ||
FormMessage, | ||
} from "@/components/ui/form"; | ||
import { FullPageSpinner } from "@/components/ui/full-page-spinner"; | ||
import { Input } from "@/components/ui/input"; | ||
import { | ||
Select, | ||
SelectContent, | ||
SelectGroup, | ||
SelectItem, | ||
SelectTrigger, | ||
SelectValue, | ||
} from "@/components/ui/select"; | ||
import { toast } from "@/components/ui/use-toast"; | ||
import { useClientConfig } from "@/lib/clientConfig"; | ||
import { api } from "@/lib/trpc"; | ||
import { zodResolver } from "@hookform/resolvers/zod"; | ||
import { Plus, Save, Trash2 } from "lucide-react"; | ||
import { useForm } from "react-hook-form"; | ||
import { z } from "zod"; | ||
|
||
import { buildImagePrompt, buildTextPrompt } from "@hoarder/shared/prompts"; | ||
import { | ||
zNewPromptSchema, | ||
ZPrompt, | ||
zUpdatePromptSchema, | ||
} from "@hoarder/shared/types/prompts"; | ||
|
||
export function PromptEditor() { | ||
const apiUtils = api.useUtils(); | ||
|
||
const form = useForm<z.infer<typeof zNewPromptSchema>>({ | ||
resolver: zodResolver(zNewPromptSchema), | ||
defaultValues: { | ||
text: "", | ||
appliesTo: "all", | ||
}, | ||
}); | ||
|
||
const { mutateAsync: createPrompt, isPending: isCreating } = | ||
api.prompts.create.useMutation({ | ||
onSuccess: () => { | ||
toast({ | ||
description: "Prompt has been created!", | ||
}); | ||
apiUtils.prompts.list.invalidate(); | ||
}, | ||
}); | ||
|
||
return ( | ||
<Form {...form}> | ||
<form | ||
className="flex gap-2" | ||
onSubmit={form.handleSubmit(async (value) => { | ||
await createPrompt(value); | ||
form.resetField("text"); | ||
})} | ||
> | ||
<FormField | ||
control={form.control} | ||
name="text" | ||
render={({ field }) => { | ||
return ( | ||
<FormItem className="flex-1"> | ||
<FormControl> | ||
<Input | ||
placeholder="Add a custom prompt" | ||
type="text" | ||
{...field} | ||
/> | ||
</FormControl> | ||
<FormMessage /> | ||
</FormItem> | ||
); | ||
}} | ||
/> | ||
|
||
<FormField | ||
control={form.control} | ||
name="appliesTo" | ||
render={({ field }) => { | ||
return ( | ||
<FormItem className="flex-0"> | ||
<FormControl> | ||
<Select | ||
onValueChange={field.onChange} | ||
defaultValue={field.value} | ||
> | ||
<SelectTrigger> | ||
<SelectValue placeholder="Applies To" /> | ||
</SelectTrigger> | ||
<SelectContent> | ||
<SelectGroup> | ||
<SelectItem value="all">All</SelectItem> | ||
<SelectItem value="text">Text</SelectItem> | ||
<SelectItem value="images">Images</SelectItem> | ||
</SelectGroup> | ||
</SelectContent> | ||
</Select> | ||
</FormControl> | ||
<FormMessage /> | ||
</FormItem> | ||
); | ||
}} | ||
/> | ||
<ActionButton | ||
type="submit" | ||
loading={isCreating} | ||
variant="default" | ||
className="items-center" | ||
> | ||
<Plus className="mr-2 size-4" /> | ||
Add | ||
</ActionButton> | ||
</form> | ||
</Form> | ||
); | ||
} | ||
|
||
export function PromptRow({ prompt }: { prompt: ZPrompt }) { | ||
const apiUtils = api.useUtils(); | ||
const { mutateAsync: updatePrompt, isPending: isUpdating } = | ||
api.prompts.update.useMutation({ | ||
onSuccess: () => { | ||
toast({ | ||
description: "Prompt has been updated!", | ||
}); | ||
apiUtils.prompts.list.invalidate(); | ||
}, | ||
}); | ||
const { mutate: deletePrompt, isPending: isDeleting } = | ||
api.prompts.delete.useMutation({ | ||
onSuccess: () => { | ||
toast({ | ||
description: "Prompt has been deleted!", | ||
}); | ||
apiUtils.prompts.list.invalidate(); | ||
}, | ||
}); | ||
|
||
const form = useForm<z.infer<typeof zUpdatePromptSchema>>({ | ||
resolver: zodResolver(zUpdatePromptSchema), | ||
defaultValues: { | ||
promptId: prompt.id, | ||
text: prompt.text, | ||
appliesTo: prompt.appliesTo, | ||
}, | ||
}); | ||
|
||
return ( | ||
<Form {...form}> | ||
<form | ||
className="flex gap-2" | ||
onSubmit={form.handleSubmit(async (value) => { | ||
await updatePrompt(value); | ||
})} | ||
> | ||
<FormField | ||
control={form.control} | ||
name="promptId" | ||
render={({ field }) => { | ||
return ( | ||
<FormItem className="hidden"> | ||
<FormControl> | ||
<Input | ||
placeholder="Add a custom prompt" | ||
type="hidden" | ||
{...field} | ||
/> | ||
</FormControl> | ||
<FormMessage /> | ||
</FormItem> | ||
); | ||
}} | ||
/> | ||
<FormField | ||
control={form.control} | ||
name="text" | ||
render={({ field }) => { | ||
return ( | ||
<FormItem className="flex-1"> | ||
<FormControl> | ||
<Input | ||
placeholder="Add a custom prompt" | ||
type="text" | ||
{...field} | ||
/> | ||
</FormControl> | ||
<FormMessage /> | ||
</FormItem> | ||
); | ||
}} | ||
/> | ||
|
||
<FormField | ||
control={form.control} | ||
name="appliesTo" | ||
render={({ field }) => { | ||
return ( | ||
<FormItem className="flex-0"> | ||
<FormControl> | ||
<Select | ||
onValueChange={field.onChange} | ||
defaultValue={field.value} | ||
> | ||
<SelectTrigger> | ||
<SelectValue placeholder="Applies To" /> | ||
</SelectTrigger> | ||
<SelectContent> | ||
<SelectGroup> | ||
<SelectItem value="all">All</SelectItem> | ||
<SelectItem value="text">Text</SelectItem> | ||
<SelectItem value="images">Images</SelectItem> | ||
</SelectGroup> | ||
</SelectContent> | ||
</Select> | ||
</FormControl> | ||
<FormMessage /> | ||
</FormItem> | ||
); | ||
}} | ||
/> | ||
<ActionButton | ||
loading={isUpdating} | ||
variant="secondary" | ||
type="submit" | ||
className="items-center" | ||
> | ||
<Save className="mr-2 size-4" /> | ||
Save | ||
</ActionButton> | ||
<ActionButton | ||
loading={isDeleting} | ||
variant="destructive" | ||
onClick={() => deletePrompt({ promptId: prompt.id })} | ||
className="items-center" | ||
type="button" | ||
> | ||
<Trash2 className="mr-2 size-4" /> | ||
Delete | ||
</ActionButton> | ||
</form> | ||
</Form> | ||
); | ||
} | ||
|
||
export function CustomPrompts() { | ||
const { data: prompts, isLoading } = api.prompts.list.useQuery(); | ||
|
||
return ( | ||
<div className="mt-2 flex flex-col gap-2"> | ||
<div className="w-full text-xl font-medium sm:w-1/3">Custom Prompts</div> | ||
<p className="mb-1 text-xs italic text-muted-foreground"> | ||
Prompts that you add here will be included as rules to the model during | ||
tag generation. You can view the final prompts in the prompt preview | ||
section. | ||
</p> | ||
{isLoading && <FullPageSpinner />} | ||
{prompts && prompts.length == 0 && ( | ||
<p className="rounded-md bg-muted p-2 text-sm text-muted-foreground"> | ||
You don't have any custom prompts yet. | ||
</p> | ||
)} | ||
{prompts && | ||
prompts.map((prompt) => <PromptRow key={prompt.id} prompt={prompt} />)} | ||
<PromptEditor /> | ||
</div> | ||
); | ||
} | ||
|
||
export function PromptDemo() { | ||
const { data: prompts } = api.prompts.list.useQuery(); | ||
const clientConfig = useClientConfig(); | ||
return ( | ||
<div className="flex flex-col gap-2"> | ||
<div className="mb-4 w-full text-xl font-medium sm:w-1/3"> | ||
Prompt Preview | ||
</div> | ||
<p>Text Prompt</p> | ||
<code className="whitespace-pre-wrap rounded-md bg-muted p-3 text-sm text-muted-foreground"> | ||
{buildTextPrompt( | ||
clientConfig.inference.inferredTagLang, | ||
(prompts ?? []) | ||
.filter((p) => p.appliesTo == "text" || p.appliesTo == "all") | ||
.map((p) => p.text), | ||
"\n<CONTENT_HERE>\n", | ||
).trim()} | ||
</code> | ||
<p>Image Prompt</p> | ||
<code className="whitespace-pre-wrap rounded-md bg-muted p-3 text-sm text-muted-foreground"> | ||
{buildImagePrompt( | ||
clientConfig.inference.inferredTagLang, | ||
(prompts ?? []) | ||
.filter((p) => p.appliesTo == "images" || p.appliesTo == "all") | ||
.map((p) => p.text), | ||
).trim()} | ||
</code> | ||
</div> | ||
); | ||
} | ||
|
||
export default function PromptsPage() { | ||
return ( | ||
<> | ||
<div className="rounded-md border bg-background p-4"> | ||
<div className="mb-2 flex flex-col gap-3"> | ||
<div className="w-full text-2xl font-medium sm:w-1/3"> | ||
Inference Settings | ||
</div> | ||
<CustomPrompts /> | ||
</div> | ||
</div> | ||
<div className="mt-4 rounded-md border bg-background p-4"> | ||
<PromptDemo /> | ||
</div> | ||
</> | ||
); | ||
} |
This file contains 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
Oops, something went wrong.