Skip to content
This repository was archived by the owner on Jul 22, 2025. It is now read-only.

Commit b80d979

Browse files
committed
FEATURE: add support for uploads when starting a convo
(work in progress)
1 parent 8b90ce7 commit b80d979

File tree

3 files changed

+228
-12
lines changed

3 files changed

+228
-12
lines changed

assets/javascripts/discourse/controllers/discourse-ai-bot-conversations.js

Lines changed: 136 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,117 @@
1+
import { tracked } from "@glimmer/tracking";
12
import Controller from "@ember/controller";
23
import { action } from "@ember/object";
4+
import { getOwner } from "@ember/owner";
35
import { service } from "@ember/service";
6+
import UppyUpload from "discourse/lib/uppy/uppy-upload";
7+
import UppyMediaOptimization from "discourse/lib/uppy-media-optimization-plugin";
8+
import { clipboardHelpers } from "discourse/lib/utilities";
49

510
export default class DiscourseAiBotConversations extends Controller {
611
@service aiBotConversationsHiddenSubmit;
712
@service currentUser;
13+
@service mediaOptimizationWorker;
14+
@service site;
15+
@service siteSettings;
16+
17+
@tracked uploads = [];
18+
// Don't track this directly - we'll get it from uppyUpload
819

920
textarea = null;
21+
uppyUpload = null;
22+
fileInputEl = null;
23+
24+
_handlePaste = (event) => {
25+
if (document.activeElement !== this.textarea) {
26+
return;
27+
}
28+
29+
const { canUpload, canPasteHtml, types } = clipboardHelpers(event, {
30+
siteSettings: this.siteSettings,
31+
canUpload: true,
32+
});
33+
34+
if (!canUpload || canPasteHtml || types.includes("text/plain")) {
35+
return;
36+
}
1037

38+
if (event && event.clipboardData && event.clipboardData.files) {
39+
this.uppyUpload.addFiles([...event.clipboardData.files], {
40+
pasted: true,
41+
});
42+
}
43+
};
1144
init() {
1245
super.init(...arguments);
46+
47+
this.uploads = [];
48+
49+
this.uppyUpload = new UppyUpload(getOwner(this), {
50+
id: "ai-bot-file-uploader",
51+
type: "ai-bot-conversation",
52+
useMultipartUploadsIfAvailable: true,
53+
54+
uppyReady: () => {
55+
if (this.siteSettings.composer_media_optimization_image_enabled) {
56+
this.uppyUpload.uppyWrapper.useUploadPlugin(UppyMediaOptimization, {
57+
optimizeFn: (data, opts) =>
58+
this.mediaOptimizationWorker.optimizeImage(data, opts),
59+
runParallel: !this.site.isMobileDevice,
60+
});
61+
}
62+
63+
this.uppyUpload.uppyWrapper.onPreProcessProgress((file) => {
64+
const inProgressUpload = this.inProgressUploads?.find(
65+
(upl) => upl.id === file.id
66+
);
67+
if (inProgressUpload && !inProgressUpload.processing) {
68+
inProgressUpload.processing = true;
69+
}
70+
});
71+
72+
this.uppyUpload.uppyWrapper.onPreProcessComplete((file) => {
73+
const inProgressUpload = this.inProgressUploads?.find(
74+
(upl) => upl.id === file.id
75+
);
76+
if (inProgressUpload) {
77+
inProgressUpload.processing = false;
78+
}
79+
});
80+
81+
// Setup paste listener for the textarea
82+
this.textarea?.addEventListener("paste", this._handlePaste);
83+
},
84+
85+
uploadDone: (upload) => {
86+
this.uploads.pushObject(upload);
87+
},
88+
89+
// Fix: Don't try to set inProgressUploads directly
90+
onProgressUploadsChanged: () => {
91+
// This is just for UI triggers - we're already tracking inProgressUploads
92+
this.notifyPropertyChange("inProgressUploads");
93+
},
94+
});
95+
}
96+
97+
willDestroy() {
98+
super.willDestroy(...arguments);
99+
this.textarea?.removeEventListener("paste", this._handlePaste);
100+
this.uppyUpload?.teardown();
13101
}
14102

15103
get loading() {
16104
return this.aiBotConversationsHiddenSubmit?.loading;
17105
}
18106

107+
get inProgressUploads() {
108+
return this.uppyUpload?.inProgressUploads || [];
109+
}
110+
111+
get showUploadsContainer() {
112+
return this.uploads?.length > 0 || this.inProgressUploads?.length > 0;
113+
}
114+
19115
@action
20116
setPersonaId(id) {
21117
this.aiBotConversationsHiddenSubmit.personaId = id;
@@ -36,13 +132,52 @@ export default class DiscourseAiBotConversations extends Controller {
36132
@action
37133
handleKeyDown(event) {
38134
if (event.key === "Enter" && !event.shiftKey) {
39-
this.aiBotConversationsHiddenSubmit.submitToBot();
135+
this.prepareAndSubmitToBot();
40136
}
41137
}
42138

43139
@action
44140
setTextArea(element) {
45141
this.textarea = element;
142+
if (this.textarea) {
143+
this.textarea.addEventListener("paste", this._handlePaste);
144+
}
145+
}
146+
147+
@action
148+
registerFileInput(element) {
149+
if (element) {
150+
this.fileInputEl = element;
151+
if (this.uppyUpload) {
152+
this.uppyUpload.setup(element);
153+
}
154+
}
155+
}
156+
157+
@action
158+
openFileUpload() {
159+
if (this.fileInputEl) {
160+
this.fileInputEl.click();
161+
}
162+
}
163+
164+
@action
165+
removeUpload(upload) {
166+
this.uploads.removeObject(upload);
167+
}
168+
169+
@action
170+
cancelUpload(upload) {
171+
this.uppyUpload.cancelSingleUpload({
172+
fileId: upload.id,
173+
});
174+
}
175+
176+
@action
177+
prepareAndSubmitToBot() {
178+
// Pass uploads to the service before submitting
179+
this.aiBotConversationsHiddenSubmit.uploads = this.uploads;
180+
this.aiBotConversationsHiddenSubmit.submitToBot();
46181
}
47182

48183
_autoExpandTextarea() {

assets/javascripts/discourse/services/ai-bot-conversations-hidden-submit.js

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export default class AiBotConversationsHiddenSubmit extends Service {
1717

1818
personaId;
1919
targetUsername;
20+
uploads = [];
2021

2122
inputValue = "";
2223

@@ -25,7 +26,7 @@ export default class AiBotConversationsHiddenSubmit extends Service {
2526
this.composer.destroyDraft();
2627
this.composer.close();
2728
next(() => {
28-
document.getElementById("custom-homepage-input").focus();
29+
document.getElementById("ai-bot-conversations-input").focus();
2930
});
3031
}
3132

@@ -41,26 +42,60 @@ export default class AiBotConversationsHiddenSubmit extends Service {
4142
});
4243
}
4344

45+
// Don't submit if there are still uploads in progress
46+
if (document.querySelector(".ai-bot-upload--in-progress")) {
47+
return this.dialog.alert({
48+
message: i18n("discourse_ai.ai_bot.conversations.uploads_in_progress"),
49+
});
50+
}
51+
4452
this.loading = true;
4553
const title = i18n("discourse_ai.ai_bot.default_pm_prefix");
4654

55+
// Prepare the raw content with any uploads appended
56+
let rawContent = this.inputValue;
57+
58+
// Append upload markdown if we have uploads
59+
if (this.uploads && this.uploads.length > 0) {
60+
rawContent += "\n\n";
61+
62+
this.uploads.forEach((upload) => {
63+
const isImage = /jpeg|jpg|png|webp/.test(upload.extension);
64+
const displayName = upload.original_filename || upload.filename;
65+
const uploadMarkdown = isImage
66+
? `![${displayName}|${upload.width}x${upload.height}](${
67+
upload.short_url || upload.url
68+
})`
69+
: `[${displayName}|${upload.filesize || "unknown"}](${
70+
upload.short_url || upload.url
71+
})`;
72+
73+
rawContent += uploadMarkdown + "\n";
74+
});
75+
}
76+
4777
try {
4878
const response = await ajax("/posts.json", {
4979
method: "POST",
5080
data: {
51-
raw: this.inputValue,
81+
raw: rawContent,
5282
title,
5383
archetype: "private_message",
5484
target_recipients: this.targetUsername,
5585
meta_data: { ai_persona_id: this.personaId },
5686
},
5787
});
5888

89+
// Reset uploads after successful submission
90+
this.uploads = [];
91+
this.inputValue = "";
92+
5993
this.appEvents.trigger("discourse-ai:bot-pm-created", {
6094
id: response.topic_id,
6195
slug: response.topic_slug,
6296
title,
6397
});
98+
6499
this.router.transitionTo(response.post_url);
65100
} catch (e) {
66101
popupAjaxError(e);

assets/javascripts/discourse/templates/discourse-ai-bot-conversations.gjs

Lines changed: 55 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { hash } from "@ember/helper";
1+
import { fn, hash } from "@ember/helper";
22
import { on } from "@ember/modifier";
33
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
44
import RouteTemplate from "ember-route-template";
@@ -22,9 +22,37 @@ export default RouteTemplate(
2222
@name="ai-bot-conversations-above-input"
2323
@outletArgs={{hash
2424
updateInput=@controller.updateInputValue
25-
submit=@controller.aiBotConversationsHiddenSubmit.submitToBot
25+
submit=@controller.prepareAndSubmitToBot
2626
}}
2727
/>
28+
29+
{{#if @controller.showUploadsContainer}}
30+
<div class="ai-bot-conversations__uploads-container">
31+
{{#each @controller.uploads as |upload|}}
32+
<div class="ai-bot-upload">
33+
<span class="ai-bot-upload__filename">{{upload.original_filename}}</span>
34+
<DButton
35+
@icon="xmark"
36+
@action={{fn @controller.removeUpload upload}}
37+
class="btn-flat ai-bot-upload__remove"
38+
/>
39+
</div>
40+
{{/each}}
41+
42+
{{#each @controller.inProgressUploads as |upload|}}
43+
<div class="ai-bot-upload ai-bot-upload--in-progress">
44+
<span class="ai-bot-upload__filename">{{upload.fileName}}</span>
45+
<span class="ai-bot-upload__progress">{{upload.progress}}%</span>
46+
<DButton
47+
@icon="xmark"
48+
@action={{fn @controller.cancelUpload upload}}
49+
class="btn-flat ai-bot-upload__remove"
50+
/>
51+
</div>
52+
{{/each}}
53+
</div>
54+
{{/if}}
55+
2856
<div class="ai-bot-conversations__input-wrapper">
2957
<textarea
3058
{{didInsert @controller.setTextArea}}
@@ -36,14 +64,32 @@ export default RouteTemplate(
3664
disabled={{@controller.loading}}
3765
rows="1"
3866
/>
39-
<DButton
40-
@action={{@controller.aiBotConversationsHiddenSubmit.submitToBot}}
41-
@icon="paper-plane"
42-
@isLoading={{@controller.loading}}
43-
@title="discourse_ai.ai_bot.conversations.header"
44-
class="ai-bot-button btn-primary ai-conversation-submit"
45-
/>
67+
<div class="ai-bot-conversations__buttons">
68+
<DButton
69+
@icon="upload"
70+
@action={{@controller.openFileUpload}}
71+
@title="discourse_ai.ai_bot.conversations.upload_files"
72+
class="btn no-text ai-bot-upload-btn"
73+
/>
74+
<DButton
75+
@action={{@controller.prepareAndSubmitToBot}}
76+
@icon="paper-plane"
77+
@isLoading={{@controller.loading}}
78+
@title="discourse_ai.ai_bot.conversations.header"
79+
class="ai-bot-button btn-primary ai-conversation-submit"
80+
/>
81+
</div>
4682
</div>
83+
84+
{{! Hidden file input element }}
85+
<input
86+
type="file"
87+
id="ai-bot-file-uploader"
88+
class="hidden-upload-field"
89+
multiple="multiple"
90+
{{didInsert @controller.registerFileInput}}
91+
/>
92+
4793
<p class="ai-disclaimer">
4894
{{i18n "discourse_ai.ai_bot.conversations.disclaimer"}}
4995
</p>

0 commit comments

Comments
 (0)