@graineai/inapp-react-native 0.20.1 → 0.22.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/client.d.ts CHANGED
@@ -17,6 +17,16 @@ export interface SessionConfig {
17
17
  appearance?: Record<string, any>;
18
18
  variables?: Record<string, string>;
19
19
  components?: any[];
20
+ appActions?: Array<{
21
+ tool_name?: string;
22
+ toolName?: string;
23
+ name?: string;
24
+ }>;
25
+ }
26
+ export interface ActionDiagnosis {
27
+ usable: string[];
28
+ undeclared: string[];
29
+ unhandled: string[];
20
30
  }
21
31
  export declare class GraineInAppClient {
22
32
  readonly opts: GraineInAppOptions;
@@ -46,6 +56,8 @@ export declare class GraineInAppClient {
46
56
  private emit;
47
57
  private get fetch();
48
58
  session(): Promise<SessionConfig>;
59
+ checkActions(): ActionDiagnosis;
60
+ private reportActionMismatch;
49
61
  private ticket;
50
62
  connect(): Promise<void>;
51
63
  private openSocket;
package/dist/client.js CHANGED
@@ -68,8 +68,37 @@ export class GraineInAppClient {
68
68
  throw new Error(data?.error || `Embed rejected (${res.status})`);
69
69
  }
70
70
  this.config = data;
71
+ this.reportActionMismatch();
71
72
  return this.config;
72
73
  }
74
+ checkActions() {
75
+ const registered = [...this.actions.keys()];
76
+ const declared = new Set((this.config?.appActions ?? [])
77
+ .map((a) => a?.tool_name || a?.toolName || a?.name)
78
+ .filter((n) => Boolean(n)));
79
+ return {
80
+ usable: registered.filter((n) => declared.has(n)),
81
+ undeclared: registered.filter((n) => !declared.has(n)),
82
+ unhandled: [...declared].filter((n) => !this.actions.has(n)),
83
+ };
84
+ }
85
+ reportActionMismatch() {
86
+ if (this.actions.size === 0)
87
+ return;
88
+ const { undeclared, unhandled } = this.checkActions();
89
+ if (!undeclared.length && !unhandled.length)
90
+ return;
91
+ this.emit("diagnostic", { type: "actions", ...this.checkActions() });
92
+ if (undeclared.length) {
93
+ console.warn(`[Graine] This app registered ${undeclared.length} action(s) the agent is not ` +
94
+ `configured to call, so the agent cannot use them: ${undeclared.join(", ")}. ` +
95
+ `Add them to the agent's app_actions in Embed & Widgets.`);
96
+ }
97
+ if (unhandled.length) {
98
+ console.warn(`[Graine] The agent is configured to call ${unhandled.length} action(s) this ` +
99
+ `build does not implement: ${unhandled.join(", ")}. It will be refused mid-sentence.`);
100
+ }
101
+ }
73
102
  async ticket() {
74
103
  try {
75
104
  const res = await this.fetch(`${this.opts.baseUrl}/api/embed/ticket`, {
package/dist/index.d.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  export type { ActionHandler, AppActionRequest, AppActionResult, AppEvent, ScreenContext, ScreenField, } from "./protocol.js";
2
- export { GraineInAppClient, type GraineInAppOptions, type SessionConfig } from "./client.js";
2
+ export { GraineInAppClient, type GraineInAppOptions, type SessionConfig, type ActionDiagnosis, } from "./client.js";
3
3
  export { VoiceSession, liveAudioStreamAdapter, base64ToPcm16, CAPTURE_SAMPLE_RATE, type AudioAdapter, type VoiceSessionOptions, } from "./voice.js";
4
4
  export { EchoGuard, decodeAgentAudio, decodeMuLaw, decodePcm16, base64ToBytes, type AudioFormat, type DecodedAudio, } from "./audio.js";
5
5
  export { addMaskRule, maskDeep, maskString } from "./mask.js";
6
6
  export { GraineProvider, useGraineAgent, useGraineScreen, useGraineAction, useGraineVoice, useGraineIdentify, useGraineEvents, useGraineTrack, useGraineTap, useGraineHighlight, useGraineWidget, type GraineEvent, type GraineProviderProps, type Turn, type Caption, } from "./react-native/index.js";
7
+ export { resolveNativeAppearance, type NativeAppearance, type NativeAppearanceFallback, } from "./react-native/appearance.js";
7
8
  export { RtcVoiceSessionController, RtcVoiceError, type RtcVoiceState, type RtcVoiceErrorCode, type RtcVoiceSession, type RtcVoiceOptions, } from "./react-native/voice-rtc.js";
8
9
  export { GraineAgentBar, GraineLauncher, type GraineAgentBarProps, type GraineLauncherProps, type GraineBarTheme, } from "./react-native/ui.js";
9
10
  export { GraineVoiceLauncher, type GraineVoiceLauncherProps, type GraineVoiceApi, } from "./react-native/voice-launcher.js";
package/dist/index.js CHANGED
@@ -1,8 +1,9 @@
1
- export { GraineInAppClient } from "./client.js";
1
+ export { GraineInAppClient, } from "./client.js";
2
2
  export { VoiceSession, liveAudioStreamAdapter, base64ToPcm16, CAPTURE_SAMPLE_RATE, } from "./voice.js";
3
3
  export { EchoGuard, decodeAgentAudio, decodeMuLaw, decodePcm16, base64ToBytes, } from "./audio.js";
4
4
  export { addMaskRule, maskDeep, maskString } from "./mask.js";
5
5
  export { GraineProvider, useGraineAgent, useGraineScreen, useGraineAction, useGraineVoice, useGraineIdentify, useGraineEvents, useGraineTrack, useGraineTap, useGraineHighlight, useGraineWidget, } from "./react-native/index.js";
6
+ export { resolveNativeAppearance, } from "./react-native/appearance.js";
6
7
  export { RtcVoiceSessionController, RtcVoiceError, } from "./react-native/voice-rtc.js";
7
8
  export { GraineAgentBar, GraineLauncher, } from "./react-native/ui.js";
8
9
  export { GraineVoiceLauncher, } from "./react-native/voice-launcher.js";
@@ -0,0 +1,14 @@
1
+ export interface NativeAppearance {
2
+ accent: string;
3
+ heading: string;
4
+ logoUrl: string | null;
5
+ radius: number;
6
+ }
7
+ export interface NativeAppearanceFallback {
8
+ accent: string;
9
+ heading: string;
10
+ radius?: number;
11
+ maxRadius?: number;
12
+ origin?: string;
13
+ }
14
+ export declare function resolveNativeAppearance(appearance: Record<string, any> | null | undefined, fallback: NativeAppearanceFallback): NativeAppearance;
@@ -0,0 +1,29 @@
1
+ const HEX = /^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
2
+ const RADIUS_CEILING = 48;
3
+ const DEFAULT_ORIGIN = "https://www.graine.ai";
4
+ function clamp(value, max) {
5
+ if (value < 0)
6
+ return 0;
7
+ return value > max ? max : value;
8
+ }
9
+ function numeric(value) {
10
+ if (typeof value === "number")
11
+ return Number.isFinite(value) ? value : null;
12
+ if (typeof value === "string" && value.trim() !== "") {
13
+ const n = Number(value);
14
+ return Number.isFinite(n) ? n : null;
15
+ }
16
+ return null;
17
+ }
18
+ export function resolveNativeAppearance(appearance, fallback) {
19
+ const a = appearance || {};
20
+ const max = clamp(numeric(fallback.maxRadius) ?? RADIUS_CEILING, RADIUS_CEILING);
21
+ const rawAccent = String(a.accent ?? "").trim();
22
+ const accent = HEX.test(rawAccent) ? "#" + rawAccent.replace(/^#/, "") : fallback.accent;
23
+ const heading = String(a.title ?? "").trim() || fallback.heading;
24
+ const logoKey = String(a.logo ?? "").trim();
25
+ const origin = String(fallback.origin || DEFAULT_ORIGIN).replace(/\/+$/, "");
26
+ const logoUrl = logoKey ? `${origin}/api/embed/logo?k=${encodeURIComponent(logoKey)}` : null;
27
+ const radius = clamp(numeric(a.radius) ?? numeric(fallback.radius) ?? 18, max);
28
+ return { accent, heading, logoUrl, radius };
29
+ }
@@ -148,6 +148,15 @@ export function GraineProvider({ children, autoConnect = true, onProactive, endO
148
148
  ? { ...prev, text: prev.text + text }
149
149
  : { role: "agent", text, live: true })),
150
150
  ];
151
+ (async () => {
152
+ try {
153
+ const cfg = client.config ?? (await client.session());
154
+ if (cfg?.appearance)
155
+ setAppearance(cfg.appearance);
156
+ }
157
+ catch {
158
+ }
159
+ })();
151
160
  offs.push(client.registerAction("highlight_element", async (args) => {
152
161
  const name = String(args?.name || args?.element || args?.target || "");
153
162
  const known = client.getHighlightables();
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
3
3
  import { ActivityIndicator, Image, KeyboardAvoidingView, Platform, Pressable, ScrollView, StyleSheet, Text, TextInput, useColorScheme, View, } from "react-native";
4
4
  import { useGraineAgent } from "./index.js";
5
+ import { resolveNativeAppearance } from "./appearance.js";
5
6
  const DARK = {
6
7
  surface: "#17161A",
7
8
  border: "rgba(255,255,255,0.09)",
@@ -22,9 +23,13 @@ const LIGHT = {
22
23
  };
23
24
  export function GraineAgentBar({ accent: accentProp, name: nameProp, avatarUrl: avatarProp, bottomInset = 96, hidden = false, captionsOn = false, scheme = "auto", theme: themeProp, }) {
24
25
  const { connected, connecting, error, messages, send, muted, setMuted, caption, agentSpeaking, launcherVisible, launcherInset, appearance, } = useGraineAgent();
25
- const accent = accentProp ?? appearance?.accent ?? "#318CE7";
26
- const name = nameProp ?? appearance?.title ?? "Assistant";
27
- const avatarUrl = avatarProp ?? appearance?.logo ?? undefined;
26
+ const dashboard = resolveNativeAppearance(appearance, {
27
+ accent: "#318CE7",
28
+ heading: "Assistant",
29
+ });
30
+ const accent = accentProp ?? dashboard.accent;
31
+ const name = nameProp ?? dashboard.heading;
32
+ const avatarUrl = avatarProp ?? dashboard.logoUrl ?? undefined;
28
33
  const deviceScheme = useColorScheme();
29
34
  const styles = useMemo(() => {
30
35
  const base = (scheme === "auto" ? deviceScheme === "light" : scheme === "light") ? LIGHT : DARK;
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@graineai/inapp-react-native",
3
- "version": "0.20.1",
3
+ "version": "0.22.0",
4
4
  "type": "module",
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.",
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",
@@ -32,7 +32,8 @@
32
32
  "scripts": {
33
33
  "build": "rm -rf dist && tsc -p tsconfig.build.json",
34
34
  "typecheck": "tsc -p tsconfig.build.json --noEmit",
35
- "prepublishOnly": "npm run build && node scripts/verify-package-clean.mjs"
35
+ "test": "npm run build && node --test test/*.test.mjs",
36
+ "prepublishOnly": "npm test && node scripts/verify-package-clean.mjs"
36
37
  },
37
38
  "peerDependencies": {
38
39
  "react": ">=17",