Files
m105andClaude Sonnet 5 55fa2ebfda Add script/TTS step handlers, dashboard, and pipeline UI (#35, #36, #43, #44)
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>
2026-08-06 20:56:01 +03:00

79 lines
3.7 KiB
PHP

<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>