@tambo-ai/react 0.71.0 → 0.72.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.
Files changed (83) hide show
  1. package/dist/v1/hooks/use-tambo-v1-component-state.d.ts +44 -0
  2. package/dist/v1/hooks/use-tambo-v1-component-state.d.ts.map +1 -0
  3. package/dist/v1/hooks/use-tambo-v1-component-state.js +134 -0
  4. package/dist/v1/hooks/use-tambo-v1-component-state.js.map +1 -0
  5. package/dist/v1/hooks/use-tambo-v1-component-state.test.d.ts +2 -0
  6. package/dist/v1/hooks/use-tambo-v1-component-state.test.d.ts.map +1 -0
  7. package/dist/v1/hooks/use-tambo-v1-component-state.test.js +292 -0
  8. package/dist/v1/hooks/use-tambo-v1-component-state.test.js.map +1 -0
  9. package/dist/v1/hooks/use-tambo-v1-thread-input.d.ts +62 -0
  10. package/dist/v1/hooks/use-tambo-v1-thread-input.d.ts.map +1 -0
  11. package/dist/v1/hooks/use-tambo-v1-thread-input.js +76 -0
  12. package/dist/v1/hooks/use-tambo-v1-thread-input.js.map +1 -0
  13. package/dist/v1/hooks/use-tambo-v1-thread-input.test.d.ts +2 -0
  14. package/dist/v1/hooks/use-tambo-v1-thread-input.test.d.ts.map +1 -0
  15. package/dist/v1/hooks/use-tambo-v1-thread-input.test.js +168 -0
  16. package/dist/v1/hooks/use-tambo-v1-thread-input.test.js.map +1 -0
  17. package/dist/v1/index.d.ts +23 -12
  18. package/dist/v1/index.d.ts.map +1 -1
  19. package/dist/v1/index.js +48 -14
  20. package/dist/v1/index.js.map +1 -1
  21. package/dist/v1/providers/tambo-v1-provider.d.ts +43 -1
  22. package/dist/v1/providers/tambo-v1-provider.d.ts.map +1 -1
  23. package/dist/v1/providers/tambo-v1-provider.js +24 -3
  24. package/dist/v1/providers/tambo-v1-provider.js.map +1 -1
  25. package/dist/v1/providers/tambo-v1-provider.test.js +58 -0
  26. package/dist/v1/providers/tambo-v1-provider.test.js.map +1 -1
  27. package/dist/v1/types/message.d.ts +27 -2
  28. package/dist/v1/types/message.d.ts.map +1 -1
  29. package/dist/v1/types/message.js.map +1 -1
  30. package/dist/v1/utils/component-renderer.d.ts +89 -0
  31. package/dist/v1/utils/component-renderer.d.ts.map +1 -0
  32. package/dist/v1/utils/component-renderer.js +216 -0
  33. package/dist/v1/utils/component-renderer.js.map +1 -0
  34. package/dist/v1/utils/component-renderer.test.d.ts +2 -0
  35. package/dist/v1/utils/component-renderer.test.d.ts.map +1 -0
  36. package/dist/v1/utils/component-renderer.test.js +380 -0
  37. package/dist/v1/utils/component-renderer.test.js.map +1 -0
  38. package/dist/v1/utils/event-accumulator.js +28 -8
  39. package/dist/v1/utils/event-accumulator.js.map +1 -1
  40. package/dist/v1/utils/event-accumulator.test.js +201 -6
  41. package/dist/v1/utils/event-accumulator.test.js.map +1 -1
  42. package/esm/v1/hooks/use-tambo-v1-component-state.d.ts +44 -0
  43. package/esm/v1/hooks/use-tambo-v1-component-state.d.ts.map +1 -0
  44. package/esm/v1/hooks/use-tambo-v1-component-state.js +131 -0
  45. package/esm/v1/hooks/use-tambo-v1-component-state.js.map +1 -0
  46. package/esm/v1/hooks/use-tambo-v1-component-state.test.d.ts +2 -0
  47. package/esm/v1/hooks/use-tambo-v1-component-state.test.d.ts.map +1 -0
  48. package/esm/v1/hooks/use-tambo-v1-component-state.test.js +290 -0
  49. package/esm/v1/hooks/use-tambo-v1-component-state.test.js.map +1 -0
  50. package/esm/v1/hooks/use-tambo-v1-thread-input.d.ts +62 -0
  51. package/esm/v1/hooks/use-tambo-v1-thread-input.d.ts.map +1 -0
  52. package/esm/v1/hooks/use-tambo-v1-thread-input.js +73 -0
  53. package/esm/v1/hooks/use-tambo-v1-thread-input.js.map +1 -0
  54. package/esm/v1/hooks/use-tambo-v1-thread-input.test.d.ts +2 -0
  55. package/esm/v1/hooks/use-tambo-v1-thread-input.test.d.ts.map +1 -0
  56. package/esm/v1/hooks/use-tambo-v1-thread-input.test.js +166 -0
  57. package/esm/v1/hooks/use-tambo-v1-thread-input.test.js.map +1 -0
  58. package/esm/v1/index.d.ts +23 -12
  59. package/esm/v1/index.d.ts.map +1 -1
  60. package/esm/v1/index.js +31 -12
  61. package/esm/v1/index.js.map +1 -1
  62. package/esm/v1/providers/tambo-v1-provider.d.ts +43 -1
  63. package/esm/v1/providers/tambo-v1-provider.d.ts.map +1 -1
  64. package/esm/v1/providers/tambo-v1-provider.js +24 -4
  65. package/esm/v1/providers/tambo-v1-provider.js.map +1 -1
  66. package/esm/v1/providers/tambo-v1-provider.test.js +59 -1
  67. package/esm/v1/providers/tambo-v1-provider.test.js.map +1 -1
  68. package/esm/v1/types/message.d.ts +27 -2
  69. package/esm/v1/types/message.d.ts.map +1 -1
  70. package/esm/v1/types/message.js.map +1 -1
  71. package/esm/v1/utils/component-renderer.d.ts +89 -0
  72. package/esm/v1/utils/component-renderer.d.ts.map +1 -0
  73. package/esm/v1/utils/component-renderer.js +175 -0
  74. package/esm/v1/utils/component-renderer.js.map +1 -0
  75. package/esm/v1/utils/component-renderer.test.d.ts +2 -0
  76. package/esm/v1/utils/component-renderer.test.d.ts.map +1 -0
  77. package/esm/v1/utils/component-renderer.test.js +375 -0
  78. package/esm/v1/utils/component-renderer.test.js.map +1 -0
  79. package/esm/v1/utils/event-accumulator.js +28 -8
  80. package/esm/v1/utils/event-accumulator.js.map +1 -1
  81. package/esm/v1/utils/event-accumulator.test.js +201 -6
  82. package/esm/v1/utils/event-accumulator.test.js.map +1 -1
  83. package/package.json +1 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"component-renderer.js","sourceRoot":"","sources":["../../../src/v1/utils/component-renderer.tsx"],"names":[],"mappings":"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","sourcesContent":["\"use client\";\n\n/**\n * Component Renderer Utility for v1 API\n *\n * Provides utilities for rendering React components from component content blocks.\n * Components are looked up in the registry and wrapped with context providers.\n */\n\nimport React, {\n createContext,\n createElement,\n useContext,\n useMemo,\n type ReactElement,\n} from \"react\";\n\n/**\n * Props that should be filtered out when rendering components.\n * These could be used for event handler injection or other security concerns.\n */\nconst DANGEROUS_PROP_PATTERNS = [\n /^on[A-Z]/, // Event handlers (onClick, onError, etc.)\n /^dangerouslySetInnerHTML$/,\n];\n\nconst DANGEROUS_PROP_NAMES = new Set([\"ref\", \"key\", \"children\"]);\n\n/**\n * Sanitize props by removing potentially dangerous properties.\n * Filters out event handlers, refs, and other props that could be abused.\n * @param props - Raw props from the component content\n * @returns Sanitized props safe for spreading to components\n */\nfunction sanitizeProps(\n props: Record<string, unknown>,\n): Record<string, unknown> {\n const sanitized: Record<string, unknown> = {};\n\n for (const [key, value] of Object.entries(props)) {\n // Skip dangerous prop names\n if (DANGEROUS_PROP_NAMES.has(key)) {\n continue;\n }\n\n // Skip props matching dangerous patterns\n if (DANGEROUS_PROP_PATTERNS.some((pattern) => pattern.test(key))) {\n continue;\n }\n\n sanitized[key] = value;\n }\n\n return sanitized;\n}\nimport type { ComponentRegistry } from \"../../model/component-metadata\";\nimport type {\n Content,\n TamboV1Message,\n V1ComponentContent,\n} from \"../types/message\";\n\n/**\n * Context for component content blocks.\n * Provides access to the component ID and thread ID for component state hooks.\n */\nexport interface V1ComponentContentContext {\n /** Component instance ID */\n componentId: string;\n /** Thread ID the component belongs to */\n threadId: string;\n /** Message ID the component belongs to */\n messageId: string;\n /** Component name */\n componentName: string;\n}\n\nconst ComponentContentContext = createContext<V1ComponentContentContext | null>(\n null,\n);\n\n/**\n * Provider for component content context.\n * Wraps rendered components to provide access to component metadata.\n * @returns Provider component with memoized context value\n */\nfunction V1ComponentContentProvider({\n children,\n componentId,\n threadId,\n messageId,\n componentName,\n}: V1ComponentContentContext & { children: React.ReactNode }) {\n // Memoize context value to prevent unnecessary re-renders of consumers\n const value = useMemo(\n () => ({ componentId, threadId, messageId, componentName }),\n [componentId, threadId, messageId, componentName],\n );\n\n return (\n <ComponentContentContext.Provider value={value}>\n {children}\n </ComponentContentContext.Provider>\n );\n}\n\n/**\n * Hook to access the current component content context.\n * Must be used within a rendered component.\n * @returns Component content context\n * @throws {Error} If used outside a rendered component\n */\nexport function useV1ComponentContent(): V1ComponentContentContext {\n const context = useContext(ComponentContentContext);\n if (!context) {\n throw new Error(\n \"useV1ComponentContent must be used within a rendered component\",\n );\n }\n return context;\n}\n\n/**\n * Hook to optionally access the current component content context.\n * Returns null if not within a rendered component.\n * @returns Component content context or null\n */\nexport function useV1ComponentContentOptional(): V1ComponentContentContext | null {\n return useContext(ComponentContentContext);\n}\n\n/**\n * Options for rendering a component content block.\n */\nexport interface RenderComponentOptions {\n /** Thread ID for the component context */\n threadId: string;\n /** Message ID the component belongs to */\n messageId: string;\n /** Component registry to look up components */\n componentList: ComponentRegistry;\n}\n\n/**\n * Check if a content block is a component.\n * @param content - Content block to check\n * @returns True if content is a V1ComponentContent\n */\nexport function isComponentContent(\n content: Content,\n): content is V1ComponentContent {\n return content.type === \"component\";\n}\n\n/**\n * Render a component content block into a React element.\n *\n * Looks up the component in the registry, creates a React element with props,\n * and wraps it with the component content context provider.\n * @param content - Component content block to render\n * @param options - Rendering options including registry and context info\n * @returns V1ComponentContent with the renderedComponent attached\n * @example\n * ```tsx\n * const rendered = renderComponentContent(componentContent, {\n * threadId: 'thread_123',\n * messageId: 'msg_456',\n * componentList: registry.componentList,\n * });\n *\n * // Use in JSX:\n * {rendered.renderedComponent}\n * ```\n */\nexport function renderComponentContent(\n content: V1ComponentContent,\n options: RenderComponentOptions,\n): V1ComponentContent {\n const { threadId, messageId, componentList } = options;\n\n // Look up component in registry\n const registeredComponent = componentList[content.name];\n\n if (!registeredComponent) {\n console.warn(`Component \"${content.name}\" not found in registry`);\n return {\n ...content,\n renderedComponent: null,\n };\n }\n\n const Component = registeredComponent.component;\n const LoadingComponent = registeredComponent.loadingComponent;\n\n // Determine if we should show loading state\n const isStreaming = content.streamingState !== \"done\";\n\n // Sanitize props to prevent injection of event handlers or other dangerous props\n const props = sanitizeProps(content.props as Record<string, unknown>);\n\n // Create the component element\n let element: ReactElement;\n\n if (isStreaming && LoadingComponent) {\n // Show loading component during streaming (with props for partial data display)\n element = createElement(LoadingComponent, props);\n } else {\n // Show main component - props stream in as they're filled out\n element = createElement(Component, {\n ...props,\n // Pass state as initialState prop only if not already provided\n ...(props.initialState === undefined\n ? { initialState: content.state }\n : {}),\n });\n }\n\n // Wrap with component content context\n const wrappedElement = (\n <V1ComponentContentProvider\n componentId={content.id}\n threadId={threadId}\n messageId={messageId}\n componentName={content.name}\n >\n {element}\n </V1ComponentContentProvider>\n );\n\n return {\n ...content,\n renderedComponent: wrappedElement,\n };\n}\n\n/**\n * Render all component content blocks in a message.\n *\n * Renders component content blocks and attaches renderedComponent.\n * Non-component content blocks are passed through unchanged.\n * @param content - Array of content blocks\n * @param options - Rendering options including registry and context info\n * @returns Array of content with rendered components\n */\nexport function renderMessageContent(\n content: Content[],\n options: RenderComponentOptions,\n): Content[] {\n return content.map((block) => {\n if (isComponentContent(block)) {\n return renderComponentContent(block, options);\n }\n // Pass through non-component content unchanged\n return block;\n });\n}\n\n/**\n * Render all components in a message.\n *\n * Creates a new message object with all component content blocks rendered.\n * @param message - Message to render components for\n * @param options - Rendering options (threadId is extracted from message if not provided)\n * @returns Message with rendered component content\n */\nexport function renderMessageComponents(\n message: TamboV1Message,\n options: Omit<RenderComponentOptions, \"messageId\"> & { threadId: string },\n): TamboV1Message {\n const renderedContent = renderMessageContent(message.content, {\n ...options,\n messageId: message.id,\n });\n\n return {\n ...message,\n content: renderedContent,\n };\n}\n"]}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=component-renderer.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"component-renderer.test.d.ts","sourceRoot":"","sources":["../../../src/v1/utils/component-renderer.test.tsx"],"names":[],"mappings":""}
@@ -0,0 +1,380 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ // React import needed for JSX transform (jsxImportSource is not set to react-jsx)
7
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
8
+ const react_1 = __importDefault(require("react"));
9
+ const react_2 = require("@testing-library/react");
10
+ const component_renderer_1 = require("./component-renderer");
11
+ // Test component that displays its props
12
+ function TestComponent({ title, count }) {
13
+ return (react_1.default.createElement("div", { "data-testid": "test-component" },
14
+ react_1.default.createElement("span", { "data-testid": "title" }, title),
15
+ react_1.default.createElement("span", { "data-testid": "count" }, count)));
16
+ }
17
+ // Test loading component
18
+ function TestLoadingComponent() {
19
+ return react_1.default.createElement("div", { "data-testid": "loading" }, "Loading...");
20
+ }
21
+ // Test component that uses the content context
22
+ function ContextAwareComponent() {
23
+ const context = (0, component_renderer_1.useV1ComponentContent)();
24
+ return (react_1.default.createElement("div", { "data-testid": "context-aware" },
25
+ react_1.default.createElement("span", { "data-testid": "componentId" }, context.componentId),
26
+ react_1.default.createElement("span", { "data-testid": "threadId" }, context.threadId),
27
+ react_1.default.createElement("span", { "data-testid": "messageId" }, context.messageId)));
28
+ }
29
+ const mockRegistry = {
30
+ TestComponent: {
31
+ component: TestComponent,
32
+ name: "TestComponent",
33
+ description: "A test component",
34
+ props: {},
35
+ contextTools: [],
36
+ },
37
+ TestWithLoading: {
38
+ component: TestComponent,
39
+ loadingComponent: TestLoadingComponent,
40
+ name: "TestWithLoading",
41
+ description: "A test component with loading state",
42
+ props: {},
43
+ contextTools: [],
44
+ },
45
+ ContextAware: {
46
+ component: ContextAwareComponent,
47
+ name: "ContextAware",
48
+ description: "A context-aware component",
49
+ props: {},
50
+ contextTools: [],
51
+ },
52
+ };
53
+ describe("isComponentContent", () => {
54
+ it("returns true for component content", () => {
55
+ const content = {
56
+ type: "component",
57
+ id: "comp_1",
58
+ name: "Test",
59
+ props: {},
60
+ streamingState: "done",
61
+ };
62
+ expect((0, component_renderer_1.isComponentContent)(content)).toBe(true);
63
+ });
64
+ it("returns false for text content", () => {
65
+ const content = { type: "text", text: "hello" };
66
+ expect((0, component_renderer_1.isComponentContent)(content)).toBe(false);
67
+ });
68
+ });
69
+ describe("renderComponentContent", () => {
70
+ it("renders a component from registry", () => {
71
+ const content = {
72
+ type: "component",
73
+ id: "comp_1",
74
+ name: "TestComponent",
75
+ props: { title: "Hello", count: 42 },
76
+ streamingState: "done",
77
+ };
78
+ const result = (0, component_renderer_1.renderComponentContent)(content, {
79
+ threadId: "thread_1",
80
+ messageId: "msg_1",
81
+ componentList: mockRegistry,
82
+ });
83
+ expect(result.renderedComponent).not.toBeNull();
84
+ // Render and check output
85
+ (0, react_2.render)(react_1.default.createElement(react_1.default.Fragment, null, result.renderedComponent));
86
+ expect(react_2.screen.getByTestId("title")).toHaveTextContent("Hello");
87
+ expect(react_2.screen.getByTestId("count")).toHaveTextContent("42");
88
+ });
89
+ it("returns null renderedComponent for unknown component", () => {
90
+ const content = {
91
+ type: "component",
92
+ id: "comp_1",
93
+ name: "UnknownComponent",
94
+ props: {},
95
+ streamingState: "done",
96
+ };
97
+ const consoleWarn = jest.spyOn(console, "warn").mockImplementation();
98
+ const result = (0, component_renderer_1.renderComponentContent)(content, {
99
+ threadId: "thread_1",
100
+ messageId: "msg_1",
101
+ componentList: mockRegistry,
102
+ });
103
+ expect(result.renderedComponent).toBeNull();
104
+ expect(consoleWarn).toHaveBeenCalledWith('Component "UnknownComponent" not found in registry');
105
+ consoleWarn.mockRestore();
106
+ });
107
+ it("shows loading component when streaming", () => {
108
+ const content = {
109
+ type: "component",
110
+ id: "comp_1",
111
+ name: "TestWithLoading",
112
+ props: { title: "Loading Test", count: 0 },
113
+ streamingState: "streaming",
114
+ };
115
+ const result = (0, component_renderer_1.renderComponentContent)(content, {
116
+ threadId: "thread_1",
117
+ messageId: "msg_1",
118
+ componentList: mockRegistry,
119
+ });
120
+ (0, react_2.render)(react_1.default.createElement(react_1.default.Fragment, null, result.renderedComponent));
121
+ expect(react_2.screen.getByTestId("loading")).toHaveTextContent("Loading...");
122
+ });
123
+ it("shows main component when done streaming", () => {
124
+ const content = {
125
+ type: "component",
126
+ id: "comp_1",
127
+ name: "TestWithLoading",
128
+ props: { title: "Done Test", count: 99 },
129
+ streamingState: "done",
130
+ };
131
+ const result = (0, component_renderer_1.renderComponentContent)(content, {
132
+ threadId: "thread_1",
133
+ messageId: "msg_1",
134
+ componentList: mockRegistry,
135
+ });
136
+ (0, react_2.render)(react_1.default.createElement(react_1.default.Fragment, null, result.renderedComponent));
137
+ expect(react_2.screen.getByTestId("title")).toHaveTextContent("Done Test");
138
+ expect(react_2.screen.getByTestId("count")).toHaveTextContent("99");
139
+ });
140
+ it("provides component context to rendered components", () => {
141
+ const content = {
142
+ type: "component",
143
+ id: "comp_123",
144
+ name: "ContextAware",
145
+ props: {},
146
+ streamingState: "done",
147
+ };
148
+ const result = (0, component_renderer_1.renderComponentContent)(content, {
149
+ threadId: "thread_456",
150
+ messageId: "msg_789",
151
+ componentList: mockRegistry,
152
+ });
153
+ (0, react_2.render)(react_1.default.createElement(react_1.default.Fragment, null, result.renderedComponent));
154
+ expect(react_2.screen.getByTestId("componentId")).toHaveTextContent("comp_123");
155
+ expect(react_2.screen.getByTestId("threadId")).toHaveTextContent("thread_456");
156
+ expect(react_2.screen.getByTestId("messageId")).toHaveTextContent("msg_789");
157
+ });
158
+ });
159
+ describe("renderMessageContent", () => {
160
+ it("renders component content blocks and passes through others", () => {
161
+ const content = [
162
+ { type: "text", text: "Hello" },
163
+ {
164
+ type: "component",
165
+ id: "comp_1",
166
+ name: "TestComponent",
167
+ props: { title: "Test", count: 1 },
168
+ streamingState: "done",
169
+ },
170
+ { type: "text", text: "World" },
171
+ ];
172
+ const result = (0, component_renderer_1.renderMessageContent)(content, {
173
+ threadId: "thread_1",
174
+ messageId: "msg_1",
175
+ componentList: mockRegistry,
176
+ });
177
+ expect(result).toHaveLength(3);
178
+ expect(result[0]).toEqual({ type: "text", text: "Hello" });
179
+ expect(result[1].renderedComponent).not.toBeNull();
180
+ expect(result[2]).toEqual({ type: "text", text: "World" });
181
+ });
182
+ it("handles multiple component blocks in a message", () => {
183
+ const content = [
184
+ {
185
+ type: "component",
186
+ id: "comp_1",
187
+ name: "TestComponent",
188
+ props: { title: "First", count: 1 },
189
+ streamingState: "done",
190
+ },
191
+ {
192
+ type: "component",
193
+ id: "comp_2",
194
+ name: "TestComponent",
195
+ props: { title: "Second", count: 2 },
196
+ streamingState: "done",
197
+ },
198
+ ];
199
+ const result = (0, component_renderer_1.renderMessageContent)(content, {
200
+ threadId: "thread_1",
201
+ messageId: "msg_1",
202
+ componentList: mockRegistry,
203
+ });
204
+ expect(result).toHaveLength(2);
205
+ expect(result[0].renderedComponent).not.toBeNull();
206
+ expect(result[1].renderedComponent).not.toBeNull();
207
+ });
208
+ it("handles tool_use content blocks unchanged", () => {
209
+ const content = [
210
+ {
211
+ type: "tool_use",
212
+ id: "tool_1",
213
+ name: "search",
214
+ input: { query: "test" },
215
+ },
216
+ ];
217
+ const result = (0, component_renderer_1.renderMessageContent)(content, {
218
+ threadId: "thread_1",
219
+ messageId: "msg_1",
220
+ componentList: mockRegistry,
221
+ });
222
+ expect(result).toHaveLength(1);
223
+ expect(result[0]).toEqual(content[0]);
224
+ });
225
+ });
226
+ describe("renderComponentContent edge cases", () => {
227
+ it("shows main component when streamingState is 'started' (no loading component)", () => {
228
+ const content = {
229
+ type: "component",
230
+ id: "comp_1",
231
+ name: "TestComponent",
232
+ props: { title: "Started", count: 0 },
233
+ streamingState: "started",
234
+ };
235
+ const result = (0, component_renderer_1.renderComponentContent)(content, {
236
+ threadId: "thread_1",
237
+ messageId: "msg_1",
238
+ componentList: mockRegistry,
239
+ });
240
+ // TestComponent has no loading component, so it shows main even when streaming
241
+ (0, react_2.render)(react_1.default.createElement(react_1.default.Fragment, null, result.renderedComponent));
242
+ expect(react_2.screen.getByTestId("title")).toHaveTextContent("Started");
243
+ });
244
+ it("shows loading component when streamingState is 'started' and loading exists", () => {
245
+ const content = {
246
+ type: "component",
247
+ id: "comp_1",
248
+ name: "TestWithLoading",
249
+ props: { title: "Started", count: 0 },
250
+ streamingState: "started",
251
+ };
252
+ const result = (0, component_renderer_1.renderComponentContent)(content, {
253
+ threadId: "thread_1",
254
+ messageId: "msg_1",
255
+ componentList: mockRegistry,
256
+ });
257
+ (0, react_2.render)(react_1.default.createElement(react_1.default.Fragment, null, result.renderedComponent));
258
+ expect(react_2.screen.getByTestId("loading")).toHaveTextContent("Loading...");
259
+ });
260
+ it("passes state as initialState prop", () => {
261
+ // Component that reads initialState
262
+ function StatefulComponent({ initialState, }) {
263
+ return (react_1.default.createElement("div", { "data-testid": "initial-count" }, initialState?.count ?? "none"));
264
+ }
265
+ const registryWithStateful = {
266
+ StatefulComponent: {
267
+ component: StatefulComponent,
268
+ name: "StatefulComponent",
269
+ description: "A stateful component",
270
+ props: {},
271
+ contextTools: [],
272
+ },
273
+ };
274
+ const content = {
275
+ type: "component",
276
+ id: "comp_1",
277
+ name: "StatefulComponent",
278
+ props: {},
279
+ state: { count: 42 },
280
+ streamingState: "done",
281
+ };
282
+ const result = (0, component_renderer_1.renderComponentContent)(content, {
283
+ threadId: "thread_1",
284
+ messageId: "msg_1",
285
+ componentList: registryWithStateful,
286
+ });
287
+ (0, react_2.render)(react_1.default.createElement(react_1.default.Fragment, null, result.renderedComponent));
288
+ expect(react_2.screen.getByTestId("initial-count")).toHaveTextContent("42");
289
+ });
290
+ it("preserves original content properties in returned object", () => {
291
+ const content = {
292
+ type: "component",
293
+ id: "comp_custom",
294
+ name: "TestComponent",
295
+ props: { title: "Test", count: 5 },
296
+ state: { value: "preserved" },
297
+ streamingState: "done",
298
+ };
299
+ const result = (0, component_renderer_1.renderComponentContent)(content, {
300
+ threadId: "thread_1",
301
+ messageId: "msg_1",
302
+ componentList: mockRegistry,
303
+ });
304
+ expect(result.id).toBe("comp_custom");
305
+ expect(result.name).toBe("TestComponent");
306
+ expect(result.props).toEqual({ title: "Test", count: 5 });
307
+ expect(result.state).toEqual({ value: "preserved" });
308
+ expect(result.streamingState).toBe("done");
309
+ });
310
+ });
311
+ describe("useV1ComponentContentOptional", () => {
312
+ it("returns null when used outside rendered component", () => {
313
+ function TestConsumer() {
314
+ const context = (0, component_renderer_1.useV1ComponentContentOptional)();
315
+ return (react_1.default.createElement("div", { "data-testid": "context" }, context ? "has context" : "no context"));
316
+ }
317
+ (0, react_2.render)(react_1.default.createElement(TestConsumer, null));
318
+ expect(react_2.screen.getByTestId("context")).toHaveTextContent("no context");
319
+ });
320
+ });
321
+ describe("renderMessageComponents", () => {
322
+ it("renders all components in a message", () => {
323
+ const message = {
324
+ id: "msg_1",
325
+ role: "assistant",
326
+ content: [
327
+ { type: "text", text: "Here is the weather:" },
328
+ {
329
+ type: "component",
330
+ id: "comp_1",
331
+ name: "TestComponent",
332
+ props: { title: "Weather", count: 72 },
333
+ streamingState: "done",
334
+ },
335
+ ],
336
+ createdAt: "2024-01-01T00:00:00.000Z",
337
+ };
338
+ const result = (0, component_renderer_1.renderMessageComponents)(message, {
339
+ threadId: "thread_1",
340
+ componentList: mockRegistry,
341
+ });
342
+ expect(result.id).toBe("msg_1");
343
+ expect(result.content).toHaveLength(2);
344
+ expect(result.content[0]).toEqual({
345
+ type: "text",
346
+ text: "Here is the weather:",
347
+ });
348
+ expect(result.content[1].renderedComponent).not.toBeNull();
349
+ });
350
+ it("preserves message metadata", () => {
351
+ const message = {
352
+ id: "msg_123",
353
+ role: "assistant",
354
+ content: [],
355
+ createdAt: "2024-01-01T00:00:00.000Z",
356
+ metadata: { custom: "value" },
357
+ };
358
+ const result = (0, component_renderer_1.renderMessageComponents)(message, {
359
+ threadId: "thread_1",
360
+ componentList: mockRegistry,
361
+ });
362
+ expect(result.id).toBe("msg_123");
363
+ expect(result.role).toBe("assistant");
364
+ expect(result.createdAt).toBe("2024-01-01T00:00:00.000Z");
365
+ expect(result.metadata).toEqual({ custom: "value" });
366
+ });
367
+ });
368
+ describe("useV1ComponentContent error handling", () => {
369
+ it("throws when used outside rendered component", () => {
370
+ function TestConsumer() {
371
+ (0, component_renderer_1.useV1ComponentContent)();
372
+ return react_1.default.createElement("div", null, "Should not render");
373
+ }
374
+ // Suppress React error boundary logs
375
+ const consoleSpy = jest.spyOn(console, "error").mockImplementation();
376
+ expect(() => (0, react_2.render)(react_1.default.createElement(TestConsumer, null))).toThrow("useV1ComponentContent must be used within a rendered component");
377
+ consoleSpy.mockRestore();
378
+ });
379
+ });
380
+ //# sourceMappingURL=component-renderer.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"component-renderer.test.js","sourceRoot":"","sources":["../../../src/v1/utils/component-renderer.test.tsx"],"names":[],"mappings":"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","sourcesContent":["// React import needed for JSX transform (jsxImportSource is not set to react-jsx)\n// eslint-disable-next-line @typescript-eslint/no-unused-vars\nimport React from \"react\";\nimport { render, screen } from \"@testing-library/react\";\nimport type { ComponentRegistry } from \"../../model/component-metadata\";\nimport type { V1ComponentContent } from \"../types/message\";\nimport {\n isComponentContent,\n renderComponentContent,\n renderMessageContent,\n renderMessageComponents,\n useV1ComponentContent,\n useV1ComponentContentOptional,\n} from \"./component-renderer\";\n\n// Test component that displays its props\nfunction TestComponent({ title, count }: { title: string; count: number }) {\n return (\n <div data-testid=\"test-component\">\n <span data-testid=\"title\">{title}</span>\n <span data-testid=\"count\">{count}</span>\n </div>\n );\n}\n\n// Test loading component\nfunction TestLoadingComponent() {\n return <div data-testid=\"loading\">Loading...</div>;\n}\n\n// Test component that uses the content context\nfunction ContextAwareComponent() {\n const context = useV1ComponentContent();\n return (\n <div data-testid=\"context-aware\">\n <span data-testid=\"componentId\">{context.componentId}</span>\n <span data-testid=\"threadId\">{context.threadId}</span>\n <span data-testid=\"messageId\">{context.messageId}</span>\n </div>\n );\n}\n\nconst mockRegistry: ComponentRegistry = {\n TestComponent: {\n component: TestComponent,\n name: \"TestComponent\",\n description: \"A test component\",\n props: {},\n contextTools: [],\n },\n TestWithLoading: {\n component: TestComponent,\n loadingComponent: TestLoadingComponent,\n name: \"TestWithLoading\",\n description: \"A test component with loading state\",\n props: {},\n contextTools: [],\n },\n ContextAware: {\n component: ContextAwareComponent,\n name: \"ContextAware\",\n description: \"A context-aware component\",\n props: {},\n contextTools: [],\n },\n};\n\ndescribe(\"isComponentContent\", () => {\n it(\"returns true for component content\", () => {\n const content: V1ComponentContent = {\n type: \"component\",\n id: \"comp_1\",\n name: \"Test\",\n props: {},\n streamingState: \"done\",\n };\n expect(isComponentContent(content)).toBe(true);\n });\n\n it(\"returns false for text content\", () => {\n const content = { type: \"text\", text: \"hello\" };\n expect(isComponentContent(content as any)).toBe(false);\n });\n});\n\ndescribe(\"renderComponentContent\", () => {\n it(\"renders a component from registry\", () => {\n const content: V1ComponentContent = {\n type: \"component\",\n id: \"comp_1\",\n name: \"TestComponent\",\n props: { title: \"Hello\", count: 42 },\n streamingState: \"done\",\n };\n\n const result = renderComponentContent(content, {\n threadId: \"thread_1\",\n messageId: \"msg_1\",\n componentList: mockRegistry,\n });\n\n expect(result.renderedComponent).not.toBeNull();\n\n // Render and check output\n render(<>{result.renderedComponent}</>);\n expect(screen.getByTestId(\"title\")).toHaveTextContent(\"Hello\");\n expect(screen.getByTestId(\"count\")).toHaveTextContent(\"42\");\n });\n\n it(\"returns null renderedComponent for unknown component\", () => {\n const content: V1ComponentContent = {\n type: \"component\",\n id: \"comp_1\",\n name: \"UnknownComponent\",\n props: {},\n streamingState: \"done\",\n };\n\n const consoleWarn = jest.spyOn(console, \"warn\").mockImplementation();\n\n const result = renderComponentContent(content, {\n threadId: \"thread_1\",\n messageId: \"msg_1\",\n componentList: mockRegistry,\n });\n\n expect(result.renderedComponent).toBeNull();\n expect(consoleWarn).toHaveBeenCalledWith(\n 'Component \"UnknownComponent\" not found in registry',\n );\n\n consoleWarn.mockRestore();\n });\n\n it(\"shows loading component when streaming\", () => {\n const content: V1ComponentContent = {\n type: \"component\",\n id: \"comp_1\",\n name: \"TestWithLoading\",\n props: { title: \"Loading Test\", count: 0 },\n streamingState: \"streaming\",\n };\n\n const result = renderComponentContent(content, {\n threadId: \"thread_1\",\n messageId: \"msg_1\",\n componentList: mockRegistry,\n });\n\n render(<>{result.renderedComponent}</>);\n expect(screen.getByTestId(\"loading\")).toHaveTextContent(\"Loading...\");\n });\n\n it(\"shows main component when done streaming\", () => {\n const content: V1ComponentContent = {\n type: \"component\",\n id: \"comp_1\",\n name: \"TestWithLoading\",\n props: { title: \"Done Test\", count: 99 },\n streamingState: \"done\",\n };\n\n const result = renderComponentContent(content, {\n threadId: \"thread_1\",\n messageId: \"msg_1\",\n componentList: mockRegistry,\n });\n\n render(<>{result.renderedComponent}</>);\n expect(screen.getByTestId(\"title\")).toHaveTextContent(\"Done Test\");\n expect(screen.getByTestId(\"count\")).toHaveTextContent(\"99\");\n });\n\n it(\"provides component context to rendered components\", () => {\n const content: V1ComponentContent = {\n type: \"component\",\n id: \"comp_123\",\n name: \"ContextAware\",\n props: {},\n streamingState: \"done\",\n };\n\n const result = renderComponentContent(content, {\n threadId: \"thread_456\",\n messageId: \"msg_789\",\n componentList: mockRegistry,\n });\n\n render(<>{result.renderedComponent}</>);\n expect(screen.getByTestId(\"componentId\")).toHaveTextContent(\"comp_123\");\n expect(screen.getByTestId(\"threadId\")).toHaveTextContent(\"thread_456\");\n expect(screen.getByTestId(\"messageId\")).toHaveTextContent(\"msg_789\");\n });\n});\n\ndescribe(\"renderMessageContent\", () => {\n it(\"renders component content blocks and passes through others\", () => {\n const content = [\n { type: \"text\", text: \"Hello\" },\n {\n type: \"component\",\n id: \"comp_1\",\n name: \"TestComponent\",\n props: { title: \"Test\", count: 1 },\n streamingState: \"done\",\n } as V1ComponentContent,\n { type: \"text\", text: \"World\" },\n ];\n\n const result = renderMessageContent(content as any, {\n threadId: \"thread_1\",\n messageId: \"msg_1\",\n componentList: mockRegistry,\n });\n\n expect(result).toHaveLength(3);\n expect(result[0]).toEqual({ type: \"text\", text: \"Hello\" });\n expect((result[1] as any).renderedComponent).not.toBeNull();\n expect(result[2]).toEqual({ type: \"text\", text: \"World\" });\n });\n\n it(\"handles multiple component blocks in a message\", () => {\n const content = [\n {\n type: \"component\",\n id: \"comp_1\",\n name: \"TestComponent\",\n props: { title: \"First\", count: 1 },\n streamingState: \"done\",\n } as V1ComponentContent,\n {\n type: \"component\",\n id: \"comp_2\",\n name: \"TestComponent\",\n props: { title: \"Second\", count: 2 },\n streamingState: \"done\",\n } as V1ComponentContent,\n ];\n\n const result = renderMessageContent(content as any, {\n threadId: \"thread_1\",\n messageId: \"msg_1\",\n componentList: mockRegistry,\n });\n\n expect(result).toHaveLength(2);\n expect((result[0] as V1ComponentContent).renderedComponent).not.toBeNull();\n expect((result[1] as V1ComponentContent).renderedComponent).not.toBeNull();\n });\n\n it(\"handles tool_use content blocks unchanged\", () => {\n const content = [\n {\n type: \"tool_use\",\n id: \"tool_1\",\n name: \"search\",\n input: { query: \"test\" },\n },\n ];\n\n const result = renderMessageContent(content as any, {\n threadId: \"thread_1\",\n messageId: \"msg_1\",\n componentList: mockRegistry,\n });\n\n expect(result).toHaveLength(1);\n expect(result[0]).toEqual(content[0]);\n });\n});\n\ndescribe(\"renderComponentContent edge cases\", () => {\n it(\"shows main component when streamingState is 'started' (no loading component)\", () => {\n const content: V1ComponentContent = {\n type: \"component\",\n id: \"comp_1\",\n name: \"TestComponent\",\n props: { title: \"Started\", count: 0 },\n streamingState: \"started\",\n };\n\n const result = renderComponentContent(content, {\n threadId: \"thread_1\",\n messageId: \"msg_1\",\n componentList: mockRegistry,\n });\n\n // TestComponent has no loading component, so it shows main even when streaming\n render(<>{result.renderedComponent}</>);\n expect(screen.getByTestId(\"title\")).toHaveTextContent(\"Started\");\n });\n\n it(\"shows loading component when streamingState is 'started' and loading exists\", () => {\n const content: V1ComponentContent = {\n type: \"component\",\n id: \"comp_1\",\n name: \"TestWithLoading\",\n props: { title: \"Started\", count: 0 },\n streamingState: \"started\",\n };\n\n const result = renderComponentContent(content, {\n threadId: \"thread_1\",\n messageId: \"msg_1\",\n componentList: mockRegistry,\n });\n\n render(<>{result.renderedComponent}</>);\n expect(screen.getByTestId(\"loading\")).toHaveTextContent(\"Loading...\");\n });\n\n it(\"passes state as initialState prop\", () => {\n // Component that reads initialState\n function StatefulComponent({\n initialState,\n }: {\n initialState?: { count: number };\n }) {\n return (\n <div data-testid=\"initial-count\">{initialState?.count ?? \"none\"}</div>\n );\n }\n\n const registryWithStateful: ComponentRegistry = {\n StatefulComponent: {\n component: StatefulComponent,\n name: \"StatefulComponent\",\n description: \"A stateful component\",\n props: {},\n contextTools: [],\n },\n };\n\n const content: V1ComponentContent = {\n type: \"component\",\n id: \"comp_1\",\n name: \"StatefulComponent\",\n props: {},\n state: { count: 42 },\n streamingState: \"done\",\n };\n\n const result = renderComponentContent(content, {\n threadId: \"thread_1\",\n messageId: \"msg_1\",\n componentList: registryWithStateful,\n });\n\n render(<>{result.renderedComponent}</>);\n expect(screen.getByTestId(\"initial-count\")).toHaveTextContent(\"42\");\n });\n\n it(\"preserves original content properties in returned object\", () => {\n const content: V1ComponentContent = {\n type: \"component\",\n id: \"comp_custom\",\n name: \"TestComponent\",\n props: { title: \"Test\", count: 5 },\n state: { value: \"preserved\" },\n streamingState: \"done\",\n };\n\n const result = renderComponentContent(content, {\n threadId: \"thread_1\",\n messageId: \"msg_1\",\n componentList: mockRegistry,\n });\n\n expect(result.id).toBe(\"comp_custom\");\n expect(result.name).toBe(\"TestComponent\");\n expect(result.props).toEqual({ title: \"Test\", count: 5 });\n expect(result.state).toEqual({ value: \"preserved\" });\n expect(result.streamingState).toBe(\"done\");\n });\n});\n\ndescribe(\"useV1ComponentContentOptional\", () => {\n it(\"returns null when used outside rendered component\", () => {\n function TestConsumer() {\n const context = useV1ComponentContentOptional();\n return (\n <div data-testid=\"context\">\n {context ? \"has context\" : \"no context\"}\n </div>\n );\n }\n\n render(<TestConsumer />);\n expect(screen.getByTestId(\"context\")).toHaveTextContent(\"no context\");\n });\n});\n\ndescribe(\"renderMessageComponents\", () => {\n it(\"renders all components in a message\", () => {\n const message = {\n id: \"msg_1\",\n role: \"assistant\" as const,\n content: [\n { type: \"text\" as const, text: \"Here is the weather:\" },\n {\n type: \"component\",\n id: \"comp_1\",\n name: \"TestComponent\",\n props: { title: \"Weather\", count: 72 },\n streamingState: \"done\",\n } as V1ComponentContent,\n ],\n createdAt: \"2024-01-01T00:00:00.000Z\",\n };\n\n const result = renderMessageComponents(message, {\n threadId: \"thread_1\",\n componentList: mockRegistry,\n });\n\n expect(result.id).toBe(\"msg_1\");\n expect(result.content).toHaveLength(2);\n expect(result.content[0]).toEqual({\n type: \"text\",\n text: \"Here is the weather:\",\n });\n expect(\n (result.content[1] as V1ComponentContent).renderedComponent,\n ).not.toBeNull();\n });\n\n it(\"preserves message metadata\", () => {\n const message = {\n id: \"msg_123\",\n role: \"assistant\" as const,\n content: [],\n createdAt: \"2024-01-01T00:00:00.000Z\",\n metadata: { custom: \"value\" },\n };\n\n const result = renderMessageComponents(message, {\n threadId: \"thread_1\",\n componentList: mockRegistry,\n });\n\n expect(result.id).toBe(\"msg_123\");\n expect(result.role).toBe(\"assistant\");\n expect(result.createdAt).toBe(\"2024-01-01T00:00:00.000Z\");\n expect(result.metadata).toEqual({ custom: \"value\" });\n });\n});\n\ndescribe(\"useV1ComponentContent error handling\", () => {\n it(\"throws when used outside rendered component\", () => {\n function TestConsumer() {\n useV1ComponentContent();\n return <div>Should not render</div>;\n }\n\n // Suppress React error boundary logs\n const consoleSpy = jest.spyOn(console, \"error\").mockImplementation();\n\n expect(() => render(<TestConsumer />)).toThrow(\n \"useV1ComponentContent must be used within a rendered component\",\n );\n\n consoleSpy.mockRestore();\n });\n});\n"]}
@@ -617,7 +617,7 @@ function handleCustomEvent(threadState, event) {
617
617
  }
618
618
  /**
619
619
  * Handle tambo.component.start event.
620
- * Adds a component content block to the message.
620
+ * Adds a component content block to the message with 'started' streaming state.
621
621
  * @param threadState - Current thread state
622
622
  * @param event - Component start event
623
623
  * @returns Updated thread state
@@ -631,12 +631,13 @@ function handleComponentStart(threadState, event) {
631
631
  throw new Error(`Message ${messageId} not found for tambo.component.start event`);
632
632
  }
633
633
  const message = messages[messageIndex];
634
- // Add component content block
634
+ // Add component content block with 'started' streaming state
635
635
  const newContent = {
636
636
  type: "component",
637
637
  id: event.value.componentId,
638
638
  name: event.value.componentName,
639
639
  props: {},
640
+ streamingState: "started",
640
641
  };
641
642
  const updatedMessage = {
642
643
  ...message,
@@ -646,7 +647,7 @@ function handleComponentStart(threadState, event) {
646
647
  }
647
648
  /**
648
649
  * Handle component delta events (both props_delta and state_delta).
649
- * Applies JSON Patch to the specified field.
650
+ * Applies JSON Patch to the specified field and sets streamingState to 'streaming'.
650
651
  * @param threadState - Current thread state
651
652
  * @param event - Component delta event (props or state)
652
653
  * @param field - Which field to update ('props' or 'state')
@@ -670,9 +671,11 @@ function handleComponentDelta(threadState, event, field) {
670
671
  : (componentContent.state ?? {});
671
672
  // Apply JSON Patch
672
673
  const updatedValue = (0, json_patch_1.applyJsonPatch)(currentValue, operations);
674
+ // Update field and set streaming state to 'streaming'
673
675
  const updatedContent = {
674
676
  ...componentContent,
675
677
  [field]: updatedValue,
678
+ streamingState: "streaming",
676
679
  };
677
680
  const updatedMessage = {
678
681
  ...message,
@@ -682,14 +685,31 @@ function handleComponentDelta(threadState, event, field) {
682
685
  }
683
686
  /**
684
687
  * Handle tambo.component.end event.
685
- * Marks component as complete.
688
+ * Sets component streaming state to 'done'.
686
689
  * @param threadState - Current thread state
687
- * @param _event - Component end event (unused)
690
+ * @param event - Component end event
688
691
  * @returns Updated thread state
689
692
  */
690
- function handleComponentEnd(threadState, _event) {
691
- // For now, this doesn't change state
692
- return threadState;
693
+ function handleComponentEnd(threadState, event) {
694
+ const componentId = event.value.componentId;
695
+ const messages = threadState.thread.messages;
696
+ // Find the component content block
697
+ const { messageIndex, contentIndex } = findContentById(messages, "component", componentId, "tambo.component.end event");
698
+ const message = messages[messageIndex];
699
+ const componentContent = message.content[contentIndex];
700
+ if (componentContent.type !== "component") {
701
+ throw new Error(`Content at index ${contentIndex} is not a component block for tambo.component.end event`);
702
+ }
703
+ // Set streaming state to 'done'
704
+ const updatedContent = {
705
+ ...componentContent,
706
+ streamingState: "done",
707
+ };
708
+ const updatedMessage = {
709
+ ...message,
710
+ content: updateContentAtIndex(message.content, contentIndex, updatedContent),
711
+ };
712
+ return updateThreadMessages(threadState, updateMessageAtIndex(messages, messageIndex, updatedMessage));
693
713
  }
694
714
  /**
695
715
  * Handle tambo.run.awaiting_input event.