transcript block

This commit is contained in:
Arjun 2026-03-30 16:08:27 +05:30
parent 066ac81791
commit cf33e78243
5 changed files with 168 additions and 4 deletions

View file

@ -3476,7 +3476,8 @@ function App() {
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 newBody = `# ${noteTitle}\n\n` + cleanedNotes + '\n\n---\n\n## Raw transcript\n\n' + bodyWithoutTitle
const transcriptData = JSON.stringify({ transcript: bodyWithoutTitle.trim() })
const newBody = `# ${noteTitle}\n\n` + cleanedNotes + '\n\n```transcript\n' + transcriptData + '\n```'
const newContent = fm ? `${fm}\n${newBody}` : newBody
await window.ipc.invoke('workspace:writeFile', {
path: notePath,

View file

@ -15,6 +15,7 @@ import { ChartBlockExtension } from '@/extensions/chart-block'
import { TableBlockExtension } from '@/extensions/table-block'
import { CalendarBlockExtension } from '@/extensions/calendar-block'
import { EmailBlockExtension } from '@/extensions/email-block'
import { TranscriptBlockExtension } from '@/extensions/transcript-block'
import { Markdown } from 'tiptap-markdown'
import { useEffect, useCallback, useMemo, useRef, useState } from 'react'
import { Calendar, ChevronDown, ExternalLink } from 'lucide-react'
@ -155,6 +156,8 @@ function getMarkdownWithBlankLines(editor: Editor): string {
blocks.push('```calendar\n' + (node.attrs?.data as string || '{}') + '\n```')
} else if (node.type === 'emailBlock') {
blocks.push('```email\n' + (node.attrs?.data as string || '{}') + '\n```')
} else if (node.type === 'transcriptBlock') {
blocks.push('```transcript\n' + (node.attrs?.data as string || '{}') + '\n```')
} else if (node.type === 'codeBlock') {
const lang = (node.attrs?.language as string) || ''
blocks.push('```' + lang + '\n' + nodeToText(node) + '\n```')
@ -567,6 +570,7 @@ export function MarkdownEditor({
TableBlockExtension,
CalendarBlockExtension,
EmailBlockExtension,
TranscriptBlockExtension,
WikiLink.configure({
onCreate: wikiLinks?.onCreate
? (path) => {

View file

@ -0,0 +1,102 @@
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'
function TranscriptBlockView({ node }: {
node: { attrs: Record<string, unknown> }
}) {
const raw = node.attrs.data as string
let config: blocks.TranscriptBlock | null = null
try {
config = blocks.TranscriptBlockSchema.parse(JSON.parse(raw))
} catch {
// fallback below
}
const [expanded, setExpanded] = useState(false)
if (!config) {
return (
<NodeViewWrapper className="transcript-block-wrapper" data-type="transcript-block">
<div className="transcript-block-card transcript-block-error">
<FileText size={16} />
<span>Invalid transcript block</span>
</div>
</NodeViewWrapper>
)
}
return (
<NodeViewWrapper className="transcript-block-wrapper" data-type="transcript-block">
<div className="transcript-block-card" onMouseDown={(e) => e.stopPropagation()}>
<button
className="transcript-block-toggle"
onClick={(e) => { e.stopPropagation(); setExpanded(!expanded) }}
onMouseDown={(e) => e.stopPropagation()}
>
<ChevronDown size={14} className={`transcript-block-chevron ${expanded ? 'transcript-block-chevron-open' : ''}`} />
<FileText size={14} />
<span>Raw transcript</span>
</button>
{expanded && (
<div className="transcript-block-content">
{config.transcript}
</div>
)}
</div>
</NodeViewWrapper>
)
}
export const TranscriptBlockExtension = Node.create({
name: 'transcriptBlock',
group: 'block',
atom: true,
selectable: true,
draggable: false,
addAttributes() {
return {
data: { default: '{}' },
}
},
parseHTML() {
return [{
tag: 'pre',
priority: 60,
getAttrs(element) {
const code = element.querySelector('code')
if (!code) return false
const cls = code.className || ''
if (cls.includes('language-transcript')) {
return { data: code.textContent || '{}' }
}
return false
},
}]
},
renderHTML({ HTMLAttributes }: { HTMLAttributes: Record<string, unknown> }) {
return ['div', mergeAttributes(HTMLAttributes, { 'data-type': 'transcript-block' })]
},
addNodeView() {
return ReactNodeViewRenderer(TranscriptBlockView)
},
addStorage() {
return {
markdown: {
serialize(state: { write: (text: string) => void; closeBlock: (node: unknown) => void }, node: { attrs: { data: string } }) {
state.write('```transcript\n' + node.attrs.data + '\n```')
state.closeBlock(node)
},
parse: {},
},
}
},
})

View file

@ -618,7 +618,8 @@
.tiptap-editor .ProseMirror .chart-block-wrapper,
.tiptap-editor .ProseMirror .table-block-wrapper,
.tiptap-editor .ProseMirror .calendar-block-wrapper,
.tiptap-editor .ProseMirror .email-block-wrapper {
.tiptap-editor .ProseMirror .email-block-wrapper,
.tiptap-editor .ProseMirror .transcript-block-wrapper {
margin: 8px 0;
}
@ -628,7 +629,8 @@
.tiptap-editor .ProseMirror .table-block-card,
.tiptap-editor .ProseMirror .calendar-block-card,
.tiptap-editor .ProseMirror .email-block-card,
.tiptap-editor .ProseMirror .email-draft-block-card {
.tiptap-editor .ProseMirror .email-draft-block-card,
.tiptap-editor .ProseMirror .transcript-block-card {
position: relative;
padding: 12px 14px;
border: 1px solid var(--border);
@ -644,7 +646,8 @@
.tiptap-editor .ProseMirror .table-block-card:hover,
.tiptap-editor .ProseMirror .calendar-block-card:hover,
.tiptap-editor .ProseMirror .email-block-card:hover,
.tiptap-editor .ProseMirror .email-draft-block-card:hover {
.tiptap-editor .ProseMirror .email-draft-block-card:hover,
.tiptap-editor .ProseMirror .transcript-block-card:hover {
background-color: color-mix(in srgb, var(--muted) 70%, transparent);
}
@ -1488,6 +1491,54 @@
margin-left: 4px;
}
/* Transcript block */
.tiptap-editor .ProseMirror .transcript-block-toggle {
display: flex;
align-items: center;
gap: 6px;
width: 100%;
padding: 0;
font-size: 13px;
font-weight: 500;
color: color-mix(in srgb, var(--foreground) 60%, transparent);
background: none;
border: none;
cursor: pointer;
transition: color 0.12s ease;
}
.tiptap-editor .ProseMirror .transcript-block-toggle:hover {
color: var(--foreground);
}
.tiptap-editor .ProseMirror .transcript-block-chevron {
transition: transform 0.15s ease;
flex-shrink: 0;
}
.tiptap-editor .ProseMirror .transcript-block-chevron-open {
transform: rotate(180deg);
}
.tiptap-editor .ProseMirror .transcript-block-content {
margin-top: 10px;
padding-top: 10px;
border-top: 1px solid color-mix(in srgb, var(--foreground) 10%, transparent);
font-size: 13px;
line-height: 1.6;
color: color-mix(in srgb, var(--foreground) 70%, transparent);
white-space: pre-wrap;
word-break: break-word;
}
.tiptap-editor .ProseMirror .transcript-block-error {
display: flex;
align-items: center;
gap: 6px;
color: color-mix(in srgb, var(--foreground) 55%, transparent);
font-size: 13px;
}
/* Meeting event banner */
.meeting-event-banner {
position: relative;

View file

@ -74,3 +74,9 @@ export const EmailBlockSchema = z.object({
});
export type EmailBlock = z.infer<typeof EmailBlockSchema>;
export const TranscriptBlockSchema = z.object({
transcript: z.string(),
});
export type TranscriptBlock = z.infer<typeof TranscriptBlockSchema>;