FEATURE: Topic summarization (#41)
* FEATURE: Topic summarization Summarize topics using the TopicView's "summary" filter. The UI is similar to what we do for chat, but we don't allow the user to select a timeframe. Co-authored-by: Rafael dos Santos Silva <xfalcox@gmail.com>
This commit is contained in:
parent
9783e3b025
commit
38e007a3a5
|
@ -6,19 +6,28 @@ module DiscourseAi
|
|||
requires_plugin ::DiscourseAi::PLUGIN_NAME
|
||||
requires_login
|
||||
|
||||
VALID_SINCE_VALUES = [1, 3, 6, 12, 24]
|
||||
VALID_SINCE_VALUES = [1, 3, 6, 12, 24, 72, 168]
|
||||
VALID_TARGETS = %w[chat_channel topic]
|
||||
|
||||
def chat_channel
|
||||
def show
|
||||
raise PluginDisabled unless SiteSetting.ai_summarization_enabled
|
||||
target_type = params[:target_type]
|
||||
|
||||
raise Discourse::InvalidParameters.new(:target_type) if !VALID_TARGETS.include?(target_type)
|
||||
|
||||
since = nil
|
||||
|
||||
if target_type == "chat_channel"
|
||||
since = params[:since].to_i
|
||||
|
||||
raise Discourse::InvalidParameters.new(:since) if !VALID_SINCE_VALUES.include?(since)
|
||||
chat_channel = Chat::Channel.find_by(id: params[:chat_channel_id])
|
||||
raise Discourse::NotFound.new(:chat_channel) if !chat_channel
|
||||
|
||||
if !(SiteSetting.discourse_ai_enabled && SiteSetting.ai_summarization_enabled)
|
||||
raise PluginDisabled
|
||||
target = Chat::Channel.find_by(id: params[:target_id])
|
||||
raise Discourse::NotFound.new(:chat_channel) if !target
|
||||
raise Discourse::InvalidAccess if !guardian.can_join_chat_channel?(target)
|
||||
else
|
||||
target = Topic.find_by(id: params[:target_id])
|
||||
raise Discourse::NotFound.new(:topic) if !target
|
||||
raise Discourse::InvalidAccess if !guardian.can_see_topic?(target)
|
||||
end
|
||||
raise Discourse::InvalidAccess if !guardian.can_join_chat_channel?(chat_channel)
|
||||
|
||||
RateLimiter.new(
|
||||
current_user,
|
||||
|
@ -28,7 +37,8 @@ module DiscourseAi
|
|||
).performed!
|
||||
|
||||
hijack do
|
||||
summary = DiscourseAi::Summarization::SummaryGenerator.new(chat_channel).summarize!(since)
|
||||
summary =
|
||||
DiscourseAi::Summarization::SummaryGenerator.new(target, current_user).summarize!(since)
|
||||
|
||||
render json: { summary: summary }, status: 200
|
||||
end
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
<DModalBody @title="discourse_ai.summarization.title">
|
||||
{{#if @allowTimeframe}}
|
||||
<span>{{i18n "discourse_ai.summarization.description"}}</span>
|
||||
<ComboBox
|
||||
@value={{this.sinceHours}}
|
||||
@content={{this.sinceOptions}}
|
||||
@onChange={{action this.summarize}}
|
||||
@valueProperty="value"
|
||||
@class="summarization-since"
|
||||
/>
|
||||
{{/if}}
|
||||
|
||||
<div class="channel-summary">
|
||||
<ConditionalLoadingSpinner @condition={{this.loading}} />
|
||||
|
||||
{{#unless this.loading}}
|
||||
<Textarea @value={{this.summary}} disabled="true" class="summary-area" />
|
||||
{{/unless}}
|
||||
</div>
|
||||
|
||||
</DModalBody>
|
||||
|
||||
<div class="modal-footer">
|
||||
{{#unless @allowTimeframe}}
|
||||
<DButton
|
||||
@class="btn-primary create"
|
||||
@action={{this.summarize}}
|
||||
@label="discourse_ai.summarization.summarize"
|
||||
/>
|
||||
{{/unless}}
|
||||
<DModalCancel @close={{route-action "closeModal"}} />
|
||||
</div>
|
|
@ -0,0 +1,83 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { action } from "@ember/object";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import I18n from "I18n";
|
||||
|
||||
export default class AiSummary extends Component {
|
||||
@tracked sinceHours = null;
|
||||
@tracked loading = false;
|
||||
@tracked availableSummaries = {};
|
||||
@tracked summary = null;
|
||||
sinceOptions = [
|
||||
{
|
||||
name: I18n.t("discourse_ai.summarization.since", { count: 1 }),
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
name: I18n.t("discourse_ai.summarization.since", { count: 3 }),
|
||||
value: 3,
|
||||
},
|
||||
{
|
||||
name: I18n.t("discourse_ai.summarization.since", { count: 6 }),
|
||||
value: 6,
|
||||
},
|
||||
{
|
||||
name: I18n.t("discourse_ai.summarization.since", { count: 12 }),
|
||||
value: 12,
|
||||
},
|
||||
{
|
||||
name: I18n.t("discourse_ai.summarization.since", { count: 24 }),
|
||||
value: 24,
|
||||
},
|
||||
{
|
||||
name: I18n.t("discourse_ai.summarization.since", { count: 72 }),
|
||||
value: 72,
|
||||
},
|
||||
{
|
||||
name: I18n.t("discourse_ai.summarization.since", { count: 168 }),
|
||||
value: 168,
|
||||
},
|
||||
];
|
||||
|
||||
get canSummarize() {
|
||||
return (!this.args.allowTimeframe || this.sinceHours) && !this.loading;
|
||||
}
|
||||
|
||||
@action
|
||||
summarize(value) {
|
||||
this.loading = true;
|
||||
const attrs = {
|
||||
target_id: this.args.targetId,
|
||||
target_type: this.args.targetType,
|
||||
};
|
||||
|
||||
if (this.args.allowTimeframe) {
|
||||
this.sinceHours = value;
|
||||
|
||||
if (this.availableSummaries[this.sinceHours]) {
|
||||
this.summary = this.availableSummaries[this.sinceHours];
|
||||
this.loading = false;
|
||||
return;
|
||||
} else {
|
||||
attrs.since = this.sinceHours;
|
||||
}
|
||||
}
|
||||
|
||||
ajax("/discourse-ai/summarization/summary", {
|
||||
method: "POST",
|
||||
data: attrs,
|
||||
})
|
||||
.then((data) => {
|
||||
if (this.args.allowTimeframe) {
|
||||
this.availableSummaries[this.sinceHours] = data.summary;
|
||||
this.summary = this.availableSummaries[this.sinceHours];
|
||||
} else {
|
||||
this.summary = data.summary;
|
||||
}
|
||||
})
|
||||
.catch(popupAjaxError)
|
||||
.finally(() => (this.loading = false));
|
||||
}
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
<DModalBody @title="discourse_ai.summarization.title">
|
||||
<span>{{i18n "discourse_ai.summarization.description"}}</span>
|
||||
<ComboBox
|
||||
@value={{this.sinceHours}}
|
||||
@content={{this.sinceOptions}}
|
||||
@onChange={{action this.summarize}}
|
||||
@valueProperty="value"
|
||||
@class="summarization-since"
|
||||
/>
|
||||
|
||||
<div class="channel-summary">
|
||||
<ConditionalLoadingSpinner @condition={{this.loading}} />
|
||||
|
||||
{{#unless this.loading}}
|
||||
<Textarea @value={{this.summary}} disabled="true" class="summary-area" />
|
||||
{{/unless}}
|
||||
</div>
|
||||
|
||||
</DModalBody>
|
||||
|
||||
<div class="modal-footer">
|
||||
<DModalCancel @close={{route-action "closeModal"}} />
|
||||
</div>
|
|
@ -1,69 +0,0 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { action } from "@ember/object";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import I18n from "I18n";
|
||||
|
||||
export default class ChatChannelSummary extends Component {
|
||||
@tracked sinceHours = null;
|
||||
@tracked loading = false;
|
||||
@tracked availableSummaries = {};
|
||||
@tracked summary = null;
|
||||
sinceOptions = [
|
||||
{
|
||||
name: I18n.t("discourse_ai.summarization.since", { hours: "1" }),
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
name: I18n.t("discourse_ai.summarization.since", { hours: "3" }),
|
||||
value: 3,
|
||||
},
|
||||
{
|
||||
name: I18n.t("discourse_ai.summarization.since", { hours: "6" }),
|
||||
value: 6,
|
||||
},
|
||||
{
|
||||
name: I18n.t("discourse_ai.summarization.since", { hours: "12" }),
|
||||
value: 12,
|
||||
},
|
||||
{
|
||||
name: I18n.t("discourse_ai.summarization.since", { hours: "24" }),
|
||||
value: 24,
|
||||
},
|
||||
];
|
||||
|
||||
get modalTitle() {
|
||||
return I18n.t("discourse_ai.summarization.modal_title", {
|
||||
channel_title: this.args.chatChannel.escapedTitle,
|
||||
});
|
||||
}
|
||||
|
||||
get canSummarize() {
|
||||
return this.sinceHours && !this.loading;
|
||||
}
|
||||
|
||||
@action
|
||||
summarize(value) {
|
||||
this.sinceHours = value;
|
||||
this.loading = true;
|
||||
const chatChannelId = this.args.chatChannel.id;
|
||||
|
||||
if (this.availableSummaries[this.sinceHours]) {
|
||||
this.summary = this.availableSummaries[this.sinceHours];
|
||||
this.loading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
ajax("/discourse-ai/summarization/chat-channel", {
|
||||
method: "POST",
|
||||
data: { chat_channel_id: chatChannelId, since: this.sinceHours },
|
||||
})
|
||||
.then((data) => {
|
||||
this.availableSummaries[this.sinceHours] = data.summary;
|
||||
this.summary = this.availableSummaries[this.sinceHours];
|
||||
})
|
||||
.catch(popupAjaxError)
|
||||
.finally(() => (this.loading = false));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
<AiSummary
|
||||
@targetId={{this.targetId}}
|
||||
@targetType={{this.targetType}}
|
||||
@allowTimeframe={{this.allowTimeframe}}
|
||||
@closeModal={{route-action "closeModal"}}
|
||||
/>
|
|
@ -1,4 +0,0 @@
|
|||
<ChatChannelSummary
|
||||
@chatChannel={{this.chatChannel}}
|
||||
@closeModal={{route-action "closeModal"}}
|
||||
/>
|
|
@ -18,8 +18,10 @@ function initializeChatChannelSummary(api) {
|
|||
|
||||
@action
|
||||
showChannelSummary() {
|
||||
showModal("composer-chat-channel-summary").setProperties({
|
||||
chatChannel: this.chatChannel,
|
||||
showModal("ai-summary").setProperties({
|
||||
targetId: this.chatChannel.id,
|
||||
targetType: "chat_channel",
|
||||
allowTimeframe: true,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
import { withPluginApi } from "discourse/lib/plugin-api";
|
||||
import showModal from "discourse/lib/show-modal";
|
||||
|
||||
function initializeTopicSummary(api) {
|
||||
api.modifyClass("component:scrolling-post-stream", {
|
||||
showAiSummary() {
|
||||
showModal("ai-summary").setProperties({
|
||||
targetId: this.posts["posts"][0].topic_id,
|
||||
targetType: "topic",
|
||||
allowTimeframe: false,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
api.addTopicSummaryCallback((html, attrs, widget) => {
|
||||
html.push(
|
||||
widget.attach("button", {
|
||||
className: "btn btn-primary topic-ai-summarization",
|
||||
icon: "magic",
|
||||
title: "discourse_ai.summarization.title",
|
||||
label: "discourse_ai.summarization.title",
|
||||
action: "showAiSummary",
|
||||
})
|
||||
);
|
||||
|
||||
return html;
|
||||
});
|
||||
}
|
||||
|
||||
export default {
|
||||
name: "discourse_ai-topic_summary",
|
||||
|
||||
initialize(container) {
|
||||
const settings = container.lookup("site-settings:main");
|
||||
|
||||
const summarizationEnabled =
|
||||
settings.discourse_ai_enabled && settings.ai_summarization_enabled;
|
||||
|
||||
if (summarizationEnabled) {
|
||||
withPluginApi("1.6.0", initializeTopicSummary);
|
||||
}
|
||||
},
|
||||
};
|
|
@ -1,4 +1,4 @@
|
|||
.composer-chat-channel-summary-modal {
|
||||
.ai-summary-modal {
|
||||
.summarization-since,
|
||||
.summary-area {
|
||||
margin: 10px 0 10px 0;
|
||||
|
@ -8,3 +8,7 @@
|
|||
min-height: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
.topic-ai-summarization {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
|
|
@ -20,7 +20,10 @@ en:
|
|||
summarization:
|
||||
title: "Summarize using AI"
|
||||
description: "Select an option below to summarize the conversation sent during the desired timeframe."
|
||||
since: "Last %{hours} hours"
|
||||
summarize: "Summarize"
|
||||
since:
|
||||
one: "Last hour"
|
||||
other: "Last %{count} hours"
|
||||
review:
|
||||
types:
|
||||
reviewable_ai_post:
|
||||
|
|
|
@ -11,7 +11,7 @@ DiscourseAi::Engine.routes.draw do
|
|||
end
|
||||
|
||||
scope module: :summarization, path: "/summarization", defaults: { format: :json } do
|
||||
post "chat-channel" => "summary#chat_channel"
|
||||
post "summary" => "summary#show"
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -3,19 +3,20 @@
|
|||
module DiscourseAi
|
||||
module Summarization
|
||||
class SummaryGenerator
|
||||
def initialize(target)
|
||||
def initialize(target, user)
|
||||
@target = target
|
||||
@user = user
|
||||
end
|
||||
|
||||
def summarize!(content_since)
|
||||
content = get_content(content_since)
|
||||
|
||||
send("#{summarization_provider}_summarization", content)
|
||||
send("#{summarization_provider}_summarization", content[0..(max_length - 1)])
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :target
|
||||
attr_reader :target, :user
|
||||
|
||||
def summarization_provider
|
||||
case model
|
||||
|
@ -35,7 +36,20 @@ module DiscourseAi
|
|||
in Post
|
||||
target.raw
|
||||
in Topic
|
||||
target.posts.order(:post_number).pluck(:raw).join("\n")
|
||||
TopicView
|
||||
.new(
|
||||
target,
|
||||
user,
|
||||
{
|
||||
filter: "summary",
|
||||
exclude_deleted_users: true,
|
||||
exclude_hidden: true,
|
||||
show_deleted: false,
|
||||
},
|
||||
)
|
||||
.posts
|
||||
.pluck(:raw)
|
||||
.join("\n")
|
||||
in ::Chat::Channel
|
||||
target
|
||||
.chat_messages
|
||||
|
@ -46,7 +60,7 @@ module DiscourseAi
|
|||
.map { "#{_1}: #{_2}" }
|
||||
.join("\n")
|
||||
else
|
||||
raise "Invalid target to classify"
|
||||
raise "Can't find content to summarize"
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -92,6 +106,19 @@ module DiscourseAi
|
|||
def model
|
||||
SiteSetting.ai_summarization_model
|
||||
end
|
||||
|
||||
def max_length
|
||||
lengths = {
|
||||
"bart-large-cnn-samsum" => 8192,
|
||||
"flan-t5-base-samsum" => 8192,
|
||||
"long-t5-tglobal-base-16384-book-summary" => 8192,
|
||||
"gpt-3.5-turbo" => 8192,
|
||||
"gpt-4" => 8192,
|
||||
"claude-v1" => 8192,
|
||||
}
|
||||
|
||||
lengths[model]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe DiscourseAi::Summarization::SummaryController do
|
||||
describe "#chat_channel" do
|
||||
describe "#show" do
|
||||
fab!(:user) { Fabricate(:user) }
|
||||
let!(:channel_group) { Fabricate(:group) }
|
||||
let!(:chat_channel) { Fabricate(:private_category_channel, group: channel_group) }
|
||||
|
@ -11,20 +11,27 @@ RSpec.describe DiscourseAi::Summarization::SummaryController do
|
|||
sign_in(user)
|
||||
end
|
||||
|
||||
context "when the user can see the channel" do
|
||||
context "when summarizing a chat channel" do
|
||||
context "if the user can see the channel" do
|
||||
before { channel_group.add(user) }
|
||||
|
||||
describe "validating inputs" do
|
||||
it "returns a 404 if there is no chat channel" do
|
||||
post "/discourse-ai/summarization/chat-channel", params: { chat_channel_id: 99, since: 3 }
|
||||
post "/discourse-ai/summarization/summary",
|
||||
params: {
|
||||
target_type: "chat_channel",
|
||||
target_id: 99,
|
||||
since: 3,
|
||||
}
|
||||
|
||||
expect(response.status).to eq(404)
|
||||
end
|
||||
|
||||
it "returns a 400 if the since param is invalid" do
|
||||
post "/discourse-ai/summarization/chat-channel",
|
||||
post "/discourse-ai/summarization/summary",
|
||||
params: {
|
||||
chat_channel_id: chat_channel.id,
|
||||
target_type: "chat_channel",
|
||||
target_id: chat_channel.id,
|
||||
since: 0,
|
||||
}
|
||||
|
||||
|
@ -34,9 +41,10 @@ RSpec.describe DiscourseAi::Summarization::SummaryController do
|
|||
it "returns a 404 when the module is disabled" do
|
||||
SiteSetting.ai_summarization_enabled = false
|
||||
|
||||
post "/discourse-ai/summarization/chat-channel",
|
||||
post "/discourse-ai/summarization/summary",
|
||||
params: {
|
||||
chat_channel_id: chat_channel.id,
|
||||
target_type: "chat_channel",
|
||||
target_id: chat_channel.id,
|
||||
since: 1,
|
||||
}
|
||||
|
||||
|
@ -44,13 +52,14 @@ RSpec.describe DiscourseAi::Summarization::SummaryController do
|
|||
end
|
||||
end
|
||||
|
||||
context "when the user can't see the channel" do
|
||||
context "if the user can't see the channel" do
|
||||
before { channel_group.remove(user) }
|
||||
|
||||
it "returns a 403 if the user can't see the chat channel" do
|
||||
post "/discourse-ai/summarization/chat-channel",
|
||||
post "/discourse-ai/summarization/summary",
|
||||
params: {
|
||||
chat_channel_id: chat_channel.id,
|
||||
target_type: "chat_channel",
|
||||
target_id: chat_channel.id,
|
||||
since: 1,
|
||||
}
|
||||
|
||||
|
@ -59,4 +68,5 @@ RSpec.describe DiscourseAi::Summarization::SummaryController do
|
|||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class SummarizationStubs
|
||||
class << self
|
||||
def test_summary
|
||||
"This is a summary"
|
||||
end
|
||||
|
||||
def openai_response(content)
|
||||
{
|
||||
id: "chatcmpl-6sZfAb30Rnv9Q7ufzFwvQsMpjZh8S",
|
||||
object: "chat.completion",
|
||||
created: 1_678_464_820,
|
||||
model: "gpt-3.5-turbo-0301",
|
||||
usage: {
|
||||
prompt_tokens: 337,
|
||||
completion_tokens: 162,
|
||||
total_tokens: 499,
|
||||
},
|
||||
choices: [
|
||||
{ message: { role: "assistant", content: content }, finish_reason: "stop", index: 0 },
|
||||
],
|
||||
}
|
||||
end
|
||||
|
||||
def openai_chat_summarization_stub(chat_messages)
|
||||
prompt_messages =
|
||||
chat_messages
|
||||
.sort_by(&:created_at)
|
||||
.map { |m| "#{m.user.username_lower}: #{m.message}" }
|
||||
.join("\n")
|
||||
|
||||
summary_prompt = [{ role: "system", content: <<~TEXT }]
|
||||
Summarize the following article:\n\n#{prompt_messages}
|
||||
TEXT
|
||||
|
||||
WebMock
|
||||
.stub_request(:post, "https://api.openai.com/v1/chat/completions")
|
||||
.with(body: { model: "gpt-4", messages: summary_prompt }.to_json)
|
||||
.to_return(status: 200, body: JSON.dump(openai_response(test_summary)))
|
||||
end
|
||||
|
||||
def openai_topic_summarization_stub(topic, user)
|
||||
prompt_posts = TopicView.new(topic, user, { filter: "summary" }).posts.map(&:raw).join("\n")
|
||||
|
||||
summary_prompt = [{ role: "system", content: <<~TEXT }]
|
||||
Summarize the following article:\n\n#{prompt_posts}
|
||||
TEXT
|
||||
|
||||
WebMock
|
||||
.stub_request(:post, "https://api.openai.com/v1/chat/completions")
|
||||
.with(body: { model: "gpt-4", messages: summary_prompt }.to_json)
|
||||
.to_return(status: 200, body: JSON.dump(openai_response(test_summary)))
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,24 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module PageObjects
|
||||
module Modals
|
||||
class Summarization < PageObjects::Modals::Base
|
||||
def visible?
|
||||
page.has_css?(".ai-summary-modal", wait: 5)
|
||||
end
|
||||
|
||||
def select_timeframe(option)
|
||||
find(".summarization-since").click
|
||||
find(".select-kit-row[data-value=\"#{option}\"]").click
|
||||
end
|
||||
|
||||
def summary_value
|
||||
find(".summary-area").value
|
||||
end
|
||||
|
||||
def generate_summary
|
||||
find(".ai-summary-modal .create").click
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,36 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require_relative "../../support/summarization_stubs"
|
||||
|
||||
RSpec.describe "AI chat channel summarization", type: :system, js: true do
|
||||
fab!(:user) { Fabricate(:leader) }
|
||||
fab!(:channel) { Fabricate(:chat_channel) }
|
||||
|
||||
fab!(:message_1) { Fabricate(:chat_message, chat_channel: channel) }
|
||||
fab!(:message_2) { Fabricate(:chat_message, chat_channel: channel) }
|
||||
fab!(:message_3) { Fabricate(:chat_message, chat_channel: channel) }
|
||||
|
||||
before do
|
||||
sign_in(user)
|
||||
chat_system_bootstrap(user, [channel])
|
||||
SiteSetting.ai_summarization_enabled = true
|
||||
SiteSetting.ai_summarization_model = "gpt-4"
|
||||
end
|
||||
|
||||
let(:summarization_modal) { PageObjects::Modals::Summarization.new }
|
||||
|
||||
it "returns a summary using the selected timeframe" do
|
||||
visit("/chat/c/-/#{channel.id}")
|
||||
|
||||
SummarizationStubs.openai_chat_summarization_stub([message_1, message_2, message_3])
|
||||
|
||||
find(".chat-composer-dropdown__trigger-btn").click
|
||||
find(".chat-composer-dropdown__action-btn.chat_channel_summary").click
|
||||
|
||||
expect(summarization_modal).to be_visible
|
||||
|
||||
summarization_modal.select_timeframe("3")
|
||||
|
||||
expect(summarization_modal.summary_value).to eq(SummarizationStubs.test_summary)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,33 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require_relative "../../support/summarization_stubs"
|
||||
|
||||
RSpec.describe "AI chat channel summarization", type: :system, js: true do
|
||||
fab!(:user) { Fabricate(:leader) }
|
||||
fab!(:topic) { Fabricate(:topic, has_summary: true) }
|
||||
|
||||
fab!(:post_1) { Fabricate(:post, topic: topic) }
|
||||
fab!(:post_2) { Fabricate(:post, topic: topic) }
|
||||
|
||||
before do
|
||||
sign_in(user)
|
||||
SiteSetting.ai_summarization_enabled = true
|
||||
SiteSetting.ai_summarization_model = "gpt-4"
|
||||
end
|
||||
|
||||
let(:summarization_modal) { PageObjects::Modals::Summarization.new }
|
||||
|
||||
it "returns a summary using the selected timeframe" do
|
||||
visit("/t/-/#{topic.id}")
|
||||
|
||||
SummarizationStubs.openai_topic_summarization_stub(topic, user)
|
||||
|
||||
find(".topic-ai-summarization").click
|
||||
|
||||
expect(summarization_modal).to be_visible
|
||||
|
||||
summarization_modal.generate_summary
|
||||
|
||||
expect(summarization_modal.summary_value).to eq(SummarizationStubs.test_summary)
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue