@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 +51 -202
- package/dist/index.d.cts +20 -52
- package/dist/index.d.ts +20 -52
- package/dist/index.js +51 -197
- package/package.json +3 -2
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
|
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
|
|
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
|
|
434
|
-
(
|
|
435
|
-
|
|
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
|
|
448
|
-
(tokens) =>
|
|
449
|
-
|
|
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
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
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
|
-
/**
|
|
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.
|
|
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
|
-
/**
|
|
59
|
+
/** @deprecated Use feedChunk/feedResponse instead. Direct block snapshot setter. */
|
|
53
60
|
push: (blocks: ContentBlock[]) => void;
|
|
54
|
-
/**
|
|
61
|
+
/** @deprecated Use feedMeta instead. */
|
|
55
62
|
setDesign: (tokens: Record<string, string>) => void;
|
|
56
|
-
/**
|
|
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
|
-
*
|
|
69
|
+
* React hook for streaming Outkit blocks with rAF-batched updates at 60fps.
|
|
63
70
|
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
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
|
-
/**
|
|
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.
|
|
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
|
-
/**
|
|
59
|
+
/** @deprecated Use feedChunk/feedResponse instead. Direct block snapshot setter. */
|
|
53
60
|
push: (blocks: ContentBlock[]) => void;
|
|
54
|
-
/**
|
|
61
|
+
/** @deprecated Use feedMeta instead. */
|
|
55
62
|
setDesign: (tokens: Record<string, string>) => void;
|
|
56
|
-
/**
|
|
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
|
-
*
|
|
69
|
+
* React hook for streaming Outkit blocks with rAF-batched updates at 60fps.
|
|
63
70
|
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
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
|
|
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
|
|
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
|
|
401
|
-
(
|
|
402
|
-
|
|
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
|
|
415
|
-
(tokens) =>
|
|
416
|
-
|
|
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
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
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.
|
|
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"
|