À titre d’information, j’ai travaillé avec GPT-4 pour créer ce message (avec quelques modifications mineures)
Discourse AI Bot : Nouvelle fonctionnalité de partage
IA
sam:
Étant donné :
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
Expliquer la nouvelle fonctionnalité de Discourse AI
gpt4_bot:
La nouvelle fonctionnalité introduite dans Discourse AI permet aux utilisateurs de partager facilement les conversations qu’ils ont eues avec des bots IA au sein de la plateforme Discourse. Cette fonctionnalité est particulièrement utile pour les utilisateurs qui souhaitent partager les idées ou les informations qu’ils ont obtenues grâce à leurs interactions avec des bots IA avec d’autres utilisateurs ou pour leur propre référence.
Voici un aperçu du fonctionnement de la fonctionnalité et des modifications apportées :
-
Composant de la fenêtre modale de partage : Un nouveau composant Glimmer (ShareModal) a été ajouté, offrant une interface utilisateur pour sélectionner la plage de conversation à partager et la copier dans le presse-papiers. Cette fenêtre modale inclut un curseur permettant de choisir la quantité de contexte de la conversation à inclure lors du partage.
-
Fonctionnalité de copie de conversation : Une nouvelle fonction JavaScript (copyConversation) a été mise en œuvre pour gérer la copie réelle du texte de la conversation. Elle construit la conversation en récupérant les messages bruts entre les numéros de message spécifiés et la formate en Markdown, en incluant une balise details (<details class='ai-quote'>) pour rendre le contenu partagé repliable.
-
Bouton du menu des messages : Un nouveau bouton de menu de message (share) a été ajouté pour déclencher la fonctionnalité de partage. Si la conversation est courte (jusqu’à un certain seuil), elle sera automatiquement copiée dans le presse-papiers. Pour les conversations plus longues, la fenêtre modale de partage s’affichera, permettant à l’utilisateur de sélectionner la partie de la conversation à partager.
-
Styles : De nouveaux styles SCSS ont été ajoutés (bot-replies.scss) pour styliser la fenêtre modale de partage et la balise details de la conversation partagée.
-
Modifications des paramètres régionaux : Le fichier client.en.yml a été mis à jour pour inclure de nouvelles chaînes de paramètres régionaux pour la fonctionnalité de partage, telles que les libellés de boutons et les infobulles.
-
Test système : Un nouveau test système (share_spec.rb) a été ajouté pour s’assurer que la fonctionnalité de partage fonctionne comme prévu.
-
Liste blanche Markdown : Le fichier ai-tags.js a été créé pour autoriser la balise personnalisée details[class=ai-quote] utilisée pour la conversation partagée, afin qu’elle soit acceptée par le moteur de rendu Markdown de Discourse.
-
Co-auteur : Le message de commit indique qu’Alan Guo Xiang Tan a co-signé certaines des modifications, suggérant un effort collaboratif dans le développement de cette fonctionnalité.
Dans l’ensemble, cette fonctionnalité améliore l’expérience utilisateur en facilitant le partage d’interactions précieuses avec des bots IA, favorisant ainsi la collaboration et le partage des connaissances au sein de la communauté Discourse.
sam:
Essayez d’expliquer la fonctionnalité aux utilisateurs du forum en répondant aux questions courantes qu’ils pourraient se poser à ce sujet.
gpt4_bot:
Annonce de la nouvelle fonctionnalité : Partagez vos conversations avec des bots IA !
Nous sommes ravis d’annoncer une nouvelle fonctionnalité qui vous permet de partager facilement vos conversations avec des bots IA sur notre forum. Que vous ayez eu un échange instructif, amusant ou utile avec l’un de nos bots IA, vous pouvez maintenant partager cette interaction avec d’autres en quelques clics. Voici ce que vous devez savoir :
Comment partager une conversation avec un bot IA ?
Lorsque vous consultez une conversation avec un bot IA, vous verrez une nouvelle icône « Partager » dans le menu du message. En cliquant sur cette icône, vous aurez deux options :
-
Pour les conversations courtes : Si la conversation est d’une certaine longueur (jusqu’à quatre interactions), cliquer sur l’icône « Partager » copiera automatiquement l’intégralité de la conversation dans votre presse-papiers. Vous pourrez ensuite la coller où vous le souhaitez, que ce soit dans un nouveau message, un message privé ou en dehors du forum.
-
Pour les conversations plus longues : Pour les interactions plus étendues, cliquer sur l’icône « Partager » ouvrira une fenêtre modale avec un aperçu de votre conversation. Vous pourrez utiliser un curseur pour sélectionner la partie de la conversation que vous souhaitez partager. Une fois la plage souhaitée choisie, cliquez sur le bouton « Copier » pour copier la conversation sélectionnée dans votre presse-papiers.
À quoi ressemblera la conversation partagée ?
La conversation partagée sera formatée dans un bloc « details » repliable, ce qui la rendra facile à lire et à gérer dans les messages. Elle inclura les noms d’utilisateur et le contenu des messages dans la plage que vous avez sélectionnée.
Puis-je partager des conversations avec n’importe quel bot IA ?
Oui, vous pouvez partager des conversations avec n’importe quel bot IA activé sur notre forum. Recherchez simplement l’icône « Partager » dans le menu du message lorsque vous consultez la conversation.
Y a-t-il une limite à la quantité de conversation que je peux partager ?
Il n’y a pas de limite stricte, mais nous vous encourageons à partager uniquement les parties pertinentes de la conversation pour rester concis et ciblé pour les autres lecteurs.
Où puis-je partager ces conversations ?
Vous pouvez les partager dans un nouveau message du forum, dans un message privé à d’autres utilisateurs, ou même en dehors du forum — partout où vous pouvez coller du texte depuis votre presse-papiers.
La conversation partagée inclura-t-elle des informations personnelles ?
Aucune information personnelle ne sera incluse dans le texte partagé. Seuls les noms d’utilisateur et le contenu des messages seront copiés.
Que faire si je rencontre des problèmes avec la fonctionnalité de partage ?
Si vous rencontrez des problèmes ou si vous avez des commentaires sur la fonctionnalité de partage, n’hésitez pas à contacter notre équipe de support ou à poster dans la section de commentaires de notre forum. Nous sommes là pour vous aider et cherchons toujours à améliorer votre expérience.
Nous espérons que vous apprécierez cette nouvelle fonctionnalité et nous avons hâte de voir les conversations intéressantes que vous partagerez !