docs: Comprehensive documentation update for Dashboard V3 and Canvas

## Documentation Updates
- DASHBOARD.md: Updated to V3.0 with Canvas V3 features, file browser, introspection
- DASHBOARD_IMPLEMENTATION_STATUS.md: Marked Canvas V3 features as COMPLETE
- CANVAS.md: New comprehensive guide for Canvas Builder V3 with all features
- CLAUDE.md: Added dashboard quick reference and Canvas V3 features

## Canvas V3 Features Documented
- File Browser: Browse studies directory for model files
- Model Introspection: Auto-discover expressions, solver type, dependencies
- One-Click Add: Add expressions as design variables instantly
- Claude Bug Fixes: WebSocket reconnection, SQL errors resolved
- Health Check: /api/health endpoint for monitoring

## Backend Services
- NX introspection service with expression discovery
- File browser API with type filtering
- Claude session management improvements
- Context builder enhancements

## Frontend Components
- FileBrowser: Modal for file selection with search
- IntrospectionPanel: View discovered model information
- ExpressionSelector: Dropdown for design variable configuration
- Improved chat hooks with reconnection logic

## Plan Documents
- Added RALPH_LOOP_CANVAS_V2/V3 implementation records
- Added ATOMIZER_DASHBOARD_V2_MASTER_PLAN
- Added investigation and sync documentation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-16 20:48:58 -05:00
parent 1c7c7aff05
commit ac5e9b4054
23 changed files with 10860 additions and 773 deletions

View File

@@ -3,15 +3,22 @@
*
* Bridges the Canvas UI with the Chat system, allowing canvas intents
* to be sent to Claude for intelligent execution.
*
* Key features:
* - Passes canvas state to Claude for context awareness
* - Handles canvas modification instructions from Claude
*/
import { useCallback, useState } from 'react';
import { useChat, ChatMode } from './useChat';
import { useCallback, useState, useEffect } from 'react';
import { useChat, ChatMode, CanvasState } from './useChat';
import { OptimizationIntent, formatIntentForChat } from '../lib/canvas/intent';
import { useCanvasStore } from './useCanvasStore';
interface UseCanvasChatOptions {
mode?: ChatMode;
onError?: (error: string) => void;
studyName?: string;
studyPath?: string;
}
interface CanvasChatState {
@@ -32,8 +39,39 @@ interface ExecutionResult {
export function useCanvasChat({
mode = 'user',
onError,
studyName,
studyPath,
}: UseCanvasChatOptions = {}) {
const chat = useChat({ mode, onError });
// Get canvas state from the store
const { nodes, edges, addNode, updateNodeData } = useCanvasStore();
// Build canvas state object for chat context
const canvasState: CanvasState = {
nodes: nodes.map(n => ({
id: n.id,
type: n.type,
data: n.data,
position: n.position,
})),
edges: edges.map(e => ({
id: e.id,
source: e.source,
target: e.target,
})),
studyName,
studyPath,
};
const chat = useChat({
mode,
onError,
canvasState,
});
// Sync canvas state to chat whenever it changes
useEffect(() => {
chat.updateCanvasState(canvasState);
}, [nodes, edges, studyName, studyPath]);
const [state, setState] = useState<CanvasChatState>({
isExecuting: false,
@@ -156,6 +194,61 @@ ${question}`;
[chat]
);
/**
* Apply a canvas modification from Claude's tool response
* This is called when Claude uses canvas_add_node, canvas_update_node, etc.
*/
const applyModification = useCallback(
(modification: {
action: 'add_node' | 'update_node' | 'remove_node' | 'add_edge';
nodeType?: string;
nodeId?: string;
data?: Record<string, unknown>;
source?: string;
target?: string;
}) => {
switch (modification.action) {
case 'add_node':
if (modification.nodeType && modification.data) {
// Calculate a position for the new node
const existingNodesOfType = nodes.filter(n => n.type === modification.nodeType);
const baseX = modification.nodeType === 'designVar' ? 50 : 740;
const newY = 50 + existingNodesOfType.length * 100;
addNode(modification.nodeType as any, { x: baseX, y: newY }, modification.data as any);
}
break;
case 'update_node':
if (modification.nodeId && modification.data) {
const findBy = (modification.data.findBy as string) || 'label';
const updates = { ...modification.data };
delete updates.findBy;
// Find node by ID or label
let targetNode;
if (findBy === 'id') {
targetNode = nodes.find(n => n.id === modification.nodeId);
} else {
targetNode = nodes.find(n =>
n.data?.label === modification.nodeId ||
(n.data as any)?.expressionName === modification.nodeId ||
(n.data as any)?.name === modification.nodeId
);
}
if (targetNode) {
updateNodeData(targetNode.id, updates as any);
}
}
break;
// Add other cases as needed
}
},
[nodes, addNode, updateNodeData]
);
return {
// Chat state
messages: chat.messages,
@@ -175,10 +268,12 @@ ${question}`;
executeIntent,
analyzeIntent,
askAboutCanvas,
applyModification,
// Base chat actions
sendMessage: chat.sendMessage,
clearMessages: chat.clearMessages,
switchMode: chat.switchMode,
updateCanvasState: chat.updateCanvasState,
};
}

View File

@@ -28,7 +28,7 @@ interface CanvasState {
loadFromConfig: (config: OptimizationConfig) => void;
}
// Optimization config structure (from optimization_config.json)
// Optimization config structure (matching actual optimization_config.json format)
export interface OptimizationConfig {
study_name?: string;
model?: {
@@ -39,27 +39,68 @@ export interface OptimizationConfig {
type?: string;
solution?: number;
};
// Design variables - actual format uses min/max, not lower/upper
design_variables?: Array<{
name: string;
expression_name?: string;
lower: number;
upper: number;
min: number;
max: number;
baseline?: number;
units?: string;
enabled?: boolean;
notes?: string;
type?: string;
// Legacy support - some configs use lower/upper
lower?: number;
upper?: number;
}>;
// Extraction method for Zernike or other physics
extraction_method?: {
type?: 'zernike_opd' | 'displacement' | 'stress' | 'mass' | 'frequency';
class?: string;
method?: string;
inner_radius?: number;
outer_radius?: number;
};
// Zernike-specific settings
zernike_settings?: {
n_modes?: number;
filter_low_orders?: number;
subcases?: string[];
subcase_labels?: Record<string, string>;
reference_subcase?: string;
};
objectives?: Array<{
name: string;
direction?: string;
weight?: number;
extractor?: string;
penalty_weight?: number; // For hard constraint conversion
}>;
constraints?: Array<{
name: string;
type?: string;
value?: number;
extractor?: string;
penalty_weight?: number;
}>;
// Hard constraints (common in real configs)
hard_constraints?: Array<{
name: string;
limit: number;
penalty_weight: number;
}>;
// Fixed parameters (not optimized)
fixed_parameters?: Record<string, number | string>;
// Optimization settings
method?: string;
max_trials?: number;
optimization?: {
sampler?: string;
n_trials?: number;
sigma0?: number;
restart_strategy?: string;
};
surrogate?: {
type?: string;
min_trials?: number;
@@ -340,19 +381,20 @@ export const useCanvasStore = create<CanvasState>((set, get) => ({
},
loadFromConfig: (config) => {
// Complete rewrite: Create all nodes and edges directly from config
// Complete rewrite: Create all nodes and edges from actual optimization_config.json
nodeIdCounter = 0;
const nodes: Node<CanvasNodeData>[] = [];
const edges: Edge[] = [];
// Column positions for proper layout
const COLS = {
modelDvar: 50,
solver: 280,
extractor: 510,
objCon: 740,
algo: 970,
surrogate: 1200,
designVar: 50,
model: 280,
solver: 510,
extractor: 740,
objCon: 1020,
algo: 1300,
surrogate: 1530,
};
const ROW_HEIGHT = 100;
const START_Y = 50;
@@ -370,64 +412,104 @@ export const useCanvasStore = create<CanvasState>((set, get) => ({
};
// 1. Model node
const modelId = createNode('model', COLS.modelDvar, START_Y, {
const modelId = createNode('model', COLS.model, START_Y, {
label: config.study_name || 'Model',
filePath: config.model?.path,
fileType: config.model?.type as 'prt' | 'fem' | 'sim' | undefined,
});
// 2. Solver node
const solverType = config.solver?.solution ? `SOL${config.solver.solution}` : undefined;
const solverType = config.solver?.solution ? `SOL${config.solver.solution}` : config.solver?.type;
const solverId = createNode('solver', COLS.solver, START_Y, {
label: 'Solver',
label: solverType || 'Solver',
solverType: solverType as any,
});
edges.push({ id: `e_model_solver`, source: modelId, target: solverId });
// 3. Design variables (column 0, below model)
let dvRow = 1;
for (const dv of config.design_variables || []) {
const dvId = createNode('designVar', COLS.modelDvar, START_Y + dvRow * ROW_HEIGHT, {
// 3. Design variables - use min/max (actual format), fallback to lower/upper (legacy)
let dvRow = 0;
const enabledDvs = (config.design_variables || []).filter(dv => dv.enabled !== false);
for (const dv of enabledDvs) {
const minVal = dv.min ?? dv.lower ?? 0;
const maxVal = dv.max ?? dv.upper ?? 1;
const dvId = createNode('designVar', COLS.designVar, START_Y + dvRow * ROW_HEIGHT, {
label: dv.expression_name || dv.name,
expressionName: dv.expression_name || dv.name,
minValue: dv.lower,
maxValue: dv.upper,
minValue: minVal,
maxValue: maxVal,
baseline: dv.baseline,
unit: dv.units,
enabled: dv.enabled ?? true,
notes: dv.notes,
});
edges.push({ id: `e_dv_${dvRow}_model`, source: dvId, target: modelId });
dvRow++;
}
// 4. Extractors - infer from objectives and constraints
// 4. Extractors - create from extraction_method if available
const extractorNames: Record<string, string> = {
'E1': 'Displacement', 'E2': 'Frequency', 'E3': 'Solid Stress',
'E4': 'BDF Mass', 'E5': 'CAD Mass', 'E8': 'Zernike (OP2)',
'E9': 'Zernike (CSV)', 'E10': 'Zernike (RMS)',
'zernike_opd': 'Zernike OPD',
};
const extractorIds = new Set<string>();
for (const obj of config.objectives || []) {
if (obj.extractor) extractorIds.add(obj.extractor);
}
for (const con of config.constraints || []) {
if (con.extractor) extractorIds.add(con.extractor);
}
// If no extractors found, add a default based on objectives
if (extractorIds.size === 0 && (config.objectives?.length || 0) > 0) {
extractorIds.add('E5'); // Default to CAD Mass
}
let extRow = 0;
const extractorMap: Record<string, string> = {};
for (const extId of extractorIds) {
const nodeId = createNode('extractor', COLS.extractor, START_Y + extRow * ROW_HEIGHT, {
label: extractorNames[extId] || extId,
extractorId: extId,
extractorName: extractorNames[extId] || extId,
const extractorNodeIds: string[] = [];
// Check for extraction_method (Zernike configs)
if (config.extraction_method) {
const extType = config.extraction_method.type || 'zernike_opd';
const zernikeSettings = config.zernike_settings || {};
const extId = createNode('extractor', COLS.extractor, START_Y + extRow * ROW_HEIGHT, {
label: extractorNames[extType] || config.extraction_method.class || 'Extractor',
extractorId: extType === 'zernike_opd' ? 'E8' : extType,
extractorName: extractorNames[extType] || extType,
extractorType: extType,
extractMethod: config.extraction_method.method,
innerRadius: config.extraction_method.inner_radius,
nModes: zernikeSettings.n_modes,
subcases: zernikeSettings.subcases,
config: {
innerRadius: config.extraction_method.inner_radius,
outerRadius: config.extraction_method.outer_radius,
nModes: zernikeSettings.n_modes,
filterLowOrders: zernikeSettings.filter_low_orders,
subcases: zernikeSettings.subcases,
subcaseLabels: zernikeSettings.subcase_labels,
referenceSubcase: zernikeSettings.reference_subcase,
extractMethod: config.extraction_method.method,
},
// Output names from objectives that use this extractor
outputNames: config.objectives?.map(o => o.name) || [],
});
extractorMap[extId] = nodeId;
edges.push({ id: `e_solver_ext_${extRow}`, source: solverId, target: nodeId });
extractorNodeIds.push(extId);
edges.push({ id: `e_solver_ext_${extRow}`, source: solverId, target: extId });
extRow++;
} else {
// Fallback: infer extractors from objectives
const extractorIds = new Set<string>();
for (const obj of config.objectives || []) {
if (obj.extractor) extractorIds.add(obj.extractor);
}
for (const con of config.constraints || []) {
if (con.extractor) extractorIds.add(con.extractor);
}
if (extractorIds.size === 0 && (config.objectives?.length || 0) > 0) {
extractorIds.add('E5'); // Default
}
for (const extId of extractorIds) {
const nodeId = createNode('extractor', COLS.extractor, START_Y + extRow * ROW_HEIGHT, {
label: extractorNames[extId] || extId,
extractorId: extId,
extractorName: extractorNames[extId] || extId,
});
extractorNodeIds.push(nodeId);
edges.push({ id: `e_solver_ext_${extRow}`, source: solverId, target: nodeId });
extRow++;
}
}
// 5. Objectives
@@ -439,18 +521,34 @@ export const useCanvasStore = create<CanvasState>((set, get) => ({
name: obj.name,
direction: (obj.direction as 'minimize' | 'maximize') || 'minimize',
weight: obj.weight || 1,
penaltyWeight: obj.penalty_weight,
});
objIds.push(objId);
// Connect to extractor
const extNodeId = obj.extractor ? extractorMap[obj.extractor] : Object.values(extractorMap)[0];
if (extNodeId) {
edges.push({ id: `e_ext_obj_${objRow}`, source: extNodeId, target: objId });
// Connect to first extractor (or specific if mapped)
if (extractorNodeIds.length > 0) {
edges.push({ id: `e_ext_obj_${objRow}`, source: extractorNodeIds[0], target: objId });
}
objRow++;
}
// 6. Constraints
// 6. Hard constraints (converted to objectives with penalties)
for (const hc of config.hard_constraints || []) {
const hcId = createNode('objective', COLS.objCon, START_Y + objRow * ROW_HEIGHT, {
label: `${hc.name} (constraint)`,
name: hc.name,
direction: 'minimize',
weight: hc.penalty_weight,
penaltyWeight: hc.penalty_weight,
});
objIds.push(hcId);
if (extractorNodeIds.length > 0) {
edges.push({ id: `e_ext_hc_${objRow}`, source: extractorNodeIds[0], target: hcId });
}
objRow++;
}
// 7. Regular constraints
let conRow = objRow;
const conIds: string[] = [];
for (const con of config.constraints || []) {
@@ -461,22 +559,21 @@ export const useCanvasStore = create<CanvasState>((set, get) => ({
value: con.value || 0,
});
conIds.push(conId);
// Connect to extractor
const extNodeId = con.extractor ? extractorMap[con.extractor] : Object.values(extractorMap)[0];
if (extNodeId) {
edges.push({ id: `e_ext_con_${conRow}`, source: extNodeId, target: conId });
if (extractorNodeIds.length > 0) {
edges.push({ id: `e_ext_con_${conRow}`, source: extractorNodeIds[0], target: conId });
}
conRow++;
}
// 7. Algorithm node
const method = config.method || (config as any).optimization?.sampler || 'TPE';
const maxTrials = config.max_trials || (config as any).optimization?.n_trials || 100;
// 8. Algorithm node
const method = config.method || config.optimization?.sampler || 'TPE';
const maxTrials = config.max_trials || config.optimization?.n_trials || 100;
const algoId = createNode('algorithm', COLS.algo, START_Y, {
label: 'Algorithm',
label: method,
method: method as any,
maxTrials: maxTrials,
sigma0: config.optimization?.sigma0,
restartStrategy: config.optimization?.restart_strategy as any,
});
// Connect objectives to algorithm
@@ -488,7 +585,7 @@ export const useCanvasStore = create<CanvasState>((set, get) => ({
edges.push({ id: `e_con_${i}_algo`, source: conIds[i], target: algoId });
}
// 8. Surrogate node (if enabled)
// 9. Surrogate node (if enabled)
if (config.surrogate) {
const surId = createNode('surrogate', COLS.surrogate, START_Y, {
label: 'Surrogate',

View File

@@ -4,10 +4,18 @@ import { ToolCall } from '../components/chat/ToolCallCard';
export type ChatMode = 'user' | 'power';
export interface CanvasState {
nodes: any[];
edges: any[];
studyName?: string;
studyPath?: string;
}
interface UseChatOptions {
studyId?: string | null;
mode?: ChatMode;
useWebSocket?: boolean;
canvasState?: CanvasState | null;
onError?: (error: string) => void;
}
@@ -25,6 +33,7 @@ export function useChat({
studyId,
mode = 'user',
useWebSocket = true,
canvasState: initialCanvasState,
onError,
}: UseChatOptions = {}) {
const [state, setState] = useState<ChatState>({
@@ -37,6 +46,9 @@ export function useChat({
isConnected: false,
});
// Track canvas state for sending with messages
const canvasStateRef = useRef<CanvasState | null>(initialCanvasState || null);
const abortControllerRef = useRef<AbortController | null>(null);
const conversationHistoryRef = useRef<Array<{ role: string; content: string }>>([]);
const wsRef = useRef<WebSocket | null>(null);
@@ -196,6 +208,10 @@ export function useChat({
// Study context was updated - could show notification
break;
case 'canvas_updated':
// Canvas state was updated - could show notification
break;
case 'pong':
// Heartbeat response - ignore
break;
@@ -283,11 +299,12 @@ export function useChat({
currentMessageRef.current = '';
currentToolCallsRef.current = [];
// Send message via WebSocket
// Send message via WebSocket with canvas state
wsRef.current.send(
JSON.stringify({
type: 'message',
content: content.trim(),
canvas_state: canvasStateRef.current || undefined,
})
);
return;
@@ -430,6 +447,21 @@ export function useChat({
}
}, []);
// Update canvas state (call this when canvas changes)
const updateCanvasState = useCallback((newCanvasState: CanvasState | null) => {
canvasStateRef.current = newCanvasState;
// Also send to backend to update context
if (useWebSocket && wsRef.current?.readyState === WebSocket.OPEN) {
wsRef.current.send(
JSON.stringify({
type: 'set_canvas',
canvas_state: newCanvasState,
})
);
}
}, [useWebSocket]);
return {
messages: state.messages,
isThinking: state.isThinking,
@@ -442,5 +474,6 @@ export function useChat({
clearMessages,
cancelRequest,
switchMode,
updateCanvasState,
};
}