@mongoosejs/studio 0.3.11 → 0.4.1

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.
Files changed (45) hide show
  1. package/README.md +9 -0
  2. package/backend/actions/ChatThread/createChatMessage.js +15 -3
  3. package/backend/actions/ChatThread/createChatThread.js +8 -1
  4. package/backend/actions/ChatThread/index.js +1 -0
  5. package/backend/actions/ChatThread/streamChatMessage.js +49 -14
  6. package/backend/actions/ChatThread/toggleAgentMode.js +37 -0
  7. package/backend/actions/Model/getDocuments.js +7 -4
  8. package/backend/actions/Model/getDocumentsStream.js +7 -4
  9. package/backend/actions/Model/streamChatMessage.js +15 -4
  10. package/backend/authorize.js +1 -0
  11. package/backend/chatAgent/agentSystemPrompt.js +45 -0
  12. package/backend/chatAgent/agentToolMetadata.js +20 -0
  13. package/backend/chatAgent/getAgentTools.js +140 -0
  14. package/backend/db/chatMessageSchema.js +5 -0
  15. package/backend/db/chatThreadSchema.js +4 -0
  16. package/backend/helpers/isBindIPConnection.js +8 -0
  17. package/backend/helpers/isLocalhostConnection.js +8 -0
  18. package/backend/helpers/normalizeBindIPOption.js +16 -0
  19. package/backend/helpers/parseProjectionParam.js +180 -0
  20. package/backend/integrations/callLLM.js +4 -2
  21. package/backend/integrations/streamLLM.js +12 -4
  22. package/eslint.config.js +2 -1
  23. package/express.js +16 -0
  24. package/frontend/public/app.js +913 -110
  25. package/frontend/public/tw.css +34 -8
  26. package/frontend/src/api.js +6 -0
  27. package/frontend/src/chat/chat-message/chat-message.html +12 -1
  28. package/frontend/src/chat/chat-message/chat-message.js +8 -0
  29. package/frontend/src/chat/chat-message-script/chat-message-script.html +1 -1
  30. package/frontend/src/chat/chat.html +91 -44
  31. package/frontend/src/chat/chat.js +119 -19
  32. package/frontend/src/create-document/create-document.js +3 -0
  33. package/frontend/src/detail-objectid/detail-objectid.html +1 -0
  34. package/frontend/src/detail-objectid/detail-objectid.js +67 -0
  35. package/frontend/src/document-details/document-property/document-property.html +8 -3
  36. package/frontend/src/document-details/document-property/document-property.js +7 -0
  37. package/frontend/src/document-details/objectid-view-mode-picker/objectid-view-mode-picker.html +12 -0
  38. package/frontend/src/document-details/objectid-view-mode-picker/objectid-view-mode-picker.js +19 -0
  39. package/frontend/src/models/document-search/document-search.js +6 -0
  40. package/frontend/src/models/models.html +6 -8
  41. package/frontend/src/models/models.js +257 -85
  42. package/frontend/src/models/projection-search/projection-search.html +27 -0
  43. package/frontend/src/models/projection-search/projection-search.js +226 -0
  44. package/index.d.ts +1 -1
  45. package/package.json +3 -2
@@ -1072,6 +1072,10 @@ video {
1072
1072
  height: 0.75rem;
1073
1073
  }
1074
1074
 
1075
+ .h-3\.5 {
1076
+ height: 0.875rem;
1077
+ }
1078
+
1075
1079
  .h-4 {
1076
1080
  height: 1rem;
1077
1081
  }
@@ -1212,6 +1216,10 @@ video {
1212
1216
  width: 0.75rem;
1213
1217
  }
1214
1218
 
1219
+ .w-3\.5 {
1220
+ width: 0.875rem;
1221
+ }
1222
+
1215
1223
  .w-36 {
1216
1224
  width: 9rem;
1217
1225
  }
@@ -1256,6 +1264,10 @@ video {
1256
1264
  width: 2rem;
1257
1265
  }
1258
1266
 
1267
+ .w-80 {
1268
+ width: 20rem;
1269
+ }
1270
+
1259
1271
  .w-\[240px\] {
1260
1272
  width: 240px;
1261
1273
  }
@@ -1320,14 +1332,14 @@ video {
1320
1332
  max-width: 280px;
1321
1333
  }
1322
1334
 
1323
- .max-w-\[calc\(100vw-3rem\)\] {
1324
- max-width: calc(100vw - 3rem);
1325
- }
1326
-
1327
1335
  .max-w-\[calc\(100vw-4rem\)\] {
1328
1336
  max-width: calc(100vw - 4rem);
1329
1337
  }
1330
1338
 
1339
+ .max-w-full {
1340
+ max-width: 100%;
1341
+ }
1342
+
1331
1343
  .max-w-lg {
1332
1344
  max-width: 32rem;
1333
1345
  }
@@ -1774,6 +1786,10 @@ video {
1774
1786
  border-bottom-width: 4px;
1775
1787
  }
1776
1788
 
1789
+ .border-l {
1790
+ border-left-width: 1px;
1791
+ }
1792
+
1777
1793
  .border-l-0 {
1778
1794
  border-left-width: 0px;
1779
1795
  }
@@ -3757,6 +3773,10 @@ video {
3757
3773
  margin-right: -2rem;
3758
3774
  }
3759
3775
 
3776
+ .lg\:flex {
3777
+ display: flex;
3778
+ }
3779
+
3760
3780
  .lg\:hidden {
3761
3781
  display: none;
3762
3782
  }
@@ -3773,10 +3793,6 @@ video {
3773
3793
  width: 16rem;
3774
3794
  }
3775
3795
 
3776
- .lg\:max-w-\[calc\(100vw-15rem\)\] {
3777
- max-width: calc(100vw - 15rem);
3778
- }
3779
-
3780
3796
  .lg\:max-w-\[calc\(100vw-20rem\)\] {
3781
3797
  max-width: calc(100vw - 20rem);
3782
3798
  }
@@ -3813,4 +3829,14 @@ video {
3813
3829
 
3814
3830
  :is(:where(.dark) .dark\:bg-shark-800\/80) {
3815
3831
  background-color: rgb(33 37 41 / 0.8);
3832
+ }
3833
+
3834
+ :is(:where(.dark) .dark\:text-gray-400) {
3835
+ --tw-text-opacity: 1;
3836
+ color: rgb(156 163 175 / var(--tw-text-opacity));
3837
+ }
3838
+
3839
+ :is(:where(.dark) .dark\:text-gray-500) {
3840
+ --tw-text-opacity: 1;
3841
+ color: rgb(107 114 128 / var(--tw-text-opacity));
3816
3842
  }
@@ -62,6 +62,9 @@ if (window.MONGOOSE_STUDIO_CONFIG.isLambda) {
62
62
  shareChatThread(params) {
63
63
  return client.post('', { action: 'ChatThread.shareChatThread', ...params }).then(res => res.data);
64
64
  },
65
+ toggleAgentMode(params) {
66
+ return client.post('', { action: 'ChatThread.toggleAgentMode', ...params }).then(res => res.data);
67
+ },
65
68
  streamChatMessage: async function* streamChatMessage(params) {
66
69
  // Don't stream on Next.js or Netlify for now.
67
70
  const data = await client.post('', { action: 'ChatThread.createChatMessage', ...params }).then(res => res.data);
@@ -238,6 +241,9 @@ if (window.MONGOOSE_STUDIO_CONFIG.isLambda) {
238
241
  shareChatThread: function shareChatThread(params) {
239
242
  return client.post('/ChatThread/shareChatThread', params).then(res => res.data);
240
243
  },
244
+ toggleAgentMode: function toggleAgentMode(params) {
245
+ return client.post('/ChatThread/toggleAgentMode', params).then(res => res.data);
246
+ },
241
247
  streamChatMessage: async function* streamChatMessage(params) {
242
248
  const accessToken = window.localStorage.getItem('_mongooseStudioAccessToken') || null;
243
249
  const url = window.MONGOOSE_STUDIO_CONFIG.baseURL + '/ChatThread/streamChatMessage?' + new URLSearchParams(params).toString();
@@ -1,8 +1,19 @@
1
1
  <div class="relative flex items-start" :class="{'justify-end': message.role === 'user'}">
2
2
  <div
3
- class="min-w-0 max-w-[calc(100vw-3rem)] lg:max-w-[calc(100vw-15rem)]"
3
+ class="min-w-0 max-w-full"
4
4
  :class="{'text-right': message.role === 'user'}">
5
5
 
6
+ <div v-if="message.toolCalls && message.toolCalls.length" class="mb-2 text-xs text-gray-500 dark:text-gray-400">
7
+ <div
8
+ v-for="(tc, i) in message.toolCalls"
9
+ :key="i"
10
+ class="flex items-center gap-1.5 py-0.5 font-mono">
11
+ <svg :class="['w-3.5 h-3.5 shrink-0', tc.status === 'running' ? 'animate-spin text-gray-400 dark:text-gray-500' : 'text-gray-500 dark:text-gray-400']" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M21.75 6.75a4.5 4.5 0 0 1-4.884 4.484c-1.076-.091-2.264.071-2.95.904l-7.152 8.684a2.548 2.548 0 1 1-3.586-3.586l8.684-7.152c.833-.686.995-1.874.904-2.95a4.5 4.5 0 0 1 6.336-4.486l-3.276 3.276a3.004 3.004 0 0 0 2.25 2.25l3.276-3.276c.256.565.398 1.192.398 1.852Z" /><path stroke-linecap="round" stroke-linejoin="round" d="M4.867 19.125h.008v.008h-.008v-.008Z" /></svg>
12
+ <span>{{ tc.toolName }}</span>
13
+ <span v-if="tc.toolName !== 'typeCheck'" class="text-gray-400 dark:text-gray-500">{{ formatToolInput(tc.input) }}</span>
14
+ </div>
15
+ </div>
16
+
6
17
  <div class="text-sm text-content rounded-md inline-block relative" :class="styleForMessage">
7
18
  <div v-for="part in contentSplitByScripts">
8
19
  <div v-if="part.type === 'text'" v-html="marked(part.content)">
@@ -54,6 +54,14 @@ module.exports = app => app.component('chat-message', {
54
54
  marked(text) {
55
55
  return marked(text);
56
56
  },
57
+ formatToolInput(input) {
58
+ if (!input) return '';
59
+ const parts = [];
60
+ if (input.modelName) parts.push(input.modelName);
61
+ if (input.filter && Object.keys(input.filter).length) parts.push(JSON.stringify(input.filter));
62
+ if (input.limit && input.limit !== 10) parts.push('limit=' + input.limit);
63
+ return parts.length ? '(' + parts.join(', ') + ')' : '';
64
+ },
57
65
  async executeScript(message, script) {
58
66
  const { chatMessage } = await api.ChatMessage.executeScript({
59
67
  chatMessageId: message._id,
@@ -35,7 +35,7 @@
35
35
  v-if="activeTab === 'code' && !isEditing"
36
36
  class="px-2 py-1 mr-1 text-xs bg-gray-700 text-white border-none rounded cursor-pointer hover:bg-gray-600 transition-colors flex items-center"
37
37
  @click.stop="startEditing">
38
- <svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3" viewBox="0 0 20 20" fill="currentColor">
38
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-4" viewBox="0 0 20 20" fill="currentColor">
39
39
  <path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM4 13.5V16h2.5l7.086-7.086-2.828-2.828L4 13.5z" />
40
40
  </svg>
41
41
  </button>
@@ -69,7 +69,7 @@
69
69
  class="rounded p-1.5 text-gray-400 hover:text-gray-600 hover:bg-muted"
70
70
  title="Hide sidebar"
71
71
  >
72
- <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
72
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
73
73
  <path stroke-linecap="round" stroke-linejoin="round" d="m18.75 4.5-7.5 7.5 7.5 7.5m-6-15L5.25 12l7.5 7.5" />
74
74
  </svg>
75
75
  </button>
@@ -82,57 +82,104 @@
82
82
  aria-label="Share thread with workspace"
83
83
  :title="'Share thread with workspace' + (!hasWorkspace ? ' (requires a pro workspace)' : !chatThreadId ? ': Open a thread first!' : '')"
84
84
  >
85
- <svg v-if="hasWorkspace" xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><path d="M18 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7a2.48 2.48 0 0 0 0-1.39l7.02-4.11a2.5 2.5 0 1 0-.87-1.37L8.04 9.94a2.5 2.5 0 1 0 0 4.12l7.12 4.16a2.5 2.5 0 1 0 .84-1.34l-7.05-4.12c-.04-.02-.08-.05-.11-.07a2.48 2.48 0 0 0 0-1.39c.03-.02.07-.04.11-.07l7.11-4.16c.52.47 1.2.76 1.94.76a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0-1.94.94L7.97 8.43a2.5 2.5 0 1 0 0 7.14l9.09 5.3c.52-.47 1.2-.76 1.94-.76a2.5 2.5 0 1 0 0-5z"/></svg>
86
- <svg v-else xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><path d="M12 1a5 5 0 00-5 5v3H6a2 2 0 00-2 2v9a2 2 0 002 2h12a2 2 0 002-2v-9a2 2 0 00-2-2h-1V6a5 5 0 00-5-5zm-3 8V6a3 3 0 016 0v3H9zm9 2v9H6v-9h12z"/></svg>
85
+ <svg v-if="hasWorkspace" xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24"><path d="M18 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7a2.48 2.48 0 0 0 0-1.39l7.02-4.11a2.5 2.5 0 1 0-.87-1.37L8.04 9.94a2.5 2.5 0 1 0 0 4.12l7.12 4.16a2.5 2.5 0 1 0 .84-1.34l-7.05-4.12c-.04-.02-.08-.05-.11-.07a2.48 2.48 0 0 0 0-1.39c.03-.02.07-.04.11-.07l7.11-4.16c.52.47 1.2.76 1.94.76a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0-1.94.94L7.97 8.43a2.5 2.5 0 1 0 0 7.14l9.09 5.3c.52-.47 1.2-.76 1.94-.76a2.5 2.5 0 1 0 0-5z"/></svg>
86
+ <svg v-else xmlns="http://www.w3.org/2000/svg" class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M12 1a5 5 0 00-5 5v3H6a2 2 0 00-2 2v9a2 2 0 002 2h12a2 2 0 002-2v-9a2 2 0 00-2-2h-1V6a5 5 0 00-5-5zm-3 8V6a3 3 0 016 0v3H9zm9 2v9H6v-9h12z"/></svg>
87
+ </button>
88
+ <button
89
+ type="button"
90
+ @click="toggleAgentMode"
91
+ class="rounded p-1.5 transition-colors"
92
+ :class="isAgentModeEnabled ? 'text-primary bg-primary/10' : 'text-gray-400 hover:text-gray-600 hover:bg-muted'"
93
+ :aria-label="isAgentModeEnabled ? 'Turn agent mode off' : 'Turn agent mode on'"
94
+ :aria-pressed="isAgentModeEnabled"
95
+ :title="isAgentModeEnabled ? 'Agent mode on' : 'Agent mode off'"
96
+ >
97
+ <svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5" fill="currentColor" viewBox="0 0 200 200"><path d="m187 170.9-38.72-13.73-0.55-0.25-11.24-12.18v-9.98c6.3-6.82 6.43-11.03 8.3-23.22 7.34-1.41 10.2-7.7 12.28-18.9 0.97-5.02 2.72-9.88 0.14-13.92-1.38-2.35-3.49-3.08-5.62-3.38 1.65-11.57 5.47-27.07-2.68-42.38-5.47-9.94-14-14.18-21.8-14.92-3.5-9.15-12.32-13.06-27.11-13.06h-1.06c-27.2 0-48.53 17.96-52.18 39.74-2.05 11.38-0.41 20.41 0.74 30.03-2.23 0.19-4.38 1.02-5.84 3.42-2.21 3.83-1.27 8.84 0.06 14.19 2 8.28 2.26 16.18 12.9 18.96 1.47 6.52 0.58 14.07 8.3 22.84v10.77l-12.54 12.83-38.38 12.85v24.18h57.31c-3.11-14.37-6.53-23.75-5.45-42.72l0.44-0.34c5.71 9.04 16.02 15.97 29.22 25.91l-12.68 17.15h38.07l-13.16-17.2c11.44-9.29 22.31-16.55 29.2-25.38 4.68 9.53-1.48 26.7-4.72 42.58h56.77v-23.89zm-49.56-49.09c-1.43 5.43-2.53 6.16-9.69 14.68l-11.68 13.11c-3.48 3.27-4.8 3.27-11.11 3.27h-10.39c-6.25 0-7.45-0.78-11.01-4.46-6.78-7.7-11.76-13.37-15.09-17.32-5.39-6.33-5.85-7.2-7.16-18.89-0.73-4.86-3.02-14.25-2.94-14.91 3.94 3.43 13.59 3.35 16.89 3.35 12.76 0 15.79-2.74 19.92-17.12 0.68-2.98 1.71-3.53 4-3.53 2.04 0 3.04 0.98 3.86 3.71 3.9 13.69 5.2 16.52 16.15 16.76h2.99c8.1 0 15.01-1.52 18.58-2.93-0.5 5.06-2.28 16.7-3.32 24.28zm4.72-39.02c-1.97 10.98-2.94 12.3-17.69 12.91-9.05 0.38-11.35-0.71-14.17-8.69-3.69-11.25-2.17-13 5.55-13.83 6.36-0.83 11.74-0.79 15.92-0.79 10.01 0 12.11 0.27 10.39 10.4zm-84.38-32c0-11.99 9.26-21.63 15.54-23.52 4.46-1.14 7.99 3.61 15.25 5.74 5.06 1.54 10.71 1.32 11.84 1.32 11.49 0 16.71-2.62 21.55-6.91 1.66-1.28 3.22-1.57 4.86-0.67 10.89 6.36 15.34 16.28 14.74 26.07-0.36 5.5-2.18 9.03-1.38 12.23l1.14 3.62c-3.83-0.82-6.2-0.92-10.69-0.92-9.44 0-17.61 1.24-22.14 2.5-3.66 1.24-5.2 1.44-8.35 1.44-2.98 0-4.92-0.2-7.56-1.44-4.97-1.54-14.49-2.5-21.47-2.5-3.69 0-7 0.01-10.9 0.62l-1.84 0.3 0.42-3.07c0.82-3.01-1.01-7.56-1.01-14.81z"/><path d="m116.6 125.4-12.18-2.58c-2.29-0.68-3.7 0.76-5.4 0.76-1.66 0-2.79-1.35-5.4-0.66l-10.88 2.36c-1.35 0.12-1.55 1.44-0.79 3.32 0.38 1.02 1.26 0.7 3.07 0.36 3.25-0.46 4.65-0.84 8.11-1.04 1.85-0.17 2.59 0.41 5.06 0.41h1.14c1.72 0 2.62-0.55 4.15-0.31 3.81 0.31 5.64 0.72 11.64 1.22 2.08 0.38 2.4-1.15 2.21-2.52-0.07-0.65-0.24-1.1-0.73-1.32z"/><path d="m94.08 133.5c-1.96 0-2.49 1.23-2.4 2.39 0.06 1.38 0.93 2.27 2.57 2.27h10.06c2.13 0 2.7-1.25 2.7-2.56 0-1.13-0.65-2.1-2.53-2.1h-10.4z"/><path d="m91.31 106.6c-2.13 0-2.51 3.32-1.24 4.55 1.11 1.08 7.39 3.76 9.34 3.76s8.38-3.24 9.4-3.87c1.9-1.2 0.85-4.82-1.7-4.44-1.84 0.3-6.01 3.39-7.96 3.01-1.53-0.32-6.08-3.01-7.84-3.01z"/><path d="m85.62 73.71c-5.3-1.1-10.53-1.15-14.51-1.15-10.96 0-14.08-0.44-14.33 5.26-0.47 11.15 1.82 15.96 6.69 17.19 4.7 1.26 6.84 1.08 11.71 1.08 8.88 0 10.9-1.18 14.23-10.67 2.38-6.81 1.87-10.59-3.79-11.71z"/><path d="m114.5 73.51c-7.72 0.83-7.35 2.71-6.39 9.73 1.78 7.47 3.64 11.94 10.97 12.49 3.07 0.14 4.97-0.05 8-0.32 10.72-0.96 12.6-2.48 14.45-12.22 1.88-10.29-0.54-10.77-9.79-10.77-4.18 0-10.45 0.26-17.24 1.09z"/></svg>
87
98
  </button>
88
99
  </div>
89
100
  </aside>
90
101
 
91
- <!-- Main Chat Area -->
92
- <main class="flex-1 flex flex-col bg-slate-50">
93
- <div class="flex-1 overflow-y-auto p-6 space-y-4" ref="messagesContainer">
94
- <div v-if="chatMessages?.length === 0">
95
- <div class="flex items-center w-full h-full justify-center py-3 mb-4">
96
- <p class="mx-4 font-bold text-content">
97
- Ask Mongoose Studio to analyze your data or generate a script
98
- </p>
102
+ <div class="flex-1 min-w-0 flex">
103
+ <!-- Main Chat Area -->
104
+ <main class="flex-1 min-w-0 flex flex-col bg-slate-50">
105
+ <div class="flex-1 overflow-y-auto p-6 space-y-4" ref="messagesContainer">
106
+ <div v-if="chatMessages?.length === 0">
107
+ <div class="flex items-center w-full h-full justify-center py-3 mb-4">
108
+ <p class="mx-4 font-bold text-content">
109
+ Ask Mongoose Studio to analyze your data or generate a script
110
+ </p>
111
+ </div>
99
112
  </div>
113
+ <ul role="list" class="space-y-4" v-else>
114
+ <li v-for="message in chatMessages" :key="message._id">
115
+ <chat-message :message="message" :target-dashboard-id="currentThread?.dashboardId"></chat-message>
116
+ </li>
117
+ </ul>
100
118
  </div>
101
- <ul role="list" class="space-y-4" v-else>
102
- <li v-for="message in chatMessages" :key="message._id">
103
- <chat-message :message="message" :target-dashboard-id="currentThread?.dashboardId"></chat-message>
104
- </li>
105
- </ul>
106
- </div>
107
119
 
108
120
 
109
- <!-- Input Area -->
110
- <div class="border-t p-4">
111
- <form @submit.prevent="sendMessage" :disabled="sendingMessage" class="flex gap-2 items-end justify-end">
112
- <textarea
113
- v-model="newMessage"
114
- :placeholder="sendingMessage ? 'Sending...' : 'Ask about your data, generate a query, or build a chart…'"
115
- class="flex-1 border rounded px-4 py-2 resize-none overflow-y-auto shadow-sm"
116
- :disabled="sendingMessage"
117
- rows="2"
118
- ref="messageInput"
119
- @input="adjustTextareaHeight"
120
- @keydown.enter.exact.prevent="handleEnter"
121
- ></textarea>
122
- <button class="bg-primary hover:bg-primary-hover text-white px-4 h-[42px] rounded disabled:bg-gray-600" :disabled="sendingMessage">
123
- <svg v-if="sendingMessage" style="height: 1em" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
124
- <g>
125
- <circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2" opacity="0.3" />
126
- <path d="M12 2a10 10 0 0 1 10 10" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
127
- <animateTransform attributeName="transform" type="rotate" from="0 12 12" to="360 12 12" dur="1s" repeatCount="indefinite" />
128
- </path>
129
- </g>
130
- </svg>
131
- <span v-else>Send</span>
132
- </button>
133
- </form>
134
- </div>
135
- </main>
121
+ <!-- Input Area -->
122
+ <div class="border-t p-4">
123
+ <form @submit.prevent="sendMessage" :disabled="sendingMessage" class="flex gap-2 items-end justify-end">
124
+ <textarea
125
+ v-model="newMessage"
126
+ :placeholder="sendingMessage ? 'Sending...' : 'Ask about your data, generate a query, or build a chart…'"
127
+ class="flex-1 border rounded px-4 py-2 resize-none overflow-y-auto shadow-sm"
128
+ :disabled="sendingMessage"
129
+ rows="2"
130
+ ref="messageInput"
131
+ @input="adjustTextareaHeight"
132
+ @keydown.enter.exact.prevent="handleEnter"
133
+ ></textarea>
134
+ <button class="bg-primary hover:bg-primary-hover text-white px-4 h-[42px] rounded disabled:bg-gray-600" :disabled="sendingMessage">
135
+ <svg v-if="sendingMessage" style="height: 1em" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
136
+ <g>
137
+ <circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2" opacity="0.3" />
138
+ <path d="M12 2a10 10 0 0 1 10 10" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
139
+ <animateTransform attributeName="transform" type="rotate" from="0 12 12" to="360 12 12" dur="1s" repeatCount="indefinite" />
140
+ </path>
141
+ </g>
142
+ </svg>
143
+ <span v-else>Send</span>
144
+ </button>
145
+ </form>
146
+ </div>
147
+ </main>
148
+
149
+ <aside
150
+ v-if="isAgentModeEnabled"
151
+ class="hidden lg:flex shrink-0 border-l border-edge bg-page transition-all duration-300 ease-in-out overflow-hidden"
152
+ :class="showAgentSidebar ? 'w-80' : 'w-0 border-l-0'"
153
+ >
154
+ <div v-if="showAgentSidebar" class="flex h-full w-80 flex-col">
155
+ <div class="flex items-center justify-between border-b border-edge px-4 py-3">
156
+ <div>
157
+ <h2 class="text-sm font-semibold text-content">Agent Mode</h2>
158
+ <p class="mt-1 text-xs text-content-tertiary">Agent mode lets Mongoose Studio inspect your data and validate scripts before it responds.</p>
159
+ </div>
160
+ <button
161
+ type="button"
162
+ @click="closeAgentSidebar"
163
+ class="rounded p-1.5 text-gray-400 hover:bg-muted hover:text-gray-600"
164
+ title="Hide agent sidebar"
165
+ >
166
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
167
+ <path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
168
+ </svg>
169
+ </button>
170
+ </div>
171
+ <div class="flex-1 overflow-y-auto px-4 py-4">
172
+ <div class="mb-3 text-xs font-semibold uppercase tracking-wider text-gray-400">Available Tools</div>
173
+ <ul role="list" class="space-y-3">
174
+ <li v-for="tool in agentTools" :key="tool.name" class="rounded-md border border-edge bg-surface px-3 py-3">
175
+ <div class="text-sm font-medium text-content">{{ tool.name }}</div>
176
+ <div class="mt-1 text-sm text-content-secondary">{{ tool.description }}</div>
177
+ </li>
178
+ </ul>
179
+ </div>
180
+ </div>
181
+ </aside>
182
+ </div>
136
183
 
137
184
  <pro-upgrade-modal
138
185
  :show="showProUpgradeModal"
@@ -1,9 +1,12 @@
1
1
  'use strict';
2
2
 
3
3
  const api = require('../api');
4
+ const agentToolMetadata = require('../../../backend/chatAgent/agentToolMetadata');
4
5
  const getCurrentDateTimeContext = require('../getCurrentDateTimeContext');
5
6
  const template = require('./chat.html');
6
7
 
8
+ const AGENT_MODE_STORAGE_KEY = '_mongooseStudioAgentMode';
9
+
7
10
  module.exports = {
8
11
  template: template,
9
12
  props: ['threadId'],
@@ -12,12 +15,15 @@ module.exports = {
12
15
  sendingMessage: false,
13
16
  newMessage: '',
14
17
  chatThreadId: null,
18
+ draftAgentMode: false,
15
19
  chatThreads: [],
16
20
  chatMessages: [],
17
21
  hideSidebar: null,
22
+ showAgentSidebar: false,
18
23
  sharingThread: false,
19
24
  threadSearch: '',
20
- showProUpgradeModal: false
25
+ showProUpgradeModal: false,
26
+ agentTools: agentToolMetadata
21
27
  }),
22
28
  methods: {
23
29
  async sendMessage() {
@@ -26,7 +32,9 @@ module.exports = {
26
32
  const content = this.newMessage;
27
33
  this.newMessage = '';
28
34
  if (!this.chatThreadId) {
29
- const { chatThread } = await api.ChatThread.createChatThread();
35
+ const { chatThread } = await api.ChatThread.createChatThread({
36
+ agentMode: this.draftAgentMode
37
+ });
30
38
  this.chatThreads.unshift(chatThread);
31
39
  this.chatThreadId = chatThread._id;
32
40
  this.chatMessages = [];
@@ -40,11 +48,7 @@ module.exports = {
40
48
  role: 'user'
41
49
  });
42
50
 
43
- this.$nextTick(() => {
44
- if (this.$refs.messagesContainer) {
45
- this.$refs.messagesContainer.scrollTop = this.$refs.messagesContainer.scrollHeight;
46
- }
47
- });
51
+ this.scrollToBottom();
48
52
 
49
53
  const params = {
50
54
  chatThreadId: this.chatThreadId,
@@ -53,6 +57,7 @@ module.exports = {
53
57
  };
54
58
  let userChatMessage = null;
55
59
  let assistantChatMessage = null;
60
+ const toolCalls = [];
56
61
  for await (const event of api.ChatThread.streamChatMessage(params)) {
57
62
  if (event.chatMessage) {
58
63
  if (!userChatMessage) {
@@ -61,6 +66,7 @@ module.exports = {
61
66
  } else {
62
67
  const assistantChatMessageIndex = this.chatMessages.indexOf(assistantChatMessage);
63
68
  assistantChatMessage = event.chatMessage;
69
+ assistantChatMessage.toolCalls = toolCalls;
64
70
  if (assistantChatMessageIndex !== -1) {
65
71
  this.chatMessages.splice(assistantChatMessageIndex, 1, assistantChatMessage);
66
72
  } else {
@@ -73,27 +79,44 @@ module.exports = {
73
79
  thread.title = event.chatThread.title;
74
80
  }
75
81
  }
82
+ } else if (event.toolCall) {
83
+ toolCalls.push({ toolName: event.toolCall.toolName, input: event.toolCall.input, status: 'running' });
84
+ if (!assistantChatMessage) {
85
+ assistantChatMessage = {
86
+ _id: Math.random().toString(36).substr(2, 9),
87
+ content: '',
88
+ role: 'assistant',
89
+ toolCalls: [...toolCalls]
90
+ };
91
+ this.chatMessages.push(assistantChatMessage);
92
+ assistantChatMessage = this.chatMessages[this.chatMessages.length - 1];
93
+ } else {
94
+ assistantChatMessage.toolCalls = [...toolCalls];
95
+ }
96
+ this.scrollToBottom();
97
+ } else if (event.toolResult) {
98
+ const tc = toolCalls.find(t => t.toolName === event.toolResult.toolName && t.status === 'running');
99
+ if (tc) {
100
+ tc.status = 'done';
101
+ }
102
+ if (assistantChatMessage) {
103
+ assistantChatMessage.toolCalls = [...toolCalls];
104
+ }
105
+ this.scrollToBottom();
76
106
  } else if (event.textPart) {
77
107
  if (!assistantChatMessage) {
78
108
  assistantChatMessage = {
79
109
  _id: Math.random().toString(36).substr(2, 9),
80
110
  content: event.textPart,
81
- role: 'assistant'
111
+ role: 'assistant',
112
+ toolCalls: [...toolCalls]
82
113
  };
83
114
  this.chatMessages.push(assistantChatMessage);
84
115
  assistantChatMessage = this.chatMessages[this.chatMessages.length - 1];
85
- this.$nextTick(() => {
86
- if (this.$refs.messagesContainer) {
87
- this.$refs.messagesContainer.scrollTop = this.$refs.messagesContainer.scrollHeight;
88
- }
89
- });
116
+ this.scrollToBottom();
90
117
  } else {
91
118
  assistantChatMessage.content += event.textPart;
92
- this.$nextTick(() => {
93
- if (this.$refs.messagesContainer) {
94
- this.$refs.messagesContainer.scrollTop = this.$refs.messagesContainer.scrollHeight;
95
- }
96
- });
119
+ this.scrollToBottom();
97
120
  }
98
121
  } else if (event.message) {
99
122
  throw new Error(event.message);
@@ -112,10 +135,38 @@ module.exports = {
112
135
  this.$refs.messagesContainer.scrollTop = this.$refs.messagesContainer.scrollHeight;
113
136
  }
114
137
  });
138
+ } catch (err) {
139
+ this.$toast.error(err?.message || 'Failed to send message.');
115
140
  } finally {
116
141
  this.sendingMessage = false;
117
142
  }
118
143
  },
144
+ async toggleAgentMode() {
145
+ const wasEnabled = this.isAgentModeEnabled;
146
+ const newValue = !this.isAgentModeEnabled;
147
+ this.draftAgentMode = newValue;
148
+ this.persistAgentModePreference(newValue);
149
+ if (!this.chatThreadId) {
150
+ this.maybeOpenAgentSidebar({ wasEnabled, isEnabled: newValue });
151
+ return;
152
+ }
153
+ const { chatThread } = await api.ChatThread.toggleAgentMode({
154
+ chatThreadId: this.chatThreadId,
155
+ agentMode: newValue
156
+ });
157
+ const idx = this.chatThreads.findIndex(t => t._id === chatThread._id);
158
+ if (idx !== -1) {
159
+ this.chatThreads.splice(idx, 1, chatThread);
160
+ }
161
+ this.maybeOpenAgentSidebar({ wasEnabled, isEnabled: newValue });
162
+ },
163
+ scrollToBottom() {
164
+ this.$nextTick(() => {
165
+ if (this.$refs.messagesContainer) {
166
+ this.$refs.messagesContainer.scrollTop = this.$refs.messagesContainer.scrollHeight;
167
+ }
168
+ });
169
+ },
119
170
  handleEnter(ev) {
120
171
  if (!ev.shiftKey) {
121
172
  this.sendMessage();
@@ -135,7 +186,9 @@ module.exports = {
135
186
  return message.role === 'user' ? 'bg-muted' : '';
136
187
  },
137
188
  async createNewThread() {
138
- const { chatThread } = await api.ChatThread.createChatThread();
189
+ const { chatThread } = await api.ChatThread.createChatThread({
190
+ agentMode: this.draftAgentMode
191
+ });
139
192
  this.$toast.success('Chat thread created!');
140
193
  this.$router.push('/chat/' + chatThread._id);
141
194
  },
@@ -177,12 +230,57 @@ module.exports = {
177
230
  } finally {
178
231
  this.sharingThread = false;
179
232
  }
233
+ },
234
+ isDesktopViewport() {
235
+ return typeof window !== 'undefined' &&
236
+ typeof window.matchMedia === 'function' &&
237
+ window.matchMedia('(min-width: 1024px)').matches;
238
+ },
239
+ getAgentModePreference() {
240
+ if (typeof window === 'undefined') {
241
+ return false;
242
+ }
243
+ return window.localStorage?.getItem(AGENT_MODE_STORAGE_KEY) === 'true';
244
+ },
245
+ persistAgentModePreference(agentMode) {
246
+ if (typeof window !== 'undefined') {
247
+ window.localStorage?.setItem(AGENT_MODE_STORAGE_KEY, String(agentMode));
248
+ }
249
+ },
250
+ async syncCurrentThreadAgentMode() {
251
+ if (!this.chatThreadId || !this.currentThread || !this.draftAgentMode || this.currentThread.agentMode) {
252
+ return;
253
+ }
254
+ const { chatThread } = await api.ChatThread.toggleAgentMode({
255
+ chatThreadId: this.chatThreadId,
256
+ agentMode: true
257
+ });
258
+ const idx = this.chatThreads.findIndex(t => t._id === chatThread._id);
259
+ if (idx !== -1) {
260
+ this.chatThreads.splice(idx, 1, chatThread);
261
+ }
262
+ },
263
+ maybeOpenAgentSidebar({ wasEnabled, isEnabled }) {
264
+ if (!isEnabled) {
265
+ this.showAgentSidebar = false;
266
+ return;
267
+ }
268
+ if (wasEnabled || !this.isDesktopViewport()) {
269
+ return;
270
+ }
271
+ this.showAgentSidebar = true;
272
+ },
273
+ closeAgentSidebar() {
274
+ this.showAgentSidebar = false;
180
275
  }
181
276
  },
182
277
  computed: {
183
278
  currentThread() {
184
279
  return this.chatThreads.find(t => t._id === this.chatThreadId);
185
280
  },
281
+ isAgentModeEnabled() {
282
+ return this.currentThread?.agentMode ?? this.draftAgentMode;
283
+ },
186
284
  hasWorkspace() {
187
285
  return !!window.MONGOOSE_STUDIO_CONFIG.workspace?._id;
188
286
  },
@@ -200,10 +298,12 @@ module.exports = {
200
298
  async mounted() {
201
299
  window.pageState = this;
202
300
 
301
+ this.draftAgentMode = this.getAgentModePreference();
203
302
  this.chatThreadId = this.threadId;
204
303
  const { chatThreads } = await api.ChatThread.listChatThreads();
205
304
  this.chatThreads = chatThreads;
206
305
  if (this.chatThreadId) {
306
+ await this.syncCurrentThreadAgentMode();
207
307
  const { chatMessages } = await api.ChatThread.getChatThread({ chatThreadId: this.chatThreadId });
208
308
  this.chatMessages = chatMessages;
209
309
  }
@@ -53,6 +53,9 @@ module.exports = app => app.component('create-document', {
53
53
  documentData: this.aiOriginalDocument,
54
54
  currentDateTime: getCurrentDateTimeContext()
55
55
  })) {
56
+ if (event?.message) {
57
+ throw new Error(event.message);
58
+ }
56
59
  if (event?.textPart) {
57
60
  this.aiSuggestion += event.textPart;
58
61
  }
@@ -0,0 +1 @@
1
+ <pre class="w-full whitespace-pre-wrap break-words font-mono text-sm text-content-secondary m-0">{{displayValue}}</pre>
@@ -0,0 +1,67 @@
1
+ 'use strict';
2
+
3
+ const template = require('./detail-objectid.html');
4
+ const ObjectId = require('mongoose/lib/types/objectid');
5
+
6
+ module.exports = app => app.component('detail-objectid', {
7
+ template,
8
+ props: ['value', 'viewMode'],
9
+ emits: ['updated'],
10
+ watch: {
11
+ displayValue: {
12
+ immediate: true,
13
+ handler(val) {
14
+ this.$emit('updated', val);
15
+ }
16
+ }
17
+ },
18
+ computed: {
19
+ format() {
20
+ return this.viewMode || 'hex';
21
+ },
22
+ hexValue() {
23
+ if (this.value == null) {
24
+ return null;
25
+ }
26
+ if (typeof this.value === 'string') {
27
+ return this.value;
28
+ }
29
+ if (typeof this.value === 'object') {
30
+ if (typeof this.value.toHexString === 'function') {
31
+ return this.value.toHexString();
32
+ }
33
+ if (typeof this.value.$oid === 'string') {
34
+ return this.value.$oid;
35
+ }
36
+ }
37
+ return String(this.value);
38
+ },
39
+ objectIdDate() {
40
+ const hex = this.hexValue;
41
+ return new ObjectId(hex).getTimestamp();
42
+ },
43
+ displayValue() {
44
+ if (this.value == null) {
45
+ return String(this.value);
46
+ }
47
+
48
+ const hex = this.hexValue;
49
+ if (this.format === 'object_call') {
50
+ return `ObjectId('${hex}')`;
51
+ }
52
+ if (this.format === 'unix_seconds') {
53
+ if (!this.objectIdDate) {
54
+ return 'Invalid ObjectId';
55
+ }
56
+ return String(Math.floor(this.objectIdDate.getTime() / 1000));
57
+ }
58
+ if (this.format === 'date') {
59
+ if (!this.objectIdDate) {
60
+ return 'Invalid ObjectId';
61
+ }
62
+ return this.objectIdDate.toISOString();
63
+ }
64
+ return hex;
65
+ }
66
+ }
67
+ });