@bitbaum/chatkit 0.8.2 → 0.10.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 (51) hide show
  1. package/README.md +55 -2
  2. package/dist/index.d.ts +1 -0
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +1 -0
  5. package/dist/index.js.map +1 -1
  6. package/dist/markdown.d.ts +39 -5
  7. package/dist/markdown.d.ts.map +1 -1
  8. package/dist/markdown.js +108 -11
  9. package/dist/markdown.js.map +1 -1
  10. package/dist/react/AnswerActions.d.ts +39 -0
  11. package/dist/react/AnswerActions.d.ts.map +1 -0
  12. package/dist/react/AnswerActions.js +49 -0
  13. package/dist/react/AnswerActions.js.map +1 -0
  14. package/dist/react/AttachMenu.d.ts +11 -2
  15. package/dist/react/AttachMenu.d.ts.map +1 -1
  16. package/dist/react/AttachMenu.js +21 -24
  17. package/dist/react/AttachMenu.js.map +1 -1
  18. package/dist/react/Markdown.d.ts +53 -4
  19. package/dist/react/Markdown.d.ts.map +1 -1
  20. package/dist/react/Markdown.js +92 -28
  21. package/dist/react/Markdown.js.map +1 -1
  22. package/dist/react/Sheet.d.ts +20 -0
  23. package/dist/react/Sheet.d.ts.map +1 -0
  24. package/dist/react/Sheet.js +37 -0
  25. package/dist/react/Sheet.js.map +1 -0
  26. package/dist/react/Thread.d.ts +19 -2
  27. package/dist/react/Thread.d.ts.map +1 -1
  28. package/dist/react/Thread.js +10 -8
  29. package/dist/react/Thread.js.map +1 -1
  30. package/dist/react/icons.d.ts +3 -0
  31. package/dist/react/icons.d.ts.map +1 -1
  32. package/dist/react/icons.js +3 -0
  33. package/dist/react/icons.js.map +1 -1
  34. package/dist/react/index.d.ts +7 -2
  35. package/dist/react/index.d.ts.map +1 -1
  36. package/dist/react/index.js +4 -1
  37. package/dist/react/index.js.map +1 -1
  38. package/dist/react/use-speech.d.ts +25 -0
  39. package/dist/react/use-speech.d.ts.map +1 -0
  40. package/dist/react/use-speech.js +66 -0
  41. package/dist/react/use-speech.js.map +1 -0
  42. package/dist/replies.d.ts +6 -3
  43. package/dist/replies.d.ts.map +1 -1
  44. package/dist/replies.js +8 -5
  45. package/dist/replies.js.map +1 -1
  46. package/dist/speech.d.ts +17 -0
  47. package/dist/speech.d.ts.map +1 -0
  48. package/dist/speech.js +81 -0
  49. package/dist/speech.js.map +1 -0
  50. package/package.json +1 -1
  51. package/styles.css +288 -0
@@ -12,8 +12,13 @@ export type { ComposerProps, ComposerLabels, ComposerLabelOverrides, ComposerVoi
12
12
  export { ChatThread, ChatMessage, ChatReplies, ChatStarters, DEFAULT_THREAD_LABELS, } from "./Thread.js";
13
13
  export type { ChatMessageData, ChatSpeaker, LiveTurn, Starter, ThreadLabels } from "./Thread.js";
14
14
  export { ChatScreen } from "./Screen.js";
15
- export { Markdown, defaultRenderLink } from "./Markdown.js";
16
- export type { CitationMap, RenderLink } from "./Markdown.js";
15
+ export { Markdown, SourcesRow, defaultRenderLink, defaultRenderCitation } from "./Markdown.js";
16
+ export { AnswerActions, DEFAULT_ANSWER_ACTIONS_LABELS } from "./AnswerActions.js";
17
+ export type { AnswerActionsLabels } from "./AnswerActions.js";
18
+ export { useSpeech } from "./use-speech.js";
19
+ export type { SpeechController } from "./use-speech.js";
20
+ export { Sheet, prefersSheet } from "./Sheet.js";
21
+ export type { CitationMap, RenderLink, RenderCitation, RenderSources } from "./Markdown.js";
17
22
  export { useDictation } from "./use-dictation.js";
18
23
  export type { DictationController, DictationStatus, UseDictationOptions } from "./use-dictation.js";
19
24
  export { AttachMenu, DEFAULT_ATTACH_MENU_LABELS } from "./AttachMenu.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAE,QAAQ,EAAE,uBAAuB,EAAE,MAAM,eAAe,CAAC;AAClE,YAAY,EACV,aAAa,EACb,cAAc,EACd,sBAAsB,EACtB,aAAa,GACd,MAAM,eAAe,CAAC;AACvB,OAAO,EACL,UAAU,EACV,WAAW,EACX,WAAW,EACX,YAAY,EACZ,qBAAqB,GACtB,MAAM,aAAa,CAAC;AACrB,YAAY,EAAE,eAAe,EAAE,WAAW,EAAE,QAAQ,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AACjG,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,QAAQ,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAC5D,YAAY,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAC7D,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,YAAY,EAAE,mBAAmB,EAAE,eAAe,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AACpG,OAAO,EAAE,UAAU,EAAE,0BAA0B,EAAE,MAAM,iBAAiB,CAAC;AACzE,YAAY,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACxD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,YAAY,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAClE,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAE,QAAQ,EAAE,uBAAuB,EAAE,MAAM,eAAe,CAAC;AAClE,YAAY,EACV,aAAa,EACb,cAAc,EACd,sBAAsB,EACtB,aAAa,GACd,MAAM,eAAe,CAAC;AACvB,OAAO,EACL,UAAU,EACV,WAAW,EACX,WAAW,EACX,YAAY,EACZ,qBAAqB,GACtB,MAAM,aAAa,CAAC;AACrB,YAAY,EAAE,eAAe,EAAE,WAAW,EAAE,QAAQ,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AACjG,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,iBAAiB,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;AAC/F,OAAO,EAAE,aAAa,EAAE,6BAA6B,EAAE,MAAM,oBAAoB,CAAC;AAClF,YAAY,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAC9D,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,YAAY,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACxD,OAAO,EAAE,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AACjD,YAAY,EAAE,WAAW,EAAE,UAAU,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC5F,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,YAAY,EAAE,mBAAmB,EAAE,eAAe,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AACpG,OAAO,EAAE,UAAU,EAAE,0BAA0B,EAAE,MAAM,iBAAiB,CAAC;AACzE,YAAY,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACxD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,YAAY,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAClE,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC"}
@@ -10,7 +10,10 @@
10
10
  export { Composer, DEFAULT_COMPOSER_LABELS } from "./Composer.js";
11
11
  export { ChatThread, ChatMessage, ChatReplies, ChatStarters, DEFAULT_THREAD_LABELS, } from "./Thread.js";
12
12
  export { ChatScreen } from "./Screen.js";
13
- export { Markdown, defaultRenderLink } from "./Markdown.js";
13
+ export { Markdown, SourcesRow, defaultRenderLink, defaultRenderCitation } from "./Markdown.js";
14
+ export { AnswerActions, DEFAULT_ANSWER_ACTIONS_LABELS } from "./AnswerActions.js";
15
+ export { useSpeech } from "./use-speech.js";
16
+ export { Sheet, prefersSheet } from "./Sheet.js";
14
17
  export { useDictation } from "./use-dictation.js";
15
18
  export { AttachMenu, DEFAULT_ATTACH_MENU_LABELS } from "./AttachMenu.js";
16
19
  export { useAttachments } from "./use-attachments.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAE,QAAQ,EAAE,uBAAuB,EAAE,MAAM,eAAe,CAAC;AAOlE,OAAO,EACL,UAAU,EACV,WAAW,EACX,WAAW,EACX,YAAY,EACZ,qBAAqB,GACtB,MAAM,aAAa,CAAC;AAErB,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,QAAQ,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAE5D,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAElD,OAAO,EAAE,UAAU,EAAE,0BAA0B,EAAE,MAAM,iBAAiB,CAAC;AAEzE,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAEtD,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAE,QAAQ,EAAE,uBAAuB,EAAE,MAAM,eAAe,CAAC;AAOlE,OAAO,EACL,UAAU,EACV,WAAW,EACX,WAAW,EACX,YAAY,EACZ,qBAAqB,GACtB,MAAM,aAAa,CAAC;AAErB,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,iBAAiB,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;AAC/F,OAAO,EAAE,aAAa,EAAE,6BAA6B,EAAE,MAAM,oBAAoB,CAAC;AAElF,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAE5C,OAAO,EAAE,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAEjD,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAElD,OAAO,EAAE,UAAU,EAAE,0BAA0B,EAAE,MAAM,iBAAiB,CAAC;AAEzE,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAEtD,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC"}
@@ -0,0 +1,25 @@
1
+ import { type Speaker } from "../speech.js";
2
+ export type SpeechController = {
3
+ /** Something can speak here: the app's speaker, or the browser's voice. */
4
+ supported: boolean;
5
+ speaking: boolean;
6
+ /** Seconds since this reading started; 0 while silent. */
7
+ elapsed: number;
8
+ /** Read this markdown aloud (flattened to prose first), stopping any other. */
9
+ speak: (markdown: string) => void;
10
+ stop: () => void;
11
+ toggle: (markdown: string) => void;
12
+ };
13
+ /**
14
+ * Read an answer aloud — the play button under every answer in the reference
15
+ * apps. The browser's own voices by default: no upload, no key, works
16
+ * offline. An app with a studio voice passes its own `speaker` (loki's
17
+ * Orpheus voice, through the same two methods) and the button, the timer and
18
+ * the stop are the same.
19
+ *
20
+ * One reading at a time: play on a second answer stops the first. Unmounting
21
+ * stops it too — a voice that keeps reading a page you left is the
22
+ * microphone bug's twin.
23
+ */
24
+ export declare function useSpeech(speaker?: Speaker | null): SpeechController;
25
+ //# sourceMappingURL=use-speech.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-speech.d.ts","sourceRoot":"","sources":["../../src/react/use-speech.ts"],"names":[],"mappings":"AAGA,OAAO,EAA0C,KAAK,OAAO,EAAE,MAAM,cAAc,CAAC;AAIpF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,2EAA2E;IAC3E,SAAS,EAAE,OAAO,CAAC;IACnB,QAAQ,EAAE,OAAO,CAAC;IAClB,0DAA0D;IAC1D,OAAO,EAAE,MAAM,CAAC;IAChB,+EAA+E;IAC/E,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,IAAI,EAAE,MAAM,IAAI,CAAC;IACjB,MAAM,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;CACpC,CAAC;AAEF;;;;;;;;;;GAUG;AACH,wBAAgB,SAAS,CAAC,OAAO,CAAC,EAAE,OAAO,GAAG,IAAI,GAAG,gBAAgB,CA4DpE"}
@@ -0,0 +1,66 @@
1
+ "use client";
2
+ import { useCallback, useEffect, useRef, useState, useSyncExternalStore } from "react";
3
+ import { createSynthSpeaker, plainTextForSpeech } from "../speech.js";
4
+ const noSubscribe = () => () => { };
5
+ /**
6
+ * Read an answer aloud — the play button under every answer in the reference
7
+ * apps. The browser's own voices by default: no upload, no key, works
8
+ * offline. An app with a studio voice passes its own `speaker` (loki's
9
+ * Orpheus voice, through the same two methods) and the button, the timer and
10
+ * the stop are the same.
11
+ *
12
+ * One reading at a time: play on a second answer stops the first. Unmounting
13
+ * stops it too — a voice that keeps reading a page you left is the
14
+ * microphone bug's twin.
15
+ */
16
+ export function useSpeech(speaker) {
17
+ const synthSupported = useSyncExternalStore(noSubscribe, () => typeof window !== "undefined" && "speechSynthesis" in window, () => false);
18
+ const supported = Boolean(speaker) || synthSupported;
19
+ const [speaking, setSpeaking] = useState(false);
20
+ const [elapsed, setElapsed] = useState(0);
21
+ const synth = useRef(null);
22
+ const active = useRef(null);
23
+ const token = useRef(0);
24
+ const stop = useCallback(() => {
25
+ token.current++;
26
+ active.current?.cancel();
27
+ active.current = null;
28
+ setSpeaking(false);
29
+ setElapsed(0);
30
+ }, []);
31
+ const speak = useCallback((markdown) => {
32
+ const who = speaker ?? (synth.current ??= createSynthSpeaker());
33
+ if (!who)
34
+ return;
35
+ const text = plainTextForSpeech(markdown);
36
+ if (!text)
37
+ return;
38
+ stop();
39
+ const my = ++token.current;
40
+ active.current = who;
41
+ setSpeaking(true);
42
+ void who.speak(text).then(() => {
43
+ if (my === token.current) {
44
+ active.current = null;
45
+ setSpeaking(false);
46
+ setElapsed(0);
47
+ }
48
+ });
49
+ }, [speaker, stop]);
50
+ const toggle = useCallback((markdown) => {
51
+ if (speaking)
52
+ stop();
53
+ else
54
+ speak(markdown);
55
+ }, [speak, speaking, stop]);
56
+ useEffect(() => {
57
+ if (!speaking)
58
+ return;
59
+ const started = Date.now();
60
+ const t = setInterval(() => setElapsed((Date.now() - started) / 1000), 500);
61
+ return () => clearInterval(t);
62
+ }, [speaking]);
63
+ useEffect(() => () => stop(), [stop]);
64
+ return { supported, speaking, elapsed, speak, stop, toggle };
65
+ }
66
+ //# sourceMappingURL=use-speech.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-speech.js","sourceRoot":"","sources":["../../src/react/use-speech.ts"],"names":[],"mappings":"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"}
package/dist/replies.d.ts CHANGED
@@ -24,9 +24,12 @@ export type RepliesOptions = {
24
24
  maxLength?: number;
25
25
  };
26
26
  /**
27
- * Append to a system prompt. Written so a model leaves the block off when no
28
- * reply is likely — a button row on every answer is noise, and noise is what
29
- * makes people stop reading buttons.
27
+ * Append to a system prompt. The model leads: every answer ends with the two
28
+ * to four things the person is most likely to say next, the first being the
29
+ * recommended next step. That is what makes a chat on a phone usable with a
30
+ * thumb — the answer is read, the next move is one tap (owner, 2026-10-21:
31
+ * "AI helps us to answer and lead the conversation"). Only an answer that
32
+ * ends the conversation leaves the block off.
30
33
  */
31
34
  export declare const REPLIES_INSTRUCTION: string;
32
35
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"replies.d.ts","sourceRoot":"","sources":["../src/replies.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,eAAO,MAAM,aAAa,kBAAkB,CAAC;AAE7C,MAAM,MAAM,cAAc,GAAG;IAC3B,4CAA4C;IAC5C,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,gEAAgE;IAChE,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,QAOpB,CAAC;AA+Bb;;;;GAIG;AACH,wBAAgB,cAAc,CAC5B,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE,cAAmB,GAC3B;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAA;CAAE,CAarC"}
1
+ {"version":3,"file":"replies.d.ts","sourceRoot":"","sources":["../src/replies.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,eAAO,MAAM,aAAa,kBAAkB,CAAC;AAE7C,MAAM,MAAM,cAAc,GAAG;IAC3B,4CAA4C;IAC5C,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,gEAAgE;IAChE,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,mBAAmB,QAOpB,CAAC;AA+Bb;;;;GAIG;AACH,wBAAgB,cAAc,CAC5B,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE,cAAmB,GAC3B;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAA;CAAE,CAarC"}
package/dist/replies.js CHANGED
@@ -18,17 +18,20 @@
18
18
  */
19
19
  export const REPLIES_FENCE = "quick_replies";
20
20
  /**
21
- * Append to a system prompt. Written so a model leaves the block off when no
22
- * reply is likely — a button row on every answer is noise, and noise is what
23
- * makes people stop reading buttons.
21
+ * Append to a system prompt. The model leads: every answer ends with the two
22
+ * to four things the person is most likely to say next, the first being the
23
+ * recommended next step. That is what makes a chat on a phone usable with a
24
+ * thumb — the answer is read, the next move is one tap (owner, 2026-10-21:
25
+ * "AI helps us to answer and lead the conversation"). Only an answer that
26
+ * ends the conversation leaves the block off.
24
27
  */
25
28
  export const REPLIES_INSTRUCTION = [
26
29
  "## Suggested replies",
27
- "When the person's next message is predictable — you asked a question, offered a choice, or there is an obvious next step — end your answer with a fenced block of 2–4 short replies they could tap instead of typing:",
30
+ "End every answer with a fenced block of 2–4 short replies the person could tap instead of typing. Put the most useful next step FIRST — the one you would recommend — then alternatives:",
28
31
  "```" + REPLIES_FENCE,
29
32
  '["Yes, do it", "Show me first", "Not now"]',
30
33
  "```",
31
- "Rules: write them in the person's language, in their voice (what THEY would say, not what you would). Each under 40 characters. Each must make sense sent on its own. Never repeat what they already said. Put the block last, after everything else. Leave it out entirely when no reply is likely, or when the conversation is over.",
34
+ "Rules: write them in the person's language, in their voice (what THEY would say, not what you would). Each under 40 characters. Each must make sense sent on its own. Never repeat what they already said. Put the block last, after everything else. Leave it out only when the conversation is over (a goodbye, a thanks with nothing left to do).",
32
35
  ].join("\n");
33
36
  const COMPLETE = new RegExp("\\n?```" + REPLIES_FENCE + "\\s*([\\s\\S]*?)```", "i");
34
37
  // Still streaming: the fence has opened (or is opening) and not yet closed.
@@ -1 +1 @@
1
- {"version":3,"file":"replies.js","sourceRoot":"","sources":["../src/replies.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,MAAM,CAAC,MAAM,aAAa,GAAG,eAAe,CAAC;AAS7C;;;;GAIG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG;IACjC,sBAAsB;IACtB,uNAAuN;IACvN,KAAK,GAAG,aAAa;IACrB,4CAA4C;IAC5C,KAAK;IACL,wUAAwU;CACzU,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAEb,MAAM,QAAQ,GAAG,IAAI,MAAM,CAAC,SAAS,GAAG,aAAa,GAAG,qBAAqB,EAAE,GAAG,CAAC,CAAC;AACpF,4EAA4E;AAC5E,MAAM,SAAS,GAAG,IAAI,MAAM,CAAC,YAAY,GAAG,aAAa,GAAG,uBAAuB,EAAE,GAAG,CAAC,CAAC;AAE1F,SAAS,KAAK,CAAC,GAAW,EAAE,EAAE,GAAG,GAAG,CAAC,EAAE,SAAS,GAAG,EAAE,EAAkB;IACrE,IAAI,MAAe,CAAC;IACpB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC;IAClC,CAAC;IAAC,MAAM,CAAC;QACP,uEAAuE;QACvE,MAAM,GAAG,GAAG;aACT,KAAK,CAAC,OAAO,CAAC;aACd,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,0BAA0B,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,gBAAgB,EAAE,EAAE,CAAC,CAAC,CAAC;IACzF,CAAC;IACD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC;QAAE,OAAO,EAAE,CAAC;IACtC,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,MAAM,GAAG,GAAa,EAAE,CAAC;IACzB,KAAK,MAAM,IAAI,IAAI,MAAM,EAAE,CAAC;QAC1B,IAAI,OAAO,IAAI,KAAK,QAAQ;YAAE,SAAS;QACvC,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;QAC1B,MAAM,GAAG,GAAG,KAAK,CAAC,WAAW,EAAE,CAAC;QAChC,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC,MAAM,GAAG,SAAS,IAAI,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;YAAE,SAAS;QAClE,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACd,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAChB,IAAI,GAAG,CAAC,MAAM,IAAI,GAAG;YAAE,MAAM;IAC/B,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAC5B,IAAY,EACZ,UAA0B,EAAE;IAE5B,MAAM,KAAK,GAAG,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAClC,IAAI,KAAK,EAAE,CAAC;QACV,MAAM,IAAI,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC;QAChG,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,EAAE,OAAO,CAAC,EAAE,CAAC;IACjE,CAAC;IACD,MAAM,IAAI,GAAG,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAClC,yEAAyE;IACzE,4CAA4C;IAC5C,IAAI,IAAI,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,KAAK,KAAK,EAAE,CAAC;QACrC,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,OAAO,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC;IACpE,CAAC;IACD,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC;AAC/B,CAAC"}
1
+ {"version":3,"file":"replies.js","sourceRoot":"","sources":["../src/replies.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ export declare function plainTextForSpeech(markdown: string): string;
2
+ /**
3
+ * Which voice language to ask for. The fleet's operators write mixed German
4
+ * and English; a German sentence is detectable cheaply enough by its most
5
+ * common function words to pick the voice, and the page's language otherwise.
6
+ */
7
+ export declare function guessSpeechLang(text: string, fallback?: string): string;
8
+ /** Who reads the words. The browser's own voice by default (`useSpeech`); an
9
+ * app with a studio voice hands in its own with the same two methods. */
10
+ export type Speaker = {
11
+ /** Resolves when the text has been read, or cut off. Never rejects. */
12
+ speak(text: string): Promise<void>;
13
+ cancel(): void;
14
+ };
15
+ /** The browser's own voice: free, offline, no key. `null` where there is none. */
16
+ export declare function createSynthSpeaker(): Speaker | null;
17
+ //# sourceMappingURL=speech.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"speech.d.ts","sourceRoot":"","sources":["../src/speech.ts"],"names":[],"mappings":"AAUA,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CA8B3D;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,SAAO,GAAG,MAAM,CAWrE;AAED;0EAC0E;AAC1E,MAAM,MAAM,OAAO,GAAG;IACpB,uEAAuE;IACvE,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnC,MAAM,IAAI,IAAI,CAAC;CAChB,CAAC;AAEF,kFAAkF;AAClF,wBAAgB,kBAAkB,IAAI,OAAO,GAAG,IAAI,CAuBnD"}
package/dist/speech.js ADDED
@@ -0,0 +1,81 @@
1
+ /**
2
+ * An answer as it should be READ ALOUD.
3
+ *
4
+ * Markdown read literally is noise — "hash hash Summary, asterisk asterisk",
5
+ * a citation "F eight" after every claim — so the text is flattened first.
6
+ * From loki's `speech-text`, where the play button under every answer first
7
+ * lived. Pure, so it is tested without a voice.
8
+ */
9
+ import { extractReplies } from "./replies.js";
10
+ export function plainTextForSpeech(markdown) {
11
+ // Suggested replies are buttons, never words: a quick_replies block is
12
+ // dropped outright rather than announced as "(code)".
13
+ const prose = extractReplies(markdown).text;
14
+ return (prose
15
+ // Fenced code is not prose; say that it is there rather than reading it.
16
+ .replace(/```[\s\S]*?```/g, " (code) ")
17
+ .replace(/`([^`]*)`/g, "$1")
18
+ // Citations like [F8] or [D1] are for the eye.
19
+ .replace(/\[(?:[A-Z]{1,2}\d{1,3}(?:,\s*[A-Z]{1,2}\d{1,3})*)\]/g, "")
20
+ // Links and images: keep the words, drop the URL.
21
+ .replace(/!\[[^\]]*\]\([^)]*\)/g, "")
22
+ .replace(/\[([^\]]+)\]\([^)]*\)/g, "$1")
23
+ .replace(/https?:\/\/\S+/g, "")
24
+ // Headings, emphasis, quotes, bullets, tasks, rules, tables.
25
+ .replace(/^\s{0,3}#{1,6}\s+/gm, "")
26
+ .replace(/^\s*>\s?/gm, "")
27
+ .replace(/^\s*[-*+]\s+(?:\[[ xX]\]\s+)?/gm, "")
28
+ .replace(/^\s*\d+[.)]\s+/gm, "")
29
+ .replace(/^\s*[-*_]{3,}\s*$/gm, "")
30
+ .replace(/^\s*\|?\s*:?-{2,}:?\s*(\|\s*:?-{2,}:?\s*)*\|?\s*$/gm, "")
31
+ .replace(/\|/g, " ")
32
+ .replace(/(\*\*|__)(.*?)\1/g, "$2")
33
+ .replace(/(\*|_)(.*?)\1/g, "$2")
34
+ .replace(/~~(.*?)~~/g, "$1")
35
+ .replace(/[ \t]+/g, " ")
36
+ .replace(/\s*\n\s*\n\s*/g, "\n")
37
+ .trim());
38
+ }
39
+ /**
40
+ * Which voice language to ask for. The fleet's operators write mixed German
41
+ * and English; a German sentence is detectable cheaply enough by its most
42
+ * common function words to pick the voice, and the page's language otherwise.
43
+ */
44
+ export function guessSpeechLang(text, fallback = "en") {
45
+ const sample = text.toLowerCase().slice(0, 600);
46
+ const german = (sample.match(/\b(und|nicht|das|ist|ich|mit|für|auf|eine?|der|die|sind|wird|noch|auch)\b/g) ?? []).length;
47
+ const english = (sample.match(/\b(and|not|the|is|with|for|on|a|an|are|will|also|this|that)\b/g) ?? []).length;
48
+ if (german > english && german >= 3)
49
+ return "de";
50
+ if (english >= 3)
51
+ return "en";
52
+ return fallback;
53
+ }
54
+ /** The browser's own voice: free, offline, no key. `null` where there is none. */
55
+ export function createSynthSpeaker() {
56
+ if (typeof window === "undefined" || !("speechSynthesis" in window))
57
+ return null;
58
+ let current = null;
59
+ return {
60
+ speak(text) {
61
+ return new Promise((resolve) => {
62
+ window.speechSynthesis.cancel();
63
+ const u = new SpeechSynthesisUtterance(text);
64
+ u.lang = guessSpeechLang(text, document.documentElement.lang || "en");
65
+ u.onend = () => {
66
+ if (current === u)
67
+ current = null;
68
+ resolve();
69
+ };
70
+ u.onerror = u.onend;
71
+ current = u;
72
+ window.speechSynthesis.speak(u);
73
+ });
74
+ },
75
+ cancel() {
76
+ window.speechSynthesis.cancel();
77
+ current = null;
78
+ },
79
+ };
80
+ }
81
+ //# sourceMappingURL=speech.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"speech.js","sourceRoot":"","sources":["../src/speech.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bitbaum/chatkit",
3
- "version": "0.8.2",
3
+ "version": "0.10.0",
4
4
  "description": "One chat for every product: a composer with a microphone that always works, a thread that does not fight the reader, messages with copy and retry, and LLM markdown \u2014 the fleet's chat standard as a package.",
5
5
  "license": "MIT",
6
6
  "author": "Cato",
package/styles.css CHANGED
@@ -122,6 +122,15 @@
122
122
  }
123
123
  .ck-input::placeholder {
124
124
  color: var(--ck-muted);
125
+ /* One line, always: an empty box is one row high on every phone, and a
126
+ long placeholder is cut at the edge rather than wrapped into a second
127
+ line that the box then clips in half (320px, 2026-10-21). */
128
+ white-space: nowrap;
129
+ overflow: hidden;
130
+ text-overflow: ellipsis;
131
+ }
132
+ .ck-input:placeholder-shown {
133
+ overflow: hidden;
125
134
  }
126
135
  .ck-actions {
127
136
  display: flex;
@@ -786,7 +795,175 @@
786
795
  margin-top: 0.25em;
787
796
  }
788
797
  .ck-md-h {
798
+ margin: 0;
789
799
  font-weight: 650;
800
+ line-height: 1.3;
801
+ }
802
+ .ck-md > .ck-md-h + * {
803
+ margin-top: 0.5em;
804
+ }
805
+ .ck-md > * + .ck-md-h {
806
+ margin-top: 1.25em;
807
+ }
808
+ .ck-md-h1 {
809
+ font-size: 1.375em;
810
+ }
811
+ .ck-md-h2 {
812
+ font-size: 1.1875em;
813
+ }
814
+ .ck-md-h3 {
815
+ font-size: 1.0625em;
816
+ }
817
+ .ck-md ul ul,
818
+ .ck-md ol ul {
819
+ margin-top: 0.25em;
820
+ }
821
+ .ck-task {
822
+ list-style: none;
823
+ margin-left: -1.25em;
824
+ }
825
+ .ck-task > input {
826
+ margin: 0 0.5em 0 0;
827
+ vertical-align: -0.1em;
828
+ accent-color: var(--ck-accent);
829
+ pointer-events: none;
830
+ }
831
+ .ck-hr {
832
+ height: 1px;
833
+ margin: 1.25em 0;
834
+ border: 0;
835
+ background: var(--ck-line);
836
+ }
837
+ .ck-img {
838
+ display: block;
839
+ max-width: 100%;
840
+ height: auto;
841
+ margin: 0.25em 0;
842
+ border: 1px solid var(--ck-line);
843
+ border-radius: var(--ck-radius-sm);
844
+ }
845
+
846
+ /* A list of named things, as cards: a badge, the name, the body. The row is
847
+ a grid so a long name wraps under itself, never under the badge. */
848
+ .ck-cards {
849
+ display: flex;
850
+ flex-direction: column;
851
+ gap: 0.875em;
852
+ margin: 0;
853
+ padding: 0;
854
+ list-style: none;
855
+ }
856
+ .ck-card {
857
+ display: grid;
858
+ grid-template-columns: 2.5em minmax(0, 1fr);
859
+ gap: 0.75em;
860
+ align-items: start;
861
+ }
862
+ .ck-card-badge {
863
+ display: flex;
864
+ align-items: center;
865
+ justify-content: center;
866
+ width: 2.5em;
867
+ height: 2.5em;
868
+ border-radius: 0.75em;
869
+ background: var(--ck-raised);
870
+ color: var(--ck-fg-2);
871
+ font-size: 1em;
872
+ font-weight: 600;
873
+ font-variant-numeric: tabular-nums;
874
+ }
875
+ .ck-cards:not(.ck-cards-numbered) .ck-card-badge:not(.ck-card-icon) {
876
+ font-size: 1.5em;
877
+ line-height: 1;
878
+ }
879
+ .ck-card-icon {
880
+ font-size: 1.25em;
881
+ }
882
+ .ck-card-body {
883
+ min-width: 0;
884
+ padding-top: 0.2em;
885
+ }
886
+ .ck-card-title {
887
+ font-weight: 650;
888
+ }
889
+ .ck-card-text {
890
+ margin-top: 0.25em;
891
+ color: var(--ck-fg-2);
892
+ }
893
+ .ck-card-body > ul {
894
+ margin-top: 0.375em;
895
+ }
896
+
897
+ /* A table that scrolls sideways on a phone instead of breaking the page. */
898
+ .ck-table-wrap {
899
+ max-width: 100%;
900
+ overflow-x: auto;
901
+ -webkit-overflow-scrolling: touch;
902
+ }
903
+ .ck-table {
904
+ width: 100%;
905
+ border-collapse: collapse;
906
+ font-size: 0.9375em;
907
+ line-height: 1.45;
908
+ }
909
+ .ck-table th,
910
+ .ck-table td {
911
+ padding: 0.5em 0.75em 0.5em 0;
912
+ border-bottom: 1px solid var(--ck-line);
913
+ text-align: left;
914
+ vertical-align: top;
915
+ }
916
+ .ck-table th {
917
+ color: var(--ck-fg);
918
+ font-weight: 650;
919
+ }
920
+ .ck-table td {
921
+ color: var(--ck-fg);
922
+ }
923
+ .ck-table tr:last-child td {
924
+ border-bottom: 0;
925
+ }
926
+
927
+ /* The sources an answer cites, once, under it: a label and a chip each. */
928
+ .ck-sources {
929
+ display: flex;
930
+ flex-wrap: wrap;
931
+ align-items: center;
932
+ gap: 0.375rem;
933
+ margin-top: 1em;
934
+ }
935
+ .ck-sources-label {
936
+ margin-right: 0.25rem;
937
+ color: var(--ck-muted);
938
+ font-size: 0.8125rem;
939
+ }
940
+ .ck-source {
941
+ display: inline-flex;
942
+ align-items: center;
943
+ gap: 0.375em;
944
+ max-width: 100%;
945
+ min-height: 2rem;
946
+ padding: 0 0.625rem;
947
+ border: 1px solid var(--ck-line);
948
+ border-radius: 999px;
949
+ background: var(--ck-raised);
950
+ color: var(--ck-fg-2);
951
+ font-size: 0.8125rem;
952
+ text-decoration: none;
953
+ }
954
+ a.ck-source:hover {
955
+ border-color: var(--ck-line-2);
956
+ color: var(--ck-fg);
957
+ }
958
+ .ck-source-id {
959
+ font-family: var(--ck-mono);
960
+ font-size: 0.75em;
961
+ color: var(--ck-muted);
962
+ }
963
+ .ck-source-label {
964
+ overflow: hidden;
965
+ text-overflow: ellipsis;
966
+ white-space: nowrap;
790
967
  }
791
968
  .ck-md blockquote {
792
969
  padding-left: 0.875em;
@@ -889,6 +1066,13 @@
889
1066
  .ck-reply:hover {
890
1067
  border-color: var(--ck-accent);
891
1068
  }
1069
+ /* The first reply is the recommended next step: the same pill, drawn as the
1070
+ lead — a firmer line and the reading colour — so the eye lands on it. */
1071
+ .ck-reply-lead {
1072
+ border-color: var(--ck-line-2);
1073
+ background: var(--ck-raised);
1074
+ font-weight: 500;
1075
+ }
892
1076
  .ck-reply:disabled {
893
1077
  opacity: 0.5;
894
1078
  cursor: default;
@@ -1026,6 +1210,110 @@
1026
1210
  cursor: not-allowed;
1027
1211
  opacity: 0.45;
1028
1212
  }
1213
+ /* An app's own rows under Camera · Photos · Files in the attach sheet. */
1214
+ .ck-sheet-extra {
1215
+ display: flex;
1216
+ flex-direction: column;
1217
+ gap: 0.125rem;
1218
+ margin-top: 0.75rem;
1219
+ padding-top: 0.75rem;
1220
+ border-top: 1px solid var(--ck-line);
1221
+ }
1222
+
1223
+ /* while an answer is read aloud: Stop, a quiet wave, the time so far */
1224
+ .ck-player {
1225
+ align-items: center;
1226
+ }
1227
+ .ck-action-stop {
1228
+ color: var(--ck-fg);
1229
+ }
1230
+ .ck-player-wave {
1231
+ display: inline-flex;
1232
+ align-items: center;
1233
+ gap: 2px;
1234
+ height: 1rem;
1235
+ }
1236
+ .ck-player-wave > span {
1237
+ display: block;
1238
+ width: 2px;
1239
+ height: 0.375rem;
1240
+ border-radius: 1px;
1241
+ background: var(--ck-fg-2);
1242
+ animation: ck-wave 1s ease-in-out infinite;
1243
+ }
1244
+ .ck-player-wave > span:nth-child(2n) {
1245
+ animation-delay: 0.15s;
1246
+ }
1247
+ .ck-player-wave > span:nth-child(3n) {
1248
+ animation-delay: 0.3s;
1249
+ }
1250
+ @keyframes ck-wave {
1251
+ 50% {
1252
+ height: 1rem;
1253
+ }
1254
+ }
1255
+ .ck-player-time {
1256
+ font-size: 0.8125rem;
1257
+ font-variant-numeric: tabular-nums;
1258
+ color: var(--ck-fg-2);
1259
+ }
1260
+ @media (prefers-reduced-motion: reduce) {
1261
+ .ck-player-wave > span {
1262
+ animation: none;
1263
+ height: 0.75rem;
1264
+ }
1265
+ }
1266
+
1267
+ /* the sources sheet on a touch screen: one row per record */
1268
+ .ck-sources-open {
1269
+ cursor: pointer;
1270
+ font: inherit;
1271
+ }
1272
+ .ck-sources-open > svg {
1273
+ font-size: 1rem;
1274
+ }
1275
+ .ck-source-list {
1276
+ display: flex;
1277
+ flex-direction: column;
1278
+ margin: 0;
1279
+ padding: 0;
1280
+ list-style: none;
1281
+ }
1282
+ .ck-source-row {
1283
+ display: flex;
1284
+ align-items: flex-start;
1285
+ gap: 0.75rem;
1286
+ min-height: var(--ck-target);
1287
+ padding: 0.625rem 0.25rem;
1288
+ border-top: 1px solid var(--ck-line);
1289
+ color: var(--ck-fg);
1290
+ text-decoration: none;
1291
+ }
1292
+ .ck-source-list > li:first-child .ck-source-row {
1293
+ border-top: 0;
1294
+ }
1295
+ .ck-source-row .ck-source-id {
1296
+ margin-top: 0.125rem;
1297
+ }
1298
+ .ck-source-body {
1299
+ display: flex;
1300
+ min-width: 0;
1301
+ flex-direction: column;
1302
+ gap: 0.125rem;
1303
+ }
1304
+ .ck-source-title {
1305
+ font-weight: 500;
1306
+ overflow-wrap: anywhere;
1307
+ }
1308
+ .ck-source-detail {
1309
+ font-size: 0.8125rem;
1310
+ color: var(--ck-fg-2);
1311
+ overflow-wrap: anywhere;
1312
+ }
1313
+ a.ck-source-row:hover .ck-source-title {
1314
+ text-decoration: underline;
1315
+ }
1316
+
1029
1317
  @keyframes ck-rise {
1030
1318
  from {
1031
1319
  transform: translateY(1.5rem);