Compare commits

..
Author SHA1 Message Date
Krzysztof Kotlarek 8eceb6ef06 FIX: start zeitwerk structure 2023-11-15 09:41:04 +11:00
Discourse Translator Bot c6902c40ce Update translations (#294) 2023-11-14 14:30:22 +01:00
Roman Rizzi 1588ab99a6 FIX: ChatChannel no longer inherits from RestModel. (#292)
Trying to hydrate using the store will fail. We need to use the model create function instead.
2023-11-13 14:45:20 -03:00
Roman Rizzi 244a338558 UX: Graph surprise next to joy in post_emotion chart (#291) 2023-11-10 14:41:50 -03:00
Sam a4f419f54f FEATURE: basic infrastructure for custom personas (#288)
- New AiPersona model which can store custom personas
- Persona are restricted via group security
- They can contain custom system messages
- They can support a list of commands optionally

To avoid expensive DB calls in the serializer a Multisite friendly Hash was introduced (which can be expired on transaction commit)
2023-11-10 11:39:49 +11:00
Roman Rizzi d0198c5c5b FIX: Changes to the sentiment reports. (#289)
This PR aims to clarify sentiment reports by replacing averages with a count of posts that have one of their values above a threshold (60), meaning we have some level of confidence they are, in fact, positive or negative.

Same thing happen with post emotions, with the difference that a post can have multiple values above it (30). Additionally, we dropped the "Neutral" axis.

We also reworded the tooltip next to each report title, and added an early return to signal we have no data available instead of displaying an empty chart.
2023-11-09 17:23:25 -03:00
Martin Brennan 0c83963a28 DEV: Fix plugin.rb wording (#287)
This was not a complete sentence before
2023-11-09 11:23:43 +10:00
Roman Rizzi 458e66aef9 FIX: Filter classification type using the correct column (#286) 2023-11-08 14:58:35 -03:00
Roman Rizzi 231cf91cc2 FIX: Don't divide by zero if there is no emotion data for TL group (#285) 2023-11-08 13:05:36 -03:00
Roman Rizzi b172ef11c4 FEATURE: Expose sentiment classifications via the admin dashboard. (#284)
This PR adds new reports for displaying information about post sentiments grouped by date and emotions group by TL.

Depends on discourse/discourse#24274
2023-11-08 10:50:37 -03:00
Martin BrennanandAlexDev 30821badf2 Update plugin.rb metadata (#279)
Co-authored-by: AlexDev <[email protected]>
2023-11-07 15:45:02 -08:00
Ty Correll 3df6cb1ec9 UX: prevent suggestion button from wrapping (#282)
* UX: prevent suggestion button from wrapping

This PR addresses the visual bug where the suggestion button wraps on any screen size.

* UX: edge case screen size

This addition fixes screen size issues at specific screen sizes without affecting larger screen sizes, no media query necessary.
2023-11-04 12:52:51 -05:00
94 changed files with 966 additions and 31 deletions
@@ -0,0 +1,12 @@
# frozen_string_literal: true
module DiscourseAi
module Admin
class DashboardController < ::Admin::StaffController
requires_plugin DiscourseAi::PLUGIN_NAME
def sentiment
end
end
end
end
+121
View File
@@ -0,0 +1,121 @@
# frozen_string_literal: true
class AiPersona < ActiveRecord::Base
# places a hard limit, so per site we cache a maximum of 500 classes
MAX_PERSONAS_PER_SITE = 500
class MultisiteHash
def initialize(id)
@hash = Hash.new { |h, k| h[k] = {} }
@id = id
MessageBus.subscribe(channel_name) { |message| @hash[message.data] = {} }
end
def channel_name
"/multisite-hash-#{@id}"
end
def current_db
RailsMultisite::ConnectionManagement.current_db
end
def [](key)
@hash.dig(current_db, key)
end
def []=(key, val)
@hash[current_db][key] = val
end
def flush!
@hash[current_db] = {}
MessageBus.publish(channel_name, current_db)
end
end
def self.persona_cache
@persona_cache ||= MultisiteHash.new("persona_cache")
end
def self.all_personas
persona_cache[:value] ||= AiPersona
.order(:name)
.where(enabled: true)
.all
.limit(MAX_PERSONAS_PER_SITE)
.map do |ai_persona|
name = ai_persona.name
description = ai_persona.description
ai_persona_id = ai_persona.id
allowed_group_ids = ai_persona.allowed_group_ids
commands =
ai_persona.commands.filter_map do |inner_name|
begin
("DiscourseAi::AiBot::Commands::#{inner_name}").constantize
rescue StandardError
nil
end
end
Class.new(DiscourseAi::AiBot::Personas::Persona) do
define_singleton_method :name do
name
end
define_singleton_method :description do
description
end
define_singleton_method :allowed_group_ids do
allowed_group_ids
end
define_singleton_method :to_s do
"#<DiscourseAi::AiBot::Personas::Persona::Custom @name=#{self.name} @allowed_group_ids=#{self.allowed_group_ids.join(",")}>"
end
define_singleton_method :inspect do
"#<DiscourseAi::AiBot::Personas::Persona::Custom @name=#{self.name} @allowed_group_ids=#{self.allowed_group_ids.join(",")}>"
end
define_method :initialize do |*args, **kwargs|
@ai_persona = AiPersona.find_by(id: ai_persona_id)
super(*args, **kwargs)
end
define_method :commands do
commands
end
define_method :system_prompt do
@ai_persona&.system_prompt || "You are a helpful bot."
end
end
end
end
after_commit :bump_cache
def bump_cache
self.class.persona_cache.flush!
end
end
# == Schema Information
#
# Table name: ai_personas
#
# id :bigint not null, primary key
# name :string(100) not null
# description :string(2000) not null
# commands :string default([]), not null, is an Array
# system_prompt :string not null
# allowed_group_ids :integer default([]), not null, is an Array
# created_at :datetime not null
# updated_at :datetime not null
#
# Indexes
#
# index_ai_personas_on_name (name) UNIQUE
#
@@ -0,0 +1,10 @@
export default {
resource: "admin.dashboard",
path: "/dashboard",
map() {
this.route("admin.dashboardSentiment", {
path: "/dashboard/sentiment",
resetNamespace: true,
});
},
};
@@ -1,13 +1,8 @@
import Component from "@glimmer/component";
import { inject as service } from "@ember/service";
import ChatChannel from "discourse/plugins/chat/discourse/models/chat-channel";
export default class ReviewableAiChatMessage extends Component {
@service store;
get chatChannel() {
return this.store.createRecord(
"chat-channel",
this.args.reviewable.chat_channel
);
return ChatChannel.create(this.args.reviewable.chat_channel);
}
}
@@ -0,0 +1,19 @@
import Component from "@glimmer/component";
import { LinkTo } from "@ember/routing";
import I18n from "discourse-i18n";
const i18n = I18n.t.bind(I18n);
export default class AISentimentDashboard extends Component {
<template>
<li class="navigation-item sentiment">
<LinkTo @route="admin.dashboardSentiment" class="navigation-link">
{{i18n "discourse_ai.sentiments.dashboard.title"}}
</LinkTo>
</li>
</template>
static shouldRender(_outletArgs, helper) {
return helper.siteSettings.ai_sentiment_enabled;
}
}
@@ -0,0 +1,38 @@
import Controller from "@ember/controller";
import { action } from "@ember/object";
import { inject as service } from "@ember/service";
import getURL from "discourse-common/lib/get-url";
import discourseComputed from "discourse-common/utils/decorators";
import CustomDateRangeModal from "admin/components/modal/custom-date-range";
import PeriodComputationMixin from "admin/mixins/period-computation";
export default class AdminDashboardSentiment extends Controller.extend(
PeriodComputationMixin
) {
@service modal;
@discourseComputed("startDate", "endDate")
filters(startDate, endDate) {
return { startDate, endDate };
}
_reportsForPeriodURL(period) {
return getURL(`/admin/dashboard/sentiment?period=${period}`);
}
@action
setCustomDateRange(startDate, endDate) {
this.setProperties({ startDate, endDate });
}
@action
openCustomDateRangeModal() {
this.modal.show(CustomDateRangeModal, {
model: {
startDate: this.startDate,
endDate: this.endDate,
setCustomDateRange: this.setCustomDateRange,
},
});
}
}
@@ -0,0 +1,39 @@
<div class="sentiment section">
<div class="period-section">
<div class="section-title">
<h2>
{{i18n "discourse_ai.sentiments.dashboard.title"}}
</h2>
<span>
<PeriodChooser
@period={{this.period}}
@action={{action "changePeriod"}}
@content={{this.availablePeriods}}
@fullDay={{false}}
/>
<DButton
@icon="cog"
class="custom-date-range-button"
@action={{this.openCustomDateRangeModal}}
@title="admin.dashboard.custom_date_range"
/>
</span>
</div>
</div>
<div class="section-body">
<div class="charts">
<AdminReport
@dataSourceName="overall_sentiment"
@filters={{this.filters}}
@showHeader={{true}}
/>
<AdminReport
@dataSourceName="post_emotion"
@filters={{this.filters}}
@showHeader={{true}}
/>
</div>
</div>
</div>
@@ -262,10 +262,6 @@
}
}
#reply-control .showing-ai-suggestions .category-chooser {
max-width: calc(100% - 41px); // width of AI button
}
.suggest-tags-button,
.suggest-category-button {
display: block;
@@ -296,17 +292,13 @@
position: relative;
}
// Prevent suggestion button from wrapping on smaller screens
@media screen and (max-width: 768px) {
#reply-control .composer-fields.showing-ai-suggestions {
.category-input {
.category-chooser {
width: 10px;
}
+ .mini-tag-chooser {
width: 10px;
}
// Prevent suggestion button from wrapping
#reply-control {
.with-category .showing-ai-suggestions .category-input,
.with-tags .showing-ai-suggestions .mini-tag-chooser {
flex-wrap: nowrap;
.select-kit {
max-width: calc(100% - 34px);
}
}
}
@@ -0,0 +1,16 @@
.dashboard.dashboard-sentiment {
.sentiment {
margin-bottom: 1em;
}
.navigation-item.sentiment {
border-bottom: 0.4em solid var(--tertiary);
}
.charts {
display: grid;
grid-template-columns: repeat(12, 1fr);
grid-column-gap: 1em;
grid-row-gap: 1em;
}
}
@@ -0,0 +1,8 @@
.dashboard.dashboard-sentiment .charts {
.overall-sentiment {
grid-column: span 8;
}
.post-emotion {
grid-column: span 4;
}
}
@@ -0,0 +1,10 @@
.dashboard.dashboard-sentiment {
.charts {
.overall-sentiment {
grid-column: span 12;
}
.post-emotion {
grid-column: span 12;
}
}
}
+3
View File
@@ -93,6 +93,9 @@ de:
gpt-3:
5-turbo: "GPT-3.5"
claude-2: "Claude 2"
sentiments:
dashboard:
title: "Stimmung"
review:
types:
reviewable_ai_post:
+3
View File
@@ -92,6 +92,9 @@ en:
gpt-3:
5-turbo: "GPT-3.5"
claude-2: "Claude 2"
sentiments:
dashboard:
title: "Sentiment"
review:
types:
+3
View File
@@ -93,6 +93,9 @@ he:
gpt-3:
5-turbo: "GPT-3.5"
claude-2: "Claude 2"
sentiments:
dashboard:
title: "רגש"
review:
types:
reviewable_ai_post:
+7
View File
@@ -70,6 +70,9 @@ ar:
errors:
prompt_message_length: تتجاوز الرسالة الحد الأقصى لعدد الحروف، والبالغ 1000 حرف، بمقدار %{idx}.
invalid_prompt_role: تنطوي الرسالة %{idx} على دور غير صالح.
reports:
overall_sentiment:
yaxis: "التاريخ"
discourse_ai:
ai_helper:
errors:
@@ -129,3 +132,7 @@ ar:
few: "قم بتكوين هذه الإعدادات أولًا: %{settings}"
many: "قم بتكوين هذه الإعدادات أولًا: %{settings}"
other: "قم بتكوين هذه الإعدادات أولًا: %{settings}"
sentiment:
reports:
post_emotion:
neutral: "محايد"
+5
View File
@@ -5,3 +5,8 @@
# https://translate.discourse.org/
be:
discourse_ai:
sentiment:
reports:
post_emotion:
neutral: "нейтральны"
+3
View File
@@ -5,3 +5,6 @@
# https://translate.discourse.org/
bg:
reports:
overall_sentiment:
yaxis: "Дата"
+3
View File
@@ -5,3 +5,6 @@
# https://translate.discourse.org/
bs_BA:
reports:
overall_sentiment:
yaxis: "Datum"
+8
View File
@@ -5,3 +5,11 @@
# https://translate.discourse.org/
ca:
reports:
overall_sentiment:
yaxis: "Data"
discourse_ai:
sentiment:
reports:
post_emotion:
neutral: "Neutre"
+3
View File
@@ -5,3 +5,6 @@
# https://translate.discourse.org/
cs:
reports:
overall_sentiment:
yaxis: "Datum"
+8
View File
@@ -5,3 +5,11 @@
# https://translate.discourse.org/
da:
reports:
overall_sentiment:
yaxis: "Dato"
discourse_ai:
sentiment:
reports:
post_emotion:
neutral: "Neutral"
+24
View File
@@ -92,6 +92,15 @@ de:
errors:
prompt_message_length: Die Nachricht %{idx} hat die 1000-Zeichen-Grenze überschritten.
invalid_prompt_role: Die Nachricht %{idx} hat eine ungültige Rolle.
reports:
overall_sentiment:
title: "Allgemeine Stimmung"
description: "Diese Grafik vergleicht die Anzahl der Beiträge, die als positiv oder negativ eingestuft werden."
xaxis: "Positiv(%)"
yaxis: "Datum"
post_emotion:
title: "Emotionen posten"
description: "Anzahl der Beiträge, die mit einer der folgenden Emotionen klassifiziert wurden, gruppiert nach dem Vertrauenslevel des Posters."
discourse_ai:
ai_helper:
errors:
@@ -163,3 +172,18 @@ de:
configuration_hint:
one: "Konfiguriere zunächst die Einstellung `%{setting}`."
other: "Konfiguriere zunächst diese Einstellungen: %{settings}"
sentiment:
reports:
overall_sentiment:
positive: "Positiv"
negative: "Negativ"
post_emotion:
tl_01: "Vertrauensstufen 0-1"
tl_234: "Vertrauensstufen 2+"
sadness: "Traurigkeit"
surprise: "Überraschung"
neutral: "Neutral"
fear: "Furcht"
anger: "Wut"
joy: "Freude"
disgust: "Ekel"
+3
View File
@@ -5,3 +5,6 @@
# https://translate.discourse.org/
el:
reports:
overall_sentiment:
yaxis: "Ημερομηνία"
+28
View File
@@ -99,6 +99,18 @@ en:
prompt_message_length: The message %{idx} is over the 1000 character limit.
invalid_prompt_role: The message %{idx} has an invalid role.
reports:
overall_sentiment:
title: "Overall sentiment"
description: "This chart compares the number of posts classified either positive or negative."
xaxis: "Positive(%)"
yaxis: "Date"
post_emotion:
title: "Post emotion"
description: "Number of posts classified with one of the following emotions, grouped by poster's trust level."
xaxis:
yaxis:
discourse_ai:
ai_helper:
errors:
@@ -172,3 +184,19 @@ en:
configuration_hint:
one: "Configure the `%{setting}` setting first."
other: "Configure these settings first: %{settings}"
sentiment:
reports:
overall_sentiment:
positive: "Positive"
negative: "Negative"
post_emotion:
tl_01: "Trust levels 0-1"
tl_234: "Trust levels 2+"
sadness: "Sadness"
surprise: "Surprise"
neutral: "Neutral"
fear: "Fear"
anger: "Anger"
joy: "Joy"
disgust: "Disgust"
+7
View File
@@ -70,6 +70,9 @@ es:
errors:
prompt_message_length: El mensaje %{idx} supera el límite de 1000 caracteres.
invalid_prompt_role: El mensaje %{idx} tiene un rol no válido.
reports:
overall_sentiment:
yaxis: "Fecha"
discourse_ai:
ai_helper:
errors:
@@ -109,3 +112,7 @@ es:
configuration_hint:
one: "Configura primero el ajuste `%{setting}`."
other: "Configura primero estos ajustes: %{settings}"
sentiment:
reports:
post_emotion:
neutral: "Neutro"
+8
View File
@@ -5,3 +5,11 @@
# https://translate.discourse.org/
et:
reports:
overall_sentiment:
yaxis: "Date"
discourse_ai:
sentiment:
reports:
post_emotion:
neutral: "Neutraalne"
+3
View File
@@ -7,6 +7,9 @@
fa_IR:
site_settings:
discourse_ai_enabled: "افزونه discourse AI را فعال کنید."
reports:
overall_sentiment:
yaxis: "تاریخ"
discourse_ai:
ai_bot:
command_summary:
+7
View File
@@ -70,6 +70,9 @@ fi:
errors:
prompt_message_length: Viesti %{idx} ylittää 1 000 merkin rajan.
invalid_prompt_role: Viestillä %{idx} on virheellinen rooli.
reports:
overall_sentiment:
yaxis: "Päivämäärä"
discourse_ai:
ai_helper:
errors:
@@ -109,3 +112,7 @@ fi:
configuration_hint:
one: "Määritä ensin %{setting}-asetus."
other: "Määritä ensin nämä asetukset: %{settings}"
sentiment:
reports:
post_emotion:
neutral: "Neutraali"
+7
View File
@@ -70,6 +70,9 @@ fr:
errors:
prompt_message_length: Le message %{idx} dépasse la limite de 1 000 caractères.
invalid_prompt_role: Le message %{idx} a un rôle invalide.
reports:
overall_sentiment:
yaxis: "Date"
discourse_ai:
ai_helper:
errors:
@@ -109,3 +112,7 @@ fr:
configuration_hint:
one: "Configurez d'abord le paramètre « %{setting} »."
other: "Configurez d'abord ces paramètres : %{settings}"
sentiment:
reports:
post_emotion:
neutral: "Neutre"
+8
View File
@@ -5,3 +5,11 @@
# https://translate.discourse.org/
gl:
reports:
overall_sentiment:
yaxis: "Data"
discourse_ai:
sentiment:
reports:
post_emotion:
neutral: "Neutro"

Some files were not shown because too many files have changed in this diff Show More