From bd6f72aceb4e66584030c36093954aac76bc0c3c Mon Sep 17 00:00:00 2001 From: Ivan <8611739+IRBorisov@users.noreply.github.com> Date: Wed, 13 Aug 2025 13:32:03 +0300 Subject: [PATCH] F: Implement association graph UI --- .vscode/settings.json | 1 + rsconcept/frontend/src/components/icons.tsx | 1 - .../help/items/cc/help-concept-relations.tsx | 11 +++- .../help/items/ui/help-rsgraph-term.tsx | 7 +-- .../dlg-show-term-graph/tg-readonly-flow.tsx | 9 ++- .../frontend/src/features/rsform/colors.ts | 13 +++- .../components/term-graph/select-coloring.tsx | 4 +- .../term-graph/select-graph-type.tsx | 31 ++++++++++ .../term-graph}/view-hidden.tsx | 41 ++++++++----- .../rsform/dialogs/dlg-graph-params.tsx | 14 ++--- .../frontend/src/features/rsform/labels.ts | 13 +++- .../src/features/rsform/models/graph-api.ts | 29 +++++++-- .../rsform-page/editor-term-graph/tg-flow.tsx | 59 +++++++++++++++---- .../editor-term-graph/toolbar-term-graph.tsx | 10 ++-- .../src/features/rsform/stores/term-graph.ts | 33 ++++++++--- 15 files changed, 212 insertions(+), 64 deletions(-) create mode 100644 rsconcept/frontend/src/features/rsform/components/term-graph/select-graph-type.tsx rename rsconcept/frontend/src/features/rsform/{pages/rsform-page/editor-term-graph => components/term-graph}/view-hidden.tsx (74%) diff --git a/.vscode/settings.json b/.vscode/settings.json index 863c595b..b6c6bf6d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -218,6 +218,7 @@ "Никанорова", "Номеноид", "номеноида", + "номеноидом", "Номеноиды", "операционализации", "операционализированных", diff --git a/rsconcept/frontend/src/components/icons.tsx b/rsconcept/frontend/src/components/icons.tsx index c0fdf7ef..1fb83dd0 100644 --- a/rsconcept/frontend/src/components/icons.tsx +++ b/rsconcept/frontend/src/components/icons.tsx @@ -161,7 +161,6 @@ export { BiGitBranch as IconGraphInputs } from 'react-icons/bi'; export { TbEarScan as IconGraphInverse } from 'react-icons/tb'; export { BiGitMerge as IconGraphOutputs } from 'react-icons/bi'; export { LuAtom as IconGraphCore } from 'react-icons/lu'; -export { LuRotate3D as IconRotate3D } from 'react-icons/lu'; export { MdOutlineFitScreen as IconFitImage } from 'react-icons/md'; export { RiFocus3Line as IconFocus } from 'react-icons/ri'; export { LuSparkles as IconClustering } from 'react-icons/lu'; diff --git a/rsconcept/frontend/src/features/help/items/cc/help-concept-relations.tsx b/rsconcept/frontend/src/features/help/items/cc/help-concept-relations.tsx index 4513d512..f863bcd2 100644 --- a/rsconcept/frontend/src/features/help/items/cc/help-concept-relations.tsx +++ b/rsconcept/frontend/src/features/help/items/cc/help-concept-relations.tsx @@ -6,9 +6,14 @@ export function HelpConceptRelations() {

Связи между конституентами

- Конституенты связаны между собой через использование одних конституент при определении других. Такую связь в - общем случае называют используется в определении. Она является основой для построения Графа термов - , отображающего последовательность вывода понятий в концептуальной схеме. + Наиболее общей связью между конституентами является ассоциация, устанавливаемая между номеноидом и относимыми к + нему другими конституентами. Такая связь задается до установления точных определений и применяется для + предварительной фиксации групп связанных конституент. +

+

+ Конституенты также связаны между собой через использование одних конституент при определении других. Такую связь + в общем случае называют используется в определении. Она является основой для построения{' '} + Графа термов, отображающего последовательность вывода понятий в концептуальной схеме.

diff --git a/rsconcept/frontend/src/features/help/items/ui/help-rsgraph-term.tsx b/rsconcept/frontend/src/features/help/items/ui/help-rsgraph-term.tsx index b9ae7f4c..14f0d1d6 100644 --- a/rsconcept/frontend/src/features/help/items/ui/help-rsgraph-term.tsx +++ b/rsconcept/frontend/src/features/help/items/ui/help-rsgraph-term.tsx @@ -20,7 +20,6 @@ import { IconOSS, IconPredecessor, IconReset, - IconRotate3D, IconText, IconTypeGraph } from '@/components/icons'; @@ -37,15 +36,15 @@ export function HelpRSGraphTerm() {

Настройка графа

diff --git a/rsconcept/frontend/src/features/oss/dialogs/dlg-show-term-graph/tg-readonly-flow.tsx b/rsconcept/frontend/src/features/oss/dialogs/dlg-show-term-graph/tg-readonly-flow.tsx index 060a82ea..1839c070 100644 --- a/rsconcept/frontend/src/features/oss/dialogs/dlg-show-term-graph/tg-readonly-flow.tsx +++ b/rsconcept/frontend/src/features/oss/dialogs/dlg-show-term-graph/tg-readonly-flow.tsx @@ -7,11 +7,14 @@ import { type IConstituenta, type IRSForm } from '@/features/rsform'; import { TGEdgeTypes } from '@/features/rsform/components/term-graph/graph/tg-edge-types'; import { TGNodeTypes } from '@/features/rsform/components/term-graph/graph/tg-node-types'; import { SelectColoring } from '@/features/rsform/components/term-graph/select-coloring'; +import { SelectGraphType } from '@/features/rsform/components/term-graph/select-graph-type'; import { ToolbarFocusedCst } from '@/features/rsform/components/term-graph/toolbar-focused-cst'; +import { ViewHidden } from '@/features/rsform/components/term-graph/view-hidden'; import { applyLayout, produceFilteredGraph, type TGNodeData } from '@/features/rsform/models/graph-api'; import { useTermGraphStore } from '@/features/rsform/stores/term-graph'; import { DiagramFlow, useReactFlow } from '@/components/flow/diagram-flow'; +import { useFitHeight } from '@/stores/app-layout'; import { PARAMETER } from '@/utils/constants'; import ToolbarGraphFilter from './toolbar-graph-filter'; @@ -35,6 +38,8 @@ export function TGReadonlyFlow({ schema }: TGReadonlyFlowProps) { const filter = useTermGraphStore(state => state.filter); const filteredGraph = produceFilteredGraph(schema, filter, focusCst); + const hidden = schema.items.filter(cst => !filteredGraph.hasNode(cst.id)).map(cst => cst.id); + const hiddenHeight = useFitHeight('15.5rem + 2px', '4rem'); const [nodes, setNodes, onNodesChange] = useNodesState([]); const [edges, setEdges] = useEdgesState([]); @@ -102,7 +107,9 @@ export function TGReadonlyFlow({ schema }: TGReadonlyFlowProps) { ) : null}
- + + +
state.setColoring); return ( -
+
{coloring === 'status' ? : null} {coloring === 'type' ? : null} diff --git a/rsconcept/frontend/src/features/rsform/components/term-graph/select-graph-type.tsx b/rsconcept/frontend/src/features/rsform/components/term-graph/select-graph-type.tsx new file mode 100644 index 00000000..e2b40edd --- /dev/null +++ b/rsconcept/frontend/src/features/rsform/components/term-graph/select-graph-type.tsx @@ -0,0 +1,31 @@ +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/input/select'; +import { cn } from '@/components/utils'; + +import { labelGraphType } from '../../labels'; +import { graphTypes, useTermGraphStore } from '../../stores/term-graph'; + +interface SelectGraphTypeProps { + className?: string; +} + +export function SelectGraphType({ className }: SelectGraphTypeProps) { + const graphType = useTermGraphStore(state => state.filter.graphType); + const setGraphType = useTermGraphStore(state => state.setGraphType); + + return ( +
+ +
+ ); +} diff --git a/rsconcept/frontend/src/features/rsform/pages/rsform-page/editor-term-graph/view-hidden.tsx b/rsconcept/frontend/src/features/rsform/components/term-graph/view-hidden.tsx similarity index 74% rename from rsconcept/frontend/src/features/rsform/pages/rsform-page/editor-term-graph/view-hidden.tsx rename to rsconcept/frontend/src/features/rsform/components/term-graph/view-hidden.tsx index 1da56b9d..341b22f6 100644 --- a/rsconcept/frontend/src/features/rsform/pages/rsform-page/editor-term-graph/view-hidden.tsx +++ b/rsconcept/frontend/src/features/rsform/components/term-graph/view-hidden.tsx @@ -4,35 +4,44 @@ import clsx from 'clsx'; import { MiniButton } from '@/components/control'; import { IconDropArrow, IconDropArrowUp } from '@/components/icons'; -import { useWindowSize } from '@/hooks/use-window-size'; -import { useFitHeight } from '@/stores/app-layout'; import { globalIDs, prefixes } from '@/utils/constants'; -import { colorBgGraphNode } from '../../../colors'; -import { type IConstituenta } from '../../../models/rsform'; -import { useCstTooltipStore } from '../../../stores/cst-tooltip'; -import { useTermGraphStore } from '../../../stores/term-graph'; -import { useRSEdit } from '../rsedit-context'; +import { colorBgGraphNode } from '../../colors'; +import { type IConstituenta, type IRSForm } from '../../models/rsform'; +import { useCstTooltipStore } from '../../stores/cst-tooltip'; +import { useTermGraphStore } from '../../stores/term-graph'; interface ViewHiddenProps { items: number[]; + listHeight?: string; + + schema: IRSForm; + selected?: number[]; + toggleSelect?: (id: number) => void; + setFocus: (cst: IConstituenta) => void; + onActivate?: (id: number) => void; } -export function ViewHidden({ items }: ViewHiddenProps) { - const { isSmall } = useWindowSize(); +export function ViewHidden({ + items, + listHeight, + schema, + selected, + toggleSelect, + setFocus, + onActivate +}: ViewHiddenProps) { const coloring = useTermGraphStore(state => state.coloring); - const { navigateCst, setFocus, schema, selected, toggleSelect } = useRSEdit(); - const localSelected = items.filter(id => selected.includes(id)); + const localSelected = selected ? items.filter(id => selected.includes(id)) : []; const isFolded = useTermGraphStore(state => state.foldHidden); const toggleFolded = useTermGraphStore(state => state.toggleFoldHidden); const setActiveCst = useCstTooltipStore(state => state.setActiveCst); - const hiddenHeight = useFitHeight(isSmall ? '10.4rem + 2px' : '12.5rem + 2px'); function handleClick(event: React.MouseEvent, cstID: number) { event.preventDefault(); event.stopPropagation(); - toggleSelect(cstID); + toggleSelect?.(cstID); } function handleContextMenu(event: React.MouseEvent, target: IConstituenta) { @@ -56,7 +65,7 @@ export function ViewHidden({ items }: ViewHiddenProps) {
- {`Скрытые [${localSelected.length} | ${items.length}]`} + {localSelected ? `Скрытые [${localSelected.length} | ${items.length}]` : 'Скрытые'}
@@ -70,7 +79,7 @@ export function ViewHidden({ items }: ViewHiddenProps) { !isFolded && 'open' )} inert={isFolded} - style={{ maxHeight: hiddenHeight }} + style={{ maxHeight: listHeight }} > {items.map(cstID => { const cst = schema.cstByID.get(cstID)!; @@ -87,7 +96,7 @@ export function ViewHidden({ items }: ViewHiddenProps) { style={{ backgroundColor: colorBgGraphNode(cst, coloring) }} onClick={event => handleClick(event, cstID)} onContextMenu={event => handleContextMenu(event, cst)} - onDoubleClick={() => navigateCst(cstID)} + onDoubleClick={() => onActivate?.(cstID)} data-tooltip-id={globalIDs.constituenta_tooltip} onMouseEnter={() => setActiveCst(cst)} > diff --git a/rsconcept/frontend/src/features/rsform/dialogs/dlg-graph-params.tsx b/rsconcept/frontend/src/features/rsform/dialogs/dlg-graph-params.tsx index ab1fb34a..9b026dd2 100644 --- a/rsconcept/frontend/src/features/rsform/dialogs/dlg-graph-params.tsx +++ b/rsconcept/frontend/src/features/rsform/dialogs/dlg-graph-params.tsx @@ -37,6 +37,13 @@ export function DlgGraphParams() { name='noText' render={({ field }) => } /> + ( + + )} + /> )} /> - ( - - )} - />

Типы конституент

diff --git a/rsconcept/frontend/src/features/rsform/labels.ts b/rsconcept/frontend/src/features/rsform/labels.ts index 475db24e..bdcced4d 100644 --- a/rsconcept/frontend/src/features/rsform/labels.ts +++ b/rsconcept/frontend/src/features/rsform/labels.ts @@ -14,7 +14,7 @@ import { Grammeme, ReferenceType } from './models/language'; import { CstClass, ExpressionStatus, type IConstituenta } from './models/rsform'; import { type IArgumentInfo, type ISyntaxTreeNode } from './models/rslang'; import { CstMatchMode, DependencyMode } from './stores/cst-search'; -import { type GraphColoring } from './stores/term-graph'; +import { type GraphColoring, type GraphType } from './stores/term-graph'; // --- Records for label/describe functions --- const labelCstTypeRecord: Record = { @@ -57,6 +57,12 @@ const labelColoringRecord: Record = { schemas: 'Цвет: Схемы' }; +const labelGraphTypeRecord: Record = { + full: 'Связи: Все', + definition: 'Связи: Определения', + association: 'Связи: Ассоциации' +}; + const labelCstMatchModeRecord: Record = { [CstMatchMode.ALL]: 'фильтр', [CstMatchMode.EXPR]: 'выражение', @@ -396,6 +402,11 @@ export function labelColoring(mode: GraphColoring): string { return labelColoringRecord[mode] ?? `UNKNOWN COLORING: ${mode}`; } +/** Retrieves label for {@link GraphType}. */ +export function labelGraphType(mode: GraphType): string { + return labelGraphTypeRecord[mode] ?? `UNKNOWN GRAPH TYPE: ${mode}`; +} + /** * Retrieves label for {@link ExpressionStatus}. */ diff --git a/rsconcept/frontend/src/features/rsform/models/graph-api.ts b/rsconcept/frontend/src/features/rsform/models/graph-api.ts index 17123f92..9659d617 100644 --- a/rsconcept/frontend/src/features/rsform/models/graph-api.ts +++ b/rsconcept/frontend/src/features/rsform/models/graph-api.ts @@ -7,7 +7,7 @@ import dagre from '@dagrejs/dagre'; import { PARAMETER } from '@/utils/constants'; import { CstType } from '../backend/types'; -import { type GraphFilterParams } from '../stores/term-graph'; +import { type GraphFilterParams, type GraphType } from '../stores/term-graph'; import { type IConstituenta, type IRSForm } from './rsform'; @@ -57,8 +57,27 @@ export function applyLayout(nodes: Node[], edges: Edge[], subLabels }); } +export function inferEdgeType(schema: IRSForm, source: number, target: number): GraphType | null { + const isDefinition = schema.graph.hasEdge(source, target); + const isAssociation = schema.association_graph.hasEdge(source, target); + if (!isDefinition && !isAssociation) { + return null; + } else if (isDefinition && isAssociation) { + return 'full'; + } else if (isDefinition) { + return 'definition'; + } else { + return 'association'; + } +} + export function produceFilteredGraph(schema: IRSForm, params: GraphFilterParams, focusCst: IConstituenta | null) { - const filtered = schema.graph.clone(); + const filtered = + params.graphType === 'full' + ? schema.full_graph.clone() + : params.graphType === 'association' + ? schema.association_graph.clone() + : schema.graph.clone(); const allowedTypes: CstType[] = (() => { const result: CstType[] = []; if (params.allowBase) result.push(CstType.BASE); @@ -73,9 +92,6 @@ export function produceFilteredGraph(schema: IRSForm, params: GraphFilterParams, return result; })(); - if (params.noHermits) { - filtered.removeIsolated(); - } if (params.noTemplates) { schema.items.forEach(cst => { if (cst !== focusCst && cst.is_template) { @@ -97,6 +113,9 @@ export function produceFilteredGraph(schema: IRSForm, params: GraphFilterParams, } }); } + if (params.noHermits) { + filtered.removeIsolated(); + } if (focusCst) { const includes: number[] = [ focusCst.id, diff --git a/rsconcept/frontend/src/features/rsform/pages/rsform-page/editor-term-graph/tg-flow.tsx b/rsconcept/frontend/src/features/rsform/pages/rsform-page/editor-term-graph/tg-flow.tsx index 1c64ad7d..cc2999ff 100644 --- a/rsconcept/frontend/src/features/rsform/pages/rsform-page/editor-term-graph/tg-flow.tsx +++ b/rsconcept/frontend/src/features/rsform/pages/rsform-page/editor-term-graph/tg-flow.tsx @@ -4,21 +4,24 @@ import { useEffect, useRef } from 'react'; import { type Edge, MarkerType, type Node, useEdgesState, useNodesState, useOnSelectionChange } from 'reactflow'; import { DiagramFlow, useReactFlow } from '@/components/flow/diagram-flow'; -import { useMainHeight } from '@/stores/app-layout'; +import { useWindowSize } from '@/hooks/use-window-size'; +import { useFitHeight, useMainHeight } from '@/stores/app-layout'; import { PARAMETER } from '@/utils/constants'; import { withPreventDefault } from '@/utils/utils'; import { useMutatingRSForm } from '../../../backend/use-mutating-rsform'; +import { colorGraphEdge } from '../../../colors'; import { TGEdgeTypes } from '../../../components/term-graph/graph/tg-edge-types'; import { TGNodeTypes } from '../../../components/term-graph/graph/tg-node-types'; import { SelectColoring } from '../../../components/term-graph/select-coloring'; -import { applyLayout, type TGNodeData } from '../../../models/graph-api'; +import { SelectGraphType } from '../../../components/term-graph/select-graph-type'; +import { ViewHidden } from '../../../components/term-graph/view-hidden'; +import { applyLayout, inferEdgeType, type TGNodeData } from '../../../models/graph-api'; import { useTermGraphStore } from '../../../stores/term-graph'; import { useRSEdit } from '../rsedit-context'; import { ToolbarTermGraph } from './toolbar-term-graph'; import { useFilteredGraph } from './use-filtered-graph'; -import { ViewHidden } from './view-hidden'; export const flowOptions = { fitView: true, @@ -31,17 +34,28 @@ export const flowOptions = { } as const; export function TGFlow() { + const { isSmall } = useWindowSize(); const mainHeight = useMainHeight(); const { fitView, viewportInitialized } = useReactFlow(); const isProcessing = useMutatingRSForm(); - const { isContentEditable, schema, selected, setSelected, promptDeleteCst, focusCst, setFocus, navigateCst } = - useRSEdit(); + const { + isContentEditable, + schema, + selected, + setSelected, + promptDeleteCst, + focusCst, + setFocus, + toggleSelect, + navigateCst + } = useRSEdit(); const [nodes, setNodes, onNodesChange] = useNodesState([]); const [edges, setEdges] = useEdgesState([]); const filter = useTermGraphStore(state => state.filter); const { filteredGraph, hidden } = useFilteredGraph(); + const hiddenHeight = useFitHeight(isSmall ? '15rem + 2px' : '13.5rem + 2px', '4rem'); function onSelectionChange({ nodes }: { nodes: Node[] }) { const ids = nodes.map(node => Number(node.id)); @@ -73,17 +87,21 @@ export function TGFlow() { let edgeID = 1; filteredGraph.nodes.forEach(source => { source.outputs.forEach(target => { - if (newNodes.find(node => node.id === String(target))) { + const edgeType = inferEdgeType(schema, source.id, target); + if (edgeType && newNodes.find(node => node.id === String(target))) { + const color = filter.graphType === 'full' ? colorGraphEdge(edgeType) : colorGraphEdge(filter.graphType); newEdges.push({ id: String(edgeID), source: String(source.id), target: String(target), type: 'termEdge', + style: { stroke: color }, focusable: false, markerEnd: { type: MarkerType.ArrowClosed, width: 20, - height: 20 + height: 20, + color: color } }); edgeID += 1; @@ -97,7 +115,17 @@ export function TGFlow() { setEdges(newEdges); setTimeout(() => fitView(flowOptions.fitViewOptions), PARAMETER.minimalTimeout); - }, [schema, filteredGraph, setNodes, setEdges, filter.noText, fitView, viewportInitialized, focusCst]); + }, [ + schema, + filteredGraph, + setNodes, + setEdges, + filter.noText, + fitView, + viewportInitialized, + focusCst, + filter.graphType + ]); const prevSelected = useRef([]); if ( @@ -150,8 +178,19 @@ export function TGFlow() { Выбор {selected.length} из {schema.stats?.count_all ?? 0} - - + + + + +
- } - title='Граф ступеней' - onClick={handleShowTypeGraph} - />
@@ -198,6 +193,11 @@ export function ToolbarTermGraph({ className }: ToolbarTermGraphProps) { disabled={!canDeleteSelected || isProcessing} /> ) : null} + } + title='Граф ступеней' + onClick={handleShowTypeGraph} + />
); diff --git a/rsconcept/frontend/src/features/rsform/stores/term-graph.ts b/rsconcept/frontend/src/features/rsform/stores/term-graph.ts index c67d0e7c..93ad40f8 100644 --- a/rsconcept/frontend/src/features/rsform/stores/term-graph.ts +++ b/rsconcept/frontend/src/features/rsform/stores/term-graph.ts @@ -4,16 +4,18 @@ import { persist } from 'zustand/middleware'; import { CstType } from '../backend/types'; export const graphColorings = ['none', 'status', 'type', 'schemas'] as const; +export const graphTypes = ['full', 'association', 'definition'] as const; -/** - * Represents graph node coloring scheme. - */ +/** Represents graph node coloring scheme. */ export type GraphColoring = (typeof graphColorings)[number]; -/** - * Represents parameters for GraphEditor. - */ +/** Represents graph type. */ +export type GraphType = (typeof graphTypes)[number]; + +/** Represents parameters for GraphEditor. */ export interface GraphFilterParams { + graphType: GraphType; + noHermits: boolean; noTransitive: boolean; noTemplates: boolean; @@ -49,10 +51,12 @@ export const cstTypeToFilterKey: Record = { interface TermGraphStore { filter: GraphFilterParams; setFilter: (value: GraphFilterParams) => void; + setGraphType: (value: GraphType) => void; toggleFocusInputs: () => void; toggleFocusOutputs: () => void; toggleText: () => void; toggleClustering: () => void; + toggleGraphType: () => void; foldHidden: boolean; toggleFoldHidden: () => void; @@ -65,6 +69,8 @@ export const useTermGraphStore = create()( persist( set => ({ filter: { + graphType: 'full', + noTemplates: false, noHermits: true, noTransitive: true, @@ -85,12 +91,25 @@ export const useTermGraphStore = create()( allowNominal: true }, setFilter: value => set({ filter: value }), + setGraphType: value => set(state => ({ filter: { ...state.filter, graphType: value } })), toggleFocusInputs: () => set(state => ({ filter: { ...state.filter, focusShowInputs: !state.filter.focusShowInputs } })), toggleFocusOutputs: () => set(state => ({ filter: { ...state.filter, focusShowOutputs: !state.filter.focusShowOutputs } })), toggleText: () => set(state => ({ filter: { ...state.filter, noText: !state.filter.noText } })), toggleClustering: () => set(state => ({ filter: { ...state.filter, foldDerived: !state.filter.foldDerived } })), + toggleGraphType: () => + set(state => ({ + filter: { + ...state.filter, + graphType: + state.filter.graphType === 'full' + ? 'association' + : state.filter.graphType === 'association' + ? 'definition' + : 'full' + } + })), foldHidden: false, toggleFoldHidden: () => set(state => ({ foldHidden: !state.foldHidden })), @@ -99,7 +118,7 @@ export const useTermGraphStore = create()( setColoring: value => set({ coloring: value }) }), { - version: 1, + version: 3, name: 'portal.termGraph' } )