Skip to content

Commit 961d9ec

Browse files
committed
create morph registry
manage morph changes in the UI
1 parent ceb17ee commit 961d9ec

8 files changed

Lines changed: 565 additions & 34 deletions

File tree

nodebook-base/data-store.js

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import crypto from 'crypto';
44
import { PolyNode, RelationNode, AttributeNode, FunctionNode } from './models.js';
55
import { getOperationsFromCnl } from './cnl-parser.js';
66
import { GitVersionControl } from './version-control.js';
7+
import { MorphRegistry } from './morph-registry.js';
78

89
/**
910
* Abstract Data Store Interface
@@ -40,6 +41,7 @@ export class FileSystemStore extends DataStore {
4041
super('file-system', { dataPath });
4142
this.dataPath = dataPath;
4243
this.gitVersionControl = new GitVersionControl(dataPath);
44+
this.morphRegistry = new MorphRegistry();
4345
}
4446

4547
async initialize() {
@@ -79,7 +81,12 @@ export class FileSystemStore extends DataStore {
7981
const graphPath = path.join(this.getGraphDataDir(userId, graphId), 'graph.json');
8082
try {
8183
const data = await fsp.readFile(graphPath, 'utf-8');
82-
return JSON.parse(data);
84+
const graphData = JSON.parse(data);
85+
86+
// Build morph registry when loading graph
87+
this.buildMorphRegistry(graphData);
88+
89+
return graphData;
8390
} catch (error) {
8491
if (error.code === 'ENOENT') return null;
8592
throw error;
@@ -626,10 +633,79 @@ export class FileSystemStore extends DataStore {
626633
// Save the updated graph
627634
await this.saveGraph(userId, graphId, graphData);
628635

636+
// Rebuild morph registry after morph change
637+
this.buildMorphRegistry(graphData);
638+
629639
console.log(`[DataStore] Changed node ${nodeId} to morph ${targetMorph.name}`);
630640
return { success: true, morphName: targetMorph.name };
631641
}
632642

643+
// --- Morph Registry Methods ---
644+
645+
/**
646+
* Build morph registry from graph data
647+
* @param {Object} graphData - Graph data object
648+
*/
649+
buildMorphRegistry(graphData) {
650+
this.morphRegistry.clear();
651+
652+
// Process each node and its morphs
653+
graphData.nodes.forEach(node => {
654+
if (node.morphs && node.morphs.length > 0) {
655+
node.morphs.forEach(morph => {
656+
this.morphRegistry.addMorph(
657+
morph.morph_id,
658+
node.id,
659+
morph.name,
660+
morph.relationNode_ids || [],
661+
morph.attributeNode_ids || []
662+
);
663+
});
664+
}
665+
});
666+
667+
const stats = this.morphRegistry.getStats();
668+
console.log(`[DataStore] Built morph registry with ${stats.totalMorphs} morphs for ${stats.totalNodes} nodes`);
669+
}
670+
671+
/**
672+
* Get filtered relations for a specific morph
673+
* @param {Array} relations - All relations
674+
* @param {string} morphId - Active morph ID
675+
* @returns {Array} Filtered relations
676+
*/
677+
getFilteredRelations(relations, morphId) {
678+
return this.morphRegistry.filterRelationsForMorph(relations, morphId);
679+
}
680+
681+
/**
682+
* Get filtered attributes for a specific morph
683+
* @param {Array} attributes - All attributes
684+
* @param {string} morphId - Active morph ID
685+
* @returns {Array} Filtered attributes
686+
*/
687+
getFilteredAttributes(attributes, morphId) {
688+
return this.morphRegistry.filterAttributesForMorph(attributes, morphId);
689+
}
690+
691+
/**
692+
* Get morph data by ID
693+
* @param {string} morphId - Morph ID
694+
* @returns {Object|null} Morph data
695+
*/
696+
getMorphData(morphId) {
697+
return this.morphRegistry.getMorph(morphId);
698+
}
699+
700+
/**
701+
* Get all morphs for a node
702+
* @param {string} nodeId - Node ID
703+
* @returns {Array} Array of morph data
704+
*/
705+
getNodeMorphs(nodeId) {
706+
return this.morphRegistry.getNodeMorphs(nodeId);
707+
}
708+
633709
// --- Collaboration (Invites) ---
634710
getCollabInvitesPath(ownerId) {
635711
return path.join(this.getUserDataDir(ownerId), 'collab_invites.json');

nodebook-base/frontend/src/App.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -728,6 +728,7 @@ function App({ onLogout, onGoToDashboard, user }: AppProps) {
728728
publication_state={publicationState}
729729
onPublicationStateChange={handlePublicationStateChange}
730730
graphMode={graphMode}
731+
onMorphChange={handleMorphChange}
731732
/>
732733
) : (
733734
<>
@@ -745,6 +746,7 @@ function App({ onLogout, onGoToDashboard, user }: AppProps) {
745746
nodeRegistry={{}}
746747
isPublic={false}
747748
graphId={activeGraphId || undefined}
749+
onMorphChange={handleMorphChange}
748750
/>
749751
</div>
750752
)}
@@ -852,6 +854,7 @@ function App({ onLogout, onGoToDashboard, user }: AppProps) {
852854
publication_state={publicationState}
853855
onPublicationStateChange={handlePublicationStateChange}
854856
graphMode={graphMode}
857+
onMorphChange={handleMorphChange}
855858
/>}
856859
{viewMode === 'schema' && <SchemaView onSchemaChange={fetchSchemas} />}
857860
{viewMode === 'peers' && <PeerTab activeGraphId={activeGraphId} graphKey={activeGraphKey} />}

nodebook-base/frontend/src/DataView.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ interface DataViewProps {
2020
publication_state?: 'Private' | 'P2P' | 'Public';
2121
onPublicationStateChange?: (newState: 'Private' | 'P2P' | 'Public') => void;
2222
graphMode?: 'markdown' | 'mindmap' | 'richgraph' | 'strictgraph';
23+
onMorphChange?: (nodeId: string, morphId: string) => void;
2324
}
2425

2526
export function DataView({
@@ -32,7 +33,8 @@ export function DataView({
3233
onCnlChange,
3334
publication_state = 'Private',
3435
onPublicationStateChange,
35-
graphMode = 'richgraph'
36+
graphMode = 'richgraph',
37+
onMorphChange
3638
}: DataViewProps) {
3739
const [activeNodeId, setActiveNodeId] = useState<string | null>(null);
3840
const [searchTerm, setSearchTerm] = useState('');
@@ -350,6 +352,7 @@ export function DataView({
350352
onImportContext={handleImportContext}
351353
nodeRegistry={nodeRegistry}
352354
graphId={activeGraphId}
355+
onMorphChange={onMorphChange}
353356
/>
354357
))}
355358
</div>

nodebook-base/frontend/src/NodeCard.tsx

Lines changed: 32 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,10 @@ interface NodeCardProps {
2020
nodeRegistry: any;
2121
isPublic?: boolean; // Optional prop for public view mode
2222
graphId?: string; // Explicit graph id for actions
23+
onMorphChange?: (nodeId: string, morphId: string) => void; // Callback for morph changes
2324
}
2425

25-
export function NodeCard({ node, allNodes, allRelations, attributes, isActive, onSelectNode, onImportContext, nodeRegistry, isPublic = false, graphId }: NodeCardProps) {
26+
export function NodeCard({ node, allNodes, allRelations, attributes, isActive, onSelectNode, onImportContext, nodeRegistry, isPublic = false, graphId, onMorphChange }: NodeCardProps) {
2627
const cardRef = React.useRef<HTMLDivElement>(null);
2728
const subgraphSvgRef = React.useRef<string | null>(null);
2829
const registryEntry = nodeRegistry[node.id];
@@ -158,23 +159,15 @@ export function NodeCard({ node, allNodes, allRelations, attributes, isActive, o
158159
};
159160

160161
const handleMorphChange = async (morphId: string) => {
161-
if (!graphId || isChangingMorph) return;
162+
if (!graphId || isChangingMorph || !onMorphChange) return;
162163

163164
setIsChangingMorph(true);
164165
try {
165-
const response = await authenticatedFetch(`${API_BASE_URL}/api/graphs/${graphId}/nodes/${node.id}/morph`, {
166-
method: 'POST',
167-
body: JSON.stringify({ morphId })
168-
});
169-
170-
if (response.ok) {
171-
const result = await response.json();
172-
console.log(`Changed to morph: ${result.morphName}`);
173-
// Refresh the page to show the updated morph
174-
window.location.reload();
175-
} else {
176-
throw new Error('Failed to change morph');
177-
}
166+
// Update the node's nbh property locally for immediate UI update
167+
node.nbh = morphId;
168+
169+
// Notify parent component to handle the API call and refresh graph data
170+
await onMorphChange(node.id, morphId);
178171
} catch (error) {
179172
console.error('Error changing morph:', error);
180173
alert('Failed to change morph. See console for details.');
@@ -226,10 +219,16 @@ export function NodeCard({ node, allNodes, allRelations, attributes, isActive, o
226219
);
227220
};
228221

229-
// Calculate subgraph data
222+
// Backend should already filter attributes and relations by active morph
223+
// Use all attributes and relations since backend filtering is now handled by morph registry
224+
const filteredAttributes = attributes.filter(attr => attr.source_id === node.id);
225+
const filteredRelations = allRelations.filter(rel => rel.source_id === node.id || rel.target_id === node.id);
226+
227+
// Calculate subgraph data using Cytoscape's neighborhood concept
230228
const subgraphNodes = [node];
231-
const subgraphRelations = allRelations.filter(r => r.source_id === node.id || r.target_id === node.id);
232-
for (const rel of subgraphRelations) {
229+
230+
// Add related nodes (targets of outgoing relations and sources of incoming relations)
231+
for (const rel of filteredRelations) {
233232
const otherNodeId = rel.source_id === node.id ? rel.target_id : rel.source_id;
234233
if (!subgraphNodes.find(n => n.id === otherNodeId)) {
235234
const otherNode = allNodes.find(n => n.id === otherNodeId);
@@ -240,7 +239,18 @@ export function NodeCard({ node, allNodes, allRelations, attributes, isActive, o
240239
return (
241240
<div ref={cardRef} className={`node-card ${isActive ? 'active' : ''}`}>
242241
<div className="node-card-header">
243-
<h3>{node.name}</h3>
242+
<h3>
243+
{(() => {
244+
// Show morph name if active morph is not basic
245+
if (node.morphs && node.nbh) {
246+
const activeMorph = node.morphs.find(m => m.morph_id === node.nbh);
247+
if (activeMorph && activeMorph.name !== 'basic') {
248+
return `${node.name} (${activeMorph.name})`;
249+
}
250+
}
251+
return node.name;
252+
})()}
253+
</h3>
244254
<div className="node-card-header-actions">
245255
<button
246256
className={`publication-toggle ${node.publication_mode?.toLowerCase()}`}
@@ -255,9 +265,10 @@ export function NodeCard({ node, allNodes, allRelations, attributes, isActive, o
255265

256266
<div className="node-card-image">
257267
<Subgraph
268+
key={`subgraph-${node.id}-${node.nbh || 'default'}`}
258269
nodes={subgraphNodes}
259-
relations={subgraphRelations}
260-
attributes={attributes.filter(a => a.source_id === node.id)}
270+
relations={filteredRelations}
271+
attributes={filteredAttributes}
261272
onReady={({ exportSvg }) => { subgraphSvgRef.current = exportSvg(); }}
262273
/>
263274
</div>

nodebook-base/frontend/src/Subgraph.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export function Subgraph({ nodes, relations, attributes = [], onReady }: Subgrap
2222
useEffect(() => {
2323
if (!containerRef.current || nodes.length === 0) return;
2424

25+
2526
const attributeValueNodes = (attributes || []).map(a => ({
2627
data: {
2728
id: a.id,

nodebook-base/frontend/src/Visualization.tsx

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,26 @@ export function Visualization({ nodes, relations, attributes, onNodeSelect, onMo
3232
useEffect(() => {
3333
if (!containerRef.current) return;
3434

35-
const cyNodes = nodes.map(node => ({
36-
data: {
37-
id: node.id,
38-
label: node.name,
39-
type: node.role === 'Transition' ? 'transition' : 'polynode'
35+
const cyNodes = nodes.map(node => {
36+
// For polynodes, show morph name if active morph is not basic
37+
let displayName = node.name;
38+
if (node.role !== 'Transition' && node.morphs && node.nbh) {
39+
const activeMorph = node.morphs.find(m => m.morph_id === node.nbh);
40+
if (activeMorph && activeMorph.name !== 'basic') {
41+
displayName = `${node.name} (${activeMorph.name})`;
42+
}
4043
}
41-
}));
44+
45+
return {
46+
data: {
47+
id: node.id,
48+
label: displayName,
49+
type: node.role === 'Transition' ? 'transition' : 'polynode'
50+
}
51+
};
52+
});
4253

54+
// Backend should already filter attributes and relations by active morph
4355
const attributeValueNodes = attributes.map(attr => ({
4456
data: {
4557
id: attr.id,

0 commit comments

Comments
 (0)