diff options
| author | MohamedBassem <me@mbassem.com> | 2024-10-05 16:35:47 +0000 |
|---|---|---|
| committer | MohamedBassem <me@mbassem.com> | 2024-10-05 17:15:36 +0000 |
| commit | 3a8d197437868ca10d294f0174afa013f138ec33 (patch) | |
| tree | a78f8957cbb04b3f38727a6d8842f3749528b0ed /apps/web/components | |
| parent | 03e938a903f520ff4c6976d873355a0f44278a9e (diff) | |
| download | karakeep-3a8d197437868ca10d294f0174afa013f138ec33.tar.zst | |
feature(web): Change the settings page to be tabbed
Diffstat (limited to 'apps/web/components')
3 files changed, 330 insertions, 11 deletions
diff --git a/apps/web/components/dashboard/settings/AISettings.tsx b/apps/web/components/dashboard/settings/AISettings.tsx new file mode 100644 index 00000000..12f656ba --- /dev/null +++ b/apps/web/components/dashboard/settings/AISettings.tsx @@ -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 TaggingRules() { + 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">Tagging Rules</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 AISettings() { + 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"> + AI Settings + </div> + <TaggingRules /> + </div> + </div> + <div className="mt-4 rounded-md border bg-background p-4"> + <PromptDemo /> + </div> + </> + ); +} diff --git a/apps/web/components/dashboard/settings/ImportExport.tsx b/apps/web/components/dashboard/settings/ImportExport.tsx index 25b2073c..2bc930f7 100644 --- a/apps/web/components/dashboard/settings/ImportExport.tsx +++ b/apps/web/components/dashboard/settings/ImportExport.tsx @@ -187,7 +187,7 @@ export function ImportExportRow() { return ( <div className="flex flex-col gap-3"> - <div className="flex flex-row gap-2"> + <div className="flex flex-row flex-wrap gap-2"> <FilePickerButton loading={false} accept=".html" @@ -233,15 +233,9 @@ export function ImportExportRow() { export default function ImportExport() { return ( - <div> - <div className="flex items-center justify-between"> - <div className="mb-4 text-lg font-medium"> - Import / Export Bookmarks - </div> - </div> - <div className="mt-2"> - <ImportExportRow /> - </div> + <div className="flex w-full flex-col gap-2"> + <p className="mb-4 text-lg font-medium">Import / Export Bookmarks</p> + <ImportExportRow /> </div> ); } diff --git a/apps/web/components/dashboard/settings/UserDetails.tsx b/apps/web/components/dashboard/settings/UserDetails.tsx index 3915782b..471a6e09 100644 --- a/apps/web/components/dashboard/settings/UserDetails.tsx +++ b/apps/web/components/dashboard/settings/UserDetails.tsx @@ -16,7 +16,7 @@ export default async function UserDetails() { ]; return ( - <div className="mb-8 flex flex-col sm:flex-row"> + <div className="mb-8 flex w-full flex-col sm:flex-row"> <div className="mb-4 w-full text-lg font-medium sm:w-1/3"> Basic Details </div> |
