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
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions src/apps/review/src/lib/assets/icons/icon-thumb-up.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions src/apps/review/src/lib/assets/icons/icon-thumbs-down.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 8 additions & 0 deletions src/apps/review/src/lib/assets/icons/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ import { ReactComponent as IconPremium } from './icon-premium.svg'
import { ReactComponent as IconComment } from './icon-comment.svg'
import { ReactComponent as IconEdit } from './icon-edit.svg'
import { ReactComponent as IconFile } from './icon-file.svg'
import { ReactComponent as IconThumbsUp } from './icon-thumb-up.svg'
import { ReactComponent as IconThumbsDown } from './icon-thumbs-down.svg'
import { ReactComponent as IconThumbsUpFilled } from './icon-thumb-up-filled.svg'
import { ReactComponent as IconThumbsDownFilled } from './icon-thumbs-down-filled.svg'

export * from './editor/bold'
export * from './editor/code'
Expand Down Expand Up @@ -49,6 +53,10 @@ export {
IconComment,
IconEdit,
IconFile,
IconThumbsUp,
IconThumbsDown,
IconThumbsUpFilled,
IconThumbsDownFilled,
}

export const phasesIcons = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,49 @@
margin-bottom: $sp-2;
}
}

.comment {
margin-top: $sp-3;
padding-left: $sp-4;
border-left: 2px solid black;
}

.commentContent {
margin-bottom: $sp-1;
}

.commentActions {
display: flex;
gap: $sp-2;
margin-bottom: $sp-2;
}

.commentBtn {
display: inline-flex;
align-items: center;
gap: $sp-1;
background: transparent;
border: none;
cursor: pointer;
padding: $sp-1;
}

.replies {
margin-left: $sp-4;
margin-top: $sp-2;
}

.reply {
margin-top: $sp-2;
}

.count {
font-size: 12px;
margin-left: $sp-1;
}

.active {
color: blue;
font-weight: 600;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { ScorecardViewerContextValue, useScorecardViewerContext } from '../../Sc
import { ScorecardQuestionRow } from '../ScorecardQuestionRow'
import { ScorecardScore } from '../../ScorecardScore'
import { MarkdownReview } from '../../../../MarkdownReview'
import { AiFeedbackActions } from '../AiFeedbackActions/AiFeedbackActions'
import { AiFeedbackComments } from '../AiFeedbackComments/AiFeedbackComments'

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

Expand All @@ -16,10 +18,12 @@ interface AiFeedbackProps {

const AiFeedback: FC<AiFeedbackProps> = props => {
const { aiFeedbackItems, scoreMap }: ScorecardViewerContextValue = useScorecardViewerContext()
const feedback = useMemo(() => (
aiFeedbackItems?.find(r => r.scorecardQuestionId === props.question.id)
const feedback: any = useMemo(() => (
aiFeedbackItems?.find((r: any) => r.scorecardQuestionId === props.question.id)
), [props.question.id, aiFeedbackItems])

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

if (!aiFeedbackItems?.length || !feedback) {
return <></>
}
Expand All @@ -43,7 +47,14 @@ const AiFeedback: FC<AiFeedbackProps> = props => {
<strong>{feedback.questionScore ? 'Yes' : 'No'}</strong>
</p>
)}

<MarkdownReview value={feedback.content} />

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

{commentsArr.length > 0 && (
<AiFeedbackComments comments={commentsArr} feedback={feedback} />
)}
</ScorecardQuestionRow>
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
@import '@libs/ui/styles/includes';

.actions {
display: flex;
gap: $sp-2;
margin-top: $sp-3;
.count {
font-family: $font-roboto;
color: #0D61BF;
font-weight: 700;
font-size: 14px;
}
}

.actionBtn {
display: inline-flex;
align-items: center;
gap: $sp-2;
background: transparent;
border: none;
cursor: pointer;
color: black;
padding: $sp-1 $sp-2;
&.active {
svg {
path {
fill: $link-blue-dark;
}
}
}

svg {
width: 20px;
height: 20px;
}

&:hover {
opacity: 0.85;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
/* eslint-disable react/jsx-no-bind */
import { FC, useCallback, useContext, useEffect, useState } from 'react'
import { mutate } from 'swr'

import {
IconThumbsDown,
IconThumbsDownFilled,
IconThumbsUp,
IconThumbsUpFilled,
} from '~/apps/review/src/lib/assets/icons'
import { ReviewAppContext } from '~/apps/review/src/lib/contexts'
import { useReviewsContext } from '~/apps/review/src/pages/reviews/ReviewsContext'
import { updateLikesOrDislikesOnRunItem, updateLikesOrDislikesOnRunItemComment } from '~/apps/review/src/lib/services'
import { EnvironmentConfig } from '~/config'
import { ReviewAppContextModel, ReviewsContextModel } from '~/apps/review/src/lib/models'

import { AiFeedbackComment } from '../AiFeedbackComments/AiFeedbackComments'

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

export enum VOTE_TYPE {
UPVOTE = 'UPVOTE',
DOWNVOTE = 'DOWNVOTE'
}

interface AiFeedbackActionsProps {
actionType: 'comment' | 'runItem'
comment?: AiFeedbackComment
feedback?: any
}

export const AiFeedbackActions: FC<AiFeedbackActionsProps> = props => {

const [userVote, setUserVote] = useState<string | undefined>(undefined)
const [upVotes, setUpVotes] = useState<number>(0)
const [downVotes, setDownVotes] = useState<number>(0)

const { loginUserInfo }: ReviewAppContextModel = useContext(ReviewAppContext)
const { workflowId, workflowRun }: ReviewsContextModel = useReviewsContext()

const votesArr: any[] = (props.actionType === 'runItem' ? (props.feedback?.votes) : (props.comment?.votes)) || []

const setInitialVotesForFeedback = useCallback((): void => {
const initialUp = props.feedback?.upVotes ?? votesArr.filter(v => String(v.voteType)
.toLowerCase()
.includes('up')).length
const initialDown = props.feedback?.downVotes ?? votesArr.filter(v => String(v.voteType)
.toLowerCase()
.includes('down')).length

const myVote = votesArr.find(v => String(v.createdBy) === String(loginUserInfo?.userId))
setUpVotes(initialUp)
setDownVotes(initialDown)
setUserVote(myVote?.voteType ?? undefined)
}, [votesArr, props.feedback])

const setInitialVotesForComment = useCallback((): void => {
const initialUp = votesArr.filter(v => String(v.voteType)
.toLowerCase()
.includes('up')).length
const initialDown = votesArr.filter(v => String(v.voteType)
.toLowerCase()
.includes('down')).length

const myVote = votesArr.find(v => String(v.createdBy) === String(loginUserInfo?.userId))
setUpVotes(initialUp)
setDownVotes(initialDown)
setUserVote(myVote?.voteType ?? undefined)
}, [votesArr])

useEffect(() => {
if (props.actionType === 'runItem') {
setInitialVotesForFeedback()
} else {
setInitialVotesForComment()
}
}, [props.actionType, props.feedback?.id, votesArr.length, loginUserInfo?.userId])

const voteOnItem = useCallback(async (type: VOTE_TYPE) => {
if (!workflowId || !workflowRun?.id) return
const current = userVote
let up = false
let down = false

if (current === type) {
// remove vote
up = false
down = false
} else if (!current) {
up = type === VOTE_TYPE.UPVOTE
down = type === VOTE_TYPE.DOWNVOTE
} else {
// switch vote
up = type === VOTE_TYPE.UPVOTE
down = type === VOTE_TYPE.DOWNVOTE
}

// optimistic update
const prevUserVote = userVote
const prevUp = upVotes
const prevDown = downVotes

if (current === type) {
// removing
if (type === VOTE_TYPE.UPVOTE) setUpVotes(Math.max(0, upVotes - 1))
else setDownVotes(Math.max(0, downVotes - 1))
setUserVote(undefined)
} else if (!current) {
if (type === VOTE_TYPE.UPVOTE) setUpVotes(upVotes + 1)
else setDownVotes(downVotes + 1)
setUserVote(type)
} else {
// switch
if (type === VOTE_TYPE.UPVOTE) {
setUpVotes(upVotes + 1)
setDownVotes(Math.max(0, downVotes - 1))
} else {
setDownVotes(downVotes + 1)
setUpVotes(Math.max(0, upVotes - 1))
}

setUserVote(type)
}

try {
await updateLikesOrDislikesOnRunItem(workflowId, workflowRun.id, props.feedback.id, {
downVote: down,
upVote: up,
})
} catch (err) {
// rollback
setUserVote(prevUserVote)
setUpVotes(prevUp)
setDownVotes(prevDown)
}
}, [workflowId, workflowRun, props.feedback?.id, userVote, upVotes, downVotes])

const voteOnComment = useCallback(async (c: any, type: VOTE_TYPE) => {
if (!workflowId || !workflowRun?.id) return
const votes = (c.votes || [])
const my = votes.find((v: any) => String(v.createdBy) === String(loginUserInfo?.userId))
const current = my?.voteType ?? undefined

let up = false
let down = false

if (current === type) {
up = false
down = false
} else if (!current) {
up = type === VOTE_TYPE.UPVOTE
down = type === VOTE_TYPE.DOWNVOTE
} else {
up = type === VOTE_TYPE.UPVOTE
down = type === VOTE_TYPE.DOWNVOTE
}

const prevUserVote = userVote
const prevUp = upVotes
const prevDown = downVotes

if (current === type) {
// removing
if (type === VOTE_TYPE.UPVOTE) setUpVotes(Math.max(0, upVotes - 1))
else setDownVotes(Math.max(0, downVotes - 1))
setUserVote(undefined)
} else if (!current) {
if (type === VOTE_TYPE.UPVOTE) setUpVotes(upVotes + 1)
else setDownVotes(downVotes + 1)
setUserVote(type)
} else {
// switch
if (type === VOTE_TYPE.UPVOTE) {
setUpVotes(upVotes + 1)
setDownVotes(Math.max(0, downVotes - 1))
} else {
setDownVotes(downVotes + 1)
setUpVotes(Math.max(0, upVotes - 1))
}

setUserVote(type)

}

try {
await updateLikesOrDislikesOnRunItemComment(workflowId, workflowRun.id, props.feedback.id, c.id, {
downVote: down,
upVote: up,
})
await mutate(`${EnvironmentConfig.API.V6}/workflows/${workflowId}/runs/${workflowRun.id}/items`)
} catch (err) {
setUserVote(prevUserVote)
setUpVotes(prevUp)
setDownVotes(prevDown)
}
}, [workflowId, workflowRun, props.feedback?.id, loginUserInfo])

const onVote = (action: VOTE_TYPE): void => {
if (props.actionType === 'comment') {
voteOnComment(props.comment as AiFeedbackComment, action)
} else {
voteOnItem(action)
}
}

return (
<div className={styles.actions}>
<button
type='button'
className={styles.actionBtn}
onClick={() => onVote(VOTE_TYPE.UPVOTE)}
>
{userVote === 'UPVOTE' ? <IconThumbsUpFilled /> : <IconThumbsUp />}
<span className={styles.count}>{upVotes}</span>
</button>

<button
type='button'
className={styles.actionBtn}
onClick={() => onVote(VOTE_TYPE.DOWNVOTE)}
>
{userVote === 'DOWNVOTE' ? <IconThumbsDownFilled /> : <IconThumbsDown />}
<span className={styles.count}>{downVotes}</span>
</button>
</div>
)
}
Loading
Loading