@fridaplatform-stk/figma2frida-mcp 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +25 -0
- package/README.md +216 -0
- package/dist/figma2frida.d.ts +1 -0
- package/dist/figma2frida.js +2840 -0
- package/dist/figma2frida.js.map +1 -0
- package/package.json +87 -0
|
@@ -0,0 +1,2840 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/figma2frida/figma2frida.ts
|
|
4
|
+
import "dotenv/config";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import yargs from "yargs";
|
|
7
|
+
import { hideBin } from "yargs/helpers";
|
|
8
|
+
import { FastMCP } from "fastmcp";
|
|
9
|
+
|
|
10
|
+
// src/figma2frida/clients/figma-client.ts
|
|
11
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
12
|
+
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
|
13
|
+
var FigmaClient = class {
|
|
14
|
+
client = null;
|
|
15
|
+
connected = false;
|
|
16
|
+
figmaUrl;
|
|
17
|
+
constructor(figmaUrl = "http://127.0.0.1:3845/sse") {
|
|
18
|
+
this.figmaUrl = figmaUrl;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Connect to Figma MCP server
|
|
22
|
+
*/
|
|
23
|
+
async connect(retries = 3) {
|
|
24
|
+
if (this.connected && this.client) {
|
|
25
|
+
console.log("[FIGMA CLIENT] Already connected, skipping connection attempt");
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
console.log(`[FIGMA CLIENT] ========================================`);
|
|
29
|
+
console.log(`[FIGMA CLIENT] \u{1F504} Starting connection attempt`);
|
|
30
|
+
console.log(`[FIGMA CLIENT] URL: ${this.figmaUrl}`);
|
|
31
|
+
console.log(`[FIGMA CLIENT] Max retries: ${retries}`);
|
|
32
|
+
console.log(`[FIGMA CLIENT] Current status: connected=${this.connected}, client=${!!this.client}`);
|
|
33
|
+
console.log(`[FIGMA CLIENT] ========================================`);
|
|
34
|
+
for (let attempt = 0; attempt < retries; attempt++) {
|
|
35
|
+
const attemptStartTime = Date.now();
|
|
36
|
+
console.log(`[FIGMA CLIENT] \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
|
|
37
|
+
console.log(`[FIGMA CLIENT] Attempt ${attempt + 1}/${retries} starting...`);
|
|
38
|
+
try {
|
|
39
|
+
console.log(`[FIGMA CLIENT] Creating MCP Client object...`);
|
|
40
|
+
this.client = new Client(
|
|
41
|
+
{
|
|
42
|
+
name: "figma-proxy-client",
|
|
43
|
+
version: "1.0.0"
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
capabilities: {
|
|
47
|
+
tools: {}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
);
|
|
51
|
+
console.log(`[FIGMA CLIENT] \u2713 Client object created`);
|
|
52
|
+
console.log(`[FIGMA CLIENT] Creating SSE Transport for: ${this.figmaUrl}`);
|
|
53
|
+
const transport = new SSEClientTransport(new URL(this.figmaUrl));
|
|
54
|
+
console.log(`[FIGMA CLIENT] \u2713 Transport object created`);
|
|
55
|
+
console.log(`[FIGMA CLIENT] Attempting to connect...`);
|
|
56
|
+
const connectStartTime = Date.now();
|
|
57
|
+
await this.client.connect(transport);
|
|
58
|
+
const connectTime = Date.now() - connectStartTime;
|
|
59
|
+
console.log(`[FIGMA CLIENT] \u2713 Connection established in ${connectTime}ms`);
|
|
60
|
+
this.connected = true;
|
|
61
|
+
const totalTime = Date.now() - attemptStartTime;
|
|
62
|
+
console.log(`[FIGMA CLIENT] ========================================`);
|
|
63
|
+
console.log(`[FIGMA CLIENT] \u2705 CONNECTION SUCCESS!`);
|
|
64
|
+
console.log(`[FIGMA CLIENT] URL: ${this.figmaUrl}`);
|
|
65
|
+
console.log(`[FIGMA CLIENT] Attempt: ${attempt + 1}/${retries}`);
|
|
66
|
+
console.log(`[FIGMA CLIENT] Total time: ${totalTime}ms`);
|
|
67
|
+
console.log(`[FIGMA CLIENT] ========================================`);
|
|
68
|
+
return;
|
|
69
|
+
} catch (error) {
|
|
70
|
+
const attemptTime = Date.now() - attemptStartTime;
|
|
71
|
+
console.log(`[FIGMA CLIENT] \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
|
|
72
|
+
console.log(`[FIGMA CLIENT] \u274C Connection attempt ${attempt + 1}/${retries} FAILED`);
|
|
73
|
+
console.log(`[FIGMA CLIENT] Time elapsed: ${attemptTime}ms`);
|
|
74
|
+
console.log(`[FIGMA CLIENT] Error type: ${error?.constructor?.name || typeof error}`);
|
|
75
|
+
console.log(`[FIGMA CLIENT] Error message: ${error instanceof Error ? error.message : String(error)}`);
|
|
76
|
+
if (error && typeof error === "object") {
|
|
77
|
+
const errorObj = error;
|
|
78
|
+
if ("code" in errorObj) {
|
|
79
|
+
console.log(`[FIGMA CLIENT] Error code: ${errorObj.code}`);
|
|
80
|
+
}
|
|
81
|
+
if ("cause" in errorObj) {
|
|
82
|
+
console.log(`[FIGMA CLIENT] Error cause:`, errorObj.cause);
|
|
83
|
+
}
|
|
84
|
+
if ("status" in errorObj) {
|
|
85
|
+
console.log(`[FIGMA CLIENT] HTTP status: ${errorObj.status}`);
|
|
86
|
+
}
|
|
87
|
+
if ("statusText" in errorObj) {
|
|
88
|
+
console.log(`[FIGMA CLIENT] HTTP status text: ${errorObj.statusText}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (error instanceof Error && error.stack) {
|
|
92
|
+
console.log(`[FIGMA CLIENT] Error stack:`);
|
|
93
|
+
console.log(error.stack);
|
|
94
|
+
}
|
|
95
|
+
if (attempt === retries - 1) {
|
|
96
|
+
console.log(`[FIGMA CLIENT] ========================================`);
|
|
97
|
+
console.log(`[FIGMA CLIENT] \u274C ALL CONNECTION ATTEMPTS FAILED`);
|
|
98
|
+
console.log(`[FIGMA CLIENT] Total attempts: ${retries}`);
|
|
99
|
+
console.log(`[FIGMA CLIENT] URL: ${this.figmaUrl}`);
|
|
100
|
+
console.log(`[FIGMA CLIENT] ========================================`);
|
|
101
|
+
throw new Error(
|
|
102
|
+
`Failed to connect to Figma MCP at ${this.figmaUrl} after ${retries} attempts. Make sure Figma Desktop is running with MCP enabled.`
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
const waitTime = 1e3 * (attempt + 1);
|
|
106
|
+
console.log(`[FIGMA CLIENT] Waiting ${waitTime}ms before retry...`);
|
|
107
|
+
await new Promise(
|
|
108
|
+
(resolve) => setTimeout(resolve, waitTime)
|
|
109
|
+
);
|
|
110
|
+
console.log(`[FIGMA CLIENT] Wait complete, retrying...`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Ensure connection is established
|
|
116
|
+
*/
|
|
117
|
+
async ensureConnected() {
|
|
118
|
+
if (!this.connected || !this.client) {
|
|
119
|
+
console.log("[FIGMA CLIENT] ensureConnected() called - not connected, attempting connection...");
|
|
120
|
+
await this.connect();
|
|
121
|
+
} else {
|
|
122
|
+
console.log("[FIGMA CLIENT] ensureConnected() called - already connected, skipping");
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Call Figma's get_design_context tool
|
|
127
|
+
*/
|
|
128
|
+
async getDesignContext(nodeId, options) {
|
|
129
|
+
await this.ensureConnected();
|
|
130
|
+
const args = {
|
|
131
|
+
...nodeId ? { nodeId } : {},
|
|
132
|
+
clientLanguages: options?.clientLanguages || "typescript",
|
|
133
|
+
clientFrameworks: options?.clientFrameworks || "react",
|
|
134
|
+
forceCode: options?.forceCode ?? true
|
|
135
|
+
};
|
|
136
|
+
console.log(`[FIGMA CLIENT] Calling get_design_context with args:`, args);
|
|
137
|
+
try {
|
|
138
|
+
const result = await this.client.callTool({
|
|
139
|
+
name: "get_design_context",
|
|
140
|
+
arguments: args
|
|
141
|
+
});
|
|
142
|
+
console.log(`[FIGMA CLIENT] get_design_context response type:`, typeof result);
|
|
143
|
+
console.log(
|
|
144
|
+
`[FIGMA CLIENT] get_design_context response keys:`,
|
|
145
|
+
Object.keys(result)
|
|
146
|
+
);
|
|
147
|
+
return result;
|
|
148
|
+
} catch (error) {
|
|
149
|
+
console.warn("Error calling get_design_context, attempting reconnect...");
|
|
150
|
+
this.connected = false;
|
|
151
|
+
await this.connect();
|
|
152
|
+
const result = await this.client.callTool({
|
|
153
|
+
name: "get_design_context",
|
|
154
|
+
arguments: {
|
|
155
|
+
...nodeId ? { nodeId } : {},
|
|
156
|
+
clientLanguages: options?.clientLanguages || "typescript",
|
|
157
|
+
clientFrameworks: options?.clientFrameworks || "react",
|
|
158
|
+
forceCode: options?.forceCode ?? true
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
return result;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Call Figma's get_metadata tool to get actual node structure
|
|
166
|
+
*/
|
|
167
|
+
async getMetadata(nodeId, options) {
|
|
168
|
+
await this.ensureConnected();
|
|
169
|
+
const args = {
|
|
170
|
+
...nodeId ? { nodeId } : {},
|
|
171
|
+
clientLanguages: options?.clientLanguages || "typescript",
|
|
172
|
+
clientFrameworks: options?.clientFrameworks || "react"
|
|
173
|
+
};
|
|
174
|
+
console.log(`[FIGMA CLIENT] Calling get_metadata with args:`, args);
|
|
175
|
+
try {
|
|
176
|
+
const result = await this.client.callTool({
|
|
177
|
+
name: "get_metadata",
|
|
178
|
+
arguments: args
|
|
179
|
+
});
|
|
180
|
+
console.log(`[FIGMA CLIENT] get_metadata response type:`, typeof result);
|
|
181
|
+
console.log(
|
|
182
|
+
`[FIGMA CLIENT] get_metadata response keys:`,
|
|
183
|
+
Object.keys(result)
|
|
184
|
+
);
|
|
185
|
+
return result;
|
|
186
|
+
} catch (error) {
|
|
187
|
+
console.warn("Error calling get_metadata, attempting reconnect...");
|
|
188
|
+
this.connected = false;
|
|
189
|
+
await this.connect();
|
|
190
|
+
const result = await this.client.callTool({
|
|
191
|
+
name: "get_metadata",
|
|
192
|
+
arguments: args
|
|
193
|
+
});
|
|
194
|
+
return result;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Call Figma's get_screenshot tool
|
|
199
|
+
*/
|
|
200
|
+
async getScreenshot(nodeId, options) {
|
|
201
|
+
await this.ensureConnected();
|
|
202
|
+
try {
|
|
203
|
+
const result = await this.client.callTool({
|
|
204
|
+
name: "get_screenshot",
|
|
205
|
+
arguments: {
|
|
206
|
+
...nodeId ? { nodeId } : {},
|
|
207
|
+
clientLanguages: options?.clientLanguages || "typescript",
|
|
208
|
+
clientFrameworks: options?.clientFrameworks || "react"
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
return result;
|
|
212
|
+
} catch (error) {
|
|
213
|
+
console.warn("Error calling get_screenshot, attempting reconnect...");
|
|
214
|
+
this.connected = false;
|
|
215
|
+
await this.connect();
|
|
216
|
+
const result = await this.client.callTool({
|
|
217
|
+
name: "get_screenshot",
|
|
218
|
+
arguments: {
|
|
219
|
+
...nodeId ? { nodeId } : {},
|
|
220
|
+
clientLanguages: options?.clientLanguages || "typescript",
|
|
221
|
+
clientFrameworks: options?.clientFrameworks || "react"
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
return result;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Close connection
|
|
229
|
+
*/
|
|
230
|
+
async disconnect() {
|
|
231
|
+
if (this.client) {
|
|
232
|
+
await this.client.close();
|
|
233
|
+
this.connected = false;
|
|
234
|
+
this.client = null;
|
|
235
|
+
console.log("Disconnected from Figma MCP");
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
// src/figma2frida/handlers/component-suggestion-handler.ts
|
|
241
|
+
import "dotenv/config";
|
|
242
|
+
|
|
243
|
+
// src/figma2frida/services/pinecone/pinecone-service.ts
|
|
244
|
+
import { Pinecone } from "@pinecone-database/pinecone";
|
|
245
|
+
import "dotenv/config";
|
|
246
|
+
var PineconeSearchService = class {
|
|
247
|
+
apiKey;
|
|
248
|
+
indexName;
|
|
249
|
+
namespace;
|
|
250
|
+
minScore;
|
|
251
|
+
topK;
|
|
252
|
+
fridaEmbeddingUrl;
|
|
253
|
+
fridaApiKey;
|
|
254
|
+
pinecone = null;
|
|
255
|
+
index = null;
|
|
256
|
+
constructor(options = {}) {
|
|
257
|
+
this.apiKey = options.apiKey || process.env.PINECONE_API_KEY || (() => {
|
|
258
|
+
throw new Error(
|
|
259
|
+
"PINECONE_API_KEY is required. Set it in .env or pass it in options."
|
|
260
|
+
);
|
|
261
|
+
})();
|
|
262
|
+
this.indexName = process.env.PINECONE_INDEX;
|
|
263
|
+
this.namespace = options.namespace;
|
|
264
|
+
this.minScore = parseFloat(process.env.PINECONE_MIN_SCORE);
|
|
265
|
+
this.topK = parseInt(process.env.PINECONE_TOP_K, 10);
|
|
266
|
+
this.fridaEmbeddingUrl = options.fridaEmbeddingUrl || process.env.FRIDA_EMBEDDING_URL || (() => {
|
|
267
|
+
throw new Error(
|
|
268
|
+
"FRIDA_EMBEDDING_URL is required. Set it in .env or pass it in options."
|
|
269
|
+
);
|
|
270
|
+
})();
|
|
271
|
+
this.fridaApiKey = options.fridaApiKey || process.env.FRIDA_BEARER_TOKEN || (() => {
|
|
272
|
+
throw new Error(
|
|
273
|
+
"FRIDA_BEARER_TOKEN is required. Set it in .env or pass it in options."
|
|
274
|
+
);
|
|
275
|
+
})();
|
|
276
|
+
this.pinecone = new Pinecone({ apiKey: this.apiKey });
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Initializes the Pinecone index (lazy loading)
|
|
280
|
+
* Namespace is set here when getting the index reference using method chaining
|
|
281
|
+
*/
|
|
282
|
+
async initialize() {
|
|
283
|
+
if (!this.index) {
|
|
284
|
+
console.log(`[PINECONE] Connecting to index: ${this.indexName}${this.namespace ? ` namespace "${this.namespace}"` : ""}`);
|
|
285
|
+
const indexRef = this.pinecone.index(this.indexName);
|
|
286
|
+
this.index = this.namespace ? indexRef.namespace(this.namespace) : indexRef;
|
|
287
|
+
console.log(`[PINECONE] Connected to index: ${this.indexName}${this.namespace ? ` namespace "${this.namespace}"` : ""}`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Generates embedding for a text using Frida API
|
|
292
|
+
* @param text - Text to embed
|
|
293
|
+
* @returns Promise<number[]> Embedding vector (1536 dimensions)
|
|
294
|
+
*/
|
|
295
|
+
async generateEmbedding(text) {
|
|
296
|
+
try {
|
|
297
|
+
console.log(`[PINECONE] Calling Frida Embedding API...`);
|
|
298
|
+
const startTime = Date.now();
|
|
299
|
+
const response = await fetch(this.fridaEmbeddingUrl, {
|
|
300
|
+
method: "POST",
|
|
301
|
+
headers: {
|
|
302
|
+
accept: "application/json",
|
|
303
|
+
Authorization: `Bearer ${this.fridaApiKey}`,
|
|
304
|
+
"Content-Type": "application/json"
|
|
305
|
+
},
|
|
306
|
+
body: JSON.stringify({
|
|
307
|
+
input: text,
|
|
308
|
+
model: "text-embedding-ada-002",
|
|
309
|
+
user_id: "pinecone-search",
|
|
310
|
+
email: "design_system@example.com"
|
|
311
|
+
})
|
|
312
|
+
});
|
|
313
|
+
console.log(`[PINECONE] API response status: ${response.status}`);
|
|
314
|
+
if (!response.ok) {
|
|
315
|
+
const errorText = await response.text();
|
|
316
|
+
console.error(`[PINECONE] API Error response:`, errorText);
|
|
317
|
+
throw new Error(`API Error: ${response.status} ${response.statusText}`);
|
|
318
|
+
}
|
|
319
|
+
console.log(`[PINECONE] Parsing API response...`);
|
|
320
|
+
const data = await response.json();
|
|
321
|
+
if (!data.data || !data.data[0] || !data.data[0].embedding) {
|
|
322
|
+
console.error(`[PINECONE] Invalid response format:`, JSON.stringify(data).substring(0, 200));
|
|
323
|
+
throw new Error("Invalid API response format");
|
|
324
|
+
}
|
|
325
|
+
const embedding = data.data[0].embedding;
|
|
326
|
+
const elapsed = Date.now() - startTime;
|
|
327
|
+
console.log(`[PINECONE] \u2713 Embedding generated (${embedding.length} dims) in ${elapsed}ms`);
|
|
328
|
+
return embedding;
|
|
329
|
+
} catch (error) {
|
|
330
|
+
console.error(`[PINECONE] \u2717 Error generating embedding:`, error instanceof Error ? error.message : String(error));
|
|
331
|
+
throw new Error(`Failed to generate embedding: ${error instanceof Error ? error.message : String(error)}`);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Parses a Pinecone match to structured format
|
|
336
|
+
* Supports new schema with properties, events, methods, slots, etc.
|
|
337
|
+
*/
|
|
338
|
+
parseMatch(match) {
|
|
339
|
+
const metadata = match.metadata || {};
|
|
340
|
+
let props = [];
|
|
341
|
+
try {
|
|
342
|
+
if (metadata.properties) {
|
|
343
|
+
const parsedProps = JSON.parse(
|
|
344
|
+
metadata.properties
|
|
345
|
+
);
|
|
346
|
+
if (Array.isArray(parsedProps)) {
|
|
347
|
+
props = parsedProps.map((p) => ({
|
|
348
|
+
name: p.name || "unknown",
|
|
349
|
+
type_signature: p.type_signature || "unknown",
|
|
350
|
+
default_value: p.default_value,
|
|
351
|
+
description: p.description || "",
|
|
352
|
+
is_utility_prop: p.is_utility_prop || false,
|
|
353
|
+
reflects_to_attribute: p.reflects_to_attribute || false
|
|
354
|
+
}));
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
} catch {
|
|
358
|
+
props = [];
|
|
359
|
+
}
|
|
360
|
+
let events = [];
|
|
361
|
+
try {
|
|
362
|
+
if (metadata.events) {
|
|
363
|
+
const parsedEvents = JSON.parse(
|
|
364
|
+
metadata.events
|
|
365
|
+
);
|
|
366
|
+
if (Array.isArray(parsedEvents)) {
|
|
367
|
+
events = parsedEvents.map((e) => ({
|
|
368
|
+
name: e.name || "unknown",
|
|
369
|
+
payload_type: e.payload_type || "any",
|
|
370
|
+
description: e.description || ""
|
|
371
|
+
}));
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
} catch {
|
|
375
|
+
events = [];
|
|
376
|
+
}
|
|
377
|
+
let methods = [];
|
|
378
|
+
try {
|
|
379
|
+
if (metadata.methods) {
|
|
380
|
+
const parsedMethods = JSON.parse(metadata.methods);
|
|
381
|
+
if (Array.isArray(parsedMethods)) {
|
|
382
|
+
methods = parsedMethods.map((m) => ({
|
|
383
|
+
name: m.name || "unknown",
|
|
384
|
+
parameters: m.parameters || "null",
|
|
385
|
+
description: m.description || ""
|
|
386
|
+
}));
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
} catch {
|
|
390
|
+
methods = [];
|
|
391
|
+
}
|
|
392
|
+
let slots = [];
|
|
393
|
+
try {
|
|
394
|
+
if (metadata.slots) {
|
|
395
|
+
const parsedSlots = JSON.parse(metadata.slots);
|
|
396
|
+
if (Array.isArray(parsedSlots)) {
|
|
397
|
+
slots = parsedSlots.map((s) => ({
|
|
398
|
+
name: s.name || "unknown",
|
|
399
|
+
description: s.description || ""
|
|
400
|
+
}));
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
} catch {
|
|
404
|
+
slots = [];
|
|
405
|
+
}
|
|
406
|
+
let usage_examples = [];
|
|
407
|
+
try {
|
|
408
|
+
if (metadata.usage_examples) {
|
|
409
|
+
const parsedExamples = JSON.parse(metadata.usage_examples);
|
|
410
|
+
if (Array.isArray(parsedExamples)) {
|
|
411
|
+
usage_examples = parsedExamples.map((ex) => ({
|
|
412
|
+
title: ex.title || "",
|
|
413
|
+
description: ex.description || "",
|
|
414
|
+
code: {
|
|
415
|
+
template: ex.code?.template || "",
|
|
416
|
+
script: ex.code?.script
|
|
417
|
+
}
|
|
418
|
+
}));
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
} catch {
|
|
422
|
+
usage_examples = [];
|
|
423
|
+
}
|
|
424
|
+
let usage_limitations = [];
|
|
425
|
+
try {
|
|
426
|
+
if (metadata.usage_limitations) {
|
|
427
|
+
const parsedLimitations = JSON.parse(metadata.usage_limitations);
|
|
428
|
+
if (Array.isArray(parsedLimitations)) {
|
|
429
|
+
usage_limitations = parsedLimitations;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
} catch {
|
|
433
|
+
usage_limitations = [];
|
|
434
|
+
}
|
|
435
|
+
let hooks_or_services = [];
|
|
436
|
+
try {
|
|
437
|
+
if (metadata.hooks_or_services) {
|
|
438
|
+
const parsedHooks = JSON.parse(metadata.hooks_or_services);
|
|
439
|
+
if (Array.isArray(parsedHooks)) {
|
|
440
|
+
hooks_or_services = parsedHooks;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
} catch {
|
|
444
|
+
hooks_or_services = [];
|
|
445
|
+
}
|
|
446
|
+
const tag = metadata.selector || metadata.id || "unknown";
|
|
447
|
+
return {
|
|
448
|
+
tag,
|
|
449
|
+
score: match.score || 0,
|
|
450
|
+
category: metadata.category || "General",
|
|
451
|
+
description: metadata.short_description || "",
|
|
452
|
+
keywords: metadata.keywords || "",
|
|
453
|
+
props,
|
|
454
|
+
events,
|
|
455
|
+
// New fields
|
|
456
|
+
id: metadata.id,
|
|
457
|
+
selector: metadata.selector,
|
|
458
|
+
human_name: metadata.human_name,
|
|
459
|
+
full_readme: metadata.full_readme,
|
|
460
|
+
visual_variants_description: metadata.visual_variants_description,
|
|
461
|
+
framework_type: metadata.framework_type,
|
|
462
|
+
import_statement: metadata.import_statement,
|
|
463
|
+
is_global: metadata.is_global,
|
|
464
|
+
module_import: metadata.module_import,
|
|
465
|
+
content_injection_method: metadata.content_injection_method,
|
|
466
|
+
methods,
|
|
467
|
+
slots,
|
|
468
|
+
usage_limitations,
|
|
469
|
+
usage_examples,
|
|
470
|
+
hooks_or_services
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* Searches for components in Pinecone
|
|
475
|
+
* @param query - Search query
|
|
476
|
+
* @param options - Search options
|
|
477
|
+
* @returns Structured results
|
|
478
|
+
*/
|
|
479
|
+
async search(query, options = {}) {
|
|
480
|
+
if (!query || typeof query !== "string") {
|
|
481
|
+
throw new Error("Query must be a non-empty string");
|
|
482
|
+
}
|
|
483
|
+
await this.initialize();
|
|
484
|
+
const minScore = options.minScore !== void 0 ? options.minScore : this.minScore;
|
|
485
|
+
const topK = options.topK !== void 0 ? options.topK : this.topK;
|
|
486
|
+
console.log(`[PINECONE] Starting search - Query: "${query}", topK: ${topK}, minScore: ${minScore}${this.namespace ? `, namespace: "${this.namespace}"` : ""}`);
|
|
487
|
+
const embedding = await this.generateEmbedding(query);
|
|
488
|
+
console.log(`[PINECONE] Querying Pinecone index "${this.indexName}"${this.namespace ? ` namespace "${this.namespace}"` : ""}...`);
|
|
489
|
+
const startTime = Date.now();
|
|
490
|
+
const queryResponse = await this.index.query({
|
|
491
|
+
vector: embedding,
|
|
492
|
+
topK,
|
|
493
|
+
includeMetadata: true
|
|
494
|
+
});
|
|
495
|
+
const queryTime = Date.now() - startTime;
|
|
496
|
+
console.log(`[PINECONE] Query completed in ${queryTime}ms - Found ${queryResponse.matches?.length || 0} matches`);
|
|
497
|
+
const allMatches = queryResponse.matches || [];
|
|
498
|
+
const relevantMatches = allMatches.filter((m) => (m.score || 0) >= minScore);
|
|
499
|
+
console.log(`[PINECONE] Filtered results: ${relevantMatches.length} relevant (score >= ${minScore}) out of ${allMatches.length} total`);
|
|
500
|
+
return {
|
|
501
|
+
query,
|
|
502
|
+
totalMatches: allMatches.length,
|
|
503
|
+
relevantMatches: relevantMatches.length,
|
|
504
|
+
matches: relevantMatches.map((match) => this.parseMatch(match)),
|
|
505
|
+
lowScoreMatches: allMatches.filter((m) => (m.score || 0) < minScore).map((match) => {
|
|
506
|
+
const metadata = match.metadata || {};
|
|
507
|
+
const tag = metadata.selector || metadata.id || metadata.tag || "unknown";
|
|
508
|
+
return {
|
|
509
|
+
tag,
|
|
510
|
+
score: match.score || 0
|
|
511
|
+
};
|
|
512
|
+
}),
|
|
513
|
+
searchMetadata: {
|
|
514
|
+
minScore,
|
|
515
|
+
topK,
|
|
516
|
+
indexName: this.indexName,
|
|
517
|
+
namespace: this.namespace
|
|
518
|
+
}
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Gets details of a specific component by tag, selector, or id
|
|
523
|
+
* @param tag - Component tag, selector, or id (e.g.: 'my-badge', 'Button', 'custom-table')
|
|
524
|
+
* @returns Component details or null if it doesn't exist
|
|
525
|
+
*/
|
|
526
|
+
async getComponentDetails(tag) {
|
|
527
|
+
if (!tag || typeof tag !== "string") {
|
|
528
|
+
throw new Error("Tag must be a non-empty string");
|
|
529
|
+
}
|
|
530
|
+
await this.initialize();
|
|
531
|
+
const results = await this.search(tag, { topK: 10, minScore: 0 });
|
|
532
|
+
const exactMatch = results.matches.find(
|
|
533
|
+
(m) => m.tag === tag || m.selector === tag || m.id === tag
|
|
534
|
+
);
|
|
535
|
+
if (exactMatch) {
|
|
536
|
+
return exactMatch;
|
|
537
|
+
}
|
|
538
|
+
if (results.matches.length > 0) {
|
|
539
|
+
return results.matches[0];
|
|
540
|
+
}
|
|
541
|
+
return null;
|
|
542
|
+
}
|
|
543
|
+
/**
|
|
544
|
+
* Searches for multiple components by tags
|
|
545
|
+
* @param tags - Array of component tags
|
|
546
|
+
* @returns Array of component details
|
|
547
|
+
*/
|
|
548
|
+
async getMultipleComponents(tags) {
|
|
549
|
+
if (!Array.isArray(tags)) {
|
|
550
|
+
throw new Error("Tags must be an array");
|
|
551
|
+
}
|
|
552
|
+
const results = await Promise.all(
|
|
553
|
+
tags.map((tag) => this.getComponentDetails(tag))
|
|
554
|
+
);
|
|
555
|
+
return results.filter(
|
|
556
|
+
(r) => r !== null
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
};
|
|
560
|
+
|
|
561
|
+
// src/figma2frida/services/frida/frida-client.ts
|
|
562
|
+
import "dotenv/config";
|
|
563
|
+
var FridaClient = class {
|
|
564
|
+
apiKey;
|
|
565
|
+
apiUrl;
|
|
566
|
+
model;
|
|
567
|
+
maxTokens;
|
|
568
|
+
enabled;
|
|
569
|
+
constructor(options = {}) {
|
|
570
|
+
this.apiKey = options.apiKey || process.env.FRIDA_BEARER_TOKEN;
|
|
571
|
+
this.apiUrl = options.apiUrl || process.env.FRIDA_API_URL;
|
|
572
|
+
this.model = options.model || process.env.FRIDA_MODEL || "Innovation-gpt4o";
|
|
573
|
+
this.maxTokens = options.maxTokens || (process.env.FRIDA_MAX_TOKENS ? parseInt(process.env.FRIDA_MAX_TOKENS, 10) : 4096);
|
|
574
|
+
this.enabled = !!(this.apiKey && this.apiUrl);
|
|
575
|
+
}
|
|
576
|
+
/**
|
|
577
|
+
* Checks if Frida is configured
|
|
578
|
+
*/
|
|
579
|
+
isEnabled() {
|
|
580
|
+
return this.enabled;
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* Generates the optimized prompt for code generation
|
|
584
|
+
*/
|
|
585
|
+
buildPrompt(question, context) {
|
|
586
|
+
return `You are an expert agent in code generation for UI design systems and component libraries.
|
|
587
|
+
Your goal is to provide technical, precise, and ready-to-use answers that enable generating functional code immediately.
|
|
588
|
+
|
|
589
|
+
**\u{1F6A8} CRITICAL RULES - MANDATORY COMPLIANCE:**
|
|
590
|
+
|
|
591
|
+
1. **You MUST use ONLY the components** from the AVAILABLE COMPONENT INFORMATION below
|
|
592
|
+
2. **NEVER suggest or use:**
|
|
593
|
+
- \u274C Components from other libraries not listed in the AVAILABLE COMPONENT INFORMATION
|
|
594
|
+
- \u274C Generic HTML form elements: <button>, <input>, <select>, <textarea> (unless they are wrapped in design system components)
|
|
595
|
+
- \u274C Any other component library or framework-specific components not in the AVAILABLE COMPONENT INFORMATION
|
|
596
|
+
|
|
597
|
+
3. **Use components exactly as documented** - they may be Web Components, React components, Angular components, or any other framework
|
|
598
|
+
4. **If a component is not in the AVAILABLE COMPONENT INFORMATION**, say so clearly - DO NOT invent or use generic alternatives
|
|
599
|
+
|
|
600
|
+
**EXAMPLES OF WHAT NOT TO DO:**
|
|
601
|
+
- \u274C WRONG: Using components from other libraries (mat-*, MUI, etc.) unless they appear in AVAILABLE COMPONENT INFORMATION
|
|
602
|
+
- \u274C WRONG: Using any component that is NOT in the AVAILABLE COMPONENT INFORMATION below
|
|
603
|
+
- \u274C WRONG: Inventing component names or props not in the documentation
|
|
604
|
+
|
|
605
|
+
**EXAMPLES OF WHAT TO DO:**
|
|
606
|
+
- \u2705 Use ONLY components from the AVAILABLE COMPONENT INFORMATION below
|
|
607
|
+
- \u2705 Check the component tag/selector in the AVAILABLE COMPONENT INFORMATION before using it
|
|
608
|
+
- \u2705 Use the exact component names, props, and events as shown in the AVAILABLE COMPONENT INFORMATION
|
|
609
|
+
- \u2705 Follow the framework_type and import_statement provided for each component
|
|
610
|
+
- \u2705 If a component is not listed in AVAILABLE COMPONENT INFORMATION, do NOT use it - say clearly that it doesn't exist
|
|
611
|
+
|
|
612
|
+
## AVAILABLE COMPONENT INFORMATION:
|
|
613
|
+
|
|
614
|
+
The component information below is provided in JSON format for accurate parsing.
|
|
615
|
+
Use the exact prop names, types, and structure as shown in the JSON.
|
|
616
|
+
|
|
617
|
+
${context}
|
|
618
|
+
|
|
619
|
+
**IMPORTANT:** The component information above is in JSON format. Parse it carefully and use:
|
|
620
|
+
- Exact prop names as shown in the props array
|
|
621
|
+
- Exact prop types from type_signature field
|
|
622
|
+
- Event names exactly as shown in the events array
|
|
623
|
+
- Framework type from framework_type field
|
|
624
|
+
- Import statements from import_statement field
|
|
625
|
+
- Follow the structure and patterns from the JSON data
|
|
626
|
+
|
|
627
|
+
## RESPONSE INSTRUCTIONS:
|
|
628
|
+
|
|
629
|
+
### 1. MANDATORY STRUCTURE:
|
|
630
|
+
Your response MUST follow this format:
|
|
631
|
+
|
|
632
|
+
\u{1F6A8} **RECOMMENDED COMPONENT:** [component name - MUST be from AVAILABLE COMPONENT INFORMATION]
|
|
633
|
+
**CATEGORY:** [component category]
|
|
634
|
+
**FRAMEWORK:** [framework_type from the component data]
|
|
635
|
+
|
|
636
|
+
**DESCRIPTION:**
|
|
637
|
+
[Brief explanation of the component and when to use it]
|
|
638
|
+
|
|
639
|
+
**IMPORT:**
|
|
640
|
+
\`\`\`
|
|
641
|
+
[import_statement from the component data if available]
|
|
642
|
+
\`\`\`
|
|
643
|
+
|
|
644
|
+
**EXAMPLE CODE:**
|
|
645
|
+
\`\`\`html
|
|
646
|
+
[Minimal functional and complete example]
|
|
647
|
+
\`\`\`
|
|
648
|
+
|
|
649
|
+
**IMPORTANT PROPERTIES:**
|
|
650
|
+
[List of key properties with types and possible values]
|
|
651
|
+
- \`property\`: \`type_signature\` - [description]
|
|
652
|
+
|
|
653
|
+
**EVENT HANDLING:**
|
|
654
|
+
[If applicable, how to listen and handle events]
|
|
655
|
+
\`\`\`javascript
|
|
656
|
+
[Example code to handle events]
|
|
657
|
+
\`\`\`
|
|
658
|
+
|
|
659
|
+
**ADDITIONAL NOTES:**
|
|
660
|
+
[Any important considerations, best practices, or limitations]
|
|
661
|
+
|
|
662
|
+
### 2. CODE GENERATION RULES:
|
|
663
|
+
|
|
664
|
+
1. **Use ONLY components from the AVAILABLE COMPONENT INFORMATION above** - Do NOT use components from other libraries
|
|
665
|
+
2. **Do not invent properties or events** - Use only what exists in the component documentation provided
|
|
666
|
+
3. **Exact types:** Include the exact types of properties from type_signature field
|
|
667
|
+
4. **Functional code:** The example code MUST be copyable and functional
|
|
668
|
+
5. **Required props:** If a prop is required, indicate it clearly
|
|
669
|
+
6. **Default values:** If there are default values (default_value field), mention them
|
|
670
|
+
7. **Events:** Include examples of how to listen to events if the component emits them
|
|
671
|
+
8. **Framework compatibility:** Use the framework_type and import_statement from the component data
|
|
672
|
+
9. **Necessary imports:** Include the import_statement from the component data
|
|
673
|
+
10. **If a component doesn't exist:** Say clearly "No component available for [feature]. Available components are: [list]"
|
|
674
|
+
|
|
675
|
+
### 3. QUESTION ANALYSIS:
|
|
676
|
+
|
|
677
|
+
- If asking about a specific component: Provide complete technical documentation
|
|
678
|
+
- If asking "how to do X": Suggest the most suitable component AND provide functional code
|
|
679
|
+
- If asking about properties: List ALL relevant properties with exact types
|
|
680
|
+
- If asking about events: Provide complete examples of event handling
|
|
681
|
+
|
|
682
|
+
### 4. TECHNICAL ACCURACY:
|
|
683
|
+
|
|
684
|
+
- Use the exact names of properties as they appear in the documentation
|
|
685
|
+
- Respect exact types (union types, enums, etc.)
|
|
686
|
+
- Include all attributes necessary for the code to work
|
|
687
|
+
- Mention if there are slots available and how to use them
|
|
688
|
+
|
|
689
|
+
### 5. IF NO INFORMATION:
|
|
690
|
+
|
|
691
|
+
If the context does not contain enough information to answer, say clearly:
|
|
692
|
+
"I don't have enough information about [aspect] in the provided context. Available components are: [list]"
|
|
693
|
+
|
|
694
|
+
## USER QUESTION:
|
|
695
|
+
${question}
|
|
696
|
+
|
|
697
|
+
## RESPONSE (Follow the mandatory structure above):`.trim();
|
|
698
|
+
}
|
|
699
|
+
/**
|
|
700
|
+
* Builds a specialized prompt for code generation from Figma designs
|
|
701
|
+
*/
|
|
702
|
+
buildCodeGenerationPrompt(originalCode, componentContext) {
|
|
703
|
+
const elementMatches = originalCode.match(/<\w+/g) || [];
|
|
704
|
+
const elementCount = elementMatches.length;
|
|
705
|
+
return `You are an expert in generating code using design system components.
|
|
706
|
+
|
|
707
|
+
**TASK:** Transform the Figma-generated code below to use design system components from the AVAILABLE COMPONENT INFORMATION.
|
|
708
|
+
|
|
709
|
+
**\u{1F6A8} CRITICAL RULES - MANDATORY:**
|
|
710
|
+
|
|
711
|
+
1. **PRESERVE EVERY SINGLE ELEMENT** - The original code has approximately ${elementCount} HTML elements. You MUST transform ALL of them, not just a few examples. Do NOT summarize, simplify, or create examples.
|
|
712
|
+
2. **PRESERVE ALL TEXT CONTENT EXACTLY** - Keep ALL text, labels, placeholders, button text, input values, and content exactly as shown in the original code. DO NOT change, invent, or modify any text content.
|
|
713
|
+
3. **PRESERVE ALL STRUCTURE** - Maintain EVERY div, container, class, style, id, data attribute, and layout element exactly as in the original.
|
|
714
|
+
4. **Use ONLY components from AVAILABLE COMPONENT INFORMATION** - Do NOT use components from other libraries or generic HTML elements unless no alternative exists
|
|
715
|
+
5. **Use EXACT prop names and types** from the component documentation (check type_signature field)
|
|
716
|
+
6. **Map HTML attributes to correct component props** based on component documentation
|
|
717
|
+
7. **Handle events properly** if the component emits them (check events array)
|
|
718
|
+
8. **Use slots or content_injection_method** if the component supports them
|
|
719
|
+
9. **Generate functional, copyable code** that works immediately
|
|
720
|
+
10. **Follow framework_type and import_statement** from the component data
|
|
721
|
+
|
|
722
|
+
**CRITICAL REQUIREMENTS:**
|
|
723
|
+
- Transform EVERY element in the original code (approximately ${elementCount} elements)
|
|
724
|
+
- Keep ALL text content exactly as it appears in the original (no changes to labels, placeholders, values, etc.)
|
|
725
|
+
- Maintain ALL CSS classes, inline styles, ids, data attributes, and structure
|
|
726
|
+
- Do NOT summarize, simplify, or create examples - transform the ENTIRE code
|
|
727
|
+
- If you cannot transform an element (component doesn't exist), preserve it as-is with all its attributes and content
|
|
728
|
+
|
|
729
|
+
**VALIDATION:**
|
|
730
|
+
After transformation, you should have approximately the same number of elements as the original (${elementCount}). Every element must be accounted for.
|
|
731
|
+
|
|
732
|
+
## AVAILABLE COMPONENT INFORMATION:
|
|
733
|
+
|
|
734
|
+
${componentContext}
|
|
735
|
+
|
|
736
|
+
## ORIGINAL CODE FROM FIGMA:
|
|
737
|
+
|
|
738
|
+
\`\`\`html
|
|
739
|
+
${originalCode}
|
|
740
|
+
\`\`\`
|
|
741
|
+
|
|
742
|
+
## YOUR TASK:
|
|
743
|
+
|
|
744
|
+
Transform the ENTIRE code above to use design system components. Return ONLY the complete transformed HTML code wrapped in \`\`\`html code blocks. Include ALL ${elementCount} elements and ALL text from the original. No explanations or markdown outside the code block.
|
|
745
|
+
|
|
746
|
+
**Transformed Code (must include all ${elementCount} elements with all original text):**
|
|
747
|
+
\`\`\`html
|
|
748
|
+
`;
|
|
749
|
+
}
|
|
750
|
+
/**
|
|
751
|
+
* Generates code using component documentation from Pinecone
|
|
752
|
+
* @param originalCode - Original code from Figma
|
|
753
|
+
* @param componentContext - Component documentation from Pinecone (JSON format)
|
|
754
|
+
* @returns Frida response with generated code
|
|
755
|
+
*/
|
|
756
|
+
async generateCode(originalCode, componentContext) {
|
|
757
|
+
if (!this.enabled) {
|
|
758
|
+
return {
|
|
759
|
+
success: false,
|
|
760
|
+
error: "Frida AI is not configured. Set FRIDA_BEARER_TOKEN and FRIDA_API_URL in .env",
|
|
761
|
+
response: null
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
if (!originalCode || typeof originalCode !== "string") {
|
|
765
|
+
return {
|
|
766
|
+
success: false,
|
|
767
|
+
error: "Original code must be a non-empty string",
|
|
768
|
+
response: null
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
console.log(`
|
|
772
|
+
[FRIDA CODE GEN] ========================================`);
|
|
773
|
+
console.log(`[FRIDA CODE GEN] \u{1F680} CALLING FRIDA AI FOR CODE GENERATION`);
|
|
774
|
+
console.log(`[FRIDA CODE GEN] ========================================`);
|
|
775
|
+
const prompt = this.buildCodeGenerationPrompt(originalCode, componentContext);
|
|
776
|
+
const codeGenMaxTokens = process.env.FRIDA_CODE_GEN_MAX_TOKENS ? parseInt(process.env.FRIDA_CODE_GEN_MAX_TOKENS, 10) : Math.max(this.maxTokens, 16384);
|
|
777
|
+
console.log(`[FRIDA CODE GEN] Model: ${this.model}`);
|
|
778
|
+
console.log(`[FRIDA CODE GEN] Original code length: ${originalCode.length} chars`);
|
|
779
|
+
console.log(`[FRIDA CODE GEN] Component context length: ${componentContext.length} chars`);
|
|
780
|
+
console.log(`[FRIDA CODE GEN] Max tokens: ${codeGenMaxTokens} (default: ${this.maxTokens})`);
|
|
781
|
+
try {
|
|
782
|
+
const startTime = Date.now();
|
|
783
|
+
const response = await fetch(this.apiUrl, {
|
|
784
|
+
method: "POST",
|
|
785
|
+
headers: {
|
|
786
|
+
"Content-Type": "application/json",
|
|
787
|
+
Authorization: `Bearer ${this.apiKey}`
|
|
788
|
+
},
|
|
789
|
+
body: JSON.stringify({
|
|
790
|
+
model: this.model,
|
|
791
|
+
input: prompt,
|
|
792
|
+
max_tokens: codeGenMaxTokens,
|
|
793
|
+
stream: false,
|
|
794
|
+
email: "design_system@example.com"
|
|
795
|
+
})
|
|
796
|
+
});
|
|
797
|
+
const fetchTime = Date.now() - startTime;
|
|
798
|
+
console.log(`[FRIDA CODE GEN] API request completed in ${fetchTime}ms - Status: ${response.status}`);
|
|
799
|
+
if (!response.ok) {
|
|
800
|
+
console.error(`[FRIDA CODE GEN] API Error: ${response.status} ${response.statusText}`);
|
|
801
|
+
throw new Error(`API Error: ${response.status} ${response.statusText}`);
|
|
802
|
+
}
|
|
803
|
+
const data = await response.json();
|
|
804
|
+
console.log(`[FRIDA CODE GEN] Response received successfully`);
|
|
805
|
+
const answer = data.response || data.output || data.choices?.[0]?.message?.content || data.content || JSON.stringify(data);
|
|
806
|
+
const tokensUsed = data.usage?.total_tokens || null;
|
|
807
|
+
const answerLength = typeof answer === "string" ? answer.length : String(answer).length;
|
|
808
|
+
console.log(`[FRIDA CODE GEN] ========================================`);
|
|
809
|
+
console.log(`[FRIDA CODE GEN] \u2705 CODE GENERATION COMPLETE`);
|
|
810
|
+
console.log(`[FRIDA CODE GEN] ========================================`);
|
|
811
|
+
console.log(`[FRIDA CODE GEN] \u{1F4CA} TOKENS USED: ${tokensUsed || "N/A"}`);
|
|
812
|
+
console.log(`[FRIDA CODE GEN] \u{1F4DD} Answer length: ${answerLength} chars`);
|
|
813
|
+
console.log(`[FRIDA CODE GEN] \u23F1\uFE0F Total time: ${fetchTime}ms`);
|
|
814
|
+
console.log(`[FRIDA CODE GEN] ========================================
|
|
815
|
+
`);
|
|
816
|
+
return {
|
|
817
|
+
success: true,
|
|
818
|
+
error: null,
|
|
819
|
+
response: typeof answer === "string" ? answer : String(answer),
|
|
820
|
+
metadata: {
|
|
821
|
+
model: this.model,
|
|
822
|
+
tokensUsed
|
|
823
|
+
}
|
|
824
|
+
};
|
|
825
|
+
} catch (error) {
|
|
826
|
+
console.error(`[FRIDA CODE GEN] Error calling Frida AI:`, error instanceof Error ? error.message : String(error));
|
|
827
|
+
return {
|
|
828
|
+
success: false,
|
|
829
|
+
error: error instanceof Error ? error.message : String(error),
|
|
830
|
+
response: null
|
|
831
|
+
};
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
/**
|
|
835
|
+
* Asks a question to Frida AI
|
|
836
|
+
* @param question - User question
|
|
837
|
+
* @param context - Context with component information
|
|
838
|
+
* @returns Frida response
|
|
839
|
+
*/
|
|
840
|
+
async ask(question, context) {
|
|
841
|
+
if (!this.enabled) {
|
|
842
|
+
return {
|
|
843
|
+
success: false,
|
|
844
|
+
error: "Frida AI is not configured. Set FRIDA_BEARER_TOKEN and FRIDA_API_URL in .env",
|
|
845
|
+
response: null
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
if (!question || typeof question !== "string") {
|
|
849
|
+
return {
|
|
850
|
+
success: false,
|
|
851
|
+
error: "Question must be a non-empty string",
|
|
852
|
+
response: null
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
console.log(`
|
|
856
|
+
[FRIDA] ========================================`);
|
|
857
|
+
console.log(`[FRIDA] \u{1F680} CALLING FRIDA AI`);
|
|
858
|
+
console.log(`[FRIDA] ========================================`);
|
|
859
|
+
const prompt = this.buildPrompt(question, context);
|
|
860
|
+
console.log(`[FRIDA] API URL: ${this.apiUrl}`);
|
|
861
|
+
console.log(`[FRIDA] Model: ${this.model}`);
|
|
862
|
+
console.log(`[FRIDA] Question length: ${question.length} chars`);
|
|
863
|
+
console.log(`[FRIDA] Context length: ${context.length} chars`);
|
|
864
|
+
console.log(`[FRIDA] Max tokens: ${this.maxTokens}`);
|
|
865
|
+
try {
|
|
866
|
+
const startTime = Date.now();
|
|
867
|
+
const response = await fetch(this.apiUrl, {
|
|
868
|
+
method: "POST",
|
|
869
|
+
headers: {
|
|
870
|
+
"Content-Type": "application/json",
|
|
871
|
+
Authorization: `Bearer ${this.apiKey}`
|
|
872
|
+
},
|
|
873
|
+
body: JSON.stringify({
|
|
874
|
+
model: this.model,
|
|
875
|
+
input: prompt,
|
|
876
|
+
max_tokens: this.maxTokens,
|
|
877
|
+
stream: false,
|
|
878
|
+
email: "design_system@example.com"
|
|
879
|
+
})
|
|
880
|
+
});
|
|
881
|
+
const fetchTime = Date.now() - startTime;
|
|
882
|
+
console.log(`[FRIDA] API request completed in ${fetchTime}ms - Status: ${response.status}`);
|
|
883
|
+
if (!response.ok) {
|
|
884
|
+
console.error(`[FRIDA] API Error: ${response.status} ${response.statusText}`);
|
|
885
|
+
throw new Error(`API Error: ${response.status} ${response.statusText}`);
|
|
886
|
+
}
|
|
887
|
+
const data = await response.json();
|
|
888
|
+
console.log(`[FRIDA] Response received successfully`);
|
|
889
|
+
const answer = data.response || data.output || data.choices?.[0]?.message?.content || data.content || JSON.stringify(data);
|
|
890
|
+
const tokensUsed = data.usage?.total_tokens || null;
|
|
891
|
+
const answerLength = typeof answer === "string" ? answer.length : String(answer).length;
|
|
892
|
+
console.log(`[FRIDA] ========================================`);
|
|
893
|
+
console.log(`[FRIDA] \u2705 FRIDA AI RESPONSE RECEIVED`);
|
|
894
|
+
console.log(`[FRIDA] ========================================`);
|
|
895
|
+
console.log(`[FRIDA] \u{1F4CA} TOKENS USED: ${tokensUsed || "N/A"}`);
|
|
896
|
+
console.log(`[FRIDA] \u{1F4DD} Answer length: ${answerLength} chars`);
|
|
897
|
+
console.log(`[FRIDA] \u23F1\uFE0F Total time: ${fetchTime}ms`);
|
|
898
|
+
console.log(`[FRIDA] ========================================
|
|
899
|
+
`);
|
|
900
|
+
return {
|
|
901
|
+
success: true,
|
|
902
|
+
error: null,
|
|
903
|
+
response: typeof answer === "string" ? answer : String(answer),
|
|
904
|
+
metadata: {
|
|
905
|
+
model: this.model,
|
|
906
|
+
tokensUsed
|
|
907
|
+
}
|
|
908
|
+
};
|
|
909
|
+
} catch (error) {
|
|
910
|
+
console.error(`[FRIDA] Error calling Frida AI:`, error instanceof Error ? error.message : String(error));
|
|
911
|
+
return {
|
|
912
|
+
success: false,
|
|
913
|
+
error: error instanceof Error ? error.message : String(error),
|
|
914
|
+
response: null
|
|
915
|
+
};
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
};
|
|
919
|
+
|
|
920
|
+
// src/figma2frida/utils/formatters.ts
|
|
921
|
+
var Formatters = class {
|
|
922
|
+
/**
|
|
923
|
+
* Formats results in structured format (JSON-friendly)
|
|
924
|
+
*/
|
|
925
|
+
static toStructured(searchResults) {
|
|
926
|
+
return {
|
|
927
|
+
query: searchResults.query,
|
|
928
|
+
summary: {
|
|
929
|
+
totalMatches: searchResults.totalMatches,
|
|
930
|
+
relevantMatches: searchResults.relevantMatches,
|
|
931
|
+
minScore: searchResults.searchMetadata.minScore
|
|
932
|
+
},
|
|
933
|
+
components: searchResults.matches.map((match) => ({
|
|
934
|
+
tag: match.tag,
|
|
935
|
+
score: match.score,
|
|
936
|
+
category: match.category,
|
|
937
|
+
description: match.description,
|
|
938
|
+
keywords: match.keywords || "",
|
|
939
|
+
props: match.props,
|
|
940
|
+
events: match.events
|
|
941
|
+
})),
|
|
942
|
+
lowScoreComponents: searchResults.lowScoreMatches || []
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
/**
|
|
946
|
+
* Formats results in readable markdown format
|
|
947
|
+
*/
|
|
948
|
+
static toMarkdown(searchResults) {
|
|
949
|
+
if (!searchResults.matches || searchResults.matches.length === 0) {
|
|
950
|
+
return `## No similar components found
|
|
951
|
+
|
|
952
|
+
**Query:** "${searchResults.query}"
|
|
953
|
+
|
|
954
|
+
Suggestion: Try more general terms or verify that components are indexed.`;
|
|
955
|
+
}
|
|
956
|
+
let markdown = `## Search Results
|
|
957
|
+
|
|
958
|
+
**Query:** "${searchResults.query}"
|
|
959
|
+
**Components found:** ${searchResults.relevantMatches} of ${searchResults.totalMatches}
|
|
960
|
+
**Minimum score:** ${searchResults.searchMetadata.minScore}
|
|
961
|
+
|
|
962
|
+
---
|
|
963
|
+
|
|
964
|
+
`;
|
|
965
|
+
searchResults.matches.forEach((match, index) => {
|
|
966
|
+
markdown += `### ${index + 1}. ${match.tag} (Score: ${match.score.toFixed(4)})
|
|
967
|
+
|
|
968
|
+
**Category:** ${match.category}
|
|
969
|
+
|
|
970
|
+
**Description:**
|
|
971
|
+
${match.description || "No description available"}
|
|
972
|
+
|
|
973
|
+
**Keywords:** ${match.keywords || "N/A"}
|
|
974
|
+
|
|
975
|
+
`;
|
|
976
|
+
if (match.props && match.props.length > 0) {
|
|
977
|
+
markdown += `**Properties (${match.props.length}):**
|
|
978
|
+
|
|
979
|
+
`;
|
|
980
|
+
match.props.forEach((prop, idx) => {
|
|
981
|
+
markdown += `${idx + 1}. \`${prop.name}\`
|
|
982
|
+
`;
|
|
983
|
+
markdown += ` - **Type:** \`${prop.type_signature}\`
|
|
984
|
+
`;
|
|
985
|
+
if (prop.default_value !== void 0) {
|
|
986
|
+
markdown += ` - **Default:** \`${prop.default_value}\`
|
|
987
|
+
`;
|
|
988
|
+
}
|
|
989
|
+
if (prop.description) {
|
|
990
|
+
markdown += ` - **Documentation:** ${prop.description}
|
|
991
|
+
`;
|
|
992
|
+
}
|
|
993
|
+
markdown += "\n";
|
|
994
|
+
});
|
|
995
|
+
} else {
|
|
996
|
+
markdown += `**Properties:** None
|
|
997
|
+
|
|
998
|
+
`;
|
|
999
|
+
}
|
|
1000
|
+
if (match.events && match.events.length > 0) {
|
|
1001
|
+
markdown += `**Events (${match.events.length}):**
|
|
1002
|
+
|
|
1003
|
+
`;
|
|
1004
|
+
match.events.forEach((event, idx) => {
|
|
1005
|
+
markdown += `${idx + 1}. \`${event.name}\`
|
|
1006
|
+
`;
|
|
1007
|
+
markdown += ` - **Payload type:** ${event.payload_type}
|
|
1008
|
+
`;
|
|
1009
|
+
if (event.description) {
|
|
1010
|
+
markdown += ` - **Documentation:** ${event.description}
|
|
1011
|
+
`;
|
|
1012
|
+
}
|
|
1013
|
+
markdown += "\n";
|
|
1014
|
+
});
|
|
1015
|
+
} else {
|
|
1016
|
+
markdown += `**Events:** None
|
|
1017
|
+
|
|
1018
|
+
`;
|
|
1019
|
+
}
|
|
1020
|
+
markdown += "---\n\n";
|
|
1021
|
+
});
|
|
1022
|
+
if (searchResults.lowScoreMatches && searchResults.lowScoreMatches.length > 0) {
|
|
1023
|
+
markdown += `
|
|
1024
|
+
### Components with low relevance (score < ${searchResults.searchMetadata.minScore})
|
|
1025
|
+
|
|
1026
|
+
`;
|
|
1027
|
+
searchResults.lowScoreMatches.forEach((match, idx) => {
|
|
1028
|
+
markdown += `${idx + 1}. ${match.tag} (Score: ${match.score.toFixed(4)})
|
|
1029
|
+
`;
|
|
1030
|
+
});
|
|
1031
|
+
}
|
|
1032
|
+
return markdown;
|
|
1033
|
+
}
|
|
1034
|
+
/**
|
|
1035
|
+
* Formats an individual component in context for prompts
|
|
1036
|
+
* Returns JSON-structured format for better LLM understanding
|
|
1037
|
+
*/
|
|
1038
|
+
static toContextFormat(match) {
|
|
1039
|
+
const componentJson = {
|
|
1040
|
+
tag: match.tag,
|
|
1041
|
+
readme: match.description,
|
|
1042
|
+
props: match.props.map((p) => ({
|
|
1043
|
+
name: p.name,
|
|
1044
|
+
type: p.type_signature,
|
|
1045
|
+
default_value: p.default_value,
|
|
1046
|
+
docs: p.description || ""
|
|
1047
|
+
})),
|
|
1048
|
+
events: match.events.map((e) => ({
|
|
1049
|
+
name: e.name,
|
|
1050
|
+
payload_type: e.payload_type,
|
|
1051
|
+
docs: e.description || ""
|
|
1052
|
+
})),
|
|
1053
|
+
// Include new fields if available
|
|
1054
|
+
...match.usage_examples && match.usage_examples.length > 0 ? { usage_examples: match.usage_examples } : {},
|
|
1055
|
+
...match.methods && match.methods.length > 0 ? { methods: match.methods } : {},
|
|
1056
|
+
...match.slots && match.slots.length > 0 ? { slots: match.slots } : {}
|
|
1057
|
+
};
|
|
1058
|
+
const jsonStr = JSON.stringify(componentJson, null, 2);
|
|
1059
|
+
return `=== COMPONENT: ${match.tag || "N/A"} ===
|
|
1060
|
+
${match.human_name ? `HUMAN NAME: ${match.human_name}
|
|
1061
|
+
` : ""}CATEGORY: ${match.category || "General"}
|
|
1062
|
+
RELEVANCE SCORE: ${match.score.toFixed(4)}
|
|
1063
|
+
|
|
1064
|
+
DESCRIPTION:
|
|
1065
|
+
${match.description || "No description available"}
|
|
1066
|
+
|
|
1067
|
+
${match.full_readme ? `FULL README:
|
|
1068
|
+
${match.full_readme}
|
|
1069
|
+
|
|
1070
|
+
` : ""}${match.keywords ? `KEYWORDS: ${match.keywords}
|
|
1071
|
+
|
|
1072
|
+
` : ""}PROPERTIES (${match.props?.length || 0}):
|
|
1073
|
+
${match.props && match.props.length > 0 ? match.props.map((p, idx) => {
|
|
1074
|
+
return `${idx + 1}. \`${p.name}\`
|
|
1075
|
+
Type: \`${p.type_signature}\`
|
|
1076
|
+
${p.default_value !== void 0 ? `Default: \`${p.default_value}\`
|
|
1077
|
+
` : ""}Documentation: ${p.description || "No documentation"}`;
|
|
1078
|
+
}).join("\n\n") : "None"}
|
|
1079
|
+
|
|
1080
|
+
EVENTS (${match.events?.length || 0}):
|
|
1081
|
+
${match.events && match.events.length > 0 ? match.events.map((e, idx) => {
|
|
1082
|
+
return `${idx + 1}. ${e.name}
|
|
1083
|
+
Payload type: ${e.payload_type}
|
|
1084
|
+
Documentation: ${e.description || "No documentation"}`;
|
|
1085
|
+
}).join("\n\n") : "None"}
|
|
1086
|
+
|
|
1087
|
+
${match.methods && match.methods.length > 0 ? `METHODS (${match.methods.length}):
|
|
1088
|
+
${match.methods.map((m, idx) => {
|
|
1089
|
+
return `${idx + 1}. ${m.name}(${m.parameters})
|
|
1090
|
+
Documentation: ${m.description || "No documentation"}`;
|
|
1091
|
+
}).join("\n\n")}
|
|
1092
|
+
|
|
1093
|
+
` : ""}${match.usage_examples && match.usage_examples.length > 0 ? `USAGE EXAMPLES:
|
|
1094
|
+
${match.usage_examples.map((ex, idx) => {
|
|
1095
|
+
return `${idx + 1}. ${ex.title}
|
|
1096
|
+
${ex.description}
|
|
1097
|
+
Template: ${ex.code.template}
|
|
1098
|
+
${ex.code.script ? `Script: ${ex.code.script}` : ""}`;
|
|
1099
|
+
}).join("\n\n")}
|
|
1100
|
+
|
|
1101
|
+
` : ""}COMPONENT JSON STRUCTURE (for reference):
|
|
1102
|
+
\`\`\`json
|
|
1103
|
+
${jsonStr}
|
|
1104
|
+
\`\`\`
|
|
1105
|
+
|
|
1106
|
+
---`;
|
|
1107
|
+
}
|
|
1108
|
+
/**
|
|
1109
|
+
* Formats components as JSON array for component documentation
|
|
1110
|
+
* This format worked well with Copilot
|
|
1111
|
+
*/
|
|
1112
|
+
static toJSONContext(matches) {
|
|
1113
|
+
if (!matches || matches.length === 0) {
|
|
1114
|
+
return "No components found.";
|
|
1115
|
+
}
|
|
1116
|
+
const componentsArray = matches.map((match) => ({
|
|
1117
|
+
tag: match.tag,
|
|
1118
|
+
readme: match.description,
|
|
1119
|
+
props: match.props.map((p) => ({
|
|
1120
|
+
name: p.name,
|
|
1121
|
+
type: p.type_signature,
|
|
1122
|
+
default_value: p.default_value,
|
|
1123
|
+
docs: p.description || ""
|
|
1124
|
+
})),
|
|
1125
|
+
events: match.events.map((e) => ({
|
|
1126
|
+
name: e.name,
|
|
1127
|
+
payload_type: e.payload_type,
|
|
1128
|
+
docs: e.description || ""
|
|
1129
|
+
}))
|
|
1130
|
+
}));
|
|
1131
|
+
return JSON.stringify({ components: componentsArray }, null, 2);
|
|
1132
|
+
}
|
|
1133
|
+
/**
|
|
1134
|
+
* Formats multiple components for context in prompts
|
|
1135
|
+
* Uses JSON format for better LLM understanding
|
|
1136
|
+
*/
|
|
1137
|
+
static matchesToContext(matches) {
|
|
1138
|
+
if (!matches || matches.length === 0) {
|
|
1139
|
+
return "No components found.";
|
|
1140
|
+
}
|
|
1141
|
+
const componentsArray = matches.map((match) => ({
|
|
1142
|
+
tag: match.tag,
|
|
1143
|
+
readme: match.description,
|
|
1144
|
+
category: match.category,
|
|
1145
|
+
keywords: match.keywords,
|
|
1146
|
+
props: match.props.map((p) => ({
|
|
1147
|
+
name: p.name,
|
|
1148
|
+
type: p.type_signature,
|
|
1149
|
+
default_value: p.default_value,
|
|
1150
|
+
docs: p.description || ""
|
|
1151
|
+
})),
|
|
1152
|
+
events: match.events.map((e) => ({
|
|
1153
|
+
name: e.name,
|
|
1154
|
+
payload_type: e.payload_type,
|
|
1155
|
+
docs: e.description || ""
|
|
1156
|
+
})),
|
|
1157
|
+
...match.usage_examples && match.usage_examples.length > 0 ? { usage_examples: match.usage_examples } : {},
|
|
1158
|
+
...match.methods && match.methods.length > 0 ? { methods: match.methods } : {}
|
|
1159
|
+
}));
|
|
1160
|
+
return JSON.stringify({ components: componentsArray }, null, 2);
|
|
1161
|
+
}
|
|
1162
|
+
/**
|
|
1163
|
+
* Formats results in pure JSON format
|
|
1164
|
+
*/
|
|
1165
|
+
static toJSON(searchResults) {
|
|
1166
|
+
return JSON.stringify(this.toStructured(searchResults), null, 2);
|
|
1167
|
+
}
|
|
1168
|
+
/**
|
|
1169
|
+
* Formats results according to the requested format
|
|
1170
|
+
*/
|
|
1171
|
+
static format(searchResults, format = "structured") {
|
|
1172
|
+
switch (format.toLowerCase()) {
|
|
1173
|
+
case "json":
|
|
1174
|
+
return this.toJSON(searchResults);
|
|
1175
|
+
case "markdown":
|
|
1176
|
+
case "md":
|
|
1177
|
+
return this.toMarkdown(searchResults);
|
|
1178
|
+
case "structured":
|
|
1179
|
+
return this.toStructured(searchResults);
|
|
1180
|
+
case "context":
|
|
1181
|
+
return this.matchesToContext(searchResults.matches || []);
|
|
1182
|
+
default:
|
|
1183
|
+
return this.toStructured(searchResults);
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
};
|
|
1187
|
+
|
|
1188
|
+
// src/figma2frida/utils/design-query-extractor.ts
|
|
1189
|
+
var DesignQueryExtractor = class {
|
|
1190
|
+
strategies;
|
|
1191
|
+
constructor() {
|
|
1192
|
+
this.strategies = [
|
|
1193
|
+
new TextExtractionStrategy(),
|
|
1194
|
+
new PatternMatchingStrategy(),
|
|
1195
|
+
new MetadataExtractionStrategy()
|
|
1196
|
+
];
|
|
1197
|
+
}
|
|
1198
|
+
/**
|
|
1199
|
+
* Extracts query from design context using all available strategies
|
|
1200
|
+
*/
|
|
1201
|
+
extractQuery(designContext, fallbackQuery = "UI component") {
|
|
1202
|
+
const queries = [];
|
|
1203
|
+
for (const strategy of this.strategies) {
|
|
1204
|
+
try {
|
|
1205
|
+
const extracted = strategy.extract(designContext);
|
|
1206
|
+
queries.push(...extracted);
|
|
1207
|
+
} catch (error) {
|
|
1208
|
+
console.warn(
|
|
1209
|
+
`[QUERY EXTRACTOR] Strategy ${strategy.name} failed:`,
|
|
1210
|
+
error instanceof Error ? error.message : String(error)
|
|
1211
|
+
);
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
const uniqueQueries = Array.from(
|
|
1215
|
+
new Set(queries.filter((q) => q && q.trim().length > 0))
|
|
1216
|
+
);
|
|
1217
|
+
if (uniqueQueries.length > 0) {
|
|
1218
|
+
const topQueries = uniqueQueries.slice(0, 3);
|
|
1219
|
+
return `${topQueries.join(" ")} component`.trim();
|
|
1220
|
+
}
|
|
1221
|
+
return fallbackQuery;
|
|
1222
|
+
}
|
|
1223
|
+
/**
|
|
1224
|
+
* Extracts multiple candidate queries for experimentation
|
|
1225
|
+
*/
|
|
1226
|
+
extractMultipleQueries(designContext) {
|
|
1227
|
+
const allQueries = [];
|
|
1228
|
+
for (const strategy of this.strategies) {
|
|
1229
|
+
try {
|
|
1230
|
+
const extracted = strategy.extract(designContext);
|
|
1231
|
+
allQueries.push(...extracted);
|
|
1232
|
+
} catch {
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
return Array.from(
|
|
1236
|
+
new Set(allQueries.filter((q) => q && q.trim().length > 0))
|
|
1237
|
+
);
|
|
1238
|
+
}
|
|
1239
|
+
};
|
|
1240
|
+
var TextExtractionStrategy = class {
|
|
1241
|
+
name = "text-extraction";
|
|
1242
|
+
extract(context) {
|
|
1243
|
+
const queries = [];
|
|
1244
|
+
if (!context.content) {
|
|
1245
|
+
return queries;
|
|
1246
|
+
}
|
|
1247
|
+
for (const item of context.content) {
|
|
1248
|
+
if (item.type === "text" && typeof item.text === "string") {
|
|
1249
|
+
const text = item.text.trim();
|
|
1250
|
+
if (text.length > 0) {
|
|
1251
|
+
const componentKeywords = this.extractComponentKeywords(text);
|
|
1252
|
+
queries.push(...componentKeywords);
|
|
1253
|
+
const descriptivePhrases = this.extractDescriptivePhrases(text);
|
|
1254
|
+
queries.push(...descriptivePhrases);
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
return queries;
|
|
1259
|
+
}
|
|
1260
|
+
extractComponentKeywords(text) {
|
|
1261
|
+
const keywords = [];
|
|
1262
|
+
const patterns = [
|
|
1263
|
+
{ pattern: /button|btn/gi, keyword: "button" },
|
|
1264
|
+
{ pattern: /input|textfield|text field/gi, keyword: "input" },
|
|
1265
|
+
{ pattern: /card|panel/gi, keyword: "card" },
|
|
1266
|
+
{ pattern: /badge|label|tag/gi, keyword: "badge" },
|
|
1267
|
+
{ pattern: /alert|message|notification/gi, keyword: "alert" },
|
|
1268
|
+
{ pattern: /modal|dialog|popup/gi, keyword: "modal" },
|
|
1269
|
+
{ pattern: /dropdown|select|picker/gi, keyword: "dropdown" },
|
|
1270
|
+
{ pattern: /checkbox|check box/gi, keyword: "checkbox" },
|
|
1271
|
+
{ pattern: /radio|radio button/gi, keyword: "radio" },
|
|
1272
|
+
{ pattern: /slider|range/gi, keyword: "slider" },
|
|
1273
|
+
{ pattern: /table|grid|list/gi, keyword: "table" },
|
|
1274
|
+
{ pattern: /breadcrumb|breadcrumb navigation/gi, keyword: "breadcrumb" },
|
|
1275
|
+
{ pattern: /tooltip|tip/gi, keyword: "tooltip" },
|
|
1276
|
+
{ pattern: /accordion|collapsible/gi, keyword: "accordion" },
|
|
1277
|
+
{ pattern: /progress|loader|spinner/gi, keyword: "progress" }
|
|
1278
|
+
];
|
|
1279
|
+
for (const { pattern, keyword } of patterns) {
|
|
1280
|
+
if (pattern.test(text)) {
|
|
1281
|
+
keywords.push(keyword);
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
return keywords;
|
|
1285
|
+
}
|
|
1286
|
+
extractDescriptivePhrases(text) {
|
|
1287
|
+
const phrases = [];
|
|
1288
|
+
const descriptionPatterns = [
|
|
1289
|
+
/(?:a |an |the )?([a-z]+(?: [a-z]+){0,3}) (?:that |for |to |with |in |on )/gi,
|
|
1290
|
+
/(?:create |make |add |use |show |display )([a-z]+(?: [a-z]+){0,3})/gi
|
|
1291
|
+
];
|
|
1292
|
+
for (const pattern of descriptionPatterns) {
|
|
1293
|
+
const matches = text.matchAll(pattern);
|
|
1294
|
+
for (const match of matches) {
|
|
1295
|
+
if (match[1] && match[1].length > 2) {
|
|
1296
|
+
phrases.push(match[1].trim());
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
return phrases.slice(0, 5);
|
|
1301
|
+
}
|
|
1302
|
+
};
|
|
1303
|
+
var PatternMatchingStrategy = class {
|
|
1304
|
+
name = "pattern-matching";
|
|
1305
|
+
extract(context) {
|
|
1306
|
+
const queries = [];
|
|
1307
|
+
if (!context.content) {
|
|
1308
|
+
return queries;
|
|
1309
|
+
}
|
|
1310
|
+
const combinedText = context.content.filter((item) => item.type === "text" && typeof item.text === "string").map((item) => item.text).join(" ");
|
|
1311
|
+
if (combinedText.length === 0) {
|
|
1312
|
+
return queries;
|
|
1313
|
+
}
|
|
1314
|
+
const elementTypes = [];
|
|
1315
|
+
if (/<(?:button|Button)[^>]*>/gi.test(combinedText)) {
|
|
1316
|
+
elementTypes.push("button");
|
|
1317
|
+
}
|
|
1318
|
+
if (/<(?:input|Input)[^>]*>/gi.test(combinedText)) {
|
|
1319
|
+
elementTypes.push("input");
|
|
1320
|
+
if (/type=["'](?:text|email|password|number)/gi.test(combinedText)) {
|
|
1321
|
+
elementTypes.push("text input");
|
|
1322
|
+
}
|
|
1323
|
+
if (/type=["']checkbox/gi.test(combinedText)) {
|
|
1324
|
+
elementTypes.push("checkbox");
|
|
1325
|
+
}
|
|
1326
|
+
if (/type=["']radio/gi.test(combinedText)) {
|
|
1327
|
+
elementTypes.push("radio");
|
|
1328
|
+
}
|
|
1329
|
+
if (/type=["']range/gi.test(combinedText)) {
|
|
1330
|
+
elementTypes.push("slider");
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
if (/<(?:textarea|Textarea)[^>]*>/gi.test(combinedText)) {
|
|
1334
|
+
elementTypes.push("textarea");
|
|
1335
|
+
}
|
|
1336
|
+
if (/<(?:select|Select)[^>]*>/gi.test(combinedText)) {
|
|
1337
|
+
elementTypes.push("dropdown");
|
|
1338
|
+
elementTypes.push("select");
|
|
1339
|
+
}
|
|
1340
|
+
if (/<(?:div|Div)[^>]*class[^>]*(?:card|Card|panel|Panel)/gi.test(combinedText)) {
|
|
1341
|
+
elementTypes.push("card");
|
|
1342
|
+
}
|
|
1343
|
+
if (/<(?:span|Span)[^>]*class[^>]*(?:badge|Badge|label|Label|tag|Tag)/gi.test(combinedText)) {
|
|
1344
|
+
elementTypes.push("badge");
|
|
1345
|
+
}
|
|
1346
|
+
if (/<(?:div|Div)[^>]*class[^>]*(?:alert|Alert|message|Message|notification|Notification)/gi.test(combinedText)) {
|
|
1347
|
+
elementTypes.push("alert");
|
|
1348
|
+
}
|
|
1349
|
+
if (/<(?:form|Form)[^>]*>/gi.test(combinedText)) {
|
|
1350
|
+
elementTypes.push("form");
|
|
1351
|
+
}
|
|
1352
|
+
if (/<(?:nav|Nav|ul|Ul)[^>]*class[^>]*(?:nav|Nav|menu|Menu|breadcrumb|Breadcrumb)/gi.test(combinedText)) {
|
|
1353
|
+
elementTypes.push("navigation");
|
|
1354
|
+
}
|
|
1355
|
+
if (/<(?:a|A)[^>]*href/gi.test(combinedText)) {
|
|
1356
|
+
elementTypes.push("link");
|
|
1357
|
+
elementTypes.push("action link");
|
|
1358
|
+
}
|
|
1359
|
+
for (const elementType of elementTypes) {
|
|
1360
|
+
queries.push(`${elementType} component`);
|
|
1361
|
+
}
|
|
1362
|
+
if (elementTypes.includes("button")) {
|
|
1363
|
+
queries.push("button Buttons category");
|
|
1364
|
+
}
|
|
1365
|
+
if (elementTypes.some((e) => e.includes("input") || e.includes("select") || e.includes("textarea"))) {
|
|
1366
|
+
queries.push("form Forms category");
|
|
1367
|
+
}
|
|
1368
|
+
if (elementTypes.includes("alert") || elementTypes.includes("badge")) {
|
|
1369
|
+
queries.push("feedback Feedback category");
|
|
1370
|
+
}
|
|
1371
|
+
return queries;
|
|
1372
|
+
}
|
|
1373
|
+
};
|
|
1374
|
+
var MetadataExtractionStrategy = class {
|
|
1375
|
+
name = "metadata-extraction";
|
|
1376
|
+
extract(context) {
|
|
1377
|
+
const queries = [];
|
|
1378
|
+
if (!context.content) {
|
|
1379
|
+
return queries;
|
|
1380
|
+
}
|
|
1381
|
+
const componentPattern = /<([a-z]+-[\w-]+)/gi;
|
|
1382
|
+
for (const item of context.content) {
|
|
1383
|
+
if (item.type === "text" && typeof item.text === "string") {
|
|
1384
|
+
const matches = item.text.matchAll(componentPattern);
|
|
1385
|
+
for (const match of matches) {
|
|
1386
|
+
const componentName = match[2];
|
|
1387
|
+
queries.push(componentName.replace(/-/g, " "));
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
return queries;
|
|
1392
|
+
}
|
|
1393
|
+
};
|
|
1394
|
+
|
|
1395
|
+
// src/figma2frida/handlers/component-suggestion-handler.ts
|
|
1396
|
+
var componentSuggestionsCache = /* @__PURE__ */ new Map();
|
|
1397
|
+
var COMPONENT_CACHE_TTL_MS = 3e5;
|
|
1398
|
+
var pineconeService = null;
|
|
1399
|
+
var pineconeServiceNamespace = void 0;
|
|
1400
|
+
var fridaClient = null;
|
|
1401
|
+
var queryExtractor = null;
|
|
1402
|
+
function getPineconeService(namespace) {
|
|
1403
|
+
if (!pineconeService || pineconeServiceNamespace !== namespace) {
|
|
1404
|
+
pineconeService = new PineconeSearchService({ namespace });
|
|
1405
|
+
pineconeServiceNamespace = namespace;
|
|
1406
|
+
}
|
|
1407
|
+
return pineconeService;
|
|
1408
|
+
}
|
|
1409
|
+
function getFridaClient() {
|
|
1410
|
+
if (!fridaClient) {
|
|
1411
|
+
fridaClient = new FridaClient();
|
|
1412
|
+
}
|
|
1413
|
+
return fridaClient;
|
|
1414
|
+
}
|
|
1415
|
+
function getQueryExtractor() {
|
|
1416
|
+
if (!queryExtractor) {
|
|
1417
|
+
queryExtractor = new DesignQueryExtractor();
|
|
1418
|
+
}
|
|
1419
|
+
return queryExtractor;
|
|
1420
|
+
}
|
|
1421
|
+
async function handleComponentSuggestion(options) {
|
|
1422
|
+
const {
|
|
1423
|
+
nodeId,
|
|
1424
|
+
minScore = 0.05,
|
|
1425
|
+
topK = 10,
|
|
1426
|
+
useFrida = true,
|
|
1427
|
+
query: customQuery,
|
|
1428
|
+
namespace,
|
|
1429
|
+
designContext,
|
|
1430
|
+
streamContent
|
|
1431
|
+
} = options;
|
|
1432
|
+
try {
|
|
1433
|
+
const cacheKey = `${nodeId || "current"}:${minScore}:${topK}:${useFrida}:${customQuery || "auto"}`;
|
|
1434
|
+
const now = Date.now();
|
|
1435
|
+
const cached = componentSuggestionsCache.get(cacheKey);
|
|
1436
|
+
if (cached && now - cached.timestamp < COMPONENT_CACHE_TTL_MS) {
|
|
1437
|
+
console.log(
|
|
1438
|
+
`[COMPONENT SUGGEST] Using cached result (age: ${((now - cached.timestamp) / 1e3).toFixed(1)}s)`
|
|
1439
|
+
);
|
|
1440
|
+
if (streamContent) {
|
|
1441
|
+
await streamContent({
|
|
1442
|
+
type: "text",
|
|
1443
|
+
text: `\u26A1 Using cached component suggestions (${((now - cached.timestamp) / 1e3).toFixed(1)}s old)`
|
|
1444
|
+
});
|
|
1445
|
+
}
|
|
1446
|
+
return { content: cached.data };
|
|
1447
|
+
}
|
|
1448
|
+
if (!designContext.content || designContext.content.length === 0) {
|
|
1449
|
+
return {
|
|
1450
|
+
content: [
|
|
1451
|
+
{
|
|
1452
|
+
type: "text",
|
|
1453
|
+
text: `\u274C No design context returned from Figma MCP.
|
|
1454
|
+
|
|
1455
|
+
**Node ID:** ${nodeId || "current selection"}
|
|
1456
|
+
|
|
1457
|
+
Cannot suggest components without design context.`
|
|
1458
|
+
}
|
|
1459
|
+
]
|
|
1460
|
+
};
|
|
1461
|
+
}
|
|
1462
|
+
if (streamContent) {
|
|
1463
|
+
await streamContent({
|
|
1464
|
+
type: "text",
|
|
1465
|
+
text: "\u{1F50D} Extracting semantic query from design..."
|
|
1466
|
+
});
|
|
1467
|
+
}
|
|
1468
|
+
const extractor = getQueryExtractor();
|
|
1469
|
+
const searchQuery = customQuery || extractor.extractQuery(designContext);
|
|
1470
|
+
console.log(`[COMPONENT SUGGEST] Extracted query: "${searchQuery}"`);
|
|
1471
|
+
if (streamContent) {
|
|
1472
|
+
await streamContent({
|
|
1473
|
+
type: "text",
|
|
1474
|
+
text: `\u{1F50E} Searching Pinecone for: "${searchQuery}"...`
|
|
1475
|
+
});
|
|
1476
|
+
}
|
|
1477
|
+
console.log(`[COMPONENT SUGGEST] Calling Pinecone search service...`);
|
|
1478
|
+
const searchService = getPineconeService(namespace);
|
|
1479
|
+
const searchStartTime = Date.now();
|
|
1480
|
+
const searchResults = await searchService.search(searchQuery, {
|
|
1481
|
+
minScore,
|
|
1482
|
+
topK
|
|
1483
|
+
});
|
|
1484
|
+
const searchTime = Date.now() - searchStartTime;
|
|
1485
|
+
console.log(
|
|
1486
|
+
`[COMPONENT SUGGEST] Pinecone search completed in ${searchTime}ms - Found ${searchResults.relevantMatches} relevant components`
|
|
1487
|
+
);
|
|
1488
|
+
if (searchResults.matches.length === 0) {
|
|
1489
|
+
const namespaceInfo = searchResults.searchMetadata.namespace ? `
|
|
1490
|
+
**Namespace:** ${searchResults.searchMetadata.namespace}` : "";
|
|
1491
|
+
return {
|
|
1492
|
+
content: [
|
|
1493
|
+
{
|
|
1494
|
+
type: "text",
|
|
1495
|
+
text: `\u26A0\uFE0F **No Components Found**
|
|
1496
|
+
|
|
1497
|
+
**Query:** "${searchQuery}"
|
|
1498
|
+
**Min Score:** ${minScore}${namespaceInfo}
|
|
1499
|
+
**Index:** ${searchResults.searchMetadata.indexName}
|
|
1500
|
+
|
|
1501
|
+
No components from your design system matched the design context with sufficient relevance.
|
|
1502
|
+
|
|
1503
|
+
**Possible Causes:**
|
|
1504
|
+
- The namespace "${searchResults.searchMetadata.namespace || "default"}" may not exist or may be empty
|
|
1505
|
+
- Components may not be indexed in this namespace
|
|
1506
|
+
- The query may not match any components in this namespace
|
|
1507
|
+
|
|
1508
|
+
**Suggestions:**
|
|
1509
|
+
- Verify that components are indexed in Pinecone${searchResults.searchMetadata.namespace ? ` namespace "${searchResults.searchMetadata.namespace}"` : ""}
|
|
1510
|
+
- Set namespace via \`--pinecone-namespace\` CLI argument if not set
|
|
1511
|
+
- Try lowering the \`minScore\` parameter
|
|
1512
|
+
- Provide a more specific \`query\` parameter`
|
|
1513
|
+
}
|
|
1514
|
+
]
|
|
1515
|
+
};
|
|
1516
|
+
}
|
|
1517
|
+
const componentTags = searchResults.matches.map((m) => m.tag).join(", ");
|
|
1518
|
+
const frameworkType = searchResults.matches[0]?.framework_type || "Web Components";
|
|
1519
|
+
const prefixPatterns = Array.from(
|
|
1520
|
+
new Set(
|
|
1521
|
+
searchResults.matches.map((m) => {
|
|
1522
|
+
const match = m.tag.match(/^([a-z]+-)/);
|
|
1523
|
+
return match ? match[1] : null;
|
|
1524
|
+
}).filter(Boolean)
|
|
1525
|
+
)
|
|
1526
|
+
).join(", ");
|
|
1527
|
+
const content = [
|
|
1528
|
+
{
|
|
1529
|
+
type: "text",
|
|
1530
|
+
text: `\u{1F6A8} **MANDATORY: USE ONLY COMPONENTS FROM YOUR DESIGN SYSTEM**
|
|
1531
|
+
|
|
1532
|
+
**CRITICAL INSTRUCTIONS:**
|
|
1533
|
+
- You MUST use ONLY the components listed below from your organization's design system
|
|
1534
|
+
- **DO NOT** use components from other libraries unless they appear in the search results
|
|
1535
|
+
- **DO NOT** use generic HTML elements directly - use the design system components instead
|
|
1536
|
+
${prefixPatterns ? `- Component prefixes in your system: ${prefixPatterns}` : ""}
|
|
1537
|
+
${frameworkType ? `- Framework type: ${frameworkType}` : ""}
|
|
1538
|
+
|
|
1539
|
+
**Available Components for this design:**
|
|
1540
|
+
${componentTags || "No components found"}
|
|
1541
|
+
|
|
1542
|
+
---
|
|
1543
|
+
|
|
1544
|
+
\u2705 **Component Suggestions**
|
|
1545
|
+
|
|
1546
|
+
**Query:** "${searchQuery}"
|
|
1547
|
+
**Components found:** ${searchResults.relevantMatches} of ${searchResults.totalMatches}
|
|
1548
|
+
**Min Score:** ${minScore}
|
|
1549
|
+
|
|
1550
|
+
---
|
|
1551
|
+
|
|
1552
|
+
`
|
|
1553
|
+
}
|
|
1554
|
+
];
|
|
1555
|
+
content.push({
|
|
1556
|
+
type: "text",
|
|
1557
|
+
text: `## \u{1F4CB} MANDATORY USAGE INSTRUCTIONS
|
|
1558
|
+
|
|
1559
|
+
**You MUST follow these rules when generating code:**
|
|
1560
|
+
|
|
1561
|
+
1. **Use ONLY the components from the search results below**
|
|
1562
|
+
2. **DO NOT use:**
|
|
1563
|
+
- \u274C Components from other libraries not listed in the results
|
|
1564
|
+
- \u274C Generic HTML elements unless the component wraps them
|
|
1565
|
+
- \u274C Any component not found in your organization's design system
|
|
1566
|
+
|
|
1567
|
+
3. **Use the recommended components:**
|
|
1568
|
+
- \u2705 Use ONLY the components listed in the search results below
|
|
1569
|
+
- \u2705 Follow the import statements and usage examples provided
|
|
1570
|
+
- \u2705 Respect the component's props, events, and methods as documented
|
|
1571
|
+
- \u2705 If a component is not in the list below, do NOT use it
|
|
1572
|
+
|
|
1573
|
+
4. **Framework compatibility:** Check the \`framework_type\` and \`import_statement\` fields for each component
|
|
1574
|
+
|
|
1575
|
+
---
|
|
1576
|
+
|
|
1577
|
+
## Search Results (${searchResults.relevantMatches} components)
|
|
1578
|
+
|
|
1579
|
+
${Formatters.toMarkdown(searchResults)}
|
|
1580
|
+
|
|
1581
|
+
---
|
|
1582
|
+
|
|
1583
|
+
`
|
|
1584
|
+
});
|
|
1585
|
+
if (useFrida) {
|
|
1586
|
+
const frida = getFridaClient();
|
|
1587
|
+
if (frida.isEnabled()) {
|
|
1588
|
+
if (streamContent) {
|
|
1589
|
+
await streamContent({
|
|
1590
|
+
type: "text",
|
|
1591
|
+
text: "\u{1F916} Processing with Frida AI for intelligent recommendations..."
|
|
1592
|
+
});
|
|
1593
|
+
}
|
|
1594
|
+
const contextForFrida = Formatters.matchesToContext(
|
|
1595
|
+
searchResults.matches
|
|
1596
|
+
);
|
|
1597
|
+
const question = `Based on this Figma design context, which component(s) from the design system should I use? Design description: ${searchQuery}`;
|
|
1598
|
+
console.log(`[COMPONENT SUGGEST] Calling Frida AI with ${searchResults.matches.length} component matches...`);
|
|
1599
|
+
const fridaStartTime = Date.now();
|
|
1600
|
+
const fridaResponse = await frida.ask(question, contextForFrida);
|
|
1601
|
+
const fridaTime = Date.now() - fridaStartTime;
|
|
1602
|
+
console.log(`[COMPONENT SUGGEST] Frida AI completed in ${fridaTime}ms - Success: ${fridaResponse.success}`);
|
|
1603
|
+
if (fridaResponse.success && fridaResponse.response) {
|
|
1604
|
+
content.push({
|
|
1605
|
+
type: "text",
|
|
1606
|
+
text: `## \u{1F916} Frida AI Recommendation
|
|
1607
|
+
|
|
1608
|
+
${fridaResponse.response}
|
|
1609
|
+
|
|
1610
|
+
---
|
|
1611
|
+
|
|
1612
|
+
`
|
|
1613
|
+
});
|
|
1614
|
+
const topComponents = searchResults.matches.slice(0, 3);
|
|
1615
|
+
if (topComponents.length > 0) {
|
|
1616
|
+
let examplesText = `## \u{1F4DD} CODE GENERATION RULES - Recommended Usage
|
|
1617
|
+
|
|
1618
|
+
**MANDATORY: Use ONLY components from your design system. Here are examples:**
|
|
1619
|
+
|
|
1620
|
+
`;
|
|
1621
|
+
topComponents.forEach((comp) => {
|
|
1622
|
+
const componentName = comp.tag;
|
|
1623
|
+
const importStmt = comp.import_statement;
|
|
1624
|
+
const frameworkType2 = comp.framework_type || "Web Component";
|
|
1625
|
+
examplesText += `### ${comp.human_name || componentName}
|
|
1626
|
+
**Tag:** \`${componentName}\`
|
|
1627
|
+
**Framework:** ${frameworkType2}
|
|
1628
|
+
${importStmt ? `**Import:** \`${importStmt}\`` : ""}
|
|
1629
|
+
|
|
1630
|
+
`;
|
|
1631
|
+
if (comp.usage_examples && comp.usage_examples.length > 0) {
|
|
1632
|
+
comp.usage_examples.slice(0, 1).forEach((example) => {
|
|
1633
|
+
examplesText += `\u2705 **${example.title}**
|
|
1634
|
+
${example.description ? `*${example.description}*
|
|
1635
|
+
` : ""}
|
|
1636
|
+
\`\`\`html
|
|
1637
|
+
${example.code.template}
|
|
1638
|
+
\`\`\`
|
|
1639
|
+
${example.code.script ? `\`\`\`javascript
|
|
1640
|
+
${example.code.script}
|
|
1641
|
+
\`\`\`` : ""}
|
|
1642
|
+
|
|
1643
|
+
`;
|
|
1644
|
+
});
|
|
1645
|
+
} else {
|
|
1646
|
+
const propsExample = comp.props.filter((p) => !p.is_utility_prop).slice(0, 2).map((p) => `${p.name}="${p.default_value || "value"}"`).join(" ");
|
|
1647
|
+
examplesText += `\u2705 **Recommended Usage:**
|
|
1648
|
+
\`\`\`html
|
|
1649
|
+
<${componentName}${propsExample ? ` ${propsExample}` : ""}></${componentName}>
|
|
1650
|
+
\`\`\`
|
|
1651
|
+
|
|
1652
|
+
`;
|
|
1653
|
+
}
|
|
1654
|
+
if (comp.props.length > 0) {
|
|
1655
|
+
examplesText += `**Key Props:**
|
|
1656
|
+
${comp.props.slice(0, 3).map((p) => `- \`${p.name}\`: ${p.description || p.type_signature}`).join("\n")}
|
|
1657
|
+
|
|
1658
|
+
`;
|
|
1659
|
+
}
|
|
1660
|
+
examplesText += `---
|
|
1661
|
+
`;
|
|
1662
|
+
});
|
|
1663
|
+
examplesText += `
|
|
1664
|
+
**Remember:** Always use components from your design system as listed above. Follow the import statements and usage examples provided.
|
|
1665
|
+
|
|
1666
|
+
---
|
|
1667
|
+
|
|
1668
|
+
`;
|
|
1669
|
+
content.push({
|
|
1670
|
+
type: "text",
|
|
1671
|
+
text: examplesText
|
|
1672
|
+
});
|
|
1673
|
+
}
|
|
1674
|
+
if (fridaResponse.metadata?.tokensUsed) {
|
|
1675
|
+
content.push({
|
|
1676
|
+
type: "text",
|
|
1677
|
+
text: `*(Tokens used: ${fridaResponse.metadata.tokensUsed})*
|
|
1678
|
+
|
|
1679
|
+
---
|
|
1680
|
+
|
|
1681
|
+
`
|
|
1682
|
+
});
|
|
1683
|
+
}
|
|
1684
|
+
} else {
|
|
1685
|
+
content.push({
|
|
1686
|
+
type: "text",
|
|
1687
|
+
text: `\u26A0\uFE0F **Frida AI Processing Failed**
|
|
1688
|
+
|
|
1689
|
+
${fridaResponse.error || "Unknown error"}
|
|
1690
|
+
|
|
1691
|
+
Falling back to raw search results.
|
|
1692
|
+
|
|
1693
|
+
---
|
|
1694
|
+
|
|
1695
|
+
`
|
|
1696
|
+
});
|
|
1697
|
+
}
|
|
1698
|
+
} else {
|
|
1699
|
+
content.push({
|
|
1700
|
+
type: "text",
|
|
1701
|
+
text: `\u2139\uFE0F **Frida AI Not Configured**
|
|
1702
|
+
|
|
1703
|
+
Frida AI is not enabled. Set \`FRIDA_BEARER_TOKEN\` and \`FRIDA_API_URL\` environment variables to enable intelligent recommendations.
|
|
1704
|
+
|
|
1705
|
+
Showing raw search results instead.
|
|
1706
|
+
|
|
1707
|
+
---
|
|
1708
|
+
|
|
1709
|
+
`
|
|
1710
|
+
});
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
componentSuggestionsCache.set(cacheKey, {
|
|
1714
|
+
data: content,
|
|
1715
|
+
timestamp: now
|
|
1716
|
+
});
|
|
1717
|
+
console.log(`[COMPONENT SUGGEST] Completed successfully`);
|
|
1718
|
+
return { content };
|
|
1719
|
+
} catch (error) {
|
|
1720
|
+
console.error(`[COMPONENT SUGGEST ERROR]`, error);
|
|
1721
|
+
return {
|
|
1722
|
+
content: [
|
|
1723
|
+
{
|
|
1724
|
+
type: "text",
|
|
1725
|
+
text: `\u274C **Failed to Suggest Components**
|
|
1726
|
+
|
|
1727
|
+
**Error:** ${error instanceof Error ? error.message : String(error)}
|
|
1728
|
+
|
|
1729
|
+
**Troubleshooting:**
|
|
1730
|
+
1. Is Figma Desktop running?
|
|
1731
|
+
2. Is Pinecone configured? (Check PINECONE_API_KEY)
|
|
1732
|
+
3. Are components indexed in Pinecone?
|
|
1733
|
+
4. Check the console for detailed error messages
|
|
1734
|
+
|
|
1735
|
+
${error instanceof Error && error.stack ? `
|
|
1736
|
+
**Stack:**
|
|
1737
|
+
\`\`\`
|
|
1738
|
+
${error.stack}
|
|
1739
|
+
\`\`\`` : ""}`
|
|
1740
|
+
}
|
|
1741
|
+
]
|
|
1742
|
+
};
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
// src/figma2frida/utils/component-extractor.ts
|
|
1747
|
+
var ComponentExtractor = class {
|
|
1748
|
+
/**
|
|
1749
|
+
* Pattern to match custom components in various formats:
|
|
1750
|
+
* - Web Components (hyphenated): <my-button>, <custom-badge>
|
|
1751
|
+
* - PascalCase components: <Button>, <CustomBadge>
|
|
1752
|
+
* - Standalone references
|
|
1753
|
+
* - Import statements
|
|
1754
|
+
*/
|
|
1755
|
+
static COMPONENT_PATTERNS = [
|
|
1756
|
+
// Web Component tags (hyphenated custom elements): <my-button>, <custom-badge>
|
|
1757
|
+
/<([a-z]+-[\w-]+)/gi,
|
|
1758
|
+
// PascalCase component tags (React/Vue/etc): <Button>, <CustomBadge>
|
|
1759
|
+
/<([A-Z][a-zA-Z0-9]+)(?:\s|>|\/)/g,
|
|
1760
|
+
// Standalone hyphenated references: my-button, custom-badge
|
|
1761
|
+
/\b([a-z]+-[\w-]+)\b/gi
|
|
1762
|
+
];
|
|
1763
|
+
/**
|
|
1764
|
+
* Extracts all unique component tags from code content
|
|
1765
|
+
* @param content - Code content as string or array of content items
|
|
1766
|
+
* @returns Array of unique component tags found
|
|
1767
|
+
*/
|
|
1768
|
+
static extractComponents(content) {
|
|
1769
|
+
const componentMap = /* @__PURE__ */ new Map();
|
|
1770
|
+
const textContents = [];
|
|
1771
|
+
if (typeof content === "string") {
|
|
1772
|
+
textContents.push(content);
|
|
1773
|
+
} else if (Array.isArray(content)) {
|
|
1774
|
+
for (const item of content) {
|
|
1775
|
+
if (item.type === "text" && typeof item.text === "string") {
|
|
1776
|
+
textContents.push(item.text);
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
for (const text of textContents) {
|
|
1781
|
+
for (const pattern of this.COMPONENT_PATTERNS) {
|
|
1782
|
+
const matches = text.matchAll(pattern);
|
|
1783
|
+
for (const match of matches) {
|
|
1784
|
+
if (!match[1]) continue;
|
|
1785
|
+
let fullTag = match[1];
|
|
1786
|
+
const normalizedTag = fullTag.includes("-") ? fullTag.toLowerCase().replace(/-+$/, "") : fullTag;
|
|
1787
|
+
if (this.isCommonHtmlTag(normalizedTag)) {
|
|
1788
|
+
continue;
|
|
1789
|
+
}
|
|
1790
|
+
if (componentMap.has(normalizedTag)) {
|
|
1791
|
+
componentMap.get(normalizedTag).occurrences++;
|
|
1792
|
+
} else {
|
|
1793
|
+
componentMap.set(normalizedTag, {
|
|
1794
|
+
tag: normalizedTag,
|
|
1795
|
+
fullTag: normalizedTag,
|
|
1796
|
+
occurrences: 1
|
|
1797
|
+
});
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
return Array.from(componentMap.values()).sort(
|
|
1803
|
+
(a, b) => b.occurrences - a.occurrences
|
|
1804
|
+
);
|
|
1805
|
+
}
|
|
1806
|
+
/**
|
|
1807
|
+
* Check if a tag is a common HTML tag (not a custom component)
|
|
1808
|
+
*/
|
|
1809
|
+
static isCommonHtmlTag(tag) {
|
|
1810
|
+
const commonTags = /* @__PURE__ */ new Set([
|
|
1811
|
+
"div",
|
|
1812
|
+
"span",
|
|
1813
|
+
"p",
|
|
1814
|
+
"a",
|
|
1815
|
+
"img",
|
|
1816
|
+
"ul",
|
|
1817
|
+
"li",
|
|
1818
|
+
"ol",
|
|
1819
|
+
"table",
|
|
1820
|
+
"tr",
|
|
1821
|
+
"td",
|
|
1822
|
+
"th",
|
|
1823
|
+
"form",
|
|
1824
|
+
"input",
|
|
1825
|
+
"button",
|
|
1826
|
+
"select",
|
|
1827
|
+
"option",
|
|
1828
|
+
"textarea",
|
|
1829
|
+
"label",
|
|
1830
|
+
"h1",
|
|
1831
|
+
"h2",
|
|
1832
|
+
"h3",
|
|
1833
|
+
"h4",
|
|
1834
|
+
"h5",
|
|
1835
|
+
"h6",
|
|
1836
|
+
"header",
|
|
1837
|
+
"footer",
|
|
1838
|
+
"nav",
|
|
1839
|
+
"main",
|
|
1840
|
+
"section",
|
|
1841
|
+
"article",
|
|
1842
|
+
"aside",
|
|
1843
|
+
"figure",
|
|
1844
|
+
"figcaption",
|
|
1845
|
+
"strong",
|
|
1846
|
+
"em",
|
|
1847
|
+
"code",
|
|
1848
|
+
"pre",
|
|
1849
|
+
"iframe",
|
|
1850
|
+
"video",
|
|
1851
|
+
"audio",
|
|
1852
|
+
"canvas",
|
|
1853
|
+
"svg",
|
|
1854
|
+
"path",
|
|
1855
|
+
"g",
|
|
1856
|
+
"circle",
|
|
1857
|
+
"rect",
|
|
1858
|
+
"script",
|
|
1859
|
+
"style",
|
|
1860
|
+
"link",
|
|
1861
|
+
"meta",
|
|
1862
|
+
"title",
|
|
1863
|
+
"head",
|
|
1864
|
+
"body",
|
|
1865
|
+
"html"
|
|
1866
|
+
]);
|
|
1867
|
+
return commonTags.has(tag.toLowerCase());
|
|
1868
|
+
}
|
|
1869
|
+
/**
|
|
1870
|
+
* Extracts component tags and returns just the tag names
|
|
1871
|
+
* @param content - Code content
|
|
1872
|
+
* @returns Array of unique component tag names
|
|
1873
|
+
*/
|
|
1874
|
+
static extractComponentTags(content) {
|
|
1875
|
+
return this.extractComponents(content).map((c) => c.tag);
|
|
1876
|
+
}
|
|
1877
|
+
/**
|
|
1878
|
+
* Checks if content contains any custom components
|
|
1879
|
+
* @param content - Code content
|
|
1880
|
+
* @returns true if components are found
|
|
1881
|
+
*/
|
|
1882
|
+
static hasComponents(content) {
|
|
1883
|
+
const components = this.extractComponents(content);
|
|
1884
|
+
return components.length > 0;
|
|
1885
|
+
}
|
|
1886
|
+
};
|
|
1887
|
+
|
|
1888
|
+
// src/figma2frida/services/component-lookup.ts
|
|
1889
|
+
var ComponentLookupService = class {
|
|
1890
|
+
pineconeService;
|
|
1891
|
+
lookupCache = /* @__PURE__ */ new Map();
|
|
1892
|
+
cacheEnabled;
|
|
1893
|
+
constructor(pineconeService2, options = {}) {
|
|
1894
|
+
this.pineconeService = pineconeService2 || new PineconeSearchService({ namespace: options.namespace });
|
|
1895
|
+
this.cacheEnabled = options.cacheResults !== false;
|
|
1896
|
+
}
|
|
1897
|
+
/**
|
|
1898
|
+
* Looks up a single component by tag
|
|
1899
|
+
* @param tag - Component tag/selector (e.g., 'my-button', 'Button')
|
|
1900
|
+
* @param options - Lookup options
|
|
1901
|
+
* @returns Component lookup result
|
|
1902
|
+
*/
|
|
1903
|
+
async lookupComponent(tag, options = {}) {
|
|
1904
|
+
const cacheKey = options.namespace ? `${tag}:${options.namespace}` : tag;
|
|
1905
|
+
if (this.cacheEnabled && this.lookupCache.has(cacheKey)) {
|
|
1906
|
+
const cached = this.lookupCache.get(cacheKey) ?? null;
|
|
1907
|
+
return {
|
|
1908
|
+
tag,
|
|
1909
|
+
found: cached !== null,
|
|
1910
|
+
component: cached
|
|
1911
|
+
};
|
|
1912
|
+
}
|
|
1913
|
+
try {
|
|
1914
|
+
const component = await this.pineconeService.getComponentDetails(tag);
|
|
1915
|
+
const componentResult = component ?? null;
|
|
1916
|
+
if (this.cacheEnabled) {
|
|
1917
|
+
this.lookupCache.set(cacheKey, componentResult);
|
|
1918
|
+
}
|
|
1919
|
+
return {
|
|
1920
|
+
tag,
|
|
1921
|
+
found: componentResult !== null,
|
|
1922
|
+
component: componentResult
|
|
1923
|
+
};
|
|
1924
|
+
} catch (error) {
|
|
1925
|
+
return {
|
|
1926
|
+
tag,
|
|
1927
|
+
found: false,
|
|
1928
|
+
component: null,
|
|
1929
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1930
|
+
};
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
/**
|
|
1934
|
+
* Looks up multiple components in parallel
|
|
1935
|
+
* @param tags - Array of component tags
|
|
1936
|
+
* @param options - Lookup options
|
|
1937
|
+
* @returns Array of lookup results
|
|
1938
|
+
*/
|
|
1939
|
+
async lookupMultipleComponents(tags, options = {}) {
|
|
1940
|
+
if (!tags || tags.length === 0) {
|
|
1941
|
+
return [];
|
|
1942
|
+
}
|
|
1943
|
+
const uniqueTags = Array.from(new Set(tags));
|
|
1944
|
+
const lookupPromises = uniqueTags.map(
|
|
1945
|
+
(tag) => this.lookupComponent(tag, options)
|
|
1946
|
+
);
|
|
1947
|
+
const results = await Promise.all(lookupPromises);
|
|
1948
|
+
return results.sort((a, b) => {
|
|
1949
|
+
if (a.found !== b.found) {
|
|
1950
|
+
return a.found ? -1 : 1;
|
|
1951
|
+
}
|
|
1952
|
+
return a.tag.localeCompare(b.tag);
|
|
1953
|
+
});
|
|
1954
|
+
}
|
|
1955
|
+
/**
|
|
1956
|
+
* Gets a summary of found components
|
|
1957
|
+
* @param results - Lookup results
|
|
1958
|
+
* @returns Summary object
|
|
1959
|
+
*/
|
|
1960
|
+
static getSummary(results) {
|
|
1961
|
+
const found = results.filter((r) => r.found);
|
|
1962
|
+
const notFound = results.filter((r) => !r.found);
|
|
1963
|
+
return {
|
|
1964
|
+
total: results.length,
|
|
1965
|
+
found: found.length,
|
|
1966
|
+
notFound: notFound.length,
|
|
1967
|
+
foundComponents: found.map((r) => r.component).filter((c) => c !== null),
|
|
1968
|
+
notFoundTags: notFound.map((r) => r.tag)
|
|
1969
|
+
};
|
|
1970
|
+
}
|
|
1971
|
+
/**
|
|
1972
|
+
* Clears the lookup cache
|
|
1973
|
+
*/
|
|
1974
|
+
clearCache() {
|
|
1975
|
+
this.lookupCache.clear();
|
|
1976
|
+
}
|
|
1977
|
+
/**
|
|
1978
|
+
* Gets cache statistics
|
|
1979
|
+
*/
|
|
1980
|
+
getCacheStats() {
|
|
1981
|
+
return {
|
|
1982
|
+
size: this.lookupCache.size,
|
|
1983
|
+
keys: Array.from(this.lookupCache.keys())
|
|
1984
|
+
};
|
|
1985
|
+
}
|
|
1986
|
+
};
|
|
1987
|
+
|
|
1988
|
+
// src/figma2frida/figma2frida.ts
|
|
1989
|
+
console.log("\n[CONFIG] Validating environment variables...\n");
|
|
1990
|
+
var missingVars = [];
|
|
1991
|
+
if (!process.env.PINECONE_API_KEY) {
|
|
1992
|
+
console.error(`[CONFIG] \u274C ERROR: PINECONE_API_KEY is missing from .env file`);
|
|
1993
|
+
missingVars.push("PINECONE_API_KEY");
|
|
1994
|
+
} else {
|
|
1995
|
+
console.log(`[CONFIG] \u2713 PINECONE_API_KEY: ${process.env.PINECONE_API_KEY.substring(0, 8)}... (hidden)`);
|
|
1996
|
+
}
|
|
1997
|
+
if (!process.env.PINECONE_INDEX) {
|
|
1998
|
+
console.error(`[CONFIG] \u274C ERROR: PINECONE_INDEX is missing from .env file`);
|
|
1999
|
+
missingVars.push("PINECONE_INDEX");
|
|
2000
|
+
} else {
|
|
2001
|
+
console.log(`[CONFIG] \u2713 PINECONE_INDEX: ${process.env.PINECONE_INDEX}`);
|
|
2002
|
+
}
|
|
2003
|
+
if (!process.env.PINECONE_MIN_SCORE) {
|
|
2004
|
+
console.error(`[CONFIG] \u274C ERROR: PINECONE_MIN_SCORE is missing from .env file`);
|
|
2005
|
+
missingVars.push("PINECONE_MIN_SCORE");
|
|
2006
|
+
} else {
|
|
2007
|
+
const parsed = parseFloat(process.env.PINECONE_MIN_SCORE);
|
|
2008
|
+
if (isNaN(parsed)) {
|
|
2009
|
+
console.error(`[CONFIG] \u274C ERROR: PINECONE_MIN_SCORE is not a valid number: "${process.env.PINECONE_MIN_SCORE}"`);
|
|
2010
|
+
missingVars.push("PINECONE_MIN_SCORE");
|
|
2011
|
+
} else {
|
|
2012
|
+
console.log(`[CONFIG] \u2713 PINECONE_MIN_SCORE: ${parsed}`);
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
2015
|
+
if (!process.env.PINECONE_TOP_K) {
|
|
2016
|
+
console.error(`[CONFIG] \u274C ERROR: PINECONE_TOP_K is missing from .env file`);
|
|
2017
|
+
missingVars.push("PINECONE_TOP_K");
|
|
2018
|
+
} else {
|
|
2019
|
+
const parsed = parseInt(process.env.PINECONE_TOP_K, 10);
|
|
2020
|
+
if (isNaN(parsed)) {
|
|
2021
|
+
console.error(`[CONFIG] \u274C ERROR: PINECONE_TOP_K is not a valid integer: "${process.env.PINECONE_TOP_K}"`);
|
|
2022
|
+
missingVars.push("PINECONE_TOP_K");
|
|
2023
|
+
} else {
|
|
2024
|
+
console.log(`[CONFIG] \u2713 PINECONE_TOP_K: ${parsed}`);
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2027
|
+
if (missingVars.length > 0) {
|
|
2028
|
+
console.error(`
|
|
2029
|
+
[CONFIG] \u274C Missing required environment variables: ${missingVars.join(", ")}
|
|
2030
|
+
`);
|
|
2031
|
+
process.exit(1);
|
|
2032
|
+
}
|
|
2033
|
+
console.log(`[CONFIG] \u2713 All required Pinecone environment variables are set
|
|
2034
|
+
`);
|
|
2035
|
+
var argv = yargs(hideBin(process.argv)).option("pinecone-namespace", {
|
|
2036
|
+
type: "string",
|
|
2037
|
+
description: "Pinecone namespace (passed via options, not from .env)"
|
|
2038
|
+
}).parseSync();
|
|
2039
|
+
var PINECONE_NAMESPACE = argv.pineconeNamespace || void 0;
|
|
2040
|
+
if (PINECONE_NAMESPACE) {
|
|
2041
|
+
console.log(`[CONFIG] \u2713 PINECONE_NAMESPACE: ${PINECONE_NAMESPACE} (from CLI)
|
|
2042
|
+
`);
|
|
2043
|
+
}
|
|
2044
|
+
var HTTP_STREAM = process.env.HTTP_STREAM === "true" || process.env.HTTP_STREAM === "1";
|
|
2045
|
+
var PORT = parseInt(process.env.PORT || "8080", 10);
|
|
2046
|
+
var designContextCache = /* @__PURE__ */ new Map();
|
|
2047
|
+
var CACHE_TTL_MS = 6e4;
|
|
2048
|
+
var server = new FastMCP({
|
|
2049
|
+
instructions: `You are a Figma design assistant optimized for fast code generation using your organization's design system components.
|
|
2050
|
+
|
|
2051
|
+
**\u{1F6A8} CRITICAL: Follow Figma Design Exactly**
|
|
2052
|
+
|
|
2053
|
+
When generating code from Figma designs, you MUST:
|
|
2054
|
+
|
|
2055
|
+
1. **USE THE TRANSFORMED CODE AS YOUR BASE** - The \`figma_get_design_context\` tool with \`transformToDesignSystem: true\` returns code that:
|
|
2056
|
+
- Preserves the EXACT structure, layout, and styling from Figma
|
|
2057
|
+
- Transforms generic HTML elements to design system components
|
|
2058
|
+
- Maintains all divs, containers, classes, and CSS from the original design
|
|
2059
|
+
- **DO NOT invent new layouts or structures** - Use the transformed code exactly as provided
|
|
2060
|
+
|
|
2061
|
+
2. **SHOW ELEMENT ANALYSIS** - Before showing code, clearly display:
|
|
2062
|
+
- What HTML elements were found in the Figma design
|
|
2063
|
+
- Which design system components will be used for each element
|
|
2064
|
+
- The mapping: HTML element \u2192 Design system component
|
|
2065
|
+
- Any elements that couldn't be transformed (and why)
|
|
2066
|
+
|
|
2067
|
+
3. **PRESERVE DESIGN STRUCTURE** - The transformed code maintains:
|
|
2068
|
+
- All container divs and layout structure
|
|
2069
|
+
- All CSS classes and inline styles
|
|
2070
|
+
- All spacing, positioning, and visual hierarchy
|
|
2071
|
+
- The exact visual appearance from Figma
|
|
2072
|
+
|
|
2073
|
+
4. **USE ONLY DESIGN SYSTEM COMPONENTS** - Components must be:
|
|
2074
|
+
- From your organization's design system library
|
|
2075
|
+
- From the component suggestions provided
|
|
2076
|
+
- NEVER use generic component libraries unless they appear in search results
|
|
2077
|
+
- Framework/language agnostic - use whatever framework type is specified in component metadata (Web Components, React, Angular, Vue, etc.)
|
|
2078
|
+
|
|
2079
|
+
**Available tools:**
|
|
2080
|
+
- figma_get_screenshot: Get visual screenshot (fast, ~0.5-1s)
|
|
2081
|
+
- figma_get_design_context: Get design context and transformed code with design system components (main tool)
|
|
2082
|
+
- figma_suggest_components: Suggest design system components based on Figma design
|
|
2083
|
+
|
|
2084
|
+
**Workflow for code generation:**
|
|
2085
|
+
1. User asks for code from Figma design
|
|
2086
|
+
2. Call \`figma_get_design_context\` with \`forceCode: true\` and \`transformToDesignSystem: true\`
|
|
2087
|
+
3. The tool will return:
|
|
2088
|
+
- Analysis of HTML elements found
|
|
2089
|
+
- Mapping to design system components
|
|
2090
|
+
- Transformed code preserving Figma design structure
|
|
2091
|
+
- Component metadata including framework type, import statements, and usage examples
|
|
2092
|
+
4. **USE THE TRANSFORMED CODE AS PROVIDED** - Do not modify the structure or layout
|
|
2093
|
+
5. Show the element analysis and mapping clearly to the user
|
|
2094
|
+
6. Present the transformed code maintaining all Figma design elements
|
|
2095
|
+
7. **Use the component's framework type and import statements** as specified in the component metadata
|
|
2096
|
+
|
|
2097
|
+
**IMPORTANT:** The transformed code from Figma is the source of truth. Do not create new layouts or modify the structure. Use only the design system components as mapped in the transformation. The framework/language depends on what's configured in your component library.
|
|
2098
|
+
|
|
2099
|
+
Connection is established on startup. Always use design system components from the configured library.`,
|
|
2100
|
+
name: "figma-proxy",
|
|
2101
|
+
version: "1.0.0"
|
|
2102
|
+
});
|
|
2103
|
+
console.log("\n[FIGMA2FRIDA] ========================================");
|
|
2104
|
+
console.log("[FIGMA2FRIDA] Initializing Figma Client...");
|
|
2105
|
+
var figmaClient = new FigmaClient("http://127.0.0.1:3845/sse");
|
|
2106
|
+
console.log("[FIGMA2FRIDA] \u2713 Figma Client object created");
|
|
2107
|
+
console.log("[FIGMA2FRIDA] ========================================\n");
|
|
2108
|
+
var figmaConnected = false;
|
|
2109
|
+
console.log("[FIGMA2FRIDA] Starting initial connection attempt to Figma MCP...");
|
|
2110
|
+
console.log("[FIGMA2FRIDA] This is a non-blocking async operation - server will continue starting\n");
|
|
2111
|
+
var connectionStartTime = Date.now();
|
|
2112
|
+
figmaClient.connect().then(() => {
|
|
2113
|
+
const connectionTime = Date.now() - connectionStartTime;
|
|
2114
|
+
figmaConnected = true;
|
|
2115
|
+
console.log("\n[FIGMA2FRIDA] ========================================");
|
|
2116
|
+
console.log("[FIGMA2FRIDA] \u2705 STARTUP CONNECTION SUCCESS!");
|
|
2117
|
+
console.log(`[FIGMA2FRIDA] Connection time: ${connectionTime}ms`);
|
|
2118
|
+
console.log(`[FIGMA2FRIDA] Status: figmaConnected = ${figmaConnected}`);
|
|
2119
|
+
console.log("[FIGMA2FRIDA] ========================================\n");
|
|
2120
|
+
}).catch((error) => {
|
|
2121
|
+
const connectionTime = Date.now() - connectionStartTime;
|
|
2122
|
+
console.log("\n[FIGMA2FRIDA] ========================================");
|
|
2123
|
+
console.log("[FIGMA2FRIDA] \u274C STARTUP CONNECTION FAILED");
|
|
2124
|
+
console.log(`[FIGMA2FRIDA] Time elapsed: ${connectionTime}ms`);
|
|
2125
|
+
console.log(`[FIGMA2FRIDA] Status: figmaConnected = ${figmaConnected}`);
|
|
2126
|
+
console.log("[FIGMA2FRIDA] \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
2127
|
+
console.error("[FIGMA2FRIDA] Error type:", error?.constructor?.name || typeof error);
|
|
2128
|
+
console.error("[FIGMA2FRIDA] Error message:", error instanceof Error ? error.message : String(error));
|
|
2129
|
+
if (error && typeof error === "object") {
|
|
2130
|
+
const errorObj = error;
|
|
2131
|
+
if ("code" in errorObj) {
|
|
2132
|
+
console.error("[FIGMA2FRIDA] Error code:", errorObj.code);
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2135
|
+
if (error instanceof Error && error.stack) {
|
|
2136
|
+
console.error("[FIGMA2FRIDA] Error stack:");
|
|
2137
|
+
console.error(error.stack);
|
|
2138
|
+
}
|
|
2139
|
+
console.log("[FIGMA2FRIDA] \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
2140
|
+
console.error("[FIGMA2FRIDA] Make sure Figma Desktop is running with MCP enabled at http://127.0.0.1:3845/sse");
|
|
2141
|
+
console.log("[FIGMA2FRIDA] Note: Tools will attempt to reconnect when called");
|
|
2142
|
+
console.log("[FIGMA2FRIDA] ========================================\n");
|
|
2143
|
+
});
|
|
2144
|
+
server.addTool({
|
|
2145
|
+
name: "figma_get_screenshot",
|
|
2146
|
+
description: `Get a screenshot of a Figma node for visual reference.
|
|
2147
|
+
|
|
2148
|
+
**Fast & Simple:**
|
|
2149
|
+
- Takes ~0.5-1 second
|
|
2150
|
+
- Returns just the image, no code generation
|
|
2151
|
+
- Use this when you need to see what the design looks like
|
|
2152
|
+
|
|
2153
|
+
**Usage:**
|
|
2154
|
+
- Provide a nodeId (e.g., "315-2920")
|
|
2155
|
+
- Or leave empty to use current Figma selection
|
|
2156
|
+
|
|
2157
|
+
**Example:**
|
|
2158
|
+
\`\`\`json
|
|
2159
|
+
{
|
|
2160
|
+
"nodeId": "315-2920"
|
|
2161
|
+
}
|
|
2162
|
+
\`\`\``,
|
|
2163
|
+
annotations: {
|
|
2164
|
+
title: "Get Screenshot",
|
|
2165
|
+
readOnlyHint: true,
|
|
2166
|
+
openWorldHint: false
|
|
2167
|
+
},
|
|
2168
|
+
parameters: z.object({
|
|
2169
|
+
nodeId: z.string().optional().describe('Figma node ID (e.g., "315-2920"). Leave empty for current selection.')
|
|
2170
|
+
}),
|
|
2171
|
+
execute: async (args, _context) => {
|
|
2172
|
+
try {
|
|
2173
|
+
console.log(`[FIGMA_GET_SCREENSHOT] Tool called with nodeId: ${args.nodeId || "current selection"}`);
|
|
2174
|
+
console.log(`[FIGMA_GET_SCREENSHOT] figmaConnected flag: ${figmaConnected}`);
|
|
2175
|
+
if (!figmaConnected) {
|
|
2176
|
+
console.log(`[FIGMA_GET_SCREENSHOT] \u26A0\uFE0F Early return: figmaConnected is false`);
|
|
2177
|
+
const result = {
|
|
2178
|
+
content: [{
|
|
2179
|
+
type: "text",
|
|
2180
|
+
text: "\u274C Not connected to Figma MCP. Check logs for connection details."
|
|
2181
|
+
}]
|
|
2182
|
+
};
|
|
2183
|
+
return result;
|
|
2184
|
+
}
|
|
2185
|
+
console.log(`[FIGMA_GET_SCREENSHOT] Calling figmaClient.getScreenshot()...`);
|
|
2186
|
+
const screenshot = await figmaClient.getScreenshot(args.nodeId);
|
|
2187
|
+
if (!screenshot.content || screenshot.content.length === 0) {
|
|
2188
|
+
const result = {
|
|
2189
|
+
content: [{ type: "text", text: "\u274C No screenshot returned" }]
|
|
2190
|
+
};
|
|
2191
|
+
return result;
|
|
2192
|
+
}
|
|
2193
|
+
return { content: screenshot.content };
|
|
2194
|
+
} catch (error) {
|
|
2195
|
+
const result = {
|
|
2196
|
+
content: [{
|
|
2197
|
+
type: "text",
|
|
2198
|
+
text: `\u274C Failed to get screenshot: ${error instanceof Error ? error.message : String(error)}`
|
|
2199
|
+
}]
|
|
2200
|
+
};
|
|
2201
|
+
return result;
|
|
2202
|
+
}
|
|
2203
|
+
}
|
|
2204
|
+
});
|
|
2205
|
+
server.addTool({
|
|
2206
|
+
name: "figma_get_design_context",
|
|
2207
|
+
description: `Get design context and generated code directly from Figma Desktop MCP.
|
|
2208
|
+
|
|
2209
|
+
**What this does:**
|
|
2210
|
+
- Calls Figma's get_design_context tool
|
|
2211
|
+
- Returns design data and optionally generated code (React, HTML, etc.)
|
|
2212
|
+
- Automatically searches for design system components in generated code
|
|
2213
|
+
- Results are cached for 60 seconds for instant subsequent access
|
|
2214
|
+
- Streams progress updates for better responsiveness
|
|
2215
|
+
|
|
2216
|
+
**Performance Modes:**
|
|
2217
|
+
- \`forceCode: false\` (default) - Fast metadata-only mode (~1-2s)
|
|
2218
|
+
- \`forceCode: true\` - Full code generation (~3-15s depending on complexity)
|
|
2219
|
+
|
|
2220
|
+
**Component Search:**
|
|
2221
|
+
- When \`forceCode: true\` and \`autoSearchComponents: true\` (default), automatically:
|
|
2222
|
+
- Extracts component tags from generated code
|
|
2223
|
+
- Searches component information in Pinecone
|
|
2224
|
+
- Streams component details while processing
|
|
2225
|
+
- Adds summary section with found/not found components
|
|
2226
|
+
|
|
2227
|
+
**Code Transformation:**
|
|
2228
|
+
- When \`forceCode: true\` and \`transformToDesignSystem: true\` (default), automatically:
|
|
2229
|
+
- **Analyzes** all HTML elements found in Figma design
|
|
2230
|
+
- **Shows mapping** of HTML elements \u2192 design system components before transformation
|
|
2231
|
+
- **Transforms** generic HTML elements (button, input, etc.) to design system components
|
|
2232
|
+
- **Preserves** ALL structure, layout, CSS classes, and styling from Figma design
|
|
2233
|
+
- **Maintains** all container divs, spacing, positioning, and visual hierarchy
|
|
2234
|
+
- **Uses component metadata** including framework type, import statements, and usage examples
|
|
2235
|
+
- Returns transformed code as the main output, with original code for reference
|
|
2236
|
+
- Shows detailed analysis and transformation summary
|
|
2237
|
+
|
|
2238
|
+
**Output includes:**
|
|
2239
|
+
1. Element Analysis - What HTML elements were found in Figma
|
|
2240
|
+
2. Element Mapping - Which design system components will be used for each element
|
|
2241
|
+
3. Transformed Code - Ready-to-use code preserving Figma design structure
|
|
2242
|
+
4. Original Code - For reference and comparison
|
|
2243
|
+
|
|
2244
|
+
**Usage:**
|
|
2245
|
+
- Provide a nodeId (e.g., "315-2920")
|
|
2246
|
+
- Or leave empty to use current Figma selection
|
|
2247
|
+
- Set forceCode to true only when you need the actual React/HTML code
|
|
2248
|
+
- Set autoSearchComponents to false to disable automatic component search
|
|
2249
|
+
|
|
2250
|
+
**Example:**
|
|
2251
|
+
\`\`\`json
|
|
2252
|
+
{
|
|
2253
|
+
"nodeId": "315-2920",
|
|
2254
|
+
"forceCode": true,
|
|
2255
|
+
"autoSearchComponents": true
|
|
2256
|
+
}
|
|
2257
|
+
\`\`\`
|
|
2258
|
+
|
|
2259
|
+
**Tip:** Start with \`forceCode: false\` to quickly see what's available, then request code if needed.`,
|
|
2260
|
+
annotations: {
|
|
2261
|
+
title: "Get Design Context from Figma",
|
|
2262
|
+
readOnlyHint: true,
|
|
2263
|
+
openWorldHint: false
|
|
2264
|
+
},
|
|
2265
|
+
parameters: z.object({
|
|
2266
|
+
nodeId: z.string().optional().describe('Figma node ID (e.g., "315-2920" or "315:2920"). Leave empty for current selection.'),
|
|
2267
|
+
forceCode: z.boolean().optional().describe("Force code generation even for large outputs (default: false). Set to true only when you need the actual React/HTML code."),
|
|
2268
|
+
autoSearchComponents: z.boolean().optional().describe("Automatically search for design system components in the generated code (default: true). Only works when forceCode is true."),
|
|
2269
|
+
transformToDesignSystem: z.boolean().optional().describe("Transform Figma-generated code to use design system components (default: true). Only works when forceCode is true. Replaces generic HTML elements (button, input, etc.) with components from your design system library. Framework and language are determined by component metadata.")
|
|
2270
|
+
}),
|
|
2271
|
+
execute: async (args, context) => {
|
|
2272
|
+
try {
|
|
2273
|
+
console.log(`
|
|
2274
|
+
[FIGMA_GET_DESIGN_CONTEXT] ========================================`);
|
|
2275
|
+
console.log(`[FIGMA_GET_DESIGN_CONTEXT] Tool called`);
|
|
2276
|
+
console.log(`[FIGMA_GET_DESIGN_CONTEXT] nodeId: ${args.nodeId || "current selection"}`);
|
|
2277
|
+
console.log(`[FIGMA_GET_DESIGN_CONTEXT] forceCode: ${args.forceCode ?? false}`);
|
|
2278
|
+
console.log(`[FIGMA_GET_DESIGN_CONTEXT] figmaConnected flag: ${figmaConnected}`);
|
|
2279
|
+
console.log(`[FIGMA_GET_DESIGN_CONTEXT] ========================================`);
|
|
2280
|
+
if (!figmaConnected) {
|
|
2281
|
+
console.log(`[FIGMA_GET_DESIGN_CONTEXT] \u26A0\uFE0F Early return: figmaConnected is false`);
|
|
2282
|
+
return {
|
|
2283
|
+
content: [
|
|
2284
|
+
{
|
|
2285
|
+
type: "text",
|
|
2286
|
+
text: "\u274C Not connected to Figma MCP. Check logs for connection details."
|
|
2287
|
+
}
|
|
2288
|
+
]
|
|
2289
|
+
};
|
|
2290
|
+
}
|
|
2291
|
+
console.log(`[FIGMA_GET_DESIGN_CONTEXT] Calling figmaClient.getDesignContext()...`);
|
|
2292
|
+
console.log(`
|
|
2293
|
+
[DESIGN CONTEXT] Starting...`);
|
|
2294
|
+
const nodeId = args.nodeId;
|
|
2295
|
+
const forceCode = args.forceCode ?? false;
|
|
2296
|
+
const autoSearchComponents = args.autoSearchComponents ?? true;
|
|
2297
|
+
const transformToDesignSystem = args.transformToDesignSystem ?? true;
|
|
2298
|
+
console.log(`[DESIGN CONTEXT] NodeId:`, nodeId || "current selection");
|
|
2299
|
+
console.log(`[DESIGN CONTEXT] ForceCode:`, forceCode);
|
|
2300
|
+
console.log(`[DESIGN CONTEXT] AutoSearchComponents:`, autoSearchComponents);
|
|
2301
|
+
console.log(`[DESIGN CONTEXT] TransformToDesignSystem:`, transformToDesignSystem);
|
|
2302
|
+
const cacheKey = `${nodeId || "current"}:${forceCode}`;
|
|
2303
|
+
const now = Date.now();
|
|
2304
|
+
const cached = designContextCache.get(cacheKey);
|
|
2305
|
+
if (cached && now - cached.timestamp < CACHE_TTL_MS) {
|
|
2306
|
+
console.log(`[DESIGN CONTEXT] Using cached result (age: ${(now - cached.timestamp).toFixed(0)}ms)`);
|
|
2307
|
+
await context.streamContent({
|
|
2308
|
+
type: "text",
|
|
2309
|
+
text: `\u26A1 Using cached design context (${((now - cached.timestamp) / 1e3).toFixed(1)}s old)`
|
|
2310
|
+
});
|
|
2311
|
+
const header2 = {
|
|
2312
|
+
type: "text",
|
|
2313
|
+
text: `\u2705 **Design Context from Figma** (Cached)
|
|
2314
|
+
|
|
2315
|
+
**Node ID:** ${nodeId || "current selection"}
|
|
2316
|
+
**Content Items:** ${cached.data.content?.length || 0}
|
|
2317
|
+
**Cache Age:** ${((now - cached.timestamp) / 1e3).toFixed(1)}s
|
|
2318
|
+
**Mode:** ${forceCode ? "Full code generation" : "Metadata only (fast mode)"}
|
|
2319
|
+
|
|
2320
|
+
\u{1F4BE} **Cache hit!** This response was retrieved from cache for instant performance.
|
|
2321
|
+
|
|
2322
|
+
---
|
|
2323
|
+
|
|
2324
|
+
`
|
|
2325
|
+
};
|
|
2326
|
+
return {
|
|
2327
|
+
content: [header2, ...cached.data.content || []]
|
|
2328
|
+
};
|
|
2329
|
+
}
|
|
2330
|
+
await context.streamContent({
|
|
2331
|
+
type: "text",
|
|
2332
|
+
text: `\u{1F504} Fetching design from Figma${forceCode ? " (including code generation)" : ""}...`
|
|
2333
|
+
});
|
|
2334
|
+
console.log(`[DESIGN CONTEXT] Calling Figma MCP get_design_context...`);
|
|
2335
|
+
const startTime = performance.now();
|
|
2336
|
+
const designResult = await figmaClient.getDesignContext(nodeId, {
|
|
2337
|
+
clientLanguages: "typescript",
|
|
2338
|
+
clientFrameworks: "react",
|
|
2339
|
+
forceCode
|
|
2340
|
+
});
|
|
2341
|
+
const endTime = performance.now();
|
|
2342
|
+
designContextCache.set(cacheKey, {
|
|
2343
|
+
data: designResult,
|
|
2344
|
+
timestamp: now,
|
|
2345
|
+
forceCode
|
|
2346
|
+
});
|
|
2347
|
+
console.log(`[DESIGN CONTEXT] Cached result for key: ${cacheKey}`);
|
|
2348
|
+
console.log(`[DESIGN CONTEXT] Response received in ${(endTime - startTime).toFixed(0)}ms`);
|
|
2349
|
+
console.log(`[DESIGN CONTEXT] Content items:`, designResult.content?.length || 0);
|
|
2350
|
+
if (!designResult.content || designResult.content.length === 0) {
|
|
2351
|
+
return {
|
|
2352
|
+
content: [
|
|
2353
|
+
{
|
|
2354
|
+
type: "text",
|
|
2355
|
+
text: `\u274C No design context returned from Figma MCP.
|
|
2356
|
+
|
|
2357
|
+
**Node ID:** ${nodeId || "current selection"}
|
|
2358
|
+
|
|
2359
|
+
**Troubleshooting:**
|
|
2360
|
+
- Ensure the node exists and is accessible in Figma
|
|
2361
|
+
- Try selecting the node in Figma first
|
|
2362
|
+
- Check if the node has any visible design elements`
|
|
2363
|
+
}
|
|
2364
|
+
]
|
|
2365
|
+
};
|
|
2366
|
+
}
|
|
2367
|
+
designResult.content.forEach((item, idx) => {
|
|
2368
|
+
console.log(`[DESIGN CONTEXT] Content[${idx}]:`, {
|
|
2369
|
+
type: item.type,
|
|
2370
|
+
hasText: item.type === "text" && "text" in item,
|
|
2371
|
+
textLength: item.type === "text" && "text" in item ? item.text?.length : 0,
|
|
2372
|
+
hasData: item.type === "image" && "data" in item
|
|
2373
|
+
});
|
|
2374
|
+
});
|
|
2375
|
+
const header = {
|
|
2376
|
+
type: "text",
|
|
2377
|
+
text: `\u2705 **Design Context from Figma**
|
|
2378
|
+
|
|
2379
|
+
**Node ID:** ${nodeId || "current selection"}
|
|
2380
|
+
**Content Items:** ${designResult.content.length}
|
|
2381
|
+
**Fetch Time:** ${(endTime - startTime).toFixed(0)}ms
|
|
2382
|
+
**Mode:** ${forceCode ? "Full code generation" : "Metadata only (fast mode)"}
|
|
2383
|
+
|
|
2384
|
+
${forceCode ? "" : "\u{1F4A1} **Tip:** If you need the actual React/HTML code, call this tool again with `forceCode: true`\n\n"}**What you're seeing:** Raw output from Figma's get_design_context tool.
|
|
2385
|
+
|
|
2386
|
+
---
|
|
2387
|
+
|
|
2388
|
+
`
|
|
2389
|
+
};
|
|
2390
|
+
console.log(`\u2705 Design context retrieved successfully`);
|
|
2391
|
+
let transformedCodeSection = null;
|
|
2392
|
+
let componentSuggestions = [];
|
|
2393
|
+
if (forceCode && transformToDesignSystem && designResult.content) {
|
|
2394
|
+
try {
|
|
2395
|
+
await context.streamContent({
|
|
2396
|
+
type: "text",
|
|
2397
|
+
text: "\u{1F504} Transforming code to use design system components..."
|
|
2398
|
+
});
|
|
2399
|
+
if (autoSearchComponents) {
|
|
2400
|
+
await context.streamContent({
|
|
2401
|
+
type: "text",
|
|
2402
|
+
text: "\u{1F50D} Discovering design system components from design analysis..."
|
|
2403
|
+
});
|
|
2404
|
+
try {
|
|
2405
|
+
const queryExtractor2 = new DesignQueryExtractor();
|
|
2406
|
+
const searchQuery = queryExtractor2.extractQuery(designResult);
|
|
2407
|
+
console.log(`[TRANSFORM] Extracted query: "${searchQuery}"`);
|
|
2408
|
+
await context.streamContent({
|
|
2409
|
+
type: "text",
|
|
2410
|
+
text: `\u{1F50E} Searching Pinecone for: "${searchQuery}"...`
|
|
2411
|
+
});
|
|
2412
|
+
const searchService = new PineconeSearchService({
|
|
2413
|
+
namespace: PINECONE_NAMESPACE
|
|
2414
|
+
});
|
|
2415
|
+
const searchResults = await searchService.search(searchQuery, {
|
|
2416
|
+
// topK and minScore use service defaults (read from PINECONE_TOP_K and PINECONE_MIN_SCORE env vars)
|
|
2417
|
+
});
|
|
2418
|
+
componentSuggestions = searchResults.matches;
|
|
2419
|
+
const componentNames = componentSuggestions.map((comp) => comp.tag).join(", ");
|
|
2420
|
+
console.log(
|
|
2421
|
+
`[TRANSFORM] Found ${componentSuggestions.length} components from Pinecone: ${componentNames}`
|
|
2422
|
+
);
|
|
2423
|
+
if (componentSuggestions.length > 0) {
|
|
2424
|
+
await context.streamContent({
|
|
2425
|
+
type: "text",
|
|
2426
|
+
text: `\u2705 Discovered ${componentSuggestions.length} design system components:
|
|
2427
|
+
${componentSuggestions.map((c) => ` - ${c.tag} (${c.category})`).join("\n")}`
|
|
2428
|
+
});
|
|
2429
|
+
} else {
|
|
2430
|
+
await context.streamContent({
|
|
2431
|
+
type: "text",
|
|
2432
|
+
text: `\u26A0\uFE0F No components found in Pinecone. Using default mappings.`
|
|
2433
|
+
});
|
|
2434
|
+
}
|
|
2435
|
+
} catch (error) {
|
|
2436
|
+
console.warn(
|
|
2437
|
+
`[TRANSFORM] Could not get component suggestions:`,
|
|
2438
|
+
error
|
|
2439
|
+
);
|
|
2440
|
+
await context.streamContent({
|
|
2441
|
+
type: "text",
|
|
2442
|
+
text: `\u26A0\uFE0F Error searching Pinecone: ${error instanceof Error ? error.message : String(error)}
|
|
2443
|
+
Falling back to default mappings.`
|
|
2444
|
+
});
|
|
2445
|
+
}
|
|
2446
|
+
}
|
|
2447
|
+
if (componentSuggestions.length > 0) {
|
|
2448
|
+
await context.streamContent({
|
|
2449
|
+
type: "text",
|
|
2450
|
+
text: `\u2705 Found ${componentSuggestions.length} design system components from Pinecone. Returning component documentation to agent.`
|
|
2451
|
+
});
|
|
2452
|
+
const queryExtractor2 = new DesignQueryExtractor();
|
|
2453
|
+
const searchQuery = queryExtractor2.extractQuery(designResult);
|
|
2454
|
+
const minScore = parseFloat(process.env.PINECONE_MIN_SCORE);
|
|
2455
|
+
const topK = parseInt(process.env.PINECONE_TOP_K, 10);
|
|
2456
|
+
const indexName = process.env.PINECONE_INDEX;
|
|
2457
|
+
transformedCodeSection = {
|
|
2458
|
+
type: "text",
|
|
2459
|
+
text: `## \u{1F50D} **Available Design System Components from Pinecone**
|
|
2460
|
+
|
|
2461
|
+
**Search Query:** "${searchQuery}"
|
|
2462
|
+
**Components Found:** ${componentSuggestions.length}
|
|
2463
|
+
|
|
2464
|
+
Use the component documentation below to transform the Figma code into design system components. The agent will generate the code using these component specifications.
|
|
2465
|
+
|
|
2466
|
+
---
|
|
2467
|
+
|
|
2468
|
+
${Formatters.toMarkdown({
|
|
2469
|
+
query: searchQuery,
|
|
2470
|
+
matches: componentSuggestions,
|
|
2471
|
+
relevantMatches: componentSuggestions.length,
|
|
2472
|
+
totalMatches: componentSuggestions.length,
|
|
2473
|
+
searchMetadata: { minScore, topK, indexName },
|
|
2474
|
+
lowScoreMatches: []
|
|
2475
|
+
})}
|
|
2476
|
+
|
|
2477
|
+
**\u{1F4A1} Instructions:** Use the component documentation above to transform the Figma code. Each component includes:
|
|
2478
|
+
- Exact prop names and types
|
|
2479
|
+
- Event names and types
|
|
2480
|
+
- Full documentation
|
|
2481
|
+
- Usage examples
|
|
2482
|
+
|
|
2483
|
+
Generate code that uses these components with the correct props and structure.
|
|
2484
|
+
|
|
2485
|
+
---
|
|
2486
|
+
|
|
2487
|
+
`
|
|
2488
|
+
};
|
|
2489
|
+
} else {
|
|
2490
|
+
await context.streamContent({
|
|
2491
|
+
type: "text",
|
|
2492
|
+
text: `\u26A0\uFE0F No components found in Pinecone for this design.`
|
|
2493
|
+
});
|
|
2494
|
+
}
|
|
2495
|
+
} catch (error) {
|
|
2496
|
+
console.error(`[CODE TRANSFORM ERROR]`, error);
|
|
2497
|
+
await context.streamContent({
|
|
2498
|
+
type: "text",
|
|
2499
|
+
text: `\u26A0\uFE0F Error transforming code: ${error instanceof Error ? error.message : String(error)}
|
|
2500
|
+
`
|
|
2501
|
+
});
|
|
2502
|
+
}
|
|
2503
|
+
}
|
|
2504
|
+
let componentSection = null;
|
|
2505
|
+
if (forceCode && autoSearchComponents && designResult.content) {
|
|
2506
|
+
try {
|
|
2507
|
+
await context.streamContent({
|
|
2508
|
+
type: "text",
|
|
2509
|
+
text: "\u{1F50D} Analyzing generated code for design system components..."
|
|
2510
|
+
});
|
|
2511
|
+
const extractedComponents = ComponentExtractor.extractComponents(
|
|
2512
|
+
designResult.content
|
|
2513
|
+
);
|
|
2514
|
+
if (extractedComponents.length > 0) {
|
|
2515
|
+
await context.streamContent({
|
|
2516
|
+
type: "text",
|
|
2517
|
+
text: `\u{1F4E6} Found ${extractedComponents.length} component reference(s): ${extractedComponents.map((c) => c.tag).join(", ")}`
|
|
2518
|
+
});
|
|
2519
|
+
const lookupService = new ComponentLookupService(void 0, { namespace: PINECONE_NAMESPACE });
|
|
2520
|
+
const componentTags = extractedComponents.map((c) => c.tag);
|
|
2521
|
+
await context.streamContent({
|
|
2522
|
+
type: "text",
|
|
2523
|
+
text: `\u{1F50E} Searching component information in Pinecone...`
|
|
2524
|
+
});
|
|
2525
|
+
const lookupResults = await lookupService.lookupMultipleComponents(
|
|
2526
|
+
componentTags
|
|
2527
|
+
);
|
|
2528
|
+
const summary = ComponentLookupService.getSummary(lookupResults);
|
|
2529
|
+
for (const result of lookupResults) {
|
|
2530
|
+
if (result.found && result.component) {
|
|
2531
|
+
const comp = result.component;
|
|
2532
|
+
await context.streamContent({
|
|
2533
|
+
type: "text",
|
|
2534
|
+
text: `\u2705 **${comp.tag}** - ${comp.category}
|
|
2535
|
+
${comp.description || "No description available"}
|
|
2536
|
+
`
|
|
2537
|
+
});
|
|
2538
|
+
} else {
|
|
2539
|
+
await context.streamContent({
|
|
2540
|
+
type: "text",
|
|
2541
|
+
text: `\u26A0\uFE0F **${result.tag}** - Not found in component library
|
|
2542
|
+
`
|
|
2543
|
+
});
|
|
2544
|
+
}
|
|
2545
|
+
}
|
|
2546
|
+
let summaryText = `
|
|
2547
|
+
## \u{1F4CB} **Component Analysis Summary**
|
|
2548
|
+
|
|
2549
|
+
**Total components found in code:** ${summary.total}
|
|
2550
|
+
**Found in library:** ${summary.found}
|
|
2551
|
+
**Not found:** ${summary.notFound}
|
|
2552
|
+
|
|
2553
|
+
`;
|
|
2554
|
+
if (summary.foundComponents.length > 0) {
|
|
2555
|
+
summaryText += `### \u2705 Components Found:
|
|
2556
|
+
|
|
2557
|
+
`;
|
|
2558
|
+
summary.foundComponents.forEach((comp) => {
|
|
2559
|
+
summaryText += `- **${comp.tag}** (${comp.category}) - Score: ${comp.score.toFixed(4)}
|
|
2560
|
+
`;
|
|
2561
|
+
if (comp.description) {
|
|
2562
|
+
summaryText += ` - ${comp.description.substring(0, 100)}${comp.description.length > 100 ? "..." : ""}
|
|
2563
|
+
`;
|
|
2564
|
+
}
|
|
2565
|
+
});
|
|
2566
|
+
summaryText += `
|
|
2567
|
+
`;
|
|
2568
|
+
}
|
|
2569
|
+
if (summary.notFoundTags.length > 0) {
|
|
2570
|
+
summaryText += `### \u26A0\uFE0F Components Not Found:
|
|
2571
|
+
|
|
2572
|
+
`;
|
|
2573
|
+
summary.notFoundTags.forEach((tag) => {
|
|
2574
|
+
summaryText += `- **${tag}** - This component may not be indexed in Pinecone or may not exist
|
|
2575
|
+
`;
|
|
2576
|
+
});
|
|
2577
|
+
summaryText += `
|
|
2578
|
+
`;
|
|
2579
|
+
}
|
|
2580
|
+
summaryText += `\u{1F4A1} **Tip:** Use \`figma_suggest_components\` tool to get detailed information and usage examples for these components.
|
|
2581
|
+
|
|
2582
|
+
---
|
|
2583
|
+
|
|
2584
|
+
`;
|
|
2585
|
+
componentSection = {
|
|
2586
|
+
type: "text",
|
|
2587
|
+
text: summaryText
|
|
2588
|
+
};
|
|
2589
|
+
} else {
|
|
2590
|
+
await context.streamContent({
|
|
2591
|
+
type: "text",
|
|
2592
|
+
text: `\u2139\uFE0F No design system components found in the generated code.
|
|
2593
|
+
`
|
|
2594
|
+
});
|
|
2595
|
+
}
|
|
2596
|
+
} catch (error) {
|
|
2597
|
+
console.error(`[COMPONENT SEARCH ERROR]`, error);
|
|
2598
|
+
await context.streamContent({
|
|
2599
|
+
type: "text",
|
|
2600
|
+
text: `\u26A0\uFE0F Error searching for components: ${error instanceof Error ? error.message : String(error)}
|
|
2601
|
+
`
|
|
2602
|
+
});
|
|
2603
|
+
}
|
|
2604
|
+
}
|
|
2605
|
+
const finalContent = [header];
|
|
2606
|
+
finalContent.push(...designResult.content);
|
|
2607
|
+
if (transformedCodeSection) {
|
|
2608
|
+
finalContent.push(transformedCodeSection);
|
|
2609
|
+
}
|
|
2610
|
+
if (componentSection) {
|
|
2611
|
+
finalContent.push(componentSection);
|
|
2612
|
+
}
|
|
2613
|
+
return {
|
|
2614
|
+
content: finalContent
|
|
2615
|
+
};
|
|
2616
|
+
} catch (error) {
|
|
2617
|
+
console.error(`[DESIGN CONTEXT ERROR]`, error);
|
|
2618
|
+
return {
|
|
2619
|
+
content: [
|
|
2620
|
+
{
|
|
2621
|
+
type: "text",
|
|
2622
|
+
text: `\u274C **Failed to Get Design Context**
|
|
2623
|
+
|
|
2624
|
+
**Error:** ${error instanceof Error ? error.message : String(error)}
|
|
2625
|
+
|
|
2626
|
+
**Troubleshooting:**
|
|
2627
|
+
1. Is Figma Desktop running?
|
|
2628
|
+
2. Is the node ID valid?
|
|
2629
|
+
3. Is Figma MCP enabled? (Figma \u2192 Preferences \u2192 Enable MCP)
|
|
2630
|
+
4. Try restarting Figma Desktop
|
|
2631
|
+
|
|
2632
|
+
${error instanceof Error && error.stack ? `
|
|
2633
|
+
**Stack:**
|
|
2634
|
+
${error.stack}` : ""}`
|
|
2635
|
+
}
|
|
2636
|
+
]
|
|
2637
|
+
};
|
|
2638
|
+
}
|
|
2639
|
+
}
|
|
2640
|
+
});
|
|
2641
|
+
server.addTool({
|
|
2642
|
+
name: "figma_suggest_components",
|
|
2643
|
+
description: `**USE THIS TOOL FIRST** when generating code from Figma designs.
|
|
2644
|
+
|
|
2645
|
+
Get component suggestions from your organization's design system based on a Figma design. This tool searches the component library configured in your environment and returns specific components that match your design.
|
|
2646
|
+
|
|
2647
|
+
**CRITICAL:**
|
|
2648
|
+
- This tool returns ONLY components from your organization's design system
|
|
2649
|
+
- NEVER use generic component libraries unless explicitly found in the search results
|
|
2650
|
+
- Always call this tool BEFORE generating code from Figma designs
|
|
2651
|
+
- Use the recommended components from the response in your code generation
|
|
2652
|
+
|
|
2653
|
+
**What this does:**
|
|
2654
|
+
- Fetches design context from Figma (uses cache when available)
|
|
2655
|
+
- Extracts semantic query from the design
|
|
2656
|
+
- Searches your component library for relevant components
|
|
2657
|
+
- Processes results with Frida AI for intelligent recommendations
|
|
2658
|
+
- Returns component suggestions with code examples and usage guidelines
|
|
2659
|
+
|
|
2660
|
+
**Performance:**
|
|
2661
|
+
- Typically completes in 3-10 seconds (depending on Frida processing)
|
|
2662
|
+
- Uses cache to avoid redundant Figma calls
|
|
2663
|
+
- Component search is fast (~1-2s)
|
|
2664
|
+
- Frida AI processing adds 2-5s for intelligent suggestions
|
|
2665
|
+
|
|
2666
|
+
**Usage:**
|
|
2667
|
+
- Provide a nodeId (e.g., "315-2920") or leave empty for current selection
|
|
2668
|
+
- Optionally provide a custom query to override automatic extraction
|
|
2669
|
+
- Configure minScore and topK to control search results
|
|
2670
|
+
- Set useFrida to false to skip AI processing (faster, less intelligent)
|
|
2671
|
+
|
|
2672
|
+
**Example:**
|
|
2673
|
+
\`\`\`json
|
|
2674
|
+
{
|
|
2675
|
+
"nodeId": "315-2920",
|
|
2676
|
+
"minScore": 0.1,
|
|
2677
|
+
"topK": 5,
|
|
2678
|
+
"useFrida": true
|
|
2679
|
+
}
|
|
2680
|
+
\`\`\`
|
|
2681
|
+
|
|
2682
|
+
**Returns:**
|
|
2683
|
+
- Recommended components from Frida AI (if enabled)
|
|
2684
|
+
- Search results with components from your design system
|
|
2685
|
+
- Component details (props, events, methods, slots, usage examples)
|
|
2686
|
+
- Generated code examples with framework-specific imports`,
|
|
2687
|
+
annotations: {
|
|
2688
|
+
title: "Suggest Design System Components",
|
|
2689
|
+
readOnlyHint: true,
|
|
2690
|
+
openWorldHint: false
|
|
2691
|
+
},
|
|
2692
|
+
parameters: z.object({
|
|
2693
|
+
nodeId: z.string().optional().describe('Figma node ID (e.g., "315-2920" or "315:2920"). Leave empty for current selection.'),
|
|
2694
|
+
minScore: z.number().optional().describe("Minimum Pinecone relevance score (default: 0.05). Higher values return more relevant results."),
|
|
2695
|
+
topK: z.number().optional().describe("Maximum number of components to retrieve (default: 10)."),
|
|
2696
|
+
useFrida: z.boolean().optional().describe("Enable Frida AI processing for intelligent recommendations (default: true)."),
|
|
2697
|
+
query: z.string().optional().describe("Custom search query. If not provided, will be extracted automatically from the Figma design context.")
|
|
2698
|
+
}),
|
|
2699
|
+
execute: async (args, context) => {
|
|
2700
|
+
console.log(`
|
|
2701
|
+
[FIGMA_SUGGEST_COMPONENTS] ========================================`);
|
|
2702
|
+
console.log(`[FIGMA_SUGGEST_COMPONENTS] Tool called`);
|
|
2703
|
+
console.log(`[FIGMA_SUGGEST_COMPONENTS] nodeId: ${args.nodeId || "current selection"}`);
|
|
2704
|
+
console.log(`[FIGMA_SUGGEST_COMPONENTS] figmaConnected flag: ${figmaConnected}`);
|
|
2705
|
+
console.log(`[FIGMA_SUGGEST_COMPONENTS] ========================================`);
|
|
2706
|
+
if (!figmaConnected) {
|
|
2707
|
+
console.log(`[FIGMA_SUGGEST_COMPONENTS] \u26A0\uFE0F Early return: figmaConnected is false`);
|
|
2708
|
+
return {
|
|
2709
|
+
content: [
|
|
2710
|
+
{
|
|
2711
|
+
type: "text",
|
|
2712
|
+
text: "\u274C Not connected to Figma MCP. Check logs for connection details."
|
|
2713
|
+
}
|
|
2714
|
+
]
|
|
2715
|
+
};
|
|
2716
|
+
}
|
|
2717
|
+
console.log(`[FIGMA_SUGGEST_COMPONENTS] Proceeding with component suggestion...`);
|
|
2718
|
+
const nodeId = args.nodeId;
|
|
2719
|
+
const minScore = args.minScore ?? 0.05;
|
|
2720
|
+
const topK = args.topK ?? 10;
|
|
2721
|
+
const useFrida = args.useFrida ?? true;
|
|
2722
|
+
const customQuery = args.query;
|
|
2723
|
+
console.log(`
|
|
2724
|
+
[COMPONENT SUGGEST] Starting...`);
|
|
2725
|
+
console.log(`[COMPONENT SUGGEST] NodeId:`, nodeId || "current selection");
|
|
2726
|
+
console.log(`[COMPONENT SUGGEST] MinScore:`, minScore);
|
|
2727
|
+
console.log(`[COMPONENT SUGGEST] TopK:`, topK);
|
|
2728
|
+
console.log(`[COMPONENT SUGGEST] UseFrida:`, useFrida);
|
|
2729
|
+
await context.streamContent({
|
|
2730
|
+
type: "text",
|
|
2731
|
+
text: "\u{1F3A8} Fetching design context from Figma..."
|
|
2732
|
+
});
|
|
2733
|
+
const designCacheKey = `${nodeId || "current"}:false`;
|
|
2734
|
+
const now = Date.now();
|
|
2735
|
+
let designResult;
|
|
2736
|
+
const cachedDesign = designContextCache.get(designCacheKey);
|
|
2737
|
+
const designCacheAge = cachedDesign ? now - cachedDesign.timestamp : CACHE_TTL_MS + 1;
|
|
2738
|
+
if (cachedDesign && designCacheAge < CACHE_TTL_MS) {
|
|
2739
|
+
console.log(`[COMPONENT SUGGEST] Using cached design context`);
|
|
2740
|
+
designResult = cachedDesign.data;
|
|
2741
|
+
} else {
|
|
2742
|
+
designResult = await figmaClient.getDesignContext(nodeId, {
|
|
2743
|
+
clientLanguages: "typescript",
|
|
2744
|
+
clientFrameworks: "react",
|
|
2745
|
+
forceCode: false
|
|
2746
|
+
// Use metadata mode for faster response
|
|
2747
|
+
});
|
|
2748
|
+
designContextCache.set(designCacheKey, {
|
|
2749
|
+
data: designResult,
|
|
2750
|
+
timestamp: now,
|
|
2751
|
+
forceCode: false
|
|
2752
|
+
});
|
|
2753
|
+
}
|
|
2754
|
+
return handleComponentSuggestion({
|
|
2755
|
+
nodeId,
|
|
2756
|
+
minScore,
|
|
2757
|
+
topK,
|
|
2758
|
+
useFrida,
|
|
2759
|
+
query: customQuery,
|
|
2760
|
+
namespace: PINECONE_NAMESPACE,
|
|
2761
|
+
designContext: designResult,
|
|
2762
|
+
designContextCache,
|
|
2763
|
+
getDesignContext: figmaClient.getDesignContext.bind(figmaClient),
|
|
2764
|
+
streamContent: async (content) => {
|
|
2765
|
+
if (Array.isArray(content)) {
|
|
2766
|
+
for (const item of content) {
|
|
2767
|
+
await context.streamContent(item);
|
|
2768
|
+
}
|
|
2769
|
+
} else {
|
|
2770
|
+
await context.streamContent(content);
|
|
2771
|
+
}
|
|
2772
|
+
}
|
|
2773
|
+
});
|
|
2774
|
+
}
|
|
2775
|
+
});
|
|
2776
|
+
server.addTool({
|
|
2777
|
+
name: "figma_improve_layout",
|
|
2778
|
+
description: `Trigger layout improvement to match the Figma frame reference.
|
|
2779
|
+
|
|
2780
|
+
**What this does:**
|
|
2781
|
+
- Returns an instruction to improve the generated layout to match the Figma design
|
|
2782
|
+
- Use this tool after code generation to refine the layout
|
|
2783
|
+
- The agent will use this instruction to adjust spacing, positioning, and styling
|
|
2784
|
+
|
|
2785
|
+
**Usage:**
|
|
2786
|
+
- Call this tool after generating code from Figma
|
|
2787
|
+
- No parameters required - simply triggers the improvement instruction
|
|
2788
|
+
|
|
2789
|
+
**Example:**
|
|
2790
|
+
\`\`\`json
|
|
2791
|
+
{}
|
|
2792
|
+
\`\`\``,
|
|
2793
|
+
annotations: {
|
|
2794
|
+
title: "Improve Layout to Match Figma",
|
|
2795
|
+
readOnlyHint: false,
|
|
2796
|
+
openWorldHint: false
|
|
2797
|
+
},
|
|
2798
|
+
parameters: z.object({}),
|
|
2799
|
+
execute: async (_args, _context) => {
|
|
2800
|
+
return {
|
|
2801
|
+
content: [
|
|
2802
|
+
{
|
|
2803
|
+
type: "text",
|
|
2804
|
+
text: "#figma2frida improve the layout to match the figma frame reference"
|
|
2805
|
+
}
|
|
2806
|
+
]
|
|
2807
|
+
};
|
|
2808
|
+
}
|
|
2809
|
+
});
|
|
2810
|
+
if (HTTP_STREAM) {
|
|
2811
|
+
server.start({
|
|
2812
|
+
httpStream: {
|
|
2813
|
+
port: PORT
|
|
2814
|
+
},
|
|
2815
|
+
transportType: "httpStream"
|
|
2816
|
+
});
|
|
2817
|
+
console.log(`
|
|
2818
|
+
\u2705 Figma Proxy MCP Server running on HTTP Stream!
|
|
2819
|
+
|
|
2820
|
+
Endpoint: http://localhost:${PORT}/mcp
|
|
2821
|
+
|
|
2822
|
+
Available Tools:
|
|
2823
|
+
- figma_get_screenshot: Get visual screenshot (fast)
|
|
2824
|
+
- figma_get_design_context: Get design data and generated code (with caching & streaming)
|
|
2825
|
+
- figma_suggest_components: Suggest design system components based on Figma design (with Pinecone & Frida AI)
|
|
2826
|
+
- figma_improve_layout: Trigger layout improvement to match Figma frame reference
|
|
2827
|
+
|
|
2828
|
+
Configuration:
|
|
2829
|
+
- Pinecone Namespace: ${PINECONE_NAMESPACE || "not set (use --pinecone-namespace)"}
|
|
2830
|
+
|
|
2831
|
+
Test with curl:
|
|
2832
|
+
curl -X POST http://localhost:${PORT}/mcp \\
|
|
2833
|
+
-H "Content-Type: application/json" \\
|
|
2834
|
+
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'
|
|
2835
|
+
`);
|
|
2836
|
+
} else {
|
|
2837
|
+
server.start({ transportType: "stdio" });
|
|
2838
|
+
console.log("Started with stdio transport (for MCP clients like Cursor)");
|
|
2839
|
+
}
|
|
2840
|
+
//# sourceMappingURL=figma2frida.js.map
|