Refresh list status over Reverb with polling fallback, clarify queued vs processing, streamline upload empty states, and seed an admin login.
226 lines
9.6 KiB
PHP
226 lines
9.6 KiB
PHP
<div
|
|
x-data="transcriptionMonitor(@js([
|
|
'statusUrl' => route('recordings.transcription-status', $recording),
|
|
'initial' => $recording->transcriptionStatusPayload(),
|
|
]))"
|
|
x-init="
|
|
start();
|
|
return () => destroy();
|
|
"
|
|
>
|
|
<div class="mb-6 flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
|
|
<div>
|
|
<flux:link href="{{ route('recordings.index') }}" wire:navigate class="text-sm">← Recordings</flux:link>
|
|
<flux:heading size="xl" class="mt-2">{{ $recording->title }}</flux:heading>
|
|
<div class="mt-2 flex flex-wrap items-center gap-2">
|
|
<x-transcription-status-badge
|
|
:status="$recording->transcription_status"
|
|
:label="$recording->transcriptionStatusLabel()"
|
|
alpine-row="status"
|
|
/>
|
|
<flux:text
|
|
class="text-xs"
|
|
x-show="status.driver_label"
|
|
x-cloak
|
|
x-text="status.driver_label"
|
|
></flux:text>
|
|
</div>
|
|
</div>
|
|
<div class="flex flex-wrap items-center gap-2">
|
|
<flux:button
|
|
type="button"
|
|
variant="primary"
|
|
icon="play"
|
|
@click="$refs.player.paused ? $refs.player.play() : $refs.player.pause()"
|
|
>
|
|
Play
|
|
</flux:button>
|
|
|
|
<flux:modal.trigger name="delete-recording">
|
|
<flux:button type="button" variant="danger">Delete</flux:button>
|
|
</flux:modal.trigger>
|
|
|
|
<flux:modal name="delete-recording" class="max-w-md">
|
|
<div class="space-y-6">
|
|
<div>
|
|
<flux:heading size="lg">Delete recording?</flux:heading>
|
|
<flux:text class="mt-2">
|
|
This permanently removes the recording and its audio file.
|
|
</flux:text>
|
|
</div>
|
|
<div class="flex justify-end gap-2">
|
|
<flux:modal.close>
|
|
<flux:button variant="ghost">Cancel</flux:button>
|
|
</flux:modal.close>
|
|
<flux:button type="button" variant="danger" wire:click="delete">Delete</flux:button>
|
|
</div>
|
|
</div>
|
|
</flux:modal>
|
|
</div>
|
|
</div>
|
|
|
|
<flux:card class="mb-6">
|
|
<flux:heading size="sm" class="uppercase tracking-wide text-zinc-500 dark:text-zinc-400">Audio</flux:heading>
|
|
<audio
|
|
x-ref="player"
|
|
class="mt-4 w-full"
|
|
controls
|
|
preload="metadata"
|
|
src="{{ route('recordings.audio', $recording) }}"
|
|
>
|
|
Your browser does not support audio playback.
|
|
</audio>
|
|
</flux:card>
|
|
|
|
<div class="grid gap-6 lg:grid-cols-2">
|
|
<flux:card>
|
|
<flux:heading size="sm" class="uppercase tracking-wide text-zinc-500 dark:text-zinc-400">Metadata</flux:heading>
|
|
<dl class="mt-4 space-y-3 text-sm">
|
|
<div class="flex justify-between gap-4">
|
|
<dt><flux:text>Original file</flux:text></dt>
|
|
<dd class="text-right font-medium">{{ $recording->original_filename }}</dd>
|
|
</div>
|
|
<div class="flex justify-between gap-4">
|
|
<dt><flux:text>Duration</flux:text></dt>
|
|
<dd class="font-medium">{{ $recording->duration_formatted }}</dd>
|
|
</div>
|
|
<div class="flex justify-between gap-4">
|
|
<dt><flux:text>Artist</flux:text></dt>
|
|
<dd class="font-medium">{{ $recording->artist ?: '—' }}</dd>
|
|
</div>
|
|
<div class="flex justify-between gap-4">
|
|
<dt><flux:text>Album</flux:text></dt>
|
|
<dd class="font-medium">{{ $recording->album ?: '—' }}</dd>
|
|
</div>
|
|
<div class="flex justify-between gap-4">
|
|
<dt><flux:text>Recorded</flux:text></dt>
|
|
<dd class="font-medium">{{ $recording->recorded_at?->format('Y-m-d') ?: '—' }}</dd>
|
|
</div>
|
|
<div class="flex justify-between gap-4">
|
|
<dt><flux:text>Size</flux:text></dt>
|
|
<dd class="font-medium">{{ number_format($recording->file_size_bytes / 1024, 1) }} KB</dd>
|
|
</div>
|
|
<div class="flex justify-between gap-4">
|
|
<dt><flux:text>Uploaded</flux:text></dt>
|
|
<dd class="font-medium">{{ $recording->created_at?->format('Y-m-d H:i') }}</dd>
|
|
</div>
|
|
</dl>
|
|
</flux:card>
|
|
|
|
<flux:card>
|
|
<flux:heading size="sm" class="uppercase tracking-wide text-zinc-500 dark:text-zinc-400">Transcribe</flux:heading>
|
|
<div class="mt-4">
|
|
<flux:button type="button" variant="primary" wire:click="startTranscription">
|
|
<span x-text="startButtonLabel"></span>
|
|
</flux:button>
|
|
</div>
|
|
|
|
<div
|
|
x-show="status.is_active"
|
|
x-cloak
|
|
class="mt-3"
|
|
>
|
|
<flux:button type="button" variant="outline" wire:click="cancelTranscription">
|
|
Stop transcription
|
|
</flux:button>
|
|
</div>
|
|
</flux:card>
|
|
</div>
|
|
|
|
<flux:card class="mt-6">
|
|
<div class="flex items-center justify-between gap-4">
|
|
<flux:heading size="sm" class="uppercase tracking-wide text-zinc-500 dark:text-zinc-400">Transcript</flux:heading>
|
|
<flux:button
|
|
type="button"
|
|
variant="ghost"
|
|
size="sm"
|
|
x-show="!status.is_active && status.has_transcript"
|
|
x-cloak
|
|
@click="navigator.clipboard.writeText(status.transcript || '')"
|
|
>
|
|
Copy
|
|
</flux:button>
|
|
</div>
|
|
|
|
<div x-show="status.is_active" x-cloak class="mt-4">
|
|
<flux:callout variant="warning" icon="arrow-path">
|
|
<flux:callout.heading>
|
|
<span class="inline-flex items-center gap-2">
|
|
<flux:icon.loading
|
|
variant="micro"
|
|
class="size-4"
|
|
x-show="status.status === 'processing'"
|
|
x-cloak
|
|
/>
|
|
<span x-text="status.progress || 'Working…'"></span>
|
|
</span>
|
|
</flux:callout.heading>
|
|
<flux:callout.text>
|
|
<span
|
|
class="tabular-nums"
|
|
x-show="status.status === 'processing' && status.percent != null"
|
|
x-cloak
|
|
>
|
|
<span x-text="status.percent + '%'"></span>
|
|
·
|
|
</span>
|
|
Elapsed <span x-text="status.elapsed_human || '0s'"></span>
|
|
</flux:callout.text>
|
|
</flux:callout>
|
|
|
|
<div class="mt-3" x-show="status.status === 'processing'" x-cloak>
|
|
<flux:progress color="amber" x-bind:value="status.percent || 0" />
|
|
</div>
|
|
|
|
<ul class="mt-3 space-y-1 text-xs text-zinc-600 dark:text-zinc-400">
|
|
<li>
|
|
Engine:
|
|
<span class="font-medium" x-text="status.driver_label || '—'"></span>
|
|
</li>
|
|
<template x-if="status.duration_seconds">
|
|
<li>
|
|
Audio length:
|
|
<span class="font-medium" x-text="formatDuration(status.duration_seconds)"></span>
|
|
<span class="text-zinc-500">(longer files take longer)</span>
|
|
</li>
|
|
</template>
|
|
<li x-show="pollError" class="text-red-600 dark:text-red-400" x-text="pollError"></li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div x-show="!status.is_active && status.status === 'cancelled'" x-cloak class="mt-4">
|
|
<flux:callout icon="stop-circle">
|
|
<flux:callout.text>
|
|
Transcription stopped. Use the button above to start again.
|
|
</flux:callout.text>
|
|
</flux:callout>
|
|
</div>
|
|
|
|
<div x-show="!status.is_active && status.status === 'failed'" x-cloak class="mt-4">
|
|
<flux:callout variant="danger" icon="exclamation-triangle">
|
|
<flux:callout.heading>Transcription failed</flux:callout.heading>
|
|
<flux:callout.text>
|
|
<span x-text="status.error || 'Check the logs and try again.'"></span>
|
|
</flux:callout.text>
|
|
</flux:callout>
|
|
</div>
|
|
|
|
<div x-show="!status.is_active && status.has_transcript" x-cloak>
|
|
<p class="mt-4 whitespace-pre-wrap text-sm leading-relaxed text-zinc-800 dark:text-zinc-100" x-text="status.transcript"></p>
|
|
<flux:text
|
|
class="mt-4 text-xs"
|
|
x-show="status.transcribed_at"
|
|
x-text="status.transcribed_at ? ('Transcribed ' + formatTimestamp(status.transcribed_at)) : ''"
|
|
></flux:text>
|
|
</div>
|
|
|
|
<flux:text
|
|
x-show="!status.is_active && status.status !== 'failed' && status.status !== 'cancelled' && !status.has_transcript"
|
|
x-cloak
|
|
class="mt-4"
|
|
>
|
|
No transcript yet. Transcription starts automatically after upload, or use the button above.
|
|
</flux:text>
|
|
</flux:card>
|
|
</div>
|