bunnyquery 1.6.2 → 1.8.0
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 +157 -29
- package/bunnyquery.css +134 -0
- package/bunnyquery.js +1057 -120
- package/dist/engine.cjs +784 -42
- package/dist/engine.cjs.map +1 -1
- package/dist/engine.d.mts +455 -4
- package/dist/engine.d.ts +455 -4
- package/dist/engine.mjs +769 -43
- package/dist/engine.mjs.map +1 -1
- package/package.json +1 -1
- package/src/engine/history.ts +30 -2
- package/src/engine/host.ts +34 -1
- package/src/engine/index.ts +25 -1
- package/src/engine/indexing_groups.ts +389 -0
- package/src/engine/links.ts +343 -9
- package/src/engine/prompts/chat_system_prompt.ts +4 -1
- package/src/engine/prompts/indexing_user_message.ts +4 -0
- package/src/engine/requests.ts +1 -1
- package/src/engine/session.ts +412 -33
- package/src/engine/time.ts +34 -0
- package/src/engine/viewport_fill.ts +186 -0
- package/styles/chat.css +134 -0
package/README.md
CHANGED
|
@@ -23,10 +23,16 @@ you can build your own chat UI on top of it. See
|
|
|
23
23
|
- **Conversation history** — paginated, with "Fetching history…" indicators on
|
|
24
24
|
first load and on scroll-up.
|
|
25
25
|
- **Attachments** — drag-and-drop files and folders, per-file upload status
|
|
26
|
-
(uploading / failed / indexed),
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
(uploading / failed / indexed), overflow collapsing for large batches, and a
|
|
27
|
+
prompt when an upload hits a file that already exists (skip / reindex only /
|
|
28
|
+
overwrite, with "apply to all remaining"). Images are read with vision/OCR,
|
|
29
|
+
large documents and spreadsheets are read window by window, PDFs are rendered
|
|
30
|
+
to page images, and everything else extractable is inlined as text. See
|
|
29
31
|
[Supported file types](#supported-file-types).
|
|
32
|
+
- **Background indexing**: an uploaded file is indexed in the background,
|
|
33
|
+
across as many passes as it takes. A file's passes collapse into a single
|
|
34
|
+
status row in the chat that can be expanded, and stopped: "Stop" cancels every
|
|
35
|
+
queued and running pass at once and ends the continuation chain.
|
|
30
36
|
- **Attachment parser plugins** — register a client-side parser so the widget
|
|
31
37
|
extracts text in the browser from formats the model can't otherwise read, and
|
|
32
38
|
indexes it directly. See [Attachment parser plugins](#attachment-parser-plugins).
|
|
@@ -138,15 +144,30 @@ BunnyQuery.logout();
|
|
|
138
144
|
|
|
139
145
|
> `init()` is idempotent — calling it twice logs a warning and returns the existing
|
|
140
146
|
> instance rather than re-mounting. On a successful mount it logs its version, e.g.
|
|
141
|
-
> `[bunnyquery] v1.
|
|
147
|
+
> `[bunnyquery] v1.7.0`.
|
|
148
|
+
|
|
149
|
+
See [HISTORY.md](HISTORY.md) for the release-by-release changelog.
|
|
142
150
|
|
|
143
151
|
## Supported file types
|
|
144
152
|
|
|
145
153
|
When a user attaches a file, BunnyQuery makes its contents available to the AI
|
|
146
|
-
automatically
|
|
147
|
-
configure.
|
|
154
|
+
automatically, detected by extension (with a MIME-type fallback), nothing to
|
|
155
|
+
configure.
|
|
156
|
+
|
|
157
|
+
An attachment is used in two places, and they take different routes:
|
|
158
|
+
|
|
159
|
+
- **In the chat message.** Extractable files are inlined as text; anything else
|
|
160
|
+
(PDFs, images) is handed over as a temporary link, which the proxy worker
|
|
161
|
+
re-mints just before the upstream call so a queued message can never hand the
|
|
162
|
+
model a stale URL.
|
|
163
|
+
- **In background indexing**, where the file is read in full and saved into the
|
|
164
|
+
project's knowledge. This is the path with the window and page loops below.
|
|
148
165
|
|
|
149
|
-
|
|
166
|
+
The routes are tried in this order: a [parser plugin](#attachment-parser-plugins),
|
|
167
|
+
then PDF page rendering, then windowed or paged reading, then server-side
|
|
168
|
+
extraction, then a plain link.
|
|
169
|
+
|
|
170
|
+
### 1. Images: read directly by the model (vision + OCR)
|
|
150
171
|
|
|
151
172
|
`.jpg` · `.jpeg` · `.png` · `.gif` · `.webp`
|
|
152
173
|
|
|
@@ -155,10 +176,51 @@ picture** and **reads any text in it (OCR)**. Works on both Claude and OpenAI.
|
|
|
155
176
|
Only images referenced in the **most recent** message are inlined (older links
|
|
156
177
|
may have expired).
|
|
157
178
|
|
|
158
|
-
### 2.
|
|
179
|
+
### 2. PDFs: rendered to page images
|
|
180
|
+
|
|
181
|
+
`.pdf`
|
|
182
|
+
|
|
183
|
+
PDF text layers are often absent or unreliable, so a PDF is indexed **visually**:
|
|
184
|
+
the proxy worker renders a window of pages (5 at a time) to images and injects
|
|
185
|
+
them as image blocks in the indexing message. Tool-result images render on
|
|
186
|
+
neither provider, which is why the pages have to be in the message itself. That
|
|
187
|
+
makes scanned PDFs work as well as digital ones.
|
|
188
|
+
|
|
189
|
+
The worker advances the window itself, off its renderer's true page count, and
|
|
190
|
+
enqueues the next pass. Indexing a long document therefore does not depend on
|
|
191
|
+
the browser tab staying open, and does not depend on the model correctly
|
|
192
|
+
declaring itself finished.
|
|
193
|
+
|
|
194
|
+
### 3. Large documents, spreadsheets & data: read window by window
|
|
195
|
+
|
|
196
|
+
```
|
|
197
|
+
.xls .xlsx .xlsm .ods grids (rows plus embedded photos)
|
|
198
|
+
.csv .tsv .tab row-bounded windows with absolute row numbers
|
|
199
|
+
.docx .pptx documents
|
|
200
|
+
.txt .md .markdown .log plain text
|
|
201
|
+
.json .jsonl .ndjson .xml .yaml .yml
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
These are read **one window at a time** and continued until the file is
|
|
205
|
+
exhausted, rather than inlined once. Whole-file extraction is capped at 200,000
|
|
206
|
+
characters, and against real files that cap was discarding most of every large
|
|
207
|
+
upload: a 5MB `.txt` indexed 4.0% of its content, a 4.8MB `.json` 4.2%, a
|
|
208
|
+
1.9M-character Korean `.txt` 10.5%, a `.docx` 70.6%. Nothing surfaced the loss,
|
|
209
|
+
because the agent received a plausible-looking document with no way to know most
|
|
210
|
+
of it was missing.
|
|
211
|
+
|
|
212
|
+
Two drivers exist for this loop. By default the agent pages the file itself with
|
|
213
|
+
the `readFileContent` tool. With the engine's `windowedIndexing` option enabled,
|
|
214
|
+
the **worker** reads a window per request and continues from the reader's own
|
|
215
|
+
cursor, so the traversal no longer has to fit inside the model's turn budget.
|
|
216
|
+
That option is off by default and must only be turned on against a deployed
|
|
217
|
+
worker (see [Importing the chat engine](#importing-the-chat-engine)); the widget
|
|
218
|
+
does not enable it.
|
|
219
|
+
|
|
220
|
+
### 4. Everything else extractable: inlined as text server-side
|
|
159
221
|
|
|
160
222
|
The skapi proxy downloads the file, extracts its text **server-side**, and
|
|
161
|
-
inlines that text into the request
|
|
223
|
+
inlines that text into the request, so the model reads it directly with no
|
|
162
224
|
fetching. This keeps indexing consistent across model providers.
|
|
163
225
|
|
|
164
226
|
**Office & e-book** (binary/zip, parsed):
|
|
@@ -178,21 +240,24 @@ Plus a **MIME fallback**: any file whose content type is text-like (`text/*`,
|
|
|
178
240
|
`application/json`, `application/xml`, `*+json`, `*+xml`, `*+yaml`, …) is decoded
|
|
179
241
|
even when its extension isn't in the list above.
|
|
180
242
|
|
|
181
|
-
Encoding is auto-detected
|
|
182
|
-
Extracted text is capped at **200,000 characters**; longer files are
|
|
183
|
-
with a `...[truncated for length; original N characters]` marker.
|
|
243
|
+
Encoding is auto-detected: UTF-8 (BOM-aware), then CP949/EUC-KR (Korean), then
|
|
244
|
+
Latin-1. Extracted text is capped at **200,000 characters**; longer files are
|
|
245
|
+
truncated with a `...[truncated for length; original N characters]` marker. The
|
|
246
|
+
formats listed in section 3 are windowed precisely so they never hit that cap.
|
|
247
|
+
|
|
248
|
+
Note the overlap between sections 3 and 4 is deliberate: a `.docx` or a `.csv`
|
|
249
|
+
is windowed when it is indexed, and extracted whole when it rides along in a
|
|
250
|
+
chat message.
|
|
184
251
|
|
|
185
|
-
###
|
|
252
|
+
### 5. Anything else: a plain link
|
|
186
253
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
enabled). Both can open and read PDFs, so PDFs work on either provider.
|
|
254
|
+
A file that is none of the above is handed to the model as a temporary link,
|
|
255
|
+
which it opens with its built-in web tool: **Claude** via `web_fetch`, **OpenAI**
|
|
256
|
+
via `web_search` (external web access is enabled).
|
|
191
257
|
|
|
192
|
-
> A provider's web tool opens document/page-style URLs
|
|
193
|
-
>
|
|
194
|
-
>
|
|
195
|
-
> being left to the model to fetch.
|
|
258
|
+
> A provider's web tool opens document/page-style URLs, but not necessarily a
|
|
259
|
+
> bare *data-file* download (e.g. a raw `.csv`/`.tsv` link). That is why those
|
|
260
|
+
> data formats are extracted server-side instead of being left to the model.
|
|
196
261
|
|
|
197
262
|
### Caveats
|
|
198
263
|
|
|
@@ -205,16 +270,32 @@ enabled). Both can open and read PDFs, so PDFs work on either provider.
|
|
|
205
270
|
[Attachment parser plugin](#attachment-parser-plugins) — it runs in the browser
|
|
206
271
|
and feeds parsed text straight into indexing.
|
|
207
272
|
|
|
273
|
+
### Re-indexing an existing file
|
|
274
|
+
|
|
275
|
+
Uploading over a file that already exists prompts for skip, "reindex only", or
|
|
276
|
+
overwrite. Choosing either of the latter two deletes the file's existing
|
|
277
|
+
`src::<path>` index record first, and the skapi backend cascades that delete to
|
|
278
|
+
the record's reference-linked children, so re-indexing **replaces** the file's
|
|
279
|
+
knowledge rather than duplicating it.
|
|
280
|
+
|
|
281
|
+
### Filenames
|
|
282
|
+
|
|
283
|
+
Storage keys preserve Unicode letters, digits and spaces, NFC-normalized, so
|
|
284
|
+
Korean, Japanese and accented Latin filenames survive upload intact. Only
|
|
285
|
+
genuinely unsafe characters are replaced. The original name is always kept for
|
|
286
|
+
display.
|
|
287
|
+
|
|
208
288
|
## Attachment parser plugins
|
|
209
289
|
|
|
210
|
-
By default the chat agent reads images with vision/OCR,
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
290
|
+
By default the chat agent reads images with vision/OCR, renders PDF pages to
|
|
291
|
+
images, reads large documents and spreadsheets window by window, and extracts
|
|
292
|
+
Office/OpenDocument/EPUB and text/data/code files on the server. See
|
|
293
|
+
[Supported file types](#supported-file-types). For any format read by **none**
|
|
294
|
+
of these (e.g. a proprietary binary format), register a **parser plugin**: it
|
|
295
|
+
runs in the browser, turns the uploaded file into text (or an HTML string), and
|
|
296
|
+
the widget sends that content **inline** for indexing. A parser plugin takes
|
|
297
|
+
precedence over every other route, so nothing is fetched or extracted for that
|
|
298
|
+
file.
|
|
218
299
|
|
|
219
300
|
BunnyQuery ships only the **mechanism**. You bring the parsing library (so the
|
|
220
301
|
widget stays lean and you choose which formats and which library).
|
|
@@ -346,6 +427,37 @@ helpers — see the `.d.ts` shipped with `bunnyquery/engine`.
|
|
|
346
427
|
| `clientSecretRequestHistory` | `function` | `skapi.clientSecretRequestHistory`, bound to your Skapi instance. **Required.** |
|
|
347
428
|
| `mcpBaseUrl` | `string` | MCP server base URL (you resolve prod vs dev). **Required.** |
|
|
348
429
|
| `poll` | `number?` | Value attached as `poll` on every request. Omit it if your `clientSecretRequest` already resolves with the final body; pass `0` for the deployed `skapi-js@latest` (needed for the early ack + a manual `.poll()` handle that powers queued-send cancel — the widget's case). |
|
|
430
|
+
| `attachmentParsers` | `array?` | Client-side attachment parsers, registered at configure time. More can be added later with `registerAttachmentParser()`. See [Attachment parser plugins](#attachment-parser-plugins). |
|
|
431
|
+
| `windowedIndexing` | `boolean?` | Opt in to **server-driven** windowed indexing for text and grid files (see [file types](#supported-file-types)). Off by default and must stay off until the worker that strips the `_skapi_window` directive is deployed: against an older worker the directive reaches the provider as an unknown body field and the call fails terminally with no retry. |
|
|
432
|
+
|
|
433
|
+
### Display and paging helpers
|
|
434
|
+
|
|
435
|
+
Two shared transforms exist so that a second chat UI behaves identically to the
|
|
436
|
+
widget rather than approximately:
|
|
437
|
+
|
|
438
|
+
- **`buildChatDisplayList`** collapses a file's many background-indexing turns
|
|
439
|
+
into one status row per indexing run, wherever those turns sit in the
|
|
440
|
+
conversation, rendered at that run's newest turn. It is pure and
|
|
441
|
+
view-agnostic; you render the resulting `DisplayEntry` list. Pair it with
|
|
442
|
+
`ChatSession.cancelIndexingGroup(group)` to give the row a working Stop
|
|
443
|
+
button, which cancels every queued and running pass of that file at once and
|
|
444
|
+
ends the continuation chain.
|
|
445
|
+
- **`fillHistoryViewport` / `createHistoryFiller`** keep older history
|
|
446
|
+
reachable. Paging is triggered only by scrolling to the top of the message
|
|
447
|
+
box, so a box too short to scroll has no trigger at all, which is the normal
|
|
448
|
+
state once a page of history collapses into a single indexing row. Implement
|
|
449
|
+
the optional `ChatHost.onHistoryLoaded` hook, measure your own box, and let
|
|
450
|
+
the loop page until the reader genuinely gained reachable content.
|
|
451
|
+
|
|
452
|
+
Other optional `ChatHost` hooks worth implementing: `deleteExistingFileRecord`
|
|
453
|
+
(so a reindex replaces the file's knowledge instead of duplicating it) and
|
|
454
|
+
`promptOverwrite` (the skip / reindex / overwrite prompt).
|
|
455
|
+
|
|
456
|
+
`ChatSession.pausePolling(reason)` and `resumePolling(reason)` stop background
|
|
457
|
+
indexing polls when nobody is looking (a hidden tab, a detached view). Replies
|
|
458
|
+
the user is waiting on keep polling deliberately, so their results still land in
|
|
459
|
+
the cache. Server-side work is untouched either way, so pausing drops traffic,
|
|
460
|
+
never progress.
|
|
349
461
|
|
|
350
462
|
## OAuth & redirects
|
|
351
463
|
|
|
@@ -356,11 +468,27 @@ host page** — BunnyQuery reads the `?code=…&state=…` parameters, completes
|
|
|
356
468
|
exchange, and cleans them from the URL automatically. No dedicated callback page is
|
|
357
469
|
needed; just make sure the page that hosts the widget is a stable, reachable URL.
|
|
358
470
|
|
|
471
|
+
Once granted, the connection is kept alive **silently**. When the stored grant
|
|
472
|
+
ages out, BunnyQuery refreshes it through the OAuth `refresh_token` flow with no
|
|
473
|
+
redirect, so an embedded widget never yanks the host page away mid-chat. It also
|
|
474
|
+
refreshes on tab focus, because returning to a backgrounded tab after the grant
|
|
475
|
+
expired would otherwise disconnect the next message. The full redirect is only a
|
|
476
|
+
boot-time fallback for when the silent path cannot refresh.
|
|
477
|
+
|
|
359
478
|
## Notes
|
|
360
479
|
|
|
361
480
|
- The widget fills its mount element. Give that element a real height (e.g.
|
|
362
481
|
`height: 100dvh`) or it will collapse.
|
|
363
482
|
- File and folder uploads are stored in your Skapi project's database storage and
|
|
364
483
|
served from a temporary db-CDN URL (`hostDomain`); links in chat refresh on expiry.
|
|
484
|
+
Links a queued message carries are re-minted server-side immediately before the
|
|
485
|
+
upstream call, so a message that waits in the queue never hands the model a dead
|
|
486
|
+
URL.
|
|
487
|
+
- The number of files attachable to a single message is capped, and beyond a
|
|
488
|
+
point the chips collapse into a "...(n) more" pill rather than being rendered.
|
|
489
|
+
Very large batches belong on a dedicated upload page, not the chat composer.
|
|
490
|
+
- When your service database is frozen, the attach button and drag-and-drop are
|
|
491
|
+
hidden for non-admin users, mirroring the backend's own upload gate, so there
|
|
492
|
+
is no upload path that fails only at the end.
|
|
365
493
|
- The agent shown in the header (`BunnyQuery · <project name>`) reflects the project
|
|
366
494
|
configured for your Skapi service.
|
package/bunnyquery.css
CHANGED
|
@@ -1072,6 +1072,18 @@
|
|
|
1072
1072
|
font-style: italic;
|
|
1073
1073
|
clear: both;
|
|
1074
1074
|
}
|
|
1075
|
+
/* Timestamp under a settled bubble (created for a user turn, response time for an
|
|
1076
|
+
assistant turn). Muted and small so it never competes with the message. */
|
|
1077
|
+
.bq-msg-time {
|
|
1078
|
+
display: block;
|
|
1079
|
+
margin-top: 0.25rem;
|
|
1080
|
+
font-size: 0.62rem;
|
|
1081
|
+
line-height: 1;
|
|
1082
|
+
color: var(--bq-muted);
|
|
1083
|
+
opacity: 0.7;
|
|
1084
|
+
clear: both;
|
|
1085
|
+
}
|
|
1086
|
+
.bq-message.is-user .bq-msg-time { text-align: right; }
|
|
1075
1087
|
.bq-cancel-queue-btn {
|
|
1076
1088
|
float: right;
|
|
1077
1089
|
margin: -0.25rem -0.25rem 0.2rem 0.4rem;
|
|
@@ -1095,6 +1107,128 @@
|
|
|
1095
1107
|
.bq-cancel-queue-btn:hover:not(.is-disabled) { background: var(--bq-warning-bg); color: var(--bq-warning); }
|
|
1096
1108
|
.bq-cancel-queue-btn.is-disabled { opacity: 0.3; cursor: not-allowed; pointer-events: none; }
|
|
1097
1109
|
|
|
1110
|
+
/* ---- collapsed background-indexing group ---------------------------------*/
|
|
1111
|
+
/* One file's many indexing passes (first pass + every CONTINUE pass, each with
|
|
1112
|
+
a request AND a response bubble) render as a single status row instead of
|
|
1113
|
+
filling the conversation with the same turn over and over. The row is a full
|
|
1114
|
+
width strip rather than a bubble: it is chat CHROME, not something anyone
|
|
1115
|
+
said. See engine/indexing_groups.ts for the grouping itself. */
|
|
1116
|
+
.bq-index-group {
|
|
1117
|
+
display: block;
|
|
1118
|
+
margin-bottom: 0.5rem;
|
|
1119
|
+
border: 1px solid var(--bq-line);
|
|
1120
|
+
background: rgba(127, 127, 127, 0.05);
|
|
1121
|
+
}
|
|
1122
|
+
.bq-index-group.is-active { border-color: var(--bq-warning-border); background: var(--bq-warning-bg); }
|
|
1123
|
+
.bq-index-group.is-error { border-color: var(--bq-danger); background: var(--bq-danger-bg); }
|
|
1124
|
+
|
|
1125
|
+
.bq-index-head {
|
|
1126
|
+
display: flex;
|
|
1127
|
+
align-items: center;
|
|
1128
|
+
gap: 0.5rem;
|
|
1129
|
+
width: 100%;
|
|
1130
|
+
padding: 0.45rem 0.6rem;
|
|
1131
|
+
border: 0;
|
|
1132
|
+
border-radius: 0;
|
|
1133
|
+
box-shadow: none;
|
|
1134
|
+
background: transparent;
|
|
1135
|
+
color: var(--bq-muted);
|
|
1136
|
+
font: inherit;
|
|
1137
|
+
font-size: 0.78rem;
|
|
1138
|
+
line-height: 1.35;
|
|
1139
|
+
text-align: left;
|
|
1140
|
+
cursor: pointer;
|
|
1141
|
+
min-height: 0;
|
|
1142
|
+
}
|
|
1143
|
+
.bq-index-head:hover { background: rgba(127, 127, 127, 0.08); color: var(--bq-muted); }
|
|
1144
|
+
.bq-index-group.is-active .bq-index-head { color: var(--bq-warning); }
|
|
1145
|
+
.bq-index-group.is-error .bq-index-head { color: var(--bq-danger); }
|
|
1146
|
+
|
|
1147
|
+
/* Status glyph. The active one is a circular-arrow SVG the consumer inlines
|
|
1148
|
+
(never an icon font: agent.vue's Material Symbols build gates glyphs behind
|
|
1149
|
+
an icon_names allowlist, which a shared stylesheet cannot reach). */
|
|
1150
|
+
.bq-index-icon {
|
|
1151
|
+
flex: 0 0 auto;
|
|
1152
|
+
width: 0.95rem;
|
|
1153
|
+
height: 0.95rem;
|
|
1154
|
+
display: inline-flex;
|
|
1155
|
+
align-items: center;
|
|
1156
|
+
justify-content: center;
|
|
1157
|
+
}
|
|
1158
|
+
.bq-index-icon svg { width: 100%; height: 100%; display: block; }
|
|
1159
|
+
.bq-index-group.is-active .bq-index-icon svg { animation: bq-index-spin 1.1s linear infinite; }
|
|
1160
|
+
@keyframes bq-index-spin { to { transform: rotate(360deg); } }
|
|
1161
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1162
|
+
.bq-index-group.is-active .bq-index-icon svg { animation-duration: 3s; }
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
.bq-index-label {
|
|
1166
|
+
flex: 1 1 auto;
|
|
1167
|
+
min-width: 0;
|
|
1168
|
+
overflow: hidden;
|
|
1169
|
+
text-overflow: ellipsis;
|
|
1170
|
+
white-space: nowrap;
|
|
1171
|
+
}
|
|
1172
|
+
.bq-index-label .bq-link-button { max-width: 100%; font-size: inherit; }
|
|
1173
|
+
/* The label runs through the same markdown renderer as a bubble, which wraps it
|
|
1174
|
+
in a block <p>. Inline it so the row stays one line high. */
|
|
1175
|
+
.bq-index-label .bq-md,
|
|
1176
|
+
.bq-index-label .bq-md p { display: inline; margin: 0; }
|
|
1177
|
+
/* Passes LOADED, never a server-side total: history pages newest-first, so any
|
|
1178
|
+
total derived from what is on screen is a lower bound. */
|
|
1179
|
+
.bq-index-count { flex: 0 0 auto; opacity: 0.75; font-size: 0.72rem; }
|
|
1180
|
+
/* Stop indexing. Only ever rendered while a file has a queued/running pass, so
|
|
1181
|
+
it never competes with the row's normal (collapsed, finished) look. Sized off
|
|
1182
|
+
the row's own font so it stays a thin strip, not a chat action button. */
|
|
1183
|
+
.bq-index-cancel {
|
|
1184
|
+
flex: 0 0 auto;
|
|
1185
|
+
padding: 0.1rem 0.4rem;
|
|
1186
|
+
min-height: 0;
|
|
1187
|
+
border: 1px solid currentColor;
|
|
1188
|
+
border-radius: 0;
|
|
1189
|
+
background: transparent;
|
|
1190
|
+
color: inherit;
|
|
1191
|
+
font: inherit;
|
|
1192
|
+
font-size: 0.68rem;
|
|
1193
|
+
line-height: 1.3;
|
|
1194
|
+
letter-spacing: 0.02em;
|
|
1195
|
+
cursor: pointer;
|
|
1196
|
+
box-shadow: none;
|
|
1197
|
+
opacity: 0.8;
|
|
1198
|
+
}
|
|
1199
|
+
.bq-index-cancel:hover:not(.is-disabled) { background: var(--bq-warning-bg); color: var(--bq-warning); opacity: 1; }
|
|
1200
|
+
.bq-index-cancel.is-disabled { opacity: 0.45; cursor: default; pointer-events: none; }
|
|
1201
|
+
.bq-index-chevron {
|
|
1202
|
+
flex: 0 0 auto;
|
|
1203
|
+
font-size: 0.62rem;
|
|
1204
|
+
opacity: 0.7;
|
|
1205
|
+
transition: transform 0.15s ease;
|
|
1206
|
+
}
|
|
1207
|
+
.bq-index-group.is-open .bq-index-chevron { transform: rotate(90deg); }
|
|
1208
|
+
|
|
1209
|
+
/* Expanded: the file's own turns, in order, rendered as ordinary messages
|
|
1210
|
+
directly under the row they collapsed into (they are scattered through the
|
|
1211
|
+
conversation otherwise). Rendered as SIBLINGS of the row, not children, so
|
|
1212
|
+
both consumers keep exactly one copy of the message-bubble markup. */
|
|
1213
|
+
.bq-index-group.is-open { margin-bottom: 0.35rem; }
|
|
1214
|
+
.bq-message.bq-index-pass {
|
|
1215
|
+
margin-bottom: 0.3rem;
|
|
1216
|
+
margin-left: 0.7rem;
|
|
1217
|
+
padding-left: 0.7rem;
|
|
1218
|
+
border-left: 2px solid var(--bq-line);
|
|
1219
|
+
}
|
|
1220
|
+
.bq-message.bq-index-pass .bq-bubble { font-size: 0.78rem; }
|
|
1221
|
+
/* Close the run: the next ordinary message needs its normal breathing room. */
|
|
1222
|
+
.bq-message.bq-index-pass + :not(.bq-index-pass) { margin-top: 0.55rem; }
|
|
1223
|
+
.bq-index-note {
|
|
1224
|
+
padding: 0 0.6rem 0.45rem;
|
|
1225
|
+
font-size: 0.7rem;
|
|
1226
|
+
font-style: italic;
|
|
1227
|
+
color: var(--bq-muted);
|
|
1228
|
+
}
|
|
1229
|
+
/* A cancel that the server refused (the pass had already finished). */
|
|
1230
|
+
.bq-index-note.is-error { color: var(--bq-danger); }
|
|
1231
|
+
|
|
1098
1232
|
/* ---- in-bubble file / link anchors --------------------------------------*/
|
|
1099
1233
|
.bq-file-download,
|
|
1100
1234
|
.bq-link-button {
|