@colixsystems/widget-sdk 0.89.0 → 0.91.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/README.md +19 -2
- package/dist/contract.cjs +76 -7
- package/dist/contract.js +76 -7
- package/dist/hooks.js +228 -0
- package/dist/index.d.ts +88 -7
- package/dist/index.js +2 -0
- package/dist/index.native.js +2 -0
- package/dist/linter.cjs +6 -5
- package/dist/linter.js +6 -5
- package/dist/manifest.cjs +6 -5
- package/dist/manifest.js +6 -5
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -35,6 +35,7 @@ The data layer lives in **four separate domain-client packages**, each instantia
|
|
|
35
35
|
| **CORE** | `useClipboard()` | `{ copy, paste, hasContent }` | platform clipboard (web `navigator.clipboard` / native `expo-clipboard`); rejects with `ClipboardError` — no scope |
|
|
36
36
|
| **CORE** | `useToast()` | `{ showToast }` | `ctx.toast.showToast` — wired by the Player and the Expo export; an authoring preview omits it and the call is a no-op — no scope |
|
|
37
37
|
| **CORE** | `useGeolocation(options?)` | `{ latitude, longitude, accuracy, loading, error, getCurrentPosition }` | `ctx.device.geolocation` — no scope. Capture is IMPERATIVE: call `getCurrentPosition()` from a user gesture (a tap), never on mount. Resolves to `{ latitude, longitude, accuracy }`; rejects with `GeolocationError` (`.code` in `PERMISSION_DENIED \| UNAVAILABLE \| TIMEOUT \| UNSUPPORTED \| INTERNAL`). Identical on web (`navigator.geolocation`) and the Expo export (`expo-location`). |
|
|
38
|
+
| **CORE** | `useSpeechToText(options?)` | `{ transcript, partial, listening, supported, error, start, stop, abort, reset }` | `ctx.device.speech` — no scope. Dictation with the device's **on-device** recogniser: no audio is uploaded and no AI credit is spent. Capture is IMPERATIVE: call `start()` from a user gesture (a tap), never on mount. `transcript` accumulates finalised speech, `partial` holds the uncommitted guess (needs `options.interimResults`); `stop()` keeps it, `abort()` discards it. Rejects with `SpeechToTextError` (`.code` in `PERMISSION_DENIED \| NO_SPEECH \| LANGUAGE_UNSUPPORTED \| NETWORK \| ABORTED \| UNSUPPORTED \| INTERNAL`). **Gate your mic button on `supported`** — Firefox ships no `SpeechRecognition`. Identical on web (`SpeechRecognition`) and the Expo export (`expo-speech-recognition`). |
|
|
38
39
|
| **CORE** | `useI18n()` | `{ t, locale }` | `ctx.i18n` — no scope. `t(key)` resolves the widget-namespaced key (`widget.<id>.<key>`, declared in `manifest.translations`) first, then a **predefined shared key** (`shared.<key>`) when `key` is one of the standard strings (`submit`, `cancel`, `save`, `loading`, …), then the raw key. Use a shared key for an identical default string so it translates once and any per-instance `widget.<id>.<key>` override still wins. |
|
|
39
40
|
| **CORE** | `useTranslate()` | `{ translate, translating, error, language, available }` | `ctx.i18n.translate` — no scope. Machine-translates **user-generated content** (record text, file names, API payloads) into the app user's language; `useI18n().t()` is still the answer for your own copy. `translate(str)` → `Promise<string>`, `translate(str[])` → `Promise<string[]>` in ONE request. Target defaults to the app user's language. Cached per session, per pod, and durably per workspace, so repeat text is free. Limits: 50 segments / 5 000 chars each / 20 000 total. Rejects with `TranslateError`; `available` is false where the host cannot translate. |
|
|
40
41
|
| **DATASTORE** (`ctx.datastore`) | `useDatastoreQuery(table, options?)` | `{ data, loading, error, refetch }` | `records(table).list` (unwraps `{ data, meta }` to `data: []`) — `datastore.read:*` |
|
|
@@ -61,7 +62,19 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
|
|
|
61
62
|
|
|
62
63
|
## Status
|
|
63
64
|
|
|
64
|
-
`v0.
|
|
65
|
+
`v0.91.0` — pre-publish. The package surface (types, function names, export paths) is the v1 contract; runtime behaviour for some hooks is stubbed (each hook documents what's wired and what isn't). It is **not yet published to npm**.
|
|
66
|
+
|
|
67
|
+
### What's new in 0.91.0 (contract 1.64.0)
|
|
68
|
+
|
|
69
|
+
**New `useSpeechToText()` hook — dictate into text with the device's on-device recogniser.** A new CORE hook reading a new `speech` capability on the existing `ctx.device` slice. Returns `{ transcript, partial, listening, supported, error, start, stop, abort, reset }`. Capture is **imperative** — call `start()` from a user gesture (a `Pressable.onPress`); the browser and the mobile OS gate the microphone prompt on a gesture, so it NEVER listens on mount. `transcript` accumulates finalised speech across utterances; `partial` holds the guess the recogniser has not committed yet (empty unless `options.interimResults`). `stop()` finalises and keeps what was heard, `abort()` discards the current utterance, `reset()` clears both. `options` (`{ lang, continuous, interimResults }`) pass through to the host. Rejections surface as a structured `SpeechToTextError` (new named export) with a stable `.code` (`PERMISSION_DENIED` / `NO_SPEECH` / `LANGUAGE_UNSUPPORTED` / `NETWORK` / `ABORTED` / `UNSUPPORTED` / `INTERNAL`). It needs **no manifest scope** and **no `requestedScopes` entry**.
|
|
70
|
+
|
|
71
|
+
Recognition runs **on device**: no audio is uploaded, nothing reaches our servers, and no AI credit is spent — so the hook is available to every workspace regardless of its AI data-residency policy. The web Player brokers it via the browser's `SpeechRecognition`; the Expo export via `expo-speech-recognition`, whose config plugin declares the microphone and speech permissions the runtime needs. Both hosts emit the same Web Speech error vocabulary, so one mapping serves both.
|
|
72
|
+
|
|
73
|
+
**Always gate your mic affordance on `supported`.** Firefox ships no `SpeechRecognition` at all, so `supported` is `false` there and `start()` rejects with `UNSUPPORTED` — render the plain text field instead of a dead button. The `speech` capability is optional and forwarded independently of `geolocation`, so a host that brokers one still brokers the other.
|
|
74
|
+
|
|
75
|
+
Additive — one new hook, one new optional device capability, one new error class; no existing export changed signature.
|
|
76
|
+
|
|
77
|
+
`v0.90.0` — pre-publish. The package surface (types, function names, export paths) is the v1 contract; runtime behaviour for some hooks is stubbed (each hook documents what's wired and what isn't). It is **not yet published to npm**.
|
|
65
78
|
|
|
66
79
|
### What's new in 0.89.0 (contract unchanged)
|
|
67
80
|
|
|
@@ -172,6 +185,10 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
|
|
|
172
185
|
- **The colour maths ignores alpha, on purpose.** `hexChannels` reads the R/G/B pair and skips any alpha, so contrast, readable text and the derived accent tints reason about the opaque colour. None of them can composite without knowing the backdrop, which a token table does not have — so transparency lives in the VALUE your widget renders, not in the decision about whether that colour reads as light or dark.
|
|
173
186
|
- **`CONTRACT.version` → `1.61.0`** (additive: `themeTokens.spacingScale` + `widgetStyles` and their bounds, `themeComponents.card.universalFields`, and the `normaliseWidgetStyles` / `deriveSurfaceTokens` host exports). No author-facing export changed signature, and a theme that sets none of it resolves exactly as before.
|
|
174
187
|
|
|
188
|
+
### What's new in 0.90.0 (contract 1.63.0)
|
|
189
|
+
|
|
190
|
+
**A manifest action declares `triggerTypes` — a set — and the script learns which one fired (sc-4915).** An action could carry exactly one trigger, so a widget that needed the same work done on create *and* on delete had to ship the script twice: two `actions` entries, two operator bindings, two run histories, and the usual drift between the copies. `triggerTypes` replaces `triggerType`: a non-empty array of unique values from `schedule`, `record_created`, `record_updated`, `record_deleted`, freely combined (`scheduleCron` is required iff the array contains `schedule`). The pre-0.90.0 scalar `triggerType` is still read and normalised into the array, so a widget already published against it keeps validating and nothing needs republishing. What makes the combination useful is the other half: the script's `triggerType` global now names the trigger that **actually fired this run** — including `"manual"` (an operator's Run now) and `"app"` (a button press) — instead of echoing the row's configuration, so one script can branch on whether its record was created or deleted. `CONTRACT.version` → `1.63.0`. Additive for every existing manifest.
|
|
191
|
+
|
|
175
192
|
### What's new in 0.85.1 (contract 1.60.1)
|
|
176
193
|
|
|
177
194
|
**`useWidgetEvent(name)` returns the emitter FUNCTION — the declared contract said otherwise (sc-4753).** `CONTRACT.hooks`'s entry for the hook declared `returnShape: { emit }`, so every surface derived from it — chiefly the Widget Builder Agent's hooks table — told authors the hook resolves to an object. It never did: `useWidgetEvent("slotChosen")` hands back the callable you invoke directly (`emitSlot({ courtId })`), exactly as the typings and the Developer guide have always documented. A widget written against the declared shape destructured a function, got `undefined`, and threw the moment a user interacted — a cross-widget wire that rendered perfectly and only failed on click. The declaration is now a bare callable and the publish-time render harness models the same shape, so a wrong destructure is caught instead of waved through. `CONTRACT.version` → `1.60.1`. Documentation-only correction: no export, signature, or runtime behaviour changed — a widget already calling the result is unaffected.
|
|
@@ -619,7 +636,7 @@ Two additive features land in this version.
|
|
|
619
636
|
|
|
620
637
|
**A widget may declare server-side actions in its manifest.**
|
|
621
638
|
|
|
622
|
-
- **`WidgetManifest.actions` is now part of the public contract.** An optional array; each entry is `{ key, name, description?,
|
|
639
|
+
- **`WidgetManifest.actions` is now part of the public contract.** An optional array; each entry is `{ key, name, description?, triggerTypes, scheduleCron?, timeoutMs?, scriptSource }`. `triggerTypes` is a non-empty array of unique values from `schedule`, `record_created`, `record_updated`, `record_deleted` — combine them so one script serves several events; `scheduleCron` is required when it contains `schedule`. The `scriptSource` (≤ 200 KiB) runs in the **shared isolated-vm action runner** — against `datastore` / `fetch` / `connectors` / `console` / `record` / `tenantId` (the runner surface, **not** the React/SDK widget surface, so SDK imports and hooks are unavailable there and the component linter does not scan it). `connectors.call(slug, { method, path, query, body, headers })` resolves a tenant-configured REST connector by slug and returns `{ status, headers, body }` (auth + SSRF handled by the platform; an unknown slug / SSRF / timeout throws a catchable Error). Actions never run in the rendered app, so they have **no effect on Player ↔ export parity**.
|
|
623
640
|
- **Operators enable actions per tenant** from the Properties Panel. An enabled action materialises a tenant `Action` row **DISABLED** until the operator binds an integration API key (and, for `record_*` triggers, a target table) in the Actions admin page — those bindings are tenant-local, so `triggerTableId` / `apiKeyId` must **not** appear in the manifest (the validator and linter reject them).
|
|
624
641
|
- **New contract fields** `CONTRACT.actionTriggerTypes`, `CONTRACT.actionScriptGlobals`, `CONTRACT.actionScriptMaxBytes` expose the grammar so the Developer page, the AI agent prompt, and `validateManifest` derive it from one source. `validateManifest` now structurally validates `actions`; the marketplace linter rejects malformed / oversized declarations.
|
|
625
642
|
- **New manifest category `ADMINISTRATION`** for app-administration widgets such as User Management. Added to `CONTRACT.manifestCategories`, `validateManifest`, the `WidgetCategory` type, the marketplace category list, and the master-DB `WidgetCategory` enum.
|
package/dist/contract.cjs
CHANGED
|
@@ -1194,6 +1194,35 @@ const HOOKS = [
|
|
|
1194
1194
|
requiredContextSlice: [],
|
|
1195
1195
|
scopes: null,
|
|
1196
1196
|
},
|
|
1197
|
+
// Host-brokered on-device speech recognition. Optional slice; the hook
|
|
1198
|
+
// reports supported:false rather than throwing at render.
|
|
1199
|
+
{
|
|
1200
|
+
name: "useSpeechToText",
|
|
1201
|
+
signature: "useSpeechToText(options?)",
|
|
1202
|
+
description:
|
|
1203
|
+
"Dictate into text with the device's ON-DEVICE speech recogniser. Returns { transcript, partial, listening, supported, " +
|
|
1204
|
+
"error, start, stop, abort, reset }. Capture is IMPERATIVE — call start() from a user gesture (a tap on a mic button); " +
|
|
1205
|
+
"the browser and the mobile OS gate the microphone prompt on a gesture, so it NEVER listens on mount. `transcript` " +
|
|
1206
|
+
"accumulates finalised speech; `partial` holds the uncommitted guess (empty unless options.interimResults). stop() " +
|
|
1207
|
+
"finalises and keeps what was heard, abort() discards it, reset() clears both. Recognition runs ON DEVICE — no audio is " +
|
|
1208
|
+
"uploaded and no AI credit is spent. start() rejects with a SpeechToTextError whose .code is one of PERMISSION_DENIED | " +
|
|
1209
|
+
"NO_SPEECH | LANGUAGE_UNSUPPORTED | NETWORK | ABORTED | UNSUPPORTED | INTERNAL. options: { lang, continuous, " +
|
|
1210
|
+
"interimResults }. Check `supported` before rendering a mic button — a browser without SpeechRecognition (Firefox) " +
|
|
1211
|
+
"reports false. Identical on web (SpeechRecognition) and the Expo export (expo-speech-recognition).",
|
|
1212
|
+
returnShape: {
|
|
1213
|
+
transcript: "string // finalised speech, accumulated",
|
|
1214
|
+
partial: "string // uncommitted guess; '' unless interimResults",
|
|
1215
|
+
listening: "boolean",
|
|
1216
|
+
supported: "boolean // false when the host brokers no recogniser",
|
|
1217
|
+
error: "SpeechToTextError | null",
|
|
1218
|
+
start: "() => Promise<void> // rejects with SpeechToTextError",
|
|
1219
|
+
stop: "() => Promise<void> // finalise, keep the transcript",
|
|
1220
|
+
abort: "() => void // cancel, discard the utterance",
|
|
1221
|
+
reset: "() => void // clear transcript + partial + error",
|
|
1222
|
+
},
|
|
1223
|
+
requiredContextSlice: [],
|
|
1224
|
+
scopes: null,
|
|
1225
|
+
},
|
|
1197
1226
|
];
|
|
1198
1227
|
|
|
1199
1228
|
// REQ-WSDK-RN-WEB: the SDK exposes the React Native primitive API
|
|
@@ -1373,6 +1402,34 @@ const ACTION_SCRIPT_GLOBALS = [
|
|
|
1373
1402
|
// Mirrors action.service.js SCRIPT_MAX_BYTES.
|
|
1374
1403
|
const ACTION_SCRIPT_MAX_BYTES = 200 * 1024;
|
|
1375
1404
|
|
|
1405
|
+
/**
|
|
1406
|
+
* sc-4915 — the trigger set a manifest action declares, canonically ordered.
|
|
1407
|
+
*
|
|
1408
|
+
* `triggerTypes` is the current shape; the pre-sc-4915 scalar `triggerType` is
|
|
1409
|
+
* still read so a widget published before the change keeps validating. Returns
|
|
1410
|
+
* null when the declaration is empty, holds an unknown value, or repeats one —
|
|
1411
|
+
* every caller reports that as a manifest error. ONE reader, so the SDK
|
|
1412
|
+
* validator, the CLI linter and the backend cannot disagree about what a
|
|
1413
|
+
* manifest declared.
|
|
1414
|
+
*/
|
|
1415
|
+
function normaliseActionTriggerTypes(action) {
|
|
1416
|
+
const a = action !== null && typeof action === "object" ? action : {};
|
|
1417
|
+
const raw = Array.isArray(a.triggerTypes)
|
|
1418
|
+
? a.triggerTypes
|
|
1419
|
+
: a.triggerTypes === undefined && a.triggerType !== undefined
|
|
1420
|
+
? [a.triggerType]
|
|
1421
|
+
: null;
|
|
1422
|
+
if (raw === null || raw.length === 0) return null;
|
|
1423
|
+
const seen = new Set();
|
|
1424
|
+
for (const t of raw) {
|
|
1425
|
+
if (typeof t !== "string" || !ACTION_TRIGGER_TYPES.includes(t)) return null;
|
|
1426
|
+
if (seen.has(t)) return null;
|
|
1427
|
+
seen.add(t);
|
|
1428
|
+
}
|
|
1429
|
+
return ACTION_TRIGGER_TYPES.filter((t) => seen.has(t));
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
|
|
1376
1433
|
// Reverse-DNS-ish manifest id, e.g. "com.acme.charts.barchart". Two or
|
|
1377
1434
|
// more labels, lowercase alnum + hyphen, label starts with a letter. The
|
|
1378
1435
|
// analyzer + the SDK validator both read this from the contract so a
|
|
@@ -1501,9 +1558,9 @@ const MANIFEST_SCHEMA = {
|
|
|
1501
1558
|
type: "object[]",
|
|
1502
1559
|
required: false,
|
|
1503
1560
|
description:
|
|
1504
|
-
"Optional. Server-side actions the widget declares. Each runs in the shared isolated-vm action runner (cron- or record-triggered) — NEVER in the rendered app. Operators enable them per tenant from the Properties Panel; the action materialises DISABLED until they bind an integration API key (and, for record_* triggers, a target table) in the Actions admin page. Each entry: { key (stable, unique within the manifest), name, description?,
|
|
1561
|
+
"Optional. Server-side actions the widget declares. Each runs in the shared isolated-vm action runner (cron- or record-triggered) — NEVER in the rendered app. Operators enable them per tenant from the Properties Panel; the action materialises DISABLED until they bind an integration API key (and, for record_* triggers, a target table) in the Actions admin page. Each entry: { key (stable, unique within the manifest), name, description?, triggerTypes (a non-empty array of unique values from " +
|
|
1505
1562
|
ACTION_TRIGGER_TYPES.join(", ") +
|
|
1506
|
-
"), scheduleCron? (required iff
|
|
1563
|
+
"; combine them so one script serves several events — the script's `triggerType` global names the event that actually fired), scheduleCron? (required iff triggerTypes contains 'schedule'; node-cron syntax), timeoutMs? (100–300000), scriptSource (≤200 KiB; runs against " +
|
|
1507
1564
|
ACTION_SCRIPT_GLOBALS.join(", ") +
|
|
1508
1565
|
" — NOT the React surface, so SDK imports/hooks are unavailable) }. Do NOT include triggerTableId or apiKeyId — those are tenant-local and bound after install.",
|
|
1509
1566
|
default: [],
|
|
@@ -1741,11 +1798,15 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
1741
1798
|
device: {
|
|
1742
1799
|
description:
|
|
1743
1800
|
"Optional host-brokered device capabilities. " +
|
|
1744
|
-
"{ geolocation: { getCurrentPosition(options?) -> Promise<{ latitude, longitude, accuracy }> }
|
|
1745
|
-
"
|
|
1746
|
-
"
|
|
1801
|
+
"{ geolocation: { getCurrentPosition(options?) -> Promise<{ latitude, longitude, accuracy }> }, " +
|
|
1802
|
+
"speech: { isSupported() -> boolean, start(options, { onResult, onError, onEnd }) -> Promise<{ stop(), abort() }> } }. " +
|
|
1803
|
+
"Backs useGeolocation() and useSpeechToText(). The web Player brokers them via navigator.geolocation and " +
|
|
1804
|
+
"window.SpeechRecognition; the Expo export via expo-location and expo-speech-recognition. " +
|
|
1805
|
+
"getCurrentPosition rejects with a GeolocationError (.code PERMISSION_DENIED | UNAVAILABLE | TIMEOUT | UNSUPPORTED | INTERNAL). " +
|
|
1806
|
+
"speech.start streams { transcript, isFinal } to onResult and runs ON DEVICE — it uploads no audio and spends no AI credit; " +
|
|
1807
|
+
"its onError carries the Web Speech error vocabulary (not-allowed | no-speech | language-not-supported | network | aborted).",
|
|
1747
1808
|
required: false,
|
|
1748
|
-
fields: { geolocation: "object" },
|
|
1809
|
+
fields: { geolocation: "object", speech: "object" },
|
|
1749
1810
|
},
|
|
1750
1811
|
};
|
|
1751
1812
|
|
|
@@ -2787,7 +2848,14 @@ const CONTRACT = deepFreeze({
|
|
|
2787
2848
|
// Naming one widget is strictly more specific than restyling a scope, and
|
|
2788
2849
|
// the Properties Panel stays the final word. Additive throughout: a theme
|
|
2789
2850
|
// that sets none of it resolves exactly as before.
|
|
2790
|
-
|
|
2851
|
+
// 1.63.0: additive (sc-4915) — a manifest action declares `triggerTypes`, a
|
|
2852
|
+
// non-empty ARRAY, so one script can serve a create, an update and a
|
|
2853
|
+
// delete instead of being copied into three actions. The pre-sc-4915
|
|
2854
|
+
// scalar `triggerType` is still read and normalised, so an already
|
|
2855
|
+
// published widget keeps validating. The script's `triggerType` global
|
|
2856
|
+
// now names the trigger that actually FIRED the run ('manual' and 'app'
|
|
2857
|
+
// included), which is what makes a multi-trigger script able to branch.
|
|
2858
|
+
version: "1.64.0",
|
|
2791
2859
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
2792
2860
|
hooks: HOOKS,
|
|
2793
2861
|
primitives: PRIMITIVES,
|
|
@@ -3073,6 +3141,7 @@ module.exports = {
|
|
|
3073
3141
|
CONTRACT,
|
|
3074
3142
|
clampSpacingScale,
|
|
3075
3143
|
scaleSpacing,
|
|
3144
|
+
normaliseActionTriggerTypes,
|
|
3076
3145
|
isHookAllowed,
|
|
3077
3146
|
requiredContextKeys,
|
|
3078
3147
|
isHexColor,
|
package/dist/contract.js
CHANGED
|
@@ -1194,6 +1194,35 @@ const HOOKS = [
|
|
|
1194
1194
|
requiredContextSlice: [],
|
|
1195
1195
|
scopes: null,
|
|
1196
1196
|
},
|
|
1197
|
+
// Host-brokered on-device speech recognition. Optional slice; the hook
|
|
1198
|
+
// reports supported:false rather than throwing at render.
|
|
1199
|
+
{
|
|
1200
|
+
name: "useSpeechToText",
|
|
1201
|
+
signature: "useSpeechToText(options?)",
|
|
1202
|
+
description:
|
|
1203
|
+
"Dictate into text with the device's ON-DEVICE speech recogniser. Returns { transcript, partial, listening, supported, " +
|
|
1204
|
+
"error, start, stop, abort, reset }. Capture is IMPERATIVE — call start() from a user gesture (a tap on a mic button); " +
|
|
1205
|
+
"the browser and the mobile OS gate the microphone prompt on a gesture, so it NEVER listens on mount. `transcript` " +
|
|
1206
|
+
"accumulates finalised speech; `partial` holds the uncommitted guess (empty unless options.interimResults). stop() " +
|
|
1207
|
+
"finalises and keeps what was heard, abort() discards it, reset() clears both. Recognition runs ON DEVICE — no audio is " +
|
|
1208
|
+
"uploaded and no AI credit is spent. start() rejects with a SpeechToTextError whose .code is one of PERMISSION_DENIED | " +
|
|
1209
|
+
"NO_SPEECH | LANGUAGE_UNSUPPORTED | NETWORK | ABORTED | UNSUPPORTED | INTERNAL. options: { lang, continuous, " +
|
|
1210
|
+
"interimResults }. Check `supported` before rendering a mic button — a browser without SpeechRecognition (Firefox) " +
|
|
1211
|
+
"reports false. Identical on web (SpeechRecognition) and the Expo export (expo-speech-recognition).",
|
|
1212
|
+
returnShape: {
|
|
1213
|
+
transcript: "string // finalised speech, accumulated",
|
|
1214
|
+
partial: "string // uncommitted guess; '' unless interimResults",
|
|
1215
|
+
listening: "boolean",
|
|
1216
|
+
supported: "boolean // false when the host brokers no recogniser",
|
|
1217
|
+
error: "SpeechToTextError | null",
|
|
1218
|
+
start: "() => Promise<void> // rejects with SpeechToTextError",
|
|
1219
|
+
stop: "() => Promise<void> // finalise, keep the transcript",
|
|
1220
|
+
abort: "() => void // cancel, discard the utterance",
|
|
1221
|
+
reset: "() => void // clear transcript + partial + error",
|
|
1222
|
+
},
|
|
1223
|
+
requiredContextSlice: [],
|
|
1224
|
+
scopes: null,
|
|
1225
|
+
},
|
|
1197
1226
|
];
|
|
1198
1227
|
|
|
1199
1228
|
// REQ-WSDK-RN-WEB: the SDK exposes the React Native primitive API
|
|
@@ -1373,6 +1402,34 @@ const ACTION_SCRIPT_GLOBALS = [
|
|
|
1373
1402
|
// Mirrors action.service.js SCRIPT_MAX_BYTES.
|
|
1374
1403
|
const ACTION_SCRIPT_MAX_BYTES = 200 * 1024;
|
|
1375
1404
|
|
|
1405
|
+
/**
|
|
1406
|
+
* sc-4915 — the trigger set a manifest action declares, canonically ordered.
|
|
1407
|
+
*
|
|
1408
|
+
* `triggerTypes` is the current shape; the pre-sc-4915 scalar `triggerType` is
|
|
1409
|
+
* still read so a widget published before the change keeps validating. Returns
|
|
1410
|
+
* null when the declaration is empty, holds an unknown value, or repeats one —
|
|
1411
|
+
* every caller reports that as a manifest error. ONE reader, so the SDK
|
|
1412
|
+
* validator, the CLI linter and the backend cannot disagree about what a
|
|
1413
|
+
* manifest declared.
|
|
1414
|
+
*/
|
|
1415
|
+
function normaliseActionTriggerTypes(action) {
|
|
1416
|
+
const a = action !== null && typeof action === "object" ? action : {};
|
|
1417
|
+
const raw = Array.isArray(a.triggerTypes)
|
|
1418
|
+
? a.triggerTypes
|
|
1419
|
+
: a.triggerTypes === undefined && a.triggerType !== undefined
|
|
1420
|
+
? [a.triggerType]
|
|
1421
|
+
: null;
|
|
1422
|
+
if (raw === null || raw.length === 0) return null;
|
|
1423
|
+
const seen = new Set();
|
|
1424
|
+
for (const t of raw) {
|
|
1425
|
+
if (typeof t !== "string" || !ACTION_TRIGGER_TYPES.includes(t)) return null;
|
|
1426
|
+
if (seen.has(t)) return null;
|
|
1427
|
+
seen.add(t);
|
|
1428
|
+
}
|
|
1429
|
+
return ACTION_TRIGGER_TYPES.filter((t) => seen.has(t));
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
|
|
1376
1433
|
// Reverse-DNS-ish manifest id, e.g. "com.acme.charts.barchart". Two or
|
|
1377
1434
|
// more labels, lowercase alnum + hyphen, label starts with a letter. The
|
|
1378
1435
|
// analyzer + the SDK validator both read this from the contract so a
|
|
@@ -1501,9 +1558,9 @@ const MANIFEST_SCHEMA = {
|
|
|
1501
1558
|
type: "object[]",
|
|
1502
1559
|
required: false,
|
|
1503
1560
|
description:
|
|
1504
|
-
"Optional. Server-side actions the widget declares. Each runs in the shared isolated-vm action runner (cron- or record-triggered) — NEVER in the rendered app. Operators enable them per tenant from the Properties Panel; the action materialises DISABLED until they bind an integration API key (and, for record_* triggers, a target table) in the Actions admin page. Each entry: { key (stable, unique within the manifest), name, description?,
|
|
1561
|
+
"Optional. Server-side actions the widget declares. Each runs in the shared isolated-vm action runner (cron- or record-triggered) — NEVER in the rendered app. Operators enable them per tenant from the Properties Panel; the action materialises DISABLED until they bind an integration API key (and, for record_* triggers, a target table) in the Actions admin page. Each entry: { key (stable, unique within the manifest), name, description?, triggerTypes (a non-empty array of unique values from " +
|
|
1505
1562
|
ACTION_TRIGGER_TYPES.join(", ") +
|
|
1506
|
-
"), scheduleCron? (required iff
|
|
1563
|
+
"; combine them so one script serves several events — the script's `triggerType` global names the event that actually fired), scheduleCron? (required iff triggerTypes contains 'schedule'; node-cron syntax), timeoutMs? (100–300000), scriptSource (≤200 KiB; runs against " +
|
|
1507
1564
|
ACTION_SCRIPT_GLOBALS.join(", ") +
|
|
1508
1565
|
" — NOT the React surface, so SDK imports/hooks are unavailable) }. Do NOT include triggerTableId or apiKeyId — those are tenant-local and bound after install.",
|
|
1509
1566
|
default: [],
|
|
@@ -1741,11 +1798,15 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
1741
1798
|
device: {
|
|
1742
1799
|
description:
|
|
1743
1800
|
"Optional host-brokered device capabilities. " +
|
|
1744
|
-
"{ geolocation: { getCurrentPosition(options?) -> Promise<{ latitude, longitude, accuracy }> }
|
|
1745
|
-
"
|
|
1746
|
-
"
|
|
1801
|
+
"{ geolocation: { getCurrentPosition(options?) -> Promise<{ latitude, longitude, accuracy }> }, " +
|
|
1802
|
+
"speech: { isSupported() -> boolean, start(options, { onResult, onError, onEnd }) -> Promise<{ stop(), abort() }> } }. " +
|
|
1803
|
+
"Backs useGeolocation() and useSpeechToText(). The web Player brokers them via navigator.geolocation and " +
|
|
1804
|
+
"window.SpeechRecognition; the Expo export via expo-location and expo-speech-recognition. " +
|
|
1805
|
+
"getCurrentPosition rejects with a GeolocationError (.code PERMISSION_DENIED | UNAVAILABLE | TIMEOUT | UNSUPPORTED | INTERNAL). " +
|
|
1806
|
+
"speech.start streams { transcript, isFinal } to onResult and runs ON DEVICE — it uploads no audio and spends no AI credit; " +
|
|
1807
|
+
"its onError carries the Web Speech error vocabulary (not-allowed | no-speech | language-not-supported | network | aborted).",
|
|
1747
1808
|
required: false,
|
|
1748
|
-
fields: { geolocation: "object" },
|
|
1809
|
+
fields: { geolocation: "object", speech: "object" },
|
|
1749
1810
|
},
|
|
1750
1811
|
};
|
|
1751
1812
|
|
|
@@ -2787,7 +2848,14 @@ const CONTRACT = deepFreeze({
|
|
|
2787
2848
|
// Naming one widget is strictly more specific than restyling a scope, and
|
|
2788
2849
|
// the Properties Panel stays the final word. Additive throughout: a theme
|
|
2789
2850
|
// that sets none of it resolves exactly as before.
|
|
2790
|
-
|
|
2851
|
+
// 1.63.0: additive (sc-4915) — a manifest action declares `triggerTypes`, a
|
|
2852
|
+
// non-empty ARRAY, so one script can serve a create, an update and a
|
|
2853
|
+
// delete instead of being copied into three actions. The pre-sc-4915
|
|
2854
|
+
// scalar `triggerType` is still read and normalised, so an already
|
|
2855
|
+
// published widget keeps validating. The script's `triggerType` global
|
|
2856
|
+
// now names the trigger that actually FIRED the run ('manual' and 'app'
|
|
2857
|
+
// included), which is what makes a multi-trigger script able to branch.
|
|
2858
|
+
version: "1.64.0",
|
|
2791
2859
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
2792
2860
|
hooks: HOOKS,
|
|
2793
2861
|
primitives: PRIMITIVES,
|
|
@@ -3073,6 +3141,7 @@ export {
|
|
|
3073
3141
|
CONTRACT,
|
|
3074
3142
|
clampSpacingScale,
|
|
3075
3143
|
scaleSpacing,
|
|
3144
|
+
normaliseActionTriggerTypes,
|
|
3076
3145
|
isHookAllowed,
|
|
3077
3146
|
requiredContextKeys,
|
|
3078
3147
|
isHexColor,
|
package/dist/hooks.js
CHANGED
|
@@ -916,6 +916,234 @@ export function useGeolocation(options) {
|
|
|
916
916
|
};
|
|
917
917
|
}
|
|
918
918
|
|
|
919
|
+
/**
|
|
920
|
+
* Error surfaced by `useSpeechToText()` (thrown by `start()` and stored in the
|
|
921
|
+
* hook's `error` slot). Carries a stable `code` so widgets branch on the error
|
|
922
|
+
* class without parsing message strings.
|
|
923
|
+
*
|
|
924
|
+
* `code` is one of:
|
|
925
|
+
* - "PERMISSION_DENIED" — the user or OS refused microphone access.
|
|
926
|
+
* - "NO_SPEECH" — capture ended without any speech detected.
|
|
927
|
+
* - "LANGUAGE_UNSUPPORTED" — no recogniser exists for the requested language.
|
|
928
|
+
* - "NETWORK" — the recogniser needed the network and failed.
|
|
929
|
+
* - "ABORTED" — capture was cancelled (abort(), or a new start()).
|
|
930
|
+
* - "UNSUPPORTED" — this host does not broker speech recognition.
|
|
931
|
+
* - "INTERNAL" — anything else.
|
|
932
|
+
*/
|
|
933
|
+
export class SpeechToTextError extends Error {
|
|
934
|
+
constructor(code, message, opts) {
|
|
935
|
+
super(message);
|
|
936
|
+
this.name = "SpeechToTextError";
|
|
937
|
+
this.code = code;
|
|
938
|
+
if (opts && opts.cause) this.cause = opts.cause;
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
// Both hosts emit the Web Speech API's error vocabulary (the Expo export's
|
|
943
|
+
// recogniser mirrors it deliberately), so one map serves web and native.
|
|
944
|
+
const SPEECH_ERROR_CODES = {
|
|
945
|
+
"not-allowed": "PERMISSION_DENIED",
|
|
946
|
+
"service-not-allowed": "PERMISSION_DENIED",
|
|
947
|
+
PERMISSION_DENIED: "PERMISSION_DENIED",
|
|
948
|
+
"no-speech": "NO_SPEECH",
|
|
949
|
+
NO_SPEECH: "NO_SPEECH",
|
|
950
|
+
// Android's recogniser reports a silence timeout instead of "no-speech".
|
|
951
|
+
"speech-timeout": "NO_SPEECH",
|
|
952
|
+
"language-not-supported": "LANGUAGE_UNSUPPORTED",
|
|
953
|
+
LANGUAGE_UNSUPPORTED: "LANGUAGE_UNSUPPORTED",
|
|
954
|
+
network: "NETWORK",
|
|
955
|
+
NETWORK: "NETWORK",
|
|
956
|
+
aborted: "ABORTED",
|
|
957
|
+
ABORTED: "ABORTED",
|
|
958
|
+
UNSUPPORTED: "UNSUPPORTED",
|
|
959
|
+
};
|
|
960
|
+
|
|
961
|
+
/** Coerce a thrown/emitted value into a SpeechToTextError with a stable code. */
|
|
962
|
+
function toSpeechToTextError(err) {
|
|
963
|
+
if (err instanceof SpeechToTextError) return err;
|
|
964
|
+
const raw =
|
|
965
|
+
err && err.code !== undefined && err.code !== null ? err.code : null;
|
|
966
|
+
const code = (raw !== null && SPEECH_ERROR_CODES[raw]) || "INTERNAL";
|
|
967
|
+
const message =
|
|
968
|
+
(err && typeof err.message === "string" && err.message) ||
|
|
969
|
+
"Speech recognition failed";
|
|
970
|
+
return new SpeechToTextError(code, message, { cause: err });
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
/**
|
|
974
|
+
* Dictate into text with the device's on-device speech recogniser. Returns
|
|
975
|
+
* `{ transcript, partial, listening, supported, error, start, stop, abort,
|
|
976
|
+
* reset }`.
|
|
977
|
+
*
|
|
978
|
+
* Capture is IMPERATIVE — call `start()` from a user gesture (a tap on a mic
|
|
979
|
+
* button). Both the browser and the mobile OS gate the microphone prompt on a
|
|
980
|
+
* user gesture, so the hook never listens on mount.
|
|
981
|
+
*
|
|
982
|
+
* `transcript` accumulates finalised speech; `partial` holds the in-flight
|
|
983
|
+
* guess the recogniser has not committed yet (empty unless
|
|
984
|
+
* `options.interimResults`). `stop()` finalises and keeps what was heard;
|
|
985
|
+
* `abort()` discards it. `reset()` clears both back to empty.
|
|
986
|
+
*
|
|
987
|
+
* Recognition runs ON DEVICE — no audio is uploaded and no AI credit is spent.
|
|
988
|
+
* The SAME hook drives both platforms: the web Player brokers it through the
|
|
989
|
+
* browser's SpeechRecognition, the Expo export through
|
|
990
|
+
* expo-speech-recognition.
|
|
991
|
+
*
|
|
992
|
+
* Safe-by-default: on a host that does not inject `ctx.device.speech` (or a
|
|
993
|
+
* browser without SpeechRecognition), `supported` is false and `start()`
|
|
994
|
+
* rejects with `code: "UNSUPPORTED"` rather than throwing at render, so a
|
|
995
|
+
* widget can call the hook unconditionally and hide its mic button.
|
|
996
|
+
*/
|
|
997
|
+
export function useSpeechToText(options) {
|
|
998
|
+
const ctx = useWidgetContextOrThrow("useSpeechToText");
|
|
999
|
+
const [transcript, setTranscript] = useState("");
|
|
1000
|
+
const [partial, setPartial] = useState("");
|
|
1001
|
+
const [listening, setListening] = useState(false);
|
|
1002
|
+
const [error, setError] = useState(null);
|
|
1003
|
+
|
|
1004
|
+
// `ctx` is a fresh identity every host render — hold the live client and
|
|
1005
|
+
// options in refs so the returned callbacks stay stable.
|
|
1006
|
+
const clientRef = useRef(ctx.device && ctx.device.speech);
|
|
1007
|
+
clientRef.current = ctx.device && ctx.device.speech;
|
|
1008
|
+
const optionsRef = useRef(options);
|
|
1009
|
+
optionsRef.current = options;
|
|
1010
|
+
// The live capture session, so stop/abort/unmount can reach it.
|
|
1011
|
+
const sessionRef = useRef(null);
|
|
1012
|
+
const runRef = useRef(0);
|
|
1013
|
+
|
|
1014
|
+
const supported = Boolean(
|
|
1015
|
+
clientRef.current &&
|
|
1016
|
+
typeof clientRef.current.start === "function" &&
|
|
1017
|
+
(typeof clientRef.current.isSupported !== "function" ||
|
|
1018
|
+
clientRef.current.isSupported()),
|
|
1019
|
+
);
|
|
1020
|
+
|
|
1021
|
+
// Abandoning a mounted recogniser leaves the mic hot on both hosts, so
|
|
1022
|
+
// release it when the widget unmounts.
|
|
1023
|
+
useEffect(
|
|
1024
|
+
() => () => {
|
|
1025
|
+
runRef.current += 1;
|
|
1026
|
+
const session = sessionRef.current;
|
|
1027
|
+
sessionRef.current = null;
|
|
1028
|
+
if (session && typeof session.abort === "function") {
|
|
1029
|
+
try {
|
|
1030
|
+
session.abort();
|
|
1031
|
+
} catch {
|
|
1032
|
+
/* the host is already tearing the session down */
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
},
|
|
1036
|
+
[],
|
|
1037
|
+
);
|
|
1038
|
+
|
|
1039
|
+
const reset = useCallback(() => {
|
|
1040
|
+
setTranscript("");
|
|
1041
|
+
setPartial("");
|
|
1042
|
+
setError(null);
|
|
1043
|
+
}, []);
|
|
1044
|
+
|
|
1045
|
+
const stop = useCallback(async () => {
|
|
1046
|
+
const session = sessionRef.current;
|
|
1047
|
+
sessionRef.current = null;
|
|
1048
|
+
setListening(false);
|
|
1049
|
+
setPartial("");
|
|
1050
|
+
if (session && typeof session.stop === "function") await session.stop();
|
|
1051
|
+
}, []);
|
|
1052
|
+
|
|
1053
|
+
const abort = useCallback(() => {
|
|
1054
|
+
runRef.current += 1;
|
|
1055
|
+
const session = sessionRef.current;
|
|
1056
|
+
sessionRef.current = null;
|
|
1057
|
+
setListening(false);
|
|
1058
|
+
setPartial("");
|
|
1059
|
+
if (session && typeof session.abort === "function") session.abort();
|
|
1060
|
+
}, []);
|
|
1061
|
+
|
|
1062
|
+
const start = useCallback(async () => {
|
|
1063
|
+
const client = clientRef.current;
|
|
1064
|
+
if (
|
|
1065
|
+
!client ||
|
|
1066
|
+
typeof client.start !== "function" ||
|
|
1067
|
+
(typeof client.isSupported === "function" && !client.isSupported())
|
|
1068
|
+
) {
|
|
1069
|
+
const e = new SpeechToTextError(
|
|
1070
|
+
"UNSUPPORTED",
|
|
1071
|
+
"This host does not provide speech recognition.",
|
|
1072
|
+
);
|
|
1073
|
+
setError(e);
|
|
1074
|
+
setListening(false);
|
|
1075
|
+
throw e;
|
|
1076
|
+
}
|
|
1077
|
+
// A second start() supersedes the first: drop the old session's events.
|
|
1078
|
+
const myRun = ++runRef.current;
|
|
1079
|
+
const previous = sessionRef.current;
|
|
1080
|
+
sessionRef.current = null;
|
|
1081
|
+
if (previous && typeof previous.abort === "function") previous.abort();
|
|
1082
|
+
|
|
1083
|
+
setError(null);
|
|
1084
|
+
setPartial("");
|
|
1085
|
+
setListening(true);
|
|
1086
|
+
try {
|
|
1087
|
+
const session = await client.start(optionsRef.current || {}, {
|
|
1088
|
+
onResult: (result) => {
|
|
1089
|
+
if (runRef.current !== myRun) return;
|
|
1090
|
+
const text =
|
|
1091
|
+
result && typeof result.transcript === "string"
|
|
1092
|
+
? result.transcript
|
|
1093
|
+
: "";
|
|
1094
|
+
if (result && result.isFinal) {
|
|
1095
|
+
setPartial("");
|
|
1096
|
+
// Append rather than replace: a continuous session emits one final
|
|
1097
|
+
// result per utterance, not a growing whole.
|
|
1098
|
+
setTranscript((prev) => (prev ? `${prev} ${text}`.trim() : text));
|
|
1099
|
+
} else {
|
|
1100
|
+
setPartial(text);
|
|
1101
|
+
}
|
|
1102
|
+
},
|
|
1103
|
+
onError: (err) => {
|
|
1104
|
+
if (runRef.current !== myRun) return;
|
|
1105
|
+
sessionRef.current = null;
|
|
1106
|
+
setError(toSpeechToTextError(err));
|
|
1107
|
+
setPartial("");
|
|
1108
|
+
setListening(false);
|
|
1109
|
+
},
|
|
1110
|
+
onEnd: () => {
|
|
1111
|
+
if (runRef.current !== myRun) return;
|
|
1112
|
+
sessionRef.current = null;
|
|
1113
|
+
setPartial("");
|
|
1114
|
+
setListening(false);
|
|
1115
|
+
},
|
|
1116
|
+
});
|
|
1117
|
+
if (runRef.current !== myRun) {
|
|
1118
|
+
// Superseded while the host was still opening the mic.
|
|
1119
|
+
if (session && typeof session.abort === "function") session.abort();
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
sessionRef.current = session || null;
|
|
1123
|
+
} catch (err) {
|
|
1124
|
+
const se = toSpeechToTextError(err);
|
|
1125
|
+
if (runRef.current === myRun) {
|
|
1126
|
+
sessionRef.current = null;
|
|
1127
|
+
setError(se);
|
|
1128
|
+
setListening(false);
|
|
1129
|
+
}
|
|
1130
|
+
throw se;
|
|
1131
|
+
}
|
|
1132
|
+
}, []);
|
|
1133
|
+
|
|
1134
|
+
return {
|
|
1135
|
+
transcript,
|
|
1136
|
+
partial,
|
|
1137
|
+
listening,
|
|
1138
|
+
supported,
|
|
1139
|
+
error,
|
|
1140
|
+
start,
|
|
1141
|
+
stop,
|
|
1142
|
+
abort,
|
|
1143
|
+
reset,
|
|
1144
|
+
};
|
|
1145
|
+
}
|
|
1146
|
+
|
|
919
1147
|
/* ============================================================================
|
|
920
1148
|
* DATASTORE CLIENT — ctx.datastore (@colixsystems/datastore-client)
|
|
921
1149
|
*
|
package/dist/index.d.ts
CHANGED
|
@@ -223,18 +223,23 @@ export interface WidgetManifestAction {
|
|
|
223
223
|
key: string;
|
|
224
224
|
name: string;
|
|
225
225
|
description?: string;
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
226
|
+
/**
|
|
227
|
+
* sc-4915 — a non-empty set of unique triggers. Combine them so one script
|
|
228
|
+
* serves several events; the script's `triggerType` global names the one
|
|
229
|
+
* that actually fired.
|
|
230
|
+
*/
|
|
231
|
+
triggerTypes: Array<
|
|
232
|
+
"schedule" | "record_created" | "record_updated" | "record_deleted"
|
|
233
|
+
>;
|
|
234
|
+
/** Required iff `triggerTypes` contains `"schedule"`. node-cron syntax. */
|
|
232
235
|
scheduleCron?: string;
|
|
233
236
|
/** 100–300000. Defaults to 30000 on materialise. */
|
|
234
237
|
timeoutMs?: number;
|
|
235
238
|
/**
|
|
236
239
|
* Runs against `datastore`, `fetch`, `console`, `record`, `tenantId`,
|
|
237
240
|
* `triggerType`, `triggerTableId` — NOT the React/SDK surface. ≤ 200 KiB.
|
|
241
|
+
* `triggerType` is the trigger that fired THIS run — one of the declared
|
|
242
|
+
* `triggerTypes`, or `"manual"` / `"app"` for an operator or button run.
|
|
238
243
|
*/
|
|
239
244
|
scriptSource: string;
|
|
240
245
|
}
|
|
@@ -585,8 +590,22 @@ export interface WidgetContext<TProps = unknown> {
|
|
|
585
590
|
toast?: {
|
|
586
591
|
showToast(args: { kind?: string; message: string }): void;
|
|
587
592
|
};
|
|
588
|
-
/**
|
|
593
|
+
/**
|
|
594
|
+
* Optional host-brokered device capabilities; backs useGeolocation and
|
|
595
|
+
* useSpeechToText.
|
|
596
|
+
*/
|
|
589
597
|
device?: {
|
|
598
|
+
speech?: {
|
|
599
|
+
isSupported?(): boolean;
|
|
600
|
+
start(
|
|
601
|
+
options: SpeechToTextOptions,
|
|
602
|
+
handlers: {
|
|
603
|
+
onResult(result: { transcript: string; isFinal: boolean }): void;
|
|
604
|
+
onError(error: unknown): void;
|
|
605
|
+
onEnd(): void;
|
|
606
|
+
},
|
|
607
|
+
): Promise<{ stop(): void | Promise<void>; abort(): void }>;
|
|
608
|
+
};
|
|
590
609
|
geolocation?: {
|
|
591
610
|
getCurrentPosition(options?: GeolocationOptions): Promise<{
|
|
592
611
|
latitude: number;
|
|
@@ -1262,6 +1281,68 @@ export class GeolocationError extends Error {
|
|
|
1262
1281
|
);
|
|
1263
1282
|
}
|
|
1264
1283
|
|
|
1284
|
+
/** Pass-through options for `useSpeechToText(...)`. */
|
|
1285
|
+
export interface SpeechToTextOptions {
|
|
1286
|
+
/** BCP-47 tag, e.g. "sv-SE". Defaults to the host's UI language. */
|
|
1287
|
+
lang?: string;
|
|
1288
|
+
/** Keep listening across pauses instead of stopping at the first result. */
|
|
1289
|
+
continuous?: boolean;
|
|
1290
|
+
/** Emit uncommitted guesses to `partial` while the user is still speaking. */
|
|
1291
|
+
interimResults?: boolean;
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
export interface SpeechToTextResult {
|
|
1295
|
+
/** Finalised speech, accumulated across utterances in this session. */
|
|
1296
|
+
transcript: string;
|
|
1297
|
+
/** The uncommitted guess; `""` unless `interimResults` was requested. */
|
|
1298
|
+
partial: string;
|
|
1299
|
+
listening: boolean;
|
|
1300
|
+
/** False when the host brokers no recogniser (e.g. Firefox). */
|
|
1301
|
+
supported: boolean;
|
|
1302
|
+
error: SpeechToTextError | null;
|
|
1303
|
+
/** Begin listening — call from a user gesture. Rejects with SpeechToTextError. */
|
|
1304
|
+
start(): Promise<void>;
|
|
1305
|
+
/** Stop listening and keep what was heard. */
|
|
1306
|
+
stop(): Promise<void>;
|
|
1307
|
+
/** Cancel listening and discard the current utterance. */
|
|
1308
|
+
abort(): void;
|
|
1309
|
+
/** Clear `transcript`, `partial`, and `error`. */
|
|
1310
|
+
reset(): void;
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
/**
|
|
1314
|
+
* Dictate into text with the device's ON-DEVICE speech recogniser. Capture is
|
|
1315
|
+
* imperative (call `start()` from a user gesture; it never listens on mount).
|
|
1316
|
+
* The same hook drives both platforms — the web Player brokers it via
|
|
1317
|
+
* `window.SpeechRecognition`, the Expo export via `expo-speech-recognition`.
|
|
1318
|
+
* No audio is uploaded and no AI credit is spent. Safe to call on a host that
|
|
1319
|
+
* brokers no recogniser: `supported` is then false and `start()` rejects with
|
|
1320
|
+
* `code: "UNSUPPORTED"`, so gate the mic button on `supported`.
|
|
1321
|
+
*/
|
|
1322
|
+
export function useSpeechToText(
|
|
1323
|
+
options?: SpeechToTextOptions,
|
|
1324
|
+
): SpeechToTextResult;
|
|
1325
|
+
|
|
1326
|
+
/**
|
|
1327
|
+
* Error surfaced by `useSpeechToText()` — thrown by `start()` and stored in the
|
|
1328
|
+
* hook's `error` slot. `code` is a stable categorisation.
|
|
1329
|
+
*/
|
|
1330
|
+
export class SpeechToTextError extends Error {
|
|
1331
|
+
code:
|
|
1332
|
+
| "PERMISSION_DENIED"
|
|
1333
|
+
| "NO_SPEECH"
|
|
1334
|
+
| "LANGUAGE_UNSUPPORTED"
|
|
1335
|
+
| "NETWORK"
|
|
1336
|
+
| "ABORTED"
|
|
1337
|
+
| "UNSUPPORTED"
|
|
1338
|
+
| "INTERNAL";
|
|
1339
|
+
constructor(
|
|
1340
|
+
code: SpeechToTextError["code"],
|
|
1341
|
+
message: string,
|
|
1342
|
+
opts?: { cause?: unknown },
|
|
1343
|
+
);
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1265
1346
|
/**
|
|
1266
1347
|
* Error class thrown by useDatastoreMutation callbacks (and surfaced by
|
|
1267
1348
|
* useDatastoreQuery in its `error` slot). The `code` is a stable
|
package/dist/index.js
CHANGED
package/dist/index.native.js
CHANGED
package/dist/linter.cjs
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
"use strict";
|
|
12
12
|
|
|
13
|
-
const { CONTRACT } = require("./contract.cjs");
|
|
13
|
+
const { CONTRACT, normaliseActionTriggerTypes } = require("./contract.cjs");
|
|
14
14
|
const { LUCIDE_ICON_NAMES, LUCIDE_VERSION } = require("./lucideIconNames.cjs");
|
|
15
15
|
|
|
16
16
|
function _ruleForIdentifier(identifier, reason) {
|
|
@@ -579,10 +579,11 @@ function _manifestActionRules(manifest) {
|
|
|
579
579
|
if (typeof a.name !== "string" || a.name.length === 0) {
|
|
580
580
|
push("manifest.actions[].name must be a non-empty string");
|
|
581
581
|
}
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
582
|
+
const triggerTypes = normaliseActionTriggerTypes(a);
|
|
583
|
+
if (triggerTypes === null) {
|
|
584
|
+
push(`manifest.actions[].triggerTypes must be a non-empty array of unique values from ${[...validTriggers].join(", ")}`);
|
|
585
|
+
} else if (triggerTypes.includes("schedule") && (typeof a.scheduleCron !== "string" || !a.scheduleCron)) {
|
|
586
|
+
push("manifest.actions[].scheduleCron is required when triggerTypes contains 'schedule'");
|
|
586
587
|
}
|
|
587
588
|
if (typeof a.scriptSource !== "string" || a.scriptSource.length === 0) {
|
|
588
589
|
push("manifest.actions[].scriptSource must be a non-empty string");
|
package/dist/linter.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// The banned-identifier list is derived from `CONTRACT.bannedApis` so the
|
|
7
7
|
// system prompt, the linter, and the runtime allowlist agree.
|
|
8
8
|
|
|
9
|
-
import { CONTRACT } from "./contract.js";
|
|
9
|
+
import { CONTRACT, normaliseActionTriggerTypes } from "./contract.js";
|
|
10
10
|
import { LUCIDE_ICON_NAMES, LUCIDE_VERSION } from "./lucideIconNames.js";
|
|
11
11
|
|
|
12
12
|
// Per-identifier match rule. Most banned identifiers compile to a
|
|
@@ -674,16 +674,17 @@ function _manifestActionRules(manifest) {
|
|
|
674
674
|
if (typeof a.name !== "string" || a.name.length === 0) {
|
|
675
675
|
push("manifest.actions[].name must be a non-empty string");
|
|
676
676
|
}
|
|
677
|
-
|
|
677
|
+
const triggerTypes = normaliseActionTriggerTypes(a);
|
|
678
|
+
if (triggerTypes === null) {
|
|
678
679
|
push(
|
|
679
|
-
`manifest.actions[].
|
|
680
|
+
`manifest.actions[].triggerTypes must be a non-empty array of unique values from ${[...validTriggers].join(", ")}`,
|
|
680
681
|
);
|
|
681
682
|
} else if (
|
|
682
|
-
|
|
683
|
+
triggerTypes.includes("schedule") &&
|
|
683
684
|
(typeof a.scheduleCron !== "string" || !a.scheduleCron)
|
|
684
685
|
) {
|
|
685
686
|
push(
|
|
686
|
-
"manifest.actions[].scheduleCron is required when
|
|
687
|
+
"manifest.actions[].scheduleCron is required when triggerTypes contains 'schedule'",
|
|
687
688
|
);
|
|
688
689
|
}
|
|
689
690
|
if (typeof a.scriptSource !== "string" || a.scriptSource.length === 0) {
|
package/dist/manifest.cjs
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// re-exports the same functions defined here so there is exactly one
|
|
5
5
|
// implementation.
|
|
6
6
|
|
|
7
|
-
const { CONTRACT } = require("./contract.cjs");
|
|
7
|
+
const { CONTRACT, normaliseActionTriggerTypes } = require("./contract.cjs");
|
|
8
8
|
|
|
9
9
|
const PAYLOAD_VALUE_TYPES = CONTRACT.payloadValueTypes;
|
|
10
10
|
|
|
@@ -152,15 +152,16 @@ function validateManifestActions(actions, errors) {
|
|
|
152
152
|
seenKeys.add(a.key);
|
|
153
153
|
}
|
|
154
154
|
pushIf(errors, isNonEmptyString(a.name), "manifest.actions[].name must be a non-empty string");
|
|
155
|
-
|
|
155
|
+
const triggerTypes = normaliseActionTriggerTypes(a);
|
|
156
|
+
if (triggerTypes === null) {
|
|
156
157
|
errors.push(
|
|
157
|
-
`manifest.actions[].
|
|
158
|
+
`manifest.actions[].triggerTypes must be a non-empty array of unique values from ${[...VALID_ACTION_TRIGGERS].join(", ")}`,
|
|
158
159
|
);
|
|
159
|
-
} else if (
|
|
160
|
+
} else if (triggerTypes.includes("schedule")) {
|
|
160
161
|
pushIf(
|
|
161
162
|
errors,
|
|
162
163
|
isNonEmptyString(a.scheduleCron),
|
|
163
|
-
"manifest.actions[].scheduleCron is required when
|
|
164
|
+
"manifest.actions[].scheduleCron is required when triggerTypes contains 'schedule'",
|
|
164
165
|
);
|
|
165
166
|
}
|
|
166
167
|
if (!isNonEmptyString(a.scriptSource)) {
|
package/dist/manifest.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// re-exports the same functions defined here so there is exactly one
|
|
5
5
|
// implementation.
|
|
6
6
|
|
|
7
|
-
import { CONTRACT } from "./contract.js";
|
|
7
|
+
import { CONTRACT, normaliseActionTriggerTypes } from "./contract.js";
|
|
8
8
|
|
|
9
9
|
const PAYLOAD_VALUE_TYPES = CONTRACT.payloadValueTypes;
|
|
10
10
|
|
|
@@ -152,15 +152,16 @@ function validateManifestActions(actions, errors) {
|
|
|
152
152
|
seenKeys.add(a.key);
|
|
153
153
|
}
|
|
154
154
|
pushIf(errors, isNonEmptyString(a.name), "manifest.actions[].name must be a non-empty string");
|
|
155
|
-
|
|
155
|
+
const triggerTypes = normaliseActionTriggerTypes(a);
|
|
156
|
+
if (triggerTypes === null) {
|
|
156
157
|
errors.push(
|
|
157
|
-
`manifest.actions[].
|
|
158
|
+
`manifest.actions[].triggerTypes must be a non-empty array of unique values from ${[...VALID_ACTION_TRIGGERS].join(", ")}`,
|
|
158
159
|
);
|
|
159
|
-
} else if (
|
|
160
|
+
} else if (triggerTypes.includes("schedule")) {
|
|
160
161
|
pushIf(
|
|
161
162
|
errors,
|
|
162
163
|
isNonEmptyString(a.scheduleCron),
|
|
163
|
-
"manifest.actions[].scheduleCron is required when
|
|
164
|
+
"manifest.actions[].scheduleCron is required when triggerTypes contains 'schedule'",
|
|
164
165
|
);
|
|
165
166
|
}
|
|
166
167
|
if (!isNonEmptyString(a.scriptSource)) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@colixsystems/widget-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.91.0",
|
|
4
4
|
"description": "Common widget interface for AppStudio. Implements WidgetManifest, WidgetContext, property schema, and helper hooks.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
],
|
|
49
49
|
"scripts": {
|
|
50
50
|
"build": "node scripts/build.js",
|
|
51
|
-
"test": "node --test src/__tests__/contract.test.js src/__tests__/hooks-users.test.js src/__tests__/hooks-groups.test.js src/__tests__/hooks-schema.test.js src/__tests__/hooks-assets-by-tag.test.js src/__tests__/hooks-filestore-upload.test.js src/__tests__/hooks-filestore-file.test.js src/__tests__/hooks-mutation.test.js src/__tests__/hooks-payments.test.js src/__tests__/hooks-record-permissions.test.js src/__tests__/hooks-geolocation.test.js src/__tests__/hooks-section-empty.test.js src/__tests__/hooks-widget-event.test.js src/__tests__/hooks-widget-input.test.js src/__tests__/hooks-identification.test.js src/__tests__/hooks-subscription.test.js src/__tests__/hooks-volatile-query-key.test.js src/__tests__/linter-users-scope.test.js src/__tests__/linter-comments.test.js src/__tests__/linter-translation-api.test.js src/__tests__/linter-image-height.test.js src/__tests__/linter-measured-padding.test.js src/__tests__/linter-payment-error.test.js src/__tests__/linter-platform.test.js src/__tests__/linter-react-import.test.js src/__tests__/lucide-icon-names.test.js src/__tests__/lucideIconName.test.js src/__tests__/manifest-actions.test.js src/__tests__/widget-translations.test.js src/__tests__/hooks-translate.test.js src/__tests__/devserver.test.js src/__tests__/host-externals.test.js src/__tests__/datetimepicker.test.js src/__tests__/property-schema-resolve.test.js src/__tests__/theme-components-parity.test.js src/__tests__/theme-depth-tokens.test.js src/__tests__/toast-host.test.js src/__tests__/hooks-domain-error-mapping.test.js src/__tests__/linter-datastore-error.test.js src/__tests__/linter-write-gating.test.js"
|
|
51
|
+
"test": "node --test src/__tests__/contract.test.js src/__tests__/hooks-users.test.js src/__tests__/hooks-groups.test.js src/__tests__/hooks-schema.test.js src/__tests__/hooks-assets-by-tag.test.js src/__tests__/hooks-filestore-upload.test.js src/__tests__/hooks-filestore-file.test.js src/__tests__/hooks-mutation.test.js src/__tests__/hooks-payments.test.js src/__tests__/hooks-record-permissions.test.js src/__tests__/hooks-geolocation.test.js src/__tests__/hooks-section-empty.test.js src/__tests__/hooks-widget-event.test.js src/__tests__/hooks-widget-input.test.js src/__tests__/hooks-identification.test.js src/__tests__/hooks-subscription.test.js src/__tests__/hooks-volatile-query-key.test.js src/__tests__/linter-users-scope.test.js src/__tests__/linter-comments.test.js src/__tests__/linter-translation-api.test.js src/__tests__/linter-image-height.test.js src/__tests__/linter-measured-padding.test.js src/__tests__/linter-payment-error.test.js src/__tests__/linter-platform.test.js src/__tests__/linter-react-import.test.js src/__tests__/lucide-icon-names.test.js src/__tests__/lucideIconName.test.js src/__tests__/manifest-actions.test.js src/__tests__/widget-translations.test.js src/__tests__/hooks-translate.test.js src/__tests__/devserver.test.js src/__tests__/host-externals.test.js src/__tests__/datetimepicker.test.js src/__tests__/property-schema-resolve.test.js src/__tests__/theme-components-parity.test.js src/__tests__/theme-depth-tokens.test.js src/__tests__/toast-host.test.js src/__tests__/hooks-domain-error-mapping.test.js src/__tests__/linter-datastore-error.test.js src/__tests__/linter-write-gating.test.js src/__tests__/hooks-speech-to-text.test.js"
|
|
52
52
|
},
|
|
53
53
|
"engines": {
|
|
54
54
|
"node": ">=18"
|