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 './types';
|
||||||
export * from './createProcess';
|
export * from './createProcess';
|
||||||
|
export * from './getProcess';
|
||||||
|
export * from './getText';
|
||||||
|
|
|
@ -1 +1,2 @@
|
||||||
export const QUERY_KEY_PROCESSES = 'processes';
|
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 = {
|
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 = {
|
||||||
|
|
|
@ -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}`;
|
||||||
|
|
|
@ -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));
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,6 +44,10 @@
|
||||||
&:active {
|
&:active {
|
||||||
color: $color-brand-active;
|
color: $color-brand-active;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.Link_disabled {
|
||||||
|
color: $color-brand-disabled;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.Link_underlined {
|
.Link_underlined {
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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={'Текст пресс-релиза...'}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 { 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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 {
|
||||||
|
@ -78,3 +85,51 @@
|
||||||
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;
|
||||||
|
}
|
|
@ -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
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