main
js 245 lines 6.56 KB
Raw
1 import { createStore } from "/js/AlpineStore.js";
2 import { store as navStore } from "/components/chat/navigation/chat-navigation-store.js";
3 import * as api from "/js/api.js";
4 import {
5 toastFrontendInfo,
6 NotificationPriority,
7 } from "/components/notifications/notification-store.js";
8 import { sleep } from "/js/sleep.js";
9 import { store as chatsStore } from "/components/sidebar/chats/chats-store.js";
10
11 const model = {
12 // items: [],
13 get items() {
14 return chatsStore.selectedContext?.message_queue || [];
15 },
16
17 pendingItems: [], // Local pending items (uploading to queue)
18
19 _pendingAddOps: {},
20
21 _lastAddToQueuePromise: Promise.resolve(),
22
23 _getQueueScrollerEl() {
24 return document.querySelector(".queue-preview .queue-items");
25 },
26
27 scrollQueueToBottom() {
28 const el = this._getQueueScrollerEl();
29 if (!el) return;
30
31 const scroll = () => {
32 el.scrollTop = el.scrollHeight;
33 };
34
35 requestAnimationFrame(() => {
36 scroll();
37 requestAnimationFrame(scroll);
38 });
39 },
40
41 get hasQueue() {
42 return this.items.length > 0 || this.pendingItems.length > 0;
43 },
44
45 get count() {
46 return this.items.length + this.pendingItems.length;
47 },
48
49 // Combined items for display: confirmed first, then pending at the end
50 get allItems() {
51 return [...this.items, ...this.pendingItems];
52 },
53
54 async addToQueue(text, attachments = []) {
55 const context = globalThis.getContext?.();
56 if (!context) return false;
57
58 // Generate a temporary ID for pending item
59 const tempId = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
60 const pendingItem = {
61 id: tempId,
62 text: text.substring(0, 200) || "(attachment only)",
63 attachments: attachments.map((a) => a.name || a.file?.name || "file"),
64 pending: true,
65 };
66
67 const controller =
68 typeof AbortController !== "undefined" ? new AbortController() : null;
69 this._pendingAddOps = {
70 ...this._pendingAddOps,
71 [tempId]: {
72 canceled: false,
73 controller,
74 },
75 };
76
77 // Add to pending immediately for UI feedback
78 this.pendingItems = [...this.pendingItems, pendingItem];
79 this.scrollQueueToBottom();
80
81 const run = async () => {
82 const op = this._pendingAddOps?.[tempId];
83 if (!op || op.canceled) {
84 this._pendingAddOps = { ...this._pendingAddOps };
85 delete this._pendingAddOps[tempId];
86 return false;
87 }
88
89 try {
90 let filenames = [];
91 if (attachments.length > 0) {
92 const formData = new FormData();
93 for (const att of attachments) {
94 formData.append("file", att.file || att);
95 }
96 const resp = await api.fetchApi("/upload", {
97 method: "POST",
98 body: formData,
99 signal: op.controller ? op.controller.signal : undefined,
100 });
101 if (resp.ok) {
102 const result = await resp.json();
103 filenames = result.filenames || [];
104 }
105 }
106
107 const resp = await api.fetchApi("/message_queue_add", {
108 method: "POST",
109 headers: {
110 "Content-Type": "application/json",
111 },
112 credentials: "same-origin",
113 body: JSON.stringify({
114 context,
115 text,
116 attachments: filenames,
117 item_id: tempId,
118 }),
119 signal: op.controller ? op.controller.signal : undefined,
120 });
121
122 if (!resp || !resp.ok) {
123 return false;
124 }
125
126 const response = await resp.json();
127
128 return response?.ok || false;
129 } catch (e) {
130 if (e?.name !== "AbortError") {
131 console.error("Failed to queue message:", e);
132 }
133 return false;
134 } finally {
135 this._pendingAddOps = { ...this._pendingAddOps };
136 delete this._pendingAddOps[tempId];
137 }
138 };
139
140 // Chain promises to ensure sequential execution
141 const previous = this._lastAddToQueuePromise || Promise.resolve();
142 const chained = previous.catch(() => false).then(run);
143 this._lastAddToQueuePromise = chained.catch(() => false);
144 return await chained;
145 },
146
147 async removeItem(itemId) {
148 const context = globalThis.getContext?.();
149 if (!context) return;
150
151 const isPending = this.pendingItems.some((p) => p.id === itemId);
152 if (isPending) {
153 const op = this._pendingAddOps?.[itemId];
154 if (op) {
155 op.canceled = true;
156 if (op.controller) {
157 op.controller.abort();
158 }
159 }
160 this.pendingItems = this.pendingItems.filter((p) => p.id !== itemId);
161 return;
162 }
163
164 try {
165 await api.callJsonApi("/message_queue_remove", {
166 context,
167 item_id: itemId,
168 });
169 } catch (e) {
170 console.error("Failed to remove from queue:", e);
171 }
172 },
173
174 async clearQueue() {
175 const context = globalThis.getContext?.();
176 if (!context) return;
177 try {
178 await api.callJsonApi("/message_queue_remove", { context });
179 } catch (e) {
180 console.error("Failed to clear queue:", e);
181 }
182 },
183
184 async sendItem(itemId) {
185 const context = globalThis.getContext?.();
186 if (!context) return;
187 try {
188 await api.callJsonApi("/message_queue_send", {
189 context,
190 item_id: itemId,
191 });
192 } catch (e) {
193 console.error("Failed to send queued message:", e);
194 }
195 },
196
197 async sendAll() {
198 const context = globalThis.getContext?.();
199 if (!context || !this.hasQueue) return;
200
201 // check for pending uploads and notify user
202 if (this.pendingItems.length > 0) {
203 await sleep(1000);
204 if (this.pendingItems.length > 0) {
205 toastFrontendInfo(
206 "There are pending uploads in the queue. You can wait for them to finish or remove them.",
207 "Pending uploads",
208 3,
209 "pending-uploads",
210 NotificationPriority.NORMAL,
211 true,
212 );
213 return;
214 }
215 }
216
217 if (!this.hasQueue) return;
218 try {
219 navStore.scrollToBottom();
220 await api.callJsonApi("/message_queue_send", { context, send_all: true });
221 } catch (e) {
222 console.error("Failed to send all queued:", e);
223 }
224 },
225
226 updateFromPoll() {
227 // this.items = queue || [];
228
229 if (this.pendingItems.length > 0) {
230 const serverIds = new Set(this.items.map((i) => i.id).filter(Boolean));
231 this.pendingItems = this.pendingItems.filter((p) => {
232 if (!p.id) return true;
233 return !serverIds.has(p.id);
234 });
235 }
236 // this.scrollQueueToBottom();
237 },
238
239 getAttachmentUrl(filename) {
240 return `/api/image_get?path=/a0/usr/uploads/${encodeURIComponent(filename)}`;
241 },
242 };
243
244 const store = createStore("messageQueue", model);
245 export { store };