diff --git a/src/pages/Plan/modules/Factory/modules/AdditionalTarget/Component.tsx b/src/pages/Plan/modules/Factory/modules/AdditionalTarget/Component.tsx index 7bb2438e5..4530c7674 100644 --- a/src/pages/Plan/modules/Factory/modules/AdditionalTarget/Component.tsx +++ b/src/pages/Plan/modules/Factory/modules/AdditionalTarget/Component.tsx @@ -1,54 +1,99 @@ import { - Card, - Hint, + AccordionNew, + Button, + Editor, + EditorRef, + FormField, IconButton, Label, - Message, - Paragraph, + Notification, + SM, Span, - Textarea, Tooltip, + useToast, } from '@appquality/unguess-design-system'; -import { useState } from 'react'; -import { useTranslation } from 'react-i18next'; +import { useEffect, useRef, useState } from 'react'; +import { Trans, useTranslation } from 'react-i18next'; import { appTheme } from 'src/app/theme'; +import { ReactComponent as AlertIcon } from 'src/assets/icons/alert-icon.svg'; +import { ReactComponent as InfoIcon } from 'src/assets/icons/info-icon.svg'; +import { ReactComponent as AiPencilIcon } from 'src/assets/icons/ai-test.svg'; import { ReactComponent as TrashIcon } from 'src/assets/icons/trash-stroke.svg'; import { components } from 'src/common/schema'; +import { usePostAiJobsMutation } from 'src/features/api'; import { useModule } from 'src/features/modules/useModule'; import { useModuleConfiguration } from 'src/features/modules/useModuleConfiguration'; import { useValidation } from 'src/features/modules/useModuleValidation'; import useWindowSize from 'src/hooks/useWindowSize'; import { DeleteModuleConfirmationModal } from 'src/pages/Plan/modules/modal/DeleteModuleConfirmationModal'; import styled from 'styled-components'; + +import { + ModuleAdditionalTargetContextProvider, + useModuleAdditionalTargetContext, +} from './Context/AdditionalTargetModalContext'; +import { ImproveWithAIModal } from '../shared/ImproveWithAIModal'; import { useIconWithValidation } from './useIcon'; +import { CommandBar } from '../shared/CommandBar'; -const StyledCard = styled(Card)` +const StyledInfoBox = styled.div` display: flex; - flex-direction: column; - padding-top: ${({ theme }) => theme.space.md}; - padding-left: ${({ theme }) => theme.space.md}; - padding-right: ${({ theme }) => theme.space.md}; - padding-bottom: ${({ theme }) => theme.space.lg}; - box-shadow: ${({ theme }) => theme.shadows.boxShadow()}; + align-items: center; + margin-top: ${appTheme.space.sm}; + gap: ${appTheme.space.xxs}; `; -const StyledCardHeader = styled.div` +const BarContainer = styled.div` display: flex; - align-items: center; justify-content: space-between; - width: 100%; - padding-bottom: ${({ theme }) => theme.space.md}; + flex: 1; + align-items: center; + padding: ${({ theme }) => theme.space.xs} 0; `; -const AdditionalTarget = () => { +const getWordCount = (text: string) => text.split(/\s+/).filter(Boolean).length; + +const MIN_WORDS = 4; + +const sanitizeText = (text: string): string => + // eslint-disable-next-line no-control-regex + text.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, '').trim(); + +const stripHtml = (html: string): string => { + const div = document.createElement('div'); + div.innerHTML = html; + return div.textContent ?? div.innerText ?? ''; +}; + +const AdditionalTargetContent = () => { const { value, setOutput, remove } = useModule('additional_target'); const { getPlanStatus } = useModuleConfiguration(); const { t } = useTranslation(); - const [isOpenDeleteModal, setIsOpenDeleteModal] = useState(false); const Icon = useIconWithValidation(); + const [isOpenDeleteModal, setIsOpenDeleteModal] = useState(false); + const { addToast } = useToast(); + const aiButtonRef = useRef(null); + const { + modalRef, + setModalRef, + setEditorContent, + editorContent, + setAiSuggestion, + setIsAiLoading, + setAiError, + aiSuggestion, + isAiLoading, + aiError, + generateSuggestion, + acceptSuggestion, + registerGenerateSuggestion, + registerAcceptSuggestion, + } = useModuleAdditionalTargetContext(); const { width } = useWindowSize(); + const [generateAISuggestion] = usePostAiJobsMutation(); const breakpointSm = parseInt(appTheme.breakpoints.sm, 10); const isMobile = width < breakpointSm; + const editorRef = useRef(null); const validation = ( module: components['schemas']['ModuleAdditionalTarget'] @@ -57,7 +102,6 @@ const AdditionalTarget = () => { if (module.output.length > 512) { error = t('__PLAN_ADDITIONAL_TARGET_ERROR_TOO_LONG'); } - return error || true; }; @@ -65,92 +109,249 @@ const AdditionalTarget = () => { type: 'additional_target', validate: validation, }); + const handleBlur = () => { validate(); }; + const [wordCount, setWordCount] = useState(() => + getWordCount(stripHtml(value?.output ?? '')) + ); + const isGenerateDisabled = + wordCount < MIN_WORDS || getPlanStatus() !== 'draft'; + + const handleChange = (content: { + editor: { getHTML: () => string; getText: () => string }; + }) => { + const htmlContent = content.editor.getHTML(); + const strippedContent = sanitizeText(content.editor.getText()); + setOutput(htmlContent); + setWordCount(getWordCount(strippedContent)); + setEditorContent(strippedContent); + }; + + const handleAiSuggestion = async () => { + const editor = editorRef.current?.getEditor(); + const currentContent = editor + ? sanitizeText(editor.getText()) + : editorContent; + setModalRef(aiButtonRef.current); + setIsAiLoading(true); + setAiSuggestion(null); + setAiError(null); + try { + const response = await generateAISuggestion({ + body: { + action: 'improve-criteria', + target: 'criteria_controller_agent', + input: currentContent, + }, + }).unwrap(); + setAiSuggestion(response.output); + } catch (e: any) { + const message = + e instanceof Error + ? e.message + : e?.data?.message ?? e?.message ?? String(e); + setAiError(message); + } finally { + setIsAiLoading(false); + } + }; + + useEffect(() => { + registerGenerateSuggestion(handleAiSuggestion); + }, [editorContent, registerGenerateSuggestion, handleAiSuggestion]); + + const handleAcceptSuggestion = () => { + if (!aiSuggestion) return; + const editor = editorRef.current?.getEditor(); + if (editor) { + const sanitizedHtml = sanitizeText(aiSuggestion); + const plainText = sanitizeText(stripHtml(aiSuggestion)); + editor.commands.setContent(sanitizedHtml); + setOutput(sanitizedHtml); + setEditorContent(plainText); + setWordCount(getWordCount(plainText)); + } + setModalRef(null); + addToast( + ({ close }) => ( + + ), + { placement: 'top' } + ); + }; + + useEffect(() => { + registerAcceptSuggestion(handleAcceptSuggestion); + }, [aiSuggestion, registerAcceptSuggestion, handleAcceptSuggestion]); + const handleDelete = () => { setIsOpenDeleteModal(true); }; return ( <> - - -
- {Icon} - -
- {!isMobile && getPlanStatus() === 'draft' && ( - - { - handleDelete(); - e.stopPropagation(); - }} - > - - - - )} -
- <> -
- - {t('__FORM_OPTIONAL_LABEL')} -
- - {t('__PLAN_PAGE_MODULE_ADDITIONAL_TARGET_TEXTAREA_DESCRIPTION')} - -