@mujian/js-sdk 0.0.6-beta.3 → 0.0.6-beta.31

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/dist/index.js CHANGED
@@ -113,9 +113,14 @@ class MujianSdk {
113
113
  pendingRequests = new Map();
114
114
  async init() {
115
115
  const handshake = new postmate.Model({
116
- reply: ({ id, complete, data })=>{
116
+ reply: ({ id, complete, data, error })=>{
117
117
  const call = this.pendingRequests.get(id);
118
118
  if (!call) return;
119
+ if (error) {
120
+ call.reject(error);
121
+ this.pendingRequests.delete(id);
122
+ return;
123
+ }
119
124
  call.onData?.(data);
120
125
  if (complete) {
121
126
  call.onComplete?.();
@@ -10,7 +10,7 @@ export type { Message };
10
10
  */
11
11
  export type UseChatProps = {
12
12
  body?: object;
13
- onError?: (e: Error) => void;
13
+ onError?: (e: unknown) => void;
14
14
  onFinish?: (message: Message) => void;
15
15
  };
16
16
  /**
@@ -24,7 +24,7 @@ export type UseChatProps = {
24
24
  export type UseChatReturn = {
25
25
  messages: Message[];
26
26
  status: 'uninitialized' | 'ready' | 'streaming' | 'error';
27
- error?: Error;
27
+ error?: unknown;
28
28
  /** 提交用户输入,让AI回答 */
29
29
  append: (query: string) => Promise<void>;
30
30
  /** 重新生成最后一条回答 */
@@ -8,6 +8,7 @@ interface ChatStreamingProps {
8
8
  setMessages: Dispatch<SetStateAction<Message[]>>;
9
9
  mujian: MujianSdk;
10
10
  }
11
+ export declare const NOT_SAVED_MSG_ID_PREFIX = "not_saved";
11
12
  export declare const useChatStreaming: ({ common, setError, setMessages, mujian, }: ChatStreamingProps) => {
12
13
  chatStreaming: ({ query, regenerate, is_continue, signal, }: {
13
14
  /** 用户输入 */
@@ -1,6 +1,5 @@
1
- /** biome-ignore-all lint/security/noDangerouslySetInnerHtml: <explanation> */
2
- import React from 'react';
3
- import './MdTheme.css';
1
+ import React from "react";
2
+ import "./MdTheme.css";
4
3
  export type MdRendererProps = {
5
4
  /**
6
5
  * The markdown content to render
@@ -0,0 +1,5 @@
1
+ export declare function adjustIframeHeight(iframe: HTMLIFrameElement): void;
2
+ export declare function useHeightObserver(): {
3
+ observe: (iframe: HTMLIFrameElement) => void;
4
+ unobserve: (iframe: HTMLIFrameElement) => void;
5
+ };
@@ -0,0 +1,9 @@
1
+ export declare const unescapeHTML: (str: string) => string;
2
+ export declare const replaceVhInContent: (content: string) => string;
3
+ /**
4
+ * 转义 HTML 属性值中的引号,防止破坏 HTML 属性
5
+ * @param value 需要转义的属性值
6
+ * @returns 转义后的值
7
+ */
8
+ export declare function escapeHtmlAttribute(value: string): string;
9
+ export declare function createSrcContent(content: string): string;
@@ -0,0 +1,3 @@
1
+ export declare const iframeDOMLoaded = "\n(function () {\n function emit_loaded_event() {\n window.parent.postMessage({ type: 'MJ_DOM_CONTENT_LOADED', iframeId: '123' }, '*');\n }\n\n if (window.document.readyState === 'loading') {\n window.document.addEventListener('DOMContentLoaded', emit_loaded_event, { once: true });\n } else {\n emit_loaded_event();\n }\n})();\n";
2
+ export declare const init = "\n(async function () {\n window.$mj_engine = window.parent.$mj_engine;\n})();\n";
3
+ export declare const thirdParty = "\n<script src=\"https://cdn.jsdmirror.com/npm/@fortawesome/fontawesome-free/js/all.min.js\"></script>\n<script src=\"https://cdn.jsdmirror.com/npm/@tailwindcss/browser/dist/index.global.min.js\"></script>\n<script src=\"https://cdn.jsdmirror.com/npm/jquery/dist/jquery.min.js\"></script>\n<script src=\"https://cdn.jsdmirror.com/npm/jquery-ui/dist/jquery-ui.min.js\"></script>\n<link rel=\"stylesheet\" href=\"https://cdn.jsdmirror.com/npm/jquery-ui/themes/base/theme.min.css\" />\n<script src=\"https://cdn.jsdmirror.com/npm/jquery-ui-touch-punch\"></script>\n";
@@ -0,0 +1,7 @@
1
+ import type { CSSProperties } from 'react';
2
+ interface Props {
3
+ className?: string;
4
+ style: CSSProperties;
5
+ }
6
+ export declare const MujianSpinner: ({ className, style }: Props) => import("react/jsx-runtime").JSX.Element;
7
+ export {};
@@ -1,4 +1,5 @@
1
1
  export type { VListHandle } from 'virtua';
2
2
  export { MdRenderer } from './MdRenderer';
3
3
  export { MujianProvider, useMujian } from './MujianProvider';
4
+ export { MujianSpinner } from './MujianSpinner';
4
5
  export { Thread } from './Thread';
package/dist/react.css CHANGED
@@ -42,7 +42,7 @@
42
42
  --SmartThemeBodyColor: #abc6df;
43
43
  --SmartThemeEmColor: #fff;
44
44
  --SmartThemeUnderlineColor: #bce7cf;
45
- --SmartThemeQuoteColor: #6f85fd;
45
+ --SmartThemeQuoteColor: #b983ff;
46
46
  --SmartThemeBlurTintColor: #171717;
47
47
  --SmartThemeChatTintColor: #171717;
48
48
  --SmartThemeUserMesBlurTintColor: #0000004d;
@@ -89,76 +89,72 @@
89
89
  -moz-osx-font-smoothing: grayscale;
90
90
  }
91
91
 
92
- html {
93
- -webkit-backface-visibility: hidden;
94
- -webkit-perspective: 1000px;
95
- -webkit-transform: translateZ(0);
92
+ .spin-overlay {
93
+ background-color: #000;
94
+ animation: 3s ease-in-out infinite breath;
96
95
  }
97
96
 
98
- body {
99
- background-color: var(--SmartThemeBlurTintColor);
100
- width: 100%;
101
- height: 100dvh;
102
- font-family: var(--mainFontFamily);
103
- font-size: var(--mainFontSize);
104
- color: var(--SmartThemeBodyColor);
105
- color-scheme: light only;
106
- background-repeat: no-repeat;
107
- background-size: cover;
108
- background-attachment: fixed;
109
- margin: 0;
110
- padding: 0;
97
+ @keyframes breath {
98
+ 0% {
99
+ background-color: #0009;
100
+ }
101
+
102
+ 50% {
103
+ background-color: #00000080;
104
+ }
105
+
106
+ 100% {
107
+ background-color: #0009;
108
+ }
111
109
  }
112
110
 
113
- ::-webkit-scrollbar {
114
- scrollbar-gutter: stable;
115
- width: .7rem;
111
+ .wave-text span {
112
+ font-family: var(--mainFontFamily);
113
+ animation: 2s ease-in-out infinite wave;
114
+ display: inline-block;
116
115
  }
117
116
 
118
- ::-webkit-scrollbar-track {
119
- cursor: default;
117
+ .wave-text span:first-child {
118
+ animation-delay: 0s;
120
119
  }
121
120
 
122
- ::-webkit-scrollbar-track:hover {
123
- background-color: #7e7e7e33;
121
+ .wave-text span:nth-child(2) {
122
+ animation-delay: .2s;
124
123
  }
125
124
 
126
- ::-webkit-scrollbar-thumb {
127
- cursor: grab;
125
+ .wave-text span:nth-child(3) {
126
+ animation-delay: .4s;
128
127
  }
129
128
 
130
- ::-webkit-scrollbar-thumb:active {
131
- cursor: grabbing;
129
+ .wave-text span:nth-child(4) {
130
+ animation-delay: .6s;
132
131
  }
133
132
 
134
- .scrollY {
135
- overflow-y: auto !important;
133
+ .wave-text span:nth-child(5) {
134
+ animation-delay: .8s;
136
135
  }
137
136
 
138
- ::-webkit-scrollbar-thumb:vertical {
139
- background-color: var(--grey7070a);
140
- box-shadow: inset 0 0 0 1px var(--black50a);
141
- background-clip: content-box;
142
- border: 2px solid #0000;
143
- border-radius: 10px;
144
- min-height: 40px;
137
+ .wave-text span:nth-child(6) {
138
+ animation-delay: 1s;
145
139
  }
146
140
 
147
- body.movingUI ::-webkit-scrollbar-thumb:vertical {
148
- border-top: 20px solid #0000;
141
+ .wave-text span:nth-child(7) {
142
+ animation-delay: 1.2s;
149
143
  }
150
144
 
151
- ::-webkit-scrollbar-thumb:horizontal {
152
- background-color: var(--grey7070a);
153
- box-shadow: inset 0 0 0 1px var(--black50a);
154
- background-clip: content-box;
155
- border: 2px solid #0000;
156
- border-radius: 10px;
157
- min-width: 40px;
145
+ @keyframes wave {
146
+ 0%, 100% {
147
+ transform: translateY(0);
148
+ }
149
+
150
+ 50% {
151
+ transform: translateY(-8px);
152
+ }
158
153
  }
159
154
 
160
- ::-webkit-scrollbar-corner {
161
- background-color: #0000;
155
+ body {
156
+ font-size: var(--mainFontSize);
157
+ color: var(--SmartThemeBodyColor);
162
158
  }
163
159
 
164
160
  .mes_text table, .mes_reasoning table {
@@ -218,19 +214,11 @@ body.movingUI ::-webkit-scrollbar-thumb:vertical {
218
214
  }
219
215
 
220
216
  .mes_text, .mes_reasoning {
221
- font-weight: 500;
222
217
  line-height: calc(var(--mainFontSize) + .5rem);
223
218
  overflow-wrap: anywhere;
224
219
  max-width: 100%;
225
220
  }
226
221
 
227
- .mes_text {
228
- padding-top: 5px;
229
- padding-bottom: 5px;
230
- padding-left: 0;
231
- padding-right: var(--mes-right-spacing);
232
- }
233
-
234
222
  .mes_text p:only-child, .mes_text p:only-of-type {
235
223
  margin-bottom: 0;
236
224
  }
@@ -239,7 +227,7 @@ body.movingUI ::-webkit-scrollbar-thumb:vertical {
239
227
  unicode-bidi: isolate;
240
228
  margin-block: 1em;
241
229
  margin-inline: 0;
242
- padding-inline-start: 40px;
230
+ padding-inline-start: 24px;
243
231
  list-style-type: decimal;
244
232
  display: block;
245
233
  }
@@ -336,11 +324,9 @@ body.movingUI ::-webkit-scrollbar-thumb:vertical {
336
324
  code {
337
325
  font-family: var(--monoFontFamily);
338
326
  white-space: pre-wrap;
339
- border: 1px solid var(--SmartThemeBorderColor);
340
- background-color: var(--black70a);
341
327
  line-height: var(--mainFontSize);
342
328
  color: var(--white70a);
343
- border-radius: 5px;
329
+ border: 1px solid #fff;
344
330
  padding: 0 3px;
345
331
  }
346
332
 
package/dist/react.js CHANGED
@@ -1,9 +1,10 @@
1
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";
2
+ import react, { createContext, forwardRef, useCallback, useContext, useEffect, useRef, useState } from "react";
3
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
4
4
  import css_tools from "@adobe/css-tools";
5
5
  import dompurify from "dompurify";
6
6
  import showdown from "showdown";
7
+ import { v4 } from "uuid";
7
8
  import postmate from "postmate";
8
9
  import { Virtualizer } from "virtua";
9
10
  addDOMPurifyHooks();
@@ -95,7 +96,7 @@ function addDOMPurifyHooks() {
95
96
  * @returns {string} Encoded message text
96
97
  * @copyright https://github.com/kwaroran/risuAI
97
98
  */ function encodeStyleTags(text) {
98
- const styleRegex = /<style>(.+?)<\/style>/gi;
99
+ const styleRegex = /<style>([\s\S]+?)<\/style>/gi;
99
100
  return text.replaceAll(styleRegex, (_, match)=>`<custom-style>${encodeURIComponent(match)}</custom-style>`);
100
101
  }
101
102
  /**
@@ -234,18 +235,203 @@ function messageFormatting(mes, sanitizerOverrides = {}) {
234
235
  });
235
236
  return mes;
236
237
  }
238
+ const init = `
239
+ (async function () {
240
+ window.$mj_engine = window.parent.$mj_engine;
241
+ })();
242
+ `;
243
+ const thirdParty = `
244
+ <script src="https://cdn.jsdmirror.com/npm/@fortawesome/fontawesome-free/js/all.min.js"></script>
245
+ <script src="https://cdn.jsdmirror.com/npm/@tailwindcss/browser/dist/index.global.min.js"></script>
246
+ <script src="https://cdn.jsdmirror.com/npm/jquery/dist/jquery.min.js"></script>
247
+ <script src="https://cdn.jsdmirror.com/npm/jquery-ui/dist/jquery-ui.min.js"></script>
248
+ <link rel="stylesheet" href="https://cdn.jsdmirror.com/npm/jquery-ui/themes/base/theme.min.css" />
249
+ <script src="https://cdn.jsdmirror.com/npm/jquery-ui-touch-punch"></script>
250
+ `;
251
+ const unescapeHTML = (str)=>{
252
+ const named = {
253
+ amp: "&",
254
+ lt: "<",
255
+ gt: ">",
256
+ quot: '"',
257
+ apos: "'",
258
+ nbsp: "\u00A0"
259
+ };
260
+ return str.replace(/&(#x?[0-9a-fA-F]+|[a-zA-Z]+);/g, (_m, body)=>{
261
+ if ("#" === body[0]) {
262
+ const isHex = body[1]?.toLowerCase() === "x";
263
+ const numStr = isHex ? body.slice(2) : body.slice(1);
264
+ const codePoint = parseInt(numStr, isHex ? 16 : 10);
265
+ if (Number.isFinite(codePoint)) try {
266
+ return String.fromCodePoint(codePoint);
267
+ } catch {}
268
+ return _m;
269
+ }
270
+ const lower = body.toLowerCase();
271
+ return Object.hasOwn(named, lower) ? named[lower] : _m;
272
+ });
273
+ };
274
+ const replaceVhInContent = (content)=>{
275
+ const has_css_min_vh = /min-height\s*:\s*[^;{}]*\d+(?:\.\d+)?vh/gi.test(content);
276
+ const has_inline_style_vh = /style\s*=\s*(["'])[\s\S]*?min-height\s*:\s*[^;]*?\d+(?:\.\d+)?vh[\s\S]*?\1/gi.test(content);
277
+ const has_js_vh = /(\.style\.minHeight\s*=\s*(["']))([\s\S]*?vh)(\2)/gi.test(content) || /(setProperty\s*\(\s*(["'])min-height\2\s*,\s*(["']))([\s\S]*?vh)(\3\s*\))/gi.test(content);
278
+ if (!has_css_min_vh && !has_inline_style_vh && !has_js_vh) return content;
279
+ const convertVhToVariable = (value)=>value.replace(/(\d+(?:\.\d+)?)vh\b/gi, (match, value)=>{
280
+ const parsed = parseFloat(value);
281
+ if (!isFinite(parsed)) return match;
282
+ const VARIABLE_EXPRESSION = "var(--MJ-iframe-height)";
283
+ if (100 === parsed) return VARIABLE_EXPRESSION;
284
+ return `calc(${VARIABLE_EXPRESSION} * ${parsed / 100})`;
285
+ });
286
+ content = content.replace(/(min-height\s*:\s*)([^;{}]*?\d+(?:\.\d+)?vh)(?=\s*[;}])/gi, (_m, prefix, value)=>`${prefix}${convertVhToVariable(value)}`);
287
+ content = content.replace(/(style\s*=\s*(["']))([^"'"]*?)(\2)/gi, (match, prefix, _quote, styleContent, suffix)=>{
288
+ if (!/min-height\s*:\s*[^;]*vh/i.test(styleContent)) return match;
289
+ const replaced = styleContent.replace(/(min-height\s*:\s*)([^;]*?\d+(?:\.\d+)?vh)/gi, (_m, p1, p2)=>`${p1}${convertVhToVariable(p2)}`);
290
+ return `${prefix}${replaced}${suffix}`;
291
+ });
292
+ content = content.replace(/(\.style\.minHeight\s*=\s*(["']))([\s\S]*?)(\2)/gi, (match, prefix, _q, val, suffix)=>{
293
+ if (!/\b\d+(?:\.\d+)?vh\b/i.test(val)) return match;
294
+ const converted = convertVhToVariable(val);
295
+ return `${prefix}${converted}${suffix}`;
296
+ });
297
+ content = content.replace(/(setProperty\s*\(\s*(["'])min-height\2\s*,\s*(["']))([\s\S]*?)(\3\s*\))/gi, (match, prefix, _q1, _q2, val, suffix)=>{
298
+ if (!/\b\d+(?:\.\d+)?vh\b/i.test(val)) return match;
299
+ const converted = convertVhToVariable(val);
300
+ return `${prefix}${converted}${suffix}`;
301
+ });
302
+ return content;
303
+ };
304
+ function escapeHtmlAttribute(value) {
305
+ return value.replace(/"/g, "&quot;").replace(/'/g, "&#39;");
306
+ }
307
+ function createSrcContent(content) {
308
+ content = replaceVhInContent(content);
309
+ const getUserAvatarPath = ()=>"https://www.mujian.com/avatar.png";
310
+ const getCharAvatarPath = ()=>"https://www.mujian.com/avatar.png";
311
+ return `
312
+ <html>
313
+ <head>
314
+ <style>
315
+
316
+ html,body{margin:0;padding:0;overflow:hidden!important;max-width:100%!important;box-sizing:border-box}
317
+ .user_avatar,.user-avatar{background-image:url('${getUserAvatarPath()}')}
318
+ .char_avatar,.char-avatar{background-image:url('${getCharAvatarPath()}')}
319
+
320
+ </style>
321
+ ${thirdParty}
322
+ <script>
323
+ ${init}
324
+ </script>
325
+ </head>
326
+ <body>
327
+ ${content}
328
+ </body>
329
+ </html>
330
+ `;
331
+ }
332
+ function adjustIframeHeight(iframe) {
333
+ if (!iframe.contentWindow) return;
334
+ const document1 = iframe.contentWindow.document;
335
+ const height = Math.max(document1.body.offsetHeight, document1.documentElement.offsetHeight);
336
+ if (!Number.isFinite(height) || height <= 0) return;
337
+ iframe.style.height = `${height}px`;
338
+ }
339
+ const observed_elements = new Map();
340
+ const observer = new ResizeObserver((entries)=>{
341
+ for (const entry of entries){
342
+ const element = entry.target;
343
+ const iframe = observed_elements.get(element);
344
+ if (iframe) adjustIframeHeight(iframe);
345
+ }
346
+ });
347
+ function useHeightObserver() {
348
+ return {
349
+ observe: (iframe)=>{
350
+ if (!iframe?.contentWindow?.document?.body) return;
351
+ const body = iframe.contentWindow.document.body;
352
+ observed_elements.set(body, iframe);
353
+ observer.observe(body);
354
+ adjustIframeHeight(iframe);
355
+ },
356
+ unobserve: (iframe)=>{
357
+ if (!iframe.contentWindow?.document?.body) return;
358
+ const body = iframe.contentWindow.document.body;
359
+ observed_elements.delete(body);
360
+ observer.unobserve(body);
361
+ }
362
+ };
363
+ }
237
364
  const MdRendererBase = ({ content })=>{
238
- const mes = messageFormatting(content);
365
+ const containerRef = useRef(null);
366
+ const [iframeIdList, setIframeIdList] = useState([]);
367
+ const { observe, unobserve } = useHeightObserver();
368
+ useEffect(()=>{
369
+ let mes = messageFormatting(content);
370
+ mes = mes.replace(/<pre><code(.*)>[\s\S]*?<\/code><\/pre>/g, (match)=>{
371
+ if (!match.includes("&lt;body&gt;") && !match.includes("&lt;/body&gt;")) return match;
372
+ const code = match.replace(/<pre><code(.*?)>/g, "").replace(/<\/code><\/pre>/g, "");
373
+ const srcdoc = createSrcContent(unescapeHTML(code));
374
+ const id = v4();
375
+ setIframeIdList((prev)=>[
376
+ ...prev,
377
+ id
378
+ ]);
379
+ const escapedSrcdoc = escapeHtmlAttribute(srcdoc);
380
+ return `
381
+ <div class="MJ-iframe-container" id="MJ-iframe-container-${id}" style="display:flex;width:100%;height:100%;position:relative;">
382
+ <div class="spin-overlay" id="MJ-spin-${id}" style="width:100%;height:100%;position:absolute;top:0;left:0;z-index:1000;background-color:rgba(0,0,0,0.5);display:flex;justify-content:center;align-items:center;">
383
+ <div class="spin-inner wave-text" style="font-weight:500;font-size:16px;color:white;display:flex;justify-content:center;align-items:center;">
384
+ <span>加</span>
385
+ <span>载</span>
386
+ <span>中</span>
387
+ <span>.</span>
388
+ <span>.</span>
389
+ <span>.</span>
390
+ </div>
391
+ </div>
392
+ <iframe id="MJ-iframe-${id}" sandbox="allow-scripts allow-same-origin" loading="lazy" referrerpolicy="no-referrer" allowTransparency="true" style="color-scheme: none;background-color: transparent;width:100%;height:100%;border:none;" srcdoc="` + escapedSrcdoc + `"></iframe>
393
+ </div>
394
+ `;
395
+ });
396
+ if (containerRef.current) containerRef.current.innerHTML = mes;
397
+ }, [
398
+ content
399
+ ]);
400
+ useEffect(()=>{
401
+ iframeIdList.forEach((id)=>{
402
+ const iframe = document.getElementById("MJ-iframe-" + id);
403
+ if (!iframe) return;
404
+ iframe.addEventListener("load", ()=>{
405
+ observe(iframe);
406
+ const spinElement = iframe.parentElement?.querySelector("#MJ-spin-" + id);
407
+ if (spinElement) spinElement.remove();
408
+ });
409
+ });
410
+ return ()=>{
411
+ iframeIdList.forEach((id)=>{
412
+ const iframe = document.getElementById("MJ-iframe-" + id);
413
+ if (!iframe) return;
414
+ unobserve(iframe);
415
+ const spinElement = iframe.parentElement?.querySelector("#MJ-spin-" + iframe.id);
416
+ if (spinElement) spinElement.remove();
417
+ iframe.removeEventListener("load", ()=>{
418
+ console.log("iframe loaded", iframe.id);
419
+ });
420
+ });
421
+ };
422
+ }, [
423
+ iframeIdList,
424
+ observe,
425
+ unobserve
426
+ ]);
239
427
  return /*#__PURE__*/ jsx("div", {
240
428
  className: "mes_text",
241
- dangerouslySetInnerHTML: {
242
- __html: mes
243
- }
429
+ ref: containerRef
244
430
  });
245
431
  };
246
432
  const MdRenderer = /*#__PURE__*/ react.memo(MdRendererBase, (prev, next)=>prev.content === next.content);
247
- MdRendererBase.displayName = 'MdRenderer';
248
- MdRenderer.displayName = 'MdRenderer';
433
+ MdRendererBase.displayName = "MdRenderer";
434
+ MdRenderer.displayName = "MdRenderer";
249
435
  const chat_complete = async function(message, onData, signal) {
250
436
  return await this.call("mujian:ai:chat:complete", {
251
437
  content: message
@@ -341,9 +527,14 @@ class MujianSdk {
341
527
  pendingRequests = new Map();
342
528
  async init() {
343
529
  const handshake = new postmate.Model({
344
- reply: ({ id, complete, data })=>{
530
+ reply: ({ id, complete, data, error })=>{
345
531
  const call = this.pendingRequests.get(id);
346
532
  if (!call) return;
533
+ if (error) {
534
+ call.reject(error);
535
+ this.pendingRequests.delete(id);
536
+ return;
537
+ }
347
538
  call.onData?.(data);
348
539
  if (complete) {
349
540
  call.onComplete?.();
@@ -445,6 +636,43 @@ class MujianSdk {
445
636
  }
446
637
  };
447
638
  }
639
+ const styleSheet = `
640
+ @keyframes spin {
641
+ 0% { transform: rotate(0deg); }
642
+ 100% { transform: rotate(360deg); }
643
+ }
644
+ `;
645
+ const spinnerStyle = {
646
+ width: 48,
647
+ height: 48,
648
+ animation: 'spin 1s linear infinite'
649
+ };
650
+ const MujianSpinner = ({ className, style })=>/*#__PURE__*/ jsxs(Fragment, {
651
+ children: [
652
+ /*#__PURE__*/ jsx("style", {
653
+ children: styleSheet
654
+ }),
655
+ /*#__PURE__*/ jsx("svg", {
656
+ xmlns: "http://www.w3.org/2000/svg",
657
+ width: "24",
658
+ height: "24",
659
+ viewBox: "0 0 24 24",
660
+ fill: "none",
661
+ stroke: "currentColor",
662
+ strokeWidth: "2",
663
+ strokeLinecap: "round",
664
+ strokeLinejoin: "round",
665
+ className: className,
666
+ style: {
667
+ ...spinnerStyle,
668
+ ...style
669
+ },
670
+ children: /*#__PURE__*/ jsx("path", {
671
+ d: "M21 12a9 9 0 1 1-6.219-8.56"
672
+ })
673
+ })
674
+ ]
675
+ });
448
676
  const MujianContext = /*#__PURE__*/ createContext(null);
449
677
  const MujianProvider = ({ children, loadingComponent })=>{
450
678
  const [mujian, setMujian] = useState(null);
@@ -457,7 +685,20 @@ const MujianProvider = ({ children, loadingComponent })=>{
457
685
  }, []);
458
686
  return /*#__PURE__*/ jsx(MujianContext.Provider, {
459
687
  value: mujian,
460
- children: mujian ? children : loadingComponent ?? 'Mujian is loading'
688
+ children: mujian ? children : loadingComponent ?? /*#__PURE__*/ jsx("div", {
689
+ style: {
690
+ height: '100%',
691
+ width: '100%',
692
+ display: 'flex',
693
+ justifyContent: 'center',
694
+ alignItems: 'center'
695
+ },
696
+ children: /*#__PURE__*/ jsx(MujianSpinner, {
697
+ style: {
698
+ color: '#EC4342'
699
+ }
700
+ })
701
+ })
461
702
  });
462
703
  };
463
704
  const useMujian = ()=>{
@@ -574,6 +815,9 @@ async function* callSdk(mujian, content, type = 'generate', signal) {
574
815
  resolveWait = resolve;
575
816
  });
576
817
  }
818
+ const NOT_SAVED_MSG_ID_PREFIX = 'not_saved';
819
+ const FALLBACK_MESSAGE = `<!-- 此条HTML消息为系统提示,请勿复读 -->
820
+ 哎呀,AI线路好像抽风了,重说一下试试吧~`;
577
821
  const useChatStreaming = ({ common, setError, setMessages, mujian })=>{
578
822
  const { body } = common;
579
823
  const [isStreaming, setIsStreaming] = useState(false);
@@ -582,7 +826,7 @@ const useChatStreaming = ({ common, setError, setMessages, mujian })=>{
582
826
  setError(void 0);
583
827
  setIsStreaming(true);
584
828
  const userMessage = {
585
- id: Date.now().toString(),
829
+ id: `${NOT_SAVED_MSG_ID_PREFIX}_user_${Date.now()}`,
586
830
  content: query || '',
587
831
  role: 'user',
588
832
  swipes: [],
@@ -591,10 +835,12 @@ const useChatStreaming = ({ common, setError, setMessages, mujian })=>{
591
835
  sendAt: new Date()
592
836
  };
593
837
  const aiMessage = {
594
- id: Date.now().toString() + '1',
838
+ id: `${NOT_SAVED_MSG_ID_PREFIX}_assistant_${Date.now()}`,
595
839
  content: '',
596
840
  role: 'assistant',
597
- swipes: [],
841
+ swipes: [
842
+ ''
843
+ ],
598
844
  activeSwipeId: 0,
599
845
  isStreaming: true,
600
846
  sendAt: new Date()
@@ -636,7 +882,7 @@ const useChatStreaming = ({ common, setError, setMessages, mujian })=>{
636
882
  const data = line.slice(6);
637
883
  try {
638
884
  const parsedData = JSON.parse(data);
639
- if (!regenerate && parsedData.question_id) {
885
+ if (!regenerate && parsedData.reply_id) {
640
886
  const { question_id, reply_id } = parsedData;
641
887
  setMessages((prev)=>{
642
888
  const newMessages = [
@@ -683,16 +929,24 @@ const useChatStreaming = ({ common, setError, setMessages, mujian })=>{
683
929
  }
684
930
  }
685
931
  }
932
+ } catch (error) {
933
+ console.log('Stream error', error);
934
+ setError(error instanceof Error ? error : new SendMessageError(String(error)));
686
935
  } finally{
687
936
  console.log('stream end finally');
688
937
  setMessages((prev)=>{
689
938
  const newMessages = [
690
939
  ...prev
691
940
  ];
692
- newMessages[newMessages.length - 1] = {
941
+ const lastMessage = {
693
942
  ...newMessages[newMessages.length - 1],
694
943
  isStreaming: false
695
944
  };
945
+ if (!lastMessage.swipes[lastMessage.activeSwipeId]) {
946
+ lastMessage.swipes[lastMessage.activeSwipeId] = FALLBACK_MESSAGE;
947
+ lastMessage.content = FALLBACK_MESSAGE;
948
+ }
949
+ newMessages[newMessages.length - 1] = lastMessage;
696
950
  return newMessages;
697
951
  });
698
952
  }
@@ -742,39 +996,67 @@ const useChat = ({ body, onError, onFinish })=>{
742
996
  }, [
743
997
  isStreaming
744
998
  ]);
745
- const append = async (query)=>{
999
+ const append = useCallback(async (query)=>{
746
1000
  if (isStreaming) throw new Error('useChat is streaming already.');
747
1001
  const controller = new AbortController();
748
1002
  setAbortController(controller);
749
- await chatStreaming({
750
- query,
751
- signal: controller.signal
752
- });
753
- setAbortController(void 0);
754
- };
755
- const regenerate = async ()=>{
1003
+ try {
1004
+ await chatStreaming({
1005
+ query,
1006
+ signal: controller.signal
1007
+ });
1008
+ } catch (e) {
1009
+ setError(e);
1010
+ throw e;
1011
+ } finally{
1012
+ setAbortController(void 0);
1013
+ }
1014
+ }, [
1015
+ isStreaming,
1016
+ chatStreaming
1017
+ ]);
1018
+ const regenerate = useCallback(async ()=>{
756
1019
  if (isStreaming) throw new Error('useChat is streaming already.');
757
1020
  const controller = new AbortController();
758
1021
  setAbortController(controller);
759
1022
  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 ()=>{
1023
+ try {
1024
+ await chatStreaming({
1025
+ query: lastMessage?.content || '',
1026
+ regenerate: true,
1027
+ signal: controller.signal
1028
+ });
1029
+ } catch (e) {
1030
+ setError(e);
1031
+ throw e;
1032
+ } finally{
1033
+ setAbortController(void 0);
1034
+ }
1035
+ }, [
1036
+ isStreaming,
1037
+ messages,
1038
+ chatStreaming
1039
+ ]);
1040
+ const continueGenerate = useCallback(async ()=>{
768
1041
  if (isStreaming) throw new Error('useChat is streaming already.');
769
1042
  const controller = new AbortController();
770
1043
  setAbortController(controller);
771
- await chatStreaming({
772
- query: '',
773
- is_continue: true,
774
- signal: controller.signal
775
- });
776
- setAbortController(void 0);
777
- };
1044
+ try {
1045
+ await chatStreaming({
1046
+ query: '',
1047
+ is_continue: true,
1048
+ signal: controller.signal
1049
+ });
1050
+ } catch (e) {
1051
+ setError(e);
1052
+ throw e;
1053
+ } finally{
1054
+ setAbortController(void 0);
1055
+ }
1056
+ }, [
1057
+ isStreaming,
1058
+ chatStreaming
1059
+ ]);
778
1060
  const stop = ()=>{
779
1061
  abortController?.abort();
780
1062
  };
@@ -803,7 +1085,11 @@ const useChat = ({ body, onError, onFinish })=>{
803
1085
  setMessages((prev)=>prev.map((msg)=>msg.id === messageId ? patchMessage(msg) : msg));
804
1086
  };
805
1087
  const deleteMessage = async (messageId)=>{
806
- await mujian.ai.chat.message.deleteOne(messageId);
1088
+ if (!messageId.startsWith(NOT_SAVED_MSG_ID_PREFIX)) try {
1089
+ await mujian.ai.chat.message.deleteOne(messageId);
1090
+ } catch (e) {
1091
+ if (e?.code !== 2011030004) throw e;
1092
+ }
807
1093
  setMessages((prev)=>prev.filter((msg)=>msg.id !== messageId));
808
1094
  };
809
1095
  return {
@@ -829,4 +1115,4 @@ const useChat = ({ body, onError, onFinish })=>{
829
1115
  deleteMessage
830
1116
  };
831
1117
  };
832
- export { MdRenderer, MujianProvider, Thread, useChat, useMujian };
1118
+ export { MdRenderer, MujianProvider, MujianSpinner, Thread, useChat, useMujian };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mujian/js-sdk",
3
- "version": "0.0.6-beta.3",
3
+ "version": "0.0.6-beta.31",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -76,6 +76,7 @@
76
76
  },
77
77
  "dependencies": {
78
78
  "dompurify": "^3.2.6",
79
+ "uuid": "^13.0.0",
79
80
  "@adobe/css-tools": "^4.4.4",
80
81
  "showdown": "^2.1.0",
81
82
  "postmate": "^1.5.2",