@mongoosejs/studio 0.5.3 → 0.5.4

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/frontend/index.js CHANGED
@@ -35,8 +35,7 @@ module.exports = async function frontend(apiUrl, isLambda, options, workspace) {
35
35
  baseURL: apiUrl,
36
36
  isLambda,
37
37
  mothershipUrl: apiKey ? mothershipUrl : null,
38
- workspace: workspaceData,
39
- hasLLMAPIKey: [options?.anthropicAPIKey, options?.googleGeminiAPIKey, options?.openAIAPIKey].some(v => v != null)
38
+ workspace: workspaceData
40
39
  };
41
40
 
42
41
  if (isLambda) {
@@ -2118,9 +2118,9 @@ module.exports = {
2118
2118
 
2119
2119
  this.draftAgentMode = this.getAgentModePreference();
2120
2120
  this.chatThreadId = this.threadId;
2121
- const [{ chatThreads }, { capabilities }] = await Promise.all([
2121
+ const [{ chatThreads }, capabilities] = await Promise.all([
2122
2122
  api.ChatThread.listChatThreads(),
2123
- api.getCapabilities()
2123
+ api.getCapabilities().catch(() => this.capabilities)
2124
2124
  ]);
2125
2125
  this.chatThreads = chatThreads;
2126
2126
  this.capabilities = capabilities;
@@ -6646,6 +6646,7 @@ const {
6646
6646
  appendCSS(__webpack_require__(/*! vue-toastification/dist/index.css */ "./node_modules/vue-toastification/dist/index.css"));
6647
6647
 
6648
6648
  const TRACKED_RECENT_PAGE_ROUTE_NAMES = new Set(['model', 'document', 'dashboard', 'chat']);
6649
+ const CHAT_ROUTE_NAMES = new Set(['chat index', 'chat']);
6649
6650
 
6650
6651
  function formatHistoryLabel(route) {
6651
6652
  if (!route || typeof route.path !== 'string') {
@@ -6858,8 +6859,12 @@ app.component('app-component', {
6858
6859
  window.localStorage.setItem('_mongooseStudioAccessToken', accessToken._id);
6859
6860
 
6860
6861
  try {
6861
- const { nodeEnv } = await api.status();
6862
+ const [{ nodeEnv }, capabilities] = await Promise.all([
6863
+ api.status(),
6864
+ loadStudioCapabilities()
6865
+ ]);
6862
6866
  this.nodeEnv = nodeEnv;
6867
+ this.capabilities = capabilities;
6863
6868
  } catch (err) {
6864
6869
  this.authError = 'Error connecting to Mongoose Studio API: ' + err.response?.data?.message ?? err.message;
6865
6870
  this.status = 'loaded';
@@ -6879,12 +6884,14 @@ app.component('app-component', {
6879
6884
  const { user, roles } = await mothership.me();
6880
6885
 
6881
6886
  try {
6882
- const [{ nodeEnv }, { modelSchemaPaths }] = await Promise.all([
6887
+ const [{ nodeEnv }, { modelSchemaPaths }, capabilities] = await Promise.all([
6883
6888
  api.status(),
6884
- api.Model.listModels()
6889
+ api.Model.listModels(),
6890
+ loadStudioCapabilities()
6885
6891
  ]);
6886
6892
  this.nodeEnv = nodeEnv;
6887
6893
  this.modelSchemaPaths = modelSchemaPaths;
6894
+ this.capabilities = capabilities;
6888
6895
  } catch (err) {
6889
6896
  this.authError = 'Error connecting to Mongoose Studio API: ' + (err.response?.data?.message ?? err.message);
6890
6897
  this.status = 'loaded';
@@ -6897,12 +6904,14 @@ app.component('app-component', {
6897
6904
  }
6898
6905
  } else {
6899
6906
  try {
6900
- const [{ nodeEnv }, { modelSchemaPaths }] = await Promise.all([
6907
+ const [{ nodeEnv }, { modelSchemaPaths }, capabilities] = await Promise.all([
6901
6908
  api.status(),
6902
- api.Model.listModels()
6909
+ api.Model.listModels(),
6910
+ loadStudioCapabilities()
6903
6911
  ]);
6904
6912
  this.nodeEnv = nodeEnv;
6905
6913
  this.modelSchemaPaths = modelSchemaPaths;
6914
+ this.capabilities = capabilities;
6906
6915
  } catch (err) {
6907
6916
  this.authError = 'Error connecting to Mongoose Studio API: ' + (err.response?.data?.message ?? err.message);
6908
6917
  }
@@ -6917,6 +6926,7 @@ app.component('app-component', {
6917
6926
  const nodeEnv = Vue.ref(null);
6918
6927
  const authError = Vue.ref(null);
6919
6928
  const modelSchemaPaths = Vue.ref(null);
6929
+ const capabilities = Vue.ref(null);
6920
6930
 
6921
6931
  const state = Vue.reactive({
6922
6932
  user,
@@ -6924,7 +6934,8 @@ app.component('app-component', {
6924
6934
  status,
6925
6935
  nodeEnv,
6926
6936
  authError,
6927
- modelSchemaPaths
6937
+ modelSchemaPaths,
6938
+ capabilities
6928
6939
  });
6929
6940
  Vue.provide('state', state);
6930
6941
 
@@ -6976,13 +6987,13 @@ router.beforeEach((to, from, next) => {
6976
6987
  next();
6977
6988
  });
6978
6989
 
6979
- router.beforeEach((to, from, next) => {
6980
- if (['chat index', 'chat'].includes(to.name) && !window.MONGOOSE_STUDIO_CONFIG.hasLLMAPIKey) {
6990
+ router.beforeEach(async(to, from, next) => {
6991
+ if (CHAT_ROUTE_NAMES.has(to.name) && !(await supportsAI())) {
6981
6992
  next({ name: 'root' });
6982
6993
  return;
6983
6994
  }
6984
6995
 
6985
- if (to.name === 'root' && window.state.roles && window.state.roles[0] === 'dashboards') {
6996
+ if (to.name === 'root' && window.state?.roles && window.state.roles[0] === 'dashboards') {
6986
6997
  return next({ name: 'dashboards' });
6987
6998
  } else {
6988
6999
  next();
@@ -6998,6 +7009,31 @@ app.use(router);
6998
7009
 
6999
7010
  app.mount('#content');
7000
7011
 
7012
+ async function getStudioCapabilities() {
7013
+ if (window.__mongooseStudioCapabilitiesPromise == null) {
7014
+ window.__mongooseStudioCapabilitiesPromise = api.getCapabilities().catch(err => {
7015
+ window.__mongooseStudioCapabilitiesPromise = null;
7016
+ throw err;
7017
+ });
7018
+ }
7019
+ return window.__mongooseStudioCapabilitiesPromise;
7020
+ }
7021
+
7022
+ async function loadStudioCapabilities() {
7023
+ try {
7024
+ return await getStudioCapabilities();
7025
+ } catch (err) {
7026
+ return null;
7027
+ }
7028
+ }
7029
+
7030
+ async function supportsAI() {
7031
+ if (window.state?.capabilities == null) {
7032
+ return true;
7033
+ }
7034
+ return window.state.capabilities.supportsAI !== false;
7035
+ }
7036
+
7001
7037
 
7002
7038
  /***/ },
7003
7039
 
@@ -10100,7 +10136,6 @@ exports.hasAPIKey = client.hasAPIKey;
10100
10136
  "use strict";
10101
10137
 
10102
10138
 
10103
- const api = __webpack_require__(/*! ../api */ "./frontend/src/api.js");
10104
10139
  const mothership = __webpack_require__(/*! ../mothership */ "./frontend/src/mothership.js");
10105
10140
  const template = __webpack_require__(/*! ./navbar.html */ "./frontend/src/navbar/navbar.html");
10106
10141
  const { routes, hasAccess } = __webpack_require__(/*! ../routes */ "./frontend/src/routes.js");
@@ -10207,8 +10242,8 @@ module.exports = app => app.component('navbar', {
10207
10242
  recentPagesList() {
10208
10243
  return this.localRecentPages;
10209
10244
  },
10210
- hasLLMAPIKey() {
10211
- return !!window.MONGOOSE_STUDIO_CONFIG.hasLLMAPIKey;
10245
+ supportsAI() {
10246
+ return this.state.capabilities == null || this.state.capabilities.supportsAI !== false;
10212
10247
  },
10213
10248
  chatDisabledReason() {
10214
10249
  return 'Chat requires an Anthropic, Gemini, or OpenAI API key.';
@@ -52687,7 +52722,7 @@ module.exports = ".active {\n text-decoration: underline;\n}\n\n.navbar .nav-le
52687
52722
  (module) {
52688
52723
 
52689
52724
  "use strict";
52690
- module.exports = "<div>\n<div class=\"navbar w-full bg-page border-b border-edge !h-[55px] hidden md:grid grid-cols-[1fr_auto_1fr] items-stretch px-4\">\n <!-- Left: Logo + Brand -->\n <div class=\"flex items-center gap-2 self-center\">\n <router-link class=\"flex items-center gap-2\" :to=\"{ name: defaultRoute }\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"h-10 text-primary\" viewBox=\"0 0 250 250\" fill=\"currentColor\" aria-label=\"Mongoose Studio Logo\">\n <path d=\"M 241.66,125.01 C 237.81,112.98 227.41,111.95 216.76,110.36 C 215.12,95.83 210.99,83.09 202.29,70.33 C 202.99,68.37 201.92,66.65 200.74,65.11 C 183.14,41.46 157.52,27 124.66,27 C 92.62,27 66.09,40.77 48.28,63.38 C 46.21,65.85 46.26,67.71 47.12,69.36 C 37.81,81.15 33.21,93.78 30.99,109.86 C 20.19,110.58 11.71,115.92 9.04,124.94 C 3.28,145.66 5.62,157.95 10.01,169.16 C 16.14,184.65 29.12,186.31 36.04,185.81 C 42.48,185.68 44.94,184.31 44.72,182.11 C 44.26,177.79 41.01,165.31 41.01,149.12 C 41.01,138.22 43.19,127.74 46.21,117.7 C 47.51,113.33 44.66,112.05 39.16,110.28 C 41.01,96.88 45.88,84.85 52.61,73.79 C 55.17,75.56 58.07,74.88 59.61,72.71 C 75.86,54.31 97.89,45.16 124.61,45.16 C 149.71,45.16 171.91,55.91 189.29,73.47 C 191.95,76.59 193.91,76.67 197.6,74.42 C 204.93,86.26 208.28,98.24 208.98,110.39 C 203.84,111.81 201.28,113.19 202.92,118.61 C 206.51,128.99 207.92,137.93 207.92,148.73 C 207.92,162.11 204.8,174.71 202.84,182.71 C 201.92,186.23 204.31,186.91 210.79,186.99 H 213.91 C 225.87,186.61 235.39,180.64 239.49,169.05 C 244.83,155.26 245.08,138.01 241.66,125.01 Z\"/>\n <path d=\"M 124.71,70.81 C 79.63,70.81 47.49,107.12 47.49,148.72 C 47.49,169.16 56.41,188.19 73.71,200.61 L 73.96,200.77 C 77.43,177.81 84.16,162.21 95.29,145.42 C 92.04,137.09 91.71,130.87 92.62,120.83 C 93.81,111.23 96.08,107.12 102.19,107.68 C 110.04,108.87 117.98,116.18 126.22,124.96 C 139.89,124.05 157.81,131.25 178.33,139.09 C 183.11,140.86 183.72,142.63 182.97,148.33 C 179.45,160.93 170.86,166.63 158.11,174.05 C 143.09,183.07 141.13,199.42 153.88,220.09 L 153.96,221.01 L 154.71,221.17 C 181.31,208.43 201.34,181.01 201.34,148.72 C 201.34,108.15 169.29,70.81 124.71,70.81 Z\"/>\n </svg>\n <span class=\"text-base font-semibold text-content whitespace-nowrap\">Mongoose Studio</span>\n </router-link>\n </div>\n <!-- Center: Nav Links -->\n <nav class=\"flex items-stretch gap-6\">\n <a v-if=\"hasAccess(roles, 'root')\"\n href=\"#/\"\n class=\"inline-flex items-center px-1 border-b-2 text-sm font-medium\"\n :class=\"documentView ? 'text-content border-primary' : 'border-transparent text-content-tertiary hover:text-content'\">Documents</a>\n <span v-else class=\"inline-flex items-center px-1 border-b-2 border-transparent text-sm font-medium text-gray-300 cursor-not-allowed\" aria-disabled=\"true\">\n Documents\n <svg class=\"h-4 w-4 ml-1\" viewBox=\"0 0 20 20\" fill=\"currentColor\" aria-hidden=\"true\">\n <path fill-rule=\"evenodd\" d=\"M10 2a4 4 0 00-4 4v2H5a2 2 0 00-2 2v6a2 2 0 002 2h10a2 2 0 002-2v-6a2 2 0 00-2-2h-1V6a4 4 0 00-4-4zm-3 6V6a3 3 0 116 0v2H7z\" clip-rule=\"evenodd\" />\n </svg>\n </span>\n <a v-if=\"hasAccess(roles, 'dashboards')\"\n href=\"#/dashboards\"\n class=\"inline-flex items-center px-1 border-b-2 text-sm font-medium\"\n :class=\"dashboardView ? 'text-content border-primary' : 'border-transparent text-content-tertiary hover:text-content'\">Dashboards</a>\n <span v-else class=\"inline-flex items-center px-1 border-b-2 border-transparent text-sm font-medium text-gray-300 cursor-not-allowed\">\n Dashboards\n <svg class=\"h-4 w-4 ml-1\" viewBox=\"0 0 20 20\" fill=\"currentColor\" aria-hidden=\"true\">\n <path fill-rule=\"evenodd\" d=\"M10 2a4 4 0 00-4 4v2H5a2 2 0 00-2 2v6a2 2 0 002 2h10a2 2 0 002-2v-6a2 2 0 00-2-2h-1V6a4 4 0 00-4-4zm-3 6V6a3 3 0 116 0v2H7z\" clip-rule=\"evenodd\" />\n </svg>\n </span>\n <a v-if=\"hasAccess(roles, 'chat') && hasLLMAPIKey\"\n href=\"#/chat\"\n class=\"inline-flex items-center px-1 border-b-2 text-sm font-medium\"\n :class=\"chatView ? 'text-content border-primary' : 'border-transparent text-content-tertiary hover:text-content'\">Chat</a>\n <span v-else-if=\"hasAccess(roles, 'chat') && !hasLLMAPIKey\"\n class=\"inline-flex items-center px-1 border-b-2 border-transparent text-sm font-medium text-gray-300 cursor-not-allowed\"\n :title=\"chatDisabledReason\">\n Chat\n <svg class=\"h-4 w-4 ml-1\" viewBox=\"0 0 20 20\" fill=\"currentColor\" aria-hidden=\"true\">\n <path fill-rule=\"evenodd\" d=\"M10 2a4 4 0 00-4 4v2H5a2 2 0 00-2 2v6a2 2 0 002 2h10a2 2 0 002-2v-6a2 2 0 00-2-2h-1V6a4 4 0 00-4-4zm-3 6V6a3 3 0 116 0v2H7z\" clip-rule=\"evenodd\" />\n </svg>\n </span>\n <span v-else class=\"inline-flex items-center px-1 border-b-2 border-transparent text-sm font-medium text-gray-300 cursor-not-allowed\">\n Chat\n <svg class=\"h-4 w-4 ml-1\" viewBox=\"0 0 20 20\" fill=\"currentColor\" aria-hidden=\"true\">\n <path fill-rule=\"evenodd\" d=\"M10 2a4 4 0 00-4 4v2H5a2 2 0 00-2 2v6a2 2 0 002 2h10a2 2 0 002-2v-6a2 2 0 00-2-2h-1V6a4 4 0 00-4-4zm-3 6V6a3 3 0 116 0v2H7z\" clip-rule=\"evenodd\" />\n </svg>\n </span>\n <a\n v-if=\"hasTaskVisualizer\"\n href=\"#/tasks\"\n class=\"inline-flex items-center px-1 border-b-2 text-sm font-medium\"\n :class=\"taskView ? 'text-content border-primary' : 'border-transparent text-content-tertiary hover:text-content'\">\n Tasks\n </a>\n <a\n href=\"https://studio.mongoosejs.io/docs\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n class=\"inline-flex items-center px-1 border-b-2 border-transparent text-sm font-medium text-content-tertiary hover:text-content\"\n >\n Docs\n </a>\n </nav>\n <!-- Right: Env Badge + User -->\n <div class=\"flex items-center justify-end gap-3 self-center\">\n <button\n type=\"button\"\n class=\"inline-flex items-center justify-center rounded-md p-2 text-content-tertiary hover:bg-muted hover:text-content-secondary\"\n title=\"Recent pages\"\n aria-label=\"Recent pages\"\n aria-haspopup=\"dialog\"\n :aria-expanded=\"showRecentPagesModal ? 'true' : 'false'\"\n @click=\"openRecentPagesModal\"\n >\n <svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\" class=\"h-5 w-5\" aria-hidden=\"true\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z\" />\n </svg>\n </button>\n <div\n v-if=\"!!state.nodeEnv\"\n title=\"NODE_ENV\"\n class=\"inline-flex items-center rounded px-2 py-1 text-xs text-content-tertiary bg-surface border border-edge gap-2\"\n >\n <span\n :class=\"warnEnv ? 'bg-red-400' : 'bg-yellow-400'\"\n class=\"inline-block rounded-full\"\n style=\"width: 0.5em; height: 0.5em;\"\n ></span>\n <span>{{state.nodeEnv}}</span>\n </div>\n <button\n v-if=\"!user || !hasAPIKey\"\n type=\"button\"\n @click=\"toggleDarkMode\"\n :title=\"darkMode ? 'Switch to light mode' : 'Switch to dark mode'\"\n class=\"inline-flex items-center justify-center rounded-md p-2 text-content-tertiary hover:text-content-secondary hover:bg-muted\"\n aria-label=\"Toggle dark mode\">\n <svg v-if=\"darkMode\" xmlns=\"http://www.w3.org/2000/svg\" class=\"h-5 w-5\" viewBox=\"0 0 20 20\" fill=\"currentColor\" aria-hidden=\"true\">\n <path d=\"M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z\"/>\n </svg>\n <svg v-else xmlns=\"http://www.w3.org/2000/svg\" class=\"h-5 w-5\" viewBox=\"0 0 20 20\" fill=\"currentColor\" aria-hidden=\"true\">\n <path d=\"M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z\"/>\n </svg>\n </button>\n <div class=\"flex items-center\" v-if=\"!user && hasAPIKey\">\n <button\n type=\"button\"\n @click=\"loginWithGithub\"\n class=\"rounded bg-primary px-2 py-2 text-sm font-semibold text-primary-text shadow-sm hover:bg-primary-hover focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary\">\n Login\n </button>\n </div>\n <div v-if=\"user && hasAPIKey\" class=\"flex items-center relative\" v-clickOutside=\"hideFlyout\">\n <button type=\"button\" @click=\"showFlyout = !showFlyout\" class=\"relative flex rounded-full text-sm focus:outline-none focus:ring-2 focus:ring-gray-300\" id=\"user-menu-button\" aria-expanded=\"false\" aria-haspopup=\"true\">\n <span class=\"absolute -inset-1.5\"></span>\n <span class=\"sr-only\">Open user menu</span>\n <img class=\"size-8 rounded-full\" :src=\"user.picture\" alt=\"\">\n </button>\n\n <div\n v-if=\"showFlyout\"\n class=\"absolute right-0 top-[90%] w-48 origin-top-right rounded-md bg-surface py-1 shadow-lg ring-1 ring-black/5 focus:outline-none\"\n role=\"menu\"\n aria-orientation=\"vertical\"\n aria-labelledby=\"user-menu-button\"\n style=\"z-index: 10000\"\n tabindex=\"-1\">\n <button\n type=\"button\"\n @click=\"toggleDarkMode(); showFlyout = false\"\n class=\"w-full cursor-pointer block px-4 py-2 text-sm text-content-secondary hover:bg-primary-subtle text-left flex items-center gap-2\"\n role=\"menuitem\"\n tabindex=\"-1\">\n <svg v-if=\"darkMode\" xmlns=\"http://www.w3.org/2000/svg\" class=\"h-4 w-4\" viewBox=\"0 0 20 20\" fill=\"currentColor\"><path d=\"M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z\"/></svg>\n <svg v-else xmlns=\"http://www.w3.org/2000/svg\" class=\"h-4 w-4\" viewBox=\"0 0 20 20\" fill=\"currentColor\"><path d=\"M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z\"/></svg>\n {{ darkMode ? 'Light mode' : 'Dark mode' }}\n </button>\n <router-link to=\"/team\" v-if=\"hasAccess(roles, 'team')\" @click=\"showFlyout = false\" class=\"cursor-pointer block px-4 py-2 text-sm text-content-secondary hover:bg-primary-subtle\" role=\"menuitem\" tabindex=\"-1\" id=\"user-menu-item-2\">Team</router-link>\n <span v-else class=\"block px-4 py-2 text-sm text-gray-300 cursor-not-allowed\" role=\"menuitem\" tabindex=\"-1\" id=\"user-menu-item-2\">\n Team\n <svg class=\"h-4 w-4 ml-1 inline\" viewBox=\"0 0 20 20\" fill=\"currentColor\" aria-hidden=\"true\">\n <path fill-rule=\"evenodd\" d=\"M10 2a4 4 0 00-4 4v2H5a2 2 0 00-2 2v6a2 2 0 002 2h10a2 2 0 002-2v-6a2 2 0 00-2-2h-1V6a4 4 0 00-4-4zm-3 6V6a3 3 0 116 0v2H7z\" clip-rule=\"evenodd\" />\n </svg>\n </span>\n <span @click=\"logout\" class=\"cursor-pointer block px-4 py-2 text-sm text-content-secondary hover:bg-primary-subtle\" role=\"menuitem\" tabindex=\"-1\" id=\"user-menu-item-2\">Sign out</span>\n </div>\n </div>\n </div>\n</div>\n<!-- Mobile navbar -->\n<div class=\"navbar w-full bg-page flex justify-between border-b border-edge !h-[55px] md:hidden\">\n <div class=\"flex items-center gap-2 pl-4\">\n <router-link class=\"flex items-center gap-2\" :to=\"{ name: defaultRoute }\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"h-8 text-primary\" viewBox=\"0 0 250 250\" fill=\"currentColor\" aria-label=\"Mongoose Studio Logo\">\n <path d=\"M 241.66,125.01 C 237.81,112.98 227.41,111.95 216.76,110.36 C 215.12,95.83 210.99,83.09 202.29,70.33 C 202.99,68.37 201.92,66.65 200.74,65.11 C 183.14,41.46 157.52,27 124.66,27 C 92.62,27 66.09,40.77 48.28,63.38 C 46.21,65.85 46.26,67.71 47.12,69.36 C 37.81,81.15 33.21,93.78 30.99,109.86 C 20.19,110.58 11.71,115.92 9.04,124.94 C 3.28,145.66 5.62,157.95 10.01,169.16 C 16.14,184.65 29.12,186.31 36.04,185.81 C 42.48,185.68 44.94,184.31 44.72,182.11 C 44.26,177.79 41.01,165.31 41.01,149.12 C 41.01,138.22 43.19,127.74 46.21,117.7 C 47.51,113.33 44.66,112.05 39.16,110.28 C 41.01,96.88 45.88,84.85 52.61,73.79 C 55.17,75.56 58.07,74.88 59.61,72.71 C 75.86,54.31 97.89,45.16 124.61,45.16 C 149.71,45.16 171.91,55.91 189.29,73.47 C 191.95,76.59 193.91,76.67 197.6,74.42 C 204.93,86.26 208.28,98.24 208.98,110.39 C 203.84,111.81 201.28,113.19 202.92,118.61 C 206.51,128.99 207.92,137.93 207.92,148.73 C 207.92,162.11 204.8,174.71 202.84,182.71 C 201.92,186.23 204.31,186.91 210.79,186.99 H 213.91 C 225.87,186.61 235.39,180.64 239.49,169.05 C 244.83,155.26 245.08,138.01 241.66,125.01 Z\"/>\n <path d=\"M 124.71,70.81 C 79.63,70.81 47.49,107.12 47.49,148.72 C 47.49,169.16 56.41,188.19 73.71,200.61 L 73.96,200.77 C 77.43,177.81 84.16,162.21 95.29,145.42 C 92.04,137.09 91.71,130.87 92.62,120.83 C 93.81,111.23 96.08,107.12 102.19,107.68 C 110.04,108.87 117.98,116.18 126.22,124.96 C 139.89,124.05 157.81,131.25 178.33,139.09 C 183.11,140.86 183.72,142.63 182.97,148.33 C 179.45,160.93 170.86,166.63 158.11,174.05 C 143.09,183.07 141.13,199.42 153.88,220.09 L 153.96,221.01 L 154.71,221.17 C 181.31,208.43 201.34,181.01 201.34,148.72 C 201.34,108.15 169.29,70.81 124.71,70.81 Z\"/>\n </svg>\n <span class=\"text-base font-semibold text-content\">Mongoose Studio</span>\n </router-link>\n </div>\n <div class=\"flex items-center gap-1 pr-2\">\n <button\n type=\"button\"\n class=\"rounded-md p-2 text-content-tertiary hover:bg-muted hover:text-content-secondary md:hidden\"\n title=\"Recent pages\"\n aria-label=\"Recent pages\"\n aria-haspopup=\"dialog\"\n :aria-expanded=\"showRecentPagesModal ? 'true' : 'false'\"\n @click=\"openRecentPagesModal\"\n >\n <svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\" class=\"h-6 w-6\" aria-hidden=\"true\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z\" />\n </svg>\n </button>\n <!-- Mobile menu toggle, controls the 'mobileMenuOpen' state. -->\n <button type=\"button\" id=\"open-mobile-menu\" class=\"-ml-2 rounded-md p-2 text-gray-400\">\n <span class=\"sr-only\">Open menu</span>\n <svg class=\"h-6 w-6\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\" aria-hidden=\"true\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5\" />\n </svg>\n </button>\n </div>\n\n <!-- Mobile menu mask -->\n <div id=\"mobile-menu-mask\" class=\"fixed inset-0 bg-black bg-opacity-40 z-40 hidden\"></div>\n <!-- Mobile menu drawer -->\n <div id=\"mobile-menu\" style=\"z-index: 100000\" class=\"fixed inset-0 bg-page shadow-lg transform translate-x-full transition-transform duration-200 ease-in-out flex flex-col\">\n <div class=\"flex items-center justify-between px-4 !h-[55px] border-b border-edge\">\n <router-link :to=\"{ name: defaultRoute }\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"h-[32px] text-primary\" viewBox=\"0 0 250 250\" fill=\"currentColor\" aria-label=\"Mongoose Studio Logo\">\n <path d=\"M 241.66,125.01 C 237.81,112.98 227.41,111.95 216.76,110.36 C 215.12,95.83 210.99,83.09 202.29,70.33 C 202.99,68.37 201.92,66.65 200.74,65.11 C 183.14,41.46 157.52,27 124.66,27 C 92.62,27 66.09,40.77 48.28,63.38 C 46.21,65.85 46.26,67.71 47.12,69.36 C 37.81,81.15 33.21,93.78 30.99,109.86 C 20.19,110.58 11.71,115.92 9.04,124.94 C 3.28,145.66 5.62,157.95 10.01,169.16 C 16.14,184.65 29.12,186.31 36.04,185.81 C 42.48,185.68 44.94,184.31 44.72,182.11 C 44.26,177.79 41.01,165.31 41.01,149.12 C 41.01,138.22 43.19,127.74 46.21,117.7 C 47.51,113.33 44.66,112.05 39.16,110.28 C 41.01,96.88 45.88,84.85 52.61,73.79 C 55.17,75.56 58.07,74.88 59.61,72.71 C 75.86,54.31 97.89,45.16 124.61,45.16 C 149.71,45.16 171.91,55.91 189.29,73.47 C 191.95,76.59 193.91,76.67 197.6,74.42 C 204.93,86.26 208.28,98.24 208.98,110.39 C 203.84,111.81 201.28,113.19 202.92,118.61 C 206.51,128.99 207.92,137.93 207.92,148.73 C 207.92,162.11 204.8,174.71 202.84,182.71 C 201.92,186.23 204.31,186.91 210.79,186.99 H 213.91 C 225.87,186.61 235.39,180.64 239.49,169.05 C 244.83,155.26 245.08,138.01 241.66,125.01 Z\"/>\n <path d=\"M 124.71,70.81 C 79.63,70.81 47.49,107.12 47.49,148.72 C 47.49,169.16 56.41,188.19 73.71,200.61 L 73.96,200.77 C 77.43,177.81 84.16,162.21 95.29,145.42 C 92.04,137.09 91.71,130.87 92.62,120.83 C 93.81,111.23 96.08,107.12 102.19,107.68 C 110.04,108.87 117.98,116.18 126.22,124.96 C 139.89,124.05 157.81,131.25 178.33,139.09 C 183.11,140.86 183.72,142.63 182.97,148.33 C 179.45,160.93 170.86,166.63 158.11,174.05 C 143.09,183.07 141.13,199.42 153.88,220.09 L 153.96,221.01 L 154.71,221.17 C 181.31,208.43 201.34,181.01 201.34,148.72 C 201.34,108.15 169.29,70.81 124.71,70.81 Z\"/>\n </svg>\n </router-link>\n <button type=\"button\" id=\"close-mobile-menu\" class=\"text-gray-400 p-2 rounded-md\">\n <span class=\"sr-only\">Close menu</span>\n <svg class=\"h-6 w-6\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\" aria-hidden=\"true\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M6 18L18 6M6 6l12 12\" />\n </svg>\n </button>\n </div>\n <nav class=\"flex-1 px-4 py-4 space-y-2\">\n <div class=\"flex items-center gap-2 px-3 py-2\" v-if=\"!user || !hasAPIKey\">\n <button\n type=\"button\"\n @click=\"toggleDarkMode\"\n class=\"inline-flex items-center gap-2 rounded-md px-3 py-2 text-base font-medium text-content-secondary hover:bg-muted\"\n aria-label=\"Toggle dark mode\">\n <svg v-if=\"darkMode\" xmlns=\"http://www.w3.org/2000/svg\" class=\"h-5 w-5\" viewBox=\"0 0 20 20\" fill=\"currentColor\"><path d=\"M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z\"/></svg>\n <svg v-else xmlns=\"http://www.w3.org/2000/svg\" class=\"h-5 w-5\" viewBox=\"0 0 20 20\" fill=\"currentColor\"><path d=\"M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z\"/></svg>\n <span>{{ darkMode ? 'Light mode' : 'Dark mode' }}</span>\n </button>\n </div>\n <a v-if=\"hasAccess(roles, 'root')\"\n href=\"#/\"\n class=\"block px-3 py-2 rounded-md text-base font-medium\"\n :class=\"documentView ? 'text-content bg-primary-subtle' : 'text-content-secondary hover:bg-muted'\">Documents</a>\n <span v-else class=\"block px-3 py-2 rounded-md text-base font-medium text-gray-300 cursor-not-allowed\">\n Documents\n <svg class=\"h-4 w-4 ml-1 inline\" viewBox=\"0 0 20 20\" fill=\"currentColor\" aria-hidden=\"true\">\n <path fill-rule=\"evenodd\" d=\"M10 2a4 4 0 00-4 4v2H5a2 2 0 00-2 2v6a2 2 0 002 2h10a2 2 0 002-2v-6a2 2 0 00-2-2h-1V6a4 4 0 00-4-4zm-3 6V6a3 3 0 116 0v2H7z\" clip-rule=\"evenodd\" />\n </svg>\n </span>\n <a v-if=\"hasAccess(roles, 'dashboards')\"\n href=\"#/dashboards\"\n class=\"block px-3 py-2 rounded-md text-base font-medium\"\n :class=\"dashboardView ? 'text-content bg-primary-subtle' : 'text-content-secondary hover:bg-muted'\">Dashboards</a>\n <span v-else class=\"block px-3 py-2 rounded-md text-base font-medium text-gray-300 cursor-not-allowed\">\n Dashboards\n <svg class=\"h-4 w-4 ml-1 inline\" viewBox=\"0 0 20 20\" fill=\"currentColor\" aria-hidden=\"true\">\n <path fill-rule=\"evenodd\" d=\"M10 2a4 4 0 00-4 4v2H5a2 2 0 00-2 2v6a2 2 0 002 2h10a2 2 0 002-2v-6a2 2 0 00-2-2h-1V6a4 4 0 00-4-4zm-3 6V6a3 3 0 116 0v2H7z\" clip-rule=\"evenodd\" />\n </svg>\n </span>\n <a v-if=\"hasAccess(roles, 'chat') && hasLLMAPIKey\"\n href=\"#/chat\"\n class=\"block px-3 py-2 rounded-md text-base font-medium\"\n :class=\"chatView ? 'text-content bg-primary-subtle' : 'text-content-secondary hover:bg-muted'\">Chat</a>\n <span v-else-if=\"hasAccess(roles, 'chat') && !hasLLMAPIKey\"\n class=\"block px-3 py-2 rounded-md text-base font-medium text-gray-300 cursor-not-allowed\"\n :title=\"chatDisabledReason\">\n Chat\n <svg class=\"h-4 w-4 ml-1 inline\" viewBox=\"0 0 20 20\" fill=\"currentColor\" aria-hidden=\"true\">\n <path fill-rule=\"evenodd\" d=\"M10 2a4 4 0 00-4 4v2H5a2 2 0 00-2 2v6a2 2 0 002 2h10a2 2 0 002-2v-6a2 2 0 00-2-2h-1V6a4 4 0 00-4-4zm-3 6V6a3 3 0 116 0v2H7z\" clip-rule=\"evenodd\" />\n </svg>\n </span>\n <span v-else class=\"block px-3 py-2 rounded-md text-base font-medium text-gray-300 cursor-not-allowed\">\n Chat\n <svg class=\"h-4 w-4 ml-1 inline\" viewBox=\"0 0 20 20\" fill=\"currentColor\" aria-hidden=\"true\">\n <path fill-rule=\"evenodd\" d=\"M10 2a4 4 0 00-4 4v2H5a2 2 0 00-2 2v6a2 2 0 002 2h10a2 2 0 002-2v-6a2 2 0 00-2-2h-1V6a4 4 0 00-4-4zm-3 6V6a3 3 0 116 0v2H7z\" clip-rule=\"evenodd\" />\n </svg>\n </span>\n <a v-if=\"hasTaskVisualizer\"\n href=\"#/tasks\"\n class=\"block px-3 py-2 rounded-md text-base font-medium\"\n :class=\"taskView ? 'text-content bg-primary-subtle' : 'text-content-secondary hover:bg-muted'\">Tasks</a>\n <div v-if=\"!user && hasAPIKey\" class=\"mt-4\">\n <button\n type=\"button\"\n @click=\"loginWithGithub\"\n class=\"w-full rounded bg-muted text-content px-3 py-2 text-base font-semibold shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary\">\n Login\n </button>\n </div>\n <div v-if=\"user && hasAPIKey\" class=\"mt-4\">\n <div class=\"flex items-center gap-3 px-3 py-2 bg-page rounded-md\">\n <img class=\"size-8 rounded-full\" :src=\"user.picture\" alt=\"\">\n <span class=\"text-content font-medium\">{{ user.name }}</span>\n </div>\n <div class=\"mt-2 space-y-1\">\n <button\n type=\"button\"\n @click=\"toggleDarkMode\"\n class=\"w-full inline-flex items-center gap-2 px-3 py-2 rounded-md text-base text-content-secondary hover:bg-primary-subtle text-left\">\n <svg v-if=\"darkMode\" xmlns=\"http://www.w3.org/2000/svg\" class=\"h-5 w-5\" viewBox=\"0 0 20 20\" fill=\"currentColor\"><path d=\"M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z\"/></svg>\n <svg v-else xmlns=\"http://www.w3.org/2000/svg\" class=\"h-5 w-5\" viewBox=\"0 0 20 20\" fill=\"currentColor\"><path d=\"M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z\"/></svg>\n {{ darkMode ? 'Light mode' : 'Dark mode' }}\n </button>\n <router-link to=\"/team\" v-if=\"hasAccess(roles, 'team')\" class=\"block px-3 py-2 rounded-md text-base text-content-secondary hover:bg-primary-subtle\">Team</router-link>\n <span v-else class=\"block px-3 py-2 rounded-md text-base text-gray-300 cursor-not-allowed\">\n Team\n <svg class=\"h-4 w-4 ml-1 inline\" viewBox=\"0 0 20 20\" fill=\"currentColor\" aria-hidden=\"true\">\n <path fill-rule=\"evenodd\" d=\"M10 2a4 4 0 00-4 4v2H5a2 2 0 00-2 2v6a2 2 0 002 2h10a2 2 0 002-2v-6a2 2 0 00-2-2h-1V6a4 4 0 00-4-4zm-3 6V6a3 3 0 116 0v2H7z\" clip-rule=\"evenodd\" />\n </svg>\n </span>\n <span @click=\"logout\" class=\"block px-3 py-2 rounded-md text-base text-content-secondary hover:bg-primary-subtle cursor-pointer\">Sign out</span>\n </div>\n </div>\n </nav>\n </div>\n</div>\n\n<modal\n v-if=\"showRecentPagesModal\"\n flush-container\n containerClass=\"!max-w-md !w-[min(94vw,28rem)] !overflow-hidden !rounded-lg !border !border-edge !bg-surface !shadow-xl dark:!bg-shark-900 text-content\"\n bodyClass=\"modal-recent-pages-body !p-0\"\n>\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"closeRecentPagesModal\" role=\"button\" aria-label=\"Close recent pages\">&times;</div>\n <div\n class=\"shrink-0 flex items-center gap-3 border-b border-edge bg-surface px-4 py-4 pr-12 dark:bg-shark-900\"\n >\n <h2 class=\"m-0 text-lg font-semibold text-content\">Recent pages</h2>\n <button\n type=\"button\"\n class=\"rounded px-2 py-1 text-sm font-medium text-content-secondary hover:bg-muted disabled:opacity-50\"\n @click=\"clearRecentPagesHistory\"\n :disabled=\"recentPagesList.length === 0\"\n >\n Clear\n </button>\n </div>\n <div class=\"modal-recent-pages-scroll\">\n <div\n v-if=\"recentPagesList.length === 0\"\n class=\"m-4 rounded border border-dashed border-edge px-3 py-6 text-center text-sm text-content-tertiary\"\n >\n No recent pages yet.\n </div>\n <button\n v-for=\"entry in recentPagesList\"\n :key=\"entry.path + '-' + entry.visitedAt\"\n type=\"button\"\n class=\"w-full px-4 py-2.5 text-left hover:bg-muted\"\n :class=\"entry.path === $route.fullPath ? 'bg-primary-subtle' : ''\"\n @click=\"goToRecentPage(entry)\"\n >\n <div class=\"truncate text-sm font-medium text-content\">{{ entry.label }}</div>\n <div class=\"truncate font-mono text-xs text-content-tertiary\">{{ entry.path }}</div>\n </button>\n </div>\n </template>\n</modal>\n</div>\n";
52725
+ module.exports = "<div>\n<div class=\"navbar w-full bg-page border-b border-edge !h-[55px] hidden md:grid grid-cols-[1fr_auto_1fr] items-stretch px-4\">\n <!-- Left: Logo + Brand -->\n <div class=\"flex items-center gap-2 self-center\">\n <router-link class=\"flex items-center gap-2\" :to=\"{ name: defaultRoute }\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"h-10 text-primary\" viewBox=\"0 0 250 250\" fill=\"currentColor\" aria-label=\"Mongoose Studio Logo\">\n <path d=\"M 241.66,125.01 C 237.81,112.98 227.41,111.95 216.76,110.36 C 215.12,95.83 210.99,83.09 202.29,70.33 C 202.99,68.37 201.92,66.65 200.74,65.11 C 183.14,41.46 157.52,27 124.66,27 C 92.62,27 66.09,40.77 48.28,63.38 C 46.21,65.85 46.26,67.71 47.12,69.36 C 37.81,81.15 33.21,93.78 30.99,109.86 C 20.19,110.58 11.71,115.92 9.04,124.94 C 3.28,145.66 5.62,157.95 10.01,169.16 C 16.14,184.65 29.12,186.31 36.04,185.81 C 42.48,185.68 44.94,184.31 44.72,182.11 C 44.26,177.79 41.01,165.31 41.01,149.12 C 41.01,138.22 43.19,127.74 46.21,117.7 C 47.51,113.33 44.66,112.05 39.16,110.28 C 41.01,96.88 45.88,84.85 52.61,73.79 C 55.17,75.56 58.07,74.88 59.61,72.71 C 75.86,54.31 97.89,45.16 124.61,45.16 C 149.71,45.16 171.91,55.91 189.29,73.47 C 191.95,76.59 193.91,76.67 197.6,74.42 C 204.93,86.26 208.28,98.24 208.98,110.39 C 203.84,111.81 201.28,113.19 202.92,118.61 C 206.51,128.99 207.92,137.93 207.92,148.73 C 207.92,162.11 204.8,174.71 202.84,182.71 C 201.92,186.23 204.31,186.91 210.79,186.99 H 213.91 C 225.87,186.61 235.39,180.64 239.49,169.05 C 244.83,155.26 245.08,138.01 241.66,125.01 Z\"/>\n <path d=\"M 124.71,70.81 C 79.63,70.81 47.49,107.12 47.49,148.72 C 47.49,169.16 56.41,188.19 73.71,200.61 L 73.96,200.77 C 77.43,177.81 84.16,162.21 95.29,145.42 C 92.04,137.09 91.71,130.87 92.62,120.83 C 93.81,111.23 96.08,107.12 102.19,107.68 C 110.04,108.87 117.98,116.18 126.22,124.96 C 139.89,124.05 157.81,131.25 178.33,139.09 C 183.11,140.86 183.72,142.63 182.97,148.33 C 179.45,160.93 170.86,166.63 158.11,174.05 C 143.09,183.07 141.13,199.42 153.88,220.09 L 153.96,221.01 L 154.71,221.17 C 181.31,208.43 201.34,181.01 201.34,148.72 C 201.34,108.15 169.29,70.81 124.71,70.81 Z\"/>\n </svg>\n <span class=\"text-base font-semibold text-content whitespace-nowrap\">Mongoose Studio</span>\n </router-link>\n </div>\n <!-- Center: Nav Links -->\n <nav class=\"flex items-stretch gap-6\">\n <a v-if=\"hasAccess(roles, 'root')\"\n href=\"#/\"\n class=\"inline-flex items-center px-1 border-b-2 text-sm font-medium\"\n :class=\"documentView ? 'text-content border-primary' : 'border-transparent text-content-tertiary hover:text-content'\">Documents</a>\n <span v-else class=\"inline-flex items-center px-1 border-b-2 border-transparent text-sm font-medium text-gray-300 cursor-not-allowed\" aria-disabled=\"true\">\n Documents\n <svg class=\"h-4 w-4 ml-1\" viewBox=\"0 0 20 20\" fill=\"currentColor\" aria-hidden=\"true\">\n <path fill-rule=\"evenodd\" d=\"M10 2a4 4 0 00-4 4v2H5a2 2 0 00-2 2v6a2 2 0 002 2h10a2 2 0 002-2v-6a2 2 0 00-2-2h-1V6a4 4 0 00-4-4zm-3 6V6a3 3 0 116 0v2H7z\" clip-rule=\"evenodd\" />\n </svg>\n </span>\n <a v-if=\"hasAccess(roles, 'dashboards')\"\n href=\"#/dashboards\"\n class=\"inline-flex items-center px-1 border-b-2 text-sm font-medium\"\n :class=\"dashboardView ? 'text-content border-primary' : 'border-transparent text-content-tertiary hover:text-content'\">Dashboards</a>\n <span v-else class=\"inline-flex items-center px-1 border-b-2 border-transparent text-sm font-medium text-gray-300 cursor-not-allowed\">\n Dashboards\n <svg class=\"h-4 w-4 ml-1\" viewBox=\"0 0 20 20\" fill=\"currentColor\" aria-hidden=\"true\">\n <path fill-rule=\"evenodd\" d=\"M10 2a4 4 0 00-4 4v2H5a2 2 0 00-2 2v6a2 2 0 002 2h10a2 2 0 002-2v-6a2 2 0 00-2-2h-1V6a4 4 0 00-4-4zm-3 6V6a3 3 0 116 0v2H7z\" clip-rule=\"evenodd\" />\n </svg>\n </span>\n <a v-if=\"hasAccess(roles, 'chat') && supportsAI\"\n href=\"#/chat\"\n class=\"inline-flex items-center px-1 border-b-2 text-sm font-medium\"\n :class=\"chatView ? 'text-content border-primary' : 'border-transparent text-content-tertiary hover:text-content'\">Chat</a>\n <span v-else-if=\"hasAccess(roles, 'chat') && !supportsAI\"\n class=\"inline-flex items-center px-1 border-b-2 border-transparent text-sm font-medium text-gray-300 cursor-not-allowed\"\n :title=\"chatDisabledReason\">\n Chat\n <svg class=\"h-4 w-4 ml-1\" viewBox=\"0 0 20 20\" fill=\"currentColor\" aria-hidden=\"true\">\n <path fill-rule=\"evenodd\" d=\"M10 2a4 4 0 00-4 4v2H5a2 2 0 00-2 2v6a2 2 0 002 2h10a2 2 0 002-2v-6a2 2 0 00-2-2h-1V6a4 4 0 00-4-4zm-3 6V6a3 3 0 116 0v2H7z\" clip-rule=\"evenodd\" />\n </svg>\n </span>\n <span v-else class=\"inline-flex items-center px-1 border-b-2 border-transparent text-sm font-medium text-gray-300 cursor-not-allowed\">\n Chat\n <svg class=\"h-4 w-4 ml-1\" viewBox=\"0 0 20 20\" fill=\"currentColor\" aria-hidden=\"true\">\n <path fill-rule=\"evenodd\" d=\"M10 2a4 4 0 00-4 4v2H5a2 2 0 00-2 2v6a2 2 0 002 2h10a2 2 0 002-2v-6a2 2 0 00-2-2h-1V6a4 4 0 00-4-4zm-3 6V6a3 3 0 116 0v2H7z\" clip-rule=\"evenodd\" />\n </svg>\n </span>\n <a\n v-if=\"hasTaskVisualizer\"\n href=\"#/tasks\"\n class=\"inline-flex items-center px-1 border-b-2 text-sm font-medium\"\n :class=\"taskView ? 'text-content border-primary' : 'border-transparent text-content-tertiary hover:text-content'\">\n Tasks\n </a>\n <a\n href=\"https://studio.mongoosejs.io/docs\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n class=\"inline-flex items-center px-1 border-b-2 border-transparent text-sm font-medium text-content-tertiary hover:text-content\"\n >\n Docs\n </a>\n </nav>\n <!-- Right: Env Badge + User -->\n <div class=\"flex items-center justify-end gap-3 self-center\">\n <button\n type=\"button\"\n class=\"inline-flex items-center justify-center rounded-md p-2 text-content-tertiary hover:bg-muted hover:text-content-secondary\"\n title=\"Recent pages\"\n aria-label=\"Recent pages\"\n aria-haspopup=\"dialog\"\n :aria-expanded=\"showRecentPagesModal ? 'true' : 'false'\"\n @click=\"openRecentPagesModal\"\n >\n <svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\" class=\"h-5 w-5\" aria-hidden=\"true\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z\" />\n </svg>\n </button>\n <div\n v-if=\"!!state.nodeEnv\"\n title=\"NODE_ENV\"\n class=\"inline-flex items-center rounded px-2 py-1 text-xs text-content-tertiary bg-surface border border-edge gap-2\"\n >\n <span\n :class=\"warnEnv ? 'bg-red-400' : 'bg-yellow-400'\"\n class=\"inline-block rounded-full\"\n style=\"width: 0.5em; height: 0.5em;\"\n ></span>\n <span>{{state.nodeEnv}}</span>\n </div>\n <button\n v-if=\"!user || !hasAPIKey\"\n type=\"button\"\n @click=\"toggleDarkMode\"\n :title=\"darkMode ? 'Switch to light mode' : 'Switch to dark mode'\"\n class=\"inline-flex items-center justify-center rounded-md p-2 text-content-tertiary hover:text-content-secondary hover:bg-muted\"\n aria-label=\"Toggle dark mode\">\n <svg v-if=\"darkMode\" xmlns=\"http://www.w3.org/2000/svg\" class=\"h-5 w-5\" viewBox=\"0 0 20 20\" fill=\"currentColor\" aria-hidden=\"true\">\n <path d=\"M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z\"/>\n </svg>\n <svg v-else xmlns=\"http://www.w3.org/2000/svg\" class=\"h-5 w-5\" viewBox=\"0 0 20 20\" fill=\"currentColor\" aria-hidden=\"true\">\n <path d=\"M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z\"/>\n </svg>\n </button>\n <div class=\"flex items-center\" v-if=\"!user && hasAPIKey\">\n <button\n type=\"button\"\n @click=\"loginWithGithub\"\n class=\"rounded bg-primary px-2 py-2 text-sm font-semibold text-primary-text shadow-sm hover:bg-primary-hover focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary\">\n Login\n </button>\n </div>\n <div v-if=\"user && hasAPIKey\" class=\"flex items-center relative\" v-clickOutside=\"hideFlyout\">\n <button type=\"button\" @click=\"showFlyout = !showFlyout\" class=\"relative flex rounded-full text-sm focus:outline-none focus:ring-2 focus:ring-gray-300\" id=\"user-menu-button\" aria-expanded=\"false\" aria-haspopup=\"true\">\n <span class=\"absolute -inset-1.5\"></span>\n <span class=\"sr-only\">Open user menu</span>\n <img class=\"size-8 rounded-full\" :src=\"user.picture\" alt=\"\">\n </button>\n\n <div\n v-if=\"showFlyout\"\n class=\"absolute right-0 top-[90%] w-48 origin-top-right rounded-md bg-surface py-1 shadow-lg ring-1 ring-black/5 focus:outline-none\"\n role=\"menu\"\n aria-orientation=\"vertical\"\n aria-labelledby=\"user-menu-button\"\n style=\"z-index: 10000\"\n tabindex=\"-1\">\n <button\n type=\"button\"\n @click=\"toggleDarkMode(); showFlyout = false\"\n class=\"w-full cursor-pointer block px-4 py-2 text-sm text-content-secondary hover:bg-primary-subtle text-left flex items-center gap-2\"\n role=\"menuitem\"\n tabindex=\"-1\">\n <svg v-if=\"darkMode\" xmlns=\"http://www.w3.org/2000/svg\" class=\"h-4 w-4\" viewBox=\"0 0 20 20\" fill=\"currentColor\"><path d=\"M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z\"/></svg>\n <svg v-else xmlns=\"http://www.w3.org/2000/svg\" class=\"h-4 w-4\" viewBox=\"0 0 20 20\" fill=\"currentColor\"><path d=\"M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z\"/></svg>\n {{ darkMode ? 'Light mode' : 'Dark mode' }}\n </button>\n <router-link to=\"/team\" v-if=\"hasAccess(roles, 'team')\" @click=\"showFlyout = false\" class=\"cursor-pointer block px-4 py-2 text-sm text-content-secondary hover:bg-primary-subtle\" role=\"menuitem\" tabindex=\"-1\" id=\"user-menu-item-2\">Team</router-link>\n <span v-else class=\"block px-4 py-2 text-sm text-gray-300 cursor-not-allowed\" role=\"menuitem\" tabindex=\"-1\" id=\"user-menu-item-2\">\n Team\n <svg class=\"h-4 w-4 ml-1 inline\" viewBox=\"0 0 20 20\" fill=\"currentColor\" aria-hidden=\"true\">\n <path fill-rule=\"evenodd\" d=\"M10 2a4 4 0 00-4 4v2H5a2 2 0 00-2 2v6a2 2 0 002 2h10a2 2 0 002-2v-6a2 2 0 00-2-2h-1V6a4 4 0 00-4-4zm-3 6V6a3 3 0 116 0v2H7z\" clip-rule=\"evenodd\" />\n </svg>\n </span>\n <span @click=\"logout\" class=\"cursor-pointer block px-4 py-2 text-sm text-content-secondary hover:bg-primary-subtle\" role=\"menuitem\" tabindex=\"-1\" id=\"user-menu-item-2\">Sign out</span>\n </div>\n </div>\n </div>\n</div>\n<!-- Mobile navbar -->\n<div class=\"navbar w-full bg-page flex justify-between border-b border-edge !h-[55px] md:hidden\">\n <div class=\"flex items-center gap-2 pl-4\">\n <router-link class=\"flex items-center gap-2\" :to=\"{ name: defaultRoute }\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"h-8 text-primary\" viewBox=\"0 0 250 250\" fill=\"currentColor\" aria-label=\"Mongoose Studio Logo\">\n <path d=\"M 241.66,125.01 C 237.81,112.98 227.41,111.95 216.76,110.36 C 215.12,95.83 210.99,83.09 202.29,70.33 C 202.99,68.37 201.92,66.65 200.74,65.11 C 183.14,41.46 157.52,27 124.66,27 C 92.62,27 66.09,40.77 48.28,63.38 C 46.21,65.85 46.26,67.71 47.12,69.36 C 37.81,81.15 33.21,93.78 30.99,109.86 C 20.19,110.58 11.71,115.92 9.04,124.94 C 3.28,145.66 5.62,157.95 10.01,169.16 C 16.14,184.65 29.12,186.31 36.04,185.81 C 42.48,185.68 44.94,184.31 44.72,182.11 C 44.26,177.79 41.01,165.31 41.01,149.12 C 41.01,138.22 43.19,127.74 46.21,117.7 C 47.51,113.33 44.66,112.05 39.16,110.28 C 41.01,96.88 45.88,84.85 52.61,73.79 C 55.17,75.56 58.07,74.88 59.61,72.71 C 75.86,54.31 97.89,45.16 124.61,45.16 C 149.71,45.16 171.91,55.91 189.29,73.47 C 191.95,76.59 193.91,76.67 197.6,74.42 C 204.93,86.26 208.28,98.24 208.98,110.39 C 203.84,111.81 201.28,113.19 202.92,118.61 C 206.51,128.99 207.92,137.93 207.92,148.73 C 207.92,162.11 204.8,174.71 202.84,182.71 C 201.92,186.23 204.31,186.91 210.79,186.99 H 213.91 C 225.87,186.61 235.39,180.64 239.49,169.05 C 244.83,155.26 245.08,138.01 241.66,125.01 Z\"/>\n <path d=\"M 124.71,70.81 C 79.63,70.81 47.49,107.12 47.49,148.72 C 47.49,169.16 56.41,188.19 73.71,200.61 L 73.96,200.77 C 77.43,177.81 84.16,162.21 95.29,145.42 C 92.04,137.09 91.71,130.87 92.62,120.83 C 93.81,111.23 96.08,107.12 102.19,107.68 C 110.04,108.87 117.98,116.18 126.22,124.96 C 139.89,124.05 157.81,131.25 178.33,139.09 C 183.11,140.86 183.72,142.63 182.97,148.33 C 179.45,160.93 170.86,166.63 158.11,174.05 C 143.09,183.07 141.13,199.42 153.88,220.09 L 153.96,221.01 L 154.71,221.17 C 181.31,208.43 201.34,181.01 201.34,148.72 C 201.34,108.15 169.29,70.81 124.71,70.81 Z\"/>\n </svg>\n <span class=\"text-base font-semibold text-content\">Mongoose Studio</span>\n </router-link>\n </div>\n <div class=\"flex items-center gap-1 pr-2\">\n <button\n type=\"button\"\n class=\"rounded-md p-2 text-content-tertiary hover:bg-muted hover:text-content-secondary md:hidden\"\n title=\"Recent pages\"\n aria-label=\"Recent pages\"\n aria-haspopup=\"dialog\"\n :aria-expanded=\"showRecentPagesModal ? 'true' : 'false'\"\n @click=\"openRecentPagesModal\"\n >\n <svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\" class=\"h-6 w-6\" aria-hidden=\"true\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z\" />\n </svg>\n </button>\n <!-- Mobile menu toggle, controls the 'mobileMenuOpen' state. -->\n <button type=\"button\" id=\"open-mobile-menu\" class=\"-ml-2 rounded-md p-2 text-gray-400\">\n <span class=\"sr-only\">Open menu</span>\n <svg class=\"h-6 w-6\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\" aria-hidden=\"true\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5\" />\n </svg>\n </button>\n </div>\n\n <!-- Mobile menu mask -->\n <div id=\"mobile-menu-mask\" class=\"fixed inset-0 bg-black bg-opacity-40 z-40 hidden\"></div>\n <!-- Mobile menu drawer -->\n <div id=\"mobile-menu\" style=\"z-index: 100000\" class=\"fixed inset-0 bg-page shadow-lg transform translate-x-full transition-transform duration-200 ease-in-out flex flex-col\">\n <div class=\"flex items-center justify-between px-4 !h-[55px] border-b border-edge\">\n <router-link :to=\"{ name: defaultRoute }\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"h-[32px] text-primary\" viewBox=\"0 0 250 250\" fill=\"currentColor\" aria-label=\"Mongoose Studio Logo\">\n <path d=\"M 241.66,125.01 C 237.81,112.98 227.41,111.95 216.76,110.36 C 215.12,95.83 210.99,83.09 202.29,70.33 C 202.99,68.37 201.92,66.65 200.74,65.11 C 183.14,41.46 157.52,27 124.66,27 C 92.62,27 66.09,40.77 48.28,63.38 C 46.21,65.85 46.26,67.71 47.12,69.36 C 37.81,81.15 33.21,93.78 30.99,109.86 C 20.19,110.58 11.71,115.92 9.04,124.94 C 3.28,145.66 5.62,157.95 10.01,169.16 C 16.14,184.65 29.12,186.31 36.04,185.81 C 42.48,185.68 44.94,184.31 44.72,182.11 C 44.26,177.79 41.01,165.31 41.01,149.12 C 41.01,138.22 43.19,127.74 46.21,117.7 C 47.51,113.33 44.66,112.05 39.16,110.28 C 41.01,96.88 45.88,84.85 52.61,73.79 C 55.17,75.56 58.07,74.88 59.61,72.71 C 75.86,54.31 97.89,45.16 124.61,45.16 C 149.71,45.16 171.91,55.91 189.29,73.47 C 191.95,76.59 193.91,76.67 197.6,74.42 C 204.93,86.26 208.28,98.24 208.98,110.39 C 203.84,111.81 201.28,113.19 202.92,118.61 C 206.51,128.99 207.92,137.93 207.92,148.73 C 207.92,162.11 204.8,174.71 202.84,182.71 C 201.92,186.23 204.31,186.91 210.79,186.99 H 213.91 C 225.87,186.61 235.39,180.64 239.49,169.05 C 244.83,155.26 245.08,138.01 241.66,125.01 Z\"/>\n <path d=\"M 124.71,70.81 C 79.63,70.81 47.49,107.12 47.49,148.72 C 47.49,169.16 56.41,188.19 73.71,200.61 L 73.96,200.77 C 77.43,177.81 84.16,162.21 95.29,145.42 C 92.04,137.09 91.71,130.87 92.62,120.83 C 93.81,111.23 96.08,107.12 102.19,107.68 C 110.04,108.87 117.98,116.18 126.22,124.96 C 139.89,124.05 157.81,131.25 178.33,139.09 C 183.11,140.86 183.72,142.63 182.97,148.33 C 179.45,160.93 170.86,166.63 158.11,174.05 C 143.09,183.07 141.13,199.42 153.88,220.09 L 153.96,221.01 L 154.71,221.17 C 181.31,208.43 201.34,181.01 201.34,148.72 C 201.34,108.15 169.29,70.81 124.71,70.81 Z\"/>\n </svg>\n </router-link>\n <button type=\"button\" id=\"close-mobile-menu\" class=\"text-gray-400 p-2 rounded-md\">\n <span class=\"sr-only\">Close menu</span>\n <svg class=\"h-6 w-6\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\" aria-hidden=\"true\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M6 18L18 6M6 6l12 12\" />\n </svg>\n </button>\n </div>\n <nav class=\"flex-1 px-4 py-4 space-y-2\">\n <div class=\"flex items-center gap-2 px-3 py-2\" v-if=\"!user || !hasAPIKey\">\n <button\n type=\"button\"\n @click=\"toggleDarkMode\"\n class=\"inline-flex items-center gap-2 rounded-md px-3 py-2 text-base font-medium text-content-secondary hover:bg-muted\"\n aria-label=\"Toggle dark mode\">\n <svg v-if=\"darkMode\" xmlns=\"http://www.w3.org/2000/svg\" class=\"h-5 w-5\" viewBox=\"0 0 20 20\" fill=\"currentColor\"><path d=\"M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z\"/></svg>\n <svg v-else xmlns=\"http://www.w3.org/2000/svg\" class=\"h-5 w-5\" viewBox=\"0 0 20 20\" fill=\"currentColor\"><path d=\"M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z\"/></svg>\n <span>{{ darkMode ? 'Light mode' : 'Dark mode' }}</span>\n </button>\n </div>\n <a v-if=\"hasAccess(roles, 'root')\"\n href=\"#/\"\n class=\"block px-3 py-2 rounded-md text-base font-medium\"\n :class=\"documentView ? 'text-content bg-primary-subtle' : 'text-content-secondary hover:bg-muted'\">Documents</a>\n <span v-else class=\"block px-3 py-2 rounded-md text-base font-medium text-gray-300 cursor-not-allowed\">\n Documents\n <svg class=\"h-4 w-4 ml-1 inline\" viewBox=\"0 0 20 20\" fill=\"currentColor\" aria-hidden=\"true\">\n <path fill-rule=\"evenodd\" d=\"M10 2a4 4 0 00-4 4v2H5a2 2 0 00-2 2v6a2 2 0 002 2h10a2 2 0 002-2v-6a2 2 0 00-2-2h-1V6a4 4 0 00-4-4zm-3 6V6a3 3 0 116 0v2H7z\" clip-rule=\"evenodd\" />\n </svg>\n </span>\n <a v-if=\"hasAccess(roles, 'dashboards')\"\n href=\"#/dashboards\"\n class=\"block px-3 py-2 rounded-md text-base font-medium\"\n :class=\"dashboardView ? 'text-content bg-primary-subtle' : 'text-content-secondary hover:bg-muted'\">Dashboards</a>\n <span v-else class=\"block px-3 py-2 rounded-md text-base font-medium text-gray-300 cursor-not-allowed\">\n Dashboards\n <svg class=\"h-4 w-4 ml-1 inline\" viewBox=\"0 0 20 20\" fill=\"currentColor\" aria-hidden=\"true\">\n <path fill-rule=\"evenodd\" d=\"M10 2a4 4 0 00-4 4v2H5a2 2 0 00-2 2v6a2 2 0 002 2h10a2 2 0 002-2v-6a2 2 0 00-2-2h-1V6a4 4 0 00-4-4zm-3 6V6a3 3 0 116 0v2H7z\" clip-rule=\"evenodd\" />\n </svg>\n </span>\n <a v-if=\"hasAccess(roles, 'chat') && supportsAI\"\n href=\"#/chat\"\n class=\"block px-3 py-2 rounded-md text-base font-medium\"\n :class=\"chatView ? 'text-content bg-primary-subtle' : 'text-content-secondary hover:bg-muted'\">Chat</a>\n <span v-else-if=\"hasAccess(roles, 'chat') && !supportsAI\"\n class=\"block px-3 py-2 rounded-md text-base font-medium text-gray-300 cursor-not-allowed\"\n :title=\"chatDisabledReason\">\n Chat\n <svg class=\"h-4 w-4 ml-1 inline\" viewBox=\"0 0 20 20\" fill=\"currentColor\" aria-hidden=\"true\">\n <path fill-rule=\"evenodd\" d=\"M10 2a4 4 0 00-4 4v2H5a2 2 0 00-2 2v6a2 2 0 002 2h10a2 2 0 002-2v-6a2 2 0 00-2-2h-1V6a4 4 0 00-4-4zm-3 6V6a3 3 0 116 0v2H7z\" clip-rule=\"evenodd\" />\n </svg>\n </span>\n <span v-else class=\"block px-3 py-2 rounded-md text-base font-medium text-gray-300 cursor-not-allowed\">\n Chat\n <svg class=\"h-4 w-4 ml-1 inline\" viewBox=\"0 0 20 20\" fill=\"currentColor\" aria-hidden=\"true\">\n <path fill-rule=\"evenodd\" d=\"M10 2a4 4 0 00-4 4v2H5a2 2 0 00-2 2v6a2 2 0 002 2h10a2 2 0 002-2v-6a2 2 0 00-2-2h-1V6a4 4 0 00-4-4zm-3 6V6a3 3 0 116 0v2H7z\" clip-rule=\"evenodd\" />\n </svg>\n </span>\n <a v-if=\"hasTaskVisualizer\"\n href=\"#/tasks\"\n class=\"block px-3 py-2 rounded-md text-base font-medium\"\n :class=\"taskView ? 'text-content bg-primary-subtle' : 'text-content-secondary hover:bg-muted'\">Tasks</a>\n <div v-if=\"!user && hasAPIKey\" class=\"mt-4\">\n <button\n type=\"button\"\n @click=\"loginWithGithub\"\n class=\"w-full rounded bg-muted text-content px-3 py-2 text-base font-semibold shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary\">\n Login\n </button>\n </div>\n <div v-if=\"user && hasAPIKey\" class=\"mt-4\">\n <div class=\"flex items-center gap-3 px-3 py-2 bg-page rounded-md\">\n <img class=\"size-8 rounded-full\" :src=\"user.picture\" alt=\"\">\n <span class=\"text-content font-medium\">{{ user.name }}</span>\n </div>\n <div class=\"mt-2 space-y-1\">\n <button\n type=\"button\"\n @click=\"toggleDarkMode\"\n class=\"w-full inline-flex items-center gap-2 px-3 py-2 rounded-md text-base text-content-secondary hover:bg-primary-subtle text-left\">\n <svg v-if=\"darkMode\" xmlns=\"http://www.w3.org/2000/svg\" class=\"h-5 w-5\" viewBox=\"0 0 20 20\" fill=\"currentColor\"><path d=\"M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z\"/></svg>\n <svg v-else xmlns=\"http://www.w3.org/2000/svg\" class=\"h-5 w-5\" viewBox=\"0 0 20 20\" fill=\"currentColor\"><path d=\"M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z\"/></svg>\n {{ darkMode ? 'Light mode' : 'Dark mode' }}\n </button>\n <router-link to=\"/team\" v-if=\"hasAccess(roles, 'team')\" class=\"block px-3 py-2 rounded-md text-base text-content-secondary hover:bg-primary-subtle\">Team</router-link>\n <span v-else class=\"block px-3 py-2 rounded-md text-base text-gray-300 cursor-not-allowed\">\n Team\n <svg class=\"h-4 w-4 ml-1 inline\" viewBox=\"0 0 20 20\" fill=\"currentColor\" aria-hidden=\"true\">\n <path fill-rule=\"evenodd\" d=\"M10 2a4 4 0 00-4 4v2H5a2 2 0 00-2 2v6a2 2 0 002 2h10a2 2 0 002-2v-6a2 2 0 00-2-2h-1V6a4 4 0 00-4-4zm-3 6V6a3 3 0 116 0v2H7z\" clip-rule=\"evenodd\" />\n </svg>\n </span>\n <span @click=\"logout\" class=\"block px-3 py-2 rounded-md text-base text-content-secondary hover:bg-primary-subtle cursor-pointer\">Sign out</span>\n </div>\n </div>\n </nav>\n </div>\n</div>\n\n<modal\n v-if=\"showRecentPagesModal\"\n flush-container\n containerClass=\"!max-w-md !w-[min(94vw,28rem)] !overflow-hidden !rounded-lg !border !border-edge !bg-surface !shadow-xl dark:!bg-shark-900 text-content\"\n bodyClass=\"modal-recent-pages-body !p-0\"\n>\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"closeRecentPagesModal\" role=\"button\" aria-label=\"Close recent pages\">&times;</div>\n <div\n class=\"shrink-0 flex items-center gap-3 border-b border-edge bg-surface px-4 py-4 pr-12 dark:bg-shark-900\"\n >\n <h2 class=\"m-0 text-lg font-semibold text-content\">Recent pages</h2>\n <button\n type=\"button\"\n class=\"rounded px-2 py-1 text-sm font-medium text-content-secondary hover:bg-muted disabled:opacity-50\"\n @click=\"clearRecentPagesHistory\"\n :disabled=\"recentPagesList.length === 0\"\n >\n Clear\n </button>\n </div>\n <div class=\"modal-recent-pages-scroll\">\n <div\n v-if=\"recentPagesList.length === 0\"\n class=\"m-4 rounded border border-dashed border-edge px-3 py-6 text-center text-sm text-content-tertiary\"\n >\n No recent pages yet.\n </div>\n <button\n v-for=\"entry in recentPagesList\"\n :key=\"entry.path + '-' + entry.visitedAt\"\n type=\"button\"\n class=\"w-full px-4 py-2.5 text-left hover:bg-muted\"\n :class=\"entry.path === $route.fullPath ? 'bg-primary-subtle' : ''\"\n @click=\"goToRecentPage(entry)\"\n >\n <div class=\"truncate text-sm font-medium text-content\">{{ entry.label }}</div>\n <div class=\"truncate font-mono text-xs text-content-tertiary\">{{ entry.path }}</div>\n </button>\n </div>\n </template>\n</modal>\n</div>\n";
52691
52726
 
52692
52727
  /***/ },
52693
52728
 
@@ -64161,7 +64196,7 @@ var src_default = VueToastificationPlugin;
64161
64196
  (module) {
64162
64197
 
64163
64198
  "use strict";
64164
- module.exports = /*#__PURE__*/JSON.parse('{"name":"@mongoosejs/studio","version":"0.5.3","description":"AI-Powered Data Workspace for MongoDB: Turn your MongoDB data into dashboards, maps, and interactive workflows - powered by your Mongoose models and AI.","homepage":"https://mongoosestudio.app/","repository":{"type":"git","url":"https://github.com/mongoosejs/studio"},"license":"Apache-2.0","dependencies":{"@ai-sdk/anthropic":"2.x","@ai-sdk/google":"2.x","@ai-sdk/openai":"2.x","ace-builds":"^1.43.6","ai":"5.x","archetype":"0.13.1","csv-stringify":"6.3.0","ejson":"^2.2.3","extrovert":"^0.2.0","marked":"15.0.12","node-inspect-extracted":"3.x","regexp.escape":"^2.0.1","tailwindcss":"3.4.0","time-commando":"1.0.1","typescript":"^6.0.2","vue":"3.x","vue-toastification":"^2.0.0-rc.5","webpack":"5.x","xss":"^1.0.15"},"peerDependencies":{"mongoose":"7.x || 8.x || ^9.0.0"},"optionalPeerDependencies":{"@mongoosejs/task":"0.5.x || 0.6.x"},"devDependencies":{"@masteringjs/eslint-config":"0.1.1","axios":"1.2.2","dedent":"^1.6.0","eslint":"9.30.0","express":"4.x","mocha":"10.2.0","mongodb-memory-server":"^11.0.1","mongoose":"9.x","sinon":"^21.0.1"},"scripts":{"lint":"eslint .","seed":"node seed/index.js","start":"node ./local.js","tailwind":"tailwindcss -o ./frontend/public/tw.css","tailwind:watch":"tailwindcss -o ./frontend/public/tw.css --watch","test":"mocha test/*.test.js","test:frontend":"mocha test/frontend/*.test.js","eval:agent":"node eval/agent/run.js"}}');
64199
+ module.exports = /*#__PURE__*/JSON.parse('{"name":"@mongoosejs/studio","version":"0.5.4","description":"AI-Powered Data Workspace for MongoDB: Turn your MongoDB data into dashboards, maps, and interactive workflows - powered by your Mongoose models and AI.","homepage":"https://mongoosestudio.app/","repository":{"type":"git","url":"https://github.com/mongoosejs/studio"},"license":"Apache-2.0","dependencies":{"@ai-sdk/anthropic":"2.x","@ai-sdk/google":"2.x","@ai-sdk/openai":"2.x","ace-builds":"^1.43.6","ai":"5.x","archetype":"0.13.1","csv-stringify":"6.3.0","ejson":"^2.2.3","extrovert":"^0.2.0","marked":"15.0.12","node-inspect-extracted":"3.x","regexp.escape":"^2.0.1","tailwindcss":"3.4.0","time-commando":"1.0.1","typescript":"^6.0.2","vue":"3.x","vue-toastification":"^2.0.0-rc.5","webpack":"5.x","xss":"^1.0.15"},"peerDependencies":{"mongoose":"7.x || 8.x || ^9.0.0"},"optionalPeerDependencies":{"@mongoosejs/task":"0.5.x || 0.6.x"},"devDependencies":{"@masteringjs/eslint-config":"0.1.1","axios":"1.2.2","dedent":"^1.6.0","eslint":"9.30.0","express":"4.x","mocha":"10.2.0","mongodb-memory-server":"^11.0.1","mongoose":"9.x","sinon":"^21.0.1"},"scripts":{"lint":"eslint .","seed":"node seed/index.js","start":"node ./local.js","tailwind":"tailwindcss -o ./frontend/public/tw.css","tailwind:watch":"tailwindcss -o ./frontend/public/tw.css --watch","test":"mocha test/*.test.js","test:frontend":"mocha test/frontend/*.test.js","eval:agent":"node eval/agent/run.js"}}');
64165
64200
 
64166
64201
  /***/ }
64167
64202
 
@@ -305,9 +305,9 @@ module.exports = {
305
305
 
306
306
  this.draftAgentMode = this.getAgentModePreference();
307
307
  this.chatThreadId = this.threadId;
308
- const [{ chatThreads }, { capabilities }] = await Promise.all([
308
+ const [{ chatThreads }, capabilities] = await Promise.all([
309
309
  api.ChatThread.listChatThreads(),
310
- api.getCapabilities()
310
+ api.getCapabilities().catch(() => this.capabilities)
311
311
  ]);
312
312
  this.chatThreads = chatThreads;
313
313
  this.capabilities = capabilities;
@@ -25,6 +25,7 @@ const {
25
25
  appendCSS(require('vue-toastification/dist/index.css'));
26
26
 
27
27
  const TRACKED_RECENT_PAGE_ROUTE_NAMES = new Set(['model', 'document', 'dashboard', 'chat']);
28
+ const CHAT_ROUTE_NAMES = new Set(['chat index', 'chat']);
28
29
 
29
30
  function formatHistoryLabel(route) {
30
31
  if (!route || typeof route.path !== 'string') {
@@ -240,8 +241,12 @@ app.component('app-component', {
240
241
  window.localStorage.setItem('_mongooseStudioAccessToken', accessToken._id);
241
242
 
242
243
  try {
243
- const { nodeEnv } = await api.status();
244
+ const [{ nodeEnv }, capabilities] = await Promise.all([
245
+ api.status(),
246
+ loadStudioCapabilities()
247
+ ]);
244
248
  this.nodeEnv = nodeEnv;
249
+ this.capabilities = capabilities;
245
250
  } catch (err) {
246
251
  this.authError = 'Error connecting to Mongoose Studio API: ' + err.response?.data?.message ?? err.message;
247
252
  this.status = 'loaded';
@@ -261,12 +266,14 @@ app.component('app-component', {
261
266
  const { user, roles } = await mothership.me();
262
267
 
263
268
  try {
264
- const [{ nodeEnv }, { modelSchemaPaths }] = await Promise.all([
269
+ const [{ nodeEnv }, { modelSchemaPaths }, capabilities] = await Promise.all([
265
270
  api.status(),
266
- api.Model.listModels()
271
+ api.Model.listModels(),
272
+ loadStudioCapabilities()
267
273
  ]);
268
274
  this.nodeEnv = nodeEnv;
269
275
  this.modelSchemaPaths = modelSchemaPaths;
276
+ this.capabilities = capabilities;
270
277
  } catch (err) {
271
278
  this.authError = 'Error connecting to Mongoose Studio API: ' + (err.response?.data?.message ?? err.message);
272
279
  this.status = 'loaded';
@@ -279,12 +286,14 @@ app.component('app-component', {
279
286
  }
280
287
  } else {
281
288
  try {
282
- const [{ nodeEnv }, { modelSchemaPaths }] = await Promise.all([
289
+ const [{ nodeEnv }, { modelSchemaPaths }, capabilities] = await Promise.all([
283
290
  api.status(),
284
- api.Model.listModels()
291
+ api.Model.listModels(),
292
+ loadStudioCapabilities()
285
293
  ]);
286
294
  this.nodeEnv = nodeEnv;
287
295
  this.modelSchemaPaths = modelSchemaPaths;
296
+ this.capabilities = capabilities;
288
297
  } catch (err) {
289
298
  this.authError = 'Error connecting to Mongoose Studio API: ' + (err.response?.data?.message ?? err.message);
290
299
  }
@@ -299,6 +308,7 @@ app.component('app-component', {
299
308
  const nodeEnv = Vue.ref(null);
300
309
  const authError = Vue.ref(null);
301
310
  const modelSchemaPaths = Vue.ref(null);
311
+ const capabilities = Vue.ref(null);
302
312
 
303
313
  const state = Vue.reactive({
304
314
  user,
@@ -306,7 +316,8 @@ app.component('app-component', {
306
316
  status,
307
317
  nodeEnv,
308
318
  authError,
309
- modelSchemaPaths
319
+ modelSchemaPaths,
320
+ capabilities
310
321
  });
311
322
  Vue.provide('state', state);
312
323
 
@@ -358,13 +369,13 @@ router.beforeEach((to, from, next) => {
358
369
  next();
359
370
  });
360
371
 
361
- router.beforeEach((to, from, next) => {
362
- if (['chat index', 'chat'].includes(to.name) && !window.MONGOOSE_STUDIO_CONFIG.hasLLMAPIKey) {
372
+ router.beforeEach(async(to, from, next) => {
373
+ if (CHAT_ROUTE_NAMES.has(to.name) && !(await supportsAI())) {
363
374
  next({ name: 'root' });
364
375
  return;
365
376
  }
366
377
 
367
- if (to.name === 'root' && window.state.roles && window.state.roles[0] === 'dashboards') {
378
+ if (to.name === 'root' && window.state?.roles && window.state.roles[0] === 'dashboards') {
368
379
  return next({ name: 'dashboards' });
369
380
  } else {
370
381
  next();
@@ -379,3 +390,28 @@ app.config.globalProperties = { format, arrayUtils, $toast: toast };
379
390
  app.use(router);
380
391
 
381
392
  app.mount('#content');
393
+
394
+ async function getStudioCapabilities() {
395
+ if (window.__mongooseStudioCapabilitiesPromise == null) {
396
+ window.__mongooseStudioCapabilitiesPromise = api.getCapabilities().catch(err => {
397
+ window.__mongooseStudioCapabilitiesPromise = null;
398
+ throw err;
399
+ });
400
+ }
401
+ return window.__mongooseStudioCapabilitiesPromise;
402
+ }
403
+
404
+ async function loadStudioCapabilities() {
405
+ try {
406
+ return await getStudioCapabilities();
407
+ } catch (err) {
408
+ return null;
409
+ }
410
+ }
411
+
412
+ async function supportsAI() {
413
+ if (window.state?.capabilities == null) {
414
+ return true;
415
+ }
416
+ return window.state.capabilities.supportsAI !== false;
417
+ }
@@ -32,11 +32,11 @@
32
32
  <path fill-rule="evenodd" d="M10 2a4 4 0 00-4 4v2H5a2 2 0 00-2 2v6a2 2 0 002 2h10a2 2 0 002-2v-6a2 2 0 00-2-2h-1V6a4 4 0 00-4-4zm-3 6V6a3 3 0 116 0v2H7z" clip-rule="evenodd" />
33
33
  </svg>
34
34
  </span>
35
- <a v-if="hasAccess(roles, 'chat') && hasLLMAPIKey"
35
+ <a v-if="hasAccess(roles, 'chat') && supportsAI"
36
36
  href="#/chat"
37
37
  class="inline-flex items-center px-1 border-b-2 text-sm font-medium"
38
38
  :class="chatView ? 'text-content border-primary' : 'border-transparent text-content-tertiary hover:text-content'">Chat</a>
39
- <span v-else-if="hasAccess(roles, 'chat') && !hasLLMAPIKey"
39
+ <span v-else-if="hasAccess(roles, 'chat') && !supportsAI"
40
40
  class="inline-flex items-center px-1 border-b-2 border-transparent text-sm font-medium text-gray-300 cursor-not-allowed"
41
41
  :title="chatDisabledReason">
42
42
  Chat
@@ -236,11 +236,11 @@
236
236
  <path fill-rule="evenodd" d="M10 2a4 4 0 00-4 4v2H5a2 2 0 00-2 2v6a2 2 0 002 2h10a2 2 0 002-2v-6a2 2 0 00-2-2h-1V6a4 4 0 00-4-4zm-3 6V6a3 3 0 116 0v2H7z" clip-rule="evenodd" />
237
237
  </svg>
238
238
  </span>
239
- <a v-if="hasAccess(roles, 'chat') && hasLLMAPIKey"
239
+ <a v-if="hasAccess(roles, 'chat') && supportsAI"
240
240
  href="#/chat"
241
241
  class="block px-3 py-2 rounded-md text-base font-medium"
242
242
  :class="chatView ? 'text-content bg-primary-subtle' : 'text-content-secondary hover:bg-muted'">Chat</a>
243
- <span v-else-if="hasAccess(roles, 'chat') && !hasLLMAPIKey"
243
+ <span v-else-if="hasAccess(roles, 'chat') && !supportsAI"
244
244
  class="block px-3 py-2 rounded-md text-base font-medium text-gray-300 cursor-not-allowed"
245
245
  :title="chatDisabledReason">
246
246
  Chat
@@ -1,6 +1,5 @@
1
1
  'use strict';
2
2
 
3
- const api = require('../api');
4
3
  const mothership = require('../mothership');
5
4
  const template = require('./navbar.html');
6
5
  const { routes, hasAccess } = require('../routes');
@@ -107,8 +106,8 @@ module.exports = app => app.component('navbar', {
107
106
  recentPagesList() {
108
107
  return this.localRecentPages;
109
108
  },
110
- hasLLMAPIKey() {
111
- return !!window.MONGOOSE_STUDIO_CONFIG.hasLLMAPIKey;
109
+ supportsAI() {
110
+ return this.state.capabilities == null || this.state.capabilities.supportsAI !== false;
112
111
  },
113
112
  chatDisabledReason() {
114
113
  return 'Chat requires an Anthropic, Gemini, or OpenAI API key.';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mongoosejs/studio",
3
- "version": "0.5.3",
3
+ "version": "0.5.4",
4
4
  "description": "AI-Powered Data Workspace for MongoDB: Turn your MongoDB data into dashboards, maps, and interactive workflows - powered by your Mongoose models and AI.",
5
5
  "homepage": "https://mongoosestudio.app/",
6
6
  "repository": {