@vanira/sdk-react-native 0.0.2
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 +239 -0
- package/package.json +53 -0
- package/src/__tests__/WebRTCClient.integration.test.ts +396 -0
- package/src/__tests__/adapters.test.ts +475 -0
- package/src/__tests__/httpResponse.test.ts +25 -0
- package/src/__tests__/mocks/react-native-incall-manager.ts +8 -0
- package/src/__tests__/mocks/react-native-permissions.ts +15 -0
- package/src/__tests__/mocks/react-native-webrtc.ts +6 -0
- package/src/__tests__/mocks/react-native.ts +28 -0
- package/src/__tests__/preset.test.ts +239 -0
- package/src/__tests__/resolveRuntimeConfig.test.ts +90 -0
- package/src/__tests__/storage.test.ts +211 -0
- package/src/__tests__/webrtcSignaling.test.ts +42 -0
- package/src/adapters/PeerConnectionAdapter.ts +101 -0
- package/src/adapters/browser/BrowserAudioAdapter.ts +43 -0
- package/src/adapters/browser/BrowserDataChannelAdapter.ts +69 -0
- package/src/adapters/browser/BrowserMediaAdapter.ts +15 -0
- package/src/adapters/browser/BrowserPeerAdapter.ts +14 -0
- package/src/adapters/browser/index.ts +4 -0
- package/src/adapters/interfaces.ts +84 -0
- package/src/adapters/react-native/RNAudioAdapter.ts +42 -0
- package/src/adapters/react-native/RNDataChannelAdapter.ts +79 -0
- package/src/adapters/react-native/RNMediaAdapter.ts +46 -0
- package/src/adapters/react-native/RNPeerAdapter.ts +28 -0
- package/src/adapters/react-native/callAudioRouting.ts +115 -0
- package/src/adapters/react-native/decodeUtf8.ts +72 -0
- package/src/adapters/react-native/index.ts +4 -0
- package/src/adapters/react-native/rnUploadFile.ts +76 -0
- package/src/adapters/storage/BrowserDualStorageAdapter.ts +71 -0
- package/src/adapters/storage/MemoryStorageAdapter.ts +50 -0
- package/src/adapters/storage/StorageAdapter.ts +21 -0
- package/src/adapters/storage/createSyncStorageAdapter.ts +40 -0
- package/src/adapters/storage/index.ts +7 -0
- package/src/api/services/ChatService.ts +304 -0
- package/src/api/services/ConfigService.ts +33 -0
- package/src/assets/icons.js +35 -0
- package/src/cdn.ts +68 -0
- package/src/core/CallSessionStore.ts +137 -0
- package/src/core/DraggableController.ts +83 -0
- package/src/core/SessionManager.ts +322 -0
- package/src/core/VaniraAI.ts +464 -0
- package/src/core/WebRTCClient.ts +1012 -0
- package/src/core/httpResponse.ts +22 -0
- package/src/core/iceServers.ts +18 -0
- package/src/core/toolCallNormalize.ts +80 -0
- package/src/core/voice-client.js +236 -0
- package/src/core/webrtcSignaling.ts +72 -0
- package/src/index.js +34 -0
- package/src/index.ts +6 -0
- package/src/platforms/browser.ts +67 -0
- package/src/platforms/react-native.ts +105 -0
- package/src/presets/BookingCalendarModal.tsx +457 -0
- package/src/presets/CameraModal.tsx +576 -0
- package/src/presets/DynamicFormModal.tsx +378 -0
- package/src/presets/NativePresetRenderer.tsx +350 -0
- package/src/presets/NavigateHandler.tsx +75 -0
- package/src/presets/PresetHost.tsx +155 -0
- package/src/presets/PresetShellModal.tsx +97 -0
- package/src/presets/UploadModal.tsx +321 -0
- package/src/presets/calendar/calendarUtils.ts +386 -0
- package/src/presets/call/CallSpeakerToggle.tsx +59 -0
- package/src/presets/call/callAudioRouting.ts +2 -0
- package/src/presets/call/useCallSpeaker.ts +31 -0
- package/src/presets/camera/cameraPermissions.ts +18 -0
- package/src/presets/camera/cameraStream.ts +19 -0
- package/src/presets/camera/cameraUtils.ts +21 -0
- package/src/presets/camera/useLivenessFlow.ts +95 -0
- package/src/presets/chalkboard/ChalkboardOverlay.tsx +156 -0
- package/src/presets/chalkboard/EraseTextHandler.tsx +95 -0
- package/src/presets/chalkboard/TypeTextHandler.tsx +107 -0
- package/src/presets/chalkboard/boardAbort.ts +36 -0
- package/src/presets/chalkboard/boardQueue.ts +620 -0
- package/src/presets/chalkboard/chalkboardSession.ts +75 -0
- package/src/presets/chalkboard/drawUtils.ts +123 -0
- package/src/presets/chalkboard/textUtils.ts +109 -0
- package/src/presets/clipRegion/ClipRegionModal.tsx +261 -0
- package/src/presets/clipRegion/clipRegionBridge.ts +19 -0
- package/src/presets/form/formValidation.ts +104 -0
- package/src/presets/form/parseFormFields.ts +171 -0
- package/src/presets/host/HostElementPresetHandler.tsx +155 -0
- package/src/presets/host/hostPresetBridge.ts +71 -0
- package/src/presets/index.ts +63 -0
- package/src/presets/liveScreen/CloseLiveScreenHandler.tsx +36 -0
- package/src/presets/liveScreen/LiveScreenCaptureHost.tsx +312 -0
- package/src/presets/liveScreen/LiveScreenHandler.tsx +25 -0
- package/src/presets/liveScreen/LiveScreenPipOverlay.tsx +6 -0
- package/src/presets/liveScreen/liveScreenSession.ts +73 -0
- package/src/presets/liveVision/CloseLiveVisionHandler.tsx +29 -0
- package/src/presets/liveVision/LiveVisionCameraHost.tsx +317 -0
- package/src/presets/liveVision/LiveVisionHandler.tsx +26 -0
- package/src/presets/liveVision/LiveVisionPipOverlay.tsx +7 -0
- package/src/presets/liveVision/liveVisionFrameLoop.ts +38 -0
- package/src/presets/liveVision/liveVisionSession.ts +75 -0
- package/src/presets/liveVision/liveVisionUpload.ts +62 -0
- package/src/presets/navigation/internalRouteRegistry.ts +25 -0
- package/src/presets/navigation/navigationBridge.ts +76 -0
- package/src/presets/navigation/navigationTypes.ts +12 -0
- package/src/presets/parseToolCall.ts +60 -0
- package/src/presets/presetClientAdapter.ts +29 -0
- package/src/presets/presetCompletion.ts +91 -0
- package/src/presets/presetEventHelpers.ts +45 -0
- package/src/presets/registry.ts +128 -0
- package/src/presets/streaming/mediaFrameUpload.ts +93 -0
- package/src/presets/types.ts +74 -0
- package/src/presets/upload/pickUploadFile.ts +256 -0
- package/src/presets/upload/uploadFormats.ts +163 -0
- package/src/presets/upload/uploadUtils.ts +68 -0
- package/src/react/PresetRenderer.tsx +144 -0
- package/src/react/index.ts +1 -0
- package/src/runtime/browserRuntime.ts +54 -0
- package/src/runtime/platform.ts +17 -0
- package/src/runtime/reactNativeRuntime.ts +68 -0
- package/src/runtime/resolveRuntimeConfig.ts +75 -0
- package/src/runtime/runtimeBundles.ts +74 -0
- package/src/runtime/types.ts +135 -0
- package/src/types/react-native-incall-manager.d.ts +17 -0
- package/src/types/react-native-webrtc.d.ts +47 -0
- package/src/types.ts +133 -0
- package/src/ui/VaniraWidget.ts +87 -0
- package/src/ui/abstraction/AbstractWidgetProvider.ts +18 -0
- package/src/ui/abstraction/interfaces.ts +12 -0
- package/src/ui/adapters/VaniraChatAdapter.ts +42 -0
- package/src/ui/components/AvatarView.ts +81 -0
- package/src/ui/components/ChatWindow.ts +263 -0
- package/src/ui/components/FloatingButton.ts +163 -0
- package/src/ui/components/FloatingWelcomeChips.ts +137 -0
- package/src/ui/components/Panel.ts +120 -0
- package/src/ui/components/VoiceOrb.ts +79 -0
- package/src/ui/components/VoiceOverlay.ts +497 -0
- package/src/ui/components/index.ts +7 -0
- package/src/ui/factory/WidgetFactory.ts +16 -0
- package/src/ui/icons_data.ts +2 -0
- package/src/ui/presets/WidgetPresetRenderer.ts +1802 -0
- package/src/ui/presets/types.ts +16 -0
- package/src/ui/providers/VaniraInternalProvider.ts +1066 -0
- package/src/ui/styles/index.ts +323 -0
- package/src/ui/styles/keyframes.ts +76 -0
- package/src/ui/styles/theme.ts +57 -0
- package/src/ui/styles/widget.css.ts +838 -0
- package/src/ui/utils.ts +37 -0
- package/src/ui/views/AbstractChatView.ts +93 -0
- package/src/ui/views/AbstractVoiceView.ts +57 -0
- package/src/ui/views/AvatarOnlyView.ts +78 -0
- package/src/ui/views/ChatAvatarView.ts +66 -0
- package/src/ui/views/ChatOnlyView.ts +28 -0
- package/src/ui/views/ChatVoiceView.ts +15 -0
- package/src/ui/views/VoiceOnlyView.ts +25 -0
- package/src/ui/views/index.ts +5 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React Native file descriptor for FormData uploads.
|
|
3
|
+
* RN FormData.append('file', { uri, type, name }) — not Blob/File.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type RNUploadFileDescriptor = {
|
|
7
|
+
uri: string;
|
|
8
|
+
type: string;
|
|
9
|
+
name: string;
|
|
10
|
+
size?: number;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type UploadMediaInput = File | Blob | RNUploadFileDescriptor;
|
|
14
|
+
|
|
15
|
+
const EXT_TO_MIME: Record<string, string> = {
|
|
16
|
+
jpg: 'image/jpeg',
|
|
17
|
+
jpeg: 'image/jpeg',
|
|
18
|
+
png: 'image/png',
|
|
19
|
+
gif: 'image/gif',
|
|
20
|
+
webp: 'image/webp',
|
|
21
|
+
bmp: 'image/bmp',
|
|
22
|
+
heic: 'image/jpeg',
|
|
23
|
+
heif: 'image/jpeg',
|
|
24
|
+
pdf: 'application/pdf',
|
|
25
|
+
txt: 'text/plain',
|
|
26
|
+
csv: 'text/csv',
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const MIME_ALIASES: Record<string, string> = {
|
|
30
|
+
'image/jpg': 'image/jpeg',
|
|
31
|
+
'image/pjpeg': 'image/jpeg',
|
|
32
|
+
'text/comma-separated-values': 'text/csv',
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export function normalizeUploadMimeForUpload(
|
|
36
|
+
file: UploadMediaInput,
|
|
37
|
+
): string {
|
|
38
|
+
if (isRNUploadFileDescriptor(file)) {
|
|
39
|
+
const raw = file.type?.trim().toLowerCase() ?? '';
|
|
40
|
+
if (raw && raw in MIME_ALIASES) {
|
|
41
|
+
return MIME_ALIASES[raw];
|
|
42
|
+
}
|
|
43
|
+
if (raw && raw !== 'application/octet-stream') {
|
|
44
|
+
return raw;
|
|
45
|
+
}
|
|
46
|
+
const ext = file.name.split('.').pop()?.toLowerCase();
|
|
47
|
+
if (ext && ext in EXT_TO_MIME) {
|
|
48
|
+
return EXT_TO_MIME[ext];
|
|
49
|
+
}
|
|
50
|
+
return raw || 'application/octet-stream';
|
|
51
|
+
}
|
|
52
|
+
const raw = file.type?.trim().toLowerCase() ?? '';
|
|
53
|
+
return MIME_ALIASES[raw] ?? raw;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function isRNUploadFileDescriptor(
|
|
57
|
+
file: unknown
|
|
58
|
+
): file is RNUploadFileDescriptor {
|
|
59
|
+
return (
|
|
60
|
+
typeof file === 'object' &&
|
|
61
|
+
file !== null &&
|
|
62
|
+
'uri' in file &&
|
|
63
|
+
typeof (file as RNUploadFileDescriptor).uri === 'string'
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function uploadFileMimeType(file: UploadMediaInput): string {
|
|
68
|
+
return normalizeUploadMimeForUpload(file);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function uploadFileByteSize(file: UploadMediaInput): number | undefined {
|
|
72
|
+
if (isRNUploadFileDescriptor(file)) {
|
|
73
|
+
return file.size;
|
|
74
|
+
}
|
|
75
|
+
return file.size;
|
|
76
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { StorageAdapter } from './StorageAdapter';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Browser storage adapter matching existing WebRTCClient dual-write semantics.
|
|
5
|
+
*
|
|
6
|
+
* Keys (unchanged): `vanira_prospect_id`, `vanira_latest_call_id`
|
|
7
|
+
*
|
|
8
|
+
* - getItem: sessionStorage first, then localStorage fallback
|
|
9
|
+
* - setItem: writes to both (each wrapped in try/catch)
|
|
10
|
+
* - removeItem: removes from both (each wrapped in try/catch)
|
|
11
|
+
*/
|
|
12
|
+
export class BrowserDualStorageAdapter implements StorageAdapter {
|
|
13
|
+
getItem(key: string): string | null {
|
|
14
|
+
try {
|
|
15
|
+
if (typeof sessionStorage !== 'undefined') {
|
|
16
|
+
const sessionValue = sessionStorage.getItem(key);
|
|
17
|
+
if (sessionValue !== null) {
|
|
18
|
+
return sessionValue;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
} catch {
|
|
22
|
+
// SecurityError, private mode, SSR — fall through to localStorage
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
if (typeof localStorage !== 'undefined') {
|
|
27
|
+
return localStorage.getItem(key);
|
|
28
|
+
}
|
|
29
|
+
} catch {
|
|
30
|
+
// ignore
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
setItem(key: string, value: string): void {
|
|
37
|
+
try {
|
|
38
|
+
if (typeof sessionStorage !== 'undefined') {
|
|
39
|
+
sessionStorage.setItem(key, value);
|
|
40
|
+
}
|
|
41
|
+
} catch {
|
|
42
|
+
// ignore blocked / quota errors on sessionStorage
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
if (typeof localStorage !== 'undefined') {
|
|
47
|
+
localStorage.setItem(key, value);
|
|
48
|
+
}
|
|
49
|
+
} catch {
|
|
50
|
+
// ignore blocked / quota errors on localStorage
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
removeItem(key: string): void {
|
|
55
|
+
try {
|
|
56
|
+
if (typeof sessionStorage !== 'undefined') {
|
|
57
|
+
sessionStorage.removeItem(key);
|
|
58
|
+
}
|
|
59
|
+
} catch {
|
|
60
|
+
// ignore
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
if (typeof localStorage !== 'undefined') {
|
|
65
|
+
localStorage.removeItem(key);
|
|
66
|
+
}
|
|
67
|
+
} catch {
|
|
68
|
+
// ignore
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { StorageAdapter } from './StorageAdapter';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* In-memory StorageAdapter for React Native default bundle, unit tests, and SSR.
|
|
5
|
+
*
|
|
6
|
+
* Data does not survive app restarts. In production (`NODE_ENV === 'production'`),
|
|
7
|
+
* setItem logs a one-time warning directing apps to inject a persistent store via
|
|
8
|
+
* `createSyncStorageAdapter()` or `config.storageAdapter`.
|
|
9
|
+
*/
|
|
10
|
+
export class MemoryStorageAdapter implements StorageAdapter {
|
|
11
|
+
private readonly store = new Map<string, string>();
|
|
12
|
+
private productionWarned = false;
|
|
13
|
+
|
|
14
|
+
getItem(key: string): string | null {
|
|
15
|
+
if (!this.store.has(key)) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
return this.store.get(key)!;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
setItem(key: string, value: string): void {
|
|
22
|
+
this.warnProductionOnce();
|
|
23
|
+
this.store.set(key, value);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
removeItem(key: string): void {
|
|
27
|
+
this.store.delete(key);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
private warnProductionOnce(): void {
|
|
31
|
+
if (this.productionWarned) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const isProduction =
|
|
36
|
+
typeof process !== 'undefined' &&
|
|
37
|
+
process.env?.NODE_ENV === 'production';
|
|
38
|
+
|
|
39
|
+
if (!isProduction) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
this.productionWarned = true;
|
|
44
|
+
console.warn(
|
|
45
|
+
'[VaniraSDK] MemoryStorageAdapter does not persist across app restarts. ' +
|
|
46
|
+
'Inject a persistent store via createSyncStorageAdapter() or config.storageAdapter ' +
|
|
47
|
+
'for production React Native apps.'
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Synchronous key-value storage abstraction for session persistence.
|
|
3
|
+
*
|
|
4
|
+
* Used by WebRTCClient for `vanira_prospect_id` and `vanira_latest_call_id`
|
|
5
|
+
* when `sessionBehavior === 'continue'`.
|
|
6
|
+
*
|
|
7
|
+
* Platform defaults (via VaniraRuntime.storage):
|
|
8
|
+
* Browser — BrowserDualStorageAdapter (sessionStorage + localStorage)
|
|
9
|
+
* React Native — MemoryStorageAdapter (in-memory; inject persistent store for production)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export interface StorageAdapter {
|
|
13
|
+
/** Returns the stored value, or null if the key is absent. */
|
|
14
|
+
getItem(key: string): string | null;
|
|
15
|
+
|
|
16
|
+
/** Persists a string value under key. */
|
|
17
|
+
setItem(key: string, value: string): void;
|
|
18
|
+
|
|
19
|
+
/** Removes key from storage. */
|
|
20
|
+
removeItem(key: string): void;
|
|
21
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { StorageAdapter } from './StorageAdapter';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Minimal synchronous backing store for createSyncStorageAdapter().
|
|
5
|
+
*
|
|
6
|
+
* Generic — no dependency on MMKV, AsyncStorage, or any specific RN package.
|
|
7
|
+
* Apps wire their own implementation, e.g. react-native-mmkv:
|
|
8
|
+
*
|
|
9
|
+
* createSyncStorageAdapter({
|
|
10
|
+
* getString: (key) => mmkv.getString(key),
|
|
11
|
+
* setString: (key, value) => { mmkv.set(key, value); },
|
|
12
|
+
* delete: (key) => { mmkv.delete(key); },
|
|
13
|
+
* })
|
|
14
|
+
*/
|
|
15
|
+
export interface SyncStorageBackend {
|
|
16
|
+
getString(key: string): string | undefined;
|
|
17
|
+
setString(key: string, value: string): void;
|
|
18
|
+
/** When omitted, removeItem is a no-op on the backing store. */
|
|
19
|
+
delete?(key: string): void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Wrap a generic sync key-value backend as a StorageAdapter.
|
|
24
|
+
*/
|
|
25
|
+
export function createSyncStorageAdapter(backend: SyncStorageBackend): StorageAdapter {
|
|
26
|
+
return {
|
|
27
|
+
getItem(key: string): string | null {
|
|
28
|
+
const value = backend.getString(key);
|
|
29
|
+
return value === undefined ? null : value;
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
setItem(key: string, value: string): void {
|
|
33
|
+
backend.setString(key, value);
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
removeItem(key: string): void {
|
|
37
|
+
backend.delete?.(key);
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type { StorageAdapter } from './StorageAdapter';
|
|
2
|
+
export { BrowserDualStorageAdapter } from './BrowserDualStorageAdapter';
|
|
3
|
+
export { MemoryStorageAdapter } from './MemoryStorageAdapter';
|
|
4
|
+
export {
|
|
5
|
+
createSyncStorageAdapter,
|
|
6
|
+
type SyncStorageBackend,
|
|
7
|
+
} from './createSyncStorageAdapter';
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
export interface ChatMessage {
|
|
2
|
+
role: 'user' | 'assistant';
|
|
3
|
+
content: string;
|
|
4
|
+
widget?: {
|
|
5
|
+
type: 'carousel' | 'button_list';
|
|
6
|
+
data: any;
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const HASURA_URL = 'https://coredb.travelr.club/v1/graphql';
|
|
11
|
+
let CHAT_URL = 'https://inboxapi.vanira.io';
|
|
12
|
+
|
|
13
|
+
export class ChatService {
|
|
14
|
+
static setChatUrl(url: string) {
|
|
15
|
+
CHAT_URL = url;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
static async createChatProspect(prospectGroupId: string): Promise<string> {
|
|
19
|
+
const mutation = `
|
|
20
|
+
mutation CreateChatProspect($prospectGroupId: uuid!, $name: String!) {
|
|
21
|
+
insert_prospects_one(object: {prospect_group_id: $prospectGroupId, name: $name, source: WEBSITE_WIDGET}) {
|
|
22
|
+
id
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
`;
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const response = await fetch(HASURA_URL, {
|
|
29
|
+
method: 'POST',
|
|
30
|
+
headers: { 'Content-Type': 'application/json' },
|
|
31
|
+
body: JSON.stringify({
|
|
32
|
+
query: mutation,
|
|
33
|
+
variables: {
|
|
34
|
+
prospectGroupId,
|
|
35
|
+
name: `Widget Guest ${new Date().toLocaleString()}`
|
|
36
|
+
}
|
|
37
|
+
}),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const data = await response.json();
|
|
41
|
+
|
|
42
|
+
if (data.errors) {
|
|
43
|
+
console.warn('[VaniraAI] Prospect creation failed, using anonymous ID', data.errors);
|
|
44
|
+
// Fallback Logic
|
|
45
|
+
return `anon_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return data.data?.insert_prospects_one?.id || `anon_${Date.now()}`;
|
|
49
|
+
} catch (error) {
|
|
50
|
+
console.error('[VaniraAI] Failed to create prospect:', error);
|
|
51
|
+
return `anon_${Date.now()}`;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Calls the new Go bridge /widget/chat endpoint with an empty message to initialize the session and get the Welcome config + inbox_id
|
|
56
|
+
static async fetchWelcomeMessage(agentId: string, prospectId: string, widgetId?: string, _pkKey?: string): Promise<ChatMessage & { chatId?: string, conversationId?: string }> {
|
|
57
|
+
try {
|
|
58
|
+
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
|
|
59
|
+
|
|
60
|
+
const response = await fetch(`${CHAT_URL}/widget/chat`, {
|
|
61
|
+
method: 'POST',
|
|
62
|
+
headers,
|
|
63
|
+
body: JSON.stringify({
|
|
64
|
+
agent_id: agentId,
|
|
65
|
+
...(widgetId ? { widget_id: widgetId } : {}),
|
|
66
|
+
message: '',
|
|
67
|
+
prospect_id: prospectId,
|
|
68
|
+
stream: false
|
|
69
|
+
}),
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
if (!response.ok) throw new Error('Failed to fetch welcome message');
|
|
73
|
+
|
|
74
|
+
const data = await response.json();
|
|
75
|
+
let content = data.response || 'Hey! how can I help you ?';
|
|
76
|
+
let widget = undefined;
|
|
77
|
+
const chatId = data.chat_id || data.inbox_id; // Go bridge returns chat_id which is the inbox_id
|
|
78
|
+
const conversationId = data.conversation_id || null;
|
|
79
|
+
|
|
80
|
+
if (data.widget) {
|
|
81
|
+
widget = data.widget;
|
|
82
|
+
// content stays as plain text — widget field carries structured buttons/cards
|
|
83
|
+
// handleWelcomePayload already reads payload.widget for button chips
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return { role: 'assistant', content, widget, chatId, conversationId };
|
|
87
|
+
} catch (error) {
|
|
88
|
+
console.error('[VaniraAI] Failed to fetch welcome message:', error);
|
|
89
|
+
// Graceful Degradation
|
|
90
|
+
return { role: 'assistant', content: 'Hey! how can I help you ?' };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
static async sendChatMessage(
|
|
95
|
+
agentId: string,
|
|
96
|
+
prospectId: string,
|
|
97
|
+
message: string,
|
|
98
|
+
chatId: string | null,
|
|
99
|
+
onChunk: (text: string) => void,
|
|
100
|
+
onWidget: (widget: any) => void,
|
|
101
|
+
onDone: (newChatId: string | null, newConversationId: string | null) => void,
|
|
102
|
+
widgetId?: string,
|
|
103
|
+
_pkKey?: string
|
|
104
|
+
): Promise<void> {
|
|
105
|
+
try {
|
|
106
|
+
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
|
|
107
|
+
|
|
108
|
+
const payload: any = {
|
|
109
|
+
agent_id: agentId,
|
|
110
|
+
message: message,
|
|
111
|
+
prospect_id: prospectId,
|
|
112
|
+
stream: true
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
if (chatId) payload.inbox_id = chatId;
|
|
116
|
+
if (widgetId) payload.widget_id = widgetId;
|
|
117
|
+
|
|
118
|
+
const response = await fetch(`${CHAT_URL}/widget/chat`, {
|
|
119
|
+
method: 'POST',
|
|
120
|
+
headers,
|
|
121
|
+
body: JSON.stringify(payload),
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
if (!response.ok) throw new Error('Chat request failed');
|
|
125
|
+
|
|
126
|
+
const reader = response.body?.getReader();
|
|
127
|
+
const decoder = new TextDecoder();
|
|
128
|
+
if (!reader) throw new Error('No reader');
|
|
129
|
+
|
|
130
|
+
let assistantContent = '';
|
|
131
|
+
let buffer = '';
|
|
132
|
+
let newChatId: string | null = null;
|
|
133
|
+
let newConversationId: string | null = null;
|
|
134
|
+
|
|
135
|
+
while (true) {
|
|
136
|
+
const { done, value } = await reader.read();
|
|
137
|
+
if (done) break;
|
|
138
|
+
|
|
139
|
+
buffer += decoder.decode(value, { stream: true });
|
|
140
|
+
const lines = buffer.split('\n');
|
|
141
|
+
buffer = lines.pop() || '';
|
|
142
|
+
|
|
143
|
+
for (const line of lines) {
|
|
144
|
+
const trimmedLine = line.trim();
|
|
145
|
+
if (!trimmedLine) continue;
|
|
146
|
+
|
|
147
|
+
if (trimmedLine.startsWith('data: ')) {
|
|
148
|
+
const data = trimmedLine.slice(6);
|
|
149
|
+
if (data === '[DONE]') {
|
|
150
|
+
onDone(newChatId, newConversationId);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
try {
|
|
154
|
+
const parsed = JSON.parse(data);
|
|
155
|
+
if (parsed.type === 'metadata') {
|
|
156
|
+
// Go bridge sends inbox_id (not chat_id) in metadata — read both
|
|
157
|
+
if (parsed.chat_id || parsed.inbox_id) newChatId = parsed.chat_id || parsed.inbox_id;
|
|
158
|
+
if (parsed.conversation_id) newConversationId = parsed.conversation_id;
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
const content = parsed.choices?.[0]?.delta?.content;
|
|
162
|
+
if (content) {
|
|
163
|
+
assistantContent += content;
|
|
164
|
+
onChunk(assistantContent);
|
|
165
|
+
}
|
|
166
|
+
if (parsed.widget) {
|
|
167
|
+
onWidget(parsed.widget);
|
|
168
|
+
}
|
|
169
|
+
if (parsed.chat_id && !newChatId) {
|
|
170
|
+
newChatId = parsed.chat_id;
|
|
171
|
+
}
|
|
172
|
+
if (parsed.conversation_id && !newConversationId) {
|
|
173
|
+
newConversationId = parsed.conversation_id;
|
|
174
|
+
}
|
|
175
|
+
} catch (e) { }
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
onDone(newChatId, newConversationId);
|
|
181
|
+
} catch (error) {
|
|
182
|
+
console.error('[VaniraAI] Chat error:', error);
|
|
183
|
+
onChunk('Sorry, I encountered an error. Please try again.');
|
|
184
|
+
onDone(null, null);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
static listenForAdminReplies(inboxId: string, sender: string, onMessage: (content: string) => void): { close: () => void } {
|
|
188
|
+
const url = `${CHAT_URL}/inbox/stream?inbox_id=${inboxId}&sender=${encodeURIComponent(sender)}`;
|
|
189
|
+
let es: EventSource | null = null;
|
|
190
|
+
let reconnectAttempts = 0;
|
|
191
|
+
let isClosed = false;
|
|
192
|
+
|
|
193
|
+
const connect = () => {
|
|
194
|
+
if (isClosed) return;
|
|
195
|
+
|
|
196
|
+
es = new EventSource(url);
|
|
197
|
+
console.log(`[VaniraAI] Subscribed to SSE EventSource at ${es.url}`);
|
|
198
|
+
|
|
199
|
+
es.onopen = () => {
|
|
200
|
+
console.log("[VaniraAI] SSE connection opened successfully.");
|
|
201
|
+
reconnectAttempts = 0; // Reset backoff on success
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
es.onmessage = (event) => {
|
|
205
|
+
console.log("[VaniraAI] 📡 Raw SSE Event triggered:", event.data);
|
|
206
|
+
try {
|
|
207
|
+
const data = JSON.parse(event.data);
|
|
208
|
+
console.log("[VaniraAI] SSE stream JSON parsed:", data);
|
|
209
|
+
|
|
210
|
+
const isOutgoing = data.direction === 'outgoing';
|
|
211
|
+
const hasContent = !!data.content;
|
|
212
|
+
const isNotAI = data.source !== 'ai';
|
|
213
|
+
|
|
214
|
+
if (isOutgoing && hasContent && isNotAI) {
|
|
215
|
+
console.log("[VaniraAI] 🎯 Displaying admin message in chat UI:", data.content);
|
|
216
|
+
onMessage(data.content);
|
|
217
|
+
}
|
|
218
|
+
} catch (e) {
|
|
219
|
+
console.error('[VaniraAI] SSE parse error', e);
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
es.onerror = (err) => {
|
|
224
|
+
console.error('[VaniraAI] SSE connection error or disconnect', err);
|
|
225
|
+
if (es) {
|
|
226
|
+
es.close();
|
|
227
|
+
es = null;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (isClosed) return;
|
|
231
|
+
|
|
232
|
+
// Exponential backoff: 2s, 4s, 8s, 16s... max 30s
|
|
233
|
+
const delay = Math.min(2000 * Math.pow(2, reconnectAttempts), 30000);
|
|
234
|
+
console.log(`[VaniraAI] Reconnecting SSE in ${delay}ms (Attempt ${reconnectAttempts + 1})...`);
|
|
235
|
+
reconnectAttempts++;
|
|
236
|
+
|
|
237
|
+
setTimeout(connect, delay);
|
|
238
|
+
};
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
connect(); // Start initial connection
|
|
242
|
+
|
|
243
|
+
return {
|
|
244
|
+
close: () => {
|
|
245
|
+
isClosed = true;
|
|
246
|
+
if (es) {
|
|
247
|
+
es.close();
|
|
248
|
+
es = null;
|
|
249
|
+
console.log("[VaniraAI] SSE connection manually closed.");
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
static async createCall(agentId: string, _clientId: string | null, prospectId: string | null, _widgetMode: string, pkKey?: string): Promise<{ callId: string, workerUrl: string }> {
|
|
256
|
+
try {
|
|
257
|
+
const BACKEND_URL = 'https://api.vanira.io';
|
|
258
|
+
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
|
|
259
|
+
if (pkKey) headers['X-API-Key'] = pkKey;
|
|
260
|
+
|
|
261
|
+
const response = await fetch(`${BACKEND_URL}/calls/create`, {
|
|
262
|
+
method: 'POST',
|
|
263
|
+
headers,
|
|
264
|
+
body: JSON.stringify({
|
|
265
|
+
agent_id: agentId,
|
|
266
|
+
prospect_id: prospectId || undefined,
|
|
267
|
+
type: 'web',
|
|
268
|
+
}),
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
const data = await response.json();
|
|
272
|
+
|
|
273
|
+
if (!response.ok) {
|
|
274
|
+
throw new Error(`[VaniraAI] Call creation failed HTTP ${response.status}`);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (!data.worker_url) {
|
|
278
|
+
throw new Error('[VaniraAI] Worker URL missing from response. Call cannot proceed.');
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return {
|
|
282
|
+
callId: data.call_id || data.id || `web_${Date.now()}`,
|
|
283
|
+
workerUrl: data.worker_url // Pass the full authenticated URL
|
|
284
|
+
};
|
|
285
|
+
} catch (error) {
|
|
286
|
+
console.error('[VaniraAI] Failed to create call:', error);
|
|
287
|
+
throw error; // Re-throw to ensure caller knows it failed
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
static async resolveConversation(conversationId: string): Promise<void> {
|
|
292
|
+
try {
|
|
293
|
+
const response = await fetch(`${CHAT_URL}/inbox/conversations/resolve`, {
|
|
294
|
+
method: 'POST',
|
|
295
|
+
headers: { 'Content-Type': 'application/json' },
|
|
296
|
+
body: JSON.stringify({ conversation_id: conversationId }),
|
|
297
|
+
});
|
|
298
|
+
if (!response.ok) throw new Error('Failed to resolve conversation');
|
|
299
|
+
} catch (error) {
|
|
300
|
+
console.error('[VaniraAI] Failed to resolve conversation:', error);
|
|
301
|
+
throw error;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { WidgetConfig } from '../../types';
|
|
2
|
+
|
|
3
|
+
const BACKEND_URL = 'https://api.vanira.io';
|
|
4
|
+
|
|
5
|
+
export class ConfigService {
|
|
6
|
+
static async fetchWidgetConfig(widgetId: string, pkKey?: string): Promise<WidgetConfig> {
|
|
7
|
+
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
|
|
8
|
+
if (pkKey) headers['X-API-Key'] = pkKey;
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
const response = await fetch(`${BACKEND_URL}/assistant/widget/${widgetId}/config`, {
|
|
12
|
+
method: 'GET',
|
|
13
|
+
headers,
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
if (!response.ok) {
|
|
17
|
+
throw new Error(`Widget config fetch failed: HTTP ${response.status}`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const config = await response.json();
|
|
21
|
+
|
|
22
|
+
// Flatten agent.client into config.client to match WidgetConfig shape
|
|
23
|
+
if (config.agent?.client) {
|
|
24
|
+
config.client = config.agent.client;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return config as WidgetConfig;
|
|
28
|
+
} catch (error) {
|
|
29
|
+
console.error('[VaniraAI] Failed to fetch widget config:', error);
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export const icons = {
|
|
2
|
+
phone: `
|
|
3
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
4
|
+
<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"/>
|
|
5
|
+
</svg>
|
|
6
|
+
`,
|
|
7
|
+
close: `
|
|
8
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
9
|
+
<line x1="18" y1="6" x2="6" y2="18"/>
|
|
10
|
+
<line x1="6" y1="6" x2="18" y2="18"/>
|
|
11
|
+
</svg>
|
|
12
|
+
`,
|
|
13
|
+
mic: `
|
|
14
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
15
|
+
<path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3z"/>
|
|
16
|
+
<path d="M19 10v2a7 7 0 0 1-14 0v-2"/>
|
|
17
|
+
<line x1="12" y1="19" x2="12" y2="22"/>
|
|
18
|
+
</svg>
|
|
19
|
+
`,
|
|
20
|
+
micOff: `
|
|
21
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
22
|
+
<line x1="1" y1="1" x2="23" y2="23"/>
|
|
23
|
+
<path d="M9 9v3a3 3 0 0 0 5.12 2.12M15 9.34V4a3 3 0 0 0-5.94-.6"/>
|
|
24
|
+
<path d="M17 16.95A7 7 0 0 1 5 12v-2m14 0v2a7 7 0 0 1-.11 1.23"/>
|
|
25
|
+
<line x1="12" y1="19" x2="12" y2="23"/>
|
|
26
|
+
</svg>
|
|
27
|
+
`,
|
|
28
|
+
waves: `
|
|
29
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
30
|
+
<path d="M2 6c.6.5 1.2 1 2.5 1C7 7 7 5 9.5 5c2.6 0 2.4 2 5 2 2.5 0 2.5-2 5-2 1.3 0 1.9.5 2.5 1"/>
|
|
31
|
+
<path d="M2 12c.6.5 1.2 1 2.5 1 2.5 0 2.5-2 5-2 2.6 0 2.4 2 5 2 2.5 0 2.5-2 5-2 1.3 0 1.9.5 2.5 1"/>
|
|
32
|
+
<path d="M2 18c.6.5 1.2 1 2.5 1 2.5 0 2.5-2 5-2 2.6 0 2.4 2 5 2 2.5 0 2.5-2 5-2 1.3 0 1.9.5 2.5 1"/>
|
|
33
|
+
</svg>
|
|
34
|
+
`
|
|
35
|
+
};
|