@tambo-ai/react 0.46.3 → 0.46.4

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.
@@ -0,0 +1,322 @@
1
+ import { act, renderHook } from "@testing-library/react";
2
+ import { useTamboClient } from "../../providers/tambo-client-provider";
3
+ import { useTamboThread } from "../../providers/tambo-thread-provider";
4
+ import { useTamboComponentState } from "../use-component-state";
5
+ import { useTamboCurrentMessage } from "../use-current-message";
6
+ // Mock the required providers
7
+ jest.mock("../../providers/tambo-client-provider", () => ({
8
+ useTamboClient: jest.fn(),
9
+ }));
10
+ jest.mock("../../providers/tambo-thread-provider", () => ({
11
+ useTamboThread: jest.fn(),
12
+ }));
13
+ jest.mock("../use-current-message", () => ({
14
+ useTamboCurrentMessage: jest.fn(),
15
+ }));
16
+ // Create a mock debounced function with flush method
17
+ const createMockDebouncedFunction = (fn) => {
18
+ const debouncedFn = jest.fn((...args) => fn(...args));
19
+ debouncedFn.flush = jest.fn();
20
+ debouncedFn.cancel = jest.fn();
21
+ debouncedFn.isPending = jest.fn(() => false);
22
+ return debouncedFn;
23
+ };
24
+ // Mock use-debounce
25
+ jest.mock("use-debounce", () => ({
26
+ useDebouncedCallback: jest.fn(),
27
+ }));
28
+ // Import the mocked useDebouncedCallback
29
+ import { useDebouncedCallback } from "use-debounce";
30
+ describe("useTamboComponentState", () => {
31
+ // Helper function to create mock TamboThreadMessage
32
+ const createMockMessage = (overrides = {}) => ({
33
+ id: "test-message-id",
34
+ threadId: "test-thread-id",
35
+ componentState: {},
36
+ content: [{ type: "text", text: "Test message" }],
37
+ createdAt: new Date().toISOString(),
38
+ role: "assistant",
39
+ ...overrides,
40
+ });
41
+ const mockUpdateThreadMessage = jest.fn();
42
+ const mockUpdateComponentState = jest.fn();
43
+ beforeEach(() => {
44
+ jest.clearAllMocks();
45
+ // Setup default mock for useDebouncedCallback
46
+ jest
47
+ .mocked(useDebouncedCallback)
48
+ .mockImplementation((fn) => createMockDebouncedFunction(fn));
49
+ // Setup default mocks
50
+ jest.mocked(useTamboClient).mockReturnValue({
51
+ beta: {
52
+ threads: {
53
+ messages: {
54
+ updateComponentState: mockUpdateComponentState,
55
+ },
56
+ },
57
+ },
58
+ });
59
+ jest.mocked(useTamboThread).mockReturnValue({
60
+ updateThreadMessage: mockUpdateThreadMessage,
61
+ });
62
+ jest.mocked(useTamboCurrentMessage).mockReturnValue(createMockMessage());
63
+ });
64
+ describe("Initial State Management", () => {
65
+ it("should initialize with initialValue when no componentState exists", () => {
66
+ const initialValue = "test-initial";
67
+ jest
68
+ .mocked(useTamboCurrentMessage)
69
+ .mockReturnValue(createMockMessage({ componentState: {} }));
70
+ const { result } = renderHook(() => useTamboComponentState("testKey", initialValue));
71
+ expect(result.current[0]).toBe(initialValue);
72
+ });
73
+ it("should use existing componentState value over initialValue", () => {
74
+ const initialValue = "initial";
75
+ const existingValue = "existing";
76
+ jest.mocked(useTamboCurrentMessage).mockReturnValue(createMockMessage({
77
+ componentState: { testKey: existingValue },
78
+ }));
79
+ const { result } = renderHook(() => useTamboComponentState("testKey", initialValue));
80
+ expect(result.current[0]).toBe(existingValue);
81
+ });
82
+ it("should handle undefined initialValue gracefully", () => {
83
+ jest
84
+ .mocked(useTamboCurrentMessage)
85
+ .mockReturnValue(createMockMessage({ componentState: {} }));
86
+ const { result } = renderHook(() => useTamboComponentState("testKey"));
87
+ expect(result.current[0]).toBeUndefined();
88
+ });
89
+ it("should handle different data types correctly", () => {
90
+ const testCases = [
91
+ { value: "string" },
92
+ { value: 42 },
93
+ { value: true },
94
+ { value: { name: "test" } },
95
+ { value: [1, 2, 3] },
96
+ ];
97
+ testCases.forEach(({ value }) => {
98
+ jest.mocked(useTamboCurrentMessage).mockReturnValue(createMockMessage({
99
+ componentState: { testKey: value },
100
+ }));
101
+ const { result } = renderHook(() => useTamboComponentState("testKey", value));
102
+ expect(result.current[0]).toEqual(value);
103
+ });
104
+ });
105
+ });
106
+ describe("State Updates", () => {
107
+ it("should update local state immediately when setValue is called", () => {
108
+ const initialValue = "initial";
109
+ jest
110
+ .mocked(useTamboCurrentMessage)
111
+ .mockReturnValue(createMockMessage({ componentState: { testKey: initialValue } }));
112
+ const { result } = renderHook(() => useTamboComponentState("testKey", initialValue));
113
+ const newValue = "updated";
114
+ act(() => {
115
+ result.current[1](newValue);
116
+ });
117
+ expect(result.current[0]).toBe(newValue);
118
+ });
119
+ it("should trigger local thread message update when setValue is called", () => {
120
+ const message = createMockMessage({
121
+ componentState: { testKey: "initial" },
122
+ });
123
+ jest.mocked(useTamboCurrentMessage).mockReturnValue(message);
124
+ const { result } = renderHook(() => useTamboComponentState("testKey", "initial"));
125
+ const newValue = "updated";
126
+ act(() => {
127
+ result.current[1](newValue);
128
+ });
129
+ expect(mockUpdateThreadMessage).toHaveBeenCalledWith(message.id, {
130
+ threadId: message.threadId,
131
+ componentState: {
132
+ testKey: newValue,
133
+ },
134
+ }, false);
135
+ });
136
+ it("should trigger debounced remote API call when setValue is called", () => {
137
+ const message = createMockMessage({
138
+ componentState: { testKey: "initial" },
139
+ });
140
+ jest.mocked(useTamboCurrentMessage).mockReturnValue(message);
141
+ const { result } = renderHook(() => useTamboComponentState("testKey", "initial"));
142
+ const newValue = "updated";
143
+ act(() => {
144
+ result.current[1](newValue);
145
+ });
146
+ // The debounced function should be called
147
+ expect(mockUpdateComponentState).toHaveBeenCalledWith(message.threadId, message.id, { state: { testKey: newValue } });
148
+ });
149
+ it("should work with complex objects and arrays", () => {
150
+ const initialValue = { name: "test", items: [1, 2, 3] };
151
+ jest
152
+ .mocked(useTamboCurrentMessage)
153
+ .mockReturnValue(createMockMessage({ componentState: { testKey: initialValue } }));
154
+ const { result } = renderHook(() => useTamboComponentState("testKey", initialValue));
155
+ const newValue = { name: "updated", items: [4, 5, 6] };
156
+ act(() => {
157
+ result.current[1](newValue);
158
+ });
159
+ expect(result.current[0]).toEqual(newValue);
160
+ expect(mockUpdateThreadMessage).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({
161
+ componentState: {
162
+ testKey: newValue,
163
+ },
164
+ }), false);
165
+ });
166
+ });
167
+ describe("Debouncing Behavior", () => {
168
+ it("should use default debounce time of 500ms", () => {
169
+ renderHook(() => useTamboComponentState("testKey", "initial"));
170
+ expect(useDebouncedCallback).toHaveBeenCalledWith(expect.any(Function), 500);
171
+ });
172
+ it("should use custom debounce time when provided", () => {
173
+ const customDebounceTime = 1000;
174
+ renderHook(() => useTamboComponentState("testKey", "initial", undefined, customDebounceTime));
175
+ expect(useDebouncedCallback).toHaveBeenCalledWith(expect.any(Function), customDebounceTime);
176
+ });
177
+ it("should flush debounced callback on unmount", () => {
178
+ const mockFlush = jest.fn();
179
+ const mockDebouncedFn = createMockDebouncedFunction(jest.fn());
180
+ mockDebouncedFn.flush = mockFlush;
181
+ // Mock the debounced callback to return our specific mock
182
+ jest.mocked(useDebouncedCallback).mockReturnValue(mockDebouncedFn);
183
+ const { unmount } = renderHook(() => useTamboComponentState("testKey", "initial"));
184
+ unmount();
185
+ expect(mockFlush).toHaveBeenCalled();
186
+ });
187
+ });
188
+ describe("Multi-Hook Scenarios", () => {
189
+ it("should handle multiple hooks with different keyNames independently", () => {
190
+ const message = createMockMessage({
191
+ componentState: {
192
+ key1: "value1",
193
+ key2: "value2",
194
+ },
195
+ });
196
+ jest.mocked(useTamboCurrentMessage).mockReturnValue(message);
197
+ const { result: result1 } = renderHook(() => useTamboComponentState("key1", "default1"));
198
+ const { result: result2 } = renderHook(() => useTamboComponentState("key2", "default2"));
199
+ expect(result1.current[0]).toBe("value1");
200
+ expect(result2.current[0]).toBe("value2");
201
+ // Update first hook
202
+ act(() => {
203
+ result1.current[1]("updated1");
204
+ });
205
+ expect(result1.current[0]).toBe("updated1");
206
+ expect(result2.current[0]).toBe("value2"); // Should remain unchanged
207
+ });
208
+ it("should preserve existing componentState when updating another key", () => {
209
+ const message = createMockMessage({
210
+ componentState: {
211
+ existingKey: "existing",
212
+ testKey: "initial",
213
+ },
214
+ });
215
+ jest.mocked(useTamboCurrentMessage).mockReturnValue(message);
216
+ const { result } = renderHook(() => useTamboComponentState("testKey", "initial"));
217
+ act(() => {
218
+ result.current[1]("updated");
219
+ });
220
+ expect(mockUpdateThreadMessage).toHaveBeenCalledWith(message.id, {
221
+ threadId: message.threadId,
222
+ componentState: {
223
+ existingKey: "existing", // Should preserve existing keys
224
+ testKey: "updated",
225
+ },
226
+ }, false);
227
+ });
228
+ });
229
+ describe("SetFromProp Feature", () => {
230
+ it("should set value from prop when hasSetFromMessage is false", () => {
231
+ jest
232
+ .mocked(useTamboCurrentMessage)
233
+ .mockReturnValue(createMockMessage({ componentState: {} }));
234
+ const propValue = "from-prop";
235
+ const { result } = renderHook(() => useTamboComponentState("testKey", "initial", propValue));
236
+ // Initially, hasSetFromMessage should be false, so prop value should be used
237
+ expect(result.current[0]).toBe(propValue);
238
+ });
239
+ it("should ignore setFromProp when initialized from message state", async () => {
240
+ const existingValue = "existing";
241
+ jest.mocked(useTamboCurrentMessage).mockReturnValue(createMockMessage({
242
+ componentState: { testKey: existingValue },
243
+ }));
244
+ const propValue = "from-prop";
245
+ const { result } = renderHook(() => useTamboComponentState("testKey", "initial", propValue));
246
+ // Should use existing value from message, not prop value
247
+ await act(async () => {
248
+ await new Promise((resolve) => setTimeout(resolve, 0));
249
+ });
250
+ expect(result.current[0]).toBe(existingValue);
251
+ });
252
+ it("should update state from setFromProp changes when no message state exists", () => {
253
+ jest
254
+ .mocked(useTamboCurrentMessage)
255
+ .mockReturnValue(createMockMessage({ componentState: {} }));
256
+ const { result, rerender } = renderHook(({ propValue }) => useTamboComponentState("testKey", "initial", propValue), { initialProps: { propValue: "prop1" } });
257
+ expect(result.current[0]).toBe("prop1");
258
+ // Change prop value
259
+ rerender({ propValue: "prop2" });
260
+ // Since hasSetFromMessage is still false (no message state),
261
+ // it should update to new prop value
262
+ expect(result.current[0]).toBe("prop2");
263
+ });
264
+ it("should handle undefined setFromProp gracefully", () => {
265
+ jest
266
+ .mocked(useTamboCurrentMessage)
267
+ .mockReturnValue(createMockMessage({ componentState: {} }));
268
+ const { result } = renderHook(() => useTamboComponentState("testKey", "initial", undefined));
269
+ expect(result.current[0]).toBe("initial");
270
+ });
271
+ });
272
+ describe("Message State Sync", () => {
273
+ it("should sync with message.componentState changes", () => {
274
+ const { result, rerender } = renderHook(({ message }) => {
275
+ jest.mocked(useTamboCurrentMessage).mockReturnValue(message);
276
+ return useTamboComponentState("testKey", "initial");
277
+ }, {
278
+ initialProps: {
279
+ message: createMockMessage({
280
+ componentState: { testKey: "value1" },
281
+ }),
282
+ },
283
+ });
284
+ // Change the message
285
+ const newMessage = createMockMessage({
286
+ componentState: { testKey: "value2" },
287
+ });
288
+ rerender({ message: newMessage });
289
+ // The hook should sync with the new message state
290
+ expect(result.current[0]).toBe("value2");
291
+ expect(mockUpdateThreadMessage).not.toHaveBeenCalled();
292
+ });
293
+ it("should handle message without componentState gracefully", () => {
294
+ jest
295
+ .mocked(useTamboCurrentMessage)
296
+ .mockReturnValue(createMockMessage({ componentState: undefined }));
297
+ const { result } = renderHook(() => useTamboComponentState("testKey", "initial"));
298
+ expect(result.current[0]).toBe("initial");
299
+ });
300
+ it("should preserve state when message updates but componentState[keyName] unchanged", () => {
301
+ const message1 = createMockMessage({
302
+ id: "message1",
303
+ componentState: { testKey: "unchanged" },
304
+ });
305
+ const message2 = createMockMessage({
306
+ id: "message2",
307
+ componentState: { testKey: "unchanged" },
308
+ });
309
+ const { result, rerender } = renderHook(({ message }) => {
310
+ jest.mocked(useTamboCurrentMessage).mockReturnValue(message);
311
+ return useTamboComponentState("testKey", "initial");
312
+ }, { initialProps: { message: message1 } });
313
+ // Clear previous calls
314
+ mockUpdateThreadMessage.mockClear();
315
+ // Change message but keep same componentState value
316
+ rerender({ message: message2 });
317
+ // Should preserve the "unchanged" value
318
+ expect(result.current[0]).toBe("unchanged");
319
+ });
320
+ });
321
+ });
322
+ //# sourceMappingURL=use-component-state.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-component-state.test.js","sourceRoot":"","sources":["../../../src/hooks/__tests__/use-component-state.test.tsx"],"names":[],"mappings":"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","sourcesContent":["import { act, renderHook } from \"@testing-library/react\";\nimport { TamboThreadMessage } from \"../../model/generate-component-response\";\nimport { useTamboClient } from \"../../providers/tambo-client-provider\";\nimport { useTamboThread } from \"../../providers/tambo-thread-provider\";\nimport { PartialTamboAI } from \"../../testing/types\";\nimport { useTamboComponentState } from \"../use-component-state\";\nimport { useTamboCurrentMessage } from \"../use-current-message\";\n\n// Mock the required providers\njest.mock(\"../../providers/tambo-client-provider\", () => ({\n useTamboClient: jest.fn(),\n}));\n\njest.mock(\"../../providers/tambo-thread-provider\", () => ({\n useTamboThread: jest.fn(),\n}));\n\njest.mock(\"../use-current-message\", () => ({\n useTamboCurrentMessage: jest.fn(),\n}));\n\n// Create a mock debounced function with flush method\nconst createMockDebouncedFunction = (fn: any) => {\n const debouncedFn = jest.fn((...args: any[]) => fn(...args)) as jest.Mock & {\n flush: jest.Mock;\n cancel: jest.Mock;\n isPending: () => boolean;\n };\n debouncedFn.flush = jest.fn();\n debouncedFn.cancel = jest.fn();\n debouncedFn.isPending = jest.fn(() => false);\n return debouncedFn;\n};\n\n// Mock use-debounce\njest.mock(\"use-debounce\", () => ({\n useDebouncedCallback: jest.fn(),\n}));\n\n// Import the mocked useDebouncedCallback\nimport { useDebouncedCallback } from \"use-debounce\";\n\ndescribe(\"useTamboComponentState\", () => {\n // Helper function to create mock TamboThreadMessage\n const createMockMessage = (\n overrides: Partial<TamboThreadMessage> = {},\n ): TamboThreadMessage => ({\n id: \"test-message-id\",\n threadId: \"test-thread-id\",\n componentState: {},\n content: [{ type: \"text\", text: \"Test message\" }],\n createdAt: new Date().toISOString(),\n role: \"assistant\",\n ...overrides,\n });\n\n const mockUpdateThreadMessage = jest.fn();\n const mockUpdateComponentState = jest.fn();\n\n beforeEach(() => {\n jest.clearAllMocks();\n\n // Setup default mock for useDebouncedCallback\n jest\n .mocked(useDebouncedCallback)\n .mockImplementation((fn) => createMockDebouncedFunction(fn));\n\n // Setup default mocks\n jest.mocked(useTamboClient).mockReturnValue({\n beta: {\n threads: {\n messages: {\n updateComponentState: mockUpdateComponentState,\n },\n },\n },\n } satisfies PartialTamboAI as any);\n\n jest.mocked(useTamboThread).mockReturnValue({\n updateThreadMessage: mockUpdateThreadMessage,\n } as any);\n\n jest.mocked(useTamboCurrentMessage).mockReturnValue(createMockMessage());\n });\n\n describe(\"Initial State Management\", () => {\n it(\"should initialize with initialValue when no componentState exists\", () => {\n const initialValue = \"test-initial\";\n jest\n .mocked(useTamboCurrentMessage)\n .mockReturnValue(createMockMessage({ componentState: {} }));\n\n const { result } = renderHook(() =>\n useTamboComponentState(\"testKey\", initialValue),\n );\n\n expect(result.current[0]).toBe(initialValue);\n });\n\n it(\"should use existing componentState value over initialValue\", () => {\n const initialValue = \"initial\";\n const existingValue = \"existing\";\n jest.mocked(useTamboCurrentMessage).mockReturnValue(\n createMockMessage({\n componentState: { testKey: existingValue },\n }),\n );\n\n const { result } = renderHook(() =>\n useTamboComponentState(\"testKey\", initialValue),\n );\n\n expect(result.current[0]).toBe(existingValue);\n });\n\n it(\"should handle undefined initialValue gracefully\", () => {\n jest\n .mocked(useTamboCurrentMessage)\n .mockReturnValue(createMockMessage({ componentState: {} }));\n\n const { result } = renderHook(() => useTamboComponentState(\"testKey\"));\n\n expect(result.current[0]).toBeUndefined();\n });\n\n it(\"should handle different data types correctly\", () => {\n const testCases = [\n { value: \"string\" },\n { value: 42 },\n { value: true },\n { value: { name: \"test\" } },\n { value: [1, 2, 3] },\n ];\n\n testCases.forEach(({ value }) => {\n jest.mocked(useTamboCurrentMessage).mockReturnValue(\n createMockMessage({\n componentState: { testKey: value },\n }),\n );\n\n const { result } = renderHook(() =>\n useTamboComponentState(\"testKey\", value),\n );\n\n expect(result.current[0]).toEqual(value);\n });\n });\n });\n\n describe(\"State Updates\", () => {\n it(\"should update local state immediately when setValue is called\", () => {\n const initialValue = \"initial\";\n jest\n .mocked(useTamboCurrentMessage)\n .mockReturnValue(\n createMockMessage({ componentState: { testKey: initialValue } }),\n );\n\n const { result } = renderHook(() =>\n useTamboComponentState(\"testKey\", initialValue),\n );\n\n const newValue = \"updated\";\n act(() => {\n result.current[1](newValue);\n });\n\n expect(result.current[0]).toBe(newValue);\n });\n\n it(\"should trigger local thread message update when setValue is called\", () => {\n const message = createMockMessage({\n componentState: { testKey: \"initial\" },\n });\n jest.mocked(useTamboCurrentMessage).mockReturnValue(message);\n\n const { result } = renderHook(() =>\n useTamboComponentState(\"testKey\", \"initial\"),\n );\n\n const newValue = \"updated\";\n act(() => {\n result.current[1](newValue);\n });\n\n expect(mockUpdateThreadMessage).toHaveBeenCalledWith(\n message.id,\n {\n threadId: message.threadId,\n componentState: {\n testKey: newValue,\n },\n },\n false,\n );\n });\n\n it(\"should trigger debounced remote API call when setValue is called\", () => {\n const message = createMockMessage({\n componentState: { testKey: \"initial\" },\n });\n jest.mocked(useTamboCurrentMessage).mockReturnValue(message);\n\n const { result } = renderHook(() =>\n useTamboComponentState(\"testKey\", \"initial\"),\n );\n\n const newValue = \"updated\";\n act(() => {\n result.current[1](newValue);\n });\n\n // The debounced function should be called\n expect(mockUpdateComponentState).toHaveBeenCalledWith(\n message.threadId,\n message.id,\n { state: { testKey: newValue } },\n );\n });\n\n it(\"should work with complex objects and arrays\", () => {\n const initialValue = { name: \"test\", items: [1, 2, 3] };\n jest\n .mocked(useTamboCurrentMessage)\n .mockReturnValue(\n createMockMessage({ componentState: { testKey: initialValue } }),\n );\n\n const { result } = renderHook(() =>\n useTamboComponentState(\"testKey\", initialValue),\n );\n\n const newValue = { name: \"updated\", items: [4, 5, 6] };\n act(() => {\n result.current[1](newValue);\n });\n\n expect(result.current[0]).toEqual(newValue);\n expect(mockUpdateThreadMessage).toHaveBeenCalledWith(\n expect.any(String),\n expect.objectContaining({\n componentState: {\n testKey: newValue,\n },\n }),\n false,\n );\n });\n });\n\n describe(\"Debouncing Behavior\", () => {\n it(\"should use default debounce time of 500ms\", () => {\n renderHook(() => useTamboComponentState(\"testKey\", \"initial\"));\n\n expect(useDebouncedCallback).toHaveBeenCalledWith(\n expect.any(Function),\n 500,\n );\n });\n\n it(\"should use custom debounce time when provided\", () => {\n const customDebounceTime = 1000;\n\n renderHook(() =>\n useTamboComponentState(\n \"testKey\",\n \"initial\",\n undefined,\n customDebounceTime,\n ),\n );\n\n expect(useDebouncedCallback).toHaveBeenCalledWith(\n expect.any(Function),\n customDebounceTime,\n );\n });\n\n it(\"should flush debounced callback on unmount\", () => {\n const mockFlush = jest.fn();\n const mockDebouncedFn = createMockDebouncedFunction(jest.fn());\n mockDebouncedFn.flush = mockFlush;\n\n // Mock the debounced callback to return our specific mock\n jest.mocked(useDebouncedCallback).mockReturnValue(mockDebouncedFn);\n\n const { unmount } = renderHook(() =>\n useTamboComponentState(\"testKey\", \"initial\"),\n );\n\n unmount();\n\n expect(mockFlush).toHaveBeenCalled();\n });\n });\n\n describe(\"Multi-Hook Scenarios\", () => {\n it(\"should handle multiple hooks with different keyNames independently\", () => {\n const message = createMockMessage({\n componentState: {\n key1: \"value1\",\n key2: \"value2\",\n },\n });\n jest.mocked(useTamboCurrentMessage).mockReturnValue(message);\n\n const { result: result1 } = renderHook(() =>\n useTamboComponentState(\"key1\", \"default1\"),\n );\n const { result: result2 } = renderHook(() =>\n useTamboComponentState(\"key2\", \"default2\"),\n );\n\n expect(result1.current[0]).toBe(\"value1\");\n expect(result2.current[0]).toBe(\"value2\");\n\n // Update first hook\n act(() => {\n result1.current[1](\"updated1\");\n });\n\n expect(result1.current[0]).toBe(\"updated1\");\n expect(result2.current[0]).toBe(\"value2\"); // Should remain unchanged\n });\n\n it(\"should preserve existing componentState when updating another key\", () => {\n const message = createMockMessage({\n componentState: {\n existingKey: \"existing\",\n testKey: \"initial\",\n },\n });\n jest.mocked(useTamboCurrentMessage).mockReturnValue(message);\n\n const { result } = renderHook(() =>\n useTamboComponentState(\"testKey\", \"initial\"),\n );\n\n act(() => {\n result.current[1](\"updated\");\n });\n\n expect(mockUpdateThreadMessage).toHaveBeenCalledWith(\n message.id,\n {\n threadId: message.threadId,\n componentState: {\n existingKey: \"existing\", // Should preserve existing keys\n testKey: \"updated\",\n },\n },\n false,\n );\n });\n });\n\n describe(\"SetFromProp Feature\", () => {\n it(\"should set value from prop when hasSetFromMessage is false\", () => {\n jest\n .mocked(useTamboCurrentMessage)\n .mockReturnValue(createMockMessage({ componentState: {} }));\n\n const propValue = \"from-prop\";\n const { result } = renderHook(() =>\n useTamboComponentState(\"testKey\", \"initial\", propValue),\n );\n\n // Initially, hasSetFromMessage should be false, so prop value should be used\n expect(result.current[0]).toBe(propValue);\n });\n\n it(\"should ignore setFromProp when initialized from message state\", async () => {\n const existingValue = \"existing\";\n jest.mocked(useTamboCurrentMessage).mockReturnValue(\n createMockMessage({\n componentState: { testKey: existingValue },\n }),\n );\n\n const propValue = \"from-prop\";\n const { result } = renderHook(() =>\n useTamboComponentState(\"testKey\", \"initial\", propValue),\n );\n\n // Should use existing value from message, not prop value\n await act(async () => {\n await new Promise((resolve) => setTimeout(resolve, 0));\n });\n\n expect(result.current[0]).toBe(existingValue);\n });\n\n it(\"should update state from setFromProp changes when no message state exists\", () => {\n jest\n .mocked(useTamboCurrentMessage)\n .mockReturnValue(createMockMessage({ componentState: {} }));\n\n const { result, rerender } = renderHook(\n ({ propValue }) =>\n useTamboComponentState(\"testKey\", \"initial\", propValue),\n { initialProps: { propValue: \"prop1\" } },\n );\n\n expect(result.current[0]).toBe(\"prop1\");\n\n // Change prop value\n rerender({ propValue: \"prop2\" });\n\n // Since hasSetFromMessage is still false (no message state),\n // it should update to new prop value\n expect(result.current[0]).toBe(\"prop2\");\n });\n\n it(\"should handle undefined setFromProp gracefully\", () => {\n jest\n .mocked(useTamboCurrentMessage)\n .mockReturnValue(createMockMessage({ componentState: {} }));\n\n const { result } = renderHook(() =>\n useTamboComponentState(\"testKey\", \"initial\", undefined),\n );\n\n expect(result.current[0]).toBe(\"initial\");\n });\n });\n\n describe(\"Message State Sync\", () => {\n it(\"should sync with message.componentState changes\", () => {\n const { result, rerender } = renderHook(\n ({ message }) => {\n jest.mocked(useTamboCurrentMessage).mockReturnValue(message);\n return useTamboComponentState(\"testKey\", \"initial\");\n },\n {\n initialProps: {\n message: createMockMessage({\n componentState: { testKey: \"value1\" },\n }),\n },\n },\n );\n\n // Change the message\n const newMessage = createMockMessage({\n componentState: { testKey: \"value2\" },\n });\n\n rerender({ message: newMessage });\n\n // The hook should sync with the new message state\n expect(result.current[0]).toBe(\"value2\");\n expect(mockUpdateThreadMessage).not.toHaveBeenCalled();\n });\n\n it(\"should handle message without componentState gracefully\", () => {\n jest\n .mocked(useTamboCurrentMessage)\n .mockReturnValue(\n createMockMessage({ componentState: undefined as any }),\n );\n\n const { result } = renderHook(() =>\n useTamboComponentState(\"testKey\", \"initial\"),\n );\n\n expect(result.current[0]).toBe(\"initial\");\n });\n\n it(\"should preserve state when message updates but componentState[keyName] unchanged\", () => {\n const message1 = createMockMessage({\n id: \"message1\",\n componentState: { testKey: \"unchanged\" },\n });\n const message2 = createMockMessage({\n id: \"message2\",\n componentState: { testKey: \"unchanged\" },\n });\n\n const { result, rerender } = renderHook(\n ({ message }) => {\n jest.mocked(useTamboCurrentMessage).mockReturnValue(message);\n return useTamboComponentState(\"testKey\", \"initial\");\n },\n { initialProps: { message: message1 } },\n );\n\n // Clear previous calls\n mockUpdateThreadMessage.mockClear();\n\n // Change message but keep same componentState value\n rerender({ message: message2 });\n\n // Should preserve the \"unchanged\" value\n expect(result.current[0]).toBe(\"unchanged\");\n });\n });\n});\n"]}
@@ -1,24 +1,16 @@
1
- interface ComponentStateMeta {
2
- isPending: boolean;
3
- }
4
- type StateUpdateResult<T> = [
5
- currentState: T,
6
- setState: (newState: T) => void,
7
- meta: ComponentStateMeta
8
- ];
1
+ type StateUpdateResult<T> = [currentState: T, setState: (newState: T) => void];
9
2
  /**
10
- * A React hook that provides state management and passes user updates to Tambo.
3
+ * A React hook that acts like useState, but also automatically updates the thread message's componentState.
11
4
  * Benefits: Passes user changes to AI, and when threads are returned, state is preserved.
12
- * @param keyName - The unique key to identify this state within the message's componentState object
13
- * @param initialValue - Optional initial value for the state, used if no value exists in the message
14
- * @param debounceTime - Optional debounce time in milliseconds (default: 300ms) to limit API calls
5
+ * @param keyName - The unique key to identify this state value within the message's componentState object
6
+ * @param initialValue - Optional initial value for the state, used if no componentState value exists in the Tambo message containing this hook usage.
7
+ * @param setFromProp - Optional value used to set the state value, only while no componentState value exists in the Tambo message containing this hook usage. Use this to allow streaming updates from a prop to the state value.
8
+ * @param debounceTime - Optional debounce time in milliseconds (default: 500ms) to limit API calls.
15
9
  * @returns A tuple containing:
16
10
  * - The current state value
17
11
  * - A setter function to update the state (updates UI immediately, debounces server sync)
18
- * - A metadata object with properties like isPending to track sync status
19
12
  * @example
20
- * // Basic usage
21
- * const [count, setCount, { isPending }] = useTamboComponentState("counter", 0);
13
+ * const [count, setCount] = useTamboComponentState("counter", 0);
22
14
  *
23
15
  * // Usage with object state
24
16
  * const [formState, setFormState] = useTamboComponentState("myForm", {
@@ -35,7 +27,7 @@ type StateUpdateResult<T> = [
35
27
  * });
36
28
  * };
37
29
  */
38
- export declare function useTamboComponentState<S = undefined>(keyName: string, initialValue?: S, debounceTime?: number): StateUpdateResult<S | undefined>;
39
- export declare function useTamboComponentState<S>(keyName: string, initialValue: S, debounceTime?: number): StateUpdateResult<S>;
30
+ export declare function useTamboComponentState<S = undefined>(keyName: string, initialValue?: S, setFromProp?: S, debounceTime?: number): StateUpdateResult<S | undefined>;
31
+ export declare function useTamboComponentState<S>(keyName: string, initialValue: S, setFromProp?: S, debounceTime?: number): StateUpdateResult<S>;
40
32
  export {};
41
33
  //# sourceMappingURL=use-component-state.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"use-component-state.d.ts","sourceRoot":"","sources":["../../src/hooks/use-component-state.tsx"],"names":[],"mappings":"AAMA,UAAU,kBAAkB;IAC1B,SAAS,EAAE,OAAO,CAAC;CACpB;AAED,KAAK,iBAAiB,CAAC,CAAC,IAAI;IAC1B,YAAY,EAAE,CAAC;IACf,QAAQ,EAAE,CAAC,QAAQ,EAAE,CAAC,KAAK,IAAI;IAC/B,IAAI,EAAE,kBAAkB;CACzB,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,sBAAsB,CAAC,CAAC,GAAG,SAAS,EAClD,OAAO,EAAE,MAAM,EACf,YAAY,CAAC,EAAE,CAAC,EAChB,YAAY,CAAC,EAAE,MAAM,GACpB,iBAAiB,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC;AACpC,wBAAgB,sBAAsB,CAAC,CAAC,EACtC,OAAO,EAAE,MAAM,EACf,YAAY,EAAE,CAAC,EACf,YAAY,CAAC,EAAE,MAAM,GACpB,iBAAiB,CAAC,CAAC,CAAC,CAAC"}
1
+ {"version":3,"file":"use-component-state.d.ts","sourceRoot":"","sources":["../../src/hooks/use-component-state.tsx"],"names":[],"mappings":"AAMA,KAAK,iBAAiB,CAAC,CAAC,IAAI,CAAC,YAAY,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,QAAQ,EAAE,CAAC,KAAK,IAAI,CAAC,CAAC;AAE/E;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,sBAAsB,CAAC,CAAC,GAAG,SAAS,EAClD,OAAO,EAAE,MAAM,EACf,YAAY,CAAC,EAAE,CAAC,EAChB,WAAW,CAAC,EAAE,CAAC,EACf,YAAY,CAAC,EAAE,MAAM,GACpB,iBAAiB,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC;AACpC,wBAAgB,sBAAsB,CAAC,CAAC,EACtC,OAAO,EAAE,MAAM,EACf,YAAY,EAAE,CAAC,EACf,WAAW,CAAC,EAAE,CAAC,EACf,YAAY,CAAC,EAAE,MAAM,GACpB,iBAAiB,CAAC,CAAC,CAAC,CAAC"}
@@ -1,136 +1,59 @@
1
1
  "use client";
2
2
  import { useCallback, useEffect, useState } from "react";
3
3
  import { useDebouncedCallback } from "use-debounce";
4
- import { useTamboClient, useTamboThread } from "../providers";
4
+ import { useTamboClient, useTamboThread } from "..";
5
5
  import { useTamboCurrentMessage } from "./use-current-message";
6
- export function useTamboComponentState(keyName, initialValue, debounceTime = 500) {
6
+ export function useTamboComponentState(keyName, initialValue, setFromProp, debounceTime = 500) {
7
7
  const message = useTamboCurrentMessage();
8
- const { updateThreadMessage, thread } = useTamboThread();
8
+ const { updateThreadMessage } = useTamboThread();
9
9
  const client = useTamboClient();
10
- const messageId = message.id;
11
- const threadId = thread.id;
12
- // Initial value management
13
- const [cachedInitialValue] = useState(() => initialValue);
14
- // UI state management
15
- const [localState, setLocalState] = useState(cachedInitialValue);
16
- // Synchronization state
17
- const [isPending, setIsPending] = useState(false);
18
- // Track the last user-initiated value instead of a simple boolean flag
19
- const [lastUserValue, setLastUserValue] = useState(null);
20
- const [haveInitialized, setHaveInitialized] = useState(false);
21
- // Determine if we need to initialize state
22
- const shouldInitialize = !haveInitialized &&
23
- message &&
24
- cachedInitialValue !== undefined &&
25
- (!message.componentState || !(keyName in message.componentState));
26
- // Sync local state with message state on initial load and when message changes
27
- useEffect(() => {
28
- if (message?.componentState && keyName in message.componentState) {
29
- const messageState = message.componentState[keyName];
30
- // Only update local state if we haven't had any user changes yet
31
- if (lastUserValue === null) {
32
- setLocalState(messageState);
33
- }
34
- }
35
- // Otherwise fall back to initial value if we have one and no user changes
36
- else if (cachedInitialValue !== undefined &&
37
- !localState &&
38
- lastUserValue === null) {
39
- setLocalState(cachedInitialValue);
40
- }
41
- }, [
42
- keyName,
43
- message?.componentState,
44
- cachedInitialValue,
45
- lastUserValue,
46
- localState,
47
- ]);
48
- // Create debounced save function for efficient server synchronization
49
- const debouncedServerWrite = useDebouncedCallback(async (newValue) => {
50
- setIsPending(true);
51
- try {
52
- const componentStateUpdate = {
53
- state: { [keyName]: newValue },
54
- };
55
- await client.beta.threads.messages.updateComponentState(threadId, messageId, componentStateUpdate);
56
- }
57
- catch (err) {
58
- console.error(`Failed to save component state for key "${keyName}":`, err);
59
- }
60
- finally {
61
- setIsPending(false);
62
- }
10
+ const messageState = message?.componentState?.[keyName];
11
+ const [localState, setLocalState] = useState(messageState ?? initialValue);
12
+ const [initializedFromThreadMessage, setInitializedFromThreadMessage] = useState(messageState ? true : false);
13
+ // Optimistically update the local thread message's componentState
14
+ const updateLocalThreadMessage = useCallback((newState, existingMessage) => {
15
+ const updatedMessage = {
16
+ threadId: existingMessage.threadId,
17
+ componentState: {
18
+ ...existingMessage.componentState,
19
+ [keyName]: newState,
20
+ },
21
+ };
22
+ updateThreadMessage(existingMessage.id, updatedMessage, false);
23
+ }, [updateThreadMessage, keyName]);
24
+ // Debounced callback to update the remote thread message's componentState
25
+ const updateRemoteThreadMessage = useDebouncedCallback(async (newState, existingMessage) => {
26
+ const componentStateUpdate = {
27
+ state: { [keyName]: newState },
28
+ };
29
+ await client.beta.threads.messages.updateComponentState(existingMessage.threadId, existingMessage.id, componentStateUpdate);
63
30
  }, debounceTime);
64
- // Initialize state on first render if needed
65
- const initializeState = useCallback(async () => {
66
- if (!message) {
67
- console.warn(`Cannot initialize state for missing message ${messageId} with key "${keyName}"`);
31
+ const setValue = useCallback((newState) => {
32
+ setLocalState(newState);
33
+ updateLocalThreadMessage(newState, message);
34
+ updateRemoteThreadMessage(newState, message);
35
+ }, [message, updateLocalThreadMessage, updateRemoteThreadMessage]);
36
+ // Mirror the thread message's componentState value to the local state
37
+ useEffect(() => {
38
+ const messageState = message?.componentState?.[keyName];
39
+ if (!messageState) {
68
40
  return;
69
41
  }
70
- try {
71
- const messageUpdate = {
72
- ...message,
73
- componentState: {
74
- ...message.componentState,
75
- [keyName]: cachedInitialValue,
76
- },
77
- };
78
- const componentStateUpdate = {
79
- state: { [keyName]: cachedInitialValue },
80
- };
81
- await Promise.all([
82
- updateThreadMessage(messageId, messageUpdate, false),
83
- client.beta.threads.messages.updateComponentState(threadId, messageId, componentStateUpdate),
84
- ]);
85
- }
86
- catch (err) {
87
- console.warn(`Failed to initialize component state for key "${keyName}":`, err);
88
- }
89
- }, [
90
- cachedInitialValue,
91
- client.beta.threads.messages,
92
- keyName,
93
- message,
94
- messageId,
95
- threadId,
96
- updateThreadMessage,
97
- ]);
98
- // Send initial state when component mounts
42
+ setInitializedFromThreadMessage(true);
43
+ setLocalState(message.componentState?.[keyName]);
44
+ }, [message?.componentState?.[keyName], message, keyName]);
45
+ // For editable fields that are set from a prop to allow streaming updates, don't overwrite a fetched state value set from the thread message with prop value on initial load.
99
46
  useEffect(() => {
100
- if (shouldInitialize) {
101
- initializeState();
102
- setHaveInitialized(true);
103
- }
104
- }, [initializeState, shouldInitialize]);
105
- // setValue function for updating state
106
- // Updates local state immediately and schedules debounced server sync
107
- const setValue = useCallback((newValue) => {
108
- // Track this as a user-initiated update
109
- setLastUserValue(newValue);
110
- setLocalState(newValue);
111
- // Only trigger server updates if we have a message
112
- if (message) {
113
- debouncedServerWrite(newValue);
114
- const messageUpdate = {
115
- ...message,
116
- componentState: {
117
- ...message.componentState,
118
- [keyName]: newValue,
119
- },
120
- };
121
- updateThreadMessage(messageId, messageUpdate, false);
122
- }
123
- else {
124
- console.warn(`Cannot update server for missing message ${messageId} with key "${keyName}"`);
47
+ if (setFromProp !== undefined && !initializedFromThreadMessage) {
48
+ setLocalState(setFromProp);
125
49
  }
126
- }, [message, debouncedServerWrite, keyName, updateThreadMessage, messageId]);
50
+ }, [setFromProp, initializedFromThreadMessage]);
127
51
  // Ensure pending changes are flushed on unmount
128
52
  useEffect(() => {
129
53
  return () => {
130
- debouncedServerWrite.flush();
54
+ updateRemoteThreadMessage.flush();
131
55
  };
132
- }, [debouncedServerWrite]);
133
- // Return the local state for immediate UI rendering
134
- return [localState, setValue, { isPending }];
56
+ }, [updateRemoteThreadMessage]);
57
+ return [localState, setValue];
135
58
  }
136
59
  //# sourceMappingURL=use-component-state.js.map