wtfai 1.6.8 → 1.6.9

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/README.md CHANGED
@@ -219,6 +219,49 @@ SDK 会自动将其渲染为一个美观的“药丸状”卡片按钮,包含
219
219
 
220
220
  点击该卡片会触发 `contentAction` 事件,`type` 为 `'workflow'`。
221
221
 
222
+ #### 3. 内容包装器 (contentWrapper)
223
+
224
+ `WorkflowRegistry` 支持 `contentWrapper` 属性,允许开发者在渲染 Markdown 内容前对其进行包装或增强。目前支持对 `code` 块进行包装。
225
+
226
+ **配置类型**:
227
+
228
+ ```typescript
229
+ export type ContentWrapperConfig = {
230
+ code?: (
231
+ props: ComponentProps, // props 包含 lang, children, streamStatus 等
232
+ ) => ((children: ReactNode) => ReactNode) | null | undefined | false
233
+ }
234
+ ```
235
+
236
+ **使用示例**:
237
+
238
+ 比如,你想给所有的 `html:run` 类型的代码预览块增加一个自定义的消息提示:
239
+
240
+ ```tsx
241
+ import { WorkflowRegistry } from 'wtfai';
242
+
243
+ <WorkflowRegistry
244
+ contentWrapper={{
245
+ code: (props) => {
246
+ // 只有特定语言才进行包装
247
+ if (props.lang === 'html:run') {
248
+ return (children) => (
249
+ <div className="my-custom-wrapper">
250
+ <div className="wrapper-header">这是我自定义加的内容</div>
251
+ {children}
252
+ </div>
253
+ )
254
+ }
255
+ return false // 返回 false 表示不包装,由 SDK 按默认方式渲染
256
+ },
257
+ }}
258
+ >
259
+ <ChatPage />
260
+ </WorkflowRegistry>
261
+ ```
262
+
263
+ 这种机制非常适合在不修改 SDK 源码的前提下,为特定类型的内容增加业务相关的 UI 装饰(如操作按钮、免责声明、权限校验提示等)。
264
+
222
265
  **最佳实践(推荐组合)**
223
266
 
224
267
  1. **打字机效果**:使用 `token` 拼接流式内容。
package/dist/ui/code.css CHANGED
@@ -1,51 +1,7 @@
1
- .iframe-code {
1
+ .iframe-code, .html-preview-iframe {
2
2
  aspect-ratio: 16 / 9;
3
3
  border: none;
4
4
  min-width: 50vw;
5
5
  display: block;
6
6
  }
7
7
 
8
- .html-preview-container {
9
- background: #fff;
10
- border: 1px solid #e5e7eb;
11
- border-radius: 12px;
12
- width: 100%;
13
- min-width: min(900px, 100%);
14
- margin: 20px 0;
15
- transition: all .3s cubic-bezier(.4, 0, .2, 1);
16
- overflow: hidden;
17
- box-shadow: 0 10px 15px -3px #0000001a, 0 4px 6px -4px #0000001a;
18
- }
19
-
20
- .html-preview-container:hover {
21
- transform: translateY(-2px);
22
- box-shadow: 0 20px 25px -5px #0000001a, 0 8px 10px -6px #0000001a;
23
- }
24
-
25
- .html-preview-header {
26
- color: #374151;
27
- background: #fdfdfd;
28
- border-bottom: 1px solid #f3f4f6;
29
- justify-content: space-between;
30
- align-items: center;
31
- padding: 10px 16px;
32
- font-size: 13px;
33
- font-weight: 600;
34
- display: flex;
35
- }
36
-
37
- .html-preview-header .preview-label {
38
- color: #2563eb;
39
- align-items: center;
40
- gap: 8px;
41
- display: flex;
42
- }
43
-
44
- .html-preview-iframe {
45
- background: #fff;
46
- border: none;
47
- width: 100%;
48
- min-height: 500px;
49
- display: block;
50
- }
51
-
package/dist/ui/code.js CHANGED
@@ -1,19 +1,17 @@
1
- import { jsx, jsxs } from "react/jsx-runtime";
1
+ import { jsx } from "react/jsx-runtime";
2
2
  import { useEffect, useRef } from "react";
3
3
  import { CodeHighlighter, Mermaid } from "@ant-design/x";
4
- import { EyeOutlined, LoadingOutlined } from "@ant-design/icons";
5
4
  import { jsonrepair } from "jsonrepair";
6
- import { useWorkflowSession } from "./context.js";
5
+ import { useWorkflowRegistry, useWorkflowSession } from "./context.js";
7
6
  import { IframeBridgeHost } from "../iframe-bridge.js";
8
7
  import "./code.css";
8
+ import clsx from "clsx";
9
9
  const Code = (props)=>{
10
- var _className_match;
11
- const { className, children, streamStatus, lang: infoString } = props;
12
- const fullLang = infoString || (null == className ? void 0 : null == (_className_match = className.match(/language-([\w:-]+)/)) ? void 0 : _className_match[1]) || '';
13
- const lang = fullLang.split(':')[0];
10
+ const { children, streamStatus, lang } = props;
14
11
  const session = useWorkflowSession();
15
12
  const iframeRef = useRef(null);
16
13
  const bridgeRef = useRef(null);
14
+ const { contentWrapper } = useWorkflowRegistry();
17
15
  useEffect(()=>{
18
16
  if (session && iframeRef.current) {
19
17
  if (!bridgeRef.current) bridgeRef.current = new IframeBridgeHost(session);
@@ -26,13 +24,14 @@ const Code = (props)=>{
26
24
  }, [
27
25
  session
28
26
  ]);
29
- if ('string' != typeof children) return null;
30
- if ('mermaid' === lang) return /*#__PURE__*/ jsx(Mermaid, {
27
+ let node = null;
28
+ if ('string' != typeof children) node = null;
29
+ else if ('mermaid' === lang) node = /*#__PURE__*/ jsx(Mermaid, {
31
30
  children: children
32
31
  });
33
- if ('tmpl' === lang) try {
32
+ else if ('tmpl' === lang) try {
34
33
  const json = JSON.parse(jsonrepair(children));
35
- if ('iframe' === json.type) return /*#__PURE__*/ jsx("iframe", {
34
+ if ('iframe' === json.type) node = /*#__PURE__*/ jsx("iframe", {
36
35
  ref: iframeRef,
37
36
  src: json.data.src,
38
37
  className: "iframe-code",
@@ -40,44 +39,24 @@ const Code = (props)=>{
40
39
  allow: "clipboard-read; clipboard-write; camera; microphone"
41
40
  });
42
41
  } catch {}
43
- if ('html:run' === fullLang) {
42
+ else if ('html:run' === lang) {
44
43
  const isLoading = 'loading' === streamStatus;
45
- return /*#__PURE__*/ jsxs("div", {
46
- className: "html-preview-container",
47
- children: [
48
- /*#__PURE__*/ jsxs("div", {
49
- className: "html-preview-header",
50
- children: [
51
- /*#__PURE__*/ jsxs("div", {
52
- className: "preview-label",
53
- children: [
54
- /*#__PURE__*/ jsx(EyeOutlined, {}),
55
- /*#__PURE__*/ jsx("span", {
56
- children: "预览"
57
- })
58
- ]
59
- }),
60
- isLoading && /*#__PURE__*/ jsx(LoadingOutlined, {
61
- spin: true,
62
- className: "loading-icon",
63
- style: {
64
- color: '#3b82f6'
65
- }
66
- })
67
- ]
68
- }),
69
- /*#__PURE__*/ jsx("iframe", {
70
- srcDoc: children,
71
- className: "html-preview-iframe",
72
- title: "HTML Preview",
73
- sandbox: "allow-scripts allow-forms allow-popups"
74
- })
75
- ]
44
+ node = /*#__PURE__*/ jsx("iframe", {
45
+ srcDoc: children,
46
+ className: clsx('html-preview-iframe', {
47
+ 'html-preview-iframe-loading': isLoading
48
+ }),
49
+ title: "HTML Preview",
50
+ sandbox: "allow-scripts allow-forms allow-popups"
76
51
  });
77
- }
78
- return /*#__PURE__*/ jsx(CodeHighlighter, {
52
+ } else node = /*#__PURE__*/ jsx(CodeHighlighter, {
79
53
  lang: lang,
80
54
  children: children
81
55
  });
56
+ if (null == contentWrapper ? void 0 : contentWrapper.code) {
57
+ const result = contentWrapper.code(props);
58
+ if (false !== result && null != result) return result(node);
59
+ }
60
+ return node;
82
61
  };
83
62
  export { Code };
@@ -1,17 +1,28 @@
1
+ import { ReactNode } from 'react';
1
2
  import type { WorkflowSession } from '../session';
3
+ import { type ComponentProps } from '@ant-design/x-markdown';
2
4
  export interface WorkflowInfoMapping {
3
5
  name: string;
4
6
  icon?: React.ReactNode;
5
7
  /** 额外的信息,会在点击的时候透传给调用方,方便使用 */
6
8
  extra?: Record<string, any>;
7
9
  }
10
+ export type ContentWrapperConfig = {
11
+ code?: (props: ComponentProps) => ((children: ReactNode) => ReactNode) | null | undefined | false;
12
+ };
8
13
  export declare const SessionContext: import("react").Context<WorkflowSession | null>;
9
- export declare const WorkflowRegistryContext: import("react").Context<Record<string, WorkflowInfoMapping>>;
14
+ export declare const WorkflowRegistryContext: import("react").Context<{
15
+ mapping?: Record<string, WorkflowInfoMapping>;
16
+ contentWrapper?: ContentWrapperConfig;
17
+ }>;
10
18
  export declare const MarkdownContext: import("react").Context<{
11
19
  isStreaming?: boolean;
12
20
  }>;
13
21
  export declare const useWorkflowSession: () => WorkflowSession | null;
14
- export declare const useWorkflowRegistry: () => Record<string, WorkflowInfoMapping>;
22
+ export declare const useWorkflowRegistry: () => {
23
+ mapping?: Record<string, WorkflowInfoMapping>;
24
+ contentWrapper?: ContentWrapperConfig;
25
+ };
15
26
  export declare const useMarkdownContext: () => {
16
27
  isStreaming?: boolean;
17
28
  };
@@ -1,7 +1,7 @@
1
1
  import { createContext, useContext } from "react";
2
- const SessionContext = createContext(null);
3
- const WorkflowRegistryContext = createContext({});
4
- const MarkdownContext = createContext({
2
+ const SessionContext = /*#__PURE__*/ createContext(null);
3
+ const WorkflowRegistryContext = /*#__PURE__*/ createContext({});
4
+ const MarkdownContext = /*#__PURE__*/ createContext({
5
5
  isStreaming: false
6
6
  });
7
7
  const useWorkflowSession = ()=>useContext(SessionContext);
@@ -1,7 +1,7 @@
1
1
  import { XMarkdownProps } from '@ant-design/x-markdown';
2
2
  import '@ant-design/x-markdown/themes/light.css';
3
3
  import './markdown.css';
4
- import { WorkflowInfoMapping } from './context';
4
+ import { WorkflowInfoMapping, ContentWrapperConfig } from './context';
5
5
  import type { WorkflowSession } from '../session';
6
6
  export interface MarkdownProps extends XMarkdownProps {
7
7
  session?: WorkflowSession;
@@ -9,8 +9,15 @@ export interface MarkdownProps extends XMarkdownProps {
9
9
  /**
10
10
  * 工作流信息注册表提供者
11
11
  */
12
- export declare const WorkflowRegistry: ({ children, mapping, }: {
12
+ export declare const WorkflowRegistry: ({ children, mapping, contentWrapper, }: {
13
13
  children: React.ReactNode;
14
- mapping: Record<string, WorkflowInfoMapping>;
14
+ /**
15
+ * 工作流信息映射,控制工作流跳转 a 标签的样式
16
+ */
17
+ mapping?: Record<string, WorkflowInfoMapping>;
18
+ /**
19
+ * 内容包装器,对渲染的内容进行包装
20
+ */
21
+ contentWrapper?: ContentWrapperConfig;
15
22
  }) => import("react/jsx-runtime").JSX.Element;
16
23
  export declare const Markdown: ({ className, streaming, config, components, session, ...props }: MarkdownProps) => import("react/jsx-runtime").JSX.Element;
@@ -9,16 +9,23 @@ import { Code } from "./code.js";
9
9
  import { XProvider } from "@ant-design/x";
10
10
  import "./markdown.css";
11
11
  import { MarkdownContext, SessionContext, WorkflowRegistryContext, useWorkflowRegistry } from "./context.js";
12
- const WorkflowRegistry = ({ children, mapping })=>{
12
+ const WorkflowRegistry = ({ children, mapping, contentWrapper })=>{
13
13
  const parentRegistry = useWorkflowRegistry();
14
14
  const mergedRegistry = {
15
- ...parentRegistry,
15
+ ...parentRegistry.mapping,
16
16
  ...mapping
17
17
  };
18
+ const mergedContentWrapper = {
19
+ ...parentRegistry.contentWrapper,
20
+ ...contentWrapper
21
+ };
18
22
  return /*#__PURE__*/ jsx(SessionContext.Provider, {
19
23
  value: null,
20
24
  children: /*#__PURE__*/ jsx(WorkflowRegistryContext.Provider, {
21
- value: mergedRegistry,
25
+ value: {
26
+ mapping: mergedRegistry,
27
+ contentWrapper: mergedContentWrapper
28
+ },
22
29
  children: children
23
30
  })
24
31
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wtfai",
3
- "version": "1.6.8",
3
+ "version": "1.6.9",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -32,8 +32,8 @@
32
32
  },
33
33
  "dependencies": {
34
34
  "@ant-design/icons": "^6.1.0",
35
- "@ant-design/x": "^2.3.0",
36
- "@ant-design/x-markdown": "^2.3.0",
35
+ "@ant-design/x": "^2.4.0",
36
+ "@ant-design/x-markdown": "^2.4.0",
37
37
  "@microsoft/fetch-event-source": "^2.0.1",
38
38
  "clsx": "^2.1.1",
39
39
  "compressorjs": "^1.2.1",