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.
- package/CHANGELOG.md +349 -1
- package/README.md +40 -0
- package/dist/Artifact/ArtifactCard.d.ts +8 -1
- package/dist/Artifact/ChatLayout.d.ts +10 -1
- package/dist/Conversation/Conversation.d.ts +51 -0
- package/dist/ReplyThreadPopup/placePanel.d.ts +43 -0
- package/dist/examples/minimal.d.ts +1 -0
- package/dist/inline-chat-kit.css +1 -1
- package/dist/inline-chat-kit.js +1385 -1153
- package/dist/inline-chat-kit.js.map +1 -1
- package/getting-started.md +156 -0
- package/package.json +2 -1
- package/theming.md +51 -0
|
@@ -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.
|
|
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.
|