@cccarv82/freya 2.17.1 → 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 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
- chatAppend('user', query);
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
- chatAppend('user', text);
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 = '&times;';
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 modal or blur active element
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.&#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;"
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;"
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)">&times;</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
- if (!text) return safeJson(res, 400, { error: 'Missing text' });
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
- const block = `\n\n## [${hh}:${mm}] Raw Input\n${text}${linksText}\n`;
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`;
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
- 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`;
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
- 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`;
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
- // Removed --allow-all-tools and --add-dir to force reliance on RAG context
3683
- const r = await run(cmd, ['-s', '--no-color', '--stream', 'off', '-p', prompt], workspaceDir, oracleEnv);
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)">&times;</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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cccarv82/freya",
3
- "version": "2.17.1",
3
+ "version": "2.18.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",
@@ -1,128 +0,0 @@
1
- /**
2
- * generate-weekly-report.js
3
- * Generates a weekly Markdown report aggregating Tasks, Blockers, Career entries,
4
- * and Project Updates from the SQLite database.
5
- *
6
- * Usage: node scripts/generate-weekly-report.js
7
- */
8
-
9
- const fs = require('fs');
10
- const path = require('path');
11
-
12
- const { toIsoDate, safeParseToMs } = require('./lib/date-utils');
13
- const DataManager = require('./lib/DataManager');
14
- const { ready } = require('./lib/DataLayer');
15
-
16
- // --- Configuration (BUG-30: use FREYA_WORKSPACE_DIR instead of __dirname) ---
17
- const WORKSPACE_DIR = process.env.FREYA_WORKSPACE_DIR
18
- ? path.resolve(process.env.FREYA_WORKSPACE_DIR)
19
- : path.join(__dirname, '..'); // fallback: scripts/ is one level below workspace root
20
-
21
- const REPORT_DIR = path.join(WORKSPACE_DIR, 'docs', 'reports');
22
-
23
- // --- Date Logic ---
24
- const now = new Date();
25
- const oneDay = 24 * 60 * 60 * 1000;
26
-
27
- function isWithinWeek(dateStr) {
28
- const ms = safeParseToMs(dateStr);
29
- if (!Number.isFinite(ms)) return false;
30
- const sevenDaysAgo = now.getTime() - (7 * oneDay);
31
- return ms >= sevenDaysAgo && ms <= now.getTime();
32
- }
33
-
34
- function getFormattedDate() {
35
- return toIsoDate(now);
36
- }
37
-
38
- function getFormattedTime() {
39
- const hh = String(now.getHours()).padStart(2, '0');
40
- const mm = String(now.getMinutes()).padStart(2, '0');
41
- const ss = String(now.getSeconds()).padStart(2, '0');
42
- return `${hh}${mm}${ss}`;
43
- }
44
-
45
- // --- Report Generation ---
46
- async function generateWeeklyReport() {
47
- await ready;
48
-
49
- const start = new Date(now.getTime() - 7 * oneDay);
50
- const end = now;
51
-
52
- const dm = new DataManager();
53
-
54
- // Fetch data from SQLite
55
- const { completed: completedTasks } = dm.getTasks(start, end);
56
- const { open: openBlockers, resolvedRecent } = dm.getBlockers(start, end);
57
- const projectUpdates = dm.getProjectUpdates(start, end);
58
- const careerEntries = dm.getCareerEntries ? dm.getCareerEntries(start, end) : [];
59
-
60
- // Ensure output dir exists
61
- if (!fs.existsSync(REPORT_DIR)) {
62
- fs.mkdirSync(REPORT_DIR, { recursive: true });
63
- }
64
-
65
- const reportDate = getFormattedDate();
66
- const reportTime = getFormattedTime();
67
- let report = `# Weekly Report - ${reportDate}\n\n`;
68
-
69
- // Projects
70
- report += '## Project Updates\n';
71
- if (projectUpdates.length > 0) {
72
- projectUpdates.forEach(p => {
73
- report += `### ${p.client || 'Unknown'} - ${p.project || p.slug || 'Unknown'}\n`;
74
- const events = Array.isArray(p.events) ? p.events : [];
75
- events.forEach(e => {
76
- const dateStr = e.date ? String(e.date).slice(0, 10) : 'Unknown Date';
77
- report += `- **${dateStr}**: ${e.content || ''}\n`;
78
- });
79
- report += '\n';
80
- });
81
- } else {
82
- report += 'No project updates recorded this week.\n\n';
83
- }
84
-
85
- // Completed Tasks
86
- report += '## Completed Tasks\n';
87
- if (completedTasks.length > 0) {
88
- completedTasks.forEach(t => {
89
- const projectTag = t.projectSlug || t.project_slug ? `[${t.projectSlug || t.project_slug}] ` : '';
90
- report += `- ${projectTag}${t.description}\n`;
91
- });
92
- } else {
93
- report += 'No tasks completed this week.\n';
94
- }
95
- report += '\n';
96
-
97
- // Open Blockers
98
- report += '## Open Blockers\n';
99
- if (openBlockers.length > 0) {
100
- openBlockers.forEach(b => {
101
- const sev = b.severity ? `[${b.severity}] ` : '';
102
- report += `- ${sev}${b.title}\n`;
103
- });
104
- } else {
105
- report += 'No open blockers.\n';
106
- }
107
- report += '\n';
108
-
109
- // Career entries (if DataManager supports it)
110
- if (Array.isArray(careerEntries) && careerEntries.length > 0) {
111
- report += '## Career Highlights\n';
112
- careerEntries.forEach(e => {
113
- report += `- **[${e.type || 'Note'}]**: ${e.description || e.content || ''}\n`;
114
- });
115
- report += '\n';
116
- }
117
-
118
- // 3. Save and Output
119
- const outputPath = path.join(REPORT_DIR, `weekly-${reportDate}-${reportTime}.md`);
120
- fs.writeFileSync(outputPath, report);
121
-
122
- console.log(`Report generated at: ${outputPath}`);
123
- console.log('---------------------------------------------------');
124
- console.log(report);
125
- console.log('---------------------------------------------------');
126
- }
127
-
128
- generateWeeklyReport().catch(err => { console.error(err); process.exit(1); });