264 lines
16 KiB
HTML
264 lines
16 KiB
HTML
<!-- Desktop Transcription Panel (Left Column) -->
|
|
<div id="leftMainColumn" class="flex flex-col overflow-hidden" :style="{width: leftColumnWidth + '%'}">
|
|
<!-- Transcription Header -->
|
|
<div class="bg-[var(--bg-tertiary)] px-4 py-3 border-b border-[var(--border-primary)] flex items-center justify-between">
|
|
<h3 class="font-semibold flex items-center">
|
|
<i class="fas fa-file-text mr-2"></i>
|
|
<span v-text="t('transcription.title')"></span>
|
|
</h3>
|
|
<div class="flex items-center gap-2">
|
|
<!-- Follow Player Checkbox -->
|
|
<div v-if="processedTranscription.isJson && processedTranscription.hasDialogue"
|
|
class="follow-player-control text-[var(--text-muted)] hover:text-[var(--text-primary)]"
|
|
@click="toggleFollowPlayerMode"
|
|
:title="followPlayerMode ? t('tooltips.followPlayerEnabled') : t('tooltips.followPlayerDisabled')">
|
|
<input type="checkbox"
|
|
:checked="followPlayerMode"
|
|
@click.stop="toggleFollowPlayerMode">
|
|
<i class="fas fa-arrows-alt-v follow-icon"></i>
|
|
</div>
|
|
|
|
<!-- View Mode Toggle -->
|
|
<div v-if="processedTranscription.hasDialogue" class="view-mode-toggle">
|
|
<button @click="toggleTranscriptionViewMode"
|
|
:class="['toggle-button', transcriptionViewMode === 'simple' ? 'active' : '']">
|
|
<i class="fas fa-list"></i><span v-text="t('transcription.simple')"></span>
|
|
</button>
|
|
<button @click="toggleTranscriptionViewMode"
|
|
:class="['toggle-button', transcriptionViewMode === 'bubble' ? 'active' : '']">
|
|
<i class="fas fa-comments"></i><span v-text="t('transcription.bubble')"></span>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Copy Button -->
|
|
<button @click="copyTranscription"
|
|
class="copy-btn"
|
|
:title="t('tooltips.copyTranscript')">
|
|
<i class="fas fa-copy"></i>
|
|
</button>
|
|
<!-- Download Button with Dropdown -->
|
|
<div v-if="selectedRecording && selectedRecording.transcription" class="relative">
|
|
<button @click="showDownloadMenu = !showDownloadMenu"
|
|
data-download-toggle
|
|
class="copy-btn flex items-center"
|
|
:title="t('tooltips.downloadTranscriptWithTemplate')">
|
|
<i class="fas fa-download"></i>
|
|
<i class="fas fa-caret-down ml-1 text-[10px] opacity-50"></i>
|
|
</button>
|
|
<!-- Dropdown Menu -->
|
|
<div v-if="showDownloadMenu"
|
|
data-download-dropdown
|
|
class="absolute right-0 top-full mt-1 bg-[var(--bg-secondary)] border border-[var(--border-primary)] rounded-lg shadow-lg z-50 overflow-hidden whitespace-nowrap">
|
|
<button @click="downloadWithDefaultTemplate(); showDownloadMenu = false"
|
|
class="w-full text-left px-3 py-1.5 text-xs hover:bg-[var(--bg-tertiary)] transition-colors flex items-center gap-2">
|
|
<i class="fas fa-star text-[var(--text-accent)] text-[10px]"></i>
|
|
<span v-text="t('transcriptTemplates.downloadDefault')"></span>
|
|
</button>
|
|
<button @click="downloadTranscriptWord(); showDownloadMenu = false"
|
|
class="w-full text-left px-3 py-1.5 text-xs hover:bg-[var(--bg-tertiary)] transition-colors flex items-center gap-2 border-t border-[var(--border-secondary)]">
|
|
<i class="fas fa-file-word text-blue-500 text-[10px]"></i>
|
|
<span>Télécharger Word</span>
|
|
</button>
|
|
<button @click="showTemplateSelector(); showDownloadMenu = false"
|
|
class="w-full text-left px-3 py-1.5 text-xs hover:bg-[var(--bg-tertiary)] transition-colors flex items-center gap-2 border-t border-[var(--border-secondary)]">
|
|
<i class="fas fa-list text-[var(--text-muted)] text-[10px]"></i>
|
|
<span v-text="t('transcriptTemplates.chooseTemplate')"></span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<!-- Edit Transcription Button -->
|
|
<button @click="openTranscriptionEditor"
|
|
v-if="selectedRecording && selectedRecording.transcription"
|
|
class="copy-btn"
|
|
:title="t('tooltips.editTranscript')">
|
|
<i class="fas fa-edit"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Transcription Content -->
|
|
<div class="flex-1 overflow-y-auto p-4 relative">
|
|
<!-- Floating Processing Indicator -->
|
|
<div v-if="selectedRecording.status === 'PROCESSING'"
|
|
:class="['processing-indicator-floating', processingIndicatorMinimized ? 'minimized' : '']">
|
|
<template v-if="!processingIndicatorMinimized">
|
|
<div class="processing-indicator-content">
|
|
<i class="fas fa-spinner fa-spin text-[var(--text-accent)]"></i>
|
|
<span class="text-sm text-[var(--text-secondary)]" v-text="t('help.processingTranscription')"></span>
|
|
</div>
|
|
<button @click="processingIndicatorMinimized = true"
|
|
class="processing-indicator-minimize"
|
|
:title="t('tooltips.minimize')">
|
|
<i class="fas fa-chevron-up"></i>
|
|
</button>
|
|
</template>
|
|
<template v-else>
|
|
<button @click="processingIndicatorMinimized = false"
|
|
class="processing-indicator-expand"
|
|
:title="t('help.processingTranscription')">
|
|
<i class="fas fa-spinner fa-spin"></i>
|
|
</button>
|
|
</template>
|
|
</div>
|
|
|
|
<!-- No transcription state (only show if not processing and no transcription) -->
|
|
<div v-if="selectedRecording.status !== 'PROCESSING' && !selectedRecording.transcription" class="text-center py-8">
|
|
<i class="fas fa-file-text text-3xl text-[var(--text-muted)] mb-3"></i>
|
|
<p class="text-[var(--text-muted)]" v-text="t('transcription.noTranscription')"></p>
|
|
</div>
|
|
|
|
<!-- Error Display (when transcription is an error message) -->
|
|
<div v-if="processedTranscription.isError" class="error-display-container">
|
|
<div :class="[
|
|
'rounded-lg p-5 border',
|
|
processedTranscription.error.type === 'size_limit' ? 'bg-amber-500/10 border-amber-500/30' :
|
|
processedTranscription.error.type === 'timeout' ? 'bg-orange-500/10 border-orange-500/30' :
|
|
processedTranscription.error.type === 'auth' ? 'bg-red-500/10 border-red-500/30' :
|
|
processedTranscription.error.type === 'rate_limit' ? 'bg-yellow-500/10 border-yellow-500/30' :
|
|
processedTranscription.error.type === 'connection' ? 'bg-blue-500/10 border-blue-500/30' :
|
|
processedTranscription.error.type === 'service_error' ? 'bg-purple-500/10 border-purple-500/30' :
|
|
'bg-gray-500/10 border-gray-500/30'
|
|
]">
|
|
<div class="flex items-start gap-4">
|
|
<div :class="[
|
|
'flex-shrink-0 w-12 h-12 rounded-full flex items-center justify-center',
|
|
processedTranscription.error.type === 'size_limit' ? 'bg-amber-500/20' :
|
|
processedTranscription.error.type === 'timeout' ? 'bg-orange-500/20' :
|
|
processedTranscription.error.type === 'auth' ? 'bg-red-500/20' :
|
|
processedTranscription.error.type === 'rate_limit' ? 'bg-yellow-500/20' :
|
|
processedTranscription.error.type === 'connection' ? 'bg-blue-500/20' :
|
|
processedTranscription.error.type === 'service_error' ? 'bg-purple-500/20' :
|
|
'bg-gray-500/20'
|
|
]">
|
|
<i :class="[
|
|
'fas text-xl',
|
|
processedTranscription.error.icon,
|
|
processedTranscription.error.type === 'size_limit' ? 'text-amber-500' :
|
|
processedTranscription.error.type === 'timeout' ? 'text-orange-500' :
|
|
processedTranscription.error.type === 'auth' ? 'text-red-500' :
|
|
processedTranscription.error.type === 'rate_limit' ? 'text-yellow-500' :
|
|
processedTranscription.error.type === 'connection' ? 'text-blue-500' :
|
|
processedTranscription.error.type === 'service_error' ? 'text-purple-500' :
|
|
'text-gray-500'
|
|
]"></i>
|
|
</div>
|
|
<div class="flex-1 min-w-0">
|
|
<h3 :class="[
|
|
'text-lg font-semibold mb-2',
|
|
processedTranscription.error.type === 'size_limit' ? 'text-amber-600 dark:text-amber-400' :
|
|
processedTranscription.error.type === 'timeout' ? 'text-orange-600 dark:text-orange-400' :
|
|
processedTranscription.error.type === 'auth' ? 'text-red-600 dark:text-red-400' :
|
|
processedTranscription.error.type === 'rate_limit' ? 'text-yellow-600 dark:text-yellow-400' :
|
|
processedTranscription.error.type === 'connection' ? 'text-blue-600 dark:text-blue-400' :
|
|
processedTranscription.error.type === 'service_error' ? 'text-purple-600 dark:text-purple-400' :
|
|
'text-gray-600 dark:text-gray-400'
|
|
]">
|
|
${processedTranscription.error.title}
|
|
</h3>
|
|
<p class="text-[var(--text-primary)] mb-3">
|
|
${processedTranscription.error.message}
|
|
</p>
|
|
<div v-if="processedTranscription.error.guidance" class="flex items-start gap-2 text-sm text-[var(--text-secondary)] bg-[var(--bg-tertiary)]/50 rounded-lg p-3">
|
|
<i class="fas fa-lightbulb text-yellow-500 mt-0.5 flex-shrink-0"></i>
|
|
<span>${processedTranscription.error.guidance}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- Action buttons -->
|
|
<div class="mt-4 flex items-center gap-3 pt-4 border-t border-[var(--border-secondary)]">
|
|
<button @click="reprocessTranscription(selectedRecording.id)"
|
|
v-if="selectedRecording.can_edit !== false"
|
|
class="px-4 py-2 bg-[var(--bg-accent)] hover:bg-[var(--bg-accent-hover)] text-white rounded-lg transition-colors flex items-center gap-2">
|
|
<i class="fas fa-redo-alt"></i>
|
|
<span>Reprocess</span>
|
|
</button>
|
|
<details class="text-xs w-full">
|
|
<summary class="cursor-pointer text-[var(--text-muted)] hover:text-[var(--text-secondary)]">
|
|
Technical details
|
|
</summary>
|
|
<pre v-if="processedTranscription.error.technical" class="mt-2 p-3 bg-[var(--bg-tertiary)] rounded-lg text-[var(--text-muted)] text-xs max-h-32 overflow-auto whitespace-pre-wrap break-all w-full">${processedTranscription.error.technical}</pre>
|
|
</details>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Speaker Legend (for dialogue transcriptions in bubble view only) -->
|
|
<div v-if="!processedTranscription.isError && processedTranscription.hasDialogue && processedTranscription.speakers.length > 0 && transcriptionViewMode === 'bubble'"
|
|
:class="['speaker-legend', legendExpanded ? 'expanded' : '']">
|
|
<div class="speaker-legend-header" @click="legendExpanded = !legendExpanded">
|
|
<div class="speaker-legend-title">
|
|
<i class="fas fa-users"></i>
|
|
<span v-text="t('help.speakers')"></span>
|
|
<span class="speaker-count-indicator">(${processedTranscription.speakers.length})</span>
|
|
</div>
|
|
<i class="fas fa-chevron-down speaker-legend-toggle"></i>
|
|
</div>
|
|
<div class="speaker-legend-content">
|
|
<div v-for="(speaker, index) in processedTranscription.speakers"
|
|
:key="`${selectedRecording.id}-speaker-legend-${index}`"
|
|
:class="['speaker-legend-item', speaker.color]">
|
|
<span class="speaker-name">${speaker.name}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Transcription Display (hide if it's an error) -->
|
|
<div v-if="selectedRecording.transcription && !processedTranscription.isError">
|
|
<!-- Simple View -->
|
|
<div v-if="!processedTranscription.hasDialogue || transcriptionViewMode === 'simple'"
|
|
class="transcription-simple-view">
|
|
<div v-if="processedTranscription.hasDialogue">
|
|
<div v-for="(segment, index) in processedTranscription.simpleSegments"
|
|
:key="`seg-${index}-${segment.startTime}`"
|
|
:class="['speaker-segment', { 'active-playing-segment': currentPlayingSegmentIndex === index }]"
|
|
@click="seekAudioFromEvent"
|
|
:data-start-time="segment.startTime"
|
|
:data-segment-index="index">
|
|
<div v-if="segment.showSpeaker"
|
|
:class="['speaker-tablet', segment.color]">
|
|
${segment.speaker}
|
|
</div>
|
|
<div class="speaker-text">
|
|
${segment.sentence}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div v-else>
|
|
<div v-if="processedTranscription.simpleSegments && processedTranscription.simpleSegments.length > 0">
|
|
<div v-for="(segment, index) in processedTranscription.simpleSegments"
|
|
:key="`seg-${index}-${segment.startTime}`"
|
|
:class="['transcript-segment cursor-pointer hover:bg-[var(--bg-accent-hover)] p-2 rounded transition-colors', { 'active-playing-segment': currentPlayingSegmentIndex === index }]"
|
|
@click="seekAudioFromEvent"
|
|
:data-start-time="segment.startTime"
|
|
:data-segment-index="index">
|
|
${segment.sentence}
|
|
</div>
|
|
</div>
|
|
<div v-else class="whitespace-pre-wrap">
|
|
${processedTranscription.content}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Bubble View -->
|
|
<div v-else-if="transcriptionViewMode === 'bubble'"
|
|
class="transcription-with-speakers">
|
|
<template v-for="(row, rowIndex) in processedTranscription.bubbleRows" :key="`${selectedRecording.id}-bubble-row-${rowIndex}`">
|
|
<div :class="['bubble-row', row.isMe ? 'speaker-me' : '']">
|
|
<div v-for="(bubble, bubbleIndex) in row.bubbles"
|
|
:key="`bubble-${rowIndex}-${bubbleIndex}-${bubble.startTime}`"
|
|
:class="['speaker-bubble', bubble.color, row.isMe ? 'speaker-me' : '', { 'active-playing-segment': currentPlayingSegmentIndex === getBubbleGlobalIndex(rowIndex, bubbleIndex) }]"
|
|
@click="seekAudioFromEvent"
|
|
:data-start-time="bubble.startTime"
|
|
:data-segment-index="getBubbleGlobalIndex(rowIndex, bubbleIndex)">
|
|
<div class="speaker-bubble-content">
|
|
${bubble.sentence}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|