@adminide-stack/yantra-mobile 12.0.123 → 12.0.124-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/api/stt.js CHANGED
@@ -1,28 +1,34 @@
1
- const BRAIN_STT_URL = "https://cdecli-agent.cdebase.dev/v1/yantra-brain/audio/transcriptions";
1
+ var __defProp = Object.defineProperty;
2
+ var __defProps = Object.defineProperties;
3
+ var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
4
+ var __getOwnPropSymbols = Object.getOwnPropertySymbols;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __propIsEnum = Object.prototype.propertyIsEnumerable;
7
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
8
+ var __spreadValues = (a, b) => {
9
+ for (var prop in b || (b = {}))
10
+ if (__hasOwnProp.call(b, prop))
11
+ __defNormalProp(a, prop, b[prop]);
12
+ if (__getOwnPropSymbols)
13
+ for (var prop of __getOwnPropSymbols(b)) {
14
+ if (__propIsEnum.call(b, prop))
15
+ __defNormalProp(a, prop, b[prop]);
16
+ }
17
+ return a;
18
+ };
19
+ var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
2
20
  const WHISPER_MODEL = "whisper-large-v3-turbo";
3
- async function transcribeAudio(params) {
4
- const {
5
- uri,
6
- mimeType = "audio/m4a",
7
- filename = "audio.m4a",
8
- prompt
9
- } = params;
10
- if (!uri) {
11
- throw new Error("Audio file uri is empty or not provided");
21
+ const STT_SIGN_IN_REQUIRED = "Voice input needs you signed in to Yantra. Sign in, then try again.";
22
+ class SttRefusedError extends Error {
23
+ constructor(message) {
24
+ super(message);
25
+ this.name = "SttRefusedError";
12
26
  }
13
- const formData = new FormData();
14
- formData.append("file", {
15
- uri,
16
- name: filename,
17
- type: mimeType
18
- });
19
- formData.append("model", WHISPER_MODEL);
20
- if (prompt) {
21
- formData.append("prompt", prompt);
22
- }
23
- return requestTranscription(BRAIN_STT_URL, formData);
24
27
  }
25
- async function requestTranscription(url, formData) {
28
+ function brainSttUrl(endpoint) {
29
+ return `${endpoint.trim().replace(/\/+$/, "")}/v1/yantra-brain/audio/transcriptions`;
30
+ }
31
+ async function requestTranscription(url, formData, token) {
26
32
  var _a, _b, _c;
27
33
  const response = await fetch(url, {
28
34
  method: "POST",
@@ -31,6 +37,9 @@ async function requestTranscription(url, formData) {
31
37
  * it from FormData ensures the multipart boundary string is set
32
38
  * correctly. Overriding here would break the upload silently.
33
39
  */
40
+ headers: {
41
+ Authorization: `Bearer ${token}`
42
+ },
34
43
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
35
44
  body: formData
36
45
  });
@@ -39,12 +48,54 @@ async function requestTranscription(url, formData) {
39
48
  let errorMessage;
40
49
  try {
41
50
  const errorJson = JSON.parse(errorText);
42
- errorMessage = ((_a = errorJson == null ? void 0 : errorJson.error) == null ? void 0 : _a.message) || (errorJson == null ? void 0 : errorJson.message) || errorText;
51
+ errorMessage = (typeof (errorJson == null ? void 0 : errorJson.error) === "string" ? errorJson.error : (_a = errorJson == null ? void 0 : errorJson.error) == null ? void 0 : _a.message) || (errorJson == null ? void 0 : errorJson.message) || errorText;
43
52
  } catch (e) {
44
53
  errorMessage = errorText || response.statusText;
45
54
  }
46
- throw new Error(`STT API error (${response.status}): ${errorMessage}`);
55
+ const message = `STT API error (${response.status}): ${errorMessage}`;
56
+ throw response.status === 401 ? new SttRefusedError(message) : new Error(message);
47
57
  }
48
58
  const data = await response.json();
49
59
  return (_c = (_b = data == null ? void 0 : data.text) == null ? void 0 : _b.trim()) != null ? _c : "";
50
- }export{transcribeAudio};//# sourceMappingURL=stt.js.map
60
+ }
61
+ async function transcribeAudio(params) {
62
+ var _a;
63
+ const {
64
+ uri,
65
+ mimeType = "audio/m4a",
66
+ filename = "audio.m4a",
67
+ prompt
68
+ } = params;
69
+ if (!uri) {
70
+ throw new Error("Audio file uri is empty or not provided");
71
+ }
72
+ const token = (_a = params.token) == null ? void 0 : _a.trim();
73
+ if (!token) {
74
+ throw new Error(STT_SIGN_IN_REQUIRED);
75
+ }
76
+ const formData = new FormData();
77
+ formData.append("file", {
78
+ uri,
79
+ name: filename,
80
+ type: mimeType
81
+ });
82
+ formData.append("model", WHISPER_MODEL);
83
+ if (prompt) {
84
+ formData.append("prompt", prompt);
85
+ }
86
+ return requestTranscription(brainSttUrl(params.endpoint), formData, token);
87
+ }
88
+ async function transcribeAsCaller(params, caller) {
89
+ const send = async () => transcribeAudio(__spreadProps(__spreadValues({}, params), {
90
+ endpoint: caller.endpoint,
91
+ token: await caller.obtainToken().catch(() => null)
92
+ }));
93
+ try {
94
+ return await send();
95
+ } catch (err) {
96
+ if (!(err instanceof SttRefusedError) || !caller.recover) throw err;
97
+ const recovered = await caller.recover().catch(() => false);
98
+ if (!recovered) throw err;
99
+ return send();
100
+ }
101
+ }export{STT_SIGN_IN_REQUIRED,SttRefusedError,brainSttUrl,transcribeAsCaller,transcribeAudio};//# sourceMappingURL=stt.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"stt.js","sources":["../../src/api/stt.ts"],"sourcesContent":["/**\n * Speech-to-Text (STT) API using Groq's Whisper model — mobile port.\n *\n * Mirrors `packages-modules/account/browser/src/api/stt.ts` (which uploads\n * a `Blob` produced by the browser's `MediaRecorder`). On React Native we\n * upload a file by URI instead, because:\n * • `expo-audio` writes the recording to a local `file://` URI on disk,\n * not an in-memory Blob.\n * • RN's `fetch` + `FormData` already supports `{uri, name, type}` parts —\n * it streams the file directly without us reading bytes into JS, which\n * keeps a 3-minute clip from spiking memory.\n *\n * Mobile calls ONLY the yantra-brain resource endpoint: the gateway holds\n * the Groq key server-side. A shipped app bundle must never embed a provider\n * key (it cannot be rotated out of installed apps), so unlike web there is\n * NO direct-Groq BYO-key fallback here.\n */\n\nconst BRAIN_STT_URL = 'https://cdecli-agent.cdebase.dev/v1/yantra-brain/audio/transcriptions';\nconst WHISPER_MODEL = 'whisper-large-v3-turbo';\n\nexport interface STTParams {\n /** Local file URI returned by `expo-audio`'s `recorder.uri` (e.g. `file:///...m4a`). */\n uri: string;\n /** Optional MIME type override. HIGH_QUALITY preset → `audio/m4a` on iOS & Android. */\n mimeType?: string;\n /** Optional filename hint sent with the multipart form. Whisper uses the extension to pick a decoder. */\n filename?: string;\n /** Optional Whisper \"prompt\" — passed through to bias decoding. */\n prompt?: string;\n}\n\nexport async function transcribeAudio(params: STTParams): Promise<string> {\n const { uri, mimeType = 'audio/m4a', filename = 'audio.m4a', prompt } = params;\n\n if (!uri) {\n throw new Error('Audio file uri is empty or not provided');\n }\n\n const formData = new FormData();\n /*\n * React Native's FormData accepts file parts in `{uri, name, type}` shape;\n * the runtime streams the file from disk on send. The cast is the usual\n * escape hatch — web FormData's TS lib types only know about\n * `Blob | string`, but the RN polyfill widens it at runtime.\n */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n formData.append('file', { uri, name: filename, type: mimeType } as any);\n formData.append('model', WHISPER_MODEL);\n if (prompt) {\n formData.append('prompt', prompt);\n }\n\n // yantra-brain resource endpoint — the gateway holds the Groq key, the\n // app sends NO Authorization header and ships NO key.\n return requestTranscription(BRAIN_STT_URL, formData);\n}\n\nasync function requestTranscription(url: string, formData: FormData): Promise<string> {\n const response = await fetch(url, {\n method: 'POST',\n /*\n * Intentionally NOT setting Content-Type. Letting `fetch` derive\n * it from FormData ensures the multipart boundary string is set\n * correctly. Overriding here would break the upload silently.\n */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n body: formData as any,\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n let errorMessage: string;\n try {\n const errorJson = JSON.parse(errorText);\n errorMessage = errorJson?.error?.message || errorJson?.message || errorText;\n } catch {\n errorMessage = errorText || response.statusText;\n }\n throw new Error(`STT API error (${response.status}): ${errorMessage}`);\n }\n\n const data = (await response.json()) as { text?: string };\n return data?.text?.trim() ?? '';\n}\n"],"names":[],"mappings":"AAkBA,MAAM,aAAgB,GAAA,uEAAA;AACtB,MAAM,aAAgB,GAAA,wBAAA;AAWtB,eAAsB,gBAAgB,MAAoC,EAAA;AACxE,EAAM,MAAA;AAAA,IACJ,GAAA;AAAA,IACA,QAAW,GAAA,WAAA;AAAA,IACX,QAAW,GAAA,WAAA;AAAA,IACX;AAAA,GACE,GAAA,MAAA;AACJ,EAAA,IAAI,CAAC,GAAK,EAAA;AACR,IAAM,MAAA,IAAI,MAAM,yCAAyC,CAAA;AAAA;AAE3D,EAAM,MAAA,QAAA,GAAW,IAAI,QAAS,EAAA;AAQ9B,EAAA,QAAA,CAAS,OAAO,MAAQ,EAAA;AAAA,IACtB,GAAA;AAAA,IACA,IAAM,EAAA,QAAA;AAAA,IACN,IAAM,EAAA;AAAA,GACA,CAAA;AACR,EAAS,QAAA,CAAA,MAAA,CAAO,SAAS,aAAa,CAAA;AACtC,EAAA,IAAI,MAAQ,EAAA;AACV,IAAS,QAAA,CAAA,MAAA,CAAO,UAAU,MAAM,CAAA;AAAA;AAKlC,EAAO,OAAA,oBAAA,CAAqB,eAAe,QAAQ,CAAA;AACrD;AACA,eAAe,oBAAA,CAAqB,KAAa,QAAqC,EAAA;AA9DtF,EAAA,IAAA,EAAA,EAAA,EAAA,EAAA,EAAA;AA+DE,EAAM,MAAA,QAAA,GAAW,MAAM,KAAA,CAAM,GAAK,EAAA;AAAA,IAChC,MAAQ,EAAA,MAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAOR,IAAM,EAAA;AAAA,GACP,CAAA;AACD,EAAI,IAAA,CAAC,SAAS,EAAI,EAAA;AAChB,IAAM,MAAA,SAAA,GAAY,MAAM,QAAA,CAAS,IAAK,EAAA;AACtC,IAAI,IAAA,YAAA;AACJ,IAAI,IAAA;AACF,MAAM,MAAA,SAAA,GAAY,IAAK,CAAA,KAAA,CAAM,SAAS,CAAA;AACtC,MAAA,YAAA,GAAA,CAAA,CAAe,EAAW,GAAA,SAAA,IAAA,IAAA,GAAA,KAAA,CAAA,GAAA,SAAA,CAAA,KAAA,KAAX,IAAkB,GAAA,KAAA,CAAA,GAAA,EAAA,CAAA,OAAA,MAAW,uCAAW,OAAW,CAAA,IAAA,SAAA;AAAA,KAC5D,CAAA,OAAA,CAAA,EAAA;AACN,MAAA,YAAA,GAAe,aAAa,QAAS,CAAA,UAAA;AAAA;AAEvC,IAAA,MAAM,IAAI,KAAM,CAAA,CAAA,eAAA,EAAkB,SAAS,MAAM,CAAA,GAAA,EAAM,YAAY,CAAE,CAAA,CAAA;AAAA;AAEvE,EAAM,MAAA,IAAA,GAAQ,MAAM,QAAA,CAAS,IAAK,EAAA;AAGlC,EAAA,OAAA,CAAO,EAAM,GAAA,CAAA,EAAA,GAAA,IAAA,IAAA,IAAA,GAAA,MAAA,GAAA,IAAA,CAAA,IAAA,KAAN,IAAY,GAAA,MAAA,GAAA,EAAA,CAAA,IAAA,EAAA,KAAZ,IAAsB,GAAA,EAAA,GAAA,EAAA;AAC/B"}
1
+ {"version":3,"file":"stt.js","sources":["../../src/api/stt.ts"],"sourcesContent":["/**\n * Speech-to-Text (STT) API using Groq's Whisper model - mobile port.\n *\n * Mirrors `packages-modules/account/browser/src/api/stt.ts` (which uploads\n * a `Blob` produced by the browser's `MediaRecorder`). On React Native we\n * upload a file by URI instead, because:\n * • `expo-audio` writes the recording to a local `file://` URI on disk,\n * not an in-memory Blob.\n * • RN's `fetch` + `FormData` already supports `{uri, name, type}` parts -\n * it streams the file directly without us reading bytes into JS, which\n * keeps a 3-minute clip from spiking memory.\n *\n * Mobile calls ONLY the agent's yantra-brain lane: the agent holds the Groq\n * key server-side. A shipped app bundle must never embed a provider key (it\n * cannot be rotated out of installed apps), so unlike web there is NO\n * direct-Groq BYO-key fallback here.\n *\n * The call goes to the agent THIS build's chat talks to (CdecliConnection's\n * `agentEndpoint`, from CDECLI_AGENT_ENDPOINT) as the signed-in user: the token\n * that connection hands the agent. cdecli 2.20.22 runs no model for an\n * anonymous caller, and the old hard-coded dev host with no header answered\n * every recording, on every build, with a 401.\n */\n\nconst WHISPER_MODEL = 'whisper-large-v3-turbo';\n\n/** What the panel shows when there is no signed-in user to transcribe as. */\nexport const STT_SIGN_IN_REQUIRED = 'Voice input needs you signed in to Yantra. Sign in, then try again.';\n\n/** The agent refused the token it was sent (HTTP 401): re-mint once, then retry. */\nexport class SttRefusedError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'SttRefusedError';\n }\n}\n\nexport interface STTParams {\n /** Local file URI returned by `expo-audio`'s `recorder.uri` (e.g. `file:///...m4a`). */\n uri: string;\n /** Optional MIME type override. HIGH_QUALITY preset → `audio/m4a` on iOS & Android. */\n mimeType?: string;\n /** Optional filename hint sent with the multipart form. Whisper uses the extension to pick a decoder. */\n filename?: string;\n /** Optional Whisper \"prompt\" - passed through to bias decoding. */\n prompt?: string;\n /** The cdecli agent the chat connects to (CdecliConnection's `agentEndpoint`). */\n endpoint: string;\n /** The signed-in user's system JWT (CdecliConnection's `obtainAgentToken`). None: nothing is sent. */\n token: string | null | undefined;\n}\n\n/** The agent's transcription lane on `endpoint`. */\nexport function brainSttUrl(endpoint: string): string {\n return `${endpoint.trim().replace(/\\/+$/, '')}/v1/yantra-brain/audio/transcriptions`;\n}\n\nasync function requestTranscription(url: string, formData: FormData, token: string): Promise<string> {\n const response = await fetch(url, {\n method: 'POST',\n /*\n * Intentionally NOT setting Content-Type. Letting `fetch` derive\n * it from FormData ensures the multipart boundary string is set\n * correctly. Overriding here would break the upload silently.\n */\n headers: { Authorization: `Bearer ${token}` },\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n body: formData as any,\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n let errorMessage: string;\n try {\n const errorJson = JSON.parse(errorText);\n errorMessage =\n (typeof errorJson?.error === 'string' ? errorJson.error : errorJson?.error?.message) ||\n errorJson?.message ||\n errorText;\n } catch {\n errorMessage = errorText || response.statusText;\n }\n const message = `STT API error (${response.status}): ${errorMessage}`;\n throw response.status === 401 ? new SttRefusedError(message) : new Error(message);\n }\n\n const data = (await response.json()) as { text?: string };\n return data?.text?.trim() ?? '';\n}\n\nexport async function transcribeAudio(params: STTParams): Promise<string> {\n const { uri, mimeType = 'audio/m4a', filename = 'audio.m4a', prompt } = params;\n\n if (!uri) {\n throw new Error('Audio file uri is empty or not provided');\n }\n const token = params.token?.trim();\n if (!token) {\n // Not worth an upload the agent will refuse: say what the person can do about it.\n throw new Error(STT_SIGN_IN_REQUIRED);\n }\n\n const formData = new FormData();\n /*\n * React Native's FormData accepts file parts in `{uri, name, type}` shape;\n * the runtime streams the file from disk on send. The cast is the usual\n * escape hatch - web FormData's TS lib types only know about\n * `Blob | string`, but the RN polyfill widens it at runtime.\n */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n formData.append('file', { uri, name: filename, type: mimeType } as any);\n formData.append('model', WHISPER_MODEL);\n if (prompt) {\n formData.append('prompt', prompt);\n }\n\n return requestTranscription(brainSttUrl(params.endpoint), formData, token);\n}\n\n/** Who to transcribe as: the chat connection's endpoint, token source and token recovery. */\nexport interface STTCaller {\n endpoint: string;\n obtainToken: () => Promise<string | null>;\n /** Re-mint a refused token (CdecliConnection's `recoverAgentAuth`). */\n recover?: () => Promise<boolean>;\n}\n\n/**\n * Transcribe as the signed-in user. A refused token (401) is re-minted once and\n * the upload sent again, the same recovery the computer-action lane uses; any\n * other failure is the caller's to show.\n */\nexport async function transcribeAsCaller(\n params: Omit<STTParams, 'endpoint' | 'token'>,\n caller: STTCaller,\n): Promise<string> {\n const send = async () =>\n transcribeAudio({ ...params, endpoint: caller.endpoint, token: await caller.obtainToken().catch(() => null) });\n try {\n return await send();\n } catch (err) {\n if (!(err instanceof SttRefusedError) || !caller.recover) throw err;\n const recovered = await caller.recover().catch(() => false);\n if (!recovered) throw err;\n return send();\n }\n}\n"],"names":[],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import {jsx}from'react/jsx-runtime';import {useContext,createContext,useState,useCallback,useMemo}from'react';import {useCdecliAutoConnect}from'../hooks/useCdecliAutoConnect.js';import {useGatewayContext}from'./GatewayContext.js';var __defProp = Object.defineProperty;
1
+ import {jsx}from'react/jsx-runtime';import {useContext,createContext,useState,useCallback,useRef,useEffect,useMemo}from'react';import {useCdecliAutoConnect}from'../hooks/useCdecliAutoConnect.js';import {registerBrainTokenProvider}from'../services/brainQuickReply.js';import {useGatewayContext}from'./GatewayContext.js';var __defProp = Object.defineProperty;
2
2
  var __defProps = Object.defineProperties;
3
3
  var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
4
4
  var __getOwnPropSymbols = Object.getOwnPropertySymbols;
@@ -31,6 +31,9 @@ function CdecliConnectionProvider({
31
31
  setActiveChannelIdState((prev) => prev === channelId ? prev : channelId);
32
32
  }, []);
33
33
  const connection = useCdecliAutoConnect(cdecliSelected, activeChannelId);
34
+ const obtainAgentTokenRef = useRef(connection.obtainAgentToken);
35
+ obtainAgentTokenRef.current = connection.obtainAgentToken;
36
+ useEffect(() => registerBrainTokenProvider(() => obtainAgentTokenRef.current()), [connection.accountId]);
34
37
  const value = useMemo(() => __spreadProps(__spreadValues({}, connection), {
35
38
  cdecliSelected,
36
39
  activeChannelId,
@@ -1 +1 @@
1
- {"version":3,"file":"CdecliConnectionContext.js","sources":["../../src/contexts/CdecliConnectionContext.tsx"],"sourcesContent":["/**\n * Single, app-wide owner of the CDeCLI auto-connect lifecycle.\n *\n * Previously every consumer that needed CDeCLI status (the header\n * `GatewayConnector`, the Chat screen for streaming routing, etc.) called\n * `useCdecliAutoConnect` directly. Each instance kept its own state, so they\n * raced each other: the header would mount and connect with `chatId=undefined`\n * while the Chat screen mounted and connected with `chatId=<channelId>`. The\n * backend `CdecliServeProvider.connect()` reuses the per-account session, so\n * the LAST `gatewayConnect` mutation wins — when the header's \"no chatId\"\n * connect lost the chat binding, the agent received messages without a chat\n * context and replied with the generic \"agent did not return a response /\n * API key may be missing\" fallback.\n *\n * The fix: lift `useCdecliAutoConnect` into a context that runs ONCE per app\n * mount. Screens that need to bind a specific chat call `setActiveChannelId`;\n * everyone else (header pill, status banners) just reads the live status.\n *\n * Mount this provider INSIDE `GatewayContextProvider` — the hook depends on\n * `useGatewayContext` for the current selection.\n */\nimport type { ReactNode } from 'react';\nimport React, { createContext, useCallback, useContext, useMemo, useState } from 'react';\nimport { useCdecliAutoConnect, type UseCdecliAutoConnectResult } from '../hooks/useCdecliAutoConnect';\nimport { useGatewayContext } from './GatewayContext';\n\nexport interface CdecliConnectionContextValue extends UseCdecliAutoConnectResult {\n /** Whether the CDeCLI gateway is the currently selected gateway. */\n cdecliSelected: boolean;\n /** Current chat binding for the gateway session (undefined on Home / Chat history). */\n activeChannelId: string | undefined;\n /**\n * Register / clear the chat binding for the gateway session.\n * Screens that own a channel (Chat) call this on mount with their channelId\n * and on unmount with `undefined` (or simply leave the last binding in\n * place — the gateway tolerates stale chatIds, individual messages still\n * pass their own chatId via `gatewaySendMessage`).\n */\n setActiveChannelId: (channelId: string | undefined) => void;\n}\n\nconst CdecliConnectionContext = createContext<CdecliConnectionContextValue | null>(null);\nCdecliConnectionContext.displayName = 'CdecliConnectionContext';\n\nexport function CdecliConnectionProvider({ children }: { children: ReactNode }) {\n const { selectedGateway } = useGatewayContext();\n const cdecliSelected = selectedGateway?.channelType === 'cdecli-serve';\n\n const [activeChannelId, setActiveChannelIdState] = useState<string | undefined>(undefined);\n\n const setActiveChannelId = useCallback((channelId: string | undefined) => {\n setActiveChannelIdState((prev) => (prev === channelId ? prev : channelId));\n }, []);\n\n const connection = useCdecliAutoConnect(cdecliSelected, activeChannelId);\n\n const value = useMemo<CdecliConnectionContextValue>(\n () => ({\n ...connection,\n cdecliSelected,\n activeChannelId,\n setActiveChannelId,\n }),\n [connection, cdecliSelected, activeChannelId, setActiveChannelId],\n );\n\n return <CdecliConnectionContext.Provider value={value}>{children}</CdecliConnectionContext.Provider>;\n}\n\nexport function useCdecliConnection(): CdecliConnectionContextValue {\n const ctx = useContext(CdecliConnectionContext);\n if (!ctx) {\n throw new Error('useCdecliConnection must be used within CdecliConnectionProvider');\n }\n return ctx;\n}\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;AAuCA,MAAM,uBAAA,GAA0B,cAAmD,IAAI,CAAA;AACvF,uBAAA,CAAwB,WAAc,GAAA,yBAAA;AAC/B,SAAS,wBAAyB,CAAA;AAAA,EACvC;AACF,CAEG,EAAA;AACD,EAAM,MAAA;AAAA,IACJ;AAAA,MACE,iBAAkB,EAAA;AACtB,EAAM,MAAA,cAAA,GAAA,CAAiB,mDAAiB,WAAgB,MAAA,cAAA;AACxD,EAAA,MAAM,CAAC,eAAA,EAAiB,uBAAuB,CAAA,GAAI,SAA6B,MAAS,CAAA;AACzF,EAAM,MAAA,kBAAA,GAAqB,WAAY,CAAA,CAAC,SAAkC,KAAA;AACxE,IAAA,uBAAA,CAAwB,CAAQ,IAAA,KAAA,IAAA,KAAS,SAAY,GAAA,IAAA,GAAO,SAAS,CAAA;AAAA,GACvE,EAAG,EAAE,CAAA;AACL,EAAM,MAAA,UAAA,GAAa,oBAAqB,CAAA,cAAA,EAAgB,eAAe,CAAA;AACvE,EAAA,MAAM,KAAQ,GAAA,OAAA,CAAsC,MAAO,aAAA,CAAA,cAAA,CAAA,EAAA,EACtD,UADsD,CAAA,EAAA;AAAA,IAEzD,cAAA;AAAA,IACA,eAAA;AAAA,IACA;AAAA,MACE,CAAC,UAAA,EAAY,cAAgB,EAAA,eAAA,EAAiB,kBAAkB,CAAC,CAAA;AACrE,EAAA,uBAAQ,GAAA,CAAA,uBAAA,CAAwB,QAAxB,EAAA,EAAiC,OAAe,QAAS,EAAA,CAAA;AACnE;AACO,SAAS,mBAAoD,GAAA;AAClE,EAAM,MAAA,GAAA,GAAM,WAAW,uBAAuB,CAAA;AAC9C,EAAA,IAAI,CAAC,GAAK,EAAA;AACR,IAAM,MAAA,IAAI,MAAM,kEAAkE,CAAA;AAAA;AAEpF,EAAO,OAAA,GAAA;AACT"}
1
+ {"version":3,"file":"CdecliConnectionContext.js","sources":["../../src/contexts/CdecliConnectionContext.tsx"],"sourcesContent":["/**\n * Single, app-wide owner of the CDeCLI auto-connect lifecycle.\n *\n * Previously every consumer that needed CDeCLI status (the header\n * `GatewayConnector`, the Chat screen for streaming routing, etc.) called\n * `useCdecliAutoConnect` directly. Each instance kept its own state, so they\n * raced each other: the header would mount and connect with `chatId=undefined`\n * while the Chat screen mounted and connected with `chatId=<channelId>`. The\n * backend `CdecliServeProvider.connect()` reuses the per-account session, so\n * the LAST `gatewayConnect` mutation wins — when the header's \"no chatId\"\n * connect lost the chat binding, the agent received messages without a chat\n * context and replied with the generic \"agent did not return a response /\n * API key may be missing\" fallback.\n *\n * The fix: lift `useCdecliAutoConnect` into a context that runs ONCE per app\n * mount. Screens that need to bind a specific chat call `setActiveChannelId`;\n * everyone else (header pill, status banners) just reads the live status.\n *\n * Mount this provider INSIDE `GatewayContextProvider` — the hook depends on\n * `useGatewayContext` for the current selection.\n */\nimport type { ReactNode } from 'react';\nimport React, { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';\nimport { useCdecliAutoConnect, type UseCdecliAutoConnectResult } from '../hooks/useCdecliAutoConnect';\nimport { registerBrainTokenProvider } from '../services/brainQuickReply';\nimport { useGatewayContext } from './GatewayContext';\n\nexport interface CdecliConnectionContextValue extends UseCdecliAutoConnectResult {\n /** Whether the CDeCLI gateway is the currently selected gateway. */\n cdecliSelected: boolean;\n /** Current chat binding for the gateway session (undefined on Home / Chat history). */\n activeChannelId: string | undefined;\n /**\n * Register / clear the chat binding for the gateway session.\n * Screens that own a channel (Chat) call this on mount with their channelId\n * and on unmount with `undefined` (or simply leave the last binding in\n * place — the gateway tolerates stale chatIds, individual messages still\n * pass their own chatId via `gatewaySendMessage`).\n */\n setActiveChannelId: (channelId: string | undefined) => void;\n}\n\nconst CdecliConnectionContext = createContext<CdecliConnectionContextValue | null>(null);\nCdecliConnectionContext.displayName = 'CdecliConnectionContext';\n\nexport function CdecliConnectionProvider({ children }: { children: ReactNode }) {\n const { selectedGateway } = useGatewayContext();\n const cdecliSelected = selectedGateway?.channelType === 'cdecli-serve';\n\n const [activeChannelId, setActiveChannelIdState] = useState<string | undefined>(undefined);\n\n const setActiveChannelId = useCallback((channelId: string | undefined) => {\n setActiveChannelIdState((prev) => (prev === channelId ? prev : channelId));\n }, []);\n\n const connection = useCdecliAutoConnect(cdecliSelected, activeChannelId);\n\n // The brain helpers (services/brainQuickReply) call the web app's brain proxy as\n // the signed-in user, with the same token this connection hands the agent. The\n // ref keeps the newest obtainAgentToken without re-registering on every render;\n // re-registering per account drops the previous account's cached token.\n const obtainAgentTokenRef = useRef(connection.obtainAgentToken);\n obtainAgentTokenRef.current = connection.obtainAgentToken;\n useEffect(() => registerBrainTokenProvider(() => obtainAgentTokenRef.current()), [connection.accountId]);\n\n const value = useMemo<CdecliConnectionContextValue>(\n () => ({\n ...connection,\n cdecliSelected,\n activeChannelId,\n setActiveChannelId,\n }),\n [connection, cdecliSelected, activeChannelId, setActiveChannelId],\n );\n\n return <CdecliConnectionContext.Provider value={value}>{children}</CdecliConnectionContext.Provider>;\n}\n\nexport function useCdecliConnection(): CdecliConnectionContextValue {\n const ctx = useContext(CdecliConnectionContext);\n if (!ctx) {\n throw new Error('useCdecliConnection must be used within CdecliConnectionProvider');\n }\n return ctx;\n}\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;AAwCA,MAAM,uBAAA,GAA0B,cAAmD,IAAI,CAAA;AACvF,uBAAA,CAAwB,WAAc,GAAA,yBAAA;AAC/B,SAAS,wBAAyB,CAAA;AAAA,EACvC;AACF,CAEG,EAAA;AACD,EAAM,MAAA;AAAA,IACJ;AAAA,MACE,iBAAkB,EAAA;AACtB,EAAM,MAAA,cAAA,GAAA,CAAiB,mDAAiB,WAAgB,MAAA,cAAA;AACxD,EAAA,MAAM,CAAC,eAAA,EAAiB,uBAAuB,CAAA,GAAI,SAA6B,MAAS,CAAA;AACzF,EAAM,MAAA,kBAAA,GAAqB,WAAY,CAAA,CAAC,SAAkC,KAAA;AACxE,IAAA,uBAAA,CAAwB,CAAQ,IAAA,KAAA,IAAA,KAAS,SAAY,GAAA,IAAA,GAAO,SAAS,CAAA;AAAA,GACvE,EAAG,EAAE,CAAA;AACL,EAAM,MAAA,UAAA,GAAa,oBAAqB,CAAA,cAAA,EAAgB,eAAe,CAAA;AAMvE,EAAM,MAAA,mBAAA,GAAsB,MAAO,CAAA,UAAA,CAAW,gBAAgB,CAAA;AAC9D,EAAA,mBAAA,CAAoB,UAAU,UAAW,CAAA,gBAAA;AACzC,EAAU,SAAA,CAAA,MAAM,0BAA2B,CAAA,MAAM,mBAAoB,CAAA,OAAA,EAAS,CAAG,EAAA,CAAC,UAAW,CAAA,SAAS,CAAC,CAAA;AACvG,EAAA,MAAM,KAAQ,GAAA,OAAA,CAAsC,MAAO,aAAA,CAAA,cAAA,CAAA,EAAA,EACtD,UADsD,CAAA,EAAA;AAAA,IAEzD,cAAA;AAAA,IACA,eAAA;AAAA,IACA;AAAA,MACE,CAAC,UAAA,EAAY,cAAgB,EAAA,eAAA,EAAiB,kBAAkB,CAAC,CAAA;AACrE,EAAA,uBAAQ,GAAA,CAAA,uBAAA,CAAwB,QAAxB,EAAA,EAAiC,OAAe,QAAS,EAAA,CAAA;AACnE;AACO,SAAS,mBAAoD,GAAA;AAClE,EAAM,MAAA,GAAA,GAAM,WAAW,uBAAuB,CAAA;AAC9C,EAAA,IAAI,CAAC,GAAK,EAAA;AACR,IAAM,MAAA,IAAI,MAAM,kEAAkE,CAAA;AAAA;AAEpF,EAAO,OAAA,GAAA;AACT"}
@@ -1,4 +1,4 @@
1
- import {jsxs,jsx}from'react/jsx-runtime';import {useState,useRef,useEffect,useCallback,useMemo}from'react';import {Animated,Easing,View,StyleSheet,ActivityIndicator,Pressable}from'react-native';import {Ionicons,Feather}from'@expo/vector-icons';import {useAudioRecorder,RecordingPresets,useAudioRecorderState,setAudioModeAsync}from'expo-audio';import {transcribeAudio}from'../../api/stt.js';const MAX_DURATION_MS = 3 * 60 * 1e3;
1
+ import {jsxs,jsx}from'react/jsx-runtime';import {useState,useRef,useEffect,useCallback,useMemo}from'react';import {Animated,Easing,View,StyleSheet,ActivityIndicator,Pressable}from'react-native';import {Ionicons,Feather}from'@expo/vector-icons';import {useAudioRecorder,RecordingPresets,useAudioRecorderState,setAudioModeAsync}from'expo-audio';import {transcribeAsCaller}from'../../api/stt.js';import {useCdecliConnection}from'../../contexts/CdecliConnectionContext.js';const MAX_DURATION_MS = 3 * 60 * 1e3;
2
2
  function AudioRecorderPanel({
3
3
  isDark,
4
4
  onTranscriptionComplete,
@@ -8,6 +8,11 @@ function AudioRecorderPanel({
8
8
  const recorder = useAudioRecorder(RecordingPresets.HIGH_QUALITY);
9
9
  const recorderState = useAudioRecorderState(recorder, 200);
10
10
  const [isTranscribing, setIsTranscribing] = useState(false);
11
+ const {
12
+ agentEndpoint,
13
+ obtainAgentToken,
14
+ recoverAgentAuth
15
+ } = useCdecliConnection();
11
16
  const hasStartedRef = useRef(false);
12
17
  const isMountedRef = useRef(true);
13
18
  const sendInFlightRef = useRef(false);
@@ -48,14 +53,20 @@ function AudioRecorderPanel({
48
53
  if (isMountedRef.current) setIsTranscribing(true);
49
54
  try {
50
55
  await stopAndCleanup();
51
- const uri = recorder.uri;
56
+ const {
57
+ uri
58
+ } = recorder;
52
59
  if (!uri) {
53
60
  throw new Error("No recording produced");
54
61
  }
55
- const text = await transcribeAudio({
62
+ const text = await transcribeAsCaller({
56
63
  uri,
57
64
  mimeType: "audio/m4a",
58
65
  filename: "audio.m4a"
66
+ }, {
67
+ endpoint: agentEndpoint,
68
+ obtainToken: obtainAgentToken,
69
+ recover: recoverAgentAuth
59
70
  });
60
71
  if (isMountedRef.current) {
61
72
  onTranscriptionComplete(text);
@@ -68,7 +79,7 @@ function AudioRecorderPanel({
68
79
  sendInFlightRef.current = false;
69
80
  if (isMountedRef.current) setIsTranscribing(false);
70
81
  }
71
- }, [isTranscribing, recorder, stopAndCleanup, onTranscriptionComplete, onError, onCancel]);
82
+ }, [isTranscribing, recorder, stopAndCleanup, onTranscriptionComplete, onError, onCancel, agentEndpoint, obtainAgentToken, recoverAgentAuth]);
72
83
  useEffect(() => {
73
84
  if (hasStartedRef.current) return;
74
85
  hasStartedRef.current = true;
@@ -89,12 +100,14 @@ function AudioRecorderPanel({
89
100
  cancelled = true;
90
101
  };
91
102
  }, []);
92
- useEffect(() => {
93
- return () => {
103
+ useEffect(
104
+ () => () => {
94
105
  isMountedRef.current = false;
95
106
  void stopAndCleanup();
96
- };
97
- }, []);
107
+ },
108
+ // eslint-disable-next-line react-hooks/exhaustive-deps
109
+ []
110
+ );
98
111
  useEffect(() => {
99
112
  var _a;
100
113
  if (!recorderState.isRecording) return;
@@ -1 +1 @@
1
- {"version":3,"file":"AudioRecorderPanel.js","sources":["../../../src/features/audio-input/AudioRecorderPanel.tsx"],"sourcesContent":["import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport { ActivityIndicator, Animated, Easing, Pressable, StyleSheet, View } from 'react-native';\nimport { Feather, Ionicons } from '@expo/vector-icons';\nimport { AudioModule, RecordingPresets, setAudioModeAsync, useAudioRecorder, useAudioRecorderState } from 'expo-audio';\nimport { transcribeAudio } from '../../api/stt';\n\n/**\n * Mirrors the web ceiling in\n * `packages-modules/account/browser/src/features/audio-input/components/AudioRecorder.tsx`.\n * Three minutes is a soft Whisper ceiling — past that, accuracy and upload\n * time both degrade more than the marginal extra context is worth.\n */\nconst MAX_DURATION_MS = 3 * 60 * 1000;\n\nexport interface AudioRecorderPanelProps {\n isDark: boolean;\n /**\n * Called once with the final transcribed string after the user taps \"Send\".\n * The host screen typically appends this to its composer state and re-focuses\n * the text input — mirroring the web `handleTranscriptionComplete` flow.\n */\n onTranscriptionComplete: (text: string) => void;\n /** Called when the user discards the recording (X) or recording cannot start. */\n onCancel: () => void;\n /** Optional error sink — host can surface a toast/banner. */\n onError?: (error: string) => void;\n}\n\n/**\n * Inline voice-capture panel that replaces the composer while recording —\n * the React Native port of the web `AudioRecorder` component\n * (`packages-modules/account/browser/src/features/audio-input/components/AudioRecorder.tsx`).\n *\n * Flow (parity with web):\n * 1. Mount → request mic permission → set audio mode → prepare + start recording.\n * 2. While recording, display a live timer and pulsing record dot.\n * 3. User taps \"Send\" → stop recorder → upload `recorder.uri` to Groq Whisper\n * via `transcribeAudio` → return the transcribed text to the host via\n * `onTranscriptionComplete`.\n * 4. User taps \"Cancel\" (X) → stop + discard, no upload.\n * 5. If recording exceeds `MAX_DURATION_MS`, auto-finalize as if the user\n * tapped Send (matches web safety net).\n *\n * The panel is intentionally NOT rendered inside the `InputToolBar`'s `topContent`\n * slot — web fully swaps the toolbar for the recorder. Keeping the toolbar\n * visible during recording invites accidental sends with empty input and an\n * unfocused recorder state.\n */\nexport function AudioRecorderPanel({ isDark, onTranscriptionComplete, onCancel, onError }: AudioRecorderPanelProps) {\n const recorder = useAudioRecorder(RecordingPresets.HIGH_QUALITY);\n const recorderState = useAudioRecorderState(recorder, 200);\n const [isTranscribing, setIsTranscribing] = useState(false);\n\n /**\n * Guard against re-entry. `useEffect` runs once on mount but a fast double-\n * tap on the mic can produce two panel mounts in dev mode (StrictMode);\n * `hasStartedRef` ensures we don't start a second recorder against the\n * same shared object.\n */\n const hasStartedRef = useRef(false);\n const isMountedRef = useRef(true);\n /**\n * Caches the result of the in-flight transcription/cleanup pipeline so the\n * MAX_DURATION watcher and the explicit Send button can both call\n * `finalize()` without racing each other into a double-upload.\n */\n const sendInFlightRef = useRef(false);\n\n /**\n * Pulsing dot for the \"REC\" indicator. Pure UI sugar; avoids importing\n * reanimated for a 2-keyframe loop.\n */\n const pulse = useRef(new Animated.Value(1)).current;\n useEffect(() => {\n const loop = Animated.loop(\n Animated.sequence([\n Animated.timing(pulse, {\n toValue: 0.3,\n duration: 700,\n easing: Easing.inOut(Easing.ease),\n useNativeDriver: true,\n }),\n Animated.timing(pulse, {\n toValue: 1,\n duration: 700,\n easing: Easing.inOut(Easing.ease),\n useNativeDriver: true,\n }),\n ]),\n );\n loop.start();\n return () => loop.stop();\n }, [pulse]);\n\n const stopAndCleanup = useCallback(async () => {\n try {\n if (recorderState.isRecording || recorder.isRecording) {\n await recorder.stop().catch(() => undefined);\n }\n } catch {\n /* best-effort */\n }\n try {\n /*\n * Releasing the recording audio session matters more on iOS — leaving\n * `allowsRecording: true` will dim playback for the rest of the app\n * lifecycle. We tolerate failures because the AVAudioSession may\n * already be torn down by the time we get here.\n */\n await setAudioModeAsync({ allowsRecording: false, playsInSilentMode: true });\n } catch {\n /* best-effort */\n }\n }, [recorder, recorderState.isRecording]);\n\n /**\n * One-shot finalize: stop → upload → emit transcript. Swallowed errors get\n * surfaced via `onError` so the host can show a toast. We always clear\n * `sendInFlightRef` even on error so a retry path is possible if needed.\n */\n const finalize = useCallback(async () => {\n if (sendInFlightRef.current || isTranscribing) return;\n sendInFlightRef.current = true;\n if (isMountedRef.current) setIsTranscribing(true);\n try {\n await stopAndCleanup();\n const uri = recorder.uri;\n if (!uri) {\n throw new Error('No recording produced');\n }\n const text = await transcribeAudio({ uri, mimeType: 'audio/m4a', filename: 'audio.m4a' });\n if (isMountedRef.current) {\n onTranscriptionComplete(text);\n }\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n onError?.(msg);\n onCancel();\n } finally {\n sendInFlightRef.current = false;\n if (isMountedRef.current) setIsTranscribing(false);\n }\n }, [isTranscribing, recorder, stopAndCleanup, onTranscriptionComplete, onError, onCancel]);\n\n useEffect(() => {\n if (hasStartedRef.current) return;\n hasStartedRef.current = true;\n let cancelled = false;\n const start = async () => {\n try {\n /*\n * Host screen already called `requestMicPermission()` (expo-audio\n * docs flow) before mounting this panel — go straight to\n * prepare + record.\n */\n await recorder.prepareToRecordAsync();\n if (cancelled || !isMountedRef.current) return;\n recorder.record();\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n onError?.(msg);\n onCancel();\n }\n };\n void start();\n return () => {\n cancelled = true;\n };\n // Mount-only effect: recorder/start refs are stable for the life of the panel.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n useEffect(() => {\n return () => {\n isMountedRef.current = false;\n /*\n * Unmount cleanup: fire-and-forget. We don't await anything here\n * because React doesn't allow async destructors and we can't\n * guarantee the recorder shared object outlives this tick. The\n * stopAndCleanup util is null-safe.\n */\n void stopAndCleanup();\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n /**\n * Max-duration safety net. `RecorderState.durationMillis` already reports\n * elapsed time in milliseconds (see `expo-audio` `Audio.types.ts`), which\n * lines up directly with our `MAX_DURATION_MS` ceiling (web parity).\n */\n useEffect(() => {\n if (!recorderState.isRecording) return;\n const elapsedMs = recorderState.durationMillis ?? 0;\n if (elapsedMs >= MAX_DURATION_MS && !sendInFlightRef.current) {\n void finalize();\n }\n }, [recorderState.isRecording, recorderState.durationMillis, finalize]);\n\n const handleCancelPress = useCallback(async () => {\n if (isTranscribing) return;\n await stopAndCleanup();\n onCancel();\n }, [isTranscribing, stopAndCleanup, onCancel]);\n\n const handleSendPress = useCallback(() => {\n void finalize();\n }, [finalize]);\n\n const formatted = useMemo(() => {\n const totalSeconds = Math.floor((recorderState.durationMillis ?? 0) / 1000);\n const m = Math.floor(totalSeconds / 60);\n const s = totalSeconds % 60;\n return `${m}:${s.toString().padStart(2, '0')}`;\n }, [recorderState.durationMillis]);\n\n const bg = isDark ? '#0f172a' : '#ffffff';\n const border = isDark ? 'rgba(148,163,184,0.18)' : '#e4e4e7';\n const fg = isDark ? '#e5e7eb' : '#18181b';\n const muted = isDark ? '#94a3b8' : '#71717a';\n const cancelBg = isDark ? 'rgba(148,163,184,0.08)' : '#f4f4f5';\n\n const showSpinner = isTranscribing;\n const isRecording = recorderState.isRecording && !showSpinner;\n const statusLabel = showSpinner ? 'Transcribing…' : isRecording ? formatted : 'Initializing…';\n\n return (\n <View\n accessibilityRole=\"summary\"\n accessibilityLabel=\"Voice recorder\"\n style={[styles.container, { backgroundColor: bg, borderColor: border }]}\n >\n <View style={styles.statusRow}>\n {showSpinner ? (\n <ActivityIndicator size=\"small\" color={fg} />\n ) : (\n <Animated.View style={[styles.recDot, { opacity: isRecording ? pulse : 0.35 }]} />\n )}\n <Animated.Text style={[styles.timer, { color: fg }]} accessibilityLiveRegion=\"polite\">\n {statusLabel}\n </Animated.Text>\n {isRecording && <Animated.Text style={[styles.maxLabel, { color: muted }]}>/ 3:00</Animated.Text>}\n </View>\n <View style={styles.actionsRow}>\n <Pressable\n onPress={handleCancelPress}\n disabled={isTranscribing}\n accessibilityLabel=\"Cancel recording\"\n accessibilityRole=\"button\"\n style={({ pressed }) => [\n styles.iconBtn,\n {\n backgroundColor: cancelBg,\n borderColor: border,\n opacity: isTranscribing ? 0.4 : pressed ? 0.7 : 1,\n },\n ]}\n >\n <Ionicons name=\"close\" size={16} color={fg} />\n </Pressable>\n <Pressable\n onPress={handleSendPress}\n disabled={isTranscribing || !isRecording}\n accessibilityLabel=\"Stop and transcribe\"\n accessibilityRole=\"button\"\n style={({ pressed }) => [\n styles.sendBtn,\n {\n opacity: isTranscribing ? 0.7 : !isRecording ? 0.5 : pressed ? 0.85 : 1,\n },\n ]}\n >\n {isTranscribing ? (\n <ActivityIndicator size=\"small\" color=\"#ffffff\" />\n ) : (\n <Feather name=\"arrow-up\" size={16} color=\"#ffffff\" />\n )}\n </Pressable>\n </View>\n </View>\n );\n}\n\nconst styles = StyleSheet.create({\n container: {\n flexDirection: 'row',\n alignItems: 'center',\n justifyContent: 'space-between',\n borderRadius: 24,\n borderWidth: 1,\n paddingVertical: 10,\n paddingHorizontal: 14,\n gap: 12,\n shadowColor: '#0f172a',\n shadowOpacity: 0.08,\n shadowRadius: 12,\n shadowOffset: { width: 0, height: 4 },\n elevation: 4,\n },\n statusRow: {\n flexDirection: 'row',\n alignItems: 'center',\n gap: 8,\n flexShrink: 1,\n },\n recDot: {\n width: 8,\n height: 8,\n borderRadius: 4,\n backgroundColor: '#ef4444',\n },\n timer: {\n fontSize: 13,\n fontWeight: '600',\n fontVariant: ['tabular-nums'],\n },\n maxLabel: {\n fontSize: 11,\n },\n actionsRow: {\n flexDirection: 'row',\n alignItems: 'center',\n gap: 8,\n },\n iconBtn: {\n width: 32,\n height: 32,\n borderRadius: 16,\n borderWidth: 1,\n alignItems: 'center',\n justifyContent: 'center',\n },\n sendBtn: {\n width: 32,\n height: 32,\n borderRadius: 16,\n alignItems: 'center',\n justifyContent: 'center',\n backgroundColor: '#18181b',\n },\n});\n\nexport default AudioRecorderPanel;\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"AudioRecorderPanel.js","sources":["../../../src/features/audio-input/AudioRecorderPanel.tsx"],"sourcesContent":["import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport { ActivityIndicator, Animated, Easing, Pressable, StyleSheet, View } from 'react-native';\nimport { Feather, Ionicons } from '@expo/vector-icons';\nimport { RecordingPresets, setAudioModeAsync, useAudioRecorder, useAudioRecorderState } from 'expo-audio';\nimport { transcribeAsCaller } from '../../api/stt';\nimport { useCdecliConnection } from '../../contexts/CdecliConnectionContext';\n\n/**\n * Mirrors the web ceiling in\n * `packages-modules/account/browser/src/features/audio-input/components/AudioRecorder.tsx`.\n * Three minutes is a soft Whisper ceiling — past that, accuracy and upload\n * time both degrade more than the marginal extra context is worth.\n */\nconst MAX_DURATION_MS = 3 * 60 * 1000;\n\nexport interface AudioRecorderPanelProps {\n isDark: boolean;\n /**\n * Called once with the final transcribed string after the user taps \"Send\".\n * The host screen typically appends this to its composer state and re-focuses\n * the text input — mirroring the web `handleTranscriptionComplete` flow.\n */\n onTranscriptionComplete: (text: string) => void;\n /** Called when the user discards the recording (X) or recording cannot start. */\n onCancel: () => void;\n /** Optional error sink — host can surface a toast/banner. */\n onError?: (error: string) => void;\n}\n\n/**\n * Inline voice-capture panel that replaces the composer while recording —\n * the React Native port of the web `AudioRecorder` component\n * (`packages-modules/account/browser/src/features/audio-input/components/AudioRecorder.tsx`).\n *\n * Flow (parity with web):\n * 1. Mount → request mic permission → set audio mode → prepare + start recording.\n * 2. While recording, display a live timer and pulsing record dot.\n * 3. User taps \"Send\" → stop recorder → upload `recorder.uri` to Groq Whisper\n * via `transcribeAudio` → return the transcribed text to the host via\n * `onTranscriptionComplete`.\n * 4. User taps \"Cancel\" (X) → stop + discard, no upload.\n * 5. If recording exceeds `MAX_DURATION_MS`, auto-finalize as if the user\n * tapped Send (matches web safety net).\n *\n * The panel is intentionally NOT rendered inside the `InputToolBar`'s `topContent`\n * slot — web fully swaps the toolbar for the recorder. Keeping the toolbar\n * visible during recording invites accidental sends with empty input and an\n * unfocused recorder state.\n */\nexport function AudioRecorderPanel({ isDark, onTranscriptionComplete, onCancel, onError }: AudioRecorderPanelProps) {\n const recorder = useAudioRecorder(RecordingPresets.HIGH_QUALITY);\n const recorderState = useAudioRecorderState(recorder, 200);\n const [isTranscribing, setIsTranscribing] = useState(false);\n // Transcribe on the agent the chat talks to, as the user the chat runs as (see api/stt).\n const { agentEndpoint, obtainAgentToken, recoverAgentAuth } = useCdecliConnection();\n\n /**\n * Guard against re-entry. `useEffect` runs once on mount but a fast double-\n * tap on the mic can produce two panel mounts in dev mode (StrictMode);\n * `hasStartedRef` ensures we don't start a second recorder against the\n * same shared object.\n */\n const hasStartedRef = useRef(false);\n const isMountedRef = useRef(true);\n /**\n * Caches the result of the in-flight transcription/cleanup pipeline so the\n * MAX_DURATION watcher and the explicit Send button can both call\n * `finalize()` without racing each other into a double-upload.\n */\n const sendInFlightRef = useRef(false);\n\n /**\n * Pulsing dot for the \"REC\" indicator. Pure UI sugar; avoids importing\n * reanimated for a 2-keyframe loop.\n */\n const pulse = useRef(new Animated.Value(1)).current;\n useEffect(() => {\n const loop = Animated.loop(\n Animated.sequence([\n Animated.timing(pulse, {\n toValue: 0.3,\n duration: 700,\n easing: Easing.inOut(Easing.ease),\n useNativeDriver: true,\n }),\n Animated.timing(pulse, {\n toValue: 1,\n duration: 700,\n easing: Easing.inOut(Easing.ease),\n useNativeDriver: true,\n }),\n ]),\n );\n loop.start();\n return () => loop.stop();\n }, [pulse]);\n\n const stopAndCleanup = useCallback(async () => {\n try {\n if (recorderState.isRecording || recorder.isRecording) {\n await recorder.stop().catch(() => undefined);\n }\n } catch {\n /* best-effort */\n }\n try {\n /*\n * Releasing the recording audio session matters more on iOS — leaving\n * `allowsRecording: true` will dim playback for the rest of the app\n * lifecycle. We tolerate failures because the AVAudioSession may\n * already be torn down by the time we get here.\n */\n await setAudioModeAsync({ allowsRecording: false, playsInSilentMode: true });\n } catch {\n /* best-effort */\n }\n }, [recorder, recorderState.isRecording]);\n\n /**\n * One-shot finalize: stop → upload → emit transcript. Swallowed errors get\n * surfaced via `onError` so the host can show a toast. We always clear\n * `sendInFlightRef` even on error so a retry path is possible if needed.\n */\n const finalize = useCallback(async () => {\n if (sendInFlightRef.current || isTranscribing) return;\n sendInFlightRef.current = true;\n if (isMountedRef.current) setIsTranscribing(true);\n try {\n await stopAndCleanup();\n const { uri } = recorder;\n if (!uri) {\n throw new Error('No recording produced');\n }\n const text = await transcribeAsCaller(\n { uri, mimeType: 'audio/m4a', filename: 'audio.m4a' },\n { endpoint: agentEndpoint, obtainToken: obtainAgentToken, recover: recoverAgentAuth },\n );\n if (isMountedRef.current) {\n onTranscriptionComplete(text);\n }\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n onError?.(msg);\n onCancel();\n } finally {\n sendInFlightRef.current = false;\n if (isMountedRef.current) setIsTranscribing(false);\n }\n }, [\n isTranscribing,\n recorder,\n stopAndCleanup,\n onTranscriptionComplete,\n onError,\n onCancel,\n agentEndpoint,\n obtainAgentToken,\n recoverAgentAuth,\n ]);\n\n useEffect(() => {\n if (hasStartedRef.current) return;\n hasStartedRef.current = true;\n let cancelled = false;\n const start = async () => {\n try {\n /*\n * Host screen already called `requestMicPermission()` (expo-audio\n * docs flow) before mounting this panel — go straight to\n * prepare + record.\n */\n await recorder.prepareToRecordAsync();\n if (cancelled || !isMountedRef.current) return;\n recorder.record();\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n onError?.(msg);\n onCancel();\n }\n };\n void start();\n return () => {\n cancelled = true;\n };\n // Mount-only effect: recorder/start refs are stable for the life of the panel.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n useEffect(\n () => () => {\n isMountedRef.current = false;\n /*\n * Unmount cleanup: fire-and-forget. We don't await anything here\n * because React doesn't allow async destructors and we can't\n * guarantee the recorder shared object outlives this tick. The\n * stopAndCleanup util is null-safe.\n */\n void stopAndCleanup();\n },\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [],\n );\n\n /**\n * Max-duration safety net. `RecorderState.durationMillis` already reports\n * elapsed time in milliseconds (see `expo-audio` `Audio.types.ts`), which\n * lines up directly with our `MAX_DURATION_MS` ceiling (web parity).\n */\n useEffect(() => {\n if (!recorderState.isRecording) return;\n const elapsedMs = recorderState.durationMillis ?? 0;\n if (elapsedMs >= MAX_DURATION_MS && !sendInFlightRef.current) {\n void finalize();\n }\n }, [recorderState.isRecording, recorderState.durationMillis, finalize]);\n\n const handleCancelPress = useCallback(async () => {\n if (isTranscribing) return;\n await stopAndCleanup();\n onCancel();\n }, [isTranscribing, stopAndCleanup, onCancel]);\n\n const handleSendPress = useCallback(() => {\n void finalize();\n }, [finalize]);\n\n const formatted = useMemo(() => {\n const totalSeconds = Math.floor((recorderState.durationMillis ?? 0) / 1000);\n const m = Math.floor(totalSeconds / 60);\n const s = totalSeconds % 60;\n return `${m}:${s.toString().padStart(2, '0')}`;\n }, [recorderState.durationMillis]);\n\n const bg = isDark ? '#0f172a' : '#ffffff';\n const border = isDark ? 'rgba(148,163,184,0.18)' : '#e4e4e7';\n const fg = isDark ? '#e5e7eb' : '#18181b';\n const muted = isDark ? '#94a3b8' : '#71717a';\n const cancelBg = isDark ? 'rgba(148,163,184,0.08)' : '#f4f4f5';\n\n const showSpinner = isTranscribing;\n const isRecording = recorderState.isRecording && !showSpinner;\n const statusLabel = showSpinner ? 'Transcribing…' : isRecording ? formatted : 'Initializing…';\n\n return (\n <View\n accessibilityRole=\"summary\"\n accessibilityLabel=\"Voice recorder\"\n style={[styles.container, { backgroundColor: bg, borderColor: border }]}\n >\n <View style={styles.statusRow}>\n {showSpinner ? (\n <ActivityIndicator size=\"small\" color={fg} />\n ) : (\n <Animated.View style={[styles.recDot, { opacity: isRecording ? pulse : 0.35 }]} />\n )}\n <Animated.Text style={[styles.timer, { color: fg }]} accessibilityLiveRegion=\"polite\">\n {statusLabel}\n </Animated.Text>\n {isRecording && <Animated.Text style={[styles.maxLabel, { color: muted }]}>/ 3:00</Animated.Text>}\n </View>\n <View style={styles.actionsRow}>\n <Pressable\n onPress={handleCancelPress}\n disabled={isTranscribing}\n accessibilityLabel=\"Cancel recording\"\n accessibilityRole=\"button\"\n style={({ pressed }) => [\n styles.iconBtn,\n {\n backgroundColor: cancelBg,\n borderColor: border,\n opacity: isTranscribing ? 0.4 : pressed ? 0.7 : 1,\n },\n ]}\n >\n <Ionicons name=\"close\" size={16} color={fg} />\n </Pressable>\n <Pressable\n onPress={handleSendPress}\n disabled={isTranscribing || !isRecording}\n accessibilityLabel=\"Stop and transcribe\"\n accessibilityRole=\"button\"\n style={({ pressed }) => [\n styles.sendBtn,\n {\n opacity: isTranscribing ? 0.7 : !isRecording ? 0.5 : pressed ? 0.85 : 1,\n },\n ]}\n >\n {isTranscribing ? (\n <ActivityIndicator size=\"small\" color=\"#ffffff\" />\n ) : (\n <Feather name=\"arrow-up\" size={16} color=\"#ffffff\" />\n )}\n </Pressable>\n </View>\n </View>\n );\n}\n\nconst styles = StyleSheet.create({\n container: {\n flexDirection: 'row',\n alignItems: 'center',\n justifyContent: 'space-between',\n borderRadius: 24,\n borderWidth: 1,\n paddingVertical: 10,\n paddingHorizontal: 14,\n gap: 12,\n shadowColor: '#0f172a',\n shadowOpacity: 0.08,\n shadowRadius: 12,\n shadowOffset: { width: 0, height: 4 },\n elevation: 4,\n },\n statusRow: {\n flexDirection: 'row',\n alignItems: 'center',\n gap: 8,\n flexShrink: 1,\n },\n recDot: {\n width: 8,\n height: 8,\n borderRadius: 4,\n backgroundColor: '#ef4444',\n },\n timer: {\n fontSize: 13,\n fontWeight: '600',\n fontVariant: ['tabular-nums'],\n },\n maxLabel: {\n fontSize: 11,\n },\n actionsRow: {\n flexDirection: 'row',\n alignItems: 'center',\n gap: 8,\n },\n iconBtn: {\n width: 32,\n height: 32,\n borderRadius: 16,\n borderWidth: 1,\n alignItems: 'center',\n justifyContent: 'center',\n },\n sendBtn: {\n width: 32,\n height: 32,\n borderRadius: 16,\n alignItems: 'center',\n justifyContent: 'center',\n backgroundColor: '#18181b',\n },\n});\n\nexport default AudioRecorderPanel;\n"],"names":[],"mappings":"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"}
@@ -11,7 +11,7 @@ const ChatWidgetCard = memo(function ChatWidgetCard2({
11
11
  }
12
12
  if (widget.component === TEMPLATES_WIDGET) {
13
13
  const opening = parseTemplatesOpening(widget.attachment);
14
- return opening ? /* @__PURE__ */ jsx(TemplatesWidgetCard, { opening, isDark }) : null;
14
+ return opening ? /* @__PURE__ */ jsx(TemplatesWidgetCard, { opening, link: widget.link, isDark }) : null;
15
15
  }
16
16
  if (widget.component === CANVAS_DESIGN_WIDGET) {
17
17
  const design = toCanvasDesignAttachment(widget.attachment);
@@ -1 +1 @@
1
- {"version":3,"file":"ChatWidgetCard.js","sources":["../../../src/features/chat/ChatWidgetCard.tsx"],"sourcesContent":["import React, { memo } from 'react';\nimport { Linking } from 'react-native';\n// The flight-cards entry, not the package root: this renders inside the chat\n// that surface remotes bundle, and the root (layout, redux, client barrel)\n// would ship as a private copy in each of them.\nimport {\n FlightCardRow,\n isFlightAttachment,\n} from '@admin-layout/gluestack-ui-mobile/lib/components/FlightCards/index.js';\nimport type { FlightOfferSetLike } from '@admin-layout/gluestack-ui-mobile/lib/components/FlightCards/index.js';\nimport { CANVAS_DESIGN_WIDGET, TEMPLATES_WIDGET, toCanvasDesignAttachment, parseTemplatesOpening } from 'common';\nimport type { ChatWidget } from './chatWidgets';\nimport { CanvasDesignWidgetCard, TemplatesWidgetCard } from './TemplatesChatCards';\n\nconst openOffer = (url: string) => {\n void Linking.openURL(url);\n};\n\n/**\n * The native card for a widget envelope in the main chat. Flights render as\n * the shared gluestack flight cards (the web chat renders the same results\n * with ai-chat-ext's tailwind cards, from the same model); templates and a\n * design made from one have their own cards (TemplatesChatCards). A widget with no\n * native card yet renders nothing: its envelope is still kept out of the\n * text, so the thread never shows raw JSON.\n */\nexport const ChatWidgetCard = memo(function ChatWidgetCard({\n widget,\n isDark,\n}: {\n widget: ChatWidget;\n isDark: boolean;\n}) {\n const attachment = widget.attachment as FlightOfferSetLike & { title?: string; totalCount?: number };\n if (widget.component === 'travel' && isFlightAttachment(attachment)) {\n return <FlightCardRow attachment={attachment} onOpenOffer={openOffer} scheme={isDark ? 'dark' : 'light'} />;\n }\n if (widget.component === TEMPLATES_WIDGET) {\n const opening = parseTemplatesOpening(widget.attachment);\n return opening ? <TemplatesWidgetCard opening={opening} isDark={isDark} /> : null;\n }\n if (widget.component === CANVAS_DESIGN_WIDGET) {\n const design = toCanvasDesignAttachment(widget.attachment);\n return design ? <CanvasDesignWidgetCard design={design} isDark={isDark} /> : null;\n }\n return null;\n});\n"],"names":["ChatWidgetCard"],"mappings":"gZAUA,MAAM,SAAA,GAAY,CAAC,GAAgB,KAAA;AACjC,EAAK,KAAA,OAAA,CAAQ,QAAQ,GAAG,CAAA;AAC1B,CAAA;AAUa,MAAA,cAAA,GAAiB,IAAK,CAAA,SAASA,eAAe,CAAA;AAAA,EACzD,MAAA;AAAA,EACA;AACF,CAGG,EAAA;AACD,EAAA,MAAM,aAAa,MAAO,CAAA,UAAA;AAI1B,EAAA,IAAI,MAAO,CAAA,SAAA,KAAc,QAAY,IAAA,kBAAA,CAAmB,UAAU,CAAG,EAAA;AACnE,IAAO,uBAAA,GAAA,CAAC,iBAAc,UAAwB,EAAA,WAAA,EAAa,WAAW,MAAQ,EAAA,MAAA,GAAS,SAAS,OAAS,EAAA,CAAA;AAAA;AAE3G,EAAI,IAAA,MAAA,CAAO,cAAc,gBAAkB,EAAA;AACzC,IAAM,MAAA,OAAA,GAAU,qBAAsB,CAAA,MAAA,CAAO,UAAU,CAAA;AACvD,IAAA,OAAO,OAAU,mBAAA,GAAA,CAAC,mBAAoB,EAAA,EAAA,OAAA,EAAkB,QAAgB,CAAK,GAAA,IAAA;AAAA;AAE/E,EAAI,IAAA,MAAA,CAAO,cAAc,oBAAsB,EAAA;AAC7C,IAAM,MAAA,MAAA,GAAS,wBAAyB,CAAA,MAAA,CAAO,UAAU,CAAA;AACzD,IAAA,OAAO,MAAS,mBAAA,GAAA,CAAC,sBAAuB,EAAA,EAAA,MAAA,EAAgB,QAAgB,CAAK,GAAA,IAAA;AAAA;AAE/E,EAAO,OAAA,IAAA;AACT,CAAC"}
1
+ {"version":3,"file":"ChatWidgetCard.js","sources":["../../../src/features/chat/ChatWidgetCard.tsx"],"sourcesContent":["import React, { memo } from 'react';\nimport { Linking } from 'react-native';\n// The flight-cards entry, not the package root: this renders inside the chat\n// that surface remotes bundle, and the root (layout, redux, client barrel)\n// would ship as a private copy in each of them.\nimport {\n FlightCardRow,\n isFlightAttachment,\n} from '@admin-layout/gluestack-ui-mobile/lib/components/FlightCards/index.js';\nimport type { FlightOfferSetLike } from '@admin-layout/gluestack-ui-mobile/lib/components/FlightCards/index.js';\nimport { CANVAS_DESIGN_WIDGET, TEMPLATES_WIDGET, toCanvasDesignAttachment, parseTemplatesOpening } from 'common';\nimport type { ChatWidget } from './chatWidgets';\nimport { CanvasDesignWidgetCard, TemplatesWidgetCard } from './TemplatesChatCards';\n\nconst openOffer = (url: string) => {\n void Linking.openURL(url);\n};\n\n/**\n * The native card for a widget envelope in the main chat. Flights render as\n * the shared gluestack flight cards (the web chat renders the same results\n * with ai-chat-ext's tailwind cards, from the same model); templates and a\n * design made from one have their own cards (TemplatesChatCards), the templates\n * row opening the reply's own Templates link when it has one. A widget with no\n * native card yet renders nothing: its envelope is still kept out of the\n * text, so the thread never shows raw JSON.\n */\nexport const ChatWidgetCard = memo(function ChatWidgetCard({\n widget,\n isDark,\n}: {\n widget: ChatWidget;\n isDark: boolean;\n}) {\n const attachment = widget.attachment as FlightOfferSetLike & { title?: string; totalCount?: number };\n if (widget.component === 'travel' && isFlightAttachment(attachment)) {\n return <FlightCardRow attachment={attachment} onOpenOffer={openOffer} scheme={isDark ? 'dark' : 'light'} />;\n }\n if (widget.component === TEMPLATES_WIDGET) {\n const opening = parseTemplatesOpening(widget.attachment);\n return opening ? <TemplatesWidgetCard opening={opening} link={widget.link} isDark={isDark} /> : null;\n }\n if (widget.component === CANVAS_DESIGN_WIDGET) {\n const design = toCanvasDesignAttachment(widget.attachment);\n return design ? <CanvasDesignWidgetCard design={design} isDark={isDark} /> : null;\n }\n return null;\n});\n"],"names":["ChatWidgetCard"],"mappings":"gZAUA,MAAM,SAAA,GAAY,CAAC,GAAgB,KAAA;AACjC,EAAK,KAAA,OAAA,CAAQ,QAAQ,GAAG,CAAA;AAC1B,CAAA;AAWa,MAAA,cAAA,GAAiB,IAAK,CAAA,SAASA,eAAe,CAAA;AAAA,EACzD,MAAA;AAAA,EACA;AACF,CAGG,EAAA;AACD,EAAA,MAAM,aAAa,MAAO,CAAA,UAAA;AAI1B,EAAA,IAAI,MAAO,CAAA,SAAA,KAAc,QAAY,IAAA,kBAAA,CAAmB,UAAU,CAAG,EAAA;AACnE,IAAO,uBAAA,GAAA,CAAC,iBAAc,UAAwB,EAAA,WAAA,EAAa,WAAW,MAAQ,EAAA,MAAA,GAAS,SAAS,OAAS,EAAA,CAAA;AAAA;AAE3G,EAAI,IAAA,MAAA,CAAO,cAAc,gBAAkB,EAAA;AACzC,IAAM,MAAA,OAAA,GAAU,qBAAsB,CAAA,MAAA,CAAO,UAAU,CAAA;AACvD,IAAO,OAAA,OAAA,uBAAW,mBAAoB,EAAA,EAAA,OAAA,EAAkB,MAAM,MAAO,CAAA,IAAA,EAAM,QAAgB,CAAK,GAAA,IAAA;AAAA;AAElG,EAAI,IAAA,MAAA,CAAO,cAAc,oBAAsB,EAAA;AAC7C,IAAM,MAAA,MAAA,GAAS,wBAAyB,CAAA,MAAA,CAAO,UAAU,CAAA;AACzD,IAAA,OAAO,MAAS,mBAAA,GAAA,CAAC,sBAAuB,EAAA,EAAA,MAAA,EAAgB,QAAgB,CAAK,GAAA,IAAA;AAAA;AAE/E,EAAO,OAAA,IAAA;AACT,CAAC"}
@@ -1,10 +1,23 @@
1
- import {jsx,jsxs}from'react/jsx-runtime';import {memo,useMemo,useCallback,useState}from'react';import {Pressable,StyleSheet,View,Text,ScrollView,Image}from'react-native';import {MaterialCommunityIcons}from'@expo/vector-icons';import {useNavigation}from'@react-navigation/native';import {templatesOpeningTitle,canvasDesignKindLabel,templatesSurfaceUrl}from'common';import {config}from'../../config/env-config.js';import {openSurfaceInApp}from'../links/openInApp.js';const ACCENT = "#2563eb";
1
+ import {jsx,jsxs}from'react/jsx-runtime';import {memo,useMemo,useCallback,useState}from'react';import {Pressable,StyleSheet,View,Text,ScrollView,Image}from'react-native';import {MaterialCommunityIcons}from'@expo/vector-icons';import {useNavigation}from'@react-navigation/native';import {templatesOpeningTitle,canvasDesignKindLabel,isTemplatesSurfaceUrl,templatesSurfaceLinkWithOpening,templatesSurfaceUrl}from'common';import {config}from'../../config/env-config.js';import {useChatLinkOpener}from'../links/chatLinkOpener.js';import {openSurfaceInApp}from'../links/openInApp.js';const ACCENT = "#2563eb";
2
2
  const TEMPLATES_SURFACE_SLUG = "templates";
3
3
  function templatesSurfaceLink(surfaceUrlTemplate, opening) {
4
4
  const template = surfaceUrlTemplate == null ? void 0 : surfaceUrlTemplate.trim();
5
5
  if (!template || !template.includes("{slug}")) return null;
6
6
  return templatesSurfaceUrl(template.replace(/\{slug\}/g, TEMPLATES_SURFACE_SLUG), opening);
7
7
  }
8
+ function templatesRowTarget(surfaceUrlTemplate, opening, replyLink) {
9
+ if (replyLink && isTemplatesSurfaceUrl(replyLink)) {
10
+ return {
11
+ kind: "reply",
12
+ url: templatesSurfaceLinkWithOpening(replyLink.trim(), opening)
13
+ };
14
+ }
15
+ const url = templatesSurfaceLink(surfaceUrlTemplate, opening);
16
+ return url ? {
17
+ kind: "browse",
18
+ url
19
+ } : null;
20
+ }
8
21
  function palette(isDark) {
9
22
  return {
10
23
  card: isDark ? "#111827" : "#ffffff",
@@ -85,19 +98,23 @@ const styles = StyleSheet.create({
85
98
  });
86
99
  const TemplatesWidgetCard = memo(function TemplatesWidgetCard2({
87
100
  opening,
101
+ link,
88
102
  isDark
89
103
  }) {
90
104
  const navigation = useNavigation();
105
+ const openChatLink = useChatLinkOpener();
91
106
  const colors = palette(isDark);
92
- const url = useMemo(() => templatesSurfaceLink(config.SURFACE_URL_TEMPLATE, opening), [opening]);
107
+ const target = useMemo(() => templatesRowTarget(config.SURFACE_URL_TEMPLATE, opening, link), [opening, link]);
93
108
  const title = templatesOpeningTitle(opening);
94
109
  const open = useCallback(() => {
95
- if (url) openSurfaceInApp(navigation, {
110
+ if (!target) return;
111
+ if (target.kind === "reply") openChatLink(target.url);
112
+ else openSurfaceInApp(navigation, {
96
113
  surface: TEMPLATES_SURFACE_SLUG,
97
- url
114
+ url: target.url
98
115
  });
99
- }, [navigation, url]);
100
- if (!url) return null;
116
+ }, [navigation, openChatLink, target]);
117
+ if (!target) return null;
101
118
  return /* @__PURE__ */ jsx(Pressable, { onPress: open, accessibilityRole: "button", accessibilityLabel: `Browse ${title.charAt(0).toLowerCase()}${title.slice(1)}`, style: ({
102
119
  pressed
103
120
  }) => [styles.card, {
@@ -172,4 +189,4 @@ const CanvasDesignWidgetCard = memo(function CanvasDesignWidgetCard2({
172
189
  backgroundColor: colors.table
173
190
  }, children: design.previews.map((uri, i) => /* @__PURE__ */ jsx(PagePreview, { uri, label: pages > 1 ? `${title}, page ${i + 1} of ${pages}` : title, isDark }, `${i}:${uri}`)) }) : null
174
191
  ] });
175
- });export{CanvasDesignWidgetCard,TEMPLATES_SURFACE_SLUG,TemplatesWidgetCard,templatesSurfaceLink};//# sourceMappingURL=TemplatesChatCards.js.map
192
+ });export{CanvasDesignWidgetCard,TEMPLATES_SURFACE_SLUG,TemplatesWidgetCard,templatesRowTarget,templatesSurfaceLink};//# sourceMappingURL=TemplatesChatCards.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"TemplatesChatCards.js","sources":["../../../src/features/chat/TemplatesChatCards.tsx"],"sourcesContent":["/**\n * The phone's cards for templates in the chat (common TEMPLATES_CHAT_PROTOCOL), the twins of the\n * web's TemplatesMessage and CanvasDesignMessage:\n *\n * - `templates`: one row that opens the Templates surface on that category in the app's surface\n * screen (signed in with this device), where a tap on a template sends the pick back into the\n * chat, as from a texted card.\n * - `canvas-design`: the pages of a design the agent made from a template, side by side.\n */\nimport React, { memo, useCallback, useMemo, useState } from 'react';\nimport { Image, Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';\nimport { MaterialCommunityIcons } from '@expo/vector-icons';\nimport { useNavigation, type NavigationProp, type ParamListBase } from '@react-navigation/native';\nimport {\n canvasDesignKindLabel,\n templatesOpeningTitle,\n templatesSurfaceUrl,\n type CanvasDesignAttachment,\n type TemplatesOpening,\n} from 'common';\nimport { config } from '../../config/env-config';\nimport { openSurfaceInApp } from '../links/openInApp';\n\nconst ACCENT = '#2563eb';\n/** The Templates surface's slug: `templates-surface.<env>` through SURFACE_URL_TEMPLATE. */\nexport const TEMPLATES_SURFACE_SLUG = 'templates';\n\n/** The Templates surface page for an opening, or null when the template has no `{slug}` (it would open another surface). */\nexport function templatesSurfaceLink(\n surfaceUrlTemplate: string | undefined,\n opening: TemplatesOpening | null | undefined,\n): string | null {\n const template = surfaceUrlTemplate?.trim();\n if (!template || !template.includes('{slug}')) return null;\n return templatesSurfaceUrl(template.replace(/\\{slug\\}/g, TEMPLATES_SURFACE_SLUG), opening);\n}\n\nfunction palette(isDark: boolean) {\n return {\n card: isDark ? '#111827' : '#ffffff',\n border: isDark ? '#1f2937' : '#e5e7eb',\n title: isDark ? '#f1f5f9' : '#111827',\n muted: isDark ? '#94a3b8' : '#6b7280',\n iconBg: isDark ? '#1e293b' : '#eff6ff',\n icon: isDark ? '#60a5fa' : ACCENT,\n table: isDark ? '#0b1220' : '#f9fafb',\n pressed: isDark ? '#1f2937' : '#f3f4f6',\n };\n}\n\nconst styles = StyleSheet.create({\n card: { width: '100%', borderWidth: 1, borderRadius: 16, marginTop: 4, marginBottom: 8, overflow: 'hidden' },\n row: { flexDirection: 'row', alignItems: 'center', gap: 12, padding: 14 },\n icon: { width: 40, height: 40, borderRadius: 10, alignItems: 'center', justifyContent: 'center' },\n body: { flex: 1, minWidth: 0 },\n title: { fontSize: 15, fontWeight: '700', lineHeight: 20 },\n subtitle: { fontSize: 13, lineHeight: 18, marginTop: 2 },\n header: { flexDirection: 'row', alignItems: 'center', gap: 8, paddingHorizontal: 14, paddingVertical: 10 },\n badge: { borderRadius: 6, paddingHorizontal: 6, paddingVertical: 2 },\n badgeText: { fontSize: 11, fontWeight: '600' },\n pages: { paddingHorizontal: 12, paddingVertical: 12, gap: 10 },\n page: { width: 200, height: 150, borderRadius: 8 },\n count: { fontSize: 11 },\n});\n\n/** A `templates` widget: one row that opens the Templates surface on its category. */\nexport const TemplatesWidgetCard = memo(function TemplatesWidgetCard({\n opening,\n isDark,\n}: {\n opening: TemplatesOpening;\n isDark: boolean;\n}) {\n const navigation = useNavigation<NavigationProp<ParamListBase>>();\n const colors = palette(isDark);\n const url = useMemo(() => templatesSurfaceLink(config.SURFACE_URL_TEMPLATE, opening), [opening]);\n const title = templatesOpeningTitle(opening);\n const open = useCallback(() => {\n if (url) openSurfaceInApp(navigation, { surface: TEMPLATES_SURFACE_SLUG, url });\n }, [navigation, url]);\n if (!url) return null;\n return (\n <Pressable\n onPress={open}\n accessibilityRole=\"button\"\n accessibilityLabel={`Browse ${title.charAt(0).toLowerCase()}${title.slice(1)}`}\n style={({ pressed }) => [\n styles.card,\n { borderColor: colors.border, backgroundColor: pressed ? colors.pressed : colors.card },\n ]}\n >\n <View style={styles.row}>\n <View style={[styles.icon, { backgroundColor: colors.iconBg }]}>\n <MaterialCommunityIcons name=\"view-grid-outline\" size={22} color={colors.icon} />\n </View>\n <View style={styles.body}>\n <Text style={[styles.title, { color: colors.title }]} numberOfLines={1}>\n Browse {title.charAt(0).toLowerCase() + title.slice(1)}\n </Text>\n <Text style={[styles.subtitle, { color: colors.muted }]} numberOfLines={1}>\n {opening.query ? `Matching “${opening.query}”` : 'Pick one to start from it'}\n </Text>\n </View>\n <MaterialCommunityIcons name=\"chevron-right\" size={20} color={colors.muted} />\n </View>\n </Pressable>\n );\n});\n\n/** One page; a render that will not load leaves a quiet tile, never a broken image. */\nfunction PagePreview({ uri, label, isDark }: { uri: string; label: string; isDark: boolean }) {\n const [failed, setFailed] = useState(false);\n const colors = palette(isDark);\n if (failed) {\n return (\n <View\n accessible\n accessibilityLabel={`${label} (preview unavailable)`}\n style={[\n styles.page,\n { backgroundColor: colors.border, alignItems: 'center', justifyContent: 'center' },\n ]}\n >\n <MaterialCommunityIcons name=\"image-off-outline\" size={22} color={colors.muted} />\n </View>\n );\n }\n return (\n <Image\n source={{ uri }}\n accessibilityLabel={label}\n resizeMode=\"contain\"\n onError={() => setFailed(true)}\n style={styles.page}\n />\n );\n}\n\n/** A `canvas-design` widget: what was made and its pages, side by side. */\nexport const CanvasDesignWidgetCard = memo(function CanvasDesignWidgetCard({\n design,\n isDark,\n}: {\n design: CanvasDesignAttachment;\n isDark: boolean;\n}) {\n const colors = palette(isDark);\n const kind = canvasDesignKindLabel(design);\n const title = design.title ?? `Your ${kind.toLowerCase()}`;\n const pages = design.previews.length;\n return (\n <View\n accessibilityLabel={`${kind}: ${title}`}\n style={[styles.card, { borderColor: colors.border, backgroundColor: colors.card }]}\n >\n <View style={styles.header}>\n <View style={[styles.badge, { backgroundColor: colors.iconBg }]}>\n <Text style={[styles.badgeText, { color: colors.icon }]}>{kind}</Text>\n </View>\n <Text style={[styles.title, styles.body, { color: colors.title }]} numberOfLines={1}>\n {title}\n </Text>\n {pages > 1 ? <Text style={[styles.count, { color: colors.muted }]}>{pages} pages</Text> : null}\n </View>\n {pages > 0 ? (\n <ScrollView\n horizontal\n showsHorizontalScrollIndicator={false}\n contentContainerStyle={styles.pages}\n style={{ backgroundColor: colors.table }}\n >\n {design.previews.map((uri, i) => (\n <PagePreview\n key={`${i}:${uri}`}\n uri={uri}\n label={pages > 1 ? `${title}, page ${i + 1} of ${pages}` : title}\n isDark={isDark}\n />\n ))}\n </ScrollView>\n ) : null}\n </View>\n );\n});\n"],"names":["TemplatesWidgetCard","CanvasDesignWidgetCard"],"mappings":"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"}
1
+ {"version":3,"file":"TemplatesChatCards.js","sources":["../../../src/features/chat/TemplatesChatCards.tsx"],"sourcesContent":["/**\n * The phone's cards for templates in the chat (common TEMPLATES_CHAT_PROTOCOL), the twins of the\n * web's TemplatesMessage and CanvasDesignMessage:\n *\n * - `templates`: one row that opens the Templates surface on that category in the app's surface\n * screen (signed in with this device). Where it opens is templatesRowTarget: the reply's own\n * Templates link when it has one, so a pick there can come back into this chat, else the\n * surface of this app's environment, to browse.\n * - `canvas-design`: the pages of a design the agent made from a template, side by side.\n */\nimport React, { memo, useCallback, useMemo, useState } from 'react';\nimport { Image, Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';\nimport { MaterialCommunityIcons } from '@expo/vector-icons';\nimport { useNavigation, type NavigationProp, type ParamListBase } from '@react-navigation/native';\nimport {\n canvasDesignKindLabel,\n isTemplatesSurfaceUrl,\n templatesOpeningTitle,\n templatesSurfaceLinkWithOpening,\n templatesSurfaceUrl,\n type CanvasDesignAttachment,\n type TemplatesOpening,\n} from 'common';\nimport { config } from '../../config/env-config';\nimport { useChatLinkOpener } from '../links/chatLinkOpener';\nimport { openSurfaceInApp } from '../links/openInApp';\n\nconst ACCENT = '#2563eb';\n/** The Templates surface's slug: `templates-surface.<env>` through SURFACE_URL_TEMPLATE. */\nexport const TEMPLATES_SURFACE_SLUG = 'templates';\n\n/** The Templates surface page for an opening, or null when the template has no `{slug}` (it would open another surface). */\nexport function templatesSurfaceLink(\n surfaceUrlTemplate: string | undefined,\n opening: TemplatesOpening | null | undefined,\n): string | null {\n const template = surfaceUrlTemplate?.trim();\n if (!template || !template.includes('{slug}')) return null;\n return templatesSurfaceUrl(template.replace(/\\{slug\\}/g, TEMPLATES_SURFACE_SLUG), opening);\n}\n\n/**\n * Where a templates row opens.\n *\n * - `reply`: the reply's own Templates link - the one the messenger gateway ended the reply with\n * (chatWidgets takes it out of the text for the row), carrying the reply's snapshot (`ys`) - with\n * the row's category and search added. It opens as that link would have from the text: the\n * gateway says it is a surface page and the app opens it in its surface screen, signed in. The\n * Templates page reads the snapshot and, when this chat may take a pick, offers \"Make it here in\n * the chat\", which sends the pick back here as the person's next turn.\n * - `browse`: a reply with no such link (older replies, or the agent asked for no surface). The\n * Templates surface of this app's own environment, on the category: browsing and \"Edit on the\n * web\" work, but with no snapshot the page has no chat to send a pick to.\n *\n * Null when neither exists (no surface template configured).\n */\nexport type TemplatesRowTarget = { kind: 'reply' | 'browse'; url: string };\n\nexport function templatesRowTarget(\n surfaceUrlTemplate: string | undefined,\n opening: TemplatesOpening | null | undefined,\n replyLink?: string | null,\n): TemplatesRowTarget | null {\n if (replyLink && isTemplatesSurfaceUrl(replyLink)) {\n return { kind: 'reply', url: templatesSurfaceLinkWithOpening(replyLink.trim(), opening) };\n }\n const url = templatesSurfaceLink(surfaceUrlTemplate, opening);\n return url ? { kind: 'browse', url } : null;\n}\n\nfunction palette(isDark: boolean) {\n return {\n card: isDark ? '#111827' : '#ffffff',\n border: isDark ? '#1f2937' : '#e5e7eb',\n title: isDark ? '#f1f5f9' : '#111827',\n muted: isDark ? '#94a3b8' : '#6b7280',\n iconBg: isDark ? '#1e293b' : '#eff6ff',\n icon: isDark ? '#60a5fa' : ACCENT,\n table: isDark ? '#0b1220' : '#f9fafb',\n pressed: isDark ? '#1f2937' : '#f3f4f6',\n };\n}\n\nconst styles = StyleSheet.create({\n card: { width: '100%', borderWidth: 1, borderRadius: 16, marginTop: 4, marginBottom: 8, overflow: 'hidden' },\n row: { flexDirection: 'row', alignItems: 'center', gap: 12, padding: 14 },\n icon: { width: 40, height: 40, borderRadius: 10, alignItems: 'center', justifyContent: 'center' },\n body: { flex: 1, minWidth: 0 },\n title: { fontSize: 15, fontWeight: '700', lineHeight: 20 },\n subtitle: { fontSize: 13, lineHeight: 18, marginTop: 2 },\n header: { flexDirection: 'row', alignItems: 'center', gap: 8, paddingHorizontal: 14, paddingVertical: 10 },\n badge: { borderRadius: 6, paddingHorizontal: 6, paddingVertical: 2 },\n badgeText: { fontSize: 11, fontWeight: '600' },\n pages: { paddingHorizontal: 12, paddingVertical: 12, gap: 10 },\n page: { width: 200, height: 150, borderRadius: 8 },\n count: { fontSize: 11 },\n});\n\n/** A `templates` widget: one row that opens the Templates surface on its category. */\nexport const TemplatesWidgetCard = memo(function TemplatesWidgetCard({\n opening,\n link,\n isDark,\n}: {\n opening: TemplatesOpening;\n /** The reply's Templates surface link (with its snapshot), when the reply carried one. */\n link?: string;\n isDark: boolean;\n}) {\n const navigation = useNavigation<NavigationProp<ParamListBase>>();\n const openChatLink = useChatLinkOpener();\n const colors = palette(isDark);\n const target = useMemo(() => templatesRowTarget(config.SURFACE_URL_TEMPLATE, opening, link), [opening, link]);\n const title = templatesOpeningTitle(opening);\n const open = useCallback(() => {\n if (!target) return;\n // The reply's link opens the way a tapped link does (the gateway names its surface);\n // the browse page is this app's own surface, from its own config.\n if (target.kind === 'reply') openChatLink(target.url);\n else openSurfaceInApp(navigation, { surface: TEMPLATES_SURFACE_SLUG, url: target.url });\n }, [navigation, openChatLink, target]);\n if (!target) return null;\n return (\n <Pressable\n onPress={open}\n accessibilityRole=\"button\"\n accessibilityLabel={`Browse ${title.charAt(0).toLowerCase()}${title.slice(1)}`}\n style={({ pressed }) => [\n styles.card,\n { borderColor: colors.border, backgroundColor: pressed ? colors.pressed : colors.card },\n ]}\n >\n <View style={styles.row}>\n <View style={[styles.icon, { backgroundColor: colors.iconBg }]}>\n <MaterialCommunityIcons name=\"view-grid-outline\" size={22} color={colors.icon} />\n </View>\n <View style={styles.body}>\n <Text style={[styles.title, { color: colors.title }]} numberOfLines={1}>\n Browse {title.charAt(0).toLowerCase() + title.slice(1)}\n </Text>\n <Text style={[styles.subtitle, { color: colors.muted }]} numberOfLines={1}>\n {opening.query ? `Matching “${opening.query}”` : 'Pick one to start from it'}\n </Text>\n </View>\n <MaterialCommunityIcons name=\"chevron-right\" size={20} color={colors.muted} />\n </View>\n </Pressable>\n );\n});\n\n/** One page; a render that will not load leaves a quiet tile, never a broken image. */\nfunction PagePreview({ uri, label, isDark }: { uri: string; label: string; isDark: boolean }) {\n const [failed, setFailed] = useState(false);\n const colors = palette(isDark);\n if (failed) {\n return (\n <View\n accessible\n accessibilityLabel={`${label} (preview unavailable)`}\n style={[\n styles.page,\n { backgroundColor: colors.border, alignItems: 'center', justifyContent: 'center' },\n ]}\n >\n <MaterialCommunityIcons name=\"image-off-outline\" size={22} color={colors.muted} />\n </View>\n );\n }\n return (\n <Image\n source={{ uri }}\n accessibilityLabel={label}\n resizeMode=\"contain\"\n onError={() => setFailed(true)}\n style={styles.page}\n />\n );\n}\n\n/** A `canvas-design` widget: what was made and its pages, side by side. */\nexport const CanvasDesignWidgetCard = memo(function CanvasDesignWidgetCard({\n design,\n isDark,\n}: {\n design: CanvasDesignAttachment;\n isDark: boolean;\n}) {\n const colors = palette(isDark);\n const kind = canvasDesignKindLabel(design);\n const title = design.title ?? `Your ${kind.toLowerCase()}`;\n const pages = design.previews.length;\n return (\n <View\n accessibilityLabel={`${kind}: ${title}`}\n style={[styles.card, { borderColor: colors.border, backgroundColor: colors.card }]}\n >\n <View style={styles.header}>\n <View style={[styles.badge, { backgroundColor: colors.iconBg }]}>\n <Text style={[styles.badgeText, { color: colors.icon }]}>{kind}</Text>\n </View>\n <Text style={[styles.title, styles.body, { color: colors.title }]} numberOfLines={1}>\n {title}\n </Text>\n {pages > 1 ? <Text style={[styles.count, { color: colors.muted }]}>{pages} pages</Text> : null}\n </View>\n {pages > 0 ? (\n <ScrollView\n horizontal\n showsHorizontalScrollIndicator={false}\n contentContainerStyle={styles.pages}\n style={{ backgroundColor: colors.table }}\n >\n {design.previews.map((uri, i) => (\n <PagePreview\n key={`${i}:${uri}`}\n uri={uri}\n label={pages > 1 ? `${title}, page ${i + 1} of ${pages}` : title}\n isDark={isDark}\n />\n ))}\n </ScrollView>\n ) : null}\n </View>\n );\n});\n"],"names":["TemplatesWidgetCard","CanvasDesignWidgetCard"],"mappings":"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"}
@@ -1,4 +1,6 @@
1
- import {TEMPLATES_WIDGET,parseTemplatesOpening,CANVAS_DESIGN_WIDGET,canvasDesignFromWidget}from'common';var __defProp = Object.defineProperty;
1
+ import {TEMPLATES_WIDGET,splitTemplatesSurfaceLink,parseTemplatesOpening,CANVAS_DESIGN_WIDGET,canvasDesignFromWidget}from'common';var __defProp = Object.defineProperty;
2
+ var __defProps = Object.defineProperties;
3
+ var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
2
4
  var __getOwnPropSymbols = Object.getOwnPropertySymbols;
3
5
  var __hasOwnProp = Object.prototype.hasOwnProperty;
4
6
  var __propIsEnum = Object.prototype.propertyIsEnumerable;
@@ -14,6 +16,7 @@ var __spreadValues = (a, b) => {
14
16
  }
15
17
  return a;
16
18
  };
19
+ var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
17
20
  const WIDGET_FENCE = /```(?:yantra:widget|json)[^\S\n]*\n?([\s\S]*?)```/gi;
18
21
  const OPEN_WIDGET_TAIL = /```(?:yantra:widget|json)[^\S\n]*\n?\s*\{\s*"propsComponent"[\s\S]*$/i;
19
22
  const OPEN_YANTRA_WIDGET_TAIL = /```yantra:widget[\s\S]*$/i;
@@ -61,6 +64,19 @@ function splitChatWidgets(content, streaming = false) {
61
64
  if (streaming) {
62
65
  text = text.replace(OPEN_WIDGET_TAIL, "").replace(OPEN_YANTRA_WIDGET_TAIL, "");
63
66
  }
67
+ const templates = widgets.map((w) => w.component).lastIndexOf(TEMPLATES_WIDGET);
68
+ if (templates >= 0) {
69
+ const {
70
+ text: rest,
71
+ url
72
+ } = splitTemplatesSurfaceLink(text);
73
+ if (url) {
74
+ text = rest;
75
+ widgets[templates] = __spreadProps(__spreadValues({}, widgets[templates]), {
76
+ link: url
77
+ });
78
+ }
79
+ }
64
80
  return {
65
81
  text: text.replace(/\n{3,}/g, "\n\n").trim(),
66
82
  widgets