diff --git a/components/Agents/AgentEditDialog.tsx b/components/Agents/AgentEditDialog.tsx index 703151aaa..d96ac363b 100644 --- a/components/Agents/AgentEditDialog.tsx +++ b/components/Agents/AgentEditDialog.tsx @@ -10,10 +10,11 @@ import { import { Button } from "@/components/ui/button"; import { Pencil } from "lucide-react"; import CreateAgentForm from "./CreateAgentForm"; -import { useMutation, useQueryClient } from "@tanstack/react-query"; -import { useUserProvider } from "@/providers/UserProvder"; import type { AgentTemplateRow } from "@/types/AgentTemplates"; import { useState, useEffect } from "react"; +import { type CreateAgentFormData } from "./schemas"; +import { useAgentForm } from "@/hooks/useAgentForm"; +import { useEditAgentTemplate } from "@/hooks/useEditAgentTemplate"; interface AgentEditDialogProps { agent: AgentTemplateRow; @@ -21,68 +22,45 @@ interface AgentEditDialogProps { const AgentEditDialog: React.FC = ({ agent }) => { const [open, setOpen] = useState(false); - const { userData } = useUserProvider(); - const queryClient = useQueryClient(); - const [currentSharedEmails, setCurrentSharedEmails] = useState(agent.shared_emails || []); - - const editTemplate = useMutation({ - mutationFn: async (values: { - title?: string; - description?: string; - prompt?: string; - tags?: string[]; - isPrivate?: boolean; - shareEmails?: string[]; - }) => { - // Combine existing emails (after removals) with new emails - const finalShareEmails = values.shareEmails && values.shareEmails.length > 0 - ? [...currentSharedEmails, ...values.shareEmails] - : currentSharedEmails; - - const res = await fetch("/api/agent-templates", { - method: "PATCH", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - id: agent.id, - userId: userData?.id, - title: values.title, - description: values.description, - prompt: values.prompt, - tags: values.tags, - isPrivate: values.isPrivate, - shareEmails: finalShareEmails, - }), - }); - if (!res.ok) throw new Error("Failed to update template"); - return res.json(); - }, - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ["agent-templates"] }); - setOpen(false); - }, + const [currentSharedEmails, setCurrentSharedEmails] = useState( + agent.shared_emails || [] + ); + const form = useAgentForm({ + title: agent.title, + description: agent.description, + prompt: agent.prompt, + tags: agent.tags ?? [], + isPrivate: agent.is_private, + shareEmails: [], + }); + const editTemplate = useEditAgentTemplate({ + agent, + currentSharedEmails, + onSuccess: () => setOpen(false), }); - - const onSubmit = (values: { - title: string; - description: string; - prompt: string; - tags: string[]; - isPrivate: boolean; - shareEmails?: string[]; - }) => { - editTemplate.mutate(values); - }; const handleExistingEmailsChange = (emails: string[]) => { setCurrentSharedEmails(emails); }; - // Reset current shared emails when dialog opens or agent changes + // Reset form state when the dialog opens for a specific agent, but do not + // clobber in-progress edits on background refetches. useEffect(() => { if (open) { setCurrentSharedEmails(agent.shared_emails || []); + form.reset({ + title: agent.title, + description: agent.description, + prompt: agent.prompt, + tags: agent.tags ?? [], + isPrivate: agent.is_private, + shareEmails: [], + }); } - }, [open, agent.shared_emails]); + // Reset only when the dialog opens for a different agent identity. + // Depending on all agent fields would wipe in-progress edits on background refetch. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [open, agent.id, form]); return ( @@ -97,16 +75,9 @@ const AgentEditDialog: React.FC = ({ agent }) => { Update the agent template details. editTemplate.mutate(values)} isSubmitting={editTemplate.isPending} - initialValues={{ - title: agent.title, - description: agent.description, - prompt: agent.prompt, - tags: agent.tags ?? [], - isPrivate: agent.is_private, - shareEmails: [], - }} existingSharedEmails={currentSharedEmails} onExistingEmailsChange={handleExistingEmailsChange} submitLabel="Save changes" @@ -117,5 +88,3 @@ const AgentEditDialog: React.FC = ({ agent }) => { }; export default AgentEditDialog; - - diff --git a/components/Agents/AgentVisibilityControl.tsx b/components/Agents/AgentVisibilityControl.tsx new file mode 100644 index 000000000..6872dcdb5 --- /dev/null +++ b/components/Agents/AgentVisibilityControl.tsx @@ -0,0 +1,39 @@ +import { UseFormReturn } from "react-hook-form"; +import { Switch } from "@/components/ui/switch"; +import { CreateAgentFormData } from "./schemas"; + +interface AgentVisibilityControlProps { + form: UseFormReturn; +} + +const AgentVisibilityControl = ({ form }: AgentVisibilityControlProps) => { + const isPrivate = form.watch("isPrivate"); + + return ( +
+ + Public + + + form.setValue("isPrivate", checked, { + shouldDirty: true, + shouldValidate: true, + }) + } + /> + + Private + +
+ ); +}; + +export default AgentVisibilityControl; diff --git a/components/Agents/Agents.tsx b/components/Agents/Agents.tsx index f9f12c672..a6e8be113 100644 --- a/components/Agents/Agents.tsx +++ b/components/Agents/Agents.tsx @@ -6,8 +6,8 @@ import { useAgentData } from "./useAgentData"; import { useAgentToggleFavorite } from "./useAgentToggleFavorite"; import type { Agent } from "./useAgentData"; import CreateAgentButton from "./CreateAgentButton"; -import { Switch } from "@/components/ui/switch"; import AgentsSkeleton from "./AgentsSkeleton"; +import AgentsVisibilityFilter from "./AgentsVisibilityFilter"; const Agents = () => { const { push } = useRouter(); @@ -35,12 +35,10 @@ const Agents = () => { Agents
-
- - {isPrivate ? "Private" : "Public"} - - togglePrivate()} /> -
+
diff --git a/components/Agents/AgentsVisibilityFilter.tsx b/components/Agents/AgentsVisibilityFilter.tsx new file mode 100644 index 000000000..f3905ad46 --- /dev/null +++ b/components/Agents/AgentsVisibilityFilter.tsx @@ -0,0 +1,52 @@ +import { cn } from "@/lib/utils"; + +interface AgentsVisibilityFilterProps { + isPrivate: boolean; + togglePrivate: () => void; +} + +const AgentsVisibilityFilter = ({ + isPrivate, + togglePrivate, +}: AgentsVisibilityFilterProps) => { + return ( +
+ + +
+ ); +}; + +export default AgentsVisibilityFilter; diff --git a/components/Agents/CreateAgentDialog.tsx b/components/Agents/CreateAgentDialog.tsx index bbcac6a10..c406ad264 100644 --- a/components/Agents/CreateAgentDialog.tsx +++ b/components/Agents/CreateAgentDialog.tsx @@ -7,10 +7,11 @@ import { DialogTrigger, } from "@/components/ui/dialog"; import CreateAgentForm from "./CreateAgentForm"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import { type CreateAgentFormData } from "./schemas"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { useUserProvider } from "@/providers/UserProvder"; +import { useAgentForm } from "@/hooks/useAgentForm"; interface CreateAgentDialogProps { children: React.ReactNode; @@ -20,6 +21,7 @@ const CreateAgentDialog = ({ children }: CreateAgentDialogProps) => { const [open, setOpen] = useState(false); const { userData } = useUserProvider(); const queryClient = useQueryClient(); + const form = useAgentForm(); const createTemplate = useMutation({ mutationFn: async (values: CreateAgentFormData) => { @@ -44,6 +46,12 @@ const CreateAgentDialog = ({ children }: CreateAgentDialogProps) => { createTemplate.mutate(values); }; + useEffect(() => { + if (!open) { + form.reset(); + } + }, [form, open]); + return ( {children} @@ -56,7 +64,11 @@ const CreateAgentDialog = ({ children }: CreateAgentDialogProps) => { Create a new intelligent agent to help manage your roster tasks. - + ); diff --git a/components/Agents/CreateAgentForm.tsx b/components/Agents/CreateAgentForm.tsx index 03ceb38b4..8a48f21f5 100644 --- a/components/Agents/CreateAgentForm.tsx +++ b/components/Agents/CreateAgentForm.tsx @@ -1,45 +1,27 @@ -import { useForm } from "react-hook-form"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { useEffect } from "react"; -import { createAgentSchema, type CreateAgentFormData } from "./schemas"; +import { UseFormReturn } from "react-hook-form"; +import { type CreateAgentFormData } from "./schemas"; import FormFields from "./FormFields"; import TagSelector from "./TagSelector"; import PrivacySection from "./PrivacySection"; import SubmitButton from "./SubmitButton"; interface CreateAgentFormProps { + form: UseFormReturn; onSubmit: (values: CreateAgentFormData) => void; isSubmitting?: boolean; - initialValues?: Partial; submitLabel?: string; existingSharedEmails?: string[]; onExistingEmailsChange?: (emails: string[]) => void; } -const CreateAgentForm = ({ onSubmit, isSubmitting, initialValues, submitLabel, existingSharedEmails, onExistingEmailsChange }: CreateAgentFormProps) => { - const form = useForm({ - resolver: zodResolver(createAgentSchema), - defaultValues: { - title: initialValues?.title ?? "", - description: initialValues?.description ?? "", - prompt: initialValues?.prompt ?? "", - tags: initialValues?.tags ?? [], - isPrivate: initialValues?.isPrivate ?? false, - shareEmails: initialValues?.shareEmails ?? [], - }, - }); - - const isPrivate = form.watch("isPrivate"); - - // Ensure shareEmails is initialized when private is toggled - useEffect(() => { - if (!isPrivate) { - form.setValue("shareEmails", []); - } else if (!form.getValues("shareEmails")) { - form.setValue("shareEmails", []); - } - }, [isPrivate, form]); - +const CreateAgentForm = ({ + form, + onSubmit, + isSubmitting, + submitLabel, + existingSharedEmails, + onExistingEmailsChange, +}: CreateAgentFormProps) => { return (
diff --git a/components/Agents/EmailShareInput.tsx b/components/Agents/EmailShareInput.tsx index 90196d4f1..e2e9ca7ac 100644 --- a/components/Agents/EmailShareInput.tsx +++ b/components/Agents/EmailShareInput.tsx @@ -57,6 +57,10 @@ const EmailShareInput = ({ emails, existingSharedEmails = [], onEmailsChange, on return (
+

+ Add email addresses for the people who should be able to view this + private agent. +

; @@ -14,27 +14,25 @@ const PrivacySection = ({ form, existingSharedEmails = [], onExistingEmailsChang const isPrivate = form.watch("isPrivate"); return ( - <> -
- form.setValue("isPrivate", checked)} - /> - +
+
+ +
{isPrivate && ( { - form.setValue("shareEmails", emails, { shouldDirty: true, shouldValidate: true }); - }} - onExistingEmailsChange={onExistingEmailsChange} - /> + emails={form.watch("shareEmails") ?? []} + existingSharedEmails={existingSharedEmails} + onEmailsChange={(emails) => { + form.setValue("shareEmails", emails, { shouldDirty: true, shouldValidate: true }); + }} + onExistingEmailsChange={onExistingEmailsChange} + /> )} - +
); }; diff --git a/components/Agents/TagSelector.tsx b/components/Agents/TagSelector.tsx index 70190e640..18c371899 100644 --- a/components/Agents/TagSelector.tsx +++ b/components/Agents/TagSelector.tsx @@ -10,6 +10,7 @@ interface TagSelectorProps { const TagSelector = ({ form }: TagSelectorProps) => { const { tags } = useAgentData(); + const availableTags = tags.filter((tag) => tag !== "Recommended"); const selectedTags = form.watch("tags") ?? []; const toggleTag = (tag: string) => { @@ -24,7 +25,7 @@ const TagSelector = ({ form }: TagSelectorProps) => {
- {tags.filter((t) => t !== "Recommended").map((tag) => { + {availableTags.map((tag) => { const isSelected = selectedTags.includes(tag); return ( ) { + const form = useForm({ + resolver: zodResolver(createAgentSchema), + defaultValues: { + title: initialValues?.title ?? "", + description: initialValues?.description ?? "", + prompt: initialValues?.prompt ?? "", + tags: initialValues?.tags ?? [], + isPrivate: initialValues?.isPrivate ?? false, + shareEmails: initialValues?.shareEmails ?? [], + }, + }); + + const isPrivate = form.watch("isPrivate"); + + useEffect(() => { + if (!isPrivate) { + form.setValue("shareEmails", []); + } else if (!form.getValues("shareEmails")) { + form.setValue("shareEmails", []); + } + }, [isPrivate, form]); + + return form; +} diff --git a/hooks/useEditAgentTemplate.ts b/hooks/useEditAgentTemplate.ts new file mode 100644 index 000000000..fcc7fcb66 --- /dev/null +++ b/hooks/useEditAgentTemplate.ts @@ -0,0 +1,52 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { useUserProvider } from "@/providers/UserProvder"; +import type { AgentTemplateRow } from "@/types/AgentTemplates"; +import type { CreateAgentFormData } from "@/components/Agents/schemas"; + +interface UseEditAgentTemplateOptions { + agent: AgentTemplateRow; + currentSharedEmails: string[]; + onSuccess: () => void; +} + +export function useEditAgentTemplate({ + agent, + currentSharedEmails, + onSuccess, +}: UseEditAgentTemplateOptions) { + const { userData } = useUserProvider(); + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async (values: CreateAgentFormData) => { + const finalShareEmails = values.isPrivate + ? [...currentSharedEmails, ...(values.shareEmails ?? [])] + : []; + + const res = await fetch("/api/agent-templates", { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + id: agent.id, + userId: userData?.id, + title: values.title, + description: values.description, + prompt: values.prompt, + tags: values.tags, + isPrivate: values.isPrivate, + shareEmails: finalShareEmails, + }), + }); + + if (!res.ok) { + throw new Error("Failed to update template"); + } + + return res.json(); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["agent-templates"] }); + onSuccess(); + }, + }); +}