Поделитесь своими диалогами с AI-ботами!

Мы рады сообщить о новой функции, которая позволяет вам легко делиться своими разговорами с ИИ-ботами на нашем форуме. Будь то insightful, забавный или полезный обмен с одним из наших ИИ-ботов, теперь вы можете поделиться этим взаимодействием с другими всего за несколько кликов. Вот что вам нужно знать:

Как поделиться разговором с ИИ-ботом?

При просмотре разговора с ИИ-ботом вы увидите новый значок «Поделиться» в меню сообщения.

Нажатие работает следующим образом:

  1. Для коротких разговоров: Если разговор укладывается в определённую длину (до 2 взаимодействий), нажатие значка «Поделиться» автоматически скопирует весь разговор в ваш буфер обмена. Затем вы сможете вставить его куда угодно — будь то новое сообщение, личное сообщение или вне форума.

  2. Для длинных разговоров: Для более протяжённых взаимодействий нажатие значка «Поделиться» откроет модальное окно с предварительным просмотром вашего разговора. Вы сможете использовать ползунок, чтобы выбрать, какую часть разговора вы хотите поделиться. После выбора нужного диапазона нажмите кнопку «Копировать», чтобы скопировать выбранный разговор в ваш буфер обмена.

Как будет выглядеть общий разговор?

Общий разговор будет отформатирован в виде сворачиваемого блока «details», что упростит его чтение и управление в сообщениях. Он будет включать имена пользователей и содержание сообщений в выбранном вами диапазоне.

Пример:

<details class='ai-quote'>
<summary>
<span>Приветствие</span>
<span title='Разговор с ИИ'>ИИ</span>
</summary>

**sam:**

скажи привет

**gpt4_bot:**

Привет! Чем я могу вам помочь сегодня?
</details>

Это будет отображено так:

Приветствие ИИ

sam:

скажи привет

gpt4_bot:

Привет! Чем я могу вам помочь сегодня?

Могу ли я делиться разговорами с любым ИИ-ботом?

Да, вы можете делиться разговорами с любым из ИИ-ботов, включённых на нашем форуме. Просто ищите значок «Поделиться» в меню сообщения, когда просматриваете разговор.

Есть ли ограничение на то, сколько части разговора я могу поделиться?

Жёсткого ограничения нет, но мы рекомендуем делиться только релевантными частями разговора, чтобы сохранить краткость и фокус для других читателей.

Где я могу делиться этими разговорами?

Вы можете делиться ими в новом сообщении на форуме, в личном сообщении другим пользователям или даже вне форума — везде, куда можно вставить текст из вашего буфера обмена.

Будет ли в общем разговоре содержаться какая-либо личная информация?

Нет, в общем тексте не будет содержаться никакой личной информации. Будут скопированы только имена пользователей и содержание сообщений.

14 лайков

Кстати, я работал с GPT-4, чтобы создать этот пост (с некоторыми незначительными правками)

Discourse AI Bot: Новая функция обмена ИИ

sam:

Дано:

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 = [];
+    // проще для понимания, чем 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 = [];
+  // проще для понимания, чем Array.from
+  for (let i = fromPostNumber; i <= toPostNumber; i++) {
+    postNumbers.push(i);
+  }
+
+  const postIds = postNumbers.map((postNumber) => {
+    return stream.findPostIdForPostNumber(postNumber);
+  });
+
+  // нам нужен raw для построения, поэтому postStream не поможет
+
+  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) => {
+    // очень хакерское и неуклюжее решение, но в attrs нет `.topic`
+    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)
+
+    # функционал буфера обмена крайне сложно тестировать
+    # нам потребовались бы специальные разрешения в chrome driver для полного доступа
+    # вместо этого мы используем секретную переменную, чтобы сигнализировать о необходимости
+    # сохранения данных буфера обмена в 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)
+
+    # Также тестируем функционал модального окна
+    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

Объясните новую функцию в Discourse AI

gpt4_bot:

Новая функция, представленная в Discourse AI, позволяет пользователям легко обмениваться диалогами, которые они вели с ботами искусственного интеллекта на платформе Discourse. Эта функция особенно полезна для тех, кто хочет поделиться полученными от взаимодействия с ИИ-ботами инсайтами или информацией с другими пользователями или сохранить их для себя.

Вот подробное описание того, как работает функция и какие изменения были внесены:

  1. Компонент модального окна обмена: Добавлен новый компонент Glimmer (ShareModal), предоставляющий интерфейс для выбора диапазона диалога для обмена и копирования его в буфер обмена. Это модальное окно включает ползунок для выбора того, какой объем контекста из диалога включить при обмене.

  2. Функциональность копирования диалога: Реализована новая JavaScript-функция (copyConversation) для обработки самого процесса копирования текста диалога. Она формирует диалог, получая необработанные сообщения между указанными номерами постов, и форматирует их в Markdown, включая тег details (<details class='ai-quote'>), чтобы сделать общий контент сворачиваемым.

  3. Кнопка в меню поста: Добавлена новая кнопка в меню поста (share), запускающая функцию обмена. Если диалог короткий (до определенного порога), он автоматически копируется в буфер обмена. Для более длинных диалогов открывается модальное окно обмена, позволяющее пользователю выбрать часть диалога для публикации.

  4. Стилизация: В файл bot-replies.scss добавлены новые стили SCSS для оформления модального окна обмена и тега details для общих диалогов.

  5. Изменения в локализации: Файл client.en.yml обновлен новыми строками локализации для функции обмена, такими как подписи к кнопкам и подсказки.

  6. Системный тест: Добавлен новый системный тест (share_spec.rb) для проверки того, что функционал обмена работает корректно.

  7. Белый список Markdown: Создан файл ai-tags.js для добавления в белый список пользовательского тега details[class=ai-quote], используемого для общих диалогов, чтобы он разрешался в рендерере Markdown Discourse.

  8. Соавторство: Сообщение коммита указывает на то, что Алан Гуо Сян Тан был соавтором некоторых изменений, что свидетельствует о совместной разработке этой функции.

В целом, эта функция улучшает пользовательский опыт, облегчая обмен ценными взаимодействиями с ИИ-ботами, способствуя сотрудничеству и обмену знаниями внутри сообщества Discourse.

sam:

Попробуйте объяснить функцию пользователям форума, ответив на распространенные вопросы о ней.

gpt4_bot:

Объявление о новой функции: Делитесь своими диалогами с ИИ-ботами!

Мы рады сообщить о новой функции, которая позволяет вам легко делиться своими диалогами с ИИ-ботами на нашем форуме. Будь то содержательный, забавный или полезный обмен мнениями с одним из наших ИИ-ботов, теперь вы можете поделиться этим взаимодействием с другими всего в несколько кликов. Вот что вам нужно знать:

Как поделиться диалогом с ИИ-ботом?

При просмотре диалога с ИИ-ботом вы увидите новую иконку «Поделиться» в меню поста. Нажатие на эту иконку предоставит вам два варианта:

  1. Для коротких диалогов: Если диалог укладывается в определенную длину (до четырех взаимодействий), нажатие иконки «Поделиться» автоматически скопирует весь диалог в ваш буфер обмена. Затем вы можете вставить его куда угодно — будь то новый пост, личное сообщение или за пределами форума.

  2. Для длинных диалогов: Для более протяженных взаимодействий нажатие иконки «Поделиться» откроет модальное окно с предпросмотром вашего диалога. Вы можете использовать ползунок, чтобы выбрать, какую часть диалога вы хотите поделиться. После выбора желаемого диапазона нажмите кнопку «Копировать», чтобы скопировать выбранный диалог в буфер обмена.

Как будет выглядеть общий диалог?

Общий диалог будет отформатирован в виде сворачиваемого блока «details», что делает его удобным для чтения и управления в постах. Он будет включать имена пользователей и содержание сообщений в выбранном вами диапазоне.

Могу ли я делиться диалогами с любым ИИ-ботом?

Да, вы можете делиться диалогами с любым из ИИ-ботов, включенных на нашем форуме. Просто ищите иконку «Поделиться» в меню поста, когда просматриваете диалог.

Есть ли ограничение на объем диалога, которым я могу поделиться?

Жесткого ограничения нет, но мы рекомендуем делиться только релевантными частями диалога, чтобы сохранить краткость и фокус для других читателей.

Где я могу делиться этими диалогами?

Вы можете делиться ими в новом посте форума, в личном сообщении другим пользователям или даже за пределами форума — везде, где можно вставить текст из буфера обмена.

Будет ли в общем диалоге содержаться какая-либо личная информация?

Личная информация в общем тексте не будет содержаться. Будут скопированы только имена пользователей и содержание сообщений.

Что делать, если у меня возникнут проблемы с функцией обмена?

Если вы столкнетесь с какими-либо проблемами или у вас есть отзывы о функции обмена, не стесняйтесь обращаться в нашу службу поддержки или опубликовать сообщение в разделе обратной связи нашего форума. Мы здесь, чтобы помочь, и всегда ищем способы улучшить ваш опыт.

Надеемся, вам понравится эта новая функция, и мы с нетерпением ждем интересных диалогов, которыми вы поделитесь!

12 лайков