- content_for :head do meta name="turbo-cache-control" content="no-cache" = turbo_stream_from @respondent, "tasks" .container-fluid#transcriptPage v-cloak="" data-controller="transcripts" .row .col-md-3 = render partial: "respondent_categories/sidebar" .col-md-9 .sticky-top.bg-white.z-3.w-100.px-3.pt-3.pb-2.shadow-bottom-only style="top: 60px;" div = render_breadcrumbs([ \ { name: 'Home', path: projects_path }, \ { name: @project.title, path: project_path(@project) }, \ { name: @respondent_category.name, path: [@project, @respondent_category] }, \ { name: @respondent.name } \ ]) .d-flex.justify-content-between.align-items-center .d-flex.align-items-center style="max-width: 40vw;" .fs-30.semi-bold.flex-grow-1.text-break style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis;" = @respondent.name - if current_user.admin? span.badge.bg-dark.ms-1= AsrModel.find_by(value: @respondent.asr_model).name rescue "Error" = link_to edit_project_respondent_category_respondent_path(@project, @respondent_category, @respondent), class: 'btn btn-link text-black p-0 ms-2 modal-trigger', data: { turbo_frame: "modal_frame" } do i.feather-20(data-feather="edit-2") - unless @respondent.tags.empty? .w-50.text-end - @respondent.tags.includes(:tag_category).each do |tag| span.badge.bg-secondary.me-2.text-black.fs-10 = tag.tag_category.name |: = tag.name - if !@respondent.audio_file_file_name.nil? .mt-3 v-show="currentPlayer == 'audio'" .d-flex.justify-content-between.align-items-center h6= t(:transcript_audio) - if !@respondent.video_file_file_name.nil? button.btn.btn-sm.bg-primary.text-white.mb-1 @click="toggleMedia('video')" | Show Video .player audio.plyr-player controls="" ref="audioElement" source src="#{@respondent.signed_audio_url}" type="#{@mime_type}" / Video Player Wrapper .mt-3 v-show="currentPlayer == 'video'" .d-flex.justify-content-between.align-items-center h6= t(:transcript_video) button.btn.btn-sm.bg-primary.text-white.mb-1 @click="toggleMedia('audio')" | Hide Video .player.video-container video.plyr-player controls="" preload="metadata" ref="videoElement" source src="#{@respondent.signed_video_url}" type="#{@mime_type}" .d-flex.justify-content-between.align-items-center.mt-5.mb-2.w-100 .d-flex.align-items-center v-if="transcriptEditing == false" .fs-20 Transcript .dropdown.ms-auto / The 3-dot Trigger Button button.btn.btn-link.text-dark.text-decoration-none.p-1.border-0 data-bs-toggle="dropdown" aria-expanded="false" = lucide_icon("more-vertical") / The Dropdown Menu ul.dropdown-menu.dropdown-menu-end.custom-dropdown-border.shadow-sm.rounded-3.py-2 aria-labelledby="transcriptMenuButton" - if @respondent.transcript_available? li = link_to download_transcript_project_respondent_category_respondent_path(@project, @respondent_category, @respondent), class: 'dropdown-item d-flex align-items-center gap-3 py-2' do i data-feather="download" span.fs-14 Download li = link_to translation_modal_project_respondent_category_respondent_path(@project, @respondent_category, @respondent), class: 'dropdown-item d-flex align-items-center gap-3 py-2 modal-trigger', data: { turbo_frame: "modal_frame" } do = lucide_icon("languages") span.fs-14 Translate - if @respondent.docx_transcript.nil? - if @respondent.transcript_available? li: hr.dropdown-divider.opacity-50 - if @respondent.any_archived_transcripts? li = link_to archived_transcripts_project_respondent_category_respondent_path(@project, @respondent_category, @respondent), class: 'dropdown-item d-flex align-items-center gap-3 py-2' do i data-feather="archive" span.fs-14 Explore archive li = link_to transcript_feedback_project_respondent_category_respondent_path(@project, @respondent_category, @respondent), class: 'dropdown-item d-flex align-items-center gap-3 py-2 modal-trigger', data: { turbo_frame: "modal_frame" } do = lucide_icon("edit") span.fs-14 Feedback - if @respondent.transcript_available? .d-flex.align-items-center v-if="transcriptEditing == true" h4 Edit Transcript .d-flex.align-items-center.gap-2 v-if="transcriptEditing == false" .bg-white.border.rounded.d-flex.align-items-center.gap-2.px-2.py-1.fs-14 style="height: 36px;" i.bi.bi-type.text-primary button.btn.btn-outline-secondary.btn-sm.fs-10 @click="decreaseFontSize" :disabled="fontSize <= minFontSize" vue-feather type="minus" class="text-primary mt-1" size="10" span.small.text-muted | {{ fontSize }} button.btn.btn-outline-secondary.btn-sm.fs-10 @click="increaseFontSize" :disabled="fontSize >= maxFontSize" vue-feather type="plus" class="text-primary mt-1" size="10" - if !@respondent.whisper_transcript.nil? = button_to "Archive", remove_transcript_project_respondent_category_respondent_path(@project, @respondent_category, @respondent), method: :delete, class: "btn btn-outline-info fs-14", form: { data: { turbo_confirm: "Are you sure you want to archive this transcript?" }, class: "d-inline-block" } - if @respondent.generating_bookmarks? button.btn.btn-secondary.d-flex.align-items-center.fs-14 disabled="true" .span.spinner.spinner-primary | Generating Sections - else = button_to queue_bookmark_generation_project_respondent_category_respondent_path(@project, @respondent_category, @respondent), class: 'btn btn-secondary fs-14', data: { turbo_method: :post } do | Generate Sections = render "respondents/summary_button", respondent: @respondent, project: @project, respondent_category: @respondent_category - if ["hi", "multi"].include?(@respondent.language) button.transliteration-toggle.btn.btn-secondary.d-flex.align-items-center.fs-14 @click="toggleTransliteration" vue-feather type="eye" class="text-primary me-2" size="12" v-if="transliterationEnabled == false" vue-feather type="eye-off" class="text-primary me-2" size="12" v-if="transliterationEnabled == true" | Transliteration - if @respondent.docx_transcript.nil? = link_to "Map Speakers", speaker_map_project_respondent_category_respondent_path(@project, @respondent_category, @respondent), class: "btn btn-secondary fs-14 modal-trigger", data: { turbo_frame: "modal_frame" } button class="btn btn-secondary fs-14" v-on:click="toggleTranscriptEditing()" Edit / = link_to "Edit Transcript", edit_auto_transcript_project_respondent_category_respondent_path(@project, @respondent_category, @respondent), class: "btn btn-secondary ms-2" .d-flex.semi-bold.gap-2 v-if="transcriptEditing == true" button class="btn btn-secondary fs-12 py-2 px-5" style="font-weight: 600" v-on:click="resetEditing()" Reset input type="submit" value="Done" form="transcript_form" class="btn btn-primary fs-12 px-5" style="font-weight: 600" / AI Transcript - if !@respondent.whisper_transcript.nil? div v-show="transcriptEditing == false" div .row .col .section-group v-for="section in groupedUtterances" .section-header.mt-3 v-if="section.ungrouped == false" :id="'section-' + section.header.id" .d-flex.justify-content-between.align-items-center div | {{section.header.name}} button.btn.btn-link.p-0.text-danger @click="removeSection(section.header.id)" title="Remove Section" vue-feather type="trash-2" size="16" hr class="border-primary m-0" v-if="section.ungrouped == false" .section-body class="w-100 rounded bg-light py-2" .sectionable v-for="utterance in section.utterances" :key="utterance.index" @mousemove="updateFloatingButton($event, utterance.index)" @mouseleave="delayHideFloatingButton" .row .col .form-group.mb-1 class="w-100" .px-3 .d-flex.align-items-center .mt-2.ms-2 :class="getFontSizeClass()" - if ["hi", "multi"].include?(@respondent.language) span :key="utterance.utterance.start + '-' + utterance.utterance.end" :class="getHighlightId(utterance.utterance) ? 'highlight-text' : 'no-highlight'" :data-start-time="utterance.utterance.start" :data-end-time="utterance.utterance.end" :data-highlight-id="getHighlightId(utterance.utterance)" :style="{ backgroundColor: getHighlightColor(utterance.utterance) }" @contextmenu="showContextMenu($event)" @click.left="playerSeek(utterance.utterance.start)" | {{ display_speakers[utterance.utterance.speaker] }} : {{ transliterate(utterance.utterance.text) }} - else span :key="utterance.utterance.start + '-' + utterance.utterance.end" :class="getHighlightId(utterance.utterance) ? 'highlight-text' : 'no-highlight'" :data-start-time="utterance.utterance.start" :data-end-time="utterance.utterance.end" :data-highlight-id="getHighlightId(utterance.utterance)" :style="{ backgroundColor: getHighlightColor(utterance.utterance) }" @contextmenu="showContextMenu($event)" @click.left="playerSeek(utterance.utterance.start)" | {{ display_speakers[utterance.utterance.speaker] }} : {{ utterance.utterance.text }} .highlight-info.shadow-sm v-if="hoverData.visible" :style="{ left: hoverData.x + 'px', top: hoverData.y + 'px' }" .highlighted-by.mb-1 | Highlighter: {{ hoverData.highlightedBy }} .theme v-if="hoverData.theme && hoverData.theme.length > 0" | Theme: {{ hoverData.theme }} / Pagination - uncomment to enable / .text-center.my-4 v-if="hasMoreUtterances" / button.btn.btn-primary.btn-lg @click="loadMore" :disabled="isLoadingMore" / span v-if="!isLoadingMore" Load More / span v-if="isLoadingMore" / span.spinner-border.spinner-border-sm.me-2 / | Loading... / .text-muted.mt-2.fs-14 / | Showing {{ displayedUtterancesCount }} of {{ totalUtterancesCount }} utterances div v-show="transcriptEditing == true" / .container class="w-100 rounded bg-light py-2" div = form_tag update_auto_transcript_project_respondent_category_respondent_path(@project, @respondent_category, @respondent), method: :post, id: 'transcript_form' .row .col .section-group v-for="section in groupedUtterances" .section-header.mt-3 v-if="section.ungrouped == false" :id="'edit-section-' + section.header.id" .d-flex.justify-content-between.align-items-center div | {{section.header.name}} hr class="border-primary m-0" v-if="section.ungrouped == false" .section-body class="w-100 rounded bg-light py-2" .sectionable v-for="utterance in section.utterances" .row .col .form-group.mb-1 class="w-100" .px-3 .d-flex.align-items-center.semi-bold.fs-10 select.p-1.medium style="border-style: none;" v-model="utterance.utterance.speaker" option.medium v-for="speaker, label in speakers" v-bind:value="speaker" | {{label}} | @ {{formatTime(utterance.utterance.start)}} button.btn.btn-primary.btn-sm.rounded-circle.p-0.ms-2.d-flex.align-items-center.justify-content-center.play-btn type="button" @click.left="playerSeek(utterance.utterance.start)" i.bi.bi-play-fill style="font-size: 11px; color: white;" textarea.mt-2.fs-12 class="form-control" style="border-style: none; border-color: transparent; overflow: auto; background-color: #F9FAFB; resize: none;" v-model="utterance.utterance.text" / .section-end-container class="w-100" v-if="sectionForOffset(utterance.index)" / .section-end-line / .section-end-text / | {{sectionForOffset(utterance.index).name}} End / .section-end-line / Pagination - uncomment to enable / .text-center.my-4 v-if="hasMoreUtterances" / button.btn.btn-outline-primary.btn-lg @click="loadMore" :disabled="isLoadingMore" / span v-if="!isLoadingMore" Load More to Edit / span v-if="isLoadingMore" / span.spinner-border.spinner-border-sm.me-2 / | Loading... / .text-muted.mt-2.fs-14 / | Showing {{ displayedUtterancesCount }} of {{ totalUtterancesCount }} utterances / Manual Transcript - elsif !@respondent.docx_transcript.nil? div v-show="transcriptEditing == false" div .row .col .section-group v-for="section in groupedUtterances" .section-header.mt-3 v-if="section.ungrouped == false" :id="'section-' + section.header.id" .d-flex.justify-content-between.align-items-center div | {{section.header.name}} button.btn.btn-link.p-0.text-danger @click="removeSection(section.header.id)" title="Remove Section" vue-feather type="trash-2" size="16" hr class="border-primary m-0" v-if="section.ungrouped == false" .section-body class="w-100 rounded bg-light py-2" .sectionable v-for="utterance in section.utterances" :key="utterance.index" @mousemove="updateFloatingButton($event, utterance.index)" @mouseleave="delayHideFloatingButton" .row .col .form-group.mb-1 class="w-100" .px-2 .mt-2.ms-2 :class="getFontSizeClass()" / div v-if="translationEnabled == true" / span :key="'utterance-' + utterance.index" :class="{ 'highlight-text': getHighlightId(utterance.utterance) }" :data-line-number="utterance.utterance.index" :data-highlight-id="getHighlightId(utterance.utterance)" :style="{ backgroundColor: getHighlightColor(utterance.utterance) }" @contextmenu="showContextMenu($event)" @mouseover="showHoverInfo($event, utterance.utterance)" @mouseleave="hideHoverInfo" / | {{ translatedUtteranceMap[utterance.utterance.index] || utterance.utterance.text }} span :key="'utterance-' + utterance.index" :class="getHighlightId(utterance.utterance) ? 'highlight-text' : 'no-highlight'" :data-line-number="utterance.utterance.index" :data-highlight-id="getHighlightId(utterance.utterance)" :style="{ backgroundColor: getHighlightColor(utterance.utterance) }" @contextmenu="showContextMenu($event)" | {{utterance.utterance.text}} .highlight-info.shadow-sm v-if="hoverData.visible" :style="{ left: hoverData.x + 'px', top: hoverData.y + 'px' }" .highlighted-by.mb-1 | Highlighter: {{ hoverData.highlightedBy }} .theme v-if="hoverData.theme && hoverData.theme.length > 0" | Theme: {{ hoverData.theme }} / Pagination - uncomment to enable / .text-center.my-4 v-if="hasMoreUtterances" / button.btn.btn-outline-primary.btn-lg @click="loadMore" :disabled="isLoadingMore" / span v-if="!isLoadingMore" Load More to Edit / span v-if="isLoadingMore" / span.spinner-border.spinner-border-sm.me-2 / | Loading... / .text-muted.mt-2.fs-14 / | Showing {{ displayedUtterancesCount }} of {{ totalUtterancesCount }} utterances div v-show="transcriptEditing == true" div = form_tag update_docx_transcript_project_respondent_category_respondent_path(@project, @respondent_category, @respondent), method: :post, id: 'transcript_form' do .row .col .section-group v-for="section in groupedUtterances" .section-header.mt-3 v-if="section.ungrouped == false" :id="'edit-section-' + section.header.id" .d-flex.justify-content-between.align-items-center input type="hidden" name="sections[][name]" v-bind:value="section.header.name" input type="hidden" name="sections[][offset]" v-bind:value="section.header.offset" input type="hidden" name="sections[][master_section_id]" v-bind:value="section.header.master_section_id" div | {{section.header.name}} hr class="border-primary m-0" v-if="section.ungrouped == false" .section-body class="w-100 rounded bg-light py-2" .sectionable v-for="utterance in section.utterances" .row .col .form-group.mb-1 class="w-100" .px-2 textarea.mt-1.fs-12 style="border-style: none; border-color: transparent; overflow: auto; background-color: #F9FAFB; resize: none;" class="form-control" v-bind:name="'transcript[' + utterance.index + ']'" | {{utterance.utterance.text}} / Pagination - uncomment to enable / .text-center.my-4 v-if="hasMoreUtterances" / button.btn.btn-outline-primary.btn-lg @click="loadMore" :disabled="isLoadingMore" / span v-if="!isLoadingMore" Load More to Edit / span v-if="isLoadingMore" / span.spinner-border.spinner-border-sm.me-2 / | Loading... / .text-muted.mt-2.fs-14 / | Showing {{ displayedUtterancesCount }} of {{ totalUtterancesCount }} utterances div ref="modelRef" class="modal fade" id="addSectionDialog" div class="modal-dialog modal-dialog-centered" div class="modal-content" div class="modal-body p-4" .d-flex.align-items-center.justify-content-between .fs-24.medium | Select Section .d-flex.align-items-center button class="btn btn-light fs-12 px-3 py-2" style="font-weight: 600;" ref="CloseModal" v-on:click="closeAddSectionModal()" Cancel button class="btn btn-primary ms-2 fs-12 px-3 py-2" style="font-weight: 600;" v-on:click="addSection()" Submit .fs-12.text-primary = @respondent.name .my-3 select v-bind:name="newSectionName" class="form-select" id="sectionSelect" style="width: 100%" option v-for="section in masterSections" v-bind:value="[section.id, section.name]" | {{section.name}} .my-2.text-danger v-if="showInvalidSectionError == true" | Invalid Section Name .d-flex.align-items-center i.text-warning.me-2 data-feather="alert-circle" small.text-muted This action will wipe the highlights made in the transcript and reset the assistant. - if @respondent.transcript_available? .sticky-bottom class="shadow-lg p-3 mb-5 bg-body-tertiary mx-auto" style="bottom: 50px; background-color: red; width:35vw;" div v-if="transcriptEditing == false" button.fs-18 class="btn btn-secondary" style="width: 100%; font-weight: 600;" v-on:click="toggleTranscriptEditing()" Toggle to Editing div v-if="transcriptEditing == true" .d-flex style="whitespace: nowrap;" button.fs-18 class="btn btn-secondary" style="width: 100%; font-weight: 600;" v-on:click="toggleTranscriptEditing()" Toggle to Transcript input.fs-18 type="submit" value="Save" form="transcript_form" class="btn btn-primary ms-2" style="width: 100%; font-weight: 600;" .scroll-top-btn.position-fixed.d-flex.justify-content-center.align-items-center v-if="showScrollButton == true" class="shadow-lg rounded-circle bg-primary" style="bottom: 50px; right: 30px; width: 50px; height: 50px; cursor: pointer; z-index: 1000;" v-on:click="scrollToTop()" vue-feather type="arrow-up" size="24" class="text-white" div#context-menu.context-menu // For new highlights, only show color options div v-if="!selectedRange || !selectedRange.existingHighlight" .color-grid .color-box v-for="(color, index) in highlightColors" :key="index" :style="{ backgroundColor: color }" @click="applyHighlight(color)" // For existing highlights, show color options, themes, and remove button div.d-flex.flex-column.align-items-center v-else="" .color-grid .color-box v-for="(color, index) in highlightColors" :key="index" :style="{ backgroundColor: color }" @click="applyHighlight(color)" .theme-section.mt-2.w-100.px-2 // Existing themes dropdown select.form-select.form-select-sm.mb-2 v-model="selectedThemeId" @change="handleThemeSelect" option value="" Select existing theme option v-for="theme in highlightThemes" :key="theme.id" :value="theme.id" | {{ theme.theme }} hr.my-2 // New theme creation .input-group.input-group-sm input.form-control type="text" v-model="newThemeName" placeholder="Create new theme" @keyup.enter="handleNewThemeSubmit" button.btn.btn-outline-primary.d-flex.align-items-center @click="handleNewThemeSubmit" vue-feather type="plus" size="16" - if @is_manual == false && (@respondent.video_file_file_name.present? || @respondent.audio_file_file_name.present?) .reel-section hr.my-2 div v-if="!selectedRange.existingHighlight.processing" div v-if="selectedRange.existingHighlight.reel_available" span.text-body-secondary Reel Generated div v-if="!selectedRange.existingHighlight.reel_available" button.btn.btn-create-reel.rounded-pill.px-4.py-1[ @click="queueReelCreation()" ] i.bi.bi-camera-video.me-2 | Create Reel .d-flex.align-items-center.justify-content-center v-if="selectedRange.existingHighlight.processing" span.text-body-secondary Processing span.spinner.spinner-primary.ms-2 hr.my-2 .delete-highlight.mt-2 button.btn.btn-link.text-danger.p-0 @click="removeCurrentHighlight" vue-feather type="x" size="14" span.ms-1.fs-14 Remove #confirmationModal.modal.fade tabindex="-1" aria-labelledby="confirmationModalLabel" aria-hidden="true" .modal-dialog.modal-dialog-centered .modal-content .modal-header.border-bottom-0 h5#confirmationModalLabel.modal-title.d-flex.align-items-center vue-feather.text-warning.me-2 type="alert-circle" span Confirm Changes button.btn-close type="button" data-bs-dismiss="modal" aria-label="Close" .modal-body.text-center.py-4 p.mb-0.fs-5 This action will wipe the highlights made in the transcript and reset the assistant. p.text-muted.mt-2 Do you want to continue? .modal-footer.border-top-0.justify-content-center.gap-2 button.btn.btn-light.px-4 type="button" @click="cancelSubmission" :disabled="isSubmitting" | Cancel button.btn.btn-primary.px-4 type="button" @click="confirmSubmission" :disabled="isSubmitting" span.spinner-border.spinner-border-sm.me-2 v-if="isSubmitting" | Proceed #summaryOffcanvas.offcanvas.offcanvas-end data-bs-scroll="true" data-bs-backdrop="true" tabindex="-1" style="width: 50vw;" .offcanvas-header.border-bottom .d-flex.align-items-center.gap-2 h5.offcanvas-title.mb-0 Summary = link_to download_summary_project_respondent_category_respondent_path(@project, @respondent_category, @respondent), class: 'btn btn-light btn-sm text-primary', title: 'Download Summary' do i.bi.bi-download = button_to queue_transcript_summarization_project_respondent_category_respondent_path(@project, @respondent_category, @respondent), class: 'btn btn-light btn-sm text-primary', title: 'Regenerate Summary', data: { turbo_method: :post } do i.bi.bi-arrow-clockwise button.btn-close type="button" data-bs-dismiss="offcanvas" aria-label="Close" .offcanvas-body = render "respondents/summary_content", respondent: @respondent .section-split-overlay[ v-if="hoverState.visible && !transcriptEditing" :style="{ top: hoverState.top + 'px', left: hoverState.rowLeft + 'px', width: hoverState.rowWidth + 'px' }" @mouseenter="cancelHideFloatingButton" @mouseleave="delayHideFloatingButton" ] / The full-width clickable area .split-line-clickable[ @click.stop="showAddSectionModal(hoverState.index)" title="Click to add section" ] / The dashed visual .line-dash / The inline text span.line-text + Click to add section / Navigation Panel - if @respondent.transcript_available? .nav-panel.position-fixed.end-0.top-50.translate-middle-y.bg-white.shadow-lg.rounded-start .nav-panel-container.d-flex.flex-column .nav-panel-header.d-flex.align-items-center .icon-box.d-flex.align-items-center.justify-content-center i.bi.bi-list.fs-4.text-primary .title-box.ps-2.pe-3.fw-bold.text-primary.text-uppercase.fs-12.ls-1 | Jump to Section .nav-panel-content.ps-3.pe-2.pb-3 ul.list-unstyled.m-0.custom-scrollbar.nav-sections-list template v-for="section in groupedUtterances" :key="section.header ? section.header.id : 'ungrouped'" li.mb-1 v-if="section.header" a.nav-link-item.text-decoration-none.text-primary.cursor-pointer.fs-14 @click.prevent="scrollToSection(section.header.id)" href="#" i.bi.bi-chevron-right.me-2.fs-10 span.link-text | {{ section.header.name }}