Compare commits

..
Author SHA1 Message Date
Sam Saffron d544f2101b more tweaks... 2023-05-31 17:15:32 +10:00
Sam Saffron 2d299900b9 we certainly do not need search through pinned topics for now...
This just causes bots to get confused.
2023-05-29 14:15:12 +10:00
Sam Saffron c7781c57fc Fix all specs
Also ensure triage is more consistent by reducing temp
2023-05-29 14:11:32 +10:00
Sam Saffron db11fe391d FEATURE: use triage command to attempt to ground models on GPT 3.5
Previous to this change our results were ungrounded and simpler models like
GPT 3.5 and claude had a real tough time figuring out how to run commands

This splits responses into 2 phases:

Phase 1: figure out if you need to run a command
Phase 2: respond to user

This seems to produce better results on both Claude and GPT 3.5 but still
needs a fair bit of tuning.
2023-05-26 17:07:52 +10:00
239 changed files with 1738 additions and 1101675 deletions
-2
View File
@@ -1,3 +1 @@
3.1.999: c0415bb7eb878b1b7abf112d65cba981030df8df
< 3.2.0.beta1-dev: 3e7c99de894b03e9161d9481da118fe893991a1d
3.1.0.beta3: 057fbe1ce6
@@ -19,8 +19,13 @@ module DiscourseAi
use_pg_headlines_for_excerpt: false,
)
model =
DiscourseAi::Embeddings::Model.instantiate(
SiteSetting.ai_embeddings_semantic_search_model,
)
DiscourseAi::Embeddings::SemanticSearch
.new(guardian)
.new(guardian, model)
.search_for_topics(query, page)
.each { |topic_post| grouped_results.add(topic_post) }
@@ -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
-1
View File
@@ -4,7 +4,6 @@ class AiApiAuditLog < ActiveRecord::Base
module Provider
OpenAI = 1
Anthropic = 2
HuggingFaceTextGeneration = 3
end
end
@@ -1,11 +1,5 @@
# frozen_string_literal: true
class AiChatChannelSerializer < ApplicationSerializer
attributes :id, :chatable, :chatable_type, :chatable_url, :slug
def title
# Display all participants for a DM.
# For category channels, the argument is ignored.
object.title(nil)
end
attributes :id, :chatable, :chatable_type, :chatable_url, :title, :slug
end
@@ -10,7 +10,7 @@ export default class AiBotHeaderPanel extends Component {
@service composer;
@action
composeMessageWithTargetBot(target) {
async composeMessageWithTargetBot(target) {
this.#composeAiBotMessage(target);
}
@@ -27,7 +27,7 @@ export default class AiBotHeaderPanel extends Component {
return this.siteSettings.ai_bot_enabled_chat_bots.split("|");
}
#composeAiBotMessage(targetBot) {
async #composeAiBotMessage(targetBot) {
this.args.closePanel();
composeAiBotMessage(targetBot, this.composer);
}
@@ -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,48 +0,0 @@
import Component from "@glimmer/component";
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 "I18n";
import { htmlSafe } from "@ember/template";
const t = I18n.t.bind(I18n);
export default class ModalDiffModal extends Component {
<template>
<DModal
class="composer-ai-helper-modal"
@title={{t "discourse_ai.ai_helper.context_menu.changes"}}
@closeModal={{@closeModal}}
>
<:body>
{{#if @diff}}
{{htmlSafe @diff}}
{{else}}
<div class="composer-ai-helper-modal__old-value">
{{@oldValue}}
</div>
<div class="composer-ai-helper-modal__new-value">
{{@newValue}}
</div>
{{/if}}
</:body>
<:footer>
<DButton
class="btn-primary confirm"
@action={{this.triggerConfirmChanges}}
@label="discourse_ai.ai_helper.context_menu.confirm"
/>
<DModalCancel @close={{@closeModal}} />
</:footer>
</DModal>
</template>
@action
triggerConfirmChanges() {
this.args.closeModal();
this.args.confirm();
}
}
@@ -1,116 +0,0 @@
<div {{did-insert this.setupContextMenu}}>
{{#if this.showContextMenu}}
<div class="ai-helper-context-menu">
{{#if (eq this.menuState this.CONTEXT_MENU_STATES.triggers)}}
<ul class="ai-helper-context-menu__trigger">
<li>
<DButton
@icon="magic"
@action={{this.toggleAiHelperOptions}}
@label="discourse_ai.ai_helper.context_menu.trigger"
class="btn-flat"
/>
</li>
</ul>
{{else if (eq this.menuState this.CONTEXT_MENU_STATES.options)}}
<ul class="ai-helper-context-menu__options">
{{#each this.helperOptions as |option|}}
<li data-name={{option.name}} data-value={{option.value}}>
<DButton
@class="btn-flat"
@translatedLabel={{option.name}}
@action={{this.updateSelected}}
@actionParam={{option.value}}
/>
</li>
{{/each}}
</ul>
{{else if (eq this.menuState this.CONTEXT_MENU_STATES.suggestions)}}
<ul class="ai-helper-context-menu__suggestions">
{{#each this.generatedTitleSuggestions as |suggestion index|}}
<li data-name={{suggestion}} data-value={{index}}>
<DButton
@class="btn-flat"
@translatedLabel={{suggestion}}
@action={{this.updateTopicTitle}}
@actionParam={{suggestion}}
/>
</li>
{{/each}}
</ul>
{{else if (eq this.menuState this.CONTEXT_MENU_STATES.loading)}}
<ul class="ai-helper-context-menu__loading">
<li>
<div class="dot-falling"></div>
<span>
{{i18n "discourse_ai.ai_helper.context_menu.loading"}}
</span>
</li>
</ul>
{{else if (eq this.menuState this.CONTEXT_MENU_STATES.review)}}
<ul class="ai-helper-context-menu__review">
<li>
<DButton
@icon="exchange-alt"
@label="discourse_ai.ai_helper.context_menu.view_changes"
@action={{this.viewChanges}}
class="btn-flat view-changes"
/>
</li>
<li>
<DButton
@icon="undo"
@label="discourse_ai.ai_helper.context_menu.revert"
@action={{this.undoAIAction}}
class="btn-flat revert"
/>
</li>
<li>
<DButton
@icon="check"
@label="discourse_ai.ai_helper.context_menu.confirm"
@action={{this.confirmChanges}}
class="btn-flat confirm"
/>
</li>
</ul>
{{else if (eq this.menuState this.CONTEXT_MENU_STATES.resets)}}
<ul class="ai-helper-context-menu__resets">
<li>
<DButton
@icon="undo"
@label="discourse_ai.ai_helper.context_menu.undo"
@action={{this.undoAIAction}}
class="btn-flat undo"
/>
</li>
<li>
<DButton
@icon="discourse-sparkles"
@label="discourse_ai.ai_helper.context_menu.regen"
@action={{this.updateSelected}}
@actionParam={{this.lastUsedOption}}
class="btn-flat"
/>
</li>
</ul>
{{/if}}
</div>
{{/if}}
</div>
{{#if this.showDiffModal}}
<Modal::DiffModal
@confirm={{this.confirmChanges}}
@diff={{this.diff}}
@oldValue={{this.selectedText}}
@newValue={{this.newSelectedText}}
@closeModal={{fn (mut this.showDiffModal) false}}
/>
{{/if}}
@@ -1,292 +0,0 @@
import Component from "@glimmer/component";
import { action } from "@ember/object";
import { afterRender, bind, debounce } from "discourse-common/utils/decorators";
import { tracked } from "@glimmer/tracking";
import { INPUT_DELAY } from "discourse-common/config/environment";
import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { createPopper } from "@popperjs/core";
import { caretPosition, getCaretPosition } from "discourse/lib/utilities";
import { inject as service } from "@ember/service";
export default class AiHelperContextMenu extends Component {
static shouldRender(outletArgs, helper) {
const helperEnabled =
helper.siteSettings.discourse_ai_enabled &&
helper.siteSettings.composer_ai_helper_enabled;
const allowedGroups = helper.siteSettings.ai_helper_allowed_groups
.split("|")
.map((id) => parseInt(id, 10));
const canUseAssistant = helper.currentUser?.groups.some((g) =>
allowedGroups.includes(g.id)
);
const canShowInPM = helper.siteSettings.ai_helper_allowed_in_pm;
if (outletArgs?.composer?.privateMessage) {
return helperEnabled && canUseAssistant && canShowInPM;
}
return helperEnabled && canUseAssistant;
}
@service siteSettings;
@tracked helperOptions = [];
@tracked showContextMenu = false;
@tracked menuState = this.CONTEXT_MENU_STATES.triggers;
@tracked caretCoords;
@tracked virtualElement;
@tracked selectedText = "";
@tracked newSelectedText;
@tracked loading = false;
@tracked oldEditorValue;
@tracked newEditorValue;
@tracked generatedTitleSuggestions = [];
@tracked lastUsedOption = null;
@tracked showDiffModal = false;
@tracked diff;
CONTEXT_MENU_STATES = {
triggers: "TRIGGERS",
options: "OPTIONS",
resets: "RESETS",
loading: "LOADING",
suggesions: "SUGGESTIONS",
review: "REVIEW",
};
prompts = [];
promptTypes = {};
@tracked _popper;
@tracked _dEditorInput;
@tracked _contextMenu;
willDestroy() {
super.willDestroy(...arguments);
document.removeEventListener("selectionchange", this.selectionChanged);
document.removeEventListener("keydown", this.onKeyDown);
this._popper?.destroy();
}
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,
};
});
}
@bind
selectionChanged() {
if (document.activeElement !== this._dEditorInput) {
return;
}
const canSelect = Boolean(
window.getSelection() &&
document.activeElement &&
document.activeElement.value
);
this.selectedText = canSelect
? document.activeElement.value.substring(
document.activeElement.selectionStart,
document.activeElement.selectionEnd
)
: "";
if (this.selectedText.length === 0) {
if (this.loading || this.menuState === this.CONTEXT_MENU_STATES.review) {
// prevent accidentally closing context menu
// while results loading or in review state
return;
}
this.closeContextMenu();
return;
}
this._onSelectionChanged();
}
@bind
updatePosition() {
if (!this.showContextMenu) {
return;
}
this.positionContextMenu();
}
@bind
onKeyDown(event) {
const cmdOrCtrl = event.ctrlKey || event.metaKey;
if (cmdOrCtrl && event.key === "z" && this.oldEditorValue) {
return this.undoAIAction();
}
if (event.key === "Escape") {
return this.closeContextMenu();
}
}
@debounce(INPUT_DELAY)
_onSelectionChanged() {
this.positionContextMenu();
this.showContextMenu = true;
}
generateGetBoundingClientRect(width = 0, height = 0, x = 0, y = 0) {
return () => ({
width,
height,
top: y,
right: x,
bottom: y,
left: x,
});
}
closeContextMenu() {
this.showContextMenu = false;
this.menuState = this.CONTEXT_MENU_STATES.triggers;
}
_updateSuggestedByAI(data) {
const composer = this.args.outletArgs.composer;
this.oldEditorValue = this._dEditorInput.value;
this.newSelectedText = data.suggestions[0];
this.newEditorValue = this.oldEditorValue.replace(
this.selectedText,
this.newSelectedText
);
if (data.diff) {
this.diff = data.diff;
}
composer.set("reply", this.newEditorValue);
this.menuState = this.CONTEXT_MENU_STATES.review;
}
@afterRender
positionContextMenu() {
this._contextMenu = document.querySelector(".ai-helper-context-menu");
this.caretCoords = getCaretPosition(this._dEditorInput, {
pos: caretPosition(this._dEditorInput),
});
this.virtualElement = {
getBoundingClientRect: this.generateGetBoundingClientRect(
this._contextMenu.clientWidth,
this._contextMenu.clientHeight,
this.caretCoords.x,
this.caretCoords.y
),
};
this._popper = createPopper(this.virtualElement, this._contextMenu, {
placement: "top-start",
modifiers: [
{
name: "offset",
options: {
offset: [10, 0],
},
},
],
});
}
@action
setupContextMenu() {
document.addEventListener("selectionchange", this.selectionChanged);
document.addEventListener("keydown", this.onKeyDown);
this._dEditorInput = document.querySelector(".d-editor-input");
if (this._dEditorInput) {
this._dEditorInput.addEventListener("scroll", this.updatePosition);
}
}
@action
toggleAiHelperOptions() {
// Fetch prompts only if it hasn't been fetched yet
if (this.helperOptions.length === 0) {
this.loadPrompts();
}
this.menuState = this.CONTEXT_MENU_STATES.options;
}
@action
undoAIAction() {
const composer = this.args.outletArgs.composer;
composer.set("reply", this.oldEditorValue);
this.closeContextMenu();
}
@action
async updateSelected(option) {
this.loading = true;
this.lastUsedOption = option;
this._dEditorInput.classList.add("loading");
this.menuState = this.CONTEXT_MENU_STATES.loading;
return ajax("/discourse-ai/ai-helper/suggest", {
method: "POST",
data: { mode: option, text: this.selectedText },
})
.then((data) => {
// resets the values if new suggestion is started:
this.diff = null;
this.newSelectedText = null;
if (this.prompts[option].name === "generate_titles") {
this.menuState = this.CONTEXT_MENU_STATES.suggestions;
this.generatedTitleSuggestions = data.suggestions;
} else {
this._updateSuggestedByAI(data);
}
})
.catch(popupAjaxError)
.finally(() => {
this.loading = false;
this._dEditorInput.classList.remove("loading");
});
}
@action
updateTopicTitle(title) {
const composer = this.args.outletArgs?.composer;
if (composer) {
composer.set("title", title);
this.closeContextMenu();
}
}
@action
viewChanges() {
this.showDiffModal = true;
}
@action
confirmChanges() {
this.menuState = this.CONTEXT_MENU_STATES.resets;
}
}
@@ -1,8 +0,0 @@
{{#if this.isAiBotChat}}
<DSection @bodyClass="ai-bot-chat" />
{{#if this.renderChatWarning}}
<div class="ai-bot-chat-warning">{{i18n
"discourse_ai.ai_bot.pm_warning"
}}</div>
{{/if}}
{{/if}}
@@ -1,36 +0,0 @@
import Component from "@glimmer/component";
import { inject as service } from "@ember/service";
import { computed } from "@ember/object";
export default class extends Component {
static shouldRender() {
return true;
}
@service currentUser;
@service siteSettings;
get composerModel() {
return this.args?.outletArgs?.composer;
}
get renderChatWarning() {
return this.siteSettings.ai_bot_enable_chat_warning;
}
@computed("composerModel.targetRecipients")
get isAiBotChat() {
if (
this.composerModel &&
this.composerModel.targetRecipients &&
this.currentUser.ai_enabled_chat_bots
) {
let reciepients = this.composerModel.targetRecipients.split(",");
return this.currentUser.ai_enabled_chat_bots.any((bot) =>
reciepients.any((username) => username === bot.username)
);
}
return false;
}
}
@@ -0,0 +1,15 @@
export default {
shouldRender(args) {
return (args.model.related_topics?.length || 0) > 0;
},
setupComponent(args, component) {
if (component.model.related_topics) {
component.set(
"relatedTopics",
component.model.related_topics.map((topic) =>
this.store.createRecord("topic", topic)
)
);
}
},
};
@@ -1,15 +0,0 @@
<div
id="related-topics"
class="more-topics__list {{if this.hidden 'hidden'}}"
role="complementary"
aria-labelledby="related-topics-title"
{{did-insert this.registerList}}
{{will-destroy this.removeList}}
>
<h3 id="related-topics-title" class="more-topics__list-title">
{{i18n "discourse_ai.related_topics.title"}}
</h3>
<div class="topics">
<BasicTopicList @topics={{this.relatedTopics}} />
</div>
</div>
@@ -1,41 +0,0 @@
import Component from "@glimmer/component";
import { inject as service } from "@ember/service";
import { action, computed } from "@ember/object";
import I18n from "I18n";
export default class extends Component {
static shouldRender(args) {
return (args.model.related_topics?.length || 0) > 0;
}
@service store;
@service site;
@service moreTopicsPreferenceTracking;
listId = "related-topics";
@computed("moreTopicsPreferenceTracking.selectedTab")
get hidden() {
return this.moreTopicsPreferenceTracking.selectedTab !== this.listId;
}
get relatedTopics() {
return this.args.outletArgs.model.related_topics.map((topic) =>
this.store.createRecord("topic", topic)
);
}
@action
registerList() {
this.moreTopicsPreferenceTracking.registerTopicList({
name: I18n.t("discourse_ai.related_topics.pill"),
id: this.listId,
icon: "magic",
});
}
@action
removeList() {
this.moreTopicsPreferenceTracking.removeTopicList(this.listId);
}
}
@@ -1,12 +1,13 @@
import { ajax } from "discourse/lib/ajax";
import Composer from "discourse/models/composer";
import I18n from "I18n";
export function composeAiBotMessage(targetBot, composer) {
const currentUser = composer.currentUser;
let botUsername = currentUser.ai_enabled_chat_bots.find(
(bot) => bot.model_name === targetBot
).username;
export async function composeAiBotMessage(targetBot, composer) {
let botUsername = await ajax("/discourse-ai/ai-bot/bot-username", {
data: { username: targetBot },
}).then((data) => {
return data.bot_username;
});
composer.focusComposer({
fallbackToNewTopic: true,
@@ -0,0 +1,6 @@
<div>
<h3 id="related-topics-title" class="related-topics-title">
{{i18n "discourse_ai.related_topics.title"}}
</h3>
<BasicTopicList @topics={{this.relatedTopics}} />
</div>
@@ -0,0 +1,6 @@
<AiSummary
@targetId={{this.targetId}}
@targetType={{this.targetType}}
@allowTimeframe={{this.allowTimeframe}}
@closeModal={{route-action "closeModal"}}
/>
@@ -149,7 +149,7 @@ export default {
const aiBotsAllowedGroups = settings.ai_bot_allowed_groups
.split("|")
.map((id) => parseInt(id, 10));
.map(parseInt);
const canInteractWithAIBots = user?.groups.some((g) =>
aiBotsAllowedGroups.includes(g.id)
);
@@ -0,0 +1,43 @@
import { withPluginApi } from "discourse/lib/plugin-api";
import showModal from "discourse/lib/show-modal";
import { action } from "@ember/object";
function initializeChatChannelSummary(api) {
const chat = api.container.lookup("service:chat");
if (chat) {
api.registerChatComposerButton?.({
translatedLabel: "discourse_ai.summarization.title",
id: "chat_channel_summary",
icon: "discourse-sparkles",
action: "showChannelSummary",
position: "dropdown",
});
api.modifyClass("component:chat-composer", {
pluginId: "discourse-ai",
@action
showChannelSummary() {
showModal("ai-summary").setProperties({
targetId: this.args.channel.id,
targetType: "chat_channel",
allowTimeframe: true,
});
},
});
}
}
export default {
name: "discourse-ai-chat-channel-summary",
initialize(container) {
const settings = container.lookup("service:site-settings");
const summarizationEnabled =
settings.discourse_ai_enabled && settings.ai_summarization_enabled;
if (summarizationEnabled) {
withPluginApi("1.6.0", initializeChatChannelSummary);
}
},
};
@@ -60,7 +60,7 @@ export default {
const allowedGroups = settings.ai_helper_allowed_groups
.split("|")
.map((id) => parseInt(id, 10));
.map(parseInt);
const canUseAssistant = user?.groups.some((g) =>
allowedGroups.includes(g.id)
);
@@ -1,29 +0,0 @@
import { withPluginApi } from "discourse/lib/plugin-api";
export default {
name: "discourse-ai-related-topics",
initialize(container) {
const settings = container.lookup("service:site-settings");
if (settings.ai_embeddings_semantic_related_topics_enabled) {
withPluginApi("1.1.0", (api) => {
api.modifyClass("model:post-stream", {
pluginId: "discourse-ai",
_setSuggestedTopics(result) {
this._super(...arguments);
if (!result.related_topics) {
return;
}
this.topic.setProperties({
related_topics: result.related_topics,
});
},
});
});
}
},
};
@@ -0,0 +1,48 @@
import { withPluginApi } from "discourse/lib/plugin-api";
import showModal from "discourse/lib/show-modal";
function initializeTopicSummary(api) {
api.modifyClass("component:scrolling-post-stream", {
pluginId: "discourse-ai",
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 user = container.lookup("service:current-user");
if (user) {
const settings = container.lookup("service:site-settings");
const summarizationEnabled =
settings.discourse_ai_enabled && settings.ai_summarization_enabled;
if (summarizationEnabled) {
withPluginApi("1.6.0", initializeTopicSummary);
}
}
},
};
@@ -2,25 +2,6 @@ nav.post-controls .actions button.cancel-streaming {
display: none;
}
.ai-bot-chat #reply-control {
.title-and-category {
display: none;
}
}
.ai-bot-chat-warning {
color: var(--tertiary);
background-color: var(--tertiary-low);
border-top: 1px solid var(--tertiary-medium);
opacity: 0.75;
.d-icon {
color: var(--tertiary);
}
margin: 0;
padding: 4px 10px;
width: calc(100% - 20px);
}
article.streaming nav.post-controls .actions button.cancel-streaming {
display: inline-block;
}
@@ -5,8 +5,7 @@
margin: 10px 0 10px 0;
}
.text-preview,
.inline-diff {
.text-preview {
ins {
background-color: var(--success-low);
text-decoration: underline;
@@ -21,17 +20,6 @@
}
}
&__old-value {
background-color: var(--danger-low);
color: var(--danger);
margin-bottom: 1rem;
}
&__new-value {
background-color: var(--success-low);
color: var(--success);
}
.selection-hint {
font-size: var(--font-down-2);
margin-bottom: 20px;
@@ -41,155 +29,3 @@
.topic-above-suggested-outlet.related-topics {
margin: 4.5em 0 1em;
}
.ai-helper-context-menu {
background: var(--secondary);
box-shadow: var(--shadow-dropdown);
padding: 0.25rem;
max-width: 25rem;
border: 1px solid var(--primary-low);
list-style: none;
z-index: 999;
ul {
margin: 0;
list-style: none;
}
ul:not(.ai-helper-context-menu__loading) li {
transition: background-color 0.25s ease;
&:hover {
background: var(--primary-low);
}
}
.d-button-label {
color: var(--primary-very-high);
}
&__options {
padding: 0.25rem;
}
&__loading {
.dot-falling {
margin-inline: 1rem;
margin-left: 1.5rem;
}
li {
display: flex;
padding: 0.5rem;
gap: 1rem;
justify-content: flex-start;
align-items: center;
}
}
&__resets {
display: flex;
align-items: center;
flex-flow: row wrap;
}
&__review {
display: flex;
align-items: center;
flex-flow: row wrap;
}
}
.d-editor-input.loading {
animation: loading-text 1.5s infinite linear;
}
@keyframes loading-text {
0% {
color: var(--primary);
}
50% {
color: var(--tertiary);
}
100% {
color: var(--primary);
}
}
// AI Typing indicator (taken from: https://github.com/nzbin/three-dots)
.dot-falling {
position: relative;
left: -9999px;
width: 10px;
height: 10px;
border-radius: 5px;
background-color: var(--tertiary);
color: var(--tertiary);
box-shadow: 9999px 0 0 0 var(--tertiary);
animation: dot-falling 1s infinite linear;
animation-delay: 0.1s;
}
.dot-falling::before,
.dot-falling::after {
content: "";
display: inline-block;
position: absolute;
top: 0;
}
.dot-falling::before {
width: 10px;
height: 10px;
border-radius: 5px;
background-color: var(--tertiary);
color: var(--tertiary);
animation: dot-falling-before 1s infinite linear;
animation-delay: 0s;
}
.dot-falling::after {
width: 10px;
height: 10px;
border-radius: 5px;
background-color: var(--tertiary);
color: var(--tertiary);
animation: dot-falling-after 1s infinite linear;
animation-delay: 0.2s;
}
@keyframes dot-falling {
0% {
box-shadow: 9999px -15px 0 0 rgba(152, 128, 255, 0);
}
25%,
50%,
75% {
box-shadow: 9999px 0 0 0 var(--tertiary);
}
100% {
box-shadow: 9999px 15px 0 0 rgba(152, 128, 255, 0);
}
}
@keyframes dot-falling-before {
0% {
box-shadow: 9984px -15px 0 0 rgba(152, 128, 255, 0);
}
25%,
50%,
75% {
box-shadow: 9984px 0 0 0 var(--tertiary);
}
100% {
box-shadow: 9984px 15px 0 0 rgba(152, 128, 255, 0);
}
}
@keyframes dot-falling-after {
0% {
box-shadow: 10014px -15px 0 0 rgba(152, 128, 255, 0);
}
25%,
50%,
75% {
box-shadow: 10014px 0 0 0 var(--tertiary);
}
100% {
box-shadow: 10014px 15px 0 0 rgba(152, 128, 255, 0);
}
}
@@ -1,3 +0,0 @@
.related-topics {
margin: 4.5em 0 1em;
}
@@ -0,0 +1,10 @@
.ai-summary-modal {
.summarization-since,
.summary-area {
margin: 10px 0 10px 0;
}
.summary-area {
min-height: 200px;
}
}
@@ -0,0 +1,3 @@
.topic-ai-summarization {
margin-left: 10px;
}
@@ -0,0 +1,9 @@
.toggle-summary {
button {
width: 100%;
}
.topic-ai-summarization {
margin-top: 10px;
}
}

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