wtfai 1.2.1 → 1.3.0
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 +69 -30
- package/dist/session.d.ts +8 -0
- package/dist/session.js +117 -89
- package/dist/types.d.ts +33 -14
- package/dist/ui/code.d.ts +2 -0
- package/dist/ui/code.js +16 -0
- package/dist/ui/markdown.d.ts +1 -1
- package/dist/ui/markdown.js +22 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -169,35 +169,63 @@ session.off('token', onToken)
|
|
|
169
169
|
发送消息执行工作流。
|
|
170
170
|
|
|
171
171
|
```typescript
|
|
172
|
-
//
|
|
172
|
+
// 基础用法:纯文本
|
|
173
173
|
await session.send({
|
|
174
|
-
|
|
174
|
+
parts: [
|
|
175
|
+
{ type: 'text', text: '你好' }
|
|
176
|
+
]
|
|
175
177
|
})
|
|
176
178
|
|
|
177
|
-
//
|
|
179
|
+
// 图片 + 文本(使用 File)
|
|
178
180
|
await session.send({
|
|
179
|
-
|
|
180
|
-
|
|
181
|
+
parts: [
|
|
182
|
+
{ type: 'text', text: '请分析下面的图片:' },
|
|
183
|
+
{ type: 'image', file: imageFile },
|
|
184
|
+
{ type: 'text', text: '说明这张图的内容' }
|
|
185
|
+
]
|
|
181
186
|
})
|
|
182
187
|
|
|
183
|
-
//
|
|
188
|
+
// 图片 + 文本(使用 URL)
|
|
184
189
|
await session.send({
|
|
185
|
-
|
|
186
|
-
|
|
190
|
+
parts: [
|
|
191
|
+
{ type: 'text', text: '请对比这两张图片:' },
|
|
192
|
+
{ type: 'image', url: 'https://example.com/img1.jpg' },
|
|
193
|
+
{ type: 'image', url: 'https://example.com/img2.jpg' }
|
|
194
|
+
]
|
|
187
195
|
})
|
|
188
196
|
|
|
189
|
-
//
|
|
197
|
+
// 完整示例:文本 + 图片 + 文档
|
|
190
198
|
await session.send({
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
199
|
+
parts: [
|
|
200
|
+
{ type: 'text', text: '请对比下面两张图片:' },
|
|
201
|
+
{ type: 'image', file: imageFile1 },
|
|
202
|
+
{ type: 'image', file: imageFile2 },
|
|
203
|
+
{ type: 'text', text: '然后参考这份报告:' },
|
|
204
|
+
{
|
|
205
|
+
type: 'document',
|
|
206
|
+
file: pdfFile,
|
|
207
|
+
filename: 'report.pdf'
|
|
208
|
+
},
|
|
209
|
+
{ type: 'text', text: '说明第一张图的问题在哪里' }
|
|
210
|
+
]
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
// 使用 blob URL
|
|
214
|
+
await session.send({
|
|
215
|
+
parts: [
|
|
216
|
+
{ type: 'text', text: '分析这张图:' },
|
|
217
|
+
{ type: 'image', url: blobUrl }, // 会自动上传
|
|
218
|
+
]
|
|
198
219
|
})
|
|
199
220
|
```
|
|
200
221
|
|
|
222
|
+
**说明**:
|
|
223
|
+
- `parts` 数组中的元素会按顺序传递给 LLM,使大模型能准确理解位置指代(如"下面的图片"、"第一张图")
|
|
224
|
+
- `image` 和 `document` 类型必须提供 `file` 或 `url` 之一
|
|
225
|
+
- 图片 File 会自动压缩到 1920px 以内,质量 0.8
|
|
226
|
+
- blob URL 会自动上传到服务器
|
|
227
|
+
|
|
228
|
+
|
|
201
229
|
##### `restore()`
|
|
202
230
|
|
|
203
231
|
恢复历史会话(需要在创建 session 时传入 threadId)。
|
|
@@ -302,16 +330,24 @@ interface SessionState {
|
|
|
302
330
|
|
|
303
331
|
```typescript
|
|
304
332
|
interface SendInput {
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
imageUrls?: string[]
|
|
308
|
-
documents?: File[]
|
|
309
|
-
documentInfos?: Array<{
|
|
310
|
-
url: string
|
|
311
|
-
filename: string
|
|
312
|
-
mimeType: string
|
|
313
|
-
}>
|
|
333
|
+
/** 内容部件数组,按顺序传递给 LLM */
|
|
334
|
+
parts: ContentPart[]
|
|
314
335
|
}
|
|
336
|
+
|
|
337
|
+
type ContentPart =
|
|
338
|
+
| { type: 'text'; text: string }
|
|
339
|
+
| {
|
|
340
|
+
type: 'image'
|
|
341
|
+
file?: File // 图片文件(会自动压缩并上传)
|
|
342
|
+
url?: string // 图片 URL(支持 blob URL 和普通 URL)
|
|
343
|
+
}
|
|
344
|
+
| {
|
|
345
|
+
type: 'document'
|
|
346
|
+
file?: File // 文档文件(会自动上传)
|
|
347
|
+
url?: string // 文档 URL(支持 blob URL 和普通 URL)
|
|
348
|
+
filename: string // 文件名
|
|
349
|
+
mimeType?: string // MIME 类型
|
|
350
|
+
}
|
|
315
351
|
```
|
|
316
352
|
|
|
317
353
|
---
|
|
@@ -356,7 +392,9 @@ function ChatComponent({ workflowId }: { workflowId: string }) {
|
|
|
356
392
|
})
|
|
357
393
|
|
|
358
394
|
try {
|
|
359
|
-
await session.send({
|
|
395
|
+
await session.send({
|
|
396
|
+
parts: [{ type: 'text', text: input }]
|
|
397
|
+
})
|
|
360
398
|
setInput('')
|
|
361
399
|
} catch (error) {
|
|
362
400
|
console.error(error)
|
|
@@ -390,7 +428,8 @@ function ChatComponent({ workflowId }: { workflowId: string }) {
|
|
|
390
428
|
|
|
391
429
|
## 注意事项
|
|
392
430
|
|
|
393
|
-
1.
|
|
394
|
-
2.
|
|
395
|
-
3.
|
|
396
|
-
4.
|
|
431
|
+
1. **顺序保持**:`parts` 数组中的元素会按顺序传递给 LLM,确保大模型能理解位置指代
|
|
432
|
+
2. **图片自动压缩**:通过 File 上传的图片会自动压缩到 1920px 以内,质量 0.8
|
|
433
|
+
3. **会话管理**:每次调用 `send()` 都会更新 `threadId`,如需保存会话请在 `start` 事件中获取
|
|
434
|
+
4. **错误处理**:建议始终监听 `error` 事件处理异常情况
|
|
435
|
+
5. **资源清理**:组件卸载时调用 `session.abort()` 避免内存泄漏
|
package/dist/session.d.ts
CHANGED
|
@@ -41,6 +41,14 @@ export declare class WorkflowSession {
|
|
|
41
41
|
* 发送消息执行工作流
|
|
42
42
|
*/
|
|
43
43
|
send(input: SendInput): Promise<void>;
|
|
44
|
+
/**
|
|
45
|
+
* 解析图片 URL(支持 File 和 URL)
|
|
46
|
+
*/
|
|
47
|
+
private resolveImageUrl;
|
|
48
|
+
/**
|
|
49
|
+
* 解析文档信息(支持 File 和 URL)
|
|
50
|
+
*/
|
|
51
|
+
private resolveDocumentInfo;
|
|
44
52
|
/**
|
|
45
53
|
* 中止执行
|
|
46
54
|
*/
|
package/dist/session.js
CHANGED
|
@@ -60,89 +60,33 @@ class WorkflowSession {
|
|
|
60
60
|
});
|
|
61
61
|
try {
|
|
62
62
|
const contentParts = [];
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const imageUrls = await Promise.all(input.images.map((file)=>this.uploadService.uploadImage({
|
|
69
|
-
file,
|
|
70
|
-
resourceType: 'conversation'
|
|
71
|
-
})));
|
|
72
|
-
for (const url of imageUrls)contentParts.push({
|
|
73
|
-
type: 'image',
|
|
74
|
-
url
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
if (input.imageUrls && input.imageUrls.length > 0) {
|
|
78
|
-
const resolvedUrls = await Promise.all(input.imageUrls.map(async (url)=>{
|
|
79
|
-
if (url.startsWith('blob:')) {
|
|
80
|
-
const response = await fetch(url);
|
|
81
|
-
const blob = await response.blob();
|
|
82
|
-
const filename = `image_${Date.now()}.${blob.type.split('/')[1] || 'png'}`;
|
|
83
|
-
const file = new File([
|
|
84
|
-
blob
|
|
85
|
-
], filename, {
|
|
86
|
-
type: blob.type
|
|
87
|
-
});
|
|
88
|
-
return this.uploadService.uploadImage({
|
|
89
|
-
file,
|
|
90
|
-
resourceType: 'conversation'
|
|
91
|
-
});
|
|
92
|
-
}
|
|
93
|
-
return url;
|
|
94
|
-
}));
|
|
95
|
-
for (const url of resolvedUrls)contentParts.push({
|
|
96
|
-
type: 'image',
|
|
97
|
-
url
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
if (input.documents && input.documents.length > 0) {
|
|
101
|
-
const docInfos = await Promise.all(input.documents.map(async (file)=>{
|
|
102
|
-
const url = await this.uploadService.uploadFile({
|
|
103
|
-
file,
|
|
104
|
-
resourceType: 'conversation'
|
|
63
|
+
for (const part of input.parts)switch(part.type){
|
|
64
|
+
case 'text':
|
|
65
|
+
contentParts.push({
|
|
66
|
+
type: 'text',
|
|
67
|
+
text: part.text
|
|
105
68
|
});
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
type: 'document',
|
|
114
|
-
url: doc.url,
|
|
115
|
-
filename: doc.filename,
|
|
116
|
-
mimeType: doc.mimeType
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
if (input.documentInfos && input.documentInfos.length > 0) {
|
|
120
|
-
const resolvedDocs = await Promise.all(input.documentInfos.map(async (doc)=>{
|
|
121
|
-
if (doc.url.startsWith('blob:')) {
|
|
122
|
-
const response = await fetch(doc.url);
|
|
123
|
-
const blob = await response.blob();
|
|
124
|
-
const file = new File([
|
|
125
|
-
blob
|
|
126
|
-
], doc.filename, {
|
|
127
|
-
type: doc.mimeType || blob.type
|
|
69
|
+
break;
|
|
70
|
+
case 'image':
|
|
71
|
+
{
|
|
72
|
+
const imageUrl = await this.resolveImageUrl(part);
|
|
73
|
+
contentParts.push({
|
|
74
|
+
type: 'image',
|
|
75
|
+
url: imageUrl
|
|
128
76
|
});
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
case 'document':
|
|
80
|
+
{
|
|
81
|
+
const docInfo = await this.resolveDocumentInfo(part);
|
|
82
|
+
contentParts.push({
|
|
83
|
+
type: 'document',
|
|
84
|
+
url: docInfo.url,
|
|
85
|
+
filename: docInfo.filename,
|
|
86
|
+
mimeType: docInfo.mimeType
|
|
132
87
|
});
|
|
133
|
-
|
|
134
|
-
...doc,
|
|
135
|
-
url: uploadedUrl
|
|
136
|
-
};
|
|
88
|
+
break;
|
|
137
89
|
}
|
|
138
|
-
return doc;
|
|
139
|
-
}));
|
|
140
|
-
for (const doc of resolvedDocs)contentParts.push({
|
|
141
|
-
type: 'document',
|
|
142
|
-
url: doc.url,
|
|
143
|
-
filename: doc.filename,
|
|
144
|
-
mimeType: doc.mimeType
|
|
145
|
-
});
|
|
146
90
|
}
|
|
147
91
|
const executionInput = {
|
|
148
92
|
messages: [
|
|
@@ -151,16 +95,31 @@ class WorkflowSession {
|
|
|
151
95
|
}
|
|
152
96
|
]
|
|
153
97
|
};
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
98
|
+
const parts = contentParts.map((part)=>{
|
|
99
|
+
switch(part.type){
|
|
100
|
+
case 'text':
|
|
101
|
+
return {
|
|
102
|
+
type: 'text',
|
|
103
|
+
text: part.text
|
|
104
|
+
};
|
|
105
|
+
case 'image':
|
|
106
|
+
return {
|
|
107
|
+
type: 'image',
|
|
108
|
+
url: part.url
|
|
109
|
+
};
|
|
110
|
+
case 'document':
|
|
111
|
+
return {
|
|
112
|
+
type: 'document',
|
|
113
|
+
filename: part.filename,
|
|
114
|
+
url: part.url
|
|
115
|
+
};
|
|
116
|
+
default:
|
|
117
|
+
throw new Error(`Unknown part type: ${part.type}`);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
159
120
|
const userMessage = {
|
|
160
121
|
type: 'human',
|
|
161
|
-
|
|
162
|
-
images: uploadedImages.length > 0 ? uploadedImages : void 0,
|
|
163
|
-
documents: uploadedDocs.length > 0 ? uploadedDocs : void 0
|
|
122
|
+
parts
|
|
164
123
|
};
|
|
165
124
|
this.updateState({
|
|
166
125
|
messages: [
|
|
@@ -213,7 +172,12 @@ class WorkflowSession {
|
|
|
213
172
|
if (!currentAiMessage) {
|
|
214
173
|
currentAiMessage = {
|
|
215
174
|
type: 'ai',
|
|
216
|
-
|
|
175
|
+
parts: [
|
|
176
|
+
{
|
|
177
|
+
type: 'text',
|
|
178
|
+
text: ''
|
|
179
|
+
}
|
|
180
|
+
]
|
|
217
181
|
};
|
|
218
182
|
const newMessages = [
|
|
219
183
|
...this.state.messages,
|
|
@@ -224,7 +188,8 @@ class WorkflowSession {
|
|
|
224
188
|
messages: newMessages
|
|
225
189
|
});
|
|
226
190
|
}
|
|
227
|
-
|
|
191
|
+
const textPart = currentAiMessage.parts.find((p)=>'text' === p.type);
|
|
192
|
+
if (textPart && 'text' === textPart.type) textPart.text += data.c;
|
|
228
193
|
const updatedMessages = [
|
|
229
194
|
...this.state.messages
|
|
230
195
|
];
|
|
@@ -277,6 +242,69 @@ class WorkflowSession {
|
|
|
277
242
|
throw error;
|
|
278
243
|
}
|
|
279
244
|
}
|
|
245
|
+
async resolveImageUrl(part) {
|
|
246
|
+
if (part.file) return this.uploadService.uploadImage({
|
|
247
|
+
file: part.file,
|
|
248
|
+
resourceType: 'conversation'
|
|
249
|
+
});
|
|
250
|
+
if (part.url) {
|
|
251
|
+
if (part.url.startsWith('blob:')) {
|
|
252
|
+
const response = await fetch(part.url);
|
|
253
|
+
const blob = await response.blob();
|
|
254
|
+
const filename = `image_${Date.now()}.${blob.type.split('/')[1] || 'png'}`;
|
|
255
|
+
const file = new File([
|
|
256
|
+
blob
|
|
257
|
+
], filename, {
|
|
258
|
+
type: blob.type
|
|
259
|
+
});
|
|
260
|
+
return this.uploadService.uploadImage({
|
|
261
|
+
file,
|
|
262
|
+
resourceType: 'conversation'
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
return part.url;
|
|
266
|
+
}
|
|
267
|
+
throw new Error('Image part must have either file or url');
|
|
268
|
+
}
|
|
269
|
+
async resolveDocumentInfo(part) {
|
|
270
|
+
if (part.file) {
|
|
271
|
+
const url = await this.uploadService.uploadFile({
|
|
272
|
+
file: part.file,
|
|
273
|
+
resourceType: 'conversation'
|
|
274
|
+
});
|
|
275
|
+
return {
|
|
276
|
+
url,
|
|
277
|
+
filename: part.filename,
|
|
278
|
+
mimeType: part.mimeType || part.file.type
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
if (part.url) {
|
|
282
|
+
if (part.url.startsWith('blob:')) {
|
|
283
|
+
const response = await fetch(part.url);
|
|
284
|
+
const blob = await response.blob();
|
|
285
|
+
const file = new File([
|
|
286
|
+
blob
|
|
287
|
+
], part.filename, {
|
|
288
|
+
type: part.mimeType || blob.type
|
|
289
|
+
});
|
|
290
|
+
const url = await this.uploadService.uploadFile({
|
|
291
|
+
file,
|
|
292
|
+
resourceType: 'conversation'
|
|
293
|
+
});
|
|
294
|
+
return {
|
|
295
|
+
url,
|
|
296
|
+
filename: part.filename,
|
|
297
|
+
mimeType: part.mimeType || blob.type
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
return {
|
|
301
|
+
url: part.url,
|
|
302
|
+
filename: part.filename,
|
|
303
|
+
mimeType: part.mimeType || 'application/octet-stream'
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
throw new Error('Document part must have either file or url');
|
|
307
|
+
}
|
|
280
308
|
abort() {
|
|
281
309
|
if (this.abortController) {
|
|
282
310
|
this.abortController.abort();
|
package/dist/types.d.ts
CHANGED
|
@@ -49,24 +49,43 @@ export interface CompressOptions {
|
|
|
49
49
|
/** 超过此大小(字节)的图片会被转换为 JPEG,默认 1MB */
|
|
50
50
|
convertSize?: number;
|
|
51
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* 内容部件类型
|
|
54
|
+
*/
|
|
55
|
+
export type ContentPart = {
|
|
56
|
+
type: 'text';
|
|
57
|
+
text: string;
|
|
58
|
+
} | {
|
|
59
|
+
type: 'image';
|
|
60
|
+
/** 图片文件(会自动压缩并上传) */
|
|
61
|
+
file: File;
|
|
62
|
+
} | {
|
|
63
|
+
type: 'image';
|
|
64
|
+
/** 图片 URL(支持 blob URL 和普通 URL) */
|
|
65
|
+
url: string;
|
|
66
|
+
} | {
|
|
67
|
+
type: 'document';
|
|
68
|
+
/** 文档 URL(支持 blob URL 和普通 URL) */
|
|
69
|
+
url: string;
|
|
70
|
+
/** 文件名 */
|
|
71
|
+
filename: string;
|
|
72
|
+
/** MIME 类型 */
|
|
73
|
+
mimeType: string;
|
|
74
|
+
} | {
|
|
75
|
+
type: 'document';
|
|
76
|
+
/** 文档文件(会自动上传) */
|
|
77
|
+
file: File;
|
|
78
|
+
/** 文件名 */
|
|
79
|
+
filename: string;
|
|
80
|
+
/** MIME 类型 */
|
|
81
|
+
mimeType: string;
|
|
82
|
+
};
|
|
52
83
|
/**
|
|
53
84
|
* 发送消息的输入
|
|
54
85
|
*/
|
|
55
86
|
export interface SendInput {
|
|
56
|
-
/**
|
|
57
|
-
|
|
58
|
-
/** 图片文件列表(会自动压缩并上传) */
|
|
59
|
-
images?: File[];
|
|
60
|
-
/** 已上传的图片 URL 列表 */
|
|
61
|
-
imageUrls?: string[];
|
|
62
|
-
/** 文档文件列表(会自动上传) */
|
|
63
|
-
documents?: File[];
|
|
64
|
-
/** 已上传的文档信息列表 */
|
|
65
|
-
documentInfos?: Array<{
|
|
66
|
-
url: string;
|
|
67
|
-
filename: string;
|
|
68
|
-
mimeType: string;
|
|
69
|
-
}>;
|
|
87
|
+
/** 内容部件数组,按顺序传递给 LLM */
|
|
88
|
+
parts: ContentPart[];
|
|
70
89
|
}
|
|
71
90
|
/**
|
|
72
91
|
* 上传临时凭证
|
package/dist/ui/code.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import { CodeHighlighter, Mermaid } from "@ant-design/x";
|
|
3
|
+
const Code = (props)=>{
|
|
4
|
+
var _className_match;
|
|
5
|
+
const { className, children } = props;
|
|
6
|
+
const lang = (null == className ? void 0 : null == (_className_match = className.match(/language-(\w+)/)) ? void 0 : _className_match[1]) || '';
|
|
7
|
+
if ('string' != typeof children) return null;
|
|
8
|
+
if ('mermaid' === lang) return /*#__PURE__*/ jsx(Mermaid, {
|
|
9
|
+
children: children
|
|
10
|
+
});
|
|
11
|
+
return /*#__PURE__*/ jsx(CodeHighlighter, {
|
|
12
|
+
lang: lang,
|
|
13
|
+
children: children
|
|
14
|
+
});
|
|
15
|
+
};
|
|
16
|
+
export { Code };
|
package/dist/ui/markdown.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import { XMarkdownProps } from '@ant-design/x-markdown';
|
|
2
2
|
import '@ant-design/x-markdown/themes/light.css';
|
|
3
|
-
declare const Markdown: ({ className, streaming, ...props }: XMarkdownProps) => import("react/jsx-runtime").JSX.Element;
|
|
3
|
+
declare const Markdown: ({ className, streaming, config, components, ...props }: XMarkdownProps) => import("react/jsx-runtime").JSX.Element;
|
|
4
4
|
export { Markdown };
|
package/dist/ui/markdown.js
CHANGED
|
@@ -1,13 +1,28 @@
|
|
|
1
1
|
import { jsx } from "react/jsx-runtime";
|
|
2
2
|
import { XMarkdown } from "@ant-design/x-markdown";
|
|
3
3
|
import "@ant-design/x-markdown/themes/light.css";
|
|
4
|
+
import Latex from "@ant-design/x-markdown/plugins/Latex";
|
|
5
|
+
import zh_CN from "@ant-design/x/locale/zh_CN";
|
|
4
6
|
import clsx from "clsx";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
import { Code } from "./code.js";
|
|
8
|
+
import { XProvider } from "@ant-design/x";
|
|
9
|
+
const Markdown = ({ className, streaming, config, components, ...props })=>/*#__PURE__*/ jsx(XProvider, {
|
|
10
|
+
locale: zh_CN,
|
|
11
|
+
children: /*#__PURE__*/ jsx(XMarkdown, {
|
|
12
|
+
className: clsx(className, 'x-markdown-light'),
|
|
13
|
+
streaming: {
|
|
14
|
+
...streaming,
|
|
15
|
+
enableAnimation: true
|
|
16
|
+
},
|
|
17
|
+
config: {
|
|
18
|
+
...config,
|
|
19
|
+
extensions: Latex()
|
|
20
|
+
},
|
|
21
|
+
components: {
|
|
22
|
+
...components,
|
|
23
|
+
code: Code
|
|
24
|
+
},
|
|
25
|
+
...props
|
|
26
|
+
})
|
|
12
27
|
});
|
|
13
28
|
export { Markdown };
|