@colixsystems/widget-sdk 0.91.0 → 0.92.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 CHANGED
@@ -41,6 +41,7 @@ The data layer lives in **four separate domain-client packages**, each instantia
41
41
  | **DATASTORE** (`ctx.datastore`) | `useDatastoreQuery(table, options?)` | `{ data, loading, error, refetch }` | `records(table).list` (unwraps `{ data, meta }` to `data: []`) — `datastore.read:*` |
42
42
  | **DATASTORE** | `useDatastoreRecord(table, id)` | `{ data, loading, error, refetch }` | `records(table).get` — `datastore.read:<table>` |
43
43
  | **DATASTORE** | `useDatastoreSchema(tableId)` | `{ schema, loading, error, refetch }` | `schema(tableId)` — `datastore.read:<table>` |
44
+ | **DATASTORE** | `useInterpretDraft(tableId)` | `{ interpret, interpreting, error, result, available }` | `interpret(tableId, body)` — `datastore.read:<table>`. Turns ONE sentence a user typed ("walk at 11 am tomorrow") into DRAFT column values so a form can prefill itself. IMPERATIVE: call `interpret(text, { fields?, timeZone? })` from an event handler, never on mount. It DRAFTS and writes nothing — show the values for review, then submit through `useDatastoreMutation().create`. Resolves to `{ values, unresolved }`; `values` is keyed by column NAME (the shape `create()` takes) and `unresolved` names the fields the sentence did not state. Only text / number / boolean / date / datetime / array columns are drafted — `FILE`, `RELATION`, `USER` and `USER_GROUP` carry ids and are never guessed. Fails closed to an empty draft. **Every call spends the workspace's AI credits** and is rate-limited per actor, so call it once per user action (never on mount or in a render loop); once the workspace runs out the call is refused with a generic 429 — an app user is deliberately **not** told the workspace's billing state, since they have never heard of an AI credit and cannot buy one. Never surface a raw error to the person filling the form: say drafting is unavailable and keep every field editable by hand. `available` is false where the host brokers no interpreter. |
44
45
  | **DATASTORE** | `useDatastoreMutation(table)` | `{ create, update, delete }` | `records(table).{ create, update (PATCH), delete }` — `datastore.write:*` |
45
46
  | **DATASTORE** | `useRecordPermissions(tableId, recordId)` | `{ permissions, loading, error, grant, revoke, update, refetch }` | `records(table).permissions(record).{ list, grant, update, revoke }` — `acl.write:records` (+ `can_grant` on the record) |
46
47
  | **FILES** (`ctx.assets`) | `useAsset(id)` | `{ url, file, loading, error, refetch }` | `ctx.assets.get` — no scope |
@@ -64,6 +65,18 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
64
65
 
65
66
  `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
 
68
+ ### What's new in 0.92.0 (contract 1.65.0)
69
+
70
+ **New `useInterpretDraft(tableId)` hook — turn one sentence into DRAFT record values.** A new DATASTORE hook reading a new `interpret` method on the existing `ctx.datastore` slice (`@colixsystems/datastore-client` 0.13.0). Returns `{ interpret, interpreting, error, result, available }`. Call `interpret(text, { fields, timeZone })` **imperatively** from an event handler — never on mount or in a render loop — and it resolves to `{ values, unresolved }`, where `values` is keyed by column NAME (the same shape `useDatastoreMutation().create` takes) and `unresolved` names the fields the sentence did not state.
71
+
72
+ **It DRAFTS and writes nothing.** Prefill your inputs from `values`, let the person review and correct them, then submit as usual. A model reading free text must never create a record on its own.
73
+
74
+ Only columns a sentence can honestly produce are drafted — string, text, number, float, boolean, date, datetime and array. `FILE`, `RELATION`, `USER` and `USER_GROUP` are never guessed because they carry identifiers, and encrypted columns are skipped. Every value is coerced against its column's `data_type` and dropped when it does not fit, so a value the model got wrong is reported `unresolved` rather than written through.
75
+
76
+ **Every call spends the workspace's AI credits** and is rate-limited per actor. Once the workspace runs out, the call is refused with a **generic** 429: the person filling the form is never told the workspace's billing state — they have not heard of an AI credit and cannot buy one. Never surface a raw error to them; say drafting is unavailable and keep every field editable by hand. `available` is `false` where the host brokers no interpreter (an unbound preview, or an export with no reachable backend) — hide the affordance rather than rendering a dead button.
77
+
78
+ Additive — one new hook, one new client method, one new context-slice function; no existing export changed signature.
79
+
67
80
  ### What's new in 0.91.0 (contract 1.64.0)
68
81
 
69
82
  **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**.
package/dist/contract.cjs CHANGED
@@ -820,6 +820,42 @@ const HOOKS = [
820
820
  requiredContextSlice: ["datastore.schema"],
821
821
  scopes: ["datastore.read:<table>"],
822
822
  },
823
+ {
824
+ name: "useInterpretDraft",
825
+ signature: "useInterpretDraft(tableId)",
826
+ description:
827
+ "sc-4932 — turns ONE sentence a user typed into DRAFT values for the " +
828
+ "table's columns (\"walk at 11 am tomorrow\" -> { title: 'Walk', " +
829
+ "due_at: '...T11:00' }). IMPERATIVE: call interpret(text, { fields, " +
830
+ "timeZone }) from an event handler (a button press), never on mount. It " +
831
+ "DRAFTS and writes NOTHING — prefill your form from `values`, let the " +
832
+ "user review and correct it, then submit through " +
833
+ "useDatastoreMutation().create as usual. `values` is keyed by column " +
834
+ "NAME, the same shape create() takes. `unresolved` lists the fields the " +
835
+ "sentence did not state — leave those blank rather than guessing. Pass " +
836
+ "`fields` to narrow the draft to the columns you actually render, each " +
837
+ "optionally carrying a dropdown's closed `options` list; pass `timeZone` " +
838
+ "(an IANA zone) so relative times resolve correctly. Only text, number, " +
839
+ "boolean, date/datetime and array columns are drafted — FILE, RELATION, " +
840
+ "USER and USER_GROUP columns are never guessed because they carry ids. " +
841
+ "Every call spends the workspace's AI CREDITS and is rate-limited per " +
842
+ "actor, so call it once per user action — never on mount or in a render " +
843
+ "loop. Once the workspace runs out of credits the call is refused with a " +
844
+ "generic 429 (an app user is never told the workspace's billing state — " +
845
+ "they have not heard of an AI credit and cannot buy one). NEVER surface a " +
846
+ "raw error to the person filling the form: say drafting is unavailable and " +
847
+ "keep every field editable by hand. Reads ctx.datastore.interpret.",
848
+ returnShape: {
849
+ interpret:
850
+ "(text, { fields?, timeZone? }) => Promise<{ values, unresolved }>",
851
+ interpreting: "boolean",
852
+ error: "DatastoreError | null",
853
+ result: "{ values, unresolved } | null",
854
+ available: "boolean // false when the host brokers no interpreter",
855
+ },
856
+ requiredContextSlice: ["datastore.interpret"],
857
+ scopes: ["datastore.read:<table>"],
858
+ },
823
859
  {
824
860
  name: "useDatastoreMutation",
825
861
  signature: "useDatastoreMutation(tableId)",
@@ -1637,13 +1673,13 @@ const WIDGET_CONTEXT_SHAPE = {
1637
1673
  datastore: {
1638
1674
  description:
1639
1675
  "Injected @colixsystems/datastore-client instance. " +
1640
- "{ tables: { list(), get(idOrName) }, schema(tableId) -> Promise<{ id, name, columns: [...] }>, " +
1676
+ "{ tables: { list(), get(idOrName), interpret(idOrName, body) }, schema(tableId) -> Promise<{ id, name, columns: [...] }>, " +
1641
1677
  "records(tableId) -> { list(query) -> Promise<{ data, meta }>, get(id), create(values), update(id, values), delete(id), aggregate(spec), " +
1642
1678
  "permissions(recordId) -> { list() -> Promise<{ data, meta }>, grant(body), update(permId, patch), revoke(permId) } } }. " +
1643
- "`records` backs the query/record/mutation hooks; `records(t).permissions(r)` backs useRecordPermissions(); `schema` backs useDatastoreSchema(). " +
1679
+ "`records` backs the query/record/mutation hooks; `records(t).permissions(r)` backs useRecordPermissions(); `schema` backs useDatastoreSchema(); `interpret` backs useInterpretDraft() (sc-4932 — drafts column values from one sentence; writes nothing). " +
1644
1680
  "List methods return the { data, meta } envelope verbatim (hooks unwrap res.data); rows/bodies are snake_case (author column values keep their author-given names).",
1645
1681
  required: true,
1646
- fields: { records: "function", schema: "function", tables: "object" },
1682
+ fields: { records: "function", schema: "function", tables: "object", interpret: "function" },
1647
1683
  },
1648
1684
  directory: {
1649
1685
  description:
@@ -2855,7 +2891,7 @@ const CONTRACT = deepFreeze({
2855
2891
  // published widget keeps validating. The script's `triggerType` global
2856
2892
  // now names the trigger that actually FIRED the run ('manual' and 'app'
2857
2893
  // included), which is what makes a multi-trigger script able to branch.
2858
- version: "1.64.0",
2894
+ version: "1.65.0",
2859
2895
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
2860
2896
  hooks: HOOKS,
2861
2897
  primitives: PRIMITIVES,
package/dist/contract.js CHANGED
@@ -820,6 +820,42 @@ const HOOKS = [
820
820
  requiredContextSlice: ["datastore.schema"],
821
821
  scopes: ["datastore.read:<table>"],
822
822
  },
823
+ {
824
+ name: "useInterpretDraft",
825
+ signature: "useInterpretDraft(tableId)",
826
+ description:
827
+ "sc-4932 — turns ONE sentence a user typed into DRAFT values for the " +
828
+ "table's columns (\"walk at 11 am tomorrow\" -> { title: 'Walk', " +
829
+ "due_at: '...T11:00' }). IMPERATIVE: call interpret(text, { fields, " +
830
+ "timeZone }) from an event handler (a button press), never on mount. It " +
831
+ "DRAFTS and writes NOTHING — prefill your form from `values`, let the " +
832
+ "user review and correct it, then submit through " +
833
+ "useDatastoreMutation().create as usual. `values` is keyed by column " +
834
+ "NAME, the same shape create() takes. `unresolved` lists the fields the " +
835
+ "sentence did not state — leave those blank rather than guessing. Pass " +
836
+ "`fields` to narrow the draft to the columns you actually render, each " +
837
+ "optionally carrying a dropdown's closed `options` list; pass `timeZone` " +
838
+ "(an IANA zone) so relative times resolve correctly. Only text, number, " +
839
+ "boolean, date/datetime and array columns are drafted — FILE, RELATION, " +
840
+ "USER and USER_GROUP columns are never guessed because they carry ids. " +
841
+ "Every call spends the workspace's AI CREDITS and is rate-limited per " +
842
+ "actor, so call it once per user action — never on mount or in a render " +
843
+ "loop. Once the workspace runs out of credits the call is refused with a " +
844
+ "generic 429 (an app user is never told the workspace's billing state — " +
845
+ "they have not heard of an AI credit and cannot buy one). NEVER surface a " +
846
+ "raw error to the person filling the form: say drafting is unavailable and " +
847
+ "keep every field editable by hand. Reads ctx.datastore.interpret.",
848
+ returnShape: {
849
+ interpret:
850
+ "(text, { fields?, timeZone? }) => Promise<{ values, unresolved }>",
851
+ interpreting: "boolean",
852
+ error: "DatastoreError | null",
853
+ result: "{ values, unresolved } | null",
854
+ available: "boolean // false when the host brokers no interpreter",
855
+ },
856
+ requiredContextSlice: ["datastore.interpret"],
857
+ scopes: ["datastore.read:<table>"],
858
+ },
823
859
  {
824
860
  name: "useDatastoreMutation",
825
861
  signature: "useDatastoreMutation(tableId)",
@@ -1637,13 +1673,13 @@ const WIDGET_CONTEXT_SHAPE = {
1637
1673
  datastore: {
1638
1674
  description:
1639
1675
  "Injected @colixsystems/datastore-client instance. " +
1640
- "{ tables: { list(), get(idOrName) }, schema(tableId) -> Promise<{ id, name, columns: [...] }>, " +
1676
+ "{ tables: { list(), get(idOrName), interpret(idOrName, body) }, schema(tableId) -> Promise<{ id, name, columns: [...] }>, " +
1641
1677
  "records(tableId) -> { list(query) -> Promise<{ data, meta }>, get(id), create(values), update(id, values), delete(id), aggregate(spec), " +
1642
1678
  "permissions(recordId) -> { list() -> Promise<{ data, meta }>, grant(body), update(permId, patch), revoke(permId) } } }. " +
1643
- "`records` backs the query/record/mutation hooks; `records(t).permissions(r)` backs useRecordPermissions(); `schema` backs useDatastoreSchema(). " +
1679
+ "`records` backs the query/record/mutation hooks; `records(t).permissions(r)` backs useRecordPermissions(); `schema` backs useDatastoreSchema(); `interpret` backs useInterpretDraft() (sc-4932 — drafts column values from one sentence; writes nothing). " +
1644
1680
  "List methods return the { data, meta } envelope verbatim (hooks unwrap res.data); rows/bodies are snake_case (author column values keep their author-given names).",
1645
1681
  required: true,
1646
- fields: { records: "function", schema: "function", tables: "object" },
1682
+ fields: { records: "function", schema: "function", tables: "object", interpret: "function" },
1647
1683
  },
1648
1684
  directory: {
1649
1685
  description:
@@ -2855,7 +2891,7 @@ const CONTRACT = deepFreeze({
2855
2891
  // published widget keeps validating. The script's `triggerType` global
2856
2892
  // now names the trigger that actually FIRED the run ('manual' and 'app'
2857
2893
  // included), which is what makes a multi-trigger script able to branch.
2858
- version: "1.64.0",
2894
+ version: "1.65.0",
2859
2895
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
2860
2896
  hooks: HOOKS,
2861
2897
  primitives: PRIMITIVES,
package/dist/hooks.js CHANGED
@@ -1611,6 +1611,83 @@ export function useDatastoreSchema(tableId) {
1611
1611
  return { schema, loading, error, refetch };
1612
1612
  }
1613
1613
 
1614
+ /**
1615
+ * sc-4932 — draft record values for `tableId` from one sentence the user typed.
1616
+ * Returns `{ interpret, interpreting, error, result, available }`.
1617
+ *
1618
+ * interpret(text, { fields?, timeZone? }) → Promise<{ values, unresolved }>
1619
+ * `values` is keyed by column NAME — the same shape
1620
+ * `useDatastoreMutation().create` takes — so a caller prefills its form
1621
+ * from it and submits through the ordinary create path. `unresolved` names
1622
+ * the fields the sentence did not state; they are left for the user.
1623
+ *
1624
+ * The hook is IMPERATIVE — it NEVER fires on mount; the widget calls
1625
+ * `interpret` from an event handler. `interpreting` tracks an in-flight call
1626
+ * and `error` holds the last DatastoreError (cleared at the start of each
1627
+ * call). `result` holds the last successful draft.
1628
+ *
1629
+ * This DRAFTS, it does not write: the user reviews every value before the
1630
+ * record is created. `fields` narrows the draft to the columns the caller
1631
+ * actually renders, each optionally carrying the closed `options` list of a
1632
+ * dropdown. `timeZone` is an IANA zone; the server resolves "at 11 am" against
1633
+ * it and falls back to UTC when it is absent or unknown.
1634
+ *
1635
+ * Routes through the injected `@colixsystems/datastore-client` at
1636
+ * `ctx.datastore.interpret`, so the Player and the Expo export resolve against
1637
+ * the identical client. A host that brokers no interpreter (an unbound canvas
1638
+ * preview) reports `available: false` rather than throwing, matching how
1639
+ * useTranslate degrades.
1640
+ */
1641
+ export function useInterpretDraft(tableId) {
1642
+ const ctx = useWidgetContextOrThrow("useInterpretDraft");
1643
+ const available =
1644
+ Boolean(ctx.datastore) && typeof ctx.datastore.interpret === "function";
1645
+
1646
+ const fnRef = useRef(available ? ctx.datastore.interpret : null);
1647
+ fnRef.current = available ? ctx.datastore.interpret : null;
1648
+ const tableIdRef = useRef(tableId);
1649
+ tableIdRef.current = tableId;
1650
+
1651
+ const [interpreting, setInterpreting] = useState(false);
1652
+ const [error, setError] = useState(null);
1653
+ const [result, setResult] = useState(null);
1654
+
1655
+ const interpret = useCallback(async (text, options) => {
1656
+ const table = tableIdRef.current;
1657
+ if (!fnRef.current || !table) {
1658
+ throw new DatastoreError(
1659
+ "UNAVAILABLE",
1660
+ "No interpreter available for this host",
1661
+ );
1662
+ }
1663
+ setInterpreting(true);
1664
+ setError(null);
1665
+ try {
1666
+ // snake_case verbatim on the wire (REQ-GEN-09) — the SDK does not
1667
+ // transform, so the camelCase `timeZone` argument is mapped here, once.
1668
+ const body = { text };
1669
+ if (options && Array.isArray(options.fields)) body.fields = options.fields;
1670
+ if (options && options.timeZone) body.time_zone = options.timeZone;
1671
+ const draft = await fnRef.current(table, body);
1672
+ const safe = {
1673
+ values: draft && draft.values ? draft.values : {},
1674
+ unresolved:
1675
+ draft && Array.isArray(draft.unresolved) ? draft.unresolved : [],
1676
+ };
1677
+ setResult(safe);
1678
+ setInterpreting(false);
1679
+ return safe;
1680
+ } catch (err) {
1681
+ const e = toDatastoreError(err);
1682
+ setError(e);
1683
+ setInterpreting(false);
1684
+ throw e;
1685
+ }
1686
+ }, []);
1687
+
1688
+ return { interpret, interpreting, error, result, available };
1689
+ }
1690
+
1614
1691
  /**
1615
1692
  * Datastore mutation hook. Returns { create, update, delete }, each method
1616
1693
  * returning a Promise. Routes through the injected
package/dist/index.d.ts CHANGED
@@ -963,6 +963,50 @@ export function useDatastoreSchema(
963
963
  tableId: string | null | undefined,
964
964
  ): SchemaResult;
965
965
 
966
+ /** sc-4932 — one field a draft may target, with a dropdown's closed option set. */
967
+ export interface InterpretDraftField {
968
+ column: string;
969
+ options?: Array<string | number>;
970
+ }
971
+
972
+ /** sc-4932 — options for `useInterpretDraft().interpret`. */
973
+ export interface InterpretDraftOptions {
974
+ fields?: Array<InterpretDraftField | string>;
975
+ /** IANA zone the server resolves relative times against. Defaults to UTC. */
976
+ timeZone?: string;
977
+ }
978
+
979
+ /**
980
+ * sc-4932 — a drafted record. `values` is keyed by column NAME (the shape
981
+ * `useDatastoreMutation().create` takes); `unresolved` names the fields the
982
+ * sentence did not state.
983
+ */
984
+ export interface InterpretDraftResult {
985
+ values: Record<string, unknown>;
986
+ unresolved: string[];
987
+ }
988
+
989
+ export interface InterpretDraftApi {
990
+ interpret(
991
+ text: string,
992
+ options?: InterpretDraftOptions,
993
+ ): Promise<InterpretDraftResult>;
994
+ interpreting: boolean;
995
+ error: DatastoreError | null;
996
+ result: InterpretDraftResult | null;
997
+ /** False when the host brokers no interpreter (e.g. an unbound preview). */
998
+ available: boolean;
999
+ }
1000
+
1001
+ /**
1002
+ * sc-4932 — draft record values for `tableId` from one sentence the user typed
1003
+ * ("walk at 11 am tomorrow"). IMPERATIVE: never fires on mount, and DRAFTS
1004
+ * only — the user reviews the values before the record is created.
1005
+ */
1006
+ export function useInterpretDraft(
1007
+ tableId: string | null | undefined,
1008
+ ): InterpretDraftApi;
1009
+
966
1010
  // REQ-RT-07 realtime subscription transport state.
967
1011
  export type DatastoreSubscriptionStatus =
968
1012
  | "connecting"
package/dist/index.js CHANGED
@@ -15,6 +15,7 @@ export {
15
15
  useDatastoreQuery,
16
16
  useDatastoreRecord,
17
17
  useDatastoreSchema,
18
+ useInterpretDraft,
18
19
  useAsset,
19
20
  useAssetsByTag,
20
21
  useFilestoreFiles,
@@ -15,6 +15,7 @@ export {
15
15
  useDatastoreQuery,
16
16
  useDatastoreRecord,
17
17
  useDatastoreSchema,
18
+ useInterpretDraft,
18
19
  useAsset,
19
20
  useAssetsByTag,
20
21
  useFilestoreFiles,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@colixsystems/widget-sdk",
3
- "version": "0.91.0",
3
+ "version": "0.92.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",