StepHandler abstraction (app/Support/VideoWorkflow/StepHandlers/) plugs into WorkflowEngine without coupling it to how any given step type is actually executed: - ScriptGenerationHandler: builds a prompt from the project's title/ description (or an explicit outline/tone/keywords/target length in step input), generates via an active OpenAI AiProvider, stores the result as a 'script' asset. - TtsVoiceoverHandler: voices step.input.text, or falls back to the project's latest script asset, via an active ElevenLabs AiProvider; voice_id/model_id are configurable per step. No direct "speed" control — ElevenLabs' base TTS endpoint doesn't expose one. RunWorkflowStepJob resolves a step's handler by type and runs it on the queue, matching the existing PublishPostJob pattern (including a stale-dispatch guard: no-op if the step isn't 'in_progress' anymore). UI, under its own /video-projects routes and nav entry — separate from /projects per the earlier decision: - VideoProjects/Index (#43): list projects with status and step progress, create new ones. - VideoProjects/Show (#44): start a project from a WorkflowTemplate or a hardcoded default 8-step pipeline; per-step Run/Approve/Reject/ Retry/Skip actions; auto-queues the next step after any transition when a handler exists for its type (steps with no handler yet just sit in_progress rather than auto-failing); lists project assets. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -16,6 +16,9 @@
|
||||
<a href="{{ route('tasks.index') }}" class="font-medium hover:text-blue-600">Tasks</a>
|
||||
<a href="{{ route('ideas.index') }}" class="font-medium hover:text-blue-600">Ideas</a>
|
||||
|
||||
<span class="border-l border-gray-200 h-5"></span>
|
||||
<a href="{{ route('video-projects.index') }}" class="font-medium hover:text-blue-600">Video Projects</a>
|
||||
|
||||
@auth
|
||||
<form method="POST" action="{{ route('logout') }}" class="ml-auto">
|
||||
@csrf
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
<div>
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h1 class="text-xl font-semibold">Video Projects</h1>
|
||||
<button wire:click="create" class="bg-blue-600 text-white px-4 py-2 rounded">New Video Project</button>
|
||||
</div>
|
||||
|
||||
<table class="w-full bg-white border border-gray-200 rounded">
|
||||
<thead>
|
||||
<tr class="text-left border-b border-gray-200">
|
||||
<th class="p-3">Title</th>
|
||||
<th class="p-3">Status</th>
|
||||
<th class="p-3">Current Step</th>
|
||||
<th class="p-3">Progress</th>
|
||||
<th class="p-3">Created</th>
|
||||
<th class="p-3"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse ($projects as $project)
|
||||
<tr wire:key="video-project-{{ $project->id }}" class="border-b border-gray-100">
|
||||
<td class="p-3">
|
||||
<a href="{{ route('video-projects.show', $project) }}" wire:navigate class="text-blue-600 hover:underline">{{ $project->title }}</a>
|
||||
</td>
|
||||
<td class="p-3">{{ ucfirst($project->status) }}</td>
|
||||
<td class="p-3">
|
||||
@if ($project->steps->isNotEmpty())
|
||||
{{ str($project->steps->first()->type)->headline() }}
|
||||
<span class="text-xs text-gray-500">({{ str($project->steps->first()->status)->headline() }})</span>
|
||||
@else
|
||||
—
|
||||
@endif
|
||||
</td>
|
||||
<td class="p-3">
|
||||
@if ($project->steps_count > 0)
|
||||
{{ $project->completed_steps_count }} / {{ $project->steps_count }}
|
||||
@else
|
||||
not started
|
||||
@endif
|
||||
</td>
|
||||
<td class="p-3">{{ $project->created_at->format('Y-m-d') }}</td>
|
||||
<td class="p-3 text-right space-x-2">
|
||||
<a href="{{ route('video-projects.show', $project) }}" wire:navigate class="text-blue-600">Open</a>
|
||||
<button wire:click="delete({{ $project->id }})" wire:confirm="Delete this video project?" class="text-red-600">Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="6" class="p-3 text-center text-gray-500">No video projects yet.</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@if ($showForm)
|
||||
<div class="fixed inset-0 bg-black/30 flex items-center justify-center">
|
||||
<div class="bg-white p-6 rounded w-full max-w-md space-y-4">
|
||||
<h2 class="text-lg font-semibold">New Video Project</h2>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium">Title</label>
|
||||
<input type="text" wire:model="title" class="w-full border rounded p-2">
|
||||
@error('title') <span class="text-red-600 text-sm">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium">Description (optional)</label>
|
||||
<textarea wire:model="description" rows="3" class="w-full border rounded p-2"></textarea>
|
||||
@error('description') <span class="text-red-600 text-sm">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-2">
|
||||
<button wire:click="$set('showForm', false)" class="px-4 py-2">Cancel</button>
|
||||
<button wire:click="save" class="bg-blue-600 text-white px-4 py-2 rounded">Create</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@@ -0,0 +1,123 @@
|
||||
<div @if ($hasActiveStep) wire:poll.3s @endif>
|
||||
<div class="mb-4">
|
||||
<a href="{{ route('video-projects.index') }}" wire:navigate class="text-sm text-blue-600 hover:underline">← Video Projects</a>
|
||||
</div>
|
||||
|
||||
<div class="bg-white p-6 rounded border border-gray-200 mb-6">
|
||||
<div class="flex justify-between items-start">
|
||||
<div>
|
||||
<h1 class="text-xl font-semibold">{{ $videoProject->title }}</h1>
|
||||
@if ($videoProject->description)
|
||||
<p class="text-gray-600 mt-1">{{ $videoProject->description }}</p>
|
||||
@endif
|
||||
</div>
|
||||
<span class="text-sm font-medium px-2 py-1 rounded bg-gray-100">{{ ucfirst($videoProject->status) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if ($notice)
|
||||
<div class="mb-4 p-3 bg-amber-50 border border-amber-200 text-amber-800 rounded text-sm">{{ $notice }}</div>
|
||||
@endif
|
||||
|
||||
@if ($steps->isEmpty())
|
||||
<div class="bg-white p-6 rounded border border-gray-200 mb-6 space-y-4">
|
||||
<h2 class="text-lg font-semibold">Start Workflow</h2>
|
||||
|
||||
@if ($templates->isNotEmpty())
|
||||
<div class="flex items-end gap-2">
|
||||
<div class="flex-1">
|
||||
<label class="block text-sm font-medium">From a template</label>
|
||||
<select wire:model="workflowTemplateId" class="w-full border rounded p-2">
|
||||
<option value="">Select template</option>
|
||||
@foreach ($templates as $template)
|
||||
<option value="{{ $template->id }}">{{ $template->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<button wire:click="startWithTemplate" class="bg-blue-600 text-white px-4 py-2 rounded">Start</button>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div>
|
||||
<button wire:click="startDefault" class="bg-gray-800 text-white px-4 py-2 rounded">
|
||||
Start with default pipeline
|
||||
</button>
|
||||
<p class="text-xs text-gray-500 mt-1">Script → Voiceover → Storyboard → Video → Editing → Thumbnail → Metadata → Publish, each requiring approval.</p>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<div class="bg-white rounded border border-gray-200 mb-6 divide-y divide-gray-100">
|
||||
@foreach ($steps as $step)
|
||||
<div wire:key="step-{{ $step->id }}" class="p-4">
|
||||
<div class="flex justify-between items-center">
|
||||
<div>
|
||||
<span class="font-medium">{{ $loop->iteration }}. {{ str($step->type)->headline() }}</span>
|
||||
<span @class([
|
||||
'text-xs px-2 py-0.5 rounded ml-2',
|
||||
'bg-gray-100 text-gray-700' => in_array($step->status, ['pending', 'skipped']),
|
||||
'bg-blue-100 text-blue-700' => $step->status === 'in_progress',
|
||||
'bg-amber-100 text-amber-700' => $step->status === 'awaiting_approval',
|
||||
'bg-green-100 text-green-700' => $step->status === 'completed',
|
||||
'bg-red-100 text-red-700' => in_array($step->status, ['failed', 'rejected']),
|
||||
])>{{ str($step->status)->headline() }}</span>
|
||||
</div>
|
||||
|
||||
<div class="space-x-2">
|
||||
@if ($step->status === 'in_progress')
|
||||
<button wire:click="runStep({{ $step->id }})" class="text-blue-600 text-sm">Run</button>
|
||||
<button wire:click="skipStep({{ $step->id }})" wire:confirm="Skip this step?" class="text-gray-500 text-sm">Skip</button>
|
||||
@elseif ($step->status === 'awaiting_approval')
|
||||
<button wire:click="approveStep({{ $step->id }})" class="text-green-700 text-sm">Approve</button>
|
||||
<button wire:click="rejectStep({{ $step->id }})" class="text-red-600 text-sm">Reject</button>
|
||||
@elseif (in_array($step->status, ['failed', 'rejected']))
|
||||
<button wire:click="retryStep({{ $step->id }})" class="text-blue-600 text-sm">Retry</button>
|
||||
@elseif ($step->status === 'pending')
|
||||
<button wire:click="skipStep({{ $step->id }})" wire:confirm="Skip this step?" class="text-gray-500 text-sm">Skip</button>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if ($step->error)
|
||||
<p class="text-sm text-red-600 mt-1">{{ $step->error }}</p>
|
||||
@endif
|
||||
|
||||
@if ($step->output)
|
||||
<p class="text-xs text-gray-500 mt-1">
|
||||
@if (isset($step->output['word_count']))
|
||||
{{ $step->output['word_count'] }} words generated
|
||||
@elseif (isset($step->output['characters']))
|
||||
{{ $step->output['characters'] }} characters synthesized
|
||||
@endif
|
||||
</p>
|
||||
@endif
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if ($assets->isNotEmpty())
|
||||
<div class="bg-white rounded border border-gray-200">
|
||||
<h2 class="text-lg font-semibold p-4 border-b border-gray-100">Assets</h2>
|
||||
<table class="w-full">
|
||||
<thead>
|
||||
<tr class="text-left border-b border-gray-100 text-sm text-gray-500">
|
||||
<th class="p-3">Type</th>
|
||||
<th class="p-3">Version</th>
|
||||
<th class="p-3">Size</th>
|
||||
<th class="p-3">Created</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach ($assets as $asset)
|
||||
<tr class="border-b border-gray-50">
|
||||
<td class="p-3">{{ ucfirst($asset->type) }}</td>
|
||||
<td class="p-3">v{{ $asset->version }}</td>
|
||||
<td class="p-3">{{ $asset->size_bytes ? number_format($asset->size_bytes / 1024, 1).' KB' : '—' }}</td>
|
||||
<td class="p-3">{{ $asset->created_at->format('Y-m-d H:i') }}</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
Reference in New Issue
Block a user