@cccarv82/freya 3.0.0 → 3.1.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 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
- pastedImages: [] // { file: File, dataUrl: string, name: string }
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,2000})/gi;
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
- lines.push(num + '. \u{1F4DD} **Registrar no log:** ' + m[1]);
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
- // Upload pasted images if any (for visual context in log)
507
- var askAttachments = [];
508
- if (state.pastedImages.length > 0) {
509
- askAttachments = await uploadPastedImages();
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
- setPill('run', askAttachments.length ? 'enviando imagem + pesquisando…' : 'pesquisando…');
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
- const r = await api('/api/chat/ask', { dir: dirOrDefault(), sessionId, query, attachments: askAttachments });
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
- // Upload pasted images if any
2366
- var attachments = [];
2367
- var hasImages = state.pastedImages.length > 0;
2368
- if (hasImages) {
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
- // Typing indicator while processing
2389
- var saveTypingId = 'typing-save-' + Date.now();
2390
- chatAppend('assistant', '<div class="typing-indicator"><span></span><span></span><span></span></div>', { id: saveTypingId, html: true });
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
- await api('/api/inbox/add', { dir: dirOrDefault(), text, attachments });
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', attachments.length ? 'processando texto + imagens…' : 'processando…');
2396
- const r = await api('/api/agents/plan', { dir: dirOrDefault(), text, attachments });
2390
+ setPill('run', 'processando…');
2391
+ const r = await api('/api/agents/plan', { dir: dirOrDefault(), text });
2397
2392
 
2398
2393
  // Remove typing indicator
2399
- var typEl = $(saveTypingId);
2400
- if (typEl) typEl.remove();
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
- var applyTypingId = 'typing-apply-' + Date.now();
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,31 @@
2485
2476
  state.lastApplied = r.applied || null;
2486
2477
  const summary = r.applied || {};
2487
2478
 
2488
- // Build human-friendly summary instead of raw JSON
2479
+ // Build natural language summary instead of raw JSON
2489
2480
  var parts = [];
2490
- var tasks = Number(summary.tasks || 0);
2491
- var blockers = Number(summary.blockers || 0);
2492
- var skippedT = Number(summary.tasksSkipped || 0);
2493
- var skippedB = Number(summary.blockersSkipped || 0);
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 + '**');
2494
2488
 
2495
- if (tasks > 0) parts.push('✅ **' + tasks + ' tarefa' + (tasks > 1 ? 's' : '') + '** criada' + (tasks > 1 ? 's' : ''));
2496
- if (blockers > 0) parts.push('🚧 **' + blockers + ' blocker' + (blockers > 1 ? 's' : '') + '** registrado' + (blockers > 1 ? 's' : ''));
2497
- if (skippedT > 0) parts.push('⏭️ ' + skippedT + ' tarefa' + (skippedT > 1 ? 's' : '') + ' já existente' + (skippedT > 1 ? 's' : ''));
2498
- if (skippedB > 0) parts.push('⏭️ ' + skippedB + ' blocker' + (skippedB > 1 ? 's' : '') + ' já existente' + (skippedB > 1 ? 's' : ''));
2499
- if (tasks === 0 && blockers === 0) parts.push('📝 Informação registrada no log diário');
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
+ }
2500
2493
 
2501
- let msg = parts.join('\n');
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
+ }
2502
2500
 
2503
2501
  if (summary && Array.isArray(summary.reportsSuggested) && summary.reportsSuggested.length) {
2504
- msg += '\n\n📊 **Relatórios sugeridos:** ' + summary.reportsSuggested.join(', ');
2502
+ msg += '\n**Relatórios sugeridos:** ' + summary.reportsSuggested.join(', ');
2503
+ msg += '\n\nUse: **Rodar relatórios sugeridos** (barra lateral)';
2505
2504
  }
2506
2505
 
2507
2506
  setOut(msg);
@@ -2527,6 +2526,98 @@
2527
2526
  loadLocal();
2528
2527
  wireRailNav();
2529
2528
 
2529
+ // ── Image paste (Ctrl+V) support ──
2530
+ (function setupImagePaste() {
2531
+ var ta = $('inboxText');
2532
+ if (!ta) return;
2533
+
2534
+ ta.addEventListener('paste', async function(e) {
2535
+ var items = e.clipboardData && e.clipboardData.items;
2536
+ if (!items) return;
2537
+ for (var i = 0; i < items.length; i++) {
2538
+ if (items[i].type.indexOf('image') !== -1) {
2539
+ e.preventDefault();
2540
+ var file = items[i].getAsFile();
2541
+ if (!file) return;
2542
+ var reader = new FileReader();
2543
+ reader.onload = async function(ev) {
2544
+ var base64 = ev.target.result.split(',')[1];
2545
+ var mimeType = file.type || 'image/png';
2546
+ try {
2547
+ var r = await api('/api/attachments/upload', { dir: dirOrDefault(), data: base64, mimeType: mimeType });
2548
+ if (r && r.ok) {
2549
+ state.pendingImage = { filename: r.filename, url: r.url, mimeType: mimeType };
2550
+ showImagePreview(r.url, r.filename);
2551
+ }
2552
+ } catch (err) {
2553
+ console.error('Image upload failed:', err);
2554
+ }
2555
+ };
2556
+ reader.readAsDataURL(file);
2557
+ break;
2558
+ }
2559
+ }
2560
+ });
2561
+ })();
2562
+
2563
+ function showImagePreview(url, filename) {
2564
+ var existing = document.getElementById('pastePreview');
2565
+ if (existing) existing.remove();
2566
+ var ta = $('inboxText');
2567
+ if (!ta) return;
2568
+ var container = document.createElement('div');
2569
+ container.id = 'pastePreview';
2570
+ 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;';
2571
+ var img = document.createElement('img');
2572
+ img.src = url;
2573
+ img.style.cssText = 'max-height:48px; max-width:80px; border-radius:4px; cursor:pointer;';
2574
+ img.onclick = function() { openLightbox(url); };
2575
+ var label = document.createElement('span');
2576
+ label.textContent = '📎 ' + filename;
2577
+ label.style.cssText = 'font-size:12px; color:var(--muted); flex:1;';
2578
+ var removeBtn = document.createElement('button');
2579
+ removeBtn.textContent = '✕';
2580
+ removeBtn.style.cssText = 'background:none; border:none; color:var(--muted); cursor:pointer; font-size:14px;';
2581
+ removeBtn.onclick = function() { container.remove(); state.pendingImage = null; };
2582
+ container.appendChild(img);
2583
+ container.appendChild(label);
2584
+ container.appendChild(removeBtn);
2585
+ ta.parentElement.insertBefore(container, ta.nextSibling);
2586
+ }
2587
+
2588
+ // ── Image lightbox modal ──
2589
+ function openLightbox(url) {
2590
+ var existing = document.getElementById('freyaLightbox');
2591
+ if (existing) existing.remove();
2592
+ var overlay = document.createElement('div');
2593
+ overlay.id = 'freyaLightbox';
2594
+ 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;';
2595
+ overlay.onclick = function(e) { if (e.target === overlay) overlay.remove(); };
2596
+ var img = document.createElement('img');
2597
+ img.src = url;
2598
+ img.style.cssText = 'max-width:90vw; max-height:90vh; border-radius:8px; box-shadow:0 8px 40px rgba(0,0,0,0.6);';
2599
+ var closeBtn = document.createElement('button');
2600
+ closeBtn.textContent = '✕';
2601
+ 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;';
2602
+ closeBtn.onclick = function() { overlay.remove(); };
2603
+ overlay.appendChild(img);
2604
+ overlay.appendChild(closeBtn);
2605
+ document.body.appendChild(overlay);
2606
+ // ESC to close
2607
+ function onEsc(e) { if (e.key === 'Escape') { overlay.remove(); document.removeEventListener('keydown', onEsc); } }
2608
+ document.addEventListener('keydown', onEsc);
2609
+ }
2610
+ window.openLightbox = openLightbox;
2611
+
2612
+ // Make all chat images clickable for lightbox (delegate)
2613
+ document.addEventListener('click', function(e) {
2614
+ var img = e.target;
2615
+ if (img.tagName === 'IMG' && img.closest && img.closest('#chatThread')) {
2616
+ e.preventDefault();
2617
+ openLightbox(img.src);
2618
+ }
2619
+ });
2620
+
2530
2621
  // Developer drawer (persist open/close)
2531
2622
  try {
2532
2623
  const d = $('devDrawer');
@@ -2616,149 +2707,8 @@
2616
2707
  refreshToday();
2617
2708
  reloadSlugRules();
2618
2709
  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
2710
  })();
2631
2711
 
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 = '&times;';
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
2712
  setPill('ok', 'pronto');
2763
2713
 
2764
2714
  /* ── Global Keyboard Shortcuts ── */
@@ -2775,13 +2725,8 @@
2775
2725
  openQuickAdd();
2776
2726
  return;
2777
2727
  }
2778
- // Escape: Close lightbox → quick-add blur
2728
+ // Escape: Close quick-add modal or blur active element
2779
2729
  if (e.key === 'Escape') {
2780
- const lb = $('imgLightbox');
2781
- if (lb && lb.classList.contains('active')) {
2782
- closeLightbox();
2783
- return;
2784
- }
2785
2730
  const overlay = $('quickAddOverlay');
2786
2731
  if (overlay && overlay.style.display !== 'none') {
2787
2732
  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.&#10;&#10;▸ Salvar & Processar → extrai tarefas e blockers do texto&#10;▸ Perguntar → consulta o histórico via busca semântica (RAG)&#10;▸ 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;"
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.&#10;&#10;▸ Salvar & Processar → extrai tarefas e blockers do texto&#10;▸ 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)">&times;</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 (pasted screenshots)
2399
+ // Serve attachment images from workspace data/attachments/
2412
2400
  if (req.method === 'GET' && req.url.startsWith('/attachments/')) {
2413
- const relPath = decodeURIComponent(req.url.replace('/attachments/', ''));
2401
+ const fname = decodeURIComponent(req.url.slice('/attachments/'.length)).replace(/[\/\\]/g, '');
2414
2402
  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);
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
- res.writeHead(404); res.end(); return;
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
- const attachments = Array.isArray(payload.attachments) ? payload.attachments : [];
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
- // Build attachment markdown references
3281
- let attachBlock = '';
3282
- if (attachments.length) {
3283
- attachBlock = '\n\n**Anexos:**\n' + attachments.map(a => `![${a.name || 'screenshot'}](${a.path})`).join('\n');
3284
- }
3285
-
3286
- const block = `\n\n## [${hh}:${mm}] Raw Input\n${text}${linksText}${attachBlock}\n`;
3246
+ const imgRef = payload.imagePath ? `\n![anexo](../${payload.imagePath})` : '';
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
- // 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`;
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', '--add-dir', workspaceDir], workspaceDir, agentEnv);
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, {
@@ -3707,9 +3659,25 @@ async function cmdWeb({ port, dir, open, dev }) {
3707
3659
  return safeJson(res, r.code === 0 ? 200 : 400, r.code === 0 ? { ok: true, output: out } : { error: out || 'index rebuild failed', output: out });
3708
3660
  }
3709
3661
 
3662
+ // Upload attachment (base64 image from clipboard paste)
3663
+ if (req.url === '/api/attachments/upload') {
3664
+ const base64 = String(payload.data || '').trim();
3665
+ const mimeType = String(payload.mimeType || 'image/png').trim();
3666
+ if (!base64) return safeJson(res, 400, { error: 'Missing data' });
3667
+ const ext = mimeType === 'image/jpeg' ? '.jpg' : mimeType === 'image/gif' ? '.gif' : mimeType === 'image/webp' ? '.webp' : '.png';
3668
+ const fname = 'paste-' + Date.now() + ext;
3669
+ const attachDir = path.join(workspaceDir, 'data', 'attachments');
3670
+ if (!exists(attachDir)) fs.mkdirSync(attachDir, { recursive: true });
3671
+ const filePath = path.join(attachDir, fname);
3672
+ const buf = Buffer.from(base64, 'base64');
3673
+ fs.writeFileSync(filePath, buf);
3674
+ return safeJson(res, 200, { ok: true, filename: fname, url: '/attachments/' + fname });
3675
+ }
3676
+
3710
3677
  if (req.url === '/api/chat/ask') {
3711
3678
  const sessionId = String(payload.sessionId || '').trim();
3712
3679
  const query = String(payload.query || '').trim();
3680
+ const imagePath = payload.imagePath ? String(payload.imagePath).trim() : null;
3713
3681
  if (!query) return safeJson(res, 400, { error: 'Missing query' });
3714
3682
 
3715
3683
  const workspaceRulesBase = path.join(workspaceDir, '.agent', 'rules', 'freya');
@@ -3742,27 +3710,23 @@ async function cmdWeb({ port, dir, open, dev }) {
3742
3710
  console.error('[oracle] RAG search failed (embedder/sharp unavailable), continuing without context:', ragErr.message);
3743
3711
  }
3744
3712
 
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`;
3713
+ 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
3714
 
3757
3715
  const cmd = process.env.COPILOT_CMD || 'copilot';
3758
3716
 
3759
3717
  // BUG-48: pass FREYA_WORKSPACE_DIR so the Copilot subprocess uses correct DB
3760
3718
  const oracleEnv = { FREYA_WORKSPACE_DIR: workspaceDir };
3761
3719
  try {
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);
3720
+ // Build copilot args; add image if user pasted a screenshot
3721
+ const copilotArgs = ['-s', '--no-color', '--stream', 'off'];
3722
+ if (imagePath) {
3723
+ const absImg = path.isAbsolute(imagePath) ? imagePath : path.join(workspaceDir, imagePath);
3724
+ if (exists(absImg)) {
3725
+ copilotArgs.push('--add-image', absImg);
3726
+ }
3727
+ }
3728
+ copilotArgs.push('-p', prompt);
3729
+ const r = await run(cmd, copilotArgs, workspaceDir, oracleEnv);
3766
3730
  const out = (r.stdout + r.stderr).trim();
3767
3731
  if (r.code !== 0) {
3768
3732
  return safeJson(res, 200, { ok: false, answer: 'Falha na busca do agente Oracle:\n' + (out || 'Exit code != 0'), sessionId });
@@ -3893,7 +3857,8 @@ async function cmdWeb({ port, dir, open, dev }) {
3893
3857
 
3894
3858
  const rawTasks = dl.db.prepare(query).all(...params);
3895
3859
  const tasks = rawTasks.map(t => {
3896
- const meta = t.metadata ? JSON.parse(t.metadata) : {};
3860
+ let meta = {};
3861
+ try { meta = t.metadata ? JSON.parse(t.metadata) : {}; } catch (_) {}
3897
3862
  return {
3898
3863
  id: t.id,
3899
3864
  description: t.description,
@@ -4091,7 +4056,8 @@ async function cmdWeb({ port, dir, open, dev }) {
4091
4056
 
4092
4057
  const rawBlockers = dl.db.prepare(query).all(...params);
4093
4058
  const blockers = rawBlockers.map(b => {
4094
- const meta = b.metadata ? JSON.parse(b.metadata) : {};
4059
+ let meta = {};
4060
+ try { meta = b.metadata ? JSON.parse(b.metadata) : {}; } catch (_) {}
4095
4061
  return {
4096
4062
  id: b.id,
4097
4063
  title: b.title,
@@ -4671,13 +4637,6 @@ function buildKanbanHtml(safeDefault, appVersion) {
4671
4637
  </div>
4672
4638
  </div>
4673
4639
 
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)">&times;</button>
4677
- <img id="imgLightboxImg" src="" alt="preview" onclick="event.stopPropagation()" />
4678
- <div id="imgLightboxName" class="img-lightbox-name"></div>
4679
- </div>
4680
-
4681
4640
  <script>
4682
4641
  window.__FREYA_DEFAULT_DIR = "${safeDefault}";
4683
4642
  </script>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cccarv82/freya",
3
- "version": "3.0.0",
3
+ "version": "3.1.0",
4
4
  "description": "Personal AI Assistant with local-first persistence",
5
5
  "scripts": {
6
6
  "health": "node scripts/validate-data.js && node scripts/validate-structure.js",