feat: add text api

This commit is contained in:
Pavel Torbeev 2023-09-09 15:14:45 +03:00
parent b2285e263b
commit 5b47207bfd
19 changed files with 497 additions and 178 deletions

View File

@ -0,0 +1,34 @@
import { useQuery } from '@tanstack/react-query';
import { axios } from '../../lib/axios';
import { ExtractFnReturnType, QueryConfig } from '../../lib/react-query';
import { ProcessDescriptor, ScoreType, TextDescriptor } from './types';
import { PROCESS_API_URL, TEXT_API_URL, TEXT_PARAM } from './urlKeys';
import { QUERY_KEY_PROCESSES, QUERY_KEY_TEXTS } from './queryKeys';
export type GetTextResponse = TextDescriptor;
export const getText = ({ textId, type = 'bert' }: { textId: number; type: ScoreType }): Promise<GetTextResponse> => {
return axios.get(TEXT_API_URL.replace(`:${TEXT_PARAM}`, String(textId)), {
params: {
type
}
});
};
type QueryFnType = typeof getText;
type UseTextOptions = {
textId: number;
type: ScoreType;
config?: QueryConfig<QueryFnType>;
};
export const useText = ({ textId, type, config }: UseTextOptions) => {
return useQuery<ExtractFnReturnType<QueryFnType>>({
...config,
queryKey: [QUERY_KEY_TEXTS, textId, type],
queryFn: async () => {
return await getText({ textId, type });
}
});
};

View File

@ -1,2 +1,4 @@
export * from './types'; export * from './types';
export * from './createProcess'; export * from './createProcess';
export * from './getProcess';
export * from './getText';

View File

@ -1 +1,2 @@
export const QUERY_KEY_PROCESSES = 'processes'; export const QUERY_KEY_PROCESSES = 'processes';
export const QUERY_KEY_TEXTS = 'texts';

View File

@ -1,6 +1,27 @@
export type ScoreType = 'bert' | 'f';
export type ScoreDescriptor = {
[key in ScoreType]: {
text: string;
answer: string;
};
};
export type TextDescriptor = { export type TextDescriptor = {
score: string; id: number;
file_name: string;
description:
| {
[key in ScoreType]?: {
file?: string;
pdf?: string;
text: string;
};
}
| null;
text: string; text: string;
summary: string;
score: ScoreDescriptor;
}; };
export type ProcessDescriptor = { export type ProcessDescriptor = {

View File

@ -1,2 +1,5 @@
export const PROCESS_API_URL = '/process'; export const PROCESS_API_URL = '/process';
export const PROCESS_PARAM = 'processId'; export const PROCESS_PARAM = 'processId';
export const TEXT_PARAM = 'textId';
export const TEXT_API_URL = `/process/describe/:${TEXT_PARAM}`;

View File

@ -1,5 +1,6 @@
import { TEXT_PAGE_PARAM, TEXT_PAGE_ROUTE } from './routes'; import { RESPONSE_PAGE_PARAM, RESPONSE_PAGE_ROUTE, TEXT_PAGE_PARAM, TEXT_PAGE_ROUTE } from './routes';
export class PathBuilder { export class PathBuilder {
static getProcessPath = (id: string) => RESPONSE_PAGE_ROUTE.replace(`:${RESPONSE_PAGE_PARAM}`, String(id));
static getTextPath = (id: number) => TEXT_PAGE_ROUTE.replace(`:${TEXT_PAGE_PARAM}`, String(id)); static getTextPath = (id: number) => TEXT_PAGE_ROUTE.replace(`:${TEXT_PAGE_PARAM}`, String(id));
} }

View File

@ -44,6 +44,10 @@
&:active { &:active {
color: $color-brand-active; color: $color-brand-active;
} }
&.Link_disabled {
color: $color-brand-disabled;
}
} }
.Link_underlined { .Link_underlined {

View File

@ -7,5 +7,6 @@
@include media-down(tablet-small) { @include media-down(tablet-small) {
border-radius: 0 0 $radius-small $radius-small; border-radius: 0 0 $radius-small $radius-small;
@include text-body-s-regular;
} }
} }

View File

@ -3,5 +3,6 @@
// export const BACKEND_URL = 'https://ed68-77-234-219-9.ngrok-free.app'; // export const BACKEND_URL = 'https://ed68-77-234-219-9.ngrok-free.app';
// export const BACKEND_URL = 'https://16c2-77-234-219-9.ngrok-free.app'; // export const BACKEND_URL = 'https://16c2-77-234-219-9.ngrok-free.app';
export const BACKEND_URL = 'http://192.168.107.4'; export const BACKEND_URL = 'http://192.168.107.4';
export const BACKEND_MEDIA_PORT = '8000';
export const API_URL = BACKEND_URL + '/api'; export const API_URL = BACKEND_URL + '/api';

View File

@ -81,6 +81,10 @@ $textarea-height: 192px;
min-height: $textarea-height; min-height: $textarea-height;
} }
.HomePage__textareaInput {
min-height: 160px;
}
.HomePage__filesContainer { .HomePage__filesContainer {
@include flex-middle; @include flex-middle;
flex-wrap: wrap; flex-wrap: wrap;

View File

@ -1,6 +1,7 @@
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { SubmitHandler, useForm } from 'react-hook-form'; import { SubmitHandler, useForm } from 'react-hook-form';
import { useDropzone } from 'react-dropzone'; import { useDropzone } from 'react-dropzone';
import { useNavigate } from 'react-router-dom';
import clsx from 'clsx'; import clsx from 'clsx';
import { ReactFCC } from '../../utils/ReactFCC'; import { ReactFCC } from '../../utils/ReactFCC';
import { Heading, HeadingSize } from '../../components/Heading'; import { Heading, HeadingSize } from '../../components/Heading';
@ -14,6 +15,7 @@ import { useSingleTimeout } from '../../hooks/useSingleTimeout';
import { Upload } from '../../components/Upload'; import { Upload } from '../../components/Upload';
import { Attachment } from '../../components/Attachment'; import { Attachment } from '../../components/Attachment';
import { Loader } from '../../components/Loader'; import { Loader } from '../../components/Loader';
import { PathBuilder } from '../../app/routes';
import { ReactComponent as PlusIcon } from './assets/plus.svg'; import { ReactComponent as PlusIcon } from './assets/plus.svg';
import s from './HomePage.module.scss'; import s from './HomePage.module.scss';
@ -61,12 +63,10 @@ export const HomePage: ReactFCC = () => {
const onSubmit: SubmitHandler<FormFields> = useCallback( const onSubmit: SubmitHandler<FormFields> = useCallback(
async (data) => { async (data) => {
const response = await createProcess({ await createProcess({
text: data.text, text: data.text,
files: data.files files: data.files
}); });
// setProcessId(response.id);
}, },
[createProcess] [createProcess]
); );
@ -88,6 +88,14 @@ export const HomePage: ReactFCC = () => {
} }
}, [processId, refetchProcess, timeout]); }, [processId, refetchProcess, timeout]);
const navigate = useNavigate();
useEffect(() => {
if (processId && process && process.current === process.total) {
navigate(PathBuilder.getProcessPath(processId));
}
}, [navigate, process, processId]);
// ------ Обработка DnD ------ // ------ Обработка DnD ------
const currentText = watch('text'); const currentText = watch('text');
@ -141,6 +149,9 @@ export const HomePage: ReactFCC = () => {
{currentFiles.length === 0 ? ( {currentFiles.length === 0 ? (
<Textarea <Textarea
className={s.HomePage__textarea} className={s.HomePage__textarea}
classes={{
input: s.HomePage__textareaInput
}}
registration={register('text')} registration={register('text')}
rows={8} rows={8}
placeholder={'Текст пресс-релиза...'} placeholder={'Текст пресс-релиза...'}

View File

@ -1,7 +1,7 @@
@import 'src/app/styles/vars'; @import 'src/app/styles/vars';
.ResponsePage { .ResponsePage {
padding-bottom: $spacing-medium-x;
} }
.ResponsePage__title { .ResponsePage__title {
@ -13,7 +13,7 @@
@include mobile-down { @include mobile-down {
@include flex-col; @include flex-col;
row-gap: $spacing-small-2x; row-gap: $spacing-small-x;
} }
} }
@ -63,11 +63,11 @@
@include transition(border-color, color); @include transition(border-color, color);
&.ResponsePage__tableSummary { &.ResponsePage__tableSummary {
@include transition(opacity); //@include transition(opacity);
opacity: 0; //opacity: 0;
&:hover { &:hover {
opacity: 1; //opacity: 1;
} }
} }
@ -75,4 +75,12 @@
@include transition(color); @include transition(color);
} }
} }
}
.ResponsePage__modalBody {
line-height: $line-height-24;
@include mobile-down {
//line-height: $line-height-20;
}
} }

View File

@ -1,21 +1,47 @@
import { FC, useCallback, useState } from 'react'; import { FC, useCallback, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { Heading, HeadingSize } from '../../components/Heading'; import { Heading, HeadingSize } from '../../components/Heading';
import { Link } from '../../components/Link'; import { Link } from '../../components/Link';
import { getPercentageColor } from '../../utils/getPercentageColor'; import { getPercentageColor } from '../../utils/getPercentageColor';
import { EMDASH } from '../../utils/chars'; import { EMDASH } from '../../utils/chars';
import { ModalBody, useModal, ModalContainer } from '../../components/Modal'; import { ModalBody, useModal, ModalContainer } from '../../components/Modal';
import { useIsMobile } from '../../hooks/useIsMobile'; import { useIsMobile } from '../../hooks/useIsMobile';
import { useUrlParam } from '../../hooks/useUrlParam';
import { PathBuilder, RESPONSE_PAGE_PARAM } from '../../app/routes';
import { useProcess } from '../../api/process/getProcess';
import { TextItem } from './components'; import { TextItem } from './components';
import s from './ResponsePage.module.scss'; import s from './ResponsePage.module.scss';
export const ResponsePage: FC = () => { export const ResponsePage: FC = () => {
const processId = useUrlParam(RESPONSE_PAGE_PARAM);
const isMobile = useIsMobile();
const navigate = useNavigate();
// ------ Работа с данными ------
const { data } = useProcess({
processId: processId || '',
config: {
enabled: !!processId
}
});
const texts = data?.texts || [];
// ------ Обработка модалки с саммари ------
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const onClose = useCallback(() => { const onClose = useCallback(() => {
setIsOpen(false); setIsOpen(false);
}, []); }, []);
const { modalIsVisible, isClosing, close } = useModal({ isOpen, onClose }); const { modalIsVisible, isClosing, close } = useModal({ isOpen, onClose });
const isMobile = useIsMobile(); const [summaryText, setSummaryText] = useState('');
const openSummary = useCallback((text: string = '') => {
setSummaryText(text);
setIsOpen(true);
}, []);
return ( return (
<div className={s.ResponsePage}> <div className={s.ResponsePage}>
@ -26,8 +52,14 @@ export const ResponsePage: FC = () => {
<div className={s.ResponsePage__container}> <div className={s.ResponsePage__container}>
{isMobile ? ( {isMobile ? (
<> <>
<TextItem onClickSummary={() => setIsOpen(true)} /> {texts.map((text, index) => (
<TextItem /> <TextItem
text={text}
onClick={() => navigate(PathBuilder.getTextPath(text.id))}
onClickSummary={() => openSummary(text.summary)}
key={index}
/>
))}
</> </>
) : ( ) : (
<table className={s.ResponsePage__table}> <table className={s.ResponsePage__table}>
@ -37,71 +69,79 @@ export const ResponsePage: FC = () => {
<th>Имя</th> <th>Имя</th>
<th>М. н-с.</th> <th>М. н-с.</th>
<th>М. стат.</th> <th>М. стат.</th>
<th>М. п.</th> {/*<th>М. п.</th>*/}
<th>Рез.</th> {/*<th>Рез.</th>*/}
<th>Крат. сод.</th> <th>Крат. сод.</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr> {texts.map((text, index) => (
<td>1</td> <tr onClick={() => navigate(PathBuilder.getTextPath(text.id))} key={index}>
<td>file.txt</td> <td>{text.id}</td>
<td> <td>{text.file_name.length > 10 ? `${text.file_name.slice(0, 10)}...` : text.file_name}</td>
AA+ | <span style={{ color: getPercentageColor(0.63) }}>0.63</span> <td>
</td> {text.score.bert.answer}
<td> {/*| <span style={{ color: getPercentageColor(0.99) }}>0.99</span>*/}
AA+ | <span style={{ color: getPercentageColor(0.95) }}>0.95</span> </td>
</td> <td>
<td> {text.score.f.answer}
AA+ | <span style={{ color: getPercentageColor(0.95) }}>0.95</span> {/*| <span style={{ color: getPercentageColor(0.99) }}>0.99</span>*/}
</td> </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>*/}
<td className={s.ResponsePage__tableSummary}> {/*<td>*/}
<Link standalone={false}>Открыть</Link> {/* AA+ | <span style={{ color: getPercentageColor(0.95) }}>0.95</span>*/}
</td> {/*</td>*/}
</tr> <td className={s.ResponsePage__tableSummary}>
<Link
component={'button'}
standalone={false}
onClick={(e) => {
e.stopPropagation();
openSummary(text.summary);
}}>
Открыть
</Link>
</td>
</tr>
))}
<tr> {/*<tr>*/}
<td>1</td> {/* <td>1</td>*/}
<td>{EMDASH}</td> {/* <td>{EMDASH}</td>*/}
<td> {/* <td>*/}
AA+ | <span style={{ color: getPercentageColor(0.63) }}>0.63</span> {/* AA+ | <span style={{ color: getPercentageColor(0.63) }}>0.63</span>*/}
</td> {/* </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>*/}
<td> {/* <td>*/}
AA+ | <span style={{ color: getPercentageColor(0.95) }}>0.95</span> {/* AA+ | <span style={{ color: getPercentageColor(0.95) }}>0.95</span>*/}
</td> {/* </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>*/}
<td className={s.ResponsePage__tableSummary}> {/* <td className={s.ResponsePage__tableSummary}>*/}
<Link {/* <Link*/}
component={'button'} {/* component={'button'}*/}
standalone={false} {/* standalone={false}*/}
onClick={(e) => { {/* onClick={(e) => {*/}
e.stopPropagation(); {/* e.stopPropagation();*/}
setIsOpen(true); {/* setIsOpen(true);*/}
}}> {/* }}>*/}
Открыть {/* Открыть*/}
</Link> {/* </Link>*/}
</td> {/* </td>*/}
</tr> {/*</tr>*/}
</tbody> </tbody>
</table> </table>
)} )}
</div> </div>
<ModalContainer isOpen={modalIsVisible} onClose={close} isClosing={isClosing}> <ModalContainer isOpen={modalIsVisible} onClose={close} isClosing={isClosing}>
<ModalBody> <ModalBody className={s.ResponsePage__modalBody}>{summaryText}</ModalBody>
Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys
standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to
make a type specimen book.
</ModalBody>
</ModalContainer> </ModalContainer>
</div> </div>
); );

View File

@ -1,9 +1,16 @@
@import 'src/app/styles/vars'; @import 'src/app/styles/vars';
.TextItem { .TextItem {
@include transition(background-color);
background-color: $color-background-dark-100; background-color: $color-background-dark-100;
border-radius: $radius-medium; border-radius: $radius-medium;
padding: $spacing-small-2x; padding: $spacing-small-2x;
cursor: pointer;
&:hover {
background-color: $color-background-dark-200;
}
} }
.TextItem__row { .TextItem__row {
@ -18,13 +25,15 @@
.TextItem__row_name { .TextItem__row_name {
margin-bottom: $spacing-small-3x; margin-bottom: $spacing-small-3x;
overflow: hidden;
text-overflow: ellipsis;
} }
.TextItem__name { .TextItem__name {
//font-size: $font-size-18; //font-size: $font-size-18;
text-transform: uppercase; text-transform: uppercase;
font-weight: $font-weight-600; font-weight: $font-weight-600;
white-space: nowrap;
} }
.TextItem__row_link { .TextItem__row_link {

View File

@ -3,6 +3,7 @@ import { ReactFCC } from '../../../../utils/ReactFCC';
import { ETextVariants, Text } from '../../../../components/Text'; import { ETextVariants, Text } from '../../../../components/Text';
import { getPercentageColor } from '../../../../utils/getPercentageColor'; import { getPercentageColor } from '../../../../utils/getPercentageColor';
import { Link } from '../../../../components/Link'; import { Link } from '../../../../components/Link';
import { TextDescriptor } from '../../../../api/process';
import s from './TextItem.module.scss'; import s from './TextItem.module.scss';
export interface TextItemProps { export interface TextItemProps {
@ -10,49 +11,51 @@ export interface TextItemProps {
* Дополнительный css-класс * Дополнительный css-класс
*/ */
className?: string; className?: string;
text: TextDescriptor;
onClick?: () => void;
onClickSummary?: () => void; onClickSummary?: () => void;
} }
export const TextItem: ReactFCC<TextItemProps> = (props) => { export const TextItem: ReactFCC<TextItemProps> = (props) => {
const { className, onClickSummary } = props; const { className, text, onClick, onClickSummary } = props;
return ( return (
<div className={clsx(s.TextItem, className)}> <div className={clsx(s.TextItem, className)} onClick={onClick}>
<div className={clsx(s.TextItem__row, s.TextItem__row_name)}> <div className={clsx(s.TextItem__row, s.TextItem__row_name)}>
{/*<Text component={'span'} variant={ETextVariants.BODY_S_MEDIUM}>*/}
{/* Имя:*/}
{/*</Text>{' '}*/}
<Text className={s.TextItem__name} component={'span'} variant={ETextVariants.PROGRAMMING_CODE_REGULAR}> <Text className={s.TextItem__name} component={'span'} variant={ETextVariants.PROGRAMMING_CODE_REGULAR}>
file.txt #1 #{text.id} | {text.file_name}
</Text> </Text>
{/*{' | '}*/}
{/*<Text component={'span'} variant={ETextVariants.BODY_S_REGULAR}>*/}
{/* file.txt*/}
{/*</Text>*/}
</div> </div>
<div className={s.TextItem__row}> <div className={s.TextItem__row}>
<Text component={'span'} variant={ETextVariants.PROGRAMMING_CODE_REGULAR}> <Text component={'span'} variant={ETextVariants.PROGRAMMING_CODE_REGULAR}>
М. н-с: AA+ | <span style={{ color: getPercentageColor(0.63) }}>0.63</span> М. н-с: {text.score.bert.answer}
{/*| <span style={{ color: getPercentageColor(0.99) }}>0.99</span>*/}
</Text>{' '} </Text>{' '}
</div> </div>
<div className={s.TextItem__row}> <div className={s.TextItem__row}>
<Text component={'span'} variant={ETextVariants.PROGRAMMING_CODE_REGULAR}> <Text component={'span'} variant={ETextVariants.PROGRAMMING_CODE_REGULAR}>
М. стат: AA+ | <span style={{ color: getPercentageColor(0.63) }}>0.63</span> М. стат: {text.score.f.answer}
{/*| <span style={{ color: getPercentageColor(0.99) }}>0.99</span>*/}
</Text>{' '} </Text>{' '}
</div> </div>
<div className={s.TextItem__row}> {/*<div className={s.TextItem__row}>*/}
<Text component={'span'} variant={ETextVariants.PROGRAMMING_CODE_REGULAR}> {/* <Text component={'span'} variant={ETextVariants.PROGRAMMING_CODE_REGULAR}>*/}
М. п: AA+ | <span style={{ color: getPercentageColor(0.63) }}>0.63</span> {/* М. п: AA+ | <span style={{ color: getPercentageColor(0.63) }}>0.63</span>*/}
</Text>{' '} {/* </Text>{' '}*/}
</div> {/*</div>*/}
<div className={clsx(s.TextItem__row, s.TextItem__row_link)}> <div className={clsx(s.TextItem__row, s.TextItem__row_link)}>
<Text component={'span'} variant={ETextVariants.BODY_S_REGULAR}> <Text component={'span'} variant={ETextVariants.BODY_S_REGULAR}>
<Link component={'button'} standalone={false} onClick={() => onClickSummary?.()}> <Link
component={'button'}
standalone={false}
onClick={(e) => {
e.stopPropagation();
onClickSummary?.();
}}>
Краткое содержание Краткое содержание
</Link> </Link>
</Text> </Text>

View File

@ -1,7 +1,13 @@
@import 'src/app/styles/vars'; @import 'src/app/styles/vars';
.TextPage { .TextPage {
@include flex-col;
padding-bottom: $spacing-medium-x; padding-bottom: $spacing-medium-x;
height: 100%;
}
.TextPage__container {
width: 100%;
} }
.TextPage__title { .TextPage__title {
@ -57,6 +63,7 @@
} }
.TextPage__fullText { .TextPage__fullText {
@include text-body-m-regular;
line-height: $line-height-24; line-height: $line-height-24;
@include mobile-up { @include mobile-up {
@ -77,4 +84,52 @@
background-color: #efe1ae; background-color: #efe1ae;
padding: 0 2px; padding: 0 2px;
margin: 0 -2px; margin: 0 -2px;
}
.TextPage__selectLabel {
margin-bottom: $spacing-small-4x;
}
.TextPage__select {
@include reset-default-input;
padding: 0 ($spacing-small-2x - 1px);
color: $color-text-primary;
border: 1px solid $color-border-default;
border-radius: $radius-medium-x;
box-sizing: border-box;
height: 32px;
min-width: 200px;
margin-bottom: $spacing-small-x;
@include mobile-down {
width: 100%;
}
}
.TextPage__textTip {
@include transition(opacity);
@include text-body-s-regular;
pointer-events: none;
position: fixed;
top: 0;
left: 0;
visibility: hidden;
opacity: 0;
padding: 8px 12px;
border-radius: $radius-medium;
background-color: rgba($color-text-primary, 0.8);
color: $color-background-primary;
}
.TextPage__loaderContainer {
@include flex-col-middle;
justify-content: center;
row-gap: $spacing-small-4x;
height: 100%;
}
.TextPage__loader {
--loader-size: 96px;
color: $color-background-secondary !important;
} }

View File

@ -1,4 +1,5 @@
import { FC } from 'react'; import { FC, useEffect, useMemo, useRef } from 'react';
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';
import { TEXT_PAGE_PARAM } from '../../app/routes'; import { TEXT_PAGE_PARAM } from '../../app/routes';
@ -6,114 +7,209 @@ import { ETextVariants, Text } from '../../components/Text';
import { getPercentageColor } from '../../utils/getPercentageColor'; import { getPercentageColor } from '../../utils/getPercentageColor';
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 { ScoreType, useText } from '../../api/process';
import { Loader } from '../../components/Loader';
import { BACKEND_MEDIA_PORT, BACKEND_URL } from '../../config';
import { getEntriesFromText } from './utils/getEntriesFromText';
import s from './TextPage.module.scss'; import s from './TextPage.module.scss';
export type TextFields = {
type: ScoreType;
};
export const TEXT_REFETCH_MS = 2000;
export const TextPage: FC = () => { export const TextPage: FC = () => {
const textId = useUrlParam(TEXT_PAGE_PARAM, { parser: parseInt }); const textId = useUrlParam(TEXT_PAGE_PARAM, { parser: parseInt });
// ------ Работа с данными ------
const { register, watch } = useForm<TextFields>({
defaultValues: {
type: 'bert'
}
});
const scoreType = watch('type');
const {
data: textEntity,
isLoading,
error
} = useText({
textId: textId || 0,
type: scoreType,
config: {
enabled: !!textId,
refetchInterval: (data) =>
data?.description?.[scoreType]?.file && data?.description?.[scoreType]?.pdf ? false : TEXT_REFETCH_MS
}
});
const parsedText = useMemo(
() => getEntriesFromText(textEntity?.description?.[scoreType]?.text || ''),
[scoreType, textEntity]
);
const docxHref = textEntity?.description?.[scoreType]?.file
? `${BACKEND_URL}:${BACKEND_MEDIA_PORT}${textEntity?.description?.[scoreType]?.file}`
: undefined;
const pdfHref = textEntity?.description?.[scoreType]?.pdf
? `${BACKEND_URL}:${BACKEND_MEDIA_PORT}${textEntity?.description?.[scoreType]?.pdf}`
: undefined;
// ------ Обработка UI ------
const textRef = useRef<HTMLDivElement>(null);
const tipRef = useRef<HTMLDivElement>(null);
const timeout = useSingleTimeout();
useEffect(() => {
if (!textRef.current) {
return;
}
const resetTip = () => {
if (tipRef.current) {
tipRef.current.style.opacity = `0`;
timeout.set(() => {
if (tipRef.current) {
tipRef.current.style.top = `0px`;
tipRef.current.style.left = `0px`;
tipRef.current.style.visibility = `hidden`;
tipRef.current.innerText = ``;
}
}, 500);
}
};
textRef.current.querySelectorAll('span').forEach((item) => {
item.addEventListener('mouseover', () => {
const rect = item.getBoundingClientRect();
const value = Number(item.getAttribute('data-value'));
if (tipRef.current && value > 0.1) {
timeout.clear();
tipRef.current.style.top = `${rect.y - 24}px`;
tipRef.current.style.left = `${rect.x + 8}px`;
tipRef.current.style.visibility = `visible`;
tipRef.current.style.opacity = `1`;
tipRef.current.innerText = `Точность ${value.toFixed(2)}`;
}
});
item.addEventListener('mouseout', resetTip);
});
window.addEventListener('scroll', resetTip);
window.addEventListener('touchmove', resetTip);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [parsedText]);
// ------ Обработка ошибки ------
if (error) {
return null;
}
return ( return (
<div className={s.TextPage}> <div className={s.TextPage}>
<Heading size={HeadingSize.H2} className={s.TextPage__title}> {textEntity && !isLoading ? (
Результат обработки запроса {textId} <div className={s.TextPage__container}>
</Heading> <Heading size={HeadingSize.H2} className={s.TextPage__title}>
Результат обработки запроса {textEntity.id}
</Heading>
<div className={s.TextPage__props}> <div className={s.TextPage__props}>
<Text className={s.TextPage__prop} variant={ETextVariants.PROGRAMMING_CODE_REGULAR}> <Text className={s.TextPage__prop} variant={ETextVariants.PROGRAMMING_CODE_REGULAR}>
Имя файла: file.txt Имя файла: {textEntity.file_name}
</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={'Языковая модель (Berd)'}> <Tooltip className={s.TextPage__tooltip} content={'Языковая модель (Bert)'} placement={'right'}>
<span className={s.TextPage__underline}>нейросетевому</span> <span className={s.TextPage__underline}>нейросетевому</span>
</Tooltip>{' '} </Tooltip>{' '}
методу: АА+ | Accuracy: <span style={{ color: getPercentageColor(0.95) }}>0.95</span> методу: {textEntity.score.bert.answer}
</Text> {/*| Accuracy: <span style={{ color: getPercentageColor(0.95) }}>0.95</span>*/}
</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={'Лемматизация + TF/IDF + RandomForest'}> <Tooltip
<span className={s.TextPage__underline}>статистическому</span> className={s.TextPage__tooltip}
</Tooltip>{' '} content={'Лемматизация + TF/IDF + RandomForest'}
методу: АА+ | Accuracy: <span style={{ color: getPercentageColor(0.71) }}>0.71</span> placement={'right'}>
</Text> <span className={s.TextPage__underline}>статистическому</span>
</Tooltip>{' '}
методу: {textEntity.score.f.answer}
{/*| Accuracy: <span style={{ color: getPercentageColor(0.71) }}>0.71</span>*/}
</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={'Berd + 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> {/* : АА+ | Accuracy: <span style={{ color: getPercentageColor(0.63) }}>0.63</span>*/}
</Text> {/*</Text>*/}
</div> </div>
<div className={s.TextPage__summary}> <div className={s.TextPage__summary}>
<Heading size={HeadingSize.H4} className={s.TextPage__summaryHeading}> <Heading size={HeadingSize.H4} className={s.TextPage__summaryHeading}>
Summary Краткое содержание
</Heading> </Heading>
<Text className={s.TextPage__summaryText} variant={ETextVariants.BODY_M_REGULAR}> <Text className={s.TextPage__summaryText} variant={ETextVariants.BODY_M_REGULAR}>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore {textEntity.summary}
magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo </Text>
consequat. </div>
</Text>
</div>
<div className={s.TextPage__full}> <div className={s.TextPage__full}>
<Heading size={HeadingSize.H4} className={s.TextPage__fullHeading}> <Heading size={HeadingSize.H4} className={s.TextPage__fullHeading}>
Полный текст Полный текст
</Heading> </Heading>
<Link className={s.TextPage__summaryLink}>Скачать DOCX</Link> <Text className={s.TextPage__selectLabel} variant={ETextVariants.CAPTION_M_REGULAR}>
<Link className={s.TextPage__summaryLink}>Скачать PDF</Link> Метод
</Text>
<select className={s.TextPage__select} {...register('type')}>
<option value="bert">Нейросетевой</option>
<option value="f">Статистический</option>
{/*<option value="f">Схожести</option>*/}
</select>
<Text className={s.TextPage__fullText} variant={ETextVariants.BODY_M_REGULAR}> <Link
Повышение кредитного рейтинга Акционерного общества «Уральская сталь» (далее «Уральская сталь», Компания) component={'a'}
вызвано улучшением качественной оценки ликвидности в связи с рефинансированием краткосрочного банковского className={s.TextPage__summaryLink}
кредита посредством выпуска облигационного займа с погашением в 2025 году. Также{' '} href={docxHref}
<span className={s.TextPage__tag}>пересмотр стратегических планов</span> по реализации ряда инвестиционных target={'_blank'}
проектов способствовал улучшению показателя «капитальные затраты к выручке». Улучшение ценовой конъюнктуры на // download={textEntity.file_name}
мировом рынке чугуна обеспечило запуск доменной печи 3, находившейся ранее в резерве, что окажет disabled={!docxHref}>
дополнительное положительное влияние на денежный поток Компании в 2023 году. Кредитный рейтинг Компании Скачать DOCX
определяется средними рыночной позицией, бизнес-профилем и уровнем корпоративного управления, а также средней </Link>
оценкой за размер бизнеса. Показатели рентабельности, ликвидности, долговой нагрузки, обслуживания долга и <Link
денежного потока получили высокие оценки. «Уральская сталь» один из крупнейших в России производителей component={'a'}
товарного чугуна, мостостали и стали для производства труб большого диаметра (ТБД). В начале 2022 года className={s.TextPage__summaryLink}
Акционерное общество «Загорский трубный завод» ( рейтинг АКРА rating, прогноз «Стабильный» ; далее ЗТЗ) href={pdfHref}
<span className={s.TextPage__tag}>приобрело 100% уставного капитала</span> Компании у АО «ХК «МЕТАЛЛОИНВЕСТ» ( target={'_blank'}
рейтинг АКРА rating, прогноз «Стабильный» ). Ключевые факторы оценки Средняя оценка рыночной позиции // download={textEntity.file_name}
обусловлена оценкой рыночных позиций «Уральской стали» по основным видам продукции (мостосталь, штрипс и disabled={!pdfHref}>
чугун), взвешенных с учетом их доли в консолидированной выручке Компании. Средняя оценка бизнес-профиля Скачать PDF
Компании определяется: низкой оценкой степени вертикальной интеграции, которая отсутствует в Компании, </Link>
поскольку она не обеспечена собственными углем и железорудным сырьем; средней оценкой за долю продукции с
высокой добавленной стоимостью, которая учитывает сталь для ТБД и мостосталь как высокотехнологичные виды
продукции; средней оценкой за характеристику и диверсификацию рынков сбыта, так как рынки сбыта основной
продукции «Уральской стали» характеризуются умеренной цикличностью и насыщенностью, а продуктовый портфель
Компании умеренно диверсифицирован. Средняя оценка географической диверсификации является следствием наличия
экспорта чугуна, толстолистового проката и заготовки, доля которого формирует до{' '}
<span className={s.TextPage__tag}>50% консолидированной выручки</span>
Компании. С одной стороны, это обуславливает высокую оценку субфактора «доступность и диверсификация рынков
сбыта», а с другой очень низкую оценку субфактора «концентрация на одном заводе». Средний уровень
корпоративного управления обусловлен прозрачной структурой бизнеса и успешной реализацией Компанией стратегии
роста и расширения продуктового портфеля. Топ-менеджмент Компании представлен экспертами с большим опытом
работы в отрасли. «Уральская сталь» применяет отдельные элементы системы риск-менеджмента (например,
хеджирование валютного риска в определенных случаях), однако единые документы по стратегии и управлению
рисками, а также по дивидендной политике пока не утверждены. Совет директоров и ключевые комитеты пока не
сформированы. Структура бизнеса проста. Компания готовит отчетность по МСФО. Высокая оценка финансового
риск-профиля Компании обусловлена: высокой оценкой за рентабельность (рентабельность по FFO до процентов и
налогов за 2022 год составила 12% и ожидается АКРА на уровне{' '}
<span className={s.TextPage__tag}>около 18%</span> в 2023-м); высокой оценкой за обслуживание долга (отношение
FFO до чистых процентных платежей к процентным платежам составило 24,7х по результатам 2022 года и
прогнозируется АКРА на уровне около 11,7х в 2023-м); высокой оценкой за долговую нагрузку (отношение общего
долга с учетом поручительства по долгу ЗТЗ к FFO до чистых процентных платежей ожидается АКРА на уровне 2,5х
(0,8х без учета поручительств) по результатам 2023 года); средней оценкой размера бизнеса (абсолютное значение
годового FFO до чистых процентных платежей и налогов менее 30 млрд руб.). Высокая оценка уровня ликвидности.
</Text>
</div>
{/*<div className={s.TextPage__downloads}>*/} <div className={s.TextPage__fullText} dangerouslySetInnerHTML={{ __html: parsedText }} ref={textRef} />
{/* <a href="#">Скачать DOCX</a>*/}
{/*</div>*/} <div className={s.TextPage__textTip} ref={tipRef} />
</div>
</div>
) : (
<div className={s.TextPage__loaderContainer}>
<Loader className={s.TextPage__loader} />
</div>
)}
</div> </div>
); );
}; };

2
src/pages/text/text.ts Normal file

File diff suppressed because one or more lines are too long

View File

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