Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -158,14 +158,51 @@ export const ExtractionBlockExtension = Node.create<ExtractionBlockOptions>({
},

addKeyboardShortcuts() {
const isSelectionInExtractionBlock = () => {
const { $from } = this.editor.state.selection
const getSelectionInExtractionBlock = () => {
const { selection } = this.editor.state
if (!selection.empty) {
return null
}

const { $from } = selection
let blockDepth: number | null = null

for (let depth = $from.depth; depth > 0; depth--) {
if ($from.node(depth).type.name === this.name) {
return true
blockDepth = depth
break
}
}
return false

if (blockDepth === null) {
return null
}

return { $from, blockDepth }
}

const isSelectionInExtractionBlock = () => {
return getSelectionInExtractionBlock() !== null
}

const isSelectionAtExtractionBlockStart = () => {
const selectionInBlock = getSelectionInExtractionBlock()
if (!selectionInBlock) {
return false
}

const { $from, blockDepth } = selectionInBlock
if ($from.parentOffset !== 0) {
return false
}

for (let depth = $from.depth; depth > blockDepth; depth--) {
if ($from.index(depth) !== 0) {
return false
}
}

return true
}

const splitToParagraphInBlock = () => {
Expand All @@ -178,6 +215,14 @@ export const ExtractionBlockExtension = Node.create<ExtractionBlockOptions>({
}

return {
Backspace: () => {
const selectionInBlock = getSelectionInExtractionBlock()
if (!selectionInBlock || !isSelectionAtExtractionBlockStart()) {
return false
}

return true
},
Enter: () => splitToParagraphInBlock(),
'Shift-Enter': () => {
return splitToParagraphInBlock()
Expand Down
180 changes: 171 additions & 9 deletions web/frontend/src/features/workspace/components/extraction-block.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,75 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip
import { extractionChatScopePluginKey } from './extraction-context-plugin'
import { cn } from '@/helpers/utils'

const CONFLICT_GROUP_PREFIX = 'conflict-group--'
const CONFLICT_VARIANT_PREFIX = 'conflict-variant--'

const conflictVariantStore = new Map<string, string>()
const conflictVariantListeners = new Set<() => void>()

function notifyConflictVariantListeners() {
conflictVariantListeners.forEach((listener) => listener())
}

Comment thread
andreisakirkinepam marked this conversation as resolved.
Comment thread
andreisakirkinepam marked this conversation as resolved.
function getConflictClasses(value: unknown) {
return String(value || '')
.split(/\s+/)
.filter(Boolean)
}

function getConflictClassValue(classes: string[], prefix: string) {
const match = classes.find((className) => className.startsWith(prefix))
return match ? match.slice(prefix.length) : null
}

function formatVariantLabel(variantName: string) {
if (variantName === 'council') {
return 'council'
}

return variantName.replace(/[-_]+/g, ' ')
}

function getConflictGroupVariants(editor: NodeViewProps['editor'], conflictGroup: string) {
const variants = new Map<string, { label: string; blockId: string | null }>()

editor.state.doc.descendants((currentNode) => {
if (currentNode.type.name !== 'extractionBlock') {
return true
}

const classes = getConflictClasses(currentNode.attrs.htmlClass)
const currentGroup = getConflictClassValue(classes, CONFLICT_GROUP_PREFIX)
const variant = getConflictClassValue(classes, CONFLICT_VARIANT_PREFIX)

if (currentGroup !== conflictGroup || !variant) {
return true
}

variants.set(variant, {
label: formatVariantLabel(variant),
blockId: (currentNode.attrs.blockId as string | null) ?? null,
})

return true
})

return Array.from(variants.entries())
.map(([value, meta]) => ({ value, ...meta }))
.sort((left, right) => {
if (left.value === 'council') return -1
if (right.value === 'council') return 1
return left.label.localeCompare(right.label)
})
}
Comment thread
andreisakirkinepam marked this conversation as resolved.
Comment thread
andreisakirkinepam marked this conversation as resolved.

interface ExtractionBlockProps extends NodeViewProps {
onBlockSelect?: (blockId: string | null, pageNumber: number | null) => void
onBlockDelete?: (blockId: string, pageNumber: number | null) => void
}

export function ExtractionBlock({ node, editor, extension, deleteNode }: ExtractionBlockProps) {
const { blockId, title, type, page, isNew } = node.attrs
const { blockId, title, type, page, isNew, htmlClass } = node.attrs
const onBlockSelect = extension.options.onBlockSelect
const onBlockDelete = extension.options.onBlockDelete
const onToggleBlockContext = extension.options.onToggleBlockContext
Expand All @@ -31,6 +93,20 @@ export function ExtractionBlock({ node, editor, extension, deleteNode }: Extract
pageNumber !== null && (chatScopePluginState?.pageNumbersInChatScope ?? []).includes(pageNumber)
const isWholeDocumentInChatScope = chatScopePluginState?.isWholeDocumentInChatScope ?? false
const isContextInteractionDisabled = chatScopePluginState?.isInteractionDisabled ?? false
const htmlClasses = getConflictClasses(htmlClass)
Comment thread
andreisakirkinepam marked this conversation as resolved.
const hasConflictClass = htmlClasses.includes('conflict')
const conflictGroup = getConflictClassValue(htmlClasses, CONFLICT_GROUP_PREFIX)
const conflictVariant = getConflictClassValue(htmlClasses, CONFLICT_VARIANT_PREFIX)
const [activeConflictVariant, setActiveConflictVariant] = useState(() => {
if (!conflictGroup) {
return null
}

return conflictVariantStore.get(conflictGroup) ?? 'council'
Comment thread
andreisakirkinepam marked this conversation as resolved.
})
const conflictVariants = conflictGroup ? getConflictGroupVariants(editor, conflictGroup) : []
Comment thread
andreisakirkinepam marked this conversation as resolved.
const isConflictVariantVisible =
!conflictGroup || !conflictVariant || activeConflictVariant === conflictVariant
const blockContextTitle = isContextInteractionDisabled
Comment thread
andreisakirkinepam marked this conversation as resolved.
? 'Prompt context is unavailable while you have unsaved changes.'
: isWholeDocumentInChatScope
Expand All @@ -57,6 +133,43 @@ export function ExtractionBlock({ node, editor, extension, deleteNode }: Extract
}
}, [editor])

useEffect(() => {
if (!conflictGroup) {
return
}

const initialVariant = conflictVariantStore.get(conflictGroup) ?? 'council'
conflictVariantStore.set(conflictGroup, initialVariant)
setActiveConflictVariant(initialVariant)

const syncVariant = () => {
setActiveConflictVariant(conflictVariantStore.get(conflictGroup) ?? 'council')
}
Comment thread
andreisakirkinepam marked this conversation as resolved.

conflictVariantListeners.add(syncVariant)

return () => {
conflictVariantListeners.delete(syncVariant)
}
}, [conflictGroup])

useEffect(() => {
if (!hasConflictClass || !blockId) {
return
}

const blockElement = editor.view.dom.querySelector(`[data-block-id="${blockId}"]`)
if (!(blockElement instanceof HTMLElement)) {
return
}

blockElement
.querySelectorAll<HTMLElement>('.conflict-comment, .conflict-comment *')
.forEach((element) => {
element.setAttribute('contenteditable', 'false')
})
}, [editor, blockId, hasConflictClass, activeConflictVariant])

const handleToggleBlockScope = (event: MouseEvent<HTMLButtonElement>) => {
event.preventDefault()
event.stopPropagation()
Expand Down Expand Up @@ -110,16 +223,35 @@ export function ExtractionBlock({ node, editor, extension, deleteNode }: Extract
onBlockDelete?.(blockId, pageNumber)
}

const handleConflictTabClick = (variant: string) => (event: MouseEvent<HTMLButtonElement>) => {
event.preventDefault()
event.stopPropagation()

if (!conflictGroup) {
return
}

conflictVariantStore.set(conflictGroup, variant)
notifyConflictVariantListeners()
}

return (
<NodeViewWrapper
data-block-id={blockId}
data-block-title={title}
className={cn('border-b transition-colors duration-150 ease-out focus-within:bg-primary/5', {
'border-l-2 border-l-amber-400 bg-amber-50/40': isNew,
})}
className={cn(
'border-b transition-colors duration-150 ease-out focus-within:bg-primary/5',
htmlClasses,
{
'border-l-2 border-l-amber-400 bg-amber-50/40': isNew,
'border-l-2 border-l-amber-500 bg-amber-50/70 ring-1 ring-inset ring-amber-200':
hasConflictClass,
hidden: !isConflictVariantVisible,
}
)}
Comment thread
andreisakirkinepam marked this conversation as resolved.
Comment thread
andreisakirkinepam marked this conversation as resolved.
onMouseDown={handleWrapperMouseDown}
>
{(type || page || isNew) && (
{(type || page || isNew || conflictVariants.length > 0) && (
<div
className="flex gap-2 p-4 pb-2 items-center [&>span:not(:last-of-type)]:after:content-['·'] [&>span:not(:last-child)]:after:ml-2 cursor-pointer"
contentEditable={false}
Expand Down Expand Up @@ -170,11 +302,41 @@ export function ExtractionBlock({ node, editor, extension, deleteNode }: Extract
</div>
)}

{conflictVariants.length > 0 && (
<div className="border-b border-border/80 px-4" contentEditable={false}>
<div className="flex flex-wrap gap-2">
{conflictVariants.map((variant) => {
const isActive = activeConflictVariant === variant.value

return (
<button
key={variant.value}
type="button"
onClick={handleConflictTabClick(variant.value)}
className={cn(
'border-b-2 px-1 py-2 text-sm capitalize transition-colors cursor-pointer',
isActive
? 'border-foreground text-foreground font-medium'
: 'border-transparent text-muted-foreground hover:text-foreground'
)}
>
{variant.label}
</button>
)
})}
</div>
</div>
Comment thread
andreisakirkinepam marked this conversation as resolved.
)}

<NodeViewContent
className={cn('px-4 pb-4 min-h-[2.5rem]', {
'min-h-[3rem] rounded-md border border-dashed border-amber-300 bg-white mx-4 mb-4 p-2 focus-within:border-amber-500 focus-within:ring-1 focus-within:ring-amber-500/30':
isNew,
})}
className={cn(
'px-4 pb-4 pt-4 min-h-[2.5rem] [&_.conflict-target]:rounded-sm [&_.conflict-target]:bg-amber-200/80 [&_.conflict-target]:px-1 [&_.conflict-comment]:mt-4 [&_.conflict-comment]:rounded-2xl [&_.conflict-comment]:border [&_.conflict-comment]:border-slate-200 [&_.conflict-comment]:bg-slate-50 [&_.conflict-comment]:px-4 [&_.conflict-comment]:py-3 [&_.conflict-comment]:text-sm [&_.conflict-comment]:text-slate-600',
{
'min-h-[3rem] rounded-md border border-dashed border-amber-300 bg-white mx-4 mb-4 p-2 focus-within:border-amber-500 focus-within:ring-1 focus-within:ring-amber-500/30':
isNew,
'mx-4 mb-4 rounded-md border border-slate-200 bg-white shadow-sm': hasConflictClass,
}
)}
/>
</NodeViewWrapper>
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import StarterKit from '@tiptap/starter-kit'
import { mergeAttributes, Node } from '@tiptap/core'
import Document from '@tiptap/extension-document'
import Paragraph from '@tiptap/extension-paragraph'
import { Table, TableCell, TableHeader, TableRow } from '@tiptap/extension-table'
Expand Down Expand Up @@ -28,6 +29,55 @@ const OcrParagraph = Paragraph.extend({
},
})

const OcrSpan = Node.create({
name: 'ocrSpan',

group: 'inline',

inline: true,

content: 'inline*',

selectable: false,

addAttributes() {
return {
htmlClass: {
default: null,
parseHTML: (element) => element.getAttribute('class'),
renderHTML: (attributes) => {
if (!attributes.htmlClass) return {}
return { class: attributes.htmlClass }
},
},
htmlId: {
default: null,
parseHTML: (element) => element.getAttribute('id'),
renderHTML: (attributes) => {
if (!attributes.htmlId) return {}
return { id: attributes.htmlId }
},
},
title: {
default: null,
parseHTML: (element) => element.getAttribute('title'),
renderHTML: (attributes) => {
if (!attributes.title) return {}
return { title: attributes.title }
Comment thread
andreisakirkinepam marked this conversation as resolved.
},
},
}
},

parseHTML() {
return [{ tag: 'span' }]
Comment thread
andreisakirkinepam marked this conversation as resolved.
},

renderHTML({ HTMLAttributes }) {
return ['span', mergeAttributes(HTMLAttributes), 0]
},
})

function parseCellAttrs(node: HTMLElement) {
const colspan = Number(node.getAttribute('colspan') || 1)
const rowspan = Number(node.getAttribute('rowspan') || 1)
Expand Down Expand Up @@ -185,6 +235,7 @@ export function createExtractionTableExtensions() {
ListItem.configure(ListKitStylingConfig.listItem),
ExtractionDocument,
OcrParagraph,
OcrSpan,
Table.configure(TableKitStylingConfig.table),
TableRow.configure(TableKitStylingConfig.tableRow),
OcrTableHeader.configure(TableKitStylingConfig.tableHeader),
Expand Down
Loading