feat: add nearest type

This commit is contained in:
Pavel Torbeev 2023-09-09 19:31:24 +03:00
parent 2ee63822cf
commit fc6a3d8f9c
8 changed files with 142 additions and 54 deletions

View File

@ -1,9 +1,17 @@
export type ScoreType = 'bert' | 'f';
export type DetailFeatures = [answer: string, metric: number, texts: string[]];
export type DetailDescriptor = {
text: string;
features: DetailFeatures;
};
export type ScoreType = 'bert' | 'f' | 'nearest';
export type ScoreDescriptor = {
[key in ScoreType]: {
text: string;
answer: string;
metric?: number;
// detailed?: DetailDescriptor[];
};
};
@ -15,7 +23,7 @@ export type TextDescriptor = {
[key in ScoreType]?: {
file?: string;
pdf?: string;
text: string;
text: string | DetailDescriptor[];
};
}
| null;

View File

@ -16,7 +16,6 @@ export interface ModalProps {
export const Modal = (props: ModalProps) => {
const { className, children, isOpen, onClose, isClosing, preventWindowScroll } = props;
const nodeRef = useRef(null);
usePreventWindowScroll(preventWindowScroll ?? isOpen);

View File

@ -2,7 +2,7 @@ import { useCallback, useEffect } from 'react';
export const usePreventWindowScroll = (isPrevented?: boolean) => {
const preventScroll = useCallback((isPrevented: boolean) => {
document.body.classList.toggle('scroll-prevented', isPrevented);
document.documentElement.classList.toggle('scroll-prevented', isPrevented);
}, []);
useEffect(() => {

View File

@ -69,7 +69,7 @@ export const ResponsePage: FC = () => {
<th>Имя</th>
<th>М. н-с.</th>
<th>М. стат.</th>
{/*<th>М. п.</th>*/}
<th>М. c.</th>
{/*<th>Рез.</th>*/}
<th>Крат. сод.</th>
</tr>
@ -88,6 +88,10 @@ export const ResponsePage: FC = () => {
{text.score.f.answer}
{/*| <span style={{ color: getPercentageColor(0.99) }}>0.99</span>*/}
</td>
<td>
{text.score.nearest.answer}
{/*| <span style={{ color: getPercentageColor(0.99) }}>0.99</span>*/}
</td>
{/*<td>*/}
{/* AA+ | <span style={{ color: getPercentageColor(0.95) }}>0.95</span>*/}
{/*</td>*/}
@ -107,34 +111,6 @@ export const ResponsePage: FC = () => {
</td>
</tr>
))}
{/*<tr>*/}
{/* <td>1</td>*/}
{/* <td>{EMDASH}</td>*/}
{/* <td>*/}
{/* AA+ | <span style={{ color: getPercentageColor(0.63) }}>0.63</span>*/}
{/* </td>*/}
{/* <td>*/}
{/* AA+ | <span style={{ color: getPercentageColor(0.95) }}>0.95</span>*/}
{/* </td>*/}
{/* <td>*/}
{/* AA+ | <span style={{ color: getPercentageColor(0.95) }}>0.95</span>*/}
{/* </td>*/}
{/* <td>*/}
{/* AA+ | <span style={{ color: getPercentageColor(0.95) }}>0.95</span>*/}
{/* </td>*/}
{/* <td className={s.ResponsePage__tableSummary}>*/}
{/* <Link*/}
{/* component={'button'}*/}
{/* standalone={false}*/}
{/* onClick={(e) => {*/}
{/* e.stopPropagation();*/}
{/* setIsOpen(true);*/}
{/* }}>*/}
{/* Открыть*/}
{/* </Link>*/}
{/* </td>*/}
{/*</tr>*/}
</tbody>
</table>
)}

View File

@ -133,3 +133,21 @@
--loader-size: 96px;
color: $color-background-secondary !important;
}
.TextPage__modalBody {
@include flex-col;
line-height: $line-height-24;
}
.TextPage__modalParagraph {
margin-top: $spacing-small-3x;
padding-top: $spacing-small-3x;
border-top: 1px solid $color-divider-darker;
}
:global {
.detailedText {
margin-bottom: 8px;
cursor: pointer;
}
}

View File

@ -1,4 +1,4 @@
import { FC, useEffect, useMemo, useRef } from 'react';
import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useForm } from 'react-hook-form';
import { Heading, HeadingSize } from '../../components/Heading';
import { useUrlParam } from '../../hooks/useUrlParam';
@ -7,10 +7,13 @@ import { ETextVariants, Text } from '../../components/Text';
import { Tooltip } from '../../components/Tooltip';
import { Link } from '../../components/Link';
import { useSingleTimeout } from '../../hooks/useSingleTimeout';
import { ScoreType, useText } from '../../api/process';
import { DetailDescriptor, ScoreType, useText } from '../../api/process';
import { Loader } from '../../components/Loader';
import { BACKEND_MEDIA_PORT, BACKEND_URL } from '../../config';
import { getPercentageColor } from '../../utils/getPercentageColor';
import { ModalBody, ModalContainer, useModal } from '../../components/Modal';
import { getEntriesFromText } from './utils/getEntriesFromText';
import { getTextFromDetailed } from './utils/getTextFromDetailed';
import s from './TextPage.module.scss';
export type TextFields = {
@ -31,6 +34,7 @@ export const TextPage: FC = () => {
});
const scoreType = watch('type');
const isDetailedScoreType = !(['bert', 'f'] as ScoreType[]).includes(scoreType);
const {
data: textEntity,
@ -42,13 +46,19 @@ export const TextPage: FC = () => {
config: {
enabled: !!textId,
refetchInterval: (data) =>
data?.description?.[scoreType]?.file && data?.description?.[scoreType]?.pdf ? false : TEXT_REFETCH_MS
// !isDetailedScoreType &&
!data?.description?.[scoreType]?.file || !data?.description?.[scoreType]?.pdf ? TEXT_REFETCH_MS : false
}
});
console.log(textEntity?.description?.nearest?.text);
const parsedText = useMemo(
() => getEntriesFromText(textEntity?.description?.[scoreType]?.text || ''),
[scoreType, textEntity]
() =>
isDetailedScoreType
? getTextFromDetailed(textEntity?.description?.nearest?.text as DetailDescriptor[])
: getEntriesFromText((textEntity?.description?.[scoreType]?.text as string) || ''),
[isDetailedScoreType, scoreType, textEntity]
);
const docxHref = textEntity?.description?.[scoreType]?.file
@ -64,11 +74,26 @@ export const TextPage: FC = () => {
const tipRef = useRef<HTMLDivElement>(null);
const timeout = useSingleTimeout();
// Модалка
const [isOpen, setIsOpen] = useState(false);
const [activeDetailedIndex, setActiveDetailedIndex] = useState<number>(-1);
const onClose = useCallback(() => {
setIsOpen(false);
}, []);
const { modalIsVisible, isClosing, close } = useModal({ isOpen, onClose });
const openDetailed = useCallback((index: number = -1) => {
setActiveDetailedIndex(index);
setIsOpen(true);
}, []);
const activeDetailed = (textEntity?.description?.nearest?.text as DetailDescriptor[])?.[activeDetailedIndex];
useEffect(() => {
if (!textRef.current) {
return;
}
// ------ Обработка хинтов ------
const resetTip = () => {
if (tipRef.current) {
tipRef.current.style.opacity = `0`;
@ -84,7 +109,7 @@ export const TextPage: FC = () => {
}
};
textRef.current.querySelectorAll('span').forEach((item) => {
textRef.current.querySelectorAll('.hintText').forEach((item) => {
item.addEventListener('mouseover', () => {
const rect = item.getBoundingClientRect();
const value = Number(item.getAttribute('data-value'));
@ -104,8 +129,18 @@ export const TextPage: FC = () => {
window.addEventListener('scroll', resetTip);
window.addEventListener('touchmove', resetTip);
// ------ Обработка текста с похожими текстами ------
textRef.current.querySelectorAll('.detailedText').forEach((item) => {
item.addEventListener('click', (e) => {
const index = Number(item.getAttribute('data-index')) ?? -1;
openDetailed(index);
});
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [parsedText]);
}, [parsedText, openDetailed]);
// ------ Обработка ошибки ------
@ -147,13 +182,21 @@ export const TextPage: FC = () => {
{/*| Accuracy: <span style={{ color: getPercentageColor(0.71) }}>0.71</span>*/}
</Text>
{/*<Text component={'div'} className={s.TextPage__prop} variant={ETextVariants.PROGRAMMING_CODE_REGULAR}>*/}
{/* Результат по методу{' '}*/}
{/* <Tooltip className={s.TextPage__tooltip} content={'Bert + Annoy'}>*/}
{/* <span className={s.TextPage__underline}>похожести</span>*/}
{/* </Tooltip>*/}
{/* : АА+ | Accuracy: <span style={{ color: getPercentageColor(0.63) }}>0.63</span>*/}
{/*</Text>*/}
<Text component={'div'} className={s.TextPage__prop} variant={ETextVariants.PROGRAMMING_CODE_REGULAR}>
Результат по методу{' '}
<Tooltip className={s.TextPage__tooltip} content={'Bert + Annoy'}>
<span className={s.TextPage__underline}>схожести</span>
</Tooltip>
: {textEntity.score.nearest.answer}
{textEntity.score.nearest.metric && (
<>
| Точность:{' '}
<span style={{ color: getPercentageColor(textEntity.score.nearest.metric / 100) }}>
{(textEntity.score.nearest.metric / 100).toFixed(2)}
</span>
</>
)}
</Text>
</div>
<div className={s.TextPage__summary}>
@ -177,7 +220,7 @@ export const TextPage: FC = () => {
<select className={s.TextPage__select} {...register('type')}>
<option value="bert">Нейросетевой</option>
<option value="f">Статистический</option>
{/*<option value="f">Схожести</option>*/}
<option value="nearest">Схожести</option>
</select>
<Link
@ -199,9 +242,16 @@ export const TextPage: FC = () => {
Скачать PDF
</Link>
{isDetailedScoreType ? (
<>
<div className={s.TextPage__fullText} dangerouslySetInnerHTML={{ __html: parsedText }} ref={textRef} />
</>
) : (
<>
<div className={s.TextPage__fullText} dangerouslySetInnerHTML={{ __html: parsedText }} ref={textRef} />
<div className={s.TextPage__textTip} ref={tipRef} />
</>
)}
</div>
</div>
) : (
@ -209,6 +259,29 @@ export const TextPage: FC = () => {
<Loader className={s.TextPage__loader} />
</div>
)}
<ModalContainer isOpen={modalIsVisible} onClose={close} isClosing={isClosing}>
<ModalBody className={s.TextPage__modalBody}>
{activeDetailed && (
<>
<p>
<b>Рейтинг {activeDetailed.features[0]}</b>
</p>
<p>
Точность{' '}
<span style={{ color: getPercentageColor(activeDetailed.features[1] / 100) }}>
{(activeDetailed.features[1] / 100).toFixed(2)}
</span>
</p>
{activeDetailed.features[2].map((text, index) => (
<p className={s.TextPage__modalParagraph} key={index}>
{text}
</p>
))}
</>
)}
</ModalBody>
</ModalContainer>
</div>
);
};

View File

@ -2,7 +2,7 @@ export const getColorFromValue = (value: number) => {
return `rgba(255, 255, 0, ${value > 0.1 ? value + 0.2 : 0})`;
};
export const getEntriesFromText = (html: string) => {
export const getEntriesFromText = (html: string = '') => {
let copiedHtml = html;
const matches = Array.from(html.matchAll(/<span[^>]+>(.*?)<\/span>/gi));
@ -13,7 +13,7 @@ export const getEntriesFromText = (html: string) => {
copiedHtml = copiedHtml.replace(
match[0],
`<span data-value="${value}" style="background-color: ${getColorFromValue(
`<span class="hintText" data-value="${value}" style="background-color: ${getColorFromValue(
value
)}; /*padding: 0 2px; margin: 0 -2px;*/">${text}</span>`
);

View File

@ -0,0 +1,14 @@
import { DetailDescriptor } from '../../../api/process';
import { getColorFromValue } from './getEntriesFromText';
export const getTextFromDetailed = (detailed: DetailDescriptor[] = []) => {
let html = '';
detailed.forEach((item, index) => {
const [_, metric] = item.features;
const color = getColorFromValue(metric / 100);
html += `<p class="detailedText" data-index="${index}" style="background-color: ${color};">${item.text}</p> `;
});
return html;
};