diff --git a/web/src/components/Editor/Editor.module.scss b/web/src/components/Editor/Editor.module.scss index 6467a8386..1d6c03607 100644 --- a/web/src/components/Editor/Editor.module.scss +++ b/web/src/components/Editor/Editor.module.scss @@ -46,6 +46,20 @@ padding: var(--spacing-small); .cm-line { + > .aidaGenText { + color: var(--ai-purple-800) !important; + } + + > .aidaGenText::before { + padding-top: 4px; + padding-right: 3px; + content: url('../../icons/spinner.svg?url'); + } + > .highlightText { + padding: 1px; + background: var(--ai-purple-100) !important; + } + &, * { @include markdown-font; diff --git a/web/src/components/MarkdownEditorWithPreview/MarkdownEditorWithPreview.module.scss b/web/src/components/MarkdownEditorWithPreview/MarkdownEditorWithPreview.module.scss index c680c96f0..550d99121 100644 --- a/web/src/components/MarkdownEditorWithPreview/MarkdownEditorWithPreview.module.scss +++ b/web/src/components/MarkdownEditorWithPreview/MarkdownEditorWithPreview.module.scss @@ -119,6 +119,11 @@ // @include mono-font; // } // } + .cm-line { + > .aidaGenText { + background: var(--ai-purple-800) !important; + } + } .md-editor { background-color: transparent !important; @@ -270,3 +275,8 @@ background: var(--grey-50); } } + +.aidaGenText { + background: var(--ai-purple-800) !important; + font-weight: bold !important; +} diff --git a/web/src/components/MarkdownEditorWithPreview/MarkdownEditorWithPreview.module.scss.d.ts b/web/src/components/MarkdownEditorWithPreview/MarkdownEditorWithPreview.module.scss.d.ts index d86522f88..553f41302 100644 --- a/web/src/components/MarkdownEditorWithPreview/MarkdownEditorWithPreview.module.scss.d.ts +++ b/web/src/components/MarkdownEditorWithPreview/MarkdownEditorWithPreview.module.scss.d.ts @@ -16,6 +16,7 @@ /* eslint-disable */ // This is an auto-generated file +export declare const aidaGenText: string export declare const buttonsBar: string export declare const container: string export declare const dialog: string diff --git a/web/src/components/MarkdownEditorWithPreview/MarkdownEditorWithPreview.tsx b/web/src/components/MarkdownEditorWithPreview/MarkdownEditorWithPreview.tsx index a20e2cfb1..46fa24b44 100644 --- a/web/src/components/MarkdownEditorWithPreview/MarkdownEditorWithPreview.tsx +++ b/web/src/components/MarkdownEditorWithPreview/MarkdownEditorWithPreview.tsx @@ -29,18 +29,25 @@ import { import type { IconName } from '@harnessio/icons' import { Color, FontVariation } from '@harnessio/design-system' import cx from 'classnames' -import type { EditorView } from '@codemirror/view' -import { keymap } from '@codemirror/view' +import { DecorationSet, EditorView, Decoration, keymap } from '@codemirror/view' import { undo, redo, history } from '@codemirror/commands' -import { EditorSelection } from '@codemirror/state' +import { EditorSelection, StateEffect, StateField } from '@codemirror/state' import { isEmpty } from 'lodash-es' import { useMutate } from 'restful-react' import { Editor } from 'components/Editor/Editor' import { MarkdownViewer } from 'components/MarkdownViewer/MarkdownViewer' import { useStrings } from 'framework/strings' -import { CommentBoxOutletPosition, formatBytes, getErrorMessage, handleFileDrop, handlePaste } from 'utils/Utils' +import { + CommentBoxOutletPosition, + formatBytes, + getErrorMessage, + handleFileDrop, + handlePaste, + removeSpecificTextOptimized +} from 'utils/Utils' import { decodeGitContent, handleUpload, normalizeGitRef } from 'utils/GitUtils' import type { TypesRepository } from 'services/code' +import { useEventListener } from 'hooks/useEventListener' import css from './MarkdownEditorWithPreview.module.scss' enum MarkdownEditorTab { @@ -74,6 +81,32 @@ const toolbar: ToolbarItem[] = [ { icon: 'main-code-yaml', action: ToolbarAction.CODE_BLOCK } ] +// Define a unique effect to update decorations +const addDecorationEffect = StateEffect.define<{ decoration: Decoration; from: number; to: number }[]>() +const removeDecorationEffect = StateEffect.define<{}>() // No payload needed for removal in this simple case// Create a state field to hold decorations +const decorationField = StateField.define({ + create() { + return Decoration.none + }, + update(decorations, tr) { + decorations = decorations.map(tr.changes) + + for (const effect of tr.effects) { + if (effect.is(addDecorationEffect)) { + const add = [] + for (const { decoration, from, to } of effect.value) { + add.push(decoration.range(from, to)) + } + decorations = decorations.update({ add }) + } else if (effect.is(removeDecorationEffect)) { + decorations = Decoration.none + } + } + return decorations + }, + provide: f => EditorView.decorations.from(f) +}) + interface MarkdownEditorWithPreviewProps { className?: string value?: string @@ -151,6 +184,7 @@ export function MarkdownEditorWithPreview({ path: `/api/v1/repos/${repoMetadata?.path}/+/genai/change-summary` }) const isDirty = useRef(dirty) + const [data, setData] = useState({}) useEffect( function setDirtyRef() { @@ -175,22 +209,54 @@ export function MarkdownEditorWithPreview({ preventDefault: true } ]) + const handleMouseDown = useCallback( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (event: any) => { + const editorDom = viewRef?.current?.dom + if (!editorDom?.contains(event.target)) { + // Clicked outside the editor + viewRef?.current?.dispatch({ + effects: removeDecorationEffect.of({}) + }) + } + }, + [viewRef] + ) - const dispatchContent = (content: string, userEvent: boolean) => { + useEventListener('mousedown', handleMouseDown) + + const dispatchContent = (content: string, userEvent: boolean, decoration = false) => { const view = viewRef.current - const currentContent = view?.state.doc.toString() + const { from, to } = view?.state.selection.main ?? { from: 0, to: 0 } + const changeColorDecoration = Decoration.mark({ class: 'aidaGenText' }) + const highlightDecoration = Decoration.mark({ class: 'highlightText' }) - view?.dispatch({ - changes: { from: 0, to: currentContent?.length, insert: content }, - userEvent: userEvent ? 'input' : 'ignore' // Marking this transaction as an input event makes it part of the undo history - }) + if (decoration) { + view?.dispatch({ + changes: { from: from, to: to, insert: content }, + effects: addDecorationEffect.of([ + { decoration: changeColorDecoration, from: from, to: content?.length + from } + ]), + userEvent: userEvent ? 'input' : 'ignore' // Marking this transaction as an input event makes it part of the undo history, + }) + } else { + view?.dispatch({ + effects: removeDecorationEffect.of({ from: from, to: to }), + userEvent: userEvent ? 'input' : 'ignore' // Marking this transaction as an input event makes it part of the undo history + }) + view?.dispatch({ + changes: { from: from, to: to, insert: content }, + effects: addDecorationEffect.of([{ decoration: highlightDecoration, from: from, to: content?.length + from }]), + userEvent: userEvent ? 'input' : 'ignore' // Marking this transaction as an input event makes it part of the undo history + }) + removeSpecificTextOptimized(viewRef, getString('aidaGenSummary')) + } } - const [data, setData] = useState({}) useEffect(() => { if (flag) { if (handleCopilotClick) { - dispatchContent(getString('aidaGenSummary'), false) + dispatchContent(getString('aidaGenSummary'), false, true) mutate({ head_ref: normalizeGitRef(sourceGitRef), base_ref: normalizeGitRef(targetGitRef) @@ -204,13 +270,13 @@ export function MarkdownEditorWithPreview({ } setFlag?.(false) } - }, [handleCopilotClick]) + }, [handleCopilotClick]) // eslint-disable-line react-hooks/exhaustive-deps useEffect(() => { if (!isEmpty(data)) { dispatchContent(`${data}`, true) } - }, [data]) + }, [data]) // eslint-disable-line react-hooks/exhaustive-deps const onToolbarAction = useCallback((action: ToolbarAction) => { const view = viewRef.current @@ -406,7 +472,7 @@ export function MarkdownEditorWithPreview({ })) ) } - }, [markdownContent]) + }, [markdownContent]) // eslint-disable-line react-hooks/exhaustive-deps const handleButtonClick = () => { if (fileInputRef.current) { @@ -536,7 +602,7 @@ export function MarkdownEditorWithPreview({ \ No newline at end of file diff --git a/web/src/pages/Compare/Compare.module.scss b/web/src/pages/Compare/Compare.module.scss index 48aa19c11..d028c8030 100644 --- a/web/src/pages/Compare/Compare.module.scss +++ b/web/src/pages/Compare/Compare.module.scss @@ -120,3 +120,11 @@ font-weight: normal; } } + +.aidaIcon { + :global(> svg) { + border-radius: 4px !important; + background: linear-gradient(317deg, #5e2ce0 11.36%, #a652f9 79.62%) !important; + padding: 2px !important; + } +} diff --git a/web/src/pages/Compare/Compare.module.scss.d.ts b/web/src/pages/Compare/Compare.module.scss.d.ts index 677100018..4a3125904 100644 --- a/web/src/pages/Compare/Compare.module.scss.d.ts +++ b/web/src/pages/Compare/Compare.module.scss.d.ts @@ -16,6 +16,7 @@ /* eslint-disable */ // This is an auto-generated file +export declare const aidaIcon: string export declare const changesContainer: string export declare const generalTab: string export declare const hyperlink: string diff --git a/web/src/pages/Compare/Compare.tsx b/web/src/pages/Compare/Compare.tsx index 17d1149e5..16d5f8663 100644 --- a/web/src/pages/Compare/Compare.tsx +++ b/web/src/pages/Compare/Compare.tsx @@ -66,7 +66,7 @@ import css from './Compare.module.scss' export default function Compare() { const { routes, standalone, hooks, routingId } = useAppContext() const [flag, setFlag] = useState(false) - const { SEMANTIC_SEARCH_ENABLED: isSemanticSearchFFEnabled } = hooks?.useFeatureFlags() + const { SEMANTIC_SEARCH_ENABLED } = hooks?.useFeatureFlags() const { getString } = useStrings() const history = useHistory() const { repoMetadata, error, loading, diffRefs } = useGetRepositoryMetadata() @@ -304,13 +304,18 @@ export default function Compare() { outlets={{ [CommentBoxOutletPosition.START_OF_MARKDOWN_EDITOR_TOOLBAR]: ( <> - {isSemanticSearchFFEnabled && !standalone ? ( + {SEMANTIC_SEARCH_ENABLED && !standalone ? (