Skip to content

Commit 74b038f

Browse files
authored
Merge pull request #1322 from topcoder-platform/pm-2177_likes
feat(PM-2177): likes and dislikes on run items and comments
2 parents 186fc86 + f9c98ad commit 74b038f

File tree

12 files changed

+464
-3
lines changed

12 files changed

+464
-3
lines changed
Lines changed: 3 additions & 0 deletions
Loading
Lines changed: 3 additions & 0 deletions
Loading
Lines changed: 3 additions & 0 deletions
Loading
Lines changed: 3 additions & 0 deletions
Loading

src/apps/review/src/lib/assets/icons/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ import { ReactComponent as IconPremium } from './icon-premium.svg'
1515
import { ReactComponent as IconComment } from './icon-comment.svg'
1616
import { ReactComponent as IconEdit } from './icon-edit.svg'
1717
import { ReactComponent as IconFile } from './icon-file.svg'
18+
import { ReactComponent as IconThumbsUp } from './icon-thumb-up.svg'
19+
import { ReactComponent as IconThumbsDown } from './icon-thumbs-down.svg'
20+
import { ReactComponent as IconThumbsUpFilled } from './icon-thumb-up-filled.svg'
21+
import { ReactComponent as IconThumbsDownFilled } from './icon-thumbs-down-filled.svg'
1822

1923
export * from './editor/bold'
2024
export * from './editor/code'
@@ -49,6 +53,10 @@ export {
4953
IconComment,
5054
IconEdit,
5155
IconFile,
56+
IconThumbsUp,
57+
IconThumbsDown,
58+
IconThumbsUpFilled,
59+
IconThumbsDownFilled,
5260
}
5361

5462
export const phasesIcons = {

src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/AiFeedback/AiFeedback.module.scss

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,49 @@
88
margin-bottom: $sp-2;
99
}
1010
}
11+
12+
.comment {
13+
margin-top: $sp-3;
14+
padding-left: $sp-4;
15+
border-left: 2px solid black;
16+
}
17+
18+
.commentContent {
19+
margin-bottom: $sp-1;
20+
}
21+
22+
.commentActions {
23+
display: flex;
24+
gap: $sp-2;
25+
margin-bottom: $sp-2;
26+
}
27+
28+
.commentBtn {
29+
display: inline-flex;
30+
align-items: center;
31+
gap: $sp-1;
32+
background: transparent;
33+
border: none;
34+
cursor: pointer;
35+
padding: $sp-1;
36+
}
37+
38+
.replies {
39+
margin-left: $sp-4;
40+
margin-top: $sp-2;
41+
}
42+
43+
.reply {
44+
margin-top: $sp-2;
45+
}
46+
47+
.count {
48+
font-size: 12px;
49+
margin-left: $sp-1;
50+
}
51+
52+
.active {
53+
color: blue;
54+
font-weight: 600;
55+
}
1156
}

src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/AiFeedback/AiFeedback.tsx

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import { ScorecardViewerContextValue, useScorecardViewerContext } from '../../Sc
77
import { ScorecardQuestionRow } from '../ScorecardQuestionRow'
88
import { ScorecardScore } from '../../ScorecardScore'
99
import { MarkdownReview } from '../../../../MarkdownReview'
10+
import { AiFeedbackActions } from '../AiFeedbackActions/AiFeedbackActions'
11+
import { AiFeedbackComments } from '../AiFeedbackComments/AiFeedbackComments'
1012

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

@@ -16,10 +18,12 @@ interface AiFeedbackProps {
1618

1719
const AiFeedback: FC<AiFeedbackProps> = props => {
1820
const { aiFeedbackItems, scoreMap }: ScorecardViewerContextValue = useScorecardViewerContext()
19-
const feedback = useMemo(() => (
20-
aiFeedbackItems?.find(r => r.scorecardQuestionId === props.question.id)
21+
const feedback: any = useMemo(() => (
22+
aiFeedbackItems?.find((r: any) => r.scorecardQuestionId === props.question.id)
2123
), [props.question.id, aiFeedbackItems])
2224

25+
const commentsArr: any[] = (feedback?.comments) || []
26+
2327
if (!aiFeedbackItems?.length || !feedback) {
2428
return <></>
2529
}
@@ -43,7 +47,14 @@ const AiFeedback: FC<AiFeedbackProps> = props => {
4347
<strong>{feedback.questionScore ? 'Yes' : 'No'}</strong>
4448
</p>
4549
)}
50+
4651
<MarkdownReview value={feedback.content} />
52+
53+
<AiFeedbackActions feedback={feedback} actionType='runItem' />
54+
55+
{commentsArr.length > 0 && (
56+
<AiFeedbackComments comments={commentsArr} feedback={feedback} />
57+
)}
4758
</ScorecardQuestionRow>
4859
)
4960
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
@import '@libs/ui/styles/includes';
2+
3+
.actions {
4+
display: flex;
5+
gap: $sp-2;
6+
margin-top: $sp-3;
7+
.count {
8+
font-family: $font-roboto;
9+
color: #0D61BF;
10+
font-weight: 700;
11+
font-size: 14px;
12+
}
13+
}
14+
15+
.actionBtn {
16+
display: inline-flex;
17+
align-items: center;
18+
gap: $sp-2;
19+
background: transparent;
20+
border: none;
21+
cursor: pointer;
22+
color: black;
23+
padding: $sp-1 $sp-2;
24+
&.active {
25+
svg {
26+
path {
27+
fill: $link-blue-dark;
28+
}
29+
}
30+
}
31+
32+
svg {
33+
width: 20px;
34+
height: 20px;
35+
}
36+
37+
&:hover {
38+
opacity: 0.85;
39+
}
40+
}
Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
/* eslint-disable react/jsx-no-bind */
2+
import { FC, useCallback, useContext, useEffect, useState } from 'react'
3+
import { mutate } from 'swr'
4+
5+
import {
6+
IconThumbsDown,
7+
IconThumbsDownFilled,
8+
IconThumbsUp,
9+
IconThumbsUpFilled,
10+
} from '~/apps/review/src/lib/assets/icons'
11+
import { ReviewAppContext } from '~/apps/review/src/lib/contexts'
12+
import { useReviewsContext } from '~/apps/review/src/pages/reviews/ReviewsContext'
13+
import { updateLikesOrDislikesOnRunItem, updateLikesOrDislikesOnRunItemComment } from '~/apps/review/src/lib/services'
14+
import { EnvironmentConfig } from '~/config'
15+
import { ReviewAppContextModel, ReviewsContextModel } from '~/apps/review/src/lib/models'
16+
17+
import { AiFeedbackComment } from '../AiFeedbackComments/AiFeedbackComments'
18+
19+
import styles from './AiFeedbackActions.module.scss'
20+
21+
export enum VOTE_TYPE {
22+
UPVOTE = 'UPVOTE',
23+
DOWNVOTE = 'DOWNVOTE'
24+
}
25+
26+
interface AiFeedbackActionsProps {
27+
actionType: 'comment' | 'runItem'
28+
comment?: AiFeedbackComment
29+
feedback?: any
30+
}
31+
32+
export const AiFeedbackActions: FC<AiFeedbackActionsProps> = props => {
33+
34+
const [userVote, setUserVote] = useState<string | undefined>(undefined)
35+
const [upVotes, setUpVotes] = useState<number>(0)
36+
const [downVotes, setDownVotes] = useState<number>(0)
37+
38+
const { loginUserInfo }: ReviewAppContextModel = useContext(ReviewAppContext)
39+
const { workflowId, workflowRun }: ReviewsContextModel = useReviewsContext()
40+
41+
const votesArr: any[] = (props.actionType === 'runItem' ? (props.feedback?.votes) : (props.comment?.votes)) || []
42+
43+
const setInitialVotesForFeedback = useCallback((): void => {
44+
const initialUp = props.feedback?.upVotes ?? votesArr.filter(v => String(v.voteType)
45+
.toLowerCase()
46+
.includes('up')).length
47+
const initialDown = props.feedback?.downVotes ?? votesArr.filter(v => String(v.voteType)
48+
.toLowerCase()
49+
.includes('down')).length
50+
51+
const myVote = votesArr.find(v => String(v.createdBy) === String(loginUserInfo?.userId))
52+
setUpVotes(initialUp)
53+
setDownVotes(initialDown)
54+
setUserVote(myVote?.voteType ?? undefined)
55+
}, [votesArr, props.feedback])
56+
57+
const setInitialVotesForComment = useCallback((): void => {
58+
const initialUp = votesArr.filter(v => String(v.voteType)
59+
.toLowerCase()
60+
.includes('up')).length
61+
const initialDown = votesArr.filter(v => String(v.voteType)
62+
.toLowerCase()
63+
.includes('down')).length
64+
65+
const myVote = votesArr.find(v => String(v.createdBy) === String(loginUserInfo?.userId))
66+
setUpVotes(initialUp)
67+
setDownVotes(initialDown)
68+
setUserVote(myVote?.voteType ?? undefined)
69+
}, [votesArr])
70+
71+
useEffect(() => {
72+
if (props.actionType === 'runItem') {
73+
setInitialVotesForFeedback()
74+
} else {
75+
setInitialVotesForComment()
76+
}
77+
}, [props.actionType, props.feedback?.id, votesArr.length, loginUserInfo?.userId])
78+
79+
const voteOnItem = useCallback(async (type: VOTE_TYPE) => {
80+
if (!workflowId || !workflowRun?.id) return
81+
const current = userVote
82+
let up = false
83+
let down = false
84+
85+
if (current === type) {
86+
// remove vote
87+
up = false
88+
down = false
89+
} else if (!current) {
90+
up = type === VOTE_TYPE.UPVOTE
91+
down = type === VOTE_TYPE.DOWNVOTE
92+
} else {
93+
// switch vote
94+
up = type === VOTE_TYPE.UPVOTE
95+
down = type === VOTE_TYPE.DOWNVOTE
96+
}
97+
98+
// optimistic update
99+
const prevUserVote = userVote
100+
const prevUp = upVotes
101+
const prevDown = downVotes
102+
103+
if (current === type) {
104+
// removing
105+
if (type === VOTE_TYPE.UPVOTE) setUpVotes(Math.max(0, upVotes - 1))
106+
else setDownVotes(Math.max(0, downVotes - 1))
107+
setUserVote(undefined)
108+
} else if (!current) {
109+
if (type === VOTE_TYPE.UPVOTE) setUpVotes(upVotes + 1)
110+
else setDownVotes(downVotes + 1)
111+
setUserVote(type)
112+
} else {
113+
// switch
114+
if (type === VOTE_TYPE.UPVOTE) {
115+
setUpVotes(upVotes + 1)
116+
setDownVotes(Math.max(0, downVotes - 1))
117+
} else {
118+
setDownVotes(downVotes + 1)
119+
setUpVotes(Math.max(0, upVotes - 1))
120+
}
121+
122+
setUserVote(type)
123+
}
124+
125+
try {
126+
await updateLikesOrDislikesOnRunItem(workflowId, workflowRun.id, props.feedback.id, {
127+
downVote: down,
128+
upVote: up,
129+
})
130+
} catch (err) {
131+
// rollback
132+
setUserVote(prevUserVote)
133+
setUpVotes(prevUp)
134+
setDownVotes(prevDown)
135+
}
136+
}, [workflowId, workflowRun, props.feedback?.id, userVote, upVotes, downVotes])
137+
138+
const voteOnComment = useCallback(async (c: any, type: VOTE_TYPE) => {
139+
if (!workflowId || !workflowRun?.id) return
140+
const votes = (c.votes || [])
141+
const my = votes.find((v: any) => String(v.createdBy) === String(loginUserInfo?.userId))
142+
const current = my?.voteType ?? undefined
143+
144+
let up = false
145+
let down = false
146+
147+
if (current === type) {
148+
up = false
149+
down = false
150+
} else if (!current) {
151+
up = type === VOTE_TYPE.UPVOTE
152+
down = type === VOTE_TYPE.DOWNVOTE
153+
} else {
154+
up = type === VOTE_TYPE.UPVOTE
155+
down = type === VOTE_TYPE.DOWNVOTE
156+
}
157+
158+
const prevUserVote = userVote
159+
const prevUp = upVotes
160+
const prevDown = downVotes
161+
162+
if (current === type) {
163+
// removing
164+
if (type === VOTE_TYPE.UPVOTE) setUpVotes(Math.max(0, upVotes - 1))
165+
else setDownVotes(Math.max(0, downVotes - 1))
166+
setUserVote(undefined)
167+
} else if (!current) {
168+
if (type === VOTE_TYPE.UPVOTE) setUpVotes(upVotes + 1)
169+
else setDownVotes(downVotes + 1)
170+
setUserVote(type)
171+
} else {
172+
// switch
173+
if (type === VOTE_TYPE.UPVOTE) {
174+
setUpVotes(upVotes + 1)
175+
setDownVotes(Math.max(0, downVotes - 1))
176+
} else {
177+
setDownVotes(downVotes + 1)
178+
setUpVotes(Math.max(0, upVotes - 1))
179+
}
180+
181+
setUserVote(type)
182+
183+
}
184+
185+
try {
186+
await updateLikesOrDislikesOnRunItemComment(workflowId, workflowRun.id, props.feedback.id, c.id, {
187+
downVote: down,
188+
upVote: up,
189+
})
190+
await mutate(`${EnvironmentConfig.API.V6}/workflows/${workflowId}/runs/${workflowRun.id}/items`)
191+
} catch (err) {
192+
setUserVote(prevUserVote)
193+
setUpVotes(prevUp)
194+
setDownVotes(prevDown)
195+
}
196+
}, [workflowId, workflowRun, props.feedback?.id, loginUserInfo])
197+
198+
const onVote = (action: VOTE_TYPE): void => {
199+
if (props.actionType === 'comment') {
200+
voteOnComment(props.comment as AiFeedbackComment, action)
201+
} else {
202+
voteOnItem(action)
203+
}
204+
}
205+
206+
return (
207+
<div className={styles.actions}>
208+
<button
209+
type='button'
210+
className={styles.actionBtn}
211+
onClick={() => onVote(VOTE_TYPE.UPVOTE)}
212+
>
213+
{userVote === 'UPVOTE' ? <IconThumbsUpFilled /> : <IconThumbsUp />}
214+
<span className={styles.count}>{upVotes}</span>
215+
</button>
216+
217+
<button
218+
type='button'
219+
className={styles.actionBtn}
220+
onClick={() => onVote(VOTE_TYPE.DOWNVOTE)}
221+
>
222+
{userVote === 'DOWNVOTE' ? <IconThumbsDownFilled /> : <IconThumbsDown />}
223+
<span className={styles.count}>{downVotes}</span>
224+
</button>
225+
</div>
226+
)
227+
}

0 commit comments

Comments
 (0)