|
|
@@ -3,17 +3,60 @@
|
|
|
*
|
|
|
* 基于 Vercel Labs json-render 的生成式 UI 渲染器
|
|
|
* 接收 AI 生成的 spec 并渲染为 React 组件
|
|
|
+ * 支持组件交互事件(通过 ActionProvider 传递 emit 函数)
|
|
|
*/
|
|
|
|
|
|
'use client';
|
|
|
|
|
|
-import { useState, useMemo } from 'react';
|
|
|
+import React, { useState, useMemo, useContext } from 'react';
|
|
|
import { jsonRenderRegistry } from '@/lib/json-render-registry';
|
|
|
import type { ComponentSpec } from '@/lib/json-render-catalog';
|
|
|
+import { ActionContext } from '@/lib/action-context'; // 导入 ActionContext
|
|
|
|
|
|
// 避免直接导入 json-render 类型,使用 any 来处理类型兼容性
|
|
|
type AnySpec = Record<string, any>;
|
|
|
|
|
|
+/**
|
|
|
+ * 从 markdown 代码块中提取 JSON
|
|
|
+ *
|
|
|
+ * 支持以下格式:
|
|
|
+ * - ```json {...} ```
|
|
|
+ * - ``` {...} ```
|
|
|
+ * - 直接的 JSON 字符串
|
|
|
+ */
|
|
|
+function extractJsonFromMarkdown(text: string): any | null {
|
|
|
+ if (typeof text !== 'string') {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 1. 尝试匹配 ```json ... ``` 或 ``` ... ``` 代码块
|
|
|
+ const codeBlockMatch = text.match(/```(?:json)?\s*(\{[\s\S]*?\})\s*```/);
|
|
|
+ if (codeBlockMatch) {
|
|
|
+ try {
|
|
|
+ return JSON.parse(codeBlockMatch[1]);
|
|
|
+ } catch {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. 尝试匹配数组形式的代码块
|
|
|
+ const arrayBlockMatch = text.match(/```(?:json)?\s*(\[[\s\S]*?\])\s*```/);
|
|
|
+ if (arrayBlockMatch) {
|
|
|
+ try {
|
|
|
+ return JSON.parse(arrayBlockMatch[1]);
|
|
|
+ } catch {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3. 尝试直接解析整个文本
|
|
|
+ try {
|
|
|
+ return JSON.parse(text);
|
|
|
+ } catch {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
interface JsonRendererProps {
|
|
|
// 单个组件 spec
|
|
|
spec?: ComponentSpec | null;
|
|
|
@@ -56,9 +99,25 @@ export default function JsonRenderer({
|
|
|
|
|
|
// 处理原始 data
|
|
|
if (data) {
|
|
|
- // 尝试直接作为 spec
|
|
|
- if (typeof data === 'object' && data !== null && 'type' in data) {
|
|
|
- result.push(data as AnySpec);
|
|
|
+ let parsedData: any = null;
|
|
|
+
|
|
|
+ // 如果 data 是字符串,尝试从 markdown 中提取 JSON
|
|
|
+ if (typeof data === 'string') {
|
|
|
+ parsedData = extractJsonFromMarkdown(data);
|
|
|
+ } else if (typeof data === 'object' && data !== null && 'type' in data) {
|
|
|
+ // 直接作为 spec
|
|
|
+ parsedData = data;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果成功解析了数据
|
|
|
+ if (parsedData) {
|
|
|
+ if (Array.isArray(parsedData)) {
|
|
|
+ // 如果是数组,添加所有元素
|
|
|
+ result.push(...parsedData);
|
|
|
+ } else if (parsedData.type) {
|
|
|
+ // 如果是单个 spec,添加它
|
|
|
+ result.push(parsedData);
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -108,6 +167,9 @@ export default function JsonRenderer({
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
+ // 获取 ActionContext 中的 emit 函数
|
|
|
+ const actionContext = useContext(ActionContext);
|
|
|
+
|
|
|
// 渲染组件
|
|
|
return (
|
|
|
<div className={`json-renderer space-y-4 ${className}`}>
|
|
|
@@ -118,7 +180,12 @@ export default function JsonRenderer({
|
|
|
const CustomComponent = (jsonRenderRegistry as any)[componentType];
|
|
|
|
|
|
if (CustomComponent) {
|
|
|
- return <CustomComponent key={index} {...componentSpec} />;
|
|
|
+ // 传递 emit 函数给组件(如果存在 ActionContext)
|
|
|
+ const props = {
|
|
|
+ ...componentSpec,
|
|
|
+ ...(actionContext && { emit: actionContext.emit }),
|
|
|
+ };
|
|
|
+ return <CustomComponent key={index} {...props} />;
|
|
|
}
|
|
|
|
|
|
// Fallback
|