Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
@@ -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>
{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,18 @@
import { FC, useMemo } from 'react'
import { FC, useCallback, useMemo, useState } from 'react'

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 +25,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 +68,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