@cccarv82/freya 2.17.2 → 2.18.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 +128 -0
- package/cli/web-ui.js +192 -9
- package/cli/web.js +97 -8
- package/package.json +1 -1
package/cli/web-ui.css
CHANGED
|
@@ -1279,6 +1279,134 @@ 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
|
+
|
|
1282
1410
|
.chatComposer {
|
|
1283
1411
|
display: none;
|
|
1284
1412
|
}
|
package/cli/web-ui.js
CHANGED
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
timelineProject: '',
|
|
21
21
|
timelineTag: '',
|
|
22
22
|
chatSessionId: null,
|
|
23
|
-
chatLoaded: false
|
|
23
|
+
chatLoaded: false,
|
|
24
|
+
pastedImages: [] // { file: File, dataUrl: string, name: string }
|
|
24
25
|
};
|
|
25
26
|
|
|
26
27
|
function applyDarkTheme() {
|
|
@@ -503,16 +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
|
+
// Upload pasted images if any (for visual context in log)
|
|
508
|
+
var askAttachments = [];
|
|
509
|
+
if (state.pastedImages.length > 0) {
|
|
510
|
+
askAttachments = await uploadPastedImages();
|
|
511
|
+
}
|
|
512
|
+
if (askAttachments.length) {
|
|
513
|
+
var askHtml = escapeHtml(query).replace(/\n/g, '<br>');
|
|
514
|
+
askHtml += '<div class="bubble-attachments">';
|
|
515
|
+
for (var ai = 0; ai < askAttachments.length; ai++) {
|
|
516
|
+
askHtml += '<img src="/attachments/' + askAttachments[ai].path.replace('data/attachments/', '') + '" class="bubble-img" alt="' + escapeHtml(askAttachments[ai].name) + '" />';
|
|
517
|
+
}
|
|
518
|
+
askHtml += '</div>';
|
|
519
|
+
chatAppend('user', askHtml, { html: true });
|
|
520
|
+
} else {
|
|
521
|
+
chatAppend('user', query);
|
|
522
|
+
}
|
|
523
|
+
clearPastedImages();
|
|
507
524
|
syncChatThreadVisibility();
|
|
508
|
-
setPill('run', 'pesquisando…');
|
|
525
|
+
setPill('run', askAttachments.length ? 'enviando imagem + pesquisando…' : 'pesquisando…');
|
|
509
526
|
|
|
510
527
|
const typingId = 'typing-' + Date.now();
|
|
511
528
|
chatAppend('assistant', '<div class="typing-indicator"><span></span><span></span><span></span></div>', { id: typingId, html: true });
|
|
512
529
|
|
|
513
530
|
try {
|
|
514
531
|
const sessionId = ensureChatSession();
|
|
515
|
-
const r = await api('/api/chat/ask', { dir: dirOrDefault(), sessionId, query });
|
|
532
|
+
const r = await api('/api/chat/ask', { dir: dirOrDefault(), sessionId, query, attachments: askAttachments });
|
|
516
533
|
const answer = r && r.answer ? r.answer : 'Não encontrei registro';
|
|
517
534
|
|
|
518
535
|
const el = $(typingId);
|
|
@@ -2346,14 +2363,34 @@
|
|
|
2346
2363
|
const tag = $('chatModeTag');
|
|
2347
2364
|
if (tag) { tag.textContent = '📥 inbox'; tag.style.display = ''; tag.style.color = 'var(--primary)'; tag.style.borderColor = 'var(--primary)'; }
|
|
2348
2365
|
|
|
2349
|
-
|
|
2366
|
+
// Upload pasted images if any
|
|
2367
|
+
var attachments = [];
|
|
2368
|
+
var hasImages = state.pastedImages.length > 0;
|
|
2369
|
+
if (hasImages) {
|
|
2370
|
+
setPill('run', 'enviando imagens…');
|
|
2371
|
+
attachments = await uploadPastedImages();
|
|
2372
|
+
}
|
|
2373
|
+
|
|
2374
|
+
// Show user message with image thumbnails
|
|
2375
|
+
var userHtml = escapeHtml(text).replace(/\n/g, '<br>');
|
|
2376
|
+
if (attachments.length) {
|
|
2377
|
+
userHtml += '<div class="bubble-attachments">';
|
|
2378
|
+
for (var ai = 0; ai < attachments.length; ai++) {
|
|
2379
|
+
userHtml += '<img src="/attachments/' + attachments[ai].path.replace('data/attachments/', '') + '" class="bubble-img" alt="' + escapeHtml(attachments[ai].name) + '" />';
|
|
2380
|
+
}
|
|
2381
|
+
userHtml += '</div>';
|
|
2382
|
+
chatAppend('user', userHtml, { html: true });
|
|
2383
|
+
} else {
|
|
2384
|
+
chatAppend('user', text);
|
|
2385
|
+
}
|
|
2386
|
+
clearPastedImages();
|
|
2350
2387
|
syncChatThreadVisibility();
|
|
2351
2388
|
|
|
2352
2389
|
setPill('run', 'salvando…');
|
|
2353
|
-
await api('/api/inbox/add', { dir: dirOrDefault(), text });
|
|
2390
|
+
await api('/api/inbox/add', { dir: dirOrDefault(), text, attachments });
|
|
2354
2391
|
|
|
2355
|
-
setPill('run', 'processando…');
|
|
2356
|
-
const r = await api('/api/agents/plan', { dir: dirOrDefault(), text });
|
|
2392
|
+
setPill('run', attachments.length ? 'processando texto + imagens…' : 'processando…');
|
|
2393
|
+
const r = await api('/api/agents/plan', { dir: dirOrDefault(), text, attachments });
|
|
2357
2394
|
|
|
2358
2395
|
state.lastPlan = r.plan || '';
|
|
2359
2396
|
|
|
@@ -2555,8 +2592,149 @@
|
|
|
2555
2592
|
refreshToday();
|
|
2556
2593
|
reloadSlugRules();
|
|
2557
2594
|
loadChatHistory();
|
|
2595
|
+
initPasteHandler();
|
|
2596
|
+
|
|
2597
|
+
// Click on chat images → open lightbox
|
|
2598
|
+
var thread = $('chatThread');
|
|
2599
|
+
if (thread) {
|
|
2600
|
+
thread.addEventListener('click', function(e) {
|
|
2601
|
+
if (e.target && e.target.classList && e.target.classList.contains('bubble-img')) {
|
|
2602
|
+
openLightbox(e.target.src, e.target.alt || 'Anexo');
|
|
2603
|
+
}
|
|
2604
|
+
});
|
|
2605
|
+
}
|
|
2558
2606
|
})();
|
|
2559
2607
|
|
|
2608
|
+
// ── Image Paste Handler ──────────────────────────────────────
|
|
2609
|
+
function initPasteHandler() {
|
|
2610
|
+
var ta = $('inboxText');
|
|
2611
|
+
if (!ta) return;
|
|
2612
|
+
|
|
2613
|
+
ta.addEventListener('paste', function(e) {
|
|
2614
|
+
var items = e.clipboardData && e.clipboardData.items;
|
|
2615
|
+
if (!items) return;
|
|
2616
|
+
|
|
2617
|
+
for (var i = 0; i < items.length; i++) {
|
|
2618
|
+
if (items[i].type.indexOf('image/') === 0) {
|
|
2619
|
+
e.preventDefault();
|
|
2620
|
+
var file = items[i].getAsFile();
|
|
2621
|
+
if (!file) continue;
|
|
2622
|
+
|
|
2623
|
+
var reader = new FileReader();
|
|
2624
|
+
reader.onload = function(ev) {
|
|
2625
|
+
var ext = file.type.split('/')[1] || 'png';
|
|
2626
|
+
if (ext === 'jpeg') ext = 'jpg';
|
|
2627
|
+
var ts = Date.now();
|
|
2628
|
+
var name = 'paste-' + ts + '.' + ext;
|
|
2629
|
+
state.pastedImages.push({ file: file, dataUrl: ev.target.result, name: name });
|
|
2630
|
+
renderPastePreview();
|
|
2631
|
+
};
|
|
2632
|
+
reader.readAsDataURL(file);
|
|
2633
|
+
break; // handle one image per paste
|
|
2634
|
+
}
|
|
2635
|
+
}
|
|
2636
|
+
});
|
|
2637
|
+
}
|
|
2638
|
+
|
|
2639
|
+
function renderPastePreview() {
|
|
2640
|
+
var strip = $('pastePreview');
|
|
2641
|
+
var inner = $('pastePreviewInner');
|
|
2642
|
+
if (!strip || !inner) return;
|
|
2643
|
+
|
|
2644
|
+
if (!state.pastedImages.length) {
|
|
2645
|
+
strip.style.display = 'none';
|
|
2646
|
+
inner.innerHTML = '';
|
|
2647
|
+
return;
|
|
2648
|
+
}
|
|
2649
|
+
|
|
2650
|
+
strip.style.display = 'block';
|
|
2651
|
+
inner.innerHTML = '';
|
|
2652
|
+
|
|
2653
|
+
for (var i = 0; i < state.pastedImages.length; i++) {
|
|
2654
|
+
(function(idx) {
|
|
2655
|
+
var img = state.pastedImages[idx];
|
|
2656
|
+
var wrap = document.createElement('div');
|
|
2657
|
+
wrap.className = 'paste-thumb';
|
|
2658
|
+
|
|
2659
|
+
var thumb = document.createElement('img');
|
|
2660
|
+
thumb.src = img.dataUrl;
|
|
2661
|
+
thumb.alt = img.name;
|
|
2662
|
+
thumb.style.cssText = 'max-height:60px; max-width:100px; border-radius:6px; border:1px solid var(--border); cursor:pointer;';
|
|
2663
|
+
thumb.title = img.name;
|
|
2664
|
+
thumb.onclick = function() {
|
|
2665
|
+
openLightbox(img.dataUrl, img.name);
|
|
2666
|
+
};
|
|
2667
|
+
|
|
2668
|
+
var removeBtn = document.createElement('button');
|
|
2669
|
+
removeBtn.className = 'paste-thumb-remove';
|
|
2670
|
+
removeBtn.innerHTML = '×';
|
|
2671
|
+
removeBtn.title = 'Remover';
|
|
2672
|
+
removeBtn.onclick = function() {
|
|
2673
|
+
state.pastedImages.splice(idx, 1);
|
|
2674
|
+
renderPastePreview();
|
|
2675
|
+
};
|
|
2676
|
+
|
|
2677
|
+
wrap.appendChild(thumb);
|
|
2678
|
+
wrap.appendChild(removeBtn);
|
|
2679
|
+
inner.appendChild(wrap);
|
|
2680
|
+
})(i);
|
|
2681
|
+
}
|
|
2682
|
+
|
|
2683
|
+
// label
|
|
2684
|
+
var label = document.createElement('span');
|
|
2685
|
+
label.style.cssText = 'font-size:11px; color:var(--faint); margin-left:4px;';
|
|
2686
|
+
label.textContent = state.pastedImages.length + ' imagem(ns) anexada(s)';
|
|
2687
|
+
inner.appendChild(label);
|
|
2688
|
+
}
|
|
2689
|
+
|
|
2690
|
+
// Upload pasted images and return array of { name, path }
|
|
2691
|
+
async function uploadPastedImages() {
|
|
2692
|
+
var results = [];
|
|
2693
|
+
for (var i = 0; i < state.pastedImages.length; i++) {
|
|
2694
|
+
var img = state.pastedImages[i];
|
|
2695
|
+
try {
|
|
2696
|
+
var r = await api('/api/attachments/upload', {
|
|
2697
|
+
dir: dirOrDefault(),
|
|
2698
|
+
data: img.dataUrl,
|
|
2699
|
+
filename: img.name
|
|
2700
|
+
});
|
|
2701
|
+
if (r && r.ok) {
|
|
2702
|
+
results.push({ name: img.name, path: r.path });
|
|
2703
|
+
}
|
|
2704
|
+
} catch(err) {
|
|
2705
|
+
// best-effort, skip failed uploads
|
|
2706
|
+
}
|
|
2707
|
+
}
|
|
2708
|
+
return results;
|
|
2709
|
+
}
|
|
2710
|
+
|
|
2711
|
+
function clearPastedImages() {
|
|
2712
|
+
state.pastedImages = [];
|
|
2713
|
+
renderPastePreview();
|
|
2714
|
+
}
|
|
2715
|
+
|
|
2716
|
+
// ── Image Lightbox ──────────────────────────────────────────
|
|
2717
|
+
function openLightbox(src, name) {
|
|
2718
|
+
var lb = $('imgLightbox');
|
|
2719
|
+
var img = $('imgLightboxImg');
|
|
2720
|
+
var label = $('imgLightboxName');
|
|
2721
|
+
if (!lb || !img) return;
|
|
2722
|
+
img.src = src;
|
|
2723
|
+
if (label) label.textContent = name || '';
|
|
2724
|
+
lb.classList.add('active');
|
|
2725
|
+
}
|
|
2726
|
+
|
|
2727
|
+
function closeLightbox(e) {
|
|
2728
|
+
if (e) e.stopPropagation();
|
|
2729
|
+
var lb = $('imgLightbox');
|
|
2730
|
+
if (lb) lb.classList.remove('active');
|
|
2731
|
+
var img = $('imgLightboxImg');
|
|
2732
|
+
if (img) img.src = '';
|
|
2733
|
+
}
|
|
2734
|
+
|
|
2735
|
+
window.openLightbox = openLightbox;
|
|
2736
|
+
window.closeLightbox = closeLightbox;
|
|
2737
|
+
|
|
2560
2738
|
setPill('ok', 'pronto');
|
|
2561
2739
|
|
|
2562
2740
|
/* ── Global Keyboard Shortcuts ── */
|
|
@@ -2573,8 +2751,13 @@
|
|
|
2573
2751
|
openQuickAdd();
|
|
2574
2752
|
return;
|
|
2575
2753
|
}
|
|
2576
|
-
// Escape: Close quick-add
|
|
2754
|
+
// Escape: Close lightbox → quick-add → blur
|
|
2577
2755
|
if (e.key === 'Escape') {
|
|
2756
|
+
const lb = $('imgLightbox');
|
|
2757
|
+
if (lb && lb.classList.contains('active')) {
|
|
2758
|
+
closeLightbox();
|
|
2759
|
+
return;
|
|
2760
|
+
}
|
|
2578
2761
|
const overlay = $('quickAddOverlay');
|
|
2579
2762
|
if (overlay && overlay.style.display !== 'none') {
|
|
2580
2763
|
closeQuickAdd();
|
package/cli/web.js
CHANGED
|
@@ -1192,9 +1192,14 @@ 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)" 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;"
|
|
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) ▸ Ctrl+V com imagem → anexa screenshot ao contexto" 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
|
+
|
|
1198
1203
|
<!-- Actions bar -->
|
|
1199
1204
|
<div style="padding: 10px 14px; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 8px; flex-shrink:0;">
|
|
1200
1205
|
<div style="display:flex; gap:8px; flex-wrap:wrap;">
|
|
@@ -1346,6 +1351,13 @@ function buildHtml(safeDefault, appVersion) {
|
|
|
1346
1351
|
</div>
|
|
1347
1352
|
</div>
|
|
1348
1353
|
|
|
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
|
+
|
|
1349
1361
|
<script>
|
|
1350
1362
|
window.__FREYA_DEFAULT_DIR = "${safeDefault}";
|
|
1351
1363
|
</script>
|
|
@@ -2396,6 +2408,28 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
2396
2408
|
return;
|
|
2397
2409
|
}
|
|
2398
2410
|
|
|
2411
|
+
// Serve attachment images (pasted screenshots)
|
|
2412
|
+
if (req.method === 'GET' && req.url.startsWith('/attachments/')) {
|
|
2413
|
+
const relPath = decodeURIComponent(req.url.replace('/attachments/', ''));
|
|
2414
|
+
const requestedDir = dir || './freya';
|
|
2415
|
+
const wsDir = path.resolve(process.cwd(), requestedDir);
|
|
2416
|
+
const filePath = path.join(wsDir, 'data', 'attachments', relPath);
|
|
2417
|
+
// Security: ensure path stays within data/attachments
|
|
2418
|
+
if (!filePath.startsWith(path.join(wsDir, 'data', 'attachments'))) {
|
|
2419
|
+
res.writeHead(403); res.end(); return;
|
|
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);
|
|
2428
|
+
return;
|
|
2429
|
+
}
|
|
2430
|
+
res.writeHead(404); res.end(); return;
|
|
2431
|
+
}
|
|
2432
|
+
|
|
2399
2433
|
if (req.url.startsWith('/api/')) {
|
|
2400
2434
|
const raw = await readBody(req);
|
|
2401
2435
|
const payload = raw ? JSON.parse(raw) : {};
|
|
@@ -3206,9 +3240,30 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
3206
3240
|
}
|
|
3207
3241
|
}
|
|
3208
3242
|
|
|
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
|
+
|
|
3209
3263
|
if (req.url === '/api/inbox/add') {
|
|
3210
3264
|
const text = String(payload.text || '').trim();
|
|
3211
|
-
|
|
3265
|
+
const attachments = Array.isArray(payload.attachments) ? payload.attachments : [];
|
|
3266
|
+
if (!text && !attachments.length) return safeJson(res, 400, { error: 'Missing text' });
|
|
3212
3267
|
|
|
3213
3268
|
const d = isoDate();
|
|
3214
3269
|
const file = path.join(workspaceDir, 'logs', 'daily', `${d}.md`);
|
|
@@ -3222,7 +3277,13 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
3222
3277
|
const linksText = linkInfo && linkInfo.linksText ? linkInfo.linksText : '';
|
|
3223
3278
|
const slugs = linkInfo && Array.isArray(linkInfo.slugs) ? linkInfo.slugs : [];
|
|
3224
3279
|
|
|
3225
|
-
|
|
3280
|
+
// Build attachment markdown references
|
|
3281
|
+
let attachBlock = '';
|
|
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`;
|
|
3226
3287
|
fs.appendFileSync(file, block, 'utf8');
|
|
3227
3288
|
|
|
3228
3289
|
if (slugs.length) {
|
|
@@ -3281,7 +3342,16 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
3281
3342
|
]
|
|
3282
3343
|
};
|
|
3283
3344
|
|
|
3284
|
-
|
|
3345
|
+
// Build image references if attachments were provided
|
|
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`;
|
|
3285
3355
|
|
|
3286
3356
|
// Prefer COPILOT_CMD if provided, otherwise try 'copilot'
|
|
3287
3357
|
const cmd = process.env.COPILOT_CMD || 'copilot';
|
|
@@ -3291,7 +3361,7 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
3291
3361
|
// BUG-48: pass FREYA_WORKSPACE_DIR so the Copilot subprocess uses correct DB
|
|
3292
3362
|
const agentEnv = { FREYA_WORKSPACE_DIR: workspaceDir };
|
|
3293
3363
|
try {
|
|
3294
|
-
const r = await run(cmd, ['-s', '--no-color', '--stream', 'off', '-p', prompt, '--allow-all-tools'], workspaceDir, agentEnv);
|
|
3364
|
+
const r = await run(cmd, ['-s', '--no-color', '--stream', 'off', '-p', prompt, '--allow-all-tools', '--add-dir', workspaceDir], workspaceDir, agentEnv);
|
|
3295
3365
|
const out = (r.stdout + r.stderr).trim();
|
|
3296
3366
|
if (r.code !== 0) {
|
|
3297
3367
|
return safeJson(res, 200, {
|
|
@@ -3672,15 +3742,27 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
3672
3742
|
console.error('[oracle] RAG search failed (embedder/sharp unavailable), continuing without context:', ragErr.message);
|
|
3673
3743
|
}
|
|
3674
3744
|
|
|
3675
|
-
|
|
3745
|
+
// Build image references if attachments were provided
|
|
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`;
|
|
3676
3756
|
|
|
3677
3757
|
const cmd = process.env.COPILOT_CMD || 'copilot';
|
|
3678
3758
|
|
|
3679
3759
|
// BUG-48: pass FREYA_WORKSPACE_DIR so the Copilot subprocess uses correct DB
|
|
3680
3760
|
const oracleEnv = { FREYA_WORKSPACE_DIR: workspaceDir };
|
|
3681
3761
|
try {
|
|
3682
|
-
//
|
|
3683
|
-
const
|
|
3762
|
+
// Add --add-dir when images are attached so Copilot can access them
|
|
3763
|
+
const oracleArgs = ['-s', '--no-color', '--stream', 'off', '-p', prompt];
|
|
3764
|
+
if (askHasImages) { oracleArgs.push('--allow-all-tools', '--add-dir', workspaceDir); }
|
|
3765
|
+
const r = await run(cmd, oracleArgs, workspaceDir, oracleEnv);
|
|
3684
3766
|
const out = (r.stdout + r.stderr).trim();
|
|
3685
3767
|
if (r.code !== 0) {
|
|
3686
3768
|
return safeJson(res, 200, { ok: false, answer: 'Falha na busca do agente Oracle:\n' + (out || 'Exit code != 0'), sessionId });
|
|
@@ -4589,6 +4671,13 @@ function buildKanbanHtml(safeDefault, appVersion) {
|
|
|
4589
4671
|
</div>
|
|
4590
4672
|
</div>
|
|
4591
4673
|
|
|
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
|
+
|
|
4592
4681
|
<script>
|
|
4593
4682
|
window.__FREYA_DEFAULT_DIR = "${safeDefault}";
|
|
4594
4683
|
</script>
|
package/package.json
CHANGED