From 9df7f50ebc0681ddcec241222f14e8724518fbb6 Mon Sep 17 00:00:00 2001 From: Mark VanLandingham Date: Fri, 28 Feb 2025 20:03:16 -0600 Subject: [PATCH 1/2] Mega WIP version --- .../ai_bot/conversations_controller.rb | 29 + .../components/ai-bot-header-icon.gjs | 4 + .../ai-bot-sidebar-new-conversation.gjs | 23 + .../ai-bot-conversation.gjs | 15 + .../discourse-ai-bot-conversations.js | 33 ++ .../discourse-ai-bot-dashboard-route-map.js | 5 + .../lib/simple-textarea-interactor.js | 29 + .../routes/discourse-ai-bot-conversations.js | 3 + .../ai-bot-conversations-hidden-submit.js | 59 ++ .../discourse-ai-bot-conversations.hbs | 25 + .../initializers/ai-bot-sidebar.js | 140 +++++ .../modules/ai-bot-conversations/common.scss | 506 ++++++++++++++++++ config/routes.rb | 4 + config/settings.yml | 4 + plugin.rb | 2 + 15 files changed, 881 insertions(+) create mode 100644 app/controllers/discourse_ai/ai_bot/conversations_controller.rb create mode 100644 assets/javascripts/discourse/components/ai-bot-sidebar-new-conversation.gjs create mode 100644 assets/javascripts/discourse/connectors/topic-above-post-stream/ai-bot-conversation.gjs create mode 100644 assets/javascripts/discourse/controllers/discourse-ai-bot-conversations.js create mode 100644 assets/javascripts/discourse/discourse-ai-bot-dashboard-route-map.js create mode 100644 assets/javascripts/discourse/lib/simple-textarea-interactor.js create mode 100644 assets/javascripts/discourse/routes/discourse-ai-bot-conversations.js create mode 100644 assets/javascripts/discourse/services/ai-bot-conversations-hidden-submit.js create mode 100644 assets/javascripts/discourse/templates/discourse-ai-bot-conversations.hbs create mode 100644 assets/javascripts/initializers/ai-bot-sidebar.js create mode 100644 assets/stylesheets/modules/ai-bot-conversations/common.scss diff --git a/app/controllers/discourse_ai/ai_bot/conversations_controller.rb b/app/controllers/discourse_ai/ai_bot/conversations_controller.rb new file mode 100644 index 000000000..8b0614772 --- /dev/null +++ b/app/controllers/discourse_ai/ai_bot/conversations_controller.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module DiscourseAi + module AiBot + class ConversationsController < ::ApplicationController + requires_plugin ::DiscourseAi::PLUGIN_NAME + requires_login + + def index + # Step 1: Retrieve all AI bot user IDs + bot_user_ids = EntryPoint.all_bot_ids + + # Step 2: Query for PM topics including current_user and any bot ID + pms = + Topic + .joins(:topic_users) + .private_messages + .where("topic_users.user_id IN (?)", bot_user_ids + [current_user.id]) + .group("topics.id") # Group by topic to ensure distinct results + .having("COUNT(topic_users.user_id) > 1") # Ensure multiple participants in the PM + + # Step 3: Serialize (empty array if no results) + serialized_pms = serialize_data(pms, BasicTopicSerializer) + + render json: serialized_pms, status: 200 + end + end + end +end diff --git a/assets/javascripts/discourse/components/ai-bot-header-icon.gjs b/assets/javascripts/discourse/components/ai-bot-header-icon.gjs index 1907f5172..3cbda25cd 100644 --- a/assets/javascripts/discourse/components/ai-bot-header-icon.gjs +++ b/assets/javascripts/discourse/components/ai-bot-header-icon.gjs @@ -9,6 +9,7 @@ export default class AiBotHeaderIcon extends Component { @service currentUser; @service siteSettings; @service composer; + @service router; get bots() { const availableBots = this.currentUser.ai_enabled_chat_bots @@ -24,6 +25,9 @@ export default class AiBotHeaderIcon extends Component { @action compose() { + if (this.siteSettings.ai_enable_experimental_bot_ux) { + return this.router.transitionTo("discourse-ai-bot-conversations"); + } composeAiBotMessage(this.bots[0], this.composer); } diff --git a/assets/javascripts/discourse/components/ai-bot-sidebar-new-conversation.gjs b/assets/javascripts/discourse/components/ai-bot-sidebar-new-conversation.gjs new file mode 100644 index 000000000..91587601e --- /dev/null +++ b/assets/javascripts/discourse/components/ai-bot-sidebar-new-conversation.gjs @@ -0,0 +1,23 @@ +import Component from "@glimmer/component"; +import { service } from "@ember/service"; +import DButton from "discourse/components/d-button"; +import { i18n } from "discourse-i18n"; + +export default class AiBotSidebarNewConversation extends Component { + @service router; + + get show() { + return this.router.currentRouteName !== "discourse-ai-bot-conversations"; + } + + +} diff --git a/assets/javascripts/discourse/connectors/topic-above-post-stream/ai-bot-conversation.gjs b/assets/javascripts/discourse/connectors/topic-above-post-stream/ai-bot-conversation.gjs new file mode 100644 index 000000000..4964ad8f0 --- /dev/null +++ b/assets/javascripts/discourse/connectors/topic-above-post-stream/ai-bot-conversation.gjs @@ -0,0 +1,15 @@ +import bodyClass from "discourse/helpers/body-class"; +import Component from "@glimmer/component"; + +export default class AiBotConversaion extends Component { + get show() { + return this.args.outletArgs.model?.ai_persona_name + } + + +} + diff --git a/assets/javascripts/discourse/controllers/discourse-ai-bot-conversations.js b/assets/javascripts/discourse/controllers/discourse-ai-bot-conversations.js new file mode 100644 index 000000000..396a8a0b0 --- /dev/null +++ b/assets/javascripts/discourse/controllers/discourse-ai-bot-conversations.js @@ -0,0 +1,33 @@ +import Controller from "@ember/controller"; +import { on } from "@ember/modifier"; +import { computed } from "@ember/object"; +import { action } from "@ember/object"; +import didInsert from "@ember/render-modifiers/modifiers/did-insert"; +import { service } from "@ember/service"; +import DButton from "discourse/components/d-button"; +import bodyClass from "discourse/helpers/body-class"; +import { i18n } from "discourse-i18n"; +import SimpleTextareaInteractor from "../lib/simple-textarea-interactor"; + +export default class DiscourseAiBotConversations extends Controller { + @service aiBotConversationsHiddenSubmit; + + textareaInteractor = null; + + @action + updateInputValue(event) { + this.aiBotConversationsHiddenSubmit.inputValue = event.target.value; + } + + @action + handleKeyDown(event) { + if (event.key === "Enter" && !event.shiftKey) { + this.aiBotConversationsHiddenSubmit.submitToBot(); + } + } + + @action + initializeTextarea(element) { + this.textareaInteractor = new SimpleTextareaInteractor(element); + } +} diff --git a/assets/javascripts/discourse/discourse-ai-bot-dashboard-route-map.js b/assets/javascripts/discourse/discourse-ai-bot-dashboard-route-map.js new file mode 100644 index 000000000..0c5b92139 --- /dev/null +++ b/assets/javascripts/discourse/discourse-ai-bot-dashboard-route-map.js @@ -0,0 +1,5 @@ +export default function () { + this.route("discourse-ai-bot-conversations", { + path: "/discourse-ai/ai-bot/conversations", + }); +} diff --git a/assets/javascripts/discourse/lib/simple-textarea-interactor.js b/assets/javascripts/discourse/lib/simple-textarea-interactor.js new file mode 100644 index 000000000..32d2e38a4 --- /dev/null +++ b/assets/javascripts/discourse/lib/simple-textarea-interactor.js @@ -0,0 +1,29 @@ +import { schedule } from "@ember/runloop"; + +export default class SimpleTextareaInteractor { + // lifted from "discourse/plugins/chat/discourse/lib/textarea-interactor" + // because the chat plugin isn't active on this site + constructor(textarea) { + this.textarea = textarea; + this.init(); + this.refreshHeightBound = this.refreshHeight.bind(this); + this.textarea.addEventListener("input", this.refreshHeightBound); + } + + init() { + schedule("afterRender", () => { + this.refreshHeight(); + }); + } + + teardown() { + this.textarea.removeEventListener("input", this.refreshHeightBound); + } + + refreshHeight() { + schedule("afterRender", () => { + this.textarea.style.height = "auto"; + this.textarea.style.height = `${this.textarea.scrollHeight + 2}px`; + }); + } +} diff --git a/assets/javascripts/discourse/routes/discourse-ai-bot-conversations.js b/assets/javascripts/discourse/routes/discourse-ai-bot-conversations.js new file mode 100644 index 000000000..08c4d8b15 --- /dev/null +++ b/assets/javascripts/discourse/routes/discourse-ai-bot-conversations.js @@ -0,0 +1,3 @@ +import DiscourseRoute from "discourse/routes/discourse"; + +export default class DiscourseAiBotConversationsRoute extends DiscourseRoute {} diff --git a/assets/javascripts/discourse/services/ai-bot-conversations-hidden-submit.js b/assets/javascripts/discourse/services/ai-bot-conversations-hidden-submit.js new file mode 100644 index 000000000..3148f1433 --- /dev/null +++ b/assets/javascripts/discourse/services/ai-bot-conversations-hidden-submit.js @@ -0,0 +1,59 @@ +import { action } from "@ember/object"; +import { next } from "@ember/runloop"; +import Service, { service } from "@ember/service"; +import Composer from "discourse/models/composer"; +import { i18n } from "discourse-i18n"; + +export default class AiBotConversationsHiddenSubmit extends Service { + @service composer; + @service dialog; + + inputValue = ""; + + @action + focusInput() { + this.composer.destroyDraft(); + this.composer.close(); + next(() => { + document.getElementById("custom-homepage-input").focus(); + }); + } + + @action + async submitToBot() { + this.composer.destroyDraft(); + this.composer.close(); + + if (this.inputValue.length < 10) { + // TODO: Translate + this.dialog.alert({ + message: "Message must be longer than 10 characters", + didConfirm: () => this.focusInput(), + didCancel: () => this.focusInput(), + }); + } + + // this is a total hack, the composer is hidden on the homepage with CSS + await this.composer.open({ + action: Composer.PRIVATE_MESSAGE, + draftKey: "private_message_ai", + recipients: "DiscourseHelper", // TODO: Figure out how to grab the right bot + topicTitle: i18n("discourse_ai.ai_bot.default_pm_prefix"), + topicBody: this.inputValue, + archetypeId: "private_message", + disableDrafts: true, + }); + + try { + await this.composer.save(); + if (this.inputValue.length > 10) { + // prevents submitting same message again when returning home + // but avoids deleting too-short message on submit + this.inputValue = ""; + } + } catch (error) { + // eslint-disable-next-line no-console + console.error("Failed to submit message:", error); + } + } +} diff --git a/assets/javascripts/discourse/templates/discourse-ai-bot-conversations.hbs b/assets/javascripts/discourse/templates/discourse-ai-bot-conversations.hbs new file mode 100644 index 000000000..479b330a5 --- /dev/null +++ b/assets/javascripts/discourse/templates/discourse-ai-bot-conversations.hbs @@ -0,0 +1,25 @@ +{{body-class "discourse-ai-bot-conversations-page"}} + +
+

Ask a question

+
+