FEATURE: Generate post illustrations (#367)
This commit is contained in:
parent
529703b5ec
commit
7b4710d5c9
|
@ -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)
|
||||
|
||||
|
|
|
@ -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 }
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
<template>
|
||||
<DModal
|
||||
class="thumbnail-suggestions-modal"
|
||||
@title={{i18n "discourse_ai.ai_helper.thumbnail_suggestions.title"}}
|
||||
@closeModal={{@closeModal}}
|
||||
>
|
||||
<:body>
|
||||
<div class="ai-thumbnail-suggestions">
|
||||
{{#each @thumbnails as |thumbnail|}}
|
||||
<ThumbnailSuggestionItem
|
||||
@thumbnail={{thumbnail}}
|
||||
@addSelection={{this.addSelection}}
|
||||
@removeSelection={{this.removeSelection}}
|
||||
/>
|
||||
{{/each}}
|
||||
</div>
|
||||
</:body>
|
||||
|
||||
<:footer>
|
||||
<DButton
|
||||
@action={{this.appendSelectedImages}}
|
||||
@label="save"
|
||||
@disabled={{this.isDisabled}}
|
||||
class="btn-primary create"
|
||||
/>
|
||||
<DModalCancel @close={{@closeModal}} />
|
||||
</:footer>
|
||||
</DModal>
|
||||
</template>
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
<template>
|
||||
<div class="ai-thumbnail-suggestions__item">
|
||||
<DButton
|
||||
class={{if this.selected "btn-primary" ""}}
|
||||
@icon={{this.selectIcon}}
|
||||
@label={{this.selectLabel}}
|
||||
@action={{this.toggleSelection}}
|
||||
@actionParam={{@thumbnail}}
|
||||
/>
|
||||
<img
|
||||
src={{@thumbnail.url}}
|
||||
loading="lazy"
|
||||
width={{@thumbnail.thumbnail_width}}
|
||||
height={{@thumbnail.thumbnail_height}}
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
}
|
|
@ -102,3 +102,11 @@
|
|||
@closeModal={{fn (mut this.showDiffModal) false}}
|
||||
/>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.showThumbnailModal}}
|
||||
<Modal::ThumbnailSuggestions
|
||||
@composer={{@outletArgs.composer}}
|
||||
@thumbnails={{this.thumbnailSuggestions}}
|
||||
@closeModal={{fn (mut this.showThumbnailModal) false}}
|
||||
/>
|
||||
{{/if}}
|
|
@ -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;
|
||||
|
||||
if (option.name === "illustrate_post") {
|
||||
this._toggleLoadingState(false);
|
||||
this.closeContextMenu();
|
||||
this.showThumbnailModal = true;
|
||||
this.thumbnailSuggestions = data.thumbnails;
|
||||
} else {
|
||||
this._updateSuggestedByAI(data);
|
||||
}
|
||||
})
|
||||
.catch(popupAjaxError)
|
||||
.finally(() => {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -128,6 +128,7 @@ en:
|
|||
markdown_table: Generate Markdown table
|
||||
custom_prompt: "Custom Prompt"
|
||||
explain: "Explain"
|
||||
illustrate_post: "Illustrate Post"
|
||||
|
||||
ai_bot:
|
||||
personas:
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -173,3 +173,10 @@ CompletionPrompt.seed do |cp|
|
|||
post_insts: "Wrap each title between <item></item> XML tags.",
|
||||
}
|
||||
end
|
||||
|
||||
CompletionPrompt.seed do |cp|
|
||||
cp.id = -308
|
||||
cp.name = "illustrate_post"
|
||||
cp.prompt_type = CompletionPrompt.prompt_types[:list]
|
||||
cp.messages = {}
|
||||
end
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue