Compare commits

...

9 Commits

Author SHA1 Message Date
Ivan
25029a212b B: Small dialog fixes
Some checks failed
Frontend CI / build (22.x) (push) Has been cancelled
2024-08-29 12:41:59 +03:00
Ivan
88652d57d3 F: Add xs size screen media query 2024-08-28 23:49:17 +03:00
Ivan
7790cc4ef2 B: Fix tooltip clipping after cst==null 2024-08-28 23:02:31 +03:00
Ivan
51ad937b99 B: Small UI fixes 2024-08-28 22:38:34 +03:00
Ivan
a7ad9b86f5 B: Fix library cache update after changing OSS 2024-08-28 21:31:57 +03:00
Ivan
800e492d89 M: Small graph fixes 2024-08-28 17:02:16 +03:00
Ivan
7555b2219d F: Add suggestions to substitution table 2024-08-28 15:42:57 +03:00
Ivan
0a5fb5eecf Add warning if term expressions are not equal 2024-08-28 12:33:47 +03:00
Ivan
49ff1c8f6c M: Small bug fixes 2024-08-27 23:59:43 +03:00
30 changed files with 510 additions and 246 deletions

View File

@ -83,7 +83,6 @@ export { LuNewspaper as IconDefinition } from 'react-icons/lu';
export { LuDna as IconTerminology } from 'react-icons/lu'; export { LuDna as IconTerminology } from 'react-icons/lu';
export { FaRegHandshake as IconConvention } from 'react-icons/fa6'; export { FaRegHandshake as IconConvention } from 'react-icons/fa6';
export { LiaCloneSolid as IconChild } from 'react-icons/lia'; export { LiaCloneSolid as IconChild } from 'react-icons/lia';
export { RiParentLine as IconParent } from 'react-icons/ri';
export { TbTopologyRing as IconConsolidation } from 'react-icons/tb'; export { TbTopologyRing as IconConsolidation } from 'react-icons/tb';
export { BiSpa as IconPredecessor } from 'react-icons/bi'; export { BiSpa as IconPredecessor } from 'react-icons/bi';
export { LuArchive as IconArchive } from 'react-icons/lu'; export { LuArchive as IconArchive } from 'react-icons/lu';

View File

@ -5,7 +5,7 @@ import { toast } from 'react-toastify';
import BadgeConstituenta from '@/components/info/BadgeConstituenta'; import BadgeConstituenta from '@/components/info/BadgeConstituenta';
import SelectConstituenta from '@/components/select/SelectConstituenta'; import SelectConstituenta from '@/components/select/SelectConstituenta';
import DataTable, { createColumnHelper } from '@/components/ui/DataTable'; import DataTable, { createColumnHelper, IConditionalStyle } from '@/components/ui/DataTable';
import MiniButton from '@/components/ui/MiniButton'; import MiniButton from '@/components/ui/MiniButton';
import { useConceptOptions } from '@/context/ConceptOptionsContext'; import { useConceptOptions } from '@/context/ConceptOptionsContext';
import { ILibraryItem } from '@/models/library'; import { ILibraryItem } from '@/models/library';
@ -13,13 +13,14 @@ import { ICstSubstitute, IMultiSubstitution } from '@/models/oss';
import { ConstituentaID, IConstituenta, IRSForm } from '@/models/rsform'; import { ConstituentaID, IConstituenta, IRSForm } from '@/models/rsform';
import { errors } from '@/utils/labels'; import { errors } from '@/utils/labels';
import { IconPageLeft, IconPageRight, IconRemove, IconReplace } from '../Icons'; import { IconAccept, IconPageLeft, IconPageRight, IconRemove, IconReplace } from '../Icons';
import NoData from '../ui/NoData'; import NoData from '../ui/NoData';
import SelectLibraryItem from './SelectLibraryItem'; import SelectLibraryItem from './SelectLibraryItem';
interface PickSubstitutionsProps { interface PickSubstitutionsProps {
substitutions: ICstSubstitute[]; substitutions: ICstSubstitute[];
setSubstitutions: React.Dispatch<React.SetStateAction<ICstSubstitute[]>>; setSubstitutions: React.Dispatch<React.SetStateAction<ICstSubstitute[]>>;
suggestions?: ICstSubstitute[];
prefixID: string; prefixID: string;
rows?: number; rows?: number;
@ -34,6 +35,7 @@ const columnHelper = createColumnHelper<IMultiSubstitution>();
function PickSubstitutions({ function PickSubstitutions({
substitutions, substitutions,
setSubstitutions, setSubstitutions,
suggestions,
prefixID, prefixID,
rows, rows,
schemas, schemas,
@ -55,6 +57,15 @@ function PickSubstitutions({
const [deleteRight, setDeleteRight] = useState(true); const [deleteRight, setDeleteRight] = useState(true);
const toggleDelete = () => setDeleteRight(prev => !prev); const toggleDelete = () => setDeleteRight(prev => !prev);
const [ignores, setIgnores] = useState<ICstSubstitute[]>([]);
const filteredSuggestions = useMemo(
() =>
suggestions?.filter(
item => !ignores.find(ignore => ignore.original === item.original && ignore.substitution === item.substitution)
) ?? [],
[ignores, suggestions]
);
const getSchemaByCst = useCallback( const getSchemaByCst = useCallback(
(id: ConstituentaID): IRSForm | undefined => { (id: ConstituentaID): IRSForm | undefined => {
for (const schema of schemas) { for (const schema of schemas) {
@ -82,14 +93,23 @@ function PickSubstitutions({
); );
const substitutionData: IMultiSubstitution[] = useMemo( const substitutionData: IMultiSubstitution[] = useMemo(
() => () => [
substitutions.map(item => ({ ...substitutions.map(item => ({
original_source: getSchemaByCst(item.original)!, original_source: getSchemaByCst(item.original)!,
original: getConstituenta(item.original)!, original: getConstituenta(item.original)!,
substitution: getConstituenta(item.substitution)!, substitution: getConstituenta(item.substitution)!,
substitution_source: getSchemaByCst(item.substitution)! substitution_source: getSchemaByCst(item.substitution)!,
is_suggestion: false
})), })),
[getConstituenta, getSchemaByCst, substitutions] ...filteredSuggestions.map(item => ({
original_source: getSchemaByCst(item.original)!,
original: getConstituenta(item.original)!,
substitution: getConstituenta(item.substitution)!,
substitution_source: getSchemaByCst(item.substitution)!,
is_suggestion: true
}))
],
[getConstituenta, getSchemaByCst, substitutions, filteredSuggestions]
); );
function addSubstitution() { function addSubstitution() {
@ -121,19 +141,34 @@ function PickSubstitutions({
setRightCst(undefined); setRightCst(undefined);
} }
const handleDeleteRow = useCallback( const handleDeclineSuggestion = useCallback(
(row: number) => { (item: IMultiSubstitution) => {
setIgnores(prev => [...prev, { original: item.original.id, substitution: item.substitution.id }]);
},
[setIgnores]
);
const handleAcceptSuggestion = useCallback(
(item: IMultiSubstitution) => {
setSubstitutions(prev => [...prev, { original: item.original.id, substitution: item.substitution.id }]);
},
[setSubstitutions]
);
const handleDeleteSubstitution = useCallback(
(target: IMultiSubstitution) => {
handleDeclineSuggestion(target);
setSubstitutions(prev => { setSubstitutions(prev => {
const newItems: ICstSubstitute[] = []; const newItems: ICstSubstitute[] = [];
prev.forEach((item, index) => { prev.forEach(item => {
if (index !== row) { if (item.original !== target.original.id || item.substitution !== target.substitution.id) {
newItems.push(item); newItems.push(item);
} }
}); });
return newItems; return newItems;
}); });
}, },
[setSubstitutions] [setSubstitutions, handleDeclineSuggestion]
); );
const columns = useMemo( const columns = useMemo(
@ -169,19 +204,47 @@ function PickSubstitutions({
}), }),
columnHelper.display({ columnHelper.display({
id: 'actions', id: 'actions',
cell: props => ( cell: props =>
<div className='max-w-fit'> props.row.original.is_suggestion ? (
<MiniButton <div className='max-w-fit'>
noHover <MiniButton
title='Удалить' noHover
icon={<IconRemove size='1rem' className='icon-red' />} title='Принять предложение'
onClick={() => handleDeleteRow(props.row.index)} icon={<IconAccept size='1rem' className='icon-green' />}
/> onClick={() => handleAcceptSuggestion(props.row.original)}
</div> />
) <MiniButton
noHover
title='Игнорировать предложение'
icon={<IconRemove size='1rem' className='icon-red' />}
onClick={() => handleDeclineSuggestion(props.row.original)}
/>
</div>
) : (
<div className='max-w-fit'>
<MiniButton
noHover
title='Удалить'
icon={<IconRemove size='1rem' className='icon-red' />}
onClick={() => handleDeleteSubstitution(props.row.original)}
/>
</div>
)
}) })
], ],
[handleDeleteRow, colors, prefixID] [handleDeleteSubstitution, handleDeclineSuggestion, handleAcceptSuggestion, colors, prefixID]
);
const conditionalRowStyles = useMemo(
(): IConditionalStyle<IMultiSubstitution>[] => [
{
when: (item: IMultiSubstitution) => item.is_suggestion,
style: {
backgroundColor: colors.bgOrange50
}
}
],
[colors]
); );
return ( return (
@ -265,6 +328,7 @@ function PickSubstitutions({
<p>Добавьте отождествление</p> <p>Добавьте отождествление</p>
</NoData> </NoData>
} }
conditionalRowStyles={conditionalRowStyles}
/> />
</div> </div>
); );

View File

@ -6,6 +6,7 @@ import Label from './Label';
export interface TextAreaProps extends CProps.Editor, CProps.Colors, CProps.TextArea { export interface TextAreaProps extends CProps.Editor, CProps.Colors, CProps.TextArea {
dense?: boolean; dense?: boolean;
noResize?: boolean; noResize?: boolean;
fitContent?: boolean;
} }
function TextArea({ function TextArea({
@ -18,6 +19,7 @@ function TextArea({
noOutline, noOutline,
noResize, noResize,
className, className,
fitContent,
colors = 'clr-input', colors = 'clr-input',
...restProps ...restProps
}: TextAreaProps) { }: TextAreaProps) {
@ -40,6 +42,7 @@ function TextArea({
'leading-tight', 'leading-tight',
'overflow-x-hidden overflow-y-auto', 'overflow-x-hidden overflow-y-auto',
{ {
'cc-fit-content': fitContent,
'resize-none': noResize, 'resize-none': noResize,
'border': !noBorder, 'border': !noBorder,
'flex-grow max-w-full': dense, 'flex-grow max-w-full': dense,

View File

@ -134,12 +134,13 @@ export const OssState = ({ itemID, children }: OssStateProps) => {
onError: setProcessingError, onError: setProcessingError,
onSuccess: () => { onSuccess: () => {
model.owner = newOwner; model.owner = newOwner;
library.localUpdateItem(model); library.reloadItems(() => {
if (callback) callback(); if (callback) callback();
});
} }
}); });
}, },
[itemID, model, library.localUpdateItem] [itemID, model, library.reloadItems]
); );
const setAccessPolicy = useCallback( const setAccessPolicy = useCallback(
@ -157,12 +158,13 @@ export const OssState = ({ itemID, children }: OssStateProps) => {
onError: setProcessingError, onError: setProcessingError,
onSuccess: () => { onSuccess: () => {
model.access_policy = newPolicy; model.access_policy = newPolicy;
library.localUpdateItem(model); library.reloadItems(() => {
if (callback) callback(); if (callback) callback();
});
} }
}); });
}, },
[itemID, model, library.localUpdateItem] [itemID, model, library.reloadItems]
); );
const setLocation = useCallback( const setLocation = useCallback(
@ -180,12 +182,13 @@ export const OssState = ({ itemID, children }: OssStateProps) => {
onError: setProcessingError, onError: setProcessingError,
onSuccess: () => { onSuccess: () => {
model.location = newLocation; model.location = newLocation;
library.localUpdateItem(model); library.reloadItems(() => {
if (callback) callback(); if (callback) callback();
});
} }
}); });
}, },
[itemID, model, library.localUpdateItem] [itemID, model, library.reloadItems]
); );
const setEditors = useCallback( const setEditors = useCallback(
@ -203,11 +206,13 @@ export const OssState = ({ itemID, children }: OssStateProps) => {
onError: setProcessingError, onError: setProcessingError,
onSuccess: () => { onSuccess: () => {
model.editors = newEditors; model.editors = newEditors;
if (callback) callback(); library.reloadItems(() => {
if (callback) callback();
});
} }
}); });
}, },
[itemID, model] [itemID, model, library.reloadItems]
); );
const savePositions = useCallback( const savePositions = useCallback(

View File

@ -59,14 +59,7 @@ function DlgChangeLocation({ hideWindow, initial, onChangeLocation }: DlgChangeL
onChange={handleSelectLocation} onChange={handleSelectLocation}
className='max-h-[9.2rem]' className='max-h-[9.2rem]'
/> />
<TextArea <TextArea id='dlg_cst_body' label='Путь' rows={3} value={body} onChange={event => setBody(event.target.value)} />
id='dlg_cst_body'
label='Путь'
className='w-[23rem]'
rows={3}
value={body}
onChange={event => setBody(event.target.value)}
/>
</Modal> </Modal>
); );
} }

View File

@ -128,7 +128,6 @@ function DlgCloneLibraryItem({ hideWindow, base, initialLocation, selected, tota
<TextArea <TextArea
id='dlg_cst_body' id='dlg_cst_body'
label='Путь' label='Путь'
className='w-[18rem]'
rows={3} rows={3}
value={body} value={body}
onChange={event => setBody(event.target.value)} onChange={event => setBody(event.target.value)}

View File

@ -9,6 +9,7 @@ import Modal, { ModalProps } from '@/components/ui/Modal';
import Overlay from '@/components/ui/Overlay'; import Overlay from '@/components/ui/Overlay';
import TabLabel from '@/components/ui/TabLabel'; import TabLabel from '@/components/ui/TabLabel';
import AnimateFade from '@/components/wrap/AnimateFade'; import AnimateFade from '@/components/wrap/AnimateFade';
import { useLibrary } from '@/context/LibraryContext';
import usePartialUpdate from '@/hooks/usePartialUpdate'; import usePartialUpdate from '@/hooks/usePartialUpdate';
import { HelpTopic } from '@/models/miscellaneous'; import { HelpTopic } from '@/models/miscellaneous';
import { CstType, ICstCreateData, IRSForm } from '@/models/rsform'; import { CstType, ICstCreateData, IRSForm } from '@/models/rsform';
@ -33,8 +34,10 @@ export enum TabID {
} }
function DlgConstituentaTemplate({ hideWindow, schema, onCreate, insertAfter }: DlgConstituentaTemplateProps) { function DlgConstituentaTemplate({ hideWindow, schema, onCreate, insertAfter }: DlgConstituentaTemplateProps) {
const { retrieveTemplate } = useLibrary();
const [activeTab, setActiveTab] = useState(TabID.TEMPLATE); const [activeTab, setActiveTab] = useState(TabID.TEMPLATE);
const [templateSchema, setTemplateSchema] = useState<IRSForm | undefined>(undefined);
const [template, updateTemplate] = usePartialUpdate<ITemplateState>({}); const [template, updateTemplate] = usePartialUpdate<ITemplateState>({});
const [substitutes, updateSubstitutes] = usePartialUpdate<IArgumentsState>({ const [substitutes, updateSubstitutes] = usePartialUpdate<IArgumentsState>({
definition: '', definition: '',
@ -43,7 +46,7 @@ function DlgConstituentaTemplate({ hideWindow, schema, onCreate, insertAfter }:
const [constituenta, updateConstituenta] = usePartialUpdate<ICstCreateData>({ const [constituenta, updateConstituenta] = usePartialUpdate<ICstCreateData>({
cst_type: CstType.TERM, cst_type: CstType.TERM,
insert_after: insertAfter ?? null, insert_after: insertAfter ?? null,
alias: '', alias: generateAlias(CstType.TERM, schema),
convention: '', convention: '',
definition_formal: '', definition_formal: '',
definition_raw: '', definition_raw: '',
@ -55,8 +58,12 @@ function DlgConstituentaTemplate({ hideWindow, schema, onCreate, insertAfter }:
const handleSubmit = () => onCreate(constituenta); const handleSubmit = () => onCreate(constituenta);
useLayoutEffect(() => { useLayoutEffect(() => {
updateConstituenta({ alias: generateAlias(constituenta.cst_type, schema) }); if (!template.templateID) {
}, [constituenta.cst_type, updateConstituenta, schema]); setTemplateSchema(undefined);
} else {
retrieveTemplate(template.templateID, setTemplateSchema);
}
}, [template.templateID, retrieveTemplate]);
useLayoutEffect(() => { useLayoutEffect(() => {
if (!template.prototype) { if (!template.prototype) {
@ -72,6 +79,7 @@ function DlgConstituentaTemplate({ hideWindow, schema, onCreate, insertAfter }:
} else { } else {
updateConstituenta({ updateConstituenta({
cst_type: template.prototype.cst_type, cst_type: template.prototype.cst_type,
alias: generateAlias(template.prototype.cst_type, schema),
definition_raw: template.prototype.definition_raw, definition_raw: template.prototype.definition_raw,
definition_formal: template.prototype.definition_formal, definition_formal: template.prototype.definition_formal,
term_raw: template.prototype.term_raw term_raw: template.prototype.term_raw
@ -85,7 +93,7 @@ function DlgConstituentaTemplate({ hideWindow, schema, onCreate, insertAfter }:
})) }))
}); });
} }
}, [template.prototype, updateConstituenta, updateSubstitutes]); }, [template.prototype, updateConstituenta, updateSubstitutes, schema]);
useLayoutEffect(() => { useLayoutEffect(() => {
if (substitutes.arguments.length === 0 || !template.prototype) { if (substitutes.arguments.length === 0 || !template.prototype) {
@ -95,12 +103,13 @@ function DlgConstituentaTemplate({ hideWindow, schema, onCreate, insertAfter }:
const type = inferTemplatedType(template.prototype.cst_type, substitutes.arguments); const type = inferTemplatedType(template.prototype.cst_type, substitutes.arguments);
updateConstituenta({ updateConstituenta({
cst_type: type, cst_type: type,
alias: generateAlias(type, schema),
definition_formal: definition definition_formal: definition
}); });
updateSubstitutes({ updateSubstitutes({
definition: definition definition: definition
}); });
}, [substitutes.arguments, template.prototype, updateConstituenta, updateSubstitutes]); }, [substitutes.arguments, template.prototype, updateConstituenta, updateSubstitutes, schema]);
useLayoutEffect(() => { useLayoutEffect(() => {
setValidated(!!template.prototype && validateNewAlias(constituenta.alias, constituenta.cst_type, schema)); setValidated(!!template.prototype && validateNewAlias(constituenta.alias, constituenta.cst_type, schema));
@ -109,10 +118,10 @@ function DlgConstituentaTemplate({ hideWindow, schema, onCreate, insertAfter }:
const templatePanel = useMemo( const templatePanel = useMemo(
() => ( () => (
<TabPanel> <TabPanel>
<TabTemplate state={template} partialUpdate={updateTemplate} /> <TabTemplate state={template} partialUpdate={updateTemplate} templateSchema={templateSchema} />
</TabPanel> </TabPanel>
), ),
[template, updateTemplate] [template, templateSchema, updateTemplate]
); );
const argumentsPanel = useMemo( const argumentsPanel = useMemo(

View File

@ -216,6 +216,7 @@ function TabArguments({ state, schema, partialUpdate }: TabArgumentsProps) {
<RSInput <RSInput
disabled disabled
noTooltip
id='result' id='result'
placeholder='Итоговое определение' placeholder='Итоговое определение'
className='mt-[1.2rem]' className='mt-[1.2rem]'

View File

@ -11,6 +11,7 @@ import { useLibrary } from '@/context/LibraryContext';
import { CATEGORY_CST_TYPE, IConstituenta, IRSForm } from '@/models/rsform'; import { CATEGORY_CST_TYPE, IConstituenta, IRSForm } from '@/models/rsform';
import { applyFilterCategory } from '@/models/rsformAPI'; import { applyFilterCategory } from '@/models/rsformAPI';
import { prefixes } from '@/utils/constants'; import { prefixes } from '@/utils/constants';
export interface ITemplateState { export interface ITemplateState {
templateID?: number; templateID?: number;
prototype?: IConstituenta; prototype?: IConstituenta;
@ -20,11 +21,11 @@ export interface ITemplateState {
interface TabTemplateProps { interface TabTemplateProps {
state: ITemplateState; state: ITemplateState;
partialUpdate: Dispatch<Partial<ITemplateState>>; partialUpdate: Dispatch<Partial<ITemplateState>>;
templateSchema?: IRSForm;
} }
function TabTemplate({ state, partialUpdate }: TabTemplateProps) { function TabTemplate({ state, partialUpdate, templateSchema }: TabTemplateProps) {
const { templates, retrieveTemplate } = useLibrary(); const { templates } = useLibrary();
const [templateSchema, setTemplateSchema] = useState<IRSForm | undefined>(undefined);
const [filteredData, setFilteredData] = useState<IConstituenta[]>([]); const [filteredData, setFilteredData] = useState<IConstituenta[]>([]);
@ -65,14 +66,6 @@ function TabTemplate({ state, partialUpdate }: TabTemplateProps) {
} }
}, [templates, state.templateID, partialUpdate]); }, [templates, state.templateID, partialUpdate]);
useEffect(() => {
if (!state.templateID) {
setTemplateSchema(undefined);
} else {
retrieveTemplate(state.templateID, setTemplateSchema);
}
}, [state.templateID, retrieveTemplate]);
useEffect(() => { useEffect(() => {
if (!templateSchema) { if (!templateSchema) {
return; return;

View File

@ -1,11 +1,11 @@
'use client'; 'use client';
import { useEffect, useLayoutEffect, useState } from 'react'; import { useState } from 'react';
import Modal, { ModalProps } from '@/components/ui/Modal'; import Modal, { ModalProps } from '@/components/ui/Modal';
import usePartialUpdate from '@/hooks/usePartialUpdate'; import usePartialUpdate from '@/hooks/usePartialUpdate';
import { CstType, ICstCreateData, IRSForm } from '@/models/rsform'; import { CstType, ICstCreateData, IRSForm } from '@/models/rsform';
import { generateAlias, validateNewAlias } from '@/models/rsformAPI'; import { generateAlias } from '@/models/rsformAPI';
import FormCreateCst from './FormCreateCst'; import FormCreateCst from './FormCreateCst';
@ -21,7 +21,7 @@ function DlgCreateCst({ hideWindow, initial, schema, onCreate }: DlgCreateCstPro
initial || { initial || {
cst_type: CstType.BASE, cst_type: CstType.BASE,
insert_after: null, insert_after: null,
alias: '', alias: generateAlias(CstType.BASE, schema),
convention: '', convention: '',
definition_formal: '', definition_formal: '',
definition_raw: '', definition_raw: '',
@ -32,14 +32,6 @@ function DlgCreateCst({ hideWindow, initial, schema, onCreate }: DlgCreateCstPro
const handleSubmit = () => onCreate(cstData); const handleSubmit = () => onCreate(cstData);
useLayoutEffect(() => {
updateCstData({ alias: generateAlias(cstData.cst_type, schema) });
}, [cstData.cst_type, updateCstData, schema]);
useEffect(() => {
setValidated(validateNewAlias(cstData.alias, cstData.cst_type, schema));
}, [cstData.alias, cstData.cst_type, schema]);
return ( return (
<Modal <Modal
header='Создание конституенты' header='Создание конституенты'

View File

@ -2,7 +2,7 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { AnimatePresence } from 'framer-motion'; import { AnimatePresence } from 'framer-motion';
import { useLayoutEffect, useMemo, useState } from 'react'; import { useCallback, useLayoutEffect, useMemo, useState } from 'react';
import BadgeHelp from '@/components/info/BadgeHelp'; import BadgeHelp from '@/components/info/BadgeHelp';
import RSInput from '@/components/RSInput'; import RSInput from '@/components/RSInput';
@ -33,7 +33,6 @@ function FormCreateCst({ schema, state, partialUpdate, setValidated }: FormCreat
const showConvention = useMemo(() => !!state.convention || forceComment || isBasic, [state, forceComment, isBasic]); const showConvention = useMemo(() => !!state.convention || forceComment || isBasic, [state, forceComment, isBasic]);
useLayoutEffect(() => { useLayoutEffect(() => {
partialUpdate({ alias: generateAlias(state.cst_type, schema) });
setForceComment(false); setForceComment(false);
}, [state.cst_type, partialUpdate, schema]); }, [state.cst_type, partialUpdate, schema]);
@ -43,44 +42,51 @@ function FormCreateCst({ schema, state, partialUpdate, setValidated }: FormCreat
} }
}, [state.alias, state.cst_type, schema, setValidated]); }, [state.alias, state.cst_type, schema, setValidated]);
const handleTypeChange = useCallback(
(target: CstType) => partialUpdate({ cst_type: target, alias: generateAlias(target, schema) }),
[partialUpdate, schema, generateAlias]
);
return ( return (
<AnimatePresence> <AnimatePresence>
<div key='dlg_cst_alias_picker' className='flex items-center self-center'> <div key='dlg_cst_alias_picker' className='flex items-center self-center gap-3'>
<SelectSingle <SelectSingle
id='dlg_cst_type' id='dlg_cst_type'
placeholder='Выберите тип' placeholder='Выберите тип'
className='w-[15rem]' className='w-[15rem]'
options={SelectorCstType} options={SelectorCstType}
value={{ value: state.cst_type, label: labelCstType(state.cst_type) }} value={{ value: state.cst_type, label: labelCstType(state.cst_type) }}
onChange={data => partialUpdate({ cst_type: data?.value ?? CstType.BASE })} onChange={data => handleTypeChange(data?.value ?? CstType.BASE)}
/>
<TextInput
id='dlg_cst_alias'
dense
label='Имя'
className='w-[7rem] mr-8'
value={state.alias}
onChange={event => partialUpdate({ alias: event.target.value })}
/> />
<BadgeHelp <BadgeHelp
topic={HelpTopic.CC_CONSTITUENTA} topic={HelpTopic.CC_CONSTITUENTA}
offset={16} offset={16}
className={clsx(PARAMETER.TOOLTIP_WIDTH, 'sm:max-w-[40rem]')} className={clsx(PARAMETER.TOOLTIP_WIDTH, 'sm:max-w-[40rem]')}
/> />
<TextInput
id='dlg_cst_alias'
dense
label='Имя'
className='w-[7rem] ml-3'
value={state.alias}
onChange={event => partialUpdate({ alias: event.target.value })}
/>
</div> </div>
<TextArea <TextArea
key='dlg_cst_term' key='dlg_cst_term'
id='dlg_cst_term' id='dlg_cst_term'
fitContent
spellCheck spellCheck
label='Термин' label='Термин'
placeholder='Обозначение, используемое в текстовых определениях' placeholder='Обозначение, используемое в текстовых определениях'
className='cc-fit-content max-h-[3.6rem]' className='max-h-[3.6rem]'
value={state.term_raw} value={state.term_raw}
onChange={event => partialUpdate({ term_raw: event.target.value })} onChange={event => partialUpdate({ term_raw: event.target.value })}
/> />
<AnimateFade key='dlg_cst_expression' hideContent={!state.definition_formal && isElementary}> <AnimateFade key='dlg_cst_expression' hideContent={!state.definition_formal && isElementary}>
<RSInput <RSInput
id='dlg_cst_expression' id='dlg_cst_expression'
noTooltip
label={ label={
state.cst_type === CstType.STRUCTURED state.cst_type === CstType.STRUCTURED
? 'Область определения' ? 'Область определения'
@ -102,9 +108,10 @@ function FormCreateCst({ schema, state, partialUpdate, setValidated }: FormCreat
<TextArea <TextArea
id='dlg_cst_definition' id='dlg_cst_definition'
spellCheck spellCheck
fitContent
label='Текстовое определение' label='Текстовое определение'
placeholder='Текстовая интерпретация формального выражения' placeholder='Текстовая интерпретация формального выражения'
className='cc-fit-content max-h-[3.6rem]' className='max-h-[3.6rem]'
value={state.definition_raw} value={state.definition_raw}
onChange={event => partialUpdate({ definition_raw: event.target.value })} onChange={event => partialUpdate({ definition_raw: event.target.value })}
/> />
@ -126,9 +133,10 @@ function FormCreateCst({ schema, state, partialUpdate, setValidated }: FormCreat
key='dlg_cst_convention' key='dlg_cst_convention'
id='dlg_cst_convention' id='dlg_cst_convention'
spellCheck spellCheck
fitContent
label={isBasic ? 'Конвенция' : 'Комментарий'} label={isBasic ? 'Конвенция' : 'Комментарий'}
placeholder={isBasic ? 'Договоренность об интерпретации' : 'Пояснение разработчика'} placeholder={isBasic ? 'Договоренность об интерпретации' : 'Пояснение разработчика'}
className='cc-fit-content max-h-[5.4rem]' className='max-h-[5.4rem]'
value={state.convention} value={state.convention}
onChange={event => partialUpdate({ convention: event.target.value })} onChange={event => partialUpdate({ convention: event.target.value })}
/> />

View File

@ -1,7 +1,7 @@
'use client'; 'use client';
import clsx from 'clsx'; import clsx from 'clsx';
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useLayoutEffect, useMemo, useState } from 'react';
import { TabList, TabPanel, Tabs } from 'react-tabs'; import { TabList, TabPanel, Tabs } from 'react-tabs';
import BadgeHelp from '@/components/info/BadgeHelp'; import BadgeHelp from '@/components/info/BadgeHelp';
@ -54,7 +54,10 @@ function DlgEditOperation({ hideWindow, oss, target, onSubmit }: DlgEditOperatio
() => inputOperations.map(operation => operation.result).filter(id => id !== null), () => inputOperations.map(operation => operation.result).filter(id => id !== null),
[inputOperations] [inputOperations]
); );
const [substitutions, setSubstitutions] = useState<ICstSubstitute[]>(target.substitutions); const [substitutions, setSubstitutions] = useState<ICstSubstitute[]>(target.substitutions);
const [suggestions, setSuggestions] = useState<ICstSubstitute[]>([]);
const cache = useRSFormCache(); const cache = useRSFormCache();
const schemas = useMemo( const schemas = useMemo(
() => schemasIDs.map(id => cache.getSchema(id)).filter(item => item !== undefined), () => schemasIDs.map(id => cache.getSchema(id)).filter(item => item !== undefined),
@ -63,11 +66,11 @@ function DlgEditOperation({ hideWindow, oss, target, onSubmit }: DlgEditOperatio
const canSubmit = useMemo(() => alias !== '', [alias]); const canSubmit = useMemo(() => alias !== '', [alias]);
useEffect(() => { useLayoutEffect(() => {
cache.preload(schemasIDs); cache.preload(schemasIDs);
}, [schemasIDs]); }, [schemasIDs]);
useEffect(() => { useLayoutEffect(() => {
if (cache.loading || schemas.length !== schemasIDs.length) { if (cache.loading || schemas.length !== schemasIDs.length) {
return; return;
} }
@ -86,13 +89,14 @@ function DlgEditOperation({ hideWindow, oss, target, onSubmit }: DlgEditOperatio
); );
}, [schemasIDs, schemas, cache.loading]); }, [schemasIDs, schemas, cache.loading]);
useEffect(() => { useLayoutEffect(() => {
if (cache.loading || schemas.length !== schemasIDs.length) { if (cache.loading || schemas.length !== schemasIDs.length) {
return; return;
} }
const validator = new SubstitutionValidator(schemas, substitutions); const validator = new SubstitutionValidator(schemas, substitutions);
setIsCorrect(validator.validate()); setIsCorrect(validator.validate());
setValidationText(validator.msg); setValidationText(validator.msg);
setSuggestions(validator.suggestions);
}, [substitutions, cache.loading, schemas, schemasIDs.length]); }, [substitutions, cache.loading, schemas, schemasIDs.length]);
const handleSubmit = useCallback(() => { const handleSubmit = useCallback(() => {
@ -151,10 +155,11 @@ function DlgEditOperation({ hideWindow, oss, target, onSubmit }: DlgEditOperatio
isCorrect={isCorrect} isCorrect={isCorrect}
substitutions={substitutions} substitutions={substitutions}
setSubstitutions={setSubstitutions} setSubstitutions={setSubstitutions}
suggestions={suggestions}
/> />
</TabPanel> </TabPanel>
), ),
[cache.loading, cache.error, substitutions, schemas, validationText, isCorrect] [cache.loading, cache.error, substitutions, suggestions, schemas, validationText, isCorrect]
); );
return ( return (

View File

@ -16,6 +16,7 @@ interface TabSynthesisProps {
schemas: IRSForm[]; schemas: IRSForm[];
substitutions: ICstSubstitute[]; substitutions: ICstSubstitute[];
setSubstitutions: React.Dispatch<React.SetStateAction<ICstSubstitute[]>>; setSubstitutions: React.Dispatch<React.SetStateAction<ICstSubstitute[]>>;
suggestions: ICstSubstitute[];
} }
function TabSynthesis({ function TabSynthesis({
@ -25,7 +26,8 @@ function TabSynthesis({
validationText, validationText,
isCorrect, isCorrect,
substitutions, substitutions,
setSubstitutions setSubstitutions,
suggestions
}: TabSynthesisProps) { }: TabSynthesisProps) {
const { colors } = useConceptOptions(); const { colors } = useConceptOptions();
return ( return (
@ -36,8 +38,14 @@ function TabSynthesis({
rows={10} rows={10}
substitutions={substitutions} substitutions={substitutions}
setSubstitutions={setSubstitutions} setSubstitutions={setSubstitutions}
suggestions={suggestions}
/>
<TextArea
disabled
value={validationText}
rows={4}
style={{ borderColor: isCorrect ? undefined : colors.fgRed }}
/> />
<TextArea disabled value={validationText} style={{ borderColor: isCorrect ? undefined : colors.fgRed }} />
</DataLoader> </DataLoader>
); );
} }

View File

@ -241,27 +241,27 @@ export class Graph {
topologicalOrder(): number[] { topologicalOrder(): number[] {
const result: number[] = []; const result: number[] = [];
const marked = new Map<number, boolean>(); const marked = new Set<number>();
const toVisit: number[] = []; const nodeStack: number[] = [];
this.nodes.forEach(node => { this.nodes.forEach(node => {
if (marked.get(node.id)) { if (marked.has(node.id)) {
return; return;
} }
toVisit.push(node.id); nodeStack.push(node.id);
while (toVisit.length > 0) { while (nodeStack.length > 0) {
const item = toVisit[toVisit.length - 1]; const item = nodeStack[nodeStack.length - 1];
if (marked.get(item)) { if (marked.has(item)) {
if (!result.find(id => id === item)) { if (!result.find(id => id === item)) {
result.push(item); result.push(item);
} }
toVisit.pop(); nodeStack.pop();
} else { } else {
marked.set(item, true); marked.add(item);
const itemNode = this.nodes.get(item); const itemNode = this.nodes.get(item);
if (itemNode && itemNode.outputs.length > 0) { if (itemNode && itemNode.outputs.length > 0) {
itemNode.outputs.forEach(child => { itemNode.outputs.forEach(child => {
if (!marked.get(child)) { if (!marked.has(child)) {
toVisit.push(child); nodeStack.push(child);
} }
}); });
} }

View File

@ -133,6 +133,7 @@ export interface IMultiSubstitution {
original: IConstituenta; original: IConstituenta;
substitution: IConstituenta; substitution: IConstituenta;
substitution_source: ILibraryItem; substitution_source: ILibraryItem;
is_suggestion: boolean;
} }
/** /**
@ -213,6 +214,7 @@ export enum SubstitutionErrorType {
typificationCycle, typificationCycle,
baseSubstitutionNotSet, baseSubstitutionNotSet,
unequalTypification, unequalTypification,
unequalExpressions,
unequalArgsCount, unequalArgsCount,
unequalArgs unequalArgs
} }

View File

@ -2,13 +2,14 @@
* Module: API for OperationSystem. * Module: API for OperationSystem.
*/ */
import { limits } from '@/utils/constants';
import { describeSubstitutionError, information } from '@/utils/labels'; import { describeSubstitutionError, information } from '@/utils/labels';
import { TextMatcher } from '@/utils/utils'; import { TextMatcher } from '@/utils/utils';
import { Graph } from './Graph'; import { Graph } from './Graph';
import { ILibraryItem, LibraryItemID } from './library'; import { ILibraryItem, LibraryItemID } from './library';
import { ICstSubstitute, IOperation, IOperationSchema, SubstitutionErrorType } from './oss'; import { ICstSubstitute, IOperation, IOperationSchema, SubstitutionErrorType } from './oss';
import { ConstituentaID, CstType, IConstituenta, IRSForm } from './rsform'; import { ConstituentaID, CstClass, CstType, IConstituenta, IRSForm } from './rsform';
import { AliasMapping, ParsingStatus } from './rslang'; import { AliasMapping, ParsingStatus } from './rslang';
import { applyAliasMapping, applyTypificationMapping, extractGlobals, isSetTypification } from './rslangAPI'; import { applyAliasMapping, applyTypificationMapping, extractGlobals, isSetTypification } from './rslangAPI';
@ -51,14 +52,20 @@ export function sortItemsForOSS(oss: IOperationSchema, items: ILibraryItem[]): I
type CrossMapping = Map<LibraryItemID, AliasMapping>; type CrossMapping = Map<LibraryItemID, AliasMapping>;
// TODO: test validator
/** /**
* Validator for Substitution table. * Validator for Substitution table.
*/ */
export class SubstitutionValidator { export class SubstitutionValidator {
public msg: string = ''; public msg: string = '';
public suggestions: ICstSubstitute[] = [];
private schemas: IRSForm[]; private schemas: IRSForm[];
private substitutions: ICstSubstitute[]; private substitutions: ICstSubstitute[];
private constituents = new Set<ConstituentaID>();
private originals = new Set<ConstituentaID>();
private mapping: CrossMapping = new Map();
private cstByID = new Map<ConstituentaID, IConstituenta>(); private cstByID = new Map<ConstituentaID, IConstituenta>();
private schemaByID = new Map<LibraryItemID, IRSForm>(); private schemaByID = new Map<LibraryItemID, IRSForm>();
private schemaByCst = new Map<ConstituentaID, IRSForm>(); private schemaByCst = new Map<ConstituentaID, IRSForm>();
@ -66,16 +73,36 @@ export class SubstitutionValidator {
constructor(schemas: IRSForm[], substitutions: ICstSubstitute[]) { constructor(schemas: IRSForm[], substitutions: ICstSubstitute[]) {
this.schemas = schemas; this.schemas = schemas;
this.substitutions = substitutions; this.substitutions = substitutions;
if (this.substitutions.length === 0) {
return;
}
schemas.forEach(schema => { schemas.forEach(schema => {
this.schemaByID.set(schema.id, schema); this.schemaByID.set(schema.id, schema);
this.mapping.set(schema.id, {});
schema.items.forEach(item => { schema.items.forEach(item => {
this.cstByID.set(item.id, item); this.cstByID.set(item.id, item);
this.schemaByCst.set(item.id, schema); this.schemaByCst.set(item.id, schema);
}); });
}); });
let index = limits.max_semantic_index;
substitutions.forEach(item => {
this.constituents.add(item.original);
this.constituents.add(item.substitution);
this.originals.add(item.original);
const original = this.cstByID.get(item.original);
const substitution = this.cstByID.get(item.substitution);
if (!original || !substitution) {
return;
}
index++;
const newAlias = `${substitution.alias[0]}${index}`;
this.mapping.get(original.schema)![original.alias] = newAlias;
this.mapping.get(substitution.schema)![substitution.alias] = newAlias;
});
} }
public validate(): boolean { public validate(): boolean {
this.calculateSuggestions();
if (this.substitutions.length === 0) { if (this.substitutions.length === 0) {
return this.setValid(); return this.setValid();
} }
@ -85,13 +112,62 @@ export class SubstitutionValidator {
if (!this.checkCycles()) { if (!this.checkCycles()) {
return false; return false;
} }
if (!this.checkTypifications()) { if (!this.checkSubstitutions()) {
return false; return false;
} }
return this.setValid(); return this.setValid();
} }
private calculateSuggestions(): void {
const candidates = new Map<ConstituentaID, string>();
const minors = new Set<ConstituentaID>();
const schemaByCst = new Map<ConstituentaID, IRSForm>();
for (const schema of this.schemas) {
for (const cst of schema.items) {
if (this.originals.has(cst.id)) {
continue;
}
if (cst.cst_class === CstClass.BASIC) {
continue;
}
const inputs = schema.graph.at(cst.id)!.inputs;
if (inputs.length === 0 || inputs.some(id => !this.constituents.has(id))) {
continue;
}
if (inputs.some(id => this.originals.has(id))) {
minors.add(cst.id);
}
candidates.set(cst.id, applyAliasMapping(cst.definition_formal, this.mapping.get(schema.id)!).replace(' ', ''));
schemaByCst.set(cst.id, schema);
}
}
for (const [key1, value1] of candidates) {
for (const [key2, value2] of candidates) {
if (key1 >= key2) {
continue;
}
if (schemaByCst.get(key1) === schemaByCst.get(key2)) {
continue;
}
if (value1 != value2) {
continue;
}
if (minors.has(key2)) {
this.suggestions.push({
original: key2,
substitution: key1
});
} else {
this.suggestions.push({
original: key1,
substitution: key2
});
}
}
}
}
private checkTypes(): boolean { private checkTypes(): boolean {
for (const item of this.substitutions) { for (const item of this.substitutions) {
const original = this.cstByID.get(item.original); const original = this.cstByID.get(item.original);
@ -204,7 +280,7 @@ export class SubstitutionValidator {
return true; return true;
} }
private checkTypifications(): boolean { private checkSubstitutions(): boolean {
const baseMappings = this.prepareBaseMappings(); const baseMappings = this.prepareBaseMappings();
const typeMappings = this.calculateSubstituteMappings(baseMappings); const typeMappings = this.calculateSubstituteMappings(baseMappings);
if (typeMappings === null) { if (typeMappings === null) {
@ -216,6 +292,13 @@ export class SubstitutionValidator {
continue; continue;
} }
const substitution = this.cstByID.get(item.substitution)!; const substitution = this.cstByID.get(item.substitution)!;
if (original.cst_type === substitution.cst_type && original.cst_class !== CstClass.BASIC) {
if (!this.checkEqual(original, substitution)) {
this.reportError(SubstitutionErrorType.unequalExpressions, [substitution.alias, original.alias]);
// Note: do not interrupt the validation process. Only warn about the problem.
}
}
const originalType = applyTypificationMapping( const originalType = applyTypificationMapping(
applyAliasMapping(original.parse.typification, baseMappings.get(original.schema)!), applyAliasMapping(original.parse.typification, baseMappings.get(original.schema)!),
typeMappings typeMappings
@ -287,7 +370,6 @@ export class SubstitutionValidator {
} else { } else {
substitutionText = applyAliasMapping(substitution.parse.typification, baseMappings.get(substitution.schema)!); substitutionText = applyAliasMapping(substitution.parse.typification, baseMappings.get(substitution.schema)!);
substitutionText = applyTypificationMapping(substitutionText, result); substitutionText = applyTypificationMapping(substitutionText, result);
console.log(substitutionText);
if (!isSetTypification(substitutionText)) { if (!isSetTypification(substitutionText)) {
this.reportError(SubstitutionErrorType.baseSubstitutionNotSet, [ this.reportError(SubstitutionErrorType.baseSubstitutionNotSet, [
substitution.alias, substitution.alias,
@ -310,13 +392,35 @@ export class SubstitutionValidator {
return result; return result;
} }
private checkEqual(left: IConstituenta, right: IConstituenta): boolean {
const schema1 = this.schemaByID.get(left.schema)!;
const inputs1 = schema1.graph.at(left.id)!.inputs;
if (inputs1.some(id => !this.constituents.has(id))) {
return false;
}
const schema2 = this.schemaByID.get(right.schema)!;
const inputs2 = schema2.graph.at(right.id)!.inputs;
if (inputs2.some(id => !this.constituents.has(id))) {
return false;
}
const expression1 = applyAliasMapping(left.definition_formal, this.mapping.get(schema1.id)!);
const expression2 = applyAliasMapping(right.definition_formal, this.mapping.get(schema2.id)!);
return expression1.replace(' ', '') === expression2.replace(' ', '');
}
private setValid(): boolean { private setValid(): boolean {
this.msg = information.substitutionsCorrect; if (this.msg.length > 0) {
this.msg += '\n';
}
this.msg += information.substitutionsCorrect;
return true; return true;
} }
private reportError(errorType: SubstitutionErrorType, params: string[]): boolean { private reportError(errorType: SubstitutionErrorType, params: string[]): boolean {
this.msg = describeSubstitutionError({ if (this.msg.length > 0) {
this.msg += '\n';
}
this.msg += describeSubstitutionError({
errorType: errorType, errorType: errorType,
params: params params: params
}); });

View File

@ -93,7 +93,7 @@ export function inferTemplate(expression: string): boolean {
* - `CstClass.DERIVED` if the CstType is TERM, FUNCTION, or PREDICATE. * - `CstClass.DERIVED` if the CstType is TERM, FUNCTION, or PREDICATE.
* - `CstClass.STATEMENT` if the CstType is AXIOM or THEOREM. * - `CstClass.STATEMENT` if the CstType is AXIOM or THEOREM.
*/ */
export function inferClass(type: CstType, isTemplate: boolean): CstClass { export function inferClass(type: CstType, isTemplate: boolean = false): CstClass {
if (isTemplate) { if (isTemplate) {
return CstClass.TEMPLATE; return CstClass.TEMPLATE;
} }

View File

@ -1,4 +1,4 @@
import { extractGlobals, isSimpleExpression, splitTemplateDefinition } from './rslangAPI'; import { applyTypificationMapping, extractGlobals, isSimpleExpression, splitTemplateDefinition } from './rslangAPI';
const globalsData = [ const globalsData = [
['', ''], ['', ''],
@ -47,3 +47,24 @@ describe('Testing split template', () => {
expect(`${result.head}||${result.body}`).toBe(expected); expect(`${result.head}||${result.body}`).toBe(expected);
}); });
}); });
const typificationMappingData = [
['', '', '', ''],
['X1', 'X2', 'X1', 'X2'],
['X1', 'X2', '(X1)', '(X2)'],
['X1', 'X2', 'X1×X3', 'X2×X3'],
['X1', '(X1×X1)', 'X1', 'X1×X1'],
['X1', '(X1×X1)', '(X1)', '(X1×X1)'],
['X1', '(X1×X1)', '(X1×X2)', '((X1×X1)×X2)'],
['X1', '(X3)', '(X1)', '(X3)'],
['X1', '(X3)', '(X1×X1)', '((X3)×(X3))']
];
describe('Testing typification mapping', () => {
it.each(typificationMappingData)(
'Apply typification mapping %p',
(original: string, replacement: string, input: string, expected: string) => {
const result = applyTypificationMapping(input, { [original]: replacement });
expect(result).toBe(expected);
}
);
});

View File

@ -164,13 +164,55 @@ export function applyAliasMapping(target: string, mapping: AliasMapping): string
* Apply alias typification mapping. * Apply alias typification mapping.
*/ */
export function applyTypificationMapping(target: string, mapping: AliasMapping): string { export function applyTypificationMapping(target: string, mapping: AliasMapping): string {
const result = applyAliasMapping(target, mapping); const modified = applyAliasMapping(target, mapping);
if (result === target) { if (modified === target) {
return target; return target;
} }
// remove double parentheses const deleteBrackets: number[] = [];
// deal with () const positions: number[] = [];
const booleans: number[] = [];
let boolCount: number = 0;
let stackSize: number = 0;
for (let i = 0; i < modified.length; i++) {
const char = modified[i];
if (char === '') {
boolCount++;
continue;
}
if (char === '(') {
stackSize++;
positions.push(i);
booleans.push(boolCount);
}
boolCount = 0;
if (char === ')') {
if (
i < modified.length - 1 &&
modified[i + 1] === ')' &&
stackSize > 1 &&
positions[stackSize - 2] + booleans[stackSize - 1] + 1 === positions[stackSize - 1]
) {
deleteBrackets.push(i);
deleteBrackets.push(positions[stackSize - 2]);
}
if (i === modified.length - 1 && stackSize === 1 && positions[0] === 0) {
deleteBrackets.push(i);
deleteBrackets.push(positions[0]);
}
stackSize--;
positions.pop();
booleans.pop();
}
}
let result = '';
for (let i = 0; i < modified.length; i++) {
if (!deleteBrackets.includes(i)) {
result += modified[i];
}
}
return result; return result;
} }

View File

@ -9,16 +9,17 @@ import TopicItem from '../TopicItem';
function HelpMain() { function HelpMain() {
return ( return (
<div> <div className='text-justify'>
<h1>Портал</h1> <h1>Портал</h1>
<p> <p>
Портал позволяет анализировать предметные области, формально записывать системы определений и синтезировать их с Портал позволяет анализировать предметные области, формально записывать системы определений и синтезировать их с
помощью математического аппарата <LinkTopic text='Родов структур' topic={HelpTopic.RSLANG} /> помощью математического аппарата <LinkTopic text='Родов структур' topic={HelpTopic.RSLANG} />
</p> </p>
<p> <p>
Такие системы называются <b>Концептуальными схемами</b> и состоят из отдельных{' '} Такие системы называются <LinkTopic text='Концептуальными схемами' topic={HelpTopic.CC_SYSTEM} /> и состоят из
<LinkTopic text='Конституент' topic={HelpTopic.CC_CONSTITUENTA} />, обладающих уникальными обозначениями и отдельных <LinkTopic text='Конституент' topic={HelpTopic.CC_CONSTITUENTA} />, обладающих уникальными
формальными определениями обозначениями и формальными определениями. Концептуальные схемы могут быть получены в рамках операций синтеза в{' '}
<LinkTopic text='Операционной схеме синтеза' topic={HelpTopic.CC_OSS} />.
</p> </p>
<h2>Разделы Портала</h2> <h2>Разделы Портала</h2>
@ -69,8 +70,9 @@ function HelpMain() {
Портал разрабатывается <TextURL text='Центром Концепт' href={external_urls.concept} /> Портал разрабатывается <TextURL text='Центром Концепт' href={external_urls.concept} />
</p> </p>
<p> <p>
Портал поддерживает актуальные версии браузеров Chrome, Firefox, Safari. Убедитесь, что используете последнюю Портал поддерживает актуальные версии браузеров Chrome, Firefox, Safari, включая мобильные устройства.
версию браузера в случае возникновения визуальных ошибок или проблем с производительностью. Убедитесь, что используете последнюю версию браузера в случае возникновения визуальных ошибок или проблем с
производительностью.
</p> </p>
<p> <p>
Ваши пожелания по доработке, найденные ошибки и иные предложения можно направлять на email:{' '} Ваши пожелания по доработке, найденные ошибки и иные предложения можно направлять на email:{' '}

View File

@ -181,7 +181,7 @@ function HelpThesaurus() {
</li> </li>
<li>основа данного понятия понятие, на котором основано порождающее выражение данной конституенты;</li> <li>основа данного понятия понятие, на котором основано порождающее выражение данной конституенты;</li>
<li> <li>
порожденное понятие данным понятием понятие, определение которого является порождающим выражением, порожденное понятие данным понятием понятие, определением которого является порождающим выражением,
основанным на данном понятии. основанным на данном понятии.
</li> </li>
</ul> </ul>
@ -267,7 +267,7 @@ function HelpThesaurus() {
</li> </li>
<li> <li>
<IconSynthesis size='1rem' className='inline-icon' /> <IconSynthesis size='1rem' className='inline-icon' />
{'\u2009'}синтез концептуальных схем.ыф {'\u2009'}синтез концептуальных схем.
</li> </li>
</ul> </ul>

View File

@ -46,8 +46,9 @@ function HelpConceptOSS() {
<p> <p>
Операция синтеза в рамках ОСС задаются набором операций-аргументов и <b>таблицей отождествлений</b> понятий из Операция синтеза в рамках ОСС задаются набором операций-аргументов и <b>таблицей отождествлений</b> понятий из
КС, привязанных к выбранным аргументам. Таким образом{' '} КС, привязанных к выбранным аргументам. Таким образом{' '}
<LinkTopic text='конституенты' topic={HelpTopic.CC_CONSTITUENTA} /> в каждой КС разделяются на исходные <LinkTopic text='конституенты' topic={HelpTopic.CC_CONSTITUENTA} /> в каждой КС разделяются на исходные и
(дописанные), наследованные, отождествленные (удаляемые). наследованные. При формировании таблицы отождествлений пользователю предлагается синтезировать производные
понятия, выражения которых совпадают после проведения заданных отождествлений.
</p> </p>
<p> <p>
После задания аргументов и таблицы отождествления необходимо единожды{' '} После задания аргументов и таблицы отождествления необходимо единожды{' '}

View File

@ -5,7 +5,7 @@ import { AnimatePresence } from 'framer-motion';
import { useEffect, useLayoutEffect, useMemo, useState } from 'react'; import { useEffect, useLayoutEffect, useMemo, useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { IconChild, IconParent, IconSave } from '@/components/Icons'; import { IconChild, IconPredecessor, IconSave } from '@/components/Icons';
import RefsInput from '@/components/RefsInput'; import RefsInput from '@/components/RefsInput';
import MiniButton from '@/components/ui/MiniButton'; import MiniButton from '@/components/ui/MiniButton';
import Overlay from '@/components/ui/Overlay'; import Overlay from '@/components/ui/Overlay';
@ -128,7 +128,7 @@ function FormConstituenta({
} }
return ( return (
<AnimateFade className='mx-0 md:mx-auto'> <AnimateFade className='mx-0 md:mx-auto pt-[2rem] xs:pt-0'>
{state ? ( {state ? (
<ControlsOverlay <ControlsOverlay
disabled={disabled} disabled={disabled}
@ -161,7 +161,7 @@ function FormConstituenta({
{state ? ( {state ? (
<TextArea <TextArea
id='cst_typification' id='cst_typification'
className='cc-fit-content' fitContent
dense dense
noResize noResize
noBorder noBorder
@ -171,100 +171,103 @@ function FormConstituenta({
colors='clr-app clr-text-default' colors='clr-app clr-text-default'
/> />
) : null} ) : null}
<AnimatePresence> {state ? (
<AnimateFade key='cst_expression_fade' hideContent={!state || (!state?.definition_formal && isElementary)}> <AnimatePresence>
<EditorRSExpression <AnimateFade key='cst_expression_fade' hideContent={!state.definition_formal && isElementary}>
id='cst_expression' <EditorRSExpression
label={ id='cst_expression'
state?.cst_type === CstType.STRUCTURED label={
? 'Область определения' state.cst_type === CstType.STRUCTURED
: !!state && isFunctional(state.cst_type) ? 'Область определения'
? 'Определение функции' : isFunctional(state.cst_type)
: 'Формальное определение' ? 'Определение функции'
} : 'Формальное определение'
placeholder={ }
state?.cst_type !== CstType.STRUCTURED placeholder={
? 'Родоструктурное выражение' state.cst_type !== CstType.STRUCTURED
: 'Определение множества, которому принадлежат элементы родовой структуры' ? 'Родоструктурное выражение'
} : 'Определение множества, которому принадлежат элементы родовой структуры'
value={expression} }
activeCst={state} value={expression}
disabled={disabled || state?.is_inherited} activeCst={state}
toggleReset={toggleReset} disabled={disabled || state.is_inherited}
onChange={newValue => setExpression(newValue)} toggleReset={toggleReset}
setTypification={setTypification} onChange={newValue => setExpression(newValue)}
onOpenEdit={onOpenEdit} setTypification={setTypification}
/> onOpenEdit={onOpenEdit}
</AnimateFade>
<AnimateFade key='cst_definition_fade' hideContent={!state || (!state?.definition_raw && isElementary)}>
<RefsInput
id='cst_definition'
label='Текстовое определение'
placeholder='Текстовая интерпретация формального выражения'
minHeight='3.75rem'
maxHeight='8rem'
schema={schema}
onOpenEdit={onOpenEdit}
value={textDefinition}
initialValue={state?.definition_raw ?? ''}
resolved={state?.definition_resolved ?? ''}
disabled={disabled}
onChange={newValue => setTextDefinition(newValue)}
/>
</AnimateFade>
<AnimateFade key='cst_convention_fade' hideContent={!showConvention || !state}>
<TextArea
id='cst_convention'
className='cc-fit-content max-h-[8rem]'
spellCheck
label={isBasic ? 'Конвенция' : 'Комментарий'}
placeholder={isBasic ? 'Договоренность об интерпретации' : 'Пояснение разработчика'}
value={convention}
disabled={disabled || (isBasic && state?.is_inherited)}
onChange={event => setConvention(event.target.value)}
/>
</AnimateFade>
<AnimateFade key='cst_convention_button' hideContent={showConvention || (disabled && !processing)}>
<button
key='cst_disable_comment'
id='cst_disable_comment'
type='button'
tabIndex={-1}
className='self-start cc-label clr-text-url hover:underline'
onClick={() => setForceComment(true)}
>
Добавить комментарий
</button>
</AnimateFade>
{!disabled || processing ? (
<div className='self-center flex'>
<SubmitButton
key='cst_form_submit'
id='cst_form_submit'
text='Сохранить изменения'
disabled={disabled || !isModified}
icon={<IconSave size='1.25rem' />}
/> />
<Overlay position='top-[0.1rem] left-[0.4rem]' className='cc-icons'> </AnimateFade>
{state?.is_inherited_parent ? ( <AnimateFade key='cst_definition_fade' hideContent={!state.definition_raw && isElementary}>
<MiniButton <RefsInput
icon={<IconChild size='1.25rem' className='clr-text-red' />} id='cst_definition'
disabled label='Текстовое определение'
titleHtml='Внимание!</br> Конституента имеет потомков<br/> в операционной схеме синтеза' placeholder='Текстовая интерпретация формального выражения'
/> minHeight='3.75rem'
) : null} maxHeight='8rem'
{state?.is_inherited ? ( schema={schema}
<MiniButton onOpenEdit={onOpenEdit}
icon={<IconParent size='1.25rem' className='clr-text-red' />} value={textDefinition}
disabled initialValue={state.definition_raw}
titleHtml='Внимание!</br> Конституента является наследником<br/>' resolved={state.definition_resolved}
/> disabled={disabled}
) : null} onChange={newValue => setTextDefinition(newValue)}
</Overlay> />
</div> </AnimateFade>
) : null} <AnimateFade key='cst_convention_fade' hideContent={!showConvention}>
</AnimatePresence> <TextArea
id='cst_convention'
fitContent
className='max-h-[8rem]'
spellCheck
label={isBasic ? 'Конвенция' : 'Комментарий'}
placeholder={isBasic ? 'Договоренность об интерпретации' : 'Пояснение разработчика'}
value={convention}
disabled={disabled || (isBasic && state.is_inherited)}
onChange={event => setConvention(event.target.value)}
/>
</AnimateFade>
<AnimateFade key='cst_convention_button' hideContent={showConvention || (disabled && !processing)}>
<button
key='cst_disable_comment'
id='cst_disable_comment'
type='button'
tabIndex={-1}
className='self-start cc-label clr-text-url hover:underline'
onClick={() => setForceComment(true)}
>
Добавить комментарий
</button>
</AnimateFade>
{!disabled || processing ? (
<div className='self-center flex'>
<SubmitButton
key='cst_form_submit'
id='cst_form_submit'
text='Сохранить изменения'
disabled={disabled || !isModified}
icon={<IconSave size='1.25rem' />}
/>
<Overlay position='top-[0.1rem] left-[0.4rem]' className='cc-icons'>
{state.is_inherited_parent ? (
<MiniButton
icon={<IconPredecessor size='1.25rem' className='clr-text-red' />}
disabled
titleHtml='Внимание!</br> Конституента имеет потомков<br/> в операционной схеме синтеза'
/>
) : null}
{state.is_inherited ? (
<MiniButton
icon={<IconChild size='1.25rem' className='clr-text-red' />}
disabled
titleHtml='Внимание!</br> Конституента является наследником<br/>'
/>
) : null}
</Overlay>
</div>
) : null}
</AnimatePresence>
) : null}
</form> </form>
</AnimateFade> </AnimateFade>
); );

View File

@ -50,8 +50,8 @@ function ToolbarConstituenta({
return ( return (
<Overlay <Overlay
position='top-1 right-1/2 translate-x-1/2 sm:right-4 sm:translate-x-0 md:right-1/2 md:translate-x-1/2' position='top-1 right-1/2 translate-x-1/2 xs:right-4 xs:translate-x-0 md:right-1/2 md:translate-x-1/2'
className='cc-icons outline-none transition-all duration-500' className='cc-icons outline-none transition-all duration-500'
> >
{controller.schema && controller.schema?.oss.length > 0 ? ( {controller.schema && controller.schema?.oss.length > 0 ? (
<MiniSelectorOSS <MiniSelectorOSS

View File

@ -28,7 +28,7 @@ import ToolbarRSExpression from './ToolbarRSExpression';
interface EditorRSExpressionProps { interface EditorRSExpressionProps {
id?: string; id?: string;
activeCst?: IConstituenta; activeCst: IConstituenta;
value: string; value: string;
label: string; label: string;
placeholder?: string; placeholder?: string;
@ -70,13 +70,10 @@ function EditorRSExpression({
function handleChange(newValue: string) { function handleChange(newValue: string) {
onChange(newValue); onChange(newValue);
setIsModified(newValue !== activeCst?.definition_formal); setIsModified(newValue !== activeCst.definition_formal);
} }
function handleCheckExpression(callback?: (parse: IExpressionParse) => void) { function handleCheckExpression(callback?: (parse: IExpressionParse) => void) {
if (!activeCst) {
return;
}
const prefix = getDefinitionPrefix(activeCst); const prefix = getDefinitionPrefix(activeCst);
const expression = prefix + value; const expression = prefix + value;
parser.checkExpression(expression, activeCst, parse => { parser.checkExpression(expression, activeCst, parse => {
@ -99,7 +96,7 @@ function EditorRSExpression({
const onShowError = useCallback( const onShowError = useCallback(
(error: IRSErrorDescription) => { (error: IRSErrorDescription) => {
if (!activeCst || !rsInput.current) { if (!rsInput.current) {
return; return;
} }
const prefix = getDefinitionPrefix(activeCst); const prefix = getDefinitionPrefix(activeCst);
@ -136,7 +133,7 @@ function EditorRSExpression({
toast.error(errors.astFailed); toast.error(errors.astFailed);
} else { } else {
setSyntaxTree(parse.ast); setSyntaxTree(parse.ast);
setExpression(getDefinitionPrefix(activeCst!) + value); setExpression(getDefinitionPrefix(activeCst) + value);
setShowAST(true); setShowAST(true);
} }
}); });
@ -145,7 +142,7 @@ function EditorRSExpression({
const controls = useMemo( const controls = useMemo(
() => ( () => (
<RSEditorControls <RSEditorControls
isOpen={showControls && (!disabled || (model.processing && !activeCst?.is_inherited))} isOpen={showControls && (!disabled || (model.processing && !activeCst.is_inherited))}
disabled={disabled} disabled={disabled}
onEdit={handleEdit} onEdit={handleEdit}
/> />
@ -172,7 +169,7 @@ function EditorRSExpression({
<StatusBar <StatusBar
processing={parser.processing} processing={parser.processing}
isModified={isModified} isModified={isModified}
constituenta={activeCst} activeCst={activeCst}
parseData={parser.parseData} parseData={parser.parseData}
onAnalyze={() => handleCheckExpression()} onAnalyze={() => handleCheckExpression()}
/> />

View File

@ -19,11 +19,11 @@ interface StatusBarProps {
processing?: boolean; processing?: boolean;
isModified?: boolean; isModified?: boolean;
parseData?: IExpressionParse; parseData?: IExpressionParse;
constituenta?: IConstituenta; activeCst: IConstituenta;
onAnalyze: () => void; onAnalyze: () => void;
} }
function StatusBar({ isModified, processing, constituenta, parseData, onAnalyze }: StatusBarProps) { function StatusBar({ isModified, processing, activeCst, parseData, onAnalyze }: StatusBarProps) {
const { colors } = useConceptOptions(); const { colors } = useConceptOptions();
const status = useMemo(() => { const status = useMemo(() => {
if (isModified) { if (isModified) {
@ -33,8 +33,8 @@ function StatusBar({ isModified, processing, constituenta, parseData, onAnalyze
const parse = parseData.parseResult ? ParsingStatus.VERIFIED : ParsingStatus.INCORRECT; const parse = parseData.parseResult ? ParsingStatus.VERIFIED : ParsingStatus.INCORRECT;
return inferStatus(parse, parseData.valueClass); return inferStatus(parse, parseData.valueClass);
} }
return inferStatus(constituenta?.parse?.status, constituenta?.parse?.valueClass); return inferStatus(activeCst.parse.status, activeCst.parse.valueClass);
}, [isModified, constituenta, parseData]); }, [isModified, activeCst, parseData]);
return ( return (
<div <div

View File

@ -437,10 +437,14 @@ export const RSEditState = ({
const createCst = useCallback( const createCst = useCallback(
(type: CstType | undefined, skipDialog: boolean, definition?: string) => { (type: CstType | undefined, skipDialog: boolean, definition?: string) => {
if (!model.schema) {
return;
}
const targetType = type ?? activeCst?.cst_type ?? CstType.BASE;
const data: ICstCreateData = { const data: ICstCreateData = {
insert_after: activeCst?.id ?? null, insert_after: activeCst?.id ?? null,
cst_type: type ?? activeCst?.cst_type ?? CstType.BASE, cst_type: targetType,
alias: '', alias: generateAlias(targetType, model.schema),
term_raw: '', term_raw: '',
definition_formal: definition ?? '', definition_formal: definition ?? '',
definition_raw: '', definition_raw: '',
@ -454,17 +458,17 @@ export const RSEditState = ({
setShowCreateCst(true); setShowCreateCst(true);
} }
}, },
[activeCst, handleCreateCst] [activeCst, handleCreateCst, model.schema]
); );
const cloneCst = useCallback(() => { const cloneCst = useCallback(() => {
if (!activeCst) { if (!activeCst || !model.schema) {
return; return;
} }
const data: ICstCreateData = { const data: ICstCreateData = {
insert_after: activeCst.id, insert_after: activeCst.id,
cst_type: activeCst.cst_type, cst_type: activeCst.cst_type,
alias: '', alias: generateAlias(activeCst.cst_type, model.schema),
term_raw: activeCst.term_raw, term_raw: activeCst.term_raw,
definition_formal: activeCst.definition_formal, definition_formal: activeCst.definition_formal,
definition_raw: activeCst.definition_raw, definition_raw: activeCst.definition_raw,
@ -472,7 +476,7 @@ export const RSEditState = ({
term_forms: activeCst.term_forms term_forms: activeCst.term_forms
}; };
handleCreateCst(data); handleCreateCst(data);
}, [activeCst, handleCreateCst]); }, [activeCst, handleCreateCst, model.schema]);
const renameCst = useCallback(() => { const renameCst = useCallback(() => {
if (!activeCst) { if (!activeCst) {

View File

@ -28,7 +28,7 @@ export const PARAMETER = {
typificationTruncate: 42, // characters - threshold for long typification - truncate typificationTruncate: 42, // characters - threshold for long typification - truncate
ossLongLabel: 14, // characters - threshold for long labels - small font ossLongLabel: 14, // characters - threshold for long labels - small font
ossTruncateLabel: 28, // characters - threshold for long labels - truncate ossTruncateLabel: 32, // characters - threshold for long labels - truncate
statSmallThreshold: 3, // characters - threshold for small labels - small font statSmallThreshold: 3, // characters - threshold for small labels - small font
@ -42,7 +42,8 @@ export const PARAMETER = {
* Numeric limitations. * Numeric limitations.
*/ */
export const limits = { export const limits = {
location_len: 500 location_len: 500,
max_semantic_index: 900
}; };
/** /**

View File

@ -824,6 +824,8 @@ export function describeSubstitutionError(error: ISubstitutionErrorDescription):
return `Ошибка ${error.params[0]} -> ${error.params[1]}: количество аргументов не совпадает`; return `Ошибка ${error.params[0]} -> ${error.params[1]}: количество аргументов не совпадает`;
case SubstitutionErrorType.unequalArgs: case SubstitutionErrorType.unequalArgs:
return `Ошибка ${error.params[0]} -> ${error.params[1]}: типизация аргументов не совпадает`; return `Ошибка ${error.params[0]} -> ${error.params[1]}: типизация аргументов не совпадает`;
case SubstitutionErrorType.unequalExpressions:
return `Предупреждение ${error.params[0]} -> ${error.params[1]}: определения понятий не совпадают`;
} }
return 'UNKNOWN ERROR'; return 'UNKNOWN ERROR';
} }

View File

@ -1,4 +1,6 @@
/** @type {import('tailwindcss').Config} */ /** @type {import('tailwindcss').Config} */
import defaultTheme from 'tailwindcss/defaultTheme';
export default { export default {
darkMode: 'class', darkMode: 'class',
content: ['./src/**/*.{js,jsx,ts,tsx}'], content: ['./src/**/*.{js,jsx,ts,tsx}'],
@ -14,6 +16,10 @@ export default {
modalControls: '70', modalControls: '70',
modalTooltip: '90' modalTooltip: '90'
}, },
screens: {
xs: '475px',
...defaultTheme.screens
},
extend: {} extend: {}
}, },
plugins: [], plugins: [],