@bitbaum/chatkit 0.2.1 → 0.7.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 +104 -8
- package/bin/chatkit-check-file-inputs.mjs +87 -0
- package/dist/attachments.d.ts +37 -0
- package/dist/attachments.d.ts.map +1 -1
- package/dist/attachments.js +75 -1
- package/dist/attachments.js.map +1 -1
- package/dist/composer.d.ts +4 -1
- package/dist/composer.d.ts.map +1 -1
- package/dist/composer.js +4 -2
- package/dist/composer.js.map +1 -1
- package/dist/dictation.d.ts +62 -2
- package/dist/dictation.d.ts.map +1 -1
- package/dist/dictation.js +103 -0
- package/dist/dictation.js.map +1 -1
- package/dist/image.d.ts +7 -0
- package/dist/image.d.ts.map +1 -0
- package/dist/image.js +41 -0
- package/dist/image.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/react/AttachMenu.d.ts +37 -0
- package/dist/react/AttachMenu.d.ts.map +1 -0
- package/dist/react/AttachMenu.js +70 -0
- package/dist/react/AttachMenu.js.map +1 -0
- package/dist/react/Composer.d.ts +24 -2
- package/dist/react/Composer.d.ts.map +1 -1
- package/dist/react/Composer.js +59 -10
- package/dist/react/Composer.js.map +1 -1
- package/dist/react/Thread.d.ts +27 -2
- package/dist/react/Thread.d.ts.map +1 -1
- package/dist/react/Thread.js +17 -4
- package/dist/react/Thread.js.map +1 -1
- package/dist/react/attach.d.ts +17 -0
- package/dist/react/attach.d.ts.map +1 -0
- package/dist/react/attach.js +16 -0
- package/dist/react/attach.js.map +1 -0
- package/dist/react/hooks.d.ts +8 -1
- package/dist/react/hooks.d.ts.map +1 -1
- package/dist/react/hooks.js +27 -3
- package/dist/react/hooks.js.map +1 -1
- package/dist/react/icons.d.ts +3 -0
- package/dist/react/icons.d.ts.map +1 -1
- package/dist/react/icons.js +3 -0
- package/dist/react/icons.js.map +1 -1
- package/dist/react/index.d.ts +3 -1
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +2 -1
- package/dist/react/index.js.map +1 -1
- package/dist/react/use-attachments.d.ts.map +1 -1
- package/dist/react/use-attachments.js +6 -34
- package/dist/react/use-attachments.js.map +1 -1
- package/dist/react/use-dictation.d.ts +23 -0
- package/dist/react/use-dictation.d.ts.map +1 -1
- package/dist/react/use-dictation.js +175 -13
- package/dist/react/use-dictation.js.map +1 -1
- package/dist/replies.d.ts +41 -0
- package/dist/replies.d.ts.map +1 -0
- package/dist/replies.js +84 -0
- package/dist/replies.js.map +1 -0
- package/package.json +9 -1
- package/styles.css +179 -3
package/README.md
CHANGED
|
@@ -15,11 +15,15 @@ interface that calls them.)
|
|
|
15
15
|
## Install
|
|
16
16
|
|
|
17
17
|
```sh
|
|
18
|
-
pnpm add
|
|
18
|
+
pnpm add @bitbaum/chatkit@^0.6.0
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
**Install releases from npm, never a commit or a branch.** A semver range is
|
|
22
|
+
what lets a fix reach every product: Renovate/Dependabot opens the bump PR in
|
|
23
|
+
each app when a release ships. A pinned commit or tag silently freezes the app
|
|
24
|
+
on whatever it pinned — heidi and substrata kept the broken Android paperclip
|
|
25
|
+
for days after the fix merged, because one pinned `v0.2.0` and npm had only
|
|
26
|
+
`0.2.1`.
|
|
23
27
|
|
|
24
28
|
## Use
|
|
25
29
|
|
|
@@ -42,6 +46,8 @@ import "@bitbaum/chatkit/styles.css";
|
|
|
42
46
|
voice={{ transcribeUrl: "/api/transcribe" }}
|
|
43
47
|
attach // only if your API accepts attachments
|
|
44
48
|
tools={<ModelPicker />} // app-specific controls go in slots
|
|
49
|
+
heldAttachments={refs.length} // attachments the APP keeps (shown in `header`)
|
|
50
|
+
attachmentOnlyText="See attached" // so a message of only those can still send
|
|
45
51
|
/>
|
|
46
52
|
```
|
|
47
53
|
|
|
@@ -62,6 +68,94 @@ nothing heard, not available), never as a button that does nothing.
|
|
|
62
68
|
`prefer: "server"` skips the browser leg (one model everywhere, language
|
|
63
69
|
detected by the model). `rememberKey: null` stores nothing on the device.
|
|
64
70
|
|
|
71
|
+
### Suggested replies
|
|
72
|
+
|
|
73
|
+
The two to four things the person is likely to say next, as buttons under the
|
|
74
|
+
latest answer — one tap sends it. The model writes them in the same turn, so
|
|
75
|
+
there is no second call and no wait:
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
import { REPLIES_INSTRUCTION } from "@bitbaum/chatkit";
|
|
79
|
+
const system = `${yourPrompt}\n\n${REPLIES_INSTRUCTION}`; // server side
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
```tsx
|
|
83
|
+
<ChatThread messages={messages} onReply={send} … />
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
That is the whole integration. `ChatThread` takes the `quick_replies` block out
|
|
87
|
+
of every answer (and out of a streaming one before it has finished, so nobody
|
|
88
|
+
watches JSON appear), and shows the buttons under the latest answer only, never
|
|
89
|
+
while a turn runs. Pass `replies` on a message to supply your own instead.
|
|
90
|
+
An app with its own thread uses `extractReplies(text)` and `<ChatReplies>`, so
|
|
91
|
+
the buttons look and behave the same everywhere.
|
|
92
|
+
|
|
93
|
+
### The next message while a turn runs
|
|
94
|
+
|
|
95
|
+
`queue` keeps Send live while `sending`, beside Stop, and the placeholder
|
|
96
|
+
reads "Queue a message…": the app receives the message through `onSend` as
|
|
97
|
+
usual and sends it on when the turn ends. Without it a send during a turn is
|
|
98
|
+
refused, which holds the next thought until a spinner stops.
|
|
99
|
+
|
|
100
|
+
### Recordings
|
|
101
|
+
|
|
102
|
+
With a server leg, the paperclip also takes a recording that already exists —
|
|
103
|
+
a voice memo from the phone's recorder, a taped meeting. It is dictation that
|
|
104
|
+
happened earlier: the file is posted to `transcribeUrl` under its own name
|
|
105
|
+
(Whisper reads the container from the extension) and the words land in the box
|
|
106
|
+
like a mic take, never staged as an attachment the model would have to read.
|
|
107
|
+
Files are recognised by type and, where a picker hands them over typeless as
|
|
108
|
+
Android does with `.m4a`, by extension. Uploads are capped at
|
|
109
|
+
`MAX_AUDIO_FILE_BYTES` (120 MB — an hour of phone memo with room to spare);
|
|
110
|
+
over it, the person is told in words before any bytes move. Your server leg
|
|
111
|
+
should compress and split long audio before Whisper; loki's does.
|
|
112
|
+
|
|
113
|
+
### Camera, Photos, Files
|
|
114
|
+
|
|
115
|
+
On a touch screen the paperclip opens a sheet with three choices — Camera,
|
|
116
|
+
Photos, Files — each its own file input. One input with a mixed `accept`
|
|
117
|
+
(`image/*,text/*,audio/*`) is what a phone answers with a chooser of capture
|
|
118
|
+
apps (Camera, Camera, Recorder, "Photos & Videos"), where the screenshot you
|
|
119
|
+
came to send is three levels down and the file browser is not offered at all.
|
|
120
|
+
`ATTACH_SOURCE_INPUT` is the SSOT: Camera captures, Photos opens the system
|
|
121
|
+
photo picker, Files has no `accept` and opens the real document browser.
|
|
122
|
+
Because Files takes anything, `isTextFile` decides afterwards what can be read
|
|
123
|
+
inline; a PDF gets a sentence, not a prompt full of bytes. With a mouse the
|
|
124
|
+
paperclip opens the file dialog directly — on a desktop all three are the
|
|
125
|
+
same dialog. A box that is not chatkit's composer renders `<AttachMenu
|
|
126
|
+
onFiles={…} />` rather than a bare `<input type="file">`. Labels:
|
|
127
|
+
`labels={{ attachMenu: { title, close, sources: { camera, photos, files } } }}`.
|
|
128
|
+
|
|
129
|
+
### Picking files anywhere — `@bitbaum/chatkit/attach`
|
|
130
|
+
|
|
131
|
+
An app with no chat still has uploaders (an avatar, product photos, a receipt).
|
|
132
|
+
They need the same thing from a phone, so they use the same piece:
|
|
133
|
+
|
|
134
|
+
```tsx
|
|
135
|
+
import { AttachMenu, shrinkImage } from "@bitbaum/chatkit/attach";
|
|
136
|
+
|
|
137
|
+
<AttachMenu
|
|
138
|
+
sources={["camera", "photos"]} // an image uploader offers no Files
|
|
139
|
+
trigger="Add photo"
|
|
140
|
+
onFiles={(files) => upload(files)}
|
|
141
|
+
/>;
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
`sources` narrows the sheet (one source skips it). `shrinkImage(file, maxEdge,
|
|
145
|
+
maxBytes)` is the in-browser downscale the composer uses. `acceptFamilies` and
|
|
146
|
+
`ATTACH_SOURCE_INPUT` are the rule itself.
|
|
147
|
+
|
|
148
|
+
### The check every app runs in CI
|
|
149
|
+
|
|
150
|
+
```sh
|
|
151
|
+
pnpm exec chatkit-check-file-inputs # src app components
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Fails on any file input whose literal `accept` mixes families (image + text,
|
|
155
|
+
audio + text…) — the exact shape that hid screenshots on Android. One input per
|
|
156
|
+
family, or `AttachMenu`. A real exception carries
|
|
157
|
+
`// chatkit-allow-mixed-accept: <reason>` on the line or the line above.
|
|
158
|
+
|
|
65
159
|
### Screenshots
|
|
66
160
|
|
|
67
161
|
`attach` takes limits: `attach={{ maxImageEdge: 1024 }}` shrinks a picture in
|
|
@@ -90,7 +184,8 @@ on `Composer`, `labels` on `ChatThread`.
|
|
|
90
184
|
|
|
91
185
|
## The standard (what this package guarantees)
|
|
92
186
|
|
|
93
|
-
1. A microphone in the composer, with the server fallback above
|
|
187
|
+
1. A microphone in the composer, with the server fallback above — and the
|
|
188
|
+
same server leg for a recording picked as a file.
|
|
94
189
|
2. 16px text in the input and the messages (below that iOS zooms the page).
|
|
95
190
|
3. A composer that belongs to the conversation: auto-growing textarea, Enter
|
|
96
191
|
sends, Shift+Enter breaks the line, IME composition respected, 44px targets,
|
|
@@ -101,8 +196,8 @@ on `Composer`, `labels` on `ChatThread`.
|
|
|
101
196
|
jump-down button otherwise.
|
|
102
197
|
7. Markdown rendered, never as HTML; `javascript:` links stay text; Copy on
|
|
103
198
|
every answer.
|
|
104
|
-
8. Starters in the empty state;
|
|
105
|
-
than one agent speaks.
|
|
199
|
+
8. Starters in the empty state; suggested replies under the latest answer;
|
|
200
|
+
the speaker's name on every answer once more than one agent speaks.
|
|
106
201
|
9. Nothing overflows at 320–390px.
|
|
107
202
|
|
|
108
203
|
`pnpm run verify` proves all of it: unit and render tests, then
|
|
@@ -118,8 +213,9 @@ different chats. If your app needs something truly its own, it goes in a slot
|
|
|
118
213
|
(`tools`, `header`, `footer`, `renderFooter`, `renderLink`) — ask for a new slot
|
|
119
214
|
here rather than forking the component.
|
|
120
215
|
|
|
121
|
-
Release: bump `version` in `package.json
|
|
122
|
-
`
|
|
216
|
+
Release: bump `version` in `package.json` and merge. The merge publishes it
|
|
217
|
+
(`publish.yml` runs on main, skips a version already on npm, and tags the
|
|
218
|
+
commit `v<version>`). No manual tag.
|
|
123
219
|
|
|
124
220
|
## Develop
|
|
125
221
|
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Fails when a file input asks for more than one family of files at once.
|
|
3
|
+
//
|
|
4
|
+
// chatkit-check-file-inputs [dir ...] (default: src app components)
|
|
5
|
+
//
|
|
6
|
+
// A phone answers <input type="file" accept="image/*,.txt,audio/*"> with a
|
|
7
|
+
// chooser of capture apps — Camera, Recorder, "Photos & Videos" — and no file
|
|
8
|
+
// browser, so the screenshot the person came to send is out of reach. Found on
|
|
9
|
+
// Android on 2026-10-05 in loki, orangecat and evig at once: each app had
|
|
10
|
+
// written its own input. The fix is one input per source (AttachMenu from
|
|
11
|
+
// @bitbaum/chatkit/attach); this check keeps a new one from coming back.
|
|
12
|
+
//
|
|
13
|
+
// Comments are skipped. It reads literal accept values (accept="…", accept={"…"}, accept: "…",
|
|
14
|
+
// accept={`…`} without interpolation). A value built at runtime is not seen,
|
|
15
|
+
// so build mixed lists nowhere. A line, or the line above it, carrying
|
|
16
|
+
// `chatkit-allow-mixed-accept: <reason>` is skipped — say why.
|
|
17
|
+
import { readdirSync, readFileSync, statSync, existsSync } from "node:fs";
|
|
18
|
+
import { join, relative } from "node:path";
|
|
19
|
+
import { acceptFamilies } from "../dist/index.js";
|
|
20
|
+
|
|
21
|
+
const roots = process.argv.slice(2).filter((a) => !a.startsWith("-"));
|
|
22
|
+
const dirs = (roots.length ? roots : ["src", "app", "components"]).filter((d) => existsSync(d));
|
|
23
|
+
const SKIP = new Set(["node_modules", ".next", "dist", "build", "out", "coverage", ".git"]);
|
|
24
|
+
const EXT = /\.(tsx|jsx|ts|js|mjs)$/;
|
|
25
|
+
const ACCEPT = /accept\s*[=:]\s*\{?\s*(?:"([^"]*)"|'([^']*)'|`([^`$]*)`)/g;
|
|
26
|
+
|
|
27
|
+
function* walk(dir) {
|
|
28
|
+
for (const name of readdirSync(dir)) {
|
|
29
|
+
if (SKIP.has(name)) continue;
|
|
30
|
+
const path = join(dir, name);
|
|
31
|
+
const st = statSync(path);
|
|
32
|
+
if (st.isDirectory()) yield* walk(path);
|
|
33
|
+
else if (EXT.test(name) && !/\.(test|spec)\./.test(name)) yield path;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const problems = [];
|
|
38
|
+
let scanned = 0;
|
|
39
|
+
for (const dir of dirs) {
|
|
40
|
+
for (const file of walk(dir)) {
|
|
41
|
+
scanned++;
|
|
42
|
+
const lines = readFileSync(file, "utf8").split("\n");
|
|
43
|
+
let inBlockComment = false;
|
|
44
|
+
lines.forEach((line, i) => {
|
|
45
|
+
// Comments describe inputs; they are not inputs. A note recording the
|
|
46
|
+
// old bug ("used to be accept=\"image/*,text/*\"") must not fail CI.
|
|
47
|
+
const trimmed = line.trim();
|
|
48
|
+
if (inBlockComment) {
|
|
49
|
+
if (trimmed.includes("*/")) inBlockComment = false;
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (/^(\/\/|\*|\{\s*\/\*)/.test(trimmed)) return;
|
|
53
|
+
if (trimmed.startsWith("/*")) {
|
|
54
|
+
if (!trimmed.includes("*/")) inBlockComment = true;
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
// "/*" opens a comment only after space, "{" or "(" — inside an accept
|
|
58
|
+
// value it follows a letter ("image/*") and must survive.
|
|
59
|
+
const code = line.replace(/(^|[\s{(])\/\*.*?\*\//g, "$1").replace(/(^|\s)\/\/.*$/, "$1");
|
|
60
|
+
for (const m of code.matchAll(ACCEPT)) {
|
|
61
|
+
const value = m[1] ?? m[2] ?? m[3] ?? "";
|
|
62
|
+
const families = acceptFamilies(value);
|
|
63
|
+
if (families.length < 2) continue;
|
|
64
|
+
const allowed = [line, lines[i - 1] ?? ""].some((l) =>
|
|
65
|
+
l.includes("chatkit-allow-mixed-accept:"),
|
|
66
|
+
);
|
|
67
|
+
if (allowed) continue;
|
|
68
|
+
problems.push(
|
|
69
|
+
`${relative(process.cwd(), file)}:${i + 1} accept="${value}" mixes ${families.join(" + ")}`,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (problems.length) {
|
|
77
|
+
console.error(`✗ ${problems.length} file input(s) mix file families:\n`);
|
|
78
|
+
for (const p of problems) console.error(` ${p}`);
|
|
79
|
+
console.error(
|
|
80
|
+
"\n A phone answers a mixed accept with a chooser of capture apps and no file browser." +
|
|
81
|
+
"\n Use AttachMenu from @bitbaum/chatkit/attach (Camera / Photos / Files), or one input per family.",
|
|
82
|
+
);
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
console.log(
|
|
86
|
+
`✓ file inputs: none mixes file families (${scanned} files in ${dirs.join(", ") || "nothing"})`,
|
|
87
|
+
);
|
package/dist/attachments.d.ts
CHANGED
|
@@ -61,6 +61,43 @@ export declare function fitWithin(width: number, height: number, maxEdge: number
|
|
|
61
61
|
/** Bytes a base64 data URL decodes to — close enough to compare to a limit. */
|
|
62
62
|
export declare function dataUrlBytes(dataUrl: string): number;
|
|
63
63
|
export declare function isImageMime(mime: string): boolean;
|
|
64
|
+
/**
|
|
65
|
+
* Whether a file is text the model can read inline. The Files picker accepts
|
|
66
|
+
* anything — that is what keeps it the phone's real document browser instead
|
|
67
|
+
* of a camera chooser — so the decision about what can actually be sent moves
|
|
68
|
+
* here, after the pick, where a PDF gets a sentence instead of being read as
|
|
69
|
+
* binary noise and pasted into a prompt.
|
|
70
|
+
*/
|
|
71
|
+
export declare function isTextFile(name: string, mime: string): boolean;
|
|
72
|
+
/**
|
|
73
|
+
* Where an attachment comes from. Three sources, three inputs, because a phone
|
|
74
|
+
* answers ONE input with a mixed `accept` ("image/*,text/*,audio/*") by
|
|
75
|
+
* opening a chooser of capture apps — Camera, Camera, Recorder, "Photos &
|
|
76
|
+
* Videos" — in which the screenshot you came to send is three levels down,
|
|
77
|
+
* and the file browser is not offered at all. Measured on Android/Brave,
|
|
78
|
+
* 2026-10-05; it is what made attaching to Loki "impossible".
|
|
79
|
+
*
|
|
80
|
+
* - camera: `capture` opens the camera straight away.
|
|
81
|
+
* - photos: `image/*` alone opens the system photo picker (Screenshots is its
|
|
82
|
+
* first album), not a chooser.
|
|
83
|
+
* - files: no `accept` at all opens the real document browser — Downloads,
|
|
84
|
+
* Drive, any folder. The type check happens after, in `isTextFile`.
|
|
85
|
+
*/
|
|
86
|
+
export type AttachSource = "camera" | "photos" | "files";
|
|
87
|
+
export declare const ATTACH_SOURCE_INPUT: Record<AttachSource, {
|
|
88
|
+
accept?: string;
|
|
89
|
+
capture?: "environment";
|
|
90
|
+
}>;
|
|
91
|
+
/** What kind of picker a file `accept` asks a phone for. */
|
|
92
|
+
export type AcceptFamily = "image" | "audio" | "video" | "document";
|
|
93
|
+
/**
|
|
94
|
+
* The families an `accept` value spans. More than one is the bug
|
|
95
|
+
* `ATTACH_SOURCE_INPUT` exists to prevent: a phone answers a mixed accept with
|
|
96
|
+
* a chooser of capture apps instead of the picker the person needed. Pure, so
|
|
97
|
+
* the fleet's file-input check (`chatkit-check-file-inputs`) and the tests
|
|
98
|
+
* share one definition.
|
|
99
|
+
*/
|
|
100
|
+
export declare function acceptFamilies(accept: string): AcceptFamily[];
|
|
64
101
|
/** `data:image/png;base64,AAAA` → `AAAA`: FileReader hands back a data URL,
|
|
65
102
|
* the wire carries raw base64. */
|
|
66
103
|
export declare function stripDataUrlBase64(dataUrl: string): string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"attachments.d.ts","sourceRoot":"","sources":["../src/attachments.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,MAAM,cAAc,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAC7E,MAAM,MAAM,eAAe,GAAG;IAC5B,IAAI,EAAE,OAAO,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AACF,MAAM,MAAM,UAAU,GAAG,cAAc,GAAG,eAAe,CAAC;AAE1D,uDAAuD;AACvD,MAAM,MAAM,gBAAgB,GAAG,UAAU,GAAG;IAAE,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAEpE,MAAM,MAAM,gBAAgB,GAAG;IAC7B,yBAAyB;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,8DAA8D;IAC9D,YAAY,EAAE,MAAM,CAAC;IACrB,mEAAmE;IACnE,aAAa,EAAE,MAAM,CAAC;IACtB;;;;;;;;OAQG;IACH,YAAY,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,eAAO,MAAM,yBAAyB,EAAE,gBAKvC,CAAC;AAEF;yBACyB;AACzB,MAAM,MAAM,eAAe,GAAG;IAC5B,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IACpC,aAAa,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;IACvD,YAAY,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,KAAK,MAAM,CAAC;IACjE,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IACrC,OAAO,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAAC;CAClC,CAAC;AAEF,eAAO,MAAM,wBAAwB,EAAE,eAMtC,CAAC;AAEF;;;GAGG;AACH,wBAAgB,SAAS,CACvB,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,GACd;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,OAAO,CAAA;CAAE,CASpD;AAED,+EAA+E;AAC/E,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAEpD;AAID,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEjD;AAED;mCACmC;AACnC,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAG1D;AAED;6DAC6D;AAC7D,wBAAgB,aAAa,CAAC,CAAC,EAAE,UAAU,GAAG,MAAM,CAInD;AAED,wDAAwD;AACxD,wBAAgB,MAAM,CAAC,MAAM,EAAE,gBAAgB,EAAE,GAAG,UAAU,EAAE,CAM/D"}
|
|
1
|
+
{"version":3,"file":"attachments.d.ts","sourceRoot":"","sources":["../src/attachments.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,MAAM,cAAc,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAC7E,MAAM,MAAM,eAAe,GAAG;IAC5B,IAAI,EAAE,OAAO,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AACF,MAAM,MAAM,UAAU,GAAG,cAAc,GAAG,eAAe,CAAC;AAE1D,uDAAuD;AACvD,MAAM,MAAM,gBAAgB,GAAG,UAAU,GAAG;IAAE,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAEpE,MAAM,MAAM,gBAAgB,GAAG;IAC7B,yBAAyB;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,8DAA8D;IAC9D,YAAY,EAAE,MAAM,CAAC;IACrB,mEAAmE;IACnE,aAAa,EAAE,MAAM,CAAC;IACtB;;;;;;;;OAQG;IACH,YAAY,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,eAAO,MAAM,yBAAyB,EAAE,gBAKvC,CAAC;AAEF;yBACyB;AACzB,MAAM,MAAM,eAAe,GAAG;IAC5B,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IACpC,aAAa,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;IACvD,YAAY,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,KAAK,MAAM,CAAC;IACjE,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IACrC,OAAO,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAAC;CAClC,CAAC;AAEF,eAAO,MAAM,wBAAwB,EAAE,eAMtC,CAAC;AAEF;;;GAGG;AACH,wBAAgB,SAAS,CACvB,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,GACd;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,OAAO,CAAA;CAAE,CASpD;AAED,+EAA+E;AAC/E,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAEpD;AAID,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEjD;AAQD;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAI9D;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,YAAY,GAAG,QAAQ,GAAG,QAAQ,GAAG,OAAO,CAAC;AAEzD,eAAO,MAAM,mBAAmB,EAAE,MAAM,CACtC,YAAY,EACZ;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,aAAa,CAAA;CAAE,CAK7C,CAAC;AAEF,4DAA4D;AAC5D,MAAM,MAAM,YAAY,GAAG,OAAO,GAAG,OAAO,GAAG,OAAO,GAAG,UAAU,CAAC;AAyBpE;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,YAAY,EAAE,CAgB7D;AAED;mCACmC;AACnC,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAG1D;AAED;6DAC6D;AAC7D,wBAAgB,aAAa,CAAC,CAAC,EAAE,UAAU,GAAG,MAAM,CAInD;AAED,wDAAwD;AACxD,wBAAgB,MAAM,CAAC,MAAM,EAAE,gBAAgB,EAAE,GAAG,UAAU,EAAE,CAM/D"}
|
package/dist/attachments.js
CHANGED
|
@@ -11,7 +11,7 @@ export const DEFAULT_ATTACHMENT_LIMITS = {
|
|
|
11
11
|
maxImageEdge: 0,
|
|
12
12
|
};
|
|
13
13
|
export const DEFAULT_ATTACHMENT_NOTES = {
|
|
14
|
-
wrongType: (name) => `${name}:
|
|
14
|
+
wrongType: (name) => `${name}: attach an image (PNG, JPEG, GIF, WebP) or a text file.`,
|
|
15
15
|
imageTooLarge: (name, maxMb) => `${name} is too large (max ${maxMb} MB).`,
|
|
16
16
|
textTooLarge: (name, k) => `${name} is too large (max ${k}k characters).`,
|
|
17
17
|
unreadable: (name) => `Could not read ${name}.`,
|
|
@@ -40,6 +40,80 @@ const IMAGE_MIME = /^image\/(jpeg|jpg|png|gif|webp)$/i;
|
|
|
40
40
|
export function isImageMime(mime) {
|
|
41
41
|
return IMAGE_MIME.test(mime);
|
|
42
42
|
}
|
|
43
|
+
/** Extensions a picker may hand over with no type, or as
|
|
44
|
+
* application/octet-stream, that are still plain text a model can read. */
|
|
45
|
+
const TEXT_EXTENSIONS = /\.(txt|md|markdown|json|jsonl|csv|tsv|log|ya?ml|toml|ini|xml|html?|css|scss|js|jsx|mjs|cjs|ts|tsx|py|rb|go|rs|java|kt|swift|c|h|cpp|hpp|cs|php|sh|bash|zsh|sql|env|diff|patch|srt|vtt)$/i;
|
|
46
|
+
const TEXT_MIME = /^(text\/|application\/(json|xml|x-yaml|yaml|javascript|x-sh|sql))/i;
|
|
47
|
+
/**
|
|
48
|
+
* Whether a file is text the model can read inline. The Files picker accepts
|
|
49
|
+
* anything — that is what keeps it the phone's real document browser instead
|
|
50
|
+
* of a camera chooser — so the decision about what can actually be sent moves
|
|
51
|
+
* here, after the pick, where a PDF gets a sentence instead of being read as
|
|
52
|
+
* binary noise and pasted into a prompt.
|
|
53
|
+
*/
|
|
54
|
+
export function isTextFile(name, mime) {
|
|
55
|
+
if (TEXT_MIME.test(mime))
|
|
56
|
+
return true;
|
|
57
|
+
if (mime && mime !== "application/octet-stream")
|
|
58
|
+
return false;
|
|
59
|
+
return TEXT_EXTENSIONS.test(name);
|
|
60
|
+
}
|
|
61
|
+
export const ATTACH_SOURCE_INPUT = {
|
|
62
|
+
camera: { accept: "image/*", capture: "environment" },
|
|
63
|
+
photos: { accept: "image/*" },
|
|
64
|
+
files: {},
|
|
65
|
+
};
|
|
66
|
+
const FAMILY_BY_EXTENSION = {};
|
|
67
|
+
for (const ext of [
|
|
68
|
+
"png",
|
|
69
|
+
"jpg",
|
|
70
|
+
"jpeg",
|
|
71
|
+
"gif",
|
|
72
|
+
"webp",
|
|
73
|
+
"heic",
|
|
74
|
+
"heif",
|
|
75
|
+
"avif",
|
|
76
|
+
"bmp",
|
|
77
|
+
"svg",
|
|
78
|
+
"tif",
|
|
79
|
+
"tiff",
|
|
80
|
+
])
|
|
81
|
+
FAMILY_BY_EXTENSION[ext] = "image";
|
|
82
|
+
for (const ext of ["mp3", "m4a", "wav", "ogg", "oga", "opus", "aac", "flac", "amr", "wma"])
|
|
83
|
+
FAMILY_BY_EXTENSION[ext] = "audio";
|
|
84
|
+
for (const ext of ["mp4", "mov", "m4v", "mkv", "avi", "wmv"])
|
|
85
|
+
FAMILY_BY_EXTENSION[ext] = "video";
|
|
86
|
+
// Containers that hold either sound or picture (.webm, .3gp) say nothing about
|
|
87
|
+
// the picker on their own, so they are not counted for either family.
|
|
88
|
+
const AMBIGUOUS_EXTENSIONS = new Set(["webm", "3gp", "3g2", "mpeg", "mpg"]);
|
|
89
|
+
/**
|
|
90
|
+
* The families an `accept` value spans. More than one is the bug
|
|
91
|
+
* `ATTACH_SOURCE_INPUT` exists to prevent: a phone answers a mixed accept with
|
|
92
|
+
* a chooser of capture apps instead of the picker the person needed. Pure, so
|
|
93
|
+
* the fleet's file-input check (`chatkit-check-file-inputs`) and the tests
|
|
94
|
+
* share one definition.
|
|
95
|
+
*/
|
|
96
|
+
export function acceptFamilies(accept) {
|
|
97
|
+
const found = new Set();
|
|
98
|
+
for (const raw of accept.split(",")) {
|
|
99
|
+
const token = raw.trim().toLowerCase();
|
|
100
|
+
if (!token)
|
|
101
|
+
continue;
|
|
102
|
+
if (token.startsWith(".")) {
|
|
103
|
+
const ext = token.slice(1);
|
|
104
|
+
if (AMBIGUOUS_EXTENSIONS.has(ext))
|
|
105
|
+
continue;
|
|
106
|
+
found.add(FAMILY_BY_EXTENSION[ext] ?? "document");
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
const major = token.split("/")[0];
|
|
110
|
+
if (major === "image" || major === "audio" || major === "video")
|
|
111
|
+
found.add(major);
|
|
112
|
+
else
|
|
113
|
+
found.add("document");
|
|
114
|
+
}
|
|
115
|
+
return [...found];
|
|
116
|
+
}
|
|
43
117
|
/** `data:image/png;base64,AAAA` → `AAAA`: FileReader hands back a data URL,
|
|
44
118
|
* the wire carries raw base64. */
|
|
45
119
|
export function stripDataUrlBase64(dataUrl) {
|
package/dist/attachments.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"attachments.js","sourceRoot":"","sources":["../src/attachments.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAiCH,MAAM,CAAC,MAAM,yBAAyB,GAAqB;IACzD,QAAQ,EAAE,CAAC;IACX,YAAY,EAAE,OAAO;IACrB,aAAa,EAAE,SAAS;IACxB,YAAY,EAAE,CAAC;CAChB,CAAC;AAYF,MAAM,CAAC,MAAM,wBAAwB,GAAoB;IACvD,SAAS,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,GAAG,IAAI
|
|
1
|
+
{"version":3,"file":"attachments.js","sourceRoot":"","sources":["../src/attachments.ts"],"names":[],"mappings":"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"}
|
package/dist/composer.d.ts
CHANGED
|
@@ -15,13 +15,16 @@ export type ComposerMode = {
|
|
|
15
15
|
/** `false` keeps the draft (the send failed or was refused); anything else
|
|
16
16
|
* clears it. A failed send must be retryable from the same box, not retyped. */
|
|
17
17
|
export type ComposerSendResult = void | boolean;
|
|
18
|
-
export declare function composerCanSend({ text, attachmentCount, attachmentOnlyText, sending, disabled, blocked, }: {
|
|
18
|
+
export declare function composerCanSend({ text, attachmentCount, attachmentOnlyText, sending, disabled, blocked, queue, }: {
|
|
19
19
|
text: string;
|
|
20
20
|
attachmentCount: number;
|
|
21
21
|
attachmentOnlyText?: string;
|
|
22
22
|
sending: boolean;
|
|
23
23
|
disabled: boolean;
|
|
24
24
|
blocked: boolean;
|
|
25
|
+
/** The app takes a message while a turn runs and sends it after. With it,
|
|
26
|
+
* `sending` no longer refuses the send — it queues it. */
|
|
27
|
+
queue?: boolean;
|
|
25
28
|
}): boolean;
|
|
26
29
|
/** The words actually sent: the draft, or the app's stand-in for an
|
|
27
30
|
* attachments-only send. */
|
package/dist/composer.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"composer.d.ts","sourceRoot":"","sources":["../src/composer.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,mEAAmE;AACnE,MAAM,MAAM,YAAY,GAAG;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAExE;iFACiF;AACjF,MAAM,MAAM,kBAAkB,GAAG,IAAI,GAAG,OAAO,CAAC;AAEhD,wBAAgB,eAAe,CAAC,EAC9B,IAAI,EACJ,eAAe,EACf,kBAAkB,EAClB,OAAO,EACP,QAAQ,EACR,OAAO,
|
|
1
|
+
{"version":3,"file":"composer.d.ts","sourceRoot":"","sources":["../src/composer.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,mEAAmE;AACnE,MAAM,MAAM,YAAY,GAAG;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAExE;iFACiF;AACjF,MAAM,MAAM,kBAAkB,GAAG,IAAI,GAAG,OAAO,CAAC;AAEhD,wBAAgB,eAAe,CAAC,EAC9B,IAAI,EACJ,eAAe,EACf,kBAAkB,EAClB,OAAO,EACP,QAAQ,EACR,OAAO,EACP,KAAa,GACd,EAAE;IACD,IAAI,EAAE,MAAM,CAAC;IACb,eAAe,EAAE,MAAM,CAAC;IACxB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,OAAO,CAAC;IAClB,OAAO,EAAE,OAAO,CAAC;IACjB;+DAC2D;IAC3D,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB,GAAG,OAAO,CAOV;AAED;6BAC6B;AAC7B,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,MAAM,EACZ,eAAe,EAAE,MAAM,EACvB,kBAAkB,CAAC,EAAE,MAAM,GAC1B,MAAM,CAIR;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,kBAAkB,GAAG,OAAO,CAEpE;AAED,kFAAkF;AAClF,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAIpE;AAED,0CAA0C;AAC1C,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAGrD"}
|
package/dist/composer.js
CHANGED
|
@@ -6,8 +6,10 @@
|
|
|
6
6
|
* From loki `src/components/composer/composer-logic.ts`, where these were
|
|
7
7
|
* written after four composers in one app gave four different answers.
|
|
8
8
|
*/
|
|
9
|
-
export function composerCanSend({ text, attachmentCount, attachmentOnlyText, sending, disabled, blocked, }) {
|
|
10
|
-
if (
|
|
9
|
+
export function composerCanSend({ text, attachmentCount, attachmentOnlyText, sending, disabled, blocked, queue = false, }) {
|
|
10
|
+
if (disabled || blocked)
|
|
11
|
+
return false;
|
|
12
|
+
if (sending && !queue)
|
|
11
13
|
return false;
|
|
12
14
|
if (text.trim().length > 0)
|
|
13
15
|
return true;
|
package/dist/composer.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"composer.js","sourceRoot":"","sources":["../src/composer.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AASH,MAAM,UAAU,eAAe,CAAC,EAC9B,IAAI,EACJ,eAAe,EACf,kBAAkB,EAClB,OAAO,EACP,QAAQ,EACR,OAAO,
|
|
1
|
+
{"version":3,"file":"composer.js","sourceRoot":"","sources":["../src/composer.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AASH,MAAM,UAAU,eAAe,CAAC,EAC9B,IAAI,EACJ,eAAe,EACf,kBAAkB,EAClB,OAAO,EACP,QAAQ,EACR,OAAO,EACP,KAAK,GAAG,KAAK,GAWd;IACC,IAAI,QAAQ,IAAI,OAAO;QAAE,OAAO,KAAK,CAAC;IACtC,IAAI,OAAO,IAAI,CAAC,KAAK;QAAE,OAAO,KAAK,CAAC;IACpC,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IACxC,0EAA0E;IAC1E,4CAA4C;IAC5C,OAAO,eAAe,GAAG,CAAC,IAAI,OAAO,CAAC,kBAAkB,CAAC,CAAC;AAC5D,CAAC;AAED;6BAC6B;AAC7B,MAAM,UAAU,oBAAoB,CAClC,IAAY,EACZ,eAAuB,EACvB,kBAA2B;IAE3B,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;IAC5B,IAAI,OAAO;QAAE,OAAO,OAAO,CAAC;IAC5B,OAAO,eAAe,GAAG,CAAC,IAAI,kBAAkB,CAAC,CAAC,CAAC,kBAAkB,CAAC,CAAC,CAAC,EAAE,CAAC;AAC7E,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,MAA0B;IACzD,OAAO,MAAM,KAAK,KAAK,CAAC;AAC1B,CAAC;AAED,kFAAkF;AAClF,MAAM,UAAU,gBAAgB,CAAC,KAAa,EAAE,IAAY;IAC1D,MAAM,CAAC,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;IACtB,IAAI,CAAC,CAAC;QAAE,OAAO,KAAK,CAAC;IACrB,OAAO,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;AAChE,CAAC;AAED,0CAA0C;AAC1C,MAAM,UAAU,aAAa,CAAC,OAAe;IAC3C,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC;IAC3C,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,CAAC,IAAI,MAAM,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC;AACpE,CAAC"}
|
package/dist/dictation.d.ts
CHANGED
|
@@ -16,8 +16,10 @@
|
|
|
16
16
|
* Kept pure so "never forever" and "which failures the fallback rescues" are
|
|
17
17
|
* tested properties rather than behaviour buried in a timeout.
|
|
18
18
|
*/
|
|
19
|
-
/** Why a dictation produced no text, in terms the person can act on.
|
|
20
|
-
|
|
19
|
+
/** Why a dictation produced no text, in terms the person can act on.
|
|
20
|
+
* `fileTooLarge` is the fourth word, for a recording handed over as a file
|
|
21
|
+
* (see `isAudioFile`) that the server leg would refuse. */
|
|
22
|
+
export type DictationProblem = "mic" | "silence" | "unavailable" | "fileTooLarge";
|
|
21
23
|
/** Which path to try first. `browser` is free and instant; `server` is one
|
|
22
24
|
* model everywhere (and detects the language by itself). Either falls back to
|
|
23
25
|
* the other when it cannot work. */
|
|
@@ -36,6 +38,14 @@ export declare function problemFor(error: string | undefined): DictationProblem
|
|
|
36
38
|
/** getUserMedia / MediaRecorder failures → the same three words, so the UI
|
|
37
39
|
* never grows a second vocabulary for the same situations. */
|
|
38
40
|
export declare function problemForRecording(error: unknown): DictationProblem;
|
|
41
|
+
/**
|
|
42
|
+
* The words on screen while a take is live: everything the recogniser has
|
|
43
|
+
* settled on, then what it is still guessing at — one line, read as it is
|
|
44
|
+
* spoken. A preview only: when the server leg transcribes, ITS words are
|
|
45
|
+
* what lands in the box; these are what the person watches meanwhile, so a
|
|
46
|
+
* take is never a timer and a wave with nothing to show for twenty seconds.
|
|
47
|
+
*/
|
|
48
|
+
export declare function liveWords(finals: readonly string[], interim: string): string;
|
|
39
49
|
/** A working recogniser fires `start` well inside this once the mic is allowed. */
|
|
40
50
|
export declare const START_TIMEOUT_MS = 4000;
|
|
41
51
|
/**
|
|
@@ -55,6 +65,56 @@ export declare function deadRecogniserStillTrusted(rememberedAt: number | null,
|
|
|
55
65
|
export declare const MAX_RECORDING_MS = 120000;
|
|
56
66
|
/** Candidate containers for MediaRecorder, best first. */
|
|
57
67
|
export declare const RECORDING_MIME_CANDIDATES: readonly ["audio/webm;codecs=opus", "audio/webm", "audio/mp4", "audio/ogg"];
|
|
68
|
+
/**
|
|
69
|
+
* A recording that already exists — a voice memo from the phone's recorder, a
|
|
70
|
+
* meeting someone taped — is dictation that happened earlier. It goes to the
|
|
71
|
+
* same server leg as the mic and its words land in the box the same way.
|
|
72
|
+
*
|
|
73
|
+
* The cap is on the upload, not the recording: the server compresses and
|
|
74
|
+
* splits long audio before Whisper, but a phone on mobile data still has to
|
|
75
|
+
* send the bytes. A one-hour memo from a phone recorder is about 60 MB; this
|
|
76
|
+
* takes that with room to spare, and refuses the ten-hour one in words rather
|
|
77
|
+
* than after a ten-minute upload that fails.
|
|
78
|
+
*/
|
|
79
|
+
export declare const MAX_AUDIO_FILE_BYTES: number;
|
|
80
|
+
/**
|
|
81
|
+
* Does this file hold a recording? By MIME where the browser gives one, by
|
|
82
|
+
* extension where it does not: Android's picker hands over `.m4a` from the
|
|
83
|
+
* stock recorder with an empty type often enough that mime alone would call
|
|
84
|
+
* it a text file and inline the bytes into a prompt.
|
|
85
|
+
*/
|
|
86
|
+
export declare function isAudioFile(file: {
|
|
87
|
+
type?: string;
|
|
88
|
+
name?: string;
|
|
89
|
+
}): boolean;
|
|
90
|
+
/** The name a recording travels under. Whisper services read the container
|
|
91
|
+
* from the extension, so a mic take keeps the type's extension and a file
|
|
92
|
+
* keeps its own name. */
|
|
93
|
+
export declare function audioFileName(audio: {
|
|
94
|
+
type?: string;
|
|
95
|
+
name?: string;
|
|
96
|
+
}): string;
|
|
97
|
+
/**
|
|
98
|
+
* A server leg that refused, with the server's own words when it gave any.
|
|
99
|
+
*
|
|
100
|
+
* A failed transcription used to reach the person as one sentence whatever
|
|
101
|
+
* went wrong — and in OrangeCat as "check your connection", while the server
|
|
102
|
+
* had answered plainly that it was busy (2026-10-07). `reason` is what the
|
|
103
|
+
* server said, for the UI to show beside the generic line; a custom
|
|
104
|
+
* `transcribe` may throw one too.
|
|
105
|
+
*/
|
|
106
|
+
export declare class TranscriptionError extends Error {
|
|
107
|
+
readonly reason: string | null;
|
|
108
|
+
readonly status: number | null;
|
|
109
|
+
constructor(reason: string | null, status?: number | null);
|
|
110
|
+
}
|
|
111
|
+
/** The server's sentence from a failed transcription, if it sent one. Reads
|
|
112
|
+
* the shapes servers actually answer with: `{ error }`, `{ message }`,
|
|
113
|
+
* `{ error: { message } }`. Capped, because it is shown in a status line. */
|
|
114
|
+
export declare function reasonFromBody(body: unknown): string | null;
|
|
115
|
+
/** What a thrown transcription error says, for any thrower: ours, or an
|
|
116
|
+
* app's `transcribe` that sets `reason` on whatever it throws. */
|
|
117
|
+
export declare function reasonOf(error: unknown): string | null;
|
|
58
118
|
/** The default words for each problem. Override per app (and per language). */
|
|
59
119
|
export declare const DEFAULT_DICTATION_MESSAGES: Record<DictationProblem, string>;
|
|
60
120
|
//# sourceMappingURL=dictation.d.ts.map
|
package/dist/dictation.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dictation.d.ts","sourceRoot":"","sources":["../src/dictation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH
|
|
1
|
+
{"version":3,"file":"dictation.d.ts","sourceRoot":"","sources":["../src/dictation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH;;4DAE4D;AAC5D,MAAM,MAAM,gBAAgB,GAAG,KAAK,GAAG,SAAS,GAAG,aAAa,GAAG,cAAc,CAAC;AAElF;;qCAEqC;AACrC,MAAM,MAAM,mBAAmB,GAAG,SAAS,GAAG,QAAQ,CAAC;AAEvD;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,gBAAgB,GAAG,IAAI,GAAG,OAAO,CAE3E;AAED;gFACgF;AAChF,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,gBAAgB,GAAG,IAAI,CAa7E;AAED;+DAC+D;AAC/D,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,gBAAgB,CAKpE;AAED;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAK5E;AAED,mFAAmF;AACnF,eAAO,MAAM,gBAAgB,OAAO,CAAC;AAErC;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,QAAS,CAAC;AAEzC,wBAAgB,2BAA2B,CACzC,SAAS,EAAE,MAAM,EACjB,iBAAiB,EAAE,OAAO,GACzB,OAAO,CAET;AAED;;uCAEuC;AACvC,eAAO,MAAM,sBAAsB,QAA2B,CAAC;AAE/D,wBAAgB,0BAA0B,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,EAAE,GAAG,SAAa,GAAG,OAAO,CAKjG;AAED,uEAAuE;AACvE,eAAO,MAAM,gBAAgB,SAAU,CAAC;AAExC,0DAA0D;AAC1D,eAAO,MAAM,yBAAyB,6EAK5B,CAAC;AAEX;;;;;;;;;;GAUG;AACH,eAAO,MAAM,oBAAoB,QAAoB,CAAC;AAItD;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAK3E;AAED;;0BAE0B;AAC1B,wBAAgB,aAAa,CAAC,KAAK,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAc7E;AAED;;;;;;;;GAQG;AACH,qBAAa,kBAAmB,SAAQ,KAAK;IAC3C,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;gBACnB,MAAM,EAAE,MAAM,GAAG,IAAI,EAAE,MAAM,GAAE,MAAM,GAAG,IAAW;CAMhE;AAED;;8EAE8E;AAC9E,wBAAgB,cAAc,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAe3D;AAED;mEACmE;AACnE,wBAAgB,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAGtD;AAED,+EAA+E;AAC/E,eAAO,MAAM,0BAA0B,EAAE,MAAM,CAAC,gBAAgB,EAAE,MAAM,CAKvE,CAAC"}
|