"use client"; import Modal from "../modal"; import useTopics from "../useTopics"; import useWaveform from "../useWaveform"; import useMp3 from "../useMp3"; import useParticipants from "../useParticipants"; import { TopicList } from "./_components/TopicList"; import { TranscriptWithGutter } from "./_components/TranscriptWithGutter"; import { Topic } from "../webSocketTypes"; import React, { useEffect, useState, use } from "react"; import FinalSummary from "./finalSummary"; import TranscriptTitle from "../transcriptTitle"; import Player from "../player"; import { useRouter } from "next/navigation"; import { Box, Flex, Grid, GridItem, Skeleton, Text, Spinner, } from "@chakra-ui/react"; import { useTranscriptGet } from "../../../lib/apiHooks"; import { TranscriptStatus } from "../../../lib/transcript"; type TranscriptDetails = { params: Promise<{ transcriptId: string; }>; }; export default function TranscriptDetails(details: TranscriptDetails) { const params = use(details.params); const transcriptId = params.transcriptId; const router = useRouter(); const statusToRedirect = [ "idle", "recording", "processing", "uploaded", ] satisfies TranscriptStatus[] as TranscriptStatus[]; const transcript = useTranscriptGet(transcriptId); const waiting = transcript.data && statusToRedirect.includes(transcript.data.status); const mp3 = useMp3(transcriptId, waiting); const topics = useTopics(transcriptId); const participants = useParticipants(transcriptId); const waveform = useWaveform( transcriptId, waiting || mp3.audioDeleted === true, ); const useActiveTopic = useState(null); const [activeTopic, setActiveTopic] = useActiveTopic; const [finalSummaryElement, setFinalSummaryElement] = useState(null); // IntersectionObserver for active topic detection based on scroll position useEffect(() => { if (!topics.topics || topics.topics.length === 0) return; const observer = new IntersectionObserver( (entries) => { // Find the most visible segment let mostVisibleEntry: IntersectionObserverEntry | null = null; let maxRatio = 0; entries.forEach((entry) => { if (entry.isIntersecting && entry.intersectionRatio > maxRatio) { maxRatio = entry.intersectionRatio; mostVisibleEntry = entry; } }); if (mostVisibleEntry) { // Extract topicId from segment id (format: "segment-{topicId}-{idx}") const segmentId = mostVisibleEntry.target.id; const match = segmentId.match(/^segment-([^-]+)-/); if (match) { const topicId = match[1]; const topic = topics.topics?.find((t) => t.id === topicId); if (topic && activeTopic?.id !== topic.id) { setActiveTopic(topic); } } } }, { threshold: [0, 0.25, 0.5, 0.75, 1], rootMargin: "-20% 0px -20% 0px", }, ); // Observe all segment elements const segments = document.querySelectorAll('[id^="segment-"]'); segments.forEach((segment) => observer.observe(segment)); return () => observer.disconnect(); }, [topics.topics, activeTopic?.id, setActiveTopic]); // Scroll handlers for bidirectional navigation const handleTopicClick = (topicId: string) => { // Scroll to first segment of this topic in transcript const firstSegment = document.querySelector(`[id^="segment-${topicId}-"]`); if (firstSegment) { firstSegment.scrollIntoView({ behavior: "smooth", block: "center", }); } }; const handleGutterClick = (topicId: string) => { // Scroll to topic in list const topicChip = document.getElementById(`topic-${topicId}`); if (topicChip) { topicChip.scrollIntoView({ behavior: "smooth", block: "center", }); } }; const getSpeakerName = (speakerNumber: number) => { if (!participants.response) return `Speaker ${speakerNumber}`; return ( participants.response.find( (participant) => participant.speaker == speakerNumber, )?.name || `Speaker ${speakerNumber}` ); }; useEffect(() => { if (!waiting || !transcript.data) return; const status = transcript.data.status; let newUrl: string | null = null; if (status === "processing" || status === "uploaded") { newUrl = `/transcripts/${params.transcriptId}/processing`; } else if (status === "recording") { newUrl = `/transcripts/${params.transcriptId}/record`; } else if (status === "idle") { newUrl = transcript.data.source_kind === "file" ? `/transcripts/${params.transcriptId}/upload` : `/transcripts/${params.transcriptId}/record`; } if (newUrl) { // Shallow redirection does not work on NextJS 13 // https://github.com/vercel/next.js/discussions/48110 // https://github.com/vercel/next.js/discussions/49540 router.replace(newUrl); } }, [waiting, transcript.data?.status, transcript.data?.source_kind]); if (waiting) { return ( Loading transcript... ); } if (transcript.error || topics?.error) { return ( ); } if (transcript?.isLoading || topics?.loading) { return ; } return ( <> {!mp3.audioDeleted && ( <> {waveform.waveform && mp3.media && topics.topics ? ( ) : !mp3.loading && (waveform.error || mp3.error) ? ( Error loading{" "} {[waveform.error && "waveform", mp3.error && "mp3"] .filter(Boolean) .join(" and ")} ) : ( )} )} {/* Title */} { transcript.refetch().then(() => {}); }} transcript={transcript.data || null} topics={topics.topics} finalSummaryElement={finalSummaryElement} /> {mp3.audioDeleted && ( No audio is available because one or more participants didn't consent to keep the audio )} {/* Topics List (top section - fixed height, scrollable) */} {/* Transcript with colored gutter (bottom section - scrollable container) */} {topics.topics && topics.topics.length > 0 && ( )} {/* Final Summary (at bottom) */} {transcript.data && topics.topics ? ( { transcript.refetch().then(() => {}); }} finalSummaryRef={setFinalSummaryElement} /> ) : ( {transcript?.data?.status == "processing" ? ( Loading Transcript ) : ( There was an error generating the final summary, please come back later )} )} ); }