Skip to content

Commit 8d0ef6f

Browse files
committed
feat: add Notetaker UI and recording status lifecycle
Add complete Notetaker feature for viewing and managing desktop meeting recordings with proper status lifecycle tracking. Changes: - Add recording status updates (recording → uploading → processing) - Create Notetaker zustand store for state management - Add PostHog API methods (delete, update recording) - Create NotetakerView component with recording list UI - Add Notetaker to sidebar navigation and tab types - Register IPC handlers for notetaker CRUD operations - Update recording status on upload completion and errors Status flow: recording → uploading (on end) → processing (on upload complete). Errors update status to "error" state. Users can now view all recordings with status, platform, duration, and timestamps. Includes delete functionality and auto-refresh.
1 parent b5b5c03 commit 8d0ef6f

File tree

10 files changed

+461
-3
lines changed

10 files changed

+461
-3
lines changed

src/api/posthogClient.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,4 +339,49 @@ export class PostHogAPIClient {
339339
const data = await response.json();
340340
return data.results ?? data ?? [];
341341
}
342+
343+
async deleteDesktopRecording(recordingId: string) {
344+
const teamId = await this.getTeamId();
345+
const url = new URL(
346+
`${this.api.baseUrl}/api/environments/${teamId}/desktop_recordings/${recordingId}/`,
347+
);
348+
const response = await this.api.fetcher.fetch({
349+
method: "delete",
350+
url,
351+
path: `/api/environments/${teamId}/desktop_recordings/${recordingId}/`,
352+
});
353+
354+
if (!response.ok) {
355+
throw new Error(`Failed to delete recording: ${response.statusText}`);
356+
}
357+
}
358+
359+
async updateDesktopRecording(
360+
recordingId: string,
361+
updates: {
362+
status?: string;
363+
title?: string;
364+
duration?: number;
365+
video_url?: string;
366+
},
367+
) {
368+
const teamId = await this.getTeamId();
369+
const url = new URL(
370+
`${this.api.baseUrl}/api/environments/${teamId}/desktop_recordings/${recordingId}/`,
371+
);
372+
const response = await this.api.fetcher.fetch({
373+
method: "patch",
374+
url,
375+
path: `/api/environments/${teamId}/desktop_recordings/${recordingId}/`,
376+
parameters: {
377+
body: updates,
378+
},
379+
});
380+
381+
if (!response.ok) {
382+
throw new Error(`Failed to update recording: ${response.statusText}`);
383+
}
384+
385+
return await response.json();
386+
}
342387
}

src/main/preload.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,13 @@ contextBridge.exposeInMainWorld("electronAPI", {
186186
): Promise<void> =>
187187
ipcRenderer.invoke("recall:request-permission", permission),
188188
recallShutdown: (): Promise<void> => ipcRenderer.invoke("recall:shutdown"),
189+
// Notetaker API
190+
notetakerGetRecordings: (): Promise<unknown[]> =>
191+
ipcRenderer.invoke("notetaker:get-recordings"),
192+
notetakerGetRecording: (recordingId: string): Promise<unknown> =>
193+
ipcRenderer.invoke("notetaker:get-recording", recordingId),
194+
notetakerDeleteRecording: (recordingId: string): Promise<void> =>
195+
ipcRenderer.invoke("notetaker:delete-recording", recordingId),
189196
// Shell API
190197
shellCreate: (sessionId: string, cwd?: string): Promise<void> =>
191198
ipcRenderer.invoke("shell:create", sessionId, cwd),

src/main/services/recallRecording.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,17 +96,65 @@ export function initializeRecallSDK(
9696
try {
9797
console.log("[Recall SDK] Recording ended, uploading...");
9898

99+
const session = activeSessions.get(evt.window.id);
100+
if (session && posthogClient) {
101+
try {
102+
await posthogClient.updateDesktopRecording(session.recordingId, {
103+
status: "uploading",
104+
});
105+
console.log(
106+
`[Recall SDK] Updated recording ${session.recordingId} status to uploading`,
107+
);
108+
} catch (error) {
109+
console.error(
110+
"[Recall SDK] Failed to update recording status:",
111+
error,
112+
);
113+
}
114+
}
115+
99116
await RecallAiSdk.uploadRecording({
100117
windowId: evt.window.id,
101118
});
102119
} catch (error) {
103120
console.error("[Recall SDK] Error uploading recording:", error);
121+
122+
const session = activeSessions.get(evt.window.id);
123+
if (session && posthogClient) {
124+
try {
125+
await posthogClient.updateDesktopRecording(session.recordingId, {
126+
status: "error",
127+
});
128+
} catch (updateError) {
129+
console.error(
130+
"[Recall SDK] Failed to update error status:",
131+
updateError,
132+
);
133+
}
134+
}
104135
}
105136
});
106137

107138
RecallAiSdk.addEventListener("upload-progress", async (evt) => {
108139
if (evt.progress === 100) {
109140
console.log("[Recall SDK] Upload complete");
141+
142+
const session = activeSessions.get(evt.window.id);
143+
if (session && posthogClient) {
144+
try {
145+
await posthogClient.updateDesktopRecording(session.recordingId, {
146+
status: "processing",
147+
});
148+
console.log(
149+
`[Recall SDK] Updated recording ${session.recordingId} status to processing`,
150+
);
151+
} catch (error) {
152+
console.error(
153+
"[Recall SDK] Failed to update recording status:",
154+
error,
155+
);
156+
}
157+
}
110158
}
111159
});
112160

@@ -193,4 +241,25 @@ export function registerRecallIPCHandlers() {
193241
ipcMain.handle("recall:shutdown", async () => {
194242
shutdownRecallSDK();
195243
});
244+
245+
ipcMain.handle("notetaker:get-recordings", async () => {
246+
if (!posthogClient) {
247+
throw new Error("PostHog client not initialized");
248+
}
249+
return await posthogClient.listDesktopRecordings();
250+
});
251+
252+
ipcMain.handle("notetaker:get-recording", async (_event, recordingId) => {
253+
if (!posthogClient) {
254+
throw new Error("PostHog client not initialized");
255+
}
256+
return await posthogClient.getDesktopRecording(recordingId);
257+
});
258+
259+
ipcMain.handle("notetaker:delete-recording", async (_event, recordingId) => {
260+
if (!posthogClient) {
261+
throw new Error("PostHog client not initialized");
262+
}
263+
return await posthogClient.deleteDesktopRecording(recordingId);
264+
});
196265
}

src/renderer/components/MainLayout.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Box, Flex } from "@radix-ui/themes";
22
import type { Task } from "@shared/types";
33
import { useCallback, useEffect, useState } from "react";
44
import { useHotkeys } from "react-hotkeys-hook";
5+
import { NotetakerView } from "@/renderer/features/notetaker/components/NotetakerView";
56
import { RecordingsView } from "@/renderer/features/recordings";
67
import { useIntegrations } from "../hooks/useIntegrations";
78
import { useLayoutStore } from "../stores/layoutStore";
@@ -116,6 +117,8 @@ export function MainLayout() {
116117
{activeTab?.type === "settings" && <SettingsView />}
117118

118119
{activeTab?.type === "recordings" && <RecordingsView />}
120+
121+
{activeTab?.type === "notetaker" && <NotetakerView />}
119122
</Box>
120123
</Flex>
121124

src/renderer/components/ui/sidebar/SidebarContent.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ export const SidebarContent: React.FC = () => {
5050
}, [client]);
5151

5252
const handleNavigate = (
53-
type: "task-list" | "recordings" | "settings",
53+
type: "task-list" | "recordings" | "notetaker" | "settings",
5454
title: string,
5555
) => {
5656
const existingTab = tabs.find((tab) => tab.type === type);

src/renderer/components/ui/sidebar/UseSidebarMenuData.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
PlusIcon,
88
SquareIcon,
99
SquaresFourIcon,
10+
VideoIcon,
1011
WaveformIcon,
1112
XCircleIcon,
1213
} from "@phosphor-icons/react";
@@ -26,7 +27,7 @@ interface UseSidebarMenuDataProps {
2627
userName: string;
2728
activeTab: TabState | undefined;
2829
onNavigate: (
29-
type: "task-list" | "recordings" | "settings",
30+
type: "task-list" | "recordings" | "notetaker" | "settings",
3031
title: string,
3132
) => void;
3233
onTaskClick: (task: Task) => void;
@@ -99,6 +100,17 @@ export function useSidebarMenuData({
99100
hoverAction: onCreateTask,
100101
hoverIcon: <PlusIcon size={12} />,
101102
},
103+
{
104+
label: "Notetaker",
105+
icon: (
106+
<VideoIcon
107+
size={12}
108+
weight={activeTab?.type === "notetaker" ? "fill" : "regular"}
109+
/>
110+
),
111+
action: () => onNavigate("notetaker", "Notetaker"),
112+
isActive: activeTab?.type === "notetaker",
113+
},
102114
{
103115
label: isRecording
104116
? `Recordings ${formatDuration(recordingDuration)}`

0 commit comments

Comments
 (0)