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>
173 lines
5.2 KiB
TypeScript
173 lines
5.2 KiB
TypeScript
/**
|
|
* 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,
|
|
};
|
|
}
|