@assistant-ui/core 0.1.9 → 0.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/index.d.ts +3 -0
- package/dist/adapters/index.d.ts.map +1 -1
- package/dist/adapters/index.js +1 -0
- package/dist/adapters/index.js.map +1 -1
- package/dist/adapters/voice.d.ts +49 -0
- package/dist/adapters/voice.d.ts.map +1 -0
- package/dist/adapters/voice.js +109 -0
- package/dist/adapters/voice.js.map +1 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/model-context/types.d.ts +4 -0
- package/dist/model-context/types.d.ts.map +1 -1
- package/dist/model-context/types.js.map +1 -1
- package/dist/react/client/Interactables.d.ts.map +1 -1
- package/dist/react/client/Interactables.js +155 -65
- package/dist/react/client/Interactables.js.map +1 -1
- package/dist/react/client/interactable-model-context.d.ts +8 -0
- package/dist/react/client/interactable-model-context.d.ts.map +1 -0
- package/dist/react/client/interactable-model-context.js +62 -0
- package/dist/react/client/interactable-model-context.js.map +1 -0
- package/dist/react/index.d.ts +6 -3
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +5 -2
- package/dist/react/index.js.map +1 -1
- package/dist/react/model-context/useAssistantContext.d.ts +4 -0
- package/dist/react/model-context/useAssistantContext.d.ts.map +1 -0
- package/dist/react/model-context/useAssistantContext.js +18 -0
- package/dist/react/model-context/useAssistantContext.js.map +1 -0
- package/dist/react/model-context/useAssistantInteractable.d.ts +18 -0
- package/dist/react/model-context/useAssistantInteractable.d.ts.map +1 -0
- package/dist/react/model-context/useAssistantInteractable.js +31 -0
- package/dist/react/model-context/useAssistantInteractable.js.map +1 -0
- package/dist/react/model-context/useInteractableState.d.ts +15 -0
- package/dist/react/model-context/useInteractableState.d.ts.map +1 -0
- package/dist/react/model-context/useInteractableState.js +36 -0
- package/dist/react/model-context/useInteractableState.js.map +1 -0
- package/dist/react/model-context/useToolArgsStatus.d.ts +8 -0
- package/dist/react/model-context/useToolArgsStatus.d.ts.map +1 -0
- package/dist/react/model-context/useToolArgsStatus.js +31 -0
- package/dist/react/model-context/useToolArgsStatus.js.map +1 -0
- package/dist/react/primitive-hooks/useVoice.d.ts +10 -0
- package/dist/react/primitive-hooks/useVoice.d.ts.map +1 -0
- package/dist/react/primitive-hooks/useVoice.js +28 -0
- package/dist/react/primitive-hooks/useVoice.js.map +1 -0
- package/dist/react/runtimes/RemoteThreadListHookInstanceManager.d.ts +14 -0
- package/dist/react/runtimes/RemoteThreadListHookInstanceManager.d.ts.map +1 -1
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts +14 -0
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts.map +1 -1
- package/dist/react/runtimes/useLocalRuntime.d.ts +1 -0
- package/dist/react/runtimes/useLocalRuntime.d.ts.map +1 -1
- package/dist/react/types/scopes/interactables.d.ts +17 -0
- package/dist/react/types/scopes/interactables.d.ts.map +1 -1
- package/dist/runtime/api/thread-runtime.d.ts +21 -1
- package/dist/runtime/api/thread-runtime.d.ts.map +1 -1
- package/dist/runtime/api/thread-runtime.js +25 -0
- package/dist/runtime/api/thread-runtime.js.map +1 -1
- package/dist/runtime/base/base-thread-runtime-core.d.ts +24 -1
- package/dist/runtime/base/base-thread-runtime-core.d.ts.map +1 -1
- package/dist/runtime/base/base-thread-runtime-core.js +205 -1
- package/dist/runtime/base/base-thread-runtime-core.js.map +1 -1
- package/dist/runtime/interfaces/thread-runtime-core.d.ts +14 -0
- package/dist/runtime/interfaces/thread-runtime-core.d.ts.map +1 -1
- package/dist/runtimes/external-store/external-store-adapter.d.ts +2 -0
- package/dist/runtimes/external-store/external-store-adapter.d.ts.map +1 -1
- package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts +2 -1
- package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts.map +1 -1
- package/dist/runtimes/external-store/external-store-thread-runtime-core.js +3 -1
- package/dist/runtimes/external-store/external-store-thread-runtime-core.js.map +1 -1
- package/dist/runtimes/local/local-runtime-options.d.ts +2 -0
- package/dist/runtimes/local/local-runtime-options.d.ts.map +1 -1
- package/dist/runtimes/local/local-thread-runtime-core.d.ts +2 -0
- package/dist/runtimes/local/local-thread-runtime-core.d.ts.map +1 -1
- package/dist/runtimes/local/local-thread-runtime-core.js +6 -0
- package/dist/runtimes/local/local-thread-runtime-core.js.map +1 -1
- package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.d.ts +8 -0
- package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.d.ts.map +1 -1
- package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.js +15 -6
- package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.js.map +1 -1
- package/dist/runtimes/remote-thread-list/empty-thread-core.d.ts.map +1 -1
- package/dist/runtimes/remote-thread-list/empty-thread-core.js +17 -1
- package/dist/runtimes/remote-thread-list/empty-thread-core.js.map +1 -1
- package/dist/store/runtime-clients/thread-runtime-client.d.ts.map +1 -1
- package/dist/store/runtime-clients/thread-runtime-client.js +7 -8
- package/dist/store/runtime-clients/thread-runtime-client.js.map +1 -1
- package/dist/store/scopes/thread.d.ts +9 -9
- package/dist/store/scopes/thread.d.ts.map +1 -1
- package/dist/types/message.d.ts +1 -0
- package/dist/types/message.d.ts.map +1 -1
- package/package.json +9 -15
- package/src/adapters/index.ts +5 -0
- package/src/adapters/voice.ts +166 -0
- package/src/index.ts +10 -0
- package/src/model-context/types.ts +5 -0
- package/src/react/client/Interactables.ts +221 -122
- package/src/react/client/interactable-model-context.ts +83 -0
- package/src/react/index.ts +19 -8
- package/src/react/model-context/useAssistantContext.ts +22 -0
- package/src/react/model-context/useAssistantInteractable.ts +47 -0
- package/src/react/model-context/useInteractableState.ts +63 -0
- package/src/react/model-context/useToolArgsStatus.ts +51 -0
- package/src/react/primitive-hooks/useVoice.ts +41 -0
- package/src/react/types/scopes/interactables.ts +22 -0
- package/src/runtime/api/thread-runtime.ts +41 -0
- package/src/runtime/base/base-thread-runtime-core.ts +243 -2
- package/src/runtime/interfaces/thread-runtime-core.ts +17 -0
- package/src/runtimes/external-store/external-store-adapter.ts +2 -0
- package/src/runtimes/external-store/external-store-thread-runtime-core.ts +3 -1
- package/src/runtimes/local/local-runtime-options.ts +2 -0
- package/src/runtimes/local/local-thread-runtime-core.ts +7 -0
- package/src/runtimes/readonly/ReadonlyThreadRuntimeCore.ts +20 -6
- package/src/runtimes/remote-thread-list/empty-thread-core.ts +22 -1
- package/src/store/runtime-clients/thread-runtime-client.ts +7 -8
- package/src/store/scopes/thread.ts +9 -8
- package/src/types/message.ts +1 -0
- package/dist/react/model-context/makeInteractable.d.ts +0 -10
- package/dist/react/model-context/makeInteractable.d.ts.map +0 -1
- package/dist/react/model-context/makeInteractable.js +0 -10
- package/dist/react/model-context/makeInteractable.js.map +0 -1
- package/dist/react/model-context/useInteractable.d.ts +0 -16
- package/dist/react/model-context/useInteractable.d.ts.map +0 -1
- package/dist/react/model-context/useInteractable.js +0 -36
- package/dist/react/model-context/useInteractable.js.map +0 -1
- package/src/react/model-context/makeInteractable.ts +0 -21
- package/src/react/model-context/useInteractable.ts +0 -73
|
@@ -13,34 +13,21 @@ import {
|
|
|
13
13
|
} from "@assistant-ui/store";
|
|
14
14
|
import type {
|
|
15
15
|
InteractablesState,
|
|
16
|
-
InteractableDefinition,
|
|
17
16
|
InteractableRegistration,
|
|
18
17
|
InteractableStateSchema,
|
|
18
|
+
InteractablePersistedState,
|
|
19
|
+
InteractablePersistenceAdapter,
|
|
19
20
|
} from "../types/scopes/interactables";
|
|
20
21
|
import { toJSONSchema, toPartialJSONSchema } from "assistant-stream";
|
|
21
|
-
import type { Tool } from "assistant-stream";
|
|
22
22
|
import { ModelContext } from "../../store";
|
|
23
|
+
import { buildInteractableModelContext } from "./interactable-model-context";
|
|
23
24
|
|
|
24
|
-
|
|
25
|
-
if (
|
|
26
|
-
typeof prev !== "object" ||
|
|
27
|
-
prev === null ||
|
|
28
|
-
typeof partial !== "object" ||
|
|
29
|
-
partial === null ||
|
|
30
|
-
Array.isArray(prev) ||
|
|
31
|
-
Array.isArray(partial)
|
|
32
|
-
) {
|
|
33
|
-
return partial;
|
|
34
|
-
}
|
|
35
|
-
return {
|
|
36
|
-
...(prev as Record<string, unknown>),
|
|
37
|
-
...(partial as Record<string, unknown>),
|
|
38
|
-
};
|
|
39
|
-
}
|
|
25
|
+
const PERSISTENCE_DEBOUNCE_MS = 500;
|
|
40
26
|
|
|
41
27
|
export const Interactables = resource((): ClientOutput<"interactables"> => {
|
|
42
28
|
const [state, setState] = tapState<InteractablesState>(() => ({
|
|
43
29
|
definitions: {},
|
|
30
|
+
persistence: {},
|
|
44
31
|
}));
|
|
45
32
|
|
|
46
33
|
const clientRef = tapAssistantClientRef();
|
|
@@ -51,12 +38,167 @@ export const Interactables = resource((): ClientOutput<"interactables"> => {
|
|
|
51
38
|
}, [state]);
|
|
52
39
|
|
|
53
40
|
const subscribersRef = tapRef(new Set<() => void>());
|
|
54
|
-
|
|
55
41
|
const partialSchemaCacheRef = tapRef(
|
|
56
42
|
new Map<string, InteractableStateSchema>(),
|
|
57
43
|
);
|
|
58
44
|
const detachedStateRef = tapRef(new Map<string, unknown>());
|
|
59
45
|
|
|
46
|
+
const adapterRef = tapRef<InteractablePersistenceAdapter | undefined>(
|
|
47
|
+
undefined,
|
|
48
|
+
);
|
|
49
|
+
const debounceTimerRef = tapRef<ReturnType<typeof setTimeout> | undefined>(
|
|
50
|
+
undefined,
|
|
51
|
+
);
|
|
52
|
+
const syncSeqRef = tapRef(0);
|
|
53
|
+
const hasPendingLocalChangeRef = tapRef(false);
|
|
54
|
+
const flushResolversRef = tapRef<Array<() => void>>([]);
|
|
55
|
+
const dirtyIdsRef = tapRef(new Set<string>());
|
|
56
|
+
|
|
57
|
+
const runPersistence = tapCallback(async () => {
|
|
58
|
+
const adapter = adapterRef.current;
|
|
59
|
+
if (!adapter) {
|
|
60
|
+
for (const resolve of flushResolversRef.current) resolve();
|
|
61
|
+
flushResolversRef.current = [];
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const seq = ++syncSeqRef.current;
|
|
66
|
+
const dirtyIds = new Set(dirtyIdsRef.current);
|
|
67
|
+
dirtyIdsRef.current.clear();
|
|
68
|
+
hasPendingLocalChangeRef.current = true;
|
|
69
|
+
|
|
70
|
+
// Snapshot before any await so unregistered definitions are still included.
|
|
71
|
+
const exported = stateRef.current.definitions;
|
|
72
|
+
const payload: InteractablePersistedState = {};
|
|
73
|
+
for (const [id, def] of Object.entries(exported)) {
|
|
74
|
+
payload[id] = { name: def.name, state: def.state };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
setState((prev) => ({
|
|
78
|
+
...prev,
|
|
79
|
+
persistence: {
|
|
80
|
+
...prev.persistence,
|
|
81
|
+
...Object.fromEntries(
|
|
82
|
+
[...dirtyIds].map((id) => [
|
|
83
|
+
id,
|
|
84
|
+
{ isPending: true, error: undefined },
|
|
85
|
+
]),
|
|
86
|
+
),
|
|
87
|
+
},
|
|
88
|
+
}));
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
await adapter.save(payload);
|
|
92
|
+
if (syncSeqRef.current === seq) {
|
|
93
|
+
hasPendingLocalChangeRef.current = false;
|
|
94
|
+
setState((prev) => {
|
|
95
|
+
const persistence = { ...prev.persistence };
|
|
96
|
+
for (const id of dirtyIds) delete persistence[id];
|
|
97
|
+
return { ...prev, persistence };
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
} catch (e) {
|
|
101
|
+
if (syncSeqRef.current === seq) {
|
|
102
|
+
hasPendingLocalChangeRef.current = false;
|
|
103
|
+
setState((prev) => ({
|
|
104
|
+
...prev,
|
|
105
|
+
persistence: {
|
|
106
|
+
...prev.persistence,
|
|
107
|
+
...Object.fromEntries(
|
|
108
|
+
[...dirtyIds].map((id) => [id, { isPending: false, error: e }]),
|
|
109
|
+
),
|
|
110
|
+
},
|
|
111
|
+
}));
|
|
112
|
+
}
|
|
113
|
+
} finally {
|
|
114
|
+
if (dirtyIdsRef.current.size > 0 && adapterRef.current) {
|
|
115
|
+
runPersistence();
|
|
116
|
+
} else {
|
|
117
|
+
for (const resolve of flushResolversRef.current) resolve();
|
|
118
|
+
flushResolversRef.current = [];
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}, []);
|
|
122
|
+
|
|
123
|
+
const schedulePersistence = tapCallback(
|
|
124
|
+
(id: string) => {
|
|
125
|
+
if (!adapterRef.current) return;
|
|
126
|
+
dirtyIdsRef.current.add(id);
|
|
127
|
+
if (debounceTimerRef.current !== undefined) {
|
|
128
|
+
clearTimeout(debounceTimerRef.current);
|
|
129
|
+
}
|
|
130
|
+
debounceTimerRef.current = setTimeout(() => {
|
|
131
|
+
debounceTimerRef.current = undefined;
|
|
132
|
+
if (!hasPendingLocalChangeRef.current) {
|
|
133
|
+
runPersistence();
|
|
134
|
+
} else {
|
|
135
|
+
debounceTimerRef.current = setTimeout(() => {
|
|
136
|
+
debounceTimerRef.current = undefined;
|
|
137
|
+
runPersistence();
|
|
138
|
+
}, PERSISTENCE_DEBOUNCE_MS);
|
|
139
|
+
}
|
|
140
|
+
}, PERSISTENCE_DEBOUNCE_MS);
|
|
141
|
+
},
|
|
142
|
+
[runPersistence],
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
const exportState = tapCallback((): InteractablePersistedState => {
|
|
146
|
+
const result: InteractablePersistedState = {};
|
|
147
|
+
for (const [id, def] of Object.entries(stateRef.current.definitions)) {
|
|
148
|
+
result[id] = { name: def.name, state: def.state };
|
|
149
|
+
}
|
|
150
|
+
return result;
|
|
151
|
+
}, []);
|
|
152
|
+
|
|
153
|
+
const importState = tapCallback((saved: InteractablePersistedState) => {
|
|
154
|
+
for (const [id, entry] of Object.entries(saved)) {
|
|
155
|
+
detachedStateRef.current.set(id, entry.state);
|
|
156
|
+
}
|
|
157
|
+
setState((prev) => {
|
|
158
|
+
let changed = false;
|
|
159
|
+
const definitions = { ...prev.definitions };
|
|
160
|
+
for (const [id, entry] of Object.entries(saved)) {
|
|
161
|
+
if (definitions[id]) {
|
|
162
|
+
definitions[id] = { ...definitions[id], state: entry.state };
|
|
163
|
+
changed = true;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return changed ? { ...prev, definitions } : prev;
|
|
167
|
+
});
|
|
168
|
+
}, []);
|
|
169
|
+
|
|
170
|
+
const setPersistenceAdapter = tapCallback(
|
|
171
|
+
(adapter: InteractablePersistenceAdapter | undefined) => {
|
|
172
|
+
adapterRef.current = adapter;
|
|
173
|
+
},
|
|
174
|
+
[],
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
const flush = tapCallback(async () => {
|
|
178
|
+
if (debounceTimerRef.current !== undefined) {
|
|
179
|
+
clearTimeout(debounceTimerRef.current);
|
|
180
|
+
debounceTimerRef.current = undefined;
|
|
181
|
+
}
|
|
182
|
+
if (!adapterRef.current) return;
|
|
183
|
+
if (!hasPendingLocalChangeRef.current && dirtyIdsRef.current.size === 0)
|
|
184
|
+
return;
|
|
185
|
+
const p = new Promise<void>((resolve) => {
|
|
186
|
+
flushResolversRef.current.push(resolve);
|
|
187
|
+
});
|
|
188
|
+
if (!hasPendingLocalChangeRef.current) {
|
|
189
|
+
runPersistence();
|
|
190
|
+
}
|
|
191
|
+
return p;
|
|
192
|
+
}, [runPersistence]);
|
|
193
|
+
|
|
194
|
+
const flushIfPending = tapCallback(() => {
|
|
195
|
+
if (adapterRef.current && debounceTimerRef.current !== undefined) {
|
|
196
|
+
clearTimeout(debounceTimerRef.current);
|
|
197
|
+
debounceTimerRef.current = undefined;
|
|
198
|
+
runPersistence();
|
|
199
|
+
}
|
|
200
|
+
}, [runPersistence]);
|
|
201
|
+
|
|
60
202
|
const setDefState = tapCallback(
|
|
61
203
|
(id: string, updater: (prev: unknown) => unknown) => {
|
|
62
204
|
setState((prev) => {
|
|
@@ -70,8 +212,9 @@ export const Interactables = resource((): ClientOutput<"interactables"> => {
|
|
|
70
212
|
},
|
|
71
213
|
};
|
|
72
214
|
});
|
|
215
|
+
if (stateRef.current.definitions[id]) schedulePersistence(id);
|
|
73
216
|
},
|
|
74
|
-
[],
|
|
217
|
+
[schedulePersistence],
|
|
75
218
|
);
|
|
76
219
|
|
|
77
220
|
const setDefSelected = tapCallback((id: string, selected: boolean) => {
|
|
@@ -92,63 +235,13 @@ export const Interactables = resource((): ClientOutput<"interactables"> => {
|
|
|
92
235
|
() => ({
|
|
93
236
|
getModelContext: () => {
|
|
94
237
|
const defs = stateRef.current.definitions;
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
byName.set(def.name, list);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const systemParts: string[] = [];
|
|
106
|
-
const tools: Record<string, Tool<any, any>> = {};
|
|
107
|
-
|
|
108
|
-
for (const [name, instances] of byName) {
|
|
109
|
-
const isMulti = instances.length > 1;
|
|
110
|
-
|
|
111
|
-
for (const def of instances) {
|
|
112
|
-
const selectedTag = def.selected ? " (SELECTED)" : "";
|
|
113
|
-
const idTag = isMulti ? ` [id="${def.id}"]` : "";
|
|
114
|
-
|
|
115
|
-
systemParts.push(
|
|
116
|
-
`Interactable component "${name}"${idTag}${selectedTag} (${def.description}). Current state: ${JSON.stringify(def.state)}`,
|
|
117
|
-
);
|
|
118
|
-
|
|
119
|
-
const safeName = name.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
120
|
-
const safeId = def.id.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
121
|
-
const toolName = isMulti
|
|
122
|
-
? `update_${safeName}_${safeId}`
|
|
123
|
-
: `update_${safeName}`;
|
|
124
|
-
|
|
125
|
-
const partialSchema =
|
|
126
|
-
partialSchemaCacheRef.current.get(def.id) ?? def.stateSchema;
|
|
127
|
-
|
|
128
|
-
tools[toolName] = {
|
|
129
|
-
type: "frontend" as const,
|
|
130
|
-
description: `Update the state of interactable component "${name}"${isMulti ? ` (id: ${def.id})` : ""}. Only include the fields you want to change; omitted fields keep their current values. ${def.description}`,
|
|
131
|
-
parameters: partialSchema,
|
|
132
|
-
streamCall: async (reader) => {
|
|
133
|
-
try {
|
|
134
|
-
for await (const partialArgs of reader.args.streamValues()) {
|
|
135
|
-
setDefState(def.id, (prev) =>
|
|
136
|
-
shallowMerge(prev, partialArgs),
|
|
137
|
-
);
|
|
138
|
-
}
|
|
139
|
-
} catch {
|
|
140
|
-
// Non-fatal: execute handles the final state
|
|
141
|
-
}
|
|
142
|
-
},
|
|
143
|
-
execute: async (partialState: unknown) => {
|
|
144
|
-
setDefState(def.id, (prev) => shallowMerge(prev, partialState));
|
|
145
|
-
return { success: true };
|
|
146
|
-
},
|
|
147
|
-
};
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
return { system: systemParts.join("\n"), tools };
|
|
238
|
+
return (
|
|
239
|
+
buildInteractableModelContext(
|
|
240
|
+
defs,
|
|
241
|
+
partialSchemaCacheRef.current,
|
|
242
|
+
setDefState,
|
|
243
|
+
) ?? {}
|
|
244
|
+
);
|
|
152
245
|
},
|
|
153
246
|
subscribe: (callback: () => void) => {
|
|
154
247
|
subscribersRef.current.add(callback);
|
|
@@ -162,67 +255,73 @@ export const Interactables = resource((): ClientOutput<"interactables"> => {
|
|
|
162
255
|
|
|
163
256
|
// biome-ignore lint/correctness/useExhaustiveDependencies: state dep triggers notification
|
|
164
257
|
tapEffect(() => {
|
|
165
|
-
for (const cb of subscribersRef.current)
|
|
166
|
-
cb();
|
|
167
|
-
}
|
|
258
|
+
for (const cb of subscribersRef.current) cb();
|
|
168
259
|
}, [state]);
|
|
169
260
|
|
|
170
261
|
tapEffect(() => {
|
|
171
262
|
return clientRef.current!.modelContext().register(provider);
|
|
172
263
|
}, [clientRef, provider]);
|
|
173
264
|
|
|
174
|
-
const register = tapCallback(
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
265
|
+
const register = tapCallback(
|
|
266
|
+
(def: InteractableRegistration) => {
|
|
267
|
+
try {
|
|
268
|
+
const jsonSchema = toJSONSchema(def.stateSchema);
|
|
269
|
+
partialSchemaCacheRef.current.set(
|
|
270
|
+
def.id,
|
|
271
|
+
toPartialJSONSchema(jsonSchema),
|
|
272
|
+
);
|
|
273
|
+
} catch (e) {
|
|
274
|
+
console.warn(
|
|
275
|
+
`[Interactables] Failed to create partial schema for "${def.name}". The update tool will require all fields.`,
|
|
276
|
+
e,
|
|
277
|
+
);
|
|
278
|
+
}
|
|
188
279
|
|
|
189
|
-
|
|
190
|
-
|
|
280
|
+
const detached = detachedStateRef.current.get(def.id);
|
|
281
|
+
detachedStateRef.current.delete(def.id);
|
|
191
282
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
283
|
+
setState((prev) => ({
|
|
284
|
+
...prev,
|
|
285
|
+
definitions: {
|
|
286
|
+
...prev.definitions,
|
|
287
|
+
[def.id]: {
|
|
288
|
+
id: def.id,
|
|
289
|
+
name: def.name,
|
|
290
|
+
description: def.description,
|
|
291
|
+
stateSchema: def.stateSchema,
|
|
292
|
+
state:
|
|
293
|
+
prev.definitions[def.id]?.state ?? detached ?? def.initialState,
|
|
294
|
+
selected: def.selected,
|
|
295
|
+
},
|
|
204
296
|
},
|
|
205
|
-
}
|
|
206
|
-
}));
|
|
297
|
+
}));
|
|
207
298
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
299
|
+
return () => {
|
|
300
|
+
flushIfPending();
|
|
301
|
+
setState((prev) => {
|
|
302
|
+
const existing = prev.definitions[def.id];
|
|
303
|
+
if (existing) {
|
|
304
|
+
detachedStateRef.current.set(def.id, existing.state);
|
|
305
|
+
}
|
|
306
|
+
partialSchemaCacheRef.current.delete(def.id);
|
|
307
|
+
const { [def.id]: _, ...rest } = prev.definitions;
|
|
308
|
+
const { [def.id]: __, ...restPersistence } = prev.persistence;
|
|
309
|
+
return { ...prev, definitions: rest, persistence: restPersistence };
|
|
310
|
+
});
|
|
311
|
+
};
|
|
312
|
+
},
|
|
313
|
+
[flushIfPending],
|
|
314
|
+
);
|
|
220
315
|
|
|
221
316
|
return {
|
|
222
317
|
getState: () => state,
|
|
223
318
|
register,
|
|
224
319
|
setState: setDefState,
|
|
225
320
|
setSelected: setDefSelected,
|
|
321
|
+
exportState,
|
|
322
|
+
importState,
|
|
323
|
+
setPersistenceAdapter,
|
|
324
|
+
flush,
|
|
226
325
|
};
|
|
227
326
|
});
|
|
228
327
|
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { Tool } from "assistant-stream";
|
|
2
|
+
import type {
|
|
3
|
+
InteractableDefinition,
|
|
4
|
+
InteractableStateSchema,
|
|
5
|
+
} from "../types/scopes/interactables";
|
|
6
|
+
|
|
7
|
+
export function shallowMerge(prev: unknown, partial: unknown): unknown {
|
|
8
|
+
if (
|
|
9
|
+
typeof prev !== "object" ||
|
|
10
|
+
prev === null ||
|
|
11
|
+
typeof partial !== "object" ||
|
|
12
|
+
partial === null ||
|
|
13
|
+
Array.isArray(prev) ||
|
|
14
|
+
Array.isArray(partial)
|
|
15
|
+
) {
|
|
16
|
+
return partial;
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
...(prev as Record<string, unknown>),
|
|
20
|
+
...(partial as Record<string, unknown>),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function buildInteractableModelContext(
|
|
25
|
+
definitions: Record<string, InteractableDefinition>,
|
|
26
|
+
partialSchemaCache: Map<string, InteractableStateSchema>,
|
|
27
|
+
setDefState: (id: string, updater: (prev: unknown) => unknown) => void,
|
|
28
|
+
): { system: string; tools: Record<string, Tool<any, any>> } | undefined {
|
|
29
|
+
const entries = Object.values(definitions);
|
|
30
|
+
if (entries.length === 0) return undefined;
|
|
31
|
+
|
|
32
|
+
const byName = new Map<string, InteractableDefinition[]>();
|
|
33
|
+
for (const def of entries) {
|
|
34
|
+
const list = byName.get(def.name) ?? [];
|
|
35
|
+
list.push(def);
|
|
36
|
+
byName.set(def.name, list);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const systemParts: string[] = [];
|
|
40
|
+
const tools: Record<string, Tool<any, any>> = {};
|
|
41
|
+
|
|
42
|
+
for (const [name, instances] of byName) {
|
|
43
|
+
const isMulti = instances.length > 1;
|
|
44
|
+
|
|
45
|
+
for (const def of instances) {
|
|
46
|
+
const selectedTag = def.selected ? " (SELECTED)" : "";
|
|
47
|
+
const idTag = isMulti ? ` [id="${def.id}"]` : "";
|
|
48
|
+
|
|
49
|
+
systemParts.push(
|
|
50
|
+
`Interactable component "${name}"${idTag}${selectedTag} (${def.description}). Current state: ${JSON.stringify(def.state)}`,
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
const safeName = name.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
54
|
+
const safeId = def.id.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
55
|
+
const toolName = isMulti
|
|
56
|
+
? `update_${safeName}_${safeId}`
|
|
57
|
+
: `update_${safeName}`;
|
|
58
|
+
|
|
59
|
+
const partialSchema = partialSchemaCache.get(def.id) ?? def.stateSchema;
|
|
60
|
+
|
|
61
|
+
tools[toolName] = {
|
|
62
|
+
type: "frontend" as const,
|
|
63
|
+
description: `Update the state of interactable component "${name}"${isMulti ? ` (id: ${def.id})` : ""}. Only include the fields you want to change; omitted fields keep their current values. ${def.description}`,
|
|
64
|
+
parameters: partialSchema,
|
|
65
|
+
streamCall: async (reader) => {
|
|
66
|
+
try {
|
|
67
|
+
for await (const partialArgs of reader.args.streamValues()) {
|
|
68
|
+
setDefState(def.id, (prev) => shallowMerge(prev, partialArgs));
|
|
69
|
+
}
|
|
70
|
+
} catch {
|
|
71
|
+
// Non-fatal: execute handles the final state
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
execute: async (partialState: unknown) => {
|
|
75
|
+
setDefState(def.id, (prev) => shallowMerge(prev, partialState));
|
|
76
|
+
return { success: true };
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return { system: systemParts.join("\n"), tools };
|
|
83
|
+
}
|
package/src/react/index.ts
CHANGED
|
@@ -14,6 +14,10 @@ export {
|
|
|
14
14
|
makeAssistantDataUI,
|
|
15
15
|
} from "./model-context/makeAssistantDataUI";
|
|
16
16
|
export { useAssistantInstructions } from "./model-context/useAssistantInstructions";
|
|
17
|
+
export {
|
|
18
|
+
useAssistantContext,
|
|
19
|
+
type AssistantContextConfig,
|
|
20
|
+
} from "./model-context/useAssistantContext";
|
|
17
21
|
export {
|
|
18
22
|
useAssistantTool,
|
|
19
23
|
type AssistantToolProps,
|
|
@@ -29,15 +33,14 @@ export {
|
|
|
29
33
|
export { useInlineRender } from "./model-context/useInlineRender";
|
|
30
34
|
export type { Toolkit, ToolDefinition } from "./model-context/toolbox";
|
|
31
35
|
export {
|
|
32
|
-
|
|
33
|
-
type
|
|
34
|
-
|
|
35
|
-
} from "./model-context/
|
|
36
|
+
useAssistantInteractable,
|
|
37
|
+
type AssistantInteractableProps,
|
|
38
|
+
} from "./model-context/useAssistantInteractable";
|
|
39
|
+
export { useInteractableState } from "./model-context/useInteractableState";
|
|
36
40
|
export {
|
|
37
|
-
|
|
38
|
-
type
|
|
39
|
-
|
|
40
|
-
} from "./model-context/makeInteractable";
|
|
41
|
+
useToolArgsStatus,
|
|
42
|
+
type ToolArgsStatus,
|
|
43
|
+
} from "./model-context/useToolArgsStatus";
|
|
41
44
|
|
|
42
45
|
// client
|
|
43
46
|
export { Tools } from "./client/Tools";
|
|
@@ -85,6 +88,9 @@ export type {
|
|
|
85
88
|
InteractableDefinition,
|
|
86
89
|
InteractableRegistration,
|
|
87
90
|
InteractablesMethods,
|
|
91
|
+
InteractablePersistedState,
|
|
92
|
+
InteractablePersistenceAdapter,
|
|
93
|
+
InteractablePersistenceStatus,
|
|
88
94
|
InteractablesClientSchema,
|
|
89
95
|
} from "./types/scopes/interactables";
|
|
90
96
|
|
|
@@ -211,6 +217,11 @@ export {
|
|
|
211
217
|
} from "./primitive-hooks/useActionBarFeedback";
|
|
212
218
|
export { useActionBarSpeak } from "./primitive-hooks/useActionBarSpeak";
|
|
213
219
|
export { useActionBarStopSpeaking } from "./primitive-hooks/useActionBarStopSpeaking";
|
|
220
|
+
export {
|
|
221
|
+
useVoiceState,
|
|
222
|
+
useVoiceVolume,
|
|
223
|
+
useVoiceControls,
|
|
224
|
+
} from "./primitive-hooks/useVoice";
|
|
214
225
|
export { useBranchPickerNext } from "./primitive-hooks/useBranchPickerNext";
|
|
215
226
|
export { useBranchPickerPrevious } from "./primitive-hooks/useBranchPickerPrevious";
|
|
216
227
|
export {
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { useEffect, useRef } from "react";
|
|
2
|
+
import { useAui } from "@assistant-ui/store";
|
|
3
|
+
import type { AssistantContextConfig } from "../..";
|
|
4
|
+
|
|
5
|
+
export type { AssistantContextConfig };
|
|
6
|
+
|
|
7
|
+
export const useAssistantContext = (config: AssistantContextConfig) => {
|
|
8
|
+
const { getContext, disabled = false } = config;
|
|
9
|
+
const aui = useAui();
|
|
10
|
+
const getContextRef = useRef(getContext);
|
|
11
|
+
getContextRef.current = getContext;
|
|
12
|
+
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
if (disabled) return;
|
|
15
|
+
|
|
16
|
+
return aui.modelContext().register({
|
|
17
|
+
getModelContext: () => ({
|
|
18
|
+
system: getContextRef.current(),
|
|
19
|
+
}),
|
|
20
|
+
});
|
|
21
|
+
}, [aui, disabled]);
|
|
22
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { useEffect, useId, useRef } from "react";
|
|
2
|
+
import { useAui } from "@assistant-ui/store";
|
|
3
|
+
import type { InteractableStateSchema } from "../types/scopes/interactables";
|
|
4
|
+
|
|
5
|
+
export type AssistantInteractableProps = {
|
|
6
|
+
description: string;
|
|
7
|
+
stateSchema: InteractableStateSchema;
|
|
8
|
+
initialState: unknown;
|
|
9
|
+
id?: string;
|
|
10
|
+
selected?: boolean;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Registers an interactable with the AI assistant.
|
|
15
|
+
*
|
|
16
|
+
* This hook handles registration only. To read and write the interactable's
|
|
17
|
+
* state, use {@link useInteractableState} with the returned id.
|
|
18
|
+
*
|
|
19
|
+
* @returns The interactable instance id.
|
|
20
|
+
*/
|
|
21
|
+
export const useAssistantInteractable = (
|
|
22
|
+
name: string,
|
|
23
|
+
config: AssistantInteractableProps,
|
|
24
|
+
): string => {
|
|
25
|
+
const aui = useAui();
|
|
26
|
+
|
|
27
|
+
const autoId = useId().replace(/[^a-zA-Z0-9]/g, "");
|
|
28
|
+
const id = config.id ?? autoId;
|
|
29
|
+
|
|
30
|
+
const stateSchemaRef = useRef(config.stateSchema);
|
|
31
|
+
stateSchemaRef.current = config.stateSchema;
|
|
32
|
+
const initialStateRef = useRef(config.initialState);
|
|
33
|
+
initialStateRef.current = config.initialState;
|
|
34
|
+
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
return aui.interactables().register({
|
|
37
|
+
id,
|
|
38
|
+
name,
|
|
39
|
+
description: config.description,
|
|
40
|
+
stateSchema: stateSchemaRef.current,
|
|
41
|
+
initialState: initialStateRef.current,
|
|
42
|
+
selected: config.selected,
|
|
43
|
+
});
|
|
44
|
+
}, [aui, id, name, config.description, config.selected]);
|
|
45
|
+
|
|
46
|
+
return id;
|
|
47
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { useCallback } from "react";
|
|
2
|
+
import { useAui, useAuiState } from "@assistant-ui/store";
|
|
3
|
+
|
|
4
|
+
type StateUpdater<TState> = TState | ((prev: TState) => TState);
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Reads and writes the state of a registered interactable.
|
|
8
|
+
*
|
|
9
|
+
* Pair with {@link useAssistantInteractable} which handles registration.
|
|
10
|
+
*/
|
|
11
|
+
export const useInteractableState = <TState>(
|
|
12
|
+
id: string,
|
|
13
|
+
fallback: TState,
|
|
14
|
+
): [
|
|
15
|
+
TState,
|
|
16
|
+
{
|
|
17
|
+
setState: (updater: StateUpdater<TState>) => void;
|
|
18
|
+
setSelected: (selected: boolean) => void;
|
|
19
|
+
isPending: boolean;
|
|
20
|
+
error: unknown;
|
|
21
|
+
flush: () => Promise<void>;
|
|
22
|
+
},
|
|
23
|
+
] => {
|
|
24
|
+
const aui = useAui();
|
|
25
|
+
|
|
26
|
+
const state =
|
|
27
|
+
(useAuiState((s) => s.interactables.definitions[id]?.state) as TState) ??
|
|
28
|
+
(fallback as TState);
|
|
29
|
+
|
|
30
|
+
const persistenceStatus = useAuiState((s) => s.interactables.persistence[id]);
|
|
31
|
+
|
|
32
|
+
const setState = useCallback(
|
|
33
|
+
(updater: StateUpdater<TState>) => {
|
|
34
|
+
aui.interactables().setState(id, (prev) => {
|
|
35
|
+
if (typeof updater === "function") {
|
|
36
|
+
return (updater as (prev: TState) => TState)(prev as TState);
|
|
37
|
+
}
|
|
38
|
+
return updater;
|
|
39
|
+
});
|
|
40
|
+
},
|
|
41
|
+
[aui, id],
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
const setSelected = useCallback(
|
|
45
|
+
(selected: boolean) => {
|
|
46
|
+
aui.interactables().setSelected(id, selected);
|
|
47
|
+
},
|
|
48
|
+
[aui, id],
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const flush = useCallback(() => aui.interactables().flush(), [aui]);
|
|
52
|
+
|
|
53
|
+
return [
|
|
54
|
+
state,
|
|
55
|
+
{
|
|
56
|
+
setState,
|
|
57
|
+
setSelected,
|
|
58
|
+
isPending: persistenceStatus?.isPending ?? false,
|
|
59
|
+
error: persistenceStatus?.error,
|
|
60
|
+
flush,
|
|
61
|
+
},
|
|
62
|
+
];
|
|
63
|
+
};
|