@mujian/js-sdk 0.0.6-beta.1
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 +23 -0
- package/dist/events/index.d.ts +71 -0
- package/dist/index.d.ts +96 -0
- package/dist/index.js +220 -0
- package/dist/modules/ai/chat/chat.d.ts +35 -0
- package/dist/modules/ai/chat/index.d.ts +4 -0
- package/dist/modules/ai/chat/message/index.d.ts +43 -0
- package/dist/modules/ai/chat/project/index.d.ts +3 -0
- package/dist/modules/ai/chat/settings/index.d.ts +3 -0
- package/dist/modules/ai/chat/settings/model.d.ts +24 -0
- package/dist/modules/ai/chat/settings/persona.d.ts +17 -0
- package/dist/modules/ai/chat/settings/style.d.ts +0 -0
- package/dist/modules/ai/index.d.ts +5 -0
- package/dist/modules/ai/text/index.d.ts +2 -0
- package/dist/modules/game/index.d.ts +4 -0
- package/dist/modules/game/saves.d.ts +3 -0
- package/dist/modules/ui/index.d.ts +0 -0
- package/dist/react/chat/index.d.ts +1 -0
- package/dist/react/chat/useChat/error.d.ts +15 -0
- package/dist/react/chat/useChat/index.d.ts +55 -0
- package/dist/react/chat/useChat/inner/chatStreaming.d.ts +23 -0
- package/dist/react/chat/useChat/inner/stream.d.ts +3 -0
- package/dist/react/chat/useChat/message.d.ts +37 -0
- package/dist/react/components/MdRenderer/index.d.ts +20 -0
- package/dist/react/components/MdRenderer/utils.d.ts +38 -0
- package/dist/react/components/MujianProvider/index.d.ts +17 -0
- package/dist/react/components/Thread/MessageItem.d.ts +21 -0
- package/dist/react/components/Thread/MessageList.d.ts +19 -0
- package/dist/react/components/Thread/index.d.ts +4 -0
- package/dist/react/components/button.d.ts +14 -0
- package/dist/react/components/index.d.ts +4 -0
- package/dist/react/index.d.ts +2 -0
- package/dist/react.css +156 -0
- package/dist/react.js +832 -0
- package/dist/types/index.d.ts +27 -0
- package/dist/utils/index.d.ts +0 -0
- package/package.json +89 -0
package/dist/react.js
ADDED
|
@@ -0,0 +1,832 @@
|
|
|
1
|
+
import { useMount, useRequest, useUpdateEffect } from "ahooks";
|
|
2
|
+
import react, { createContext, forwardRef, useCallback, useContext, useEffect, useState } from "react";
|
|
3
|
+
import { jsx } from "react/jsx-runtime";
|
|
4
|
+
import css_tools from "@adobe/css-tools";
|
|
5
|
+
import dompurify from "dompurify";
|
|
6
|
+
import showdown from "showdown";
|
|
7
|
+
import postmate from "postmate";
|
|
8
|
+
import { Virtualizer } from "virtua";
|
|
9
|
+
addDOMPurifyHooks();
|
|
10
|
+
function canUseNegativeLookbehind() {
|
|
11
|
+
try {
|
|
12
|
+
new RegExp('(?<!_)');
|
|
13
|
+
return true;
|
|
14
|
+
} catch (e) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function addDOMPurifyHooks() {
|
|
19
|
+
dompurify.addHook('afterSanitizeAttributes', function(node) {
|
|
20
|
+
if ('target' in node) {
|
|
21
|
+
node.setAttribute('target', '_blank');
|
|
22
|
+
node.setAttribute('rel', 'noopener');
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
dompurify.addHook('uponSanitizeAttribute', (node, data, config)=>{
|
|
26
|
+
if (!config['MESSAGE_SANITIZE']) return;
|
|
27
|
+
const permittedNodeTypes = [
|
|
28
|
+
'BUTTON',
|
|
29
|
+
'DIV'
|
|
30
|
+
];
|
|
31
|
+
if (config['MESSAGE_ALLOW_SYSTEM_UI'] && node.classList.contains('menu_button') && permittedNodeTypes.includes(node.nodeName)) return;
|
|
32
|
+
switch(data.attrName){
|
|
33
|
+
case 'class':
|
|
34
|
+
if (data.attrValue) data.attrValue = data.attrValue.split(' ').map((v)=>{
|
|
35
|
+
if (v.startsWith('fa-') || v.startsWith('note-') || 'monospace' === v) return v;
|
|
36
|
+
return 'custom-' + v;
|
|
37
|
+
}).join(' ');
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
dompurify.addHook('uponSanitizeElement', (node, _, config)=>{
|
|
42
|
+
if (!config['MESSAGE_SANITIZE']) return;
|
|
43
|
+
if (node instanceof HTMLUnknownElement) node.innerHTML = node.innerHTML.trim().replaceAll('\n', '<br>');
|
|
44
|
+
const isMediaAllowed = true;
|
|
45
|
+
if (isMediaAllowed) return;
|
|
46
|
+
if (!(node instanceof Element)) return;
|
|
47
|
+
let mediaBlocked = false;
|
|
48
|
+
switch(node.tagName){
|
|
49
|
+
case 'AUDIO':
|
|
50
|
+
case 'VIDEO':
|
|
51
|
+
case 'SOURCE':
|
|
52
|
+
case 'TRACK':
|
|
53
|
+
case 'EMBED':
|
|
54
|
+
case 'OBJECT':
|
|
55
|
+
case 'IMG':
|
|
56
|
+
{
|
|
57
|
+
const isExternalUrl = (url)=>(url.indexOf('://') > 0 || 0 === url.indexOf('//')) && !url.startsWith(window.location.origin);
|
|
58
|
+
const src = node.getAttribute('src');
|
|
59
|
+
const data = node.getAttribute('data');
|
|
60
|
+
const srcset = node.getAttribute('srcset');
|
|
61
|
+
if (srcset) {
|
|
62
|
+
const srcsetUrls = srcset.split(',');
|
|
63
|
+
for (const srcsetUrl of srcsetUrls){
|
|
64
|
+
const [url] = srcsetUrl.trim().split(' ');
|
|
65
|
+
if (isExternalUrl(url)) {
|
|
66
|
+
console.warn('External media blocked', url);
|
|
67
|
+
node.remove();
|
|
68
|
+
mediaBlocked = true;
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (src && isExternalUrl(src)) {
|
|
74
|
+
console.warn('External media blocked', src);
|
|
75
|
+
mediaBlocked = true;
|
|
76
|
+
node.remove();
|
|
77
|
+
}
|
|
78
|
+
if (data && isExternalUrl(data)) {
|
|
79
|
+
console.warn('External media blocked', data);
|
|
80
|
+
mediaBlocked = true;
|
|
81
|
+
node.remove();
|
|
82
|
+
}
|
|
83
|
+
if (mediaBlocked && node instanceof HTMLMediaElement) {
|
|
84
|
+
node.autoplay = false;
|
|
85
|
+
node.pause();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Replaces style tags in the message text with custom tags with encoded content.
|
|
94
|
+
* @param {string} text
|
|
95
|
+
* @returns {string} Encoded message text
|
|
96
|
+
* @copyright https://github.com/kwaroran/risuAI
|
|
97
|
+
*/ function encodeStyleTags(text) {
|
|
98
|
+
const styleRegex = /<style>(.+?)<\/style>/gi;
|
|
99
|
+
return text.replaceAll(styleRegex, (_, match)=>`<custom-style>${encodeURIComponent(match)}</custom-style>`);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Sanitizes custom style tags in the message text to prevent DOM pollution.
|
|
103
|
+
* @param {string} text Message text
|
|
104
|
+
* @param {object} options Options object
|
|
105
|
+
* @param {string} options.prefix Prefix the selectors with this value
|
|
106
|
+
* @returns {string} Sanitized message text
|
|
107
|
+
* @copyright https://github.com/kwaroran/risuAI
|
|
108
|
+
*/ function decodeStyleTags(text, { prefix } = {
|
|
109
|
+
prefix: '.mes_text '
|
|
110
|
+
}) {
|
|
111
|
+
const styleDecodeRegex = /<custom-style>(.+?)<\/custom-style>/g;
|
|
112
|
+
function sanitizeRule(rule) {
|
|
113
|
+
if (Array.isArray(rule.selectors)) for(let i = 0; i < rule.selectors.length; i++){
|
|
114
|
+
const selector = rule.selectors[i];
|
|
115
|
+
if (selector) rule.selectors[i] = prefix + sanitizeSelector(selector);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function sanitizeSelector(selector) {
|
|
119
|
+
const pseudoClasses = [
|
|
120
|
+
'has',
|
|
121
|
+
'not',
|
|
122
|
+
'where',
|
|
123
|
+
'is',
|
|
124
|
+
'matches',
|
|
125
|
+
'any'
|
|
126
|
+
];
|
|
127
|
+
const pseudoRegex = new RegExp(`:(${pseudoClasses.join('|')})\\(([^)]+)\\)`, 'g');
|
|
128
|
+
selector = selector.replace(pseudoRegex, (_match, pseudoClass, content)=>{
|
|
129
|
+
const sanitizedContent = sanitizeSimpleSelector(content);
|
|
130
|
+
return `:${pseudoClass}(${sanitizedContent})`;
|
|
131
|
+
});
|
|
132
|
+
return sanitizeSimpleSelector(selector);
|
|
133
|
+
}
|
|
134
|
+
function sanitizeSimpleSelector(selector) {
|
|
135
|
+
return selector.split(/\s+/).map((part)=>part.replace(/\.([\w-]+)/g, (match, className)=>{
|
|
136
|
+
if (className.startsWith('custom-')) return match;
|
|
137
|
+
return `.custom-${className}`;
|
|
138
|
+
})).join(' ');
|
|
139
|
+
}
|
|
140
|
+
function sanitizeRuleSet(ruleSet) {
|
|
141
|
+
if (Array.isArray(ruleSet.selectors) || Array.isArray(ruleSet.declarations)) sanitizeRule(ruleSet);
|
|
142
|
+
if (Array.isArray(ruleSet.rules)) {
|
|
143
|
+
ruleSet.rules = ruleSet.rules.filter((rule)=>'import' !== rule.type);
|
|
144
|
+
for (const mediaRule of ruleSet.rules)sanitizeRuleSet(mediaRule);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return text.replaceAll(styleDecodeRegex, (_, style)=>{
|
|
148
|
+
try {
|
|
149
|
+
const styleCleaned = decodeURIComponent(style).replaceAll(/<br\/>/g, '');
|
|
150
|
+
const ast = css_tools.parse(styleCleaned);
|
|
151
|
+
const sheet = ast?.stylesheet;
|
|
152
|
+
if (sheet) sanitizeRuleSet(ast.stylesheet);
|
|
153
|
+
return `<style>${css_tools.stringify(ast)}</style>`;
|
|
154
|
+
} catch (error) {
|
|
155
|
+
return `CSS ERROR: ${error}`;
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
const markdownUnderscoreExt = ()=>{
|
|
160
|
+
try {
|
|
161
|
+
if (!canUseNegativeLookbehind()) {
|
|
162
|
+
console.log('Showdown-underscore extension: Negative lookbehind not supported. Skipping.');
|
|
163
|
+
return [];
|
|
164
|
+
}
|
|
165
|
+
return [
|
|
166
|
+
{
|
|
167
|
+
type: 'output',
|
|
168
|
+
regex: new RegExp('(<code(?:\\s+[^>]*)?>[\\s\\S]*?<\\/code>|<style(?:\\s+[^>]*)?>[\\s\\S]*?<\\/style>)|\\b(?<!_)_(?!_)(.*?)(?<!_)_(?!_)\\b', 'gi'),
|
|
169
|
+
replace: function(match, tagContent, italicContent) {
|
|
170
|
+
if (tagContent) ;
|
|
171
|
+
else if (italicContent) return '<em>' + italicContent + '</em>';
|
|
172
|
+
return match;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
];
|
|
176
|
+
} catch (e) {
|
|
177
|
+
console.error('Error in Showdown-underscore extension:', e);
|
|
178
|
+
return [];
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
const converter = new showdown.Converter({
|
|
182
|
+
emoji: true,
|
|
183
|
+
literalMidWordUnderscores: true,
|
|
184
|
+
parseImgDimensions: true,
|
|
185
|
+
tables: true,
|
|
186
|
+
underline: true,
|
|
187
|
+
simpleLineBreaks: true,
|
|
188
|
+
strikethrough: true,
|
|
189
|
+
disableForced4SpacesIndentedSublists: true,
|
|
190
|
+
extensions: [
|
|
191
|
+
markdownUnderscoreExt()
|
|
192
|
+
]
|
|
193
|
+
});
|
|
194
|
+
function messageFormatting(mes, sanitizerOverrides = {}) {
|
|
195
|
+
if (!mes) return '';
|
|
196
|
+
mes = mes.replace(/<([^>]+)>/g, function(_, contents) {
|
|
197
|
+
return '<' + contents.replace(/"/g, '\ufffe') + '>';
|
|
198
|
+
});
|
|
199
|
+
mes = mes.replace(/<style>[\s\S]*?<\/style>|```[\s\S]*?```|~~~[\s\S]*?~~~|``[\s\S]*?``|`[\s\S]*?`|(".*?")|(\u201C.*?\u201D)|(\u00AB.*?\u00BB)|(\u300C.*?\u300D)|(\u300E.*?\u300F)|(\uFF02.*?\uFF02)/gim, function(match, p1, p2, p3, p4, p5, p6) {
|
|
200
|
+
if (p1) return `<q>"${p1.slice(1, -1)}"</q>`;
|
|
201
|
+
if (p2) return `<q>“${p2.slice(1, -1)}”</q>`;
|
|
202
|
+
if (p3) return `<q>«${p3.slice(1, -1)}»</q>`;
|
|
203
|
+
if (p4) return `<q>「${p4.slice(1, -1)}」</q>`;
|
|
204
|
+
if (p5) return `<q>『${p5.slice(1, -1)}』</q>`;
|
|
205
|
+
else if (p6) return `<q>"${p6.slice(1, -1)}"</q>`;
|
|
206
|
+
else return match;
|
|
207
|
+
});
|
|
208
|
+
mes = mes.replace(/\ufffe/g, '"');
|
|
209
|
+
mes = mes.replaceAll('\\begin{align*}', '$$');
|
|
210
|
+
mes = mes.replaceAll('\\end{align*}', '$$');
|
|
211
|
+
mes = converter.makeHtml(mes);
|
|
212
|
+
mes = mes.replace(/<code(.*)>[\s\S]*?<\/code>/g, function(match) {
|
|
213
|
+
return match.replace(/\n/gm, '\u0000');
|
|
214
|
+
});
|
|
215
|
+
mes = mes.replace(/\u0000/g, '\n');
|
|
216
|
+
mes = mes.trim();
|
|
217
|
+
mes = mes.replace(/<code(.*)>[\s\S]*?<\/code>/g, function(match) {
|
|
218
|
+
return match.replace(/&/g, '&');
|
|
219
|
+
});
|
|
220
|
+
const config = {
|
|
221
|
+
RETURN_DOM: false,
|
|
222
|
+
RETURN_DOM_FRAGMENT: false,
|
|
223
|
+
RETURN_TRUSTED_TYPE: false,
|
|
224
|
+
MESSAGE_SANITIZE: true,
|
|
225
|
+
ADD_TAGS: [
|
|
226
|
+
'custom-style'
|
|
227
|
+
],
|
|
228
|
+
...sanitizerOverrides
|
|
229
|
+
};
|
|
230
|
+
mes = encodeStyleTags(mes);
|
|
231
|
+
mes = dompurify.sanitize(mes, config);
|
|
232
|
+
mes = decodeStyleTags(mes, {
|
|
233
|
+
prefix: '.mes_text '
|
|
234
|
+
});
|
|
235
|
+
return mes;
|
|
236
|
+
}
|
|
237
|
+
const MdRendererBase = ({ content })=>{
|
|
238
|
+
const mes = messageFormatting(content);
|
|
239
|
+
return /*#__PURE__*/ jsx("div", {
|
|
240
|
+
className: "mes_text",
|
|
241
|
+
dangerouslySetInnerHTML: {
|
|
242
|
+
__html: mes
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
};
|
|
246
|
+
const MdRenderer = /*#__PURE__*/ react.memo(MdRendererBase, (prev, next)=>prev.content === next.content);
|
|
247
|
+
MdRendererBase.displayName = 'MdRenderer';
|
|
248
|
+
MdRenderer.displayName = 'MdRenderer';
|
|
249
|
+
const chat_complete = async function(message, onData, signal) {
|
|
250
|
+
return await this.call("mujian:ai:chat:complete", {
|
|
251
|
+
content: message
|
|
252
|
+
}, {
|
|
253
|
+
onData,
|
|
254
|
+
signal
|
|
255
|
+
});
|
|
256
|
+
};
|
|
257
|
+
const applyRegex = async function(props) {
|
|
258
|
+
return await this.call("mujian:ai:chat:applyRegex", props);
|
|
259
|
+
};
|
|
260
|
+
const continueComplete = async function(onData, signal) {
|
|
261
|
+
return await this.call("mujian:ai:chat:complete", {
|
|
262
|
+
isContinue: true
|
|
263
|
+
}, {
|
|
264
|
+
onData,
|
|
265
|
+
signal
|
|
266
|
+
});
|
|
267
|
+
};
|
|
268
|
+
const chat_regenerate = async function(onData, signal) {
|
|
269
|
+
return await this.call("mujian:ai:chat:complete", {
|
|
270
|
+
isRegenerate: true
|
|
271
|
+
}, {
|
|
272
|
+
onData,
|
|
273
|
+
signal
|
|
274
|
+
});
|
|
275
|
+
};
|
|
276
|
+
const getAll = async function() {
|
|
277
|
+
return await this.call("mujian:ai:chat:message:getAll");
|
|
278
|
+
};
|
|
279
|
+
const messageDeleteOne = async function(messageId) {
|
|
280
|
+
return await this.call("mujian:ai:chat:message:deleteOne", {
|
|
281
|
+
messageId
|
|
282
|
+
});
|
|
283
|
+
};
|
|
284
|
+
const messageEditOne = async function(messageId, content) {
|
|
285
|
+
return await this.call("mujian:ai:chat:message:editOne", {
|
|
286
|
+
messageId,
|
|
287
|
+
content
|
|
288
|
+
});
|
|
289
|
+
};
|
|
290
|
+
const messageSwipe = async function(messageId, swipeId) {
|
|
291
|
+
return await this.call("mujian:ai:chat:message:swipe", {
|
|
292
|
+
messageId,
|
|
293
|
+
swipeId
|
|
294
|
+
});
|
|
295
|
+
};
|
|
296
|
+
const getPrompt = async function(messageId) {
|
|
297
|
+
return await this.call("mujian:ai:chat:message:getPrompt", {
|
|
298
|
+
messageId
|
|
299
|
+
});
|
|
300
|
+
};
|
|
301
|
+
const getInfo = async function() {
|
|
302
|
+
return await this.call("mujian:ai:chat:project:getInfo");
|
|
303
|
+
};
|
|
304
|
+
const getActive = async function() {
|
|
305
|
+
return await this.call("mujian:ai:settings:persona:getActive");
|
|
306
|
+
};
|
|
307
|
+
const setActive = async function(personaId) {
|
|
308
|
+
return await this.call("mujian:ai:settings:persona:setActive", {
|
|
309
|
+
personaId
|
|
310
|
+
});
|
|
311
|
+
};
|
|
312
|
+
const model_getActive = async function() {
|
|
313
|
+
return await this.call("mujian:ai:settings:model:getActive");
|
|
314
|
+
};
|
|
315
|
+
const model_setActive = async function(modelId) {
|
|
316
|
+
return await this.call("mujian:ai:settings:model:setActive", {
|
|
317
|
+
modelId
|
|
318
|
+
});
|
|
319
|
+
};
|
|
320
|
+
const model_getAll = async function() {
|
|
321
|
+
return await this.call("mujian:ai:settings:model:getAll");
|
|
322
|
+
};
|
|
323
|
+
const generate = async function() {
|
|
324
|
+
return await this.call("mujian:ai:text:generate", {
|
|
325
|
+
content: 'q'
|
|
326
|
+
});
|
|
327
|
+
};
|
|
328
|
+
const saveGame = async function() {};
|
|
329
|
+
const loadGame = async function() {};
|
|
330
|
+
class MujianSdk {
|
|
331
|
+
constructor(){}
|
|
332
|
+
static getInstance() {
|
|
333
|
+
if (!window.$mujian) window.$mujian = new MujianSdk();
|
|
334
|
+
return window.$mujian;
|
|
335
|
+
}
|
|
336
|
+
parent = null;
|
|
337
|
+
ready = false;
|
|
338
|
+
get isReady() {
|
|
339
|
+
return this.ready;
|
|
340
|
+
}
|
|
341
|
+
pendingRequests = new Map();
|
|
342
|
+
async init() {
|
|
343
|
+
const handshake = new postmate.Model({
|
|
344
|
+
reply: ({ id, complete, data })=>{
|
|
345
|
+
const call = this.pendingRequests.get(id);
|
|
346
|
+
if (!call) return;
|
|
347
|
+
call.onData?.(data);
|
|
348
|
+
if (complete) {
|
|
349
|
+
call.onComplete?.();
|
|
350
|
+
call.resolve(data);
|
|
351
|
+
this.pendingRequests.delete(id);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
try {
|
|
356
|
+
const parent = await handshake;
|
|
357
|
+
this.ready = true;
|
|
358
|
+
this.parent = parent;
|
|
359
|
+
console.log('mujian sdk client init');
|
|
360
|
+
await this.call("mujian:init");
|
|
361
|
+
} catch (error) {
|
|
362
|
+
console.log('init error', error);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
emit(event, data) {
|
|
366
|
+
if (!this.ready) throw new Error('Mujian is not initialized');
|
|
367
|
+
this.parent?.emit(event, data);
|
|
368
|
+
}
|
|
369
|
+
nextCallId = 0;
|
|
370
|
+
getCallId() {
|
|
371
|
+
const id = this.nextCallId;
|
|
372
|
+
this.nextCallId += 1;
|
|
373
|
+
return id;
|
|
374
|
+
}
|
|
375
|
+
async call(event, data, controller) {
|
|
376
|
+
if (!this.ready) throw new Error('Mujian is not initialized');
|
|
377
|
+
const callId = this.getCallId();
|
|
378
|
+
return new Promise((resolve, reject)=>{
|
|
379
|
+
this.pendingRequests.set(callId, {
|
|
380
|
+
resolve,
|
|
381
|
+
reject,
|
|
382
|
+
...controller
|
|
383
|
+
});
|
|
384
|
+
this.emit(event, {
|
|
385
|
+
id: callId,
|
|
386
|
+
data
|
|
387
|
+
});
|
|
388
|
+
controller?.signal?.addEventListener('abort', ()=>{
|
|
389
|
+
this.emit("mujian:ai:chat:stop", {
|
|
390
|
+
id: callId
|
|
391
|
+
});
|
|
392
|
+
});
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
ai = {
|
|
396
|
+
chat: {
|
|
397
|
+
project: {
|
|
398
|
+
getInfo: getInfo.bind(this)
|
|
399
|
+
},
|
|
400
|
+
settings: {
|
|
401
|
+
model: {
|
|
402
|
+
getAll: model_getAll.bind(this),
|
|
403
|
+
getActive: model_getActive.bind(this),
|
|
404
|
+
setActive: model_setActive.bind(this)
|
|
405
|
+
},
|
|
406
|
+
persona: {
|
|
407
|
+
getActive: getActive.bind(this),
|
|
408
|
+
setActive: setActive.bind(this)
|
|
409
|
+
}
|
|
410
|
+
},
|
|
411
|
+
complete: chat_complete.bind(this),
|
|
412
|
+
applyRegex: applyRegex.bind(this),
|
|
413
|
+
continue: continueComplete.bind(this),
|
|
414
|
+
regenerate: chat_regenerate.bind(this),
|
|
415
|
+
message: {
|
|
416
|
+
getAll: getAll.bind(this),
|
|
417
|
+
deleteOne: messageDeleteOne.bind(this),
|
|
418
|
+
editOne: messageEditOne.bind(this),
|
|
419
|
+
swipe: messageSwipe.bind(this),
|
|
420
|
+
getPrompt: getPrompt.bind(this)
|
|
421
|
+
}
|
|
422
|
+
},
|
|
423
|
+
text: {
|
|
424
|
+
complete: generate.bind(this)
|
|
425
|
+
}
|
|
426
|
+
};
|
|
427
|
+
ui = {};
|
|
428
|
+
util = {
|
|
429
|
+
cn: ()=>{
|
|
430
|
+
console.log('cn');
|
|
431
|
+
}
|
|
432
|
+
};
|
|
433
|
+
hybrid = {};
|
|
434
|
+
player = {};
|
|
435
|
+
game = {
|
|
436
|
+
assets: ()=>{
|
|
437
|
+
console.log('assets');
|
|
438
|
+
},
|
|
439
|
+
saves: {
|
|
440
|
+
saveGame: saveGame.bind(this),
|
|
441
|
+
loadGame: loadGame.bind(this)
|
|
442
|
+
},
|
|
443
|
+
ranking: ()=>{
|
|
444
|
+
console.log('ranking');
|
|
445
|
+
}
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
const MujianContext = /*#__PURE__*/ createContext(null);
|
|
449
|
+
const MujianProvider = ({ children, loadingComponent })=>{
|
|
450
|
+
const [mujian, setMujian] = useState(null);
|
|
451
|
+
useEffect(()=>{
|
|
452
|
+
(async ()=>{
|
|
453
|
+
const mujian = MujianSdk.getInstance();
|
|
454
|
+
await mujian.init();
|
|
455
|
+
setMujian(mujian);
|
|
456
|
+
})();
|
|
457
|
+
}, []);
|
|
458
|
+
return /*#__PURE__*/ jsx(MujianContext.Provider, {
|
|
459
|
+
value: mujian,
|
|
460
|
+
children: mujian ? children : loadingComponent ?? 'Mujian is loading'
|
|
461
|
+
});
|
|
462
|
+
};
|
|
463
|
+
const useMujian = ()=>{
|
|
464
|
+
const mujian = useContext(MujianContext);
|
|
465
|
+
if (!mujian) {
|
|
466
|
+
const message = 'Mujian is not initialized. Please check that useMujian is called inside MujianProvider';
|
|
467
|
+
console.error(message);
|
|
468
|
+
throw new Error(message);
|
|
469
|
+
}
|
|
470
|
+
return mujian;
|
|
471
|
+
};
|
|
472
|
+
const MessageItem = (props)=>{
|
|
473
|
+
const { message, children, depth, index } = props;
|
|
474
|
+
const mujian = useMujian();
|
|
475
|
+
const { data: renderedMessage } = useRequest(async ()=>{
|
|
476
|
+
const { isStreaming } = message;
|
|
477
|
+
if (isStreaming) return message;
|
|
478
|
+
return await mujian.ai.chat.applyRegex({
|
|
479
|
+
message,
|
|
480
|
+
depth,
|
|
481
|
+
index
|
|
482
|
+
});
|
|
483
|
+
}, {
|
|
484
|
+
refreshDeps: [
|
|
485
|
+
message
|
|
486
|
+
]
|
|
487
|
+
});
|
|
488
|
+
return children(renderedMessage || message, message);
|
|
489
|
+
};
|
|
490
|
+
const MessageList = /*#__PURE__*/ forwardRef((props, ref)=>{
|
|
491
|
+
const { children, messages, vListProps } = props;
|
|
492
|
+
return /*#__PURE__*/ jsx(Virtualizer, {
|
|
493
|
+
...vListProps,
|
|
494
|
+
data: messages,
|
|
495
|
+
ref: ref,
|
|
496
|
+
children: (msg, i)=>children({
|
|
497
|
+
message: msg,
|
|
498
|
+
index: i,
|
|
499
|
+
depth: messages.length - 1 - i
|
|
500
|
+
})
|
|
501
|
+
});
|
|
502
|
+
});
|
|
503
|
+
const Thread = {
|
|
504
|
+
MessageItem: MessageItem,
|
|
505
|
+
MessageList: MessageList
|
|
506
|
+
};
|
|
507
|
+
class SendMessageError extends Error {
|
|
508
|
+
constructor(message, cause){
|
|
509
|
+
super(message);
|
|
510
|
+
this.name = this.constructor.name;
|
|
511
|
+
this.cause = cause;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
class InvalidDeltaContentError extends Error {
|
|
515
|
+
constructor(message, cause){
|
|
516
|
+
super(message);
|
|
517
|
+
this.name = this.constructor.name;
|
|
518
|
+
this.cause = cause;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
class UnexpectedSseEndingError extends Error {
|
|
522
|
+
constructor(message, cause){
|
|
523
|
+
super(message);
|
|
524
|
+
this.name = this.constructor.name;
|
|
525
|
+
this.cause = cause;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
class InsufficientBalanceError extends Error {
|
|
529
|
+
constructor(message, cause){
|
|
530
|
+
super(message);
|
|
531
|
+
this.name = this.constructor.name;
|
|
532
|
+
this.cause = cause;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
async function* callSdk(mujian, content, type = 'generate', signal) {
|
|
536
|
+
const queue = [];
|
|
537
|
+
let resolveWait = null;
|
|
538
|
+
let done = false;
|
|
539
|
+
let error = null;
|
|
540
|
+
try {
|
|
541
|
+
const onData = (data)=>{
|
|
542
|
+
if (error) return;
|
|
543
|
+
queue.push(data);
|
|
544
|
+
if (resolveWait) {
|
|
545
|
+
resolveWait();
|
|
546
|
+
resolveWait = null;
|
|
547
|
+
}
|
|
548
|
+
};
|
|
549
|
+
const then = ()=>{
|
|
550
|
+
done = true;
|
|
551
|
+
if (resolveWait) {
|
|
552
|
+
resolveWait();
|
|
553
|
+
resolveWait = null;
|
|
554
|
+
}
|
|
555
|
+
};
|
|
556
|
+
const catcher = (e)=>{
|
|
557
|
+
error = e;
|
|
558
|
+
if (resolveWait) {
|
|
559
|
+
resolveWait();
|
|
560
|
+
resolveWait = null;
|
|
561
|
+
}
|
|
562
|
+
};
|
|
563
|
+
if ('generate' === type) mujian.ai.chat.complete(content, onData, signal).then(then).catch(catcher);
|
|
564
|
+
else if ('regenerate' === type) mujian.ai.chat.regenerate(onData, signal).then(then).catch(catcher);
|
|
565
|
+
else mujian.ai.chat.continue(onData, signal).then(then).catch(catcher);
|
|
566
|
+
} catch (err) {
|
|
567
|
+
error = err;
|
|
568
|
+
done = true;
|
|
569
|
+
}
|
|
570
|
+
while(true)if (queue.length > 0) yield queue.shift();
|
|
571
|
+
else if (done) break;
|
|
572
|
+
else if (error) throw error;
|
|
573
|
+
else await new Promise((resolve)=>{
|
|
574
|
+
resolveWait = resolve;
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
const useChatStreaming = ({ common, setError, setMessages, mujian })=>{
|
|
578
|
+
const { body } = common;
|
|
579
|
+
const [isStreaming, setIsStreaming] = useState(false);
|
|
580
|
+
const chatStreaming = useCallback(async ({ query, regenerate, is_continue, signal })=>{
|
|
581
|
+
try {
|
|
582
|
+
setError(void 0);
|
|
583
|
+
setIsStreaming(true);
|
|
584
|
+
const userMessage = {
|
|
585
|
+
id: Date.now().toString(),
|
|
586
|
+
content: query || '',
|
|
587
|
+
role: 'user',
|
|
588
|
+
swipes: [],
|
|
589
|
+
activeSwipeId: 0,
|
|
590
|
+
isStreaming: false,
|
|
591
|
+
sendAt: new Date()
|
|
592
|
+
};
|
|
593
|
+
const aiMessage = {
|
|
594
|
+
id: Date.now().toString() + '1',
|
|
595
|
+
content: '',
|
|
596
|
+
role: 'assistant',
|
|
597
|
+
swipes: [],
|
|
598
|
+
activeSwipeId: 0,
|
|
599
|
+
isStreaming: true,
|
|
600
|
+
sendAt: new Date()
|
|
601
|
+
};
|
|
602
|
+
if (regenerate) setMessages((prev)=>{
|
|
603
|
+
const newMessages = [
|
|
604
|
+
...prev
|
|
605
|
+
];
|
|
606
|
+
const lastMessage = newMessages[newMessages.length - 1];
|
|
607
|
+
newMessages[newMessages.length - 1] = {
|
|
608
|
+
...lastMessage,
|
|
609
|
+
activeSwipeId: lastMessage.swipes.length,
|
|
610
|
+
swipes: [
|
|
611
|
+
...lastMessage.swipes,
|
|
612
|
+
''
|
|
613
|
+
],
|
|
614
|
+
content: '',
|
|
615
|
+
isStreaming: true
|
|
616
|
+
};
|
|
617
|
+
return newMessages;
|
|
618
|
+
});
|
|
619
|
+
else is_continue ? setMessages((prev)=>[
|
|
620
|
+
...prev,
|
|
621
|
+
aiMessage
|
|
622
|
+
]) : setMessages((prev)=>[
|
|
623
|
+
...prev,
|
|
624
|
+
userMessage,
|
|
625
|
+
aiMessage
|
|
626
|
+
]);
|
|
627
|
+
const stream = callSdk(mujian, query || '', is_continue ? 'continue' : regenerate ? 'regenerate' : 'generate', signal);
|
|
628
|
+
let buffer = '';
|
|
629
|
+
let content = '';
|
|
630
|
+
try {
|
|
631
|
+
for await (const chunk of stream){
|
|
632
|
+
buffer += chunk;
|
|
633
|
+
const lines = buffer.split('\n');
|
|
634
|
+
buffer = lines.pop() || '';
|
|
635
|
+
for (const line of lines)if (line.startsWith('data: ')) {
|
|
636
|
+
const data = line.slice(6);
|
|
637
|
+
try {
|
|
638
|
+
const parsedData = JSON.parse(data);
|
|
639
|
+
if (!regenerate && parsedData.question_id) {
|
|
640
|
+
const { question_id, reply_id } = parsedData;
|
|
641
|
+
setMessages((prev)=>{
|
|
642
|
+
const newMessages = [
|
|
643
|
+
...prev
|
|
644
|
+
];
|
|
645
|
+
if (question_id && !is_continue) newMessages[newMessages.length - 2].id = question_id;
|
|
646
|
+
const lastMessage = newMessages[newMessages.length - 1];
|
|
647
|
+
newMessages[newMessages.length - 1] = {
|
|
648
|
+
...lastMessage,
|
|
649
|
+
id: reply_id
|
|
650
|
+
};
|
|
651
|
+
return newMessages;
|
|
652
|
+
});
|
|
653
|
+
continue;
|
|
654
|
+
}
|
|
655
|
+
if (!parsedData.choices?.[0]) continue;
|
|
656
|
+
const partialContent = parsedData.choices[0].delta.content;
|
|
657
|
+
parsedData.choices[0].delta.reasoning;
|
|
658
|
+
content += partialContent;
|
|
659
|
+
if (content || '' === content) {
|
|
660
|
+
setMessages((prev)=>{
|
|
661
|
+
const newMessages = [
|
|
662
|
+
...prev
|
|
663
|
+
];
|
|
664
|
+
const lastMessage = newMessages[newMessages.length - 1];
|
|
665
|
+
lastMessage.swipes[lastMessage.activeSwipeId] = content;
|
|
666
|
+
newMessages[newMessages.length - 1] = {
|
|
667
|
+
...lastMessage,
|
|
668
|
+
content: content,
|
|
669
|
+
isStreaming: true
|
|
670
|
+
};
|
|
671
|
+
return newMessages;
|
|
672
|
+
});
|
|
673
|
+
await new Promise((resolve)=>setTimeout(resolve, 10));
|
|
674
|
+
} else {
|
|
675
|
+
console.error('data', data);
|
|
676
|
+
setError(new InvalidDeltaContentError(data));
|
|
677
|
+
}
|
|
678
|
+
} catch {
|
|
679
|
+
if ('[DONE]' !== data) {
|
|
680
|
+
console.error('Received plain SSE message:', data);
|
|
681
|
+
setError(new UnexpectedSseEndingError(data));
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
} finally{
|
|
687
|
+
console.log('stream end finally');
|
|
688
|
+
setMessages((prev)=>{
|
|
689
|
+
const newMessages = [
|
|
690
|
+
...prev
|
|
691
|
+
];
|
|
692
|
+
newMessages[newMessages.length - 1] = {
|
|
693
|
+
...newMessages[newMessages.length - 1],
|
|
694
|
+
isStreaming: false
|
|
695
|
+
};
|
|
696
|
+
return newMessages;
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
} catch (error) {
|
|
700
|
+
console.error('Error:', error);
|
|
701
|
+
error instanceof Error && JSON.parse(error.message || '{}')?.code === 111 ? setError(new InsufficientBalanceError()) : setError(new SendMessageError('Send message failed', error));
|
|
702
|
+
setMessages((prev)=>prev.slice(0, -1));
|
|
703
|
+
} finally{
|
|
704
|
+
setIsStreaming(false);
|
|
705
|
+
}
|
|
706
|
+
}, [
|
|
707
|
+
body,
|
|
708
|
+
mujian
|
|
709
|
+
]);
|
|
710
|
+
return {
|
|
711
|
+
chatStreaming,
|
|
712
|
+
isStreaming
|
|
713
|
+
};
|
|
714
|
+
};
|
|
715
|
+
const useChat = ({ body, onError, onFinish })=>{
|
|
716
|
+
const [error, _setError] = useState();
|
|
717
|
+
const [messages, setMessages] = useState([]);
|
|
718
|
+
const [input, setInput] = useState('');
|
|
719
|
+
const [abortController, setAbortController] = useState();
|
|
720
|
+
const mujian = useMujian();
|
|
721
|
+
const setError = (error)=>{
|
|
722
|
+
_setError(error);
|
|
723
|
+
if (error) onError?.(error);
|
|
724
|
+
};
|
|
725
|
+
const { chatStreaming, isStreaming } = useChatStreaming({
|
|
726
|
+
common: {
|
|
727
|
+
body
|
|
728
|
+
},
|
|
729
|
+
setError,
|
|
730
|
+
setMessages,
|
|
731
|
+
mujian
|
|
732
|
+
});
|
|
733
|
+
useMount(async ()=>{
|
|
734
|
+
const resp = await mujian.ai.chat.message.getAll();
|
|
735
|
+
setMessages(resp);
|
|
736
|
+
});
|
|
737
|
+
useUpdateEffect(()=>{
|
|
738
|
+
if (!isStreaming) {
|
|
739
|
+
const lastMessage = messages[messages.length - 1];
|
|
740
|
+
onFinish?.(lastMessage);
|
|
741
|
+
}
|
|
742
|
+
}, [
|
|
743
|
+
isStreaming
|
|
744
|
+
]);
|
|
745
|
+
const append = async (query)=>{
|
|
746
|
+
if (isStreaming) throw new Error('useChat is streaming already.');
|
|
747
|
+
const controller = new AbortController();
|
|
748
|
+
setAbortController(controller);
|
|
749
|
+
await chatStreaming({
|
|
750
|
+
query,
|
|
751
|
+
signal: controller.signal
|
|
752
|
+
});
|
|
753
|
+
setAbortController(void 0);
|
|
754
|
+
};
|
|
755
|
+
const regenerate = async ()=>{
|
|
756
|
+
if (isStreaming) throw new Error('useChat is streaming already.');
|
|
757
|
+
const controller = new AbortController();
|
|
758
|
+
setAbortController(controller);
|
|
759
|
+
const lastMessage = messages.findLast((message)=>'user' === message.role);
|
|
760
|
+
await chatStreaming({
|
|
761
|
+
query: lastMessage?.content || '',
|
|
762
|
+
regenerate: true,
|
|
763
|
+
signal: controller.signal
|
|
764
|
+
});
|
|
765
|
+
setAbortController(void 0);
|
|
766
|
+
};
|
|
767
|
+
const continueGenerate = async ()=>{
|
|
768
|
+
if (isStreaming) throw new Error('useChat is streaming already.');
|
|
769
|
+
const controller = new AbortController();
|
|
770
|
+
setAbortController(controller);
|
|
771
|
+
await chatStreaming({
|
|
772
|
+
query: '',
|
|
773
|
+
is_continue: true,
|
|
774
|
+
signal: controller.signal
|
|
775
|
+
});
|
|
776
|
+
setAbortController(void 0);
|
|
777
|
+
};
|
|
778
|
+
const stop = ()=>{
|
|
779
|
+
abortController?.abort();
|
|
780
|
+
};
|
|
781
|
+
const setSwipe = async (messageId, swipeId)=>{
|
|
782
|
+
await mujian.ai.chat.message.swipe(messageId, swipeId);
|
|
783
|
+
setMessages((prev)=>prev.map((msg)=>msg.id === messageId ? {
|
|
784
|
+
...msg,
|
|
785
|
+
activeSwipeId: swipeId
|
|
786
|
+
} : msg));
|
|
787
|
+
};
|
|
788
|
+
const editMessage = async (messageId, content)=>{
|
|
789
|
+
await mujian.ai.chat.message.editOne(messageId, content);
|
|
790
|
+
const patchMessage = (msg)=>{
|
|
791
|
+
const newMsg = {
|
|
792
|
+
...msg,
|
|
793
|
+
content
|
|
794
|
+
};
|
|
795
|
+
if ('assistant' === newMsg.role) {
|
|
796
|
+
newMsg.swipes = [
|
|
797
|
+
...newMsg.swipes
|
|
798
|
+
];
|
|
799
|
+
newMsg.swipes[newMsg.activeSwipeId] = content;
|
|
800
|
+
}
|
|
801
|
+
return newMsg;
|
|
802
|
+
};
|
|
803
|
+
setMessages((prev)=>prev.map((msg)=>msg.id === messageId ? patchMessage(msg) : msg));
|
|
804
|
+
};
|
|
805
|
+
const deleteMessage = async (messageId)=>{
|
|
806
|
+
await mujian.ai.chat.message.deleteOne(messageId);
|
|
807
|
+
setMessages((prev)=>prev.filter((msg)=>msg.id !== messageId));
|
|
808
|
+
};
|
|
809
|
+
return {
|
|
810
|
+
append,
|
|
811
|
+
regenerate,
|
|
812
|
+
continueGenerate,
|
|
813
|
+
stop,
|
|
814
|
+
messages: messages,
|
|
815
|
+
status: (()=>{
|
|
816
|
+
if (error) return 'error';
|
|
817
|
+
if (isStreaming) return 'streaming';
|
|
818
|
+
return 'ready';
|
|
819
|
+
})(),
|
|
820
|
+
error,
|
|
821
|
+
setMessages: setMessages,
|
|
822
|
+
input,
|
|
823
|
+
setInput,
|
|
824
|
+
handleSubmit: ()=>{
|
|
825
|
+
append(input);
|
|
826
|
+
},
|
|
827
|
+
setSwipe,
|
|
828
|
+
editMessage,
|
|
829
|
+
deleteMessage
|
|
830
|
+
};
|
|
831
|
+
};
|
|
832
|
+
export { MdRenderer, MujianProvider, Thread, useChat, useMujian };
|