@graineai/inapp-react-native 0.31.0 → 0.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +5 -2
- package/dist/index.js +3 -1
- package/dist/protocol.d.ts +2 -0
- package/dist/protocol.js +12 -0
- package/dist/react-native/appearance.d.ts +13 -0
- package/dist/react-native/appearance.js +22 -0
- package/dist/react-native/index.d.ts +7 -0
- package/dist/react-native/index.js +9 -3
- package/dist/react-native/widget.d.ts +14 -0
- package/dist/react-native/widget.js +183 -0
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
export type { ActionHandler, AppActionRequest, AppActionResult, AppEvent, ScreenContext, ScreenField, } from "./protocol.js";
|
|
1
|
+
export type { ActionHandler, AppActionRequest, AppActionResult, AppEvent, ScreenContext, ScreenField, WidgetSurface, } from "./protocol.js";
|
|
2
|
+
export { widgetSurface } from "./protocol.js";
|
|
3
|
+
export { GraineWidget } from "./react-native/widget.js";
|
|
4
|
+
export type { GraineWidgetProps, GraineWidgetTheme } from "./react-native/widget.js";
|
|
2
5
|
export { GraineInAppClient, type GraineInAppOptions, type SessionConfig, type ActionDiagnosis, } from "./client.js";
|
|
3
6
|
export { FrictionDetector, resolveFriction, rulesFor, DEFAULT_FRICTION, type FrictionConfig, type FrictionScreenRules, type FrictionNudge, type FrictionSignal, type FrictionKind, } from "./friction.js";
|
|
4
7
|
export { VoiceSession, liveAudioStreamAdapter, base64ToPcm16, CAPTURE_SAMPLE_RATE, type AudioAdapter, type VoiceSessionOptions, } from "./voice.js";
|
|
@@ -8,7 +11,7 @@ export { GraineProvider, useGraineAgent, useGraineScreen, useGraineAction, useGr
|
|
|
8
11
|
export { requestMicrophonePermission } from "./react-native/permissions.js";
|
|
9
12
|
export { GraineFab, GraineTouchCapture, TAP_MAX_MOVE_PX, TAP_MAX_MS, type GraineFabProps, type GraineTouchCaptureProps, } from "./react-native/fab.js";
|
|
10
13
|
export { UNNAMED_TAP, resolveWidgetImages } from "./protocol.js";
|
|
11
|
-
export { resolveNativeAppearance, type NativeAppearance, type NativeAppearanceFallback, } from "./react-native/appearance.js";
|
|
14
|
+
export { resolveNativeAppearance, type NativeAppearance, type NativeAppearanceFallback, resolveWidgetPalette, type WidgetPalette, type WidgetMode, } from "./react-native/appearance.js";
|
|
12
15
|
export { RtcVoiceSessionController, RtcVoiceError, type RtcVoiceState, type RtcVoiceErrorCode, type RtcVoiceSession, type RtcVoiceOptions, } from "./react-native/voice-rtc.js";
|
|
13
16
|
export { GraineAgentBar, GraineLauncher, type GraineAgentBarProps, type GraineLauncherProps, type GraineBarTheme, } from "./react-native/ui.js";
|
|
14
17
|
export { GraineVoiceLauncher, CAPTION_HISTORY, type GraineVoiceLauncherProps, type GraineVoiceApi, } from "./react-native/voice-launcher.js";
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
export { widgetSurface } from "./protocol.js";
|
|
2
|
+
export { GraineWidget } from "./react-native/widget.js";
|
|
1
3
|
export { GraineInAppClient, } from "./client.js";
|
|
2
4
|
export { FrictionDetector, resolveFriction, rulesFor, DEFAULT_FRICTION, } from "./friction.js";
|
|
3
5
|
export { VoiceSession, liveAudioStreamAdapter, base64ToPcm16, CAPTURE_SAMPLE_RATE, } from "./voice.js";
|
|
@@ -7,7 +9,7 @@ export { GraineProvider, useGraineAgent, useGraineScreen, useGraineAction, useGr
|
|
|
7
9
|
export { requestMicrophonePermission } from "./react-native/permissions.js";
|
|
8
10
|
export { GraineFab, GraineTouchCapture, TAP_MAX_MOVE_PX, TAP_MAX_MS, } from "./react-native/fab.js";
|
|
9
11
|
export { UNNAMED_TAP, resolveWidgetImages } from "./protocol.js";
|
|
10
|
-
export { resolveNativeAppearance, } from "./react-native/appearance.js";
|
|
12
|
+
export { resolveNativeAppearance, resolveWidgetPalette, } from "./react-native/appearance.js";
|
|
11
13
|
export { RtcVoiceSessionController, RtcVoiceError, } from "./react-native/voice-rtc.js";
|
|
12
14
|
export { GraineAgentBar, GraineLauncher, } from "./react-native/ui.js";
|
|
13
15
|
export { GraineVoiceLauncher, CAPTION_HISTORY, } from "./react-native/voice-launcher.js";
|
package/dist/protocol.d.ts
CHANGED
|
@@ -62,3 +62,5 @@ export declare const ACTION_DEADLINE_MS = 6000;
|
|
|
62
62
|
export declare const CONTEXT_THROTTLE_MS = 400;
|
|
63
63
|
export declare const UNNAMED_TAP = "tap:screen";
|
|
64
64
|
export declare function resolveWidgetImages(data: Record<string, unknown>, baseUrl: string): Record<string, unknown>;
|
|
65
|
+
export type WidgetSurface = "screen" | "sheet" | "card";
|
|
66
|
+
export declare function widgetSurface(kind: string | null | undefined): WidgetSurface;
|
package/dist/protocol.js
CHANGED
|
@@ -29,3 +29,15 @@ export function resolveWidgetImages(data, baseUrl) {
|
|
|
29
29
|
}
|
|
30
30
|
return out;
|
|
31
31
|
}
|
|
32
|
+
const SURFACE = {
|
|
33
|
+
product_list: "screen",
|
|
34
|
+
products: "screen",
|
|
35
|
+
plan_comparison: "sheet",
|
|
36
|
+
choices: "sheet",
|
|
37
|
+
quick_actions: "sheet",
|
|
38
|
+
calendar_slots: "sheet",
|
|
39
|
+
info_card: "card",
|
|
40
|
+
};
|
|
41
|
+
export function widgetSurface(kind) {
|
|
42
|
+
return SURFACE[String(kind ?? "").trim().toLowerCase()] ?? "card";
|
|
43
|
+
}
|
|
@@ -12,3 +12,16 @@ export interface NativeAppearanceFallback {
|
|
|
12
12
|
origin?: string;
|
|
13
13
|
}
|
|
14
14
|
export declare function resolveNativeAppearance(appearance: Record<string, any> | null | undefined, fallback: NativeAppearanceFallback): NativeAppearance;
|
|
15
|
+
export type WidgetMode = "light" | "dark";
|
|
16
|
+
export interface WidgetPalette {
|
|
17
|
+
surface: string;
|
|
18
|
+
border: string;
|
|
19
|
+
text: string;
|
|
20
|
+
muted: string;
|
|
21
|
+
onAccent: string;
|
|
22
|
+
mode: WidgetMode;
|
|
23
|
+
}
|
|
24
|
+
export declare function resolveWidgetPalette(appearance: Record<string, any> | null | undefined, opts?: {
|
|
25
|
+
scheme?: "light" | "dark" | null;
|
|
26
|
+
accent?: string;
|
|
27
|
+
}): WidgetPalette;
|
|
@@ -27,3 +27,25 @@ export function resolveNativeAppearance(appearance, fallback) {
|
|
|
27
27
|
const radius = clamp(numeric(a.radius) ?? numeric(fallback.radius) ?? 18, max);
|
|
28
28
|
return { accent, heading, logoUrl, radius };
|
|
29
29
|
}
|
|
30
|
+
const LIGHT = { surface: "#FFFFFF", border: "#E5E4E0", text: "#101318", muted: "#5F6672" };
|
|
31
|
+
const DARK = { surface: "#17181B", border: "#2C2E33", text: "#F5F5F4", muted: "#A0A5AE" };
|
|
32
|
+
function luminance(hex) {
|
|
33
|
+
const m = /^#?([0-9a-f]{6})$/i.exec(String(hex).trim());
|
|
34
|
+
if (!m)
|
|
35
|
+
return 0;
|
|
36
|
+
const n = parseInt(m[1], 16);
|
|
37
|
+
const channel = (c) => {
|
|
38
|
+
const s = c / 255;
|
|
39
|
+
return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
|
|
40
|
+
};
|
|
41
|
+
return 0.2126 * channel((n >> 16) & 255) + 0.7152 * channel((n >> 8) & 255) + 0.0722 * channel(n & 255);
|
|
42
|
+
}
|
|
43
|
+
export function resolveWidgetPalette(appearance, opts) {
|
|
44
|
+
const want = String((appearance || {}).theme ?? "auto").trim().toLowerCase();
|
|
45
|
+
const mode = want === "dark" ? "dark"
|
|
46
|
+
: want === "light" ? "light"
|
|
47
|
+
: opts?.scheme === "dark" ? "dark" : "light";
|
|
48
|
+
const set = mode === "dark" ? DARK : LIGHT;
|
|
49
|
+
const onAccent = luminance(opts?.accent ?? "") > 0.45 ? "#101318" : "#FFFFFF";
|
|
50
|
+
return { ...set, onAccent, mode };
|
|
51
|
+
}
|
|
@@ -4,6 +4,8 @@ import { type AudioAdapter, type VoiceSessionOptions } from "../voice.js";
|
|
|
4
4
|
import type { ActionHandler, ScreenContext, ScreenField } from "../protocol.js";
|
|
5
5
|
export { GraineFab, GraineTouchCapture } from "./fab.js";
|
|
6
6
|
export type { GraineFabProps, GraineTouchCaptureProps } from "./fab.js";
|
|
7
|
+
export { widgetSurface } from "../protocol.js";
|
|
8
|
+
export type { WidgetSurface } from "../protocol.js";
|
|
7
9
|
import type { FrictionConfig } from "../friction.js";
|
|
8
10
|
import { type LauncherInset, type LauncherVisibility } from "./navigation.js";
|
|
9
11
|
interface GraineContextValue {
|
|
@@ -123,6 +125,11 @@ export declare function useGraineHighlight(name: string): {
|
|
|
123
125
|
};
|
|
124
126
|
export declare function useGraineWidget(): {
|
|
125
127
|
widget: GraineWidget | null;
|
|
128
|
+
answered: {
|
|
129
|
+
widget: GraineWidget;
|
|
130
|
+
data: Record<string, unknown>;
|
|
131
|
+
summary: string;
|
|
132
|
+
} | null;
|
|
126
133
|
submit: (data: Record<string, unknown>, summary: string) => void;
|
|
127
134
|
dismiss: () => void;
|
|
128
135
|
};
|
|
@@ -6,6 +6,7 @@ import { VoiceSession } from "../voice.js";
|
|
|
6
6
|
import { resolveWidgetImages } from "../protocol.js";
|
|
7
7
|
import { GraineFab, GraineTouchCapture } from "./fab.js";
|
|
8
8
|
export { GraineFab, GraineTouchCapture } from "./fab.js";
|
|
9
|
+
export { widgetSurface } from "../protocol.js";
|
|
9
10
|
import { LauncherVisibilityTracker, activeRouteName, } from "./navigation.js";
|
|
10
11
|
export const Ctx = createContext(null);
|
|
11
12
|
let defaultClient = null;
|
|
@@ -398,14 +399,17 @@ export function useGraineWidget() {
|
|
|
398
399
|
}
|
|
399
400
|
: null, [client]);
|
|
400
401
|
const [widget, setWidget] = useState(() => adopt(client.pendingWidget));
|
|
402
|
+
const [answered, setAnswered] = useState(null);
|
|
401
403
|
useEffect(() => {
|
|
402
404
|
const pending = adopt(client.pendingWidget);
|
|
403
405
|
if (pending)
|
|
404
406
|
setWidget((cur) => (cur && cur.id === pending.id ? cur : pending));
|
|
405
407
|
return client.on("widget", (w) => {
|
|
406
408
|
const next = adopt(w);
|
|
407
|
-
if (next)
|
|
408
|
-
|
|
409
|
+
if (!next)
|
|
410
|
+
return;
|
|
411
|
+
setWidget(next);
|
|
412
|
+
setAnswered(null);
|
|
409
413
|
});
|
|
410
414
|
}, [client, adopt]);
|
|
411
415
|
const submit = useCallback((data, summary) => {
|
|
@@ -414,12 +418,14 @@ export function useGraineWidget() {
|
|
|
414
418
|
client.submitWidget(widget.id, data, summary);
|
|
415
419
|
client.clearPendingWidget();
|
|
416
420
|
setWidget(null);
|
|
421
|
+
setAnswered({ widget, data, summary });
|
|
417
422
|
}, [client, widget]);
|
|
418
423
|
const dismiss = useCallback(() => {
|
|
419
424
|
client.clearPendingWidget();
|
|
420
425
|
setWidget(null);
|
|
426
|
+
setAnswered(null);
|
|
421
427
|
}, [client]);
|
|
422
|
-
return { widget, submit, dismiss };
|
|
428
|
+
return { widget, answered, submit, dismiss };
|
|
423
429
|
}
|
|
424
430
|
export function useGraineAction(name, handler) {
|
|
425
431
|
const client = useGraineClient();
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
export interface GraineWidgetTheme {
|
|
3
|
+
surface: string;
|
|
4
|
+
border: string;
|
|
5
|
+
text: string;
|
|
6
|
+
muted: string;
|
|
7
|
+
onAccent: string;
|
|
8
|
+
}
|
|
9
|
+
export interface GraineWidgetProps {
|
|
10
|
+
theme?: Partial<GraineWidgetTheme>;
|
|
11
|
+
accent?: string;
|
|
12
|
+
radius?: number;
|
|
13
|
+
}
|
|
14
|
+
export declare function GraineWidget(props: GraineWidgetProps): React.JSX.Element | null;
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { Image, Modal, Pressable, ScrollView, Text, View, useColorScheme } from "react-native";
|
|
3
|
+
import { useGraineAgent, useGraineWidget } from "./index.js";
|
|
4
|
+
import { resolveNativeAppearance, resolveWidgetPalette } from "./appearance.js";
|
|
5
|
+
import { widgetSurface } from "../protocol.js";
|
|
6
|
+
function tint(hex, alpha) {
|
|
7
|
+
const m = /^#?([0-9a-f]{6})$/i.exec(String(hex).trim());
|
|
8
|
+
if (!m)
|
|
9
|
+
return `rgba(120,140,170,${alpha})`;
|
|
10
|
+
const n = parseInt(m[1], 16);
|
|
11
|
+
return `rgba(${(n >> 16) & 255}, ${(n >> 8) & 255}, ${n & 255}, ${alpha})`;
|
|
12
|
+
}
|
|
13
|
+
function WidgetRow({ row, hero, theme, accent, radius, onPick, }) {
|
|
14
|
+
const r = Math.max(10, radius - 6);
|
|
15
|
+
return (_jsxs(View, { style: {
|
|
16
|
+
borderRadius: r, borderWidth: 1, borderColor: theme.border,
|
|
17
|
+
padding: 10, opacity: row.unavailable ? 0.55 : 1,
|
|
18
|
+
flexDirection: hero ? "column" : "row", gap: 10, alignItems: hero ? "stretch" : "center",
|
|
19
|
+
}, children: [row.image ? (_jsx(Image, { source: { uri: row.image }, accessibilityIgnoresInvertColors: true, style: hero
|
|
20
|
+
? { width: "100%", height: 140, borderRadius: Math.max(8, radius - 8), backgroundColor: theme.border }
|
|
21
|
+
: { width: 72, height: 72, borderRadius: Math.max(8, radius - 8), backgroundColor: theme.border }, resizeMode: "cover" })) : null, _jsxs(View, { style: { flex: hero ? undefined : 1, minWidth: 0, gap: 3 }, children: [row.subtitle ? (_jsx(View, { style: { flexDirection: "row" }, children: _jsx(Text, { style: {
|
|
22
|
+
color: accent, fontSize: 10.5, fontWeight: "800", letterSpacing: 0.4,
|
|
23
|
+
textTransform: "uppercase", backgroundColor: tint(accent, 0.15),
|
|
24
|
+
borderRadius: 999, paddingHorizontal: 8, paddingVertical: 3, overflow: "hidden",
|
|
25
|
+
}, children: row.subtitle }) })) : null, _jsxs(View, { style: { flexDirection: "row", alignItems: "baseline", gap: 8 }, children: [_jsx(Text, { numberOfLines: 2, style: { color: theme.text, fontSize: hero ? 16.5 : 14.5, fontWeight: "700", flex: 1 }, children: row.title }), row.price ? (_jsx(Text, { style: {
|
|
26
|
+
color: theme.text, fontSize: hero ? 17 : 14.5, fontWeight: "800",
|
|
27
|
+
fontVariant: ["tabular-nums"],
|
|
28
|
+
}, 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: {
|
|
29
|
+
color: theme.muted, fontSize: 11.5, fontWeight: "600",
|
|
30
|
+
backgroundColor: tint(theme.border, 0.35),
|
|
31
|
+
borderRadius: 8, paddingHorizontal: 8, paddingVertical: 4, overflow: "hidden",
|
|
32
|
+
}, children: f }, `${f}-${i}`))) })) : null, hero ? (_jsx(Pressable, { disabled: row.unavailable, onPress: onPick, accessibilityRole: "button", accessibilityLabel: `${row.selectLabel}: ${row.title}`, style: {
|
|
33
|
+
marginTop: 8, height: 44, borderRadius: 11, alignItems: "center", justifyContent: "center",
|
|
34
|
+
backgroundColor: row.unavailable ? "transparent" : accent,
|
|
35
|
+
borderWidth: row.unavailable ? 1 : 0, borderColor: theme.border,
|
|
36
|
+
}, 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: {
|
|
37
|
+
minHeight: 44, justifyContent: "center",
|
|
38
|
+
backgroundColor: row.unavailable ? "transparent" : accent,
|
|
39
|
+
borderWidth: row.unavailable ? 1 : 0, borderColor: theme.border,
|
|
40
|
+
borderRadius: 999, paddingHorizontal: 14,
|
|
41
|
+
}, children: _jsx(Text, { style: { color: row.unavailable ? theme.muted : theme.onAccent, fontSize: 12.5, fontWeight: "700" }, children: row.unavailable ? "Unavailable" : row.selectLabel }) }))] }));
|
|
42
|
+
}
|
|
43
|
+
function AnsweredLine({ answered, theme, accent, radius, onClear }) {
|
|
44
|
+
const d = (answered.data || {});
|
|
45
|
+
const title = String(d.name || d.title || d.label || d.slot || "").trim();
|
|
46
|
+
const detail = [String(d.price || ""), String(d.day || "")].filter(Boolean).join(" · ");
|
|
47
|
+
if (!title && !detail)
|
|
48
|
+
return null;
|
|
49
|
+
return (_jsxs(View, { style: {
|
|
50
|
+
backgroundColor: theme.surface, borderRadius: radius, borderWidth: 1, borderColor: theme.border,
|
|
51
|
+
paddingHorizontal: 12, paddingVertical: 10, marginBottom: 8,
|
|
52
|
+
flexDirection: "row", alignItems: "center", gap: 10,
|
|
53
|
+
}, children: [_jsx(View, { style: {
|
|
54
|
+
width: 26, height: 26, borderRadius: 999, backgroundColor: tint(accent, 0.18),
|
|
55
|
+
alignItems: "center", justifyContent: "center",
|
|
56
|
+
}, children: _jsx(Text, { style: { color: accent, fontSize: 14, fontWeight: "800" }, children: "✓" }) }), _jsxs(View, { style: { flex: 1, minWidth: 0 }, children: [_jsx(Text, { numberOfLines: 1, style: { color: theme.text, fontSize: 13.5, fontWeight: "700" }, children: title || detail }), title && detail ? (_jsx(Text, { numberOfLines: 1, style: { color: theme.muted, fontSize: 11.5, fontWeight: "600" }, children: detail })) : null] }), _jsx(Pressable, { onPress: onClear, accessibilityRole: "button", accessibilityLabel: "Clear what I chose", hitSlop: 10, style: { minHeight: 44, justifyContent: "center", paddingHorizontal: 4 }, children: _jsx(Text, { style: { color: theme.muted, fontSize: 12, fontWeight: "700" }, children: "Clear" }) })] }));
|
|
57
|
+
}
|
|
58
|
+
export function GraineWidget(props) {
|
|
59
|
+
const { widget, answered, submit, dismiss } = useGraineWidget();
|
|
60
|
+
const { appearance } = useGraineAgent();
|
|
61
|
+
const scheme = useColorScheme();
|
|
62
|
+
const dash = resolveNativeAppearance(appearance, { accent: "#318CE7", heading: "" });
|
|
63
|
+
const accent = props.accent ?? dash.accent;
|
|
64
|
+
const radius = props.radius ?? dash.radius;
|
|
65
|
+
const theme = {
|
|
66
|
+
...resolveWidgetPalette(appearance, { scheme, accent }),
|
|
67
|
+
...(props.theme ?? {}),
|
|
68
|
+
};
|
|
69
|
+
const chosen = answered ? (_jsx(AnsweredLine, { answered: answered, theme: theme, accent: accent, radius: radius, onClear: dismiss })) : null;
|
|
70
|
+
if (!widget)
|
|
71
|
+
return chosen;
|
|
72
|
+
const data = (widget.data ?? {});
|
|
73
|
+
const options = Array.isArray(data?.options)
|
|
74
|
+
? data.options
|
|
75
|
+
: widget.kind === "quick_actions" && Array.isArray(data?.actions)
|
|
76
|
+
? data.actions
|
|
77
|
+
: [];
|
|
78
|
+
const isQuickActions = widget.kind === "quick_actions" && options.length > 0;
|
|
79
|
+
const normalised = (() => {
|
|
80
|
+
if (widget.kind === "plan_comparison" && Array.isArray(data?.plans)) {
|
|
81
|
+
return {
|
|
82
|
+
items: data.plans.map((pl) => ({
|
|
83
|
+
id: pl?.id, title: pl?.name, subtitle: pl?.badge,
|
|
84
|
+
price: pl?.price ? `${pl.price}${pl.priceSuffix ?? ""}` : "",
|
|
85
|
+
facts: Array.isArray(pl?.facts)
|
|
86
|
+
? pl.facts.map((f) => (f?.label ? `${f.label} ${f.value}` : String(f?.value ?? "")))
|
|
87
|
+
: [],
|
|
88
|
+
selectLabel: pl?.selectLabel,
|
|
89
|
+
})),
|
|
90
|
+
selectLabel: "Select",
|
|
91
|
+
toSubmit: (it) => ({ id: it.id, name: it.title, price: it.price }),
|
|
92
|
+
toSay: (it) => `I'll take ${it.title}${it.price ? ` at ${it.price}` : ""}.`,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
if (widget.kind === "info_card" && (data?.title || data?.summary)) {
|
|
96
|
+
return {
|
|
97
|
+
items: [{
|
|
98
|
+
id: "card", title: data.title, subtitle: data.category, imageUrl: data.imageUrl,
|
|
99
|
+
facts: [
|
|
100
|
+
data.summary,
|
|
101
|
+
...(Array.isArray(data?.facts)
|
|
102
|
+
? data.facts.map((f) => (f?.label ? `${f.label} ${f.value}` : String(f?.value ?? "")))
|
|
103
|
+
: []),
|
|
104
|
+
].filter(Boolean),
|
|
105
|
+
}],
|
|
106
|
+
selectLabel: String(data?.actionLabel || "Tell me more"),
|
|
107
|
+
toSubmit: () => ({ action: String(data?.actionLabel || "Tell me more") }),
|
|
108
|
+
toSay: () => String(data?.actionLabel || "Tell me more about this."),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
const items = Array.isArray(data?.items) ? data.items : [];
|
|
112
|
+
return {
|
|
113
|
+
items,
|
|
114
|
+
selectLabel: String(data?.selectLabel || "Choose"),
|
|
115
|
+
toSubmit: (it) => ({ id: it.id, title: it.title, price: it.price }),
|
|
116
|
+
toSay: (it) => `I'll take a look at ${it.title}${it.price ? ` at ${it.price}` : ""}.`,
|
|
117
|
+
};
|
|
118
|
+
})();
|
|
119
|
+
const rows = normalised.items.slice(0, 8).map((it, i) => ({
|
|
120
|
+
id: String(it?.id ?? it?.title ?? i),
|
|
121
|
+
title: String(it?.title ?? it?.id ?? i),
|
|
122
|
+
subtitle: it?.subtitle ? String(it.subtitle) : "",
|
|
123
|
+
price: it?.price ? String(it.price) : "",
|
|
124
|
+
facts: Array.isArray(it?.facts) ? it.facts.map(String).slice(0, 3) : [],
|
|
125
|
+
image: it?.imageUrl ? String(it.imageUrl) : "",
|
|
126
|
+
unavailable: it?.unavailable === true,
|
|
127
|
+
selectLabel: String(it?.selectLabel || normalised.selectLabel),
|
|
128
|
+
}));
|
|
129
|
+
const isProducts = rows.length > 0 && (widget.kind === "product_list" || widget.kind === "products" ||
|
|
130
|
+
widget.kind === "plan_comparison" || widget.kind === "info_card" || !options.length);
|
|
131
|
+
const isChips = (widget.kind === "choices" || isQuickActions) && options.length > 0;
|
|
132
|
+
const isCalendar = widget.kind === "calendar_slots" && Array.isArray(data?.days) && data.days.length > 0;
|
|
133
|
+
const drawable = isProducts || isChips || isCalendar;
|
|
134
|
+
const surface = drawable ? widgetSurface(widget.kind) : "card";
|
|
135
|
+
const pick = (it) => submit(normalised.toSubmit(it), normalised.toSay(it));
|
|
136
|
+
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) => {
|
|
137
|
+
const dayLabel = String(d?.label ?? d?.id ?? `Day ${di + 1}`);
|
|
138
|
+
const slots = Array.isArray(d?.slots) ? d.slots.map(String) : [];
|
|
139
|
+
if (!slots.length)
|
|
140
|
+
return null;
|
|
141
|
+
return (_jsxs(View, { style: { gap: 7 }, children: [_jsx(Text, { style: { color: theme.text, fontSize: 13, fontWeight: "700" }, children: dayLabel }), _jsx(View, { style: { flexDirection: "row", flexWrap: "wrap", gap: 8 }, children: slots.slice(0, 8).map((slot, si) => (_jsx(Pressable, { onPress: () => submit({ day: dayLabel, slot }, `${dayLabel} at ${slot}, please.`), accessibilityRole: "button", accessibilityLabel: `${dayLabel} at ${slot}`, style: {
|
|
142
|
+
minHeight: 44, justifyContent: "center",
|
|
143
|
+
borderWidth: 1, borderColor: theme.border, borderRadius: 11,
|
|
144
|
+
paddingHorizontal: 14,
|
|
145
|
+
}, children: _jsx(Text, { style: { color: theme.text, fontSize: 13.5, fontWeight: "600" }, children: slot }) }, `${slot}-${si}`))) })] }, `${dayLabel}-${di}`));
|
|
146
|
+
})] })) : isProducts ? (_jsx(View, { style: { gap: 10 }, children: rows.map((row, i) => (_jsx(WidgetRow, { row: row, hero: surface === "screen" && i === 0 && !!row.image, theme: theme, accent: accent, radius: radius, onPick: () => pick({ ...normalised.items[i], id: row.id, title: row.title, price: row.price }) }, `${row.id}-${i}`))) })) : isChips ? (_jsx(View, { style: { flexDirection: "row", flexWrap: "wrap", gap: 8 }, children: options.map((o, i) => {
|
|
147
|
+
const label = String(o?.label ?? o?.title ?? o ?? "");
|
|
148
|
+
const id = String(o?.id ?? label);
|
|
149
|
+
return (_jsx(Pressable, { onPress: () => submit(isQuickActions ? { id, label } : { chosen: id, label }, `I'll go with ${label}.`), accessibilityRole: "button", accessibilityLabel: label, style: {
|
|
150
|
+
backgroundColor: accent, borderRadius: 999,
|
|
151
|
+
minHeight: 44, justifyContent: "center", paddingHorizontal: 16,
|
|
152
|
+
}, children: _jsx(Text, { style: { color: theme.onAccent, fontSize: 13.5, fontWeight: "700" }, children: label }) }, `${id}-${i}`));
|
|
153
|
+
}) })) : (_jsx(Text, { style: { color: theme.text, fontSize: 14, lineHeight: 20 }, children: widget.fallbackText || "The assistant has something to show you." })) }));
|
|
154
|
+
const title = data?.title ? String(data.title) : "";
|
|
155
|
+
const prompt = data?.prompt ? String(data.prompt) : "";
|
|
156
|
+
if (surface === "screen") {
|
|
157
|
+
return (_jsxs(_Fragment, { children: [chosen, _jsx(Modal, { visible: true, animationType: "slide", onRequestClose: dismiss, transparent: false, children: _jsxs(View, { style: { flex: 1, backgroundColor: theme.surface }, children: [_jsxs(View, { style: {
|
|
158
|
+
paddingTop: 52, paddingHorizontal: 18, paddingBottom: 12,
|
|
159
|
+
flexDirection: "row", alignItems: "center", gap: 12,
|
|
160
|
+
}, children: [_jsx(Pressable, { onPress: dismiss, accessibilityRole: "button", accessibilityLabel: "Close and keep talking", hitSlop: 10, style: {
|
|
161
|
+
width: 44, height: 44, borderRadius: 999, alignItems: "center", justifyContent: "center",
|
|
162
|
+
backgroundColor: tint(theme.border, 0.4),
|
|
163
|
+
}, children: _jsx(Text, { style: { color: theme.text, fontSize: 20, fontWeight: "600" }, children: "×" }) }), _jsxs(View, { style: { flex: 1, minWidth: 0 }, children: [title ? (_jsx(Text, { numberOfLines: 1, style: { color: theme.text, fontSize: 18, fontWeight: "800" }, children: title })) : null, prompt ? (_jsx(Text, { numberOfLines: 1, style: { color: theme.muted, fontSize: 12.5, fontWeight: "600", marginTop: 1 }, children: prompt })) : null] })] }), _jsx(ScrollView, { style: { flex: 1 }, contentContainerStyle: { paddingHorizontal: 18, paddingBottom: 24 }, keyboardShouldPersistTaps: "handled", children: body }), _jsxs(View, { style: {
|
|
164
|
+
paddingHorizontal: 18, paddingTop: 12, paddingBottom: 28,
|
|
165
|
+
borderTopWidth: 1, borderTopColor: theme.border,
|
|
166
|
+
}, 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." })] })] }) })] }));
|
|
167
|
+
}
|
|
168
|
+
if (surface === "sheet") {
|
|
169
|
+
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: {
|
|
170
|
+
backgroundColor: theme.surface,
|
|
171
|
+
borderTopLeftRadius: Math.max(20, radius), borderTopRightRadius: Math.max(20, radius),
|
|
172
|
+
paddingHorizontal: 18, paddingTop: 10, paddingBottom: 28, gap: 14,
|
|
173
|
+
}, children: [_jsx(View, { style: { alignItems: "center" }, children: _jsx(View, { style: { width: 38, height: 4, 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: {
|
|
174
|
+
alignSelf: "flex-start", minHeight: 44, justifyContent: "center",
|
|
175
|
+
paddingHorizontal: 16, borderRadius: 999, backgroundColor: tint(theme.border, 0.4),
|
|
176
|
+
}, children: _jsx(Text, { style: { color: theme.text, fontSize: 13.5, fontWeight: "700" }, children: "None of these" }) })] })] }) })] }));
|
|
177
|
+
}
|
|
178
|
+
return (_jsxs(_Fragment, { children: [chosen, _jsxs(View, { style: {
|
|
179
|
+
backgroundColor: theme.surface, borderRadius: radius, borderWidth: 1, borderColor: theme.border,
|
|
180
|
+
paddingHorizontal: 14, paddingVertical: 12, marginBottom: 8,
|
|
181
|
+
shadowColor: "#000", shadowOpacity: 0.18, shadowRadius: 12, shadowOffset: { width: 0, height: 4 }, elevation: 6,
|
|
182
|
+
}, children: [title ? (_jsx(Text, { style: { color: theme.text, fontSize: 14.5, fontWeight: "800", marginBottom: 2 }, children: title })) : null, prompt ? (_jsx(Text, { style: { color: theme.muted, fontSize: 12.5, lineHeight: 17, marginBottom: 8 }, children: prompt })) : _jsx(View, { style: { height: 6 } }), body, _jsx(Pressable, { onPress: dismiss, accessibilityRole: "button", accessibilityLabel: "Dismiss", hitSlop: 8, style: { alignSelf: "flex-end", marginTop: 10, minHeight: 44, justifyContent: "center" }, children: _jsx(Text, { style: { color: theme.muted, fontSize: 12.5, fontWeight: "600" }, children: "Dismiss" }) })] })] }));
|
|
183
|
+
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@graineai/inapp-react-native",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.33.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "Graine in-app agent for React Native
|
|
5
|
+
"description": "Graine in-app agent for React Native — 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",
|