ModulePlacementService - Architectural Design
Complete design specification for intelligent module placement service
ModulePlacementService - Architectural Design
🎯 Purpose
The ModulePlacementService is the intelligent brain that determines where modules should be placed in the canvas grid. It combines:
- Module metadata (placement preferences)
- Current canvas structure
- AI training data (patterns)
- User intent inference
📋 Core Responsibilities
- Read module placement metadata from ModuleManifest
- Suggest optimal placements based on multiple factors
- Auto-place with confidence when certainty is high
- Validate placements before execution
- Learn from user actions (AI training data)
🏗️ Service Architecture
File Location
libs/libs-structural/modular-universe/src/lib/canvas-v2/services/module-placement.service.ts
Dependencies
import { Injectable, inject, computed } from '@angular/core';
import { ModuleRegistryService } from '@libs-core/module-base';
import { LoggerService } from '@libs-core/log-system';
import type { CanvasState, Section } from '../models/canvas-v2.types';
import type { ModulePlacementMetadata, PlacementSuggestion, PlacementValidation } from '../models/module-placement.types';
📝 Type Definitions
1. ModulePlacementMetadata (extends ModuleManifest)
// libs/libs-structural/modular-universe/src/lib/canvas-v2/models/module-placement.types.ts
/**
* Placement metadata that modules can declare in their manifest
* This tells the canvas system where the module prefers to be placed
*/
export interface ModulePlacementMetadata {
/**
* Preferred location in the canvas
* Structural modules often have strong preferences
*/
preferredLocation?: 'header' | 'sidebar' | 'content' | 'footer' | 'floating';
/**
* Default grid area (for CSS Grid layouts)
* Example: { row: "1 / 2", column: "1 / -1" }
*/
defaultGridArea?: {
row: string; // "1 / 2" or "span 1"
column: string; // "1 / -1" or "span 3"
};
/**
* Auto-sizing flag (true for structural modules)
* When true, module controls its own dimensions
*/
autoSize?: boolean;
/**
* Minimum dimensions
*/
minWidth?: string; // "300px", "20rem", "30%"
minHeight?: string; // "64px", "auto"
/**
* Maximum dimensions
*/
maxWidth?: string;
maxHeight?: string;
/**
* Responsive overrides for different viewports
*/
responsive?: {
mobile?: Partial<ModulePlacementMetadata>;
tablet?: Partial<ModulePlacementMetadata>;
};
/**
* Placement constraints
*/
constraints?: {
/** Cannot be nested (must be at root level) */
requiresRootLevel?: boolean;
/** Must be full width */
requiresFullWidth?: boolean;
/** Must be fixed/sticky positioned */
requiresFixedPosition?: boolean;
/** Cannot be placed with these module types */
incompatibleWith?: string[]; // Module IDs
/** Works best with these module types */
complementsWell?: string[]; // Module IDs
};
}
2. PlacementSuggestion
/**
* A suggested placement for a module
*/
export interface PlacementSuggestion {
/** Target section ID (or 'new' to create new section) */
targetSectionId: string | 'new';
/** If 'new', parent section for the new section */
parentSectionId?: string;
/** Confidence score (0-1) */
confidence: number;
/** Reason for this suggestion */
reason: string;
/** Source of suggestion */
source: 'metadata' | 'pattern' | 'ai-learning' | 'user-history';
/** Section configuration to use */
sectionConfig?: Partial<Section>;
/** Visual preview data (optional) */
preview?: {
location: string; // "Top of canvas", "Below header"
layoutType: string; // "Full width", "Sidebar"
};
}
3. PlacementValidation
/**
* Validation result for a proposed placement
*/
export interface PlacementValidation {
/** Is the placement valid? */
valid: boolean;
/** Validation errors (if invalid) */
errors?: string[];
/** Validation warnings (non-blocking) */
warnings?: string[];
/** Suggestions to fix errors */
suggestions?: string[];
}
4. Module Manifest Extension
// Extend ModuleManifest from @libs-core/module-base
// This is done via TypeScript declaration merging
declare module '@libs-core/module-base' {
interface ModuleManifest {
/**
* Optional placement metadata
* Tells the canvas system where this module prefers to be placed
*/
placement?: ModulePlacementMetadata;
}
}
🎨 Service Implementation
Core Service Structure
@Injectable({ providedIn: 'root' })
export class ModulePlacementService {
private readonly moduleRegistry = inject(ModuleRegistryService);
private readonly logger = inject(LoggerService).child({
source: 'ModulePlacementService'
});
// ============================================================================
// PUBLIC API
// ============================================================================
/**
* Get placement metadata for a module
* @param moduleId - Module ID
* @returns Placement metadata or null if not defined
*/
getModulePlacementMetadata(moduleId: string): ModulePlacementMetadata | null {
const module = this.moduleRegistry.get(moduleId);
if (!module) {
this.logger.warn('Module not found', { moduleId });
return null;
}
return module.manifest.placement || null;
}
/**
* Get intelligent placement suggestions for a module
* @param moduleId - Module to place
* @param canvasState - Current canvas state
* @param userContext - Optional user context (intent, history)
* @returns Array of placement suggestions, ordered by confidence (highest first)
*/
getSuggestedPlacements(
moduleId: string,
canvasState: CanvasState,
userContext?: PlacementUserContext
): PlacementSuggestion[] {
const metadata = this.getModulePlacementMetadata(moduleId);
const suggestions: PlacementSuggestion[] = [];
// 1. Metadata-based suggestions (if module has placement metadata)
if (metadata) {
suggestions.push(...this.getMetadataBasedSuggestions(metadata, canvasState));
}
// 2. Pattern-based suggestions (common layouts)
suggestions.push(...this.getPatternBasedSuggestions(moduleId, canvasState));
// 3. AI-learned suggestions (from training data)
// TODO: Implement when AI training system is ready
// suggestions.push(...this.getAILearnedSuggestions(moduleId, canvasState));
// 4. User history suggestions (if user has placed this module before)
if (userContext?.userHistory) {
suggestions.push(...this.getUserHistorySuggestions(moduleId, userContext.userHistory));
}
// Sort by confidence (highest first)
suggestions.sort((a, b) => b.confidence - a.confidence);
// Limit to top 5 suggestions
return suggestions.slice(0, 5);
}
/**
* Auto-place module with intelligent defaults
* @param moduleId - Module to place
* @param canvasState - Current canvas state
* @param targetSectionId - Optional specific target section
* @returns Section configuration for the placed module
*/
autoPlaceModule(
moduleId: string,
canvasState: CanvasState,
targetSectionId?: string
): Section {
const metadata = this.getModulePlacementMetadata(moduleId);
const module = this.moduleRegistry.get(moduleId);
if (!module) {
throw new Error(`Module not found: ${moduleId}`);
}
// Create section with intelligent defaults
const section: Partial<Section> = {
type: 'content',
content: {
type: 'module',
moduleId,
config: {}
},
// Sizing based on metadata
sizing: {
width: metadata?.autoSize ? 'auto' : '100%',
height: metadata?.autoSize ? 'auto' : 'auto',
minWidth: metadata?.minWidth,
minHeight: metadata?.minHeight,
maxWidth: metadata?.maxWidth,
maxHeight: metadata?.maxHeight,
},
// Positioning based on constraints
position: {
type: metadata?.constraints?.requiresFixedPosition ? 'fixed' : 'relative',
},
// Default layout (flexbox for platform-agnostic)
layout: {
engine: 'flex',
config: {
direction: 'column',
justify: 'start',
align: 'stretch',
}
},
// AI metadata for training
ai: {
pattern: metadata?.preferredLocation || 'custom',
generatedBy: 'ai' as const,
userIntent: 'module-placement',
}
};
return section as Section; // Type assertion - full Section will be created by factory
}
/**
* Validate a proposed module placement
* @param moduleId - Module to validate
* @param sectionId - Target section
* @param canvasState - Current canvas state
* @returns Validation result
*/
validatePlacement(
moduleId: string,
sectionId: string,
canvasState: CanvasState
): PlacementValidation {
const metadata = this.getModulePlacementMetadata(moduleId);
const section = canvasState.sectionsMap[sectionId];
const errors: string[] = [];
const warnings: string[] = [];
if (!section) {
errors.push(`Section not found: ${sectionId}`);
return { valid: false, errors };
}
// Check constraints if metadata exists
if (metadata?.constraints) {
const { constraints } = metadata;
// Check root level requirement
if (constraints.requiresRootLevel && section.parentId) {
errors.push('This module must be placed at root level');
}
// Check full width requirement
if (constraints.requiresFullWidth && section.sizing.width !== '100%') {
warnings.push('This module works best at full width');
}
// Check incompatible modules
if (constraints.incompatibleWith) {
const conflictingModule = this.findConflictingModule(
section,
constraints.incompatibleWith,
canvasState
);
if (conflictingModule) {
errors.push(`This module cannot be placed with: ${conflictingModule}`);
}
}
}
return {
valid: errors.length === 0,
errors: errors.length > 0 ? errors : undefined,
warnings: warnings.length > 0 ? warnings : undefined,
};
}
// ============================================================================
// PRIVATE HELPERS
// ============================================================================
/**
* Get suggestions based on module placement metadata
*/
private getMetadataBasedSuggestions(
metadata: ModulePlacementMetadata,
canvasState: CanvasState
): PlacementSuggestion[] {
const suggestions: PlacementSuggestion[] = [];
// If module has preferred location
if (metadata.preferredLocation) {
const targetSection = this.findSectionByLocation(
metadata.preferredLocation,
canvasState
);
if (targetSection) {
suggestions.push({
targetSectionId: targetSection.id,
confidence: 0.9,
reason: `Module prefers ${metadata.preferredLocation} location`,
source: 'metadata',
preview: {
location: this.getLocationDescription(metadata.preferredLocation),
layoutType: metadata.autoSize ? 'Auto-sized' : 'Flexible'
}
});
} else {
// Suggest creating new section
suggestions.push({
targetSectionId: 'new',
parentSectionId: canvasState.rootSection.id,
confidence: 0.85,
reason: `Create new ${metadata.preferredLocation} section`,
source: 'metadata',
});
}
}
return suggestions;
}
/**
* Get suggestions based on common patterns
*/
private getPatternBasedSuggestions(
moduleId: string,
canvasState: CanvasState
): PlacementSuggestion[] {
const suggestions: PlacementSuggestion[] = [];
const module = this.moduleRegistry.get(moduleId);
if (!module) return suggestions;
// Pattern: If canvas is empty, suggest root section
if (Object.keys(canvasState.sectionsMap).length === 1) {
suggestions.push({
targetSectionId: canvasState.rootSection.id,
confidence: 0.7,
reason: 'Place in empty canvas',
source: 'pattern',
});
}
// Pattern: Structural modules go at edges
if (module.manifest.category === 'structural') {
// Find header/footer sections
const headerSection = this.findSectionByPattern(canvasState, 'header');
if (!headerSection) {
suggestions.push({
targetSectionId: 'new',
parentSectionId: canvasState.rootSection.id,
confidence: 0.75,
reason: 'Structural modules typically go in header area',
source: 'pattern',
});
}
}
return suggestions;
}
/**
* Get suggestions based on user history
*/
private getUserHistorySuggestions(
moduleId: string,
userHistory: any // TODO: Define UserPlacementHistory type
): PlacementSuggestion[] {
// TODO: Implement when user history tracking is ready
return [];
}
/**
* Find section by location preference
*/
private findSectionByLocation(
location: string,
canvasState: CanvasState
): Section | null {
// Simple implementation - can be enhanced with pattern matching
const sections = Object.values(canvasState.sectionsMap);
return sections.find(section =>
section.ai?.pattern === location ||
section.name?.toLowerCase().includes(location)
) || null;
}
/**
* Find section by pattern (heuristic)
*/
private findSectionByPattern(
canvasState: CanvasState,
pattern: string
): Section | null {
const sections = Object.values(canvasState.sectionsMap);
return sections.find(s => s.ai?.pattern === pattern) || null;
}
/**
* Check for conflicting modules
*/
private findConflictingModule(
section: Section,
incompatibleModules: string[],
canvasState: CanvasState
): string | null {
// Check siblings and parent sections for incompatible modules
const parentId = section.parentId;
if (!parentId) return null;
const parent = canvasState.sectionsMap[parentId];
if (!parent) return null;
for (const childId of parent.children) {
const sibling = canvasState.sectionsMap[childId];
if (sibling?.content?.moduleId &&
incompatibleModules.includes(sibling.content.moduleId)) {
return sibling.content.moduleId;
}
}
return null;
}
/**
* Get human-readable location description
*/
private getLocationDescription(location: string): string {
const descriptions: Record<string, string> = {
header: 'Top of canvas',
footer: 'Bottom of canvas',
sidebar: 'Left or right edge',
content: 'Main content area',
floating: 'Overlay/modal'
};
return descriptions[location] || 'Custom position';
}
}
🎯 Usage Examples
Example 1: Get Suggestions for Module Placement
// In component
export class CanvasRootComponent {
private modulePlacement = inject(ModulePlacementService);
private canvasState = inject(CanvasStateService);
onModuleSelected(moduleId: string): void {
const canvas = this.canvasState.canvas();
if (!canvas) return;
// Get intelligent suggestions
const suggestions = this.modulePlacement.getSuggestedPlacements(
moduleId,
canvas
);
console.log('Placement suggestions:', suggestions);
// If high confidence, auto-place
if (suggestions[0]?.confidence > 0.9) {
this.autoPlace(moduleId, suggestions[0]);
} else {
// Show suggestions to user
this.showPlacementOptions(suggestions);
}
}
}
Example 2: Auto-Place Module
onModuleDrop(moduleId: string, targetSectionId?: string): void {
const canvas = this.canvasState.canvas();
if (!canvas) return;
// Create section with intelligent defaults
const section = this.modulePlacement.autoPlaceModule(
moduleId,
canvas,
targetSectionId
);
// Add to canvas
this.canvasState.addSection(targetSectionId || null, section);
}
Example 3: Validate Before Placement
onPlacementAttempt(moduleId: string, sectionId: string): void {
const canvas = this.canvasState.canvas();
if (!canvas) return;
// Validate first
const validation = this.modulePlacement.validatePlacement(
moduleId,
sectionId,
canvas
);
if (!validation.valid) {
this.showErrors(validation.errors!);
return;
}
if (validation.warnings) {
this.showWarnings(validation.warnings);
}
// Proceed with placement
this.placeModule(moduleId, sectionId);
}
🤖 AI Training Integration (Future)
Training Data Collection
interface PlacementTrainingData {
moduleId: string;
canvasStateBefore: CanvasSnapshot;
placementChosen: PlacementSuggestion;
suggestionsShown: PlacementSuggestion[];
userKeptPlacement: boolean;
timeTaken: number;
}
Pattern Learning
The service will eventually learn:
- Which modules are commonly placed together
- Optimal layouts for different use cases
- User preferences and patterns
- Successful placement configurations
✅ Implementation Checklist
Phase 1: Types & Basic Service
- Create
module-placement.types.tswith all type definitions - Create TypeScript declaration merging for ModuleManifest
- Create
module-placement.service.tswith basic structure - Implement
getModulePlacementMetadata() - Implement
autoPlaceModule()
Phase 2: Suggestion Engine
- Implement
getSuggestedPlacements() - Implement
getMetadataBasedSuggestions() - Implement
getPatternBasedSuggestions() - Implement helper methods
Phase 3: Validation
- Implement
validatePlacement() - Add constraint checking
- Add conflict detection
Phase 4: AI Integration (Future)
- Design training data collection
- Implement AI-learned suggestions
- Add user history tracking
- Pattern recognition enhancement
Status: ✅ Design Complete
Next: Implement types and basic service
Complexity: Medium
Estimated Time: 4-6 hours