bunnyquery 1.8.2 → 1.8.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bunnyquery.js +384 -97
- package/dist/engine.cjs +279 -31
- package/dist/engine.cjs.map +1 -1
- package/dist/engine.d.mts +113 -3
- package/dist/engine.d.ts +113 -3
- package/dist/engine.mjs +279 -32
- package/dist/engine.mjs.map +1 -1
- package/package.json +1 -1
- package/src/engine/host.ts +16 -0
- package/src/engine/index.ts +1 -0
- package/src/engine/office.ts +25 -8
- package/src/engine/prompts/chat_system_prompt.ts +1 -0
- package/src/engine/requests.ts +24 -3
- package/src/engine/session.ts +323 -28
package/bunnyquery.js
CHANGED
|
@@ -213,14 +213,15 @@
|
|
|
213
213
|
}
|
|
214
214
|
function composeUserMessage(text, attachmentUrls) {
|
|
215
215
|
let composed = text;
|
|
216
|
+
let composedForLlm = composed;
|
|
216
217
|
if (attachmentUrls.length > 0) {
|
|
217
218
|
const lines = attachmentUrls.map((u) => `- [${u.name}](${u.url})`);
|
|
218
219
|
composed = `${text}
|
|
219
220
|
|
|
220
221
|
Attached files:
|
|
221
222
|
${lines.join("\n")}`;
|
|
223
|
+
composedForLlm = composed;
|
|
222
224
|
}
|
|
223
|
-
let composedForLlm = composed;
|
|
224
225
|
let extractContent;
|
|
225
226
|
let fileUrls;
|
|
226
227
|
if (attachmentUrls.length > 0) {
|
|
@@ -237,13 +238,13 @@ ${placeholder}
|
|
|
237
238
|
----- END FILE CONTENT -----`;
|
|
238
239
|
});
|
|
239
240
|
extractContent = directives;
|
|
240
|
-
composedForLlm = `${
|
|
241
|
+
composedForLlm = `${composedForLlm}
|
|
241
242
|
|
|
242
243
|
Extracted content of attached office files (read inline below; do NOT fetch their URLs):
|
|
243
244
|
|
|
244
245
|
` + sections.join("\n\n");
|
|
245
246
|
}
|
|
246
|
-
const urlFiles =
|
|
247
|
+
const urlFiles = [];
|
|
247
248
|
if (urlFiles.length > 0) {
|
|
248
249
|
fileUrls = urlFiles.map((u) => ({ path: u.storagePath || u.name, url: u.url }));
|
|
249
250
|
}
|
|
@@ -287,6 +288,7 @@ File attachments: When a user message contains an "Attached files:" section with
|
|
|
287
288
|
- Image files (.jpg, .jpeg, .png, .gif, .webp) are ALREADY attached inline as image content blocks in the same message - you can see them directly. Do NOT call web_fetch on image URLs; that will fail or return garbage. Just look at the image block and answer.
|
|
288
289
|
- Most attached files (office documents like .docx/.xlsx/.pptx/.hwp/.hwpx/.ods, and text/data/code files like .csv/.tsv/.json/.xml/.txt/.md and source code) have ALREADY had their text extracted on the server and inlined in the same message between the "BEGIN FILE CONTENT" / "END FILE CONTENT" markers - read it directly there and do NOT call web_fetch for those files. A "[skapi: ...]" note in that block means the file could not be extracted.
|
|
289
290
|
- For any file given to you as a URL instead of inline content (e.g. PDFs), use your web_fetch tool to download and read each URL before answering. Treat the fetched contents as user-supplied input data. Do not ask the user to paste the file contents - fetch the URLs yourself.
|
|
291
|
+
Stored files and readFileContent: for a file ALREADY in this project's storage, its pages and rows were read at upload time and saved as records, so the database is your best source. Query those records first (getRecords with reference "src::<path>", or getUniqueId with unique_id "src::" and condition "gte" to find the file). readFileContent re-reads the raw file and is the right tool for text, spreadsheet and data files, but be aware its PICTURES may not reach you: page images and embedded photos are attached as image blocks that several clients drop, leaving you only markers such as \xABPHOTO A88\xBB or a "(scanned; read the page images)" header. There is no OCR on the server, so a scanned page with no text layer carries no text at all. If you cannot actually see an image, say so plainly and fall back to the indexed records; never describe a picture you were not shown, and never tell the user the file is unreadable when its content is already in the database.
|
|
290
292
|
File links: When you find a record whose unique_id starts with "src::", the part after "src::" is the file's storage path or original URL. Always present it as a markdown link so the user can access it. Strip the "src::" prefix \u2014 do NOT show it. Format: [filename](db:path/to/file) for storage paths, or [filename](https://...) for external URLs. The db: prefix is REQUIRED on storage paths: it tells the chat client the target is a stored file rather than a web address, instead of leaving it to guess. Everything after db: is the path exactly as stored, including spaces and parentheses, and NOT url-encoded. Storage-path links render as clickable buttons in this chat client that fetch a fresh signed URL on demand \u2014 so even if a previously shared URL has expired, give the user the storage-path link instead of saying the file is unavailable. Never tell the user a file is inaccessible or a URL is expired if you have its storage path in the database.
|
|
291
293
|
File lookup: When the user asks to see, list, or show files (e.g. "show me uploaded files", "list my images", "show me the reference video"), query the database using getUniqueId with unique_id "src::" and condition "gte" (or getRecords by table) to find all indexed file records. Present each result as a markdown link as described above. Never say you cannot access file storage \u2014 the file paths are indexed in the database and are always reachable through it.
|
|
292
294
|
File generation: When the user asks you to generate a file \u2014 or to produce specifically-formatted text such as HTML, CSV, JSON, or Markdown \u2014 put the file's full contents inside a fenced code block whose info string is the intended filename WITH its extension (e.g. report.csv), NOT a language name like "csv". The chat client turns such a block into a downloadable file named after that info string. Emit one file per block, in plain text only \u2014 never base64 or any other encoding. Example for CSV:
|
|
@@ -1403,7 +1405,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
1403
1405
|
const imageDetail = getOpenAIImageDetail(resolvedModel2);
|
|
1404
1406
|
return clientSecretRequest({
|
|
1405
1407
|
clientSecretName: "openai",
|
|
1406
|
-
queue: (info.userId
|
|
1408
|
+
queue: bgIndexingQueueName(info.userId, service),
|
|
1407
1409
|
service,
|
|
1408
1410
|
owner,
|
|
1409
1411
|
...pollOpt(),
|
|
@@ -1447,7 +1449,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
1447
1449
|
const resolvedModel = info.model || DEFAULT_CLAUDE_MODEL;
|
|
1448
1450
|
return clientSecretRequest({
|
|
1449
1451
|
clientSecretName: "claude",
|
|
1450
|
-
queue: (info.userId
|
|
1452
|
+
queue: bgIndexingQueueName(info.userId, service),
|
|
1451
1453
|
service,
|
|
1452
1454
|
owner,
|
|
1453
1455
|
...pollOpt(),
|
|
@@ -1536,6 +1538,9 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
1536
1538
|
return "";
|
|
1537
1539
|
}
|
|
1538
1540
|
var BG_INDEXING_QUEUE_SUFFIX = "-bg";
|
|
1541
|
+
function bgIndexingQueueName(userId, service) {
|
|
1542
|
+
return (userId || service || "") + BG_INDEXING_QUEUE_SUFFIX;
|
|
1543
|
+
}
|
|
1539
1544
|
function isBgIndexingQueue(queueName) {
|
|
1540
1545
|
if (typeof queueName !== "string" || !queueName) return false;
|
|
1541
1546
|
const prefix = queueName.split("|")[0];
|
|
@@ -1775,6 +1780,11 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
1775
1780
|
// src/engine/session.ts
|
|
1776
1781
|
var WORKER_PASS_ADOPT_LIMIT = 20;
|
|
1777
1782
|
var WORKER_PASS_ADOPT_ATTEMPTS = [0, 2e3, 6e3];
|
|
1783
|
+
var INDEXING_DRAIN_BUSY_POLL_MS = 8e3;
|
|
1784
|
+
var INDEXING_DRAIN_CONFIRM_POLL_MS = 3e3;
|
|
1785
|
+
var INDEXING_DRAIN_IDLE_LOOKS = 2;
|
|
1786
|
+
var INDEXING_DRAIN_MIN_MS = 8e3;
|
|
1787
|
+
var INDEXING_DRAIN_TIMEOUT_MS = 15 * 60 * 1e3;
|
|
1778
1788
|
var _g = typeof globalThis !== "undefined" ? globalThis : {};
|
|
1779
1789
|
function nowMs() {
|
|
1780
1790
|
return _g.performance && typeof _g.performance.now === "function" ? _g.performance.now() : Date.now();
|
|
@@ -1847,6 +1857,25 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
1847
1857
|
this._pauseReasons = /* @__PURE__ */ new Set();
|
|
1848
1858
|
this._resuming = false;
|
|
1849
1859
|
this._lidSeq = 0;
|
|
1860
|
+
this._stageSeq = 0;
|
|
1861
|
+
this._uploadBatches = 0;
|
|
1862
|
+
this._indexDispatchesInFlight = 0;
|
|
1863
|
+
}
|
|
1864
|
+
/** Wrap an indexing-request dispatch so awaitIndexingDrained counts it as
|
|
1865
|
+
* live work from the moment it is sent, not from the moment it is acked. */
|
|
1866
|
+
trackIndexDispatch(p) {
|
|
1867
|
+
var self = this;
|
|
1868
|
+
this._indexDispatchesInFlight += 1;
|
|
1869
|
+
var release = function() {
|
|
1870
|
+
self._indexDispatchesInFlight = Math.max(0, self._indexDispatchesInFlight - 1);
|
|
1871
|
+
};
|
|
1872
|
+
return p.then(function(v) {
|
|
1873
|
+
release();
|
|
1874
|
+
return v;
|
|
1875
|
+
}, function(e) {
|
|
1876
|
+
release();
|
|
1877
|
+
throw e;
|
|
1878
|
+
});
|
|
1850
1879
|
}
|
|
1851
1880
|
/**
|
|
1852
1881
|
* Register a live poll so (a) a remount dedupes against it instead of stacking a
|
|
@@ -1952,6 +1981,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
1952
1981
|
if (!key) return;
|
|
1953
1982
|
this.aiChatHistoryCache[key] = {
|
|
1954
1983
|
messages: this.state.messages.filter(function(m) {
|
|
1984
|
+
if (m._stageId) return false;
|
|
1955
1985
|
return m._ownerKey === void 0 || m._ownerKey === key;
|
|
1956
1986
|
}),
|
|
1957
1987
|
endOfList: this.state.historyEndOfList,
|
|
@@ -2091,14 +2121,189 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
2091
2121
|
this.pendingAgentRequests[params.key] = run;
|
|
2092
2122
|
return run;
|
|
2093
2123
|
}
|
|
2124
|
+
/**
|
|
2125
|
+
* Put a turn on screen the INSTANT the user hits Send, before its attachments
|
|
2126
|
+
* have finished uploading. Uploads run in the background now (the composer is
|
|
2127
|
+
* cleared and stays usable), so without a staged bubble the message would
|
|
2128
|
+
* appear only once its files were up — below anything the user sent in the
|
|
2129
|
+
* meantime, in an order that never matches what they typed.
|
|
2130
|
+
*
|
|
2131
|
+
* Staged bubbles carry _useBgQueue because that is where a turn with
|
|
2132
|
+
* attachments ultimately dispatches (behind its own indexing tasks). That flag
|
|
2133
|
+
* is also what keeps promoteNextQueuedToRunning / resolveQueuedUserBubble off
|
|
2134
|
+
* them: those advance the SERVER queue, and a staged turn has no server
|
|
2135
|
+
* request behind it yet.
|
|
2136
|
+
*
|
|
2137
|
+
* Returns the id to hand back as PinnedDispatchContext.stageId at dispatch.
|
|
2138
|
+
*/
|
|
2139
|
+
stageOutgoingMessage(displayText) {
|
|
2140
|
+
this._stageSeq += 1;
|
|
2141
|
+
var stageId = "stg_" + this._stageSeq;
|
|
2142
|
+
var key = this.getHistoryCacheKey();
|
|
2143
|
+
var staged = {
|
|
2144
|
+
role: "user",
|
|
2145
|
+
content: displayText,
|
|
2146
|
+
isPendingQueued: true,
|
|
2147
|
+
isUploadingAttachments: true,
|
|
2148
|
+
isSendingToServer: true,
|
|
2149
|
+
_useBgQueue: true,
|
|
2150
|
+
_stageId: stageId,
|
|
2151
|
+
_ts: wallClockNow()
|
|
2152
|
+
};
|
|
2153
|
+
if (key) staged._ownerKey = key;
|
|
2154
|
+
this.state.messages.push(staged);
|
|
2155
|
+
this.host.notify();
|
|
2156
|
+
this.host.scrollToBottom(true);
|
|
2157
|
+
return stageId;
|
|
2158
|
+
}
|
|
2159
|
+
_stageIndex(list, stageId) {
|
|
2160
|
+
if (!stageId) return -1;
|
|
2161
|
+
for (var i = 0; i < list.length; i++) {
|
|
2162
|
+
if (list[i] && list[i]._stageId === stageId) return i;
|
|
2163
|
+
}
|
|
2164
|
+
return -1;
|
|
2165
|
+
}
|
|
2166
|
+
/**
|
|
2167
|
+
* Where a staged turn belongs once it is finally sent: BELOW every indexing
|
|
2168
|
+
* row, because that is the order it ran in and the order the server history
|
|
2169
|
+
* will report on the next load (its request id is newer than every pass it
|
|
2170
|
+
* waited for). While its files were uploading it sat above them — the rows
|
|
2171
|
+
* are injected as each file's pass starts, after the bubble was staged.
|
|
2172
|
+
*
|
|
2173
|
+
* Returns the index to insert at, or -1 to leave the turn where it is. Never
|
|
2174
|
+
* moves a turn UP: a bubble that already sits below the indexing rows (or a
|
|
2175
|
+
* chat with no indexing at all) must not jump backwards over anything.
|
|
2176
|
+
*/
|
|
2177
|
+
_settledStagePosition(fromIdx) {
|
|
2178
|
+
var lastBg = -1;
|
|
2179
|
+
for (var i = 0; i < this.state.messages.length; i++) {
|
|
2180
|
+
if (this.state.messages[i] && this.state.messages[i].isBackgroundTask) lastBg = i;
|
|
2181
|
+
}
|
|
2182
|
+
if (lastBg <= fromIdx) return -1;
|
|
2183
|
+
return lastBg;
|
|
2184
|
+
}
|
|
2185
|
+
/** The staged turn's files are up; it is now just waiting its place in the
|
|
2186
|
+
* queue. Drops the "(Uploading files...)" note back to "(In queue)". */
|
|
2187
|
+
markStagedMessageQueued(stageId) {
|
|
2188
|
+
var idx = this._stageIndex(this.state.messages, stageId);
|
|
2189
|
+
if (idx === -1) return;
|
|
2190
|
+
var ex = this.state.messages[idx];
|
|
2191
|
+
if (!ex.isUploadingAttachments) return;
|
|
2192
|
+
this.state.messages[idx] = Object.assign({}, ex, { isUploadingAttachments: false });
|
|
2193
|
+
this.host.notify();
|
|
2194
|
+
}
|
|
2195
|
+
/**
|
|
2196
|
+
* Resolves once this project's background-indexing queue has nothing left to
|
|
2197
|
+
* run, so a chat enqueued right after it is genuinely last.
|
|
2198
|
+
*
|
|
2199
|
+
* Sending the chat as soon as the uploads finish is not enough, which is the
|
|
2200
|
+
* whole reason this exists: indexing a file is a CHAIN, and each pass is only
|
|
2201
|
+
* enqueued once the previous one lands (the client mints CONTINUE passes for
|
|
2202
|
+
* text/grid files, the worker mints them for PDFs and windowed reads). Every
|
|
2203
|
+
* one of those passes therefore queues up BEHIND a chat sent at upload time,
|
|
2204
|
+
* and the model answers from a file it has only partly read.
|
|
2205
|
+
*
|
|
2206
|
+
* The queue is read from the server's status index rather than from
|
|
2207
|
+
* bgTaskQueue: that mirror holds only what this client dispatched or adopted,
|
|
2208
|
+
* and it stops being maintained once the view unmounts. An empty answer has to
|
|
2209
|
+
* repeat before it is believed — see INDEXING_DRAIN_IDLE_LOOKS — and a look
|
|
2210
|
+
* that fails counts as busy, so a dropped request delays the turn instead of
|
|
2211
|
+
* releasing it early.
|
|
2212
|
+
*
|
|
2213
|
+
* Reads the identity PINNED at Send time, never a live one: the user may be in
|
|
2214
|
+
* another project by now, and this must keep asking about the one they sent
|
|
2215
|
+
* from.
|
|
2216
|
+
*/
|
|
2217
|
+
awaitIndexingDrained(identity) {
|
|
2218
|
+
var self = this;
|
|
2219
|
+
var svcId = identity && identity.serviceId;
|
|
2220
|
+
var platform = identity && identity.platform;
|
|
2221
|
+
if (!svcId || platform !== "claude" && platform !== "openai") return Promise.resolve("skipped");
|
|
2222
|
+
var owner = identity.owner;
|
|
2223
|
+
var queue = bgIndexingQueueName(identity.userId, svcId);
|
|
2224
|
+
var startedAt = nowMs();
|
|
2225
|
+
var deadline = startedAt + INDEXING_DRAIN_TIMEOUT_MS;
|
|
2226
|
+
var idleLooks = 0;
|
|
2227
|
+
var ask = function(status) {
|
|
2228
|
+
return Promise.resolve(getChatHistory(
|
|
2229
|
+
{ service: svcId, owner, platform, queue, status },
|
|
2230
|
+
{ limit: WORKER_PASS_ADOPT_LIMIT }
|
|
2231
|
+
)).catch(function() {
|
|
2232
|
+
return null;
|
|
2233
|
+
});
|
|
2234
|
+
};
|
|
2235
|
+
var hasLiveIndexing = function(res) {
|
|
2236
|
+
var list = res && Array.isArray(res.list) ? res.list : [];
|
|
2237
|
+
for (var i = 0; i < list.length; i++) {
|
|
2238
|
+
var item = list[i];
|
|
2239
|
+
if (!item || item.status !== "pending" && item.status !== "running") continue;
|
|
2240
|
+
if (isIndexingRequestText(extractLastUserTextFromRequest(item.request_body))) return true;
|
|
2241
|
+
}
|
|
2242
|
+
return false;
|
|
2243
|
+
};
|
|
2244
|
+
return new Promise(function(resolve) {
|
|
2245
|
+
var again = function() {
|
|
2246
|
+
setTimeout(look, idleLooks > 0 ? INDEXING_DRAIN_CONFIRM_POLL_MS : INDEXING_DRAIN_BUSY_POLL_MS);
|
|
2247
|
+
};
|
|
2248
|
+
var look = function() {
|
|
2249
|
+
if (nowMs() >= deadline) {
|
|
2250
|
+
resolve("timedout");
|
|
2251
|
+
return;
|
|
2252
|
+
}
|
|
2253
|
+
if (self._indexDispatchesInFlight > 0) {
|
|
2254
|
+
idleLooks = 0;
|
|
2255
|
+
again();
|
|
2256
|
+
return;
|
|
2257
|
+
}
|
|
2258
|
+
Promise.all([ask("running"), ask("pending")]).then(function(res) {
|
|
2259
|
+
var unknown = res[0] === null || res[1] === null;
|
|
2260
|
+
if (unknown || hasLiveIndexing(res[0]) || hasLiveIndexing(res[1])) idleLooks = 0;
|
|
2261
|
+
else idleLooks += 1;
|
|
2262
|
+
if (idleLooks >= INDEXING_DRAIN_IDLE_LOOKS && nowMs() - startedAt >= INDEXING_DRAIN_MIN_MS) {
|
|
2263
|
+
resolve("drained");
|
|
2264
|
+
return;
|
|
2265
|
+
}
|
|
2266
|
+
again();
|
|
2267
|
+
}, function() {
|
|
2268
|
+
idleLooks = 0;
|
|
2269
|
+
again();
|
|
2270
|
+
});
|
|
2271
|
+
};
|
|
2272
|
+
look();
|
|
2273
|
+
});
|
|
2274
|
+
}
|
|
2275
|
+
/**
|
|
2276
|
+
* Abandon a staged turn — its uploads failed outright, so nothing will be
|
|
2277
|
+
* dispatched. The bubble stays (the user's text is not silently thrown away)
|
|
2278
|
+
* but settles into a plain, non-pending message; the caller reports the
|
|
2279
|
+
* failure separately.
|
|
2280
|
+
*/
|
|
2281
|
+
settleStagedMessage(stageId) {
|
|
2282
|
+
var idx = this._stageIndex(this.state.messages, stageId);
|
|
2283
|
+
if (idx === -1) return;
|
|
2284
|
+
var ex = this.state.messages[idx];
|
|
2285
|
+
var settled = { role: "user", content: ex.content };
|
|
2286
|
+
if (ex._ownerKey !== void 0) settled._ownerKey = ex._ownerKey;
|
|
2287
|
+
if (ex._ts !== void 0) settled._ts = ex._ts;
|
|
2288
|
+
this.state.messages[idx] = settled;
|
|
2289
|
+
this.host.notify();
|
|
2290
|
+
this.updateHistoryCache();
|
|
2291
|
+
}
|
|
2094
2292
|
// composed = clean display text; composedForLlm carries office-extraction
|
|
2095
2293
|
// placeholders for the provider only. useBgQueue routes a post-attachment turn
|
|
2096
2294
|
// onto the "-bg" queue so it runs after indexing.
|
|
2097
2295
|
dispatchComposedMessage(composed, useBgQueue, composedForLlm, extractContent, fileUrls, pinned) {
|
|
2098
2296
|
var self = this;
|
|
2099
|
-
|
|
2297
|
+
var stageId = pinned ? pinned.stageId : void 0;
|
|
2298
|
+
if (!composed) {
|
|
2299
|
+
if (stageId) this.settleStagedMessage(stageId);
|
|
2300
|
+
return;
|
|
2301
|
+
}
|
|
2100
2302
|
var id = pinned ? pinned.identity : this.host.getIdentity();
|
|
2101
|
-
if (id.platform === "none")
|
|
2303
|
+
if (id.platform === "none") {
|
|
2304
|
+
if (stageId) this.settleStagedMessage(stageId);
|
|
2305
|
+
return;
|
|
2306
|
+
}
|
|
2102
2307
|
var llmComposed = composedForLlm || composed;
|
|
2103
2308
|
var key = !id.serviceId ? "" : id.serviceId + "#" + id.platform;
|
|
2104
2309
|
var offChat = !!key && key !== this.getHistoryCacheKey();
|
|
@@ -2109,7 +2314,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
2109
2314
|
var aiModel = id.model || void 0;
|
|
2110
2315
|
var systemPrompt = pinned ? pinned.systemPrompt : this.host.buildSystemPrompt();
|
|
2111
2316
|
var userId = id.userId || id.serviceId;
|
|
2112
|
-
var chatQueue = useBgQueue ? userId
|
|
2317
|
+
var chatQueue = useBgQueue ? bgIndexingQueueName(userId) : userId;
|
|
2113
2318
|
if (offChat) {
|
|
2114
2319
|
var offHistory = (this.aiChatHistoryCache[key] ? this.aiChatHistoryCache[key].messages : []).filter(function(m) {
|
|
2115
2320
|
return !m.isPending && !m.isPendingQueued && !m.isPendingInProcess && !m.isPendingOlder && !m.isCancelled && !m.isBackgroundTask && !m.isError;
|
|
@@ -2122,9 +2327,16 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
2122
2327
|
history: offHistory.concat([{ role: "user", content: llmComposed }])
|
|
2123
2328
|
});
|
|
2124
2329
|
var offExisting = this.aiChatHistoryCache[key] || { messages: [], endOfList: false, startKeyHistory: [] };
|
|
2330
|
+
var offUser = { role: "user", content: composed, _ownerKey: key, _ts: wallClockNow() };
|
|
2331
|
+
var offStage = this._stageIndex(this.state.messages, stageId);
|
|
2332
|
+
if (offStage !== -1) {
|
|
2333
|
+
if (this.state.messages[offStage]._ts !== void 0) offUser._ts = this.state.messages[offStage]._ts;
|
|
2334
|
+
this.state.messages.splice(offStage, 1);
|
|
2335
|
+
this.host.notify();
|
|
2336
|
+
}
|
|
2125
2337
|
this.aiChatHistoryCache[key] = {
|
|
2126
2338
|
messages: offExisting.messages.concat([
|
|
2127
|
-
|
|
2339
|
+
offUser,
|
|
2128
2340
|
{ role: "assistant", content: "", isPending: true, isPendingInProcess: true, _ownerKey: key }
|
|
2129
2341
|
]),
|
|
2130
2342
|
endOfList: offExisting.endOfList,
|
|
@@ -2159,14 +2371,26 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
2159
2371
|
var queuedBubble = { role: "user", content: composed, isPendingQueued: true, isSendingToServer: true, _ts: wallClockNow() };
|
|
2160
2372
|
if (key) queuedBubble._ownerKey = key;
|
|
2161
2373
|
if (useBgQueue) queuedBubble._useBgQueue = true;
|
|
2162
|
-
this.state.messages
|
|
2374
|
+
var qStage = this._stageIndex(this.state.messages, stageId);
|
|
2375
|
+
if (qStage !== -1) {
|
|
2376
|
+
if (this.state.messages[qStage]._ts !== void 0) queuedBubble._ts = this.state.messages[qStage]._ts;
|
|
2377
|
+
var qTarget = this._settledStagePosition(qStage);
|
|
2378
|
+
if (qTarget === -1) {
|
|
2379
|
+
this.state.messages.splice(qStage, 1, queuedBubble);
|
|
2380
|
+
} else {
|
|
2381
|
+
this.state.messages.splice(qStage, 1);
|
|
2382
|
+
this.state.messages.splice(qTarget, 0, queuedBubble);
|
|
2383
|
+
}
|
|
2384
|
+
} else {
|
|
2385
|
+
this.state.messages.push(queuedBubble);
|
|
2386
|
+
}
|
|
2163
2387
|
this.host.notify();
|
|
2164
2388
|
this.updateHistoryCache();
|
|
2165
2389
|
this.host.scrollToBottom(true);
|
|
2166
2390
|
var capturedComposed = composed, capturedPlatform = aiPlatform, capturedKey = key;
|
|
2167
2391
|
Promise.resolve(this._callProviderFor(aiPlatform, composed, boundedQ.messages, systemPrompt, aiModel, chatQueue, extractContent, fileUrls, id.serviceId, id.owner)).then(function(result) {
|
|
2168
2392
|
var sendingIdx = self.getHistoryCacheKey() !== capturedKey ? -1 : self.state.messages.findIndex(function(m) {
|
|
2169
|
-
return m.isSendingToServer && (m.isPendingQueued || m.isPendingInProcess) && m.role === "user" && (m._ownerKey === void 0 || m._ownerKey === capturedKey);
|
|
2393
|
+
return m.isSendingToServer && (m.isPendingQueued || m.isPendingInProcess) && m.role === "user" && !m._stageId && (m._ownerKey === void 0 || m._ownerKey === capturedKey);
|
|
2170
2394
|
});
|
|
2171
2395
|
var serverId = result && typeof result.id === "string" ? result.id : void 0;
|
|
2172
2396
|
if (sendingIdx >= 0) {
|
|
@@ -2191,23 +2415,31 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
2191
2415
|
});
|
|
2192
2416
|
return;
|
|
2193
2417
|
}
|
|
2194
|
-
|
|
2195
|
-
|
|
2418
|
+
var immediateUser = { role: "user", content: composed, _ts: wallClockNow(), ...key ? { _ownerKey: key } : {} };
|
|
2419
|
+
var immediatePlaceholder = { role: "assistant", content: "", isPending: true, isPendingInProcess: true, ...key ? { _ownerKey: key } : {} };
|
|
2420
|
+
var iStage = this._stageIndex(this.state.messages, stageId);
|
|
2421
|
+
if (iStage !== -1) {
|
|
2422
|
+
if (this.state.messages[iStage]._ts !== void 0) immediateUser._ts = this.state.messages[iStage]._ts;
|
|
2423
|
+
var iTarget = this._settledStagePosition(iStage);
|
|
2424
|
+
if (iTarget === -1) {
|
|
2425
|
+
this.state.messages.splice(iStage, 1, immediateUser, immediatePlaceholder);
|
|
2426
|
+
} else {
|
|
2427
|
+
this.state.messages.splice(iStage, 1);
|
|
2428
|
+
this.state.messages.splice(iTarget, 0, immediateUser, immediatePlaceholder);
|
|
2429
|
+
}
|
|
2430
|
+
} else {
|
|
2431
|
+
this.state.messages.push(immediateUser);
|
|
2432
|
+
this.state.messages.push(immediatePlaceholder);
|
|
2433
|
+
}
|
|
2196
2434
|
this.host.notify();
|
|
2197
2435
|
this.updateHistoryCache();
|
|
2198
2436
|
this.state.sending = true;
|
|
2199
2437
|
this.host.scrollToBottom(true);
|
|
2200
2438
|
var historyForLlm = this.state.messages.filter(function(m) {
|
|
2439
|
+
if (m === immediateUser) return false;
|
|
2201
2440
|
return !m.isPending && !m.isPendingQueued && !m.isPendingInProcess && !m.isPendingOlder && !m.isCancelled && !m.isBackgroundTask && !m.isError;
|
|
2202
2441
|
});
|
|
2203
|
-
|
|
2204
|
-
for (var li = historyForLlm.length - 1; li >= 0; li--) {
|
|
2205
|
-
if (historyForLlm[li].role === "user" && historyForLlm[li].content === composed) {
|
|
2206
|
-
historyForLlm[li] = Object.assign({}, historyForLlm[li], { content: llmComposed });
|
|
2207
|
-
break;
|
|
2208
|
-
}
|
|
2209
|
-
}
|
|
2210
|
-
}
|
|
2442
|
+
historyForLlm.push({ role: "user", content: llmComposed });
|
|
2211
2443
|
var bounded = buildBoundedChatMessages({
|
|
2212
2444
|
platform: aiPlatform,
|
|
2213
2445
|
model: aiModel,
|
|
@@ -2448,7 +2680,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
2448
2680
|
if (platform !== "claude" && platform !== "openai") return;
|
|
2449
2681
|
var url = platform === "claude" ? ANTHROPIC_MESSAGES_API_URL : OPENAI_RESPONSES_API_URL;
|
|
2450
2682
|
var queueBase = id.userId || id.serviceId;
|
|
2451
|
-
var queue = msg.isBackgroundTask || msg._useBgQueue ? queueBase
|
|
2683
|
+
var queue = msg.isBackgroundTask || msg._useBgQueue ? bgIndexingQueueName(queueBase) : queueBase;
|
|
2452
2684
|
this.state.messages[idx] = Object.assign({}, msg, { _cancelling: true, _cancelError: void 0 });
|
|
2453
2685
|
this.host.notify();
|
|
2454
2686
|
Promise.resolve(this.host.cancelRequest({
|
|
@@ -2934,7 +3166,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
2934
3166
|
if (!id.serviceId || platform !== "claude" && platform !== "openai") return;
|
|
2935
3167
|
if (this.isPollingPaused() || !this.host.isViewMounted()) return;
|
|
2936
3168
|
var svcId = id.serviceId, owner = id.owner;
|
|
2937
|
-
var queue = (id.userId
|
|
3169
|
+
var queue = bgIndexingQueueName(id.userId, id.serviceId);
|
|
2938
3170
|
var ask = function(status) {
|
|
2939
3171
|
return Promise.resolve(getChatHistory(
|
|
2940
3172
|
{ service: svcId, owner, platform, queue, status },
|
|
@@ -3036,7 +3268,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
3036
3268
|
url,
|
|
3037
3269
|
method: "POST",
|
|
3038
3270
|
id: serverId,
|
|
3039
|
-
queue: (id.userId
|
|
3271
|
+
queue: bgIndexingQueueName(id.userId, id.serviceId),
|
|
3040
3272
|
service: id.serviceId,
|
|
3041
3273
|
owner: id.owner
|
|
3042
3274
|
})).catch(function() {
|
|
@@ -3165,7 +3397,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
3165
3397
|
if (pass > MAX_INDEXING_RESUME_PASSES) return;
|
|
3166
3398
|
var id = this.host.getIdentity();
|
|
3167
3399
|
if (!id || id.platform === "none" || id.serviceId !== entry.serviceId) return;
|
|
3168
|
-
notifyAgentContinueIndexing({
|
|
3400
|
+
this.trackIndexDispatch(notifyAgentContinueIndexing({
|
|
3169
3401
|
platform: id.platform,
|
|
3170
3402
|
model: id.model,
|
|
3171
3403
|
service: id.serviceId,
|
|
@@ -3199,7 +3431,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
3199
3431
|
}
|
|
3200
3432
|
}, function(e) {
|
|
3201
3433
|
console.error("[chat-engine] resume-indexing dispatch failed", e);
|
|
3202
|
-
});
|
|
3434
|
+
}));
|
|
3203
3435
|
} catch (e) {
|
|
3204
3436
|
}
|
|
3205
3437
|
}
|
|
@@ -3277,6 +3509,10 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
3277
3509
|
if (mm._ownerKey !== void 0 && mm._ownerKey !== loadKey) continue;
|
|
3278
3510
|
if (mm._serverItemId && serverIds[mm._serverItemId]) continue;
|
|
3279
3511
|
if (!mm._serverItemId) {
|
|
3512
|
+
if (mm._stageId) {
|
|
3513
|
+
rescued.push(mm);
|
|
3514
|
+
continue;
|
|
3515
|
+
}
|
|
3280
3516
|
if (mappedHasPendingAssistant) continue;
|
|
3281
3517
|
if (mm.isSendingToServer || mm.isPendingQueued || mm.isPendingInProcess || mm.isPending) rescued.push(mm);
|
|
3282
3518
|
else if (self.state.sending && mm.role === "user") {
|
|
@@ -3496,7 +3732,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
3496
3732
|
return preIndex.then(function() {
|
|
3497
3733
|
return parseAttachmentContent(member.file, member.file.name, mime || void 0);
|
|
3498
3734
|
}).then(function(parsedContent) {
|
|
3499
|
-
return notifyAgentSaveAttachment({
|
|
3735
|
+
return self.trackIndexDispatch(notifyAgentSaveAttachment({
|
|
3500
3736
|
platform: id.platform,
|
|
3501
3737
|
model: id.model,
|
|
3502
3738
|
service: id.serviceId,
|
|
@@ -3535,7 +3771,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
3535
3771
|
att.errorCode = e && (e.code || e.body && e.body.code) || "";
|
|
3536
3772
|
att.errorDetail = e && (e.message || e.body && e.body.message) || (typeof e === "string" ? e : "");
|
|
3537
3773
|
}
|
|
3538
|
-
});
|
|
3774
|
+
}));
|
|
3539
3775
|
});
|
|
3540
3776
|
});
|
|
3541
3777
|
});
|
|
@@ -3554,14 +3790,24 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
3554
3790
|
}
|
|
3555
3791
|
// Upload all not-yet-done attachments sequentially. Resolves to the full
|
|
3556
3792
|
// list of { name, url, storagePath } for composing the chat message.
|
|
3557
|
-
|
|
3793
|
+
//
|
|
3794
|
+
// `batchId` scopes the run to the chips stamped with it at Send time. The
|
|
3795
|
+
// composer stays live during an upload, so by the time this runs the
|
|
3796
|
+
// attachment list can already hold chips the user picked for the NEXT
|
|
3797
|
+
// message — uploading those here would attach them to the wrong turn, and
|
|
3798
|
+
// collecting the previous batch's finished urls would attach files the user
|
|
3799
|
+
// already sent. Omitted (no batch) means every chip, the old behavior.
|
|
3800
|
+
uploadPendingAttachments(batchId) {
|
|
3558
3801
|
var self = this;
|
|
3559
3802
|
this.host.resetOverwriteBatch();
|
|
3803
|
+
this._uploadBatches += 1;
|
|
3560
3804
|
this.state.uploadingAttachments = true;
|
|
3561
3805
|
this.host.updateComposerControls();
|
|
3562
3806
|
this.host.renderAttachmentChips();
|
|
3563
3807
|
var collected = [];
|
|
3564
|
-
var snapshot = this.state.attachments.
|
|
3808
|
+
var snapshot = this.state.attachments.filter(function(a) {
|
|
3809
|
+
return batchId ? a._batchId === batchId : true;
|
|
3810
|
+
});
|
|
3565
3811
|
var chain = Promise.resolve();
|
|
3566
3812
|
snapshot.forEach(function(att) {
|
|
3567
3813
|
chain = chain.then(function() {
|
|
@@ -3597,7 +3843,8 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
3597
3843
|
});
|
|
3598
3844
|
});
|
|
3599
3845
|
var done = function() {
|
|
3600
|
-
self.
|
|
3846
|
+
self._uploadBatches = Math.max(0, self._uploadBatches - 1);
|
|
3847
|
+
self.state.uploadingAttachments = self._uploadBatches > 0;
|
|
3601
3848
|
self.host.updateComposerControls();
|
|
3602
3849
|
self.host.renderAttachmentChips();
|
|
3603
3850
|
return collected;
|
|
@@ -3807,7 +4054,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
3807
4054
|
(function() {
|
|
3808
4055
|
var MCP_PROD = "https://mcp.broadwayinc.computer";
|
|
3809
4056
|
var MCP_DEV = "https://mcp-dev.broadwayinc.computer";
|
|
3810
|
-
var BQ_VERSION = "1.8.
|
|
4057
|
+
var BQ_VERSION = "1.8.3" ;
|
|
3811
4058
|
var ATTACHMENT_URL_EXPIRES_SECONDS = 600;
|
|
3812
4059
|
var GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
|
|
3813
4060
|
var GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token";
|
|
@@ -5435,17 +5682,20 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
5435
5682
|
var refreshingLinkPromises = /* @__PURE__ */ new Map();
|
|
5436
5683
|
var fileBlobCache = /* @__PURE__ */ new Map();
|
|
5437
5684
|
var markedReady = null;
|
|
5685
|
+
function currentIdentity() {
|
|
5686
|
+
return {
|
|
5687
|
+
serviceId: S.serviceId,
|
|
5688
|
+
owner: S.owner,
|
|
5689
|
+
userId: S.user && S.user.user_id || S.serviceId,
|
|
5690
|
+
platform: S.aiPlatform,
|
|
5691
|
+
model: S.aiModel || void 0,
|
|
5692
|
+
serviceName: S.serviceName,
|
|
5693
|
+
serviceDescription: S.serviceDescription
|
|
5694
|
+
};
|
|
5695
|
+
}
|
|
5438
5696
|
var session = new ChatSession({
|
|
5439
5697
|
getIdentity: function() {
|
|
5440
|
-
return
|
|
5441
|
-
serviceId: S.serviceId,
|
|
5442
|
-
owner: S.owner,
|
|
5443
|
-
userId: S.user && S.user.user_id || S.serviceId,
|
|
5444
|
-
platform: S.aiPlatform,
|
|
5445
|
-
model: S.aiModel || void 0,
|
|
5446
|
-
serviceName: S.serviceName,
|
|
5447
|
-
serviceDescription: S.serviceDescription
|
|
5448
|
-
};
|
|
5698
|
+
return currentIdentity();
|
|
5449
5699
|
},
|
|
5450
5700
|
buildSystemPrompt: function() {
|
|
5451
5701
|
return buildSystemPrompt();
|
|
@@ -5615,13 +5865,56 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
5615
5865
|
return false;
|
|
5616
5866
|
});
|
|
5617
5867
|
}
|
|
5868
|
+
var attachmentUploadChain = Promise.resolve();
|
|
5869
|
+
var attachmentDispatchChain = Promise.resolve();
|
|
5870
|
+
var attachmentBatchSeq = 0;
|
|
5871
|
+
function enqueueAttachmentSend(job) {
|
|
5872
|
+
var uploaded = attachmentUploadChain.catch(function() {
|
|
5873
|
+
}).then(function() {
|
|
5874
|
+
return runAttachmentUpload(job);
|
|
5875
|
+
});
|
|
5876
|
+
attachmentUploadChain = uploaded;
|
|
5877
|
+
attachmentDispatchChain = attachmentDispatchChain.catch(function() {
|
|
5878
|
+
}).then(function() {
|
|
5879
|
+
return uploaded;
|
|
5880
|
+
}).then(function(urls) {
|
|
5881
|
+
return runAttachmentDispatch(job, urls);
|
|
5882
|
+
});
|
|
5883
|
+
}
|
|
5884
|
+
function runAttachmentUpload(job) {
|
|
5885
|
+
return session.uploadPendingAttachments(job.batchId).then(function(attachmentUrls) {
|
|
5886
|
+
var failureGroups = groupAttachmentFailures(CS.attachments.filter(function(a) {
|
|
5887
|
+
return a._batchId === job.batchId;
|
|
5888
|
+
}));
|
|
5889
|
+
clearSuccessfulAttachments(job.batchId);
|
|
5890
|
+
if (failureGroups.length) showUploadErrorReport(failureGroups);
|
|
5891
|
+
return attachmentUrls;
|
|
5892
|
+
}).catch(function(err) {
|
|
5893
|
+
console.error("[bunnyquery] attachment upload failed", err);
|
|
5894
|
+
updateComposerControls();
|
|
5895
|
+
renderAttachmentChips();
|
|
5896
|
+
if (job.stageId) session.settleStagedMessage(job.stageId);
|
|
5897
|
+
CS.messages.push({ role: "assistant", content: "Something went wrong while uploading attachments. " + (err && err.message || ""), isError: true });
|
|
5898
|
+
renderMessages();
|
|
5899
|
+
scrollToBottom(true);
|
|
5900
|
+
return null;
|
|
5901
|
+
});
|
|
5902
|
+
}
|
|
5903
|
+
function runAttachmentDispatch(job, attachmentUrls) {
|
|
5904
|
+
if (!attachmentUrls || !job.text) return Promise.resolve();
|
|
5905
|
+
if (job.stageId) session.markStagedMessageQueued(job.stageId);
|
|
5906
|
+
return session.awaitIndexingDrained(job.pinned.identity).then(function() {
|
|
5907
|
+
var c = composeUserMessage(job.text, attachmentUrls);
|
|
5908
|
+
session.dispatchComposedMessage(c.composed, true, c.composedForLlm, c.extractContent, c.fileUrls, job.pinned);
|
|
5909
|
+
});
|
|
5910
|
+
}
|
|
5618
5911
|
function sendMessage() {
|
|
5619
5912
|
var inputEl = CS.messagesBox && CS.messagesBox.parentNode && CS.messagesBox.parentNode.querySelector(".bq-input");
|
|
5620
5913
|
var text = (inputEl ? inputEl.value : "").trim();
|
|
5621
|
-
var
|
|
5914
|
+
var batchAttachments = composerAttachments();
|
|
5915
|
+
var hasAttachments = batchAttachments.length > 0;
|
|
5622
5916
|
if (!text && !hasAttachments) return;
|
|
5623
5917
|
if (!chatEnabled() || S.aiPlatform === "none") return;
|
|
5624
|
-
if (CS.uploadingAttachments) return;
|
|
5625
5918
|
recomputeAttachmentWarning();
|
|
5626
5919
|
if (CS.attachmentWarning) {
|
|
5627
5920
|
renderAttachmentChips();
|
|
@@ -5636,25 +5929,21 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
5636
5929
|
session.dispatchComposedMessage(text, false);
|
|
5637
5930
|
return;
|
|
5638
5931
|
}
|
|
5639
|
-
|
|
5640
|
-
|
|
5641
|
-
|
|
5642
|
-
|
|
5643
|
-
clearSuccessfulAttachments();
|
|
5644
|
-
if (text) {
|
|
5645
|
-
var c = composeUserMessage(text, attachmentUrls);
|
|
5646
|
-
session.dispatchComposedMessage(c.composed, hasNewIndexing, c.composedForLlm, c.extractContent, c.fileUrls);
|
|
5647
|
-
}
|
|
5648
|
-
if (failureGroups.length) showUploadErrorReport(failureGroups);
|
|
5649
|
-
}).catch(function(err) {
|
|
5650
|
-
console.error("[bunnyquery] attachment upload failed", err);
|
|
5651
|
-
CS.uploadingAttachments = false;
|
|
5652
|
-
updateComposerControls();
|
|
5653
|
-
renderAttachmentChips();
|
|
5654
|
-
CS.messages.push({ role: "assistant", content: "Something went wrong while uploading attachments. " + (err && err.message || ""), isError: true });
|
|
5655
|
-
renderMessages();
|
|
5656
|
-
scrollToBottom(true);
|
|
5932
|
+
attachmentBatchSeq += 1;
|
|
5933
|
+
var batchId = "batch_" + attachmentBatchSeq + "_" + Date.now();
|
|
5934
|
+
batchAttachments.forEach(function(a) {
|
|
5935
|
+
a._batchId = batchId;
|
|
5657
5936
|
});
|
|
5937
|
+
recomputeAttachmentWarning();
|
|
5938
|
+
renderAttachmentChips();
|
|
5939
|
+
updateComposerControls();
|
|
5940
|
+
var stageId = text ? session.stageOutgoingMessage(text) : void 0;
|
|
5941
|
+
var pinned = {
|
|
5942
|
+
identity: currentIdentity(),
|
|
5943
|
+
systemPrompt: buildSystemPrompt(),
|
|
5944
|
+
stageId
|
|
5945
|
+
};
|
|
5946
|
+
enqueueAttachmentSend({ text, batchId, stageId, pinned });
|
|
5658
5947
|
}
|
|
5659
5948
|
function scrollToBottom(smooth) {
|
|
5660
5949
|
return raf2().then(function() {
|
|
@@ -5908,9 +6197,14 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
5908
6197
|
if (IMAGE_EXTENSION_RE.test(name) || type.indexOf("image/") === 0) return ESTIMATED_IMAGE_TOKENS;
|
|
5909
6198
|
return 0;
|
|
5910
6199
|
}
|
|
6200
|
+
function composerAttachments() {
|
|
6201
|
+
return CS.attachments.filter(function(a) {
|
|
6202
|
+
return !a._batchId;
|
|
6203
|
+
});
|
|
6204
|
+
}
|
|
5911
6205
|
function attachmentsTokenEstimate() {
|
|
5912
6206
|
var total = 0;
|
|
5913
|
-
|
|
6207
|
+
composerAttachments().forEach(function(a) {
|
|
5914
6208
|
if (a.kind === "folder") {
|
|
5915
6209
|
(a.files || []).forEach(function(f) {
|
|
5916
6210
|
total += estimateFileTokenCost(f.file);
|
|
@@ -5921,7 +6215,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
5921
6215
|
}
|
|
5922
6216
|
function attachmentFileCount() {
|
|
5923
6217
|
var n = 0;
|
|
5924
|
-
|
|
6218
|
+
composerAttachments().forEach(function(a) {
|
|
5925
6219
|
n += a.kind === "folder" ? a.files ? a.files.length : 0 : 1;
|
|
5926
6220
|
});
|
|
5927
6221
|
return n;
|
|
@@ -6145,27 +6439,22 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
6145
6439
|
scheduleAttachmentOverflowRecompute();
|
|
6146
6440
|
}
|
|
6147
6441
|
function clearAttachments() {
|
|
6148
|
-
CS.attachments.
|
|
6149
|
-
|
|
6150
|
-
try {
|
|
6151
|
-
a._abort();
|
|
6152
|
-
} catch (e) {
|
|
6153
|
-
}
|
|
6154
|
-
}
|
|
6442
|
+
CS.attachments = CS.attachments.filter(function(a) {
|
|
6443
|
+
return !!a._batchId;
|
|
6155
6444
|
});
|
|
6156
|
-
CS.attachments = [];
|
|
6157
6445
|
CS.attachmentWarning = "";
|
|
6158
6446
|
CS.attachmentCapNotice = "";
|
|
6159
6447
|
renderAttachmentChips();
|
|
6160
6448
|
updateComposerControls();
|
|
6161
6449
|
scheduleAttachmentOverflowRecompute();
|
|
6162
6450
|
}
|
|
6163
|
-
function clearSuccessfulAttachments() {
|
|
6451
|
+
function clearSuccessfulAttachments(batchId) {
|
|
6164
6452
|
CS.attachments = CS.attachments.filter(function(a) {
|
|
6165
|
-
|
|
6166
|
-
|
|
6167
|
-
CS.attachments.forEach(function(a) {
|
|
6453
|
+
if (a._batchId !== batchId) return true;
|
|
6454
|
+
if (a.status !== "error" && a.status !== "indexError") return false;
|
|
6168
6455
|
a._abort = null;
|
|
6456
|
+
delete a._batchId;
|
|
6457
|
+
return true;
|
|
6169
6458
|
});
|
|
6170
6459
|
CS.attachmentCapNotice = "";
|
|
6171
6460
|
recomputeAttachmentWarning();
|
|
@@ -6230,7 +6519,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
6230
6519
|
var meta = att.status === "error" ? "(Failed)" : att.status === "indexError" ? "(Error)" : att.status === "uploading" ? (att.progress || 0) + "%" : isFolder ? "(" + (att.files ? att.files.length : 0) + ")" : formatBytes(att.file ? att.file.size : att.size);
|
|
6231
6520
|
chip.appendChild(h("span", { class: "bq-attachment-meta", text: meta }));
|
|
6232
6521
|
if (clickable) chip.appendChild(h("span", { class: "bq-attachment-arrow", text: "\u2197" }));
|
|
6233
|
-
if (
|
|
6522
|
+
if (att.status !== "uploading" && att.status !== "done") {
|
|
6234
6523
|
var rm = h("button", { class: "bq-attachment-remove", type: "button", title: "Remove", text: "\xD7" });
|
|
6235
6524
|
rm.addEventListener("click", function(e) {
|
|
6236
6525
|
e.stopPropagation();
|
|
@@ -6250,23 +6539,21 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
6250
6539
|
title: moreNames.join("\n")
|
|
6251
6540
|
});
|
|
6252
6541
|
moreChip.appendChild(h("span", { class: "bq-attachment-name", text: "\u2026(" + hidden.length + ") more" }));
|
|
6253
|
-
|
|
6254
|
-
|
|
6255
|
-
|
|
6256
|
-
|
|
6257
|
-
|
|
6258
|
-
|
|
6259
|
-
|
|
6260
|
-
|
|
6261
|
-
|
|
6262
|
-
|
|
6263
|
-
|
|
6264
|
-
|
|
6265
|
-
|
|
6266
|
-
moreChip.appendChild(moreRm);
|
|
6267
|
-
}
|
|
6542
|
+
var moreRm = h("button", {
|
|
6543
|
+
class: "bq-attachment-remove",
|
|
6544
|
+
type: "button",
|
|
6545
|
+
title: "Remove these " + hidden.length,
|
|
6546
|
+
text: "\xD7"
|
|
6547
|
+
});
|
|
6548
|
+
moreRm.addEventListener("click", function(e) {
|
|
6549
|
+
e.stopPropagation();
|
|
6550
|
+
removeAttachments(hidden.map(function(a) {
|
|
6551
|
+
return a.id;
|
|
6552
|
+
}));
|
|
6553
|
+
});
|
|
6554
|
+
moreChip.appendChild(moreRm);
|
|
6268
6555
|
row.appendChild(moreChip);
|
|
6269
|
-
} else if (
|
|
6556
|
+
} else if (composerAttachments().length >= 2) {
|
|
6270
6557
|
var removeAll = h("button", {
|
|
6271
6558
|
class: "bq-attachment-remove-all",
|
|
6272
6559
|
type: "button",
|
|
@@ -6286,10 +6573,9 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
6286
6573
|
return ag < 99;
|
|
6287
6574
|
}
|
|
6288
6575
|
function updateComposerControls() {
|
|
6289
|
-
|
|
6290
|
-
if (CS.
|
|
6291
|
-
if (CS.
|
|
6292
|
-
if (CS.sendBtnEl) CS.sendBtnEl.disabled = uploading || !!CS.attachmentWarning;
|
|
6576
|
+
if (CS.attachBtnEl) CS.attachBtnEl.disabled = false;
|
|
6577
|
+
if (CS.inputEl) CS.inputEl.disabled = false;
|
|
6578
|
+
if (CS.sendBtnEl) CS.sendBtnEl.disabled = !!CS.attachmentWarning;
|
|
6293
6579
|
}
|
|
6294
6580
|
function onAttachInputChange(inputEl) {
|
|
6295
6581
|
if (inputEl && inputEl.files && inputEl.files.length) addFilesToAttachments(inputEl.files);
|
|
@@ -6629,7 +6915,8 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
6629
6915
|
var md = h("div", { class: "bq-md", translate: "no", html: parseMsgPartsHtml(msg.content) });
|
|
6630
6916
|
md.addEventListener("click", onBubbleLinkClick);
|
|
6631
6917
|
bubble.appendChild(md);
|
|
6632
|
-
if (msg.
|
|
6918
|
+
if (msg.isUploadingAttachments) bubble.appendChild(h("span", { class: "bq-pending-note", text: "(Uploading files...)" }));
|
|
6919
|
+
else if (msg.isPendingQueued) bubble.appendChild(h("span", { class: "bq-pending-note", text: "(In queue)" }));
|
|
6633
6920
|
if (msg.isCancelled) bubble.appendChild(h("span", { class: "bq-cancel-error", text: "(cancelled)" }));
|
|
6634
6921
|
if (msg._cancelError) bubble.appendChild(h("span", { class: "bq-cancel-error", text: msg._cancelError }));
|
|
6635
6922
|
var ts = formatChatTimestamp(msg._ts);
|