"use client"; import { useState } from "react"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { CheckSquare, Plus, Trash2 } from "lucide-react"; import { api } from "@/lib/client/api"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Spinner, Empty } from "@/components/ui/primitives"; import { Select, dateInput } from "./ui"; import { cn, fmtDate } from "@/lib/utils"; export const taskKey = (pageId: string) => ["research-tasks", pageId]; export function TasksTab({ pageId }: { pageId: string }) { const qc = useQueryClient(); const [title, setTitle] = useState(""); const [due, setDue] = useState(""); const { data: tasks = [], isLoading } = useQuery({ queryKey: taskKey(pageId), queryFn: () => api(`/api/research/tasks?pageId=${pageId}`) }); const refresh = () => { qc.invalidateQueries({ queryKey: taskKey(pageId) }); qc.invalidateQueries({ queryKey: ["research-overview"] }); }; async function add() { const t = title.trim(); if (!t) return; await api("/api/research/tasks", { method: "POST", json: { pageId, title: t, dueAt: due || null } }); setTitle(""); setDue(""); refresh(); } async function patch(id: string, json: any) { await api(`/api/research/tasks/${id}`, { method: "PATCH", json }); refresh(); } async function del(id: string) { await api(`/api/research/tasks/${id}`, { method: "DELETE" }); refresh(); } const open = tasks.filter((t) => t.status !== "done"); const done = tasks.filter((t) => t.status === "done"); const Row = ({ t }: { t: any }) => { const overdue = t.status !== "done" && t.dueAt && new Date(t.dueAt) < new Date(); return (
patch(t.id, { status: e.target.checked ? "done" : "todo" })} className="size-4 accent-[var(--accent)]" aria-label="Done" /> { if (e.target.value.trim() && e.target.value !== t.title) patch(t.id, { title: e.target.value.trim() }); }} /> patch(t.id, { dueAt: e.target.value || null })} className={cn("h-7 w-[8.5rem] rounded-md border border-line bg-bg-elev px-1.5 text-[11px]", overdue && "border-danger/50 text-danger")} aria-label="Due date" />
); }; return (
setTitle(e.target.value)} placeholder="New task (Enter to add)" className="h-8 min-w-[12rem] flex-1 text-[13px]" onKeyDown={(e) => { if (e.key === "Enter") add(); }} /> setDue(e.target.value)} className="h-8 rounded-md border border-line bg-bg-elev px-2 text-[12px]" aria-label="Due" />
{isLoading ?
: !tasks.length ? } title="No tasks" hint="Keep the to-do list of this paper here; the AI planner can propose tasks you accept into this list." /> : (
{open.map((t) => )}
{done.length > 0 &&
{done.length} completed
{done.map((t) => )}
}
{open.length} open · {open.filter((t) => t.dueAt && new Date(t.dueAt) < new Date()).length} overdue{open[0]?.dueAt ? ` · next due ${fmtDate(open.find((t) => t.dueAt)?.dueAt)}` : ""}
)}
); }