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]
|
prompt.custom_instruction = params[:custom_prompt]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
suggest_thumbnails(input) if prompt.id == CompletionPrompt::ILLUSTRATE_POST
|
||||||
|
|
||||||
hijack do
|
hijack do
|
||||||
render json:
|
render json:
|
||||||
DiscourseAi::AiHelper::Assistant.new.generate_and_send_prompt(
|
DiscourseAi::AiHelper::Assistant.new.generate_and_send_prompt(
|
||||||
|
@ -86,9 +88,7 @@ module DiscourseAi
|
||||||
status: 200
|
status: 200
|
||||||
end
|
end
|
||||||
|
|
||||||
def suggest_thumbnails
|
def suggest_thumbnails(input)
|
||||||
input = get_text_param!
|
|
||||||
|
|
||||||
hijack do
|
hijack do
|
||||||
thumbnails = DiscourseAi::AiHelper::Painter.new.commission_thumbnails(input, current_user)
|
thumbnails = DiscourseAi::AiHelper::Painter.new.commission_thumbnails(input, current_user)
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@ class CompletionPrompt < ActiveRecord::Base
|
||||||
MARKDOWN_TABLE = -304
|
MARKDOWN_TABLE = -304
|
||||||
CUSTOM_PROMPT = -305
|
CUSTOM_PROMPT = -305
|
||||||
EXPLAIN = -306
|
EXPLAIN = -306
|
||||||
|
ILLUSTRATE_POST = -308
|
||||||
|
|
||||||
enum :prompt_type, { text: 0, list: 1, diff: 2 }
|
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}}
|
@closeModal={{fn (mut this.showDiffModal) false}}
|
||||||
/>
|
/>
|
||||||
{{/if}}
|
{{/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 currentUser;
|
||||||
@service siteSettings;
|
@service siteSettings;
|
||||||
|
@service modal;
|
||||||
@tracked helperOptions = [];
|
@tracked helperOptions = [];
|
||||||
@tracked showContextMenu = false;
|
@tracked showContextMenu = false;
|
||||||
@tracked caretCoords;
|
@tracked caretCoords;
|
||||||
|
@ -28,6 +29,7 @@ export default class AiHelperContextMenu extends Component {
|
||||||
@tracked newEditorValue;
|
@tracked newEditorValue;
|
||||||
@tracked lastUsedOption = null;
|
@tracked lastUsedOption = null;
|
||||||
@tracked showDiffModal = false;
|
@tracked showDiffModal = false;
|
||||||
|
@tracked showThumbnailModal = false;
|
||||||
@tracked diff;
|
@tracked diff;
|
||||||
@tracked popperPlacement = "top-start";
|
@tracked popperPlacement = "top-start";
|
||||||
@tracked previousMenuState = null;
|
@tracked previousMenuState = null;
|
||||||
|
@ -361,7 +363,15 @@ export default class AiHelperContextMenu extends Component {
|
||||||
// resets the values if new suggestion is started:
|
// resets the values if new suggestion is started:
|
||||||
this.diff = null;
|
this.diff = null;
|
||||||
this.newSelectedText = 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);
|
this._updateSuggestedByAI(data);
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.catch(popupAjaxError)
|
.catch(popupAjaxError)
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
|
|
|
@ -432,3 +432,28 @@
|
||||||
display: none;
|
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"
|
cancel: "Cancel"
|
||||||
fast_edit:
|
fast_edit:
|
||||||
suggest_button: "Suggest Edit"
|
suggest_button: "Suggest Edit"
|
||||||
|
thumbnail_suggestions:
|
||||||
|
title: "Suggested Thumbnails"
|
||||||
|
select: "Select"
|
||||||
|
selected: "Selected"
|
||||||
reviewables:
|
reviewables:
|
||||||
model_used: "Model used:"
|
model_used: "Model used:"
|
||||||
accuracy: "Accuracy:"
|
accuracy: "Accuracy:"
|
||||||
|
@ -186,8 +190,6 @@ en:
|
||||||
label: "sentiment"
|
label: "sentiment"
|
||||||
title: "Experimental AI-powered sentiment analysis of this person's most recent posts."
|
title: "Experimental AI-powered sentiment analysis of this person's most recent posts."
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
review:
|
review:
|
||||||
types:
|
types:
|
||||||
reviewable_ai_post:
|
reviewable_ai_post:
|
||||||
|
|
|
@ -128,6 +128,7 @@ en:
|
||||||
markdown_table: Generate Markdown table
|
markdown_table: Generate Markdown table
|
||||||
custom_prompt: "Custom Prompt"
|
custom_prompt: "Custom Prompt"
|
||||||
explain: "Explain"
|
explain: "Explain"
|
||||||
|
illustrate_post: "Illustrate Post"
|
||||||
|
|
||||||
ai_bot:
|
ai_bot:
|
||||||
personas:
|
personas:
|
||||||
|
|
|
@ -7,7 +7,6 @@ DiscourseAi::Engine.routes.draw do
|
||||||
post "suggest_title" => "assistant#suggest_title"
|
post "suggest_title" => "assistant#suggest_title"
|
||||||
post "suggest_category" => "assistant#suggest_category"
|
post "suggest_category" => "assistant#suggest_category"
|
||||||
post "suggest_tags" => "assistant#suggest_tags"
|
post "suggest_tags" => "assistant#suggest_tags"
|
||||||
post "suggest_thumbnails" => "assistant#suggest_thumbnails"
|
|
||||||
post "explain" => "assistant#explain"
|
post "explain" => "assistant#explain"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -173,3 +173,10 @@ CompletionPrompt.seed do |cp|
|
||||||
post_insts: "Wrap each title between <item></item> XML tags.",
|
post_insts: "Wrap each title between <item></item> XML tags.",
|
||||||
}
|
}
|
||||||
end
|
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"
|
"pen"
|
||||||
when "explain"
|
when "explain"
|
||||||
"question"
|
"question"
|
||||||
|
when "illustrate_post"
|
||||||
|
"images"
|
||||||
else
|
else
|
||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
|
@ -139,6 +141,8 @@ module DiscourseAi
|
||||||
%w[post]
|
%w[post]
|
||||||
when "summarize"
|
when "summarize"
|
||||||
%w[post]
|
%w[post]
|
||||||
|
when "illustrate_posts"
|
||||||
|
%w[composer]
|
||||||
else
|
else
|
||||||
%w[composer post]
|
%w[composer post]
|
||||||
end
|
end
|
||||||
|
|
|
@ -20,10 +20,10 @@ module DiscourseAi
|
||||||
f.binmode
|
f.binmode
|
||||||
f.write(Base64.decode64(artifact))
|
f.write(Base64.decode64(artifact))
|
||||||
f.rewind
|
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
|
f.unlink
|
||||||
|
|
||||||
upload.short_url
|
UploadSerializer.new(upload, root: false)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -40,7 +40,9 @@ RSpec.describe DiscourseAi::AiHelper::Painter do
|
||||||
|
|
||||||
thumbnail_urls = Upload.last(4).map(&:short_url)
|
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
|
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
|
it "returns a list of prompts when no name_filter is provided" do
|
||||||
get "/discourse-ai/ai-helper/prompts"
|
get "/discourse-ai/ai-helper/prompts"
|
||||||
expect(response.status).to eq(200)
|
expect(response.status).to eq(200)
|
||||||
expect(response.parsed_body.length).to eq(6)
|
expect(response.parsed_body.length).to eq(7)
|
||||||
end
|
end
|
||||||
|
|
||||||
it "returns a list with with filtered prompts when name_filter is provided" do
|
it "returns a list with with filtered prompts when name_filter is provided" do
|
||||||
|
|
Loading…
Reference in New Issue