inline-chat-kit 0.52.0 → 0.54.1

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.
@@ -0,0 +1,156 @@
1
+ # Getting started
2
+
3
+ Ten minutes, one file, a chat that works with no backend. Then one paragraph to
4
+ point it at your model.
5
+
6
+ If you are looking for what each component takes, that is the
7
+ [README](./README.md); for colours and sizes, [theming.md](./theming.md). This
8
+ page is only the shortest path to something running.
9
+
10
+ ## Install
11
+
12
+ ```bash
13
+ npm install inline-chat-kit motion lucide-react
14
+ ```
15
+
16
+ `react`, `react-dom`, `motion` and `lucide-react` are peer dependencies — the
17
+ kit uses whatever copy your app already has. React 18 or 19.
18
+
19
+ ## The whole thing
20
+
21
+ ```tsx
22
+ import { useCallback, useState } from "react";
23
+ import { ChatTurnRow, Conversation, useChatTurns } from "inline-chat-kit";
24
+ import "inline-chat-kit/styles.css";
25
+
26
+ /** How far below the top edge a sent message comes to rest. */
27
+ const ANCHOR = 24;
28
+
29
+ export function MinimalChat() {
30
+ /* Which turn to hold at the top. Kept here rather than inside the kit
31
+ because "which message am I looking at" is the host's question: you may
32
+ want it to follow a regenerate, a jump from a sidebar, or nothing at all. */
33
+ const [anchored, setAnchored] = useState<string | null>(null);
34
+
35
+ const { turns, setDraft, submit, stop, beginEdit, cancelEdit } = useChatTurns({
36
+ /* Return a string, a promise of one, or an async iterable of deltas. The
37
+ kit has no answers of its own — return nothing and nothing appears. */
38
+ onSend: async (message) => `You said: ${message}`,
39
+ });
40
+
41
+ const send = useCallback(
42
+ (id: string, value: string) => {
43
+ setAnchored(id);
44
+ submit(id, value);
45
+ },
46
+ [submit]
47
+ );
48
+
49
+ /* Held while the answer is arriving, let go when it settles — otherwise the
50
+ question stays pinned to the top for ever and the composer, which is the
51
+ last turn, sits below the fold. */
52
+ const holding = turns.find((turn) => turn.id === anchored);
53
+ const anchorId =
54
+ holding && holding.state !== "resting" ? `turn-${holding.id}` : undefined;
55
+
56
+ return (
57
+ <div style={{ display: "flex", flexDirection: "column", height: "100dvh" }}>
58
+ <Conversation
59
+ anchorId={anchorId}
60
+ /* Match whatever padding sits above the conversation, or a turn
61
+ brought to the top lands underneath it. */
62
+ anchorOffset={ANCHOR}
63
+ /* Room left under the composer when an answer settles. */
64
+ endOffset={ANCHOR}
65
+ style={{ padding: ANCHOR }}
66
+ >
67
+ {turns.map((turn, i) => (
68
+ <ChatTurnRow
69
+ key={turn.id}
70
+ turn={turn}
71
+ /* The last turn is the composer: this is what makes it one. */
72
+ isActiveInput={
73
+ i === turns.length - 1 &&
74
+ (turn.state === "idle" || turn.state === "typing")
75
+ }
76
+ /* Pass the hook's own functions straight through — they are stable,
77
+ and `ChatTurnRow` is memoised on them. An arrow made during
78
+ render hands the memo a new prop every time. */
79
+ onDraft={setDraft}
80
+ onSubmit={send}
81
+ onStop={stop}
82
+ onEdit={beginEdit}
83
+ onCancelEdit={cancelEdit}
84
+ placeholder="Ask anything…"
85
+ />
86
+ ))}
87
+ </Conversation>
88
+ </div>
89
+ );
90
+ }
91
+ ```
92
+
93
+ That is a working chat: type, press enter, the pill you typed into becomes the
94
+ bubble holding your message, it travels to the top, and the answer is revealed
95
+ underneath it at reading pace. The last turn is always the composer for the
96
+ next message — that is what `isActiveInput` marks, and it is the whole idea.
97
+
98
+ **This file is compiled on every build of this package and a test asserts it
99
+ matches this page character for character.** If it does not work, that is a
100
+ bug here, not a mistake you made.
101
+
102
+ ## Point it at your model
103
+
104
+ One property changes. `onSend` may return a string, a promise of one, or an
105
+ async iterable of deltas — return a string and the kit reveals it at a readable
106
+ pace, return deltas and it shows them as they land.
107
+
108
+ ```tsx
109
+ const { turns, setDraft, submit, stop, beginEdit, cancelEdit } = useChatTurns({
110
+ onSend: async function* (message, { signal }) {
111
+ const response = await fetch("/api/chat", {
112
+ method: "POST",
113
+ body: JSON.stringify({ message }),
114
+ signal,
115
+ });
116
+ const stream = response.body!.pipeThrough(new TextDecoderStream());
117
+ for await (const chunk of stream) yield chunk;
118
+ },
119
+ });
120
+ ```
121
+
122
+ `signal` aborts when the reader presses stop. The kit never invents an answer:
123
+ there is no canned fallback anywhere in the package, so if your handler returns
124
+ nothing, nothing is what appears.
125
+
126
+ ## Three things that are easy to get wrong
127
+
128
+ **The stylesheet is not optional.** The components are CSS Modules and the
129
+ bundled sheet carries every class they reference. Without
130
+ `import "inline-chat-kit/styles.css"` you get an unstyled page, and it looks
131
+ like the package is broken rather than unstyled. This bit the demo in this very
132
+ repository: a bare side-effect import inside the kit's entry survived the dev
133
+ server and was dropped from the production build, and every colour read as
134
+ transparent.
135
+
136
+ **`anchorOffset` has to match whatever sits above the conversation.** It is how
137
+ far below the top edge a sent message comes to rest. Put a fixed header over
138
+ the feed and leave this at zero, and the message is scrolled neatly underneath
139
+ it.
140
+
141
+ **Pass the hook's own functions straight through.** `ChatTurnRow` is memoised
142
+ and that memo is load-bearing: the hook leaves untouched turns referentially
143
+ identical when it rewrites one of them, which only pays off if the rows act on
144
+ it. Measured before the memo existed: streaming one answer produced 366 DOM
145
+ mutations inside an unrelated, already-finished turn. An arrow function created
146
+ during render hands the memo a new prop every time and undoes it.
147
+
148
+ ## Where to go next
149
+
150
+ | | |
151
+ | --- | --- |
152
+ | everything a turn can contain — tools, reasoning, sources, tables, code, questions, approvals | [README](./README.md#what-a-turn-carries-turnpart) |
153
+ | colours, sizes, dark mode, one brand | [theming.md](./theming.md) |
154
+ | the side pane for documents the answer produces | [`<ArtifactCard>`, `<ArtifactPane>`, `<ChatLayout>`](./README.md) |
155
+ | what it looks like before the first message | [`<EmptyState>`](./README.md) |
156
+ | where this is meant to run, and where it is not | [README](./README.md#where-it-is-meant-to-run) |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "inline-chat-kit",
3
- "version": "0.52.0",
3
+ "version": "0.54.1",
4
4
  "description": "An inline AI chat experience for React. The input is the message: it morphs into the bubble, the answer streams beneath it, and the parts around it — tool calls, reasoning, questions, artifacts, dictation — come with it.",
5
5
  "keywords": [
6
6
  "react",
@@ -33,6 +33,7 @@
33
33
  "files": [
34
34
  "dist",
35
35
  "CHANGELOG.md",
36
+ "getting-started.md",
36
37
  "theming.md"
37
38
  ],
38
39
  "main": "./dist/inline-chat-kit.js",
package/theming.md CHANGED
@@ -251,3 +251,54 @@ size the answer is set at instead of drifting away from it.
251
251
  already honours `prefers-reduced-motion` on its own: transforms and layout snap
252
252
  to their final values while opacity and colour still fade, so state stays
253
253
  legible without travelling.
254
+
255
+ ## On a phone: two things only the host can do
256
+
257
+ **First, what a phone is for here.** The kit is built and tuned for desktop and
258
+ tablet. It runs on a phone and does not break — see *Where it is meant to run*
259
+ in the README for what holds up, what does not, and why the inline model is a
260
+ weaker idea on a small screen. Everything below is true on a phone; none of it
261
+ makes a phone the target.
262
+
263
+ The kit sizes itself, expands its own hit areas on a coarse pointer, and keeps
264
+ nothing wider than its container. Two mobile faults are outside it, because
265
+ both live in the host document rather than in any component.
266
+
267
+ **The viewport meta.** Add `viewport-fit=cover` if you want the safe-area
268
+ insets to report anything but zero:
269
+
270
+ ```html
271
+ <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
272
+ ```
273
+
274
+ **The zoom on focus.** iOS zooms the page in to any editable whose text
275
+ computes under 16px, and it does not zoom back out. Tapping the composer threw
276
+ the conversation out of frame and left it there.
277
+
278
+ **The kit handles this and you do not have to do anything.** Under
279
+ `@media (pointer: coarse)` the type scale is raised and two tokens carry a
280
+ floor:
281
+
282
+ ```css
283
+ --ick-composer-size: max(1rem, var(--ick-text-sm));
284
+ --ick-field-size: max(1rem, var(--ick-text-md));
285
+ ```
286
+
287
+ `max()` rather than a flat `1rem`, so a theme that puts the composer at 18px
288
+ keeps 18 and a theme that puts it at 13 gets 16 — a floor, not an override.
289
+ Anything somebody types into should draw at `--ick-field-size`; that is the
290
+ token the guard in `tools/mobile/zoom-check.mjs` asserts against, in both
291
+ engines, at phone width.
292
+
293
+ There were three ways out and two of them were worse.
294
+
295
+ | | |
296
+ | --- | --- |
297
+ | `maximum-scale=1` in the meta | Works. Takes pinch-zoom away from everybody, permanently, for a fault that lasts as long as somebody is typing. |
298
+ | Lock the scale **while a field has focus** | What this repo shipped for a while — a hook rewriting the host's viewport meta on `pointerdown`. It cost two ordering bugs, one race, and a guard that switched itself off for good once the page was zoomed. None of it could be checked: no engine outside a real iPhone implements zoom-on-focus, so every green run proved nothing. |
299
+ | **Stop being under 16px** | What the kit does. Checkable everywhere, and there is nothing left to go wrong. |
300
+
301
+ The objection to the third was always that the composer would become the
302
+ largest text on the page, bigger than the answer it turns into — true if only
303
+ the composer moves. So the whole scale moves, on touch devices only. 12px
304
+ reading text on a 390px screen was too small anyway.