aboutsummaryrefslogtreecommitdiffstats
path: root/apps/web/components
diff options
context:
space:
mode:
Diffstat (limited to 'apps/web/components')
-rw-r--r--apps/web/components/dashboard/settings/AISettings.tsx325
-rw-r--r--apps/web/components/dashboard/settings/ImportExport.tsx14
-rw-r--r--apps/web/components/dashboard/settings/UserDetails.tsx2
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&apos;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>