mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-04-29 10:26:23 +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 { classifySchedule } from '@x/core/dist/knowledge/inline_tasks.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.
|
||||
|
|
@ -701,6 +702,10 @@ export function setupIpcHandlers() {
|
|||
|
||||
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) => {
|
||||
const schedule = await classifySchedule(args.instruction);
|
||||
return { schedule };
|
||||
|
|
|
|||
|
|
@ -3343,12 +3343,46 @@ function App() {
|
|||
navigateToFile(notePath)
|
||||
}, [loadDirectory, navigateToFile, fileTabs])
|
||||
|
||||
const meetingNotePathRef = useRef<string | null>(null)
|
||||
|
||||
const handleToggleMeeting = useCallback(async () => {
|
||||
if (meetingTranscription.state === 'recording') {
|
||||
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') {
|
||||
const notePath = await meetingTranscription.start()
|
||||
if (notePath) {
|
||||
meetingNotePathRef.current = notePath
|
||||
await handleVoiceNoteCreated(notePath)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,11 +48,11 @@ function formatTranscript(entries: TranscriptEntry[], date: string): string {
|
|||
'---',
|
||||
'type: meeting',
|
||||
'source: rowboat',
|
||||
'title: Meeting Transcription',
|
||||
'title: Meeting note',
|
||||
`date: "${date}"`,
|
||||
'---',
|
||||
'',
|
||||
'# Meeting Transcription',
|
||||
'# Meeting note',
|
||||
'',
|
||||
];
|
||||
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(),
|
||||
}).nullable(),
|
||||
},
|
||||
'meeting:summarize': {
|
||||
req: z.object({
|
||||
transcript: z.string(),
|
||||
}),
|
||||
res: z.object({
|
||||
notes: z.string(),
|
||||
}),
|
||||
},
|
||||
// Inline task schedule classification
|
||||
'export:note': {
|
||||
req: z.object({
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue