mirror of
https://github.com/The-Low-Code-Foundation/OpenNoodl.git
synced 2026-03-08 01:53:30 +01:00
524 lines
19 KiB
JavaScript
524 lines
19 KiB
JavaScript
"use strict";
|
|
(global["webpackChunknoodl_editor"] = global["webpackChunknoodl_editor"] || []).push([[605],{
|
|
|
|
/***/ 61605:
|
|
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
|
|
|
|
|
|
// EXPORTS
|
|
__webpack_require__.d(__webpack_exports__, {
|
|
AIMigrationOrchestrator: () => (/* binding */ AIMigrationOrchestrator)
|
|
});
|
|
|
|
;// ./src/editor/src/utils/migration/claudePrompts.ts
|
|
/**
|
|
* Claude AI Prompts for React 19 Migration
|
|
*
|
|
* System prompts and templates for guiding Claude to migrate
|
|
* React components from React 17 patterns to React 19.
|
|
*
|
|
* @module migration/claudePrompts
|
|
*/
|
|
const MIGRATION_SYSTEM_PROMPT = `You are a React migration assistant for OpenNoodl, a visual programming platform. Your job is to migrate React class components from React 17 patterns to React 19.
|
|
|
|
## Your Task
|
|
Convert the provided React code to be compatible with React 19. The code may contain:
|
|
- componentWillMount (removed in React 19)
|
|
- componentWillReceiveProps (removed in React 19)
|
|
- componentWillUpdate (removed in React 19)
|
|
- UNSAFE_ prefixed lifecycle methods (removed in React 19)
|
|
- String refs (removed in React 19)
|
|
- Legacy context API (removed in React 19)
|
|
- React.createFactory (removed in React 19)
|
|
|
|
## Migration Rules
|
|
|
|
### Lifecycle Methods
|
|
1. componentWillMount → Move logic to componentDidMount or constructor
|
|
2. componentWillReceiveProps → Use getDerivedStateFromProps (static) or componentDidUpdate
|
|
3. componentWillUpdate → Use getSnapshotBeforeUpdate + componentDidUpdate
|
|
|
|
### String Refs
|
|
Convert: ref="myRef" → ref={this.myRef = React.createRef()} or useRef()
|
|
|
|
### Legacy Context
|
|
Convert contextTypes/childContextTypes/getChildContext → React.createContext
|
|
|
|
### Functional Preference
|
|
If the component doesn't use complex state or many lifecycle methods, prefer converting to a functional component with hooks.
|
|
|
|
## Output Format
|
|
You MUST respond with a JSON object in this exact format:
|
|
{
|
|
"success": true,
|
|
"code": "// The migrated code here",
|
|
"changes": [
|
|
"Converted componentWillMount to useEffect",
|
|
"Replaced string ref with useRef"
|
|
],
|
|
"warnings": [
|
|
"Verify the useEffect dependency array is correct"
|
|
],
|
|
"confidence": 0.85
|
|
}
|
|
|
|
If you cannot migrate the code:
|
|
{
|
|
"success": false,
|
|
"code": null,
|
|
"reason": "Explanation of why migration failed",
|
|
"suggestion": "What the user could do manually",
|
|
"confidence": 0
|
|
}
|
|
|
|
## Rules
|
|
1. PRESERVE all existing functionality exactly
|
|
2. PRESERVE all comments unless they reference removed APIs
|
|
3. ADD comments explaining non-obvious changes
|
|
4. DO NOT change prop names or component interfaces
|
|
5. DO NOT add new dependencies
|
|
6. If confidence < 0.7, explain why in warnings
|
|
7. Test the code mentally - would it work?
|
|
|
|
## Context
|
|
This code is from an OpenNoodl project. OpenNoodl uses a custom node system where React components are wrapped. The component may reference:
|
|
- this.props.noodlNode - Reference to the Noodl node instance
|
|
- this.forceUpdate() - Triggers re-render (still valid in React 19)
|
|
- this.setStyle() - Noodl method for styling
|
|
- this.getRef() - Noodl method for DOM access`;
|
|
const RETRY_PROMPT_TEMPLATE = (/* unused pure expression or super */ null && (`The previous migration attempt failed verification.
|
|
|
|
Previous attempt result:
|
|
{previousError}
|
|
|
|
Previous code:
|
|
\`\`\`javascript
|
|
{previousCode}
|
|
\`\`\`
|
|
|
|
Please try a different approach. Consider:
|
|
1. Maybe the conversion should stay as a class component instead of functional
|
|
2. Check if state management is correct
|
|
3. Verify event handlers are bound correctly
|
|
4. Ensure refs are used correctly
|
|
|
|
Provide a new migration with the same JSON format.`));
|
|
const HELP_PROMPT_TEMPLATE = `I attempted to migrate this React component {attempts} times but couldn't produce working code.
|
|
|
|
Original code:
|
|
\`\`\`javascript
|
|
{originalCode}
|
|
\`\`\`
|
|
|
|
Attempts and errors:
|
|
{attemptHistory}
|
|
|
|
Please analyze this component and provide:
|
|
1. Why it's difficult to migrate automatically
|
|
2. Step-by-step manual migration instructions
|
|
3. Any gotchas or things to watch out for
|
|
4. Example code snippets for the tricky parts
|
|
|
|
Format your response as helpful documentation, not JSON.`;
|
|
|
|
;// ./src/editor/src/utils/migration/claudeClient.ts
|
|
/**
|
|
* Claude API Client for Component Migration
|
|
*
|
|
* Handles communication with Anthropic's Claude API for
|
|
* AI-assisted React component migration.
|
|
*
|
|
* @module migration/claudeClient
|
|
*/
|
|
|
|
class ClaudeClient {
|
|
constructor(apiKey) {
|
|
this.model = 'claude-sonnet-4-20250514';
|
|
// Pricing per 1M tokens (as of Dec 2024)
|
|
this.pricing = {
|
|
input: 3.0, // $3 per 1M input tokens
|
|
output: 15.0 // $15 per 1M output tokens
|
|
};
|
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
const Anthropic = __webpack_require__(63065);
|
|
this.client = new Anthropic({
|
|
apiKey,
|
|
dangerouslyAllowBrowser: true // Safe in Electron - code runs locally, not in public browser
|
|
});
|
|
}
|
|
async migrateComponent(request) {
|
|
const userPrompt = this.buildUserPrompt(request);
|
|
const response = await this.client.messages.create({
|
|
model: this.model,
|
|
max_tokens: 4096,
|
|
system: MIGRATION_SYSTEM_PROMPT,
|
|
messages: [{ role: 'user', content: userPrompt }]
|
|
});
|
|
const tokensUsed = {
|
|
input: response.usage.input_tokens,
|
|
output: response.usage.output_tokens
|
|
};
|
|
const cost = this.calculateCost(tokensUsed);
|
|
// Parse the response
|
|
const content = response.content[0];
|
|
if (content.type !== 'text') {
|
|
throw new Error('Unexpected response type');
|
|
}
|
|
try {
|
|
const parsed = this.parseResponse(content.text);
|
|
return {
|
|
...parsed,
|
|
tokensUsed,
|
|
cost
|
|
};
|
|
}
|
|
catch (parseError) {
|
|
return {
|
|
success: false,
|
|
code: null,
|
|
changes: [],
|
|
warnings: [],
|
|
confidence: 0,
|
|
reason: 'Failed to parse AI response',
|
|
suggestion: content.text.slice(0, 500), // Include raw response for debugging
|
|
tokensUsed,
|
|
cost
|
|
};
|
|
}
|
|
}
|
|
async getHelp(request) {
|
|
const prompt = HELP_PROMPT_TEMPLATE.replace('{attempts}', String(request.attempts))
|
|
.replace('{originalCode}', request.originalCode)
|
|
.replace('{attemptHistory}', request.attemptHistory.map((a, i) => `Attempt ${i + 1}: ${a.error}`).join('\n'));
|
|
const response = await this.client.messages.create({
|
|
model: this.model,
|
|
max_tokens: 2048,
|
|
messages: [{ role: 'user', content: prompt }]
|
|
});
|
|
const content = response.content[0];
|
|
if (content.type !== 'text') {
|
|
throw new Error('Unexpected response type');
|
|
}
|
|
return content.text;
|
|
}
|
|
buildUserPrompt(request) {
|
|
let prompt = `Migrate this React component to React 19:\n\n`;
|
|
prompt += `Component: ${request.componentName}\n\n`;
|
|
prompt += `Issues detected:\n`;
|
|
request.issues.forEach((issue) => {
|
|
prompt += `- ${issue.type} at line ${issue.location.line}: ${issue.description}\n`;
|
|
});
|
|
prompt += `\nCode:\n\`\`\`javascript\n${request.code}\n\`\`\`\n`;
|
|
if (request.preferences.preferFunctional) {
|
|
prompt += `\nPreference: Convert to functional component with hooks if clean.\n`;
|
|
}
|
|
if (request.previousAttempt) {
|
|
prompt += `\n--- RETRY ---\n`;
|
|
prompt += `Previous attempt failed: ${request.previousAttempt.error}\n`;
|
|
if (request.previousAttempt.code) {
|
|
prompt += `Previous code:\n\`\`\`javascript\n${request.previousAttempt.code}\n\`\`\`\n`;
|
|
}
|
|
prompt += `Please try a different approach.\n`;
|
|
}
|
|
return prompt;
|
|
}
|
|
parseResponse(text) {
|
|
var _a, _b, _c, _d, _e;
|
|
// Try to extract JSON from the response
|
|
const jsonMatch = text.match(/\{[\s\S]*\}/);
|
|
if (!jsonMatch) {
|
|
throw new Error('No JSON found in response');
|
|
}
|
|
const parsed = JSON.parse(jsonMatch[0]);
|
|
return {
|
|
success: (_a = parsed.success) !== null && _a !== void 0 ? _a : false,
|
|
code: (_b = parsed.code) !== null && _b !== void 0 ? _b : null,
|
|
changes: (_c = parsed.changes) !== null && _c !== void 0 ? _c : [],
|
|
warnings: (_d = parsed.warnings) !== null && _d !== void 0 ? _d : [],
|
|
confidence: (_e = parsed.confidence) !== null && _e !== void 0 ? _e : 0,
|
|
reason: parsed.reason,
|
|
suggestion: parsed.suggestion
|
|
};
|
|
}
|
|
calculateCost(tokens) {
|
|
const inputCost = (tokens.input / 1000000) * this.pricing.input;
|
|
const outputCost = (tokens.output / 1000000) * this.pricing.output;
|
|
return inputCost + outputCost;
|
|
}
|
|
estimateCost(codeLength) {
|
|
// Rough estimation: ~4 chars per token
|
|
const estimatedInputTokens = codeLength / 4 + 1000; // +1000 for system prompt
|
|
const estimatedOutputTokens = (codeLength / 4) * 1.5; // Output usually larger
|
|
return this.calculateCost({
|
|
input: estimatedInputTokens,
|
|
output: estimatedOutputTokens
|
|
});
|
|
}
|
|
}
|
|
|
|
;// ./src/editor/src/models/migration/BudgetController.ts
|
|
/**
|
|
* Budget Controller for AI Migration
|
|
*
|
|
* Manages spending limits and approval flow for AI-assisted migrations.
|
|
* Enforces hard limits and pause-and-approve at configurable increments.
|
|
*
|
|
* @module migration/BudgetController
|
|
*/
|
|
class BudgetController {
|
|
constructor(config, onPauseRequired) {
|
|
this.state = {
|
|
maxPerSession: config.maxPerSession,
|
|
spent: 0,
|
|
pauseIncrement: config.pauseIncrement,
|
|
nextPauseAt: config.pauseIncrement,
|
|
paused: false
|
|
};
|
|
this.onPauseRequired = onPauseRequired;
|
|
}
|
|
checkBudget(estimatedCost) {
|
|
const wouldExceedMax = this.state.spent + estimatedCost > this.state.maxPerSession;
|
|
const wouldExceedPause = this.state.spent + estimatedCost > this.state.nextPauseAt;
|
|
return {
|
|
allowed: !wouldExceedMax,
|
|
requiresApproval: wouldExceedPause && !this.state.paused,
|
|
currentSpent: this.state.spent,
|
|
estimatedNext: estimatedCost,
|
|
wouldExceedMax
|
|
};
|
|
}
|
|
async requestApproval(estimatedCost) {
|
|
const check = this.checkBudget(estimatedCost);
|
|
if (check.wouldExceedMax) {
|
|
return false; // Hard limit, can't approve
|
|
}
|
|
if (check.requiresApproval) {
|
|
const approved = await this.onPauseRequired(this.state);
|
|
if (approved) {
|
|
this.state.nextPauseAt += this.state.pauseIncrement;
|
|
}
|
|
return approved;
|
|
}
|
|
return true;
|
|
}
|
|
recordSpend(amount) {
|
|
this.state.spent += amount;
|
|
}
|
|
getState() {
|
|
return { ...this.state };
|
|
}
|
|
getRemainingBudget() {
|
|
return Math.max(0, this.state.maxPerSession - this.state.spent);
|
|
}
|
|
}
|
|
|
|
;// ./src/editor/src/models/migration/AIMigrationOrchestrator.ts
|
|
/**
|
|
* AI Migration Orchestrator
|
|
*
|
|
* Coordinates AI-assisted migration of multiple components with
|
|
* retry logic, verification, and user decision points.
|
|
*
|
|
* @module migration/AIMigrationOrchestrator
|
|
*/
|
|
|
|
|
|
class AIMigrationOrchestrator {
|
|
constructor(apiKey, budgetConfig, config, onBudgetPause) {
|
|
this.aborted = false;
|
|
this.client = new ClaudeClient(apiKey);
|
|
this.budget = new BudgetController(budgetConfig, onBudgetPause);
|
|
this.config = config;
|
|
}
|
|
async migrateComponent(component, code, preferences, onProgress, onDecisionRequired) {
|
|
let attempts = 0;
|
|
let totalCost = 0;
|
|
let lastError = null;
|
|
let lastCode = null;
|
|
const attemptHistory = [];
|
|
while (attempts < this.config.maxRetries && !this.aborted) {
|
|
attempts++;
|
|
// Check budget
|
|
const estimatedCost = this.client.estimateCost(code.length);
|
|
const budgetCheck = this.budget.checkBudget(estimatedCost);
|
|
if (!budgetCheck.allowed) {
|
|
return {
|
|
componentId: component.id,
|
|
componentName: component.name,
|
|
status: 'failed',
|
|
changes: [],
|
|
warnings: [],
|
|
attempts,
|
|
totalCost,
|
|
error: 'Budget exceeded'
|
|
};
|
|
}
|
|
if (budgetCheck.requiresApproval) {
|
|
const approved = await this.budget.requestApproval(estimatedCost);
|
|
if (!approved) {
|
|
return {
|
|
componentId: component.id,
|
|
componentName: component.name,
|
|
status: 'skipped',
|
|
changes: [],
|
|
warnings: ['Migration paused by user'],
|
|
attempts,
|
|
totalCost
|
|
};
|
|
}
|
|
}
|
|
// Attempt migration
|
|
onProgress({
|
|
phase: 'ai-migrating',
|
|
component: component.name,
|
|
attempt: attempts,
|
|
message: attempts === 1 ? 'Analyzing code patterns...' : `Retry attempt ${attempts}...`
|
|
});
|
|
const response = await this.client.migrateComponent({
|
|
code,
|
|
issues: component.issues,
|
|
componentName: component.name,
|
|
preferences,
|
|
previousAttempt: lastError ? { code: lastCode, error: lastError } : undefined
|
|
});
|
|
totalCost += response.cost;
|
|
this.budget.recordSpend(response.cost);
|
|
if (response.success && response.confidence >= this.config.minConfidence) {
|
|
// Verify the migration if enabled
|
|
if (this.config.verifyMigration && response.code) {
|
|
const verification = await this.verifyMigration(response.code, component);
|
|
if (!verification.valid) {
|
|
lastError = verification.error || 'Verification failed';
|
|
lastCode = response.code;
|
|
attemptHistory.push({
|
|
code: response.code,
|
|
error: lastError,
|
|
cost: response.cost
|
|
});
|
|
continue;
|
|
}
|
|
}
|
|
return {
|
|
componentId: component.id,
|
|
componentName: component.name,
|
|
status: 'success',
|
|
migratedCode: response.code,
|
|
changes: response.changes,
|
|
warnings: response.warnings,
|
|
attempts,
|
|
totalCost
|
|
};
|
|
}
|
|
// Migration failed or low confidence
|
|
lastError = response.reason || 'Low confidence migration';
|
|
lastCode = response.code;
|
|
attemptHistory.push({
|
|
code: response.code,
|
|
error: lastError,
|
|
cost: response.cost
|
|
});
|
|
}
|
|
// All retries exhausted - ask user what to do
|
|
const decision = await onDecisionRequired({
|
|
componentId: component.id,
|
|
componentName: component.name,
|
|
attempts,
|
|
attemptHistory,
|
|
costSpent: totalCost,
|
|
retryCost: this.client.estimateCost(code.length)
|
|
});
|
|
switch (decision.action) {
|
|
case 'retry':
|
|
// Recursive retry with fresh attempts
|
|
return this.migrateComponent(component, code, preferences, onProgress, onDecisionRequired);
|
|
case 'skip':
|
|
return {
|
|
componentId: component.id,
|
|
componentName: component.name,
|
|
status: 'skipped',
|
|
changes: [],
|
|
warnings: ['Skipped by user after failed attempts'],
|
|
attempts,
|
|
totalCost
|
|
};
|
|
case 'getHelp': {
|
|
const help = await this.client.getHelp({
|
|
originalCode: code,
|
|
attempts,
|
|
attemptHistory
|
|
});
|
|
totalCost += 0.02; // Approximate cost for help request
|
|
return {
|
|
componentId: component.id,
|
|
componentName: component.name,
|
|
status: 'failed',
|
|
changes: [],
|
|
warnings: attemptHistory.map((a) => a.error),
|
|
attempts,
|
|
totalCost,
|
|
aiSuggestion: help
|
|
};
|
|
}
|
|
case 'manual':
|
|
return {
|
|
componentId: component.id,
|
|
componentName: component.name,
|
|
status: 'partial',
|
|
migratedCode: lastCode || undefined,
|
|
changes: [],
|
|
warnings: ['Marked for manual review'],
|
|
attempts,
|
|
totalCost
|
|
};
|
|
}
|
|
}
|
|
async verifyMigration(code,
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
component) {
|
|
// Basic syntax check using Babel
|
|
try {
|
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
const babel = __webpack_require__(46773);
|
|
babel.parse(code, {
|
|
sourceType: 'module',
|
|
plugins: ['jsx', 'typescript']
|
|
});
|
|
}
|
|
catch (syntaxError) {
|
|
const err = syntaxError;
|
|
return {
|
|
valid: false,
|
|
error: `Syntax error: ${err.message || 'Unknown syntax error'}`
|
|
};
|
|
}
|
|
// Check that no forbidden patterns remain
|
|
const forbiddenPatterns = [
|
|
{ regex: /componentWillMount\s*\(/, name: 'componentWillMount' },
|
|
{ regex: /componentWillReceiveProps\s*\(/, name: 'componentWillReceiveProps' },
|
|
{ regex: /componentWillUpdate\s*\(/, name: 'componentWillUpdate' },
|
|
{ regex: /ref\s*=\s*["'][^"']+["']/, name: 'string ref' },
|
|
{ regex: /contextTypes\s*=/, name: 'legacy contextTypes' }
|
|
];
|
|
for (const pattern of forbiddenPatterns) {
|
|
if (pattern.regex.test(code)) {
|
|
return {
|
|
valid: false,
|
|
error: `Code still contains ${pattern.name}`
|
|
};
|
|
}
|
|
}
|
|
return { valid: true };
|
|
}
|
|
abort() {
|
|
this.aborted = true;
|
|
}
|
|
getBudgetState() {
|
|
return this.budget.getState();
|
|
}
|
|
}
|
|
|
|
|
|
/***/ })
|
|
|
|
}]);
|
|
//# sourceMappingURL=605.bundle.js.map
|