@outkit-dev/react 0.0.5 → 0.0.7

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,96 @@ function useBlockStream() {
416
269
  streamingRef.current = false;
417
270
  setIsStreaming(false);
418
271
  }, []);
272
+ const streamRef = (0, import_react2.useRef)(null);
273
+ const getStream = (0, import_react2.useCallback)(() => {
274
+ if (streamRef.current === null || streamRef.current.streamState === "destroyed") {
275
+ streamRef.current = new import_core.OutkitStream({
276
+ onBlocks: (b) => batchBlocks(b),
277
+ onDesign: (d) => setDesignState(d),
278
+ onComplete: () => handleComplete()
279
+ });
280
+ }
281
+ return streamRef.current;
282
+ }, [batchBlocks, handleComplete]);
283
+ const stream = getStream();
284
+ (0, import_react2.useEffect)(() => {
285
+ const s = getStream();
286
+ return () => {
287
+ if (rafRef.current) cancelAnimationFrame(rafRef.current);
288
+ s.destroy();
289
+ };
290
+ }, [getStream]);
419
291
  const reset = (0, import_react2.useCallback)(() => {
420
292
  if (rafRef.current) {
421
293
  cancelAnimationFrame(rafRef.current);
422
294
  rafRef.current = 0;
423
295
  }
424
296
  pendingRef.current = null;
425
- accumulatedRef.current = "";
426
- bufferedBlocksRef.current = null;
427
- designReceivedRef.current = false;
428
297
  streamingRef.current = false;
298
+ getStream().reset();
429
299
  setBlocks([]);
430
300
  setIsStreaming(false);
431
301
  setDesignState(void 0);
432
- }, []);
302
+ }, [getStream]);
303
+ const push = (0, import_react2.useCallback)(
304
+ (newBlocks) => batchBlocks(newBlocks),
305
+ [batchBlocks]
306
+ );
307
+ const setDesign = (0, import_react2.useCallback)(
308
+ (tokens) => getStream().feedMeta(tokens),
309
+ [getStream]
310
+ );
311
+ const complete = (0, import_react2.useCallback)(() => getStream().feedDone(), [getStream]);
312
+ const feedResponse = (0, import_react2.useCallback)(
313
+ (response) => getStream().feedResponse(response),
314
+ [getStream]
315
+ );
316
+ const feedSSE = (0, import_react2.useCallback)(
317
+ (rawText) => getStream().feedSSE(rawText),
318
+ [getStream]
319
+ );
320
+ const feedEvent = (0, import_react2.useCallback)(
321
+ (data) => getStream().feedEvent(data),
322
+ [getStream]
323
+ );
433
324
  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]
325
+ (data) => getStream().feedChunk(data),
326
+ [getStream]
446
327
  );
447
328
  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]
329
+ (tokens) => getStream().feedMeta(tokens),
330
+ [getStream]
457
331
  );
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 };
332
+ const feedDone = (0, import_react2.useCallback)(
333
+ () => getStream().feedDone(),
334
+ [getStream]
335
+ );
336
+ return {
337
+ blocks,
338
+ isStreaming,
339
+ design,
340
+ feedResponse,
341
+ feedSSE,
342
+ feedEvent,
343
+ feedChunk,
344
+ feedMeta,
345
+ feedDone,
346
+ push,
347
+ setDesign,
348
+ complete,
349
+ reset
350
+ };
477
351
  }
478
352
 
479
353
  // src/index.ts
354
+ var import_core2 = require("@outkit-dev/core");
355
+ var import_core3 = require("@outkit-dev/core");
480
356
  var import_renderer2 = require("@outkit-dev/renderer");
481
357
  // Annotate the CommonJS export names for ESM import in node:
482
358
  0 && (module.exports = {
483
359
  AIRenderer,
484
360
  OUTKIT_TOKENS,
361
+ OutkitStream,
485
362
  completePartialJson,
486
363
  expandWireBlock,
487
364
  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,101 @@ function useBlockStream() {
383
235
  streamingRef.current = false;
384
236
  setIsStreaming(false);
385
237
  }, []);
238
+ const streamRef = useRef2(null);
239
+ const getStream = useCallback(() => {
240
+ if (streamRef.current === null || streamRef.current.streamState === "destroyed") {
241
+ streamRef.current = new OutkitStream({
242
+ onBlocks: (b) => batchBlocks(b),
243
+ onDesign: (d) => setDesignState(d),
244
+ onComplete: () => handleComplete()
245
+ });
246
+ }
247
+ return streamRef.current;
248
+ }, [batchBlocks, handleComplete]);
249
+ const stream = getStream();
250
+ useEffect2(() => {
251
+ const s = getStream();
252
+ return () => {
253
+ if (rafRef.current) cancelAnimationFrame(rafRef.current);
254
+ s.destroy();
255
+ };
256
+ }, [getStream]);
386
257
  const reset = useCallback(() => {
387
258
  if (rafRef.current) {
388
259
  cancelAnimationFrame(rafRef.current);
389
260
  rafRef.current = 0;
390
261
  }
391
262
  pendingRef.current = null;
392
- accumulatedRef.current = "";
393
- bufferedBlocksRef.current = null;
394
- designReceivedRef.current = false;
395
263
  streamingRef.current = false;
264
+ getStream().reset();
396
265
  setBlocks([]);
397
266
  setIsStreaming(false);
398
267
  setDesignState(void 0);
399
- }, []);
268
+ }, [getStream]);
269
+ const push = useCallback(
270
+ (newBlocks) => batchBlocks(newBlocks),
271
+ [batchBlocks]
272
+ );
273
+ const setDesign = useCallback(
274
+ (tokens) => getStream().feedMeta(tokens),
275
+ [getStream]
276
+ );
277
+ const complete = useCallback(() => getStream().feedDone(), [getStream]);
278
+ const feedResponse = useCallback(
279
+ (response) => getStream().feedResponse(response),
280
+ [getStream]
281
+ );
282
+ const feedSSE = useCallback(
283
+ (rawText) => getStream().feedSSE(rawText),
284
+ [getStream]
285
+ );
286
+ const feedEvent = useCallback(
287
+ (data) => getStream().feedEvent(data),
288
+ [getStream]
289
+ );
400
290
  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]
291
+ (data) => getStream().feedChunk(data),
292
+ [getStream]
413
293
  );
414
294
  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]
295
+ (tokens) => getStream().feedMeta(tokens),
296
+ [getStream]
424
297
  );
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 };
298
+ const feedDone = useCallback(
299
+ () => getStream().feedDone(),
300
+ [getStream]
301
+ );
302
+ return {
303
+ blocks,
304
+ isStreaming,
305
+ design,
306
+ feedResponse,
307
+ feedSSE,
308
+ feedEvent,
309
+ feedChunk,
310
+ feedMeta,
311
+ feedDone,
312
+ push,
313
+ setDesign,
314
+ complete,
315
+ reset
316
+ };
444
317
  }
445
318
 
446
319
  // src/index.ts
320
+ import { OutkitStream as OutkitStream2 } from "@outkit-dev/core";
321
+ import {
322
+ parseStreamingBlocks,
323
+ completePartialJson,
324
+ expandWireBlock,
325
+ isRenderableBlock,
326
+ extractCompleteBlocks
327
+ } from "@outkit-dev/core";
447
328
  import { OUTKIT_TOKENS } from "@outkit-dev/renderer";
448
329
  export {
449
330
  AIRenderer,
450
331
  OUTKIT_TOKENS,
332
+ OutkitStream2 as OutkitStream,
451
333
  completePartialJson,
452
334
  expandWireBlock,
453
335
  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.7",
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.7"
20
21
  },
21
22
  "peerDependencies": {
22
23
  "react": "^18.0.0 || ^19.0.0"