bunnyquery 1.8.2 → 1.8.4
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/README.md +38 -39
- package/bunnyquery.css +108 -2
- package/bunnyquery.js +1859 -310
- package/dist/engine.cjs +1503 -188
- package/dist/engine.cjs.map +1 -1
- package/dist/engine.d.mts +906 -37
- package/dist/engine.d.ts +906 -37
- package/dist/engine.mjs +1480 -189
- package/dist/engine.mjs.map +1 -1
- package/package.json +1 -1
- package/src/engine/budget.ts +11 -11
- package/src/engine/history.ts +23 -6
- package/src/engine/host.ts +77 -3
- package/src/engine/image_preview.ts +0 -0
- package/src/engine/index.ts +13 -0
- package/src/engine/indexing_groups.ts +323 -6
- package/src/engine/link_markup.ts +124 -0
- package/src/engine/links.ts +159 -26
- package/src/engine/office.ts +25 -8
- package/src/engine/prompts/chat_system_prompt.ts +24 -13
- package/src/engine/prompts/indexing_system_prompt.ts +19 -11
- package/src/engine/prompts/indexing_user_message.ts +32 -22
- package/src/engine/requests.ts +302 -14
- package/src/engine/session.ts +1424 -114
- package/src/engine/viewport_fill.ts +51 -4
- package/styles/chat.css +108 -2
|
@@ -151,10 +151,51 @@ export async function fillHistoryViewport(opts: FillHistoryViewportOptions): Pro
|
|
|
151
151
|
* come true are dropped as it goes, so the cost stays flat.
|
|
152
152
|
*/
|
|
153
153
|
export function createHistoryFiller(
|
|
154
|
-
base: Omit<FillHistoryViewportOptions, 'isSatisfied'
|
|
154
|
+
base: Omit<FillHistoryViewportOptions, 'isSatisfied'> & {
|
|
155
|
+
/** Fired when the loop starts FETCHING and when it stops, and only on a real
|
|
156
|
+
* change.
|
|
157
|
+
*
|
|
158
|
+
* This — not the caller's own per-request `isLoading` — is what "older
|
|
159
|
+
* history is still coming in" means to a view. A fill is many pages, and
|
|
160
|
+
* `isLoading` drops to false between every one of them, so anything
|
|
161
|
+
* rendered off it flickers once per page for the whole loop. A collapsed
|
|
162
|
+
* indexing row whose run begins above the loaded window renders exactly
|
|
163
|
+
* that ("still loading this run" vs a status it cannot know yet), which is
|
|
164
|
+
* why the loop has to publish its own span.
|
|
165
|
+
*
|
|
166
|
+
* Fetching, NOT requested. Most fills fetch nothing: they are fired on every
|
|
167
|
+
* window resize, every row a user collapses, and every first-page load, and
|
|
168
|
+
* the overwhelmingly common outcome is `isSatisfied` returning true on the
|
|
169
|
+
* first look. Announcing at request time published a true/false pair for
|
|
170
|
+
* each of those, and the widget's own satisfied-check spans two animation
|
|
171
|
+
* frames — long enough for the browser to PAINT the intermediate state. Every
|
|
172
|
+
* collapsed row strobed through "loading" on every resize tick. So the span
|
|
173
|
+
* opens at the first actual page request, which is also the first moment the
|
|
174
|
+
* claim is true. */
|
|
175
|
+
onRunningChange?: (running: boolean) => void;
|
|
176
|
+
},
|
|
155
177
|
): { fill: (isSatisfied: () => boolean | Promise<boolean>) => Promise<void>; isRunning: () => boolean } {
|
|
156
178
|
var pending: Array<() => boolean | Promise<boolean>> = [];
|
|
179
|
+
// One loop at a time. Set the instant a fill is REQUESTED, whether or not that
|
|
180
|
+
// fill goes on to fetch anything.
|
|
157
181
|
var running = false;
|
|
182
|
+
// What the view is told: a page is actually being fetched. Deliberately a
|
|
183
|
+
// different fact from `running` — see onRunningChange.
|
|
184
|
+
var fetching = false;
|
|
185
|
+
|
|
186
|
+
// The callback can NEVER break the loop, and the swallow is the point. It is a
|
|
187
|
+
// view render (the widget rebuilds its whole message list from here), so it is
|
|
188
|
+
// the least trustworthy code this module touches, and it is invoked from two
|
|
189
|
+
// places that must both survive it: inside the fetch wrapper, and inside `done`
|
|
190
|
+
// — where a throw would reject the promise `fill()` returns and leave the
|
|
191
|
+
// closing edge unsent, pinning every row that renders off this at "loading"
|
|
192
|
+
// forever. A view that cannot paint is the view's problem, not the pager's.
|
|
193
|
+
function announce(next: boolean): void {
|
|
194
|
+
if (fetching === next) return;
|
|
195
|
+
fetching = next;
|
|
196
|
+
if (!base.onRunningChange) return;
|
|
197
|
+
try { base.onRunningChange(next); } catch (e) { /* never break the pager */ }
|
|
198
|
+
}
|
|
158
199
|
|
|
159
200
|
async function allSatisfied(): Promise<boolean> {
|
|
160
201
|
var next: Array<() => boolean | Promise<boolean>> = [];
|
|
@@ -166,18 +207,24 @@ export function createHistoryFiller(
|
|
|
166
207
|
}
|
|
167
208
|
|
|
168
209
|
return {
|
|
169
|
-
|
|
210
|
+
// The published fact, so a view and `isRunning()` can never disagree about
|
|
211
|
+
// what they are showing. A fill that never fetches is not something anyone
|
|
212
|
+
// outside this module has any use for knowing about.
|
|
213
|
+
isRunning: function () { return fetching; },
|
|
170
214
|
fill: function (isSatisfied) {
|
|
171
215
|
pending.push(isSatisfied);
|
|
172
216
|
if (running) return Promise.resolve();
|
|
173
217
|
running = true;
|
|
174
|
-
var done = function () {
|
|
218
|
+
var done = function () { pending = []; running = false; announce(false); };
|
|
175
219
|
return fillHistoryViewport({
|
|
176
220
|
isSatisfied: allSatisfied,
|
|
177
221
|
isEndOfList: base.isEndOfList,
|
|
178
222
|
isLoading: base.isLoading,
|
|
179
223
|
messageCount: base.messageCount,
|
|
180
|
-
|
|
224
|
+
// The span opens HERE, at the first real page request: past
|
|
225
|
+
// isEndOfList, past isStale, past isSatisfied. Everything before this
|
|
226
|
+
// point is a fill that concluded there was nothing to do.
|
|
227
|
+
fetchOlder: function () { announce(true); return base.fetchOlder(); },
|
|
181
228
|
isStale: base.isStale,
|
|
182
229
|
maxPages: base.maxPages,
|
|
183
230
|
}).then(done, done);
|
package/styles/chat.css
CHANGED
|
@@ -123,7 +123,17 @@
|
|
|
123
123
|
border: 1px solid var(--bq-line);
|
|
124
124
|
background: rgba(127, 127, 127, 0.05);
|
|
125
125
|
}
|
|
126
|
+
/* Three states, three colours, and the row is readable at a glance without
|
|
127
|
+
opening it:
|
|
128
|
+
yellow working on it now (.is-active)
|
|
129
|
+
green indexed, confirmed (.is-indexed)
|
|
130
|
+
grey not known yet (.is-resolving, and the base look)
|
|
131
|
+
Grey is the DEFAULT rather than a colour of its own, so anything that has not
|
|
132
|
+
earned green or yellow reads as "no claim" — which is also what a cancelled
|
|
133
|
+
row should look like, and what an unrecognised future state would fall back
|
|
134
|
+
to. Red stays reserved for a failure. */
|
|
126
135
|
.bq-index-group.is-active { border-color: var(--bq-warning-border); background: var(--bq-warning-bg); }
|
|
136
|
+
.bq-index-group.is-indexed { border-color: var(--bq-success-border); background: var(--bq-success-bg); }
|
|
127
137
|
.bq-index-group.is-error { border-color: var(--bq-danger); background: var(--bq-danger-bg); }
|
|
128
138
|
|
|
129
139
|
.bq-index-head {
|
|
@@ -146,7 +156,12 @@
|
|
|
146
156
|
}
|
|
147
157
|
.bq-index-head:hover { background: rgba(127, 127, 127, 0.08); color: var(--bq-muted); }
|
|
148
158
|
.bq-index-group.is-active .bq-index-head { color: var(--bq-warning); }
|
|
159
|
+
.bq-index-group.is-indexed .bq-index-head { color: var(--bq-success); }
|
|
149
160
|
.bq-index-group.is-error .bq-index-head { color: var(--bq-danger); }
|
|
161
|
+
/* Stated rather than inherited: a waiting row keeping the muted default is a
|
|
162
|
+
DECISION (it is not claiming a state), not an oversight, and writing it here
|
|
163
|
+
means adding a colour to the base rule cannot silently give it one. */
|
|
164
|
+
.bq-index-group.is-resolving .bq-index-head { color: var(--bq-muted); }
|
|
150
165
|
|
|
151
166
|
/* Status glyph. The active one is a circular-arrow SVG the consumer inlines
|
|
152
167
|
(never an icon font: agent.vue's Material Symbols build gates glyphs behind
|
|
@@ -162,8 +177,17 @@
|
|
|
162
177
|
.bq-index-icon svg { width: 100%; height: 100%; display: block; }
|
|
163
178
|
.bq-index-group.is-active .bq-index-icon svg { animation: bq-index-spin 1.1s linear infinite; }
|
|
164
179
|
@keyframes bq-index-spin { to { transform: rotate(360deg); } }
|
|
180
|
+
/* Waiting to find out (a run whose earlier passes are still being paged in, a
|
|
181
|
+
file whose queue state has not been answered). Deliberately NOT the active
|
|
182
|
+
look: no warning colour and no spin, because a spinning glyph on a chat row is
|
|
183
|
+
read as "this file is being worked on right now" and that is the one thing this
|
|
184
|
+
state does not know. A slow fade says "pending" without claiming progress. */
|
|
185
|
+
.bq-index-group.is-resolving .bq-index-icon svg { animation: bq-index-fade 1.6s ease-in-out infinite; }
|
|
186
|
+
@keyframes bq-index-fade { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } }
|
|
165
187
|
@media (prefers-reduced-motion: reduce) {
|
|
166
188
|
.bq-index-group.is-active .bq-index-icon svg { animation-duration: 3s; }
|
|
189
|
+
/* A fade has no end state to settle on, so it stops rather than slows. */
|
|
190
|
+
.bq-index-group.is-resolving .bq-index-icon svg { animation: none; opacity: 0.7; }
|
|
167
191
|
}
|
|
168
192
|
|
|
169
193
|
.bq-index-label {
|
|
@@ -222,8 +246,28 @@
|
|
|
222
246
|
border-left: 2px solid var(--bq-line);
|
|
223
247
|
}
|
|
224
248
|
.bq-message.bq-index-pass .bq-bubble { font-size: 0.78rem; }
|
|
225
|
-
/* Close the run: the next ordinary message needs its normal breathing room.
|
|
226
|
-
.bq-message
|
|
249
|
+
/* Close the run: the next ordinary message needs its normal breathing room.
|
|
250
|
+
NOT scoped to .bq-message: the trailing loader below is a run member too, and
|
|
251
|
+
requiring .bq-message would silently drop the gap whenever it is the last one. */
|
|
252
|
+
.bq-index-pass + :not(.bq-index-pass) { margin-top: 0.55rem; }
|
|
253
|
+
/* Trailing loader: an open row keeps a "still working" mark of its own for as long
|
|
254
|
+
as the file's indexing is not confirmed over. It sits on the same rail as the
|
|
255
|
+
passes so it reads as the run continuing rather than as a message of its own —
|
|
256
|
+
which is why the rail is repeated here instead of reusing the rule above, whose
|
|
257
|
+
selector requires .bq-message. */
|
|
258
|
+
.bq-index-tail {
|
|
259
|
+
margin-bottom: 0.3rem;
|
|
260
|
+
margin-left: 0.7rem;
|
|
261
|
+
padding: 0.1rem 0 0.1rem 0.7rem;
|
|
262
|
+
border-left: 2px solid var(--bq-line);
|
|
263
|
+
font-size: 0.78rem;
|
|
264
|
+
color: var(--bq-muted);
|
|
265
|
+
}
|
|
266
|
+
/* An open row animates for as long as the file is indexing, which can be minutes.
|
|
267
|
+
Readers who ask for less motion get the static end state. */
|
|
268
|
+
@media (prefers-reduced-motion: reduce) {
|
|
269
|
+
.bq-index-tail .bq-loader::after { animation: none; content: '...'; }
|
|
270
|
+
}
|
|
227
271
|
.bq-index-note {
|
|
228
272
|
padding: 0 0.6rem 0.45rem;
|
|
229
273
|
font-size: 0.7rem;
|
|
@@ -257,6 +301,68 @@
|
|
|
257
301
|
/* While the temporary URL is being re-resolved the click is swallowed — show a
|
|
258
302
|
busy cursor and dim it so it reads as "working, don't click again". */
|
|
259
303
|
.bq-link-button.is-refreshing { cursor: progress; opacity: 0.6; }
|
|
304
|
+
/* The client asked for a url for this file and did not get one, so the chip is
|
|
305
|
+
emitted with NO href (it cannot navigate) and with ✕ in place of ↗. Paint it
|
|
306
|
+
as the dead text it now is: muted, no underline, default cursor, and no hover
|
|
307
|
+
response, since the base rule above still fires on hover and would otherwise
|
|
308
|
+
promising a link. */
|
|
309
|
+
.bq-link-button.is-unavailable,
|
|
310
|
+
.bq-link-button.is-unavailable:hover {
|
|
311
|
+
color: var(--bq-muted);
|
|
312
|
+
text-decoration: none;
|
|
313
|
+
cursor: default;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/* ---- inline image previews ----------------------------------------------*/
|
|
317
|
+
/* The anchor IS the preview: picture on top, the ordinary chip underneath as a
|
|
318
|
+
caption. The caption is the FALLBACK, not decoration: when the url dies or the
|
|
319
|
+
file is gone the <img> hides itself and what is left is exactly the text chip
|
|
320
|
+
this feature replaced, still clickable. */
|
|
321
|
+
.bq-link-button.is-image-preview {
|
|
322
|
+
display: inline-block;
|
|
323
|
+
max-width: 100%;
|
|
324
|
+
white-space: normal;
|
|
325
|
+
overflow: visible;
|
|
326
|
+
text-overflow: clip;
|
|
327
|
+
vertical-align: top;
|
|
328
|
+
margin: 0.25em 0;
|
|
329
|
+
}
|
|
330
|
+
/* Qualified with .bq-md because `.bq-md img` below already sets max-width and
|
|
331
|
+
would otherwise out-specify a bare .bq-img-preview. */
|
|
332
|
+
.bq-md img.bq-img-preview {
|
|
333
|
+
display: block;
|
|
334
|
+
/* Bounded on BOTH axes so a 12000px panorama and a 40x9000 sliver each land
|
|
335
|
+
inside the bubble; width/height auto keep the aspect ratio. The bubble cap
|
|
336
|
+
differs per client, so the preview carries its own. */
|
|
337
|
+
max-width: min(100%, 320px);
|
|
338
|
+
max-height: 320px;
|
|
339
|
+
width: auto;
|
|
340
|
+
height: auto;
|
|
341
|
+
border-radius: 4px;
|
|
342
|
+
border: 1px solid var(--bq-line);
|
|
343
|
+
/* A transparent PNG is invisible on paper without this. */
|
|
344
|
+
background: rgba(127, 127, 127, 0.08);
|
|
345
|
+
}
|
|
346
|
+
/* Keyed on the MISSING src, not on a state class: the element is emitted with no
|
|
347
|
+
src at all (the url has to be minted first) and a src-less <img> would
|
|
348
|
+
otherwise paint the browser's broken-image glyph. */
|
|
349
|
+
.bq-md img.bq-img-preview:not([src]),
|
|
350
|
+
.bq-md img.bq-img-preview[data-bq-img-state="error"] { display: none; }
|
|
351
|
+
/* The dot trail covers the mint round trip, then gets out of the way. */
|
|
352
|
+
.bq-md img.bq-img-preview[src] ~ [data-bq-img-loader],
|
|
353
|
+
.bq-md img.bq-img-preview[data-bq-img-state="error"] ~ [data-bq-img-loader] { display: none; }
|
|
354
|
+
.bq-img-preview-caption {
|
|
355
|
+
display: inline-block;
|
|
356
|
+
max-width: 100%;
|
|
357
|
+
overflow: hidden;
|
|
358
|
+
text-overflow: ellipsis;
|
|
359
|
+
white-space: nowrap;
|
|
360
|
+
vertical-align: bottom;
|
|
361
|
+
}
|
|
362
|
+
/* A collapsed indexing row is a single line and its label is the indexed file's
|
|
363
|
+
own path, which is frequently an image. Never grow that row into a picture. */
|
|
364
|
+
.bq-index-label .bq-md img.bq-img-preview,
|
|
365
|
+
.bq-index-label [data-bq-img-loader] { display: none; }
|
|
260
366
|
|
|
261
367
|
/* ---- rendered-markdown body ----------------------------------------------*/
|
|
262
368
|
/* Overrides the bubble's `white-space: pre-wrap` since marked already produces
|