Skip to content

Latest commit

 

History

History
338 lines (307 loc) · 12.2 KB

File metadata and controls

338 lines (307 loc) · 12.2 KB
tags
dashboard/ai
cssclasses
jarvis-page
const container = this.container;

// ── Prevent full re-render on vault file changes ──
// Dataview re-runs this block on every file change. Reuse existing DOM
// instead of rebuilding the entire dashboard (preserves terminal state, timers, etc.).
// Uses labeled block + break (DataviewJS uses eval, so bare `return` is illegal).
__jarvis__: {
if (window.__jarvisDashboard?.wrapper) {
  container.appendChild(window.__jarvisDashboard.styleEl);
  container.appendChild(window.__jarvisDashboard.wrapper);
  break __jarvis__;
}

const nodeFs = require("fs");
const nodePath = require("path");

// ── Resolve base path (works wherever the dashboard folder is placed) ──
const vaultBase = app.vault.adapter.basePath;
const currentFilePath = dv.current().file.path;
const dashboardDir = nodePath.dirname(nodePath.join(vaultBase, currentFilePath));
const srcDir = nodePath.join(dashboardDir, "src");

// ── Module loader ──
const _AsyncFunction = Object.getPrototypeOf(async function(){}).constructor;
function loadModule(relativePath) {
  const fullPath = nodePath.join(srcDir, relativePath);
  const code = nodeFs.readFileSync(fullPath, "utf8");
  return new _AsyncFunction("ctx", code);
}

// ── Load config ──
const config = JSON.parse(nodeFs.readFileSync(nodePath.join(srcDir, "config", "config.json"), "utf8"));

// ── Load core: theme ──
const themeResult = await loadModule("core/theme.js")({ container, config });
const { T, isNarrow, isMedium, isWide, CARD_PAD, FONT_SM, leafEl } = themeResult;

// ── Load core: styles ──
const styleEl = await loadModule("core/styles.js")({ T, config });
container.appendChild(styleEl);

// ── Load core: helpers ──
const helpers = await loadModule("core/helpers.js")({ T, isNarrow });
const { el, fmtTokens, fmtCost, formatModel, describeAction, getModelFamily, addHoverEffect, createSectionTitle } = helpers;

// ── Load core: markdown renderer ──
const markdownRenderer = await loadModule("core/markdown-renderer.js")({ el, T, config });

// ── Load registry ──
const registryPath = config.widgets?.agentCards?.registryPath || "src/config/Jarvis-Registry";
const dashboardFolder = currentFilePath.replace(/\/[^/]+$/, "");
const fullRegistryPath = dashboardFolder + "/" + registryPath;
const registry = dv.page(fullRegistryPath);
const agents = registry?.agents || [];
const agentNames = new Set();
const skillToAgent = new Map();
agents.forEach(a => {
  agentNames.add(a.name);
  (a.skills || []).forEach(s => skillToAgent.set(s, a.name));
});

// ── Performance config shortcuts ──
const perf = config.performance || {};
const animationsEnabled = perf.animationsEnabled !== false;

// ── Build shared context ──
const ctx = {
  el, T, config, container, dv,
  isNarrow, isMedium, isWide, leafEl,
  nodeFs, nodePath, CARD_PAD, FONT_SM,
  fmtTokens, fmtCost, formatModel, describeAction, getModelFamily, addHoverEffect, createSectionTitle,
  markdownRenderer,
  agents, agentNames, skillToAgent,
  agentCardRefs: new Map(),
  onStatsReady: [],
  intervals: [],
  cleanups: [],
  _paused: false,
  _srcDir: srcDir + "/",
  _adapter: {
    readFile(path) { return nodeFs.readFileSync(path, "utf8"); },
    readFileAsync(path) { return nodeFs.readFileSync(path, "utf8"); },
    vaultBasePath() { return vaultBase; },
  },
  // Performance
  perf,
  animationsEnabled,
  // Pausable registry for centralized visibility pause/resume
  _pausables: [],
  registerPausable(startFn, stopFn) {
    ctx._pausables.push({ start: startFn, stop: stopFn });
  },
};

// ── Load services ──
ctx.sessionParser = await loadModule("services/session-parser.js")(ctx);
ctx.cleanups.push(() => { if (ctx.sessionParser.cleanup) ctx.sessionParser.cleanup(); });
ctx.statsEngine = await loadModule("services/stats-engine.js")(ctx);
ctx.timerService = await loadModule("services/timer-service.js")(ctx);
ctx.voiceService = await loadModule("services/voice-service.js")(ctx);
ctx.cleanups.push(() => ctx.voiceService.cleanup());
ctx.ttsService = await loadModule("services/tts-service.js")(ctx);
ctx.cleanups.push(() => ctx.ttsService.cleanup());
ctx._sessionManagerCore = await loadModule("services/session-manager-core.js")(ctx);
ctx.sessionManager = await loadModule("services/session-manager.js")(ctx);
ctx.cleanups.push(() => ctx.sessionManager.cleanup());

// ── Load network client for remote voice mode ──
if (config.widgets?.voiceCommand?.mode === "remote") {
  let localConfig = {};
  try {
    localConfig = JSON.parse(nodeFs.readFileSync(
      nodePath.join(srcDir, "config", "config.local.json"), "utf8"));
    function deepMerge(target, source) {
      const result = { ...target };
      for (const key of Object.keys(source)) {
        if (source[key] && typeof source[key] === "object" && !Array.isArray(source[key])) {
          result[key] = deepMerge(result[key] || {}, source[key]);
        } else {
          result[key] = source[key];
        }
      }
      return result;
    }
    Object.assign(config, deepMerge(config, localConfig));
  } catch {}
  ctx._localConfig = localConfig;
  ctx.networkClient = await loadModule("services/network-client.js")(ctx);
  ctx.cleanups.push(() => ctx.networkClient.cleanup());
}

// ── Create main wrapper ──
const wrapper = el("div", {
  background: T.bg,
  minHeight: "100vh",
  padding: isNarrow ? "16px" : "32px",
  fontFamily: "'Inter', -apple-system, BlinkMacSystemFont, sans-serif",
  color: T.text,
  position: "relative",
  overflow: "hidden",
});
container.appendChild(wrapper);

// ── Scan line overlay ──
if (config.dashboard?.showScanLine !== false) {
  wrapper.appendChild(el("div", {
    position: "absolute",
    left: "0", width: "100%", height: "6%",
    background: "linear-gradient(180deg, transparent, rgba(0,212,255,0.04), transparent)",
    pointerEvents: "none", zIndex: "1",
    animation: animationsEnabled ? "jarvisScanLine 8s linear infinite" : "none",
    willChange: animationsEnabled ? "top" : "auto",
    contain: "layout style",
  }));
}

// ── Widget map ──
const WIDGET_MAP = {
  "header":                "widgets/header/index.js",
  "live-sessions":         "widgets/live-sessions/index.js",
  "system-diagnostics":    "widgets/system-diagnostics/index.js",
  "agent-cards":           "widgets/agent-cards/index.js",
  "activity-analytics":    "widgets/activity-analytics/index.js",
  "communication-link":    "widgets/communication-link/index.js",
  "focus-timer":           "widgets/focus-timer/index.js",
  "quick-capture":         "widgets/quick-capture/index.js",
  "quick-launch":          "widgets/quick-launch/index.js",
  "mission-control":       "widgets/mission-control/index.js",
  "recent-activity":       "widgets/recent-activity/index.js",
  "jarvis-voice-command":  "widgets/voice-command/index.js",
  "footer":                "widgets/footer/index.js",
};

// ── Widgets that benefit from content-visibility: auto (below the fold) ──
const DEFERRED_WIDGETS = new Set([
  "activity-analytics", "recent-activity", "footer",
  "system-diagnostics", "mission-control",
]);

// ── Render layout ──
const layout = config.layout || [
  { type: "header" },
  { type: "live-sessions" },
  { type: "row", columns: 2, widgets: ["focus-timer", "quick-capture"] },
  { type: "agent-cards" },
  { type: "communication-link" },
  { type: "row", columns: 2, widgets: ["quick-launch", "mission-control"] },
  { type: "system-diagnostics" },
  { type: "activity-analytics" },
  { type: "recent-activity" },
  { type: "footer" },
];

const gridRefs = [];

for (const entry of layout) {
  if (entry.type === "row" && entry.widgets) {
    const row = el("div", {
      display: "grid",
      gridTemplateColumns: isNarrow ? "1fr" : `repeat(${entry.columns || 2}, 1fr)`,
      gap: isNarrow ? "12px" : "20px",
      position: "relative",
      zIndex: "2",
      marginBottom: isNarrow ? "24px" : "40px",
      contain: "layout style",
    });
    for (const widgetType of entry.widgets) {
      if (WIDGET_MAP[widgetType]) {
        const widget = await loadModule(WIDGET_MAP[widgetType])(ctx);
        if (widget.style) {
          widget.style.marginBottom = "0";
          widget.style.contain = "layout style";
          if (DEFERRED_WIDGETS.has(widgetType)) {
            widget.style.contentVisibility = "auto";
          }
        }
        row.appendChild(widget);
      }
    }
    wrapper.appendChild(row);
    gridRefs.push({ el: row, columns: entry.columns || 2 });
  } else if (WIDGET_MAP[entry.type]) {
    const widget = await loadModule(WIDGET_MAP[entry.type])(ctx);
    if (widget.style) {
      widget.style.contain = "layout style";
      if (DEFERRED_WIDGETS.has(entry.type)) {
        widget.style.contentVisibility = "auto";
      }
    }
    wrapper.appendChild(widget);
  }
}

// ── Trigger async stats ──
if (ctx.sessionParser.hasWorker) {
  // Worker mode: request stats from background thread
  ctx._onWorkerStats = (stats) => {
    ctx.onStatsReady.forEach(cb => { try { cb(stats); } catch {} });
    ctx._onWorkerStats = null; // one-shot
  };
  ctx.sessionParser.requestWorkerStats();
} else {
  // Fallback: compute on main thread after initial render
  setTimeout(() => {
    try {
      const stats = ctx.statsEngine.computeStats();
      ctx.onStatsReady.forEach(cb => cb(stats));
    } catch {}
  }, 100);
}

// ── Centralized pause/resume when dashboard is not visible ──
const _visibilityHandler = () => {
  const hidden = document.hidden;
  ctx._paused = hidden;
  if (hidden) {
    // Pause all CSS animations via class toggle (no DOM iteration)
    wrapper.classList.add("jarvis-bg-paused");
    // Stop all registered interval-based work
    ctx._pausables.forEach(p => { try { p.stop(); } catch(e) {} });
  } else {
    // Resume all CSS animations
    wrapper.classList.remove("jarvis-bg-paused");
    // Restart all registered interval-based work
    ctx._pausables.forEach(p => { try { p.start(); } catch(e) {} });
  }
};
document.addEventListener("visibilitychange", _visibilityHandler);
ctx.cleanups.push(() => document.removeEventListener("visibilitychange", _visibilityHandler));

// ── Store dashboard reference for re-render prevention ──
window.__jarvisDashboard = { wrapper, styleEl };

// ── Cleanup (runs when note is actually closed, not on Dataview re-render) ──
function dashboardCleanup() {
  ctx.intervals.forEach(id => clearInterval(id));
  ctx.cleanups.forEach(fn => { try { fn(); } catch(e) {} });
  window.__jarvisDashboard = null;
}

// MutationObserver: detect when wrapper is removed from DOM
// Checks wrapper (not container) because wrapper is re-parented across Dataview re-renders
let cleanupTimer = null;
const observer = new MutationObserver(() => {
  if (!document.contains(wrapper)) {
    if (!cleanupTimer) {
      cleanupTimer = setTimeout(() => {
        if (!document.contains(wrapper)) {
          observer.disconnect();
          dashboardCleanup();
        }
        cleanupTimer = null;
      }, 500);
    }
  }
});
const observeTarget = container.parentElement || document.body;
observer.observe(observeTarget, { childList: true, subtree: false });

// Fallback: periodic check in case MutationObserver misses note close
ctx.intervals.push(setInterval(() => {
  if (!document.contains(wrapper)) {
    observer.disconnect();
    dashboardCleanup();
  }
}, 10000));

// ── Responsive resize ──
const resizeTarget = leafEl || container;
const ro = new ResizeObserver(() => {
  const w = leafEl ? leafEl.clientWidth : window.innerWidth;
  const narrow = w < 500;
  const wide = w >= 950;

  if (ctx._agentsGrid) ctx._agentsGrid.style.gridTemplateColumns = narrow ? "1fr" : (wide ? "repeat(3, 1fr)" : "1fr");
  if (ctx._diagGrid) ctx._diagGrid.style.gridTemplateColumns = narrow ? "repeat(2, 1fr)" : "repeat(4, 1fr)";
  if (ctx._analyticsGrid) ctx._analyticsGrid.style.gridTemplateColumns = narrow ? "1fr" : "repeat(3, 1fr)";
  if (ctx._bookmarkGroups) {
    ctx._bookmarkGroups.forEach(ref => {
      ref.el.style.gridTemplateColumns = narrow ? "repeat(3, 1fr)" : `repeat(${Math.min(ref.count, 4)}, 1fr)`;
    });
  }

  gridRefs.forEach(ref => {
    ref.el.style.gridTemplateColumns = narrow ? "1fr" : `repeat(${ref.columns}, 1fr)`;
  });
});
ro.observe(resizeTarget);
} // end __jarvis__ block