diff --git a/app/controllers/discourse_ai/ai_helper/assistant_controller.rb b/app/controllers/discourse_ai/ai_helper/assistant_controller.rb index 5d987e62..559d1382 100644 --- a/app/controllers/discourse_ai/ai_helper/assistant_controller.rb +++ b/app/controllers/discourse_ai/ai_helper/assistant_controller.rb @@ -32,6 +32,8 @@ module DiscourseAi prompt.custom_instruction = params[:custom_prompt] end + suggest_thumbnails(input) if prompt.id == CompletionPrompt::ILLUSTRATE_POST + hijack do render json: DiscourseAi::AiHelper::Assistant.new.generate_and_send_prompt( @@ -86,9 +88,7 @@ module DiscourseAi status: 200 end - def suggest_thumbnails - input = get_text_param! - + def suggest_thumbnails(input) hijack do thumbnails = DiscourseAi::AiHelper::Painter.new.commission_thumbnails(input, current_user) diff --git a/app/models/completion_prompt.rb b/app/models/completion_prompt.rb index c4f4bf61..12614278 100644 --- a/app/models/completion_prompt.rb +++ b/app/models/completion_prompt.rb @@ -10,6 +10,7 @@ class CompletionPrompt < ActiveRecord::Base MARKDOWN_TABLE = -304 CUSTOM_PROMPT = -305 EXPLAIN = -306 + ILLUSTRATE_POST = -308 enum :prompt_type, { text: 0, list: 1, diff: 2 } diff --git a/assets/javascripts/discourse/components/modal/thumbnail-suggestions.gjs b/assets/javascripts/discourse/components/modal/thumbnail-suggestions.gjs new file mode 100644 index 00000000..889258df --- /dev/null +++ b/assets/javascripts/discourse/components/modal/thumbnail-suggestions.gjs @@ -0,0 +1,75 @@ +import Component from "@glimmer/component"; +import { tracked } from "@glimmer/tracking"; +import { action } from "@ember/object"; +import DButton from "discourse/components/d-button"; +import DModal from "discourse/components/d-modal"; +import DModalCancel from "discourse/components/d-modal-cancel"; +import i18n from "discourse-common/helpers/i18n"; +import ThumbnailSuggestionItem from "../thumbnail-suggestion-item"; + +export default class ThumbnailSuggestions extends Component { + @tracked selectedImages = []; + + get isDisabled() { + return this.selectedImages.length === 0; + } + + @action + addSelection(selection) { + const thumbnailMarkdown = `![${selection.original_filename}|${selection.width}x${selection.height}](${selection.short_url})`; + this.selectedImages = [...this.selectedImages, thumbnailMarkdown]; + } + + @action + removeSelection(selection) { + const thumbnailMarkdown = `![${selection.original_filename}|${selection.width}x${selection.height}](${selection.short_url})`; + + this.selectedImages = this.selectedImages.filter((thumbnail) => { + if (thumbnail !== thumbnailMarkdown) { + return thumbnail; + } + }); + } + + @action + appendSelectedImages() { + const composerValue = this.args.composer?.reply || ""; + + const newValue = composerValue.concat( + "\n\n", + this.selectedImages.join("\n") + ); + this.args.composer.set("reply", newValue); + this.args.closeModal(); + } + + + + <:body> + + {{#each @thumbnails as |thumbnail|}} + + {{/each}} + + + + <:footer> + + + + + +} diff --git a/assets/javascripts/discourse/components/thumbnail-suggestion-item.gjs b/assets/javascripts/discourse/components/thumbnail-suggestion-item.gjs new file mode 100644 index 00000000..a6f06a8a --- /dev/null +++ b/assets/javascripts/discourse/components/thumbnail-suggestion-item.gjs @@ -0,0 +1,43 @@ +import Component from "@glimmer/component"; +import { tracked } from "@glimmer/tracking"; +import { action } from "@ember/object"; +import DButton from "discourse/components/d-button"; + +export default class ThumbnailSuggestionItem extends Component { + @tracked selected = false; + @tracked selectIcon = "far-circle"; + @tracked selectLabel = "discourse_ai.ai_helper.thumbnail_suggestions.select"; + + @action + toggleSelection(thumbnail) { + if (this.selected) { + this.selectIcon = "far-circle"; + this.selectLabel = "discourse_ai.ai_helper.thumbnail_suggestions.select"; + this.selected = false; + return this.args.removeSelection(thumbnail); + } + + this.selectIcon = "check-circle"; + this.selectLabel = "discourse_ai.ai_helper.thumbnail_suggestions.selected"; + this.selected = true; + return this.args.addSelection(thumbnail); + } + + + + + + + +} diff --git a/assets/javascripts/discourse/connectors/after-d-editor/ai-helper-context-menu.hbs b/assets/javascripts/discourse/connectors/after-d-editor/ai-helper-context-menu.hbs index 14843893..bf6843b9 100644 --- a/assets/javascripts/discourse/connectors/after-d-editor/ai-helper-context-menu.hbs +++ b/assets/javascripts/discourse/connectors/after-d-editor/ai-helper-context-menu.hbs @@ -101,4 +101,12 @@ @revert={{this.undoAIAction}} @closeModal={{fn (mut this.showDiffModal) false}} /> +{{/if}} + +{{#if this.showThumbnailModal}} + {{/if}} \ No newline at end of file diff --git a/assets/javascripts/discourse/connectors/after-d-editor/ai-helper-context-menu.js b/assets/javascripts/discourse/connectors/after-d-editor/ai-helper-context-menu.js index 9d02df2a..3f1af2cc 100644 --- a/assets/javascripts/discourse/connectors/after-d-editor/ai-helper-context-menu.js +++ b/assets/javascripts/discourse/connectors/after-d-editor/ai-helper-context-menu.js @@ -17,6 +17,7 @@ export default class AiHelperContextMenu extends Component { @service currentUser; @service siteSettings; + @service modal; @tracked helperOptions = []; @tracked showContextMenu = false; @tracked caretCoords; @@ -28,6 +29,7 @@ export default class AiHelperContextMenu extends Component { @tracked newEditorValue; @tracked lastUsedOption = null; @tracked showDiffModal = false; + @tracked showThumbnailModal = false; @tracked diff; @tracked popperPlacement = "top-start"; @tracked previousMenuState = null; @@ -361,7 +363,15 @@ export default class AiHelperContextMenu extends Component { // resets the values if new suggestion is started: this.diff = null; this.newSelectedText = null; - this._updateSuggestedByAI(data); + + if (option.name === "illustrate_post") { + this._toggleLoadingState(false); + this.closeContextMenu(); + this.showThumbnailModal = true; + this.thumbnailSuggestions = data.thumbnails; + } else { + this._updateSuggestedByAI(data); + } }) .catch(popupAjaxError) .finally(() => { diff --git a/assets/stylesheets/modules/ai-helper/common/ai-helper.scss b/assets/stylesheets/modules/ai-helper/common/ai-helper.scss index 63f50869..93f219ea 100644 --- a/assets/stylesheets/modules/ai-helper/common/ai-helper.scss +++ b/assets/stylesheets/modules/ai-helper/common/ai-helper.scss @@ -432,3 +432,28 @@ display: none; } } + +.thumbnail-suggestions-modal { + .ai-thumbnail-suggestions { + display: flex; + flex-flow: row wrap; + position: relative; + gap: 0.5em; + + &__item { + flex: 35%; + position: relative; + } + + img { + width: 100%; + height: auto; + } + + .btn { + position: absolute; + top: 0.5rem; + left: 0.5rem; + } + } +} diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index a55342de..85851d1b 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -153,6 +153,10 @@ en: cancel: "Cancel" fast_edit: suggest_button: "Suggest Edit" + thumbnail_suggestions: + title: "Suggested Thumbnails" + select: "Select" + selected: "Selected" reviewables: model_used: "Model used:" accuracy: "Accuracy:" @@ -186,8 +190,6 @@ en: label: "sentiment" title: "Experimental AI-powered sentiment analysis of this person's most recent posts." - - review: types: reviewable_ai_post: diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index b7382742..0023594c 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -128,6 +128,7 @@ en: markdown_table: Generate Markdown table custom_prompt: "Custom Prompt" explain: "Explain" + illustrate_post: "Illustrate Post" ai_bot: personas: diff --git a/config/routes.rb b/config/routes.rb index 7a8b2373..66a29e02 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -7,7 +7,6 @@ DiscourseAi::Engine.routes.draw do post "suggest_title" => "assistant#suggest_title" post "suggest_category" => "assistant#suggest_category" post "suggest_tags" => "assistant#suggest_tags" - post "suggest_thumbnails" => "assistant#suggest_thumbnails" post "explain" => "assistant#explain" end diff --git a/db/fixtures/ai_helper/603_completion_prompts.rb b/db/fixtures/ai_helper/603_completion_prompts.rb index 4b8df0f7..d08f78e5 100644 --- a/db/fixtures/ai_helper/603_completion_prompts.rb +++ b/db/fixtures/ai_helper/603_completion_prompts.rb @@ -47,9 +47,9 @@ CompletionPrompt.seed do |cp| Intensity 1: Hello, - + Sometimes the logo isn't changing automatically when color scheme changes. - + ![Screen Recording 2023-03-17 at 18.04.22|video](upload://2rcVL0ZMxHPNtPWQbZjwufKpWVU.mov) TEXT @@ -139,13 +139,13 @@ CompletionPrompt.seed do |cp| cp.prompt_type = CompletionPrompt.prompt_types[:text] cp.messages = { insts: <<~TEXT } You are a tutor explaining a term to a student in a specific context. - + I will provide everything you need to know inside tags, which consists of the term I want you to explain inside tags, the context of where it was used inside tags, the title of the topic where it was used inside tags, and optionally, the previous post in the conversation - in tags. - - Using all this information, write a paragraph with a brief explanation + in tags. + + Using all this information, write a paragraph with a brief explanation of what the term means. Format the response using Markdown. Reply only with the explanation and nothing more. TEXT @@ -173,3 +173,10 @@ CompletionPrompt.seed do |cp| post_insts: "Wrap each title between XML tags.", } end + +CompletionPrompt.seed do |cp| + cp.id = -308 + cp.name = "illustrate_post" + cp.prompt_type = CompletionPrompt.prompt_types[:list] + cp.messages = {} +end diff --git a/lib/ai_helper/assistant.rb b/lib/ai_helper/assistant.rb index 24e4758a..03e6fa54 100644 --- a/lib/ai_helper/assistant.rb +++ b/lib/ai_helper/assistant.rb @@ -114,6 +114,8 @@ module DiscourseAi "pen" when "explain" "question" + when "illustrate_post" + "images" else nil end @@ -139,6 +141,8 @@ module DiscourseAi %w[post] when "summarize" %w[post] + when "illustrate_posts" + %w[composer] else %w[composer post] end diff --git a/lib/ai_helper/painter.rb b/lib/ai_helper/painter.rb index d08dc2fd..208d4501 100644 --- a/lib/ai_helper/painter.rb +++ b/lib/ai_helper/painter.rb @@ -20,10 +20,10 @@ module DiscourseAi f.binmode f.write(Base64.decode64(artifact)) f.rewind - upload = UploadCreator.new(f, "ai_helper_image.png").create_for(user.id) + upload = UploadCreator.new(f, "ai_helper_image_#{i}.png").create_for(user.id) f.unlink - upload.short_url + UploadSerializer.new(upload, root: false) end end diff --git a/spec/lib/modules/ai_helper/painter_spec.rb b/spec/lib/modules/ai_helper/painter_spec.rb index 2c627b35..e0b0e665 100644 --- a/spec/lib/modules/ai_helper/painter_spec.rb +++ b/spec/lib/modules/ai_helper/painter_spec.rb @@ -40,7 +40,9 @@ RSpec.describe DiscourseAi::AiHelper::Painter do thumbnail_urls = Upload.last(4).map(&:short_url) - expect(thumbnails).to contain_exactly(*thumbnail_urls) + expect(thumbnails.map { |upload_serializer| upload_serializer.short_url }).to contain_exactly( + *thumbnail_urls, + ) end end end diff --git a/spec/requests/ai_helper/assistant_controller_spec.rb b/spec/requests/ai_helper/assistant_controller_spec.rb index 8b115a27..67dbc1b2 100644 --- a/spec/requests/ai_helper/assistant_controller_spec.rb +++ b/spec/requests/ai_helper/assistant_controller_spec.rb @@ -143,7 +143,7 @@ RSpec.describe DiscourseAi::AiHelper::AssistantController do it "returns a list of prompts when no name_filter is provided" do get "/discourse-ai/ai-helper/prompts" expect(response.status).to eq(200) - expect(response.parsed_body.length).to eq(6) + expect(response.parsed_body.length).to eq(7) end it "returns a list with with filtered prompts when name_filter is provided" do