| 1 | // Adapted from zai-org/ZCode ConversationQueuePanel.tsx at 872ad960. |
| 2 | // Apache-2.0; see licenses/ZCode.txt. Changed: Reasonix controls, keyboard |
| 3 | // sorting, explicit retry, and edit-in-place rather than withdrawal. |
| 4 | import { useRef, useState, type ReactNode } from "react"; |
| 5 | import { AnchoredPopover } from "./AnchoredPopover"; |
| 6 | import { useSortable } from "@dnd-kit/sortable"; |
| 7 | import { CSS } from "@dnd-kit/utilities"; |
| 8 | import { GripVertical, Pencil, MoreHorizontal, CornerDownRight } from "lucide-react"; |
| 9 | import type { PendingGuidance } from "./ComposerGuidanceShelf"; |
| 10 | import { queueActionAnchor } from "../lib/inboxQueueCommands"; |
| 11 | import { inboxQueueCopy } from "../lib/inboxQueueCopy"; |
| 12 | |
| 13 | export function InboxQueueRow({ item, index, ids, locked, editing, children, copy, onEdit, onMove, onDelete, onRetry, onGuide }: { |
| 14 | item: PendingGuidance; index: number; ids: string[]; locked: boolean; editing: boolean; |
| 15 | copy: ReturnType<typeof inboxQueueCopy>; onEdit: () => void; onMove: (before: string | null) => void; |
| 16 | onDelete: () => void; onRetry: () => void; onGuide?: () => void; |
| 17 | children?: ReactNode; |
| 18 | }) { |
| 19 | const [menu, setMenu] = useState(false); |
| 20 | const menuAnchor = useRef<HTMLButtonElement>(null); |
| 21 | const { attributes, listeners, isDragging, setNodeRef, setActivatorNodeRef, transform, transition } = useSortable({ id: item.id, disabled: locked || editing }); |
| 22 | return <li ref={setNodeRef} className={`inbox-queue__row${editing ? " inbox-queue__row--editing" : ""}`} data-item-id={item.id} |
| 23 | style={{ transform: CSS.Transform.toString(transform ? { ...transform, scaleX: 1, scaleY: 1 } : null), transition, zIndex: isDragging ? 1 : undefined }}> |
| 24 | <button type="button" ref={setActivatorNodeRef} className="inbox-queue__handle" disabled={locked || editing} aria-label={`${copy.drag} ${index + 1}`} {...attributes} {...listeners}><GripVertical size={16} /></button> |
| 25 | {editing ? children : <> |
| 26 | <span className="inbox-queue__ordinal">{index + 1}</span> |
| 27 | <span className="inbox-queue__body" title={item.text}><span>{item.text}</span> |
| 28 | {item.state !== "queued" && <small>{item.state === "uncertain" ? copy.uncertain : item.blockReason || copy.blocked}</small>} |
| 29 | </span> |
| 30 | {onGuide && <button type="button" className="inbox-queue__icon" disabled={locked || item.state !== "queued" || item.paused} onClick={onGuide} aria-label={copy.guide} title={copy.guide}><CornerDownRight size={15} /></button>} |
| 31 | <button type="button" className="inbox-queue__icon" disabled={locked} onClick={onEdit} aria-label={copy.edit} title={copy.edit}><Pencil size={15} /></button> |
| 32 | <div className="inbox-queue__more" onKeyDown={event => { if (event.key === "Escape") { setMenu(false); event.stopPropagation(); } }}> |
| 33 | <button ref={menuAnchor} type="button" className="inbox-queue__icon" disabled={locked} aria-label={copy.more} aria-expanded={menu} onClick={() => setMenu(!menu)}><MoreHorizontal size={16} /></button> |
| 34 | <AnchoredPopover open={menu} anchorRef={menuAnchor} onClose={() => setMenu(false)} className="inbox-queue__menu" align="end"> |
| 35 | {(["first", "up", "down", "last"] as const).map(action => { |
| 36 | const before = queueActionAnchor(ids, item.id, action); |
| 37 | return <button type="button" key={action} disabled={locked || before === undefined} onClick={() => { if (before !== undefined) onMove(before); setMenu(false); }}>{copy[action]}</button>; |
| 38 | })} |
| 39 | {(item.state === "uncertain" || item.state === "blocked") && <button type="button" disabled={locked} onClick={() => { onRetry(); setMenu(false); }}>{copy.retry}</button>} |
| 40 | <button type="button" disabled={locked} onClick={() => { onDelete(); setMenu(false); }}>{copy.remove}</button> |
| 41 | </AnchoredPopover> |
| 42 | </div> |
| 43 | </>} |
| 44 | </li>; |
| 45 | } |
| 46 |