SmartMeeting/speakr/templates/index copy.html

3803 lines
280 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en" class="h-full">
<!-- ==================== 功能块:页面头部资源区 Start ==================== -->
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>智能(会议)文秘 - Audio Transcription & Summarization</title>
<script>
//------------------- 深色模式初始化 start------------------
// 检查是否已经设置过偏好,如果没有则默认使用深色模式
(function () {
const savedMode = localStorage.getItem('darkMode');
if (savedMode === null) {
// 首次访问,默认深色模式
localStorage.setItem('darkMode', 'true');
document.documentElement.classList.add('dark');
} else if (savedMode === 'true') {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
})();
//--------------------深色模式初始化 end------------------
</script>
{% include 'includes/loading_overlay.html' %}
<!-- ==================== 功能块:基础依赖资源 Start ==================== -->
<!-- All dependencies bundled locally for offline support -->
<script src="/tool/speakr/static/vendor/js/tailwind.min.js"></script>
<script src="/tool/speakr/static/vendor/js/vue.global.js"></script>
<script src="/tool/speakr/static/vendor/js/marked.min.js"></script>
<link rel="stylesheet" href="/tool/speakr/static/vendor/css/easymde.min.css">
<script src="/tool/speakr/static/vendor/js/easymde.min.js"></script>
<script src="/tool/speakr/static/js/recorder-core.js" charset="UTF-8"></script>
<script src="/tool/speakr/static/js/wav.js" charset="UTF-8"></script>
<script src="/tool/speakr/static/js/pcm.js" charset="UTF-8"></script>
<link rel="stylesheet" href="/tool/speakr/static/vendor/css/fontawesome.min.css">
<link rel="stylesheet" href="/tool/speakr/static/css/styles.css">
<link rel="icon" href="/tool/speakr/static/img/favicon.ico">
<link rel="icon" type="image/png" sizes="32x32" href="/tool/speakr/static/img/icon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/tool/speakr/static/img/icon-16x16.png">
<link rel="apple-touch-icon" sizes="180x180" href="/tool/speakr/static/img/icon-180x180.png">
<link rel="manifest" href="/tool/speakr/static/manifest.json">
<meta name="theme-color" content="#2563eb">
<!-- ==================== 功能块:基础依赖资源 End ==================== -->
<script>
//------------------- Tailwind 页面主题扩展 start------------------
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
maxHeight: {
'85vh': '85vh',
'90vh': '90vh'
},
colors: {
primary: 'var(--bg-primary)',
secondary: 'var(--bg-secondary)',
accent: 'var(--bg-accent)'
}
}
}
}
//--------------------Tailwind 页面主题扩展 end------------------
</script>
</head>
<!-- ==================== 功能块:页面头部资源区 End ==================== -->
<!-- ==================== 功能块:页面主体 Start ==================== -->
<body class="h-full bg-[var(--bg-primary)] text-[var(--text-primary)] transition-colors duration-300">
<!-- ==================== 功能块:全局加载遮罩 Start ==================== -->
<div id="loader" class="fixed inset-0 bg-gray-800 flex items-center justify-center z-50">
<div class="text-white text-lg">Loading...</div>
</div>
<!-- ==================== 功能块:全局加载遮罩 End ==================== -->
<!-- ==================== 功能块Vue 应用根容器 Start ==================== -->
<div id="app" v-cloak data-use-asr-endpoint="{{ use_asr_endpoint }}"
data-current-user-name="{{ current_user.name if current_user.is_authenticated else '' }}"
data-user-language="{{ user_language }}" class="h-full flex flex-col opacity-0 transition-opacity duration-500">
<!-- ==================== 功能块:顶部导航区 Start ==================== -->
<!-- Header -->
<header
class="bg-[var(--bg-secondary)] border-b border-[var(--border-primary)] px-3 sm:px-4 py-3 flex items-center justify-between flex-shrink-0 z-50">
<!-- Left side: Menu toggle and logo -->
<div class="flex items-center gap-2 sm:gap-4 min-w-0 flex-shrink">
<!-- Menu Toggle Button -->
<button @click="toggleSidebar"
class="p-2 rounded-lg hover:bg-[var(--bg-tertiary)] transition-colors duration-200 flex-shrink-0 flex items-center justify-center">
<i class="fas fa-bars text-lg"></i>
</button>
<!-- Logo and Title -->
<div class="flex items-center gap-2 sm:gap-3 min-w-0">
<img src="/tool/speakr/static/img/icon-32x32.png" alt="智能(会议)文秘"
class="w-7 h-7 sm:w-8 sm:h-8 flex-shrink-0">
<h1 class="text-lg sm:text-xl font-bold text-[var(--text-primary)] truncate">智能(会议)文秘</h1>
</div>
</div>
<!-- Right side: User menu and controls -->
<div class="flex items-center gap-3">
<!-- Inquire Mode Button -->
{% if inquire_mode_enabled %}
<a href="/tool/speakr/inquire"
class="px-2 py-1.5 bg-[var(--bg-accent)] text-[var(--text-accent)] rounded-lg hover:opacity-80 transition-opacity text-sm sm:px-4 sm:py-2 sm:min-w-[120px] sm:flex sm:items-center sm:gap-2 sm:justify-center">
<i class="fas fa-search"></i>
<span class="hidden sm:inline" v-text="t('inquire.title')"></span>
</a>
{% endif %}
<!-- New Recording Button -->
<button @click="switchToUploadView"
class="px-3 py-1.5 bg-[var(--bg-button)] text-[var(--text-button)] rounded-lg hover:bg-[var(--bg-button-hover)] transition-colors text-sm sm:px-4 sm:py-2 sm:min-w-[120px] sm:flex sm:items-center sm:gap-2 sm:justify-center">
<i class="fas fa-plus mr-1"></i><span v-text="t('nav.newRecording')"></span>
</button>
<!-- User menu -->
{% if current_user.is_authenticated %}
<div class="relative">
<button @click="isUserMenuOpen = !isUserMenuOpen"
class="flex items-center gap-1 sm:gap-2 p-2 rounded-lg hover:bg-[var(--bg-tertiary)] transition-colors duration-200"
:title="t('admin.userMenu')">
<i class="fas fa-user-circle text-lg"></i>
<span class="hidden lg:inline text-sm">{{ current_user.name if current_user.is_authenticated
else 'User' }}</span>
<i class="fas fa-chevron-down text-xs hidden sm:inline"></i>
</button>
<!-- User dropdown -->
<div v-if="isUserMenuOpen"
class="absolute right-0 mt-2 w-56 bg-[var(--bg-secondary)] border border-[var(--border-primary)] rounded-lg shadow-lg z-50">
<a href="/tool/speakr/account"
class="block px-4 py-2 hover:bg-[var(--bg-tertiary)] transition-colors">
<i class="fas fa-cog mr-2 w-4 text-center"></i><span v-text="t('nav.settings')"></span>
</a>
<a href="/tool/speakr/account#tags"
class="block px-4 py-2 hover:bg-[var(--bg-tertiary)] transition-colors">
<i class="fas fa-tags mr-2 w-4 text-center"></i><span
v-text="t('help.tagManagement')"></span>
</a>
<button @click="openSharesList"
class="w-full text-left px-4 py-2 hover:bg-[var(--bg-tertiary)] transition-colors flex items-center">
<i class="fas fa-share-alt mr-2 w-4 text-center"></i><span
v-text="t('modal.sharedTranscripts')"></span>
</button>
{% if current_user.is_admin %}
<a href="/tool/speakr/admin"
class="block px-4 py-2 hover:bg-[var(--bg-tertiary)] transition-colors">
<i class="fas fa-user-shield mr-2 w-4 text-center"></i><span
v-text="t('admin.title')"></span>
</a>
{% endif %}
<div class="border-t border-[var(--border-primary)] my-1"></div>
<button @click="toggleDarkMode"
class="w-full text-left px-4 py-2 hover:bg-[var(--bg-tertiary)] transition-colors flex items-center">
<i :class="isDarkMode ? 'fas fa-sun' : 'fas fa-moon'" class="mr-2 w-4 text-center"></i>
<span v-text="isDarkMode ? t('nav.lightMode') : t('nav.darkMode')"></span>
</button>
<button @click="openColorSchemeModal"
class="w-full text-left px-4 py-2 hover:bg-[var(--bg-tertiary)] transition-colors flex items-center">
<i class="fas fa-palette mr-2 w-4 text-center"></i>
<span v-text="t('modal.colorScheme')"></span>
</button>
<div class="border-t border-[var(--border-primary)] my-1"></div>
<a href="/tool/speakr/logout"
class="block px-4 py-2 hover:bg-[var(--bg-tertiary)] transition-colors">
<i class="fas fa-sign-out-alt mr-2 w-4 text-center"></i><span
v-text="t('nav.signOut')"></span>
</a>
</div>
</div>
{% endif %}
</div>
</header>
<!-- ==================== 功能块:顶部导航区 End ==================== -->
<!-- ==================== 功能块:应用主工作区 Start ==================== -->
<div class="flex-1 flex overflow-hidden relative">
<!-- Mobile Sidebar Backdrop -->
<div v-if="!isSidebarCollapsed && isMobileScreen" @click="toggleSidebar"
class="fixed inset-0 bg-black bg-opacity-50 z-40 lg:hidden">
</div>
<!-- ==================== 功能块:录音列表侧边栏 Start ==================== -->
<!-- Sidebar -->
<aside :class="['sidebar', isSidebarCollapsed ? 'collapsed' : '']">
<div class="sidebar-content-wrapper">
<!-- Sidebar Header -->
<div class="p-4 border-b border-[var(--border-primary)] flex-shrink-0">
<div class="flex items-center justify-between mb-4">
<h2 class="text-lg font-semibold" v-text="t('nav.recording')"></h2>
<div class="flex gap-2">
{% if inquire_mode_enabled %}
<a href="/tool/speakr/inquire"
class="px-2 py-1.5 bg-[var(--bg-accent)] text-[var(--text-accent)] rounded-lg hover:opacity-80 transition-opacity text-sm">
<i class="fas fa-search"></i>
</a>
{% endif %}
<button @click="switchToUploadView"
class="px-3 py-1.5 bg-[var(--bg-button)] text-[var(--text-button)] rounded-lg hover:bg-[var(--bg-button-hover)] transition-colors text-sm">
<i class="fas fa-plus mr-1"></i><span v-text="t('common.new')"></span>
</button>
</div>
</div>
<!-- Search and Sort Controls -->
<div class="space-y-3">
<!-- Filter Toggle Button -->
<div class="flex items-center space-x-2">
<button @click="showAdvancedFilters = !showAdvancedFilters"
class="flex-1 flex items-center justify-between px-3 py-2 bg-[var(--bg-input)] border border-[var(--border-secondary)] rounded-lg text-sm hover:bg-[var(--bg-tertiary)] transition-colors">
<span class="flex items-center">
<i class="fas fa-filter mr-2 text-[var(--text-muted)]"></i>
<span
v-if="!searchQuery && filterTags.length === 0 && !filterDatePreset && !filterDateRange.start && !filterDateRange.end && !filterTextQuery"
v-text="t('sidebar.searchRecordings')">
</span>
<span v-else class="text-[var(--text-accent)]">
活动过滤器 (${ (filterTags.length > 0 ? 1 : 0) + (filterDatePreset ||
filterDateRange.start || filterDateRange.end ? 1 : 0) + (filterTextQuery ? 1
: 0) })
</span>
</span>
<i
:class="['fas fa-chevron-down transition-transform text-[var(--text-muted)]', showAdvancedFilters ? 'rotate-180' : '']"></i>
</button>
<button
v-if="searchQuery || filterTags.length > 0 || filterDatePreset || filterDateRange.start || filterDateRange.end || filterTextQuery"
@click="clearAllFilters"
class="px-3 py-2 bg-[var(--bg-input)] border border-[var(--border-secondary)] rounded-lg text-sm hover:bg-[var(--bg-tertiary)] hover:text-[var(--text-primary)] transition-colors"
:title="t('buttons.clearAllFilters')">
<i class="fas fa-times text-[var(--text-muted)]"></i>
</button>
</div>
<!-- Advanced Filters Panel -->
<div v-show="showAdvancedFilters"
class="p-3 bg-[var(--bg-tertiary)] border border-[var(--border-secondary)] rounded-lg space-y-3">
<!-- Text Search -->
<div>
<label class="block text-xs font-medium text-[var(--text-muted)] mb-1"
v-text="t('sidebar.filters')"></label>
<div class="relative">
<input v-model="filterTextQuery" type="text"
:placeholder="t('sidebar.searchRecordings')"
class="w-full px-3 py-1.5 pl-8 pr-8 bg-[var(--bg-input)] border border-[var(--border-secondary)] rounded-md text-sm focus:outline-none focus:ring-1 focus:ring-[var(--ring-focus)]">
<i
class="fas fa-search absolute left-2.5 top-1/2 transform -translate-y-1/2 text-[var(--text-muted)] text-xs"></i>
<button v-if="filterTextQuery" @click="filterTextQuery = ''"
class="absolute right-2.5 top-1/2 transform -translate-y-1/2 text-[var(--text-muted)] hover:text-[var(--text-primary)] text-xs"
:title="t('buttons.clearSearchText')">
<i class="fas fa-times"></i>
</button>
</div>
</div>
<!-- Tag Filter -->
<div>
<label class="block text-xs font-medium text-[var(--text-muted)] mb-1"
v-text="t('tags.filterByTag')"></label>
<div class="flex flex-wrap gap-1.5">
<button v-for="tag in availableTags" :key="tag.id"
@click="filterTags.includes(tag.id) ? filterTags = filterTags.filter(id => id !== tag.id) : filterTags.push(tag.id)"
:class="[
'px-2 py-1 rounded-full text-xs font-medium transition-all',
filterTags.includes(tag.id)
? 'bg-[var(--bg-accent)] text-[var(--text-accent)] ring-1 ring-[var(--border-accent)]'
: 'bg-[var(--bg-secondary)] text-[var(--text-secondary)] hover:bg-[var(--bg-input)]'
]">
<span class="inline-block w-2 h-2 rounded-full mr-1"
:style="{ backgroundColor: tag.color || '#6B7280' }"></span>
${ tag.name }
</button>
</div>
<p v-if="availableTags.length === 0" class="text-xs text-[var(--text-muted)] italic"
v-text="t('tags.noTags')"></p>
</div>
<!-- Date Filter -->
<div>
<label class="block text-xs font-medium text-[var(--text-muted)] mb-1"
v-text="t('sidebar.dateRange')"></label>
<div class="grid grid-cols-3 gap-1.5 mb-2">
<button v-for="preset in datePresetOptions" :key="preset.value"
@click="filterDatePreset = filterDatePreset === preset.value ? '' : preset.value; filterDateRange = { start: '', end: '' }"
:class="[
'px-2 py-1 rounded-md text-xs transition-all',
filterDatePreset === preset.value
? 'bg-[var(--bg-accent)] text-[var(--text-accent)]'
: 'bg-[var(--bg-secondary)] text-[var(--text-secondary)] hover:bg-[var(--bg-input)]'
]" :title="preset.label">
${ preset.label }
</button>
</div>
<div class="grid grid-cols-2 gap-2">
<div>
<input v-model="filterDateRange.start" @change="filterDatePreset = ''"
type="date"
class="w-full px-2 py-1 bg-[var(--bg-input)] border border-[var(--border-secondary)] rounded-md text-xs focus:outline-none focus:ring-1 focus:ring-[var(--ring-focus)]"
:placeholder="t('form.dateFrom')">
</div>
<div>
<input v-model="filterDateRange.end" @change="filterDatePreset = ''"
type="date"
class="w-full px-2 py-1 bg-[var(--bg-input)] border border-[var(--border-secondary)] rounded-md text-xs focus:outline-none focus:ring-1 focus:ring-[var(--ring-focus)]"
:placeholder="t('form.dateTo')">
</div>
</div>
</div>
</div>
<!-- Sort Toggle -->
<button @click="sortBy = sortBy === 'created_at' ? 'meeting_date' : 'created_at'"
class="w-full flex items-center justify-between px-3 py-2 bg-[var(--bg-input)] border border-[var(--border-secondary)] rounded-lg text-sm hover:bg-[var(--bg-tertiary)] transition-colors">
<span class="flex items-center">
<i
:class="['fas mr-2 text-[var(--text-muted)]', sortBy === 'meeting_date' ? 'fa-calendar' : 'fa-upload']"></i>
<span v-text="t('sidebar.sortBy')"></span>: <span
v-text="sortBy === 'meeting_date' ? t('sidebar.sortByMeetingDate') : t('sidebar.sortByDate')"></span>
</span>
<i class="fas fa-exchange-alt text-[var(--text-muted)]"></i>
</button>
</div>
</div>
<!-- Recordings List -->
<!-- Draft Recordings (草稿箱) -->
<div v-if="draftRecordings.length > 0" class="px-4 pt-4">
<div @click="draftsExpanded = !draftsExpanded"
class="flex items-center justify-between cursor-pointer p-2 bg-[var(--bg-tertiary)] rounded-lg border border-[var(--border-secondary)] hover:bg-[var(--bg-accent-hover)] transition-colors">
<span class="font-medium text-sm text-[var(--text-primary)]">
<i class="fas fa-edit mr-2 text-yellow-500"></i>未完成的录音 (${draftRecordings.length})
</span>
<i :class="draftsExpanded ? 'fa-chevron-up' : 'fa-chevron-down'" class="fas text-[var(--text-muted)] text-xs"></i>
</div>
<div v-show="draftsExpanded" class="mt-2 space-y-2">
<div v-for="draft in draftRecordings" :key="draft.id"
class="p-3 bg-[var(--bg-secondary)] rounded-lg border border-[var(--border-secondary)] hover:border-[var(--border-accent)] transition-colors">
<div class="flex justify-between items-center mb-2">
<div class="flex-1 mr-2 min-w-0">
<input v-if="editingDraftId === draft.id"
v-model="editingDraftName"
@blur="saveDraftRename(draft)"
@keyup.enter="saveDraftRename(draft)"
@click.stop
ref="draftNameInput"
class="w-full bg-[var(--bg-input)] text-[var(--text-primary)] border border-[var(--border-accent)] rounded px-1 py-0.5 text-sm focus:outline-none focus:ring-1 focus:ring-[var(--ring-focus)]">
<span v-else
@dblclick="startDraftRename(draft)"
class="text-sm font-medium text-[var(--text-primary)] truncate block cursor-text hover:text-[var(--text-accent)] transition-colors"
title="双击重命名">${draft.name}</span>
</div>
<span class="text-xs text-[var(--text-muted)] ml-2">${formatTime(Math.round(draft.total_duration || 0))}</span>
</div>
<div class="flex items-center justify-between">
<span class="text-xs text-[var(--text-muted)]">
<i class="fas fa-layer-group mr-1"></i>${draft.segment_count || 0} 个片段
</span>
<div class="flex gap-2">
<button @click="continueDraftRecording(draft.id)"
class="text-xs px-2 py-1 bg-green-500 text-white rounded hover:bg-green-600 transition-colors"
title="继续录音">
<i class="fas fa-play mr-1"></i>继续
</button>
<button @click="deleteDraft(draft.id)"
class="text-xs px-2 py-1 bg-red-500 text-white rounded hover:bg-red-600 transition-colors"
title="删除草稿">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
</div>
</div>
</div>
<div class="flex-1 overflow-y-auto p-4" @scroll="(e) => {
const element = e.target;
const threshold = 100; // Load more when 100px from bottom
if (element.scrollHeight - element.scrollTop - element.clientHeight < threshold) {
loadMoreRecordings();
}
}">
<div v-if="isLoadingRecordings && recordings.length === 0" class="text-center py-8">
<i class="fas fa-spinner fa-spin text-2xl text-[var(--text-muted)]"></i>
<p class="mt-2 text-[var(--text-muted)]" v-text="t('help.loadingRecordings')"></p>
</div>
<div v-else-if="recordings.length === 0 && !isLoadingRecordings" class="text-center py-8">
<i class="fas fa-microphone-slash text-3xl text-[var(--text-muted)] mb-3"></i>
<p class="text-[var(--text-muted)]" v-text="t('sidebar.noRecordings')"></p>
<p v-if="searchQuery.trim()" class="text-sm text-[var(--text-muted)] mt-1">
<span v-text="t('help.tryAdjustingSearch')"></span> <button @click="clearAllFilters()"
class="text-[var(--text-accent)] hover:underline"
v-text="t('help.clearFilters')"></button>
</p>
</div>
<div v-if="recordings.length > 0" class="space-y-2">
<div v-for="group in groupedRecordings" :key="group.title" class="mb-6">
<h3 class="text-xs font-semibold text-[var(--text-muted)] uppercase tracking-wide mb-2">
${group.title}
</h3>
<div class="space-y-1">
<div v-for="recording in group.items" :key="recording.id"
@click="selectRecording(recording)" :class="[
'p-3 rounded-lg cursor-pointer transition-all duration-200 border',
selectedRecording?.id === recording.id
? 'bg-[var(--bg-accent)] border-[var(--border-accent)]'
: 'bg-[var(--bg-tertiary)] border-transparent hover:bg-[var(--bg-accent-hover)]'
]">
<!-- Title and Status Row -->
<div class="flex items-center justify-between mb-1">
<h4 class="font-medium text-sm truncate flex-1 mr-2"
:class="selectedRecording?.id === recording.id ? 'text-[var(--text-accent)]' : 'text-[var(--text-primary)]'">
${ recording.title || t('common.untitled') }
</h4>
<div class="flex items-center gap-1 flex-shrink-0">
<!-- Show Failed status for failed recordings -->
<span v-if="recording.status === 'FAILED'"
class="status-badge status-failed">
<i class="fas fa-exclamation-circle mr-1"></i>
${ t('common.failed') }
</span>
<!-- Show Processing status for non-completed recordings -->
<span v-else-if="recording.status !== 'COMPLETED'"
:class="getStatusClass(recording.status)" class="status-badge">
${formatStatus(recording.status)}
</span>
<!-- For completed recordings, show highlight and inbox badges -->
<template v-else>
<span v-if="recording.is_highlighted"
@click.stop="toggleHighlight(recording)"
class="status-badge status-highlighted clickable-badge"
:title="t('sidebar.removeFromHighlighted')">
<i class="fas fa-star"></i>
</span>
<span v-if="recording.is_inbox" @click.stop="toggleInbox(recording)"
class="status-badge status-inbox clickable-badge"
:title="t('sidebar.markAsRead')">
<i class="fas fa-inbox"></i>
</span>
</template>
</div>
</div>
<!-- Date -->
<p class="text-xs text-[var(--text-muted)] mt-1 flex items-center">
<i class="fas fa-calendar-alt mr-1.5 text-[var(--text-muted)]"
style="font-size: 10px;"></i>
${formatDisplayDate(sortBy === 'meeting_date' ? recording.meeting_date :
recording.created_at)}
</p>
<!-- Participants -->
<div v-if="recording.participants"
class="text-xs text-[var(--text-muted)] mt-1 flex items-start">
<i class="fas fa-users mr-1.5 text-[var(--text-muted)] flex-shrink-0"
style="font-size: 10px; margin-top: 2px;"></i>
<span class="truncate">
${recording.participants}
</span>
</div>
<!-- Tags -->
<div v-if="getRecordingTags(recording).length > 0"
class="flex flex-wrap gap-1 mt-1">
<button v-for="tag in getRecordingTags(recording).slice(0, 2)" :key="tag.id"
@click.stop="filterByTag(tag)"
class="inline-flex items-center px-1.5 py-0.5 rounded-md text-xs font-medium text-white hover:opacity-80 transition-opacity cursor-pointer"
:style="{ backgroundColor: tag.color }"
:title="'Filter by ' + tag.name">
<span v-text="tag.name"></span>
</button>
<span v-if="getRecordingTags(recording).length > 2"
class="inline-flex items-center px-1.5 py-0.5 rounded-md text-xs text-[var(--text-muted)]"
:title="getRecordingTags(recording).slice(2).map(t => t.name).join(', ')">
+${getRecordingTags(recording).length - 2} more
</span>
</div>
</div>
</div>
</div>
<!-- Load More Indicator -->
<div v-if="isLoadingMore" class="text-center py-4">
<i class="fas fa-spinner fa-spin text-lg text-[var(--text-muted)]"></i>
<p class="text-sm text-[var(--text-muted)] mt-1" v-text="t('help.loadingMore')"></p>
</div>
<!-- End of Results Indicator -->
<div v-else-if="!hasNextPage && totalRecordings > 0"
class="text-center py-4 text-xs text-[var(--text-muted)]">
<i class="fas fa-check-circle mr-1"></i>
<span v-text="t('help.allRecordingsLoaded')"></span>
</div>
</div>
</div>
</div>
</aside>
<!-- ==================== 功能块:录音列表侧边栏 End ==================== -->
<!-- ==================== 功能块:主内容视图区 Start ==================== -->
<!-- Main Content -->
<main class="main-content" :class="{ 'sidebar-open': !isSidebarCollapsed }">
<div class="flex-1 flex flex-col overflow-hidden">
<!-- ==================== 功能块:上传与新建录音视图 Start ==================== -->
<!-- Upload View -->
<div v-if="currentView === 'upload'"
class="flex-1 flex items-center justify-center p-4 md:p-8 overflow-y-auto">
<div class="max-w-lg w-full my-auto">
<div class="text-center mb-8">
<i class="fas fa-microphone text-5xl md:text-6xl text-[var(--text-accent)] mb-4"></i>
<h2 class="text-2xl md:text-3xl font-bold mb-2" v-text="t('upload.title')"></h2>
<p class="text-[var(--text-muted)]" v-text="t('upload.dropzone')"></p>
</div>
<div class="bg-[var(--bg-secondary)] p-6 rounded-xl border border-[var(--border-primary)]">
<!-- File Upload Area -->
<div @drop="handleDrop" @dragover="handleDragOver" @dragleave="handleDragLeave"
@click="$refs.fileInput.click()"
:class="['border-2 border-dashed rounded-xl p-8 text-center cursor-pointer transition-all duration-300', dragover ? 'border-[var(--border-accent)] bg-[var(--bg-accent)]' : 'border-[var(--border-secondary)] hover:border-[var(--border-accent)]']">
<i class="fas fa-cloud-upload-alt text-3xl text-[var(--text-muted)] mb-3"></i>
<p class="text-[var(--text-secondary)] mb-2" v-text="t('upload.dropzone')"></p>
<p class="text-xs text-[var(--text-muted)]" v-text="t('upload.supportedFormats')">
</p>
</div>
<input ref="fileInput" type="file" @change="handleFileSelect"
accept="audio/*,video/mp4,video/quicktime,video/x-msvideo,video/webm,.mp4,.mov,.avi,.mkv,.webm"
multiple class="hidden">
<!-- Divider -->
<div class="my-6 flex items-center">
<div class="flex-grow border-t border-[var(--border-secondary)]"></div>
<span class="flex-shrink mx-4 text-xs text-[var(--text-muted)] uppercase"
v-text="t('common.or')"></span>
<div class="flex-grow border-t border-[var(--border-secondary)]"></div>
</div>
<!-- Audio Recording Options -->
<div class="space-y-3">
<h3 class="text-sm font-medium text-[var(--text-secondary)] text-center"
v-text="t('recording.title')"></h3>
<!-- Microphone Recording -->
<button @click="startRecording('microphone')"
class="w-full px-4 py-3 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors flex items-center justify-center gap-2">
<i class="fas fa-microphone"></i>
<span v-text="t('recording.microphone')"></span>
</button>
<!-- System Audio Recording -->
<button @click="startRecording('system')" v-if="canRecordSystemAudio"
class="w-full px-4 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors flex items-center justify-center gap-2">
<i class="fas fa-desktop"></i>
<span v-text="t('recording.systemAudio')"></span>
</button>
<!-- Both Audio Sources -->
<button @click="startRecording('both')" v-if="canRecordSystemAudio"
class="w-full px-4 py-3 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors flex items-center justify-center gap-2">
<i class="fas fa-microphone"></i>
<i class="fas fa-desktop ml-1"></i>
<span v-text="t('recording.microphoneAndSystem')"></span>
</button>
<!-- Help Text -->
<div class="text-xs text-[var(--text-muted)] space-y-1 mt-3">
<div class="flex items-center justify-between">
<span><strong v-text="t('recording.microphone') + ':'"></strong> <span
v-text="t('help.microphoneDesc')"></span></span>
<i class="fas fa-microphone text-red-500 flex-shrink-0"></i>
</div>
<div v-if="canRecordSystemAudio" class="flex items-center justify-between">
<span><strong v-text="t('recording.systemAudio') + ':'"></strong> <span
v-text="t('help.systemAudioDesc')"></span></span>
<i class="fas fa-desktop text-blue-500 flex-shrink-0"></i>
</div>
<div v-if="canRecordSystemAudio" class="flex items-center justify-between">
<span><strong v-text="t('recording.microphoneAndSystem') + ':'"></strong>
<span v-text="t('help.bothAudioDesc')"></span></span>
<div class="flex items-center gap-1 flex-shrink-0">
<i class="fas fa-microphone text-purple-500"></i>
<i class="fas fa-desktop text-purple-500"></i>
</div>
</div>
<div v-if="!canRecordSystemAudio" class="text-amber-600 dark:text-amber-400">
<i class="fas fa-exclamation-triangle mr-1"></i>
${ t('recording.systemAudioNotSupported') }
<button @click="showSystemAudioHelp = true"
class="ml-2 text-blue-500 hover:text-blue-600 underline text-sm">
<span v-text="t('buttons.help')"></span>
</button>
</div>
</div>
</div>
<!-- Tag Selection -->
<div class="mt-4">
<label class="block text-sm font-medium text-[var(--text-secondary)] mb-2">
<i class="fas fa-tags mr-1 text-[var(--text-muted)]"></i>
${ t('tags.title') }
</label>
<!-- Selected Tags Display - Compact -->
<div v-if="selectedTags.length > 0" class="mb-2">
<div
class="p-1.5 bg-[var(--bg-tertiary)] rounded-md border border-[var(--border-secondary)] max-h-16 overflow-y-auto">
<div class="flex flex-wrap gap-1">
<span v-for="(tag, index) in selectedTags" :key="tag.id"
class="inline-flex items-center px-1.5 py-0.5 rounded-full text-[11px] font-medium text-white"
:style="{ backgroundColor: tag.color }">
<span class="opacity-75 mr-0.5 text-[9px]">${index + 1}.</span>
<span>${tag.name}</span>
<button @click="removeTagFromSelection(tag.id)"
class="ml-1 hover:opacity-100 opacity-70">
<i class="fas fa-times" style="font-size: 9px;"></i>
</button>
</span>
</div>
</div>
<p class="text-[10px] text-[var(--text-muted)] mt-0.5">
<i class="fas fa-info-circle mr-0.5" style="font-size: 9px;"></i>
Tags ordered by priority
</p>
</div>
<!-- Tag Selection Container - Compact -->
<div v-if="availableTags.filter(tag => !selectedTagIds.includes(tag.id)).length > 0"
class="border border-[var(--border-secondary)] rounded-md bg-[var(--bg-tertiary)] p-2">
<!-- Search Filter (always visible if tags available) -->
<div class="mb-1.5">
<div class="relative">
<input v-model="uploadTagSearchFilter" type="text"
:placeholder="t('tagsModal.searchTags')"
class="w-full px-2 py-1 pl-6 text-xs bg-[var(--bg-primary)] text-[var(--text-primary)] border border-[var(--border-primary)] rounded focus:outline-none focus:ring-1 focus:ring-[var(--ring-focus)]">
<i class="fas fa-search absolute left-2 top-1/2 transform -translate-y-1/2 text-[var(--text-muted)]"
style="font-size: 9px;"></i>
<button v-if="uploadTagSearchFilter" @click="uploadTagSearchFilter = ''"
class="absolute right-2 top-1/2 transform -translate-y-1/2 text-[var(--text-muted)] hover:text-[var(--text-primary)]">
<i class="fas fa-times" style="font-size: 9px;"></i>
</button>
</div>
</div>
<!-- Available Tags Grid - Fixed Height -->
<div v-if="filteredAvailableTagsForUpload.length > 0" class="overflow-y-auto"
style="max-height: 100px;">
<div class="grid grid-cols-2 gap-1">
<button v-for="tag in filteredAvailableTagsForUpload" :key="tag.id"
@click="addTagToSelection(tag.id)"
class="group flex items-center justify-between px-1.5 py-1 rounded border border-[var(--border-secondary)] hover:border-[var(--border-focus)] bg-[var(--bg-primary)] hover:bg-[var(--bg-secondary)] transition-all">
<div class="flex items-center gap-1 min-w-0">
<span class="w-2 h-2 rounded-full flex-shrink-0"
:style="{ backgroundColor: tag.color || '#6B7280' }"></span>
<span class="text-[11px] text-[var(--text-primary)] truncate"
v-text="tag.name"></span>
<span class="text-[11px] text-[var(--text-primary)] truncate"
v-text="tag.visibility=='global'?'(全局)':'(个人)'"></span>
</div>
<i class="fas fa-plus text-[var(--text-muted)] group-hover:text-[var(--text-accent)] transition-colors"
style="font-size: 9px;"></i>
</button>
</div>
</div>
<div v-else class="py-2 text-center">
<p class="text-[11px] text-[var(--text-muted)]">
<i class="fas fa-search mr-1" style="font-size: 9px;"></i>
No matching tags
</p>
</div>
</div>
<!-- Empty States -->
<div v-else-if="availableTags.length === 0"
class="p-2 bg-[var(--bg-tertiary)] rounded-md border border-[var(--border-secondary)]">
<p class="text-xs text-[var(--text-muted)] text-center">
<i class="fas fa-info-circle mr-1" style="font-size: 10px;"></i>
<span v-text="t('help.noTagsCreated')"></span> <a
href="/tool/speakr/account#tags"
class="text-[var(--text-accent)] hover:underline"
v-text="t('help.createTags')"></a>
</p>
</div>
<div v-else
class="p-2 bg-[var(--bg-tertiary)] rounded-md border border-[var(--border-secondary)]">
<p class="text-xs text-[var(--text-muted)] text-center">
<i class="fas fa-check-circle mr-1" style="font-size: 10px;"></i>
All tags selected
</p>
</div>
<p v-if="selectedTags.some(tag => tag.custom_prompt)"
class="text-xs text-[var(--text-muted)] mt-1">
<i class="fas fa-info-circle mr-1 text-[var(--text-accent)]"></i>
Selected tags include custom summary prompts
</p>
<p v-if="selectedTags.length > 0 && useAsrEndpoint && selectedTags.some(tag => tag.default_language || tag.default_min_speakers || tag.default_max_speakers)"
class="text-xs text-[var(--text-muted)] mt-1">
<i class="fas fa-cog mr-1 text-[var(--text-accent)]"></i>
First tag's ASR settings will be applied: ${selectedTags[0].name}
</p>
</div>
<!-- Advanced Options for ASR (collapsible) -->
<div v-if="useAsrEndpoint" class="mt-4">
<button @click="showAdvancedOptions = !showAdvancedOptions"
class="w-full flex items-center justify-between px-3 py-2 bg-[var(--bg-tertiary)] rounded-md hover:bg-[var(--bg-accent)] transition-colors text-xs font-medium">
<span class="flex items-center gap-2 text-[var(--text-secondary)]">
<i class="fas fa-cog text-[var(--text-muted)]"></i>
<span v-text="t('help.advancedAsrOptions')"></span>
</span>
<i
:class="['fas text-[var(--text-muted)]', showAdvancedOptions ? 'fa-chevron-up' : 'fa-chevron-down']"></i>
</button>
<div v-show="showAdvancedOptions"
class="mt-2 p-3 bg-[var(--bg-tertiary)] rounded-md space-y-3 border border-[var(--border-secondary)]">
<!-- Language Selection -->
<div>
<label class="block text-xs font-medium text-[var(--text-secondary)] mb-1">
转录语言
</label>
<select v-model="uploadLanguage"
class="w-full px-2 py-1 text-sm bg-[var(--bg-primary)] text-[var(--text-primary)] border border-[var(--border-primary)] rounded focus:outline-none focus:ring-1 focus:ring-[var(--ring-focus)]">
<option v-for="lang in languageOptions" :key="lang.value"
:value="lang.value" v-text="lang.label"></option>
</select>
</div>
<!-- Speaker Settings -->
<div class="grid grid-cols-2 gap-3">
<div>
<label
class="block text-xs font-medium text-[var(--text-secondary)] mb-1"
v-text="t('form.minSpeakers')">
</label>
<input v-model.number="uploadMinSpeakers" type="number" min="1" max="20"
class="w-full px-2 py-1 text-sm bg-[var(--bg-primary)] text-[var(--text-primary)] border border-[var(--border-primary)] rounded focus:outline-none focus:ring-1 focus:ring-[var(--ring-focus)]">
</div>
<div>
<label
class="block text-xs font-medium text-[var(--text-secondary)] mb-1"
v-text="t('form.maxSpeakers')">
</label>
<input v-model.number="uploadMaxSpeakers" type="number" min="1" max="20"
class="w-full px-2 py-1 text-sm bg-[var(--bg-primary)] text-[var(--text-primary)] border border-[var(--border-primary)] rounded focus:outline-none focus:ring-1 focus:ring-[var(--ring-focus)]">
</div>
</div>
<p class="text-xs text-[var(--text-muted)] pt-1">
<i class="fas fa-info-circle mr-1 text-[var(--text-accent)]"></i>
设置适用于此会话中的所有文件
</p>
</div>
</div>
</div>
</div>
</div>
<!-- ==================== 功能块:上传与新建录音视图 End ==================== -->
<!-- ==================== 功能块:实时录音视图 Start ==================== -->
<!-- Recording View -->
<div v-else-if="currentView === 'recording'" class="index_m_cot">
<div class="websockt_box border border-[var(--border-primary)] min-h-0">
<!-- Top: Visualizer and Status -->
<div
class="flex-shrink-0 p-4 md:p-6 text-center bg-[var(--bg-tertiary)] border-b border-[var(--border-primary)]">
<div v-if="isRecording || isPaused" class="w-full mx-auto mb-4">
<!-- Dual visualizer for 'both' mode -->
<div v-if="recordingMode === 'both'" class="max-w-4xl mx-auto flex gap-4">
<div
class="w-1/2 min-h-[12rem] h-48 md:h-40 flex flex-col bg-[var(--bg-primary)] rounded-lg p-2">
<div class="flex-1 w-full overflow-hidden rounded-md"
style="min-height: 10rem;">
<canvas ref="micVisualizer" class="w-full h-full"></canvas>
</div>
<p class="text-xs text-[var(--text-muted)] pt-1"
v-text="t('recording.microphone')"></p>
</div>
<div
class="w-1/2 min-h-[12rem] h-48 md:h-40 flex flex-col bg-[var(--bg-primary)] rounded-lg p-2">
<div class="flex-1 w-full overflow-hidden rounded-md"
style="min-height: 10rem;">
<canvas ref="systemVisualizer" class="w-full h-full"></canvas>
</div>
<p class="text-xs text-[var(--text-muted)] pt-1"
v-text="t('recording.systemAudio')"></p>
</div>
</div>
<!-- Single visualizer for other modes -->
<div v-else
class="min-h-[12rem] h-48 md:h-40 max-w-2xl mx-auto bg-[var(--bg-primary)] rounded-lg p-2 flex flex-col">
<div class="flex-1 w-full overflow-hidden rounded-md"
style="min-height: 10rem;">
<canvas ref="visualizer" class="w-full h-full"></canvas>
</div>
<p class="text-xs text-[var(--text-muted)] pt-1 capitalize">${recordingMode}</p>
</div>
</div>
<div v-if="!isRecording && !isPaused && audioBlobURL" class="w-full mb-4">
<div class="audio-player-container">
<audio :src="audioBlobURL" controls class="w-full">
Your browser does not support the audio element.
</audio>
</div>
</div>
<div>
<div class="text-2xl font-mono text-[var(--text-accent)]">
${formatTime(recordingTime)}</div>
<p class="text-sm text-[var(--text-muted)]">
${ isRecording ? '正在录制...' : (isPaused ? '录音已暂停' : (audioBlobURL ? '录制完成' : '准备录制')) }
</p>
<!-- Real-time file size display -->
<div v-if="(isRecording || isPaused) && estimatedFileSize > 0" class="mt-2">
<p class="text-xs text-[var(--text-muted)]">
预估大小: <span class="font-mono">${formatFileSize(estimatedFileSize)}</span>
<span v-if="actualBitrate > 0" class="ml-2">
(${Math.round(actualBitrate / 1000)}kbps)
</span>
</p>
<!-- Size warning indicator -->
<div v-if="estimatedFileSize > (maxRecordingMB * 1024 * 1024 * 0.8)"
class="mt-1">
<div class="flex items-center text-xs text-amber-600 dark:text-amber-400">
<i class="fas fa-exclamation-triangle mr-1"></i>
<span
v-text="t('help.approachingLimit', { limit: maxRecordingMB })"></span>
</div>
</div>
</div>
</div>
</div>
<!-- Middle: Notepad -->
<div class="flex-1 p-4 md:p-6 min-h-0 overflow-hidden flex flex-col">
<label for="recordingNotes"
class="block text-sm font-medium text-[var(--text-secondary)] mb-2 flex-shrink-0">
<i class="fas fa-pencil-alt mr-1"></i>
编写笔记
</label>
<div class="flex-1 min-h-0">
<!-- <textarea ref="recordingNotesEditor" v-model="recordingNotes"
class="w-full h-80 p-2 border border-[var(--border-primary)] rounded-md bg-[var(--bg-secondary)] text-[var(--text-primary)] resize-none overflow-y-auto"
:placeholder="t('form.notesPlaceholder')"
rows="10"></textarea> -->
<div>
<textarea style="height: 322px;"
class="w-full h-180 p-2 border border-[var(--border-primary)] rounded-md bg-[var(--bg-secondary)] text-[var(--text-primary)] resize-none overflow-y-auto"
v-model="recordingNotes"></textarea>
</div>
</div>
</div>
<!-- Bottom: Action Buttons -->
<div class="flex-shrink-0 p-4 md:p-6 bg-[var(--bg-tertiary)] border-[var(--border-primary)]"
style="
background: none;">
<div v-if="showRecordingControls" class="text-center flex justify-center gap-3">
<!-- 暂停按钮 (录音中显示) -->
<button v-if="isRecording" @click="pauseRecording"
class="px-6 py-3 bg-yellow-500 text-white rounded-lg hover:bg-yellow-600 transition-colors">
<i class="fas fa-pause mr-2"></i>暂停录音
</button>
<!-- 恢复按钮 (暂停时显示) -->
<button v-if="isPaused" @click="resumeRecording"
class="px-6 py-3 bg-green-500 text-white rounded-lg hover:bg-green-600 transition-colors">
<i class="fas fa-play mr-2"></i>继续录音
</button>
<!-- 停止按钮 -->
<button @click="stopRecording"
class="px-6 py-3 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors"
:class="{
'animate-pulse': isRecording,
'opacity-75 cursor-not-allowed': isStoppingRecording
}"
:disabled="isStoppingRecording">
<i class="fas fa-stop mr-2"></i>停止录音
</button>
</div>
<div v-if="!isRecording && !isPaused && (audioBlobURL || currentDraftId)" class="flex flex-col gap-4">
<!-- Tag Selection -->
<div>
<label for="recordingTagSelect"
class="block text-sm font-medium text-[var(--text-secondary)] mb-2">
<i class="fas fa-tags mr-1"></i>
选择标签(可选,按优先级顺序)
</label>
<!-- Selected Tags Display - Compact -->
<div v-if="selectedTags.length > 0" class="mb-2">
<div
class="p-1.5 bg-[var(--bg-tertiary)] rounded-md border border-[var(--border-secondary)] max-h-16 overflow-y-auto">
<div class="flex flex-wrap gap-1">
<span v-for="(tag, index) in selectedTags" :key="tag.id"
class="inline-flex items-center px-1.5 py-0.5 rounded-full text-[11px] font-medium text-white"
:style="{ backgroundColor: tag.color }">
<span class="opacity-75 mr-0.5 text-[9px]">${index + 1}.</span>
<span>${tag.name}</span>
<button @click="removeTagFromSelection(tag.id)"
class="ml-1 hover:opacity-100 opacity-70">
<i class="fas fa-times" style="font-size: 9px;"></i>
</button>
</span>
</div>
</div>
</div>
<!-- Tag Selection Container - Compact -->
<div v-if="availableTags.filter(tag => !selectedTagIds.includes(tag.id)).length > 0"
class="border border-[var(--border-secondary)] rounded-md bg-[var(--bg-tertiary)] p-2">
<!-- Search Filter -->
<div class="mb-1.5">
<div class="relative">
<input v-model="uploadTagSearchFilter" type="text"
:placeholder="t('tagsModal.searchTags')"
class="w-full px-2 py-1 pl-6 text-xs bg-[var(--bg-primary)] text-[var(--text-primary)] border border-[var(--border-primary)] rounded focus:outline-none focus:ring-1 focus:ring-[var(--ring-focus)]">
<i class="fas fa-search absolute left-2 top-1/2 transform -translate-y-1/2 text-[var(--text-muted)]"
style="font-size: 9px;"></i>
<button v-if="uploadTagSearchFilter"
@click="uploadTagSearchFilter = ''"
class="absolute right-2 top-1/2 transform -translate-y-1/2 text-[var(--text-muted)] hover:text-[var(--text-primary)]">
<i class="fas fa-times" style="font-size: 9px;"></i>
</button>
</div>
</div>
<!-- Available Tags Grid -->
<div v-if="filteredAvailableTagsForUpload.length > 0"
class="overflow-y-auto" style="max-height: 100px;">
<div class="grid grid-cols-2 gap-1">
<button v-for="tag in filteredAvailableTagsForUpload" :key="tag.id"
@click="addTagToSelection(tag.id)"
class="group flex items-center justify-between px-1.5 py-1 rounded border border-[var(--border-secondary)] hover:border-[var(--border-focus)] bg-[var(--bg-primary)] hover:bg-[var(--bg-secondary)] transition-all">
<div class="flex items-center gap-1 min-w-0">
<span class="w-2 h-2 rounded-full flex-shrink-0"
:style="{ backgroundColor: tag.color || '#6B7280' }"></span>
<span
class="text-[11px] text-[var(--text-primary)] truncate"
v-text="tag.name"></span>
</div>
<i class="fas fa-plus text-[var(--text-muted)] group-hover:text-[var(--text-accent)] transition-colors"
style="font-size: 9px;"></i>
</button>
</div>
</div>
<div v-else class="py-2 text-center">
<p class="text-[11px] text-[var(--text-muted)]">
<i class="fas fa-search mr-1" style="font-size: 9px;"></i>
No matching tags
</p>
</div>
</div>
<!-- Empty States -->
<div v-else-if="availableTags.length === 0"
class="p-2 bg-[var(--bg-tertiary)] rounded-md border border-[var(--border-secondary)]">
<p class="text-xs text-[var(--text-muted)] text-center">
<i class="fas fa-info-circle mr-1" style="font-size: 10px;"></i>
<span v-text="t('help.noTagsCreated')"></span> <a
href="/tool/speakr/account#tags"
class="text-[var(--text-accent)] hover:underline"
v-text="t('help.createTags')"></a>
</p>
</div>
<div v-else
class="p-2 bg-[var(--bg-tertiary)] rounded-md border border-[var(--border-secondary)]">
<p class="text-xs text-[var(--text-muted)] text-center">
<i class="fas fa-check-circle mr-1" style="font-size: 10px;"></i>
All tags selected
</p>
</div>
<p v-if="selectedTags.some(tag => tag.custom_prompt)"
class="text-xs text-[var(--text-muted)] mt-1">
<i class="fas fa-info-circle mr-1 text-[var(--text-accent)]"></i>
Selected tags include custom summary prompts
</p>
<p v-if="selectedTags.length > 0 && useAsrEndpoint && selectedTags.some(tag => tag.default_language || tag.default_min_speakers || tag.default_max_speakers)"
class="text-xs text-[var(--text-muted)] mt-1">
<i class="fas fa-cog mr-1 text-[var(--text-accent)]"></i>
First tag's ASR settings will be applied: ${selectedTags[0].name}
</p>
</div>
<!-- Advanced Options for ASR (collapsible) -->
<div v-if="useAsrEndpoint">
<button @click="showAdvancedOptions = !showAdvancedOptions"
class="w-full flex items-center justify-between px-3 py-2 bg-[var(--bg-tertiary)] rounded-md hover:bg-[var(--bg-accent)] transition-colors text-xs font-medium">
<span class="flex items-center gap-2 text-[var(--text-secondary)]">
<i class="fas fa-cog text-[var(--text-muted)]"></i>
<span v-text="t('help.advancedAsrOptions')"></span>
</span>
<i
:class="['fas text-[var(--text-muted)]', showAdvancedOptions ? 'fa-chevron-up' : 'fa-chevron-down']"></i>
</button>
<div v-if="showAdvancedOptions"
class="mt-2 p-3 bg-[var(--bg-tertiary)] rounded-md border border-[var(--border-secondary)] space-y-3">
<div>
<label for="recordingAsrLanguage"
class="block text-xs font-medium text-[var(--text-secondary)] mb-1">
语言
</label>
<select id="recordingAsrLanguage" v-model="asrLanguage"
class="w-full px-2 py-1 bg-[var(--bg-primary)] text-[var(--text-primary)] border border-[var(--border-secondary)] rounded-md shadow-sm focus:outline-none focus:ring-1 focus:ring-[var(--ring-focus)] text-xs">
<option v-for="lang in languageOptions" :key="lang.value"
:value="lang.value" v-text="lang.label"></option>
</select>
</div>
<div class="grid grid-cols-2 gap-3">
<div>
<label for="recordingAsrMinSpeakers"
class="block text-xs font-medium text-[var(--text-secondary)] mb-1">
{% raw %}最少发言人数{% endraw %}
</label>
<input type="number" id="recordingAsrMinSpeakers"
v-model="asrMinSpeakers" min="1" max="20"
:placeholder="t('form.auto')"
class="w-full px-2 py-1 bg-[var(--bg-primary)] text-[var(--text-primary)] border border-[var(--border-secondary)] rounded-md shadow-sm focus:outline-none focus:ring-1 focus:ring-[var(--ring-focus)] text-xs">
</div>
<div>
<label for="recordingAsrMaxSpeakers"
class="block text-xs font-medium text-[var(--text-secondary)] mb-1">
{% raw %}最多发言人{% endraw %}
</label>
<input type="number" id="recordingAsrMaxSpeakers"
v-model="asrMaxSpeakers" min="1" max="20"
:placeholder="t('form.auto')"
class="w-full px-2 py-1 bg-[var(--bg-primary)] text-[var(--text-primary)] border border-[var(--border-secondary)] rounded-md shadow-sm focus:outline-none focus:ring-1 focus:ring-[var(--ring-focus)] text-xs">
</div>
</div>
</div>
</div>
<!-- Upload/Discard Buttons -->
<div class="flex flex-col sm:flex-row gap-3 justify-center">
<button @click="uploadRecordedAudio"
class="flex-1 px-6 py-3 bg-[var(--bg-button)] text-[var(--text-button)] rounded-lg hover:bg-[var(--bg-button-hover)] transition-colors flex items-center justify-center gap-2">
<i class="fas fa-upload"></i>
<span v-text="t('help.uploadRecordingNotes')"></span>
</button>
<button @click="downloadRecordedAudio" v-if="audioBlobURL"
class="px-6 py-3 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors flex items-center justify-center gap-2"
title="Download Recording">
<i class="fas fa-download"></i>
<span>下载录音</span>
</button>
<button @click="discardRecording"
class="px-6 py-3 bg-[var(--bg-danger)] text-white rounded-lg hover:bg-[var(--bg-danger-hover)] transition-colors flex items-center justify-center gap-2">
<i class="fas fa-trash"></i>
<span v-text="t('help.discard')"></span>
</button>
</div>
</div>
</div>
</div>
<div
class="websockt_box border border-[var(--border-primary)] min-h-0 bg-[var(--bg-tertiary)] p-4 space-y-6 rounded-lg">
<!-- 第一个区域:语音转文字结果 -->
<div>
<h2 class="text-lg font-semibold mb-2">语音实时转录 <button style=" margin-left: 20px;"
@click="isyjDialogVisible = true" class="text-gray-300 hover:text-white">
<i class="fas fa-expand"></i>
</button>
<button v-if="!isEditingTranscription" @click="enterEditMode()"
class="ml-4 px-3 py-1 text-sm text-blue-400 hover:text-blue-300 border border-blue-400/30 rounded hover:border-blue-300/50 transition-colors">
<i class="fas fa-pen mr-1"></i>编辑
</button>
<button v-else @click="exitEditMode()"
class="ml-4 px-3 py-1 text-sm text-green-400 hover:text-green-300 border border-green-400/30 rounded hover:border-green-300/50 transition-colors">
<i class="fas fa-check mr-1"></i>完成编辑
</button>
<label class="inline-flex items-center ml-4 cursor-pointer">
<span class="text-gray-300 mr-2">是否显示说话人</span>
<input type="checkbox" v-model="showSpeaker" class="hidden">
<span
class="w-10 h-5 flex items-center bg-gray-500 rounded-full p-1 duration-300 ease-in-out"
:class="{'bg-green-500': showSpeaker}">
<span
class="bg-white w-4 h-4 rounded-full shadow-md transform duration-300 ease-in-out"
:class="{'translate-x-5': showSpeaker}">
</span>
</span>
</label>
</h2>
<textarea v-if="isEditingTranscription" style="height: 650px;"
class="w-full p-2 border-2 border-blue-500 rounded-md bg-[var(--bg-secondary)] text-[var(--text-primary)] resize-none overflow-y-auto"
v-model="editableText"></textarea>
<textarea v-else ref="asrResultTextareaOnline" style="height: 650px;"
class="w-full p-2 border border-[var(--border-primary)] rounded-md bg-[var(--bg-secondary)] text-[var(--text-primary)] resize-none overflow-y-auto"
:value="fullRealtimeTranscription" readonly></textarea>
<!-- <textarea
ref="asrTextarea"
style="height: 320px;"
class="w-full p-2 border border-[var(--border-primary)] rounded-md bg-[var(--bg-secondary)] text-[var(--text-primary)] resize-none overflow-y-auto"
v-model="asrResult+asrResultOnline" readonly></textarea> -->
</div>
<!-- 第二个区域:分析结果 -->
<!-- <div>
<h2 class="text-lg font-semibold mb-2">总结结果</h2>
<textarea
class="w-full h-60 p-2 border border-[var(--border-primary)] rounded-md bg-[var(--bg-secondary)] text-[var(--text-primary)] resize-none overflow-y-auto"
v-model="analysisResult" readonly></textarea>
</div> -->
<div class="flex-shrink-0 bg-[var(--bg-tertiary)] border-[var(--border-primary)]">
<div class="text-center" :style="{ position:'relative',top:'23px' }">
<button @click="openPromptModal('zj')"
class="px-8 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 ">
<i class="fas fa-sync mr-2"></i>${baseInfo.name}
</button>
<!-- <button @click="openmodel" -->
<button @click="openPromptModal('tp')"
class="px-8 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 "
style="margin-left: 10px;">
<i class="fas"></i>推送至指挥员
</button>
</div>
</div>
</div>
<!-- 提示词弹框 -->
<div v-if="promptVisible"
style="z-index: 999;"
class="fixed inset-0 bg-black bg-opacity-50 z-50 flex justify-center items-center">
<div class="bg-[#1f2937] text-white w-1/2 p-6 rounded-lg shadow-xl border border-gray-600 space-y-4">
<h3 class="text-xl font-semibold">选择提示词</h3>
<!-- 推荐提示词标题 -->
<div class="flex items-center justify-between">
<p class="text-base font-medium">推荐提示词</p>
<span class="text-xs text-gray-400">根据你的内容智能推荐</span>
</div>
<!-- 推荐提示词卡片 -->
<div class="bg-[#111827] rounded-lg p-4 shadow-inner hover:shadow-md transition cursor-pointer border border-gray-700 hover:border-blue-500"
@click="selectedPromptId = recommendPromptId">
<div class="flex items-start gap-4">
<input type="radio" :value="recommendPromptId" v-model="selectedPromptId"
class="mt-1 h-4 w-4 accent-blue-500" />
<div class="flex-1 space-y-1">
<div class="font-semibold text-lg leading-6 text-blue-400">
${recommendPromptName}
</div>
<div class="text-sm text-gray-300 leading-5 bg-[#1f2937] p-2 rounded border border-gray-700">
${recommendPromptReason}
</div>
</div>
</div>
</div>
<!-- 提示词列表区域 -->
<div class="bg-[#111827] p-3 rounded max-h-72 overflow-y-auto text-white space-y-2">
<div class="grid grid-cols-[50px_350px] px-2 py-2 border-b border-gray-600 text-sm font-semibold">
<div class="text-center">选择</div>
<div>提示词</div>
</div>
<div id="promptList" class="space-y-2 mt-2">
<div v-for="item in promptList" :key="item.id"
class="grid grid-cols-[50px_350px] px-2 py-2 items-start gap-2 bg-[#1f2937] rounded cursor-pointer border border-transparent hover:border-blue-500 hover:bg-[#1e2531] transition"
@click="selectedPromptId = item.id">
<div class="flex justify-center items-start pt-1">
<input type="radio" :value="item.id" v-model="selectedPromptId" class="h-4 w-4 accent-blue-500" />
</div>
<div class="whitespace-normal break-words leading-5">
${item.name}
</div>
</div>
</div>
</div>
<!-- 按钮区域 -->
<div class="flex justify-end mt-4 gap-3">
<button type="button" class="px-4 py-2 bg-gray-500 rounded hover:bg-gray-600"
@click="closePromptModal">取消</button>
<button type="button" class="px-4 py-2 bg-blue-600 rounded hover:bg-blue-700"
@click="confirmPromptSelection">确定</button>
</div>
</div>
</div>
<!-- 投屏弹框 -->
<div v-if="isModalOpen" class="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<!-- 弹框容器添加相对定位,用于图标绝对定位 -->
<div class="bg-white p-8 rounded-lg w-96 text-center relative">
<!-- 右上角关闭图标 -->
<button @click="closeModal"
class="absolute top-4 right-4 text-gray-500 hover:text-gray-700 transition-colors"
aria-label="关闭">
<i class="fas fa-times text-xl"></i>
</button>
<!--
<h3 class="text-xl font-bold mb-4">投屏链接</h3> -->
<a :href="urlinfo" target="_blank" rel="noopener noreferrer"
class="text-blue-600 hover:text-blue-800 underline text-lg">
${urlmsg}
<!-- 点击打开投屏页面 -->
</a>
</div>
</div>
<!-- 弹框组件 -->
<div v-if="isDialogVisible" class="fixed inset-0 z-50 flex items-center justify-center p-4">
<!-- 遮罩层 -->
<div @click="isDialogVisible = false" class="absolute inset-0 bg-black/50 backdrop-blur-sm">
</div>
<!-- 弹框内容修改这里的class -->
<div
class="relative w-full bg-[var(--bg-secondary)] border border-[var(--border-primary)] rounded-lg shadow-xl overflow-hidden">
<!-- 弹框头部 -->
<div
class="p-4 border-b border-[var(--border-primary)] flex items-center justify-between">
<h2 class="text-lg font-semibold">${baseInfo.name}</h2>
<button @click="isDialogVisible = false" class="text-gray-500 hover:text-gray-700">
<i class="fas fa-times"></i>
</button>
</div>
<!-- 弹框主体 -->
<div class="p-4">
<!-- <textarea ref="sumupResultTextarea"
:style="{ fontSize: baseInfo.fontSize + 'px',height:'700px',backgroundColor:'rgba(0, 94, 170, 0.8)' }"
class="w-full h-60 p-2 border border-[var(--border-primary)] rounded-md bg-[var(--bg-secondary)] text-[var(--text-primary)] resize-none overflow-y-auto"
v-model="analysisResult" readonly></textarea> -->
<textarea
v-if="!isMarkdown"
ref="sumupResultTextarea"
:style="{ fontSize: baseInfo.fontSize + 'px', height:'700px', backgroundColor:'rgba(0, 94, 170, 0.8)' }"
class="w-full p-2 border border-[var(--border-primary)] rounded-md bg-[var(--bg-secondary)] text-[var(--text-primary)] resize-none overflow-y-auto"
v-model="analysisResult"
readonly
></textarea>
<div
v-else
ref="sumupResultTextarea"
:style="{ fontSize: baseInfo.fontSize + 'px', height:'700px', backgroundColor:'rgba(0, 94, 170, 0.8)' }"
class="w-full p-2 border border-[var(--border-primary)] rounded-md bg-[var(--bg-secondary)] text-[var(--text-primary)] overflow-y-auto markdown-body"
v-html="renderedMarkdown"
></div>
</div>
<!-- 弹框底部 -->
<div class="p-4 border-t border-[var(--border-primary)] flex justify-center">
<button @click="openPromptModal('zj')"
class="px-8 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors">
<i class="fas fa-sync mr-2"></i>重新总结
</button>
<button @click="openzjmodel" :disabled="zjloding"
class="px-8 py-3 rounded-lg text-white transition-colors ml-2 flex items-center justify-center gap-2"
:class="zjloding ? 'bg-blue-400' : 'bg-blue-600 hover:bg-blue-700'">
<i class="fas"></i>
<!-- 默认内容 -->
<span v-if="!zjloding">投屏</span>
<!-- loading 内容 -->
<span v-else class="flex items-center gap-2">
<svg class="animate-spin h-5 w-5 text-white"
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor"
stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor"
d="M4 12a8 8 0 018-8v4l3.5-3.5L12 0v4a8 8 0 100 16v4l3.5-3.5L12 20v4a8 8 0 01-8-8z">
</path>
</svg>
加载中...
</span>
</button>
</div>
</div>
</div>
<!-- 演讲内容弹框组件 -->
<div v-if="isyjDialogVisible" class="fixed inset-0 z-50 flex items-center justify-center ">
<!-- 遮罩层 -->
<div @click="isyjDialogVisible = false"
class="absolute inset-0 bg-black/50 backdrop-blur-sm"></div>
<!-- 弹框内容修改这里的class -->
<div
class="relative w-full bg-[var(--bg-secondary)] border border-[var(--border-primary)] rounded-lg shadow-xl overflow-hidden">
<!-- 弹框头部 -->
<div
class="p-4 border-b border-[var(--border-primary)] flex items-center justify-between gap-4">
<div class="flex items-center gap-3">
<h2 class="text-lg font-semibold">语音实时转录</h2>
<button v-if="!isEditingTranscription" @click="enterEditMode()"
class="px-3 py-1 text-sm text-blue-400 hover:text-blue-300 border border-blue-400/30 rounded hover:border-blue-300/50 transition-colors">
<i class="fas fa-pen mr-1"></i>编辑
</button>
<button v-else @click="exitEditMode()"
class="px-3 py-1 text-sm text-green-400 hover:text-green-300 border border-green-400/30 rounded hover:border-green-300/50 transition-colors">
<i class="fas fa-check mr-1"></i>完成编辑
</button>
</div>
<button @click="isyjDialogVisible = false"
class="text-gray-500 hover:text-gray-700">
<i class="fas fa-times"></i>
</button>
</div>
<!-- 弹框主体 -->
<div class="p-4">
<textarea v-if="isEditingTranscription"
:style="{ fontSize: baseInfo.fontSize + 'px',height:'90vh',backgroundColor:'rgba(0, 94, 170, 0.8)' }"
class="w-full h-60 p-2 border-2 border-blue-500 rounded-md bg-[var(--bg-secondary)] text-[var(--text-primary)] resize-none overflow-y-auto"
v-model="editableText"></textarea>
<textarea v-else ref="asrResultTextarea"
:style="{ fontSize: baseInfo.fontSize + 'px',height:'90vh',backgroundColor:'rgba(0, 94, 170, 0.8)' }"
class="w-full h-60 p-2 border border-[var(--border-primary)] rounded-md bg-[var(--bg-secondary)] text-[var(--text-primary)] resize-none overflow-y-auto"
:value="fullRealtimeTranscription" readonly></textarea>
</div>
</div>
</div>
</div>
<!-- ==================== 功能块:实时录音视图 End ==================== -->
<!-- ==================== 功能块:录音详情视图 Start ==================== -->
<!-- Recording Detail View -->
<div v-else-if="currentView === 'detail' && selectedRecording"
class="flex-1 flex flex-col overflow-hidden">
<!-- Mobile View -->
<div v-if="isMobileScreen" class="flex-1 flex flex-col overflow-hidden">
<!-- Mobile Header -->
<div
class="bg-[var(--bg-secondary)] border-b border-[var(--border-primary)] p-4 flex-shrink-0">
<div @click="isMetadataExpanded = !isMetadataExpanded" class="cursor-pointer">
<div class="flex items-start justify-between">
<div class="flex-1 min-w-0">
<h1 class="text-lg font-bold text-[var(--text-primary)] truncate">
${selectedRecording.title || 'Untitled Recording'}
</h1>
<p class="text-sm text-[var(--text-muted)] truncate">
${selectedRecording.participants || t('help.noParticipants')}
</p>
<div v-if="getRecordingTags(selectedRecording).length > 0"
class="flex flex-wrap gap-1 mt-2">
<button v-for="tag in getRecordingTags(selectedRecording)" :key="tag.id"
@click="filterByTag(tag)"
class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium text-white hover:opacity-80 transition-opacity cursor-pointer"
:style="{ backgroundColor: tag.color }"
:title="'Filter by ' + tag.name">
<i class="fas fa-tag mr-1"></i>
<span v-text="tag.name"></span>
</button>
</div>
</div>
<i
:class="['fas', 'fa-chevron-down', 'transition-transform', { 'rotate-180': isMetadataExpanded }]"></i>
</div>
</div>
<!-- Expandable Metadata and Actions -->
<div v-if="isMetadataExpanded" class="mt-4 space-y-4">
<div class="space-y-2 text-sm text-[var(--text-muted)]">
<div class="flex items-center gap-2">
<i class="fas fa-calendar text-[var(--text-accent)]"></i>
<span>${selectedRecording.meeting_date ? formatDisplayDate(selectedRecording.meeting_date) : 'No date set'}</span>
</div>
<div v-if="activeRecordingMetadata"
class="flex flex-wrap items-center gap-x-4 gap-y-2">
<template v-for="(item, index) in activeRecordingMetadata" :key="index">
<!-- Special rendering for tags -->
<div v-if="item.isTagItem" class="flex items-center gap-1.5">
<i :class="item.icon" class="text-[var(--text-muted)]"></i>
<div class="flex flex-wrap items-center gap-1">
<span v-for="tag in item.tags" :key="tag.id"
class="inline-flex items-center px-2 py-0.5 text-xs font-medium rounded-full text-white"
:style="{ backgroundColor: tag.color || '#6B7280' }">
${tag.name}
</span>
</div>
</div>
<!-- Regular metadata items -->
<span v-else class="flex items-center gap-1.5">
<i :class="item.icon"></i>
<span :title="item.fullText || item.text">${item.text}</span>
</span>
</template>
</div>
<div v-if="selectedRecording.status !== 'COMPLETED'"
class="flex items-center gap-2">
<span :class="getStatusClass(selectedRecording.status)"
class="status-badge">
${formatStatus(selectedRecording.status)}
</span>
</div>
</div>
<!-- Action Buttons -->
<div class="flex items-center gap-2 overflow-x-auto pb-2">
<button @click="toggleInbox(selectedRecording)"
class="p-2 rounded-lg hover:bg-[var(--bg-tertiary)] transition-colors"
:class="selectedRecording.is_inbox ? 'text-blue-500' : ''"
:title="selectedRecording.is_inbox ? t('sidebar.markAsRead') : t('sidebar.moveToInbox')"><i
class="fas fa-inbox"></i></button>
<button @click="toggleHighlight(selectedRecording)"
class="p-2 rounded-lg hover:bg-[var(--bg-tertiary)] transition-colors"
:class="selectedRecording.is_highlighted ? 'text-yellow-500' : ''"
:title="selectedRecording.is_highlighted ? t('sidebar.removeFromHighlighted') : t('sidebar.highlighted')"><i
class="fas fa-star"></i></button>
<button @click="editRecording(selectedRecording)"
class="p-2 rounded-lg hover:bg-[var(--bg-tertiary)] transition-colors"
:title="t('common.edit')"><i class="fas fa-edit"></i></button>
<button @click="editRecordingTags(selectedRecording)"
class="p-2 rounded-lg hover:bg-[var(--bg-tertiary)] transition-colors"
:title="t('tags.title')"><i class="fas fa-tags"></i></button>
<button @click="reprocessTranscription(selectedRecording.id)"
v-if="selectedRecording && (selectedRecording.status === 'COMPLETED' || selectedRecording.status === 'FAILED')"
class="p-2 rounded-lg hover:bg-[var(--bg-tertiary)] transition-colors"
:title="useAsrEndpoint ? 'Reprocess with ASR' : 'Reprocess transcription'"><i
class="fas fa-redo-alt"></i></button>
<button @click="reprocessSummary(selectedRecording.id)"
v-if="selectedRecording && (selectedRecording.status === 'COMPLETED' || selectedRecording.status === 'FAILED')"
class="p-2 rounded-lg hover:bg-[var(--bg-tertiary)] transition-colors"
:title="t('buttons.reprocessSummary')"><i
class="fas fa-sync-alt"></i></button>
<button @click="confirmReset(selectedRecording)"
v-if="['PROCESSING', 'SUMMARIZING', 'FAILED'].includes(selectedRecording.status)"
class="p-2 rounded-lg hover:bg-[var(--bg-tertiary)] transition-colors text-orange-500"
:title="t('buttons.resetStuckProcessing')"><i
class="fas fa-undo"></i></button>
<button @click="openSpeakerModal"
v-if="selectedRecording.transcription && useAsrEndpoint"
class="p-2 rounded-lg hover:bg-[var(--bg-tertiary)] transition-colors"
:title="t('buttons.identifySpeakers')"><i
class="fas fa-user-tag"></i></button>
<button @click="openShareModal(selectedRecording)"
class="p-2 rounded-lg hover:bg-[var(--bg-tertiary)] transition-colors"
:title="t('buttons.shareRecording')"><i
class="fas fa-share-alt"></i></button>
<button @click="confirmDelete(selectedRecording)"
class="p-2 rounded-lg hover:bg-[var(--bg-danger-light)] text-[var(--text-danger)] transition-colors"><i
class="fas fa-trash"></i></button>
</div>
</div>
</div>
<!-- Mobile Audio Player -->
<div
class="bg-[var(--bg-secondary)] p-4 border-b border-[var(--border-primary)] flex-shrink-0">
<div class="audio-player-container">
<audio controls :src="'/tool/speakr/audio/' + selectedRecording.id"
:volume="playerVolume" @volumechange="onPlayerVolumeChange" class="w-full">
Your browser does not support the audio element.
</audio>
</div>
</div>
<!-- Mobile Tab Navigation -->
<div
class="flex-shrink-0 bg-[var(--bg-tertiary)] border-b border-[var(--border-primary)] flex overflow-x-auto">
<button @click="mobileTab = 'transcript'"
:class="['flex-1 px-4 py-3 text-sm font-medium transition-colors', mobileTab === 'transcript' ? 'bg-[var(--bg-secondary)] text-[var(--text-accent)] border-b-2 border-[var(--border-focus)]' : 'text-[var(--text-muted)] hover:text-[var(--text-secondary)]']"
v-text="t('transcription.title')"></button>
<button @click="mobileTab = 'summary'"
:class="['flex-1 px-4 py-3 text-sm font-medium transition-colors', mobileTab === 'summary' ? 'bg-[var(--bg-secondary)] text-[var(--text-accent)] border-b-2 border-[var(--border-focus)]' : 'text-[var(--text-muted)] hover:text-[var(--text-secondary)]']"
v-text="t('summary.title')"></button>
<button @click="mobileTab = 'notes'"
:class="['flex-1 px-4 py-3 text-sm font-medium transition-colors', mobileTab === 'notes' ? 'bg-[var(--bg-secondary)] text-[var(--text-accent)] border-b-2 border-[var(--border-focus)]' : 'text-[var(--text-muted)] hover:text-[var(--text-secondary)]']"
v-text="t('notes.title')"></button>
<button @click="mobileTab = 'chat'"
:class="['flex-1 px-4 py-3 text-sm font-medium transition-colors', mobileTab === 'chat' ? 'bg-[var(--bg-secondary)] text-[var(--text-accent)] border-b-2 border-[var(--border-focus)]' : 'text-[var(--text-muted)] hover:text-[var(--text-secondary)]']"
v-text="t('chat.title')"></button>
<button v-if="selectedRecording.events && selectedRecording.events.length > 0"
@click="mobileTab = 'events'"
:class="['flex-1 px-4 py-3 text-sm font-medium transition-colors whitespace-nowrap', mobileTab === 'events' ? 'bg-[var(--bg-secondary)] text-[var(--text-accent)] border-b-2 border-[var(--border-focus)]' : 'text-[var(--text-muted)] hover:text-[var(--text-secondary)]']">
<i class="fas fa-calendar-alt mr-1"></i>${ selectedRecording.events.length }
</button>
</div>
<!-- Mobile Tab Content -->
<div class="flex-1 overflow-y-auto p-4">
<!-- Transcript Panel -->
<div v-if="mobileTab === 'transcript'" class="h-full flex flex-col space-y-4">
<div class="flex items-center justify-end gap-2 flex-shrink-0 pr-3">
<div v-if="processedTranscription.hasDialogue" class="view-mode-toggle">
<button @click="toggleTranscriptionViewMode"
:class="['toggle-button', transcriptionViewMode === 'simple' ? 'active' : '']"><i
class="fas fa-list"></i></button>
<button @click="toggleTranscriptionViewMode"
:class="['toggle-button', transcriptionViewMode === 'bubble' ? 'active' : '']"><i
class="fas fa-comments"></i></button>
</div>
<button @click="copyTranscription" class="copy-btn" title="Copy transcript"><i
class="fas fa-copy"></i></button>
<button @click="downloadTranscript"
v-if="selectedRecording && selectedRecording.transcription" class="copy-btn"
title="Download transcript with template"><i
class="fas fa-download"></i></button>
<button @click="openTranscriptionEditor"
v-if="selectedRecording && selectedRecording.transcription" class="copy-btn"
title="Edit transcript"><i class="fas fa-edit"></i></button>
</div>
<div class="flex-grow overflow-y-auto mobile-content-box">
<div v-if="selectedRecording.status === 'PROCESSING'" class="text-center py-8">
<i class="fas fa-spinner fa-spin text-2xl text-[var(--text-muted)]"></i>
<p class="mt-2 text-[var(--text-muted)]"
v-text="t('help.processingTranscription')"></p>
</div>
<div v-else-if="!selectedRecording.transcription" class="text-center py-8">
<i class="fas fa-file-text text-3xl text-[var(--text-muted)] mb-3"></i>
<p class="text-[var(--text-muted)]"
v-text="t('transcription.noTranscription')"></p>
</div>
<div v-else>
<div v-if="!processedTranscription.hasDialogue || transcriptionViewMode === 'simple'"
class="transcription-simple-view">
<div v-if="processedTranscription.hasDialogue">
<div v-for="segment in processedTranscription.simpleSegments"
:key="segment.startTime || Math.random()"
class="speaker-segment" @click="seekAudioFromEvent"
:data-start-time="segment.startTime">
<div v-if="segment.showSpeaker"
:class="['speaker-tablet', segment.color]">
${segment.speaker}</div>
<div class="speaker-text">${segment.sentence}</div>
</div>
</div>
<div v-else>
<div
v-if="processedTranscription.simpleSegments && processedTranscription.simpleSegments.length > 0">
<div v-for="segment in processedTranscription.simpleSegments"
:key="segment.startTime || Math.random()"
class="transcript-segment cursor-pointer hover:bg-[var(--bg-accent-hover)] p-2 rounded transition-colors"
@click="seekAudioFromEvent"
:data-start-time="segment.startTime">${segment.sentence}
</div>
</div>
<div v-else class="whitespace-pre-wrap">
${processedTranscription.content}</div>
</div>
</div>
<div v-else-if="transcriptionViewMode === 'bubble'"
class="transcription-with-speakers">
<div v-for="(row, rowIndex) in processedTranscription.bubbleRows"
:key="`${selectedRecording.id}-bubble-row-${rowIndex}`"
:class="['bubble-row', row.isMe ? 'speaker-me' : '']">
<div v-for="bubble in row.bubbles"
:key="bubble.startTime || Math.random()"
:class="['speaker-bubble', bubble.color, row.isMe ? 'speaker-me' : '']"
@click="seekAudioFromEvent" :data-start-time="bubble.startTime">
<div class="speaker-bubble-content">${bubble.sentence}</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Summary Panel -->
<div v-if="mobileTab === 'summary'" class="h-full flex flex-col space-y-4">
<div class="flex items-center justify-end gap-2 flex-shrink-0 pr-3">
<button @click="copySummary" class="copy-btn"
:title="t('buttons.copySummary')"><i class="fas fa-copy"></i></button>
<button @click="downloadSummary" class="copy-btn"
:title="t('buttons.downloadAsWord')"><i
class="fas fa-download"></i></button>
<button @click="toggleEditSummary" class="copy-btn"
:title="t('buttons.editSummary')"><i class="fas fa-edit"></i></button>
</div>
<div class="flex-grow overflow-y-auto mobile-content-box">
<div v-if="editingSummary" class="h-full flex flex-col">
<div class="markdown-editor-container flex-1">
<textarea ref="summaryMarkdownEditor"
v-model="selectedRecording.summary"></textarea>
</div>
<div class="flex justify-end gap-2 mt-2">
<button @click="cancelEditSummary"
class="px-3 py-1 text-sm bg-[var(--bg-secondary)] text-[var(--text-secondary)] border border-[var(--border-secondary)] rounded hover:bg-[var(--bg-tertiary)]"
v-text="t('common.cancel')"></button>
<button @click="saveEditSummary"
class="px-3 py-1 text-sm bg-[var(--bg-button)] text-[var(--text-button)] rounded hover:bg-[var(--bg-button-hover)]"
v-text="t('common.save')"></button>
</div>
</div>
<div v-else>
<div v-if="selectedRecording.status === 'SUMMARIZING'"
class="text-center py-8">
<i class="fas fa-spinner fa-spin text-2xl text-[var(--text-muted)]"></i>
<p class="mt-2 text-[var(--text-muted)]"
v-text="t('help.generatingSummary')"></p>
</div>
<div v-else-if="!selectedRecording.summary" class="text-center py-8">
<i class="fas fa-file-alt text-3xl text-[var(--text-muted)] mb-3"></i>
<p class="text-[var(--text-muted)] mb-4"
v-text="t('summary.noSummary')"></p>
<button @click="generateSummary"
class="px-6 py-3 bg-gradient-to-r from-blue-600 to-purple-600 text-white font-medium rounded-lg shadow-lg hover:shadow-xl hover:from-blue-700 hover:to-purple-700 transition-all duration-200 transform hover:scale-105">
<i class="fas fa-magic mr-2"></i>Generate Summary
</button>
</div>
<div v-else class="summary-box h-full"
v-html="selectedRecording.summary_html || selectedRecording.summary">
</div>
</div>
</div>
</div>
<!-- Notes Panel -->
<div v-if="mobileTab === 'notes'" class="h-full flex flex-col space-y-4">
<div class="flex items-center justify-end gap-2 flex-shrink-0 pr-3">
<button @click="copyNotes" class="copy-btn" :title="t('buttons.copyNotes')"><i
class="fas fa-copy"></i></button>
<button @click="downloadNotes" class="copy-btn"
:title="t('buttons.downloadAsWord')"><i
class="fas fa-download"></i></button>
<button @click="toggleEditNotes" class="copy-btn"
:title="t('buttons.editNotes')"><i class="fas fa-edit"></i></button>
</div>
<div class="flex-grow overflow-y-auto mobile-content-box">
<div v-if="editingNotes" class="h-full flex flex-col">
<div class="markdown-editor-container flex-1">
<textarea ref="notesMarkdownEditor"
v-model="selectedRecording.notes"></textarea>
</div>
<div class="flex justify-end gap-2 mt-2">
<button @click="cancelEditNotes"
class="px-3 py-1 text-sm bg-[var(--bg-secondary)] text-[var(--text-secondary)] border border-[var(--border-secondary)] rounded hover:bg-[var(--bg-tertiary)]"
v-text="t('common.cancel')"></button>
<button @click="saveEditNotes"
class="px-3 py-1 text-sm bg-[var(--bg-button)] text-[var(--text-button)] rounded hover:bg-[var(--bg-button-hover)]"
v-text="t('common.save')"></button>
</div>
</div>
<div v-else @click="clickToEditNotes">
<div v-if="!selectedRecording.notes"
class="text-center py-8 cursor-pointer hover:text-[var(--text-secondary)]">
<i
class="fas fa-sticky-note text-3xl text-[var(--text-muted)] mb-3"></i>
<p class="text-[var(--text-muted)]" v-text="t('help.clickToAddNotes')">
</p>
</div>
<div v-else class="notes-box h-full"
v-html="selectedRecording.notes_html || selectedRecording.notes"></div>
</div>
</div>
</div>
<!-- Chat Panel -->
<div v-if="mobileTab === 'chat'"
class="h-full flex flex-col rounded-lg border border-[var(--border-primary)] overflow-hidden relative">
<!-- Clear button - fixed at top right -->
<button v-if="chatMessages.length > 0" @click="clearChat"
class="absolute top-2 right-2 p-1 text-[var(--text-muted)] opacity-60 hover:opacity-100 hover:text-[var(--text-danger)] rounded transition-all duration-200 z-20"
title="Clear chat">
<i class="fas fa-times text-xs"></i>
</button>
<div ref="chatMessagesRef"
class="flex-1 overflow-y-auto p-4 space-y-4 bg-[var(--bg-secondary)]">
<div v-if="chatMessages.length === 0" class="text-center py-8">
<i class="fas fa-robot text-3xl text-[var(--text-muted)] mb-3"></i>
<p class="text-[var(--text-muted)]"
v-text="t('help.askAboutTranscription')"></p>
</div>
<div v-for="(message, index) in chatMessages" :key="index" :class="['message relative group', message.role === 'user' ? 'user-message ml-auto' : 'ai-message',
message.role === 'assistant' ? 'pr-10' : '']">
<!-- Copy button for assistant messages -->
<button v-if="message.role === 'assistant'"
@click="copyMessage(message.content, $event)"
class="absolute top-2 right-2 p-1 text-[var(--text-muted)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-tertiary)] rounded transition-all duration-200"
:title="t('buttons.copyMessage')">
<i class="fas fa-copy text-sm"></i>
</button>
<!-- Show thinking content if available -->
<div v-if="message.thinking && message.role === 'assistant'" class="mb-2">
<button @click="message.thinkingExpanded = !message.thinkingExpanded"
class="text-xs text-[var(--text-muted)] hover:text-[var(--text-secondary)] flex items-center gap-1">
<i
:class="['fas', message.thinkingExpanded ? 'fa-chevron-down' : 'fa-chevron-right']"></i>
<span v-text="t('help.modelReasoning')"></span>
<span
class="text-[var(--text-muted)]">(${message.thinking.split('\n').length}
lines)</span>
</button>
<div v-if="message.thinkingExpanded"
class="mt-2 p-3 bg-[var(--bg-tertiary)] border border-[var(--border-secondary)] rounded-lg text-xs text-[var(--text-muted)] max-h-64 overflow-y-auto">
<pre class="whitespace-pre-wrap font-mono">${message.thinking}</pre>
</div>
</div>
<!-- Main message content -->
<div v-if="message.html" v-html="message.html"></div>
<div v-else class="whitespace-pre-wrap">${message.content}</div>
</div>
<div v-if="isChatLoading" class="ai-message">
<i class="fas fa-spinner fa-spin mr-2"></i> Thinking...
</div>
</div>
<div class="border-t border-[var(--border-primary)] p-4 bg-[var(--bg-tertiary)]">
<div class="flex gap-2">
<textarea v-model="chatInput" @keydown="handleChatKeydown"
:disabled="isChatLoading || selectedRecording.status !== 'COMPLETED'"
:placeholder="t('chat.placeholder')"
class="flex-1 px-3 py-2 bg-[var(--bg-input)] border border-[var(--border-secondary)] rounded-lg resize-none focus:outline-none focus:ring-2 focus:ring-[var(--ring-focus)] text-sm"
rows="2"></textarea>
<button @click="sendChatMessage"
:disabled="!chatInput.trim() || isChatLoading || selectedRecording.status !== 'COMPLETED'"
class="px-4 py-2 bg-[var(--bg-button)] text-[var(--text-button)] rounded-lg hover:bg-[var(--bg-button-hover)] transition-colors disabled:opacity-50 disabled:cursor-not-allowed">
<i class="fas fa-paper-plane"></i>
</button>
</div>
</div>
</div>
<!-- Events Panel (Mobile) -->
<div v-if="mobileTab === 'events' && selectedRecording.events && selectedRecording.events.length > 0"
class="h-full flex flex-col space-y-4">
<div class="flex items-center justify-end gap-2 flex-shrink-0 pr-3">
<button @click="downloadICS"
v-if="selectedRecording.events && selectedRecording.events.length > 0"
class="copy-btn" :title="t('buttons.exportCalendar')">
<i class="fas fa-calendar-plus"></i>
</button>
</div>
<div class="flex-grow overflow-y-auto mobile-content-box">
<div class="space-y-3">
<div v-for="event in selectedRecording.events" :key="event.id"
class="bg-[var(--bg-tertiary)] rounded-lg p-3 border border-[var(--border-primary)]">
<div class="flex justify-between items-start mb-2">
<div class="flex-1">
<h3
class="text-base font-semibold text-[var(--text-primary)] mb-1">
<i
class="fas fa-calendar-check mr-2 text-[var(--text-accent)] text-sm"></i>
${ event.title }
</h3>
<p v-if="event.description"
class="text-xs text-[var(--text-secondary)] mb-2">
${ event.description }
</p>
</div>
<button @click="downloadEventICS(event)"
class="px-2 py-1 bg-[var(--bg-button)] text-[var(--text-button)] rounded-lg hover:bg-[var(--bg-button-hover)] transition-colors text-xs flex items-center gap-1"
:title="t('events.addToCalendar')">
<i class="fas fa-download text-xs"></i>
<span>Add</span>
</button>
</div>
<div class="grid grid-cols-1 gap-2 text-xs">
<div class="flex items-center text-[var(--text-muted)]">
<i class="fas fa-clock mr-2 text-[var(--text-accent)]"></i>
<span>
${ formatEventDateTime(event.start_datetime) }
<template v-if="event.end_datetime">
- ${ formatEventDateTime(event.end_datetime, true) }
</template>
</span>
</div>
<div v-if="event.location"
class="flex items-center text-[var(--text-muted)]">
<i
class="fas fa-map-marker-alt mr-2 text-[var(--text-accent)]"></i>
<span>${ event.location }</span>
</div>
<div v-if="event.attendees && event.attendees.length > 0"
class="flex items-start text-[var(--text-muted)]">
<i
class="fas fa-users mr-2 text-[var(--text-accent)] mt-0.5"></i>
<div class="flex-1">
<span v-for="(attendee, index) in event.attendees"
:key="index"
class="inline-block bg-[var(--bg-secondary)] px-2 py-0.5 rounded mr-1 mb-1">
${ attendee }
</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Desktop View -->
<div v-else class="flex-1 flex flex-col overflow-hidden">
<!-- Recording Header -->
<div
class="bg-[var(--bg-secondary)] border-b border-[var(--border-primary)] p-6 flex-shrink-0">
<div class="flex flex-col">
<div class="flex items-start justify-between">
<div class="flex-1 min-w-0">
<h1 class="text-2xl font-bold text-[var(--text-primary)] mb-2">
${selectedRecording.title || 'Untitled Recording'}
</h1>
</div>
<!-- Action Buttons -->
<div class="flex items-center gap-2 ml-4">
<button @click="toggleInbox(selectedRecording)"
class="p-2 rounded-lg hover:bg-[var(--bg-tertiary)] transition-colors"
:class="selectedRecording.is_inbox ? 'text-blue-500' : ''"
:title="selectedRecording.is_inbox ? '标记为已读' : '移到收件箱'">
<i class="fas fa-inbox"></i>
</button>
<button @click="toggleHighlight(selectedRecording)"
class="p-2 rounded-lg hover:bg-[var(--bg-tertiary)] transition-colors"
:class="selectedRecording.is_highlighted ? 'text-yellow-500' : ''"
:title="selectedRecording.is_highlighted ? '取消高亮' : '设为高亮'">
<i class="fas fa-star"></i>
</button>
<button @click="editRecording(selectedRecording)"
class="p-2 rounded-lg hover:bg-[var(--bg-tertiary)] transition-colors"
title="编辑录音">
<i class="fas fa-edit"></i>
</button>
<button @click="editRecordingTags(selectedRecording)"
class="p-2 rounded-lg hover:bg-[var(--bg-tertiary)] transition-colors"
:title="t('buttons.editTags')">
<i class="fas fa-tags"></i>
</button>
<button @click="reprocessTranscription(selectedRecording.id)"
v-if="selectedRecording && (selectedRecording.status === 'COMPLETED' || selectedRecording.status === 'FAILED')"
class="p-2 rounded-lg hover:bg-[var(--bg-tertiary)] transition-colors"
:title="useAsrEndpoint ? '重新处理录音' : '重新处理转录'">
<i class="fas fa-redo-alt"></i>
</button>
<button @click="reprocessSummary(selectedRecording.id)"
v-if="selectedRecording && (selectedRecording.status === 'COMPLETED' || selectedRecording.status === 'FAILED')"
class="p-2 rounded-lg hover:bg-[var(--bg-tertiary)] transition-colors"
:title="t('buttons.reprocessSummary')">
<i class="fas fa-sync-alt"></i>
</button>
<button @click="confirmReset(selectedRecording)"
v-if="['PROCESSING', 'SUMMARIZING', 'FAILED'].includes(selectedRecording.status)"
class="p-2 rounded-lg hover:bg-[var(--bg-tertiary)] transition-colors text-orange-500"
:title="t('buttons.resetStuckProcessing')">
<i class="fas fa-undo"></i>
</button>
<button @click="openSpeakerModal"
v-if="selectedRecording.transcription && useAsrEndpoint"
class="p-2 rounded-lg hover:bg-[var(--bg-tertiary)] transition-colors"
:title="t('buttons.identifySpeakers')">
<i class="fas fa-user-tag"></i>
</button>
<button @click="openShareModal(selectedRecording)"
class="p-2 rounded-lg hover:bg-[var(--bg-tertiary)] transition-colors"
:title="t('buttons.shareRecording')">
<i class="fas fa-share-alt"></i>
</button>
<button @click="confirmDelete(selectedRecording)"
class="p-2 rounded-lg hover:bg-[var(--bg-danger-light)] text-[var(--text-danger)] transition-colors"
title="删除录音">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
<!-- Metadata Row -->
<div
class="flex flex-wrap items-center gap-x-6 gap-y-2 text-sm text-[var(--text-muted)] mt-2">
<!-- Participants -->
<div class="flex items-center gap-2">
<i class="fas fa-users text-[var(--text-accent)]"></i>
<span v-if="!editingParticipants" @click="toggleEditParticipants"
class="cursor-pointer hover:text-[var(--text-accent)] transition-colors">
${selectedRecording.participants || t('help.noParticipants')}
</span>
<input v-else v-model="selectedRecording.participants"
@blur="toggleEditParticipants" @keyup.enter="toggleEditParticipants"
class="bg-transparent border-b border-[var(--border-secondary)] focus:border-[var(--border-focus)] outline-none px-1">
</div>
<!-- Meeting Date -->
<div class="flex items-center gap-2">
<i class="fas fa-calendar text-[var(--text-accent)]"></i>
<span v-if="!editingMeetingDate" @click="toggleEditMeetingDate"
class="cursor-pointer hover:text-[var(--text-accent)] transition-colors">
${selectedRecording.meeting_date ?
formatDisplayDate(selectedRecording.meeting_date) : 'No date set'}
</span>
<input v-else v-model="selectedRecording.meeting_date"
@blur="toggleEditMeetingDate" @keyup.enter="toggleEditMeetingDate"
type="date"
class="bg-transparent border-b border-[var(--border-secondary)] focus:border-[var(--border-focus)] outline-none px-1">
</div>
<!-- Other Metadata -->
<div v-if="activeRecordingMetadata"
class="flex flex-wrap items-center gap-x-6 gap-y-2">
<template v-for="(item, index) in activeRecordingMetadata" :key="index">
<!-- Special rendering for tags -->
<div v-if="item.isTagItem" class="flex items-center gap-1.5">
<i :class="item.icon" class="text-[var(--text-muted)]"></i>
<div class="flex flex-wrap items-center gap-1">
<span v-for="tag in item.tags" :key="tag.id"
class="inline-flex items-center px-2 py-0.5 text-xs font-medium rounded-full text-white"
:style="{ backgroundColor: tag.color || '#6B7280' }">
${tag.name}
</span>
</div>
</div>
<!-- Regular metadata items -->
<span v-else class="flex items-center gap-1.5">
<i :class="item.icon"></i>
<span :title="item.fullText || item.text">${item.text}</span>
</span>
</template>
</div>
<!-- Status -->
<div v-if="selectedRecording.status !== 'COMPLETED'"
class="flex items-center gap-2">
<span :class="getStatusClass(selectedRecording.status)"
class="status-badge">
${formatStatus(selectedRecording.status)}
</span>
</div>
</div>
</div>
</div>
<!-- Main Content Split View -->
<div id="mainContentColumns" class="flex-1 flex overflow-hidden">
<!-- Left Panel: Transcription -->
<div id="leftMainColumn" class="flex flex-col overflow-hidden"
:style="{width: leftColumnWidth + '%'}">
<!-- Transcription Header -->
<div
class="bg-[var(--bg-tertiary)] px-4 py-3 border-b border-[var(--border-primary)] flex items-center justify-between">
<h3 class="font-semibold flex items-center">
<i class="fas fa-file-text mr-2"></i>
<span v-text="t('transcription.title')"></span>
</h3>
<div class="flex items-center gap-2">
<!-- View Mode Toggle -->
<div v-if="processedTranscription.hasDialogue" class="view-mode-toggle">
<button @click="toggleTranscriptionViewMode"
:class="['toggle-button', transcriptionViewMode === 'simple' ? 'active' : '']">
<i class="fas fa-list"></i><span
v-text="t('transcription.simple')"></span>
</button>
<button @click="toggleTranscriptionViewMode"
:class="['toggle-button', transcriptionViewMode === 'bubble' ? 'active' : '']">
<i class="fas fa-comments"></i><span
v-text="t('transcription.bubble')"></span>
</button>
</div>
<!-- Copy Button -->
<button @click="copyTranscription" class="copy-btn">
<i class="fas fa-copy mr-1"></i><span
v-text="t('transcription.copy')"></span>
</button>
<!-- Download Button -->
<button @click="downloadTranscript"
v-if="selectedRecording && selectedRecording.transcription"
class="copy-btn" title="下载带有模板的字幕">
<i class="fas fa-download mr-1"></i><span
v-text="t('transcription.download')"></span>
</button>
<!-- Edit Transcription Button -->
<button @click="openTranscriptionEditor"
v-if="selectedRecording && selectedRecording.transcription"
class="copy-btn">
<i class="fas fa-edit mr-1"></i><span
v-text="t('transcription.edit')"></span>
</button>
</div>
</div>
<!-- Transcription Content -->
<div class="flex-1 overflow-y-auto p-4">
<div v-if="selectedRecording.status === 'PROCESSING'" class="text-center py-8">
<i class="fas fa-spinner fa-spin text-2xl text-[var(--text-muted)]"></i>
<p class="mt-2 text-[var(--text-muted)]"
v-text="t('help.processingTranscription')"></p>
</div>
<div v-else-if="!selectedRecording.transcription" class="text-center py-8">
<i class="fas fa-file-text text-3xl text-[var(--text-muted)] mb-3"></i>
<p class="text-[var(--text-muted)]"
v-text="t('transcription.noTranscription')"></p>
</div>
<!-- Speaker Legend (for dialogue transcriptions in bubble view only) -->
<div v-if="processedTranscription.hasDialogue && processedTranscription.speakers.length > 0 && transcriptionViewMode === 'bubble'"
:class="['speaker-legend', legendExpanded ? 'expanded' : '']">
<div class="speaker-legend-header"
@click="legendExpanded = !legendExpanded">
<div class="speaker-legend-title">
<i class="fas fa-users"></i>
<span v-text="t('help.speakers')"></span>
<span
class="speaker-count-indicator">(${processedTranscription.speakers.length})</span>
</div>
<i class="fas fa-chevron-down speaker-legend-toggle"></i>
</div>
<div class="speaker-legend-content">
<div v-for="(speaker, index) in processedTranscription.speakers"
:key="`${selectedRecording.id}-speaker-legend-${index}`"
:class="['speaker-legend-item', speaker.color]">
<span class="speaker-name">${speaker.name}</span>
</div>
</div>
</div>
<!-- Transcription Display -->
<div v-if="selectedRecording.transcription">
<!-- Simple View -->
<div v-if="!processedTranscription.hasDialogue || transcriptionViewMode === 'simple'"
class="transcription-simple-view">
<div v-if="processedTranscription.hasDialogue">
<div v-for="segment in processedTranscription.simpleSegments"
:key="segment.startTime || Math.random()"
class="speaker-segment" @click="seekAudioFromEvent"
:data-start-time="segment.startTime">
<div v-if="segment.showSpeaker"
:class="['speaker-tablet', segment.color]">
${segment.speaker}
</div>
<div class="speaker-text">
${segment.sentence}
</div>
</div>
</div>
<div v-else>
<div
v-if="processedTranscription.simpleSegments && processedTranscription.simpleSegments.length > 0">
<div v-for="segment in processedTranscription.simpleSegments"
:key="segment.startTime || Math.random()"
class="transcript-segment cursor-pointer hover:bg-[var(--bg-accent-hover)] p-2 rounded transition-colors"
@click="seekAudioFromEvent"
:data-start-time="segment.startTime">
${segment.sentence}
</div>
</div>
<div v-else class="whitespace-pre-wrap">
${processedTranscription.content}
</div>
</div>
</div>
<!-- Bubble View -->
<div v-else-if="transcriptionViewMode === 'bubble'"
class="transcription-with-speakers">
<div v-for="(row, rowIndex) in processedTranscription.bubbleRows"
:key="`${selectedRecording.id}-bubble-row-${rowIndex}`"
:class="['bubble-row', row.isMe ? 'speaker-me' : '']">
<div v-for="bubble in row.bubbles"
:key="bubble.startTime || Math.random()"
:class="['speaker-bubble', bubble.color, row.isMe ? 'speaker-me' : '']"
@click="seekAudioFromEvent" :data-start-time="bubble.startTime">
<div class="speaker-bubble-content">
${bubble.sentence}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Resizable Divider - Minimal but functional -->
<div id="mainColumnResizer" @mousedown="startColumnResize"></div>
<!-- Right Panel: Summary/Notes and Chat -->
<div id="rightMainColumn" class="flex flex-col overflow-hidden"
:style="{width: rightColumnWidth + '%'}">
<!-- Audio Player -->
<div
class="bg-[var(--bg-secondary)] p-4 border-b border-[var(--border-primary)] flex-shrink-0">
<div class="audio-player-container">
<audio controls :src="'/tool/speakr/audio/' + selectedRecording.id"
:volume="playerVolume" @volumechange="onPlayerVolumeChange"
class="w-full">
Your browser does not support the audio element.
</audio>
</div>
</div>
<!-- Summary/Notes Tabs -->
<div :class="{'flex-1': !isChatMaximized, 'flex-none': isChatMaximized}"
class="flex flex-col overflow-hidden">
<!-- Tab Navigation -->
<div
class="bg-[var(--bg-tertiary)] border-b border-[var(--border-primary)] flex">
<button @click="selectedTab = 'summary'" :class="[
'flex-1 px-4 py-3 text-sm font-medium transition-colors',
selectedTab === 'summary'
? 'bg-[var(--bg-secondary)] text-[var(--text-accent)] border-b-2 border-[var(--border-focus)]'
: 'text-[var(--text-muted)] hover:text-[var(--text-secondary)]'
]">
${ t('summary.title') }
</button>
<button @click="selectedTab = 'notes'" :class="[
'flex-1 px-4 py-3 text-sm font-medium transition-colors',
selectedTab === 'notes'
? 'bg-[var(--bg-secondary)] text-[var(--text-accent)] border-b-2 border-[var(--border-focus)]'
: 'text-[var(--text-muted)] hover:text-[var(--text-secondary)]'
]">
${ t('notes.title') }
</button>
<button
v-if="selectedRecording.events && selectedRecording.events.length > 0"
@click="selectedTab = 'events'" :class="[
'flex-1 px-4 py-3 text-sm font-medium transition-colors',
selectedTab === 'events'
? 'bg-[var(--bg-secondary)] text-[var(--text-accent)] border-b-2 border-[var(--border-focus)]'
: 'text-[var(--text-muted)] hover:text-[var(--text-secondary)]'
]">
<i class="fas fa-calendar-alt mr-1"></i>
Events (${ selectedRecording.events.length })
</button>
</div>
<!-- Tab Content -->
<div v-if="!isChatMaximized" class="flex-1 overflow-hidden">
<!-- Summary Tab -->
<div v-if="selectedTab === 'summary'" class="h-full p-4 overflow-y-auto">
<div v-if="selectedRecording.status === 'SUMMARIZING'"
class="text-center py-8">
<i
class="fas fa-spinner fa-spin text-2xl text-[var(--text-muted)]"></i>
<p class="mt-2 text-[var(--text-muted)]"
v-text="t('summary.summaryInProgress')"></p>
</div>
<div v-else-if="!selectedRecording.summary" class="text-center py-8">
<i
class="fas fa-file-alt text-3xl text-[var(--text-muted)] mb-3"></i>
<p class="text-[var(--text-muted)] mb-4"
v-text="t('summary.noSummary')"></p>
<button @click="generateSummary"
class="px-6 py-3 bg-gradient-to-r from-blue-600 to-purple-600 text-white font-medium rounded-lg shadow-lg hover:shadow-xl hover:from-blue-700 hover:to-purple-700 transition-all duration-200 transform hover:scale-105">
<i class="fas fa-magic mr-2"></i>${ t('summary.generateSummary')
}
</button>
</div>
<div v-else class="content-box relative">
<div v-if="!editingSummary"
class="absolute top-2 right-4 flex gap-1 z-10">
<button @click="copySummary" :title="t('buttons.copySummary')"
class="w-8 h-8 flex items-center justify-center bg-transparent hover:bg-[var(--bg-secondary)] text-[var(--text-muted)] hover:text-[var(--text-primary)] rounded-md border border-transparent hover:border-[var(--border-secondary)] transition-all duration-200 opacity-60 hover:opacity-100">
<i class="fas fa-copy text-sm"></i>
</button>
<button @click="downloadSummary"
:title="t('buttons.downloadSummary')"
class="w-8 h-8 flex items-center justify-center bg-transparent hover:bg-[var(--bg-secondary)] text-[var(--text-muted)] hover:text-[var(--text-primary)] rounded-md border border-transparent hover:border-[var(--border-secondary)] transition-all duration-200 opacity-60 hover:opacity-100">
<i class="fas fa-download text-sm"></i>
</button>
<button @click="toggleEditSummary"
:title="t('buttons.editSummary')"
class="w-8 h-8 flex items-center justify-center bg-transparent hover:bg-[var(--bg-secondary)] text-[var(--text-muted)] hover:text-[var(--text-primary)] rounded-md border border-transparent hover:border-[var(--border-secondary)] transition-all duration-200 opacity-60 hover:opacity-100">
<i class="fas fa-edit text-sm"></i>
</button>
</div>
<div v-if="!editingSummary" class="summary-box"
v-html="selectedRecording.summary_html || selectedRecording.summary">
</div>
<div v-else class="h-full flex flex-col">
<div class="markdown-editor-container flex-1">
<textarea ref="summaryMarkdownEditor"
v-model="selectedRecording.summary"
class="w-full h-full p-4 bg-[var(--bg-input)] border border-[var(--border-secondary)] rounded-lg resize-none focus:outline-none focus:ring-2 focus:ring-[var(--ring-focus)]"
:placeholder="t('form.enterSummaryMarkdown')">
</textarea>
</div>
<div class="flex justify-end gap-2 mt-2">
<button @click="cancelEditSummary"
class="px-3 py-1 text-sm bg-[var(--bg-secondary)] text-[var(--text-secondary)] border border-[var(--border-secondary)] rounded hover:bg-[var(--bg-tertiary)]">
取消
</button>
<button @click="saveEditSummary"
class="px-3 py-1 text-sm bg-[var(--bg-button)] text-[var(--text-button)] rounded hover:bg-[var(--bg-button-hover)]">
保存
</button>
</div>
</div>
</div>
</div>
<!-- Notes Tab -->
<div v-if="selectedTab === 'notes'" class="h-full p-4 overflow-y-auto">
<div class="content-box h-full relative">
<div v-if="!editingNotes"
class="absolute top-2 right-4 flex gap-1 z-10">
<button @click="copyNotes" :title="t('buttons.copyNotes')"
class="w-8 h-8 flex items-center justify-center bg-transparent hover:bg-[var(--bg-secondary)] text-[var(--text-muted)] hover:text-[var(--text-primary)] rounded-md border border-transparent hover:border-[var(--border-secondary)] transition-all duration-200 opacity-60 hover:opacity-100">
<i class="fas fa-copy text-sm"></i>
</button>
<button @click="downloadNotes"
:title="t('buttons.downloadNotes')"
class="w-8 h-8 flex items-center justify-center bg-transparent hover:bg-[var(--bg-secondary)] text-[var(--text-muted)] hover:text-[var(--text-primary)] rounded-md border border-transparent hover:border-[var(--border-secondary)] transition-all duration-200 opacity-60 hover:opacity-100">
<i class="fas fa-download text-sm"></i>
</button>
<button @click="toggleEditNotes" :title="t('buttons.editNotes')"
class="w-8 h-8 flex items-center justify-center bg-transparent hover:bg-[var(--bg-secondary)] text-[var(--text-muted)] hover:text-[var(--text-primary)] rounded-md border border-transparent hover:border-[var(--border-secondary)] transition-all duration-200 opacity-60 hover:opacity-100">
<i class="fas fa-edit text-sm"></i>
</button>
</div>
<div v-if="!editingNotes" class="notes-box h-full"
@click="clickToEditNotes">
<div v-if="selectedRecording.notes_html"
v-html="selectedRecording.notes_html">
</div>
<div v-else-if="selectedRecording.notes"
class="whitespace-pre-wrap">
${selectedRecording.notes}
</div>
<div v-else
class="text-[var(--text-muted)] italic cursor-pointer hover:text-[var(--text-secondary)]">
${ t('help.clickToAddNotes') }
</div>
</div>
<div v-else class="h-full flex flex-col">
<div class="markdown-editor-container flex-1">
<textarea ref="notesMarkdownEditor"
v-model="selectedRecording.notes"
class="w-full h-full p-4 bg-[var(--bg-input)] border border-[var(--border-secondary)] rounded-lg resize-none focus:outline-none focus:ring-2 focus:ring-[var(--ring-focus)]"
:placeholder="t('form.enterNotesMarkdown')">
</textarea>
</div>
<div class="flex justify-end gap-2 mt-2">
<button @click="cancelEditNotes"
class="px-3 py-1 text-sm bg-[var(--bg-secondary)] text-[var(--text-secondary)] border border-[var(--border-secondary)] rounded hover:bg-[var(--bg-tertiary)]">
取消
</button>
<button @click="saveEditNotes"
class="px-3 py-1 text-sm bg-[var(--bg-button)] text-[var(--text-button)] rounded hover:bg-[var(--bg-button-hover)]">
保存
</button>
</div>
</div>
</div>
</div>
<!-- Events Tab -->
<div v-if="selectedTab === 'events' && selectedRecording.events && selectedRecording.events.length > 0"
class="h-full p-4 overflow-y-auto">
<div class="space-y-4">
<div v-for="event in selectedRecording.events" :key="event.id"
class="bg-[var(--bg-tertiary)] rounded-lg p-4 border border-[var(--border-primary)] hover:border-[var(--border-accent)] transition-colors">
<div class="flex justify-between items-start mb-3">
<div class="flex-1">
<h3
class="text-lg font-semibold text-[var(--text-primary)] mb-1">
<i
class="fas fa-calendar-check mr-2 text-[var(--text-accent)]"></i>
${ event.title }
</h3>
<p v-if="event.description"
class="text-sm text-[var(--text-secondary)] mb-2">
${ event.description }
</p>
</div>
<button @click="downloadEventICS(event)"
class="px-3 py-1.5 bg-[var(--bg-button)] text-[var(--text-button)] rounded-lg hover:bg-[var(--bg-button-hover)] transition-colors text-sm flex items-center gap-1.5"
:title="t('events.addToCalendar')">
<i class="fas fa-download"></i>
<span>Add to Calendar</span>
</button>
</div>
<div class="space-y-2 text-sm">
<div class="flex items-center gap-2"
v-if="event.start_datetime">
<i
class="fas fa-clock text-[var(--text-muted)] w-4"></i>
<span class="text-[var(--text-secondary)]">
<strong>Start:</strong> ${
formatEventDateTime(event.start_datetime) }
</span>
</div>
<div class="flex items-center gap-2"
v-if="event.end_datetime">
<i
class="fas fa-clock text-[var(--text-muted)] w-4"></i>
<span class="text-[var(--text-secondary)]">
<strong>End:</strong> ${
formatEventDateTime(event.end_datetime) }
</span>
</div>
<div class="flex items-center gap-2" v-if="event.location">
<i
class="fas fa-map-marker-alt text-[var(--text-muted)] w-4"></i>
<span class="text-[var(--text-secondary)]">
<strong>Location:</strong> ${ event.location }
</span>
</div>
<div class="flex items-start gap-2"
v-if="event.attendees && event.attendees.length > 0">
<i
class="fas fa-users text-[var(--text-muted)] w-4 mt-0.5"></i>
<div class="text-[var(--text-secondary)]">
<strong>Attendees:</strong>
<span class="ml-1">${ event.attendees.join(', ')
}</span>
</div>
</div>
</div>
</div>
<div v-if="selectedRecording.events.length === 0"
class="text-center py-8">
<i
class="fas fa-calendar-times text-3xl text-[var(--text-muted)] mb-3"></i>
<p class="text-[var(--text-muted)]">No events detected in this
recording</p>
</div>
</div>
</div>
</div>
</div>
<!-- Chat Section -->
<div class="border-t border-[var(--border-primary)] flex flex-col"
:class="{'flex-1 overflow-hidden': isChatMaximized}">
<!-- Chat Toggle -->
<div
class="bg-[var(--bg-tertiary)] px-4 py-3 hover:bg-[var(--bg-accent-hover)] transition-colors flex items-center">
<button
@click="() => { showChat = !showChat; if (!showChat) isChatMaximized = false; }"
class="flex-1 text-left flex items-center">
<span class="font-medium flex items-center">
<i class="fas fa-comments mr-2"></i>
${ t('chat.chatWithTranscription') }
</span>
</button>
<div class="flex items-center gap-2">
<button v-if="chatMessages.length > 0" @click="downloadChat"
class="p-1 text-[var(--text-muted)] hover:text-[var(--text-primary)] transition-colors"
:title="t('buttons.downloadChat')">
<i class="fas fa-download text-sm"></i>
</button>
<button @click="toggleChatMaximize"
class="p-1 text-[var(--text-muted)] hover:text-[var(--text-primary)] transition-colors"
:title="isChatMaximized ? 'Restore chat' : 'Maximize chat'">
<i
:class="['fas text-sm', isChatMaximized ? 'fa-compress' : 'fa-expand']"></i>
</button>
<button
@click="() => { showChat = !showChat; if (!showChat) isChatMaximized = false; }"
class="p-1 text-[var(--text-muted)] hover:text-[var(--text-primary)]">
<i
:class="['fas transition-transform duration-200', showChat ? 'fa-chevron-down' : 'fa-chevron-up']"></i>
</button>
</div>
</div>
<!-- Chat Content -->
<div v-if="showChat" class="flex flex-col relative"
:class="isChatMaximized ? 'flex-1 overflow-hidden' : 'max-h-96'">
<!-- Clear button - fixed at top right -->
<button v-if="chatMessages.length > 0" @click="clearChat"
class="absolute top-2 right-2 p-1 text-[var(--text-muted)] opacity-60 hover:opacity-100 hover:text-[var(--text-danger)] rounded transition-all duration-200 z-20"
title="Clear chat">
<i class="fas fa-times text-xs"></i>
</button>
<!-- Chat Messages -->
<div ref="chatMessagesRef"
class="flex-1 overflow-y-auto p-4 space-y-4 bg-[var(--bg-secondary)]">
<div v-if="chatMessages.length === 0" class="text-center py-8">
<i class="fas fa-robot text-3xl text-[var(--text-muted)] mb-3"></i>
<p class="text-[var(--text-muted)]"
v-text="t('help.askAboutTranscription')"></p>
</div>
<div v-for="(message, index) in chatMessages" :key="index" :class="[
'message relative group',
message.role === 'user' ? 'user-message ml-auto' : 'ai-message',
message.role === 'assistant' ? 'pr-10' : ''
]">
<!-- Copy button for assistant messages -->
<button v-if="message.role === 'assistant'"
@click="copyMessage(message.content, $event)"
class="absolute top-2 right-2 p-1 text-[var(--text-muted)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-tertiary)] rounded transition-all duration-200"
:title="t('buttons.copyMessage')">
<i class="fas fa-copy text-sm"></i>
</button>
<!-- Show thinking content if available -->
<div v-if="message.thinking && message.role === 'assistant'"
class="mb-2">
<button
@click="message.thinkingExpanded = !message.thinkingExpanded"
class="text-xs text-[var(--text-muted)] hover:text-[var(--text-secondary)] flex items-center gap-1">
<i
:class="['fas', message.thinkingExpanded ? 'fa-chevron-down' : 'fa-chevron-right']"></i>
<span v-text="t('help.modelReasoning')"></span>
<span
class="text-[var(--text-muted)]">(${message.thinking.split('\n').length}
lines)</span>
</button>
<div v-if="message.thinkingExpanded"
class="mt-2 p-3 bg-[var(--bg-tertiary)] border border-[var(--border-secondary)] rounded-lg text-xs text-[var(--text-muted)] max-h-64 overflow-y-auto">
<pre
class="whitespace-pre-wrap font-mono">${message.thinking}</pre>
</div>
</div>
<!-- Main message content -->
<div v-if="message.html" v-html="message.html"></div>
<div v-else class="whitespace-pre-wrap">${message.content}</div>
</div>
<div v-if="isChatLoading" class="ai-message">
<i class="fas fa-spinner fa-spin mr-2"></i>
${ t('chat.thinking') }
</div>
</div>
<!-- Chat Input -->
<div
class="border-t border-[var(--border-primary)] p-4 bg-[var(--bg-tertiary)]">
<div class="flex gap-2">
<textarea v-model="chatInput" @keydown="handleChatKeydown"
:disabled="isChatLoading || selectedRecording.status !== 'COMPLETED'"
:placeholder="t('chat.placeholderWithHint')"
class="flex-1 px-3 py-2 bg-[var(--bg-input)] border border-[var(--border-secondary)] rounded-lg resize-none focus:outline-none focus:ring-2 focus:ring-[var(--ring-focus)] text-sm"
rows="2">
</textarea>
<button @click="sendChatMessage"
:disabled="!chatInput.trim() || isChatLoading || selectedRecording.status !== 'COMPLETED'"
class="px-4 py-2 bg-[var(--bg-button)] text-[var(--text-button)] rounded-lg hover:bg-[var(--bg-button-hover)] transition-colors disabled:opacity-50 disabled:cursor-not-allowed">
<i class="fas fa-paper-plane"></i>
</button>
</div>
<p v-if="selectedRecording.status !== 'COMPLETED'"
class="text-xs text-[var(--text-muted)] mt-2">
${ t('chat.availableAfterTranscription') }
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- ==================== 功能块:录音详情视图 End ==================== -->
<!-- ==================== 功能块:无选中录音空态视图 Start ==================== -->
<!-- Empty State -->
<div v-else-if="currentView === 'detail' && !selectedRecording"
class="flex-1 flex items-center justify-center">
<div class="text-center">
<i class="fas fa-microphone text-6xl text-[var(--text-muted)] mb-4"></i>
<h2 class="text-2xl font-bold mb-2" v-text="t('colorScheme.selectRecording')"></h2>
<p class="text-[var(--text-muted)] mb-6" v-text="t('colorScheme.chooseRecording')"></p>
<button @click="switchToUploadView"
class="px-6 py-3 bg-[var(--bg-button)] text-[var(--text-button)] rounded-lg hover:bg-[var(--bg-button-hover)] transition-colors">
<i class="fas fa-plus mr-2"></i>Upload New Recording
</button>
</div>
</div>
</div>
</div>
<!-- ==================== 功能块:无选中录音空态视图 End ==================== -->
<!-- ==================== 功能块:主内容视图区 End ==================== -->
<!-- ==================== 功能块:应用主工作区 End ==================== -->
<!-- ==================== 功能块:上传队列进度弹层 Start ==================== -->
<!-- Upload Progress Popup -->
<div v-if="uploadQueue.length > 0 && !progressPopupClosed" :class="[
'fixed bottom-4 left-4 z-[100] w-96 transition-all duration-300',
progressPopupMinimized ? 'minimized' : ''
]">
<div class="bg-[var(--bg-secondary)] border-2 border-[var(--bg-accent)] rounded-lg shadow-2xl overflow-hidden backdrop-blur-sm bg-opacity-95
ring-4 ring-[var(--bg-accent)] ring-opacity-20"
style="box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04), 0 0 0 1px var(--bg-accent), 0 0 20px rgba(var(--bg-accent-rgb), 0.3)">
<!-- Progress Header -->
<div class="bg-gradient-to-r from-[var(--bg-accent)] to-[var(--bg-accent-hover)] px-4 py-3">
<!-- Top row: Title and buttons -->
<div class="flex items-center justify-between mb-2">
<div class="flex items-center gap-2 min-w-0">
<i class="fas fa-upload text-gray-800 dark:text-white animate-pulse"></i>
<span class="font-semibold text-gray-800 dark:text-white truncate"
v-text="t('uploadProgress.title')"></span>
</div>
<div class="flex items-center gap-2 flex-shrink-0">
<button v-if="completedInQueue > 0" @click="clearCompletedUploads()"
class="px-2 py-0.5 text-xs bg-black dark:bg-white bg-opacity-10 dark:bg-opacity-20 hover:bg-opacity-20 dark:hover:bg-opacity-30 rounded transition-all duration-200 text-gray-800 dark:text-white font-medium"
:title="t('buttons.clearCompleted')">
Clear
</button>
<button @click="progressPopupMinimized = !progressPopupMinimized"
class="p-1 rounded hover:bg-black dark:hover:bg-white hover:bg-opacity-10 dark:hover:bg-opacity-20 transition-colors text-gray-800 dark:text-white">
<i :class="progressPopupMinimized ? 'fas fa-chevron-up' : 'fas fa-chevron-down'"></i>
</button>
<button @click="progressPopupClosed = true"
class="p-1 rounded hover:bg-red-600 dark:hover:bg-red-500 hover:bg-opacity-20 dark:hover:bg-opacity-30 text-gray-800 dark:text-white hover:text-white transition-colors">
<i class="fas fa-times"></i>
</button>
</div>
</div>
<!-- Overall Progress Bar -->
<div class="space-y-1">
<div class="flex justify-between items-center text-xs text-gray-800 dark:text-white">
<span class="font-medium">${completedInQueue} of ${totalInQueue} files</span>
<span class="font-bold">${Math.round((completedInQueue / Math.max(totalInQueue, 1)) *
100)}%</span>
</div>
<div
class="w-full bg-black dark:bg-white bg-opacity-10 dark:bg-opacity-20 rounded-full h-2 overflow-hidden">
<div class="bg-white dark:bg-white bg-opacity-80 dark:bg-opacity-90 h-full rounded-full transition-all duration-500 ease-out relative overflow-hidden"
:style="{width: (completedInQueue / Math.max(totalInQueue, 1) * 100) + '%'}">
<div class="absolute inset-0 bg-white opacity-30 animate-pulse"></div>
</div>
</div>
</div>
</div>
<!-- Progress Content -->
<div v-if="!progressPopupMinimized" class="p-3 max-h-64 overflow-y-auto">
<div v-if="currentlyProcessingFile"
class="mb-3 p-3 bg-[var(--bg-accent)] bg-opacity-10 border border-[var(--border-accent)] rounded-lg">
<div class="flex items-center justify-between mb-2">
<span
class="font-semibold truncate min-w-0 text-sm text-[var(--text-primary)]">${currentlyProcessingFile.file.name}</span>
<span
class="text-sm flex-shrink-0 ml-2 font-bold text-[var(--text-accent)]">${processingProgress}%</span>
</div>
<div class="w-full bg-[var(--bg-tertiary)] rounded-full h-2 mb-2 overflow-hidden">
<div class="bg-gradient-to-r from-[var(--bg-accent)] to-[var(--bg-accent-hover)] h-full rounded-full transition-all duration-300 relative"
:style="{width: processingProgress + '%'}">
<div class="absolute inset-0 bg-white opacity-20 animate-pulse"></div>
</div>
</div>
<p class="text-xs text-[var(--text-secondary)] font-medium">${processingMessage}</p>
</div>
<div v-if="finishedFilesInQueue.length > 0" class="space-y-2">
<h4 class="text-xs font-semibold text-[var(--text-secondary)] uppercase tracking-wider opacity-70"
v-text="t('help.completedFiles')"></h4>
<div v-for="item in finishedFilesInQueue" :key="item.clientId"
class="progress-list-item p-2 bg-[var(--bg-tertiary)] bg-opacity-50 rounded-lg flex items-center gap-2 hover:bg-opacity-70 transition-all duration-200">
<i :class="[
'fas',
item.status === 'completed' ? 'fa-check-circle text-green-500' : 'fa-exclamation-circle text-red-500'
]"></i>
<span class="truncate text-sm flex-1 text-[var(--text-primary)]">${item.file.name}</span>
<span :class="[
'text-xs px-2 py-1 rounded-full font-semibold',
item.status === 'completed' ? 'bg-green-500 bg-opacity-20 text-green-600 dark:text-green-400' : 'bg-red-500 bg-opacity-20 text-red-600 dark:text-red-400'
]">
${item.status === 'completed' ? t('tagsModal.done') : t('status.failed')}
</span>
</div>
</div>
</div>
</div>
</div>
<!-- ==================== 功能块:上传队列进度弹层 End ==================== -->
<!-- ==================== 功能块:共享弹窗区 Start ==================== -->
<!-- Modals -->
<!-- ==================== 功能块:录音基础信息编辑弹窗 Start ==================== -->
<!-- Edit Modal -->
<div v-if="showEditModal"
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)]">
<h3 class="text-lg font-semibold" v-text="t('modal.editRecording')"></h3>
</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>
<input v-model="editingRecording.meeting_date" type="date"
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.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">
取消
</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">
保存更改
</button>
</div>
</div>
</div>
<!-- ==================== 功能块:录音基础信息编辑弹窗 End ==================== -->
<!-- ==================== 功能块:录音删除确认弹窗 Start ==================== -->
<!-- Delete Confirmation Modal -->
<div v-if="showDeleteModal"
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 gap-4 mb-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>
<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">
取消
</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>
<!-- ==================== 功能块:录音删除确认弹窗 End ==================== -->
<!-- ==================== 功能块:录音标签编辑弹窗 Start ==================== -->
<!-- Edit Recording Tags Modal -->
<div v-if="showEditTagsModal" @click="closeEditTagsModal"
class="fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center z-50 p-4 backdrop-blur-sm">
<div @click.stop
class="bg-[var(--bg-secondary)] rounded-lg shadow-xl w-full max-w-lg max-h-[80vh] flex flex-col">
<div class="flex-shrink-0 p-6 border-b border-[var(--border-primary)]">
<div class="flex items-center justify-between">
<h3 class="text-lg font-medium text-[var(--text-primary)]" 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"></i>
</button>
</div>
</div>
<div class="flex-1 overflow-y-auto p-6 space-y-4">
<!-- Current Tags Section -->
<div>
<label class="block text-sm font-medium text-[var(--text-secondary)] mb-3">
<i class="fas fa-tags mr-1 text-[var(--text-muted)]"></i>
<span v-text="t('tagsModal.currentTags')"></span>
</label>
<div v-if="getRecordingTags(editingRecording).length > 0"
class="p-2 bg-[var(--bg-primary)] rounded-md border border-[var(--border-secondary)] max-h-20 overflow-y-auto">
<div class="flex flex-wrap gap-1">
<span v-for="tag in getRecordingTags(editingRecording)" :key="tag.id"
class="group inline-flex items-center px-1.5 py-0.5 rounded-full text-[11px] font-medium text-white transition-all"
:style="{ backgroundColor: tag.color }">
<span v-text="tag.name"></span>
<button @click="removeTagFromRecording(tag.id)"
class="ml-1 opacity-70 hover:opacity-100 transition-opacity">
<i class="fas fa-times" style="font-size: 9px;"></i>
</button>
</span>
</div>
</div>
<div v-else
class="p-4 bg-[var(--bg-tertiary)] rounded-lg border border-[var(--border-secondary)]">
<p class="text-sm text-[var(--text-muted)] text-center">
<i class="fas fa-inbox mr-1"></i>
<span v-text="t('tagsModal.noTagsAssigned')"></span>
</p>
</div>
</div>
<!-- Add Tags Section -->
<div>
<label class="block text-sm font-medium text-[var(--text-secondary)] mb-3">
<i class="fas fa-plus-circle mr-1 text-[var(--text-muted)]"></i>
<span v-text="t('tagsModal.addTags')"></span>
</label>
<!-- Search Filter (always visible if tags available) -->
<div v-if="getAvailableTagsForRecording(editingRecording).length > 0" class="mb-2">
<div class="relative">
<input v-model="tagSearchFilter" type="text" :placeholder="t('tagsModal.searchTags')"
class="w-full px-2 py-1 pl-6 text-xs bg-[var(--bg-primary)] text-[var(--text-primary)] border border-[var(--border-primary)] rounded focus:outline-none focus:ring-1 focus:ring-[var(--ring-focus)]">
<i class="fas fa-search absolute left-2 top-1/2 transform -translate-y-1/2 text-[var(--text-muted)]"
style="font-size: 9px;"></i>
<button v-if="tagSearchFilter" @click="tagSearchFilter = ''"
class="absolute right-2 top-1/2 transform -translate-y-1/2 text-[var(--text-muted)] hover:text-[var(--text-primary)]">
<i class="fas fa-times" style="font-size: 9px;"></i>
</button>
</div>
</div>
<!-- Available Tags Grid - Fixed Height -->
<div v-if="filteredAvailableTagsForModal.length > 0"
class="border border-[var(--border-secondary)] rounded-md p-1.5 bg-[var(--bg-primary)]"
style="max-height: 150px; overflow-y: auto;">
<div class="grid grid-cols-2 gap-1">
<button v-for="tag in filteredAvailableTagsForModal" :key="tag.id"
@click="addTagToRecording(tag.id)"
class="group flex items-center justify-between px-1.5 py-1 rounded border border-[var(--border-secondary)] hover:border-[var(--border-focus)] bg-[var(--bg-secondary)] hover:bg-[var(--bg-tertiary)] transition-all">
<div class="flex items-center gap-1 min-w-0">
<span class="w-2 h-2 rounded-full flex-shrink-0"
:style="{ backgroundColor: tag.color || '#6B7280' }"></span>
<span class="text-[11px] text-[var(--text-primary)] truncate"
v-text="tag.name"></span>
</div>
<i class="fas fa-plus text-[var(--text-muted)] group-hover:text-[var(--text-accent)] transition-colors"
style="font-size: 9px;"></i>
</button>
</div>
</div>
<div v-else-if="getAvailableTagsForRecording(editingRecording).length > 0"
class="p-4 bg-[var(--bg-tertiary)] rounded-lg border border-[var(--border-secondary)]">
<p class="text-sm text-[var(--text-muted)] text-center">
<i class="fas fa-search mr-1"></i>
No tags match your filter
</p>
</div>
<div v-else
class="p-4 bg-[var(--bg-tertiary)] rounded-lg border border-[var(--border-secondary)]">
<p class="text-sm text-[var(--text-muted)] text-center">
<i class="fas fa-check-circle mr-1"></i>
所有可用标签已添加
</p>
</div>
</div>
</div>
<div
class="flex-shrink-0 px-6 py-4 bg-[var(--bg-tertiary)] border-t border-[var(--border-primary)] flex justify-end gap-3 rounded-b-lg">
<button @click="closeEditTagsModal"
class="px-4 py-2 bg-[var(--bg-secondary)] text-[var(--text-primary)] rounded-md hover:bg-[var(--bg-button-hover)] transition-colors">
<span v-text="t('tagsModal.done')"></span>
</button>
</div>
</div>
</div>
<!-- ==================== 功能块:录音标签编辑弹窗 End ==================== -->
<!-- ==================== 功能块:配色方案设置弹窗 Start ==================== -->
<!-- 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">
<h2 class="color-scheme-title">
<i class="fas fa-palette"></i>
Choose Color Scheme
</h2>
<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>
${isDarkMode ? 'Dark' : 'Light'} Themes
</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">${scheme.name}</div>
<div class="color-scheme-description">${scheme.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>Reset to Default
</button>
<button @click="closeColorSchemeModal" class="color-scheme-close-btn">
Close
</button>
</div>
</div>
</div>
<!-- ==================== 功能块:配色方案设置弹窗 End ==================== -->
<!-- ==================== 功能块:重新处理确认弹窗 Start ==================== -->
<!-- Reprocess Confirmation Modal -->
<div v-if="showReprocessModal" @click="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 @click.stop
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-[var(--bg-accent)] to-[var(--bg-secondary)] p-5 rounded-t-xl flex items-center">
<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>
</div>
<!-- Content -->
<div class="p-6 modal-content">
<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' && !useAsrEndpoint">
<span v-text="t('reprocessModal.audioReTranscribedFromScratch')"></span>
</p>
<p class="text-sm text-[var(--text-muted)]"
v-else-if="reprocessType === 'transcription' && useAsrEndpoint">
<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>
<!-- ASR Options -->
<div v-if="reprocessType === 'transcription' && useAsrEndpoint"
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>
<div>
<label for="asr-language-reprocess"
class="block text-sm font-medium text-[var(--text-muted)]"
v-text="t('form.language')"></label>
<input type="text" 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)]"
placeholder="e.g., en, es, zh (optional)">
</div>
<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>
<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)]">
<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>
取消
</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>
开始重新处理
</button>
</div>
</div>
</div>
<!-- ==================== 功能块:重新处理确认弹窗 End ==================== -->
<!-- ==================== 功能块:状态重置确认弹窗 Start ==================== -->
<!-- Reset Status Confirmation Modal -->
<div v-if="showResetModal" @click="cancelReset"
class="fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center z-50 p-4 backdrop-blur-sm">
<div @click.stop class="bg-[var(--bg-secondary)] rounded-xl shadow-2xl w-full max-w-md">
<div class="p-6 text-center">
<div
class="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-orange-100 dark:bg-orange-800 mb-4">
<i class="fas fa-exclamation-triangle text-2xl text-orange-500 dark:text-orange-300"></i>
</div>
<h3 class="text-lg font-semibold text-[var(--text-primary)] mb-2" v-text="t('modal.resetStatus')">
</h3>
<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)]">
取消
</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>
<!-- ==================== 功能块:状态重置确认弹窗 End ==================== -->
<!-- ==================== 功能块:纯文本转写编辑弹窗 Start ==================== -->
<!-- Simple Transcription Editor Modal -->
<div v-if="showTextEditorModal"
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)]">
<h3 class="text-lg font-semibold" v-text="t('modal.editTranscription')"></h3>
</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">
取消
</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">
保存更改
</button>
</div>
</div>
</div>
<!-- ==================== 功能块:纯文本转写编辑弹窗 End ==================== -->
<!-- ==================== 功能块ASR 分段转写编辑弹窗 Start ==================== -->
<!-- ASR Transcription Editor Modal -->
<div v-if="showAsrEditorModal"
class="fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center z-50 p-4 backdrop-blur-sm">
<div @click.stop
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>
<div class="flex-grow overflow-y-auto custom-scrollbar p-6">
<table class="min-w-full asr-editor-table">
<thead>
<tr>
<th class="w-1/4" v-text="t('help.speakerCount')"></th>
<th class="w-24" v-text="t('help.startTime')"></th>
<th class="w-24" v-text="t('help.endTime')"></th>
<th v-text="t('help.sentence')"></th>
<th class="w-12"><span class="sr-only" v-text="t('help.actions')"></span></th>
</tr>
</thead>
<tbody>
<tr v-for="(segment, index) in editingSegments" :key="segment.id || index">
<td>
<div class="speaker-combobox" @mouseleave="closeSpeakerSuggestions(index)">
<input type="text" v-model="segment.speaker" @input="filterSpeakers(index)"
@focus="openSpeakerSuggestions(index)"
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)] speaker-combobox-input" />
<div class="speaker-combobox-arrow">
<i class="fas fa-chevron-down text-xs"></i>
</div>
<div v-if="segment.showSuggestions" class="speaker-combobox-suggestions">
<div v-for="speaker in segment.filteredSpeakers"
@mousedown="selectSpeaker(index, speaker)"
class="speaker-suggestion-item">${speaker}</div>
</div>
</div>
</td>
<td>
<div class="custom-number-input">
<input type="number" step="0.01" v-model.number="segment.start_time"
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)]" />
<div class="number-spinners">
<button @click="adjustTime(index, 'start_time', 0.1)"><svg
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round"
d="M4.5 15.75l7.5-7.5 7.5 7.5" />
</svg></button>
<button @click="adjustTime(index, 'start_time', -0.1)"><svg
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round"
d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
</svg></button>
</div>
</div>
</td>
<td>
<div class="custom-number-input">
<input type="number" step="0.01" v-model.number="segment.end_time"
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)]" />
<div class="number-spinners">
<button @click="adjustTime(index, 'end_time', 0.1)"><svg
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round"
d="M4.5 15.75l7.5-7.5 7.5 7.5" />
</svg></button>
<button @click="adjustTime(index, 'end_time', -0.1)"><svg
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round"
d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
</svg></button>
</div>
</div>
</td>
<td>
<textarea v-model="segment.sentence" rows="1"
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"></textarea>
</td>
<td class="text-center">
<button @click="removeSegment(index)" class="text-red-500 hover:text-red-700">
<i class="fas fa-trash"></i>
</button>
</td>
</tr>
</tbody>
</table>
<div class="mt-4">
<button @click="addSegment"
class="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 flex items-center">
<i class="fas fa-plus mr-2"></i> 添加片段
</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-blue-600 text-white rounded-lg hover:bg-blue-700"
v-text="t('buttons.saveChanges')"></button>
</div>
</div>
</div>
<!-- ==================== 功能块ASR 分段转写编辑弹窗 End ==================== -->
<!-- ==================== 功能块:说话人识别弹窗 Start ==================== -->
<!-- Speaker Identification Modal -->
<div v-if="showSpeakerModal" @click="closeSpeakerModal"
class="fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center z-50 p-4 backdrop-blur-sm">
<div @click.stop @click="closeSpeakerSuggestionsOnClick"
class="bg-[var(--bg-secondary)] rounded-xl shadow-2xl w-full max-w-4xl flex flex-col h-[85vh]">
<div class="p-5 border-b border-[var(--border-primary)] flex-shrink-0">
<h3 class="text-xl font-bold text-[var(--text-primary)]" v-text="t('modal.identifySpeakers')"></h3>
<!-- Warning for too many speakers -->
<div v-if="modalSpeakers.length > 8"
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>
<div class="flex-grow flex overflow-hidden modal-content">
<div
class="w-1/3 p-6 space-y-4 overflow-y-auto custom-scrollbar border-r border-[var(--border-primary)]">
<div v-for="(speaker, index) in modalSpeakers"
:key="`${selectedRecording.id}-speaker-${index}-${speaker}`" class="space-y-3">
<!-- Speaker label with color indicator and "This is Me" checkbox on same line -->
<div class="flex items-center justify-between">
<div class="flex items-center space-x-2">
<div :class="speakerMap[speaker].color"
class="w-4 h-4 rounded-full border border-white/30 shadow-sm flex-shrink-0">
</div>
<span class="font-mono text-sm text-[var(--text-muted)]">${
speakerDisplayMap[speaker] || speaker }</span>
<span v-if="index >= 8"
class="text-xs text-[var(--text-warn-strong)] bg-[var(--bg-warn-light)] px-2 py-0.5 rounded-full"
title="Color repeats from speaker ${ ((index % 8) + 1) }">
Repeat
</span>
</div>
<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="speakerMap[speaker].isMe"
@focus="highlightSpeakerInTranscript(speaker)" @blur="clearSpeakerHighlight"
class="speaker-checkbox">
<span class="ml-2 select-none" v-text="t('help.me')"></span>
</label>
</div>
<!-- Autocomplete input field - disabled when "This is Me" is checked -->
<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-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"
:placeholder="speakerMap[speaker].isMe ? t('help.me') : t('help.enterNameFor') + ' ' + (speakerDisplayMap[speaker] || speaker)"
autocomplete="off">
<!-- Loading indicator -->
<div v-if="loadingSuggestions[speaker] && !speakerMap[speaker].isMe"
class="absolute right-3 top-1/2 transform -translate-y-1/2">
<i class="fas fa-spinner fa-spin text-[var(--text-muted)] text-sm"></i>
</div>
<!-- Suggestions dropdown - Only show when this input is active -->
<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-48 overflow-y-auto">
<div class="py-1">
<div v-for="suggestion in speakerSuggestions[speaker]" :key="suggestion.id"
@click="selectSpeakerSuggestion(speaker, 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>
</div>
</div>
<div class="w-2/3 p-6 flex flex-col overflow-hidden">
<!-- Audio Player Section -->
<div class="mb-4 flex-shrink-0">
<h4 class="font-semibold text-[var(--text-secondary)] mb-2" v-text="t('help.audioPlayer')">
</h4>
<div class="audio-player-container">
<audio controls class="w-full speaker-modal-transcript" :key="selectedRecording.id"
:src="'/tool/speakr/audio/' + selectedRecording.id" :volume="playerVolume"
@volumechange="onPlayerVolumeChange">
Your browser does not support the audio element.
</audio>
</div>
</div>
<!-- Transcript Section -->
<div class="flex-grow overflow-y-auto custom-scrollbar speaker-modal-transcript"
@click="seekAudioFromEvent">
<div v-if="processedTranscription.isJson">
<div v-for="(segment, index) in processedTranscription.simpleSegments" :key="index"
class="speaker-segment mb-2" :data-start-time="segment.startTime">
<p>
<span
:class="[segment.color, 'speaker-tag', { 'speaker-highlight': highlightedSpeaker === segment.speakerId }]"
:data-speaker-id="segment.speakerId">[${ segment.speaker }]</span>:
<span
class="word cursor-pointer hover:bg-[var(--bg-accent)] hover:text-[var(--text-accent)] rounded px-0.5">${
segment.sentence }</span>
</p>
</div>
</div>
<div v-else v-html="highlightedTranscript"></div>
</div>
<!-- Speaker Navigation Controls (at bottom) - Always visible for consistent height -->
<div class="mt-3 p-3 bg-[var(--bg-tertiary)] rounded-lg flex items-center justify-between flex-shrink-0"
:class="{'opacity-50': !highlightedSpeaker || speakerGroups.length <= 1}">
<div class="flex items-center space-x-2">
<span class="text-sm text-[var(--text-muted)]">
Navigate <span class="font-semibold">${ speakerDisplayMap[highlightedSpeaker] ||
highlightedSpeaker }</span> groups:
</span>
<span class="text-xs text-[var(--text-muted)]">
(${ currentSpeakerGroupIndex + 1 } of ${ speakerGroups.length })
</span>
</div>
<div class="flex items-center space-x-2">
<button @mousedown.prevent @click="navigateToPrevSpeakerGroup"
class="px-3 py-1.5 bg-[var(--bg-secondary)] hover:bg-[var(--bg-primary)] text-[var(--text-secondary)] rounded-md border border-[var(--border-secondary)] transition-all flex items-center text-sm disabled:opacity-50 disabled:cursor-not-allowed"
:disabled="!highlightedSpeaker || speakerGroups.length <= 1">
<i class="fas fa-chevron-up mr-1.5"></i>
Previous
</button>
<button @mousedown.prevent @click="navigateToNextSpeakerGroup"
class="px-3 py-1.5 bg-[var(--bg-secondary)] hover:bg-[var(--bg-primary)] text-[var(--text-secondary)] rounded-md border border-[var(--border-secondary)] transition-all flex items-center text-sm disabled:opacity-50 disabled:cursor-not-allowed"
:disabled="!highlightedSpeaker || speakerGroups.length <= 1">
Next
<i class="fas fa-chevron-down ml-1.5"></i>
</button>
</div>
</div>
</div>
</div>
<div class="p-4 bg-[var(--bg-tertiary)] border-t border-[var(--border-primary)] flex-shrink-0">
<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="regenerateSummaryAfterSpeakerUpdate" class="speaker-checkbox">
<span class="ml-2 select-none" v-text="t('help.regenerateSummaryAfterNames')"></span>
</label>
</div>
<div
class="bg-[var(--bg-tertiary)] px-6 py-4 flex justify-between items-center border-t border-[var(--border-primary)] flex-shrink-0 rounded-b-xl">
<div>
<button @click="autoIdentifySpeakers"
class="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 flex items-center"
:disabled="isAutoIdentifying">
<i class="fas fa-magic mr-2"></i>
<span v-if="!isAutoIdentifying" v-text="t('help.autoIdentify')"></span>
<span v-else>
<i class="fas fa-spinner fa-spin mr-2"></i>
Identifying...
</span>
</button>
</div>
<div class="flex space-x-3">
<button @click="closeSpeakerModal"
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="saveSpeakerNames" :disabled="!hasSpeakerNames" :class="hasSpeakerNames
? 'px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 cursor-pointer'
: 'px-4 py-2 bg-gray-400 text-gray-200 rounded-lg cursor-not-allowed'">
保存名称
</button>
</div>
</div>
</div>
</div>
<!-- ==================== 功能块:说话人识别弹窗 End ==================== -->
<!-- ==================== 功能块:全局错误提示区 Start ==================== -->
<!-- Global Error Display -->
<div v-if="globalError"
class="fixed top-4 right-4 bg-[var(--bg-danger)] text-white p-4 rounded-lg shadow-lg z-50 max-w-md">
<div class="flex items-start gap-3">
<i class="fas fa-exclamation-circle text-lg flex-shrink-0 mt-0.5"></i>
<div class="flex-1">
<p class="font-medium" v-text="t('common.error')"></p>
<p class="text-sm opacity-90">${globalError}</p>
</div>
<button @click="globalError = null" class="text-white hover:text-gray-200 transition-colors">
<i class="fas fa-times"></i>
</button>
</div>
</div>
<!-- ==================== 功能块:全局错误提示区 End ==================== -->
<!-- ==================== 功能块:系统音频录制帮助弹窗 Start ==================== -->
<!-- System Audio Help Modal -->
<div v-if="showSystemAudioHelp"
class="fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center z-50 backdrop-blur-sm">
<div
class="bg-[var(--bg-secondary)] w-full h-full md:h-auto md:max-h-[90vh] md:w-full md:max-w-2xl md:rounded-lg md:shadow-xl flex flex-col">
<div class="p-4 border-b border-[var(--border-primary)] flex-shrink-0">
<div class="flex items-center justify-between">
<h3 class="text-lg font-semibold flex items-center">
<i class="fas fa-desktop mr-2 text-blue-500"></i>
System Audio Recording
</h3>
<button @click="showSystemAudioHelp = false"
class="text-[var(--text-muted)] hover:text-[var(--text-primary)]">
<i class="fas fa-times"></i>
</button>
</div>
</div>
<div class="p-6 space-y-6 overflow-y-auto flex-grow">
<div class="bg-[var(--bg-tertiary)] p-4 rounded-lg border border-[var(--border-primary)]">
<h4 class="font-medium text-[var(--text-primary)] mb-2"
v-text="t('help.howToRecordSystemAudio')"></h4>
<p class="text-sm text-[var(--text-muted)]" v-text="t('help.toCaptureAudioFromMeetings')">
</p>
<ol class="list-decimal list-inside space-y-2 text-sm text-[var(--text-secondary)] mt-3">
<li v-text="t('help.recordSystemSteps1')"></li>
<li><span v-text="t('help.recordSystemSteps2')"></span> <strong>"${ t('help.entireScreen')
}"</strong> or a <strong v-text="t('help.specificBrowserTab')"></strong>.</li>
<li><span v-text="t('help.recordSystemSteps3')"></span> <strong>"${ t('help.shareTabAudio')
}"</strong> or <strong>"${ t('help.shareSystemAudio') }"</strong>.</li>
</ol>
<p class="text-xs text-amber-600 dark:text-amber-400 mt-3"><i
class="fas fa-exclamation-triangle mr-1"></i><span
v-text="t('help.sharingWindowNoAudio')"></span></p>
</div>
<div class="space-y-4">
<h4 class="font-medium text-[var(--text-primary)]" v-text="t('help.troubleshooting')"></h4>
<div class="bg-[var(--bg-tertiary)] p-4 rounded-lg border border-[var(--border-primary)]">
<h5 class="font-medium text-[var(--text-primary)] mb-2" v-text="t('help.whyNotWorking')">
</h5>
<p class="text-sm text-[var(--text-muted)]">
System audio recording requires a secure (HTTPS) connection. If you are developing
locally, you may need to enable a browser flag.
</p>
</div>
<!-- Chrome Solution -->
<div v-if="browser === 'chrome'"
class="bg-[var(--bg-accent)] p-4 rounded-lg border border-[var(--border-accent)]">
<h5 class="font-medium text-[var(--text-accent)] mb-3 flex items-center">
<i class="fab fa-chrome mr-2"></i>
For Chrome on Localhost
</h5>
<ol class="list-decimal list-inside space-y-2 text-sm text-[var(--text-secondary)]">
<li>Go to: <code
class="bg-[var(--bg-secondary)] px-2 py-1 rounded text-xs ml-1">chrome://flags/#unsafely-treat-insecure-origin-as-secure</code>
</li>
<li>Add your local URL (e.g., <code
class="bg-[var(--bg-secondary)] px-2 py-1 rounded text-xs ml-1">http://localhost:8899</code>)
</li>
<li>Set to <strong>"Enabled"</strong> and relaunch Chrome.</li>
</ol>
</div>
<!-- Firefox Solution -->
<div v-else-if="browser === 'firefox'"
class="bg-[var(--bg-accent)] p-4 rounded-lg border border-[var(--border-accent)]">
<h5 class="font-medium text-[var(--text-accent)] mb-3 flex items-center">
<i class="fab fa-firefox-browser mr-2"></i>
For Firefox on Localhost
</h5>
<p class="text-sm text-[var(--text-secondary)]">
System audio recording is not well-supported on Firefox, even with these changes. For
best results, please use Chrome.
</p>
<ol class="list-decimal list-inside space-y-2 text-sm text-[var(--text-secondary)] mt-2">
<li>Go to: <code
class="bg-[var(--bg-secondary)] px-2 py-1 rounded text-xs ml-1">about:config</code>
</li>
<li>Search for <code
class="bg-[var(--bg-secondary)] px-2 py-1 rounded text-xs ml-1">media.devices.insecure.enabled</code>
and set it to `true`.</li>
<li>Restart Firefox.</li>
</ol>
</div>
<!-- Other Browsers -->
<div v-else
class="bg-[var(--bg-tertiary)] p-4 rounded-lg border border-[var(--border-primary)]">
<h5 class="font-medium text-[var(--text-primary)] mb-2"
v-text="t('help.unsupportedBrowser')"></h5>
<p class="text-sm text-[var(--text-muted)]">
For the best experience with system audio recording, we recommend using Google Chrome.
</p>
</div>
</div>
</div>
<div class="p-4 border-t border-[var(--border-primary)] flex justify-end flex-shrink-0">
<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">
Got it
</button>
</div>
</div>
</div>
<!-- ==================== 功能块:系统音频录制帮助弹窗 End ==================== -->
<!-- ==================== 功能块:录音免责声明弹窗 Start ==================== -->
<!-- Recording Disclaimer Modal -->
<div v-if="showRecordingDisclaimerModal"
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 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>
</div>
<div class="flex-1 overflow-y-auto p-6">
<!-- Render markdown content -->
<div class="prose prose-sm max-w-none text-[var(--text-secondary)]"
style="color: var(--text-secondary); line-height: 1.6;">
<div v-html="recordingDisclaimer"></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)]">
取消
</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>
<!-- ==================== 功能块:录音免责声明弹窗 End ==================== -->
<!-- ==================== 功能块:页面 Toast 提示容器 Start ==================== -->
<!-- Toast Container -->
<div id="toastContainer" class="fixed bottom-4 right-4 z-50 space-y-2"></div>
<!-- ==================== 功能块:页面 Toast 提示容器 End ==================== -->
<!-- ==================== 功能块:录音分享弹窗 Start ==================== -->
<!-- Share Modal -->
<div v-if="showShareModal"
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)]">
<h3 class="text-lg font-semibold" v-text="t('modal.shareRecording')"></h3>
</div>
<div class="p-6 space-y-4">
<p class="text-sm text-[var(--text-muted)]" v-text="t('help.createPublicLink')"></p>
<div v-if="!generatedShareLink">
<div class="flex items-center mb-2">
<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">
<label for="share_summary" class="ml-2 block text-sm text-[var(--text-secondary)]"
v-text="t('form.shareSummary')"></label>
</div>
<div 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">
<label for="share_notes" class="ml-2 block text-sm text-[var(--text-secondary)]"
v-text="t('form.shareNotes')"></label>
</div>
</div>
<div v-if="generatedShareLink" class="mt-4">
<div v-if="existingShareDetected"
class="mb-3 p-3 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg">
<div class="flex items-start gap-2">
<i class="fas fa-info-circle text-blue-600 dark:text-blue-400 mt-0.5"></i>
<div class="text-sm text-blue-800 dark:text-blue-300">
<p class="font-medium">Using existing share link</p>
<p class="mt-1 text-xs opacity-90">This recording was already shared. The same link
has been retrieved.</p>
</div>
</div>
</div>
<label class="block text-sm font-medium mb-2" v-text="t('form.shareableLink')"></label>
<div class="flex items-center gap-2">
<input :value="generatedShareLink" readonly
class="w-full px-3 py-2 bg-[var(--bg-input)] border border-[var(--border-secondary)] rounded-lg">
<button @click="copyShareLink"
class="px-3 py-2 bg-[var(--bg-button)] text-[var(--text-button)] rounded-lg hover:bg-[var(--bg-button-hover)]">
<i class="fas fa-copy"></i>
</button>
</div>
<div v-if="existingShareDetected" class="mt-3">
<button @click="createShare(true)"
class="text-sm text-[var(--text-accent)] hover:underline">
<i class="fas fa-plus-circle mr-1"></i>Create a new share link instead
</button>
</div>
</div>
</div>
<div class="p-6 border-t border-[var(--border-primary)] flex justify-end gap-3">
<button @click="closeShareModal"
class="px-4 py-2 text-[var(--text-secondary)] hover:bg-[var(--bg-tertiary)] rounded-lg transition-colors">
取消
</button>
<button @click="createShare()" v-if="!generatedShareLink"
class="px-4 py-2 bg-[var(--bg-button)] text-[var(--text-button)] rounded-lg hover:bg-[var(--bg-button-hover)] transition-colors">
创建链接
</button>
</div>
</div>
</div>
<!-- ==================== 功能块:录音分享弹窗 End ==================== -->
<!-- ==================== 功能块:共享链接列表弹窗 Start ==================== -->
<!-- Shares List Modal -->
<div v-if="showSharesListModal"
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">
<input :value="'{{ request.url_root }}share/' + share.public_id" readonly
class="w-full px-3 py-2 bg-[var(--bg-input)] border border-[var(--border-secondary)] rounded-lg text-sm">
</div>
</div>
</div>
</div>
</div>
</div>
<!-- ==================== 功能块:共享链接列表弹窗 End ==================== -->
<!-- ==================== 功能块:共享链接删除确认弹窗 Start ==================== -->
<!-- Share Delete Confirmation Modal -->
<div v-if="showShareDeleteModal"
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">
<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>
</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">
取消
</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>
<!-- ==================== 功能块:共享链接删除确认弹窗 End ==================== -->
<!-- ==================== 功能块:共享弹窗区 End ==================== -->
</div>
<!-- ==================== 功能块Vue 应用根容器 End ==================== -->
<!-- ==================== 功能块:页面底部脚本区 Start ==================== -->
<!-- CSRF Token Management -->
<script src="/tool/speakr/static/js/csrf-refresh.js"></script>
<script src="/tool/speakr/static/js/i18n.js"></script>
<!-- ==================== 功能块:国际化初始化脚本 Start ==================== -->
<!-- i18n System -->
<!-- Initialize i18n immediately with fallback -->
<script>
//------------------- i18n 预初始化 start------------------
// Ensure i18n is initialized before Vue app
(async function () {
if (window.i18n && window.i18n.init) {
const userLang = localStorage.getItem('preferredLanguage') || '{{ user_language }}' || 'en';
try {
await window.i18n.init(userLang);
console.log('i18n pre-initialized with language:', userLang);
} catch (error) {
console.error('Failed to initialize i18n:', error);
}
}
// Hide loader after i18n is ready
const loader = document.getElementById('loader');
if (loader) {
loader.style.display = 'none';
}
})();
//--------------------i18n 预初始化 end------------------
</script>
<!-- ==================== 功能块:国际化初始化脚本 End ==================== -->
<!-- ==================== 功能块Vue 应用脚本加载 Start ==================== -->
<!-- Vue.js Application Script -->
<script src="/tool/speakr/static/js/wsconnecter.js"></script>
<script src="/tool/speakr/static/js/app.js"></script>
<!-- ==================== 功能块Vue 应用脚本加载 End ==================== -->
<!-- ==================== 功能块:页面底部脚本区 End ==================== -->
<!-- ==================== 功能块:页面主体 End ==================== -->
</body>
<!-- ==================== 功能块:页面局部样式区 Start ==================== -->
<style>
/* ==================== 功能块:录音页左侧实时录音面板样式 Start ==================== */
.websockt_box {
width: 50%;
border-radius: 10px;
overflow: hidden;
/* height: 500px; */
position: relative;
}
/* ==================== 功能块:录音页左侧实时录音面板样式 End ==================== */
/* ==================== 功能块:录音页双栏容器样式 Start ==================== */
.index_m_cot {
width: 100%;
display: flex;
gap: 20px;
height: 100%;
padding: 20px;
border-radius: 10px;
overflow: hidden;
}
/* ==================== 功能块:录音页双栏容器样式 End ==================== */
/* ==================== 功能块:富文本与代码块显示样式 Start ==================== */
.prose {
white-space: pre-wrap; /* 保留换行并自动换行 */
word-wrap: break-word; /* 超长单词自动换行 */
overflow-wrap: break-word; /* 兼容老浏览器 */
color: #fff; /* 根据你背景色调整 */
}
/* 代码块样式可选 */
.prose pre {
width: 100%;
background: rgba(255, 255, 255, 0.1);
padding: 8px;
border-radius: 6px;
overflow-y: auto;
}
.prose pre {
white-space: pre-wrap !important; /* 自动换行 */
word-break: break-word !important; /* 长字符串也换行 */
overflow-wrap: break-word !important;
}
.prose code {
white-space: pre-wrap !important;
word-break: break-word !important;
}
/* ==================== 功能块:富文本与代码块显示样式 End ==================== */
</style>
<!-- ==================== 功能块:页面局部样式区 End ==================== -->
</html>