feat: implement date and time picker in HITL edit panel

- Added a DateTimePickerField component to allow users to select date and time in the HITL edit panel.
- Updated the EmailsTagField to use a ref for onChange to improve performance.
- Modified the ExtraField type to support "datetime-local" for better event management in Google Calendar tools.
This commit is contained in:
Anish Sarkar 2026-03-20 23:07:54 +05:30
parent 4b54826d78
commit 5e23949af6
3 changed files with 114 additions and 8 deletions

View file

@ -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<TagType[]>(() => parseEmailsToTags(value));
const [activeTagIndex, setActiveTagIndex] = useState<number | null>(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<Date | undefined>(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<HTMLInputElement>) => {
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 (
<div className="flex gap-2">
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<button
id={id}
type="button"
className="flex-1 flex items-center gap-2 h-9 rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-xs transition-[color,box-shadow] outline-none focus-visible:border-ring"
>
<CalendarIcon className="size-3.5 text-muted-foreground shrink-0" />
<span className={selectedDate ? "text-foreground" : "text-muted-foreground"}>
{displayLabel}
</span>
</button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar
mode="single"
selected={selectedDate}
onSelect={handleDateSelect}
defaultMonth={selectedDate}
/>
</PopoverContent>
</Popover>
<Input
type="time"
value={time}
onChange={handleTimeChange}
className="w-[120px] text-sm shrink-0 pl-1.5 [&::-webkit-calendar-picker-indicator]:order-first [&::-webkit-calendar-picker-indicator]:mr-1"
/>
</div>
);
}
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" ? (
<DateTimePickerField
id={`extra-field-${field.key}`}
value={extraFieldValues[field.key] ?? ""}
onChange={(v) => handleExtraFieldChange(field.key, v)}
/>
) : field.type === "textarea" ? (
<Textarea
id={`extra-field-${field.key}`}