@assistant-ui/store 0.0.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/LICENSE +21 -0
- package/README.md +295 -0
- package/dist/AssistantContext.d.ts +22 -0
- package/dist/AssistantContext.d.ts.map +1 -0
- package/dist/AssistantContext.js +44 -0
- package/dist/AssistantContext.js.map +1 -0
- package/dist/DerivedScope.d.ts +18 -0
- package/dist/DerivedScope.d.ts.map +1 -0
- package/dist/DerivedScope.js +11 -0
- package/dist/DerivedScope.js.map +1 -0
- package/dist/ScopeRegistry.d.ts +41 -0
- package/dist/ScopeRegistry.d.ts.map +1 -0
- package/dist/ScopeRegistry.js +17 -0
- package/dist/ScopeRegistry.js.map +1 -0
- package/dist/asStore.d.ts +20 -0
- package/dist/asStore.d.ts.map +1 -0
- package/dist/asStore.js +23 -0
- package/dist/asStore.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/tapApi.d.ts +36 -0
- package/dist/tapApi.d.ts.map +1 -0
- package/dist/tapApi.js +52 -0
- package/dist/tapApi.js.map +1 -0
- package/dist/tapLookupResources.d.ts +44 -0
- package/dist/tapLookupResources.d.ts.map +1 -0
- package/dist/tapLookupResources.js +21 -0
- package/dist/tapLookupResources.js.map +1 -0
- package/dist/tapStoreList.d.ts +76 -0
- package/dist/tapStoreList.d.ts.map +1 -0
- package/dist/tapStoreList.js +46 -0
- package/dist/tapStoreList.js.map +1 -0
- package/dist/types.d.ts +86 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/dist/types.js.map +1 -0
- package/dist/useAssistantClient.d.ts +42 -0
- package/dist/useAssistantClient.d.ts.map +1 -0
- package/dist/useAssistantClient.js +153 -0
- package/dist/useAssistantClient.js.map +1 -0
- package/dist/useAssistantState.d.ts +18 -0
- package/dist/useAssistantState.d.ts.map +1 -0
- package/dist/useAssistantState.js +53 -0
- package/dist/useAssistantState.js.map +1 -0
- package/dist/utils/splitScopes.d.ts +24 -0
- package/dist/utils/splitScopes.d.ts.map +1 -0
- package/dist/utils/splitScopes.js +18 -0
- package/dist/utils/splitScopes.js.map +1 -0
- package/package.json +50 -0
- package/src/AssistantContext.tsx +64 -0
- package/src/DerivedScope.ts +21 -0
- package/src/ScopeRegistry.ts +58 -0
- package/src/asStore.ts +40 -0
- package/src/index.ts +13 -0
- package/src/tapApi.ts +91 -0
- package/src/tapLookupResources.ts +62 -0
- package/src/tapStoreList.ts +133 -0
- package/src/types.ts +120 -0
- package/src/useAssistantClient.tsx +250 -0
- package/src/useAssistantState.tsx +80 -0
- package/src/utils/splitScopes.ts +38 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// src/useAssistantState.tsx
|
|
2
|
+
import { useMemo, useSyncExternalStore, useDebugValue } from "react";
|
|
3
|
+
import { useAssistantClient } from "./useAssistantClient.js";
|
|
4
|
+
var ProxiedAssistantState = class _ProxiedAssistantState {
|
|
5
|
+
#client;
|
|
6
|
+
constructor(client) {
|
|
7
|
+
this.#client = client;
|
|
8
|
+
}
|
|
9
|
+
#getScope(key) {
|
|
10
|
+
const scopeField = this.#client[key];
|
|
11
|
+
if (!scopeField) {
|
|
12
|
+
throw new Error(`Scope "${String(key)}" not found in client`);
|
|
13
|
+
}
|
|
14
|
+
const api = scopeField();
|
|
15
|
+
const state = api.getState();
|
|
16
|
+
return state;
|
|
17
|
+
}
|
|
18
|
+
// Create a Proxy to dynamically handle property access
|
|
19
|
+
static create(client) {
|
|
20
|
+
const instance = new _ProxiedAssistantState(client);
|
|
21
|
+
return new Proxy(instance, {
|
|
22
|
+
get(target, prop) {
|
|
23
|
+
if (typeof prop === "string" && prop in client) {
|
|
24
|
+
return target.#getScope(prop);
|
|
25
|
+
}
|
|
26
|
+
return void 0;
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
var useAssistantState = (selector) => {
|
|
32
|
+
const client = useAssistantClient();
|
|
33
|
+
const proxiedState = useMemo(
|
|
34
|
+
() => ProxiedAssistantState.create(client),
|
|
35
|
+
[client]
|
|
36
|
+
);
|
|
37
|
+
const slice = useSyncExternalStore(
|
|
38
|
+
client.subscribe,
|
|
39
|
+
() => selector(proxiedState),
|
|
40
|
+
() => selector(proxiedState)
|
|
41
|
+
);
|
|
42
|
+
useDebugValue(slice);
|
|
43
|
+
if (slice instanceof ProxiedAssistantState) {
|
|
44
|
+
throw new Error(
|
|
45
|
+
"You tried to return the entire AssistantState. This is not supported due to technical limitations."
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
return slice;
|
|
49
|
+
};
|
|
50
|
+
export {
|
|
51
|
+
useAssistantState
|
|
52
|
+
};
|
|
53
|
+
//# sourceMappingURL=useAssistantState.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/useAssistantState.tsx"],"sourcesContent":["import { useMemo, useSyncExternalStore, useDebugValue } from \"react\";\nimport type { AssistantClient, AssistantState } from \"./types\";\nimport { useAssistantClient } from \"./useAssistantClient\";\n\n/**\n * Proxied state that lazily accesses scope states\n */\nclass ProxiedAssistantState {\n #client: AssistantClient;\n\n constructor(client: AssistantClient) {\n this.#client = client;\n }\n\n #getScope<K extends keyof AssistantState>(key: K): AssistantState[K] {\n const scopeField = this.#client[key];\n if (!scopeField) {\n throw new Error(`Scope \"${String(key)}\" not found in client`);\n }\n\n const api = scopeField();\n const state = api.getState();\n return state as AssistantState[K];\n }\n\n // Create a Proxy to dynamically handle property access\n static create(client: AssistantClient): AssistantState {\n const instance = new ProxiedAssistantState(client);\n return new Proxy(instance, {\n get(target, prop) {\n if (typeof prop === \"string\" && prop in client) {\n return target.#getScope(prop as keyof AssistantState);\n }\n return undefined;\n },\n }) as unknown as AssistantState;\n }\n}\n\n/**\n * Hook to access a slice of the assistant state with automatic subscription\n *\n * @param selector - Function to select a slice of the state\n * @returns The selected state slice\n *\n * @example\n * ```typescript\n * const client = useAssistantClient({\n * foo: RootScope({ ... }),\n * });\n *\n * const bar = useAssistantState((state) => state.foo.bar);\n * ```\n */\nexport const useAssistantState = <T,>(\n selector: (state: AssistantState) => T,\n): T => {\n const client = useAssistantClient();\n\n const proxiedState = useMemo(\n () => ProxiedAssistantState.create(client),\n [client],\n );\n\n const slice = useSyncExternalStore(\n client.subscribe,\n () => selector(proxiedState),\n () => selector(proxiedState),\n );\n\n useDebugValue(slice);\n\n if (slice instanceof ProxiedAssistantState) {\n throw new Error(\n \"You tried to return the entire AssistantState. This is not supported due to technical limitations.\",\n );\n }\n\n return slice;\n};\n"],"mappings":";AAAA,SAAS,SAAS,sBAAsB,qBAAqB;AAE7D,SAAS,0BAA0B;AAKnC,IAAM,wBAAN,MAAM,uBAAsB;AAAA,EAC1B;AAAA,EAEA,YAAY,QAAyB;AACnC,SAAK,UAAU;AAAA,EACjB;AAAA,EAEA,UAA0C,KAA2B;AACnE,UAAM,aAAa,KAAK,QAAQ,GAAG;AACnC,QAAI,CAAC,YAAY;AACf,YAAM,IAAI,MAAM,UAAU,OAAO,GAAG,CAAC,uBAAuB;AAAA,IAC9D;AAEA,UAAM,MAAM,WAAW;AACvB,UAAM,QAAQ,IAAI,SAAS;AAC3B,WAAO;AAAA,EACT;AAAA;AAAA,EAGA,OAAO,OAAO,QAAyC;AACrD,UAAM,WAAW,IAAI,uBAAsB,MAAM;AACjD,WAAO,IAAI,MAAM,UAAU;AAAA,MACzB,IAAI,QAAQ,MAAM;AAChB,YAAI,OAAO,SAAS,YAAY,QAAQ,QAAQ;AAC9C,iBAAO,OAAO,UAAU,IAA4B;AAAA,QACtD;AACA,eAAO;AAAA,MACT;AAAA,IACF,CAAC;AAAA,EACH;AACF;AAiBO,IAAM,oBAAoB,CAC/B,aACM;AACN,QAAM,SAAS,mBAAmB;AAElC,QAAM,eAAe;AAAA,IACnB,MAAM,sBAAsB,OAAO,MAAM;AAAA,IACzC,CAAC,MAAM;AAAA,EACT;AAEA,QAAM,QAAQ;AAAA,IACZ,OAAO;AAAA,IACP,MAAM,SAAS,YAAY;AAAA,IAC3B,MAAM,SAAS,YAAY;AAAA,EAC7B;AAEA,gBAAc,KAAK;AAEnB,MAAI,iBAAiB,uBAAuB;AAC1C,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AACT;","names":[]}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ScopesInput } from "../types";
|
|
2
|
+
/**
|
|
3
|
+
* Splits a scopes object into root scopes and derived scopes.
|
|
4
|
+
*
|
|
5
|
+
* @param scopes - The scopes input object to split
|
|
6
|
+
* @returns An object with { rootScopes, derivedScopes }
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* const scopes = {
|
|
11
|
+
* foo: RootScope({ ... }),
|
|
12
|
+
* bar: DerivedScope({ ... }),
|
|
13
|
+
* };
|
|
14
|
+
*
|
|
15
|
+
* const { rootScopes, derivedScopes } = splitScopes(scopes);
|
|
16
|
+
* // rootScopes = { foo: ... }
|
|
17
|
+
* // derivedScopes = { bar: ... }
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export declare function splitScopes(scopes: ScopesInput): {
|
|
21
|
+
rootScopes: ScopesInput;
|
|
22
|
+
derivedScopes: ScopesInput;
|
|
23
|
+
};
|
|
24
|
+
//# sourceMappingURL=splitScopes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"splitScopes.d.ts","sourceRoot":"","sources":["../../src/utils/splitScopes.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAA+B,WAAW,EAAE,MAAM,UAAU,CAAC;AAEzE;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,WAAW;;;EAgB9C"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// src/utils/splitScopes.ts
|
|
2
|
+
import { DerivedScope } from "../DerivedScope.js";
|
|
3
|
+
function splitScopes(scopes) {
|
|
4
|
+
const rootScopes = {};
|
|
5
|
+
const derivedScopes = {};
|
|
6
|
+
for (const [key, scopeElement] of Object.entries(scopes)) {
|
|
7
|
+
if (scopeElement.type === DerivedScope) {
|
|
8
|
+
derivedScopes[key] = scopeElement;
|
|
9
|
+
} else {
|
|
10
|
+
rootScopes[key] = scopeElement;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
return { rootScopes, derivedScopes };
|
|
14
|
+
}
|
|
15
|
+
export {
|
|
16
|
+
splitScopes
|
|
17
|
+
};
|
|
18
|
+
//# sourceMappingURL=splitScopes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/utils/splitScopes.ts"],"sourcesContent":["import { DerivedScope } from \"../DerivedScope\";\nimport type { AssistantScopes, ScopeInput, ScopesInput } from \"../types\";\n\n/**\n * Splits a scopes object into root scopes and derived scopes.\n *\n * @param scopes - The scopes input object to split\n * @returns An object with { rootScopes, derivedScopes }\n *\n * @example\n * ```typescript\n * const scopes = {\n * foo: RootScope({ ... }),\n * bar: DerivedScope({ ... }),\n * };\n *\n * const { rootScopes, derivedScopes } = splitScopes(scopes);\n * // rootScopes = { foo: ... }\n * // derivedScopes = { bar: ... }\n * ```\n */\nexport function splitScopes(scopes: ScopesInput) {\n const rootScopes: ScopesInput = {};\n const derivedScopes: ScopesInput = {};\n\n for (const [key, scopeElement] of Object.entries(scopes) as [\n keyof ScopesInput,\n ScopeInput<AssistantScopes[keyof ScopesInput]>,\n ][]) {\n if (scopeElement.type === DerivedScope) {\n derivedScopes[key] = scopeElement;\n } else {\n rootScopes[key] = scopeElement;\n }\n }\n\n return { rootScopes, derivedScopes };\n}\n"],"mappings":";AAAA,SAAS,oBAAoB;AAqBtB,SAAS,YAAY,QAAqB;AAC/C,QAAM,aAA0B,CAAC;AACjC,QAAM,gBAA6B,CAAC;AAEpC,aAAW,CAAC,KAAK,YAAY,KAAK,OAAO,QAAQ,MAAM,GAGlD;AACH,QAAI,aAAa,SAAS,cAAc;AACtC,oBAAc,GAAG,IAAI;AAAA,IACvB,OAAO;AACL,iBAAW,GAAG,IAAI;AAAA,IACpB;AAAA,EACF;AAEA,SAAO,EAAE,YAAY,cAAc;AACrC;","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@assistant-ui/store",
|
|
3
|
+
"description": "Tap-based state management for @assistant-ui",
|
|
4
|
+
"version": "0.0.0",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"default": "./dist/index.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"source": "./src/index.ts",
|
|
14
|
+
"main": "./dist/index.js",
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"src",
|
|
19
|
+
"README.md"
|
|
20
|
+
],
|
|
21
|
+
"sideEffects": false,
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@assistant-ui/tap": "0.3.0"
|
|
24
|
+
},
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"react": "^18.0.0 || ^19.0.0"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/node": "^24.10.1",
|
|
30
|
+
"@types/react": "19.2.5",
|
|
31
|
+
"tsx": "^4.20.6",
|
|
32
|
+
"@assistant-ui/x-buildutils": "0.0.1"
|
|
33
|
+
},
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"access": "public",
|
|
36
|
+
"provenance": true
|
|
37
|
+
},
|
|
38
|
+
"homepage": "https://www.assistant-ui.com/",
|
|
39
|
+
"repository": {
|
|
40
|
+
"type": "git",
|
|
41
|
+
"url": "https://github.com/assistant-ui/assistant-ui/tree/main/packages/store"
|
|
42
|
+
},
|
|
43
|
+
"bugs": {
|
|
44
|
+
"url": "https://github.com/assistant-ui/assistant-ui/issues"
|
|
45
|
+
},
|
|
46
|
+
"scripts": {
|
|
47
|
+
"build": "tsx scripts/build.mts",
|
|
48
|
+
"lint": "eslint ."
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import React, { createContext, useContext } from "react";
|
|
2
|
+
import type { AssistantClient, AssistantScopes, ScopeField } from "./types";
|
|
3
|
+
import { hasRegisteredScope } from "./ScopeRegistry";
|
|
4
|
+
|
|
5
|
+
const NO_OP_SUBSCRIBE = () => () => {};
|
|
6
|
+
const NO_OP_FLUSH_SYNC = () => {};
|
|
7
|
+
const NO_OP_SCOPE_FIELD = (() => {
|
|
8
|
+
const fn = (() => {
|
|
9
|
+
throw new Error(
|
|
10
|
+
"You need to wrap this component/hook in <AssistantProvider>",
|
|
11
|
+
);
|
|
12
|
+
}) as ScopeField<never>;
|
|
13
|
+
fn.source = null;
|
|
14
|
+
fn.query = null;
|
|
15
|
+
return fn;
|
|
16
|
+
})();
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* React Context for the AssistantClient
|
|
20
|
+
*/
|
|
21
|
+
export const AssistantContext = createContext<AssistantClient>(
|
|
22
|
+
new Proxy({} as AssistantClient, {
|
|
23
|
+
get(_, prop: string) {
|
|
24
|
+
// Allow access to subscribe and flushSync without error
|
|
25
|
+
if (prop === "subscribe") return NO_OP_SUBSCRIBE;
|
|
26
|
+
|
|
27
|
+
if (prop === "flushSync") return NO_OP_FLUSH_SYNC;
|
|
28
|
+
|
|
29
|
+
// If this is a registered scope, return a function that errors when called or accessed
|
|
30
|
+
if (hasRegisteredScope(prop as keyof AssistantScopes))
|
|
31
|
+
return NO_OP_SCOPE_FIELD;
|
|
32
|
+
|
|
33
|
+
return null;
|
|
34
|
+
},
|
|
35
|
+
}),
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
export const useAssistantContextValue = (): AssistantClient => {
|
|
39
|
+
return useContext(AssistantContext);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Provider component for AssistantClient
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```typescript
|
|
47
|
+
* <AssistantProvider client={client}>
|
|
48
|
+
* <YourApp />
|
|
49
|
+
* </AssistantProvider>
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
export const AssistantProvider = ({
|
|
53
|
+
client,
|
|
54
|
+
children,
|
|
55
|
+
}: {
|
|
56
|
+
client: AssistantClient;
|
|
57
|
+
children: React.ReactNode;
|
|
58
|
+
}): React.ReactElement => {
|
|
59
|
+
return (
|
|
60
|
+
<AssistantContext.Provider value={client}>
|
|
61
|
+
{children}
|
|
62
|
+
</AssistantContext.Provider>
|
|
63
|
+
);
|
|
64
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { resource } from "@assistant-ui/tap";
|
|
2
|
+
import type { ScopeDefinition, ScopeValue, DerivedScopeProps } from "./types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Creates a derived scope field that memoizes based on source and query.
|
|
6
|
+
* The get callback always calls the most recent version (useEffectEvent pattern).
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* const MessageScope = DerivedScope<MessageScopeDefinition>({
|
|
11
|
+
* source: "thread",
|
|
12
|
+
* query: { type: "index", index: 0 },
|
|
13
|
+
* get: () => messageApi,
|
|
14
|
+
* });
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export const DerivedScope = resource(
|
|
18
|
+
<T extends ScopeDefinition>(config: DerivedScopeProps<T>): ScopeValue<T> => {
|
|
19
|
+
return config;
|
|
20
|
+
},
|
|
21
|
+
);
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { AssistantScopes } from "./types";
|
|
2
|
+
import type { ResourceElement } from "@assistant-ui/tap";
|
|
3
|
+
|
|
4
|
+
type ScopeRegistryEntry<K extends keyof AssistantScopes> = {
|
|
5
|
+
name: K;
|
|
6
|
+
defaultInitialize:
|
|
7
|
+
| ResourceElement<AssistantScopes[K]["value"]>
|
|
8
|
+
| { error: string };
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const scopeRegistry = new Map<
|
|
12
|
+
keyof AssistantScopes,
|
|
13
|
+
ResourceElement<any> | { error: string }
|
|
14
|
+
>();
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Register a default scope implementation.
|
|
18
|
+
* This allows scopes to have default values when not explicitly provided.
|
|
19
|
+
*
|
|
20
|
+
* @example With a resource:
|
|
21
|
+
* ```typescript
|
|
22
|
+
* registerAssistantScope({
|
|
23
|
+
* name: "myScope",
|
|
24
|
+
* defaultInitialize: MyResource(),
|
|
25
|
+
* });
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* @example With an error:
|
|
29
|
+
* ```typescript
|
|
30
|
+
* registerAssistantScope({
|
|
31
|
+
* name: "myScope",
|
|
32
|
+
* defaultInitialize: { error: "MyScope is not configured" },
|
|
33
|
+
* });
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export function registerAssistantScope<K extends keyof AssistantScopes>(
|
|
37
|
+
config: ScopeRegistryEntry<K>,
|
|
38
|
+
): void {
|
|
39
|
+
scopeRegistry.set(config.name, config.defaultInitialize);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Get the default initializer for a scope, if registered.
|
|
44
|
+
*/
|
|
45
|
+
export function getDefaultScopeInitializer<K extends keyof AssistantScopes>(
|
|
46
|
+
name: K,
|
|
47
|
+
):
|
|
48
|
+
| (ResourceElement<AssistantScopes[K]["value"]> | { error: string })
|
|
49
|
+
| undefined {
|
|
50
|
+
return scopeRegistry.get(name);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Get all registered scope names.
|
|
55
|
+
*/
|
|
56
|
+
export function hasRegisteredScope(name: keyof AssistantScopes): boolean {
|
|
57
|
+
return scopeRegistry.has(name);
|
|
58
|
+
}
|
package/src/asStore.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import {
|
|
2
|
+
tapMemo,
|
|
3
|
+
tapEffect,
|
|
4
|
+
ResourceElement,
|
|
5
|
+
resource,
|
|
6
|
+
createResource,
|
|
7
|
+
} from "@assistant-ui/tap";
|
|
8
|
+
import { Unsubscribe } from "./types";
|
|
9
|
+
|
|
10
|
+
export interface Store<TState> {
|
|
11
|
+
/**
|
|
12
|
+
* Get the current state of the store.
|
|
13
|
+
*/
|
|
14
|
+
getState(): TState;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Subscribe to the store.
|
|
18
|
+
*/
|
|
19
|
+
subscribe(listener: () => void): Unsubscribe;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Synchronously flush all the updates to the store.
|
|
23
|
+
*/
|
|
24
|
+
flushSync(): void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const asStore = resource(
|
|
28
|
+
<TState, TProps>(element: ResourceElement<TState, TProps>): Store<TState> => {
|
|
29
|
+
const resource = tapMemo(
|
|
30
|
+
() => createResource(element, true),
|
|
31
|
+
[element.type],
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
tapEffect(() => {
|
|
35
|
+
resource.updateInput(element.props);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
return resource;
|
|
39
|
+
},
|
|
40
|
+
);
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export { useAssistantClient } from "./useAssistantClient";
|
|
2
|
+
export { useAssistantState } from "./useAssistantState";
|
|
3
|
+
export { AssistantProvider } from "./AssistantContext";
|
|
4
|
+
export type { AssistantScopes, AssistantClient, AssistantState } from "./types";
|
|
5
|
+
export { DerivedScope } from "./DerivedScope";
|
|
6
|
+
export type { ApiObject } from "./tapApi";
|
|
7
|
+
export { tapApi } from "./tapApi";
|
|
8
|
+
export { tapLookupResources } from "./tapLookupResources";
|
|
9
|
+
export { tapStoreList } from "./tapStoreList";
|
|
10
|
+
export type { TapStoreListConfig } from "./tapStoreList";
|
|
11
|
+
export { registerAssistantScope } from "./ScopeRegistry";
|
|
12
|
+
|
|
13
|
+
export type { AssistantScopeRegistry } from "./types";
|
package/src/tapApi.ts
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { tapEffect, tapMemo, tapRef } from "@assistant-ui/tap";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* API object type
|
|
5
|
+
*/
|
|
6
|
+
export interface ApiObject {
|
|
7
|
+
[key: string]: ((...args: any[]) => any) | ApiObject;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Readonly API handler for creating stable proxies
|
|
12
|
+
*/
|
|
13
|
+
class ReadonlyApiHandler<TApi extends ApiObject> implements ProxyHandler<TApi> {
|
|
14
|
+
constructor(private readonly getApi: () => TApi) {}
|
|
15
|
+
|
|
16
|
+
get(_: unknown, prop: string | symbol) {
|
|
17
|
+
return this.getApi()[prop as keyof TApi];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
ownKeys(): ArrayLike<string | symbol> {
|
|
21
|
+
return Object.keys(this.getApi() as object);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
has(_: unknown, prop: string | symbol) {
|
|
25
|
+
return prop in (this.getApi() as object);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
getOwnPropertyDescriptor(_: unknown, prop: string | symbol) {
|
|
29
|
+
return Object.getOwnPropertyDescriptor(this.getApi(), prop);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
set() {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
defineProperty() {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
deleteProperty() {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Wraps an API object to make it stable across renders while keeping getState reactive.
|
|
45
|
+
* This is the recommended pattern for scope resources.
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* ```typescript
|
|
49
|
+
* export const FooResource = resource(() => {
|
|
50
|
+
* const [state, setState] = tapState({ bar: "Hello" });
|
|
51
|
+
*
|
|
52
|
+
* const updateBar = (newBar: string) => {
|
|
53
|
+
* setState({ bar: newBar });
|
|
54
|
+
* };
|
|
55
|
+
*
|
|
56
|
+
* return tapApi({
|
|
57
|
+
* getState: () => state,
|
|
58
|
+
* updateBar,
|
|
59
|
+
* });
|
|
60
|
+
* });
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
export const tapApi = <TApi extends ApiObject & { getState: () => any }>(
|
|
64
|
+
api: TApi,
|
|
65
|
+
options?: {
|
|
66
|
+
key?: string | undefined;
|
|
67
|
+
},
|
|
68
|
+
) => {
|
|
69
|
+
const ref = tapRef(api);
|
|
70
|
+
tapEffect(() => {
|
|
71
|
+
ref.current = api;
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const apiProxy = tapMemo(
|
|
75
|
+
() =>
|
|
76
|
+
new Proxy<TApi>({} as TApi, new ReadonlyApiHandler(() => ref.current)),
|
|
77
|
+
[],
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
const key = options?.key;
|
|
81
|
+
const state = api.getState();
|
|
82
|
+
|
|
83
|
+
return tapMemo(
|
|
84
|
+
() => ({
|
|
85
|
+
key,
|
|
86
|
+
state,
|
|
87
|
+
api: apiProxy,
|
|
88
|
+
}),
|
|
89
|
+
[state, key],
|
|
90
|
+
);
|
|
91
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { ResourceElement, tapResources } from "@assistant-ui/tap";
|
|
2
|
+
import { ApiObject } from "./tapApi";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Creates a lookup-based resource collection for managing lists of items.
|
|
6
|
+
* Returns both the combined state array and an API function to lookup specific items.
|
|
7
|
+
*
|
|
8
|
+
* @param elements - Array of resource elements, each returning { key, state, api }
|
|
9
|
+
* @returns Object with { state: TState[], api: (lookup) => TApi }
|
|
10
|
+
*
|
|
11
|
+
* The api function accepts { index: number } or { key: string } for lookups.
|
|
12
|
+
* Consumers can wrap it to rename the key field (e.g., to "id" or "toolCallId").
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```typescript
|
|
16
|
+
* const foos = tapLookupResources(
|
|
17
|
+
* items.map((item) => FooItem({ id: item.id }, { key: item.id }))
|
|
18
|
+
* );
|
|
19
|
+
*
|
|
20
|
+
* // Access state array
|
|
21
|
+
* const allStates = foos.state;
|
|
22
|
+
*
|
|
23
|
+
* // Wrap to rename key field to "id"
|
|
24
|
+
* const wrappedApi = (lookup: { index: number } | { id: string }) => {
|
|
25
|
+
* if ("id" in lookup) {
|
|
26
|
+
* return foos.api({ key: lookup.id });
|
|
27
|
+
* } else {
|
|
28
|
+
* return foos.api(lookup);
|
|
29
|
+
* }
|
|
30
|
+
* };
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export const tapLookupResources = <TState, TApi extends ApiObject>(
|
|
34
|
+
elements: ResourceElement<{
|
|
35
|
+
key: string | undefined;
|
|
36
|
+
state: TState;
|
|
37
|
+
api: TApi;
|
|
38
|
+
}>[],
|
|
39
|
+
): {
|
|
40
|
+
state: TState[];
|
|
41
|
+
api: (lookup: { index: number } | { key: string }) => TApi;
|
|
42
|
+
} => {
|
|
43
|
+
const resources = tapResources(elements);
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
state: resources.map((r) => r.state),
|
|
47
|
+
api: (lookup: { index: number } | { key: string }) => {
|
|
48
|
+
const value =
|
|
49
|
+
"index" in lookup
|
|
50
|
+
? resources[lookup.index]?.api
|
|
51
|
+
: resources.find((r) => r.key === lookup.key)?.api;
|
|
52
|
+
|
|
53
|
+
if (!value) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
`tapLookupResources: Resource not found for lookup: ${JSON.stringify(lookup)}`,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return value;
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
};
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { tapState } from "@assistant-ui/tap";
|
|
2
|
+
import type { ContravariantResource } from "@assistant-ui/tap";
|
|
3
|
+
import { tapLookupResources } from "./tapLookupResources";
|
|
4
|
+
import { ApiObject } from "./tapApi";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Resource props that will be passed to each item resource
|
|
8
|
+
*/
|
|
9
|
+
export type TapStoreListResourceProps<TProps> = {
|
|
10
|
+
initialValue: TProps;
|
|
11
|
+
remove: () => void;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Configuration for tapStoreList hook
|
|
16
|
+
*/
|
|
17
|
+
export type TapStoreListConfig<TProps, TState, TApi extends ApiObject> = {
|
|
18
|
+
/**
|
|
19
|
+
* Initial values for the list items
|
|
20
|
+
*/
|
|
21
|
+
initialValues: TProps[];
|
|
22
|
+
|
|
23
|
+
// TODO we can't use Resource type here because of contravariance
|
|
24
|
+
// I think we need a special type in tap that correctly handles the contravariance
|
|
25
|
+
// or change the behavior of the Resource type
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Resource function that creates an element for each item
|
|
29
|
+
* Should return a ResourceElement with { key, state, api }
|
|
30
|
+
*
|
|
31
|
+
* The resource will receive { initialValue, remove } as props.
|
|
32
|
+
*/
|
|
33
|
+
resource: ContravariantResource<
|
|
34
|
+
{
|
|
35
|
+
key: string | undefined;
|
|
36
|
+
state: TState;
|
|
37
|
+
api: TApi;
|
|
38
|
+
},
|
|
39
|
+
TapStoreListResourceProps<TProps>
|
|
40
|
+
>;
|
|
41
|
+
/**
|
|
42
|
+
* Optional ID generator function for new items
|
|
43
|
+
* If not provided, items must include an ID when added
|
|
44
|
+
*/
|
|
45
|
+
idGenerator?: () => string;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Creates a stateful list with add functionality, rendering each item via the provided resource.
|
|
50
|
+
* Returns state array, api lookup function, and add method.
|
|
51
|
+
*
|
|
52
|
+
* @param config - Configuration object with initialValues, resource, and optional idGenerator
|
|
53
|
+
* @returns Object with { state: TState[], api: (lookup) => TApi, add: (id?) => void }
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* ```typescript
|
|
57
|
+
* const todoList = tapStoreList({
|
|
58
|
+
* initialValues: [
|
|
59
|
+
* { id: "1", text: "First todo" },
|
|
60
|
+
* { id: "2", text: "Second todo" }
|
|
61
|
+
* ],
|
|
62
|
+
* resource: (props) => TodoItemResource(props, { key: props.id }),
|
|
63
|
+
* idGenerator: () => `todo-${Date.now()}`
|
|
64
|
+
* });
|
|
65
|
+
*
|
|
66
|
+
* // Access state array
|
|
67
|
+
* const allTodos = todoList.state;
|
|
68
|
+
*
|
|
69
|
+
* // Lookup specific item
|
|
70
|
+
* const firstTodo = todoList.api({ index: 0 });
|
|
71
|
+
* const specificTodo = todoList.api({ key: "1" });
|
|
72
|
+
*
|
|
73
|
+
* // Add new item
|
|
74
|
+
* todoList.add(); // Uses idGenerator
|
|
75
|
+
* todoList.add("custom-id"); // Uses provided id
|
|
76
|
+
* ```
|
|
77
|
+
*/
|
|
78
|
+
export const tapStoreList = <
|
|
79
|
+
TProps extends { id: string },
|
|
80
|
+
TState,
|
|
81
|
+
TApi extends ApiObject,
|
|
82
|
+
>(
|
|
83
|
+
config: TapStoreListConfig<TProps, TState, TApi>,
|
|
84
|
+
): {
|
|
85
|
+
state: TState[];
|
|
86
|
+
api: (lookup: { index: number } | { id: string }) => TApi;
|
|
87
|
+
add: (id?: string) => void;
|
|
88
|
+
} => {
|
|
89
|
+
const { initialValues, resource: Resource, idGenerator } = config;
|
|
90
|
+
|
|
91
|
+
const [items, setItems] = tapState<TProps[]>(initialValues);
|
|
92
|
+
|
|
93
|
+
const lookup = tapLookupResources(
|
|
94
|
+
items.map((item) =>
|
|
95
|
+
Resource(
|
|
96
|
+
{
|
|
97
|
+
initialValue: item,
|
|
98
|
+
remove: () => {
|
|
99
|
+
setItems(items.filter((i) => i !== item));
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
key: item.id,
|
|
104
|
+
},
|
|
105
|
+
),
|
|
106
|
+
),
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
const add = (id?: string) => {
|
|
110
|
+
const newId = id ?? idGenerator?.();
|
|
111
|
+
if (!newId) {
|
|
112
|
+
throw new Error(
|
|
113
|
+
"tapStoreList: Either provide an id to add() or configure an idGenerator",
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Create a new item with the generated/provided id
|
|
118
|
+
// This assumes TProps has an 'id' field - users will need to ensure their props type supports this
|
|
119
|
+
const newItem = { id: newId } as TProps;
|
|
120
|
+
setItems([...items, newItem]);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
state: lookup.state,
|
|
125
|
+
api: (query: { index: number } | { id: string }) => {
|
|
126
|
+
if ("index" in query) {
|
|
127
|
+
return lookup.api({ index: query.index });
|
|
128
|
+
}
|
|
129
|
+
return lookup.api({ key: query.id });
|
|
130
|
+
},
|
|
131
|
+
add,
|
|
132
|
+
};
|
|
133
|
+
};
|