F: Add suggestions to substitution table

This commit is contained in:
Ivan 2024-08-28 15:43:28 +03:00
parent fa6eb49172
commit 2fb64ef69a
8 changed files with 159 additions and 37 deletions

View File

@ -83,7 +83,6 @@ export { LuNewspaper as IconDefinition } from 'react-icons/lu';
export { LuDna as IconTerminology } from 'react-icons/lu';
export { FaRegHandshake as IconConvention } from 'react-icons/fa6';
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 { BiSpa as IconPredecessor } from 'react-icons/bi';
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 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 { useConceptOptions } from '@/context/ConceptOptionsContext';
import { ILibraryItem } from '@/models/library';
@ -13,13 +13,14 @@ import { ICstSubstitute, IMultiSubstitution } from '@/models/oss';
import { ConstituentaID, IConstituenta, IRSForm } from '@/models/rsform';
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 SelectLibraryItem from './SelectLibraryItem';
interface PickSubstitutionsProps {
substitutions: ICstSubstitute[];
setSubstitutions: React.Dispatch<React.SetStateAction<ICstSubstitute[]>>;
suggestions?: ICstSubstitute[];
prefixID: string;
rows?: number;
@ -34,6 +35,7 @@ const columnHelper = createColumnHelper<IMultiSubstitution>();
function PickSubstitutions({
substitutions,
setSubstitutions,
suggestions,
prefixID,
rows,
schemas,
@ -55,6 +57,15 @@ function PickSubstitutions({
const [deleteRight, setDeleteRight] = useState(true);
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(
(id: ConstituentaID): IRSForm | undefined => {
for (const schema of schemas) {
@ -82,14 +93,23 @@ function PickSubstitutions({
);
const substitutionData: IMultiSubstitution[] = useMemo(
() =>
substitutions.map(item => ({
() => [
...substitutions.map(item => ({
original_source: getSchemaByCst(item.original)!,
original: getConstituenta(item.original)!,
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() {
@ -121,19 +141,34 @@ function PickSubstitutions({
setRightCst(undefined);
}
const handleDeleteRow = useCallback(
(row: number) => {
const handleDeclineSuggestion = useCallback(
(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 => {
const newItems: ICstSubstitute[] = [];
prev.forEach((item, index) => {
if (index !== row) {
prev.forEach(item => {
if (item.original !== target.original.id || item.substitution !== target.substitution.id) {
newItems.push(item);
}
});
return newItems;
});
},
[setSubstitutions]
[setSubstitutions, handleDeclineSuggestion]
);
const columns = useMemo(
@ -169,19 +204,47 @@ function PickSubstitutions({
}),
columnHelper.display({
id: 'actions',
cell: props => (
cell: props =>
props.row.original.is_suggestion ? (
<div className='max-w-fit'>
<MiniButton
noHover
title='Принять предложение'
icon={<IconAccept size='1rem' className='icon-green' />}
onClick={() => handleAcceptSuggestion(props.row.original)}
/>
<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={() => handleDeleteRow(props.row.index)}
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 (
@ -265,6 +328,7 @@ function PickSubstitutions({
<p>Добавьте отождествление</p>
</NoData>
}
conditionalRowStyles={conditionalRowStyles}
/>
</div>
);

View File

@ -1,7 +1,7 @@
'use client';
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 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]
);
const [substitutions, setSubstitutions] = useState<ICstSubstitute[]>(target.substitutions);
const [suggestions, setSuggestions] = useState<ICstSubstitute[]>([]);
const cache = useRSFormCache();
const schemas = useMemo(
() => 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]);
useEffect(() => {
useLayoutEffect(() => {
cache.preload(schemasIDs);
}, [schemasIDs]);
useEffect(() => {
useLayoutEffect(() => {
if (cache.loading || schemas.length !== schemasIDs.length) {
return;
}
@ -86,13 +89,14 @@ function DlgEditOperation({ hideWindow, oss, target, onSubmit }: DlgEditOperatio
);
}, [schemasIDs, schemas, cache.loading]);
useEffect(() => {
useLayoutEffect(() => {
if (cache.loading || schemas.length !== schemasIDs.length) {
return;
}
const validator = new SubstitutionValidator(schemas, substitutions);
setIsCorrect(validator.validate());
setValidationText(validator.msg);
setSuggestions(validator.suggestions);
}, [substitutions, cache.loading, schemas, schemasIDs.length]);
const handleSubmit = useCallback(() => {
@ -151,10 +155,11 @@ function DlgEditOperation({ hideWindow, oss, target, onSubmit }: DlgEditOperatio
isCorrect={isCorrect}
substitutions={substitutions}
setSubstitutions={setSubstitutions}
suggestions={suggestions}
/>
</TabPanel>
),
[cache.loading, cache.error, substitutions, schemas, validationText, isCorrect]
[cache.loading, cache.error, substitutions, suggestions, schemas, validationText, isCorrect]
);
return (

View File

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

View File

@ -133,6 +133,7 @@ export interface IMultiSubstitution {
original: IConstituenta;
substitution: IConstituenta;
substitution_source: ILibraryItem;
is_suggestion: boolean;
}
/**

View File

@ -10,7 +10,6 @@ import { Graph } from './Graph';
import { ILibraryItem, LibraryItemID } from './library';
import { ICstSubstitute, IOperation, IOperationSchema, SubstitutionErrorType } from './oss';
import { ConstituentaID, CstClass, CstType, IConstituenta, IRSForm } from './rsform';
import { inferClass } from './rsformAPI';
import { AliasMapping, ParsingStatus } from './rslang';
import { applyAliasMapping, applyTypificationMapping, extractGlobals, isSetTypification } from './rslangAPI';
@ -59,6 +58,7 @@ type CrossMapping = Map<LibraryItemID, AliasMapping>;
*/
export class SubstitutionValidator {
public msg: string = '';
public suggestions: ICstSubstitute[] = [];
private schemas: IRSForm[];
private substitutions: ICstSubstitute[];
@ -102,6 +102,7 @@ export class SubstitutionValidator {
}
public validate(): boolean {
this.calculateSuggestions();
if (this.substitutions.length === 0) {
return this.setValid();
}
@ -118,6 +119,55 @@ export class SubstitutionValidator {
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 {
for (const item of this.substitutions) {
const original = this.cstByID.get(item.original);
@ -242,7 +292,7 @@ export class SubstitutionValidator {
continue;
}
const substitution = this.cstByID.get(item.substitution)!;
if (original.cst_type === substitution.cst_type && inferClass(original.cst_type) === CstClass.DERIVED) {
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.
@ -320,7 +370,6 @@ export class SubstitutionValidator {
} else {
substitutionText = applyAliasMapping(substitution.parse.typification, baseMappings.get(substitution.schema)!);
substitutionText = applyTypificationMapping(substitutionText, result);
console.log(substitutionText);
if (!isSetTypification(substitutionText)) {
this.reportError(SubstitutionErrorType.baseSubstitutionNotSet, [
substitution.alias,

View File

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

View File

@ -5,7 +5,7 @@ import { AnimatePresence } from 'framer-motion';
import { useEffect, useLayoutEffect, useMemo, useState } from 'react';
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 MiniButton from '@/components/ui/MiniButton';
import Overlay from '@/components/ui/Overlay';
@ -249,14 +249,14 @@ function FormConstituenta({
<Overlay position='top-[0.1rem] left-[0.4rem]' className='cc-icons'>
{state?.is_inherited_parent ? (
<MiniButton
icon={<IconChild size='1.25rem' className='clr-text-red' />}
icon={<IconPredecessor size='1.25rem' className='clr-text-red' />}
disabled
titleHtml='Внимание!</br> Конституента имеет потомков<br/> в операционной схеме синтеза'
/>
) : null}
{state?.is_inherited ? (
<MiniButton
icon={<IconParent size='1.25rem' className='clr-text-red' />}
icon={<IconChild size='1.25rem' className='clr-text-red' />}
disabled
titleHtml='Внимание!</br> Конституента является наследником<br/>'
/>