@mongoosejs/studio 0.1.9 → 0.1.11
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/backend/actions/ChatThread/createChatMessage.js +2 -4
- package/backend/actions/ChatThread/index.js +1 -0
- package/backend/actions/ChatThread/streamChatMessage.js +158 -0
- package/backend/actions/Model/createDocument.js +1 -1
- package/backend/integrations/streamLLM.js +56 -0
- package/frontend/public/app.js +105 -14
- package/frontend/public/tw.css +4 -0
- package/frontend/src/api.js +56 -0
- package/frontend/src/chat/chat-message-script/chat-message-script.html +4 -0
- package/frontend/src/chat/chat.js +39 -8
- package/frontend/src/clone-document/clone-document.js +3 -3
- package/frontend/src/dashboard-result/dashboard-map/dashboard-map.html +3 -3
- package/frontend/src/dashboard-result/dashboard-map/dashboard-map.js +4 -1
- package/package.json +2 -2
|
@@ -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,
|
|
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();
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { createAnthropic } = require('@ai-sdk/anthropic');
|
|
4
|
+
const { createOpenAI } = require('@ai-sdk/openai');
|
|
5
|
+
const { streamText } = require('ai');
|
|
6
|
+
|
|
7
|
+
module.exports = async function* streamLLM(messages, system, options) {
|
|
8
|
+
let provider = null;
|
|
9
|
+
let model = null;
|
|
10
|
+
if (options?.openAIAPIKey && options?.anthropicAPIKey) {
|
|
11
|
+
throw new Error('Cannot set both OpenAI and Anthropic API keys');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
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';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (provider) {
|
|
23
|
+
const { textStream } = streamText({
|
|
24
|
+
model: provider(model),
|
|
25
|
+
system,
|
|
26
|
+
messages
|
|
27
|
+
});
|
|
28
|
+
for await (const chunk of textStream) {
|
|
29
|
+
yield chunk;
|
|
30
|
+
}
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// If not using OpenAI or Anthropic, fallback to Mongoose (no streaming)
|
|
35
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
36
|
+
const response = await fetch('https://mongoose-js.netlify.app/.netlify/functions/getChatCompletion', {
|
|
37
|
+
method: 'POST',
|
|
38
|
+
headers,
|
|
39
|
+
body: JSON.stringify({
|
|
40
|
+
messages,
|
|
41
|
+
model: options?.model
|
|
42
|
+
})
|
|
43
|
+
}).then(response => {
|
|
44
|
+
if (!response.ok) {
|
|
45
|
+
return response.json().then(data => {
|
|
46
|
+
throw new Error(`Mongoose Studio chat completion error: ${data.message}`);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
return response;
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const data = await response.json();
|
|
53
|
+
|
|
54
|
+
// Simulate streaming by yielding the whole response at once as a single chunk
|
|
55
|
+
yield data.response;
|
|
56
|
+
};
|
package/frontend/public/app.js
CHANGED
|
@@ -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
|
+
yield JSON.parse(data);
|
|
440
|
+
} catch (err) {
|
|
441
|
+
// If not JSON, yield as string
|
|
442
|
+
yield data;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
391
447
|
}
|
|
392
448
|
};
|
|
393
449
|
exports.ChatMessage = {
|
|
@@ -612,7 +668,7 @@ module.exports = app => app.component('async-button', {
|
|
|
612
668
|
/***/ ((module) => {
|
|
613
669
|
|
|
614
670
|
"use strict";
|
|
615
|
-
module.exports = "<div class=\"relative border rounded bg-gray-100 text-black text-sm overflow-hidden\">\n <div class=\"flex border-b pt-[1px] text-xs font-medium bg-gray-200\">\n <button\n class=\"px-3 py-1 border-r border-gray-300 hover:bg-green-300\"\n :class=\"{'bg-gray-300': activeTab === 'code', 'bg-green-300': activeTab === 'code'}\"\n @click=\"activeTab = 'code'\">\n Code\n </button>\n <button\n class=\"px-3 py-1 hover:bg-green-300\"\n :class=\"{'bg-green-300': activeTab === 'output'}\"\n @click=\"activeTab = 'output'\">\n Output\n </button>\n <div class=\"ml-auto mr-1 flex\">\n <button\n v-if=\"activeTab === 'output'\"\n class=\"px-2 py-1 mr-1 text-xs bg-gray-500 text-white border-none rounded cursor-pointer hover:bg-gray-600 transition-colors flex items-center\"\n @click=\"copyOutput\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"h-3 w-3\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3\" />\n </svg>\n </button>\n <button\n v-if=\"activeTab === 'output'\"\n class=\"px-2 py-1 mr-1 text-xs bg-blue-500 text-white border-none rounded cursor-pointer hover:bg-blue-600 transition-colors flex items-center\"\n @click=\"openDetailModal\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"h-3 w-3\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 1v4m0 0h-4m4 0l-5-5\" />\n </svg>\n </button>\n <button\n v-if=\"activeTab === 'code' && !isEditing\"\n class=\"px-2 py-1 mr-1 text-xs bg-gray-500 text-white border-none rounded cursor-pointer hover:bg-gray-600 transition-colors flex items-center\"\n @click.stop=\"startEditing\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"h-3 w-3\" viewBox=\"0 0 20 20\" fill=\"currentColor\">\n <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\" />\n </svg>\n </button>\n <async-button\n v-if=\"!isEditing\"\n class=\"px-2 py-1 text-xs bg-green-500 text-white border-none rounded cursor-pointer hover:bg-green-600 transition-colors disabled:bg-gray-400\"\n @click=\"executeScript\">\n Execute\n </async-button>\n <div class=\"relative ml-1\" ref=\"dropdown\">\n <button\n @click.stop=\"toggleDropdown\"\n class=\"px-1 py-1 text-xs hover:bg-gray-300 rounded flex items-center\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"h-4 w-4\" viewBox=\"0 0 20 20\" fill=\"currentColor\">\n <path d=\"M10 6a2 2 0 110-4 2 2 0 010 4zm0 6a2 2 0 110-4 2 2 0 010 4zm0 6a2 2 0 110-4 2 2 0 010 4z\" />\n </svg>\n </button>\n <div\n v-if=\"showDropdown\"\n class=\"absolute right-0 z-10 mt-1 w-64 origin-top-right rounded-md bg-white py-1 shadow-lg ring-1 ring-black/5\">\n <button\n class=\"block w-full text-left px-4 py-2 text-xs text-gray-700 hover:bg-gray-100\"\n @click=\"openCreateDashboardModal(); showDropdown = false\">\n Create Dashboard\n </button>\n <button\n class=\"block w-full text-left px-4 py-2 text-xs text-gray-700 hover:bg-gray-100\"\n @click=\"$emit('copyMessage'); showDropdown = false\">\n Copy Full Message\n </button>\n </div>\n </div>\n </div>\n </div>\n\n <div class=\"p-3 max-h-[50vh] max-w-[calc(100vw-4rem)] lg:max-w-[calc(100vw-20rem)] overflow-y-auto\" v-show=\"activeTab === 'code'\">\n <div v-if=\"isEditing\" class=\"flex flex-col space-y-2\">\n <div class=\"border border-gray-200\">\n <textarea ref=\"scriptEditor\" class=\"w-full h-[45vh]\" @input=\"handleScriptInput\"></textarea>\n </div>\n <div class=\"flex justify-end gap-2\">\n <button\n class=\"px-2 py-1 text-xs bg-gray-300 text-gray-800 border-none rounded cursor-pointer hover:bg-gray-400 transition-colors\"\n @click.stop=\"cancelEditing\">\n Cancel\n </button>\n <async-button\n class=\"px-2 py-1 text-xs bg-green-500 text-white border-none rounded cursor-pointer hover:bg-green-600 transition-colors disabled:bg-gray-400\"\n @click=\"executeScript\">\n Execute\n </async-button>\n </div>\n </div>\n <pre v-else class=\"whitespace-pre-wrap\"><code v-text=\"script\" ref=\"code\" :class=\"'language-' + language\"></code></pre>\n </div>\n\n <div class=\"p-3 whitespace-pre-wrap max-h-[50vh] overflow-y-auto bg-white border-t max-w-[calc(100vw-4rem)] lg:max-w-[calc(100vw-20rem)] relative\" v-show=\"activeTab === 'output'\">\n <dashboard-chart v-if=\"message.executionResult?.output?.$chart\" :value=\"message.executionResult?.output\" />\n <dashboard-map v-else-if=\"message.executionResult?.output?.$featureCollection\" :value=\"message.executionResult?.output\" />\n <pre v-else>{{ message.executionResult?.output || 'No output' }}</pre>\n\n <div v-if=\"message.executionResult?.logs?.length\" class=\"mt-3 pt-3 border-t border-gray-200\">\n <div class=\"text-xs font-semibold text-gray-600 uppercase tracking-wide\">Console</div>\n <pre class=\"mt-1 bg-gray-100 text-gray-900 p-3 rounded whitespace-pre-wrap overflow-x-auto max-h-[280px]\">{{ message.executionResult.logs }}</pre>\n </div>\n </div>\n\n <modal ref=\"outputModal\" v-if=\"showDetailModal\" containerClass=\"!h-[90vh] !w-[90vw]\">\n <template #body>\n <div class=\"absolute font-mono right-1 top-1 cursor-pointer text-xl\" @click=\"showDetailModal = false;\">×</div>\n <div class=\"h-full overflow-auto\">\n <dashboard-chart v-if=\"message.executionResult?.output?.$chart\" :value=\"message.executionResult?.output\" :responsive=\"true\" />\n <pre v-else class=\"whitespace-pre-wrap\">{{ message.executionResult?.output || 'No output' }}</pre>\n </div>\n </template>\n </modal>\n <modal v-if=\"showCreateDashboardModal\">\n <template #body>\n <div class=\"modal-exit\" @click=\"showCreateDashboardModal = false\">×</div>\n <div>\n <div class=\"mt-4 text-gray-900 font-semibold\">Create Dashboard</div>\n <div class=\"mt-4\">\n <label class=\"block text-sm font-medium leading-6 text-gray-900\">Title</label>\n <div class=\"mt-2\">\n <div class=\"w-full flex rounded-md shadow-sm ring-1 ring-inset ring-gray-300 focus-within:ring-2 focus-within:ring-inset focus-within:ring-teal-600\">\n <input type=\"text\" v-model=\"newDashboardTitle\" class=\"outline-none block flex-1 border-0 bg-transparent py-1.5 pl-1 text-gray-900 placeholder:text-gray-400 focus:ring-0 sm:text-sm sm:leading-6\" placeholder=\"My Dashboard\">\n </div>\n </div>\n </div>\n <div class=\"my-4\">\n <label class=\"block text-sm font-medium leading-6 text-gray-900\">Code</label>\n <div class=\"border border-gray-200\">\n <textarea class=\"p-2 h-[300px] w-full\" ref=\"dashboardCodeEditor\"></textarea>\n </div>\n </div>\n <async-button\n @click=\"createDashboardFromScript\"\n class=\"rounded-md bg-teal-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-teal-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-teal-600\">\n Submit\n </async-button>\n <div v-if=\"createErrors.length > 0\" class=\"rounded-md bg-red-50 p-4 mt-1\">\n <div class=\"flex\">\n <div class=\"flex-shrink-0\">\n <svg class=\"h-5 w-5 text-red-400\" viewBox=\"0 0 20 20\" fill=\"currentColor\" aria-hidden=\"true\">\n <path fill-rule=\"evenodd\" d=\"M10 18a8 8 0 100-16 8 8 0 000 16zM8.28 7.22a.75.75 0 00-1.06 1.06L8.94 10l-1.72 1.72a.75.75 0 101.06 1.06L10 11.06l1.72 1.72a.75.75 0 101.06-1.06L11.06 10l1.72-1.72a.75.75 0 00-1.06-1.06L10 8.94 8.28 7.22z\" clip-rule=\"evenodd\" />\n </svg>\n </div>\n <div class=\"ml-3\">\n <h3 class=\"text-sm font-medium text-red-800\">Error</h3>\n <div class=\"mt-2 text-sm text-red-700\">\n {{createError}}\n </div>\n </div>\n </div>\n </div>\n </div>\n </template>\n </modal>\n</div>\n";
|
|
671
|
+
module.exports = "<div class=\"relative border rounded bg-gray-100 text-black text-sm overflow-hidden\">\n <div class=\"flex border-b pt-[1px] text-xs font-medium bg-gray-200\">\n <button\n class=\"px-3 py-1 border-r border-gray-300 hover:bg-green-300\"\n :class=\"{'bg-gray-300': activeTab === 'code', 'bg-green-300': activeTab === 'code'}\"\n @click=\"activeTab = 'code'\">\n Code\n </button>\n <button\n class=\"px-3 py-1 hover:bg-green-300\"\n :class=\"{'bg-green-300': activeTab === 'output'}\"\n @click=\"activeTab = 'output'\">\n Output\n </button>\n <div class=\"ml-auto mr-1 flex\">\n <button\n v-if=\"activeTab === 'output'\"\n class=\"px-2 py-1 mr-1 text-xs bg-gray-500 text-white border-none rounded cursor-pointer hover:bg-gray-600 transition-colors flex items-center\"\n @click=\"copyOutput\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"h-3 w-3\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3\" />\n </svg>\n </button>\n <button\n v-if=\"activeTab === 'output'\"\n class=\"px-2 py-1 mr-1 text-xs bg-blue-500 text-white border-none rounded cursor-pointer hover:bg-blue-600 transition-colors flex items-center\"\n @click=\"openDetailModal\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"h-3 w-3\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 1v4m0 0h-4m4 0l-5-5\" />\n </svg>\n </button>\n <button\n v-if=\"activeTab === 'code' && !isEditing\"\n class=\"px-2 py-1 mr-1 text-xs bg-gray-500 text-white border-none rounded cursor-pointer hover:bg-gray-600 transition-colors flex items-center\"\n @click.stop=\"startEditing\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"h-3 w-3\" viewBox=\"0 0 20 20\" fill=\"currentColor\">\n <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\" />\n </svg>\n </button>\n <async-button\n v-if=\"!isEditing\"\n class=\"px-2 py-1 text-xs bg-green-500 text-white border-none rounded cursor-pointer hover:bg-green-600 transition-colors disabled:bg-gray-400\"\n @click=\"executeScript\">\n Execute\n </async-button>\n <div class=\"relative ml-1\" ref=\"dropdown\">\n <button\n @click.stop=\"toggleDropdown\"\n class=\"px-1 py-1 text-xs hover:bg-gray-300 rounded flex items-center\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"h-4 w-4\" viewBox=\"0 0 20 20\" fill=\"currentColor\">\n <path d=\"M10 6a2 2 0 110-4 2 2 0 010 4zm0 6a2 2 0 110-4 2 2 0 010 4zm0 6a2 2 0 110-4 2 2 0 010 4z\" />\n </svg>\n </button>\n <div\n v-if=\"showDropdown\"\n class=\"absolute right-0 z-10 mt-1 w-64 origin-top-right rounded-md bg-white py-1 shadow-lg ring-1 ring-black/5\">\n <button\n class=\"block w-full text-left px-4 py-2 text-xs text-gray-700 hover:bg-gray-100\"\n @click=\"openCreateDashboardModal(); showDropdown = false\">\n Create Dashboard\n </button>\n <button\n class=\"block w-full text-left px-4 py-2 text-xs text-gray-700 hover:bg-gray-100\"\n @click=\"$emit('copyMessage'); showDropdown = false\">\n Copy Full Message\n </button>\n </div>\n </div>\n </div>\n </div>\n\n <div class=\"p-3 max-h-[50vh] max-w-[calc(100vw-4rem)] lg:max-w-[calc(100vw-20rem)] overflow-y-auto\" v-show=\"activeTab === 'code'\">\n <div v-if=\"isEditing\" class=\"flex flex-col space-y-2\">\n <div class=\"border border-gray-200\">\n <textarea ref=\"scriptEditor\" class=\"w-full h-[45vh]\" @input=\"handleScriptInput\"></textarea>\n </div>\n <div class=\"flex justify-end gap-2\">\n <button\n class=\"px-2 py-1 text-xs bg-gray-300 text-gray-800 border-none rounded cursor-pointer hover:bg-gray-400 transition-colors\"\n @click.stop=\"cancelEditing\">\n Cancel\n </button>\n <async-button\n class=\"px-2 py-1 text-xs bg-green-500 text-white border-none rounded cursor-pointer hover:bg-green-600 transition-colors disabled:bg-gray-400\"\n @click=\"executeScript\">\n Execute\n </async-button>\n </div>\n </div>\n <pre v-else class=\"whitespace-pre-wrap\"><code v-text=\"script\" ref=\"code\" :class=\"'language-' + language\"></code></pre>\n </div>\n\n <div class=\"p-3 whitespace-pre-wrap max-h-[50vh] overflow-y-auto bg-white border-t max-w-[calc(100vw-4rem)] lg:max-w-[calc(100vw-20rem)] relative\" v-show=\"activeTab === 'output'\">\n <dashboard-chart v-if=\"message.executionResult?.output?.$chart\" :value=\"message.executionResult?.output\" />\n <dashboard-map v-else-if=\"message.executionResult?.output?.$featureCollection\" :value=\"message.executionResult?.output\" />\n <pre v-else>{{ message.executionResult?.output || 'No output' }}</pre>\n\n <div v-if=\"message.executionResult?.logs?.length\" class=\"mt-3 pt-3 border-t border-gray-200\">\n <div class=\"text-xs font-semibold text-gray-600 uppercase tracking-wide\">Console</div>\n <pre class=\"mt-1 bg-gray-100 text-gray-900 p-3 rounded whitespace-pre-wrap overflow-x-auto max-h-[280px]\">{{ message.executionResult.logs }}</pre>\n </div>\n </div>\n\n <modal ref=\"outputModal\" v-if=\"showDetailModal\" containerClass=\"!h-[90vh] !w-[90vw]\">\n <template #body>\n <div class=\"absolute font-mono right-1 top-1 cursor-pointer text-xl\" @click=\"showDetailModal = false;\">×</div>\n <div class=\"h-full overflow-auto\">\n <dashboard-chart v-if=\"message.executionResult?.output?.$chart\" :value=\"message.executionResult?.output\" :responsive=\"true\" />\n <dashboard-map\n v-else-if=\"message.executionResult?.output?.$featureCollection\"\n :value=\"message.executionResult?.output\"\n height=\"80vh\" />\n <pre v-else class=\"whitespace-pre-wrap\">{{ message.executionResult?.output || 'No output' }}</pre>\n </div>\n </template>\n </modal>\n <modal v-if=\"showCreateDashboardModal\">\n <template #body>\n <div class=\"modal-exit\" @click=\"showCreateDashboardModal = false\">×</div>\n <div>\n <div class=\"mt-4 text-gray-900 font-semibold\">Create Dashboard</div>\n <div class=\"mt-4\">\n <label class=\"block text-sm font-medium leading-6 text-gray-900\">Title</label>\n <div class=\"mt-2\">\n <div class=\"w-full flex rounded-md shadow-sm ring-1 ring-inset ring-gray-300 focus-within:ring-2 focus-within:ring-inset focus-within:ring-teal-600\">\n <input type=\"text\" v-model=\"newDashboardTitle\" class=\"outline-none block flex-1 border-0 bg-transparent py-1.5 pl-1 text-gray-900 placeholder:text-gray-400 focus:ring-0 sm:text-sm sm:leading-6\" placeholder=\"My Dashboard\">\n </div>\n </div>\n </div>\n <div class=\"my-4\">\n <label class=\"block text-sm font-medium leading-6 text-gray-900\">Code</label>\n <div class=\"border border-gray-200\">\n <textarea class=\"p-2 h-[300px] w-full\" ref=\"dashboardCodeEditor\"></textarea>\n </div>\n </div>\n <async-button\n @click=\"createDashboardFromScript\"\n class=\"rounded-md bg-teal-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-teal-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-teal-600\">\n Submit\n </async-button>\n <div v-if=\"createErrors.length > 0\" class=\"rounded-md bg-red-50 p-4 mt-1\">\n <div class=\"flex\">\n <div class=\"flex-shrink-0\">\n <svg class=\"h-5 w-5 text-red-400\" viewBox=\"0 0 20 20\" fill=\"currentColor\" aria-hidden=\"true\">\n <path fill-rule=\"evenodd\" d=\"M10 18a8 8 0 100-16 8 8 0 000 16zM8.28 7.22a.75.75 0 00-1.06 1.06L8.94 10l-1.72 1.72a.75.75 0 101.06 1.06L10 11.06l1.72 1.72a.75.75 0 101.06-1.06L11.06 10l1.72-1.72a.75.75 0 00-1.06-1.06L10 8.94 8.28 7.22z\" clip-rule=\"evenodd\" />\n </svg>\n </div>\n <div class=\"ml-3\">\n <h3 class=\"text-sm font-medium text-red-800\">Error</h3>\n <div class=\"mt-2 text-sm text-red-700\">\n {{createError}}\n </div>\n </div>\n </div>\n </div>\n </div>\n </template>\n </modal>\n</div>\n";
|
|
616
672
|
|
|
617
673
|
/***/ }),
|
|
618
674
|
|
|
@@ -1010,14 +1066,45 @@ module.exports = app => app.component('chat', {
|
|
|
1010
1066
|
}
|
|
1011
1067
|
});
|
|
1012
1068
|
|
|
1013
|
-
const {
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
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
|
+
}
|
|
1021
1108
|
}
|
|
1022
1109
|
}
|
|
1023
1110
|
|
|
@@ -1159,7 +1246,7 @@ module.exports = "<div>\n <div class=\"mb-2\">\n <textarea class=\"borde
|
|
|
1159
1246
|
|
|
1160
1247
|
const api = __webpack_require__(/*! ../api */ "./frontend/src/api.js");
|
|
1161
1248
|
|
|
1162
|
-
const { BSON, EJSON } = __webpack_require__(/*! bson */ "./node_modules/
|
|
1249
|
+
const { BSON, EJSON } = __webpack_require__(/*! mongodb/lib/bson */ "./node_modules/mongodb/lib/bson.js");
|
|
1163
1250
|
|
|
1164
1251
|
const ObjectId = new Proxy(BSON.ObjectId, {
|
|
1165
1252
|
apply(target, thisArg, argumentsList) {
|
|
@@ -1203,7 +1290,7 @@ module.exports = app => app.component('clone-document', {
|
|
|
1203
1290
|
},
|
|
1204
1291
|
mounted: function() {
|
|
1205
1292
|
const pathsToClone = this.schemaPaths.map(x => x.path);
|
|
1206
|
-
|
|
1293
|
+
|
|
1207
1294
|
// Create a filtered version of the document data
|
|
1208
1295
|
const filteredDoc = {};
|
|
1209
1296
|
pathsToClone.forEach(path => {
|
|
@@ -1228,6 +1315,7 @@ module.exports = app => app.component('clone-document', {
|
|
|
1228
1315
|
}
|
|
1229
1316
|
});
|
|
1230
1317
|
|
|
1318
|
+
|
|
1231
1319
|
/***/ }),
|
|
1232
1320
|
|
|
1233
1321
|
/***/ "./frontend/src/create-dashboard/create-dashboard.html":
|
|
@@ -1547,7 +1635,7 @@ module.exports = app => app.component('dashboard-grid', {
|
|
|
1547
1635
|
/***/ ((module) => {
|
|
1548
1636
|
|
|
1549
1637
|
"use strict";
|
|
1550
|
-
module.exports = "<div class=\"py-2\">\n <div v-if=\"header\" class=\"border-b border-gray-100 px-2 pb-2 text-xl font-bold\">\n {{header}}\n </div>\n <div class=\"text-xl\">\n <div ref=\"map\" class=\"w-full\" style=\"
|
|
1638
|
+
module.exports = "<div class=\"py-2 h-full flex flex-col\">\n <div v-if=\"header\" class=\"border-b border-gray-100 px-2 pb-2 text-xl font-bold\">\n {{header}}\n </div>\n <div class=\"text-xl flex-1 min-h-[200px]\">\n <div ref=\"map\" class=\"w-full h-full\" :style=\"mapStyle\"></div>\n </div>\n</div>\n";
|
|
1551
1639
|
|
|
1552
1640
|
/***/ }),
|
|
1553
1641
|
|
|
@@ -1565,7 +1653,7 @@ const template = __webpack_require__(/*! ./dashboard-map.html */ "./frontend/src
|
|
|
1565
1653
|
|
|
1566
1654
|
module.exports = app => app.component('dashboard-map', {
|
|
1567
1655
|
template: template,
|
|
1568
|
-
props: ['value'],
|
|
1656
|
+
props: ['value', 'height'],
|
|
1569
1657
|
mounted() {
|
|
1570
1658
|
const fc = this.value.$featureCollection.featureCollection || this.value.$featureCollection;
|
|
1571
1659
|
const map = L.map(this.$refs.map).setView([0, 0], 1);
|
|
@@ -1583,6 +1671,9 @@ module.exports = app => app.component('dashboard-map', {
|
|
|
1583
1671
|
});
|
|
1584
1672
|
},
|
|
1585
1673
|
computed: {
|
|
1674
|
+
mapStyle() {
|
|
1675
|
+
return { height: this.height || '300px' };
|
|
1676
|
+
},
|
|
1586
1677
|
header() {
|
|
1587
1678
|
if (this.value != null && this.value.$featureCollection.header) {
|
|
1588
1679
|
return this.value.$featureCollection.header;
|
|
@@ -16846,7 +16937,7 @@ module.exports = function stringToParts(str) {
|
|
|
16846
16937
|
/***/ ((module) => {
|
|
16847
16938
|
|
|
16848
16939
|
"use strict";
|
|
16849
|
-
module.exports = /*#__PURE__*/JSON.parse('{"name":"@mongoosejs/studio","version":"0.1.
|
|
16940
|
+
module.exports = /*#__PURE__*/JSON.parse('{"name":"@mongoosejs/studio","version":"0.1.11","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"},"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"}}');
|
|
16850
16941
|
|
|
16851
16942
|
/***/ })
|
|
16852
16943
|
|
package/frontend/public/tw.css
CHANGED
package/frontend/src/api.js
CHANGED
|
@@ -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
|
+
yield JSON.parse(data);
|
|
229
|
+
} catch (err) {
|
|
230
|
+
// If not JSON, yield as string
|
|
231
|
+
yield data;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
180
236
|
}
|
|
181
237
|
};
|
|
182
238
|
exports.ChatMessage = {
|
|
@@ -106,6 +106,10 @@
|
|
|
106
106
|
<div class="absolute font-mono right-1 top-1 cursor-pointer text-xl" @click="showDetailModal = false;">×</div>
|
|
107
107
|
<div class="h-full overflow-auto">
|
|
108
108
|
<dashboard-chart v-if="message.executionResult?.output?.$chart" :value="message.executionResult?.output" :responsive="true" />
|
|
109
|
+
<dashboard-map
|
|
110
|
+
v-else-if="message.executionResult?.output?.$featureCollection"
|
|
111
|
+
:value="message.executionResult?.output"
|
|
112
|
+
height="80vh" />
|
|
109
113
|
<pre v-else class="whitespace-pre-wrap">{{ message.executionResult?.output || 'No output' }}</pre>
|
|
110
114
|
</div>
|
|
111
115
|
</template>
|
|
@@ -41,14 +41,45 @@ module.exports = app => app.component('chat', {
|
|
|
41
41
|
}
|
|
42
42
|
});
|
|
43
43
|
|
|
44
|
-
const {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
+
}
|
|
52
83
|
}
|
|
53
84
|
}
|
|
54
85
|
|
|
@@ -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
|
+
});
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
<div class="py-2">
|
|
1
|
+
<div class="py-2 h-full flex flex-col">
|
|
2
2
|
<div v-if="header" class="border-b border-gray-100 px-2 pb-2 text-xl font-bold">
|
|
3
3
|
{{header}}
|
|
4
4
|
</div>
|
|
5
|
-
<div class="text-xl">
|
|
6
|
-
<div ref="map" class="w-full" style="
|
|
5
|
+
<div class="text-xl flex-1 min-h-[200px]">
|
|
6
|
+
<div ref="map" class="w-full h-full" :style="mapStyle"></div>
|
|
7
7
|
</div>
|
|
8
8
|
</div>
|
|
@@ -5,7 +5,7 @@ const template = require('./dashboard-map.html');
|
|
|
5
5
|
|
|
6
6
|
module.exports = app => app.component('dashboard-map', {
|
|
7
7
|
template: template,
|
|
8
|
-
props: ['value'],
|
|
8
|
+
props: ['value', 'height'],
|
|
9
9
|
mounted() {
|
|
10
10
|
const fc = this.value.$featureCollection.featureCollection || this.value.$featureCollection;
|
|
11
11
|
const map = L.map(this.$refs.map).setView([0, 0], 1);
|
|
@@ -23,6 +23,9 @@ module.exports = app => app.component('dashboard-map', {
|
|
|
23
23
|
});
|
|
24
24
|
},
|
|
25
25
|
computed: {
|
|
26
|
+
mapStyle() {
|
|
27
|
+
return { height: this.height || '300px' };
|
|
28
|
+
},
|
|
26
29
|
header() {
|
|
27
30
|
if (this.value != null && this.value.$featureCollection.header) {
|
|
28
31
|
return this.value.$featureCollection.header;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mongoosejs/studio",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.11",
|
|
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": {
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"webpack": "5.x"
|
|
25
25
|
},
|
|
26
26
|
"peerDependencies": {
|
|
27
|
-
"mongoose": "7.x || 8.x || ^9.0.0
|
|
27
|
+
"mongoose": "7.x || 8.x || ^9.0.0"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"@masteringjs/eslint-config": "0.1.1",
|