mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-05-12 16:52:43 +02:00
meeting summary notes
This commit is contained in:
parent
537ca08fe5
commit
49c004b53e
5 changed files with 80 additions and 2 deletions
|
|
@ -41,6 +41,7 @@ import { search } from '@x/core/dist/search/search.js';
|
||||||
import { versionHistory, voice } from '@x/core';
|
import { versionHistory, voice } from '@x/core';
|
||||||
import { classifySchedule } from '@x/core/dist/knowledge/inline_tasks.js';
|
import { classifySchedule } from '@x/core/dist/knowledge/inline_tasks.js';
|
||||||
import { getBillingInfo } from '@x/core/dist/billing/billing.js';
|
import { getBillingInfo } from '@x/core/dist/billing/billing.js';
|
||||||
|
import { summarizeMeeting } from '@x/core/dist/knowledge/summarize_meeting.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert markdown to a styled HTML document for PDF/DOCX export.
|
* Convert markdown to a styled HTML document for PDF/DOCX export.
|
||||||
|
|
@ -701,6 +702,10 @@ export function setupIpcHandlers() {
|
||||||
|
|
||||||
return { success: false, error: 'Unknown format' };
|
return { success: false, error: 'Unknown format' };
|
||||||
},
|
},
|
||||||
|
'meeting:summarize': async (_event, args) => {
|
||||||
|
const notes = await summarizeMeeting(args.transcript);
|
||||||
|
return { notes };
|
||||||
|
},
|
||||||
'inline-task:classifySchedule': async (_event, args) => {
|
'inline-task:classifySchedule': async (_event, args) => {
|
||||||
const schedule = await classifySchedule(args.instruction);
|
const schedule = await classifySchedule(args.instruction);
|
||||||
return { schedule };
|
return { schedule };
|
||||||
|
|
|
||||||
|
|
@ -3343,12 +3343,46 @@ function App() {
|
||||||
navigateToFile(notePath)
|
navigateToFile(notePath)
|
||||||
}, [loadDirectory, navigateToFile, fileTabs])
|
}, [loadDirectory, navigateToFile, fileTabs])
|
||||||
|
|
||||||
|
const meetingNotePathRef = useRef<string | null>(null)
|
||||||
|
|
||||||
const handleToggleMeeting = useCallback(async () => {
|
const handleToggleMeeting = useCallback(async () => {
|
||||||
if (meetingTranscription.state === 'recording') {
|
if (meetingTranscription.state === 'recording') {
|
||||||
await meetingTranscription.stop()
|
await meetingTranscription.stop()
|
||||||
|
|
||||||
|
// Read the final transcript and generate meeting notes via LLM
|
||||||
|
const notePath = meetingNotePathRef.current
|
||||||
|
if (notePath) {
|
||||||
|
try {
|
||||||
|
const result = await window.ipc.invoke('workspace:readFile', { path: notePath, encoding: 'utf8' })
|
||||||
|
const fileContent = result.data
|
||||||
|
if (fileContent && fileContent.trim()) {
|
||||||
|
// Call LLM to summarize the transcript
|
||||||
|
const { notes } = await window.ipc.invoke('meeting:summarize', { transcript: fileContent })
|
||||||
|
if (notes) {
|
||||||
|
// Prepend meeting notes below the title but above the transcript
|
||||||
|
const { raw: fm, body: transcriptBody } = splitFrontmatter(fileContent)
|
||||||
|
// Strip the "# Meeting note" title from transcript body — we'll put it first
|
||||||
|
const bodyWithoutTitle = transcriptBody.replace(/^#\s+Meeting note\s*\n*/, '')
|
||||||
|
const newBody = '# Meeting note\n\n' + notes + '\n\n---\n\n## Raw transcript\n\n' + bodyWithoutTitle
|
||||||
|
const newContent = fm ? `${fm}\n${newBody}` : newBody
|
||||||
|
await window.ipc.invoke('workspace:writeFile', {
|
||||||
|
path: notePath,
|
||||||
|
data: newContent,
|
||||||
|
opts: { encoding: 'utf8' },
|
||||||
|
})
|
||||||
|
// Refresh the file view
|
||||||
|
await handleVoiceNoteCreated(notePath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[meeting] Failed to generate meeting notes:', err)
|
||||||
|
}
|
||||||
|
meetingNotePathRef.current = null
|
||||||
|
}
|
||||||
} else if (meetingTranscription.state === 'idle') {
|
} else if (meetingTranscription.state === 'idle') {
|
||||||
const notePath = await meetingTranscription.start()
|
const notePath = await meetingTranscription.start()
|
||||||
if (notePath) {
|
if (notePath) {
|
||||||
|
meetingNotePathRef.current = notePath
|
||||||
await handleVoiceNoteCreated(notePath)
|
await handleVoiceNoteCreated(notePath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -48,11 +48,11 @@ function formatTranscript(entries: TranscriptEntry[], date: string): string {
|
||||||
'---',
|
'---',
|
||||||
'type: meeting',
|
'type: meeting',
|
||||||
'source: rowboat',
|
'source: rowboat',
|
||||||
'title: Meeting Transcription',
|
'title: Meeting note',
|
||||||
`date: "${date}"`,
|
`date: "${date}"`,
|
||||||
'---',
|
'---',
|
||||||
'',
|
'',
|
||||||
'# Meeting Transcription',
|
'# Meeting note',
|
||||||
'',
|
'',
|
||||||
];
|
];
|
||||||
for (let i = 0; i < entries.length; i++) {
|
for (let i = 0; i < entries.length; i++) {
|
||||||
|
|
|
||||||
31
apps/x/packages/core/src/knowledge/summarize_meeting.ts
Normal file
31
apps/x/packages/core/src/knowledge/summarize_meeting.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { generateText } from 'ai';
|
||||||
|
import container from '../di/container.js';
|
||||||
|
import type { IModelConfigRepo } from '../models/repo.js';
|
||||||
|
import { createProvider } from '../models/models.js';
|
||||||
|
|
||||||
|
const SYSTEM_PROMPT = `You are a meeting notes assistant. Given a raw meeting transcript, create concise, well-organized meeting notes.
|
||||||
|
|
||||||
|
Format rules:
|
||||||
|
- Use ### for section headers that group related discussion topics
|
||||||
|
- Section headers should be in sentence case (e.g. "### Onboarding flow status"), NOT Title Case
|
||||||
|
- Use bullet points with sub-bullets for details
|
||||||
|
- Include a "### Action items" section at the end if any were discussed
|
||||||
|
- Focus on decisions, key discussions, and takeaways — not verbatim quotes
|
||||||
|
- Attribute statements to speakers when relevant (use their names/labels from the transcript)
|
||||||
|
- Keep it concise — the notes should be much shorter than the transcript
|
||||||
|
- Output markdown only, no preamble or explanation`;
|
||||||
|
|
||||||
|
export async function summarizeMeeting(transcript: string): Promise<string> {
|
||||||
|
const repo = container.resolve<IModelConfigRepo>('modelConfigRepo');
|
||||||
|
const config = await repo.getConfig();
|
||||||
|
const provider = createProvider(config.provider);
|
||||||
|
const model = provider.languageModel(config.model);
|
||||||
|
|
||||||
|
const result = await generateText({
|
||||||
|
model,
|
||||||
|
system: SYSTEM_PROMPT,
|
||||||
|
prompt: transcript,
|
||||||
|
});
|
||||||
|
|
||||||
|
return result.text.trim();
|
||||||
|
}
|
||||||
|
|
@ -498,6 +498,14 @@ const ipcSchemas = {
|
||||||
token: z.string(),
|
token: z.string(),
|
||||||
}).nullable(),
|
}).nullable(),
|
||||||
},
|
},
|
||||||
|
'meeting:summarize': {
|
||||||
|
req: z.object({
|
||||||
|
transcript: z.string(),
|
||||||
|
}),
|
||||||
|
res: z.object({
|
||||||
|
notes: z.string(),
|
||||||
|
}),
|
||||||
|
},
|
||||||
// Inline task schedule classification
|
// Inline task schedule classification
|
||||||
'export:note': {
|
'export:note': {
|
||||||
req: z.object({
|
req: z.object({
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue