Add Fortify auth, Flux dark mode, demo seed, and Docker hot reload.
Protect recordings per user, seed a demo login on container start, and bind-mount the app with Vite HMR for local Compose development.
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
@section('content')
|
||||
<div class="mb-8">
|
||||
<h1 class="text-2xl font-semibold tracking-tight">Upload recordings</h1>
|
||||
<p class="mt-1 text-sm text-stone-600">
|
||||
<p class="mt-1 text-sm text-stone-600 dark:text-zinc-400">
|
||||
Drop one or many pocket-recorder files. Embedded metadata is extracted when available.
|
||||
Duplicate files (same name and size, or identical content) are skipped.
|
||||
</p>
|
||||
@@ -17,12 +17,12 @@
|
||||
enctype="multipart/form-data"
|
||||
x-data="uploadDropzone(@js(['existingFingerprints' => $existingFingerprints ?? []]))"
|
||||
@submit="ensureFilesSelected($event)"
|
||||
class="max-w-2xl space-y-6 rounded border border-stone-200 bg-white p-6 shadow-sm"
|
||||
class="max-w-2xl space-y-6 rounded border border-stone-200 bg-white p-6 shadow-sm dark:border-zinc-700 dark:bg-zinc-800"
|
||||
>
|
||||
@csrf
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-stone-700">Audio files</label>
|
||||
<label class="block text-sm font-medium text-stone-700 dark:text-zinc-200">Audio files</label>
|
||||
|
||||
<div
|
||||
@dragenter.prevent="dragging = true"
|
||||
@@ -30,12 +30,12 @@
|
||||
@dragleave.prevent="dragging = false"
|
||||
@drop.prevent="onDrop($event)"
|
||||
@click="$refs.fileInput.click()"
|
||||
:class="dragging ? 'border-teal-600 bg-teal-50' : 'border-stone-300 bg-stone-50 hover:border-teal-500 hover:bg-teal-50/40'"
|
||||
:class="dragging ? 'border-teal-600 bg-teal-50 dark:bg-teal-950/40' : 'border-stone-300 bg-stone-50 hover:border-teal-500 hover:bg-teal-50/40 dark:border-zinc-600 dark:bg-zinc-900/50 dark:hover:border-teal-500 dark:hover:bg-teal-950/30'"
|
||||
class="mt-2 flex cursor-pointer flex-col items-center justify-center rounded border-2 border-dashed px-6 py-12 text-center transition-colors"
|
||||
>
|
||||
<p class="text-sm font-medium text-stone-800">Drop audio files here</p>
|
||||
<p class="mt-1 text-sm text-stone-600">or click to browse</p>
|
||||
<p class="mt-3 text-xs text-stone-500">MP3, WAV, OGG, FLAC, M4A, AAC, WebM, WMA, or AIFF · max 2 GB each · up to 50 files</p>
|
||||
<p class="text-sm font-medium text-stone-800 dark:text-zinc-100">Drop audio files here</p>
|
||||
<p class="mt-1 text-sm text-stone-600 dark:text-zinc-400">or click to browse</p>
|
||||
<p class="mt-3 text-xs text-stone-500 dark:text-zinc-500">MP3, WAV, OGG, FLAC, M4A, AAC, WebM, WMA, or AIFF · max 2 GB each · up to 50 files</p>
|
||||
</div>
|
||||
|
||||
<input
|
||||
@@ -52,26 +52,26 @@
|
||||
|
||||
<div x-show="files.length > 0" x-cloak class="space-y-2">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<p class="text-sm font-medium text-stone-700">
|
||||
<p class="text-sm font-medium text-stone-700 dark:text-zinc-200">
|
||||
<span x-text="files.length"></span>
|
||||
<span x-text="files.length === 1 ? 'file selected' : 'files selected'"></span>
|
||||
</p>
|
||||
<button type="button" @click="clearFiles()" class="text-sm text-stone-600 hover:underline">
|
||||
<button type="button" @click="clearFiles()" class="text-sm text-stone-600 dark:text-zinc-400 hover:underline dark:text-zinc-400">
|
||||
Clear all
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<ul class="divide-y divide-stone-100 rounded border border-stone-200">
|
||||
<ul class="divide-y divide-stone-100 rounded border border-stone-200 dark:divide-zinc-700 dark:border-zinc-700">
|
||||
<template x-for="(file, index) in files" :key="fileListKey(file, index)">
|
||||
<li class="flex items-center justify-between gap-3 px-3 py-2 text-sm">
|
||||
<div class="min-w-0">
|
||||
<p class="truncate font-medium text-stone-800" x-text="file.name"></p>
|
||||
<p class="text-xs text-stone-500" x-text="formatSize(file.size)"></p>
|
||||
<p class="truncate font-medium text-stone-800 dark:text-zinc-100" x-text="file.name"></p>
|
||||
<p class="text-xs text-stone-500 dark:text-zinc-400" x-text="formatSize(file.size)"></p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
@click="removeFile(index)"
|
||||
class="shrink-0 text-stone-500 hover:text-red-700"
|
||||
class="shrink-0 text-stone-500 hover:text-red-700 dark:text-zinc-400 dark:hover:text-red-400"
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
@@ -81,19 +81,19 @@
|
||||
</div>
|
||||
|
||||
<div x-show="files.length === 1" x-cloak>
|
||||
<label for="title" class="block text-sm font-medium text-stone-700">Title (optional)</label>
|
||||
<label for="title" class="block text-sm font-medium text-stone-700 dark:text-zinc-200">Title (optional)</label>
|
||||
<input
|
||||
id="title"
|
||||
type="text"
|
||||
name="title"
|
||||
value="{{ old('title') }}"
|
||||
placeholder="Leave blank to use embedded title or filename"
|
||||
class="mt-2 w-full rounded border border-stone-300 px-3 py-2 text-sm shadow-sm focus:border-teal-600 focus:outline-none focus:ring-1 focus:ring-teal-600"
|
||||
class="mt-2 w-full rounded border border-stone-300 bg-white px-3 py-2 text-sm shadow-sm focus:border-teal-600 focus:outline-none focus:ring-1 focus:ring-teal-600 dark:border-zinc-600 dark:bg-zinc-900 dark:text-zinc-100"
|
||||
>
|
||||
</div>
|
||||
|
||||
<p x-show="notice" x-cloak class="text-sm text-amber-800" x-text="notice"></p>
|
||||
<p x-show="error" x-cloak class="text-sm text-red-700" x-text="error"></p>
|
||||
<p x-show="notice" x-cloak class="text-sm text-amber-800 dark:text-amber-300" x-text="notice"></p>
|
||||
<p x-show="error" x-cloak class="text-sm text-red-700 dark:text-red-300" x-text="error"></p>
|
||||
|
||||
<div class="flex items-center gap-3">
|
||||
<button
|
||||
@@ -103,7 +103,7 @@
|
||||
>
|
||||
<span x-text="uploadLabel"></span>
|
||||
</button>
|
||||
<a href="{{ route('recordings.index') }}" class="text-sm text-stone-600 hover:underline">Cancel</a>
|
||||
<a href="{{ route('recordings.index') }}" class="text-sm text-stone-600 dark:text-zinc-400 hover:underline dark:text-zinc-400">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
@endsection
|
||||
|
||||
@@ -17,12 +17,12 @@
|
||||
? number_format($recording->word_count)
|
||||
: '—',
|
||||
'badge_class' => match ($recording->transcription_status) {
|
||||
'done' => 'bg-teal-50 text-teal-800 ring-teal-600/20',
|
||||
'processing' => 'bg-amber-50 text-amber-800 ring-amber-600/20',
|
||||
'pending' => 'bg-amber-50 text-amber-800 ring-amber-600/20',
|
||||
'failed' => 'bg-red-50 text-red-800 ring-red-600/20',
|
||||
'cancelled' => 'bg-stone-100 text-stone-700 ring-stone-500/20',
|
||||
default => 'bg-stone-100 text-stone-700 ring-stone-500/20',
|
||||
'done' => 'bg-teal-50 text-teal-800 ring-teal-600/20 dark:bg-teal-950 dark:text-teal-200 dark:ring-teal-400/30',
|
||||
'processing' => 'bg-amber-50 text-amber-800 ring-amber-600/20 dark:bg-amber-950 dark:text-amber-200 dark:ring-amber-400/30',
|
||||
'pending' => 'bg-amber-50 text-amber-800 ring-amber-600/20 dark:bg-amber-950 dark:text-amber-200 dark:ring-amber-400/30',
|
||||
'failed' => 'bg-red-50 text-red-800 ring-red-600/20 dark:bg-red-950 dark:text-red-200 dark:ring-red-400/30',
|
||||
'cancelled' => 'bg-stone-100 text-stone-700 ring-stone-500/20 dark:bg-zinc-800 dark:text-zinc-300 dark:ring-zinc-500/30',
|
||||
default => 'bg-stone-100 text-stone-700 ring-stone-500/20 dark:bg-zinc-800 dark:text-zinc-300 dark:ring-zinc-500/30',
|
||||
},
|
||||
];
|
||||
})->values();
|
||||
@@ -41,7 +41,7 @@
|
||||
<div class="mb-8 flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold tracking-tight">Recordings</h1>
|
||||
<p class="mt-1 text-sm text-stone-600">Manage pocket-recorder audio and transcripts.</p>
|
||||
<p class="mt-1 text-sm text-stone-600 dark:text-zinc-400">Manage pocket-recorder audio and transcripts.</p>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2 sm:items-end">
|
||||
<form method="GET" action="{{ route('recordings.index') }}" class="flex gap-2">
|
||||
@@ -50,9 +50,9 @@
|
||||
name="q"
|
||||
value="{{ $search ?? '' }}"
|
||||
placeholder="Search title, artist, transcript…"
|
||||
class="w-full min-w-[16rem] rounded border border-stone-300 bg-white px-3 py-2 text-sm shadow-sm focus:border-teal-600 focus:outline-none focus:ring-1 focus:ring-teal-600"
|
||||
class="w-full min-w-[16rem] rounded border border-stone-300 bg-white px-3 py-2 text-sm shadow-sm focus:border-teal-600 focus:outline-none focus:ring-1 focus:ring-teal-600 dark:border-zinc-600 dark:bg-zinc-800 dark:text-zinc-100 dark:placeholder:text-zinc-500"
|
||||
>
|
||||
<button type="submit" class="rounded border border-stone-300 bg-white px-3 py-2 text-sm hover:bg-stone-50">
|
||||
<button type="submit" class="rounded border border-stone-300 bg-white px-3 py-2 text-sm hover:bg-stone-50 dark:border-zinc-600 dark:bg-zinc-800 dark:hover:bg-zinc-700">
|
||||
Search
|
||||
</button>
|
||||
</form>
|
||||
@@ -63,7 +63,7 @@
|
||||
x-cloak
|
||||
>
|
||||
@csrf
|
||||
<button type="submit" class="text-sm font-medium text-teal-700 hover:underline">
|
||||
<button type="submit" class="text-sm font-medium text-teal-700 hover:underline dark:text-teal-300">
|
||||
Queue <span x-text="pendingCount"></span> pending
|
||||
<span x-text="pendingCount === 1 ? 'transcription' : 'transcriptions'"></span>
|
||||
</button>
|
||||
@@ -72,16 +72,16 @@
|
||||
</div>
|
||||
|
||||
@if ($recordings->isEmpty())
|
||||
<div class="rounded border border-dashed border-stone-300 bg-white px-6 py-16 text-center">
|
||||
<p class="text-stone-600">No recordings yet.</p>
|
||||
<a href="{{ route('recordings.create') }}" class="mt-4 inline-block text-sm font-medium text-teal-700 hover:underline">
|
||||
<div class="rounded border border-dashed border-stone-300 bg-white px-6 py-16 text-center dark:border-zinc-600 dark:bg-zinc-800">
|
||||
<p class="text-stone-600 dark:text-zinc-400">No recordings yet.</p>
|
||||
<a href="{{ route('recordings.create') }}" class="mt-4 inline-block text-sm font-medium text-teal-700 hover:underline dark:text-teal-300">
|
||||
Upload your first MP3
|
||||
</a>
|
||||
</div>
|
||||
@else
|
||||
<div class="overflow-hidden rounded border border-stone-200 bg-white shadow-sm">
|
||||
<table class="min-w-full divide-y divide-stone-200 text-sm">
|
||||
<thead class="bg-stone-50 text-left text-xs font-medium uppercase tracking-wide text-stone-500">
|
||||
<div class="overflow-hidden rounded border border-stone-200 bg-white shadow-sm dark:border-zinc-700 dark:bg-zinc-800">
|
||||
<table class="min-w-full divide-y divide-stone-200 text-sm dark:divide-zinc-700">
|
||||
<thead class="bg-stone-50 text-left text-xs font-medium uppercase tracking-wide text-stone-500 dark:bg-zinc-900/50 dark:text-zinc-400">
|
||||
<tr>
|
||||
<th class="px-4 py-3">Title</th>
|
||||
<th class="px-4 py-3">Duration</th>
|
||||
@@ -90,25 +90,25 @@
|
||||
<th class="px-4 py-3">Uploaded</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-stone-100">
|
||||
<tbody class="divide-y divide-stone-100 dark:divide-zinc-700">
|
||||
@foreach ($recordings as $recording)
|
||||
<tr class="hover:bg-stone-50">
|
||||
<tr class="hover:bg-stone-50 dark:hover:bg-zinc-700/50">
|
||||
<td class="px-4 py-3">
|
||||
<a href="{{ route('recordings.show', $recording) }}" class="font-medium text-teal-800 hover:underline">
|
||||
<a href="{{ route('recordings.show', $recording) }}" class="font-medium text-teal-800 hover:underline dark:text-teal-300">
|
||||
{{ $recording->title }}
|
||||
</a>
|
||||
@if ($recording->artist)
|
||||
<div class="text-xs text-stone-500">{{ $recording->artist }}</div>
|
||||
<div class="text-xs text-stone-500 dark:text-zinc-400">{{ $recording->artist }}</div>
|
||||
@endif
|
||||
@if ($snippet = $recording->transcriptSnippet($search ?: null))
|
||||
<p class="mt-1 max-w-xl text-xs leading-relaxed text-stone-500">
|
||||
<p class="mt-1 max-w-xl text-xs leading-relaxed text-stone-500 dark:text-zinc-400">
|
||||
{{ $snippet }}
|
||||
</p>
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-4 py-3 text-stone-600">{{ $recording->duration_formatted }}</td>
|
||||
<td class="px-4 py-3 text-stone-600 dark:text-zinc-400">{{ $recording->duration_formatted }}</td>
|
||||
<td
|
||||
class="px-4 py-3 tabular-nums text-stone-600"
|
||||
class="px-4 py-3 tabular-nums text-stone-600 dark:text-zinc-400"
|
||||
x-text="row({{ $recording->id }}).word_count_display"
|
||||
>
|
||||
{{ $recording->word_count > 0 ? number_format($recording->word_count) : '—' }}
|
||||
@@ -122,7 +122,7 @@
|
||||
{{ $recording->transcriptionStatusLabel() }}
|
||||
</span>
|
||||
<div
|
||||
class="mt-1 max-w-[14rem] truncate text-xs text-amber-700"
|
||||
class="mt-1 max-w-[14rem] truncate text-xs text-amber-700 dark:text-amber-300"
|
||||
x-show="row({{ $recording->id }}).is_active && row({{ $recording->id }}).progress"
|
||||
x-cloak
|
||||
:title="row({{ $recording->id }}).progress"
|
||||
@@ -131,7 +131,7 @@
|
||||
<span x-text="row({{ $recording->id }}).progress"></span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-stone-600">{{ $recording->created_at?->format('Y-m-d H:i') }}</td>
|
||||
<td class="px-4 py-3 text-stone-600 dark:text-zinc-400">{{ $recording->created_at?->format('Y-m-d H:i') }}</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
|
||||
@@ -8,12 +8,12 @@
|
||||
default => (string) $status,
|
||||
};
|
||||
$classes = match ($status) {
|
||||
'done' => 'bg-teal-50 text-teal-800 ring-teal-600/20',
|
||||
'processing' => 'bg-amber-50 text-amber-800 ring-amber-600/20',
|
||||
'pending' => 'bg-amber-50 text-amber-800 ring-amber-600/20',
|
||||
'failed' => 'bg-red-50 text-red-800 ring-red-600/20',
|
||||
'cancelled' => 'bg-stone-100 text-stone-700 ring-stone-500/20',
|
||||
default => 'bg-stone-100 text-stone-700 ring-stone-500/20',
|
||||
'done' => 'bg-teal-50 text-teal-800 ring-teal-600/20 dark:bg-teal-950 dark:text-teal-200 dark:ring-teal-400/30',
|
||||
'processing' => 'bg-amber-50 text-amber-800 ring-amber-600/20 dark:bg-amber-950 dark:text-amber-200 dark:ring-amber-400/30',
|
||||
'pending' => 'bg-amber-50 text-amber-800 ring-amber-600/20 dark:bg-amber-950 dark:text-amber-200 dark:ring-amber-400/30',
|
||||
'failed' => 'bg-red-50 text-red-800 ring-red-600/20 dark:bg-red-950 dark:text-red-200 dark:ring-red-400/30',
|
||||
'cancelled' => 'bg-stone-100 text-stone-700 ring-stone-500/20 dark:bg-zinc-800 dark:text-zinc-300 dark:ring-zinc-500/30',
|
||||
default => 'bg-stone-100 text-stone-700 ring-stone-500/20 dark:bg-zinc-800 dark:text-zinc-300 dark:ring-zinc-500/30',
|
||||
};
|
||||
@endphp
|
||||
<span class="inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ring-1 ring-inset {{ $classes }}">
|
||||
|
||||
@@ -15,70 +15,70 @@
|
||||
>
|
||||
<div class="mb-6 flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div>
|
||||
<a href="{{ route('recordings.index') }}" class="text-sm text-stone-500 hover:text-stone-800">← Recordings</a>
|
||||
<a href="{{ route('recordings.index') }}" class="text-sm text-stone-500 hover:text-stone-800 dark:text-zinc-400 dark:hover:text-zinc-200">← Recordings</a>
|
||||
<h1 class="mt-2 text-2xl font-semibold tracking-tight">{{ $recording->title }}</h1>
|
||||
<div class="mt-2 flex flex-wrap items-center gap-2 text-sm text-stone-600">
|
||||
<div class="mt-2 flex flex-wrap items-center gap-2 text-sm text-stone-600 dark:text-zinc-400">
|
||||
<span
|
||||
class="inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ring-1 ring-inset"
|
||||
:class="badgeClass"
|
||||
x-text="status.status_label || status.status"
|
||||
></span>
|
||||
<template x-if="status.driver_label">
|
||||
<span class="text-xs text-stone-500" x-text="status.driver_label"></span>
|
||||
<span class="text-xs text-stone-500 dark:text-zinc-400" x-text="status.driver_label"></span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<form method="POST" action="{{ route('recordings.destroy', $recording) }}" onsubmit="return confirm('Delete this recording and its file?')">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" class="rounded border border-red-200 bg-white px-3 py-1.5 text-sm text-red-700 hover:bg-red-50">
|
||||
<button type="submit" class="rounded border border-red-200 bg-white px-3 py-1.5 text-sm text-red-700 hover:bg-red-50 dark:border-red-900 dark:bg-zinc-800 dark:text-red-300 dark:hover:bg-red-950">
|
||||
Delete
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-6 lg:grid-cols-2">
|
||||
<section class="rounded border border-stone-200 bg-white p-6 shadow-sm">
|
||||
<h2 class="text-sm font-semibold uppercase tracking-wide text-stone-500">Metadata</h2>
|
||||
<section class="rounded border border-stone-200 bg-white p-6 shadow-sm dark:border-zinc-700 dark:bg-zinc-800">
|
||||
<h2 class="text-sm font-semibold uppercase tracking-wide text-stone-500 dark:text-zinc-400">Metadata</h2>
|
||||
<dl class="mt-4 space-y-3 text-sm">
|
||||
<div class="flex justify-between gap-4">
|
||||
<dt class="text-stone-500">Original file</dt>
|
||||
<dt class="text-stone-500 dark:text-zinc-400">Original file</dt>
|
||||
<dd class="text-right font-medium">{{ $recording->original_filename }}</dd>
|
||||
</div>
|
||||
<div class="flex justify-between gap-4">
|
||||
<dt class="text-stone-500">Duration</dt>
|
||||
<dt class="text-stone-500 dark:text-zinc-400">Duration</dt>
|
||||
<dd class="font-medium">{{ $recording->duration_formatted }}</dd>
|
||||
</div>
|
||||
<div class="flex justify-between gap-4">
|
||||
<dt class="text-stone-500">Artist</dt>
|
||||
<dt class="text-stone-500 dark:text-zinc-400">Artist</dt>
|
||||
<dd class="font-medium">{{ $recording->artist ?: '—' }}</dd>
|
||||
</div>
|
||||
<div class="flex justify-between gap-4">
|
||||
<dt class="text-stone-500">Album</dt>
|
||||
<dt class="text-stone-500 dark:text-zinc-400">Album</dt>
|
||||
<dd class="font-medium">{{ $recording->album ?: '—' }}</dd>
|
||||
</div>
|
||||
<div class="flex justify-between gap-4">
|
||||
<dt class="text-stone-500">Recorded</dt>
|
||||
<dt class="text-stone-500 dark:text-zinc-400">Recorded</dt>
|
||||
<dd class="font-medium">{{ $recording->recorded_at?->format('Y-m-d') ?: '—' }}</dd>
|
||||
</div>
|
||||
<div class="flex justify-between gap-4">
|
||||
<dt class="text-stone-500">Size</dt>
|
||||
<dt class="text-stone-500 dark:text-zinc-400">Size</dt>
|
||||
<dd class="font-medium">{{ number_format($recording->file_size_bytes / 1024, 1) }} KB</dd>
|
||||
</div>
|
||||
<div class="flex justify-between gap-4">
|
||||
<dt class="text-stone-500">Uploaded</dt>
|
||||
<dt class="text-stone-500 dark:text-zinc-400">Uploaded</dt>
|
||||
<dd class="font-medium">{{ $recording->created_at?->format('Y-m-d H:i') }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
<section class="rounded border border-stone-200 bg-white p-6 shadow-sm">
|
||||
<h2 class="text-sm font-semibold uppercase tracking-wide text-stone-500">Transcribe</h2>
|
||||
<section class="rounded border border-stone-200 bg-white p-6 shadow-sm dark:border-zinc-700 dark:bg-zinc-800">
|
||||
<h2 class="text-sm font-semibold uppercase tracking-wide text-stone-500 dark:text-zinc-400">Transcribe</h2>
|
||||
<form method="POST" action="{{ route('recordings.transcribe', $recording) }}" class="mt-4">
|
||||
@csrf
|
||||
<button
|
||||
type="submit"
|
||||
class="rounded bg-teal-700 px-4 py-2 text-sm font-medium text-white hover:bg-teal-800"
|
||||
class="rounded bg-teal-700 px-4 py-2 text-sm font-medium text-white hover:bg-teal-800 dark:bg-teal-600 dark:hover:bg-teal-500"
|
||||
>
|
||||
<span x-text="startButtonLabel"></span>
|
||||
</button>
|
||||
@@ -94,7 +94,7 @@
|
||||
@csrf
|
||||
<button
|
||||
type="submit"
|
||||
class="rounded border border-stone-300 bg-white px-4 py-2 text-sm font-medium text-stone-800 hover:bg-stone-50"
|
||||
class="rounded border border-stone-300 bg-white px-4 py-2 text-sm font-medium text-stone-800 hover:bg-stone-50 dark:border-zinc-600 dark:bg-zinc-900 dark:text-zinc-100 dark:hover:bg-zinc-700"
|
||||
>
|
||||
Stop transcription
|
||||
</button>
|
||||
@@ -102,38 +102,38 @@
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section class="mt-6 rounded border border-stone-200 bg-white p-6 shadow-sm">
|
||||
<section class="mt-6 rounded border border-stone-200 bg-white p-6 shadow-sm dark:border-zinc-700 dark:bg-zinc-800">
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<h2 class="text-sm font-semibold uppercase tracking-wide text-stone-500">Transcript</h2>
|
||||
<h2 class="text-sm font-semibold uppercase tracking-wide text-stone-500 dark:text-zinc-400">Transcript</h2>
|
||||
<button
|
||||
type="button"
|
||||
x-show="!status.is_active && status.has_transcript"
|
||||
x-cloak
|
||||
@click="navigator.clipboard.writeText(status.transcript || '')"
|
||||
class="text-sm text-teal-700 hover:underline"
|
||||
class="text-sm text-teal-700 hover:underline dark:text-teal-300"
|
||||
>
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div x-show="status.is_active" x-cloak class="mt-4 space-y-3 rounded border border-amber-200 bg-amber-50 p-4">
|
||||
<div x-show="status.is_active" x-cloak class="mt-4 space-y-3 rounded border border-amber-200 bg-amber-50 p-4 dark:border-amber-900 dark:bg-amber-950/40">
|
||||
<div class="flex flex-wrap items-center justify-between gap-2 text-sm">
|
||||
<p class="font-medium text-amber-900" x-text="status.progress || 'Working…'"></p>
|
||||
<p class="text-amber-800 tabular-nums">
|
||||
<p class="font-medium text-amber-900 dark:text-amber-100" x-text="status.progress || 'Working…'"></p>
|
||||
<p class="tabular-nums text-amber-800 dark:text-amber-200">
|
||||
<span x-text="(status.percent ?? 0) + '%'"></span>
|
||||
<span class="mx-1 text-amber-600">·</span>
|
||||
<span class="mx-1 text-amber-600 dark:text-amber-400">·</span>
|
||||
<span x-text="'Elapsed ' + (status.elapsed_human || '0s')"></span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="h-2 overflow-hidden rounded-full bg-amber-100">
|
||||
<div class="h-2 overflow-hidden rounded-full bg-amber-100 dark:bg-amber-900/50">
|
||||
<div
|
||||
class="h-full rounded-full bg-amber-500 transition-all duration-500"
|
||||
:style="`width: ${Math.max(status.percent || 5, 5)}%`"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<ul class="space-y-1 text-xs text-amber-900/80">
|
||||
<ul class="space-y-1 text-xs text-amber-900/80 dark:text-amber-200/80">
|
||||
<li>
|
||||
Engine:
|
||||
<span class="font-medium" x-text="status.driver_label || '—'"></span>
|
||||
@@ -142,26 +142,26 @@
|
||||
<li>
|
||||
Audio length:
|
||||
<span class="font-medium" x-text="formatDuration(status.duration_seconds)"></span>
|
||||
<span class="text-amber-700">(longer files take longer)</span>
|
||||
<span class="text-amber-700 dark:text-amber-300">(longer files take longer)</span>
|
||||
</li>
|
||||
</template>
|
||||
<li x-show="pollError" class="text-red-700" x-text="pollError"></li>
|
||||
<li x-show="pollError" class="text-red-700 dark:text-red-300" x-text="pollError"></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div x-show="!status.is_active && status.status === 'cancelled'" x-cloak class="mt-4 rounded border border-stone-200 bg-stone-50 p-4 text-sm text-stone-700">
|
||||
<div x-show="!status.is_active && status.status === 'cancelled'" x-cloak class="mt-4 rounded border border-stone-200 bg-stone-50 p-4 text-sm text-stone-700 dark:border-zinc-600 dark:bg-zinc-900 dark:text-zinc-300">
|
||||
Transcription stopped. Choose an engine above to start again.
|
||||
</div>
|
||||
|
||||
<div x-show="!status.is_active && status.status === 'failed'" x-cloak class="mt-4 space-y-2 rounded border border-red-200 bg-red-50 p-4 text-sm text-red-800">
|
||||
<div x-show="!status.is_active && status.status === 'failed'" x-cloak class="mt-4 space-y-2 rounded border border-red-200 bg-red-50 p-4 text-sm text-red-800 dark:border-red-900 dark:bg-red-950 dark:text-red-200">
|
||||
<p class="font-medium">Transcription failed</p>
|
||||
<p x-text="status.error || 'Check the logs and try again with another engine.'"></p>
|
||||
</div>
|
||||
|
||||
<div x-show="!status.is_active && status.has_transcript" x-cloak>
|
||||
<p class="mt-4 whitespace-pre-wrap text-sm leading-relaxed text-stone-800" x-text="status.transcript"></p>
|
||||
<p class="mt-4 whitespace-pre-wrap text-sm leading-relaxed text-stone-800 dark:text-zinc-100" x-text="status.transcript"></p>
|
||||
<p
|
||||
class="mt-4 text-xs text-stone-500"
|
||||
class="mt-4 text-xs text-stone-500 dark:text-zinc-400"
|
||||
x-show="status.transcribed_at"
|
||||
x-text="status.transcribed_at ? ('Transcribed ' + formatTimestamp(status.transcribed_at)) : ''"
|
||||
></p>
|
||||
@@ -170,7 +170,7 @@
|
||||
<p
|
||||
x-show="!status.is_active && status.status !== 'failed' && status.status !== 'cancelled' && !status.has_transcript"
|
||||
x-cloak
|
||||
class="mt-4 text-sm text-stone-500"
|
||||
class="mt-4 text-sm text-stone-500 dark:text-zinc-400"
|
||||
>
|
||||
No transcript yet. Transcription starts automatically after upload, or use the button above.
|
||||
</p>
|
||||
|
||||
Reference in New Issue
Block a user