@talk2view/sdk 0.13.0 → 0.15.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 +291 -69
- package/dist/assistant-ui/convert.d.ts +40 -0
- package/dist/assistant-ui/convert.d.ts.map +1 -0
- package/dist/assistant-ui/convert.js +113 -0
- package/dist/assistant-ui/convert.js.map +1 -0
- package/dist/assistant-ui/dictation.d.ts +39 -0
- package/dist/assistant-ui/dictation.d.ts.map +1 -0
- package/dist/assistant-ui/dictation.js +128 -0
- package/dist/assistant-ui/dictation.js.map +1 -0
- package/dist/assistant-ui/index.d.ts +37 -0
- package/dist/assistant-ui/index.d.ts.map +1 -0
- package/dist/assistant-ui/index.js +224 -0
- package/dist/assistant-ui/index.js.map +1 -0
- package/dist/auth.d.ts +26 -4
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +82 -17
- package/dist/auth.js.map +1 -1
- package/dist/client.d.ts +1 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +4 -4
- package/dist/client.js.map +1 -1
- package/dist/errors.d.ts +5 -3
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +28 -6
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +63 -15
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +231 -37
- package/dist/index.js.map +1 -1
- package/dist/react/T2VProvider.d.ts +2 -2
- package/dist/react/T2VProvider.d.ts.map +1 -1
- package/dist/react/T2VProvider.js +1 -1
- package/dist/react/T2VProvider.js.map +1 -1
- package/dist/react/index.d.ts +14 -14
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +7 -7
- package/dist/react/index.js.map +1 -1
- package/dist/react/usePartnerConfig.d.ts +1 -1
- package/dist/react/usePartnerConfig.d.ts.map +1 -1
- package/dist/react/usePartnerConfig.js +1 -1
- package/dist/react/usePartnerConfig.js.map +1 -1
- package/dist/react/useT2VAuth.d.ts +9 -2
- package/dist/react/useT2VAuth.d.ts.map +1 -1
- package/dist/react/useT2VAuth.js +26 -7
- package/dist/react/useT2VAuth.js.map +1 -1
- package/dist/react/useT2VChat.d.ts +3 -3
- package/dist/react/useT2VChat.d.ts.map +1 -1
- package/dist/react/useT2VChat.js +1 -1
- package/dist/react/useT2VChat.js.map +1 -1
- package/dist/react/useT2VTools.d.ts +1 -1
- package/dist/react/useT2VTools.d.ts.map +1 -1
- package/dist/react/useT2VTools.js +1 -1
- package/dist/react/useT2VTools.js.map +1 -1
- package/dist/react/useUserPreferences.d.ts +1 -1
- package/dist/react/useUserPreferences.d.ts.map +1 -1
- package/dist/react/useUserPreferences.js +1 -1
- package/dist/react/useUserPreferences.js.map +1 -1
- package/dist/sessions.d.ts +3 -3
- package/dist/sessions.d.ts.map +1 -1
- package/dist/skills.d.ts +2 -2
- package/dist/skills.d.ts.map +1 -1
- package/dist/streaming.d.ts +1 -1
- package/dist/streaming.d.ts.map +1 -1
- package/dist/tools.d.ts +2 -2
- package/dist/tools.d.ts.map +1 -1
- package/dist/types.d.ts +146 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/ui/components/ApprovalCard.d.ts +1 -1
- package/dist/ui/components/ApprovalCard.d.ts.map +1 -1
- package/dist/ui/components/ChatHeader.js +1 -1
- package/dist/ui/components/ChatHeader.js.map +1 -1
- package/dist/ui/components/ChatPanel.d.ts +1 -1
- package/dist/ui/components/ChatPanel.d.ts.map +1 -1
- package/dist/ui/components/ChatPanel.js +14 -13
- package/dist/ui/components/ChatPanel.js.map +1 -1
- package/dist/ui/components/ChatWidget.d.ts +1 -1
- package/dist/ui/components/ChatWidget.d.ts.map +1 -1
- package/dist/ui/components/ChatWidget.js +2 -2
- package/dist/ui/components/ChatWidget.js.map +1 -1
- package/dist/ui/components/Composer.d.ts.map +1 -1
- package/dist/ui/components/Composer.js +13 -6
- package/dist/ui/components/Composer.js.map +1 -1
- package/dist/ui/components/LoginForm.d.ts.map +1 -1
- package/dist/ui/components/LoginForm.js +19 -5
- package/dist/ui/components/LoginForm.js.map +1 -1
- package/dist/ui/components/MarkdownRenderer.d.ts +4 -0
- package/dist/ui/components/MarkdownRenderer.d.ts.map +1 -1
- package/dist/ui/components/MarkdownRenderer.js +7 -35
- package/dist/ui/components/MarkdownRenderer.js.map +1 -1
- package/dist/ui/components/MessageActions.js +1 -1
- package/dist/ui/components/MessageActions.js.map +1 -1
- package/dist/ui/components/MessageBubble.d.ts +1 -1
- package/dist/ui/components/MessageBubble.d.ts.map +1 -1
- package/dist/ui/components/MessageBubble.js +9 -9
- package/dist/ui/components/MessageBubble.js.map +1 -1
- package/dist/ui/components/MessageList.js +2 -2
- package/dist/ui/components/MessageList.js.map +1 -1
- package/dist/ui/components/SettingsPanel.js +6 -6
- package/dist/ui/components/SettingsPanel.js.map +1 -1
- package/dist/ui/components/Talk2View.d.ts +1 -1
- package/dist/ui/components/Talk2View.d.ts.map +1 -1
- package/dist/ui/components/Talk2View.js +16 -12
- package/dist/ui/components/Talk2View.js.map +1 -1
- package/dist/ui/components/ThinkingBlock.js +2 -2
- package/dist/ui/components/ThinkingBlock.js.map +1 -1
- package/dist/ui/components/WelcomeScreen.js +2 -2
- package/dist/ui/components/WelcomeScreen.js.map +1 -1
- package/dist/ui/context.d.ts +4 -2
- package/dist/ui/context.d.ts.map +1 -1
- package/dist/ui/context.js.map +1 -1
- package/dist/ui/index.d.ts +27 -26
- package/dist/ui/index.d.ts.map +1 -1
- package/dist/ui/index.js +21 -20
- package/dist/ui/index.js.map +1 -1
- package/dist/ui/safeMarkdown.d.ts +17 -0
- package/dist/ui/safeMarkdown.d.ts.map +1 -0
- package/dist/ui/safeMarkdown.js +196 -0
- package/dist/ui/safeMarkdown.js.map +1 -0
- package/dist/ui/theme.d.ts +1 -1
- package/dist/ui/theme.d.ts.map +1 -1
- package/dist/ui/utils.d.ts +1 -1
- package/dist/ui/utils.d.ts.map +1 -1
- package/package.json +26 -2
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ npm install @talk2view/sdk
|
|
|
13
13
|
### React (recommended)
|
|
14
14
|
|
|
15
15
|
```tsx
|
|
16
|
-
import {
|
|
16
|
+
import { Talk2View, ChatPanel } from '@talk2view/sdk/ui';
|
|
17
17
|
import type { ClientTool } from '@talk2view/sdk';
|
|
18
18
|
|
|
19
19
|
const tools: ClientTool[] = [
|
|
@@ -36,17 +36,21 @@ const tools: ClientTool[] = [
|
|
|
36
36
|
|
|
37
37
|
function App() {
|
|
38
38
|
return (
|
|
39
|
-
<
|
|
40
|
-
<ChatPanel
|
|
41
|
-
</
|
|
39
|
+
<Talk2View partnerKey="pk_live_..." tools={tools} systemPrompt="You control a canvas.">
|
|
40
|
+
<ChatPanel />
|
|
41
|
+
</Talk2View>
|
|
42
42
|
);
|
|
43
43
|
}
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
-
That's it.
|
|
46
|
+
That's it. `<Talk2View>` registers your tools and holds the chat state; `<ChatPanel>` is the UI — sign-in, streaming replies, tool calls and approvals. Both come from `@talk2view/sdk/ui`, and `<ChatPanel>` must be inside `<Talk2View>`.
|
|
47
|
+
|
|
48
|
+
A logged-out visitor gets an anonymous demo session automatically, so there is nothing to wire up before the first reply. Pass `allowAnonymous={false}` to `<ChatPanel>` to ask people to sign in first.
|
|
47
49
|
|
|
48
50
|
### Vanilla JavaScript
|
|
49
51
|
|
|
52
|
+
The package root exports a `Talk2View` **class** — the framework-agnostic client. It shares a name with the `<Talk2View>` provider component from `/ui` above; if you need both in one file, alias one of them.
|
|
53
|
+
|
|
50
54
|
```typescript
|
|
51
55
|
import { Talk2View } from '@talk2view/sdk';
|
|
52
56
|
|
|
@@ -69,9 +73,10 @@ await t2v.tools.register([
|
|
|
69
73
|
]);
|
|
70
74
|
|
|
71
75
|
// 3. Chat — tool calls are handled automatically
|
|
76
|
+
let reply = '';
|
|
72
77
|
for await (const event of t2v.chat('What time is it?')) {
|
|
73
|
-
if (event.type === 'text')
|
|
74
|
-
if (event.type === 'done') console.log(
|
|
78
|
+
if (event.type === 'text') reply += event.content;
|
|
79
|
+
if (event.type === 'done') console.log(reply);
|
|
75
80
|
}
|
|
76
81
|
```
|
|
77
82
|
|
|
@@ -124,49 +129,78 @@ Tools with `return_direct: true` skip the AI's post-processing step. The tool re
|
|
|
124
129
|
|
|
125
130
|
## React API
|
|
126
131
|
|
|
127
|
-
|
|
132
|
+
There are three React entry points, and they do different jobs:
|
|
133
|
+
|
|
134
|
+
- **`@talk2view/sdk/ui`** — the chat UI: `<Talk2View>`, `<ChatPanel>`, `<ChatWidget>` and the components they are built from. Start here.
|
|
135
|
+
- **`@talk2view/sdk/react`** — headless: `<T2VProvider>` and hooks, for building your own UI.
|
|
136
|
+
- **`@talk2view/sdk/assistant-ui`** — a runtime for [assistant-ui](https://www.assistant-ui.com): render the stock `<Thread />` on Talk2View. See [assistant-ui](#assistant-ui) below.
|
|
128
137
|
|
|
129
|
-
|
|
138
|
+
### `<Talk2View>` (from `/ui`)
|
|
139
|
+
|
|
140
|
+
The root provider for the UI components. It creates the client, registers your tools, injects the theme, and holds the chat state every `/ui` component reads.
|
|
130
141
|
|
|
131
142
|
```tsx
|
|
132
|
-
<
|
|
133
|
-
partnerKey="pk_live_..."
|
|
134
|
-
|
|
135
|
-
|
|
143
|
+
<Talk2View
|
|
144
|
+
partnerKey="pk_live_..." // Required
|
|
145
|
+
tools={myTools} // Optional — ClientTool[] (with execute) or schemas
|
|
146
|
+
systemPrompt="You are..." // Optional — sent with every message
|
|
147
|
+
baseUrl="https://engine.talk2view.com" // Optional, this is the default
|
|
148
|
+
model="gpt-4.1-mini" // Optional, falls back to your partner default
|
|
149
|
+
theme={{ accent: '#26C8B8' }} // Optional
|
|
150
|
+
debug={false} // Optional — log to the browser console
|
|
136
151
|
>
|
|
137
152
|
{children}
|
|
138
|
-
</
|
|
153
|
+
</Talk2View>
|
|
139
154
|
```
|
|
140
155
|
|
|
141
|
-
### `<ChatPanel>`
|
|
156
|
+
### `<ChatPanel>` (from `/ui`)
|
|
142
157
|
|
|
143
|
-
|
|
158
|
+
The full chat surface: header, message list, composer, sign-in, tool approvals. It takes its client and state from `<Talk2View>`, so it must be rendered inside one — on its own it throws `useTalk2View must be used within <Talk2View>`. Note that `tools` and `systemPrompt` belong on `<Talk2View>`, not here.
|
|
144
159
|
|
|
145
160
|
```tsx
|
|
146
161
|
<ChatPanel
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
signupUrl="https://..."
|
|
150
|
-
|
|
151
|
-
|
|
162
|
+
welcome={{ heading: 'Ask about this document', suggestions: ['Summarise it'] }}
|
|
163
|
+
allowAnonymous={true} // Default. false = require sign-in before chatting
|
|
164
|
+
signupUrl="https://..." // Link shown on the sign-in form
|
|
165
|
+
resetPasswordUrl="https://..." // Adds a "Forgot password?" link
|
|
166
|
+
resetPasswordTarget="_blank" // Default. '_self' opens in your own app
|
|
167
|
+
describeToolActivity={(name, args) => (name === 'set_color' ? 'Recolouring' : null)}
|
|
168
|
+
isToolDestructive={(name) => name.startsWith('delete_')}
|
|
169
|
+
groupAssistantMessages={false}
|
|
152
170
|
/>
|
|
153
171
|
```
|
|
154
172
|
|
|
155
|
-
|
|
173
|
+
By default a logged-out visitor is signed into an anonymous demo session rather than being shown a login form. The sign-in form appears when `allowAnonymous` is `false`, when your partner account has anonymous access switched off or its daily cap is spent, or when the demo budget for that visitor runs out.
|
|
156
174
|
|
|
157
|
-
### `<
|
|
175
|
+
### `<LoginForm>` (from `/ui`)
|
|
158
176
|
|
|
159
|
-
|
|
177
|
+
The sign-in form on its own, if you want to place it yourself. It renders unconditionally — you decide when to show it (check `isAuthenticated` from `useTalk2View()`, the `/ui` hook).
|
|
160
178
|
|
|
161
179
|
```tsx
|
|
162
|
-
<
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
180
|
+
<LoginForm
|
|
181
|
+
heading="Sign in"
|
|
182
|
+
subheading="to keep your chat history"
|
|
183
|
+
defaultMode="login" // or 'signup'
|
|
184
|
+
signupUrl="https://talk2view.com/auth?signup"
|
|
185
|
+
resetPasswordUrl="https://..."
|
|
186
|
+
termsUrl="https://..."
|
|
187
|
+
privacyUrl="https://..."
|
|
166
188
|
/>
|
|
167
189
|
```
|
|
168
190
|
|
|
169
|
-
|
|
191
|
+
### `<T2VProvider>` (from `/react`)
|
|
192
|
+
|
|
193
|
+
The headless provider. Use it when you are building your own UI on the hooks below; `<Talk2View>` already includes it, so you never need both.
|
|
194
|
+
|
|
195
|
+
```tsx
|
|
196
|
+
<T2VProvider
|
|
197
|
+
partnerKey="pk_live_..." // Required
|
|
198
|
+
baseUrl="https://engine.talk2view.com" // Optional, this is the default
|
|
199
|
+
model="gpt-4.1-mini" // Optional, uses your partner default
|
|
200
|
+
>
|
|
201
|
+
{children}
|
|
202
|
+
</T2VProvider>
|
|
203
|
+
```
|
|
170
204
|
|
|
171
205
|
### `useT2V()`
|
|
172
206
|
|
|
@@ -187,30 +221,45 @@ const {
|
|
|
187
221
|
isLoading, // boolean
|
|
188
222
|
error, // string | null
|
|
189
223
|
login, // (email: string, password: string) => Promise<void>
|
|
190
|
-
signup, // (email
|
|
224
|
+
signup, // (email, password) => Promise<{ user, confirmationRequired }>
|
|
191
225
|
logout, // () => Promise<void>
|
|
192
226
|
clearError, // () => void
|
|
227
|
+
signInWithGoogle, // () => Promise<void> — opens the Google sign-in popup
|
|
228
|
+
signInWithApple, // () => Promise<void> — opens the Apple sign-in popup
|
|
229
|
+
oauthLoading, // boolean — true while either popup is open
|
|
230
|
+
oauthProvider, // 'google' | 'apple' | null — which one
|
|
231
|
+
oauthProviders, // ('google' | 'apple')[] | null — popup sign-ins available on this website; null while loading
|
|
193
232
|
} = useT2VAuth();
|
|
194
233
|
```
|
|
195
234
|
|
|
235
|
+
`signup()` resolves to a `SignupOutcome`: `{ user: User | null; confirmationRequired: boolean }`. When your project has email confirmation switched on, `confirmationRequired` is `true` and `user` is `null` until the person clicks the link in their inbox.
|
|
236
|
+
|
|
237
|
+
Google and Apple sign-in open a popup and work on any `https://` website, with nothing to configure — your users have Talk2View accounts, and the sign-in form is the same in every app. Your publishable key is public, though, so on a website you have **not** registered your users first see a Talk2View screen naming the site that is asking, and press Continue. Add your site under **Settings → Allowed websites** in the dashboard (the exact `https://` address, nothing after the domain) and that screen goes away; the SDK logs one line to the console when it applies. If your account is set to allow only registered websites, `<LoginForm>` hides the two buttons elsewhere. Popup sign-in never runs on `localhost`; use email and password while developing.
|
|
238
|
+
|
|
196
239
|
### `useT2VChat()`
|
|
197
240
|
|
|
198
241
|
Chat state and message sending.
|
|
199
242
|
|
|
200
243
|
```tsx
|
|
201
244
|
const {
|
|
202
|
-
messages,
|
|
203
|
-
isLoading,
|
|
204
|
-
error,
|
|
205
|
-
threadId,
|
|
206
|
-
agentStatus,
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
245
|
+
messages, // DisplayMessage[]
|
|
246
|
+
isLoading, // boolean — true while streaming
|
|
247
|
+
error, // string | null
|
|
248
|
+
threadId, // string | null
|
|
249
|
+
agentStatus, // { status: string; message: string } | null
|
|
250
|
+
pendingApproval, // PendingApproval | null — a tool call awaiting a decision
|
|
251
|
+
alwaysAllowedTools, // ReadonlySet<string> — approved for the rest of the session
|
|
252
|
+
sendMessage, // (content, { attachments? }) => Promise<void>
|
|
253
|
+
approveToolCall, // (decision: HumanDecision) => Promise<void>
|
|
254
|
+
retryLastMessage, // () => Promise<void> — only meaningful while error is set
|
|
255
|
+
stop, // () => void — ends the reply, keeps the text so far
|
|
256
|
+
clearMessages, // () => void
|
|
257
|
+
clearError, // () => void
|
|
211
258
|
} = useT2VChat({ systemPrompt: 'You are a helpful assistant.' });
|
|
212
259
|
```
|
|
213
260
|
|
|
261
|
+
When a tool is registered with `permission: true`, the agent pauses and `pendingApproval` fills in. Show it, then call `approveToolCall({ action: 'once' | 'always' | 'deny' })` — `'always'` adds the tool to `alwaysAllowedTools` for the rest of the session. `<ChatPanel>` already does all of this; you only need it when building your own UI.
|
|
262
|
+
|
|
214
263
|
`DisplayMessage` shape:
|
|
215
264
|
```typescript
|
|
216
265
|
{
|
|
@@ -218,7 +267,10 @@ const {
|
|
|
218
267
|
role: 'user' | 'assistant'
|
|
219
268
|
content: string
|
|
220
269
|
timestamp: Date
|
|
221
|
-
isStreaming?: boolean
|
|
270
|
+
isStreaming?: boolean // true while the assistant is still generating
|
|
271
|
+
attachments?: Attachment[] // files sent with a user message
|
|
272
|
+
plan?: string // markdown checklist from the agent's planning tool
|
|
273
|
+
steps?: ToolStep[] // completed tool calls, rendered as inline steps
|
|
222
274
|
}
|
|
223
275
|
```
|
|
224
276
|
|
|
@@ -236,6 +288,86 @@ const {
|
|
|
236
288
|
|
|
237
289
|
---
|
|
238
290
|
|
|
291
|
+
## assistant-ui
|
|
292
|
+
|
|
293
|
+
assistant-ui already has a seam other backends plug into at the runtime level — `@assistant-ui/react-ai-sdk` for the Vercel AI SDK is the well-known example. `useTalk2ViewRuntime` is Talk2View's equivalent: it hands assistant-ui an `AssistantRuntime`, built on `useExternalStoreRuntime`, so the stock `<Thread />` (or the primitives) render Talk2View's messages, client tools, approvals and streaming — with no Talk2View-specific component in the tree.
|
|
294
|
+
|
|
295
|
+
This is a separate entry point from `/ui` and `/react`: `@assistant-ui/react` is an optional peer dependency, loaded only when you import `@talk2view/sdk/assistant-ui`. It never affects the root, `/react` or `/ui` bundles.
|
|
296
|
+
|
|
297
|
+
### Install
|
|
298
|
+
|
|
299
|
+
```bash
|
|
300
|
+
npm install @assistant-ui/react
|
|
301
|
+
npx shadcn@latest init --yes --defaults
|
|
302
|
+
npx shadcn@latest add @assistant-ui/thread --yes
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
The `shadcn` commands generate the Thread's source into your own `src/components/` — they're your files, not a Talk2View package, so you can restyle or extend them freely. See `examples/react-assistant-ui` for a full app built this way, including the exact generated output.
|
|
306
|
+
|
|
307
|
+
One known wrinkle in the registry output at the time of writing: `tooltip-icon-button.tsx` passes `delayDuration={0}` to a `TooltipProvider` whose prop is `delay`. It is inert at runtime, but it fails `tsc`, so if your build type-checks (most Vite templates run `tsc -b && vite build`) change it to `delay={0}` — the example does.
|
|
308
|
+
|
|
309
|
+
### `useTalk2ViewRuntime`
|
|
310
|
+
|
|
311
|
+
```tsx
|
|
312
|
+
import { AssistantRuntimeProvider } from '@assistant-ui/react';
|
|
313
|
+
import { useTalk2ViewRuntime } from '@talk2view/sdk/assistant-ui';
|
|
314
|
+
|
|
315
|
+
function Chat() {
|
|
316
|
+
const runtime = useTalk2ViewRuntime({ partnerKey: 'pk_live_...', tools });
|
|
317
|
+
return (
|
|
318
|
+
<AssistantRuntimeProvider runtime={runtime}>
|
|
319
|
+
<Thread />
|
|
320
|
+
</AssistantRuntimeProvider>
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
`useTalk2ViewRuntime(options)` takes the same connection options as `<T2VProvider>` (`partnerKey`, `baseUrl`, `model`, `debug`, `anonymousAutoStart`) plus `tools` and `systemPrompt`, and creates its own `Talk2View` client — a new one only when `partnerKey`, `baseUrl`, `debug` or `anonymousAutoStart` changes (`requestTimeout` is read once, at creation). `model` is sent with each message rather than baked into the client, so an end-user can switch models from a settings screen without losing the chat. `tools` are registered with the engine when their schemas change, compared by value, so an inline array is fine.
|
|
326
|
+
|
|
327
|
+
### `useTalk2ViewRuntimeForClient`
|
|
328
|
+
|
|
329
|
+
For an app that already has a `Talk2View` client — for example from `<T2VProvider>`, so auth and chat state stay shared with the rest of the app — pass it in instead of creating a second one:
|
|
330
|
+
|
|
331
|
+
```tsx
|
|
332
|
+
import { AssistantRuntimeProvider } from '@assistant-ui/react';
|
|
333
|
+
import { useT2V } from '@talk2view/sdk/react';
|
|
334
|
+
import { useTalk2ViewRuntimeForClient } from '@talk2view/sdk/assistant-ui';
|
|
335
|
+
|
|
336
|
+
function Chat() {
|
|
337
|
+
const { t2v } = useT2V(); // from <T2VProvider>
|
|
338
|
+
const runtime = useTalk2ViewRuntimeForClient(t2v, { tools });
|
|
339
|
+
return (
|
|
340
|
+
<AssistantRuntimeProvider runtime={runtime}>
|
|
341
|
+
<Thread />
|
|
342
|
+
</AssistantRuntimeProvider>
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
### What maps to what
|
|
348
|
+
|
|
349
|
+
| assistant-ui | Talk2View SDK |
|
|
350
|
+
| --- | --- |
|
|
351
|
+
| Streaming replies | `DisplayMessage`s from `t2v.messages`, converted per-message by `toThreadMessage()` |
|
|
352
|
+
| Client tools | `tools` registers schemas via `t2v.tools.register()` and handlers via `t2v.tools.handle()` — same as `<Talk2View tools>` |
|
|
353
|
+
| Tool approvals | A `PendingApproval` becomes assistant-ui's own approval card (`allow-once` / `allow-always` / `reject-once`, plus a free-form reason); the decision is sent through `t2v.approveToolCall()` |
|
|
354
|
+
| Attachments | assistant-ui's `AttachmentAdapter` uploads through `t2v.uploadAttachment()` |
|
|
355
|
+
| Stop | assistant-ui's cancel action calls `t2v.stop()` — or, while a tool call is waiting for a decision, denies it with the reason "Cancelled by the user" |
|
|
356
|
+
| Retry | assistant-ui's reload action on the **latest turn** — its reply, or the error card when it failed — calls `t2v.retryLastMessage()`; on an older reply it does nothing, because the SDK can only regenerate the last turn |
|
|
357
|
+
|
|
358
|
+
Four things do not map, on purpose:
|
|
359
|
+
|
|
360
|
+
- **Editing tool arguments before approval.** The SDK supports it (`HumanDecision.updatedInput`, used by `<ApprovalCard>` in `/ui`), but the stock assistant-ui approval card has no edit-and-resubmit affordance to drive it from — it can only approve, deny, or attach a free-form reason.
|
|
361
|
+
- **Sign-in UI.** `useTalk2ViewRuntime` only creates a client and a runtime; there is no `<LoginForm>` equivalent. A logged-out visitor gets an anonymous demo session automatically (same default as `<ChatPanel>`), and building your own sign-in UI, or gating the app before it mounts, is on the partner.
|
|
362
|
+
- **The agent's plan.** The planning checklist `<ChatPanel>` shows arrives as a `data` part named `plan` (`{ markdown }`). The stock Thread has no renderer for it, so it is silently not shown; register one to see it: `<MessagePrimitive.Parts components={{ data: { by_name: { plan: PlanView } } }} />`.
|
|
363
|
+
- **Editing and deleting sent messages.** The runtime supplies no `onEdit`/`onDelete`, so the stock Thread's pencil on user messages stays disabled. Remove it from your copy of the Thread if you'd rather not show it.
|
|
364
|
+
|
|
365
|
+
### Bundle size
|
|
366
|
+
|
|
367
|
+
`<Thread />` and its dependencies are the partner's own assistant-ui bundle, sized however assistant-ui and its registry components size it; `@talk2view/sdk/assistant-ui` itself adds about 1.9 KB gzipped on top (`dist/assistant-ui/{index,convert}.js`, bundled and minified with esbuild, `react`, `react-dom`, `@assistant-ui/react` and the SDK's own `Talk2View` client marked external since a partner already has them).
|
|
368
|
+
|
|
369
|
+
---
|
|
370
|
+
|
|
239
371
|
## Core API (framework-agnostic)
|
|
240
372
|
|
|
241
373
|
### `Talk2View`
|
|
@@ -245,7 +377,7 @@ Main entry point. Use this directly for vanilla JS or non-React frameworks.
|
|
|
245
377
|
```typescript
|
|
246
378
|
const t2v = new Talk2View({
|
|
247
379
|
partnerKey: 'pk_live_...', // Required
|
|
248
|
-
baseUrl: 'https://
|
|
380
|
+
baseUrl: 'https://engine.talk2view.com', // Optional, this is the default
|
|
249
381
|
model: 'gpt-4.1-mini', // Optional
|
|
250
382
|
});
|
|
251
383
|
```
|
|
@@ -254,7 +386,7 @@ const t2v = new Talk2View({
|
|
|
254
386
|
|
|
255
387
|
```typescript
|
|
256
388
|
await t2v.auth.login(email, password) // Returns User
|
|
257
|
-
await t2v.auth.signup(email, password) // Returns
|
|
389
|
+
await t2v.auth.signup(email, password) // Returns { user, confirmationRequired }
|
|
258
390
|
await t2v.auth.logout()
|
|
259
391
|
t2v.auth.getUser() // Returns User | null
|
|
260
392
|
t2v.auth.isAuthenticated() // Returns boolean
|
|
@@ -358,6 +490,24 @@ for await (const event of t2v.chat('Zoom in please')) {
|
|
|
358
490
|
}
|
|
359
491
|
```
|
|
360
492
|
|
|
493
|
+
#### `t2v.warmUp()`
|
|
494
|
+
|
|
495
|
+
A new end-user's first AI call is slower than the rest: the engine sets up their
|
|
496
|
+
account with the model proxy on first use, which takes a few seconds. Call
|
|
497
|
+
`warmUp()` at the first sign they are going to use the chat — their first
|
|
498
|
+
keystroke, a tap on the mic — and that setup happens while they are still typing.
|
|
499
|
+
|
|
500
|
+
```ts
|
|
501
|
+
input.addEventListener('input', () => t2v.warmUp(), { once: true });
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
It is safe to call repeatedly (one request per signed-in end-user), never
|
|
505
|
+
throws, and is a no-op against an engine that predates it. Don't call it when
|
|
506
|
+
the chat merely opens: for a logged-out visitor it starts an anonymous session,
|
|
507
|
+
which counts toward your daily anonymous cap.
|
|
508
|
+
|
|
509
|
+
`<ChatWidget>`, `<ChatPanel>` and the assistant-ui runtime already do this.
|
|
510
|
+
|
|
361
511
|
#### `t2v.clearSession()`
|
|
362
512
|
|
|
363
513
|
Reset the current session. Clears the thread ID so the next `chat()` call starts a fresh conversation.
|
|
@@ -379,8 +529,15 @@ const audioModels = await t2v.listAudioModels(); // Speech-to-text models
|
|
|
379
529
|
|
|
380
530
|
Transcribe an audio file using the server's speech-to-text service:
|
|
381
531
|
|
|
532
|
+
Takes a `FormData`, with the fields the engine's `/v1/audio/transcriptions` endpoint accepts (`file`, `model`, and optionally `language`, `translate_to`, `translation_guidance`):
|
|
533
|
+
|
|
382
534
|
```typescript
|
|
383
|
-
const
|
|
535
|
+
const form = new FormData();
|
|
536
|
+
form.append('file', audioBlob, 'recording.webm');
|
|
537
|
+
form.append('model', 'faster-whisper-large-turbo-gcp'); // one of listAudioModels()
|
|
538
|
+
form.append('language', 'en'); // optional
|
|
539
|
+
|
|
540
|
+
const result = await t2v.transcribe(form);
|
|
384
541
|
console.log(result.text);
|
|
385
542
|
```
|
|
386
543
|
|
|
@@ -492,7 +649,7 @@ Use `enum` to restrict string values:
|
|
|
492
649
|
The `execute` function receives the AI's arguments as a `Record<string, unknown>` and must return a `Promise<string>`. The returned string is sent back to the AI as the tool's result.
|
|
493
650
|
|
|
494
651
|
- Return `JSON.stringify(...)` for structured results
|
|
495
|
-
- Throw an error or return an error string if the tool fails — the SDK catches
|
|
652
|
+
- Throw an error or return an error string if the tool fails — the SDK catches it and sends the message back to the AI as `{ result: '{"error":"..."}', is_error: true }`, so the agent can explain or try something else
|
|
496
653
|
|
|
497
654
|
---
|
|
498
655
|
|
|
@@ -523,22 +680,62 @@ To mitigate this:
|
|
|
523
680
|
|
|
524
681
|
---
|
|
525
682
|
|
|
683
|
+
## Rendering model replies
|
|
684
|
+
|
|
685
|
+
Treat every model reply as untrusted. Prompt injection through web search, documents, email or tool results can make the model write anything. `<ChatPanel>` renders replies as markdown only, and never loads remote content on its own:
|
|
686
|
+
|
|
687
|
+
- **Raw HTML** in a reply is shown as text, apart from a bare `<br>`, which is kept so a GFM table cell can break a line (a cell can't hold a literal newline).
|
|
688
|
+
- **Images** become links (`Image: <alt> (<host>)`) that the end-user can choose to open.
|
|
689
|
+
- **Links** open in a new tab with `noopener noreferrer`, and only `http(s)` and `mailto` link destinations are kept.
|
|
690
|
+
|
|
691
|
+
If you build your own chat UI, render replies the same way:
|
|
692
|
+
|
|
693
|
+
```tsx
|
|
694
|
+
import { renderSafeMarkdown } from '@talk2view/sdk/ui';
|
|
695
|
+
|
|
696
|
+
<div dangerouslySetInnerHTML={{ __html: renderSafeMarkdown(reply) }} />
|
|
697
|
+
```
|
|
698
|
+
|
|
699
|
+
Never pass model output to `innerHTML` or a markdown library's raw HTML output without it.
|
|
700
|
+
|
|
701
|
+
Call it in the browser (client components): without a DOM it returns the reply as escaped plain text.
|
|
702
|
+
|
|
703
|
+
## Chat sessions and deploys
|
|
704
|
+
|
|
705
|
+
A chat session lives in the engine's memory, so a Talk2View deploy, a restart, or eviction under load can end it. The SDK handles most of that for you:
|
|
706
|
+
|
|
707
|
+
- **Sending a message:** if the session is gone before the reply has started, the SDK opens a new one, re-registers your tools and resends the turn with the conversation so far — nothing from that turn is lost. It emits `sessionRecovered` with the new session id, which you can log.
|
|
708
|
+
- **Answering an approval:** the SDK can't safely resend the tool result, because your handler has already run. It emits one `error` event with `errorType: 'session_lost'` and clears the pending approval, so the end-user can send their message again.
|
|
709
|
+
- **Mid-reply or mid-tool-resume:** if the chat session is lost after the reply has started, or while a tool result is being sent back, the SDK reports `session_lost` and doesn't resend, because the tool handler may already have run.
|
|
710
|
+
|
|
711
|
+
```ts
|
|
712
|
+
t2v.on('sessionRecovered', (sessionId) => console.info('reconnected', sessionId));
|
|
713
|
+
```
|
|
714
|
+
|
|
715
|
+
This is why `sendMessage()` and `chat()` take the conversation history: the engine works out which messages are new, so a fresh session continues where the old one stopped.
|
|
716
|
+
|
|
526
717
|
## Error Handling
|
|
527
718
|
|
|
528
719
|
The SDK throws typed errors:
|
|
529
720
|
|
|
530
|
-
| Error Class | When |
|
|
531
|
-
|
|
532
|
-
| `AuthenticationError` |
|
|
533
|
-
| `PartnerKeyError` |
|
|
534
|
-
| `
|
|
535
|
-
| `
|
|
536
|
-
| `
|
|
721
|
+
| Error Class | When | `err.type` |
|
|
722
|
+
|-------------|------|------------|
|
|
723
|
+
| `AuthenticationError` | Bad credentials, expired or rejected token | `authentication_error` |
|
|
724
|
+
| `PartnerKeyError` | Unknown or inactive partner key | `partner_key_error` |
|
|
725
|
+
| `NetworkError` | Network failure, server unreachable, request timed out | `network_error` |
|
|
726
|
+
| `T2VError` | Base class — and what everything else arrives as | the engine's own type |
|
|
727
|
+
| `SessionError` | Reserved — see below | `session_not_found` |
|
|
728
|
+
|
|
729
|
+
Every error carries the engine's machine-readable `type`, so `err.type` is the precise test; the classes above are a convenience for the common cases. Types worth knowing that arrive as a plain `T2VError`: `origin_not_allowed` (403 — the request's Origin isn't on your key's allow-list), `not_found` (404), and `rate_limit_exceeded` (429).
|
|
730
|
+
|
|
731
|
+
Some failures never reach a `catch` at all, because they arrive inside the chat stream rather than as a rejected request: a spent anonymous demo budget comes through as an `error` event with `errorType: 'budget_exceeded'`, which the SDK turns into a `demoLimitReached` event rather than an error state. Listen for that instead of trying to catch it.
|
|
537
732
|
|
|
538
|
-
|
|
733
|
+
**On lost chat sessions:** `SessionError` is reserved for a dedicated engine type that isn't in use yet — today a chat session that no longer exists comes back as a `T2VError` with `type: 'not_found'` and `statusCode: 404`. You shouldn't need to catch it: the SDK recovers the session itself where that is safe, and otherwise reports an `error` event with `errorType: 'session_lost'`. See [Chat sessions and deploys](#chat-sessions-and-deploys).
|
|
734
|
+
|
|
735
|
+
All errors extend `T2VError`, which exposes `message`, `type`, `statusCode`, `code`, and `detail` (the server's raw diagnostic text, kept out of `message` so it never reaches an end-user by accident):
|
|
539
736
|
|
|
540
737
|
```typescript
|
|
541
|
-
import { T2VError, AuthenticationError, NetworkError,
|
|
738
|
+
import { T2VError, AuthenticationError, NetworkError, PartnerKeyError } from '@talk2view/sdk';
|
|
542
739
|
|
|
543
740
|
try {
|
|
544
741
|
await t2v.auth.login(email, password);
|
|
@@ -547,8 +744,8 @@ try {
|
|
|
547
744
|
console.log('Bad credentials');
|
|
548
745
|
} else if (err instanceof NetworkError) {
|
|
549
746
|
console.log('Server unreachable');
|
|
550
|
-
} else if (err instanceof
|
|
551
|
-
console.log('
|
|
747
|
+
} else if (err instanceof PartnerKeyError) {
|
|
748
|
+
console.log('Check your partner key:', err.message);
|
|
552
749
|
} else if (err instanceof T2VError) {
|
|
553
750
|
// Access the error code for programmatic handling
|
|
554
751
|
console.log(`Error [${err.code}]: ${err.message} (HTTP ${err.statusCode})`);
|
|
@@ -575,18 +772,25 @@ const { error: chatError } = useT2VChat({ systemPrompt: '...' });
|
|
|
575
772
|
| Property | Type | Required | Default | Description |
|
|
576
773
|
|----------|------|----------|---------|-------------|
|
|
577
774
|
| `partnerKey` | `string` | Yes | — | Your partner API key |
|
|
578
|
-
| `baseUrl` | `string` | No | `'
|
|
775
|
+
| `baseUrl` | `string` | No | `'https://engine.talk2view.com'` | Talk2View engine URL |
|
|
579
776
|
| `model` | `string` | No | Server default | LLM model to use |
|
|
580
777
|
| `requestTimeout` | `number` | No | `30000` | HTTP request timeout in ms. Does not apply to SSE streams after connection. |
|
|
778
|
+
| `anonymousAutoStart` | `boolean` | No | `true` | Start an anonymous demo session on the first `chat()` when nobody is signed in. Set `false` to require sign-in. |
|
|
779
|
+
| `debug` | `boolean` | No | `false` | Log streaming events, tool calls, history and state changes to the console. |
|
|
581
780
|
|
|
582
781
|
---
|
|
583
782
|
|
|
584
783
|
## System Prompts
|
|
585
784
|
|
|
586
|
-
System prompts set the AI's behavior and context for your application. Pass
|
|
785
|
+
System prompts set the AI's behavior and context for your application. Pass one to `<Talk2View>` (the UI components) or to `useT2VChat` (the hooks):
|
|
587
786
|
|
|
588
787
|
```tsx
|
|
589
|
-
<
|
|
788
|
+
<Talk2View
|
|
789
|
+
partnerKey="pk_live_..."
|
|
790
|
+
systemPrompt="You are a medical imaging assistant. Use the provided tools to control the DICOM viewer."
|
|
791
|
+
>
|
|
792
|
+
<ChatPanel />
|
|
793
|
+
</Talk2View>
|
|
590
794
|
```
|
|
591
795
|
|
|
592
796
|
Best practices:
|
|
@@ -603,7 +807,7 @@ Here's a complete example integrating Talk2View into a hypothetical drawing app:
|
|
|
603
807
|
|
|
604
808
|
```tsx
|
|
605
809
|
import React, { useRef, useMemo } from 'react';
|
|
606
|
-
import {
|
|
810
|
+
import { Talk2View, ChatPanel } from '@talk2view/sdk/ui';
|
|
607
811
|
import type { ClientTool } from '@talk2view/sdk';
|
|
608
812
|
import { Canvas } from './Canvas';
|
|
609
813
|
|
|
@@ -656,16 +860,18 @@ function App() {
|
|
|
656
860
|
], []);
|
|
657
861
|
|
|
658
862
|
return (
|
|
659
|
-
<
|
|
863
|
+
<Talk2View
|
|
864
|
+
partnerKey="pk_live_..."
|
|
865
|
+
tools={tools}
|
|
866
|
+
systemPrompt="You control a drawing canvas. Use the tools to draw shapes and manage the canvas."
|
|
867
|
+
>
|
|
660
868
|
<div style={{ display: 'flex', height: '100vh' }}>
|
|
661
869
|
<Canvas ref={canvasRef} style={{ flex: 1 }} />
|
|
662
|
-
<
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
style={{ width: 360 }}
|
|
666
|
-
/>
|
|
870
|
+
<div style={{ width: 360 }}>
|
|
871
|
+
<ChatPanel welcome={{ heading: 'What should I draw?' }} />
|
|
872
|
+
</div>
|
|
667
873
|
</div>
|
|
668
|
-
</
|
|
874
|
+
</Talk2View>
|
|
669
875
|
);
|
|
670
876
|
}
|
|
671
877
|
```
|
|
@@ -694,16 +900,32 @@ import type {
|
|
|
694
900
|
RegisterSkillsResponse,
|
|
695
901
|
} from '@talk2view/sdk';
|
|
696
902
|
|
|
697
|
-
// React types
|
|
903
|
+
// Headless React types
|
|
698
904
|
import type {
|
|
699
905
|
T2VProviderProps,
|
|
700
906
|
UseT2VAuthResult,
|
|
701
907
|
UseT2VChatResult,
|
|
702
908
|
UseT2VToolsResult,
|
|
909
|
+
UsePartnerConfigResult,
|
|
910
|
+
UseUserPreferencesResult,
|
|
703
911
|
DisplayMessage,
|
|
704
|
-
|
|
705
|
-
|
|
912
|
+
ToolStep,
|
|
913
|
+
PendingApproval,
|
|
706
914
|
} from '@talk2view/sdk/react';
|
|
915
|
+
|
|
916
|
+
// UI component types
|
|
917
|
+
import type {
|
|
918
|
+
Talk2ViewProps,
|
|
919
|
+
ChatPanelProps,
|
|
920
|
+
ChatWidgetProps,
|
|
921
|
+
} from '@talk2view/sdk/ui';
|
|
922
|
+
|
|
923
|
+
// assistant-ui runtime
|
|
924
|
+
import { useTalk2ViewRuntime, useTalk2ViewRuntimeForClient } from '@talk2view/sdk/assistant-ui';
|
|
925
|
+
import type {
|
|
926
|
+
UseTalk2ViewRuntimeOptions, // useTalk2ViewRuntime(options)
|
|
927
|
+
Talk2ViewRuntimeOptions, // useTalk2ViewRuntimeForClient(t2v, options)
|
|
928
|
+
} from '@talk2view/sdk/assistant-ui';
|
|
707
929
|
```
|
|
708
930
|
|
|
709
931
|
## Publishing
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure mapping from the SDK's chat state onto assistant-ui's message shape.
|
|
3
|
+
*
|
|
4
|
+
* assistant-ui renders `ThreadMessageLike`; the SDK keeps `DisplayMessage`.
|
|
5
|
+
* Everything here is a function of (message, surrounding state) so it can be
|
|
6
|
+
* unit-tested without React or a runtime.
|
|
7
|
+
*/
|
|
8
|
+
import type { ThreadMessageLike } from '@assistant-ui/react';
|
|
9
|
+
import type { DisplayMessage, HumanDecision, PendingApproval } from '../types.js';
|
|
10
|
+
/** What a message needs to know about the thread around it. */
|
|
11
|
+
export interface ConvertContext {
|
|
12
|
+
/** True for the newest message — the only one that can be streaming or waiting on a decision. */
|
|
13
|
+
isLast: boolean;
|
|
14
|
+
/** A tool call waiting for the end-user; rendered as an approval gate on the last assistant message. */
|
|
15
|
+
pendingApproval: PendingApproval | null;
|
|
16
|
+
/** The SDK's error state; shown on the last assistant message. */
|
|
17
|
+
error: string | null;
|
|
18
|
+
}
|
|
19
|
+
type ThreadPart = Extract<ThreadMessageLike['content'], readonly unknown[]>[number];
|
|
20
|
+
/** Option ids the stock assistant-ui approval card sends back; we map them onto `HumanDecision`. */
|
|
21
|
+
export declare const APPROVAL_OPTION_IDS: {
|
|
22
|
+
readonly once: "allow-once";
|
|
23
|
+
readonly always: "allow-always";
|
|
24
|
+
readonly deny: "reject-once";
|
|
25
|
+
};
|
|
26
|
+
/** Stable id for a completed step: steps carry no id of their own. */
|
|
27
|
+
export declare function stepToolCallId(messageId: string, index: number): string;
|
|
28
|
+
/** The approval gate assistant-ui draws for a tool call that needs a decision. */
|
|
29
|
+
export declare function approvalPart(approval: PendingApproval): ThreadPart;
|
|
30
|
+
/** Convert one SDK message for assistant-ui. */
|
|
31
|
+
export declare function toThreadMessage(msg: DisplayMessage, ctx: ConvertContext): ThreadMessageLike;
|
|
32
|
+
/** Turn the stock approval card's answer into the SDK's `HumanDecision`. */
|
|
33
|
+
export declare function toHumanDecision(response: {
|
|
34
|
+
approved: boolean;
|
|
35
|
+
optionId?: string;
|
|
36
|
+
text?: string;
|
|
37
|
+
reason?: string;
|
|
38
|
+
}): HumanDecision;
|
|
39
|
+
export {};
|
|
40
|
+
//# sourceMappingURL=convert.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"convert.d.ts","sourceRoot":"","sources":["../../src/assistant-ui/convert.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,EAAE,iBAAiB,EAAuB,MAAM,qBAAqB,CAAC;AAClF,OAAO,KAAK,EAAc,cAAc,EAAE,aAAa,EAAE,eAAe,EAAY,MAAM,aAAa,CAAC;AAExG,+DAA+D;AAC/D,MAAM,WAAW,cAAc;IAC7B,iGAAiG;IACjG,MAAM,EAAE,OAAO,CAAC;IAChB,wGAAwG;IACxG,eAAe,EAAE,eAAe,GAAG,IAAI,CAAC;IACxC,kEAAkE;IAClE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAGD,KAAK,UAAU,GAAG,OAAO,CAAC,iBAAiB,CAAC,SAAS,CAAC,EAAE,SAAS,OAAO,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC;AAEpF,oGAAoG;AACpG,eAAO,MAAM,mBAAmB;;;;CAItB,CAAC;AAQX,sEAAsE;AACtE,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAEvE;AAgBD,kFAAkF;AAClF,wBAAgB,YAAY,CAAC,QAAQ,EAAE,eAAe,GAAG,UAAU,CAelE;AAkCD,gDAAgD;AAChD,wBAAgB,eAAe,CAAC,GAAG,EAAE,cAAc,EAAE,GAAG,EAAE,cAAc,GAAG,iBAAiB,CA0B3F;AAED,4EAA4E;AAC5E,wBAAgB,eAAe,CAAC,QAAQ,EAAE;IACxC,QAAQ,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,GAAG,aAAa,CAShB"}
|