@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.
Files changed (37) hide show
  1. package/README.md +23 -0
  2. package/dist/events/index.d.ts +71 -0
  3. package/dist/index.d.ts +96 -0
  4. package/dist/index.js +220 -0
  5. package/dist/modules/ai/chat/chat.d.ts +35 -0
  6. package/dist/modules/ai/chat/index.d.ts +4 -0
  7. package/dist/modules/ai/chat/message/index.d.ts +43 -0
  8. package/dist/modules/ai/chat/project/index.d.ts +3 -0
  9. package/dist/modules/ai/chat/settings/index.d.ts +3 -0
  10. package/dist/modules/ai/chat/settings/model.d.ts +24 -0
  11. package/dist/modules/ai/chat/settings/persona.d.ts +17 -0
  12. package/dist/modules/ai/chat/settings/style.d.ts +0 -0
  13. package/dist/modules/ai/index.d.ts +5 -0
  14. package/dist/modules/ai/text/index.d.ts +2 -0
  15. package/dist/modules/game/index.d.ts +4 -0
  16. package/dist/modules/game/saves.d.ts +3 -0
  17. package/dist/modules/ui/index.d.ts +0 -0
  18. package/dist/react/chat/index.d.ts +1 -0
  19. package/dist/react/chat/useChat/error.d.ts +15 -0
  20. package/dist/react/chat/useChat/index.d.ts +55 -0
  21. package/dist/react/chat/useChat/inner/chatStreaming.d.ts +23 -0
  22. package/dist/react/chat/useChat/inner/stream.d.ts +3 -0
  23. package/dist/react/chat/useChat/message.d.ts +37 -0
  24. package/dist/react/components/MdRenderer/index.d.ts +20 -0
  25. package/dist/react/components/MdRenderer/utils.d.ts +38 -0
  26. package/dist/react/components/MujianProvider/index.d.ts +17 -0
  27. package/dist/react/components/Thread/MessageItem.d.ts +21 -0
  28. package/dist/react/components/Thread/MessageList.d.ts +19 -0
  29. package/dist/react/components/Thread/index.d.ts +4 -0
  30. package/dist/react/components/button.d.ts +14 -0
  31. package/dist/react/components/index.d.ts +4 -0
  32. package/dist/react/index.d.ts +2 -0
  33. package/dist/react.css +156 -0
  34. package/dist/react.js +832 -0
  35. package/dist/types/index.d.ts +27 -0
  36. package/dist/utils/index.d.ts +0 -0
  37. 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(/&amp;/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 };