"use client"; import { ActionButton } from "@/components/ui/action-button"; import { Badge } from "@/components/ui/badge"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@/components/ui/card"; import { Field, FieldContent, FieldDescription, FieldError, FieldGroup, FieldLabel, FieldTitle, } from "@/components/ui/field"; 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 { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { toast } from "@/components/ui/sonner"; import { Switch } from "@/components/ui/switch"; import { useClientConfig } from "@/lib/clientConfig"; import { useTranslation } from "@/lib/i18n/client"; import { useUserSettings } from "@/lib/userSettings"; import { cn } from "@/lib/utils"; import { zodResolver } from "@hookform/resolvers/zod"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { Info, Plus, Save, Trash2 } from "lucide-react"; import { Controller, useForm } from "react-hook-form"; import { z } from "zod"; import { useUpdateUserSettings } from "@karakeep/shared-react/hooks/users"; import { useTRPC } from "@karakeep/shared-react/trpc"; import { buildImagePrompt, buildSummaryPromptUntruncated, buildTextPromptUntruncated, } from "@karakeep/shared/prompts"; import { zNewPromptSchema, ZPrompt, zUpdatePromptSchema, } from "@karakeep/shared/types/prompts"; import { zUpdateUserSettingsSchema } from "@karakeep/shared/types/users"; function SettingsSection({ title, description, children, }: { title?: string; description?: string; children: React.ReactNode; className?: string; }) { return ( {title && {title}} {description && {description}} {children} ); } export function AIPreferences() { const { t } = useTranslation(); const clientConfig = useClientConfig(); const settings = useUserSettings(); const { mutate: updateSettings, isPending } = useUpdateUserSettings({ onSuccess: () => { toast({ description: "Settings updated successfully!", }); }, onError: () => { toast({ description: "Failed to update settings", variant: "destructive", }); }, }); const form = useForm>({ resolver: zodResolver(zUpdateUserSettingsSchema), values: settings ? { inferredTagLang: settings.inferredTagLang ?? "", autoTaggingEnabled: settings.autoTaggingEnabled, autoSummarizationEnabled: settings.autoSummarizationEnabled, } : undefined, }); const showAutoTagging = clientConfig.inference.enableAutoTagging; const showAutoSummarization = clientConfig.inference.enableAutoSummarization; const onSubmit = (data: z.infer) => { updateSettings(data); }; return (
( {t("settings.ai.inference_language")} {t("settings.ai.inference_language_description")} field.onChange( e.target.value.length > 0 ? e.target.value : null, ) } aria-invalid={fieldState.invalid} placeholder={`Default (${clientConfig.inference.inferredTagLang})`} type="text" /> {fieldState.invalid && ( )} )} /> {showAutoTagging && ( ( {t("settings.ai.auto_tagging")} {t("settings.ai.auto_tagging_description")} {fieldState.invalid && ( )} )} /> )} {showAutoSummarization && ( ( {t("settings.ai.auto_summarization")} {t("settings.ai.auto_summarization_description")} {fieldState.invalid && ( )} )} /> )}
{t("actions.save")}
); } export function TagStyleSelector() { const { t } = useTranslation(); const settings = useUserSettings(); const { mutate: updateSettings, isPending: isUpdating } = useUpdateUserSettings({ onSuccess: () => { toast({ description: "Tag style updated successfully!", }); }, onError: () => { toast({ description: "Failed to update tag style", variant: "destructive", }); }, }); const tagStyleOptions = [ { value: "lowercase-hyphens", label: t("settings.ai.lowercase_hyphens"), examples: ["machine-learning", "web-development"], }, { value: "lowercase-spaces", label: t("settings.ai.lowercase_spaces"), examples: ["machine learning", "web development"], }, { value: "lowercase-underscores", label: t("settings.ai.lowercase_underscores"), examples: ["machine_learning", "web_development"], }, { value: "titlecase-spaces", label: t("settings.ai.titlecase_spaces"), examples: ["Machine Learning", "Web Development"], }, { value: "titlecase-hyphens", label: t("settings.ai.titlecase_hyphens"), examples: ["Machine-Learning", "Web-Development"], }, { value: "camelCase", label: t("settings.ai.camelCase"), examples: ["machineLearning", "webDevelopment"], }, { value: "as-generated", label: t("settings.ai.no_preference"), examples: ["Machine Learning", "web development", "AI_generated"], }, ] as const; const selectedStyle = settings?.tagStyle ?? "as-generated"; return ( { updateSettings({ tagStyle: value as typeof selectedStyle }); }} disabled={isUpdating} className="grid gap-3 sm:grid-cols-2" > {tagStyleOptions.map((option) => ( {option.label}
{option.examples.map((example) => ( {example} ))}
))}
); } export function PromptEditor() { const api = useTRPC(); const { t } = useTranslation(); const queryClient = useQueryClient(); const form = useForm>({ resolver: zodResolver(zNewPromptSchema), defaultValues: { text: "", appliesTo: "all_tagging", }, }); const { mutateAsync: createPrompt, isPending: isCreating } = useMutation( api.prompts.create.mutationOptions({ onSuccess: () => { toast({ description: "Prompt has been created!", }); queryClient.invalidateQueries(api.prompts.list.pathFilter()); }, }), ); return (
{ await createPrompt(value); form.resetField("text"); })} > { return ( ); }} /> { return ( ); }} /> {t("actions.add")} ); } export function PromptRow({ prompt }: { prompt: ZPrompt }) { const api = useTRPC(); const { t } = useTranslation(); const queryClient = useQueryClient(); const { mutateAsync: updatePrompt, isPending: isUpdating } = useMutation( api.prompts.update.mutationOptions({ onSuccess: () => { toast({ description: "Prompt has been updated!", }); queryClient.invalidateQueries(api.prompts.list.pathFilter()); }, }), ); const { mutate: deletePrompt, isPending: isDeleting } = useMutation( api.prompts.delete.mutationOptions({ onSuccess: () => { toast({ description: "Prompt has been deleted!", }); queryClient.invalidateQueries(api.prompts.list.pathFilter()); }, }), ); const form = useForm>({ resolver: zodResolver(zUpdatePromptSchema), defaultValues: { promptId: prompt.id, text: prompt.text, appliesTo: prompt.appliesTo, }, }); return (
{ await updatePrompt(value); })} > { return ( ); }} /> { return ( ); }} /> { return ( ); }} /> {t("actions.save")} deletePrompt({ promptId: prompt.id })} className="items-center" type="button" > {t("actions.delete")} ); } export function TaggingRules() { const api = useTRPC(); const { t } = useTranslation(); const { data: prompts, isLoading } = useQuery( api.prompts.list.queryOptions(), ); return ( {prompts && prompts.length == 0 && (

You don't have any custom prompts yet.

)}
{isLoading && } {prompts && prompts.map((prompt) => ( ))}
); } export function PromptDemo() { const api = useTRPC(); const { t } = useTranslation(); const { data: prompts } = useQuery(api.prompts.list.queryOptions()); const settings = useUserSettings(); const clientConfig = useClientConfig(); const tagStyle = settings?.tagStyle ?? "as-generated"; const inferredTagLang = settings?.inferredTagLang ?? clientConfig.inference.inferredTagLang; return (

{t("settings.ai.text_prompt")}

{buildTextPromptUntruncated( inferredTagLang, (prompts ?? []) .filter( (p) => p.appliesTo == "text" || p.appliesTo == "all_tagging", ) .map((p) => p.text), "\n\n", tagStyle, ).trim()}

{t("settings.ai.images_prompt")}

{buildImagePrompt( inferredTagLang, (prompts ?? []) .filter( (p) => p.appliesTo == "images" || p.appliesTo == "all_tagging", ) .map((p) => p.text), tagStyle, ).trim()}

{t("settings.ai.summarization_prompt")}

{buildSummaryPromptUntruncated( inferredTagLang, (prompts ?? []) .filter((p) => p.appliesTo == "summary") .map((p) => p.text), "\n\n", ).trim()}
); } export default function AISettings() { const { t } = useTranslation(); return (

{t("settings.ai.ai_settings")}

{/* AI Preferences */} {/* Tag Style */} {/* Tagging Rules */} {/* Prompt Preview */}
); }