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 +121 -26
- package/package.json +1 -1
- package/src/draply-features.js +31 -7
package/bin/cli.js
CHANGED
|
@@ -97,7 +97,53 @@ function extractClassName(selector) {
|
|
|
97
97
|
return null;
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
// ── Call
|
|
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
|
-
//
|
|
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.
|
|
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
|
-
${
|
|
289
|
+
${snippet}
|
|
231
290
|
\`\`\`
|
|
232
291
|
|
|
233
|
-
CHANGES
|
|
292
|
+
CHANGES:
|
|
234
293
|
${changesBlock}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
-
|
|
243
|
-
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
|
249
|
-
let
|
|
250
|
-
if
|
|
251
|
-
|
|
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
|
-
|
|
254
|
-
|
|
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
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
package/src/draply-features.js
CHANGED
|
@@ -257,13 +257,18 @@
|
|
|
257
257
|
|
|
258
258
|
<!-- SETTINGS -->
|
|
259
259
|
<div class="ps-settings" id="__ps_settings__">
|
|
260
|
-
<div class="ps-settings-label">
|
|
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="
|
|
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
|
-
|
|
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:
|
|
360
|
+
body: JSON.stringify({ apiKey: key, provider: selectedProvider })
|
|
337
361
|
}).then(r => r.json()).then(d => {
|
|
338
362
|
if (d.ok) {
|
|
339
|
-
keyStatus.textContent = '✅
|
|
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
|
};
|