Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 32 additions & 6 deletions src/renderer/features/notetaker/components/RecordingView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,17 +47,27 @@ export function RecordingView({ recordingItem }: RecordingViewProps) {
const isPastRecording = recordingItem.type === "past";
const hasSegments = segments.length > 0;

const participants =
recordingItem.type === "past"
? (recordingItem.recording.participants as string[] | undefined)
: [
...new Set(
segments
.map((s) => s.speaker)
.filter((s): s is string => s !== null && s !== undefined),
),
];

return (
<Box
p="4"
className="flex flex-1 flex-col gap-4 overflow-y-auto overflow-x-hidden"
>
{/* Meeting Header */}
<Flex direction="column" gap="2">
<Text size="4" weight="bold">
{recordingItem.recording.meeting_title || "Untitled meeting"}
</Text>
<Flex gap="2">
<Flex gap="2" wrap="wrap">
<Badge color="gray" variant="soft">
{recordingItem.recording.platform}
</Badge>
Expand All @@ -66,10 +76,29 @@ export function RecordingView({ recordingItem }: RecordingViewProps) {
recordingItem.recording.created_at || new Date(),
).toLocaleString()}
</Text>
{participants && participants.length > 0 && (
<>
<Text size="2" color="gray">
</Text>
<Text size="2" color="gray">
{participants.length} participant
{participants.length !== 1 ? "s" : ""}
</Text>
</>
)}
</Flex>
{participants && participants.length > 0 && (
<Flex gap="1" wrap="wrap">
{participants.map((participant) => (
<Badge key={participant} color="blue" variant="soft" size="1">
{participant}
</Badge>
))}
</Flex>
)}
</Flex>

{/* Summary - only for past recordings */}
{isPastRecording && (
<Flex direction="column" gap="2">
<Text size="2" weight="bold">
Expand All @@ -85,7 +114,6 @@ export function RecordingView({ recordingItem }: RecordingViewProps) {
</Flex>
)}

{/* Action items - only for past recordings */}
{isPastRecording && (
<Flex direction="column" gap="2">
<Text size="2" weight="bold">
Expand All @@ -101,7 +129,6 @@ export function RecordingView({ recordingItem }: RecordingViewProps) {
</Flex>
)}

{/* Notes - only for past recordings */}
{isPastRecording && (
<Flex direction="column" gap="2">
<Text size="2" weight="bold">
Expand All @@ -117,7 +144,6 @@ export function RecordingView({ recordingItem }: RecordingViewProps) {
</Flex>
)}

{/* Transcript */}
<Flex direction="column" gap="2">
<Flex justify="between" align="center">
<Text size="2" weight="bold">
Expand Down
73 changes: 40 additions & 33 deletions src/renderer/services/recordingService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ interface UploadBatch {

const uploadBatches = new Map<string, UploadBatch>();

// Track initialization to prevent double-initialization
let isInitialized = false;

/**
Expand All @@ -22,7 +21,6 @@ let isInitialized = false;
* Call this once when the app starts (outside React component lifecycle)
*/
export function initializeRecordingService() {
// Prevent double-initialization
if (isInitialized) {
console.warn("[RecordingService] Already initialized, skipping");
return;
Expand All @@ -31,8 +29,6 @@ export function initializeRecordingService() {
console.log("[RecordingService] Initializing...");
isInitialized = true;

// Handle crash recovery after checking auth client is ready
// This prevents the race condition where recovery tries to upload before client exists
const authStore = useAuthStore.getState();
if (authStore.client) {
handleCrashRecovery();
Expand All @@ -42,27 +38,22 @@ export function initializeRecordingService() {
);
}

// Listen for recording started events
window.electronAPI.onRecallRecordingStarted((recording) => {
console.log("[RecordingService] Recording started:", recording);

const store = useActiveRecordingStore.getState();
// Pass full DesktopRecording object to store
store.addRecording(recording);

// Initialize upload batch tracker
uploadBatches.set(recording.id, {
recordingId: recording.id,
timer: null,
segmentCount: 0,
});
});

// Listen for transcript segments
window.electronAPI.onRecallTranscriptSegment((data) => {
const store = useActiveRecordingStore.getState();

// Add segment to store
store.addSegment(data.posthog_recording_id, {
timestamp: data.timestamp,
speaker: data.speaker,
Expand All @@ -71,19 +62,16 @@ export function initializeRecordingService() {
is_final: data.is_final,
});

// Track batch for upload
const batch = uploadBatches.get(data.posthog_recording_id);
if (batch) {
batch.segmentCount++;

// Start timer if not already running
if (!batch.timer) {
batch.timer = setTimeout(() => {
uploadPendingSegments(data.posthog_recording_id);
}, BATCH_TIMEOUT_MS);
}

// Upload if batch size reached
if (batch.segmentCount >= BATCH_SIZE) {
if (batch.timer) {
clearTimeout(batch.timer);
Expand All @@ -94,7 +82,6 @@ export function initializeRecordingService() {
}
});

// Listen for meeting ended events
window.electronAPI.onRecallMeetingEnded((data) => {
console.log("[RecordingService] Meeting ended:", data);

Expand All @@ -104,14 +91,50 @@ export function initializeRecordingService() {
}
uploadBatches.delete(data.posthog_recording_id);

// Upload any pending segments, then update status to uploading
uploadPendingSegments(data.posthog_recording_id).then(() => {
uploadPendingSegments(data.posthog_recording_id).then(async () => {
const store = useActiveRecordingStore.getState();

const recording = store.getRecording(data.posthog_recording_id);
if (recording) {
const participants = [
...new Set(
recording.segments
.map((s) => s.speaker)
.filter((s): s is string => s !== null && s !== undefined),
),
];

if (participants.length > 0) {
console.log(
`[RecordingService] Extracted ${participants.length} participants:`,
participants,
);

try {
const authStore = useAuthStore.getState();
const client = authStore.client;

if (client) {
await client.updateDesktopRecording(data.posthog_recording_id, {
participants,
});
console.log(
`[RecordingService] Updated recording with participants`,
);
}
} catch (error) {
console.error(
"[RecordingService] Failed to update participants:",
error,
);
}
}
}

store.updateStatus(data.posthog_recording_id, "uploading");
});
});

// Listen for recording ready events
window.electronAPI.onRecallRecordingReady((data) => {
console.log("[RecordingService] Recording ready:", data);

Expand All @@ -123,9 +146,6 @@ export function initializeRecordingService() {
console.log("[RecordingService] Initialized successfully");
}

/**
* Upload pending transcript segments to backend
*/
async function uploadPendingSegments(recordingId: string): Promise<void> {
const store = useActiveRecordingStore.getState();
const recording = store.getRecording(recordingId);
Expand Down Expand Up @@ -153,7 +173,6 @@ async function uploadPendingSegments(recordingId: string): Promise<void> {
throw new Error("PostHog client not initialized");
}

// Upload segments to backend
await client.updateDesktopRecordingTranscript(recordingId, {
segments: pendingSegments.map((seg) => ({
timestamp_ms: seg.timestamp,
Expand All @@ -164,7 +183,6 @@ async function uploadPendingSegments(recordingId: string): Promise<void> {
})),
});

// Update last uploaded index
const newIndex =
recording.lastUploadedSegmentIndex + pendingSegments.length;
store.updateLastUploadedIndex(recordingId, newIndex);
Expand All @@ -173,7 +191,6 @@ async function uploadPendingSegments(recordingId: string): Promise<void> {
`[RecordingService] Successfully uploaded ${pendingSegments.length} segments`,
);

// Reset batch tracker
const batch = uploadBatches.get(recordingId);
if (batch) {
batch.segmentCount = 0;
Expand All @@ -196,11 +213,9 @@ async function uploadPendingSegments(recordingId: string): Promise<void> {

/**
* Handle crash recovery - upload any pending segments and clear from IDB
* Called on app startup. Keeps things simple: save what we have and move on.
*
* Tradeoff: Might lose last ~10 segments if upload fails during crash recovery.
* Acceptable because: (1) Backend already has 90%+ from batched uploads during meeting
* (2) Crashes are rare, (3) Crash + upload failure is even rarer
* Acceptable because backend already has 90%+ from batched uploads during meeting.
*/
function handleCrashRecovery() {
const store = useActiveRecordingStore.getState();
Expand All @@ -220,36 +235,28 @@ function handleCrashRecovery() {
`[RecordingService] Uploading pending segments for ${recording.id} (best effort)`,
);

// Upload pending segments in background (best effort, don't block startup)
uploadPendingSegments(recording.id).catch((error) => {
console.error(
`[RecordingService] Failed to upload segments during recovery (acceptable):`,
error,
);
});

// Clear from IDB immediately - recording is already in backend
store.clearRecording(recording.id);
console.log(`[RecordingService] Cleared ${recording.id} from IDB`);
}
}

/**
* Clean up the recording service
* Call this when the app shuts down
*/
export function shutdownRecordingService() {
console.log("[RecordingService] Shutting down...");

// Clear all upload batch timers
for (const batch of uploadBatches.values()) {
if (batch.timer) {
clearTimeout(batch.timer);
}
}
uploadBatches.clear();

// Reset initialization flag to allow re-initialization after logout/login
isInitialized = false;

console.log("[RecordingService] Shutdown complete");
Expand Down