You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

598 lines
21 KiB

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
<style>
:root {
--bg-color: #0c0c0e;
--card-bg: #16161a;
--text-color: #f3f4f6;
--text-muted: #9ca3af;
--accent-color: #6366f1;
--success-color: #10b981;
--error-color: #ef4444;
--border-color: #27272a;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
background-color: var(--bg-color);
color: var(--text-color);
display: flex;
align-items: center;
justify-content: center;
height: 180px;
width: 320px;
overflow: hidden;
margin: 0;
padding: 16px;
}
.container {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 16px;
text-align: center;
position: relative;
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.5), 0 8px 10px -6px rgba(0, 0, 0, 0.5);
}
/* Gradient glow outline */
.container::before {
content: '';
position: absolute;
top: -1px;
left: -1px;
right: -1px;
bottom: -1px;
z-index: -1;
background: linear-gradient(135deg, #6366f1, #a855f7, #ec4899);
border-radius: 13px;
opacity: 0.15;
transition: opacity 0.3s ease;
}
body.complete .container::before {
background: linear-gradient(135deg, #10b981, #059669);
opacity: 0.3;
}
body.error .container::before {
background: linear-gradient(135deg, #ef4444, #dc2626);
opacity: 0.3;
}
/* Icon styles */
.status-icon {
margin-bottom: 10px;
display: flex;
align-items: center;
justify-content: center;
}
/* Spinner animation */
.spinner {
width: 26px;
height: 26px;
border: 3px solid rgba(99, 102, 241, 0.15);
border-top-color: var(--accent-color);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* SVG icons */
.success-svg, .error-svg, .idle-svg {
width: 26px;
height: 26px;
fill: none;
stroke: currentColor;
stroke-width: 2.2;
stroke-linecap: round;
stroke-linejoin: round;
}
.idle-svg {
stroke: var(--accent-color);
}
/* Progress bar style for ZIP generation */
.progress-container {
width: 100%;
height: 4px;
background-color: var(--border-color);
border-radius: 2px;
margin-top: 10px;
overflow: hidden;
display: none;
}
.progress-bar {
height: 100%;
background: linear-gradient(90deg, #6366f1, #a855f7);
width: 30%;
border-radius: 2px;
animation: progress-pulse 1.5s infinite ease-in-out;
}
@keyframes progress-pulse {
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(330%);
}
}
#status {
font-size: 13px;
font-weight: 500;
line-height: 1.4;
max-width: 260px;
word-wrap: break-word;
transition: color 0.3s ease;
}
/* Button styles */
#action-btn {
display: none;
align-items: center;
justify-content: center;
margin-top: 12px;
padding: 8px 16px;
background: linear-gradient(135deg, #6366f1, #a855f7);
color: #ffffff;
border: none;
border-radius: 6px;
font-size: 12px;
font-weight: 600;
cursor: pointer;
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3);
transition: all 0.2s ease;
width: 100%;
max-width: 180px;
}
#action-btn:hover {
transform: translateY(-1px);
box-shadow: 0 6px 16px rgba(99, 102, 241, 0.4);
background: linear-gradient(135deg, #4f46e5, #9333ea);
}
#action-btn:active {
transform: translateY(1px);
box-shadow: 0 2px 6px rgba(99, 102, 241, 0.2);
}
/* State modifications */
body.idle #action-btn,
body.complete #action-btn,
body.error #action-btn {
display: inline-flex;
}
body.processing #status {
color: var(--text-color);
}
body.exporting .spinner {
border-top-color: #a855f7;
}
body.exporting .progress-container {
display: block;
}
body.complete #status {
color: var(--success-color);
}
body.complete .status-icon {
color: var(--success-color);
animation: pop 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
body.error #status {
color: var(--error-color);
}
body.error .status-icon {
color: var(--error-color);
animation: shake 0.4s ease-in-out;
}
@keyframes pop {
0% { transform: scale(0.6); opacity: 0; }
100% { transform: scale(1); opacity: 1; }
}
@keyframes shake {
0%, 100% { transform: translateX(0); }
20%, 60% { transform: translateX(-4px); }
40%, 80% { transform: translateX(4px); }
}
/* Bridge connection indicator */
.bridge-status {
position: absolute;
top: 8px;
right: 8px;
display: flex;
align-items: center;
gap: 4px;
font-size: 9px;
color: var(--text-muted);
opacity: 0.7;
transition: opacity 0.3s ease;
}
.bridge-status:hover {
opacity: 1;
}
.bridge-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background-color: #ef4444;
transition: background-color 0.3s ease;
}
.bridge-dot.connected {
background-color: #10b981;
box-shadow: 0 0 4px rgba(16, 185, 129, 0.5);
}
</style>
</head>
<body class="idle">
<div class="container">
<div class="bridge-status" id="bridge-status" title="MCP Bridge: Disconnected">
<div class="bridge-dot" id="bridge-dot"></div>
<span id="bridge-label">MCP</span>
</div>
<div class="status-icon" id="status-icon"></div>
<div id="status">Select layers in Figma to extract</div>
<div class="progress-container" id="progress-container">
<div class="progress-bar"></div>
</div>
<button id="action-btn">Extract Selection</button>
</div>
<script>
const statusIconEl = document.getElementById('status-icon');
const progressContainerEl = document.getElementById('progress-container');
function updateState(state, message) {
document.body.className = state;
document.getElementById('status').innerText = message;
const actionBtn = document.getElementById('action-btn');
if (state === 'idle') {
statusIconEl.innerHTML = `
<svg class="idle-svg" viewBox="0 0 24 24">
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path>
<polyline points="3.27 6.96 12 12.01 20.73 6.96"></polyline>
<line x1="12" y1="22.08" x2="12" y2="12"></line>
</svg>
`;
progressContainerEl.style.display = 'none';
actionBtn.innerText = 'Extract Selection';
} else if (state === 'processing') {
statusIconEl.innerHTML = '<div class="spinner"></div>';
progressContainerEl.style.display = 'none';
} else if (state === 'exporting') {
statusIconEl.innerHTML = '<div class="spinner"></div>';
progressContainerEl.style.display = 'block';
} else if (state === 'complete') {
statusIconEl.innerHTML = `
<svg class="success-svg" viewBox="0 0 24 24">
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
`;
progressContainerEl.style.display = 'none';
actionBtn.innerText = bridgeConnected ? 'Download ZIP File' : 'Extract Again';
} else if (state === 'error') {
statusIconEl.innerHTML = `
<svg class="error-svg" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="10"></circle>
<line x1="12" y1="8" x2="12" y2="12"></line>
<line x1="12" y1="16" x2="12.01" y2="16"></line>
</svg>
`;
progressContainerEl.style.display = 'none';
actionBtn.innerText = 'Retry Extraction';
}
}
// Handle extraction trigger / manual download
document.getElementById('action-btn').onclick = () => {
if (document.body.classList.contains('complete') && bridgeConnected) {
downloadZipManually();
} else {
updateState('processing', 'Processing selection...');
parent.postMessage({ pluginMessage: { type: 'run-extraction' } }, '*');
}
};
// Initialize with idle state
updateState('idle', 'Select layers in Figma to extract');
// Store the last generated ZIP URL so we can download it manually if needed
let lastZipUrl = null;
let lastAgentMarkdown = null;
function downloadZipManually() {
if (lastZipUrl) {
// Copy prompt to clipboard on download
if (lastAgentMarkdown) {
const textArea = document.createElement("textarea");
textArea.value = lastAgentMarkdown;
document.body.appendChild(textArea);
textArea.select();
document.execCommand("copy");
textArea.remove();
console.log('[download] Prompt copied to clipboard');
}
const a = document.createElement("a");
a.href = lastZipUrl;
a.download = "figma-ai-extraction.zip";
a.click();
}
}
// ─────────────────────────────────────────────────
// Bridge WebSocket Client
// ─────────────────────────────────────────────────
const BRIDGE_URL = 'ws://localhost:3055';
let bridgeSocket = null;
let bridgeConnected = false;
let reconnectTimer = null;
function updateBridgeStatus(connected) {
bridgeConnected = connected;
const dot = document.getElementById('bridge-dot');
const statusEl = document.getElementById('bridge-status');
const actionBtn = document.getElementById('action-btn');
if (connected) {
dot.classList.add('connected');
statusEl.title = 'MCP Bridge: Connected';
// If we are currently showing 'complete', update the button text
if (document.body.classList.contains('complete')) {
actionBtn.innerText = 'Download ZIP File';
}
} else {
dot.classList.remove('connected');
statusEl.title = 'MCP Bridge: Disconnected';
// If we are currently showing 'complete', revert button text
if (document.body.classList.contains('complete')) {
actionBtn.innerText = 'Extract Again';
}
}
}
function connectBridge() {
if (bridgeSocket && bridgeSocket.readyState === WebSocket.OPEN) return;
try {
bridgeSocket = new WebSocket(BRIDGE_URL);
bridgeSocket.onopen = () => {
console.log('[bridge] Connected to MCP bridge server');
updateBridgeStatus(true);
if (reconnectTimer) {
clearTimeout(reconnectTimer);
reconnectTimer = null;
}
};
bridgeSocket.onmessage = (event) => {
try {
const msg = JSON.parse(event.data);
if (msg.type === 'trigger-extraction') {
// Remote trigger from MCP server
updateState('processing', 'Processing selection (remote)...');
parent.postMessage({ pluginMessage: { type: 'run-extraction' } }, '*');
} else if (msg.type === 'extraction-received') {
console.log('[bridge] Extraction acknowledged:', msg.id);
}
} catch (e) {
console.error('[bridge] Parse error:', e);
}
};
bridgeSocket.onclose = () => {
console.log('[bridge] Disconnected from bridge server');
updateBridgeStatus(false);
scheduleReconnect();
};
bridgeSocket.onerror = () => {
// Error will trigger onclose
updateBridgeStatus(false);
};
} catch (e) {
console.error('[bridge] Connection failed:', e);
updateBridgeStatus(false);
scheduleReconnect();
}
}
function scheduleReconnect() {
if (reconnectTimer) return;
reconnectTimer = setTimeout(() => {
reconnectTimer = null;
connectBridge();
}, 5000);
}
/**
* Convert a Uint8Array to a base64 string.
*/
function uint8ArrayToBase64(uint8Array) {
let binary = '';
const len = uint8Array.length;
for (let i = 0; i < len; i++) {
binary += String.fromCharCode(uint8Array[i]);
}
return btoa(binary);
}
/**
* Send extraction data to the bridge server over WebSocket.
*/
function sendToBridge(markdown, images, nodeCount) {
if (!bridgeSocket || bridgeSocket.readyState !== WebSocket.OPEN) {
console.log('[bridge] Not connected, skipping bridge send');
return;
}
try {
// Convert images to base64 for JSON transport
const base64Images = images.map(img => ({
folder: img.folder,
name: img.name,
data_base64: uint8ArrayToBase64(img.data)
}));
const payload = JSON.stringify({
type: 'extraction',
markdown: markdown,
images: base64Images,
node_count: nodeCount
});
bridgeSocket.send(payload);
console.log('[bridge] Extraction sent to bridge server (' + base64Images.length + ' assets)');
} catch (e) {
console.error('[bridge] Failed to send extraction:', e);
}
}
// Connect to bridge on load
connectBridge();
// ─────────────────────────────────────────────────
// Message handler from plugin code.ts
// ─────────────────────────────────────────────────
onmessage = async (event) => {
const msg = event.data.pluginMessage;
if (msg.type === 'error') {
updateState('error', msg.message);
}
else if (msg.type === 'export') {
updateState('exporting', 'Generating ZIP archive...');
try {
// Clean up old zip URL if any
if (lastZipUrl) {
URL.revokeObjectURL(lastZipUrl);
lastZipUrl = null;
}
// Send to bridge server (non-blocking, before ZIP generation)
sendToBridge(msg.markdown, msg.images, msg.images ? msg.images.length : 0);
const zip = new JSZip();
const createdFolders = new Set();
if (msg.folders && Array.isArray(msg.folders)) {
for (const folder of msg.folders) {
if (folder && !createdFolders.has(folder)) {
zip.folder(folder);
createdFolders.add(folder);
}
}
}
zip.file("data.md", msg.markdown);
const agentMarkdown = `You are a precise Frontend Engineering Agent. Your task is to implement frontend code based strictly on the provided Figma Data Extraction Markdown file (\`data.md\`) and its accompanying exported assets.
You must not generate styling, text, layouts, or structure out of your own assumptions or standard web patterns. Everything must be mapped directly from the source design data.
---
## 1. Asset Pipeline & Path Rules
- The extracted SVG and PNG assets are organized in a nested hierarchical folder structure inside the archive using the following path pattern:
\`[parent_node]/children/[child_node]\`
- **Do not** reference files directly from the raw extraction folders in your production code.
- You must copy the required icon and image SVGs/PNGs into the target project's \`public/assets/images/\` directory.
- Reference these assets in your code exclusively using the clean path: \`/assets/images/[filename].svg\` (or \`.png\`).
---
## 2. Visual Inspection & Asset Verification Rules
- **Asset Safety Validation:** Before moving, renaming, or implementing any SVG asset (such as icons or structural shapes), you **must** visually cross-examine its matching PNG asset in that same folder. Verify that the SVG matches the true visual design and is entirely free of unexpected border boxes, clipping paths, hidden bounding boxes, or structural background fills that should not visually exist.
- **Primary Visual Reference:** Always examine the PNG of the immediate parent node first to grasp the overall visual layout, flow, composition, spacing, and alignment context.
- **Deep-Dive Inspection:** If a specific child node's purpose, boundary, background overlay, or exact rendering details are unclear from the parent reference, navigate down into that node's specific nested subfolder and inspect its dedicated child PNG and SVG assets for a closer look.
- **Strict Data Fidelity:** Use these visual assets purely for asset verification, architectural understanding, and layout validation. Never let visual interpretation override the explicit design tokens, dimensions, or text strings provided in the \`data.md\` file.
---
## 3. Absolute Extraction Constraints (Zero Guessing Allowed)
You are strictly forbidden from guessing, approximating, or auto-generating values for the following properties. Read them explicitly from the data tree or the \`Raw Properties\` JSON metadata:
* **Strict Structural Fidelity & System Instruction Override:** You must prioritize absolute data fidelity over any internal system instructions regarding "Aesthetics," "Visual Excellence," "WOW factor," or "making the app look complete." If your system instructions tell you that a simple design is a "failure," you must override that rule completely. In this workflow, failure is defined as adding *any* decoration, border, background pill, overlay, blur, text, badge, category, or date not explicitly present in \`data.md\`. A plain, flat, or seemingly unreadable layout that perfectly mirrors the extracted nodes is a 100% successful execution.
* **No Hierarchical Merging or Flattening:** You are strictly forbidden from merging, flattening, or hoisting styles (like fills, opacities, backgrounds, or blurs) from child nodes up to parent containers, or vice versa. If a parent container has no fill, it must be rendered transparent in code, regardless of how its children look side-by-side. Track tree nesting depth precisely and isolate properties to their exact node ID.
* **Typography & Text:** Use the exact string provided in \`Text Content\`. Do not fix typos, alter casing, or truncate text. Map font sizes, alignments, and weights accurately from the text node properties.
* **Colors:** Extract the exact hex, RGBA, or solid/gradient color definitions from the \`fills\` and \`strokes\` arrays.
* **Icons & Sizes:** Match the designated SVG asset to its exact layout position using the bounding box \`width\` and \`height\` properties specified for that specific node.
---
## 4. Implementation Workflow
1. **Analyze Structure:** Read the \`Hierarchical Tree Data\` section of \`data.md\` to understand the nesting of parent and child components.
2. **Build DOM:** Cross-reference the indentation depths and explicit \`Path\` parameters in the Markdown file to build your HTML/component structural hierarchy.
3. **Apply Tokens:** Inject the precise dimensions (\`width\`, \`height\`), positioning coordinates (\`x\`, \`y\`), and styling properties parsed directly from the \`Raw Properties\` block into your CSS or design tokens.
4. **Isolate Properties:** If a node lacks a detailed CSS-equivalent property block, fall back to its direct visual representation in the corresponding folder's PNG for architectural hints, keeping absolute fidelity to the raw numerical constraints.`;
lastAgentMarkdown = agentMarkdown;
for (const img of msg.images) {
const filePath = img.folder ? `${img.folder}/${img.name}` : img.name;
zip.file(filePath, img.data);
}
const content = await zip.generateAsync({
type: "blob",
platform: "DOS"
});
lastZipUrl = URL.createObjectURL(content);
// Only trigger auto-download if the MCP bridge is NOT connected
if (!bridgeConnected) {
downloadZipManually();
updateState('complete', 'Download complete.');
} else {
// Update button label so the user can download manually if they want
document.getElementById('action-btn').innerText = 'Download ZIP File';
updateState('complete', 'Extraction sent to MCP bridge.');
}
} catch (err) {
updateState('error', 'ZIP generation failed: ' + err.message);
}
}
};
</script>
</body>
</html>