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 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";
|
||||
import AiBotConversations from "discourse/plugins/discourse-ai/discourse/components/ai-bot-conversations";
|
||||
|
||||
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
|
||||
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>
|
||||
);
|
||||
export default RouteTemplate(<template><AiBotConversations /></template>);
|
||||
|
Loading…
x
Reference in New Issue
Block a user