@bitbaum/chatkit 0.18.1 → 0.20.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 +200 -0
- package/bin/chatkit-copy-pdf-assets.mjs +38 -0
- package/dist/pdf.d.ts +96 -0
- package/dist/pdf.d.ts.map +1 -0
- package/dist/pdf.js +212 -0
- package/dist/pdf.js.map +1 -0
- package/dist/react/AttachMenu.d.ts +18 -1
- package/dist/react/AttachMenu.d.ts.map +1 -1
- package/dist/react/AttachMenu.js +8 -4
- package/dist/react/AttachMenu.js.map +1 -1
- package/dist/react/ChatApp.d.ts +79 -5
- package/dist/react/ChatApp.d.ts.map +1 -1
- package/dist/react/ChatApp.js +113 -7
- package/dist/react/ChatApp.js.map +1 -1
- package/dist/react/ChatHistory.d.ts +7 -1
- package/dist/react/ChatHistory.d.ts.map +1 -1
- package/dist/react/ChatHistory.js +2 -2
- package/dist/react/ChatHistory.js.map +1 -1
- package/dist/react/Composer.d.ts +20 -3
- package/dist/react/Composer.d.ts.map +1 -1
- package/dist/react/Composer.js +19 -4
- package/dist/react/Composer.js.map +1 -1
- package/dist/react/Thread.d.ts +48 -2
- package/dist/react/Thread.d.ts.map +1 -1
- package/dist/react/Thread.js +13 -6
- package/dist/react/Thread.js.map +1 -1
- package/dist/react/attach.d.ts +1 -1
- package/dist/react/attach.d.ts.map +1 -1
- package/dist/react/index.d.ts +10 -10
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +4 -4
- package/dist/react/index.js.map +1 -1
- package/dist/react/use-attachments.d.ts +17 -0
- package/dist/react/use-attachments.d.ts.map +1 -1
- package/dist/react/use-attachments.js +89 -72
- package/dist/react/use-attachments.js.map +1 -1
- package/dist/react/use-chat-app.d.ts +53 -3
- package/dist/react/use-chat-app.d.ts.map +1 -1
- package/dist/react/use-chat-app.js +126 -15
- package/dist/react/use-chat-app.js.map +1 -1
- package/package.json +13 -4
- package/styles.css +64 -1
package/README.md
CHANGED
|
@@ -174,11 +174,172 @@ reopens the chat, a picture staged above the text and removed, delete, the
|
|
|
174
174
|
queue and Stop — and checks for sideways scroll, 44px targets and cut words
|
|
175
175
|
at every step.
|
|
176
176
|
|
|
177
|
+
#### What a product adds to it (0.19)
|
|
178
|
+
|
|
179
|
+
The first four products to adopt ChatApp (Heidi, OrangeCat, Solon,
|
|
180
|
+
Substrata) each listed what it could not do. These close those gaps; every
|
|
181
|
+
one is optional, and 0.18 code keeps working unchanged.
|
|
182
|
+
|
|
183
|
+
**Send from the page — the handle.** `<ChatApp ref={chat}>` gives
|
|
184
|
+
`ChatAppHandle`: `send(text, attachments?, { newChat?, chips? })`,
|
|
185
|
+
`fill(text, files?)`, `focus()`, `newChat()`, `openConversation(id)`.
|
|
186
|
+
`send` behaves exactly like the composer's Send (queued behind a running
|
|
187
|
+
answer, a chat created when none is open); `File`s are read like the
|
|
188
|
+
paperclip's, and if one cannot be attached nothing is sent — the words and
|
|
189
|
+
files go into the box with the reason. Never click `.ck-send` from outside.
|
|
190
|
+
|
|
191
|
+
```tsx
|
|
192
|
+
const chat = useRef<ChatAppHandle>(null);
|
|
193
|
+
<ChatApp ref={chat} transport={transport} />;
|
|
194
|
+
<button onClick={() => chat.current?.send("Check this figure", [], { newChat: true })}>Check this</button>;
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
**A message the page arrives with.** `initialMessage` is sent once, in a new
|
|
198
|
+
chat, after the list has loaded (StrictMode's double mount sends it once).
|
|
199
|
+
`send: false` only fills the box. Clear it from your URL or state yourself.
|
|
200
|
+
|
|
201
|
+
```tsx
|
|
202
|
+
<ChatApp transport={transport}
|
|
203
|
+
initialMessage={handoff ? { text: handoff.text, attachments: handoff.files } : null} />
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
**`?q=` links.** `deepLinkParam="q"` reads the parameter into a new chat's
|
|
207
|
+
box and takes it off the address. A link never sends for someone: only with
|
|
208
|
+
`deepLinkAutoSend` too.
|
|
209
|
+
|
|
210
|
+
```tsx
|
|
211
|
+
<ChatApp transport={transport} deepLinkParam="q" />
|
|
212
|
+
// /chat?q=How%20do%20invoices%20expire → the box says it; the person sends.
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
**Why a turn failed, per turn.** Throw `ChatTurnError(message, { kind })`
|
|
216
|
+
from the transport: that bubble shows `message` and keeps `failure.kind`
|
|
217
|
+
(for a footer to link the fix). Two failures in one chat keep their own
|
|
218
|
+
words. Any other error shows the `failed` label, never its raw message —
|
|
219
|
+
unless `failureMessage(error)` maps it.
|
|
220
|
+
|
|
221
|
+
```tsx
|
|
222
|
+
import { ChatTurnError } from "@bitbaum/chatkit/react";
|
|
223
|
+
if (res.status === 503) throw new ChatTurnError("The model is not set up here.", { kind: "not-configured" });
|
|
224
|
+
<ChatApp transport={transport} failureMessage={(e) => (navigator.onLine ? null : "You are offline.")} />;
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
**Retry sends the pictures again.** The files of a failed turn stay with it
|
|
228
|
+
until its answer lands; Retry re-sends the words and the files. Nothing to
|
|
229
|
+
configure.
|
|
230
|
+
|
|
231
|
+
**Stop and the half answer.** Stop keeps what had arrived (as the apps do).
|
|
232
|
+
`stopKeepsPartial={false}` drops it for every turn; a transport decides per
|
|
233
|
+
turn with `turn.stopKeepsPartial(keep)` — `false` while the text is unchecked.
|
|
234
|
+
|
|
235
|
+
```tsx
|
|
236
|
+
transport={async (turn) => {
|
|
237
|
+
turn.stopKeepsPartial(false); // the gate has not checked this text yet
|
|
238
|
+
/* … stream … */ turn.stopKeepsPartial(true); return answer; }}
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
**Starters with a language.** A starter is `string | { label, prompt?, lang? }
|
|
242
|
+
| { text, label?, lang? }`; `lang` goes on the button.
|
|
243
|
+
|
|
244
|
+
```tsx
|
|
245
|
+
<ChatApp transport={transport}
|
|
246
|
+
starters={["Explain this word", { text: "Grüezi mitenand", lang: "gsw" }]} />
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
**The chat list's own line.** `historySlot` sits under the search box, above
|
|
250
|
+
the chats; `historyEmpty` replaces "No chats yet…".
|
|
251
|
+
|
|
252
|
+
```tsx
|
|
253
|
+
<ChatApp transport={transport}
|
|
254
|
+
historySlot={signedOut && <a href="/login">Only in this browser · Sign in to keep it</a>}
|
|
255
|
+
historyEmpty={<p>What you ask is kept here.</p>} />
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
**Product rows in the attach sheet.** `composer.attachExtras` — `{ id,
|
|
259
|
+
label, detail?, icon?, onPick }[]` — are rows under Camera · Photos · Files,
|
|
260
|
+
one anatomy (icon, label, second line), in the same sheet. With `attach:
|
|
261
|
+
false` the paperclip opens a sheet of only these rows. (`AttachMenu rows`
|
|
262
|
+
takes the same list.)
|
|
263
|
+
|
|
264
|
+
```tsx
|
|
265
|
+
<ChatApp transport={transport}
|
|
266
|
+
composer={{ attachExtras: [{ id: "things", label: "Your things", onPick: openThings }],
|
|
267
|
+
heldAttachments: held.length }} />
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
**The product's things on the sent message.** `chipsFor({ text, attachments
|
|
271
|
+
})` returns `MessageChip[]` (`{ label, href?, kind? }`) shown on the person's
|
|
272
|
+
message the moment it is sent, and kept with it (`ChatMessageData.chips`).
|
|
273
|
+
`send(…, { chips })` does the same from the handle.
|
|
274
|
+
|
|
275
|
+
```tsx
|
|
276
|
+
<ChatApp transport={transport}
|
|
277
|
+
chipsFor={() => held.map((r) => ({ label: r.title, href: r.url }))} />
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
**A temporary chat ended.** `onTemporaryEnd(chatId)` fires once when a
|
|
281
|
+
temporary chat is left (New chat, another chat, Temporary off), the page is
|
|
282
|
+
hidden, or ChatApp unmounts. `turn.chatId` is the same id, so a server row
|
|
283
|
+
kept for the chat can be keyed by it and deleted here.
|
|
284
|
+
|
|
285
|
+
```tsx
|
|
286
|
+
<ChatApp transport={(turn) => myApi.send({ ...turn, hiddenRowKey: turn.chatId })}
|
|
287
|
+
onTemporaryEnd={(chatId) => myApi.deleteHiddenRow(chatId)} />
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
**Footers that know the latest answer, and can send.** `thread.renderFooter(m,
|
|
291
|
+
{ isLatest, index, send, fill })`: put "what next" only under the latest,
|
|
292
|
+
and let a chip whose label differs from its sentence send in one tap.
|
|
293
|
+
|
|
294
|
+
```tsx
|
|
295
|
+
thread={{ renderFooter: (m, { isLatest, send }) =>
|
|
296
|
+
isLatest && <button className="ck-reply" onClick={() => send("Make it shorter.")}>Shorter</button> }}
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
**After the last turn.** `thread.after` is drawn inside the thread, after the
|
|
300
|
+
last turn (a pending action, a picker), not over the composer.
|
|
301
|
+
|
|
302
|
+
`demo/app.html?lab=1` is the page these are checked on: StrictMode around
|
|
303
|
+
ChatApp, the handle on `window.ckChat`. `test/browser-app.mjs` drives it at
|
|
304
|
+
390px and 1440px.
|
|
305
|
+
|
|
177
306
|
#### Moving a product onto ChatApp
|
|
178
307
|
|
|
179
308
|
Per product, from the matrix: what goes, what stays. What stays is always
|
|
180
309
|
the same two things — the transport and the words.
|
|
181
310
|
|
|
311
|
+
**Workarounds 0.19 makes deletable** (each was written in an adoption PR
|
|
312
|
+
because ChatApp 0.18 could not do it — bitbaum/heidi#226, bitbaum/orangecat#1447,
|
|
313
|
+
bitbaum/solon#309, bitbaum/substrata#262):
|
|
314
|
+
|
|
315
|
+
- **Substrata** — `lib/chat/drive-composer.ts` (types into the composer and
|
|
316
|
+
presses Send) goes: `components/portal/chat/useAutoAsk.ts` and
|
|
317
|
+
`lib/chat/handoff.ts` hand their words and pictures to `initialMessage` (or
|
|
318
|
+
`ref.send(text, files, { newChat: true })`), and `/chat?q=` becomes
|
|
319
|
+
`deepLinkParam="q"`. Retry now re-sends the pictures.
|
|
320
|
+
- **OrangeCat** — in `src/components/ai-chat/ModernChatPanel/index.tsx`, the
|
|
321
|
+
bridge that sets the composer value and clicks `button.ck-send` goes (`?q=`
|
|
322
|
+
and `askCat` → the handle), and so does the ephemeral-row cleanup hung on
|
|
323
|
+
the empty state mounting and `onConversationChange` (→ `onTemporaryEnd`,
|
|
324
|
+
keyed by `turn.chatId` in `hooks/useCatTransport.ts`). "Your things" moves
|
|
325
|
+
from a second button into the sheet (`composer.attachExtras`), its staged
|
|
326
|
+
refs show on the sent bubble (`chipsFor`), `CatAnswerFooter` shows the
|
|
327
|
+
create links and upgrade nudge only under `isLatest`, and pending actions
|
|
328
|
+
move from `composer.above` to `thread.after`. `TurnFailure` reads the
|
|
329
|
+
turn's own `failure.kind` (throw `ChatTurnError`).
|
|
330
|
+
- **Heidi** — in `app/[locale]/_components/chat/workspace.tsx`, the
|
|
331
|
+
latest-failure state passed as `thread.labels.failed` goes (throw
|
|
332
|
+
`ChatTurnError` from `chat-app.ts`'s transport mapping), and so does
|
|
333
|
+
`turn.update({ text: "" })` before a throw on Stop (→
|
|
334
|
+
`turn.stopKeepsPartial(false)` until the variety gate has checked the
|
|
335
|
+
text). The move and learn chips and the word pick send in one tap again
|
|
336
|
+
through `renderFooter`'s `send`; the dialect starter gets `lang`; the
|
|
337
|
+
signed-out "Sign in to keep it" line moves from `startersTitle` to
|
|
338
|
+
`historySlot`.
|
|
339
|
+
- **Solon** — nothing to delete. The per-answer "Answered by <model>" fits
|
|
340
|
+
`thread.renderFooter` (it existed in 0.18; it now also gets `isLatest`).
|
|
341
|
+
ChatApp's own words still come only in English: pass `labels`.
|
|
342
|
+
|
|
182
343
|
- **Loki** — goes: `ThreadRail.tsx`, the history half of
|
|
183
344
|
`WorkspaceDrawers.tsx` and `WorkspaceChrome.tsx`, `ThreadTitle.tsx`,
|
|
184
345
|
`hooks/use-temporary-chat.ts`, `hooks/use-new-chat.ts`,
|
|
@@ -483,6 +644,45 @@ import { AttachMenu, shrinkImage } from "@bitbaum/chatkit/attach";
|
|
|
483
644
|
maxBytes)` is the in-browser downscale the composer uses. `acceptFamilies` and
|
|
484
645
|
`ATTACH_SOURCE_INPUT` are the rule itself.
|
|
485
646
|
|
|
647
|
+
### PDFs — `@bitbaum/chatkit/pdf`
|
|
648
|
+
|
|
649
|
+
No model on a free chain reads a PDF file. So a PDF someone attaches is read
|
|
650
|
+
in the browser and handed on the way the model already understands:
|
|
651
|
+
|
|
652
|
+
```ts
|
|
653
|
+
import { isPdfFile, readPdf, sheetName, PDF_MAX_BYTES } from "@bitbaum/chatkit/pdf";
|
|
654
|
+
|
|
655
|
+
if (isPdfFile(file) && file.size <= PDF_MAX_BYTES) {
|
|
656
|
+
const r = await readPdf(file, { maxSheets: picturesLeft, assetsUrl: "/pdfjs/" });
|
|
657
|
+
if (r.kind === "text") attachText(file.name, r.text); // "--- page 3 ---" markers kept
|
|
658
|
+
else
|
|
659
|
+
r.sheets.forEach((s) => attachImage(sheetName(file.name, s.firstPage, s.lastPage), s.dataUrl));
|
|
660
|
+
// r.pagesShown < r.pageCount → tell the person which pages went
|
|
661
|
+
}
|
|
662
|
+
```
|
|
663
|
+
|
|
664
|
+
- **A PDF with a text layer** (Word, Docs, a typed report) comes back as text.
|
|
665
|
+
- **A PDF without one** (slides exported as pictures, a scan) comes back as
|
|
666
|
+
pictures of its pages, **four to a sheet** at 784px a page — slide text stays
|
|
667
|
+
legible, and a 20-page deck is 5 pictures. Send them to a model that sees.
|
|
668
|
+
- pdf.js loads only on the first PDF (its own entry point and a dynamic
|
|
669
|
+
import), and the legacy build is used so older iPhones read PDFs too.
|
|
670
|
+
|
|
671
|
+
**One step per app**: pdf.js fetches its worker and image decoders by URL at
|
|
672
|
+
runtime, which no bundler follows. Copy them as the first step of `dev` and
|
|
673
|
+
`build` (pnpm does not run `prebuild` hooks by default):
|
|
674
|
+
|
|
675
|
+
```json
|
|
676
|
+
"build": "chatkit-copy-pdf-assets public/pdfjs && next build"
|
|
677
|
+
```
|
|
678
|
+
|
|
679
|
+
and pass `assetsUrl: "/pdfjs/"`. Keep that folder out of git. Served
|
|
680
|
+
from the app's own origin they pass a strict CSP, and they include the
|
|
681
|
+
decoders without which pages whose pictures are JPEG 2000 — macOS's "reduce
|
|
682
|
+
file size" export — draw blank. `test/browser-pdf.mjs` checks both
|
|
683
|
+
kinds in Chromium; `node test/browser-pdf.mjs your.pdf` prints what any PDF
|
|
684
|
+
turns into.
|
|
685
|
+
|
|
486
686
|
### The check every app runs in CI
|
|
487
687
|
|
|
488
688
|
```sh
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Copies pdf.js's runtime files where an app serves static files, so
|
|
3
|
+
// `readPdf(file, { assetsUrl })` from @bitbaum/chatkit/pdf can fetch them.
|
|
4
|
+
//
|
|
5
|
+
// chatkit-copy-pdf-assets public/pdfjs → readPdf(file, { assetsUrl: "/pdfjs/" })
|
|
6
|
+
//
|
|
7
|
+
// pdf.js loads its worker and its image decoders (JPEG 2000, JBIG2) by URL at
|
|
8
|
+
// runtime, which no bundler follows. Without them a page whose pictures are
|
|
9
|
+
// JPEG 2000 — macOS's "reduce file size" PDF export — draws blank (found
|
|
10
|
+
// 2026-10-27 on an investor deck). Run it as the first step of the app's dev
|
|
11
|
+
// and build scripts (pnpm skips `prebuild` hooks by default) so the files
|
|
12
|
+
// always match the installed pdf.js; the folder stays out of git. Idempotent.
|
|
13
|
+
import { cpSync, existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
14
|
+
import { createRequire } from "node:module";
|
|
15
|
+
import { dirname, join, resolve } from "node:path";
|
|
16
|
+
import { PDF_ASSET_DIRS, PDF_WORKER_FILE } from "../dist/pdf.js";
|
|
17
|
+
|
|
18
|
+
const dest = resolve(process.argv[2] ?? "public/pdfjs");
|
|
19
|
+
const require = createRequire(import.meta.url);
|
|
20
|
+
const pkgJson = require.resolve("pdfjs-dist/package.json");
|
|
21
|
+
const root = dirname(pkgJson);
|
|
22
|
+
const { version } = require(pkgJson);
|
|
23
|
+
|
|
24
|
+
rmSync(dest, { recursive: true, force: true });
|
|
25
|
+
mkdirSync(dest, { recursive: true });
|
|
26
|
+
cpSync(join(root, "legacy", "build", PDF_WORKER_FILE), join(dest, PDF_WORKER_FILE));
|
|
27
|
+
for (const dir of PDF_ASSET_DIRS) {
|
|
28
|
+
const from = join(root, dir);
|
|
29
|
+
if (!existsSync(from)) {
|
|
30
|
+
console.error(`chatkit-copy-pdf-assets: pdfjs-dist ${version} has no ${dir}/`);
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
// quickjs-eval is pdf.js's sandbox for a PDF's own JavaScript, which a chat
|
|
34
|
+
// never runs. Leaving it out keeps a document's scripts from having anywhere to go.
|
|
35
|
+
cpSync(from, join(dest, dir), { recursive: true, filter: (p) => !p.includes("quickjs") });
|
|
36
|
+
}
|
|
37
|
+
writeFileSync(join(dest, "VERSION"), `pdfjs-dist ${version}\n`);
|
|
38
|
+
console.log(`chatkit-copy-pdf-assets: pdfjs-dist ${version} → ${dest}`);
|
package/dist/pdf.d.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @bitbaum/chatkit/pdf — a PDF a person attaches, read in the browser and
|
|
3
|
+
* handed on the way a chat model already understands: as text, or as
|
|
4
|
+
* pictures of the pages.
|
|
5
|
+
*
|
|
6
|
+
* import { isPdfFile, readPdf } from "@bitbaum/chatkit/pdf";
|
|
7
|
+
*
|
|
8
|
+
* No model on a free chain reads a PDF file, and most paid ones bill for the
|
|
9
|
+
* privilege. So the PDF never leaves the device as a PDF:
|
|
10
|
+
*
|
|
11
|
+
* - A PDF with a text layer (exported from Word, Docs, a typed report) comes
|
|
12
|
+
* back as `{ kind: "text" }` with page markers kept, so the model can say
|
|
13
|
+
* "on page 4". Attach it like a .txt file.
|
|
14
|
+
* - A PDF without one (a deck exported as slide images, a scan) comes back as
|
|
15
|
+
* `{ kind: "pages" }`: the pages rendered and packed four to a sheet.
|
|
16
|
+
* Attach the sheets like photos, to a model that can see. Four to a sheet
|
|
17
|
+
* keeps each page ~780px wide — slide text stays legible — and turns a
|
|
18
|
+
* 20-page deck into 5 pictures instead of 20.
|
|
19
|
+
*
|
|
20
|
+
* Found 2026-10-27 in OrangeCat: an investor deck (17 slide images, 13.7 MB,
|
|
21
|
+
* no text layer) could not be shared with the Cat at all.
|
|
22
|
+
*
|
|
23
|
+
* Its own entry point, so a chat that never sees a PDF never loads pdf.js;
|
|
24
|
+
* `readPdf` imports it on the first PDF. The pure parts are exported for
|
|
25
|
+
* tests and for apps that word their own notices.
|
|
26
|
+
*/
|
|
27
|
+
/** Pages per sheet, as a grid. */
|
|
28
|
+
export declare const PDF_SHEET_COLUMNS = 2;
|
|
29
|
+
export declare const PDF_SHEET_ROWS = 2;
|
|
30
|
+
export declare const PDF_PAGES_PER_SHEET: number;
|
|
31
|
+
/** Width of one page on a sheet; a sheet is two of these side by side. */
|
|
32
|
+
export declare const PDF_PAGE_WIDTH_PX = 784;
|
|
33
|
+
/** A PDF this large is refused before pdf.js opens it. */
|
|
34
|
+
export declare const PDF_MAX_BYTES: number;
|
|
35
|
+
/**
|
|
36
|
+
* Below this many non-space characters per page, on average, the text layer
|
|
37
|
+
* is treated as absent (a slide deck's page numbers, a scanner's stamp) and
|
|
38
|
+
* the pages are looked at instead.
|
|
39
|
+
*/
|
|
40
|
+
export declare const PDF_MIN_TEXT_CHARS_PER_PAGE = 60;
|
|
41
|
+
export type PdfReading = {
|
|
42
|
+
kind: "text";
|
|
43
|
+
text: string;
|
|
44
|
+
pageCount: number;
|
|
45
|
+
} | {
|
|
46
|
+
kind: "pages";
|
|
47
|
+
sheets: PdfSheet[];
|
|
48
|
+
pageCount: number;
|
|
49
|
+
pagesShown: number;
|
|
50
|
+
};
|
|
51
|
+
export interface PdfSheet {
|
|
52
|
+
/** 1-based, inclusive. */
|
|
53
|
+
firstPage: number;
|
|
54
|
+
lastPage: number;
|
|
55
|
+
dataUrl: string;
|
|
56
|
+
}
|
|
57
|
+
export declare function isPdfFile(file: {
|
|
58
|
+
name: string;
|
|
59
|
+
type: string;
|
|
60
|
+
}): boolean;
|
|
61
|
+
/** True when the extracted text is the document, not a few stray labels. */
|
|
62
|
+
export declare function isTextRich(pageTexts: string[]): boolean;
|
|
63
|
+
/** The pages as one text, each under a marker the Cat can cite. */
|
|
64
|
+
export declare function joinPageTexts(pageTexts: string[]): string;
|
|
65
|
+
/**
|
|
66
|
+
* Which pages go on which sheet, for at most `maxSheets` sheets. Pages past
|
|
67
|
+
* what fits are left out, and the caller says so.
|
|
68
|
+
*/
|
|
69
|
+
export declare function planSheets(pageCount: number, maxSheets: number): number[][];
|
|
70
|
+
/** "deck.pdf · pages 5–8", or "· page 9" for a sheet of one. */
|
|
71
|
+
export declare function sheetName(fileName: string, firstPage: number, lastPage: number): string;
|
|
72
|
+
/**
|
|
73
|
+
* Where pdf.js's own runtime files live — the worker, the image decoders
|
|
74
|
+
* (JPEG 2000, JBIG2), CMaps, standard fonts, ICC profiles. They are fetched by
|
|
75
|
+
* URL at runtime, which a bundler cannot follow, so an app copies them into its
|
|
76
|
+
* static folder with `chatkit-copy-pdf-assets public/pdfjs` and passes
|
|
77
|
+
* `assetsUrl: "/pdfjs/"`. Served from the app's own origin, they also pass a
|
|
78
|
+
* strict Content-Security-Policy. Missing decoders are why a page whose
|
|
79
|
+
* pictures are JPEG 2000 — macOS's "reduce file size" export, the deck this
|
|
80
|
+
* was written for — draws blank.
|
|
81
|
+
*/
|
|
82
|
+
export declare const PDF_ASSET_DIRS: readonly ["wasm", "cmaps", "standard_fonts", "iccs"];
|
|
83
|
+
export declare const PDF_WORKER_FILE = "pdf.worker.min.mjs";
|
|
84
|
+
export interface ReadPdfOptions {
|
|
85
|
+
/** How many pictures the message still has room for. A text PDF needs none. */
|
|
86
|
+
maxSheets: number;
|
|
87
|
+
/** Base URL of the files `chatkit-copy-pdf-assets` copied, e.g. "/pdfjs/". */
|
|
88
|
+
assetsUrl: string;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Read a picked PDF. Throws on a file pdf.js cannot open (encrypted, corrupt)
|
|
92
|
+
* — the app words that for the person. `PDF_MAX_BYTES` is checked by the app
|
|
93
|
+
* before calling, so it can say so in its own words.
|
|
94
|
+
*/
|
|
95
|
+
export declare function readPdf(file: Blob | Uint8Array, opts: ReadPdfOptions): Promise<PdfReading>;
|
|
96
|
+
//# sourceMappingURL=pdf.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pdf.d.ts","sourceRoot":"","sources":["../src/pdf.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,kCAAkC;AAClC,eAAO,MAAM,iBAAiB,IAAI,CAAC;AACnC,eAAO,MAAM,cAAc,IAAI,CAAC;AAChC,eAAO,MAAM,mBAAmB,QAAqC,CAAC;AAEtE,0EAA0E;AAC1E,eAAO,MAAM,iBAAiB,MAAM,CAAC;AAErC,0DAA0D;AAC1D,eAAO,MAAM,aAAa,QAAmB,CAAC;AAE9C;;;;GAIG;AACH,eAAO,MAAM,2BAA2B,KAAK,CAAC;AAI9C,MAAM,MAAM,UAAU,GAClB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GACjD;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,QAAQ,EAAE,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAAC;AAEjF,MAAM,WAAW,QAAQ;IACvB,0BAA0B;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,wBAAgB,SAAS,CAAC,IAAI,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAEvE;AAED,4EAA4E;AAC5E,wBAAgB,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,OAAO,CAMvD;AAED,mEAAmE;AACnE,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,MAAM,CAKzD;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,EAAE,EAAE,CAW3E;AAED,gEAAgE;AAChE,wBAAgB,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAIvF;AAWD;;;;;;;;;GASG;AACH,eAAO,MAAM,cAAc,sDAAuD,CAAC;AACnF,eAAO,MAAM,eAAe,uBAAuB,CAAC;AAwFpD,MAAM,WAAW,cAAc;IAC7B,+EAA+E;IAC/E,SAAS,EAAE,MAAM,CAAC;IAClB,8EAA8E;IAC9E,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;GAIG;AACH,wBAAsB,OAAO,CAAC,IAAI,EAAE,IAAI,GAAG,UAAU,EAAE,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,UAAU,CAAC,CA6BhG"}
|
package/dist/pdf.js
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @bitbaum/chatkit/pdf — a PDF a person attaches, read in the browser and
|
|
3
|
+
* handed on the way a chat model already understands: as text, or as
|
|
4
|
+
* pictures of the pages.
|
|
5
|
+
*
|
|
6
|
+
* import { isPdfFile, readPdf } from "@bitbaum/chatkit/pdf";
|
|
7
|
+
*
|
|
8
|
+
* No model on a free chain reads a PDF file, and most paid ones bill for the
|
|
9
|
+
* privilege. So the PDF never leaves the device as a PDF:
|
|
10
|
+
*
|
|
11
|
+
* - A PDF with a text layer (exported from Word, Docs, a typed report) comes
|
|
12
|
+
* back as `{ kind: "text" }` with page markers kept, so the model can say
|
|
13
|
+
* "on page 4". Attach it like a .txt file.
|
|
14
|
+
* - A PDF without one (a deck exported as slide images, a scan) comes back as
|
|
15
|
+
* `{ kind: "pages" }`: the pages rendered and packed four to a sheet.
|
|
16
|
+
* Attach the sheets like photos, to a model that can see. Four to a sheet
|
|
17
|
+
* keeps each page ~780px wide — slide text stays legible — and turns a
|
|
18
|
+
* 20-page deck into 5 pictures instead of 20.
|
|
19
|
+
*
|
|
20
|
+
* Found 2026-10-27 in OrangeCat: an investor deck (17 slide images, 13.7 MB,
|
|
21
|
+
* no text layer) could not be shared with the Cat at all.
|
|
22
|
+
*
|
|
23
|
+
* Its own entry point, so a chat that never sees a PDF never loads pdf.js;
|
|
24
|
+
* `readPdf` imports it on the first PDF. The pure parts are exported for
|
|
25
|
+
* tests and for apps that word their own notices.
|
|
26
|
+
*/
|
|
27
|
+
/** Pages per sheet, as a grid. */
|
|
28
|
+
export const PDF_SHEET_COLUMNS = 2;
|
|
29
|
+
export const PDF_SHEET_ROWS = 2;
|
|
30
|
+
export const PDF_PAGES_PER_SHEET = PDF_SHEET_COLUMNS * PDF_SHEET_ROWS;
|
|
31
|
+
/** Width of one page on a sheet; a sheet is two of these side by side. */
|
|
32
|
+
export const PDF_PAGE_WIDTH_PX = 784;
|
|
33
|
+
/** A PDF this large is refused before pdf.js opens it. */
|
|
34
|
+
export const PDF_MAX_BYTES = 40 * 1024 * 1024;
|
|
35
|
+
/**
|
|
36
|
+
* Below this many non-space characters per page, on average, the text layer
|
|
37
|
+
* is treated as absent (a slide deck's page numbers, a scanner's stamp) and
|
|
38
|
+
* the pages are looked at instead.
|
|
39
|
+
*/
|
|
40
|
+
export const PDF_MIN_TEXT_CHARS_PER_PAGE = 60;
|
|
41
|
+
const SHEET_JPEG_QUALITY = 0.82;
|
|
42
|
+
export function isPdfFile(file) {
|
|
43
|
+
return file.type === "application/pdf" || file.name.toLowerCase().endsWith(".pdf");
|
|
44
|
+
}
|
|
45
|
+
/** True when the extracted text is the document, not a few stray labels. */
|
|
46
|
+
export function isTextRich(pageTexts) {
|
|
47
|
+
if (pageTexts.length === 0) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
const chars = pageTexts.reduce((n, t) => n + t.replace(/\s+/g, "").length, 0);
|
|
51
|
+
return chars / pageTexts.length >= PDF_MIN_TEXT_CHARS_PER_PAGE;
|
|
52
|
+
}
|
|
53
|
+
/** The pages as one text, each under a marker the Cat can cite. */
|
|
54
|
+
export function joinPageTexts(pageTexts) {
|
|
55
|
+
return pageTexts
|
|
56
|
+
.map((t, i) => `--- page ${i + 1} ---\n${t.trim()}`)
|
|
57
|
+
.join("\n\n")
|
|
58
|
+
.trim();
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Which pages go on which sheet, for at most `maxSheets` sheets. Pages past
|
|
62
|
+
* what fits are left out, and the caller says so.
|
|
63
|
+
*/
|
|
64
|
+
export function planSheets(pageCount, maxSheets) {
|
|
65
|
+
const shown = Math.min(pageCount, Math.max(0, maxSheets) * PDF_PAGES_PER_SHEET);
|
|
66
|
+
const sheets = [];
|
|
67
|
+
for (let first = 1; first <= shown; first += PDF_PAGES_PER_SHEET) {
|
|
68
|
+
const pages = [];
|
|
69
|
+
for (let p = first; p < first + PDF_PAGES_PER_SHEET && p <= shown; p++) {
|
|
70
|
+
pages.push(p);
|
|
71
|
+
}
|
|
72
|
+
sheets.push(pages);
|
|
73
|
+
}
|
|
74
|
+
return sheets;
|
|
75
|
+
}
|
|
76
|
+
/** "deck.pdf · pages 5–8", or "· page 9" for a sheet of one. */
|
|
77
|
+
export function sheetName(fileName, firstPage, lastPage) {
|
|
78
|
+
return firstPage === lastPage
|
|
79
|
+
? `${fileName} · page ${firstPage}`
|
|
80
|
+
: `${fileName} · pages ${firstPage}–${lastPage}`;
|
|
81
|
+
}
|
|
82
|
+
let pdfjsPromise = null;
|
|
83
|
+
/**
|
|
84
|
+
* Where pdf.js's own runtime files live — the worker, the image decoders
|
|
85
|
+
* (JPEG 2000, JBIG2), CMaps, standard fonts, ICC profiles. They are fetched by
|
|
86
|
+
* URL at runtime, which a bundler cannot follow, so an app copies them into its
|
|
87
|
+
* static folder with `chatkit-copy-pdf-assets public/pdfjs` and passes
|
|
88
|
+
* `assetsUrl: "/pdfjs/"`. Served from the app's own origin, they also pass a
|
|
89
|
+
* strict Content-Security-Policy. Missing decoders are why a page whose
|
|
90
|
+
* pictures are JPEG 2000 — macOS's "reduce file size" export, the deck this
|
|
91
|
+
* was written for — draws blank.
|
|
92
|
+
*/
|
|
93
|
+
export const PDF_ASSET_DIRS = ["wasm", "cmaps", "standard_fonts", "iccs"];
|
|
94
|
+
export const PDF_WORKER_FILE = "pdf.worker.min.mjs";
|
|
95
|
+
function withSlash(url) {
|
|
96
|
+
return url.endsWith("/") ? url : `${url}/`;
|
|
97
|
+
}
|
|
98
|
+
/** pdf.js, loaded on the first PDF — never part of the chat's first paint. */
|
|
99
|
+
function loadPdfJs(assetsUrl) {
|
|
100
|
+
if (!pdfjsPromise) {
|
|
101
|
+
pdfjsPromise = import("pdfjs-dist/legacy/build/pdf.mjs").then((pdfjs) => {
|
|
102
|
+
pdfjs.GlobalWorkerOptions.workerSrc = `${withSlash(assetsUrl)}${PDF_WORKER_FILE}`;
|
|
103
|
+
return pdfjs;
|
|
104
|
+
});
|
|
105
|
+
// A failed load (offline, blocked chunk) must not poison the next try.
|
|
106
|
+
pdfjsPromise.catch(() => {
|
|
107
|
+
pdfjsPromise = null;
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
return pdfjsPromise;
|
|
111
|
+
}
|
|
112
|
+
async function pageTextsOf(doc) {
|
|
113
|
+
const texts = [];
|
|
114
|
+
for (let n = 1; n <= doc.numPages; n++) {
|
|
115
|
+
const page = await doc.getPage(n);
|
|
116
|
+
const content = await page.getTextContent();
|
|
117
|
+
let text = "";
|
|
118
|
+
for (const item of content.items) {
|
|
119
|
+
if ("str" in item) {
|
|
120
|
+
text += item.str + (item.hasEOL ? "\n" : " ");
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
texts.push(text.replace(/[ \t]+\n/g, "\n").replace(/[ \t]{2,}/g, " "));
|
|
124
|
+
page.cleanup();
|
|
125
|
+
}
|
|
126
|
+
return texts;
|
|
127
|
+
}
|
|
128
|
+
async function renderSheet(doc, pages) {
|
|
129
|
+
// Rendering needs a canvas; outside a browser the pages are simply not drawn.
|
|
130
|
+
if (typeof document === "undefined")
|
|
131
|
+
return null;
|
|
132
|
+
const rendered = [];
|
|
133
|
+
for (const n of pages) {
|
|
134
|
+
const page = await doc.getPage(n);
|
|
135
|
+
const base = page.getViewport({ scale: 1 });
|
|
136
|
+
const viewport = page.getViewport({ scale: PDF_PAGE_WIDTH_PX / base.width });
|
|
137
|
+
const canvas = document.createElement("canvas");
|
|
138
|
+
canvas.width = Math.ceil(viewport.width);
|
|
139
|
+
canvas.height = Math.ceil(viewport.height);
|
|
140
|
+
const ctx = canvas.getContext("2d");
|
|
141
|
+
if (!ctx) {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
// White, not transparent: a JPEG turns transparency black.
|
|
145
|
+
ctx.fillStyle = "#fff";
|
|
146
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
147
|
+
await page.render({ canvas, canvasContext: ctx, viewport }).promise;
|
|
148
|
+
rendered.push(canvas);
|
|
149
|
+
page.cleanup();
|
|
150
|
+
}
|
|
151
|
+
if (rendered.length === 0) {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
const cols = Math.min(PDF_SHEET_COLUMNS, rendered.length);
|
|
155
|
+
const rows = Math.ceil(rendered.length / PDF_SHEET_COLUMNS);
|
|
156
|
+
const cellW = PDF_PAGE_WIDTH_PX;
|
|
157
|
+
const cellH = Math.max(...rendered.map((c) => c.height));
|
|
158
|
+
const gap = 8;
|
|
159
|
+
const sheet = document.createElement("canvas");
|
|
160
|
+
sheet.width = cols * cellW + (cols - 1) * gap;
|
|
161
|
+
sheet.height = rows * cellH + (rows - 1) * gap;
|
|
162
|
+
const ctx = sheet.getContext("2d");
|
|
163
|
+
if (!ctx) {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
// The gaps read as page edges; grey so a white page still has one.
|
|
167
|
+
ctx.fillStyle = "#9ca3af";
|
|
168
|
+
ctx.fillRect(0, 0, sheet.width, sheet.height);
|
|
169
|
+
rendered.forEach((c, i) => {
|
|
170
|
+
const x = (i % PDF_SHEET_COLUMNS) * (cellW + gap);
|
|
171
|
+
const y = Math.floor(i / PDF_SHEET_COLUMNS) * (cellH + gap);
|
|
172
|
+
ctx.drawImage(c, x, y);
|
|
173
|
+
});
|
|
174
|
+
return sheet.toDataURL("image/jpeg", SHEET_JPEG_QUALITY);
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Read a picked PDF. Throws on a file pdf.js cannot open (encrypted, corrupt)
|
|
178
|
+
* — the app words that for the person. `PDF_MAX_BYTES` is checked by the app
|
|
179
|
+
* before calling, so it can say so in its own words.
|
|
180
|
+
*/
|
|
181
|
+
export async function readPdf(file, opts) {
|
|
182
|
+
const pdfjs = await loadPdfJs(opts.assetsUrl);
|
|
183
|
+
const data = file instanceof Uint8Array ? file : new Uint8Array(await file.arrayBuffer());
|
|
184
|
+
const base = withSlash(opts.assetsUrl);
|
|
185
|
+
const task = pdfjs.getDocument({
|
|
186
|
+
data,
|
|
187
|
+
wasmUrl: `${base}wasm/`,
|
|
188
|
+
cMapUrl: `${base}cmaps/`,
|
|
189
|
+
standardFontDataUrl: `${base}standard_fonts/`,
|
|
190
|
+
iccUrl: `${base}iccs/`,
|
|
191
|
+
});
|
|
192
|
+
const doc = await task.promise;
|
|
193
|
+
try {
|
|
194
|
+
const pageTexts = await pageTextsOf(doc);
|
|
195
|
+
if (isTextRich(pageTexts)) {
|
|
196
|
+
return { kind: "text", text: joinPageTexts(pageTexts), pageCount: doc.numPages };
|
|
197
|
+
}
|
|
198
|
+
const sheets = [];
|
|
199
|
+
for (const pages of planSheets(doc.numPages, opts.maxSheets)) {
|
|
200
|
+
const dataUrl = await renderSheet(doc, pages);
|
|
201
|
+
if (dataUrl) {
|
|
202
|
+
sheets.push({ firstPage: pages[0], lastPage: pages[pages.length - 1], dataUrl });
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
const pagesShown = sheets.length > 0 ? sheets[sheets.length - 1].lastPage : 0;
|
|
206
|
+
return { kind: "pages", sheets, pageCount: doc.numPages, pagesShown };
|
|
207
|
+
}
|
|
208
|
+
finally {
|
|
209
|
+
void task.destroy();
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
//# sourceMappingURL=pdf.js.map
|
package/dist/pdf.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pdf.js","sourceRoot":"","sources":["../src/pdf.ts"],"names":[],"mappings":"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"}
|
|
@@ -9,6 +9,19 @@ export type AttachMenuLabels = {
|
|
|
9
9
|
sources: Record<AttachSource, string>;
|
|
10
10
|
};
|
|
11
11
|
export declare const DEFAULT_ATTACH_MENU_LABELS: AttachMenuLabels;
|
|
12
|
+
/**
|
|
13
|
+
* One of the app's own rows in the attach sheet, under Camera · Photos ·
|
|
14
|
+
* Files: an icon, what it adds (`label`), an optional second line. Every row
|
|
15
|
+
* has this one anatomy, so a product's "Your things" reads as part of the
|
|
16
|
+
* same sheet, not a second menu. Picking a row closes the sheet first.
|
|
17
|
+
*/
|
|
18
|
+
export type AttachExtra = {
|
|
19
|
+
id: string;
|
|
20
|
+
label: string;
|
|
21
|
+
detail?: string;
|
|
22
|
+
icon?: ReactNode;
|
|
23
|
+
onPick: () => void;
|
|
24
|
+
};
|
|
12
25
|
/**
|
|
13
26
|
* The paperclip, and what it opens on a phone: a sheet with Camera, Photos and
|
|
14
27
|
* Files — one input each. See `ATTACH_SOURCE_INPUT` for why three inputs and
|
|
@@ -27,7 +40,7 @@ export declare const DEFAULT_ATTACH_MENU_LABELS: AttachMenuLabels;
|
|
|
27
40
|
* they sit under Camera · Photos · Files in the same sheet, on every pointer,
|
|
28
41
|
* since a mouse cannot reach them through a file dialog.
|
|
29
42
|
*/
|
|
30
|
-
export declare function AttachMenu({ onFiles, disabled, labels, sources, trigger, extra, onOpenChange, }: {
|
|
43
|
+
export declare function AttachMenu({ onFiles, disabled, labels, sources, trigger, extra, rows, onOpenChange, }: {
|
|
31
44
|
onFiles: (files: FileList | null) => void;
|
|
32
45
|
disabled?: boolean;
|
|
33
46
|
labels?: AttachMenuLabels;
|
|
@@ -41,6 +54,10 @@ export declare function AttachMenu({ onFiles, disabled, labels, sources, trigger
|
|
|
41
54
|
* sheet opens on every pointer. A row that should close the sheet calls
|
|
42
55
|
* the `close` it is given. */
|
|
43
56
|
extra?: (close: () => void) => ReactNode;
|
|
57
|
+
/** The app's rows as data — drawn with one anatomy (see `AttachExtra`).
|
|
58
|
+
* With them the sheet opens on every pointer; with no `sources` it holds
|
|
59
|
+
* only them. */
|
|
60
|
+
rows?: readonly AttachExtra[];
|
|
44
61
|
onOpenChange?: (open: boolean) => void;
|
|
45
62
|
}): import("react").JSX.Element;
|
|
46
63
|
//# sourceMappingURL=AttachMenu.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AttachMenu.d.ts","sourceRoot":"","sources":["../../src/react/AttachMenu.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAoB,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AACzD,OAAO,EAAuB,KAAK,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAI3E,MAAM,MAAM,gBAAgB,GAAG;IAC7B,4BAA4B;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,yBAAyB;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC;CACvC,CAAC;AAEF,eAAO,MAAM,0BAA0B,EAAE,gBAKxC,CAAC;AASF;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,UAAU,CAAC,EACzB,OAAO,EACP,QAAgB,EAChB,MAAmC,EACnC,OAAiB,EACjB,OAAO,EACP,KAAK,EACL,YAAY,GACb,EAAE;IACD,OAAO,EAAE,CAAC,KAAK,EAAE,QAAQ,GAAG,IAAI,KAAK,IAAI,CAAC;IAC1C,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,MAAM,CAAC,EAAE,gBAAgB,CAAC;IAC1B;8EAC0E;IAC1E,OAAO,CAAC,EAAE,SAAS,YAAY,EAAE,CAAC;IAClC;yEACqE;IACrE,OAAO,CAAC,EAAE,SAAS,CAAC;IACpB;;mCAE+B;IAC/B,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,IAAI,KAAK,SAAS,CAAC;IACzC,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;CACxC,+
|
|
1
|
+
{"version":3,"file":"AttachMenu.d.ts","sourceRoot":"","sources":["../../src/react/AttachMenu.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAoB,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AACzD,OAAO,EAAuB,KAAK,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAI3E,MAAM,MAAM,gBAAgB,GAAG;IAC7B,4BAA4B;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,yBAAyB;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC;CACvC,CAAC;AAEF,eAAO,MAAM,0BAA0B,EAAE,gBAKxC,CAAC;AAEF;;;;;GAKG;AACH,MAAM,MAAM,WAAW,GAAG;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,MAAM,EAAE,MAAM,IAAI,CAAC;CACpB,CAAC;AASF;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,UAAU,CAAC,EACzB,OAAO,EACP,QAAgB,EAChB,MAAmC,EACnC,OAAiB,EACjB,OAAO,EACP,KAAK,EACL,IAAI,EACJ,YAAY,GACb,EAAE;IACD,OAAO,EAAE,CAAC,KAAK,EAAE,QAAQ,GAAG,IAAI,KAAK,IAAI,CAAC;IAC1C,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,MAAM,CAAC,EAAE,gBAAgB,CAAC;IAC1B;8EAC0E;IAC1E,OAAO,CAAC,EAAE,SAAS,YAAY,EAAE,CAAC;IAClC;yEACqE;IACrE,OAAO,CAAC,EAAE,SAAS,CAAC;IACpB;;mCAE+B;IAC/B,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,IAAI,KAAK,SAAS,CAAC;IACzC;;qBAEiB;IACjB,IAAI,CAAC,EAAE,SAAS,WAAW,EAAE,CAAC;IAC9B,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;CACxC,+BAkGA"}
|