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];
+}