diff --git a/apps/desktop/src/components/right-panel/views/transcript-view.tsx b/apps/desktop/src/components/right-panel/views/transcript-view.tsx index 101a52adce..93d43056d2 100644 --- a/apps/desktop/src/components/right-panel/views/transcript-view.tsx +++ b/apps/desktop/src/components/right-panel/views/transcript-view.tsx @@ -377,16 +377,19 @@ const MemoizedSpeakerSelector = memo(({ }; return ( -
+
- { - e.preventDefault(); - }} - > - + +
diff --git a/packages/tiptap/src/transcript/extensions.ts b/packages/tiptap/src/transcript/extensions.ts index 574447cd14..4d51eeb5c7 100644 --- a/packages/tiptap/src/transcript/extensions.ts +++ b/packages/tiptap/src/transcript/extensions.ts @@ -20,6 +20,7 @@ export const SpeakerSplit = Extension.create({ key: new PluginKey("hypr-speaker-split"), props: { handleKeyDown(view, event) { + // Handle Enter key for splitting speakers if (checkKey("Enter")(event)) { const { state, dispatch } = view; const { selection } = state; @@ -65,6 +66,84 @@ export const SpeakerSplit = Extension.create({ return true; } + // Handle Up/Down arrow keys for smart section navigation + if (checkKey("ArrowUp")(event) || checkKey("ArrowDown")(event)) { + const { state } = view; + const { selection } = state; + const { doc } = state; + const isUp = event.key === "ArrowUp"; + + // Find all speaker sections + const speakerSections: { pos: number; node: any; startPos: number; endPos: number }[] = []; + doc.descendants((node, pos) => { + if (node.type.name === SPEAKER_NODE_NAME) { + speakerSections.push({ + pos, + node, + startPos: pos + 1, // Content starts after the node opening + endPos: pos + node.nodeSize - 1, // Content ends before the node closing + }); + } + return false; // Don't descend into speaker nodes + }); + + if (speakerSections.length < 2) { + return false; // Need at least 2 sections to navigate + } + + // Find current speaker section + const currentPos = selection.from; + let currentSection: typeof speakerSections[0] | null = null; + let currentSectionIndex = -1; + + for (let i = 0; i < speakerSections.length; i++) { + const section = speakerSections[i]; + if (currentPos >= section.startPos && currentPos <= section.endPos) { + currentSection = section; + currentSectionIndex = i; + break; + } + } + + if (!currentSection || currentSectionIndex === -1) { + return false; // Not within a speaker section + } + + // Check if we're at the edge of the current section + const { $from } = selection; + const isAtEdge = isUp + ? currentPos === currentSection.startPos || $from.parentOffset === 0 + : currentPos === currentSection.endPos || $from.parentOffset === $from.parent.content.size; + + // Only navigate between sections if we're at the edge + if (!isAtEdge) { + return false; // Let default line navigation handle this + } + + // We're at the edge, so navigate to adjacent section + let targetSectionIndex: number; + if (isUp) { + targetSectionIndex = currentSectionIndex > 0 ? currentSectionIndex - 1 : speakerSections.length - 1; + } else { + targetSectionIndex = currentSectionIndex < speakerSections.length - 1 ? currentSectionIndex + 1 : 0; + } + + const targetSection = speakerSections[targetSectionIndex]; + let targetPos: number; + + if (isUp) { + // When going up, go to the end of the previous section + targetPos = targetSection.endPos; + } else { + // When going down, go to the beginning of the next section + targetPos = targetSection.startPos; + } + + const newSelection = TextSelection.create(doc, targetPos); + view.dispatch(state.tr.setSelection(newSelection).scrollIntoView()); + return true; + } + return false; }, }, @@ -77,5 +156,6 @@ const checkKey = (key: string) => (e: KeyboardEvent) => { return e.key === key && !e.ctrlKey && !e.metaKey - && !e.altKey; + && !e.altKey + && !e.shiftKey; // Also ignore shift key for cleaner navigation };