@kavchat/react-native 0.0.0-stage → 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 maxsave
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,181 @@
1
- # Temporary Holding Version
1
+ # @kavchat/react-native
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Kavchat for React Native: the scripted support chat (no language model) as a provider, a chat screen and an optional floating launcher.
4
+ Answers come from the scripts you publish in the Kavchat dashboard, run on the device by the shared engine `@kavchat/core`.
5
+
6
+ ## Install
7
+
8
+ The package is pure JS (no native code of its own, no icon library), so it works the same in bare React Native and in Expo.
9
+ Peer dependencies: `react` 18+, `react-native` 0.72+.
10
+
11
+ ### React Native CLI (bare)
12
+
13
+ ```sh
14
+ yarn add @kavchat/react-native @react-native-async-storage/async-storage
15
+ cd ios && pod install
16
+ ```
17
+
18
+ AsyncStorage is a native module: `pod install` links it on iOS, and autolinking handles Android. Minimum `react-native` 0.72.
19
+
20
+ ### Expo
21
+
22
+ ```sh
23
+ yarn add @kavchat/react-native
24
+ npx expo install @react-native-async-storage/async-storage
25
+ ```
26
+
27
+ Works in Expo Go and in development builds.
28
+
29
+ ### Without AsyncStorage
30
+
31
+ AsyncStorage keeps the published release and the conversation between app launches. It is optional: without it the SDK keeps
32
+ everything in memory (the bot still works, it just downloads the release again on every launch).
33
+ To use another store (for example MMKV), pass the `storage` prop with any `{ getItem, setItem, removeItem? }`, sync or async:
34
+
35
+ ```tsx
36
+ import { MMKV } from 'react-native-mmkv';
37
+ const mmkv = new MMKV();
38
+ const storage = {
39
+ getItem: (k: string) => mmkv.getString(k) ?? null,
40
+ setItem: (k: string, v: string) => mmkv.set(k, v),
41
+ removeItem: (k: string) => mmkv.delete(k),
42
+ };
43
+
44
+ <KavchatProvider publicKey="pk_live_..." storage={storage}>
45
+ ```
46
+
47
+ ## Usage
48
+
49
+ ```tsx
50
+ import { KavchatProvider, KavchatChat, KavchatLauncher, useKavchat } from '@kavchat/react-native';
51
+
52
+ export default function App() {
53
+ return (
54
+ <KavchatProvider publicKey="pk_live_..." onScreen={(screen, id) => navigation.navigate(screen, { id })}>
55
+ <Navigation />
56
+ {/* floating button, bottom end corner; opens the chat in a modal */}
57
+ <KavchatLauncher />
58
+ </KavchatProvider>
59
+ );
60
+ }
61
+
62
+ // or as a screen in your own navigator
63
+ function SupportScreen() {
64
+ return <KavchatChat />;
65
+ }
66
+
67
+ // or your own button
68
+ function HelpButton() {
69
+ const { available, open } = useKavchat();
70
+ if (!available) return null; // bot is off, limited to testers, or over quota
71
+ return <Button title="Help" onPress={open} />;
72
+ }
73
+ ```
74
+
75
+ `KavchatProvider` props:
76
+
77
+ | Prop | |
78
+ |---|---|
79
+ | `publicKey` | `pk_live_...` from the dashboard. Safe to ship in the app. |
80
+ | `apiBase` | Default `https://api.kavchat.com`. |
81
+ | `locale` | UI language (`he`, `en`, `ar`, `ru`, `es`, `fr`, `de`, `pt`, `it`, `tr`). Default: the device language, else the bot language. The scripts are matched in the bot language either way. |
82
+ | `userToken`, `userName` | Identity, see below. |
83
+ | `onScreen(screen, targetId?)` | Called for buttons of type "screen" in a script. Without it those buttons do nothing. |
84
+ | `screens` | The app screens those buttons can open, `[{ id, label, needsTarget? }]`. See Screens. |
85
+ | `onEvent(event)` | `ready`, `unavailable`, `effect`, `ticket`. |
86
+ | `storage` | Any `{ getItem, setItem, removeItem? }` (sync or async). Default AsyncStorage, else memory. |
87
+ | `theme` | Override of the colors (see Theming). |
88
+
89
+ `useKavchat()` returns `{ status, available, offline, locale, isOpen, open, close, toggle, setUser, startNewConversation, openContactForm, controller }`.
90
+
91
+ `KavchatChat` props: `onClose?` (shows a close button), `hideHeader?`, `style?`. `KavchatLauncher` props: `style?` (position), `onPress?` (use your own screen instead of the built-in modal).
92
+
93
+ ## Identity token
94
+
95
+ Without a token the user is a guest: the contact form asks for an email or a phone.
96
+ To identify a signed-in user, sign a short-lived JWT on **your server** with the workspace secret key (`sk_live_...`, never in the app)
97
+ and pass it in:
98
+
99
+ ```tsx
100
+ <KavchatProvider publicKey="pk_live_..." userToken={session?.kavchatToken} userName={session?.firstName}>
101
+ ```
102
+
103
+ Changing `userToken` (sign-in, sign-out) reloads the release, because the "testers" audience depends on who the user is.
104
+ You can also call `useKavchat().setUser({ token, name })`. The token is sent only as `Authorization: Bearer`; it is never written to storage
105
+ (a saved conversation is tied to a fingerprint of it, so another account on the same device starts fresh).
106
+
107
+ ## Links, screens and the contact form
108
+
109
+ - Script buttons of type `link` open only `https:` and `tel:` URLs, through `Linking.openURL`. Other schemes are ignored.
110
+ - Buttons of type `screen` call `onScreen`. The chat stays where it was.
111
+ - "Talk to our team" opens the contact form with the conversation attached; the ticket number is shown after sending.
112
+
113
+ ## Screens
114
+
115
+ Declare the screens of your app once. The SDK registers them with Kavchat, and the script editor in the dashboard then offers
116
+ them in a dropdown (and warns about a button that points to an unknown screen) instead of a free text field:
117
+
118
+ ```tsx
119
+ const screens = [
120
+ { id: 'orders', label: 'My orders' },
121
+ { id: 'product', label: 'Product page', needsTarget: true }, // the button also carries a product id
122
+ { id: 'settings', label: 'Settings' },
123
+ ];
124
+
125
+ <KavchatProvider
126
+ publicKey="pk_live_..."
127
+ screens={screens}
128
+ onScreen={(screen, targetId) => navigation.navigate(screen, targetId ? { id: targetId } : undefined)}
129
+ >
130
+ ```
131
+
132
+ - `onScreen` receives the screen `id` and, for screens with `needsTarget`, the `targetId` the admin set on the button.
133
+ - `id`: letters, digits, `_`, `.`, `-`, up to 40 characters. `label`: 1 to 60 characters. Up to 100 screens. Invalid entries and
134
+ duplicate ids are dropped (with a warning in development builds only).
135
+ - The list is sent after the release loads, only when it changed or once a week, in the background. A failure is retried on the
136
+ next launch; it never affects the chat or the user.
137
+ - Changing the `screens` prop later (or `useKavchat().controller.registerScreens(list)`) registers the new list.
138
+
139
+ ## Theming and RTL
140
+
141
+ Colors come from the bot settings in the dashboard (`primaryColor`); the text color on it is picked by WCAG contrast.
142
+ Override any of them: `theme={{ primary: '#0f766e', background: '#ffffff', botBubble, userBubble, text, muted, accent, onPrimary, error }}`.
143
+
144
+ The SDK does **not** change the layout direction of your app. It follows `I18nManager.isRTL` (start/end styles), and text
145
+ is aligned by its own language, so Hebrew and Arabic read correctly in an LTR app too.
146
+ If your app is Hebrew or Arabic, set RTL once in your app as usual (`I18nManager.forceRTL(true)` + restart).
147
+
148
+ The free plan shows "Powered by Kavchat" under the chat (linking to kavchat.com); paid plans hide it automatically.
149
+
150
+ ## AI answers (Premium)
151
+
152
+ Nothing to configure in the app. When the workspace owner turns on AI answers in the dashboard (Premium, with the
153
+ workspace's own AI provider key), the release carries `settings.aiAnswers: true`. Then a typed question that no script
154
+ matches is sent to `POST /v1/client/{publicKey}/ai/answer` (at most 300 characters, 20 second timeout) while a typing
155
+ indicator shows. The answer appears as plain text with a small "AI answer" label in the UI language, followed by a
156
+ button to the script the AI suggests (if any), "Talk to our team" and the main menu.
157
+
158
+ If there is no answer, the request fails, times out or is rate limited (429), the chat shows the regular "no match"
159
+ message. The unmatched question is reported to the dashboard exactly as before, and AI answers are saved with the
160
+ conversation like any other bot message.
161
+
162
+ ## Behavior
163
+
164
+ - The published release is cached with its ETag (`If-None-Match`, 304). Offline, the cached release is shown with a banner.
165
+ - The conversation is saved on the device and restored for 24 hours on the same release and user.
166
+ - The conversation path is reported 2 seconds after the last action and when the app goes to the background.
167
+ - Requests send no custom headers (only `Authorization`, `Content-Type`, `If-None-Match`).
168
+
169
+ ## Development
170
+
171
+ Logic (`src/api.ts`, `src/storage.ts`, `src/screens.ts`, `src/controller.ts`, `src/text.ts`, `src/theme.ts`) is plain TypeScript without React Native and is
172
+ tested with vitest. `@kavchat/core` is not on npm yet, so `vitest.config.mts` aliases it to `../core/src`, and the tests run with the vitest
173
+ already installed at the repo root:
174
+
175
+ ```sh
176
+ cd react-native && ../node_modules/.bin/vitest run
177
+ ```
178
+
179
+ Type checking needs `react-native` and `@types/react`, which are not installed in the monorepo. Install them in a temporary folder and
180
+ point a tsconfig at it (`paths` for `react`, `react-native` to `types_generated/index.d.ts`, and `@kavchat/core` to `../core/src/index.ts`).
181
+ `legacy/` holds the original ACB screen and engine, kept for reference only; it is not part of the package.
package/lib/api.d.ts ADDED
@@ -0,0 +1,87 @@
1
+ import type { ClientRelease, TranscriptEntry } from '@kavchat/core';
2
+ /**
3
+ * קריאות ל-Client API, כמו בווידג'ט לאתר
4
+ * כלל CORS: בלי כותרות מותאמות (Cache-Control, Pragma וכו'). רק Authorization, Content-Type
5
+ * ו-If-None-Match, שהשרת מתיר. מניעת מטמון נעשית עם cache: 'no-store'
6
+ */
7
+ export type FetchLike = (url: string, init?: Record<string, unknown>) => Promise<{
8
+ status: number;
9
+ ok: boolean;
10
+ headers: {
11
+ get(name: string): string | null;
12
+ };
13
+ json(): Promise<unknown>;
14
+ }>;
15
+ export interface ApiConfig {
16
+ apiBase: string;
17
+ publicKey: string;
18
+ getToken: () => string | undefined;
19
+ /** להחלפה בבדיקות. ברירת מחדל: fetch הגלובלי של React Native */
20
+ fetch?: FetchLike;
21
+ }
22
+ export type DisabledRelease = {
23
+ enabled: false;
24
+ reason?: string;
25
+ };
26
+ export type AnyRelease = ClientRelease | DisabledRelease;
27
+ export type EnabledRelease = Extract<ClientRelease, {
28
+ enabled: true;
29
+ }>;
30
+ export type ReleaseResult = {
31
+ kind: 'ok';
32
+ release: AnyRelease;
33
+ etag: string;
34
+ } | {
35
+ kind: 'not-modified';
36
+ }
37
+ /** מפתח שגוי או חסום: לא מציגים את הבוט */
38
+ | {
39
+ kind: 'denied';
40
+ status: number;
41
+ }
42
+ /** אין רשת או שהשרת נפל: משתמשים בעותק השמור */
43
+ | {
44
+ kind: 'offline';
45
+ };
46
+ export interface TicketInput {
47
+ message: string;
48
+ name?: string;
49
+ email?: string;
50
+ phone?: string;
51
+ topicId?: string;
52
+ scriptId?: string;
53
+ sessionId?: string;
54
+ transcript?: TranscriptEntry[];
55
+ }
56
+ export type TicketResult = {
57
+ ok: true;
58
+ id: string;
59
+ number: number;
60
+ } | {
61
+ ok: false;
62
+ code: string;
63
+ message: string;
64
+ };
65
+ export declare function fetchRelease(c: ApiConfig, etag?: string): Promise<ReleaseResult>;
66
+ export interface AiAnswer {
67
+ answer: string;
68
+ scriptId: string | null;
69
+ }
70
+ export declare const AI_TIMEOUT_MS = 20000;
71
+ /**
72
+ * תשובת AI לשאלה שאין לה תסריט (פרימיום). כל כישלון, 429, זמן קצוב או answer: null מחזירים null,
73
+ * והצ'אט מציג את הודעת "לא נמצא" הרגילה
74
+ */
75
+ export declare function postAiAnswer(c: ApiConfig, sessionId: string, question: string, timeoutMs?: number): Promise<AiAnswer | null>;
76
+ export declare function postEvents(c: ApiConfig, payload: unknown): Promise<boolean>;
77
+ export declare function postTicket(c: ApiConfig, input: TicketInput): Promise<TicketResult>;
78
+ export interface ScreensPayload {
79
+ platform: 'react-native';
80
+ screens: {
81
+ id: string;
82
+ label: string;
83
+ needsTarget: boolean;
84
+ }[];
85
+ }
86
+ /** רישום המסכים. הצלחה רק ב-200 עם success, כדי לא לשמור טביעה על תשובה חלקית */
87
+ export declare function postScreens(c: ApiConfig, payload: ScreensPayload): Promise<boolean>;
package/lib/api.js ADDED
@@ -0,0 +1,112 @@
1
+ const endpoint = (c, path) => `${c.apiBase.replace(/\/+$/, '')}/v1/client/${encodeURIComponent(c.publicKey)}/${path}`;
2
+ function headers(c, extra = {}) {
3
+ const token = c.getToken();
4
+ return token ? { ...extra, Authorization: `Bearer ${token}` } : extra;
5
+ }
6
+ const doFetch = (c) => c.fetch ?? globalThis.fetch;
7
+ export async function fetchRelease(c, etag) {
8
+ let res;
9
+ try {
10
+ res = await doFetch(c)(endpoint(c, 'release'), {
11
+ method: 'GET',
12
+ cache: 'no-store',
13
+ credentials: 'omit',
14
+ headers: headers(c, etag ? { 'If-None-Match': etag } : {}),
15
+ });
16
+ }
17
+ catch {
18
+ return { kind: 'offline' };
19
+ }
20
+ if (res.status === 304)
21
+ return { kind: 'not-modified' };
22
+ if (res.status >= 500 || res.status === 429)
23
+ return { kind: 'offline' };
24
+ if (!res.ok)
25
+ return { kind: 'denied', status: res.status };
26
+ const body = (await res.json().catch(() => null));
27
+ if (!body || body.success !== true || !body.data || typeof body.data.enabled !== 'boolean')
28
+ return { kind: 'offline' };
29
+ return { kind: 'ok', release: body.data, etag: res.headers.get('ETag') || '' };
30
+ }
31
+ function postJson(c, path, body, signal) {
32
+ return doFetch(c)(endpoint(c, path), {
33
+ method: 'POST',
34
+ cache: 'no-store',
35
+ credentials: 'omit',
36
+ headers: headers(c, { 'Content-Type': 'application/json' }),
37
+ body: JSON.stringify(body),
38
+ ...(signal ? { signal } : {}),
39
+ });
40
+ }
41
+ export const AI_TIMEOUT_MS = 20000;
42
+ /**
43
+ * תשובת AI לשאלה שאין לה תסריט (פרימיום). כל כישלון, 429, זמן קצוב או answer: null מחזירים null,
44
+ * והצ'אט מציג את הודעת "לא נמצא" הרגילה
45
+ */
46
+ export async function postAiAnswer(c, sessionId, question, timeoutMs = AI_TIMEOUT_MS) {
47
+ const ctrl = typeof AbortController === 'function' ? new AbortController() : null;
48
+ let timer = null;
49
+ const timeout = new Promise((resolve) => {
50
+ timer = setTimeout(() => {
51
+ ctrl?.abort();
52
+ resolve(null);
53
+ }, timeoutMs);
54
+ });
55
+ const request = (async () => {
56
+ try {
57
+ const res = await postJson(c, 'ai/answer', { sessionId, question }, ctrl?.signal);
58
+ if (!res.ok)
59
+ return null;
60
+ const body = (await res.json().catch(() => null));
61
+ const answer = body && body.success === true && body.data && typeof body.data.answer === 'string' ? body.data.answer.trim() : '';
62
+ if (!answer)
63
+ return null;
64
+ const scriptId = typeof body.data.scriptId === 'string' && body.data.scriptId ? body.data.scriptId : null;
65
+ return { answer, scriptId };
66
+ }
67
+ catch {
68
+ return null;
69
+ }
70
+ })();
71
+ try {
72
+ return await Promise.race([request, timeout]);
73
+ }
74
+ finally {
75
+ if (timer)
76
+ clearTimeout(timer);
77
+ }
78
+ }
79
+ export async function postEvents(c, payload) {
80
+ try {
81
+ const res = await postJson(c, 'events', payload);
82
+ return res.ok;
83
+ }
84
+ catch {
85
+ return false;
86
+ }
87
+ }
88
+ export async function postTicket(c, input) {
89
+ try {
90
+ const res = await postJson(c, 'tickets', input);
91
+ const body = (await res.json().catch(() => null));
92
+ if (res.ok && body && body.success && body.data)
93
+ return { ok: true, id: String(body.data.id), number: Number(body.data.number) };
94
+ return { ok: false, code: String(body?.code || `http_${res.status}`), message: String(body?.message || '') };
95
+ }
96
+ catch {
97
+ return { ok: false, code: 'network', message: '' };
98
+ }
99
+ }
100
+ /** רישום המסכים. הצלחה רק ב-200 עם success, כדי לא לשמור טביעה על תשובה חלקית */
101
+ export async function postScreens(c, payload) {
102
+ try {
103
+ const res = await postJson(c, 'screens', payload);
104
+ if (res.status !== 200)
105
+ return false;
106
+ const body = (await res.json().catch(() => null));
107
+ return !!body && body.success === true;
108
+ }
109
+ catch {
110
+ return false;
111
+ }
112
+ }
@@ -0,0 +1,147 @@
1
+ import type { BotChip, BotEffect, BotState, UiTexts } from '@kavchat/core';
2
+ import { EnabledRelease, FetchLike } from './api';
3
+ import { KavchatScreen } from './screens';
4
+ import { StorageAdapter } from './storage';
5
+ /**
6
+ * הבקר: טעינת הגרסה עם ETag ומטמון, שמירת השיחה, דיווח לשרת ופניות
7
+ * קוד TypeScript רגיל בלי React Native, כדי שאפשר יהיה לבדוק אותו ב-Node.
8
+ * לוגיקת ההתאמה ומכונת המצבים נמצאות ב-@kavchat/core ולא כאן
9
+ */
10
+ export declare const DEFAULT_API = "https://api.kavchat.com";
11
+ export declare const BRANDING_URL = "https://kavchat.com";
12
+ export type ContactEffect = Extract<BotEffect, {
13
+ type: 'contact';
14
+ }>;
15
+ export type KavchatStatus = 'loading' | 'ready' | 'unavailable';
16
+ export type KavchatView = 'chat' | 'form' | 'sent';
17
+ export type KavchatEvent = {
18
+ type: 'ready';
19
+ version: number;
20
+ offline: boolean;
21
+ } | {
22
+ type: 'unavailable';
23
+ reason?: string;
24
+ } | {
25
+ type: 'effect';
26
+ effect: BotEffect;
27
+ } | {
28
+ type: 'ticket';
29
+ id: string;
30
+ number: number;
31
+ };
32
+ export interface KavchatSnapshot {
33
+ status: KavchatStatus;
34
+ release: EnabledRelease | null;
35
+ /** הוצגה גרסה שמורה כי אין רשת */
36
+ offline: boolean;
37
+ /** שפת הממשק (הטקסטים הקבועים), לא שפת התסריט */
38
+ locale: string;
39
+ rtl: boolean;
40
+ texts: UiTexts;
41
+ conversation: BotState | null;
42
+ /** מחכים לתשובת AI: מציגים סימן הקלדה ולא מקבלים קלט */
43
+ typing: boolean;
44
+ view: KavchatView;
45
+ contact: ContactEffect | null;
46
+ ticketNumber: number | null;
47
+ /** למה הבוט לא זמין, למשל quota */
48
+ reason?: string;
49
+ identified: boolean;
50
+ userName?: string;
51
+ }
52
+ export interface ControllerOptions {
53
+ publicKey: string;
54
+ apiBase?: string;
55
+ locale?: string;
56
+ userToken?: string;
57
+ userName?: string;
58
+ storage?: StorageAdapter;
59
+ fetch?: FetchLike;
60
+ /** שפת המכשיר, כשלא נקבעה locale */
61
+ deviceLocale?: string;
62
+ /** כפתור מסוג screen בתסריט. בלי callback הכפתור לא עושה דבר */
63
+ onScreen?: (screen: string, targetId?: string) => void;
64
+ /** פתיחת קישור https או tel. ב-React Native זה Linking.openURL */
65
+ onLink?: (url: string) => void;
66
+ onEvent?: (event: KavchatEvent) => void;
67
+ /** מסכי האפליקציה שכפתור screen יכול לפתוח. נרשמים בשרת כדי שהעורך יציע אותם ברשימה */
68
+ screens?: readonly KavchatScreen[];
69
+ reportDelayMs?: number;
70
+ sessionMaxAgeMs?: number;
71
+ now?: () => number;
72
+ }
73
+ export interface ContactFields {
74
+ name?: string;
75
+ email?: string;
76
+ phone?: string;
77
+ topicId?: string;
78
+ message: string;
79
+ }
80
+ export declare class KavchatController {
81
+ private opts;
82
+ private api;
83
+ private store;
84
+ private snap;
85
+ private listeners;
86
+ private session;
87
+ private loadSeq;
88
+ private reportTimer;
89
+ private dirty;
90
+ private disposed;
91
+ private screens;
92
+ /** הגרסה כבר נטענה פעם אחת מהשרת, ולכן אפשר לרשום מסכים */
93
+ private releaseFetched;
94
+ /** הטביעה שכבר נוסתה בהפעלה הזו, בהצלחה או בכישלון. אחרי כישלון מנסים רק בהפעלה הבאה */
95
+ private screensTried;
96
+ /** מתממש כשהטעינה האחרונה הסתיימה */
97
+ ready: Promise<void>;
98
+ /** מתממש כשניסיון רישום המסכים האחרון הסתיים. בשביל בדיקות */
99
+ screensSync: Promise<void>;
100
+ constructor(options: ControllerOptions);
101
+ subscribe: (listener: () => void) => () => void;
102
+ getSnapshot: () => KavchatSnapshot;
103
+ private update;
104
+ /** callbacks שמשתנים בכל רינדור (onScreen וכו') מתעדכנים בלי לבנות בקר חדש */
105
+ setCallbacks(cb: Pick<ControllerOptions, 'onScreen' | 'onLink' | 'onEvent'>): void;
106
+ load(): Promise<void>;
107
+ private doLoad;
108
+ private apply;
109
+ private restoreSession;
110
+ /** החלפת משתמש (התחברות או התנתקות). הגרסה נטענת מחדש, כי הקהל testers תלוי בזהות */
111
+ setUser(user: {
112
+ token?: string | null;
113
+ name?: string | null;
114
+ } | null): Promise<void>;
115
+ /** הצהרה או עדכון של המסכים אחרי היצירה. נשלח רק אם הרשימה השתנתה */
116
+ registerScreens(screens: readonly KavchatScreen[] | null | undefined): void;
117
+ /** לא מחכים לזה בטעינה: רישום המסכים לא אמור לעכב או להפיל את הבוט */
118
+ private syncScreens;
119
+ setLocale(locale: string | undefined): void;
120
+ /** מתחיל שיחה אם עוד אין. קוראים לזה כשהצ'אט נפתח */
121
+ ensureConversation(): void;
122
+ startFresh(): void;
123
+ pressChip(chip: BotChip): void;
124
+ /** מתממש כשהתשובה הוצגה. עם תשובות AI זה אחרי הקריאה לשרת */
125
+ sendText(text: string): Promise<void>;
126
+ private askAi;
127
+ /**
128
+ * התסריט נבדק בשפת הבוט, והטקסטים הקבועים של הצ'אט מוצגים בשפת הממשק.
129
+ * ההגדרה של המנוע גלובלית, ולכן קובעים אותה לפני כל קריאה
130
+ */
131
+ private run;
132
+ private setConversation;
133
+ private afterAction;
134
+ private handleEffect;
135
+ /** פתיחת הטופס מכפתור מחוץ לתסריט */
136
+ openContactForm(prefill?: string): void;
137
+ backToChat(): void;
138
+ /** שולח פנייה. מחזיר הודעת שגיאה בשפת הממשק, או null אם נשלחה */
139
+ submitTicket(fields: ContactFields): Promise<string | null>;
140
+ private scheduleReport;
141
+ /** שולח את המסלול עכשיו. קוראים לזה גם כשהאפליקציה עוברת לרקע */
142
+ flush(): Promise<boolean>;
143
+ dispose(): void;
144
+ private now;
145
+ private emit;
146
+ private safeCall;
147
+ }