@webotme/react-native 0.1.1 → 0.1.3
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 +79 -0
- package/package.json +16 -5
- package/src/AutoFlowExecutor.tsx +19 -8
- package/src/chat.ts +371 -0
- package/src/client.ts +4 -1
- package/src/executor.ts +190 -0
- package/src/index.ts +57 -0
- package/src/lazy-webview.tsx +59 -0
- package/src/navigationAdapter.ts +117 -0
- package/src/provider.tsx +103 -76
- package/src/runtime.ts +35 -0
- package/src/safety.ts +72 -0
- package/src/scan.txt +1 -0
- package/src/screenHook.ts +82 -0
- package/src/screenScanner.tsx +382 -0
- package/src/types.ts +2 -0
- package/src/voice.ts +722 -0
- package/src/widget.tsx +3 -2
package/README.md
CHANGED
|
@@ -149,6 +149,81 @@ const { text, action } = await client.send({
|
|
|
149
149
|
});
|
|
150
150
|
```
|
|
151
151
|
|
|
152
|
+
## Voice
|
|
153
|
+
|
|
154
|
+
`voiceWithUs()` is the voice equivalent of `chatWithUs()` — one call, ask out
|
|
155
|
+
loud, hear the answer:
|
|
156
|
+
|
|
157
|
+
```tsx
|
|
158
|
+
import { voiceWithUs } from "@webotme/react-native";
|
|
159
|
+
|
|
160
|
+
const res = await voiceWithUs(); // tap the mic
|
|
161
|
+
// user speaks -> device transcribes -> bot replies -> device speaks the reply
|
|
162
|
+
console.log(res.transcript, "->", res.reply);
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Just like the web widget, **everything runs on the device**. The phone's own
|
|
166
|
+
recogniser transcribes the question and the phone's own synthesiser speaks the
|
|
167
|
+
answer, so your server only ever sees plain text. No server change, no API keys,
|
|
168
|
+
and no audio ever reaches your backend.
|
|
169
|
+
|
|
170
|
+
### Install
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
npx expo install expo-speech expo-speech-recognition
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Then add the permission strings to `app.json`:
|
|
177
|
+
|
|
178
|
+
```json
|
|
179
|
+
{
|
|
180
|
+
"expo": {
|
|
181
|
+
"plugins": [
|
|
182
|
+
[
|
|
183
|
+
"expo-speech-recognition",
|
|
184
|
+
{
|
|
185
|
+
"microphonePermission": "Allow Rover to hear you.",
|
|
186
|
+
"speechRecognitionPermission": "Allow Rover to turn your speech into text."
|
|
187
|
+
}
|
|
188
|
+
]
|
|
189
|
+
]
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Both packages are **optional peer dependencies**, resolved on first use. An app
|
|
195
|
+
that only sends text never loads them and never breaks. Speech recognition is
|
|
196
|
+
native code, so it needs a development build — `npx expo run:android` or
|
|
197
|
+
`npx expo run:ios` — not Expo Go. `expo-speech` alone also needs a rebuild.
|
|
198
|
+
|
|
199
|
+
### Options
|
|
200
|
+
|
|
201
|
+
```tsx
|
|
202
|
+
configureVoice({
|
|
203
|
+
lang: "en-US", // BCP-47, used for both listening and speaking
|
|
204
|
+
rate: 0.9, // web widget uses the same friendly pace
|
|
205
|
+
speakAllReplies: false, // true = read every reply, not just spoken ones
|
|
206
|
+
onPartial: (text) => setText(text), // live transcript while talking
|
|
207
|
+
onUserQuery: (text) => console.log(text),
|
|
208
|
+
onError: (msg) => alert(msg),
|
|
209
|
+
});
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### UI state
|
|
213
|
+
|
|
214
|
+
`useVoiceChat()` mirrors `useChat()`:
|
|
215
|
+
|
|
216
|
+
```tsx
|
|
217
|
+
import { useVoiceChat } from "@webotme/react-native";
|
|
218
|
+
|
|
219
|
+
const { state, transcript, listening, speaking, start, stop } = useVoiceChat();
|
|
220
|
+
// state: "idle" | "listening" | "thinking" | "speaking" | "unsupported" | "denied"
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Other helpers: `speakWithUs(text)` to read any line aloud, `stopVoice()` to stop
|
|
224
|
+
listening and talking, `setVoiceReplies(on)` to toggle replies, and
|
|
225
|
+
`isVoiceSupported()` to feature-detect.
|
|
226
|
+
|
|
152
227
|
## Errors
|
|
153
228
|
|
|
154
229
|
Every failure is a `WebotMeError` with a stable `code`:
|
|
@@ -184,6 +259,10 @@ your own backend and use `POST /api/sdk/chat`.
|
|
|
184
259
|
| New session | `POST /api/chatbot/public/{botId}/session/new` |
|
|
185
260
|
| Web chat page | `GET {chatBaseUrl}/chat/{botId}` |
|
|
186
261
|
|
|
262
|
+
Voice uses **none** of these. `voiceWithUs()` transcribes and speaks on the
|
|
263
|
+
device, so `/native-chat/v1/stt` and `/native-chat/v1/tts` are never called —
|
|
264
|
+
the same way the web widget runs on the browser's own speech APIs.
|
|
265
|
+
|
|
187
266
|
## License
|
|
188
267
|
|
|
189
268
|
UNLICENSED
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webotme/react-native",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Drop-in AI chatbot for React Native and Expo apps. Same bot, training and AutoFlow as the WebotMe web widget.",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"types": "src/index.ts",
|
|
@@ -11,12 +11,18 @@
|
|
|
11
11
|
"scripts": {
|
|
12
12
|
"build": "tsc --noEmit",
|
|
13
13
|
"prepublishOnly": "npm run build",
|
|
14
|
-
"typecheck": "tsc --noEmit"
|
|
14
|
+
"typecheck": "tsc --noEmit",
|
|
15
|
+
"test:voice": "tsc -p tsconfig.smoke.json && node .smoke-build/voice.smoke.js && node .smoke-build/voice-nomodules.smoke.js",
|
|
16
|
+
"test": "npm run typecheck"
|
|
15
17
|
},
|
|
16
18
|
"keywords": [
|
|
17
19
|
"chatbot",
|
|
18
20
|
"ai",
|
|
19
21
|
"chat",
|
|
22
|
+
"voice",
|
|
23
|
+
"speech",
|
|
24
|
+
"text-to-speech",
|
|
25
|
+
"speech-recognition",
|
|
20
26
|
"react-native",
|
|
21
27
|
"expo",
|
|
22
28
|
"webotme",
|
|
@@ -24,11 +30,19 @@
|
|
|
24
30
|
],
|
|
25
31
|
"license": "UNLICENSED",
|
|
26
32
|
"peerDependencies": {
|
|
33
|
+
"expo-speech": ">=13",
|
|
34
|
+
"expo-speech-recognition": ">=3",
|
|
27
35
|
"react": ">=18",
|
|
28
36
|
"react-native": ">=0.72",
|
|
29
37
|
"react-native-webview": ">=13"
|
|
30
38
|
},
|
|
31
39
|
"peerDependenciesMeta": {
|
|
40
|
+
"expo-speech": {
|
|
41
|
+
"optional": true
|
|
42
|
+
},
|
|
43
|
+
"expo-speech-recognition": {
|
|
44
|
+
"optional": true
|
|
45
|
+
},
|
|
32
46
|
"react-native-webview": {
|
|
33
47
|
"optional": true
|
|
34
48
|
}
|
|
@@ -39,8 +53,5 @@
|
|
|
39
53
|
"react-native": "^0.76.9",
|
|
40
54
|
"react-native-webview": "^13.17.0",
|
|
41
55
|
"typescript": "^5.9.3"
|
|
42
|
-
},
|
|
43
|
-
"dependencies": {
|
|
44
|
-
"expo-av": "^16.0.8"
|
|
45
56
|
}
|
|
46
57
|
}
|
package/src/AutoFlowExecutor.tsx
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
View,
|
|
12
12
|
type ViewStyle,
|
|
13
13
|
} from "react-native";
|
|
14
|
+
import { routeTargetFromUrl } from "./navigationAdapter";
|
|
14
15
|
import type {
|
|
15
16
|
WebotMeAction,
|
|
16
17
|
WebotMePendingWorkflow,
|
|
@@ -94,8 +95,12 @@ export function AutoFlowExecutor({
|
|
|
94
95
|
case "navigate": {
|
|
95
96
|
const url = action.data?.url;
|
|
96
97
|
if (!url) throw new Error("Navigate action missing URL");
|
|
97
|
-
|
|
98
|
-
//
|
|
98
|
+
|
|
99
|
+
// Resolve the raw URL/path into a real expo-router path + a
|
|
100
|
+
// friendly PascalCase name for React Navigation named screens.
|
|
101
|
+
const target = routeTargetFromUrl(url);
|
|
102
|
+
|
|
103
|
+
// Try to match known app routes first (named screens).
|
|
99
104
|
const routeMap: Record<string, string> = {
|
|
100
105
|
"/dashboard": "Dashboard",
|
|
101
106
|
"/dashboard/create": "CreateChatbot",
|
|
@@ -106,15 +111,21 @@ export function AutoFlowExecutor({
|
|
|
106
111
|
"/plans": "Plans",
|
|
107
112
|
"/login": "Login",
|
|
108
113
|
};
|
|
109
|
-
|
|
110
|
-
const routeName = routeMap[url] ||
|
|
111
|
-
|
|
114
|
+
|
|
115
|
+
const routeName = routeMap[url] || target.name;
|
|
116
|
+
const params = {
|
|
117
|
+
fromAutoFlow: true,
|
|
118
|
+
originalUrl: url,
|
|
119
|
+
screenName: routeName,
|
|
120
|
+
route: target.route,
|
|
121
|
+
};
|
|
122
|
+
|
|
112
123
|
if (push) {
|
|
113
|
-
push(
|
|
124
|
+
push(target.route, params);
|
|
114
125
|
} else {
|
|
115
|
-
navigate(
|
|
126
|
+
navigate(target.route, params);
|
|
116
127
|
}
|
|
117
|
-
|
|
128
|
+
|
|
118
129
|
return { success: true };
|
|
119
130
|
}
|
|
120
131
|
|
package/src/chat.ts
ADDED
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One-function chat for apps that already have their own UI.
|
|
3
|
+
*
|
|
4
|
+
* The provider owns the session when it is mounted, so `chatWithUs` works with
|
|
5
|
+
* no extra setup. Apps that skip the provider can call `configureChat` once.
|
|
6
|
+
*/
|
|
7
|
+
import { useEffect, useState } from "react";
|
|
8
|
+
import { WebotMeClient } from "./client";
|
|
9
|
+
import { ensureRuntime } from "./runtime";
|
|
10
|
+
import {
|
|
11
|
+
WebotMeError,
|
|
12
|
+
type WebotMeAction,
|
|
13
|
+
type WebotMeAutoTask,
|
|
14
|
+
type WebotMeConfig,
|
|
15
|
+
type WebotMeMessage,
|
|
16
|
+
type WebotMePendingWorkflow,
|
|
17
|
+
type WebotMeScreenContext,
|
|
18
|
+
} from "./types";
|
|
19
|
+
|
|
20
|
+
const makeId = (prefix: string) =>
|
|
21
|
+
`${prefix}_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
|
|
22
|
+
|
|
23
|
+
type Listener = () => void;
|
|
24
|
+
|
|
25
|
+
type ChatState = {
|
|
26
|
+
sessionId: string;
|
|
27
|
+
messages: WebotMeMessage[];
|
|
28
|
+
sending: boolean;
|
|
29
|
+
error: string | null;
|
|
30
|
+
pendingWorkflow: WebotMePendingWorkflow | null;
|
|
31
|
+
autoTask: WebotMeAutoTask | null;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/** What `chatWithUs` hands back so a custom UI can react without a hook. */
|
|
35
|
+
export type ChatResult = {
|
|
36
|
+
ok: boolean;
|
|
37
|
+
/** The bot's reply, or an empty string when the send failed. */
|
|
38
|
+
reply: string;
|
|
39
|
+
/** The user's message plus the bot's reply, ready to render. */
|
|
40
|
+
userMessage: WebotMeMessage | null;
|
|
41
|
+
botMessage: WebotMeMessage | null;
|
|
42
|
+
action?: WebotMeAction;
|
|
43
|
+
actions?: WebotMeAction[];
|
|
44
|
+
pendingWorkflow?: WebotMePendingWorkflow;
|
|
45
|
+
autoTask?: WebotMeAutoTask;
|
|
46
|
+
error?: string;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
class ChatEngine {
|
|
50
|
+
private client: WebotMeClient | null = null;
|
|
51
|
+
private userId = "";
|
|
52
|
+
private sessionId = makeId("s");
|
|
53
|
+
private screenContext: WebotMeScreenContext | undefined;
|
|
54
|
+
private listeners = new Set<Listener>();
|
|
55
|
+
private inflight = false;
|
|
56
|
+
private state: ChatState;
|
|
57
|
+
|
|
58
|
+
constructor() {
|
|
59
|
+
this.state = {
|
|
60
|
+
sessionId: this.sessionId,
|
|
61
|
+
messages: [],
|
|
62
|
+
sending: false,
|
|
63
|
+
error: null,
|
|
64
|
+
pendingWorkflow: null,
|
|
65
|
+
autoTask: null,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
configure(config: WebotMeConfig) {
|
|
70
|
+
ensureRuntime();
|
|
71
|
+
const nextClient = new WebotMeClient({
|
|
72
|
+
botId: config.botId,
|
|
73
|
+
apiBaseUrl: config.apiBaseUrl,
|
|
74
|
+
timeoutMs: config.timeoutMs,
|
|
75
|
+
});
|
|
76
|
+
const changedBot = !this.client || this.client.botId !== config.botId;
|
|
77
|
+
this.client = nextClient;
|
|
78
|
+
if (config.userId) this.userId = config.userId;
|
|
79
|
+
if (!this.userId) this.userId = makeId("v");
|
|
80
|
+
this.screenContext = config.screenContext;
|
|
81
|
+
if (changedBot) this.reset();
|
|
82
|
+
if (config.welcome && this.state.messages.length === 0) {
|
|
83
|
+
this.patch({
|
|
84
|
+
messages: [
|
|
85
|
+
{ id: makeId("m"), from: "bot", text: config.welcome, createdAt: Date.now() },
|
|
86
|
+
],
|
|
87
|
+
});
|
|
88
|
+
} else {
|
|
89
|
+
this.emit();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Keeps the live screen context in sync without re-creating the client. */
|
|
94
|
+
updateScreenContext(screenContext: WebotMeScreenContext | undefined) {
|
|
95
|
+
this.screenContext = screenContext;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private patch(next: Partial<ChatState>) {
|
|
99
|
+
this.state = { ...this.state, ...next };
|
|
100
|
+
this.emit();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private emit() {
|
|
104
|
+
for (const listener of this.listeners) listener();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
subscribe(listener: Listener) {
|
|
108
|
+
this.listeners.add(listener);
|
|
109
|
+
return () => {
|
|
110
|
+
this.listeners.delete(listener);
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
getState(): ChatState {
|
|
115
|
+
return this.state;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
reset() {
|
|
119
|
+
this.sessionId = makeId("s");
|
|
120
|
+
this.patch({
|
|
121
|
+
sessionId: this.sessionId,
|
|
122
|
+
messages: [],
|
|
123
|
+
sending: false,
|
|
124
|
+
error: null,
|
|
125
|
+
pendingWorkflow: null,
|
|
126
|
+
autoTask: null,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
isReady() {
|
|
131
|
+
return this.client !== null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
getSessionId() {
|
|
135
|
+
return this.sessionId;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async send(text: string): Promise<ChatResult> {
|
|
139
|
+
const trimmed = text.trim();
|
|
140
|
+
if (!trimmed) {
|
|
141
|
+
return { ok: false, reply: "", userMessage: null, botMessage: null, error: "Message is empty" };
|
|
142
|
+
}
|
|
143
|
+
if (!this.client) {
|
|
144
|
+
const error = "Chat is not configured. Wrap the app in <WebotMeProvider> or call configureChat() first.";
|
|
145
|
+
this.patch({ error });
|
|
146
|
+
return { ok: false, reply: "", userMessage: null, botMessage: null, error };
|
|
147
|
+
}
|
|
148
|
+
if (this.inflight) {
|
|
149
|
+
const error = "A message is already being sent.";
|
|
150
|
+
return { ok: false, reply: "", userMessage: null, botMessage: null, error };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const client = this.client;
|
|
154
|
+
const sessionId = this.sessionId;
|
|
155
|
+
const userId = this.userId;
|
|
156
|
+
|
|
157
|
+
const userMessage: WebotMeMessage = {
|
|
158
|
+
id: makeId("m"),
|
|
159
|
+
from: "user",
|
|
160
|
+
text: trimmed,
|
|
161
|
+
createdAt: Date.now(),
|
|
162
|
+
};
|
|
163
|
+
const pendingId = makeId("m");
|
|
164
|
+
|
|
165
|
+
this.inflight = true;
|
|
166
|
+
this.patch({
|
|
167
|
+
messages: [
|
|
168
|
+
...this.state.messages,
|
|
169
|
+
userMessage,
|
|
170
|
+
{ id: pendingId, from: "bot", text: "", createdAt: Date.now(), pending: true },
|
|
171
|
+
],
|
|
172
|
+
sending: true,
|
|
173
|
+
error: null,
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
try {
|
|
177
|
+
const result = await client.send({
|
|
178
|
+
message: trimmed,
|
|
179
|
+
sessionId,
|
|
180
|
+
userId,
|
|
181
|
+
screenContext: this.screenContext,
|
|
182
|
+
autoTask: this.state.autoTask ?? undefined,
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
const botMessage: WebotMeMessage = {
|
|
186
|
+
id: pendingId,
|
|
187
|
+
from: "bot",
|
|
188
|
+
text: result.text || result.reply || "",
|
|
189
|
+
createdAt: Date.now(),
|
|
190
|
+
action: result.actions?.[0] ?? result.action,
|
|
191
|
+
actions: result.actions,
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
this.patch({
|
|
195
|
+
messages: this.state.messages.map((m) => (m.id === pendingId ? botMessage : m)),
|
|
196
|
+
sending: false,
|
|
197
|
+
error: null,
|
|
198
|
+
pendingWorkflow: result.pendingWorkflow ?? null,
|
|
199
|
+
autoTask: result.autoTask ?? null,
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
ok: true,
|
|
204
|
+
reply: botMessage.text,
|
|
205
|
+
userMessage,
|
|
206
|
+
botMessage,
|
|
207
|
+
action: botMessage.action,
|
|
208
|
+
actions: result.actions,
|
|
209
|
+
pendingWorkflow: result.pendingWorkflow,
|
|
210
|
+
autoTask: result.autoTask,
|
|
211
|
+
};
|
|
212
|
+
} catch (err) {
|
|
213
|
+
const error =
|
|
214
|
+
err instanceof WebotMeError || err instanceof Error
|
|
215
|
+
? err.message
|
|
216
|
+
: "Something went wrong";
|
|
217
|
+
this.patch({
|
|
218
|
+
messages: this.state.messages.filter((m) => m.id !== pendingId),
|
|
219
|
+
sending: false,
|
|
220
|
+
error,
|
|
221
|
+
});
|
|
222
|
+
return { ok: false, reply: "", userMessage, botMessage: null, error };
|
|
223
|
+
} finally {
|
|
224
|
+
this.inflight = false;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** Push the current live screen snapshot to the autonomous AutoFlow loop. */
|
|
229
|
+
async sendStep(): Promise<ChatResult> {
|
|
230
|
+
if (!this.client) {
|
|
231
|
+
const error = "Chat is not configured. Wrap the app in <WebotMeProvider> or call configureChat() first.";
|
|
232
|
+
this.patch({ error });
|
|
233
|
+
return { ok: false, reply: "", userMessage: null, botMessage: null, error };
|
|
234
|
+
}
|
|
235
|
+
if (!this.state.autoTask || !this.state.autoTask.goal) {
|
|
236
|
+
return { ok: false, reply: "", userMessage: null, botMessage: null, error: "No AutoFlow task to advance." };
|
|
237
|
+
}
|
|
238
|
+
if (this.inflight) {
|
|
239
|
+
return { ok: false, reply: "", userMessage: null, botMessage: null, error: "A step is already in flight." };
|
|
240
|
+
}
|
|
241
|
+
const pendingId = makeId("m");
|
|
242
|
+
this.inflight = true;
|
|
243
|
+
this.patch({ sending: true, error: null });
|
|
244
|
+
try {
|
|
245
|
+
const result = await this.client.send({
|
|
246
|
+
message: "",
|
|
247
|
+
sessionId: this.sessionId,
|
|
248
|
+
userId: this.userId,
|
|
249
|
+
screenContext: this.screenContext,
|
|
250
|
+
autoTask: this.state.autoTask as any,
|
|
251
|
+
});
|
|
252
|
+
const botMessage: WebotMeMessage = {
|
|
253
|
+
id: pendingId,
|
|
254
|
+
from: "bot",
|
|
255
|
+
text: result.text || result.reply || "",
|
|
256
|
+
createdAt: Date.now(),
|
|
257
|
+
action: result.actions?.[0] ?? result.action,
|
|
258
|
+
actions: result.actions,
|
|
259
|
+
};
|
|
260
|
+
this.patch({
|
|
261
|
+
messages: [
|
|
262
|
+
...this.state.messages,
|
|
263
|
+
botMessage,
|
|
264
|
+
],
|
|
265
|
+
sending: false,
|
|
266
|
+
error: null,
|
|
267
|
+
pendingWorkflow: result.pendingWorkflow ?? null,
|
|
268
|
+
autoTask: result.autoTask ?? this.state.autoTask,
|
|
269
|
+
});
|
|
270
|
+
return {
|
|
271
|
+
ok: true,
|
|
272
|
+
reply: botMessage.text,
|
|
273
|
+
userMessage: null,
|
|
274
|
+
botMessage,
|
|
275
|
+
action: botMessage.action,
|
|
276
|
+
actions: result.actions,
|
|
277
|
+
pendingWorkflow: result.pendingWorkflow,
|
|
278
|
+
autoTask: result.autoTask,
|
|
279
|
+
};
|
|
280
|
+
} catch (err) {
|
|
281
|
+
const error =
|
|
282
|
+
err instanceof WebotMeError || err instanceof Error ? err.message : "Something went wrong";
|
|
283
|
+
this.patch({ sending: false, error });
|
|
284
|
+
return { ok: false, reply: "", userMessage: null, botMessage: null, error };
|
|
285
|
+
} finally {
|
|
286
|
+
this.inflight = false;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const engine = new ChatEngine();
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Point the one-function chat at a bot. Only needed when the app does not use
|
|
295
|
+
* `WebotMeProvider`, which configures the engine automatically.
|
|
296
|
+
*/
|
|
297
|
+
export function configureChat(config: WebotMeConfig) {
|
|
298
|
+
engine.configure(config);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Keeps `chatWithUs` sending an up-to-date screen snapshot. The provider calls
|
|
303
|
+
* this for you; only touch it when driving the engine manually.
|
|
304
|
+
*/
|
|
305
|
+
export function updateChatScreenContext(screenContext: WebotMeScreenContext | undefined) {
|
|
306
|
+
engine.updateScreenContext(screenContext);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** The id the next message will be sent under. */
|
|
310
|
+
export function getChatSessionId() {
|
|
311
|
+
return engine.getSessionId();
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** Subscribe to chat engine state (used by executor/observer utilities). */
|
|
315
|
+
export function subscribeChat(listener: () => void): () => void {
|
|
316
|
+
return engine.subscribe(listener);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/** Current chat state snapshot. */
|
|
320
|
+
export function getChatState() {
|
|
321
|
+
return engine.getState();
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Send the next AutoFlow step (empty message) to advance the server's autonomous
|
|
326
|
+
* task loop. Provider/executor wires this up internally.
|
|
327
|
+
*/
|
|
328
|
+
export async function nextAutoFlowStep(): Promise<ChatResult> {
|
|
329
|
+
return engine.sendStep();
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Send one message to the bot and wait for its reply.
|
|
334
|
+
*
|
|
335
|
+
* ```tsx
|
|
336
|
+
* <TouchableOpacity
|
|
337
|
+
* onPress={async () => {
|
|
338
|
+
* const res = await chatWithUs(text);
|
|
339
|
+
* if (res.ok) setText("");
|
|
340
|
+
* }}
|
|
341
|
+
* />
|
|
342
|
+
* ```
|
|
343
|
+
*/
|
|
344
|
+
export async function chatWithUs(text: string): Promise<ChatResult> {
|
|
345
|
+
return engine.send(text);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** Clear the conversation and start a fresh session. */
|
|
349
|
+
export function resetChat() {
|
|
350
|
+
engine.reset();
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Live chat state for rendering. Optional: use it to show the transcript, and
|
|
355
|
+
* keep `chatWithUs` for sending.
|
|
356
|
+
*/
|
|
357
|
+
export function useChat() {
|
|
358
|
+
const [state, setState] = useState<ChatState>(() => engine.getState());
|
|
359
|
+
|
|
360
|
+
useEffect(() => {
|
|
361
|
+
setState(engine.getState());
|
|
362
|
+
return engine.subscribe(() => setState(engine.getState()));
|
|
363
|
+
}, []);
|
|
364
|
+
|
|
365
|
+
return {
|
|
366
|
+
...state,
|
|
367
|
+
send: chatWithUs,
|
|
368
|
+
reset: resetChat,
|
|
369
|
+
ready: engine.isReady(),
|
|
370
|
+
};
|
|
371
|
+
}
|
package/src/client.ts
CHANGED
|
@@ -31,7 +31,8 @@ export type SendResult = {
|
|
|
31
31
|
*/
|
|
32
32
|
export class WebotMeClient {
|
|
33
33
|
private apiBaseUrl: string;
|
|
34
|
-
|
|
34
|
+
/** Public so a host app can tell which bot an instance is bound to. */
|
|
35
|
+
readonly botId: string;
|
|
35
36
|
private timeoutMs: number;
|
|
36
37
|
|
|
37
38
|
constructor(config: Pick<WebotMeConfig, "botId" | "apiBaseUrl" | "timeoutMs">) {
|
|
@@ -112,6 +113,7 @@ export class WebotMeClient {
|
|
|
112
113
|
sessionId: string;
|
|
113
114
|
userId: string;
|
|
114
115
|
screenContext?: WebotMeScreenContext;
|
|
116
|
+
autoTask?: WebotMeAutoTask;
|
|
115
117
|
}): Promise<SendResult> {
|
|
116
118
|
const body = await this.request<Record<string, unknown>>("/native-chat/v1", {
|
|
117
119
|
method: "POST",
|
|
@@ -120,6 +122,7 @@ export class WebotMeClient {
|
|
|
120
122
|
sessionId: params.sessionId,
|
|
121
123
|
userId: params.userId,
|
|
122
124
|
...(params.screenContext ? { screenContext: params.screenContext } : {}),
|
|
125
|
+
...(params.autoTask ? { autoTask: params.autoTask } : {}),
|
|
123
126
|
}),
|
|
124
127
|
});
|
|
125
128
|
|