diff --git a/surfsense_web/components/hitl-edit-panel/hitl-edit-panel.tsx b/surfsense_web/components/hitl-edit-panel/hitl-edit-panel.tsx index 1d7416837..b68f335b4 100644 --- a/surfsense_web/components/hitl-edit-panel/hitl-edit-panel.tsx +++ b/surfsense_web/components/hitl-edit-panel/hitl-edit-panel.tsx @@ -1,19 +1,22 @@ "use client"; import { TagInput, type Tag as TagType } from "emblor"; +import { format } from "date-fns"; import { useAtomValue, useSetAtom } from "jotai"; -import { XIcon } from "lucide-react"; -import { useCallback, useEffect, useRef, useState } from "react"; +import { CalendarIcon, XIcon } from "lucide-react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { closeHitlEditPanelAtom, hitlEditPanelAtom, } from "@/atoms/chat/hitl-edit-panel.atom"; import type { ExtraField } from "@/atoms/chat/hitl-edit-panel.atom"; +import { Calendar } from "@/components/ui/calendar"; import { PlateEditor } from "@/components/editor/plate-editor"; import { Button } from "@/components/ui/button"; import { Drawer, DrawerContent, DrawerHandle, DrawerTitle } from "@/components/ui/drawer"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { Textarea } from "@/components/ui/textarea"; import { useMediaQuery } from "@/hooks/use-media-query"; @@ -44,14 +47,16 @@ function EmailsTagField({ const [tags, setTags] = useState(() => parseEmailsToTags(value)); const [activeTagIndex, setActiveTagIndex] = useState(null); const isInitialMount = useRef(true); + const onChangeRef = useRef(onChange); + onChangeRef.current = onChange; useEffect(() => { if (isInitialMount.current) { isInitialMount.current = false; return; } - onChange(tagsToEmailString(tags)); - }, [tags, onChange]); + onChangeRef.current(tagsToEmailString(tags)); + }, [tags]); const handleSetTags = useCallback( (newTags: TagType[] | ((prev: TagType[]) => TagType[])) => { @@ -97,6 +102,103 @@ function EmailsTagField({ ); } +function parseDateTimeValue(value: string): { date: Date | undefined; time: string } { + if (!value) return { date: undefined, time: "09:00" }; + try { + const d = new Date(value); + if (Number.isNaN(d.getTime())) return { date: undefined, time: "09:00" }; + return { + date: d, + time: format(d, "HH:mm"), + }; + } catch { + return { date: undefined, time: "09:00" }; + } +} + +function buildLocalDateTimeString(date: Date | undefined, time: string): string { + if (!date) return ""; + const [hours, minutes] = time.split(":").map(Number); + const combined = new Date(date); + combined.setHours(hours ?? 9, minutes ?? 0, 0, 0); + const y = combined.getFullYear(); + const m = String(combined.getMonth() + 1).padStart(2, "0"); + const d = String(combined.getDate()).padStart(2, "0"); + const h = String(combined.getHours()).padStart(2, "0"); + const min = String(combined.getMinutes()).padStart(2, "0"); + return `${y}-${m}-${d}T${h}:${min}:00`; +} + +function DateTimePickerField({ + id, + value, + onChange, +}: { + id: string; + value: string; + onChange: (value: string) => void; +}) { + const parsed = useMemo(() => parseDateTimeValue(value), [value]); + const [selectedDate, setSelectedDate] = useState(parsed.date); + const [time, setTime] = useState(parsed.time); + const [open, setOpen] = useState(false); + + const handleDateSelect = useCallback( + (day: Date | undefined) => { + setSelectedDate(day); + onChange(buildLocalDateTimeString(day, time)); + setOpen(false); + }, + [time, onChange] + ); + + const handleTimeChange = useCallback( + (e: React.ChangeEvent) => { + const newTime = e.target.value; + setTime(newTime); + onChange(buildLocalDateTimeString(selectedDate, newTime)); + }, + [selectedDate, onChange] + ); + + const displayLabel = selectedDate + ? `${format(selectedDate, "MMM d, yyyy")} at ${time}` + : "Pick date & time"; + + return ( +
+ + + + + + + + + +
+ ); +} + export function HitlEditPanelContent({ title: initialTitle, content: initialContent, @@ -173,6 +275,12 @@ export function HitlEditPanelContent({ onChange={(v) => handleExtraFieldChange(field.key, v)} placeholder={`Add ${field.label.toLowerCase()}`} /> + ) : field.type === "datetime-local" ? ( + handleExtraFieldChange(field.key, v)} + /> ) : field.type === "textarea" ? (