A propósito, trabalhei com o GPT-4 para criar esta publicação (com algumas alterações menores)
Discourse AI Bot: Novo Recurso de Compartilhamento
IA
sam:
Dado:
commit 933784a873c781816857699d6d9138baf9f5a7de
Author: Sam <sam.saffron@gmail.com>
Date: Fri Dec 29 19:47:47 2023 +1100
FEATURE: permitir compartilhamento fácil de conversas de bot (#385)
* FEATURE: permitir compartilhamento fácil de conversas de bot
* Aproveitar a nova API principal i
* Adicionado teste de sistema para funcionalidade de cópia
* Atualizar assets/javascripts/initializers/ai-bot-replies.js
Co-authored-by: Alan Guo Xiang Tan <gxtan1990@gmail.com>
* discourse em vez de setTimeout
* Atualizar spec/system/ai_bot/share_spec.rb
Co-authored-by: Alan Guo Xiang Tan <gxtan1990@gmail.com>
* feedback da revisão
apenas verificar todo o payload
* remover código desnecessário
* corrigir teste
---------
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 = [];
+ // mais simples de entender do que 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 = [];
+ // mais simples de entender do que Array.from
+ for (let i = fromPostNumber; i <= toPostNumber; i++) {
+ postNumbers.push(i);
+ }
+
+ const postIds = postNumbers.map((postNumber) => {
+ return stream.findPostIdForPostNumber(postNumber);
+ });
+
+ // precisamos do raw para construir, então o post stream não ajudará
+
+ 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) => {
+ // muito hacky e feio, mas não há `.topic` em 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: "Parar resposta"
default_pm_prefix: "[PM de bot IA sem título]"
shortcut_title: "Iniciar uma PM com um bot de IA"
+ share: "Compartilhar conversa de IA"
+ conversation_shared: "Conversa copiada para a área de transferência"
+
+ ai_label: "IA"
+ ai_title: "Conversa com IA"
+
+ share_modal:
+ title: "Compartilhar conversa de IA"
+ copy: "Copiar"
+ context: "Interações para compartilhar:"
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 "Compartilhar conversa", 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: "Esta é minha PM especial",
+ 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 resposta do usuário #{i}")
+ posts << Fabricate(:post, topic: pm, user: bot_user, raw: "test test test resposta do bot #{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 "pode compartilhar uma conversa" do
+ clip_text = nil
+
+ visit(pm.url)
+
+ # funcionalidade de área de transferência é extremamente difícil de testar
+ # precisaríamos de permissões especiais no chrome driver para habilitar acesso total
+ # em vez disso, usamos uma variável secreta para sinalizar que queremos armazenar dados
+ # da área de transferência em 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>Esta é minha PM especial</span>
+ <span title='Conversa com IA'>IA</span>
+ </summary>
+
+ **ai_sharer:**
+
+ test test test resposta do usuário 1
+
+ **gpt-4:**
+
+ test test test resposta do bot 1
+ </details>
+ TEXT
+
+ expect(conversation).to eq(clip_text)
+
+ # Testar funcionalidade do modal também
+ 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>Esta é minha PM especial</span>
+ <span title='Conversa com IA'>IA</span>
+ </summary>
+
+ **ai_sharer:**
+
+ test test test resposta do usuário 2
+
+ **gpt-4:**
+
+ test test test resposta do bot 2
+
+ **ai_sharer:**
+
+ test test test resposta do usuário 3
+
+ **gpt-4:**
+
+ test test test resposta do bot 3
+ </details>
+ TEXT
+
+ expect(conversation).to eq(clip_text)
+ end
+end
Explique o novo recurso no Discourse AI
gpt4_bot:
O novo recurso introduzido no Discourse AI permite que os usuários compartilhem facilmente as conversas que tiveram com bots de IA dentro da plataforma Discourse. Este recurso é particularmente útil para usuários que desejam compartilhar as ideias ou informações recebidas ao interagir com bots de IA com outros usuários ou para sua própria referência.
Aqui está uma explicação de como o recurso funciona e quais alterações foram feitas:
-
Componente Modal de Compartilhamento: Um novo componente Glimmer (ShareModal) foi adicionado, que fornece uma interface do usuário para selecionar o intervalo da conversa a ser compartilhada e copiá-la para a área de transferência. Este modal inclui um controle deslizante para escolher quanto contexto da conversa incluir ao compartilhar.
-
Funcionalidade de Copiar Conversa: Uma nova função JavaScript (copyConversation) foi implementada para lidar com a cópia real do texto da conversa. Ela constrói a conversa buscando as postagens brutas entre os números de postagem especificados e a formata com Markdown, incluindo uma tag details (<details class='ai-quote'>) para tornar o conteúdo compartilhado colapsável.
-
Botão do Menu da Postagem: Um novo botão no menu da postagem (share) foi adicionado, que aciona a funcionalidade de compartilhamento. Se a conversa for curta (até um certo limite), ela será automaticamente copiada para a área de transferência. Para conversas mais longas, o modal de compartilhamento será exibido, permitindo que o usuário selecione a parte da conversa a ser compartilhada.
-
Estilização: Novos estilos SCSS foram adicionados (bot-replies.scss) para estilizar o modal de compartilhamento e a tag details da conversa compartilhada.
-
Alterações de Localidade: O arquivo client.en.yml foi atualizado para incluir novas strings de localidade para o recurso de compartilhamento, como rótulos de botões e dicas de ferramenta.
-
Teste de Sistema: Um novo teste de sistema (share_spec.rb) foi adicionado para garantir que a funcionalidade de compartilhamento funcione conforme o esperado.
-
Lista Branca de Markdown: O arquivo ai-tags.js foi criado para permitir a tag personalizada details[class=ai-quote] usada para a conversa compartilhada, para que seja permitida no renderizador Markdown do Discourse.
-
Coautoria: A mensagem do commit indica que Alan Guo Xiang Tan co-autorizou algumas das alterações, sugerindo um esforço colaborativo no desenvolvimento deste recurso.
No geral, este recurso melhora a experiência do usuário, facilitando o compartilhamento de interações valiosas com bots de IA, promovendo a colaboração e o compartilhamento de conhecimento dentro da comunidade Discourse.
sam:
Tente explicar o recurso para os usuários do fórum, respondendo às perguntas comuns que eles terão sobre ele.
gpt4_bot:
Anúncio do Novo Recurso: Compartilhe Suas Conversas com Bots de IA!
Estamos entusiasmados em anunciar um novo recurso que permite que você compartilhe facilmente suas conversas com bots de IA em nosso fórum. Se você teve uma troca esclarecedora, engraçada ou útil com um de nossos bots de IA, agora pode compartilhar essa interação com outros em apenas alguns cliques. Aqui está o que você precisa saber:
Como compartilhar uma conversa com bot de IA?
Ao visualizar uma conversa com um bot de IA, você verá um novo ícone de “Compartilhar” no menu da postagem. Clicar neste ícone oferecerá duas opções:
-
Para conversas curtas: Se a conversa estiver dentro de um determinado comprimento (até quatro interações), clicar no ícone de “Compartilhar” copiará automaticamente toda a conversa para sua área de transferência. Você poderá então colá-la onde quiser — seja em uma nova postagem, uma mensagem privada ou fora do fórum.
-
Para conversas mais longas: Para interações mais extensas, clicar no ícone de “Compartilhar” abrirá um modal com uma prévia da sua conversa. Você poderá usar um controle deslizante para selecionar quanto da conversa deseja compartilhar. Após escolher o intervalo desejado, clique no botão “Copiar” para copiar a conversa selecionada para sua área de transferência.
Como ficará a conversa compartilhada?
A conversa compartilhada será formatada em um bloco “details” colapsável, tornando-a fácil de ler e gerenciar nas postagens. Ela incluirá os nomes de usuário e o conteúdo das mensagens dentro do intervalo que você selecionou.
Posso compartilhar conversas com qualquer bot de IA?
Sim, você pode compartilhar conversas com qualquer um dos bots de IA habilitados em nosso fórum. Basta procurar o ícone de “Compartilhar” no menu da postagem ao visualizar a conversa.
Existe um limite para quanto da conversa posso compartilhar?
Não há um limite rígido, mas recomendamos que você compartilhe apenas as partes relevantes da conversa para manter as coisas concisas e focadas para outros leitores.
Onde posso compartilhar essas conversas?
Você pode compartilhá-las em uma nova postagem no fórum, em uma mensagem privada para outros usuários ou até mesmo fora do fórum — em qualquer lugar onde possa colar texto de sua área de transferência.
A conversa compartilhada incluirá alguma informação pessoal?
Nenhuma informação pessoal será incluída no texto compartilhado. Apenas os nomes de usuário e o conteúdo das mensagens serão copiados.
E se eu encontrar algum problema com o recurso de compartilhamento?
Se você tiver algum problema ou tiver feedback sobre o recurso de compartilhamento, não hesite em entrar em contato com nossa equipe de suporte ou postar na seção de feedback de nosso fórum. Estamos aqui para ajudar e sempre buscamos maneiras de melhorar sua experiência.
Esperamos que você goste deste novo recurso e aguardamos com expectativa para ver as conversas interessantes que você compartilhará!