From 0bdba54ba24a14e7dc2cfab64084452756bccce7 Mon Sep 17 00:00:00 2001
From: Mohamed Bassem <me@mbassem.com>
Date: Mon, 22 Dec 2025 16:59:04 +0200
Subject: feat: Add user settings to disable auto tagging/summarization (#2275)

* feat: Add per-user settings to disable auto-tagging and auto-summarization

This commit adds user-level controls for AI features when they are enabled
on the server. Users can now toggle auto-tagging and auto-summarization
on/off from the AI Settings page.

Changes:
- Added autoTaggingEnabled and autoSummarizationEnabled fields to user table
- Updated user settings schemas and API endpoints to handle new fields
- Modified inference workers to check user preferences before processing
- Added toggle switches to AI Settings page (only visible when server has features enabled)
- Generated database migration for new fields
- Exposed enableAutoTagging and enableAutoSummarization in client config

The settings default to null (use server default). When explicitly set to false,
the user's bookmarks will skip the respective AI processing.

* revert migration

* i18n

---------

Co-authored-by: Claude <noreply@anthropic.com>
---
 apps/web/components/settings/AISettings.tsx | 108 ++++++++++++++++++++++++++++
 1 file changed, 108 insertions(+)

(limited to 'apps/web/components/settings/AISettings.tsx')

diff --git a/apps/web/components/settings/AISettings.tsx b/apps/web/components/settings/AISettings.tsx
index beaa93dc..d8adcb76 100644
--- a/apps/web/components/settings/AISettings.tsx
+++ b/apps/web/components/settings/AISettings.tsx
@@ -4,8 +4,10 @@ import { ActionButton } from "@/components/ui/action-button";
 import {
   Form,
   FormControl,
+  FormDescription,
   FormField,
   FormItem,
+  FormLabel,
   FormMessage,
 } from "@/components/ui/form";
 import { FullPageSpinner } from "@/components/ui/full-page-spinner";
@@ -18,15 +20,18 @@ import {
   SelectTrigger,
   SelectValue,
 } from "@/components/ui/select";
+import { Switch } from "@/components/ui/switch";
 import { toast } from "@/components/ui/use-toast";
 import { useClientConfig } from "@/lib/clientConfig";
 import { useTranslation } from "@/lib/i18n/client";
 import { api } from "@/lib/trpc";
+import { useUserSettings } from "@/lib/userSettings";
 import { zodResolver } from "@hookform/resolvers/zod";
 import { Plus, Save, Trash2 } from "lucide-react";
 import { useForm } from "react-hook-form";
 import { z } from "zod";
 
+import { useUpdateUserSettings } from "@karakeep/shared-react/hooks/users";
 import {
   buildImagePrompt,
   buildSummaryPromptUntruncated,
@@ -37,6 +42,108 @@ import {
   ZPrompt,
   zUpdatePromptSchema,
 } from "@karakeep/shared/types/prompts";
+import { zUpdateUserSettingsSchema } from "@karakeep/shared/types/users";
+
+export function AIPreferences() {
+  const { t } = useTranslation();
+  const clientConfig = useClientConfig();
+  const settings = useUserSettings();
+
+  const { mutate: updateSettings } = useUpdateUserSettings({
+    onSuccess: () => {
+      toast({
+        description: "Settings updated successfully!",
+      });
+    },
+    onError: () => {
+      toast({
+        description: "Failed to update settings",
+        variant: "destructive",
+      });
+    },
+  });
+
+  const form = useForm<z.infer<typeof zUpdateUserSettingsSchema>>({
+    resolver: zodResolver(zUpdateUserSettingsSchema),
+    values: settings
+      ? {
+          autoTaggingEnabled: settings.autoTaggingEnabled,
+          autoSummarizationEnabled: settings.autoSummarizationEnabled,
+        }
+      : undefined,
+  });
+
+  const showAutoTagging = clientConfig.inference.enableAutoTagging;
+  const showAutoSummarization = clientConfig.inference.enableAutoSummarization;
+
+  // Don't show the section if neither feature is enabled on the server
+  if (!showAutoTagging && !showAutoSummarization) {
+    return null;
+  }
+
+  return (
+    <div className="mt-2 flex flex-col gap-2">
+      <p className="mb-1 text-xs italic text-muted-foreground">
+        {t("settings.ai.ai_preferences_description")}
+      </p>
+      <Form {...form}>
+        <form className="space-y-4">
+          {showAutoTagging && (
+            <FormField
+              control={form.control}
+              name="autoTaggingEnabled"
+              render={({ field }) => (
+                <FormItem className="flex flex-row items-center justify-between rounded-lg border p-3">
+                  <div className="space-y-0.5">
+                    <FormLabel>{t("settings.ai.auto_tagging")}</FormLabel>
+                    <FormDescription>
+                      {t("settings.ai.auto_tagging_description")}
+                    </FormDescription>
+                  </div>
+                  <FormControl>
+                    <Switch
+                      checked={field.value ?? true}
+                      onCheckedChange={(checked) => {
+                        field.onChange(checked);
+                        updateSettings({ autoTaggingEnabled: checked });
+                      }}
+                    />
+                  </FormControl>
+                </FormItem>
+              )}
+            />
+          )}
+
+          {showAutoSummarization && (
+            <FormField
+              control={form.control}
+              name="autoSummarizationEnabled"
+              render={({ field }) => (
+                <FormItem className="flex flex-row items-center justify-between rounded-lg border p-3">
+                  <div className="space-y-0.5">
+                    <FormLabel>{t("settings.ai.auto_summarization")}</FormLabel>
+                    <FormDescription>
+                      {t("settings.ai.auto_summarization_description")}
+                    </FormDescription>
+                  </div>
+                  <FormControl>
+                    <Switch
+                      checked={field.value ?? true}
+                      onCheckedChange={(checked) => {
+                        field.onChange(checked);
+                        updateSettings({ autoSummarizationEnabled: checked });
+                      }}
+                    />
+                  </FormControl>
+                </FormItem>
+              )}
+            />
+          )}
+        </form>
+      </Form>
+    </div>
+  );
+}
 
 export function PromptEditor() {
   const { t } = useTranslation();
@@ -353,6 +460,7 @@ export default function AISettings() {
           <div className="w-full text-2xl font-medium sm:w-1/3">
             {t("settings.ai.ai_settings")}
           </div>
+          <AIPreferences />
           <TaggingRules />
         </div>
       </div>
-- 
cgit v1.2.3-70-g09d2

