Compare commits

..
Author SHA1 Message Date
Penar Musaraj 059da39a4a DEV: output raw response in dev mode
Makes it easier to debug issues, for example if rate limits are hit.
2025-05-13 16:54:10 -04:00
165 changed files with 940 additions and 4439 deletions
@@ -42,21 +42,6 @@ module DiscourseAi
<style> <style>
#{artifact.css} #{artifact.css}
</style> </style>
<script>
window._discourse_user_data = {
#{current_user ? "username: #{current_user.username.to_json}" : "username: null"}
};
window.discourseArtifactReady = new Promise(resolve => {
window._resolveArtifactData = resolve;
});
window.addEventListener('message', function(event) {
if (event.data && event.data.type === 'discourse-artifact-data') {
window.discourseArtifactData = event.data.dataset || {};
Object.assign(window.discourseArtifactData, window._discourse_user_data);
window._resolveArtifactData(window.discourseArtifactData);
}
});
</script>
</head> </head>
<body> <body>
#{artifact.html} #{artifact.html}
@@ -89,19 +74,6 @@ module DiscourseAi
</head> </head>
<body> <body>
<iframe sandbox="allow-scripts allow-forms" height="100%" width="100%" srcdoc="#{ERB::Util.html_escape(untrusted_html)}" frameborder="0"></iframe> <iframe sandbox="allow-scripts allow-forms" height="100%" width="100%" srcdoc="#{ERB::Util.html_escape(untrusted_html)}" frameborder="0"></iframe>
<script>
document.querySelector('iframe').addEventListener('load', function() {
try {
const iframeWindow = this.contentWindow;
const message = { type: 'discourse-artifact-data', dataset: {} };
if (window.frameElement && window.frameElement.dataset) {
Object.assign(message.dataset, window.frameElement.dataset);
}
iframeWindow.postMessage(message, '*');
} catch (e) { console.error('Error passing data to artifact:', e); }
});
</script>
</body> </body>
</html> </html>
HTML HTML
@@ -8,7 +8,7 @@ module DiscourseAi
before_action :require_site_settings! before_action :require_site_settings!
skip_before_action :preload_json, :check_xhr, only: %i[show asset] skip_before_action :preload_json, :check_xhr, only: %i[show asset]
skip_before_action :redirect_to_login_if_required, :verify_authenticity_token, only: %i[asset] skip_before_action :verify_authenticity_token, only: ["asset"]
def create def create
ensure_allowed_create! ensure_allowed_create!
@@ -124,10 +124,6 @@ module DiscourseAi
raise Discourse::InvalidParameters.new(:custom_prompt) if params[:custom_prompt].blank? raise Discourse::InvalidParameters.new(:custom_prompt) if params[:custom_prompt].blank?
end end
# to stream we must have an appropriate client_id
# otherwise we may end up streaming the data to the wrong client
raise Discourse::InvalidParameters.new(:client_id) if params[:client_id].blank?
if location == "composer" if location == "composer"
Jobs.enqueue( Jobs.enqueue(
:stream_composer_helper, :stream_composer_helper,
@@ -136,7 +132,6 @@ module DiscourseAi
prompt: prompt.name, prompt: prompt.name,
custom_prompt: params[:custom_prompt], custom_prompt: params[:custom_prompt],
force_default_locale: params[:force_default_locale] || false, force_default_locale: params[:force_default_locale] || false,
client_id: params[:client_id],
) )
else else
post_id = get_post_param! post_id = get_post_param!
@@ -151,7 +146,6 @@ module DiscourseAi
text: text, text: text,
prompt: prompt.name, prompt: prompt.name,
custom_prompt: params[:custom_prompt], custom_prompt: params[:custom_prompt],
client_id: params[:client_id],
) )
end end
@@ -8,7 +8,6 @@ module Jobs
return unless args[:prompt] return unless args[:prompt]
return unless user = User.find_by(id: args[:user_id]) return unless user = User.find_by(id: args[:user_id])
return unless args[:text] return unless args[:text]
return unless args[:client_id]
prompt = CompletionPrompt.enabled_by_name(args[:prompt]) prompt = CompletionPrompt.enabled_by_name(args[:prompt])
@@ -22,7 +21,6 @@ module Jobs
user, user,
"/discourse-ai/ai-helper/stream_composer_suggestion", "/discourse-ai/ai-helper/stream_composer_suggestion",
force_default_locale: args[:force_default_locale], force_default_locale: args[:force_default_locale],
client_id: args[:client_id],
) )
end end
end end
-1
View File
@@ -266,7 +266,6 @@ class AiPersona < ActiveRecord::Base
define_method(:top_p) { @ai_persona&.top_p } define_method(:top_p) { @ai_persona&.top_p }
define_method(:system_prompt) { @ai_persona&.system_prompt || "You are a helpful bot." } define_method(:system_prompt) { @ai_persona&.system_prompt || "You are a helpful bot." }
define_method(:uploads) { @ai_persona&.uploads } define_method(:uploads) { @ai_persona&.uploads }
define_method(:response_format) { @ai_persona&.response_format }
define_method(:examples) { @ai_persona&.examples } define_method(:examples) { @ai_persona&.examples }
end end
end end
@@ -1,7 +1,6 @@
import Component from "@glimmer/component"; import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking"; import { tracked } from "@glimmer/tracking";
import { action } from "@ember/object"; import { action } from "@ember/object";
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
import { service } from "@ember/service"; import { service } from "@ember/service";
import DButton from "discourse/components/d-button"; import DButton from "discourse/components/d-button";
import htmlClass from "discourse/helpers/html-class"; import htmlClass from "discourse/helpers/html-class";
@@ -89,15 +88,6 @@ export default class AiArtifactComponent extends Component {
}`; }`;
} }
@action
setDataAttributes(element) {
if (this.args.dataAttributes) {
Object.entries(this.args.dataAttributes).forEach(([key, value]) => {
element.setAttribute(key, value);
});
}
}
<template> <template>
{{#if this.expanded}} {{#if this.expanded}}
{{htmlClass "ai-artifact-expanded"}} {{htmlClass "ai-artifact-expanded"}}
@@ -128,7 +118,6 @@ export default class AiArtifactComponent extends Component {
src={{this.artifactUrl}} src={{this.artifactUrl}}
width="100%" width="100%"
frameborder="0" frameborder="0"
{{didInsert this.setDataAttributes}}
></iframe> ></iframe>
{{/if}} {{/if}}
{{#unless this.requireClickToRun}} {{#unless this.requireClickToRun}}
File diff suppressed because it is too large Load Diff
@@ -10,7 +10,6 @@ import { composeAiBotMessage } from "../lib/ai-bot-helper";
import { AI_CONVERSATIONS_PANEL } from "../services/ai-conversations-sidebar-manager"; import { AI_CONVERSATIONS_PANEL } from "../services/ai-conversations-sidebar-manager";
export default class AiBotHeaderIcon extends Component { export default class AiBotHeaderIcon extends Component {
@service appEvents;
@service composer; @service composer;
@service currentUser; @service currentUser;
@service navigationMenu; @service navigationMenu;
@@ -52,7 +51,6 @@ export default class AiBotHeaderIcon extends Component {
} }
if (this.siteSettings.ai_bot_enable_dedicated_ux) { if (this.siteSettings.ai_bot_enable_dedicated_ux) {
this.appEvents.trigger("discourse-ai:bot-header-icon-clicked");
return this.router.transitionTo("discourse-ai-bot-conversations"); return this.router.transitionTo("discourse-ai-bot-conversations");
} }
@@ -242,7 +242,6 @@ export default class AiPostHelperMenu extends Component {
text: this.args.data.selectedText, text: this.args.data.selectedText,
post_id: this.args.data.quoteState.postId, post_id: this.args.data.quoteState.postId,
custom_prompt: this.customPromptValue, custom_prompt: this.customPromptValue,
client_id: this.messageBus.clientId,
}, },
}); });
@@ -12,21 +12,16 @@ import concatClass from "discourse/helpers/concat-class";
import { ajax } from "discourse/lib/ajax"; import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error"; import { popupAjaxError } from "discourse/lib/ajax-error";
import { bind } from "discourse/lib/decorators"; import { bind } from "discourse/lib/decorators";
import { escapeExpression } from "discourse/lib/utilities";
import { i18n } from "discourse-i18n"; import { i18n } from "discourse-i18n";
import DiffStreamer from "../../lib/diff-streamer"; import DiffStreamer from "../../lib/diff-streamer";
import SmoothStreamer from "../../lib/smooth-streamer"; import SmoothStreamer from "../../lib/smooth-streamer";
import AiIndicatorWave from "../ai-indicator-wave"; import AiIndicatorWave from "../ai-indicator-wave";
const CHANNEL = "/discourse-ai/ai-helper/stream_composer_suggestion";
export default class ModalDiffModal extends Component { export default class ModalDiffModal extends Component {
@service currentUser; @service currentUser;
@service messageBus; @service messageBus;
@tracked loading = false; @tracked loading = false;
@tracked finalResult = "";
@tracked escapedSelectedText = escapeExpression(this.args.model.selectedText);
@tracked diffStreamer = new DiffStreamer(this.args.model.selectedText); @tracked diffStreamer = new DiffStreamer(this.args.model.selectedText);
@tracked suggestion = ""; @tracked suggestion = "";
@tracked @tracked
@@ -40,20 +35,8 @@ export default class ModalDiffModal extends Component {
this.suggestChanges(); this.suggestChanges();
} }
get diffResult() {
if (this.diffStreamer.diff?.length > 0) {
return this.diffStreamer.diff;
}
// Prevents flash by showing the
// original text when the diff is empty
return this.escapedSelectedText;
}
get isStreaming() { get isStreaming() {
// diffStreamer stops Streaming when it is finished with a chunk, looking at isDone is safe return this.diffStreamer.isStreaming || this.smoothStreamer.isStreaming;
// it starts off not done
return !this.diffStreamer.isDone || this.smoothStreamer.isStreaming;
} }
get primaryBtnLabel() { get primaryBtnLabel() {
@@ -68,26 +51,20 @@ export default class ModalDiffModal extends Component {
@bind @bind
subscribe() { subscribe() {
this.messageBus.subscribe(CHANNEL, this.updateResult); const channel = "/discourse-ai/ai-helper/stream_composer_suggestion";
this.messageBus.subscribe(channel, this.updateResult);
} }
@bind @bind
cleanup() { unsubscribe() {
// stop all callbacks so it does not end up streaming pointlessly const channel = "/discourse-ai/ai-helper/stream_composer_suggestion";
this.smoothStreamer.resetStreaming(); this.messageBus.subscribe(channel, this.updateResult);
this.diffStreamer.reset();
this.messageBus.unsubscribe(CHANNEL, this.updateResult);
} }
@action @action
updateResult(result) { async updateResult(result) {
this.loading = false; this.loading = false;
if (result.done) {
this.finalResult = result.result;
this.loading = false;
}
if (this.args.model.showResultAsDiff) { if (this.args.model.showResultAsDiff) {
this.diffStreamer.updateResult(result, "result"); this.diffStreamer.updateResult(result, "result");
} else { } else {
@@ -110,7 +87,6 @@ export default class ModalDiffModal extends Component {
text: this.args.model.selectedText, text: this.args.model.selectedText,
custom_prompt: this.args.model.customPromptValue, custom_prompt: this.args.model.customPromptValue,
force_default_locale: true, force_default_locale: true,
client_id: this.messageBus.clientId,
}, },
}); });
} catch (e) { } catch (e) {
@@ -129,15 +105,10 @@ export default class ModalDiffModal extends Component {
); );
} }
const finalResult = if (this.args.model.showResultAsDiff && this.diffStreamer.suggestion) {
this.finalResult?.length > 0
? this.finalResult
: this.diffStreamer.suggestion;
if (this.args.model.showResultAsDiff && finalResult) {
this.args.model.toolbarEvent.replaceText( this.args.model.toolbarEvent.replaceText(
this.args.model.selectedText, this.args.model.selectedText,
finalResult this.diffStreamer.suggestion
); );
} }
} }
@@ -149,14 +120,10 @@ export default class ModalDiffModal extends Component {
@closeModal={{@closeModal}} @closeModal={{@closeModal}}
> >
<:body> <:body>
<div <div {{didInsert this.subscribe}} {{willDestroy this.unsubscribe}}>
{{didInsert this.subscribe}}
{{willDestroy this.cleanup}}
class="text-preview"
>
{{#if this.loading}} {{#if this.loading}}
<div class="composer-ai-helper-modal__loading"> <div class="composer-ai-helper-modal__loading">
{{~@model.selectedText~}} <CookText @rawText={{@model.selectedText}} />
</div> </div>
{{else}} {{else}}
<div <div
@@ -165,11 +132,10 @@ export default class ModalDiffModal extends Component {
"streamable-content" "streamable-content"
(if this.isStreaming "streaming") (if this.isStreaming "streaming")
(if @model.showResultAsDiff "inline-diff") (if @model.showResultAsDiff "inline-diff")
(if this.diffStreamer.isThinking "thinking")
}} }}
> >
{{~#if @model.showResultAsDiff~}} {{#if @model.showResultAsDiff}}
<span class="diff-inner">{{htmlSafe this.diffResult}}</span> {{htmlSafe this.diffStreamer.diff}}
{{else}} {{else}}
{{#if this.smoothStreamer.isStreaming}} {{#if this.smoothStreamer.isStreaming}}
<CookText <CookText
@@ -19,6 +19,7 @@ export default class AiPostHelperTrigger extends Component {
@tracked menuState = this.MENU_STATES.triggers; @tracked menuState = this.MENU_STATES.triggers;
@tracked showMainButtons = true; @tracked showMainButtons = true;
@tracked showAiButtons = true; @tracked showAiButtons = true;
@tracked originalPostHTML = null;
@tracked postHighlighted = false; @tracked postHighlighted = false;
@tracked currentMenu = this.menu.getByIdentifier( @tracked currentMenu = this.menu.getByIdentifier(
"post-text-selection-toolbar" "post-text-selection-toolbar"
@@ -44,6 +45,7 @@ export default class AiPostHelperTrigger extends Component {
return; return;
} }
this.originalPostHTML = postElement.innerHTML;
this.selectedText = this.args.outletArgs.data.quoteState.buffer; this.selectedText = this.args.outletArgs.data.quoteState.buffer;
const selection = window.getSelection(); const selection = window.getSelection();
@@ -119,15 +121,7 @@ export default class AiPostHelperTrigger extends Component {
return; return;
} }
const highlightedSpans = postElement.querySelectorAll( postElement.innerHTML = this.originalPostHTML;
"span.ai-helper-highlighted-selection"
);
highlightedSpans.forEach((span) => {
const textNode = document.createTextNode(span.textContent);
span.parentNode.replaceChild(textNode, span);
});
this.postHighlighted = false; this.postHighlighted = false;
} }
@@ -7,10 +7,6 @@ import AiSummaryModal from "../../components/modal/ai-summary-modal";
export default class AiSummaryTrigger extends Component { export default class AiSummaryTrigger extends Component {
@service modal; @service modal;
get isAiConversation() {
return this.args.outletArgs.topic.is_bot_pm;
}
@action @action
openAiSummaryModal() { openAiSummaryModal() {
this.modal.show(AiSummaryModal, { this.modal.show(AiSummaryModal, {
@@ -22,17 +18,15 @@ export default class AiSummaryTrigger extends Component {
} }
<template> <template>
{{#unless this.isAiConversation}} {{#if @outletArgs.topic.summarizable}}
{{#if @outletArgs.topic.summarizable}} <section class="topic-map__additional-contents toggle-summary">
<section class="topic-map__additional-contents toggle-summary"> <DButton
<DButton @label="summary.buttons.generate"
@label="summary.buttons.generate" @icon="discourse-sparkles"
@icon="discourse-sparkles" @action={{this.openAiSummaryModal}}
@action={{this.openAiSummaryModal}} class="btn-default ai-summarization-button"
class="btn-default ai-summarization-button" />
/> </section>
</section> {{/if}}
{{/if}}
{{/unless}}
</template> </template>
} }
@@ -1,55 +0,0 @@
import Component from "@glimmer/component";
import { action } from "@ember/object";
import { service } from "@ember/service";
import { or } from "truth-helpers";
import DButton from "discourse/components/d-button";
import Participant from "discourse/components/header/topic/participant";
import AddPmParticipants from "discourse/components/modal/add-pm-participants";
export default class AiConversationInvite extends Component {
static shouldRender(args) {
return args.topic.is_bot_pm;
}
@service site;
@service modal;
@service header;
@service sidebarState;
get participants() {
const participants = [
...(this.header.topicInfo.details?.allowed_users ?? []),
...(this.header.topicInfo.details?.allowed_groups ?? []),
];
return participants;
}
@action
showInvite() {
this.modal.show(AddPmParticipants, {
model: {
title: "discourse_ai.ai_bot.invite_ai_conversation.title",
inviteModel: this.args.outletArgs.topic,
},
});
}
<template>
<div class="ai-conversation__participants">
<DButton
@icon="user-plus"
@label="discourse_ai.ai_bot.invite_ai_conversation.button"
@action={{this.showInvite}}
class="btn-default ai-conversations__invite-button"
/>
{{#each this.participants as |participant|}}
<Participant
@user={{participant}}
@type={{if participant.username "user" "group"}}
@username={{or participant.username participant.name}}
@avatarSize="medium"
/>
{{/each}}
</div>
</template>
}
@@ -0,0 +1,200 @@
import { tracked } from "@glimmer/tracking";
import Controller from "@ember/controller";
import { action } from "@ember/object";
import { getOwner } from "@ember/owner";
import { service } from "@ember/service";
import { TrackedArray } from "@ember-compat/tracked-built-ins";
import { popupAjaxError } from "discourse/lib/ajax-error";
import UppyUpload from "discourse/lib/uppy/uppy-upload";
import UppyMediaOptimization from "discourse/lib/uppy-media-optimization-plugin";
import { clipboardHelpers } from "discourse/lib/utilities";
export default class DiscourseAiBotConversations extends Controller {
@service aiBotConversationsHiddenSubmit;
@service currentUser;
@service mediaOptimizationWorker;
@service site;
@service siteSettings;
@tracked uploads = new TrackedArray();
// Don't track this directly - we'll get it from uppyUpload
textarea = null;
uppyUpload = null;
fileInputEl = null;
_handlePaste = (event) => {
if (document.activeElement !== this.textarea) {
return;
}
const { canUpload, canPasteHtml, types } = clipboardHelpers(event, {
siteSettings: this.siteSettings,
canUpload: true,
});
if (!canUpload || canPasteHtml || types.includes("text/plain")) {
return;
}
if (event && event.clipboardData && event.clipboardData.files) {
this.uppyUpload.addFiles([...event.clipboardData.files], {
pasted: true,
});
}
};
init() {
super.init(...arguments);
this.uppyUpload = new UppyUpload(getOwner(this), {
id: "ai-bot-file-uploader",
type: "ai-bot-conversation",
useMultipartUploadsIfAvailable: true,
uppyReady: () => {
if (this.siteSettings.composer_media_optimization_image_enabled) {
this.uppyUpload.uppyWrapper.useUploadPlugin(UppyMediaOptimization, {
optimizeFn: (data, opts) =>
this.mediaOptimizationWorker.optimizeImage(data, opts),
runParallel: !this.site.isMobileDevice,
});
}
this.uppyUpload.uppyWrapper.onPreProcessProgress((file) => {
const inProgressUpload = this.inProgressUploads?.find(
(upl) => upl.id === file.id
);
if (inProgressUpload && !inProgressUpload.processing) {
inProgressUpload.processing = true;
}
});
this.uppyUpload.uppyWrapper.onPreProcessComplete((file) => {
const inProgressUpload = this.inProgressUploads?.find(
(upl) => upl.id === file.id
);
if (inProgressUpload) {
inProgressUpload.processing = false;
}
});
// Setup paste listener for the textarea
this.textarea?.addEventListener("paste", this._handlePaste);
},
uploadDone: (upload) => {
this.uploads.push(upload);
},
// Fix: Don't try to set inProgressUploads directly
onProgressUploadsChanged: () => {
// This is just for UI triggers - we're already tracking inProgressUploads
this.notifyPropertyChange("inProgressUploads");
},
});
}
willDestroy() {
super.willDestroy(...arguments);
this.textarea?.removeEventListener("paste", this._handlePaste);
this.uppyUpload?.teardown();
}
get loading() {
return this.aiBotConversationsHiddenSubmit?.loading;
}
get inProgressUploads() {
return this.uppyUpload?.inProgressUploads || [];
}
get showUploadsContainer() {
return this.uploads?.length > 0 || this.inProgressUploads?.length > 0;
}
@action
setPersonaId(id) {
this.aiBotConversationsHiddenSubmit.personaId = id;
}
@action
setTargetRecipient(username) {
this.aiBotConversationsHiddenSubmit.targetUsername = username;
}
@action
updateInputValue(value) {
this._autoExpandTextarea();
this.aiBotConversationsHiddenSubmit.inputValue =
value.target?.value || value;
}
@action
handleKeyDown(event) {
if (event.key === "Enter" && !event.shiftKey) {
this.prepareAndSubmitToBot();
}
}
@action
setTextArea(element) {
this.textarea = element;
}
@action
registerFileInput(element) {
if (element) {
this.fileInputEl = element;
if (this.uppyUpload) {
this.uppyUpload.setup(element);
}
}
}
@action
openFileUpload() {
if (this.fileInputEl) {
this.fileInputEl.click();
}
}
@action
removeUpload(upload) {
this.uploads = new TrackedArray(this.uploads.filter((u) => u !== upload));
}
@action
cancelUpload(upload) {
this.uppyUpload.cancelSingleUpload({
fileId: upload.id,
});
}
@action
async prepareAndSubmitToBot() {
// Pass uploads to the service before submitting
this.aiBotConversationsHiddenSubmit.uploads = this.uploads;
try {
await this.aiBotConversationsHiddenSubmit.submitToBot();
this.uploads = new TrackedArray();
} catch (error) {
popupAjaxError(error);
}
}
_autoExpandTextarea() {
this.textarea.style.height = "auto";
this.textarea.style.height = this.textarea.scrollHeight + "px";
// Get the max-height value from CSS (30vh)
const maxHeight = parseInt(getComputedStyle(this.textarea).maxHeight, 10);
// Only enable scrolling if content exceeds max-height
if (this.textarea.scrollHeight > maxHeight) {
this.textarea.style.overflowY = "auto";
} else {
this.textarea.style.overflowY = "hidden";
}
}
}
@@ -2,7 +2,7 @@ import { later } from "@ember/runloop";
import PostUpdater from "./updaters/post-updater"; import PostUpdater from "./updaters/post-updater";
const PROGRESS_INTERVAL = 40; const PROGRESS_INTERVAL = 40;
const GIVE_UP_INTERVAL = 600000; // 10 minutes which is our max thinking time for now const GIVE_UP_INTERVAL = 60000;
export const MIN_LETTERS_PER_INTERVAL = 6; export const MIN_LETTERS_PER_INTERVAL = 6;
const MAX_FLUSH_TIME = 800; const MAX_FLUSH_TIME = 800;
@@ -12,15 +12,13 @@ export default class PostUpdater extends StreamUpdater {
constructor(postStream, postId) { constructor(postStream, postId) {
super(); super();
this.postStream = postStream; this.postStream = postStream;
this.postId = postId; this.postId = postId;
this.post = postStream.findLoadedPost(postId); this.post = postStream.findLoadedPost(postId);
const topicId = postStream.topic.id;
if (this.post) { if (this.post) {
this.postElement = document.querySelector( this.postElement = document.querySelector(
`.topic-area[data-topic-id="${topicId}"] #post_${this.post.post_number}` `#post_${this.post.post_number}`
); );
} }
} }
@@ -59,10 +57,6 @@ export default class PostUpdater extends StreamUpdater {
} }
async setCooked(value) { async setCooked(value) {
if (!this.postElement) {
return;
}
this.post.set("cooked", value); this.post.set("cooked", value);
(await loadMorphlex()).morphInner( (await loadMorphlex()).morphInner(
File diff suppressed because it is too large Load Diff
@@ -5,7 +5,6 @@ import { ADMIN_PANEL, MAIN_PANEL } from "discourse/lib/sidebar/panels";
export const AI_CONVERSATIONS_PANEL = "ai-conversations"; export const AI_CONVERSATIONS_PANEL = "ai-conversations";
export default class AiConversationsSidebarManager extends Service { export default class AiConversationsSidebarManager extends Service {
@service appEvents;
@service sidebarState; @service sidebarState;
@tracked newTopicForceSidebar = false; @tracked newTopicForceSidebar = false;
@@ -27,7 +26,6 @@ export default class AiConversationsSidebarManager extends Service {
this.sidebarState.isForcingSidebar = true; this.sidebarState.isForcingSidebar = true;
document.body.classList.add("has-ai-conversations-sidebar"); document.body.classList.add("has-ai-conversations-sidebar");
this.appEvents.trigger("discourse-ai:force-conversations-sidebar");
return true; return true;
} }
@@ -42,7 +40,6 @@ export default class AiConversationsSidebarManager extends Service {
if (this.sidebarState.isForcingSidebar && !isAdminSidebarActive) { if (this.sidebarState.isForcingSidebar && !isAdminSidebarActive) {
this.sidebarState.setPanel(MAIN_PANEL); // Return to main sidebar panel this.sidebarState.setPanel(MAIN_PANEL); // Return to main sidebar panel
this.sidebarState.isForcingSidebar = false; this.sidebarState.isForcingSidebar = false;
this.appEvents.trigger("discourse-ai:stop-forcing-conversations-sidebar");
} }
} }
} }
@@ -1,4 +1,102 @@
import { fn, hash } from "@ember/helper";
import { on } from "@ember/modifier";
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
import RouteTemplate from "ember-route-template"; import RouteTemplate from "ember-route-template";
import AiBotConversations from "discourse/plugins/discourse-ai/discourse/components/ai-bot-conversations"; import DButton from "discourse/components/d-button";
import PluginOutlet from "discourse/components/plugin-outlet";
import { i18n } from "discourse-i18n";
import AiPersonaLlmSelector from "discourse/plugins/discourse-ai/discourse/components/ai-persona-llm-selector";
export default RouteTemplate(<template><AiBotConversations /></template>); export default RouteTemplate(
<template>
<div class="ai-bot-conversations">
<AiPersonaLlmSelector
@showLabels={{true}}
@setPersonaId={{@controller.setPersonaId}}
@setTargetRecipient={{@controller.setTargetRecipient}}
/>
<div class="ai-bot-conversations__content-wrapper">
<div class="ai-bot-conversations__title">
{{i18n "discourse_ai.ai_bot.conversations.header"}}
</div>
<PluginOutlet
@name="ai-bot-conversations-above-input"
@outletArgs={{hash
[email protected]
[email protected]
}}
/>
<div class="ai-bot-conversations__input-wrapper">
<DButton
@icon="upload"
@action={{@controller.openFileUpload}}
@title="discourse_ai.ai_bot.conversations.upload_files"
class="btn btn-transparent ai-bot-upload-btn"
/>
<textarea
{{didInsert @controller.setTextArea}}
{{on "input" @controller.updateInputValue}}
{{on "keydown" @controller.handleKeyDown}}
id="ai-bot-conversations-input"
autofocus="true"
placeholder={{i18n "discourse_ai.ai_bot.conversations.placeholder"}}
minlength="10"
disabled={{@controller.loading}}
rows="1"
/>
<DButton
@action={{@controller.prepareAndSubmitToBot}}
@icon="paper-plane"
@isLoading={{@controller.loading}}
@title="discourse_ai.ai_bot.conversations.header"
class="ai-bot-button btn-transparent ai-conversation-submit"
/>
<input
type="file"
id="ai-bot-file-uploader"
class="hidden-upload-field"
multiple="multiple"
{{didInsert @controller.registerFileInput}}
/>
</div>
<p class="ai-disclaimer">
{{i18n "discourse_ai.ai_bot.conversations.disclaimer"}}
</p>
{{#if @controller.showUploadsContainer}}
<div class="ai-bot-conversations__uploads-container">
{{#each @controller.uploads as |upload|}}
<div class="ai-bot-upload">
<span class="ai-bot-upload__filename">
{{upload.original_filename}}
</span>
<DButton
@icon="xmark"
@action={{fn @controller.removeUpload upload}}
class="btn-transparent ai-bot-upload__remove"
/>
</div>
{{/each}}
{{#each @controller.inProgressUploads as |upload|}}
<div class="ai-bot-upload ai-bot-upload--in-progress">
<span class="ai-bot-upload__filename">{{upload.fileName}}</span>
<span class="ai-bot-upload__progress">
{{upload.progress}}%
</span>
<DButton
@icon="xmark"
@action={{fn @controller.cancelUpload upload}}
class="btn-flat ai-bot-upload__cancel"
/>
</div>
{{/each}}
</div>
{{/if}}
</div>
</div>
</template>
);
@@ -18,24 +18,12 @@ function initializeAiArtifacts(api) {
"data-ai-artifact-version" "data-ai-artifact-version"
); );
const dataAttributes = {};
for (const attr of artifactElement.attributes) {
if (
attr.name.startsWith("data-") &&
attr.name !== "data-ai-artifact-id" &&
attr.name !== "data-ai-artifact-version"
) {
dataAttributes[attr.name] = attr.value;
}
}
helper.renderGlimmer( helper.renderGlimmer(
artifactElement, artifactElement,
<template> <template>
<AiArtifact <AiArtifact
@artifactId={{artifactId}} @artifactId={{artifactId}}
@artifactVersion={{artifactVersion}} @artifactVersion={{artifactVersion}}
@dataAttributes={{dataAttributes}}
/> />
</template> </template>
); );
@@ -30,6 +30,7 @@ export default {
); );
const appEvents = api.container.lookup("service:app-events"); const appEvents = api.container.lookup("service:app-events");
const messageBus = api.container.lookup("service:message-bus"); const messageBus = api.container.lookup("service:message-bus");
const navigationMenu = api.container.lookup("service:navigationMenu");
api.addSidebarPanel( api.addSidebarPanel(
(BaseCustomSidebarPanel) => (BaseCustomSidebarPanel) =>
@@ -27,7 +27,6 @@ function initializeAiHelperTrigger(api) {
icon: "discourse-sparkles", icon: "discourse-sparkles",
title: "discourse_ai.ai_helper.context_menu.trigger", title: "discourse_ai.ai_helper.context_menu.trigger",
preventFocus: true, preventFocus: true,
hideShortcutInTitle: true,
shortcut: "ALT+P", shortcut: "ALT+P",
shortcutAction: (toolbarEvent) => { shortcutAction: (toolbarEvent) => {
if (toolbarEvent.getText().length === 0) { if (toolbarEvent.getText().length === 0) {
-49
View File
@@ -79,52 +79,3 @@ article.streaming .cooked {
} }
} }
} }
@keyframes mark-blink {
0%,
100% {
border-color: transparent;
}
50% {
border-color: var(--highlight-high);
}
}
@keyframes fade-in-highlight {
from {
opacity: 0.5;
}
to {
opacity: 1;
}
}
mark.highlight {
background-color: var(--highlight-high);
animation: fade-in-highlight 0.5s ease-in-out forwards;
}
.composer-ai-helper-modal__suggestion.thinking mark.highlight {
animation: mark-blink 1s step-start 0s infinite;
animation-name: mark-blink;
}
.composer-ai-helper-modal__loading {
white-space: pre-wrap;
}
.composer-ai-helper-modal__suggestion.inline-diff {
white-space: pre-wrap;
del:last-child {
text-decoration: none;
background-color: transparent;
color: var(--primary-low-mid);
}
.diff-inner {
display: inline;
}
}
@@ -39,22 +39,25 @@ body.has-ai-conversations-sidebar {
.sidebar-section-link.date-heading { .sidebar-section-link.date-heading {
pointer-events: none; pointer-events: none;
cursor: default; cursor: default;
color: var(--d-sidebar-header-color); color: var(--primary-medium);
opacity: 0.8;
font-weight: 700;
margin-top: 1em; margin-top: 1em;
font-weight: bold; font-size: var(--font-down-2);
font-size: var(--font-down-2-rem);
text-transform: uppercase;
letter-spacing: 0.05rem;
} }
.sidebar-section-link { .sidebar-section-link {
height: unset;
padding-block: 0.65em;
font-size: var(--font-down-1); font-size: var(--font-down-1);
letter-spacing: 0.35px;
border-radius: 0 var(--border-radius) var(--border-radius) 0;
.sidebar-section-link-content-text { .sidebar-section-link-content-text {
white-space: normal; white-space: normal;
display: -webkit-box; display: -webkit-box;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
-webkit-line-clamp: 1; -webkit-line-clamp: 2;
overflow: hidden; overflow: hidden;
} }
} }
@@ -75,6 +78,7 @@ body.has-ai-conversations-sidebar {
.private-message-glyph-wrapper, .private-message-glyph-wrapper,
.topic-header-participants, .topic-header-participants,
.topic-above-footer-buttons-outlet, .topic-above-footer-buttons-outlet,
.topic-map,
#topic-footer-buttons .topic-footer-main-buttons details { #topic-footer-buttons .topic-footer-main-buttons details {
display: none; display: none;
} }
@@ -451,82 +455,4 @@ body.has-ai-conversations-sidebar {
display: none; display: none;
} }
} }
.topic-map {
box-sizing: border-box;
width: 100%;
margin: 0 auto;
padding-block: 0.5em;
.topic-map__views-trigger,
.topic-map__likes-trigger,
.summarization-button,
&__private-message-map,
.topic-map__users-list {
display: none;
}
&.--bottom {
padding-top: 0;
}
section {
background: transparent;
border-block: 1px solid var(--primary-low);
}
&__contents {
padding: 0.5em 1.25em;
@include viewport.from(sm) {
padding: 0.5em 0.5em 0.5em 1.9em;
}
}
&__stats {
height: 100%;
flex-wrap: nowrap;
gap: 1em;
}
.ai-conversation__participants {
display: flex;
flex-wrap: wrap;
gap: 0.5em 0.25em;
align-items: center;
.avatar {
width: 2em;
height: 2em;
}
.trigger-group-card {
display: flex;
align-items: center;
border: 1px solid var(--primary-low);
border-radius: var(--d-button-border-radius);
padding: 0.25em 0.5em;
font-size: var(--font-down-1);
span {
position: relative;
top: -1px;
}
.d-icon {
position: relative;
top: 1px;
}
a {
color: var(--primary-medium);
}
}
.btn {
font-size: var(--font-down-1);
margin-right: 0.5em;
}
}
}
} }
@@ -1,11 +1,6 @@
@use "lib/viewport";
.composer-ai-helper-modal { .composer-ai-helper-modal {
.text-preview, .text-preview,
.inline-diff { .inline-diff {
font-family: var(--d-font-family--monospace);
font-variant-ligatures: none;
ins { ins {
background-color: var(--success-low); background-color: var(--success-low);
text-decoration: none; text-decoration: none;
@@ -181,7 +176,7 @@
@keyframes dot-falling { @keyframes dot-falling {
0% { 0% {
box-shadow: 9999px -15px 0 0 rgb(152, 128, 255, 0); box-shadow: 9999px -15px 0 0 rgba(152, 128, 255, 0);
} }
25%, 25%,
@@ -191,13 +186,13 @@
} }
100% { 100% {
box-shadow: 9999px 15px 0 0 rgb(152, 128, 255, 0); box-shadow: 9999px 15px 0 0 rgba(152, 128, 255, 0);
} }
} }
@keyframes dot-falling-before { @keyframes dot-falling-before {
0% { 0% {
box-shadow: 9984px -15px 0 0 rgb(152, 128, 255, 0); box-shadow: 9984px -15px 0 0 rgba(152, 128, 255, 0);
} }
25%, 25%,
@@ -207,13 +202,13 @@
} }
100% { 100% {
box-shadow: 9984px 15px 0 0 rgb(152, 128, 255, 0); box-shadow: 9984px 15px 0 0 rgba(152, 128, 255, 0);
} }
} }
@keyframes dot-falling-after { @keyframes dot-falling-after {
0% { 0% {
box-shadow: 10014px -15px 0 0 rgb(152, 128, 255, 0); box-shadow: 10014px -15px 0 0 rgba(152, 128, 255, 0);
} }
25%, 25%,
@@ -223,7 +218,7 @@
} }
100% { 100% {
box-shadow: 10014px 15px 0 0 rgb(152, 128, 255, 0); box-shadow: 10014px 15px 0 0 rgba(152, 128, 255, 0);
} }
} }
@@ -234,6 +229,19 @@
input:focus { input:focus {
z-index: 1; z-index: 1;
} }
input {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
}
.category-chooser,
.mini-tag-chooser {
.select-kit-header {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
} }
#edit-title { #edit-title {
@@ -628,7 +636,7 @@
height: 7em; height: 7em;
min-width: var(--ai-caption-popup-min-width); min-width: var(--ai-caption-popup-min-width);
@include viewport.until(md) { @include breakpoint(tablet) {
width: 100%; width: 100%;
max-width: unset; max-width: unset;
min-width: unset; min-width: unset;
-2
View File
@@ -603,8 +603,6 @@ ar:
share_ai_conversation: share_ai_conversation:
name: "مشاركة محادثة الذكاء الاصطناعي" name: "مشاركة محادثة الذكاء الاصطناعي"
title: "مشاركة هذه المحادثة بشكلٍ علني" title: "مشاركة هذه المحادثة بشكلٍ علني"
invite_ai_conversation:
button: "دعوة"
ai_label: "الذكاء الاصطناعي" ai_label: "الذكاء الاصطناعي"
ai_title: "المحادثة مع الذكاء الاصطناعي" ai_title: "المحادثة مع الذكاء الاصطناعي"
share_modal: share_modal:
-2
View File
@@ -134,8 +134,6 @@ bg:
request: "Заявка" request: "Заявка"
next_log: "Напред" next_log: "Напред"
previous_log: "Предишни" previous_log: "Предишни"
invite_ai_conversation:
button: "Покана"
share_modal: share_modal:
copy: "Копирай" copy: "Копирай"
conversations: conversations:
-2
View File
@@ -128,8 +128,6 @@ bs_BA:
response: "Odgovor" response: "Odgovor"
next_log: "Iduće" next_log: "Iduće"
previous_log: "Previous" previous_log: "Previous"
invite_ai_conversation:
button: "Invite"
share_modal: share_modal:
copy: "Copy" copy: "Copy"
conversations: conversations:
-2
View File
@@ -133,8 +133,6 @@ ca:
response: "Reacció" response: "Reacció"
next_log: "Següent" next_log: "Següent"
previous_log: "Previ" previous_log: "Previ"
invite_ai_conversation:
button: "Convida"
share_modal: share_modal:
copy: "Còpia" copy: "Còpia"
conversations: conversations:
-45
View File
@@ -211,69 +211,24 @@ cs:
ai_artifact: ai_artifact:
collapse_view_label: "Ukončení celé obrazovky (ESC nebo tlačítko Zpět)" collapse_view_label: "Ukončení celé obrazovky (ESC nebo tlačítko Zpět)"
ai_bot: ai_bot:
pm_warning: "Zprávy chatbotů s umělou inteligencí jsou pravidelně monitorovány moderátory."
cancel_streaming: "Zastavit odpověď" cancel_streaming: "Zastavit odpověď"
default_pm_prefix: "[Nepojmenovaná SZ od AI bota]" default_pm_prefix: "[Nepojmenovaná SZ od AI bota]"
shortcut_title: "Začněte SZ s AI botem" shortcut_title: "Začněte SZ s AI botem"
share: "Kopírovat konverzaci s AI" share: "Kopírovat konverzaci s AI"
conversation_shared: "Konverzace zkopírována"
debug_ai: "Zobrazit nezpracovaný požadavek a odpověď umělé inteligence"
sidebar_empty: "Zde se zobrazí historie konverzace s botem."
debug_ai_modal: debug_ai_modal:
title: "Zobrazit interakci s umělou inteligencí"
copy_request: "Kopírovat požadavek"
copy_response: "Kopírovat odpověď"
request_tokens: "Tokeny požadavku:"
response_tokens: "Tokeny odpovědi:"
request: "Požadavek" request: "Požadavek"
response: "Odpověď" response: "Odpověď"
next_log: "Další" next_log: "Další"
previous_log: "Předchozí" previous_log: "Předchozí"
share_full_topic_modal:
title: "Veřejně sdílet konverzaci"
share: "Sdílet a kopírovat odkaz"
update: "Aktualizovat a zkopírovat odkaz"
delete: "Odstranit sdílení"
share_ai_conversation:
name: "Sdílet konverzaci s AI"
title: "Sdílejte veřejně tuto konverzaci s umělou inteligencí"
invite_ai_conversation:
button: "Pozvat"
title: "Pozvat do konverzace s AI"
ai_title: "Konverzace s AI"
share_modal: share_modal:
title: "Kopírovat konverzaci s AI"
copy: "Kopírovat" copy: "Kopírovat"
context: "Interakce ke sdílení:"
share_tip: "Můžete také sdílet celou konverzaci."
bot_names: bot_names:
fake: "Falešný testovací bot"
claude-3-opus: "Claude 3 Opus"
claude-3-sonnet: "Claude 3 Sonet"
claude-3-haiku: "Haiku Claude 3"
cohere-command-r-plus: "Cohere Command R Plus"
gpt-4: "GPT-4"
gpt-4-turbo: "GPT-4 Turbo"
gpt-4o: "GPT-4 Omni"
gpt-3:
5-turbo: "GPT-3.5"
claude-2: "Claude 2" claude-2: "Claude 2"
gemini-1: gemini-1:
5-pro: "Gemini" 5-pro: "Gemini"
mixtral-8x7B-Instruct-V0: mixtral-8x7B-Instruct-V0:
"1": "Mixtral-8x7B V0.1" "1": "Mixtral-8x7B V0.1"
conversations: conversations:
header: "S čím mohu pomoci?"
submit: "Odeslat otázku"
disclaimer: "Generativní umělá inteligence může dělat chyby. Ověřte si důležité informace."
placeholder: "Zeptejte se..."
new: "Nová otázka"
min_input_length_message:
one: "Zpráva musí mít 1 nebo více znaků"
few: "Zpráva musí mít %{count} nebo více znaků"
many: "Zpráva musí mít %{count} nebo více znaků"
other: "Zpráva musí mít %{count} nebo více znaků"
messages_sidebar_title: "Konverzace"
today: "Dnes" today: "Dnes"
last_7_days: "Posledních 7 dní" last_7_days: "Posledních 7 dní"
last_30_days: "Posledních 30 dní" last_30_days: "Posledních 30 dní"

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