From 610e467039d2cf58a79b80f4f2484f72f7e7d89f Mon Sep 17 00:00:00 2001 From: Chris Busillo Date: Sat, 6 Jun 2026 16:43:18 -0400 Subject: [PATCH] Guide single-folder encode workflow --- .../CompletedWorkstationView.svelte | 2 +- .../workstation/FolderStudioView.svelte | 334 ++++++++++++++++-- .../workstation/folder-studio-view.test.ts | 147 +++++++- .../workstation/folder-studio-view.ts | 226 ++++++++++-- 4 files changed, 637 insertions(+), 72 deletions(-) diff --git a/frontend/src/lib/components/workstation/CompletedWorkstationView.svelte b/frontend/src/lib/components/workstation/CompletedWorkstationView.svelte index 1dac230..545b0aa 100644 --- a/frontend/src/lib/components/workstation/CompletedWorkstationView.svelte +++ b/frontend/src/lib/components/workstation/CompletedWorkstationView.svelte @@ -772,7 +772,7 @@ class="control control--primary armed" disabled={reviewPending} onclick={confirmAlreadyRemoved} - >{reviewPending ? 'Working' : 'Confirm reviewed'}{reviewPending ? 'Working' : 'Mark handled'} + {:else if basicEncodeGuide.action === 'open-series' || basicEncodeGuide.action === 'open-folders'} + {basicEncodeGuide.primary} + {:else if basicEncodeGuide.action === 'open-completed'} + {basicEncodeGuide.primary} + {:else if basicEncodeGuide.action === 'open-ops' || basicEncodeGuide.action.startsWith('monitor-')} + {basicEncodeGuide.primary} + {:else if basicEncodeGuide.action === 'download-review-pack' && !guideActionState.disabled} +
+ +
+ {:else if basicEncodeGuide.action === 'start-sample' || basicEncodeGuide.action === 'retry-sample'} + + {:else if basicEncodeGuide.action === 'queue-encode' || basicEncodeGuide.action === 'approve-size-tradeoff'} + + {:else if basicEncodeGuide.action === 'retry-encode'} + + {:else if basicEncodeGuide.action === 'validate-outputs' || basicEncodeGuide.action === 'promote-outputs'} + + {:else} + + {/if} + +
    + {#each basicEncodeGuide.steps as step, index (step.label)} +
  1. + {index + 1} +
    + {step.label} + {step.detail} +
    +
  2. + {/each} +
+ +
{#if loadError}
@@ -475,6 +600,12 @@ href={resolve(seriesRoute)} data-mf-action={workflow.primaryAction}>{workflow.primary} + {:else if workflow.primaryAction === 'open-completed'} + {workflow.primary} {:else if workflow.primaryAction === 'open-ops' || workflow.primaryAction.startsWith('monitor-')} {workflow.secondary} + {:else if workflow.secondaryAction === 'open-completed'} + {workflow.secondary} {:else if workflow.secondaryAction === 'open-ops'} {workflow.secondary}{approvedSeasonShortcut?.count ?? 1} approved season policy available
Use policy as sample plan {/if} @@ -716,7 +853,7 @@
Area Source - Output / draft + Planned output Why it matters
{#each outputReviewRows as row (row.label)} @@ -741,7 +878,7 @@ {:else}
- Run a sample to generate source-versus-draft review images. + Run a sample to generate source-versus-output review images.
{/each} {#each audioReviewArtifacts as artifact (artifact.imageUrl || artifact.label)} @@ -794,7 +931,7 @@ Request @@ -847,9 +984,11 @@ {#if !workflow.isOutputWorkflow} - +
-
{pendingProposal?.proposal_id ? 'Draft' : 'Source'}
+
{pendingProposal?.proposal_id ? 'Plan' : 'Source'}
{draftReviewRow?.output ?? '—'}
Reason
{draftReviewRow?.detail ?? '—'}
@@ -909,12 +1048,12 @@ Proposed settings details {proposalRows.length ? `${proposalRows.length} draft differences` : 'No draft'}{proposalRows.length ? `${proposalRows.length} plan differences` : 'No plan'}
@@ -923,7 +1062,7 @@ - + @@ -1238,7 +1377,7 @@ border: var(--mf-border); display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); - order: 3; + order: 4; } .workflow-strip__step { @@ -1246,10 +1385,10 @@ border-right: var(--mf-border-muted); border-top: 2px solid var(--mf-line-strong); display: grid; - gap: var(--mf-space-3); + gap: var(--mf-space-2); grid-template-columns: auto minmax(0, 1fr); min-width: 0; - padding: var(--mf-space-4); + padding: var(--mf-space-3); } .workflow-strip__step:last-child { @@ -1263,10 +1402,10 @@ color: var(--mf-fg-tertiary); display: inline-flex; font-family: var(--mf-font-mono), monospace; - font-size: var(--mf-text-xs); - height: 24px; + font-size: var(--mf-text-2xs); + height: 20px; justify-content: center; - width: 24px; + width: 20px; } .workflow-strip__step div { @@ -1276,13 +1415,13 @@ } .workflow-strip__step strong { - font-size: var(--mf-text-sm); + font-size: var(--mf-text-xs); font-weight: var(--mf-weight-semibold); } .workflow-strip__step small { color: var(--mf-fg-tertiary); - font-size: var(--mf-text-xs); + font-size: var(--mf-text-2xs); line-height: var(--mf-leading-snug); overflow-wrap: anywhere; } @@ -1307,6 +1446,132 @@ border-top-color: var(--mf-fail-fg); } + .basic-run { + background: var(--mf-bg-panel); + border: var(--mf-border); + display: grid; + gap: var(--mf-space-3); + order: 3; + padding: var(--mf-space-4); + } + + .basic-run__header { + align-items: center; + display: grid; + gap: var(--mf-space-4); + grid-template-columns: minmax(0, 1fr) auto; + } + + .basic-run__header div { + display: grid; + gap: var(--mf-space-1); + min-width: 0; + } + + .basic-run__header span { + color: var(--mf-fg-tertiary); + font-size: var(--mf-text-2xs); + font-weight: var(--mf-weight-semibold); + letter-spacing: 0; + text-transform: uppercase; + } + + .basic-run__header h2 { + font-size: var(--mf-text-base); + font-weight: var(--mf-weight-semibold); + margin: 0; + } + + .basic-run__header p { + color: var(--mf-fg-muted); + font-size: var(--mf-text-xs); + line-height: var(--mf-leading-snug); + margin: 0; + } + + .basic-run__action, + .basic-run__next { + background: var(--mf-ready-bg); + border: 1px solid var(--mf-ready-line); + color: var(--mf-ready-fg); + display: inline-flex; + font-size: var(--mf-text-xs); + font-weight: var(--mf-weight-semibold); + justify-content: center; + min-width: 140px; + padding: var(--mf-space-2) var(--mf-space-3); + text-decoration: none; + white-space: nowrap; + } + + .basic-run__action { + cursor: pointer; + } + + button.basic-run__action { + font: inherit; + } + + .basic-run__steps { + display: grid; + gap: var(--mf-space-2); + grid-template-columns: repeat(7, minmax(0, 1fr)); + list-style: none; + margin: 0; + padding: 0; + } + + .basic-run__step { + background: var(--mf-bg-strip); + border: var(--mf-border-muted); + border-top: 2px solid var(--mf-line-strong); + display: grid; + gap: var(--mf-space-1); + grid-template-columns: minmax(0, 1fr); + min-width: 0; + padding: var(--mf-space-2); + } + + .basic-run__step > span { + align-items: center; + background: var(--mf-bg-input); + border: var(--mf-border-muted); + color: var(--mf-fg-tertiary); + display: inline-flex; + font-family: var(--mf-font-mono), monospace; + font-size: var(--mf-text-2xs); + height: 18px; + justify-content: center; + width: 18px; + } + + .basic-run__step div { + display: grid; + gap: var(--mf-space-1); + min-width: 0; + } + + .basic-run__step strong { + font-size: var(--mf-text-2xs); + font-weight: var(--mf-weight-semibold); + } + + .basic-run__step small { + color: var(--mf-fg-tertiary); + font-size: var(--mf-text-2xs); + line-height: var(--mf-leading-snug); + overflow-wrap: anywhere; + } + + .basic-run__step--done { + border-top-color: var(--mf-ready-fg); + } + + .basic-run__step--current { + background: var(--mf-active-bg); + border-top-color: var(--mf-active-fg); + } + .decision { --decision-line: var(--mf-idle-line); background: var(--mf-bg-panel); @@ -1315,7 +1580,7 @@ display: grid; gap: var(--mf-space-6); grid-template-columns: minmax(0, 1fr); - order: 1; + order: 5; padding: var(--mf-space-6); } @@ -2032,7 +2297,7 @@ color: var(--mf-fg-tertiary); font-size: var(--mf-text-2xs); font-weight: var(--mf-weight-semibold); - letter-spacing: 0.08em; + letter-spacing: 0; text-transform: uppercase; } @@ -2044,11 +2309,16 @@ } .kv--compact { - grid-template-columns: minmax(74px, auto) minmax(0, 1fr); + grid-template-columns: minmax(64px, auto) minmax(0, 1fr); padding: var(--mf-space-5); row-gap: var(--mf-space-3); } + .kv--compact dd { + min-width: 0; + white-space: normal; + } + .history-list--compact { padding: var(--mf-space-5); } @@ -2091,6 +2361,14 @@ grid-template-columns: minmax(190px, 240px) minmax(0, 1fr); } + .basic-run__steps { + grid-template-columns: repeat(7, minmax(0, 1fr)); + } + + .basic-run__step small { + display: none; + } + .decision__facts { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); @@ -2115,6 +2393,7 @@ .folder-header, .workflow-strip, + .basic-run__header, .decision, .bench, .review-workspace__header, @@ -2123,6 +2402,11 @@ grid-template-columns: 1fr; } + .basic-run__steps { + grid-template-columns: repeat(7, minmax(84px, 1fr)); + overflow-x: auto; + } + .output-review-table__head { display: none; } @@ -2211,7 +2495,7 @@ } .policy-table td:nth-child(4)::before { - content: 'Draft'; + content: 'Plan'; } } diff --git a/frontend/src/lib/components/workstation/folder-studio-view.test.ts b/frontend/src/lib/components/workstation/folder-studio-view.test.ts index e212d5d..1fdf92d 100644 --- a/frontend/src/lib/components/workstation/folder-studio-view.test.ts +++ b/frontend/src/lib/components/workstation/folder-studio-view.test.ts @@ -8,6 +8,7 @@ import type { import type { FolderCalibrationJob } from '$lib/folders/studio'; import { buildBenchHostOptions, + buildBasicEncodeGuide, buildBudgetEnforcementView, buildDecisionFacts, buildFooterSignals, @@ -15,6 +16,7 @@ import { buildProcessingHostOptions, buildReviewWorkspaceView, buildRuntimeFacts, + approvalStatusCopy, buildSampleFacts, buildSeasonScopeRows, buildSampleVerdict, @@ -27,6 +29,7 @@ import { resolveQueueSubmissionMode, resolveWorkflow, resolveWorkflowActionState, + sampleStatusCopy, summarizeOutputWorkflowPending } from './folder-studio-view'; import type { FolderCalibrationState, PendingSampleProposal } from '$lib/folders/studio'; @@ -378,7 +381,7 @@ describe('Folder Studio review request mapping', () => { }) ).toMatchObject({ disabled: true, - title: 'Ask the review assistant for a draft before starting the sample.' + title: 'Ask the review assistant for a sample plan before starting the sample.' }); expect( @@ -413,6 +416,116 @@ describe('Folder Studio review request mapping', () => { }); }); + it('maps sample status into basic user copy', () => { + expect(sampleStatusCopy('queued')).toBe('Sample waiting'); + expect(sampleStatusCopy('running')).toBe('Sampling'); + expect(sampleStatusCopy('pending_review')).toBe('Ready to review'); + expect(sampleStatusCopy('failed')).toBe('Sample needs retry'); + expect(sampleStatusCopy('idle')).toBe('Needs sample'); + }); + + it('maps approval status into basic user copy', () => { + expect(approvalStatusCopy('missing_sample')).toBe('Needs sample'); + expect(approvalStatusCopy('accepted')).toBe('Approved'); + expect(approvalStatusCopy('blocked')).toBe('Blocked'); + expect(approvalStatusCopy('needs_review')).toBe('Needs review'); + expect(approvalStatusCopy('pending_review')).toBe('Ready to review'); + expect(approvalStatusCopy(null)).toBe('Not reviewed'); + }); + + it('builds a single-folder guide around the current next step', () => { + const sampleWorkflow = resolveWorkflow( + folderPayload(), + folderStatusPayload(), + null, + null, + null, + null, + null + ); + expect(buildBasicEncodeGuide(sampleWorkflow)).toMatchObject({ + label: 'Single folder run', + title: 'Run sample', + primary: 'Ask review assistant', + action: 'focus-bench' + }); + expect(buildBasicEncodeGuide(sampleWorkflow).steps.map((step) => step.state)).toEqual([ + 'done', + 'current', + 'upcoming', + 'upcoming', + 'upcoming', + 'upcoming', + 'upcoming' + ]); + + const validateWorkflow = resolveWorkflow( + folderPayload({ workflow_state: workflowState() }), + folderStatusPayload(), + null, + null, + null, + null, + null + ); + expect(buildBasicEncodeGuide(validateWorkflow)).toMatchObject({ + title: 'Validate output', + primary: 'Validate outputs', + action: 'validate-outputs' + }); + expect(buildBasicEncodeGuide(validateWorkflow).steps[4]).toMatchObject({ + label: 'Validate output', + state: 'current' + }); + + const reviewWorkflow = { + tone: 'ready', + label: 'Review ready', + title: 'Approve this sample or revise it', + copy: 'Review the clips, then approve and queue if this is acceptable.', + primary: 'Approve and queue', + primaryAction: 'queue-encode', + secondary: 'Download pack', + secondaryAction: 'download-review-pack' + } as const; + expect(buildBasicEncodeGuide(reviewWorkflow)).toMatchObject({ + title: 'Review sample', + primary: 'Approve and queue', + action: 'queue-encode' + }); + expect(buildBasicEncodeGuide(reviewWorkflow).steps[2]).toMatchObject({ + label: 'Review sample', + state: 'current' + }); + + const completeWorkflow = resolveWorkflow( + folderPayload({ + workflow_state: workflowState({ + state: 'complete', + label: 'Complete', + detail: 'All folder outputs are promoted.', + next_action: { + kind: 'review_scope', + label: 'Review scope', + enabled: true, + target_prefix: 'tv/Example/Season 1' + } + }) + }), + folderStatusPayload(), + null, + null, + null, + null, + null + ); + expect(buildBasicEncodeGuide(completeWorkflow)).toMatchObject({ + title: 'Clean up', + primary: 'Review cleanup', + action: 'open-completed' + }); + }); + it('projects sample output across the whole folder', () => { const folder = folderPayload({ summary: folderSummary({ @@ -541,7 +654,7 @@ describe('Folder Studio review request mapping', () => { tone: 'wait' }), expect.objectContaining({ - label: 'Next sample draft', + label: 'Next sample plan', output: 'AV1 · max 720p', detail: 'VMAF target 89 · floor 87 · downscale allowed by the size request · grain off' }), @@ -838,9 +951,9 @@ describe('Folder Studio review request mapping', () => { workflow ) ).toEqual([ - { label: 'Calibration', value: 'idle' }, + { label: 'Sample', value: 'Needs sample' }, { label: 'Scan', value: 'idle' }, - { label: 'Approval', value: 'missing_sample' }, + { label: 'Approval', value: 'Needs sample' }, { label: 'Processing', value: 'No folder job queued' } ]); expect( @@ -852,7 +965,7 @@ describe('Folder Studio review request mapping', () => { )[1] ).toEqual({ label: 'Sample', - value: 'idle', + value: 'Needs sample', detail: 'polling idle', tone: 'idle' }); @@ -864,9 +977,9 @@ describe('Folder Studio review request mapping', () => { workflow )[0] ).toEqual({ - label: 'Review', - value: 'idle', - tone: 'active' + label: 'Sample', + value: 'Needs sample', + tone: 'idle' }); }); @@ -1392,7 +1505,7 @@ describe('Folder Studio review request mapping', () => { null ) ).toMatchObject({ - label: 'Check draft', + label: 'Check sample plan', primary: 'Download review pack', primaryAction: 'download-review-pack', secondary: 'Revise' @@ -1542,7 +1655,7 @@ describe('Folder Studio review request mapping', () => { null ) ).toMatchObject({ - label: 'Capped draft ready', + label: 'Capped sample plan ready', title: 'Run a sample with a 7% size ceiling', copy: 'Applied after 2.6x target miss against 300 MB per episode.', primary: 'Start sample', @@ -1581,10 +1694,10 @@ describe('Folder Studio review request mapping', () => { null ) ).toMatchObject({ - label: 'Draft blocked', - title: 'The draft does not match your request yet', + label: 'Sample plan blocked', + title: 'The sample plan does not match your request yet', copy: 'The draft lowers VMAF based only on a soft size target.', - primary: 'Revise draft', + primary: 'Revise sample plan', primaryAction: 'revise-proposal' }); }); @@ -1693,7 +1806,7 @@ describe('Folder Studio review request mapping', () => { expect(workflow).toMatchObject({ label: 'Not sampled', - primary: 'Ask for draft', + primary: 'Ask review assistant', primaryAction: 'focus-bench' }); }); @@ -1720,7 +1833,7 @@ describe('Folder Studio review request mapping', () => { ); expect(workflow).toMatchObject({ - label: 'Draft ready', + label: 'Sample plan ready', primary: 'Start sample', primaryAction: 'start-sample' }); @@ -1793,7 +1906,7 @@ describe('Folder Studio review request mapping', () => { ); expect(workflow).toMatchObject({ - label: 'Capped draft ready', + label: 'Capped sample plan ready', title: 'Run a sample with a 7% size ceiling', copy: 'Applied after 2.6x target miss against 300 MB per episode.', primary: 'Start sample', @@ -1903,7 +2016,7 @@ describe('Folder Studio review request mapping', () => { expect(workflow).toMatchObject({ label: 'Not sampled', - primary: 'Ask for draft', + primary: 'Ask review assistant', primaryAction: 'focus-bench' }); }); diff --git a/frontend/src/lib/components/workstation/folder-studio-view.ts b/frontend/src/lib/components/workstation/folder-studio-view.ts index aac02ea..205cef5 100644 --- a/frontend/src/lib/components/workstation/folder-studio-view.ts +++ b/frontend/src/lib/components/workstation/folder-studio-view.ts @@ -74,6 +74,7 @@ export type WorkflowAction = | 'approve-size-tradeoff' | 'download-review-pack' | 'focus-bench' + | 'open-completed' | 'monitor-processing' | 'monitor-review' | 'monitor-sample' @@ -190,6 +191,21 @@ export type WorkflowStep = { current: boolean; }; +export type BasicEncodeGuideStep = { + label: string; + detail: string; + state: 'done' | 'current' | 'upcoming'; +}; + +export type BasicEncodeGuide = { + label: string; + title: string; + detail: string; + primary: string; + action: WorkflowAction; + steps: BasicEncodeGuideStep[]; +}; + function numberValue(value: unknown): number | null { const parsed = Number(value); return Number.isFinite(parsed) ? parsed : null; @@ -438,6 +454,7 @@ export function resolveWorkflowActionState( }; } if (action === 'focus-bench') return { disabled: false, title: '' }; + if (action === 'open-completed') return { disabled: false, title: '' }; if (action === 'approve-size-tradeoff') { return approvalReviewReady ? { disabled: false, title: '' } @@ -461,7 +478,7 @@ export function resolveWorkflowActionState( if (pendingProposal?.proposal_id && pendingProposal.can_queue === false) { return { disabled: true, - title: pendingProposal.message || 'The current draft is not ready to queue.' + title: pendingProposal.message || 'The current sample plan is not ready to queue.' }; } return { disabled: false, title: '' }; @@ -479,13 +496,13 @@ export function resolveWorkflowActionState( if (!pendingProposal?.proposal_id) { return { disabled: true, - title: 'Ask the review assistant for a draft before starting the sample.' + title: 'Ask the review assistant for a sample plan before starting the sample.' }; } if (pendingProposal.can_queue === false) { return { disabled: true, - title: pendingProposal.message || 'The current draft is not ready to queue.' + title: pendingProposal.message || 'The current sample plan is not ready to queue.' }; } return { disabled: false, title: '' }; @@ -548,7 +565,7 @@ export function buildBenchMessages( role: 'operator', label: 'Operator', title: 'No request sent', - body: 'Use the composer to request a representative sample, a revision, or a validation pass.', + body: 'Use the composer to request a sample. Describe your quality or size goal, then click Send request.', tone: 'neutral' }); } @@ -1035,8 +1052,12 @@ function buildOutputScopeFact(folder: FolderPayload): { } function workflowActionDetail(workflow: WorkflowState | undefined): string { - if (!workflow) return 'Draft, sample, review, or approve from here'; - if (workflow.secondaryAction !== 'open-ops' && workflow.secondary !== workflow.primary) { + if (!workflow) return 'Plan, sample, review, or approve from here'; + if ( + workflow.secondaryAction !== 'open-ops' && + workflow.secondaryAction !== 'open-completed' && + workflow.secondary !== workflow.primary + ) { return `${workflow.secondary} also available`; } return workflow.copy; @@ -1209,7 +1230,7 @@ export function buildOutputReviewRows( tone: verdict?.missesTarget ? 'wait' : verdict ? 'ready' : 'idle' }, { - label: pendingProposal?.proposal_id ? 'Next sample draft' : 'Video output', + label: pendingProposal?.proposal_id ? 'Next sample plan' : 'Video output', source: compactParts([ codecLabel(sampleItem?.video_codec), formatResolutionCopy(sampleItem?.width, sampleItem?.height) @@ -1399,6 +1420,31 @@ export function reviewReadyCopy(calibration: FolderCalibrationState | null): str return '—'; } +export function sampleStatusCopy(value: string | null | undefined): string { + const status = String(value ?? '') + .trim() + .toLowerCase(); + if (status === 'queued') return 'Sample waiting'; + if (status === 'running') return 'Sampling'; + if (status === 'pending_review') return 'Ready to review'; + if (status === 'failed' || status === 'stopped') return 'Sample needs retry'; + if (status === 'idle' || !status) return 'Needs sample'; + return status.replaceAll('_', ' '); +} + +export function approvalStatusCopy(value: string | null | undefined): string { + const status = String(value ?? '') + .trim() + .toLowerCase(); + if (!status) return 'Not reviewed'; + if (status === 'missing_sample') return 'Needs sample'; + if (status === 'accepted') return 'Approved'; + if (status === 'blocked') return 'Blocked'; + if (status === 'needs_review') return 'Needs review'; + if (status === 'pending_review') return 'Ready to review'; + return status.replaceAll('_', ' '); +} + function workflowToneToShellTone(tone: FolderWorkflowState['tone']): ShellTone { if (tone === 'active') return 'active'; if (tone === 'ready' || tone === 'success') return 'ready'; @@ -1444,8 +1490,8 @@ function resolveBackendWorkflow( copy: workflow.detail, primary: folder.series_context ? 'Open whole show' : 'Open Folders', primaryAction: folder.series_context ? 'open-series' : 'open-folders', - secondary: 'Open Ops', - secondaryAction: 'open-ops', + secondary: 'Review cleanup', + secondaryAction: 'open-completed', isOutputWorkflow: true }; } @@ -1604,7 +1650,7 @@ export function resolveWorkflow( ) { return { tone: 'fail', - label: 'Retryable', + label: 'Sample needs retry', title: 'Sample run needs recovery', copy: String( calibrationJob?.error ?? @@ -1645,11 +1691,11 @@ export function resolveWorkflow( if (pendingProposal?.self_check?.status && pendingProposal.self_check.status !== 'passed') { return { tone: 'wait', - label: 'Check draft', + label: 'Check sample plan', title: 'Proposal needs review before approval', copy: pendingProposal.self_check.summary ?? - 'The proposal self-check returned a warning. Inspect the draft and revise before approving.', + 'The proposal self-check returned a warning. Inspect the sample plan and revise before approving.', primary: 'Download review pack', primaryAction: 'download-review-pack', secondary: 'Revise', @@ -1661,14 +1707,14 @@ export function resolveWorkflow( if (budgetEnforcement?.active || !calibration?.browser_review_ready) { return { tone: 'ready', - label: budgetEnforcement?.active ? 'Capped draft ready' : 'Draft ready', + label: budgetEnforcement?.active ? 'Capped sample plan ready' : 'Sample plan ready', title: budgetEnforcement?.active ? `Run a sample with a ${budgetEnforcement.cap} size ceiling` - : 'Review draft is ready to sample', + : 'Sample plan is ready to run', copy: budgetEnforcement?.reason ?? pendingProposal.message ?? - 'Review the draft, then queue the representative sample when it looks right.', + 'Review the sample plan, then queue the representative sample when it looks right.', primary: 'Start sample', primaryAction: 'start-sample', secondary: 'Revise', @@ -1683,12 +1729,12 @@ export function resolveWorkflow( ) { return { tone: 'wait', - label: 'Draft blocked', - title: 'The draft does not match your request yet', + label: 'Sample plan blocked', + title: 'The sample plan does not match your request yet', copy: pendingProposal.message ?? - 'The bench draft changed something outside your request. Revise it before starting another sample.', - primary: 'Revise draft', + 'The sample plan changed something outside your request. Revise it before starting another sample.', + primary: 'Revise sample plan', primaryAction: 'revise-proposal', secondary: 'Download pack', secondaryAction: 'download-review-pack' @@ -1729,7 +1775,7 @@ export function resolveWorkflow( copy: verdict === null ? (pendingProposal?.message ?? - 'Evidence is ready. Review the sample clips, then approve the draft or revise it.') + 'Evidence is ready. Review the sample clips, then approve the sample plan or revise it.') : `${verdict.predictedPerItem} per episode, ${verdict.predictedFolderTotal} for the folder. Review the clips, then approve and queue if this is acceptable.`, primary: 'Approve and queue', primaryAction: 'queue-encode', @@ -1755,7 +1801,7 @@ export function resolveWorkflow( label: 'Not sampled', title: 'No representative sample yet', copy: 'Ask the review assistant for a sample proposal before approving folder-wide settings. Worker readiness and settings context stay visible while the sample is queued.', - primary: 'Ask for draft', + primary: 'Ask review assistant', primaryAction: 'focus-bench', secondary: 'Open Ops', secondaryAction: 'open-ops' @@ -1765,8 +1811,8 @@ export function resolveWorkflow( tone: 'wait', label: 'Waiting', title: 'Folder is waiting for review evidence', - copy: 'A representative item exists, but the current review state is incomplete. Refresh status or rerun the sample if the evidence is stale.', - primary: 'Refresh draft', + copy: 'A representative item exists, but the current review state is incomplete. Ask the review assistant for the next sample plan.', + primary: 'Ask review assistant', primaryAction: 'focus-bench', secondary: 'Open Ops', secondaryAction: 'open-ops' @@ -1826,10 +1872,11 @@ export function buildWorkflowSteps(workflow: WorkflowState): WorkflowStep[] { const sampleCurrent = ['focus-bench', 'monitor-sample', 'start-sample', 'retry-sample', 'stop-sample'].includes( activeAction - ) || ['not sampled', 'sampling', 'retryable', 'draft ready'].includes(activeLabel); + ) || + ['not sampled', 'sampling', 'sample needs retry', 'sample plan ready'].includes(activeLabel); const reviewCurrent = ['download-review-pack', 'monitor-review', 'revise-proposal'].includes(activeAction) || - ['review ready', 'check draft'].includes(activeLabel); + ['review ready', 'check sample plan'].includes(activeLabel); const approveCurrent = ['queue-encode', 'approve-size-tradeoff'].includes(activeAction) || activeLabel === 'approved'; const encodeCurrent = @@ -1869,6 +1916,123 @@ export function buildWorkflowSteps(workflow: WorkflowState): WorkflowStep[] { ]; } +const BASIC_ENCODE_STEPS = [ + { + label: 'Pick folder', + detail: 'Choose one folder from Work or Folders.' + }, + { + label: 'Run sample', + detail: 'Create review evidence from one representative item.' + }, + { + label: 'Review sample', + detail: 'Compare quality and size before approving.' + }, + { + label: 'Process folder', + detail: 'Queue the approved settings and let workers run.' + }, + { + label: 'Validate output', + detail: 'Check finished files before publishing.' + }, + { + label: 'Promote', + detail: 'Move validated files into the library.' + }, + { + label: 'Clean up', + detail: 'Delete archived originals from Completed.' + } +] as const; + +function basicEncodeStepIndex(workflow: WorkflowState): number { + const action = workflow.primaryAction; + const label = workflow.label.toLowerCase(); + if (label === 'complete') return 6; + if (action === 'promote-outputs' || label === 'ready to promote') return 5; + if (action === 'validate-outputs' || label === 'ready to validate') return 4; + if ( + workflow.isOutputWorkflow || + ['monitor-processing', 'retry-encode'].includes(action) || + ['approved', 'processing', 'processing failed', 'processing stopped'].includes(label) + ) { + return 3; + } + if ( + ['download-review-pack', 'monitor-review', 'queue-encode', 'approve-size-tradeoff'].includes( + action + ) || + ['review ready', 'review pending', 'target missed', 'check sample plan'].includes(label) + ) { + return 2; + } + if ( + ['start-sample', 'retry-sample', 'monitor-sample', 'stop-sample'].includes(action) || + ['sample plan ready', 'capped sample plan ready', 'sampling', 'sample needs retry'].includes( + label + ) + ) { + return 1; + } + return 1; +} + +function basicEncodeDetail(workflow: WorkflowState, currentStep: number): string { + if (currentStep === 1) { + return 'Start by making one representative sample. Do not approve the full folder until review evidence is ready.'; + } + if (currentStep === 2) { + return 'Look at the review evidence. If it looks good, approve and queue the folder; if not, revise and sample again.'; + } + if (currentStep === 3) { + if (workflow.primaryAction === 'queue-encode') { + return 'The sample is accepted. Queue folder processing when you are ready to run the real work.'; + } + return 'Folder processing is underway or needs attention. Ops shows worker and queue details.'; + } + if (currentStep === 4) return 'Processing finished. Validate the outputs before publishing them.'; + if (currentStep === 5) return 'Outputs are validated. Promote them into the library.'; + if (currentStep === 6) { + return 'This folder is processed. Go to Completed when you are ready to delete archived originals.'; + } + return 'Choose one folder, then follow the highlighted step until the folder is complete.'; +} + +export function buildBasicEncodeGuide(workflow: WorkflowState): BasicEncodeGuide { + const currentStep = basicEncodeStepIndex(workflow); + const primary = + currentStep === 6 && workflow.secondaryAction === 'open-completed' + ? workflow.secondary + : workflow.primary; + const action = + currentStep === 6 && workflow.secondaryAction === 'open-completed' + ? workflow.secondaryAction + : workflow.primaryAction; + return { + label: 'Single folder run', + title: BASIC_ENCODE_STEPS[currentStep]?.label ?? 'Follow the next step', + detail: basicEncodeDetail(workflow, currentStep), + primary, + action, + steps: BASIC_ENCODE_STEPS.map((step, index) => ({ + ...step, + state: index < currentStep ? 'done' : index === currentStep ? 'current' : 'upcoming' + })) + }; +} + +function sampleFooterTone(status: string | null | undefined): FooterSignal['tone'] { + const normalized = String(status ?? '') + .trim() + .toLowerCase(); + if (normalized === 'failed' || normalized === 'stopped') return 'fail'; + if (normalized === 'queued' || normalized === 'running') return 'active'; + if (normalized === 'pending_review') return 'ready'; + return 'idle'; +} + export function buildProposalRows( folder: FolderPayload, pendingProposal: PendingSampleProposal | null @@ -1929,7 +2093,7 @@ export function buildStatusTiles( } : { label: 'Sample', - value: status.calibration_status || 'Unknown', + value: sampleStatusCopy(status.calibration_status), detail: status.polling_active ? 'polling active' : 'polling idle', tone: status.calibration_status === 'failed' @@ -2000,9 +2164,9 @@ export function buildRuntimeFacts( ]; } return [ - { label: 'Calibration', value: status.calibration_status || '—' }, + { label: 'Sample', value: sampleStatusCopy(status.calibration_status) }, { label: 'Scan', value: status.folder_scan_status || '—' }, - { label: 'Approval', value: reviewGate?.status ?? '—' }, + { label: 'Approval', value: approvalStatusCopy(reviewGate?.status) }, { label: 'Processing', value: queueSummary } ]; } @@ -2015,7 +2179,11 @@ export function buildFooterSignals( ): FooterSignal[] { const firstSignal: FooterSignal = workflow.isOutputWorkflow ? { label: 'Pipeline', value: workflow.label.toLowerCase(), tone: workflow.tone } - : { label: 'Review', value: status.calibration_status || 'unknown', tone: 'active' }; + : { + label: 'Sample', + value: sampleStatusCopy(status.calibration_status), + tone: sampleFooterTone(status.calibration_status) + }; return [ firstSignal, { label: 'Metric', value: resolvedMetricCopy(folder), tone: 'ready' },
Area Setting CurrentDraftPlan