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 = { export type ScoreDescriptor = {
[key in ScoreType]: { [key in ScoreType]: {
text: string;
answer: string; answer: string;
metric?: number;
// detailed?: DetailDescriptor[];
}; };
}; };
@ -15,7 +23,7 @@ export type TextDescriptor = {
[key in ScoreType]?: { [key in ScoreType]?: {
file?: string; file?: string;
pdf?: string; pdf?: string;
text: string; text: string | DetailDescriptor[];
}; };
} }
| null; | null;

View File

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

View File

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

View File

@ -69,7 +69,7 @@ export const ResponsePage: FC = () => {
<th>Имя</th> <th>Имя</th>
<th>М. н-с.</th> <th>М. н-с.</th>
<th>М. стат.</th> <th>М. стат.</th>
{/*<th>М. п.</th>*/} <th>М. c.</th>
{/*<th>Рез.</th>*/} {/*<th>Рез.</th>*/}
<th>Крат. сод.</th> <th>Крат. сод.</th>
</tr> </tr>
@ -88,6 +88,10 @@ export const ResponsePage: FC = () => {
{text.score.f.answer} {text.score.f.answer}
{/*| <span style={{ color: getPercentageColor(0.99) }}>0.99</span>*/} {/*| <span style={{ color: getPercentageColor(0.99) }}>0.99</span>*/}
</td> </td>
<td>
{text.score.nearest.answer}
{/*| <span style={{ color: getPercentageColor(0.99) }}>0.99</span>*/}
</td>
{/*<td>*/} {/*<td>*/}
{/* AA+ | <span style={{ color: getPercentageColor(0.95) }}>0.95</span>*/} {/* AA+ | <span style={{ color: getPercentageColor(0.95) }}>0.95</span>*/}
{/*</td>*/} {/*</td>*/}
@ -107,34 +111,6 @@ export const ResponsePage: FC = () => {
</td> </td>
</tr> </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> </tbody>
</table> </table>
)} )}

View File

@ -132,4 +132,22 @@
.TextPage__loader { .TextPage__loader {
--loader-size: 96px; --loader-size: 96px;
color: $color-background-secondary !important; 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 { useForm } from 'react-hook-form';
import { Heading, HeadingSize } from '../../components/Heading'; import { Heading, HeadingSize } from '../../components/Heading';
import { useUrlParam } from '../../hooks/useUrlParam'; import { useUrlParam } from '../../hooks/useUrlParam';
@ -7,10 +7,13 @@ import { ETextVariants, Text } from '../../components/Text';
import { Tooltip } from '../../components/Tooltip'; import { Tooltip } from '../../components/Tooltip';
import { Link } from '../../components/Link'; import { Link } from '../../components/Link';
import { useSingleTimeout } from '../../hooks/useSingleTimeout'; import { useSingleTimeout } from '../../hooks/useSingleTimeout';
import { ScoreType, useText } from '../../api/process'; import { DetailDescriptor, ScoreType, useText } from '../../api/process';
import { Loader } from '../../components/Loader'; import { Loader } from '../../components/Loader';
import { BACKEND_MEDIA_PORT, BACKEND_URL } from '../../config'; 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 { getEntriesFromText } from './utils/getEntriesFromText';
import { getTextFromDetailed } from './utils/getTextFromDetailed';
import s from './TextPage.module.scss'; import s from './TextPage.module.scss';
export type TextFields = { export type TextFields = {
@ -31,6 +34,7 @@ export const TextPage: FC = () => {
}); });
const scoreType = watch('type'); const scoreType = watch('type');
const isDetailedScoreType = !(['bert', 'f'] as ScoreType[]).includes(scoreType);
const { const {
data: textEntity, data: textEntity,
@ -42,13 +46,19 @@ export const TextPage: FC = () => {
config: { config: {
enabled: !!textId, enabled: !!textId,
refetchInterval: (data) => 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( 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 const docxHref = textEntity?.description?.[scoreType]?.file
@ -64,11 +74,26 @@ export const TextPage: FC = () => {
const tipRef = useRef<HTMLDivElement>(null); const tipRef = useRef<HTMLDivElement>(null);
const timeout = useSingleTimeout(); 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(() => { useEffect(() => {
if (!textRef.current) { if (!textRef.current) {
return; return;
} }
// ------ Обработка хинтов ------
const resetTip = () => { const resetTip = () => {
if (tipRef.current) { if (tipRef.current) {
tipRef.current.style.opacity = `0`; 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', () => { item.addEventListener('mouseover', () => {
const rect = item.getBoundingClientRect(); const rect = item.getBoundingClientRect();
const value = Number(item.getAttribute('data-value')); const value = Number(item.getAttribute('data-value'));
@ -104,8 +129,18 @@ export const TextPage: FC = () => {
window.addEventListener('scroll', resetTip); window.addEventListener('scroll', resetTip);
window.addEventListener('touchmove', 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 // 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>*/} {/*| Accuracy: <span style={{ color: getPercentageColor(0.71) }}>0.71</span>*/}
</Text> </Text>
{/*<Text component={'div'} className={s.TextPage__prop} variant={ETextVariants.PROGRAMMING_CODE_REGULAR}>*/} <Text component={'div'} className={s.TextPage__prop} variant={ETextVariants.PROGRAMMING_CODE_REGULAR}>
{/* Результат по методу{' '}*/} Результат по методу{' '}
{/* <Tooltip className={s.TextPage__tooltip} content={'Bert + Annoy'}>*/} <Tooltip className={s.TextPage__tooltip} content={'Bert + Annoy'}>
{/* <span className={s.TextPage__underline}>похожести</span>*/} <span className={s.TextPage__underline}>схожести</span>
{/* </Tooltip>*/} </Tooltip>
{/* : АА+ | Accuracy: <span style={{ color: getPercentageColor(0.63) }}>0.63</span>*/} : {textEntity.score.nearest.answer}
{/*</Text>*/} {textEntity.score.nearest.metric && (
<>
| Точность:{' '}
<span style={{ color: getPercentageColor(textEntity.score.nearest.metric / 100) }}>
{(textEntity.score.nearest.metric / 100).toFixed(2)}
</span>
</>
)}
</Text>
</div> </div>
<div className={s.TextPage__summary}> <div className={s.TextPage__summary}>
@ -177,7 +220,7 @@ export const TextPage: FC = () => {
<select className={s.TextPage__select} {...register('type')}> <select className={s.TextPage__select} {...register('type')}>
<option value="bert">Нейросетевой</option> <option value="bert">Нейросетевой</option>
<option value="f">Статистический</option> <option value="f">Статистический</option>
{/*<option value="f">Схожести</option>*/} <option value="nearest">Схожести</option>
</select> </select>
<Link <Link
@ -199,9 +242,16 @@ export const TextPage: FC = () => {
Скачать PDF Скачать PDF
</Link> </Link>
<div className={s.TextPage__fullText} dangerouslySetInnerHTML={{ __html: parsedText }} ref={textRef} /> {isDetailedScoreType ? (
<>
<div className={s.TextPage__textTip} ref={tipRef} /> <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>
</div> </div>
) : ( ) : (
@ -209,6 +259,29 @@ export const TextPage: FC = () => {
<Loader className={s.TextPage__loader} /> <Loader className={s.TextPage__loader} />
</div> </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> </div>
); );
}; };

View File

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