From 1cb6a4572979bf0db375c6e342e14bc6e62a0c68 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Fri, 21 Nov 2025 03:27:01 +0100 Subject: [PATCH 1/3] feat: comments implementation --- .../src/lib/assets/icons/icon-reply.svg | 3 + src/apps/review/src/lib/assets/icons/index.ts | 2 + .../FieldMarkdownEditor.tsx | 32 ++++++- .../AiFeedback/AiFeedback.tsx | 35 ++++++- .../AiFeedbackActions/AiFeedbackActions.tsx | 11 +++ .../AiFeedbackComments/AiFeedbackComment.tsx | 76 +++++++++++++++ .../AiFeedbackComments.module.scss | 5 + .../AiFeedbackComments/AiFeedbackComments.tsx | 34 ++----- .../AiFeedbackReply.module.scss | 19 ++++ .../AiFeedbackReply/AiFeedbackReply.tsx | 94 +++++++++++++++++++ .../src/lib/models/FormFeedbackReply.model.ts | 6 ++ .../src/lib/services/scorecards.service.ts | 13 +++ src/apps/review/src/lib/utils/validation.ts | 10 ++ 13 files changed, 306 insertions(+), 34 deletions(-) create mode 100644 src/apps/review/src/lib/assets/icons/icon-reply.svg create mode 100644 src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/AiFeedbackComments/AiFeedbackComment.tsx create mode 100644 src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/AiFeedbackReply/AiFeedbackReply.module.scss create mode 100644 src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/AiFeedbackReply/AiFeedbackReply.tsx create mode 100644 src/apps/review/src/lib/models/FormFeedbackReply.model.ts diff --git a/src/apps/review/src/lib/assets/icons/icon-reply.svg b/src/apps/review/src/lib/assets/icons/icon-reply.svg new file mode 100644 index 000000000..91ed863a2 --- /dev/null +++ b/src/apps/review/src/lib/assets/icons/icon-reply.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 de1e549c0..d1c6b61fd 100644 --- a/src/apps/review/src/lib/assets/icons/index.ts +++ b/src/apps/review/src/lib/assets/icons/index.ts @@ -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' @@ -47,6 +48,7 @@ export { IconAppeal, IconAppealResponse, IconPhaseWinners, + IconReply, IconDeepseekAi, IconClock, IconPremium, diff --git a/src/apps/review/src/lib/components/FieldMarkdownEditor/FieldMarkdownEditor.tsx b/src/apps/review/src/lib/components/FieldMarkdownEditor/FieldMarkdownEditor.tsx index 966b321c2..ef258aef5 100644 --- a/src/apps/review/src/lib/components/FieldMarkdownEditor/FieldMarkdownEditor.tsx +++ b/src/apps/review/src/lib/components/FieldMarkdownEditor/FieldMarkdownEditor.tsx @@ -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, { EditorChange, EditorChangeCancellable } from 'codemirror' import EasyMDE from 'easymde' import classNames from 'classnames' import 'easymde/dist/easymde.min.css' @@ -44,6 +44,7 @@ interface Props { showBorder?: boolean disabled?: boolean uploadCategory?: string + maxCharactersAllowed?: number } const errorMessages = { fileTooLarge: @@ -149,6 +150,7 @@ type CodeMirrorType = keyof typeof stateStrategy | 'variable-2' export const FieldMarkdownEditor: FC = (props: Props) => { const elementRef = useRef(null) const easyMDE = useRef(null) + const [remainingCharacters, setRemainingCharacters] = useState((props.maxCharactersAllowed || 0) - (props.initialValue?.length || 0)) const { challengeId }: ChallengeDetailContextModel = useContext(ChallengeDetailContext) const uploadCategory: string = props.uploadCategory ?? 'general' @@ -825,8 +827,30 @@ export const FieldMarkdownEditor: FC = (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 remainingCharacters = (props.maxCharactersAllowed || 0) - cm.getValue().length + setRemainingCharacters(remainingCharacters) + props.onChange?.(cm.getValue()) + } else { + props.onChange?.(cm.getValue()) + } }) easyMDE.current.codemirror.on('blur', () => { @@ -856,7 +880,7 @@ export const FieldMarkdownEditor: FC = (props: Props) => { })} >