Spaces:
Running
Running
import React, { useEffect } from "react"; | |
import { useForm } from "react-hook-form"; | |
import { zodResolver } from "@hookform/resolvers/zod"; | |
import { z } from "zod"; | |
import { useMutation } from "@tanstack/react-query"; | |
import { apiRequest, queryClient } from "@/lib/queryClient"; | |
import { useAuth } from "@/hooks/use-auth"; | |
import { useToast } from "@/hooks/use-toast"; | |
import { updateUserProfileSchema } from "@shared/schema"; | |
import { | |
Dialog, | |
DialogContent, | |
DialogDescription, | |
DialogHeader, | |
DialogTitle, | |
DialogFooter, | |
} from "@/components/ui/dialog"; | |
import { | |
Form, | |
FormControl, | |
FormDescription, | |
FormField, | |
FormItem, | |
FormLabel, | |
FormMessage, | |
} from "@/components/ui/form"; | |
import { Input } from "@/components/ui/input"; | |
import { Textarea } from "@/components/ui/textarea"; | |
import { Button } from "@/components/ui/button"; | |
import { Loader2 } from "lucide-react"; | |
// Create a form schema | |
const profileFormSchema = z.object({ | |
fullName: z.string().optional(), | |
location: z.string().optional(), | |
interests: z.array(z.string()).optional(), | |
interestsInput: z.string().optional(), // For input field value only, not submitted | |
profession: z.string().optional(), | |
pets: z.string().optional(), | |
systemContext: z.string().optional(), | |
}); | |
type ProfileFormValues = z.infer<typeof profileFormSchema>; | |
interface UserSettingsModalProps { | |
isOpen: boolean; | |
onClose: () => void; | |
} | |
export default function UserSettingsModal({ | |
isOpen, | |
onClose, | |
}: UserSettingsModalProps) { | |
const { user } = useAuth(); | |
const { toast } = useToast(); | |
// Create form with default values | |
const form = useForm<ProfileFormValues>({ | |
resolver: zodResolver(profileFormSchema), | |
defaultValues: { | |
fullName: "", | |
location: "", | |
interests: [], | |
interestsInput: "", | |
profession: "", | |
pets: "", | |
systemContext: "", | |
}, | |
}); | |
// Update form when user data changes | |
useEffect(() => { | |
if (user) { | |
// Convert interests array to comma-separated string for display | |
const interestsString = user.interests?.join(", ") || ""; | |
form.reset({ | |
fullName: user.fullName || "", | |
location: user.location || "", | |
interests: user.interests || [], | |
interestsInput: interestsString, | |
profession: user.profession || "", | |
pets: user.pets || "", | |
systemContext: user.systemContext || "", | |
}); | |
} | |
}, [user, form]); | |
const updateProfileMutation = useMutation({ | |
mutationFn: async (data: ProfileFormValues) => { | |
const res = await apiRequest("PATCH", "/api/user/profile", data); | |
if (!res.ok) { | |
const errorData = await res.json(); | |
throw new Error(errorData.message || "Failed to update profile"); | |
} | |
return await res.json(); | |
}, | |
onSuccess: (updatedUser) => { | |
queryClient.setQueryData(["/api/user"], updatedUser); | |
toast({ | |
title: "Profile updated", | |
description: "Your profile has been updated successfully.", | |
}); | |
onClose(); | |
}, | |
onError: (error: Error) => { | |
toast({ | |
title: "Update failed", | |
description: error.message, | |
variant: "destructive", | |
}); | |
}, | |
}); | |
const onSubmit = async (data: ProfileFormValues) => { | |
// Create a copy of the data object without interestsInput | |
const { interestsInput, ...submitData } = data; | |
// Submit data without the temporary interestsInput field | |
await updateProfileMutation.mutateAsync(submitData); | |
}; | |
// Convert string to array for interests field if needed | |
const handleInterestsChange = (e: React.ChangeEvent<HTMLInputElement>) => { | |
const value = e.target.value; | |
// Just store the input value as is, don't process it yet | |
form.setValue("interestsInput", value); | |
// Process for the actual interests field that gets submitted | |
const interestsArray = value | |
.split(",") | |
.map((item) => item.trim()) | |
.filter((item) => item !== ""); | |
form.setValue("interests", interestsArray); | |
}; | |
return ( | |
<Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}> | |
<DialogContent className="sm:max-w-[525px] max-h-[90vh] overflow-y-auto"> | |
<DialogHeader> | |
<DialogTitle>User Settings</DialogTitle> | |
<DialogDescription> | |
Update your profile information and AI assistant preferences | |
</DialogDescription> | |
</DialogHeader> | |
<Form {...form}> | |
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6"> | |
<div className="space-y-4"> | |
{/* Profile Information Section */} | |
<div className="border-b pb-2"> | |
<h3 className="text-lg font-medium">Profile Information</h3> | |
</div> | |
<FormField | |
control={form.control} | |
name="fullName" | |
render={({ field }) => ( | |
<FormItem> | |
<FormLabel>Full Name</FormLabel> | |
<FormControl> | |
<Input placeholder="Your full name" {...field} value={field.value || ""} /> | |
</FormControl> | |
<FormMessage /> | |
</FormItem> | |
)} | |
/> | |
<FormField | |
control={form.control} | |
name="location" | |
render={({ field }) => ( | |
<FormItem> | |
<FormLabel>Location</FormLabel> | |
<FormControl> | |
<Input placeholder="Your location" {...field} value={field.value || ""} /> | |
</FormControl> | |
<FormMessage /> | |
</FormItem> | |
)} | |
/> | |
<FormField | |
control={form.control} | |
name="profession" | |
render={({ field }) => ( | |
<FormItem> | |
<FormLabel>Profession</FormLabel> | |
<FormControl> | |
<Input placeholder="Your profession" {...field} value={field.value || ""} /> | |
</FormControl> | |
<FormMessage /> | |
</FormItem> | |
)} | |
/> | |
<FormField | |
control={form.control} | |
name="interestsInput" | |
render={({ field }) => ( | |
<FormItem> | |
<FormLabel>Interests</FormLabel> | |
<FormControl> | |
<Input | |
placeholder="Interests (comma-separated)" | |
{...field} | |
onChange={handleInterestsChange} | |
/> | |
</FormControl> | |
<FormDescription> | |
Enter your interests separated by commas | |
</FormDescription> | |
<FormMessage /> | |
</FormItem> | |
)} | |
/> | |
<FormField | |
control={form.control} | |
name="pets" | |
render={({ field }) => ( | |
<FormItem> | |
<FormLabel>Pets</FormLabel> | |
<FormControl> | |
<Input placeholder="Your pets" {...field} value={field.value || ""} /> | |
</FormControl> | |
<FormMessage /> | |
</FormItem> | |
)} | |
/> | |
{/* AI Assistant Preferences Section */} | |
<div className="border-b pb-2 pt-4"> | |
<h3 className="text-lg font-medium">AI Assistant Preferences</h3> | |
</div> | |
<FormField | |
control={form.control} | |
name="additionalInfo" | |
render={({ field }) => ( | |
<FormItem> | |
<FormLabel>Additional Information</FormLabel> | |
<FormControl> | |
<Textarea | |
placeholder="Add any additional information about yourself that you'd like the AI to know" | |
className="min-h-[100px]" | |
{...field} | |
value={field.value || ""} | |
/> | |
</FormControl> | |
<FormDescription> | |
This information will be included in your AI context | |
</FormDescription> | |
<FormMessage /> | |
</FormItem> | |
)} | |
/> | |
<div className="space-y-4"> | |
<div className="flex justify-between items-center"> | |
<FormLabel className="text-base">System Context</FormLabel> | |
<Button | |
type="button" | |
variant="outline" | |
size="sm" | |
onClick={() => { | |
// Generate structured context from profile fields | |
const fullName = form.getValues("fullName"); | |
const location = form.getValues("location"); | |
const interests = form.getValues("interests"); | |
const profession = form.getValues("profession"); | |
const pets = form.getValues("pets"); | |
const additionalInfo = form.getValues("additionalInfo"); | |
// Format profile information in a structured way | |
let profileInfo = ""; | |
if (fullName) profileInfo += `name: ${fullName}\n`; | |
if (location) profileInfo += `location: ${location}\n`; | |
if (interests && interests.length > 0) profileInfo += `interests: ${interests.join(", ")}\n`; | |
if (profession) profileInfo += `profession: ${profession}\n`; | |
if (pets) profileInfo += `pets: ${pets}\n`; | |
if (additionalInfo) profileInfo += `additional_info: ${additionalInfo}\n`; | |
// Get existing context | |
const currentContext = form.getValues("systemContext") || ""; | |
// Set the new structured context | |
form.setValue("systemContext", profileInfo + "\n" + currentContext); | |
}} | |
> | |
Include Profile Info | |
</Button> | |
</div> | |
<FormField | |
control={form.control} | |
name="systemContext" | |
render={({ field }) => ( | |
<FormItem> | |
<FormControl> | |
<Textarea | |
placeholder="Add custom context for the AI assistant to understand your requirements better" | |
className="min-h-[150px]" | |
{...field} | |
value={field.value || ""} | |
/> | |
</FormControl> | |
<FormDescription> | |
This context will be provided to the AI assistant for all your conversations. | |
Use key-value pairs like "name: Your Name" for best results. | |
</FormDescription> | |
<FormMessage /> | |
</FormItem> | |
)} | |
/> | |
</div> | |
</div> | |
<DialogFooter> | |
<Button | |
type="button" | |
variant="outline" | |
onClick={onClose} | |
disabled={updateProfileMutation.isPending} | |
> | |
Cancel | |
</Button> | |
<Button | |
type="submit" | |
disabled={updateProfileMutation.isPending} | |
> | |
{updateProfileMutation.isPending ? ( | |
<> | |
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> | |
Saving... | |
</> | |
) : ( | |
"Save Changes" | |
)} | |
</Button> | |
</DialogFooter> | |
</form> | |
</Form> | |
</DialogContent> | |
</Dialog> | |
); | |
} |