@cccarv82/freya 3.0.0 → 3.2.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/cli/web-ui.css +0 -132
- package/cli/web-ui.js +167 -213
- package/cli/web.js +114 -94
- package/package.json +1 -1
package/cli/web-ui.css
CHANGED
|
@@ -1279,134 +1279,6 @@ textarea:focus {
|
|
|
1279
1279
|
background: rgba(0, 0, 0, .18);
|
|
1280
1280
|
}
|
|
1281
1281
|
|
|
1282
|
-
/* ── Image Lightbox Modal ── */
|
|
1283
|
-
.img-lightbox {
|
|
1284
|
-
display: none;
|
|
1285
|
-
position: fixed;
|
|
1286
|
-
inset: 0;
|
|
1287
|
-
z-index: 10000;
|
|
1288
|
-
background: rgba(0, 0, 0, 0.85);
|
|
1289
|
-
backdrop-filter: blur(8px);
|
|
1290
|
-
-webkit-backdrop-filter: blur(8px);
|
|
1291
|
-
justify-content: center;
|
|
1292
|
-
align-items: center;
|
|
1293
|
-
cursor: zoom-out;
|
|
1294
|
-
animation: lbFadeIn 0.15s ease;
|
|
1295
|
-
}
|
|
1296
|
-
|
|
1297
|
-
.img-lightbox.active {
|
|
1298
|
-
display: flex;
|
|
1299
|
-
}
|
|
1300
|
-
|
|
1301
|
-
@keyframes lbFadeIn {
|
|
1302
|
-
from { opacity: 0; }
|
|
1303
|
-
to { opacity: 1; }
|
|
1304
|
-
}
|
|
1305
|
-
|
|
1306
|
-
.img-lightbox img {
|
|
1307
|
-
max-width: 92vw;
|
|
1308
|
-
max-height: 88vh;
|
|
1309
|
-
border-radius: 10px;
|
|
1310
|
-
box-shadow: 0 8px 40px rgba(0, 0, 0, 0.5);
|
|
1311
|
-
object-fit: contain;
|
|
1312
|
-
cursor: default;
|
|
1313
|
-
animation: lbZoomIn 0.2s ease;
|
|
1314
|
-
}
|
|
1315
|
-
|
|
1316
|
-
@keyframes lbZoomIn {
|
|
1317
|
-
from { transform: scale(0.9); opacity: 0; }
|
|
1318
|
-
to { transform: scale(1); opacity: 1; }
|
|
1319
|
-
}
|
|
1320
|
-
|
|
1321
|
-
.img-lightbox-close {
|
|
1322
|
-
position: absolute;
|
|
1323
|
-
top: 16px;
|
|
1324
|
-
right: 20px;
|
|
1325
|
-
width: 36px;
|
|
1326
|
-
height: 36px;
|
|
1327
|
-
border-radius: 50%;
|
|
1328
|
-
background: rgba(255, 255, 255, 0.12);
|
|
1329
|
-
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
1330
|
-
color: #fff;
|
|
1331
|
-
font-size: 20px;
|
|
1332
|
-
cursor: pointer;
|
|
1333
|
-
display: flex;
|
|
1334
|
-
align-items: center;
|
|
1335
|
-
justify-content: center;
|
|
1336
|
-
transition: background 0.15s;
|
|
1337
|
-
}
|
|
1338
|
-
|
|
1339
|
-
.img-lightbox-close:hover {
|
|
1340
|
-
background: rgba(255, 255, 255, 0.25);
|
|
1341
|
-
}
|
|
1342
|
-
|
|
1343
|
-
.img-lightbox-name {
|
|
1344
|
-
position: absolute;
|
|
1345
|
-
bottom: 16px;
|
|
1346
|
-
left: 50%;
|
|
1347
|
-
transform: translateX(-50%);
|
|
1348
|
-
color: rgba(255, 255, 255, 0.7);
|
|
1349
|
-
font-size: 12px;
|
|
1350
|
-
background: rgba(0, 0, 0, 0.4);
|
|
1351
|
-
padding: 4px 14px;
|
|
1352
|
-
border-radius: 20px;
|
|
1353
|
-
max-width: 80vw;
|
|
1354
|
-
overflow: hidden;
|
|
1355
|
-
text-overflow: ellipsis;
|
|
1356
|
-
white-space: nowrap;
|
|
1357
|
-
}
|
|
1358
|
-
|
|
1359
|
-
/* ── Paste Preview Strip ── */
|
|
1360
|
-
.paste-thumb {
|
|
1361
|
-
position: relative;
|
|
1362
|
-
display: inline-block;
|
|
1363
|
-
}
|
|
1364
|
-
|
|
1365
|
-
.paste-thumb-remove {
|
|
1366
|
-
position: absolute;
|
|
1367
|
-
top: -6px;
|
|
1368
|
-
right: -6px;
|
|
1369
|
-
width: 18px;
|
|
1370
|
-
height: 18px;
|
|
1371
|
-
border-radius: 50%;
|
|
1372
|
-
background: var(--danger, #ef4444);
|
|
1373
|
-
color: #fff;
|
|
1374
|
-
border: none;
|
|
1375
|
-
font-size: 12px;
|
|
1376
|
-
line-height: 18px;
|
|
1377
|
-
text-align: center;
|
|
1378
|
-
cursor: pointer;
|
|
1379
|
-
padding: 0;
|
|
1380
|
-
box-shadow: 0 1px 3px rgba(0,0,0,0.3);
|
|
1381
|
-
}
|
|
1382
|
-
|
|
1383
|
-
.paste-thumb-remove:hover {
|
|
1384
|
-
background: #dc2626;
|
|
1385
|
-
transform: scale(1.1);
|
|
1386
|
-
}
|
|
1387
|
-
|
|
1388
|
-
/* ── Bubble Attachments (images in chat) ── */
|
|
1389
|
-
.bubble-attachments {
|
|
1390
|
-
margin-top: 8px;
|
|
1391
|
-
display: flex;
|
|
1392
|
-
gap: 6px;
|
|
1393
|
-
flex-wrap: wrap;
|
|
1394
|
-
}
|
|
1395
|
-
|
|
1396
|
-
.bubble-img {
|
|
1397
|
-
max-height: 120px;
|
|
1398
|
-
max-width: 200px;
|
|
1399
|
-
border-radius: 8px;
|
|
1400
|
-
border: 1px solid var(--border);
|
|
1401
|
-
cursor: pointer;
|
|
1402
|
-
transition: transform 0.15s ease;
|
|
1403
|
-
}
|
|
1404
|
-
|
|
1405
|
-
.bubble-img:hover {
|
|
1406
|
-
transform: scale(1.05);
|
|
1407
|
-
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
|
|
1408
|
-
}
|
|
1409
|
-
|
|
1410
1282
|
.chatComposer {
|
|
1411
1283
|
display: none;
|
|
1412
1284
|
}
|
|
@@ -1741,10 +1613,6 @@ textarea:focus {
|
|
|
1741
1613
|
font-size: 11px;
|
|
1742
1614
|
}
|
|
1743
1615
|
|
|
1744
|
-
* {
|
|
1745
|
-
border-radius: 0 !important;
|
|
1746
|
-
}
|
|
1747
|
-
|
|
1748
1616
|
/* ── Kanban Board ── */
|
|
1749
1617
|
.kanban-board {
|
|
1750
1618
|
display: grid;
|
package/cli/web-ui.js
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
timelineTag: '',
|
|
22
22
|
chatSessionId: null,
|
|
23
23
|
chatLoaded: false,
|
|
24
|
-
|
|
24
|
+
pendingImage: null // { filename, url, mimeType } from clipboard paste
|
|
25
25
|
};
|
|
26
26
|
|
|
27
27
|
function applyDarkTheme() {
|
|
@@ -196,11 +196,12 @@
|
|
|
196
196
|
var num = 0;
|
|
197
197
|
|
|
198
198
|
// Match append_daily_log / appenddailylog actions
|
|
199
|
-
var logRe = /"type"\s*:\s*"append_?daily_?log"\s*,\s*"text"\s*:\s*"([^"]{1,
|
|
199
|
+
var logRe = /"type"\s*:\s*"append_?daily_?log"\s*,\s*"text"\s*:\s*"([^"]{1,300})/gi;
|
|
200
200
|
var m;
|
|
201
201
|
while ((m = logRe.exec(text)) !== null) {
|
|
202
202
|
num++;
|
|
203
|
-
|
|
203
|
+
var t = m[1].slice(0, 140);
|
|
204
|
+
lines.push(num + '. \u{1F4DD} **Registrar no log:** ' + t + (m[1].length > 140 ? '...' : ''));
|
|
204
205
|
}
|
|
205
206
|
|
|
206
207
|
// Match create_task actions
|
|
@@ -246,8 +247,8 @@
|
|
|
246
247
|
var num = i + 1;
|
|
247
248
|
|
|
248
249
|
if (type === 'appenddailylog') {
|
|
249
|
-
var t = String(a.text || '');
|
|
250
|
-
return num + '. ' + icon + ' **Registrar no log:** ' + t;
|
|
250
|
+
var t = String(a.text || '').slice(0, 140);
|
|
251
|
+
return num + '. ' + icon + ' **Registrar no log:** ' + t + (String(a.text || '').length > 140 ? '...' : '');
|
|
251
252
|
}
|
|
252
253
|
if (type === 'createtask') {
|
|
253
254
|
var desc = String(a.description || '').slice(0, 120);
|
|
@@ -503,32 +504,32 @@
|
|
|
503
504
|
const tag = $('chatModeTag');
|
|
504
505
|
if (tag) { tag.textContent = '🔍 oracle'; tag.style.display = ''; tag.style.color = 'var(--accent)'; tag.style.borderColor = 'var(--accent)'; }
|
|
505
506
|
|
|
506
|
-
//
|
|
507
|
-
var
|
|
508
|
-
if (
|
|
509
|
-
|
|
510
|
-
}
|
|
511
|
-
if (askAttachments.length) {
|
|
512
|
-
var askHtml = escapeHtml(query).replace(/\n/g, '<br>');
|
|
513
|
-
askHtml += '<div class="bubble-attachments">';
|
|
514
|
-
for (var ai = 0; ai < askAttachments.length; ai++) {
|
|
515
|
-
askHtml += '<img src="/attachments/' + askAttachments[ai].path.replace('data/attachments/', '') + '" class="bubble-img" alt="' + escapeHtml(askAttachments[ai].name) + '" />';
|
|
516
|
-
}
|
|
517
|
-
askHtml += '</div>';
|
|
518
|
-
chatAppend('user', askHtml, { html: true });
|
|
507
|
+
// Include pasted image if present
|
|
508
|
+
var pendingImg = state.pendingImage;
|
|
509
|
+
if (pendingImg) {
|
|
510
|
+
chatAppend('user', escapeHtml(query) + '<br><img src="' + pendingImg.url + '" style="max-height:80px;border-radius:4px;margin-top:6px;cursor:pointer;" onclick="openLightbox(\'' + pendingImg.url + '\')" />', { html: true });
|
|
519
511
|
} else {
|
|
520
512
|
chatAppend('user', query);
|
|
521
513
|
}
|
|
522
|
-
clearPastedImages();
|
|
523
514
|
syncChatThreadVisibility();
|
|
524
|
-
|
|
515
|
+
|
|
516
|
+
// Clear image preview
|
|
517
|
+
var preview = document.getElementById('pastePreview');
|
|
518
|
+
if (preview) preview.remove();
|
|
519
|
+
|
|
520
|
+
setPill('run', 'pesquisando…');
|
|
525
521
|
|
|
526
522
|
const typingId = 'typing-' + Date.now();
|
|
527
523
|
chatAppend('assistant', '<div class="typing-indicator"><span></span><span></span><span></span></div>', { id: typingId, html: true });
|
|
528
524
|
|
|
529
525
|
try {
|
|
530
526
|
const sessionId = ensureChatSession();
|
|
531
|
-
|
|
527
|
+
var askPayload = { dir: dirOrDefault(), sessionId, query };
|
|
528
|
+
if (pendingImg) {
|
|
529
|
+
askPayload.imagePath = 'data/attachments/' + pendingImg.filename;
|
|
530
|
+
}
|
|
531
|
+
state.pendingImage = null;
|
|
532
|
+
const r = await api('/api/chat/ask', askPayload);
|
|
532
533
|
const answer = r && r.answer ? r.answer : 'Não encontrei registro';
|
|
533
534
|
|
|
534
535
|
const el = $(typingId);
|
|
@@ -2362,42 +2363,36 @@
|
|
|
2362
2363
|
const tag = $('chatModeTag');
|
|
2363
2364
|
if (tag) { tag.textContent = '📥 inbox'; tag.style.display = ''; tag.style.color = 'var(--primary)'; tag.style.borderColor = 'var(--primary)'; }
|
|
2364
2365
|
|
|
2365
|
-
//
|
|
2366
|
-
var
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
setPill('run', 'enviando imagens…');
|
|
2370
|
-
attachments = await uploadPastedImages();
|
|
2371
|
-
}
|
|
2372
|
-
|
|
2373
|
-
// Show user message with image thumbnails
|
|
2374
|
-
var userHtml = escapeHtml(text).replace(/\n/g, '<br>');
|
|
2375
|
-
if (attachments.length) {
|
|
2376
|
-
userHtml += '<div class="bubble-attachments">';
|
|
2377
|
-
for (var ai = 0; ai < attachments.length; ai++) {
|
|
2378
|
-
userHtml += '<img src="/attachments/' + attachments[ai].path.replace('data/attachments/', '') + '" class="bubble-img" alt="' + escapeHtml(attachments[ai].name) + '" />';
|
|
2379
|
-
}
|
|
2380
|
-
userHtml += '</div>';
|
|
2381
|
-
chatAppend('user', userHtml, { html: true });
|
|
2366
|
+
// Include pasted image if present
|
|
2367
|
+
var pendingImg = state.pendingImage;
|
|
2368
|
+
if (pendingImg) {
|
|
2369
|
+
chatAppend('user', escapeHtml(text) + '<br><img src="' + pendingImg.url + '" style="max-height:80px;border-radius:4px;margin-top:6px;cursor:pointer;" onclick="openLightbox(\'' + pendingImg.url + '\')" />', { html: true });
|
|
2382
2370
|
} else {
|
|
2383
2371
|
chatAppend('user', text);
|
|
2384
2372
|
}
|
|
2385
|
-
clearPastedImages();
|
|
2386
2373
|
syncChatThreadVisibility();
|
|
2387
2374
|
|
|
2388
|
-
//
|
|
2389
|
-
var
|
|
2390
|
-
|
|
2375
|
+
// Clear image preview
|
|
2376
|
+
var preview = document.getElementById('pastePreview');
|
|
2377
|
+
if (preview) preview.remove();
|
|
2378
|
+
|
|
2379
|
+
// Show typing indicator while processing
|
|
2380
|
+
var typingId = 'typing-' + Date.now();
|
|
2381
|
+
var typingHtml = '<div class="typing-indicator"><span></span><span></span><span></span></div>';
|
|
2382
|
+
chatAppend('assistant', typingHtml, { id: typingId, html: true });
|
|
2391
2383
|
|
|
2392
2384
|
setPill('run', 'salvando…');
|
|
2393
|
-
|
|
2385
|
+
var inboxPayload = { dir: dirOrDefault(), text };
|
|
2386
|
+
if (pendingImg) inboxPayload.imagePath = 'data/attachments/' + pendingImg.filename;
|
|
2387
|
+
state.pendingImage = null;
|
|
2388
|
+
await api('/api/inbox/add', inboxPayload);
|
|
2394
2389
|
|
|
2395
|
-
setPill('run',
|
|
2396
|
-
const r = await api('/api/agents/plan', { dir: dirOrDefault(), text
|
|
2390
|
+
setPill('run', 'processando…');
|
|
2391
|
+
const r = await api('/api/agents/plan', { dir: dirOrDefault(), text });
|
|
2397
2392
|
|
|
2398
2393
|
// Remove typing indicator
|
|
2399
|
-
var
|
|
2400
|
-
if (
|
|
2394
|
+
var typingEl = $(typingId);
|
|
2395
|
+
if (typingEl) typingEl.remove();
|
|
2401
2396
|
|
|
2402
2397
|
state.lastPlan = r.plan || '';
|
|
2403
2398
|
|
|
@@ -2416,12 +2411,8 @@
|
|
|
2416
2411
|
}
|
|
2417
2412
|
|
|
2418
2413
|
if (state.autoApply) {
|
|
2419
|
-
|
|
2420
|
-
chatAppend('assistant', '<div class="typing-indicator"><span></span><span></span><span></span></div>', { id: applyTypingId, html: true });
|
|
2421
|
-
setPill('run', 'aplicando…');
|
|
2414
|
+
setPill('run', 'applying…');
|
|
2422
2415
|
await applyPlan();
|
|
2423
|
-
var applyEl = $(applyTypingId);
|
|
2424
|
-
if (applyEl) applyEl.remove();
|
|
2425
2416
|
const a = state.lastApplied || {};
|
|
2426
2417
|
setPill('ok', `applied(${a.tasks || 0}t, ${a.blockers || 0}b)`);
|
|
2427
2418
|
if (state.autoRunReports) {
|
|
@@ -2485,23 +2476,40 @@
|
|
|
2485
2476
|
state.lastApplied = r.applied || null;
|
|
2486
2477
|
const summary = r.applied || {};
|
|
2487
2478
|
|
|
2488
|
-
// Build
|
|
2479
|
+
// Build natural language summary instead of raw JSON
|
|
2489
2480
|
var parts = [];
|
|
2490
|
-
var
|
|
2491
|
-
var
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
if (
|
|
2496
|
-
if (
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
if (
|
|
2500
|
-
|
|
2501
|
-
|
|
2481
|
+
var tc = (summary.tasks || 0), bc = (summary.blockers || 0);
|
|
2482
|
+
var ts = (summary.tasksSkipped || 0), bs = (summary.blockersSkipped || 0);
|
|
2483
|
+
if (tc > 0) parts.push('**' + tc + ' tarefa' + (tc > 1 ? 's' : '') + '** registrada' + (tc > 1 ? 's' : ''));
|
|
2484
|
+
if (bc > 0) parts.push('**' + bc + ' blocker' + (bc > 1 ? 's' : '') + '** registrado' + (bc > 1 ? 's' : ''));
|
|
2485
|
+
if (ts > 0) parts.push(ts + ' tarefa' + (ts > 1 ? 's' : '') + ' já existente' + (ts > 1 ? 's' : '') + ' (ignorada' + (ts > 1 ? 's' : '') + ')');
|
|
2486
|
+
if (bs > 0) parts.push(bs + ' blocker' + (bs > 1 ? 's' : '') + ' já existente' + (bs > 1 ? 's' : '') + ' (ignorado' + (bs > 1 ? 's' : '') + ')');
|
|
2487
|
+
if (summary.mode) parts.push('Modo: **' + summary.mode + '**');
|
|
2488
|
+
|
|
2489
|
+
var oq = summary.oracleQueries;
|
|
2490
|
+
if (oq && Array.isArray(oq) && oq.length > 0) {
|
|
2491
|
+
parts.push(oq.length + ' consulta' + (oq.length > 1 ? 's' : '') + ' realizada' + (oq.length > 1 ? 's' : '') + ' no histórico');
|
|
2492
|
+
}
|
|
2493
|
+
|
|
2494
|
+
let msg = '## Resultado\n\n';
|
|
2495
|
+
if (parts.length > 0) {
|
|
2496
|
+
msg += parts.join(' · ') + '\n';
|
|
2497
|
+
} else if (tc === 0 && bc === 0) {
|
|
2498
|
+
msg += 'Contexto registrado no log diário. Nenhuma tarefa ou blocker identificado.\n';
|
|
2499
|
+
}
|
|
2500
|
+
|
|
2501
|
+
// Show semantic duplicates detected
|
|
2502
|
+
if (summary && Array.isArray(summary.semanticDups) && summary.semanticDups.length > 0) {
|
|
2503
|
+
msg += '\n⚠️ **Duplicatas detectadas** (não criadas):\n';
|
|
2504
|
+
for (var di = 0; di < summary.semanticDups.length; di++) {
|
|
2505
|
+
var dup = summary.semanticDups[di];
|
|
2506
|
+
msg += '- "' + dup.newDesc + '" → já existe: "' + dup.existingDesc + '" (' + dup.similarity + ' similar)\n';
|
|
2507
|
+
}
|
|
2508
|
+
}
|
|
2502
2509
|
|
|
2503
2510
|
if (summary && Array.isArray(summary.reportsSuggested) && summary.reportsSuggested.length) {
|
|
2504
|
-
msg += '\n
|
|
2511
|
+
msg += '\n**Relatórios sugeridos:** ' + summary.reportsSuggested.join(', ');
|
|
2512
|
+
msg += '\n\nUse: **Rodar relatórios sugeridos** (barra lateral)';
|
|
2505
2513
|
}
|
|
2506
2514
|
|
|
2507
2515
|
setOut(msg);
|
|
@@ -2527,6 +2535,98 @@
|
|
|
2527
2535
|
loadLocal();
|
|
2528
2536
|
wireRailNav();
|
|
2529
2537
|
|
|
2538
|
+
// ── Image paste (Ctrl+V) support ──
|
|
2539
|
+
(function setupImagePaste() {
|
|
2540
|
+
var ta = $('inboxText');
|
|
2541
|
+
if (!ta) return;
|
|
2542
|
+
|
|
2543
|
+
ta.addEventListener('paste', async function(e) {
|
|
2544
|
+
var items = e.clipboardData && e.clipboardData.items;
|
|
2545
|
+
if (!items) return;
|
|
2546
|
+
for (var i = 0; i < items.length; i++) {
|
|
2547
|
+
if (items[i].type.indexOf('image') !== -1) {
|
|
2548
|
+
e.preventDefault();
|
|
2549
|
+
var file = items[i].getAsFile();
|
|
2550
|
+
if (!file) return;
|
|
2551
|
+
var reader = new FileReader();
|
|
2552
|
+
reader.onload = async function(ev) {
|
|
2553
|
+
var base64 = ev.target.result.split(',')[1];
|
|
2554
|
+
var mimeType = file.type || 'image/png';
|
|
2555
|
+
try {
|
|
2556
|
+
var r = await api('/api/attachments/upload', { dir: dirOrDefault(), data: base64, mimeType: mimeType });
|
|
2557
|
+
if (r && r.ok) {
|
|
2558
|
+
state.pendingImage = { filename: r.filename, url: r.url, mimeType: mimeType };
|
|
2559
|
+
showImagePreview(r.url, r.filename);
|
|
2560
|
+
}
|
|
2561
|
+
} catch (err) {
|
|
2562
|
+
console.error('Image upload failed:', err);
|
|
2563
|
+
}
|
|
2564
|
+
};
|
|
2565
|
+
reader.readAsDataURL(file);
|
|
2566
|
+
break;
|
|
2567
|
+
}
|
|
2568
|
+
}
|
|
2569
|
+
});
|
|
2570
|
+
})();
|
|
2571
|
+
|
|
2572
|
+
function showImagePreview(url, filename) {
|
|
2573
|
+
var existing = document.getElementById('pastePreview');
|
|
2574
|
+
if (existing) existing.remove();
|
|
2575
|
+
var ta = $('inboxText');
|
|
2576
|
+
if (!ta) return;
|
|
2577
|
+
var container = document.createElement('div');
|
|
2578
|
+
container.id = 'pastePreview';
|
|
2579
|
+
container.style.cssText = 'display:flex; align-items:center; gap:8px; padding:6px 10px; background:var(--surface); border:1px solid var(--border); border-radius:6px; margin-top:6px;';
|
|
2580
|
+
var img = document.createElement('img');
|
|
2581
|
+
img.src = url;
|
|
2582
|
+
img.style.cssText = 'max-height:48px; max-width:80px; border-radius:4px; cursor:pointer;';
|
|
2583
|
+
img.onclick = function() { openLightbox(url); };
|
|
2584
|
+
var label = document.createElement('span');
|
|
2585
|
+
label.textContent = '📎 ' + filename;
|
|
2586
|
+
label.style.cssText = 'font-size:12px; color:var(--muted); flex:1;';
|
|
2587
|
+
var removeBtn = document.createElement('button');
|
|
2588
|
+
removeBtn.textContent = '✕';
|
|
2589
|
+
removeBtn.style.cssText = 'background:none; border:none; color:var(--muted); cursor:pointer; font-size:14px;';
|
|
2590
|
+
removeBtn.onclick = function() { container.remove(); state.pendingImage = null; };
|
|
2591
|
+
container.appendChild(img);
|
|
2592
|
+
container.appendChild(label);
|
|
2593
|
+
container.appendChild(removeBtn);
|
|
2594
|
+
ta.parentElement.insertBefore(container, ta.nextSibling);
|
|
2595
|
+
}
|
|
2596
|
+
|
|
2597
|
+
// ── Image lightbox modal ──
|
|
2598
|
+
function openLightbox(url) {
|
|
2599
|
+
var existing = document.getElementById('freyaLightbox');
|
|
2600
|
+
if (existing) existing.remove();
|
|
2601
|
+
var overlay = document.createElement('div');
|
|
2602
|
+
overlay.id = 'freyaLightbox';
|
|
2603
|
+
overlay.style.cssText = 'position:fixed; inset:0; background:rgba(0,0,0,0.85); display:flex; align-items:center; justify-content:center; z-index:99999; cursor:pointer;';
|
|
2604
|
+
overlay.onclick = function(e) { if (e.target === overlay) overlay.remove(); };
|
|
2605
|
+
var img = document.createElement('img');
|
|
2606
|
+
img.src = url;
|
|
2607
|
+
img.style.cssText = 'max-width:90vw; max-height:90vh; border-radius:8px; box-shadow:0 8px 40px rgba(0,0,0,0.6);';
|
|
2608
|
+
var closeBtn = document.createElement('button');
|
|
2609
|
+
closeBtn.textContent = '✕';
|
|
2610
|
+
closeBtn.style.cssText = 'position:absolute; top:20px; right:20px; background:rgba(255,255,255,0.15); border:none; color:white; font-size:24px; width:40px; height:40px; border-radius:50%; cursor:pointer;';
|
|
2611
|
+
closeBtn.onclick = function() { overlay.remove(); };
|
|
2612
|
+
overlay.appendChild(img);
|
|
2613
|
+
overlay.appendChild(closeBtn);
|
|
2614
|
+
document.body.appendChild(overlay);
|
|
2615
|
+
// ESC to close
|
|
2616
|
+
function onEsc(e) { if (e.key === 'Escape') { overlay.remove(); document.removeEventListener('keydown', onEsc); } }
|
|
2617
|
+
document.addEventListener('keydown', onEsc);
|
|
2618
|
+
}
|
|
2619
|
+
window.openLightbox = openLightbox;
|
|
2620
|
+
|
|
2621
|
+
// Make all chat images clickable for lightbox (delegate)
|
|
2622
|
+
document.addEventListener('click', function(e) {
|
|
2623
|
+
var img = e.target;
|
|
2624
|
+
if (img.tagName === 'IMG' && img.closest && img.closest('#chatThread')) {
|
|
2625
|
+
e.preventDefault();
|
|
2626
|
+
openLightbox(img.src);
|
|
2627
|
+
}
|
|
2628
|
+
});
|
|
2629
|
+
|
|
2530
2630
|
// Developer drawer (persist open/close)
|
|
2531
2631
|
try {
|
|
2532
2632
|
const d = $('devDrawer');
|
|
@@ -2616,149 +2716,8 @@
|
|
|
2616
2716
|
refreshToday();
|
|
2617
2717
|
reloadSlugRules();
|
|
2618
2718
|
loadChatHistory();
|
|
2619
|
-
initPasteHandler();
|
|
2620
|
-
|
|
2621
|
-
// Click on chat images → open lightbox
|
|
2622
|
-
var thread = $('chatThread');
|
|
2623
|
-
if (thread) {
|
|
2624
|
-
thread.addEventListener('click', function(e) {
|
|
2625
|
-
if (e.target && e.target.classList && e.target.classList.contains('bubble-img')) {
|
|
2626
|
-
openLightbox(e.target.src, e.target.alt || 'Anexo');
|
|
2627
|
-
}
|
|
2628
|
-
});
|
|
2629
|
-
}
|
|
2630
2719
|
})();
|
|
2631
2720
|
|
|
2632
|
-
// ── Image Paste Handler ──────────────────────────────────────
|
|
2633
|
-
function initPasteHandler() {
|
|
2634
|
-
var ta = $('inboxText');
|
|
2635
|
-
if (!ta) return;
|
|
2636
|
-
|
|
2637
|
-
ta.addEventListener('paste', function(e) {
|
|
2638
|
-
var items = e.clipboardData && e.clipboardData.items;
|
|
2639
|
-
if (!items) return;
|
|
2640
|
-
|
|
2641
|
-
for (var i = 0; i < items.length; i++) {
|
|
2642
|
-
if (items[i].type.indexOf('image/') === 0) {
|
|
2643
|
-
e.preventDefault();
|
|
2644
|
-
var file = items[i].getAsFile();
|
|
2645
|
-
if (!file) continue;
|
|
2646
|
-
|
|
2647
|
-
var reader = new FileReader();
|
|
2648
|
-
reader.onload = function(ev) {
|
|
2649
|
-
var ext = file.type.split('/')[1] || 'png';
|
|
2650
|
-
if (ext === 'jpeg') ext = 'jpg';
|
|
2651
|
-
var ts = Date.now();
|
|
2652
|
-
var name = 'paste-' + ts + '.' + ext;
|
|
2653
|
-
state.pastedImages.push({ file: file, dataUrl: ev.target.result, name: name });
|
|
2654
|
-
renderPastePreview();
|
|
2655
|
-
};
|
|
2656
|
-
reader.readAsDataURL(file);
|
|
2657
|
-
break; // handle one image per paste
|
|
2658
|
-
}
|
|
2659
|
-
}
|
|
2660
|
-
});
|
|
2661
|
-
}
|
|
2662
|
-
|
|
2663
|
-
function renderPastePreview() {
|
|
2664
|
-
var strip = $('pastePreview');
|
|
2665
|
-
var inner = $('pastePreviewInner');
|
|
2666
|
-
if (!strip || !inner) return;
|
|
2667
|
-
|
|
2668
|
-
if (!state.pastedImages.length) {
|
|
2669
|
-
strip.style.display = 'none';
|
|
2670
|
-
inner.innerHTML = '';
|
|
2671
|
-
return;
|
|
2672
|
-
}
|
|
2673
|
-
|
|
2674
|
-
strip.style.display = 'block';
|
|
2675
|
-
inner.innerHTML = '';
|
|
2676
|
-
|
|
2677
|
-
for (var i = 0; i < state.pastedImages.length; i++) {
|
|
2678
|
-
(function(idx) {
|
|
2679
|
-
var img = state.pastedImages[idx];
|
|
2680
|
-
var wrap = document.createElement('div');
|
|
2681
|
-
wrap.className = 'paste-thumb';
|
|
2682
|
-
|
|
2683
|
-
var thumb = document.createElement('img');
|
|
2684
|
-
thumb.src = img.dataUrl;
|
|
2685
|
-
thumb.alt = img.name;
|
|
2686
|
-
thumb.style.cssText = 'max-height:60px; max-width:100px; border-radius:6px; border:1px solid var(--border); cursor:pointer;';
|
|
2687
|
-
thumb.title = img.name;
|
|
2688
|
-
thumb.onclick = function() {
|
|
2689
|
-
openLightbox(img.dataUrl, img.name);
|
|
2690
|
-
};
|
|
2691
|
-
|
|
2692
|
-
var removeBtn = document.createElement('button');
|
|
2693
|
-
removeBtn.className = 'paste-thumb-remove';
|
|
2694
|
-
removeBtn.innerHTML = '×';
|
|
2695
|
-
removeBtn.title = 'Remover';
|
|
2696
|
-
removeBtn.onclick = function() {
|
|
2697
|
-
state.pastedImages.splice(idx, 1);
|
|
2698
|
-
renderPastePreview();
|
|
2699
|
-
};
|
|
2700
|
-
|
|
2701
|
-
wrap.appendChild(thumb);
|
|
2702
|
-
wrap.appendChild(removeBtn);
|
|
2703
|
-
inner.appendChild(wrap);
|
|
2704
|
-
})(i);
|
|
2705
|
-
}
|
|
2706
|
-
|
|
2707
|
-
// label
|
|
2708
|
-
var label = document.createElement('span');
|
|
2709
|
-
label.style.cssText = 'font-size:11px; color:var(--faint); margin-left:4px;';
|
|
2710
|
-
label.textContent = state.pastedImages.length + ' imagem(ns) anexada(s)';
|
|
2711
|
-
inner.appendChild(label);
|
|
2712
|
-
}
|
|
2713
|
-
|
|
2714
|
-
// Upload pasted images and return array of { name, path }
|
|
2715
|
-
async function uploadPastedImages() {
|
|
2716
|
-
var results = [];
|
|
2717
|
-
for (var i = 0; i < state.pastedImages.length; i++) {
|
|
2718
|
-
var img = state.pastedImages[i];
|
|
2719
|
-
try {
|
|
2720
|
-
var r = await api('/api/attachments/upload', {
|
|
2721
|
-
dir: dirOrDefault(),
|
|
2722
|
-
data: img.dataUrl,
|
|
2723
|
-
filename: img.name
|
|
2724
|
-
});
|
|
2725
|
-
if (r && r.ok) {
|
|
2726
|
-
results.push({ name: img.name, path: r.path });
|
|
2727
|
-
}
|
|
2728
|
-
} catch(err) {
|
|
2729
|
-
// best-effort, skip failed uploads
|
|
2730
|
-
}
|
|
2731
|
-
}
|
|
2732
|
-
return results;
|
|
2733
|
-
}
|
|
2734
|
-
|
|
2735
|
-
function clearPastedImages() {
|
|
2736
|
-
state.pastedImages = [];
|
|
2737
|
-
renderPastePreview();
|
|
2738
|
-
}
|
|
2739
|
-
|
|
2740
|
-
// ── Image Lightbox ──────────────────────────────────────────
|
|
2741
|
-
function openLightbox(src, name) {
|
|
2742
|
-
var lb = $('imgLightbox');
|
|
2743
|
-
var img = $('imgLightboxImg');
|
|
2744
|
-
var label = $('imgLightboxName');
|
|
2745
|
-
if (!lb || !img) return;
|
|
2746
|
-
img.src = src;
|
|
2747
|
-
if (label) label.textContent = name || '';
|
|
2748
|
-
lb.classList.add('active');
|
|
2749
|
-
}
|
|
2750
|
-
|
|
2751
|
-
function closeLightbox(e) {
|
|
2752
|
-
if (e) e.stopPropagation();
|
|
2753
|
-
var lb = $('imgLightbox');
|
|
2754
|
-
if (lb) lb.classList.remove('active');
|
|
2755
|
-
var img = $('imgLightboxImg');
|
|
2756
|
-
if (img) img.src = '';
|
|
2757
|
-
}
|
|
2758
|
-
|
|
2759
|
-
window.openLightbox = openLightbox;
|
|
2760
|
-
window.closeLightbox = closeLightbox;
|
|
2761
|
-
|
|
2762
2721
|
setPill('ok', 'pronto');
|
|
2763
2722
|
|
|
2764
2723
|
/* ── Global Keyboard Shortcuts ── */
|
|
@@ -2775,13 +2734,8 @@
|
|
|
2775
2734
|
openQuickAdd();
|
|
2776
2735
|
return;
|
|
2777
2736
|
}
|
|
2778
|
-
// Escape: Close
|
|
2737
|
+
// Escape: Close quick-add modal or blur active element
|
|
2779
2738
|
if (e.key === 'Escape') {
|
|
2780
|
-
const lb = $('imgLightbox');
|
|
2781
|
-
if (lb && lb.classList.contains('active')) {
|
|
2782
|
-
closeLightbox();
|
|
2783
|
-
return;
|
|
2784
|
-
}
|
|
2785
2739
|
const overlay = $('quickAddOverlay');
|
|
2786
2740
|
if (overlay && overlay.style.display !== 'none') {
|
|
2787
2741
|
closeQuickAdd();
|
package/cli/web.js
CHANGED
|
@@ -1192,14 +1192,9 @@ function buildHtml(safeDefault, appVersion) {
|
|
|
1192
1192
|
</div>
|
|
1193
1193
|
|
|
1194
1194
|
<!-- Textarea -->
|
|
1195
|
-
<textarea id="inboxText" aria-label="Entrada de texto para processar ou perguntar" placeholder="Cole updates, decisões, blockers... ou faça uma pergunta à Freya. ▸ Salvar & Processar → extrai tarefas e blockers do texto ▸ Perguntar → consulta o histórico via busca semântica (RAG)
|
|
1195
|
+
<textarea id="inboxText" aria-label="Entrada de texto para processar ou perguntar" placeholder="Cole updates, decisões, blockers... ou faça uma pergunta à Freya. ▸ Salvar & Processar → extrai tarefas e blockers do texto ▸ Perguntar → consulta o histórico via busca semântica (RAG)" style="resize:none; min-height: 200px; flex: 1; border-radius: 0; border-left: none; border-right: none; border-top: none; border-bottom: 1px solid var(--border); padding: 14px 16px; font-size: 13px; line-height: 1.6;"
|
|
1196
1196
|
onkeydown="if((event.metaKey||event.ctrlKey)&&event.key==='Enter'){event.preventDefault();window.saveAndPlan();}"></textarea>
|
|
1197
1197
|
|
|
1198
|
-
<!-- Image paste preview strip -->
|
|
1199
|
-
<div id="pastePreview" style="display:none; padding: 8px 14px; border-bottom: 1px solid var(--border); background: rgba(0,0,0,0.05); flex-shrink:0;">
|
|
1200
|
-
<div style="display:flex; align-items:center; gap:8px; flex-wrap:wrap;" id="pastePreviewInner"></div>
|
|
1201
|
-
</div>
|
|
1202
|
-
|
|
1203
1198
|
<!-- Actions bar -->
|
|
1204
1199
|
<div style="padding: 10px 14px; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 8px; flex-shrink:0;">
|
|
1205
1200
|
<div style="display:flex; gap:8px; flex-wrap:wrap;">
|
|
@@ -1351,13 +1346,6 @@ function buildHtml(safeDefault, appVersion) {
|
|
|
1351
1346
|
</div>
|
|
1352
1347
|
</div>
|
|
1353
1348
|
|
|
1354
|
-
<!-- Image lightbox modal -->
|
|
1355
|
-
<div id="imgLightbox" class="img-lightbox" onclick="window.closeLightbox(event)">
|
|
1356
|
-
<button class="img-lightbox-close" onclick="window.closeLightbox(event)" title="Fechar (Esc)">×</button>
|
|
1357
|
-
<img id="imgLightboxImg" src="" alt="preview" onclick="event.stopPropagation()" />
|
|
1358
|
-
<div id="imgLightboxName" class="img-lightbox-name"></div>
|
|
1359
|
-
</div>
|
|
1360
|
-
|
|
1361
1349
|
<script>
|
|
1362
1350
|
window.__FREYA_DEFAULT_DIR = "${safeDefault}";
|
|
1363
1351
|
</script>
|
|
@@ -2408,26 +2396,25 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
2408
2396
|
return;
|
|
2409
2397
|
}
|
|
2410
2398
|
|
|
2411
|
-
// Serve attachment images
|
|
2399
|
+
// Serve attachment images from workspace data/attachments/
|
|
2412
2400
|
if (req.method === 'GET' && req.url.startsWith('/attachments/')) {
|
|
2413
|
-
const
|
|
2401
|
+
const fname = decodeURIComponent(req.url.slice('/attachments/'.length)).replace(/[\/\\]/g, '');
|
|
2414
2402
|
const requestedDir = dir || './freya';
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
if (!filePath
|
|
2419
|
-
res.writeHead(
|
|
2420
|
-
|
|
2421
|
-
if (exists(filePath)) {
|
|
2422
|
-
const ext = path.extname(filePath).toLowerCase();
|
|
2423
|
-
const mimeMap = { '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif', '.webp': 'image/webp', '.bmp': 'image/bmp' };
|
|
2424
|
-
const mime = mimeMap[ext] || 'application/octet-stream';
|
|
2425
|
-
const data = fs.readFileSync(filePath);
|
|
2426
|
-
res.writeHead(200, { 'Content-Type': mime, 'Cache-Control': 'public, max-age=86400' });
|
|
2427
|
-
res.end(data);
|
|
2403
|
+
let wsDir;
|
|
2404
|
+
try { wsDir = normalizeWorkspaceDir(requestedDir); } catch { wsDir = path.resolve(process.cwd(), requestedDir); }
|
|
2405
|
+
const filePath = path.join(wsDir, 'data', 'attachments', fname);
|
|
2406
|
+
if (!exists(filePath)) {
|
|
2407
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
2408
|
+
res.end('Not found');
|
|
2428
2409
|
return;
|
|
2429
2410
|
}
|
|
2430
|
-
|
|
2411
|
+
const ext = path.extname(fname).toLowerCase();
|
|
2412
|
+
const mimeMap = { '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif', '.webp': 'image/webp', '.bmp': 'image/bmp', '.svg': 'image/svg+xml' };
|
|
2413
|
+
const mime = mimeMap[ext] || 'application/octet-stream';
|
|
2414
|
+
const data = fs.readFileSync(filePath);
|
|
2415
|
+
res.writeHead(200, { 'Content-Type': mime, 'Cache-Control': 'max-age=86400' });
|
|
2416
|
+
res.end(data);
|
|
2417
|
+
return;
|
|
2431
2418
|
}
|
|
2432
2419
|
|
|
2433
2420
|
if (req.url.startsWith('/api/')) {
|
|
@@ -3240,30 +3227,9 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
3240
3227
|
}
|
|
3241
3228
|
}
|
|
3242
3229
|
|
|
3243
|
-
// ── Upload attachment (image paste) ──────────────────────────
|
|
3244
|
-
if (req.url === '/api/attachments/upload') {
|
|
3245
|
-
const data = String(payload.data || '').trim();
|
|
3246
|
-
const fname = String(payload.filename || '').trim();
|
|
3247
|
-
if (!data || !fname) return safeJson(res, 400, { error: 'Missing data or filename' });
|
|
3248
|
-
|
|
3249
|
-
const d = isoDate();
|
|
3250
|
-
const attDir = path.join(workspaceDir, 'data', 'attachments', d);
|
|
3251
|
-
ensureDir(attDir);
|
|
3252
|
-
|
|
3253
|
-
// data is base64 (may have data:image/... prefix)
|
|
3254
|
-
const base64 = data.replace(/^data:image\/[^;]+;base64,/, '');
|
|
3255
|
-
const buf = Buffer.from(base64, 'base64');
|
|
3256
|
-
const filePath = path.join(attDir, fname);
|
|
3257
|
-
fs.writeFileSync(filePath, buf);
|
|
3258
|
-
|
|
3259
|
-
const relPath = `data/attachments/${d}/${fname}`;
|
|
3260
|
-
return safeJson(res, 200, { ok: true, path: relPath });
|
|
3261
|
-
}
|
|
3262
|
-
|
|
3263
3230
|
if (req.url === '/api/inbox/add') {
|
|
3264
3231
|
const text = String(payload.text || '').trim();
|
|
3265
|
-
|
|
3266
|
-
if (!text && !attachments.length) return safeJson(res, 400, { error: 'Missing text' });
|
|
3232
|
+
if (!text) return safeJson(res, 400, { error: 'Missing text' });
|
|
3267
3233
|
|
|
3268
3234
|
const d = isoDate();
|
|
3269
3235
|
const file = path.join(workspaceDir, 'logs', 'daily', `${d}.md`);
|
|
@@ -3277,13 +3243,8 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
3277
3243
|
const linksText = linkInfo && linkInfo.linksText ? linkInfo.linksText : '';
|
|
3278
3244
|
const slugs = linkInfo && Array.isArray(linkInfo.slugs) ? linkInfo.slugs : [];
|
|
3279
3245
|
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
if (attachments.length) {
|
|
3283
|
-
attachBlock = '\n\n**Anexos:**\n' + attachments.map(a => ``).join('\n');
|
|
3284
|
-
}
|
|
3285
|
-
|
|
3286
|
-
const block = `\n\n## [${hh}:${mm}] Raw Input\n${text}${linksText}${attachBlock}\n`;
|
|
3246
|
+
const imgRef = payload.imagePath ? `\n` : '';
|
|
3247
|
+
const block = `\n\n## [${hh}:${mm}] Raw Input\n${text}${imgRef}${linksText}\n`;
|
|
3287
3248
|
fs.appendFileSync(file, block, 'utf8');
|
|
3288
3249
|
|
|
3289
3250
|
if (slugs.length) {
|
|
@@ -3342,16 +3303,7 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
3342
3303
|
]
|
|
3343
3304
|
};
|
|
3344
3305
|
|
|
3345
|
-
|
|
3346
|
-
const attachments = Array.isArray(payload.attachments) ? payload.attachments : [];
|
|
3347
|
-
let imageContext = '';
|
|
3348
|
-
if (attachments.length) {
|
|
3349
|
-
imageContext = '\n\nIMAGENS ANEXADAS (use @caminho para analisar):\n' +
|
|
3350
|
-
attachments.map(a => `- @${path.join(workspaceDir, a.path)} (${a.name})`).join('\n') + '\n' +
|
|
3351
|
-
'Analise as imagens anexadas e inclua as informações visuais no plano.\n';
|
|
3352
|
-
}
|
|
3353
|
-
|
|
3354
|
-
const prompt = `Você é o planner do sistema F.R.E.Y.A.\n\nContexto: vamos receber um input bruto do usuário e propor ações estruturadas.\nRegras: siga os arquivos de regras abaixo.\nSaída: retorne APENAS JSON válido no formato: ${JSON.stringify(schema)}\n\nRestrições:\n- NÃO use code fences (\`\`\`)\n- NÃO inclua texto extra antes/depois do JSON\n- NÃO use quebras de linha dentro de strings (transforme em uma frase única)\n\nREGRAS:${rulesText}${imageContext}\n\nINPUT DO USUÁRIO:\n${text}\n`;
|
|
3306
|
+
const prompt = `Você é o planner do sistema F.R.E.Y.A.\n\nContexto: vamos receber um input bruto do usuário e propor ações estruturadas.\nRegras: siga os arquivos de regras abaixo.\nSaída: retorne APENAS JSON válido no formato: ${JSON.stringify(schema)}\n\nRestrições:\n- NÃO use code fences (\`\`\`)\n- NÃO inclua texto extra antes/depois do JSON\n- NÃO use quebras de linha dentro de strings (transforme em uma frase única)\n\nREGRAS:${rulesText}\n\nINPUT DO USUÁRIO:\n${text}\n`;
|
|
3355
3307
|
|
|
3356
3308
|
// Prefer COPILOT_CMD if provided, otherwise try 'copilot'
|
|
3357
3309
|
const cmd = process.env.COPILOT_CMD || 'copilot';
|
|
@@ -3361,7 +3313,7 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
3361
3313
|
// BUG-48: pass FREYA_WORKSPACE_DIR so the Copilot subprocess uses correct DB
|
|
3362
3314
|
const agentEnv = { FREYA_WORKSPACE_DIR: workspaceDir };
|
|
3363
3315
|
try {
|
|
3364
|
-
const r = await run(cmd, ['-s', '--no-color', '--stream', 'off', '-p', prompt, '--allow-all-tools'
|
|
3316
|
+
const r = await run(cmd, ['-s', '--no-color', '--stream', 'off', '-p', prompt, '--allow-all-tools'], workspaceDir, agentEnv);
|
|
3365
3317
|
const out = (r.stdout + r.stderr).trim();
|
|
3366
3318
|
if (r.code !== 0) {
|
|
3367
3319
|
return safeJson(res, 200, {
|
|
@@ -3539,6 +3491,49 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
3539
3491
|
const insertTask = dl.db.prepare(`INSERT INTO tasks (id, project_slug, description, category, status, metadata) VALUES (?, ?, ?, ?, ?, ?)`);
|
|
3540
3492
|
const insertBlocker = dl.db.prepare(`INSERT INTO blockers (id, project_slug, title, severity, status, owner, next_action, metadata) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
3541
3493
|
|
|
3494
|
+
// ── Semantic deduplication (async, runs BEFORE the sync transaction) ──
|
|
3495
|
+
// Load pending tasks for cosine similarity comparison
|
|
3496
|
+
const pendingTasks = dl.db.prepare("SELECT id, description, project_slug FROM tasks WHERE status = 'PENDING'").all();
|
|
3497
|
+
let pendingEmbeddings = []; // [{ id, description, project_slug, vector }]
|
|
3498
|
+
try {
|
|
3499
|
+
const { defaultEmbedder } = require(path.join(workspaceDir, 'scripts', 'lib', 'Embedder.js'));
|
|
3500
|
+
for (const pt of pendingTasks) {
|
|
3501
|
+
try {
|
|
3502
|
+
const vec = await defaultEmbedder.embedText(pt.description);
|
|
3503
|
+
pendingEmbeddings.push({ id: pt.id, description: pt.description, project_slug: pt.project_slug, vector: vec });
|
|
3504
|
+
} catch { /* skip if embedding fails for individual task */ }
|
|
3505
|
+
}
|
|
3506
|
+
} catch (embErr) {
|
|
3507
|
+
// Embedder not available — fall back to exact-match only
|
|
3508
|
+
console.error('[dedup] Semantic dedup unavailable:', embErr.message);
|
|
3509
|
+
}
|
|
3510
|
+
|
|
3511
|
+
// Pre-compute semantic duplicates for each action
|
|
3512
|
+
const semanticDupMap = new Map(); // action index → existing task id (if duplicate)
|
|
3513
|
+
const SIMILARITY_THRESHOLD = 0.78;
|
|
3514
|
+
if (pendingEmbeddings.length > 0) {
|
|
3515
|
+
try {
|
|
3516
|
+
const { defaultEmbedder } = require(path.join(workspaceDir, 'scripts', 'lib', 'Embedder.js'));
|
|
3517
|
+
for (let ai = 0; ai < actions.length; ai++) {
|
|
3518
|
+
const a = actions[ai];
|
|
3519
|
+
if (!a || a.type !== 'create_task') continue;
|
|
3520
|
+
const desc = normalizeWhitespace(a.description);
|
|
3521
|
+
if (!desc) continue;
|
|
3522
|
+
try {
|
|
3523
|
+
const newVec = await defaultEmbedder.embedText(desc);
|
|
3524
|
+
let bestScore = 0, bestMatch = null;
|
|
3525
|
+
for (const pe of pendingEmbeddings) {
|
|
3526
|
+
const score = defaultEmbedder.cosineSimilarity(newVec, pe.vector);
|
|
3527
|
+
if (score > bestScore) { bestScore = score; bestMatch = pe; }
|
|
3528
|
+
}
|
|
3529
|
+
if (bestScore >= SIMILARITY_THRESHOLD && bestMatch) {
|
|
3530
|
+
semanticDupMap.set(ai, { existingId: bestMatch.id, existingDesc: bestMatch.description, score: bestScore });
|
|
3531
|
+
}
|
|
3532
|
+
} catch { /* skip */ }
|
|
3533
|
+
}
|
|
3534
|
+
} catch { /* embedder not available */ }
|
|
3535
|
+
}
|
|
3536
|
+
|
|
3542
3537
|
// BUG-31: Move deduplication queries INSIDE the transaction to eliminate TOCTOU race
|
|
3543
3538
|
const applyTx = dl.db.transaction((actionsToApply) => {
|
|
3544
3539
|
// Query for existing keys inside the transaction for atomicity
|
|
@@ -3547,7 +3542,8 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
3547
3542
|
const recentBlockers = dl.db.prepare("SELECT title FROM blockers WHERE created_at >= datetime('now', '-1 day')").all();
|
|
3548
3543
|
const existingBlockerKeys24h = new Set(recentBlockers.map(b => sha1(normalizeTextForKey(b.title))));
|
|
3549
3544
|
|
|
3550
|
-
for (
|
|
3545
|
+
for (let ai = 0; ai < actionsToApply.length; ai++) {
|
|
3546
|
+
const a = actionsToApply[ai];
|
|
3551
3547
|
if (!a || typeof a !== 'object') continue;
|
|
3552
3548
|
const type = String(a.type || '').trim();
|
|
3553
3549
|
|
|
@@ -3557,8 +3553,25 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
3557
3553
|
if (!description) continue;
|
|
3558
3554
|
const projectSlug = String(a.projectSlug || '').trim() || inferProjectSlug(description, slugMap);
|
|
3559
3555
|
const streamSlug = String(a.streamSlug || '').trim();
|
|
3556
|
+
|
|
3557
|
+
// Exact-match dedup (24h window)
|
|
3560
3558
|
const key = sha1(normalizeTextForKey((projectSlug ? projectSlug + ' ' : '') + description));
|
|
3561
3559
|
if (existingTaskKeys24h.has(key)) { applied.tasksSkipped++; continue; }
|
|
3560
|
+
|
|
3561
|
+
// Semantic dedup (all pending tasks)
|
|
3562
|
+
if (semanticDupMap.has(ai)) {
|
|
3563
|
+
const dup = semanticDupMap.get(ai);
|
|
3564
|
+
applied.tasksSkipped++;
|
|
3565
|
+
if (!applied.semanticDups) applied.semanticDups = [];
|
|
3566
|
+
applied.semanticDups.push({
|
|
3567
|
+
newDesc: description,
|
|
3568
|
+
existingId: dup.existingId,
|
|
3569
|
+
existingDesc: dup.existingDesc,
|
|
3570
|
+
similarity: Math.round(dup.score * 100) + '%'
|
|
3571
|
+
});
|
|
3572
|
+
continue;
|
|
3573
|
+
}
|
|
3574
|
+
|
|
3562
3575
|
const category = validTaskCats.has(String(a.category || '').trim()) ? String(a.category).trim() : 'DO_NOW';
|
|
3563
3576
|
const priority = normPriority(a.priority);
|
|
3564
3577
|
|
|
@@ -3707,9 +3720,25 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
3707
3720
|
return safeJson(res, r.code === 0 ? 200 : 400, r.code === 0 ? { ok: true, output: out } : { error: out || 'index rebuild failed', output: out });
|
|
3708
3721
|
}
|
|
3709
3722
|
|
|
3723
|
+
// Upload attachment (base64 image from clipboard paste)
|
|
3724
|
+
if (req.url === '/api/attachments/upload') {
|
|
3725
|
+
const base64 = String(payload.data || '').trim();
|
|
3726
|
+
const mimeType = String(payload.mimeType || 'image/png').trim();
|
|
3727
|
+
if (!base64) return safeJson(res, 400, { error: 'Missing data' });
|
|
3728
|
+
const ext = mimeType === 'image/jpeg' ? '.jpg' : mimeType === 'image/gif' ? '.gif' : mimeType === 'image/webp' ? '.webp' : '.png';
|
|
3729
|
+
const fname = 'paste-' + Date.now() + ext;
|
|
3730
|
+
const attachDir = path.join(workspaceDir, 'data', 'attachments');
|
|
3731
|
+
if (!exists(attachDir)) fs.mkdirSync(attachDir, { recursive: true });
|
|
3732
|
+
const filePath = path.join(attachDir, fname);
|
|
3733
|
+
const buf = Buffer.from(base64, 'base64');
|
|
3734
|
+
fs.writeFileSync(filePath, buf);
|
|
3735
|
+
return safeJson(res, 200, { ok: true, filename: fname, url: '/attachments/' + fname });
|
|
3736
|
+
}
|
|
3737
|
+
|
|
3710
3738
|
if (req.url === '/api/chat/ask') {
|
|
3711
3739
|
const sessionId = String(payload.sessionId || '').trim();
|
|
3712
3740
|
const query = String(payload.query || '').trim();
|
|
3741
|
+
const imagePath = payload.imagePath ? String(payload.imagePath).trim() : null;
|
|
3713
3742
|
if (!query) return safeJson(res, 400, { error: 'Missing query' });
|
|
3714
3743
|
|
|
3715
3744
|
const workspaceRulesBase = path.join(workspaceDir, '.agent', 'rules', 'freya');
|
|
@@ -3742,27 +3771,23 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
3742
3771
|
console.error('[oracle] RAG search failed (embedder/sharp unavailable), continuing without context:', ragErr.message);
|
|
3743
3772
|
}
|
|
3744
3773
|
|
|
3745
|
-
|
|
3746
|
-
const askAttachments = Array.isArray(payload.attachments) ? payload.attachments : [];
|
|
3747
|
-
let askImageContext = '';
|
|
3748
|
-
const askHasImages = askAttachments.length > 0;
|
|
3749
|
-
if (askHasImages) {
|
|
3750
|
-
askImageContext = '\n\nIMAGENS ANEXADAS (use @caminho para analisar):\n' +
|
|
3751
|
-
askAttachments.map(a => `- @${path.join(workspaceDir, a.path)} (${a.name})`).join('\n') + '\n' +
|
|
3752
|
-
'Analise as imagens anexadas e use-as como contexto para responder à consulta.\n';
|
|
3753
|
-
}
|
|
3754
|
-
|
|
3755
|
-
const prompt = `Você é o agente Oracle do sistema F.R.E.Y.A.\n\nSiga estritamente os arquivos de regras abaixo.\nResponda de forma analítica e consultiva.\n${ragContext}${askImageContext}\n\nREGRAS:${rulesText}\n\nCONSULTA DO USUÁRIO:\n${query}\n`;
|
|
3774
|
+
const prompt = `Você é o agente Oracle do sistema F.R.E.Y.A.\n\nSiga estritamente os arquivos de regras abaixo.\nResponda de forma analítica e consultiva.\n${ragContext}\n\nREGRAS:${rulesText}\n\nCONSULTA DO USUÁRIO:\n${query}\n`;
|
|
3756
3775
|
|
|
3757
3776
|
const cmd = process.env.COPILOT_CMD || 'copilot';
|
|
3758
3777
|
|
|
3759
3778
|
// BUG-48: pass FREYA_WORKSPACE_DIR so the Copilot subprocess uses correct DB
|
|
3760
3779
|
const oracleEnv = { FREYA_WORKSPACE_DIR: workspaceDir };
|
|
3761
3780
|
try {
|
|
3762
|
-
//
|
|
3763
|
-
const
|
|
3764
|
-
if (
|
|
3765
|
-
|
|
3781
|
+
// Build copilot args; add image if user pasted a screenshot
|
|
3782
|
+
const copilotArgs = ['-s', '--no-color', '--stream', 'off'];
|
|
3783
|
+
if (imagePath) {
|
|
3784
|
+
const absImg = path.isAbsolute(imagePath) ? imagePath : path.join(workspaceDir, imagePath);
|
|
3785
|
+
if (exists(absImg)) {
|
|
3786
|
+
copilotArgs.push('--add-image', absImg);
|
|
3787
|
+
}
|
|
3788
|
+
}
|
|
3789
|
+
copilotArgs.push('-p', prompt);
|
|
3790
|
+
const r = await run(cmd, copilotArgs, workspaceDir, oracleEnv);
|
|
3766
3791
|
const out = (r.stdout + r.stderr).trim();
|
|
3767
3792
|
if (r.code !== 0) {
|
|
3768
3793
|
return safeJson(res, 200, { ok: false, answer: 'Falha na busca do agente Oracle:\n' + (out || 'Exit code != 0'), sessionId });
|
|
@@ -3893,7 +3918,8 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
3893
3918
|
|
|
3894
3919
|
const rawTasks = dl.db.prepare(query).all(...params);
|
|
3895
3920
|
const tasks = rawTasks.map(t => {
|
|
3896
|
-
|
|
3921
|
+
let meta = {};
|
|
3922
|
+
try { meta = t.metadata ? JSON.parse(t.metadata) : {}; } catch (_) {}
|
|
3897
3923
|
return {
|
|
3898
3924
|
id: t.id,
|
|
3899
3925
|
description: t.description,
|
|
@@ -4091,7 +4117,8 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
4091
4117
|
|
|
4092
4118
|
const rawBlockers = dl.db.prepare(query).all(...params);
|
|
4093
4119
|
const blockers = rawBlockers.map(b => {
|
|
4094
|
-
|
|
4120
|
+
let meta = {};
|
|
4121
|
+
try { meta = b.metadata ? JSON.parse(b.metadata) : {}; } catch (_) {}
|
|
4095
4122
|
return {
|
|
4096
4123
|
id: b.id,
|
|
4097
4124
|
title: b.title,
|
|
@@ -4671,13 +4698,6 @@ function buildKanbanHtml(safeDefault, appVersion) {
|
|
|
4671
4698
|
</div>
|
|
4672
4699
|
</div>
|
|
4673
4700
|
|
|
4674
|
-
<!-- Image lightbox modal -->
|
|
4675
|
-
<div id="imgLightbox" class="img-lightbox" onclick="window.closeLightbox(event)">
|
|
4676
|
-
<button class="img-lightbox-close" onclick="window.closeLightbox(event)" title="Fechar (Esc)">×</button>
|
|
4677
|
-
<img id="imgLightboxImg" src="" alt="preview" onclick="event.stopPropagation()" />
|
|
4678
|
-
<div id="imgLightboxName" class="img-lightbox-name"></div>
|
|
4679
|
-
</div>
|
|
4680
|
-
|
|
4681
4701
|
<script>
|
|
4682
4702
|
window.__FREYA_DEFAULT_DIR = "${safeDefault}";
|
|
4683
4703
|
</script>
|
package/package.json
CHANGED