Compare commits
1
Commits
main
..
embeddings
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e6ced4554 |
@@ -0,0 +1 @@
|
|||||||
|
3.1.0.beta3: 057fbe1ce6
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
name: Discourse Plugin
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
ci:
|
||||||
|
uses: discourse/.github/.github/workflows/discourse-plugin.yml@v1
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
node_modules
|
||||||
|
/gems
|
||||||
|
/auto_generated
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
inherit_gem:
|
||||||
|
rubocop-discourse: stree-compat.yml
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
--print-width=100
|
||||||
|
--plugins=plugin/trailing_comma,disable_ternary
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: ["ember-template-lint-plugin-discourse"],
|
||||||
|
extends: "discourse:recommended",
|
||||||
|
};
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
source "https://rubygems.org"
|
||||||
|
|
||||||
|
group :development do
|
||||||
|
gem "rubocop-discourse"
|
||||||
|
gem "syntax_tree"
|
||||||
|
gem "syntax_tree-disable_ternary"
|
||||||
|
end
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
GEM
|
||||||
|
remote: https://rubygems.org/
|
||||||
|
specs:
|
||||||
|
ast (2.4.2)
|
||||||
|
json (2.6.2)
|
||||||
|
parallel (1.22.1)
|
||||||
|
parser (3.1.2.1)
|
||||||
|
ast (~> 2.4.1)
|
||||||
|
prettier_print (1.2.0)
|
||||||
|
rainbow (3.1.1)
|
||||||
|
regexp_parser (2.6.0)
|
||||||
|
rexml (3.2.5)
|
||||||
|
rubocop (1.36.0)
|
||||||
|
json (~> 2.3)
|
||||||
|
parallel (~> 1.10)
|
||||||
|
parser (>= 3.1.2.1)
|
||||||
|
rainbow (>= 2.2.2, < 4.0)
|
||||||
|
regexp_parser (>= 1.8, < 3.0)
|
||||||
|
rexml (>= 3.2.5, < 4.0)
|
||||||
|
rubocop-ast (>= 1.20.1, < 2.0)
|
||||||
|
ruby-progressbar (~> 1.7)
|
||||||
|
unicode-display_width (>= 1.4.0, < 3.0)
|
||||||
|
rubocop-ast (1.21.0)
|
||||||
|
parser (>= 3.1.1.0)
|
||||||
|
rubocop-discourse (3.0)
|
||||||
|
rubocop (>= 1.1.0)
|
||||||
|
rubocop-rspec (>= 2.0.0)
|
||||||
|
rubocop-rspec (2.13.2)
|
||||||
|
rubocop (~> 1.33)
|
||||||
|
ruby-progressbar (1.11.0)
|
||||||
|
syntax_tree (5.1.0)
|
||||||
|
prettier_print (>= 1.2.0)
|
||||||
|
syntax_tree-disable_ternary (1.0.0)
|
||||||
|
unicode-display_width (2.3.0)
|
||||||
|
|
||||||
|
PLATFORMS
|
||||||
|
arm64-darwin-20
|
||||||
|
ruby
|
||||||
|
x86_64-darwin-18
|
||||||
|
x86_64-darwin-19
|
||||||
|
x86_64-darwin-20
|
||||||
|
x86_64-linux
|
||||||
|
|
||||||
|
DEPENDENCIES
|
||||||
|
rubocop-discourse
|
||||||
|
syntax_tree
|
||||||
|
syntax_tree-disable_ternary
|
||||||
|
|
||||||
|
BUNDLED WITH
|
||||||
|
2.3.10
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2021 Civilized Discourse Construction Kit
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
# Discourse AI is now shipped with Discourse core
|
# **Plugin Name** Plugin
|
||||||
|
|
||||||
https://github.com/discourse/discourse/tree/main/plugins/discourse-ai
|
**Plugin Summary**
|
||||||
|
|
||||||
|
For more information, please see: **url to meta topic**
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module DiscourseAi
|
||||||
|
module AiBot
|
||||||
|
class BotController < ::ApplicationController
|
||||||
|
requires_plugin ::DiscourseAi::PLUGIN_NAME
|
||||||
|
requires_login
|
||||||
|
|
||||||
|
def stop_streaming_response
|
||||||
|
post = Post.find(params[:post_id])
|
||||||
|
guardian.ensure_can_see!(post)
|
||||||
|
|
||||||
|
Discourse.redis.del("gpt_cancel:#{post.id}")
|
||||||
|
|
||||||
|
render json: {}, status: 200
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module DiscourseAi
|
||||||
|
module AiHelper
|
||||||
|
class AssistantController < ::ApplicationController
|
||||||
|
requires_plugin ::DiscourseAi::PLUGIN_NAME
|
||||||
|
requires_login
|
||||||
|
before_action :ensure_can_request_suggestions
|
||||||
|
|
||||||
|
def prompts
|
||||||
|
render json:
|
||||||
|
ActiveModel::ArraySerializer.new(
|
||||||
|
DiscourseAi::AiHelper::LlmPrompt.new.available_prompts,
|
||||||
|
root: false,
|
||||||
|
),
|
||||||
|
status: 200
|
||||||
|
end
|
||||||
|
|
||||||
|
def suggest
|
||||||
|
raise Discourse::InvalidParameters.new(:text) if params[:text].blank?
|
||||||
|
|
||||||
|
prompt = CompletionPrompt.find_by(id: params[:mode])
|
||||||
|
raise Discourse::InvalidParameters.new(:mode) if !prompt || !prompt.enabled?
|
||||||
|
|
||||||
|
RateLimiter.new(current_user, "ai_assistant", 6, 3.minutes).performed!
|
||||||
|
|
||||||
|
hijack do
|
||||||
|
render json:
|
||||||
|
DiscourseAi::AiHelper::LlmPrompt.new.generate_and_send_prompt(
|
||||||
|
prompt,
|
||||||
|
params[:text],
|
||||||
|
),
|
||||||
|
status: 200
|
||||||
|
end
|
||||||
|
rescue ::DiscourseAi::Inference::OpenAiCompletions::CompletionFailed,
|
||||||
|
::DiscourseAi::Inference::AnthropicCompletions::CompletionFailed => e
|
||||||
|
render_json_error I18n.t("discourse_ai.ai_helper.errors.completion_request_failed"),
|
||||||
|
status: 502
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def ensure_can_request_suggestions
|
||||||
|
user_group_ids = current_user.group_ids
|
||||||
|
|
||||||
|
allowed =
|
||||||
|
SiteSetting.ai_helper_allowed_groups_map.any? do |group_id|
|
||||||
|
user_group_ids.include?(group_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
raise Discourse::InvalidAccess if !allowed
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module DiscourseAi
|
||||||
|
module Embeddings
|
||||||
|
class EmbeddingsController < ::ApplicationController
|
||||||
|
requires_plugin ::DiscourseAi::PLUGIN_NAME
|
||||||
|
|
||||||
|
SEMANTIC_SEARCH_TYPE = "semantic_search"
|
||||||
|
|
||||||
|
def search
|
||||||
|
query = params[:q]
|
||||||
|
page = (params[:page] || 1).to_i
|
||||||
|
|
||||||
|
grouped_results =
|
||||||
|
Search::GroupedSearchResults.new(
|
||||||
|
type_filter: SEMANTIC_SEARCH_TYPE,
|
||||||
|
term: query,
|
||||||
|
search_context: guardian,
|
||||||
|
use_pg_headlines_for_excerpt: false,
|
||||||
|
)
|
||||||
|
|
||||||
|
model =
|
||||||
|
DiscourseAi::Embeddings::Model.instantiate(
|
||||||
|
SiteSetting.ai_embeddings_semantic_search_model,
|
||||||
|
)
|
||||||
|
|
||||||
|
DiscourseAi::Embeddings::SemanticSearch
|
||||||
|
.new(guardian, model)
|
||||||
|
.search_for_topics(query, page)
|
||||||
|
.each { |topic_post| grouped_results.add(topic_post) }
|
||||||
|
|
||||||
|
render_serialized(grouped_results, GroupedSearchResultSerializer, result: grouped_results)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module DiscourseAi
|
||||||
|
module Summarization
|
||||||
|
class SummaryController < ::ApplicationController
|
||||||
|
requires_plugin ::DiscourseAi::PLUGIN_NAME
|
||||||
|
requires_login
|
||||||
|
|
||||||
|
VALID_SINCE_VALUES = [1, 3, 6, 12, 24, 72, 168]
|
||||||
|
VALID_TARGETS = %w[chat_channel topic]
|
||||||
|
|
||||||
|
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)
|
||||||
|
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
|
||||||
|
|
||||||
|
RateLimiter.new(
|
||||||
|
current_user,
|
||||||
|
"ai_summarization",
|
||||||
|
6,
|
||||||
|
SiteSetting.ai_summarization_rate_limit_minutes.minutes,
|
||||||
|
).performed!
|
||||||
|
|
||||||
|
hijack do
|
||||||
|
summary =
|
||||||
|
DiscourseAi::Summarization::SummaryGenerator.new(target, current_user).summarize!(since)
|
||||||
|
|
||||||
|
render json: { summary: summary }, status: 200
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AiApiAuditLog < ActiveRecord::Base
|
||||||
|
module Provider
|
||||||
|
OpenAI = 1
|
||||||
|
Anthropic = 2
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: ai_api_audit_logs
|
||||||
|
#
|
||||||
|
# id :bigint not null, primary key
|
||||||
|
# provider_id :integer not null
|
||||||
|
# user_id :integer
|
||||||
|
# request_tokens :integer
|
||||||
|
# response_tokens :integer
|
||||||
|
# raw_request_payload :string
|
||||||
|
# raw_response_payload :string
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
#
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class ClassificationResult < ActiveRecord::Base
|
||||||
|
belongs_to :target, polymorphic: true
|
||||||
|
end
|
||||||
|
|
||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: classification_results
|
||||||
|
#
|
||||||
|
# id :bigint not null, primary key
|
||||||
|
# model_used :string
|
||||||
|
# classification_type :string
|
||||||
|
# target_id :integer
|
||||||
|
# target_type :string
|
||||||
|
# classification :jsonb
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
#
|
||||||
|
# Indexes
|
||||||
|
#
|
||||||
|
# unique_classification_target_per_type (target_id,target_type,model_used) UNIQUE
|
||||||
|
#
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class CompletionPrompt < ActiveRecord::Base
|
||||||
|
# TODO(roman): Remove sept 2023.
|
||||||
|
self.ignored_columns = ["value"]
|
||||||
|
|
||||||
|
enum :prompt_type, { text: 0, list: 1, diff: 2 }
|
||||||
|
|
||||||
|
validates :messages, length: { maximum: 20 }
|
||||||
|
validate :each_message_length
|
||||||
|
|
||||||
|
def messages_with_user_input(user_input)
|
||||||
|
if ::DiscourseAi::AiHelper::LlmPrompt.new.enabled_provider == "openai"
|
||||||
|
self.messages << { role: "user", content: user_input }
|
||||||
|
else
|
||||||
|
self.messages << { "role" => "Input", "content" => "<input>#{user_input}</input>" }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def each_message_length
|
||||||
|
messages.each_with_index do |msg, idx|
|
||||||
|
next if msg["content"].length <= 1000
|
||||||
|
|
||||||
|
errors.add(:messages, I18n.t("errors.prompt_message_length", idx: idx + 1))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: completion_prompts
|
||||||
|
#
|
||||||
|
# id :bigint not null, primary key
|
||||||
|
# name :string not null
|
||||||
|
# translated_name :string
|
||||||
|
# prompt_type :integer default("text"), not null
|
||||||
|
# enabled :boolean default(TRUE), not null
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
# messages :jsonb
|
||||||
|
# provider :text
|
||||||
|
#
|
||||||
|
# Indexes
|
||||||
|
#
|
||||||
|
# index_completion_prompts_on_name (name)
|
||||||
|
#
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class ModelAccuracy < ActiveRecord::Base
|
||||||
|
def self.adjust_model_accuracy(new_status, reviewable)
|
||||||
|
return unless %i[approved rejected].include?(new_status)
|
||||||
|
return unless [ReviewableAiPost, ReviewableAiChatMessage].include?(reviewable.class)
|
||||||
|
|
||||||
|
verdicts = reviewable.payload.to_h["verdicts"] || {}
|
||||||
|
|
||||||
|
verdicts.each do |model_name, verdict|
|
||||||
|
accuracy_model = find_by(model: model_name)
|
||||||
|
|
||||||
|
attribute =
|
||||||
|
if verdict
|
||||||
|
new_status == :approved ? :flags_agreed : :flags_disagreed
|
||||||
|
else
|
||||||
|
new_status == :rejected ? :flags_agreed : :flags_disagreed
|
||||||
|
end
|
||||||
|
|
||||||
|
accuracy_model.increment!(attribute)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def calculate_accuracy
|
||||||
|
return 0 if total_flags.zero?
|
||||||
|
|
||||||
|
(flags_agreed * 100) / total_flags
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def total_flags
|
||||||
|
flags_agreed + flags_disagreed
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: model_accuracies
|
||||||
|
#
|
||||||
|
# id :bigint not null, primary key
|
||||||
|
# model :string not null
|
||||||
|
# classification_type :string not null
|
||||||
|
# flags_agreed :integer default(0), not null
|
||||||
|
# flags_disagreed :integer default(0), not null
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
#
|
||||||
|
# Indexes
|
||||||
|
#
|
||||||
|
# index_model_accuracies_on_model (model) UNIQUE
|
||||||
|
#
|
||||||
@@ -0,0 +1,186 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require_dependency "reviewable"
|
||||||
|
|
||||||
|
class ReviewableAiChatMessage < Reviewable
|
||||||
|
def self.action_aliases
|
||||||
|
{
|
||||||
|
agree_and_keep_hidden: :agree_and_delete,
|
||||||
|
agree_and_silence: :agree_and_delete,
|
||||||
|
agree_and_suspend: :agree_and_delete,
|
||||||
|
delete_and_agree: :agree_and_delete,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.score_to_silence_user
|
||||||
|
sensitivity_score(SiteSetting.chat_silence_user_sensitivity, scale: 0.6)
|
||||||
|
end
|
||||||
|
|
||||||
|
def chat_message
|
||||||
|
@chat_message ||= (target || Chat::Message.with_deleted.find_by(id: target_id))
|
||||||
|
end
|
||||||
|
|
||||||
|
def chat_message_creator
|
||||||
|
@chat_message_creator ||= chat_message.user
|
||||||
|
end
|
||||||
|
|
||||||
|
def flagged_by_user_ids
|
||||||
|
@flagged_by_user_ids ||= reviewable_scores.map(&:user_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def post
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_actions(actions, guardian, args)
|
||||||
|
return unless pending?
|
||||||
|
return if chat_message.blank?
|
||||||
|
|
||||||
|
agree =
|
||||||
|
actions.add_bundle("#{id}-agree", icon: "thumbs-up", label: "reviewables.actions.agree.title")
|
||||||
|
|
||||||
|
if chat_message.deleted_at?
|
||||||
|
build_action(actions, :agree_and_restore, icon: "far-eye", bundle: agree)
|
||||||
|
build_action(actions, :agree_and_keep_deleted, icon: "thumbs-up", bundle: agree)
|
||||||
|
build_action(actions, :disagree_and_restore, icon: "thumbs-down")
|
||||||
|
else
|
||||||
|
build_action(actions, :agree_and_delete, icon: "far-eye-slash", bundle: agree)
|
||||||
|
build_action(actions, :agree_and_keep_message, icon: "thumbs-up", bundle: agree)
|
||||||
|
build_action(actions, :disagree, icon: "thumbs-down")
|
||||||
|
end
|
||||||
|
|
||||||
|
if guardian.can_suspend?(chat_message_creator)
|
||||||
|
build_action(
|
||||||
|
actions,
|
||||||
|
:agree_and_suspend,
|
||||||
|
icon: "ban",
|
||||||
|
bundle: agree,
|
||||||
|
client_action: "suspend",
|
||||||
|
)
|
||||||
|
build_action(
|
||||||
|
actions,
|
||||||
|
:agree_and_silence,
|
||||||
|
icon: "microphone-slash",
|
||||||
|
bundle: agree,
|
||||||
|
client_action: "silence",
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
build_action(actions, :ignore, icon: "external-link-alt")
|
||||||
|
|
||||||
|
build_action(actions, :delete_and_agree, icon: "far-trash-alt") unless chat_message.deleted_at?
|
||||||
|
end
|
||||||
|
|
||||||
|
def perform_agree_and_keep_message(performed_by, args)
|
||||||
|
agree
|
||||||
|
end
|
||||||
|
|
||||||
|
def perform_agree_and_restore(performed_by, args)
|
||||||
|
agree { chat_message.recover! }
|
||||||
|
end
|
||||||
|
|
||||||
|
def perform_agree_and_delete(performed_by, args)
|
||||||
|
agree { chat_message.trash!(performed_by) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def perform_disagree_and_restore(performed_by, args)
|
||||||
|
disagree { chat_message.recover! }
|
||||||
|
end
|
||||||
|
|
||||||
|
def perform_disagree(performed_by, args)
|
||||||
|
disagree
|
||||||
|
end
|
||||||
|
|
||||||
|
def perform_ignore(performed_by, args)
|
||||||
|
ignore
|
||||||
|
end
|
||||||
|
|
||||||
|
def perform_delete_and_ignore(performed_by, args)
|
||||||
|
ignore { chat_message.trash!(performed_by) }
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def agree
|
||||||
|
yield if block_given?
|
||||||
|
create_result(:success, :approved) do |result|
|
||||||
|
result.update_flag_stats = { status: :agreed, user_ids: flagged_by_user_ids }
|
||||||
|
result.recalculate_score = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def disagree
|
||||||
|
yield if block_given?
|
||||||
|
|
||||||
|
UserSilencer.unsilence(chat_message_creator)
|
||||||
|
|
||||||
|
create_result(:success, :rejected) do |result|
|
||||||
|
result.update_flag_stats = { status: :disagreed, user_ids: flagged_by_user_ids }
|
||||||
|
result.recalculate_score = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def ignore
|
||||||
|
yield if block_given?
|
||||||
|
create_result(:success, :ignored) do |result|
|
||||||
|
result.update_flag_stats = { status: :ignored, user_ids: flagged_by_user_ids }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_action(
|
||||||
|
actions,
|
||||||
|
id,
|
||||||
|
icon:,
|
||||||
|
button_class: nil,
|
||||||
|
bundle: nil,
|
||||||
|
client_action: nil,
|
||||||
|
confirm: false
|
||||||
|
)
|
||||||
|
actions.add(id, bundle: bundle) do |action|
|
||||||
|
prefix = "reviewables.actions.#{id}"
|
||||||
|
action.icon = icon
|
||||||
|
action.button_class = button_class
|
||||||
|
action.label = "chat.#{prefix}.title"
|
||||||
|
action.description = "chat.#{prefix}.description"
|
||||||
|
action.client_action = client_action
|
||||||
|
action.confirm_message = "#{prefix}.confirm" if confirm
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: reviewables
|
||||||
|
#
|
||||||
|
# id :bigint not null, primary key
|
||||||
|
# type :string not null
|
||||||
|
# status :integer default("pending"), not null
|
||||||
|
# created_by_id :integer not null
|
||||||
|
# reviewable_by_moderator :boolean default(FALSE), not null
|
||||||
|
# reviewable_by_group_id :integer
|
||||||
|
# category_id :integer
|
||||||
|
# topic_id :integer
|
||||||
|
# score :float default(0.0), not null
|
||||||
|
# potential_spam :boolean default(FALSE), not null
|
||||||
|
# target_id :integer
|
||||||
|
# target_type :string
|
||||||
|
# target_created_by_id :integer
|
||||||
|
# payload :json
|
||||||
|
# version :integer default(0), not null
|
||||||
|
# latest_score :datetime
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
# force_review :boolean default(FALSE), not null
|
||||||
|
# reject_reason :text
|
||||||
|
#
|
||||||
|
# Indexes
|
||||||
|
#
|
||||||
|
# idx_reviewables_score_desc_created_at_desc (score,created_at)
|
||||||
|
# index_reviewables_on_reviewable_by_group_id (reviewable_by_group_id)
|
||||||
|
# index_reviewables_on_status_and_created_at (status,created_at)
|
||||||
|
# index_reviewables_on_status_and_score (status,score)
|
||||||
|
# index_reviewables_on_status_and_type (status,type)
|
||||||
|
# index_reviewables_on_target_id_where_post_type_eq_post (target_id) WHERE ((target_type)::text = 'Post'::text)
|
||||||
|
# index_reviewables_on_topic_id_and_status_and_created_by_id (topic_id,status,created_by_id)
|
||||||
|
# index_reviewables_on_type_and_target_id (type,target_id) UNIQUE
|
||||||
|
#
|
||||||
@@ -0,0 +1,244 @@
|
|||||||
|
# frozen_string_literal:true
|
||||||
|
|
||||||
|
require_dependency "reviewable"
|
||||||
|
|
||||||
|
class ReviewableAiPost < Reviewable
|
||||||
|
# Penalties are handled by the modal after the action is performed
|
||||||
|
def self.action_aliases
|
||||||
|
{
|
||||||
|
agree_and_keep_hidden: :agree_and_keep,
|
||||||
|
agree_and_silence: :agree_and_keep,
|
||||||
|
agree_and_suspend: :agree_and_keep,
|
||||||
|
disagree_and_restore: :disagree,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_actions(actions, guardian, args)
|
||||||
|
return actions if !pending? || post.blank?
|
||||||
|
|
||||||
|
agree =
|
||||||
|
actions.add_bundle("#{id}-agree", icon: "thumbs-up", label: "reviewables.actions.agree.title")
|
||||||
|
|
||||||
|
if !post.user_deleted? && !post.hidden?
|
||||||
|
build_action(actions, :agree_and_hide, icon: "far-eye-slash", bundle: agree)
|
||||||
|
end
|
||||||
|
|
||||||
|
if post.hidden?
|
||||||
|
build_action(actions, :agree_and_keep_hidden, icon: "thumbs-up", bundle: agree)
|
||||||
|
else
|
||||||
|
build_action(actions, :agree_and_keep, icon: "thumbs-up", bundle: agree)
|
||||||
|
end
|
||||||
|
|
||||||
|
if guardian.can_suspend?(target_created_by)
|
||||||
|
build_action(
|
||||||
|
actions,
|
||||||
|
:agree_and_suspend,
|
||||||
|
icon: "ban",
|
||||||
|
bundle: agree,
|
||||||
|
client_action: "suspend",
|
||||||
|
)
|
||||||
|
build_action(
|
||||||
|
actions,
|
||||||
|
:agree_and_silence,
|
||||||
|
icon: "microphone-slash",
|
||||||
|
bundle: agree,
|
||||||
|
client_action: "silence",
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
build_action(actions, :agree_and_restore, icon: "far-eye", bundle: agree) if post.user_deleted?
|
||||||
|
|
||||||
|
if post.hidden?
|
||||||
|
build_action(actions, :disagree_and_restore, icon: "thumbs-down")
|
||||||
|
else
|
||||||
|
build_action(actions, :disagree, icon: "thumbs-down")
|
||||||
|
end
|
||||||
|
|
||||||
|
if guardian.can_delete_post_or_topic?(post)
|
||||||
|
delete =
|
||||||
|
actions.add_bundle(
|
||||||
|
"#{id}-delete",
|
||||||
|
icon: "far-trash-alt",
|
||||||
|
label: "reviewables.actions.delete.title",
|
||||||
|
)
|
||||||
|
build_action(actions, :delete_and_ignore, icon: "external-link-alt", bundle: delete)
|
||||||
|
if post.reply_count > 0
|
||||||
|
build_action(
|
||||||
|
actions,
|
||||||
|
:delete_and_ignore_replies,
|
||||||
|
icon: "external-link-alt",
|
||||||
|
confirm: true,
|
||||||
|
bundle: delete,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
build_action(actions, :delete_and_agree, icon: "thumbs-up", bundle: delete)
|
||||||
|
if post.reply_count > 0
|
||||||
|
build_action(
|
||||||
|
actions,
|
||||||
|
:delete_and_agree_replies,
|
||||||
|
icon: "external-link-alt",
|
||||||
|
bundle: delete,
|
||||||
|
confirm: true,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
delete_user_actions(actions) if guardian.can_delete_user?(target_created_by)
|
||||||
|
|
||||||
|
build_action(actions, :ignore, icon: "external-link-alt")
|
||||||
|
end
|
||||||
|
|
||||||
|
def perform_agree_and_hide(performed_by, args)
|
||||||
|
post.hide!(reviewable_scores.first.reviewable_score_type)
|
||||||
|
|
||||||
|
agree
|
||||||
|
end
|
||||||
|
|
||||||
|
def perform_agree_and_keep(_performed_by, _args)
|
||||||
|
agree
|
||||||
|
end
|
||||||
|
|
||||||
|
def perform_agree_and_restore(performed_by, args)
|
||||||
|
destroyer(performed_by).recover
|
||||||
|
agree
|
||||||
|
end
|
||||||
|
|
||||||
|
def perform_disagree(performed_by, args)
|
||||||
|
# Undo hide/silence if applicable
|
||||||
|
post.unhide! if post.hidden?
|
||||||
|
|
||||||
|
create_result(:success, :rejected) do |result|
|
||||||
|
result.update_flag_stats = { status: :disagreed, user_ids: [created_by_id] }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def perform_ignore(performed_by, args)
|
||||||
|
create_result(:success, :ignored) do |result|
|
||||||
|
result.update_flag_stats = { status: :ignored, user_ids: [created_by_id] }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def perform_delete_and_ignore(performed_by, args)
|
||||||
|
destroyer(performed_by).destroy
|
||||||
|
|
||||||
|
perform_ignore(performed_by, args)
|
||||||
|
end
|
||||||
|
|
||||||
|
def perform_delete_and_agree(performed_by, args)
|
||||||
|
destroyer(performed_by).destroy
|
||||||
|
|
||||||
|
agree
|
||||||
|
end
|
||||||
|
|
||||||
|
def perform_delete_and_ignore_replies(performed_by, args)
|
||||||
|
PostDestroyer.delete_with_replies(performed_by, post, self)
|
||||||
|
|
||||||
|
perform_ignore(performed_by, args)
|
||||||
|
end
|
||||||
|
|
||||||
|
def perform_delete_and_agree_replies(performed_by, args)
|
||||||
|
PostDestroyer.delete_with_replies(performed_by, post, self)
|
||||||
|
|
||||||
|
agree
|
||||||
|
end
|
||||||
|
|
||||||
|
def perform_delete_user(performed_by, args)
|
||||||
|
UserDestroyer.new(performed_by).destroy(post.user, delete_opts)
|
||||||
|
|
||||||
|
agree
|
||||||
|
end
|
||||||
|
|
||||||
|
def perform_delete_user_block(performed_by, args)
|
||||||
|
delete_options = delete_opts
|
||||||
|
|
||||||
|
delete_options.merge!(block_email: true, block_ip: true) if Rails.env.production?
|
||||||
|
|
||||||
|
UserDestroyer.new(performed_by).destroy(post.user, delete_options)
|
||||||
|
|
||||||
|
agree
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def post
|
||||||
|
@post ||= (target || Post.with_deleted.find_by(id: target_id))
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroyer(performed_by)
|
||||||
|
PostDestroyer.new(performed_by, post, reviewable: self)
|
||||||
|
end
|
||||||
|
|
||||||
|
def agree
|
||||||
|
create_result(:success, :approved) do |result|
|
||||||
|
result.update_flag_stats = { status: :agreed, user_ids: [created_by_id] }
|
||||||
|
result.recalculate_score = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def delete_opts
|
||||||
|
{
|
||||||
|
delete_posts: true,
|
||||||
|
prepare_for_destroy: true,
|
||||||
|
block_urls: true,
|
||||||
|
delete_as_spammer: true,
|
||||||
|
context: "review",
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_action(
|
||||||
|
actions,
|
||||||
|
id,
|
||||||
|
icon:,
|
||||||
|
button_class: nil,
|
||||||
|
bundle: nil,
|
||||||
|
client_action: nil,
|
||||||
|
confirm: false
|
||||||
|
)
|
||||||
|
actions.add(id, bundle: bundle) do |action|
|
||||||
|
prefix = "reviewables.actions.#{id}"
|
||||||
|
action.icon = icon
|
||||||
|
action.button_class = button_class
|
||||||
|
action.label = "#{prefix}.title"
|
||||||
|
action.description = "#{prefix}.description"
|
||||||
|
action.client_action = client_action
|
||||||
|
action.confirm_message = "#{prefix}.confirm" if confirm
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: reviewables
|
||||||
|
#
|
||||||
|
# id :bigint not null, primary key
|
||||||
|
# type :string not null
|
||||||
|
# status :integer default("pending"), not null
|
||||||
|
# created_by_id :integer not null
|
||||||
|
# reviewable_by_moderator :boolean default(FALSE), not null
|
||||||
|
# reviewable_by_group_id :integer
|
||||||
|
# category_id :integer
|
||||||
|
# topic_id :integer
|
||||||
|
# score :float default(0.0), not null
|
||||||
|
# potential_spam :boolean default(FALSE), not null
|
||||||
|
# target_id :integer
|
||||||
|
# target_type :string
|
||||||
|
# target_created_by_id :integer
|
||||||
|
# payload :json
|
||||||
|
# version :integer default(0), not null
|
||||||
|
# latest_score :datetime
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
# force_review :boolean default(FALSE), not null
|
||||||
|
# reject_reason :text
|
||||||
|
#
|
||||||
|
# Indexes
|
||||||
|
#
|
||||||
|
# idx_reviewables_score_desc_created_at_desc (score,created_at)
|
||||||
|
# index_reviewables_on_reviewable_by_group_id (reviewable_by_group_id)
|
||||||
|
# index_reviewables_on_status_and_created_at (status,created_at)
|
||||||
|
# index_reviewables_on_status_and_score (status,score)
|
||||||
|
# index_reviewables_on_status_and_type (status,type)
|
||||||
|
# index_reviewables_on_target_id_where_post_type_eq_post (target_id) WHERE ((target_type)::text = 'Post'::text)
|
||||||
|
# index_reviewables_on_topic_id_and_status_and_created_by_id (topic_id,status,created_by_id)
|
||||||
|
# index_reviewables_on_type_and_target_id (type,target_id) UNIQUE
|
||||||
|
#
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AiChatChannelSerializer < ApplicationSerializer
|
||||||
|
attributes :id, :chatable, :chatable_type, :chatable_url, :title, :slug
|
||||||
|
end
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require_dependency "reviewable_serializer"
|
||||||
|
|
||||||
|
class ReviewableAiChatMessageSerializer < ReviewableSerializer
|
||||||
|
payload_attributes :accuracies, :message_cooked
|
||||||
|
target_attributes :cooked
|
||||||
|
attributes :target_id
|
||||||
|
|
||||||
|
has_one :chat_channel, serializer: AiChatChannelSerializer, root: false, embed: :objects
|
||||||
|
|
||||||
|
def chat_channel
|
||||||
|
object.chat_message.chat_channel
|
||||||
|
end
|
||||||
|
|
||||||
|
def target_id
|
||||||
|
object.target&.id
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require_dependency "reviewable_flagged_post_serializer"
|
||||||
|
|
||||||
|
class ReviewableAiPostSerializer < ReviewableFlaggedPostSerializer
|
||||||
|
payload_attributes :accuracies
|
||||||
|
end
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
<DModalBody @title="discourse_ai.ai_helper.title">
|
||||||
|
<span>{{i18n "discourse_ai.ai_helper.description"}}</span>
|
||||||
|
|
||||||
|
<ComboBox
|
||||||
|
@value={{this.selected}}
|
||||||
|
@content={{this.helperOptions}}
|
||||||
|
@onChange={{action this.updateSelected}}
|
||||||
|
@valueProperty="value"
|
||||||
|
@class="ai-helper-mode"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="text-preview">
|
||||||
|
<Textarea
|
||||||
|
@value={{this.composedMessage}}
|
||||||
|
disabled="true"
|
||||||
|
class="preview-area"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="selection-hint">{{i18n
|
||||||
|
"discourse_ai.ai_helper.selection_hint"
|
||||||
|
}}</div>
|
||||||
|
|
||||||
|
<div class="text-preview">
|
||||||
|
<ConditionalLoadingSpinner @condition={{this.loading}} />
|
||||||
|
|
||||||
|
{{#unless this.loading}}
|
||||||
|
{{#if this.selectingTopicTitle}}
|
||||||
|
<div class="radios">
|
||||||
|
{{#each this.generatedTitlesSuggestions as |title index|}}
|
||||||
|
<label class="radio-label" for="title-suggestion-{{index}}">
|
||||||
|
<RadioButton
|
||||||
|
@id="title-suggestion-{{index}}"
|
||||||
|
@name="title-suggestion"
|
||||||
|
@value={{title}}
|
||||||
|
@selection={{this.selectedTitle}}
|
||||||
|
/>
|
||||||
|
<b>{{title}}</b>
|
||||||
|
</label>
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
{{else if this.proofreadingText}}
|
||||||
|
{{html-safe this.proofreadDiff}}
|
||||||
|
{{else if this.translatingText}}
|
||||||
|
<Textarea
|
||||||
|
@value={{this.translatedSuggestion}}
|
||||||
|
disabled="true"
|
||||||
|
class="preview-area"
|
||||||
|
/>
|
||||||
|
{{/if}}
|
||||||
|
{{/unless}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</DModalBody>
|
||||||
|
|
||||||
|
<div class="modal-footer">
|
||||||
|
{{#if this.canSave}}
|
||||||
|
<DButton
|
||||||
|
@class="btn-primary create"
|
||||||
|
@action={{this.applySuggestion}}
|
||||||
|
@label="save"
|
||||||
|
/>
|
||||||
|
<DModalCancel @close={{route-action "closeModal"}} />
|
||||||
|
{{else}}
|
||||||
|
<div class="ai-helper-waiting-selection">{{i18n
|
||||||
|
"discourse_ai.modals.select_option"
|
||||||
|
}}</div>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
import Component from "@glimmer/component";
|
||||||
|
import { tracked } from "@glimmer/tracking";
|
||||||
|
import { action, computed } from "@ember/object";
|
||||||
|
import { ajax } from "discourse/lib/ajax";
|
||||||
|
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||||
|
|
||||||
|
const LIST = "list";
|
||||||
|
const TEXT = "text";
|
||||||
|
const DIFF = "diff";
|
||||||
|
|
||||||
|
export default class AiHelper extends Component {
|
||||||
|
@tracked selected = null;
|
||||||
|
@tracked loading = false;
|
||||||
|
|
||||||
|
@tracked generatedTitlesSuggestions = [];
|
||||||
|
|
||||||
|
@tracked proofReadSuggestion = null;
|
||||||
|
@tracked translatedSuggestion = null;
|
||||||
|
@tracked selectedTitle = null;
|
||||||
|
|
||||||
|
@tracked proofreadDiff = null;
|
||||||
|
|
||||||
|
@tracked helperOptions = [];
|
||||||
|
prompts = [];
|
||||||
|
promptTypes = {};
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super(...arguments);
|
||||||
|
this.loadPrompts();
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadPrompts() {
|
||||||
|
let prompts = await ajax("/discourse-ai/ai-helper/prompts");
|
||||||
|
|
||||||
|
prompts.map((p) => {
|
||||||
|
this.prompts[p.id] = p;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.promptTypes = prompts.reduce((memo, p) => {
|
||||||
|
memo[p.name] = p.prompt_type;
|
||||||
|
return memo;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
this.helperOptions = prompts.map((p) => {
|
||||||
|
return {
|
||||||
|
name: p.translated_name,
|
||||||
|
value: p.id,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get composedMessage() {
|
||||||
|
const editor = this.args.editor;
|
||||||
|
|
||||||
|
return editor.getSelected().value || editor.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@computed("selected", "selectedTitle", "translatingText", "proofreadingText")
|
||||||
|
get canSave() {
|
||||||
|
return (
|
||||||
|
(this.selected &&
|
||||||
|
this.prompts[this.selected].prompt_type === LIST &&
|
||||||
|
this.selectedTitle) ||
|
||||||
|
this.translatingText ||
|
||||||
|
this.proofreadingText
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@computed("selected", "translatedSuggestion")
|
||||||
|
get translatingText() {
|
||||||
|
return (
|
||||||
|
this.selected &&
|
||||||
|
this.prompts[this.selected].prompt_type === TEXT &&
|
||||||
|
this.translatedSuggestion
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@computed("selected", "proofReadSuggestion")
|
||||||
|
get proofreadingText() {
|
||||||
|
return (
|
||||||
|
this.selected &&
|
||||||
|
this.prompts[this.selected].prompt_type === DIFF &&
|
||||||
|
this.proofReadSuggestion
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@computed("selected", "generatedTitlesSuggestions")
|
||||||
|
get selectingTopicTitle() {
|
||||||
|
return (
|
||||||
|
this.selected &&
|
||||||
|
this.prompts[this.selected].prompt_type === LIST &&
|
||||||
|
this.generatedTitlesSuggestions.length > 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_updateSuggestedByAI(data) {
|
||||||
|
switch (data.type) {
|
||||||
|
case LIST:
|
||||||
|
this.generatedTitlesSuggestions = data.suggestions;
|
||||||
|
break;
|
||||||
|
case TEXT:
|
||||||
|
this.translatedSuggestion = data.suggestions[0];
|
||||||
|
break;
|
||||||
|
case DIFF:
|
||||||
|
this.proofReadSuggestion = data.suggestions[0];
|
||||||
|
this.proofreadDiff = data.diff;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
async updateSelected(value) {
|
||||||
|
this.loading = true;
|
||||||
|
this.selected = value;
|
||||||
|
|
||||||
|
if (value === LIST) {
|
||||||
|
this.selectedTitle = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.hasSuggestion) {
|
||||||
|
this.loading = false;
|
||||||
|
} else {
|
||||||
|
return ajax("/discourse-ai/ai-helper/suggest", {
|
||||||
|
method: "POST",
|
||||||
|
data: { mode: this.selected, text: this.composedMessage },
|
||||||
|
})
|
||||||
|
.then((data) => {
|
||||||
|
this._updateSuggestedByAI(data);
|
||||||
|
})
|
||||||
|
.catch(popupAjaxError)
|
||||||
|
.finally(() => (this.loading = false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
applySuggestion() {
|
||||||
|
if (this.selectingTopicTitle) {
|
||||||
|
const composer = this.args.editor.outletArgs?.composer;
|
||||||
|
|
||||||
|
if (composer) {
|
||||||
|
composer.set("title", this.selectedTitle);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const newText = this.proofreadingText
|
||||||
|
? this.proofReadSuggestion
|
||||||
|
: this.translatedSuggestion;
|
||||||
|
this.args.editor.replaceText(this.composedMessage, newText);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.args.closeModal();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{{#if @accuracies}}
|
||||||
|
<table class="reviewable-scores">
|
||||||
|
<tbody>
|
||||||
|
{{#each-in @accuracies as |model acc|}}
|
||||||
|
<tr>
|
||||||
|
<td colspan="4">{{i18n "discourse_ai.reviewables.model_used"}}</td>
|
||||||
|
<td colspan="3">{{model}}</td>
|
||||||
|
<td colspan="4">{{i18n "discourse_ai.reviewables.accuracy"}}</td>
|
||||||
|
<td colspan="3">{{acc}}%</td>
|
||||||
|
</tr>
|
||||||
|
{{/each-in}}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{{/if}}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user