mirror of
https://github.com/discourse/discourse-ai.git
synced 2025-06-25 17:12:16 +00:00
FEATURE: hashtag and mention autocomplete for first bot message (#1342)
Also removes controller which is a deprecated pattern * some comment improvements * remove uneeded code
This commit is contained in:
parent
9ee82fd8be
commit
7316058dfc
372
assets/javascripts/discourse/components/ai-bot-conversations.gjs
Normal file
372
assets/javascripts/discourse/components/ai-bot-conversations.gjs
Normal file
@ -0,0 +1,372 @@
|
|||||||
|
import { tracked } from "@glimmer/tracking";
|
||||||
|
import Component from "@ember/component";
|
||||||
|
import { fn, hash } from "@ember/helper";
|
||||||
|
import { on } from "@ember/modifier";
|
||||||
|
import { action } from "@ember/object";
|
||||||
|
import { getOwner } from "@ember/owner";
|
||||||
|
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
|
||||||
|
import willDestroy from "@ember/render-modifiers/modifiers/will-destroy";
|
||||||
|
import { service } from "@ember/service";
|
||||||
|
import { TrackedArray } from "@ember-compat/tracked-built-ins";
|
||||||
|
import $ from "jquery";
|
||||||
|
import DButton from "discourse/components/d-button";
|
||||||
|
import PluginOutlet from "discourse/components/plugin-outlet";
|
||||||
|
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||||
|
import userAutocomplete from "discourse/lib/autocomplete/user";
|
||||||
|
import { setupHashtagAutocomplete } from "discourse/lib/hashtag-autocomplete";
|
||||||
|
import UppyUpload from "discourse/lib/uppy/uppy-upload";
|
||||||
|
import UppyMediaOptimization from "discourse/lib/uppy-media-optimization-plugin";
|
||||||
|
import userSearch from "discourse/lib/user-search";
|
||||||
|
import {
|
||||||
|
destroyUserStatuses,
|
||||||
|
initUserStatusHtml,
|
||||||
|
renderUserStatusHtml,
|
||||||
|
} from "discourse/lib/user-status-on-autocomplete";
|
||||||
|
import { clipboardHelpers } from "discourse/lib/utilities";
|
||||||
|
import { i18n } from "discourse-i18n";
|
||||||
|
import AiPersonaLlmSelector from "discourse/plugins/discourse-ai/discourse/components/ai-persona-llm-selector";
|
||||||
|
|
||||||
|
export default class AiBotConversations extends Component {
|
||||||
|
@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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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();
|
||||||
|
// needed for safety (textarea may not have a autocomplete)
|
||||||
|
if (this.textarea.autocomplete) {
|
||||||
|
this.textarea.autocomplete("destroy");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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.target.tagName !== "TEXTAREA") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (event.key === "Enter" && !event.shiftKey) {
|
||||||
|
this.prepareAndSubmitToBot();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
setTextArea(element) {
|
||||||
|
this.textarea = element;
|
||||||
|
this.setupAutocomplete(element);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
setupAutocomplete(textarea) {
|
||||||
|
const $textarea = $(textarea);
|
||||||
|
this.applyUserAutocomplete($textarea);
|
||||||
|
this.applyHashtagAutocomplete($textarea);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
applyUserAutocomplete($textarea) {
|
||||||
|
if (!this.siteSettings.enable_mentions) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$textarea.autocomplete({
|
||||||
|
template: userAutocomplete,
|
||||||
|
dataSource: (term) => {
|
||||||
|
destroyUserStatuses();
|
||||||
|
return userSearch({
|
||||||
|
term,
|
||||||
|
includeGroups: true,
|
||||||
|
}).then((result) => {
|
||||||
|
initUserStatusHtml(getOwner(this), result.users);
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onRender: (options) => renderUserStatusHtml(options),
|
||||||
|
key: "@",
|
||||||
|
width: "100%",
|
||||||
|
treatAsTextarea: true,
|
||||||
|
autoSelectFirstSuggestion: true,
|
||||||
|
transformComplete: (obj) => obj.username || obj.name,
|
||||||
|
afterComplete: (text) => {
|
||||||
|
this.textarea.value = text;
|
||||||
|
this.textarea.focus();
|
||||||
|
this.updateInputValue({ target: { value: text } });
|
||||||
|
},
|
||||||
|
onClose: destroyUserStatuses,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
applyHashtagAutocomplete($textarea) {
|
||||||
|
// Use the "topic-composer" configuration or create a specific one for AI bot
|
||||||
|
// You can change this to "chat-composer" if that's more appropriate
|
||||||
|
const hashtagConfig = this.site.hashtag_configurations["topic-composer"];
|
||||||
|
|
||||||
|
setupHashtagAutocomplete(hashtagConfig, $textarea, this.siteSettings, {
|
||||||
|
treatAsTextarea: true,
|
||||||
|
afterComplete: (text) => {
|
||||||
|
this.textarea.value = text;
|
||||||
|
this.textarea.focus();
|
||||||
|
this.updateInputValue({ target: { value: text } });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@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";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="ai-bot-conversations">
|
||||||
|
<AiPersonaLlmSelector
|
||||||
|
@showLabels={{true}}
|
||||||
|
@setPersonaId={{this.setPersonaId}}
|
||||||
|
@setTargetRecipient={{this.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
|
||||||
|
updateInput=this.updateInputValue
|
||||||
|
submit=this.prepareAndSubmitToBot
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="ai-bot-conversations__input-wrapper">
|
||||||
|
<DButton
|
||||||
|
@icon="upload"
|
||||||
|
@action={{this.openFileUpload}}
|
||||||
|
@title="discourse_ai.ai_bot.conversations.upload_files"
|
||||||
|
class="btn btn-transparent ai-bot-upload-btn"
|
||||||
|
/>
|
||||||
|
<textarea
|
||||||
|
{{didInsert this.setTextArea}}
|
||||||
|
{{on "input" this.updateInputValue}}
|
||||||
|
{{on "keydown" this.handleKeyDown}}
|
||||||
|
id="ai-bot-conversations-input"
|
||||||
|
autofocus="true"
|
||||||
|
placeholder={{i18n "discourse_ai.ai_bot.conversations.placeholder"}}
|
||||||
|
minlength="10"
|
||||||
|
disabled={{this.loading}}
|
||||||
|
rows="1"
|
||||||
|
/>
|
||||||
|
<DButton
|
||||||
|
@action={{this.prepareAndSubmitToBot}}
|
||||||
|
@icon="paper-plane"
|
||||||
|
@isLoading={{this.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 this.registerFileInput}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="ai-disclaimer">
|
||||||
|
{{i18n "discourse_ai.ai_bot.conversations.disclaimer"}}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{{#if this.showUploadsContainer}}
|
||||||
|
<div class="ai-bot-conversations__uploads-container">
|
||||||
|
{{#each this.uploads as |upload|}}
|
||||||
|
<div class="ai-bot-upload">
|
||||||
|
<span class="ai-bot-upload__filename">
|
||||||
|
{{upload.original_filename}}
|
||||||
|
</span>
|
||||||
|
<DButton
|
||||||
|
@icon="xmark"
|
||||||
|
@action={{fn this.removeUpload upload}}
|
||||||
|
class="btn-transparent ai-bot-upload__remove"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{{/each}}
|
||||||
|
|
||||||
|
{{#each this.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 this.cancelUpload upload}}
|
||||||
|
class="btn-flat ai-bot-upload__cancel"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
}
|
@ -1,200 +0,0 @@
|
|||||||
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";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,102 +1,4 @@
|
|||||||
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 DButton from "discourse/components/d-button";
|
import AiBotConversations from "discourse/plugins/discourse-ai/discourse/components/ai-bot-conversations";
|
||||||
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(
|
export default RouteTemplate(<template><AiBotConversations /></template>);
|
||||||
<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
|
|
||||||
updateInput=@controller.updateInputValue
|
|
||||||
submit=@controller.prepareAndSubmitToBot
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<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>
|
|
||||||
);
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user