diff --git a/src/apps/review/src/lib/assets/icons/deepseek.svg b/src/apps/review/src/lib/assets/icons/deepseek.svg new file mode 100644 index 000000000..e54d70391 --- /dev/null +++ b/src/apps/review/src/lib/assets/icons/deepseek.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/apps/review/src/lib/assets/icons/icon-clock.svg b/src/apps/review/src/lib/assets/icons/icon-clock.svg new file mode 100644 index 000000000..bc8dd3a99 --- /dev/null +++ b/src/apps/review/src/lib/assets/icons/icon-clock.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/apps/review/src/lib/assets/icons/icon-premium.svg b/src/apps/review/src/lib/assets/icons/icon-premium.svg new file mode 100644 index 000000000..afa0cf4d4 --- /dev/null +++ b/src/apps/review/src/lib/assets/icons/icon-premium.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/apps/review/src/lib/assets/icons/index.ts b/src/apps/review/src/lib/assets/icons/index.ts index 7ec1bf70b..80589e44d 100644 --- a/src/apps/review/src/lib/assets/icons/index.ts +++ b/src/apps/review/src/lib/assets/icons/index.ts @@ -9,6 +9,9 @@ import { ReactComponent as IconReview } from './icon-phase-review.svg' import { ReactComponent as IconAppeal } from './icon-phase-appeal.svg' import { ReactComponent as IconAppealResponse } from './icon-phase-appeal-response.svg' import { ReactComponent as IconPhaseWinners } from './icon-phase-winners.svg' +import { ReactComponent as IconDeepseekAi } from './deepseek.svg' +import { ReactComponent as IconClock } from './icon-clock.svg' +import { ReactComponent as IconPremium } from './icon-premium.svg' export * from './editor/bold' export * from './editor/code' @@ -37,6 +40,9 @@ export { IconAppeal, IconAppealResponse, IconPhaseWinners, + IconDeepseekAi, + IconClock, + IconPremium, } export const phasesIcons = { diff --git a/src/apps/review/src/lib/hooks/useFetchAiWorkflowRuns.ts b/src/apps/review/src/lib/hooks/useFetchAiWorkflowRuns.ts index 1e3a69717..8787d55da 100644 --- a/src/apps/review/src/lib/hooks/useFetchAiWorkflowRuns.ts +++ b/src/apps/review/src/lib/hooks/useFetchAiWorkflowRuns.ts @@ -25,10 +25,21 @@ export interface AiWorkflow { name: string; description: string; scorecard?: Scorecard + defUrl: string + llm: { + name: string + description: string + icon: string + url: string + provider: { + name: string + } + } } export interface AiWorkflowRun { id: string; + startedAt: string; completedAt: string; status: AiWorkflowRunStatusEnum; score: number; diff --git a/src/apps/review/src/pages/ai-scorecards/components/AiModelIcon.tsx b/src/apps/review/src/pages/ai-scorecards/components/AiModelIcon.tsx new file mode 100644 index 000000000..9d296780d --- /dev/null +++ b/src/apps/review/src/pages/ai-scorecards/components/AiModelIcon.tsx @@ -0,0 +1,27 @@ +import { FC, useCallback, useRef } from 'react' + +import iconDeepseekAi from '~/apps/review/src/lib/assets/icons/deepseek.svg' + +import { AiWorkflow } from '../../../lib/hooks' + +interface AiModelIconProps { + model: AiWorkflow['llm'] +} + +const AiModelIcon: FC = props => { + const llmIconImgRef = useRef(null) + + const handleError = useCallback(() => { + if (!llmIconImgRef.current) { + return + } + + llmIconImgRef.current.src = iconDeepseekAi + }, []) + + return ( + {props.model.name} + ) +} + +export default AiModelIcon diff --git a/src/apps/review/src/pages/ai-scorecards/components/AiModelModal/AiModelModal.module.scss b/src/apps/review/src/pages/ai-scorecards/components/AiModelModal/AiModelModal.module.scss new file mode 100644 index 000000000..3f0514beb --- /dev/null +++ b/src/apps/review/src/pages/ai-scorecards/components/AiModelModal/AiModelModal.module.scss @@ -0,0 +1,66 @@ +@import '@libs/ui/styles/includes'; + +.wrap { + @include ltemd { + padding-top: $sp-15; + } +} + +.modelNameWrap { + display: flex; + align-items: center; + gap: $sp-4; + @include ltemd { + flex-direction: column; + gap: $sp-4; + } +} + +.modelIcon { + width: 60px; + height: 60px; + border-radius: $sp-1; + border: 1px solid #A8A8A8; + padding: $sp-1; + + align-items: center; + display: flex; + justify-content: center; + + flex: 0 0 auto; +} + +.modelName { + display: flex; + align-items: center; + gap: $sp-3; + + h3 { + font-family: "Figtree", sans-serif; + font-size: 26px; + font-weight: 700; + line-height: 30px; + color: #0A0A0A; + } + + svg { + display: block; + width: 16px; + height: 16px; + } + + @include ltemd { + h3 { + font-size: 22px; + line-height: 26px; + } + } +} + +.modelDescription { + margin-top: $sp-6; + font-family: "Nunito Sans", sans-serif; + font-size: 14px; + line-height: 20px; + color: #0A0A0A; +} diff --git a/src/apps/review/src/pages/ai-scorecards/components/AiModelModal/AiModelModal.tsx b/src/apps/review/src/pages/ai-scorecards/components/AiModelModal/AiModelModal.tsx new file mode 100644 index 000000000..8aa2e02ae --- /dev/null +++ b/src/apps/review/src/pages/ai-scorecards/components/AiModelModal/AiModelModal.tsx @@ -0,0 +1,44 @@ +import { FC } from 'react' + +import { BaseModal } from '~/libs/ui' +import { AiWorkflow } from '~/apps/review/src/lib/hooks' +import { IconExternalLink } from '~/apps/review/src/lib/assets/icons' + +import AiModelIcon from '../AiModelIcon' + +import styles from './AiModelModal.module.scss' + +interface AiModelModalProps { + model: AiWorkflow['llm'] + onClose: () => void +} + +const AiModelModal: FC = props => ( + +
+
+
+ +
+
+

{props.model.name}

+ + + +
+
+ +

+ {props.model.description} +

+
+
+) + +export default AiModelModal diff --git a/src/apps/review/src/pages/ai-scorecards/components/AiModelModal/index.ts b/src/apps/review/src/pages/ai-scorecards/components/AiModelModal/index.ts new file mode 100644 index 000000000..948754b83 --- /dev/null +++ b/src/apps/review/src/pages/ai-scorecards/components/AiModelModal/index.ts @@ -0,0 +1 @@ +export { default as AiModelModal } from './AiModelModal' diff --git a/src/apps/review/src/pages/ai-scorecards/components/ScorecardHeader/ScorecardHeader.module.scss b/src/apps/review/src/pages/ai-scorecards/components/ScorecardHeader/ScorecardHeader.module.scss index e69de29bb..b84af1272 100644 --- a/src/apps/review/src/pages/ai-scorecards/components/ScorecardHeader/ScorecardHeader.module.scss +++ b/src/apps/review/src/pages/ai-scorecards/components/ScorecardHeader/ScorecardHeader.module.scss @@ -0,0 +1,133 @@ +@import '@libs/ui/styles/includes'; + +.wrap { + width: 100%; + color: #0A0A0A; +} + +.headerWrap { + display: flex; + align-items: flex-start; + + @include ltemd { + flex-direction: column; + align-items: stretch; + gap: $sp-6; + } +} + +.workflowInfo { + display: flex; + align-items: flex-start; + gap: $sp-4; +} + +.workflowIcon { + width: 60px; + height: 60px; + border-radius: $sp-1; + border: 1px solid #A8A8A8; + padding: $sp-1; + + align-items: center; + display: flex; + justify-content: center; + + flex: 0 0 auto; + @include ltemd { + width: 56px; + height: 56px; + } +} + +.workflowName { + h3 { + font-family: "Figtree", sans-serif; + font-size: 26px; + font-weight: 700; + line-height: 30px; + color: #0A0A0A; + margin-bottom: $sp-2; + } + + span { + color: $link-blue-dark; + font-family: "Nunito Sans", sans-serif; + font-weight: bold; + font-size: 16px; + line-height: 22px; + } + + .modelName { + cursor: pointer; + } + + @include ltemd { + h3 { + font-size: 22px; + line-height: 26px; + } + } +} + +.workflowRunStats { + margin-left: auto; + display: flex; + flex-direction: column; + gap: $sp-1; + + flex: 0 0 auto; + + > span { + display: flex; + align-items: center; + gap: $sp-2; + + font-family: "Nunito Sans", sans-serif; + font-size: 14px; + line-height: 19px; + color: var(--GrayFontColor); + } + + strong { + font-family: "Nunito Sans", sans-serif; + font-size: 14px; + font-weight: 700; + line-height: 19px; + color: var(--FontColor); + } + + @include ltemd { + margin-left: 0; + } +} + +.workflowDescription { + margin-top: $sp-6; + font-family: "Nunito Sans", sans-serif; + font-size: 14px; + line-height: 20px; + color: #0A0A0A; +} + +.workflowFileLink { + margin-top: $sp-4; + a { + display: flex; + align-items: center; + gap: $sp-1; + color: $link-blue-dark; + font-family: "Nunito Sans", sans-serif; + font-size: 14px; + line-height: 20px; + + svg { + width: 12px; + height: 12px; + path { + fill: $link-blue-dark; + } + } + } + +} diff --git a/src/apps/review/src/pages/ai-scorecards/components/ScorecardHeader/ScorecardHeader.tsx b/src/apps/review/src/pages/ai-scorecards/components/ScorecardHeader/ScorecardHeader.tsx index dc95ef666..442ee1232 100644 --- a/src/apps/review/src/pages/ai-scorecards/components/ScorecardHeader/ScorecardHeader.tsx +++ b/src/apps/review/src/pages/ai-scorecards/components/ScorecardHeader/ScorecardHeader.tsx @@ -1,17 +1,85 @@ -import { FC } from 'react' +import { FC, useCallback, useMemo, useState } from 'react' +import moment, { Duration } from 'moment' import { AiScorecardContextModel } from '~/apps/review/src/lib/models' import { useAiScorecardContext } from '../../AiScorecardContext' +import { IconClock, IconPremium } from '../../../../lib/assets/icons' +import { AiModelModal } from '../AiModelModal' +import AiModelIcon from '../AiModelIcon' import styles from './ScorecardHeader.module.scss' +const formatDuration = (duration: Duration): string => [ + !!duration.hours() && `${duration.hours()}h`, + !!duration.minutes() && `${duration.minutes()}m`, + !!duration.seconds() && `${duration.seconds()}s`, +].filter(Boolean) + .join(' ') + const ScorecardHeader: FC = () => { - const { workflow }: AiScorecardContextModel = useAiScorecardContext() + const { workflow, workflowRun }: AiScorecardContextModel = useAiScorecardContext() + const runDuration = useMemo(() => ( + workflowRun && workflowRun.completedAt && workflowRun.startedAt && moment.duration( + +new Date(workflowRun.completedAt) - +new Date(workflowRun.startedAt), + 'milliseconds', + ) + ), [workflowRun]) + const [modelDetailsModalVisible, setModelDetailsModalVisible] = useState(false) + + const toggleModelDetails = useCallback(() => { + setModelDetailsModalVisible(wasVisible => !wasVisible) + }, []) + + if (!workflow || !workflowRun) { + return <> + } return (
- {workflow?.name} +
+
+
+ +
+
+

{workflow.name}

+ {workflow.llm.name} +
+
+
+ + + + Minimum passing score: + {' '} + {workflow.scorecard?.minimumPassingScore.toFixed(2)} + + + + + + Duration: + {' '} + {!!runDuration && formatDuration(runDuration)} + + +
+
+

+ {workflow.description} +

+ {/*
+ + Workflow File + {' '} + + +
*/} + + {modelDetailsModalVisible && ( + + )}
) }