Por si acaso, trabajé con GPT-4 para crear esta publicación (con algunos cambios menores)
Bot de IA de Discourse: Nueva función de compartir
IA
sam:
Dado:
commit 933784a873c781816857699d6d9138baf9f5a7de
Author: Sam <sam.saffron@gmail.com>
Date: Fri Dec 29 19:47:47 2023 +1100
FEATURE: permitir el fácil intercambio de conversaciones de bots (#385)
* FEATURE: permitir el fácil intercambio de conversaciones de bots
* Aprovechar la nueva API central i
* Se agregó una especificación del sistema para la funcionalidad de copiar
* Actualizar assets/javascripts/initializers/ai-bot-replies.js
Co-authored-by: Alan Guo Xiang Tan <gxtan1990@gmail.com>
* discourse más tarde en lugar de setTimeout
* Actualizar spec/system/ai_bot/share_spec.rb
Co-authored-by: Alan Guo Xiang Tan <gxtan1990@gmail.com>
* retroalimentación de la revisión
solo verifica todo el payload
* eliminar código innecesario
* arreglar la especificación
---------
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 = [];
+ // más simple de entender 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 = [];
+ // más simple de entender que Array.from
+ for (let i = fromPostNumber; i <= toPostNumber; i++) {
+ postNumbers.push(i);
+ }
+
+ const postIds = postNumbers.map((postNumber) => {
+ return stream.findPostIdForPostNumber(postNumber);
+ });
+
+ // necesitamos raw para construir, por lo que el postStream no ayudará
+
+ 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) => {
+ // muy hacky y feo, pero no hay `.topic` en 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: "Detener respuesta"
default_pm_prefix: "[PM de bot de IA sin título]"
shortcut_title: "Iniciar un PM con un bot de IA"
+ share: "Compartir conversación de IA"
+ conversation_shared: "Conversación copiada al portapapeles"
+
+ ai_label: "IA"
+ ai_title: "Conversación con IA"
+
+ share_modal:
+ title: "Compartir conversación de IA"
+ copy: "Copiar"
+ context: "Interacciones a compartir:"
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 "Compartir conversación", 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: "Este es mi 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: "prueba prueba prueba respuesta de usuario #{i}")
+ posts << Fabricate(:post, topic: pm, user: bot_user, raw: "prueba prueba prueba respuesta de 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 "puede compartir una conversación" do
+ clip_text = nil
+
+ visit(pm.url)
+
+ # la funcionalidad del portapapeles es extremadamente difícil de probar
+ # necesitaríamos permisos especiales en el controlador de Chrome para habilitar el acceso completo
+ # en su lugar, usamos una variable secreta para señalar que queremos almacenar los datos del portapapeles
+ # en 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>Este es mi PM especial</span>
+ <span title='Conversación con IA'>IA</span>
+ </summary>
+
+ **ai_sharer:**
+
+ prueba prueba prueba respuesta de usuario 1
+
+ **gpt-4:**
+
+ prueba prueba prueba respuesta de bot 1
+ </details>
+ TEXT
+
+ expect(conversation).to eq(clip_text)
+
+ # Probar también la funcionalidad del modal
+ 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>Este es mi PM especial</span>
+ <span title='Conversación con IA'>IA</span>
+ </summary>
+
+ **ai_sharer:**
+
+ prueba prueba prueba respuesta de usuario 2
+
+ **gpt-4:**
+
+ prueba prueba prueba respuesta de bot 2
+
+ **ai_sharer:**
+
+ prueba prueba prueba respuesta de usuario 3
+
+ **gpt-4:**
+
+ prueba prueba prueba respuesta de bot 3
+ </details>
+ TEXT
+
+ expect(conversation).to eq(clip_text)
+ end
+end
Explica la nueva función en Discourse AI
gpt4_bot:
La nueva función introducida en Discourse AI permite a los usuarios compartir fácilmente las conversaciones que han tenido con bots de IA dentro de la plataforma Discourse. Esta función es particularmente útil para los usuarios que desean compartir las ideas o información que han recibido al interactuar con bots de IA con otros usuarios o para su propia referencia.
Aquí hay un desglose de cómo funciona la función y qué cambios se han realizado:
-
Componente Modal de Compartir: Se ha agregado un nuevo componente Glimmer (ShareModal), que proporciona una interfaz de usuario para seleccionar el rango de la conversación a compartir y copiarla al portapapeles. Este modal incluye un control deslizante para elegir cuánto contexto de la conversación incluir al compartir.
-
Funcionalidad de Copiar Conversación: Se ha implementado una nueva función de JavaScript (copyConversation) para manejar la copia real del texto de la conversación. Construye la conversación obteniendo las publicaciones sin formato entre los números de publicación especificados y la formatea con Markdown, incluyendo una etiqueta details (<details class='ai-quote'>) para hacer que el contenido compartido sea colapsable.
-
Botón del Menú de Publicación: Se ha agregado un nuevo botón en el menú de publicación (share) que activa la función de compartir. Si la conversación es corta (hasta cierto umbral), se copiará automáticamente al portapapeles. Para conversaciones más largas, se mostrará el modal de compartir, permitiendo al usuario seleccionar la parte de la conversación que desea compartir.
-
Estilos: Se han agregado nuevos estilos SCSS (bot-replies.scss) para dar estilo al modal de compartir y a la etiqueta details de la conversación compartida.
-
Cambios en la Localización: El archivo client.en.yml se ha actualizado para incluir nuevas cadenas de localización para la función de compartir, como etiquetas de botones y descripciones emergentes.
-
Prueba del Sistema: Se ha agregado una nueva prueba del sistema (share_spec.rb) para asegurar que la funcionalidad de compartir funcione como se espera.
-
Lista Blanca de Markdown: Se ha creado el archivo ai-tags.js para agregar a la lista blanca la etiqueta personalizada details[class=ai-quote] utilizada para la conversación compartida, de modo que esté permitida en el renderizador Markdown de Discourse.
-
Coautoría: El mensaje del commit indica que Alan Guo Xiang Tan co-autorizó algunos de los cambios, lo que sugiere un esfuerzo colaborativo en el desarrollo de esta función.
En general, esta función mejora la experiencia del usuario al facilitar el intercambio de interacciones valiosas con bots de IA, fomentando la colaboración y el intercambio de conocimientos dentro de la comunidad de Discourse.
sam:
Intenta explicar la función a los usuarios del foro respondiendo a las preguntas comunes que tendrán sobre ella.
gpt4_bot:
Anuncio de nueva función: ¡Comparte tus conversaciones con el bot de IA!
Estamos emocionados de anunciar una nueva función que te permite compartir fácilmente tus conversaciones con bots de IA en nuestro foro. Ya sea que hayas tenido un intercambio reflexivo, divertido o útil con uno de nuestros bots de IA, ahora puedes compartir esa interacción con otros en solo unos pocos clics. Esto es lo que necesitas saber:
¿Cómo comparto una conversación con un bot de IA?
Al ver una conversación con un bot de IA, verás un nuevo icono de “Compartir” en el menú de la publicación. Al hacer clic en este icono, tendrás dos opciones:
-
Para conversaciones cortas: Si la conversación está dentro de cierta longitud (hasta cuatro interacciones), hacer clic en el icono de “Compartir” copiará automáticamente toda la conversación a tu portapapeles. Luego puedes pegarla donde quieras, ya sea en una nueva publicación, un mensaje privado o fuera del foro.
-
Para conversaciones más largas: Para interacciones más extensas, hacer clic en el icono de “Compartir” abrirá un modal con una vista previa de tu conversación. Puedes usar un control deslizante para seleccionar cuánto de la conversación quieres compartir. Una vez que hayas elegido el rango deseado, haz clic en el botón “Copiar” para copiar la conversación seleccionada a tu portapapeles.
¿Cómo se verá la conversación compartida?
La conversación compartida se formateará en un bloque colapsable “details”, lo que facilitará su lectura y gestión en las publicaciones. Incluirá los nombres de usuario y el contenido de los mensajes dentro del rango que hayas seleccionado.
¿Puedo compartir conversaciones con cualquier bot de IA?
Sí, puedes compartir conversaciones con cualquiera de los bots de IA habilitados en nuestro foro. Solo busca el icono de “Compartir” en el menú de la publicación cuando estés viendo la conversación.
¿Hay un límite en cuánto de la conversación puedo compartir?
No hay un límite estricto, pero te recomendamos compartir solo las partes relevantes de la conversación para mantener las cosas concisas y enfocadas para otros lectores.
¿Dónde puedo compartir estas conversaciones?
Puedes compartirlas en una nueva publicación del foro, en un mensaje privado a otros usuarios o incluso fuera del foro, en cualquier lugar donde puedas pegar texto desde tu portapapeles.
¿Incluirá la conversación compartida información personal?
No se incluirá información personal en el texto compartido. Solo se copiarán los nombres de usuario y el contenido de los mensajes.
¿Qué hago si tengo algún problema con la función de compartir?
Si encuentras algún problema o tienes comentarios sobre la función de compartir, no dudes en contactar a nuestro equipo de soporte o publicar en la sección de comentarios de nuestro foro. Estamos aquí para ayudarte y siempre buscamos formas de mejorar tu experiencia.
Esperamos que disfrutes esta nueva función y esperamos ver las interesantes conversaciones que compartirás!