@hsu-react/ui 0.0.16 → 0.0.17
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/es/components/Chat/ChatList/_components/Assistant/AssistantItem/_components/Copy/index.module.scss +0 -16
- package/es/components/Copy/index.d.ts +2 -1
- package/es/components/Copy/index.module.scss +1 -1
- package/es/components/Markdown/Views/index.js +105 -74
- package/es/components/Markdown/Views/index.module.scss +15 -0
- package/lib/components/Chat/ChatList/_components/Assistant/AssistantItem/_components/Copy/index.module.scss +0 -16
- package/lib/components/Copy/index.d.ts +2 -1
- package/lib/components/Copy/index.module.scss +1 -1
- package/lib/components/Markdown/Views/index.js +108 -76
- package/lib/components/Markdown/Views/index.module.scss +15 -0
- package/package.json +1 -1
- package/src/components/Chat/ChatList/_components/Assistant/AssistantItem/_components/Copy/index.module.scss +0 -16
- package/src/components/Copy/index.module.scss +1 -1
- package/src/components/Copy/index.tsx +2 -1
- package/src/components/Markdown/Views/index.module.scss +15 -0
- package/src/components/Markdown/Views/index.tsx +99 -66
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import React, { ReactNode } from "react";
|
|
2
2
|
export interface CopyProps {
|
|
3
3
|
id: string;
|
|
4
|
-
|
|
4
|
+
/** true (default): report via a global toast; false: swap the button for an inline "已复制!" state */
|
|
5
|
+
isMessage?: boolean;
|
|
5
6
|
md?: boolean;
|
|
6
7
|
text?: string;
|
|
7
8
|
copyIcon?: ReactNode;
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
var _excluded = ["className", "children"],
|
|
2
|
-
_excluded2 = ["children"];
|
|
3
1
|
function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); }
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
var _excluded = ["className", "children", "node"],
|
|
3
|
+
_excluded2 = ["children", "node"];
|
|
6
4
|
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
|
|
7
5
|
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
|
|
8
6
|
function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
|
|
9
7
|
function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : String(i); }
|
|
10
8
|
function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
|
|
11
|
-
|
|
9
|
+
function _objectWithoutProperties(source, excluded) { if (source == null) return {}; var target = _objectWithoutPropertiesLoose(source, excluded); var key, i; if (Object.getOwnPropertySymbols) { var sourceSymbolKeys = Object.getOwnPropertySymbols(source); for (i = 0; i < sourceSymbolKeys.length; i++) { key = sourceSymbolKeys[i]; if (excluded.indexOf(key) >= 0) continue; if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue; target[key] = source[key]; } } return target; }
|
|
10
|
+
function _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; }
|
|
11
|
+
import React, { createContext, useContext, useId } from "react";
|
|
12
12
|
import ReactMarkdown from "react-markdown";
|
|
13
13
|
import rehypeHighlight from "rehype-highlight";
|
|
14
14
|
import rehypeKatex from "rehype-katex";
|
|
@@ -18,7 +18,6 @@ import classNames from "classnames";
|
|
|
18
18
|
import styles from "./index.module.scss";
|
|
19
19
|
import "highlight.js/styles/atom-one-dark.min.css";
|
|
20
20
|
import "katex/dist/katex.min.css";
|
|
21
|
-
import { generateRandomStr } from "hsu-utils";
|
|
22
21
|
import Copy from "../../Copy";
|
|
23
22
|
import MermaidBlock from "./MermaidBlock";
|
|
24
23
|
import ArtifactBlock from "./ArtifactBlock";
|
|
@@ -41,77 +40,109 @@ var extractText = function extractText(node) {
|
|
|
41
40
|
}
|
|
42
41
|
return "";
|
|
43
42
|
};
|
|
44
|
-
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Copy button config, handed to the code renderer through context rather than a closure so the
|
|
46
|
+
* renderer can stay a module-level component (see CodeBlock).
|
|
47
|
+
*/
|
|
48
|
+
var CopyPropsContext = /*#__PURE__*/createContext(undefined);
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Fenced code block: mermaid diagram, Artifacts preview, or highlighted code with a copy button.
|
|
52
|
+
*
|
|
53
|
+
* Declared at module scope on purpose. Defining it inline inside MarkdownViews would hand
|
|
54
|
+
* react-markdown a brand-new element type on every render, so React would unmount and remount
|
|
55
|
+
* every code block whenever the markdown re-renders — making the copy button flicker (and losing
|
|
56
|
+
* its "已复制!" state) on each token of streamed content.
|
|
57
|
+
*/
|
|
58
|
+
var CodeBlock = function CodeBlock(props) {
|
|
59
|
+
var _exec;
|
|
60
|
+
// `node` is react-markdown's hast node — pulled out so it never reaches the DOM
|
|
45
61
|
var className = props.className,
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
62
|
+
children = props.children,
|
|
63
|
+
_node = props.node,
|
|
64
|
+
codeProps = _objectWithoutProperties(props, _excluded);
|
|
65
|
+
var copyProps = useContext(CopyPropsContext);
|
|
66
|
+
// Stable for the lifetime of the block: regenerating the id on every render would leave the
|
|
67
|
+
// copy button's getElementById lookup pointing at a stale node. Strip ':' so the id stays
|
|
68
|
+
// usable with CSS selectors as well as getElementById.
|
|
69
|
+
var id = "md-code-".concat(useId().replace(/:/g, ""));
|
|
70
|
+
var lang = (_exec = /language-(\w+)/.exec(className || "")) === null || _exec === void 0 || (_exec = _exec[1]) === null || _exec === void 0 ? void 0 : _exec.toLowerCase();
|
|
71
|
+
if (!lang) {
|
|
72
|
+
return /*#__PURE__*/_jsx("code", _objectSpread(_objectSpread({}, codeProps), {}, {
|
|
73
|
+
children: children
|
|
74
|
+
}));
|
|
75
|
+
}
|
|
76
|
+
var codeText = extractText(children);
|
|
77
|
+
|
|
78
|
+
// Mermaid diagrams: render directly as SVG (fall back to code display when streaming output is incomplete)
|
|
79
|
+
if (lang === "mermaid") {
|
|
80
|
+
return /*#__PURE__*/_jsx(MermaidBlock, {
|
|
81
|
+
code: codeText
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
var codeView = /*#__PURE__*/_jsxs("div", {
|
|
85
|
+
className: classNames(styles.code),
|
|
86
|
+
children: [/*#__PURE__*/_jsx("div", {
|
|
87
|
+
className: classNames(styles.nav),
|
|
88
|
+
children: /*#__PURE__*/_jsx(Copy, _objectSpread({
|
|
89
|
+
id: id,
|
|
90
|
+
md: false
|
|
91
|
+
}, copyProps))
|
|
92
|
+
}), /*#__PURE__*/_jsx("div", {
|
|
93
|
+
className: classNames(styles.content),
|
|
94
|
+
children: /*#__PURE__*/_jsx("code", _objectSpread(_objectSpread({
|
|
95
|
+
id: id
|
|
96
|
+
}, codeProps), {}, {
|
|
97
|
+
className: classNames(styles.code_content, className),
|
|
98
|
+
children: children
|
|
99
|
+
}))
|
|
100
|
+
})]
|
|
101
|
+
});
|
|
61
102
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
className: classNames(styles.nav),
|
|
73
|
-
children: /*#__PURE__*/_jsx(Copy, _objectSpread({
|
|
74
|
-
id: id,
|
|
75
|
-
md: false
|
|
76
|
-
}, copyProps))
|
|
77
|
-
}), /*#__PURE__*/_jsx("div", {
|
|
78
|
-
className: classNames(styles.content),
|
|
79
|
-
children: /*#__PURE__*/_jsx("code", _objectSpread(_objectSpread({
|
|
80
|
-
id: id
|
|
81
|
-
}, props), {}, {
|
|
82
|
-
className: classNames(styles.code_content, className),
|
|
83
|
-
children: children
|
|
84
|
-
}))
|
|
85
|
-
})]
|
|
86
|
-
});
|
|
103
|
+
// HTML/SVG: Artifacts preview (sandbox iframe) + code tabs
|
|
104
|
+
if (ARTIFACT_LANGS.has(lang)) {
|
|
105
|
+
return /*#__PURE__*/_jsx(ArtifactBlock, {
|
|
106
|
+
code: codeText,
|
|
107
|
+
lang: lang,
|
|
108
|
+
codeView: codeView
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
return codeView;
|
|
112
|
+
};
|
|
87
113
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
return /*#__PURE__*/_jsx("code", _objectSpread(_objectSpread({}, props), {}, {
|
|
99
|
-
children: children
|
|
100
|
-
}));
|
|
101
|
-
},
|
|
102
|
-
td: function td(_ref2) {
|
|
103
|
-
var children = _ref2.children,
|
|
104
|
-
props = _objectWithoutProperties(_ref2, _excluded2);
|
|
105
|
-
return /*#__PURE__*/_jsx("td", _objectSpread(_objectSpread({}, props), {}, {
|
|
106
|
-
children: Array.isArray(children) ? children === null || children === void 0 ? void 0 : children.map(function (item) {
|
|
107
|
-
if (item === "<br>") {
|
|
108
|
-
return /*#__PURE__*/_jsx("br", {});
|
|
109
|
-
}
|
|
110
|
-
return item;
|
|
111
|
-
}) : children
|
|
112
|
-
}));
|
|
113
|
-
}
|
|
114
|
-
}, extraComponents || {})
|
|
114
|
+
/** Table cell that turns literal "<br>" text nodes into real line breaks. */
|
|
115
|
+
var TableCell = function TableCell(props) {
|
|
116
|
+
// `node` is react-markdown's hast node — pulled out so it never reaches the DOM
|
|
117
|
+
var children = props.children,
|
|
118
|
+
_node = props.node,
|
|
119
|
+
tdProps = _objectWithoutProperties(props, _excluded2);
|
|
120
|
+
return /*#__PURE__*/_jsx("td", _objectSpread(_objectSpread({}, tdProps), {}, {
|
|
121
|
+
children: Array.isArray(children) ? children === null || children === void 0 ? void 0 : children.map(function (item, index) {
|
|
122
|
+
return item === "<br>" ? /*#__PURE__*/_jsx("br", {}, index) : item;
|
|
123
|
+
}) : children
|
|
115
124
|
}));
|
|
116
125
|
};
|
|
126
|
+
var MarkdownViews = function MarkdownViews(props) {
|
|
127
|
+
var className = props.className,
|
|
128
|
+
copyProps = props.copyProps,
|
|
129
|
+
extraComponents = props.components;
|
|
130
|
+
return (
|
|
131
|
+
/*#__PURE__*/
|
|
132
|
+
// The components map below may be a fresh object each render; that is harmless because the
|
|
133
|
+
// renderers it points at are stable module-level types, which is what React reconciles on.
|
|
134
|
+
_jsx(CopyPropsContext.Provider, {
|
|
135
|
+
value: copyProps,
|
|
136
|
+
children: /*#__PURE__*/_jsx(ReactMarkdown, _objectSpread(_objectSpread({}, props), {}, {
|
|
137
|
+
rehypePlugins: [rehypeHighlight, rehypeKatex],
|
|
138
|
+
remarkPlugins: [remarkGfm, remarkMath],
|
|
139
|
+
className: classNames(styles.MarkdownViews, className, "markdown-body"),
|
|
140
|
+
components: _objectSpread({
|
|
141
|
+
code: CodeBlock,
|
|
142
|
+
td: TableCell
|
|
143
|
+
}, extraComponents || {})
|
|
144
|
+
}))
|
|
145
|
+
})
|
|
146
|
+
);
|
|
147
|
+
};
|
|
117
148
|
export default MarkdownViews;
|
|
@@ -68,6 +68,21 @@
|
|
|
68
68
|
color: #efefef;
|
|
69
69
|
|
|
70
70
|
background: #212121;
|
|
71
|
+
|
|
72
|
+
// Copy's root div (class is module-hashed, so it can only be reached as a child).
|
|
73
|
+
// It colors itself from theme tokens — in light mode --cf-text-2 is near-black,
|
|
74
|
+
// which disappears against this strip. The strip stays dark on purpose to match the
|
|
75
|
+
// atom-one-dark code body, so the button is pinned to light-on-dark rather than
|
|
76
|
+
// following the page theme.
|
|
77
|
+
> div {
|
|
78
|
+
color: rgba(255, 255, 255, 0.65);
|
|
79
|
+
|
|
80
|
+
&:hover {
|
|
81
|
+
color: #ffffff;
|
|
82
|
+
|
|
83
|
+
background-color: rgba(255, 255, 255, 0.12);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
71
86
|
}
|
|
72
87
|
}
|
|
73
88
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import React, { ReactNode } from "react";
|
|
2
2
|
export interface CopyProps {
|
|
3
3
|
id: string;
|
|
4
|
-
|
|
4
|
+
/** true (default): report via a global toast; false: swap the button for an inline "已复制!" state */
|
|
5
|
+
isMessage?: boolean;
|
|
5
6
|
md?: boolean;
|
|
6
7
|
text?: string;
|
|
7
8
|
copyIcon?: ReactNode;
|
|
@@ -4,7 +4,7 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
6
|
exports.default = void 0;
|
|
7
|
-
var _react =
|
|
7
|
+
var _react = _interopRequireWildcard(require("react"));
|
|
8
8
|
var _reactMarkdown = _interopRequireDefault(require("react-markdown"));
|
|
9
9
|
var _rehypeHighlight = _interopRequireDefault(require("rehype-highlight"));
|
|
10
10
|
var _rehypeKatex = _interopRequireDefault(require("rehype-katex"));
|
|
@@ -14,12 +14,13 @@ var _classnames = _interopRequireDefault(require("classnames"));
|
|
|
14
14
|
var _indexModule = _interopRequireDefault(require("./index.module.scss"));
|
|
15
15
|
require("highlight.js/styles/atom-one-dark.min.css");
|
|
16
16
|
require("katex/dist/katex.min.css");
|
|
17
|
-
var _hsuUtils = require("hsu-utils");
|
|
18
17
|
var _Copy = _interopRequireDefault(require("../../Copy"));
|
|
19
18
|
var _MermaidBlock = _interopRequireDefault(require("./MermaidBlock"));
|
|
20
19
|
var _ArtifactBlock = _interopRequireDefault(require("./ArtifactBlock"));
|
|
21
20
|
var _jsxRuntime = require("react/jsx-runtime");
|
|
22
21
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
22
|
+
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
|
|
23
|
+
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
|
|
23
24
|
/** Code block languages that support Artifacts preview (modeled after Claude Artifacts) */const ARTIFACT_LANGS = new Set(["html", "svg"]);
|
|
24
25
|
|
|
25
26
|
/**
|
|
@@ -35,86 +36,117 @@ const extractText = node => {
|
|
|
35
36
|
}
|
|
36
37
|
return "";
|
|
37
38
|
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Copy button config, handed to the code renderer through context rather than a closure so the
|
|
42
|
+
* renderer can stay a module-level component (see CodeBlock).
|
|
43
|
+
*/
|
|
44
|
+
const CopyPropsContext = /*#__PURE__*/(0, _react.createContext)(undefined);
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Fenced code block: mermaid diagram, Artifacts preview, or highlighted code with a copy button.
|
|
48
|
+
*
|
|
49
|
+
* Declared at module scope on purpose. Defining it inline inside MarkdownViews would hand
|
|
50
|
+
* react-markdown a brand-new element type on every render, so React would unmount and remount
|
|
51
|
+
* every code block whenever the markdown re-renders — making the copy button flicker (and losing
|
|
52
|
+
* its "已复制!" state) on each token of streamed content.
|
|
53
|
+
*/
|
|
54
|
+
const CodeBlock = props => {
|
|
55
|
+
// `node` is react-markdown's hast node — pulled out so it never reaches the DOM
|
|
56
|
+
const {
|
|
57
|
+
className,
|
|
58
|
+
children,
|
|
59
|
+
node: _node,
|
|
60
|
+
...codeProps
|
|
61
|
+
} = props;
|
|
62
|
+
const copyProps = (0, _react.useContext)(CopyPropsContext);
|
|
63
|
+
// Stable for the lifetime of the block: regenerating the id on every render would leave the
|
|
64
|
+
// copy button's getElementById lookup pointing at a stale node. Strip ':' so the id stays
|
|
65
|
+
// usable with CSS selectors as well as getElementById.
|
|
66
|
+
const id = `md-code-${(0, _react.useId)().replace(/:/g, "")}`;
|
|
67
|
+
const lang = /language-(\w+)/.exec(className || "")?.[1]?.toLowerCase();
|
|
68
|
+
if (!lang) {
|
|
69
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsx)("code", {
|
|
70
|
+
...codeProps,
|
|
71
|
+
children: children
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
const codeText = extractText(children);
|
|
75
|
+
|
|
76
|
+
// Mermaid diagrams: render directly as SVG (fall back to code display when streaming output is incomplete)
|
|
77
|
+
if (lang === "mermaid") {
|
|
78
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_MermaidBlock.default, {
|
|
79
|
+
code: codeText
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
const codeView = /*#__PURE__*/(0, _jsxRuntime.jsxs)("div", {
|
|
83
|
+
className: (0, _classnames.default)(_indexModule.default.code),
|
|
84
|
+
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)("div", {
|
|
85
|
+
className: (0, _classnames.default)(_indexModule.default.nav),
|
|
86
|
+
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_Copy.default, {
|
|
87
|
+
id: id,
|
|
88
|
+
md: false,
|
|
89
|
+
...copyProps
|
|
90
|
+
})
|
|
91
|
+
}), /*#__PURE__*/(0, _jsxRuntime.jsx)("div", {
|
|
92
|
+
className: (0, _classnames.default)(_indexModule.default.content),
|
|
93
|
+
children: /*#__PURE__*/(0, _jsxRuntime.jsx)("code", {
|
|
94
|
+
id: id,
|
|
95
|
+
...codeProps,
|
|
96
|
+
className: (0, _classnames.default)(_indexModule.default.code_content, className),
|
|
97
|
+
children: children
|
|
98
|
+
})
|
|
99
|
+
})]
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// HTML/SVG: Artifacts preview (sandbox iframe) + code tabs
|
|
103
|
+
if (ARTIFACT_LANGS.has(lang)) {
|
|
104
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_ArtifactBlock.default, {
|
|
105
|
+
code: codeText,
|
|
106
|
+
lang: lang,
|
|
107
|
+
codeView: codeView
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
return codeView;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
/** Table cell that turns literal "<br>" text nodes into real line breaks. */
|
|
114
|
+
const TableCell = props => {
|
|
115
|
+
// `node` is react-markdown's hast node — pulled out so it never reaches the DOM
|
|
116
|
+
const {
|
|
117
|
+
children,
|
|
118
|
+
node: _node,
|
|
119
|
+
...tdProps
|
|
120
|
+
} = props;
|
|
121
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsx)("td", {
|
|
122
|
+
...tdProps,
|
|
123
|
+
children: Array.isArray(children) ? children?.map((item, index) => item === "<br>" ? /*#__PURE__*/(0, _jsxRuntime.jsx)("br", {}, index) : item) : children
|
|
124
|
+
});
|
|
125
|
+
};
|
|
38
126
|
const MarkdownViews = props => {
|
|
39
127
|
const {
|
|
40
128
|
className,
|
|
41
129
|
copyProps,
|
|
42
130
|
components: extraComponents
|
|
43
131
|
} = props;
|
|
44
|
-
return
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
// Mermaid diagrams: render directly as SVG (fall back to code display when streaming output is incomplete)
|
|
61
|
-
if (lang === "mermaid") {
|
|
62
|
-
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_MermaidBlock.default, {
|
|
63
|
-
code: codeText
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
const id = (0, _hsuUtils.generateRandomStr)(10);
|
|
67
|
-
const codeView = /*#__PURE__*/(0, _jsxRuntime.jsxs)("div", {
|
|
68
|
-
className: (0, _classnames.default)(_indexModule.default.code),
|
|
69
|
-
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)("div", {
|
|
70
|
-
className: (0, _classnames.default)(_indexModule.default.nav),
|
|
71
|
-
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_Copy.default, {
|
|
72
|
-
id: id,
|
|
73
|
-
md: false,
|
|
74
|
-
...copyProps
|
|
75
|
-
})
|
|
76
|
-
}), /*#__PURE__*/(0, _jsxRuntime.jsx)("div", {
|
|
77
|
-
className: (0, _classnames.default)(_indexModule.default.content),
|
|
78
|
-
children: /*#__PURE__*/(0, _jsxRuntime.jsx)("code", {
|
|
79
|
-
id: id,
|
|
80
|
-
...props,
|
|
81
|
-
className: (0, _classnames.default)(_indexModule.default.code_content, className),
|
|
82
|
-
children: children
|
|
83
|
-
})
|
|
84
|
-
})]
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
// HTML/SVG: Artifacts preview (sandbox iframe) + code tabs
|
|
88
|
-
if (ARTIFACT_LANGS.has(lang)) {
|
|
89
|
-
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_ArtifactBlock.default, {
|
|
90
|
-
code: codeText,
|
|
91
|
-
lang: lang,
|
|
92
|
-
codeView: codeView
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
return codeView;
|
|
132
|
+
return (
|
|
133
|
+
/*#__PURE__*/
|
|
134
|
+
// The components map below may be a fresh object each render; that is harmless because the
|
|
135
|
+
// renderers it points at are stable module-level types, which is what React reconciles on.
|
|
136
|
+
(0, _jsxRuntime.jsx)(CopyPropsContext.Provider, {
|
|
137
|
+
value: copyProps,
|
|
138
|
+
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactMarkdown.default, {
|
|
139
|
+
...props,
|
|
140
|
+
rehypePlugins: [_rehypeHighlight.default, _rehypeKatex.default],
|
|
141
|
+
remarkPlugins: [_remarkGfm.default, _remarkMath.default],
|
|
142
|
+
className: (0, _classnames.default)(_indexModule.default.MarkdownViews, className, "markdown-body"),
|
|
143
|
+
components: {
|
|
144
|
+
code: CodeBlock,
|
|
145
|
+
td: TableCell,
|
|
146
|
+
...(extraComponents || {})
|
|
96
147
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
});
|
|
101
|
-
},
|
|
102
|
-
td: ({
|
|
103
|
-
children,
|
|
104
|
-
...props
|
|
105
|
-
}) => {
|
|
106
|
-
return /*#__PURE__*/(0, _jsxRuntime.jsx)("td", {
|
|
107
|
-
...props,
|
|
108
|
-
children: Array.isArray(children) ? children?.map(item => {
|
|
109
|
-
if (item === "<br>") {
|
|
110
|
-
return /*#__PURE__*/(0, _jsxRuntime.jsx)("br", {});
|
|
111
|
-
}
|
|
112
|
-
return item;
|
|
113
|
-
}) : children
|
|
114
|
-
});
|
|
115
|
-
},
|
|
116
|
-
...(extraComponents || {})
|
|
117
|
-
}
|
|
118
|
-
});
|
|
148
|
+
})
|
|
149
|
+
})
|
|
150
|
+
);
|
|
119
151
|
};
|
|
120
152
|
var _default = exports.default = MarkdownViews;
|
|
@@ -68,6 +68,21 @@
|
|
|
68
68
|
color: #efefef;
|
|
69
69
|
|
|
70
70
|
background: #212121;
|
|
71
|
+
|
|
72
|
+
// Copy's root div (class is module-hashed, so it can only be reached as a child).
|
|
73
|
+
// It colors itself from theme tokens — in light mode --cf-text-2 is near-black,
|
|
74
|
+
// which disappears against this strip. The strip stays dark on purpose to match the
|
|
75
|
+
// atom-one-dark code body, so the button is pinned to light-on-dark rather than
|
|
76
|
+
// following the page theme.
|
|
77
|
+
> div {
|
|
78
|
+
color: rgba(255, 255, 255, 0.65);
|
|
79
|
+
|
|
80
|
+
&:hover {
|
|
81
|
+
color: #ffffff;
|
|
82
|
+
|
|
83
|
+
background-color: rgba(255, 255, 255, 0.12);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
71
86
|
}
|
|
72
87
|
}
|
|
73
88
|
}
|
package/package.json
CHANGED
|
@@ -8,7 +8,8 @@ import styles from "./index.module.scss";
|
|
|
8
8
|
|
|
9
9
|
export interface CopyProps {
|
|
10
10
|
id: string;
|
|
11
|
-
|
|
11
|
+
/** true (default): report via a global toast; false: swap the button for an inline "已复制!" state */
|
|
12
|
+
isMessage?: boolean;
|
|
12
13
|
md?: boolean;
|
|
13
14
|
text?: string;
|
|
14
15
|
copyIcon?: ReactNode;
|
|
@@ -68,6 +68,21 @@
|
|
|
68
68
|
color: #efefef;
|
|
69
69
|
|
|
70
70
|
background: #212121;
|
|
71
|
+
|
|
72
|
+
// Copy's root div (class is module-hashed, so it can only be reached as a child).
|
|
73
|
+
// It colors itself from theme tokens — in light mode --cf-text-2 is near-black,
|
|
74
|
+
// which disappears against this strip. The strip stays dark on purpose to match the
|
|
75
|
+
// atom-one-dark code body, so the button is pinned to light-on-dark rather than
|
|
76
|
+
// following the page theme.
|
|
77
|
+
> div {
|
|
78
|
+
color: rgba(255, 255, 255, 0.65);
|
|
79
|
+
|
|
80
|
+
&:hover {
|
|
81
|
+
color: #ffffff;
|
|
82
|
+
|
|
83
|
+
background-color: rgba(255, 255, 255, 0.12);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
71
86
|
}
|
|
72
87
|
}
|
|
73
88
|
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
import ReactMarkdown, {
|
|
1
|
+
import React, { createContext, useContext, useId } from "react";
|
|
2
|
+
import ReactMarkdown, {
|
|
3
|
+
Options as ReactMarkdownProps,
|
|
4
|
+
ExtraProps,
|
|
5
|
+
} from "react-markdown";
|
|
3
6
|
import rehypeHighlight from "rehype-highlight";
|
|
4
7
|
import rehypeKatex from "rehype-katex";
|
|
5
8
|
import remarkGfm from "remark-gfm";
|
|
@@ -8,7 +11,6 @@ import classNames from "classnames";
|
|
|
8
11
|
import styles from "./index.module.scss";
|
|
9
12
|
import "highlight.js/styles/atom-one-dark.min.css";
|
|
10
13
|
import "katex/dist/katex.min.css";
|
|
11
|
-
import { generateRandomStr } from "hsu-utils";
|
|
12
14
|
import Copy, { CopyProps } from "../../Copy";
|
|
13
15
|
import MermaidBlock from "./MermaidBlock";
|
|
14
16
|
import ArtifactBlock from "./ArtifactBlock";
|
|
@@ -30,6 +32,85 @@ const extractText = (node: React.ReactNode): string => {
|
|
|
30
32
|
return "";
|
|
31
33
|
};
|
|
32
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Copy button config, handed to the code renderer through context rather than a closure so the
|
|
37
|
+
* renderer can stay a module-level component (see CodeBlock).
|
|
38
|
+
*/
|
|
39
|
+
const CopyPropsContext = createContext<Omit<CopyProps, "id"> | undefined>(
|
|
40
|
+
undefined,
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Fenced code block: mermaid diagram, Artifacts preview, or highlighted code with a copy button.
|
|
45
|
+
*
|
|
46
|
+
* Declared at module scope on purpose. Defining it inline inside MarkdownViews would hand
|
|
47
|
+
* react-markdown a brand-new element type on every render, so React would unmount and remount
|
|
48
|
+
* every code block whenever the markdown re-renders — making the copy button flicker (and losing
|
|
49
|
+
* its "已复制!" state) on each token of streamed content.
|
|
50
|
+
*/
|
|
51
|
+
const CodeBlock: React.FC<React.ComponentProps<"code"> & ExtraProps> = (
|
|
52
|
+
props,
|
|
53
|
+
) => {
|
|
54
|
+
// `node` is react-markdown's hast node — pulled out so it never reaches the DOM
|
|
55
|
+
const { className, children, node: _node, ...codeProps } = props;
|
|
56
|
+
const copyProps = useContext(CopyPropsContext);
|
|
57
|
+
// Stable for the lifetime of the block: regenerating the id on every render would leave the
|
|
58
|
+
// copy button's getElementById lookup pointing at a stale node. Strip ':' so the id stays
|
|
59
|
+
// usable with CSS selectors as well as getElementById.
|
|
60
|
+
const id = `md-code-${useId().replace(/:/g, "")}`;
|
|
61
|
+
|
|
62
|
+
const lang = /language-(\w+)/.exec(className || "")?.[1]?.toLowerCase();
|
|
63
|
+
if (!lang) {
|
|
64
|
+
return <code {...codeProps}>{children}</code>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const codeText = extractText(children);
|
|
68
|
+
|
|
69
|
+
// Mermaid diagrams: render directly as SVG (fall back to code display when streaming output is incomplete)
|
|
70
|
+
if (lang === "mermaid") {
|
|
71
|
+
return <MermaidBlock code={codeText} />;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const codeView = (
|
|
75
|
+
<div className={classNames(styles.code)}>
|
|
76
|
+
<div className={classNames(styles.nav)}>
|
|
77
|
+
<Copy id={id} md={false} {...copyProps} />
|
|
78
|
+
</div>
|
|
79
|
+
<div className={classNames(styles.content)}>
|
|
80
|
+
<code
|
|
81
|
+
id={id}
|
|
82
|
+
{...codeProps}
|
|
83
|
+
className={classNames(styles.code_content, className)}
|
|
84
|
+
>
|
|
85
|
+
{children}
|
|
86
|
+
</code>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
// HTML/SVG: Artifacts preview (sandbox iframe) + code tabs
|
|
92
|
+
if (ARTIFACT_LANGS.has(lang)) {
|
|
93
|
+
return <ArtifactBlock code={codeText} lang={lang} codeView={codeView} />;
|
|
94
|
+
}
|
|
95
|
+
return codeView;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/** Table cell that turns literal "<br>" text nodes into real line breaks. */
|
|
99
|
+
const TableCell: React.FC<React.ComponentProps<"td"> & ExtraProps> = (props) => {
|
|
100
|
+
// `node` is react-markdown's hast node — pulled out so it never reaches the DOM
|
|
101
|
+
const { children, node: _node, ...tdProps } = props;
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<td {...tdProps}>
|
|
105
|
+
{Array.isArray(children)
|
|
106
|
+
? children?.map((item, index) =>
|
|
107
|
+
item === "<br>" ? <br key={index} /> : item,
|
|
108
|
+
)
|
|
109
|
+
: children}
|
|
110
|
+
</td>
|
|
111
|
+
);
|
|
112
|
+
};
|
|
113
|
+
|
|
33
114
|
export interface MarkdownViewsProps extends ReactMarkdownProps {
|
|
34
115
|
copyProps?: Omit<CopyProps, "id">;
|
|
35
116
|
}
|
|
@@ -38,69 +119,21 @@ const MarkdownViews: React.FC<MarkdownViewsProps> = (props) => {
|
|
|
38
119
|
const { className, copyProps, components: extraComponents } = props;
|
|
39
120
|
|
|
40
121
|
return (
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const id = generateRandomStr(10);
|
|
59
|
-
const codeView = (
|
|
60
|
-
<div className={classNames(styles.code)}>
|
|
61
|
-
<div className={classNames(styles.nav)}>
|
|
62
|
-
<Copy id={id} md={false} {...copyProps} />
|
|
63
|
-
</div>
|
|
64
|
-
<div className={classNames(styles.content)}>
|
|
65
|
-
<code
|
|
66
|
-
id={id}
|
|
67
|
-
{...props}
|
|
68
|
-
className={classNames(styles.code_content, className)}
|
|
69
|
-
>
|
|
70
|
-
{children}
|
|
71
|
-
</code>
|
|
72
|
-
</div>
|
|
73
|
-
</div>
|
|
74
|
-
);
|
|
75
|
-
|
|
76
|
-
// HTML/SVG: Artifacts preview (sandbox iframe) + code tabs
|
|
77
|
-
if (ARTIFACT_LANGS.has(lang)) {
|
|
78
|
-
return (
|
|
79
|
-
<ArtifactBlock code={codeText} lang={lang} codeView={codeView} />
|
|
80
|
-
);
|
|
81
|
-
}
|
|
82
|
-
return codeView;
|
|
83
|
-
}
|
|
84
|
-
return <code {...props}>{children}</code>;
|
|
85
|
-
},
|
|
86
|
-
td: ({ children, ...props }) => {
|
|
87
|
-
return (
|
|
88
|
-
<td {...props}>
|
|
89
|
-
{Array.isArray(children)
|
|
90
|
-
? children?.map((item) => {
|
|
91
|
-
if (item === "<br>") {
|
|
92
|
-
return <br />;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
return item;
|
|
96
|
-
})
|
|
97
|
-
: children}
|
|
98
|
-
</td>
|
|
99
|
-
);
|
|
100
|
-
},
|
|
101
|
-
...(extraComponents || {}),
|
|
102
|
-
}}
|
|
103
|
-
/>
|
|
122
|
+
// The components map below may be a fresh object each render; that is harmless because the
|
|
123
|
+
// renderers it points at are stable module-level types, which is what React reconciles on.
|
|
124
|
+
<CopyPropsContext.Provider value={copyProps}>
|
|
125
|
+
<ReactMarkdown
|
|
126
|
+
{...props}
|
|
127
|
+
rehypePlugins={[rehypeHighlight, rehypeKatex]}
|
|
128
|
+
remarkPlugins={[remarkGfm, remarkMath]}
|
|
129
|
+
className={classNames(styles.MarkdownViews, className, "markdown-body")}
|
|
130
|
+
components={{
|
|
131
|
+
code: CodeBlock,
|
|
132
|
+
td: TableCell,
|
|
133
|
+
...(extraComponents || {}),
|
|
134
|
+
}}
|
|
135
|
+
/>
|
|
136
|
+
</CopyPropsContext.Provider>
|
|
104
137
|
);
|
|
105
138
|
};
|
|
106
139
|
|