running transcript in the transcription block

This commit is contained in:
Arjun 2026-03-30 16:43:43 +05:30
parent cf33e78243
commit 7a3185f201
4 changed files with 113 additions and 15 deletions

View file

@ -3467,17 +3467,15 @@ function App() {
const calendarEventJson = calEventMatch?.[1]?.replace(/''/g, "'")
const { notes } = await window.ipc.invoke('meeting:summarize', { transcript: fileContent, meetingStartTime, calendarEventJson })
if (notes) {
// Prepend meeting notes below the title but above the transcript
const { raw: fm, body: transcriptBody } = splitFrontmatter(fileContent)
// Use frontmatter title as the heading (set from calendar event summary)
// Prepend meeting notes above the existing transcript block
const { raw: fm, body } = splitFrontmatter(fileContent)
const fmTitleMatch = fileContent.match(/^title:\s*(.+)$/m)
const noteTitle = fmTitleMatch?.[1]?.trim() || 'Meeting note'
// Strip any existing top-level heading from body
const bodyWithoutTitle = transcriptBody.replace(/^#\s+.+\s*\n*/, '')
// Also strip any title/heading the LLM may have generated
const cleanedNotes = notes.replace(/^#{1,2}\s+.+\n+/, '')
const transcriptData = JSON.stringify({ transcript: bodyWithoutTitle.trim() })
const newBody = `# ${noteTitle}\n\n` + cleanedNotes + '\n\n```transcript\n' + transcriptData + '\n```'
// Extract the existing transcript block and preserve it as-is
const transcriptBlockMatch = body.match(/(```transcript\n[\s\S]*?\n```)/)
const transcriptBlock = transcriptBlockMatch?.[1] || ''
const newBody = `# ${noteTitle}\n\n` + cleanedNotes + (transcriptBlock ? '\n\n' + transcriptBlock : '')
const newContent = fm ? `${fm}\n${newBody}` : newBody
await window.ipc.invoke('workspace:writeFile', {
path: notePath,

View file

@ -2,10 +2,54 @@ import { mergeAttributes, Node } from '@tiptap/react'
import { ReactNodeViewRenderer, NodeViewWrapper } from '@tiptap/react'
import { ChevronDown, FileText } from 'lucide-react'
import { blocks } from '@x/shared'
import { useState } from 'react'
import { useState, useMemo } from 'react'
function TranscriptBlockView({ node }: {
interface TranscriptEntry {
speaker: string
text: string
}
function parseTranscript(raw: string): TranscriptEntry[] {
const entries: TranscriptEntry[] = []
const lines = raw.split('\n')
for (const line of lines) {
const trimmed = line.trim()
if (!trimmed) continue
// Match **Speaker Name:** text or **You:** text
const match = trimmed.match(/^\*\*(.+?):\*\*\s*(.*)$/)
if (match) {
entries.push({ speaker: match[1], text: match[2] })
} else if (entries.length > 0) {
// Continuation line — append to last entry
entries[entries.length - 1].text += ' ' + trimmed
}
}
return entries
}
function speakerColor(speaker: string): string {
// Simple hash to pick a consistent color per speaker
let hash = 0
for (let i = 0; i < speaker.length; i++) {
hash = speaker.charCodeAt(i) + ((hash << 5) - hash)
}
const colors = [
'#3b82f6', // blue
'#06b6d4', // cyan
'#6366f1', // indigo
'#8b5cf6', // purple
'#0ea5e9', // sky
'#2563eb', // blue darker
'#7c3aed', // violet
]
return colors[Math.abs(hash) % colors.length]
}
function TranscriptBlockView({ node, getPos, editor }: {
node: { attrs: Record<string, unknown> }
getPos: () => number | undefined
// eslint-disable-next-line @typescript-eslint/no-explicit-any
editor: any
}) {
const raw = node.attrs.data as string
let config: blocks.TranscriptBlock | null = null
@ -16,7 +60,27 @@ function TranscriptBlockView({ node }: {
// fallback below
}
const [expanded, setExpanded] = useState(false)
// Auto-detect: expand if this is the first real block (live recording),
// collapse if there's other content above (notes have been generated)
const isFirstBlock = useMemo(() => {
try {
const pos = getPos()
if (pos === undefined) return false
const firstChild = editor?.state?.doc?.firstChild
if (!firstChild) return true
// If the transcript block is right after the first node (heading), it's the main content
return pos <= (firstChild.nodeSize ?? 0) + 1
} catch {
return false
}
}, [getPos, editor])
const [expanded, setExpanded] = useState(isFirstBlock)
const entries = useMemo(() => {
if (!config) return []
return parseTranscript(config.transcript)
}, [config])
if (!config) {
return (
@ -43,7 +107,18 @@ function TranscriptBlockView({ node }: {
</button>
{expanded && (
<div className="transcript-block-content">
{config.transcript}
{entries.length > 0 ? (
entries.map((entry, i) => (
<div key={i} className="transcript-entry">
<span className="transcript-speaker" style={{ color: speakerColor(entry.speaker) }}>
{entry.speaker}
</span>
<span className="transcript-text">{entry.text}</span>
</div>
))
) : (
<div className="transcript-raw">{config.transcript}</div>
)}
</div>
)}
</div>

View file

@ -89,13 +89,18 @@ function formatTranscript(entries: TranscriptEntry[], date: string, calendarEven
`# ${noteTitle}`,
'',
);
// Build the raw transcript text
const transcriptLines: string[] = [];
for (let i = 0; i < entries.length; i++) {
if (i > 0 && entries[i].speaker !== entries[i - 1].speaker) {
lines.push('');
transcriptLines.push('');
}
lines.push(`**${entries[i].speaker}:** ${entries[i].text}`);
lines.push('');
transcriptLines.push(`**${entries[i].speaker}:** ${entries[i].text}`);
transcriptLines.push('');
}
const transcriptText = transcriptLines.join('\n').trim();
const transcriptData = JSON.stringify({ transcript: transcriptText });
lines.push('```transcript', transcriptData, '```');
return lines.join('\n');
}

View file

@ -1524,6 +1524,26 @@
margin-top: 10px;
padding-top: 10px;
border-top: 1px solid color-mix(in srgb, var(--foreground) 10%, transparent);
display: flex;
flex-direction: column;
gap: 6px;
}
.tiptap-editor .ProseMirror .transcript-entry {
font-size: 13px;
line-height: 1.5;
}
.tiptap-editor .ProseMirror .transcript-speaker {
font-weight: 600;
margin-right: 6px;
}
.tiptap-editor .ProseMirror .transcript-text {
color: color-mix(in srgb, var(--foreground) 75%, transparent);
}
.tiptap-editor .ProseMirror .transcript-raw {
font-size: 13px;
line-height: 1.6;
color: color-mix(in srgb, var(--foreground) 70%, transparent);