Files
AndyTranscribe/resources/js/transcription.js
T
ben c17f8fb506 Improve live recordings UI, upload flow, and default Flux theme.
Refresh list status over Reverb with polling fallback, clarify queued vs processing, streamline upload empty states, and seed an admin login.
2026-08-12 20:14:35 +02:00

258 lines
6.4 KiB
JavaScript

/**
* Shared helpers and Alpine components for live transcription updates via Reverb.
*/
const BADGE_COLORS = {
done: 'teal',
processing: 'amber',
pending: 'zinc',
failed: 'red',
cancelled: 'zinc',
};
export function badgeColorFor(status) {
return BADGE_COLORS[status] || 'zinc';
}
/** @deprecated Use badgeColorFor — kept for any leftover callers */
export function badgeClassFor(status) {
return badgeColorFor(status);
}
export function formatElapsed(seconds) {
const total = Math.max(0, Math.round(Number(seconds) || 0));
const minutes = Math.floor(total / 60);
const remain = total % 60;
if (minutes === 0) {
return remain + 's';
}
return minutes + 'm ' + String(remain).padStart(2, '0') + 's';
}
export function formatDuration(seconds) {
const total = Math.max(0, Math.round(Number(seconds) || 0));
const minutes = Math.floor(total / 60);
const remain = total % 60;
return minutes + ':' + String(remain).padStart(2, '0');
}
export function formatTimestamp(value) {
const date = new Date(value);
if (Number.isNaN(date.getTime())) {
return value;
}
const pad = (n) => String(n).padStart(2, '0');
return date.getFullYear()
+ '-' + pad(date.getMonth() + 1)
+ '-' + pad(date.getDate())
+ ' ' + pad(date.getHours())
+ ':' + pad(date.getMinutes());
}
function subscribeToRecording(recordingId, handler) {
if (!window.Echo) {
return () => {};
}
const channelName = 'recording.' + recordingId;
const channel = window.Echo.private(channelName);
channel.listen('.RecordingTranscriptionUpdated', handler);
return () => {
window.Echo.leave(channelName);
};
}
/**
* Show-page Alpine component: Echo push + local elapsed tick + optional status hydrate.
*/
export function transcriptionMonitor({ statusUrl, initial }) {
return {
statusUrl,
status: {
...initial,
badge_color: badgeColorFor(initial.status),
},
pollError: null,
tickTimer: null,
leaveChannel: null,
get badgeColor() {
return badgeColorFor(this.status.status);
},
get startButtonLabel() {
if (this.status.is_active) {
return 'Restart transcription';
}
return this.status.has_transcript ? 'Re-transcribe' : 'Start transcription';
},
start() {
this.leaveChannel = subscribeToRecording(this.status.id, (event) => {
if (Number(event.id) !== Number(this.status.id)) {
return;
}
this.applyPayload(event);
});
if (this.status.is_active) {
this.beginTick();
this.hydrateOnce();
}
},
destroy() {
this.stopTick();
if (this.leaveChannel) {
this.leaveChannel();
this.leaveChannel = null;
}
},
applyPayload(payload) {
const wasActive = this.status.is_active;
this.status = {
...this.status,
...payload,
badge_color: badgeColorFor(payload.status ?? this.status.status),
};
this.pollError = null;
if (this.status.is_active) {
this.beginTick();
} else {
this.stopTick();
}
if (wasActive && !this.status.is_active
&& !this.status.has_transcript
&& this.status.status !== 'failed'
&& this.status.status !== 'cancelled') {
window.location.reload();
}
},
beginTick() {
if (this.tickTimer) {
return;
}
this.tickTimer = setInterval(() => this.tickElapsed(), 1000);
},
stopTick() {
if (this.tickTimer) {
clearInterval(this.tickTimer);
this.tickTimer = null;
}
},
tickElapsed() {
if (!this.status.is_active || this.status.elapsed_seconds == null) {
return;
}
this.status.elapsed_seconds += 1;
this.status.elapsed_human = formatElapsed(this.status.elapsed_seconds);
},
async hydrateOnce() {
if (!this.statusUrl) {
return;
}
try {
const response = await fetch(this.statusUrl, {
headers: { Accept: 'application/json' },
});
if (!response.ok) {
throw new Error('Status request failed (' + response.status + ')');
}
this.applyPayload(await response.json());
} catch (error) {
this.pollError = error.message || 'Could not refresh progress.';
}
},
formatElapsed,
formatDuration,
formatTimestamp,
};
}
/**
* Index-page Alpine component: inline audio player only.
* Status/progress refresh via Livewire Echo + wire:poll.
*/
export function recordingsIndex() {
return {
playingId: null,
isPlaying: false,
start() {
//
},
destroy() {
const player = this.$refs.player;
if (player) {
player.pause();
player.removeAttribute('src');
player.load();
}
},
syncPlayer() {
const player = this.$refs.player;
this.isPlaying = Boolean(player && !player.paused && !player.ended);
if (player?.ended) {
this.playingId = null;
}
},
isPlayingRow(id) {
return this.playingId === id && this.isPlaying;
},
togglePlay(id, url) {
const player = this.$refs.player;
if (!player) {
return;
}
if (this.playingId === id && this.isPlaying) {
player.pause();
return;
}
if (this.playingId !== id) {
player.src = url;
this.playingId = id;
}
player.play().catch(() => {
this.playingId = null;
this.isPlaying = false;
});
},
};
}