mirror of
https://github.com/magnum-opus-nn-cp/frontend.git
synced 2024-11-24 10:33:47 +03:00
feat: add text api
This commit is contained in:
parent
b2285e263b
commit
5b47207bfd
34
src/api/process/getText.ts
Normal file
34
src/api/process/getText.ts
Normal 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 });
|
||||
}
|
||||
});
|
||||
};
|
|
@ -1,2 +1,4 @@
|
|||
export * from './types';
|
||||
export * from './createProcess';
|
||||
export * from './getProcess';
|
||||
export * from './getText';
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
export const QUERY_KEY_PROCESSES = 'processes';
|
||||
export const QUERY_KEY_TEXTS = 'texts';
|
||||
|
|
|
@ -1,6 +1,27 @@
|
|||
export type ScoreType = 'bert' | 'f';
|
||||
|
||||
export type ScoreDescriptor = {
|
||||
[key in ScoreType]: {
|
||||
text: string;
|
||||
answer: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type TextDescriptor = {
|
||||
score: string;
|
||||
id: number;
|
||||
file_name: string;
|
||||
description:
|
||||
| {
|
||||
[key in ScoreType]?: {
|
||||
file?: string;
|
||||
pdf?: string;
|
||||
text: string;
|
||||
};
|
||||
}
|
||||
| null;
|
||||
text: string;
|
||||
summary: string;
|
||||
score: ScoreDescriptor;
|
||||
};
|
||||
|
||||
export type ProcessDescriptor = {
|
||||
|
|
|
@ -1,2 +1,5 @@
|
|||
export const PROCESS_API_URL = '/process';
|
||||
export const PROCESS_PARAM = 'processId';
|
||||
|
||||
export const TEXT_PARAM = 'textId';
|
||||
export const TEXT_API_URL = `/process/describe/:${TEXT_PARAM}`;
|
||||
|
|
|
@ -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 {
|
||||
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));
|
||||
}
|
||||
|
|
|
@ -44,6 +44,10 @@
|
|||
&:active {
|
||||
color: $color-brand-active;
|
||||
}
|
||||
|
||||
&.Link_disabled {
|
||||
color: $color-brand-disabled;
|
||||
}
|
||||
}
|
||||
|
||||
.Link_underlined {
|
||||
|
|
|
@ -7,5 +7,6 @@
|
|||
|
||||
@include media-down(tablet-small) {
|
||||
border-radius: 0 0 $radius-small $radius-small;
|
||||
@include text-body-s-regular;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,5 +3,6 @@
|
|||
// 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 = 'http://192.168.107.4';
|
||||
export const BACKEND_MEDIA_PORT = '8000';
|
||||
|
||||
export const API_URL = BACKEND_URL + '/api';
|
||||
|
|
|
@ -81,6 +81,10 @@ $textarea-height: 192px;
|
|||
min-height: $textarea-height;
|
||||
}
|
||||
|
||||
.HomePage__textareaInput {
|
||||
min-height: 160px;
|
||||
}
|
||||
|
||||
.HomePage__filesContainer {
|
||||
@include flex-middle;
|
||||
flex-wrap: wrap;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { SubmitHandler, useForm } from 'react-hook-form';
|
||||
import { useDropzone } from 'react-dropzone';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import clsx from 'clsx';
|
||||
import { ReactFCC } from '../../utils/ReactFCC';
|
||||
import { Heading, HeadingSize } from '../../components/Heading';
|
||||
|
@ -14,6 +15,7 @@ import { useSingleTimeout } from '../../hooks/useSingleTimeout';
|
|||
import { Upload } from '../../components/Upload';
|
||||
import { Attachment } from '../../components/Attachment';
|
||||
import { Loader } from '../../components/Loader';
|
||||
import { PathBuilder } from '../../app/routes';
|
||||
import { ReactComponent as PlusIcon } from './assets/plus.svg';
|
||||
import s from './HomePage.module.scss';
|
||||
|
||||
|
@ -61,12 +63,10 @@ export const HomePage: ReactFCC = () => {
|
|||
|
||||
const onSubmit: SubmitHandler<FormFields> = useCallback(
|
||||
async (data) => {
|
||||
const response = await createProcess({
|
||||
await createProcess({
|
||||
text: data.text,
|
||||
files: data.files
|
||||
});
|
||||
|
||||
// setProcessId(response.id);
|
||||
},
|
||||
[createProcess]
|
||||
);
|
||||
|
@ -88,6 +88,14 @@ export const HomePage: ReactFCC = () => {
|
|||
}
|
||||
}, [processId, refetchProcess, timeout]);
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
if (processId && process && process.current === process.total) {
|
||||
navigate(PathBuilder.getProcessPath(processId));
|
||||
}
|
||||
}, [navigate, process, processId]);
|
||||
|
||||
// ------ Обработка DnD ------
|
||||
|
||||
const currentText = watch('text');
|
||||
|
@ -141,6 +149,9 @@ export const HomePage: ReactFCC = () => {
|
|||
{currentFiles.length === 0 ? (
|
||||
<Textarea
|
||||
className={s.HomePage__textarea}
|
||||
classes={{
|
||||
input: s.HomePage__textareaInput
|
||||
}}
|
||||
registration={register('text')}
|
||||
rows={8}
|
||||
placeholder={'Текст пресс-релиза...'}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
@import 'src/app/styles/vars';
|
||||
|
||||
.ResponsePage {
|
||||
|
||||
padding-bottom: $spacing-medium-x;
|
||||
}
|
||||
|
||||
.ResponsePage__title {
|
||||
|
@ -13,7 +13,7 @@
|
|||
|
||||
@include mobile-down {
|
||||
@include flex-col;
|
||||
row-gap: $spacing-small-2x;
|
||||
row-gap: $spacing-small-x;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -63,11 +63,11 @@
|
|||
@include transition(border-color, color);
|
||||
|
||||
&.ResponsePage__tableSummary {
|
||||
@include transition(opacity);
|
||||
opacity: 0;
|
||||
//@include transition(opacity);
|
||||
//opacity: 0;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
//opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -76,3 +76,11 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ResponsePage__modalBody {
|
||||
line-height: $line-height-24;
|
||||
|
||||
@include mobile-down {
|
||||
//line-height: $line-height-20;
|
||||
}
|
||||
}
|
|
@ -1,21 +1,47 @@
|
|||
import { FC, useCallback, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Heading, HeadingSize } from '../../components/Heading';
|
||||
import { Link } from '../../components/Link';
|
||||
import { getPercentageColor } from '../../utils/getPercentageColor';
|
||||
import { EMDASH } from '../../utils/chars';
|
||||
import { ModalBody, useModal, ModalContainer } from '../../components/Modal';
|
||||
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 s from './ResponsePage.module.scss';
|
||||
|
||||
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 onClose = useCallback(() => {
|
||||
setIsOpen(false);
|
||||
}, []);
|
||||
const { modalIsVisible, isClosing, close } = useModal({ isOpen, onClose });
|
||||
|
||||
const isMobile = useIsMobile();
|
||||
const [summaryText, setSummaryText] = useState('');
|
||||
|
||||
const openSummary = useCallback((text: string = '') => {
|
||||
setSummaryText(text);
|
||||
setIsOpen(true);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={s.ResponsePage}>
|
||||
|
@ -26,8 +52,14 @@ export const ResponsePage: FC = () => {
|
|||
<div className={s.ResponsePage__container}>
|
||||
{isMobile ? (
|
||||
<>
|
||||
<TextItem onClickSummary={() => setIsOpen(true)} />
|
||||
<TextItem />
|
||||
{texts.map((text, index) => (
|
||||
<TextItem
|
||||
text={text}
|
||||
onClick={() => navigate(PathBuilder.getTextPath(text.id))}
|
||||
onClickSummary={() => openSummary(text.summary)}
|
||||
key={index}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<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>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>1</td>
|
||||
<td>file.txt</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 standalone={false}>Открыть</Link>
|
||||
</td>
|
||||
</tr>
|
||||
{texts.map((text, index) => (
|
||||
<tr onClick={() => navigate(PathBuilder.getTextPath(text.id))} key={index}>
|
||||
<td>{text.id}</td>
|
||||
<td>{text.file_name.length > 10 ? `${text.file_name.slice(0, 10)}...` : text.file_name}</td>
|
||||
<td>
|
||||
{text.score.bert.answer}
|
||||
{/*| <span style={{ color: getPercentageColor(0.99) }}>0.99</span>*/}
|
||||
</td>
|
||||
<td>
|
||||
{text.score.f.answer}
|
||||
{/*| <span style={{ color: getPercentageColor(0.99) }}>0.99</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();
|
||||
openSummary(text.summary);
|
||||
}}>
|
||||
Открыть
|
||||
</Link>
|
||||
</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>
|
||||
{/*<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>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ModalContainer isOpen={modalIsVisible} onClose={close} isClosing={isClosing}>
|
||||
<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>
|
||||
<ModalBody className={s.ResponsePage__modalBody}>{summaryText}</ModalBody>
|
||||
</ModalContainer>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,9 +1,16 @@
|
|||
@import 'src/app/styles/vars';
|
||||
|
||||
.TextItem {
|
||||
@include transition(background-color);
|
||||
|
||||
background-color: $color-background-dark-100;
|
||||
border-radius: $radius-medium;
|
||||
padding: $spacing-small-2x;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: $color-background-dark-200;
|
||||
}
|
||||
}
|
||||
|
||||
.TextItem__row {
|
||||
|
@ -18,13 +25,15 @@
|
|||
|
||||
.TextItem__row_name {
|
||||
margin-bottom: $spacing-small-3x;
|
||||
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.TextItem__name {
|
||||
//font-size: $font-size-18;
|
||||
text-transform: uppercase;
|
||||
font-weight: $font-weight-600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.TextItem__row_link {
|
||||
|
|
|
@ -3,6 +3,7 @@ import { ReactFCC } from '../../../../utils/ReactFCC';
|
|||
import { ETextVariants, Text } from '../../../../components/Text';
|
||||
import { getPercentageColor } from '../../../../utils/getPercentageColor';
|
||||
import { Link } from '../../../../components/Link';
|
||||
import { TextDescriptor } from '../../../../api/process';
|
||||
import s from './TextItem.module.scss';
|
||||
|
||||
export interface TextItemProps {
|
||||
|
@ -10,49 +11,51 @@ export interface TextItemProps {
|
|||
* Дополнительный css-класс
|
||||
*/
|
||||
className?: string;
|
||||
|
||||
text: TextDescriptor;
|
||||
onClick?: () => void;
|
||||
onClickSummary?: () => void;
|
||||
}
|
||||
|
||||
export const TextItem: ReactFCC<TextItemProps> = (props) => {
|
||||
const { className, onClickSummary } = props;
|
||||
const { className, text, onClick, onClickSummary } = props;
|
||||
|
||||
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)}>
|
||||
{/*<Text component={'span'} variant={ETextVariants.BODY_S_MEDIUM}>*/}
|
||||
{/* Имя:*/}
|
||||
{/*</Text>{' '}*/}
|
||||
<Text className={s.TextItem__name} component={'span'} variant={ETextVariants.PROGRAMMING_CODE_REGULAR}>
|
||||
file.txt #1
|
||||
#{text.id} | {text.file_name}
|
||||
</Text>
|
||||
{/*{' | '}*/}
|
||||
{/*<Text component={'span'} variant={ETextVariants.BODY_S_REGULAR}>*/}
|
||||
{/* file.txt*/}
|
||||
{/*</Text>*/}
|
||||
</div>
|
||||
|
||||
<div className={s.TextItem__row}>
|
||||
<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>{' '}
|
||||
</div>
|
||||
|
||||
<div className={s.TextItem__row}>
|
||||
<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>{' '}
|
||||
</div>
|
||||
|
||||
<div className={s.TextItem__row}>
|
||||
<Text component={'span'} variant={ETextVariants.PROGRAMMING_CODE_REGULAR}>
|
||||
М. п: AA+ | <span style={{ color: getPercentageColor(0.63) }}>0.63</span>
|
||||
</Text>{' '}
|
||||
</div>
|
||||
{/*<div className={s.TextItem__row}>*/}
|
||||
{/* <Text component={'span'} variant={ETextVariants.PROGRAMMING_CODE_REGULAR}>*/}
|
||||
{/* М. п: AA+ | <span style={{ color: getPercentageColor(0.63) }}>0.63</span>*/}
|
||||
{/* </Text>{' '}*/}
|
||||
{/*</div>*/}
|
||||
|
||||
<div className={clsx(s.TextItem__row, s.TextItem__row_link)}>
|
||||
<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>
|
||||
</Text>
|
||||
|
|
|
@ -1,7 +1,13 @@
|
|||
@import 'src/app/styles/vars';
|
||||
|
||||
.TextPage {
|
||||
@include flex-col;
|
||||
padding-bottom: $spacing-medium-x;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.TextPage__container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.TextPage__title {
|
||||
|
@ -57,6 +63,7 @@
|
|||
}
|
||||
|
||||
.TextPage__fullText {
|
||||
@include text-body-m-regular;
|
||||
line-height: $line-height-24;
|
||||
|
||||
@include mobile-up {
|
||||
|
@ -78,3 +85,51 @@
|
|||
padding: 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;
|
||||
}
|
|
@ -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 { useUrlParam } from '../../hooks/useUrlParam';
|
||||
import { TEXT_PAGE_PARAM } from '../../app/routes';
|
||||
|
@ -6,114 +7,209 @@ import { ETextVariants, Text } from '../../components/Text';
|
|||
import { getPercentageColor } from '../../utils/getPercentageColor';
|
||||
import { Tooltip } from '../../components/Tooltip';
|
||||
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';
|
||||
|
||||
export type TextFields = {
|
||||
type: ScoreType;
|
||||
};
|
||||
|
||||
export const TEXT_REFETCH_MS = 2000;
|
||||
|
||||
export const TextPage: FC = () => {
|
||||
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 (
|
||||
<div className={s.TextPage}>
|
||||
<Heading size={HeadingSize.H2} className={s.TextPage__title}>
|
||||
Результат обработки запроса №{textId}
|
||||
</Heading>
|
||||
{textEntity && !isLoading ? (
|
||||
<div className={s.TextPage__container}>
|
||||
<Heading size={HeadingSize.H2} className={s.TextPage__title}>
|
||||
Результат обработки запроса №{textEntity.id}
|
||||
</Heading>
|
||||
|
||||
<div className={s.TextPage__props}>
|
||||
<Text className={s.TextPage__prop} variant={ETextVariants.PROGRAMMING_CODE_REGULAR}>
|
||||
Имя файла: file.txt
|
||||
</Text>
|
||||
<div className={s.TextPage__props}>
|
||||
<Text className={s.TextPage__prop} variant={ETextVariants.PROGRAMMING_CODE_REGULAR}>
|
||||
Имя файла: {textEntity.file_name}
|
||||
</Text>
|
||||
|
||||
<Text component={'div'} className={s.TextPage__prop} variant={ETextVariants.PROGRAMMING_CODE_REGULAR}>
|
||||
Результат по{' '}
|
||||
<Tooltip className={s.TextPage__tooltip} content={'Языковая модель (Berd)'}>
|
||||
<span className={s.TextPage__underline}>нейросетевому</span>
|
||||
</Tooltip>{' '}
|
||||
методу: АА+ | Accuracy: <span style={{ color: getPercentageColor(0.95) }}>0.95</span>
|
||||
</Text>
|
||||
<Text component={'div'} className={s.TextPage__prop} variant={ETextVariants.PROGRAMMING_CODE_REGULAR}>
|
||||
Результат по{' '}
|
||||
<Tooltip className={s.TextPage__tooltip} content={'Языковая модель (Bert)'} placement={'right'}>
|
||||
<span className={s.TextPage__underline}>нейросетевому</span>
|
||||
</Tooltip>{' '}
|
||||
методу: {textEntity.score.bert.answer}
|
||||
{/*| Accuracy: <span style={{ color: getPercentageColor(0.95) }}>0.95</span>*/}
|
||||
</Text>
|
||||
|
||||
<Text component={'div'} className={s.TextPage__prop} variant={ETextVariants.PROGRAMMING_CODE_REGULAR}>
|
||||
Результат по{' '}
|
||||
<Tooltip className={s.TextPage__tooltip} content={'Лемматизация + TF/IDF + RandomForest'}>
|
||||
<span className={s.TextPage__underline}>статистическому</span>
|
||||
</Tooltip>{' '}
|
||||
методу: АА+ | 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={'Лемматизация + TF/IDF + RandomForest'}
|
||||
placement={'right'}>
|
||||
<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}>
|
||||
Результат по методу{' '}
|
||||
<Tooltip className={s.TextPage__tooltip} content={'Berd + Annoy'}>
|
||||
<span className={s.TextPage__underline}>похожести</span>
|
||||
</Tooltip>
|
||||
: АА+ | Accuracy: <span style={{ color: getPercentageColor(0.63) }}>0.63</span>
|
||||
</Text>
|
||||
</div>
|
||||
{/*<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>*/}
|
||||
</div>
|
||||
|
||||
<div className={s.TextPage__summary}>
|
||||
<Heading size={HeadingSize.H4} className={s.TextPage__summaryHeading}>
|
||||
Summary
|
||||
</Heading>
|
||||
<div className={s.TextPage__summary}>
|
||||
<Heading size={HeadingSize.H4} className={s.TextPage__summaryHeading}>
|
||||
Краткое содержание
|
||||
</Heading>
|
||||
|
||||
<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
|
||||
magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
|
||||
consequat.
|
||||
</Text>
|
||||
</div>
|
||||
<Text className={s.TextPage__summaryText} variant={ETextVariants.BODY_M_REGULAR}>
|
||||
{textEntity.summary}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<div className={s.TextPage__full}>
|
||||
<Heading size={HeadingSize.H4} className={s.TextPage__fullHeading}>
|
||||
Полный текст
|
||||
</Heading>
|
||||
<div className={s.TextPage__full}>
|
||||
<Heading size={HeadingSize.H4} className={s.TextPage__fullHeading}>
|
||||
Полный текст
|
||||
</Heading>
|
||||
|
||||
<Link className={s.TextPage__summaryLink}>Скачать DOCX</Link>
|
||||
<Link className={s.TextPage__summaryLink}>Скачать PDF</Link>
|
||||
<Text className={s.TextPage__selectLabel} variant={ETextVariants.CAPTION_M_REGULAR}>
|
||||
Метод
|
||||
</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}>
|
||||
Повышение кредитного рейтинга Акционерного общества «Уральская сталь» (далее — «Уральская сталь», Компания)
|
||||
вызвано улучшением качественной оценки ликвидности в связи с рефинансированием краткосрочного банковского
|
||||
кредита посредством выпуска облигационного займа с погашением в 2025 году. Также{' '}
|
||||
<span className={s.TextPage__tag}>пересмотр стратегических планов</span> по реализации ряда инвестиционных
|
||||
проектов способствовал улучшению показателя «капитальные затраты к выручке». Улучшение ценовой конъюнктуры на
|
||||
мировом рынке чугуна обеспечило запуск доменной печи №3, находившейся ранее в резерве, что окажет
|
||||
дополнительное положительное влияние на денежный поток Компании в 2023 году. Кредитный рейтинг Компании
|
||||
определяется средними рыночной позицией, бизнес-профилем и уровнем корпоративного управления, а также средней
|
||||
оценкой за размер бизнеса. Показатели рентабельности, ликвидности, долговой нагрузки, обслуживания долга и
|
||||
денежного потока получили высокие оценки. «Уральская сталь» — один из крупнейших в России производителей
|
||||
товарного чугуна, мостостали и стали для производства труб большого диаметра (ТБД). В начале 2022 года
|
||||
Акционерное общество «Загорский трубный завод» ( рейтинг АКРА — rating, прогноз «Стабильный» ; далее — ЗТЗ)
|
||||
<span className={s.TextPage__tag}>приобрело 100% уставного капитала</span> Компании у АО «ХК «МЕТАЛЛОИНВЕСТ» (
|
||||
рейтинг АКРА — rating, прогноз «Стабильный» ). Ключевые факторы оценки Средняя оценка рыночной позиции
|
||||
обусловлена оценкой рыночных позиций «Уральской стали» по основным видам продукции (мостосталь, штрипс и
|
||||
чугун), взвешенных с учетом их доли в консолидированной выручке Компании. Средняя оценка бизнес-профиля
|
||||
Компании определяется: низкой оценкой степени вертикальной интеграции, которая отсутствует в Компании,
|
||||
поскольку она не обеспечена собственными углем и железорудным сырьем; средней оценкой за долю продукции с
|
||||
высокой добавленной стоимостью, которая учитывает сталь для ТБД и мостосталь как высокотехнологичные виды
|
||||
продукции; средней оценкой за характеристику и диверсификацию рынков сбыта, так как рынки сбыта основной
|
||||
продукции «Уральской стали» характеризуются умеренной цикличностью и насыщенностью, а продуктовый портфель
|
||||
Компании умеренно диверсифицирован. Средняя оценка географической диверсификации является следствием наличия
|
||||
экспорта чугуна, толстолистового проката и заготовки, доля которого формирует до{' '}
|
||||
<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>
|
||||
<Link
|
||||
component={'a'}
|
||||
className={s.TextPage__summaryLink}
|
||||
href={docxHref}
|
||||
target={'_blank'}
|
||||
// download={textEntity.file_name}
|
||||
disabled={!docxHref}>
|
||||
Скачать DOCX
|
||||
</Link>
|
||||
<Link
|
||||
component={'a'}
|
||||
className={s.TextPage__summaryLink}
|
||||
href={pdfHref}
|
||||
target={'_blank'}
|
||||
// download={textEntity.file_name}
|
||||
disabled={!pdfHref}>
|
||||
Скачать PDF
|
||||
</Link>
|
||||
|
||||
{/*<div className={s.TextPage__downloads}>*/}
|
||||
{/* <a href="#">Скачать DOCX</a>*/}
|
||||
{/*</div>*/}
|
||||
<div className={s.TextPage__fullText} dangerouslySetInnerHTML={{ __html: parsedText }} ref={textRef} />
|
||||
|
||||
<div className={s.TextPage__textTip} ref={tipRef} />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className={s.TextPage__loaderContainer}>
|
||||
<Loader className={s.TextPage__loader} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
2
src/pages/text/text.ts
Normal file
2
src/pages/text/text.ts
Normal file
File diff suppressed because one or more lines are too long
23
src/pages/text/utils/getEntriesFromText.ts
Normal file
23
src/pages/text/utils/getEntriesFromText.ts
Normal 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;
|
||||
};
|
Loading…
Reference in New Issue
Block a user