mirror of
https://github.com/discourse/discourse-ai.git
synced 2025-06-27 18:12:18 +00:00
FEATURE: add support for uploads when starting a convo (#1301)
This commit introduces file upload capabilities to the AI Bot conversations interface and improves the overall dedicated UX experience. It also changes the experimental setting to a more permanent one. ## Key changes: - **File upload support**: - Integrates UppyUpload for handling file uploads in conversations - Adds UI for uploading, displaying, and managing attachments - Supports drag & drop, clipboard paste, and manual file selection - Shows upload progress indicators for in-progress uploads - Appends uploaded file markdown to message content - **Renamed setting**: - Changed `ai_enable_experimental_bot_ux` to `ai_bot_enable_dedicated_ux` - Updated setting description to be clearer - Changed default value to `true` as this is now a stable feature - Added migration to handle the setting name change in database - **UI improvements**: - Enhanced input area with better focus states - Improved layout and styling for conversations page - Added visual feedback for upload states - Better error handling for uploads in progress - **Code organization**: - Refactored message submission logic to handle attachments - Updated DOM element IDs for consistency - Fixed focus management after submission - **Added tests**: - Tests for file upload functionality - Tests for removing uploads before submission - Updated existing tests to work with the renamed setting --------- Co-authored-by: awesomerobot <kris.aubuchon@discourse.org>
This commit is contained in:
parent
fc3be6f0ce
commit
8b1b6811f4
@ -39,7 +39,7 @@ export default class AiBotHeaderIcon extends Component {
|
|||||||
get clickShouldRouteOutOfConversations() {
|
get clickShouldRouteOutOfConversations() {
|
||||||
return (
|
return (
|
||||||
!this.navigationMenu.isHeaderDropdownMode &&
|
!this.navigationMenu.isHeaderDropdownMode &&
|
||||||
this.siteSettings.ai_enable_experimental_bot_ux &&
|
this.siteSettings.ai_bot_enable_dedicated_ux &&
|
||||||
this.sidebarState.currentPanel?.key === AI_CONVERSATIONS_PANEL
|
this.sidebarState.currentPanel?.key === AI_CONVERSATIONS_PANEL
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -50,7 +50,7 @@ export default class AiBotHeaderIcon extends Component {
|
|||||||
return this.router.transitionTo(`discovery.${defaultHomepage()}`);
|
return this.router.transitionTo(`discovery.${defaultHomepage()}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.siteSettings.ai_enable_experimental_bot_ux) {
|
if (this.siteSettings.ai_bot_enable_dedicated_ux) {
|
||||||
return this.router.transitionTo("discourse-ai-bot-conversations");
|
return this.router.transitionTo("discourse-ai-bot-conversations");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,21 +1,117 @@
|
|||||||
|
import { tracked } from "@glimmer/tracking";
|
||||||
import Controller from "@ember/controller";
|
import Controller from "@ember/controller";
|
||||||
import { action } from "@ember/object";
|
import { action } from "@ember/object";
|
||||||
|
import { getOwner } from "@ember/owner";
|
||||||
import { service } from "@ember/service";
|
import { service } from "@ember/service";
|
||||||
|
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 {
|
export default class DiscourseAiBotConversations extends Controller {
|
||||||
@service aiBotConversationsHiddenSubmit;
|
@service aiBotConversationsHiddenSubmit;
|
||||||
@service currentUser;
|
@service currentUser;
|
||||||
|
@service mediaOptimizationWorker;
|
||||||
|
@service site;
|
||||||
|
@service siteSettings;
|
||||||
|
|
||||||
|
@tracked uploads = [];
|
||||||
|
// Don't track this directly - we'll get it from uppyUpload
|
||||||
|
|
||||||
textarea = null;
|
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() {
|
init() {
|
||||||
super.init(...arguments);
|
super.init(...arguments);
|
||||||
|
|
||||||
|
this.uploads = [];
|
||||||
|
|
||||||
|
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.pushObject(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() {
|
get loading() {
|
||||||
return this.aiBotConversationsHiddenSubmit?.loading;
|
return this.aiBotConversationsHiddenSubmit?.loading;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get inProgressUploads() {
|
||||||
|
return this.uppyUpload?.inProgressUploads || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
get showUploadsContainer() {
|
||||||
|
return this.uploads?.length > 0 || this.inProgressUploads?.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
setPersonaId(id) {
|
setPersonaId(id) {
|
||||||
this.aiBotConversationsHiddenSubmit.personaId = id;
|
this.aiBotConversationsHiddenSubmit.personaId = id;
|
||||||
@ -36,7 +132,7 @@ export default class DiscourseAiBotConversations extends Controller {
|
|||||||
@action
|
@action
|
||||||
handleKeyDown(event) {
|
handleKeyDown(event) {
|
||||||
if (event.key === "Enter" && !event.shiftKey) {
|
if (event.key === "Enter" && !event.shiftKey) {
|
||||||
this.aiBotConversationsHiddenSubmit.submitToBot();
|
this.prepareAndSubmitToBot();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -45,6 +141,42 @@ export default class DiscourseAiBotConversations extends Controller {
|
|||||||
this.textarea = 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.removeObject(upload);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
cancelUpload(upload) {
|
||||||
|
this.uppyUpload.cancelSingleUpload({
|
||||||
|
fileId: upload.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
prepareAndSubmitToBot() {
|
||||||
|
// Pass uploads to the service before submitting
|
||||||
|
this.aiBotConversationsHiddenSubmit.uploads = this.uploads;
|
||||||
|
this.aiBotConversationsHiddenSubmit.submitToBot();
|
||||||
|
}
|
||||||
|
|
||||||
_autoExpandTextarea() {
|
_autoExpandTextarea() {
|
||||||
this.textarea.style.height = "auto";
|
this.textarea.style.height = "auto";
|
||||||
this.textarea.style.height = this.textarea.scrollHeight + "px";
|
this.textarea.style.height = this.textarea.scrollHeight + "px";
|
||||||
|
@ -4,6 +4,7 @@ import Service, { service } from "@ember/service";
|
|||||||
import { tracked } from "@ember-compat/tracked-built-ins";
|
import { tracked } from "@ember-compat/tracked-built-ins";
|
||||||
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 { getUploadMarkdown } from "discourse/lib/uploads";
|
||||||
import { i18n } from "discourse-i18n";
|
import { i18n } from "discourse-i18n";
|
||||||
|
|
||||||
export default class AiBotConversationsHiddenSubmit extends Service {
|
export default class AiBotConversationsHiddenSubmit extends Service {
|
||||||
@ -17,6 +18,7 @@ export default class AiBotConversationsHiddenSubmit extends Service {
|
|||||||
|
|
||||||
personaId;
|
personaId;
|
||||||
targetUsername;
|
targetUsername;
|
||||||
|
uploads = [];
|
||||||
|
|
||||||
inputValue = "";
|
inputValue = "";
|
||||||
|
|
||||||
@ -25,7 +27,7 @@ export default class AiBotConversationsHiddenSubmit extends Service {
|
|||||||
this.composer.destroyDraft();
|
this.composer.destroyDraft();
|
||||||
this.composer.close();
|
this.composer.close();
|
||||||
next(() => {
|
next(() => {
|
||||||
document.getElementById("custom-homepage-input").focus();
|
document.getElementById("ai-bot-conversations-input").focus();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -41,14 +43,34 @@ export default class AiBotConversationsHiddenSubmit extends Service {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Don't submit if there are still uploads in progress
|
||||||
|
if (document.querySelector(".ai-bot-upload--in-progress")) {
|
||||||
|
return this.dialog.alert({
|
||||||
|
message: i18n("discourse_ai.ai_bot.conversations.uploads_in_progress"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
const title = i18n("discourse_ai.ai_bot.default_pm_prefix");
|
const title = i18n("discourse_ai.ai_bot.default_pm_prefix");
|
||||||
|
|
||||||
|
// Prepare the raw content with any uploads appended
|
||||||
|
let rawContent = this.inputValue;
|
||||||
|
|
||||||
|
// Append upload markdown if we have uploads
|
||||||
|
if (this.uploads && this.uploads.length > 0) {
|
||||||
|
rawContent += "\n\n";
|
||||||
|
|
||||||
|
this.uploads.forEach((upload) => {
|
||||||
|
const uploadMarkdown = getUploadMarkdown(upload);
|
||||||
|
rawContent += uploadMarkdown + "\n";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await ajax("/posts.json", {
|
const response = await ajax("/posts.json", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
data: {
|
data: {
|
||||||
raw: this.inputValue,
|
raw: rawContent,
|
||||||
title,
|
title,
|
||||||
archetype: "private_message",
|
archetype: "private_message",
|
||||||
target_recipients: this.targetUsername,
|
target_recipients: this.targetUsername,
|
||||||
@ -56,11 +78,16 @@ export default class AiBotConversationsHiddenSubmit extends Service {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Reset uploads after successful submission
|
||||||
|
this.uploads = [];
|
||||||
|
this.inputValue = "";
|
||||||
|
|
||||||
this.appEvents.trigger("discourse-ai:bot-pm-created", {
|
this.appEvents.trigger("discourse-ai:bot-pm-created", {
|
||||||
id: response.topic_id,
|
id: response.topic_id,
|
||||||
slug: response.topic_slug,
|
slug: response.topic_slug,
|
||||||
title,
|
title,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.router.transitionTo(response.post_url);
|
this.router.transitionTo(response.post_url);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
popupAjaxError(e);
|
popupAjaxError(e);
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { hash } from "@ember/helper";
|
import { fn, hash } from "@ember/helper";
|
||||||
import { on } from "@ember/modifier";
|
import { on } from "@ember/modifier";
|
||||||
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
|
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
|
||||||
import RouteTemplate from "ember-route-template";
|
import RouteTemplate from "ember-route-template";
|
||||||
@ -17,15 +17,24 @@ export default RouteTemplate(
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="ai-bot-conversations__content-wrapper">
|
<div class="ai-bot-conversations__content-wrapper">
|
||||||
<h1>{{i18n "discourse_ai.ai_bot.conversations.header"}}</h1>
|
<div class="ai-bot-conversations__title">
|
||||||
|
{{i18n "discourse_ai.ai_bot.conversations.header"}}
|
||||||
|
</div>
|
||||||
<PluginOutlet
|
<PluginOutlet
|
||||||
@name="ai-bot-conversations-above-input"
|
@name="ai-bot-conversations-above-input"
|
||||||
@outletArgs={{hash
|
@outletArgs={{hash
|
||||||
updateInput=@controller.updateInputValue
|
updateInput=@controller.updateInputValue
|
||||||
submit=@controller.aiBotConversationsHiddenSubmit.submitToBot
|
submit=@controller.prepareAndSubmitToBot
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="ai-bot-conversations__input-wrapper">
|
<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
|
<textarea
|
||||||
{{didInsert @controller.setTextArea}}
|
{{didInsert @controller.setTextArea}}
|
||||||
{{on "input" @controller.updateInputValue}}
|
{{on "input" @controller.updateInputValue}}
|
||||||
@ -38,13 +47,54 @@ export default RouteTemplate(
|
|||||||
rows="1"
|
rows="1"
|
||||||
/>
|
/>
|
||||||
<DButton
|
<DButton
|
||||||
@action={{@controller.aiBotConversationsHiddenSubmit.submitToBot}}
|
@action={{@controller.prepareAndSubmitToBot}}
|
||||||
@icon="paper-plane"
|
@icon="paper-plane"
|
||||||
@isLoading={{@controller.loading}}
|
@isLoading={{@controller.loading}}
|
||||||
@title="discourse_ai.ai_bot.conversations.header"
|
@title="discourse_ai.ai_bot.conversations.header"
|
||||||
class="ai-bot-button btn-primary ai-conversation-submit"
|
class="ai-bot-button btn-primary ai-conversation-submit"
|
||||||
/>
|
/>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
id="ai-bot-file-uploader"
|
||||||
|
class="hidden-upload-field"
|
||||||
|
multiple="multiple"
|
||||||
|
{{didInsert @controller.registerFileInput}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{{#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__remove"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="ai-disclaimer">
|
<p class="ai-disclaimer">
|
||||||
{{i18n "discourse_ai.ai_bot.conversations.disclaimer"}}
|
{{i18n "discourse_ai.ai_bot.conversations.disclaimer"}}
|
||||||
</p>
|
</p>
|
||||||
|
@ -16,7 +16,7 @@ export default {
|
|||||||
initialize() {
|
initialize() {
|
||||||
withPluginApi((api) => {
|
withPluginApi((api) => {
|
||||||
const siteSettings = api.container.lookup("service:site-settings");
|
const siteSettings = api.container.lookup("service:site-settings");
|
||||||
if (!siteSettings.ai_enable_experimental_bot_ux) {
|
if (!siteSettings.ai_bot_enable_dedicated_ux) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -203,47 +203,75 @@ body.has-ai-conversations-sidebar {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
|
gap: 0.5em;
|
||||||
|
|
||||||
.loading-container {
|
.loading-container {
|
||||||
display: contents;
|
display: contents;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
font-size: var(--font-up-5);
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 0.25em;
|
||||||
|
line-height: var(--line-height-medium);
|
||||||
|
|
||||||
// optical centering for layout balance
|
// optical centering for layout balance
|
||||||
@media screen and (min-height: 600px) {
|
@media screen and (min-height: 600px) {
|
||||||
h1 {
|
margin-top: -6em;
|
||||||
margin-top: -6em;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__input-wrapper {
|
&__input-wrapper {
|
||||||
|
--input-min-height: 2.5em;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: stretch;
|
align-items: end;
|
||||||
gap: 0.5em;
|
flex-wrap: wrap;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
@include viewport.from(sm) {
|
@include viewport.from(sm) {
|
||||||
|
--input-max-width: 46em;
|
||||||
width: 80%;
|
width: 80%;
|
||||||
max-width: 46em;
|
max-width: var(--input-max-width);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ai-conversation-submit {
|
.ai-conversation-submit {
|
||||||
align-self: end;
|
margin-left: 0.5em;
|
||||||
min-height: 2.5em;
|
height: var(--input-min-height);
|
||||||
max-height: 2.5em;
|
}
|
||||||
|
|
||||||
|
.ai-bot-upload-btn {
|
||||||
|
min-height: var(--input-min-height);
|
||||||
|
align-self: stretch;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid var(--primary-low);
|
||||||
|
border-right: none;
|
||||||
|
border-radius: var(--d-button-border-radius) 0 0
|
||||||
|
var(--d-button-border-radius);
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
border-color: var(--tertiary);
|
||||||
|
|
||||||
|
+ #ai-bot-conversations-input {
|
||||||
|
border-left-color: var(--tertiary);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#ai-bot-conversations-input {
|
#ai-bot-conversations-input {
|
||||||
width: 100%;
|
flex-grow: 1;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
resize: none;
|
resize: none;
|
||||||
border-radius: var(--d-button-border-radius);
|
|
||||||
max-height: 30vh;
|
max-height: 30vh;
|
||||||
overflow-y: hidden;
|
min-height: var(--input-min-height);
|
||||||
|
border-radius: 0 var(--d-button-border-radius)
|
||||||
|
var(--d-button-border-radius) 0;
|
||||||
|
border: 1px solid var(--primary-low);
|
||||||
|
|
||||||
&:focus {
|
&:focus-visible {
|
||||||
outline: none;
|
outline: none;
|
||||||
border-color: var(--primary-high);
|
border-color: var(--tertiary);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -252,10 +280,11 @@ body.has-ai-conversations-sidebar {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: var(--font-down-1);
|
font-size: var(--font-down-1);
|
||||||
color: var(--primary-700);
|
color: var(--primary-700);
|
||||||
|
margin-top: 0;
|
||||||
|
|
||||||
@include viewport.from(sm) {
|
@include viewport.from(sm) {
|
||||||
width: 80%;
|
width: 80%;
|
||||||
max-width: 46em;
|
max-width: var(--input-max-width);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -274,6 +303,32 @@ body.has-ai-conversations-sidebar {
|
|||||||
.topic-footer-main-buttons {
|
.topic-footer-main-buttons {
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ai-bot-conversations__uploads-container {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5em;
|
||||||
|
padding-bottom: 1em;
|
||||||
|
margin-top: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-bot-upload {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
border: 1px solid var(--primary-low);
|
||||||
|
border-radius: 10em;
|
||||||
|
padding-left: 0.75em;
|
||||||
|
color: var(--primary-high);
|
||||||
|
font-size: var(--font-down-2);
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus-visible {
|
||||||
|
.d-icon {
|
||||||
|
color: var(--danger);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@include viewport.until(sm) {
|
@include viewport.until(sm) {
|
||||||
|
@ -113,7 +113,7 @@ en:
|
|||||||
ai_discord_search_mode: "Select the search mode to use for Discord search"
|
ai_discord_search_mode: "Select the search mode to use for Discord search"
|
||||||
ai_discord_search_persona: "The persona to use for Discord search."
|
ai_discord_search_persona: "The persona to use for Discord search."
|
||||||
ai_discord_allowed_guilds: "Discord guilds (servers) where the bot is allowed to search"
|
ai_discord_allowed_guilds: "Discord guilds (servers) where the bot is allowed to search"
|
||||||
ai_enable_experimental_bot_ux: "Enable experimental bot UI that allows for a more dedicated experience"
|
ai_bot_enable_dedicated_ux: "Allow for full screen bot interface, instead of a PM"
|
||||||
|
|
||||||
reviewables:
|
reviewables:
|
||||||
reasons:
|
reasons:
|
||||||
|
@ -386,7 +386,8 @@ discourse_ai:
|
|||||||
ai_rag_images_enabled:
|
ai_rag_images_enabled:
|
||||||
default: false
|
default: false
|
||||||
hidden: true
|
hidden: true
|
||||||
ai_enable_experimental_bot_ux:
|
|
||||||
default: false
|
ai_bot_enable_dedicated_ux:
|
||||||
|
default: true
|
||||||
client: true
|
client: true
|
||||||
|
|
||||||
|
@ -0,0 +1,10 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
class RenamedExperimentalAiBotSetting < ActiveRecord::Migration[7.2]
|
||||||
|
def up
|
||||||
|
execute "UPDATE site_settings SET name = 'ai_bot_enable_dedicated_ux' WHERE name = 'ai_enable_experimental_bot_ux'"
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
execute "UPDATE site_settings SET name = 'ai_enable_experimental_bot_ux' WHERE name = 'ai_bot_enable_dedicated_ux'"
|
||||||
|
end
|
||||||
|
end
|
@ -19,6 +19,8 @@ RSpec.describe "AI chat channel summarization", type: :system, js: true do
|
|||||||
end
|
end
|
||||||
|
|
||||||
it "shows the AI bot button, which is clickable (even if group is hidden)" do
|
it "shows the AI bot button, which is clickable (even if group is hidden)" do
|
||||||
|
# test was authored with this in mind
|
||||||
|
SiteSetting.ai_bot_enable_dedicated_ux = false
|
||||||
group.add(user)
|
group.add(user)
|
||||||
group.save
|
group.save
|
||||||
|
|
||||||
|
@ -97,7 +97,7 @@ RSpec.describe "AI Bot - Homepage", type: :system do
|
|||||||
pm.custom_fields[DiscourseAi::AiBot::TOPIC_AI_BOT_PM_FIELD] = "t"
|
pm.custom_fields[DiscourseAi::AiBot::TOPIC_AI_BOT_PM_FIELD] = "t"
|
||||||
pm.save!
|
pm.save!
|
||||||
|
|
||||||
SiteSetting.ai_enable_experimental_bot_ux = true
|
SiteSetting.ai_bot_enable_dedicated_ux = true
|
||||||
SiteSetting.ai_bot_enabled = true
|
SiteSetting.ai_bot_enabled = true
|
||||||
SiteSetting.navigation_menu = "sidebar"
|
SiteSetting.navigation_menu = "sidebar"
|
||||||
Jobs.run_immediately!
|
Jobs.run_immediately!
|
||||||
@ -109,7 +109,53 @@ RSpec.describe "AI Bot - Homepage", type: :system do
|
|||||||
before { SiteSetting.glimmer_post_stream_mode = value }
|
before { SiteSetting.glimmer_post_stream_mode = value }
|
||||||
|
|
||||||
context "when glimmer_post_stream_mode=#{value}" do
|
context "when glimmer_post_stream_mode=#{value}" do
|
||||||
context "when `ai_enable_experimental_bot_ux` is enabled" do
|
context "when `ai_bot_enable_dedicated_ux` is enabled" do
|
||||||
|
it "allows uploading files to a new conversation" do
|
||||||
|
ai_pm_homepage.visit
|
||||||
|
expect(ai_pm_homepage).to have_homepage
|
||||||
|
|
||||||
|
file_path_1 = file_from_fixtures("logo.png", "images").path
|
||||||
|
file_path_2 = file_from_fixtures("logo.jpg", "images").path
|
||||||
|
|
||||||
|
attach_file([file_path_1, file_path_2]) do
|
||||||
|
find(".ai-bot-upload-btn", visible: true).click
|
||||||
|
end
|
||||||
|
|
||||||
|
expect(page).to have_css(".ai-bot-upload", count: 2)
|
||||||
|
|
||||||
|
ai_pm_homepage.input.fill_in(with: "Here are two image attachments")
|
||||||
|
|
||||||
|
responses = ["hello user", "topic title"]
|
||||||
|
DiscourseAi::Completions::Llm.with_prepared_responses(responses) do
|
||||||
|
ai_pm_homepage.submit
|
||||||
|
expect(topic_page).to have_content("Here are two image attachments")
|
||||||
|
expect(page).to have_css(".cooked img", count: 2)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it "allows removing an upload before submission" do
|
||||||
|
ai_pm_homepage.visit
|
||||||
|
expect(ai_pm_homepage).to have_homepage
|
||||||
|
|
||||||
|
file_path = file_from_fixtures("logo.png", "images").path
|
||||||
|
attach_file([file_path]) { find(".ai-bot-upload-btn", visible: true).click }
|
||||||
|
|
||||||
|
expect(page).to have_css(".ai-bot-upload", count: 1)
|
||||||
|
|
||||||
|
find(".ai-bot-upload__remove").click
|
||||||
|
|
||||||
|
expect(page).to have_no_css(".ai-bot-upload")
|
||||||
|
|
||||||
|
ai_pm_homepage.input.fill_in(with: "Message without attachments")
|
||||||
|
|
||||||
|
responses = ["hello user", "topic title"]
|
||||||
|
DiscourseAi::Completions::Llm.with_prepared_responses(responses) do
|
||||||
|
ai_pm_homepage.submit
|
||||||
|
expect(topic_page).to have_content("Message without attachments")
|
||||||
|
expect(page).to have_no_css(".cooked img")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
it "renders landing page on bot click" do
|
it "renders landing page on bot click" do
|
||||||
visit "/"
|
visit "/"
|
||||||
header.click_bot_button
|
header.click_bot_button
|
||||||
@ -296,8 +342,8 @@ RSpec.describe "AI Bot - Homepage", type: :system do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context "when `ai_enable_experimental_bot_ux` is disabled" do
|
context "when `ai_bot_enable_dedicated_ux` is disabled" do
|
||||||
before { SiteSetting.ai_enable_experimental_bot_ux = false }
|
before { SiteSetting.ai_bot_enable_dedicated_ux = false }
|
||||||
|
|
||||||
it "opens composer on bot click" do
|
it "opens composer on bot click" do
|
||||||
visit "/"
|
visit "/"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user