@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 +77 -200
- package/dist/index.d.cts +20 -52
- package/dist/index.d.ts +20 -52
- package/dist/index.js +77 -195
- 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,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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
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
|
-
/**
|
|
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,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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
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.
|
|
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"
|