@mongoosejs/studio 0.1.10 → 0.1.12

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/README.md CHANGED
@@ -34,8 +34,10 @@ If you have a Mongoose Studio Pro API key, you can set it as follows:
34
34
  const opts = process.env.MONGOOSE_STUDIO_API_KEY ? { apiKey: process.env.MONGOOSE_STUDIO_API_KEY } : {};
35
35
  // Optionally specify which ChatGPT model to use for chat messages
36
36
  opts.model = 'gpt-4o-mini';
37
- // Provide your own OpenAI API key to run chat completions locally
37
+ // Provide your own OpenAI, Anthropic, or Google Gemini API key to run chat completions locally
38
38
  opts.openAIAPIKey = process.env.OPENAI_API_KEY;
39
+ opts.anthropicAPIKey = process.env.ANTHROPIC_API_KEY;
40
+ opts.googleGeminiAPIKey = process.env.GOOGLE_GEMINI_API_KEY;
39
41
 
40
42
  // Mount Mongoose Studio on '/studio'
41
43
  app.use('/studio', await studio('/studio/api', mongoose, opts));
@@ -88,8 +90,10 @@ const opts = {
88
90
  apiKey: process.env.MONGOOSE_STUDIO_API_KEY,
89
91
  // Optionally specify which ChatGPT model to use for chat messages
90
92
  model: 'gpt-4o-mini',
91
- // Provide your own OpenAI API key to run chat completions locally
92
- openAIAPIKey: process.env.OPENAI_API_KEY
93
+ // Provide your own OpenAI, Anthropic, or Google Gemini API key to run chat completions locally
94
+ openAIAPIKey: process.env.OPENAI_API_KEY,
95
+ anthropicAPIKey: process.env.ANTHROPIC_API_KEY,
96
+ googleGeminiAPIKey: process.env.GOOGLE_GEMINI_API_KEY
93
97
  };
94
98
  console.log('Creating Mongoose studio', opts);
95
99
  require('@mongoosejs/studio/frontend')(`/.netlify/functions/studio`, true, opts).then(() => {
@@ -108,7 +112,9 @@ const mongoose = require('mongoose');
108
112
  const handler = require('@mongoosejs/studio/backend/netlify')({
109
113
  apiKey: process.env.MONGOOSE_STUDIO_API_KEY,
110
114
  model: 'gpt-4o-mini',
111
- openAIAPIKey: process.env.OPENAI_API_KEY
115
+ openAIAPIKey: process.env.OPENAI_API_KEY,
116
+ anthropicAPIKey: process.env.ANTHROPIC_API_KEY,
117
+ googleGeminiAPIKey: process.env.GOOGLE_GEMINI_API_KEY
112
118
  }).handler;
113
119
 
114
120
  let conn = null;
@@ -16,16 +16,13 @@ const CreateChatMessageParams = new Archetype({
16
16
  content: {
17
17
  $type: 'string'
18
18
  },
19
- authorization: {
20
- $type: 'string'
21
- },
22
19
  roles: {
23
20
  $type: ['string']
24
21
  }
25
22
  }).compile('CreateChatMessageParams');
26
23
 
27
24
  module.exports = ({ db, studioConnection, options }) => async function createChatMessage(params) {
28
- const { chatThreadId, initiatedById, content, script, authorization, roles } = new CreateChatMessageParams(params);
25
+ const { chatThreadId, initiatedById, content, script, roles } = new CreateChatMessageParams(params);
29
26
  const ChatThread = studioConnection.model('__Studio_ChatThread');
30
27
  const ChatMessage = studioConnection.model('__Studio_ChatMessage');
31
28
 
@@ -54,6 +51,7 @@ module.exports = ({ db, studioConnection, options }) => async function createCha
54
51
  if (chatThread.title == null) {
55
52
  const threadText = messages
56
53
  .filter(m => m.role === 'user' || m.role === 'assistant')
54
+ .concat([{ role: 'user', content }])
57
55
  .map(m => `${m.role.toUpperCase()}: ${m.content}`)
58
56
  .join('\n')
59
57
  .slice(0, 5000);
@@ -5,3 +5,4 @@ exports.createChatThread = require('./createChatThread');
5
5
  exports.getChatThread = require('./getChatThread');
6
6
  exports.listChatThreads = require('./listChatThreads');
7
7
  exports.shareChatThread = require('./shareChatThread');
8
+ exports.streamChatMessage = require('./streamChatMessage');
@@ -0,0 +1,158 @@
1
+ 'use strict';
2
+
3
+ const Archetype = require('archetype');
4
+ const authorize = require('../../authorize');
5
+ const callLLM = require('../../integrations/callLLM');
6
+ const streamLLM = require('../../integrations/streamLLM');
7
+ const getModelDescriptions = require('../../helpers/getModelDescriptions');
8
+ const mongoose = require('mongoose');
9
+
10
+ const CreateChatMessageParams = new Archetype({
11
+ chatThreadId: {
12
+ $type: mongoose.Types.ObjectId
13
+ },
14
+ initiatedById: {
15
+ $type: mongoose.Types.ObjectId
16
+ },
17
+ content: {
18
+ $type: 'string'
19
+ },
20
+ roles: {
21
+ $type: ['string']
22
+ }
23
+ }).compile('CreateChatMessageParams');
24
+
25
+ module.exports = ({ db, studioConnection, options }) => async function* createChatMessage(params) {
26
+ const { chatThreadId, initiatedById, content, script, roles } = new CreateChatMessageParams(params);
27
+ const ChatThread = studioConnection.model('__Studio_ChatThread');
28
+ const ChatMessage = studioConnection.model('__Studio_ChatMessage');
29
+
30
+ await authorize('ChatThread.createChatMessage', roles);
31
+
32
+ // Check that the user owns the thread
33
+ const chatThread = await ChatThread.findOne({ _id: chatThreadId });
34
+ if (!chatThread) {
35
+ throw new Error('Chat thread not found');
36
+ }
37
+ if (initiatedById != null && chatThread.userId.toString() !== initiatedById.toString()) {
38
+ throw new Error('Not authorized');
39
+ }
40
+
41
+ const messages = await ChatMessage.find({ chatThreadId }).sort({ createdAt: 1 });
42
+ const llmMessages = messages.map(m => ({
43
+ role: m.role,
44
+ content: [{
45
+ type: 'text',
46
+ text: m.content
47
+ }]
48
+ }));
49
+ llmMessages.push({ role: 'user', content: [{ type: 'text', text: content }] });
50
+
51
+ let summarizePromise = Promise.resolve();
52
+ if (chatThread.title == null) {
53
+ const threadText = messages
54
+ .filter(m => m.role === 'user' || m.role === 'assistant')
55
+ .concat([{ role: 'user', content }])
56
+ .map(m => `${m.role.toUpperCase()}: ${m.content}`)
57
+ .join('\n')
58
+ .slice(0, 5000);
59
+ summarizePromise = callLLM(
60
+ [{
61
+ role: 'user',
62
+ content: [{
63
+ type: 'text',
64
+ text: 'Summarize the following chat thread into a concise, helpful title (≤ 6 words).\n\n' +
65
+ `${threadText}\n\n` +
66
+ 'Return only the title.'
67
+ }]
68
+ }],
69
+ 'You are a helpful assistant that summarizes chat threads into titles.',
70
+ options
71
+ ).then(res => {
72
+ const title = res.text;
73
+ chatThread.title = title;
74
+ return chatThread.save();
75
+ });
76
+ }
77
+
78
+ const modelDescriptions = getModelDescriptions(db);
79
+ const system = systemPrompt + '\n\n' + modelDescriptions + (options?.context ? '\n\n' + options.context : '');
80
+
81
+ const userChatMessage = await ChatMessage.create({
82
+ chatThreadId,
83
+ role: 'user',
84
+ content,
85
+ script,
86
+ executionResult: null
87
+ });
88
+
89
+ yield { chatMessage: userChatMessage };
90
+
91
+ const assistantChatMessage = new ChatMessage({
92
+ chatThreadId,
93
+ role: 'assistant',
94
+ content: '',
95
+ script: null,
96
+ executionResult: null
97
+ });
98
+ const textStream = streamLLM(llmMessages, system, options);
99
+ let count = 0;
100
+ for await (const textPart of textStream) {
101
+ assistantChatMessage.content += textPart;
102
+ // Only save every 10th chunk for performance
103
+ if (count++ % 10 === 0) {
104
+ await assistantChatMessage.save();
105
+ }
106
+ yield { textPart };
107
+ }
108
+
109
+ await assistantChatMessage.save();
110
+ yield { chatMessage: assistantChatMessage };
111
+
112
+ const updatedChatThread = await summarizePromise;
113
+ if (updatedChatThread != null) {
114
+ yield { chatThread: updatedChatThread };
115
+ }
116
+ return {};
117
+ };
118
+
119
+ const systemPrompt = `
120
+ You are a data querying assistant who writes scripts for users accessing MongoDB data using Node.js and Mongoose.
121
+
122
+ Keep scripts concise. Avoid unnecessary comments, error handling, and temporary variables.
123
+
124
+ Do not write any imports or require() statements, that will cause the script to break.
125
+
126
+ If the user approves the script, the script will run in the Node.js server in a sandboxed vm.createContext() call with only 1 global variable: db, which contains the Mongoose connection. The script return value will then send the response via JSON to the client. Be aware that the result of the query will be serialized to JSON before being displayed to the user. MAKE SURE TO RETURN A VALUE FROM THE SCRIPT.
127
+
128
+ Optimize scripts for readability first, followed by reliability, followed by performance. Avoid using the aggregation framework unless explicitly requested by the user. Use indexed fields in queries where possible.
129
+
130
+ Assume the user has pre-defined schemas and models. Do not define any new schemas or models for the user.
131
+
132
+ Use async/await where possible. Assume top-level await is allowed.
133
+
134
+ Write at most one script, unless the user explicitly asks for multiple scripts.
135
+
136
+ Think carefully about the user's input and identify the models referred to by the user's query.
137
+
138
+ Format output as Markdown, including code fences for any scripts the user requested.
139
+
140
+ Add a brief text description of what the script does.
141
+
142
+ If the user's query is best answered with a chart, return a Chart.js 4 configuration as \`return { $chart: chartJSConfig };\`. Disable ChartJS animation by default unless user asks for it. Set responsive: true, maintainAspectRatio: false options unless the user explicitly asks.
143
+
144
+ If the user\'s query is best answered by a map, return an object { $featureCollection } which contains a GeoJSON FeatureCollection
145
+
146
+ Example output:
147
+
148
+ The following script counts the number of users which are not deleted.
149
+
150
+ \`\`\`javascript
151
+ const users = await db.model('User').find({ isDeleted: false });
152
+ return { numUsers: users.length };
153
+ \`\`\`
154
+
155
+ -----------
156
+
157
+ Here is a description of the user's models. Assume these are the only models available in the system unless explicitly instructed otherwise by the user.
158
+ `.trim();
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const Archetype = require('archetype');
4
- const { EJSON } = require('bson');
4
+ const { EJSON } = require('mongodb/lib/bson');
5
5
  const authorize = require('../../authorize');
6
6
 
7
7
  const CreateDocumentParams = new Archetype({
@@ -1,22 +1,44 @@
1
1
  'use strict';
2
2
 
3
3
  const { createAnthropic } = require('@ai-sdk/anthropic');
4
+ const { createGoogleGenerativeAI } = require('@ai-sdk/google');
4
5
  const { createOpenAI } = require('@ai-sdk/openai');
5
6
  const { generateText } = require('ai');
6
7
 
7
8
  module.exports = async function callLLM(messages, system, options) {
8
9
  let provider = null;
9
10
  let model = null;
10
- if (options?.openAIAPIKey && options?.anthropicAPIKey) {
11
- throw new Error('Cannot set both OpenAI and Anthropic API keys');
12
- }
11
+ const providers = [];
13
12
 
14
13
  if (options?.openAIAPIKey) {
15
- provider = createOpenAI({ apiKey: options.openAIAPIKey });
16
- model = options?.model ?? 'gpt-4o-mini';
17
- } else if (options?.anthropicAPIKey) {
18
- provider = createAnthropic({ apiKey: options.anthropicAPIKey });
19
- model = options?.model ?? 'claude-haiku-4-5-20251001';
14
+ providers.push({
15
+ provider: createOpenAI({ apiKey: options.openAIAPIKey }),
16
+ model: options?.model ?? 'gpt-4o-mini'
17
+ });
18
+ }
19
+
20
+ if (options?.anthropicAPIKey) {
21
+ providers.push({
22
+ provider: createAnthropic({ apiKey: options.anthropicAPIKey }),
23
+ model: options?.model ?? 'claude-haiku-4-5-20251001'
24
+ });
25
+ }
26
+
27
+ if (options?.googleGeminiAPIKey) {
28
+ providers.push({
29
+ provider: createGoogleGenerativeAI({ apiKey: options.googleGeminiAPIKey }),
30
+ model: options?.model ?? 'gemini-2.5-flash'
31
+ });
32
+ }
33
+
34
+ if (providers.length > 1) {
35
+ throw new Error('Cannot set multiple LLM API keys');
36
+ }
37
+
38
+ if (providers.length > 0) {
39
+ const selected = providers[0];
40
+ provider = selected.provider;
41
+ model = selected.model;
20
42
  }
21
43
 
22
44
  if (provider) {
@@ -0,0 +1,85 @@
1
+ 'use strict';
2
+
3
+ const { createAnthropic } = require('@ai-sdk/anthropic');
4
+ const { createGoogleGenerativeAI } = require('@ai-sdk/google');
5
+ const { createOpenAI } = require('@ai-sdk/openai');
6
+ const { streamText } = require('ai');
7
+
8
+ module.exports = async function* streamLLM(messages, system, options) {
9
+ let provider = null;
10
+ let model = null;
11
+ const providers = [];
12
+
13
+ if (options?.openAIAPIKey) {
14
+ providers.push({
15
+ provider: createOpenAI({ apiKey: options.openAIAPIKey }),
16
+ model: options?.model ?? 'gpt-4o-mini'
17
+ });
18
+ }
19
+
20
+ if (options?.anthropicAPIKey) {
21
+ providers.push({
22
+ provider: createAnthropic({ apiKey: options.anthropicAPIKey }),
23
+ model: options?.model ?? 'claude-haiku-4-5-20251001'
24
+ });
25
+ }
26
+
27
+ if (options?.googleGeminiAPIKey) {
28
+ providers.push({
29
+ provider: createGoogleGenerativeAI({ apiKey: options.googleGeminiAPIKey }),
30
+ model: options?.model ?? 'gemini-2.5-flash'
31
+ });
32
+ }
33
+
34
+ if (providers.length > 1) {
35
+ throw new Error('Cannot set multiple LLM API keys');
36
+ }
37
+
38
+ if (providers.length > 0) {
39
+ const selected = providers[0];
40
+ provider = selected.provider;
41
+ model = selected.model;
42
+ }
43
+
44
+ if (provider) {
45
+ let error = null;
46
+ const { textStream } = streamText({
47
+ model: provider(model),
48
+ system,
49
+ messages,
50
+ onError(err) {
51
+ error = err.error;
52
+ }
53
+ });
54
+ for await (const chunk of textStream) {
55
+ yield chunk;
56
+ }
57
+ if (error) {
58
+ throw error;
59
+ }
60
+ return;
61
+ }
62
+
63
+ // If not using OpenAI, Anthropic, or Google Gemini, fallback to Mongoose (no streaming)
64
+ const headers = { 'Content-Type': 'application/json' };
65
+ const response = await fetch('https://mongoose-js.netlify.app/.netlify/functions/getChatCompletion', {
66
+ method: 'POST',
67
+ headers,
68
+ body: JSON.stringify({
69
+ messages,
70
+ model: options?.model
71
+ })
72
+ }).then(response => {
73
+ if (!response.ok) {
74
+ return response.json().then(data => {
75
+ throw new Error(`Mongoose Studio chat completion error: ${data.message}`);
76
+ });
77
+ }
78
+ return response;
79
+ });
80
+
81
+ const data = await response.json();
82
+
83
+ // Simulate streaming by yielding the whole response at once as a single chunk
84
+ yield data.response;
85
+ };
@@ -272,6 +272,13 @@ if (window.MONGOOSE_STUDIO_CONFIG.isLambda) {
272
272
  },
273
273
  shareChatThread(params) {
274
274
  return client.post('', { action: 'ChatThread.shareChatThread', ...params }).then(res => res.data);
275
+ },
276
+ streamChatMessage: async function* streamChatMessage(params) {
277
+ // Don't stream on Next.js or Netlify for now.
278
+ const data = await client.post('', { action: 'ChatThread.createChatMessage', ...params }).then(res => res.data);
279
+ yield { chatMessage: data.chatMessages[0] };
280
+ yield { chatMessage: data.chatMessages[1] };
281
+ yield { chatThread: data.chatThread };
275
282
  }
276
283
  };
277
284
  exports.ChatMessage = {
@@ -388,6 +395,55 @@ if (window.MONGOOSE_STUDIO_CONFIG.isLambda) {
388
395
  },
389
396
  shareChatThread: function shareChatThread(params) {
390
397
  return client.post('/ChatThread/shareChatThread', params).then(res => res.data);
398
+ },
399
+ streamChatMessage: async function* streamChatMessage(params) {
400
+ const accessToken = window.localStorage.getItem('_mongooseStudioAccessToken') || null;
401
+ const url = window.MONGOOSE_STUDIO_CONFIG.baseURL + '/ChatThread/streamChatMessage?' + new URLSearchParams(params).toString();
402
+
403
+ const response = await fetch(url, {
404
+ method: 'GET',
405
+ headers: {
406
+ Authorization: `${accessToken}`,
407
+ Accept: 'text/event-stream'
408
+ }
409
+ });
410
+
411
+ if (!response.ok) {
412
+ throw new Error(`HTTP error! Status: ${response.status}`);
413
+ }
414
+
415
+ const reader = response.body.getReader();
416
+ const decoder = new TextDecoder('utf-8');
417
+ let buffer = '';
418
+
419
+ while (true) {
420
+ const { done, value } = await reader.read();
421
+ if (done) break;
422
+ buffer += decoder.decode(value, { stream: true });
423
+
424
+ let eventEnd;
425
+ while ((eventEnd = buffer.indexOf('\n\n')) !== -1) {
426
+ const eventStr = buffer.slice(0, eventEnd);
427
+ buffer = buffer.slice(eventEnd + 2);
428
+
429
+ // Parse SSE event
430
+ const lines = eventStr.split('\n');
431
+ let data = '';
432
+ for (const line of lines) {
433
+ if (line.startsWith('data:')) {
434
+ data += line.slice(5).trim();
435
+ }
436
+ }
437
+ if (data) {
438
+ try {
439
+ const res = JSON.parse(data);
440
+ yield res;
441
+ } catch (err) {
442
+ yield data;
443
+ }
444
+ }
445
+ }
446
+ }
391
447
  }
392
448
  };
393
449
  exports.ChatMessage = {
@@ -1010,14 +1066,47 @@ module.exports = app => app.component('chat', {
1010
1066
  }
1011
1067
  });
1012
1068
 
1013
- const { chatMessages, chatThread } = await api.ChatThread.createChatMessage({
1014
- chatThreadId: this.chatThreadId,
1015
- content
1016
- });
1017
- this.chatMessages.push(chatMessages[1]);
1018
- for (const thread of this.chatThreads) {
1019
- if (thread._id === chatThread._id) {
1020
- thread.title = chatThread.title;
1069
+ const params = { chatThreadId: this.chatThreadId, content };
1070
+ let userChatMessage = null;
1071
+ let assistantChatMessage = null;
1072
+ for await (const event of api.ChatThread.streamChatMessage(params)) {
1073
+ if (event.chatMessage) {
1074
+ if (!userChatMessage) {
1075
+ userChatMessage = event.chatMessage;
1076
+ } else {
1077
+ const assistantChatMessageIndex = this.chatMessages.indexOf(assistantChatMessage);
1078
+ assistantChatMessage = event.chatMessage;
1079
+ this.chatMessages[assistantChatMessageIndex] = assistantChatMessage;
1080
+ }
1081
+ } else if (event.chatThread) {
1082
+ for (const thread of this.chatThreads) {
1083
+ if (thread._id === event.chatThread._id) {
1084
+ thread.title = event.chatThread.title;
1085
+ }
1086
+ }
1087
+ } else if (event.textPart) {
1088
+ if (!assistantChatMessage) {
1089
+ assistantChatMessage = {
1090
+ content: event.textPart,
1091
+ role: 'assistant'
1092
+ };
1093
+ this.chatMessages.push(assistantChatMessage);
1094
+ assistantChatMessage = this.chatMessages[this.chatMessages.length - 1];
1095
+ this.$nextTick(() => {
1096
+ if (this.$refs.messagesContainer) {
1097
+ this.$refs.messagesContainer.scrollTop = this.$refs.messagesContainer.scrollHeight;
1098
+ }
1099
+ });
1100
+ } else {
1101
+ assistantChatMessage.content += event.textPart;
1102
+ this.$nextTick(() => {
1103
+ if (this.$refs.messagesContainer) {
1104
+ this.$refs.messagesContainer.scrollTop = this.$refs.messagesContainer.scrollHeight;
1105
+ }
1106
+ });
1107
+ }
1108
+ } else if (event.message) {
1109
+ throw new Error(event.message);
1021
1110
  }
1022
1111
  }
1023
1112
 
@@ -1159,7 +1248,7 @@ module.exports = "<div>\n <div class=\"mb-2\">\n <textarea class=\"borde
1159
1248
 
1160
1249
  const api = __webpack_require__(/*! ../api */ "./frontend/src/api.js");
1161
1250
 
1162
- const { BSON, EJSON } = __webpack_require__(/*! bson */ "./node_modules/bson/lib/bson.mjs");
1251
+ const { BSON, EJSON } = __webpack_require__(/*! mongodb/lib/bson */ "./node_modules/mongodb/lib/bson.js");
1163
1252
 
1164
1253
  const ObjectId = new Proxy(BSON.ObjectId, {
1165
1254
  apply(target, thisArg, argumentsList) {
@@ -1203,7 +1292,7 @@ module.exports = app => app.component('clone-document', {
1203
1292
  },
1204
1293
  mounted: function() {
1205
1294
  const pathsToClone = this.schemaPaths.map(x => x.path);
1206
-
1295
+
1207
1296
  // Create a filtered version of the document data
1208
1297
  const filteredDoc = {};
1209
1298
  pathsToClone.forEach(path => {
@@ -1228,6 +1317,7 @@ module.exports = app => app.component('clone-document', {
1228
1317
  }
1229
1318
  });
1230
1319
 
1320
+
1231
1321
  /***/ }),
1232
1322
 
1233
1323
  /***/ "./frontend/src/create-dashboard/create-dashboard.html":
@@ -16849,7 +16939,7 @@ module.exports = function stringToParts(str) {
16849
16939
  /***/ ((module) => {
16850
16940
 
16851
16941
  "use strict";
16852
- module.exports = /*#__PURE__*/JSON.parse('{"name":"@mongoosejs/studio","version":"0.1.10","description":"A sleek, powerful MongoDB UI with built-in dashboarding and auth, seamlessly integrated with your Express, Vercel, or Netlify app.","homepage":"https://studio.mongoosejs.io/","repository":{"type":"git","url":"https://github.com/mongoosejs/studio"},"license":"Apache-2.0","dependencies":{"@ai-sdk/openai":"2.x","@ai-sdk/anthropic":"2.x","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","tailwindcss":"3.4.0","vanillatoasts":"^1.6.0","vue":"3.x","webpack":"5.x"},"peerDependencies":{"mongoose":"7.x || 8.x || ^9.0.0-0"},"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","mongoose":"9.x"},"scripts":{"lint":"eslint .","tailwind":"tailwindcss -o ./frontend/public/tw.css","tailwind:watch":"tailwindcss -o ./frontend/public/tw.css --watch","test":"mocha test/*.test.js"}}');
16942
+ module.exports = /*#__PURE__*/JSON.parse('{"name":"@mongoosejs/studio","version":"0.1.12","description":"A sleek, powerful MongoDB UI with built-in dashboarding and auth, seamlessly integrated with your Express, Vercel, or Netlify app.","homepage":"https://studio.mongoosejs.io/","repository":{"type":"git","url":"https://github.com/mongoosejs/studio"},"license":"Apache-2.0","dependencies":{"@ai-sdk/google":"2.x","@ai-sdk/openai":"2.x","@ai-sdk/anthropic":"2.x","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","tailwindcss":"3.4.0","vanillatoasts":"^1.6.0","vue":"3.x","webpack":"5.x"},"peerDependencies":{"mongoose":"7.x || 8.x || ^9.0.0"},"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","mongoose":"9.x"},"scripts":{"lint":"eslint .","tailwind":"tailwindcss -o ./frontend/public/tw.css","tailwind:watch":"tailwindcss -o ./frontend/public/tw.css --watch","test":"mocha test/*.test.js"}}');
16853
16943
 
16854
16944
  /***/ })
16855
16945
 
@@ -61,6 +61,13 @@ if (window.MONGOOSE_STUDIO_CONFIG.isLambda) {
61
61
  },
62
62
  shareChatThread(params) {
63
63
  return client.post('', { action: 'ChatThread.shareChatThread', ...params }).then(res => res.data);
64
+ },
65
+ streamChatMessage: async function* streamChatMessage(params) {
66
+ // Don't stream on Next.js or Netlify for now.
67
+ const data = await client.post('', { action: 'ChatThread.createChatMessage', ...params }).then(res => res.data);
68
+ yield { chatMessage: data.chatMessages[0] };
69
+ yield { chatMessage: data.chatMessages[1] };
70
+ yield { chatThread: data.chatThread };
64
71
  }
65
72
  };
66
73
  exports.ChatMessage = {
@@ -177,6 +184,55 @@ if (window.MONGOOSE_STUDIO_CONFIG.isLambda) {
177
184
  },
178
185
  shareChatThread: function shareChatThread(params) {
179
186
  return client.post('/ChatThread/shareChatThread', params).then(res => res.data);
187
+ },
188
+ streamChatMessage: async function* streamChatMessage(params) {
189
+ const accessToken = window.localStorage.getItem('_mongooseStudioAccessToken') || null;
190
+ const url = window.MONGOOSE_STUDIO_CONFIG.baseURL + '/ChatThread/streamChatMessage?' + new URLSearchParams(params).toString();
191
+
192
+ const response = await fetch(url, {
193
+ method: 'GET',
194
+ headers: {
195
+ Authorization: `${accessToken}`,
196
+ Accept: 'text/event-stream'
197
+ }
198
+ });
199
+
200
+ if (!response.ok) {
201
+ throw new Error(`HTTP error! Status: ${response.status}`);
202
+ }
203
+
204
+ const reader = response.body.getReader();
205
+ const decoder = new TextDecoder('utf-8');
206
+ let buffer = '';
207
+
208
+ while (true) {
209
+ const { done, value } = await reader.read();
210
+ if (done) break;
211
+ buffer += decoder.decode(value, { stream: true });
212
+
213
+ let eventEnd;
214
+ while ((eventEnd = buffer.indexOf('\n\n')) !== -1) {
215
+ const eventStr = buffer.slice(0, eventEnd);
216
+ buffer = buffer.slice(eventEnd + 2);
217
+
218
+ // Parse SSE event
219
+ const lines = eventStr.split('\n');
220
+ let data = '';
221
+ for (const line of lines) {
222
+ if (line.startsWith('data:')) {
223
+ data += line.slice(5).trim();
224
+ }
225
+ }
226
+ if (data) {
227
+ try {
228
+ const res = JSON.parse(data);
229
+ yield res;
230
+ } catch (err) {
231
+ yield data;
232
+ }
233
+ }
234
+ }
235
+ }
180
236
  }
181
237
  };
182
238
  exports.ChatMessage = {
@@ -41,14 +41,47 @@ module.exports = app => app.component('chat', {
41
41
  }
42
42
  });
43
43
 
44
- const { chatMessages, chatThread } = await api.ChatThread.createChatMessage({
45
- chatThreadId: this.chatThreadId,
46
- content
47
- });
48
- this.chatMessages.push(chatMessages[1]);
49
- for (const thread of this.chatThreads) {
50
- if (thread._id === chatThread._id) {
51
- thread.title = chatThread.title;
44
+ const params = { chatThreadId: this.chatThreadId, content };
45
+ let userChatMessage = null;
46
+ let assistantChatMessage = null;
47
+ for await (const event of api.ChatThread.streamChatMessage(params)) {
48
+ if (event.chatMessage) {
49
+ if (!userChatMessage) {
50
+ userChatMessage = event.chatMessage;
51
+ } else {
52
+ const assistantChatMessageIndex = this.chatMessages.indexOf(assistantChatMessage);
53
+ assistantChatMessage = event.chatMessage;
54
+ this.chatMessages[assistantChatMessageIndex] = assistantChatMessage;
55
+ }
56
+ } else if (event.chatThread) {
57
+ for (const thread of this.chatThreads) {
58
+ if (thread._id === event.chatThread._id) {
59
+ thread.title = event.chatThread.title;
60
+ }
61
+ }
62
+ } else if (event.textPart) {
63
+ if (!assistantChatMessage) {
64
+ assistantChatMessage = {
65
+ content: event.textPart,
66
+ role: 'assistant'
67
+ };
68
+ this.chatMessages.push(assistantChatMessage);
69
+ assistantChatMessage = this.chatMessages[this.chatMessages.length - 1];
70
+ this.$nextTick(() => {
71
+ if (this.$refs.messagesContainer) {
72
+ this.$refs.messagesContainer.scrollTop = this.$refs.messagesContainer.scrollHeight;
73
+ }
74
+ });
75
+ } else {
76
+ assistantChatMessage.content += event.textPart;
77
+ this.$nextTick(() => {
78
+ if (this.$refs.messagesContainer) {
79
+ this.$refs.messagesContainer.scrollTop = this.$refs.messagesContainer.scrollHeight;
80
+ }
81
+ });
82
+ }
83
+ } else if (event.message) {
84
+ throw new Error(event.message);
52
85
  }
53
86
  }
54
87
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  const api = require('../api');
4
4
 
5
- const { BSON, EJSON } = require('bson');
5
+ const { BSON, EJSON } = require('mongodb/lib/bson');
6
6
 
7
7
  const ObjectId = new Proxy(BSON.ObjectId, {
8
8
  apply(target, thisArg, argumentsList) {
@@ -46,7 +46,7 @@ module.exports = app => app.component('clone-document', {
46
46
  },
47
47
  mounted: function() {
48
48
  const pathsToClone = this.schemaPaths.map(x => x.path);
49
-
49
+
50
50
  // Create a filtered version of the document data
51
51
  const filteredDoc = {};
52
52
  pathsToClone.forEach(path => {
@@ -69,4 +69,4 @@ module.exports = app => app.component('clone-document', {
69
69
  smartIndent: false
70
70
  });
71
71
  }
72
- });
72
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mongoosejs/studio",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
4
4
  "description": "A sleek, powerful MongoDB UI with built-in dashboarding and auth, seamlessly integrated with your Express, Vercel, or Netlify app.",
5
5
  "homepage": "https://studio.mongoosejs.io/",
6
6
  "repository": {
@@ -9,6 +9,7 @@
9
9
  },
10
10
  "license": "Apache-2.0",
11
11
  "dependencies": {
12
+ "@ai-sdk/google": "2.x",
12
13
  "@ai-sdk/openai": "2.x",
13
14
  "@ai-sdk/anthropic": "2.x",
14
15
  "ai": "5.x",
@@ -24,7 +25,7 @@
24
25
  "webpack": "5.x"
25
26
  },
26
27
  "peerDependencies": {
27
- "mongoose": "7.x || 8.x || ^9.0.0-0"
28
+ "mongoose": "7.x || 8.x || ^9.0.0"
28
29
  },
29
30
  "devDependencies": {
30
31
  "@masteringjs/eslint-config": "0.1.1",