Initial release: DictIA v0.8.14-alpha (fork de Speakr, AGPL-3.0)

This commit is contained in:
InnovA AI
2026-03-16 21:47:37 +00:00
commit 42772a31ed
365 changed files with 103572 additions and 0 deletions

View File

@@ -0,0 +1,77 @@
<!-- Add Speaker Modal -->
<div v-if="showAddSpeakerModal" @click.self="closeAddSpeakerModal" class="fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center z-50 p-4 backdrop-blur-sm">
<div class="bg-[var(--bg-secondary)] rounded-xl shadow-2xl w-full max-w-md">
<div class="p-6 border-b border-[var(--border-primary)]">
<div class="flex items-center justify-between mb-4">
<h3 class="text-xl font-bold text-[var(--text-primary)]" v-text="t('modal.addSpeaker')"></h3>
<button @click="closeAddSpeakerModal" class="text-[var(--text-muted)] hover:text-[var(--text-primary)] transition-colors">
<i class="fas fa-times text-xl"></i>
</button>
</div>
</div>
<div class="p-6">
<!-- "This is Me" checkbox -->
<div class="mb-4">
<label class="flex items-center text-sm text-[var(--text-muted)] cursor-pointer hover:text-[var(--text-primary)] transition-colors">
<input type="checkbox" v-model="newSpeakerIsMe" class="speaker-checkbox">
<span class="ml-2 select-none" v-text="t('help.me')"></span>
</label>
</div>
<!-- Speaker name input with autocomplete (disabled if "This is Me" is checked) -->
<div class="mb-6 relative">
<label class="block text-sm font-medium text-[var(--text-secondary)] mb-2" v-text="t('help.speakerName')">
</label>
<input
type="text"
v-model="newSpeakerName"
@input="searchNewSpeaker"
@focus="showNewSpeakerSuggestions = true"
@blur="hideNewSpeakerSuggestionsDelayed"
:disabled="newSpeakerIsMe"
:placeholder="newSpeakerIsMe ? currentUserName : t('help.enterSpeakerName')"
class="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-md shadow-sm focus:outline-none focus:ring-[var(--border-focus)] focus:border-[var(--border-focus)] sm:text-sm bg-[var(--bg-input)] text-[var(--text-primary)] disabled:bg-[var(--bg-tertiary)] disabled:text-[var(--text-muted)] disabled:cursor-not-allowed"
@keyup.enter="addNewSpeaker"
autocomplete="off"
>
<!-- Loading indicator -->
<div v-if="loadingNewSpeakerSuggestions && !newSpeakerIsMe" class="absolute right-3 top-[42px] transform -translate-y-1/2">
<i class="fas fa-spinner fa-spin text-[var(--text-muted)] text-sm"></i>
</div>
<!-- Suggestions dropdown -->
<div v-if="showNewSpeakerSuggestions && newSpeakerSuggestions.length > 0 && !newSpeakerIsMe"
@click.stop
class="absolute z-10 w-full mt-1 bg-[var(--bg-secondary)] border border-[var(--border-primary)] rounded-md shadow-lg max-h-48 overflow-y-auto">
<div class="py-1">
<div v-for="suggestion in newSpeakerSuggestions"
:key="suggestion.id"
@click="selectNewSpeakerSuggestion(suggestion)"
class="px-3 py-2 cursor-pointer hover:bg-[var(--bg-tertiary)] flex items-center justify-between">
<div class="flex-grow">
<div class="text-sm font-medium text-[var(--text-primary)]">${ suggestion.name }</div>
<div class="text-xs text-[var(--text-muted)]">
Used ${ suggestion.use_count } time${ suggestion.use_count !== 1 ? 's' : '' }
<span v-if="suggestion.last_used">
• Last: ${ new Date(suggestion.last_used).toLocaleDateString() }
</span>
</div>
</div>
<i class="fas fa-user text-[var(--text-muted)] ml-2"></i>
</div>
</div>
</div>
</div>
<!-- Action buttons -->
<div class="flex justify-end space-x-3">
<button @click="closeAddSpeakerModal" class="px-4 py-2 bg-[var(--bg-secondary)] text-[var(--text-secondary)] rounded-lg border border-[var(--border-secondary)] hover:bg-[var(--bg-tertiary)] transition-colors" v-text="t('common.cancel')">
</button>
<button @click="addNewSpeaker" class="px-4 py-2 bg-[var(--bg-accent)] text-[var(--text-accent)] rounded-lg hover:bg-[var(--bg-accent-hover)] transition-colors" v-text="t('buttons.addSpeaker')">
</button>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,154 @@
<!-- ASR Editor Modal -->
<div v-if="showAsrEditorModal" @click.self="closeAsrEditorModal" class="fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center z-50 p-4 backdrop-blur-sm">
<div @click="closeAllSpeakerSuggestions" class="bg-[var(--bg-secondary)] rounded-xl shadow-2xl w-full max-w-6xl flex flex-col max-h-[90vh]">
<div class="p-5 border-b border-[var(--border-primary)] flex-shrink-0 flex justify-between items-center">
<h3 class="text-xl font-bold text-[var(--text-primary)]" v-text="t('modal.editAsrTranscription')"></h3>
<button @click="closeAsrEditorModal" class="p-2 rounded-lg hover:bg-[var(--bg-tertiary)] transition-colors">
<i class="fas fa-times"></i>
</button>
</div>
<!-- Audio Player Section -->
<div class="px-6 py-4 border-b border-[var(--border-primary)] flex-shrink-0">
<!-- Show message if audio has been deleted -->
<div v-if="selectedRecording.audio_deleted_at"
class="bg-[var(--bg-tertiary)] border border-[var(--border-primary)] text-[var(--text-secondary)] px-4 py-3 rounded-lg flex items-center gap-2 text-sm">
<i class="fas fa-info-circle"></i>
<span v-text="t('help.audioDeletedMessage')"></span>
</div>
<!-- Show message for incognito recordings -->
<div v-else-if="selectedRecording.incognito"
class="bg-[var(--bg-tertiary)] border border-[var(--border-primary)] text-[var(--text-secondary)] px-4 py-3 rounded-lg flex items-center gap-2 text-sm">
<i class="fas fa-user-secret"></i>
<span>Audio not stored in incognito mode</span>
</div>
<!-- Custom Audio Player (Independent from main player) -->
<div v-else class="flex items-center gap-3">
<audio ref="asrEditorAudioRef" class="hidden"
:key="'asr-editor-' + selectedRecording.id"
:src="'/audio/' + selectedRecording.id"
:volume="playerVolume"
@play="handleModalAudioPlayPause"
@pause="handleModalAudioPlayPause"
@timeupdate="handleModalAudioTimeUpdate"
@loadedmetadata="handleModalAudioLoadedMetadata"
@ended="modalAudioIsPlaying = false">
</audio>
<!-- Play/Pause -->
<button @click="$refs.asrEditorAudioRef?.paused ? $refs.asrEditorAudioRef.play() : $refs.asrEditorAudioRef.pause()"
class="w-10 h-10 flex items-center justify-center rounded-full bg-[var(--bg-accent)] hover:bg-[var(--bg-accent-hover)] text-white transition-all flex-shrink-0 shadow-sm"
:title="modalAudioIsPlaying ? 'Pause' : 'Play'">
<i :class="modalAudioIsPlaying ? 'fas fa-pause' : 'fas fa-play'" class="text-sm" :style="!modalAudioIsPlaying ? 'margin-left: 2px' : ''"></i>
</button>
<!-- Time -->
<div class="flex flex-col items-end flex-shrink-0 leading-none">
<span class="text-sm text-[var(--text-primary)] font-mono">${ formatAudioTime(modalAudioCurrentTime) }</span>
<span class="text-xs text-[var(--text-muted)] font-mono">${ formatAudioTime(modalAudioDuration) }</span>
</div>
<!-- Progress Bar -->
<div class="flex-1 h-2 bg-[var(--bg-tertiary)] rounded-full cursor-pointer group relative"
@click="(e) => { const rect = e.currentTarget.getBoundingClientRect(); const pct = (e.clientX - rect.left) / rect.width; if ($refs.asrEditorAudioRef) $refs.asrEditorAudioRef.currentTime = pct * modalAudioDuration; }">
<div class="h-full bg-[var(--bg-accent)] rounded-full transition-all duration-100"
:style="{ width: modalAudioProgressPercent + '%' }">
</div>
</div>
<!-- Volume -->
<div class="flex items-center gap-2 flex-shrink-0">
<button @click="$refs.asrEditorAudioRef && ($refs.asrEditorAudioRef.muted = !$refs.asrEditorAudioRef.muted)"
class="w-8 h-8 flex items-center justify-center rounded hover:bg-[var(--bg-tertiary)] text-[var(--text-muted)] hover:text-[var(--text-primary)] transition-all">
<i :class="playerVolume === 0 ? 'fas fa-volume-mute' : 'fas fa-volume-up'" class="text-sm"></i>
</button>
<input type="range" min="0" max="1" step="0.05" :value="playerVolume"
@input="(e) => { if ($refs.asrEditorAudioRef) $refs.asrEditorAudioRef.volume = parseFloat(e.target.value); }"
class="volume-slider w-20 h-1.5 rounded-full cursor-pointer">
</div>
<!-- Speed Control (compact - tap to cycle) -->
<button @click="cycleModalPlaybackRate(); if ($refs.asrEditorAudioRef) $refs.asrEditorAudioRef.playbackRate = modalPlaybackRate"
class="w-8 h-8 flex items-center justify-center rounded hover:bg-[var(--bg-tertiary)] text-[var(--text-muted)] hover:text-[var(--text-primary)] transition-all flex-shrink-0"
title="Playback speed (tap to change)">
<span class="text-xs font-semibold font-mono">${ formatPlaybackRate(modalPlaybackRate) }</span>
</button>
</div>
</div>
<div ref="asrEditorRef" class="flex-grow overflow-y-auto custom-scrollbar" @scroll="closeAllSpeakerSuggestions(); onAsrEditorScroll($event)">
<table class="min-w-full asr-editor-table border border-[var(--border-primary)]">
<thead class="sticky top-0 z-10">
<tr class="text-xs uppercase text-[var(--text-muted)]">
<th class="w-40 text-left py-2 px-3 font-medium border border-[var(--border-primary)] bg-[var(--bg-tertiary)]" v-text="t('help.speakerCount')"></th>
<th class="w-20 text-left py-2 px-3 font-medium border border-[var(--border-primary)] bg-[var(--bg-tertiary)]" v-text="t('help.startTime')"></th>
<th class="w-20 text-left py-2 px-3 font-medium border border-[var(--border-primary)] bg-[var(--bg-tertiary)]" v-text="t('help.endTime')"></th>
<th class="text-left py-2 px-3 font-medium border border-[var(--border-primary)] bg-[var(--bg-tertiary)]" v-text="t('help.sentence')"></th>
<th class="w-24 text-center py-2 px-1 font-medium border border-[var(--border-primary)] bg-[var(--bg-tertiary)]"><span class="sr-only" v-text="t('help.actions')"></span></th>
</tr>
</thead>
<tbody>
<!-- Virtual scroll spacer (top) -->
<tr v-if="asrEditorSpacerBefore > 0"><td colspan="5" :style="{ height: asrEditorSpacerBefore + 'px', padding: 0 }"></td></tr>
<tr v-for="segment in asrEditorVisibleSegments" :key="`asr-${segment._originalIndex}-${segment.start_time}`" :data-segment-index="segment._originalIndex" class="hover:bg-[var(--bg-tertiary)] transition-colors border-b border-[var(--border-primary)]">
<td class="border-r border-[var(--border-primary)] align-middle">
<div class="relative h-full">
<div class="flex items-center h-full">
<input type="text" v-model="editingSegments[segment._originalIndex].speaker"
@input="filterSpeakerSuggestions(segment._originalIndex); openSpeakerSuggestions(segment._originalIndex)"
@focus="openSpeakerSuggestions(segment._originalIndex)"
class="w-full h-full px-3 py-2.5 pr-7 bg-transparent border-0 focus:outline-none text-sm text-[var(--text-primary)]" />
<button type="button" @click.stop="openSpeakerSuggestions(segment._originalIndex)"
class="absolute right-2 top-1/2 -translate-y-1/2 p-1 text-[var(--text-muted)] hover:text-[var(--text-primary)] cursor-pointer">
<i class="fas fa-chevron-down text-xs"></i>
</button>
</div>
</div>
<!-- Fixed position dropdown -->
<teleport to="body">
<div v-if="isDropdownOpen(segment._originalIndex)"
class="fixed z-[100] w-40 bg-[var(--bg-secondary)] border border-[var(--border-primary)] rounded-md shadow-lg max-h-48 overflow-y-auto"
:style="getDropdownPosition(segment._originalIndex)">
<div v-for="speaker in editingSegments[segment._originalIndex].filteredSpeakers" :key="speaker" @mousedown.prevent="selectSpeaker(segment._originalIndex, speaker)" class="px-3 py-2 cursor-pointer hover:bg-[var(--bg-tertiary)] text-sm">${speaker}</div>
<div @mousedown.prevent="openEditSpeakersModal" class="px-3 py-2 cursor-pointer hover:bg-[var(--bg-accent)] text-sm text-[var(--text-accent)] border-t border-[var(--border-primary)] flex items-center">
<i class="fas fa-user-edit mr-2"></i> <span v-text="t('buttons.editSpeakers')"></span>
</div>
</div>
</teleport>
</td>
<td class="border-r border-[var(--border-primary)] align-middle">
<input type="number" step="0.01" v-model.number="editingSegments[segment._originalIndex].start_time" class="w-full px-3 py-2.5 bg-transparent border-0 focus:outline-none text-sm text-[var(--text-primary)] text-center" />
</td>
<td class="border-r border-[var(--border-primary)] align-middle">
<input type="number" step="0.01" v-model.number="editingSegments[segment._originalIndex].end_time" class="w-full px-3 py-2.5 bg-transparent border-0 focus:outline-none text-sm text-[var(--text-primary)] text-center" />
</td>
<td class="border-r border-[var(--border-primary)] align-top">
<textarea v-model="editingSegments[segment._originalIndex].sentence" rows="1" class="w-full px-3 py-2.5 bg-transparent border-0 focus:outline-none text-sm text-[var(--text-primary)] resize-none max-h-20 overflow-y-auto custom-scrollbar block" @input="autoResizeTextarea($event)"></textarea>
</td>
<td class="px-1 text-center align-middle">
<div class="flex items-center justify-center gap-1">
<button @click="$refs.asrEditorAudioRef && ($refs.asrEditorAudioRef.currentTime = segment.start_time, $refs.asrEditorAudioRef.play())" class="p-1.5 rounded hover:bg-[var(--bg-accent)] text-[var(--text-muted)] hover:text-[var(--text-accent)] transition-colors" :title="t('help.playFromHere')">
<i class="fas fa-play text-xs"></i>
</button>
<button @click="addSegmentBelow(segment._originalIndex)" class="p-1.5 rounded hover:bg-green-600/20 text-[var(--text-muted)] hover:text-green-500 transition-colors" :title="t('help.addSegmentBelow')">
<i class="fas fa-plus text-xs"></i>
</button>
<button @click="removeSegment(segment._originalIndex)" class="p-1.5 rounded hover:bg-red-600/20 text-[var(--text-muted)] hover:text-red-500 transition-colors" :title="t('help.deleteSegment')">
<i class="fas fa-trash text-xs"></i>
</button>
</div>
</td>
</tr>
<!-- Virtual scroll spacer (bottom) -->
<tr v-if="asrEditorSpacerAfter > 0"><td colspan="5" :style="{ height: asrEditorSpacerAfter + 'px', padding: 0 }"></td></tr>
</tbody>
</table>
<div class="py-4 flex justify-center">
<button @click="addSegment" class="px-4 py-2 bg-[var(--bg-accent)] text-[var(--text-accent)] rounded-lg hover:bg-[var(--bg-accent-hover)] flex items-center text-sm">
<i class="fas fa-plus mr-2"></i> <span v-text="t('buttons.addSegment')"></span>
</button>
</div>
</div>
<div class="bg-[var(--bg-tertiary)] px-6 py-4 flex justify-end space-x-3 border-t border-[var(--border-primary)] flex-shrink-0 rounded-b-xl">
<button @click="closeAsrEditorModal" class="px-4 py-2 bg-[var(--bg-secondary)] text-[var(--text-secondary)] rounded-lg border border-[var(--border-secondary)] hover:bg-[var(--bg-tertiary)]" v-text="t('common.cancel')"></button>
<button @click="saveAsrTranscription" class="px-4 py-2 bg-[var(--bg-accent)] text-[var(--text-accent)] rounded-lg hover:bg-[var(--bg-accent-hover)]" v-text="t('buttons.saveChanges')"></button>
</div>
</div>
</div>

View File

@@ -0,0 +1,79 @@
<!-- Bulk Action Bar - Fixed at bottom of sidebar when in selection mode -->
<transition name="slide-up">
<div v-if="selectionMode && selectedCount > 0"
class="fixed bottom-0 left-0 w-80 bg-[var(--bg-secondary)] border-t border-r border-[var(--border-primary)] shadow-lg z-50 transform transition-transform duration-300"
:class="[
{ 'translate-y-full': bulkActionInProgress },
isSidebarCollapsed ? '-translate-x-full' : ''
]">
<div class="px-3 py-2">
<!-- Compact vertical layout for sidebar width -->
<div class="flex flex-col gap-2">
<!-- Selection info row -->
<div class="flex items-center justify-between">
<span class="text-sm font-medium text-[var(--text-primary)]">
${ selectedCount } selected
</span>
<div class="flex items-center gap-2">
<button @click="allVisibleSelected ? clearSelection() : selectAll()"
class="text-xs text-[var(--text-accent)] hover:underline">
${ allVisibleSelected ? 'Clear' : 'All' }
</button>
<button @click="exitSelectionMode"
class="p-1 text-[var(--text-muted)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-tertiary)] rounded transition-colors"
title="Exit selection mode">
<i class="fas fa-times text-xs"></i>
</button>
</div>
</div>
<!-- Action buttons row -->
<div class="flex items-center justify-between gap-1">
<button @click="openBulkTagModal('add')"
class="flex-1 px-2 py-1.5 text-xs bg-[var(--bg-tertiary)] hover:bg-[var(--bg-input)] rounded-md transition-colors flex items-center justify-center gap-1"
title="Add or remove tags">
<i class="fas fa-tags text-[var(--text-muted)]"></i>
<span>Tags</span>
</button>
<button @click="bulkToggleInbox()"
class="flex-1 px-2 py-1.5 text-xs bg-[var(--bg-tertiary)] hover:bg-[var(--bg-input)] rounded-md transition-colors flex items-center justify-center gap-1"
title="Toggle inbox status">
<i class="fas fa-inbox text-blue-500"></i>
<span>Inbox</span>
</button>
<button @click="bulkToggleHighlight()"
class="flex-1 px-2 py-1.5 text-xs bg-[var(--bg-tertiary)] hover:bg-[var(--bg-input)] rounded-md transition-colors flex items-center justify-center gap-1"
title="Toggle highlight">
<i class="fas fa-star text-amber-500"></i>
<span>Star</span>
</button>
<button @click="openBulkReprocessModal"
class="flex-1 px-2 py-1.5 text-xs bg-[var(--bg-tertiary)] hover:bg-[var(--bg-input)] rounded-md transition-colors flex items-center justify-center gap-1"
title="Reprocess">
<i class="fas fa-redo text-[var(--text-muted)]"></i>
<span>Redo</span>
</button>
<button @click="openBulkDeleteModal"
class="flex-1 px-2 py-1.5 text-xs bg-red-600 hover:bg-red-700 text-white rounded-md transition-colors flex items-center justify-center gap-1"
title="Delete selected">
<i class="fas fa-trash"></i>
<span>Delete</span>
</button>
</div>
</div>
</div>
<!-- Loading overlay -->
<div v-if="bulkActionInProgress"
class="absolute inset-0 bg-[var(--bg-secondary)] bg-opacity-80 flex items-center justify-center">
<div class="flex items-center gap-2">
<i class="fas fa-spinner fa-spin text-[var(--text-accent)]"></i>
<span class="text-xs text-[var(--text-secondary)]">Processing...</span>
</div>
</div>
</div>
</transition>

View File

@@ -0,0 +1,48 @@
<!-- Bulk Delete Confirmation Modal -->
<div v-if="showBulkDeleteModal" @click.self="closeBulkDeleteModal" class="fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center z-[100] p-4 backdrop-blur-sm">
<div class="bg-[var(--bg-secondary)] rounded-lg shadow-xl w-full max-w-md">
<div class="p-6">
<div class="flex items-center justify-between mb-4">
<div class="flex items-center gap-4">
<div class="w-12 h-12 bg-red-100 dark:bg-red-900/30 rounded-full flex items-center justify-center">
<i class="fas fa-exclamation-triangle text-xl text-red-600 dark:text-red-400"></i>
</div>
<div>
<h3 class="text-lg font-semibold text-[var(--text-primary)]">Delete ${ selectedCount } Recording${ selectedCount !== 1 ? 's' : '' }</h3>
<p class="text-sm text-[var(--text-muted)]">This action cannot be undone</p>
</div>
</div>
<button @click="closeBulkDeleteModal" class="text-[var(--text-muted)] hover:text-[var(--text-primary)] transition-colors">
<i class="fas fa-times text-xl"></i>
</button>
</div>
<div class="bg-[var(--bg-tertiary)] rounded-lg p-4 mb-6">
<p class="text-sm text-[var(--text-secondary)] mb-3">
You are about to permanently delete:
</p>
<ul class="space-y-1 max-h-32 overflow-y-auto">
<li v-for="recording in selectedRecordings.slice(0, 5)" :key="recording.id" class="text-sm text-[var(--text-primary)] flex items-center gap-2">
<i class="fas fa-file-audio text-[var(--text-muted)] text-xs"></i>
<span class="truncate">${ recording.title || 'Untitled' }</span>
</li>
<li v-if="selectedRecordings.length > 5" class="text-sm text-[var(--text-muted)] italic">
...and ${ selectedRecordings.length - 5 } more
</li>
</ul>
</div>
<div class="flex justify-end gap-3">
<button @click="closeBulkDeleteModal"
class="px-4 py-2 text-[var(--text-secondary)] hover:bg-[var(--bg-tertiary)] rounded-lg transition-colors">
Cancel
</button>
<button @click="executeBulkDelete"
class="px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg transition-colors flex items-center gap-2">
<i class="fas fa-trash"></i>
Delete All
</button>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,89 @@
<!-- Bulk Reprocess Modal -->
<div v-if="showBulkReprocessModal" @click.self="closeBulkReprocessModal" class="fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center z-[100] p-4 backdrop-blur-sm">
<div class="bg-[var(--bg-secondary)] rounded-xl shadow-2xl w-full max-w-md">
<div class="p-6 border-b border-[var(--border-primary)]">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<div class="w-10 h-10 bg-gradient-to-br from-blue-500 to-purple-600 rounded-full flex items-center justify-center">
<i class="fas fa-sync-alt text-white"></i>
</div>
<div>
<h3 class="text-lg font-semibold text-[var(--text-primary)]">Bulk Reprocess</h3>
<p class="text-sm text-[var(--text-muted)]">${ selectedCount } recording${ selectedCount !== 1 ? 's' : '' } selected</p>
</div>
</div>
<button @click="closeBulkReprocessModal" class="text-[var(--text-muted)] hover:text-[var(--text-primary)] transition-colors">
<i class="fas fa-times text-xl"></i>
</button>
</div>
</div>
<div class="p-6">
<!-- Reprocess Type Selection -->
<div class="mb-6">
<label class="block text-sm font-medium text-[var(--text-secondary)] mb-3">What do you want to reprocess?</label>
<div class="space-y-2">
<label class="flex items-start gap-3 p-3 rounded-lg border border-[var(--border-secondary)] cursor-pointer hover:bg-[var(--bg-tertiary)] transition-colors"
:class="{ 'border-[var(--border-accent)] bg-[var(--bg-accent)]': bulkReprocessType === 'summary' }">
<input type="radio" v-model="bulkReprocessType" value="summary" class="mt-1">
<div>
<span class="font-medium text-[var(--text-primary)]">Summary Only</span>
<p class="text-sm text-[var(--text-muted)] mt-0.5">
Regenerate title and summary from existing transcription. Faster option.
</p>
</div>
</label>
<label class="flex items-start gap-3 p-3 rounded-lg border border-[var(--border-secondary)] cursor-pointer hover:bg-[var(--bg-tertiary)] transition-colors"
:class="{ 'border-[var(--border-accent)] bg-[var(--bg-accent)]': bulkReprocessType === 'transcription' }">
<input type="radio" v-model="bulkReprocessType" value="transcription" class="mt-1">
<div>
<span class="font-medium text-[var(--text-primary)]">Full Transcription</span>
<p class="text-sm text-[var(--text-muted)] mt-0.5">
Re-transcribe audio and regenerate everything. Takes longer.
</p>
</div>
</label>
</div>
</div>
<!-- Warning -->
<div class="p-3 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-700 rounded-lg mb-6">
<div class="flex items-start gap-2">
<i class="fas fa-exclamation-triangle text-amber-600 dark:text-amber-400 mt-0.5"></i>
<div class="text-sm">
<p class="font-medium text-amber-800 dark:text-amber-200">Note</p>
<p class="text-amber-700 dark:text-amber-300 mt-0.5" v-if="bulkReprocessType === 'summary'">
This will overwrite any manual edits to titles and summaries.
</p>
<p class="text-amber-700 dark:text-amber-300 mt-0.5" v-else>
This will overwrite all transcriptions, titles, and summaries. Manual edits will be lost.
</p>
</div>
</div>
</div>
<!-- Recording count info -->
<div class="text-sm text-[var(--text-muted)] mb-4">
<i class="fas fa-info-circle mr-1"></i>
<span v-if="bulkReprocessType === 'summary'">
${ selectedCount } recording${ selectedCount !== 1 ? 's' : '' } will be queued for summary regeneration.
</span>
<span v-else>
${ selectedCount } recording${ selectedCount !== 1 ? 's' : '' } will be queued for full reprocessing.
</span>
</div>
</div>
<div class="p-6 border-t border-[var(--border-primary)] flex justify-end gap-3">
<button @click="closeBulkReprocessModal"
class="px-4 py-2 text-[var(--text-secondary)] hover:bg-[var(--bg-tertiary)] rounded-lg transition-colors">
Cancel
</button>
<button @click="executeBulkReprocess"
class="px-4 py-2 bg-gradient-to-r from-blue-500 to-purple-600 hover:from-blue-600 hover:to-purple-700 text-white rounded-lg transition-colors flex items-center gap-2">
<i class="fas fa-sync-alt"></i>
Start Reprocessing
</button>
</div>
</div>
</div>

View File

@@ -0,0 +1,101 @@
<!-- Bulk Tag Modal -->
<div v-if="showBulkTagModal" @click.self="closeBulkTagModal" class="fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center z-[100] p-4 backdrop-blur-sm">
<div class="bg-[var(--bg-secondary)] rounded-xl shadow-2xl w-full max-w-md max-h-[80vh] flex flex-col">
<div class="p-6 border-b border-[var(--border-primary)] flex-shrink-0">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<div class="w-10 h-10 bg-[var(--bg-accent)] rounded-full flex items-center justify-center">
<i class="fas fa-tags text-[var(--text-accent)]"></i>
</div>
<div>
<h3 class="text-lg font-semibold text-[var(--text-primary)]">Bulk Tag Update</h3>
<p class="text-sm text-[var(--text-muted)]">${ selectedCount } recording${ selectedCount !== 1 ? 's' : '' } selected</p>
</div>
</div>
<button @click="closeBulkTagModal" class="text-[var(--text-muted)] hover:text-[var(--text-primary)] transition-colors">
<i class="fas fa-times text-xl"></i>
</button>
</div>
</div>
<div class="p-6 overflow-y-auto flex-1">
<!-- Action Toggle -->
<div class="flex rounded-lg bg-[var(--bg-tertiary)] p-1 mb-4">
<button @click="bulkTagAction = 'add'"
:class="[
'flex-1 px-4 py-2 text-sm font-medium rounded-md transition-colors',
bulkTagAction === 'add'
? 'bg-[var(--bg-button)] text-[var(--text-button)]'
: 'text-[var(--text-muted)] hover:text-[var(--text-primary)]'
]">
<i class="fas fa-plus mr-2"></i>Add Tag
</button>
<button @click="bulkTagAction = 'remove'"
:class="[
'flex-1 px-4 py-2 text-sm font-medium rounded-md transition-colors',
bulkTagAction === 'remove'
? 'bg-[var(--bg-button)] text-[var(--text-button)]'
: 'text-[var(--text-muted)] hover:text-[var(--text-primary)]'
]">
<i class="fas fa-minus mr-2"></i>Remove Tag
</button>
</div>
<!-- Tag Selection -->
<div>
<label class="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Select a tag to ${ bulkTagAction }
</label>
<div class="max-h-64 overflow-y-auto space-y-2 border border-[var(--border-secondary)] rounded-lg p-3 bg-[var(--bg-input)]">
<button v-for="tag in availableTags" :key="tag.id"
@click="bulkTagSelectedId = tag.id"
:class="[
'w-full flex items-center justify-between px-3 py-2 rounded-lg transition-colors',
bulkTagSelectedId == tag.id
? 'bg-[var(--bg-accent)] ring-2 ring-[var(--border-accent)]'
: 'hover:bg-[var(--bg-tertiary)]'
]">
<div class="flex items-center gap-2">
<span class="w-3 h-3 rounded-full flex-shrink-0" :style="{ backgroundColor: tag.color }"></span>
<span class="text-sm text-[var(--text-primary)]">${ tag.name }</span>
<i v-if="tag.group_id" class="fas fa-users text-xs text-[var(--text-muted)]" title="Group tag"></i>
</div>
<i v-if="bulkTagSelectedId == tag.id" class="fas fa-check text-[var(--text-accent)]"></i>
</button>
<p v-if="availableTags.length === 0" class="text-sm text-[var(--text-muted)] text-center py-4">
No tags available. Create tags in settings.
</p>
</div>
</div>
<!-- Info message -->
<div class="mt-4 p-3 bg-[var(--bg-tertiary)] rounded-lg text-sm text-[var(--text-muted)]">
<i class="fas fa-info-circle mr-2"></i>
<span v-if="bulkTagAction === 'add'">
The selected tag will be added to all ${ selectedCount } recording${ selectedCount !== 1 ? 's' : '' }.
</span>
<span v-else>
The selected tag will be removed from all ${ selectedCount } recording${ selectedCount !== 1 ? 's' : '' } that have it.
</span>
</div>
</div>
<div class="p-6 border-t border-[var(--border-primary)] flex justify-end gap-3 flex-shrink-0">
<button @click="closeBulkTagModal"
class="px-4 py-2 text-[var(--text-secondary)] hover:bg-[var(--bg-tertiary)] rounded-lg transition-colors">
Cancel
</button>
<button @click="executeBulkTag"
:disabled="!bulkTagSelectedId"
:class="[
'px-4 py-2 rounded-lg transition-colors flex items-center gap-2',
bulkTagSelectedId
? 'bg-[var(--bg-button)] text-[var(--text-button)] hover:bg-[var(--bg-button-hover)]'
: 'bg-[var(--bg-tertiary)] text-[var(--text-muted)] cursor-not-allowed'
]">
<i :class="['fas', bulkTagAction === 'add' ? 'fa-plus' : 'fa-minus']"></i>
${ bulkTagAction === 'add' ? 'Add' : 'Remove' } Tag
</button>
</div>
</div>
</div>

View File

@@ -0,0 +1,51 @@
<!-- Color Scheme Modal -->
<div v-if="showColorSchemeModal" class="color-scheme-modal" @click.self="closeColorSchemeModal">
<div class="color-scheme-modal-content">
<div class="color-scheme-header">
<div class="flex items-center justify-between mb-2">
<h2 class="color-scheme-title">
<i class="fas fa-palette"></i>
<span v-text="t('colorScheme.title')"></span>
</h2>
<button @click="closeColorSchemeModal" class="text-[var(--text-muted)] hover:text-[var(--text-primary)] transition-colors">
<i class="fas fa-times text-xl"></i>
</button>
</div>
<p class="color-scheme-subtitle" v-text="t('colorScheme.subtitle')"></p>
</div>
<div class="color-scheme-body">
<div class="color-scheme-section">
<h3 class="color-scheme-section-title">
<i :class="isDarkMode ? 'fas fa-moon' : 'fas fa-sun'"></i>
<span v-text="isDarkMode ? t('colorScheme.darkThemes') : t('colorScheme.lightThemes')"></span>
</h3>
<div class="color-scheme-grid">
<div v-for="scheme in colorSchemes[isDarkMode ? 'dark' : 'light']"
:key="scheme.id"
@click="selectColorScheme(scheme.id)"
:class="['color-scheme-option', currentColorScheme === scheme.id ? 'active' : '']">
<div class="color-scheme-preview">
<div :class="`preview-${isDarkMode ? 'dark-' : ''}${scheme.id}-primary color-scheme-preview-segment`"></div>
<div :class="`preview-${isDarkMode ? 'dark-' : ''}${scheme.id}-secondary color-scheme-preview-segment`"></div>
<div :class="`preview-${isDarkMode ? 'dark-' : ''}${scheme.id}-tertiary color-scheme-preview-segment`"></div>
</div>
<div class="color-scheme-name" v-text="t('colorScheme.themes.' + (isDarkMode ? 'dark' : 'light') + '.' + scheme.id + '.name')"></div>
<div class="color-scheme-description" v-text="t('colorScheme.themes.' + (isDarkMode ? 'dark' : 'light') + '.' + scheme.id + '.description')"></div>
<div v-if="currentColorScheme === scheme.id" class="color-scheme-check">
<i class="fas fa-check"></i>
</div>
</div>
</div>
</div>
</div>
<div class="color-scheme-footer">
<button @click="resetColorScheme" class="color-scheme-reset-btn">
<i class="fas fa-undo mr-2"></i><span v-text="t('colorScheme.resetToDefault')"></span>
</button>
<button @click="closeColorSchemeModal" class="color-scheme-close-btn" v-text="t('common.close')">
</button>
</div>
</div>
</div>

View File

@@ -0,0 +1,132 @@
<!-- DateTime Picker Modal -->
<div v-if="showDateTimePicker" @click.self="closeDateTimePicker" class="fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center z-50 p-4 backdrop-blur-sm">
<div class="bg-[var(--bg-secondary)] rounded-xl shadow-2xl w-full max-w-sm overflow-hidden">
<!-- Header -->
<div class="p-4 border-b border-[var(--border-primary)] bg-[var(--bg-tertiary)]">
<div class="flex items-center justify-between">
<h3 class="text-lg font-semibold text-[var(--text-primary)]">
<i class="fas fa-calendar-alt mr-2 text-[var(--text-accent)]"></i>
Select Date & Time
</h3>
<button @click="closeDateTimePicker" class="text-[var(--text-muted)] hover:text-[var(--text-primary)] transition-colors p-1">
<i class="fas fa-times text-lg"></i>
</button>
</div>
</div>
<!-- Calendar Section -->
<div class="p-4">
<!-- Month/Year Navigation -->
<div class="flex items-center justify-between mb-4">
<button @click="prevMonth" class="p-2 rounded-lg hover:bg-[var(--bg-tertiary)] text-[var(--text-muted)] hover:text-[var(--text-primary)] transition-colors">
<i class="fas fa-chevron-left"></i>
</button>
<div class="flex items-center gap-2">
<select v-model="pickerMonth" @change="updatePickerView"
class="pl-3 pr-8 py-1.5 border border-[var(--border-secondary)] rounded-lg text-sm bg-[var(--bg-input)] text-[var(--text-primary)] focus:outline-none focus:ring-2 focus:ring-[var(--ring-focus)]">
<option v-for="(month, index) in monthNames" :key="index" :value="index">${month}</option>
</select>
<select v-model="pickerYear" @change="updatePickerView"
class="pl-3 pr-8 py-1.5 border border-[var(--border-secondary)] rounded-lg text-sm bg-[var(--bg-input)] text-[var(--text-primary)] focus:outline-none focus:ring-2 focus:ring-[var(--ring-focus)]">
<option v-for="year in availableYears" :key="year" :value="year">${year}</option>
</select>
</div>
<button @click="nextMonth" class="p-2 rounded-lg hover:bg-[var(--bg-tertiary)] text-[var(--text-muted)] hover:text-[var(--text-primary)] transition-colors">
<i class="fas fa-chevron-right"></i>
</button>
</div>
<!-- Day Names Header -->
<div class="grid grid-cols-7 gap-1 mb-2">
<div v-for="day in dayNames" :key="day" class="text-center text-xs font-medium text-[var(--text-muted)] py-1">
${day}
</div>
</div>
<!-- Calendar Grid -->
<div class="grid grid-cols-7 gap-1">
<button v-for="(day, index) in calendarDays" :key="index"
@click="day.inMonth ? selectDate(day.date) : null"
:disabled="!day.inMonth"
:class="[
'aspect-square flex items-center justify-center rounded-lg text-sm transition-all',
day.inMonth ? 'hover:bg-[var(--bg-tertiary)]' : 'opacity-30 cursor-default',
day.isSelected ? 'bg-[var(--text-accent)] text-white font-bold' : '',
day.isToday && !day.isSelected ? 'ring-2 ring-[var(--text-accent)] ring-inset' : '',
day.inMonth && !day.isSelected ? 'text-[var(--text-primary)]' : ''
]">
${day.day}
</button>
</div>
<!-- Time Selection -->
<div class="mt-4 pt-4 border-t border-[var(--border-secondary)]">
<label class="block text-sm font-medium text-[var(--text-muted)] mb-2">
<i class="fas fa-clock mr-1"></i> Time
</label>
<div class="flex items-center justify-center gap-2">
<select v-model="pickerHour"
class="pl-3 pr-8 py-2 border border-[var(--border-secondary)] rounded-lg bg-[var(--bg-input)] text-[var(--text-primary)] focus:outline-none focus:ring-2 focus:ring-[var(--ring-focus)]">
<option v-for="h in hours12" :key="h.value" :value="h.value">${h.label}</option>
</select>
<span class="text-xl font-bold text-[var(--text-muted)]">:</span>
<select v-model="pickerMinute"
class="pl-3 pr-8 py-2 border border-[var(--border-secondary)] rounded-lg bg-[var(--bg-input)] text-[var(--text-primary)] focus:outline-none focus:ring-2 focus:ring-[var(--ring-focus)]">
<option v-for="m in minutes" :key="m" :value="m">${m.toString().padStart(2, '0')}</option>
</select>
<select v-model="pickerAmPm"
class="pl-3 pr-8 py-2 border border-[var(--border-secondary)] rounded-lg bg-[var(--bg-input)] text-[var(--text-primary)] focus:outline-none focus:ring-2 focus:ring-[var(--ring-focus)]">
<option value="AM">AM</option>
<option value="PM">PM</option>
</select>
</div>
</div>
<!-- Quick Time Presets -->
<div class="mt-3 flex flex-wrap gap-1.5 justify-center">
<button @click="pickerHour = 9; pickerMinute = 0; pickerAmPm = 'AM'" class="px-2 py-1 text-xs bg-[var(--bg-tertiary)] text-[var(--text-muted)] rounded hover:bg-[var(--bg-input)] hover:text-[var(--text-primary)] transition-colors">9 AM</button>
<button @click="pickerHour = 10; pickerMinute = 0; pickerAmPm = 'AM'" class="px-2 py-1 text-xs bg-[var(--bg-tertiary)] text-[var(--text-muted)] rounded hover:bg-[var(--bg-input)] hover:text-[var(--text-primary)] transition-colors">10 AM</button>
<button @click="pickerHour = 11; pickerMinute = 0; pickerAmPm = 'AM'" class="px-2 py-1 text-xs bg-[var(--bg-tertiary)] text-[var(--text-muted)] rounded hover:bg-[var(--bg-input)] hover:text-[var(--text-primary)] transition-colors">11 AM</button>
<button @click="pickerHour = 12; pickerMinute = 0; pickerAmPm = 'PM'" class="px-2 py-1 text-xs bg-[var(--bg-tertiary)] text-[var(--text-muted)] rounded hover:bg-[var(--bg-input)] hover:text-[var(--text-primary)] transition-colors">12 PM</button>
<button @click="pickerHour = 1; pickerMinute = 0; pickerAmPm = 'PM'" class="px-2 py-1 text-xs bg-[var(--bg-tertiary)] text-[var(--text-muted)] rounded hover:bg-[var(--bg-input)] hover:text-[var(--text-primary)] transition-colors">1 PM</button>
<button @click="pickerHour = 2; pickerMinute = 0; pickerAmPm = 'PM'" class="px-2 py-1 text-xs bg-[var(--bg-tertiary)] text-[var(--text-muted)] rounded hover:bg-[var(--bg-input)] hover:text-[var(--text-primary)] transition-colors">2 PM</button>
<button @click="pickerHour = 3; pickerMinute = 0; pickerAmPm = 'PM'" class="px-2 py-1 text-xs bg-[var(--bg-tertiary)] text-[var(--text-muted)] rounded hover:bg-[var(--bg-input)] hover:text-[var(--text-primary)] transition-colors">3 PM</button>
<button @click="pickerHour = 4; pickerMinute = 0; pickerAmPm = 'PM'" class="px-2 py-1 text-xs bg-[var(--bg-tertiary)] text-[var(--text-muted)] rounded hover:bg-[var(--bg-input)] hover:text-[var(--text-primary)] transition-colors">4 PM</button>
<button @click="pickerHour = 5; pickerMinute = 0; pickerAmPm = 'PM'" class="px-2 py-1 text-xs bg-[var(--bg-tertiary)] text-[var(--text-muted)] rounded hover:bg-[var(--bg-input)] hover:text-[var(--text-primary)] transition-colors">5 PM</button>
</div>
<!-- Quick Actions -->
<div class="mt-3 flex flex-wrap gap-2 justify-center">
<button @click="setToNow" class="px-3 py-1.5 text-xs bg-[var(--bg-tertiary)] text-[var(--text-secondary)] rounded-lg hover:bg-[var(--bg-input)] transition-colors">
<i class="fas fa-bolt mr-1"></i> Now
</button>
<button @click="setToToday" class="px-3 py-1.5 text-xs bg-[var(--bg-tertiary)] text-[var(--text-secondary)] rounded-lg hover:bg-[var(--bg-input)] transition-colors">
<i class="fas fa-calendar-day mr-1"></i> Today
</button>
<button @click="clearDateTime" class="px-3 py-1.5 text-xs bg-[var(--bg-tertiary)] text-[var(--text-secondary)] rounded-lg hover:bg-[var(--bg-input)] transition-colors">
<i class="fas fa-eraser mr-1"></i> Clear
</button>
</div>
</div>
<!-- Footer -->
<div class="p-4 border-t border-[var(--border-primary)] bg-[var(--bg-tertiary)]">
<div class="text-sm text-[var(--text-muted)] mb-3 text-center">
<span v-if="pickerSelectedDate" class="font-medium text-[var(--text-primary)]">
${formatPickerPreview()}
</span>
<span v-else class="italic">No date selected</span>
</div>
<div class="flex gap-2 justify-end">
<button @click="closeDateTimePicker"
class="px-4 py-2 text-[var(--text-secondary)] hover:bg-[var(--bg-secondary)] rounded-lg transition-colors">
Cancel
</button>
<button @click="applyDateTime"
class="px-4 py-2 bg-[var(--text-accent)] text-white rounded-lg hover:opacity-90 transition-opacity font-medium whitespace-nowrap">
<i class="fas fa-check mr-1"></i>Apply
</button>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,32 @@
<!-- Delete Confirmation Modal -->
<div v-if="showDeleteModal" @click.self="cancelDelete" class="fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center z-50 p-4 backdrop-blur-sm">
<div class="bg-[var(--bg-secondary)] rounded-lg shadow-xl w-full max-w-md">
<div class="p-6">
<div class="flex items-center justify-between mb-4">
<div class="flex items-center gap-4">
<i class="fas fa-exclamation-triangle text-3xl text-[var(--text-danger)]"></i>
<div>
<h3 class="text-lg font-semibold" v-text="t('modal.deleteRecording')"></h3>
<p class="text-[var(--text-muted)]" v-text="t('help.thisActionCannotBeUndone')"></p>
</div>
</div>
<button @click="cancelDelete" class="text-[var(--text-muted)] hover:text-[var(--text-primary)] transition-colors">
<i class="fas fa-times text-xl"></i>
</button>
</div>
<p class="text-[var(--text-secondary)] mb-6">
Are you sure you want to delete "${recordingToDelete?.title || 'this recording'}"?
</p>
<div class="flex justify-end gap-3">
<button @click="cancelDelete"
class="px-4 py-2 text-[var(--text-secondary)] hover:bg-[var(--bg-tertiary)] rounded-lg transition-colors">
Cancel
</button>
<button @click="deleteRecording"
class="px-4 py-2 bg-[var(--bg-danger)] text-white rounded-lg hover:bg-[var(--bg-danger-hover)] transition-colors">
Delete
</button>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,40 @@
<!-- Duplicates List Modal -->
<div v-if="showDuplicatesModal && duplicatesModalData" @click.self="showDuplicatesModal = false" class="fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center z-50 p-4 backdrop-blur-sm">
<div class="bg-[var(--bg-secondary)] rounded-xl shadow-2xl w-full max-w-sm">
<div class="p-5">
<div class="flex items-center justify-between mb-4">
<div class="flex items-center gap-2.5">
<div class="flex items-center justify-center h-9 w-9 rounded-full bg-amber-100 dark:bg-amber-900/50">
<i class="fas fa-copy text-amber-500 dark:text-amber-400"></i>
</div>
<h3 class="text-base font-semibold text-[var(--text-primary)]">
${ duplicatesModalData.total_copies } ${ t('upload.copies') || 'copies' }
</h3>
</div>
<button @click="showDuplicatesModal = false" class="text-[var(--text-muted)] hover:text-[var(--text-primary)] transition-colors">
<i class="fas fa-times"></i>
</button>
</div>
<div class="space-y-1">
<button v-for="copy in duplicatesModalData.copies" :key="copy.id"
@click="navigateToDuplicate(copy.id)"
class="w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-left transition-colors"
:class="copy.is_self
? 'bg-[var(--bg-accent-light)] border border-[var(--border-focus)]'
: 'hover:bg-[var(--bg-tertiary)]'">
<i class="fas fa-file-audio text-sm flex-shrink-0"
:class="copy.is_self ? 'text-[var(--text-accent)]' : 'text-[var(--text-muted)]'"></i>
<div class="min-w-0 flex-1">
<div class="text-sm font-medium truncate"
:class="copy.is_self ? 'text-[var(--text-accent)]' : 'text-[var(--text-primary)]'">
${ copy.title }
<span v-if="copy.is_self" class="text-xs font-normal text-[var(--text-muted)] ml-1">(current)</span>
</div>
<div class="text-xs text-[var(--text-muted)]">${ copy.created_at }</div>
</div>
<i v-if="!copy.is_self" class="fas fa-chevron-right text-xs text-[var(--text-muted)]"></i>
</button>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,50 @@
<!-- Edit Modal -->
<div v-if="showEditModal" @click.self="cancelEdit" class="fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center z-50 p-4 backdrop-blur-sm">
<div class="bg-[var(--bg-secondary)] rounded-lg shadow-xl w-full max-w-lg">
<div class="p-6 border-b border-[var(--border-primary)] flex items-center justify-between">
<h3 class="text-lg font-semibold" v-text="t('modal.editRecording')"></h3>
<button @click="cancelEdit" class="text-[var(--text-muted)] hover:text-[var(--text-primary)] transition-colors">
<i class="fas fa-times text-xl"></i>
</button>
</div>
<div class="p-6 space-y-4">
<div>
<label class="block text-sm font-medium mb-2" v-text="t('form.title')"></label>
<input v-model="editingRecording.title"
class="w-full px-3 py-2 bg-[var(--bg-input)] border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--ring-focus)]">
</div>
<div>
<label class="block text-sm font-medium mb-2" v-text="t('form.participants')"></label>
<input v-model="editingRecording.participants"
class="w-full px-3 py-2 bg-[var(--bg-input)] border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--ring-focus)]">
</div>
<div>
<label class="block text-sm font-medium mb-2" v-text="t('form.meetingDate')"></label>
<div @click="openDateTimePicker('edit_modal_meeting_date', editingRecording.meeting_date, (isoString) => { editingRecording.meeting_date = isoString; })"
class="w-full px-3 py-2 bg-[var(--bg-input)] border border-[var(--border-secondary)] rounded-lg cursor-pointer hover:border-[var(--border-focus)] transition-colors flex items-center justify-between">
<span :class="editingRecording.meeting_date ? 'text-[var(--text-primary)]' : 'text-[var(--text-muted)]'">
${editingRecording.meeting_date ? formatDisplayDate(editingRecording.meeting_date) : 'Select date and time...'}
</span>
<i class="fas fa-calendar-alt text-[var(--text-muted)]"></i>
</div>
</div>
<div>
<label class="block text-sm font-medium mb-2" v-text="t('form.notes')"></label>
<textarea v-model="editingRecording.notes"
rows="4"
class="w-full px-3 py-2 bg-[var(--bg-input)] border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--ring-focus)]">
</textarea>
</div>
</div>
<div class="p-6 border-t border-[var(--border-primary)] flex justify-end gap-3">
<button @click="cancelEdit"
class="px-4 py-2 text-[var(--text-secondary)] hover:bg-[var(--bg-tertiary)] rounded-lg transition-colors">
Cancel
</button>
<button @click="saveEdit"
class="px-4 py-2 bg-[var(--bg-button)] text-[var(--text-button)] rounded-lg hover:bg-[var(--bg-button-hover)] transition-colors">
Save Changes
</button>
</div>
</div>
</div>

View File

@@ -0,0 +1,61 @@
<!-- Edit Participants Modal -->
<div v-if="showEditParticipantsModal" @click.self="closeEditParticipantsModal" class="fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center z-50 p-4 backdrop-blur-sm">
<div @click="closeAllParticipantSuggestions" class="bg-[var(--bg-secondary)] rounded-xl shadow-2xl w-full max-w-md max-h-[80vh] flex flex-col">
<div class="p-6 border-b border-[var(--border-primary)] flex-shrink-0">
<div class="flex items-center justify-between">
<h3 class="text-lg font-semibold" v-text="t('modal.editParticipants')"></h3>
<button @click="closeEditParticipantsModal" class="text-[var(--text-muted)] hover:text-[var(--text-primary)] transition-colors">
<i class="fas fa-times text-xl"></i>
</button>
</div>
</div>
<div class="p-6 overflow-y-auto flex-1" @scroll="closeAllParticipantSuggestions">
<p class="text-sm text-[var(--text-muted)] mb-4">Manage participants for this recording.</p>
<div class="space-y-2">
<div v-for="(participant, index) in editingParticipantsList" :key="index" class="relative">
<div class="flex items-center gap-2">
<div class="flex-1 relative" @click.stop>
<input v-model="participant.name"
@input="filterParticipantSuggestions(index)"
@focus="filterParticipantSuggestions(index)"
@blur="closeParticipantSuggestionsDelayed(index)"
type="text"
class="w-full px-3 py-2 bg-[var(--bg-input)] border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--ring-focus)] text-sm"
placeholder="Participant name...">
</div>
<!-- Teleported Suggestions dropdown -->
<teleport to="body">
<div v-if="editingParticipantSuggestions[index]?.length > 0"
class="fixed z-[100] bg-[var(--bg-secondary)] border border-[var(--border-primary)] rounded-lg shadow-lg max-h-32 overflow-y-auto"
:style="getParticipantDropdownPosition(index)">
<button v-for="suggestion in editingParticipantSuggestions[index]" :key="suggestion"
@mousedown="selectParticipantSuggestion(index, suggestion)"
class="w-full px-3 py-2 text-left hover:bg-[var(--bg-tertiary)] transition-colors text-sm">
${suggestion}
</button>
</div>
</teleport>
<button @click="removeParticipant(index)"
class="p-1 text-[var(--text-muted)] hover:text-red-500 transition-colors">
<i class="fas fa-times"></i>
</button>
</div>
</div>
</div>
<button @click="addParticipant"
class="mt-4 w-full px-3 py-2 border border-dashed border-[var(--border-secondary)] rounded-lg text-sm text-[var(--text-muted)] hover:border-[var(--border-accent)] hover:text-[var(--text-accent)] transition-colors">
<i class="fas fa-plus mr-2"></i>Add Participant
</button>
</div>
<div class="p-6 border-t border-[var(--border-primary)] flex justify-end gap-3">
<button @click="closeEditParticipantsModal" class="px-4 py-2 text-[var(--text-secondary)] hover:bg-[var(--bg-tertiary)] rounded-lg transition-colors">
Cancel
</button>
<button @click="saveParticipants" class="px-4 py-2 bg-[var(--bg-button)] text-[var(--text-button)] rounded-lg hover:bg-[var(--bg-button-hover)] transition-colors">
Save Changes
</button>
</div>
</div>
</div>

View File

@@ -0,0 +1,63 @@
<!-- Edit Speakers Modal -->
<div v-if="showEditSpeakersModal" @click.self="closeEditSpeakersModal" class="fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center z-50 p-4 backdrop-blur-sm">
<div class="bg-[var(--bg-secondary)] rounded-xl shadow-2xl w-full max-w-md max-h-[80vh] flex flex-col">
<div class="p-6 border-b border-[var(--border-primary)] flex-shrink-0">
<div class="flex items-center justify-between">
<h3 class="text-lg font-semibold" v-text="t('modal.editSpeakers')"></h3>
<button @click="closeEditSpeakersModal" class="text-[var(--text-muted)] hover:text-[var(--text-primary)] transition-colors">
<i class="fas fa-times text-xl"></i>
</button>
</div>
</div>
<div class="p-6 overflow-y-auto flex-1">
<p class="text-sm text-[var(--text-muted)] mb-4">Rename speakers in the transcript.</p>
<div class="space-y-3">
<div v-for="(speaker, index) in editingSpeakersList" :key="index" class="relative">
<div class="flex items-center gap-2">
<span class="w-24 text-sm text-[var(--text-muted)] truncate">${speaker.original || 'New'}</span>
<i class="fas fa-arrow-right text-[var(--text-muted)]"></i>
<div class="flex-1 relative" :ref="'editSpeakerInput' + index">
<input v-model="speaker.current"
@input="filterEditingSpeakerSuggestions(index)"
@focus="filterEditingSpeakerSuggestions(index)"
@blur="onEditSpeakerBlur(index)"
type="text"
class="w-full px-3 py-2 bg-[var(--bg-input)] border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--ring-focus)] text-sm"
placeholder="New name...">
</div>
<!-- Teleported Suggestions dropdown -->
<teleport to="body">
<div v-if="editingSpeakerSuggestions[index]?.length > 0"
class="fixed z-[100] bg-[var(--bg-secondary)] border border-[var(--border-primary)] rounded-lg shadow-lg max-h-32 overflow-y-auto"
:style="getEditSpeakerDropdownPosition(index)">
<button v-for="suggestion in editingSpeakerSuggestions[index]" :key="suggestion.id"
@mousedown="selectEditingSpeakerSuggestion(index, suggestion.name)"
class="w-full px-3 py-2 text-left hover:bg-[var(--bg-tertiary)] transition-colors text-sm">
${suggestion.name}
</button>
</div>
</teleport>
<button @click="removeEditingSpeaker(index)"
class="p-1 text-[var(--text-muted)] hover:text-red-500 transition-colors">
<i class="fas fa-times"></i>
</button>
</div>
</div>
</div>
<button @click="addEditingSpeaker"
class="mt-4 w-full px-3 py-2 border border-dashed border-[var(--border-secondary)] rounded-lg text-sm text-[var(--text-muted)] hover:border-[var(--border-accent)] hover:text-[var(--text-accent)] transition-colors">
<i class="fas fa-plus mr-2"></i>Add Speaker
</button>
</div>
<div class="p-6 border-t border-[var(--border-primary)] flex justify-end gap-3">
<button @click="closeEditSpeakersModal" class="px-4 py-2 text-[var(--text-secondary)] hover:bg-[var(--bg-tertiary)] rounded-lg transition-colors">
Cancel
</button>
<button @click="saveEditingSpeakers" class="px-4 py-2 bg-[var(--bg-button)] text-[var(--text-button)] rounded-lg hover:bg-[var(--bg-button-hover)] transition-colors">
Save Changes
</button>
</div>
</div>
</div>

View File

@@ -0,0 +1,85 @@
<!-- Edit Tags Modal -->
<div v-if="showEditTagsModal" @click.self="closeEditTagsModal" class="fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center z-50 p-4 backdrop-blur-sm">
<div class="bg-[var(--bg-secondary)] rounded-xl shadow-2xl w-full max-w-md max-h-[80vh] flex flex-col">
<div class="p-6 border-b border-[var(--border-primary)] flex-shrink-0">
<div class="flex items-center justify-between">
<h3 class="text-lg font-semibold" v-text="t('modal.editTags')"></h3>
<button @click="closeEditTagsModal" class="text-[var(--text-muted)] hover:text-[var(--text-primary)] transition-colors">
<i class="fas fa-times text-xl"></i>
</button>
</div>
</div>
<div class="p-6 overflow-y-auto flex-1">
<!-- Current Tags with Drag Reorder -->
<div class="mb-4">
<label class="block text-sm font-medium text-[var(--text-secondary)] mb-2" v-text="t('tags.currentTags')"></label>
<div v-if="editingRecording && editingRecording.tags && editingRecording.tags.length > 0"
class="flex flex-wrap gap-2"
@touchmove="handleModalTagTouchMove">
<span v-for="(tag, index) in editingRecording.tags" :key="tag.id"
:data-modal-tag-index="index"
draggable="true"
@dragstart="handleModalTagDragStart(index, $event)"
@dragover="handleModalTagDragOver(index, $event)"
@drop="handleModalTagDrop(index, $event)"
@dragend="handleModalTagDragEnd"
@touchstart="handleModalTagTouchStart(index, $event)"
@touchend="handleModalTagTouchEnd"
:class="[
'inline-flex items-center px-3 py-1 rounded-full text-sm font-medium transition-all duration-150',
modalDraggedTagIndex === index ? 'opacity-50 cursor-grabbing' : 'cursor-grab',
modalDragOverTagIndex === index && modalDraggedTagIndex !== index ? 'ring-2 ring-[var(--ring-focus)] ring-offset-1' : ''
]"
:style="{ backgroundColor: tag.color, color: getContrastTextColor(tag.color) }"
:title="(tag.group_id ? ('Group: ' + tag.group_name) : tag.name) + ' (drag to reorder)'">
<span class="opacity-75 mr-1 text-xs">${index + 1}.</span>
<i v-if="tag.group_id" class="fas fa-users mr-1.5 text-xs"></i>
<span v-if="tag.group_id" class="opacity-75">${ tag.group_name }: </span>${ tag.name }
<button @click.stop="removeTagFromRecording(tag.id)" class="ml-2 hover:opacity-80">
<i class="fas fa-times text-xs"></i>
</button>
</span>
</div>
<p v-else class="text-sm text-[var(--text-muted)] italic" v-text="t('tags.noTags')"></p>
<p v-if="editingRecording && editingRecording.tags && editingRecording.tags.length > 1" class="text-xs text-[var(--text-muted)] mt-2">
<i class="fas fa-grip-vertical mr-1" style="font-size: 10px;"></i>
${ t('help.dragToReorder') } &bull; ${ t('help.firstTagDefaultsApplied') }
</p>
</div>
<!-- Add Tags -->
<div>
<label class="block text-sm font-medium text-[var(--text-secondary)] mb-2" v-text="t('tags.addTag')"></label>
<!-- Search -->
<div class="relative mb-3">
<input v-model="tagSearchFilter" type="text" :placeholder="t('tagsModal.searchTags')"
class="w-full px-3 py-2 pl-9 bg-[var(--bg-input)] border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--ring-focus)]">
<i class="fas fa-search absolute left-3 top-1/2 transform -translate-y-1/2 text-[var(--text-muted)]"></i>
</div>
<!-- Available Tags -->
<div class="max-h-48 overflow-y-auto space-y-2">
<button v-for="tag in filteredAvailableTagsForModal" :key="tag.id"
@click="addTagToRecording(tag.id)"
class="w-full flex items-center justify-between px-3 py-2 rounded-lg border border-[var(--border-secondary)] hover:bg-[var(--bg-tertiary)] transition-colors">
<div class="flex items-center gap-2">
<i v-if="tag.group_id" class="fas fa-users text-xs flex-shrink-0" :style="{ color: tag.color }"></i>
<span v-else class="w-3 h-3 rounded-full flex-shrink-0" :style="{ backgroundColor: tag.color }"></span>
<span class="text-sm">
<span v-if="tag.group_id" class="opacity-75">${ tag.group_name }: </span>${ tag.name }
</span>
</div>
<i class="fas fa-plus text-[var(--text-muted)]"></i>
</button>
</div>
<p v-if="filteredAvailableTagsForModal.length === 0" class="text-sm text-[var(--text-muted)] text-center py-4" v-text="t('tags.noAvailableTags')"></p>
</div>
</div>
<div class="p-6 border-t border-[var(--border-primary)] flex justify-end">
<button @click="closeEditTagsModal" class="px-4 py-2 bg-[var(--bg-button)] text-[var(--text-button)] rounded-lg hover:bg-[var(--bg-button-hover)] transition-colors">
<span v-text="t('tagsModal.done')"></span>
</button>
</div>
</div>
</div>

View File

@@ -0,0 +1,38 @@
<!-- Edit Text Modal -->
<div v-if="showEditTextModal" @click.self="closeEditTextModal" class="fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center z-50 p-4 backdrop-blur-sm">
<div class="bg-[var(--bg-secondary)] rounded-xl shadow-2xl w-full max-w-2xl">
<div class="p-6 border-b border-[var(--border-primary)]">
<div class="flex items-center justify-between">
<h3 class="text-xl font-bold text-[var(--text-primary)]">Edit Transcript Text</h3>
<button @click="closeEditTextModal" class="text-[var(--text-muted)] hover:text-[var(--text-primary)] transition-colors">
<i class="fas fa-times text-xl"></i>
</button>
</div>
</div>
<div class="p-6">
<!-- Text editor -->
<div class="mb-6">
<label class="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Text Content
</label>
<textarea
v-model="editedText"
rows="6"
class="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-md shadow-sm focus:outline-none focus:ring-[var(--border-focus)] focus:border-[var(--border-focus)] sm:text-sm bg-[var(--bg-input)] text-[var(--text-primary)] resize-y"
placeholder="Enter transcript text..."
></textarea>
</div>
<!-- Action buttons -->
<div class="flex justify-end space-x-3">
<button @click="closeEditTextModal" class="px-4 py-2 bg-[var(--bg-secondary)] text-[var(--text-secondary)] rounded-lg border border-[var(--border-secondary)] hover:bg-[var(--bg-tertiary)] transition-colors">
Cancel
</button>
<button @click="saveEditedText" class="px-4 py-2 bg-[var(--bg-accent)] text-[var(--text-accent)] rounded-lg hover:bg-[var(--bg-accent-hover)] transition-colors">
Save Changes
</button>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,18 @@
<!-- Global Error Display - DEPRECATED: Now using toast system for all errors -->
<!-- Keeping this commented out in case we need to revert
<div v-if="globalError"
class="fixed top-4 right-4 text-white p-4 rounded-lg shadow-lg z-[60] max-w-md border"
style="background-color: #ef4444; border-color: #dc2626;">
<div class="flex items-start gap-3">
<i class="fas fa-exclamation-circle text-xl flex-shrink-0 mt-0.5"></i>
<div class="flex-1">
<p class="font-semibold mb-1" v-text="t('common.error')"></p>
<p class="text-sm">${globalError}</p>
</div>
<button @click="globalError = null"
class="text-white hover:opacity-80 transition-opacity flex-shrink-0">
<i class="fas fa-times"></i>
</button>
</div>
</div>
-->

View File

@@ -0,0 +1,36 @@
<!-- Recording Disclaimer Modal -->
<div v-if="showRecordingDisclaimerModal" @click.self="cancelRecordingDisclaimer" class="fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center z-50 p-4 backdrop-blur-sm">
<div class="bg-[var(--bg-secondary)] rounded-lg shadow-xl w-full max-w-2xl max-h-[80vh] flex flex-col overflow-hidden">
<div class="flex-shrink-0 p-6 border-b border-[var(--border-primary)]">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<i class="fas fa-info-circle text-2xl text-[var(--text-accent)]"></i>
<h3 class="text-xl font-semibold text-[var(--text-primary)]" v-text="t('modal.recordingNotice')"></h3>
</div>
<button @click="cancelRecordingDisclaimer" class="text-[var(--text-muted)] hover:text-[var(--text-primary)] transition-colors">
<i class="fas fa-times text-xl"></i>
</button>
</div>
</div>
<div class="flex-1 overflow-y-auto p-6">
<!-- Render markdown content -->
<div class="ai-message text-[var(--text-secondary)]"
style="line-height: 1.6;">
<div v-html="recordingDisclaimerHtml"></div>
</div>
</div>
<div class="flex-shrink-0 p-6 border-t border-[var(--border-primary)] bg-[var(--bg-tertiary)] rounded-b-lg">
<div class="flex justify-end gap-3">
<button @click="cancelRecordingDisclaimer"
class="px-6 py-2 text-[var(--text-secondary)] hover:bg-[var(--bg-secondary)] rounded-lg transition-colors border border-[var(--border-secondary)]">
Cancel
</button>
<button @click="acceptRecordingDisclaimer"
class="px-6 py-2 bg-[var(--bg-accent)] text-white rounded-lg hover:bg-[var(--bg-accent-hover)] transition-colors">
<i class="fas fa-microphone mr-2"></i>
Start Recording
</button>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,64 @@
<!-- Recording Recovery Modal -->
<div v-if="showRecoveryModal" @click.self="cancelRecovery" class="fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center z-50 p-4 backdrop-blur-sm">
<div class="bg-[var(--bg-secondary)] rounded-xl shadow-2xl w-full max-w-md max-h-[85vh] overflow-y-auto">
<div class="p-6 border-b border-[var(--border-primary)]">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<i class="fas fa-history text-2xl text-blue-500"></i>
<h3 class="text-lg font-semibold" v-text="t('recording.recoveryTitle')"></h3>
</div>
<button @click="cancelRecovery" class="text-[var(--text-muted)] hover:text-[var(--text-primary)] transition-colors">
<i class="fas fa-times text-xl"></i>
</button>
</div>
</div>
<div class="p-6 space-y-4">
<div class="p-4 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg">
<div class="flex items-start gap-3">
<i class="fas fa-info-circle text-blue-600 dark:text-blue-400 mt-0.5"></i>
<div class="text-sm text-blue-700 dark:text-blue-400">
<p class="font-medium mb-1">${ t('recording.recoveryFound') }</p>
<p>${ t('recording.recoveryDescription') }</p>
</div>
</div>
</div>
<div v-if="recoverableRecording" class="space-y-2 text-sm">
<div class="flex justify-between">
<span class="text-[var(--text-muted)]">${ t('recording.recordingMode') }:</span>
<span class="font-medium">${ formatRecordingMode(recoverableRecording.mode) }</span>
</div>
<div class="flex justify-between">
<span class="text-[var(--text-muted)]">${ t('recording.duration') }:</span>
<span class="font-medium">${ formatTime(recoverableRecording.duration) }</span>
</div>
<div class="flex justify-between">
<span class="text-[var(--text-muted)]">${ t('recording.size') }:</span>
<span class="font-medium">${ formatFileSize(recoverableRecording.totalSize) }</span>
</div>
<div class="flex justify-between">
<span class="text-[var(--text-muted)]">${ t('recording.startedAt') }:</span>
<span class="font-medium">${ formatDateTime(recoverableRecording.startTime) }</span>
</div>
<div v-if="recoverableRecording.notes" class="pt-2 border-t border-[var(--border-secondary)]">
<span class="text-[var(--text-muted)]">${ t('recording.notes') }:</span>
<p class="mt-1 text-[var(--text-primary)] max-h-32 overflow-y-auto text-xs whitespace-pre-wrap">${ recoverableRecording.notes }</p>
</div>
</div>
</div>
<div class="p-6 border-t border-[var(--border-primary)] flex gap-3">
<button
@click="cancelRecovery"
class="flex-1 px-4 py-2.5 bg-[var(--bg-tertiary)] text-[var(--text-primary)] rounded-lg hover:bg-[var(--bg-hover)] transition-colors font-medium">
<i class="fas fa-trash mr-2"></i>${ t('recording.discardRecovery') }
</button>
<button
@click="recoverRecording"
class="flex-1 px-4 py-2.5 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors font-medium">
<i class="fas fa-undo mr-2"></i>${ t('recording.restoreRecording') }
</button>
</div>
</div>
</div>

View File

@@ -0,0 +1,184 @@
<!-- Reprocess Modal -->
<div v-if="showReprocessModal" @click.self="cancelReprocess" class="fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center z-50 p-4 backdrop-blur-sm transition-all duration-300 ease-in-out">
<div class="bg-[var(--bg-secondary)] rounded-xl shadow-2xl w-full max-w-md max-h-[90vh] flex flex-col transform transition-all duration-300 ease-in-out">
<!-- Header with gradient background -->
<div class="bg-gradient-to-r from-[var(--bg-accent)] to-[var(--bg-secondary)] p-5 rounded-t-xl flex-shrink-0">
<div class="flex items-center justify-between">
<div class="flex items-center">
<div class="w-12 h-12 bg-gradient-to-br from-blue-500 to-purple-600 rounded-full flex items-center justify-center mr-4 shadow-lg">
<i class="fas fa-sync-alt text-white text-lg"></i>
</div>
<div>
<h3 class="text-xl font-bold text-[var(--text-primary)] mb-1" v-text="t('help.confirmReprocessingTitle')"></h3>
<p class="text-sm text-[var(--text-muted)] capitalize">${ reprocessType } reprocessing</p>
</div>
</div>
<button @click="cancelReprocess" class="text-[var(--text-muted)] hover:text-[var(--text-primary)] transition-colors">
<i class="fas fa-times text-xl"></i>
</button>
</div>
</div>
<!-- Content -->
<div class="p-6 modal-content overflow-y-auto flex-1">
<div v-if="reprocessRecording" class="mb-6">
<div class="bg-[var(--bg-tertiary)] rounded-lg p-4 border border-[var(--border-primary)]">
<div class="flex items-start space-x-3">
<i class="fas fa-file-audio text-[var(--text-accent)] mt-1"></i>
<div class="flex-1 min-w-0">
<h4 class="font-medium text-[var(--text-primary)] truncate" :title="reprocessRecording.title">
${ reprocessRecording.title || 'Untitled Recording' }
</h4>
<p class="text-sm text-[var(--text-muted)] mt-1">
Created: ${ new Date(reprocessRecording.created_at).toLocaleDateString() }
</p>
</div>
</div>
</div>
</div>
<div class="space-y-4">
<div class="flex items-start space-x-3">
<div class="w-6 h-6 bg-amber-100 rounded-full flex items-center justify-center flex-shrink-0 mt-0.5">
<i class="fas fa-exclamation-triangle text-amber-600 text-xs"></i>
</div>
<div>
<p class="text-[var(--text-secondary)] font-medium mb-1" v-text="t('help.whatWillHappen')"></p>
<p class="text-sm text-[var(--text-muted)]" v-if="reprocessType === 'transcription' && !connectorSupportsDiarization">
<span v-text="t('reprocessModal.audioReTranscribedFromScratch')"></span>
</p>
<p class="text-sm text-[var(--text-muted)]" v-else-if="reprocessType === 'transcription' && connectorSupportsDiarization">
<span v-text="t('reprocessModal.audioReTranscribedWithAsr')"></span>
</p>
<p class="text-sm text-[var(--text-muted)]" v-else-if="reprocessType === 'summary'">
<span v-text="t('reprocessModal.newTitleAndSummary')"></span>
</p>
</div>
</div>
<!-- Language Selection - show for all transcription reprocessing -->
<div v-if="reprocessType === 'transcription'" class="space-y-4 pt-4 border-t border-[var(--border-primary)]">
<div>
<label for="asr-language-reprocess" class="block text-sm font-medium text-[var(--text-muted)]" v-text="t('form.language')"></label>
<select id="asr-language-reprocess" v-model="asrReprocessOptions.language" class="mt-1 block w-full px-3 py-2 border border-[var(--border-secondary)] rounded-md shadow-sm focus:outline-none focus:ring-[var(--border-focus)] focus:border-[var(--border-focus)] sm:text-sm bg-[var(--bg-input)] text-[var(--text-primary)]">
<option v-for="lang in languageOptions" :key="lang.value" :value="lang.value" v-text="lang.label"></option>
</select>
</div>
</div>
<!-- Diarization Options (for connectors that support it) -->
<div v-if="reprocessType === 'transcription' && connectorSupportsDiarization && connectorSupportsSpeakerCount" class="space-y-4 pt-4 border-t border-[var(--border-primary)]">
<h4 class="text-md font-semibold text-[var(--text-secondary)]" v-text="t('help.advancedAsrOptions')"></h4>
<!-- Min/Max Speakers - only show for connectors that support it (ASR endpoint, not OpenAI) -->
<div>
<label for="asr-min-speakers-reprocess" class="block text-sm font-medium text-[var(--text-muted)]" v-text="t('form.minSpeakers')"></label>
<input type="number" id="asr-min-speakers-reprocess" v-model.number="asrReprocessOptions.min_speakers" class="mt-1 block w-full px-3 py-2 border border-[var(--border-secondary)] rounded-md shadow-sm focus:outline-none focus:ring-[var(--border-focus)] focus:border-[var(--border-focus)] sm:text-sm bg-[var(--bg-input)] text-[var(--text-primary)]" :placeholder="t('form.optional')">
</div>
<div>
<label for="asr-max-speakers-reprocess" class="block text-sm font-medium text-[var(--text-muted)]" v-text="t('form.maxSpeakers')"></label>
<input type="number" id="asr-max-speakers-reprocess" v-model.number="asrReprocessOptions.max_speakers" class="mt-1 block w-full px-3 py-2 border border-[var(--border-secondary)] rounded-md shadow-sm focus:outline-none focus:ring-[var(--border-focus)] focus:border-[var(--border-focus)] sm:text-sm bg-[var(--bg-input)] text-[var(--text-primary)]" :placeholder="t('form.optional')">
</div>
</div>
<!-- Custom Prompt Options for Summary Reprocessing -->
<div v-if="reprocessType === 'summary'" class="space-y-4 pt-4 border-t border-[var(--border-primary)]">
<h4 class="text-md font-semibold text-[var(--text-secondary)]">Custom Summarization Prompt</h4>
<!-- Prompt Source Selection -->
<div>
<label class="block text-sm font-medium text-[var(--text-muted)] mb-2">Prompt Source</label>
<div class="space-y-2">
<label class="flex items-center space-x-2 cursor-pointer">
<input type="radio" v-model="summaryReprocessPromptSource" value="default" class="text-[var(--text-accent)]" name="promptSource">
<span class="text-sm text-[var(--text-secondary)]">Use default prompt</span>
</label>
<label class="flex items-center space-x-2 cursor-pointer" v-if="tagsWithCustomPrompts.length > 0">
<input type="radio" v-model="summaryReprocessPromptSource" value="tag" class="text-[var(--text-accent)]" name="promptSource">
<span class="text-sm text-[var(--text-secondary)]">Use prompt from tag</span>
</label>
<label class="flex items-center space-x-2 cursor-pointer">
<input type="radio" v-model="summaryReprocessPromptSource" value="custom" class="text-[var(--text-accent)]" name="promptSource">
<span class="text-sm text-[var(--text-secondary)]">Enter custom prompt</span>
</label>
</div>
</div>
<!-- Tag Selection (shown when 'tag' is selected) -->
<div v-if="summaryReprocessPromptSource === 'tag' && tagsWithCustomPrompts.length > 0">
<label for="reprocess-tag-select" class="block text-sm font-medium text-[var(--text-muted)] mb-2">Select Tag</label>
<select id="reprocess-tag-select" v-model="summaryReprocessSelectedTagId" class="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-md shadow-sm focus:outline-none focus:ring-[var(--border-focus)] focus:border-[var(--border-focus)] text-sm bg-[var(--bg-input)] text-[var(--text-primary)]">
<option value="">Select a tag...</option>
<option v-for="tag in tagsWithCustomPrompts" :key="tag.id" :value="tag.id">${ tag.name }</option>
</select>
<div v-if="summaryReprocessSelectedTagId" class="mt-2 p-2 bg-[var(--bg-tertiary)] rounded text-xs text-[var(--text-muted)] max-h-20 overflow-y-auto">
<strong>Preview:</strong> ${ getTagPromptPreview(summaryReprocessSelectedTagId) }
</div>
</div>
<!-- Custom Prompt Textarea (shown when 'custom' is selected) -->
<div v-if="summaryReprocessPromptSource === 'custom'">
<label for="reprocess-custom-prompt" class="block text-sm font-medium text-[var(--text-muted)] mb-2">Custom Prompt</label>
<textarea id="reprocess-custom-prompt" v-model="summaryReprocessCustomPrompt" rows="4" class="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-md shadow-sm focus:outline-none focus:ring-[var(--border-focus)] focus:border-[var(--border-focus)] text-sm bg-[var(--bg-input)] text-[var(--text-primary)]" placeholder="Enter your custom summarization instructions..."></textarea>
</div>
<!-- Info message when no tags with custom prompts exist -->
<div v-if="summaryReprocessPromptSource === 'tag' && tagsWithCustomPrompts.length === 0" class="text-xs text-[var(--text-muted)] italic">
No tags with custom prompts available. Create tags with custom prompts in the Account Settings.
</div>
</div>
<div class="flex items-start space-x-3">
<div class="w-6 h-6 bg-blue-100 rounded-full flex items-center justify-center flex-shrink-0 mt-0.5">
<i class="fas fa-clock text-blue-600 text-xs"></i>
</div>
<div>
<p class="text-[var(--text-secondary)] font-medium mb-1" v-text="t('help.processingTime')"></p>
<p class="text-sm text-[var(--text-muted)]">
<span v-text="t('help.processingTimeDescription')"></span>
</p>
</div>
</div>
<div class="flex items-start space-x-3" v-if="reprocessType === 'transcription'">
<div class="w-6 h-6 bg-red-100 rounded-full flex items-center justify-center flex-shrink-0 mt-0.5">
<i class="fas fa-info-circle text-red-600 text-xs"></i>
</div>
<div>
<p class="text-[var(--text-secondary)] font-medium mb-1" v-text="t('help.importantNote')"></p>
<p class="text-sm text-[var(--text-muted)]">
<span v-text="t('reprocessModal.manualEditsOverwritten')"></span>
</p>
</div>
</div>
<div class="flex items-start space-x-3" v-else-if="reprocessType === 'summary'">
<div class="w-6 h-6 bg-red-100 rounded-full flex items-center justify-center flex-shrink-0 mt-0.5">
<i class="fas fa-info-circle text-red-600 text-xs"></i>
</div>
<div>
<p class="text-[var(--text-secondary)] font-medium mb-1" v-text="t('help.importantNote')"></p>
<p class="text-sm text-[var(--text-muted)]">
<span v-text="t('reprocessModal.manualEditsOverwrittenSummary')"></span>
</p>
</div>
</div>
</div>
</div>
<!-- Footer with action buttons -->
<div class="bg-[var(--bg-tertiary)] px-6 py-4 rounded-b-xl flex justify-end space-x-3 border-t border-[var(--border-primary)] flex-shrink-0">
<button
@click="cancelReprocess"
class="px-5 py-2.5 bg-[var(--bg-secondary)] text-[var(--text-secondary)] rounded-lg border border-[var(--border-secondary)] hover:bg-[var(--bg-tertiary)] transition-all duration-200 flex items-center shadow-sm font-medium">
<i class="fas fa-times mr-2"></i>
Cancel
</button>
<button
@click="executeReprocess"
class="px-5 py-2.5 bg-gradient-to-r from-blue-500 to-purple-600 text-white rounded-lg hover:from-blue-600 hover:to-purple-700 transition-all duration-200 flex items-center shadow-lg font-medium transform hover:scale-105">
<i class="fas fa-sync-alt mr-2"></i>
Start Reprocessing
</button>
</div>
</div>
</div>

View File

@@ -0,0 +1,31 @@
<!-- Reset Status Confirmation Modal -->
<div v-if="showResetModal" @click.self="cancelReset" class="fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center z-50 p-4 backdrop-blur-sm">
<div class="bg-[var(--bg-secondary)] rounded-xl shadow-2xl w-full max-w-md">
<div class="p-6">
<div class="flex items-start justify-between mb-4">
<div class="flex items-start gap-3">
<div class="flex items-center justify-center h-12 w-12 rounded-full bg-orange-100 dark:bg-orange-800">
<i class="fas fa-exclamation-triangle text-2xl text-orange-500 dark:text-orange-300"></i>
</div>
<div>
<h3 class="text-lg font-semibold text-[var(--text-primary)] mb-2" v-text="t('modal.resetStatus')"></h3>
</div>
</div>
<button @click="cancelReset" class="text-[var(--text-muted)] hover:text-[var(--text-primary)] transition-colors">
<i class="fas fa-times text-xl"></i>
</button>
</div>
<p class="text-sm text-[var(--text-muted)] mb-6">
This will mark the recording as 'Failed'. This is useful if processing is stuck. You will be able to reprocess it afterwards.
</p>
</div>
<div class="bg-[var(--bg-tertiary)] px-6 py-4 rounded-b-xl flex justify-end space-x-3">
<button @click="cancelReset" class="px-4 py-2 bg-[var(--bg-secondary)] text-[var(--text-secondary)] rounded-lg border border-[var(--border-secondary)] hover:bg-[var(--bg-tertiary)]">
Cancel
</button>
<button @click="executeReset" class="px-4 py-2 bg-orange-500 text-white rounded-lg hover:bg-orange-600">
Yes, Reset Status
</button>
</div>
</div>
</div>

View File

@@ -0,0 +1,52 @@
<!-- Share Delete Confirmation Modal -->
<div v-if="showShareDeleteModal" @click.self="cancelDeleteShare" class="fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center z-50 p-4 backdrop-blur-sm">
<div class="bg-[var(--bg-secondary)] rounded-xl shadow-2xl w-full max-w-md transform transition-all duration-300 ease-in-out">
<!-- Header with gradient background -->
<div class="bg-gradient-to-r from-red-500 to-red-600 p-5 rounded-t-xl">
<div class="flex items-center justify-between">
<div class="flex items-center">
<div class="w-12 h-12 bg-white/20 backdrop-blur rounded-full flex items-center justify-center mr-4">
<i class="fas fa-trash-alt text-white text-lg"></i>
</div>
<div>
<h3 class="text-lg font-bold text-white">Delete Shared Link</h3>
<p class="text-red-100 text-sm mt-1">This action cannot be undone</p>
</div>
</div>
<button @click="cancelDeleteShare" class="text-white/80 hover:text-white transition-colors">
<i class="fas fa-times text-xl"></i>
</button>
</div>
</div>
<!-- Content -->
<div class="p-6">
<p class="text-[var(--text-secondary)] mb-4">
Are you sure you want to delete the shared link for:
</p>
<div v-if="shareToDelete" class="bg-[var(--bg-tertiary)] p-3 rounded-lg border border-[var(--border-primary)]">
<p class="font-medium text-[var(--text-primary)]">${shareToDelete.recording_title}</p>
<p class="text-sm text-[var(--text-muted)] mt-1">Shared on: ${shareToDelete.created_at}</p>
</div>
<div class="mt-4 p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
<div class="flex items-start">
<i class="fas fa-exclamation-triangle text-red-600 dark:text-red-400 mt-0.5 mr-2"></i>
<p class="text-sm text-red-800 dark:text-red-300">
The share link will be permanently deleted and anyone with this link will no longer be able to access the recording.
</p>
</div>
</div>
</div>
<!-- Actions -->
<div class="p-6 pt-0 flex justify-end gap-3">
<button @click="cancelDeleteShare" class="px-4 py-2 text-[var(--text-secondary)] hover:bg-[var(--bg-tertiary)] rounded-lg transition-colors">
Cancel
</button>
<button @click="deleteShare" class="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors flex items-center gap-2">
<i class="fas fa-trash"></i>
Delete Share Link
</button>
</div>
</div>
</div>

View File

@@ -0,0 +1,82 @@
<!-- Share Modal -->
<div v-if="showShareModal" @click.self="closeShareModal" class="fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center z-50 p-4 backdrop-blur-sm">
<div class="bg-[var(--bg-secondary)] rounded-lg shadow-xl w-full max-w-2xl max-h-[80vh] flex flex-col">
<div class="px-6 py-4 border-b border-[var(--border-primary)] flex justify-between items-center">
<h3 class="text-lg font-semibold" v-text="t('modal.shareRecording')"></h3>
<button @click="closeShareModal" class="p-2 rounded-lg hover:bg-[var(--bg-tertiary)] transition-colors">
<i class="fas fa-times"></i>
</button>
</div>
<div class="px-6 py-4 space-y-3 overflow-y-auto flex-1">
<p class="text-xs text-[var(--text-muted)]" v-text="t('help.createPublicLink')"></p>
<!-- Share Options -->
<div class="flex items-center gap-3 mb-2">
<label class="flex items-center">
<input type="checkbox" v-model="shareOptions.share_summary" id="share_summary" class="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500">
<span class="ml-2 text-xs" v-text="t('form.shareSummary')"></span>
</label>
<label class="flex items-center">
<input type="checkbox" v-model="shareOptions.share_notes" id="share_notes" class="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500">
<span class="ml-2 text-xs" v-text="t('form.shareNotes')"></span>
</label>
</div>
<!-- Create New Share Button -->
<button @click="createShare(true)" class="w-full px-3 py-2 bg-[var(--bg-accent)] text-white rounded-lg hover:opacity-90 transition-opacity text-sm font-medium">
<i class="fas fa-plus-circle mr-1.5"></i>Create New Share Link
</button>
<!-- Loading State -->
<div v-if="isLoadingPublicShares" class="text-center py-8">
<i class="fas fa-spinner fa-spin text-2xl text-[var(--text-muted)]"></i>
<p class="mt-2 text-sm text-[var(--text-muted)]">Loading share links...</p>
</div>
<!-- Existing Shares List -->
<div v-else-if="recordingPublicShares.length > 0" class="space-y-2">
<h4 class="text-xs font-medium text-[var(--text-secondary)]">
Existing Share Links (${recordingPublicShares.length})
</h4>
<div v-for="share in recordingPublicShares" :key="share.id" class="bg-[var(--bg-tertiary)] p-2.5 rounded-lg border border-[var(--border-primary)]">
<div class="flex justify-between items-start mb-2">
<div>
<p class="text-xs text-[var(--text-muted)]">${ t('help.sharedOn') }: ${share.created_at}</p>
</div>
<button @click="confirmDeletePublicShare(share)" class="text-red-500 hover:text-red-700 p-1">
<i class="fas fa-trash text-xs"></i>
</button>
</div>
<div class="flex items-center gap-3 mb-2">
<label class="flex items-center text-xs">
<input type="checkbox" v-model="share.share_summary" @change="updateShare(share)" class="h-3.5 w-3.5 rounded border-gray-300 text-blue-600 focus:ring-blue-500">
<span class="ml-1.5" v-text="t('form.shareSummary')"></span>
</label>
<label class="flex items-center text-xs">
<input type="checkbox" v-model="share.share_notes" @change="updateShare(share)" class="h-3.5 w-3.5 rounded border-gray-300 text-blue-600 focus:ring-blue-500">
<span class="ml-1.5" v-text="t('form.shareNotes')"></span>
</label>
</div>
<div class="relative">
<input :value="share.share_url" :id="'share-link-' + share.id" readonly class="w-full px-2 py-1.5 pr-10 bg-[var(--bg-input)] border border-[var(--border-secondary)] rounded text-xs font-mono">
<button @click="copyPublicShareLinkWithFeedback(share.share_url, share.id)" class="absolute right-1 top-1/2 -translate-y-1/2 w-8 h-7 flex items-center justify-center rounded bg-[var(--bg-button)] text-[var(--text-button)] hover:bg-[var(--bg-button-hover)] transition-colors" :title="t('buttons.copy')">
<i :class="copiedShareId === share.id ? 'fas fa-check' : 'fas fa-copy'" class="text-xs"></i>
</button>
</div>
</div>
</div>
<!-- No Shares Yet -->
<div v-else-if="!isLoadingPublicShares" class="text-center py-8 text-[var(--text-muted)]">
<i class="fas fa-link text-3xl mb-2"></i>
<p v-text="t('sharedTranscripts.noSharedTranscripts')"></p>
<p class="text-xs mt-1">Click the button above to create one</p>
</div>
</div>
<div class="px-6 py-3 border-t border-[var(--border-primary)] flex justify-end">
<button @click="closeShareModal" class="px-4 py-2 bg-[var(--bg-button)] text-[var(--text-button)] rounded-lg hover:bg-[var(--bg-button-hover)] transition-colors text-sm font-medium">
Done
</button>
</div>
</div>
</div>

View File

@@ -0,0 +1,46 @@
<!-- Shares List Modal -->
<div v-if="showSharesListModal" @click.self="closeSharesList" class="fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center z-50 p-4 backdrop-blur-sm">
<div class="bg-[var(--bg-secondary)] rounded-lg shadow-xl w-full max-w-2xl max-h-[80vh] flex flex-col">
<div class="p-6 border-b border-[var(--border-primary)] flex justify-between items-center">
<h3 class="text-lg font-semibold" v-text="t('modal.sharedTranscripts')"></h3>
<button @click="closeSharesList" class="p-2 rounded-lg hover:bg-[var(--bg-tertiary)] transition-colors">
<i class="fas fa-times"></i>
</button>
</div>
<div class="p-6 space-y-4 overflow-y-auto">
<div v-if="isLoadingShares" class="text-center">
<i class="fas fa-spinner fa-spin text-2xl text-[var(--text-muted)]"></i>
</div>
<div v-else-if="userShares.length === 0" class="text-center text-[var(--text-muted)]">
<span v-text="t('sharedTranscripts.noSharedTranscripts')"></span>
</div>
<div v-else class="space-y-3">
<div v-for="share in userShares" :key="share.id" class="bg-[var(--bg-tertiary)] p-4 rounded-lg border border-[var(--border-primary)]">
<div class="flex justify-between items-start">
<div>
<p class="font-semibold">${share.recording_title}</p>
<p class="text-sm text-[var(--text-muted)]">${ t('help.sharedOn') }: ${share.created_at}</p>
</div>
<button @click="confirmDeleteShare(share)" class="text-red-500 hover:text-red-700 p-1"><i class="fas fa-trash"></i></button>
</div>
<div class="mt-4 flex items-center gap-4">
<label class="flex items-center text-sm">
<input type="checkbox" v-model="share.share_summary" @change="updateShare(share)" class="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500">
<span class="ml-2" v-text="t('form.shareSummary')"></span>
</label>
<label class="flex items-center text-sm">
<input type="checkbox" v-model="share.share_notes" @change="updateShare(share)" class="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500">
<span class="ml-2" v-text="t('form.shareNotes')"></span>
</label>
</div>
<div class="mt-4 relative">
<input :value="'{{ request.url_root }}share/' + share.public_id" :id="'share-link-' + share.id" readonly class="w-full px-3 py-2 pr-12 bg-[var(--bg-input)] border border-[var(--border-secondary)] rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-[var(--border-accent)]">
<button @click="copyIndividualShareLink(share.id)" class="absolute right-1.5 top-1/2 -translate-y-1/2 w-7 h-7 flex items-center justify-center rounded bg-[var(--bg-button)] text-[var(--text-button)] hover:bg-[var(--bg-button-hover)] transition-colors" :title="t('buttons.copy')">
<i :class="copiedShareId === share.id ? 'fas fa-check' : 'fas fa-copy'" class="text-xs"></i>
</button>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,353 @@
<!-- Speaker Identification Modal -->
<div v-if="showSpeakerModal" @click.self="closeSpeakerModal" class="fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center z-50 p-2 sm:p-4 backdrop-blur-sm">
<div @click="closeSpeakerSuggestionsOnClick" class="bg-[var(--bg-secondary)] rounded-xl shadow-2xl w-full max-w-4xl flex flex-col h-[95vh] sm:h-[85vh]">
<!-- Header -->
<div class="p-3 sm:p-5 border-b border-[var(--border-primary)] flex-shrink-0">
<div class="flex items-center justify-between">
<h3 class="text-base sm:text-xl font-bold text-[var(--text-primary)]" v-text="t('modal.identifySpeakers')"></h3>
<button @click="closeSpeakerModal" class="text-[var(--text-muted)] hover:text-[var(--text-primary)] transition-colors">
<i class="fas fa-times text-xl"></i>
</button>
</div>
<!-- Warning for too many speakers -->
<div v-if="modalSpeakers.length > 16" class="mt-3 p-3 bg-[var(--bg-warn-light)] border border-amber-300 dark:border-amber-800 rounded-lg">
<div class="flex items-start">
<i class="fas fa-exclamation-triangle text-[var(--text-warn-strong)] mt-0.5 mr-2"></i>
<div class="text-sm">
<p class="font-medium text-[var(--text-warn-strong)]" v-text="t('help.moreSpeakersThanColors')"></p>
<p class="text-[var(--text-warn-strong)] mt-1" v-text="t('help.youHaveXSpeakers', { count: modalSpeakers.length })">
</p>
</div>
</div>
</div>
</div>
<!-- Mobile Tab Switcher (hidden on lg screens) -->
<div class="lg:hidden flex border-b border-[var(--border-primary)] flex-shrink-0">
<button
@click="speakerModalTab = 'speakers'"
class="flex-1 py-2 text-xs font-medium transition-colors relative"
:class="speakerModalTab === 'speakers' ? 'text-[var(--text-accent)]' : 'text-[var(--text-muted)]'">
<i class="fas fa-users mr-1"></i>Speakers
<span v-if="speakerModalTab === 'speakers'" class="absolute bottom-0 left-0 right-0 h-0.5 bg-[var(--bg-accent)]"></span>
</button>
<button
@click="speakerModalTab = 'transcript'"
class="flex-1 py-2 text-xs font-medium transition-colors relative"
:class="speakerModalTab === 'transcript' ? 'text-[var(--text-accent)]' : 'text-[var(--text-muted)]'">
<i class="fas fa-file-lines mr-1"></i>Transcript
<span v-if="speakerModalTab === 'transcript'" class="absolute bottom-0 left-0 right-0 h-0.5 bg-[var(--bg-accent)]"></span>
</button>
</div>
<!-- Main Content - Tab-based on mobile, side-by-side on desktop -->
<div class="flex-grow flex flex-col lg:flex-row overflow-hidden modal-content">
<!-- Speaker List Panel - shown on mobile when speakers tab active, always on desktop -->
<div class="lg:w-1/3 p-3 sm:p-6 space-y-2 sm:space-y-4 overflow-y-auto custom-scrollbar lg:border-r border-[var(--border-primary)]"
:class="{ 'hidden lg:block': speakerModalTab !== 'speakers', 'flex-1': speakerModalTab === 'speakers' }">
<div v-for="(speaker, index) in modalSpeakers" :key="`${selectedRecording.id}-speaker-${index}-${speaker}`" class="space-y-1.5 sm:space-y-3">
<!-- Speaker label with color indicator and "This is Me" checkbox -->
<div class="flex items-center justify-between gap-2">
<div class="flex items-center space-x-1.5 min-w-0">
<div :class="speakerMap[speaker].color" class="w-3 h-3 sm:w-4 sm:h-4 rounded-full border border-white/30 shadow-sm flex-shrink-0"></div>
<span class="font-mono text-xs sm:text-sm text-[var(--text-muted)] truncate">${ speakerDisplayMap[speaker] || speaker }</span>
<span v-if="index >= 16" class="text-[10px] text-[var(--text-warn-strong)] bg-[var(--bg-warn-light)] px-1.5 py-0.5 rounded-full flex-shrink-0" :title="`Color repeats from speaker ${((index % 16) + 1)}`">
Repeat
</span>
</div>
<label class="flex items-center text-xs sm:text-sm text-[var(--text-muted)] cursor-pointer hover:text-[var(--text-primary)] transition-colors flex-shrink-0">
<input type="checkbox"
v-model="speakerMap[speaker].isMe"
@change="handleIsMeChange(speaker)"
@focus="highlightSpeakerInTranscript(speaker)"
@blur="clearSpeakerHighlight"
class="speaker-checkbox w-4 h-4 sm:w-4 sm:h-4">
<span class="ml-1.5 select-none" v-text="t('help.me')"></span>
</label>
</div>
<!-- Autocomplete input field -->
<div class="relative">
<input
type="text"
v-model="speakerMap[speaker].name"
@input="searchSpeakers($event.target.value, speaker)"
@focus="focusSpeaker(speaker)"
@blur="blurSpeaker()"
:disabled="speakerMap[speaker].isMe"
class="w-full px-2 py-2 sm:py-2 border border-[var(--border-secondary)] rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-[var(--ring-focus)] focus:border-[var(--border-focus)] text-sm bg-[var(--bg-input)] text-[var(--text-primary)] disabled:bg-[var(--bg-tertiary)] disabled:text-[var(--text-muted)] disabled:cursor-not-allowed"
:class="{ 'pr-28': shouldShowVoiceSuggestionPill(speaker) }"
:placeholder="speakerMap[speaker].isMe ? currentUserName : t('help.enterNameFor') + ' ' + (speakerDisplayMap[speaker] || speaker)"
autocomplete="off">
<!-- Voice match suggestion pill -->
<button v-if="shouldShowVoiceSuggestionPill(speaker)"
@click.stop="applyVoiceSuggestion(speaker, voiceSuggestions[speaker][0])"
class="absolute right-1.5 top-1/2 transform -translate-y-1/2 px-1.5 py-0.5 bg-gradient-to-r from-blue-500 to-purple-500 text-white text-[10px] rounded-full flex items-center gap-0.5 hover:from-blue-600 hover:to-purple-600 transition-all shadow-sm hover:shadow group active:scale-95">
<i class="fas fa-waveform-lines text-[9px]"></i>
<span class="font-medium truncate max-w-[50px] sm:max-w-[80px]">${ voiceSuggestions[speaker][0].name }</span>
<span class="text-[9px] opacity-90">${ voiceSuggestions[speaker][0].similarity }%</span>
</button>
<!-- Loading indicator -->
<div v-if="loadingSuggestions[speaker] && !speakerMap[speaker].isMe" class="absolute right-2 top-1/2 transform -translate-y-1/2">
<i class="fas fa-spinner fa-spin text-[var(--text-muted)] text-xs"></i>
</div>
<!-- Suggestions dropdown -->
<div v-if="activeSpeakerInput === speaker && speakerSuggestions[speaker] && speakerSuggestions[speaker].length > 0 && !speakerMap[speaker].isMe"
@click.stop
class="absolute z-10 w-full mt-1 bg-[var(--bg-secondary)] border border-[var(--border-primary)] rounded-md shadow-lg max-h-40 overflow-y-auto">
<div class="py-0.5">
<div v-for="suggestion in speakerSuggestions[speaker]"
:key="suggestion.id"
@click="selectSpeakerSuggestion(speaker, suggestion)"
class="px-2 py-1.5 cursor-pointer hover:bg-[var(--bg-tertiary)] active:bg-[var(--bg-accent)] flex items-center justify-between">
<div class="flex-grow min-w-0">
<div class="text-xs font-medium text-[var(--text-primary)] truncate">${ suggestion.name }</div>
<div class="text-[10px] text-[var(--text-muted)]">
Used ${ suggestion.use_count }x
</div>
</div>
<i class="fas fa-user text-[var(--text-muted)] ml-1.5 flex-shrink-0 text-[10px]"></i>
</div>
</div>
</div>
</div>
</div>
<!-- Add Speaker Button -->
<div class="pt-2 sm:pt-4 border-t border-[var(--border-primary)]">
<button @click="openAddSpeakerModal" class="w-full px-3 py-2 sm:py-2 bg-[var(--bg-accent)] text-[var(--text-accent)] rounded-md hover:bg-[var(--bg-accent-hover)] active:scale-[0.98] transition-all flex items-center justify-center text-xs sm:text-sm font-medium">
<i class="fas fa-plus mr-1.5"></i>
<span v-text="t('buttons.addSpeaker')"></span>
</button>
</div>
</div>
<!-- Audio Player & Transcript Panel - shown on mobile when transcript tab active, always on desktop -->
<div class="lg:w-2/3 p-3 sm:p-6 flex flex-col overflow-hidden"
:class="{ 'hidden lg:flex': speakerModalTab !== 'transcript', 'flex-1': speakerModalTab === 'transcript' }">
<!-- Audio Player Section -->
<div class="mb-2 sm:mb-4 flex-shrink-0">
<!-- Show message if audio has been deleted -->
<div v-if="selectedRecording.audio_deleted_at"
class="text-[var(--text-muted)] text-xs flex items-center gap-2">
<i class="fas fa-info-circle"></i>
<span v-text="t('help.audioDeletedMessage')"></span>
</div>
<!-- Show message for incognito recordings -->
<div v-else-if="selectedRecording.incognito"
class="text-[var(--text-muted)] text-xs flex items-center gap-2">
<i class="fas fa-user-secret"></i>
<span>Audio not stored in incognito mode</span>
</div>
<!-- Custom Audio/Video Player (Independent from main player) -->
<div v-else>
<component :is="selectedRecording.mime_type && selectedRecording.mime_type.startsWith('video/') ? 'video' : 'audio'"
ref="speakerModalAudioRef"
:class="selectedRecording.mime_type && selectedRecording.mime_type.startsWith('video/') ? 'w-full rounded-lg mb-2' : 'hidden'"
:key="'speaker-modal-' + selectedRecording.id"
:src="'/audio/' + selectedRecording.id"
:volume="playerVolume"
@play="handleModalAudioPlayPause"
@pause="handleModalAudioPlayPause"
@timeupdate="handleModalAudioTimeUpdate"
@loadedmetadata="handleModalAudioLoadedMetadata"
@ended="modalAudioIsPlaying = false">
</component>
<div class="flex items-center gap-2">
<!-- Play/Pause -->
<button @click="$refs.speakerModalAudioRef?.paused ? $refs.speakerModalAudioRef.play() : $refs.speakerModalAudioRef.pause()"
class="w-8 h-8 flex items-center justify-center rounded-full bg-[var(--bg-accent)] hover:bg-[var(--bg-accent-hover)] text-white transition-all flex-shrink-0 shadow-sm"
:title="modalAudioIsPlaying ? 'Pause' : 'Play'">
<i :class="modalAudioIsPlaying ? 'fas fa-pause' : 'fas fa-play'" class="text-xs" :style="!modalAudioIsPlaying ? 'margin-left: 1px' : ''"></i>
</button>
<!-- Time -->
<div class="flex flex-col items-end flex-shrink-0 leading-none">
<span class="text-xs text-[var(--text-primary)] font-mono">${ formatAudioTime(modalAudioCurrentTime) }</span>
<span class="text-[10px] text-[var(--text-muted)] font-mono">${ formatAudioTime(modalAudioDuration) }</span>
</div>
<!-- Progress Bar -->
<div class="flex-1 h-1.5 bg-[var(--bg-tertiary)] rounded-full cursor-pointer group relative"
@click="(e) => { const rect = e.currentTarget.getBoundingClientRect(); const pct = (e.clientX - rect.left) / rect.width; if ($refs.speakerModalAudioRef) $refs.speakerModalAudioRef.currentTime = pct * modalAudioDuration; }">
<div class="h-full bg-[var(--bg-accent)] rounded-full transition-all duration-100"
:style="{ width: modalAudioProgressPercent + '%' }">
</div>
</div>
<!-- Volume Control -->
<div class="relative flex items-center flex-shrink-0"
@mouseenter="showModalVolumeSlider = true"
@mouseleave="showModalVolumeSlider = false">
<button @click="if ($refs.speakerModalAudioRef) { $refs.speakerModalAudioRef.muted = !$refs.speakerModalAudioRef.muted; audioIsMuted = $refs.speakerModalAudioRef.muted; }"
class="w-7 h-7 flex items-center justify-center rounded hover:bg-[var(--bg-tertiary)] transition-all flex-shrink-0"
:class="audioIsMuted || playerVolume === 0 ? 'text-[var(--text-muted)]' : 'text-[var(--text-primary)]'">
<i :class="audioIsMuted || playerVolume === 0 ? 'fas fa-volume-mute' : playerVolume < 0.5 ? 'fas fa-volume-down' : 'fas fa-volume-up'" class="text-xs"></i>
</button>
<!-- Volume Slider Popup (outer div is invisible hover bridge) -->
<div class="absolute top-full left-1/2 -translate-x-1/2 pt-2 z-[9999] transition-all duration-200"
:class="showModalVolumeSlider ? 'opacity-100 pointer-events-auto scale-100' : 'opacity-0 pointer-events-none scale-95'"
@mouseenter="showModalVolumeSlider = true"
@mouseleave="showModalVolumeSlider = false">
<div class="px-2.5 py-2 bg-[var(--bg-tertiary)] border border-[var(--border-accent)] rounded-lg shadow-xl flex flex-col items-center gap-1">
<span class="text-[9px] font-mono text-[var(--text-muted)]">${ Math.round(playerVolume * 100) }</span>
<input type="range" min="0" max="1" step="0.05"
:value="audioIsMuted ? 0 : playerVolume"
@input="(e) => setAudioVolume(parseFloat(e.target.value))"
class="volume-slider-vertical"
style="height: 70px;">
</div>
</div>
</div>
<!-- Speed Control (compact - tap to cycle) -->
<button @click="cycleModalPlaybackRate(); if ($refs.speakerModalAudioRef) $refs.speakerModalAudioRef.playbackRate = modalPlaybackRate"
class="w-7 h-7 flex items-center justify-center rounded hover:bg-[var(--bg-tertiary)] text-[var(--text-muted)] hover:text-[var(--text-primary)] transition-all flex-shrink-0"
title="Playback speed (tap to change)">
<span class="text-[10px] font-semibold font-mono">${ formatPlaybackRate(modalPlaybackRate) }</span>
</button>
</div>
</div>
</div>
<!-- Transcript Section (Virtual Scrolling for performance) -->
<div ref="speakerModalTranscriptRef"
class="flex-grow overflow-y-auto custom-scrollbar speaker-modal-transcript min-h-0"
@click="(e) => { const t = e.target.closest('[data-start-time]')?.dataset.startTime; if (t && $refs.speakerModalAudioRef) { $refs.speakerModalAudioRef.currentTime = parseFloat(t); $refs.speakerModalAudioRef.play(); } }"
@scroll="onSpeakerModalScroll">
<div v-if="processedTranscription.isJson">
<!-- Virtual scroll spacer (top) -->
<div :style="{ height: speakerModalSpacerBefore + 'px' }"></div>
<!-- Only render visible segments -->
<div v-for="segment in speakerModalVisibleSegments"
:key="getVirtualItemKey(segment, 'sm')"
class="speaker-segment mb-1.5 sm:mb-2 group relative"
:data-start-time="segment.startTime"
:data-segment-index="segment._originalIndex">
<div class="flex items-start gap-1">
<!-- Speaker tag on its own line on mobile -->
<span :class="[segment.color, 'speaker-tag', { 'speaker-highlight': highlightedSpeaker === segment.speakerId }]" :data-speaker-id="segment.speakerId" class="inline-block flex-shrink-0 truncate text-xs sm:text-sm font-medium w-16 sm:w-24" :title="segment.speaker">${ segment.speaker }</span>
<!-- Text content -->
<span class="word cursor-pointer hover:bg-[var(--bg-accent)] hover:text-[var(--text-accent)] rounded px-0.5 flex-1 text-xs sm:text-sm leading-relaxed">${ segment.sentence }</span>
<!-- Edit buttons - inline on mobile -->
<span class="flex items-center gap-0.5 flex-shrink-0 sm:opacity-0 sm:group-hover:opacity-100 transition-opacity">
<button @click.stop="openSpeakerChangeDropdown(segment._originalIndex)" class="p-1 rounded bg-[var(--bg-tertiary)] hover:bg-[var(--bg-accent)] text-[var(--text-muted)] hover:text-[var(--text-accent)] transition-colors active:scale-95" title="Change speaker">
<i class="fas fa-user-edit text-[10px] sm:text-xs"></i>
</button>
<button @click.stop="openEditTextModal(segment._originalIndex)" class="p-1 rounded bg-[var(--bg-tertiary)] hover:bg-[var(--bg-accent)] text-[var(--text-muted)] hover:text-[var(--text-accent)] transition-colors active:scale-95" title="Edit text">
<i class="fas fa-edit text-[10px] sm:text-xs"></i>
</button>
</span>
</div>
<!-- Speaker Change Dropdown - compact on mobile -->
<div v-if="editingSpeakerIndex === segment._originalIndex" @click.stop class="mt-1 p-1.5 bg-[var(--bg-tertiary)] rounded-md border border-[var(--border-primary)] shadow-lg">
<p class="text-[10px] text-[var(--text-muted)] mb-1">Select speaker:</p>
<div class="flex flex-wrap gap-1">
<button v-for="speaker in modalSpeakers" :key="speaker" @click="changeSpeaker(segment._originalIndex, speaker)" class="px-2 py-1 rounded-md hover:bg-[var(--bg-accent)] hover:text-[var(--text-accent)] active:scale-[0.98] transition-all flex items-center gap-1 text-xs bg-[var(--bg-secondary)]">
<div :class="speakerMap[speaker].color" class="w-2.5 h-2.5 rounded-full flex-shrink-0"></div>
<span class="truncate max-w-[60px]">${ speakerMap[speaker].name || speaker }</span>
</button>
</div>
</div>
</div>
<!-- Virtual scroll spacer (bottom) -->
<div :style="{ height: speakerModalSpacerAfter + 'px' }"></div>
</div>
<div v-else class="whitespace-pre-wrap text-sm text-[var(--text-primary)]">${ processedTranscription.plainText }</div>
</div>
<!-- Speaker Navigation Controls - more compact -->
<div class="mt-1.5 sm:mt-3 p-1.5 sm:p-3 bg-[var(--bg-tertiary)] rounded-md flex flex-col gap-1.5 flex-shrink-0">
<!-- Speaker Selector Row -->
<div class="flex items-center gap-1.5">
<select
:value="highlightedSpeaker"
@change="selectSpeakerForNavigation($event.target.value)"
class="flex-1 px-2 py-1 text-xs bg-[var(--bg-secondary)] border border-[var(--border-secondary)] rounded text-[var(--text-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--ring-focus)]">
<option value="">Navigate to speaker...</option>
<option v-for="speaker in modalSpeakers" :key="speaker" :value="speaker">
${ speakerMap[speaker]?.name || speakerDisplayMap[speaker] || speaker }
</option>
</select>
<span v-if="highlightedSpeaker && speakerGroups.length > 0" class="text-[10px] text-[var(--text-muted)] flex-shrink-0 tabular-nums">
${ currentSpeakerGroupIndex + 1 }/${ speakerGroups.length }
</span>
</div>
<!-- Navigation Buttons Row -->
<div class="flex items-center gap-1.5" :class="{'opacity-50': !highlightedSpeaker || speakerGroups.length <= 1}">
<button @mousedown.prevent @click="navigateToPrevSpeakerGroup"
class="flex-1 px-2 py-1.5 bg-[var(--bg-secondary)] hover:bg-[var(--bg-primary)] text-[var(--text-secondary)] rounded border border-[var(--border-secondary)] transition-all active:scale-95 flex items-center justify-center text-xs disabled:opacity-50 disabled:cursor-not-allowed"
:disabled="!highlightedSpeaker || speakerGroups.length <= 1">
<i class="fas fa-chevron-up mr-1"></i>Prev
</button>
<button @mousedown.prevent @click="navigateToNextSpeakerGroup"
class="flex-1 px-2 py-1.5 bg-[var(--bg-secondary)] hover:bg-[var(--bg-primary)] text-[var(--text-secondary)] rounded border border-[var(--border-secondary)] transition-all active:scale-95 flex items-center justify-center text-xs disabled:opacity-50 disabled:cursor-not-allowed"
:disabled="!highlightedSpeaker || speakerGroups.length <= 1">
Next<i class="fas fa-chevron-down ml-1"></i>
</button>
</div>
</div>
</div>
</div>
<!-- Regenerate Summary Checkbox -->
<div class="p-2 sm:p-4 bg-[var(--bg-tertiary)] border-t border-[var(--border-primary)] flex-shrink-0">
<label class="flex items-center text-xs sm:text-sm text-[var(--text-muted)] cursor-pointer hover:text-[var(--text-primary)] transition-colors">
<input type="checkbox"
v-model="regenerateSummaryAfterSpeakerUpdate"
class="speaker-checkbox w-4 h-4">
<span class="ml-1.5 select-none" v-text="t('help.regenerateSummaryAfterNames')"></span>
</label>
</div>
<!-- Footer Buttons -->
<div class="bg-[var(--bg-tertiary)] px-3 sm:px-6 py-2 sm:py-4 flex flex-row justify-between items-center gap-2 border-t border-[var(--border-primary)] flex-shrink-0 rounded-b-xl">
<!-- Left: Auto Identify Split Button + Apply Suggested -->
<div class="flex items-center gap-2">
<!-- Split Button: Auto Identify -->
<div class="relative inline-flex" ref="autoIdSplitBtn">
<!-- Main button: identify missing only -->
<button @click="autoIdentifySpeakers(false)" class="px-2 sm:px-3 py-1.5 sm:py-2 bg-purple-600 text-white rounded-l-md hover:bg-purple-700 active:scale-[0.98] transition-all flex items-center justify-center text-xs sm:text-sm font-medium" :disabled="isAutoIdentifying">
<i class="fas fa-magic mr-1 sm:mr-1.5"></i>
<span v-if="!isAutoIdentifying" class="hidden sm:inline" v-text="t('help.autoIdentify')"></span>
<span v-if="!isAutoIdentifying" class="sm:hidden" v-text="t('help.autoIdentifyMobile')"></span>
<span v-else>
<i class="fas fa-spinner fa-spin"></i>
</span>
</button>
<!-- Dropdown arrow -->
<button @click="showAutoIdDropdown = !showAutoIdDropdown" class="px-1.5 py-1.5 sm:py-2 bg-purple-600 text-white rounded-r-md border-l border-purple-500 hover:bg-purple-700 active:scale-[0.98] transition-all text-xs sm:text-sm" :disabled="isAutoIdentifying">
<i class="fas fa-caret-down"></i>
</button>
<!-- Dropdown menu -->
<div v-if="showAutoIdDropdown" class="absolute bottom-full left-0 mb-1 bg-[var(--bg-secondary)] border border-[var(--border-primary)] rounded-md shadow-lg z-50 min-w-[180px]">
<button @click="autoIdentifySpeakers(true)" class="w-full px-3 py-2 text-left text-xs sm:text-sm text-[var(--text-primary)] hover:bg-[var(--bg-tertiary)] transition-colors rounded-md">
<i class="fas fa-users mr-2"></i><span v-text="t('help.identifyAllSpeakers')"></span>
</button>
</div>
</div>
<!-- Apply Suggested Button -->
<button v-if="hasAnySuggestions" @click="applySuggestedNames" class="px-2 sm:px-3 py-1.5 sm:py-2 bg-gradient-to-r from-blue-600 to-purple-600 text-white rounded-md hover:from-blue-700 hover:to-purple-700 active:scale-[0.98] transition-all flex items-center text-xs sm:text-sm font-medium">
<i class="fas fa-lightbulb mr-1 sm:mr-1.5"></i>
<span class="hidden sm:inline" v-text="t('help.applySuggested')"></span>
<span class="sm:hidden" v-text="t('help.applySuggestedMobile')"></span>
</button>
</div>
<!-- Right: Cancel & Save Buttons -->
<div class="flex gap-2">
<button @click="closeSpeakerModal" class="px-3 sm:px-4 py-1.5 sm:py-2 bg-[var(--bg-secondary)] text-[var(--text-secondary)] rounded-md border border-[var(--border-secondary)] hover:bg-[var(--bg-tertiary)] active:scale-[0.98] transition-all text-xs sm:text-sm" v-text="t('common.cancel')"></button>
<button
@click="saveSpeakerNames"
:disabled="!hasSpeakerNames"
class="px-3 sm:px-4 py-1.5 sm:py-2 rounded-md transition-all text-xs sm:text-sm font-medium active:scale-[0.98]"
:class="hasSpeakerNames
? 'bg-[var(--bg-accent)] text-[var(--text-accent)] hover:bg-[var(--bg-accent-hover)] cursor-pointer'
: 'bg-gray-400 text-gray-200 cursor-not-allowed'"
v-text="t('buttons.saveNames')"
></button>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,53 @@
<!-- System Audio Help Modal -->
<div v-if="showSystemAudioHelp" @click.self="showSystemAudioHelp = false" class="fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center z-50 p-4 backdrop-blur-sm">
<div class="bg-[var(--bg-secondary)] rounded-xl shadow-2xl w-full max-w-lg max-h-[80vh] overflow-y-auto">
<div class="p-6 border-b border-[var(--border-primary)]">
<div class="flex items-center justify-between">
<h3 class="text-lg font-semibold" v-text="t('help.systemAudioHelp')"></h3>
<button @click="showSystemAudioHelp = false" class="text-[var(--text-muted)] hover:text-[var(--text-primary)] transition-colors">
<i class="fas fa-times text-xl"></i>
</button>
</div>
</div>
<div class="p-6 space-y-4">
<div class="p-4 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg">
<div class="flex items-start gap-3">
<i class="fas fa-info-circle text-blue-600 dark:text-blue-400 mt-0.5"></i>
<div>
<p class="font-medium text-blue-800 dark:text-blue-300 mb-2">Compatibilité des navigateurs</p>
<p class="text-sm text-blue-700 dark:text-blue-400">
L'enregistrement audio système fonctionne mieux dans Chrome, Edge et Brave. Firefox est supporté mais nécessite que l'onglet soit en train de jouer de l'audio. Non supporté sur Safari ou les appareils mobiles.
</p>
</div>
</div>
</div>
<div>
<h4 class="font-medium mb-2">Comment enregistrer l'audio système :</h4>
<ol class="list-decimal list-inside space-y-2 text-sm text-[var(--text-secondary)]">
<li>Cliquez sur le bouton "Audio Système" ou "Micro + Système"</li>
<li>Une fenêtre de partage d'écran apparaîtra</li>
<li>Sélectionnez un onglet ou une fenêtre qui <strong>joue de l'audio activement</strong></li>
<li>Assurez-vous que la case "Partager l'audio" est <strong>cochée</strong></li>
<li>Cliquez sur "Partager" pour démarrer l'enregistrement</li>
</ol>
</div>
<div>
<h4 class="font-medium mb-2">Dépannage :</h4>
<ul class="list-disc list-inside space-y-1 text-sm text-[var(--text-muted)]">
<li><strong>Important :</strong> L'onglet/fenêtre doit jouer de l'audio au moment du partage</li>
<li>Vérifiez que la case "Partager l'audio" est cochée dans la fenêtre de partage</li>
<li>Sur Firefox, lancez d'abord la lecture audio, puis cliquez sur enregistrer</li>
<li>Certains contenus peuvent avoir une protection DRM qui bloque l'enregistrement</li>
<li>Non supporté sur Safari ou les navigateurs mobiles</li>
</ul>
</div>
</div>
<div class="p-6 border-t border-[var(--border-primary)] flex justify-end">
<button @click="showSystemAudioHelp = false" class="px-4 py-2 bg-[var(--bg-button)] text-[var(--text-button)] rounded-lg hover:bg-[var(--bg-button-hover)] transition-colors">
Compris
</button>
</div>
</div>
</div>

View File

@@ -0,0 +1,32 @@
<!-- Simple Transcription Editor Modal -->
<div v-if="showTextEditorModal" @click.self="closeTextEditorModal" class="fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center z-50 p-4 backdrop-blur-sm">
<div class="bg-[var(--bg-secondary)] rounded-lg shadow-xl w-full max-w-2xl max-h-[90vh] flex flex-col">
<div class="p-6 border-b border-[var(--border-primary)]">
<div class="flex items-center justify-between">
<h3 class="text-lg font-semibold" v-text="t('modal.editTranscription')"></h3>
<button @click="closeTextEditorModal" class="text-[var(--text-muted)] hover:text-[var(--text-primary)] transition-colors">
<i class="fas fa-times text-xl"></i>
</button>
</div>
</div>
<div class="p-6 space-y-4 overflow-y-auto flex-1">
<div>
<label class="block text-sm font-medium mb-2" v-text="t('transcription.title')"></label>
<textarea v-model="editingTranscriptionContent"
rows="15"
class="w-full px-3 py-2 bg-[var(--bg-input)] border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--ring-focus)] font-mono text-sm">
</textarea>
</div>
</div>
<div class="p-6 border-t border-[var(--border-primary)] flex justify-end gap-3">
<button @click="closeTextEditorModal"
class="px-4 py-2 text-[var(--text-secondary)] hover:bg-[var(--bg-tertiary)] rounded-lg transition-colors">
Cancel
</button>
<button @click="saveTranscription"
class="px-4 py-2 bg-[var(--bg-button)] text-[var(--text-button)] rounded-lg hover:bg-[var(--bg-button-hover)] transition-colors">
Save Changes
</button>
</div>
</div>
</div>

View File

@@ -0,0 +1,2 @@
<!-- Toast Container -->
<div id="toastContainer" class="fixed top-4 right-4 z-50 pointer-events-none"></div>

View File

@@ -0,0 +1,281 @@
<!-- Unified Share Modal (Public + Internal Sharing) -->
<div v-if="showUnifiedShareModal" @click.self="closeUnifiedShareModal" class="fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center z-50 p-4 backdrop-blur-sm">
<div class="bg-[var(--bg-secondary)] rounded-xl shadow-2xl w-full max-w-2xl max-h-[90vh] flex flex-col">
<div class="px-6 py-4 border-b border-[var(--border-primary)] flex-shrink-0">
<div class="flex items-center justify-between">
<div class="flex-1">
<h3 class="text-lg font-semibold" v-text="t('modal.shareRecording')"></h3>
<p class="text-sm text-[var(--text-muted)] mt-0.5">${internalShareRecording?.title}</p>
</div>
<button @click="closeUnifiedShareModal" class="text-[var(--text-muted)] hover:text-[var(--text-primary)] transition-colors ml-4">
<i class="fas fa-times text-xl"></i>
</button>
</div>
</div>
<div class="flex-1 overflow-y-auto">
<!-- Public Sharing Section -->
<div v-if="!internalShareRecording?.is_shared || internalShareRecording?.share_info?.can_reshare" class="px-6 py-4 border-b border-[var(--border-primary)]">
<h4 class="font-medium mb-2 flex items-center gap-2">
<i class="fas fa-globe text-[var(--text-accent)]"></i>
<span v-text="t('sharing.publicLink')"></span>
</h4>
<p class="text-xs text-[var(--text-muted)] mb-3" v-text="t('help.publicLinkDesc')"></p>
<div class="space-y-3">
<!-- Share Options -->
<div class="flex items-center gap-4 mb-2">
<label class="flex items-center cursor-pointer">
<input type="checkbox" v-model="shareOptions.share_summary" class="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500">
<span class="ml-2 text-sm" v-text="t('form.shareSummary')"></span>
</label>
<label class="flex items-center cursor-pointer">
<input type="checkbox" v-model="shareOptions.share_notes" class="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500">
<span class="ml-2 text-sm" v-text="t('form.shareNotes')"></span>
</label>
</div>
<!-- Create New Share Button -->
<button @click="createShare(true)"
class="w-full px-3 py-2 bg-[var(--bg-accent)] text-white rounded-lg hover:opacity-90 transition-opacity text-sm font-medium">
<i class="fas fa-plus-circle mr-1.5"></i>Create New Public Share Link
</button>
<!-- Loading State -->
<div v-if="isLoadingPublicShares" class="text-center py-6">
<i class="fas fa-spinner fa-spin text-xl text-[var(--text-muted)]"></i>
<p class="mt-2 text-xs text-[var(--text-muted)]">Loading share links...</p>
</div>
<!-- Existing Shares List -->
<div v-else-if="recordingPublicShares.length > 0" class="space-y-2">
<h5 class="text-xs font-medium text-[var(--text-secondary)] mb-1.5">
Existing Share Links (${recordingPublicShares.length})
</h5>
<div v-for="share in recordingPublicShares" :key="share.id"
class="bg-[var(--bg-tertiary)] p-2.5 rounded-lg border border-[var(--border-primary)]">
<div class="flex items-center justify-between gap-2 mb-2">
<div class="flex items-center gap-2 flex-1 min-w-0">
<i class="fas fa-calendar text-[var(--text-muted)] text-xs"></i>
<span class="text-xs text-[var(--text-muted)] truncate">Created: ${share.created_at}</span>
<span v-if="share.share_summary" class="inline-flex items-center px-1.5 py-0.5 rounded text-xs bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-400">
<i class="fas fa-file-alt mr-1"></i>Summary
</span>
<span v-if="share.share_notes" class="inline-flex items-center px-1.5 py-0.5 rounded text-xs bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400">
<i class="fas fa-sticky-note mr-1"></i>Notes
</span>
</div>
<button @click="confirmDeletePublicShare(share)"
class="p-1.5 rounded hover:bg-red-50 dark:hover:bg-red-900/20 text-[var(--text-muted)] hover:text-red-500 transition-colors"
:title="'Delete share link'">
<i class="fas fa-trash text-xs"></i>
</button>
</div>
<div class="relative">
<input :value="share.share_url" :id="'unified-share-link-' + share.id" readonly
class="w-full px-2 py-1.5 pr-10 bg-[var(--bg-input)] border border-[var(--border-secondary)] rounded text-xs font-mono focus:outline-none focus:ring-2 focus:ring-[var(--ring-focus)]">
<button @click="copyPublicShareLinkWithFeedback(share.share_url, share.id)"
class="absolute right-1 top-1/2 -translate-y-1/2 w-8 h-7 flex items-center justify-center rounded bg-[var(--bg-button)] text-[var(--text-button)] hover:bg-[var(--bg-button-hover)] transition-all"
:title="t('buttons.copy')">
<i :class="copiedShareId === share.id ? 'fas fa-check' : 'fas fa-copy'" class="text-xs"></i>
</button>
</div>
</div>
</div>
<!-- No Shares Yet -->
<div v-else-if="!isLoadingPublicShares" class="text-center py-4 text-xs text-[var(--text-muted)]">
<i class="fas fa-link text-xl mb-1"></i>
<p>No public share links yet</p>
<p class="text-xs mt-0.5 opacity-75">Click the button above to create one</p>
</div>
</div>
</div>
<!-- Internal Sharing Section -->
<div v-if="enableInternalSharing && (!internalShareRecording?.is_shared || internalShareRecording?.share_info?.can_reshare)" class="px-6 py-4">
<h4 class="font-medium mb-2 flex items-center gap-2">
<i class="fas fa-users text-[var(--text-accent)]"></i>
<span v-text="t('sharing.internalSharing')"></span>
</h4>
<p class="text-xs text-[var(--text-muted)] mb-3" v-text="t('help.internalSharingDesc')"></p>
<!-- Share with New User Section -->
<div class="bg-[var(--bg-tertiary)] p-3 rounded-lg border border-[var(--border-primary)] mb-3">
<h5 class="font-medium text-sm text-[var(--text-primary)] mb-2 flex items-center">
<i class="fas fa-user-plus mr-1.5 text-blue-500 text-xs"></i>
Share with User
</h5>
<!-- User Search/Selection (when SHOW_USERNAMES_IN_UI is enabled) -->
<div v-if="showUsernamesInUI" class="space-y-3">
<!-- Search Input (for filtering) -->
<div class="relative">
<input v-model="internalShareUserSearch"
@input="searchInternalShareUsers()"
type="text"
placeholder="Search users..."
class="w-full px-4 py-2 pl-10 bg-[var(--bg-input)] border border-[var(--border-secondary)] rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent">
<i class="fas fa-search absolute left-3 top-3 text-[var(--text-muted)]"></i>
</div>
<!-- User Grid -->
<div v-if="isLoadingAllUsers" class="text-center py-8">
<i class="fas fa-spinner fa-spin text-2xl text-[var(--text-muted)]"></i>
<p class="mt-2 text-sm text-[var(--text-muted)]">Loading users...</p>
</div>
<div v-else-if="internalShareSearchResults.length === 0" class="text-center py-8 text-[var(--text-muted)]">
<i class="fas fa-users-slash text-3xl mb-2"></i>
<p>No users found</p>
</div>
<div v-else class="grid grid-cols-1 sm:grid-cols-2 gap-2 max-h-[50vh] sm:max-h-64 overflow-y-auto">
<div v-for="user in internalShareSearchResults" :key="user.id"
@click="createInternalShare(user.id, user.username)"
class="group p-3 sm:p-2 bg-[var(--bg-secondary)] hover:bg-[var(--bg-hover)] rounded-lg border border-[var(--border-secondary)] hover:border-blue-500 cursor-pointer transition-all active:scale-[0.98]">
<div class="flex items-center gap-3 sm:gap-2">
<div :class="['w-10 h-10 sm:w-8 sm:h-8 rounded-full flex items-center justify-center flex-shrink-0 font-bold text-sm sm:text-xs', getUserColorClass(user.username)]">
${user.username ? user.username.charAt(0).toUpperCase() : '?'}
</div>
<div class="flex-1 min-w-0">
<p class="font-medium text-base sm:text-sm text-[var(--text-primary)] truncate">${user.username}</p>
<p v-if="user.email" class="text-sm sm:text-xs text-[var(--text-muted)] truncate">${user.email}</p>
</div>
<i class="fas fa-user-plus text-blue-500 text-lg sm:text-base group-hover:scale-110 transition-transform flex-shrink-0"></i>
</div>
</div>
</div>
</div>
<!-- Direct Username Entry (when SHOW_USERNAMES_IN_UI is disabled - Privacy Mode) -->
<div v-else class="space-y-3">
<p class="text-sm text-[var(--text-muted)]">Enter the exact username to share with</p>
<div class="flex gap-2">
<input v-model="internalShareUserSearch"
@keyup.enter="shareWithUsername()"
type="text"
placeholder="Enter full username..."
class="flex-1 px-4 py-2 bg-[var(--bg-input)] border border-[var(--border-secondary)] rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent">
<button @click="shareWithUsername()"
:disabled="!internalShareUserSearch.trim() || isSearchingUsers"
class="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed transition-colors">
<i v-if="isSearchingUsers" class="fas fa-spinner fa-spin"></i>
<span v-else>Add</span>
</button>
</div>
<p class="text-xs text-[var(--text-muted)]">
<i class="fas fa-info-circle mr-1"></i>
You must enter the exact username. If the user exists, they will be added to the share list.
</p>
</div>
<!-- Permissions -->
<div class="border-t border-[var(--border-primary)] mt-3 pt-2.5 space-y-1.5">
<p class="text-xs font-medium text-[var(--text-secondary)] mb-1.5">Share Permissions</p>
<div class="flex items-center">
<input type="checkbox"
v-model="internalSharePermissions.can_edit"
:disabled="!internalShareMaxPermissions.can_edit"
id="unified_share_can_edit"
class="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed">
<label for="unified_share_can_edit"
:class="['ml-2 block text-sm', internalShareMaxPermissions.can_edit ? 'text-[var(--text-secondary)]' : 'text-[var(--text-muted)] opacity-60']">
<i class="fas fa-pencil-alt mr-1"></i>Allow editing notes and metadata
<span v-if="!internalShareMaxPermissions.can_edit" class="text-xs italic ml-1">(You don't have this permission)</span>
</label>
</div>
<div class="flex items-center">
<input type="checkbox"
v-model="internalSharePermissions.can_reshare"
:disabled="!internalShareMaxPermissions.can_reshare"
id="unified_share_can_reshare"
class="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed">
<label for="unified_share_can_reshare"
:class="['ml-2 block text-sm', internalShareMaxPermissions.can_reshare ? 'text-[var(--text-secondary)]' : 'text-[var(--text-muted)] opacity-60']">
<i class="fas fa-share-alt mr-1"></i>Allow re-sharing with others
<span v-if="!internalShareMaxPermissions.can_reshare" class="text-xs italic ml-1">(You don't have this permission)</span>
</label>
</div>
</div>
</div>
<!-- Already Shared With Section -->
<div class="bg-[var(--bg-tertiary)] p-3 rounded-lg border border-[var(--border-primary)]">
<h5 class="font-medium text-sm text-[var(--text-primary)] mb-2 flex items-center justify-between">
<span class="flex items-center">
<i class="fas fa-users-cog mr-1.5 text-purple-500 text-xs"></i>
Already Shared With
</span>
<span v-if="recordingInternalShares.length > 0" class="text-xs font-normal text-[var(--text-muted)]">
${recordingInternalShares.length} user(s)
</span>
</h5>
<div v-if="isLoadingInternalShares" class="text-center py-8">
<i class="fas fa-spinner fa-spin text-2xl text-[var(--text-muted)]"></i>
<p class="mt-2 text-sm text-[var(--text-muted)]">Loading shares...</p>
</div>
<div v-else-if="recordingInternalShares.length === 0" class="text-center py-8 text-[var(--text-muted)]">
<i class="fas fa-user-slash text-3xl mb-3"></i>
<p>Not shared with anyone yet</p>
<p class="text-sm mt-1">Select a user above to share this recording</p>
</div>
<div v-else class="grid grid-cols-1 sm:grid-cols-2 gap-2 max-h-[50vh] sm:max-h-64 overflow-y-auto">
<div v-for="share in recordingInternalShares" :key="share.user_id"
class="bg-[var(--bg-secondary)] p-3 sm:p-2 rounded-lg border border-[var(--border-secondary)] hover:border-[var(--border-primary)] transition-colors">
<div class="flex items-center gap-3 sm:gap-2">
<div :class="['w-10 h-10 sm:w-8 sm:h-8 rounded-full flex items-center justify-center flex-shrink-0 font-bold text-sm sm:text-xs', getUserColorClass(share.username)]">
${share.username ? share.username.charAt(0).toUpperCase() : '#'}
</div>
<div class="flex-1 min-w-0">
<p class="font-medium text-base sm:text-sm text-[var(--text-primary)] truncate">${share.username || 'User #' + share.user_id}</p>
<p class="text-sm sm:text-xs text-[var(--text-muted)] mb-1 sm:mb-0.5">${share.is_owner ? 'Recording Owner' : formatShareDate(share.created_at)}</p>
<div class="flex flex-wrap gap-1 text-xs sm:text-[10px]">
<span v-if="share.is_owner" class="inline-flex items-center px-2 py-0.5 sm:px-1.5 rounded bg-yellow-100 dark:bg-yellow-900/30 text-yellow-700 dark:text-yellow-400 font-medium">
<i class="fas fa-crown mr-1 sm:mr-0.5 text-xs sm:text-[9px]"></i>Owner
</span>
<template v-else>
<span v-if="share.can_edit" class="inline-flex items-center px-2 py-0.5 sm:px-1.5 rounded bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400">
<i class="fas fa-pencil-alt mr-1 sm:mr-0.5 text-xs sm:text-[8px]"></i>Edit
</span>
<span v-if="share.can_reshare" class="inline-flex items-center px-2 py-0.5 sm:px-1.5 rounded bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-400">
<i class="fas fa-share-alt mr-1 sm:mr-0.5 text-xs sm:text-[8px]"></i>Reshare
</span>
<span v-if="!share.can_edit && !share.can_reshare" class="inline-flex items-center px-2 py-0.5 sm:px-1.5 rounded bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-400">
<i class="fas fa-eye mr-1 sm:mr-0.5 text-xs sm:text-[8px]"></i>View
</span>
</template>
</div>
</div>
<button v-if="!share.is_owner"
@click="revokeInternalShare(share.id, share.username || share.user_name)"
class="p-2 sm:p-1.5 rounded-lg text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20 active:scale-95 transition-all flex-shrink-0"
:title="'Revoke access'">
<i class="fas fa-user-times text-base sm:text-xs"></i>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- No Permission Message -->
<div v-if="internalShareRecording?.is_shared && !internalShareRecording?.share_info?.can_reshare" class="px-6 py-6 text-center">
<div class="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg p-4">
<i class="fas fa-info-circle text-yellow-600 dark:text-yellow-400 text-2xl mb-2"></i>
<p class="text-sm text-yellow-800 dark:text-yellow-300 font-medium">Sharing Not Available</p>
<p class="text-xs text-yellow-700 dark:text-yellow-400 mt-1">
This recording was shared with you, but you don't have permission to share it with others.
</p>
</div>
</div>
<div class="px-6 py-3 border-t border-[var(--border-primary)] flex justify-end flex-shrink-0">
<button @click="closeUnifiedShareModal" class="px-4 py-2 bg-[var(--bg-button)] text-[var(--text-button)] rounded-lg hover:bg-[var(--bg-button-hover)] transition-colors">
<span v-text="t('tagsModal.done')"></span>
</button>
</div>
</div>
</div>

View File

@@ -0,0 +1,36 @@
<!-- Upload Disclaimer Modal -->
<div v-if="showUploadDisclaimerModal" @click.self="cancelUploadDisclaimer" class="fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center z-50 p-4 backdrop-blur-sm">
<div class="bg-[var(--bg-secondary)] rounded-lg shadow-xl w-full max-w-2xl max-h-[80vh] flex flex-col overflow-hidden">
<div class="flex-shrink-0 p-6 border-b border-[var(--border-primary)]">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<i class="fas fa-info-circle text-2xl text-[var(--text-accent)]"></i>
<h3 class="text-xl font-semibold text-[var(--text-primary)]" v-text="t('modal.uploadNotice')"></h3>
</div>
<button @click="cancelUploadDisclaimer" class="text-[var(--text-muted)] hover:text-[var(--text-primary)] transition-colors">
<i class="fas fa-times text-xl"></i>
</button>
</div>
</div>
<div class="flex-1 overflow-y-auto p-6">
<!-- Render markdown content -->
<div class="ai-message text-[var(--text-secondary)]"
style="line-height: 1.6;">
<div v-html="uploadDisclaimerHtml"></div>
</div>
</div>
<div class="flex-shrink-0 p-6 border-t border-[var(--border-primary)] bg-[var(--bg-tertiary)] rounded-b-lg">
<div class="flex justify-end gap-3">
<button @click="cancelUploadDisclaimer"
class="px-6 py-2 text-[var(--text-secondary)] hover:bg-[var(--bg-secondary)] rounded-lg transition-colors border border-[var(--border-secondary)]">
${ t('buttons.cancel') }
</button>
<button @click="acceptUploadDisclaimer"
class="px-6 py-2 bg-[var(--bg-accent)] text-white rounded-lg hover:bg-[var(--bg-accent-hover)] transition-colors">
<i class="fas fa-upload mr-2"></i>
<span v-text="t('modal.uploadFiles')"></span>
</button>
</div>
</div>
</div>
</div>