diff --git a/src/apps/review/src/lib/components/AiReviewsTable/AiReviewsTable.tsx b/src/apps/review/src/lib/components/AiReviewsTable/AiReviewsTable.tsx index 06114c48e..e3fd0b774 100644 --- a/src/apps/review/src/lib/components/AiReviewsTable/AiReviewsTable.tsx +++ b/src/apps/review/src/lib/components/AiReviewsTable/AiReviewsTable.tsx @@ -144,7 +144,11 @@ const AiReviewsTable: FC = props => { {run.status === 'SUCCESS' ? ( run.workflow.scorecard ? ( - {run.score} + + {run.score} + ) : run.score ) : '-'} diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardAttachments/ScorecardAttachments.module.scss b/src/apps/review/src/lib/components/Scorecard/ScorecardAttachments/ScorecardAttachments.module.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardAttachments/ScorecardAttachments.tsx b/src/apps/review/src/lib/components/Scorecard/ScorecardAttachments/ScorecardAttachments.tsx new file mode 100644 index 000000000..a0091861c --- /dev/null +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardAttachments/ScorecardAttachments.tsx @@ -0,0 +1,15 @@ +import { FC } from 'react' + +// import styles from './ScorecardAttachments.module.scss' + +interface ScorecardAttachmentsProps { + className?: string +} + +const ScorecardAttachments: FC = props => ( +
+ attachments +
+) + +export default ScorecardAttachments diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardAttachments/index.ts b/src/apps/review/src/lib/components/Scorecard/ScorecardAttachments/index.ts new file mode 100644 index 000000000..a360be3f0 --- /dev/null +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardAttachments/index.ts @@ -0,0 +1 @@ +export { default as ScorecardAttachments } from './ScorecardAttachments' diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardGroup/ScorecardGroup.module.scss b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardGroup/ScorecardGroup.module.scss new file mode 100644 index 000000000..f642ce0ea --- /dev/null +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardGroup/ScorecardGroup.module.scss @@ -0,0 +1,70 @@ +@import '@libs/ui/styles/includes'; + +.wrap { +} + +.headerBar { + display: flex; + align-items: center; + gap: $sp-4; + + background: #00797A; + padding: $sp-4; + + font-family: "Nunito Sans", sans-serif; + font-weight: bold; + font-size: 16px; + line-height: 22px; + color: #FFFFFF; + + cursor: pointer; + transition: 0.15s ease-in-out; + + &:hover { + background: darken(#00797A, 1.5%); + } + + &:active { + transition: none; + background: darken(#00797A, 3%); + } + + &.toggled { + .toggleBtn { + svg { + transform: rotate(180deg); + } + } + } + + @include ltemd { + flex-wrap: wrap; + row-gap: $sp-2; + + .score { + order: 7; + width: 100%; + margin-left: $sp-10; + } + } +} + +.index { + width: 24px; +} + +.mx { + margin: 0 auto; +} + +.toggleBtn { + cursor: pointer; + width: 24px; + + svg { + display: block; + width: 16px; + height: 16px; + transition: 0.15s ease-in-out; + } +} diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardGroup/ScorecardGroup.tsx b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardGroup/ScorecardGroup.tsx new file mode 100644 index 000000000..cac45c094 --- /dev/null +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardGroup/ScorecardGroup.tsx @@ -0,0 +1,61 @@ +import { FC, useCallback, useMemo } from 'react' +import classNames from 'classnames' + +import { IconOutline } from '~/libs/ui' + +import { ScorecardGroup as ScorecardGroupModel } from '../../../../models' +import { ScorecardSection } from '../ScorecardSection' +import { ScorecardViewerContextValue, useScorecardContext } from '../ScorecardViewer.context' +import { ScorecardScore } from '../ScorecardScore' +import { calcGroupScore } from '../utils' + +import styles from './ScorecardGroup.module.scss' + +interface ScorecardGroupProps { + index: number + group: ScorecardGroupModel +} + +const ScorecardGroup: FC = props => { + const { aiFeedbackItems }: ScorecardViewerContextValue = useScorecardContext() + const allFeedbackItems = aiFeedbackItems || [] + const { toggleItem, toggledItems }: ScorecardViewerContextValue = useScorecardContext() + + const isVissible = !toggledItems[props.group.id] + const toggle = useCallback(() => toggleItem(props.group.id), [props.group, toggleItem]) + + const score = useMemo(() => ( + calcGroupScore(props.group, allFeedbackItems) + ), [props.group, allFeedbackItems]) + + return ( +
+
+ + {props.index} + . + + + {props.group.name} + + + + + + + + +
+ + {isVissible && props.group.sections.map((section, index) => ( + + ))} +
+ ) +} + +export default ScorecardGroup diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardGroup/index.ts b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardGroup/index.ts new file mode 100644 index 000000000..ceab26317 --- /dev/null +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardGroup/index.ts @@ -0,0 +1 @@ +export { default as ScorecardGroup } from './ScorecardGroup' diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/AiFeedback/AiFeedback.module.scss b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/AiFeedback/AiFeedback.module.scss new file mode 100644 index 000000000..fe1718d67 --- /dev/null +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/AiFeedback/AiFeedback.module.scss @@ -0,0 +1,11 @@ +@import '@libs/ui/styles/includes'; + +.wrap { + p { + margin-bottom: $sp-4; + + @include ltemd { + margin-bottom: $sp-2; + } + } +} diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/AiFeedback/AiFeedback.tsx b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/AiFeedback/AiFeedback.tsx new file mode 100644 index 000000000..6342883c9 --- /dev/null +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/AiFeedback/AiFeedback.tsx @@ -0,0 +1,51 @@ +import { FC, useMemo } from 'react' + +import { IconAiReview } from '~/apps/review/src/lib/assets/icons' +import { ScorecardQuestion } from '~/apps/review/src/lib/models' + +import { ScorecardViewerContextValue, useScorecardContext } from '../../ScorecardViewer.context' +import { ScorecardQuestionRow } from '../ScorecardQuestionRow' +import { ScorecardScore } from '../../ScorecardScore' + +import styles from './AiFeedback.module.scss' + +interface AiFeedbackProps { + question: ScorecardQuestion +} + +const AiFeedback: FC = props => { + const { aiFeedbackItems }: ScorecardViewerContextValue = useScorecardContext() + const feedback = useMemo(() => ( + aiFeedbackItems?.find(r => r.scorecardQuestionId === props.question.id) + ), [props.question.id, aiFeedbackItems]) + + if (!aiFeedbackItems?.length || !feedback) { + return <> + } + + const isYesNo = props.question.type === 'YES_NO' + + return ( + } + index='AI Feedback' + className={styles.wrap} + score={( + + )} + > + {isYesNo && ( +

+ {feedback.questionScore ? 'Yes' : 'No'} +

+ )} + {feedback.content} +
+ ) +} + +export default AiFeedback diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/AiFeedback/index.ts b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/AiFeedback/index.ts new file mode 100644 index 000000000..5b6150e88 --- /dev/null +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/AiFeedback/index.ts @@ -0,0 +1 @@ +export { default as AiFeedback } from './AiFeedback' diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestion.module.scss b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestion.module.scss new file mode 100644 index 000000000..12c21f897 --- /dev/null +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestion.module.scss @@ -0,0 +1,25 @@ +@import '@libs/ui/styles/includes'; + +.toggleBtn { + cursor: pointer; + display: block; + width: 16px; + height: 16px; + color: #767676; + transition: 0.15s ease-in-out; + + &.toggled { + transform: rotate(180deg); + } +} + +.questionText { + font-weight: bold; + + * { + margin-top: $sp-2; + } +} + +.guidelines { + white-space: pre-wrap; +} diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestion.tsx b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestion.tsx new file mode 100644 index 000000000..fcfd40163 --- /dev/null +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestion.tsx @@ -0,0 +1,51 @@ +import { FC, useCallback } from 'react' +import classNames from 'classnames' + +import { IconOutline } from '~/libs/ui' + +import { ScorecardQuestion as ScorecardQuestionModel } from '../../../../models' +import { ScorecardViewerContextValue, useScorecardContext } from '../ScorecardViewer.context' + +import { AiFeedback } from './AiFeedback' +import { ScorecardQuestionRow } from './ScorecardQuestionRow' +import styles from './ScorecardQuestion.module.scss' + +interface ScorecardQuestionProps { + index: string + question: ScorecardQuestionModel +} + +const ScorecardQuestion: FC = props => { + const { toggleItem, toggledItems }: ScorecardViewerContextValue = useScorecardContext() + + const isToggled = toggledItems[props.question.id!] + const toggle = useCallback(() => toggleItem(props.question.id!), [props.question, toggleItem]) + + return ( +
+ + )} + index={`Question ${props.index}`} + className={styles.headerBar} + score='' + > + + {props.question.description} + + {isToggled && ( +
+ {props.question.guidelines} +
+ )} +
+ +
+ ) +} + +export default ScorecardQuestion diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestionRow/ScorecardQuestionRow.module.scss b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestionRow/ScorecardQuestionRow.module.scss new file mode 100644 index 000000000..838c2d4c0 --- /dev/null +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestionRow/ScorecardQuestionRow.module.scss @@ -0,0 +1,54 @@ +@import '@libs/ui/styles/includes'; + +.wrap { + display: grid; + gap: $sp-4; + padding: $sp-4; + grid-template-columns: 24px 128px auto 144px 24px; + + font-family: "Nunito Sans", sans-serif; + font-weight: bold; + font-size: 14px; + line-height: 20px; + color: #0A0A0A; + + @include ltemd { + display: flex; + gap: $sp-2 $sp-4; + flex-wrap: wrap; + + .icon { + width: 24px; + flex: 0 0 auto + } + + .index { + width: 128px; + flex: 0 0 auto + } + + .content { + flex: 100%; + padding-left: $sp-10; + } + + .score { + width: 100%; + padding-left: $sp-10; + + &:empty { + display: none; + } + &:not(:empty)::before { + content: 'Score'; + display: block; + font-weight: bold; + margin-bottom: $sp-2; + } + } + } +} + +.content { + font-weight: normal; +} diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestionRow/ScorecardQuestionRow.tsx b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestionRow/ScorecardQuestionRow.tsx new file mode 100644 index 000000000..36af21c84 --- /dev/null +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestionRow/ScorecardQuestionRow.tsx @@ -0,0 +1,22 @@ +import { FC, PropsWithChildren, ReactNode } from 'react' +import classNames from 'classnames' + +import styles from './ScorecardQuestionRow.module.scss' + +interface ScorecardQuestionRowProps extends PropsWithChildren { + className?: string + icon?: ReactNode + index?: string + score?: ReactNode +} + +const ScorecardQuestionRow: FC = props => ( +
+ {props.icon} + {props.index} + {props.children} + {props.score} +
+) + +export default ScorecardQuestionRow diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestionRow/index.ts b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestionRow/index.ts new file mode 100644 index 000000000..2075f87d2 --- /dev/null +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestionRow/index.ts @@ -0,0 +1 @@ +export { default as ScorecardQuestionRow } from './ScorecardQuestionRow' diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/index.ts b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/index.ts new file mode 100644 index 000000000..450320793 --- /dev/null +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/index.ts @@ -0,0 +1 @@ +export { default as ScorecardQuestion } from './ScorecardQuestion' diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardScore/ScorecardScore.module.scss b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardScore/ScorecardScore.module.scss new file mode 100644 index 000000000..5e0763c09 --- /dev/null +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardScore/ScorecardScore.module.scss @@ -0,0 +1,15 @@ +@import '@libs/ui/styles/includes'; + +.wrap { + font-weight: normal; + text-align: right; + + span { + margin-left: $sp-1; + opacity: 0.75; + } + + @include ltemd { + text-align: left; + } +} diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardScore/ScorecardScore.tsx b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardScore/ScorecardScore.tsx new file mode 100644 index 000000000..765954eed --- /dev/null +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardScore/ScorecardScore.tsx @@ -0,0 +1,31 @@ +import { FC } from 'react' + +import styles from './ScorecardScore.module.scss' + +interface ScorecardScoreProps { + score: number + scaleMax: number + weight: number +} + +export const calcScore = (score: number, scaleMax: number, weight: number): number => ( + (score / (scaleMax || 1)) * weight +) + +const ScorecardScore: FC = props => { + const score = calcScore(props.score, props.scaleMax, props.weight) + + return ( +
+ + {score.toFixed(2)} + + / + + {props.weight.toFixed(2)} + +
+ ) +} + +export default ScorecardScore diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardScore/index.ts b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardScore/index.ts new file mode 100644 index 000000000..693b38108 --- /dev/null +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardScore/index.ts @@ -0,0 +1 @@ +export { default as ScorecardScore } from './ScorecardScore' diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardSection/ScorecardSection.module.scss b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardSection/ScorecardSection.module.scss new file mode 100644 index 000000000..dbb4d4482 --- /dev/null +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardSection/ScorecardSection.module.scss @@ -0,0 +1,35 @@ +@import '@libs/ui/styles/includes'; + +.wrap { +} + +.headerBar { + display: flex; + align-items: center; + gap: $sp-4; + + background: #E0E4E8; + padding: $sp-4; + + font-family: "Nunito Sans", sans-serif; + font-weight: bold; + font-size: 14px; + line-height: 20px; + color: #0A0A0A; + + padding-right: 56px; + @include ltemd { + flex-wrap: wrap; + row-gap: $sp-2; + + .score { + order: 7; + width: 100%; + margin-left: $sp-10; + } + } +} + +.mx { + margin: 0 auto; +} diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardSection/ScorecardSection.tsx b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardSection/ScorecardSection.tsx new file mode 100644 index 000000000..1f26a9af6 --- /dev/null +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardSection/ScorecardSection.tsx @@ -0,0 +1,55 @@ +import { FC, useMemo } from 'react' + +import { ScorecardSection as ScorecardSectionModel } from '../../../../models' +import { ScorecardQuestion } from '../ScorecardQuestion' +import { ScorecardScore } from '../ScorecardScore' +import { ScorecardViewerContextValue, useScorecardContext } from '../ScorecardViewer.context' +import { calcSectionScore } from '../utils' + +import styles from './ScorecardSection.module.scss' + +interface ScorecardSectionProps { + index: string + section: ScorecardSectionModel +} + +const ScorecardSection: FC = props => { + const { aiFeedbackItems }: ScorecardViewerContextValue = useScorecardContext() + const allFeedbackItems = aiFeedbackItems || [] + + const score = useMemo(() => ( + calcSectionScore(props.section, allFeedbackItems) + ), [props.section, allFeedbackItems]) + + return ( +
+
+ + {props.index} + . + + + {props.section.name} + + + + + +
+ + {props.section.questions.map((question, index) => ( + + ))} +
+ ) +} + +export default ScorecardSection diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardSection/index.ts b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardSection/index.ts new file mode 100644 index 000000000..2b352782a --- /dev/null +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardSection/index.ts @@ -0,0 +1 @@ +export { default as ScorecardSection } from './ScorecardSection' diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardTotal/ScorecardTotal.module.scss b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardTotal/ScorecardTotal.module.scss new file mode 100644 index 000000000..4fe0e4845 --- /dev/null +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardTotal/ScorecardTotal.module.scss @@ -0,0 +1,20 @@ +@import '@libs/ui/styles/includes'; + +.wrap { + display: flex; + align-items: center; + gap: $sp-6; + + padding: $sp-4 56px; + background: #E9ECEF; + + font-family: "Nunito Sans", sans-serif; + font-weight: bold; + font-size: 16px; + line-height: 22px; + color: #0A0A0A; +} + +.mx { + margin: 0 auto; +} diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardTotal/ScorecardTotal.tsx b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardTotal/ScorecardTotal.tsx new file mode 100644 index 000000000..e5e6e6c47 --- /dev/null +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardTotal/ScorecardTotal.tsx @@ -0,0 +1,23 @@ +import { FC } from 'react' + +import { ScorecardScore } from '../ScorecardScore' + +import styles from './ScorecardTotal.module.scss' + +interface ScorecardTotalProps { + score?: number +} + +const ScorecardTotal: FC = props => ( +
+ Total Score + + +
+) + +export default ScorecardTotal diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardTotal/index.ts b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardTotal/index.ts new file mode 100644 index 000000000..60836c05c --- /dev/null +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardTotal/index.ts @@ -0,0 +1 @@ +export { default as ScorecardTotal } from './ScorecardTotal' diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardViewer.context.tsx b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardViewer.context.tsx new file mode 100644 index 000000000..b6cb305cf --- /dev/null +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardViewer.context.tsx @@ -0,0 +1,51 @@ +import { createContext, FC, ReactNode, useCallback, useContext, useEffect, useMemo, useState } from 'react' + +import { AiFeedbackItem, Scorecard } from '../../../models' + +export interface ScorecardViewerContextProps { + children: ReactNode; + scorecard: Scorecard + aiFeedbackItems?: AiFeedbackItem[] +} + +export type ScorecardViewerContextValue = { + aiFeedbackItems?: AiFeedbackItem[] + toggledItems: {[key: string]: boolean} + toggleItem: (id: string) => void +}; + +const ScorecardViewerContext = createContext({} as ScorecardViewerContextValue) + +export const ScorecardViewerContextProvider: FC = props => { + const [toggledItems, setToggledItems] = useState<{[key: string]: boolean}>({}) + + const toggleItem = useCallback((id: string, toggle?: boolean) => { + setToggledItems(prevItems => ({ + ...prevItems, + [id]: typeof toggle === 'boolean' ? toggle : !prevItems[id], + })) + }, []) + + // reset toggle state on scorecard change + useEffect(() => setToggledItems({}), [props.scorecard]) + + const ctxValue = useMemo(() => ({ + aiFeedbackItems: props.aiFeedbackItems, + toggledItems, + toggleItem, + }), [ + props.aiFeedbackItems, + toggledItems, + toggleItem, + ]) + + return ( + + {props.children} + + ) +} + +export const useScorecardContext = (): ScorecardViewerContextValue => useContext(ScorecardViewerContext) diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardViewer.module.scss b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardViewer.module.scss new file mode 100644 index 000000000..d9b9a8d08 --- /dev/null +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardViewer.module.scss @@ -0,0 +1,27 @@ +@import '@libs/ui/styles/includes'; + +.wrap { + width: 100%; + strong { + font-weight: bold; + } +} + +.conclusion { + padding: $sp-4; + background: #E0E4E8; + color: #0A0A0A; + margin-bottom: $sp-4; + + > strong { + font-size: 16px; + line-height: 22px; + + margin-bottom: $sp-2; + } + + p { + font-size: 14px; + line-height: 20px; + } +} diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardViewer.tsx b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardViewer.tsx new file mode 100644 index 000000000..fa5757c50 --- /dev/null +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardViewer.tsx @@ -0,0 +1,51 @@ +import { FC } from 'react' + +import { AiFeedbackItem, Scorecard } from '../../../models' + +import { ScorecardGroup } from './ScorecardGroup' +import { ScorecardViewerContextProvider } from './ScorecardViewer.context' +import { ScorecardTotal } from './ScorecardTotal' +import styles from './ScorecardViewer.module.scss' + +interface ScorecardViewerProps { + scorecard: Scorecard + aiFeedback?: AiFeedbackItem[] + score?: number +} + +const ScorecardViewer: FC = props => ( +
+ + {!!props.score && ( +
+ Conclusion +

+ Congratulations! You earned a score of + {' '} + + {props.score.toFixed(2)} + + {' '} + out of the maximum of + {' '} + + {props.scorecard.maxScore.toFixed(2)} + + . + You did a good job on passing the scorecard criteria. + Please check the below sections to see if there is any place for improvement. +

+
+ )} + {props.scorecard.scorecardGroups.map((group, index) => ( + + ))} + +
+
+) + +export default ScorecardViewer diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/index.ts b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/index.ts new file mode 100644 index 000000000..0cf63a0e2 --- /dev/null +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/index.ts @@ -0,0 +1 @@ +export { default as ScorecardViewer } from './ScorecardViewer' diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/utils.ts b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/utils.ts new file mode 100644 index 000000000..20e60a381 --- /dev/null +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/utils.ts @@ -0,0 +1,25 @@ +import { AiFeedbackItem, ScorecardGroup, ScorecardSection } from '../../../models' + +export const calcSectionScore = ( + section: ScorecardSection, + feedbackItems: Pick[], +): number => { + const feedbackItemsMap = Object.fromEntries(feedbackItems.map(r => [r.scorecardQuestionId, r])) + + return section.questions.reduce((sum, question) => ( + sum + ( + (feedbackItemsMap[question.id as string]?.questionScore ?? 0) / (question.scaleMax || 1) + ) * (question.weight / 100) + ), 0) +} + +export const calcGroupScore = ( + group: ScorecardGroup, + feedbackItems: Pick[], +): number => ( + group.sections.reduce((sum, section) => ( + sum + ( + calcSectionScore(section, feedbackItems) + ) * (section.weight / 100) + ), 0) +) diff --git a/src/apps/review/src/lib/components/Scorecard/index.ts b/src/apps/review/src/lib/components/Scorecard/index.ts new file mode 100644 index 000000000..2a3dc98a0 --- /dev/null +++ b/src/apps/review/src/lib/components/Scorecard/index.ts @@ -0,0 +1 @@ +export * from './ScorecardViewer' diff --git a/src/apps/review/src/lib/hooks/useFetchAiWorkflowRuns.ts b/src/apps/review/src/lib/hooks/useFetchAiWorkflowRuns.ts index 8787d55da..3120c869a 100644 --- a/src/apps/review/src/lib/hooks/useFetchAiWorkflowRuns.ts +++ b/src/apps/review/src/lib/hooks/useFetchAiWorkflowRuns.ts @@ -5,7 +5,7 @@ import { EnvironmentConfig } from '~/config' import { xhrGetAsync } from '~/libs/core' import { handleError } from '~/libs/shared/lib/utils/handle-error' -import { Scorecard } from '../models' +import { AiFeedbackItem, Scorecard } from '../models' import { useRolePermissions, UseRolePermissionsResult } from './useRolePermissions' @@ -46,6 +46,8 @@ export interface AiWorkflowRun { workflow: AiWorkflow } +export type AiWorkflowRunItem = AiFeedbackItem + const TC_API_BASE_URL = EnvironmentConfig.API.V6 export interface AiWorkflowRunsResponse { @@ -53,6 +55,11 @@ export interface AiWorkflowRunsResponse { isLoading: boolean } +export interface AiWorkflowRunItemsResponse { + runItems: AiWorkflowRunItem[] + isLoading: boolean +} + export const aiRunInProgress = (aiRun: Pick): boolean => [ AiWorkflowRunStatusEnum.INIT, AiWorkflowRunStatusEnum.QUEUED, @@ -103,3 +110,32 @@ export function useFetchAiWorkflowsRuns( runs: runs.filter(r => isAdmin || !aiRunFailed(r)), } } + +export function useFetchAiWorkflowsRunItems( + workflowId: string, + runId: string | undefined, +): AiWorkflowRunItemsResponse { + // Use swr hooks for challenge info fetching + const { + data: runItems = [], + error: fetchError, + isValidating: isLoading, + }: SWRResponse = useSWR( + `${TC_API_BASE_URL}/workflows/${workflowId}/runs/${runId}/items`, + { + isPaused: () => !workflowId || !runId, + }, + ) + + // Show backend error when fetching challenge info + useEffect(() => { + if (fetchError) { + handleError(fetchError) + } + }, [fetchError]) + + return { + isLoading, + runItems, + } +} diff --git a/src/apps/review/src/lib/models/AiFeedbackItem.model.ts b/src/apps/review/src/lib/models/AiFeedbackItem.model.ts new file mode 100644 index 000000000..5ea7040f2 --- /dev/null +++ b/src/apps/review/src/lib/models/AiFeedbackItem.model.ts @@ -0,0 +1,9 @@ +export interface AiFeedbackItem { + id: string + content: string + upVotes: number + downVotes: number + questionScore: number + comments: string[] + scorecardQuestionId: string +} diff --git a/src/apps/review/src/lib/models/index.ts b/src/apps/review/src/lib/models/index.ts index ff5ead5a7..32aaa987e 100644 --- a/src/apps/review/src/lib/models/index.ts +++ b/src/apps/review/src/lib/models/index.ts @@ -1,4 +1,5 @@ export * from './AiScorecardContext.model' +export * from './AiFeedbackItem.model' export * from './ChallengeInfo.model' export * from './SubmissionInfo.model' export * from './ReviewInfo.model' diff --git a/src/apps/review/src/lib/services/index.ts b/src/apps/review/src/lib/services/index.ts index 79cbfbd69..097c180b6 100644 --- a/src/apps/review/src/lib/services/index.ts +++ b/src/apps/review/src/lib/services/index.ts @@ -1,7 +1,6 @@ export * from './reviews.service' export * from './challenges.service' export * from './scorecards.service' -export * from './tabs.service' export * from './file-upload.service' export * from './resources.service' export * from './payments.service' diff --git a/src/apps/review/src/lib/services/tabs.service.ts b/src/apps/review/src/lib/services/tabs.service.ts deleted file mode 100644 index 770bacfd7..000000000 --- a/src/apps/review/src/lib/services/tabs.service.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { find, isFunction } from 'lodash' - -import { MockTabs } from '../../mock-datas' -import { SelectOption } from '../models' - -export const fetchTabs = async (type: string, tabsLength: number = 1): Promise => { - const tabs = (find(MockTabs, t => t.name.includes(type)) ?? MockTabs[0]).tabs - return Promise.resolve( - isFunction(tabs) ? tabs(tabsLength) as SelectOption[] : tabs as SelectOption[], - ) -} diff --git a/src/apps/review/src/mock-datas/MockTabs.ts b/src/apps/review/src/mock-datas/MockTabs.ts deleted file mode 100644 index cf9031d45..000000000 --- a/src/apps/review/src/mock-datas/MockTabs.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { - BUG_HUNT, - CODE, - COPILOT_OPPORTUNITY, - DESIGN, - FIRST2FINISH, - MARATHON_MATCH, - OTHER, - TEST_SUITE, -} from '../config/index.config' -import type { SelectOption } from '../lib/models' - -type TabsFactory = (tabsLength?: number) => SelectOption[] - -type MockTabsConfig = { - name: string - tabs: SelectOption[] | TabsFactory -} - -/** - * Mock data for the tabs - */ -export const MockTabs: MockTabsConfig[] = [ - { - name: `${CODE} ${BUG_HUNT} ${TEST_SUITE} ${COPILOT_OPPORTUNITY} ${MARATHON_MATCH} ${OTHER}`, - tabs: [ - { - label: 'Registration', - value: 'Registration', - }, - { - label: 'Submission / Screening', - value: 'Submission / Screening', - }, - { - label: 'Review / Appeals', - value: 'Review / Appeals', - }, - { - label: 'Winners', - value: 'Winners', - }, - ], - }, - { - name: `${DESIGN}`, - tabs: [ - { - label: 'Registration', - value: 'Registration', - }, - { - label: 'Submission / Screening', - value: 'Submission / Screening', - }, - { - label: 'Review', - value: 'Review', - }, - { - label: 'Approval', - value: 'Approval', - }, - { - label: 'Winners', - value: 'Winners', - }, - ], - }, - { - name: `${FIRST2FINISH}`, - tabs: () => [ - { - label: 'Registration', - value: 'Registration', - }, - { - label: 'Submission / Screening', - value: 'Submission / Screening', - }, - { - label: 'Iterative Review', - value: 'Iterative Review', - }, - { - label: 'Winners', - value: 'Winners', - }, - ], - }, -] diff --git a/src/apps/review/src/mock-datas/index.ts b/src/apps/review/src/mock-datas/index.ts deleted file mode 100644 index 5aff070c9..000000000 --- a/src/apps/review/src/mock-datas/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './MockTabs' diff --git a/src/apps/review/src/pages/ai-scorecards/AiScorecardViewer/AiScorecardViewer.module.scss b/src/apps/review/src/pages/ai-scorecards/AiScorecardViewer/AiScorecardViewer.module.scss index 8ce7e7e01..1b3b36e66 100644 --- a/src/apps/review/src/pages/ai-scorecards/AiScorecardViewer/AiScorecardViewer.module.scss +++ b/src/apps/review/src/pages/ai-scorecards/AiScorecardViewer/AiScorecardViewer.module.scss @@ -5,7 +5,7 @@ flex-direction: column; } -.contentWrap { +.pageContentWrap { display: flex; flex-direction: row; gap: $sp-10; @@ -26,3 +26,12 @@ } } } + +.contentWrap { + width: 100%; +} + +.tabs { + justify-content: center; + margin: $sp-6 0; +} diff --git a/src/apps/review/src/pages/ai-scorecards/AiScorecardViewer/AiScorecardViewer.tsx b/src/apps/review/src/pages/ai-scorecards/AiScorecardViewer/AiScorecardViewer.tsx index 7f5ef87da..7b27c1fef 100644 --- a/src/apps/review/src/pages/ai-scorecards/AiScorecardViewer/AiScorecardViewer.tsx +++ b/src/apps/review/src/pages/ai-scorecards/AiScorecardViewer/AiScorecardViewer.tsx @@ -1,25 +1,36 @@ -import { FC, useEffect, useMemo } from 'react' +import { FC, useEffect, useMemo, useState } from 'react' import { NotificationContextType, useNotification } from '~/libs/shared' import { ScorecardHeader } from '../components/ScorecardHeader' import { IconAiReview } from '../../../lib/assets/icons' -import { PageWrapper } from '../../../lib' +import { PageWrapper, Tabs } from '../../../lib' import { useAiScorecardContext } from '../AiScorecardContext' -import { AiScorecardContextModel } from '../../../lib/models' +import { AiScorecardContextModel, SelectOption } from '../../../lib/models' import { AiWorkflowsSidebar } from '../components/AiWorkflowsSidebar' +import { ScorecardViewer } from '../../../lib/components/Scorecard' +import { AiWorkflowRunItemsResponse, useFetchAiWorkflowsRunItems } from '../../../lib/hooks' +import { ScorecardAttachments } from '../../../lib/components/Scorecard/ScorecardAttachments' import styles from './AiScorecardViewer.module.scss' +const tabItems: SelectOption[] = [ + { label: 'Scorecard', value: 'scorecard' }, + { label: 'Attachments', value: 'attachments' }, +] + const AiScorecardViewer: FC = () => { const { showBannerNotification, removeNotification }: NotificationContextType = useNotification() - const { challengeInfo }: AiScorecardContextModel = useAiScorecardContext() + const { challengeInfo, scorecard, workflowId, workflowRun }: AiScorecardContextModel = useAiScorecardContext() + const { runItems }: AiWorkflowRunItemsResponse = useFetchAiWorkflowsRunItems(workflowId, workflowRun?.id) const breadCrumb = useMemo( () => [{ index: 1, label: 'My Active Challenges' }], [], ) + const [selectedTab, setSelectedTab] = useState('scorecard') + useEffect(() => { const notification = showBannerNotification({ icon: , @@ -36,9 +47,28 @@ const AiScorecardViewer: FC = () => { className={styles.container} breadCrumb={breadCrumb} > -
+
- +
+ + + {!!scorecard && selectedTab === 'scorecard' && ( + + )} + + {selectedTab === 'attachments' && ( + + )} +
)