Skip to content
Merged
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
3 changes: 3 additions & 0 deletions src/apps/review/src/lib/assets/icons/icon-reply.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions src/apps/review/src/lib/assets/icons/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { ReactComponent as IconChevronDown } from './selector.svg'
import { ReactComponent as IconError } from './icon-error.svg'
import { ReactComponent as IconAiReview } from './icon-ai-review.svg'
import { ReactComponent as IconSubmission } from './icon-phase-submission.svg'
import { ReactComponent as IconReply } from './icon-reply.svg'
import { ReactComponent as IconRegistration } from './icon-phase-registration.svg'
import { ReactComponent as IconPhaseReview } from './icon-phase-review.svg'
import { ReactComponent as IconAppeal } from './icon-phase-appeal.svg'
Expand Down Expand Up @@ -47,6 +48,7 @@ export {
IconAppeal,
IconAppealResponse,
IconPhaseWinners,
IconReply,
IconDeepseekAi,
IconClock,
IconPremium,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,13 @@ $error-line-height: 14px;
}
}

.remainingCharacters {
font-family: "Nunito Sans", sans-serif;
color: var(--GrayFontColor);
font-size: 14px;
line-height: 20px;
}

.error {
display: flex;
align-items: center;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
/**
* Field Markdown Editor.
*/
import { FC, useCallback, useContext, useEffect, useRef } from 'react'
import { FC, useCallback, useContext, useEffect, useRef, useState } from 'react'
import _ from 'lodash'
import CodeMirror from 'codemirror'
import CodeMirror, { EditorChangeCancellable } from 'codemirror'
import EasyMDE from 'easymde'
import classNames from 'classnames'
import 'easymde/dist/easymde.min.css'
Expand Down Expand Up @@ -44,6 +44,7 @@ interface Props {
showBorder?: boolean
disabled?: boolean
uploadCategory?: string
maxCharactersAllowed?: number
}
const errorMessages = {
fileTooLarge:
Expand Down Expand Up @@ -149,6 +150,9 @@ type CodeMirrorType = keyof typeof stateStrategy | 'variable-2'
export const FieldMarkdownEditor: FC<Props> = (props: Props) => {
const elementRef = useRef<HTMLTextAreaElement>(null)
const easyMDE = useRef<any>(null)
const [remainingCharacters, setRemainingCharacters] = useState(
(props.maxCharactersAllowed || 0) - (props.initialValue?.length || 0),
)
const { challengeId }: ChallengeDetailContextModel = useContext(ChallengeDetailContext)
const uploadCategory: string = props.uploadCategory ?? 'general'

Expand Down Expand Up @@ -825,8 +829,30 @@ export const FieldMarkdownEditor: FC<Props> = (props: Props) => {
uploadImage: true,
})

easyMDE.current.codemirror.on('beforeChange', (cm: CodeMirror.Editor, change: EditorChangeCancellable) => {
if (change.update) {
const current = cm.getValue().length
const incoming = change.text.join('\n').length
const replaced = cm.indexFromPos(change.to) - cm.indexFromPos(change.from)

const newLength = current + incoming - replaced

if (props.maxCharactersAllowed) {
if (newLength > props.maxCharactersAllowed) {
change.cancel()
}
}
}
})

easyMDE.current.codemirror.on('change', (cm: CodeMirror.Editor) => {
props.onChange?.(cm.getValue())
if (props.maxCharactersAllowed) {
const remaining = (props.maxCharactersAllowed || 0) - cm.getValue().length
setRemainingCharacters(remaining)
props.onChange?.(cm.getValue())
} else {
props.onChange?.(cm.getValue())
}
})

easyMDE.current.codemirror.on('blur', () => {
Expand Down Expand Up @@ -856,7 +882,13 @@ export const FieldMarkdownEditor: FC<Props> = (props: Props) => {
})}
>
<textarea ref={elementRef} placeholder={props.placeholder} />

{props.maxCharactersAllowed && (
<div className={styles.remainingCharacters}>
{remainingCharacters}
{' '}
characters remaining
</div>
)}
{props.error && (
<div className={classNames(styles.error, 'errorMessage')}>
{props.error}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
import { FC, useMemo } from 'react'
import { FC, useCallback, useMemo, useState } from 'react'
import { mutate } from 'swr'

import { IconAiReview } from '~/apps/review/src/lib/assets/icons'
import { ScorecardQuestion } from '~/apps/review/src/lib/models'
import { ReviewsContextModel, ScorecardQuestion } from '~/apps/review/src/lib/models'
import { createFeedbackComment } from '~/apps/review/src/lib/services'
import { useReviewsContext } from '~/apps/review/src/pages/reviews/ReviewsContext'
import { EnvironmentConfig } from '~/config'

import { ScorecardViewerContextValue, useScorecardViewerContext } from '../../ScorecardViewer.context'
import { ScorecardQuestionRow } from '../ScorecardQuestionRow'
import { ScorecardScore } from '../../ScorecardScore'
import { MarkdownReview } from '../../../../MarkdownReview'
import { AiFeedbackActions } from '../AiFeedbackActions/AiFeedbackActions'
import { AiFeedbackComments } from '../AiFeedbackComments/AiFeedbackComments'
import { AiFeedbackReply } from '../AiFeedbackReply/AiFeedbackReply'

import styles from './AiFeedback.module.scss'

Expand All @@ -21,9 +26,23 @@ const AiFeedback: FC<AiFeedbackProps> = props => {
const feedback: any = useMemo(() => (
aiFeedbackItems?.find((r: any) => r.scorecardQuestionId === props.question.id)
), [props.question.id, aiFeedbackItems])
const { workflowId, workflowRun }: ReviewsContextModel = useReviewsContext()
const [showReply, setShowReply] = useState(false)

const commentsArr: any[] = (feedback?.comments) || []

const onShowReply = useCallback(() => {
setShowReply(!showReply)
}, [])

const onSubmitReply = useCallback(async (content: string) => {
await createFeedbackComment(workflowId as string, workflowRun?.id as string, feedback?.id, {
content,
})
await mutate(`${EnvironmentConfig.API.V6}/workflows/${workflowId}/runs/${workflowRun?.id}/items`)
setShowReply(false)
}, [workflowId, workflowRun?.id, feedback?.id])

if (!aiFeedbackItems?.length || !feedback) {
return <></>
}
Expand All @@ -50,10 +69,21 @@ const AiFeedback: FC<AiFeedbackProps> = props => {

<MarkdownReview value={feedback.content} />

<AiFeedbackActions feedback={feedback} actionType='runItem' />
<AiFeedbackActions feedback={feedback} actionType='runItem' onPressReply={onShowReply} />

{
showReply && (
<AiFeedbackReply
onSubmitReply={onSubmitReply}
onCloseReply={function closeReply() {
setShowReply(false)
}}
/>
)
}

{commentsArr.length > 0 && (
<AiFeedbackComments comments={commentsArr} feedback={feedback} />
<AiFeedbackComments comments={commentsArr} feedback={feedback} isRoot />
)}
</ScorecardQuestionRow>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { FC, useCallback, useContext, useEffect, useState } from 'react'
import { mutate } from 'swr'

import {
IconReply,
IconThumbsDown,
IconThumbsDownFilled,
IconThumbsUp,
Expand All @@ -27,6 +28,7 @@ interface AiFeedbackActionsProps {
actionType: 'comment' | 'runItem'
comment?: AiFeedbackComment
feedback?: any
onPressReply: () => void
}

export const AiFeedbackActions: FC<AiFeedbackActionsProps> = props => {
Expand Down Expand Up @@ -222,6 +224,15 @@ export const AiFeedbackActions: FC<AiFeedbackActionsProps> = props => {
{userVote === 'DOWNVOTE' ? <IconThumbsDownFilled /> : <IconThumbsDown />}
<span className={styles.count}>{downVotes}</span>
</button>

<button
type='button'
className={styles.actionBtn}
onClick={props.onPressReply}
>
<IconReply />
<span className={styles.count}>Reply</span>
</button>
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { FC, useCallback, useState } from 'react'
import { mutate } from 'swr'
import classNames from 'classnames'
import moment from 'moment'

import { useReviewsContext } from '~/apps/review/src/pages/reviews/ReviewsContext'
import { createFeedbackComment } from '~/apps/review/src/lib/services'
import { AiFeedbackItem, ReviewsContextModel } from '~/apps/review/src/lib/models'
import { EnvironmentConfig } from '~/config'

import { AiFeedbackActions } from '../AiFeedbackActions/AiFeedbackActions'
import { AiFeedbackReply } from '../AiFeedbackReply/AiFeedbackReply'
import { MarkdownReview } from '../../../../MarkdownReview'

import { AiFeedbackComment as AiFeedbackCommentType, AiFeedbackComments } from './AiFeedbackComments'
import styles from './AiFeedbackComments.module.scss'

interface AiFeedbackCommentProps {
comment: AiFeedbackCommentType
feedback: AiFeedbackItem
isRoot: boolean
}

export const AiFeedbackComment: FC<AiFeedbackCommentProps> = props => {
const { workflowId, workflowRun }: ReviewsContextModel = useReviewsContext()
const [showReply, setShowReply] = useState(false)

const onShowReply = useCallback(() => {
setShowReply(!showReply)
}, [])

const onSubmitReply = useCallback(async (content: string, comment: AiFeedbackCommentType) => {
await createFeedbackComment(workflowId as string, workflowRun?.id as string, props.feedback?.id, {
content,
parentId: comment.id,
})
await mutate(`${EnvironmentConfig.API.V6}/workflows/${workflowId}/runs/${workflowRun?.id}/items`)
setShowReply(false)
}, [workflowId, workflowRun?.id, props.feedback?.id])
return (
<div
key={props.comment.id}
className={classNames(styles.comment, {
[styles.noMarginTop]: !props.isRoot,
})}
>
<div className={styles.info}>
<span className={styles.reply}>Reply</span>
<span className={styles.text}> by </span>
<span
style={{
color: props.comment.createdUser.ratingColor || '#0A0A0A',
}}
className={styles.name}
>
{props.comment.createdUser.handle}
</span>
<span className={styles.text}> on </span>
<span className={styles.date}>
{ moment(props.comment.createdAt)
.local()
.format('MMM DD, hh:mm A')}
</span>
</div>
<MarkdownReview value={props.comment.content} />
<AiFeedbackActions
feedback={props.feedback}
comment={props.comment}
actionType='comment'
onPressReply={onShowReply}
/>
{
showReply && (
<AiFeedbackReply
onSubmitReply={function submitReply(content: string) {
return onSubmitReply(content, props.comment)
}}
onCloseReply={function closeReply() {
setShowReply(false)
}}
/>
)
}
<AiFeedbackComments comments={props.comment.comments} feedback={props.feedback} isRoot={false} />
</div>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@
margin-top: 32px;
background-color: #E0E4E8;
padding: 16px;
&.noMarginTop {
margin-top: 0;
padding: 0;
padding-left: 16px;
}
.info {
margin: 16px 0;
font-size: 14px;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { FC } from 'react'
import moment from 'moment'

import { AiFeedbackActions } from '../AiFeedbackActions/AiFeedbackActions'
import classNames from 'classnames'

import { AiFeedbackComment } from './AiFeedbackComment'
import styles from './AiFeedbackComments.module.scss'

export interface AiFeedbackVote {
Expand All @@ -12,6 +11,7 @@ export interface AiFeedbackVote {
createdAt: string
createdBy: string
}

export interface AiFeedbackComment {
id: string
content: string
Expand All @@ -23,40 +23,21 @@ export interface AiFeedbackComment {
handle: string
ratingColor: string
}
comments: AiFeedbackComment[]
votes: AiFeedbackVote[]
}

interface AiFeedbackCommentsProps {
comments: AiFeedbackComment[]
feedback: any
isRoot: boolean
}

export const AiFeedbackComments: FC<AiFeedbackCommentsProps> = props => (
<div className={styles.comments}>
{props.comments.filter(c => !c.parentId)
<div className={classNames(styles.comments)}>
{props.comments
.map((comment: AiFeedbackComment) => (
<div key={comment.id} className={styles.comment}>
<div className={styles.info}>
<span className={styles.reply}>Reply</span>
<span className={styles.text}> by </span>
<span
style={{
color: comment.createdUser.ratingColor || '#0A0A0A',
}}
className={styles.name}
>
{comment.createdUser.handle}
</span>
<span className={styles.text}> on </span>
<span className={styles.date}>
{ moment(comment.createdAt)
.local()
.format('MMM DD, hh:mm A')}
</span>
</div>
<div className={styles.commentContent}>{comment.content}</div>
<AiFeedbackActions feedback={props.feedback} comment={comment} actionType='comment' />
</div>
<AiFeedbackComment isRoot={props.isRoot} comment={comment} feedback={props.feedback} />
))}
</div>
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
@import '@libs/ui/styles/includes';

.replyWrapper {
background-color: #E0E4E8;
padding: 16px;
margin-top: 16px;
.title {
font-family: "Nunito Sans", sans-serif;
font-weight: 700;
color: #0A0A0A;
margin-bottom: 16px;
}
.blockBtns {
margin-top: 24px;
.cancelButton {
margin-left: 16px;
}
}
}
Loading
Loading