@talk2view/sdk 0.18.1 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +71 -103
- package/dist/assistant-ui/index.d.ts.map +1 -1
- package/dist/assistant-ui/index.js +4 -2
- package/dist/assistant-ui/index.js.map +1 -1
- package/dist/auth.d.ts +8 -0
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +27 -11
- package/dist/auth.js.map +1 -1
- package/dist/chat/auth-gate.d.ts +3 -1
- package/dist/chat/auth-gate.d.ts.map +1 -1
- package/dist/chat/auth-gate.js +29 -5
- package/dist/chat/auth-gate.js.map +1 -1
- package/dist/chat/composer-extras.js +2 -2
- package/dist/chat/index.d.ts +1 -0
- package/dist/chat/index.d.ts.map +1 -1
- package/dist/chat/index.js +1 -0
- package/dist/chat/index.js.map +1 -1
- package/dist/chat/launcher.d.ts.map +1 -1
- package/dist/chat/launcher.js +6 -2
- package/dist/chat/launcher.js.map +1 -1
- package/dist/chat/markdown.d.ts +14 -4
- package/dist/chat/markdown.d.ts.map +1 -1
- package/dist/chat/markdown.js +28 -1
- package/dist/chat/markdown.js.map +1 -1
- package/dist/chat/provider.d.ts +7 -0
- package/dist/chat/provider.d.ts.map +1 -1
- package/dist/chat/provider.js +22 -1
- package/dist/chat/provider.js.map +1 -1
- package/dist/chat/vendor/LICENSE +21 -0
- package/dist/chat/vendor/MANIFEST.json +84 -0
- package/dist/chat/vendor/attachment.aui.d.ts.map +1 -1
- package/dist/chat/vendor/attachment.aui.js +8 -0
- package/dist/chat/vendor/attachment.aui.js.map +1 -1
- package/dist/chat/vendor/file.d.ts.map +1 -1
- package/dist/chat/vendor/file.js +8 -0
- package/dist/chat/vendor/file.js.map +1 -1
- package/dist/chat/vendor/follow-up-suggestions.aui.d.ts.map +1 -1
- package/dist/chat/vendor/follow-up-suggestions.aui.js +8 -0
- package/dist/chat/vendor/follow-up-suggestions.aui.js.map +1 -1
- package/dist/chat/vendor/image.d.ts.map +1 -1
- package/dist/chat/vendor/image.js +8 -0
- package/dist/chat/vendor/image.js.map +1 -1
- package/dist/chat/vendor/markdown-text.d.ts.map +1 -1
- package/dist/chat/vendor/markdown-text.js +8 -0
- package/dist/chat/vendor/markdown-text.js.map +1 -1
- package/dist/chat/vendor/reasoning.aui.d.ts.map +1 -1
- package/dist/chat/vendor/reasoning.aui.js +8 -0
- package/dist/chat/vendor/reasoning.aui.js.map +1 -1
- package/dist/chat/vendor/reasoning.d.ts.map +1 -1
- package/dist/chat/vendor/reasoning.js +8 -0
- package/dist/chat/vendor/reasoning.js.map +1 -1
- package/dist/chat/vendor/thread-list.aui.d.ts.map +1 -1
- package/dist/chat/vendor/thread-list.aui.js +8 -0
- package/dist/chat/vendor/thread-list.aui.js.map +1 -1
- package/dist/chat/vendor/thread.aui.d.ts.map +1 -1
- package/dist/chat/vendor/thread.aui.js +8 -0
- package/dist/chat/vendor/thread.aui.js.map +1 -1
- package/dist/chat/vendor/tool-fallback.aui.d.ts.map +1 -1
- package/dist/chat/vendor/tool-fallback.aui.js +8 -0
- package/dist/chat/vendor/tool-fallback.aui.js.map +1 -1
- package/dist/chat/vendor/tool-group.aui.d.ts.map +1 -1
- package/dist/chat/vendor/tool-group.aui.js +8 -0
- package/dist/chat/vendor/tool-group.aui.js.map +1 -1
- package/dist/chat/vendor/tooltip-icon-button.d.ts.map +1 -1
- package/dist/chat/vendor/tooltip-icon-button.js +8 -0
- package/dist/chat/vendor/tooltip-icon-button.js.map +1 -1
- package/dist/chat/views/account.d.ts.map +1 -1
- package/dist/chat/views/account.js +6 -0
- package/dist/chat/views/account.js.map +1 -1
- package/dist/chat/voice-button.d.ts +34 -0
- package/dist/chat/voice-button.d.ts.map +1 -0
- package/dist/chat/voice-button.js +175 -0
- package/dist/chat/voice-button.js.map +1 -0
- package/dist/chat-css.js +1 -1
- package/dist/chat.css +1 -1
- package/dist/chat.isolated.css +176 -3
- package/dist/chat.raw.css +176 -3
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +49 -0
- package/dist/client.js.map +1 -1
- package/dist/event-emitter.d.ts +1 -0
- package/dist/event-emitter.d.ts.map +1 -1
- package/dist/event-emitter.js +3 -0
- package/dist/event-emitter.js.map +1 -1
- package/dist/index.d.ts +24 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +45 -0
- package/dist/index.js.map +1 -1
- package/dist/streaming.d.ts.map +1 -1
- package/dist/streaming.js +5 -1
- package/dist/streaming.js.map +1 -1
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +13 -2
- package/dist/tools.js.map +1 -1
- package/dist/types.d.ts +60 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/ui/components/MarkdownRenderer.d.ts +1 -1
- package/dist/ui/components/MarkdownRenderer.js +1 -1
- package/dist/ui/index.d.ts +10 -0
- package/dist/ui/index.d.ts.map +1 -1
- package/dist/ui/index.js +10 -0
- package/dist/ui/index.js.map +1 -1
- package/dist/ui/safeMarkdown.d.ts.map +1 -1
- package/dist/ui/safeMarkdown.js +4 -2
- package/dist/ui/safeMarkdown.js.map +1 -1
- package/dist/voice-error.d.ts +8 -0
- package/dist/voice-error.d.ts.map +1 -0
- package/dist/voice-error.js +10 -0
- package/dist/voice-error.js.map +1 -0
- package/dist/voice-facade.d.ts +57 -0
- package/dist/voice-facade.d.ts.map +1 -0
- package/dist/voice-facade.js +139 -0
- package/dist/voice-facade.js.map +1 -0
- package/dist/voice.d.ts +157 -0
- package/dist/voice.d.ts.map +1 -0
- package/dist/voice.js +630 -0
- package/dist/voice.js.map +1 -0
- package/docs/chat.md +532 -0
- package/docs/voice.md +88 -0
- package/package.json +22 -9
package/docs/chat.md
ADDED
|
@@ -0,0 +1,532 @@
|
|
|
1
|
+
# The packaged chat — `@talk2view/sdk/chat`
|
|
2
|
+
|
|
3
|
+
The branded Talk2View chat, ready to render. Two components and one stylesheet.
|
|
4
|
+
Your app needs no Tailwind, no PostCSS, no shadcn and no copied files.
|
|
5
|
+
|
|
6
|
+
```tsx
|
|
7
|
+
import { Talk2ViewChat } from '@talk2view/sdk/chat';
|
|
8
|
+
import '@talk2view/sdk/chat.css';
|
|
9
|
+
|
|
10
|
+
<Talk2ViewChat partnerKey="pk_live_…" tools={tools} />
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
A runnable version of everything below, in a Vite app with no Tailwind, is in
|
|
14
|
+
[`examples/react-chat`](../examples/react-chat).
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Contents
|
|
19
|
+
|
|
20
|
+
- [The two components](#the-two-components)
|
|
21
|
+
- [Props](#props)
|
|
22
|
+
- [What you have to know](#what-you-have-to-know)
|
|
23
|
+
- [Theming](#theming)
|
|
24
|
+
- [Dark mode](#dark-mode)
|
|
25
|
+
- [The launcher in someone else's page](#the-launcher-in-someone-elses-page)
|
|
26
|
+
- [Content Security Policy, and hosts with no CSS loader](#content-security-policy-and-hosts-with-no-css-loader)
|
|
27
|
+
- [Next.js App Router](#nextjs-app-router)
|
|
28
|
+
- [Size](#size)
|
|
29
|
+
- [Errors you may see](#errors-you-may-see)
|
|
30
|
+
- [For contributors: the stylesheet and the vendored files](#for-contributors-the-stylesheet-and-the-vendored-files)
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## The two components
|
|
35
|
+
|
|
36
|
+
### `<Talk2ViewChat>`
|
|
37
|
+
|
|
38
|
+
Fills whatever box you put it in — a side panel, a task pane, a page section.
|
|
39
|
+
The parent needs a height: the chat is `height: 100%`, so a parent with `auto`
|
|
40
|
+
height collapses it to its content.
|
|
41
|
+
|
|
42
|
+
```tsx
|
|
43
|
+
<div style={{ height: '100%' }}>
|
|
44
|
+
<Talk2ViewChat
|
|
45
|
+
partnerKey="pk_live_…"
|
|
46
|
+
tools={tools}
|
|
47
|
+
welcome={{ heading: 'How can I help?', suggestions: ['What can you do?'] }}
|
|
48
|
+
/>
|
|
49
|
+
</div>
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
A header of one-tap views sits over a thread that never unmounts, so switching
|
|
53
|
+
to Settings and back keeps the conversation, the scroll position and a
|
|
54
|
+
half-typed message.
|
|
55
|
+
|
|
56
|
+
The views are Account, Skills, Settings and Chats.
|
|
57
|
+
|
|
58
|
+
**Skills** is the end-user's own knowledge: markdown they write once — a
|
|
59
|
+
protocol, a house style, the way their team names things — that the agent can
|
|
60
|
+
load when it helps. They are kept on that device and sent with that person's
|
|
61
|
+
chats, where they merge with the partner's and the built-in skills and win over
|
|
62
|
+
both. A skill can be switched off without deleting it, and a `.md` file can be
|
|
63
|
+
imported, taking its `name` and `description` from frontmatter when it has any.
|
|
64
|
+
Turn the view off with `features={{ skills: false }}`; the underlying
|
|
65
|
+
`client.skills` API stays available either way.
|
|
66
|
+
|
|
67
|
+
### `<Talk2ViewChatLauncher>`
|
|
68
|
+
|
|
69
|
+
The same chat in a floating panel: a mark in the corner of the page, a popover
|
|
70
|
+
on a desktop, a full-screen sheet on a phone. It takes every prop
|
|
71
|
+
`<Talk2ViewChat>` takes, plus the launcher's own.
|
|
72
|
+
|
|
73
|
+
```tsx
|
|
74
|
+
<Talk2ViewChatLauncher
|
|
75
|
+
partnerKey="pk_live_…"
|
|
76
|
+
tools={tools}
|
|
77
|
+
label="Ask Talk2View"
|
|
78
|
+
colourway="smoke-teal"
|
|
79
|
+
keepClearOf="#cookie-banner"
|
|
80
|
+
/>
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### `useTalk2ViewChatClient()`
|
|
84
|
+
|
|
85
|
+
The client the chat is using, for your own UI — a sign-out item in your account
|
|
86
|
+
menu, a button that starts a conversation from somewhere else in your app. It
|
|
87
|
+
must be called inside the chat.
|
|
88
|
+
|
|
89
|
+
```tsx
|
|
90
|
+
function SignOut() {
|
|
91
|
+
const t2v = useTalk2ViewChatClient();
|
|
92
|
+
return <button onClick={() => t2v.auth.logout()}>Sign out</button>;
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## Props
|
|
99
|
+
|
|
100
|
+
Both components also accept everything `T2VConfig` accepts (`baseUrl`, `model`,
|
|
101
|
+
`debug`, `requestTimeout`, `anonymousAutoStart`) except `partnerKey`, which they
|
|
102
|
+
take directly.
|
|
103
|
+
|
|
104
|
+
| Prop | Default | What it does |
|
|
105
|
+
|---|---|---|
|
|
106
|
+
| `partnerKey` | — | Your key. Give this **or** `client`. |
|
|
107
|
+
| `client` | — | An existing `Talk2View` instance, shared with the rest of your app. The chat will not destroy a client you passed in. |
|
|
108
|
+
| `tools` | none | Your client tools. They run in your app. |
|
|
109
|
+
| `systemPrompt` | none | Your own instructions, sent with every message. Changing it does not restart the conversation. |
|
|
110
|
+
| `welcome` | `{}` | `heading` and `suggestions` on the empty chat. Clicking a suggestion sends it. |
|
|
111
|
+
| `allowAnonymous` | `true` | Logged-out visitors chat as guests. `false` shows sign-in first — **and see the note below**. |
|
|
112
|
+
| `features` | all on | `{ dictation, attachments, settings, account, threadList, skills, voice }`. `voice` is the realtime voice button beside the launcher, shown only for partners with voice enabled — see [voice.md](voice.md). |
|
|
113
|
+
| `fontFamily` | inherit | Your own font stack. Default: whatever the page uses. |
|
|
114
|
+
| `resetPasswordUrl` | none | Where "Forgot it?" goes. Without it the link is not shown. |
|
|
115
|
+
| `footer` | none | A short line under the composer — your terms, a disclaimer. |
|
|
116
|
+
| `describeToolActivity` | none | `(name, args) => string \| null`. What a running tool is doing, in your words. |
|
|
117
|
+
| `isToolDestructive` | none | `(name, args) => boolean`. Warn before this call. |
|
|
118
|
+
| `destructiveWarning` | built-in | `(name, activity) => ReactNode`. Your own warning text. |
|
|
119
|
+
| `className` | none | On the chat's own element — **on the launcher, on the anchor**. |
|
|
120
|
+
|
|
121
|
+
Launcher only:
|
|
122
|
+
|
|
123
|
+
| Prop | Default | What it does |
|
|
124
|
+
|---|---|---|
|
|
125
|
+
| `label` | `'Ask Talk2View'` | The words beside the mark, until the visitor opens the chat once. `''` hides them; the mark and the tooltip stay. |
|
|
126
|
+
| `colourway` | `'smoke-teal'` | `'smoke-teal'`, `'teal-smoke'` or `'teal-white'`. |
|
|
127
|
+
| `visitorColourway` | `false` | Put a colour picker in Settings. |
|
|
128
|
+
| `keepClearOf` | none | A CSS selector for something pinned to the bottom of the page. |
|
|
129
|
+
| `sheetBelow` | `640` | Screen width in px at and below which the panel is a full-screen sheet. |
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## What you have to know
|
|
134
|
+
|
|
135
|
+
These are the things that surprise people. None of them is a bug; all of them
|
|
136
|
+
have cost someone an afternoon.
|
|
137
|
+
|
|
138
|
+
### `allowAnonymous: false` also stops guest sessions
|
|
139
|
+
|
|
140
|
+
It is not only a screen. The gate is computed from whether anyone is signed in,
|
|
141
|
+
and the client starts a guest session on demand — listing the models for
|
|
142
|
+
Settings would be enough — which would sign the visitor in and open the gate
|
|
143
|
+
from behind. So `allowAnonymous: false` also turns off `anonymousAutoStart`.
|
|
144
|
+
|
|
145
|
+
If you want a sign-in gate **and** guest sessions (there is no sensible reason
|
|
146
|
+
to), pass both explicitly:
|
|
147
|
+
|
|
148
|
+
```tsx
|
|
149
|
+
<Talk2ViewChat partnerKey="…" allowAnonymous={false} anonymousAutoStart />
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### A page with no font gets a serif chat
|
|
153
|
+
|
|
154
|
+
`--t2v-font` is `inherit`. That is the design: the chat looks like part of your
|
|
155
|
+
app, not like something bolted on. But a page that sets no font anywhere
|
|
156
|
+
inherits the browser's default, which is a serif — and so does the chat. Set a
|
|
157
|
+
font on `body`, or pass one:
|
|
158
|
+
|
|
159
|
+
```tsx
|
|
160
|
+
<Talk2ViewChat partnerKey="…" fontFamily='"Space Grotesk", system-ui, sans-serif' />
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Nothing is fetched for it. The chat never loads a font, a stylesheet or a script
|
|
164
|
+
from a third-party host, so whatever you name has to be available to the page.
|
|
165
|
+
|
|
166
|
+
### `isToolDestructive` is your claim about your own tool
|
|
167
|
+
|
|
168
|
+
Nothing checks it. The chat shows a warning above the approval buttons when you
|
|
169
|
+
say a call is destructive, and shows none when you do not — a tool that deletes
|
|
170
|
+
the end-user's work gets no warning unless you say so. Approval itself is a
|
|
171
|
+
different thing and is enforced: a tool declared `permission: true` is never run
|
|
172
|
+
without an answer.
|
|
173
|
+
|
|
174
|
+
```tsx
|
|
175
|
+
<Talk2ViewChat
|
|
176
|
+
partnerKey="…"
|
|
177
|
+
tools={tools}
|
|
178
|
+
describeToolActivity={(name, args) =>
|
|
179
|
+
name === 'insert_text' ? `Inserting ${String(args?.text ?? '').length} characters` : null
|
|
180
|
+
}
|
|
181
|
+
isToolDestructive={(name) => name === 'insert_text' || name === 'delete_section'}
|
|
182
|
+
/>
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Your theme override loses if your CSS loads before ours
|
|
186
|
+
|
|
187
|
+
The chat's own tokens are declared at `.t2v-chat`, and the override the theming
|
|
188
|
+
section below shows you is `.t2v-chat` too. Identical specificity, so the
|
|
189
|
+
winner is whichever stylesheet the browser read **last**. Import ours first and
|
|
190
|
+
yours wins, which is what the usual shape does:
|
|
191
|
+
|
|
192
|
+
```ts
|
|
193
|
+
import '@talk2view/sdk/chat.css'; // ours
|
|
194
|
+
import './app.css'; // yours, and it wins
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Load them the other way — a global stylesheet imported at the entry and the
|
|
198
|
+
chat's CSS pulled in later by a lazily imported component, or a bundler that
|
|
199
|
+
hoists `node_modules` CSS after your own — and every override silently does
|
|
200
|
+
nothing. No error, no warning, just the Talk2View palette.
|
|
201
|
+
|
|
202
|
+
When you cannot control the order, name the class twice. It is the same
|
|
203
|
+
element, so it still matches exactly the chat, but at double the specificity,
|
|
204
|
+
which no source order can beat:
|
|
205
|
+
|
|
206
|
+
```css
|
|
207
|
+
.t2v-chat.t2v-chat {
|
|
208
|
+
--primary: #4A73F0;
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Never reach for `!important` on a token instead. The chat's own stylesheet is
|
|
213
|
+
forbidden from using it on a custom property, and an `!important` of yours would
|
|
214
|
+
be inherited by every surface that reads the token downstream.
|
|
215
|
+
|
|
216
|
+
### Links in the agent's answers always open in a new tab
|
|
217
|
+
|
|
218
|
+
Every link in a markdown answer is rendered with `target="_blank"` and
|
|
219
|
+
`rel="noopener noreferrer"`, and there is no prop to change that. The chat is
|
|
220
|
+
usually a panel inside your application, and the links are not yours or ours —
|
|
221
|
+
the model writes them, often out of a web-search result or a file the end-user
|
|
222
|
+
attached. A same-tab navigation would take your app down with it, unsaved state
|
|
223
|
+
and all.
|
|
224
|
+
|
|
225
|
+
`javascript:` URLs and raw HTML in an answer are already neutralised, by
|
|
226
|
+
react-markdown's default URL transform and by there being no raw-HTML plugin.
|
|
227
|
+
|
|
228
|
+
### Turning attachments off turns off two things
|
|
229
|
+
|
|
230
|
+
`features.attachments: false` removes both the attach button and the drop zone.
|
|
231
|
+
There is no half-off state: either the chat takes files or it does not.
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
## Theming
|
|
236
|
+
|
|
237
|
+
Everything the chat draws is inside `<div class="t2v-chat">`, and every colour,
|
|
238
|
+
radius and font it uses is a custom property declared on that element. Redeclare
|
|
239
|
+
one and the chat follows; nothing else on your page changes.
|
|
240
|
+
|
|
241
|
+
```css
|
|
242
|
+
.t2v-chat {
|
|
243
|
+
--radius: 0.75rem; /* square by default; this rounds everything */
|
|
244
|
+
--primary: #4A73F0; /* the send button, the accent */
|
|
245
|
+
--primary-foreground: #fff;
|
|
246
|
+
--composer-radius: 1rem; /* the composer on its own */
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
Import `@talk2view/sdk/chat.css` **before** your own CSS, or write the selector
|
|
251
|
+
twice (`.t2v-chat.t2v-chat`) — see
|
|
252
|
+
[above](#your-theme-override-loses-if-your-css-loads-before-ours) for why.
|
|
253
|
+
|
|
254
|
+
| Token | What it paints |
|
|
255
|
+
|---|---|
|
|
256
|
+
| `--background` / `--foreground` | The chat's ground and its text |
|
|
257
|
+
| `--card` / `--card-foreground` | Raised blocks |
|
|
258
|
+
| `--popover` / `--popover-foreground` | The launcher's panel, menus, tooltips |
|
|
259
|
+
| `--primary` / `--primary-foreground` | Send, submit, the agreed action |
|
|
260
|
+
| `--secondary` / `--secondary-foreground` | Quiet buttons |
|
|
261
|
+
| `--muted` / `--muted-foreground` | Soft surfaces (the user's bubble) and quiet text |
|
|
262
|
+
| `--accent` / `--accent-foreground` | Hover surfaces |
|
|
263
|
+
| `--destructive` | Errors, Deny |
|
|
264
|
+
| `--border` / `--input` / `--ring` | Edges and the focus ring |
|
|
265
|
+
| `--radius` | Every corner. `--radius-sm/md/lg/xl` are computed from it |
|
|
266
|
+
| `--composer-radius` / `--composer-bg` / `--composer-padding` | The composer on its own |
|
|
267
|
+
| `--t2v-font` | The font. `inherit` by default — prefer the `fontFamily` prop |
|
|
268
|
+
|
|
269
|
+
The brand hexes are there too, if you want to reach past the semantic names:
|
|
270
|
+
`--t2v-teal`, `--t2v-smoke`, `--t2v-ivory`, `--t2v-mist`, `--t2v-stone`,
|
|
271
|
+
`--t2v-slate`, `--t2v-ash`, `--t2v-highlight`, `--t2v-harbor`, `--t2v-amber`.
|
|
272
|
+
|
|
273
|
+
**Two things do not follow your tokens**, on purpose:
|
|
274
|
+
|
|
275
|
+
- **The Apple sign-in button.** Apple's Human Interface Guidelines fix its
|
|
276
|
+
colours, and an app that repaints it can be rejected.
|
|
277
|
+
- **The launcher's mark and tile.** Three fixed colourways, chosen with the
|
|
278
|
+
`colourway` prop, each pinned in every state so a host's hover rules cannot
|
|
279
|
+
half-repaint them. `visitorColourway` hands the choice to the end-user.
|
|
280
|
+
|
|
281
|
+
Everything else, the voice button included, reads the tokens above.
|
|
282
|
+
|
|
283
|
+
---
|
|
284
|
+
|
|
285
|
+
## Dark mode
|
|
286
|
+
|
|
287
|
+
Put `dark` on the chat's container — the same class shadcn uses, scoped so it
|
|
288
|
+
never reaches your page:
|
|
289
|
+
|
|
290
|
+
```tsx
|
|
291
|
+
<Talk2ViewChat partnerKey="…" className={theme === 'dark' ? 'dark' : undefined} />
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
The class travels with the chat: tooltips, dialogs and the launcher's panel
|
|
295
|
+
render in a host element appended to `<body>`, outside the element `className`
|
|
296
|
+
lands on, and the chat carries the theme out to them for you.
|
|
297
|
+
|
|
298
|
+
`dark` on your `<html>` or `<body>` does **not** darken the chat, deliberately —
|
|
299
|
+
your page's theme and the chat's are separate choices. Wire them together in
|
|
300
|
+
your own code if you want them to move as one, as the example does.
|
|
301
|
+
|
|
302
|
+
---
|
|
303
|
+
|
|
304
|
+
## The launcher in someone else's page
|
|
305
|
+
|
|
306
|
+
### Mount it outside anything with a `transform`
|
|
307
|
+
|
|
308
|
+
The launcher's corner is `position: fixed`. An ancestor with `transform`,
|
|
309
|
+
`filter`, `perspective`, `backdrop-filter` or `contain` becomes the containing
|
|
310
|
+
block for fixed descendants, and the launcher then pins to the corner of *that
|
|
311
|
+
element* instead of the window. There is no CSS fix from our side. Render it as
|
|
312
|
+
a child of `<body>`, or of a container with none of those properties:
|
|
313
|
+
|
|
314
|
+
```tsx
|
|
315
|
+
createPortal(<Talk2ViewChatLauncher partnerKey="…" />, document.body)
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
A host page whose own CSS shouts `position: static !important` at your
|
|
319
|
+
containers will do the same thing, for the same reason.
|
|
320
|
+
|
|
321
|
+
### `className` lands on the anchor
|
|
322
|
+
|
|
323
|
+
The anchor is the fixed corner in your page, so `className` is how you move the
|
|
324
|
+
launcher to the other side:
|
|
325
|
+
|
|
326
|
+
```css
|
|
327
|
+
.my-launcher { inset-inline: 1rem auto !important; } /* bottom-left */
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
### `keepClearOf` rides above a banner
|
|
331
|
+
|
|
332
|
+
Give it a CSS selector for anything pinned to the bottom of the page — a cookie
|
|
333
|
+
notice, a playback bar — and the launcher rises above it while it is there and
|
|
334
|
+
drops back when it goes. The selector is re-resolved as the page changes, so a
|
|
335
|
+
banner that appears after the chat is picked up, and a mistyped selector means
|
|
336
|
+
"keep clear of nothing" rather than an error.
|
|
337
|
+
|
|
338
|
+
### The phone sheet
|
|
339
|
+
|
|
340
|
+
At and below `sheetBelow` (640 px by default) the panel is a full-screen sheet
|
|
341
|
+
with its own Close button, and the page behind it does not scroll. The launcher
|
|
342
|
+
stays tappable over it.
|
|
343
|
+
|
|
344
|
+
### What the visitor gets to keep
|
|
345
|
+
|
|
346
|
+
Two things are remembered **per device, in `localStorage`, not per account**:
|
|
347
|
+
the colourway (when `visitorColourway` is on) and the size they dragged the
|
|
348
|
+
panel to. A double click on the panel's top-left corner gives the default size
|
|
349
|
+
back. Whether they have opened the chat before is remembered too — that is what
|
|
350
|
+
retires the label beside the mark.
|
|
351
|
+
|
|
352
|
+
---
|
|
353
|
+
|
|
354
|
+
## Content Security Policy, and hosts with no CSS loader
|
|
355
|
+
|
|
356
|
+
The chat fetches nothing at runtime: no fonts, no CDN, no stylesheet, no script.
|
|
357
|
+
A `connect-src` that allows your Talk2View engine is all it needs.
|
|
358
|
+
|
|
359
|
+
`import '@talk2view/sdk/chat.css'` needs a build that can import CSS. Some hosts
|
|
360
|
+
cannot — an Office add-in's webpack with no CSS rule, a strict `style-src` that
|
|
361
|
+
blocks a `<style>` tag. For those:
|
|
362
|
+
|
|
363
|
+
```ts
|
|
364
|
+
import { injectTalk2ViewChatStyles } from '@talk2view/sdk/chat';
|
|
365
|
+
|
|
366
|
+
injectTalk2ViewChatStyles(); // idempotent; call it as often as you like
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
It installs the same stylesheet through `adoptedStyleSheets`, which `style-src`
|
|
370
|
+
does not govern, and falls back to one `<style>` tag where constructable
|
|
371
|
+
stylesheets are missing. Use one route or the other, not both.
|
|
372
|
+
|
|
373
|
+
---
|
|
374
|
+
|
|
375
|
+
## Next.js App Router
|
|
376
|
+
|
|
377
|
+
The chat is a client component. Import it from a file that has `'use client'`
|
|
378
|
+
at the top, or mark the boundary yourself:
|
|
379
|
+
|
|
380
|
+
```tsx
|
|
381
|
+
// app/chat/panel.tsx
|
|
382
|
+
'use client';
|
|
383
|
+
import { Talk2ViewChat } from '@talk2view/sdk/chat';
|
|
384
|
+
export default function Panel() {
|
|
385
|
+
return <Talk2ViewChat partnerKey={process.env.NEXT_PUBLIC_T2V_KEY!} />;
|
|
386
|
+
}
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
The stylesheet is a side-effect import, so put it in `app/layout.tsx` (or
|
|
390
|
+
anywhere in the client graph) rather than inside a server component.
|
|
391
|
+
|
|
392
|
+
Both components survive `renderToString` — every store the chat subscribes to
|
|
393
|
+
supplies a server snapshot, and the portal host is not created without a
|
|
394
|
+
`document` — so neither has to be loaded with `ssr: false`. Doing so is
|
|
395
|
+
harmless, though, if you would rather keep the weight out of the server bundle
|
|
396
|
+
entirely.
|
|
397
|
+
|
|
398
|
+
---
|
|
399
|
+
|
|
400
|
+
## Size
|
|
401
|
+
|
|
402
|
+
Stated plainly, because it is large.
|
|
403
|
+
|
|
404
|
+
| | gzip |
|
|
405
|
+
|---|---|
|
|
406
|
+
| `@talk2view/sdk` (the core client) | 11.1 KB |
|
|
407
|
+
| `@talk2view/sdk/ui` (the previous chat panel) | 40.3 KB |
|
|
408
|
+
| **`@talk2view/sdk/chat`** | **372.4 KB** |
|
|
409
|
+
| A whole page containing the chat and React | ~437 KB |
|
|
410
|
+
|
|
411
|
+
The first three exclude React and React DOM, which your app already has. The
|
|
412
|
+
weight is `@assistant-ui/react`, `@base-ui/react` and the markdown stack, and
|
|
413
|
+
there is no code-splitting seam inside them to defer.
|
|
414
|
+
|
|
415
|
+
**If you never import `/chat`, you pay none of it.** The entry points are
|
|
416
|
+
separate and `sideEffects` lists only CSS, so a consumer of `@talk2view/sdk` or
|
|
417
|
+
`@talk2view/sdk/ui` reaches no assistant-ui code at all. That is pinned by
|
|
418
|
+
`tests/chat/tree-shake.test.ts`, which bundles the published `dist/` on every
|
|
419
|
+
test run and fails if any of these numbers grows past a fence set just above
|
|
420
|
+
today's reading.
|
|
421
|
+
|
|
422
|
+
---
|
|
423
|
+
|
|
424
|
+
## Errors you may see
|
|
425
|
+
|
|
426
|
+
### "Two copies of @assistant-ui/react are loaded"
|
|
427
|
+
|
|
428
|
+
Thrown in development, on mount. Your app installed its own
|
|
429
|
+
`@assistant-ui/react` at a version npm could not collapse with ours, so there
|
|
430
|
+
are two physical copies on the page. Each has its own React context: the chat
|
|
431
|
+
would mount, render, and then quietly do nothing — the runtime fills one
|
|
432
|
+
context and the components read the other.
|
|
433
|
+
|
|
434
|
+
```bash
|
|
435
|
+
npm ls @assistant-ui/react # shows every copy and who asked for it
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
Fix it by aligning your version with the one `@talk2view/sdk` depends on, so npm
|
|
439
|
+
dedupes them, or by dropping your own copy if the packaged chat is the only
|
|
440
|
+
thing using it. The check does not run in production builds — it is there to
|
|
441
|
+
tell you now rather than leave you with a chat that does nothing.
|
|
442
|
+
|
|
443
|
+
### "`<Talk2ViewChat>` needs either a partnerKey or a client"
|
|
444
|
+
|
|
445
|
+
Exactly what it says. Pass one or the other.
|
|
446
|
+
|
|
447
|
+
### "This component must be rendered inside `<Talk2ViewChat>`"
|
|
448
|
+
|
|
449
|
+
`useTalk2ViewChatClient()` was called outside the chat. Create your own
|
|
450
|
+
`Talk2View` client and pass it in with the `client` prop if you need one in both
|
|
451
|
+
places.
|
|
452
|
+
|
|
453
|
+
---
|
|
454
|
+
|
|
455
|
+
## For contributors: the stylesheet and the vendored files
|
|
456
|
+
|
|
457
|
+
Skip this unless you are changing the chat itself.
|
|
458
|
+
|
|
459
|
+
### Two authoring traps in `chat.src.css`
|
|
460
|
+
|
|
461
|
+
**1. The root rule is written `:root`, never `.t2v-chat`.**
|
|
462
|
+
`scripts/chat/isolate.mjs` rewrites `:root` to `.t2v-chat` and prefixes
|
|
463
|
+
everything else as a descendant of it. So a rule you write as `.t2v-chat { … }`
|
|
464
|
+
ships as `.t2v-chat .t2v-chat { … }` and matches nothing — silently. This cost
|
|
465
|
+
the full-pane chat its height once: it collapsed to its content and nobody found
|
|
466
|
+
out until a browser rendered it.
|
|
467
|
+
|
|
468
|
+
```css
|
|
469
|
+
:root { height: 100%; } /* the chat's own element */
|
|
470
|
+
.aui-thread-root { … } /* anything inside it */
|
|
471
|
+
:root.aui-modal-anchor { … } /* another chat root: the launcher's corner */
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
**2. Armoured utilities beat inline styles and unlayered rules.**
|
|
475
|
+
`isolate.mjs --armour` marks every declaration in `@layer utilities` and
|
|
476
|
+
`@layer components` `!important`. An important author declaration beats an
|
|
477
|
+
inline `style` attribute, so `style={{ width }}` on an element that also carries
|
|
478
|
+
`w-[…]` does nothing. It also beats any hand-written rule of yours that is not
|
|
479
|
+
itself `!important` **and** more specific.
|
|
480
|
+
|
|
481
|
+
This has bitten three separate pieces of work in three disguises, and the build
|
|
482
|
+
never says a word: the rule simply has no effect. When you write CSS that has to
|
|
483
|
+
win against a utility, give it `!important` and higher specificity, and check it
|
|
484
|
+
in a browser:
|
|
485
|
+
|
|
486
|
+
```css
|
|
487
|
+
.aui-modal-content[data-sized] { width: var(--t2v-panel-width) !important; }
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
Never put `!important` on a custom property — that would break retheming, and
|
|
491
|
+
the audit script fails the build if you do.
|
|
492
|
+
|
|
493
|
+
The other side of the same coin: armour does **not** cover hand-written rules in
|
|
494
|
+
the unlayered origin, which is where the launcher's anchor, phone sheet and
|
|
495
|
+
colourways live. A host page's `!important` can beat those.
|
|
496
|
+
`tests/e2e/chat-hosts/` says exactly which ones hold and which do not.
|
|
497
|
+
|
|
498
|
+
### Changing a vendored file
|
|
499
|
+
|
|
500
|
+
Files under `src/chat/vendor/` come from assistant-ui's shadcn registry and are
|
|
501
|
+
never hand-edited. The registry is unversioned and mutable, so every file's
|
|
502
|
+
sha256 is recorded in `MANIFEST.json`.
|
|
503
|
+
|
|
504
|
+
```bash
|
|
505
|
+
npm run chat:sync # refetch, rewrite imports, run the codemod, apply patches
|
|
506
|
+
npm run chat:check # verify the tree is exactly what sync produced
|
|
507
|
+
npm run build:css # rebuild dist/chat.css (scope, namespace, audit, minify)
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
Every difference from upstream must be one of three things: a path rewrite in
|
|
511
|
+
`sync-registry.mjs`, the idempotent `asChild → render` codemod, or a patch file
|
|
512
|
+
under `scripts/chat/patches/`. To change a patch, reverse-apply the old one,
|
|
513
|
+
edit the pristine file, and regenerate with `diff -u` — do not hand-edit the
|
|
514
|
+
hunks. Then run the **online** sync to prove the patch still applies to what the
|
|
515
|
+
registry serves today.
|
|
516
|
+
|
|
517
|
+
Prefer a path rewrite to a patch when the change is one: a rewrite has no hunk
|
|
518
|
+
to reconcile when upstream edits the lines around it. Two of them are in the
|
|
519
|
+
script today — the app aliases (`@/components/ui/` and friends) and the
|
|
520
|
+
`MarkdownText` redirect, which points the thread and the reasoning block at
|
|
521
|
+
`src/chat/markdown.tsx` so the `a` override lives in first-party code. A rewrite
|
|
522
|
+
that finds nothing to do throws, because a silent no-op is how a behaviour
|
|
523
|
+
quietly reverts.
|
|
524
|
+
|
|
525
|
+
Anything else belongs in a wrapper, in `src/chat/ui/`, or in the stylesheet.
|
|
526
|
+
|
|
527
|
+
### Running the browser suites
|
|
528
|
+
|
|
529
|
+
```bash
|
|
530
|
+
npm run test:e2e:chat-hosts # five hostile host pages (also runs in CI)
|
|
531
|
+
npm run test:e2e:chat-example # the react-chat example
|
|
532
|
+
```
|
package/docs/voice.md
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Voice
|
|
2
|
+
|
|
3
|
+
The realtime voice agent: the end-user talks, the agent answers out loud, and
|
|
4
|
+
it can use your client tools while it does. It runs the **same** tools,
|
|
5
|
+
permission checks and approval card as the chat. A tool you registered for chat
|
|
6
|
+
is already a voice tool, so there is no new code to write for voice.
|
|
7
|
+
|
|
8
|
+
Voice is off until Talk2View turns it on for your partner account. While it is
|
|
9
|
+
off, `/v1/config` returns `voice_agent_enabled: false` and nothing below shows.
|
|
10
|
+
|
|
11
|
+
## In the launcher
|
|
12
|
+
|
|
13
|
+
`<Talk2ViewChatLauncher>` shows a microphone beside its mark when your partner
|
|
14
|
+
account has voice enabled. Press it to talk, and press it again to hang up. A
|
|
15
|
+
press while the call is still connecting cancels the call. To hide the button
|
|
16
|
+
even when voice is enabled:
|
|
17
|
+
|
|
18
|
+
```tsx
|
|
19
|
+
<Talk2ViewChatLauncher partnerKey="pk_live_…" features={{ voice: false }} />
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Above the button, a small note shows what the call is doing: connecting,
|
|
23
|
+
listening, working, or why it ended. Errors are written for the end-user, not
|
|
24
|
+
the developer. For example, when every voice slot is taken, the note reads
|
|
25
|
+
"Voice is busy right now. Try again shortly." When a tool needs approval, its
|
|
26
|
+
card appears in the same place.
|
|
27
|
+
|
|
28
|
+
## Standalone
|
|
29
|
+
|
|
30
|
+
```tsx
|
|
31
|
+
import { VoiceButton } from '@talk2view/sdk/chat';
|
|
32
|
+
import '@talk2view/sdk/chat.css';
|
|
33
|
+
|
|
34
|
+
<VoiceButton client={t2v} />;
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
| Prop | Default | |
|
|
38
|
+
|---|---|---|
|
|
39
|
+
| `client` | the chat's | Required outside `<Talk2ViewChat>` / the launcher. |
|
|
40
|
+
| `label` | `"Talk to Talk2View"` | The button's accessible name. |
|
|
41
|
+
| `earcon` | `true` | A short, locally generated sound when the microphone goes live. |
|
|
42
|
+
| `className` | none | Put `dark` here for the dark palette. |
|
|
43
|
+
|
|
44
|
+
A standalone `<VoiceButton>` does not check `voice_agent_enabled` itself.
|
|
45
|
+
Render it only when `(await t2v.getConfig()).voice_agent_enabled` is true.
|
|
46
|
+
|
|
47
|
+
## Without a UI: `t2v.voice`
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
await t2v.voice.start(); // resolves once the microphone is live
|
|
51
|
+
t2v.voice.on('transcript', ({ role, text, final }) => { /* … */ });
|
|
52
|
+
t2v.voice.on('toolCall', ({ toolName, arguments: args }) => { /* … */ });
|
|
53
|
+
t2v.voice.on('approvalChange', (pending) => pending?.decide({ action: 'once' }));
|
|
54
|
+
t2v.voice.on('error', ({ type, message }) => { /* … */ });
|
|
55
|
+
t2v.voice.on('ended', (reason) => { /* 'stopped', 'idle', 'session_cap', … */ });
|
|
56
|
+
await t2v.voice.stop(); // hangs up and releases the microphone
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
`on()` returns an unsubscribe function. Other events are `stateChange`
|
|
60
|
+
(`idle | connecting | listening | ended | error`) and `agentState`
|
|
61
|
+
(`working | idle`).
|
|
62
|
+
|
|
63
|
+
If `start()` fails, it rejects with an error whose `.type` gives the reason:
|
|
64
|
+
`voice_disabled`, `account_required`, `auth_expired`, `no_api_key`,
|
|
65
|
+
`insufficient_credit`, `credit_check_unavailable`, `service_unavailable`,
|
|
66
|
+
`voice_at_capacity`, `voice_ticket_invalid`, `voice_offer_invalid`,
|
|
67
|
+
`upstream_error`, `transport_error` or `voice_error`. The same types arrive on
|
|
68
|
+
the `error` event, which also reports what ends a call without `start()`
|
|
69
|
+
rejecting: `mic_unavailable` (the microphone was refused or is missing; the
|
|
70
|
+
call ends with `'error'`), `auth_expired` (the sign-in expired; the call ends with
|
|
71
|
+
`'auth_expired'`) and `insufficient_credit` (credit ran out; the call ends with
|
|
72
|
+
`'budget_exhausted'`). A deliberate `t2v.auth.logout()` hangs up a call as
|
|
73
|
+
`'stopped'`, with no error.
|
|
74
|
+
|
|
75
|
+
## Browser requirements
|
|
76
|
+
|
|
77
|
+
- **HTTPS.** Browsers only give microphone access on a secure page (localhost
|
|
78
|
+
counts as secure).
|
|
79
|
+
- **Microphone permission.** The browser asks on the first press. If the
|
|
80
|
+
end-user refuses, the call ends with a `mic_unavailable` error.
|
|
81
|
+
- **WebRTC.** The call is a WebRTC connection to Talk2View's voice service. A
|
|
82
|
+
network that blocks WebRTC fails with `transport_error`.
|
|
83
|
+
- **Content-Security-Policy.** Your bundler puts the Pipecat client libraries
|
|
84
|
+
in a separate chunk that loads from your own origin on the first press. The
|
|
85
|
+
call then connects to the voice service URL returned by the engine, so
|
|
86
|
+
`connect-src` must allow that host as well as your engine.
|
|
87
|
+
|
|
88
|
+
A chat-only integration never loads the Pipecat libraries.
|