diff --git a/packages/apollo-react/src/material/components/ap-chat/components/input/tiptap/resource-chip-node-view.tsx b/packages/apollo-react/src/material/components/ap-chat/components/input/tiptap/resource-chip-node-view.tsx index 81f2db19a..115a9edb2 100644 --- a/packages/apollo-react/src/material/components/ap-chat/components/input/tiptap/resource-chip-node-view.tsx +++ b/packages/apollo-react/src/material/components/ap-chat/components/input/tiptap/resource-chip-node-view.tsx @@ -151,7 +151,9 @@ export const ResourceChipNodeView: React.FC = React.memo(({ node, display: 'inline-flex', alignItems: 'center', height: lineHeight, + lineHeight, verticalAlign: 'top', + marginTop: token.Padding.PadXs, }} > diff --git a/packages/apollo-react/src/material/components/ap-chat/components/input/tiptap/tiptap-editor.styles.ts b/packages/apollo-react/src/material/components/ap-chat/components/input/tiptap/tiptap-editor.styles.ts index 1d0f5717a..0e555f117 100644 --- a/packages/apollo-react/src/material/components/ap-chat/components/input/tiptap/tiptap-editor.styles.ts +++ b/packages/apollo-react/src/material/components/ap-chat/components/input/tiptap/tiptap-editor.styles.ts @@ -2,35 +2,40 @@ import { styled } from '@mui/material/styles'; import token from '@uipath/apollo-core'; const EDITOR_PADDING = token.Spacing.SpacingXs; +const LINE_GAP = token.Spacing.SpacingMicro; export const EditorContainer = styled('div')<{ minRows: number; maxRows: number; lineHeight: string; -}>(({ minRows, maxRows, lineHeight }) => ({ - width: '100%', - minHeight: `calc(${minRows} * ${lineHeight} + ${EDITOR_PADDING})`, - maxHeight: `calc(${maxRows} * ${lineHeight} + ${EDITOR_PADDING})`, - overflowY: 'auto', - cursor: 'text', +}>(({ minRows, maxRows, lineHeight }) => { + const rowHeight = `calc(${lineHeight} + ${LINE_GAP})`; - '& .tiptap': { - outline: 'none', - minHeight: `calc(${minRows} * ${lineHeight} + ${EDITOR_PADDING})`, - paddingBottom: EDITOR_PADDING, + return { + width: '100%', + minHeight: `calc(${minRows} * ${rowHeight} + ${EDITOR_PADDING})`, + maxHeight: `calc(${maxRows} * ${rowHeight} + ${EDITOR_PADDING})`, + overflowY: 'auto', + cursor: 'text', - '& p': { - margin: 0, - minHeight: lineHeight, - lineHeight: lineHeight, - }, + '& .tiptap': { + outline: 'none', + minHeight: `calc(${minRows} * ${rowHeight} + ${EDITOR_PADDING})`, + paddingBottom: EDITOR_PADDING, + + '& p': { + margin: 0, + minHeight: rowHeight, + lineHeight: rowHeight, + }, - '& p.is-editor-empty:first-of-type::before': { - content: 'attr(data-placeholder)', - float: 'left', - color: 'var(--color-foreground-de-emp)', - pointerEvents: 'none', - height: 0, + '& p.is-editor-empty:first-of-type::before': { + content: 'attr(data-placeholder)', + float: 'left', + color: 'var(--color-foreground-de-emp)', + pointerEvents: 'none', + height: 0, + }, }, - }, -})); + }; +}); diff --git a/packages/apollo-react/src/material/components/ap-chat/components/input/tiptap/tiptap-resource-suggestion.tsx b/packages/apollo-react/src/material/components/ap-chat/components/input/tiptap/tiptap-resource-suggestion.tsx index 848169658..2a754fe5a 100644 --- a/packages/apollo-react/src/material/components/ap-chat/components/input/tiptap/tiptap-resource-suggestion.tsx +++ b/packages/apollo-react/src/material/components/ap-chat/components/input/tiptap/tiptap-resource-suggestion.tsx @@ -2,7 +2,6 @@ import type { Editor, Range } from '@tiptap/core'; import type { MentionOptions } from '@tiptap/extension-mention'; import { PluginKey } from '@tiptap/pm/state'; import { CHAT_RESOURCE_MENTION_TERMINATOR } from '../../../service'; -import { getFullMentionQuery } from './tiptap.utils'; export const ResourceMentionPluginKey = new PluginKey('resourceMention'); @@ -67,14 +66,12 @@ export function createResourceSuggestion( render: () => ({ onStart: (props) => { const coords = getCursorCoordinates(props.editor, props.range.from); - const { query, fullRange } = getFullMentionQuery(props.editor, props.range); - callbacks.onStart?.(fullRange, coords); - callbacks.onQueryChange?.(query, fullRange); + callbacks.onStart?.(props.range, coords); + callbacks.onQueryChange?.(props.query, props.range); }, onUpdate: (props) => { - const { query, fullRange } = getFullMentionQuery(props.editor, props.range); - callbacks.onQueryChange?.(query, fullRange); + callbacks.onQueryChange?.(props.query, props.range); }, onExit: () => { diff --git a/packages/apollo-react/src/material/components/ap-chat/components/input/tiptap/tiptap.utils.test.ts b/packages/apollo-react/src/material/components/ap-chat/components/input/tiptap/tiptap.utils.test.ts index d9e4baa20..36e7df226 100644 --- a/packages/apollo-react/src/material/components/ap-chat/components/input/tiptap/tiptap.utils.test.ts +++ b/packages/apollo-react/src/material/components/ap-chat/components/input/tiptap/tiptap.utils.test.ts @@ -1,59 +1,36 @@ -import type { Editor, Range } from '@tiptap/core'; import { describe, expect, it } from 'vitest'; -import { getFullMentionQuery } from './tiptap.utils'; +import { textToDocument } from './tiptap.utils'; -const DOUBLE_SPACE = ' '; - -function makeMockEditor(textAfterAt: string, parentEnd: number): Editor { - return { - state: { - doc: { - resolve: () => ({ end: () => parentEnd }), - textBetween: () => textAfterAt, - }, - }, - } as unknown as Editor; -} - -describe('getFullMentionQuery', () => { - it('returns the full mention text regardless of cursor position', () => { - const editor = makeMockEditor('queue name', 20); - const range: Range = { from: 5, to: 9 }; - - const result = getFullMentionQuery(editor, range); - - expect(result.query).toBe('queue name'); - expect(result.fullRange).toEqual({ from: 5, to: 16 }); +describe('textToDocument', () => { + it('returns empty content for empty string', () => { + expect(textToDocument('')).toEqual({ type: 'doc', content: [] }); }); - it('splits on the terminator and returns only the part before it', () => { - const editor = makeMockEditor(`my queue${DOUBLE_SPACE}extra text`, 30); - const range: Range = { from: 0, to: 5 }; - - const result = getFullMentionQuery(editor, range); - - expect(result.query).toBe('my queue'); - expect(result.fullRange).toEqual({ from: 0, to: 9 }); + it('creates a single paragraph for a single line', () => { + expect(textToDocument('hello')).toEqual({ + type: 'doc', + content: [{ type: 'paragraph', content: [{ type: 'text', text: 'hello' }] }], + }); }); - it('returns empty query when text after @ starts with a space', () => { - const editor = makeMockEditor(' current', 20); - const range: Range = { from: 5, to: 6 }; - - const result = getFullMentionQuery(editor, range); - - expect(result.query).toBe(''); - expect(result.fullRange).toBe(range); + it('creates multiple paragraphs for newlines', () => { + expect(textToDocument('line1\nline2')).toEqual({ + type: 'doc', + content: [ + { type: 'paragraph', content: [{ type: 'text', text: 'line1' }] }, + { type: 'paragraph', content: [{ type: 'text', text: 'line2' }] }, + ], + }); }); - it('returns empty query when cursor is at paragraph boundary', () => { - const parentEnd = 10; - const editor = makeMockEditor('', parentEnd); - const range: Range = { from: parentEnd - 1, to: parentEnd }; - - const result = getFullMentionQuery(editor, range); - - expect(result.query).toBe(''); - expect(result.fullRange).toBe(range); + it('creates empty paragraph for blank lines', () => { + expect(textToDocument('a\n\nb')).toEqual({ + type: 'doc', + content: [ + { type: 'paragraph', content: [{ type: 'text', text: 'a' }] }, + { type: 'paragraph' }, + { type: 'paragraph', content: [{ type: 'text', text: 'b' }] }, + ], + }); }); }); diff --git a/packages/apollo-react/src/material/components/ap-chat/components/input/tiptap/tiptap.utils.ts b/packages/apollo-react/src/material/components/ap-chat/components/input/tiptap/tiptap.utils.ts index e3436b6fb..d27262658 100644 --- a/packages/apollo-react/src/material/components/ap-chat/components/input/tiptap/tiptap.utils.ts +++ b/packages/apollo-react/src/material/components/ap-chat/components/input/tiptap/tiptap.utils.ts @@ -1,34 +1,3 @@ -import type { Editor, Range } from '@tiptap/core'; -import { CHAT_RESOURCE_MENTION_TERMINATOR } from '../../../service'; - -/** - * TipTap's Mention extension computes the query as text between @ and the cursor position. - * When the cursor moves backward within mention text (e.g. @te|xt), the query gets truncated. - * This helper reads the full text after @ to the end of the paragraph (or double space) - * so the search always uses the complete mention text regardless of cursor position. - */ -export function getFullMentionQuery( - editor: Editor, - range: Range -): { query: string; fullRange: Range } { - const textFrom = range.from + 1; - const parentEnd = editor.state.doc.resolve(textFrom).end(); - - if (textFrom >= parentEnd) { - return { query: '', fullRange: range }; - } - - const fullText = editor.state.doc.textBetween(textFrom, parentEnd, '', ''); - - if (fullText.length > 0 && fullText.charAt(0) === ' ') { - return { query: '', fullRange: range }; - } - - const query = fullText.split(CHAT_RESOURCE_MENTION_TERMINATOR)[0] ?? ''; - - return { query, fullRange: { from: range.from, to: textFrom + query.length } }; -} - /** * Converts plain text to a TipTap JSON document structure. * Handles newlines by creating multiple paragraphs.