@outkit-dev/react 0.0.3 → 0.0.5
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 +196 -1
- package/dist/index.d.cts +38 -1
- package/dist/index.d.ts +38 -1
- package/dist/index.js +191 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -22,6 +22,11 @@ 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
30
|
useBlockStream: () => useBlockStream
|
|
26
31
|
});
|
|
27
32
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -223,6 +228,149 @@ var SingleComponent = (0, import_react.memo)(function SingleComponent2({
|
|
|
223
228
|
|
|
224
229
|
// src/use-block-stream.ts
|
|
225
230
|
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
|
|
226
374
|
function useBlockStream() {
|
|
227
375
|
const [blocks, setBlocks] = (0, import_react2.useState)([]);
|
|
228
376
|
const [isStreaming, setIsStreaming] = (0, import_react2.useState)(false);
|
|
@@ -230,6 +378,9 @@ function useBlockStream() {
|
|
|
230
378
|
const pendingRef = (0, import_react2.useRef)(null);
|
|
231
379
|
const rafRef = (0, import_react2.useRef)(0);
|
|
232
380
|
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);
|
|
233
384
|
const flush = (0, import_react2.useCallback)(() => {
|
|
234
385
|
rafRef.current = 0;
|
|
235
386
|
if (pendingRef.current !== null) {
|
|
@@ -271,11 +422,50 @@ function useBlockStream() {
|
|
|
271
422
|
rafRef.current = 0;
|
|
272
423
|
}
|
|
273
424
|
pendingRef.current = null;
|
|
425
|
+
accumulatedRef.current = "";
|
|
426
|
+
bufferedBlocksRef.current = null;
|
|
427
|
+
designReceivedRef.current = false;
|
|
274
428
|
streamingRef.current = false;
|
|
275
429
|
setBlocks([]);
|
|
276
430
|
setIsStreaming(false);
|
|
277
431
|
setDesignState(void 0);
|
|
278
432
|
}, []);
|
|
433
|
+
const feedChunk = (0, import_react2.useCallback)(
|
|
434
|
+
(data) => {
|
|
435
|
+
accumulatedRef.current += data;
|
|
436
|
+
const parsed = parseStreamingBlocks(accumulatedRef.current);
|
|
437
|
+
if (parsed.length > 0) {
|
|
438
|
+
if (designReceivedRef.current) {
|
|
439
|
+
push(parsed);
|
|
440
|
+
} else {
|
|
441
|
+
bufferedBlocksRef.current = parsed;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
},
|
|
445
|
+
[push]
|
|
446
|
+
);
|
|
447
|
+
const feedMeta = (0, import_react2.useCallback)(
|
|
448
|
+
(tokens) => {
|
|
449
|
+
designReceivedRef.current = true;
|
|
450
|
+
setDesignState(tokens);
|
|
451
|
+
if (bufferedBlocksRef.current) {
|
|
452
|
+
push(bufferedBlocksRef.current);
|
|
453
|
+
bufferedBlocksRef.current = null;
|
|
454
|
+
}
|
|
455
|
+
},
|
|
456
|
+
[push]
|
|
457
|
+
);
|
|
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]);
|
|
279
469
|
(0, import_react2.useEffect)(() => {
|
|
280
470
|
return () => {
|
|
281
471
|
if (rafRef.current) {
|
|
@@ -283,7 +473,7 @@ function useBlockStream() {
|
|
|
283
473
|
}
|
|
284
474
|
};
|
|
285
475
|
}, []);
|
|
286
|
-
return { blocks, isStreaming, design, push, setDesign, complete, reset };
|
|
476
|
+
return { blocks, isStreaming, design, feedChunk, feedMeta, feedDone, push, setDesign, complete, reset };
|
|
287
477
|
}
|
|
288
478
|
|
|
289
479
|
// src/index.ts
|
|
@@ -292,5 +482,10 @@ var import_renderer2 = require("@outkit-dev/renderer");
|
|
|
292
482
|
0 && (module.exports = {
|
|
293
483
|
AIRenderer,
|
|
294
484
|
OUTKIT_TOKENS,
|
|
485
|
+
completePartialJson,
|
|
486
|
+
expandWireBlock,
|
|
487
|
+
extractCompleteBlocks,
|
|
488
|
+
isRenderableBlock,
|
|
489
|
+
parseStreamingBlocks,
|
|
295
490
|
useBlockStream
|
|
296
491
|
});
|
package/dist/index.d.cts
CHANGED
|
@@ -43,6 +43,12 @@ interface UseBlockStreamReturn {
|
|
|
43
43
|
isStreaming: boolean;
|
|
44
44
|
/** Design tokens received from the stream's meta event. Pass to <AIRenderer design={design} />. */
|
|
45
45
|
design: Record<string, string> | undefined;
|
|
46
|
+
/** Append a raw SSE data chunk. Parses accumulated buffer and updates blocks. */
|
|
47
|
+
feedChunk: (data: string) => void;
|
|
48
|
+
/** Set design tokens from a meta SSE event. Alias for setDesign. */
|
|
49
|
+
feedMeta: (tokens: Record<string, string>) => void;
|
|
50
|
+
/** Signal end of stream. Final parse + flush + mark complete. */
|
|
51
|
+
feedDone: () => void;
|
|
46
52
|
/** Push a new full blocks array (the latest snapshot). Batched at 60fps. */
|
|
47
53
|
push: (blocks: ContentBlock[]) => void;
|
|
48
54
|
/** Set design tokens (typically from a meta SSE event). */
|
|
@@ -71,4 +77,35 @@ interface UseBlockStreamReturn {
|
|
|
71
77
|
*/
|
|
72
78
|
declare function useBlockStream(): UseBlockStreamReturn;
|
|
73
79
|
|
|
74
|
-
|
|
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 };
|
package/dist/index.d.ts
CHANGED
|
@@ -43,6 +43,12 @@ interface UseBlockStreamReturn {
|
|
|
43
43
|
isStreaming: boolean;
|
|
44
44
|
/** Design tokens received from the stream's meta event. Pass to <AIRenderer design={design} />. */
|
|
45
45
|
design: Record<string, string> | undefined;
|
|
46
|
+
/** Append a raw SSE data chunk. Parses accumulated buffer and updates blocks. */
|
|
47
|
+
feedChunk: (data: string) => void;
|
|
48
|
+
/** Set design tokens from a meta SSE event. Alias for setDesign. */
|
|
49
|
+
feedMeta: (tokens: Record<string, string>) => void;
|
|
50
|
+
/** Signal end of stream. Final parse + flush + mark complete. */
|
|
51
|
+
feedDone: () => void;
|
|
46
52
|
/** Push a new full blocks array (the latest snapshot). Batched at 60fps. */
|
|
47
53
|
push: (blocks: ContentBlock[]) => void;
|
|
48
54
|
/** Set design tokens (typically from a meta SSE event). */
|
|
@@ -71,4 +77,35 @@ interface UseBlockStreamReturn {
|
|
|
71
77
|
*/
|
|
72
78
|
declare function useBlockStream(): UseBlockStreamReturn;
|
|
73
79
|
|
|
74
|
-
|
|
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 };
|
package/dist/index.js
CHANGED
|
@@ -195,6 +195,149 @@ var SingleComponent = memo(function SingleComponent2({
|
|
|
195
195
|
|
|
196
196
|
// src/use-block-stream.ts
|
|
197
197
|
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
|
|
198
341
|
function useBlockStream() {
|
|
199
342
|
const [blocks, setBlocks] = useState([]);
|
|
200
343
|
const [isStreaming, setIsStreaming] = useState(false);
|
|
@@ -202,6 +345,9 @@ function useBlockStream() {
|
|
|
202
345
|
const pendingRef = useRef2(null);
|
|
203
346
|
const rafRef = useRef2(0);
|
|
204
347
|
const streamingRef = useRef2(false);
|
|
348
|
+
const accumulatedRef = useRef2("");
|
|
349
|
+
const designReceivedRef = useRef2(false);
|
|
350
|
+
const bufferedBlocksRef = useRef2(null);
|
|
205
351
|
const flush = useCallback(() => {
|
|
206
352
|
rafRef.current = 0;
|
|
207
353
|
if (pendingRef.current !== null) {
|
|
@@ -243,11 +389,50 @@ function useBlockStream() {
|
|
|
243
389
|
rafRef.current = 0;
|
|
244
390
|
}
|
|
245
391
|
pendingRef.current = null;
|
|
392
|
+
accumulatedRef.current = "";
|
|
393
|
+
bufferedBlocksRef.current = null;
|
|
394
|
+
designReceivedRef.current = false;
|
|
246
395
|
streamingRef.current = false;
|
|
247
396
|
setBlocks([]);
|
|
248
397
|
setIsStreaming(false);
|
|
249
398
|
setDesignState(void 0);
|
|
250
399
|
}, []);
|
|
400
|
+
const feedChunk = useCallback(
|
|
401
|
+
(data) => {
|
|
402
|
+
accumulatedRef.current += data;
|
|
403
|
+
const parsed = parseStreamingBlocks(accumulatedRef.current);
|
|
404
|
+
if (parsed.length > 0) {
|
|
405
|
+
if (designReceivedRef.current) {
|
|
406
|
+
push(parsed);
|
|
407
|
+
} else {
|
|
408
|
+
bufferedBlocksRef.current = parsed;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
},
|
|
412
|
+
[push]
|
|
413
|
+
);
|
|
414
|
+
const feedMeta = useCallback(
|
|
415
|
+
(tokens) => {
|
|
416
|
+
designReceivedRef.current = true;
|
|
417
|
+
setDesignState(tokens);
|
|
418
|
+
if (bufferedBlocksRef.current) {
|
|
419
|
+
push(bufferedBlocksRef.current);
|
|
420
|
+
bufferedBlocksRef.current = null;
|
|
421
|
+
}
|
|
422
|
+
},
|
|
423
|
+
[push]
|
|
424
|
+
);
|
|
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]);
|
|
251
436
|
useEffect2(() => {
|
|
252
437
|
return () => {
|
|
253
438
|
if (rafRef.current) {
|
|
@@ -255,7 +440,7 @@ function useBlockStream() {
|
|
|
255
440
|
}
|
|
256
441
|
};
|
|
257
442
|
}, []);
|
|
258
|
-
return { blocks, isStreaming, design, push, setDesign, complete, reset };
|
|
443
|
+
return { blocks, isStreaming, design, feedChunk, feedMeta, feedDone, push, setDesign, complete, reset };
|
|
259
444
|
}
|
|
260
445
|
|
|
261
446
|
// src/index.ts
|
|
@@ -263,5 +448,10 @@ import { OUTKIT_TOKENS } from "@outkit-dev/renderer";
|
|
|
263
448
|
export {
|
|
264
449
|
AIRenderer,
|
|
265
450
|
OUTKIT_TOKENS,
|
|
451
|
+
completePartialJson,
|
|
452
|
+
expandWireBlock,
|
|
453
|
+
extractCompleteBlocks,
|
|
454
|
+
isRenderableBlock,
|
|
455
|
+
parseStreamingBlocks,
|
|
266
456
|
useBlockStream
|
|
267
457
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@outkit-dev/react",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.cjs",
|
|
6
6
|
"module": "./dist/index.js",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"dist"
|
|
17
17
|
],
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@outkit-dev/renderer": "0.0.
|
|
19
|
+
"@outkit-dev/renderer": "0.0.5"
|
|
20
20
|
},
|
|
21
21
|
"peerDependencies": {
|
|
22
22
|
"react": "^18.0.0 || ^19.0.0"
|