import { useRef, useState } from "react"; import Button from "@moduletrace/twui/layout/Button"; import Row from "@moduletrace/twui/layout/Row"; import Span from "@moduletrace/twui/layout/Span"; import Stack from "@moduletrace/twui/layout/Stack"; import Textarea from "@moduletrace/twui/form/Textarea"; import { OpenTaskStatuses } from "@/src/dict/task-dict"; import type { TaskDetail, TaskFile } from "@/src/types"; import TaskFileRow from "@/src/components/general/task-file-row"; import HandOverForm from "@/src/components/general/hand-over-form"; import TaskForm from "@/src/components/general/task-form"; import TaskStatusChip from "@/src/components/general/task-status-chip"; import fetchApi from "@moduletrace/twui/utils/fetch/fetchApi"; import type { APIResponseObject } from "@moduletrace/bunext/types"; type Props = { task: TaskDetail; /** * A crew manager can change it, file onto it and delete it. */ can_manage?: boolean; onChanged: (task: TaskDetail, msg?: string) => void; onDeleted: () => void; }; /** * One task opened: what it is, its notes oldest first, its files (each * opening its bytes), a box to add a note, and edit or delete. A crew * manager can file a new deliverable straight onto it, or hand an open * one to another bot, which starts on it. */ export default function TaskDetailPanel({ task, can_manage, onChanged, onDeleted }: Props) { const [editing, setEditing] = useState(false); const [handing_over, setHandingOver] = useState(false); const [note, setNote] = useState(""); const [busy, setBusy] = useState(false); const [status, setStatus] = useState(); const file_input = useRef(null); const run = async (params: { path: string; body: Record; done: string }) => { if (busy) return; setBusy(true); setStatus(undefined); const res = await fetchApi, APIResponseObject>( `/api/admin/${params.path}`, { method: "POST", body: params.body }, ); setBusy(false); if (!res.success || !res.singleRes) { setStatus(res.msg || "That didn't work."); return; } setStatus(params.done); onChanged(res.singleRes, params.done); }; const addNote = async () => { if (!note.trim()) return; await run({ path: "tasks/add-note", body: { task_id: Number(task.id), text: note }, done: "Note added." }); setNote(""); }; const attach = async (file: File) => { if (busy) return; const data_url = await new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => resolve(String(reader.result || "")); reader.onerror = () => reject(reader.error); reader.readAsDataURL(file); }).catch(() => ""); if (!data_url) { setStatus(`${file.name} couldn't be read.`); return; } await run({ path: "tasks/attach-file", body: { task_id: Number(task.id), data_url, name: file.name, kind: "deliverable" }, done: `${file.name} added as a deliverable.`, }); }; const detach = async ({ file }: { file: TaskFile }) => { if (!window.confirm(`Unlink ${file.name || "this file"} from the task?`)) return; await run({ path: "tasks/detach-file", body: { task_id: Number(task.id), task_media_id: Number(file.id) }, done: "File unlinked.", }); }; if (editing) { return ( setEditing(false)} onSaved={(saved, msg) => { setEditing(false); onChanged(saved, msg); }} /> ); } if (handing_over) { return ( what={`"${task.title}"`} targets_path="tasks/hand-over-targets" submit_path="tasks/hand-over-task" body={{ task_id: Number(task.id) }} with_note onCancel={() => setHandingOver(false)} onHandedOver={({ saved, msg }) => { setHandingOver(false); onChanged(saved, msg); }} /> ); } const deliverables = task.files.filter((file) => file.kind != "reference"); const references = task.files.filter((file) => file.kind == "reference"); return ( {task.workspace_id ? task.workspace_name || `Workspace ${task.workspace_id}` : task.bot_name || `Bot ${task.bot_id}`} {can_manage ? ( {(OpenTaskStatuses as readonly string[]).includes(task.status || "") ? ( ) : null} ) : null} {task.status == "blocked" && task.blocked_reason ? ( {`Waiting on you: ${task.blocked_reason}`} ) : null} {task.description ? ( {task.description} ) : null} Files {deliverables.length || references.length ? ( {[...deliverables, ...references].map((file) => ( detach({ file }) : undefined} /> ))} ) : ( Nothing filed yet. )} {can_manage ? ( { const file = event.target.files?.[0]; if (file) attach(file).catch(() => {}); event.target.value = ""; }} /> ) : null} Notes {task.notes.length ? ( task.notes.map((entry) => ( {entry.author == "user" ? "You" : "Bot"} {entry.text} )) ) : ( No notes yet. )}