diff --git a/src/renderer/features/notetaker/components/NotetakerView.tsx b/src/renderer/features/notetaker/components/NotetakerView.tsx index 61540923..e7932236 100644 --- a/src/renderer/features/notetaker/components/NotetakerView.tsx +++ b/src/renderer/features/notetaker/components/NotetakerView.tsx @@ -9,6 +9,7 @@ import { useAllRecordings } from "@renderer/features/notetaker/hooks/useAllRecor import { useNotetakerStore } from "@renderer/features/notetaker/stores/notetakerStore"; import { useEffect } from "react"; import { useHotkeys } from "react-hotkeys-hook"; +import { RecordingView } from "@/renderer/features/notetaker/components/RecordingView"; function getStatusIcon( status: "recording" | "uploading" | "processing" | "ready" | "error", @@ -223,6 +224,8 @@ export function NotetakerView() { + + {selectedRecording && } ); } diff --git a/src/renderer/features/notetaker/components/RecordingView.tsx b/src/renderer/features/notetaker/components/RecordingView.tsx new file mode 100644 index 00000000..246ab47e --- /dev/null +++ b/src/renderer/features/notetaker/components/RecordingView.tsx @@ -0,0 +1,243 @@ +import { Badge, Box, Card, Flex, Text } from "@radix-ui/themes"; +import type { RecordingItem } from "@renderer/features/notetaker/hooks/useAllRecordings"; +import type { TranscriptSegment } from "@renderer/stores/activeRecordingStore"; +import { useEffect, useRef, useState } from "react"; + +interface RecordingViewProps { + recordingItem: RecordingItem; +} + +export function RecordingView({ recordingItem }: RecordingViewProps) { + const scrollRef = useRef(null); + const [autoScroll, setAutoScroll] = useState(true); + + const segments: TranscriptSegment[] = + recordingItem.type === "active" + ? recordingItem.recording.segments || [] + : ( + recordingItem.recording.transcript?.segments as Array<{ + timestamp_ms: number; + speaker: string | null; + text: string; + confidence: number | null; + is_final: boolean; + }> + )?.map((seg) => ({ + timestamp: seg.timestamp_ms, + speaker: seg.speaker, + text: seg.text, + confidence: seg.confidence, + is_final: seg.is_final, + })) || []; + + useEffect(() => { + if (autoScroll && scrollRef.current && segments.length > 0) { + scrollRef.current.scrollTop = scrollRef.current.scrollHeight; + } + }, [segments.length, autoScroll]); + + const handleScroll = () => { + if (!scrollRef.current) return; + + const { scrollTop, scrollHeight, clientHeight } = scrollRef.current; + const isNearBottom = scrollHeight - scrollTop - clientHeight < 50; + setAutoScroll(isNearBottom); + }; + + const isPastRecording = recordingItem.type === "past"; + const hasSegments = segments.length > 0; + + return ( + + {/* Meeting Header */} + + + {recordingItem.recording.meeting_title || "Untitled meeting"} + + + + {recordingItem.recording.platform} + + + {new Date( + recordingItem.recording.created_at || new Date(), + ).toLocaleString()} + + + + + {/* Summary - only for past recordings */} + {isPastRecording && ( + + + Summary + + + + + Coming soon + + + + + )} + + {/* Action items - only for past recordings */} + {isPastRecording && ( + + + Action items + + + + + Coming soon + + + + + )} + + {/* Notes - only for past recordings */} + {isPastRecording && ( + + + Notes + + + + + Coming soon + + + + + )} + + {/* Transcript */} + + + + {isPastRecording ? "Transcript" : "Live transcript"} + + {hasSegments && ( + + {segments.length} segments + + )} + + + {hasSegments ? ( + + + {segments.map((segment, idx) => { + const prevSegment = idx > 0 ? segments[idx - 1] : null; + const isSameSpeaker = prevSegment?.speaker === segment.speaker; + + return ( + + + {!isSameSpeaker && segment.speaker && ( + + {segment.speaker} + + )} + + + + + + {formatTimestamp(segment.timestamp)} + + + {segment.text} + + + + + ); + })} + + + ) : ( + + + + {isPastRecording + ? "No transcript available" + : "Waiting for transcript..."} + + + + )} + + {!isPastRecording && !autoScroll && hasSegments && ( + + Scroll to bottom to enable auto-scroll + + )} + + + ); +} + +function formatTimestamp(milliseconds: number): string { + const totalSeconds = Math.floor(milliseconds / 1000); + const hours = Math.floor(totalSeconds / 3600); + const minutes = Math.floor((totalSeconds % 3600) / 60); + const seconds = totalSeconds % 60; + + if (hours > 0) { + return `${hours}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`; + } + return `${minutes}:${seconds.toString().padStart(2, "0")}`; +} + +// Consistent color assignment for speakers +function getSpeakerColor(speaker: string): string { + const colors = [ + "var(--blue-11)", + "var(--green-11)", + "var(--orange-11)", + "var(--purple-11)", + "var(--pink-11)", + "var(--cyan-11)", + ]; + + // Simple hash function to consistently map speaker to color + let hash = 0; + for (let i = 0; i < speaker.length; i++) { + hash = speaker.charCodeAt(i) + ((hash << 5) - hash); + } + return colors[Math.abs(hash) % colors.length]; +}