@langchain/react 0.3.4 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/README.md +48 -523
  2. package/dist/context.cjs +12 -30
  3. package/dist/context.cjs.map +1 -1
  4. package/dist/context.d.cts +22 -39
  5. package/dist/context.d.cts.map +1 -1
  6. package/dist/context.d.ts +22 -39
  7. package/dist/context.d.ts.map +1 -1
  8. package/dist/context.js +11 -29
  9. package/dist/context.js.map +1 -1
  10. package/dist/index.cjs +29 -30
  11. package/dist/index.d.cts +10 -7
  12. package/dist/index.d.cts.map +1 -1
  13. package/dist/index.d.ts +10 -7
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +10 -6
  16. package/dist/selectors.cjs +178 -0
  17. package/dist/selectors.cjs.map +1 -0
  18. package/dist/selectors.d.cts +183 -0
  19. package/dist/selectors.d.cts.map +1 -0
  20. package/dist/selectors.d.ts +183 -0
  21. package/dist/selectors.d.ts.map +1 -0
  22. package/dist/selectors.js +168 -0
  23. package/dist/selectors.js.map +1 -0
  24. package/dist/suspense-stream.cjs +34 -159
  25. package/dist/suspense-stream.cjs.map +1 -1
  26. package/dist/suspense-stream.d.cts +15 -71
  27. package/dist/suspense-stream.d.cts.map +1 -1
  28. package/dist/suspense-stream.d.ts +15 -71
  29. package/dist/suspense-stream.d.ts.map +1 -1
  30. package/dist/suspense-stream.js +35 -158
  31. package/dist/suspense-stream.js.map +1 -1
  32. package/dist/use-audio-player.cjs +679 -0
  33. package/dist/use-audio-player.cjs.map +1 -0
  34. package/dist/use-audio-player.d.cts +161 -0
  35. package/dist/use-audio-player.d.cts.map +1 -0
  36. package/dist/use-audio-player.d.ts +161 -0
  37. package/dist/use-audio-player.d.ts.map +1 -0
  38. package/dist/use-audio-player.js +679 -0
  39. package/dist/use-audio-player.js.map +1 -0
  40. package/dist/use-media-url.cjs +49 -0
  41. package/dist/use-media-url.cjs.map +1 -0
  42. package/dist/use-media-url.d.cts +28 -0
  43. package/dist/use-media-url.d.cts.map +1 -0
  44. package/dist/use-media-url.d.ts +28 -0
  45. package/dist/use-media-url.d.ts.map +1 -0
  46. package/dist/use-media-url.js +49 -0
  47. package/dist/use-media-url.js.map +1 -0
  48. package/dist/use-projection.cjs +41 -0
  49. package/dist/use-projection.cjs.map +1 -0
  50. package/dist/use-projection.d.cts +27 -0
  51. package/dist/use-projection.d.cts.map +1 -0
  52. package/dist/use-projection.d.ts +27 -0
  53. package/dist/use-projection.d.ts.map +1 -0
  54. package/dist/use-projection.js +41 -0
  55. package/dist/use-projection.js.map +1 -0
  56. package/dist/use-stream.cjs +185 -0
  57. package/dist/use-stream.cjs.map +1 -0
  58. package/dist/use-stream.d.cts +184 -0
  59. package/dist/use-stream.d.cts.map +1 -0
  60. package/dist/use-stream.d.ts +184 -0
  61. package/dist/use-stream.d.ts.map +1 -0
  62. package/dist/use-stream.js +183 -0
  63. package/dist/use-stream.js.map +1 -0
  64. package/dist/use-video-player.cjs +218 -0
  65. package/dist/use-video-player.cjs.map +1 -0
  66. package/dist/use-video-player.d.cts +65 -0
  67. package/dist/use-video-player.d.cts.map +1 -0
  68. package/dist/use-video-player.d.ts +65 -0
  69. package/dist/use-video-player.d.ts.map +1 -0
  70. package/dist/use-video-player.js +218 -0
  71. package/dist/use-video-player.js.map +1 -0
  72. package/package.json +9 -8
  73. package/dist/stream.cjs +0 -18
  74. package/dist/stream.cjs.map +0 -1
  75. package/dist/stream.custom.cjs +0 -209
  76. package/dist/stream.custom.cjs.map +0 -1
  77. package/dist/stream.custom.d.cts +0 -3
  78. package/dist/stream.custom.d.ts +0 -3
  79. package/dist/stream.custom.js +0 -209
  80. package/dist/stream.custom.js.map +0 -1
  81. package/dist/stream.d.cts +0 -174
  82. package/dist/stream.d.cts.map +0 -1
  83. package/dist/stream.d.ts +0 -174
  84. package/dist/stream.d.ts.map +0 -1
  85. package/dist/stream.js +0 -18
  86. package/dist/stream.js.map +0 -1
  87. package/dist/stream.lgp.cjs +0 -671
  88. package/dist/stream.lgp.cjs.map +0 -1
  89. package/dist/stream.lgp.js +0 -671
  90. package/dist/stream.lgp.js.map +0 -1
  91. package/dist/thread.cjs +0 -18
  92. package/dist/thread.cjs.map +0 -1
  93. package/dist/thread.js +0 -18
  94. package/dist/thread.js.map +0 -1
  95. package/dist/types.d.cts +0 -109
  96. package/dist/types.d.cts.map +0 -1
  97. package/dist/types.d.ts +0 -109
  98. package/dist/types.d.ts.map +0 -1
@@ -0,0 +1,183 @@
1
+ "use client";
2
+ import { useEffect, useMemo, useRef, useSyncExternalStore } from "react";
3
+ import { filterOutHeadlessToolInterrupts, flushPendingHeadlessToolInterrupts } from "@langchain/langgraph-sdk";
4
+ import { Client as Client$1 } from "@langchain/langgraph-sdk/client";
5
+ import { StreamController } from "@langchain/langgraph-sdk/stream";
6
+ //#region src/use-stream.ts
7
+ /**
8
+ * Private field on the hook return that carries the
9
+ * {@link StreamController} reference. Selector hooks (`useMessages`,
10
+ * `useToolCalls`, …) read this to reach the shared
11
+ * {@link ChannelRegistry}. Typed as a symbol-keyed field to discourage
12
+ * end-user access — use the selector hooks instead.
13
+ *
14
+ * Exported as a unique symbol so type narrowing works across
15
+ * `useMessages(stream, target)` call sites.
16
+ */
17
+ const STREAM_CONTROLLER = Symbol.for("@langchain/react/controller");
18
+ /**
19
+ * React binding for the v2-native stream runtime.
20
+ *
21
+ * `useStream` exposes three always-on projections
22
+ * (`values` / `messages` / `toolCalls`) at the thread root plus
23
+ * cheap discovery maps for subagents / subgraphs. Scoped views of
24
+ * subagents, subgraphs, or any namespaced projection are surfaced via
25
+ * the companion selector hooks:
26
+ *
27
+ * ```tsx
28
+ * const stream = useStream({ assistantId: "deep-agent" });
29
+ *
30
+ * // Root messages — always on, already class instances.
31
+ * stream.messages.map((m) => <Bubble key={m.id} msg={m} />);
32
+ *
33
+ * // Subagent view — mount = subscribe, unmount = unsubscribe.
34
+ * function SubagentCard({ subagent }) {
35
+ * const messages = useMessages(stream, subagent);
36
+ * const toolCalls = useToolCalls(stream, subagent);
37
+ * return <>{messages.map(...)}</>;
38
+ * }
39
+ * ```
40
+ *
41
+ * The first generic accepts either a plain state type
42
+ * (`useStream<MyState>()`) *or* a compiled graph type
43
+ * (`useStream<typeof agent>()`); in the latter case the
44
+ * state shape is unwrapped from the graph via {@link InferStateType}, so
45
+ * `stream.values` is always typed as the state, never as the graph
46
+ * class itself.
47
+ */
48
+ function useStream(options) {
49
+ const asBag = options;
50
+ const hasCustomAdapter = asBag.transport != null && typeof asBag.transport !== "string";
51
+ const transport = asBag.transport;
52
+ const client = useMemo(() => asBag.client ?? new Client$1({
53
+ apiUrl: asBag.apiUrl,
54
+ apiKey: asBag.apiKey,
55
+ callerOptions: asBag.callerOptions,
56
+ defaultHeaders: asBag.defaultHeaders
57
+ }), [
58
+ asBag.client,
59
+ asBag.apiUrl,
60
+ asBag.apiKey,
61
+ asBag.callerOptions,
62
+ asBag.defaultHeaders
63
+ ]);
64
+ const sentinel = "_";
65
+ const assistantId = "assistantId" in options ? options.assistantId ?? sentinel : sentinel;
66
+ const controller = useMemo(() => new StreamController({
67
+ assistantId,
68
+ client,
69
+ threadId: options.threadId ?? null,
70
+ transport,
71
+ fetch: hasCustomAdapter ? void 0 : asBag.fetch,
72
+ webSocketFactory: hasCustomAdapter ? void 0 : asBag.webSocketFactory,
73
+ onThreadId: options.onThreadId,
74
+ onCreated: options.onCreated,
75
+ initialValues: options.initialValues,
76
+ messagesKey: options.messagesKey
77
+ }), [
78
+ client,
79
+ assistantId,
80
+ transport
81
+ ]);
82
+ const lastHydratedRef = useRef(null);
83
+ useEffect(() => {
84
+ const target = options.threadId ?? null;
85
+ const last = lastHydratedRef.current;
86
+ if (last?.controller !== controller) {
87
+ lastHydratedRef.current = {
88
+ controller,
89
+ threadId: target
90
+ };
91
+ return;
92
+ }
93
+ if (last.threadId === target) return;
94
+ lastHydratedRef.current = {
95
+ controller,
96
+ threadId: target
97
+ };
98
+ controller.hydrate(target);
99
+ }, [controller, options.threadId]);
100
+ useEffect(() => controller.activate(), [controller]);
101
+ const handledToolsRef = useRef(/* @__PURE__ */ new Set());
102
+ useEffect(() => {
103
+ handledToolsRef.current.clear();
104
+ }, [options.threadId]);
105
+ const tools = options.tools;
106
+ const onTool = options.onTool;
107
+ const rootValuesForTools = useSyncExternalStore(controller.rootStore.subscribe, () => controller.rootStore.getSnapshot().values, () => controller.rootStore.getSnapshot().values);
108
+ const rootInterruptsForTools = useSyncExternalStore(controller.rootStore.subscribe, () => controller.rootStore.getSnapshot().interrupts, () => controller.rootStore.getSnapshot().interrupts);
109
+ useEffect(() => {
110
+ if (!tools?.length) return;
111
+ const valuesBag = rootValuesForTools;
112
+ const combined = [...Array.isArray(valuesBag?.__interrupt__) ? valuesBag.__interrupt__ : [], ...rootInterruptsForTools];
113
+ if (combined.length === 0) return;
114
+ flushPendingHeadlessToolInterrupts({
115
+ ...valuesBag,
116
+ __interrupt__: combined
117
+ }, tools, handledToolsRef.current, {
118
+ onTool,
119
+ defer: (run) => {
120
+ Promise.resolve().then(run);
121
+ },
122
+ resumeSubmit: (command) => controller.submit(null, { command })
123
+ });
124
+ }, [
125
+ controller,
126
+ tools,
127
+ onTool,
128
+ rootValuesForTools,
129
+ rootInterruptsForTools
130
+ ]);
131
+ const root = useSyncExternalStore(controller.rootStore.subscribe, controller.rootStore.getSnapshot, controller.rootStore.getSnapshot);
132
+ const subagents = useSyncExternalStore(controller.subagentStore.subscribe, controller.subagentStore.getSnapshot, controller.subagentStore.getSnapshot);
133
+ const subgraphs = useSyncExternalStore(controller.subgraphStore.subscribe, controller.subgraphStore.getSnapshot, controller.subgraphStore.getSnapshot);
134
+ const subgraphsByNode = useSyncExternalStore(controller.subgraphByNodeStore.subscribe, controller.subgraphByNodeStore.getSnapshot, controller.subgraphByNodeStore.getSnapshot);
135
+ return useMemo(() => {
136
+ const userFacingInterrupts = filterOutHeadlessToolInterrupts(root.interrupts);
137
+ return {
138
+ values: root.values,
139
+ messages: root.messages,
140
+ toolCalls: root.toolCalls,
141
+ interrupts: userFacingInterrupts,
142
+ interrupt: userFacingInterrupts[0],
143
+ isLoading: root.isLoading,
144
+ isThreadLoading: root.isThreadLoading,
145
+ hydrationPromise: controller.hydrationPromise,
146
+ error: root.error,
147
+ threadId: root.threadId,
148
+ subagents,
149
+ subgraphs,
150
+ subgraphsByNode,
151
+ submit: (input, submitOptions) => controller.submit(input, submitOptions),
152
+ stop: () => controller.stop(),
153
+ respond: (response, target) => controller.respond(response, target),
154
+ getThread: () => controller.getThread(),
155
+ client,
156
+ assistantId,
157
+ [STREAM_CONTROLLER]: controller
158
+ };
159
+ }, [
160
+ root,
161
+ subagents,
162
+ subgraphs,
163
+ subgraphsByNode,
164
+ controller,
165
+ client,
166
+ assistantId
167
+ ]);
168
+ }
169
+ /**
170
+ * Helper used by the selector hooks to reach the underlying
171
+ * {@link ChannelRegistry} from a stream handle. Kept internal —
172
+ * application code should call `useMessages`, `useToolCalls`, etc.
173
+ * instead of reading this directly.
174
+ *
175
+ * @internal
176
+ */
177
+ function getRegistry(stream) {
178
+ return stream[STREAM_CONTROLLER].registry;
179
+ }
180
+ //#endregion
181
+ export { STREAM_CONTROLLER, getRegistry, useStream };
182
+
183
+ //# sourceMappingURL=use-stream.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-stream.js","names":["ClientCtor"],"sources":["../src/use-stream.ts"],"sourcesContent":["/* __LC_ALLOW_ENTRYPOINT_SIDE_EFFECTS__ */\n\n\"use client\";\n\nimport { useEffect, useMemo, useRef, useSyncExternalStore } from \"react\";\nimport type { BaseMessage } from \"@langchain/core/messages\";\nimport type { Client, Interrupt } from \"@langchain/langgraph-sdk\";\nimport {\n filterOutHeadlessToolInterrupts,\n flushPendingHeadlessToolInterrupts,\n} from \"@langchain/langgraph-sdk\";\nimport {\n Client as ClientCtor,\n type ClientConfig,\n type ThreadStream,\n} from \"@langchain/langgraph-sdk/client\";\nimport {\n StreamController,\n type AgentServerAdapter,\n type AgentServerOptions as StreamAgentServerOptions,\n type AssembledToolCall,\n type ChannelRegistry,\n type CustomAdapterOptions as StreamCustomAdapterOptions,\n type InferStateType,\n type InferSubagentStates,\n type RootSnapshot,\n type StreamSubmitOptions,\n type SubagentDiscoverySnapshot,\n type SubagentMap,\n type SubgraphByNodeMap,\n type SubgraphDiscoverySnapshot,\n type SubgraphMap,\n type UseStreamOptions as StreamUseStreamOptions,\n type WidenUpdateMessages,\n} from \"@langchain/langgraph-sdk/stream\";\n\nexport type AgentServerOptions<StateType extends object> =\n StreamAgentServerOptions<StateType>;\n\nexport type CustomAdapterOptions<StateType extends object> =\n StreamCustomAdapterOptions<StateType>;\n\nexport type UseStreamOptions<\n StateType extends object = Record<string, unknown>,\n> = StreamUseStreamOptions<StateType>;\n\n/**\n * Private field on the hook return that carries the\n * {@link StreamController} reference. Selector hooks (`useMessages`,\n * `useToolCalls`, …) read this to reach the shared\n * {@link ChannelRegistry}. Typed as a symbol-keyed field to discourage\n * end-user access — use the selector hooks instead.\n *\n * Exported as a unique symbol so type narrowing works across\n * `useMessages(stream, target)` call sites.\n */\nexport const STREAM_CONTROLLER: unique symbol = Symbol.for(\n \"@langchain/react/controller\"\n);\n\nexport interface UseStreamReturn<\n T = Record<string, unknown>,\n InterruptType = unknown,\n ConfigurableType extends object = Record<string, unknown>,\n StateType extends object = InferStateType<T>,\n SubagentStates = InferSubagentStates<T>,\n> {\n // ----- always-on root projections -----\n /**\n * The most recent `values`-channel snapshot emitted at the root\n * namespace — i.e. the thread-level state as the server sees it\n * after each superstep. Updated on every root `values` event, not\n * on token-level deltas: if you render `values.messages` directly\n * you'll see full turns appear at once instead of streaming\n * token-by-token. Use {@link messages} (or `useMessages`) for the\n * token-streamed view.\n *\n * Equivalent to calling `useValues(stream)`.\n */\n readonly values: StateType;\n /**\n * Type-only: the resolved state shape. Exposed so consumers can\n * derive companion hook argument types (`useValues<typeof stream>`)\n * without plumbing `T` through their component hierarchy.\n *\n * @internal\n */\n readonly \"~stateType\"?: StateType;\n /**\n * The root message projection. Assembled from two sources and\n * merged in real time:\n *\n * 1. `messages`-channel deltas — token-level streaming events\n * (`message-start`, `content-block-delta`, `message-finish`)\n * emitted by the runtime. These drive live, token-by-token\n * updates.\n * 2. `values.messages` snapshots — the authoritative ordering\n * and any messages the agent produces without token streaming\n * (human turns, tool results, echoes from subagents).\n *\n * If the backend only emits `values` events (no `messages`\n * channel), every message will appear fully-formed on each\n * values update rather than streaming. This is a backend/runtime\n * concern — the React layer faithfully renders whatever the\n * server sends.\n *\n * Equivalent to calling `useMessages(stream)` with no target.\n */\n readonly messages: BaseMessage[];\n readonly toolCalls: AssembledToolCall[];\n readonly interrupts: Interrupt<InterruptType>[];\n readonly interrupt: Interrupt<InterruptType> | undefined;\n readonly isLoading: boolean;\n readonly isThreadLoading: boolean;\n /**\n * Promise that settles when the current thread's initial hydration\n * completes. Exposed so Suspense wrappers can `throw` it until the\n * first {@link StreamController.hydrate} call resolves (or rejects)\n * for the active thread. A fresh promise is installed on every\n * `switchThread`/`threadId` change.\n */\n readonly hydrationPromise: Promise<void>;\n readonly error: unknown;\n readonly threadId: string | null;\n\n // ----- always-on discovery -----\n /**\n * Subagents discovered on the root run. For DeepAgent-typed\n * streams the key set is narrowed to the subagent names declared\n * on the agent brand (`keyof InferSubagentStates<T>`).\n */\n readonly subagents: ReadonlyMap<\n keyof SubagentStates & string extends never\n ? string\n : keyof SubagentStates & string,\n SubagentDiscoverySnapshot\n >;\n /**\n * Subgraphs discovered on the root run.\n *\n * A namespace is classified as a subgraph iff at least one\n * strictly-deeper namespace has been observed with it as a prefix.\n * This is inferred from the lifecycle event stream — plain function\n * nodes (`orchestrator`, `writer` in the nested-stategraph example)\n * never appear here even though the server emits namespaced\n * lifecycle events for them. Promotion is monotonic and retroactive;\n * an entry appears as soon as the first descendant event lands.\n */\n readonly subgraphs: ReadonlyMap<string, SubgraphDiscoverySnapshot>;\n /**\n * Subgraphs indexed by the graph node that produced them\n * (`addNode(\"visualizer_0\", …)`). Each value is an array because\n * parallel fan-outs and loops can spawn multiple invocations of\n * the same node; arrays preserve insertion order. Updates in\n * lock-step with {@link subgraphs}.\n */\n readonly subgraphsByNode: ReadonlyMap<\n string,\n readonly SubgraphDiscoverySnapshot[]\n >;\n\n // ----- imperatives -----\n /**\n * Dispatch a new run on the bound thread.\n *\n * `input` is typed as `Partial<StateType>` so IDE autocompletion\n * surfaces the state keys declared on the root hook. Pass `null`\n * (or omit fields) when resuming an interrupt via `options.command.resume`\n * — the server accepts a null payload in that case.\n */\n submit(\n input: WidenUpdateMessages<Partial<StateType>> | null | undefined,\n options?: StreamSubmitOptions<StateType, ConfigurableType>\n ): Promise<void>;\n stop(): Promise<void>;\n respond(\n response: unknown,\n target?: { interruptId: string; namespace?: string[] }\n ): Promise<void>;\n\n // ----- identity -----\n readonly client: Client;\n readonly assistantId: string;\n\n /** v2 escape hatch — returns the bound {@link ThreadStream}. */\n getThread(): ThreadStream | undefined;\n\n /** @internal Used by selector hooks (`useMessages`, `useToolCalls`, …). */\n readonly [STREAM_CONTROLLER]: StreamController<\n StateType,\n InterruptType,\n ConfigurableType\n >;\n}\n\n/**\n * Erased stream handle useful as a parameter type for helpers and\n * wrapper components that pass a `stream` through to selector hooks\n * (`useMessages`, `useChannel`, …) without reading `values` directly.\n * Any fully-typed `UseStreamReturn<S, I, C>` is\n * assignable to `AnyStream` because the generic slots are `any`\n * (bivariant), which avoids the `CompiledStateGraph` → `Record<string,\n * unknown>` assignment friction you hit when using the bare\n * `UseStreamReturn` default.\n *\n * @example\n * ```tsx\n * function SubgraphCard({ stream, subgraph }: {\n * stream: AnyStream;\n * subgraph: SubgraphDiscoverySnapshot;\n * }) {\n * const messages = useMessages(stream, subgraph);\n * return <Feed messages={messages} />;\n * }\n * ```\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport type AnyStream = UseStreamReturn<any, any, any>;\n\n/**\n * React binding for the v2-native stream runtime.\n *\n * `useStream` exposes three always-on projections\n * (`values` / `messages` / `toolCalls`) at the thread root plus\n * cheap discovery maps for subagents / subgraphs. Scoped views of\n * subagents, subgraphs, or any namespaced projection are surfaced via\n * the companion selector hooks:\n *\n * ```tsx\n * const stream = useStream({ assistantId: \"deep-agent\" });\n *\n * // Root messages — always on, already class instances.\n * stream.messages.map((m) => <Bubble key={m.id} msg={m} />);\n *\n * // Subagent view — mount = subscribe, unmount = unsubscribe.\n * function SubagentCard({ subagent }) {\n * const messages = useMessages(stream, subagent);\n * const toolCalls = useToolCalls(stream, subagent);\n * return <>{messages.map(...)}</>;\n * }\n * ```\n *\n * The first generic accepts either a plain state type\n * (`useStream<MyState>()`) *or* a compiled graph type\n * (`useStream<typeof agent>()`); in the latter case the\n * state shape is unwrapped from the graph via {@link InferStateType}, so\n * `stream.values` is always typed as the state, never as the graph\n * class itself.\n */\nexport function useStream<\n T = Record<string, unknown>,\n InterruptType = unknown,\n ConfigurableType extends object = Record<string, unknown>,\n>(\n options: UseStreamOptions<InferStateType<T>>\n): UseStreamReturn<T, InterruptType, ConfigurableType> {\n type StateType = InferStateType<T>;\n // Branch-stable narrowings for each code path. The custom-adapter\n // branch can skip LGP client construction entirely, which keeps\n // bundles that *only* use a custom adapter free of the default\n // `sse`/`websocket` transport factories (tree-shaken).\n // Treat the options as a flat bag here — the discriminated union\n // exists to give call sites a nice error message, but at runtime\n // both branches are reachable through the same set of fields.\n interface OptionsBag {\n assistantId?: string;\n threadId?: string | null;\n client?: Client;\n apiUrl?: string;\n apiKey?: string;\n callerOptions?: ClientConfig[\"callerOptions\"];\n defaultHeaders?: ClientConfig[\"defaultHeaders\"];\n transport?: \"sse\" | \"websocket\" | AgentServerAdapter;\n fetch?: typeof fetch;\n webSocketFactory?: (url: string) => WebSocket;\n onThreadId?: (threadId: string) => void;\n onCreated?: (meta: { run_id: string; thread_id: string }) => void;\n initialValues?: StateType;\n messagesKey?: string;\n }\n const asBag = options as OptionsBag;\n // Narrow once: a non-string `transport` is a custom adapter; anything\n // else (`\"sse\"` / `\"websocket\"` / `undefined`) is a built-in.\n const hasCustomAdapter =\n asBag.transport != null && typeof asBag.transport !== \"string\";\n const transport = asBag.transport;\n\n const client = useMemo<Client>(\n () =>\n asBag.client ??\n (new ClientCtor({\n apiUrl: asBag.apiUrl,\n apiKey: asBag.apiKey,\n callerOptions: asBag.callerOptions,\n defaultHeaders: asBag.defaultHeaders,\n }) as unknown as Client),\n [\n asBag.client,\n asBag.apiUrl,\n asBag.apiKey,\n asBag.callerOptions,\n asBag.defaultHeaders,\n ]\n );\n\n // Custom adapters may omit `assistantId`; the controller still\n // requires one so it has something to forward to `threads.stream`.\n // `\"_\"` is the well-known sentinel for \"adapter doesn't care\".\n const sentinel = \"_\";\n const assistantId =\n \"assistantId\" in options ? (options.assistantId ?? sentinel) : sentinel;\n\n // Recreate the controller only on assistantId / client / transport\n // change; the ThreadStream is bound to one assistant for its\n // lifetime and we want selector-hook subscriptions to stay stable\n // across renders.\n const controller = useMemo(\n () =>\n new StreamController<StateType, InterruptType, ConfigurableType>({\n assistantId,\n // Cast: the runtime `Client` is state-shape agnostic, but the\n // controller declares `client: Client<StateType>` so its own\n // typings line up. Tightening `submit`'s `input` parameter to\n // `Partial<StateType>` surfaced this variance mismatch that\n // was previously masked — the cast is equivalent to the\n // ClientCtor cast above.\n client: client as unknown as Client<StateType>,\n threadId: options.threadId ?? null,\n transport,\n fetch: hasCustomAdapter ? undefined : asBag.fetch,\n webSocketFactory: hasCustomAdapter ? undefined : asBag.webSocketFactory,\n onThreadId: options.onThreadId,\n onCreated: options.onCreated,\n initialValues: options.initialValues,\n messagesKey: options.messagesKey,\n }),\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [client, assistantId, transport]\n );\n\n // Rehydrate on threadId change. The initial hydrate is fired\n // synchronously inside the controller constructor so Suspense\n // callers don't deadlock waiting for an effect that never runs\n // (throwing `hydrationPromise` during render unmounts the subtree\n // before effects fire). We only re-hydrate here when the threadId\n // prop changes after the controller was already constructed with a\n // matching id.\n const lastHydratedRef = useRef<{\n controller: StreamController<StateType, InterruptType, ConfigurableType>;\n threadId: string | null;\n } | null>(null);\n useEffect(() => {\n const target = options.threadId ?? null;\n const last = lastHydratedRef.current;\n if (last?.controller !== controller) {\n // Freshly constructed controller already seeded the hydrate in\n // its constructor — record the id and skip the redundant call.\n lastHydratedRef.current = { controller, threadId: target };\n return;\n }\n if (last.threadId === target) return;\n lastHydratedRef.current = { controller, threadId: target };\n void controller.hydrate(target);\n }, [controller, options.threadId]);\n\n // Dispose on unmount / controller swap.\n //\n // We use `controller.activate()` instead of a naive\n // `() => controller.dispose()` cleanup because React 18+\n // `<StrictMode>` in dev mounts → unmounts → remounts components\n // synchronously to surface cleanup bugs. A naive cleanup would\n // permanently tear the controller down on that first synthetic\n // unmount and turn every subsequent `submit()` into a silent\n // no-op. `activate()` defers disposal to the next microtask and\n // cancels it if the effect re-runs — which is exactly the\n // StrictMode remount pattern.\n useEffect(() => controller.activate(), [controller]);\n\n // Headless-tool handling: if the caller supplied `tools`, watch the\n // root `values.__interrupt__` channel for protocol interrupts that\n // target a registered tool, invoke the handler, and auto-resume the\n // run. Ref-tracks the ids we've already handled so the same\n // interrupt on a subsequent render is never executed twice\n // (StrictMode safe).\n const handledToolsRef = useRef<Set<string>>(new Set());\n useEffect(() => {\n handledToolsRef.current.clear();\n }, [options.threadId]);\n const tools = options.tools;\n const onTool = options.onTool;\n // Subscribe to values + interrupt updates via the root store so the\n // effect re-runs whenever a protocol interrupt lands or the\n // `__interrupt__` key is projected into values, not only on hook\n // re-render. We feed both sources to the flush helper because\n // v2-native runs surface protocol interrupts via\n // `rootStore.interrupts` (`input.requested` events), while legacy\n // graphs may still emit `values.__interrupt__`.\n const rootValuesForTools = useSyncExternalStore<StateType>(\n controller.rootStore.subscribe,\n () => controller.rootStore.getSnapshot().values,\n () => controller.rootStore.getSnapshot().values\n );\n const rootInterruptsForTools = useSyncExternalStore<\n readonly Interrupt<InterruptType>[]\n >(\n controller.rootStore.subscribe,\n () => controller.rootStore.getSnapshot().interrupts,\n () => controller.rootStore.getSnapshot().interrupts\n );\n useEffect(() => {\n if (!tools?.length) return;\n const valuesBag = rootValuesForTools as unknown as Record<string, unknown>;\n const existingInterrupts = Array.isArray(valuesBag?.__interrupt__)\n ? (valuesBag.__interrupt__ as Interrupt[])\n : [];\n const combined: Interrupt[] = [\n ...existingInterrupts,\n ...(rootInterruptsForTools as unknown as Interrupt[]),\n ];\n if (combined.length === 0) return;\n flushPendingHeadlessToolInterrupts(\n { ...valuesBag, __interrupt__: combined },\n tools,\n handledToolsRef.current,\n {\n onTool,\n defer: (run) => {\n void Promise.resolve().then(run);\n },\n resumeSubmit: (command) =>\n controller.submit(null, {\n command,\n } as StreamSubmitOptions<StateType, ConfigurableType>),\n }\n );\n }, [controller, tools, onTool, rootValuesForTools, rootInterruptsForTools]);\n\n const root = useSyncExternalStore<RootSnapshot<StateType, InterruptType>>(\n controller.rootStore.subscribe,\n controller.rootStore.getSnapshot,\n controller.rootStore.getSnapshot\n );\n const subagents = useSyncExternalStore<SubagentMap>(\n controller.subagentStore.subscribe,\n controller.subagentStore.getSnapshot,\n controller.subagentStore.getSnapshot\n );\n const subgraphs = useSyncExternalStore<SubgraphMap>(\n controller.subgraphStore.subscribe,\n controller.subgraphStore.getSnapshot,\n controller.subgraphStore.getSnapshot\n );\n const subgraphsByNode = useSyncExternalStore<SubgraphByNodeMap>(\n controller.subgraphByNodeStore.subscribe,\n controller.subgraphByNodeStore.getSnapshot,\n controller.subgraphByNodeStore.getSnapshot\n );\n\n return useMemo<UseStreamReturn<T, InterruptType, ConfigurableType>>(() => {\n const userFacingInterrupts = filterOutHeadlessToolInterrupts(\n root.interrupts\n );\n return {\n values: root.values,\n messages: root.messages,\n toolCalls: root.toolCalls,\n interrupts: userFacingInterrupts,\n interrupt: userFacingInterrupts[0],\n isLoading: root.isLoading,\n isThreadLoading: root.isThreadLoading,\n hydrationPromise: controller.hydrationPromise,\n error: root.error,\n threadId: root.threadId,\n subagents: subagents as UseStreamReturn<\n T,\n InterruptType,\n ConfigurableType\n >[\"subagents\"],\n subgraphs,\n subgraphsByNode,\n submit: (input, submitOptions) => controller.submit(input, submitOptions),\n stop: () => controller.stop(),\n respond: (response, target) => controller.respond(response, target),\n getThread: () => controller.getThread(),\n client,\n assistantId,\n [STREAM_CONTROLLER]: controller,\n } as UseStreamReturn<T, InterruptType, ConfigurableType>;\n }, [\n root,\n subagents,\n subgraphs,\n subgraphsByNode,\n controller,\n client,\n assistantId,\n ]);\n}\n\n/**\n * Convenience alias for the fully-resolved stream handle type.\n */\nexport type UseStreamResult<\n T = Record<string, unknown>,\n InterruptType = unknown,\n ConfigurableType extends object = Record<string, unknown>,\n> = UseStreamReturn<T, InterruptType, ConfigurableType>;\n\n/**\n * Helper used by the selector hooks to reach the underlying\n * {@link ChannelRegistry} from a stream handle. Kept internal —\n * application code should call `useMessages`, `useToolCalls`, etc.\n * instead of reading this directly.\n *\n * @internal\n */\nexport function getRegistry(\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n stream: UseStreamReturn<any, any, any>\n): ChannelRegistry {\n return stream[STREAM_CONTROLLER].registry;\n}\n\nexport type { ThreadStream };\n"],"mappings":";;;;;;;;;;;;;;;;AAwDA,MAAa,oBAAmC,OAAO,IACrD,8BACD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+LD,SAAgB,UAKd,SACqD;CAyBrD,MAAM,QAAQ;CAGd,MAAM,mBACJ,MAAM,aAAa,QAAQ,OAAO,MAAM,cAAc;CACxD,MAAM,YAAY,MAAM;CAExB,MAAM,SAAS,cAEX,MAAM,UACL,IAAIA,SAAW;EACd,QAAQ,MAAM;EACd,QAAQ,MAAM;EACd,eAAe,MAAM;EACrB,gBAAgB,MAAM;EACvB,CAAC,EACJ;EACE,MAAM;EACN,MAAM;EACN,MAAM;EACN,MAAM;EACN,MAAM;EACP,CACF;CAKD,MAAM,WAAW;CACjB,MAAM,cACJ,iBAAiB,UAAW,QAAQ,eAAe,WAAY;CAMjE,MAAM,aAAa,cAEf,IAAI,iBAA6D;EAC/D;EAOQ;EACR,UAAU,QAAQ,YAAY;EAC9B;EACA,OAAO,mBAAmB,KAAA,IAAY,MAAM;EAC5C,kBAAkB,mBAAmB,KAAA,IAAY,MAAM;EACvD,YAAY,QAAQ;EACpB,WAAW,QAAQ;EACnB,eAAe,QAAQ;EACvB,aAAa,QAAQ;EACtB,CAAC,EAEJ;EAAC;EAAQ;EAAa;EAAU,CACjC;CASD,MAAM,kBAAkB,OAGd,KAAK;AACf,iBAAgB;EACd,MAAM,SAAS,QAAQ,YAAY;EACnC,MAAM,OAAO,gBAAgB;AAC7B,MAAI,MAAM,eAAe,YAAY;AAGnC,mBAAgB,UAAU;IAAE;IAAY,UAAU;IAAQ;AAC1D;;AAEF,MAAI,KAAK,aAAa,OAAQ;AAC9B,kBAAgB,UAAU;GAAE;GAAY,UAAU;GAAQ;AACrD,aAAW,QAAQ,OAAO;IAC9B,CAAC,YAAY,QAAQ,SAAS,CAAC;AAalC,iBAAgB,WAAW,UAAU,EAAE,CAAC,WAAW,CAAC;CAQpD,MAAM,kBAAkB,uBAAoB,IAAI,KAAK,CAAC;AACtD,iBAAgB;AACd,kBAAgB,QAAQ,OAAO;IAC9B,CAAC,QAAQ,SAAS,CAAC;CACtB,MAAM,QAAQ,QAAQ;CACtB,MAAM,SAAS,QAAQ;CAQvB,MAAM,qBAAqB,qBACzB,WAAW,UAAU,iBACf,WAAW,UAAU,aAAa,CAAC,cACnC,WAAW,UAAU,aAAa,CAAC,OAC1C;CACD,MAAM,yBAAyB,qBAG7B,WAAW,UAAU,iBACf,WAAW,UAAU,aAAa,CAAC,kBACnC,WAAW,UAAU,aAAa,CAAC,WAC1C;AACD,iBAAgB;AACd,MAAI,CAAC,OAAO,OAAQ;EACpB,MAAM,YAAY;EAIlB,MAAM,WAAwB,CAC5B,GAJyB,MAAM,QAAQ,WAAW,cAAc,GAC7D,UAAU,gBACX,EAAE,EAGJ,GAAI,uBACL;AACD,MAAI,SAAS,WAAW,EAAG;AAC3B,qCACE;GAAE,GAAG;GAAW,eAAe;GAAU,EACzC,OACA,gBAAgB,SAChB;GACE;GACA,QAAQ,QAAQ;AACT,YAAQ,SAAS,CAAC,KAAK,IAAI;;GAElC,eAAe,YACb,WAAW,OAAO,MAAM,EACtB,SACD,CAAqD;GACzD,CACF;IACA;EAAC;EAAY;EAAO;EAAQ;EAAoB;EAAuB,CAAC;CAE3E,MAAM,OAAO,qBACX,WAAW,UAAU,WACrB,WAAW,UAAU,aACrB,WAAW,UAAU,YACtB;CACD,MAAM,YAAY,qBAChB,WAAW,cAAc,WACzB,WAAW,cAAc,aACzB,WAAW,cAAc,YAC1B;CACD,MAAM,YAAY,qBAChB,WAAW,cAAc,WACzB,WAAW,cAAc,aACzB,WAAW,cAAc,YAC1B;CACD,MAAM,kBAAkB,qBACtB,WAAW,oBAAoB,WAC/B,WAAW,oBAAoB,aAC/B,WAAW,oBAAoB,YAChC;AAED,QAAO,cAAmE;EACxE,MAAM,uBAAuB,gCAC3B,KAAK,WACN;AACD,SAAO;GACL,QAAQ,KAAK;GACb,UAAU,KAAK;GACf,WAAW,KAAK;GAChB,YAAY;GACZ,WAAW,qBAAqB;GAChC,WAAW,KAAK;GAChB,iBAAiB,KAAK;GACtB,kBAAkB,WAAW;GAC7B,OAAO,KAAK;GACZ,UAAU,KAAK;GACJ;GAKX;GACA;GACA,SAAS,OAAO,kBAAkB,WAAW,OAAO,OAAO,cAAc;GACzE,YAAY,WAAW,MAAM;GAC7B,UAAU,UAAU,WAAW,WAAW,QAAQ,UAAU,OAAO;GACnE,iBAAiB,WAAW,WAAW;GACvC;GACA;IACC,oBAAoB;GACtB;IACA;EACD;EACA;EACA;EACA;EACA;EACA;EACA;EACD,CAAC;;;;;;;;;;AAoBJ,SAAgB,YAEd,QACiB;AACjB,QAAO,OAAO,mBAAmB"}
@@ -0,0 +1,218 @@
1
+ "use client";
2
+ let react = require("react");
3
+ //#region src/use-video-player.ts
4
+ /**
5
+ * Bind a {@link VideoMedia} handle to a caller-owned `<video>` element.
6
+ *
7
+ * ### Contract
8
+ *
9
+ * - The caller renders `<video ref={videoRef}>` and styles it however
10
+ * they like; the hook never injects DOM nor overrides layout.
11
+ * - On `message-finish`, the underlying blob URL is minted and assigned
12
+ * as `video.src`. Progressive playback of streamed container video
13
+ * (fragmented mp4 / webm via MSE) is out of scope for this version;
14
+ * `status` stays in `"buffering"` until the blob resolves.
15
+ * - Element events (`play` / `pause` / `ended` / `timeupdate` /
16
+ * `loadedmetadata` / `error`) are translated into the shared
17
+ * {@link PlayerStatus} enum.
18
+ * - On unmount, `media.revoke()` is called to free the object URL.
19
+ *
20
+ * @param videoRef - Ref to the `<video>` element the caller renders.
21
+ * @param media - Video handle from {@link useVideo}.
22
+ * @param options - Auto-play toggle.
23
+ */
24
+ function useVideoPlayer(videoRef, media, options) {
25
+ const autoPlay = options?.autoPlay ?? false;
26
+ const [status, setStatus] = (0, react.useState)("idle");
27
+ const [error, setError] = (0, react.useState)(void 0);
28
+ const [currentTime, setCurrentTime] = (0, react.useState)(0);
29
+ const [duration, setDuration] = (0, react.useState)(void 0);
30
+ const statusRef = (0, react.useRef)("idle");
31
+ (0, react.useEffect)(() => {
32
+ statusRef.current = status;
33
+ }, [status]);
34
+ const shouldPlayRef = (0, react.useRef)(false);
35
+ const pendingResolveRef = (0, react.useRef)(null);
36
+ const pendingRejectRef = (0, react.useRef)(null);
37
+ const resolvePending = (0, react.useCallback)(() => {
38
+ const resolve = pendingResolveRef.current;
39
+ pendingResolveRef.current = null;
40
+ pendingRejectRef.current = null;
41
+ resolve?.();
42
+ }, []);
43
+ const rejectPending = (0, react.useCallback)((err) => {
44
+ const reject = pendingRejectRef.current;
45
+ pendingResolveRef.current = null;
46
+ pendingRejectRef.current = null;
47
+ reject?.(err);
48
+ }, []);
49
+ (0, react.useEffect)(() => {
50
+ if (status === "finished" || status === "paused" || status === "idle") resolvePending();
51
+ else if (status === "error") rejectPending(error ?? /* @__PURE__ */ new Error("playback error"));
52
+ }, [
53
+ status,
54
+ error,
55
+ resolvePending,
56
+ rejectPending
57
+ ]);
58
+ const play = (0, react.useCallback)(() => {
59
+ if (media == null) return;
60
+ if (statusRef.current === "error") return;
61
+ shouldPlayRef.current = true;
62
+ const video = videoRef.current;
63
+ if (video == null) {
64
+ setStatus("buffering");
65
+ return;
66
+ }
67
+ video.play().catch((err) => {
68
+ setError(err);
69
+ setStatus("error");
70
+ });
71
+ }, [media, videoRef]);
72
+ const pause = (0, react.useCallback)(() => {
73
+ shouldPlayRef.current = false;
74
+ videoRef.current?.pause();
75
+ if (statusRef.current === "playing" || statusRef.current === "buffering") setStatus("paused");
76
+ }, [videoRef]);
77
+ const stop = (0, react.useCallback)(() => {
78
+ shouldPlayRef.current = false;
79
+ const video = videoRef.current;
80
+ if (video != null) {
81
+ video.pause();
82
+ video.currentTime = 0;
83
+ }
84
+ setCurrentTime(0);
85
+ setStatus(media == null ? "idle" : "paused");
86
+ }, [videoRef, media]);
87
+ const reset = (0, react.useCallback)(() => {
88
+ stop();
89
+ setError(void 0);
90
+ setDuration(void 0);
91
+ setStatus("idle");
92
+ }, [stop]);
93
+ const toggle = (0, react.useCallback)(() => {
94
+ if (statusRef.current === "playing") pause();
95
+ else play();
96
+ }, [play, pause]);
97
+ const playToEnd = (0, react.useCallback)(() => {
98
+ pendingResolveRef.current?.();
99
+ pendingResolveRef.current = null;
100
+ pendingRejectRef.current = null;
101
+ return new Promise((resolve, reject) => {
102
+ pendingResolveRef.current = resolve;
103
+ pendingRejectRef.current = reject;
104
+ play();
105
+ });
106
+ }, [play]);
107
+ const seek = (0, react.useCallback)((seconds) => {
108
+ const video = videoRef.current;
109
+ if (video == null) return;
110
+ video.currentTime = seconds;
111
+ setCurrentTime(seconds);
112
+ }, [videoRef]);
113
+ (0, react.useEffect)(() => {
114
+ if (media?.error == null) return;
115
+ setError(new Error(media.error.message));
116
+ setStatus("error");
117
+ }, [media]);
118
+ (0, react.useEffect)(() => {
119
+ if (media == null) {
120
+ setStatus("idle");
121
+ setError(void 0);
122
+ setCurrentTime(0);
123
+ setDuration(void 0);
124
+ return;
125
+ }
126
+ setError(void 0);
127
+ setStatus("buffering");
128
+ setCurrentTime(0);
129
+ setDuration(void 0);
130
+ let cancelled = false;
131
+ const video = videoRef.current;
132
+ media.objectURL.then((resolved) => {
133
+ if (cancelled) return;
134
+ if (video == null) return;
135
+ video.src = resolved;
136
+ if (shouldPlayRef.current || autoPlay) video.play().catch((err) => {
137
+ setError(err);
138
+ setStatus("error");
139
+ });
140
+ else setStatus("paused");
141
+ }, () => {
142
+ if (!cancelled) {
143
+ setError(/* @__PURE__ */ new Error("media failed to materialise"));
144
+ setStatus("error");
145
+ }
146
+ });
147
+ if (video == null) return () => {
148
+ cancelled = true;
149
+ try {
150
+ media.revoke();
151
+ } catch {}
152
+ };
153
+ const onPlay = () => {
154
+ if (statusRef.current !== "error") setStatus("playing");
155
+ };
156
+ const onPause = () => {
157
+ if (video.ended) return;
158
+ if (statusRef.current === "playing") setStatus("paused");
159
+ };
160
+ const onEnded = () => {
161
+ setStatus("finished");
162
+ };
163
+ const onTimeUpdate = () => {
164
+ setCurrentTime(video.currentTime);
165
+ };
166
+ const onLoadedMetadata = () => {
167
+ if (Number.isFinite(video.duration)) setDuration(video.duration);
168
+ };
169
+ const onError = () => {
170
+ setError(/* @__PURE__ */ new Error("HTMLVideoElement error"));
171
+ setStatus("error");
172
+ };
173
+ video.addEventListener("play", onPlay);
174
+ video.addEventListener("pause", onPause);
175
+ video.addEventListener("ended", onEnded);
176
+ video.addEventListener("timeupdate", onTimeUpdate);
177
+ video.addEventListener("loadedmetadata", onLoadedMetadata);
178
+ video.addEventListener("error", onError);
179
+ return () => {
180
+ cancelled = true;
181
+ video.removeEventListener("play", onPlay);
182
+ video.removeEventListener("pause", onPause);
183
+ video.removeEventListener("ended", onEnded);
184
+ video.removeEventListener("timeupdate", onTimeUpdate);
185
+ video.removeEventListener("loadedmetadata", onLoadedMetadata);
186
+ video.removeEventListener("error", onError);
187
+ try {
188
+ video.pause();
189
+ video.removeAttribute("src");
190
+ video.load();
191
+ } catch {}
192
+ try {
193
+ media.revoke();
194
+ } catch {}
195
+ };
196
+ }, [
197
+ media,
198
+ videoRef,
199
+ autoPlay
200
+ ]);
201
+ return {
202
+ status,
203
+ play,
204
+ pause,
205
+ stop,
206
+ toggle,
207
+ reset,
208
+ playToEnd,
209
+ currentTime,
210
+ duration,
211
+ seek,
212
+ error
213
+ };
214
+ }
215
+ //#endregion
216
+ exports.useVideoPlayer = useVideoPlayer;
217
+
218
+ //# sourceMappingURL=use-video-player.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-video-player.cjs","names":[],"sources":["../src/use-video-player.ts"],"sourcesContent":["/* __LC_ALLOW_ENTRYPOINT_SIDE_EFFECTS__ */\n\n\"use client\";\n\nimport {\n useCallback,\n useEffect,\n useRef,\n useState,\n type RefObject,\n} from \"react\";\nimport type { VideoMedia } from \"@langchain/langgraph-sdk/stream\";\nimport type { PlayerStatus } from \"./use-audio-player.js\";\n\n/**\n * Options for {@link useVideoPlayer}.\n */\nexport interface UseVideoPlayerOptions {\n /**\n * Start playback as soon as the blob URL resolves. Subject to\n * browser autoplay policies — pair with `muted={true}` on the\n * `<video>` element to bypass the user-gesture requirement.\n */\n autoPlay?: boolean;\n}\n\n/**\n * Controls + live state returned by {@link useVideoPlayer}. Mirrors\n * {@link AudioPlayerHandle} on the shared subset so callers only ever\n * learn one shape.\n */\nexport interface VideoPlayerHandle {\n status: PlayerStatus;\n play(): void;\n pause(): void;\n stop(): void;\n toggle(): void;\n reset(): void;\n /**\n * Resolve on the next terminal transition (`finished` / `paused` /\n * `idle`). Reject on transitions to `\"error\"`. Triggers `play()`\n * when called.\n */\n playToEnd(): Promise<void>;\n\n currentTime: number;\n /** Total duration (seconds) once the element has parsed the blob. */\n duration?: number;\n /** Seek to an absolute timestamp in seconds. */\n seek(seconds: number): void;\n\n error: Error | undefined;\n}\n\n/**\n * Bind a {@link VideoMedia} handle to a caller-owned `<video>` element.\n *\n * ### Contract\n *\n * - The caller renders `<video ref={videoRef}>` and styles it however\n * they like; the hook never injects DOM nor overrides layout.\n * - On `message-finish`, the underlying blob URL is minted and assigned\n * as `video.src`. Progressive playback of streamed container video\n * (fragmented mp4 / webm via MSE) is out of scope for this version;\n * `status` stays in `\"buffering\"` until the blob resolves.\n * - Element events (`play` / `pause` / `ended` / `timeupdate` /\n * `loadedmetadata` / `error`) are translated into the shared\n * {@link PlayerStatus} enum.\n * - On unmount, `media.revoke()` is called to free the object URL.\n *\n * @param videoRef - Ref to the `<video>` element the caller renders.\n * @param media - Video handle from {@link useVideo}.\n * @param options - Auto-play toggle.\n */\nexport function useVideoPlayer(\n videoRef: RefObject<HTMLVideoElement | null>,\n media: VideoMedia | undefined,\n options?: UseVideoPlayerOptions\n): VideoPlayerHandle {\n const autoPlay = options?.autoPlay ?? false;\n\n const [status, setStatus] = useState<PlayerStatus>(\"idle\");\n const [error, setError] = useState<Error | undefined>(undefined);\n const [currentTime, setCurrentTime] = useState(0);\n const [duration, setDuration] = useState<number | undefined>(undefined);\n\n const statusRef = useRef<PlayerStatus>(\"idle\");\n useEffect(() => {\n statusRef.current = status;\n }, [status]);\n\n const shouldPlayRef = useRef(false);\n const pendingResolveRef = useRef<(() => void) | null>(null);\n const pendingRejectRef = useRef<((err: Error) => void) | null>(null);\n\n const resolvePending = useCallback(() => {\n const resolve = pendingResolveRef.current;\n pendingResolveRef.current = null;\n pendingRejectRef.current = null;\n resolve?.();\n }, []);\n\n const rejectPending = useCallback((err: Error) => {\n const reject = pendingRejectRef.current;\n pendingResolveRef.current = null;\n pendingRejectRef.current = null;\n reject?.(err);\n }, []);\n\n useEffect(() => {\n if (status === \"finished\" || status === \"paused\" || status === \"idle\") {\n resolvePending();\n } else if (status === \"error\") {\n rejectPending(error ?? new Error(\"playback error\"));\n }\n }, [status, error, resolvePending, rejectPending]);\n\n const play = useCallback(() => {\n if (media == null) return;\n if (statusRef.current === \"error\") return;\n shouldPlayRef.current = true;\n const video = videoRef.current;\n if (video == null) {\n setStatus(\"buffering\");\n return;\n }\n video.play().catch((err) => {\n setError(err as Error);\n setStatus(\"error\");\n });\n }, [media, videoRef]);\n\n const pause = useCallback(() => {\n shouldPlayRef.current = false;\n videoRef.current?.pause();\n if (statusRef.current === \"playing\" || statusRef.current === \"buffering\") {\n setStatus(\"paused\");\n }\n }, [videoRef]);\n\n const stop = useCallback(() => {\n shouldPlayRef.current = false;\n const video = videoRef.current;\n if (video != null) {\n video.pause();\n video.currentTime = 0;\n }\n setCurrentTime(0);\n setStatus(media == null ? \"idle\" : \"paused\");\n }, [videoRef, media]);\n\n const reset = useCallback(() => {\n stop();\n setError(undefined);\n setDuration(undefined);\n setStatus(\"idle\");\n }, [stop]);\n\n const toggle = useCallback(() => {\n if (statusRef.current === \"playing\") pause();\n else play();\n }, [play, pause]);\n\n const playToEnd = useCallback((): Promise<void> => {\n pendingResolveRef.current?.();\n pendingResolveRef.current = null;\n pendingRejectRef.current = null;\n\n return new Promise<void>((resolve, reject) => {\n pendingResolveRef.current = resolve;\n pendingRejectRef.current = reject;\n play();\n });\n }, [play]);\n\n const seek = useCallback(\n (seconds: number) => {\n const video = videoRef.current;\n if (video == null) return;\n video.currentTime = seconds;\n setCurrentTime(seconds);\n },\n [videoRef]\n );\n\n // Surface a media-level error immediately.\n useEffect(() => {\n if (media?.error == null) return;\n setError(new Error(media.error.message));\n setStatus(\"error\");\n }, [media]);\n\n // Bind the element to the blob URL once it resolves.\n useEffect(() => {\n if (media == null) {\n setStatus(\"idle\");\n setError(undefined);\n setCurrentTime(0);\n setDuration(undefined);\n return undefined;\n }\n\n setError(undefined);\n setStatus(\"buffering\");\n setCurrentTime(0);\n setDuration(undefined);\n\n let cancelled = false;\n const video = videoRef.current;\n\n media.objectURL.then(\n (resolved) => {\n if (cancelled) return;\n if (video == null) return;\n video.src = resolved;\n\n if (shouldPlayRef.current || autoPlay) {\n video.play().catch((err) => {\n setError(err as Error);\n setStatus(\"error\");\n });\n } else {\n setStatus(\"paused\");\n }\n },\n () => {\n if (!cancelled) {\n setError(new Error(\"media failed to materialise\"));\n setStatus(\"error\");\n }\n }\n );\n\n if (video == null) {\n return () => {\n cancelled = true;\n try {\n media.revoke();\n } catch {\n // best-effort\n }\n };\n }\n\n const onPlay = () => {\n if (statusRef.current !== \"error\") setStatus(\"playing\");\n };\n const onPause = () => {\n if (video.ended) return;\n if (statusRef.current === \"playing\") setStatus(\"paused\");\n };\n const onEnded = () => {\n setStatus(\"finished\");\n };\n const onTimeUpdate = () => {\n setCurrentTime(video.currentTime);\n };\n const onLoadedMetadata = () => {\n if (Number.isFinite(video.duration)) setDuration(video.duration);\n };\n const onError = () => {\n setError(new Error(\"HTMLVideoElement error\"));\n setStatus(\"error\");\n };\n\n video.addEventListener(\"play\", onPlay);\n video.addEventListener(\"pause\", onPause);\n video.addEventListener(\"ended\", onEnded);\n video.addEventListener(\"timeupdate\", onTimeUpdate);\n video.addEventListener(\"loadedmetadata\", onLoadedMetadata);\n video.addEventListener(\"error\", onError);\n\n return () => {\n cancelled = true;\n video.removeEventListener(\"play\", onPlay);\n video.removeEventListener(\"pause\", onPause);\n video.removeEventListener(\"ended\", onEnded);\n video.removeEventListener(\"timeupdate\", onTimeUpdate);\n video.removeEventListener(\"loadedmetadata\", onLoadedMetadata);\n video.removeEventListener(\"error\", onError);\n try {\n video.pause();\n video.removeAttribute(\"src\");\n video.load();\n } catch {\n // best-effort\n }\n try {\n media.revoke();\n } catch {\n // best-effort\n }\n };\n }, [media, videoRef, autoPlay]);\n\n return {\n status,\n play,\n pause,\n stop,\n toggle,\n reset,\n playToEnd,\n currentTime,\n duration,\n seek,\n error,\n };\n}\n"],"mappings":"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"}
@@ -0,0 +1,65 @@
1
+ import { PlayerStatus } from "./use-audio-player.cjs";
2
+ import { VideoMedia } from "@langchain/langgraph-sdk/stream";
3
+ import { RefObject } from "react";
4
+
5
+ //#region src/use-video-player.d.ts
6
+ /**
7
+ * Options for {@link useVideoPlayer}.
8
+ */
9
+ interface UseVideoPlayerOptions {
10
+ /**
11
+ * Start playback as soon as the blob URL resolves. Subject to
12
+ * browser autoplay policies — pair with `muted={true}` on the
13
+ * `<video>` element to bypass the user-gesture requirement.
14
+ */
15
+ autoPlay?: boolean;
16
+ }
17
+ /**
18
+ * Controls + live state returned by {@link useVideoPlayer}. Mirrors
19
+ * {@link AudioPlayerHandle} on the shared subset so callers only ever
20
+ * learn one shape.
21
+ */
22
+ interface VideoPlayerHandle {
23
+ status: PlayerStatus;
24
+ play(): void;
25
+ pause(): void;
26
+ stop(): void;
27
+ toggle(): void;
28
+ reset(): void;
29
+ /**
30
+ * Resolve on the next terminal transition (`finished` / `paused` /
31
+ * `idle`). Reject on transitions to `"error"`. Triggers `play()`
32
+ * when called.
33
+ */
34
+ playToEnd(): Promise<void>;
35
+ currentTime: number;
36
+ /** Total duration (seconds) once the element has parsed the blob. */
37
+ duration?: number;
38
+ /** Seek to an absolute timestamp in seconds. */
39
+ seek(seconds: number): void;
40
+ error: Error | undefined;
41
+ }
42
+ /**
43
+ * Bind a {@link VideoMedia} handle to a caller-owned `<video>` element.
44
+ *
45
+ * ### Contract
46
+ *
47
+ * - The caller renders `<video ref={videoRef}>` and styles it however
48
+ * they like; the hook never injects DOM nor overrides layout.
49
+ * - On `message-finish`, the underlying blob URL is minted and assigned
50
+ * as `video.src`. Progressive playback of streamed container video
51
+ * (fragmented mp4 / webm via MSE) is out of scope for this version;
52
+ * `status` stays in `"buffering"` until the blob resolves.
53
+ * - Element events (`play` / `pause` / `ended` / `timeupdate` /
54
+ * `loadedmetadata` / `error`) are translated into the shared
55
+ * {@link PlayerStatus} enum.
56
+ * - On unmount, `media.revoke()` is called to free the object URL.
57
+ *
58
+ * @param videoRef - Ref to the `<video>` element the caller renders.
59
+ * @param media - Video handle from {@link useVideo}.
60
+ * @param options - Auto-play toggle.
61
+ */
62
+ declare function useVideoPlayer(videoRef: RefObject<HTMLVideoElement | null>, media: VideoMedia | undefined, options?: UseVideoPlayerOptions): VideoPlayerHandle;
63
+ //#endregion
64
+ export { UseVideoPlayerOptions, VideoPlayerHandle, useVideoPlayer };
65
+ //# sourceMappingURL=use-video-player.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-video-player.d.cts","names":[],"sources":["../src/use-video-player.ts"],"mappings":";;;;;;;AAiBA;UAAiB,qBAAA;;;;AAcjB;;EARE,QAAA;AAAA;;;;;;UAQe,iBAAA;EACf,MAAA,EAAQ,YAAA;EACR,IAAA;EACA,KAAA;EACA,IAAA;EACA,MAAA;EACA,KAAA;EAMa;;;;;EAAb,SAAA,IAAa,OAAA;EAEb,WAAA;EAMY;EAJZ,QAAA;EA2Bc;EAzBd,IAAA,CAAK,OAAA;EAEL,KAAA,EAAO,KAAA;AAAA;;;;;;;;;;;;;;;;;;;;;iBAuBO,cAAA,CACd,QAAA,EAAU,SAAA,CAAU,gBAAA,UACpB,KAAA,EAAO,UAAA,cACP,OAAA,GAAU,qBAAA,GACT,iBAAA"}
@@ -0,0 +1,65 @@
1
+ import { PlayerStatus } from "./use-audio-player.js";
2
+ import { RefObject } from "react";
3
+ import { VideoMedia } from "@langchain/langgraph-sdk/stream";
4
+
5
+ //#region src/use-video-player.d.ts
6
+ /**
7
+ * Options for {@link useVideoPlayer}.
8
+ */
9
+ interface UseVideoPlayerOptions {
10
+ /**
11
+ * Start playback as soon as the blob URL resolves. Subject to
12
+ * browser autoplay policies — pair with `muted={true}` on the
13
+ * `<video>` element to bypass the user-gesture requirement.
14
+ */
15
+ autoPlay?: boolean;
16
+ }
17
+ /**
18
+ * Controls + live state returned by {@link useVideoPlayer}. Mirrors
19
+ * {@link AudioPlayerHandle} on the shared subset so callers only ever
20
+ * learn one shape.
21
+ */
22
+ interface VideoPlayerHandle {
23
+ status: PlayerStatus;
24
+ play(): void;
25
+ pause(): void;
26
+ stop(): void;
27
+ toggle(): void;
28
+ reset(): void;
29
+ /**
30
+ * Resolve on the next terminal transition (`finished` / `paused` /
31
+ * `idle`). Reject on transitions to `"error"`. Triggers `play()`
32
+ * when called.
33
+ */
34
+ playToEnd(): Promise<void>;
35
+ currentTime: number;
36
+ /** Total duration (seconds) once the element has parsed the blob. */
37
+ duration?: number;
38
+ /** Seek to an absolute timestamp in seconds. */
39
+ seek(seconds: number): void;
40
+ error: Error | undefined;
41
+ }
42
+ /**
43
+ * Bind a {@link VideoMedia} handle to a caller-owned `<video>` element.
44
+ *
45
+ * ### Contract
46
+ *
47
+ * - The caller renders `<video ref={videoRef}>` and styles it however
48
+ * they like; the hook never injects DOM nor overrides layout.
49
+ * - On `message-finish`, the underlying blob URL is minted and assigned
50
+ * as `video.src`. Progressive playback of streamed container video
51
+ * (fragmented mp4 / webm via MSE) is out of scope for this version;
52
+ * `status` stays in `"buffering"` until the blob resolves.
53
+ * - Element events (`play` / `pause` / `ended` / `timeupdate` /
54
+ * `loadedmetadata` / `error`) are translated into the shared
55
+ * {@link PlayerStatus} enum.
56
+ * - On unmount, `media.revoke()` is called to free the object URL.
57
+ *
58
+ * @param videoRef - Ref to the `<video>` element the caller renders.
59
+ * @param media - Video handle from {@link useVideo}.
60
+ * @param options - Auto-play toggle.
61
+ */
62
+ declare function useVideoPlayer(videoRef: RefObject<HTMLVideoElement | null>, media: VideoMedia | undefined, options?: UseVideoPlayerOptions): VideoPlayerHandle;
63
+ //#endregion
64
+ export { UseVideoPlayerOptions, VideoPlayerHandle, useVideoPlayer };
65
+ //# sourceMappingURL=use-video-player.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-video-player.d.ts","names":[],"sources":["../src/use-video-player.ts"],"mappings":";;;;;;;AAiBA;UAAiB,qBAAA;;;;AAcjB;;EARE,QAAA;AAAA;;;;;;UAQe,iBAAA;EACf,MAAA,EAAQ,YAAA;EACR,IAAA;EACA,KAAA;EACA,IAAA;EACA,MAAA;EACA,KAAA;EAMa;;;;;EAAb,SAAA,IAAa,OAAA;EAEb,WAAA;EAMY;EAJZ,QAAA;EA2Bc;EAzBd,IAAA,CAAK,OAAA;EAEL,KAAA,EAAO,KAAA;AAAA;;;;;;;;;;;;;;;;;;;;;iBAuBO,cAAA,CACd,QAAA,EAAU,SAAA,CAAU,gBAAA,UACpB,KAAA,EAAO,UAAA,cACP,OAAA,GAAU,qBAAA,GACT,iBAAA"}