Skip to content

Conversation

@NKoech123
Copy link
Contributor

@NKoech123 NKoech123 commented Dec 2, 2025

Symbol AI Improvements: Named Components and Top-Level Input Props

Summary

Improves Builder Symbol serialization to Mitosis JSX to make symbols more understandable for LLMs in Visual Editor AI. Symbols now serialize with meaningful component names and inputs as top-level props instead of nested in symbol.data.

Motivation

Currently, all Builder Symbols serialize to generic <Symbol> tags with inputs buried in nested symbol.data objects. This creates several challenges for LLMs:

  1. No distinguishability: <Symbol> tags all look identical, making it impossible for LLMs to differentiate between a header symbol and a button symbol
  2. Unusual prop structure: Inputs nested in symbol.data don't follow standard JSX patterns that LLMs are trained on
  3. Reduced clarity: LLMs struggle to understand what properties are available or how to modify them

Changes Made

Phase 1: Symbol Name Serialization

  • Added sanitizeSymbolName() helper function that converts symbol names to valid JSX component names (e.g., "Header Navigation" → "SymbolHeaderNavigation")
  • Updated the Symbol component mapper to use sanitized names instead of generic 'Symbol'
  • Updated extractSymbols() function to use actual symbol names when creating subComponents
  • Fixed mapper lookup to handle symbols renamed by extractSymbols() by checking if component name starts with "Symbol"

Phase 2: Inputs as Top-Level Props

  • Extracted inputs from symbol.data and created individual bindings for each input
  • Removed extracted inputs from the symbol binding to avoid duplication
  • Preserved empty data: {} objects to prevent data loss for symbols without inputs
  • Only extracts inputs when they exist (non-empty data object)

Files Changed

  • packages/core/src/parsers/builder/builder.ts - Core implementation
  • packages/core/src/__tests__/builder/builder.test.ts - Integration tests
  • packages/core/src/__tests__/data/builder/symbol-*.json - Test fixtures (4 new files)

Testing

Added comprehensive test coverage:

  • ✅ Symbol with basic metadata
  • ✅ Symbol with entry name
  • ✅ Symbol with inputs as top-level props
  • ✅ Multiple symbols with different names
  • ✅ Symbol roundtrip: Builder → Mitosis → Builder
  • ✅ No data loss for symbols without inputs (existing test)

All 10,236 tests passing.

Before & After

Before:

<Symbol symbol={{
  entry: "abc123",
  data: {
    buttonText: "Click me!",
    variant: "primary",
    disabled: false
  }
}} />
<Symbol symbol={{
  entry: "def456",
  data: {
    logoUrl: "/logo.png",
    showSearch: true
  }
}} />

After:

<SymbolButtonComponent 
  buttonText="Click me!"
  variant="primary"
  disabled={false}
  symbol={{
    entry: "abc123",
    model: "symbol",
    ownerId: "..."
  }}
/>
<SymbolHeaderNavigation 
  logoUrl="/logo.png"
  showSearch={true}
  symbol={{
    entry: "def456",
    model: "symbol",
    ownerId: "..."
  }}
/>

Impact on Visual Editor AI

This change significantly improves LLM understanding of symbols:

  1. Better targeting: LLMs can now distinguish between different symbols by name

    • "Change the header navigation logo" → targets <SymbolHeaderNavigation>
    • "Update the button text" → targets <SymbolButtonComponent>
  2. Natural JSX patterns: Top-level props follow standard JSX conventions that LLMs are trained on

    • LLMs can easily understand buttonText="Click me!" is a prop
    • Matches patterns from React, Vue, and other frameworks
  3. Improved editability: LLMs can more easily modify symbol inputs

    • Direct prop access vs nested object manipulation
    • Clear property names at the component level

Testing

Symbol Roundtrip Documentation

This document describes how Builder Symbols are serialized through the Mitosis JSX roundtrip process used by Editor AI.

Overview

The roundtrip flow is:

Builder JSON → Mitosis Component → Mitosis JSX → Mitosis Component → Builder JSON

This allows the AI to see and manipulate symbols as readable JSX, while preserving all metadata when converting back to Builder format.

Roundtrip Example

Step 1: Original Builder JSON (from MCP/API)

This is what Builder stores and what the visual editor expects:

{
  "@type": "@builder.io/sdk:Element",
  "id": "builder-abc123",
  "component": {
    "name": "Symbol",
    "options": {
      "symbol": {
        "entry": "2f27304b0ca04f578466218e27ae6d9b",
        "model": "symbol",
        "name": "Copyright Reserved",
        "data": {
          "buttonText": "Click me!",
          "year": 2025
        }
      }
    }
  },
  "responsiveStyles": {
    "large": {
      "display": "flex",
      "flexDirection": "column",
      "position": "relative",
      "flexShrink": "0",
      "boxSizing": "border-box"
    }
  }
}

Key properties:

  • component.name: Always "Symbol" - required by Builder visual editor
  • symbol.entry: Unique ID linking to the symbol content
  • symbol.name: Human-readable display name
  • symbol.data: Input values for this symbol instance

Step 2: Mitosis Component (internal representation)

After builderContentToMitosisComponent():

{
  name: "SymbolCopyrightReserved",  // Sanitized from "Copyright Reserved"
  bindings: {
    symbol: { 
      code: '{"entry":"2f27304b0ca04f578466218e27ae6d9b","model":"symbol","name":"Copyright Reserved"}',
      type: "single"
    },
    buttonText: { code: "'Click me!'", type: "single" },  // Extracted as top-level
    year: { code: "2025", type: "single" }                 // Extracted as top-level
  }
}

Transformations:

  • Component name becomes SymbolCopyrightReserved (sanitized, prefixed with "Symbol")
  • Inputs from symbol.data are extracted as top-level bindings
  • symbol.data is removed from the symbol binding (to avoid duplication)

Step 3: Mitosis JSX String (what AI sees)

After componentToMitosis():

<SymbolCopyrightReserved
  symbol={{
    entry: "2f27304b0ca04f578466218e27ae6d9b",
    model: "symbol",
    name: "Copyright Reserved",
  }}
  buttonText={"Click me!"}
  year={2025}
/>

Benefits for AI:

  • Named component (SymbolCopyrightReserved) is distinguishable from other symbols
  • Inputs are visible as standard JSX props
  • AI can add, remove, or move the entire element

Step 4: After parseJsx() (after AI edits)

After parseJsx():

{
  name: "SymbolCopyrightReserved",
  bindings: {
    symbol: { 
      code: '{ entry: "2f27304b...", model: "symbol", name: "Copyright Reserved" }',
      type: "single"
    }
  },
  properties: {
    buttonText: "Click me!",  // Simple strings become properties
    year: "2025"
  }
}

Note: Simple string values like buttonText become properties instead of bindings after JSX parsing. The generator handles both.


Step 5: Back to Builder JSON (for visual editor)

After componentToBuilder():

{
  "@type": "@builder.io/sdk:Element",
  "component": {
    "name": "Symbol",
    "options": {
      "symbol": {
        "entry": "2f27304b0ca04f578466218e27ae6d9b",
        "model": "symbol",
        "name": "Copyright Reserved",
        "data": {
          "buttonText": "Click me!",
          "year": "2025"
        }
      }
    }
  }
}

Transformations:

  • component.name is reset to "Symbol" (required by Builder)
  • symbol.name is preserved for future roundtrips
  • Inputs from both bindings and properties are merged back into symbol.data

Key Implementation Details

Name Sanitization

The sanitizeSymbolName() function converts display names to valid JSX component names:

"Copyright Reserved" → "SymbolCopyrightReserved"
"My Button"          → "SymbolMyButton"
"Footer"             → "SymbolFooter"

Rules:

  • Prefix with "Symbol" to avoid collisions with other components
  • Remove non-alphanumeric characters
  • Capitalize first letter

Input Extraction

Inputs are extracted from symbol.data as top-level props for AI readability:

// Before (hard for AI to understand)
<Symbol symbol={{ data: { buttonText: "Click" }, entry: "..." }} />

// After (standard JSX pattern)
<SymbolCopyrightReserved symbol={{ entry: "..." }} buttonText="Click" />

Input Merging (on way back)

The generator merges inputs from both sources:

  1. Bindings - Complex values like objects/arrays
  2. Properties - Simple strings (after JSX parse converts them)
// From bindings
for (const key of Object.keys(json.bindings)) {
  if (key !== 'symbol' && key !== 'css' && key !== 'style') {
    inputData[key] = json5.parse(json.bindings[key].code);
  }
}

// From properties (simple strings after JSX roundtrip)
for (const key of Object.keys(json.properties)) {
  if (!key.startsWith('$') && !key.startsWith('_') && !key.startsWith('data-')) {
    inputData[key] = json.properties[key];
  }
}

AI Interaction Rules

The AI is instructed:

  1. DO NOT modify the symbol prop (entry, model, name)
  2. DO NOT modify input props (buttonText, year, etc.)
  3. CAN add new symbol instances (copy from MCP)
  4. CAN remove symbol instances (delete element)
  5. CAN move symbol instances (change position)

Files Involved

File Purpose
parsers/builder/builder.ts Builder JSON → Mitosis Component
generators/mitosis/index.ts Mitosis Component → JSX String
parsers/jsx/index.ts JSX String → Mitosis Component
generators/builder/generator.ts Mitosis Component → Builder JSON
ai-services/.../parse-content-value.ts Post-processing (adds default styles)

@NKoech123 NKoech123 self-assigned this Dec 2, 2025
@changeset-bot
Copy link

changeset-bot bot commented Dec 2, 2025

⚠️ No Changeset found

Latest commit: 5c65b84

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@nx-cloud
Copy link

nx-cloud bot commented Dec 2, 2025

View your CI Pipeline Execution ↗ for commit 5c65b84

Command Status Duration Result
nx run-many --target test ✅ Succeeded 5m 12s View ↗
nx e2e @builder.io/e2e-app ✅ Succeeded 1m 17s View ↗
nx run-many --target build --exclude @builder.i... ✅ Succeeded 3m 44s View ↗
nx build @builder.io/mitosis-site ✅ Succeeded 2m 36s View ↗

☁️ Nx Cloud last updated this comment at 2025-12-09 16:30:26 UTC

Copy link
Contributor

@liamdebeasi liamdebeasi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm curious to hear how your testing in Editor AI went today!

* - Adds "Symbol" prefix to avoid collisions
* - Returns "Symbol" if no valid name can be generated
*/
const sanitizeSymbolName = (name: string | undefined): string => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this may not actually be true, but I thought symbols always had names so this param would never be undefined?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if there are any edge cases so I went with defense-mode. All symbols, in theory, should have names

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sounds good. since symbols should always have a name we can probably remove any undefined handling

const hasInputs = Object.keys(symbolData).length > 0;

// Only extract inputs if there are any to avoid data loss
if (hasInputs) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you explain why we only extract inputs?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we extract inputs from symbolData (which comes from symbol.data) to make them visible as top-level JSX props.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

might be good to document this in the code

@cloudflare-workers-and-pages
Copy link

cloudflare-workers-and-pages bot commented Dec 3, 2025

Deploying mitosis with  Cloudflare Pages  Cloudflare Pages

Latest commit: 5c65b84
Status: ✅  Deploy successful!
Preview URL: https://9d8136c7.mitosis-9uh.pages.dev
Branch Preview URL: https://nkoech-symbols-investigation.mitosis-9uh.pages.dev

View logs

@NKoech123 NKoech123 marked this pull request as ready for review December 9, 2025 07:37
@NKoech123 NKoech123 requested a review from samijaber as a code owner December 9, 2025 07:37
…ol.options.data and contains key-value pairs for props passed to the symbol instance in Builder.io
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants