FEATURE: Generate post illustrations (#367)

This commit is contained in:
Keegan George 2023-12-19 11:17:34 -08:00 committed by GitHub
parent 529703b5ec
commit 7b4710d5c9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 194 additions and 17 deletions

View File

@ -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)

View File

@ -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 }

View File

@ -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>
}

View File

@ -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>
}

View File

@ -101,4 +101,12 @@
@revert={{this.undoAIAction}}
@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}}

View File

@ -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(() => {

View File

@ -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;
}
}
}

View File

@ -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:

View File

@ -128,6 +128,7 @@ en:
markdown_table: Generate Markdown table
custom_prompt: "Custom Prompt"
explain: "Explain"
illustrate_post: "Illustrate Post"
ai_bot:
personas:

View File

@ -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

View File

@ -47,9 +47,9 @@ CompletionPrompt.seed do |cp|
<input>
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)
</input>
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 <input> tags, which consists of the term I want you
to explain inside <term> tags, the context of where it was used inside <context> tags, the title of
the topic where it was used inside <topic> tags, and optionally, the previous post in the conversation
in <replyTo> tags.
Using all this information, write a paragraph with a brief explanation
in <replyTo> 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 <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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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