Skip to content

Commit ab840fc

Browse files
committed
feat: add AI task extraction for notetaker recordings
- Add task extraction IPC handler using OpenAI GPT-4o-mini - Create useExtractTasks hook with TanStack Query mutation - Add Extract Tasks button in LiveTranscriptView - Display extracted tasks with title and description - Show loading, error, and empty states - Require OpenAI API key in settings This allows users to extract actionable tasks from meeting transcripts with a single click, perfect for the demo today!
1 parent 7d86d35 commit ab840fc

File tree

5 files changed

+203
-1
lines changed

5 files changed

+203
-1
lines changed

src/main/preload.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,11 @@ contextBridge.exposeInMainWorld("electronAPI", {
202202
ipcRenderer.invoke("notetaker:get-recording", recordingId),
203203
notetakerDeleteRecording: (recordingId: string): Promise<void> =>
204204
ipcRenderer.invoke("notetaker:delete-recording", recordingId),
205+
notetakerExtractTasks: (
206+
transcriptText: string,
207+
openaiApiKey: string,
208+
): Promise<Array<{ title: string; description: string }>> =>
209+
ipcRenderer.invoke("notetaker:extract-tasks", transcriptText, openaiApiKey),
205210
// Real-time transcript listener
206211
onTranscriptSegment: (
207212
listener: (segment: {

src/main/services/recallRecording.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1+
import { createOpenAI } from "@ai-sdk/openai";
12
import { PostHogAPIClient } from "@api/posthogClient";
23
import RecallAiSdk from "@recallai/desktop-sdk";
4+
import { generateObject } from "ai";
35
import { ipcMain } from "electron";
6+
import { z } from "zod";
7+
import { TASK_EXTRACTION_PROMPT } from "./transcription-prompts";
48

59
interface RecordingSession {
610
windowId: string;
@@ -428,6 +432,45 @@ export function getActiveSessions() {
428432
return Array.from(activeSessions.values());
429433
}
430434

435+
async function extractTasksFromTranscript(
436+
transcriptText: string,
437+
openaiApiKey: string,
438+
): Promise<Array<{ title: string; description: string }>> {
439+
try {
440+
const openai = createOpenAI({ apiKey: openaiApiKey });
441+
442+
const schema = z.object({
443+
tasks: z.array(
444+
z.object({
445+
title: z.string().describe("Brief task title"),
446+
description: z.string().describe("Detailed description with context"),
447+
}),
448+
),
449+
});
450+
451+
const { object } = await generateObject({
452+
model: openai("gpt-4o-mini"),
453+
schema,
454+
messages: [
455+
{
456+
role: "system",
457+
content:
458+
"You are a helpful assistant that extracts actionable tasks from conversation transcripts. Be generous in identifying work items - include feature requests, requirements, and any work that needs to be done.",
459+
},
460+
{
461+
role: "user",
462+
content: `${TASK_EXTRACTION_PROMPT}\n${transcriptText}`,
463+
},
464+
],
465+
});
466+
467+
return object.tasks || [];
468+
} catch (error) {
469+
console.error("[Task Extraction] Error:", error);
470+
throw error;
471+
}
472+
}
473+
431474
export function registerRecallIPCHandlers() {
432475
ipcMain.handle(
433476
"recall:initialize",
@@ -483,4 +526,17 @@ export function registerRecallIPCHandlers() {
483526
}
484527
return await posthogClient.deleteDesktopRecording(recordingId);
485528
});
529+
530+
ipcMain.handle(
531+
"notetaker:extract-tasks",
532+
async (_event, transcriptText, openaiApiKey) => {
533+
console.log("[Task Extraction] Starting task extraction...");
534+
const tasks = await extractTasksFromTranscript(
535+
transcriptText,
536+
openaiApiKey,
537+
);
538+
console.log(`[Task Extraction] Extracted ${tasks.length} tasks`);
539+
return tasks;
540+
},
541+
);
486542
}

src/renderer/features/notetaker/components/LiveTranscriptView.tsx

Lines changed: 96 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,18 @@
1-
import { Badge, Box, Card, Flex, ScrollArea, Text } from "@radix-ui/themes";
1+
import { ListChecksIcon, SparkleIcon } from "@phosphor-icons/react";
2+
import {
3+
Badge,
4+
Box,
5+
Button,
6+
Card,
7+
Flex,
8+
Heading,
9+
ScrollArea,
10+
Separator,
11+
Text,
12+
} from "@radix-ui/themes";
213
import { useEffect, useRef, useState } from "react";
14+
import { useAuthStore } from "../../../stores/authStore";
15+
import { useExtractTasks } from "../hooks/useExtractTasks";
316
import { useLiveTranscript } from "../hooks/useTranscript";
417

518
interface LiveTranscriptViewProps {
@@ -11,10 +24,21 @@ export function LiveTranscriptView({
1124
}: LiveTranscriptViewProps) {
1225
const scrollRef = useRef<HTMLDivElement>(null);
1326
const [autoScroll, setAutoScroll] = useState(true);
27+
const openaiApiKey = useAuthStore((state) => state.openaiApiKey);
1428

1529
const { segments, addSegment, forceUpload, pendingCount } =
1630
useLiveTranscript(posthogRecordingId);
1731

32+
const extractTasksMutation = useExtractTasks();
33+
34+
const handleExtractTasks = () => {
35+
const fullText = segments.map((s) => s.text).join(" ");
36+
extractTasksMutation.mutate({
37+
recordingId: posthogRecordingId,
38+
transcriptText: fullText,
39+
});
40+
};
41+
1842
// Auto-scroll to bottom when new segments arrive
1943
useEffect(() => {
2044
if (autoScroll && scrollRef.current && segments.length > 0) {
@@ -177,6 +201,77 @@ export function LiveTranscriptView({
177201
</Text>
178202
</Box>
179203
)}
204+
205+
{/* Extract Tasks Section */}
206+
{segments.length > 0 && (
207+
<>
208+
<Separator size="4" my="4" />
209+
<Flex direction="column" gap="3">
210+
<Flex justify="between" align="center">
211+
<Heading size="4">
212+
<Flex align="center" gap="2">
213+
<ListChecksIcon size={20} />
214+
Extracted tasks
215+
</Flex>
216+
</Heading>
217+
<Button
218+
size="2"
219+
onClick={handleExtractTasks}
220+
disabled={extractTasksMutation.isPending || !openaiApiKey}
221+
>
222+
<SparkleIcon />
223+
{extractTasksMutation.isPending
224+
? "Extracting..."
225+
: "Extract tasks"}
226+
</Button>
227+
</Flex>
228+
229+
{!openaiApiKey && (
230+
<Card>
231+
<Text size="2" color="gray">
232+
Add OpenAI API key in settings to extract tasks
233+
</Text>
234+
</Card>
235+
)}
236+
237+
{extractTasksMutation.isSuccess && extractTasksMutation.data && (
238+
<Flex direction="column" gap="2">
239+
{extractTasksMutation.data.tasks.length === 0 ? (
240+
<Card>
241+
<Text size="2" color="gray">
242+
No tasks found in transcript
243+
</Text>
244+
</Card>
245+
) : (
246+
extractTasksMutation.data.tasks.map((task, idx) => (
247+
<Card key={`${task.title}-${idx}`}>
248+
<Flex direction="column" gap="1">
249+
<Text size="2" weight="medium">
250+
{task.title}
251+
</Text>
252+
<Text size="1" color="gray">
253+
{task.description}
254+
</Text>
255+
</Flex>
256+
</Card>
257+
))
258+
)}
259+
</Flex>
260+
)}
261+
262+
{extractTasksMutation.isError && (
263+
<Card>
264+
<Text size="2" color="red">
265+
Failed to extract tasks:{" "}
266+
{extractTasksMutation.error instanceof Error
267+
? extractTasksMutation.error.message
268+
: "Unknown error"}
269+
</Text>
270+
</Card>
271+
)}
272+
</Flex>
273+
</>
274+
)}
180275
</Box>
181276
);
182277
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { useMutation, useQueryClient } from "@tanstack/react-query";
2+
import { useAuthStore } from "../../../stores/authStore";
3+
4+
export interface ExtractedTask {
5+
title: string;
6+
description: string;
7+
}
8+
9+
export function useExtractTasks() {
10+
const openaiApiKey = useAuthStore((state) => state.openaiApiKey);
11+
const queryClient = useQueryClient();
12+
13+
return useMutation({
14+
mutationFn: async ({
15+
recordingId,
16+
transcriptText,
17+
}: {
18+
recordingId: string;
19+
transcriptText: string;
20+
}) => {
21+
if (!openaiApiKey) {
22+
throw new Error("OpenAI API key not configured");
23+
}
24+
25+
const tasks = await window.electronAPI.notetakerExtractTasks(
26+
transcriptText,
27+
openaiApiKey,
28+
);
29+
30+
return { tasks, recordingId };
31+
},
32+
onSuccess: (data) => {
33+
// Invalidate recording query to refetch with new tasks
34+
queryClient.invalidateQueries({
35+
queryKey: ["notetaker-recording", data.recordingId],
36+
});
37+
queryClient.invalidateQueries({
38+
queryKey: ["notetaker-transcript", data.recordingId],
39+
});
40+
},
41+
});
42+
}

src/renderer/types/electron.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,10 @@ export interface IElectronAPI {
148148
recall_recording_id: string | null;
149149
}>;
150150
notetakerDeleteRecording: (recordingId: string) => Promise<void>;
151+
notetakerExtractTasks: (
152+
transcriptText: string,
153+
openaiApiKey: string,
154+
) => Promise<Array<{ title: string; description: string }>>;
151155
// Real-time transcript listener
152156
onTranscriptSegment: (
153157
listener: (segment: {

0 commit comments

Comments
 (0)