@outkit-dev/react 0.0.5 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -22,11 +22,12 @@ var index_exports = {};
22
22
  __export(index_exports, {
23
23
  AIRenderer: () => AIRenderer,
24
24
  OUTKIT_TOKENS: () => import_renderer2.OUTKIT_TOKENS,
25
- completePartialJson: () => completePartialJson,
26
- expandWireBlock: () => expandWireBlock,
27
- extractCompleteBlocks: () => extractCompleteBlocks,
28
- isRenderableBlock: () => isRenderableBlock,
29
- parseStreamingBlocks: () => parseStreamingBlocks,
25
+ OutkitStream: () => import_core2.OutkitStream,
26
+ completePartialJson: () => import_core3.completePartialJson,
27
+ expandWireBlock: () => import_core3.expandWireBlock,
28
+ extractCompleteBlocks: () => import_core3.extractCompleteBlocks,
29
+ isRenderableBlock: () => import_core3.isRenderableBlock,
30
+ parseStreamingBlocks: () => import_core3.parseStreamingBlocks,
30
31
  useBlockStream: () => useBlockStream
31
32
  });
32
33
  module.exports = __toCommonJS(index_exports);
@@ -227,150 +228,8 @@ var SingleComponent = (0, import_react.memo)(function SingleComponent2({
227
228
  });
228
229
 
229
230
  // src/use-block-stream.ts
231
+ var import_core = require("@outkit-dev/core");
230
232
  var import_react2 = require("react");
231
-
232
- // src/streaming/complete-partial-json.ts
233
- function completePartialJson(partial) {
234
- let inString = false;
235
- let escaped = false;
236
- const stack = [];
237
- for (let i = 0; i < partial.length; i++) {
238
- const ch = partial[i];
239
- if (escaped) {
240
- escaped = false;
241
- continue;
242
- }
243
- if (ch === "\\" && inString) {
244
- escaped = true;
245
- continue;
246
- }
247
- if (ch === '"') {
248
- inString = !inString;
249
- continue;
250
- }
251
- if (inString) continue;
252
- if (ch === "{") stack.push("}");
253
- else if (ch === "[") stack.push("]");
254
- else if ((ch === "}" || ch === "]") && stack.length > 0 && stack[stack.length - 1] === ch) {
255
- stack.pop();
256
- }
257
- }
258
- let result = partial;
259
- if (inString) {
260
- if (escaped) result = result.slice(0, -1);
261
- const trailingUnicode = result.match(/\\u[0-9a-fA-F]{0,3}$/);
262
- if (trailingUnicode) {
263
- result = result.slice(0, -trailingUnicode[0].length);
264
- }
265
- result += '"';
266
- }
267
- while (stack.length > 0) {
268
- result += stack.pop();
269
- }
270
- return result;
271
- }
272
-
273
- // src/streaming/parse-streaming-blocks.ts
274
- function expandWireBlock(block) {
275
- if ("type" in block && "component" in block) return block;
276
- if ("type" in block && block.type === "text") return block;
277
- if (typeof block.c === "string" && typeof block.p === "object" && block.p !== null) {
278
- return {
279
- type: "component",
280
- component: block.c,
281
- version: block.v ?? "1.0",
282
- props: block.p,
283
- ...block.f !== void 0 ? { fallback: block.f } : {},
284
- ...block.m ? { meta: block.m } : {}
285
- };
286
- }
287
- return block;
288
- }
289
- function isRenderableBlock(block) {
290
- if (!block || typeof block !== "object") return false;
291
- const b = block;
292
- if (b.type === "text" && typeof b.content === "string" && b.content.length > 0)
293
- return true;
294
- if (b.type === "component" && typeof b.component === "string" && b.component.length >= 2 && typeof b.props === "object" && b.props !== null)
295
- return true;
296
- return false;
297
- }
298
- function extractCompleteBlocks(accumulated) {
299
- const blocksIdx = accumulated.indexOf('"blocks"');
300
- let arrayStart = -1;
301
- if (blocksIdx !== -1) {
302
- arrayStart = accumulated.indexOf("[", blocksIdx);
303
- } else {
304
- arrayStart = accumulated.indexOf("[");
305
- }
306
- if (arrayStart === -1) return [];
307
- const content = accumulated.slice(arrayStart + 1);
308
- const blocks = [];
309
- let depth = 0;
310
- let blockStart = -1;
311
- let inString = false;
312
- let escaped = false;
313
- for (let i = 0; i < content.length; i++) {
314
- const ch = content[i];
315
- if (escaped) {
316
- escaped = false;
317
- continue;
318
- }
319
- if (ch === "\\" && inString) {
320
- escaped = true;
321
- continue;
322
- }
323
- if (ch === '"') {
324
- inString = !inString;
325
- continue;
326
- }
327
- if (inString) continue;
328
- if (ch === "{") {
329
- if (depth === 0) blockStart = i;
330
- depth++;
331
- } else if (ch === "}") {
332
- depth--;
333
- if (depth === 0 && blockStart !== -1) {
334
- try {
335
- const parsed = JSON.parse(content.slice(blockStart, i + 1));
336
- const expanded = expandWireBlock(parsed);
337
- if (isRenderableBlock(expanded)) blocks.push(expanded);
338
- } catch {
339
- }
340
- blockStart = -1;
341
- }
342
- }
343
- }
344
- return blocks;
345
- }
346
- function parseStreamingBlocks(accumulated) {
347
- if (!accumulated.trim()) return [];
348
- let target = accumulated;
349
- const blocksIdx = accumulated.indexOf('"blocks"');
350
- if (blocksIdx > 0) {
351
- let braceIdx = blocksIdx - 1;
352
- while (braceIdx >= 0 && accumulated[braceIdx] !== "{") braceIdx--;
353
- if (braceIdx > 0) target = accumulated.slice(braceIdx);
354
- }
355
- const patched = completePartialJson(target);
356
- try {
357
- const parsed = JSON.parse(patched);
358
- const rawBlocks = parsed?.blocks ?? (Array.isArray(parsed) ? parsed : []);
359
- return rawBlocks.map((b) => {
360
- if (!b || typeof b !== "object") return b;
361
- return expandWireBlock(b);
362
- }).filter(isRenderableBlock).map((block) => {
363
- if (!("version" in block)) {
364
- return Object.assign({}, block, { version: "1.0" });
365
- }
366
- return block;
367
- });
368
- } catch {
369
- return extractCompleteBlocks(accumulated);
370
- }
371
- }
372
-
373
- // src/use-block-stream.ts
374
233
  function useBlockStream() {
375
234
  const [blocks, setBlocks] = (0, import_react2.useState)([]);
376
235
  const [isStreaming, setIsStreaming] = (0, import_react2.useState)(false);
@@ -378,9 +237,6 @@ function useBlockStream() {
378
237
  const pendingRef = (0, import_react2.useRef)(null);
379
238
  const rafRef = (0, import_react2.useRef)(0);
380
239
  const streamingRef = (0, import_react2.useRef)(false);
381
- const accumulatedRef = (0, import_react2.useRef)("");
382
- const designReceivedRef = (0, import_react2.useRef)(false);
383
- const bufferedBlocksRef = (0, import_react2.useRef)(null);
384
240
  const flush = (0, import_react2.useCallback)(() => {
385
241
  rafRef.current = 0;
386
242
  if (pendingRef.current !== null) {
@@ -388,7 +244,7 @@ function useBlockStream() {
388
244
  pendingRef.current = null;
389
245
  }
390
246
  }, []);
391
- const push = (0, import_react2.useCallback)(
247
+ const batchBlocks = (0, import_react2.useCallback)(
392
248
  (newBlocks) => {
393
249
  if (!streamingRef.current) {
394
250
  streamingRef.current = true;
@@ -401,10 +257,7 @@ function useBlockStream() {
401
257
  },
402
258
  [flush]
403
259
  );
404
- const setDesign = (0, import_react2.useCallback)((tokens) => {
405
- setDesignState(tokens);
406
- }, []);
407
- const complete = (0, import_react2.useCallback)(() => {
260
+ const handleComplete = (0, import_react2.useCallback)(() => {
408
261
  if (rafRef.current) {
409
262
  cancelAnimationFrame(rafRef.current);
410
263
  rafRef.current = 0;
@@ -416,72 +269,68 @@ function useBlockStream() {
416
269
  streamingRef.current = false;
417
270
  setIsStreaming(false);
418
271
  }, []);
272
+ const streamRef = (0, import_react2.useRef)(null);
273
+ if (streamRef.current === null) {
274
+ streamRef.current = new import_core.OutkitStream({
275
+ onBlocks: (b) => batchBlocks(b),
276
+ onDesign: (d) => setDesignState(d),
277
+ onComplete: () => handleComplete()
278
+ });
279
+ }
280
+ const stream = streamRef.current;
281
+ (0, import_react2.useEffect)(() => {
282
+ return () => {
283
+ if (rafRef.current) cancelAnimationFrame(rafRef.current);
284
+ stream.destroy();
285
+ };
286
+ }, [stream]);
419
287
  const reset = (0, import_react2.useCallback)(() => {
420
288
  if (rafRef.current) {
421
289
  cancelAnimationFrame(rafRef.current);
422
290
  rafRef.current = 0;
423
291
  }
424
292
  pendingRef.current = null;
425
- accumulatedRef.current = "";
426
- bufferedBlocksRef.current = null;
427
- designReceivedRef.current = false;
428
293
  streamingRef.current = false;
294
+ stream.reset();
429
295
  setBlocks([]);
430
296
  setIsStreaming(false);
431
297
  setDesignState(void 0);
432
- }, []);
433
- const feedChunk = (0, import_react2.useCallback)(
434
- (data) => {
435
- accumulatedRef.current += data;
436
- const parsed = parseStreamingBlocks(accumulatedRef.current);
437
- if (parsed.length > 0) {
438
- if (designReceivedRef.current) {
439
- push(parsed);
440
- } else {
441
- bufferedBlocksRef.current = parsed;
442
- }
443
- }
444
- },
445
- [push]
298
+ }, [stream]);
299
+ const push = (0, import_react2.useCallback)(
300
+ (newBlocks) => batchBlocks(newBlocks),
301
+ [batchBlocks]
446
302
  );
447
- const feedMeta = (0, import_react2.useCallback)(
448
- (tokens) => {
449
- designReceivedRef.current = true;
450
- setDesignState(tokens);
451
- if (bufferedBlocksRef.current) {
452
- push(bufferedBlocksRef.current);
453
- bufferedBlocksRef.current = null;
454
- }
455
- },
456
- [push]
303
+ const setDesign = (0, import_react2.useCallback)(
304
+ (tokens) => stream.feedMeta(tokens),
305
+ [stream]
457
306
  );
458
- const feedDone = (0, import_react2.useCallback)(() => {
459
- if (accumulatedRef.current) {
460
- const parsed = parseStreamingBlocks(accumulatedRef.current);
461
- if (parsed.length > 0) {
462
- pendingRef.current = parsed;
463
- }
464
- }
465
- accumulatedRef.current = "";
466
- bufferedBlocksRef.current = null;
467
- complete();
468
- }, [complete]);
469
- (0, import_react2.useEffect)(() => {
470
- return () => {
471
- if (rafRef.current) {
472
- cancelAnimationFrame(rafRef.current);
473
- }
474
- };
475
- }, []);
476
- return { blocks, isStreaming, design, feedChunk, feedMeta, feedDone, push, setDesign, complete, reset };
307
+ const complete = (0, import_react2.useCallback)(() => stream.feedDone(), [stream]);
308
+ return {
309
+ blocks,
310
+ isStreaming,
311
+ design,
312
+ feedResponse: stream.feedResponse.bind(stream),
313
+ feedSSE: stream.feedSSE.bind(stream),
314
+ feedEvent: stream.feedEvent.bind(stream),
315
+ feedChunk: stream.feedChunk.bind(stream),
316
+ feedMeta: stream.feedMeta.bind(stream),
317
+ feedDone: stream.feedDone.bind(stream),
318
+ push,
319
+ setDesign,
320
+ complete,
321
+ reset
322
+ };
477
323
  }
478
324
 
479
325
  // src/index.ts
326
+ var import_core2 = require("@outkit-dev/core");
327
+ var import_core3 = require("@outkit-dev/core");
480
328
  var import_renderer2 = require("@outkit-dev/renderer");
481
329
  // Annotate the CommonJS export names for ESM import in node:
482
330
  0 && (module.exports = {
483
331
  AIRenderer,
484
332
  OUTKIT_TOKENS,
333
+ OutkitStream,
485
334
  completePartialJson,
486
335
  expandWireBlock,
487
336
  extractCompleteBlocks,
package/dist/index.d.cts CHANGED
@@ -2,6 +2,7 @@ import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import { ContentBlock, ComponentBlock, OutkitInteractionEvent } from '@outkit-dev/renderer';
3
3
  export { ComponentBlock, ContentBlock, InlineAnnotation, OUTKIT_TOKENS, OutkitEnhanceResult, OutkitInteractionEvent } from '@outkit-dev/renderer';
4
4
  import React from 'react';
5
+ export { OutkitStream, OutkitStreamCallbacks, OutkitStreamState, completePartialJson, expandWireBlock, extractCompleteBlocks, isRenderableBlock, parseStreamingBlocks } from '@outkit-dev/core';
5
6
 
6
7
  interface AIRendererProps {
7
8
  /** Array of content blocks from outkit_enhance */
@@ -43,69 +44,36 @@ interface UseBlockStreamReturn {
43
44
  isStreaming: boolean;
44
45
  /** Design tokens received from the stream's meta event. Pass to <AIRenderer design={design} />. */
45
46
  design: Record<string, string> | undefined;
46
- /** Append a raw SSE data chunk. Parses accumulated buffer and updates blocks. */
47
+ /** Feed a raw fetch Response. Handles SSE parsing, meta detection, and completion. */
48
+ feedResponse: (response: Response) => Promise<void>;
49
+ /** Feed raw SSE-formatted text. Handles splitting, data: stripping, and routing. */
50
+ feedSSE: (rawText: string) => void;
51
+ /** Feed one unwrapped event payload. Handles [DONE], meta detection, and chunks. */
52
+ feedEvent: (data: string) => void;
53
+ /** Append a raw LLM data chunk. Parses accumulated buffer and updates blocks. */
47
54
  feedChunk: (data: string) => void;
48
- /** Set design tokens from a meta SSE event. Alias for setDesign. */
55
+ /** Set design tokens from a meta SSE event. */
49
56
  feedMeta: (tokens: Record<string, string>) => void;
50
57
  /** Signal end of stream. Final parse + flush + mark complete. */
51
58
  feedDone: () => void;
52
- /** Push a new full blocks array (the latest snapshot). Batched at 60fps. */
59
+ /** @deprecated Use feedChunk/feedResponse instead. Direct block snapshot setter. */
53
60
  push: (blocks: ContentBlock[]) => void;
54
- /** Set design tokens (typically from a meta SSE event). */
61
+ /** @deprecated Use feedMeta instead. */
55
62
  setDesign: (tokens: Record<string, string>) => void;
56
- /** Signal that streaming is complete. Flushes any pending update. */
63
+ /** @deprecated Use feedDone instead. */
57
64
  complete: () => void;
58
- /** Reset to empty state. */
65
+ /** Reset to empty state. Aborts any in-flight feedResponse. */
59
66
  reset: () => void;
60
67
  }
61
68
  /**
62
- * Hook that batches rapid block updates to 60fps via requestAnimationFrame.
69
+ * React hook for streaming Outkit blocks with rAF-batched updates at 60fps.
63
70
  *
64
- * Usage:
65
- * ```tsx
66
- * const { blocks, isStreaming, push, complete } = useBlockStream();
67
- *
68
- * useEffect(() => {
69
- * const es = new EventSource("/stream");
70
- * es.onmessage = (e) => push(JSON.parse(e.data).blocks);
71
- * es.addEventListener("done", () => complete());
72
- * return () => es.close();
73
- * }, [push, complete]);
74
- *
75
- * return <AIRenderer blocks={blocks} streaming={isStreaming} />;
76
- * ```
71
+ * Four-tier API — pick the tier that matches your transport:
72
+ * - `feedResponse(res)` — fetch proxy (most common, handles everything)
73
+ * - `feedSSE(text)` raw SSE bytes (manual reader)
74
+ * - `feedEvent(data)` — one unwrapped payload (WebSocket, Socket.IO)
75
+ * - `feedChunk/feedMeta/feedDone` — full manual control
77
76
  */
78
77
  declare function useBlockStream(): UseBlockStreamReturn;
79
78
 
80
- /**
81
- * Close unclosed strings, arrays, and objects so partial JSON can be parsed.
82
- *
83
- * As the LLM streams a JSON structure token by token, this function patches
84
- * the accumulated buffer into valid JSON at every chunk boundary. This lets
85
- * content stream character-by-character while remaining parseable.
86
- */
87
- declare function completePartialJson(partial: string): string;
88
-
89
- /** Expand a compact wire-format block (c/v/p/f/m) into the full ContentBlock shape. */
90
- declare function expandWireBlock(block: Record<string, unknown>): Record<string, unknown>;
91
- /** Check if a parsed object is a renderable block. */
92
- declare function isRenderableBlock(block: unknown): block is ContentBlock;
93
- /** Fallback: extract only fully brace-matched block objects from accumulated data. */
94
- declare function extractCompleteBlocks(accumulated: string): ContentBlock[];
95
- /**
96
- * Parse streaming LLM JSON into renderable blocks.
97
- *
98
- * Call this with the full accumulated buffer on every chunk. It closes
99
- * unclosed strings/arrays/objects via `completePartialJson`, parses the
100
- * result, and returns all currently renderable blocks.
101
- *
102
- * Handles both wire format (`{ c, v, p }`) and expanded format
103
- * (`{ type, component, version, props }`). The renderer's `normalizeBlock()`
104
- * also handles wire format at render time as a further safety net.
105
- *
106
- * @param accumulated - The full accumulated raw LLM output so far
107
- * @returns Array of renderable ContentBlock objects
108
- */
109
- declare function parseStreamingBlocks(accumulated: string): ContentBlock[];
110
-
111
- export { AIRenderer, type AIRendererProps, type UseBlockStreamReturn, completePartialJson, expandWireBlock, extractCompleteBlocks, isRenderableBlock, parseStreamingBlocks, useBlockStream };
79
+ export { AIRenderer, type AIRendererProps, type UseBlockStreamReturn, useBlockStream };
package/dist/index.d.ts CHANGED
@@ -2,6 +2,7 @@ import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import { ContentBlock, ComponentBlock, OutkitInteractionEvent } from '@outkit-dev/renderer';
3
3
  export { ComponentBlock, ContentBlock, InlineAnnotation, OUTKIT_TOKENS, OutkitEnhanceResult, OutkitInteractionEvent } from '@outkit-dev/renderer';
4
4
  import React from 'react';
5
+ export { OutkitStream, OutkitStreamCallbacks, OutkitStreamState, completePartialJson, expandWireBlock, extractCompleteBlocks, isRenderableBlock, parseStreamingBlocks } from '@outkit-dev/core';
5
6
 
6
7
  interface AIRendererProps {
7
8
  /** Array of content blocks from outkit_enhance */
@@ -43,69 +44,36 @@ interface UseBlockStreamReturn {
43
44
  isStreaming: boolean;
44
45
  /** Design tokens received from the stream's meta event. Pass to <AIRenderer design={design} />. */
45
46
  design: Record<string, string> | undefined;
46
- /** Append a raw SSE data chunk. Parses accumulated buffer and updates blocks. */
47
+ /** Feed a raw fetch Response. Handles SSE parsing, meta detection, and completion. */
48
+ feedResponse: (response: Response) => Promise<void>;
49
+ /** Feed raw SSE-formatted text. Handles splitting, data: stripping, and routing. */
50
+ feedSSE: (rawText: string) => void;
51
+ /** Feed one unwrapped event payload. Handles [DONE], meta detection, and chunks. */
52
+ feedEvent: (data: string) => void;
53
+ /** Append a raw LLM data chunk. Parses accumulated buffer and updates blocks. */
47
54
  feedChunk: (data: string) => void;
48
- /** Set design tokens from a meta SSE event. Alias for setDesign. */
55
+ /** Set design tokens from a meta SSE event. */
49
56
  feedMeta: (tokens: Record<string, string>) => void;
50
57
  /** Signal end of stream. Final parse + flush + mark complete. */
51
58
  feedDone: () => void;
52
- /** Push a new full blocks array (the latest snapshot). Batched at 60fps. */
59
+ /** @deprecated Use feedChunk/feedResponse instead. Direct block snapshot setter. */
53
60
  push: (blocks: ContentBlock[]) => void;
54
- /** Set design tokens (typically from a meta SSE event). */
61
+ /** @deprecated Use feedMeta instead. */
55
62
  setDesign: (tokens: Record<string, string>) => void;
56
- /** Signal that streaming is complete. Flushes any pending update. */
63
+ /** @deprecated Use feedDone instead. */
57
64
  complete: () => void;
58
- /** Reset to empty state. */
65
+ /** Reset to empty state. Aborts any in-flight feedResponse. */
59
66
  reset: () => void;
60
67
  }
61
68
  /**
62
- * Hook that batches rapid block updates to 60fps via requestAnimationFrame.
69
+ * React hook for streaming Outkit blocks with rAF-batched updates at 60fps.
63
70
  *
64
- * Usage:
65
- * ```tsx
66
- * const { blocks, isStreaming, push, complete } = useBlockStream();
67
- *
68
- * useEffect(() => {
69
- * const es = new EventSource("/stream");
70
- * es.onmessage = (e) => push(JSON.parse(e.data).blocks);
71
- * es.addEventListener("done", () => complete());
72
- * return () => es.close();
73
- * }, [push, complete]);
74
- *
75
- * return <AIRenderer blocks={blocks} streaming={isStreaming} />;
76
- * ```
71
+ * Four-tier API — pick the tier that matches your transport:
72
+ * - `feedResponse(res)` — fetch proxy (most common, handles everything)
73
+ * - `feedSSE(text)` raw SSE bytes (manual reader)
74
+ * - `feedEvent(data)` — one unwrapped payload (WebSocket, Socket.IO)
75
+ * - `feedChunk/feedMeta/feedDone` — full manual control
77
76
  */
78
77
  declare function useBlockStream(): UseBlockStreamReturn;
79
78
 
80
- /**
81
- * Close unclosed strings, arrays, and objects so partial JSON can be parsed.
82
- *
83
- * As the LLM streams a JSON structure token by token, this function patches
84
- * the accumulated buffer into valid JSON at every chunk boundary. This lets
85
- * content stream character-by-character while remaining parseable.
86
- */
87
- declare function completePartialJson(partial: string): string;
88
-
89
- /** Expand a compact wire-format block (c/v/p/f/m) into the full ContentBlock shape. */
90
- declare function expandWireBlock(block: Record<string, unknown>): Record<string, unknown>;
91
- /** Check if a parsed object is a renderable block. */
92
- declare function isRenderableBlock(block: unknown): block is ContentBlock;
93
- /** Fallback: extract only fully brace-matched block objects from accumulated data. */
94
- declare function extractCompleteBlocks(accumulated: string): ContentBlock[];
95
- /**
96
- * Parse streaming LLM JSON into renderable blocks.
97
- *
98
- * Call this with the full accumulated buffer on every chunk. It closes
99
- * unclosed strings/arrays/objects via `completePartialJson`, parses the
100
- * result, and returns all currently renderable blocks.
101
- *
102
- * Handles both wire format (`{ c, v, p }`) and expanded format
103
- * (`{ type, component, version, props }`). The renderer's `normalizeBlock()`
104
- * also handles wire format at render time as a further safety net.
105
- *
106
- * @param accumulated - The full accumulated raw LLM output so far
107
- * @returns Array of renderable ContentBlock objects
108
- */
109
- declare function parseStreamingBlocks(accumulated: string): ContentBlock[];
110
-
111
- export { AIRenderer, type AIRendererProps, type UseBlockStreamReturn, completePartialJson, expandWireBlock, extractCompleteBlocks, isRenderableBlock, parseStreamingBlocks, useBlockStream };
79
+ export { AIRenderer, type AIRendererProps, type UseBlockStreamReturn, useBlockStream };
package/dist/index.js CHANGED
@@ -194,150 +194,8 @@ var SingleComponent = memo(function SingleComponent2({
194
194
  });
195
195
 
196
196
  // src/use-block-stream.ts
197
+ import { OutkitStream } from "@outkit-dev/core";
197
198
  import { useCallback, useEffect as useEffect2, useRef as useRef2, useState } from "react";
198
-
199
- // src/streaming/complete-partial-json.ts
200
- function completePartialJson(partial) {
201
- let inString = false;
202
- let escaped = false;
203
- const stack = [];
204
- for (let i = 0; i < partial.length; i++) {
205
- const ch = partial[i];
206
- if (escaped) {
207
- escaped = false;
208
- continue;
209
- }
210
- if (ch === "\\" && inString) {
211
- escaped = true;
212
- continue;
213
- }
214
- if (ch === '"') {
215
- inString = !inString;
216
- continue;
217
- }
218
- if (inString) continue;
219
- if (ch === "{") stack.push("}");
220
- else if (ch === "[") stack.push("]");
221
- else if ((ch === "}" || ch === "]") && stack.length > 0 && stack[stack.length - 1] === ch) {
222
- stack.pop();
223
- }
224
- }
225
- let result = partial;
226
- if (inString) {
227
- if (escaped) result = result.slice(0, -1);
228
- const trailingUnicode = result.match(/\\u[0-9a-fA-F]{0,3}$/);
229
- if (trailingUnicode) {
230
- result = result.slice(0, -trailingUnicode[0].length);
231
- }
232
- result += '"';
233
- }
234
- while (stack.length > 0) {
235
- result += stack.pop();
236
- }
237
- return result;
238
- }
239
-
240
- // src/streaming/parse-streaming-blocks.ts
241
- function expandWireBlock(block) {
242
- if ("type" in block && "component" in block) return block;
243
- if ("type" in block && block.type === "text") return block;
244
- if (typeof block.c === "string" && typeof block.p === "object" && block.p !== null) {
245
- return {
246
- type: "component",
247
- component: block.c,
248
- version: block.v ?? "1.0",
249
- props: block.p,
250
- ...block.f !== void 0 ? { fallback: block.f } : {},
251
- ...block.m ? { meta: block.m } : {}
252
- };
253
- }
254
- return block;
255
- }
256
- function isRenderableBlock(block) {
257
- if (!block || typeof block !== "object") return false;
258
- const b = block;
259
- if (b.type === "text" && typeof b.content === "string" && b.content.length > 0)
260
- return true;
261
- if (b.type === "component" && typeof b.component === "string" && b.component.length >= 2 && typeof b.props === "object" && b.props !== null)
262
- return true;
263
- return false;
264
- }
265
- function extractCompleteBlocks(accumulated) {
266
- const blocksIdx = accumulated.indexOf('"blocks"');
267
- let arrayStart = -1;
268
- if (blocksIdx !== -1) {
269
- arrayStart = accumulated.indexOf("[", blocksIdx);
270
- } else {
271
- arrayStart = accumulated.indexOf("[");
272
- }
273
- if (arrayStart === -1) return [];
274
- const content = accumulated.slice(arrayStart + 1);
275
- const blocks = [];
276
- let depth = 0;
277
- let blockStart = -1;
278
- let inString = false;
279
- let escaped = false;
280
- for (let i = 0; i < content.length; i++) {
281
- const ch = content[i];
282
- if (escaped) {
283
- escaped = false;
284
- continue;
285
- }
286
- if (ch === "\\" && inString) {
287
- escaped = true;
288
- continue;
289
- }
290
- if (ch === '"') {
291
- inString = !inString;
292
- continue;
293
- }
294
- if (inString) continue;
295
- if (ch === "{") {
296
- if (depth === 0) blockStart = i;
297
- depth++;
298
- } else if (ch === "}") {
299
- depth--;
300
- if (depth === 0 && blockStart !== -1) {
301
- try {
302
- const parsed = JSON.parse(content.slice(blockStart, i + 1));
303
- const expanded = expandWireBlock(parsed);
304
- if (isRenderableBlock(expanded)) blocks.push(expanded);
305
- } catch {
306
- }
307
- blockStart = -1;
308
- }
309
- }
310
- }
311
- return blocks;
312
- }
313
- function parseStreamingBlocks(accumulated) {
314
- if (!accumulated.trim()) return [];
315
- let target = accumulated;
316
- const blocksIdx = accumulated.indexOf('"blocks"');
317
- if (blocksIdx > 0) {
318
- let braceIdx = blocksIdx - 1;
319
- while (braceIdx >= 0 && accumulated[braceIdx] !== "{") braceIdx--;
320
- if (braceIdx > 0) target = accumulated.slice(braceIdx);
321
- }
322
- const patched = completePartialJson(target);
323
- try {
324
- const parsed = JSON.parse(patched);
325
- const rawBlocks = parsed?.blocks ?? (Array.isArray(parsed) ? parsed : []);
326
- return rawBlocks.map((b) => {
327
- if (!b || typeof b !== "object") return b;
328
- return expandWireBlock(b);
329
- }).filter(isRenderableBlock).map((block) => {
330
- if (!("version" in block)) {
331
- return Object.assign({}, block, { version: "1.0" });
332
- }
333
- return block;
334
- });
335
- } catch {
336
- return extractCompleteBlocks(accumulated);
337
- }
338
- }
339
-
340
- // src/use-block-stream.ts
341
199
  function useBlockStream() {
342
200
  const [blocks, setBlocks] = useState([]);
343
201
  const [isStreaming, setIsStreaming] = useState(false);
@@ -345,9 +203,6 @@ function useBlockStream() {
345
203
  const pendingRef = useRef2(null);
346
204
  const rafRef = useRef2(0);
347
205
  const streamingRef = useRef2(false);
348
- const accumulatedRef = useRef2("");
349
- const designReceivedRef = useRef2(false);
350
- const bufferedBlocksRef = useRef2(null);
351
206
  const flush = useCallback(() => {
352
207
  rafRef.current = 0;
353
208
  if (pendingRef.current !== null) {
@@ -355,7 +210,7 @@ function useBlockStream() {
355
210
  pendingRef.current = null;
356
211
  }
357
212
  }, []);
358
- const push = useCallback(
213
+ const batchBlocks = useCallback(
359
214
  (newBlocks) => {
360
215
  if (!streamingRef.current) {
361
216
  streamingRef.current = true;
@@ -368,10 +223,7 @@ function useBlockStream() {
368
223
  },
369
224
  [flush]
370
225
  );
371
- const setDesign = useCallback((tokens) => {
372
- setDesignState(tokens);
373
- }, []);
374
- const complete = useCallback(() => {
226
+ const handleComplete = useCallback(() => {
375
227
  if (rafRef.current) {
376
228
  cancelAnimationFrame(rafRef.current);
377
229
  rafRef.current = 0;
@@ -383,71 +235,73 @@ function useBlockStream() {
383
235
  streamingRef.current = false;
384
236
  setIsStreaming(false);
385
237
  }, []);
238
+ const streamRef = useRef2(null);
239
+ if (streamRef.current === null) {
240
+ streamRef.current = new OutkitStream({
241
+ onBlocks: (b) => batchBlocks(b),
242
+ onDesign: (d) => setDesignState(d),
243
+ onComplete: () => handleComplete()
244
+ });
245
+ }
246
+ const stream = streamRef.current;
247
+ useEffect2(() => {
248
+ return () => {
249
+ if (rafRef.current) cancelAnimationFrame(rafRef.current);
250
+ stream.destroy();
251
+ };
252
+ }, [stream]);
386
253
  const reset = useCallback(() => {
387
254
  if (rafRef.current) {
388
255
  cancelAnimationFrame(rafRef.current);
389
256
  rafRef.current = 0;
390
257
  }
391
258
  pendingRef.current = null;
392
- accumulatedRef.current = "";
393
- bufferedBlocksRef.current = null;
394
- designReceivedRef.current = false;
395
259
  streamingRef.current = false;
260
+ stream.reset();
396
261
  setBlocks([]);
397
262
  setIsStreaming(false);
398
263
  setDesignState(void 0);
399
- }, []);
400
- const feedChunk = useCallback(
401
- (data) => {
402
- accumulatedRef.current += data;
403
- const parsed = parseStreamingBlocks(accumulatedRef.current);
404
- if (parsed.length > 0) {
405
- if (designReceivedRef.current) {
406
- push(parsed);
407
- } else {
408
- bufferedBlocksRef.current = parsed;
409
- }
410
- }
411
- },
412
- [push]
264
+ }, [stream]);
265
+ const push = useCallback(
266
+ (newBlocks) => batchBlocks(newBlocks),
267
+ [batchBlocks]
413
268
  );
414
- const feedMeta = useCallback(
415
- (tokens) => {
416
- designReceivedRef.current = true;
417
- setDesignState(tokens);
418
- if (bufferedBlocksRef.current) {
419
- push(bufferedBlocksRef.current);
420
- bufferedBlocksRef.current = null;
421
- }
422
- },
423
- [push]
269
+ const setDesign = useCallback(
270
+ (tokens) => stream.feedMeta(tokens),
271
+ [stream]
424
272
  );
425
- const feedDone = useCallback(() => {
426
- if (accumulatedRef.current) {
427
- const parsed = parseStreamingBlocks(accumulatedRef.current);
428
- if (parsed.length > 0) {
429
- pendingRef.current = parsed;
430
- }
431
- }
432
- accumulatedRef.current = "";
433
- bufferedBlocksRef.current = null;
434
- complete();
435
- }, [complete]);
436
- useEffect2(() => {
437
- return () => {
438
- if (rafRef.current) {
439
- cancelAnimationFrame(rafRef.current);
440
- }
441
- };
442
- }, []);
443
- return { blocks, isStreaming, design, feedChunk, feedMeta, feedDone, push, setDesign, complete, reset };
273
+ const complete = useCallback(() => stream.feedDone(), [stream]);
274
+ return {
275
+ blocks,
276
+ isStreaming,
277
+ design,
278
+ feedResponse: stream.feedResponse.bind(stream),
279
+ feedSSE: stream.feedSSE.bind(stream),
280
+ feedEvent: stream.feedEvent.bind(stream),
281
+ feedChunk: stream.feedChunk.bind(stream),
282
+ feedMeta: stream.feedMeta.bind(stream),
283
+ feedDone: stream.feedDone.bind(stream),
284
+ push,
285
+ setDesign,
286
+ complete,
287
+ reset
288
+ };
444
289
  }
445
290
 
446
291
  // src/index.ts
292
+ import { OutkitStream as OutkitStream2 } from "@outkit-dev/core";
293
+ import {
294
+ parseStreamingBlocks,
295
+ completePartialJson,
296
+ expandWireBlock,
297
+ isRenderableBlock,
298
+ extractCompleteBlocks
299
+ } from "@outkit-dev/core";
447
300
  import { OUTKIT_TOKENS } from "@outkit-dev/renderer";
448
301
  export {
449
302
  AIRenderer,
450
303
  OUTKIT_TOKENS,
304
+ OutkitStream2 as OutkitStream,
451
305
  completePartialJson,
452
306
  expandWireBlock,
453
307
  extractCompleteBlocks,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@outkit-dev/react",
3
- "version": "0.0.5",
3
+ "version": "0.0.6",
4
4
  "type": "module",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.js",
@@ -16,7 +16,8 @@
16
16
  "dist"
17
17
  ],
18
18
  "dependencies": {
19
- "@outkit-dev/renderer": "0.0.5"
19
+ "@outkit-dev/renderer": "0.0.5",
20
+ "@outkit-dev/core": "0.0.6"
20
21
  },
21
22
  "peerDependencies": {
22
23
  "react": "^18.0.0 || ^19.0.0"