diff --git a/app/controllers/discourse_ai/ai_helper/assistant_controller.rb b/app/controllers/discourse_ai/ai_helper/assistant_controller.rb index d2ee87387..dd7db4510 100644 --- a/app/controllers/discourse_ai/ai_helper/assistant_controller.rb +++ b/app/controllers/discourse_ai/ai_helper/assistant_controller.rb @@ -43,7 +43,7 @@ def suggest prompt, input, current_user, - force_default_locale, + force_default_locale: force_default_locale, ), status: 200 end @@ -110,26 +110,44 @@ def suggest_thumbnails(input) end def stream_suggestion - post_id = get_post_param! text = get_text_param! - post = Post.includes(:topic).find_by(id: post_id) + + location = params[:location] + raise Discourse::InvalidParameters.new(:location) if !location + prompt = CompletionPrompt.find_by(id: params[:mode]) raise Discourse::InvalidParameters.new(:mode) if !prompt || !prompt.enabled? - raise Discourse::InvalidParameters.new(:post_id) unless post + return suggest_thumbnails(input) if prompt.id == CompletionPrompt::ILLUSTRATE_POST if prompt.id == CompletionPrompt::CUSTOM_PROMPT raise Discourse::InvalidParameters.new(:custom_prompt) if params[:custom_prompt].blank? end - Jobs.enqueue( - :stream_post_helper, - post_id: post.id, - user_id: current_user.id, - text: text, - prompt: prompt.name, - custom_prompt: params[:custom_prompt], - ) + if location == "composer" + Jobs.enqueue( + :stream_composer_helper, + user_id: current_user.id, + text: text, + prompt: prompt.name, + custom_prompt: params[:custom_prompt], + force_default_locale: params[:force_default_locale] || false, + ) + else + post_id = get_post_param! + post = Post.includes(:topic).find_by(id: post_id) + + raise Discourse::InvalidParameters.new(:post_id) unless post + + Jobs.enqueue( + :stream_post_helper, + post_id: post.id, + user_id: current_user.id, + text: text, + prompt: prompt.name, + custom_prompt: params[:custom_prompt], + ) + end render json: { success: true }, status: 200 rescue DiscourseAi::Completions::Endpoints::Base::CompletionFailed diff --git a/app/jobs/regular/stream_composer_helper.rb b/app/jobs/regular/stream_composer_helper.rb new file mode 100644 index 000000000..5e8f13d64 --- /dev/null +++ b/app/jobs/regular/stream_composer_helper.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module Jobs + class StreamComposerHelper < ::Jobs::Base + sidekiq_options retry: false + + def execute(args) + return unless args[:prompt] + return unless user = User.find_by(id: args[:user_id]) + return unless args[:text] + + prompt = CompletionPrompt.enabled_by_name(args[:prompt]) + + if prompt.id == CompletionPrompt::CUSTOM_PROMPT + prompt.custom_instruction = args[:custom_prompt] + end + + DiscourseAi::AiHelper::Assistant.new.stream_prompt( + prompt, + args[:text], + user, + "/discourse-ai/ai-helper/stream_composer_suggestion", + force_default_locale: args[:force_default_locale], + ) + end + end +end diff --git a/assets/javascripts/discourse/components/ai-post-helper-menu.gjs b/assets/javascripts/discourse/components/ai-post-helper-menu.gjs index df626e862..f181d390e 100644 --- a/assets/javascripts/discourse/components/ai-post-helper-menu.gjs +++ b/assets/javascripts/discourse/components/ai-post-helper-menu.gjs @@ -237,6 +237,7 @@ export default class AiPostHelperMenu extends Component { this._activeAiRequest = ajax(fetchUrl, { method: "POST", data: { + location: "post", mode: option.id, text: this.args.data.selectedText, post_id: this.args.data.quoteState.postId, diff --git a/assets/javascripts/discourse/components/modal/diff-modal.gjs b/assets/javascripts/discourse/components/modal/diff-modal.gjs index 4647726d8..8731cde3e 100644 --- a/assets/javascripts/discourse/components/modal/diff-modal.gjs +++ b/assets/javascripts/discourse/components/modal/diff-modal.gjs @@ -1,45 +1,92 @@ import Component from "@glimmer/component"; import { tracked } from "@glimmer/tracking"; import { action } from "@ember/object"; +import didInsert from "@ember/render-modifiers/modifiers/did-insert"; +import willDestroy from "@ember/render-modifiers/modifiers/will-destroy"; import { service } from "@ember/service"; import { htmlSafe } from "@ember/template"; import CookText from "discourse/components/cook-text"; import DButton from "discourse/components/d-button"; import DModal from "discourse/components/d-modal"; +import concatClass from "discourse/helpers/concat-class"; import { ajax } from "discourse/lib/ajax"; import { popupAjaxError } from "discourse/lib/ajax-error"; +import { bind } from "discourse/lib/decorators"; import { i18n } from "discourse-i18n"; +import SmoothStreamer from "../../lib/smooth-streamer"; import AiIndicatorWave from "../ai-indicator-wave"; export default class ModalDiffModal extends Component { @service currentUser; + @service messageBus; @tracked loading = false; @tracked diff; @tracked suggestion = ""; + @tracked + smoothStreamer = new SmoothStreamer( + () => this.suggestion, + (newValue) => (this.suggestion = newValue) + ); constructor() { super(...arguments); this.suggestChanges(); } + @bind + subscribe() { + const channel = "/discourse-ai/ai-helper/stream_composer_suggestion"; + this.messageBus.subscribe(channel, this.updateResult); + } + + @bind + unsubscribe() { + const channel = "/discourse-ai/ai-helper/stream_composer_suggestion"; + this.messageBus.subscribe(channel, this.updateResult); + } + + @action + async updateResult(result) { + if (result) { + this.loading = false; + } + await this.smoothStreamer.updateResult(result, "result"); + + if (result.done) { + this.diff = result.diff; + } + + const mdTablePromptId = this.currentUser?.ai_helper_prompts.find( + (prompt) => prompt.name === "markdown_table" + ).id; + + // Markdown table prompt looks better with + // before/after results than diff + // despite having `type: diff` + if (this.args.model.mode === mdTablePromptId) { + this.diff = null; + } + } + @action async suggestChanges() { + this.smoothStreamer.resetStreaming(); + this.diff = null; + this.suggestion = ""; this.loading = true; try { - const suggestion = await ajax("/discourse-ai/ai-helper/suggest", { + return await ajax("/discourse-ai/ai-helper/stream_suggestion", { method: "POST", data: { + location: "composer", mode: this.args.model.mode, text: this.args.model.selectedText, custom_prompt: this.args.model.customPromptValue, force_default_locale: true, }, }); - - this.diff = suggestion.diff; - this.suggestion = suggestion.suggestions[0]; } catch (e) { popupAjaxError(e); } finally { @@ -66,24 +113,42 @@ export default class ModalDiffModal extends Component { @closeModal={{@closeModal}} > <:body> - {{#if this.loading}} -