feat(canvas): Add Phase 3+4 - Bidirectional sync, templates, and visual polish
Phase 3 - Bidirectional Sync: - Add loadFromIntent and loadFromConfig to canvas store - Create useIntentParser hook for parsing Claude messages - Create ConfigImporter component (file upload, paste JSON, load study) - Add import/clear buttons to CanvasView header Phase 4 - Templates & Polish: - Create template library with 5 presets: - Mass Minimization (single-objective) - Multi-Objective Pareto (NSGA-II) - Turbo Mode (with MLP surrogate) - Mirror Zernike (optical optimization) - Frequency Optimization (modal) - Create TemplateSelector component with category filters - Enhanced BaseNode with animations, glow effects, status indicators - Add colorBg to all node types for visual distinction - Add notification toast system - Update all exports Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
172
atomizer-dashboard/frontend/src/hooks/useIntentParser.ts
Normal file
172
atomizer-dashboard/frontend/src/hooks/useIntentParser.ts
Normal file
@@ -0,0 +1,172 @@
|
||||
/**
|
||||
* useIntentParser - Parse Claude messages for canvas updates
|
||||
*
|
||||
* Detects structured intents in chat messages and applies them to canvas
|
||||
*/
|
||||
|
||||
import { useCallback } from 'react';
|
||||
import { useCanvasStore, OptimizationConfig } from './useCanvasStore';
|
||||
import { OptimizationIntent } from '../lib/canvas/intent';
|
||||
import { Message } from '../components/chat/ChatMessage';
|
||||
|
||||
interface ParsedUpdate {
|
||||
type: 'intent' | 'config' | 'suggestion' | 'none';
|
||||
data?: OptimizationIntent | OptimizationConfig;
|
||||
suggestions?: string[];
|
||||
}
|
||||
|
||||
export function useIntentParser() {
|
||||
const { loadFromIntent, loadFromConfig, updateNodeData, nodes } = useCanvasStore();
|
||||
|
||||
/**
|
||||
* Parse a message for structured intent JSON
|
||||
*/
|
||||
const parseIntent = useCallback((content: string): ParsedUpdate => {
|
||||
// Look for JSON blocks in the message
|
||||
const jsonBlockRegex = /```(?:json)?\s*\n?([\s\S]*?)\n?```/g;
|
||||
const matches = [...content.matchAll(jsonBlockRegex)];
|
||||
|
||||
for (const match of matches) {
|
||||
try {
|
||||
const parsed = JSON.parse(match[1]);
|
||||
|
||||
// Check if it's an OptimizationIntent
|
||||
if (parsed.version && parsed.source === 'canvas') {
|
||||
return { type: 'intent', data: parsed as OptimizationIntent };
|
||||
}
|
||||
|
||||
// Check if it's an OptimizationConfig (has design_variables or objectives)
|
||||
if (parsed.design_variables || parsed.objectives || parsed.model?.path) {
|
||||
return { type: 'config', data: parsed as OptimizationConfig };
|
||||
}
|
||||
} catch {
|
||||
// Not valid JSON, continue
|
||||
}
|
||||
}
|
||||
|
||||
// Look for inline JSON (without code blocks)
|
||||
const inlineJsonRegex = /\{[\s\S]*?"(?:version|design_variables|objectives)"[\s\S]*?\}/g;
|
||||
const inlineMatches = content.match(inlineJsonRegex);
|
||||
|
||||
if (inlineMatches) {
|
||||
for (const jsonStr of inlineMatches) {
|
||||
try {
|
||||
const parsed = JSON.parse(jsonStr);
|
||||
if (parsed.version && parsed.source === 'canvas') {
|
||||
return { type: 'intent', data: parsed as OptimizationIntent };
|
||||
}
|
||||
if (parsed.design_variables || parsed.objectives) {
|
||||
return { type: 'config', data: parsed as OptimizationConfig };
|
||||
}
|
||||
} catch {
|
||||
// Not valid JSON
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Look for suggestions in the message
|
||||
const suggestions = extractSuggestions(content);
|
||||
if (suggestions.length > 0) {
|
||||
return { type: 'suggestion', suggestions };
|
||||
}
|
||||
|
||||
return { type: 'none' };
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Extract actionable suggestions from Claude's response
|
||||
*/
|
||||
const extractSuggestions = (content: string): string[] => {
|
||||
const suggestions: string[] = [];
|
||||
|
||||
// Common suggestion patterns
|
||||
const patterns = [
|
||||
/suggest(?:ing)?\s+(?:to\s+)?(?:add|include|use)\s+([^.!?\n]+)/gi,
|
||||
/recommend(?:ing)?\s+([^.!?\n]+)/gi,
|
||||
/consider\s+(?:adding|using|including)\s+([^.!?\n]+)/gi,
|
||||
/you\s+(?:should|could|might)\s+(?:add|include|use)\s+([^.!?\n]+)/gi,
|
||||
];
|
||||
|
||||
for (const pattern of patterns) {
|
||||
const matches = content.matchAll(pattern);
|
||||
for (const match of matches) {
|
||||
if (match[1]) {
|
||||
suggestions.push(match[1].trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return suggestions;
|
||||
};
|
||||
|
||||
/**
|
||||
* Apply a parsed update to the canvas
|
||||
*/
|
||||
const applyUpdate = useCallback((update: ParsedUpdate): boolean => {
|
||||
switch (update.type) {
|
||||
case 'intent':
|
||||
if (update.data && 'version' in update.data) {
|
||||
loadFromIntent(update.data as OptimizationIntent);
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case 'config':
|
||||
if (update.data && !('version' in update.data)) {
|
||||
loadFromConfig(update.data as OptimizationConfig);
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case 'suggestion':
|
||||
// Suggestions are returned but not auto-applied
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}, [loadFromIntent, loadFromConfig]);
|
||||
|
||||
/**
|
||||
* Process a message and optionally apply updates
|
||||
*/
|
||||
const processMessage = useCallback((message: Message, autoApply = false): ParsedUpdate => {
|
||||
const update = parseIntent(message.content);
|
||||
|
||||
if (autoApply && (update.type === 'intent' || update.type === 'config')) {
|
||||
applyUpdate(update);
|
||||
}
|
||||
|
||||
return update;
|
||||
}, [parseIntent, applyUpdate]);
|
||||
|
||||
/**
|
||||
* Check if a message contains canvas-related content
|
||||
*/
|
||||
const hasCanvasContent = useCallback((content: string): boolean => {
|
||||
const keywords = [
|
||||
'design variable', 'objective', 'constraint', 'extractor',
|
||||
'optimization', 'algorithm', 'surrogate', 'model', 'solver',
|
||||
'canvas', 'workflow', 'node'
|
||||
];
|
||||
const lowerContent = content.toLowerCase();
|
||||
return keywords.some(kw => lowerContent.includes(kw));
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Apply parameter updates to specific nodes
|
||||
*/
|
||||
const applyParameterUpdates = useCallback((updates: Record<string, Partial<unknown>>) => {
|
||||
for (const [nodeId, data] of Object.entries(updates)) {
|
||||
const node = nodes.find(n => n.id === nodeId);
|
||||
if (node) {
|
||||
updateNodeData(nodeId, data as Record<string, unknown>);
|
||||
}
|
||||
}
|
||||
}, [nodes, updateNodeData]);
|
||||
|
||||
return {
|
||||
parseIntent,
|
||||
applyUpdate,
|
||||
processMessage,
|
||||
hasCanvasContent,
|
||||
applyParameterUpdates,
|
||||
extractSuggestions,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user