@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.
Files changed (42) hide show
  1. package/README.md +200 -0
  2. package/bin/chatkit-copy-pdf-assets.mjs +38 -0
  3. package/dist/pdf.d.ts +96 -0
  4. package/dist/pdf.d.ts.map +1 -0
  5. package/dist/pdf.js +212 -0
  6. package/dist/pdf.js.map +1 -0
  7. package/dist/react/AttachMenu.d.ts +18 -1
  8. package/dist/react/AttachMenu.d.ts.map +1 -1
  9. package/dist/react/AttachMenu.js +8 -4
  10. package/dist/react/AttachMenu.js.map +1 -1
  11. package/dist/react/ChatApp.d.ts +79 -5
  12. package/dist/react/ChatApp.d.ts.map +1 -1
  13. package/dist/react/ChatApp.js +113 -7
  14. package/dist/react/ChatApp.js.map +1 -1
  15. package/dist/react/ChatHistory.d.ts +7 -1
  16. package/dist/react/ChatHistory.d.ts.map +1 -1
  17. package/dist/react/ChatHistory.js +2 -2
  18. package/dist/react/ChatHistory.js.map +1 -1
  19. package/dist/react/Composer.d.ts +20 -3
  20. package/dist/react/Composer.d.ts.map +1 -1
  21. package/dist/react/Composer.js +19 -4
  22. package/dist/react/Composer.js.map +1 -1
  23. package/dist/react/Thread.d.ts +48 -2
  24. package/dist/react/Thread.d.ts.map +1 -1
  25. package/dist/react/Thread.js +13 -6
  26. package/dist/react/Thread.js.map +1 -1
  27. package/dist/react/attach.d.ts +1 -1
  28. package/dist/react/attach.d.ts.map +1 -1
  29. package/dist/react/index.d.ts +10 -10
  30. package/dist/react/index.d.ts.map +1 -1
  31. package/dist/react/index.js +4 -4
  32. package/dist/react/index.js.map +1 -1
  33. package/dist/react/use-attachments.d.ts +17 -0
  34. package/dist/react/use-attachments.d.ts.map +1 -1
  35. package/dist/react/use-attachments.js +89 -72
  36. package/dist/react/use-attachments.js.map +1 -1
  37. package/dist/react/use-chat-app.d.ts +53 -3
  38. package/dist/react/use-chat-app.d.ts.map +1 -1
  39. package/dist/react/use-chat-app.js +126 -15
  40. package/dist/react/use-chat-app.js.map +1 -1
  41. package/package.json +13 -4
  42. 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
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pdf.js","sourceRoot":"","sources":["../src/pdf.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,kCAAkC;AAClC,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,CAAC;AACnC,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,CAAC;AAChC,MAAM,CAAC,MAAM,mBAAmB,GAAG,iBAAiB,GAAG,cAAc,CAAC;AAEtE,0EAA0E;AAC1E,MAAM,CAAC,MAAM,iBAAiB,GAAG,GAAG,CAAC;AAErC,0DAA0D;AAC1D,MAAM,CAAC,MAAM,aAAa,GAAG,EAAE,GAAG,IAAI,GAAG,IAAI,CAAC;AAE9C;;;;GAIG;AACH,MAAM,CAAC,MAAM,2BAA2B,GAAG,EAAE,CAAC;AAE9C,MAAM,kBAAkB,GAAG,IAAI,CAAC;AAahC,MAAM,UAAU,SAAS,CAAC,IAAoC;IAC5D,OAAO,IAAI,CAAC,IAAI,KAAK,iBAAiB,IAAI,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;AACrF,CAAC;AAED,4EAA4E;AAC5E,MAAM,UAAU,UAAU,CAAC,SAAmB;IAC5C,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC3B,OAAO,KAAK,CAAC;IACf,CAAC;IACD,MAAM,KAAK,GAAG,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;IAC9E,OAAO,KAAK,GAAG,SAAS,CAAC,MAAM,IAAI,2BAA2B,CAAC;AACjE,CAAC;AAED,mEAAmE;AACnE,MAAM,UAAU,aAAa,CAAC,SAAmB;IAC/C,OAAO,SAAS;SACb,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,YAAY,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC;SACnD,IAAI,CAAC,MAAM,CAAC;SACZ,IAAI,EAAE,CAAC;AACZ,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,UAAU,CAAC,SAAiB,EAAE,SAAiB;IAC7D,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,SAAS,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,SAAS,CAAC,GAAG,mBAAmB,CAAC,CAAC;IAChF,MAAM,MAAM,GAAe,EAAE,CAAC;IAC9B,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,IAAI,KAAK,EAAE,KAAK,IAAI,mBAAmB,EAAE,CAAC;QACjE,MAAM,KAAK,GAAa,EAAE,CAAC;QAC3B,KAAK,IAAI,CAAC,GAAG,KAAK,EAAE,CAAC,GAAG,KAAK,GAAG,mBAAmB,IAAI,CAAC,IAAI,KAAK,EAAE,CAAC,EAAE,EAAE,CAAC;YACvE,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAChB,CAAC;QACD,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACrB,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,gEAAgE;AAChE,MAAM,UAAU,SAAS,CAAC,QAAgB,EAAE,SAAiB,EAAE,QAAgB;IAC7E,OAAO,SAAS,KAAK,QAAQ;QAC3B,CAAC,CAAC,GAAG,QAAQ,WAAW,SAAS,EAAE;QACnC,CAAC,CAAC,GAAG,QAAQ,YAAY,SAAS,IAAI,QAAQ,EAAE,CAAC;AACrD,CAAC;AASD,IAAI,YAAY,GAA0B,IAAI,CAAC;AAE/C;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,MAAM,EAAE,OAAO,EAAE,gBAAgB,EAAE,MAAM,CAAU,CAAC;AACnF,MAAM,CAAC,MAAM,eAAe,GAAG,oBAAoB,CAAC;AAEpD,SAAS,SAAS,CAAC,GAAW;IAC5B,OAAO,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,GAAG,GAAG,CAAC;AAC7C,CAAC;AAED,8EAA8E;AAC9E,SAAS,SAAS,CAAC,SAAiB;IAClC,IAAI,CAAC,YAAY,EAAE,CAAC;QAClB,YAAY,GAAG,MAAM,CAAC,iCAAiC,CAAC,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE;YACtE,KAAK,CAAC,mBAAmB,CAAC,SAAS,GAAG,GAAG,SAAS,CAAC,SAAS,CAAC,GAAG,eAAe,EAAE,CAAC;YAClF,OAAO,KAAK,CAAC;QACf,CAAC,CAAC,CAAC;QACH,uEAAuE;QACvE,YAAY,CAAC,KAAK,CAAC,GAAG,EAAE;YACtB,YAAY,GAAG,IAAI,CAAC;QACtB,CAAC,CAAC,CAAC;IACL,CAAC;IACD,OAAO,YAAY,CAAC;AACtB,CAAC;AAED,KAAK,UAAU,WAAW,CAAC,GAAgB;IACzC,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,GAAG,CAAC,QAAQ,EAAE,CAAC,EAAE,EAAE,CAAC;QACvC,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;QAClC,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,cAAc,EAAE,CAAC;QAC5C,IAAI,IAAI,GAAG,EAAE,CAAC;QACd,KAAK,MAAM,IAAI,IAAI,OAAO,CAAC,KAAK,EAAE,CAAC;YACjC,IAAI,KAAK,IAAI,IAAI,EAAE,CAAC;gBAClB,IAAI,IAAI,IAAI,CAAC,GAAG,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;YAChD,CAAC;QACH,CAAC;QACD,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,WAAW,EAAE,IAAI,CAAC,CAAC,OAAO,CAAC,YAAY,EAAE,GAAG,CAAC,CAAC,CAAC;QACvE,IAAI,CAAC,OAAO,EAAE,CAAC;IACjB,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,KAAK,UAAU,WAAW,CAAC,GAAgB,EAAE,KAAe;IAC1D,8EAA8E;IAC9E,IAAI,OAAO,QAAQ,KAAK,WAAW;QAAE,OAAO,IAAI,CAAC;IACjD,MAAM,QAAQ,GAAwB,EAAE,CAAC;IACzC,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;QACtB,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;QAClC,MAAM,IAAI,GAAG,IAAI,CAAC,WAAW,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC;QAC5C,MAAM,QAAQ,GAAG,IAAI,CAAC,WAAW,CAAC,EAAE,KAAK,EAAE,iBAAiB,GAAG,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC;QAC7E,MAAM,MAAM,GAAG,QAAQ,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC;QAChD,MAAM,CAAC,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;QACzC,MAAM,CAAC,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;QAC3C,MAAM,GAAG,GAAG,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;QACpC,IAAI,CAAC,GAAG,EAAE,CAAC;YACT,OAAO,IAAI,CAAC;QACd,CAAC;QACD,2DAA2D;QAC3D,GAAG,CAAC,SAAS,GAAG,MAAM,CAAC;QACvB,GAAG,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC;QAChD,MAAM,IAAI,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,aAAa,EAAE,GAAG,EAAE,QAAQ,EAAE,CAAC,CAAC,OAAO,CAAC;QACpE,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACtB,IAAI,CAAC,OAAO,EAAE,CAAC;IACjB,CAAC;IACD,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC1B,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,iBAAiB,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;IAC1D,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,GAAG,iBAAiB,CAAC,CAAC;IAC5D,MAAM,KAAK,GAAG,iBAAiB,CAAC;IAChC,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC;IACzD,MAAM,GAAG,GAAG,CAAC,CAAC;IAEd,MAAM,KAAK,GAAG,QAAQ,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC;IAC/C,KAAK,CAAC,KAAK,GAAG,IAAI,GAAG,KAAK,GAAG,CAAC,IAAI,GAAG,CAAC,CAAC,GAAG,GAAG,CAAC;IAC9C,KAAK,CAAC,MAAM,GAAG,IAAI,GAAG,KAAK,GAAG,CAAC,IAAI,GAAG,CAAC,CAAC,GAAG,GAAG,CAAC;IAC/C,MAAM,GAAG,GAAG,KAAK,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;IACnC,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,OAAO,IAAI,CAAC;IACd,CAAC;IACD,mEAAmE;IACnE,GAAG,CAAC,SAAS,GAAG,SAAS,CAAC;IAC1B,GAAG,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC,EAAE,KAAK,CAAC,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAC9C,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE;QACxB,MAAM,CAAC,GAAG,CAAC,CAAC,GAAG,iBAAiB,CAAC,GAAG,CAAC,KAAK,GAAG,GAAG,CAAC,CAAC;QAClD,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,iBAAiB,CAAC,GAAG,CAAC,KAAK,GAAG,GAAG,CAAC,CAAC;QAC5D,GAAG,CAAC,SAAS,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;IACzB,CAAC,CAAC,CAAC;IACH,OAAO,KAAK,CAAC,SAAS,CAAC,YAAY,EAAE,kBAAkB,CAAC,CAAC;AAC3D,CAAC;AASD;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,OAAO,CAAC,IAAuB,EAAE,IAAoB;IACzE,MAAM,KAAK,GAAG,MAAM,SAAS,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IAC9C,MAAM,IAAI,GAAG,IAAI,YAAY,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,UAAU,CAAC,MAAM,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC;IAC1F,MAAM,IAAI,GAAG,SAAS,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IACvC,MAAM,IAAI,GAAG,KAAK,CAAC,WAAW,CAAC;QAC7B,IAAI;QACJ,OAAO,EAAE,GAAG,IAAI,OAAO;QACvB,OAAO,EAAE,GAAG,IAAI,QAAQ;QACxB,mBAAmB,EAAE,GAAG,IAAI,iBAAiB;QAC7C,MAAM,EAAE,GAAG,IAAI,OAAO;KACvB,CAAC,CAAC;IACH,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC;IAC/B,IAAI,CAAC;QACH,MAAM,SAAS,GAAG,MAAM,WAAW,CAAC,GAAG,CAAC,CAAC;QACzC,IAAI,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;YAC1B,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,aAAa,CAAC,SAAS,CAAC,EAAE,SAAS,EAAE,GAAG,CAAC,QAAQ,EAAE,CAAC;QACnF,CAAC;QACD,MAAM,MAAM,GAAe,EAAE,CAAC;QAC9B,KAAK,MAAM,KAAK,IAAI,UAAU,CAAC,GAAG,CAAC,QAAQ,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC;YAC7D,MAAM,OAAO,GAAG,MAAM,WAAW,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;YAC9C,IAAI,OAAO,EAAE,CAAC;gBACZ,MAAM,CAAC,IAAI,CAAC,EAAE,SAAS,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC;YACnF,CAAC;QACH,CAAC;QACD,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;QAC9E,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,GAAG,CAAC,QAAQ,EAAE,UAAU,EAAE,CAAC;IACxE,CAAC;YAAS,CAAC;QACT,KAAK,IAAI,CAAC,OAAO,EAAE,CAAC;IACtB,CAAC;AACH,CAAC"}
@@ -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,+BAuEA"}
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"}