feat: [code-1687]: update ux of pr aida btn (#1164)

This commit is contained in:
Calvin Lee 2024-03-29 20:39:54 +00:00 committed by Harness
parent b02da4b78a
commit 38510ca0c1
14 changed files with 217 additions and 24 deletions

View File

@ -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;

View File

@ -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;
}

View File

@ -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

View File

@ -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<DecorationSet>({
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({
</Container>
<Container className={css.tabContent}>
<Editor
extensions={[myKeymap, history()]}
extensions={[myKeymap, decorationField, history()]}
routingId={routingId}
standalone={standalone}
repoMetadata={repoMetadata}

View File

@ -1002,8 +1002,8 @@ customMin: '{{minutes}}m'
customSecond: '{{seconds}}s'
reqChanges: 'Request changes'
summary: Summary
prGenSummary: Have Harness AIDA summarize the code changes in this pull request
aidaGenSummary: '[AIDA is generating a summary...]'
prGenSummary: AIDA generate PR summary, insert at the cursor or replace selected text only.
aidaGenSummary: 'AIDA is generating a summary...'
importFailed: Import Failed
uploadAFileError: There is no image or video uploaded. Please upload an image or video.
securitySettings:

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="15" height="15" version="1.0" viewBox="0 0 128 128"><g><path fill="#592CD7" d="M122.5 69.25H96.47a33.1 33.1 0 0 0 0-10.5h26.05a5.25 5.25 0 0 1 0 10.5z"/><path fill="#3E1E96" fill-opacity=".3" d="M112.04 97.83 89.47 84.8a33.1 33.1 0 0 0 5.25-9.1l22.57 13.03a5.25 5.25 0 0 1-5.28 9.1zM88.68 117.35 75.65 94.78a33.1 33.1 0 0 0 9.1-5.25l13.02 22.57a5.25 5.25 0 1 1-9.1 5.25zM58.7 122.57V96.5a33.1 33.1 0 0 0 10.5 0v26.07a5.25 5.25 0 0 1-10.5 0M30.1 112.1l13.04-22.57a33.1 33.1 0 0 0 9.1 5.25L39.2 117.35a5.25 5.25 0 1 1-9.1-5.25M10.6 88.74 33.16 75.7a33.1 33.1 0 0 0 5.25 9.1L15.88 97.83a5.25 5.25 0 1 1-5.25-9.1z"/><path fill="#4723AC" fill-opacity=".4" d="M5.37 58.75h26.06a33.1 33.1 0 0 0 0 10.5H5.37a5.25 5.25 0 0 1 0-10.5"/><path fill="#4B25B6" fill-opacity=".5" d="M15.85 30.17 38.4 43.2a33.1 33.1 0 0 0-5.24 9.1L10.6 39.25a5.25 5.25 0 1 1 5.25-9.1z"/><path fill="#5027C1" fill-opacity=".6" d="m39.2 10.65 13.03 22.57a33.1 33.1 0 0 0-9.1 5.25l-13-22.57a5.25 5.25 0 1 1 9.1-5.25z"/><path fill="#5429CC" fill-opacity=".7" d="M69.2 5.43V31.5a33.1 33.1 0 0 0-10.5 0V5.42a5.25 5.25 0 1 1 10.5 0z"/><path fill="#5D2EE1" fill-opacity=".8" d="M97.77 15.9 84.75 38.47a33.1 33.1 0 0 0-9.1-5.25l13.03-22.57a5.25 5.25 0 1 1 9.1 5.25z"/><path fill="#6130EC" fill-opacity=".9" d="M117.3 39.26 94.7 52.3a33.1 33.1 0 0 0-5.25-9.1l22.57-13.03a5.25 5.25 0 0 1 5.25 9.1z"/><animateTransform attributeName="transform" calcMode="discrete" dur="1200ms" repeatCount="indefinite" type="rotate" values="0 64 64;30 64 64;60 64 64;90 64 64;120 64 64;150 64 64;180 64 64;210 64 64;240 64 64;270 64 64;300 64 64;330 64 64"/></g></svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -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;
}
}

View File

@ -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

View File

@ -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 ? (
<Button
size={ButtonSize.SMALL}
variation={ButtonVariation.ICON}
icon={'harness-copilot'}
withoutCurrentColor
iconProps={{ color: Color.AI_PURPLE_800, size: 16 }}
iconProps={{
color: Color.GREY_0,
size: 22,
className: css.aidaIcon
}}
className={css.aidaIcon}
onClick={handleCopilotClick}
tooltip={
<Container padding={'small'} width={270}>

View File

@ -155,6 +155,7 @@ export const ChecksMenu: React.FC<ChecksMenuProps> = ({
const dataArr = groupedData[key]
if (groupedData && dataArr) {
const { minCreated, maxUpdated } = dataArr.reduce(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(acc: any, item: TypesCheck) => ({
minCreated: item.created && item.created < acc.minCreated ? item.created : acc.minCreated,
maxUpdated: item.updated && item.updated > acc.maxUpdated ? item.updated : acc.maxUpdated

View File

@ -274,3 +274,11 @@
.rightTextPadding {
padding-right: var(--spacing-xsmall);
}
.aidaIcon {
:global(> svg) {
border-radius: 4px !important;
background: linear-gradient(317deg, #5e2ce0 11.36%, #a652f9 79.62%) !important;
padding: 2px !important;
}
}

View File

@ -16,6 +16,7 @@
/* eslint-disable */
// This is an auto-generated file
export declare const aidaIcon: string
export declare const ascContainer: string
export declare const box: string
export declare const bp3ButtonText: string

View File

@ -14,18 +14,21 @@
* limitations under the License.
*/
import React, { useEffect, useState } from 'react'
import { Container, useToaster } from '@harnessio/uicore'
import React, { useCallback, useEffect, useState } from 'react'
import { Button, ButtonSize, ButtonVariation, Container, Layout, useToaster, Text } from '@harnessio/uicore'
import cx from 'classnames'
import { useMutate } from 'restful-react'
import { Color, FontVariation } from '@harnessio/design-system'
import { PopoverPosition } from '@blueprintjs/core'
import { MarkdownViewer } from 'components/MarkdownViewer/MarkdownViewer'
import { useStrings } from 'framework/strings'
import type { OpenapiUpdatePullReqRequest } from 'services/code'
import { OptionsMenuButton } from 'components/OptionsMenuButton/OptionsMenuButton'
import { MarkdownEditorWithPreview } from 'components/MarkdownEditorWithPreview/MarkdownEditorWithPreview'
import { NavigationCheck } from 'components/NavigationCheck/NavigationCheck'
import { getErrorMessage } from 'utils/Utils'
import { CommentBoxOutletPosition, getErrorMessage } from 'utils/Utils'
import Config from 'Config'
import { useAppContext } from 'AppContext'
import type { ConversationProps } from './Conversation'
import css from './Conversation.module.scss'
@ -41,6 +44,10 @@ export const DescriptionBox: React.FC<DescriptionBoxProps> = ({
standalone,
routingId
}) => {
const { hooks } = useAppContext()
const [flag, setFlag] = useState(false)
const { SEMANTIC_SEARCH_ENABLED } = hooks?.useFeatureFlags()
const [edit, setEdit] = useState(false)
const [dirty, setDirty] = useState(false)
const [originalContent, setOriginalContent] = useState(pullReqMetadata.description as string)
@ -61,6 +68,11 @@ export const DescriptionBox: React.FC<DescriptionBoxProps> = ({
}
}, [pullReqMetadata?.description, pullReqMetadata?.description?.length])
// write the above function handleCopilotClick in a callback
const handleCopilotClick = useCallback(() => {
setFlag(true)
}, [])
return (
<Container className={cx({ [css.box]: !edit, [css.desc]: !edit })}>
<Container padding={!edit ? { left: 'small', bottom: 'small' } : undefined}>
@ -70,6 +82,42 @@ export const DescriptionBox: React.FC<DescriptionBoxProps> = ({
standalone={standalone}
repoMetadata={repoMetadata}
value={content}
flag={flag}
setFlag={setFlag}
outlets={{
[CommentBoxOutletPosition.START_OF_MARKDOWN_EDITOR_TOOLBAR]: (
<>
{SEMANTIC_SEARCH_ENABLED && !standalone ? (
<Button
size={ButtonSize.SMALL}
variation={ButtonVariation.ICON}
icon={'harness-copilot'}
withoutCurrentColor
iconProps={{
color: Color.GREY_0,
size: 22,
className: css.aidaIcon
}}
className={css.aidaIcon}
onClick={handleCopilotClick}
tooltip={
<Container padding={'small'} width={270}>
<Layout.Vertical flex={{ align: 'center-center' }}>
<Text font={{ variation: FontVariation.BODY }}>{getString('prGenSummary')}</Text>
</Layout.Vertical>
</Container>
}
tooltipProps={{
interactionKind: 'hover',
usePortal: true,
position: PopoverPosition.BOTTOM_LEFT,
popoverClassName: cx(css.popover)
}}
/>
) : null}
</>
)
}}
onSave={value => {
if (value?.split('\n').some(line => line.length > Config.MAX_TEXT_LINE_SIZE_LIMIT)) {
return showError(getString('pr.descHasTooLongLine', { max: Config.MAX_TEXT_LINE_SIZE_LIMIT }), 0)

View File

@ -20,6 +20,7 @@ import moment from 'moment'
import langMap from 'lang-map'
import type { EditorDidMount } from 'react-monaco-editor'
import type { editor } from 'monaco-editor'
import type { EditorView } from '@codemirror/view'
import type { EnumMergeMethod, TypesRuleViolations, TypesViolation, TypesCodeOwnerEvaluationEntry } from 'services/code'
import type { GitInfoProps } from './GitUtils'
@ -650,3 +651,31 @@ export const PAGE_CONTAINER_WIDTH = '--page-container-width'
export enum CommentBoxOutletPosition {
START_OF_MARKDOWN_EDITOR_TOOLBAR = 'start_of_markdown_editor_toolbar'
}
// Helper function to escape special characters for use in regex
export function escapeRegExp(str: string) {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // $& means the whole matched string
}
export function removeSpecificTextOptimized(
viewRef: React.MutableRefObject<EditorView | undefined>,
textToRemove: string
) {
const doc = viewRef?.current?.state.doc.toString()
const regex = new RegExp(escapeRegExp(textToRemove), 'g')
let match
// Initialize an array to hold all changes
const changes = []
// Use regex to find all occurrences of the text
while (doc && (match = regex.exec(doc)) !== null) {
// Add a change object for each match to remove it
changes.push({ from: match.index, to: match.index + match[0].length })
}
// Dispatch a single transaction with all changes if any matches were found
if (changes.length > 0) {
viewRef?.current?.dispatch({ changes })
}
}