"use client"; import { useEffect, useRef, useState } from "react"; import { useStore, type Task, type ConvertStatus } from "@/lib/store"; import { useTemplates, type TemplateDef } from "@/lib/templates"; import { relativeTime, useMounted } from "@/lib/use-autosave"; import { useT, type DictKey } from "@/lib/i18n"; // Fallback so tasks render before the registry fetch completes. const FALLBACK_TPL = { id: "", zhName: "โ€”", enName: "โ€”", emoji: "๐Ÿ“„", description: "", category: "other", scenario: "marketing", aspectHint: "", tags: [] as string[], } satisfies TemplateDef; const STATUS_DOT: Record = { idle: { color: "var(--ink-faint)", key: "tasks.status.idle", live: false }, running: { color: "var(--coral)", key: "tasks.status.running", live: true }, done: { color: "var(--green)", key: "tasks.status.done", live: false }, error: { color: "var(--red)", key: "tasks.status.error", live: false }, }; export function TasksSidebar() { const tasks = useStore((s) => s.tasks); const activeTaskId = useStore((s) => s.activeTaskId); const collapsed = useStore((s) => s.sidebarCollapsed); const setCollapsed = useStore((s) => s.setSidebarCollapsed); const newTask = useStore((s) => s.newTask); const deleteTask = useStore((s) => s.deleteTask); const setActive = useStore((s) => s.setActiveTask); const renameTask = useStore((s) => s.renameTask); const duplicateTask = useStore((s) => s.duplicateTask); const templates = useTemplates(); const t = useT(); const [query, setQuery] = useState(""); const trimmed = query.trim().toLowerCase(); const filtered = trimmed ? tasks.filter((task) => { if (task.name.toLowerCase().includes(trimmed)) return true; if (task.content.toLowerCase().includes(trimmed)) return true; const tpl = templates?.find((x) => x.id === task.templateId); if (tpl) { if (tpl.zhName.toLowerCase().includes(trimmed)) return true; if (tpl.enName.toLowerCase().includes(trimmed)) return true; } return false; }) : tasks; // tick once per minute so "Xs ago" stays current const [, setTick] = useState(0); useEffect(() => { const id = setInterval(() => setTick((n) => n + 1), 30_000); return () => clearInterval(id); }, []); if (collapsed) { return ( ); } return ( ); } function TaskCard({ task, active, canDelete, onActivate, onRename, onDuplicate, onDelete, }: { task: Task; active: boolean; canDelete: boolean; onActivate: () => void; onRename: (name: string) => void; onDuplicate: () => void; onDelete: () => void; }) { const [editing, setEditing] = useState(false); const [draft, setDraft] = useState(task.name); const inputRef = useRef(null); const templates = useTemplates(); const tpl = templates?.find((tplDef) => tplDef.id === task.templateId) ?? FALLBACK_TPL; const status = STATUS_DOT[task.status]; const mounted = useMounted(); const t = useT(); const locale = useStore((s) => s.locale); useEffect(() => { if (editing) { setDraft(task.name); requestAnimationFrame(() => inputRef.current?.select()); } }, [editing, task.name]); const commit = () => { setEditing(false); if (draft.trim() && draft.trim() !== task.name) onRename(draft); }; const preview = task.content.replace(/\s+/g, " ").trim().slice(0, 64); const sizeKB = task.html ? `${(task.html.length / 1024).toFixed(1)} KB` : null; return (
!editing && onActivate()} className={`group relative mb-1.5 cursor-pointer rounded-xl px-3 py-2.5 transition-colors ${ active ? "" : "hover:bg-[var(--surface)]" }`} style={ active ? { background: "var(--surface)", boxShadow: "0 1px 0 var(--line-faint), 0 8px 18px -16px rgba(21,20,15,0.18)", border: "1px solid var(--line-soft)", } : { border: "1px solid transparent" } } >
{editing ? ( setDraft(e.target.value)} onBlur={commit} onKeyDown={(e) => { if (e.key === "Enter") commit(); if (e.key === "Escape") setEditing(false); }} onClick={(e) => e.stopPropagation()} className="w-full rounded-md bg-[var(--paper)] px-1.5 py-0.5 text-[13px] font-medium text-[var(--ink)] outline-none ring-1 ring-[var(--coral)]" /> ) : (
{ e.stopPropagation(); setEditing(true); }} className="truncate text-[13px] font-semibold text-[var(--ink)]" title={t("tasks.renameDblHint")} > {task.name}
)}
{tpl.emoji} {locale === "en" ? tpl.enName : tpl.zhName} {sizeKB && ( <> ยท {sizeKB} )}
{preview || {t("tasks.emptyContent")}}
{mounted ? relativeTime(task.updatedAt) : ""}
{canDelete && ( )}
); }