@graineai/inapp-react-native 0.33.2 → 0.33.4
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/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/protocol.d.ts +54 -0
- package/dist/protocol.js +83 -0
- package/dist/react-native/index.d.ts +2 -0
- package/dist/react-native/index.js +2 -1
- package/dist/react-native/voice-launcher.d.ts +13 -1
- package/dist/react-native/voice-launcher.js +105 -5
- package/dist/react-native/widget.js +104 -37
- package/package.json +8 -6
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export type { ActionHandler, AppActionRequest, AppActionResult, AppEvent, ScreenContext, ScreenField, WidgetSurface, } from "./protocol.js";
|
|
2
2
|
export { widgetSurface } from "./protocol.js";
|
|
3
|
+
export { WIDGET_PRESETS, WIDGET_IMAGE_SIZES, WIDGET_DENSITIES, WIDGET_BUTTONS, WIDGET_SURFACE_CHOICES, DEFAULT_WIDGET_PRESET, WIDGET_PLATFORMS, PLATFORM_GRAMMAR, normaliseWidgetStyle, resolveWidgetLook, resolvePlatform, } from "./protocol.js";
|
|
4
|
+
export type { WidgetStyle, WidgetLook, WidgetPreset, WidgetImageSize, WidgetDensity, WidgetButton, WidgetPlatform, PlatformGrammar } from "./protocol.js";
|
|
3
5
|
export { GraineWidget } from "./react-native/widget.js";
|
|
4
6
|
export type { GraineWidgetProps, GraineWidgetTheme } from "./react-native/widget.js";
|
|
5
7
|
export { GraineInAppClient, type GraineInAppOptions, type SessionConfig, type ActionDiagnosis, } from "./client.js";
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { widgetSurface } from "./protocol.js";
|
|
2
|
+
export { WIDGET_PRESETS, WIDGET_IMAGE_SIZES, WIDGET_DENSITIES, WIDGET_BUTTONS, WIDGET_SURFACE_CHOICES, DEFAULT_WIDGET_PRESET, WIDGET_PLATFORMS, PLATFORM_GRAMMAR, normaliseWidgetStyle, resolveWidgetLook, resolvePlatform, } from "./protocol.js";
|
|
2
3
|
export { GraineWidget } from "./react-native/widget.js";
|
|
3
4
|
export { GraineInAppClient, } from "./client.js";
|
|
4
5
|
export { FrictionDetector, resolveFriction, rulesFor, DEFAULT_FRICTION, } from "./friction.js";
|
package/dist/protocol.d.ts
CHANGED
|
@@ -64,3 +64,57 @@ export declare const UNNAMED_TAP = "tap:screen";
|
|
|
64
64
|
export declare function resolveWidgetImages(data: Record<string, unknown>, baseUrl: string): Record<string, unknown>;
|
|
65
65
|
export type WidgetSurface = "screen" | "sheet" | "card";
|
|
66
66
|
export declare function widgetSurface(kind: string | null | undefined): WidgetSurface;
|
|
67
|
+
export type WidgetPreset = "gallery" | "cards" | "list" | "spotlight" | "compact";
|
|
68
|
+
export type WidgetImageSize = "small" | "medium" | "large";
|
|
69
|
+
export type WidgetDensity = "comfortable" | "compact";
|
|
70
|
+
export type WidgetButton = "filled" | "outline" | "row";
|
|
71
|
+
export type WidgetPlatform = "auto" | "ios" | "material" | "oneui";
|
|
72
|
+
export interface WidgetStyle {
|
|
73
|
+
preset?: WidgetPreset;
|
|
74
|
+
imageSize?: WidgetImageSize;
|
|
75
|
+
density?: WidgetDensity;
|
|
76
|
+
button?: WidgetButton;
|
|
77
|
+
facts?: boolean;
|
|
78
|
+
columns?: 1 | 2;
|
|
79
|
+
surface?: "auto" | WidgetSurface;
|
|
80
|
+
radius?: number;
|
|
81
|
+
accent?: string;
|
|
82
|
+
platform?: WidgetPlatform;
|
|
83
|
+
}
|
|
84
|
+
export declare const WIDGET_PRESETS: ReadonlyArray<WidgetPreset>;
|
|
85
|
+
export declare const WIDGET_IMAGE_SIZES: ReadonlyArray<WidgetImageSize>;
|
|
86
|
+
export declare const WIDGET_DENSITIES: ReadonlyArray<WidgetDensity>;
|
|
87
|
+
export declare const WIDGET_BUTTONS: ReadonlyArray<WidgetButton>;
|
|
88
|
+
export declare const WIDGET_SURFACE_CHOICES: ReadonlyArray<"auto" | WidgetSurface>;
|
|
89
|
+
export declare const WIDGET_PLATFORMS: ReadonlyArray<WidgetPlatform>;
|
|
90
|
+
export declare const DEFAULT_WIDGET_PRESET: WidgetPreset;
|
|
91
|
+
export declare function normaliseWidgetStyle(raw: unknown): WidgetStyle;
|
|
92
|
+
export interface WidgetLook {
|
|
93
|
+
preset: WidgetPreset;
|
|
94
|
+
imageSize: WidgetImageSize;
|
|
95
|
+
density: WidgetDensity;
|
|
96
|
+
button: WidgetButton;
|
|
97
|
+
facts: boolean;
|
|
98
|
+
columns: 1 | 2;
|
|
99
|
+
surface: WidgetSurface;
|
|
100
|
+
radius?: number;
|
|
101
|
+
accent?: string;
|
|
102
|
+
platform: WidgetPlatform;
|
|
103
|
+
}
|
|
104
|
+
export declare function resolveWidgetLook(style: WidgetStyle | null | undefined, kind: string | null | undefined): WidgetLook;
|
|
105
|
+
export interface PlatformGrammar {
|
|
106
|
+
radius: number;
|
|
107
|
+
chipRadius: number;
|
|
108
|
+
buttonRadius: number;
|
|
109
|
+
pillButtons: boolean;
|
|
110
|
+
separatorInset: number;
|
|
111
|
+
grabber: {
|
|
112
|
+
width: number;
|
|
113
|
+
height: number;
|
|
114
|
+
};
|
|
115
|
+
titleWeight: "600" | "700" | "500";
|
|
116
|
+
padBoost: number;
|
|
117
|
+
chipOutlined: boolean;
|
|
118
|
+
}
|
|
119
|
+
export declare const PLATFORM_GRAMMAR: Record<Exclude<WidgetPlatform, "auto">, PlatformGrammar>;
|
|
120
|
+
export declare function resolvePlatform(platform: WidgetPlatform | undefined, os: string | undefined, manufacturer?: string | null): Exclude<WidgetPlatform, "auto">;
|
package/dist/protocol.js
CHANGED
|
@@ -41,3 +41,86 @@ const SURFACE = {
|
|
|
41
41
|
export function widgetSurface(kind) {
|
|
42
42
|
return SURFACE[String(kind ?? "").trim().toLowerCase()] ?? "card";
|
|
43
43
|
}
|
|
44
|
+
export const WIDGET_PRESETS = ["gallery", "cards", "list", "spotlight", "compact"];
|
|
45
|
+
export const WIDGET_IMAGE_SIZES = ["small", "medium", "large"];
|
|
46
|
+
export const WIDGET_DENSITIES = ["comfortable", "compact"];
|
|
47
|
+
export const WIDGET_BUTTONS = ["filled", "outline", "row"];
|
|
48
|
+
export const WIDGET_SURFACE_CHOICES = ["auto", "screen", "sheet", "card"];
|
|
49
|
+
export const WIDGET_PLATFORMS = ["auto", "ios", "material", "oneui"];
|
|
50
|
+
export const DEFAULT_WIDGET_PRESET = "cards";
|
|
51
|
+
export function normaliseWidgetStyle(raw) {
|
|
52
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
53
|
+
return {};
|
|
54
|
+
const r = raw;
|
|
55
|
+
const out = {};
|
|
56
|
+
const pick = (v, allowed) => {
|
|
57
|
+
const s = String(v ?? "").trim().toLowerCase();
|
|
58
|
+
return allowed.includes(s) ? s : undefined;
|
|
59
|
+
};
|
|
60
|
+
const preset = pick(r.preset, WIDGET_PRESETS);
|
|
61
|
+
if (preset)
|
|
62
|
+
out.preset = preset;
|
|
63
|
+
const imageSize = pick(r.imageSize, WIDGET_IMAGE_SIZES);
|
|
64
|
+
if (imageSize)
|
|
65
|
+
out.imageSize = imageSize;
|
|
66
|
+
const density = pick(r.density, WIDGET_DENSITIES);
|
|
67
|
+
if (density)
|
|
68
|
+
out.density = density;
|
|
69
|
+
const button = pick(r.button, WIDGET_BUTTONS);
|
|
70
|
+
if (button)
|
|
71
|
+
out.button = button;
|
|
72
|
+
if (typeof r.facts === "boolean")
|
|
73
|
+
out.facts = r.facts;
|
|
74
|
+
if (r.columns === 2 || r.columns === "2")
|
|
75
|
+
out.columns = 2;
|
|
76
|
+
else if (r.columns === 1 || r.columns === "1")
|
|
77
|
+
out.columns = 1;
|
|
78
|
+
const surface = pick(r.surface, WIDGET_SURFACE_CHOICES);
|
|
79
|
+
if (surface)
|
|
80
|
+
out.surface = surface;
|
|
81
|
+
if (typeof r.radius === "number" && Number.isFinite(r.radius))
|
|
82
|
+
out.radius = Math.max(0, Math.min(24, Math.round(r.radius)));
|
|
83
|
+
if (typeof r.accent === "string" && /^#[0-9a-f]{6}$/i.test(r.accent.trim()))
|
|
84
|
+
out.accent = r.accent.trim().toUpperCase();
|
|
85
|
+
const platform = pick(r.platform, WIDGET_PLATFORMS);
|
|
86
|
+
if (platform)
|
|
87
|
+
out.platform = platform;
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
const PRESET_DEFAULTS = {
|
|
91
|
+
gallery: { imageSize: "large", button: "filled" },
|
|
92
|
+
cards: { imageSize: "medium", button: "filled" },
|
|
93
|
+
list: { imageSize: "small", button: "row" },
|
|
94
|
+
spotlight: { imageSize: "large", button: "filled" },
|
|
95
|
+
compact: { imageSize: "small", button: "outline" },
|
|
96
|
+
};
|
|
97
|
+
export function resolveWidgetLook(style, kind) {
|
|
98
|
+
const s = normaliseWidgetStyle(style);
|
|
99
|
+
const preset = s.preset ?? DEFAULT_WIDGET_PRESET;
|
|
100
|
+
const d = PRESET_DEFAULTS[preset];
|
|
101
|
+
const surface = s.surface && s.surface !== "auto" ? s.surface : widgetSurface(kind);
|
|
102
|
+
return {
|
|
103
|
+
preset,
|
|
104
|
+
imageSize: s.imageSize ?? d.imageSize,
|
|
105
|
+
density: s.density ?? "comfortable",
|
|
106
|
+
button: s.button ?? d.button,
|
|
107
|
+
facts: s.facts ?? true,
|
|
108
|
+
columns: (preset === "gallery" || preset === "cards") && s.columns === 2 ? 2 : 1,
|
|
109
|
+
surface,
|
|
110
|
+
...(s.radius !== undefined ? { radius: s.radius } : {}),
|
|
111
|
+
...(s.accent ? { accent: s.accent } : {}),
|
|
112
|
+
platform: s.platform ?? "auto",
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
export const PLATFORM_GRAMMAR = {
|
|
116
|
+
ios: { radius: 12, chipRadius: 8, buttonRadius: 12, pillButtons: false, separatorInset: 16, grabber: { width: 36, height: 5 }, titleWeight: "600", padBoost: 0, chipOutlined: false },
|
|
117
|
+
material: { radius: 16, chipRadius: 8, buttonRadius: 20, pillButtons: true, separatorInset: 0, grabber: { width: 32, height: 4 }, titleWeight: "500", padBoost: 0, chipOutlined: true },
|
|
118
|
+
oneui: { radius: 26, chipRadius: 999, buttonRadius: 999, pillButtons: true, separatorInset: 0, grabber: { width: 40, height: 4 }, titleWeight: "700", padBoost: 2, chipOutlined: false },
|
|
119
|
+
};
|
|
120
|
+
export function resolvePlatform(platform, os, manufacturer) {
|
|
121
|
+
if (platform && platform !== "auto")
|
|
122
|
+
return platform;
|
|
123
|
+
if (os === "android")
|
|
124
|
+
return /samsung/i.test(String(manufacturer ?? "")) ? "oneui" : "material";
|
|
125
|
+
return "ios";
|
|
126
|
+
}
|
|
@@ -2,6 +2,7 @@ import React from "react";
|
|
|
2
2
|
import { GraineInAppClient, type GraineInAppOptions } from "../client.js";
|
|
3
3
|
import { type AudioAdapter, type VoiceSessionOptions } from "../voice.js";
|
|
4
4
|
import type { ActionHandler, ScreenContext, ScreenField } from "../protocol.js";
|
|
5
|
+
import { type WidgetStyle } from "../protocol.js";
|
|
5
6
|
export { GraineFab, GraineTouchCapture } from "./fab.js";
|
|
6
7
|
export type { GraineFabProps, GraineTouchCaptureProps } from "./fab.js";
|
|
7
8
|
export { widgetSurface } from "../protocol.js";
|
|
@@ -73,6 +74,7 @@ export interface GraineWidget {
|
|
|
73
74
|
kind: string;
|
|
74
75
|
data: Record<string, unknown>;
|
|
75
76
|
fallbackText: string;
|
|
77
|
+
style?: WidgetStyle;
|
|
76
78
|
}
|
|
77
79
|
export interface Caption {
|
|
78
80
|
role: "agent" | "customer";
|
|
@@ -3,7 +3,7 @@ import { createContext, useCallback, useContext, useEffect, useMemo, useRef, use
|
|
|
3
3
|
import { AppState } from "react-native";
|
|
4
4
|
import { GraineInAppClient } from "../client.js";
|
|
5
5
|
import { VoiceSession } from "../voice.js";
|
|
6
|
-
import { resolveWidgetImages } from "../protocol.js";
|
|
6
|
+
import { resolveWidgetImages, normaliseWidgetStyle } from "../protocol.js";
|
|
7
7
|
import { GraineFab, GraineTouchCapture } from "./fab.js";
|
|
8
8
|
export { GraineFab, GraineTouchCapture } from "./fab.js";
|
|
9
9
|
export { widgetSurface } from "../protocol.js";
|
|
@@ -396,6 +396,7 @@ export function useGraineWidget() {
|
|
|
396
396
|
kind: String(w.kind || ""),
|
|
397
397
|
data: resolveWidgetImages((w.data || {}), client.opts.baseUrl),
|
|
398
398
|
fallbackText: String(w.fallback_text || ""),
|
|
399
|
+
style: normaliseWidgetStyle(w.style),
|
|
399
400
|
}
|
|
400
401
|
: null, [client]);
|
|
401
402
|
const [widget, setWidget] = useState(() => adopt(client.pendingWidget));
|
|
@@ -23,9 +23,21 @@ export interface GraineVoiceLauncherProps {
|
|
|
23
23
|
onEnded?: () => void;
|
|
24
24
|
onMicDenied?: (reason: string) => void;
|
|
25
25
|
onError?: (message: string) => void;
|
|
26
|
+
timing?: {
|
|
27
|
+
readyTimeoutMs?: number;
|
|
28
|
+
retryDelaysMs?: number[];
|
|
29
|
+
endWatchdogMs?: number;
|
|
30
|
+
};
|
|
26
31
|
children?: (api: GraineVoiceApi) => React.ReactNode;
|
|
27
32
|
}
|
|
33
|
+
export interface GraineConnectivity {
|
|
34
|
+
state: "loading" | "ready" | "retrying" | "failed";
|
|
35
|
+
attempt: number;
|
|
36
|
+
reason?: string;
|
|
37
|
+
}
|
|
28
38
|
export interface GraineVoiceApi {
|
|
39
|
+
connectivity: GraineConnectivity;
|
|
40
|
+
retry: () => void;
|
|
29
41
|
connected: boolean;
|
|
30
42
|
connecting: boolean;
|
|
31
43
|
muted: boolean;
|
|
@@ -40,4 +52,4 @@ export interface GraineVoiceApi {
|
|
|
40
52
|
clearCaptions: () => void;
|
|
41
53
|
}
|
|
42
54
|
export declare const CAPTION_HISTORY = 12;
|
|
43
|
-
export declare function GraineVoiceLauncher({ webView: WebView, autoStart, active, transport, onTransport, onCaption, onCallState, onMicDenied, onError, onEnded, children, }: GraineVoiceLauncherProps): React.JSX.Element;
|
|
55
|
+
export declare function GraineVoiceLauncher({ webView: WebView, autoStart, active, transport, onTransport, onCaption, onCallState, onMicDenied, onError, timing, onEnded, children, }: GraineVoiceLauncherProps): React.JSX.Element;
|
|
@@ -4,13 +4,14 @@ import { AppState, View } from "react-native";
|
|
|
4
4
|
import { useGraineAgent } from "./index.js";
|
|
5
5
|
import { requestMicrophonePermission } from "./permissions.js";
|
|
6
6
|
export const CAPTION_HISTORY = 12;
|
|
7
|
-
export function GraineVoiceLauncher({ webView: WebView, autoStart = false, active = true, transport, onTransport, onCaption, onCallState, onMicDenied, onError, onEnded, children, }) {
|
|
7
|
+
export function GraineVoiceLauncher({ webView: WebView, autoStart = false, active = true, transport, onTransport, onCaption, onCallState, onMicDenied, onError, timing, onEnded, children, }) {
|
|
8
8
|
const { client, appearance } = useGraineAgent();
|
|
9
9
|
const ref = useRef(null);
|
|
10
10
|
const startedRef = useRef(false);
|
|
11
11
|
const activeRef = useRef(active);
|
|
12
12
|
activeRef.current = active;
|
|
13
13
|
const mountedIdleRef = useRef(!active);
|
|
14
|
+
const mountStampRef = useRef(Date.now());
|
|
14
15
|
const [connected, setConnected] = useState(false);
|
|
15
16
|
const [connecting, setConnecting] = useState(false);
|
|
16
17
|
const [muted, setMutedState] = useState(false);
|
|
@@ -37,6 +38,88 @@ export function GraineVoiceLauncher({ webView: WebView, autoStart = false, activ
|
|
|
37
38
|
const [voiceAgentId, setVoiceAgentId] = useState(client.config ? client.config.webcallAgentId || client.config.agentId : null);
|
|
38
39
|
const onErrorRef = useRef(onError);
|
|
39
40
|
onErrorRef.current = onError;
|
|
41
|
+
const READY_TIMEOUT_MS = timing?.readyTimeoutMs ?? 15000;
|
|
42
|
+
const RETRY_DELAYS_MS = timing?.retryDelaysMs ?? [1000, 2000, 4000, 8000, 15000, 15000];
|
|
43
|
+
const END_WATCHDOG_MS = timing?.endWatchdogMs ?? 4000;
|
|
44
|
+
const [connectivity, setConnectivity] = useState({ state: "loading", attempt: 0 });
|
|
45
|
+
const attemptRef = useRef(0);
|
|
46
|
+
const retryTimerRef = useRef(null);
|
|
47
|
+
const readyTimerRef = useRef(null);
|
|
48
|
+
const endWatchdogRef = useRef(null);
|
|
49
|
+
const connectedRef = useRef(false);
|
|
50
|
+
useEffect(() => { connectedRef.current = connected; }, [connected]);
|
|
51
|
+
const endedNotifiedRef = useRef(true);
|
|
52
|
+
const onEndedRef = useRef(onEnded);
|
|
53
|
+
onEndedRef.current = onEnded;
|
|
54
|
+
const clearTimer = (r) => {
|
|
55
|
+
if (r.current) {
|
|
56
|
+
clearTimeout(r.current);
|
|
57
|
+
r.current = null;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
const scheduleRetry = useCallback((reason) => {
|
|
61
|
+
clearTimer(readyTimerRef);
|
|
62
|
+
if (connectedRef.current)
|
|
63
|
+
return;
|
|
64
|
+
const attempt = attemptRef.current;
|
|
65
|
+
if (attempt >= RETRY_DELAYS_MS.length) {
|
|
66
|
+
setConnectivity({ state: "failed", attempt, reason });
|
|
67
|
+
setConnecting(false);
|
|
68
|
+
pendingStartRef.current = false;
|
|
69
|
+
onErrorRef.current?.("The assistant could not load. Check your connection and try again.");
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const base = RETRY_DELAYS_MS[attempt];
|
|
73
|
+
const delay = Math.round(base * (0.75 + Math.random() * 0.5));
|
|
74
|
+
attemptRef.current = attempt + 1;
|
|
75
|
+
setConnectivity({ state: "retrying", attempt: attempt + 1, reason });
|
|
76
|
+
setReady(false);
|
|
77
|
+
clearTimer(retryTimerRef);
|
|
78
|
+
retryTimerRef.current = setTimeout(() => {
|
|
79
|
+
retryTimerRef.current = null;
|
|
80
|
+
try {
|
|
81
|
+
ref.current?.reload();
|
|
82
|
+
}
|
|
83
|
+
catch { }
|
|
84
|
+
}, delay);
|
|
85
|
+
}, []);
|
|
86
|
+
const armReadyTimer = useCallback(() => {
|
|
87
|
+
clearTimer(readyTimerRef);
|
|
88
|
+
readyTimerRef.current = setTimeout(() => {
|
|
89
|
+
readyTimerRef.current = null;
|
|
90
|
+
scheduleRetry("ready-timeout");
|
|
91
|
+
}, READY_TIMEOUT_MS);
|
|
92
|
+
}, [scheduleRetry]);
|
|
93
|
+
const restartLoad = useCallback(() => {
|
|
94
|
+
clearTimer(retryTimerRef);
|
|
95
|
+
attemptRef.current = 0;
|
|
96
|
+
setConnectivity({ state: "loading", attempt: 0 });
|
|
97
|
+
setReady(false);
|
|
98
|
+
try {
|
|
99
|
+
ref.current?.reload();
|
|
100
|
+
}
|
|
101
|
+
catch { }
|
|
102
|
+
armReadyTimer();
|
|
103
|
+
}, [armReadyTimer]);
|
|
104
|
+
const forceEnd = useCallback((why) => {
|
|
105
|
+
clearTimer(endWatchdogRef);
|
|
106
|
+
setConnected(false);
|
|
107
|
+
setConnecting(false);
|
|
108
|
+
connectedRef.current = false;
|
|
109
|
+
if (!endedNotifiedRef.current) {
|
|
110
|
+
endedNotifiedRef.current = true;
|
|
111
|
+
console.warn(`[graine] the page did not confirm the call ended (${why}); ending it locally`);
|
|
112
|
+
onEndedRef.current?.();
|
|
113
|
+
}
|
|
114
|
+
restartLoad();
|
|
115
|
+
}, [restartLoad]);
|
|
116
|
+
const armEndWatchdog = useCallback(() => {
|
|
117
|
+
clearTimer(endWatchdogRef);
|
|
118
|
+
if (!connectedRef.current)
|
|
119
|
+
return;
|
|
120
|
+
endWatchdogRef.current = setTimeout(() => forceEnd("watchdog"), END_WATCHDOG_MS);
|
|
121
|
+
}, [forceEnd]);
|
|
122
|
+
useEffect(() => () => { clearTimer(retryTimerRef); clearTimer(readyTimerRef); clearTimer(endWatchdogRef); }, []);
|
|
40
123
|
useEffect(() => {
|
|
41
124
|
if (voiceAgentId)
|
|
42
125
|
return;
|
|
@@ -74,6 +157,7 @@ export function GraineVoiceLauncher({ webView: WebView, autoStart = false, activ
|
|
|
74
157
|
return client.on("screen", () => pushContext());
|
|
75
158
|
}, [client, ready, pushContext]);
|
|
76
159
|
const beginCall = useCallback(async () => {
|
|
160
|
+
endedNotifiedRef.current = false;
|
|
77
161
|
if (client.isConnected) {
|
|
78
162
|
console.warn("[graine] The provider has its own connection open while GraineVoiceLauncher " +
|
|
79
163
|
"is starting a call. Pass autoConnect={false} to <GraineProvider> — otherwise " +
|
|
@@ -131,6 +215,10 @@ export function GraineVoiceLauncher({ webView: WebView, autoStart = false, activ
|
|
|
131
215
|
}
|
|
132
216
|
return;
|
|
133
217
|
case "graine:ready":
|
|
218
|
+
clearTimer(readyTimerRef);
|
|
219
|
+
clearTimer(retryTimerRef);
|
|
220
|
+
attemptRef.current = 0;
|
|
221
|
+
setConnectivity({ state: "ready", attempt: 0 });
|
|
134
222
|
setReady(true);
|
|
135
223
|
pushContext();
|
|
136
224
|
if ((autoStart || pendingStartRef.current) && !startedRef.current && activeRef.current) {
|
|
@@ -143,6 +231,8 @@ export function GraineVoiceLauncher({ webView: WebView, autoStart = false, activ
|
|
|
143
231
|
}
|
|
144
232
|
return;
|
|
145
233
|
case "graine:call":
|
|
234
|
+
if (msg.connected)
|
|
235
|
+
endedNotifiedRef.current = false;
|
|
146
236
|
setConnected(!!msg.connected);
|
|
147
237
|
setConnecting(!!msg.connecting);
|
|
148
238
|
if (!msg.connected && !msg.connecting)
|
|
@@ -163,9 +253,14 @@ export function GraineVoiceLauncher({ webView: WebView, autoStart = false, activ
|
|
|
163
253
|
return;
|
|
164
254
|
}
|
|
165
255
|
case "graine:ended":
|
|
256
|
+
clearTimer(endWatchdogRef);
|
|
166
257
|
setConnected(false);
|
|
167
258
|
setConnecting(false);
|
|
168
|
-
|
|
259
|
+
connectedRef.current = false;
|
|
260
|
+
if (!endedNotifiedRef.current) {
|
|
261
|
+
endedNotifiedRef.current = true;
|
|
262
|
+
onEnded?.();
|
|
263
|
+
}
|
|
169
264
|
return;
|
|
170
265
|
case "graine:mic-denied":
|
|
171
266
|
client.emit("mic_denied", { reason: String(msg.reason || "NotAllowedError") });
|
|
@@ -210,7 +305,9 @@ export function GraineVoiceLauncher({ webView: WebView, autoStart = false, activ
|
|
|
210
305
|
}
|
|
211
306
|
void beginCall();
|
|
212
307
|
}, [beginCall, ready]),
|
|
213
|
-
end: useCallback(() => post({ type: "graine:end-call" }), [post]),
|
|
308
|
+
end: useCallback(() => { post({ type: "graine:end-call" }); armEndWatchdog(); }, [post, armEndWatchdog]),
|
|
309
|
+
connectivity,
|
|
310
|
+
retry: restartLoad,
|
|
214
311
|
setMuted: useCallback((m) => {
|
|
215
312
|
setMutedState(m);
|
|
216
313
|
post({ type: "graine:set-muted", muted: m });
|
|
@@ -222,6 +319,9 @@ export function GraineVoiceLauncher({ webView: WebView, autoStart = false, activ
|
|
|
222
319
|
const first = activeSeenRef.current === null;
|
|
223
320
|
activeSeenRef.current = active;
|
|
224
321
|
if (active) {
|
|
322
|
+
if (attemptRef.current >= RETRY_DELAYS_MS.length) {
|
|
323
|
+
restartLoad();
|
|
324
|
+
}
|
|
225
325
|
if (autoStart) {
|
|
226
326
|
if (startedRef.current)
|
|
227
327
|
return;
|
|
@@ -242,6 +342,7 @@ export function GraineVoiceLauncher({ webView: WebView, autoStart = false, activ
|
|
|
242
342
|
if (first)
|
|
243
343
|
return;
|
|
244
344
|
post({ type: "graine:end-call" });
|
|
345
|
+
armEndWatchdog();
|
|
245
346
|
startedRef.current = false;
|
|
246
347
|
pendingStartRef.current = false;
|
|
247
348
|
setConnected(false);
|
|
@@ -278,7 +379,6 @@ export function GraineVoiceLauncher({ webView: WebView, autoStart = false, activ
|
|
|
278
379
|
}, [post]);
|
|
279
380
|
if (!voiceAgentId)
|
|
280
381
|
return _jsx(_Fragment, { children: children?.(api) });
|
|
281
|
-
const mountStampRef = useRef(Date.now());
|
|
282
382
|
const effectiveTransport = transport ?? (appearance?.audioTransport === "rtc" ? "rtc" : "ws");
|
|
283
383
|
const src = `${client.opts.baseUrl.replace(/\/$/, "")}/embed/${voiceAgentId}` +
|
|
284
384
|
`?mode=voice&branding=0&embed=1&transport=${effectiveTransport}` +
|
|
@@ -286,5 +386,5 @@ export function GraineVoiceLauncher({ webView: WebView, autoStart = false, activ
|
|
|
286
386
|
(mountedIdleRef.current ? "&idle=1" : "") +
|
|
287
387
|
`&pk=${encodeURIComponent(client.opts.publishableKey)}` +
|
|
288
388
|
(appearance?.accent ? `&accent=${encodeURIComponent(String(appearance.accent).replace("#", ""))}` : "");
|
|
289
|
-
return (_jsxs(_Fragment, { children: [_jsx(View, { style: { position: "absolute", width: 1, height: 1, opacity: 0, bottom: 0, left: 0 }, pointerEvents: "none", children: _jsx(WebView, { ref: ref, source: { uri: src }, originWhitelist: ["*"], javaScriptEnabled: true, domStorageEnabled: true, allowsInlineMediaPlayback: true, mediaPlaybackRequiresUserAction: false, mediaCapturePermissionGrantType: "grant", onMessage: onMessage, style: { width: 1, height: 1, backgroundColor: "transparent" } }) }), children?.(api)] }));
|
|
389
|
+
return (_jsxs(_Fragment, { children: [_jsx(View, { style: { position: "absolute", width: 1, height: 1, opacity: 0, bottom: 0, left: 0 }, pointerEvents: "none", children: _jsx(WebView, { ref: ref, source: { uri: src }, originWhitelist: ["*"], javaScriptEnabled: true, domStorageEnabled: true, allowsInlineMediaPlayback: true, mediaPlaybackRequiresUserAction: false, mediaCapturePermissionGrantType: "grant", onMessage: onMessage, onLoadStart: armReadyTimer, onError: (e) => scheduleRetry(`load-error:${e?.nativeEvent?.code ?? ""}`), onHttpError: (e) => scheduleRetry(`http-${e?.nativeEvent?.statusCode ?? ""}`), onRenderProcessGone: () => scheduleRetry("render-process-gone"), style: { width: 1, height: 1, backgroundColor: "transparent" } }) }), children?.(api)] }));
|
|
290
390
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { useContext } from "react";
|
|
3
|
-
import { Image, Modal, Pressable, ScrollView, Text, View, useColorScheme } from "react-native";
|
|
2
|
+
import { useContext, useState } from "react";
|
|
3
|
+
import { Image, Modal, Platform, Pressable, ScrollView, Text, View, useColorScheme } from "react-native";
|
|
4
4
|
import { Ctx, useGraineWidget } from "./index.js";
|
|
5
5
|
import { resolveNativeAppearance, resolveWidgetPalette } from "./appearance.js";
|
|
6
|
-
import {
|
|
6
|
+
import { PLATFORM_GRAMMAR, resolvePlatform, resolveWidgetLook } from "../protocol.js";
|
|
7
7
|
function tint(hex, alpha) {
|
|
8
8
|
const m = /^#?([0-9a-f]{6})$/i.exec(String(hex).trim());
|
|
9
9
|
if (!m)
|
|
@@ -11,35 +11,74 @@ function tint(hex, alpha) {
|
|
|
11
11
|
const n = parseInt(m[1], 16);
|
|
12
12
|
return `rgba(${(n >> 16) & 255}, ${(n >> 8) & 255}, ${n & 255}, ${alpha})`;
|
|
13
13
|
}
|
|
14
|
-
|
|
14
|
+
const HERO_HEIGHT = { small: 100, medium: 140, large: 180 };
|
|
15
|
+
const THUMB = { small: 56, medium: 72, large: 96 };
|
|
16
|
+
const LIST_THUMB = { small: 36, medium: 44, large: 56 };
|
|
17
|
+
function WidgetRow({ row, variant, look, grammar, theme, accent, radius, onPick, }) {
|
|
15
18
|
const r = Math.max(10, radius - 6);
|
|
16
|
-
|
|
19
|
+
const tight = look.density === "compact";
|
|
20
|
+
const pad = (variant === "list" ? (tight ? 6 : 9) : (tight ? 7 : 10)) + grammar.padBoost;
|
|
21
|
+
const gap = tight ? 6 : 10;
|
|
22
|
+
const rowIsButton = look.button === "row";
|
|
23
|
+
const outline = look.button === "outline";
|
|
24
|
+
const label = row.unavailable ? "Unavailable" : row.selectLabel;
|
|
25
|
+
const a11y = `${row.selectLabel}: ${row.title}`;
|
|
26
|
+
const badge = row.subtitle ? (_jsx(View, { style: { flexDirection: "row" }, children: _jsx(Text, { style: {
|
|
27
|
+
color: accent, fontSize: 10.5, fontWeight: "800", letterSpacing: 0.4,
|
|
28
|
+
textTransform: "uppercase", backgroundColor: tint(accent, 0.15),
|
|
29
|
+
borderRadius: 999, paddingHorizontal: 8, paddingVertical: 3, overflow: "hidden",
|
|
30
|
+
}, children: row.subtitle }) })) : null;
|
|
31
|
+
const titleLine = (big) => (_jsxs(View, { style: { flexDirection: "row", alignItems: "baseline", gap: 8 }, children: [_jsx(Text, { numberOfLines: 2, style: { color: theme.text, fontSize: big ? 16.5 : (variant === "list" ? 13.5 : 14.5), fontWeight: grammar.titleWeight, flex: 1 }, children: row.title }), row.price ? (_jsx(Text, { style: {
|
|
32
|
+
color: theme.text, fontSize: big ? 17 : (variant === "list" ? 13.5 : 14.5), fontWeight: "800",
|
|
33
|
+
fontVariant: ["tabular-nums"],
|
|
34
|
+
}, children: row.price })) : null] }));
|
|
35
|
+
const facts = look.facts && row.facts.length ? (_jsx(View, { style: { flexDirection: "row", flexWrap: "wrap", gap: 6, marginTop: 2 }, children: (variant === "list" ? row.facts.slice(0, 2) : row.facts).map((f, i) => (_jsx(Text, { style: {
|
|
36
|
+
color: theme.muted, fontSize: variant === "list" ? 10.5 : 11.5, fontWeight: "600",
|
|
37
|
+
backgroundColor: grammar.chipOutlined ? "transparent" : tint(theme.border, 0.35),
|
|
38
|
+
borderWidth: grammar.chipOutlined ? 1 : 0, borderColor: theme.border,
|
|
39
|
+
borderRadius: grammar.chipRadius, paddingHorizontal: variant === "list" ? 6 : 8, paddingVertical: variant === "list" ? 2 : 4, overflow: "hidden",
|
|
40
|
+
}, children: f }, `${f}-${i}`))) })) : null;
|
|
41
|
+
const button = (wide) => (_jsx(Pressable, { disabled: row.unavailable, onPress: onPick, accessibilityRole: "button", accessibilityLabel: a11y, hitSlop: wide ? undefined : 8, style: {
|
|
42
|
+
minHeight: 44, justifyContent: "center", alignItems: "center",
|
|
43
|
+
borderRadius: wide ? grammar.buttonRadius : (grammar.pillButtons ? 999 : grammar.buttonRadius), paddingHorizontal: wide ? 12 : 14,
|
|
44
|
+
marginTop: wide ? 8 : 0,
|
|
45
|
+
backgroundColor: row.unavailable || outline ? "transparent" : accent,
|
|
46
|
+
borderWidth: row.unavailable || outline ? 1.5 : 0,
|
|
47
|
+
borderColor: row.unavailable ? theme.border : accent,
|
|
48
|
+
}, children: _jsx(Text, { style: {
|
|
49
|
+
color: row.unavailable ? theme.muted : (outline ? accent : theme.onAccent),
|
|
50
|
+
fontSize: wide ? 14 : 12.5, fontWeight: "700",
|
|
51
|
+
}, children: label }) }));
|
|
52
|
+
const image = (style) => row.image ? (_jsx(Image, { source: { uri: row.image }, accessibilityIgnoresInvertColors: true, resizeMode: "cover", style: { borderRadius: Math.max(8, radius - 8), backgroundColor: theme.border, ...style } })) : null;
|
|
53
|
+
const wrap = (children, style) => rowIsButton ? (_jsx(Pressable, { disabled: row.unavailable, onPress: onPick, accessibilityRole: "button", accessibilityLabel: a11y, style: style, children: children })) : (_jsx(View, { style: style, children: children }));
|
|
54
|
+
if (variant === "hero") {
|
|
55
|
+
return wrap(_jsxs(_Fragment, { children: [image({ width: "100%", height: HERO_HEIGHT[look.imageSize] }), _jsxs(View, { style: { gap: 3 }, children: [badge, titleLine(true), facts, rowIsButton ? null : button(true)] })] }), {
|
|
56
|
+
borderRadius: r, borderWidth: 1, borderColor: theme.border, padding: pad,
|
|
57
|
+
opacity: row.unavailable ? 0.55 : 1, gap, flexDirection: "column",
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
if (variant === "list") {
|
|
61
|
+
const t = LIST_THUMB[look.imageSize];
|
|
62
|
+
return wrap(_jsxs(_Fragment, { children: [image({ width: t, height: t }), _jsxs(View, { style: { flex: 1, minWidth: 0, gap: 2 }, children: [titleLine(false), facts] }), rowIsButton ? (_jsx(Text, { style: { color: theme.muted, fontSize: 22, fontWeight: "600", paddingLeft: 4 }, children: "›" })) : button(false)] }), {
|
|
63
|
+
flexDirection: "row", alignItems: "center", gap,
|
|
64
|
+
paddingVertical: pad, paddingHorizontal: 2, minHeight: 44,
|
|
65
|
+
borderBottomWidth: 1, borderBottomColor: theme.border, marginLeft: grammar.separatorInset,
|
|
66
|
+
opacity: row.unavailable ? 0.55 : 1,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
if (variant === "compact") {
|
|
70
|
+
return wrap(_jsxs(_Fragment, { children: [_jsxs(View, { style: { flex: 1, minWidth: 0, gap: 2 }, children: [badge, titleLine(false), facts] }), rowIsButton ? null : button(false)] }), {
|
|
17
71
|
borderRadius: r, borderWidth: 1, borderColor: theme.border,
|
|
18
|
-
|
|
19
|
-
flexDirection:
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
fontVariant: ["tabular-nums"],
|
|
29
|
-
}, children: row.price })) : null] }), row.facts.length ? (_jsx(View, { style: { flexDirection: "row", flexWrap: "wrap", gap: 6, marginTop: 2 }, children: row.facts.map((f, i) => (_jsx(Text, { style: {
|
|
30
|
-
color: theme.muted, fontSize: 11.5, fontWeight: "600",
|
|
31
|
-
backgroundColor: tint(theme.border, 0.35),
|
|
32
|
-
borderRadius: 8, paddingHorizontal: 8, paddingVertical: 4, overflow: "hidden",
|
|
33
|
-
}, children: f }, `${f}-${i}`))) })) : null, hero ? (_jsx(Pressable, { disabled: row.unavailable, onPress: onPick, accessibilityRole: "button", accessibilityLabel: `${row.selectLabel}: ${row.title}`, style: {
|
|
34
|
-
marginTop: 8, height: 44, borderRadius: 11, alignItems: "center", justifyContent: "center",
|
|
35
|
-
backgroundColor: row.unavailable ? "transparent" : accent,
|
|
36
|
-
borderWidth: row.unavailable ? 1 : 0, borderColor: theme.border,
|
|
37
|
-
}, children: _jsx(Text, { style: { color: row.unavailable ? theme.muted : theme.onAccent, fontSize: 14, fontWeight: "700" }, children: row.unavailable ? "Unavailable" : row.selectLabel }) })) : null] }), hero ? null : (_jsx(Pressable, { disabled: row.unavailable, onPress: onPick, accessibilityRole: "button", accessibilityLabel: `${row.selectLabel}: ${row.title}`, hitSlop: 8, style: {
|
|
38
|
-
minHeight: 44, justifyContent: "center",
|
|
39
|
-
backgroundColor: row.unavailable ? "transparent" : accent,
|
|
40
|
-
borderWidth: row.unavailable ? 1 : 0, borderColor: theme.border,
|
|
41
|
-
borderRadius: 999, paddingHorizontal: 14,
|
|
42
|
-
}, children: _jsx(Text, { style: { color: row.unavailable ? theme.muted : theme.onAccent, fontSize: 12.5, fontWeight: "700" }, children: row.unavailable ? "Unavailable" : row.selectLabel }) }))] }));
|
|
72
|
+
paddingVertical: pad, paddingHorizontal: pad + 2,
|
|
73
|
+
flexDirection: "row", alignItems: "center", gap,
|
|
74
|
+
opacity: row.unavailable ? 0.55 : 1, minHeight: 44,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
const t = THUMB[look.imageSize];
|
|
78
|
+
return wrap(_jsxs(_Fragment, { children: [image({ width: t, height: t }), _jsxs(View, { style: { flex: 1, minWidth: 0, gap: 3 }, children: [badge, titleLine(false), facts] }), rowIsButton ? null : button(false)] }), {
|
|
79
|
+
borderRadius: r, borderWidth: 1, borderColor: theme.border, padding: pad,
|
|
80
|
+
opacity: row.unavailable ? 0.55 : 1, flexDirection: "row", gap, alignItems: "center",
|
|
81
|
+
});
|
|
43
82
|
}
|
|
44
83
|
function AnsweredLine({ answered, theme, accent, radius, onClear }) {
|
|
45
84
|
const d = (answered.data || {});
|
|
@@ -60,9 +99,13 @@ export function GraineWidget(props) {
|
|
|
60
99
|
const { widget, answered, submit, dismiss } = useGraineWidget();
|
|
61
100
|
const appearance = useContext(Ctx)?.appearance ?? null;
|
|
62
101
|
const scheme = useColorScheme();
|
|
102
|
+
const [pagerWidth, setPagerWidth] = useState(0);
|
|
103
|
+
const [page, setPage] = useState(0);
|
|
104
|
+
const look = resolveWidgetLook(widget?.style, widget?.kind);
|
|
105
|
+
const grammar = PLATFORM_GRAMMAR[resolvePlatform(look.platform, Platform.OS, Platform.constants?.Manufacturer ?? Platform.constants?.Brand)];
|
|
63
106
|
const dash = resolveNativeAppearance(appearance, { accent: "#318CE7", heading: "" });
|
|
64
|
-
const accent = props.accent ?? dash.accent;
|
|
65
|
-
const radius = props.radius ?? dash.radius;
|
|
107
|
+
const accent = props.accent ?? look.accent ?? dash.accent;
|
|
108
|
+
const radius = props.radius ?? look.radius ?? (look.platform !== "auto" ? grammar.radius : dash.radius);
|
|
66
109
|
const theme = {
|
|
67
110
|
...resolveWidgetPalette(appearance, { scheme, accent }),
|
|
68
111
|
...(props.theme ?? {}),
|
|
@@ -132,7 +175,7 @@ export function GraineWidget(props) {
|
|
|
132
175
|
const isChips = (widget.kind === "choices" || isQuickActions) && options.length > 0;
|
|
133
176
|
const isCalendar = widget.kind === "calendar_slots" && Array.isArray(data?.days) && data.days.length > 0;
|
|
134
177
|
const drawable = isProducts || isChips || isCalendar;
|
|
135
|
-
const surface = drawable ?
|
|
178
|
+
const surface = drawable ? look.surface : "card";
|
|
136
179
|
const pick = (it) => submit(normalised.toSubmit(it), normalised.toSay(it));
|
|
137
180
|
const body = (_jsx(_Fragment, { children: isCalendar ? (_jsxs(View, { style: { gap: 12 }, children: [data.note ? (_jsx(Text, { style: { color: theme.muted, fontSize: 12.5, lineHeight: 18 }, children: String(data.note) })) : null, data.days.slice(0, 4).map((d, di) => {
|
|
138
181
|
const dayLabel = String(d?.label ?? d?.id ?? `Day ${di + 1}`);
|
|
@@ -144,7 +187,31 @@ export function GraineWidget(props) {
|
|
|
144
187
|
borderWidth: 1, borderColor: theme.border, borderRadius: 11,
|
|
145
188
|
paddingHorizontal: 14,
|
|
146
189
|
}, children: _jsx(Text, { style: { color: theme.text, fontSize: 13.5, fontWeight: "600" }, children: slot }) }, `${slot}-${si}`))) })] }, `${dayLabel}-${di}`));
|
|
147
|
-
})] })) : isProducts ? (
|
|
190
|
+
})] })) : isProducts ? ((() => {
|
|
191
|
+
const draw = (row, i, variant) => (_jsx(WidgetRow, { row: row, variant: variant, look: look, grammar: grammar, theme: theme, accent: accent, radius: radius, onPick: () => pick({ ...normalised.items[i], id: row.id, title: row.title, price: row.price }) }, `${row.id}-${i}`));
|
|
192
|
+
const gap = look.density === "compact" ? 6 : 10;
|
|
193
|
+
if (look.preset === "spotlight") {
|
|
194
|
+
const cardW = pagerWidth ? Math.round(pagerWidth * 0.86) : 300;
|
|
195
|
+
const step = cardW + gap;
|
|
196
|
+
return (_jsxs(View, { onLayout: (e) => setPagerWidth(e?.nativeEvent?.layout?.width ?? 0), children: [_jsx(ScrollView, { horizontal: true, showsHorizontalScrollIndicator: false, snapToInterval: step, decelerationRate: "fast", onMomentumScrollEnd: (e) => setPage(Math.round((e?.nativeEvent?.contentOffset?.x ?? 0) / step)), contentContainerStyle: { gap, paddingRight: 24 }, children: rows.map((row, i) => (_jsx(View, { style: { width: cardW }, children: draw(row, i, "hero") }, `${row.id}-${i}`))) }), rows.length > 1 ? (_jsx(View, { style: { flexDirection: "row", justifyContent: "center", gap: 5, paddingTop: 10 }, accessibilityLabel: `${Math.min(page + 1, rows.length)} of ${rows.length}`, children: rows.map((row, i) => (_jsx(View, { style: {
|
|
197
|
+
height: 5, borderRadius: 999, width: i === page ? 14 : 5,
|
|
198
|
+
backgroundColor: i === page ? accent : theme.border,
|
|
199
|
+
} }, `dot-${row.id}-${i}`))) })) : null] }));
|
|
200
|
+
}
|
|
201
|
+
if (look.columns === 2) {
|
|
202
|
+
return (_jsx(View, { style: { flexDirection: "row", flexWrap: "wrap", gap, justifyContent: "space-between" }, children: rows.map((row, i) => (_jsx(View, { style: { width: "48%" }, children: draw(row, i, "hero") }, `${row.id}-${i}`))) }));
|
|
203
|
+
}
|
|
204
|
+
if (look.preset === "gallery") {
|
|
205
|
+
return _jsx(View, { style: { gap }, children: rows.map((row, i) => draw(row, i, "hero")) });
|
|
206
|
+
}
|
|
207
|
+
if (look.preset === "list") {
|
|
208
|
+
return _jsx(View, { children: rows.map((row, i) => draw(row, i, "list")) });
|
|
209
|
+
}
|
|
210
|
+
if (look.preset === "compact") {
|
|
211
|
+
return _jsx(View, { style: { gap }, children: rows.map((row, i) => draw(row, i, "compact")) });
|
|
212
|
+
}
|
|
213
|
+
return _jsx(View, { style: { gap }, children: rows.map((row, i) => draw(row, i, "row")) });
|
|
214
|
+
})()) : isChips ? (_jsx(View, { style: { flexDirection: "row", flexWrap: "wrap", gap: 8 }, children: options.map((o, i) => {
|
|
148
215
|
const label = String(o?.label ?? o?.title ?? o ?? "");
|
|
149
216
|
const id = String(o?.id ?? label);
|
|
150
217
|
return (_jsx(Pressable, { onPress: () => submit(isQuickActions ? { id, label } : { chosen: id, label }, `I'll go with ${label}.`), accessibilityRole: "button", accessibilityLabel: label, style: {
|
|
@@ -155,7 +222,7 @@ export function GraineWidget(props) {
|
|
|
155
222
|
const title = data?.title ? String(data.title) : "";
|
|
156
223
|
const prompt = data?.prompt ? String(data.prompt) : "";
|
|
157
224
|
if (surface === "screen") {
|
|
158
|
-
return (_jsxs(_Fragment, { children: [chosen, _jsx(Modal, { visible: true, animationType: "slide", onRequestClose: dismiss, transparent:
|
|
225
|
+
return (_jsxs(_Fragment, { children: [chosen, _jsx(Modal, { visible: true, animationType: "slide", onRequestClose: dismiss, transparent: true, presentationStyle: "overFullScreen", statusBarTranslucent: true, children: _jsxs(View, { style: { flex: 1, backgroundColor: theme.surface }, children: [_jsxs(View, { style: {
|
|
159
226
|
paddingTop: 52, paddingHorizontal: 18, paddingBottom: 12,
|
|
160
227
|
flexDirection: "row", alignItems: "center", gap: 12,
|
|
161
228
|
}, children: [_jsx(Pressable, { onPress: dismiss, accessibilityRole: "button", accessibilityLabel: "Close and keep talking", hitSlop: 10, style: {
|
|
@@ -167,11 +234,11 @@ export function GraineWidget(props) {
|
|
|
167
234
|
}, children: [_jsx(Text, { style: { color: theme.muted, fontSize: 10.5, fontWeight: "800", letterSpacing: 0.4, textTransform: "uppercase" }, children: "Still listening" }), _jsx(Text, { numberOfLines: 1, style: { color: theme.text, fontSize: 13.5, marginTop: 2 }, children: prompt || "Tap one, or just say it." })] })] }) })] }));
|
|
168
235
|
}
|
|
169
236
|
if (surface === "sheet") {
|
|
170
|
-
return (_jsxs(_Fragment, { children: [chosen, _jsx(Modal, { visible: true, animationType: "slide", transparent: true, onRequestClose: dismiss, children: _jsxs(View, { style: { flex: 1, backgroundColor: "rgba(0,0,0,0.45)", justifyContent: "flex-end" }, children: [_jsx(Pressable, { onPress: dismiss, accessibilityRole: "button", accessibilityLabel: "Close", style: { flex: 1 } }), _jsxs(View, { style: {
|
|
237
|
+
return (_jsxs(_Fragment, { children: [chosen, _jsx(Modal, { visible: true, animationType: "slide", transparent: true, presentationStyle: "overFullScreen", statusBarTranslucent: true, onRequestClose: dismiss, children: _jsxs(View, { style: { flex: 1, backgroundColor: "rgba(0,0,0,0.45)", justifyContent: "flex-end" }, children: [_jsx(Pressable, { onPress: dismiss, accessibilityRole: "button", accessibilityLabel: "Close", style: { flex: 1 } }), _jsxs(View, { style: {
|
|
171
238
|
backgroundColor: theme.surface,
|
|
172
239
|
borderTopLeftRadius: Math.max(20, radius), borderTopRightRadius: Math.max(20, radius),
|
|
173
240
|
paddingHorizontal: 18, paddingTop: 10, paddingBottom: 28, gap: 14,
|
|
174
|
-
}, children: [_jsx(View, { style: { alignItems: "center" }, children: _jsx(View, { style: { width:
|
|
241
|
+
}, children: [_jsx(View, { style: { alignItems: "center" }, children: _jsx(View, { style: { width: grammar.grabber.width, height: grammar.grabber.height, borderRadius: 999, backgroundColor: theme.border } }) }), title ? (_jsx(Text, { style: { color: theme.text, fontSize: 18, fontWeight: "800" }, children: title })) : null, prompt ? (_jsx(Text, { style: { color: theme.muted, fontSize: 13, lineHeight: 18, marginTop: -8 }, children: prompt })) : null, _jsx(ScrollView, { style: { maxHeight: 420 }, contentContainerStyle: { paddingBottom: 4 }, keyboardShouldPersistTaps: "handled", children: body }), _jsx(Pressable, { onPress: dismiss, accessibilityRole: "button", accessibilityLabel: "None of these", style: {
|
|
175
242
|
alignSelf: "flex-start", minHeight: 44, justifyContent: "center",
|
|
176
243
|
paddingHorizontal: 16, borderRadius: 999, backgroundColor: tint(theme.border, 0.4),
|
|
177
244
|
}, children: _jsx(Text, { style: { color: theme.text, fontSize: 13.5, fontWeight: "700" }, children: "None of these" }) })] })] }) })] }));
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@graineai/inapp-react-native",
|
|
3
|
-
"version": "0.33.
|
|
3
|
+
"version": "0.33.4",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "Graine in-app agent for React Native
|
|
5
|
+
"description": "Graine in-app agent for React Native \u2014 an agent that sees the screen your customer is on and can act on it.",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"private": false,
|
|
8
8
|
"main": "dist/index.js",
|
|
@@ -32,18 +32,20 @@
|
|
|
32
32
|
"scripts": {
|
|
33
33
|
"build": "rm -rf dist && tsc -p tsconfig.build.json",
|
|
34
34
|
"typecheck": "tsc -p tsconfig.build.json --noEmit",
|
|
35
|
-
"test": "npm run build && node --test test/*.test.mjs",
|
|
36
|
-
"prepublishOnly": "npm test && node scripts/verify-package-clean.mjs"
|
|
35
|
+
"test": "npm run build && node --test test/*.test.mjs && npm run test:render",
|
|
36
|
+
"prepublishOnly": "npm test && node scripts/verify-package-clean.mjs",
|
|
37
|
+
"test:render": "node --import ./test/harness/register.mjs --test test/render/*.test.mjs"
|
|
37
38
|
},
|
|
38
39
|
"peerDependencies": {
|
|
40
|
+
"@jambonz/client-sdk-react-native": "^0.1.4",
|
|
39
41
|
"react": ">=17",
|
|
40
|
-
"react-native": ">=0.68"
|
|
41
|
-
"@jambonz/client-sdk-react-native": "^0.1.4"
|
|
42
|
+
"react-native": ">=0.68"
|
|
42
43
|
},
|
|
43
44
|
"devDependencies": {
|
|
44
45
|
"@types/react": "^18.2.0",
|
|
45
46
|
"react": "^18.2.0",
|
|
46
47
|
"react-native": "^0.74.0",
|
|
48
|
+
"react-test-renderer": "^18.2.0",
|
|
47
49
|
"typescript": "^5.4.0"
|
|
48
50
|
},
|
|
49
51
|
"keywords": [
|