draply-dev 1.3.3 → 1.3.5

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/bin/cli.js CHANGED
@@ -97,7 +97,53 @@ function extractClassName(selector) {
97
97
  return null;
98
98
  }
99
99
 
100
- // ── Call Gemini API ───────────────────────────────────────────────────────────
100
+ // ── Call AI API (Gemini or Groq) ──────────────────────────────────────────────
101
+ function callAI(apiKey, prompt, provider) {
102
+ if (provider === 'groq') return callGroq(apiKey, prompt);
103
+ return callGemini(apiKey, prompt);
104
+ }
105
+
106
+ function callGroq(apiKey, prompt) {
107
+ return new Promise((resolve, reject) => {
108
+ const body = JSON.stringify({
109
+ model: 'llama-3.3-70b-versatile',
110
+ messages: [{ role: 'user', content: prompt }],
111
+ temperature: 0.1,
112
+ max_tokens: 8192
113
+ });
114
+
115
+ const options = {
116
+ hostname: 'api.groq.com',
117
+ path: '/openai/v1/chat/completions',
118
+ method: 'POST',
119
+ headers: {
120
+ 'Content-Type': 'application/json',
121
+ 'Authorization': `Bearer ${apiKey}`,
122
+ 'Content-Length': Buffer.byteLength(body)
123
+ }
124
+ };
125
+
126
+ const req = https.request(options, res => {
127
+ const chunks = [];
128
+ res.on('data', c => chunks.push(c));
129
+ res.on('end', () => {
130
+ try {
131
+ const data = JSON.parse(Buffer.concat(chunks).toString());
132
+ if (data.error) {
133
+ reject(new Error(data.error.message || 'Groq API error'));
134
+ return;
135
+ }
136
+ const text = data.choices?.[0]?.message?.content || '';
137
+ resolve(text);
138
+ } catch (e) { reject(e); }
139
+ });
140
+ });
141
+ req.on('error', reject);
142
+ req.write(body);
143
+ req.end();
144
+ });
145
+ }
146
+
101
147
  function callGemini(apiKey, prompt) {
102
148
  return new Promise((resolve, reject) => {
103
149
  const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-lite:generateContent?key=${apiKey}`;
@@ -216,47 +262,96 @@ const server = http.createServer((req, res) => {
216
262
  for (const [filePath, { found, items }] of fileChanges) {
217
263
  console.log(` \x1b[36m🤖\x1b[0m AI applying ${items.length} changes → ${found.relativePath}`);
218
264
 
219
- // Build combined prompt
265
+ // Find the relevant context snippet (~50 lines around the class usage)
266
+ const lines = found.content.split('\n');
267
+ let contextStart = 0, contextEnd = lines.length - 1;
268
+ for (const item of items) {
269
+ for (let i = 0; i < lines.length; i++) {
270
+ if (lines[i].includes(item.className)) {
271
+ contextStart = Math.min(contextStart || i, Math.max(0, i - 25));
272
+ contextEnd = Math.min(lines.length - 1, i + 25);
273
+ }
274
+ }
275
+ }
276
+ const snippet = lines.slice(contextStart, contextEnd + 1).join('\n');
277
+
278
+ // Build changes description
220
279
  let changesBlock = '';
221
280
  items.forEach(item => {
222
281
  const propsStr = Object.entries(item.props).map(([k, v]) => ` ${k}: ${v}`).join('\n');
223
282
  changesBlock += `\nElement .${item.className}:\n${propsStr}\n`;
224
283
  });
225
284
 
226
- const prompt = `You are a code editor. Modify the file to apply CSS style changes.
285
+ const prompt = `You are a code editor. I need to apply CSS style changes to a file.
227
286
 
228
- FILE: ${found.relativePath}
287
+ FILE: ${found.relativePath} (lines ${contextStart + 1}-${contextEnd + 1})
229
288
  \`\`\`
230
- ${found.content}
289
+ ${snippet}
231
290
  \`\`\`
232
291
 
233
- CHANGES TO APPLY:
292
+ CHANGES:
234
293
  ${changesBlock}
235
- RULES:
236
- - If CSS/SCSS file: find each class rule and update/add properties. Add rules if missing.
237
- - If JSX/TSX file: apply in the most appropriate way:
238
- - Existing inline styles → update them
239
- - File imports CSS note which CSS file to edit
240
- - Tailwind classes update className with Tailwind classes
241
- - Otherwise add style prop
242
- - Keep ALL existing code intact. Only modify what's needed.
243
- - Do NOT add extra comments.
244
-
245
- Return ONLY the complete modified file. No markdown fences, no explanations.`;
294
+ IMPORTANT: Return ONLY a JSON array of search-and-replace pairs. Each pair is:
295
+ {"search": "exact existing code to find", "replace": "modified code to replace it with"}
296
+
297
+ Rules:
298
+ - "search" must be an EXACT substring from the file above (copy it precisely)
299
+ - "search" should be minimal just the line(s) that need changing
300
+ - For CSS: modify the class rule properties. If rule doesn't exist, set search to the closing bracket of the nearest rule and add the new rule after it.
301
+ - For JSX with className: add/update the style prop on that element
302
+ - For JSX with imported CSS: add CSS rules (search for the import line, replace with import + note)
303
+ - Keep changes minimal
304
+
305
+ Example response:
306
+ [{"search": "className=\\"hero\\"", "replace": "className=\\"hero\\" style={{color: 'red', fontSize: '20px'}}"}]
307
+
308
+ Return ONLY valid JSON array. No markdown, no explanation.`;
246
309
 
247
310
  try {
248
- const result = await callGemini(cfg.apiKey, prompt);
249
- let code = result.trim();
250
- if (code.startsWith('```')) {
251
- code = code.replace(/^```[a-z]*\n?/, '').replace(/\n?```$/, '');
311
+ const result = await callAI(cfg.apiKey, prompt, cfg.provider || 'groq');
312
+ let jsonStr = result.trim();
313
+ // Clean markdown fences if present
314
+ if (jsonStr.startsWith('```')) {
315
+ jsonStr = jsonStr.replace(/^```[a-z]*\n?/, '').replace(/\n?```$/, '');
316
+ }
317
+
318
+ let patches;
319
+ try {
320
+ patches = JSON.parse(jsonStr);
321
+ } catch {
322
+ console.log(` \x1b[31m✗\x1b[0m AI returned invalid JSON`);
323
+ console.log(` Response: ${jsonStr.substring(0, 200)}`);
324
+ items.forEach(item => results.push({ selector: item.selector, ok: false, reason: 'AI returned invalid JSON' }));
325
+ continue;
252
326
  }
253
- if (code.length < 20) {
254
- items.forEach(item => results.push({ selector: item.selector, ok: false, reason: 'AI returned invalid response' }));
327
+
328
+ if (!Array.isArray(patches) || patches.length === 0) {
329
+ items.forEach(item => results.push({ selector: item.selector, ok: false, reason: 'AI returned no changes' }));
255
330
  continue;
256
331
  }
257
- fs.writeFileSync(found.file, code, 'utf8');
258
- console.log(` \x1b[32m✓\x1b[0m Applied to ${found.relativePath}`);
259
- items.forEach(item => results.push({ selector: item.selector, ok: true, file: found.relativePath }));
332
+
333
+ // Apply patches
334
+ let content = found.content;
335
+ let applied = 0;
336
+ for (const patch of patches) {
337
+ if (!patch.search || patch.replace === undefined) continue;
338
+ if (content.includes(patch.search)) {
339
+ content = content.replace(patch.search, patch.replace);
340
+ applied++;
341
+ console.log(` \x1b[32m ✓\x1b[0m Patch applied`);
342
+ } else {
343
+ console.log(` \x1b[33m ⚠\x1b[0m Search string not found: "${patch.search.substring(0, 60)}..."`);
344
+ }
345
+ }
346
+
347
+ if (applied > 0) {
348
+ fs.writeFileSync(found.file, content, 'utf8');
349
+ console.log(` \x1b[32m✓\x1b[0m Applied ${applied} patches to ${found.relativePath}`);
350
+ items.forEach(item => results.push({ selector: item.selector, ok: true, file: found.relativePath }));
351
+ } else {
352
+ items.forEach(item => results.push({ selector: item.selector, ok: false, reason: 'No patches matched' }));
353
+ }
354
+
260
355
  } catch (aiErr) {
261
356
  console.log(` \x1b[31m✗\x1b[0m AI error: ${aiErr.message}`);
262
357
  items.forEach(item => results.push({ selector: item.selector, ok: false, reason: aiErr.message }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "draply-dev",
3
- "version": "1.3.3",
3
+ "version": "1.3.5",
4
4
  "description": "Visual overlay for any frontend project — move, resize, restyle live in the browser, save to CSS",
5
5
  "author": "Arman",
6
6
  "type": "commonjs",
@@ -257,13 +257,18 @@
257
257
 
258
258
  <!-- SETTINGS -->
259
259
  <div class="ps-settings" id="__ps_settings__">
260
- <div class="ps-settings-label">Gemini API Key</div>
260
+ <div class="ps-settings-label">AI Provider</div>
261
+ <div class="ps-settings-row" style="margin-bottom:6px">
262
+ <button class="ps-eb active" id="__ps_prov_groq__" data-prov="groq" style="flex:1;text-align:center">Groq (free)</button>
263
+ <button class="ps-eb" id="__ps_prov_gemini__" data-prov="gemini" style="flex:1;text-align:center">Gemini</button>
264
+ </div>
265
+ <div class="ps-settings-label">API Key</div>
261
266
  <div class="ps-settings-row">
262
- <input class="ps-settings-input" id="__ps_apikey__" type="password" placeholder="AIza...">
267
+ <input class="ps-settings-input" id="__ps_apikey__" type="password" placeholder="gsk_...">
263
268
  <button class="ps-settings-save" id="__ps_keysave__">Save</button>
264
269
  </div>
265
270
  <div class="ps-settings-status" id="__ps_keystatus__">
266
- Get free key at <a href="https://aistudio.google.com/apikey" target="_blank" style="color:#a855f7">aistudio.google.com</a>
271
+ Free key <a href="https://console.groq.com/keys" target="_blank" style="color:#a855f7">console.groq.com</a>
267
272
  </div>
268
273
  </div>
269
274
 
@@ -313,11 +318,30 @@
313
318
  const keySaveBtn = document.getElementById('__ps_keysave__');
314
319
  const keyStatus = document.getElementById('__ps_keystatus__');
315
320
  const aiBtn = document.getElementById('__ps_ai__');
321
+ const provGroq = document.getElementById('__ps_prov_groq__');
322
+ const provGemini = document.getElementById('__ps_prov_gemini__');
323
+ let selectedProvider = 'groq';
324
+
325
+ // Provider toggle
326
+ provGroq.onclick = () => {
327
+ selectedProvider = 'groq';
328
+ provGroq.classList.add('active');
329
+ provGemini.classList.remove('active');
330
+ apiKeyInput.placeholder = 'gsk_...';
331
+ keyStatus.innerHTML = 'Free key → <a href="https://console.groq.com/keys" target="_blank" style="color:#a855f7">console.groq.com</a>';
332
+ };
333
+ provGemini.onclick = () => {
334
+ selectedProvider = 'gemini';
335
+ provGemini.classList.add('active');
336
+ provGroq.classList.remove('active');
337
+ apiKeyInput.placeholder = 'AIza...';
338
+ keyStatus.innerHTML = 'Free key → <a href="https://aistudio.google.com/apikey" target="_blank" style="color:#a855f7">aistudio.google.com</a>';
339
+ };
316
340
 
317
341
  function checkApiKey() {
318
342
  fetch('/draply-config').then(r => r.json()).then(d => {
319
343
  if (d.hasKey) {
320
- keyStatus.textContent = '✅ API key configured';
344
+ keyStatus.textContent = '✅ API key configured (' + (d.provider || 'groq') + ')';
321
345
  keyStatus.className = 'ps-settings-status ok';
322
346
  aiBtn.disabled = false;
323
347
  } else {
@@ -333,14 +357,14 @@
333
357
  fetch('/draply-config', {
334
358
  method: 'POST',
335
359
  headers: { 'Content-Type': 'application/json' },
336
- body: JSON.stringify({ apiKey: key, provider: 'gemini' })
360
+ body: JSON.stringify({ apiKey: key, provider: selectedProvider })
337
361
  }).then(r => r.json()).then(d => {
338
362
  if (d.ok) {
339
- keyStatus.textContent = '✅ API key saved!';
363
+ keyStatus.textContent = '✅ Saved! (' + selectedProvider + ')';
340
364
  keyStatus.className = 'ps-settings-status ok';
341
365
  apiKeyInput.value = '';
342
366
  aiBtn.disabled = false;
343
- showToast('🔑 API key saved');
367
+ showToast('🔑 API key saved (' + selectedProvider + ')');
344
368
  }
345
369
  });
346
370
  };