Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 34 additions & 65 deletions components/Agents/AgentEditDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,79 +10,57 @@ 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;
}

const AgentEditDialog: React.FC<AgentEditDialogProps> = ({ agent }) => {
const [open, setOpen] = useState(false);
const { userData } = useUserProvider();
const queryClient = useQueryClient();
const [currentSharedEmails, setCurrentSharedEmails] = useState<string[]>(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<string[]>(
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,
Comment on lines 48 to +52
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Limit edit-form reset to initial dialog open

The new reset effect now depends on the entire agent object, so while the dialog is open any background query refresh that replaces agent (for example React Query refetch on window focus) will call form.reset(...) and discard unsaved edits. This is a user-facing data-loss regression in AgentEditDialog because typing into the form can be silently wiped mid-edit; the reset should be keyed to opening the dialog (or a stable identity change like agent.id) rather than every object refresh.

Useful? React with 👍 / 👎.

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 (
<Dialog open={open} onOpenChange={setOpen}>
Expand All @@ -97,16 +75,9 @@ const AgentEditDialog: React.FC<AgentEditDialogProps> = ({ agent }) => {
<DialogDescription>Update the agent template details.</DialogDescription>
</DialogHeader>
<CreateAgentForm
onSubmit={onSubmit}
form={form}
onSubmit={(values: CreateAgentFormData) => 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"
Expand All @@ -117,5 +88,3 @@ const AgentEditDialog: React.FC<AgentEditDialogProps> = ({ agent }) => {
};

export default AgentEditDialog;


39 changes: 39 additions & 0 deletions components/Agents/AgentVisibilityControl.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { UseFormReturn } from "react-hook-form";
import { Switch } from "@/components/ui/switch";
import { CreateAgentFormData } from "./schemas";

interface AgentVisibilityControlProps {
form: UseFormReturn<CreateAgentFormData>;
}

const AgentVisibilityControl = ({ form }: AgentVisibilityControlProps) => {
const isPrivate = form.watch("isPrivate");

return (
<div className="flex flex-wrap items-center gap-3">
<span
className={`text-sm ${!isPrivate ? "text-foreground" : "text-muted-foreground"}`}
>
Public
</span>
<Switch
id="isPrivate"
checked={isPrivate}
Comment on lines +19 to +21
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Add an accessible name to the visibility switch

This switch is rendered without an associated <label>/aria-label, so assistive technologies will announce an unlabeled control and users cannot tell it changes agent visibility. The previous implementation had an explicit label, so this introduces an accessibility regression for screen-reader users in the create/edit form.

Useful? React with 👍 / 👎.

aria-label={isPrivate ? "Private agent" : "Public agent"}
onCheckedChange={(checked) =>
form.setValue("isPrivate", checked, {
shouldDirty: true,
shouldValidate: true,
})
}
/>
<span
className={`text-sm ${isPrivate ? "text-foreground" : "text-muted-foreground"}`}
>
Private
</span>
</div>
);
};

export default AgentVisibilityControl;
12 changes: 5 additions & 7 deletions components/Agents/Agents.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -35,12 +35,10 @@ const Agents = () => {
Agents
</h1>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground dark:text-muted-foreground">
{isPrivate ? "Private" : "Public"}
</span>
<Switch checked={isPrivate} onCheckedChange={() => togglePrivate()} />
</div>
<AgentsVisibilityFilter
isPrivate={isPrivate}
togglePrivate={togglePrivate}
/>
<CreateAgentButton />
</div>
</div>
Expand Down
52 changes: 52 additions & 0 deletions components/Agents/AgentsVisibilityFilter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { cn } from "@/lib/utils";

interface AgentsVisibilityFilterProps {
isPrivate: boolean;
togglePrivate: () => void;
}

const AgentsVisibilityFilter = ({
isPrivate,
togglePrivate,
}: AgentsVisibilityFilterProps) => {
return (
<div
role="group"
aria-label="Agent visibility filter"
className="inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground"
>
<button
type="button"
aria-pressed={!isPrivate}
onClick={() => {
if (isPrivate) togglePrivate();
}}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium transition-all cursor-pointer",
!isPrivate
? "bg-background text-foreground shadow"
: "text-muted-foreground hover:text-foreground"
)}
>
Public
</button>
<button
type="button"
aria-pressed={isPrivate}
onClick={() => {
if (!isPrivate) togglePrivate();
}}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium transition-all cursor-pointer",
isPrivate
? "bg-background text-foreground shadow"
: "text-muted-foreground hover:text-foreground"
)}
>
Private
</button>
</div>
);
};

export default AgentsVisibilityFilter;
16 changes: 14 additions & 2 deletions components/Agents/CreateAgentDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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) => {
Expand All @@ -44,6 +46,12 @@ const CreateAgentDialog = ({ children }: CreateAgentDialogProps) => {
createTemplate.mutate(values);
};

useEffect(() => {
if (!open) {
form.reset();
}
}, [form, open]);

return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>{children}</DialogTrigger>
Expand All @@ -56,7 +64,11 @@ const CreateAgentDialog = ({ children }: CreateAgentDialogProps) => {
Create a new intelligent agent to help manage your roster tasks.
</DialogDescription>
</DialogHeader>
<CreateAgentForm onSubmit={onSubmit} isSubmitting={createTemplate.isPending} />
<CreateAgentForm
form={form}
onSubmit={onSubmit}
isSubmitting={createTemplate.isPending}
/>
</DialogContent>
</Dialog>
);
Expand Down
40 changes: 11 additions & 29 deletions components/Agents/CreateAgentForm.tsx
Original file line number Diff line number Diff line change
@@ -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<CreateAgentFormData>;
onSubmit: (values: CreateAgentFormData) => void;
isSubmitting?: boolean;
initialValues?: Partial<CreateAgentFormData>;
submitLabel?: string;
existingSharedEmails?: string[];
onExistingEmailsChange?: (emails: string[]) => void;
}

const CreateAgentForm = ({ onSubmit, isSubmitting, initialValues, submitLabel, existingSharedEmails, onExistingEmailsChange }: CreateAgentFormProps) => {
const form = useForm<CreateAgentFormData>({
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 (
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormFields form={form} />
Expand Down
4 changes: 4 additions & 0 deletions components/Agents/EmailShareInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ const EmailShareInput = ({ emails, existingSharedEmails = [], onEmailsChange, on
return (
<div className="space-y-2">
<Label htmlFor="share-emails">Share with (email addresses)</Label>
<p className="text-sm text-muted-foreground">
Add email addresses for the people who should be able to view this
private agent.
</p>
<Input
id="share-emails"
type="email"
Expand Down
Loading
Loading