// Enhanced SchemaBuilder class for Crawl4AI Chrome Extension
// Singleton instance to prevent multiple toolbars
let schemaBuilderInstance = null;
class SchemaBuilder {
constructor() {
// Prevent multiple instances
if (schemaBuilderInstance) {
schemaBuilderInstance.stop();
}
schemaBuilderInstance = this;
this.container = null;
this.fields = [];
this.overlay = null;
this.toolbar = null;
this.highlightBox = null; // For hover preview
this.selectedBox = null; // For selected element
this.currentElement = null; // Currently hovered element
this.selectedElement = null; // Currently selected element (container)
this.selectedElements = new Set();
this.inspectingFields = false; // Field inspection mode
this.codeModal = null;
this.previewMode = false;
this.previewElements = [];
this.schema = null;
this.parentLevels = 1; // Default parent levels for base container
this.handleMouseMove = this.handleMouseMove.bind(this);
this.handleClick = this.handleClick.bind(this);
this.handleKeyPress = this.handleKeyPress.bind(this);
this.handleMouseLeave = this.handleMouseLeave.bind(this);
}
start() {
this.createOverlay();
this.createToolbar();
this.attachEventListeners();
this.updateToolbar();
}
stop() {
this.detachEventListeners();
this.overlay?.remove();
this.toolbar?.remove();
this.highlightBox?.remove();
this.selectedBox?.remove();
this.removeAllHighlights();
this.clearPreview();
this.container = null;
this.fields = [];
this.selectedElements.clear();
this.schema = null;
this.currentElement = null;
this.selectedElement = null;
this.inspectingFields = false;
this.parentLevels = 1;
// Clear singleton reference
if (schemaBuilderInstance === this) {
schemaBuilderInstance = null;
}
}
// Alias for content script compatibility
deactivate() {
this.stop();
}
createOverlay() {
// Create highlight box for hover preview
this.highlightBox = document.createElement('div');
this.highlightBox.className = 'c4ai-highlight-box';
document.body.appendChild(this.highlightBox);
// Create selected box for permanent selection
this.selectedBox = document.createElement('div');
this.selectedBox.className = 'c4ai-selected-box';
this.selectedBox.style.display = 'none';
document.body.appendChild(this.selectedBox);
}
createToolbar() {
// Remove any existing toolbar first
const existingToolbar = document.querySelector('.c4ai-toolbar');
if (existingToolbar) {
existingToolbar.remove();
}
this.toolbar = document.createElement('div');
this.toolbar.className = 'c4ai-toolbar';
this.toolbar.innerHTML = `
๐๏ธ Preview Matches
๐งช Test Schema
โ๏ธ Deploy
๐ Schema
๐ Data
Matches Found:
0 items
Schema Valid:
Not tested
Click on a container element (e.g., product card, article, etc.)
`;
document.body.appendChild(this.toolbar);
// Force toolbar to top of z-index stack
this.toolbar.style.zIndex = '2147483647'; // Maximum z-index
// Add event listeners for toolbar buttons with error handling
const addClickHandler = (id, handler) => {
const element = document.getElementById(id);
if (element) {
element.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
handler();
});
}
};
// Add all event listeners
addClickHandler('c4ai-inspect-fields', () => this.toggleFieldInspection());
addClickHandler('c4ai-preview', () => this.togglePreview());
addClickHandler('c4ai-test', () => this.testSchema());
addClickHandler('c4ai-export-schema', () => this.exportSchema());
addClickHandler('c4ai-export-data', () => this.exportData());
addClickHandler('c4ai-deploy-cloud', () => this.deployToCloud());
addClickHandler('c4ai-close', () => this.stop());
// Navigation controls
addClickHandler('c4ai-nav-up', () => this.navigateUp());
addClickHandler('c4ai-nav-down', () => this.navigateDown());
addClickHandler('c4ai-nav-close', () => this.deselectContainer());
// Parent level controls
addClickHandler('c4ai-parent-minus', () => this.adjustParentLevels(-1));
addClickHandler('c4ai-parent-plus', () => this.adjustParentLevels(1));
// Make toolbar draggable
if (window.C4AI_Utils && window.C4AI_Utils.makeDraggable) {
window.C4AI_Utils.makeDraggable(this.toolbar);
}
}
attachEventListeners() {
document.addEventListener('mousemove', this.handleMouseMove, true);
document.addEventListener('click', this.handleClick, true);
document.addEventListener('keydown', this.handleKeyPress, true);
document.addEventListener('mouseleave', this.handleMouseLeave, true);
}
detachEventListeners() {
document.removeEventListener('mousemove', this.handleMouseMove, true);
document.removeEventListener('click', this.handleClick, true);
document.removeEventListener('keydown', this.handleKeyPress, true);
document.removeEventListener('mouseleave', this.handleMouseLeave, true);
}
handleMouseMove(e) {
const element = document.elementFromPoint(e.clientX, e.clientY);
// Don't highlight if hovering over our UI elements
if (this.isOurElement(element)) {
this.highlightBox.style.display = 'none';
return;
}
// Only show highlight if:
// 1. No container selected (selection mode)
// 2. Or inspecting fields inside container
if (!this.container || (this.inspectingFields && this.container)) {
if (element) {
// If inspecting fields, only highlight elements inside container
if (this.inspectingFields && !this.container.element.contains(element)) {
this.highlightBox.style.display = 'none';
return;
}
this.currentElement = element;
this.highlightElement(element);
}
} else {
// Container selected but not inspecting fields - no highlight
this.highlightBox.style.display = 'none';
}
}
handleMouseLeave(e) {
// Hide highlight when mouse leaves
if (e.target === document) {
this.highlightBox.style.display = 'none';
}
}
handleClick(e) {
const element = e.target;
// Check if clicking on our UI elements
if (this.isOurElement(element)) {
return; // Let toolbar clicks work normally
}
// Use current element
const targetElement = this.currentElement || element;
if (!this.container) {
// Container selection mode - prevent default
e.preventDefault();
e.stopPropagation();
this.selectContainer(targetElement);
} else if (this.inspectingFields && this.container.element.contains(targetElement)) {
// Field selection mode AND clicking inside container - prevent default
e.preventDefault();
e.stopPropagation();
this.selectField(targetElement);
}
// Otherwise, let the click work normally
}
handleKeyPress(e) {
if (e.key === 'Escape') {
this.stop();
}
}
isOurElement(element) {
return window.C4AI_Utils.isOurElement(element) ||
(this.selectedBox && element === this.selectedBox);
}
showSelectedBox(element) {
if (!element) return;
const rect = element.getBoundingClientRect();
this.selectedBox.style.cssText = `
position: absolute;
left: ${rect.left + window.scrollX}px;
top: ${rect.top + window.scrollY}px;
width: ${rect.width}px;
height: ${rect.height}px;
display: block;
`;
this.selectedBox.className = 'c4ai-selected-box c4ai-selected-container';
}
updateNavButtonStates() {
const upBtn = document.getElementById('c4ai-nav-up');
const downBtn = document.getElementById('c4ai-nav-down');
if (this.selectedElement) {
// Disable up button if no parent or parent is body
upBtn.disabled = !this.selectedElement.parentElement || this.selectedElement.parentElement === document.body;
// Disable down button if no children
downBtn.disabled = this.selectedElement.children.length === 0;
}
}
navigateUp() {
if (!this.selectedElement || !this.selectedElement.parentElement) return;
const parent = this.selectedElement.parentElement;
if (parent === document.body) return;
// Update selected element and container
this.selectedElement = parent;
this.container.element = parent;
this.container.tagName = parent.tagName.toLowerCase();
this.container.selector = this.generateContainerSelector(parent);
// Update visual selection
this.showSelectedBox(parent);
this.updateNavButtonStates();
this.updateToolbar();
this.updateStats();
}
navigateDown() {
if (!this.selectedElement || this.selectedElement.children.length === 0) return;
const firstChild = this.selectedElement.children[0];
// Update selected element and container
this.selectedElement = firstChild;
this.container.element = firstChild;
this.container.tagName = firstChild.tagName.toLowerCase();
this.container.selector = this.generateContainerSelector(firstChild);
// Update visual selection
this.showSelectedBox(firstChild);
this.updateNavButtonStates();
this.updateToolbar();
this.updateStats();
}
deselectContainer() {
if (this.container) {
// Remove visual selection
this.container.element.classList.remove('c4ai-selected-container');
this.selectedBox.style.display = 'none';
// Clear container and related state
this.container = null;
this.selectedElement = null;
this.inspectingFields = false;
// Clear all fields
this.fields.forEach(field => {
field.element.classList.remove('c4ai-selected-field');
field.element.removeAttribute('data-c4ai-field');
});
this.fields = [];
this.selectedElements.clear();
this.updateToolbar();
this.updateStats();
}
}
toggleFieldInspection() {
this.inspectingFields = !this.inspectingFields;
const fieldsBtn = document.getElementById('c4ai-inspect-fields');
if (this.inspectingFields) {
fieldsBtn.classList.add('c4ai-active');
fieldsBtn.innerHTML = '
Configure Field
Field Name:
Field Type:
Text Content
Attribute
Link (href)
Image (src)
List
Nested Object
Select Attribute:
${attributeOptions}
Preview Value:
${element.textContent.trim().substring(0, 100)}
Selector (auto-generated):
${this.generateSmartSelector(element, this.container.element)}
โ Save
โ Cancel
`;
document.body.appendChild(dialog);
const nameInput = dialog.querySelector('#c4ai-field-name');
const typeSelect = dialog.querySelector('#c4ai-field-type');
const attributeSelect = dialog.querySelector('#c4ai-field-attribute');
const attributeContainer = dialog.querySelector('#c4ai-attribute-select');
const previewValue = dialog.querySelector('#c4ai-preview-value');
const saveBtn = dialog.querySelector('#c4ai-field-save');
const cancelBtn = dialog.querySelector('#c4ai-field-cancel');
// Update preview based on type selection
const updatePreview = () => {
const type = typeSelect.value;
let value = '';
switch(type) {
case 'text':
value = element.textContent.trim();
attributeContainer.style.display = 'none';
break;
case 'attribute':
attributeContainer.style.display = 'block';
value = element.getAttribute(attributeSelect.value) || '';
break;
case 'link':
value = element.getAttribute('href') || element.querySelector('a')?.getAttribute('href') || '';
attributeContainer.style.display = 'none';
break;
case 'image':
value = element.getAttribute('src') || element.querySelector('img')?.getAttribute('src') || '';
attributeContainer.style.display = 'none';
break;
case 'list':
const listItems = element.querySelectorAll('li, option');
value = `[${listItems.length} items]`;
attributeContainer.style.display = 'none';
break;
case 'nested':
value = '[Complex nested structure]';
attributeContainer.style.display = 'none';
break;
}
previewValue.textContent = value.substring(0, 100) + (value.length > 100 ? '...' : '');
};
typeSelect.addEventListener('change', updatePreview);
attributeSelect.addEventListener('change', updatePreview);
const save = () => {
const fieldName = nameInput.value.trim();
if (fieldName) {
const type = typeSelect.value;
const selector = this.generateSmartSelector(element, this.container.element);
const field = {
name: fieldName,
type: type,
selector: selector,
element: element,
value: previewValue.textContent
};
// Add attribute if needed
if (type === 'attribute') {
field.attribute = attributeSelect.value;
} else if (type === 'link') {
field.type = 'attribute';
field.attribute = 'href';
} else if (type === 'image') {
field.type = 'attribute';
field.attribute = 'src';
}
this.fields.push(field);
element.classList.add('c4ai-selected-field');
element.setAttribute('data-c4ai-field', fieldName);
this.selectedElements.add(element);
this.updateToolbar();
this.updateStats();
this.generateSchema();
}
dialog.remove(); // Always close dialog
};
const cancel = () => {
dialog.remove();
};
saveBtn.addEventListener('click', save);
cancelBtn.addEventListener('click', cancel);
nameInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') save();
if (e.key === 'Escape') cancel();
});
nameInput.focus();
}
adjustParentLevels(delta) {
if (!this.container) return;
const newLevel = this.parentLevels + delta;
if (newLevel < 0 || newLevel > 5) return;
this.parentLevels = newLevel;
document.getElementById('c4ai-parent-value').textContent = newLevel;
// Update container selector with new parent levels
this.updateContainerSelector();
}
updateContainerSelector() {
if (!this.container || !this.selectedElement) return;
this.container.selector = this.generateContainerSelector(this.selectedElement);
this.container.element = this.selectedElement;
// Update the schema
this.generateSchema();
// Update display
const containerDisplay = document.getElementById('c4ai-container');
// containerDisplay.textContent = `${this.container.tagName} (${this.parentLevels} levels)`;
containerDisplay.textContent = `${this.container.tagName}`;
// Update selector display
const containerSelector = document.getElementById('c4ai-container-selector');
if (containerSelector) {
containerSelector.textContent = this.container.selector;
}
}
generateContainerSelector(element) {
// For container, include parent levels
let current = element;
const parts = [];
// Start from the target element
for (let i = 0; i <= this.parentLevels; i++) {
if (!current || current === document.body) break;
const selector = this.generateSingleElementSelector(current);
parts.unshift(selector);
if (i < this.parentLevels) {
current = current.parentElement;
}
}
// If we have parent levels, show them clearly
if (this.parentLevels > 0 && parts.length > 1) {
// Make it clear which part is the container
const containerPart = parts[parts.length - 1];
const parentParts = parts.slice(0, -1);
return parentParts.join(' > ') + ' > ' + containerPart;
}
return parts.join(' > ');
}
generateSingleElementSelector(element) {
// Generate selector for a single element
if (element.id) {
return `#${CSS.escape(element.id)}`;
}
// Check for data attributes (most stable)
const dataAttrs = ['data-testid', 'data-id', 'data-test', 'data-cy'];
for (const attr of dataAttrs) {
const value = element.getAttribute(attr);
if (value) {
return `[${attr}="${value}"]`;
}
}
// Check for aria-label
if (element.getAttribute('aria-label')) {
return `[aria-label="${element.getAttribute('aria-label')}"]`;
}
const tagName = element.tagName.toLowerCase();
// Check for simple, non-utility classes
const classes = Array.from(element.classList)
.filter(c => !c.startsWith('c4ai-')) // Exclude our classes
.filter(c => !c.includes('[') && !c.includes('(') && !c.includes(':')) // Exclude utility classes
.filter(c => c.length < 30); // Exclude very long classes
if (classes.length > 0 && classes.length <= 2) {
return tagName + classes.map(c => `.${CSS.escape(c)}`).join('');
}
return tagName;
}
generateSelector(element, context = document) {
// Try to generate a robust selector
if (element.id) {
return `#${CSS.escape(element.id)}`;
}
// Check for data attributes (most stable)
const dataAttrs = ['data-testid', 'data-id', 'data-test', 'data-cy'];
for (const attr of dataAttrs) {
const value = element.getAttribute(attr);
if (value) {
return `[${attr}="${value}"]`;
}
}
// Check for aria-label
if (element.getAttribute('aria-label')) {
return `[aria-label="${element.getAttribute('aria-label')}"]`;
}
// Try semantic HTML elements with text
const tagName = element.tagName.toLowerCase();
if (['button', 'a', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tagName)) {
const text = element.textContent.trim();
if (text && text.length < 50) {
// Use tag name with partial text match
return `${tagName}`;
}
}
// Check for simple, non-utility classes
const classes = Array.from(element.classList)
.filter(c => !c.startsWith('c4ai-')) // Exclude our classes
.filter(c => !c.includes('[') && !c.includes('(') && !c.includes(':')) // Exclude utility classes
.filter(c => c.length < 30); // Exclude very long classes
if (classes.length > 0 && classes.length <= 3) {
const selector = classes.map(c => `.${CSS.escape(c)}`).join('');
try {
if (context.querySelectorAll(selector).length === 1) {
return selector;
}
} catch (e) {
// Invalid selector, continue
}
}
// Use nth-child with simple parent tag
const parent = element.parentElement;
if (parent && parent !== context) {
const siblings = Array.from(parent.children);
const index = siblings.indexOf(element) + 1;
// Just use parent tag name to avoid recursion
const parentTag = parent.tagName.toLowerCase();
return `${parentTag} > ${tagName}:nth-child(${index})`;
}
// Final fallback
return tagName;
}
updateToolbar() {
// Update mode display
if (!this.container) {
document.getElementById('c4ai-mode').textContent = 'Select Container';
} else if (this.inspectingFields) {
document.getElementById('c4ai-mode').textContent = 'Select Fields';
} else {
document.getElementById('c4ai-mode').textContent = 'Container Selected';
}
// Show/hide container info and controls
const containerItem = document.getElementById('c4ai-container-item');
const parentLevelControls = document.getElementById('c4ai-parent-levels');
const footerSection = document.getElementById('c4ai-footer-section');
const selectorDisplay = document.getElementById('c4ai-selector-display');
const containerSelector = document.getElementById('c4ai-container-selector');
if (this.container) {
containerItem.style.display = 'flex';
parentLevelControls.style.display = 'flex';
footerSection.style.display = 'flex';
selectorDisplay.style.display = 'block';
// Update container display
document.getElementById('c4ai-container').textContent =
`${this.container.tagName} (${this.parentLevels} levels)`;
// Update selector display
containerSelector.textContent = this.container.selector;
} else {
containerItem.style.display = 'none';
parentLevelControls.style.display = 'none';
footerSection.style.display = 'none';
selectorDisplay.style.display = 'none';
}
// Show/hide sections based on state
const schemaSection = document.getElementById('c4ai-schema-section');
const actionsSection = document.getElementById('c4ai-actions-section');
const statsSection = document.getElementById('c4ai-stats-section');
if (this.fields.length > 0) {
schemaSection.style.display = 'block';
actionsSection.style.display = 'block';
statsSection.style.display = 'block';
// Update field count
document.getElementById('c4ai-field-count').textContent = this.fields.length;
// Update fields list with enhanced UI
const fieldsList = document.getElementById('c4ai-fields-list');
fieldsList.innerHTML = this.fields.map((field, index) => {
const icon = this.getFieldIcon(field.type);
return `
Edit Field
Field Name:
Field Type:
Text Content
Attribute
Link (href)
Image (src)
List
Nested Object
Select Attribute:
${attributeOptions}
Preview Value:
${field.value}
Selector (auto-generated):
${field.selector}
โ Update
โ Cancel
`;
document.body.appendChild(dialog);
const nameInput = dialog.querySelector('#c4ai-field-name');
const typeSelect = dialog.querySelector('#c4ai-field-type');
const attributeSelect = dialog.querySelector('#c4ai-field-attribute');
const attributeContainer = dialog.querySelector('#c4ai-attribute-select');
const previewValue = dialog.querySelector('#c4ai-preview-value');
const saveBtn = dialog.querySelector('#c4ai-field-save');
const cancelBtn = dialog.querySelector('#c4ai-field-cancel');
// Update preview based on type selection
const updatePreview = () => {
const type = typeSelect.value;
let value = '';
switch(type) {
case 'text':
value = field.element.textContent.trim();
attributeContainer.style.display = 'none';
break;
case 'attribute':
attributeContainer.style.display = 'block';
value = field.element.getAttribute(attributeSelect.value) || '';
break;
case 'link':
value = field.element.getAttribute('href') || field.element.querySelector('a')?.getAttribute('href') || '';
attributeContainer.style.display = 'none';
break;
case 'image':
value = field.element.getAttribute('src') || field.element.querySelector('img')?.getAttribute('src') || '';
attributeContainer.style.display = 'none';
break;
case 'list':
const listItems = field.element.querySelectorAll('li, option');
value = `[${listItems.length} items]`;
attributeContainer.style.display = 'none';
break;
case 'nested':
value = '[Complex nested structure]';
attributeContainer.style.display = 'none';
break;
}
previewValue.textContent = value.substring(0, 100) + (value.length > 100 ? '...' : '');
};
typeSelect.addEventListener('change', updatePreview);
attributeSelect.addEventListener('change', updatePreview);
const save = () => {
const fieldName = nameInput.value.trim();
if (fieldName) {
const type = typeSelect.value;
// Update field
field.name = fieldName;
field.type = type;
field.value = previewValue.textContent;
// Update attribute if needed
if (type === 'attribute') {
field.attribute = attributeSelect.value;
} else if (type === 'link') {
field.type = 'attribute';
field.attribute = 'href';
} else if (type === 'image') {
field.type = 'attribute';
field.attribute = 'src';
} else {
delete field.attribute;
}
// Update element attribute
field.element.setAttribute('data-c4ai-field', fieldName);
this.updateToolbar();
this.updateStats();
this.generateSchema();
}
dialog.remove();
};
const cancel = () => {
dialog.remove();
};
saveBtn.addEventListener('click', save);
cancelBtn.addEventListener('click', cancel);
nameInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') save();
if (e.key === 'Escape') cancel();
});
nameInput.focus();
nameInput.select();
}
updateStats() {
chrome.runtime.sendMessage({
action: 'updateStats',
stats: {
container: !!this.container,
fields: this.fields.length
}
});
}
removeAllHighlights() {
document.querySelectorAll('.c4ai-selected-container').forEach(el => {
el.classList.remove('c4ai-selected-container');
});
document.querySelectorAll('.c4ai-selected-field').forEach(el => {
el.classList.remove('c4ai-selected-field');
});
}
// New helper methods for enhanced functionality
getElementAttributes(element) {
const attributes = [];
for (const attr of element.attributes) {
attributes.push({
name: attr.name,
value: attr.value
});
}
return attributes;
}
generateSmartSelector(element, container) {
// Smart selector generation with 2-level parent context
const parts = [];
let current = element;
let depth = 0;
// Build path from element up to container (max 3 levels)
while (current && current !== container && depth < 3) {
let selector = current.tagName.toLowerCase();
// Add ID if available
if (current.id && !current.id.includes(':') && !current.id.includes('[')) {
selector = `#${CSS.escape(current.id)}`;
parts.unshift(selector);
break; // ID is unique enough
}
// Add classes (filter out dynamic/utility classes)
const classes = Array.from(current.classList)
.filter(c => !c.startsWith('c4ai-'))
.filter(c => !c.includes('[') && !c.includes('(') && !c.includes(':'))
.filter(c => c.length < 30)
.slice(0, 2); // Max 2 classes
if (classes.length > 0) {
selector += classes.map(c => `.${CSS.escape(c)}`).join('');
}
// Add data attributes for more specificity
const dataAttrs = ['data-testid', 'data-id', 'data-test'];
for (const attr of dataAttrs) {
if (current.hasAttribute(attr)) {
selector += `[${attr}="${CSS.escape(current.getAttribute(attr))}"]`;
break;
}
}
// Add nth-child if needed for disambiguation
if (current.parentElement && depth === 0) {
const siblings = Array.from(current.parentElement.children);
const sameTagSiblings = siblings.filter(s => s.tagName === current.tagName);
if (sameTagSiblings.length > 1) {
const index = sameTagSiblings.indexOf(current) + 1;
selector += `:nth-of-type(${index})`;
}
}
parts.unshift(selector);
current = current.parentElement;
depth++;
}
// Create relative selector from container
const fullSelector = parts.join(' > ');
// Test selector uniqueness within container
try {
const matches = container.querySelectorAll(fullSelector);
if (matches.length === 1 && matches[0] === element) {
return fullSelector;
}
} catch (e) {
// Invalid selector, continue with fallback
}
// Fallback to simple selector
return parts[parts.length - 1] || element.tagName.toLowerCase();
}
generateSchema() {
if (!this.container || this.fields.length === 0) {
return null;
}
// Build schema object
this.schema = {
name: `${window.location.hostname} Schema`,
baseSelector: this.container.selector,
fields: this.fields.map(field => {
const schemaField = {
name: field.name,
selector: field.selector,
type: field.type
};
if (field.attribute) {
schemaField.attribute = field.attribute;
}
return schemaField;
})
};
return this.schema;
}
togglePreview() {
this.previewMode = !this.previewMode;
const previewBtn = document.getElementById('c4ai-preview');
if (this.previewMode) {
previewBtn.innerHTML = '
${JSON.stringify(data, null, 2)}
`;
document.body.appendChild(modal);
// Event listeners
document.getElementById('c4ai-close-results').addEventListener('click', () => modal.remove());
document.getElementById('c4ai-download-data').addEventListener('click', () => {
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `extracted_data_${Date.now()}.json`;
a.click();
URL.revokeObjectURL(url);
});
document.getElementById('c4ai-copy-data').addEventListener('click', () => {
navigator.clipboard.writeText(JSON.stringify(data, null, 2)).then(() => {
const btn = document.getElementById('c4ai-copy-data');
btn.innerHTML = '
${window.C4AI_Utils.escapeHtml(code)}
`;
document.body.appendChild(this.codeModal);
// Add event listeners
document.getElementById('c4ai-close-modal').addEventListener('click', () => {
this.codeModal.remove();
this.codeModal = null;
// Don't stop the capture session
});
document.getElementById('c4ai-download-code').addEventListener('click', () => {
chrome.runtime.sendMessage({
action: 'downloadCode',
code: code,
filename: `crawl4ai_schema_${Date.now()}.py`
}, (response) => {
if (response && response.success) {
const btn = document.getElementById('c4ai-download-code');
const originalHTML = btn.innerHTML;
btn.innerHTML = '