ai-chat-embed 0.1.6 → 0.1.8

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 CHANGED
@@ -219,6 +219,95 @@ const chat = await AIChatEmbed.init({
219
219
  });
220
220
  ```
221
221
 
222
+ ### 插槽自定义
223
+
224
+ `ai-chat-embed` 支持将底层组件的插槽通过 `slots` 配置传入。可用插槽为 `more`、`welcome`、`chat-box-head` 和 `chat-box-footer`。插槽值可以是 HTML 字符串、DOM 节点,或接收 slot 参数并返回 HTML 字符串的函数。
225
+
226
+ #### 默认悬浮球模式(`ball`)
227
+
228
+ `mode` 默认值是 `"ball"`。悬浮球和展开后的聊天面板默认挂载到 `#ai-chat-embed-root`,因此可以在 `document` 上做事件委托,再通过容器 id 限定作用范围。
229
+
230
+ ```html
231
+ <script>
232
+ // 不能把 @click / v-on:click 写进 HTML 字符串,它们不会被 Vue 编译。
233
+ document.addEventListener("click", (event) => {
234
+ const target = event.target;
235
+ if (!(target instanceof Element)) return;
236
+
237
+ const actionElement = target.closest(
238
+ '#ai-chat-embed-root [data-chat-action]'
239
+ );
240
+ if (!actionElement) return;
241
+
242
+ if (actionElement.dataset.chatAction === "human") {
243
+ console.log("悬浮球模式:用户点击了转人工服务");
244
+ // 在这里调用你的业务逻辑,例如打开人工客服窗口。
245
+ }
246
+ });
247
+
248
+ const chat = await AIChatEmbed.init({
249
+ // mode 可省略,默认就是 "ball"
250
+ slots: {
251
+ "chat-box-footer": `
252
+ <button type="button" data-chat-action="human">
253
+ 转人工服务
254
+ </button>
255
+ `
256
+ }
257
+ });
258
+ </script>
259
+ ```
260
+
261
+ #### 内嵌面板模式(`panel`)
262
+
263
+ `panel` 模式下,插槽内容位于 `mountTo` 指定的容器内,可以直接在该容器上绑定事件。
264
+
265
+ ```html
266
+ <div id="chat-panel"></div>
267
+ <script>
268
+ const chatPanel = document.querySelector("#chat-panel");
269
+
270
+ chatPanel.addEventListener("click", (event) => {
271
+ const target = event.target;
272
+ if (!(target instanceof Element)) return;
273
+
274
+ const actionElement = target.closest("[data-chat-action]");
275
+ if (!actionElement) return;
276
+
277
+ if (actionElement.dataset.chatAction === "human") {
278
+ console.log("面板模式:用户点击了转人工服务");
279
+ // 在这里调用你的业务逻辑,例如打开人工客服窗口。
280
+ }
281
+ });
282
+
283
+ const chat = await AIChatEmbed.init({
284
+ mode: "panel",
285
+ mountTo: chatPanel,
286
+ slots: {
287
+ welcome: ({ title }) => `
288
+ <section class="my-welcome">
289
+ <strong>${title}</strong>
290
+ <p>欢迎咨询,请选择一个问题开始。</p>
291
+ </section>
292
+ `,
293
+ "chat-box-head": ({ uiHistory }) =>
294
+ `<div class="my-chat-head">已发送 ${uiHistory.length} 条消息</div>`,
295
+ "chat-box-footer": `
296
+ <button type="button" data-chat-action="human">
297
+ 转人工服务
298
+ </button>
299
+ `,
300
+ more: '<a href="/help">帮助中心</a>'
301
+ }
302
+ });
303
+ </script>
304
+ ```
305
+
306
+ 函数 slot 的参数由底层组件提供:`welcome` 包含 `title`、`welcomeConfig`、`presetTasks`、`onTaskClick`;`chat-box-head` 和 `chat-box-footer` 包含 `uiHistory`、`showWelcomeScreen`、`isStreaming`;`more` 的参数取决于底层版本。传入 `welcome` 后会覆盖默认欢迎内容。HTML 会被转换为 Vue VNode,建议只传入可信内容;需要绑定交互时,可按上面的方式在稳定的外层容器上进行事件委托。
307
+
308
+ > 具体支持哪些插槽和配置能力,请查看 [ai-suspended-ball-chat](https://www.npmjs.com/package/ai-suspended-ball-chat) 原生组件文档。
309
+
310
+
222
311
  ### `chat.update(options)`
223
312
 
224
313
  通过实例动态更新组件配置(如切换标题、接口地址等),无需重新挂载。
@@ -270,6 +359,7 @@ chat.destroy();
270
359
  | `onEmbedLoading` | function | - | 封装层资源加载状态回调:`(loading, meta)` |
271
360
  | `onEmbedReady` | function | - | 封装层初始化完成回调:`(meta)` |
272
361
  | `onEmbedError` | function | - | 封装层初始化失败回调:`(error, meta)` |
362
+ | `slots` | object | - | 插槽配置:支持 `more`、`welcome`、`chat-box-head`、`chat-box-footer`等,值可以是 HTML 字符串、DOM 节点或渲染函数 |
273
363
 
274
364
  > 其余所有配置(`url`、`appName`、`domainName`、`title`、`callbacks`、`enableStreaming` 等)会直接透传给底层 `SuspendedBallChat` 组件。完整配置参考原包文档:[ai-suspended-ball-chat](https://www.npmjs.com/package/ai-suspended-ball-chat)
275
365
 
@@ -1 +1 @@
1
- var AIChatEmbed=(()=>{var A=Object.defineProperty;var D=Object.getOwnPropertyDescriptor;var R=Object.getOwnPropertyNames;var $=Object.prototype.hasOwnProperty;var F=(e,t)=>{for(var o in t)A(e,o,{get:t[o],enumerable:!0})},T=(e,t,o,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let r of R(t))!$.call(e,r)&&r!==o&&A(e,r,{get:()=>t[r],enumerable:!(n=D(t,r))||n.enumerable});return e};var _=e=>T(A({},"__esModule",{value:!0}),e);var Q={};F(Q,{default:()=>J,init:()=>V});var v={vueUrl:"https://unpkg.com/vue@3/dist/vue.global.prod.js",lodashUrl:"https://unpkg.com/lodash@4.17.21/lodash.min.js",chatLibUrl:"https://unpkg.com/ai-suspended-ball-chat/dist/suspended-ball-chat.umd.js",mode:"ball",mountTo:"body",containerId:"ai-chat-embed-root"},u={},f=null,l=new Map,y=new Map;function h(e,...t){if(typeof e=="function")try{e(...t)}catch{}}function H(){if(typeof window>"u"||typeof document>"u")throw new Error("AIChatEmbed can only run in browser environments.")}function S(e,t){if(typeof t=="function"&&t())return Promise.resolve();if(l.has(e))return l.get(e);let o=document.querySelector(`script[data-ai-chat-embed-src="${e}"]`);if(o){let r=o.getAttribute("data-ai-chat-embed-loaded"),d=o.getAttribute("data-ai-chat-embed-error");if(r==="1"){let a=Promise.resolve();return l.set(e,a),a}if(d==="1")o.remove();else{let a=new Promise((E,c)=>{o.addEventListener("load",()=>E(),{once:!0}),o.addEventListener("error",()=>{o.setAttribute("data-ai-chat-embed-error","1"),c(new Error(`Failed loading script: ${e}`))},{once:!0})});return a.catch(()=>{l.delete(e)}),l.set(e,a),a}}let n=new Promise((r,d)=>{let a=document.createElement("script");a.src=e,a.async=!0,a.setAttribute("data-ai-chat-embed-src",e),a.onload=()=>{a.setAttribute("data-ai-chat-embed-loaded","1"),r()},a.onerror=()=>{a.setAttribute("data-ai-chat-embed-error","1"),d(new Error(`Failed loading script: ${e}`))},document.head.appendChild(a)});return n.catch(()=>{l.delete(e)}),l.set(e,n),n}function I(e){let t=[window.SuspendedBallChat,window.suspendedBallChat,window.AISuspendedBallChat,window.aiSuspendedBallChat,window.AiSuspendedBallChat,window.AI_SUSPENDED_BALL_CHAT,window["ai-suspended-ball-chat"]];for(let o of t)if(o&&o.SuspendedBallChat)return o;for(let o of e){let n=window[o];if(n&&typeof n=="object"&&n.SuspendedBallChat)return n}return null}function K(){window.Vue&&(typeof window.Vue.component!="function"&&(window.Vue.component=function(){return this}),typeof window.Vue.extend!="function"&&(window.Vue.extend=function(t){return function(n){return{...t,props:n}}}),typeof window.Vue.set!="function"&&(window.Vue.set=function(t,o,n){return t[o]=n,n}),typeof window.Vue.delete!="function"&&(window.Vue.delete=function(t,o){delete t[o]}))}async function q(e){return f||(f=(async()=>{await S(e.vueUrl,()=>!!(window.Vue&&window.Vue.createApp)),await S(e.lodashUrl,()=>!!(window._&&typeof window._.cloneDeep=="function")),K();let t=new Set(Object.keys(window));await S(e.chatLibUrl,()=>!!I([]));let o=Object.keys(window).filter(d=>!t.has(d)),n=window.Vue;if(!n||!n.createApp||!n.h||!n.reactive||!n.ref)throw new Error("Vue 3 runtime not found after loading script.");let r=I(o)||I([]);if(!r||!r.SuspendedBallChat)throw new Error("Cannot find SuspendedBallChat export from ai-suspended-ball-chat.");if(!r.ChatPanel)throw new Error("Cannot find ChatPanel export from ai-suspended-ball-chat.");return{vue:n,chatLib:r}})().catch(t=>{throw f=null,t}),f)}function N(e){if(!e||e==="body")return document.body;if(typeof e=="string"){let t=document.querySelector(e);if(!t)throw new Error(`mountTo selector not found: ${e}`);return t}if(e instanceof HTMLElement)return e;throw new Error("mountTo must be a CSS selector, HTMLElement, or 'body'.")}function z(e){let t=N(e.mountTo),o=e.containerId,n=document.getElementById(o),r=!1;return n?n.parentNode||t.appendChild(n):(n=document.createElement("div"),n.id=o,t.appendChild(n),r=!0),{container:n,createdContainer:r}}function B(e){let t={...e};return delete t.vueUrl,delete t.lodashUrl,delete t.chatLibUrl,delete t.mode,delete t.mountTo,delete t.containerId,delete t.onEmbedLoading,delete t.onEmbedReady,delete t.onEmbedError,t}async function V(e={}){H();let t=e.mode||v.mode;if(t!=="ball"&&t!=="panel")throw new Error(`Invalid mode "${t}". Use "ball" or "panel".`);let o=t==="panel"?"ai-chat-panel-root":v.containerId,n={...v,containerId:o,...e,mode:t},r=n.containerId,d={mode:t,containerId:n.containerId};if(u[r])return h(e.onEmbedReady,d),u[r].publicApi;if(y.has(r))return y.get(r);let a=(async()=>{let E=Date.now();h(e.onEmbedLoading,!0,d);try{let k=function(i,...s){let p=g.value;if(!p||typeof p[i]!="function")throw new Error(`Method "${i}" is unavailable before component ready.`);return p[i](...s)},{vue:c,chatLib:m}=await q(n),{container:w,createdContainer:P}=z(n),C=c.reactive(B(e)),g=c.ref(null),x=t==="panel"?m.ChatPanel:m.SuspendedBallChat,b=c.createApp({name:"AIChatEmbedRoot",render(){return c.h(x,{...C,ref:g})}});typeof m.install=="function"&&b.use(m),b.mount(w);let M={update(i={}){if(!u[r])throw new Error("Instance already destroyed.");Object.assign(C,B(i))},destroy(){u[r]&&(b.unmount(),P&&w.parentNode&&w.parentNode.removeChild(w),delete u[r],Object.keys(u).length===0&&(f=null,l.clear()))},invoke(i,...s){return k(i,...s)}},L=new Proxy(M,{get(i,s,p){if(Reflect.has(i,s))return Reflect.get(i,s,p);if(typeof s=="string"&&!["then","catch","finally"].includes(s))return(...j)=>i.invoke(s,...j)}});return u[r]={app:b,componentProps:C,componentRef:g,container:w,createdContainer:P,publicApi:L},h(e.onEmbedReady,{...d,elapsedMs:Date.now()-E}),L}catch(c){throw h(e.onEmbedError,c,d),c}finally{h(e.onEmbedLoading,!1,d),y.delete(r)}})();return y.set(r,a),a}var G={init:V},U=G;typeof window<"u"&&(window.AIChatEmbed=U);var J=U;return _(Q);})();
1
+ var AIChatEmbed=(()=>{var A=Object.defineProperty;var _=Object.getOwnPropertyDescriptor;var R=Object.getOwnPropertyNames;var $=Object.prototype.hasOwnProperty;var F=(e,t)=>{for(var o in t)A(e,o,{get:t[o],enumerable:!0})},H=(e,t,o,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let r of R(t))!$.call(e,r)&&r!==o&&A(e,r,{get:()=>t[r],enumerable:!(n=_(t,r))||n.enumerable});return e};var K=e=>H(A({},"__esModule",{value:!0}),e);var Y={};F(Y,{default:()=>W,init:()=>M});var S={vueUrl:"https://unpkg.com/vue@3/dist/vue.global.prod.js",lodashUrl:"https://unpkg.com/lodash@4.17.21/lodash.min.js",chatLibUrl:"https://unpkg.com/ai-suspended-ball-chat/dist/suspended-ball-chat.umd.js",mode:"ball",mountTo:"body",containerId:"ai-chat-embed-root"},u={},f=null,l=new Map,E=new Map;function h(e,...t){if(typeof e=="function")try{e(...t)}catch{}}function q(){if(typeof window>"u"||typeof document>"u")throw new Error("AIChatEmbed can only run in browser environments.")}function I(e,t){if(typeof t=="function"&&t())return Promise.resolve();if(l.has(e))return l.get(e);let o=document.querySelector(`script[data-ai-chat-embed-src="${e}"]`);if(o){let r=o.getAttribute("data-ai-chat-embed-loaded"),a=o.getAttribute("data-ai-chat-embed-error");if(r==="1"){let d=Promise.resolve();return l.set(e,d),d}if(a==="1")o.remove();else{let d=new Promise((p,i)=>{o.addEventListener("load",()=>p(),{once:!0}),o.addEventListener("error",()=>{o.setAttribute("data-ai-chat-embed-error","1"),i(new Error(`Failed loading script: ${e}`))},{once:!0})});return d.catch(()=>{l.delete(e)}),l.set(e,d),d}}let n=new Promise((r,a)=>{let d=document.createElement("script");d.src=e,d.async=!0,d.setAttribute("data-ai-chat-embed-src",e),d.onload=()=>{d.setAttribute("data-ai-chat-embed-loaded","1"),r()},d.onerror=()=>{d.setAttribute("data-ai-chat-embed-error","1"),a(new Error(`Failed loading script: ${e}`))},document.head.appendChild(d)});return n.catch(()=>{l.delete(e)}),l.set(e,n),n}function L(e){let t=[window.SuspendedBallChat,window.suspendedBallChat,window.AISuspendedBallChat,window.aiSuspendedBallChat,window.AiSuspendedBallChat,window.AI_SUSPENDED_BALL_CHAT,window["ai-suspended-ball-chat"]];for(let o of t)if(o&&o.SuspendedBallChat)return o;for(let o of e){let n=window[o];if(n&&typeof n=="object"&&n.SuspendedBallChat)return n}return null}function X(){window.Vue&&(typeof window.Vue.component!="function"&&(window.Vue.component=function(){return this}),typeof window.Vue.extend!="function"&&(window.Vue.extend=function(t){return function(n){return{...t,props:n}}}),typeof window.Vue.set!="function"&&(window.Vue.set=function(t,o,n){return t[o]=n,n}),typeof window.Vue.delete!="function"&&(window.Vue.delete=function(t,o){delete t[o]}))}async function z(e){return f||(f=(async()=>{await I(e.vueUrl,()=>!!(window.Vue&&window.Vue.createApp)),await I(e.lodashUrl,()=>!!(window._&&typeof window._.cloneDeep=="function")),X();let t=new Set(Object.keys(window));await I(e.chatLibUrl,()=>!!L([]));let o=Object.keys(window).filter(a=>!t.has(a)),n=window.Vue;if(!n||!n.createApp||!n.h||!n.reactive||!n.ref)throw new Error("Vue 3 runtime not found after loading script.");let r=L(o)||L([]);if(!r||!r.SuspendedBallChat)throw new Error("Cannot find SuspendedBallChat export from ai-suspended-ball-chat.");if(!r.ChatPanel)throw new Error("Cannot find ChatPanel export from ai-suspended-ball-chat.");return{vue:n,chatLib:r}})().catch(t=>{throw f=null,t}),f)}function G(e){if(!e||e==="body")return document.body;if(typeof e=="string"){let t=document.querySelector(e);if(!t)throw new Error(`mountTo selector not found: ${e}`);return t}if(e instanceof HTMLElement)return e;throw new Error("mountTo must be a CSS selector, HTMLElement, or 'body'.")}function J(e){let t=G(e.mountTo),o=e.containerId,n=document.getElementById(o),r=!1;return n?n.parentNode||t.appendChild(n):(n=document.createElement("div"),n.id=o,t.appendChild(n),r=!0),{container:n,createdContainer:r}}function N(e,t){let o=document.createElement("template");o.innerHTML=t;let n=r=>{if(r.nodeType===Node.TEXT_NODE)return e.h(e.Text,null,r.textContent||"");if(r.nodeType!==Node.ELEMENT_NODE)return null;let a=r,d={};for(let i of Array.from(a.attributes))d[i.name]=i.value;let p=Array.from(a.childNodes).map(n).filter(Boolean);return e.h(a.tagName.toLowerCase(),d,p)};return Array.from(o.content.childNodes).map(n).filter(Boolean)}function O(e,t){return(o={})=>{let n=typeof t=="function"?t(o):t;return n==null||n===!1?[]:typeof n=="object"&&!n.nodeType&&!Array.isArray(n)?n:(Array.isArray(n)?n:[n]).flatMap(a=>a==null||a===!1?[]:typeof a=="string"?N(e,a):a instanceof Node?N(e,a.nodeType===Node.ELEMENT_NODE?a.outerHTML:a.textContent||""):[])}}function T(e,t={}){return!t||typeof t!="object"?{}:Object.fromEntries(Object.entries(t).filter(([,o])=>typeof o=="function"||typeof o=="string"||o instanceof Node).map(([o,n])=>[o,O(e,n)]))}function V(e){let t={...e};return delete t.vueUrl,delete t.lodashUrl,delete t.chatLibUrl,delete t.mode,delete t.mountTo,delete t.containerId,delete t.onEmbedLoading,delete t.onEmbedReady,delete t.onEmbedError,delete t.slots,t}async function M(e={}){q();let t=e.mode||S.mode;if(t!=="ball"&&t!=="panel")throw new Error(`Invalid mode "${t}". Use "ball" or "panel".`);let o=t==="panel"?"ai-chat-panel-root":S.containerId,n={...S,containerId:o,...e,mode:t},r=n.containerId,a={mode:t,containerId:n.containerId};if(u[r])return h(e.onEmbedReady,a),u[r].publicApi;if(E.has(r))return E.get(r);let d=(async()=>{let p=Date.now();h(e.onEmbedLoading,!0,a);try{let x=function(c,...s){let w=g.value;if(!w||typeof w[c]!="function")throw new Error(`Method "${c}" is unavailable before component ready.`);return w[c](...s)},{vue:i,chatLib:b}=await z(n),{container:m,createdContainer:v}=J(n),C=i.reactive(V(e)),B=i.reactive(T(i,e.slots)),g=i.ref(null),U=t==="panel"?b.ChatPanel:b.SuspendedBallChat,y=i.createApp({name:"AIChatEmbedRoot",render(){return i.h(U,{...C,ref:g},B)}});typeof b.install=="function"&&y.use(b),y.mount(m);let D={update(c={}){if(!u[r])throw new Error("Instance already destroyed.");Object.assign(C,V(c)),c.slots&&Object.assign(B,T(i,c.slots))},destroy(){u[r]&&(y.unmount(),v&&m.parentNode&&m.parentNode.removeChild(m),delete u[r],Object.keys(u).length===0&&(f=null,l.clear()))},invoke(c,...s){return x(c,...s)}},P=new Proxy(D,{get(c,s,w){if(Reflect.has(c,s))return Reflect.get(c,s,w);if(typeof s=="string"&&!["then","catch","finally"].includes(s))return(...k)=>c.invoke(s,...k)}});return u[r]={app:y,componentProps:C,componentRef:g,container:m,createdContainer:v,publicApi:P},h(e.onEmbedReady,{...a,elapsedMs:Date.now()-p}),P}catch(i){throw h(e.onEmbedError,i,a),i}finally{h(e.onEmbedLoading,!1,a),E.delete(r)}})();return E.set(r,d),d}var Q={init:M},j=Q;typeof window<"u"&&(window.AIChatEmbed=j);var W=j;return K(Y);})();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-chat-embed",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "private": false,
5
5
  "description": "Framework-agnostic embed wrapper for ai-suspended-ball-chat",
6
6
  "main": "dist/ai-chat-embed.min.js",