@mongoosejs/studio 0.0.112 → 0.0.114

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.
@@ -6,7 +6,7 @@ const mongoose = require('mongoose');
6
6
  const vm = require('vm');
7
7
 
8
8
  const ExecuteScriptParams = new Archetype({
9
- userId: {
9
+ initiatedById: {
10
10
  $type: mongoose.Types.ObjectId
11
11
  },
12
12
  chatMessageId: {
@@ -21,7 +21,7 @@ const ExecuteScriptParams = new Archetype({
21
21
  }).compile('ExecuteScriptParams');
22
22
 
23
23
  module.exports = ({ db, studioConnection }) => async function executeScript(params) {
24
- const { userId, chatMessageId, script, roles } = new ExecuteScriptParams(params);
24
+ const { initiatedById, chatMessageId, script, roles } = new ExecuteScriptParams(params);
25
25
  const ChatMessage = studioConnection.model('__Studio_ChatMessage');
26
26
 
27
27
  await authorize('ChatMessage.executeScript', roles);
@@ -31,6 +31,10 @@ module.exports = ({ db, studioConnection }) => async function executeScript(para
31
31
  throw new Error('Chat message not found');
32
32
  }
33
33
 
34
+ if (initiatedById && chatMessage.userId?.toString() !== initiatedById.toString()) {
35
+ throw new Error('Unauthorized');
36
+ }
37
+
34
38
  // Create a sandbox with the db object
35
39
  const logs = [];
36
40
  const sandbox = { db, console: {}, ObjectId: mongoose.Types.ObjectId };
@@ -9,7 +9,7 @@ const CreateChatMessageParams = new Archetype({
9
9
  chatThreadId: {
10
10
  $type: mongoose.Types.ObjectId
11
11
  },
12
- userId: {
12
+ initiatedById: {
13
13
  $type: mongoose.Types.ObjectId
14
14
  },
15
15
  content: {
@@ -24,7 +24,7 @@ const CreateChatMessageParams = new Archetype({
24
24
  }).compile('CreateChatMessageParams');
25
25
 
26
26
  module.exports = ({ db, studioConnection, options }) => async function createChatMessage(params) {
27
- const { chatThreadId, userId, content, script, authorization, roles } = new CreateChatMessageParams(params);
27
+ const { chatThreadId, initiatedById, content, script, authorization, roles } = new CreateChatMessageParams(params);
28
28
  const ChatThread = studioConnection.model('__Studio_ChatThread');
29
29
  const ChatMessage = studioConnection.model('__Studio_ChatMessage');
30
30
 
@@ -35,7 +35,7 @@ module.exports = ({ db, studioConnection, options }) => async function createCha
35
35
  if (!chatThread) {
36
36
  throw new Error('Chat thread not found');
37
37
  }
38
- if (userId != null && chatThread.userId.toString() !== userId.toString()) {
38
+ if (initiatedById != null && chatThread.userId.toString() !== initiatedById.toString()) {
39
39
  throw new Error('Not authorized');
40
40
  }
41
41
 
@@ -46,12 +46,13 @@ module.exports = ({ db, studioConnection, options }) => async function createCha
46
46
  }));
47
47
  llmMessages.push({ role: 'user', content });
48
48
 
49
+ let summarizePromise = Promise.resolve();
49
50
  if (chatThread.title == null) {
50
- summarizeChatThread(llmMessages).then(res => {
51
+ summarizePromise = summarizeChatThread(llmMessages, authorization).then(res => {
51
52
  const title = res.response;
52
53
  chatThread.title = title;
53
54
  return chatThread.save();
54
- }).catch(() => {});
55
+ });
55
56
  }
56
57
 
57
58
  if (options.context) {
@@ -82,6 +83,7 @@ module.exports = ({ db, studioConnection, options }) => async function createCha
82
83
  })
83
84
  ]);
84
85
 
86
+ await summarizePromise;
85
87
  return { chatMessages, chatThread };
86
88
  };
87
89
 
@@ -5,21 +5,32 @@ const authorize = require('../../authorize');
5
5
  const mongoose = require('mongoose');
6
6
 
7
7
  const CreateChatThreadParams = new Archetype({
8
- userId: {
8
+ initiatedById: {
9
9
  $type: mongoose.Types.ObjectId
10
10
  },
11
11
  roles: {
12
12
  $type: ['string']
13
+ },
14
+ $workspaceId: {
15
+ $type: mongoose.Types.ObjectId,
16
+ $required: false
13
17
  }
14
18
  }).compile('CreateChatThreadParams');
15
19
 
16
20
  module.exports = ({ studioConnection }) => async function createChatThread(params) {
17
- const { userId, roles } = new CreateChatThreadParams(params);
21
+ const { initiatedById, roles, $workspaceId } = new CreateChatThreadParams(params);
18
22
  const ChatThread = studioConnection.model('__Studio_ChatThread');
19
23
 
20
24
  await authorize('ChatThread.createChatThread', roles);
21
25
 
22
- const chatThread = await ChatThread.create({ userId });
26
+ const doc = { userId: initiatedById };
27
+ if ($workspaceId) {
28
+ doc.workspaceId = $workspaceId;
29
+ }
30
+ if ($workspaceId && !initiatedById) {
31
+ throw new Error('initiatedById is required when creating a chat thread in a workspace');
32
+ }
33
+ const chatThread = await ChatThread.create(doc);
23
34
 
24
35
  return { chatThread };
25
36
  };
@@ -8,16 +8,19 @@ const GetChatThreadParams = new Archetype({
8
8
  chatThreadId: {
9
9
  $type: mongoose.Types.ObjectId
10
10
  },
11
- userId: {
11
+ initiatedById: {
12
12
  $type: mongoose.Types.ObjectId
13
13
  },
14
14
  roles: {
15
15
  $type: ['string']
16
+ },
17
+ $workspaceId: {
18
+ $type: mongoose.Types.ObjectId
16
19
  }
17
20
  }).compile('GetChatThreadParams');
18
21
 
19
22
  module.exports = ({ db, studioConnection }) => async function getChatThread(params) {
20
- const { chatThreadId, userId, roles } = new GetChatThreadParams(params);
23
+ const { chatThreadId, initiatedById, roles, $workspaceId } = new GetChatThreadParams(params);
21
24
  const ChatThread = studioConnection.model('__Studio_ChatThread');
22
25
  const ChatMessage = studioConnection.model('__Studio_ChatMessage');
23
26
 
@@ -28,8 +31,11 @@ module.exports = ({ db, studioConnection }) => async function getChatThread(para
28
31
  if (!chatThread) {
29
32
  throw new Error('Chat thread not found');
30
33
  }
31
- if (userId && chatThread.userId?.toString() !== userId.toString()) {
32
- throw new Error('Not authorized');
34
+ if (initiatedById && chatThread.userId?.toString() !== initiatedById.toString()) {
35
+
36
+ if (!$workspaceId || chatThread.workspaceId?.toString() !== $workspaceId.toString() || !chatThread.sharingOptions?.sharedWithWorkspace) {
37
+ throw new Error('Not authorized');
38
+ }
33
39
  }
34
40
 
35
41
  const chatMessages = await ChatMessage.find({ chatThreadId })
@@ -4,3 +4,4 @@ exports.createChatMessage = require('./createChatMessage');
4
4
  exports.createChatThread = require('./createChatThread');
5
5
  exports.getChatThread = require('./getChatThread');
6
6
  exports.listChatThreads = require('./listChatThreads');
7
+ exports.shareChatThread = require('./shareChatThread');
@@ -5,7 +5,7 @@ const authorize = require('../../authorize');
5
5
  const mongoose = require('mongoose');
6
6
 
7
7
  const ListChatThreadsParams = new Archetype({
8
- userId: {
8
+ initiatedById: {
9
9
  $type: mongoose.Types.ObjectId
10
10
  },
11
11
  roles: {
@@ -15,13 +15,15 @@ const ListChatThreadsParams = new Archetype({
15
15
 
16
16
  module.exports = ({ db, studioConnection }) => async function listChatThreads(params) {
17
17
  // Validate the params object
18
- const { userId, roles } = new ListChatThreadsParams(params);
18
+ const { initiatedById, roles } = new ListChatThreadsParams(params);
19
19
  const ChatThread = studioConnection.model('__Studio_ChatThread');
20
20
 
21
21
  await authorize('ChatThread.listChatThreads', roles);
22
22
 
23
+ const query = { userId: initiatedById };
24
+
23
25
  // Get all chat threads
24
- const chatThreads = await ChatThread.find(userId ? { userId } : {})
26
+ const chatThreads = await ChatThread.find(query)
25
27
  .sort({ updatedAt: -1 }); // Sort by most recently updated
26
28
 
27
29
  return {
@@ -0,0 +1,46 @@
1
+ 'use strict';
2
+
3
+ const Archetype = require('archetype');
4
+ const authorize = require('../../authorize');
5
+ const mongoose = require('mongoose');
6
+
7
+ const ShareChatThreadParams = new Archetype({
8
+ chatThreadId: {
9
+ $type: mongoose.Types.ObjectId
10
+ },
11
+ share: {
12
+ $type: 'boolean'
13
+ },
14
+ initiatedById: {
15
+ $type: mongoose.Types.ObjectId
16
+ },
17
+ roles: {
18
+ $type: ['string']
19
+ },
20
+ $workspaceId: {
21
+ $type: mongoose.Types.ObjectId
22
+ }
23
+ }).compile('ShareChatThreadParams');
24
+
25
+ module.exports = ({ studioConnection }) => async function shareChatThread(params) {
26
+ const { chatThreadId, share, initiatedById, roles, $workspaceId } = new ShareChatThreadParams(params);
27
+ const ChatThread = studioConnection.model('__Studio_ChatThread');
28
+
29
+ await authorize('ChatThread.shareChatThread', roles);
30
+
31
+ const chatThread = await ChatThread.findById(chatThreadId).orFail();
32
+ if (initiatedById != null && chatThread.userId?.toString() !== initiatedById.toString()) {
33
+ throw new Error('Not authorized');
34
+ }
35
+
36
+ if (!$workspaceId || !chatThread.workspaceId || chatThread.workspaceId.toString() !== $workspaceId.toString()) {
37
+ throw new Error('Workspace required to share');
38
+ }
39
+
40
+ chatThread.sharingOptions = chatThread.sharingOptions || {};
41
+ chatThread.sharingOptions.sharedWithWorkspace = !!share;
42
+
43
+ await chatThread.save();
44
+
45
+ return { chatThread };
46
+ };
@@ -6,6 +6,7 @@ const actionsToRequiredRoles = {
6
6
  'ChatThread.createChatThread': ['owner', 'admin', 'member'],
7
7
  'ChatThread.getChatThread': ['owner', 'admin', 'member'],
8
8
  'ChatThread.listChatThreads': ['owner', 'admin', 'member'],
9
+ 'ChatThread.shareChatThread': ['owner', 'admin', 'member'],
9
10
  'Dashboard.createDashboard': ['owner', 'admin', 'member'],
10
11
  'Dashboard.deleteDashboard': ['owner', 'admin', 'member'],
11
12
  'Dashboard.getDashboard': ['owner', 'admin', 'member', 'readonly', 'dashboards'],
@@ -9,6 +9,16 @@ const chatThreadSchema = new mongoose.Schema({
9
9
  userId: {
10
10
  type: mongoose.ObjectId,
11
11
  ref: 'User'
12
+ },
13
+ workspaceId: {
14
+ type: mongoose.ObjectId,
15
+ ref: 'Workspace'
16
+ },
17
+ sharingOptions: {
18
+ sharedWithWorkspace: {
19
+ type: Boolean,
20
+ default: false
21
+ }
12
22
  }
13
23
  }, { timestamps: true });
14
24
 
@@ -61,6 +61,7 @@ module.exports = function netlify(conn, options) {
61
61
  params.$workspaceId = workspace._id;
62
62
  params.roles = roles;
63
63
  params.userId = user._id;
64
+ params.initiatedById = user._id;
64
65
  }
65
66
 
66
67
  if (typeof actionName !== 'string') {
@@ -68,6 +68,9 @@ if (window.MONGOOSE_STUDIO_CONFIG.isLambda) {
68
68
  },
69
69
  listChatThreads(params) {
70
70
  return client.post('', { action: 'ChatThread.listChatThreads', ...params }).then(res => res.data);
71
+ },
72
+ shareChatThread(params) {
73
+ return client.post('', { action: 'ChatThread.shareChatThread', ...params }).then(res => res.data);
71
74
  }
72
75
  };
73
76
  exports.ChatMessage = {
@@ -170,6 +173,9 @@ if (window.MONGOOSE_STUDIO_CONFIG.isLambda) {
170
173
  },
171
174
  listChatThreads: function listChatThreads(params) {
172
175
  return client.post('/ChatThread/listChatThreads', params).then(res => res.data);
176
+ },
177
+ shareChatThread: function shareChatThread(params) {
178
+ return client.post('/ChatThread/shareChatThread', params).then(res => res.data);
173
179
  }
174
180
  };
175
181
  exports.ChatMessage = {
@@ -528,6 +534,7 @@ module.exports = app => app.component('chat-message', {
528
534
 
529
535
  const api = __webpack_require__(/*! ../api */ "./frontend/src/api.js");
530
536
  const template = __webpack_require__(/*! ./chat.html */ "./frontend/src/chat/chat.html");
537
+ const vanillatoasts = __webpack_require__(/*! vanillatoasts */ "./node_modules/vanillatoasts/vanillatoasts.js");
531
538
 
532
539
  module.exports = app => app.component('chat', {
533
540
  template: template,
@@ -539,7 +546,8 @@ module.exports = app => app.component('chat', {
539
546
  chatThreadId: null,
540
547
  chatThreads: [],
541
548
  chatMessages: [],
542
- hideSidebar: null
549
+ hideSidebar: null,
550
+ sharingThread: false
543
551
  }),
544
552
  methods: {
545
553
  async sendMessage() {
@@ -563,11 +571,16 @@ module.exports = app => app.component('chat', {
563
571
  }
564
572
  });
565
573
 
566
- const { chatMessages } = await api.ChatThread.createChatMessage({
574
+ const { chatMessages, chatThread } = await api.ChatThread.createChatMessage({
567
575
  chatThreadId: this.chatThreadId,
568
576
  content: this.newMessage
569
577
  });
570
578
  this.chatMessages.push(chatMessages[1]);
579
+ for (const thread of this.chatThreads) {
580
+ if (thread._id === chatThread._id) {
581
+ thread.title = chatThread.title;
582
+ }
583
+ }
571
584
 
572
585
  this.newMessage = '';
573
586
  this.$nextTick(() => {
@@ -606,6 +619,44 @@ module.exports = app => app.component('chat', {
606
619
  async createNewThread() {
607
620
  const { chatThread } = await api.ChatThread.createChatThread();
608
621
  this.$router.push('/chat/' + chatThread._id);
622
+ },
623
+ async toggleShareThread() {
624
+ if (!this.chatThreadId || !this.hasWorkspace) {
625
+ return;
626
+ }
627
+ this.sharingThread = true;
628
+ try {
629
+ const share = true;
630
+ const { chatThread } = await api.ChatThread.shareChatThread({ chatThreadId: this.chatThreadId, share });
631
+ const idx = this.chatThreads.findIndex(t => t._id === chatThread._id);
632
+ if (idx !== -1) {
633
+ this.chatThreads.splice(idx, 1, chatThread);
634
+ }
635
+
636
+ // Copy current URL to clipboard and show a toast
637
+ const url = window.location.href;
638
+ await navigator.clipboard.writeText(url);
639
+ vanillatoasts.create({
640
+ title: 'Share link copied!',
641
+ type: 'success',
642
+ timeout: 3000,
643
+ icon: 'images/success.png',
644
+ positionClass: 'bottomRight'
645
+ });
646
+ } finally {
647
+ this.sharingThread = false;
648
+ }
649
+ }
650
+ },
651
+ computed: {
652
+ currentThread() {
653
+ return this.chatThreads.find(t => t._id === this.chatThreadId);
654
+ },
655
+ hasWorkspace() {
656
+ return !!window.MONGOOSE_STUDIO_CONFIG.workspace?._id;
657
+ },
658
+ sharedWithWorkspace() {
659
+ return !!this.currentThread?.sharingOptions?.sharedWithWorkspace;
609
660
  }
610
661
  },
611
662
  async mounted() {
@@ -1096,7 +1147,8 @@ module.exports = app => app.component('dashboard', {
1096
1147
  showEditor: false,
1097
1148
  dashboard: null,
1098
1149
  dashboardResults: [],
1099
- errorMessage: null
1150
+ errorMessage: null,
1151
+ showDetailModal: false
1100
1152
  };
1101
1153
  },
1102
1154
  methods: {
@@ -1128,6 +1180,9 @@ module.exports = app => app.component('dashboard', {
1128
1180
  } finally {
1129
1181
  this.status = 'loaded';
1130
1182
  }
1183
+ },
1184
+ openDetailModal() {
1185
+ this.showDetailModal = true;
1131
1186
  }
1132
1187
  },
1133
1188
  computed: {
@@ -4136,7 +4191,7 @@ module.exports = "<div class=\"relative flex items-start\" :class=\"{'justify-en
4136
4191
  /***/ ((module) => {
4137
4192
 
4138
4193
  "use strict";
4139
- module.exports = "<div class=\"flex\" style=\"height: calc(100vh - 55px); height: calc(100dvh - 55px)\">\n <div class=\"fixed top-[65px] cursor-pointer bg-gray-100 rounded-r-md z-10\" @click=\"hideSidebar = false\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" style=\"h-5 w-5\" viewBox=\"0 -960 960 960\" class=\"w-5\" fill=\"#5f6368\"><path d=\"M360-120v-720h80v720h-80Zm160-160v-400l200 200-200 200Z\"/></svg>\n </div>\n <!-- Sidebar: Chat Threads -->\n <aside class=\"bg-gray-50 border-r overflow-y-auto overflow-x-hidden h-full transition-all duration-300 ease-in-out z-20 w-0 lg:w-64 fixed lg:relative\" :class=\"hideSidebar === true ? '!w-0' : hideSidebar === false ? '!w-64' : ''\">\n <div class=\"flex items-center border-b border-gray-100 w-64 overflow-x-hidden\">\n <div class=\"p-4 font-bold text-lg\">Chat Threads</div>\n <button\n @click=\"hideSidebar = true\"\n class=\"ml-auto mr-2 p-2 rounded hover:bg-gray-200 focus:outline-none\"\n aria-label=\"Close sidebar\"\n >\n <svg xmlns=\"http://www.w3.org/2000/svg\" style=\"h-5 w-5\" viewBox=\"0 -960 960 960\" class=\"w-5\" fill=\"currentColor\"><path d=\"M660-320v-320L500-480l160 160ZM200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h560q33 0 56.5 23.5T840-760v560q0 33-23.5 56.5T760-120H200Zm120-80v-560H200v560h120Zm80 0h360v-560H400v560Zm-80 0H200h120Z\"/></svg>\n </button>\n </div>\n <div class=\"p-4 w-64\">\n <async-button\n @click=\"createNewThread\"\n class=\"w-full bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700\"\n >\n Create New Thread\n </async-button>\n </div>\n <div v-if=\"status === 'loaded' && chatThreads.length === 0\" class=\"p-4 text-sm text-gray-700\">\n No threads yet\n </div>\n <ul v-if=\"status === 'loaded'\" class=\"w-64\">\n <li\n v-for=\"thread in chatThreads\"\n :key=\"thread._id\"\n @click=\"selectThread(thread._id)\"\n class=\"p-4 hover:bg-gray-200 cursor-pointer w-64\"\n :class=\"{ 'bg-gray-300': thread._id === chatThreadId }\"\n >\n {{ thread.title || 'Untitled Thread' }}\n </li>\n </ul>\n </aside>\n\n <!-- Main Chat Area -->\n <main class=\"flex-1 flex flex-col\">\n <div class=\"flex-1 overflow-y-auto p-6 space-y-4\" ref=\"messagesContainer\">\n <ul role=\"list\" class=\"space-y-4\">\n <div v-if=\"true\">\n <div class=\"flex items-center justify-center py-3 mb-4\">\n <div class=\"bg-gray-300 h-px flex-grow max-w-xs\"></div>\n <p class=\"mx-4 text-sm font-medium text-gray-500\">This is the beginning of the message thread</p>\n <div class=\"bg-gray-300 h-px flex-grow max-w-xs\"></div>\n </div>\n </div>\n <li v-for=\"message in chatMessages\" :key=\"message._id\">\n <chat-message :message=\"message\"></chat-message>\n </li>\n </ul>\n </div>\n\n\n <!-- Input Area -->\n <div class=\"border-t p-4\">\n <form @submit.prevent=\"sendMessage\" :disabled=\"sendingMessage\" class=\"flex gap-2 items-end justify-end\">\n <textarea\n v-model=\"newMessage\"\n placeholder=\"Ask something...\"\n class=\"flex-1 border rounded px-4 py-2 resize-none overflow-y-auto\"\n rows=\"1\"\n ref=\"messageInput\"\n @input=\"adjustTextareaHeight\"\n @keydown.enter.exact.prevent=\"handleEnter\"\n ></textarea>\n <button class=\"bg-blue-600 text-white px-4 h-[42px] rounded disabled:bg-gray-600\" :disabled=\"sendingMessage\">\n <svg v-if=\"sendingMessage\" style=\"height: 1em\" viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\">\n <g>\n <circle cx=\"12\" cy=\"12\" r=\"10\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" opacity=\"0.3\" />\n <path d=\"M12 2a10 10 0 0 1 10 10\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\">\n <animateTransform attributeName=\"transform\" type=\"rotate\" from=\"0 12 12\" to=\"360 12 12\" dur=\"1s\" repeatCount=\"indefinite\" />\n </path>\n </g>\n </svg>\n <span v-else>Send</span>\n </button>\n </form>\n </div>\n </main>\n</div>\n";
4194
+ module.exports = "<div class=\"flex\" style=\"height: calc(100vh - 55px); height: calc(100dvh - 55px)\">\n <div class=\"fixed top-[65px] cursor-pointer bg-gray-100 rounded-r-md z-10\" @click=\"hideSidebar = false\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" style=\"h-5 w-5\" viewBox=\"0 -960 960 960\" class=\"w-5\" fill=\"#5f6368\"><path d=\"M360-120v-720h80v720h-80Zm160-160v-400l200 200-200 200Z\"/></svg>\n </div>\n <button\n class=\"fixed top-[65px] right-4 z-10 p-2 rounded-md shadow bg-white\"\n :class=\"hasWorkspace ? 'text-gray-700 hover:bg-gray-100' : 'text-gray-300 cursor-not-allowed bg-gray-50'\"\n @click=\"toggleShareThread\"\n :disabled=\"!hasWorkspace || !chatThreadId || sharingThread\"\n aria-label=\"Share thread with workspace\"\n title=\"Share thread with workspace\"\n >\n <svg v-if=\"hasWorkspace\" xmlns=\"http://www.w3.org/2000/svg\" class=\"w-5 h-5\" 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>\n <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>\n </button>\n <!-- Sidebar: Chat Threads -->\n <aside class=\"bg-gray-50 border-r overflow-y-auto overflow-x-hidden h-full transition-all duration-300 ease-in-out z-20 w-0 lg:w-64 fixed lg:relative\" :class=\"hideSidebar === true ? '!w-0' : hideSidebar === false ? '!w-64' : ''\">\n <div class=\"flex items-center border-b border-gray-100 w-64 overflow-x-hidden\">\n <div class=\"p-4 font-bold text-lg\">Chat Threads</div>\n <button\n @click=\"hideSidebar = true\"\n class=\"ml-auto mr-2 p-2 rounded hover:bg-gray-200 focus:outline-none\"\n aria-label=\"Close sidebar\"\n >\n <svg xmlns=\"http://www.w3.org/2000/svg\" style=\"h-5 w-5\" viewBox=\"0 -960 960 960\" class=\"w-5\" fill=\"currentColor\"><path d=\"M660-320v-320L500-480l160 160ZM200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h560q33 0 56.5 23.5T840-760v560q0 33-23.5 56.5T760-120H200Zm120-80v-560H200v560h120Zm80 0h360v-560H400v560Zm-80 0H200h120Z\"/></svg>\n </button>\n </div>\n <div class=\"p-4 w-64\">\n <async-button\n @click=\"createNewThread\"\n class=\"w-full bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700\"\n >\n Create New Thread\n </async-button>\n </div>\n <div v-if=\"status === 'loaded' && chatThreads.length === 0\" class=\"p-4 text-sm text-gray-700\">\n No threads yet\n </div>\n <ul v-if=\"status === 'loaded'\" class=\"w-64\">\n <li\n v-for=\"thread in chatThreads\"\n :key=\"thread._id\"\n @click=\"selectThread(thread._id)\"\n class=\"p-4 hover:bg-gray-200 cursor-pointer w-64\"\n :class=\"{ 'bg-gray-300': thread._id === chatThreadId }\"\n >\n {{ thread.title || 'Untitled Thread' }}\n </li>\n </ul>\n </aside>\n\n <!-- Main Chat Area -->\n <main class=\"flex-1 flex flex-col\">\n <div class=\"flex-1 overflow-y-auto p-6 space-y-4\" ref=\"messagesContainer\">\n <ul role=\"list\" class=\"space-y-4\">\n <div v-if=\"true\">\n <div class=\"flex items-center justify-center py-3 mb-4\">\n <div class=\"bg-gray-300 h-px flex-grow max-w-xs\"></div>\n <p class=\"mx-4 text-sm font-medium text-gray-500\">This is the beginning of the message thread</p>\n <div class=\"bg-gray-300 h-px flex-grow max-w-xs\"></div>\n </div>\n </div>\n <li v-for=\"message in chatMessages\" :key=\"message._id\">\n <chat-message :message=\"message\"></chat-message>\n </li>\n </ul>\n </div>\n\n\n <!-- Input Area -->\n <div class=\"border-t p-4\">\n <form @submit.prevent=\"sendMessage\" :disabled=\"sendingMessage\" class=\"flex gap-2 items-end justify-end\">\n <textarea\n v-model=\"newMessage\"\n placeholder=\"Ask something...\"\n class=\"flex-1 border rounded px-4 py-2 resize-none overflow-y-auto\"\n rows=\"1\"\n ref=\"messageInput\"\n @input=\"adjustTextareaHeight\"\n @keydown.enter.exact.prevent=\"handleEnter\"\n ></textarea>\n <button class=\"bg-blue-600 text-white px-4 h-[42px] rounded disabled:bg-gray-600\" :disabled=\"sendingMessage\">\n <svg v-if=\"sendingMessage\" style=\"height: 1em\" viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\">\n <g>\n <circle cx=\"12\" cy=\"12\" r=\"10\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" opacity=\"0.3\" />\n <path d=\"M12 2a10 10 0 0 1 10 10\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\">\n <animateTransform attributeName=\"transform\" type=\"rotate\" from=\"0 12 12\" to=\"360 12 12\" dur=\"1s\" repeatCount=\"indefinite\" />\n </path>\n </g>\n </svg>\n <span v-else>Send</span>\n </button>\n </form>\n </div>\n </main>\n</div>\n";
4140
4195
 
4141
4196
  /***/ }),
4142
4197
 
@@ -4268,7 +4323,7 @@ module.exports = "<div class=\"py-2\">\n <div v-if=\"header\" class=\"border-b
4268
4323
  /***/ ((module) => {
4269
4324
 
4270
4325
  "use strict";
4271
- module.exports = "<div class=\"dashboard px-1\">\n <div v-if=\"status === 'loading'\" class=\"max-w-5xl mx-auto text-center\">\n <img src=\"images/loader.gif\" class=\"inline mt-10\">\n </div>\n <div v-if=\"dashboard && status !== 'loading'\" class=\"max-w-5xl mx-auto\">\n <div class=\"flex items-center w-full\" v-if=\"!showEditor\">\n <h2 class=\"mt-4 mb-4 text-gray-900 font-semibold text-xl grow shrink\">{{title}}</h2>\n <div class=\"flex gap-2\">\n <button\n @click=\"showEditor = true\"\n type=\"button\"\n :disabled=\"status === 'evaluating'\"\n class=\"flex items-center rounded-md bg-ultramarine-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-ultramarine-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ultramarine-600 disabled:cursor-not-allowed disabled:bg-gray-600\">\n <img src=\"images/edit.svg\" class=\"inline h-[1.25em] mr-1\" /> Edit\n </button>\n\n <async-button\n @click=\"evaluateDashboard\"\n type=\"button\"\n :disabled=\"status === 'evaluating'\"\n class=\"flex items-center rounded-md bg-ultramarine-600 px-4 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-ultramarine-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ultramarine-600 disabled:cursor-not-allowed disabled:bg-gray-600\"\n >\n <svg class=\"inline h-[1.25em] mr-1\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 -960 960 960\" fill=\"currentColor\"><path d=\"m670-140 160-100-160-100v200ZM240-600h480v-80H240v80ZM720-40q-83 0-141.5-58.5T520-240q0-83 58.5-141.5T720-440q83 0 141.5 58.5T920-240q0 83-58.5 141.5T720-40ZM120-80v-680q0-33 23.5-56.5T200-840h560q33 0 56.5 23.5T840-760v267q-19-9-39-15t-41-9v-243H200v562h243q5 31 15.5 59T486-86l-6 6-60-60-60 60-60-60-60 60-60-60-60 60Zm120-200h203q3-21 9-41t15-39H240v80Zm0-160h284q38-37 88.5-58.5T720-520H240v80Zm-40 242v-562 562Z\"/></svg>\n Evaluate\n </async-button>\n </div>\n </div>\n <div v-if=\"!showEditor\" class=\"mt-4 mb-4\">\n <div v-if=\"dashboardResults.length === 0\">\n <div class=\"flex flex-col items-center justify-center py-8\">\n <p class=\"text-gray-700 text-base mb-4\">This dashboard hasn't been evaluated yet.</p>\n <async-button\n @click=\"evaluateDashboard\"\n type=\"button\"\n :disabled=\"status === 'evaluating'\"\n class=\"rounded-md bg-ultramarine-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-ultramarine-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ultramarine-600 disabled:cursor-not-allowed disabled:bg-gray-600\"\n >\n Evaluate Dashboard\n </async-button>\n </div>\n </div>\n <div v-else>\n <dashboard-result\n :key=\"dashboardResult.finishedEvaluatingAt\"\n :result=\"dashboardResult.result\"\n :finishedEvaluatingAt=\"dashboardResult.finishedEvaluatingAt\">\n </dashboard-result>\n </div>\n </div>\n <div v-if=\"showEditor\" class=\"mt-4\">\n <edit-dashboard\n :dashboardId=\"dashboard._id\"\n :code=\"code\"\n :currentDescription=\"description\"\n :currentTitle=\"title\"\n @close=\"showEditor=false;\"\n @update=\"updateCode\"></edit-dashboard>\n </div>\n <div v-if=\"errorMessage\" class=\"rounded-md bg-red-50 p-4 mt-4\">\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\" data-slot=\"icon\">\n <path fill-rule=\"evenodd\" d=\"M10 18a8 8 0 1 0 0-16 8 8 0 0 0 0 16ZM8.28 7.22a.75.75 0 0 0-1.06 1.06L8.94 10l-1.72 1.72a.75.75 0 1 0 1.06 1.06L10 11.06l1.72 1.72a.75.75 0 1 0 1.06-1.06L11.06 10l1.72-1.72a.75.75 0 0 0-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\">{{errorMessage}}</h3>\n </div>\n </div>\n </div>\n\n </div>\n <div v-if=\"!dashboard && status !== 'loading'\">\n No dashboard with the given id could be found.\n </div>\n</div>\n";
4326
+ module.exports = "<div class=\"dashboard px-1\">\n <div v-if=\"status === 'loading'\" class=\"max-w-5xl mx-auto text-center\">\n <img src=\"images/loader.gif\" class=\"inline mt-10\">\n </div>\n <div v-if=\"dashboard && status !== 'loading'\" class=\"max-w-5xl mx-auto\">\n <div class=\"flex items-center w-full\" v-if=\"!showEditor\">\n <h2 class=\"mt-4 mb-4 text-gray-900 font-semibold text-xl grow shrink\">{{title}}</h2>\n <div class=\"flex gap-2\">\n <button\n @click=\"showEditor = true\"\n type=\"button\"\n :disabled=\"status === 'evaluating'\"\n class=\"flex items-center rounded-md bg-ultramarine-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-ultramarine-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ultramarine-600 disabled:cursor-not-allowed disabled:bg-gray-600\">\n <img src=\"images/edit.svg\" class=\"inline h-[1.25em] mr-1\" /> Edit\n </button>\n\n <async-button\n @click=\"evaluateDashboard\"\n type=\"button\"\n :disabled=\"status === 'evaluating'\"\n class=\"flex items-center rounded-md bg-ultramarine-600 px-4 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-ultramarine-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ultramarine-600 disabled:cursor-not-allowed disabled:bg-gray-600\"\n >\n <svg class=\"inline h-[1.25em] mr-1\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 -960 960 960\" fill=\"currentColor\"><path d=\"m670-140 160-100-160-100v200ZM240-600h480v-80H240v80ZM720-40q-83 0-141.5-58.5T520-240q0-83 58.5-141.5T720-440q83 0 141.5 58.5T920-240q0 83-58.5 141.5T720-40ZM120-80v-680q0-33 23.5-56.5T200-840h560q33 0 56.5 23.5T840-760v267q-19-9-39-15t-41-9v-243H200v562h243q5 31 15.5 59T486-86l-6 6-60-60-60 60-60-60-60 60-60-60-60 60Zm120-200h203q3-21 9-41t15-39H240v80Zm0-160h284q38-37 88.5-58.5T720-520H240v80Zm-40 242v-562 562Z\"/></svg>\n Evaluate\n </async-button>\n </div>\n </div>\n <div v-if=\"!showEditor\" class=\"mt-4 mb-4\">\n <div v-if=\"dashboardResults.length === 0\">\n <div class=\"flex flex-col items-center justify-center py-8\">\n <p class=\"text-gray-700 text-base mb-4\">This dashboard hasn't been evaluated yet.</p>\n <async-button\n @click=\"evaluateDashboard\"\n type=\"button\"\n :disabled=\"status === 'evaluating'\"\n class=\"rounded-md bg-ultramarine-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-ultramarine-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ultramarine-600 disabled:cursor-not-allowed disabled:bg-gray-600\"\n >\n Evaluate Dashboard\n </async-button>\n </div>\n </div>\n <div v-else>\n <div class=\"relative\">\n <button\n class=\"absolute top-2 right-2 px-2 py-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 aria-label=\"Expand dashboard result\">\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 <dashboard-result\n :key=\"dashboardResult.finishedEvaluatingAt\"\n :result=\"dashboardResult.result\"\n :finishedEvaluatingAt=\"dashboardResult.finishedEvaluatingAt\">\n </dashboard-result>\n </div>\n </div>\n </div>\n <div v-if=\"showEditor\" class=\"mt-4\">\n <edit-dashboard\n :dashboardId=\"dashboard._id\"\n :code=\"code\"\n :currentDescription=\"description\"\n :currentTitle=\"title\"\n @close=\"showEditor=false;\"\n @update=\"updateCode\"></edit-dashboard>\n </div>\n <div v-if=\"errorMessage\" class=\"rounded-md bg-red-50 p-4 mt-4\">\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\" data-slot=\"icon\">\n <path fill-rule=\"evenodd\" d=\"M10 18a8 8 0 1 0 0-16 8 8 0 0 0 0 16ZM8.28 7.22a.75.75 0 0 0-1.06 1.06L8.94 10l-1.72 1.72a.75.75 0 1 0 1.06 1.06L10 11.06l1.72 1.72a.75.75 0 1 0 1.06-1.06L11.06 10l1.72-1.72a.75.75 0 0 0-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\">{{errorMessage}}</h3>\n </div>\n </div>\n </div>\n\n </div>\n <div v-if=\"!dashboard && status !== 'loading'\">\n No dashboard with the given id could be found.\n </div>\n</div>\n\n<modal\n v-if=\"showDetailModal\"\n containerClass=\"!h-[90vh] !w-[90vw]\"\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label=\"Dashboard Details\"\n>\n <template #body>\n <div class=\"absolute font-mono right-1 top-1 cursor-pointer text-xl\" @click=\"showDetailModal = false;\" role=\"button\" aria-label=\"Close modal\">&times;</div>\n <div class=\"h-full overflow-auto\">\n <dashboard-result\n v-if=\"dashboardResult\"\n :result=\"dashboardResult.result\"\n :finishedEvaluatingAt=\"dashboardResult.finishedEvaluatingAt\">\n </dashboard-result>\n </div>\n </template>\n</modal>\n";
4272
4327
 
4273
4328
  /***/ }),
4274
4329
 
@@ -14782,7 +14837,7 @@ var bson = /*#__PURE__*/Object.freeze({
14782
14837
  /***/ ((module) => {
14783
14838
 
14784
14839
  "use strict";
14785
- module.exports = /*#__PURE__*/JSON.parse('{"name":"@mongoosejs/studio","version":"0.0.112","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"},"dependencies":{"archetype":"0.13.1","csv-stringify":"6.3.0","ejson":"^2.2.3","extrovert":"0.0.26","marked":"15.0.12","node-inspect-extracted":"3.x","tailwindcss":"3.4.0","vanillatoasts":"^1.6.0","vue":"3.x","webpack":"5.x"},"peerDependencies":{"bson":"^5.5.1 || 6.x","express":"4.x","mongoose":"7.x || 8.x"},"devDependencies":{"@masteringjs/eslint-config":"0.1.1","axios":"1.2.2","dedent":"^1.6.0","eslint":"9.30.0","express":"4.x","mocha":"10.2.0","mongoose":"8.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"}}');
14840
+ module.exports = /*#__PURE__*/JSON.parse('{"name":"@mongoosejs/studio","version":"0.0.114","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"},"dependencies":{"archetype":"0.13.1","csv-stringify":"6.3.0","ejson":"^2.2.3","extrovert":"0.0.26","marked":"15.0.12","node-inspect-extracted":"3.x","tailwindcss":"3.4.0","vanillatoasts":"^1.6.0","vue":"3.x","webpack":"5.x"},"peerDependencies":{"bson":"^5.5.1 || 6.x","express":"4.x","mongoose":"7.x || 8.x"},"devDependencies":{"@masteringjs/eslint-config":"0.1.1","axios":"1.2.2","dedent":"^1.6.0","eslint":"9.30.0","express":"4.x","mocha":"10.2.0","mongoose":"8.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"}}');
14786
14841
 
14787
14842
  /***/ })
14788
14843
 
@@ -626,6 +626,10 @@ video {
626
626
  right: 0.25rem;
627
627
  }
628
628
 
629
+ .right-4 {
630
+ right: 1rem;
631
+ }
632
+
629
633
  .top-1 {
630
634
  top: 0.25rem;
631
635
  }
@@ -1788,6 +1792,12 @@ video {
1788
1792
  opacity: 0.5;
1789
1793
  }
1790
1794
 
1795
+ .shadow {
1796
+ --tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
1797
+ --tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);
1798
+ box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
1799
+ }
1800
+
1791
1801
  .shadow-lg {
1792
1802
  --tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
1793
1803
  --tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);
@@ -58,6 +58,9 @@ if (window.MONGOOSE_STUDIO_CONFIG.isLambda) {
58
58
  },
59
59
  listChatThreads(params) {
60
60
  return client.post('', { action: 'ChatThread.listChatThreads', ...params }).then(res => res.data);
61
+ },
62
+ shareChatThread(params) {
63
+ return client.post('', { action: 'ChatThread.shareChatThread', ...params }).then(res => res.data);
61
64
  }
62
65
  };
63
66
  exports.ChatMessage = {
@@ -160,6 +163,9 @@ if (window.MONGOOSE_STUDIO_CONFIG.isLambda) {
160
163
  },
161
164
  listChatThreads: function listChatThreads(params) {
162
165
  return client.post('/ChatThread/listChatThreads', params).then(res => res.data);
166
+ },
167
+ shareChatThread: function shareChatThread(params) {
168
+ return client.post('/ChatThread/shareChatThread', params).then(res => res.data);
163
169
  }
164
170
  };
165
171
  exports.ChatMessage = {
@@ -2,6 +2,17 @@
2
2
  <div class="fixed top-[65px] cursor-pointer bg-gray-100 rounded-r-md z-10" @click="hideSidebar = false">
3
3
  <svg xmlns="http://www.w3.org/2000/svg" style="h-5 w-5" viewBox="0 -960 960 960" class="w-5" fill="#5f6368"><path d="M360-120v-720h80v720h-80Zm160-160v-400l200 200-200 200Z"/></svg>
4
4
  </div>
5
+ <button
6
+ class="fixed top-[65px] right-4 z-10 p-2 rounded-md shadow bg-white"
7
+ :class="hasWorkspace ? 'text-gray-700 hover:bg-gray-100' : 'text-gray-300 cursor-not-allowed bg-gray-50'"
8
+ @click="toggleShareThread"
9
+ :disabled="!hasWorkspace || !chatThreadId || sharingThread"
10
+ aria-label="Share thread with workspace"
11
+ title="Share thread with workspace"
12
+ >
13
+ <svg v-if="hasWorkspace" xmlns="http://www.w3.org/2000/svg" class="w-5 h-5" 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>
14
+ <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>
15
+ </button>
5
16
  <!-- Sidebar: Chat Threads -->
6
17
  <aside class="bg-gray-50 border-r overflow-y-auto overflow-x-hidden h-full transition-all duration-300 ease-in-out z-20 w-0 lg:w-64 fixed lg:relative" :class="hideSidebar === true ? '!w-0' : hideSidebar === false ? '!w-64' : ''">
7
18
  <div class="flex items-center border-b border-gray-100 w-64 overflow-x-hidden">
@@ -2,6 +2,7 @@
2
2
 
3
3
  const api = require('../api');
4
4
  const template = require('./chat.html');
5
+ const vanillatoasts = require('vanillatoasts');
5
6
 
6
7
  module.exports = app => app.component('chat', {
7
8
  template: template,
@@ -13,7 +14,8 @@ module.exports = app => app.component('chat', {
13
14
  chatThreadId: null,
14
15
  chatThreads: [],
15
16
  chatMessages: [],
16
- hideSidebar: null
17
+ hideSidebar: null,
18
+ sharingThread: false
17
19
  }),
18
20
  methods: {
19
21
  async sendMessage() {
@@ -37,11 +39,16 @@ module.exports = app => app.component('chat', {
37
39
  }
38
40
  });
39
41
 
40
- const { chatMessages } = await api.ChatThread.createChatMessage({
42
+ const { chatMessages, chatThread } = await api.ChatThread.createChatMessage({
41
43
  chatThreadId: this.chatThreadId,
42
44
  content: this.newMessage
43
45
  });
44
46
  this.chatMessages.push(chatMessages[1]);
47
+ for (const thread of this.chatThreads) {
48
+ if (thread._id === chatThread._id) {
49
+ thread.title = chatThread.title;
50
+ }
51
+ }
45
52
 
46
53
  this.newMessage = '';
47
54
  this.$nextTick(() => {
@@ -80,6 +87,44 @@ module.exports = app => app.component('chat', {
80
87
  async createNewThread() {
81
88
  const { chatThread } = await api.ChatThread.createChatThread();
82
89
  this.$router.push('/chat/' + chatThread._id);
90
+ },
91
+ async toggleShareThread() {
92
+ if (!this.chatThreadId || !this.hasWorkspace) {
93
+ return;
94
+ }
95
+ this.sharingThread = true;
96
+ try {
97
+ const share = true;
98
+ const { chatThread } = await api.ChatThread.shareChatThread({ chatThreadId: this.chatThreadId, share });
99
+ const idx = this.chatThreads.findIndex(t => t._id === chatThread._id);
100
+ if (idx !== -1) {
101
+ this.chatThreads.splice(idx, 1, chatThread);
102
+ }
103
+
104
+ // Copy current URL to clipboard and show a toast
105
+ const url = window.location.href;
106
+ await navigator.clipboard.writeText(url);
107
+ vanillatoasts.create({
108
+ title: 'Share link copied!',
109
+ type: 'success',
110
+ timeout: 3000,
111
+ icon: 'images/success.png',
112
+ positionClass: 'bottomRight'
113
+ });
114
+ } finally {
115
+ this.sharingThread = false;
116
+ }
117
+ }
118
+ },
119
+ computed: {
120
+ currentThread() {
121
+ return this.chatThreads.find(t => t._id === this.chatThreadId);
122
+ },
123
+ hasWorkspace() {
124
+ return !!window.MONGOOSE_STUDIO_CONFIG.workspace?._id;
125
+ },
126
+ sharedWithWorkspace() {
127
+ return !!this.currentThread?.sharingOptions?.sharedWithWorkspace;
83
128
  }
84
129
  },
85
130
  async mounted() {
@@ -40,11 +40,21 @@
40
40
  </div>
41
41
  </div>
42
42
  <div v-else>
43
- <dashboard-result
44
- :key="dashboardResult.finishedEvaluatingAt"
45
- :result="dashboardResult.result"
46
- :finishedEvaluatingAt="dashboardResult.finishedEvaluatingAt">
47
- </dashboard-result>
43
+ <div class="relative">
44
+ <button
45
+ class="absolute top-2 right-2 px-2 py-1 text-xs bg-blue-500 text-white border-none rounded cursor-pointer hover:bg-blue-600 transition-colors flex items-center"
46
+ @click="openDetailModal"
47
+ aria-label="Expand dashboard result">
48
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
49
+ <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" />
50
+ </svg>
51
+ </button>
52
+ <dashboard-result
53
+ :key="dashboardResult.finishedEvaluatingAt"
54
+ :result="dashboardResult.result"
55
+ :finishedEvaluatingAt="dashboardResult.finishedEvaluatingAt">
56
+ </dashboard-result>
57
+ </div>
48
58
  </div>
49
59
  </div>
50
60
  <div v-if="showEditor" class="mt-4">
@@ -74,3 +84,22 @@
74
84
  No dashboard with the given id could be found.
75
85
  </div>
76
86
  </div>
87
+
88
+ <modal
89
+ v-if="showDetailModal"
90
+ containerClass="!h-[90vh] !w-[90vw]"
91
+ role="dialog"
92
+ aria-modal="true"
93
+ aria-label="Dashboard Details"
94
+ >
95
+ <template #body>
96
+ <div class="absolute font-mono right-1 top-1 cursor-pointer text-xl" @click="showDetailModal = false;" role="button" aria-label="Close modal">&times;</div>
97
+ <div class="h-full overflow-auto">
98
+ <dashboard-result
99
+ v-if="dashboardResult"
100
+ :result="dashboardResult.result"
101
+ :finishedEvaluatingAt="dashboardResult.finishedEvaluatingAt">
102
+ </dashboard-result>
103
+ </div>
104
+ </template>
105
+ </modal>
@@ -15,7 +15,8 @@ module.exports = app => app.component('dashboard', {
15
15
  showEditor: false,
16
16
  dashboard: null,
17
17
  dashboardResults: [],
18
- errorMessage: null
18
+ errorMessage: null,
19
+ showDetailModal: false
19
20
  };
20
21
  },
21
22
  methods: {
@@ -47,6 +48,9 @@ module.exports = app => app.component('dashboard', {
47
48
  } finally {
48
49
  this.status = 'loaded';
49
50
  }
51
+ },
52
+ openDetailModal() {
53
+ this.showDetailModal = true;
50
54
  }
51
55
  },
52
56
  computed: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mongoosejs/studio",
3
- "version": "0.0.112",
3
+ "version": "0.0.114",
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": {
package/valnotes.md ADDED
@@ -0,0 +1,2 @@
1
+ 1. Export to PNG causes charts to scroll down
2
+ 2. Chat summarization broken