"use client"; import { useEffect, useMemo, useRef, useState } from "react"; import Link from "next/link"; import { type AutoQueueGroup, type AutoQueueLeaf, type AutoQueueNode, type AutoQueueOrder, isGroup, } from "yt-dlp-transcript-common/jobs/autoQueuePolicy"; import type { AutoQueueKind } from "yt-dlp-transcript-common/jobs/autoQueueState"; import { saveAutoQueueAction, type SaveResult } from "../actions"; import type { AutoQueueKindStatus } from "yt-dlp-transcript-common/views/autoQueueStatus"; import { ClaimLadder } from "./ClaimLadder"; import type { RungOps } from "./LadderRung"; import { LaneOrder } from "./LaneOrder"; import { SaveBar } from "./SaveBar"; import { type Channel, LANE_ORDERS, NUM_CLASS, leafOrder, orderTradeoff, } from "./dispatch"; // Owns one runner's editable policy: the tree, the switches, and the save. The // DRAWING of the tree moved to ClaimLadder/LadderRung, which also render the // live counts — so what used to be a form sitting above a separate counts list // is now one object. // // THE TREE GOES READ-ONLY WHEN IT IS GENERATED, and only the tree. // `status.policyCompiled` is true whenever `settings.channelPriority` says // anything: the lane then dispatches from a tree compiled off that document per // tick, so a rule typed in here could not change what the lane does — the // compiler wins (controller/autoRunner.ts, S1). Rather than let the ladder // offer edits that are silently overruled, it renders as the statement of // dispatch order it is, with one line saying where the order is set. // // WHAT STAYS EDITABLE IS EVERYTHING THAT IS NOT THE TREE: the enable switch, the // worker cap, the order, and the auto-captions opt-in. None of them is derivable // from a channel priority, none of them is touched by the compiler (every writer // SPREADS the policy), and moving them to /channels would put lane settings on a // channels page. The Save button therefore still means something here; it just // writes back the same root it was given. // // It is NOT deleted for a compiled lane, either: the bucket and operation axes a // hand-authored leaf can express have no equivalent in the priority model, and a // corpus with no priorities set — which is every corpus until one is — edits its // trees here exactly as before. let idSeq = 0; function newId(): string { idSeq += 1; const rnd = typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID().slice(0, 8) : `${idSeq}`; return `n-${rnd}-${idSeq}`; } // --- Immutable tree ops (by node id) --------------------------------------- function mapNode( node: AutoQueueNode, id: string, fn: (n: AutoQueueNode) => AutoQueueNode, ): AutoQueueNode { if (node.id === id) return fn(node); if (!isGroup(node)) return node; return { ...node, children: node.children.map((c) => mapNode(c, id, fn)) }; } function removeFrom(node: AutoQueueGroup, id: string): AutoQueueGroup { return { ...node, children: node.children .filter((c) => c.id !== id) .map((c) => (isGroup(c) ? removeFrom(c, id) : c)), }; } function addChildTo( node: AutoQueueNode, parentId: string, child: AutoQueueNode, ): AutoQueueNode { if (!isGroup(node)) return node; if (node.id === parentId) { return { ...node, children: [...node.children, child] }; } return { ...node, children: node.children.map((c) => addChildTo(c, parentId, child)), }; } function moveChildIn( node: AutoQueueNode, parentId: string, index: number, dir: -1 | 1, ): AutoQueueNode { if (!isGroup(node)) return node; if (node.id === parentId) { const next = [...node.children]; const j = index + dir; if (j < 0 || j >= next.length) return node; [next[index], next[j]] = [next[j], next[index]]; return { ...node, children: next }; } return { ...node, children: node.children.map((c) => moveChildIn(c, parentId, index, dir)), }; } function moveChildToTop( node: AutoQueueNode, parentId: string, index: number, ): AutoQueueNode { if (!isGroup(node)) return node; if (node.id === parentId) { if (index <= 0 || index >= node.children.length) return node; const next = [...node.children]; const [item] = next.splice(index, 1); next.unshift(item); return { ...node, children: next }; } return { ...node, children: node.children.map((c) => moveChildToTop(c, parentId, index)), }; } function makeLeaf(type: "channel" | "platform" | "all"): AutoQueueLeaf { return { id: newId(), match: { type }, weight: 1, maxWorkers: null }; } function makeGroup(): AutoQueueGroup { return { id: newId(), mode: "strict", weight: 1, maxWorkers: null, children: [], }; } // --- Form state -------------------------------------------------------------- type Form = { enabled: boolean; maxWorkers: number | null; replaceAutoSubs: boolean; order: AutoQueueOrder; root: AutoQueueGroup; }; function formOf(status: AutoQueueKindStatus): Form { return { enabled: status.policy.enabled, maxWorkers: status.policy.maxWorkers, replaceAutoSubs: status.policy.replaceAutoSubs === true, order: status.policy.order ?? "listed", root: status.policy.root, }; } export function PolicyTreeEditor({ kind, status, channels, platforms, buckets, operations, }: { kind: AutoQueueKind; status: AutoQueueKindStatus; channels: Channel[]; platforms: string[]; buckets: string[]; operations: string[]; }) { // The lane's tree is compiled from settings.channelPriority, so the ladder is // a reading of dispatch rather than a control on it. const compiled = status.policyCompiled; const [form, setForm] = useState