Share Your AI Bot Conversations!

We’re excited to announce a new feature that allows you to easily share your conversations with AI bots on our forum. Whether you’ve had an insightful, funny, or helpful exchange with one of our AI bots, you can now share that interaction with others in just a few clicks. Here’s what you need to know:

How do I share an AI bot conversation?

When viewing a conversation with an AI bot, you’ll see a new “Share” icon in the post menu.

Clicking works as following:

  1. For short conversations: If the conversation is within a certain length (up to 2 interactions), clicking the “Share” icon will automatically copy the entire conversation to your clipboard. You can then paste it wherever you like—be it in a new post, a private message, or outside the forum.

  2. For longer conversations: For more extended interactions, clicking the “Share” icon will open a modal with a preview of your conversation. You can use a slider to select how much of the conversation you want to share. Once you’ve chosen the desired range, click the “Copy” button to copy the selected conversation to your clipboard.

What will the shared conversation look like?

The shared conversation will be formatted in a collapsible “details” block, making it easy to read and manage in posts. It will include the usernames and the content of the messages within the range you’ve selected.

Example:

<details class='ai-quote'>
<summary>
<span>Greetings</span>
<span title='Conversation with AI'>AI</span>
</summary>

**sam:**

say hello

**gpt4_bot:**

Hello! How can I assist you today?
</details>

It will be rendered as:

Greetings AI

sam:

say hello

gpt4_bot:

Hello! How can I assist you today?

Can I share conversations with any AI bot?

Yes, you can share conversations with any of the AI bots enabled on our forum. Just look for the “Share” icon in the post menu when you’re viewing the conversation.

Is there a limit to how much of the conversation I can share?

There’s no hard limit, but we encourage you to share only the relevant parts of the conversation to keep things concise and focused for other readers.

Where can I share these conversations?

You can share them in a new forum post, in a private message to other users, or even outside the forum—anywhere you can paste text from your clipboard.

Will the shared conversation include any personal information?

No personal information will be included in the shared text. Only the usernames and the content of the messages will be copied.

12 Likes

FWIW, I worked with GPT-4 to create this post (with some minor changes)

Discourse AI Bot: New Sharing Feature AI

sam:

Given:

commit 933784a873c781816857699d6d9138baf9f5a7de
Author: Sam <sam.saffron@gmail.com>
Date:   Fri Dec 29 19:47:47 2023 +1100

    FEATURE: allow easy sharing of bot conversations (#385)
    
    * FEATURE: allow easy sharing of bot conversations
    
    * Lean on new core API i
    
    * Added system spec for copy functionality
    
    
    * Update assets/javascripts/initializers/ai-bot-replies.js
    
    Co-authored-by: Alan Guo Xiang Tan <gxtan1990@gmail.com>
    
    * discourse later insted of setTimeout
    
    * Update spec/system/ai_bot/share_spec.rb
    
    Co-authored-by: Alan Guo Xiang Tan <gxtan1990@gmail.com>
    
    * feedback from review
    
    just check the whole payload
    
    * remove uneeded code
    
    * fix spec
    
    ---------
    
    Co-authored-by: Alan Guo Xiang Tan <gxtan1990@gmail.com>

diff --git a/assets/javascripts/discourse/components/modal/share-modal.gjs b/assets/javascripts/discourse/components/modal/share-modal.gjs
new file mode 100644
index 0000000..b122f62
--- /dev/null
+++ b/assets/javascripts/discourse/components/modal/share-modal.gjs
@@ -0,0 +1,109 @@
+import Component from "@glimmer/component";
+import { tracked } from "@glimmer/tracking";
+import { Input } from "@ember/component";
+import { on } from "@ember/modifier";
+import { action } from "@ember/object";
+import { htmlSafe } from "@ember/template";
+import DButton from "discourse/components/d-button";
+import DModal from "discourse/components/d-modal";
+import discourseLater from "discourse-common/lib/later";
+import I18n from "I18n";
+import copyConversation from "../../lib/copy-conversation";
+
+const t = I18n.t.bind(I18n);
+
+export default class ShareModal extends Component {
+  @tracked contextValue = 1;
+  @tracked htmlContext = "";
+  @tracked maxContext = 0;
+  @tracked allPosts = [];
+  @tracked justCopiedText = "";
+
+  constructor() {
+    super(...arguments);
+
+    const postStream = this.args.model.topic.get("postStream");
+
+    let postNumbers = [];
+    // simpler to understand than Array.from
+    for (let i = 1; i <= this.args.model.post_number; i++) {
+      postNumbers.push(i);
+    }
+
+    this.allPosts = postNumbers
+      .map((postNumber) => {
+        let postId = postStream.findPostIdForPostNumber(postNumber);
+        if (postId) {
+          return postStream.findLoadedPost(postId);
+        }
+      })
+      .filter((post) => post);
+
+    this.maxContext = this.allPosts.length / 2;
+    this.contextValue = 1;
+
+    this.updateHtmlContext();
+  }
+
+  @action
+  updateHtmlContext() {
+    let context = [];
+
+    const start = this.allPosts.length - this.contextValue * 2;
+    for (let i = start; i < this.allPosts.length; i++) {
+      const post = this.allPosts[i];
+      context.push(`<p><b>${post.username}:</b></p>`);
+      context.push(post.cooked);
+    }
+    this.htmlContext = htmlSafe(context.join("\n"));
+  }
+
+  @action
+  async copyContext() {
+    const from =
+      this.allPosts[this.allPosts.length - this.contextValue * 2].post_number;
+    const to = this.args.model.post_number;
+    await copyConversation(this.args.model.topic, from, to);
+    this.justCopiedText = t("discourse_ai.ai_bot.conversation_shared");
+    discourseLater(() => {
+      this.justCopiedText = "";
+    }, 2000);
+  }
+
+  <template>
+    <DModal
+      class="ai-share-modal"
+      @title={{t "discourse_ai.ai_bot.share_modal.title"}}
+      @closeModal={{@closeModal}}
+    >
+      <:body>
+        <div class="ai-share-modal__preview">
+          {{this.htmlContext}}
+        </div>
+      </:body>
+
+      <:footer>
+        <div class="ai-share-modal__slider">
+          <Input
+            @type="range"
+            min="1"
+            max={{this.maxContext}}
+            @value={{this.contextValue}}
+            {{on "change" this.updateHtmlContext}}
+          />
+          <div class="ai-share-modal__context">
+            {{t "discourse_ai.ai_bot.share_modal.context"}}
+            {{this.contextValue}}
+          </div>
+        </div>
+        <DButton
+          class="btn-primary confirm"
+          @icon="copy"
+          @action={{this.copyContext}}
+          @label="discourse_ai.ai_bot.share_modal.copy"
+        />
+        <span class="ai-share-modal__just-copied">{{this.justCopiedText}}</span>
+      </:footer>
+    </DModal>
+  </template>
+}
diff --git a/assets/javascripts/discourse/lib/copy-conversation.js b/assets/javascripts/discourse/lib/copy-conversation.js
new file mode 100644
index 0000000..bcf3188
--- /dev/null
+++ b/assets/javascripts/discourse/lib/copy-conversation.js
@@ -0,0 +1,55 @@
+import { ajax } from "discourse/lib/ajax";
+import { clipboardCopy } from "discourse/lib/utilities";
+import I18n from "discourse-i18n";
+
+export default async function (topic, fromPostNumber, toPostNumber) {
+  const stream = topic.get("postStream");
+
+  let postNumbers = [];
+  // simpler to understand than Array.from
+  for (let i = fromPostNumber; i <= toPostNumber; i++) {
+    postNumbers.push(i);
+  }
+
+  const postIds = postNumbers.map((postNumber) => {
+    return stream.findPostIdForPostNumber(postNumber);
+  });
+
+  // we need raw to construct so post stream will not help
+
+  const url = `/t/${topic.id}/posts.json`;
+  const data = {
+    post_ids: postIds,
+    include_raw: true,
+  };
+
+  const response = await ajax(url, { data });
+
+  let buffer = [];
+  buffer.push("<details class='ai-quote'>");
+  buffer.push("<summary>");
+  buffer.push(`<span>${topic.title}</span>`);
+  buffer.push(
+    `<span title='${I18n.t("discourse_ai.ai_bot.ai_title")}'>${I18n.t(
+      "discourse_ai.ai_bot.ai_label"
+    )}</span>`
+  );
+  buffer.push("</summary>");
+
+  response.post_stream.posts.forEach((post) => {
+    buffer.push("");
+    buffer.push(`**${post.username}:**`);
+    buffer.push("");
+    buffer.push(post.raw);
+  });
+
+  buffer.push("</details>");
+
+  const text = buffer.join("\n");
+
+  if (window.discourseAiTestClipboard) {
+    window.discourseAiClipboard = text;
+  }
+
+  await clipboardCopy(text);
+}
diff --git a/assets/javascripts/initializers/ai-bot-replies.js b/assets/javascripts/initializers/ai-bot-replies.js
index e136963..050e1ed 100644
--- a/assets/javascripts/initializers/ai-bot-replies.js
+++ b/assets/javascripts/initializers/ai-bot-replies.js
@@ -6,6 +6,10 @@ import { withPluginApi } from "discourse/lib/plugin-api";
 import { cook } from "discourse/lib/text";
 import { registerWidgetShim } from "discourse/widgets/render-glimmer";
 import { composeAiBotMessage } from "discourse/plugins/discourse-ai/discourse/lib/ai-bot-helper";
+import ShareModal from "../discourse/components/modal/share-modal";
+import copyConversation from "../discourse/lib/copy-conversation";
+
+const AUTO_COPY_THRESHOLD = 4;
 
 function isGPTBot(user) {
   return user && [-110, -111, -112, -113].includes(user.id);
@@ -166,6 +170,43 @@ function initializePersonaDecorator(api) {
   );
 }
 
+function initializeShareButton(api) {
+  const currentUser = api.getCurrentUser();
+  if (!currentUser || !currentUser.ai_enabled_chat_bots) {
+    return;
+  }
+
+  let shareAiResponse = async function ({ post, showFeedback }) {
+    if (post.post_number <= AUTO_COPY_THRESHOLD) {
+      await copyConversation(post.topic, 1, post.post_number);
+      showFeedback("discourse_ai.ai_bot.conversation_shared");
+    } else {
+      modal.show(ShareModal, { model: post });
+    }
+  };
+
+  api.addPostMenuButton("share", (post) => {
+    // very hacky and ugly, but there is no `.topic` in attrs
+    if (
+      !currentUser.ai_enabled_chat_bots.any(
+        (bot) => post.username === bot.username
+      )
+    ) {
+      return;
+    }
+
+    return {
+      action: shareAiResponse,
+      icon: "share",
+      className: "post-action-menu__share",
+      title: "discourse_ai.ai_bot.share",
+      position: "first",
+    };
+  });
+
+  const modal = api.container.lookup("service:modal");
+}
+
 export default {
   name: "discourse-ai-bot-replies",
 
@@ -179,6 +220,7 @@ export default {
       }
       withPluginApi("1.6.0", initializeAIBotReplies);
       withPluginApi("1.6.0", initializePersonaDecorator);
+      withPluginApi("1.22.0", (api) => initializeShareButton(api, container));
     }
   },
 };
diff --git a/assets/javascripts/lib/discourse-markdown/ai-tags.js b/assets/javascripts/lib/discourse-markdown/ai-tags.js
new file mode 100644
index 0000000..c2d9b67
--- /dev/null
+++ b/assets/javascripts/lib/discourse-markdown/ai-tags.js
@@ -0,0 +1,3 @@
+export function setup(helper) {
+  helper.allowList(["details[class=ai-quote]"]);
+}
diff --git a/assets/stylesheets/modules/ai-bot/common/bot-replies.scss b/assets/stylesheets/modules/ai-bot/common/bot-replies.scss
index 4943b9b..2a8ba80 100644
--- a/assets/stylesheets/modules/ai-bot/common/bot-replies.scss
+++ b/assets/stylesheets/modules/ai-bot/common/bot-replies.scss
@@ -67,3 +67,37 @@ article.streaming nav.post-controls .actions button.cancel-streaming {
   font-size: var(--font-down-1);
   padding-top: 3px;
 }
+
+details.ai-quote {
+  > summary {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    span:first-child {
+      margin-right: auto;
+    }
+    span:nth-child(2) {
+      font-size: var(--font-down-2);
+      background: var(--primary-medium);
+      padding: 2px 6px 0;
+      color: var(--secondary);
+    }
+  }
+}
+
+.ai-share-modal {
+  .d-modal__footer {
+    position: relative;
+    padding: 10px 20px 25px;
+    .btn-primary {
+      margin-left: auto;
+    }
+  }
+  &__just-copied {
+    position: absolute;
+    font-size: var(--font-down-1);
+    right: 20px;
+    bottom: 5px;
+    color: var(--success);
+  }
+}
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml
index e13f953..e2f2e7d 100644
--- a/config/locales/client.en.yml
+++ b/config/locales/client.en.yml
@@ -180,6 +180,16 @@ en:
         cancel_streaming: "Stop reply"
         default_pm_prefix: "[Untitled AI bot PM]"
         shortcut_title: "Start a PM with an AI bot"
+        share: "Share AI conversation"
+        conversation_shared: "Conversation copied to clipboard"
+
+        ai_label: "AI"
+        ai_title: "Conversation with AI"
+
+        share_modal:
+          title: "Share AI conversation"
+          copy: "Copy"
+          context: "Interactions to share:"
 
         bot_names:
           gpt-4: "GPT-4"
diff --git a/spec/system/ai_bot/share_spec.rb b/spec/system/ai_bot/share_spec.rb
new file mode 100644
index 0000000..708e96b
--- /dev/null
+++ b/spec/system/ai_bot/share_spec.rb
@@ -0,0 +1,118 @@
+# frozen_string_literal: true
+RSpec.describe "Share conversation", type: :system do
+  fab!(:admin) { Fabricate(:admin, username: "ai_sharer") }
+  let(:bot_user) { User.find(DiscourseAi::AiBot::EntryPoint::GPT4_ID) }
+
+  let(:pm) do
+    Fabricate(
+      :private_message_topic,
+      title: "This is my special PM",
+      user: admin,
+      topic_allowed_users: [
+        Fabricate.build(:topic_allowed_user, user: admin),
+        Fabricate.build(:topic_allowed_user, user: bot_user),
+      ],
+    )
+  end
+
+  let(:pm_posts) do
+    posts = []
+    i = 1
+    3.times do
+      posts << Fabricate(:post, topic: pm, user: admin, raw: "test test test user reply #{i}")
+      posts << Fabricate(:post, topic: pm, user: bot_user, raw: "test test test bot reply #{i}")
+      i += 1
+    end
+
+    posts
+  end
+
+  before do
+    SiteSetting.ai_bot_enabled = true
+    SiteSetting.ai_bot_enabled_chat_bots = "gpt-4"
+    sign_in(admin)
+
+    bot_user.update!(username: "gpt-4")
+
+    Group.refresh_automatic_groups!
+    pm
+    pm_posts
+  end
+
+  it "can share a conversation" do
+    clip_text = nil
+
+    visit(pm.url)
+
+    # clipboard functionality is extremely hard to test
+    # we would need special permissions in chrome driver to enable full access
+    # instead we use a secret variable to signal that we want to store clipboard
+    # data in window.discourseAiClipboard
+    page.execute_script("window.discourseAiTestClipboard = true")
+
+    find("#post_2 .post-action-menu__share").click
+
+    try_until_success do
+      clip_text = page.evaluate_script("window.discourseAiClipboard")
+      expect(clip_text).to be_present
+    end
+
+    conversation = (<<~TEXT).strip
+      <details class='ai-quote'>
+      <summary>
+      <span>This is my special PM</span>
+      <span title='Conversation with AI'>AI</span>
+      </summary>
+
+      **ai_sharer:**
+
+      test test test user reply 1
+
+      **gpt-4:**
+
+      test test test bot reply 1
+      </details>
+    TEXT
+
+    expect(conversation).to eq(clip_text)
+
+    # Test modal functionality as well
+    page.evaluate_script("window.discourseAiClipboard = null")
+
+    find("#post_6 .post-action-menu__share").click
+    find(".ai-share-modal__slider input").set("2")
+    find(".ai-share-modal button.btn-primary").click
+
+    try_until_success do
+      clip_text = page.evaluate_script("window.discourseAiClipboard")
+      expect(clip_text).to be_present
+    end
+
+    conversation = (<<~TEXT).strip
+      <details class='ai-quote'>
+      <summary>
+      <span>This is my special PM</span>
+      <span title='Conversation with AI'>AI</span>
+      </summary>
+
+      **ai_sharer:**
+
+      test test test user reply 2
+
+      **gpt-4:**
+
+      test test test bot reply 2
+
+      **ai_sharer:**
+
+      test test test user reply 3
+
+      **gpt-4:**
+
+      test test test bot reply 3
+      </details>
+    TEXT
+
+    expect(conversation).to eq(clip_text)
+  end
+end

Explain the new feature in Discourse AI

gpt4_bot:

The new feature introduced in Discourse AI allows users to easily share conversations they’ve had with AI bots within the Discourse platform. This feature is particularly useful for users who want to share the insights or information they’ve received from interacting with AI bots with other users or for their own reference.

Here’s a breakdown of how the feature works and what changes have been made:

  1. Share Modal Component: A new Glimmer component (ShareModal) has been added, which provides a user interface for selecting the range of the conversation to share and copying it to the clipboard. This modal includes a slider to choose how much context from the conversation to include when sharing.

  2. Copy Conversation Functionality: A new JavaScript function (copyConversation) has been implemented to handle the actual copying of the conversation text. It constructs the conversation by fetching the raw posts between specified post numbers and formats it with Markdown, including a details tag (<details class='ai-quote'>) to make the shared content collapsible.

  3. Post Menu Button: A new post menu button (share) has been added that triggers the share functionality. If the conversation is short (up to a certain threshold), it will be automatically copied to the clipboard. For longer conversations, the share modal will be displayed, allowing the user to select the portion of the conversation to share.

  4. Styling: New SCSS styles have been added (bot-replies.scss) to style the share modal and the shared conversation details tag.

  5. Locale Changes: The client.en.yml file has been updated to include new locale strings for the share feature, such as button labels and tooltips.

  6. System Test: A new system test (share_spec.rb) has been added to ensure that the sharing functionality works as expected.

  7. Markdown Whitelisting: The ai-tags.js file has been created to whitelist the custom details[class=ai-quote] tag used for the shared conversation, so it’s allowed in the Discourse Markdown renderer.

  8. Co-authorship: The commit message indicates that Alan Guo Xiang Tan co-authored some of the changes, suggesting a collaborative effort in developing this feature.

Overall, this feature enhances the user experience by making it easier to share valuable interactions with AI bots, fostering collaboration, and knowledge sharing within the Discourse community.

sam:

Try to explain the feature to forum users answering common questions they will have about it.

gpt4_bot:

New Feature Announcement: Share Your AI Bot Conversations!

We’re excited to announce a new feature that allows you to easily share your conversations with AI bots on our forum. Whether you’ve had an insightful, funny, or helpful exchange with one of our AI bots, you can now share that interaction with others in just a few clicks. Here’s what you need to know:

How do I share an AI bot conversation?

When viewing a conversation with an AI bot, you’ll see a new “Share” icon in the post menu. Clicking this icon will give you two options:

  1. For short conversations: If the conversation is within a certain length (up to four interactions), clicking the “Share” icon will automatically copy the entire conversation to your clipboard. You can then paste it wherever you like—be it in a new post, a private message, or outside the forum.

  2. For longer conversations: For more extended interactions, clicking the “Share” icon will open a modal with a preview of your conversation. You can use a slider to select how much of the conversation you want to share. Once you’ve chosen the desired range, click the “Copy” button to copy the selected conversation to your clipboard.

What will the shared conversation look like?

The shared conversation will be formatted in a collapsible “details” block, making it easy to read and manage in posts. It will include the usernames and the content of the messages within the range you’ve selected.

Can I share conversations with any AI bot?

Yes, you can share conversations with any of the AI bots enabled on our forum. Just look for the “Share” icon in the post menu when you’re viewing the conversation.

Is there a limit to how much of the conversation I can share?

There’s no hard limit, but we encourage you to share only the relevant parts of the conversation to keep things concise and focused for other readers.

Where can I share these conversations?

You can share them in a new forum post, in a private message to other users, or even outside the forum—anywhere you can paste text from your clipboard.

Will the shared conversation include any personal information?

No personal information will be included in the shared text. Only the usernames and the content of the messages will be copied.

What if I encounter any issues with the sharing feature?

If you run into any problems or have feedback about the sharing feature, please don’t hesitate to reach out to our support team or post in the feedback section of our forum. We’re here to help and always looking for ways to improve your experience.

We hope you enjoy this new feature and look forward to seeing the interesting conversations you’ll share!

8 Likes